Implementing a Property Page

When Beeper6 is asked for its list of property page CLSIDs, it specifies only CLSID_BeeperPropertyPage, whose server is BEEPPROP.DLL. BeepProp is a normal in-process server for an object that implements IPropertyPage and supports both English and German in its user interface. Its registry entries are extremely simple; we require only an entry for InprocServer32 under the appropriate CLSID subkey. Because a property page is always specified by its CLSID, there is no need to assign a ProgID or VersionIndependentProgID or to create registry entries for them. A property page also has no need for type information (at least at this point in time).

The source code for BeepProp is contained in BEEPPROP.CPP, including the class factory, required in-process server exports, and the property page object. In addition, BEEPPROP.RC contains the dialog templates for the property page itself, in both English and German, as well as the title strings that appear in the frame's tab for this page:


//From BEEPPROP.RC
STRINGTABLE
BEGIN
IDS_0_PAGETITLE, "General"
IDS_7_PAGETITLE, "Allgemein"
END

IDD_BEEPERPROPS_0 DIALOG DISCARDABLE 0, 0, 172, 88
STYLE WS_CHILD
FONT 8, "MS Sans Serif"
BEGIN
CONTROL "&Default",IDC_BEEPDEFAULT,"Button",
BS_AUTORADIOBUTTON œ WS_GROUP,13,8,84,12
CONTROL "&Hand",IDC_BEEPHAND,"Button",BS_AUTORADIOBUTTON,
13,23,84,12
CONTROL "&Question",IDC_BEEPQUESTION,"Button",
BS_AUTORADIOBUTTON,13,38,84,12
CONTROL "&Exclamation",IDC_BEEPEXCLAMATION,"Button",
BS_AUTORADIOBUTTON,13,53,84,12
CONTROL "&Asterisk",IDC_BEEPASTERISK,"Button",
BS_AUTORADIOBUTTON,13,68,84,12
DEFPUSHBUTTON "&Test",IDOK,118,8,50,14
END


IDD_BEEPERPROPS_7 DIALOG DISCARDABLE 0, 0, 172, 88
STYLE WS_CHILD
FONT 8, "MS Sans Serif"
BEGIN
CONTROL "&Standard",IDC_BEEPDEFAULT,"Button",
BS_AUTORADIOBUTTON œ WS_GROUP,13,8,84,12
CONTROL "&Hand",IDC_BEEPHAND,"Button",BS_AUTORADIOBUTTON,
13,23,84,12
CONTROL "&Frage",IDC_BEEPQUESTION,"Button",
BS_AUTORADIOBUTTON,13,38,84,12
CONTROL "&Ausruf",IDC_BEEPEXCLAMATION,"Button",
BS_AUTORADIOBUTTON,13,53,84,12
CONTROL "Ste&rn",IDC_BEEPASTERISK,"Button",
BS_AUTORADIOBUTTON,13,68,84,12
DEFPUSHBUTTON "&Test",IDOK,118,8,50,14
END

Notice the inclusion of WS_CHILD and the conspicuous lack of WS_CAPTION and WS_THICKFRAME styles on both dialog templates—creating a dialog box from either of the templates will create a window displaying the listed controls, but the dialog window itself will have no other embellishments. We can then create a dialog from a template as a child window of the frame dialog itself. This results in the proper user interface, as shown in Figure 16-7.

Figure 16-7.

The Beeper property page inside a property frame. The page itself is a modeless dialog created inside the frame dialog.

Deciding which template to load (and which page title string to use) occurs at run time within BeepProp's implementation of IPropertyPage::SetPageSite, the page's initialization function. Here it saves the LCID returned from IPropertyPageSite::GetLocaleID and initializes its variable m_uIDTemplate accordingly. It then calculates the size of the page on the basis of the dialog template and uses it later within IPropertyPage::GetPageInfo:


STDMETHODIMP CBeeperPropPage::SetPageSite
(LPPROPERTYPAGESITE pPageSite)
{
if (NULL==pPageSite)
ReleaseInterface(m_pIPropertyPageSite)
else
{
HWND hDlg;
RECT rc;
LCID lcid;

m_pIPropertyPageSite=pPageSite;
m_pIPropertyPageSite->AddRef();

if (SUCCEEDED(m_pIPropertyPageSite->GetLocaleID(&lcid)))
m_lcid=lcid;

switch (PRIMARYLANGID(m_lcid))
{
case LANG_GERMAN:
m_uIDTemplate=IDD_BEEPERPROPS_7;
break;

case LANG_NEUTRAL:
case LANG_ENGLISH:
default:
m_uIDTemplate=IDD_BEEPERPROPS_0;
break;
}

hDlg=CreateDialogParam(m_hInst
, MAKEINTRESOURCE(m_uIDTemplate), GetDesktopWindow()
, (DLGPROC)BeepPropPageProc, 0L);

//If creation fails, use default values set in constructor.
if (NULL!=hDlg)
{
GetWindowRect(hDlg, &rc);
m_cx=rc.right-rc.left;
m_cy=rc.bottom-rc.top;

DestroyWindow(hDlg);
}
}

return NOERROR;
}

STDMETHODIMP CBeeperPropPage::GetPageInfo(LPPROPPAGEINFO pPageInfo)
{
IMalloc *pIMalloc;

if (FAILED(CoGetMalloc(MEMCTX_TASK, &pIMalloc)))
return ResultFromScode(E_FAIL);

pPageInfo->pszTitle=(LPOLESTR)pIMalloc->Alloc(CCHSTRINGMAX);

if (NULL!=pPageInfo->pszTitle)
{
UINT ids=IDS_0_PAGETITLE;

if (PRIMARYLANGID(m_lcid)==LANG_GERMAN)
ids=IDS_7_PAGETITLE;

LoadString(m_hInst, ids, pPageInfo->pszTitle, CCHSTRINGMAX);
}

pIMalloc->Release();

pPageInfo->size.cx = m_cx;
pPageInfo->size.cy = m_cy;
pPageInfo->pszDocString = NULL;
pPageInfo->pszHelpFile = NULL;
pPageInfo->dwHelpContext= 0;
return NOERROR;
}

Notice how SetPageSite creates the page dialog, retrieves its dimensions, and destroys the dialog immediately. This is done only to initialize m_cx and m_cy for use in GetPageInfo, which will be called before this page is activated, if ever. Creating the dialog and calling the Windows function GetWindowRect is much easier than loading the dialog template directly and trying to calculate the extents on the basis of the dialog units in the template. This would involve creating a font, mucking with font sizes and extents, and converting all the values. Windows does this automatically when you create a dialog from the same template, so here I'm simply taking advantage of that convenience. I destroy the dialog immediately because I don't want to hog extra resources by keeping this page in memory when it might not be displayed at all. We want the IPropertyPage::Activate and Deactivate functions to control our use of these resources, in which we create the appropriate dialog or destroy it:


STDMETHODIMP CBeeperPropPage::Activate(HWND hWndParent
, LPCRECT prc, BOOL fModal)
{
if (NULL!=m_hDlg)
return ResultFromScode(E_UNEXPECTED);

m_hDlg=CreateDialogParam(m_hInst, MAKEINTRESOURCE(m_uIDTemplate)
, hWndParent, BeepPropPageProc, (LPARAM)this);

if (NULL==m_hDlg)
return ResultFromScode(E_OUTOFMEMORY);

//Move page into position and show it.
SetWindowPos(m_hDlg, NULL, prc->left, prc->top
, prc->right-prc->left, prc->bottom-prc->top, 0);

return NOERROR;
}

STDMETHODIMP CBeeperPropPage::Deactivate(void)
{
if (NULL==m_hDlg)
return ResultFromScode(E_UNEXPECTED);

DestroyWindow(m_hDlg);
m_hDlg=NULL;
return NOERROR;
}

Nothing fancy going on, simply straight Windows programming, which applies equally for the Show and Move members of IPropertyPage as well:


STDMETHODIMP CBeeperPropPage::Show(UINT nCmdShow)
{
if (NULL==m_hDlg)
ResultFromScode(E_UNEXPECTED);

ShowWindow(m_hDlg, nCmdShow);

//Take the focus.
if (SW_SHOWNORMAL==nCmdShow œœ SW_SHOW==nCmdShow)
SetFocus(m_hDlg);

return NOERROR;
}

STDMETHODIMP CBeeperPropPage::Move(LPCRECT prc)
{
SetWindowPos(m_hDlg, NULL, prc->left, prc->top
, prc->right-prc->left, prc->bottom-prc->top, 0);

return NOERROR;
}

Before Activate is called, however, the frame will pass the array of IUnknown pointers for the affected objects to our IPropertyPage::SetObjects. Here we must free any object selection we might already have and then copy the pointers we need, being sure to call AddRef through them. BeepProp specifically queries for IBeeper, through which it knows it can access the necessary features of the Beeper object:


STDMETHODIMP CBeeperPropPage::SetObjects(ULONG cObjects
, IUnknown **ppUnk)
{
BOOL fRet=TRUE;

FreeAllObjects();

if (0!=cObjects)
{
UINT i;
HRESULT hr;

m_ppIBeeper=new IBeeper * [(UINT)cObjects];

for (i=0; i < cObjects; i++)
{
hr=ppUnk[i]->QueryInterface(IID_IBeeper
, (void **)&m_ppIBeeper[i]);

if (FAILED(hr))
fRet=FALSE;
}
}

//If we didn't get one of our objects, fail this call.
if (!fRet)
return ResultFromScode(E_FAIL);

m_cObjects=cObjects;
return NOERROR;
}

The internal function CBeeperPropPage::FreeAllObjects calls Release on every pointer in m_ppIBeeper and deletes the m_ppIBeeper array itself.

After we know the objects for which this property page is being displayed, we can use those objects to set up the initial state of the control on the page. This happens in the WM_INITDIALOG case of BeepPropPageProc, which is the dialog procedure for the property page. (The local variable pObj inside this dialog procedure is the CBeeperPropPage pointer, managed with the Windows API functions SetProp and GetProp.) We retrieve the current sound from the underlying Beeper object and check the appropriate radio button:


case WM_INITDIALOG:
§
if (1==pObj->m_cObjects)
{
UINT iButton;

iButton=(UINT)pObj->m_ppIBeeper[0]->get_Sound();

if (0==iButton)
iButton=IDC_BEEPDEFAULT;

CheckRadioButton(hDlg, IDC_BEEPDEFAULT
, IDC_BEEPASTERISK, iButton);

pObj->m_uIDLastSound=iButton;
}
§

This code initializes the dialog state only if there is a single object—otherwise, it leaves the state uninitialized. A more sophisticated property page would try to reconcile the states of all underlying objects in a way that if they all have the same state, some of the dialog controls could be initialized.

The variable m_uIDLastSound in CBeeperPropPage is used to keep track of changes that occur in the radio button selection in this property page. In the WM_COMMAND message processing of BeepPropPageProc, we execute the following code whenever a radio button is selected:


if (pObj->m_uIDLastSound==wID)
break;

//Save most recently selected sound.
pObj->m_uIDLastSound=LOWORD(wParam);
pObj->m_fDirty=TRUE;

if (NULL!=pObj->m_pIPropertyPageSite)
{
pObj->m_pIPropertyPageSite
->OnStatusChange(PROPPAGESTATUS_DIRTY);
}

In short, if the newly selected sound is different from the last selection, we consider the page to be dirty by setting m_fDirty. At the same time, we have to notify the page site of the change by calling IPropertyPageSite::OnStatusChange with PROPPAGESTATUS_DIRTY. Now our dirty flag will affect the return value from IPropertyPage::IsPageDirty in this way:


STDMETHODIMP CBeeperPropPage::IsPageDirty(void)
{
return ResultFromScode(m_fDirty ? S_OK : S_FALSE);
}

