[ Home | FAQ | OCX Book | Sites | Articles | Classes ]
How can I develop OLE Controls?
Currently, the best tool to develop OLE Controls is Visual C++ and the MFC libraries. I would imagine that other development environments (e.g., Borland's C++ compiler) will soon allow development of OLE controls. All of my experience is with VC++ and the MFC libraries and so the questions and answers are based on these tools.
Recently, as part of the ActiveX SDK, Microsoft has made available an OLE Control framework
that should work with any Windows-based C++ compiler. If you don't want to use MFC or if MFC
is overkill for your control, you should check this out. Eventually the ActiveX portion of this FAQ will
contain details on this particular topic.
How are OLE Controls (OCXs) different from VBXs?
VBXs and OLE controls are more similar than different from a functionality standpoint. The original goal of the OLE Control standard was to replace the VBX standard. VBXs are very much tied to both the 16-bit and Intel (x86) architecture. To work with Win32 on various platforms (e.g.,Mac,Alpha, etc.) the architecture had to be redesigned. Around the time the guys at Microsoft started working on VB 4.0 various OLE technologies were also being developed. OLE, which is implemented via Microsoft's Component Object Model (COM), provides system services that facilitate the development of component software. VBXs were the first widely used software components and OLE seemed to fit right into the design. So the OLE spec was augmented to include some additional interfaces that would make the development of VBX-like components easier (An example is the concept of event communication between OLE components).
So, from a functionality standpoint VBXs and OLE Controls were engineered to be equivalent, but in the process the underlying architecture was drastically changed. To effectively develop OLE Controls you need to be pretty familiar with COM and OLE, although MFC can hide much of OLE's complexity. If you don't use MFC and do all of the work yourself, your in for quite a bit of work. At least until someone produces a tool better than MFC (which I think is just fine) for their development.
Another important difference between VBXs and OLE controls is that the OLE Control standard is an open one. The internal VBX architecture was not documented by Microsoft and so it was difficult for vendors to provide VBX support in their development tools. This is not the case with the (more open) OLE Control standard. There are quite a few tools that support the USE of OLE Controls, and this is a major benefit both to control developers and control users.
What development environments support the use of OLE Controls?
The following development products support the use of OLE controls.
OLE controls are contained within control containers. These containers (e.g., a VB form) are responsible for providing and managing a control site for each OLE control within the container. Since the container provides this and other services to a control, it maintains certain information about the control that control users may want to access and modify. A good example of this is the actual position of a control within its container. A control is contained and has no (inherent) knowledge of where it is located on a form.
To present a consistent interface to a control user and to provide a way for a control container to modify the functionality of an embedded control, the OLE control standard describes an extended control. The container creates this extended control and then uses COM containment and aggregation to encapsulate the functionality of the original OLE control. Containment and Aggregation are COM techniques that are similar to the C++ concept of class composition. This allows a container to augment the original code by adding new properties such as Visible, Top, and Name. Also, the container may choose to hide existing properties that it deems unnecessary or wishes to re-implement. Currently, the OLE control standard defines five extended properties: Name, Visible, Parent, Cancel, and Default. A container may also add, remove, or change methods and events using the extended control.
The definition of ambient is "surrounding or encircling" and this precisely describes the purpose of a container's ambient properties. The OLE Control standard defines a set of ambient properties that are read-only characteristics of a control container. Ambient properties typically provide default values for properties of a control. A good example is a containers ambient font. To provide a uniform and aesthetically pleasing interface, a control should initially use the ambient font provided by the container. If the control user specifically wants a different font for a specific control it is easily changed via a property browser. BackColor, Font, and DisplayName are some examples of ambients described by the OLE Control standard. MFC provides easy-to-use methods to access these properties.
UserMode is one of the most important ambient properties. It indicates to a control whether or not the container is in design-mode or run-mode. Based on this property, A control's behavior typically changes significantly.
Of course, containers don't have to provide ambient properties, so you should code for those cases when your control is not provided a default property value. The MFC-based CDK does a pretty good job of this.
How do I draw my controls in 3-D?
If your control is only for the Windows 95 environment, you can use a combination of the WS_EX_CLIENTEDGE window style bit (if you are subclassing an existing control) (e.g., EDIT) and/or the Win32 DrawEdge function. DrawEdge can be used to create a 3-D appearance for just about anything. If you need you control to be 3-D in all environments you will have to use the CTL3D*.DLL functions. Here is some code that draws a 3-D rectangle around control based on the standard Windows EDIT control.
void CEeditCtrl::DrawDesign( CDC* pdc, const CRect& rcBounds ) { ... #ifdef _WIN32 RECT rect; rect = rcBounds; ::DrawEdge( pdc->GetSafeHdc(), &rect, EDGE_SUNKEN, BF_RECT | BF_ADJUST ); #endif ... }
The Microsoft knowledge base now has an article that discusses this as well. It's article Q113898
How do I make a property only available at run-time?
It's easy to disallow modification of your controls properties when in design mode. COleControl provides two methods that are useful in this situation: AmbientUserMode and GetNotSupported. The AmbientUserMode property indicates whether or not the container is in design mode or run mode, and the GetNotSupported method is a shorthand way to throw and OLE Automation exception with the CTL_E_GETNOTSUPPORTED error. These techniques are all illustrated below.
void CPipeCtrl::SetPipeType(short nNewValue) { // Don't allow setting of the property at run-time // This isn't absolutely necessary, but it's an example // of a property that cannot be modified when running. // If you were to allow modification of the control's mode // during run-time. We would have to ensure that any active // pipe connections were cleaned up, etc. if ( AmbientUserMode() ) ThrowError( CTL_E_SETNOTSUPPORTEDATRUNTIME, "You can't change the PipeType property at runtime" ); m_sPipeType = nNewValue; SetModifiedFlag(); } BSTR CPipeCtrl::GetErrorMsg() { // Most containers that provide property browsers (e.g. VB) // will trap this exception and will not display the property // in the property browser. This is just what we want. // If we're not in run mode don't allow anyone to get the // properties value. if ( AmbientUserMode() == FALSE ) GetNotSupported(); return m_strError.AllocSysString(); }
Using the GetNotSupported method as in the above example also indicates to Visual Basic 4.0 that the property should not be displayed within its property browser. Other containers, like VC++ 4.0, do not have property browsers so the best way to disallow modification of properties at design-time is to not provide access to them through your control's custom property page.
How do I restrict the size of a control?
To disable or modify the sizing of a control you must override the COleControl::OnSetExtent method. Here's some code from one of my example programs (CLOCK.OCX) available from my OLE Control website (http:://www.sky.net/~toma):
BOOL CClockCtrl::OnSetExtent( LPSIZEL lpSizeL ) { // Some containers can't handle the control directly // modifying the provided lpSizeL structure. So we // create a copy to modify and pass it on to the COleControl // implementation SIZEL sizeL; // Ensure that the clock is always sized // as 200x200 pixels CDC cdc; cdc.CreateCompatibleDC( NULL ); CSize size( 200, 200 ); // Convert the device points to HIMETRIC // OLE uses HIMETRICE measurements cdc.DPtoHIMETRIC( &size ); sizeL.cx = size.cx; sizeL.cy = size.cy; // Call the parent implementation return COleControl::OnSetExtent( &sizeL ); }
The above code ensures that the clock is always 200x200 device units. You can also restrict the shape of your control using the technique above. Just modify the incoming SIZEL structure to suit your purpose and most containers will honor the new size.
Can I build a base control class and use C++ inheritance to derive other controls?
Yes, it can be done and is fairly easy. The tricky part involves setting up the MFC-based macros that are used by AppWizard and ClassWizard. When deriving from the base class you have to cut-and-paste to build the ODL file properly, use specific base-class DISPIDs, etc. All of this is explained in detail in Microsoft Knowledgebase article Q138411. An example is also provided (SHAPES2.EXE) that illustrates the techniques.
What steps are required to convert a VC++ 2.2 OCX project to Visual C++ 4.0?
Here are the steps along with some caveats:
///////////////////////////////////////////////////////////////////////////// // CMyCtrl::OnDraw - Drawing function void CMyCtrl::OnDraw(CDC* pdc, const CRect& rcBounds, const CRect& rcInvalid) { ... // Get the text and draw it pdc->DrawText( InternalGetText(), -1, rcBounds, DT_LEFT | DT_WORDBREAK ); // This worked in VC++ 2.x, but it won't work now. You will get a 2664 error because // we are passing a const reference to a method that requires a non-const pointer // Create a copy in the method call and everything is fine pdc->DrawText( InternalGetText(), -1, CRect( rcBounds ), DT_LEFT | DT_WORDBREAK ); ... }
First you need to include the MFC AFXPRIV.H file. This file contains various MFC implementation items. By using the items in this file, you may have to make changes when newer versions of MFC are released (i.e., they are privileged, specific for MFC's use).
#include "stdafx.h" // To support Unicode conversion in 4.0 #if ( _MFC_VER >= 0x400 ) #include <afxpriv.h> #endif
Wherever you need to convert from an ANSI string to Unicode you need to add an additional line:
USES_CONVERSION
This declares some local variables that the T2COLE and OLE2TC macros use. Here's how to use them:
// CMyCtrl::CMyCtrlFactory::VerifyUserLicense - // Checks for existence of a user license BOOL CMyCtrl::CMyCtrlFactory::VerifyUserLicense() { // Convert the file contents to Unicode for ver 4.0 #if ( _MFC_VER >= 0x400 ) // Use the MFC macros to convert the string to Unicode USES_CONVERSION LPCOLESTR lpOleStr = T2COLE( _szLicString ); return AfxVerifyLicFile(AfxGetInstanceHandle(), _szLicFileName, lpOleStr ); #else // Here's the old 2.x way return AfxVerifyLicFile(AfxGetInstanceHandle(), _szLicFileName, _szLicString); #endif } ///////////////////////////////////////////////////////////////////////////// // CMyCtrl::CMyCtrlFactory::GetLicenseKey - // Returns a runtime licensing key BOOL CMyCtrl::CMyCtrlFactory::GetLicenseKey(DWORD dwReserved,BSTR FAR* pbstrKey) { if (pbstrKey == NULL) return FALSE; #if ( _MFC_VER >= 0x400 ) // Use the MFC macros to convert the string to Unicode USES_CONVERSION LPCOLESTR lpOleStr = T2COLE( _szLicString ); *pbstrKey = SysAllocString( lpOleStr ); #else *pbstrKey = SysAllocString(_szLicString); #endif return (*pbstrKey != NULL); }
That should handle most (basic) version differences encountered during the conversion process. Let me know if there are items that I've left out.
How can I communicate with other controls in the container?
To communicate with other OLE controls within a container, you need to use the IOleItemContainer::EnumObjects method. COleControl provides a method, GetClientSite, that provides access to the IOleClientSite interface. Through this you can get a pointer to the IOleItemContainer interface. Once you have a pointer to this interface you can enumerate over the contained controls. Here's some code:
void CMyCtrl::EnumControls() { LPOLECONTAINER pContainer = NULL; // Get a pointer to the IOleItemContainer interface HRESULT hr = GetClientSite()->GetContainer( &pContainer ); if ( SUCCEEDED( hr )) { // Types of objects to enum DWORD dwFlags = OLECONTF_ONLYIFRUNNING | OLECONTF_EMBEDDINGS | OLECONTF_ONLYUSER; LPENUMUNKNOWN pEnumUnknown = NULL; hr = pContainer->EnumObjects( dwFlags, &pEnumUnknown ); if ( SUCCEEDED( hr )) { LPUNKNOWN pNextControl = NULL; // Loop through the controls while( SUCCEEDED( hr ) && pEnumUnknown->Next( 1, &pNextControl, NULL ) == S_OK) { LPDISPATCH pDispatch = NULL; // Get the IDispatch of the control hr = pNextControl->QueryInterface( IID_IDispatch, (LPVOID*) &pDispatch ); if ( SUCCEEDED( hr )) { COleDispatchDriver PropDispDriver; DISPID dwDispID; // Use automation to access various properties and methods USES_CONVERSION; LPCOLESTR lpOleStr = T2COLE( "SomeProperty" ); if (SUCCEEDED( pDispatch->GetIDsOfNames(IID_NULL, (LPOLESTR*)&lpOleStr, 1, 0, &dwDispID))) { PropDispDriver.AttachDispatch( pDispatch, FALSE); UINT uiCount; PropDispDriver.GetProperty( dwDispID, VT_I4, &uiCount ); PropDispDriver.DetachDispatch(); pNextControl->Release(); } } } pEnumUnknown->Release(); } } }
The above example demonstrates accessing the IDispatch of all of the controls within the container. There are many other things you could do as well. To identify the controls you're looking for, you could implement a custom interface within the (target) control and then QueryInterface to find it. You could also look for a specific CLSID of a control after retrieving the IOleObject interface. You should be able to do just about anything once you've got the control that you're looking for.
How can I provide an Align property for my control?
The Align property (and functionality) is provided by the container. All you have to do to enable this property for your control is to set the OLEMISC_ALIGNABLE status bit and re-register the control.
How do I return an array of items from my control?
To return an array of items, you can use an OLE Automation SafeArray. There isn't room to discuss all of the features of SafeArrays here, so I'll just briefly cover how to use them in an Automation method. There is a tremendous amount of documentation that comes with VC++ that covers the various SafeArray APIs, etc.
Add a method to your control that takes a VARIANT pointer as a parameter. A variant is a generic data type that can hold values or pointers to other more specific OLE automation types. One of the data types that can be contained within a variant is a SafeArray. You can have an array of shorts, longs, BSTRs, Dates, etc. In the method you need to allocate a SAFEARRAY, allocate space for the items, and then populate the array with these values. You then need to initialize the VARIANT structure. Here's some code that creates a SAFEARRAY of BSTR elements As with all automation data, the server allocates the storage and the client (e.g., VB) is responsible for the deallocation.
void CMyControl::GetArray( VARIANT FAR* pVariant ) { // Get the number of items int nCount = GetCount(); // Create a safe array of type BSTR // cElements of the first item indicates the size of the array SAFEARRAYBOUND saBound[1]; SAFEARRAY* pSA; saBound[0].cElements = nCount; saBound[0].lLbound = 0; pSA = SafeArrayCreate( VT_BSTR, 1, saBound ); for( long i = 0; i < nCount; i++ ) { BSTR bstr; // Get the next item, create a BSTR, and // stuff it in the array. GetItem returns a CString. bstr = GetItem( i ).AllocSysString(); SafeArrayPutElement( pSA, &i, bstr ); ::SysFreeString( bstr ); } // Init the variant VariantInit( pVariant ); // Specify its type and value pVariant->vt = VT_ARRAY | VT_BSTR; pVariant->parray = pSA; }
The Visual Basic code to access the elements of the array would look something like this:
Dim t As Variant Dim i as Integer MyControl1.GetArray t For i = 0 To MyControl1.Count - 1 ListBox.AddItem t( i ) Next i
How do I change the actual Name value of my control (i.e, VB's Name property)?
The Name property that Visual Basic uses is the control's actual coclass name. To quickly change this exposed name, just modify your control's coclass interface name. The example below shows where this is located in the .ODL file. I've changed the name from Ccc to SomethingElse.
// Class information for CCccCtrl [ uuid(3B082A53-6888-11CF-A4EE-524153480001), helpstring("Ccc"), control ] coclass SomethingElse { [default] dispinterface _DCcc; [default, source] dispinterface _DCccEvents; };