oceanheart 2016年12月05日
DuiVision自定义控件开发指南
DuiVision是参考了仿PC管家程序、金山界面库、DuiEngine、DuiLib等多个基于DirectUI的界面库开发的。
本文主要介绍基于DuiVision的自定义控件如何开发,通过自定义控件实现控件的扩展。

DuiVision代码结构

DuiVision代码的整体架构如下图所示:

代码可以分为DuiVision库、DuiVision应用程序、DuiVision插件几部分,其中DuiVision应用程序和DuiVision插件都是基于DuiVision库开发的。
DuiVision库的类结构中几个重要的类说明如下:
CDuiObject:
是所有DuiVision对象的基类,每个对象都包含有ID、名字、区域这些DUI对象基础的东西,同时也可以给对象设置一个事件处理对象(CDuiHandler),DUI基类还封装了一些虚函数,包括对象加载(每个对象都可以对应到xml描述文件中的一个node节点,DUI基类封装了对xml节点的加载方法,包括读取xml节点名、节点属性,并对基础的属性进行处理)、消息处理、设置对象更新标识等。
CControlBase:
是所有DUI控件的基类,定义了所有控件都具有的属性,以及公共的方法、虚函数等,CControlBase定义的虚函数用于派生的控件实现特定的功能,包括鼠标、键盘操作、控件的画图函数、控件大小和位置变更函数等。
CControlBaseFont:
所有需要显示文字的控件都可以从此控件派生,这个控件定义了一些文字的公共属性和方法,包括文字标题、字体、对齐方式等。
CDlgBase:
对话框类,实现了对话框的所有功能,可以支持模态和非模态对话框,对话框中会包含若干子控件,对话框可以定义对应的xml描述文件,通过xml文件可以加载下层的所有子控件,所有的事件处理也都是从对话框发起,然后逐层递归调用到子控件的处理函数。
DuiSystem:
是DuiVision的所有资源的入口,承担了所有资源的统一管理功能(图片、配置、字符串、对话框、事件处理对象都可以作为资源),同时作为工具类提供了很多公共的函数,DuiSystem是一个单例对象,一个应用程序或插件中只有一个DuiSystem的实例对象。

自定义控件的开发

控件概述

所有控件都是从CControlBase或ControlBase的某个派生类派生的,对于一个控件,主要要完成的事情包括:初始化(属性的解析和处理)、位置数据的处理(位置和大小变更)、画图、事件的处理(鼠标事件、键盘事件等);
初次之外,对于复杂一些的控件,可能还要考虑控件焦点、tooltip、动画、动作响应等事情。下面会依次对控件的这些功能如何开发进行说明。

构造函数和析构函数

控件的初始化在控件的构造函数中:
CControlBase(HWND hWnd, CDuiObject* pDuiObject);
控件的析构函数主要用于释放控件中申请的内存、图片等资源,特别需要注意的是,如果控件中有用到一些图片资源,在析构函数中一定要释放,释放的方法都比较类似,例如Button控件中定义了Button的图片,在析构函数中就用如下的方法进行图片的释放:

CDuiButton::~CDuiButton(void)
{
if(m_pImageBtn != NULL)
{
delete m_pImageBtn;
m_pImageBtn = NULL;
}
}


图片的定义

很多控件中都需要定义图片资源,例如Button控件中定义的Button的图片,就是由下面这种四种状态的图片组成的,DuiVision中很多控件的图片都需要包含多个状态的图片:

在ControlBase.h中有按钮类图片的状态定义:

enum enumButtonState
{
enBSNormal = 0,
enBSHover,
enBSDown,
enBSDisable,
enBSHoverDown,
enBSDisableDown
};

完整的状态有六种,分别是正常状态、鼠标移动到控件上的状态、鼠标在控件上按下的状态、禁用状态、按下时鼠标移动到控件上的状态、按下时控件被禁用的状态,一般只会用到前四种状态,后面两种状态是检查框、广播按钮这样的控件才有的。
DuiVision中的控件用到的这种多状态图片一般是按照每种状态的图片横向排列在一个大图中,顺序就是按照上面定义的顺序,当然对于自定义控件也可以按照其他的方式制作大图,只要画图时候按照定义的方式进行解析就可以了。
除了按钮的图片,下面列出了一些常见的多状态图片。
检查框的六状态图片:

广播按钮的六状态图片:

Edit控件的外框的四状态图片:

在自定义控件中定义图片资源,可以使用DuiVision提供的几个宏来减少代码量,举个例子,例如定义上面按钮的图片资源,需要写的代码如下:
1、在头文件的类定义中使用DUI_IMAGE_ATTRIBUTE_DEFINE宏来定义一个图片,这个宏的参数是图片资源的变量名中的一部分,例如Btn表示按钮的图片,这个宏一般定义在控件的属性定义部分的上面:
    DUI_IMAGE_ATTRIBUTE_DEFINE(Btn);        // 定义按钮图片
DUI_DECLARE_ATTRIBUTES_BEGIN()
DUI_COLOR_ATTRIBUTE(_T("crtext"), m_clrText, FALSE)
DUI_DECLARE_ATTRIBUTES_END()


实际通过这个宏会定义两个类成员变量,分别是:
Image* m_pImageBtn;
CSize m_szieBtn;

这两个变量分别是图片对象指针和图片的大小,同时这个宏会定义几个针对这个图片的初始化和通过xml属性加载用到的函数,使用这个宏之后我们可以不用关心复杂的图片定义、加载的细节。

2、定义图片的属性,用于xml文件中定义图片时候的属性名字,例如针对这个Btn图片的属性定义:
DUI_CUSTOM_ATTRIBUTE(_T("img-btn"), OnAttributeImageBtn)

这里定义的img-btn就是在xml中对应的按钮的图片属性名,OnAttributeImageBtn是对应的图片属性加载用到的函数,在上面的图片定义宏中已经隐含的定义了这个函数,所以这里只要按照命名规则写就可以了,命名规则就是OnAttributeImage+前面定义的图片变量名一部分。

3、析构函数中对图片资源的释放
按照上一节所说的析构函数写法就可以,注意其中的图片资源的变量名就是按照 “m_pImage+前面定义的图片变量名一部分” 这样的规则。

4、在控件的cpp实现文件的析构函数下面增加下面这段代码
// 图片属性的实现
DUI_IMAGE_ATTRIBUTE_IMPLEMENT(CDuiButton, Btn, 4)

通过上面这个宏可以实现在头文件中定义的图片的初始化、xml属性加载相关的几个函数,这个宏的第一个参数是控件的类型,第二个参数和头文件中定义的名字一部分相同,第三个参数表示这个图片横向分割为几个小图片。

5、图片的使用
主要是在画图函数中使用,参加控件的画图的章节。

位置数据处理

控件的位置数据是根据父控件的位置变化可能需要进行调整的,DuiVision已经对位置数据的计算进行了封装,包括支持相对位置,当一个控件的位置发生变化时候,控件内的一些位置信息可能需要相应的进行调整,控件内的位置调整就需要控件重载SetControlRect虚函数进行调整了。
对于自定义控件,如果控件中没有类似需要计算的相对位置,则不需要实现这个函数,如果有的话,就需要重载这个函数进行内部的一些位置数据的计算。

画图

