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