Programming Windows with MFC, Second Edition
One feature of Windows that programmers of every stripe will appreciate is property sheets—tabbed dialog boxes containing pages of controls that the user can switch among with mouse clicks. Property sheets live in the common controls library provided with every copy of Windows. They're something of a chore to program using the Windows API, but they're relatively easy to implement in MFC thanks to the support provided by the framework. In fact, adding a property sheet to an MFC application isn't all that different from adding a dialog box. An MFC application that uses property sheets and runs on Windows 95 or later or Windows NT 3.51 or later uses the operating system's native property sheet implementation. On other platforms, MFC's private implementation is used instead.
The functionality of property sheets is neatly encapsulated in a pair of MFC classes named CPropertySheet and CPropertyPage. CPropertySheet represents the property sheet itself and is derived from CWnd. CPropertyPage represents a page in a property sheet and is derived from CDialog. Both are defined in the header file Afxdlgs.h. Like dialog boxes, property sheets can be modal or modeless. CPropertySheet::DoModal creates a modal property sheet, and CPropertySheet::Create creates a modeless property sheet.
The general procedure for creating a modal property sheet goes like this:
- For each page in the property sheet, create a dialog template defining the page's contents and characteristics. Set the dialog title to the title you want to appear on the tab at the top of the property sheet page.
- For each page in the property sheet, derive a dialog-like class from CPropertyPage that includes public data members linked to the page's controls via DDX or DDV.
- Derive a property sheet class from CPropertySheet. Instantiate the property sheet class and the property sheet page classes you derived in step 2. Use CPropertySheet::AddPage to add the pages to the property sheet in the order in which you want them to appear.
- Call the property sheet's DoModal function to display it on the screen.
To simplify property sheet creation, most MFC programmers declare instances of their property sheet page classes inside the derived property sheet class. They also write the property sheet class's constructor such that it calls AddPage to add the pages to the property sheet. The class declarations for a simple property sheet and its pages might look like this:
class CFirstPage : public CPropertyPage { public: CFirstPage () : CPropertyPage (IDD_FIRSTPAGE) {}; // Declare CFirstPage's data members here. protected: virtual void DoDataExchange (CDataExchange*); }; class CSecondPage : public CPropertyPage { public: CSecondPage () : CPropertyPage (IDD_SECONDPAGE) {}; // Declare CSecondPage's data members here. protected: virtual void DoDataExchange (CDataExchange*); }; class CMyPropertySheet : public CPropertySheet { public: CFirstPage m_firstPage; // First page CSecondPage m_secondPage; // Second page // Constructor adds the pages automatically. CMyPropertySheet (LPCTSTR pszCaption, CWnd* pParentWnd = NULL) : CPropertySheet (pszCaption, pParentWnd, 0) { AddPage (&m_firstPage); AddPage (&m_secondPage); } }; |
In this example, CFirstPage represents the first page in the property sheet, and CSecondPage represents the second. The associated dialog resources, which are referenced in the pages' class constructors, are IDD_FIRSTPAGE and IDD_SECONDPAGE. With this infrastructure in place, a modal property sheet featuring the caption "Properties" in its title bar can be constructed and displayed with two simple statements:
CMyPropertySheet ps (_T ("Properties")); ps.DoModal (); |
Like CDialog::DoModal, CPropertySheet::DoModal returns IDOK if the property sheet was dismissed with the OK button, or IDCANCEL otherwise.
The dialog templates for property sheet pages shouldn't include OK and Cancel buttons because the property sheet provides these buttons. A property sheet also includes an Apply button and an optional Help button. The Apply button is disabled when the property sheet first appears and is enabled when a property sheet page calls the SetModified function it inherits from CPropertyPage and passes in TRUE. SetModified should be called anytime the settings embodied in the property sheet are changed—for example, whenever the text of an edit control is modified or a radio button is clicked. To trap clicks of the Apply button, you must include an ON_BN_CLICKED handler in the derived property sheet class. The button's ID is ID_APPLY_NOW. The click handler should call UpdateData with a TRUE parameter to update the active page's member variables and transmit the current property values to the property sheet's owner. Afterward, the click handler should disable the Apply button by calling SetModified with a FALSE parameter—once for each of the property sheet pages.
Note that the Apply button's ON_BN_CLICKED handler calls UpdateData for only the active property sheet page—the one that's currently displayed. That's important, because property sheet pages aren't physically created until they are activated by the person using the property sheet. Calling UpdateData for a property sheet page whose tab hasn't been clicked results in an assertion error from MFC. The framework calls UpdateData for the active page when the user switches to another page, so when the user clicks the Apply button, the only page whose data members need to be updated is the page that's currently active. You can get a pointer to the active page with CPropertySheet::GetActivePage.
Using DDX and DDV to transfer data between controls and data members in property sheet pages and to validate data extracted from the controls is more than a matter of convenience; it allows MFC to do much of the dirty work involved in property sheet handling. The first time a property sheet page is displayed, for example, the page's OnInitDialog function is called. The default implementation of OnInitDialog calls UpdateData to initialize the page's controls. If the user then clicks a tab to activate another page, the current page's OnKillActive function is called and the framework calls UpdateData to retrieve and validate the controls' data. Shortly thereafter, the newly activated page receives an OnSetActive notification and possibly an OnInitDialog notification, too. If the user then goes on to click the property sheet's OK button, the current page's OnOK handler is called and the framework calls UpdateData to retrieve and validate that page's data.
The point is that a property sheet works the way it does because the framework provides default implementations of key virtual functions that govern the property sheet's behavior. You can customize a property sheet's operation by overriding the pages' OnInitDialog, OnSetActive, OnKillActive, OnOK, and OnCancel functions and performing specialized processing of your own; but if you do, be sure to call the equivalent functions in the base class so that the framework can do its part. And if you don't use DDX and DDV, you need to override all of these functions for every page in the property sheet to ensure that each page's data is handled properly. DDX and DDV simplify property sheet usage by letting the framework do the bulk of the work.
The PropDemo Application
The PropDemo application shown in Figure 8-11 is similar to DlgDemo1 and DlgDemo2, but it uses a property sheet instead of a dialog box to expose configuration settings to the user. The property sheet's Size page contains controls for setting the size of the ellipse displayed in the view. The Color page contains controls for modifying the ellipse's color. The property sheet is modal, so the main window can't be reactivated while the property sheet is displayed.
Figure 8-11. The PropDemo window and property sheet.
Selected portions of PropDemo's source code are reproduced in Figure 8-12. CMyPropertySheet represents the property sheet itself, and CSizePage and CColorPage represent the property sheet pages. All three classes were derived with ClassWizard. Instances of CSizePage and CColorPage named m_sizePage and m_colorPage are declared in CMyPropertySheet so that the page objects will be constructed automatically when the property sheet object is constructed. Furthermore, both m_sizePage and m_colorPage are declared public so that they can be accessed from outside of CMyPropertySheet.
The property sheet is created by CChildView::OnFileProperties when the user selects the Properties command from the File menu. After constructing a CMyPropertySheet object on the stack, OnFileProperties copies the current settings—width, height, units, and color—to the corresponding member variables in the property sheet page objects:
CMyPropertySheet ps (_T ("Properties")); ps.m_sizePage.m_nWidth = m_nWidth; ps.m_sizePage.m_nHeight = m_nHeight; ps.m_sizePage.m_nUnits = m_nUnits; ps.m_colorPage.m_nColor = m_nColor; |
OnFileProperties then displays the property sheet by calling DoModal. If the property sheet is dismissed with the OK button, the new settings are copied from the property sheet pages and Invalidate is called to repaint the view and apply the changes:
if (ps.DoModal () == IDOK) { m_nWidth = ps.m_sizePage.m_nWidth; m_nHeight = ps.m_sizePage.m_nHeight; m_nUnits = ps.m_sizePage.m_nUnits; m_nColor = ps.m_colorPage.m_nColor; Invalidate (); } |
Both CSizePage and CColorPage map ON_BN_CLICKED notifications from radio buttons and EN_CHANGE notifications from edit controls to a class member function named OnChange. OnChange contains just one statement: a call to SetModified to enable the property sheet's Apply button. Consequently, any button click in a property sheet page, or any change to the text of an edit control, automatically enables the Apply button if it isn't already enabled.
When the Apply button is clicked, CMyPropertySheet's OnApply function takes control. It first calls UpdateData on the active property sheet page to transfer the user's input from the page's controls to its data members. It then initializes an ELLPROP structure with the property settings obtained from each page's data members and sends a message to the main window containing the structure's address. The main window forwards the message to the view, which responds by copying the property values to its own data members and calling Invalidate to force a repaint. After SendMessage returns, OnApply disables the Apply button by calling each property sheet page's SetModified function.
Figure 8-12. The PropDemo application.
MainFrm.h
// MainFrm.h : interface of the CMainFrame class // /////////////////////////////////////////////////////////////////////////// #if !defined(AFX_MAINFRM_H__9CE2B4A8_9067_11D2_8E53_006008A82731__INCLUDED_) #define AFX_MAINFRM_H__9CE2B4A8_9067_11D2_8E53_006008A82731__INCLUDED_ #if _MSC_VER > 1000 #pragma once #endif // _MSC_VER > 1000 #include "ChildView.h" class CMainFrame : public CFrameWnd { public: CMainFrame(); protected: DECLARE_DYNAMIC(CMainFrame) // Attributes public: // Operations public: // Overrides // ClassWizard generated virtual function overrides //AFX_VIRTUAL // Implementation public: virtual ~CMainFrame(); #ifdef _DEBUG virtual void AssertValid() const; virtual void Dump(CDumpContext& dc) const; #endif CChildView m_wndView; // Generated message map functions protected: //AFX_MSG afx_msg LRESULT OnApply (WPARAM wParam, LPARAM lParam); DECLARE_MESSAGE_MAP() }; /////////////////////////////////////////////////////////////////////////// // // Microsoft Visual C++ will insert additional declarations immediately // before the previous line. #endif // !defined(AFX_MAINFRM_H__9CE2B4A8_9067_11D2_8E53_006008A82731__INCLUDED_) |
MainFrm.cpp
// MainFrm.cpp : implementation of the CMainFrame class // #include "stdafx.h" #include "PropDemo.h" #include "MainFrm.h" #ifdef _DEBUG #define new DEBUG_NEW #undef THIS_FILE static char THIS_FILE[] = __FILE__; #endif /////////////////////////////////////////////////////////////////////////// // CMainFrame IMPLEMENT_DYNAMIC(CMainFrame, CFrameWnd) BEGIN_MESSAGE_MAP(CMainFrame, CFrameWnd) //AFX_MSG_MAP ON_MESSAGE (WM_USER_APPLY, OnApply) END_MESSAGE_MAP() /////////////////////////////////////////////////////////////////////////// // CMainFrame construction/destruction CMainFrame::CMainFrame() { } CMainFrame::~CMainFrame() { } BOOL CMainFrame::PreCreateWindow(CREATESTRUCT& cs) { if( !CFrameWnd::PreCreateWindow(cs) ) return FALSE; cs.dwExStyle &= ~WS_EX_CLIENTEDGE; cs.lpszClass = AfxRegisterWndClass(0); return TRUE; } /////////////////////////////////////////////////////////////////////////// // CMainFrame diagnostics #ifdef _DEBUG void CMainFrame::AssertValid() const { CFrameWnd::AssertValid(); } void CMainFrame::Dump(CDumpContext& dc) const { CFrameWnd::Dump(dc); } #endif //_DEBUG /////////////////////////////////////////////////////////////////////////// // CMainFrame message handlers void CMainFrame::OnSetFocus(CWnd* pOldWnd) { // forward focus to the view window m_wndView.SetFocus(); } BOOL CMainFrame::OnCmdMsg(UINT nID, int nCode, void* pExtra, AFX_CMDHANDLERINFO* pHandlerInfo) { // let the view have first crack at the command if (m_wndView.OnCmdMsg(nID, nCode, pExtra, pHandlerInfo)) return TRUE; // otherwise, do default handling return CFrameWnd::OnCmdMsg(nID, nCode, pExtra, pHandlerInfo); } int CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct) { if (CFrameWnd::OnCreate(lpCreateStruct) == -1) return -1; if (!m_wndView.Create(NULL, NULL, AFX_WS_DEFAULT_VIEW, CRect(0, 0, 0, 0), this, AFX_IDW_PANE_FIRST, NULL)) return -1; return 0; } LRESULT CMainFrame::OnApply (WPARAM wParam, LPARAM lParam) { m_wndView.SendMessage (WM_USER_APPLY, wParam, lParam); return 0; } |
ChildView.h
// ChildView.h : interface of the CChildView class // /////////////////////////////////////////////////////////////////////////// #if !defined(AFX_CHILDVIEW_H__9CE2B4AA_9067_11D2_8E53_006008A82731__INCLUDED_) #define AFX_CHILDVIEW_H__9CE2B4AA_9067_11D2_8E53_006008A82731__INCLUDED_ #if _MSC_VER > 1000 #pragma once #endif // _MSC_VER > 1000 /////////////////////////////////////////////////////////////////////////// // CChildView window class CChildView : public CWnd { // Construction public: CChildView(); // Attributes public: // Operations public: // Overrides // ClassWizard generated virtual function overrides //AFX_VIRTUAL // Implementation public: virtual ~CChildView(); // Generated message map functions protected: int m_nUnits; int m_nHeight; int m_nWidth; int m_nColor; //AFX_MSG afx_msg LRESULT OnApply (WPARAM wParam, LPARAM lParam); DECLARE_MESSAGE_MAP() }; /////////////////////////////////////////////////////////////////////////// // // Microsoft Visual C++ will insert additional declarations immediately // before the previous line. #endif //!defined(AFX_CHILDVIEW_H__9CE2B4AA_9067_11D2_8E53_006008A82731__INCLUDED_) |
ChildView.cpp
// ChildView.cpp : implementation of the CChildView class // #include "stdafx.h" #include "PropDemo.h" #include "ChildView.h" #include "MyPropertySheet.h" #ifdef _DEBUG #define new DEBUG_NEW #undef THIS_FILE static char THIS_FILE[] = __FILE__; #endif /////////////////////////////////////////////////////////////////////////// // CChildView CChildView::CChildView() { m_nWidth = 4; m_nHeight = 2; m_nUnits = 0; m_nColor = 0; } CChildView::~CChildView() { } BEGIN_MESSAGE_MAP(CChildView,CWnd ) //AFX_MSG_MAP ON_MESSAGE (WM_USER_APPLY, OnApply) END_MESSAGE_MAP() /////////////////////////////////////////////////////////////////////////// // CChildView message handlers BOOL CChildView::PreCreateWindow(CREATESTRUCT& cs) { if (!CWnd::PreCreateWindow(cs)) return FALSE; cs.dwExStyle ¦= WS_EX_CLIENTEDGE; cs.style &= ~WS_BORDER; cs.lpszClass = AfxRegisterWndClass(CS_HREDRAW¦CS_VREDRAW¦CS_DBLCLKS, ::LoadCursor(NULL, IDC_ARROW), HBRUSH(COLOR_WINDOW+1), NULL); return TRUE; } void CChildView::OnPaint() { CPaintDC dc(this); // Device context for painting. CBrush brush (CColorPage::m_clrColors[m_nColor]); CBrush* pOldBrush = dc.SelectObject (&brush); switch (m_nUnits) { case 0: // Inches. dc.SetMapMode (MM_LOENGLISH); dc.Ellipse (0, 0, m_nWidth * 100, -m_nHeight * 100); break; case 1: // Centimeters. dc.SetMapMode (MM_LOMETRIC); dc.Ellipse (0, 0, m_nWidth * 100, -m_nHeight * 100); break; case 2: // Pixels. dc.SetMapMode (MM_TEXT); dc.Ellipse (0, 0, m_nWidth, m_nHeight); break; } dc.SelectObject (pOldBrush); } void CChildView::OnFileProperties() { CMyPropertySheet ps (_T ("Properties")); ps.m_sizePage.m_nWidth = m_nWidth; ps.m_sizePage.m_nHeight = m_nHeight; ps.m_sizePage.m_nUnits = m_nUnits; ps.m_colorPage.m_nColor = m_nColor; if (ps.DoModal () == IDOK) { m_nWidth = ps.m_sizePage.m_nWidth; m_nHeight = ps.m_sizePage.m_nHeight; m_nUnits = ps.m_sizePage.m_nUnits; m_nColor = ps.m_colorPage.m_nColor; Invalidate (); } } LRESULT CChildView::OnApply (WPARAM wParam, LPARAM lParam) { ELLPROP* pep = (ELLPROP*) lParam; m_nWidth = pep->nWidth; m_nHeight = pep->nHeight; m_nUnits = pep->nUnits; m_nColor = pep->nColor; Invalidate (); return 0; } |
MyPropertySheet.h
#if !defined(AFX_MYPROPERTYSHEET_H__418271A3_90D4_11D2_8E53_006008A82731__INCLUDED_) #define AFX_MYPROPERTYSHEET_H__418271A3_90D4_11D2_8E53_006008A82731__INCLUDED_ #include "SizePage.h" // Added by ClassView #include "ColorPage.h" // Added by ClassView #if _MSC_VER > 1000 #pragma once #endif // _MSC_VER > 1000 // MyPropertySheet.h : header file // /////////////////////////////////////////////////////////////////////////// // CMyPropertySheet class CMyPropertySheet : public CPropertySheet { DECLARE_DYNAMIC(CMyPropertySheet) // Construction public: CMyPropertySheet(UINT nIDCaption, CWnd* pParentWnd = NULL, UINT iSelectPage = 0); CMyPropertySheet(LPCTSTR pszCaption, CWnd* pParentWnd = NULL, UINT iSelectPage = 0); // Attributes public: CColorPage m_colorPage; CSizePage m_sizePage; // Operations public: // Overrides // ClassWizard generated virtual function overrides //AFX_VIRTUAL // Implementation public: virtual ~CMyPropertySheet(); // Generated message map functions protected: //AFX_MSG afx_msg void OnApply (); DECLARE_MESSAGE_MAP() }; /////////////////////////////////////////////////////////////////////////// // // Microsoft Visual C++ will insert additional declarations immediately // before the previous line. #endif // !defined( // AFX_MYPROPERTYSHEET_H__418271A3_90D4_11D2_8E53_006008A82731__INCLUDED_) |
MyPropertySheet.cpp
// MyPropertySheet.cpp : implementation file // #include "stdafx.h" #include "PropDemo.h" #include "MyPropertySheet.h" #ifdef _DEBUG #define new DEBUG_NEW #undef THIS_FILE static char THIS_FILE[] = __FILE__; #endif /////////////////////////////////////////////////////////////////////////// // CMyPropertySheet IMPLEMENT_DYNAMIC(CMyPropertySheet, CPropertySheet) CMyPropertySheet::CMyPropertySheet(UINT nIDCaption, CWnd* pParentWnd, UINT iSelectPage) : CPropertySheet(nIDCaption, pParentWnd, iSelectPage) { AddPage (&m_sizePage); AddPage (&m_colorPage); } CMyPropertySheet::CMyPropertySheet(LPCTSTR pszCaption, CWnd* pParentWnd, UINT iSelectPage) : CPropertySheet(pszCaption, pParentWnd, iSelectPage) { AddPage (&m_sizePage); AddPage (&m_colorPage); } CMyPropertySheet::~CMyPropertySheet() { } BEGIN_MESSAGE_MAP(CMyPropertySheet, CPropertySheet) //AFX_MSG_MAP ON_BN_CLICKED (ID_APPLY_NOW, OnApply) END_MESSAGE_MAP() /////////////////////////////////////////////////////////////////////////// // CMyPropertySheet message handlers void CMyPropertySheet::OnApply () { GetActivePage ()->UpdateData (TRUE); ELLPROP ep; ep.nWidth = m_sizePage.m_nWidth; ep.nHeight = m_sizePage.m_nHeight; ep.nUnits = m_sizePage.m_nUnits; ep.nColor = m_colorPage.m_nColor; GetParent ()->SendMessage (WM_USER_APPLY, 0, (LPARAM) &ep); m_sizePage.SetModified (FALSE); m_colorPage.SetModified (FALSE); } |
SizePage.h
#if !defined(AFX_SIZEPAGE_H__418271A1_90D4_11D2_8E53_006008A82731__INCLUDED_) #define AFX_SIZEPAGE_H__418271A1_90D4_11D2_8E53_006008A82731__INCLUDED_ #if _MSC_VER > 1000 #pragma once #endif // _MSC_VER > 1000 // SizePage.h : header file // /////////////////////////////////////////////////////////////////////////// // CSizePage dialog class CSizePage : public CPropertyPage { DECLARE_DYNCREATE(CSizePage) // Construction public: CSizePage(); ~CSizePage(); // Dialog Data //{{AFX_DATA(CSizePage) enum { IDD = IDD_SIZE_PAGE }; int m_nWidth; int m_nHeight; int m_nUnits; //}}AFX_DATA // Overrides // ClassWizard generate virtual function overrides //AFX_VIRTUAL // Implementation protected: // Generated message map functions //AFX_MSG afx_msg void OnChange (); DECLARE_MESSAGE_MAP() }; // // Microsoft Visual C++ will insert additional declarations immediately // before the previous line. #endif //!defined(AFX_SIZEPAGE_H__418271A1_90D4_11D2_8E53_006008A82731__INCLUDED_) |
SizePage.cpp
// SizePage.cpp : implementation file // #include "stdafx.h" #include "PropDemo.h" #include "SizePage.h" #ifdef _DEBUG #define new DEBUG_NEW #undef THIS_FILE static char THIS_FILE[] = __FILE__; #endif /////////////////////////////////////////////////////////////////////////// // CSizePage property page IMPLEMENT_DYNCREATE(CSizePage, CPropertyPage) CSizePage::CSizePage() : CPropertyPage(CSizePage::IDD) { //AFX_DATA_INIT } CSizePage::~CSizePage() { } void CSizePage::DoDataExchange(CDataExchange* pDX) { CPropertyPage::DoDataExchange(pDX); //AFX_DATA_MAP } BEGIN_MESSAGE_MAP(CSizePage, CPropertyPage) //AFX_MSG_MAP ON_EN_CHANGE (IDC_WIDTH, OnChange) ON_EN_CHANGE (IDC_HEIGHT, OnChange) ON_BN_CLICKED (IDC_INCHES, OnChange) ON_BN_CLICKED (IDC_CENTIMETERS, OnChange) ON_BN_CLICKED (IDC_PIXELS, OnChange) END_MESSAGE_MAP() /////////////////////////////////////////////////////////////////////////// // CSizePage message handlers void CSizePage::OnChange () { SetModified (TRUE); } |
ColorPage.h
#if !defined(AFX_COLORPAGE_H__418271A2_90D4_11D2_8E53_006008A82731__INCLUDED_) #define AFX_COLORPAGE_H__418271A2_90D4_11D2_8E53_006008A82731__INCLUDED_ #if _MSC_VER > 1000 #pragma once #endif // _MSC_VER > 1000 // ColorPage.h : header file // /////////////////////////////////////////////////////////////////////////// // CColorPage dialog class CColorPage : public CPropertyPage { DECLARE_DYNCREATE(CColorPage) // Construction public: CColorPage(); ~CColorPage(); static const COLORREF m_clrColors[3]; // Dialog Data //{{AFX_DATA(CColorPage) enum { IDD = IDD_COLOR_PAGE }; int m_nColor; //}}AFX_DATA // Overrides // ClassWizard generate virtual function overrides //AFX_VIRTUAL // Implementation protected: // Generated message map functions //AFX_MSG afx_msg void OnChange (); DECLARE_MESSAGE_MAP() }; // // Microsoft Visual C++ will insert additional declarations immediately // before the previous line. #endif //defined(AFX_COLORPAGE_H__418271A2_90D4_11D2_8E53_006008A82731__INCLUDED_) |
ColorPage.cpp
// ColorPage.cpp : implementation file // #include "stdafx.h" #include "PropDemo.h" #include "ColorPage.h" #ifdef _DEBUG #define new DEBUG_NEW #undef THIS_FILE static char THIS_FILE[] = __FILE__; #endif /////////////////////////////////////////////////////////////////////////// // CColorPage property page IMPLEMENT_DYNCREATE(CColorPage, CPropertyPage) const COLORREF CColorPage::m_clrColors[3] = { RGB (255, 0, 0), // Red RGB ( 0, 255, 0), // Green RGB ( 0, 0, 255) // Blue }; CColorPage::CColorPage() : CPropertyPage(CColorPage::IDD) { //AFX_DATA_INIT } CColorPage::~CColorPage() { } void CColorPage::DoDataExchange(CDataExchange* pDX) { CPropertyPage::DoDataExchange(pDX); //AFX_DATA_MAP } BEGIN_MESSAGE_MAP(CColorPage, CPropertyPage) //AFX_MSG_MAP ON_BN_CLICKED (IDC_RED, OnChange) ON_BN_CLICKED (IDC_GREEN, OnChange) ON_BN_CLICKED (IDC_BLUE, OnChange) END_MESSAGE_MAP() /////////////////////////////////////////////////////////////////////////// // CColorPage message handlers void CColorPage::OnChange () { SetModified (TRUE); } |