Try   HackMD

wxWidgets 新手教學

tags: C++ wxWidgets blog 文章

安裝

  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)。

    但在開始編譯前,我們還要考慮一件事情

  1. 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. 按「確定」離開。
  2. 開始編譯。

    1. 選擇你想編譯的「方案組態」和「方案平台」後,選擇「建置 -> 建置方案」。
    2. 編譯完成後,再選擇另一個「組態/平台」組合,再選擇「建置 -> 建置方案」。直到所有要編譯的組合都完成。
  3. 其實基本上安裝已經完成了。不過為了之後專案的設定方便,我建議再新增一個叫做 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
    ​​​​#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
    ​​​​#include "MyApp.h"
    
    ​​​​#include "MyFrame.h"
    
    ​​​​wxIMPLEMENT_APP(MyApp);
    
    ​​​​bool MyApp::OnInit(void) {
    ​​​​  MyFrame *frame = new MyFrame("Hello");
    
    ​​​​  frame->Show();
    
    ​​​​  return true;
    ​​​​}
    
  • MyFrame.h
    ​​​​#ifndef __MY_FRAME_H__
    ​​​​#define __MY_FRAME_H__
    
    ​​​​#include <wx/wx.h>
    
    ​​​​class MyFrame : public wxFrame {
    ​​​​ public:
    ​​​​  MyFrame(const wxString& title);
    ​​​​};
    
    ​​​​#endif
    
  • MyFrame.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()

只有繼承自 wxEventHandler 的類別,才能處理事件,也才能加入 wxDECLARE_EVENT_TABLE()wxWindow 就是繼承自 wxEventHandler,因此所有的 GUI 元件都可以處理事件。

  • MyFrame.h
    ​​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
    ​​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
    ​​// 注意這些 macros 後面都不需要加分號,加了會 build 不過 ​​wxBEGIN_EVENT_TABLE(MyFrame, wxFrame) ​​ EVT_BUTTON(ID_BUTTON1, MyFrame::OnButton_Clicked) ​​wxEND_EVENT_TABLE()

當然最後還是要實作 event handler:

  • MyFrame.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
    ​​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 ​​ // 物件。 ​​ //... ​​}

因為我們是直接執行 button1Bind<>(),因此就不需要特別指定 Window ID 了。

在 wxWidgets 的事件處理機制中,所有繼承自 wxCommandEvent 的事件,若是本身的物件沒有處理的話,就會把它往上丟到它的 parent。因此,像是這個例子中 button1wxEVT_BUTTON 事件,我們也可以透過 MyFrame 物件來擷取。不過 MyFrame 中可能會有很多會發出 wxEVT_BUTTON 的物件,因此此時就必須要指定 ID 了:

  • MyFrame.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 衍生的類別,用來排列、縮放元件,控制所有元件的布局。

一般我們不會直接使用 wxSizer 建立物件,而是使用衍生類別。常用的 sizer 類別有:

  • wxBoxSizer - 以一個方向(水平或垂直)排列元件。
  • wxGridSizer - 以棋盤方式排列元件。每個格子的大小都一樣。
  • wxFlexGridSizer - 也是格狀的,但每一欄/每一列的寬度/高度都可以自由調整。
  • wxGridBagSizer - 以 wxFlexGridSizer 為基礎,加入可以任意指定元件要放在哪個特定欄列的功能。另外,格子也可以跨欄或跨列。

另外還有一些有特殊功能的 sizer,例如 wxStaticBoxSizer 等。

wxBoxSizer 的使用範例

  • 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 行:

    1. wxSizer 物件 set 給 wxWindow 後,其生命週期會由 wxWindow 管理,因此要用 new 的方式建立。
    2. wxBoxSizer 的建構子需要一個參數,而且必須是 wxVERTICALwxHORIZONTAL,二擇一。因為我們希望元件是垂直排列的,所以這裡給的參數是 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