程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C++ >> C++入門知識 >> 用c++11封裝win32界面庫

用c++11封裝win32界面庫

編輯:C++入門知識

0. 前言

  你是否也和我一樣是一個業余c++玩家,經常用c++寫一些帶界面的小程序呢?每次都在vs裡用鼠標拖各種控件,然後copy / paste一大堆win32的api?沒用過mfc,wtl,qt,只用sdk? 本文不是介紹各api的用法,而是用抽象的方法來對這堆api進行封裝,弄一個界面庫方便自己使用,當然前提是對這些api有基本的了解。

  之前看過些界面庫源碼,尤其是egui,好多東西都是從它那學來的。這些庫大多用到其他第三方的庫,比如boost,一個原因是當時c++自帶語法不完善,比如沒有shared_ptr,lambda,functional, 現在的c++11包含了這大部分東西,也就不需要第三方庫了,但需要較新的編譯器。用vs編譯的話,最低版本是vs2012+update 1 CTP 補丁。
 
 


1. 介紹

  就稱這界面庫叫 _gui 吧,整個 _gui 可以分為以下幾部分

  1. thunk  用來把wnd_proc這種回調函數封裝到class內部

  2. property 類似vb的屬性,比如要把窗口灰掉(disable)


[cpp]
win->enabled = false; 

 win->enabled = false;
  3. event  事件,如按鈕點擊


[cpp]
btn->event.click += []() { cout << "button clicked" << endl; }; 

btn->event.click += []() { cout << "button clicked" << endl; };  4. initor 創建時初始化,比如 


[cpp]
wnd<edit> edt_psw = new<edit>().text('admin').size(200,30).password_type(true); 

wnd<edit> edt_psw = new<edit>().text('admin').size(200,30).password_type(true);  5. layout 布局

  如下圖的垂直分割布局,拖動中間那條分隔條可以改變左右大小

先看個例子吧

 

\

 

 

2. thunk

  win32的窗口消息都是發送給該窗口類的wnd_proc,注冊窗口類時都是給個全局函數:


[cpp]
WNDCLASS cls; 
... 
cls.lpfnWndProc = wnd_proc; // 則所有該類窗口的所有消息都會發送到這個 wnd_proc 

WNDCLASS cls;
...
cls.lpfnWndProc = wnd_proc; // 則所有該類窗口的所有消息都會發送到這個 wnd_proc但如果封裝控件的話就有個問題,比如我們希望button類被點擊時候執行 on_click() 成員函數

[cpp]
LRESULT button_proc(HWND, UINT msg, WPARAM, LPARAM) { 
    if(msg == WM_CLICK) 
        btn->on_click(); //無法調用,因為無法獲得btn是哪個實例  
}; 
struct button { 
    void on_click() {} 
}; 
 
// 除非這樣  
struct button { 
    void on_click() {} 
 
    LRESULT button_proc(HWND, UINT msg, WPARAM, LPARAM) { 
        if(msg == WM_CLICK) 
            this->on_click(); // 這樣就ok  
    }; 
}; 

LRESULT button_proc(HWND, UINT msg, WPARAM, LPARAM) {
 if(msg == WM_CLICK)
  btn->on_click(); //無法調用,因為無法獲得btn是哪個實例
};
struct button {
 void on_click() {}
};

// 除非這樣
struct button {
 void on_click() {}

 LRESULT button_proc(HWND, UINT msg, WPARAM, LPARAM) {
  if(msg == WM_CLICK)
   this->on_click(); // 這樣就ok
 };
};
thunk可以把上面的全局函數變成成員函數,先來看一下全局函數和成員函數的區別,調試時從反匯編可以看到

[cpp]
call global_func  //調用全局函數  
 
push ecx //對象指針,也就是 this  
call member_func  //調用成員函數 

call global_func  //調用全局函數

push ecx //對象指針,也就是 this
call member_func  //調用成員函數區別就是成員函數需要一個額外的this指針, 所以如果把 WNDCLASS.wnd_proc 指向一段內存,在這段內存裡做兩件事

