天天看點

[VC控件]子類化控件

作為一個程式員,我們經常會在程式中用到Windows通用控件。比如按鈕控件,進度條控件等等。但是有時我們需要給控件更多的特色,這就需要做控件的子類化(subclassing). 子類化一個Windows控件與子類化一個C++類不同,子類化一個控件要求你把一個視窗的一些或所有的消息映射都替換成自己的函數來響應,這樣你就有效的阻止了控件去做系統預設的行為,而按自己的想法去做。子類化有兩種類型: 執行個體子類化(instance subclassing)和全局子類化(global subclassing)。執行個體子類化是子類化一個視窗中的單一執行個體,全局子類化是把整個視窗子類化為一個特殊的類型。這裡我們僅讨論單一執行個體子類化。 記住CWnd派生類對象與視窗本身(一個HWND)的差别是很重要的。你的C++ CWnd-派生類對象包含了一個指向HWND的成員函數,并且包含了當處理消息時HWND消息泵的響應函數(比如WM_PAINT, WM_MOUSEMOVE)。但你用一個C++對象子類化一個視窗時,你就把HWND與C++對象關聯起來,并且設定了處理消息時把自定義的回調函數提供給HWND消息使用。 子類化過程很簡單,首先建立一個類映射視窗的所有消息,然後把控件用作為這個類的執行個體。例如,下面的例子中我們做一個按鈕的子類化。 新類

為了子類化一個控件,我們需要建立一個新類,并映射所有我們感興趣的消息。為了簡便,我們一般都從控件标準類中派生自己的新類,這裡與按鈕控件對應的标準類為CButton。 下面假定我們要實作的效果是,當滑鼠懸停在按鈕上方時,按鈕顯示為黃色。首先我們使用ClassWizard建立一個CButton的派生類,叫做CMyButton。 在MFC架構中從CButton派生自己的類有許多好處,最大的好處是我們不用手工添加任何一行代碼就可以建立了一個擁有全部預設功能的Windows控件。因為MFC實作了所有的預設的消息映射,是以我們可以挑選我們感興趣的消息自己處理,而不用去管其他消息。 這裡我們要為按鈕設計的功能是,滑鼠懸停時變為黃色。 為了檢查滑鼠是否懸停于按鈕上,我們設定一個成員變量m_bOverControl ,TRUE表示滑鼠懸停,然後設定一個周期(使用定時器)跟蹤滑鼠是否已離開控件,這是因為,系統并沒有OnMouseEnter和 OnMouseLeave函數供我們調用,是以我們必須使用OnMouseMove。如果,在一個時間點上,發現滑鼠已離開按鈕,我們關閉定時器并重畫控件。 使用ClassWizard加入WM_MOUSEMOVE和WM_TIMER的消息映射,響應函數分别是OnMouseMove和OnTimer。 ClassWizard将在你的按鈕類檔案中加入下面的代碼: BEGIN_MESSAGE_MAP(CMyButton, CButton)

    //{{AFX_MSG_MAP(CMyButton)

    ON_WM_MOUSEMOVE()

    ON_WM_TIMER()

    //}}AFX_MSG_MAP

END_MESSAGE_MAP() /

// CMyButton message handlers void CMyButton::OnMouseMove(UINT nFlags, CPoint point)

{

    // TODO: Add your message handler code here and/or call default

    CButton::OnMouseMove(nFlags, point);

} void CMyButton::OnTimer(UINT nIDEvent)

{

    // TODO: Add your message handler code here and/or call default

    CButton::OnTimer(nIDEvent);

} 消息映射的入口(即BEGIN_MESSAGE_MAP) 建立了視窗消息與響應函數的對應關系。ON_WM_MOUSEMOVE把WM_MOUSEMOVE消息與OnMouseMove函數建立響應的關系, ON_WM_TIMER m把WM_TIMER消息與OnTimer函數建立了響應的關系。這些宏定義在MFC的源檔案中,我們不需要去看,隻要按照約定來做就可以了。 假設我們已經聲明了兩個變量m_bOverControl和m_nTimerID,類型分别是BOOL和UINT, 并且在類的構造函數中把它們初始化,我們的消息處理應使用下面的代碼: void CMyButton::OnMouseMove(UINT nFlags, CPoint point)

