• 用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


  • 相关阅读:
    安全预警-防范新型勒索软件“BlackRouter”
    线程入门
    线程状态
    支付开发总结
    springboot处理date参数
    函数接口
    Excel通用类工具(一)
    Excel通用类工具(二)
    spring bean生命周期
    springboot整合netty(二)
  • 原文地址:https://www.cnblogs.com/aj3423/p/3150500.html
Copyright © 2020-2023  润新知