The EX13A Example Revisited

Now we'll add a property sheet to EX13A that allows the user to change the rich edit control's font characteristics. Of course, we could have used the standard MFC CFontDialog function, but then you wouldn't have learned how to create property sheets. Figure 13-5 shows the property sheet that you'll build as you continue with EX13A.

Figure 13-5. The property sheet from EX13A.

If you haven't built EX13A, follow the instructions that begin under the EX13A Example to build it. If you already have EX13A working with the Transfer menu commands, just continue on with these steps:

  1. Use the resource editor to edit the application's main menu. Click on the ResourceView tab in the Workspace window. Edit the IDR_MAINFRAME menu resource to add a Format menu that looks like this.

    Use the following command IDs for the new Format menu items.
    CaptionCommand ID
    &DefaultID_FORMAT_DEFAULT
    &SelectionID_FORMAT_SELECTION

    Add appropriate prompt strings for the two menu items.

  1. Use ClassWizard to add the view class command and update command UI message handlers. Select the CEx13aView class, and then add the following member functions.

    Object IDMessageMember Function
    ID_FORMAT_DEFAULTCOMMANDOnFormatDefault
    ID_FORMAT_SELECTIONCOMMANDOnFormatSelection
    ID_FORMAT_SELECTIONUPDATE_COMMAND_UI OnUpdateFormatSelection

  1. Use the resource editor to add four property page dialog templates. The templates are shown here with their associated IDs.

    Click to view at full size.

    Use the IDs in the table below for the controls in the dialogs. Set the Auto Buddy and the Set Buddy Integer properties for the spin button control, and set the Group property for the IDC_FONT and IDC_COLOR radio buttons. Set the minimum value of IDC_FONTSIZE to 8 and its maximum value to 24.

    Use ClassWizard to create the classes CPage1, CPage2, CPage3, and CPage4. In each case, select CPropertyPage as the base class. Click the Change button in ClassWizard's New Class dialog to generate the code for all these classes in the files Property.h and Property.cpp. Then add the data members shown here.

    DialogControlIDTypeData Member
    IDD_PAGE1First radio button IDC_FONTintm_nFont
    IDD_PAGE2Bold check box IDC_BOLDBOOLm_bBold
    IDD_PAGE2Italic check box IDC_ITALICBOOLm_bItalic
    IDD_PAGE2Underline check box IDC_UNDERLINEBOOLm_bUnderline
    IDD_PAGE3First radio button IDC_COLORintm_nColor
    IDD_PAGE4Edit control IDC_FONTSIZEintm_nFontSize
    IDD_PAGE4Spin button controlIDC_SPIN1  

    Finally, use ClassWizard to add an OnInitDialog message handler function for CPage4.

  1. Use ClassWizard to create a class derived from CPropertySheet. Choose the name CFontSheet. Generate the code in the files Property.h and Property.cpp, the same files you used for the property page classes. Figure 13-6 shows these files with the added code in boldface.

    PROPERTY.H

    #if !defined(AFX_PROPERTY_H__CD702F99_7495_11D0_8FDC_00C04FC2A0C2__INCLUDED_)
    #define AFX_PROPERTY_H_ _CD702F99_7495_11D0_8FDC_00C04FC2A0C2__INCLUDED_
    
    #if _MSC_VER > 1000
    #pragma once
    #endif // _MSC_VER > 1000
    // Property.h : header file
    //
    
    #define WM_USERAPPLY WM_USER + 5
    extern CView* g_pView;
    
    ////////////////////////////////////////////////////////////////////
    // CPage1 dialog
    
    class CPage1 : public CPropertyPage
    {
        DECLARE_DYNCREATE(CPage1)
    
    // Construction
    public:
        CPage1();
        ~CPage1();
    
    // Dialog Data
        //{{AFX_DATA(CPage1)
        enum { IDD = IDD_PAGE1 };
        int     m_nFont;
        //}}AFX_DATA
    
    
    // Overrides
        // ClassWizard generate virtual function overrides
        //{{AFX_VIRTUAL(CPage1)
        protected:
        virtual void DoDataExchange(CDataExchange* pDX); // DDX/DDV
                                                          // support
        //}}AFX_VIRTUAL
        virtual BOOL OnApply();
        virtual BOOL OnCommand(WPARAM wParam, LPARAM lParam);
    
    // Implementation
    protected:
        // Generated message map functions
        //{{AFX_MSG(CPage1)
            // NOTE: the ClassWizard will add member functions here
        //}}AFX_MSG
        DECLARE_MESSAGE_MAP()
    
    };
    
    ////////////////////////////////////////////////////////////////////
    // CPage2 dialog
    
    class CPage2 : public CPropertyPage
    {
        DECLARE_DYNCREATE(CPage2)
    
    // Construction
    public:
        CPage2();
        ~CPage2();
    
    // Dialog Data
        //{{AFX_DATA(CPage2)
        enum { IDD = IDD_PAGE2 };
        BOOL    m_bBold;
        BOOL    m_bItalic;
        BOOL    m_bUnderline;
        //}}AFX_DATA
    
    
    // Overrides
        // ClassWizard generate virtual function overrides
        //{{AFX_VIRTUAL(CPage2)
        protected:
        virtual void DoDataExchange(CDataExchange* pDX); // DDX/DDV
                                                          // support
        //}}AFX_VIRTUAL
        virtual BOOL OnCommand(WPARAM wParam, LPARAM lParam);
    
    // Implementation
    protected:
        // Generated message map functions
        //{{AFX_MSG(CPage2)
            // NOTE: the ClassWizard will add member functions here
        //}}AFX_MSG
        DECLARE_MESSAGE_MAP()
    
    };
    
    ////////////////////////////////////////////////////////////////////
    // CPage3 dialog
    
    class CPage3 : public CPropertyPage
    {
        DECLARE_DYNCREATE(CPage3)
    
    // Construction
    public:
        CPage3();
        ~CPage3();
    
    // Dialog Data
        //{{AFX_DATA(CPage3)
        enum { IDD = IDD_PAGE3 };
        int     m_nColor;
        //}}AFX_DATA
    
    
    // Overrides
        // ClassWizard generate virtual function overrides
        //{{AFX_VIRTUAL(CPage3)
        protected:
        virtual void DoDataExchange(CDataExchange* pDX); // DDX/DDV
                                                          // support
        //}}AFX_VIRTUAL
        virtual BOOL OnCommand(WPARAM wParam, LPARAM lParam);
    
    // Implementation
    protected:
        // Generated message map functions
        //{{AFX_MSG(CPage3)
            // NOTE: the ClassWizard will add member functions here
        //}}AFX_MSG
        DECLARE_MESSAGE_MAP()
    
    };
    
    ////////////////////////////////////////////////////////////////////
    // CPage4 dialog
    
    class CPage4 : public CPropertyPage
    {
        DECLARE_DYNCREATE(CPage4)
    
    // Construction
    public:
        CPage4();
        ~CPage4();
    
    // Dialog Data
        //{{AFX_DATA(CPage4)
        enum { IDD = IDD_PAGE4 };
        int     m_nFontSize;
        //}}AFX_DATA
    
    // Overrides
        // ClassWizard generate virtual function overrides
        //{{AFX_VIRTUAL(CPage4)
        protected:
        virtual void DoDataExchange(CDataExchange* pDX); // DDX/DDV
                                                         // support
        //}}AFX_VIRTUAL
        virtual BOOL OnCommand(WPARAM wParam, LPARAM lParam);
    
    // Implementation
    protected:
        // Generated message map functions
        //{{AFX_MSG(CPage4)
        virtual BOOL OnInitDialog();
        //}}AFX_MSG
        DECLARE_MESSAGE_MAP()
    
    };
    
    ////////////////////////////////////////////////////////////////////
    // CFontSheet
    
    class CFontSheet : public CPropertySheet
    {
        DECLARE_DYNAMIC(CFontSheet)
    
    public:
        CPage1 m_page1;
        CPage2 m_page2;
        CPage3 m_page3;
        CPage4 m_page4;
    
    // Construction
    public:
        CFontSheet(UINT nIDCaption, CWnd* pParentWnd = NULL,
                   UINT iSelectPage = 0);
        CFontSheet(LPCTSTR pszCaption, CWnd* pParentWnd = NULL,
                   UINT iSelectPage = 0);
    
    // Attributes
    public:
    
    // Operations
    public:
    // Overrides
        // ClassWizard generated virtual function overrides
        //{{AFX_VIRTUAL(CFontSheet)
        //}}AFX_VIRTUAL
    
    // Implementation
    public:
        virtual ~CFontSheet();
    
        // Generated message map functions
    protected:
        //{{AFX_MSG(CFontSheet)
            // NOTE - the ClassWizard will add and remove member functions here.
        //}}AFX_MSG
        DECLARE_MESSAGE_MAP()
    };
    
    ////////////////////////////////////////////////////////////////////
    //{{AFX_INSERT_LOCATION}}
    // Microsoft Visual C++ will insert additional declarations
    //  immediately before the previous line.
    
    #endif // !defined(AFX_PROPERTY_H_ _CD702F99_7495_11D0_8FDC_00C04FC2A0C2__INCLUDED_)
    

    PROPERTY.CPP

    // Property.cpp : implementation file
    //
    
    #include "stdafx.h"
    #include "ex13a.h"
    #include "Property.h"
    
    #ifdef _DEBUG
    #define new DEBUG_NEW
    #undef THIS_FILE
    static char THIS_FILE[] = __FILE__;
    #endif
    
    CView* g_pView;
    
    ////////////////////////////////////////////////////////////////////
    // CPage1 property page
    
    IMPLEMENT_DYNCREATE(CPage1, CPropertyPage)
    
    CPage1::CPage1() : CPropertyPage(CPage1::IDD)
    {
        //{{AFX_DATA_INIT(CPage1)
        m_nFont = -1;
        //}}AFX_DATA_INIT
    }
    
    CPage1::~CPage1()
    {
    }
    
    BOOL CPage1::OnApply()
    {
        TRACE("CPage1::OnApply\n");
        g_pView->SendMessage(WM_USERAPPLY);
        return TRUE;
    }
    
    BOOL CPage1::OnCommand(WPARAM wParam, LPARAM lParam)
    {
        SetModified(TRUE);
        return CPropertyPage::OnCommand(wParam, lParam);
    }
    
    void CPage1::DoDataExchange(CDataExchange* pDX)
    {
        TRACE("Entering CPage1::DoDataExchange -- %d\n",
              pDX->m_bSaveAndValidate);
        CPropertyPage::DoDataExchange(pDX);
        //{{AFX_DATA_MAP(CPage1)
        DDX_Radio(pDX, IDC_FONT, m_nFont);
        //}}AFX_DATA_MAP
    }
    
    
    BEGIN_MESSAGE_MAP(CPage1, CPropertyPage)
        //{{AFX_MSG_MAP(CPage1)
            // NOTE: the ClassWizard will add message map macros here
        //}}AFX_MSG_MAP
    END_MESSAGE_MAP()
    
    ////////////////////////////////////////////////////////////////////
    // CPage1 message handlers
    
    ////////////////////////////////////////////////////////////////////
    // CPage2 property page
    
    IMPLEMENT_DYNCREATE(CPage2, CPropertyPage)
    
    CPage2::CPage2() : CPropertyPage(CPage2::IDD)
    {
        //{{AFX_DATA_INIT(CPage2)
        m_bBold = FALSE;
        m_bItalic = FALSE;
        m_bUnderline = FALSE;
        //}}AFX_DATA_INIT
    }
    
    CPage2::~CPage2()
    {
    }
    
    BOOL CPage2::OnCommand(WPARAM wParam, LPARAM lParam)
    {
        SetModified(TRUE);
        return CPropertyPage::OnCommand(wParam, lParam);
    }
    
    void CPage2::DoDataExchange(CDataExchange* pDX)
    {
        TRACE("Entering CPage2::DoDataExchange -- %d\n",
              pDX->m_bSaveAndValidate);
        CPropertyPage::DoDataExchange(pDX);
        //{{AFX_DATA_MAP(CPage2)
        DDX_Check(pDX, IDC_BOLD, m_bBold);
        DDX_Check(pDX, IDC_ITALIC, m_bItalic);
        DDX_Check(pDX, IDC_UNDERLINE, m_bUnderline);
        //}}AFX_DATA_MAP
    }
    
    
    BEGIN_MESSAGE_MAP(CPage2, CPropertyPage)
        //{{AFX_MSG_MAP(CPage2)
            // NOTE: the ClassWizard will add message map macros here
        //}}AFX_MSG_MAP
    END_MESSAGE_MAP()
    
    ////////////////////////////////////////////////////////////////////
    // CPage2 message handlers
    
    ////////////////////////////////////////////////////////////////////
    // CPage3 property page
    
    IMPLEMENT_DYNCREATE(CPage3, CPropertyPage)
    
    CPage3::CPage3() : CPropertyPage(CPage3::IDD)
    {
        //{{AFX_DATA_INIT(CPage3)
        m_nColor = -1;
        //}}AFX_DATA_INIT
    }
    
    CPage3::~CPage3()
    {
    }
    
    BOOL CPage3::OnCommand(WPARAM wParam, LPARAM lParam)
    {
        SetModified(TRUE);
        return CPropertyPage::OnCommand(wParam, lParam);
    }
    
    void CPage3::DoDataExchange(CDataExchange* pDX)
    {
        TRACE("Entering CPage3::DoDataExchange -- %d\n",
              pDX->m_bSaveAndValidate);
        CPropertyPage::DoDataExchange(pDX);
        //{{AFX_DATA_MAP(CPage3)
        DDX_Radio(pDX, IDC_COLOR, m_nColor);
        //}}AFX_DATA_MAP
    }
    
    
    BEGIN_MESSAGE_MAP(CPage3, CPropertyPage)
        //{{AFX_MSG_MAP(CPage3)
            // NOTE: the ClassWizard will add message map macros here
        //}}AFX_MSG_MAP
    END_MESSAGE_MAP()
    
    ////////////////////////////////////////////////////////////////////
    // CPage3 message handlers
    
    ////////////////////////////////////////////////////////////////////
    // CPage4 property page
    
    IMPLEMENT_DYNCREATE(CPage4, CPropertyPage)
    
    CPage4::CPage4() : CPropertyPage(CPage4::IDD)
    {
        //{{AFX_DATA_INIT(CPage4)
        m_nFontSize = 0;
        //}}AFX_DATA_INIT
    }
    
    CPage4::~CPage4()
    {
    }
    
    BOOL CPage4::OnCommand(WPARAM wParam, LPARAM lParam)
    {
        SetModified(TRUE);
        return CPropertyPage::OnCommand(wParam, lParam);
    }
    
    void CPage4::DoDataExchange(CDataExchange* pDX)
    {
        TRACE("Entering CPage4::DoDataExchange -- %d\n",
              pDX->m_bSaveAndValidate);
        CPropertyPage::DoDataExchange(pDX);
        //{{AFX_DATA_MAP(CPage4)
        DDX_Text(pDX, IDC_FONTSIZE, m_nFontSize);
        DDV_MinMaxInt(pDX, m_nFontSize, 8, 24);
        //}}AFX_DATA_MAP
    }
    
    
    BEGIN_MESSAGE_MAP(CPage4, CPropertyPage)
        //{{AFX_MSG_MAP(CPage4)
        //}}AFX_MSG_MAP
    END_MESSAGE_MAP()
    
    ////////////////////////////////////////////////////////////////////
    // CPage4 message handlers
    
    BOOL CPage4::OnInitDialog() 
    {
        CPropertyPage::OnInitDialog();
        ((CSpinButtonCtrl*) GetDlgItem(IDC_SPIN1))->SetRange(8, 24);
        return TRUE;  // return TRUE unless you set the focus to a control
                      //  EXCEPTION: OCX Property Pages should return FALSE
    }
    
    ////////////////////////////////////////////////////////////////////
    // CFontSheet
    
    IMPLEMENT_DYNAMIC(CFontSheet, CPropertySheet)
    
    CFontSheet::CFontSheet(UINT nIDCaption, CWnd* pParentWnd,
                           UINT iSelectPage)
        :CPropertySheet(nIDCaption, pParentWnd, iSelectPage)
    {
    }
    
    CFontSheet::CFontSheet(LPCTSTR pszCaption, CWnd* pParentWnd,
                           UINT iSelectPage)
        :CPropertySheet(pszCaption, pParentWnd, iSelectPage)
    {
        AddPage(&m_page1);
        AddPage(&m_page2);
        AddPage(&m_page3);
        AddPage(&m_page4);
    }
    
    CFontSheet::~CFontSheet()
    {
    }
    
    
    BEGIN_MESSAGE_MAP(CFontSheet, CPropertySheet)
        //{{AFX_MSG_MAP(CFontSheet)
            // NOTE - the ClassWizard will add and remove mapping macros here.
        //}}AFX_MSG_MAP
    END_MESSAGE_MAP()
    
    ////////////////////////////////////////////////////////////////////
    // CFontSheet message handlers
    
    Figure 13-6. The EX13A Header and implementation file listings for the property page and property sheet classes.

  2. Add two data members and two prototypes to the CEx13aView class. If you use ClassView for the data members, the #include for Property.h will be added automatically.

    private:
        CFontSheet m_sh;
        BOOL m_bDefault; // TRUE default format, FALSE selection

    Now add the prototype for the private function Format:

    void Format(CHARFORMAT &cf);

    Insert the prototype for the protected function OnUserApply before the DECLARE_MESSAGE_MAP macro.

    afx_msg LRESULT OnUserApply(WPARAM wParam, LPARAM lParam);

  3. Edit and add code in the file ex13aView.cpp. Map the user-defined WM_USERAPPLY message, as shown here:

    ON_MESSAGE(WM_USERAPPLY, OnUserApply)

    Add the following lines to the OnCreate function, just before the return 0 statement:

    CHARFORMAT cf;
    Format(cf);
    m_rich.SetDefaultCharFormat(cf);
    

    Edit the view constructor to set default values for the property sheet data members, as follows:

    CEx13aView::CEx13aView() : m_sh("")
    {
        m_sh.m_page1.m_nFont = 0;
        m_sh.m_page2.m_bBold = FALSE;
        m_sh.m_page2.m_bItalic = FALSE;
        m_sh.m_page2.m_bUnderline = FALSE;
        m_sh.m_page3.m_nColor = 0;
        m_sh.m_page4.m_nFontSize = 12;
        g_pView = this;
        m_bDefault = TRUE;
    }

    Edit the format command handlers, as shown here:

    void CEx13aView::OnFormatDefault()
    {
        m_sh.SetTitle("Default Format");
        m_bDefault = TRUE;
        m_sh.DoModal();
    }
    
    void CEx13aView::OnFormatSelection()
    {
        m_sh.SetTitle("Selection Format");
        m_bDefault = FALSE;
        m_sh.DoModal();
    }
    
    void CEx13aView::OnUpdateFormatSelection(CCmdUI* pCmdUI)
    {
        long nStart, nEnd;
        m_rich.GetSel(nStart, nEnd);
        pCmdUI->Enable(nStart != nEnd);
    }
    

    Add the following handler for the user-defined WM_USERAPPLY message:

    LRESULT CEx13aView::OnUserApply(WPARAM wParam, LPARAM lParam)
    {
        TRACE("CEx13aView::OnUserApply -- wParam = %x\n", wParam);
        CHARFORMAT cf;
        Format(cf);
        if (m_bDefault) {
            m_rich.SetDefaultCharFormat(cf);
        }
        else {
            m_rich.SetSelectionCharFormat(cf);
        }
        return 0;
    }

    Add the Format helper function, as shown below, to set a CHARFORMAT structure based on the values of the property sheet data members.

    void CEx13aView::Format(CHARFORMAT& cf)
    {
        cf.cbSize = sizeof(CHARFORMAT);
        cf.dwMask = CFM_BOLD | CFM_COLOR | CFM_FACE |
                    CFM_ITALIC | CFM_SIZE | CFM_UNDERLINE;
        cf.dwEffects = (m_sh.m_page2.m_bBold ? CFE_BOLD : 0) |
                       (m_sh.m_page2.m_bItalic ? CFE_ITALIC : 0) |
                       (m_sh.m_page2.m_bUnderline ? CFE_UNDERLINE : 0);
        cf.yHeight = m_sh.m_page4.m_nFontSize * 20;
        switch(m_sh.m_page3.m_nColor) {
        case -1:
        case 0:
            cf.crTextColor = RGB(0, 0, 0);
            break;
        case 1:
            cf.crTextColor = RGB(255, 0, 0);
            break;
        case 2:
            cf.crTextColor = RGB(0, 255, 0);
            break;
        }
        switch(m_sh.m_page1.m_nFont) {
        case -1:
        case 0:
            strcpy(cf.szFaceName, "Times New Roman");
            break;
        case 1:
            strcpy(cf.szFaceName, "Arial");
            break;
        case 2:
            strcpy(cf.szFaceName, "Courier New");
            break;
        }
        cf.bCharSet = 0;
        cf.bPitchAndFamily = 0;
    }

  4. Build and test the enhanced EX13A application. Type some text, and then choose Default from the Format menu. Observe the TRACE messages in the Debug window as you click on property sheet tabs and click the Apply button. Try highlighting some text and then formatting the selection.

    Apply Button Processing

    You might be curious about the way the property sheet classes process the Apply button. In all the page classes, the overridden OnCommand functions enable the Apply button whenever a control sends a message to the page. This works fine for pages 1 through 3 in EX13A, but for page 4, OnCommand is called during the initial conversation between the spin button control and its buddy.

    The OnApply virtual override in the CPage1 class sends a user-defined message to the view. The function finds the view in an expedient way—by using a global variable set by the view class. A better approach would be to pass the view pointer to the sheet constructor and then to the page constructor.

    The view class calls the property sheet's DoModal function for both default formatting and selection formatting. It sets the m_bDefault flag to indicate the mode. We don't need to check the return from DoModal because the user-defined message is sent for both the OK button and the Apply button. If the user clicks Cancel, no message is sent.