This function is called when the user closes the dialog box with the OK button. If our page is dirty, the frame will call IPropertyPage::Apply before deactivating this page and destroying the object. Of course, we might receive an Apply call before this time because our call to IPropertyPageSite::OnStatusChange will tell the frame to enable its Apply Now button. The frame will call Apply when the user presses this button. In response, we send the current sound value (m_uIDLastSound) to the affected objects:


STDMETHODIMP CBeeperPropPage::Apply(void)
{
UINT i;
UINT lSound, lSoundNew;
BOOL fChanged;

if (0==m_cObjects)
return NOERROR;

lSound=(IDC_BEEPDEFAULT==m_uIDLastSound) ? 0L : m_uIDLastSound;
fChanged=TRUE;

for (i=0; i < m_cObjects; i++)
{
m_ppIBeeper[i]->put_Sound(lSound);
lSoundNew=m_ppIBeeper[i]->get_Sound();

fChanged &= (lSound==lSoundNew);
}

m_fDirty=!fChanged;
return NOERROR;
}

Because this property page knows that the Beeper object implements the interface IBeeper (defined in ..\BEEPER6\IBEEPER.H, an output file from MKTYPLIB), we can call its put_Sound member to apply the changes. However, because put_Sound doesn't return an error code, we must call get_Sound afterward to check whether the sound actually did change. For example, if you choose Enforce Read-Only in AutoCli2 before choosing Properties, changes made in the property page will not affect the Beeper object at all. Our implementation of Apply here will not clear its dirty flag unless changes are applied successfully. What is the result? After calling Apply, the frame will immediately call IsPageDirty to see whether the changes made the page clean. If so, it disables the Apply Now button until another call to IPropertyPageSite::OnStatusChange. If we do not clear the dirty flag within Apply, the Apply Now button will remain enabled, as it should be to indicate a dirty state.

Remember, the way a property page applies its changes to the affected objects is decided between the page and the object. In this sample, BeepProp knows about Beeper6's custom interface, so it uses that interface directly, which has the nice side effect of being independent of localization concerns. We could accomplish the same thing by calling IDispatch::GetIDsOfNames for the Sound property (or Ton in German) followed by a call to IDispatch::Invoke. It doesn't matter when a custom property page is involved. A standard property page—one that is intended to be used for different object classes—must specify exactly how it intends to apply changes to the objects. The standard font, color, and picture pages in the OLE Control Development Kit will always send standard dispID values to the object's IDispatch::Invoke and assume that the object knows what to do with the new values.

Only three things are left in this implementation. First, the Test button in our property page is really only a convenience for the user. To implement it, we call MessageBeep with m_uIDLastSound because we know exactly what the Beeper object does with a sound. This is appropriate because the object may, in fact, not be allowed to change its properties at all if the client is disallowing changes through IPropertyNotifySink::OnRequestEdit. But even if changes were allowed, we'd have to save the existing sound value, set the new one, have the object play the sound, and then restore the original—a big waste of time. We already assume knowledge about the object, so we might as well use it.

The final two items are the Help and TranslateAccelerator members of IProp- ertyNotifySink. We don't implement any help, so this function returns E_NOTIMPL, but it will never be called because our GetPageInfo didn't provide any help information. BeepProp also returns E_NOTIMPL from TranslateAccelerator, which means that this page lacks a keyboard interface. You'll notice that the Tab key does nothing and that mnemonics do not work (except for the buttons that the frame owns). Supporting these requires that TranslateAccelerator watch for Alt key combinations as well as for Tab and Shift+Tab, setting the focus to the appropriate control for the various keystrokes. This is necessary because the frame's message loop sees all keyboard messages first and can't do anything more than send the keystrokes to the active page. The page dialog will not handle this automatically; you have to implement the keyboard interface directly. The OLE Control Development Kit has facilities for creating property pages with little effort, and MFC provides the implementation of a keyboard interface based on your dialog template.