上一章我们初步介绍了MDXUI如何创建出一个窗口,那么可能有些朋友比较好奇,这个GUI框架到底能够做什么,又能够做到什么程度,那么我们来看看MDXUI能够做些什么,也让大家知道学习该框架可以干嘛,下面展示的是基于该框架开发的一套测试系统:

MDXUI最佳实现(2)_MDXUI


这是软件的开始界面,鼠标放在上面的按钮上可以实现相应的展示图片的切换,同时按钮有相应状态的颜色改变,下面的一排六边形按钮对应了六个独立的模块。点击菜单栏的文件菜单进入如下界面,该界面由一个列表导航和一个StackedWidget模块组成,点击列表导航上面的列表项,StackedWidget将会展示相应的模块界面。

MDXUI最佳实现(2)_MDXUI_02

MDXUI最佳实现(2)_MDXUI_03

MDXUI最佳实现(2)_MDXUI_04

MDXUI最佳实现(2)_MDXUI_05

MDXUI最佳实现(2)_MDXUI_06

MDXUI最佳实现(2)_MDXUI_07

MDXUI最佳实现(2)_MDXUI_08


好吧,这里就仅仅展示一部分,还有为同事基于MDXUI编写一个类Excel的组件,可以实现一些简单的Excel的功能,方便在程序中嵌入处理数据。


        那么MDXUI只能到这种程度了吗?当然不是,因为DirectUI整个界面都是绘制出来的,所以只要有UI那么就可以做出任何想要的效果界面,说了这些只是让大家更好的理解,MDXUI可以做到任何想要的效果,不管多大的项目都能够驾驭,唯一的要求就是需要配置一张支持DX11的显卡。


MDXUI提供有哪些默认的组件呢?一般常用的组件都有默认实现:


//+-----------------------------


CDxFlatButton;      // 按钮,没有PushButton的凸起效果

CDxPushButton  ;  //常用的按钮

CDxStatusButton;  // 状态按钮,选中和非选中呈现出不同的效果

CDxUserButton;    // 可以通过CDxEffects自由设置按钮的外观

CDxHexagonButton; // 六边形按钮 当长宽比例满足 h = w / 2 * std::sqrt(3) 时为正六边形

CDxCheckBox;      // 常用的复选框

CDxRadioButton;  // 常用的单选框

CDxLabel;             // 常用的标签

CDxEdit;             // 单行文本框

CDxLinkEdit ;       // 具有超链接效果的文本框

CDxEditWidget;    // 多行文本框

CDxTextBlock;      // 可以任意修改背景的文本框

CDxListWindow;   // 基于数据模型的列表框

CDxListWidget;    // 基于窗口的列表框

CDxTreeWindow; // 树形结构窗口

CDxMultTreeWindow; // 支持多列的树形结构窗口

CDxMenu;           // 支持右键弹出的pop菜单,也可以用于常规菜单

CDxPictrue;         // 图形窗口,快速展示图形使用

CDxTableWindow; // 基于数据模型的表格窗口

CDxTableWidget;  // 基于窗口的表格窗口

CDxSpinBox;         // 数据微调窗口

CDxComboBox;    // 下拉窗口

CDxSpliterWindow; // 分割窗口,可以容纳两个窗口,可以是上下也可以是左右,可以通过鼠标改变两个窗口的大小比例

CDxHSpliter;           // 绘制一条水平线

CDxVSpliter;           // 绘制一条垂直线

CDxTopWindow;    // 顶层弹出窗口

CDxPopwindow;    // 弹出窗口

CDxFloatWindow;  // 浮动窗口

CDxGroupBox;     // GroupBox

CDxAnimationWindow; // 动画窗口

CDxColorPalette;  // 调色板

CDxAction ;          // 菜单项

CDx3DWidget ;    // 该类会对3D资源的初始化,需要绘制3D模型继承该类即可

CDxStackedWidget ; // 同一时间只显示一个窗口

CDxTabWidget;        // 效果同 CDxStackedWidget  但是多一个Tab栏,可以通过Tab栏对窗口进行切换

