[Win] 金山界面库KUI

博客首页 » Win 金山界面库KUI

发布于 14 Feb 2015 03:39
标签 blog
金山界面库KUI是伴随金山卫士开源项目而被开发者所了解的一套Windows界面库。

金山的这套开源地址是
http://code.ijinshan.com/
界面库的地址是
https://openkui.googlecode.com/svn/trunk

有不少文章分析了这套界面库
http://blog.csdn.net/breaksoftware/article/details/8652106

以金山界面库(openkui)为例思考和分析界面库的设计和实现——问题

随着物质生活的丰富,人们的精神生活也越来越丰富。人们闲暇的时间也相对变多,于是很多人就开始寻找打发时间的方法。其中电视便是其中一种非常重要的消遣方式。假如我们打开电视机,看到了一个电视台正在播一部我们之前没看过的,正在一招一式进行打斗的武侠片;另一个电视台正在播一部之前也没看过的,正在重复太极推手的教学片。我想大部分人会选择那部武侠片。为什么?因为那是一个动作体系,不会让人厌烦。而那个推手教学片,可能非常高端,可能非常有内涵,但是总是让人缺乏点新鲜感。我之前更关注技术的细节,而今天开始,我将开始分析一款开源的软件界面库。这系列文章不再拘泥于一些技术细节,而从一个思路的方向去分析这个库。
我介绍这套界面库是目前开源的金山卫士开源计划中的一部分。具体的访问地址是 http://code.ijinshan.com/。其中代码的下载SVN地址是https://openkui.googlecode.com/svn/trunk。我将分析的是版本号为54的版本。(转载请指明出于breaksoftware的csdn博客)
在分析这个库之前,我们可以闭上眼睛,清空大脑,思考一下:如果自己要设计和编写一个界面库,将如何规划和设计?将会遇到什么技术问题?将如何做出一些选择?
选择控件方式还是选择直接绘制方式?
首先,我们要做出一个抉择,我们采用窗口控件方式还是采用直接绘制的方式?我们知道windows系统又可称为“视窗系统”,正如其义,我们可以发现,windows系统就是通过一个个窗口展现给我们的。如果使用过SPY++的同学应该发现,windows系统中大部分窗口下的子控件其实也是一些小窗口,只是他们的父窗口被指向了我们看到的那个最最大的最最外层的那个窗口。

