# wxWidgets 新手教學
###### tags: `C++` `wxWidgets` `blog 文章`
[TOC]
## 安裝
1. 安裝 Visual Studio 2017/2019 Community/Professional.
2. 從 wxWidgets 官網下載 release 版的 source code。
* 建議不要下載 binary。因為比較能控制我們的選項。
3. 解壓/安裝 source code。假設我們安裝在 `D:\wxWidgets-3.1.4`。
4. 打開 `D:\wxWidgets-3.1.4\build\msw` 目錄中、對應你安裝的 VS 的 `.sln`。例如 2017 就是 `wx_vc15.sln`,2019 就是 `wx_vc16.sln`。
5. 方案打開之後,可以選擇「方案平台 (platform)」和「方案組態 (configuration)」。
* 「平台」中有 Win32 (32-bit) 和 x64 (64-bit) 兩種。
如果你以後要寫的 app 只會有 32-bit or 64-bit 的話,可以只編譯其中一種。但如果都有可能,就必須兩種都編譯。
* 「組態」分為 "Release", "Debug", "DLL Release" 和 "DLL Debug" 四種。
"DLL" 開頭的指的是以後 release 出去的執行檔需要有 **wxWidgets 的 DLL** 才能執行。通常我不會這麼做,所以 "DLL" 開頭的我不編譯。
而另外兩個,就分別是你的 app 編 debug 或 release 時會用到的函式庫,應該是兩個都用得到。
因此,依照你的需求,你需要編譯 2 次(Win32 和 x64 擇一、Debug 和 Release 都編),或是 4 次(Win32/Release, Win32/Debug, x64/Release, x64/Debug)。
但在開始編譯前,我們還要考慮一件事情....
6. Visual Studio 中有一個選項,可以決定 C/C++ 的 run-time library 要採用「靜態連結」還是「動態連結」。如果是「動態連結」的話,那麼使用你的 app 的人,就必須在他的系統中安裝對應的「可轉發套件」。我覺得如果是要 release 給其他人用的工具,「動態連結」就會造成不必要的麻煩。因此通常我會這麼做:
* 對於 Debug 版本,反正在只在我的機器上跑,就維持「動態連結」。
* 對於 Release 版本,改成「靜態連結」。
這個設定不只要在 app 中設定,wxWidgets 的函式庫也必須要有對應的設定。因此在編譯之前,我們要手動把 release 版的連結方式改成靜態的。
1. 在「方案總管」中,選取 *除了 _custom_build 以外* 的所有專案。
2. 在選取的專案上按右鍵,選擇「屬性」,打開「屬性頁」對話視窗。
3. 首先要確認「組態」(這裡應該會是 *Release*)和「平台」是不是我們想要修改的對象。如果沒錯的話,就切到 「C/C++ -> 程式碼產生」這個頁面。
4. 此時「執行階段程式庫」中的設定預設應該會是「多執行緒 DLL (/MD)」。請把它改成「多執行緒 (/MD)」。
5. 按「套用」。
6. 如果你 Win32 和 x64 兩個平台都要編譯的話,記得要再選另一個平台,重覆 3~5。
7. 按「確定」離開。
7. 開始編譯。
1. 選擇你想編譯的「方案組態」和「方案平台」後,選擇「建置 -> 建置方案」。
2. 編譯完成後,再選擇另一個「組態/平台」組合,再選擇「建置 -> 建置方案」。直到所有要編譯的組合都完成。
8. 其實基本上安裝已經完成了。不過為了之後專案的設定方便,我建議再新增一個叫做 WXWIN 的環境變數,指向 wxWidgets 所在的根目錄。
1. 從「開始」,開啟「設定」。
2. 在「尋找設定」中輸入「環境」,應該會找到「編輯系統環境變數」和「編輯您的帳戶的環境變數」等兩個選項。可以只編自己帳戶的就好(比較不需要特別權限)。
3. 按下「新增」後,跳出「新增使用者變數」的視窗。變數名稱輸入 **WXWIN**,變數值輸入 wxWidgets 所在的目錄。以我的例子即為 **D:\wxWidgets-3.1.4**(最後面不需要反斜線)。
* 如果怕輸入錯誤的話,可以按「瀏覽目錄...」選取。
4. 確定離開。
## 開新專案及專案設定
以下以 Visual Studio 2019 為例。
1. 開啟 Visual Studio 2019,點選「建立新的專案」。
2. 在範本中,選取「Windows 傳統式精靈」。
* 要從「Windows 傳統型應用程式」開始選也是 OK,但到時候要手動刪除一些不需要的東西。
* 要從「空白專案」開始也可以,不過下面會提到的的「子系統」設定要手動調整。
3. 輸入想要的專案名稱,以及放置專案的位置。
4. 在「Windows 桌面專案」視窗中,「應用程式類型」請選擇「**桌面應用程式 (.exe)**」;點選下方的「**空白專案**」。點選「完成」。
5. 主畫面開啟後,從主選單中選取「專案 -> 屬性」。
* 以下的設定,除非特別指定,否則 Debug/Win32、Debug/x64、Release/Win32、Release/x64 等都必須要做。
6. 首先,選取「連結器 -> 系統」,確認「子系統」的設定是 **Windows (/SUBSYSTEM:WINDOWS)** 。
7. 接著我們要設定 .h 和 .lib 的路徑,compiler 才不會找不到。
1. 點選左邊的「VC++ 目錄」。
2. 點選右邊的「Include 目錄」,將下列兩個路徑加進去:
```
$(WXWIN)\include
$(WXWIN)\include\msvc
```
可以觀察下面的「評估值」,應該會把 `$(WXWIN)` 展開成 `D:\wxWidgets-3.1.4`,這表示我們的環境變數設定沒有問題。
3. 按「確定」離開。
4. 點選「程式庫目錄」。依照平台是 Win32 或是 x64,把不同的目錄加進去:
* Win32: 加入 `$(WXWIN)\lib\vc_lib`。
* x64: 加入 `$(WXWIN)\lib\vc_x64_lib`。
5. 按「確定」離開。
8. 最後,這個設定雖然和 wxWidgets 無關,但建議把「DPI 感知」打開。這樣在高 DPI 的螢幕上,UI 才不會看起來糊糊的。
1. 點選左邊的「資訊清單工具 -> 輸入與輸出」。
2. 右的「DPI 感知」,改成「高 DPI 感知」或是「以螢幕為基礎的高 DPI 感知」(後者的意思其實是在多螢幕的機器上,針對不同的螢幕的 DPI 做不同的設定。)
## 最小專案
* MyApp.h
```cpp
#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
```
* MyApp.cpp
```cpp
#include "MyApp.h"
#include "MyFrame.h"
wxIMPLEMENT_APP(MyApp);
bool MyApp::OnInit(void) {
MyFrame *frame = new MyFrame("Hello");
frame->Show();
return true;
}
```
* MyFrame.h
```cpp
#ifndef __MY_FRAME_H__
#define __MY_FRAME_H__
#include <wx/wx.h>
class MyFrame : public wxFrame {
public:
MyFrame(const wxString& title);
};
#endif
```
* MyFrame.cpp
```cpp
#include "MyFrame.h"
MyFrame::MyFrame(const wxString& title) :
wxFrame(nullptr, wxID_ANY, title) {
// 示範如何在視窗中加入元件。
// 不算在「最小」的專案中。
wxStaticText* message = new wxStaticText(this, wxID_ANY, "...world.");
}
```
## Event handling
wxWidgets 有三種不同處理 event 的方法。
1. Event table
2. 利用 `Connect()` 動態管理
3. 利用 `Bind<>()` 動態管理
其中 `Connect()` 因為彈性沒有 `Bind<>()` 大,因此已經不再建議使用,在此就不多做介紹。(官方的 Event Handler 技術文件已經不再提這個方法了)。
### Event Table
首先,在要處理事件的 class 宣告中,加入 `wxDECLARE_EVENT_TABLE()` 。
:::warning
只有繼承自 `wxEventHandler` 的類別,才能處理事件,也才能加入 `wxDECLARE_EVENT_TABLE()`。`wxWindow` 就是繼承自 `wxEventHandler`,因此所有的 GUI 元件都可以處理事件。
:::
* MyFrame.h
```cpp=
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:
* MyFrame.cpp
```cpp=
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:
* MyFrame.cpp
```cpp=
// 注意這些 macros 後面都不需要加分號,加了會 build 不過
wxBEGIN_EVENT_TABLE(MyFrame, wxFrame)
EVT_BUTTON(ID_BUTTON1, MyFrame::OnButton_Clicked)
wxEND_EVENT_TABLE()
```
當然最後還是要實作 event handler:
* MyFrame.cpp
```cpp=
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.cpp
```cpp=
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.cpp
```cpp=
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
## 關於 Sizer
wxWidgets 提供了一系列從 [`wxSizer`](https://docs.wxwidgets.org/3.0/classwx_sizer.html) 衍生的類別,用來排列、縮放元件,控制所有元件的布局。
一般我們不會直接使用 `wxSizer` 建立物件,而是使用衍生類別。常用的 sizer 類別有:
* `wxBoxSizer` - 以一個方向(水平或垂直)排列元件。
* `wxGridSizer` - 以棋盤方式排列元件。每個格子的大小都一樣。
* `wxFlexGridSizer` - 也是格狀的,但每一欄/每一列的寬度/高度都可以自由調整。
* `wxGridBagSizer` - 以 `wxFlexGridSizer` 為基礎,加入可以任意指定元件要放在哪個特定欄列的功能。另外,格子也可以跨欄或跨列。
另外還有一些有特殊功能的 sizer,例如 `wxStaticBoxSizer` 等。
### wxBoxSizer 的使用範例
* MyFrame.cpp
```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 行:
1. `wxSizer` 物件 set 給 `wxWindow` 後,其生命週期會由 `wxWindow` 管理,因此要用 `new` 的方式建立。
2. `wxBoxSizer` 的建構子需要一個參數,而且必須是 `wxVERTICAL` 或 `wxHORIZONTAL`,二擇一。因為我們希望元件是垂直排列的,所以這裡給的參數是 `wxVERTICAL`。
* 13~17 行:
1. 使用 `wxSizer::Add()` 將元件加入。
2. 在早期,元件加入時要給定的屬性(例如哪幾邊要有 border、border 寬度多少...等)是直接以各別參數的方式傳遞給 `wxSizer::Add()` 的。後來加入了 `wxSizerFlags` 這個類別,可以用串連的方式呼叫修改內容的成員函式,可讀性較佳。建議使用這樣的做法。
3. 因為範例中的 `wxBoxSizer` 排列方式為垂直,因此 alignment 相關的設定,只能是「左、中、右」(例如這個範例中的 `Align(wxCENTER)` 和 `Right()`)。若是使用了「上、中、下」的設定,在執行時會出現 assertion error(如果是 debug build 的話),然後設定也沒有作用。
4. `Proportion()` 表示 sizer 若是以設定的方向(例如這個例子是垂直)縮放時,所有的元件要以怎樣的比例跟著縮放。0 表示不縮放(固定大小)。所有設定值為 1 以上的元件,則依 proportion 的比例縮放。
5. `Expand()` 算是 "alignment" 的一種。這個例子的元件是以垂直排列,因此設定 `Expand()` 的元件,在寬度方面會填滿整個 sizer 所占的大小。
* 21~26 行:
1. Sizer 設定完後最重要的就是要用 `wxWindow::SetSizer()`(注意不是 `wxWindow::SetSize()`)讓視窗知道要把縮放資訊傳給這個 sizer。
2. 如果希望有些元件保持最小尺寸的話,可以再呼叫 `wxSizer::SetSizeHint()`。
3. 上面兩件事常常會一起做,因此多了一個 `wxWindow::SetSizerAndFit()`,一次搞定。
### 其他補充
* 若是要在兩個元件之間塞入可變大小的空間(例如:希望有兩個 button,一個靠左、一個靠右,然後視窗拉大時它們的大小不要變),可以利用 `wxSizer::AddStretchSpacer()`。若大小固定,也有 `wxSizer::AddSpacer()` 可以用。
## 其他常用的類別/函式
* `::wxMessageBox()`
* `wxRegConfig` / `wxFileConfig`(或直接使用 `wxConfig`)
* `wxLogWindow`(或 `wxLog` 相關類別)
* `wxScrolledWindow`