CDxToolTipWindow; // 提示窗口

CDxDialog;                 // 对话框

CDxGridLayout;          // 窗口布局管理

CDxHorizonLayout;    // 用于窗口的水平布局

CDxVerticLayout;       // 用于窗口的垂直布局

……


//+-------------------------------


对于一个应用程序,我们只需要一个CDxWigdet对象即可,因为我们所有的窗口都是绘制出来的,所以整个应用程序只有一个HWND实例存在,CDxWidget会创建HWND,所以为了只创建一个HWND,我们提供一个类——CDxWindow:


//+------------------------------


//

// class CDxWindow 

// 不创建HWND

//

class DXUI_API CDxWindow : public CDxWidget

{

DECLARE_CLASS(CDxWindow)

public:

CDxWindow();

~CDxWindow();

void CreateHwnd(){}

void CreateHwnd(HWND parent){}

};


//+------------------------------


CDxWindow只是将CDxWidget的CreateHwnd函数给重载了,而且是什么都不做,所以这样创建出来的对象就不会再创建HWND。那么没有HWND的窗口是如何绘制的呢?首先我们窗口可以没有HWND,但不能没有父窗口,所以带有HWND的窗口注定会是整个程序中所有窗口的祖窗口,所以我们可以通过这个祖窗口的HWND对子孙窗口进行渲染:



//+-----------------------------


void CDxRendImpl::OnRender2D(){

CDxWidget* window = dynamic_cast<CDxWidget*>(this);

if (window->IsNeedRender() == false)

return;


if (pRendTarget && window){

pRendTarget->BeginDraw();


pRendTarget->Clear(ToD2DColor(window->GetBackGroundColor()));

window->OnRendWindow(pPainter);

if (window->GetWindowSelfDesc() == Dx_PopWindow || window->GetWindowSelfDesc() == Dx_DialogWindow)

pPainter->DrawRoundedRectangle(window->GetFrameRect(), window->GetRoundRectSize(), RgbI(128, 128, 128), 2);


HRESULT hr = pRendTarget->EndDraw();

if (FAILED(hr)){

DxTRACE(L"渲染出错[%1]\n", hr);

}

}

else{

if (window->GetWindowSelfDesc() == Dx_DialogWindow || window->GetWindowSelfDesc() == Dx_MenuItem || window->GetWindowSelfDesc() == Dx_PopWindow){

return;

}

if (window->GetWindowSelfDesc() == Dx_Layout){

if (window->GetParent()){

window->GetParent()->OnRender();

return;

}

}

else if (window && window->IsVisible() ){

if (window->GetOpacity() < 0.99999999){

if (window->GetParent()){

window->GetParent()->OnRender();

return;

}

}

DxColor col = window->GetEraseColor();

CDxWidget* parent = window->GetParent();

if (col.rgb.a == 0 || (parent && parent->HasFloatWindow())){

if (parent){

parent->OnRender();

return;

}

}

auto render = GetRenderTarget();

if (render){

CDxPainter* painter = nullptr;

if (g_PainterMap.count(render)){

painter = g_PainterMap.at(render);

}

else{

painter = new CDxPainter(render);

g_PainterMap[render] = painter;

}

render->BeginDraw();


ID2D1Layer* p_ClipLayout{ nullptr };

ID2D1Geometry* p_ClipGeometry{ nullptr };

safe_release(p_ClipGeometry);

safe_release(p_ClipLayout);

render->CreateLayer(&p_ClipLayout);

RECT rc = window->GetInvalidateRect();

p_ClipGeometry = CDxResource::CreateRectGeometry(rc);

if (p_ClipLayout == nullptr || p_ClipGeometry == nullptr)

return;



render->PushLayer(D2D1::LayerParameters(

D2D1::InfiniteRect(),

p_ClipGeometry,

D2D1_ANTIALIAS_MODE_PER_PRIMITIVE,

D2D1::IdentityMatrix(),

1.0f,

NULL,

D2D1_LAYER_OPTIONS_NONE

), p_ClipLayout);


render->Clear(ToD2DColor(col));

window->OnRendWindow(painter);

render->PopLayer();

safe_release(p_ClipGeometry);

safe_release(p_ClipLayout);


HRESULT hr = render->EndDraw();

if (FAILED(hr)){

DxTRACE(L"渲染出错[%1]\n", hr);

}

}

}

}

}