{

    if (!m_bOverControl)                    // Cursor has just moved over control

    {

        TRACE0("Entering controln");         m_bOverControl = TRUE;              // Set flag telling us the mouse is in

        Invalidate();                       // Force a redraw         SetTimer(m_nTimerID, 100, NULL);    // Keep checking back every 1/10 sec

    }

    CButton::OnMouseMove(nFlags, point);    // drop through to default handler

} void CMyButton::OnTimer(UINT nIDEvent)

{

    // Where is the mouse?

    CPoint p(GetMessagePos());

    ScreenToClient(&p);     // Get the bounds of the control (just the client area)

    CRect rect;

    GetClientRect(rect);     // Check the mouse is inside the control

    if (!rect.PtInRect(p))

    {

        TRACE0("Leaving controln");         // if not then stop looking...

        m_bOverControl = FALSE;

        KillTimer(m_nTimerID);         // ...and redraw the control

        Invalidate();

    }

    // drop through to default handler

    CButton::OnTimer(nIDEvent);

}

最後我們來畫出我們需要的效果,我們不再進行消息映射,而是重載CWnd::DrawItem虛函數。隻有當控件設定owner-drawn風格時這個函數才能被調用,并且這個函數沒有預設的實作代碼,虛函數的設計隻為了在派生類中進行實作。 使用ClassWizard重載DrawItem函數,并加入下面的代碼 void CMyButton::DrawItem(LPDRAWITEMSTRUCT lpDrawItemStruct)

{

    CDC* pDC   = CDC::FromHandle(lpDrawItemStruct->hDC);

    CRect rect = lpDrawItemStruct->rcItem;

    UINT state = lpDrawItemStruct->itemState;     CString strText;

    GetWindowText(strText);     // draw the control edges (DrawFrameControl is handy!)

    if (state & ODS_SELECTED)

        pDC->DrawFrameControl(rect, DFC_BUTTON, DFCS_BUTTONPUSH | DFCS_PUSHED);

    else

        pDC->DrawFrameControl(rect, DFC_BUTTON, DFCS_BUTTONPUSH);     // Deflate the drawing rect by the size of the button’s edges

    rect.DeflateRect( CSize(GetSystemMetrics(SM_CXEDGE), GetSystemMetrics(SM_CYEDGE)));

    // Fill the interior color if necessary

    if (m_bOverControl)

        pDC->FillSolidRect(rect, RGB(255, 255, 0)); // yellow     // Draw the text

    if (!strText.IsEmpty())

    {

        CSize Extent = pDC->GetTextExtent(strText);

        CPoint pt( rect.CenterPoint().x - Extent.cx/2,

        rect.CenterPoint().y - Extent.cy/2 );         if (state & ODS_SELECTED)

            pt.Offset(1,1);         int nMode = pDC->SetBkMode(TRANSPARENT);         if (state & ODS_DISABLED)

            pDC->DrawState(pt, Extent, strText, DSS_DISABLED, TRUE, 0, (HBRUSH)NULL);

        else

            pDC->TextOut(pt.x, pt.y, strText);         pDC->SetBkMode(nMode);

    }

}

接下來,我們剩下最後一步。為控件 設定owner drawn風格。我們可以在對話框的資源編輯器中,右鍵單擊按鈕控件,選擇“屬性”,然後在Style中選中owner drawn風格。但是有一種更好的方法,使得使用建立類子類化的按鈕自動的設定owner drawn風格。為了完成這個功能,我們重載最後一個函數: PreSubclassWindow。 這個函數将在子類化視窗時被調用,次序是在CWnd::Create或DDX_Control之後,這就是說,無論是動态的建立視窗執行個體還是使用對話框模闆建立,這個函數都将被調用。PreSubclassWindow在視窗子類化建立後和視窗被顯示前被調用,換句話說,這是我們來做視窗初始化的一個最好時機。 一個重點要注意的地方是: 如果你是用對話框資源建立一個控件,那麼你要子類化的控件将不會響應WM_CREATE消息,是以我們不能在OnCreate函數中做初始化的工作,因為它并不是在所有的情況下都被調用。 使用ClassWizard重載PreSubclassWindow函數并加入下面的代碼 void CMyButton::PreSubclassWindow()

