这是四年多来在博客园的第二篇博客。有了上一次的排版使用经验,这一篇文章应该有些进步(^_^)。
闲话按下不表,言归正传。在编译、成功运行了上一个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 ---- 函数流程控制
这些源代码分别上传如下:
1 /**************************************************************** 2 Hello world expample, copied from www.wxwidgets.org 3 For study purpose only. 4 ----Boyue.Zhang 5 28-Jun-2013 6 ****************************************************************/ 7 8 #if !defined(__HW_APP_H__) 9 #define __HW_APP_H__ 10 11 #include <wx/wx.h> 12 13 class MyApp: public wxApp 14 { 15 public: 16 virtual bool OnInit(); 17 }; 18 19 #endif
1 /**************************************************************** 2 Hello world expample, copied from www.wxwidgets.org 3 For study purpose only. 4 ----Boyue.Zhang 5 28-Jun-2013 6 ****************************************************************/ 7 8 #include "hwApp.h" 9 #include "hwFrame.h" 10 11 bool MyApp::OnInit() 12 { 13 MyFrame *frame = new MyFrame("Hello World", wxPoint( 50, 50 ), wxSize( 450, 340 ) ); 14 frame->Show( true ); 15 return true; 16 }
1 /**************************************************************** 2 Hello world expample, copied from www.wxwidgets.org 3 For study purpose only. 4 ----Boyue.Zhang 5 28-Jun-2013 6 ****************************************************************/ 7 8 #if !defined( __HW_FRAME_H_) 9 #define __HW_FRAME_H_ 10 11 #include <wx/wx.h> 12 13 enum 14 { 15 ID_Hello = 1 16 }; 17 18 class MyFrame: public wxFrame 19 { 20 public: 21 MyFrame( const wxString& title, const wxPoint& pos, 22 const wxSize& size ); 23 private: 24 void OnHello( wxCommandEvent& event ); 25 void OnExit( wxCommandEvent& event ); 26 void OnAbout( wxCommandEvent& event ); 27 28 DECLARE_EVENT_TABLE( ); 29 }; 30 31 32 #endif
1 /**************************************************************** 2 Hello world expample, copied from www.wxwidgets.org 3 For study purpose only. 4 ----Boyue.Zhang 5 28-Jun-2013 6 ****************************************************************/ 7 8 #include "hwFrame.h" 9 10 MyFrame::MyFrame( const wxString& title, const wxPoint& pos, const wxSize& size) 11 : wxFrame( NULL, wxID_ANY, title, pos, size ) 12 { 13 wxMenu *menuFile = new wxMenu; 14 menuFile->Append( ID_Hello, "&Hello...\tCtrl-H", 15 "Help string shown in status bar for this menu item"); 16 menuFile->AppendSeparator( ); 17 menuFile->Append( wxID_EXIT ); 18 19 wxMenu *menuHelp = new wxMenu; 20 menuHelp->Append( wxID_ABOUT); 21 22 wxMenuBar *menuBar = new wxMenuBar; 23 menuBar->Append( menuFile, "&File" ); 24 menuBar->Append( menuHelp, "&Help" ); 25 26 SetMenuBar( menuBar ); 27 28 CreateStatusBar( ); 29 SetStatusText( "Welcome to wxWidgets!" ); 30 31 } 32 33 void MyFrame::OnExit( wxCommandEvent & event ) 34 { 35 close( true ); 36 } 37 38 void MyFrame::OnAbout( wxCommandEvent& event ) 39 { 40 wxMessageBox(" Ths is a wxWidgets' Hello world sample", 41 "About Hello World", wxOK|wxICON_INFORMATION ); 42 } 43 44 void MyFrame::OnHello( wxCommandEvent& event ) 45 { 46 wxLogMessage( "Hello world from wxWidgets!" ); 47 } 48 49 BEGIN_EVENT_TABLE( MyFrame, wxFrame ) 50 EVT_MENU( ID_Hello, MyFrame::OnHello ) 51 EVT_MENU( wxID_EXIT, MyFrame::OnExit ) 52 EVT_MENU( wxID_ABOUT, MyFrame::OnAbout ) 53 END_EVENT_TABLE()
5). helloWorld.cpp
上篇文章,我说这个程序不太适合作为hello world示例,现在我发现错怪原作者了。进行了这一系列重组变换以后,发现hw示例的原型是这个样子的。何止是简洁啊,简直是见鬼。一个宏把一切东西搞定了。main函数、消息循环什么的统统不见鸟。
1 /**************************************************************** 2 Hello world expample, copied from www.wxwidgets.org 3 For study purpose only. 4 ----Boyue.Zhang 5 28-Jun-2013 6 ****************************************************************/ 7 8 #include <wx/wx.h> 9 #include "hwApp.h" 10 #include "hwFrame.h" 11 12 13 IMPLEMENT_APP( MyApp )
附带说明一下,分开以后的程序编译的时候指令有点长,使用Makefile是一个不错的选择。示例如下清单。
1 default: 2 g++ hwApp.cpp hwFrame.cpp helloWorld.cpp `wx-config --cxxflags --libs` -o helloWorld 3 clean: 4 rm -f ./helloWorld
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就没有机会被执行。 这样申请到的资源在过期以后就堂而皇之地逍遥法外。
1 int wxEntryReal(int& argc, wxChar **argv) 2 { 3 // library initialization 4 wxInitializer initializer(argc, argv); 5 6 if ( !initializer.IsOk() ) 7 { 8 #if wxUSE_LOG 9 // flush any log messages explaining why we failed 10 delete wxLog::SetActiveTarget(NULL); 11 #endif 12 return -1; 13 } 14 15 wxTRY 16 { 17 18 // app initialization 19 if ( !wxTheApp->CallOnInit() ) 20 { 21 // don't call OnExit() if OnInit() failed 22 return -1; 23 } 24 25 // ensure that OnExit() is called if OnInit() had succeeded 26 class CallOnExit 27 { 28 public: 29 ~CallOnExit() { wxTheApp->OnExit(); } 30 } callOnExit; 31 32 WX_SUPPRESS_UNUSED_WARN(callOnExit); 33 34 // app execution 35 return wxTheApp->OnRun(); 36 } 37 wxCATCH_ALL( wxTheApp->OnUnhandledException(); return -1; ) 38 }
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原来的值将被恢复。
1 // this defines wxEventLoopPtr 2 wxDEFINE_TIED_SCOPED_PTR_TYPE(wxEventLoop) 3 4 //-------------------------------------------------------------------------------------------------------------------------- 5 #define wxDEFINE_TIED_SCOPED_PTR_TYPE(T) \ 6 wxDEFINE_SCOPED_PTR_TYPE(T) \ 7 class T ## TiedPtr : public T ## Ptr \ 8 { \ 9 public: \ 10 T ## TiedPtr(T **pp, T *p) \ 11 : T ## Ptr(p), m_pp(pp) \ 12 { \ 13 m_pOld = *pp; \ 14 *pp = p; \ 15 } \ 16 \ 17 ~ T ## TiedPtr() \ 18 { \ 19 *m_pp = m_pOld; \ 20 } \ 21 \ 22 private: \ 23 T **m_pp; \ 24 T *m_pOld; \ 25 }; 26 27 28 //------------------------------------------------------------------------------------------------------------------------------ 29 // this macro can be used for the most common case when you want to declare and 30 // define the scoped pointer at the same time and want to use the standard 31 // naming convention: auto pointer to Foo is called FooPtr 32 #define wxDEFINE_SCOPED_PTR_TYPE(T) \ 33 wxDECLARE_SCOPED_PTR(T, T ## Ptr) \ 34 wxDEFINE_SCOPED_PTR(T, T ## Ptr) 35 36 37 //------------------------------------------------------------------------------------------------------------------------------ 38 #define wxDECLARE_SCOPED_PTR(T, name) \ 39 class name \ 40 { \ 41 private: \ 42 T * m_ptr; \ 43 \ 44 name(name const &); \ 45 name & operator=(name const &); \ 46 \ 47 public: \ 48 wxEXPLICIT name(T * ptr = NULL) \ 49 : m_ptr(ptr) { } \ 50 \ 51 ~name(); \ 52 \ 53 void reset(T * ptr = NULL) \ 54 { \ 55 if (m_ptr != ptr) \ 56 { \ 57 delete m_ptr; \ 58 m_ptr = ptr; \ 59 } \ 60 } \ 61 \ 62 T *release() \ 63 { \ 64 T *ptr = m_ptr; \ 65 m_ptr = NULL; \ 66 return ptr; \ 67 } \ 68 \ 69 T & operator*() const \ 70 { \ 71 wxASSERT(m_ptr != NULL); \ 72 return *m_ptr; \ 73 } \ 74 \ 75 T * operator->() const \ 76 { \ 77 wxASSERT(m_ptr != NULL); \ 78 return m_ptr; \ 79 } \ 80 \ 81 T * get() const \ 82 { \ 83 return m_ptr; \ 84 } \ 85 \ 86 void swap(name & ot) \ 87 { \ 88 T * tmp = ot.m_ptr; \ 89 ot.m_ptr = m_ptr; \ 90 m_ptr = tmp; \ 91 } \ 92 }; 93 94 95 //------------------------------------------------------------------------------------------------------------------------------ 96 #define wxDEFINE_SCOPED_PTR(T, name)\ 97 name::~name() \ 98 { \ 99 wxCHECKED_DELETE(m_ptr); \ 100 }
1 int wxAppBase::MainLoop() 2 { 3 wxEventLoopTiedPtr mainLoop(&m_mainLoop, new wxEventLoop); 4 5 return m_mainLoop->Run(); 6 }
如上面说是在事件循环中扮演重要角色的是m_mailLoop(wxEventLoop 的实例)。沿着 m_mainLoop 提供的线索继续查下去,会找到wxWidgets中与平台相关的部分。对于gtk而言,这一部分看起来伐善可陈最核心的地方是调用了gtk_main( )。为方面没有源码的同学阅读,我也把这一部份的代码copy过来,有兴趣的话请展开阅读。对于事件驱动的应用程序而言,主事件循环在程序中扮演着重要的地位。维基百科上对这一机制的描述比较有用。“事件”可以由底层硬件产生、也可以由程序产生,产生事件的主体称为事件源。操作系统负责捕捉事件、将事件放在队列里面,并且提供相应的系统调用对事件队列进行访问操作。事件到达应用程序以后,便越过操作系统的边界重新来到应用程序的管辖范围。
1 int wxEventLoop::Run() 2 { 3 // event loops are not recursive, you need to create another loop! 4 wxCHECK_MSG( !IsRunning(), -1, _T("can't reenter a message loop") ); 5 6 wxEventLoopActivator activate(this); 7 8 m_impl = new wxEventLoopImpl; 9 10 gtk_main(); 11 12 OnExit(); 13 14 int exitcode = m_impl->GetExitCode(); 15 delete m_impl; 16 m_impl = NULL; 17 18 return exitcode; 19 }
1 function main 2 initialize() 3 while message != quit 4 message := get_next_message() 5 process_message(message) 6 end while 7 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) 提供了三个静态成员的定义语句。
1 #define DECLARE_EVENT_TABLE() \ 2 private: \ 3 static const wxEventTableEntry sm_eventTableEntries[]; \ 4 protected: \ 5 static const wxEventTable sm_eventTable; \ 6 virtual const wxEventTable* GetEventTable() const; \ 7 static wxEventHashTable sm_eventHashTable; \ 8 virtual wxEventHashTable& GetEventHashTable() const;
1 #define DECLARE_EVENT_TABLE() \ 2 private: \ 3 static const wxEventTableEntry sm_eventTableEntries[]; \ 4 protected: \ 5 static const wxEventTable sm_eventTable; \ 6 virtual const wxEventTable* GetEventTable() const; \ 7 static wxEventHashTable sm_eventHashTable; \ 8 virtual wxEventHashTable& GetEventHashTable() const;
1 #define END_EVENT_TABLE() DECLARE_EVENT_TABLE_ENTRY( wxEVT_NULL, 0, 0, 0, 0 ) };
1 #define EVT_MENU(winid, func) wx__DECLARE_EVT1(wxEVT_COMMAND_MENU_SELECTED, winid, wxCommandEventHandler(func))
1 #define DECLARE_EVENT_TABLE_ENTRY(type, winid, idLast, fn, obj) \ 2 { type, winid, idLast, fn, obj }
1 #define wx__DECLARE_EVT2(evt, id1, id2, fn) \ 2 DECLARE_EVENT_TABLE_ENTRY(evt, id1, id2, fn, NULL), 3 #define wx__DECLARE_EVT1(evt, id, fn) \ 4 wx__DECLARE_EVT2(evt, id, wxID_ANY, fn)
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. ~_~
浙公网安备 33010602011771号