如何用 C++ 从零编写 GUI?

姚冬中老年程序员

GUI库可大可小,大可以是Qt WPF这种数以百万行计的代码,小的可以是WTL这种只有几个头文件。

对一般人来说,不要奢望能做出大GUI库,写一个小一点的,满足自己的需求,针对某类应用就好了。

我曾经遇到一个需求,需要一个小型的GUI库来写个安装程序。
安装程序是比较特别的,对于互联网下载安装的软件,要满足以下要求:
1. 不能带DLL,必须是静态链接,对系统的依赖越小约好。
2. 可执行代码必须足够小,一般来说要500KB左右最好。
3. 有比较好看的图形效果,比如安装过程的过场动画,窗口要有个半透明阴影光圈什么的。

用Qt写显然不合适,虽然我在知乎多次说过Qt库其实不大,但是对于写安装程序还是真的有点大了,Qt的静态链接出的Exe有2MB左右。
用MFC也不合适,MFC静态链接出来有400KB左右,算上安装程序自身的代码和资源肯定突破500KB了。
用VC++ 6.0的MFC去写,可以小很多,但是用这种古董不符合我的品味。
用WTL写,这个肯定很小,只创建一个窗口的程序静态链接只有50KB左右,但是什么功能都没有啊,只能创建使用基础的标准悾件,做个透明窗口都要自己再用其他API实现。

还是自己写一个吧,小一点实用一点,就用来做安装程序好了,不追求有多么高大上的能力。
跨平台就不追求了,只解决Windows问题就好了。

实现GUI库,有几个基本的子系统:
1. 窗口管理系统,这个代码就是封装Win API,但是这个工作很无聊,又很麻烦,我索性用WTL实现了,把自己的窗口类去聚合WTL的 CWindow,拿WTL做后端帮我创建管理窗口,对外是看不到WTL的,我没有用派生是因为不想让WTL污染我的接口设计。

我用私有类的方法,把WTL封装起来了,外面看不到
//伪代码
class RWindow : public RObject
{
private:
    RWindowPrivate *d;
}

class RWindowPrivate
{
public:
    CWindow m_wnd;
}

2. 事件系统,WTL的消息映射宏太丑了,我喜欢Qt的signal/slot,但是实现一个Qt那样的signal/slot可不容易,相当于发明一种C++扩展语法,还要自己实现一个MOC这种编译预处理器,工作量太大了。用Boost::signal 也太笨重了,boost会引入一个很大的依赖库,我还希望这个GUI库可以用默认的VC++就能编译呢,不想依赖太多其他库,而且boost的function会带来编译困难。
我选择了用一个轻量级的sigslot库,
基于C++ template实现的,功能简单,实现也很简单,只有一个头文件,很符合我的要求,本来就不需要那么复杂的功能。
class RWindow : public RObject
{
    sigslot::signal0<> Clicked
}

class MyApp
{
    void on_clicked()
    {
    }
    void init()
    {
        m_win.Clicked.connect(this, &MyApp::on_clicked);
    }

    RWindow m_win;
}

3. 图形系统
既然是GUI库,总不能还用GDI函数往hDC上绘制吧,好歹要弄个FrameBuffer,支持RGBA,渲染好了可以通过UpdateLayeredWindow更新到窗口上,以实现半透明异形窗口图形效果,比如实现个阴影边缘什么的。
自己写一个还是很麻烦的,光基本的点线圆绘制,基本的Alpha混合就要写上万行,更别提文字输出了。用第三方库的话,2D图形库就没有小的,光图形库就突破500KB的限制了,用Direct2D是不是有点小题大做了?还是用GDI+吧,虽然这个函数库不太受待见,但好歹是标准库,所有Windows都内置,而且我要求的基本图形功能都是有的。
自己写个 RPainter 包装GDI+的函数,顺便把 PNG JPG的编解码也解决了。

4. 布局系统
GUI库总不能让用户自己一个一个创建控件然后用绝对坐标摆放吧。基本的UI描述文件,Layout支持还是要有的。
但是我没有用XML,而是用了JSON,这两种格式描述能力是差不多了,仅是我个人偏好JSON,另外JSON库比较小,我用的是这个

根据JSON的描述来构建窗口控件的对象树。
我没有去实现复杂的布局,只实现了Anchor Layout,基本可以保证够用了。
给每个控件设定好object name,在C++里提供 
template<typename T>
T *findObject(const RString &name)
搞自动绑定可不容易,开发者自己手工绑定吧,好在小程序控件也不多。

5. 基本数据类型和容器类型
身为一个Qt粉当然要自己实现一套string类和泛型容器,向Qt致敬啦。
我没觉得自己有能力重写一遍STL,就是用系统的STL做后端,聚合STL的类,实现COW(copy-on-write),实现统一内存池。后来我把vc++的STL换成了 eastl
paulhodge/EASTL · GitHub
这个STL的实现非常好,解决了代码膨胀问题,编译出来的代码比用VC STL小得多。
RString是我自己写的,但是很多代码是照抄QString的。
但是实现string和数据容器不是GUI库必须做的,只是我个人偏好。

