12.7 编写自定义的控件

这一小节,我们来介绍一下怎样创建自定义的控件.实际上,wxWidgets并不具有象别的应用程序开发平台上的二进制的,支持鼠标拖入应用程序窗口的这种控件.第三方控件通常都和wxWidgets自带的控件比如wxCalendarCtrl和wxGrid一样,是通过源代码的方式提供的.我们这里用的 "控件"一词,含义是比较松散的,你不一定非要从"wxControl"进行派生,比如有时候,你可能更愿意从wxScrolledWindow产生派生类.

制作一个自定义的控件通常需要经过下面10个步骤:

  1. 编写类声明,它应该拥有一个默认构造函数,一个完整构造函数,一个Create函数用于两步创建, 最好还有一个Init函数用于初始化内部数据.
  2. 增加一个函数DoGetBestSize,这个函数应该根据内部控件的情况(比如标签尺寸)返回该控件最合适的大小.
  3. 如果已有的事件类不能满足需要,为你的控件增加新的事件类. 比如对于内部的一个按钮被按下的事件,可能使用已有的wxCommandEvent就可以了,但是更复杂的控件需要更复杂的事件类.并且如果你增加了新的事件类,也应改增加相应的事件映射宏.
  4. 编写代码在你的新控件上显示信息.
  5. 编写底层鼠标和键盘控制代码,并在其处理函数中产生你自定义的新的事件,以便应用程序可以作出相应处理.
  6. 编写默认事件处理函数,以便控件可以处理那些标准事件(比如处理wxID_COPY或wxID_UNDO等标准命令的事件)或者默认的用户界面更新事件.
  7. 可选的增加一个验证器类,以便应用程序可以用它使得数据和控件之间的传输变得容易,并且增加数据校验功能.
  8. 可选的增加一个资源处理类,以便可以在XRC文件中使用你自定义的控件.
  9. 在你准备使用的所有平台上测试你的自定义控件.
  10. 编写文档

我们来举一个简单的例子,这个例子我们在第三章"事件处理"中曾经使用过,当时我们用来讨论自定义的事件: wxFontSelectorCtrl,你可以在随书光盘的examples/chap03中找到这个例子.这个类提供了一个字体预览窗口,当用户在这个窗口上点击鼠标的时候会弹出一个标准的字体选择窗口用于更改当前字体.这将导致一个新的事件wxFontSelectorCtrlEvent,应用程序可以使用EVT_FONT_SELECTION_CHANGED(id, func)宏来拦截这个事件.

这个控件的运行效果如下图所示,下图中静态文本下方即为我们自定义的控件.

自定义控件的类声明

下面的代码展示了自定义控件wxFontSelectorCtrl的类声明.其中DoGetBestSize只简单的返回固定值200x40象素,如果构造函数中没有指定大小,我们会默认使用这个大小.

/*!
 * 一个显示显示字体预览的自定义控件.
 */
class wxFontSelectorCtrl: public wxControl
{
    DECLARE_DYNAMIC_CLASS(wxFontSelectorCtrl)
    DECLARE_EVENT_TABLE()
public:
    // 默认构造函数
    wxFontSelectorCtrl() { Init(); }
    wxFontSelectorCtrl(wxWindow* parent, wxWindowID id,
        const wxPoint& pos = wxDefaultPosition,
        const wxSize& size = wxDefaultSize,
        long style = wxSUNKEN_BORDER,
        const wxValidator& validator = wxDefaultValidator)
    {
        Init();
        Create(parent, id, pos, size, style, validator);
    }
    // Create函数
    bool Create(wxWindow* parent, wxWindowID id,
        const wxPoint& pos = wxDefaultPosition,
        const wxSize& size = wxDefaultSize,
        long style = wxSUNKEN_BORDER,
        const wxValidator& validator = wxDefaultValidator);
    // 通用初始化函数
    void Init() { m_sampleText = wxT("abcdeABCDE"); }
    // 重载函数
    wxSize DoGetBestSize() const { return wxSize(200, 40); }
    // 事件处理函数
    void OnPaint(wxPaintEvent& event);
    void OnMouseEvent(wxMouseEvent& event);
    // 操作函数
    void SetFontData(const wxFontData& fontData) { m_fontData = fontData; };
    const wxFontData& GetFontData() const { return m_fontData; };
    wxFontData& GetFontData() { return m_fontData; };
    void SetSampleText(const wxString& sample);
    const wxString& GetSampleText() const { return m_sampleText; };
protected:
    wxFontData  m_fontData;
    wxString    m_sampleText;
};

象wxFontDialog中的那样,我们使用wxFontData类型来保存字体数据,这个类型可以额外的保存字体颜色信息.

这个控件的RTTI(运行期类型标识)事件表和创建函数的代码列举如下:

BEGIN_EVENT_TABLE(wxFontSelectorCtrl, wxControl)
    EVT_PAINT(wxFontSelectorCtrl::OnPaint)
    EVT_MOUSE_EVENTS(wxFontSelectorCtrl::OnMouseEvent)
END_EVENT_TABLE()
IMPLEMENT_DYNAMIC_CLASS(wxFontSelectorCtrl, wxControl)
bool wxFontSelectorCtrl::Create(wxWindow* parent, wxWindowID id,
             const wxPoint& pos, const wxSize& size, long style,
             const wxValidator& validator)
{
    if (!wxControl::Create(parent, id, pos, size, style, validator))
        return false;
    SetBackgroundColour(wxSystemSettings::GetColour(
                                          wxSYS_COLOUR_WINDOW));
    m_fontData.SetInitialFont(GetFont());
    m_fontData.SetChosenFont(GetFont());
    m_fontData.SetColour(GetForegroundColour());
    // 这个函数告诉相应的布局控件使用指定的最佳大小.
    SetBestFittingSize(size);
    return true;
}

对于函数SetBestFittingSize的调用告诉布局控件或者使用构造函数中指定的大小,或者使用DoGetBestSize函数返回的大小作为这个控件的最小尺寸.当控件被增加到一个布局控件中时,根据增加函数的参数不同,这个控件的尺寸有可能被放大.

增加DoGetBestSize函数

实现DoGetBestSize函数的目的是为了让wxWidgets可以以此作为控件的最小尺寸以便更好的布局.如果你提供了这个函数,用户就可以在创建控件的时候使用默认的大小(wxDefaultSize)以便控件自己决定自己的大小.在这里我们只是选择一个固定值200x40象素,虽然是固定的,但是是合理的.当然,应用程序可以通过在构造函数中传递不同的大小来覆盖它.类似按钮或者标签这样的控件,我们可以提供一个合理的大小,当然,你的控件也可能不能够决定自己的大小,比如一个没有子窗口的滚动窗口通常无法知道自己最合适的大小,在这种情况下,你可以不理会 wxWindow::DoGetBestSize.在这种情况下,你的控件大小将取决于用户在构造函数中指定的非默认大小或者应用程序需要通过一个布局控件来自动觉得你的控件的大小.

如果你的控件包含拥有可感知大小的子窗口,你可以通过所有子窗口的大小来决定你自己控件的大小,子窗口的合适大小可以通过 GetAdjustedBestSize函数获得.比如如果你的控件包含水平平铺的两个子窗口,你可以用下面的代码来实现DoGetBestSize函数:

wxSize ContainerCtrl::DoGetBestSize() const
{
    // 获取子窗口的最佳大小
    wxSize size1, size2;
    if ( m_windowOne )
        size1 = m_windowOne->GetAdjustedBestSize();
    if ( m_windowTwo )
        size2 = m_windowTwo->GetAdjustedBestSize();
    // 因为子窗口是水平平铺的,因此
    // 通过下面的方法计算控件的最佳大小.   
    wxSize bestSize;
    bestSize.x = size1.x + size2.x;
    bestSize.y = wxMax(size1.y, size2.y);
    return bestSize;
}

定义一个新的事件类

我们在第三章中详细介绍了怎样创建一个新的事件类(wxFontSelectorCtrlEvent)及其事件映射宏 (EVT_FONT_SELECTION_CHANGED).使用这个控件的应用程序可能并不需要拦截这个事件,因为可以直接使用数据传送机制会更方便. 不过在一个更复杂的例子中,事件类可以提供特别的函数,以便应用程序的事件处理函数可以从事件中获取更有用的信息,比如在这个例子中,我们可以增加 wxFontSelectorCtrlEvent::GetFont函数以返回用户当前选择的字体.

显示控件信息

我们的自定义控件使用一个简单的重绘函数显示控件信息(一个居中放置的使用指定字体的文本),如下所示:

void wxFontSelectorCtrl::OnPaint(wxPaintEvent& event)
{
    wxPaintDC dc(this);
    wxRect rect = GetClientRect();
    int topMargin = 2;
    int leftMargin = 2;
    dc.SetFont(m_fontData.GetChosenFont());
    wxCoord width, height;
    dc.GetTextExtent(m_sampleText, & width, & height);
    int x = wxMax(leftMargin, ((rect.GetWidth() - width) / 2)) ;
    int y = wxMax(topMargin, ((rect.GetHeight() - height) / 2)) ;
    dc.SetBackgroundMode(wxTRANSPARENT);
    dc.SetTextForeground(m_fontData.GetColour());
    dc.DrawText(m_sampleText, x, y);
    dc.SetFont(wxNullFont);
}

如果你需要绘制标准元素,比如分割窗口的分割条或者一个边框,你可以考虑使用wxNativeRenderer类(更多信息请参考使用手册).

处理输入

我们的控件会拦截左键单击事件来显示一个字体对话框,如果用户选择了新的字体,则一个新的事件会使用ProcessEvent函数发送给这个控件.这个事件可以被wxFontSelectorCtrl的派生类处理,也可以被包含我们自定义控件的对话框或者窗口处理.

void wxFontSelectorCtrl::OnMouseEvent(wxMouseEvent& event)
{
    if (event.LeftDown())
    {
        // 获取父窗口
        wxWindow* parent = GetParent();
        while (parent != NULL &&
               !parent->IsKindOf(CLASSINFO(wxDialog)) &&
               !parent->IsKindOf(CLASSINFO(wxFrame)))
            parent = parent->GetParent();
        wxFontDialog dialog(parent, m_fontData);
        dialog.SetTitle(_("Please choose a font"));
        if (dialog.ShowModal() == wxID_OK)
        {
            m_fontData = dialog.GetFontData();
            m_fontData.SetInitialFont(
                          dialog.GetFontData().GetChosenFont());
            Refresh();
            wxFontSelectorCtrlEvent event(
                  wxEVT_COMMAND_FONT_SELECTION_CHANGED, GetId());
            event.SetEventObject(this);
            GetEventHandler()->ProcessEvent(event);
        }
    }
}

这个控件没有拦截键盘事件,不过你还是可以通过拦截它们来实现和左键单击相同的动作.你也可以在你的控件上绘制一个虚线框来表明是否其当前拥有焦点,这可以通过wxWindow::FindFocus函数判断,如果你决定这样作,你就需要拦截EVT_SET_FOCUS和 EVT_KILL_FOCUS事件来在合式的时候进行窗口刷新.

定义默认事件处理函数

如果你看过了wxTextCtrl的实现代码,比如src/msw/textctrl.cpp中的代码,你就会发现一些标准的标识符比如 wxID_COPY,wxID_PASTE, wxID_UNDO和wxID_REDO以及用户界面更新事件都有默认的处理函数.这意味着如果你的应用程序设置了将事件首先交给活动控件处理(参考第 20章:"优化你的程序"),这些标准菜单项事件或者工具按钮事件将会拥有自动处理这些事件的能力.当然我们自定义的控件还没有复杂到这种程度,不过你还是可以通过这种机制实现撤消/重做操作以及剪贴板相关操作.我们来看看wxTextCtrl的例子:

BEGIN_EVENT_TABLE(wxTextCtrl, wxControl)
    ...
    EVT_MENU(wxID_COPY, wxTextCtrl::OnCopy)
    EVT_MENU(wxID_PASTE, wxTextCtrl::OnPaste)
    EVT_MENU(wxID_SELECTALL, wxTextCtrl::OnSelectAll)

    EVT_UPDATE_UI(wxID_COPY, wxTextCtrl::OnUpdateCopy)
    EVT_UPDATE_UI(wxID_PASTE, wxTextCtrl::OnUpdatePaste)
    EVT_UPDATE_UI(wxID_SELECTALL, wxTextCtrl::OnUpdateSelectAll)
    ...
END_EVENT_TABLE()
void wxTextCtrl::OnCopy(wxCommandEvent& event)
{
    Copy();
}
void wxTextCtrl::OnPaste(wxCommandEvent& event)
{
    Paste();
}
void wxTextCtrl::OnSelectAll(wxCommandEvent& event)
{
    SetSelection(-1, -1);
}
void wxTextCtrl::OnUpdateCopy(wxUpdateUIEvent& event)
{
    event.Enable( CanCopy() );
}
void wxTextCtrl::OnUpdatePaste(wxUpdateUIEvent& event)
{
    event.Enable( CanPaste() );
}
void wxTextCtrl::OnUpdateSelectAll(wxUpdateUIEvent& event)
{
    event.Enable( GetLastPosition() > 0 );
}

实现验证器

正如我们在第9章,"创建自定义的对话框"中看到的那样,验证器是数据在变量和控件之间传输的一种很方便地手段.当你创建自定义控件的时候,你可以考虑创建一个特殊的验证器以便应用程序可以使用它来和你的控件进行数据传输.

wxFontSelectorValidator是我们为wxFontSelectorCtrl控件事件的验证器,你可以将其和一个字体指针和颜色指针或者一个wxFontData对象绑定.这些变量通常是在对话框的成员变量中声明的,以便对话框持续跟踪控件改变并且在对话框被关闭以后可以通过其成员返回相应的值.注意验证器的使用方式,不需要使用new函数创建验证器,SetValidator函数将创建一份验证器的拷贝并且在需要的时候自动释放它.如下所示:

wxFontSelectorCtrl* fontCtrl =
   new wxFontSelectorCtrl( this, ID_FONTCTRL,
               wxDefaultPosition, wxSize(100, 40), wxSIMPLE_BORDER );
// 或者使用字体指针和颜色指针作为参数
fontCtrl->SetValidator( wxFontSelectorValidator(& m_font,
                                                & m_fontColor) );
// ...或者使用wxFontData指针作为参数
fontCtrl->SetValidator( wxFontSelectorValidator(& m_fontData) );

m_font和m_fontColor变量(或者m_fontData变量)将反应用户对字体预览控件所做的任何改变.数据传输在控件所在的对话框的transferDataFromWindow函数被调用的时候发生(这个函数将被默认的wxID_OK处理函数调用).

实现验证器必须的函数包括默认构造函数,带参数的构造函数,一个Clone函数用于复制自己.Validate函数用于校验数据并在数据非法的时候显示相关信息.transferToWindow和transferFromWindow则实现具体的数据传输.

下面列出了wxFontSelectorValidator的声明:

/*!
 * wxFontSelectorCtrl验证器
 */
class wxFontSelectorValidator: public wxValidator
{
DECLARE_DYNAMIC_CLASS(wxFontSelectorValidator)
public:
    // 构造函数
    wxFontSelectorValidator(wxFontData *val = NULL);
    wxFontSelectorValidator(wxFont *fontVal,
                            wxColour* colourVal = NULL);
    wxFontSelectorValidator(const wxFontSelectorValidator& val);
    // 析构函数
    ~wxFontSelectorValidator();
    // 复制自己
    virtual wxObject *Clone() const
    { return new wxFontSelectorValidator(*this); }
    // 拷贝数据到变量
    bool Copy(const wxFontSelectorValidator& val);
    // 在需要校验的时候被调用
    // 此函数应该弹出错误信息
    virtual bool Validate(wxWindow *parent);
    // 传输数据到窗口
    virtual bool TransferToWindow();
    // 传输数据到变量
    virtual bool TransferFromWindow();
    wxFontData* GetFontData() { return m_fontDataValue; }
DECLARE_EVENT_TABLE()
protected:
    wxFontData*     m_fontDataValue;
    wxFont*         m_fontValue;
    wxColour*       m_colourValue;
    // 检测是否验证器已经被正确设置
    bool CheckValidator() const;
};