//+----------------------------------


在渲染层通过OnRender2D()获取绘图引擎接口IPainterInterface,然根据窗口的区域创建Layout,再对该窗口进行单独渲染,渲染过程在OnRendWindow(IPainterInterface* painter)中:


//+----------------------------------


void CDxWidget::OnRendWindow(IPainterInterface* painter){

if (bIsVisible == false){

return;

}


mEffects.SetCurrentStatus(GetWindowStatus());

UpdateArea();




if (bIsNeedBorder){

RECT rc = mRendArea;

if (mEffects.GetEffectType() == CDxEffects::Dx_ImageType){

painter->FillRectangle(mFrameArea, mBorderColor);

painter->FillRectangle(rc, RgbI(255, 255, 255));

}

else{

//

// 图像颜色一起绘制

//

DXShape shape = GetWindowShape();

switch (shape)

{

case DxUI::Dx_Rectangle:

painter->FillRectangle(mFrameArea, mBorderColor);

painter->FillRectangle(rc, RgbI(255,255,255));

break;


case DxUI::Dx_RoundedRectangle:

painter->FillRoundedRectangle(mFrameArea, mRoundRectSize, mBorderColor);

painter->FillRoundedRectangle(rc, mRoundRectSize, RgbI(255, 255, 255));

break;


case DxUI::Dx_Ellipse:

painter->FillEllipse(mFrameArea, mRoundRectSize, mBorderColor);

painter->FillEllipse(rc, mRoundRectSize, RgbI(255, 255, 255));

break;


default:

break;

}

}

}


//+---------------

//

// 渲染Title

//

//+--------------

if (mHwnd && !pCaptionLabel&& !::GetParent(mHwnd)){

pCaptionLabel = new CDxCaption;

pCaptionLabel->SetParent(this);

RECT rc = mFrameArea;

rc.bottom =  mCaptionBox.bottom;

pCaptionLabel->SetGeomety(rc);

mRendArea = mFrameArea;

mRendArea.X(mFrameArea.X() + mSizeBox.left);

mRendArea.Y(mRendArea.Y() + mCaptionBox.bottom);

mRendArea.Width(mFrameArea.Width() - mSizeBox.left - mSizeBox.right);

mRendArea.Height(mFrameArea.Height() - mCaptionBox.bottom - mSizeBox.bottom);

if (!mIcon.empty()){

pCaptionLabel->GetIconEffects()->SetBitmaps(Dx_Normal, mIcon);

}

UpdateChildWindowPos();

}

if (pCaptionLabel){

RECT rc = mFrameArea;

rc.bottom = rc.top + pCaptionLabel->GetFrameRect().Height();

pCaptionLabel->SetGeomety(rc);

pCaptionLabel->SetText(mTitle);

pCaptionLabel->OnRendWindow(painter);

}

if (mEffects.GetEffectType() == CDxEffects::Dx_ImageType){

painter->DrawBitmap(mImageRendArea, &mEffects);

}

else if (mEffects.GetEffectType() == CDxEffects::Dx_ColorType){

DXShape shape = GetWindowShape();

switch (shape)

{

case DxUI::Dx_Rectangle:

painter->FillRectangle(mRendArea, &mEffects);

break;


case DxUI::Dx_RoundedRectangle:

painter->FillRoundedRectangle(mRendArea, mRoundRectSize, &mEffects);

break;


case DxUI::Dx_Ellipse:

painter->FillEllipse(mRendArea, mRoundRectSize, &mEffects);

break;


default:

break;

}

}

else{

DXShape shape = GetWindowShape();

switch (shape)

{

case DxUI::Dx_Rectangle:

painter->FillRectangle(mRendArea, &mEffects);

painter->DrawBitmap(mImageRendArea, &mEffects);

break;


case DxUI::Dx_RoundedRectangle:

painter->FillRoundedRectangle(mRendArea, mRoundRectSize, &mEffects);

painter->DrawBitmap(mImageRendArea, &mEffects);

break;


case DxUI::Dx_Ellipse:

painter->FillEllipse(mRendArea, mRoundRectSize, &mEffects);

painter->DrawBitmap(mImageRendArea, &mEffects);

break;


default:

break;

}

}


if (!mText.empty() ){

painter->DrawText(mText, mTextRendArea, &mEffects);

}


if (pLayout){

pLayout->OnRendWindow(painter);

}



//+-----------------------------

//

// 渲染子窗口

//

//+-----------------------------

if (!mChildList.empty()){

UpdateChildWindowPos();

for (auto& window : mChildList){

CDxWidget*& windowref = window.ref();

if (windowref->GetHwnd() == nullptr){

windowref->OnRendWindow(painter);

}

}

}


//

// 绘制表面效果

//

if (bIsNeedSurfaceEffects){

DXShape shape = GetWindowShape();

if (mSurfaceEffects.GetEffectType() != CDxEffects::Dx_ColorType){

painter->DrawBitmap(mImageRendArea, &mSurfaceEffects);

}

else{

switch (shape)

{

case DxUI::Dx_Rectangle:

painter->FillRectangle(mRendArea, &mSurfaceEffects);

break;


case DxUI::Dx_RoundedRectangle:

painter->FillRoundedRectangle(mRendArea, mRoundRectSize, &mSurfaceEffects);

break;


case DxUI::Dx_Ellipse:

painter->FillEllipse(mRendArea, mRoundRectSize, &mSurfaceEffects);

break;


default:

break;

}

}

}


//

// 绘制禁用效果

//

if (!bIsEnabel){

DXShape shape = GetWindowShape();

mEffects.SetCurrentStatus(Dx_Disable);

mEffects.SetDisabelColor(mDisabelColor);

switch (shape)

{

case DxUI::Dx_Rectangle:

painter->FillRectangle(mRendArea, &mEffects);

break;


case DxUI::Dx_RoundedRectangle:

painter->FillRoundedRectangle(mRendArea, mRoundRectSize, &mEffects);

break;


case DxUI::Dx_Ellipse:

painter->FillEllipse(mRendArea, mRoundRectSize, &mEffects);

break;


default:

break;

}

}

}