1.  push ecx

2.  call member_func

就ok了, 這段內存就是thunk,用一個結構體來表示:


[cpp]
struct thunk_code { 
#pragma pack(push, 1) //取消默認的4字節對齊,pack後char,short只占1,2字節  
    unsigned short stub1;      // lea ecx, p_this  
    unsigned long  p_this;    
    unsigned char  stub2;      // mov eax,member_func  
    unsigned long  member_func;   
    unsigned short stub3;      // jmp eax  
#pragma pack(pop)  
    void init() { 
        stub1       = 0x0D8D; // lea ecx 的機器碼  
        p_this      = 0; 
        stub2       = 0xB8; // mov eax 的機器碼  
        member_func = 0; 
        stub3       = 0xE0FF; // jmp eax  
    } 
}; 
這段內存相當於執行了 
mov dword ptr [esp+4], p_this 
mov eax, member_func 
jmp eax 

struct thunk_code {
#pragma pack(push, 1) //取消默認的4字節對齊,pack後char,short只占1,2字節
 unsigned short stub1;      // lea ecx, p_this
 unsigned long  p_this;  
 unsigned char  stub2;      // mov eax,member_func
 unsigned long  member_func; 
 unsigned short stub3;      // jmp eax
#pragma pack(pop)
 void init() {
  stub1  = 0x0D8D; // lea ecx 的機器碼
  p_this  = 0;
  stub2  = 0xB8; // mov eax 的機器碼
  member_func = 0;
  stub3  = 0xE0FF; // jmp eax
 }
};
這段內存相當於執行了
mov dword ptr [esp+4], p_this
mov eax, member_func
jmp eax
(因為這段內存需要被執行,而如果直接 thunk_code code;  這個code是不可執行的,所以這裡用 HeapAlloc 分配 sizeof(thunk_code) 大小的內存,然後調用init()來填充,參考 thunk.h 和 heap.h)

剩下要做的事就創建控件實例時給 p_this 和 member_func 賦值了,以button為例


[cpp]
struct button { 
    thunk<button, LRESULT(HWND,DWORD,WPARAM,LPARAM)> wnd_thunk; 
 
    button() { 
        wnd_thunk.init(this, &button::wnd_proc); // 給 thunk 的 p_this 和 member_func 賦值  
    } 
 
    LRESULT wnd_proc(HWND hwnd, DWORD msg, WPARAM wp, LPARAM lp) { 
        if(msg == WM_CLICK) { 
            this->on_click(); 
        } 
    } 
    void on_click() {} 
    void create() { 
        CreateWindow("BUTTON", ...); 
 
        // 創建完後替換原 wnd_proc 為 thunk  
        ::SetWindowLong(hwnd, GWL_WNDPROC, wnd_thunk.addr()); 
    } 
}; 

struct button {
 thunk<button, LRESULT(HWND,DWORD,WPARAM,LPARAM)> wnd_thunk;

 button() {
  wnd_thunk.init(this, &button::wnd_proc); // 給 thunk 的 p_this 和 member_func 賦值
 }

 LRESULT wnd_proc(HWND hwnd, DWORD msg, WPARAM wp, LPARAM lp) {
  if(msg == WM_CLICK) {
   this->on_click();
  }
 }
 void on_click() {}
 void create() {
  CreateWindow("BUTTON", ...);

  // 創建完後替換原 wnd_proc 為 thunk
  ::SetWindowLong(hwnd, GWL_WNDPROC, wnd_thunk.addr());
 }
};

_gui的所有控件都是用的這種方式處理事件,所以thunk的初始化放在了基類 wnd_base 中(參考 wnd_base.h)

 

3 property

  操作屬性的通常做法是對外提供兩個接口 getter 和 setter,類似這樣


[cpp]
struct listview { 
    void set_title(string s) { SetWindowText(...); } 
    string get_title() { GetWindowText(...); } 
}; 

