这是四年多来在博客园的第二篇博客。有了上一次的排版使用经验,这一篇文章应该有些进步(^_^)。

闲话按下不表,言归正传。在编译、成功运行了上一个helloWorld.cpp(后文中'hw'简称helloWorld程序)以后,我们需要回过头来看看这一小段程序是怎么运作的。 本文对hw做了比较深入的分析,并对以下的问题进行了讨论:(1)main 函数哪里去了;(2) wxApp以及wxFrame各是何方神圣; (3)构建一个完整的wxWidgets应用需要那些元素


1. 重组hw

为了看透这几十行代码究竟做了什么,作者将这段程序五马分尸重组到了5个独立的文件中去了。这一过程遵循了两个原则:(1) 声明与实现分开; (2) 一个头文件中只定义一个类。

  • hwApp.h   ---- 声明类MyApp
  • hwApp.cpp  ---- 实现类MyApp
  • hwFrame.h ---- 声明类 MyFrame
  • hwFrame.cpp ---- 实现类 MyFrame
  • helloWorld.cpp ---- 函数流程控制

这些源代码分别上传如下:

 /****************************************************************
Hello world expample, copied from www.wxwidgets.org
For study purpose only.
----Boyue.Zhang
28-Jun-2013
****************************************************************/ #if !defined(__HW_APP_H__)
#define __HW_APP_H__ #include <wx/wx.h> class MyApp: public wxApp
{
public:
virtual bool OnInit();
}; #endif

1)hwApp.h

 /****************************************************************
Hello world expample, copied from www.wxwidgets.org
For study purpose only.
----Boyue.Zhang
28-Jun-2013
****************************************************************/ #include "hwApp.h"
#include "hwFrame.h" bool MyApp::OnInit()
{
MyFrame *frame = new MyFrame("Hello World", wxPoint( , ), wxSize( , ) );
frame->Show( true );
return true;
}

2) hwApp.cpp

 /****************************************************************
Hello world expample, copied from www.wxwidgets.org
For study purpose only.
----Boyue.Zhang
28-Jun-2013
****************************************************************/ #if !defined( __HW_FRAME_H_)
#define __HW_FRAME_H_ #include <wx/wx.h> enum
{
ID_Hello =
}; class MyFrame: public wxFrame
{
public:
MyFrame( const wxString& title, const wxPoint& pos,
const wxSize& size );
private:
void OnHello( wxCommandEvent& event );
void OnExit( wxCommandEvent& event );
void OnAbout( wxCommandEvent& event ); DECLARE_EVENT_TABLE( );
}; #endif

3) hwFrame.h

 /****************************************************************
Hello world expample, copied from www.wxwidgets.org
For study purpose only.
----Boyue.Zhang
28-Jun-2013
****************************************************************/ #include "hwFrame.h" MyFrame::MyFrame( const wxString& title, const wxPoint& pos, const wxSize& size)
: wxFrame( NULL, wxID_ANY, title, pos, size )
{
wxMenu *menuFile = new wxMenu;
menuFile->Append( ID_Hello, "&Hello...\tCtrl-H",
"Help string shown in status bar for this menu item");
menuFile->AppendSeparator( );
menuFile->Append( wxID_EXIT ); wxMenu *menuHelp = new wxMenu;
menuHelp->Append( wxID_ABOUT); wxMenuBar *menuBar = new wxMenuBar;
menuBar->Append( menuFile, "&File" );
menuBar->Append( menuHelp, "&Help" ); SetMenuBar( menuBar ); CreateStatusBar( );
SetStatusText( "Welcome to wxWidgets!" ); } void MyFrame::OnExit( wxCommandEvent & event )
{
close( true );
} void MyFrame::OnAbout( wxCommandEvent& event )
{
wxMessageBox(" Ths is a wxWidgets' Hello world sample",
"About Hello World", wxOK|wxICON_INFORMATION );
} void MyFrame::OnHello( wxCommandEvent& event )
{
wxLogMessage( "Hello world from wxWidgets!" );
} BEGIN_EVENT_TABLE( MyFrame, wxFrame )
EVT_MENU( ID_Hello, MyFrame::OnHello )
EVT_MENU( wxID_EXIT, MyFrame::OnExit )
EVT_MENU( wxID_ABOUT, MyFrame::OnAbout )
END_EVENT_TABLE()

4) hwFrame.cpp

5). helloWorld.cpp

