Objective Grid Control Architecture
Objective Grid consists of several cells that display information in the grid window. Each cell is associated with a cell type object. Some examples of cell types are edit box, static text, combo box, list box, radio button, push button and check box. As we mentioned earlier, in Objective Grid, each cell type is implemented through a cell type class derived from CGXControl. CGXControl is an abstract base class that defines the interface between a grid object and a cell type object. Responsibilities of the cell type object are drawing the cell and interpreting user input. Additional interfaces in the control class support find/replace and copy/paste.
MFC already provides classes that wrap many Windows controls and we wanted to reuse these classes as cell types in Objective Grid. For example, the CEdit class could be used as edit field, the CComboBox class as combo box cell type. In general, a programmer can very easily inherit from these window classes. However, with Objective Grid the problem was that these window controls all have a very different interface. Therefore we had to come up with ways in which we could use these controls from the grid in a consistent manner adapting each to our requirement.
The solution to this problem was to adapt the interface of the window class to the CGXControl interface. We did this in two ways:
1. By multiply inheriting from the CGXControl interface and the window class.
2. By composing a CWnd instance within a wrapper control. Since CWnd is a consistent interface that is implemented (or rather available) on all windows controls (For those ActiveX people, 'Windowed', Windows controls).
Figure 115 shows the resulting class hierarchy for controls in Objective Grid. The implementation of complex controls like CListBox, CEdit, CComboBox and CRichEditCtrl could be reused by multiply inheriting from CGXControl and the window class. You can also observe that primitive controls like push button or check box are implemented directly in Objective Grid.
Figure 115 – Control classes hierarchy
The pure CGXControl approach
The grid control classes, CGXEditControl, CGXListBox, CGXComboBoxWnd and CGXRichEditCtrl (among several others) use multiple inheritance to adapt the CWnd interface to the CGXControl interface. Let us take CGXEditControl as an example. The implementation of the other controls is very similar.
Figure 116 – CGXEditControl implementation
CGXEditControl is not only an adapter that adapts the functionality of the CEdit class to the grid, it also provides a lot of functionality not provided by the CEdit control class. For example, CGXEditControl is responsible for hiding, showing and setting the focus to the cell, changing the text in the window control and the implementation of Find/Replace, Cut/Paste and text formatting.
The nice part is that the editing functionality could be reused from the CEdit class. Once the focus is set to the edit window, the windows edit control lets the user scroll and edit the text, move the caret, copy/paste text.
There are two types of interfaces in the CGXControl class. Some functions are called from the grid window (e.g. Init(), Draw()) and their implementations will translate the call to the edit window, others will be called from within the control and translate the call to the grid object (e.g. Store(), OnValidate(). Therefore the CGXControl object needs a pointer to the grid object.
Let us look at some important interfaces that implement the core of this control.
Functions that are called from the grid and translate into calls to the edit window
virtual void
Init(ROWCOL nRow, ROWCOL nCol);
This function is called from the grid when the user has moved the current cell to a new position. This function will initialize the contents of the edit window with the data of the current cell.
virtual void
Draw(CDC* pDC, CRect rect, ROWCOL nRow, ROWCOL nCol,
const CGXStyle& style, const CGXStyle* pStandardStyle);
This is the function that adapts the behavior of the edit window to the grid. The Draw() function is only called from the grid. It will position the edit window in the grid window, show the window and set the focus to it. After the focus has been set to the edit window, the edit window will continue interpreting user actions like keystrokes and mouse messages.
Draw() operates in two different ways. It will be called for every cell in the grid, but only one cell in a grid can have the focus. Therefore, Draw() checks if the row (nRow) and column (nCol) coordinates passed to the function are the same coordinates as the current cell's coordinates. Only if the coordinates match with the current cell's coordinates, Draw() will show the edit window and set the focus to it. All other cells will only be drawn static. That means only the text is drawn to the screen and no editing functionality is needed.
virtual BOOL
GetValue(CString& strResult);
GetValue() is called from the grid to determine the current value in the edit control. An interesting aspect of this function is that it combines the very different behavior of many controls. For example, the value of the edit window is the text displayed in the cell, but for a list box or combo box it is the index of the selected item and not the displayed text. Therefore, CGXListBox and CGXComboBoxWnd have special implementations of this function that adapts their behavior to the grid.
virtual void
SetValue(LPCTSTR pszRawValue);
SetValue() is called from the grid and is the converse to GetValue(). It updates the displayed text in the edit window.
virtual BOOL
LButtonDown(UINT nFlags, CPoint pt, UINT nHitState);
virtual BOOL
KeyPressed(UINT nMessage, UINT nChar,
UINT nRepCnt = 1, UINT flags = 0);
Mouse and keyboard messages are delegated from the grid to the CGXControl class (for that cell) when the edit window does not already have the focus (for example this is how tab keys are handled when no cell has focus). Once the focus has been set to the edit window, these messages don't need to be delegated any more because the messages will be sent directly to the edit window from the MS Windows.
Functions called from within the control which translate the call to the grid object
virtual BOOL
Store();
This function is called from within the control when the user has changed the cell contents and is about to leave the current cell. The function writes changes back into the cell data object in the grid.
virtual BOOL
OnValidate();
This function is called from within the control when the user is about to leave the current cell. It sends a notification to the grid window to give the grid a chance to validate the cell's contents. If the value is invalid, a message box will appear and the current cell will remain at its old position.
virtual CRect
GetCellRect(ROWCOL nRow, ROWCOL nCol);
This function is called from within the control and determines the cell rectangle for the given coordinates which needs to be computed in the grid object because only the grid object knows the row heights and column widths of other rows and columns.
Other interfaces in the CGXControl class handle special events in the grid and provide feedback about changes in the control (e.g. OnModifyCell will be called when the user modifies text).
The Window Wrapper Approach
This approach is used by the CGXWndWrapper class. This class can be used to host any CWnd-based object in the grid. CGXWndWrapper relies on object composition.
Figure 117 – CGXWndWrapper implementation
CGXWndWrapper is a kind of "one-way" adapter in opposite to the implementation of class adapters like CGXEditControl. Only interfaces that are called from the grid window (e.g. Init(), Draw()) could be implemented because the CWnd object (the composed object) does not know anything about the grid object. This has the advantage that any kind of windows control can be used through the CGXWndWrapper class but has the disadvantage that several limitations arise because the CWnd cannot send feedback about its state or changes to the grid (except through the limited CWnd interface).
Another disadvantage is that there is no way of transferring data between the grid and the contained object because the CWnd class does not provide a consistent interface (the manipulation interfaces are specific to the control) which lets the grid exchange data with the control. But this problem could be easily solved by providing special CGXWndWrapper derivatives that know how to transfer data with special kinds of Windows controls (e.g. a CGXListBoxWrapper). We do this in the grid to adapt ActiveX controls to the grid.
As there is no way to get feedback from the contained object, the only important interface for the CGXWndWrapper class is:
virtual void
Draw(CDC* pDC, CRect rect, ROWCOL nRow, ROWCOL nCol,
const CGXStyle& style, const CGXStyle* pStandardStyle);
This is the function that primarily adapts the general behavior of any CWnd to the grid. The Draw() function will position the contained object window in the grid, show the window and set the focus to it. After the focus has been set to the window, the window will continue interpreting user actions like keystrokes and mouse messages.
We have illustrated both approaches that were used in creating grid controls. The pure CGXControl (both multiple inheritance and plain derivation from CGXControl) is preferred over the object wrapper (CGXWndWrapper approach). Some of the issues that relate to the use of these discrete approaches are listed below.
1. The pure CGXControl approach allows us to have special adaptation for the particular controls. This essentially enables data transfer to and from the control. This results in the possibility that one control object can be reused between several cells. In fact this is used in the grid and is the basis for Objective Grid's control sharing architecture that is explained later in this chapter.
2. While it is often easy to port a control into the grid with the window wrapper approach it is often more difficult to maintain the control (besides this being resource intensive as mentioned above)
There are some situations when the window wrapper method may be the only usable (or more usable) method. In general this is the case with controls that implement special functionality that they retain as part of their internal state. For example take the case of ActiveX control containers that are used as controls. To have a system to serialize and de-serialize the data that is associated with these and to retain this in the grid would be more work that to use window wrapper and single instance per cell approach. This is also the more easily workable approach with ActiveX controls.