控件要在界面显示出来,就必须要实现DrawControl虚函数,下面是检查框控件的画图函数,增加了每一步的说明:
void CCheckButton::DrawControl(CDC &dc, CRect rcUpdate)
{
int nWidth = m_rc.Width();
int nHeight = m_rc.Height();

if(!m_bUpdate) // 是否需要重画整个控件
{
// 申请内存dc
UpdateMemDC(dc, nWidth * 6, nHeight);

// 用内存dc初始化GDI+的graph对象
Graphics graphics(m_memDC);
CRect rcTemp(0, 0, nWidth, nHeight);

// 画种状态的图片
for(int i = 0; i < 6; i++)
{
// 将背景信息先复制到内存dc对应状态的位置
m_memDC.BitBlt(i * nWidth, 0, nWidth, nHeight, &dc, m_rc.left ,m_rc.top, SRCCOPY);

// 在内存dc对应状态的位置画检查框对应状态的图片
graphics.DrawImage(m_pImage, Rect(rcTemp.left, rcTemp.top + (nHeight - m_sizeImage.cy) / 2, m_sizeImage.cx, m_sizeImage.cy),
i * m_sizeImage.cx, 0, m_sizeImage.cx, m_sizeImage.cy, UnitPixel);

// 计算下一个状态的内存dc位置
rcTemp.OffsetRect(nWidth, 0);
}

// 如果设置了检查框文字,则画文字到内存dc
if(!m_strTitle.IsEmpty())
{
m_memDC.SetBkMode(TRANSPARENT);

rcTemp.SetRect(0, 0, nWidth, nHeight);

// 设置字体
......

// 设置对齐方式等参数
......

// 每一种状态的内存dc中叠加上文字的内容
......
}
}

// 将保存的内存dc内容中对应当前状态的部分复制到实际界面显示的dc
dc.BitBlt(m_rc.left,m_rc.top, m_rc.Width(), m_rc.Height(), &m_memDC, m_enButtonState * nWidth, 0, SRCCOPY);
}


检查框控件的实际显示效果如下:

DrawControl函数的实现思路:
1、传入的参数dc和rcUpdate分别表示实际画图的dc和刷新的区域,在DrawControl中第一次需要执行一次完整的画图的流程,将显示内容存储到一个内存dc中,以后如果这个控件的界面显示没有变化的话,DrawControl只需要将保存的内存dc的内容复制到显示的dc就可以了,这样可以加快显示的速度,如果所有控件都没有变化,实际上整个界面的显示就是把所有控件的内存dc内容一层一层复制到显示dc,先后一次性显示出来,复制时候是按照控件添加的顺序进行复制的。DrawControl中用于控制是否重新画图还是仅复制内存dc的开关是m_bUpdate变量,如果为true表示仅复制内存dc,false表示要重画控件。
2、如果是重画整个控件,则需要使用UpdateMemDC函数申请内存dc,每个控件类中都有保存一个内存dc成员变量,UpdateMemDC函数中会判断如果申请的宽度或高度和以前保存的不一样,就会先释放掉以前保存的内存dc,然后在按照新的尺寸申请一个新的,如果和以前一样大小就使用以前的。注意申请内存dc时候一般并不是只申请和控件大小相同的内存dc,而是按照控件有多少种状态,就申请几倍大小的内存dc,例如检查框控件有六种状态,一般的按钮有四种状态,如果是简单显示一个文字或图形,没有多种状态的,则申请和控件大小相同的内存dc就可以了。对于多个状态的内存dc申请,可以按照每种状态的图片在内存dc中横向排列或纵向排列,或者复杂的控件可能会包含了多个子图形的多个状态混合排列在内存dc中,上面检查框的代码中是按照横向排列的,因此申请时候是width乘以6。
3、DuiVision大部分的控件都使用的GDI+进行画图,因此会使用内存dc创建一个GDI+的图形对象,Graphics graphics(m_memDC);然后就是具体的控件的画图操作。
4、对于检查框的具体画图操作,是先画6种状态下的检查框图片到内存dc对应图片的位置,然后再画文字到每种状态对应的位置,这里检查框图片对应每种状态是不同的,文字在每种状态下都是相同的,但也需要重复画到每种状态的位置。
5、对于检查框,还需要考虑控件处于焦点状态下时候要画控件周围的虚线框m_bIsFocus成员变量表示当前控件是否处于焦点状态,m_bShowFocus表示这个控件是否要显示焦点框,这是由showfocus属性决定的,只有这两个变量都为true时候才需要画焦点框。
6、DrawControl最后将内存dc复制到显示dc时候,需要注意的是根据当前的状态,决定把内存dc中对应状态部分的内容复制到显示dc,检查框的内存dc是按照状态横向排列的,所以使用m_enButtonState * nWidth定位到横向的具体内存dc位置。

事件处理

控件的事件处理最常用的是鼠标和键盘事件的处理,相关的几个需要实现的虚函数如下:

virtual BOOL OnControlMouseMove(UINT nFlags, CPoint point);
virtual BOOL OnControlLButtonDown(UINT nFlags, CPoint point);
virtual BOOL OnControlLButtonUp(UINT nFlags, CPoint point);
virtual BOOL OnControlKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags);

检查框的鼠标移动事件处理函数如下:

BOOL CCheckButton::OnControlMouseMove(UINT nFlags, CPoint point)
{
enumButtonState buttonState = m_enButtonState;
if (!m_bIsDisable && !m_bMouseDown)
{
// 设置状态
......
}

if(buttonState != m_enButtonState)
{
UpdateControl();
return true;
}
return false;
}

主要实现思路就是判断鼠标位置是否在控件的范围内,从而决定控件当前的状态应该更改为什么,并且要判断鼠标按下和非按下情况下,对应的状态是不一样的。
根据这些判断可以计算出新的控件状态,在函数进入的时候会保存之前的状态,最后需要比较一下新旧状态是否一致,不一致情况下就需要重新刷新一下控件的界面,刷新方法是调用UpdateControl函数。
鼠标按下和放开的事件处理和上面的函数是类似的,另外像按钮或检查框等控件,在鼠标放开的时候需要发送一个点击事件(鼠标点击就是鼠标在一个控件上按下又放开,一般都是在放开时候触发一个点击事件),检查框的鼠标放开事件代码如下,可以看到通过调用SendMessage函数可以发送一个点击事件:

BOOL CCheckButton::OnControlLButtonUp(UINT nFlags, CPoint point)
{
enumButtonState buttonState = m_enButtonState;
if (!m_bIsDisable)
{
if(m_rc.PtInRect(point))
{
// 发送DUI消息
}
else
{
......
}
}
m_bMouseDown = false;

......
return false;
}


控件的使用

按照以上控件的开发方法开发了一个控件之后,如果将控件应用在DuiVision应用程序中,有两种方法,可以将控件添加到DuiVision库代码中或者仅作为自定义控件,添加到自己的应用程序中。
如果你开发的控件是一个很多人都会用到的控件,建议添加到DuiVision库中,可以提交到github项目中,如果觉得合适会合入主线版本,这样其他人也都可以使用。
如果仅是某个项目中使用的特殊控件,建议按照自定义控件的方式添加到自己的应用程序工程中。

控件添加到DuiVision库中的方法

1)首先把控件的头文件和cpp分别放在DuiVision库的include和source目录下,然后添加到DuiVision工程中;
2)在DuiVision.h中添加对控件头文件的引用;
3)在DuiSystem的LoadDuiControls函数中添加如下的控件注册代码:
REGISTER_DUICONTROL(CDuiUserControl, NULL);

完成以上几步就可以使用新的控件了。

控件添加到自己的应用程序工程中的方法

自定义的控件代码开发完成后,将代码添加到用户自己的代码工程中,然后在控件使用之前注册到DuiVision库中就可以,建议注册代码放在主程序的DuiSystem初始化之后,下面的代码演示了在主程序中注册类名为CDuiUserControl的自定义控件的方法,蓝色部分代码就是用于注册自定义控件的代码:

BOOL CDuiVision1App::InitInstance()
{
//初始化
......

// 注册用户自定义控件控件
REGISTER_DUICONTROL(CDuiUserControl, NULL);

// 创建主窗口
......
return FALSE;
}


说明:因为字数限制,部分代码省略,完整内容可以参考CSDN博客中的文章
附:
DuiVision开源代码下载地址(github):https://github.com/blueantst/DuiVision
蓝蚂蚁工作室主页:http://www.blueantstudio.net
DuiVision QQ群:325880743

使用DuiVision开发的一些界面演示:








...全文
414 点赞 收藏 5
写回复
5 条回复

还没有回复,快来抢沙发~

发动态
发帖子
界面
创建于2007-09-28

7973

社区成员

11.5w+

社区内容

VC/MFC 界面
社区公告
暂无公告