第四部分:消息映射

应用程序放在窗口中的任何用户界面对象都具有两种可控制的特性:1) 它的外观,2) 它响应事件的行为。在上一讲中,你已经学习了 CStatic 控制和如何使用样式属性来定制用户界面对象的外观。这些概念可用于 MFC 中的所有不同控制类。

在本讲中,我们将介绍 CButton 控制,以理解消息映射和简单的事件处理。然后还要介绍使用 CScrollBar 控制的稍微复杂点的例子。

理解消息映射

在第二讲中,MFC 程序不包括主要函数或时间循环。所有的事件处理都是作为 CWinApp 的一部分在后台处理的。因为它们是隐藏的,所以我们需要一种方法来告诉不可见的时间循环通告我们应用程序所感兴趣的事件。这需要一种叫做消息映射的机制。消息映射识别感兴趣的事件然后调用函数来响应这些事件。

例如,如果你要编写一个程序,当用户按下标有“退出”的按钮时要退出应用程序。在程序中,你编写代码来建立按钮:你指示按钮应如何动作。然后,为其父窗口建立用户单击按钮时的消息映射,它试图要传递消息给其父窗口。为了建立父窗口的消息,你要建立截取消息映射的机制,并且使用按钮的消息。当一指定的按钮事件发生时,消息映射会请求 MFC 调用一指定的函数。在这种情况下,单击退出按钮就是所感兴趣的事件。然后你把退出应用程序的代码放到指定的函数中。

其它的工作就由 MFC 来做了。当程序执行时,用户单击“退出”按钮时,按钮就会自己加亮。然后 MFC 自动调用相应的函数,并且程序会终止。只使用很少的几行代码你就响应了用户事件。

CButton 类

在上一讲中所讨论的 CStatic 控制是唯一不响应用户时间的控制。Windows 中所有的其它控制都可响应用户事件。第一,当用户处理它们时,它们会自动更新其外观(例如,当用户单击按钮时,按钮会自己加亮以给用户一个反馈)。第二,每个不同的控制都要发送信息给你的代码以使程序能响应用户的需要。例如,当单击按钮时,按钮就会发送一个命令消息。如果你编写代码接收消息,则你的代码就能响应用户事件。

为了理解这个过程,我们从 CButton 控制开始。下面的代码说明了建立按钮的过程:

// button1.cpp #include <afxwin.h> #define IDB_BUTTON 100

// Declare the application class class CButtonApp : public CWinApp

{

public:

virtual BOOL InitInstance();

};

// Create an instance of the application class CButtonApp ButtonApp;

// Declare the main window class

class CButtonWindow : public CFrameWnd

{

CButton *button; public:

CButtonWindow();

};

// The InitInstance function is called once

// when the application first executes BOOL CButtonApp::InitInstance()

{

m_pMainWnd = new CButtonWindow(); m_pMainWnd->ShowWindow(m_nCmdShow); m_pMainWnd->UpdateWindow();

return TRUE;

}

// The constructor for the window class CButtonWindow::CButtonWindow()

{

CRect r;

// Create the window itself Create(NULL,

"CButton Tests", WS_OVERLAPPEDWINDOW, CRect(0,0,200,200));

// Get the size of the client rectangle GetClientRect(&r);

r.InflateRect(-20,-20);

// Create a button button = new CButton();

button->Create("Push me", WS_CHILD|WS_VISIBLE|BS_PUSHBUTTON,

r, this,

IDB_BUTTON);

}

上面的代码与前面介绍的代码几乎相同。CButton 类的 Create 函数共有 5 个参数。前四个与 CStatic 的相同。第五个参数为按钮的资源 ID。资源 ID 是用来标识消息映射中按钮的唯一整数值。常数值 IDB_BUTTON 已经在程序的顶部做了定义。“IDB_”是任选的,只是该常量 ID 是用来表示按钮的。它的值为 100,因为 100 以内的值都为系统所保留。你可以使用任何大于 99 的值。

CButton 类所允许的样式属性与 CStatic 类的是不同的。定义了 11 个不同的“BS”

