Introduction
This series of articles will present you a simple to use and expandable way of implementing
options sheets and pages. Here are some advantages over other implementations:
- Small footprint.
- Elegant OO design. (isn't it?)
- Easily expandable
- Can use the same option page (layout) with different options (content), several times in the
same sheet.
This first part will simply explain how to use the OptionSheet system in the
simplest form
and in another form near the behaviour of MFC CPropertySheet
.
Note that WTL is the required framework for this system. If you are MFC, don't go away, you
can integrate WTL very easily in MFC without changing a line of your code. For the details,
please refer to the first part of Paolo Messina's article "Mix up WTL with MFC".
Using COptionSheet with COptionPage
Like the majority of other systems out there, it is split in two classes. One that manages
options pages and another that collect all these pages in one sheet.
In this first approach we will use a dialog template for the sheet layout. This can sounds
strange
but gives you the opportunity to place the controls at your pleasure. You can also add special
controls that will be handled by your own message handlers.
The dialog template can of course be used for several different sheets. So generally you'll have
only one dialog template for your app or even for your software suite.
If you want dynamically created sheets, read the second article.
But, let's start with an example of a page.
Simple COptionPage
Here is the code for the simplest page. The page contains only passive controls.
#include "resource.h"
#include <MOptionPage.h>
class COptionPageAbout : public Mortimer::COptionPageImpl< COptionPageAbout >
{
public:
enum { IDD = IDD_OPTIONPAGE_ABOUT };
DECLARE_EMPTY_MSG_MAP()
};
Your project has to contain a dialog template IDD_OPTIONPAGE_ABOUT
. Make it with Style=Child,
Border=Thin and a titlebar (to have a caption).
Simple COptionSheet
Every sheet you want to display must have its own class deriving from COptionSheet
.
Adding pages to the sheet is done internally and not by calling AddPage()
before a
DoModal()
like in CPropertySheet
. In fact pages window creations are left to you. Once again it could
sounds strange but it let's you add pages that you have not created, coming from a plugin for
example.
Here is the specialized options sheet class, note the DoInit()
function.
#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()
has two sections, the first is enclosed within the if statement. Pages creations are
done there.
The instance of the sheet is passed (with the this pointer) to the pages, so that they know their
parent.
Now the second, AddItem()
is called passing an instance of COptionItem
. This is the system to add
pages.
In fact we do not add pages but like the name tells, we add an item. What's an item? In this example
it is
degenerated to a page. The first NULL
parameter of COptionItem
is the string for the item caption.
Passing
NULL
tells the sheet to get the caption from the page dialog template.
Do not forget to call the base class implementation of DoInit()
, and like you see it in the code
you have
to call it at the end of the function. The standard implementation will simply activate the first
page if
you didn't activate one yourself with COptionSheet::SetActiveItem()
.
And now here is the code to display the sheet:
COptionSheetDialogImpl< COptionSelectionTabCtrl,
CMyOptionSheet > Sheet(IDD_MYOPTIONSHEET);
Sheet.DoModal();
The first line creates the sheet by declaring an object. Let's investigate this a little bit.
We use the COptionSheetDialogImpl
template which is a window implementation for
COptionSheet
that
handles
creation from dialog resource template. The first argument is the class that represent the control
used
for item selection. Currently two classes are supplied: one for TabCtrl and the other for TreeCtrl.
The second argument is YOUR COptionSheet
derived class. The last thing to do is pass the dialog ID
as the constructor
parameter (here IDD_MYOPTIONSHEET
). That's it.
The second line simply shows and handles the sheet.
Well, this example shows a neat sheet with two pages but it doesn't handle options at all. Let's
move on to add
some functionality.
Extended COptionSheet and COptionPage
Before we can handle dialogs for options we must have some options. So here is a way to store
some options.
Note that the system presented here is created for the example, in real life you either have your
own system
or if not, see another article I wrote: "Settings Storage".
The last part of this article series will talk about tightening together CSettingsStorage
with
COptionSheet
.
struct CGeneralOptions
{
CString Name;
bool Enable;
};
struct CAppOptions
{
CGeneralOptions Default;
CGeneralOptions Current;
void Load();
void Save();
};
What we have is a set of options for default settings and another for the current settings.
This will show you the use of the same option page twice in the sheet but with different options.
For this to work we subclass the COptionItem
class to add a member which will be a pointer to
our CXXXOptions object. Here is its definition:
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;
};
Here is our updated sheet class that handles the new items:
#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;
};
Two items were added with the same page but with different captions and also different options
pointers.
Also note the new protected member which is a pointer to our app options. This one has to be
initialized
before we call DoModal()
.
Because several items can share the same page, committing the options is done in the sheet. For
that implement
OnOK()
, OnCancel()
and 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();
COptionSheet::OnCancel(uCode, nID, hWndCtl);
}
...
Let's add a page to our project, for that create a dialog template (IDD_OPTIONPAGE_GENERAL
) with
an Edit (IDC_EDIT
) and a checkbox (IDC_CHECK
).
To move the page at the right position on the sheet, use the XPos and YPos fields in the 'Dialog
Properties' from the resource editor.
Here is the code for this class:
#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)
{
CMyOptionItem *pMyItem = dynamic_cast< CMyOptionItem* >(pItem);
if (pMyItem)
{
m_pOptions = (CGeneralOptions*)pMyItem->GetOptions();
if (m_pOptions)
{
SetDlgItemText(IDC_EDIT, m_pOptions->Name);
CheckDlgButton(IDC_CHECK, m_pOptions->Enable);
}
}
return true;
}
bool OnKillActive(COptionItem *pItem)
{
if (m_pOptions)
{
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;
};
The OnSetActive()
function will be called each time the page is displayed, what we do is get the
options pointer and update the dialogs controls.
We also backup the options pointer to later use it in OnKillActive()
(this could be avoided by
adding the same code in OnKillActive()
).
The OnKillActive()
function is the opposite, it is called before the page disappears. At this
point we fetch the values from the controls to update
the options.
That's it. You now have a functional OptionSheet system. Read the next section for another
possible behaviour. Also look forward for Part III for
better options handling.
The old way - CPropSheet and CPropPage
If you don't want to use the multi-items with same page feature, you can fall back with the
behaviour of CPropertySheet
.
I've done that in CPropSheet
and CPropPage
.
Instead implementing the sheet OnOK()
and OnCancel()
, you will do that for each page you have. So
committing the options
are left to the page responsibility. When the user clicks on the sheet buttons, every page gets
called automatically.
Here is the code for the sheet and a page:
#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));
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;
};
#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)
{
SetDlgItemText(IDC_EDIT, m_pOptions->Name);
CheckDlgButton(IDC_CHECK, m_pOptions->Enable);
return true;
}
void OnOK()
{
GetDlgItemText(IDC_EDIT, m_pOptions->Name.GetBuffer(100), 100);
m_pOptions->Name.ReleaseBuffer();
m_pOptions->Enable = IsDlgButtonChecked(IDC_CHECK);
}
void OnCancel()
{
}
CGeneralOptions *m_pOptions;
};
The sheet code is nearly the same as for COptionSheet
except it does not contain code for
OnOK()
,
OnCancel()
and OnApply()
.
It also sets a pointer to the settings in the page. Once again this is dependant of your settings
system.
The page code contains 3 methods: OnSetActive()
which will load the controls with the settings.
OnOK()
updates the settings from the controls. OnCancel()
does nothing, so the controls contents are
simply lost.
To get this sheet displayed, use the same code as for COptionSheet
.
Note
Two more articles are yet to be written, part II will focus on dynamically created sheets (no
more dialog templates for the sheet),
part III can be the more interesting one from the "lesser code" point of view, it will present an
extension to allow
automatic settings handling with COptionSheet
and CSettingsStorage
(see "Settings Storage").
Like always, feel free to use and modify this piece of software. Suggestions and comments are
also welcomed.