[ Home | FAQ | OCX Book | Sites ]  

OLE Controls Frequently Asked Questions
Property Page Questions

Last Update: May 24, 1996

How can I provide a list of valid options (i.e., enumerated properties) for a property?

Static enumerated properties are easy to implement. All you need to do is add some entries to your control's ODL file. The standard BorderStyle property is a good example. Below is the code added to the CLOCK.OCX example of Chapter 9.

library ClockLib
{
  importlib(STDOLE_TLB);
  importlib(STDTYPE_TLB);

   typedef enum
   {
      [helpstring("None")] None = 0,
      [helpstring("Single")] Single = 1
   } enumBorderStyle;

  //  Primary dispatch interface for CClockCtrl

  [ uuid(CC57ABB5-AD4E-11CE-B44B-08005A564718),
  helpstring("Dispatch interface for Clock Control"), hidden ]
  dispinterface _DClock
  {
    properties:
      // NOTE - ClassWizard will maintain property information here.
      //    Use extreme caution when editing this section.
      //{{AFX_ODL_PROP(CClockCtrl)
      ...
      [id(DISPID_BORDERSTYLE), bindable, requestedit] enumBorderStyle BorderStyle;
      ...
      //}}AFX_ODL_PROP
  ...
  }

Within Visual Basic's property browser only the two enumerated options will be provided. You should also use these in your control's custom property page as well. You can do this using the DDP/DDX_CbIndex functions within the DoDataExchange method. Something like this:

void CClockPropPage::DoDataExchange(CDataExchange* pDX)
{
  //{{AFX_DATA_MAP(CClockPropPage)
  DDP_CBIndex(pDX, IDC_BORDERSTYLE, m_sBorderStyle, _T("BorderStyle") );
  DDX_CBIndex(pDX, IDC_BORDERSTYLE, m_sBorderStyle);
  ...
  //}}AFX_DATA_MAP
  DDP_PostProcessing(pDX);
}

You can also provide enumerated property values dynamically. This is a little more complicated and is best used when the enumerated values change, or are dependent on other properties within your control. Details on implementing dynamic enumerated properties can be found in Microsoft knowledge base article Q137354

Back to top


How can I cause Visual Basic's property viewer to update when a change is made via my custom property page?

If you want your design-time property changes via your control's property page to immediately update Visual Basic's property browser, all you have to do is call the BoundPropertyChanged method like this.

void CYourCtrl::SetSomeProperty(short nNewValue)
{
   m_sSomeProperty = nNewValue;

   // Update VB's property browser
   BoundPropertyChanged( DISPID_UNKNOWN );
   SetModifiedFlag();
}

BoundPropertyChanged informs the container, via IPropertyNotifySink::OnChanged, that a control's property value has changed. The parameter provides the dispatch ID of the specific property. DISPID_UNKNOWN can be used to force an update of all known properties.

Back to top


How do I access the IDispatch of a control from within its property page

It's really easy. COlePropertyPage::GetObjectArray returns an array of IDispatches associated with the property page. GetObjectArray returns an array of interfaces because a property page may be connected to multiple controls. This can occur if the user selects multiple controls within a container and then wants to access the properties that are common among the selected controls. Here's some code to access the array of IDispatches:

// Get the property page's IDispatch array
ULONG ulObjects;
LPDISPATCH* lpDispatchArray = GetObjectArray( &ulObjects );
ASSERT( lpDispatchArray != NULL );

Back to top


What if the DDP functions aren't enough. I need more general access to my control?

The DDP functions only work with automation supported types. You can talk directly with your control by using the COlePropertyPage::GetObjectArray method. The code below shows updating a control with automation.

   // Get the property page's IDispatch array
   ULONG ulObjects;
   LPDISPATCH* lpObjectArray = GetObjectArray( &ulObjects );
   ASSERT( lpObjectArray != NULL );

   // I'm assuming there is but one control, mine
   LPDISPATCH lpControl = lpObjectArray[0];

    // Update the control here using automation calls
    COleDispatchDriver PropDispDriver;
    DISPID dwDispID;
    LPCOLESTR lpOleStr = T2COLE( "List" );
    if (SUCCEEDED( lpControl->GetIDsOfNames(IID_NULL, (LPOLESTR*)&lpOleStr,
                        1, 0, &dwDispID)))
     {
        PropDispDriver.AttachDispatch( lpControl, FALSE);
        static BYTE parms[] = VTS_I2 VTS_BSTR;
        PropDispDriver.InvokeHelper( dwDispID, DISPATCH_PROPERTYPUT,
                                      VT_EMPTY, NULL, parms, line, szLine );
        PropDispDriver.DetachDispatch();
   }

You can also get a pointer to your COleControl derived class by using the FromIDispatch method of CCmdTarget like this:

   // Get the property page's IDispatch array
   ULONG ulObjects;
   LPDISPATCH* lpObjectArray = GetObjectArray( &ulObjects );
   ASSERT( lpObjectArray != NULL );

   // I'm assuming there is but one control, mine
   LPDISPATCH lpControl = lpObjectArray[0];

  // Get a pointer to the control object
   CMyCtrl* pControl = (CMyCtrl*) CCmdTarget::FromIDispatch( lpControl );

   // Call a public method in CMyCtrl
   long x = pControl->SomeMethod( 100 );

By communicating directly with the control, you should be able to do just about anything you need to.

Back to top


How do I implement per-property browsing (i.e., a custom "..." property dialog in Visual Basic's browser)?

To provide explicit, edited modification of your control's properties, you can implement per-property browsing. In Visual Basic, per-property browsing provides the ellipses ("...") option within the browser. By clicking the ellipses button, Visual Basic will load a property specific property sheet that allows you to provide very specific property interaction with your control. This may be necessary when your control has properties that aren't native, OLE-supported types. The details of adding this functionality to your controls is detailed in Microsoft Knowledge Base article Q140592

Back to top


How do I add support for the Help button in my property page?

First, of course, you need to create a .HLP file for your properties. Then for each of your custom property pages add a call to SetHelpInfo in the constructor of your property page class. You must provide a short comment for tooltip support, the filename of your .HLP file, and finally the actual help context ID that you would like passed during the WinHelp call. The default implementation of the Help button just calls WinHelp with the parameters provided via SetHelpInfo. If necessary, you can change this default behavior by overriding and implementing the COlePropertyPage::OnHelp method.

Back to top


What is necessary to provide F1 support for my properties within VB's property browser?

To add support for F1 help within various browsers, you need to modify your control's .ODL file. The modifications specify the help context IDs for your control's properties, methods, and events. The steps and keywords required to modify the .ODL file are explained in Microsoft Knowledge Base article Q130275

Back to top


Can I display my control's property page at run-time?

Original Question:
We would like our OCX end users to be able to pop up the design-time OLE property page at run-time, as well. I tried a quick hack to make this happen:

1) I trapped the right-button mouse click
2) In the mouse-click message handler, I invoked COleControl::OnProperties(0, NULL, NULL)