6 一些杂项 utility:
基本算法,MD5 SHA1 ZIP 7Z
网络支持,TCP UDP HTTP,没搞太复杂网络模型就是简单的select,HTTP是封装的WinHTTP。
IO支持,RFile RStream 

7. 至于基本控件,早期只提供了RButton RLabel RTextEdit,其他的按需求用到哪个就实现哪个。

好了,差不多了吧,有这些做个安装程序基本算够了。
这个GUI库写大程序还是不行,格局太小,只能做小玩意儿,而且GUI库要有配套的工具链,这个很麻烦工作量又大,所以开发大工程还是推荐用Qt。

这个库早期的基础版本写下来也就两万行左右代码,基本只有自己用,想到如果要给别人用的话还要写文档,脑袋瞬间大了一圈圈。

后来有个同事把Lua集成进去了,做了脚本绑定,支持拿Lua脚本写程序,有点QML的感觉了,不过没有在真正的产品里用到。

编辑于 2014-07-12 70 条评论 
 

王斌数学爱好者,喜欢平面设计、户外运动

伪专业人士过来怒答一记。由于是想到哪写到哪,所以可能写的有点乱。
大概2009年的时候,我就沉醉在编写一款能跨时代的GUI上,从哪时候开始,我开始大量研究国内外所有开源非开源的GUI。
最开始的时候,每个写GUI的人都会经历那么几个阶段(windows下),
使用MFC,windows控件 ->发现windows控件不足,开始改装->发现改的多了,重复造轮子,开始思考从头完全自己实现控件->GUI的雏形->总结各种gui,写出自己的DirectUI->发现光是DirectUI还不够满足日益增长的界面需求,开始思考更合理的设计理念

所以你会发现,要从头实现一款自己的界面库,你需要了解界面库是如何运作的,这就牵扯到消息机制、绘图机制,这算界面库的最底层。在这层其实就有大量细节。比如在windows上,在用GDI的过程中,会发现GDI有各种各样极其不爽的地方,不支持alpha通道、抗锯齿效果不好、矢量功能太弱、图像处理功能太弱等等。所以在这一步,你需要一个好用的渲染框架。个人非常推荐skia,效果强大,速度极快。
有了渲染层,你要设计一套消息循环机制,你需要设计你的各个控件是如何接受消息,并做出反应。然后之上到了控件层,你需要设计你的控件体系。比如你要怎么分类你的控件,各种控件之间怎么协作。控件层之上,你又需要布局,就是说你需要有一种方式,让你的控件方便的放置在应该在的位置。
写完了这些,你会需要一个更方便的配置系统。这时候你会想到xml。这种结构化的语言对你来说很适合描述控件的各种信息。于是你又加入xml配置控件。

等等等等,写完这些,你基本上已经算半个界面专家了。但其实这一阶段,才是真正界面高手和界面熟手的分界点。到这一阶段,如果你对界面有更高的要求,你会发现用xml配置那些写好的控件,到底还是太麻烦。产品会给你提无数乱七八糟的需求,这种需求不是一个通用控件可以搞定的。这时候,你会有所思,想怎么才能更加方便的开发。当年,在这个阶段,我苦思了很久,最后直到在公司的某个公共目录,发现一份对迅雷界面库的分析,我才恍然大悟。原来你少了一种打破控件系统的勇气和创意。这个时候,你会发现,市面上许许多多的js库,其实就代表未来界面库的发展方向:不再使用传统的控件体系,而是用比控件更小粒度的原子控件---就比如迅雷界面库里的各种原子控件,又如js库里所使用的那些html元素。你会发现,只使用基本的几个元素,然后强化他们的拼装方式,能更方便快捷的实现产品提出的那些奇怪需求。
有了原子控件,你拼装出异性控件更加方便,你要实现控件各种动画也更加简洁。当你还是觉得有点不足,你会发现C++写这种异步逻辑的工作,太tm麻烦了。你会发现写网页的那群人永远写出炫酷界面的时间比你短。这时候你会再次醒悟,用脚本来实现这一切,用脚本去黏合这些逻辑。你会发现,用脚本去写动画,写消息响应,由于脚本天生自带闭包,写这种异步逻辑简直爽到爆…………

写完这些,我的库基本也被公司所用。小小得瑟一下,现在毒霸的加速球悬浮窗就是用这套逻辑写的,使用的库叫kdgui :-)
未完待续,有时间还想讲下kdgui的设计思路,当时可是把10多m的webkit,给完全解剖了,变成2m的kdgui:-)
 

Gdier就是一死写界面的

其实很少逛知乎,今天被同事在QQ上邀请,作为一个工作十年的一个死写界面的程序员,我想我应该还是有些可以分享的。当然,有很多观点或许和我经历的时代有关,现在的程序员不见得百分之百适合,姑且当作抛砖引玉吧。

