Skip to content

SOUI亮点

huangjianxiong edited this page Nov 20, 2018 · 1 revision

原文链接:《UI神器-SOUI》

宽泛的说SOUI多好大家并没直观的感觉,下面从一些具体的点来介绍SOUI。

界面布局

也许初学者对于SOUI的布局还不太适应,特别是对于那些习惯了Duilib的布局方式的朋友。事实上SOUI的布局应该是最接近程序思维的布局方式。前段时间开发Android,仅仅是它的5大Layout就能让人崩溃,而且不同的layout对应的布局属性还不一样。 SOUI的布局非常简单,只有两个布局属性:pos + offset,具体参考博客:http://www.cnblogs.com/setoutsoft/p/3925952.html 通常使用一个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间隔的定时器,需要处理动画的窗口向系统注册使用该定时器,动画记录下一次动画需要等待的时间,使用该统一的定时器计数。 我们看一下面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年心血的结晶,重复一下我以前做《启程输入之星》时说的那句话:因为努力,所以美丽! 希望能够为您能够喜欢。

WIKI 导航

SOUI 概述
使用教程

﹊﹊﹊﹊﹊﹊﹊﹊﹊﹊
This wiki is created by [SOUI Team]

Clone this wiki locally