用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)。

下载:http://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


转载于:https://www.cnblogs.com/aj3423/archive/2013/03/29/3150500.html

使用C++代码封装win32操作类, 与MFC相似,对于学习SDK与C++是巨好的参考 Tutorials Menu of tutorials Tutorial 1: The Simplest Window Tutorial 2: Using Classes and Inheritance Tutorial 3: Using Messages to Create a Scribble Window Tutorial 4: Repainting the Window Tutorial 5: Wrapping a Frame around our Scribble Window Tutorial 6: Customising Window Creation Tutorial 7: Customising the Toolbar Tutorial 8: Loading and Saving Files Tutorial 9: Printing Tutorial 10: Finishing Touches Tutorial 1: The Simplest Window The following code uses Win32++ to create a window. This is all the code you need (in combination with Win32++) to create and display a simple window. Note that in order to add the Win32++ code to our program, we use an #include statement as shown below. #include "../Win32++/Wincore.h" INT WINAPI WinMain(HINSTANCE, HINSTANCE, LPTSTR, int) { //Start Win32++ CWinApp MyApp; //Create a CWnd object CWnd MyWindow; //Create (and display) the window MyWindow.Create(); //Run the application return MyApp.Run(); } This program has four key steps: Start Win32++. We do this here by creating a CWinApp object called MyApp. Create a CWnd object called MyWindow. Create a default window by calling the Create function. Start the message loop, by calling the Run function. If you compile and run this program, you'll find that the application doesn't end when the window is closed. This is behaviour is normal. An illustration of how to use messages to control the windows behaviour (including closing the application) will be left until tutorial 3.
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值