上篇文章,我说这个程序不太适合作为hello world示例,现在我发现错怪原作者了。进行了这一系列重组变换以后,发现hw示例的原型是这个样子的。何止是简洁啊,简直是见鬼。一个宏把一切东西搞定了。main函数、消息循环什么的统统不见鸟。

 /****************************************************************
Hello world expample, copied from www.wxwidgets.org
For study purpose only.
----Boyue.Zhang
28-Jun-2013
****************************************************************/ #include <wx/wx.h>
#include "hwApp.h"
#include "hwFrame.h" IMPLEMENT_APP( MyApp )

附带说明一下,分开以后的程序编译的时候指令有点长,使用Makefile是一个不错的选择。示例如下清单。

 default:
g++ hwApp.cpp hwFrame.cpp helloWorld.cpp `wx-config --cxxflags --libs` -o helloWorld
clean:
rm -f ./helloWorld

Make file for HW demo

2.  探究main函数

一致、良好的编程风格对于代码的可阅读性的作用在这里可以得以体现。从名称上看,程序的主体部分实现了两个功能。IMPLEMENT_APP()宏应该包含了应用的实现;而BEGIN_EVENT_TABLE()和END_EVENT_TABLE()之间的部分则定义了从事件到事件处理函数之间的映射。因此有必要看一看IMPLEMENT_EVENT_TABLE到底包装了什么内容。实际上这个宏隐藏得不是很深,在".../include/wx/app.h"中可以找的到。该宏的结构示意图如下所示,图中的箭头含有“depend on"的意义,箭头旁边的编号则代表了在定义中出现的先后次序 (例如,图中前两行表明了IMPLEMNET_APP由另外两个宏定义而来,依次是:IMPLEMENT_APP_NO_THEMES 和 IMPLEMENT_WX_THEME_SUPPORT)。

此外,这个图有可能因为屏幕分辨率的问题而现实不玩全。如果发生这样的情况请点击这里看完整的图

图中标记深绿色的节点显示了 IMPLEMENT_APP 的实现。分支1.1部分做了以下的事情:

  • 声明了一个创建app类的函数 wxCreateApp,注意appname通过宏参数层层传递到了这里;
  • 定义了一个全局的辅助类wxTheAppInitializer的实例,用于初始化app类;看到这里应该有以下的联想(当然这是猜测,目前为止尚未从源码查证)
    • a) 由于我们重载了wxApp的OnInit()函数,所以不出意外 wxTheAppInitializer 中应该调用wxCreateApp创建了App类以后,显示地调用了OnInit()函数
    • b) 从命名上看,wxAppInitializer可能用了单件模式(singleton)
  • 声明并且定义了获取App实例的接口: wxGetApp(), 该函数以引用的形式返回appname类(在hw中appname被替换为MyApp)的实例。

分支1.2则定义了main 函数。main函数只是一个壳子(wrapper),它将所流程控制权交给了wxEntry( )。这暗示了对于wxWidgets这个工程而言,实质上的等效main函数是wxEntry( )。笔者认为这样做在逻辑上使得wxWidgets项目有一个清晰的边界,不太可能出现一些看起来像 “glu logic"的代码,逻辑上站在一般c/c++的位置、实践上却在做wxWidgets 的事情。我们有必要再潜入到源码里看看wxEntry( )做了什么事情。这一次的代码不是在头文件里面找到的,所以如果是从包管理程序安装的同学有可能找不到相应的源码。wxEntry( )其实也是一个壳子,通过层层的剥壳最终在 common/init.cpp 中发现了它的真迹——wxEntryReal( int& argc, wxChar **argv )。 在这个等效的main( )函数里面,我们发现了以下的勾当:

  • wxWidgets库在这里被初始化
  • 调用用户定义的App类中的OnInit,以用户定制的方式对App类进行初始化
  • 如果用户定制了App的初始化方式,那么在程序退出的时候调用户用定义的退出方式。因为初始化的过程往往伴随着资源申请,这里提供一个机会让用户对自己申请到的资源负责到底——释放它们
  • 启动事件循环,即调用App的OnRun( )函数

鉴于wxEntryReal()做的这几件事情对于一个基于GUI的应用程序而言都是重量级的,现将这段源码粘贴如下以便加深读者的印象。到此为止,对main函数的挖掘可以告一段落。在这个搜寻main函数的过程中,我们也意外(对于熟知GUI架构的同学或或许十一点也不意外)地发现了关于App类的两点线索:a) App类中OnInit()以及OnExit()看起来是一个理想的申请/释放资源的地方; b) App类负责了wxWidgets应用中的时间循环

