用c++11封装win32界面库 (一)

2014-11-24 03:23:30 · 作者: · 浏览: 0

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 edt_psw = new().text('admin').size(200,30).password_type(true);

wnd edt_psw = new().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