struct listview {
 void set_title(string s) { SetWindowText(...); }
 string get_title() { GetWindowText(...); }
};
把"屬性"的概念封裝起來就變成
[cpp]
struct listview { 
    property::rw<string> title; 
 
    listview() { 
        title.綁定(get_title, set_title); 
    } 
    void set_title(string s) { SetWindowText(...); } 
    string get_title() { GetWindowText(...); } 
}; 
 
wnd<listview> lv; 
sting s = lv->title; //會調用 get_title()  
lv->title = "new_title"; //會調用 set_title("new_title") 

struct listview {
 property::rw<string> title;

 listview() {
  title.綁定(get_title, set_title);
 }
 void set_title(string s) { SetWindowText(...); }
 string get_title() { GetWindowText(...); }
};

wnd<listview> lv;
sting s = lv->title; //會調用 get_title()
lv->title = "new_title"; //會調用 set_title("new_title")這樣對外只要訪問屬性 title 就好了,按權限控制可以分為 property::r  property::w  property::rw,是不是感覺好一些。

實現時只要重載兩個個操作符:


[cpp]
string s = lv->title; // 重載 operator string() { return getter(); }  
 
lv->title = "new_title"; // 重載 operator=(const string& s) { setter(s); } 

string s = lv->title; // 重載 operator string() { return getter(); }

lv->title = "new_title"; // 重載 operator=(const string& s) { setter(s); }
再看個復雜點,帶參數的情況。操作listview中(1,2)的單元格:


[cpp]
lv_item i = lv->item(1,2); // lv->item(1,2) 返回一個 r_helper, 重載r_helper::operator lv_item()   
lv->item(1,2) = lv_item("item_1_2"); // lv->item(1,2) 返回一個 r_helper,重載 r_helper 的 operator=(const lv_item& )  

lv_item i = lv->item(1,2); // lv->item(1,2) 返回一個 r_helper, 重載r_helper::operator lv_item()
lv->item(1,2) = lv_item("item_1_2"); // lv->item(1,2) 返回一個 r_helper,重載 r_helper 的 operator=(const lv_item& )
具體參數類型是用template,不定的參數個數是用 c++11 不定長模板解決,詳見 property.h 

 


4 event


[cpp]
btn->event.click += on_btn_click_1; 
btn->event.click += []() { cout << "button clicked" << endl; }; 
btn->event.click += bind(x::func, &x_obj); 

btn->event.click += on_btn_click_1;
btn->event.click += []() { cout << "button clicked" << endl; };
btn->event.click += bind(x::func, &x_obj);

有一點 .net 的味道,這樣用起來比較方便。 每個事件都是一個event_handler:


[cpp]
template<typename... _t> 
struct event_handler { 
    typedef function<void(_t...)> fn_t; 
    vector<fn_t> handlers; //每次 += 就放到這個vector中  
 
    void operator+=(fn_t f) { 
        handlers.push_back(f); 
    } 
    void operator()(_t... args) { // call的時候遍歷vector,每個call一遍  
        for(auto& h : handlers)  
            h(args...); 
    } 
}; 

template<typename... _t>
struct event_handler {
 typedef function<void(_t...)> fn_t;
 vector<fn_t> handlers; //每次 += 就放到這個vector中

 void operator+=(fn_t f) {
  handlers.push_back(f);
 }
 void operator()(_t... args) { // call的時候遍歷vector,每個call一遍
  for(auto& h : handlers)
   h(args...);
 }
};然後是各種 event_handler


[cpp]
namespace event { 
 
    struct base { 
        event_handler<pos_t&> move; 
        event_handler<size&> size; 
        event_handler<wnd_msg&> paint; 
        event_handler<bool> enable; 
        // ...  
 
