用c++11封装win32界面库
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 b
补充:软件开发 , C++ ,