This worked OK, except

a) The Property page was modeless, rather than modal
b) because of (a), I was able to popup several copies of the property page!
c) The property page sometimes would eat the first (activating) mouse click before it would respond.

Now, Visual Basic pops up the properties in a modal way ... is there any way for me to get Visual Basic (or the container, in general) to display the properties at run-time as it does at design time?

Answer:
I did a little research on your problem and here's what I've come up with.

According to the documentation for IOleControlSite, if a control wants to display a property sheet, it must first ask the container to do it, something like this:

   m_pControlSite->ShowPropertyFrame();

This is basically because the container may have an extended control and so would possibly like to add its property pages to those of the control. So, a control should call IOleControlSite::ShowPropertyFrame if it wants to display its property pages. This will allow the container to "handle" the property sheet. If the call returns E_NOTIMPL, then the control must perform the work itself. But, if it returns S_OK, then the container has successfully displayed it and everything should be just fine. This is exactly what COleControl::OnProperties does. You would expect then that all you need to do is call OnProperties from your control when you want to display its property pages. Well, as you've noticed, at least in Visual Basic anyway, the container shows the property sheet modeless. It also allows multiple instances of the property sheet.

To solve the problem, I just cut the code from COleControl::OnProperties and placed in the OnRButtonDown handler and everything seemed to work. It may not be the "recommended way", but it works. I've only tried it in VB though, this approach may not work in other containers (e.g. VC++). Let me know what you find out. Here's the code for OnRButtonDown.

