利用MFC開發用戶界面往往需要需要根據要求進行界面美化,界面的美化包括很多內容,比如說界面各功能模塊空間布局,控件位置選擇,各功能模塊區域的字體、背景顏色選擇、添加位圖,標題欄、菜單欄、狀態欄等的重繪等等。總的來說,界面美化包括客戶區和非客戶區,本文主要結合本人的第一個MFC軟件界面開發項目的經驗教訓,簡要介紹MFC單文檔應用程序界面非客戶區的重繪,主要包括標題欄和菜單欄。
重繪標題欄和菜單欄可以從以下幾方面考慮:1. 自行繪制的標題欄和菜單欄覆蓋默認的標題欄和菜單欄;2.相應的控件(最大、最小、關閉)和菜單命令響應函數需要在新的標題欄和菜單欄得以實現; 3.自繪標題欄和菜單欄需要在界面上穩定顯示,避免閃爍問題。后文主要從以下幾方面介紹重繪標題欄和菜單欄功能的具體實現:
一、獲取原有標題欄和菜單欄的大小
GetSystemMetrics()函數用於獲取窗口的像素尺寸等參數信息,這里主要用到的參數包括SM_CXFRAME(X方向可變邊框厚度)、SM_CYFRAME(Y方向可變邊框厚度)、SM_CXBORDER(X方向不可變邊框厚度)、SM_CYFRAME(Y方向不可變邊框厚度)SM_CYCAPTION(窗口標題的像素高度)、SM_CXSIZE(標題欄按鈕的X方向像素尺寸)、SM_CYSIZE(標題欄按鈕的Y方向像素尺寸)、SM_CXICON(以像素計算的標題欄圖標X方向尺寸)、SM_CYICON(以像素計算的標題欄圖標Y方向尺寸)、SM_CXMENUSIZE(菜單欄按鈕的X方向尺寸)、SM_CYMENUSIZE(菜單欄按鈕的Y方向尺寸)、SM_CYMENU(以像素計算的菜單欄高度)。利用以上獲取的參數信息,結合界面窗口尺寸(用GetWindowRect()函數獲取,用ScreenToClient()函數調整)即可分別計算得到標題欄和菜單欄的矩形尺寸區域位置和大小。
二、雙緩沖繪圖,避免閃爍
雙緩沖繪圖利用內存緩沖區解決多重繪制操作帶來的閃爍問題。啟用雙緩沖繪圖時,所有的繪制操作首先在內存緩沖區完成,而非界面上的繪圖區域,所有繪圖操作完成后,將內存緩沖區中完成的圖像直接復制到界面上的相應繪圖區域。由於在屏幕上只執行的速度極快的圖形復制操作,因而解決了由復雜繪制操作造成的閃爍。
1 CDC* pDisplayMemDC = new CDC; 2 CBitmap memBitmap; // 定義兼容位圖 3 int cxClient = rtWnd.Width(); 4 int cyClient = rtWnd.Height(); 5 if(!pDisplayMemDC->m_hDC) 6 { 7 pDisplayMemDC->CreateCompatibleDC(pDC); 8 memBitmap.CreateCompatibleBitmap(pDC,cxClient,cyClient); // 定義並創建兼容位圖對象 9 pDisplayMemDC->SelectObject(&memBitmap); // 將兼容位圖對象選入兼容設備描述表 10 pDisplayMemDC->BitBlt(0,0,cxClient,cyClient,pDC,0,0,SRCCOPY); 11 12 }
以上代碼之后通過定義需要的繪圖工具對象,如字體、畫刷、畫筆等,即可在繪圖畫布上執行所需的各種繪圖操作,完成后,即可將內存緩沖區圖像復制到界面上相關聯的繪圖區域。最后,還要釋放相關的GDI對象等,避免內存溢出。
pDC->BitBlt(rtWnd.left,rtWnd.top,cxClient,cyClient,pDisplayMemDC,rtWnd.left,rtWnd.top,SRCCOPY); memBitmap.DeleteObject(); font.DeleteObject(); ReleaseDC(pDisplayMemDC); delete pDisplayMemDC;
三、定義Window窗口消息,確定重繪時機
LRESULT DefWindowProc(HWND hWnd,UINT Msg,WPARAM wParam,LPARAM lParam)是默認的窗口消息處理函數,它用來為應用程序沒有處理的任何窗口消息提供缺省的處理,從而確保每一個消息都得到處理。
1 // 自定義窗口消息處理函數 2 LRESULT CMainFrame::DefWindowProc(UINT message, WPARAM wParam, LPARAM lParam) 3 { 4 // TODO: Add your specialized code here and/or call the base class 5 LRESULT lrst=CFrameWnd::DefWindowProc(message, wParam, lParam); 6 if(!::IsWindow(m_hWnd)) 7 return lrst; 8 9 if( message == WM_NCPAINT 10 || message == WM_NCACTIVATE 11 || message == WM_INITMENU) 12 { 13 CDC* pWinDC = GetWindowDC(); 14 if(pWinDC) 15 { 16 DrawTitleBar(pWinDC); 17 DrawMenuBar(pWinDC); 18 } 19 ReleaseDC(pWinDC); 20 return TRUE; 21 } 22 else 23 return lrst; 24 }
示例代碼中定義的消息只有三個,WM_NCPAINT(重繪框架窗口非客戶區),WM_NCACTIVATE(非客戶區得到或者失去焦點時的程序操作),WM_INITMENU(A WM_INITMENU message is sent only when a menu is first accessed; only one WM_INITMENU message is generated for each access).
四、菜單欄的繪制和菜單命令選擇
菜單欄的繪制包括主菜單的繪制和各菜單項下拉彈出菜單的繪制。窗口獲取到WM_INITMENU消息時,重繪主菜單欄,當鼠標在菜單欄條目上單擊按下鼠標左鍵時,窗口獲取到菜單追蹤消息WM_MENUSELECT。
void CMainFrame::OnMenuSelect(UINT nItemID, UINT nFlags, HMENU hSysMenu) { // TODO: Add your message handler code here if (nFlags & MF_POPUP) { CDC* pWinDC = GetWindowDC(); if (pWinDC) { if (nFlags != 0xFFFF) DrawMenuBar(pWinDC,CPoint(0,0),nItemID); else DrawMenuBar(pWinDC); } ReleaseDC(pWinDC); TRACE("nItemID == %d\tn Flags == %d\r\n", nItemID, nFlags); } else CFrameWnd::OnMenuSelect(nItemID, nFlags, hSysMenu); }
五、非客戶區重繪的尺寸選擇和調整
WM_NCCALCSIZE消息在客戶區尺寸和位置發生變化時發送,用於計算和調整非客戶區尺寸的大小。
afx_msg void OnNcCalcSize( BOOL bCalcValidRects, NCCALCSIZE_PARAMS* lpncsp );
參數含義:
bCalcValidRects
Specifies whether the application should specify which part of the client area contains valid information. Windows will copy the valid information to the specified area within the new client area. If this parameter is TRUE, the application should specify which part of the client area is valid.
lpncsp
Points to a NCCALCSIZE_PARAMS data structure that contains information an application can use to calculate the new size and position of theCWnd rectangle (including client area, borders, caption, scroll bars, and so on).
NCCALCSIZE_PARAMS data structure如下所示:
typedef struct tagNCCALCSIZE_PARAMS { RECT rgrc[3]; PWINDOWPOS lppos; } NCCALCSIZE_PARAMS;
相關參數含義:
rgrc
Specifies an array of rectangles. The first contains the new coordinates of a window that has been moved or resized. The second contains the coordinates of the window before it was moved or resized. The third contains the coordinates of the client area of a window before it was moved or resized. If the window is a child window, the coordinates are relative to the client area of the parent window. If the window is a top-level window, the coordinates are relative to the screen.
lppos
Points to a WINDOWPOS structure that contains the size and position values specified in the operation that caused the window to be moved or resized.
六、非客戶區的焦點響應函數調整
在DefWindowProc()函數中定義了對WM_NCACTIVATE消息的處理,如果對其默認消息響應函數OnNcActivate(BOOL bActive) 不加更改,盡管采取了雙緩沖繪圖,在執行菜單命令時,菜單欄區域總會閃爍一次,因此,對其作以下更改:
BOOL CMainFrame::OnNcActivate(BOOL bActive) { // TODO: Add your message handler code here and/or call default return TRUE; //return CFrameWnd::OnNcActivate(bActive); }
OnNcActivate()函數返回值始終為TRUE使得Windows必須進行缺省處理。
七、各種非客戶區消息響應函數的處理
一些非客戶區消息響應函數需要加以處理,以實現所需功能,比如OnNcMouseMove()、OnNcHitTest() 、OnNcLButtonDown() 、OnNcPaint()等。
void CMainFrame::OnNcMouseMove(UINT nHitTest, CPoint point) { // TODO: Add your message handler code here and/or call default CRect rcWindow; GetWindowRect(&rcWindow); point.Offset(-rcWindow.left,-rcWindow.top); CDC* pDC = GetWindowDC(); if(pDC) { if (nHitTest== HTCLOSE) { CRect rtWnd,rtButtons; GetWindowRect(rtWnd); rtButtons = m_rtButtExit; rtButtons.OffsetRect(-rtWnd.TopLeft().x,-rtWnd.TopLeft().y); DrawExitButton(pDC,rtButtons,!m_rtButtExit.PtInRect(point)); } else if (nHitTest!=HTCLIENT) { DrawTitleBar(pDC); DrawMenuBar(pDC,point); } else { CFrameWnd::OnNcMouseMove(nHitTest,point); } } ReleaseDC(pDC); }
void CMainFrame::OnNcLButtonDown(UINT nHitTest, CPoint point) { // TODO: Add your message handler code here and/or call default if (m_rtButtExit.PtInRect(point)) SendMessage(WM_CLOSE); else if (nHitTest == HTMENU) { SendMessage(WM_SYSCOMMAND,SC_MOUSEMENU, MAKELPARAM(point.x, point.y)); } else CFrameWnd::OnNcLButtonDown(nHitTest, point); }
八、相關繪圖命令的使用
與繪圖操作相關的工具包括設備描述表對象、畫布、畫刷、畫筆、字體等,此外,繪圖函數、繪圖矩形區域描述、背景顏色、前景顏色等顏色設定、內存緩沖區圖形復制函數PatBlt()、BitBlt()等函數的使用,繪圖工具對象的選入、使用和銷毀,一般繪圖和雙緩沖繪圖的步驟、繪圖區與坐標的選擇與設定控制等。
常用的繪圖類包括CPen,CBrush、CFont、CBitmap、CDC、CRect、CPoint、CString、COLORREF等
常用的繪圖相關函數包括ScreenToClient()、ClientToScreen()、GetSystemMetrics()、CreateCompatibleDC()、CreateCompatibleBitmap()、SelectObject()、BitBlt()、PatBlt()、DrawIconEx()、SetBkMode()、SetTextColor()、CreatePointFont()、DrawText()、OffsetRect()、DeflateRect()、DeleteObject()、CreateSolidBrush()、CreatePen()、Rectangle()、Polyline()、GetWindowRect()、GetClientRect()、GetMenu()、GetMenuItemCount()、GetMenuString()、PtInRect()、FillRect()、TextOut()、Ellipse()、ReleaseDC()、InvalidateRect()、SetWindowText()、GetWindowText()等。
經驗總結
本人項目中非客戶區重繪實現的功能相對比較簡單,而且實現效果並不完美,只是初步實現所需功能。根據網上資料和個人實際編程體會,重繪非客戶區主要注意以下幾點:
(1)總結提煉重繪非客戶區所需的功能,比如背景貼圖、顏色設定、字體設定等;
(2)計算非客戶區尺寸,如標題欄和菜單欄高度、按鈕、圖標位置等,獲取相應的矩形區域;
(3)利用雙緩沖繪圖技術簡單重繪標題欄和菜單欄、圖標、按鈕等;
(4)屏蔽原有的按鈕或菜單區域消息或命令響應,讓相應的消息或命令響應函數對應新的按鈕或者菜單區域;
(5)對標題欄和菜單欄的字體和背景顏色、圖標、甚至位圖等加以適當修飾,以實現所需功能
(6)針對(1)中提煉的功能一一調試,看看程序是否滿足功能要求,能夠長時間運行,尤其檢查GDI句柄資源的釋放是否完整等。
本人是MFC編程新手,整個項目的完成過程中各種搜索,整合了網絡上的很多程序等資源,勉強實現了所要求的功能,但是對MFC的機制還不具備很深的認識,還需項目的歷練。在此衷心感謝在網絡上共享資源、編程心得的大牛們!