3.2 事件表和事件处理过程

wxWidgets事件处理系统比起通常的虚方法机制来说要稍微复杂一点,但它的一个好处是可以避免需要实现基类中所有的虚方法,因为实现所有的基类虚方法有时候是不切实际的或者是效率很低的。

每一个wxEvtHandler的派生类,例如frame,按钮,菜单以及文档等,都会在其内部维护一个事件表,用来告诉wxWidgets事件和事件处理过程的对应关系。所有继承自wxWindow的窗口类,以及应用程序类都是wxEvtHandler的派生类.

要创建一个静态的事件表(意味着它是在编译期间创建的),你需要下面几个步骤:

  1. 定义一个直接或者间接继承自wxEvtHandler的类.
  2. 为每一个你想要处理的事件定义一个处理函数。
  3. 在这个类中使用DECLARE_EVENT_TABLE声明事件表。
  4. 在.cpp文件中使用使用BEGIN_EVENT_TABLE和END_EVENT_TABLE实现一个事件表。
  5. 在事件表的实现中增加事件宏,来实现从事件到事件处理过程的映射。

所有的事件处理函数拥有相同的形式。他们的返回值都是void,他们都不是虚函数,他们都只有一个事件对象作为参数。(如果你熟悉MFC,这可能会让你觉得轻松,因为在MFC中消息处理函数并没有一个统一的形式。)这个事件对象的类型是随这个处理函数要处理的事件的变化而变化的。例如简单控件(比如按钮)的命令处理函数和菜单命令的处理函数的参数都是wxCommandEvent类型,而size事件(这个事件通常是由用户改变窗口的客户区尺寸而引起的)处理函数的参数则是wxSizeEvent的类型。不同的事件参数类型可以调用的方法也不相同,通过这些方法,你可以获得事件产生的原因以及产生这个事件的控件的值的改变情况(比如,文本框中的值的改变)。当然最简单的情形是你完全不需要访问这个参数的任何方法,比如按钮点击事件。

让我们来扩展一下前一章中的例子,来增加一个窗口大小改变事件的处理和一个确定按钮的处理。下面是扩展以后的MyFrame的定义:

class MyFrame : public wxFrame
{
public:
    MyFrame(const wxString& title);
    void OnQuit(wxCommandEvent& event);
    void OnAbout(wxCommandEvent& event);
    void OnSize(wxSizeEvent& event);
    void OnButtonOK(wxCommandEvent& event);
private:
    DECLARE_EVENT_TABLE()
};

增加菜单项的代码和前一章的代码类似,而在frame窗口增加一个按钮的代码也只需要在MyFrame的构造函数中增加下面的代码:

wxButton* button = new wxButton(this, wxID_OK, wxT("OK"),
                                   wxPoint(200, 200));

类似的,在事件表中也需要相应的增加事件映射宏:

BEGIN_EVENT_TABLE(MyFrame, wxFrame)
    EVT_MENU     (wxID_ABOUT,     MyFrame::OnAbout)
    EVT_MENU     (wxID_EXIT,      MyFrame::OnQuit)
    EVT_SIZE     (                MyFrame::OnSize)
    EVT_BUTTON   (wxID_OK,        MyFrame::OnButtonOK)
END_EVENT_TABLE()

当用户点击关于菜单或者退出菜单的时候,这个事件被发送到frame窗口,而MyFrame的事件表告诉wxWidgets,对于标识符为 wxID_ABOUT的菜单事件应该发送到MyFrame::OnAbout函数,而标识符为wxID_EXIT的菜单事件应该发送到MyFrame:: OnQuit函数。换句话说:当事件处理循环处理这两个事件的时候,相应的函数将会以一个的wxCommandEvent类型的参数被调用。

EVT_SIZE事件映射宏不需要标识符参数因为这个事件只会被产生这个事件的控件所处理。

EVT_BUTTON这一行将导致当frame窗口及其子窗口中标识符为wxID_OK的按钮被点击的时候,OnButtonOK函数被调用。这个例子表明,事件可以不必被产生这个事件的控件所处理。让我们假定这个按钮是MyFrame的子窗口。当按钮被点击的时候,wxWidgets会首先检查wxButton类是否指定了相应的处理函数,如果找不到,则在其父亲的类所属的事件表中进行查找,在这个例子中,按钮的父亲是MyFrame类型的一个实例,在其事件表中指明了一个对应的处理函数,因此MyFrame::OnButtonOK函数就被调用了。类似的搜索过程不光发生在窗口控件的父子继承树中,也发生在普通的类继承关系中,这意味着你可以选择在哪里定义事件的处理函数。举例来说,如果你设计了一个对话框,这个对话框需要响应的类似标识符为wxID_OK的command事件。但是你可能需要把这个控件的创建工作留给使用你的代码的其他的程序员,只要他在创建这个控件的时候使用同样的标识符,你仍然可以给这个控件为这个事件定义默认的处理函数。

下图中演示了一次普通的按钮点击事件发生以后,wxWidgets搜索所有事件表的顺序。图中只演示了wxButton和 MyFrame两次继承关系。当用户点击了确认按钮的时候,一个新的wxCommandEvent事件被创建,其中包含标识符wxID_OK和事件类型 wxEVT_COMMAND_BUTTON_CLICKED,然后这个按钮的事件表开始通过wxEvtHandler::ProcessEvent函数进行匹配,事件表中的每一个条目都会去尝试匹配,然后是其父类wxControl的事件表,然后是wxWindow的。如果都没有匹配到, wxWidgets会搜索其父亲的类事件表,然后就找到了一条匹配条目:

EVT_BUTTON(wxID_OK,MyFrame::OnButtonOK)

因此MyFrame::OnButtonOK被调用了。

需要注意的事:只有Command事件(其事件类型直接或者间接的继承自wxCommandEvent)才会被递归的应用到其父亲的事件表。通常这是wxWidgets的用户经常会感到困惑的地方,因此我们把那些不会传递给其父亲的事件表的事件列举如下:wxActivate, wxCloseEvent, wxEraseEvent, wxFocusEvent, wxKeyEvent, wxIdleEvent, wxInitDialogEvent, wxJoystickEvent, wxMenuEvent, wxMouseEvent, wxMoveEvent, wxPaintEvent, wxQueryLayoutInfoEvent, wxSizeEvent, wxScrollWinEvent, 和wxSysColourChangedEvent,这些事件都不会传给事件源控件的父亲.

这些事件不会传递给其父亲,是因为这些事件仅对产生这个事件的窗口才有意义,举例来说,把一个子窗口的重绘事件发送给它的父亲,其实是没有任何意义的。