我住包子山

this->blog.MoveTo("blog.baozishan.in")

翻译习作:Create your own controls - the art of subclassing 子类化,继承公共控件

Create your own controls - the art of subclassing

By Chris Maunder

An introduction to subclassing the Windows common controls using MFC

Introduction

程序员们可以用许多windows通用控件提供的功能方便的编程,这些控件从listbutton甚至是进度条都是可以直接拿来用的.即便如此,仍然有时候你所选择的标准控件并不够用.欢迎学习子类化控件这个经典技法.

子类化一个窗体控件同子类化一个C++类并不一样.子类化一个控件意味着你要替换这个窗口的某些默认消息处理方法(message handlers),子类化能让你可以高效的劫持这个控件让它按照你要求的行为工作,而不是默认Windows的默认的行为.子类化几乎允许你把控件做成你想的那么完美.有两种子类化类型.实例对象实例化(instance subclassing)和全局实例化(global subclassing).实例对象子类化是当你将一个单个的窗口实例作为子类.全局子类化是将某一种某一窗口类型的(CLASSWND,我吃不准)窗口(控件)做成自定义版本.这里只讲下前者,实例对象实例化..

很重要的一点要了解继承自CWnd的对象同窗口本身(一个HWND)的区别.你的CWnd继承类对象包含一个成员变量指向HWND,并且包含HWND处理消息(eg WM_PAINT, WM_MOUSEMOVE)用到的处理调用函数?(message pump calls吃不准这个)

子类化是简单的.首先你建立一个能够处理你关注消息的窗口消息类,接着将你想要子类化的存在的窗体的行为用你指定的窗口消息类替换.之后这个窗口就很神奇了..下面就是一个对Button的子类化演示.

A New Class

为了子类化控件,我们要新建一个消息处理类,分别处理我们有兴趣的所有消息,我们都很懒,所以就尽量少处理几个消息,就处理我们常用的几个就好,最好的建立消息处理类的方法是直接继承存在的类 CButton ,这个类是Button 的默认消息处理类.

让我们假定我们要做写古怪的事,比如在每次鼠标落在按钮时后让按钮变成亮黄色.第一件事用ClassWizard建立CButton继承类CMyButton.

clip_image001

MFC里继承CButton有很多好处,最大好处就是不用再写多一行来实现原有的默认消息处理.如果我们愿意我们可以直接进行下一步,用这个CMyButton子类化一个按钮实例,这个按钮实例已经是一个功能完善的控件了,只是有点无趣(废话),button control 而已.因为MFC实现了所有的默认消息处理,所以我们可以仅仅重载几个我们有兴趣的消息处理,忽略其余的.

然而在这个例子中我们还是将其做成自己喜欢的怪异按钮吧.

判断鼠标是否在按钮上需要一个bool类型变量m_bOverControl. TRUE代表鼠标在按钮上,这个检查是定期的(使用定时器timer).不幸的是对于我们来说没有OnMouseEnter OnMouseLeave 函数能够跨越平台使用(?不解?can be used across platforms),所以我们使用OnMouseMove做这件事. 当计时器激发时,我们发现鼠标不在按钮上时我们让计时器失效,重绘空间.

使用ClassWizard添加WM_MOUSEMOVE WM_TIMER消息处理,他们分别对应 OnMouseMove OnTimer.

clip_image002

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 映射 WM_TIMER消息 => OnTimer(..). 这两个宏在MFC里很常用,有兴趣可以看看.

假设我们定义了两个变BOOL m_bOverControl , UINT m_nTimerID, 在构造函数初始化好,我们的消息处理如下:

void CMyButton::OnMouseMove(UINT nFlags, CPoint point)

{

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

    {

        TRACE0("Entering control\n");

        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 control\n");

        // 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这个虚函数.只有自绘控件才会调用这个函数,它没有默认的实现(it ASSERT's if you try).这个函数只用来对于控件类的重载.

clip_image003

使用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);

    }

}

这个类的各部分就差不多讲完了,但是这里要注意一点点. DrawItem 函数需要控件设置自绘属性,在资源编辑界面为控件填上自绘属性.不过还有一种更好的方法让这个子类自动设置它的窗体风格,打开自绘属性.要这样做我们需要重载这最后的函数PreSubclassWindow.

这个函数叫做SubclassWindow, 当调用CWnd::Create DDX_Control后都会被调用,也就是说如果你建立一个类的实例,无论是动态建立还是用对话框模板,PreSubclassWindow都会被调用. PreSubclassWindow 将会在你要子类化的窗口建立后,在这个窗口显示之前调用.也就是说它是需要对窗体表现进行初始化的一个最适合的位置.

非常重要的一点:如果你使用对话框资源新建了一个控件,那么你子类化的控件将不会得到WM_CREATE 消息, 因此我们不能使用 OnCreate 来初始化控件, 因为它有时候不会被调用(译注:但是SubclassWindow都可以).

使用ClassWizard重载PreSubclassWindow 添加如下代码:

void CMyButton::PreSubclassWindow()

{

    CButton::PreSubclassWindow();

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

}

恭喜你你现在有了CMybutton 这个继承类!

子类化

使用DDX 在创建过程中子类化一个窗体

在这个例子中,我在Demo对话框放上了一个按钮控件

clip_image004

我让默认的对话框创建过程创建这个控件,然后使用DDX_...将控件关联于自定义的类. 这种方法可以用ClassWizard简单的实现:添加一个成员变量,连接对应的控件ID(这里是IDC_BUTTON),设定为控件类型变量(Control type),class nameCMyButton我们自定义的类.

clip_image005

ClassWizard添加了一个DDX_Control调用在对话框的DoDataExchange 函数里. DDX_Control 调用SubclassWindow函数将CMyButton与控件关联,于是有了自定义消息处理的效果.这个按钮算是已经被劫持了,现在它的行为就跟我们想要的一样了.

使用ClassWizard子类化窗体使用一个不识别的类

如果你在工程中添加了一个窗体类并且想要子类化一个窗体,但是ClassWizard添加变量时里面没有显示新的类型,你需要重新build Classwizard 文件. (这个是VC6BUG)

备份.clw,删除原来的文件,回去原来的界面Ctrl+W.添加工程需要包括的文件,这样应该就行了 不行的话只有将控件关联的基类名称替换成自定义类名称了..

子类化一个存在的窗体

使用DDX 是简单的,但是如果我们需要子类化一个已经存在的空间就没有用了. 比如,如果你想要子类化一个在combobox中的Edit控件.你需要在你能子类化Edit窗口之前先拥有combobox(combobox包含一个Edit控件)

在这种情况下你需要用到 SubclassDlgItem SubclassWindow 函数.这两个函数允许你动态子类化窗体,换句话说,将已存在的某窗体关联于你的自定义窗体类.

举例来说,假设我们有一个对话框包括一个按钮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());

 

这个按钮绘制很简单, 但是也很经典了..是个基础吧.程序编译运行后就是如图:

clip_image006

注意我们只是重载了绘制函数,使用获取鼠标状态函数.这意味着你的控件仍然是一个按钮..为你的对话框类添加一个click的消息处理,你能看到它一样会被调用.

gohan 2008.1.24

23:59

Conclusion

Subclassing is not hard - you just need to choose the class you wish to subclass carefully, and be aware of what messages you need to handle. Read up on the control you are subclassing - learn about the messages it handles and also the virtual member functions of its implementation class. Once you've hooked into a control and taken over it's inner workings the sky's the limit.

History

26 Oct 2001 - added info in SubclassWindow and SubclassDlgItem

License

This article is licensed under The Code Project Open License (CPOL)

About the Author

Chris Maunder

clip_image007


Sitebuilder, Editor, Staff, Admin

Chris is the Co-founder, Administrator, Architect, Chief Editor and Shameless Hack who wrote and runs CodeProject. He's been programming since 1988 while pretending to be, in various guises, an astrophysicist, mathematician, physicist, hydrologist, geomorphologist, defence intelligence researcher and then, when all that got a bit rough on the nerves, a web developer. He is a Microsoft Visual C++ MVP both globally and for Canada locally.

His programming experience includes C/C++, C#, MFC, ASP, ASP.NET, and far, far too much FORTRAN. He has worked on PocketPCs, AIX mainframes, Sun workstations, and a CRAY YMP C90 behemoth but finds notebooks take up less desk space.

He dodges, he weaves, and he never gets enough sleep. He is kind to small animals.

Chris was born and bred in Australia but splits his time between Toronto and Melbourne, depending on the weather. For relaxation he is into road cycling, snowboarding, rock climbing, and storm chasing.

Occupation:

Founder

Company:

The Code Project

Location:

clip_image008Canada

 

 

posted on 2008-01-25 00:02 Gohan 阅读(1453) 评论(0)  编辑 收藏 引用 所属分类: MFC/SDK


只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理