如上图中,各个用粗线框起来的部分,就是一个个窗口。这就是问题中所提到的用窗口控件方式。
还有一种方式就是直接绘制,又称为DirectUI。顾名思义,它就是直接在父窗口中绘制各个部分,而不是通过子窗口的形式将各种窗口组成成一个可以协同工作的窗口。最最常见的一个例子便是IE的最最里层那个窗口,它通过其渲染引擎将网页内容绘制在窗口上。这样做有什么好处呢?我们知道,如果我们用控件方式组织网页的话,每个控件都会保存一个句柄,如果一个稍微复杂点的网页,可能有成千上万个元素,也就意味着有成千上万个句柄。这些子窗口还要依赖消息进行窗口管理和绘制。可以想象,这将导致整个网页展现和管理变得非常复杂和庞大。它的执行效率可能连最最差版本的IE都无法比。
那我们选择DirectUI?不,如果我们选择DirectUI,那我在此写这系列文章就没有意义了。而且客户端界面,一般不会有太过于复杂的渲染问题,所以选择窗口控件方式还是可以接受的。如果对内嵌IE式的DirectUI技术赶兴趣的同学可以看两篇相关的博文《如何定制一款12306抢票浏览器——完结篇》和《内嵌IE网页窗口中消除IE默认脚本设置影响的方法》。我这儿就不再赘述。
现在我们确认了使用窗口控件的方式。那我们再抛出第二个问题:使用什么框架?
使用WTL还是MFC?
我相信做windows开发的同学,对MFC很熟悉。我刚毕业的时候,也是看了遍侯捷的《深入浅出MFC》才开始踏上windows开发之路的。但是,之后一直耳闻MFC的种种弊端,其中人们提到最多的一点就是MFC框架复杂容余,编译出来的文件相对较大。于是WTL就进入我们的视野,我曾记得有人给我推荐WTL时,说WTL是微软内部开发用的,从可靠性上来说是没有问题的。但是WTL相对于MFC则要复杂很多,因为你可以发现到处都是模板泛型技术,如果没有一定的C++功底,使用WTL就像云里雾里,非常难受。综合以上分析,我们似乎可以觉得WTL更可以适合我们的开发,因为我们要设计的是一套界面库,我们要设计自己的框架,所以越基础对我们来说是越合适的。
选择好了WTL后,我们来思考下我们这个界面库如何构成?
如何选择描述文件的格式?
自定义一种格式?个人觉得没有必要,毕竟这不是我们界面库的中心问题,我们应该选择一个稳定的,易于表达的格式。可能你会想到HTML,是的,我觉得可以。但是是否我们还可以再精简一点呢?那就是XML了,而且目前已经有开源的XML解析库。我们这样就可以不拘泥XML的细节,专心于其他业务逻辑。
但是有些东西我们还是要考虑的,就是XML内部的属性定义和组织形式。
我们先讨论下组织形式。为了在一开始表述的清晰,我并不准备以XML来讲解,因为其中我们似乎还要探讨我们自定义的XML属性名等问题。为了简化,同时为了贴近我们日常中能遇到的场景,我将使用大家比较熟悉的HTML作为例子。HTML已经为我们定义好了属性和语法,我们将主要从组织形式来思考,并且可以在已有的HTML技术中吸取其发展中产生的优化点。
我们先看一个例子
[html] view plaincopy
<html>
<head>
</head>
<body>
<img id="id1" src="http://xxxx.xxx.xx/xx.xx" height="200" width="200" atl="AAAAAA"/>
<img id="id2" src="http://yyyy.yyy.yy/yy.yy" height="200" width="200" atl="BBBBBB"/>
<img id="id3" src="http://xxxx.xxx.xx/xx.xx" height="300" width="400" atl="CCCCCC"/>
<img id="id4" src="http://yyyy.yyy.yy/yy.yy" height="300" width="400" atl="DDDDDD"/>
<body>
</html>
上面这段包含四张图片的网页,经过我们观察发现,这段代码是非常容余的,可以精简之。比如我们可以将height="200" width="200" 表示为一个class属性,height="300" width="400" 表示为一个class的属性。这样网页就修改为
[html] view plaincopy
<html>
<head>
<style type="text/css">
class small{
heigth:200;
width:200;
};
class big{
height:300;
width:400
};
</style>
</head>
<body>
<img id="id1" src="http://xxxx.xxx.xx/xx.xx" class="small" atl="AAAAAA"/>
<img id="id2" src="http://yyyy.yyy.yy/yy.yy" class="small" atl="BBBBBB"/>
<img id="id3" src="http://xxxx.xxx.xx/xx.xx" class="big" atl="CCCCCC"/>
<img id="id4" src="http://yyyy.yyy.yy/yy.yy" class="big" atl="DDDDDD"/>
<body>
</html>
我们还可以发现有些容余,就是src和atl字段。我们有没有办法将这两个东西简化呢?我对HTML不熟悉,我知识范围内不知道该如何解决这个问题。但是记得曾经做MFC时,在资源文件RC中,有个字符串表(string table),其中保存的是多个字符串键值对。这也是种思路,当然HTML可能不支持这种形式。如此,HTML已经不能满足我们的描述了。我们回到XML来。对于以上的情况,我们可以分为3个XML文件,其中一个用于描述字符串,一个用于描述类型,一个用于描述界面。
字符串描述表
[html] view plaincopy
<string id=" xx">http://xxxx.xxx.xx/xx.xx<string/>
<string id= "yy">http://yyyy.yyy.yy/yy.yy<string/>
<string id= "A">AAAAAA<string/>
<string id= "B">BBBBBB<string/>
<string id= "C">CCCCCC<string/>
<string id= "D">DDDDDD<string/>
类型描述
[html] view plaincopy
<class name="small" height=200 width=200></class>
<class name="big" height=300 width=400></class>
界面描述
[html] view plaincopy
<body>
<img id="id1" src="xx" class="small" atl="A"/>
<img id="id2" src="yy" class="small" atl="B"/>
<img id="id3" src="xx" class="big" atl="C"/>
<img id="id4" src="yy" class="big" atl="D"/>
</body>
这样就清爽很多了。KUI对我之上的设计做了更细的划分,我们将在之后介绍。
如何读取保存界面元素属性?
有了界面描述文件,下一步就是读取这个文件了。我们大致想象一下这个过程,我们可能需要新建一个结构体,用于描述子控件的属性,举个简单的例子,以下是一个子控件A的描述结构体:
[cpp] view plaincopy
struct StControl{
int x;
int y;
int width;
int heght;
};
因为子控件B内部可能包含多个其他子控件A。于是这种关系可以使用如下结构体表示
[cpp] view plaincopy
struct StControlEx{
StControl stParant;
list<StControl> ListChildrenControl;
};
而多个子控件B可能又同时组成了另一个窗口控件C,那么就该表示为
[cpp] view plaincopy
struct StControlExEx{
StControl stParant;
list<StControlEx> ListChildrenControlEx;
};
那么多个子控件C可能又同时组成另一个窗口控件D,那么就该表示为
[cpp] view plaincopy
struct StControlExExEx{
StControl stParant;
list<StControlExEx> ListChildrenControlExEx;
};
……
如此将子子孙孙无穷无尽矣。因为控件有多少层,我们要有多少个控件描述结构体与其对应。很明显这样的设计非常不好。那么我们将如何设计呢?对这个问题,我们将在之后对KUI源码进行分析时,给出它的解决方案。
如何通过界面元素属性设置控件?
一般来说,窗口必然会存在以下的属性:
位置:X,Y,Width,Height或者LeftTopX, LeftTopY,RightBottonX,RightBottonY
那么是否我们可以定义如下的结构体和XML
[cpp] view plaincopy
struct StWindow{
int nLeftTopX;
int nLeftTopY;
int nRightBottomX;
int nRightBottomY;
};
[html] view plaincopy
<window pos=''10,10,30,40' ></windows>
对应的,现在我们可以设想下我们可以定义一个基础类CBaseWindow
[cpp] view plaincopy
class CBaseWindow{
public:
void SetCommonAttribute(const StWindow& );
}
继承于该类的类都将具有SetCommonAttribute函数以用于设置这些基础属性。
如果只有这些属性,该控件可能就是一个有底色的窗口。但是我们的控件是丰富多彩的,这意味着它们也会有丰富多彩的属性。以按钮为例,我们可能要新增文字内容属性。于是我们要扩展我们的按钮类为
[cpp] view plaincopy
class CButton: public CBaseWindow{
public:
void SetTextAttribute(const CString& cstrText);
}
再假设我们有个特殊的按钮,那个按钮的文字颜色要是可以指定的,于是我们又要扩展个按钮类出来
[cpp] view plaincopy
class CSpecialButton: public Cbutton{
public:
void SetTextColor(const RGB& );
}
再假设……
可以想象,如果我们这么设计将会导致非常的繁琐。而上层的调用也将非常复杂,比如我们这个CSpecialButton类,它实例化时将执行如下
[cpp] view plaincopy
SetCommonAttribute(stwindow);
SetTextAttribute(cstrText);
SetTextColor(rgb);
这个还算好的,如果还有更多的属性,那这个调用将非常的没有复杂。怎么解决这样的问题呢?我们将分析KUI库,看看它是如何解决这个问题的。
界面描述文件的放置位置
如果以上问题解决了,我们之后将不会出现构架上的问题。因为我们已经拿到了界面描述信息了,下步就是在合适的地方,让子控件接收并设置这些属性即可。
现在我们再抛出一个问题:如果我们将我们界面描述文件作为独立的文件放在用户的电脑上,可能存在被恶意篡改的可能。还有就是,作为独立的文件,如果其中任何一个文件被破坏了(比如下载失败了),将导致整个界面出现异常。可以见得这样做存在比较大的风险。那么如何解决呢?目前市面上很多软件都将界面描述文件作为资源文件保存在PE文件中,这样PE完好,则界面完好;PE受损,可能程序就不能执行了。而且从技术角度说,修改PE文件的难度比修改XML文件门槛要高些。假如你也认为这是一个好方法,那么坏的问题就来了。一款软件的界面可能需要很多界面描述文件以及图片资源,我们总不能让使用我们界面库的同学,在编译工程时将这些资源文件一个一个加入到工程中吧!想想这个也是一个繁琐的问题。程序员最最讨厌重复无聊的工作!那怎么办呢?我们可以让他们将这些资源文件合并成一个文件,一个简单的方法就是将这些文件变成一个压缩包。然后将这个压缩包放到资源文件中。如果你认为这也是个好办法,那么坏的问题又来了。我们如何通过资源文件来使用压缩包中的文件呢?我们可以初步设想下过程:
读取指定资源,将其保存到硬盘(内存)中。
将保存到硬盘(内存)中的压缩包文件解压。
遍历读取解压包中的文件。
那KUI是不是这么做的呢?我们拭目以待。
带着以上这么多选择和问题,我们将在之后的章节中,一一介绍KUI是如何解决问题的,并从中尽量吸取其思想的精髓。