{

    CButton::PreSubclassWindow();     ModifyStyle(0, BS_OWNERDRAW); // make the button owner drawn

} 祝賀 - 你的Cbutton派生類已經完成。 子類化

在建立時使用DDX子類化

在這個例子中,我們使用對話框編輯器在對話框中加入了一個新的按鈕: 然後,使用ClassWizard為你的按鈕控件添加成員變量,變量類型選擇我們剛剛建立的類CMyButton ClassWizard g會在對話框的DoDataExchange函數中建立一個DDX_Control調用。DDX_Control啟動了子類化過程,使得按鈕控件使用CMyButton類進行消息映射,而不是使用通常的CButton。 使用沒有在ClassWizard中注冊的類子類化視窗

如果你在工程中加入了一個新的視窗類,并且希望使用這個新類類型子類化你的視窗,但是ClassWizard中并沒有提供新類的選項,那麼你需要重新生成class wizard檔案。 先備份以下工程中的.clw檔案,然後删除它。接下來在Visual Studio中按Ctrl+W。你将看到一個提示框,要求你加入ClassWizard中包含類的檔案,确認選擇的檔案中包含了新類的檔案(soarlove注:一般情況下,選擇“add all”即可。 現在你的新類已經可以供選擇。如果不想這樣做,你還有一個通用的方法,就是在選擇類型的時候使用通用的類(比如CButton),然後在頭檔案中手工把通用類(CButton)改為你的新類(CMyButton)。 子類化一個存在的視窗

使用DDX固然簡單,但是不能幫助我們實作一個已存在視窗的子類化。比如你想在combobox中子類化一個Edit控件,那麼在你子類化Edit控件之前,你需要先建立combobox控件。 這種情況下,我們使用 SubclassDlgItem或者 SubclassWindow函數。這兩個函數允許你動态的子類化一個視窗,換句話說,把一個新的視窗執行個體與已經存在的視窗建立關聯。 比如,假設有一個對話框中包含了一個按鈕ID IDC_BUTTON1。這個按鈕已經被建立,我們想用一個CMyButton的執行個體來與之關聯,以使得按鈕符合我們需要的行為。 為了做到這些,我們需要有一個新類型的執行個體,最後的方法是在對話框或視的頭檔案中加入成員函數。 CMyButton m_btnMyButton;

然後在對話框的OnInitDialog (或任何适當的地方) 中調用: m_btnMyButton. SubclassDlgItem(IDC_BUTTON1, this);

假設你已經有了一個視窗的指針,或者你工作在一個CView或其他CWnd派生類中裡面的控件被動态的建立,或者你不想使用 SubclassDlgItem函數,那麼你可以使用下面的方法: CWnd* pWnd = GetDlgItem(IDC_BUTTON1); // or use some other method to get

                                      // a pointer to the window you wish

                                      // to subclass

ASSERT( pWnd && pWnd->GetSafeHwnd() );

m_btnMyButton. SubclassWindow(pWnd->GetSafeHwnd());

畫按鈕是非常簡單的,不需要考慮按鈕的風格(比如flat風格),也不需要考慮适應文字,僅僅需要考慮你畫的範圍。如果你編譯運作提供的示範代碼,那麼你将看到,當滑鼠懸停于按鈕上方時,按鈕變為黃色。 注意,實際上我們隻重載了畫的函數,并截取了滑鼠移動的函數。其餘的功能都還是使預設響應的。 結論

子類化并不難 - 你隻要認真的選擇你要子類化的類并且知道你要映射那些消息。要熟悉你要子類化的類,了解提供的消息和類中的虛函數。

繼續閱讀