除此之外,笔者觉得源码中26--30行这个依靠destructor来调用wxTheApp->OnExit( )的trick怪有意思。如果是一般的实现,可以在wxTry这个代码块的结束部分显示地调用 wxTheApp->OnExit( )。但是这里有充分的必要这么做,否则程序就提供了一个不太容易察觉的资源泄漏的渠道:如果wxTheApp->OnInit( )申请到了部分而不是全部的资源并且返回一个非零的数值,那么wxTheApp->OnExit就没有机会被执行。 这样申请到的资源在过期以后就堂而皇之地逍遥法外。

 int wxEntryReal(int& argc, wxChar **argv)
{
// library initialization
wxInitializer initializer(argc, argv); if ( !initializer.IsOk() )
{
#if wxUSE_LOG
// flush any log messages explaining why we failed
delete wxLog::SetActiveTarget(NULL);
#endif
return -;
} wxTRY
{ // app initialization
if ( !wxTheApp->CallOnInit() )
{
// don't call OnExit() if OnInit() failed
return -;
} // ensure that OnExit() is called if OnInit() had succeeded
class CallOnExit
{
public:
~CallOnExit() { wxTheApp->OnExit(); }
} callOnExit; WX_SUPPRESS_UNUSED_WARN(callOnExit); // app execution
return wxTheApp->OnRun();
}
wxCATCH_ALL( wxTheApp->OnUnhandledException(); return -; )
}

 3.  wxApp 类和事件循环

如上图所示,wxApp派生于wxAppBase。OnRun()在wxAppBase中实现。这一步分源代码可以在 ".../src/common/appcmn.cpp" 以及 “.../include/wx/app.h" 中找得到。实际上 wxAppBase::OnRun( ) 依然是一个壳子(wrapper),实际的执行者是wxAppBase::MainLoop( )。 尽管wxAppBase::MainLoop( )的实现只有三行,但是笔者依然想把它贴出来,因为这里又看到一个有趣的事情。

那就是这里的这个 wxEventLoopTiedPtr 是何物什? 从逻辑功能上看,这里需要新分配一个wxEventLoop型的变量然后将其地址赋给成员m_mainLoop。可是总觉的这里写得怪怪的,而且最拽的是grep了整个源码居然没有找到这个 wxEventLoopTiedPtr 型别在哪定义的(代码写到这个份上,窃以为architect是值得拜一拜了)。好在我不是第一个遇到这个问题的人,看这里。现将关于 wxEventLoopTiedPtr 的定义复制于下以便读者观看,为不影响整体阅读已将默认设为折叠。简单地说,宏wxDEFINE_TIED_SCOPED_PTR_TYPE接受一个参数,展开以后形成了两个类定义。对本例而言,宏参数是"wxEventLoop",展开以后形成的两个类分别是“wxEventLoopPtr"以及“wxEventLoopTiedPtr"后者派生于前者。关于这里展示出来的scope pointer笔者认为可以在相对广泛的范围内应用,计划以后在做深入探讨,在此暂且不做深究。可以肯定的是,在这里wxEventLoopTiedPtr的作用是,将m_mainLoop的值保存,然后将新的值(即new wxEventLoop的返回地址)赋给m_mainLoop。出栈变量mainLoop作用域的时候,m_mainLoop原来的值将被恢复。

 // this defines wxEventLoopPtr
