万事开头难,良好的开端,就是成功的一半。尤其是在程序开发的学习过程中,许多初学者抑或是久经实战考验的高手在面对新技术时,也常常不得其门而入,而一旦迈入了新技术的大门,就算是驶入了飞速提升的快车道了。
程序开发所涉及的知识和技能如果不能算是博大精深,那也一定是浩如烟海。而对于其中任何一个分支,当您打算深入探索时,又会发现自己突然陷入了一片汪洋大海。呈现在您面前的往往是由既纷繁芜杂而又琐碎零散的知识相互交织成的一张“大网”,然而这张网“有纪有纲”,“一引其纪,万目皆起,一引其纲,万目皆张”。聪明的做法正是抓住这张“网”的“纲”和“纪”,而这个“纲纪”正是那个家喻户晓、盛名远播的“Hello World”程序。
这个看似寥寥数行、毫无用处的程序却是所有更大规模wxWidgets程序的缩影,是您开启高手之路的起点。它不仅展现了一个完整程序所应该具备的基本面貌和主体框架,还演示了编写一个完整程序所需要经历的主要步骤和经常运用的开发技巧。因此,本专栏其他章节的内容大致是在这个“Hello World”的基础上不断延伸并开枝散叶的。
“举一纲而万目张,解一卷而众篇明”,HelloWorld的地位和作用不言而喻。“夫小者大之渐,微者著之萌,故君子慎初”,建议初学者在完全熟悉本篇内容并成功编译运行样例程序后再开始后续文章的学习。
“合抱之木,生于毫末,千里之行,始于足下”,如有必要,请将Hello world的代码熟记于胸。
您可以在您喜欢的任意平台上搭建wxWidgets开发环境。如何搭建环境不是我们的重点,下面仅演示在开发人员最常使用的Windows操作系统中搭建wxWidgets开发环境过程,以使您能够尽快进入正题。如果您工作在其它操作系统中,可以参考源代码中的安装说明或者直接下载已编译好的开发包。
本教程使用Windows上的Linux仿真环境MYSYS2.
MYSYS2基于Cpywin和MinGW-w64,提供了bash shell、GCC、pacman等工具,是在Windows上的Linux仿真开发环境,使用MYSYS2,可以按照Linux的方式开发Windows程序。
登录MSYS2官方网站https://www.msys2.org/下载MYSYS2安装软件并安装。
安装结束后,运行MYSYS2,弹出Shell环境,运行pacman -S mingw-w64-x86_64-toolchain安装GCC编译工具。
pacman -S mingw-w64-x86_64-toolchain
输入gcc --version查看最新gcc版本。
gcc --version
本教程使用gcc 13.2,该版本支持最新的C++23标准。
wxWidgets支持Visual Studio、Dev-C++、CLion等多种IDE工具,您可以自由选择自己称手的工具。本系列教程选用Code::Blocks,这是一个开源的全功能跨平台IDE,由纯粹的C++语言开发并基于wxWidgets构建。
(1)下载
浏览器打开http://www.codeblocks.org/downloads页面。点击“Download the binary release”链接,下载CodeBlocks安装版。
(2)安装
下载完成后,右键点击安装文件,选择“以管理员身份运行”。按默认设置安装。
(3)配置
打开CodeBlocks。选择菜单“Settings->Compiler...”,弹出编译器设置对话框,在“GlobalCompiler Settings”中配置“Toolchains executable”为MinGW-64中的相关工具,具体配置如下。

图 3 CodeBlocks编译工具链设置
此外,如果您有兴趣,也可以在https://sourceforge.net/上下载CodeBlocks的nightly版本,该版本没有提供安装打包程序,安装上略显繁琐,但其更新速度快于CodeBlocks官方网站的版本。
下面将演示如何在Windows系统上利用MYSYS2听MinGW和CMake编译wxWidgets源码,CMake的安装过程可参阅相关他资料。MinGW在本机的安装目录为D:/mysys64/MinGW64。
(1)获取wxWidgets源码
登录官网https://www.wxwidgets.org/downloads/下载最新源码包。Windows系统可以点击“Windows ZIP”、“Windows 7z”或“Windows Installer”链接下载,Linux、macOS或其他操作系统点击“Source for Linux, macOS, etc”链接下载。
(2)CMake编译源码
在源码目录中创建编译目录如“mybuild”。
打开CMake,设置源码目录和编译目录。点击“Configure”按纽生成配置文件。
检查Configure操作生成的配置项。
1.检查EGROUP、CMAKE_SH的目录D:/mysys64/MinGW64/bin;
2.检查编译工具的目录,确保其目录都为D:/mysys64/MinGW64/bin;
3.配置安装目录CMAKE_INSTALL_PREFIX,如E:/devsoft/wxWidgets。
点击“Generate”按纽生成Makefile文件。
运行D:MinGWmsys1.0msys.bat,打开命令行界面,切换至mybuild目录。运行mingw32-make && mingw32-make install编译程序。
mingw32-make && mingw32-make install
编译生成的*.a、*.dll和*.h文件存放在编译前指定的安装目录中。
libgcc_x64_dll中包含了编译和运行wxWidgets应用程序所需要的链接库、动态库文件以及编译所需的setup.h文件。为了让基于wxWidgets的应用程序能够正常启动,其中*.dll文件可以拷贝至system32目录。setup.h文件需要拷贝至include/wx目录下。
尽管CodeBlocks是基于wxWidgets开发,但在发布版的程序中不带wxWidgets开发包。需要设置wxWidgets的include目录和lib目录。菜单“Setting->Compiler...”,在Global Setting的Search Directory中,分别添加Compiler、Linker和Resource Compiler的头文件目录。

图 4 编译器搜索目录设置

图 5 链接器搜索目录设置

图 6 资源编译器搜索目录设置
在Linker Setting中添加wxWidgets的lib库文件。