(“Button Style”)常量。完整的“BS”常量列表可在用 Search 命令查找 CButton,并选择“button style”。这里我们要用的是 BS_PUSHBUTTON 样式,它表示我们要一正常的的按钮方式来显示该按钮。我们还使用了两个熟悉的“WS”属性: WS_CHILD 和 WS_VISIBLE。我们将在后面介绍其它一些样式。

当你运行代码时,会注意到按钮响应了用户事件。既它加亮了。除此之外它没有做任

何事情,因为我们还没有教它怎样去做。我们需要编写消息映射来使按钮做一些感兴趣的事情。

建立消息映射

下面的代码包含有消息映射,也包含有新的处理单击按钮的函数(当用户单击按钮时会响一下喇叭)。它只是前面代码的一个简单的扩充:

// button2.cpp #include <afxwin.h> #define IDB_BUTTON 100

// Declare the application class

class CButtonApp : public CWinApp

{

public:

virtual BOOL InitInstance();

};

// Create an instance of the application class CButtonApp ButtonApp;

// Declare the main window class

class CButtonWindow : public CFrameWnd

{

CButton *button;

public:

CButtonWindow();

afx_msg void HandleButton(); DECLARE_MESSAGE_MAP()

};

// The message handler function void CButtonWindow::HandleButton()

{

MessageBeep(-1);

}

// The message map BEGIN_MESSAGE_MAP(CButtonWindow, CFrameWnd) ON_BN_CLICKED(IDB_BUTTON, HandleButton) END_MESSAGE_MAP()

// The InitInstance function is called once

// when the application first executes BOOL CButtonApp::InitInstance()

{

m_pMainWnd = new CButtonWindow(); m_pMainWnd->ShowWindow(m_nCmdShow); m_pMainWnd->UpdateWindow();

return TRUE;

}

// The constructor for the window class CButtonWindow::CButtonWindow()

{

CRect r;

// Create the window itself Create(NULL,

"CButton Tests", WS_OVERLAPPEDWINDOW, CRect(0,0,200,200));

// Get the size of the client rectangle

GetClientRect(&r); r.InflateRect(-20,-20);

// Create a button button = new CButton();

button->Create("Push me",

WS_CHILD|WS_VISIBLE|BS_PUSHBUTTON,

r, this,

IDB_BUTTON);

}

主要修改了三个方面:

  1. CButtonWindow 的类说明现在包含了一个新的成员函数和一个新的表示消息映射的宏。HandleButton 函数是正常的 C++函数,它通过 afx_msg 标签确定为消息处理函数。该函数需要一些特殊的约束,例如,它必须是 void 型并且它不能接收任何参数。DECLARE_MESSAGE_MAP 宏建立了消息映射。函数和宏都必须是 public 型的。

  2. HandleButton 函数作为成员函数以同样的方式来建立。在该函数中,我们调用了Windows API 中的 MessageBeep 函数。

  3. 用宏来建立消息映射。在代码中,你可以看见 BEGIN_MESSAGE_MAP

    宏接收两各参

数。第一个指定了使用消息映射的类的名称。第二个是基类。然后是 ON_BN_CLICKED 宏,接受两个参数控制的 ID 和该 ID 发送命令消息时所调用的函数。最后,消息映射用 END_MESSAGE_MAP 来结束。

当用户单击按钮时,它向其包含该按钮的父窗口发送了一个包含其 ID 的命令消息。那是按钮的缺省行为,这就是该代码工作的原因。按钮向其父窗口发送消息,是因为它是子窗口。父窗口截取该消息并用消息映射来确定所要调用的函数。MFC 来安排,只要指定的消息一出现,相应的函数就会被调用。

ON_BN_CLICKED 消息是 CButton 发送的唯一感兴趣的消息。它等同于 CWnd 中的ON_COMMAND 消息,只是一个更简单方便的同义词而已。

改变大小的消息

在上面的代码中,由于有了消息映射,从 CFrameWnd 继承来的应用程序窗口认出按钮有按钮产生的单击消息并响应之。加入消息映射的 ON_BN_CLICKED 宏指定了按钮的 ID 和窗口在接收到来自按钮的命令消息时应调用的函数。因为只要用户单击了按钮,按钮就会自动把其 ID 发送父窗口,这样才能允许代码正确地处理按钮事件。