        virtual void process_msg(wnd_msg& msg) { 
            switch(msg.type) { 
                case WM_MOVE:           move(pos(msg.lp.loword(), msg.lp.hiword())); break; 
                case WM_SIZE:           size(size(msg.lp.loword(), msg.lp.hiword())); break; 
                case WM_PAINT:          paint(msg); break; 
                case WM_ENABLE:         enable(!(msg.wp == 0)); break;  
                // ...  
            } 
        } 
    }; 

namespace event {

 struct base {
  event_handler<pos_t&> move;
  event_handler<size&> size;
  event_handler<wnd_msg&> paint;
  event_handler<bool> enable;
  // ...

  virtual void process_msg(wnd_msg& msg) {
   switch(msg.type) {
    case WM_MOVE:   move(pos(msg.lp.loword(), msg.lp.hiword())); break;
    case WM_SIZE:   size(size(msg.lp.loword(), msg.lp.hiword())); break;
    case WM_PAINT:   paint(msg); break;
    case WM_ENABLE:   enable(!(msg.wp == 0)); break;
    // ...
   }
  }
 };
}
每個類都有一個 event 成員


[cpp]
template<typename event_t = event::base> 
struct wnd_base : wnd32 { 
 
    event_t event; 
 
    virtual void process_msg(wnd_msg& msg) { 
        event.process_msg(msg); // thunk 把消息發送給 wnd_base::process_msg,這裡再調用event.process_msg  
    } 
}; 

template<typename event_t = event::base>
struct wnd_base : wnd32 {

 event_t event;

 virtual void process_msg(wnd_msg& msg) {
  event.process_msg(msg); // thunk 把消息發送給 wnd_base::process_msg,這裡再調用event.process_msg
 }
};

5 initor
常見的類設計是提供多個構造函數以支持不同的參數[cpp] view plaincopyprint?class window { 
    window() {} 
    window(string text) { ... } 
    window(string text, int w, int h) { ... } 
    window(string text, int w, int h, int x, int y) { ... } 
    ... 
}; 
 
window w("title", 100, 200, 300, 400);// 很容易記錯,到底 100,200是長寬,還是xy坐標?  

class window {
 window() {}
 window(string text) { ... }
 window(string text, int w, int h) { ... }
 window(string text, int w, int h, int x, int y) { ... }
 ...
};

window w("title", 100, 200, 300, 400);// 很容易記錯,到底 100,200是長寬,還是xy坐標?
所以有了 initor,或者叫 create_info, wnd_init, 用來存放創建信息,每個實例都保存一份initor, create() 的時候會去拿 initor 裡的各種信息(text, size...)


[cpp]
wnd<window> w = new_<button>().text("...").size(100, 200).pos(300, 400);// 這樣就不會錯了  
wnd<button> b = new_<button>("..."); // 其他創建方法  
wnd<label> l("..."); 

wnd<window> w = new_<button>().text("...").size(100, 200).pos(300, 400);// 這樣就不會錯了
wnd<button> b = new_<button>("..."); // 其他創建方法
wnd<label> l("...");為了支持鏈式賦值和擴展性,initor的設計感覺有點復雜,如果有更好的設計還請告之~

考慮以下代碼


[cpp]
initor().text("aa").visible(true);  // 如果要做到這點,我最開始是這樣設計這個 initor 類  
 
template<typename value_t, typename owner_t> 
struct attr { 
    value_t value; 
 
    owner_t* owner; //賦值後返回owner, 以便下一個鏈式賦值  
 
    owner_t& operator(const value_t& val) { 
        value = val; 
        return *owner; 
    } 
}; 
struct initor { 
    attr<string, initor> text; 
    attr<bool, initor> visible; 
 
    initor() { 
        text.owner = this; 
        visible.owner = this; 
    } 
}; 

initor().text("aa").visible(true);  // 如果要做到這點,我最開始是這樣設計這個 initor 類

template<typename value_t, typename owner_t>
struct attr {
 value_t value;

 owner_t* owner; //賦值後返回owner, 以便下一個鏈式賦值

 owner_t& operator(const value_t& val) {
  value = val;
  return *owner;
 }
};
struct initor {
 attr<string, initor> text;
 attr<bool, initor> visible;

