也谈谈GUI框架
事情的缘起是,耐不住寂寞,准备开始造GUI的轮子。
GUI框架,要做的事情我想大概是这么几步:
- 实现回调函数的成员化。
- 实现方便程度可接受的消息映射。
- 确定上述核心部件的使用方式。
- 制造大量的控件。
前三步要走的比较小心,第四步是体力劳动。
第一步,Windows下可参考的是MFC方式、WTL方式,以及利用Window相关属性中的某些空位。前不久刚初步看过WTL的机制,虽然当时没写GUI框架的打算,不过也有点技术准备的意思了。现学现用吧。这里一个可以预见的问题是64位兼容,现在没有测试环境,先不管。
接下来看第二步了,所要做的事情就是把 WndProc 下的 一堆 case 有效地组织起来,或者换个写法。之前还真不知道 MFC/WTL 的 BEGIN_MSG_MAP。以为很高深的,想不到就是拼装成一个大的 WndProc。先抄了,做成一个可运行的版本。但是,这方面会直接决定以后的大部分使用方式,单单抄一下意义不大。后来去 @OwnWaterloo 曾推荐过的 @cexer 的博客上逛了几圈,第一圈看了一些描述性文字,第二圈大概看了下技术,第三圈是挖坟,那个传说中的 cppblog 第一高楼啊。。其中有一个使用方式很新颖,嗯……是那个不需要手动写映射代码,直接实现消息处理函数的方式。不过我后来觉得还是不要这种样子了,凭我个人的直觉,如果我写下这样的处理函数,我大概会因为不知道何时注册了这个函数而找不到调用来源而感到郁闷。在Windows回调机制的影响下,我可能会很抱有偏见地认为,只有直接来自WndProc的调用,才算是来源明确的,不需要继续追踪的——当然,这是建立在我不熟悉这个框架的基础上的。框架必然需要隐藏调用来源,以及其他一些细节,但是在这一步,我觉得稍微有点早。
刚才说到的都是静态绑定。现在我有点倾向于动态绑定。从使用方便程度上来看,动态绑定更具灵活性。从性能上,动态绑定下,消息到处理函数的查找过程可以更快,静态绑定只能遍历。当然,未必将“添加处理函数”这样的接口提供给最终用户,但是这个操作对于整个控件体系的形成应该蛮有帮助的吧。比如MFC下一个控件类使用Message Map做了一些事情,继承类就无法直接继承这个动作,于是可能需要做两套处理函数调用机制,一套是给内部继承用的,一套是给用户的。如果在最开始的基类保存一个消息映射,每个消息对应一族处理函数,每个继承类都可以添加处理函数,但不删除父类已添加的函数,这样就可以在一套Message Map机制下获得父类的行为。以上,不知道考虑得对不对,欢迎讨论。
其中,父类保存子类给出的可调用体并正确执行是个问题。折腾了一些时间,都没有成功。我比较纠结,想知道除了用function之类的玩意儿外还有没有其他简单可行的办法。后来去@zblc的群上问,@vczh也说需要一套function机制。看来是逃不开这个问题了。嗯……想起来大约两个月前一个同事从codeproject找来了一个GUI框架看,看到几行整整齐齐的 AddMsgHandler(WM_CREATE, XXX(this, &MyWindow::OnCreate));,叹不已。我当时打趣说,这很简单的,无非是搞了个 function 而已,哥哥两天就能搞定。于是他们叫我两天搞定。我鼓捣了10分钟,搞不定,只好丢一句,真的很简单的,类似boost::function,你去看一下就知道了,哥哥要干活了。
既然现在还是绕不开这个问题,那还是搞一下了,搞好以后就权且当做给他们交作业吧。我会另写一篇文章说说function的事情,这里先略过。现在开始假设这个设施已经造好了。那么,窗口类中大概可以这么定义相关类型:
typedef Function<bool (WPARAM, LPARAM)> MsgHandler;
typedef List<MsgHandler> MsgHandlerList;
typedef Map<UINT, MsgHandlerList> MsgMap;
然后再定义一个变量:
MsgMap m_MsgMap;
它用于保存消息映射。最终的回调函数可以写成:
LRESULT WndProc(UINT uMsg, WPARAM wParam, LPARAM lParam)
{
bool bHandled = false;
MsgMap::Iterator itMsgMap = m_MsgMap.Find(uMsg);
if (itMsgMap != m_MsgMap.End())
{
for (MsgHandlerList::Iterator it = itMsgMap->Value.Begin();
!bHandled && it != itMsgMap->Value.End(); ++it)
{
bHandled = (*it)(wParam, lParam);
}
}
return bHandled ? TRUE : DefWindowProc(m_hWnd, uMsg, wParam, lParam);
}
最后给个添加消息映射的接口:
void AppendMsgHandler(UINT uMsg, MsgHandler pMsgHandler)
{
m_MsgMap[uMsg].PushBack(pMsgHandler);
}
到目前为止,我们的窗口类大致上可以写成这样:
#include <Windows.h>
#include <tchar.h>
#include "../GUIFramework/xlWindowBase.h"
class Window : public xl::WindowBase
{
public:
Window()
{
AppendMsgHandler(WM_ERASEBKGND, MsgHandler(this, &Window::OnEraseBackground));
AppendMsgHandler(WM_PAINT, MsgHandler(this, &Window::OnPaint));
AppendMsgHandler(WM_LBUTTONUP, MsgHandler(this, &Window::OnLButtonUp));
AppendMsgHandler(WM_RBUTTONUP, MsgHandler(this, &Window::OnRButtonUp));
AppendMsgHandler(WM_DESTROY, MsgHandler(this, &Window::OnDestroy));
}
protected:
bool OnEraseBackground(WPARAM wParam, LPARAM lParam)
{
return false;
}
bool OnPaint(WPARAM wParam, LPARAM lParam)
{
PAINTSTRUCT ps = {};
BeginPaint(m_hWnd, &ps);
RECT rect = { 200, 200, 400, 400 };
DrawText(ps.hdc, _T("Hello, world!"), -1, &rect, DT_CENTER | DT_VCENTER);
EndPaint(m_hWnd, &ps);
return false;
}
bool OnLButtonUp(WPARAM wParam, LPARAM lParam)
{
MessageBox(m_hWnd, _T("LButtonUp"), _T("Message"), MB_OK | MB_ICONINFORMATION);
return false;
}
bool OnRButtonUp(WPARAM wParam, LPARAM lParam)
{
MessageBox(m_hWnd, _T("RButtonUp"), _T("Message"), MB_OK | MB_ICONINFORMATION);
return false;
}
bool OnDestroy(WPARAM wParam, LPARAM lParam)
{
PostQuitMessage(0);
return false;
}
};
在最基础的 WindowBase 里,搞成这样大概差不是很多了。暂时先看第三步。到目前为止,我所听说过的 GUI 框架都是真正的框架,似乎没有“GUI 库”。为什么一定要以继承某个基类的方式来使用呢?如果像下面这样使用呢?
class Window
{
private:
xl::WindowBase m_WindowBase;
public:
Window()
{
m_WindowBase.AppendMsgHandler(WM_ERASEBKGND, MsgHandler(this, &Window::OnEraseBackground));
m_WindowBase.AppendMsgHandler(WM_PAINT, MsgHandler(this, &Window::OnPaint));
m_WindowBase.AppendMsgHandler(WM_LBUTTONUP, MsgHandler(this, &Window::OnLButtonUp));
m_WindowBase.AppendMsgHandler(WM_RBUTTONUP, MsgHandler(this, &Window::OnRButtonUp));
m_WindowBase.AppendMsgHandler(WM_DESTROY, MsgHandler(this, &Window::OnDestroy));
}
};
这个问题,不知道各位有没有什么思考?
还有一个问题是,接下去要不要将 WPARAM 和 LPARAM 的含义彻底解析掉,搞成一系列 PaintParam、EraseBackgroundParam、LButtonUpParam、RButtonUpParam,DestroyParam,让使用的时候与原始消息参数彻底隔离呢?
最后一步,虽说是体力活,但这跟最终的应用场合密切相关,需要提供怎么样的功能是一件需要考量的事。
目前走在第二步,所以下面的两个问题思考得不多。求经验,求意见。