作为该应用程序的主窗口的框架窗口自己也有传递消息的能力。大约有 100 不同的消息可用,它们都是从 CWnd 类继承来的。从 MFC 帮助文件中浏览 CWnd 类的成员函数,你就会看到所有的这些消息。查看所有以“On”开头的成员函数。

你可能已经注意到了,至今为止所有的代码都不能很好地处理尺寸变化。当窗口变化大小时,窗口的框架会做相应的调整,但是窗口中调的内容仍原处不动。可以通过处理尺寸变化的事件来更好的处理这一问题。任何窗口发送的消息之一就是变尺寸消息。该消息是当改变形状时发出的。我们可以使用该消息来控制框架中子窗口的大小,如下所示:

// button3.cpp #include <afxwin.h> #define IDB_BUTTON 100

// Declare the application class class CButtonApp : public CWinApp

{

public:

virtual BOOL InitInstance();

};

// Create an instance of the application class CButtonApp ButtonApp;

// Declare the main window class

class CButtonWindow : public CFrameWnd

{

CButton *button; public:

CButtonWindow();

afx_msg void HandleButton();

afx_msg void OnSize(UINT, int, int); DECLARE_MESSAGE_MAP()

};

// A message handler function

void CButtonWindow::HandleButton()

{

MessageBeep(-1);

}

// A message handler function

void CButtonWindow::OnSize(UINT nType, int cx, int cy)

{

CRect r; GetClientRect(&r); r.InflateRect(-20,-20); button->MoveWindow(r);

}

// The message map BEGIN_MESSAGE_MAP(CButtonWindow, CFrameWnd) ON_BN_CLICKED(IDB_BUTTON, HandleButton) ON_WM_SIZE()

END_MESSAGE_MAP()

// The InitInstance function is called once

// when the application first executes BOOL CButtonApp::InitInstance()

{

m_pMainWnd = new CButtonWindow(); m_pMainWnd->ShowWindow(m_nCmdShow); m_pMainWnd->UpdateWindow();

return TRUE;

}

// The constructor for the window class CButtonWindow::CButtonWindow()

{

CRect r;

// Create the window itself Create(NULL,

"CButton Tests", WS_OVERLAPPEDWINDOW, CRect(0,0,200,200));

// Get the size of the client rectangle GetClientRect(&r);

r.InflateRect(-20,-20);

// Create a button button = new CButton();

button->Create("Push me", WS_CHILD|WS_VISIBLE|BS_PUSHBUTTON,

r, this,

IDB_BUTTON);

}

为了理解上面的代码,从窗口的消息映射开始。你会发现入口 ON_WM_SIZE。该入口表示消息映射是对来自 CButtonWindow 对象的变尺寸消息发生响应。变尺寸消息是当用户改变窗口的大小时产生的。该消息来自窗口本身,而不是作为 ON_COMMAND 消息由按钮向其父窗口发送的。这是因为窗口框架不是子窗口。

要注意的是消息映射中的 ON_WM_SIZE 入口没有参数。你在 MFC 文档中 CWnd 类,消息映射中的 ON_WM_SIZE 入口总是调用 OnSize 函数,并且该函数必须接收三个参数。OnSize 函数必须是消息映射所属类的成员函数,并且该函数必须用 afx_msg 来说明(正如上面在CButtonWindow 的定义中所见到的一样)。

如果你查看 MFC 文档,就会发现 CWnd 中有近 100 名为“On...”的函数。CWnd::OnSize 是其中之一。所有这些函数都在消息映射中有形如 ON_WM_对应的标签。例如,ON_WM_SIZE 对应 OnSize。ON_WM_入口不接收任何参数,如 ON_BN_CLICKED 一样。参数是假设的并自动传递给相应的如 OnSize 的“On...”函数。

重复一遍,因为它很重要: OnSize 函数总是与消息映射中的 ON_WM_SIZE 入口想对应。你必须命名处理函数 OnSize, 并且它必须接收三个参数。不同的函数的参数会有所不同。