图 7 编译器链接库设置
wxWidgets框架封装了main()函数和消息循环,因此,开发者不需要编写main()函数,只需创建一个继承自wxApp的子类(如MyApp)并在MyApp.cpp中添加宏wxIMPLEMENT_APP(MyApp)即可创建一个可运行的wxWidgets应用程序。
宏wxIMPLEMENT_APP(MyApp)的功能是实现MyApp类的实例化并进入消息循环。在进入消息循环之前,wxWidgets会调用MyApp::OnInit()。您通常需要重写该函数并在其中添加程序的程序初始化和创建主窗口(如wxFrame、wxDialog及其派生类)功能,窗口上的控件一般作为主窗口类的指针型成员变量,在主窗口类的构造函数中初始化。
主窗口为一个或多个wxFrame或wxDialog的派生类。每个窗体可以包含一个或多个诸如wxPanel、wxSplitterWindow等窗口类、控件类或其子类的实例。
wxFrame窗体还可以有一个菜单栏wxMenuBar、一个工具栏wxToolBar、一个状态栏wxStatusBar,以及一个wxIcon对象用于设置窗体的图标。
而对话框类wxDialog实例也可以用于放置控件,可以独立于窗口wxFrame而单独创建。
事件处理方面,wxWidgets方式与Windows的MFC框架非常相似,采用宏的方式定义事件。在窗体类的头文件中添加wxDECLARE_EVENT_TABLE(),在实现文件中添加wxBEGIN_EVENT_TABLE() 和wxEND_EVENT_TABLE()即可引入事件机制。
下面示例代码是一个可运行的wxWidgets程序的全部代码:
// 引入头文件
// wxprec.h会根据setup.h中的预编译设置引入适当的头文件
#include <wx/wxprec.h>
// 并非所有平台都支持wxprec.h,针对这些平台,需要引入wx.h头文件
#ifndef WX_PRECOMP
#include <wx/wx.h>
#endif
// 定义应用程序类,wxWidgets中的应用程序入口不是函数,而是wxApp的派生类
class MyApp: public wxApp{
public:
virtual bool OnInit();
};
// 定义主窗体类
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);
wxDECLARE_EVENT_TABLE();
};
// 定义控件ID
enum{
ID_Hello = 1
};
// 关联事件与事件处理例程,类似MFC中的事件表
wxBEGIN_EVENT_TABLE(MyFrame, wxFrame)
EVT_MENU(ID_Hello, MyFrame::OnHello)
EVT_MENU(wxID_EXIT, MyFrame::OnExit)
EVT_MENU(wxID_ABOUT, MyFrame::OnAbout)
wxEND_EVENT_TABLE()
// 实例化MyApp类并在main()中启用MyApp
wxIMPLEMENT_APP(MyApp);
// 应用程序用户初始化函数,您通常会在这里做一些初始化操作以及创建主窗体
bool MyApp::OnInit(){
MyFrame *frame = new MyFrame( "Hello World", wxPoint(50, 50), wxSize(450, 340) );
frame->Show( true );
return true;
}
// 窗体初始化
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... Ctrl-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!" );
// wxWidgets支持静态和动态两种事件机制,下面的方式为动态事件机制
//Bind(wxEVT_MENU, &MyFrame::OnHello, this, ID_Hello);
//Bind(wxEVT_MENU, &MyFrame::OnAbout, this, wxID_ABOUT);
//Bind(wxEVT_MENU, &MyFrame::OnExit, this, wxID_EXIT);
}
// 退出事件响应函数
void MyFrame::OnExit(wxCommandEvent& event){
Close( true );
}
// “关于”菜单响应函数
void MyFrame::OnAbout(wxCommandEvent& event){
wxMessageBox( "This is a wxWidgets' Hello world sample",
"About Hello World", wxOK | wxICON_INFORMATION );
}
// 自定义菜单ID_Hello的响应函数
void MyFrame::OnHello(wxCommandEvent& event){
wxLogMessage("Hello world from wxWidgets!");
}
尽管上面的代码没有实现什么有用的功能,但是它引入了一些重要的概念,并解释了如何编写一个可运行的wxWidgets应用程序。
首先,在程序中引入wxWidgets的头文件。头文件可以按需逐个引用,也可以引用一个全局的<wx/wx.h>,它引用了大多数应用中需要经常用到的头文件。对于支持预编译头WX_PRECOMP的平台,<wx/wxprec.h>已包含<wx/wx.h>,因此我们只将其包含在其他平台上:
#include <wx/wxprec.h>
#ifndef WX_PRECOMP
#include <wx/wx.h>
#endif
每个wxWidgets应用程序都始于定义一个从wxApp派生的新类并重写虚拟方法OnInit(),该函数相当于程序的入口函数。通常在函数中编写程序初始化代码,例如创建一个新的主窗口。
class MyApp: public wxApp{
public:
virtual bool OnInit();
};
主窗口一般是从wxFrame或wxDialog派生。示例MyFrame派生至wxFrame,并在其构造函数中创建菜单栏和状态栏。
class MyFrame: public wxFrame{
public:
MyFrame(const wxString& title, const wxPoint& pos, const wxSize& size);
private:
//这些事件处理例程不必定义为virtual或public
void OnHello(wxCommandEvent& event);
void OnExit(wxCommandEvent& event);
void OnAbout(wxCommandEvent& event);
wxDECLARE_EVENT_TABLE();
};
在wxFrame的声明中使用宏wxDECLARE_EVENT_TABLE()来声明一个静态事件表来增加事件处理功能,以响应菜单事件。对事件的响应是通过事件处理函数实现。在示例中,我们对三个菜单项做出响应,一个是自定义的菜单命令,另外两个是标准的“退出”和“关于”命令。
为了能够对菜单项命令作出响应,需要给每一个菜单项设置唯一标识符,该标识符可以定义为一个常量或枚举值。而枚举值是一种常用的方式,它可以自动编号,在需要定义许多标识符的情况,这样可以省去您去重的工作。通过枚举定义标识符的示例为:
enum{
ID_Hello = 1
};
您不需要为所有的菜单项都定义一个标识符,类似“About”和“Exit”这样的通用事件有自己默认的标识符,可以在程序中直接使用。
如果控件或菜单项以wxID_ANY为标识符,则将为指定类型的所有事件调用该的处理函数。可以在事件表中为所有菜单命令或按钮命令等添加一个事件处理项。事件处理例程通过wxEvent对象来判断事件源。
所有系统事件都有自己的宏。菜单事件宏为EVT_MENU。
wxBEGIN_EVENT_TABLE(MyFrame, wxFrame)
EVT_MENU(ID_Hello, MyFrame::OnHello)
EVT_MENU(wxID_EXIT, MyFrame::OnExit)
EVT_MENU(wxID_ABOUT, MyFrame::OnAbout)
wxEND_EVENT_TABLE()
与所有其他C/C++程序一样,wxWidgets真正的运行入口依然是main()函数。在wxWidgets下,main()函数在宏wxIMPLEMENT_APP中定义并实现,它创建了一个MyApp实例并调用其上的OnInit()来启动wxWidgets程序。因此,所有的程序都必须添加以下代码,否则程序将无法运行。
wxIMPLEMENT_APP(MyApp)
OnInit()的任务是完成程序初始化,例如,显示一个“启动画面”,创建一个或多个主窗体等。通常,主窗口构造函数有标题、位置和大小等参数。返回true表示初始化成功。
bool MyApp::OnInit(){
MyFrame *frame = new MyFrame("hw",wxPoint(50, 50),wxSize(450, 340) );
frame->Show( true );
return true;
}
示例窗体的构造函数创建了一个菜单栏和状态栏。并通过SetMenuBar()和CreateStatusBar()函数将其与窗体关联。
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... Ctrl-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!" );
}
以下是标准的事件处理例程。OnExit()通过调用Close()关闭主窗口。参数true表示其他窗口没有否决权,例如在询问“是否确实要关闭?”。如果没有其他主窗口,应用程序将退出。
void MyFrame::OnExit(wxCommandEvent& event){
Close( true );
}
OnAbout()将显示一个小窗口,其中包含一些文本。
void MyFrame::OnAbout(wxCommandEvent& event){
wxMessageBox( "This is a wxWidgets' Hello world sample",
"About Hello World", wxOK | wxICON_INFORMATION );
}
自定义菜单命令处理例程可以添加任何功能,示例中,我们简单地显示一个消息框:
void MyFrame::OnHello(wxCommandEvent& event){
wxLogMessage("Hello world from wxWidgets!");
}
下面将演示如何使用CodeBlocks创建和运行该实例。和其它IDE一样,首先创建工程,通过菜单File->New->Project,弹出工程模板界面,可以有两种选择,一是Empty Project,二是wxWidgets Project。本教程采用第一种方式创建示例工程。两种方式在功能是等价的,没有优劣之分。