从0开始学GUI编程,但是不用库,这件事本身是个伪命题。GUI编程是个复杂的知识体系,并不像大多数人想像中那么简单。所以如果题主真的想对Win32下(我猜的)的GUI编程有比较深刻的理解,建议还是从使用库开始。

先简单的说下,一个能用的界面程序员需要储备哪些知识:
1.各种系统内置控件的使用方法及特性
2.界面相关的消息机制。比如Windows下的消息循环,iOS中的RunLoop机制等
3.消息及事件派发机制
4.简单的绘图方法

优秀的界面程序员需要具备的进阶技能包括但不限于:
1.熟悉进程、线程调度机制,各种内核对象的应用
2.基本的图形学和图像处理技能,位图基础
3.图像编辑工具的使用,熟练使用PhotoShop、AI、mspaint等至少一种
4.有过多个平台的界面开发经验

回到题目,从0开始,当然先要成为能用的界面程序员。熟悉系统内置控件的最佳方式莫过于使用各种成熟framework来实现需求。至于是MFC还是WTL还是别的什么,倒不是什么大不了的事。

还是简单说下MFC和WTL(ATL)的区别。以前经常会看到有人说MFC如何如何渣,如何如何误导观众,其实这也是一个误解。
MFC
优点:易上手,对于界面订制不高的需求更容易做到快速实现
缺点:不够灵活,效率略低,运行库体积较大且版本太多
WTL(ATL)
优点:灵活,代码执行效率高(其实就是Win32 API的简单封装,当然快),运行库小,老版本甚至有个minicrt版本,一个简单的helloworld只有80k且不依赖任何运行库
缺点:封装的不够完全,很多功能需要自己实现;细节暴露太多,控制能力较差的程序员很容易把代码写的乱七八糟;WTL的支持者大多对范型这种东西有着阶段性盲目崇拜(顺便怀念一下当年那青葱的自己_(:зゝ∠)_)

OK,我的意图已经很明显了,如果完全没有基础,先看看MFC吧。如果能够做到熟练运用(如果悟性好,这个时间段可以非常非常短,所以不要着急),并且开始接触Win32原生API,再开始试着用下WTL,你顿时会发现世界美妙多了。

当你已经可以熟练使用框架进行界面开发之后,就可以开始探讨一些原理性质的东西了,这个时候可以试着读一下界面框架的源代码,然后你就能了解为什么同样是API封装,不同的框架使用起来会有那么多的区别。

举个例子,比如消息机制,以上两个框架的消息循环封装都写的非常漂亮,MFC的消息注册机制、WTL的thunk机制,都是非常值得一读的。

如果你已经走到这一步,恭喜你,你已经是个不错的界面程序员了,如果这个时候你仍然觉得自己非常喜欢写界面,你就有机会成为一个很优秀的界面程序员。那么,接下来会发生些什么呢?

1.你会嫌弃各种系统控件,什么都想自己画。你手上会有一套常用的自绘控件代码
2.你会开始崇拜DirectUI,对spy++抓不到的窗口怀有深深的敬意
3.你开始尝试自己写一个简单的界面库
4.你会后悔高中没学好解析几何,大学没学好线性代数
5.觉得写界面的程序员好不受重视啊,要不要也去做几个牛逼的功能?

于是那年我辞职出去创业了,为了公司的产品,我花了一个多星期写了bkwin的原型,基于WTL实现了一套基本的DirectUI体系,用XML描述界面布局,并实现了一个RelativeLayout和一个FrameLayout(以至于一年后当我看到AndroidSDK里描述界面的方法时,各种即视感,然后感慨自己道行尚浅)。后来又断断续续的增加了一些复杂控件实现,以及缓存优化、换肤支持等等。现在这个库的早期版本已经被公司开源,并且应用在有上亿用户量的产品中。(好久没得瑟了,一下没忍住 ´▽`)

越往后写,越发现自己知识的匮乏。当GDI+解码PNG的效率成为瓶颈的时候,我开始学习自己处理DIB;当C++代码处理DIB的效率已经不能再提升的时候,我开始看SSE的流水线指令...当我觉得我已经实在改不动的时候,有幸读到了WebKit的代码,被WebCore彻底击溃;然后我转行做了iOS程序员,于是三观又被毁了...

话题扯的有点远,其实我的根本意思还是想说,学无止境,既然打算从0开始,就认认真真打好基础,每向前多走一步,都要夯实,尝试去了解一些原理性的东西,不能浅尝辄止。不求甚解是成为一个优秀程序员的大忌讳

最后推荐两本书:《Windows核心编程》、《Windows图形编程》,其中《Windows图形编程》早已绝版,不过网上可以买到影印版的,非常值得细读。希望题主以后有机会成为一个牛逼的界面程序员。
 
posted @ 2015-02-07 14:32  StuJnX6ry  阅读(13350)  评论(1编辑  收藏  举报