上面的代码中在 OnSize 函数自身的内部,有三行代码修改了按钮在窗口中的尺寸。你可以在该函数中输入任何你想要的代码。

调用 GetClientRect 是为了恢复窗口用户区域的新尺寸。该矩形会被缩小,并调用按钮的 MoveWindow 函数。MoveWindow 是从 CWnd 继承来的,改变尺寸和移动子窗口是在一步完成的。

当你执行上面改变窗口大小的程序时,你就会发现按钮自己能正确地改变大小。在代码中,变尺寸事件他国消息映射中的 OnSize 函数而产生一调用,它调用 MoveWindow 函数来改变按钮的大小。

窗口消息

查看 MFC 文档,你可以看到主窗口处理的各种各样的 CWnd 消息。有些与我们上面介绍的类似。例如,ON_WM_MOVE 消息是当用户移动窗口时发送的消息,ON_WM_PAINT 消息是当窗口的任何部分需要重画时发出的。至今为止,我们的所有程序,重画工作都是自动完成的,因为是控制自己来负责其外观。如果你自己使用 GDI 命令来在用户区域中绘制,应用程序就应负责重画工作。因此 ON_WM_PAINT 就变得重要了。

还有一些发送给窗口的事件消息更深奥。例如,你可以使用 ON_WM_TIMER 消息与SetTimer 函数来使接收预先设置的时间间隔。下面的代码给出了该过程。当你运行该代码时,程序会每隔 1 秒钟鸣笛一声。你可以用其它更有用的功能来代替鸣笛。

// button4.cpp #include <afxwin.h> #define IDB_BUTTON 100

#define IDT_TIMER1 200

// Declare the application class class CButtonApp : public CWinApp

{

public:

virtual BOOL InitInstance();

};

// Create an instance of the application class CButtonApp ButtonApp;

// Declare the main window class

class CButtonWindow : public CFrameWnd

{

CButton *button; public:

CButtonWindow();

afx_msg void HandleButton();

afx_msg void OnSize(UINT, int, int); afx_msg void OnTimer(UINT); DECLARE_MESSAGE_MAP()

};

// A message handler function

void CButtonWindow::HandleButton()

{

MessageBeep(-1);

}

// A message handler function

void CButtonWindow::OnSize(UINT nType, int cx, int cy)

{

CRect r; GetClientRect(&r); r.InflateRect(-20,-20); button->MoveWindow(r);

}

// A message handler function

void CButtonWindow::OnTimer(UINT id)

{

MessageBeep(-1);

}

// The message map BEGIN_MESSAGE_MAP(CButtonWindow, CFrameWnd)

ON_BN_CLICKED(IDB_BUTTON, HandleButton) ON_WM_SIZE()

ON_WM_TIMER()

END_MESSAGE_MAP()

// The InitInstance function is called once

// when the application first executes BOOL CButtonApp::InitInstance()

{

m_pMainWnd = new CButtonWindow(); m_pMainWnd->ShowWindow(m_nCmdShow); m_pMainWnd->UpdateWindow();

return TRUE;

}

// The constructor for the window class CButtonWindow::CButtonWindow()

{

CRect r;

// Create the window itself Create(NULL,

"CButton Tests", WS_OVERLAPPEDWINDOW, CRect(0,0,200,200));

// Set up the timer

SetTimer(IDT_TIMER1, 1000, NULL); // 1000 ms.

// Get the size of the client rectangle GetClientRect(&r);

r.InflateRect(-20,-20);

// Create a button button = new CButton();

button->Create("Push me", WS_CHILD|WS_VISIBLE|BS_PUSHBUTTON,

r, this,

IDB_BUTTON);

}

在上面的程序内部,我们建立了一个按钮,如前所示,改变尺寸的代码没有变动。在窗口的构造函数中,我们添加了 SetTimer 函数的调用。该函数接收三个参数:时钟的 ID(可以同时使用多个时钟,每次时钟关闭时都会把 ID 传递给所调用的函数),时间以毫秒为单位。在这里,我们向函数传送了 NULL,以使窗口消息映射自己自动发送函数。在消息映射中,我们已经通知了 ON_WM_TIMER 消息,它会自动调用 OnTimer 函数来传递已经关闭了的时钟的 ID。

