C++
wxWidgets
blog 文章
安裝 Visual Studio 2017/2019 Community/Professional.
從 wxWidgets 官網下載 release 版的 source code。
解壓/安裝 source code。假設我們安裝在 D:\wxWidgets-3.1.4
。
打開 D:\wxWidgets-3.1.4\build\msw
目錄中、對應你安裝的 VS 的 .sln
。例如 2017 就是 wx_vc15.sln
,2019 就是 wx_vc16.sln
。
方案打開之後,可以選擇「方案平台 (platform)」和「方案組態 (configuration)」。
「平台」中有 Win32 (32-bit) 和 x64 (64-bit) 兩種。
如果你以後要寫的 app 只會有 32-bit or 64-bit 的話,可以只編譯其中一種。但如果都有可能,就必須兩種都編譯。
因此,依照你的需求,你需要編譯 2 次(Win32 和 x64 擇一、Debug 和 Release 都編),或是 4 次(Win32/Release, Win32/Debug, x64/Release, x64/Debug)。
但在開始編譯前,我們還要考慮一件事情…
Visual Studio 中有一個選項,可以決定 C/C++ 的 run-time library 要採用「靜態連結」還是「動態連結」。如果是「動態連結」的話,那麼使用你的 app 的人,就必須在他的系統中安裝對應的「可轉發套件」。我覺得如果是要 release 給其他人用的工具,「動態連結」就會造成不必要的麻煩。因此通常我會這麼做:
這個設定不只要在 app 中設定,wxWidgets 的函式庫也必須要有對應的設定。因此在編譯之前,我們要手動把 release 版的連結方式改成靜態的。
開始編譯。
其實基本上安裝已經完成了。不過為了之後專案的設定方便,我建議再新增一個叫做 WXWIN 的環境變數,指向 wxWidgets 所在的根目錄。
以下以 Visual Studio 2019 為例。
開啟 Visual Studio 2019,點選「建立新的專案」。
在範本中,選取「Windows 傳統式精靈」。
輸入想要的專案名稱,以及放置專案的位置。
在「Windows 桌面專案」視窗中,「應用程式類型」請選擇「桌面應用程式 (.exe)」;點選下方的「空白專案」。點選「完成」。
主畫面開啟後,從主選單中選取「專案 -> 屬性」。
首先,選取「連結器 -> 系統」,確認「子系統」的設定是 Windows (/SUBSYSTEM:WINDOWS) 。
接著我們要設定 .h 和 .lib 的路徑,compiler 才不會找不到。
$(WXWIN)\include
$(WXWIN)\include\msvc
$(WXWIN)
展開成 D:\wxWidgets-3.1.4
,這表示我們的環境變數設定沒有問題。$(WXWIN)\lib\vc_lib
。$(WXWIN)\lib\vc_x64_lib
。最後,這個設定雖然和 wxWidgets 無關,但建議把「DPI 感知」打開。這樣在高 DPI 的螢幕上,UI 才不會看起來糊糊的。
#ifndef __MY_APP_H__
#define __MY_APP_H__
#include <wx/wx.h>
class MyApp : public wxApp {
public:
bool OnInit(void) override;
};
wxDECLARE_APP(MyApp);
#endif
#include "MyApp.h"
#include "MyFrame.h"
wxIMPLEMENT_APP(MyApp);
bool MyApp::OnInit(void) {
MyFrame *frame = new MyFrame("Hello");
frame->Show();
return true;
}
#ifndef __MY_FRAME_H__
#define __MY_FRAME_H__
#include <wx/wx.h>
class MyFrame : public wxFrame {
public:
MyFrame(const wxString& title);
};
#endif
#include "MyFrame.h"
MyFrame::MyFrame(const wxString& title) :
wxFrame(nullptr, wxID_ANY, title) {
// 示範如何在視窗中加入元件。
// 不算在「最小」的專案中。
wxStaticText* message = new wxStaticText(this, wxID_ANY, "...world.");
}
wxWidgets 有三種不同處理 event 的方法。
Connect()
動態管理Bind<>()
動態管理其中 Connect()
因為彈性沒有 Bind<>()
大,因此已經不再建議使用,在此就不多做介紹。(官方的 Event Handler 技術文件已經不再提這個方法了)。
首先,在要處理事件的 class 宣告中,加入 wxDECLARE_EVENT_TABLE()
。
只有繼承自 wxEventHandler
的類別,才能處理事件,也才能加入 wxDECLARE_EVENT_TABLE()
。wxWindow
就是繼承自 wxEventHandler
,因此所有的 GUI 元件都可以處理事件。
class MyFrame : public wxFrame {
//...
private:
// event handler
void OnButton1_Clicked(wxCommandEvt& evt);
// 宣告 event table
wxDECLARE_EVENT_TABLE()
};
如果我們要處理的事件,是由某個子元件(或是選單)發出來的,那麼該元件在建立的時候,就必須要有特定的 ID(因為 event table 需要給 ID)。建議可以利用 wxWindow::NewControlId()
產生,才不會重覆。例如我們可以在 MyFrame.cpp 的 global 作用域中定義我們要操作的 button 需要用到的 ID:
const wxWindowID ID_BUTTON1 = wxWindow::NewControlId();
// 在建立 button 時,記得要設定這個 ID,而不是用 wxID_ANY
MyFrame::MyFrame(/*...*/) {
//...
wxButton *button1 = new wxButton(this, ID_BUTTON1, "Press me!");
//...
}
接著,在 .cpp 的 global 區域中,加入 event table:
// 注意這些 macros 後面都不需要加分號,加了會 build 不過
wxBEGIN_EVENT_TABLE(MyFrame, wxFrame)
EVT_BUTTON(ID_BUTTON1, MyFrame::OnButton_Clicked)
wxEND_EVENT_TABLE()
當然最後還是要實作 event handler:
void MyFrame::OnButton1_Clicked(wxCommandEvt& evt) {
// do something...
}
這樣 button1 被按下去的時候,就會跳到 OnButton1_Clicked()
去執行了。
這個方法在 Bind<>()
實作出來之前算是標準作法,因此很多現有的專案都可以看到這個做法。不過現在有了 Bind<>()
,可以完全不用 event table,只用 Bind<>()
處理事件;也可以同時混用兩種方法。看哪種方便(同時也要考慮可讀性)。
Bind<>()
動態指定 Event Handler和 event table 的方式相比,使用 Bind<>()
不需要在 class 宣告中使用 wxDECLARE_EVENT_TABLE()
,也不需要在實作處利用 wxBEGIN_EVENT_TABLE()
和 wxEND_EVENT_TABLE()
建立 event table。但 event handler 還是需要建立的。至於特定的 Window ID,也不是一定需要的。
在元件建立之後,就可以利用 Bind<>()
指定事件發生時要執行的 event handler:
MyFrame::MyFrame(/*...*/) {
//...
wxButton *button1 = new wxButton(this, ID_BUTTON1, "Press me!");
button1->Bind(wxEVT_BUTTON, // 指定要處理的事件類別
&MyFrame::OnButton1_Clicked, // event handler
this); // 因為這裡的 event handler 是成員函式,
// 因此必須要指定是由哪個物件去執行。在這個
// 例子之中,就是 this 所指向的 MyFrame
// 物件。
//...
}
因為我們是直接執行 button1
的 Bind<>()
,因此就不需要特別指定 Window ID 了。
在 wxWidgets 的事件處理機制中,所有繼承自 wxCommandEvent
的事件,若是本身的物件沒有處理的話,就會把它往上丟到它的 parent。因此,像是這個例子中 button1
的 wxEVT_BUTTON
事件,我們也可以透過 MyFrame
物件來擷取。不過 MyFrame
中可能會有很多會發出 wxEVT_BUTTON
的物件,因此此時就必須要指定 ID 了:
MyFrame::MyFrame(/*...*/) {
//...
wxButton *button1 = new wxButton(this, ID_BUTTON1, "Press me!");
Bind(wxEVT_BUTTON, // 此時呼叫到的是 MyFrame::Bind<>()。
&MyFrame::OnButton1_Clicked,
this,
ID_BUTTON1);
//...
}
使用Bind<>()
時,event handler 除了可以是成員函式外,也可以塞一般的全域函式、函式物件、匿名函式,甚至是 boost::function
。不過在大部份的情況下,我們收到事件後也是跟 MyFrame
的成員互動,因此以成員函式當作 event handler 的情況還是比較多,所以其他的應用就不多提。
關於 wxWidgets 事件處理的細節,可以參考官方的技術文件:https://docs.wxwidgets.org/3.0/overview_events.html
wxWidgets 提供了一系列從 wxSizer
衍生的類別,用來排列、縮放元件,控制所有元件的布局。
一般我們不會直接使用 wxSizer
建立物件,而是使用衍生類別。常用的 sizer 類別有:
wxBoxSizer
- 以一個方向(水平或垂直)排列元件。wxGridSizer
- 以棋盤方式排列元件。每個格子的大小都一樣。wxFlexGridSizer
- 也是格狀的,但每一欄/每一列的寬度/高度都可以自由調整。wxGridBagSizer
- 以 wxFlexGridSizer
為基礎,加入可以任意指定元件要放在哪個特定欄列的功能。另外,格子也可以跨欄或跨列。另外還有一些有特殊功能的 sizer,例如 wxStaticBoxSizer
等。
MyFrame.cpp
MyFrame::MyFrame(const wxString& title) /*...*/ {
// Create components
auto label_1 = new wxStaticText(/*...*/);
auto button_1 = new wxButton(/*...*/);
auto text_area =
new wxTextCtrl(this, wxID_ANY, "", wxDefaultPosition,
wxDefaultSize, wxTE_MULTILINE);
/*...*/
// Sizer
wxBoxSizer* main_sizer = new wxBoxSizer(wxVERTICAL);
main_sizer->Add(label_1,
wxSizerFlags().Border(wxALL, 5).Align(wxCENTER));
main_sizer->Add(button_1,
wxSizerFlags().Border(wxALL, 5).Right());
main_sizer->Add(text_area_,
wxSizerFlags().Border(wxALL, 5).Proportion(1).Expand());
// 下面兩種寫法其實是一樣的
#if 0
main_sizer->SetSizeHint(this);
SetSizer(main_sizer);
#else
SetSizerAndFit(main_sizer);
#endif
}
11 行:
wxSizer
物件 set 給 wxWindow
後,其生命週期會由 wxWindow
管理,因此要用 new
的方式建立。wxBoxSizer
的建構子需要一個參數,而且必須是 wxVERTICAL
或 wxHORIZONTAL
,二擇一。因為我們希望元件是垂直排列的,所以這裡給的參數是 wxVERTICAL
。13~17 行:
wxSizer::Add()
將元件加入。wxSizer::Add()
的。後來加入了 wxSizerFlags
這個類別,可以用串連的方式呼叫修改內容的成員函式,可讀性較佳。建議使用這樣的做法。wxBoxSizer
排列方式為垂直,因此 alignment 相關的設定,只能是「左、中、右」(例如這個範例中的 Align(wxCENTER)
和 Right()
)。若是使用了「上、中、下」的設定,在執行時會出現 assertion error(如果是 debug build 的話),然後設定也沒有作用。Proportion()
表示 sizer 若是以設定的方向(例如這個例子是垂直)縮放時,所有的元件要以怎樣的比例跟著縮放。0 表示不縮放(固定大小)。所有設定值為 1 以上的元件,則依 proportion 的比例縮放。Expand()
算是 "alignment" 的一種。這個例子的元件是以垂直排列,因此設定 Expand()
的元件,在寬度方面會填滿整個 sizer 所占的大小。21~26 行:
wxWindow::SetSizer()
(注意不是 wxWindow::SetSize()
)讓視窗知道要把縮放資訊傳給這個 sizer。wxSizer::SetSizeHint()
。wxWindow::SetSizerAndFit()
,一次搞定。wxSizer::AddStretchSpacer()
。若大小固定,也有 wxSizer::AddSpacer()
可以用。::wxMessageBox()
wxRegConfig
/ wxFileConfig
(或直接使用 wxConfig
)wxLogWindow
(或 wxLog
相關類別)wxScrolledWindow