前面的三章,我们说了多态的一些技术内幕还有一些关于C++对象模型的内容,所以我就在想是要继续深入C++的知识点呢还是就目前的内容我们来聊聊如何来设计一个应用程序,最后选择了后者,这一章的内容我们来说说如何搭建一个GUI框架,由于GUI框架涉及到方方面面,所以我们这里只能算是一个简单的切入点,不涉及详细的编码的实现。
GUI框架很多,在windows上面C++有MFC,WTL,还有跨平台的Qt等等,我们可以随便找一个来作为参考,有了参考之后我们还需要对我们的框架的模块规划。
我们打算写一个DirectUI框架, 所以我们需要一个窗口——CDxWindowWnd。
创一个小群,供大家学习交流聊天
如果有对学C++方面有什么疑惑问题的,或者有什么想说的想聊的大家可以一起交流学习一起进步呀。
也希望大家对学C++能够持之以恒
C++爱好群,
如果你想要学好C++最好加入一个组织,这样大家学习的话就比较方便,还能够共同交流和分享资料,给你推荐一个学习的组织:快乐学习C++组织 可以点击组织二字,可以直达
CDxWindowWnd,作为我们的基本窗口类,该类我们只需要对HWND进行简单的包装。
//+--------------------------
//
// class CDxWindowWnd
// windows的基本窗口、
//
//
class CDxWindowWnd{
public:
CDxWindowWnd();
virtual ~CDxWindowWnd();
operator HWND() const;
void ShowWindow(bool bShow = true, bool bTakeFocus = true);
UINT ShowModal();
virtual void CloseHwnd(UINT nRet = IDOK);
void CenterWindow();
protected:
virtual LRESULT HandleMessage(UINT uMsg, WPARAM wParam, LPARAM lParam);
private:
HWND m_Hwnd{nullptr};
};
//+-------------------------------
窗口有了,我们需要一个消息循环,对于windows应用程序来说写个消息循环很简单:
//+--------------------------------
MSG msg = { 0 };
while (::GetMessage(&msg, NULL, 0, 0)) {
::TranslateMessage(&msg);
::DispatchMessage(&msg);
if (msg.message == WM_CLOSE && ::GetParent(msg.hwnd) == NULL)
break;
}
//+-------------------------------
虽然这几句代码可以实现我们所要的消息循环,但是我们可以将该消息循环进行封装,并且将该模块作为单例,这样一样我们可以在里面进行更多的操作:
//+-------------------------------
//
// class CDxApplication
// 负责窗口程序的消息循环
// 以即一些公有资料的管理
//
class CDxApplication{
public:
CDxApplication(HINSTANCE __hInstance = nullptr);
~CDxApplication();
static CDxApplication* InstanceWithArgs(HINSTANCE __hInstance);
static CDxApplication* Instance();
static void Destroy();
static void SetFont(const MString& fontName, unsigned fSize, bool isBold = false, bool isItalic = false);
static HINSTANCE GetHInstance();
static HFONT GetFont();
static DXFontInfo GetFontInfo();
static MString GetExePath();
static MString GetCurrentTime();
static MString GetUUIDStr();
//
// 消息循环
//
void Quit();
int Run();
protected:
static CDxApplication* __sPtr;
};
//
// 现在我们可以直接创建窗口并显示出来
//
int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE prevInstance, LPWSTR cmdLine, int cmdShow)
{
DxUI::CDxApplication* App = DxUI::CDxApplication::InstanceWithArgs(hInstance);
DxUI::CDxWindowWnd WndWindow;
WndWindow.Create(nullptr, L"TestWindow", DXUI_WNDSTYLE_FRAME, 0);
WndWindow.ShowWindow();
App->Run();
DxUI::CDxApplication::Destroy();
return 0;
}
//+--------------------------------
窗口我们创建出来了,但不符我们的DirectUI的预期,我们需要将标题栏去掉,所以我们可以在CDxWindowWnd的基础上进一步修改:
//+--------------------------------
class CDxWindowImpl : public CDxWindowWnd
{
public:
CDxWindowImpl();
~CDxWindowImpl();
virtual LRESULT OnNcHitTest(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled);
virtual LRESULT OnSize(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled);
virtual LRESULT OnSysCommand(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled);
virtual LRESULT OnCreate(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled);
virtual LRESULT OnLButtonDown(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM /*lParam*/, BOOL& bHandled);
LRESULT HandleMessage(UINT uMsg, WPARAM wParam, LPARAM lParam); // 从基类基础而来
//
// 其他
//
protected:
int mIdentifyId{ DX_InvalidID };
bool bIsVisible{ true };
bool bIsZoomable{ true };
RECT mCaptionBox;
RECT mSizeBox;
SIZE mMaxSize;
SIZE mMinSize;
SIZE mRoundRectSize;
};
//+-------------------------------
修改窗口风格我们放在OnCreate函数中进行实现:
//+------------------------------
LRESULT CDxWindowImpl::OnCreate(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
{
//+-------------------
//
// 调整窗口样式
//
//+--------------------
LONG styleValue = ::GetWindowLong(*this, GWL_STYLE);
styleValue &= ~WS_CAPTION;
::SetWindowLong(*this, GWL_STYLE, styleValue | WS_CLIPSIBLINGS | WS_CLIPCHILDREN);
return 0;
}
//+------------------------------
当然,如果我们就这样把标题栏去掉之后,窗口就没法拉动,也没法关闭,就一直停在桌面上,一动不动,所以为了解决这个问题,我们必须换种方式把标题栏给重新绘制出来,这就是 OnNcHitTest 的功劳了。
//+-----------------------------
LRESULT CDxWindowImpl::OnNcHitTest(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
{
POINT pt;
pt.x = GET_X_LPARAM(lParam);
pt.y = GET_Y_LPARAM(lParam);
::ScreenToClient(*this, &pt);
RECT rcClient;
::GetClientRect(*this, &rcClient);
if (!::IsZoomed(*this) && bIsZoomable)
{
RECT rcSizeBox = mSizeBox;
if (pt.y < rcClient.top + rcSizeBox.top)
{
if (pt.x < rcClient.left + rcSizeBox.left) return HTTOPLEFT;
if (pt.x > rcClient.right - rcSizeBox.right) return HTTOPRIGHT;
return HTTOP;
}
else if (pt.y > rcClient.bottom - rcSizeBox.bottom)
{
if (pt.x < rcClient.left + rcSizeBox.left) return HTBOTTOMLEFT;
if (pt.x > rcClient.right - rcSizeBox.right) return HTBOTTOMRIGHT;
return HTBOTTOM;
}
if (pt.x < rcClient.left + rcSizeBox.left) return HTLEFT;
if (pt.x > rcClient.right - rcSizeBox.right) return HTRIGHT;
}
RECT rcCaption = mCaptionBox;
if (-1 == rcCaption.bottom)
{
rcCaption.bottom = rcClient.bottom;
}
if (pt.x >= rcClient.left + rcCaption.left && pt.x < rcClient.right - rcCaption.right
&& pt.y >= rcCaption.top && pt.y < rcCaption.bottom)
{
return HTCAPTION;
}
return HTCLIENT;
}
//+-------------------------------------
标题栏的关键在于mCaptionBox的bottom的值,所以我们可以设置mCaptionBox来修改标题栏的高度。
我们现在虽然得到了我们想要的无标题栏窗口,但是这只是一张白板,所以我们还需要对齐进行绘制,我们称该层为绘制资源管理层:
//+-------------------------------------
class CDxRendImpl : public CDxWindowImpl
{
public:
CDxRendImpl();
~CDxRendImpl();
virtual bool OnInitResource2D();
virtual bool OnInitResource3D();
virtual void UnInitResource();
virtual void OnRender();
virtual void OnRender2D();
virtual void OnRender3D();
virtual void SaveToFile(const MString& fileName);
virtual void OnRendWindow(IPainterInterface* painter);
};
//+--------------------------------------
我们想要绘制那么我们就需要一个绘制模块,绘制的时候我们还需要效果,所以我们还需要两个模块:
//+-------------------------------------
//
// 效果接口
//
class CDxEffects
{
///
/// 多种效果
///
};
//
// 二维平面变换矩阵
//
struct TransformMatrix{
FLOAT _11;
FLOAT _12;
FLOAT _21;
FLOAT _22;
FLOAT _31;
FLOAT _32;
};
//
// 绘图接口
//
class IPainterInterface{
public:
//+-------------------------------------------
//
// 为绘制方便,下面的接口都得实现
// 当然如果实际没有用处的可以简单的实现即可
//
//+-------------------------------------------
virtual ~IPainterInterface(){};
virtual void BeginDraw() = 0; // 开始绘制
virtual void Clear(const DxColor& col) = 0; // 使用特定色彩擦除背景
virtual void EndDraw() = 0; // 结束绘制
virtual void DrawRectangle(const RECT& rc, const DxColor& col,double size) = 0;
virtual void DrawRoundedRectangle(const RECT& rc, const SIZE& radius, const DxColor& col, double size) = 0;
virtual void DrawEllipse(const RECT& rc, const SIZE& radius, const DxColor& col, double size) = 0;
virtual void DrawDashRectangle(const RECT& rc, const DxColor& col, double size) = 0;
virtual void DrawDashRoundedRectangle(const RECT& rc, const SIZE& radius, const DxColor& col, double size) = 0;
virtual void DrawDashEllipse(const RECT& rc, const SIZE& radius, const DxColor& col, double size) = 0;
virtual void FillRectangle(const RECT& rc, const DxColor& col) = 0;
virtual void FillRoundedRectangle(const RECT& rc, const SIZE& radius, const DxColor& col) = 0;
virtual void FillEllipse(const RECT& rc, const SIZE& radius, const DxColor& col) = 0;
virtual void FillRectangle(const RECT& rc, CDxEffects* pEffects) = 0;
virtual void FillRoundedRectangle(const RECT& rc, const SIZE& radius, CDxEffects* pEffects) = 0;
virtual void FillEllipse(const RECT& rc, const SIZE& radius, CDxEffects* pEffects) = 0;
virtual void DrawBitmap(const RECT& rc, CDxEffects* pEffects) = 0;
virtual void DrawBitmap(const RECT& rc, const MString& bitmap,int w = -1,int h = -1) = 0;
virtual void DrawText(const MString& Text, const RECT& rc, CDxEffects* pEffects) = 0; // 只绘制文本,超出区域不管 效率更高
virtual void DrawText(const MString& Text, const RECT& rc, const DxColor& col, const DXFontInfo& font,DXAlignment alignt) = 0; // 不使用效果直接绘制
virtual void DrawTextWithClip(const MString& Text, const RECT& rc, CDxEffects* pEffects, const RECT& cliprc = { 0, 0, 0, 0 }) = 0; // 绘制文本,超出区域裁剪
virtual void BeginClipRect(const RECT& rc) = 0; // 在绘制之前调用
virtual void BeginClipRect(const std::vector& points) = 0; // 在绘制之前调用
virtual void EndClipRect() = 0; // 在绘制完成之后调用
//
// 绘制线体
// DrawLines效率更高
//
virtual void DrawLine(const DxPointD& first, const DxPointD& second, const DxColor& col, double Size) = 0;
virtual void DrawLines(const std::vector& points, const DxColor& col, double Size) = 0;
virtual void DrawDashLine(const DxPointD& first, const DxPointD& second, const DxColor& col, double Size) = 0;
virtual void DrawDashLines(const std::vector& points, const DxColor& col, double Size) = 0;
//
// 变换
//
virtual void SetTransform(const TransformMatrix& mat) = 0;
};
//+---------------------------
IPainterInterface 是一个纯虚类,换句话说就是接口类,该类没有对他的子类提供任何便利,反而要求子类必须实现自己定义的所有纯虚接口,而所谓纯虚接口就是虚函数等于0的函数,而只要含有纯虚函数的类就是纯虚类,也就是所谓的接口类,之所以我们这里要将绘制操作定义为纯虚类主要是考虑到以后可能会使用不同的图像引擎来绘制图形,比如我们这里可以使用D2D,也可以使用OpenGL,还可以使用GDI等等,那么我们为什么能够同时展示二维和三维图形,所以我们选择D2D:
//+---------------------------
class CDxPainter : public IPainterInterface
{
public:
typedef ID2D1RenderTarget* LPID2D1HwndRenderTarget;
public:
CDxPainter(ID2D1RenderTarget* render);
~CDxPainter();
ID2D1RenderTarget* getRenderTarget() const;
ID2D1RenderTarget* operator->() const;
operator LPID2D1HwndRenderTarget() const;
operator bool() const;
//
// 下面是 IPainterInterface 继承而来的所有接口
//
private:
ID2D1RenderTarget* pRenderTarget;
CDxCharaterFormat* pTextRender{ nullptr };
ID2D1Layer* p_ClipLayout{ nullptr };
ID2D1Geometry* p_ClipGeometry{ nullptr };
};
//+------------------------------
到现在我们已经拥有了窗口,效果,绘图三个模块,所以我们只需要将三个模块组合起来就可以进行图形绘制,这个操作我们放在CDxRendImpl::OnRender()中,不过CDxRendImpl::OnRender()中我们默认绘制2D平面,所以真正的操作我们放在CDxRendImpl::OnRender2D()中:
//+------------------------------
void CDxRendImpl::OnRender(){
OnRender2D();
}
void CDxRendImpl::OnRender2D(){
CDxPainter painter(pRendTarget);
painter.BeginDraw();
this->OnRendWindow(&painter);
painter.EndDraw();
}
void CDxRendImpl::OnRendWindow(IPainterInterface* painter){
;
}
//+-------------------------------
事实上我们并不对该层进行渲染,所以该层的OnRendWindow函数被实现为空,因为我们需要的是做一个DirectUI框架,而我们现在还是基于窗口HWND的,所以我们还差我们的DirectUI窗口,当然DirectUI至少需要一个持有HWND,所以我们必须继承至CDxRendImpl.
//+-------------------------------
//
// DirectUI 窗口类
//
class CDxWidget : public CDxRendImpl
{
public:
CDxWidget();
~CDxWidget();
//+---------------------------
//
// 创建Hwnd
//
//+---------------------------
virtual void CreateHwnd();
virtual void CreateHwnd(HWND parent);
//
// 其他
//
};
//+---------------------------------
我们可以通过CDxWidget::CreateHwnd()决定是否需要创建HWND,我们只对主窗口创建HWND对于子窗口不创建,在渲染的时候我们先渲染当前窗口,再对子窗口进行渲染。
//+--------------------------------
//
// 绘制窗口
//
void CDxWidget::OnRendWindow(IPainterInterface* painter){
if (bIsVisible == false){
return;
}
mEffects.SetCurrentStatus(GetWindowStatus());
//+---------------
//
// 渲染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);
if (bIsNeedBorder && mBorderWidth > 0){
painter->DrawRoundedRectangle(mRendArea, mRoundRectSize, mBorderColor, mBorderWidth);
}
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 (bIsNeedBorder){
RECT rc = mRendArea;
rc.left += 1;
rc.right -= 1;
rc.top += 1;
rc.bottom -= 1;
if (mEffects.GetEffectType() == CDxEffects::Dx_ImageType){
painter->DrawRectangle(rc, mBorderColor, 1);
}
else{
DXShape shape = GetWindowShape();
switch (shape)
{
case DxUI::Dx_Rectangle:
painter->DrawRectangle(rc, mBorderColor, 1);
break;
case DxUI::Dx_RoundedRectangle:
painter->DrawRoundedRectangle(rc, mRoundRectSize, mBorderColor, 1);
break;
case DxUI::Dx_Ellipse:
painter->DrawEllipse(rc, mRoundRectSize, mBorderColor, 1);
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;
}
}
}
//+----------------------------------
显然对CDxRendImpl::OnRender2D()进行完善:
//+---------------------------------
void CDxRendImpl::OnRender2D(){
CDxWidget* window = dynamic_cast(this);
if (window->IsNeedRender() == false)
return;
if (pRendTarget && window){
pRendTarget->BeginDraw();
pRendTarget->Clear(ToD2DColor(window->GetBackGroundColor()));
window->OnRendWindow(pPainter);
if(window->GetWindowSelfDesc() == Dx_PopWindow)
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_Layout){
if (window->GetParent()){
window->GetParent()->OnRender2D();
return;
}
}
else if (window && window->IsVisible() ){
if (window->GetOpacity() < 0.99999999){
if (window->GetParent()){
window->GetParent()->OnRender2D();
return;
}
}
DxColor col = window->GetEraseColor();
CDxWidget* parent = window->GetParent();
if (col.rgb.a == 0 || (parent && parent->HasFloatWindow())){
if (parent){
parent->OnRender2D();
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);
}
}
}
}
}
//+---------------------------------
我们现在我们可以通过子类化CDxWidget实现各种控件,实现不同的效果,最终我们可以很简单的编写出各种样式的界面。
这一章的重点并非是在这个GUI框架的实现上,而是主要让我们对class,继承和多态的使用有进一步的理解,以及对纯虚类的引入,当然至于对该框架感兴趣的同学,我们会在后续的内容里面一点点的引入,毕竟里面有很多模板的东西,现在一下子拿出来很多东西可能会比较吃力,所以这一章的内容还是在对C++程序设计上的一些理解,怎么组合class,怎么使用继承,怎么使用多态,怎么使用纯虚类等,这些其实是仁者见仁智者见智的问题,但在自己没有比较好的想法的时候有个参考总归是好的。