OptionSheet, 第一部分 - 使用 COptionSheet 和 CPropSheet






4.71/5 (11投票s)
2004年5月27日
6分钟阅读

95314

2575
WTL 的 PropertyPages 的灵活实现。支持 TreeCtrl 或 TabCtrl 进行选择
引言
本系列文章将为您呈现一种易于使用且可扩展的选项页和选项页面的实现方式。与其他实现相比,它具有以下优点:
- 占用空间小。
- 优雅的面向对象设计。(不是吗?)
- 易于扩展
- 可以在同一个选项卡中,多次使用相同的选项页面(布局),但内容不同。
第一部分将仅介绍如何以最简单的形式使用 OptionSheet 系统,以及另一种接近 MFC CPropertySheet
行为的形式。
请注意,WTL 是此系统的必需框架。如果您使用的是 MFC,请不要离开,您可以非常轻松地将 WTL 集成到 MFC 中,而无需更改一行代码。有关详细信息,请参阅 Paolo Messina 的文章 "Mix up WTL with MFC" 的第一部分。
使用 COptionSheet 和 COptionPage
与其他大多数系统一样,它被分为两个类。一个类负责管理选项页面,另一个类将所有这些页面收集到一个选项卡中。
在第一种方法中,我们将使用对话框模板来布局选项卡。这听起来可能很奇怪,但它为您提供了随意放置控件的机会。您还可以添加由您自己的消息处理程序处理的特殊控件。当然,对话框模板可用于多个不同的选项卡。因此,通常您只会为您的应用程序甚至您的软件套件使用一个对话框模板。
如果您想要动态创建的选项卡,请阅读第二篇文章。但让我们从一个页面示例开始。
简单的 COptionPage
这是最简单的页面代码。该页面仅包含被动控件。
// OptionPageAbout.h #include "resource.h" #include <MOptionPage.h> class COptionPageAbout : public Mortimer::COptionPageImpl< COptionPageAbout > { public: enum { IDD = IDD_OPTIONPAGE_ABOUT }; DECLARE_EMPTY_MSG_MAP() };
您的项目必须包含一个对话框模板 IDD_OPTIONPAGE_ABOUT
。将其样式设置为 Child,边框设置为 Thin,并带有标题栏(以便有标题)。
简单的 COptionSheet
您想显示的每个选项卡都必须有自己的派生自 COptionSheet
的类。将页面添加到选项卡是在内部完成的,而不是像 CPropertySheet
那样在调用 DoModal()
之前调用 AddPage()
。实际上,页面的窗口创建留给您。再次,这听起来可能很奇怪,但它允许您添加尚未创建的页面,例如来自插件的页面。
这是专门的选项卡类,请注意 DoInit()
函数。
// MyOptionSheet.h #include < MOptionSheet.h > #include "OptionPageAbout.h" class CMyOptionSheet : public Mortimer::COptionSheet { public: bool DoInit(bool FirstTime) { if (FirstTime) { m_PageAbout.Create(this); m_PageGeneral.Create(this); } AddItem(new COptionItem(NULL, &m_PageAbout)); AddItem(new COptionItem(NULL, &m_PageGeneral)); return COptionSheet::DoInit(FirstTime); } protected: COptionPageAbout m_PageAbout; COptionPageGeneral m_PageGeneral; };
DoInit()
有两个部分,第一部分包含在 if 语句中。页面创建在那里完成。将选项卡的实例(通过 this 指针)传递给页面,以便它们知道它们的父级。
现在是第二个,调用 AddItem()
并传递 COptionItem
的实例。这是添加页面的系统。实际上,我们不是添加页面,而是根据名称所示,我们添加一个项。什么是项?在本例中,它退化为页面。COptionItem
的第一个 NULL
参数是项标题的字符串。传递 NULL
会告诉选项卡从页面对话框模板中获取标题。
不要忘记调用基类的 DoInit()
实现,就像您在代码中看到的那样,您必须在函数末尾调用它。标准实现将简单地激活第一个页面,如果您没有使用 COptionSheet::SetActiveItem()
激活一个页面。
现在,这是显示选项卡的_代码
COptionSheetDialogImpl< COptionSelectionTabCtrl, CMyOptionSheet > Sheet(IDD_MYOPTIONSHEET); Sheet.DoModal();
第一行通过声明一个对象来创建选项卡。让我们对此进行一些研究。
我们使用 COptionSheetDialogImpl
模板,这是一个 COptionSheet
的窗口实现,它处理从对话框资源模板创建。第一个参数是表示用于项选择的控件的类。目前提供了两个类:一个用于 TabCtrl,另一个用于 TreeCtrl。第二个参数是您派生自 COptionSheet
的类。最后要做的是将对话框 ID 作为构造函数参数传递(这里是 IDD_MYOPTIONSHEET
)。就是这样。
第二行简单地显示并处理选项卡。
好了,这个例子展示了一个整洁的选项卡,包含两个页面,但它根本不处理选项。让我们继续添加一些功能。
扩展的 COptionSheet 和 COptionPage
在处理选项对话框之前,我们必须有一些选项。所以这里有一个存储一些选项的方法。请注意,这里提出的系统是为示例创建的,在实际生活中,您要么有自己的系统,要么如果没有,请参阅我写的另一篇文章:"Settings Storage"。本文系列的最后一部分将讨论如何将 CSettingsStorage
与 COptionSheet
结合使用。
struct CGeneralOptions { CString Name; bool Enable; }; struct CAppOptions { CGeneralOptions Default; CGeneralOptions Current; void Load(); // Load the options from storage void Save(); // Save the options to the storage };
我们有一组用于默认设置的选项和另一组用于当前设置的选项。这将向您展示如何在同一个选项卡中使用相同的选项页面两次,但使用不同的选项。为了实现这一点,我们对 COptionItem
类进行子类化,以添加一个成员,该成员将是指向我们 CXXXOptions 对象的指针。这是它的定义
class CMyOptionItem : public COptionItem { public: CMyOptionItem(LPCTSTR Caption, COptionPage *pPage, void *pOptions) : COptionItem(Caption, pPage), m_pOptions(pOptions) {} void SetOptions(void *pOptions) { m_pOptions = pOptions; } void *GetOptions() { return m_pOptions; } protected: void *m_pOptions; };
这是我们处理新项的更新后的选项卡类
// MyOptionSheet.h #include < MOptionSheet.h > #include "OptionPageAbout.h" #include "OptionPageGeneral.h" class CMyOptionSheet : public Mortimer::COptionSheet { public: bool DoInit(bool FirstTime) { if (FirstTime) { m_PageAbout.Create(this); m_PageGeneral.Create(this); } AddItem(new COptionItem(NULL, &m_PageAbout)); AddItem(new CMyOptionItem("Default", &m_PageGeneral, m_pOptions->Default)); AddItem(new CMyOptionItem("Current", &m_PageGeneral, m_pOptions->Current)); return COptionSheet::DoInit(FirstTime); } protected: COptionPageAbout m_PageAbout; COptionPageGeneral m_PageGeneral; CAppOptions *m_pOptions; };
添加了两个项,它们使用相同的页面,但具有不同的标题和不同的选项指针。另请注意新的受保护成员,它是一个指向我们应用程序选项的指针。必须在调用 DoModal()
之前初始化它。
由于多个项可以共享同一个页面,因此选项的提交是在选项卡中完成的。为此,请实现 OnOK()
、OnCancel()
和 OnApply()
。
... class CMyOptionSheet : public Mortimer::COptionSheet { public: ... void OnOK(UINT uCode, int nID, HWND hWndCtl) { m_pOptions.Save(); COptionSheet::OnOK(uCode, nID, hWndCtl); } void OnApply(UINT uCode, int nID, HWND hWndCtl) { m_pOptions.Save(); COptionSheet::OnApply(uCode, nID, hWndCtl); } void OnCancel(UINT uCode, int nID, HWND hWndCtl) { m_pOptions.Load(); // Reload the saved options COptionSheet::OnCancel(uCode, nID, hWndCtl); } ...
让我们向我们的项目添加一个页面,为此创建一个对话框模板(IDD_OPTIONPAGE_GENERAL
),其中包含一个 Edit(IDC_EDIT
)和一个 checkbox(IDC_CHECK
)。要将页面移动到选项卡上的正确位置,请使用资源编辑器中的“Dialog Properties”中的 XPos 和 YPos 字段。
这是该类的代码
// OptionPageGeneral.h #include "resource.h" #include < MOptionPage.h > class COptionPageGeneral : public Mortimer::COptionPageImpl< COptionPageGeneral > { public: enum { IDD = IDD_OPTIONPAGE_GENERAL }; DECLARE_EMPTY_MSG_MAP() bool OnSetActive(COptionItem *pItem) { // cast the Item to our own CMyOptionItem *pMyItem = dynamic_cast< CMyOptionItem* >(pItem); if (pMyItem) { // Get the options pointer (void*) m_pOptions = (CGeneralOptions*)pMyItem->GetOptions(); if (m_pOptions) { // Okay, sets the control contents SetDlgItemText(IDC_EDIT, m_pOptions->Name); CheckDlgButton(IDC_CHECK, m_pOptions->Enable); } } return true; } bool OnKillActive(COptionItem *pItem) { // Do we have options to update ? if (m_pOptions) { // Yes, fetch the values from the controls GetDlgItemText(IDC_EDIT, m_pOptions->Name.GetBuffer(100), 100); m_pOptions->Name.ReleaseBuffer(); m_pOptions->Enable = IsDlgButtonChecked(IDC_CHECK); } return true; } protected: CGeneralOptions *m_pOptions; };
OnSetActive()
函数将在每次显示页面时调用,我们所做的是获取选项指针并更新对话框控件。我们还备份选项指针,以便稍后在 OnKillActive()
中使用它(可以通过在 OnKillActive()
中添加相同的代码来避免此操作)。
OnKillActive()
函数则相反,它在页面消失之前被调用。此时,我们从控件中获取值以更新选项。
就是这样。您现在拥有一个功能齐全的 OptionSheet 系统。请阅读下一节以了解另一种可能的行为。敬请期待第三部分,以获得更好的选项处理。
旧方法 - CPropSheet 和 CPropPage
如果您不想使用具有相同页面的多项功能,则可以回退到 CPropertySheet
的行为。我在 CPropSheet
和 CPropPage
中就是这样做的。而不是实现选项卡的 OnOK()
和 OnCancel()
,您将在每个页面上执行此操作。因此,选项的提交留给页面负责。当用户单击选项卡按钮时,所有页面都会自动被调用。
这是选项卡和页面代码
// MyPropSheet.h #include "OptionPageAbout.h" #include "PropPageGeneral.h" class CMyPropSheet : public Mortimer::CPropSheet { public: bool DoInit(bool FirstTime) { if (FirstTime) { m_PageAbout.Create(this); m_PageGeneral.Create(this); } AddItem(new COptionItem(NULL, &m_PageAbout)); AddItem(new COptionItem(NULL, &m_PageGeneral)); // Set the options pointer for the page m_PageGeneral.m_pOptions = &m_pOptions->Current; return CPropSheet::DoInit(FirstTime); } void SetOptions(CAppOptions *pOptions) { m_pOptions = pOptions; } protected: COptionPageAbout m_PageAbout; CPropPageGeneral m_PageGeneral; CAppOptions *m_pOptions; };
// PropPageGeneral.h #include "resource.h" #include <MOptionPage.h> class CPropPageGeneral : public Mortimer::COptionPageImpl< CPropPageGeneral, Mortimer::CPropPage > { public: enum { IDD = IDD_OPTIONPAGE_GENERAL }; DECLARE_EMPTY_MSG_MAP() bool OnSetActive(COptionItem *pItem) { // Sets the control contents SetDlgItemText(IDC_EDIT, m_pOptions->Name); CheckDlgButton(IDC_CHECK, m_pOptions->Enable); return true; } void OnOK() { // Retrieve values from the controls GetDlgItemText(IDC_EDIT, m_pOptions->Name.GetBuffer(100), 100); m_pOptions->Name.ReleaseBuffer(); m_pOptions->Enable = IsDlgButtonChecked(IDC_CHECK); } void OnCancel() { // Do nothing } CGeneralOptions *m_pOptions; };
选项卡代码与 COptionSheet
的代码几乎相同,只是它不包含 OnOK()
、OnCancel()
和 OnApply()
的代码。它还为页面设置了一个指向设置的指针。再说一遍,这取决于您的设置系统。
页面代码包含 3 个方法:OnSetActive()
,它将用设置加载控件。OnOK()
从控件更新设置。OnCancel()
什么也不做,因此控件的内容将被简单地丢弃。
要显示此选项卡,请使用与 COptionSheet
相同的代码。
注意
还有两篇文章尚未撰写,第二部分将专注于动态创建的选项卡(不再使用对话框模板作为选项卡),第三部分可能是“代码更少”方面更有趣的一篇,它将介绍一个扩展,允许使用 COptionSheet
和 CSettingsStorage
进行自动设置处理(请参阅 "Settings Storage")。
一如既往,欢迎随意使用和修改此软件。也欢迎提出建议和意见。