代码改变世界

10 插件系统设计 (终章)

2013-05-18 01:18  浪客倩心  阅读(1787)  评论(0编辑  收藏  举报

10 插件系统设计

本章作为终章将模仿Windosw 8的开始屏幕设计一个插件系统,插件将由UI、业务逻辑组成。

10.1 UI插件的可行性

插件被编译为dll,被主程序载入的dll将拥有自己独立的栈空间。UI主程序将拥有一套GXUI框架对象(包括UIDXInit、UIAnimateManage、UIMessageLoopManage、UICamera、UIRender、UIWinShell),如果插件包括UI,那么该插件中也将包含一套GXUI框架对象,这样会产生冲突。

为解决该冲突,设计中将已主程序的GXUI框架对象为主,并引用到UI插件中。具体实现方式为,在框架对象中添加一个引用赋值函数,已类UIAnimateManage为例如下:

         class UIAnimateManage

{

public:

                   ……

         static void Assign(UIAnimateManage * pPara );

{

                  _pSingleton = pPara;

                  UICaret::GetSingleton()->Assign( (UICaret*)pPara->_animateList.front() );

}

};

UI插件中需定义一个专用接口将整个GXUI框架对象引用到dll插件中,如下:

__declspec(dllexport) void PlugINInit( UIDXInit* pPara1, UIAnimateManage* pPara2, UIMessageLoopManage* pPara3, UICamera* pPara4, UIRender* pPara5, UIWinShell* pPara6 )

{

UIDXInit::Assign( (UIDXInit*)pPara1 );

UIAnimateManage::Assign( (UIAnimateManage*)pPara2 ); 

UIMessageLoopManage::Assign( (UIMessageLoopManage*)pPara3 ); 

UICamera::Assign( (UICamera*)pPara4 ); 

UIRender::Assign( (UIRender*)pPara5 );

UIWinShell::_pWinShell = (UIWinShell*)pPara6;

}

 

解决GXUI框架对象冲突问题后,在载入插件dll后需创建该dll中的UI对象,该UI对象的父容器来自于主程序,同时创建的相对位置也由主程序决定,为了创建功能插件dll中需再定义一个创建接口,代码如下:

__declspec(dllexport) void PlugINCreate( UIContainer* pPara1, RECT* pPara2 );

 

至此,主程序可以载入并调用上述两个接口来显示UI插件。在Demo10_1中示例了一个基础UI插件的载入显示功能,显示效果与Demo9_1一致。

 

 

单件类通过跨dll的指针赋值是比较危险的行为,一般不建议这样做,因为其函数的调用行为涉及到对象的内存布局而较复杂甚至难以保证设计中的功能,下面将粗略的讨论下该问题。

 

C++中类对象的内存布局分为虚函数表地址、成员对象两部分,每个类对象都有自己的内存空间(注意:该类所有对象的虚函数地址都将指向同一张虚函数表)。而类的静态成员和静态函数属于整个类,在该类没有任何对象时便已分配空间。而成员函数则在类的第一个对象构造时分配到某一个地址段,但后续的类对象构造将不再分配,而是直接指向第一次分配的地址段。

 

         主进程和dll模块都拥有个子独立的栈空间,假设将dll中的A2指针赋值给主进程中A1指针,要想运行不崩溃,A1和A2的内存布局必须要一致,最简单的方式即为主进程中类A1和dll中类A2拥有同样的类定义(注意:成员函数声明必须一致,定义可以有区别)。

 

A1在获得A2的指针赋值后,运行角度而言,主程序只知道A1定义,并不知道A2的存在,其运行按以下规则:

 

1)  成员函数调用将按照A1的定义进行调用,即将会调用A1类的成员函数地址段;

 

2)  虚函数调用是通过对象的虚函数表进行的,而对象是dll内存空间中A2类生成,最终通过虚函数表将调用dll中的A2类的虚函数;

 

3)  对于第2点,不只是类的虚函数,类的成员对象中包含的虚函数都将最终调用到dll中定义的虚函数,这可能会引起严重混乱;

 

10.2 UIStartScreen开始屏幕

GXUI库设计了一个控件UIStartScreen用于主程序,能查找并载入插件目录下所有UI插件,在界面上列出所有UI插件,并可显示选中的UI插件。

从界面上看,UIStartScreen由两部分组成:

1)底部的主菜单栏;

2)仿metro界面,用以列出所有UI插件;

当仿metro界面的上某个UI插件被选中时,仿metro界面会被插件UI替换掉,同时主菜单栏也会由选中的UI插件指定并更新。

10.2.1 UIMainBar

主菜单栏定义UIMainBar,由三部分组成:

1)home button按钮;

2)活动菜单;

3)信息显示tip

 

本质上UIMainBar也为控件,其设计目标如下:

1) 点击Home button按钮会通告父窗口即开始屏幕;

2) 点击活动菜单会通告父窗口,最终该通告将将被转发到对应的插件;

3) 活动菜单上鼠标悬停、选中能有不同的绘制效果;

4) 信息显示tip能显示不同信息;

 

内部计算函数如下:

函数

功能

void CalcArea();

计算内部布局;

void CalcMenu();

计算活动菜单每个菜单项的位置;

 

设置读取相关函数如下:

函数

功能

void AttachMenu( VECTOR_STRING& strList, int selectIndex=-1 );

重置活动菜单并设选中项;

void ClearMenu();

清除活动菜单;

void SetShowTip( std::string str );

设置要显示的信息;

 

消息处理函数如下:

函数

功能

bool OnMouseMove( POINT pt );

维护鼠标悬停标志;

void OnMouseLeave( POINT pt );

维护鼠标悬停标志;

bool OnLButtonUp( POINT pt );

发送NOTIFY消息;

void OnUpdate( UINT flag, void* data );

安全设置内部数据,对应上表格的设置函数;

 

UIMainBar绘制按以下过程:

1) 绘制hover效果;

2) 绘制home button

3) 绘制活动menu

4) 绘制显示tip

10.2.2 UIMetro

仿metro界面被定义为UIMetro,用于列出UI插件,而一个UIMetro包含6默认不显示的瓷片(tile),每个瓷片都可设置与UI插件相绑定并显示。

 

UIMetro内部瓷片被定义为UITile控件,其设计目标如下:

1) 每个瓷片的绘制包括一张图片和title,而图片IDtitle都从绑定插件中获得;

2) 鼠标悬停、离开会产生不同的动画效果;

3) 点击瓷片会产生击鼓动画,且该动画不能被离开动画所打断;

4) 击鼓动画完成后会通告UIMetro其绑定插件被选中,最终该通告消息被传递到UIStartScreen内进行处理;

 

继承以及内部计算函数函数:

函数

功能

void UninitAnimate();

击鼓动画完成后通告父窗口;

void CalcArea();

计算图片位置、title位置;

 

设置读取相关函数如下:

函数

功能

void BindPlugIN( PlugINOBJ* pPlugIN );

绑定插件;

void SetHoverDirect( bool flag );

设置悬停状态;

 

消息处理函数如下:

函数

功能

bool OnMouseMove( POINT pt );

维护鼠标悬停标志;

void OnMouseLeave( POINT pt );

维护鼠标悬停标志;

bool OnLButtonDown ( POINT pt );

触发击鼓动画,并在动画完毕后发送NOTIFY消息;

void OnUpdate( UINT flag, void* data );

安全设置内部数据,对应上表格的设置函数;

 

UITile绘制函数如下:

函数

功能

void DrawNoHot();

绘制非悬停状态的tile

void DrawBackGround();

绘制背景;

void DrawHotEffect( UCHAR alphy );

绘制悬停效果;

void DrawImage();

绘制图片;

void DrawTitle();

绘制title

 

 

UIMetro用于管理UITile,其设计目标如下:

1) 管理并绘制所有UITile,最多6个;

2) 启动时会进行冒泡动画,即所有显示的tile依次缩放显示出来,像依次冒泡出来;

3) 将UITile的插件选中Notify传送给UIStartScreen进行处理;

 

继承以及内部计算函数函数:

函数

功能

void CalcArea();

依次计算内部6UITile的位置,排列为:

1 2 5

3 4 6;

RECT CalcZoomRC( int index, float zoomScale );

根据UITile当前位置大小按比例进行缩放;

 

设置读取相关函数如下:

函数

功能

void SetTitle( int index, PlugINOBJ* pPlugIN );

设置UITile绑定的插件,调用BindPlugIN();

 

消息处理函数如下:

函数

功能

bool OnMouseMove( POINT pt );

设置6UITile的hover状态为false

void OnNotify( int id, int param );

Notify进行转发;

void OnCreate();

创建6UITile,默认不显示;

 

UIMetro绘制函数如下:

函数

功能

void DrawCommon();

普通绘制所有tile

void DrawStartBubble();

冒泡动画绘制所有tile

10.2.3 UIStartScreen

UIStartScreen设计目标如下:

1) 查找并载入插件目录下所有UI插件;

2) 选中某个插件时能进行创建并显示;

3) 包含1UIMainBar、1UITab以及若干个UIMetro;

4) 当有多个UIMetro时,修改UITab添加箭头区域提供切换功能;