http://blog.csdn.net/breaksoftware/article/details/8664450

按照软件的执行流程,我们首先遇到《以金山界面库(openkui)为例思考和分析界面库的设计和实现——问题》中提出的最后一个问题:界面描述文件的放置位置。我们曾提出一种方案:将界面描述文件打包后放在资源文件中;在使用时,解析并读取资源文件。实际上Kui也是按照我们这个思路在做的,只是做得比我们要精巧。在阅读这部分代码的过程中,我发现其存在一定的编码缺陷以及设计缺陷。我会在文中适时指出问题并提出修正及改进的方案。(转载请指明出于breaksoftware的csdn博客)
为了表述方便,我们将以KUI自带的例子工程Sample1为例。在该项目的res目录下,我们看到一个名字为sample1.kui的文件。

在Sample1工程的资源文件中,上图中sample1.kui将作为一个类型为“SKIN”,名字为“KUIRED.DAT”的资源。

从这个特殊的后缀名.kui可以猜测出,这个文件是一个压缩文件。

这样,我们心里有了底,同时为我们阅读Kui的资源管理代码提供了视觉上的参考。
在openkui\KUILib\kscbase\src下有个文件kscres.cpp。它定义了资源文件处理逻辑。
首先,我们查看这段代码
[cpp] view plaincopy
KAppRes& KAppRes::Instance()
{
static KAppRes _singleton;
return _singleton;
}
可以看出,这是个单例类。因为界面描述数据只需要读取和解析一次,所以这儿设计成单例类。以后使用它的地方,就不用重复读取和解析了。
我们再看下作为私有函数的构造函数,它显示该类执行的脉络
[cpp] view plaincopy
KAppRes::KAppRes() : m_hTempRes(INVALID_HANDLE_VALUE)
{
PrepareRes();
OpenResPack();
LoadStringRes();
LoadImageRes();
LoadXmlRes();
LoadFontRes();
}
粗略看了函数名。可以得出如下流程