 initor() {
  text.owner = this;
  visible.owner = this;
 }
};
後來發覺,這個initor是不可擴展的


[cpp]
struct checkbox_initor : initor { 
    attr<bool, checkbox_initor> checked; 
    checkbox_initor() { 
        checked.owner = this; 
    } 
}; 
 
checkbox_initor().checked(true).text("."); // 這樣ok  
checkbox_initor().text(".").checked(true); // 這樣不行,因為.text(".")返回一個initor基類,不具備checked 

struct checkbox_initor : initor {
 attr<bool, checkbox_initor> checked;
 checkbox_initor() {
  checked.owner = this;
 }
};

checkbox_initor().checked(true).text("."); // 這樣ok
checkbox_initor().text(".").checked(true); // 這樣不行,因為.text(".")返回一個initor基類,不具備checked
解決辦法是給基類 initor 加上模板參數


[cpp]
template<typename derive_t> 
struct initor { 
    attr<string, derive_t> text; 
    attr<pos_t, derive_t> pos; 
    ... 
}; 

template<typename derive_t>
struct initor {
 attr<string, derive_t> text;
 attr<pos_t, derive_t> pos;
 ...
};
詳見 initor.h

每種控件對應的initor,用traits來定義:


[cpp]
// wnd_traits 定義  
template<typename wnd_t> 
struct wnd_traits { 
    typedef initor::wnd initor_t; 
}; 
 
// 針對按鈕的特化  
struct button; 
 
template<> 
struct wnd_traits<button> { 
    typedef initor::button initor_t; 
}; 

// wnd_traits 定義
template<typename wnd_t>
struct wnd_traits {
 typedef initor::wnd initor_t;
};

// 針對按鈕的特化
struct button;

template<>
struct wnd_traits<button> {
 typedef initor::button initor_t;
};

6 layout
 _gui 分為兩種控件,普通控件和容器,容器多出了 layout 和 children 兩樣東西,所以window, tab, panel 這些從 container 繼承,而 button,label 等從 wnd_base 繼承。


布局這個概念只有容器才有,當容器獲大小改變會收到 WM_SIZE 消息,這時候用 layout 進行布局。 參考 container.h

layout 只有一個接口 apply


[cpp]
namespace layout { 
    struct base { 
        virtual void apply(wnd_ptr& parent, vector<wnd_ptr>& children) = 0; 
    }; 

namespace layout {
 struct base {
  virtual void apply(wnd_ptr& parent, vector<wnd_ptr>& children) = 0;
 };
}
各種layout實現這個apply來布置窗口,比如 fit 是把子窗口填充滿整個容器


[cpp]
// fit layout  
namespace layout { 
    struct fit : base { 
        virtual void apply(wnd_ptr& p, vector<wnd_ptr>& ch) {  
            rect r = p->client_rect; 
 
            for(auto& c : ch) { // 通常只有一個子窗口  
                c->rect = r; 
            } 
        } 
    }; 

// fit layout
namespace layout {
 struct fit : base {
  virtual void apply(wnd_ptr& p, vector<wnd_ptr>& ch) {
   rect r = p->client_rect;

   for(auto& c : ch) { // 通常只有一個子窗口
    c->rect = r;
   }
  }
 };
}
比如垂直分割布局 vsplit:


[cpp]
// layout/split.h  
namespace layout { 
 
    struct vsplit : base { 
        wnd<vsplitter> sp; // 分隔條  
 
        vsplit(int offset) { 
            sp = 創建vsplitter; 
        } 
 
        virtual void apply(wnd_ptr& p, vector<wnd_ptr>& ch) {  
            std::call_once(在容器p上畫出 sp); 
 
            ch[0]->rect = 分隔條左邊區域大小; 
 
            // splitter  
            sp->rect = ..;// 拉伸分隔條高度 = 容器高度  
                 
            ch[1]->rect = 分隔條右邊區域大小; 
        } 
    }; 

// layout/split.h
namespace layout {

