随笔-90  评论-947  文章-0  trackbacks-0

事情的缘起是,耐不住寂寞,准备开始造GUI的轮子。

GUI框架,要做的事情我想大概是这么几步:

  1. 实现回调函数的成员化。
  2. 实现方便程度可接受的消息映射。
  3. 确定上述核心部件的使用方式。
  4. 制造大量的控件。

前三步要走的比较小心,第四步是体力劳动。

第一步,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,让使用的时候与原始消息参数彻底隔离呢?

最后一步,虽说是体力活,但这跟最终的应用场合密切相关,需要提供怎么样的功能是一件需要考量的事。

目前走在第二步,所以下面的两个问题思考得不多。求经验,求意见。

posted on 2011-01-16 20:05 溪流 阅读(4180) 评论(11)  编辑 收藏 引用 所属分类: C++Windows

评论:
# re: 也谈谈GUI框架 2011-01-17 01:06 | 饭中淹
这个GUI框架挺好的
我是保留WPARAM和LPARAM
不过一些常用的消息,做进了内部逻辑了。比如onpaint这种,在这个内部逻辑里,PARAM就被转换成真实的变量了,比如HDC这样的。

另外,我直接用VS的DIALOG编辑器,编辑成无窗口模式,然后用一个FORM套住这个无窗口模式的DIALOG,就间接实现了界面的所见即所得编辑。

用法,无所谓。可用就行。
  回复  更多评论
  
# re: 也谈谈GUI框架 2011-01-17 09:54 | right
觉得QT的sigslot用起来最舒服  回复  更多评论
  
# re: 也谈谈GUI框架 2011-01-17 11:59 | abeng
对GUI框架了解不多,不过如果能把QT跟DirectUI结合起来就完美了。  回复  更多评论
  
# re: 也谈谈GUI框架 2011-01-26 00:29 | 欲三更
按照我暂时的体会,我倒觉得GUI框架的难点不在你说的这几个点上。我说说我的体会:

首先,基本库的选型。基本的容器和算法可以使用stl,但是stl有个问题就是他是模板库,可能会遇到版本和内存问题,而且stl的string类不太好用,至少在多种编码方式共存的情况下不太好用,我的解决方法是自己写一个string类,内部使用ucs-2,支持比较常用的编码转换,不常用的那些可以引入iconv。string用自己会带来附加问题,就是一些基于字符串的功能要自己写,比如常用的xml封装等等,可以自己实现够用的就行,也可以找支持多种编码的库来集成。

然后是GUI API的封装,最常用的比如win21和Xwindows,这个其实也不好做,主要是要制定一套合理的接口和错误码机制,这个要对几种系统的功能覆盖面和特点有全面的了解,以我现在的水平还不太行。而且这部分很繁杂。

再然后是类层次的设计,这个我们可以参考比较经典的实现,比如MFC或者QT。我个人比较喜欢的是borland的VCL,思想朴实但是绝对够用。这里面比较复杂的部分是我觉得是:首先控件主要分两种,一种是带窗口id(win上师句柄)的,另一种是不带id的,完全自己封装的“伪控件”,这两种结合在一起统一管理,不太好做。其次这一步如果不合理,那可扩展性就会变得不好。

接下来就是伪控件的实现,这个部分要实现的很多,要仿照原生控件实现z-order的管理,消息传递等等,要思考得很清楚才行。

再下来是渲染,我个人倾向于完全自己渲染,大部分人不同意,但是我觉的这样更好实现界面的统一。如果自己渲染的话,首先得懂一点美工或者认识一个洞美工的给你把把关,不能太难看,其次分块渲染这部分本来就不太好做,可以使用gdi+,但是跨平台的话就得用一些开源库比如agg。那你还得封装一层,这个确实是工作量的问题。

下面的问题我和你的观点倒是一样的,就是回调函数的问题。这是个大问题,首先要实现回调函数成员化,这个不难,属于小技巧吧,可能高手实现的话效率会好一点。但是回调函数和多线程结合起来,怎么做一个比较好的实现还是不容易,我是做了一个qt机制的简化版,勉强能用吧,但是我觉得不太好。而且这里还有一个问题就是如果你的框架被用来写模块,主程序不是你的,怎么办?比如我用你的框架写了一个ActiveX,怎么搞。所以这个回调机制得是应单线程,同步,异步,以及没有消息队列的情况。我觉得高手来做的话这部分会很精彩,比如qt。

最后就是附带性的问题了,比如怎么支持皮肤,怎么支持RAD开发,是不是要支持外挂脚本。这些工作量绝对不小于核心功能的工作量。还有你说的大量空间,别的就不说了光一个彩色多字号的文本编辑器,就要用去不少时间了。

以上是我的想法,有点啰嗦,欢迎批评^_^  回复  更多评论
  
# re: 也谈谈GUI框架 2011-01-26 01:40 | 欲三更
顺便说一句,你要是想如你所说把两个param的解析彻底封装起来,你这个MsgMap的模式可能要改。因为为了方便使用,对于不同消息可能要有一些特殊的处理步骤,而且你自己可能还要塞一些自己的消息进去,你可能得多封装一层  回复  更多评论
  
# re: 也谈谈GUI框架 2011-02-15 14:48 | 小龙红
@欲三更
ls分析得相当明了,做GUI框架工作就在与此吧。至于伪控件问题,我也建议尽量用这个实现,因为这样就不需要windows的窗体资源,而且可以完全自己实现管理,统一资源管理。  回复  更多评论
  
# re: 也谈谈GUI框架 2011-02-16 00:38 | 溪流
@欲三更
谢谢你的分享~
控件工作量确实很多啊,标准的,自己的,才做了一个半~突然又有点不想解析 WPARAM LPARAM 了。。。  回复  更多评论
  
# re: 也谈谈GUI框架 2011-02-16 00:40 | 溪流
@abeng
@小龙红
你们俩都有DirectUI倾向。。。就是因为不想要Windows来管理吗?想了解下,Windows管理方式的那些方面值得我们抛弃它?  回复  更多评论
  
# re: 也谈谈GUI框架 2011-02-28 13:19 | zdhsoft
如果你了解一下VCL,估计会有新的收获。  回复  更多评论
  
# re: 也谈谈GUI框架 2011-02-28 23:10 | 溪流
@zdhsoft
谢谢推荐,抽空一定看看  回复  更多评论
  
# re: 也谈谈GUI框架 2011-05-26 13:46 | 放屁阿狗
cpp的很多深入级的技术也琢磨了有一段时间,但最终还是要做项目,python成了我的首选,webservice,ice,gui native 全部使用python,前端的就用pyqt和flex来解决  回复  更多评论
  

只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   博问   Chat2DB   管理