MFC下按鈕自繪的實現(一)
在MFC下編程,很多時候對於標准的按鈕控件不是很滿意,想要弄的美觀點。這就需要按鈕自繪。這里簡單記錄一下方法,以免過個十天半個月的,自己又忘的一干二凈了。
首先給工程添加一個MFC類,基類為CButton。要想讓按鈕具備自繪功能,就要為按鈕添加BS_OWNERDRAW屬性。為類CButton重載PreSubclassWindow虛函數。在該函數中添加如下一行代碼:
ModifyStyle(0, BS_OWNERDRAW);
當按鈕控件具有了自繪功能之后,每次控件狀態改變都會觸發DrawItem函數,在該函數中來繪制按鈕的形態外觀,所以第二步就要重載DrawItem虛函數。在這個函數中就可以自由發揮了,比如繪制外邊框,底色,按鈕標題,內邊框等等。
一般都會為按鈕定義幾種不同狀態時的外觀,比如光標滑過時的狀態,按鈕按下時的狀態,按鈕禁用時的狀態,以及按鈕的正常狀態等等。這就要為新的按鈕添加幾種重要的消息響應。比如WM_MOUSELEAVE消息,WM_MOUSEHOVER消息和WM_MOUSEMOVE消息等等,值得一提的是前兩個消息的響應函數需要自己手動添加,微軟提供了一個TrackMouseEvent函數在光標離開一個窗口時投遞WM_MOUSELEAVE消息,光標滑過窗口時投遞WM_MOUSEHOVER消息。一般來說可以在WM_MOUSEMOVE消息響應函數中調用TrackMouseEvent函數來投遞WM_MOUSELEAVE消息和WM_MOUSEHOVER消息。然后在WM_MOUSELEAVE消息的響應函數中標記“光標已經離開按鈕”,然后調用InvalidateRect函數讓按鈕重繪。在WM_MOUSEHOVER消息的響應函數中標記“光標正在按鈕上方”,並調用InvalidateRect函數讓按鈕重繪。
典型代碼:
if (!m_bTracking) // 判斷此時按鈕是否已經按下
{
TRACKMOUSEEVENT tme;
tme.cbSize = sizeof(tme);
tme.hwndTrack = m_hWnd;
tme.dwFlags = TME_LEAVE | TME_HOVER;
tme.dwHoverTime = 1;
m_bTracking = _TrackMouseEvent(&tme);
}
這幾天都是這么困,不知道是怎么搞的,呆會接着寫。
MFC下按鈕自繪的實現(二)
上篇文章中提到使用WM_MOUSELEAVE消息,但是在Windows CE操作系統下,手動添加WM_MOUSELEAVE消息響應函數之后,編譯會發現WM_MOUSELEAVE沒有定義。之前在Windows XP操作系統下執行的程序沒有這個提示。找到原來的程序,發現WM_MOUSELEAVE的定義在\microsoft visual studio 8\vc\platformsdk\include\winuser.h文件中。
#if(WINVER >= 0x0500)
#define WM_NCMOUSEHOVER 0x02A0
#define WM_NCMOUSELEAVE 0x02A2
#endif /* WINVER >= 0x0500 */
據此手動添加如下代碼:
#ifndef WM_MOUSELEAVE
#define WM_MOUSELEAVE 0x02A3
#endif
對於WM_MOUSEHOVER消息也是一樣:
#ifndef WM_MOUSEHOVER
#define WM_MOUSEHOVER 0x02A1
#endif
重新編譯即可。
另外上篇文章中說道TrackMouseEvent函數用來投遞WM_MOUSELEAVE和WM_MOUSEHOVER消息。貌似這個函數在Windows CE操作系統下也找不到。找不到就不用它了,自己直接調用PostMessage投遞出去算了。
比如:
::PostMessage(m_hWnd, WM_MOUSELEAVE, 0, 0);
那么當光標滑過按鈕時,會觸發WM_MOUSEMOVE消息,在這個函數中如何判斷光標是在按鈕上停留着還是離開了,從而是發送WM_MOUSELEAVE消息還是WM_MOUSEHOVER消息呢?這個不難吧,至少PtInRect函數可以搞定。
自己測試了一下,完全可以。
MFC下按鈕自繪的實現(三)
按鈕的繪制主要在DrawItem函數中來完成,下面來簡單的繪制一下。
第一步先繪制按鈕的外邊框。定義了一個成員變量:
CPen m_OutBorderPen;
這是一個用來繪制按鈕外邊框的畫筆,在類的構造函數中創建它,在類的析構函數中銷毀之。然后在DrawItem函數中開始繪制按鈕的外邊框:
CRect rect = lpDrawItemStruct->rcItem;
CDC *pDC = CDC::FromHandle(lpDrawItemStruct->hDC);
int nSavedDC = pDC->SaveDC();
// 繪制按鈕的外邊框
POINT pt;
pt.x = 5;
pt.y = 5;
CPen *pOldPen = pDC->SelectObject(&m_OutBorderPen);
pDC->RoundRect(&rect, pt);
pDC->SelectObject(pOldPen);
編譯之后執行一下程序,看下效果:
按鈕的輪廓出來了。
第二步繪制按鈕的底色。
// 繪制按鈕的底色
rect.DeflateRect(3, 3, 3, 3);
CBrush *pOldBrush = pDC->SelectObject(&m_BackgroundBrush);
pDC->Rectangle(rect);
pDC->SelectObject(pOldBrush);
這里只是簡單的示范一下而已,畫出來的按鈕不一定好看。這里將按鈕的底色設置為純白色。程序的執行效果如下:
第三步繪制按鈕的文本。
// 繪制按鈕文本
TCHAR strButtonText[MAX_PATH + 1];
::GetWindowText(m_hWnd, strButtonText, MAX_PATH); // 獲取按鈕文本
if (strButtonText != NULL)
{
CFont *pFont = GetFont();
CFont *pOldFont = pDC->SelectObject(pFont);
CSize szExtent = pDC->GetTextExtent(strButtonText, _tcslen(strButtonText));
CRect rectText = lpDrawItemStruct->rcItem;
rectText.DeflateRect(rect.CenterPoint().x - szExtent.cx / 2, rect.CenterPoint().y - szExtent.cy / 2, rect.CenterPoint().x - szExtent.cx / 2, rect.CenterPoint().y - szExtent.cy / 2);
int nOldBkMode = pDC->SetBkMode(TRANSPARENT);
pDC->DrawText(strButtonText, -1, rectText, DT_WORDBREAK | DT_CENTER);
pDC->SelectObject(pOldFont);
pDC->SetBkMode(nOldBkMode);
}
重新編譯,執行效果如下:
按鈕的基本外觀已經繪制出來了。接下來還要繪制按鈕按下時,光標滑過時,光標離開時等等狀態下按鈕的外觀。當然還要探索一下圓形按鈕,三角形按鈕,以及不規則圖形按鈕的繪制。累了,先寫到這里。
MFC下按鈕自繪的實現(四)
接下來為自繪的按鈕繪制不同狀態下的外觀,首先繪制按鈕按下時的狀態。一般在按鈕按下時,按鈕的文本會向右下方移動一個微小的距離,使其看起來有被“壓下”的視覺。
通過如下代碼獲取按鈕的狀態:
UINT state = lpDrawItemStruct->itemState;
然后在DrawItem函數中繪制按鈕文本之前(DrawText)添加如下代碼:
if (state & ODS_SELECTED)
{
rectText.OffsetRect(1, 1);
}
編譯運行程序,看下效果。
按鈕正常狀態:
按鈕被按下時:
對比一下,可以看出按鈕按下時,按鈕上文本向右下方移動一小段距離。測試了一下,效果還不錯。
接下來繪制當光標位於按鈕之上但按鈕並沒有被按下時的狀態。這里僅僅大致的介紹一下我的方法。
首先在OnMouseMove函數中,添加如下代碼:
if (!m_bOver)
{
m_nTimerId = SetTimer(1, 50, TimerProc);
m_bOver = TRUE;
}
其中m_bOver是用來標記光標此時是否在按鈕之上的BOOL類型的變量。當光標經過按鈕時,觸發OnMouseMove函數,在此函數中設置一個定時器,定時時間為50ms,定時時間到了之后將觸發TimerProc回調函數。在TimerProc函數中不斷的判斷此時光標停留在按鈕之上,還是已經離開了按鈕。
POINT CursorPos;
RECT ButtonRect;
::GetCursorPos(&CursorPos);
::ScreenToClient(hwnd, &CursorPos);
::GetClientRect(hwnd, &ButtonRect);
if (!::PtInRect(&ButtonRect, CursorPos))
{
::PostMessage(hwnd, WM_MOUSELEAVE, 0, 0);
}
else
{
::PostMessage(hwnd, WM_MOUSEHOVER, 0, 0);
}
如果光標停留在按鈕之上,投遞出WM_MOUSEHOVER消息之后將觸發OnMouseHover函數,在該函數中將m_bOver設置為TURE,並調用InvalidateRect函數更新窗口;如果光標已經離開了按鈕,投遞出WM_MOUSELEAVE消息,觸發OnMouseLeave函數,在該函數中將m_bOver設置為FALSE,然后調用InvalidateRect函數更新窗口,接着關閉定時器,因為此時已經不必再連續判斷光標是否還停留在按鈕之上了。最后在DrawItem函數中判斷如果m_bOver為TURE,則繪制一定的圖形進行標記。
如下圖:
光標不在按鈕之上:
光標位於按鈕之上: