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, 實在太慢了 @_@, 可能和裡面的字符串有關,也許有開關可以避免處理字符串,也不知道有沒有快點的庫,如果有好的建議請告知:)
我想實在不行自己封裝一個解方程庫也是可以的,用矩陣啊什麼的!@#$%^&&*()