Dr. Dobb's Sourcebook May/June 1997
Visual programming environments such as Microsoft's Visual Basic and Borland's Delphi have become popular (in part) because visual interaction with objects on the screen is highly intuitive. Users manipulate and set up various user-interface components by assigning appropriate data to the objects' properties. With complex systems, however, managing visual objects can be a chore. Consequently, I'll present an object-oriented model for managing visual objects. This model is based on a flexible Microsoft Foundation Class (MFC) extension cluster implementation. The complete system -- including source code, executables, related files, and sample programs -- is available electronically; see "Programmer's Services," page 3.
To implement the model, I started by defining an abstraction for an object property (see PROP.H, also available electronically). The class CProperty derives from the class CObject, enabling it to be serializable. CProperty encapsulates not only the data type of an object's property, but also the string representation and validation that goes with it. Table 1 describes the methods.
From the CProperty abstraction, I derived other base property classes for (most of) the commonly used built-in Win32-specific data types. Figure 1 shows the inheritance hierarchy for this. All the base property class types require data access methods, such as data set and data get member functions. The signatures for the methods are identical, except that the underlying data type argument and return values are different. Listing One llustrates this by showing how to read/write CIntProperty and CColorProperty instances. CProperty, of course, knows nothing about the data. However, it does stipulate that the base properties provide their own string representations. This is done through the pure virtual ValueString() method. In Listing Two, ValueString() is implemented for CColorProperty. Thus, the data type of a property (COLORREF in Listing One) is independent of its data's string representation.
The overridable ValueStrings() method is useful for the property object to return valid choices that its data can take. This is handy for implicit data validation on assignment -- users are allowed to set a property's value only from the available choices. Listing Three is the ValueStrings() implementation for CBoolProperty. Whether ValueStrings() is implemented or not depends on where the CProperty protected member m_valuetype is assigned. ValueStrings() is required only if m_valuetype is VT_RANGE or VT_CHOICE. I have implemented some custom properties that illustrate this (see CUSTPROP.H, available electronically). For instance, CFontNameProperty overrides this part of its CStringProperty base to return all the names of fonts I want my application to use; see Listing Four.
First, the properties value type is set to VT_CHOICE in the constructor, overriding the default setting of VT_NORMAL. Then, the allowed fonts are returned in the CStringArray variable passed to ValueStrings(). Similarly, CFontSizeProperty (see CUSTPROP.H) sets the CUIntProperty default setting to VT_RANGE and returns the string representations for point sizes between 8 and 24. Note that the property data type is still independent of its string representation.
Clearly, this model is extensible. CFileProperty (see PROP.H), which inherits from CStringProperty, overrides its Data() method to confirm when users want to allow a (as yet nonexistent) filename to be set to the property's data; see Listing Five. Explicit data validation can be performed in this fashion. Another example is the CRpmProperty (see CUSTPROP.H), which only allows an RPM value between 10 and 100 inclusive.
By keeping the encapsulated data types of property objects independent of their string representations, more meaningful versions of properties can be created. CSwitchProperty (see CUSTPROP.H) overrides the ValueStrings() method of its base CBoolProperty (see Listing Six) to return "Off" and "On" instead of the default "False" and "True" string representations of the Boolean value. From these examples, it should be clear how implicit and explicit validation of the underlying data type can be achieved while changing the string representation at will.
Finally, CProperty::StringData() lets all derived classes provide proper data assignment from a string representation of the property data; see CIntProperty::StringData() in Listing Seven.
With the property representation out of the way, I concentrated on the visual object's data representation. On its own, the object's data is simply a bunch of properties. The data, coupled with its visual representation, has a control window associated with it. CControlData (see CTRLDATA.H, available electronically), the abstraction for representing the data, has some stock properties defined on it. More complex objects can be created by deriving other objects from this base class, and adding other base property class members as required. The CreateControl() pure virtual method is used to force any derived object class to provide its visual representation on the screen. For example, a fan object CFan (see FAN.H) has its visual representation implemented through a CFanWnd (see FANW.H and FANW.CPP) instance as depicted in Figure 2.
Just like the CProperty-derived base property classes, CControlData is perfectly extensible. The file-display object CFileText (see FILETXT.H) and its visual representation CFileTextWnd (see FILETXTW.H and FILETXTW.CPP) allow a text file to be displayed on the screen. An extension of this object, CFileTextEx (see FILETXT.H), and its visual representation CFileTextExWnd (see FILETXTW.H and FILETXTW.CPP) allow you to extend that implementation by providing additional capabilities to the original object; for example, the ability to choose the text's typeface and point size. CFileTextEx is subclassed from CFileText, while CFileTextExWnd is derived from CFileTextWnd.
Together, the CProperty-based property type implementations and the CControlData-based control data objects let you create a reusable framework of C++ classes for defining object data and visual representations that are loosely coupled with each other. This loose coupling facilitates code reuse.
The Visual User Interface Manager (VUIM) is a sample application (available electronically) that demonstrates how the aforementioned model can be put to use. My implementation fits well with the MFC Document/View architecture. The document class OnNewDocument() method looks like Listing Eight. Ordinarily, the information for creating the objects would be serialized from disk. OnNewDocument() simply decides to create a CFan, a CFileText, and a CFileTextEx object.
VUIM operates in design and application mode and is, therefore, an SDI application with two views -- one for each mode in which the application executes at any instant; see Figure 3. In design mode, objects are set up by manipulating their properties through a property list (see the accompanying text box, entitled "Property Lists"). The design view looks at the document data and prompts the control objects to create their visual representations through a CreateControl() implementation required by all CControlData derived objects, supplying a CControlDesignWnd* as the parent. The design view maintains the currently selected object at all times through its m_pSCDW member, which is also reflected in the document. Listing Nine is the view's OnInitialUpdate() method.
CControlDesignWnd (see CTRLDWND.H and CTRLDWND.CPP) allows the contained visual representation to be sized and moved. By simply having the visual-representation window disabled in design mode, CControlDesignWnd uses a CRectTracker MFC helper to control how the windows are manipulated on the screen. The visual-control window knows that it is being created in design mode through the last parameter passed to object data class CreateControl() method by CControlDesignWnd::Create(); see Listing Ten.
CreateControl(), in turn, passes this information on to the visual-object control window (see Listing Eleven) for CFileTextEx and CFileTextExWnd. This case is trivial since CFileTextExWnd simply delegates creation to CFileTextWnd. A WM_NCHITTEST handler is used in CControlDesignWnd to return the appropriate code for sizing the control, using the code returned by the CRectTracker instance. WM_SIZE and WM_MOVE handlers are used to handle additional coordinate translation and to keep the object property data in sync with what is displayed in the object's property list.
The application view simply lets the control data object create the visual representation as an enabled child window of the view. Listing Twelve presents the view's OnInitialUpdate() method. This time, the object data class CreateControl() method is invoked with the final bDesign parameter set to FALSE.
VUIM is a good testing tool, letting users switch at will between the two modes of execution. The application was built and tested on Windows 95. Figure 4 shows the application running in design mode and Figure 5 shows it in application mode.
With the model implemented here, you can easily incorporate visual objects in any application. For a visual programming environment, triggering events simply requires public methods in the visual-representation window class acting as event handlers. The classes described here could even be deployed in off-the-shelf applications for configuring dynamic interfaces. For example, instead of the vendor providing an application that can view one or more text files, the underlying component could be exposed to the end user. This way, if an application is intended to be used to view the same three files simultaneously in a proposed MDI application, it could be set up once to bring those files up in design mode.
DDJ
CIntProperty int_p; // an integer (int) property instanceCColorProperty color_p; // a color (COLORREF) property instance int_p.Data (1); // set data to 1 color_p.Data (RGB(255,0,0)); // set data to 'Red' int intval int_p.Data (); // fetch integer value COLORREF colorval color_p.Data (); // fetch color value
void CColorProperty::ValueString (CString& szValue){
szValue.Format ("RGB (%u,%u,%u)",
GetRValue (m_data),
GetGValue (m_data),
GetBValue (m_data));
}
void CBoolProperty::ValueStrings (CStringArray& szValueA){
szValueA.Add ("False");
szValueA.Add ("True");
}
CFontNameProperty::CFontNameProperty (){
m_valuetype VT_CHOICE;
}
void CFontNameProperty::ValueStrings (CStringArray& szValueA)
{
szValueA.Add ("Arial");
szValueA.Add ("Courier New");
szValueA.Add ("Ms Sans Serif");
szValueA.Add ("Ms Serif");
szValueA.Add ("Times New Roman");
}
void Data (LPCTSTR data){
HANDLE handle
CreateFile (data, GENERIC_READ, FILE_SHARE_READ, NULL,
OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
if (handle INVALID_HANDLE_VALUE) {
int ret
AfxMessageBox("Invalid File Assignment.\n Continue assignment?",MB_YESNO);
if (ret IDNO)
return;
}
CStringProperty::Data (data);
}
void CBoolProperty::ValueStrings (CStringArray& szValueA){
szValueA.Add ("False");
szValueA.Add ("True");
}
void CSwitchProperty::ValueStrings (CStringArray& szValueA)
{
szValueA.Add ("Off");
szValueA.Add ("On");
}
void CIntProperty::StringData (LPCTSTR lpszValue){
Data (atoi (lpszValue));
}
BOOL CVuimDoc::OnNewDocument(){
if (!CDocument::OnNewDocument())
return FALSE;
CFileText* pFT new CFileText;
if (pFT) {
pFT->m_top.Data (300);
pFT->m_filename.Data ("c:\\windows\\win.ini");
m_controls.Add (pFT);
}
CFileTextEx* pFTX new CFileTextEx;
if (pFTX) {
pFTX->m_left.Data (300); pFTX->m_top.Data (300);
pFTX->m_color_fg.Data (RGB (192, 192, 192));
pFTX->m_color_bg.Data (RGB (0, 0, 0));
pFTX->m_filename.Data ("c:\\windows\\system.ini");
m_controls.Add (pFTX);
}
CFan* pFan new CFan;
if (pFan) {
pFan->m_color_fg.Data (RGB (255, 0, 255));
m_controls.Add (pFan);
}
m_pSCDW 0;
return TRUE;
}
void CVuimDesignView::OnInitialUpdate(){
CView::OnInitialUpdate();
int nControls GetDocument()->m_controls.GetUpperBound () + 1;
for (int n 0; n < nControls; n ++) {
CControlDesignWnd* pCDW new CControlDesignWnd;
if (pCDW) {
pCDW->Create (GetDocument()->m_controls[n], this, ++m_idcounter);
GetDocument()->m_controls[n]->UpdateControl ();
}
else
AfxMessageBox ("Unable to create visual representation for object");
m_visualwindows.Add (pCDW);
if (!m_pSCDW && pCDW) {
pCDW->m_IsSelected TRUE;
m_pSCDW pCDW;
GetDocument()->m_pSCDW m_pSCDW;
}
}
}
BOOL CControlDesignWnd::Create (CControlData* pData, CWnd* pParent, UINT id){
if (!(m_pData pData)) return FALSE;
CRect rcData;
rcData.left m_pData->m_left.Data ();
rcData.top m_pData->m_top.Data ();
rcData.right rcData.left + m_pData->m_width.Data ();
rcData.bottom rcData.top + m_pData->m_height.Data ();
rcData.left -4; rcData.top -4; rcData.right +4;
rcData.bottom +4;
if (CWnd::Create (NULL, NULL,WS_CHILD|WS_VISIBLE,rcData,pParent, id))
{
m_pCWnd pData->CreateControl (this, id, TRUE);
return TRUE;
}
return FALSE;
}
CWnd* CFileTextEx::CreateControl (CWnd* pParent, UINT id, BOOL bDesignMode){
CFileTextExWnd* pW new CFileTextExWnd;
if (pW)
pW->Create (this, pParent, id, bDesignMode);
m_pW pW;
return m_pW;
}
BOOL CFileTextExWnd::Create (CControlData* pData,
CWnd* pParent, UINT id, BOOL bDesignMode)
{
BOOL bCreated CFileTextWnd::Create (pData, pParent, id, bDesignMode);
if (bCreated)
UpdateFont ();
return bCreated;
}
void CVuimApplicationView::OnInitialUpdate(){
CView::OnInitialUpdate();
int nControls GetDocument()->m_controls.GetUpperBound () + 1;
for (int n 0; n < nControls; n ++) {
CWnd* pW
GetDocument()->m_controls[n]->CreateControl (this,
++m_idcounter, FALSE);
if (!pW)
AfxMessageBox ("Unable to create visual representation for object");
m_visualwindows.Add (pW);
}
}
void CVuimDesignView::OnUpdate(CView* pSender, LPARAM lHint, CObject* pHint){
CControlDesignWnd* pCDW (CControlDesignWnd*)pHint;
if (pHint) {
// only certain aspects of object could be updated
CProperty* pP (CProperty*)lHint;
CControlData* pData pCDW->m_pData;
// update the contained control with changed attributes
pData->ApplyProperties ();
// force a repaint
CRect rc pData->GetRect();
rc.left - 4; rc.top - 4; rc.right + 4; rc.bottom + 4;
m_pSCDW->Refresh (&rc);
return;
}
CView::OnUpdate (pSender, lHint, pHint);
}