wxDEFINE_TIED_SCOPED_PTR_TYPE(wxEventLoop) //--------------------------------------------------------------------------------------------------------------------------
#define wxDEFINE_TIED_SCOPED_PTR_TYPE(T) \
wxDEFINE_SCOPED_PTR_TYPE(T) \
class T ## TiedPtr : public T ## Ptr \
{ \
public: \
T ## TiedPtr(T **pp, T *p) \
: T ## Ptr(p), m_pp(pp) \
{ \
m_pOld = *pp; \
*pp = p; \
} \
\
~ T ## TiedPtr() \
{ \
*m_pp = m_pOld; \
} \
\
private: \
T **m_pp; \
T *m_pOld; \
}; //------------------------------------------------------------------------------------------------------------------------------
// this macro can be used for the most common case when you want to declare and
// define the scoped pointer at the same time and want to use the standard
// naming convention: auto pointer to Foo is called FooPtr
#define wxDEFINE_SCOPED_PTR_TYPE(T) \
wxDECLARE_SCOPED_PTR(T, T ## Ptr) \
wxDEFINE_SCOPED_PTR(T, T ## Ptr) //------------------------------------------------------------------------------------------------------------------------------
#define wxDECLARE_SCOPED_PTR(T, name) \
class name \
{ \
private: \
T * m_ptr; \
\
name(name const &); \
name & operator=(name const &); \
\
public: \
wxEXPLICIT name(T * ptr = NULL) \
: m_ptr(ptr) { } \
\
~name(); \
\
void reset(T * ptr = NULL) \
{ \
if (m_ptr != ptr) \
{ \
delete m_ptr; \
m_ptr = ptr; \
} \
} \
\
T *release() \
{ \
T *ptr = m_ptr; \
m_ptr = NULL; \
return ptr; \
} \
\
T & operator*() const \
{ \
wxASSERT(m_ptr != NULL); \
return *m_ptr; \
} \
\
T * operator->() const \
{ \
wxASSERT(m_ptr != NULL); \
return m_ptr; \
} \
\
T * get() const \
{ \
return m_ptr; \
} \
\
void swap(name & ot) \
{ \
T * tmp = ot.m_ptr; \
ot.m_ptr = m_ptr; \
m_ptr = tmp; \
} \
}; //------------------------------------------------------------------------------------------------------------------------------
#define wxDEFINE_SCOPED_PTR(T, name)\
name::~name() \
{ \
wxCHECKED_DELETE(m_ptr); \
}

Definition of wxEventLoopTiedPtr

 int wxAppBase::MainLoop()
{
wxEventLoopTiedPtr mainLoop(&m_mainLoop, new wxEventLoop); return m_mainLoop->Run();
}

如上面说是在事件循环中扮演重要角色的是m_mailLoop(wxEventLoop 的实例)。沿着 m_mainLoop 提供的线索继续查下去,会找到wxWidgets中与平台相关的部分。对于gtk而言,这一部分看起来伐善可陈最核心的地方是调用了gtk_main( )。为方面没有源码的同学阅读,我也把这一部份的代码copy过来,有兴趣的话请展开阅读。对于事件驱动的应用程序而言,主事件循环在程序中扮演着重要的地位。维基百科上对这一机制的描述比较有用。“事件”可以由底层硬件产生、也可以由程序产生,产生事件的主体称为事件源。操作系统负责捕捉事件、将事件放在队列里面,并且提供相应的系统调用对事件队列进行访问操作。事件到达应用程序以后,便越过操作系统的边界重新来到应用程序的管辖范围。

 int wxEventLoop::Run()
{
// event loops are not recursive, you need to create another loop!
wxCHECK_MSG( !IsRunning(), -, _T("can't reenter a message loop") ); wxEventLoopActivator activate(this); m_impl = new wxEventLoopImpl; gtk_main(); OnExit(); int exitcode = m_impl->GetExitCode();
delete m_impl;
m_impl = NULL; return exitcode;
}

wxEventLoop::Run( )

 function main
initialize()
while message != quit
message := get_next_message()
process_message(message)
end while
end function

有了操作系统的支持,一个事件循环大体上如上伪代码所示(copy自维基百科)。需要注意的是,这里的get_next_message( ),它是一个非同步事件处理机制区别于忙等待的关键所在。当消息队列为空的时候,调用 get_next_message( )的进程被阻塞,不再占有CPU资源。当有事件到达时,进程重新被唤醒。可以近似地理解为,不是应用程序主动地去询问有没有事件到来。而是事件到达的时候自动通知应用程序。此二者的效率有天壤之别。

现在来看看与事件映射表(event table, MS世界里称之为消息映射)有关的内容。事件映射表的职责是建立事件与事件处理函数之间的关联。首先是出现的在MyFrame类声明中的 DECLARE_EVENT_TABLE。这个宏给所在的类里面添加了三个静态变量(sm_eventTableEntries, sm_eventTable 和 sm_eventHashTable)和访问这三个静态变量的接口。将这写宏逐个展开就可以看到这里真实面目:a) 在派生的Frame里面添加了静态成员以及静态接口的声明; b) 提供了三个静态成员的定义语句。

 #define DECLARE_EVENT_TABLE() \
private: \
static const wxEventTableEntry sm_eventTableEntries[]; \
protected: \
static const wxEventTable sm_eventTable; \
virtual const wxEventTable* GetEventTable() const; \
static wxEventHashTable sm_eventHashTable; \
virtual wxEventHashTable& GetEventHashTable() const;