//+----------------------------------


在窗口的渲染过程中,我们先对自身进行渲染绘制,然后检查是否有layout存在,如果有layout存在对layout进行绘制,然后检查是否存在直接子窗口,如果存在对子窗口进行绘制。


现在我们对创建的渲染过程已经知道,那么我们就可以在我们的白板窗口上添加我们想要的任何子窗口啦,通过上面的细节,我们了解到想要添加子窗口有两种方式,一种是添加layout,一种是直接添加子窗口,也可以同时使用,同时使用的时候先绘制layout再绘制子窗口,这是什么意思呢?如果两个窗口有重叠,那么子窗口将覆盖Layout的窗口。


我们先来使用直接添加子窗口的方式添加子窗口。


//+------------------------------------


CDxListWindowEx m_List;

window.AddChild(&m_List);

m_List.SetGeometyDim({ 0.0, 100 }, { 0.0, 100 }, { 1.0, -200 }, { 1.0, -200 });

m_List.AddItems({ "1", "2", "3", "4", "5", "6", "7" });

m_List.SetText("ListWindow");


//+-------------------------------------

MDXUI最佳实现(2)_MDXUI_09

想要将一个窗口添加为直接子窗口,通过AddChild即可,那么想要让该子窗口显示在何处那么就需要调用SetGeomety:



//+------------------------------------


void GetGeometyDim(DxDimI& x, DxDimI& y, DxDimI& w, DxDimI& h);