5) UIMainBar上的home button通告时能重置开始画面;

6) UIMainBar上的活动菜单栏上通告能转发给选中的插件UI上;

7) 特定通告消息能设置UIMainBar上的消息tip

 

对于目标第1点和第2点,在载入UI插件时并不进行插件内UI创建工作,因为大量的插件UI创建可能带来较大延时,因此插件UI的创建将被放置到插件第一次选中时进行。创建后插件UI将作为UIStartScreen的子窗口存在。

对于目标第3点,UIMetro的个数取决于载入的插件个数,每个UIMetro能对应6个插件,所有的UIMetro都挂到UITab上;

 

内部计算函数函数:

函数

功能

inline int FindSelPlugINIndex();

计算选中插件序号;

 

设置读取相关函数如下:

函数

功能

void LoadPlugINs( std::string folder );

根据目录查找并载入所有插件,并根据插件数生成相应的UIMetro;

 

消息处理函数如下:

函数

功能

void OnNotify( int id, int param );

处理的notify消息,分3类:1UIMainBar上home button通告;2UIMainBar上活动菜单消息;3)选中插件通告;

void OnCreate();

创建UIMainBar、UITab

 

UIStartScreen绘制过程:

1)绘制UIMainBar;

2)如果选中插件则绘制插件;

3)否则绘制UITab以及挂在UITab上的UIMetro

10.3 UI插件对象及相关

10.3.1 UI插件对象

UI插件对象被定义为PlugINOBJ,包含了载入函数bool Load( std::string path ),能通过路径载入相应的插件并获取插件提供的标准接口。

10.3.2 标准接口

为了与UIStartScreen相配合,每个UI插件必须提供以下几个标准接口:

函数

功能

char* PlugINInfor();

提供插件信息;

void PlugINInit( UIDXInit* pPara1, UIAnimateManage* pPara2, UIMessageLoopManage* pPara3, UICamera* pPara4, UIRender* pPara5, UIWinShell* pPara6 );

插件初始化;

void PlugINCreate( UIContainer* pPara1, RECT* pPara2 );

插件内UI创建

void PlugINSetShow( bool );

插件内UI显示设置

void PlugINDraw();

插件内UI绘制;

int PlugINMenuCount();

插件对应活动菜单项数目;

const char* PlugINMenuCell( int index );

插件对应活动菜单项;

void PlugINMenuNotify( int param );

活动菜单通告;

char* PlugINTileTitle();

插件对应Tiletitle

int PlugINTileImageID();

插件对应TileImageID

10.3.2 UI插件信息回显

有些时候,UI插件需要把一段信息显示到UIMainBar的信息显示tip上,因为UIMainBar是位于主程序中创建,所以插件的UI不能直接找到该UIMainBar对象。

有两种方式可以信息回显:

1)再添加一个回显函数注册接口,将主程序内的显示函数注册到UI插件中来;

2)因为UIMainBar对象是位于UIWinshell内的,而UI插件引用了主程序的UIWinshell,因此可以通过UIWinshell来获取UIMainBar对象进行显示;

因为第1点增加了复杂度,GXUI库采纳了第2种方式,在UIWinshell内添加一个虚函数virtual void ShowPlugINMsg( const char* msg ),只需通过继承并实现该函数,然后在UI插件中通过UIWinshell指针调用该函数即可。

10.4 GXUI库状态

Demo10_2示例了多个插件(实际上只创建了一个插件,并将其拷贝复制多份),下图为多个插件状态:

 

下图示例了选中一个插件时的状态:

 

 

最终,GXUI库引入了2个新文件,UIPlugIN.h/cpp所有插件相关都位于里面,同时其它文件的改变也如下表所示:

 

更改文件

添加内容

描述

UIUtil.h/cpp

bool CheckSplit( …… );

判定是否为分隔符;

UIUtil.h/cpp

void SplitWords( …… );

分隔字符串;

UIUtil.h/cpp

bool GetExeDirectory( …… );

获得exe所在目录

UIUtil.h/cpp

bool GetUpDirectory( …… );

获得給定目录的上一级目录;

UIUtil.h/cpp

bool ReadFolderAllFiles2( …… );

读取指定目录下所有特定后缀的文件;

UIWidget.h/cpp

UITab控件的修改;

添加arrow显示并支持切换

 

 

 下载相关:

Demo10_1Demo10_2Resource.zipD3DX9_43.dll

 

参考

1 中文名《DirectX 9.0 3D游戏开发编程基础

英文名<Introduction to 3D Game Programming with DirectX 9.0>

 

作者:biyou  联系方式:yancheng.huang@gmail.com