DECLARE_EVENT_TABLE

 #define DECLARE_EVENT_TABLE() \
private: \
static const wxEventTableEntry sm_eventTableEntries[]; \
protected: \
static const wxEventTable sm_eventTable; \
virtual const wxEventTable* GetEventTable() const; \
static wxEventHashTable sm_eventHashTable; \
virtual wxEventHashTable& GetEventHashTable() const;

BEGIN_EVENT_TABLE

 #define END_EVENT_TABLE() DECLARE_EVENT_TABLE_ENTRY( wxEVT_NULL, 0, 0, 0, 0 ) };

END_EVENT_TABLE

 #define EVT_MENU(winid, func) wx__DECLARE_EVT1(wxEVT_COMMAND_MENU_SELECTED, winid, wxCommandEventHandler(func))

EVT_MENU

 #define DECLARE_EVENT_TABLE_ENTRY(type, winid, idLast, fn, obj) \
{ type, winid, idLast, fn, obj }

DECLARE_EVENT_TABLE

 #define wx__DECLARE_EVT2(evt, id1, id2, fn) \
DECLARE_EVENT_TABLE_ENTRY(evt, id1, id2, fn, NULL),
#define wx__DECLARE_EVT1(evt, id, fn) \
wx__DECLARE_EVT2(evt, id, wxID_ANY, fn)

wx__DECLARE_EVT

 4.  wxFrame是什么

在对 wxWidgets 的类继承体系有一个直观的认识以后,我们就能够回答wxFrame是什么的问题。'wxOject' 类是 wxWidgets 的世界里的太极,几乎所有的类都是派生于wxObject类。如下图所示,wxEvtHandler 直接派生于wxObject,并且构成整个体系中具有事件处理能力的一个分支。在 wxEvtHandler 的下面有两条线索与我们的程序密切相关。其一是窗口有关的类,继承于 wxWindowBase;其二是代表主事件循环的App类。如此以来就对 wxFrame 有了一个比较感官的认识:wxFrame 对应着程序的主窗口,负责窗口的显示,并且响应接受到的与窗口有关的事件。

5.  构建一个wxWidgets 应用的基本要素

现在回过头来总结一下构建一个完整的应用程序需要有哪些基本要素。首先,作为一个c++程序必须要有main( )函数的戏份,wxWidgets 中宏 IMPLEMENT_APP 当此重任;其次,作为 IMPLEMENT_APP 的前提条件需要有App类,其角色是查询系统的事件队列并且分配事件; 最后,是事件的最终归宿——窗口实体,由 wxFrame负责。

也许 IMPLEMENT_APP 对main函数进行深度封装的意图之一即是弱化c/c++世界的基本公理(main充当所有函数的入口),巩固 wxWidgets 用户对 App 类的重视程度。如果这样的猜测正确的话,那么在此我们可以得到一个简洁的结论:wxWidgets应用有两个基本要素:App 和 Frame/Window。站在应用程序员的角度来看,这二者分别是事件的源头和终点(source & sink)。

 6.  参考文献

【1】http://www.gtk.org/api/2.6/gtk/gtk-General.html,gtk的API,讲述了gtk的事件和事件循环(main loop)。

【2】https://groups.google.com/forum/#!topic/wx-users/QPtpBcPr0WY,google论坛上的一个topic,讨论了EVT_IDLE有关的内容。

【3】http://zetcode.com/tutorials/gtktutorial/gtkevents/,gtk+的教程(tutorial)中“events and signals”一节。

【4】http://en.wikipedia.org/wiki/Event_loop,wikipedia中关于event loop的词条


作者通信地址:Boyue.K.Cheung@outlook.com

欢迎交流、指错、提出改进建议。

Please feel free to raise any questions, advices. And I'll feel free to ignore them if necessary. ~_~