void SetGeomety(const DxRectI& rc)

//+------------------------------------


这两个方法可以控制窗口的位置,其中的区别GetGeometyDim设定的位置会随着父窗口的大小变化而变化,而SetGeomety设定的位置是一个固定位置,不会跟随父窗口的变化而变化,其中的参数:



//+--------------------------------


typedef DxDim<int>   DxDimI;

template<class T>

class DxDim{

public:

DxDim(const float& ratio = 0.f, const T& offset = T());

……

};


typedef DxRect<int> DxRectI;


//+----------------------------


DxRectI 表示的是一个矩形,DxDimI 表示的是一个位置,而这个位置由两个因素决定,比例和偏移,比例是指在父窗口的尺寸的比例,偏移是指在比例位置处的偏移大小,比如我们上面设定的x = {0.0,100}就是指在父窗口的整个宽的0.0倍位置处向由偏移100像素的位置,而w = {1.0,-200}表示的宽是整个父窗口的宽的1.0倍减去200,所以这样一来,该窗口永远在父窗口的正中间,并且离四边都是100像素,如果我们修改为: m_List.SetGeomety({ 100,100,200,200}); 我们就会看到不同的效果,这时我们无论怎么改变父窗口他的大小和位置都不会发生改变。


MDXUI最佳实现(2)_MDXUI_10

MDXUI最佳实现(2)_MDXUI_11

如果我们觉得直接添加子窗口的方式太过繁琐,因为我们需要去计算这些窗口应该落在什么位置,那么我们可以使用更常用的方式——Layout,使用Layout添加窗口非常的简单和方便,但是它同样简单粗暴,很多时候我们需要实现完美定位的时候还是需要使用直接子窗口模式进行添加。下面我们使用Layout来达到同样的效果:


//+------------------------------


CDxGridLayout lg;

window.SetLayout(&lg);

lg.SetMargin(100);

lg.AddWidget(&m_List, 0, 0, 1, 1);


//+-------------------------------

MDXUI最佳实现(2)_MDXUI_09


现在我们来尝试添加两个窗口,一个是列表框,一个是文本框:


//+------------------------------


CDxTextBlock m_TextBlock;

lg.AddWidget(&m_TextBlock, 0, 1, 1, 1);

m_TextBlock.GetEffects()->SetColor(Dx_Normal, RgbI(220, 220, 220));

m_TextBlock.SetWindowShape(Dx_RoundedRectangle);

m_TextBlock.SetRoundRadius({ 10, 10 });


//+-------------------------------

MDXUI最佳实现(2)_MDXUI_13

引入事件:当我们点击列表框的列表项时让文本框显示出所点击的列表项的索引以及文本信息



//+------------------------------


typedef std::function<void(int, int, CDxWidget*)> FunType;

FunType fun = [&](int cur, int pre, CDxWidget* sender){

if (cur == -1)

return;


auto item = m_List.GetItemByIndex(cur);

if (item){

MString Text = item->Text();

MString str = cur;

str << L"\t" << Text;

m_TextBlock.SetText(str);

m_TextBlock.OnPainterEvent();

}


};


TL::MTLDataBind::Connect(MSIGNAL(CDxListWindowEx, SelectedChanged, &m_List), fun);


//+------------------------------

MDXUI最佳实现(2)_MDXUI_14

对于列表框,当选中列表项改变的时候会触发SelectedChanged事件,所以,我们只需要接收该事件就可以,如果大家还记得前面我们说过的信号槽的话,那么对这里的理解相当的简单,对于事件的接受函数我们是自由函数,可以是成员函数,还可以是lambda函数,这里为了方便我们使用了lambda函数来接受该事件。


在列表框的SelectedChanged事件中,其中的三个参数分辨代表的意义是第一个参数是当前选中的索引,第二个参数是上一个选中的索引,第三个参数就是发送事件的对象,就是列表框本身。我们可以通过sender来区分消息到底来源哪个实例。

转自公众号: C/C++的编程教室