建议阅读fontctrl.cpp文件中相关的函数实现以便对齐有进一步的了解.

实现资源处理器

如果你希望你自定义的控件可以在XRC文件中使用,你可以提供一个对应的资源处理器.我们在这里不对此作过多的介绍,请参考第9章,介绍XRC体系时候的相关介绍.同时也可以参考wxWidgets发行目录include/wx/xrc和src/xrc中已经的实现.

你的资源处理器在应用程序中登记以后,XRC文件可以包含你的自定义控件了,它们也可以被你的应用程序自动加载.不过如果制作这个XRC 文件也称为一个麻烦事.因为通常对话框设计工具都不支持动态加载自定义控件.不过通过DialogBlocks的简单的自定义控件定义机制,你可以指定你的自定义控件的名称和属性,以便生成正确的XRC文件,虽然,DialogBlocks只能作到在其设计界面上显示一个近似的图形以代替你的自定义控件.

检测控件显示效果

当创建自定义控件的时候,你需要给wxWidgets一些关于控件外观的小提示.记住,wxWidgets总会尽可能的使用系统默认的颜色和字体,不过也允许应用程序或者自定义控件在平台允许的时候自己决定这些属性.wxWidgets也会让应用程序或者控件自己决定是否这些属性应该被其子对象继承.控制这些属性的体系确实有一些复杂,不过,除非要大量定制控件的颜色(这是不推荐的)或者实现完全属于自己的控件,程序员很少需要关心这些细节.

除非显式指明,应用程序通常会允许子窗口(其中可能包含你自定义的控件)继承它们父窗口的前景颜色和字体.然后这种行为可以通过使用 SetOwnFont函数设置字体或者使用SetOwnForegroundColour函数设置前景颜色来改变.你的控件也可以通过调用 ShouldInheritColours函数来决定是否要继承父窗口的颜色(wxControl默认需要,而wxWindow则默认不需要).背景颜色通常不需要显式继承,你的控件应该通过不绘制不需要的区域的方法来保持和它的父窗口一致的背景.

为了实现属性继承,你的控件应该在构造函数中调用InheritAttributes函数,依平台的不同,这个函数通常可以在构造函数调用wxControl::Create函数的时候被调用.

某些类型实现了静态函数GetClassDefaultAttributes,这个函数返回一个wxVisualAttributes对象,包含前景色,背景色以及字体设定.这个函数包含一个只有在Mac OS X平台上才有意义的参数wxWindowVariant.这个函数指定的相关属性被用在类似GetBackgroundColour这样的函数中作为应用未指定值时候的返回值.如果你不希望默认的值被返回,你可以在你的控件中重新实现这个函数.同时你也需要重载GetDefaultAttributes虚函数,在其中调用GetClassDefaultAttributes函数以便允许针对某个特定的对象返回默认属性.如果你的控件包含一个标准控件的类似属性,你可以直接使用它,如下所示:

// 静态函数,用于全局访问
static wxVisualAttributes GetClassDefaultAttributes(
                wxWindowVariant variant = wxWINDOW_VARIANT_NORMAL)
{
    return wxListBox::GetClassDefaultAttributes(variant);
}
// 虚函数,用户对象返回访问
virtual wxVisualAttributes GetDefaultAttributes() const
{
    return GetClassDefaultAttributes(GetWindowVariant());
}

wxVisualAttributes的结构定义如下:

struct wxVisualAttributes
{
    // 内部标签或者文本使用的字体
    wxFont font;
    // 前景色
    wxColour colFg;
    // 背景色; 背景不为纯色背景时可能为wxNullColour
    wxColour colBg;
};

如果你的自定义控件使用了透明背景,比如说,它是一个类似静态文本标签的控件,你应该提供一个函数HasTransparentBackground以便wxWidgets了解这个情况(目前仅支持windows).