void CMyCtrl::OnRButtonDown( UINT nFlags, CPoint point ) 
{
   CWnd* pWnd = GetParent();
   HWND hWndParent;

   // Various ways to get the parent window
   if ( pWnd )
      hWndParent = pWnd->GetSafeHwnd();
   else
      hWndParent = NULL;
   hWndParent = GetApplicationWindow();

  // Need to include <afxpriv.h> for this macro
  USES_CONVERSION;      
  HRESULT hr;

  LPUNKNOWN pUnk = GetIDispatch(FALSE);
  CWnd* pWndOwner = CWnd::GetSafeOwner(CWnd::FromHandle(hWndParent));
  HWND hWndOwner = pWndOwner->GetSafeHwnd();

  LCID lcid = AmbientLocaleID();

  ULONG cPropPages;
  LPCLSID pclsidPropPages = GetPropPageIDs(cPropPages);
 
  RECT rectParent;
  RECT rectTop;
  ::GetWindowRect(hWndParent, &rectParent);
  ::GetWindowRect(hWndOwner, &rectTop);

  TCHAR szUserType[256];
  GetUserType(szUserType);

  // Display the property page modally
  PreModalDialog(hWndOwner);
  hr = OleCreatePropertyFrame(hWndOwner, rectParent.left - rectTop.left,
         rectParent.top - rectTop.top, T2COLE(szUserType), 1, &pUnk,
         cPropPages, pclsidPropPages, lcid, NULL, 0);
  PostModalDialog(hWndOwner);

Back to top


Can I use the same EDIT field to display different property values from my property page?

Here's the original question:
I am trying to use a list box in my property page to allow the user to dynamically add an object to the control. When the user selects an item from the list, I would like to display the interface for that item on the remaining area of the property page. Specifically, the problem is that I would like to use the same edit box to display the different values for each of the 16 possible objects, without creating 16 different member variables and edit boxes on the property page and switching between them.

For example, say the user wants to add a line object to the control,and the line has a integer width. So the user selects line from the list box (where say, rect, is also an item in the list that has a width property). Then the users should be able to enter the desired line width into the edit box labeled "Width". The problem is, can I use the same edit box to display different property values from the control? It appears to me that I cannot do this without affecting how the properties will be exposed to the container. I seem to be limited this way b/c of the method of data exchange between the property pages and the actual control. I want to expose a width property for each object type and allow the user to change this and have the change correctly map to the right object property. Using the same edit box to display the same property, but that pertains to different objects with the same property name?

Answer:
Yes, but you will have to do most of the work yourself. Instead of using MFC's built in DDX/DDP functions to pass the property values to and from the control, you will have to do it yourself. For starters you should check out the CB.ZIP example on the "General Questions" FAQ. It shows how to communicate directly with the control from the property page. It bypasses the DDX/DDP functions altogether. You'll have to do something similar.

When the user selects an object from the LISTBOX, you will have to retrieve the existing value of the associated property from the control. Actually, you should keep a linked-list or array of the associated property values so the retrieval and update can be handled all together in the DoDataExchange method. This linked-list should be valued when the property page is initially created. Each time the user selects a new item from the LISTBOX, you will take the associated value from the list and display it in the edit field. Later, when the user eventually hits the "Apply" or "OK" buttons on the property page, you will have to spin through the list and pass all of the values back over to the control. One issue is when will you take the value from the entry field and update the list? You may need an local "Apply" button, or you could query and apply the value every time the LISTBOX is changed and whenever the property page "Apply" or "OK" buttons are pushed. Anyway, here is some pseudo code of what you might do:

void CMyPropPage::DoDataExchange(CDataExchange* pDX)
{
   // DDX/DDP stuff left our for clarity.

   // Either set or retrieve the list from the control instance
   if ( pDX->m_bSaveAndValidate )
      UpdateListValues();
   else
      RetrieveListValues();

   DDP_PostProcessing(pDX); 
}

Of course, the real code will be in the Update... and Retrieve... methods. The CB.ZIP example shows you how to do this. It describes how to get a pointer to the actual control instance from inside the property page. This will allow you to call methods from your COleControl-derived class.

Back to top


How can I force an update of a property that I've directly affected in my property page?

Sometimes when developing a property page, you will find it necessary to maintain inter-related properties. The value of one property changes whenever the user modifies the value of another property. In order to ensure that both properties are updated in your control, you should call COlePropertyPage::SetControlStatus. This method takes the ID of the control and a Boolean flag, bDirty. You can ensure that both property values are updated by setting the value of the dependent property and then ensuring that its value is updated when DoDataExchange is called. You can to this by calling SetControlStatus with the dirty flag set to TRUE.

Back to top


How do I get rid of that pesky message saying "Invalid Property Page size..."?

The OLE Control standard only allows two different sizes for property pages. The size of a property page dialog must be either 250x62 Dialog Units (DLUs) or 250x112 DLUs. Anything else, and you'll get that nasty error. A steady hand in the resource editor will solve the problem.

If you really want a property page size that is different than the standard sizes above you can make your property page whatever size you want. The "Invalid Property Page Size" message actually comes from MFC's COlePropertyPage class, and will only display if your control is compiled in "Debug" mode. COlePropertyPage has a private member, m_bNonStandardSize, that is always set to FALSE, but is only declared as part of the _DEBUG version of the class, and the check of the dialog size is only performed when compiled debug. So, if you're willing to put up with the message when debugging, you'll be fine. The release version will not display the message. Check out CTLPPG.CPP for additional details.

Back to top


I don't use the property page feature for my control. Can I disable it?

Here's the orignial question:
If I'm not using the tabbed property pages how do I disable them? In VB, with focus on the control on the form, the left mouse button displays a popup menu with properties... at the end. I would like this to be grayed.

Here's my response:
I played around with this for awhile. Containers call IOleObject::EnumVerbs to populate the right-click popup menu. By default, MFC controls provides one verb: Properties. This is controlled by the ON_OLEVERB message map entry. Below is an example of the default implementation.

BEGIN_MESSAGE_MAP(CMyCtrl, COleControl)
   //{{AFX_MSG_MAP(CMyCtrl)
      ON_WM_CREATE()
   //}}AFX_MSG_MAP
   ON_MESSAGE(OCM_COMMAND, OnOcmCommand)
   ON_OLEVERB(AFX_IDS_VERB_PROPERTIES, OnProperties)
END_MESSAGE_MAP()

So, to disable the ability to invoke property pages you just have to comment out the ON_OLEVERB map entry and your finished. I know of no way to tell the container to "gray" an entry, only to not display it.

There is one strange side effect with the approach above. It appears that Visual Basic will provide an "Activate Object" menu item if none are returned by your control. This menu item seems to do strange things. It appears to activate the control even when in design mode! I didn't mess around with it too long, but Visual Basic seems to be doing something that I've not seen before.

A better approach may be to override the COleControl::OnProperties method and do nothing. Just return TRUE like this:

BOOL CMyCtrl::OnProperties( LPMSG lpMsg, HWND hWndParent, LPCRECT lpRect )
{
   return TRUE;
}

This leaves the "Properties" menu item, but it won't actually do anything. You can get real fancy and throw focus over to Visual Basic's property browser. This is what the standard Visual Basic controls do.

Back to top


What is SetPropText(...) used for?

Here's the original question:
I am using SetPropText(_T("someproperty"),m_someproperty) to communicate between my Custom Property Dialog Box, and the Property sheet in VB. Without it , even with BoundPropertyChanged(..), and DDP... in DoDataExchange(..) my dialog box won't change the property values. There is no Help available on SetPropText(..), I had seen it in some sample code and used it. Can someone enlighten me as to what is the use of SetProText(..), is it an undocumented function, and how to change the properties, custom or stock, from your dialog box.

Answer:
The SetPropText methods are used by MFC to communicate with your *control*. The method is overloaded to take various automation types, and basically provides a shorthand mechanism to get and set control property values from within the property page. I'm not sure what you're trying to do, but it appears that maybe you're going about it the wrong way. If you want Visual Basic's property browser to update, you need the BoundPropertyChanged method in your control's Set method. That should do it. When the property page updates the control, VB will be informed as well.

You can use SetPropText if you need to. It's just shorthand for getting the IDispatch of your control, attaching COleDispatchDriver, and finally get/setting a property. The DDP functions use the various SetPropText methods so they are probably sufficient. I've used SetPropText when I want to communicate with my control outside of the DDP functions, but now I just get a pointer to instance of my control and communicate directly.

Back to top


Typical Disclaimer: This FAQ is copyright (c) 1996 by Tom Armstrong (toma@sky.net). You have the right to copy and distribute this file in any way you see fit as long as this paragraph is included with the distribution. No warranties or claims are made as to the validity of the information contained herein. So use it at your own risk


Send mail to Tom - toma@sky.net