[wxWidgets] 2. 重访“Hello World” 程序的更多相关文章

  1. wxWidgets刚開始学习的人导引(3)——wxWidgets应用程序初体验

    wxWidgets刚開始学习的人导引全文件夹   PDF版及附件下载 1 前言2 下载.安装wxWidgets3 wxWidgets应用程序初体验4 wxWidgets学习资料及利用方法指导5 用wx ...

  2. wxWidgets初学者导引(3)——wxWidgets应用程序初体验

    wxWidgets初学者导引全目录   PDF版及附件下载 1 前言2 下载.安装wxWidgets3 wxWidgets应用程序初体验4 wxWidgets学习资料及利用方法指导5 用wxSmith ...

  3. wxWidgets刚開始学习的人导引(1)——前言

    wxWidgets刚開始学习的人导引全文件夹   PDF版及附件下载 1 前言2 下载.安装wxWidgets3 wxWidgets应用程序初体验4 wxWidgets学习资料及利用方法指导5 用wx ...

  4. wxWidgets 和 QT 之间的选择

    (非原创,网络摘抄) 跨平台的C++ GUI工具库很多,可是应用广泛的也就那么几个,Qt.wxWidgets便是其中的翘楚这里把GTK+排除在外,以C实现面向对象,上手相当困难,而且Windows平台 ...

  5. PostgreSQL 10 如何使用 PgAdmin3

    自从 PgAdmin4 出来以后,PgAdmin3 就停止开发了,PgAdmin 官网下载的 PgAdmin3 无法支持 PostgreSQL 10 或者更高版本的数据库服务器端. 但是 PgAdmi ...

  6. [wxWidgets] 1. 安装及"hello world"程序

    关于wxWidgets的优越已经在它的官方网站有所阐述,本文不再赘述. 本系列主要记录学习这个软件包过程中遇到的问题以及心得. 1.  安装 从源码安装虽然大多时候不是一件轻松的过程,但是基于以下两个 ...

  7. 在ubuntu上使用wxWidgets成功开发一个图形界面程序

    编译wxWidgets 下载最新版的源码,wxWidgets-3.0.2.wxWidgets在liunx是通过wxGTK实现的.wxGTK和wxWidgets的源码打包在一起.wxGTK依赖GTK+, ...

  8. 第一个wxWidgets程序

    wxWidgets的安装方法网上有一大堆,可以参照http://wiki.codeblocks.org/index.php?title=WxWindowsQuickRef 这里解压并编译 ,也可以参照 ...

  9. [ZETCODE]wxWidgets教程三:第一个窗体程序

    本教程原文链接:http://zetcode.com/gui/wxwidgets/firstprograms/ 翻译:瓶哥 日期:2013年11月27日星期三 邮箱:414236069@qq.com ...

随机推荐

  1. Redis 常用链接

    安装部署 http://www.linuxidc.com/Linux/2014-05/101979.htm 更改访问权限配置(如密码,可访问地址):http://www.jb51.net/articl ...

  2. UE4 执行Console Command ----ExecuteConsoleCommand

    void UKismetSystemLibrary::ExecuteConsoleCommand(UObject* WorldContextObject, const FString& Com ...

  3. PHP时区配置

    在PHP安装目录中找到 php.ini-development 复制创建新的副本 找到 :date.timezone = 修改为 date.timezone = PRC 并保存为php.ini PRC ...

  4. software_testing_work3_question1

    package com.Phantom; import java.io.IOException; import java.util.Scanner; public class Work3_1 { /* ...

  5. SqlServer性能优化 通过压缩与计算列提高性能(十一)

    压缩: 1.压缩的对象 1.表   2.索引(非聚集索引手工做)   3.备份(手工做) 2.对性能影响 1.提高IO性能     2.降低CPU性能 行压缩: 1.对null值不占用空间 2.对Nu ...

  6. 【JS】IE兼容placeholder

    直接上代码: $(document).ready(function () { var doc = document, textareas = doc.getElementsByTagName('tex ...

  7. HowTo系列之virtualenv

    1. 简介 virtualenv是一个用于创建Python独立运行环境的命令行工具. 在我们使用python进行开发的时候,我们会依赖不同的Python版本和不同的库版本.当我们的项目需要依赖别的开发 ...

  8. 面试时被问到js的绑定事件,我居然不知道怎么回答。回来查了下,做个笔记

    事件绑定是几种方法 以下为例: <button id='btn'>click me</button> function Btn(){ alert('click'); } 1.直 ...

  9. SQL中跨服务器查询

    1.跨库查询 select * from [库名].dbo.表名 2.跨库查询 select * from OPENDATASOURCE('SQLOLEDB','Data Source=服务器名或IP ...

  10. 详解SVN 的使用

    一.什么是SVN SVN是Subversion的简称,是一个开放源代码的版本控制系统,相较于RCS.CVS,它采用了分支管理系统,它的设计目标就是取代CVS. 二.SVN的下载安装 下载地址:http ...