除了“读取String”、“读取Image”和“读取字体”资源外,我们可能比较难以猜测到其他过程做了什么。如果按照我前一篇的思路,“预处理资源文件”可能对应于“读取指定资源”,“打开资源文件”可能对应于“将压缩包文件解压”,是不是如此呢?我们拭目以待。在解读之后的代码之前,我有个疑问,这些操作如果有一步没有成功,还有必要继续往下走么?怎么就没一个判断?放下这个问题,我们看之后的代码。
我们先看
[cpp] view plaincopy
bool KAppRes::PrepareRes()
{
bool retval = false;
KFilePath pathRes = KFilePath::GetFilePath(g_hInstance);
HRSRC hResInfo = NULL;
HGLOBAL hResDat = NULL;
PVOID pResBuffer = NULL;
DWORD dwResBuffer;
wchar_t szTempPath[MAX_PATH] = { 0 };
wchar_t szTempFilePath[MAX_PATH] = { 0 };
pathRes.RemoveExtension();
pathRes.AddExtension(L"kui");

if (GetFileAttributesW(pathRes) != INVALID_FILE_ATTRIBUTES)
{
m_strResPackPath = pathRes.value();
}
else
{
hResInfo = FindResourceW(_ModulePtr->GetResourceInstance(), L"kuires.dat", L"SKIN");
if (!hResInfo)
goto clean0;

hResDat = LoadResource(_ModulePtr->GetResourceInstance(), hResInfo);
if (!hResDat)
goto clean0;

pResBuffer = LockResource(hResDat);
if (!pResBuffer)
goto clean0;

dwResBuffer = SizeofResource(_ModulePtr->GetResourceInstance(), hResInfo);
m_memZipRes.SetData(pResBuffer, dwResBuffer);
}

retval = true;

clean0:
return retval;
}
到12行,都是在Exe文件所在目录拼接出与Exe文件同名,但是后缀为kui的资源文件。比如我的电脑上,调试文件目录是D:\快盘\Code Project\openkui\Samples\Sample1\Debug\Sample1.exe,得到的pathRes对应的目录是D:\快盘\Code Project\openkui\Samples\Sample1\Debug\Sample1.kui。如果该资源文件独立存在于Exe目录下,则使用该文件做后续操作。如果该文件不存在,则从PE文件资源中,读取出类型为“SKIN”、名字为“kuires.data”的资源,并保存在memZipRes(一段内存中)中。