图 8 选择工程模板

图 9 设置工程参数

图 10 设置编译参数

图 11 空白工程创建成功
选中空白工程,菜单”Project->properties”,设置生成目标的配置,设置Build targets类型为”GUI appliction”。

图 12 设置程序类型
菜单“File->New->Class”新建MyApp类,继承至wxApp。

图 13 新建类
在代码MyApp.h中,添加OnInit()方法,并将
#include <wx/wx.h>
替换为
#include <wx/wxprec.h>
#ifndef WX_PRECOMP
#include <wx/wx.h>
#endif
按上述方法继续创建MyFrame窗口类,继承至wxFrame。在头文件中添加引用文件#include <wx/wx.h>。
将演示代码拷贝至对应的文件中,编译运行结果如下:

图 14 Hello World示例图
在工程开发中,界面设计可借助可视化工具。CodeBlocks的插件wxSmith就是一款优秀的wxWidgets界面设计工具,另一款免费的工具wxFormBuilder功能也非常强大。对于习惯于VC++、C#或Java的程序员来讲,重新接受wxSmith/wxFormBuilder的使用习惯可能需要一定的时间,但只要接受并认同它的思想,就会发现它们与您之前遇到的可视化工具相比,尽管风格迥异,但同样好用。
本系列教程的重点是让您能够有机会接触wxWidgets的基本原理,同时具备使用C++语言开发跨平台GUI程序的能力。因此,后续所有文章示例均以代码方式构建,不使用可视化设计工具。但在实际的软件开发中,您应该借助可视化工具来提高开发效率。