前面的三章,我們說了多態的一些技術內幕還有一些關於C++對象模型的內容,所以我就在想是要繼續深入C++的知識點呢還是就目前的內容我們來聊聊如何來設計一個應用程序,最後選擇了後者,這一章的內容我們來說說如何搭建一個GUI框架,由於GUI框架涉及到方方面面,所以我們這裡只能算是一個簡單的切入點,不涉及詳細的編碼的實現。
GUI框架很多,在windows上面C++有MFC,WTL,還有跨平臺的Qt等等,我們可以隨便找一個來作為參考,有了參考之後我們還需要對我們的框架的模塊規劃。
我們打算寫一個DirectUI框架, 所以我們需要一個窗口——CDxWindowWnd。
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 LRESULTHandleMessage(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 MStringGetExePath();
static MStringGetCurrentTime();
static MStringGetUUIDStr();
//
// 消息循環
//
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 LRESULTOnNcHitTest(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled);
virtual LRESULTOnSize(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled);
virtual LRESULTOnSysCommand(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled);
virtual LRESULTOnCreate(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled);
virtual LRESULTOnLButtonDown(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM /*lParam*/, BOOL& bHandled);
LRESULTHandleMessage(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 voidBeginDraw() = 0; // 開始繪製
virtual voidClear(const DxColor& col) = 0; // 使用特定色彩擦除背景
virtual voidEndDraw() = 0; // 結束繪製
virtual voidDrawRectangle(const RECT& rc, const DxColor& col,double size) = 0;
virtual voidDrawRoundedRectangle(const RECT& rc, const SIZE& radius, const DxColor& col, double size) = 0;
virtual voidDrawEllipse(const RECT& rc, const SIZE& radius, const DxColor& col, double size) = 0;
virtual voidDrawDashRectangle(const RECT& rc, const DxColor& col, double size) = 0;
virtual voidDrawDashRoundedRectangle(const RECT& rc, const SIZE& radius, const DxColor& col, double size) = 0;
virtual voidDrawDashEllipse(const RECT& rc, const SIZE& radius, const DxColor& col, double size) = 0;
virtual voidFillRectangle(const RECT& rc, const DxColor& col) = 0;
virtual voidFillRoundedRectangle(const RECT& rc, const SIZE& radius, const DxColor& col) = 0;
virtual voidFillEllipse(const RECT& rc, const SIZE& radius, const DxColor& col) = 0;
virtual voidFillRectangle(const RECT& rc, CDxEffects* pEffects) = 0;
virtual voidFillRoundedRectangle(const RECT& rc, const SIZE& radius, CDxEffects* pEffects) = 0;
virtual voidFillEllipse(const RECT& rc, const SIZE& radius, CDxEffects* pEffects) = 0;
virtual voidDrawBitmap(const RECT& rc, CDxEffects* pEffects) = 0;
virtual voidDrawBitmap(const RECT& rc, const MString& bitmap,int w = -1,int h = -1) = 0;
virtual voidDrawText(const MString& Text, const RECT& rc, CDxEffects* pEffects) = 0; // 只繪製文本,超出區域不管 效率更高
virtual voidDrawText(const MString& Text, const RECT& rc, const DxColor& col, const DXFontInfo& font,DXAlignment alignt) = 0; // 不使用效果直接繪製
virtual voidDrawTextWithClip(const MString& Text, const RECT& rc, CDxEffects* pEffects, const RECT& cliprc = { 0, 0, 0, 0 }) = 0; // 繪製文本,超出區域裁剪
virtual voidBeginClipRect(const RECT& rc) = 0; // 在繪製之前調用
virtual voidBeginClipRect(const std::vector
virtual voidEndClipRect() = 0;// 在繪製完成之後調用
//
// 繪製線體
// DrawLines效率更高
//
virtual voidDrawLine(const DxPointD& first, const DxPointD& second, const DxColor& col, double Size) = 0;
virtual voidDrawLines(const std::vector
virtual voidDrawDashLine(const DxPointD& first, const DxPointD& second, const DxColor& col, double Size) = 0;
virtual voidDrawDashLines(const std::vector
//
// 變換
//
virtual voidSetTransform(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 voidCreateHwnd();
virtual voidCreateHwnd(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
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,怎麼使用繼承,怎麼使用多態,怎麼使用純虛類等,這些其實是仁者見仁智者見智的問題,但在自己沒有比較好的想法的時候有個參考總歸是好的。
每天會更新論文和視頻,還有如果想學習c++知識在晚上8.30免費觀看這個直播:https://ke.qq.com/course/131973#tuin=b52b9a80
閱讀更多 IT布丁老師 的文章