这个流程,我们可以看出来,其大体思路和我之前猜测的一致,只是它增加了优先对独立的压缩包资源文件的处理。于是我们可以得出:Kui的界面描述文件,可以放在:
1 Exe文件所在的目录下,名字和Exe相同的、后缀为kui的文件(以后简称界面文件包)中
2 PE文件资源类型为“SKIN”、名字为“kuires.dat”的资源(以后简称界面内存块)中
其中1的优先级要高于2。
这种设计方案还是很有意思的。因为这个流程可以实现换肤功能。比如我们下载了A.kui、B.kui、C.kui和D.kui四套皮肤。如果用户选择了A皮肤,则我们可以将A.kui拷贝到Exe所在目录,并将其命名为与Exe同名、后缀为kui的名字。这样就实现了换肤。即使这套外置皮肤坏了,或者被删了,我们还可以使用资源中的那套皮肤。
虽然想法很好,但是代码中的逻辑却存在一定的编码缺陷和设计缺陷,我们先说编码缺陷:
[cpp] view plaincopy
if (GetFileAttributesW(pathRes) != INVALID_FILE_ATTRIBUTES)
{
m_strResPackPath = pathRes.value();
}
这步,可以用来判断一个文件是否存在么?其实不可以。因为如果我新建一个与压缩包同名的“文件夹”,GetFileAttributesW将返回FILE_ATTRIBUTE_DIRECTORY,这将导致这个错误的逻辑认为该文件夹是一个压缩文件,从而导致之后的逻辑出现处理异常。该函数应该写成
[cpp] view plaincopy
if ( PathFileExists(pathRes) &&
0 == ( GetFileAttributesW(pathRes) & FILE_ATTRIBUTE_DIRECTORY ) )
{
m_strResPackPath = pathRes.value();
}
其中还有个设计缺陷。假如我们是使用这个库的开发者,我们在调试过程中,难免会修改界面描述文件。那么难道我们每修改一次,都要将描述文件压缩成一个包么?这样不是很难调用?我觉得,可以在PrepareRes函数中,新增一段对debug情况的处理:在debug情况下我们应该获取工程res目录下一个特定的文件夹,该文件夹保存了未压缩的各个文件。这样我们就可以不用每次修改资源后都要打个资源包了。
我们在KAppRes类私有成员中增加
[cpp] view plaincopy
#ifdef DEBUG
// 保存debug环境下界面描述文件文件夹目录
std::wstring m_strResFloderPath;
#endif
在PrepareRes的pathRes.RemoveExtension();之前新增
[cpp] view plaincopy
#ifdef DEBUG
pathRes.RemoveFileSpec();
pathRes.RemoveFileSpec();
pathRes.AddBackslash();
pathRes.Append(L"res");
pathRes.AddBackslash();
pathRes.Append(L"skin");
pathRes.AddBackslash();
if ( PathFileExists(pathRes) && GetFileAttributesW(pathRes) & FILE_ATTRIBUTE_DIRECTORY )
{
m_strResFloderPath = pathRes.value();
return true;
}
else
{
_ASSERT_EXPR(FALSE, L"Debug环境下要求res目录下skin目录保存界面描述文件");
return false;
}
#endif
这样我们将方便我们调试工作。
接下来我们看OpenResPack这个函数。在PrepareRes中,我们可能会得到界面文件包或者界面内存块。OpenResPack将先后尝试从这两个位置获取界面信息。在这个函数中,我们将看到,如何使用开源的Zlib代码去获取压缩包(内存)中文件的信息。
[cpp] view plaincopy
bool KAppRes::OpenResPack()
{
bool retval = false;
zlib_filefunc_def zip_funcs;
std::string strPathAnsi;
int nRetCode;

HRSRC hResInfo = NULL;
HGLOBAL hResDat = NULL;
PVOID pResBuffer = NULL;
DWORD dwResBuffer = 0;

fill_win32_filefunc(&zip_funcs);
strPathAnsi = UnicodeToAnsi(m_strResPackPath);
m_pResPackData = unzOpen2(strPathAnsi.c_str(), &zip_funcs);

if (m_pResPackData)
goto UNZRESPACKDATA;
这段代码是尝试预处理界面文件包。我们注意下这儿使用了fill_win32_filefunc填充了zlib_filefunc_def结构体,还要注意下我们对unzOpen2传入了界面文件包的路径。我们接着看,预处理之后的流程
[cpp] view plaincopy
UNZRESPACKDATA:
nRetCode = unzGoToFirstFile(m_pResPackData);
while (UNZ_OK == nRetCode)
{
char szCurrentFile[260];
unz_file_info fileInfo;
uLong dwSeekPos;
uLong dwSize;

nRetCode = unzGetCurrentFileInfo(
m_pResPackData,
&fileInfo,
szCurrentFile,
sizeof(szCurrentFile),
NULL,
0,
NULL,
0
);
if (nRetCode != UNZ_OK)
goto clean0;

dwSeekPos = unzGetOffset(m_pResPackData);
dwSize = fileInfo.uncompressed_size;
m_mapResOffset.insert(KResOffset::value_type(szCurrentFile, KResInfo(dwSeekPos, dwSize)));

nRetCode = unzGoToNextFile(m_pResPackData);
}
这段代码,大致可以看出来,这种遍历方式和VC中遍历文件的一种方法——FindFirstFile、FindNextFile很相似。

如此,便将压缩包中的文件信息保存到Map结构体对象m_mapResOffset中。其中信息包括文件的相对目录,文件的相对偏移和大小。
有了这组信息,我们之后读取单个文件,将变得非常方便了。
以上我们讨论了如何使用Zlib获取界面压缩包中文件信息的方法。现在我们再看下如何使用Zlib从界面内存块中获取压缩后的文件信息。

是否还记得,之前我着重提到一点“使用了fill_win32_filefunc填充了zlib_filefunc_def结构体”。之所以着重,是因为我们现在解析界面内存块的信息时,将要自己填充zlib_filefunc_def结构体中各个回调函数。我们先看fill_win32_filefunc内部的实现
[cpp] view plaincopy
void fill_win32_filefunc (pzlib_filefunc_def)
zlib_filefunc_def* pzlib_filefunc_def;
{
pzlib_filefunc_def->zopen_file = win32_open_file_func;
pzlib_filefunc_def->zread_file = win32_read_file_func;
pzlib_filefunc_def->zwrite_file = win32_write_file_func;
pzlib_filefunc_def->ztell_file = win32_tell_file_func;
pzlib_filefunc_def->zseek_file = win32_seek_file_func;
pzlib_filefunc_def->zclose_file = win32_close_file_func;
pzlib_filefunc_def->zerror_file = win32_error_file_func;
pzlib_filefunc_def->opaque=NULL;
}
可以见得,它传递了“打开文件”、“读取文件”、“写入文件”、“移动读标识”和“关闭文件”等操作的函数地址。我粗略看下这些函数的实现,它们只是对CreateFile、ReadFile和WriteFile等文件操作的封装。对应的,对于不在磁盘上的文件,我们可以封装相应的操作内存的函数,然后将这些函数地址传递给该结构体对象。
[cpp] view plaincopy
zip_funcs.zopen_file = ZipOpenFunc;
zip_funcs.zread_file = ZipReadFunc;
zip_funcs.zwrite_file = ZipWriteFunc;
zip_funcs.ztell_file = ZipTellFunc;
zip_funcs.zseek_file = ZipSeekFunc;
zip_funcs.zclose_file = ZipCloseFunc;
zip_funcs.zerror_file = ZipErrorFunc;
zip_funcs.opaque=NULL;
m_pResPackData = unzOpen2((const char*)&m_memZipRes, &zip_funcs);

if (!m_pResPackData)
goto clean0;
我们注意下unzOpen2函数,该函数在声明时指明其是一个文件路径,而我们却将资源的内存块首地址传递进去了。那么unzOpen2可以正确处理么?我们看下ZipOpenFunc函数的实现,就知道这个问题是如何巧妙的解决掉的。
[cpp] view plaincopy
void* ZipOpenFunc(void* opaque, const char* filename, int mode)
{
return (void*)filename;
}
看,它直接将filename返回了。可以想象ZipOpenFunc就是为了打开文件,并定位到首地址。既然传进来的就是内存块首地址,那么直接返回之就行了。而其他函数的实现,也是很简单的,和操作文件一样。比如
[cpp] view plaincopy
long ZipSeekFunc (void* opaque, void* stream, uLong offset, int origin)
{
uLong ret = -1;
CMemFile* pMemFile = (CMemFile*)stream;
DWORD dwRetCode;

if (!pMemFile)
goto clean0;

dwRetCode = pMemFile->SetFilePointer(offset, NULL, origin);
if (INVALID_SET_FILE_POINTER == dwRetCode)
goto clean0;

ret = 0;

clean0:
return ret;
}
在调用解析界面内存块的函数前。OpenResPack还多了一个判断:判断已读取的m_memZipRes是否为空,如果为空,则再从资源文件中读取界面描述块到内存中。
[cpp] view plaincopy
if (strlen((const char*)&m_memZipRes) == 0)
{//防止.kui格式错误导致unzOpen2返回空的m_pResPackData
hResInfo = FindResourceW(_ModulePtr->GetResourceInstance(), L"kuires.dat", L"SKIN");
if (!hResInfo)
goto clean0;

hResDat = LoadResource(_ModulePtr->GetResourceInstance(), hResInfo);
if (!hResDat)
goto clean0;

pResBuffer = LockResource(hResDat);
if (!pResBuffer)
goto clean0;

dwResBuffer = SizeofResource(_ModulePtr->GetResourceInstance(), hResInfo);
m_memZipRes.SetData(pResBuffer, dwResBuffer);
}
这个代码一开始判断m_memZipRes是否为空,存在一定的漏洞:假如资源文件的第一个字符就是\0,则就会认为这段读取的数据为空了。当然,一般不存在这样的问题,因为目前压缩包文件的第一个字符肯定不是\0。但是从代码的严谨性上来说,应该给openkui\KUILib\Include\kscbase下kscmemfile.h中的CMemFile新增一个共有函数
[cpp] view plaincopy
BOOL IsEmpty()
{
return m_buffer.GetCount() == 0 ? TRUE : FALSE;
}
然后那个判断应该改成
[cpp] view plaincopy
If( m_memZipRes.IsEmpty())
{
……
}
还有,这个if中的逻辑PrepareRes中读取资源逻辑一样。应该将其提炼出来,这样可以不会让代码看着十分冗余。我在之后附加的工程中,会将这个函数提炼到一个名字为 GetResInResfile的函数中。
我们接着看之后对数据的读取和保存。
[cpp] view plaincopy
LoadStringRes();
LoadImageRes();
LoadXmlRes();
LoadFontRes();
中前三个函数对应于

KUI提供的例子中,都没有LoadFontRes对应的fonts.xml文件存在。所以我们可以先忽略字体处理这块逻辑。
我们以LoadXmlRes为例,讲解其执行过程。
[cpp] view plaincopy
bool KAppRes::LoadXmlRes()
{
bool retval = false;
void* pBuffer = NULL;
unsigned long dwBuffer = 0;
TiXmlDocument xmlDoc;
const TiXmlElement* pXmlChild = NULL;
const TiXmlElement* pXmlItem = NULL;

if (!GetRawDataFromRes("xmls.xml", &pBuffer, dwBuffer))
goto clean0;

if (!xmlDoc.LoadBuffer((char*)pBuffer, (long)dwBuffer, TIXML_ENCODING_UTF8))
goto clean0;

pXmlChild = xmlDoc.FirstChildElement("xmls");
if (!pXmlChild)
goto clean0;

pXmlItem = pXmlChild->FirstChildElement("xml");
while (pXmlItem)
{
std::string strId;
std::string strPath;

strId = pXmlItem->Attribute("id");
strPath = pXmlItem->Attribute("path");

if (strId.length() && strPath.length())
{
m_mapXmlTable[strId] = strPath;
}

pXmlItem = pXmlItem->NextSiblingElement("xml");
}

retval = true;

clean0:
if (pBuffer)
{
FreeRawData(pBuffer);
}

return retval;

}
第10行的GetRawDataFromRes是我们特别需要注意的一个函数。该函数传入一个文件相对路径、用于保存该文件内容的内存块首地址和该内存块的大小。
[cpp] view plaincopy
bool KAppRes::GetRawDataFromRes(
const std::string& strId,
void** ppBuffer,
unsigned long& dwSize
)
{
bool retval = false;
KResStore::iterator store;
KResOffset::iterator offset;
unsigned long dwOffset;
int nRetCode;

if (!ppBuffer)
goto clean0;

offset = m_mapResOffset.find(strId);
if (offset == m_mapResOffset.end())
goto clean0;

dwOffset = offset->second.first;
dwSize = offset->second.second;

*ppBuffer = new unsigned char[dwSize+1];
if (!*ppBuffer)
goto clean0;

nRetCode = unzSetOffset(m_pResPackData, dwOffset);
if (nRetCode != UNZ_OK)
goto clean0;

nRetCode = unzOpenCurrentFile(m_pResPackData);
if (nRetCode != UNZ_OK)
goto clean0;

nRetCode = unzReadCurrentFile(m_pResPackData, *ppBuffer, dwSize);
if (0 == nRetCode)
goto clean0;

retval = true;

clean0:
if (!retval)
{
if (ppBuffer)
{
if (*ppBuffer)
{
delete[] (*ppBuffer);
*ppBuffer = NULL;
}
}
}

return retval;
}
该函数先在保存文件信息的map中寻找传入的相对路径对应的文件信息,然后动态分配一段大小合适的空间(如果成功,则在函数外部释放,否则在函数内部释放),再使用unzSetOffset将压缩包读取位置设置到相应的偏移处,通过unzReadCurrentFile将指定文件读到内存中。是否还记得,我曾提出,这个库在设计时存在一定的缺陷:没有考虑debug情况下会经常修改界面文件的问题。我们之前在PrepareRes函数中获取了保存界面描述文件(非压缩)的路径。这样,我们可以对该函数做段修改,入参都不用改,我们只是让该函数读取指定文件的内容。
[cpp] view plaincopy
#ifdef DEBUG
if ( ReadResFile(strId, ppBuffer, dwSize) ) {
return true;
}
else {
// _ASSERT_EXPR(FALSE, L"debug下从界面描述目录读取文件失败");
return false;
}
#endif
我封装了一个读取文件的函数ReadResFile
[cpp] view plaincopy
#define NEWBUFFERSIZE 0x100

bool KAppRes::ReadResFile(
const std::string& strId,
void** ppBuffer,
unsigned long& dwSize )
{
std::string strFilePath = CW2A(m_strResFloderPath.c_str());
strFilePath += strId;
HANDLE hFile = CreateFileA(strFilePath.c_str(), GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL );
if ( NULL == hFile ) {
return false;
}

// 先分配读取的数据空间
DWORD dwTotalSize = NEWBUFFERSIZE; // 总空间
char* pchReadBuffer = new char[dwTotalSize];
memset(pchReadBuffer, 0, NEWBUFFERSIZE);

DWORD dwFreeSize = dwTotalSize; // 闲置空间

bool bSuc = false;
do {

char chTmpReadBuffer[NEWBUFFERSIZE] = {0};
DWORD dwbytesRead = 0;

// 用于控制读取偏移
OVERLAPPED Overlapped;
memset(&Overlapped, 0, sizeof(OVERLAPPED) );

while (true) {

// 清空缓存
memset(chTmpReadBuffer, 0, NEWBUFFERSIZE);

// 读取管道
BOOL bRead = ReadFile( hFile, chTmpReadBuffer, NEWBUFFERSIZE, &dwbytesRead, &Overlapped );
DWORD dwLastError = GetLastError();

if ( bRead ) {
if ( dwFreeSize >= dwbytesRead ) {
// 空闲空间足够的情况下,将读取的信息拷贝到剩下的空间中
memcpy_s( pchReadBuffer + Overlapped.Offset, dwFreeSize, chTmpReadBuffer, dwbytesRead );
// 重新计算新空间的空闲空间
dwFreeSize -= dwbytesRead;
}
else {
// 计算要申请的空间大小
DWORD dwAddSize = ( 1 + dwbytesRead / NEWBUFFERSIZE ) * NEWBUFFERSIZE;
// 计算新空间大小
DWORD dwNewTotalSize = dwTotalSize + dwAddSize;
// 计算新空间的空闲大小
dwFreeSize += dwAddSize;
// 新分配合适大小的空间
char* pTempBuffer = new char[dwNewTotalSize];
// 清空新分配的空间
memset( pTempBuffer, 0, dwNewTotalSize );
// 将原空间数据拷贝过来
memcpy_s( pTempBuffer, dwNewTotalSize, pchReadBuffer, dwTotalSize );
// 保存新的空间大小
dwTotalSize = dwNewTotalSize;
// 将读取的信息保存到新的空间中
memcpy_s( pTempBuffer + Overlapped.Offset, dwFreeSize, chTmpReadBuffer, dwbytesRead );
// 重新计算新空间的空闲空间
dwFreeSize -= dwbytesRead;
// 将原空间释放掉
delete [] pchReadBuffer;
// 将原空间指针指向新空间地址
pchReadBuffer = pTempBuffer;
}

// 读取成功,则继续读取,设置偏移
Overlapped.Offset += dwbytesRead;
}
else{
if ( ERROR_HANDLE_EOF == dwLastError ) {
bSuc = TRUE;
}
break;
}
}

if ( bSuc ) {
*ppBuffer = pchReadBuffer;
dwSize = dwTotalSize - dwFreeSize;
}
else {
if ( NULL != pchReadBuffer ) {
delete [] pchReadBuffer;
pchReadBuffer = NULL;
}
}
} while (0);

if ( NULL != hFile ) {
CloseHandle(hFile);
hFile = NULL;
}

return bSuc;
}
这样,我们只要在res下新建一个skin文件夹,然后将我们的界面描述文件放在这个目录下即可。

我们看一下xmls.xml文件内容
[cpp] view plaincopy
<?xml version="1.0" encoding="utf-8" standalone="yes" ?>
<xmls>
<xml id="IDR_KSC_SKIN" path="res/def_skin.xml" />
<xml id="IDR_KSC_STYLE" path="res/def_style.xml" />
<xml id="IDR_KSC_STRING" path="res/def_string.xml" />
<xml id="IDR_DLG_MAIN" path="res/dlg_main.xml" />
</xmls>
可以见到其中对应的文件是

在使用KUI库的程序中,我们将使用到这些id。
我们看下最终的读取结果

我们注意到res目录下三个文件这个时候并没有加载。为什么不加载,我们之后会在探索《以金山界面库(openkui)为例思考和分析界面库的设计和实现——问题》中“如何读取保存界面元素属性”问题时,对这个问题作出解释。
总体来说,KUI这套资源管理逻辑存在以下问题:
1 部分代码不严谨
2 设计缺乏对debug环境下的优化
3 读取资源代码容余,应该封装下
[cpp] view plaincopy
bool KAppRes::GetResInResfile()
{
bool retval = false;
HRSRC hResInfo = NULL;
HGLOBAL hResDat = NULL;
PVOID pResBuffer = NULL;
DWORD dwResBuffer;

hResInfo = FindResourceW(_ModulePtr->GetResourceInstance(), L"kuires.dat", L"SKIN");
if (!hResInfo)
goto clean0;

hResDat = LoadResource(_ModulePtr->GetResourceInstance(), hResInfo);
if (!hResDat)
goto clean0;

pResBuffer = LockResource(hResDat);
if (!pResBuffer)
goto clean0;

dwResBuffer = SizeofResource(_ModulePtr->GetResourceInstance(), hResInfo);
m_memZipRes.SetData(pResBuffer, dwResBuffer);

retval = true;
clean0:
return retval;
}
也有其出彩的地方:
1 CMemFile类的编写
2 从内存中解压文件

金山界面库VC向导
http://bbs.csdn.net/topics/390368164
http://www.blueantstudio.net/bkwin/bkwinwiz.html

如何定制一款12306抢票浏览器
http://blog.csdn.net/breaksoftware/article/details/8551035
http://pan.baidu.com/s/1gdGcKl9 密码:z4km

内嵌IE网页窗口中消除IE默认脚本设置影响的方法
http://blog.csdn.net/breaksoftware/article/details/7104632

界面库RingSDK和金山开源KUI界面
http://blog.csdn.net/wangxiaoqin00007/article/details/17239845

利用金山开源界面库仿照QQ的界面
http://www.ylzx8.cn/windows/vc-mfc/135507.html

金山KUI库界面实现
http://express.ruanko.com/ruanko-express_42/tech-overnight4.html

VC++通用GIS功能开发解决方案 2.0v 介绍
http://www.vckbase.com/index.php/wv/652

分离自金山卫士开源项目的DirectUI界面库
http://www.educity.cn/xiazai/8443779.html


本页面的文字允许在知识共享 署名-相同方式共享 3.0协议和GNU自由文档许可证下修改和再使用,仅有一个特殊要求,请用链接方式注明文章引用出处及作者。请协助维护作者合法权益。


系列文章

文章列表

  • Win 金山界面库KUI

这篇文章对你有帮助吗,投个票吧?

rating: 0+x

留下你的评论

Add a New Comment
Unless otherwise stated, the content of this page is licensed under Creative Commons Attribution-ShareAlike 3.0 License