# 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`