 struct vsplit : base {
  wnd<vsplitter> sp; // 分隔條

  vsplit(int offset) {
   sp = 創建vsplitter;
  }

  virtual void apply(wnd_ptr& p, vector<wnd_ptr>& ch) {
   std::call_once(在容器p上畫出 sp);

   ch[0]->rect = 分隔條左邊區域大小;

   // splitter
   sp->rect = ..;// 拉伸分隔條高度 = 容器高度
    
   ch[1]->rect = 分隔條右邊區域大小;
  }
 };
}

總之在 apply 內可以實現所有布局,比如可以做一套傳統的java布局,我沒有考慮實現那些,覺得不夠通用。以經典 border 為例,支持5個東西以 "東南西北中" 放置,但我要在界面上放7個東西 “東南西北中發白”, 他就不支持了,除非用嵌套 panel 的方法, 既浪費一些內存,代碼寫出來也不易讀。

需要一個萬能的布局。為此我google了老半天,發覺兩個還不錯
1. PageLayout A Layout Manager for Java Swing/AWT 
    它的 doc 裡說道  PageLayout: The Only Layout Manager You Will Ever Need
2. DesignGridLayout for java 
    如果裝了java,可以直接運行他的demo
 

但還是感覺不夠通用,還要記一大堆api。

 


把 layout 問題抽象,其實可以看做一個約束問題。比如一個窗口,寬度是W,它包含左右兩部分,左邊寬度是右邊兩倍,可以描述成:


[html]
w1 == 2 * 2w; // 左邊寬度是右邊兩倍 
w1 + w2 == W; // 總寬度是W 

w1 == 2 * 2w; // 左邊寬度是右邊兩倍
w1 + w2 == W; // 總寬度是W
或者固定寬度100:


[html]
w1 == 100; 

w1 == 100;
或者播放器保持 16:9 比例,最小寬度200:


[html]
w / h = 16 / 9; 
w >= 200; 

w / h = 16 / 9;
w >= 200;
這樣一來,布局問題就變成了數學問題,通過解n元一次方程組就能算出每個控件的位置和大小。以後布局就不用記什麼 layout api了,直接給幾個公式就ok,如果覺得公式不直觀也可以稍微封裝幾個 api。

我找了一個線性問題的c++庫 SymbolicC++ ,簡單測試了下,解三元一次方程:
x + y + z == 26;

x - y == 1;

2x - y + z == 18;


[cpp]
#pragma warning(disable: 4800 4801 4101 4390)  
#include<iostream>  
using namespace std; 
#include "Symbolic/symbolicc++.h"  
 
int main() { 
     
    Symbolic x("x"), y("y"), z("z"); 
    Equations rules = ( 
        x + y + z == 26, 
        x - y == 1, 
        2*x - y + z == 18 
    ); 
 
    list<Symbolic> s = (x, y, z); 
 
    list<Equations> result = solve(rules, s); 
 
    for(auto& r : result) { 
        cout << r << endl; // 輸出 x==10  y==9  z==7  
    } 

#pragma warning(disable: 4800 4801 4101 4390)
#include<iostream>
using namespace std;
#include "Symbolic/symbolicc++.h"

int main() {
 
 Symbolic x("x"), y("y"), z("z");
 Equations rules = (
  x + y + z == 26,
  x - y == 1,
  2*x - y + z == 18
 );

 list<Symbolic> s = (x, y, z);

 list<Equations> result = solve(rules, s);

 for(auto& r : result) {
  cout << r << endl; // 輸出 x==10  y==9  z==7
 }
}
語法非常簡潔,但結果debug下耗時402ms, release下67ms, 實在太慢了 @_@, 可能和裡面的字符串有關,也許有開關可以避免處理字符串,也不知道有沒有快點的庫,如果有好的建議請告知:) 


我想實在不行自己封裝一個解方程庫也是可以的,用矩陣啊什麼的!@#$%^&&*()

 

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved