为什么要使用 wxWidgets 呢?原因很简单,它让您可以快速轻松地编写能够跨平台运行的 GUI,能让您随意选用编程语言,还能让您的 GUI 如下图所示一样优秀:
图 1 显示了 Chandler,一个在开放源码应用程序基础上开发的日历和电子邮件管理程序。它是使用 wxWidgets 工具包编写的。虽然原始版本的 wxWidgets 是在 C++ 中实现的,Chandler 的创建者们却使用了 wxPython 工具包及 Python 作为包装程序来与 C++ 库进行无缝的交互。wxWidgets 工具包会尽可能地利用原生对象,这些对象通过在需要的地方使用强大的定制窗口小部件得以扩充。您可以编写能够运行在多种平台的 wxWidgets 程序,并且可以使用多种编程语言来实现。
开始使用 wxWidgets
开始之前,我假定您已经到 wxWidgets 主页下载了所用平台的相关软件包。如果还没有,请参见 参考资料 部分的链接并下载它。我还进一步假定您已经掌握了将 wxWidgets 库与编译器或所选的集成开发环境(IDE)进行集成所需的命令和设置。如果还不是很明白,本文后面 参考资料 部分中有指向所需信息的相关链接。完成上述工作后,您就可以着重进行程序代码编写了。
wxWidgets 程序的主体包括两种主要的对象:应用程序对象和框架对象。可以有多个框架,而且在代码中还可能需要一些特定于 wxWidgets 的宏。以下解释了它们是如何组合在一起的。
链接到 wxWidgets 库
要链接到 wxWidgets 库,必须先包括它。在头文件的顶部放上如下一行代码:
#include "wx/wx.h" |
wx/wx.h 头文件包括所有可能需要的 wxWidgets 定义。如果十分关注于性能,则可以用一些针对将要用到的特定头文件的 include
语句来替代该文件。
定义应用程序类
接下来,必须定义应用程序类。在很多简单的情况下,该类的作用不大,但是您必须有一个自己的类。wxWidgets 应用程序继承自 wxApp
类,其定义很简单:
class DemoApp : public wxApp { public: virtual bool OnInit(); } |
应用程序开始时,调用 OnInit()
函数 —— 实际上是 main()
方法。
定义了应用程序类之后,在代码中放上如下的宏:
IMPLEMENT_APP(DemoApp) |
您可以用自己的应用程序类的名称来替代 DemoApp
。这个宏创建 wxWidgets 所使用的真正的 main()
方法。另外,它还创建应用程序对象的一个实例并开始初始化过程。
定义框架类
现在开始定义框架类,它代表应用程序中的主窗口。xWidgets 父类是 wxFrame
。清单 1 给出了一个简单的示例。
class DemoFrame : public wxFrame { public: DemoFrame(const wxString& title); void OnButtonPress(wxCommandEvent& event); private: DECLARE_EVENT_TABLE() }; |
为了去除不太熟悉的名称,wxString
是一种特定于 wxWidgets 的字符串包装程序类,它在整个 wxWidgets 工具包中用于字符串操作。工具包拒绝标准模板库 (STL) 类的使用以避免将 wxWidgets 限制在 STL 可用的平台。如果需要,可以采用编译时间开关将 STL 用作底层实现。类似地,wxCommandEvent
是事件的一种父类 —— 具体来说是命令事件,是通常与用户操作(如单击按钮或从列表中选择)相关的高级事件。而 DECLARE_EVENT_TABLE
宏是任何需要对事件做出响应的 wxWidgets 对象(它无疑包括本文中的小演示框架)所必需的。
定义事件表
要真正响应事件,必须在实现文件内定义事件表。它是另一种宏,在本文的示例中它如清单 2 所示。
BEGIN_EVENT_TABLE(DemoFrame, wxFrame) EVT_BUTTON(wxID_CLOSE, DemoFrame::OnButtonPress) END_EVENT_TABLE() |
BEGIN_EVENT_TABLE()
宏有两个参数:事件表实际针对的类和此类的中间父类。如您所预期的一样,END_EVENT_TABLE
宏指出事件表的末尾。中间可以包含若干个特定的事件宏,本例中只有一个。
wxWidgets 工具包包含几个不同的事件宏,每个对应于不同的事件。在本例中,EVT_BUTTON
代表按钮单击。这个宏的第一个参数是指代正在处理的特定按钮的标识符。wxID_CLOSE
标识符是与应用程序的一些公共特性相关的几个预定义的标识符之一。这里使用预定义的标识符完全是为了方便,虽然在有些情况下,某些特定的标识符会触发 wxWidgets 系统的特别处理。第二个参数是在触发菜单事件时调用的方法的全限定名。您可以用类似于此处所描述的事件宏来管理 wxWidgets 中的所有事件。
定义方法
现在,可以开始定义一些方法了。我这里给出了三个简单的方法,第一个是清单 3 中所示的 OnInit()
方法。
bool DemoApp::OnInit() { DemoFrame *frame = new DemoFrame("DeveloperWorks Demo"); frame->Show(true); return true; } |
该方法的前两行的用途在 GUI 程序的开头部分很常见:它们创建和显示主窗口。第三行对应用程序的其余部分非常重要。如果返回 true
,则向 wxWidgets 引擎的其余部分发出一个信号表明初始化已经成功完成,程序可以继续。相反,如果返回 false
,则应用程序会停止并退出。
OnInit()
方法为 DemoFrame
引用构造函数。在该方法,可以向框架添加按钮,如清单 4 所示。
DemoFrame::DemoFrame(const wxString& title) : wxFrame(NULL, wxID_ANY, title) { wxSizer *sizer = new wxBoxSizer(wxVERTICAL); this->SetSizer(sizer); wxButton *button = new wxButton(this, wxID_CLOSE, "Click Me"); sizer->Add(50, 50); sizer->Add(button, 0, wxALL, 50); } |
但是在可以向框架添加按钮之前,需要先创建 sizer。wxWidgets 中的 Sizer 相当于 Java 编程语言中的布局管理器:它们允许您使用预定义的规则在窗口中放置对象,而不需要单独为每个窗口小部件设置大小和位置。本例中使用了 wxBoxSizer
,它将窗口小部件布局在一条直线上。一个方框 sizer 可以是垂直的也可以是水平的。创建 sizer 之后,使用 SetSizer()
方法将它附加到了框架。然后创建按钮。按钮构造函数的参数有:
不必显式地将按钮添加到框架,只要将框架标识为父容器就可以了。但必须将按钮显式地添加到 sizer,以便 sizer 的布局算法知道它。在方法的最后一行实现这一点,但必须是在行的顶部添加了 50 x 50 像素的空白之后。当添加按钮时,sizer 还会用 50 像素的边框围绕按钮。这可以通过使用 wxALL
标志和最后一个参数 50
来实现。
定义事件处理程序
最后,需要定义简单的事件处理程序,如清单 5 所示。
void DemoFrame::OnButtonPress(wxCommandEvent& event) { Close(true); } |
这再简单不过了。编译后会得到如图 2 所示的包含有一个按钮的窗口。单击按钮,窗口会关闭。在 wxWidgets,关闭最后一个父框架会自动退出应用程序,所以单击此按钮也会导致完全退出应用程序。
在本例中还刚刚接触到用 wxWidgets 所能实现的功能的皮毛。进一步的探索请参阅 参考资料 部分的指南。
|
wxPython
wxWidgets 确实是一种功能强大的工具包,但并不是所有人都愿意使用 C++ 析构函数、内存管理等等。所幸的是一组优秀的程序员已经创建了到 wxWidgets 库的、可从其他编程语言使用的包装程序绑定。所以,即使所选择使用的编程工具不是 C++ ,仍然可以从 wxWidgets 库获益。
发展得最为成熟和全面的 wxWidgets 绑定是 wxPython, 通过它可以使用 Python 编程语言创建 wxWidgets 程序。有针对 Microsoft® Windows®、Mac 和 Linux® 平台的下载,其用户社区规模也很大并十分活跃。想了解一下它?清单 6 中给出的 Python 程序可以创建与前面在 C++ 中创建的一样的空白窗口。
#!/usr/bin/env python import wx class DemoApp(wx.App): def OnInit(self): frame = DemoFrame(parent=None, id=-1, title='DeveloperWorks') frame.Show() return True class DemoFrame(wx.Frame): def __init__(self, parent, id, title): wx.Frame.__init__(self, parent, id, title) sizer = wx.BoxSizer(wx.VERTICAL) self.SetSizer(sizer) button = wx.Button(self, wx.ID_CLOSE, "Click Me") sizer.Add((50, 50)) sizer.Add(button, 0, wx.ALL, 50) self.Bind(wx.EVT_BUTTON, self.OnButtonClick) def OnButtonClick(self, event): self.Close(True) if __name__ == "__main__": app = DemoApp() app.MainLoop() |
如您所见,在这样一个短小的程序中,在 C++ API 调用和 wxPython 调用之间几乎存在一对一的对应。在两种情况下,都创建了一个应用程序对象和一个框架对象。二者都以 OnInit()
方法开头,并且定义了相似的构造函数和对象处理程序。
本例中最大的不同是事件到处理程序的绑定。C++ 版本使用事件表宏管理该绑定,而 Python 版本则使用 Bind()
方法来管理,这个方法将代表事件类型的 Python 对象和在事件被调用时实际调用的对象作为参数。这种结构利用了 Python 可以将方法视为变量并可以将它们作为参数传递(传递的方式与传递字符串或整数相同)的优势。
wxPython 较 C++ wxWidgets 工具包的优势在更长或更复杂一些的程序中会更明显。即使不将 C++ 和 Python 作为两种编程语言进行专门的对比,wxPython 工具包的一些吸引人的优秀特性仍然非常具有吸引力。使用 Bind()
方法的事件处理机制融合进 wxPython 中比融合进 wxWidgets 中更容易。在 Python 版本中更容易在运行时动态地更新处理程序。一些复杂或复合的窗口小部件,比如树状列表控件或图像单选按钮,是 wxPython 工具包中的标准组件,但在 C++ 版中不是的。而且,wxPython 包含开发工具的 Py 程序包,它使得向 wxPython 程序添加交互式调试变得很简单。
|
wxEverythingElse
Python 并不是惟一一种具有访问 wxWidgets 库的绑定的编程语言。虽然 wxPython 是其中最为成熟的一种,如果更愿意使用某种特定的编程语言,很值得了解一下其余的几个。既然这样,就让我们在 wxWorld 中的几个地方稍作停留吧。请注意这些项目的可靠性和健壮性的评估基于可用资料。在这些项目中,很多都源自一两个专家程序员对其的热爱。如果对某个项目感兴趣,请务必自己查看一下。
wxPerl
wxPerl 绑定是在 2006 年 6 月正式发布的。它已重新开始了日常快照的交付,但可用的文档却是几年前编制的。活动邮件列表的范围一般都是一天两个或三个消息。有针对 Win32、Linux 和 Mac OS X 的二进制下载。除了主要的工具包,还有一些附加工具包可用,包括 OpenGL 包装程序和用于创建 Mac OS X 应用程序的程序包。
wxPerl 的主要问题在于如何将 wxWidgets API 翻译成 Perl 中的面向对象编程(OOP)的有些异质的变体。清单 7 中所示的代码片段与前面所给出的框架示例有些相似。
package MyFrame; use base 'Wx::Frame'; use Wx::Event qw(EVT_BUTTON); sub new { my $class = shift; my $self = $class->SUPER::new(undef, -1, 'Trying wxPerl', [-1, -1], [250, 200]); my $sizer = Wx::BoxSizer->new(wxVERTICAL); my $button = Wx::Button->new($self, -1, 'Click me!', [-1, -1], [-1, -1]); EVT_BUTTON($self, $button, /&OnButtonClick); $sizer->Add($button); $self->SetSizer($sizer); return $self; } sub OnButtonClick { my($self, $event) = @_; $self->SetTitle('You Did It'); } |
这段代码基本上是前面已经见过的 C++ 和 Python 代码的逐行翻译。在本例中,wxWidgets 库是 Perl 程序包的形式,而且使用了 EVT_BUTTON
函数调用,这看起来很像 C++ 版本中的宏定义。
wxRuby
wxRuby 项目现在的情况比较复杂。在最早的版本中,到 wxWidgets API 的绑定是手工创建的。该工具最近的版本于 2004 年 11 月发布,但从那时开始,对新的版本的开发就时断时续,新版本利用更强大的 Simplified Wrapper and Interface Generator(SWIG)工具包来生成 Ruby 和 wxWidgets 之间的绑定。在其邮件列表中对新版本发布时间的陈述常常是 “很快但也可能要几个月以后”。
wxRuby 另一个有趣的方面是,与大多数其他的 wxWidgets 绑定不同,开发人员大都选择对 wxWidgets API 调用的名称进行调整以更符合 Ruby 命名约定(具体来说,要采用 lower_case_with_underscores 而非 wxWidgets 的 UpperCaseWithCamelCase)。所以,上述所有代码示例使用 SetSizer()
函数,在 wxRuby 中会称为 set_sizer()
。 除此之外,涉及到 wxWidgets API 的大部分 wxRuby 程序都将与前面已经给出的示例相似。
wxWidgets 的世界
其他 wxWidgets 端口完成与否的程度各不相同。以下是关于其余 wxWidgets 的概括:
|
结束语
wxWidgets 可以为各类程序员提供大量的可用功能。其基本工具包非常灵活,能够处理您的大多数 GUI 需求。多种语言绑定让大多数程序员都能得心应手地使用 wxWidgets。充分了解所选择的语言中的 wxWidgets 工具包将有助于您在自己的应用程序中构建优秀的界面。
参考资料
学习关于作者
|
Noel Rappin 从 Georgia Institute of Technology 的 Graphics, Visualization and Usability Center 获得了博士学位,现在是 Motorola, Inc. 的一名高级软件工程师。他还是 wxPython in Action(Manning Publications,2006 年 3 月)一书和 Jython Essentials(O'Reilly,2002 年 5 月)一书的合著者之一。 |