最后需要说明的是,如果某些操作需要在某些属性已经确定或者需要在最后的步骤才可以运行.你可以使用空闲时间来作这种处理,在第17章,"多线程编程"的"多线程替代方案"中对此有进一步的描述.

一个更复杂一点的例子:wxThumbnailCtrl

前面我们演示的例子wxFontSelectorCtrl是一个非常简单的例子,很方便我们逐一介绍自定义控件的一些基本原则,比如事件,验证器等.然后,对于显示以及处理输入方面则显得有些不足.在随书光盘的examples/chap12/thumbnail目录中演示了一个更复杂的例子wxThumbnailCtrl,这个控件用来显示缩略图.你可以在任何应用程序中使用它来显示图片的缩略图.(事实上它也可以显示一些别的缩略图,你可以实现自己的wxThumbnailItem的派生类以便使其可以支持显示某种文件类型的缩略图,比如显示那些包含图片的文档中的缩略图).

下图演示的wxThumbnailBrowserDialog对话框使用了wxGenericDirCtrl控件,这个控件使用了wxThumbnailCtrl控件.出于演示的目的,正在显示的这个目录放置了一些图片.

这个控件演示了下面的一些主题,当然列出的并不是全部:

  • 鼠标输入: 缩略图子项可以通过单击鼠标左键进行选择或者通过按着Ctrl键单击鼠标左键进行多选.
  • 键盘输入: 缩略图网格可以通过键盘导航,也可以通过方向键翻页,子项可以通过按住Shift键进行选择.
  • 焦点处理: 设置和丢失焦点将导致当前的活动缩略图的显示被更新.
  • 优化绘图: 通过wxBufferedPaintDC以及仅更新需要更新的区域的方法实现无闪烁更新当前显示.
  • 滚动窗口: 这个控件继承自wxScrolledWindow窗口,可以根据子项的数目自动调整滚动条.
  • 自定义事件: 在选择,去选择以及右键单击的时候产生wxThumbnailEvent类型的事件.

为了灵活处理,wxThumbnailCtrl并不会自动加载一个目录中所有的图片,而是需要通过下面的代码显式的增加wxThumbnailItem对象.如下所示:

// 创建一个多选的缩略图控件
wxThumbnailCtrl* imageBrowser =
   new wxThumbnailCtrl(parent, wxID_ANY,
         wxDefaultPosition, wxSize(300, 400),
         wxSUNKEN_BORDER|wxHSCROLL|wxVSCROLL|wxTH_TEXT_LABEL|
         wxTH_IMAGE_LABEL|wxTH_EXTENSION_LABEL|wxTH_MULTIPLE_SELECT);
// 设置一个漂亮的大的缩略图大小
imageBrowser->SetThumbnailImageSize(wxSize(200, 200));
// 在填充的时候不要显示
imageBrowser->Freeze();
// 设置一些高对比的颜色
imageBrowser->SetUnselectedThumbnailBackgroundColour(*wxRED);
imageBrowser->SetSelectedThumbnailBackgroundColour(*wxGREEN);
// 从'path'路径查找图片并且增加
wxDir dir;
if (dir.Open(path))
{
    wxString filename;
    bool cont = dir.GetFirst(&filename, wxT("*.*"), wxDIR_FILES);
    while ( cont )
    {
        wxString file = path + wxFILE_SEP_PATH + filename;
        if (wxFileExists(file) && DetermineImageType(file) != -1)
        {
            imageBrowser->Append(new wxImageThumbnailItem(file));
        }
        cont = dir.GetNext(&filename);
    }
}
// 按照名称排序
imageBrowser->Sort(wxTHUMBNAIL_SORT_NAME_DOWN);
// 标记和选择第一个缩略图
imageBrowser->Tag(0);
imageBrowser->Select(0);
// 删除第二个缩略图
imageBrowser->Delete(1);
// 显示图片
imageBrowser->Thaw();

如果你完整的阅读thumbnailctrl.h和thumbnail.cpp中的代码,你一定会得到关于自定义控件的足够的知识.另外,你也可以在你的应用程序中直接使用wxThumbnailCtrl控件,不要客气.