UI神器-SOUI
前⾔
在Windows平台上开发客户端产品是⼀个⾮常痛苦的过程,特别是还要⽤C++的时候。
尽管很多语⾔很多⽅法都可以开发Windows桌⾯程序,⽬前国内流⾏的客户端产品都是C++开发的,⽐如QQ,YY语⾳,迅雷等。
快速,稳定是我认为的应⽤软件开发框架最基本的要求,对于UI还有两个要求就是界⾯美观,配置灵活。
C++语⾔满⾜了快速的要求,传统的客户端软件开发框架如MFC,WTL等满⾜了稳定的要求。然⽽界⾯美观,配置灵活是MFC,WTL这样的开发框架所不能满⾜的。
腾讯是做客户端发家的,他们的UI经验积累⾮常好,有⾃⼰专门的UI框架;迅雷有⼀个专业的团队开发⾃⼰的UI框架;然⽽⼤多数公司只希望有⼀个能够快速完成项⽬开发的UI库来使⽤,它们没有专业的团队来维护UI库。国企有钱任性,所以成就了UIPower:⼀个商业化的DirectUI库(具体怎么样不好说,优点在于有⼈给你服务),⼀般的⼩公司没有谁愿意当这个冤⼤头。这就是Duilib这样⼀个简单到简陋的UI库(请原谅我这样说)为什么这样流⾏的原因(百度⼀下Duilib就知道它有多少⼈在⽤)。Duilib基本满
⾜了界⾯美观,配置灵活的需求,然⽽由于框架本⾝的限制,要实现复杂的效果将不可避免的遇到各种坑。好在Duilib代码量很少,随便⼀个有经验的UI开发⼯程师都能够相对容易的使⽤并修改它,所以在⼀般的应⽤中使⽤并不会有太⼤的问题,这也应该是为什么会有那么多的Duilib变种的原因:每⼀个使⽤它的公司或者个⼈都会有⼀份独⼀⽆⼆的副本。
其实上⾯我还漏了说QT, QT在国外有专业的团队维护,⽂档也很好,但⾄少有两个缺点:1、它是跨平台的,跨平台即是优点,也是缺点,为了实现跨平台,很多时候需要做出取舍,就算抽象的100%的完美,它也不可避免的带来体积庞⼤;2、代码量太⼤,普通⼈很难驾驭:就算是看懂都不容易,更别说修改了,这样的结果就是⼀旦在使⽤中遇到问题你唯⼀的选择就是提交BUG给QT开发⼩组等待补丁(要知道不存在没有BUG的产品)。
SOUI是什么?
SOUI是⼀套和Duilib类似的开源C++ UI开发框架。它的祖宗是⾦⼭卫⼠开源版本中使⽤的UI库Bkwin,之后由启程软件(也就是我了)开发维护升级为Duiengine,最后历经多次重构改名为SOUI,寓意“瘦UI”,“UI, just so so!”。使⽤MIT开源协议,公司、个⼈兼可免费作⽤,只需要发布时带上SOUI的license。
SOUI代码的获取
SVN:不要在浏览器中打开该⽹址,只能使⽤SVN客户端签出。
GIT:
SOUI界⾯效果
SOUI的特点
使⽤层:⾼速,稳定,美观,可配置
代码层:精⼼设计,模块低耦合,插件化设计,对象可靠的命令周期管理,类似WTL的编码⽅式,现代化的事件处理模型及优异的扩展能⼒。
代码量:核⼼模块代码量4W+,编译后DLL Release版本在900K左右。得益于精⼼组织的代码框架,虽然代码量较Duilib这样的UI库有⽐较⼤的提⾼(核⼼框架更完善,控件更多,注释量更⼤),但是阅读代码还是很轻松的(⼤量实际⽤户的亲⾝体验)。⾼速主要体现在3个⽅⾯:
1、框架设计扁平化,层次简单(和QT相⽐):从宿主窗⼝收到消息到控件响应消息只有⼀个中间层。
2、简单有效的刷新策略:通过对剪裁区及刷新时机的有较控制,能有效的提⾼刷新效率。
3、⾼效的渲染引擎:通过将渲染引擎接⼝化,成功的将skia渲染引擎引⼊到SOUI中,Skia是Google的Chrome的渲染引
擎,Chrome⽐IE渲染速度快,Skia功不可没。
稳定性⽅⾯,SOUI脱胎于Bkwin,再经过本⼈的不断精⼼重构,已经在多个⼤量⽤户的产品中应⽤,包括最近开发的瑞雪医⽣客户端,多玩魔盒2.0, Dota2游戏盒⼦及多玩多个游戏盒⼦中使⽤,及百度云管家的⼤部分界⾯。
百度云管家据说最初使⽤的是腾讯QQ界⾯库的早期版本(⽆从考证),然⽽QQ界⾯库⼤量使⽤COM技术,扩展⾮常⿇烦,使⽤很是不便,在后续的UI需求中开始⼤量使⽤SOUI的前⾝DuiEngine。
美观⽅⾯,SOUI原⽣⽀持Alpha通道,能够实现各种半透明效果,包括主窗体半透明,DUI窗⼝半透明,DUI窗⼝模仿LayeredWindow(分层窗⼝)效果等,轻松实现各种异形效果。
可配置⽅⾯,SOUI中所有UI资源都采⽤XML描述,调整UI效果⼀般只需要修改XML资源即可完成。
说到代码层的设计很难⽤语⾔描述,只有亲⾃阅读代码⽅能理解。为⼤部分需要在外部(APP层)经常引⽤的UI相关对象提供引⽤计数设计能够有效减少C++开发中常见的野指针问题,这⼀点还是很好体会,同时系统中也重点解决了如消息分发的分层设计,窗⼝对象的消息重⼊等影响UI使⽤体验的关
键性问题。
SOUI亮点
宽泛的说SOUI多好⼤家并没直观的感觉,下⾯从⼀些具体的点来介绍SOUI。
界⾯布局
也许初学者对于SOUI的布局还不太适应,特别是对于那些习惯了Duilib的布局⽅式的朋友。事实上SOUI的布局应该是最接近程序思维的布局⽅式。前段时间开发Android,仅仅是它的5⼤Layout就能让⼈崩溃,⽽且不同的layout对应的布局属性还不⼀样。SOUI的布局⾮常简单,只有两个布局属性:pos + offset,具体参考博客:
通常使⽤⼀个pos属性就解决布局问题了,pos在XML中使⽤"x1,y1,x2,y2"这样的4个坐标定义⼀个控件在⽗窗⼝中的相对位置,⽽offset则定义通过pos计算出来的位置后在X,Y两个⽅向需要叠加的偏移,偏移值需要乘上窗⼝⼤⼩。
例如下⾯这个需求:
只知道窗⼝需要靠右下⾓,不知道窗⼝⼤⼩的情况,在SOUI中只需要使⽤属性pos=“-20,-30” offset="-1,-1"即可。
渲染流程
⼀个UI中的界⾯元素最后会通过各级⼦窗⼝形成⼀个树状结构。⼀般的渲染流程⾃然是从根节点⼀层⼀层的直到渲染完成所有叶结点。这个过程很简单,可能很多UI库也就做到这个层次(例如DuiLib)。但是对于⼀个⾼性能的UI库仅做到这个层次是不够的,举例来说:⼀个画笔程序需要在OnMouseMove⾥⾯绘制新拾取的线条,本能的做法是获取窗⼝画布,绘制完成后再提交画布(类似Windows API: GetDC and ReleaseDC),⽽不是每⼀次绘制只能请求宿主刷新(请求宿主⽴即刷新依赖于系统对UpdateWindow这个API的响应速度)。
因此⼀个成熟的UI引擎有必要实现GetDC及ReleaseDC这样的接⼝。和基于HWND的窗⼝获取HDC不同,在⼀套DirectUI系统中实现GetDC及ReleaseDC要更加复杂:最关键的问题在于获取前绘制窗⼝的背景,以及提交后绘制窗⼝的前景,要实现窗⼝背景前景的分开绘制⼜需要系统提供绘制在指定Z-Order范围内的窗⼝的能⼒,当然前提是系统中有Z-Order这样的概念。
就算实现了窗⼝的背景与前景的分别绘制,对于⼀个⾼性能的UI引擎可能还是不够的。因为有些时候⼀个窗⼝中的内容是不需要和背景混合的,窗⼝刷新的时候绘制背景是没有意义的(如视频播放窗⼝),就是需要另⼀种技术:窗⼝的跨层渲染(不知道这样命名是不是合适)。当⼀个视频窗⼝需要刷新的时候,它的刷新流程和基本的刷新流程是不⼀样的,渲染时它会跳过它的所有⽗窗⼝直接到这个窗⼝层来,从⽽⼤⼤加速渲染过程。
分层窗⼝
Windows的分层窗⼝是Windows 2000提供的⼀项重要更新。苹果系统的UI很漂亮,有了分层窗⼝,Windows系统上开发的应⽤也可以同样漂亮。
这⾥说的分层窗⼝有两个层次:⼀个是DirectUI的宿主窗⼝中使⽤分层窗⼝技术;另⼀层是在DirectUI的DUI窗⼝系统内部实现分层窗⼝技术。
使⽤分层窗⼝技术听起来⽐较简单,不就是设计⼀个WS_EX_LAYEREDWINDOW属性再使⽤UpdateLayeredWindow(EX)更新窗⼝吗?!如果SOUI只达到这个层次,那和codeproject上随便⼀个demo也没有什么区别。
⾸先要搞清楚,SOUI是⼀套DirectUI系统,⽽不是Demo,因此它不能停留在加载⼀个32位PNG图⽚并显⽰出来这样的层次上。它必须要能够让⽤户能够调⽤各种绘制图形,图像,⽂字的API来组合出⼀个最终需要呈现的32位位图。这⼀点要求看起来简单,在Windows系统上实现起来并不简单,因为Windows上最基本的绘制API(GDI)都是不⽀持alpha通道的。有⼀个简单的选择:GDIPlus。然⽽GDIPlus有⼀个⽑病就是速度太慢,这对于⼀个通⽤的UI引擎来说,全部依赖GDIPlus基本上就宣判了这个引擎的死刑。在SOUI中采⽤渲染引擎抽象的⽅法实现了两种渲染引擎:Skia + GDI。前⾯不是说GDI不⽀持Alpha通过不能⽤吗?没错,直接⽤GDI函数是不⾏的,我们需要适当的改造(具体⽅法参
见代码)。
解决了绘制⽅法,要更新到窗⼝中显⽰也还是有技巧的。有⼈可能知道,使⽤UpdateLayeredWindow这个API更新的窗⼝将收不到WM_PAINT消息。由于在半透明窗⼝中不能直接⽀持有窗⼝句柄的⼦窗⼝的显⽰(如IE控件),SOUI还必须为那些需要容纳窗⼝句柄⼦窗⼝的情况提供⽀持,即通过配置同时⽀持半透明窗⼝与不透明窗⼝。但是我不愿意为两种不同的最终位图呈现模型提供两套不同的机制。解决的办法很简单,通过为半透明类型的窗⼝设计⼀个辅助窗⼝,使⽤它来接收WM_PAINT消息,收到该消息时调⽤UpdateLayeredWindow更新窗⼝。注:这个技术是学习另⼀套UI库MetalBone实现的。
讲完了使⽤宿主窗⼝分层窗⼝,下⾯讲讲DUI窗⼝的分层窗⼝技术的实现。
使⽤分层窗⼝技术能够使UI效果更漂亮,关键技术就在这个层。层是什么?层是⼀组窗⼝的绘制容器,它将该层下所有⼦窗⼝的绘制内容绘制到⼀个独⽴的缓冲区上,最后再⼀起绘制到分层窗⼝的上⼀层绘制缓冲区中。如下图:
A、B、B1、B2、C为DUI系统中5个DUI窗⼝。其中,B、B1、B2是同⼀个渲染层。也就是说设计需要它们先绘制好后再和
A,C做融合。类似的需求对于⼀个漂亮的UI来说可能会很常见。如果在UI引擎中没有层的概念是不可能实现的。如果不需要实现前⾯提到的背景和前景分别渲染的情况,实现会层窗⼝其实也不难,只需要在渲染到B窗⼝时创建⼀个缓冲区,把从B开始的内容渲染到这个缓冲区,完成后再回到正常渲染流程,就像没有B1、B2⼀样。但是SOUI是⽀持背景前景分别渲染的,实现这个过程的代码逻辑就可能很复杂了(可以⾃⼰想象⼀下)。
⾮客户区
HWND的⾮客户区⽤来绘制滚动条及边框及标题栏,菜单栏。客户区是⽤户绘制的常规区域,在设计上将窗⼝的显⽰区域划分为客户区和⾮客户区,有利于⽤户在重写客户区的绘制代码时不被⾮客户区⼲扰,也有利于代码的复⽤。
在DuiLib中,⼀个控件如Richedit需要显⽰滚动条,它需要给这个控件组合两个滚动条控件。这种⽅式虽然看上去没有什么⼤的问题,如果由于窗⼝中内容的变化需要动态显⽰隐藏滚动条时可能会很⿇烦,⾄少它会引起窗⼝布局系统的重排,因为滚动条显⽰和隐藏时控件的客户区⼤⼩是变化的。
⽽在SOUI系统中,滚动条和HWND⼀样,⽤户根本不需要关⼼,因为内部已经⾃动处理好了滚动条,也不会引起布局系统的重
排。
资源加载
⼀般来说SOUI中引⽤的所有资源都在XML中描述。刚⼊门的朋友通常反映SOUI中使⽤资源的⽅式不如DuiLib直接,很难⼊门。但是⼀旦真正理解了SOUI的这种资源组织⽅式⼀定会更喜欢SOUI。
SOUI提供3种资源加载⽅式:⽂件,PE资源,ZIP包。
⾸先SOUI的资源包必须提供⼀个⽂件索引表,对于使⽤PE资源的资源包,索引表就是资源的类型及ID,⽽对于直接使⽤⽂件或者ZIP包的资源,索引表则是⼀个XML⽂件。在索引表中,定义每⼀个资源的type及name两个KEY,SOUI界⾯布局中只能使⽤type和name两个key来引⽤资源。
⽤户只需要准备⼀套⽂件资源,如果需要将资源编译到PE⽂件中,系统提供⼀个⼯具直接从⽂件资源的索引XML转换成rc编译器可以识别的rc⽂件;⽽如果⽤户需要使⽤ZIP资源包,则只需要使⽤⼀个ZIP⼯具如rar, 7z将资源⽂件夹打包即可(推荐使⽤7z打包资源,SOUI内⾃带的zlib 1.2.5能够识别7z打包的带密码的zip包,但不能识别rar打包的带密码的zip包。
窗⼝动画改进
⼀般情况下我们推荐使⽤窗⼝定时器来创建动画。使⽤窗⼝定时器创建动画的好处是定时器和UI是同⼀个线程,⽽SOUI不⽀持多线程同步更新UI(事实上⼀般的DirectUI库都不推荐在⼯作线程中操作UI,
如Android)。那么问题来了,如果为每⼀个DUI窗⼝创建很多定时器,那么系统的消息队列中将充满定时器消息,严重时可能⼤⼤降低UI性能。
解决⽅案:在主窗⼝中创建⼀个10ms间隔的定时器,需要处理动画的窗⼝向系统注册使⽤该定时器,动画记录下⼀次动画需要等待的时间,使⽤该统⼀的定时器计数。
windows开发平台我们看⼀下⾯DEMO中显⽰⼤量动画表情时SOUI的效率:
这⼀CPU占⽤率甚⾄⽐QQ中同样情况下还低。
容器分层
什么叫容器分层?在DirectUI中所有的DUI窗⼝都必须⽣存在⼀个容器中。DUI窗⼝的绘制请求等最终需要由这个容器来实现。在容器不分层的情况下,所有DUI窗⼝在容器中的物理坐标都是从(0,0)开始。这样有什么问题呢?如果要在列表控件的列表项中使⽤DUI控件就会变得⾮常⿇烦,因为在窗⼝滚动时你可能不得不同时更新所有这些控件的坐标。
有了窗⼝层的概念就不⼀样了,每珍上列表项是⼀个新的容器,⽆论列表项显⽰在哪,列表项中的控件(容器中的控件)的坐标都不需要调整。因为有了容器分层,在SOUI中实现包含⼦控件的列表变得⾮常简单(参考下节:⾼性能列表控件)。
⾼性能列表控件
Windows系统中提供的列表控件⾮常简单,只能满⾜简单的数据显⽰需求。注意,是显⽰。然⽽现在的UI需求中经常出现那种即时修改列表控件内容的情况,你将不得不花⼤量的时间对列表控件进⾏⾃绘,⽽效果只能说勉强。
通过研究Android系统中提供的列表控件的代码,借鉴Android中ListView的思想,SOUI实现了⼀套⾼性能的列表控件SListView 及SMcListView。
SListView及SMcListView都是基于虚表技术,同时只创建当前正在显⽰的及部分备⽤的列表项容器,将资源占⽤缩⼩到最少。同时ListView在滚动时能够⾼效刷新,实现了海量数据的⾼性能显⽰及更新。
实现这个⾼性能列表控件的关键有两点:
⾸先是SOUI中实现的容器层的概念,使得列表位位置变化时,容器内部的控件不需要调整坐标。
其次就是容器数据的充分重⽤。
注:上⾯列表中只测试了7W⾏数据,实际上listview中显⽰的数据量多少完全不影响UI性能,亲测700W⾏数据和7W⾏效果⼀样。
⽆窗⼝Richedit
Edit控件是UI中最常⽤的控件之⼀。在允许存在⼦窗⼝句柄的情况下,系统Edit控件已经能够很好的满⾜我们的需求。然⽽在不允许⼦窗⼝句柄的情况下,实现⼀个Edit控件会⾮常⿇烦。
当然,程序可以选择⾃⼰去重新实现⼀套edit,Edit也许还可⾏,⼀般情况下要实现⼀个Richedit基本不可⾏。好在实现Richedit 的模块riched20.dll中把UI和逻辑分离开来,即可以⽤它直接创建有窗⼝的Richedit,也可以⽤它来创建提供⽆窗⼝Richedit的ITextServices接⼝。然⽽即使是这样,程序员需要为ITextServices实现⼀个ITextHost接⼝。尽管MSDN上有相关的⽂档及⽰例,但是根据它们提供的这些资料实现的效果很不理想。必竟只是Demo,不是完整的代码,它不能演⽰开发中可能遇到的每⼀个细节。然⽽恰好是这些细节是影响UI⽤户体验的关键。
所以我们需要另辟蹊径来解决这个问题。我解决这些细节,关键在于理解它们的逻辑。SOUI的办法是到riched20.dll的源代码。好在⽹络上流传着⼀份从WinCE源代码中分离出来的Riched20.dll的源代码,虽然⽤它编译出来的Richedit有很多BUG,但利⽤它可以让我们更好的理解各种细节。⼤家可以测试SOUI中的Edit,效果应该是各种类似库中最好的⼀个之⼀。
XML+LUA
部分模块在SOUI中采⽤了接⼝化设计,如前⾯提到的渲染引擎,以及后⾯要说的多语⾔翻译,以及这⾥要说的脚本模块。
脚本语⾔⽅便灵活,更新简单,LUA脚本还有⾼效的特点。和WEB的HTML+JS类似,SOUI实现了XML+LUA的UI开发解决⽅案。XML实现UI布局,LUA实现逻辑控制。
实现⽅法:
在XML中使⽤<script>标签声明UI中需要脚本⽀持。
通过XML创建UI时⾃动从脚本模块为该UI实例化脚本对象。
采⽤lua_tinker⾃动导出C++类到LUA脚本空间,包含控件对象,及控件事件对象。
在LUA脚本中处理事件响应。
多语⾔翻译
多语⾔翻译对于需要国际化的应⽤来说可能⾮常重要。SOUI通过⼀个语⾔翻译接⼝来执⾏特定上下⽂的多语⾔翻译并且实现了⼀个类似QT语⾔翻译功能的基本XML的语⾔翻译模块。⽤户只需要按照Demo中的语⾔翻译⽂件的组织⽅式组织翻译XML就可以了。
String及其它基于模板的集合对象的参数传递
由于String通常要同时⽀持char及wchar_t这两种字符类型,通常String在⼀个类库都都是以模板形式存在,⽐如WTL,ATL,(MFC太久不⽤,记不清了)。使⽤模板实现的对象有⼀个特点,那就是代码会编译到使⽤它的模块中。如此⼀来,如果在这些模板类中直接调⽤malloc, new等内存分配函数时会在调⽤模块的堆上分配内存,相应地,内存的翻译也需要在调⽤者模块中执⾏。
这有什么问题呢?最⼤的问题莫过于这样的对象不宜在不同的模块之间⽐参数进⾏传递(当然,const参数是没问题的)。如果⼀个这样的对象在A模块中分配的内存到B模块中被翻译,结果只有崩溃。(如果所有模块采⽤MD⽅式动态链接VS的运⾏库是没有问题的)
很多⼩软件是不希望采⽤MD编译的,因为这样的话为了确保程序的正常运⾏,还需要带上VS中相对应的运⾏库,尽管体积不⼤,但也⿇烦。
SOUI中采⽤了⼀点技巧,所有上述模板类都在⼀个独⽴的模块中实现,同时改写了这些类中的内存分配及释放代码。将它们重位向到该模块中的两个内存分配释放⽅法。经过这样处理后,不管这些模板类在哪⼀个模块中实例化,它们要在堆上申请及翻译内存时都是在这个独⽴模块中。通过这个简单的技术有效的解决了这些模板对象不能在不同模块之间传递参数的问题。
先进的事件处理模型
SOUI同时⽀持类似WTL消息映射表⽅式的事件映射表来响应事件,也⽀持新式事件订阅的⽅式响应事件。事件映射表处理事件的优点在于能够规范化的把所有事件处理⽅法在代码⽔平集中到⼀起,⽅便代码的阅读;⽽事件订阅则提供了事件的动态处理能⼒,能够在任意时刻灵活的响应不同控件发出的事件。
结束语
除上述亮点,我相信还有很多细节的处理都体现了SOUI的⼯匠精神,相信⽤⼼的朋友⼀定可以在阅读及使⽤代码的过程中更深的体会到。SOUI是启程软件历时5年⼼⾎的结晶,重复⼀下我以前做《启程输⼊之星》时说的那句话:因为努⼒,所以美丽!
希望能够为您能够喜欢。
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论