当程序运行时,它每隔 1 毫秒鸣笛一声。每次时钟的时间增量流逝,窗口都会发送消息给自己。消息映射选择消息给 OnTimer 函数,它鸣笛。你可以在此放置更有用的代码。

滚动条控制

Windows 用两种不同的方式来处理滚动条。一些控制,如编辑控制和列表控制,可以带有滚动条。在这种情况下,滚动条会被自动处理,不不要额外的代码来处理。

滚动条也可以作为单独的元件来使用。当这样使用时,滚动条就拥有独立的权力。你可以参见 MFC 参考手册中有关 CScrollBar 的有关章节。滚动条控制的建立与前面介绍的静态标签和按钮的一样。它有四个成员函数允许你设置和获取滚动条的位置和范围。

下面的代码演示了建立水平滚动条的过程和其消息映射:

// sb1.cpp

#include <afxwin.h> #define IDM_SCROLLBAR 100 const int MAX_RANGE=100; const int MIN_RANGE=0;

// Declare the application class

class CScrollBarApp : public CWinApp

{

public:

virtual BOOL InitInstance();

};

// Create an instance of the application class CScrollBarApp ScrollBarApp;

// Declare the main window class

class CScrollBarWindow : public CFrameWnd

{

CScrollBar *sb; public:

CScrollBarWindow();

afx_msg void OnHScroll(UINT nSBCode, UINT nPos, CScrollBar* pScrollBar);

DECLARE_MESSAGE_MAP()

};

// The message handler function

void CScrollBarWindow::OnHScroll(UINT nSBCode, UINT nPos, CScrollBar* pScrollBar)

{

MessageBeep(-1);

}

// The message map BEGIN_MESSAGE_MAP(CScrollBarWindow, CFrameWnd) ON_WM_HSCROLL()

END_MESSAGE_MAP()

// The InitInstance function is called once

// when the application first executes BOOL CScrollBarApp::InitInstance()

{

m_pMainWnd = new CScrollBarWindow(); m_pMainWnd->ShowWindow(m_nCmdShow); m_pMainWnd->UpdateWindow();

return TRUE;

}

// The constructor for the window class CScrollBarWindow::CScrollBarWindow()

{

CRect r;

// Create the window itself Create(NULL,

"CScrollBar Tests", WS_OVERLAPPEDWINDOW, CRect(0,0,200,200));

// Get the size of the client rectangle GetClientRect(&r);

// Create a scroll bar

sb = new CScrollBar();

sb->Create(WS_CHILD|WS_VISIBLE|SBS_HORZ, CRect(10,10,r.Width()-10,30),

this, IDM_SCROLLBAR);

sb->SetScrollRange(MIN_RANGE,MAX_RANGE,TRUE);

}

Windows 会区分水平和垂直滚动条,同时还支持 CScrollBar 中一称为尺寸盒的控制。尺寸盒是一个小方块。它处于水平和垂直滚动条的交叉处,呀鼠标拖动它会自动改变窗口的大小。在后面的代码中你看到如何用 Create 函数的 SBS_HORZ 样式来建立一水平滚动条。在建立了滚动条之后,马上用 SetScrollRange 中的 MIN_RANGE 和 MAX_RANGE 龙个常数给出了滚动条的范围 0~100(它们定义在程序的顶部)。

事件处理函数 OnHScroll 来自 CWnd 类。我们使用该函数是因为该代码建立了水平滚动条。对于垂直滚动条应使用 OnVScroll。在代码中,消息映射与滚动函数相联系,并使滚动条在用户操作时发出鸣笛声。当你运行该程序时,你可以单击箭头、拖动滚动条上的小方块等等。每次操作都会出现鸣笛声,但是滚动条上的小方块实际上不会移动,因为我们还没有把它与实际的代码相关联。

每次滚动条调用 OnHScroll 时,你的代码都要确定用户的操作。在 OnHScroll 函数内部,你可以检验传递给处理函数的第一参数,如下所示。如果你与上面的代码一起使用, 滚动条的小方块就会移动到用户操作的位置处。

// The message handling function

