Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

OptionSheet, Part I - Using COptionSheet and CPropSheet

0.00/5 (No votes)
31 May 2004 4  
Flexible implementation of PropertyPages for WTL. Handles TreeCtrl or TabCtrl for the selection

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.

 
// OptionPageAbout.h

#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.

// 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() 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();  // Load the options from storage

  void Save();  // Save the options to the storage

};

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:

// 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;
};

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();  // Reload the saved options

    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:

 
// 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;

};

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:

// 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;
};

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.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here