用c++11封装win32界面库

0. 前言
  你是否也是一个c++玩家,经常用c++写一些带界面的小程序?厌倦了每次在vs里用鼠标拖各种控件,然后copy / paste一大堆win32的api?没用过mfc,wtl,qt,只用sdk? 本文介绍一种方法把这些api进行封装,弄一个界面库出来,当然前提是对这些api有基本了解。

  之前看过些界面库源码,尤其是egui,好多东西都是从它那学来的。它们都用到像boost这种第三方库,因为当时c++版本没有自带shared_ptr,lambda,functional这些工具, c++11之后包含了这大部分东西,也就不需要第三方库了,但需要较新的编译器。下面的源码可以用MinGW编译,或者vs2012+November 2012 CTP  补丁(vs2012不支持xp)。

下载:https://files.cnblogs.com/aj3423/gui.rar

1. 介绍
  就称这界面库叫 _gui 吧,整个 _gui 可以分为以下几部分

  1. thunk  封装wnd_proc这种回调函数

  2. property 类似vb的属性
  win->enabled = false;
  edit->text = "xxx";

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

  4. initor 初始属性 
  wnd<edit> edt_psw = new_<edit>().text('admin').size(200,30).password_type(true);

  5. layout 布局
  如下图的垂直分割布局,拖动中间那条分隔条可以改变左右大小

例子:


2. thunk

  win32教程上的 wnd_proc 一般都是全局函数,缺点是全局函数无法和类实例一一对应,所以用thunk 把 wnd_proc 封装到类的成员函数,据说ATL就这么搞。
先看一下全局函数和成员函数的区别,调试时从汇编可以看到

push args
call global_func  //call 全局函数

push args
lea ecx, p_this //对象指针放到ecx
call member_func  //call 成员函数

区别就是成员函数会在ecx 中放入this指针, 所以如果把 WNDCLASS.wnd_proc 指向一段内存,在这段内存里做两件事

1.  lea ecx, p_this(窗口实例)
2.  call member_func

就ok了, 这段内存就是thunk,用一个结构体来表示:

#pragma pack(push, 1) //取消默认的4字节对齐,pack后char,short固定只占1,2字节
struct thunk_code {
	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
	void init() {
		stub1 = 0x0D8D; // lea ecx 的机器码
		p_this = 0;
		stub2 = 0xB8; // mov eax 的机器码
		member_func = 0;
		stub3 = 0xE0FF; // jmp eax
	}
};
#pragma pack(pop)
调试可以看到内存中代码:


(因为这段内存需要被执行,而如果直接 thunk_code code;  这个code是不可执行的,所以这里用 HeapCreate / HeapAlloc 带上 HEAP_CREATE_ENABLE_EXECUTE 来分配内存,参考 thunk.h 和 heap.h)

_gui的所有控件都是用的这种方式处理事件,所以thunk的初始化放在了基类 wnd_base 中(参考 wnd_base.h)

 

3 property

  操作属性的通常做法是对外提供两个接口 getter 和 setter,类似这样

struct listview {
	void set_title(string s) { SetWindowText(...); }
	string get_title() { GetWindowText(...); }
};
可以把"属性"的概念封装起来
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,有没有感觉简洁一些。(详见 property.h)

 

4 event

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

  有一点 .net 的味道,用起来比较方便。 每个事件都是一个signal (见signal.h):

// event.h
namespace event {
	struct base {
		signal<void(pos_t&)> move;
		signal<void(size&)> size;
		signal<void(wnd_msg&)> paint;
		signal<void(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 成员,如果要自定义消息,创建时候提供event_t 就ok

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

常见的做法是,给类提供多个构造函数以支持不同的参数
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() 的时候会去拿 initor 里的各种信息(text, size...)

wnd<window> w = new_<button>().text("...").size(100, 200).pos(300, 400);// 这样就不会错了
wnd<button> b = new_<button>("..."); // 其他创建方法
wnd<label> l("...");

为了支持链式赋值和扩展性,initor的设计稍显复杂,见 initor.h

每种控件对应的initor,用traits来定义(还在想办法去掉这层定义@_@):

// 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

namespace layout {
	struct base {
		virtual void apply(wnd_ptr& parent, vector<wnd_ptr>& children) = 0;
	};
}
各种layout实现这个apply来布置窗口,比如 fit 是把子窗口填充满整个容器
// 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:

// 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个东西以 "东南西北中" 放置,但要多于5个它就不支持了,除非用嵌套 panel 的方法, 既浪费内存,代码也不易读。

需要一个更通用的布局。

我google了老半天,发觉两个还不错
1. PageLayout A Layout Manager for Java Swing/AWT  (http://pagelayout.sourceforge.net/
    它的 doc 里说道  PageLayout: The Only Layout Manager You Will Ever Need

2. DesignGridLayout for java (http://designgridlayout.java.net/)
    如果装了java,可以直接运行他的demo (http://designgridlayout.java.net/examples.jnlp)

但还是感觉不够通用,还要记一大堆api。

把 layout 问题抽象,其实可以看做一个线性约束问题。比如一个窗口,宽度是W,它包含左右两部分,左边宽度是右边两倍,可以描述成:

w1 == 2 * 2w; // 左边宽度是右边两倍
w1 + w2 == W; // 总宽度是W

 或者固定宽度100:

w1 == 100;
或者播放器保持 16:9 比例,最小宽度200
w / h = 16 / 9;
w >= 200;

这样一来,布局问题就变成了数学问题,通过解n元一次方程组就能算出每个控件的位置和大小。以后布局就不用记什么 layout api了,直接给几个公式就ok。
Auckland Layout 就是这么做的,看了它的demo后又发觉个问题,太不直观了。。

继续寻找,发现最直观的是这个 Eva Layout,就写了个layout::eva:

可以用各大IDE的列模式编辑eva表格,vim的话还有插件可以格式化竖线: easy_align

最后

 如果觉得太素就加个win7 style:

#pragma comment(linker,"/manifestdependency:\"type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='x86' publicKeyToken='6595b64144ccf1df' language='*'\"")

目前的进度也就到这,只有一个大致框架,准备改成mvc的,然后用到什么控件就改进什么。

有建议请联系, 企鹅号 94566062

good luck


posted @ 2013-03-29 14:45  aj3423  阅读(2803)  评论(0编辑  收藏  举报