void CScrollBarWindow::OnHScroll(UINT nSBCode, UINT nPos, CScrollBar* pScrollBar)

{

int pos;

pos = sb->GetScrollPos();

switch ( nSBCode )

{

case SB_LINEUP: pos -= 1; break;

case SB_LINEDOWN: pos += 1; break;

case SB_PAGEUP: pos -= 10; break;

case SB_PAGEDOWN: pos += 10; break;

case SB_TOP:

pos = MIN_RANGE;

break;

case SB_BOTTOM:

pos = MAX_RANGE;

break;

case SB_THUMBPOSITION:

pos = nPos; break;

default:

return;

}

if ( pos < MIN_RANGE ) pos = MIN_RANGE;

else if ( pos > MAX_RANGE ) pos = MAX_RANGE;

sb->SetScrollPos( pos, TRUE );

}

SB_LINEUP 和 SB_LINEDOWN 的不同常数值在 CWnd::OnHScroll 函数文档中有介绍。上面的代码首先使用 GetScrollPos 函数来恢复滚动条的当前位置。然后使用开关语句来确定用户对滚动条的操作。SB_LINEUP 和 SB_LINEDOWN 常数值意味着垂直方向,但也可用于水平方向表示左右移动。SB_PAGEUP 和 SB_PAGEDOWN 是用在用户单击滚动条时。SB_TOP 和SB_BOTTOM 用于当用户移动滚动条小方块到滚动条的顶部和底部。SB_THUMBPOSITION 用于当用户拖动小方块到指定位置时。代码会自动调整位置,然后确保它在设置其新位置时仍然在范围内。一旦设置了滚动条,小方块就会移动到适当的位置。

垂直滚动条的处理也是类似的,只是要用 OnVScroll 函数中的 SBS_VERT 样式。

理解消息映射

消息映射结构只能用于 MFC。掌握它和如何在你的代码中应用它是很重要的。

可能纯 C++使用者会对消息映射产生疑问: 为什么 Microsoft 不用虚拟函数来替代消息映射?虚拟函数是MFC 中处理消息映射的标准C++方式,所以使用宏DECLARE_MESSAGE_MAP 和 BEGIN_MESSAGE_MAP 可能有些怪异。

MFC 使用消息映射来解决虚拟函数的基本问题。参见 MFC 帮助文件中的 CWnd 类。它包含 200 多个成员函数,所有的成员函数当不使用消息映射时都是虚拟的。现在来看一下所有 CWnd 类的子类。MFC 中大约有近 30 个类是以 CWnd 为基类的。这包括所有的可见控制如按钮、静态标签和列表。现在想象一下,MFC 使用虚拟函数,并且你建立一应用程序包含有 20 个控制。CWnd 中的 200 个虚拟函数中的每个都需要自己的虚拟函数表,并且一个控

制的每个例程都应有一组 200 个虚拟函数与之关联。则程序可能就有近 4,000 个虚拟函数表在内存中,这对内存有限的机器来说是个大问题。因为其中的大部分是不用的。

消息映射复制了虚拟函数表的操作,但是它是基于需要的基础之上的。当你在消息映射中建立一个入口时,你是在对系统说,“当你看见一个特殊的消息时,请调用指定的函数”。只有这些函数实际上被重载到消息映射中,着就节省了内存和 CPU 的负担。

当你用 DECLARE_MESSAGE_MAP 和 BEGIN_MESSAGE_MAP 说明消息映射时,系统会通过你的消息映射选择所有的消息。如果消息映射处理了给定的消息,则你的函数会被调用,卸车也就停留在此。但是,如果你的消息映射中不包含某个消息的入口,则系统会把该消息发送第二个 BEGIN_MESSAGE_MAP 指定的类。那个类可能会也可能不会处理它,如此重复。最后,如果没有消息映射处理一给定的消息,该消息会到由一缺省的处理函数来处理。

结论

本讲中所介绍的所有消息映射处理概念可适用于 Windows NT 中所有的控制和窗口。在大部分情况下,你可以使用 ClassWizard 来安装消息映射的入口,它将在后面的有关ClassWizard、AppWizard 和资源编辑器一文中介绍。