Kevin P. Welch
The ability to create and formally install custom controls is a feature of the Microsoft WindowsÔ graphical environment Version 3.0 that will be of great interest to software developers. This ability permits the developer to extend the user interface of Windows1 and encourages the creation of small, self-contained, reusable software objects that can be shared between applications.
This article describes the design and definition of new control classes, discusses the new interface supported by Windows 3.0 for such installable controls, and demonstrates the use of the interface with a sample Page selection control. The sample control can be used with the new dialog editor supplied with the Windows Version 3.0 Software Development Kit (SDK).
A few fairly simple steps greatly simplify the process of designing a new installable control class. While these guidelines are helpful when creating any new window class, they are particularly applicable when designing an installable control.
The first step is to name the control. As with any other window, each control must be associated with a class name. This class name is used in your RC file whenever you wish to use the control explicitly in a dialog box. When naming the control, make sure not to use one of the public class names defined by Windows or other applications. If you do, the registration process for your control class will not succeed.
Second, you must design the user interface. This is usually the most difficult aspect of creating a control. The interface you design for your control determines to a large degree how difficult the control will be for you to implement, and how the end user will perceive it.
When you design a new control, you are extending the functionality of the Windows user interface. Your new control should look and act similarly to those already provided by Windows. For example, you should be consistent with the commonly used mechanisms to indicate selection and keyboard input focus.
You should also avoid device dependencies when you design your control. As you know, Windows is device-independent so it can run on a wide variety of display subsystems, with varying screen-aspect ratios, pixel densities, and color capabilities. For instance, you probably shouldn't put fixed-size bitmaps in the control interface. Since aspect ratios and pixel densities vary widely, the bitmap will probably be scaled or distorted, resulting in a less-than-optimal interface.
Another issue to consider is the use of colors. Predefined system colors work best. Don't assume, for example, that the window background color will always be white and the window frame black. Although it takes a little extra work to incorporate such flexibility into your control, it is almost always necessary when producing controls for commercial use.
Finally, if you are creating an active control, be prepared to support some mechanism to define the input focus.
The third step is to define the interface. Almost all controls interact with other windows in some way or another. Although this interaction can occur using any of the mechanisms supported by Windows, almost all the controls predefined by Windows (except the scroll bar) use a completely message-based interface. Therefore, you must decide which messages your control will work with.
You should also design the style flags your control will support. Whenever a new instance of your control is created, these style flags are combined with flags that are predefined by Windows and are passed to the system as a parameter in the CreateWindow function.
After you have designed the control interface, you should create a public header file defining each supported style, command, and notification message. This header file can then be used by any other module that uses the control.
Fourth, define the instance variables. By their nature, controls must be completely reentrant and totally modular. As a result, the data structures used by your control must be dynamically allocated and readily accessible by each instance. Although you could create your own scheme for defining instance variables, in most cases you should consider allocating extra class or window bytes when the class is registered, or perhaps use property lists or atoms.
The fifth step is to define the window function. Each control must be associated with a window function that processes all related messages. By definition, controls must be able to work with the Windows dialog box manager. Consequently, your window function should handle at least the following messages:
Message Window Function Response
WM_CREATE Initialize any instance variables and perform any other necessary steps to use the control.
WM_DESTROY Release all the resources associated with the control instance, including the removal of property lists and deletion of any atoms.
WM_PAINT Update the client region of the control. Carefully tune the update operations for a variety of display devices, paying special attention to the quality of the result. This is often one of the most tedious aspects of the entire control development process.
If your control is to be capable of receiving input from both the keyboard and the mouse, your window function should also be able to handle the following messages:
Message Window Function Response
WM_GETDLGCODE This message is sent to your control by the dialog box manager. The response to this message determines how the control will be managed inside a dialog box. By responding with one of the following return codes, your control can manage a particular type of input and process the information itself:
DLGC_WANTALLKEYS Intercept all keyboard input
DLGC_WANTARROWS Intercept cursor keys
DLGC_WANTCHARS Intercept WM_CHAR messages
DLGC_WANTMESSAGE Intercept all messages
DLGC_WANTTAB Intercept the TAB key
WM_KEYDOWN Received when a key is held down. By processing the virtual key codes within this message, you can perform any necessary action. When an action is taken that changes the state of the control, be sure to provide appropriate notification codes to the parent window.
WM_KILLFOCUS Received when the input focus changes to another window. If the control displays some sort of highlight, it should be destroyed at this time.
WM_LBUTTONDOWN Received when the user presses the left mouse button inside the control client area. You will want to capture the system input focus, display your highlight, and start capturing mouse movements. If the mouse click results in a new selection, you may also want to notify the parent window of the change.
WM_LBUTTONUP Received when the user releases the left mouse button. If you are capturing mouse movements, you should be careful to release the mouse capture at this time. Note that it is entirely possible to receive extra button-down and button-up messages. If you want to animate a drag operation you will need to capture the mouse and process each of the WM_MOUSEMOVE messages.
WM_SETFOCUS When this message is received, a highlight should indicate that the control has the system input focus.
In addition to these messages, your window function should also be capable of processing the command messages you define as part of your message-based interface. Note that you should be careful to return the appropriate response for each message received, especially when an internal error occurs.
The next step in the process is to define related utility functions. The implementation of even a simple control often requires the definition of several related utility functions that perform often-repeated processes. As with any Windows program, you should structure and organize these functions carefully, preferably placing them in logical groups using separate, discardable code segments.
Finally, you must physically create the control modules. Depending on your needs, you can either link each control object module directly into your application or combine them into a library and let the linker extract the required components. Be aware that if your control uses any resources, you must also incorporate them into your application.
You might also want to modularize your control completely (with all associated resources) into a self-contained dynamic-link library (DLL). This extra degree of encapsulation can be very beneficial, enabling you to use the control simultaneously in several applications. You will also be able to use it with the new Windows 3.0 Dialog Editor by adhering to a few simple standards.
Creating Installable Control Libraries
When creating an installable control you must follow the procedures just outlined, plus a few additional steps that permit the control to exist in a DLL. DLLs form the foundation upon which Windows is built. Unfortunately, DLLs impose certain programming restrictions. Perhaps the most significant is the SS != DS issue. Unlike conventional Windows programs, DLLs have no stack segment and only one data segment. Consequently, when an application calls a function within a DLL, it has to use its own stack segment and the library's data segment. The result is that many of the standard C run-time library functions cannot be used within a DLL.
Library Module Definition File To create an installable control, first define a DLL containing a series of exported functions that are associated with a series of predefined ordinal values. Create the DLL using the following module definition file, where Xxxx represents the class name of the control:
LIBRARY Xxxx
DESCRIPTION Xxxx Control Library'
EXETYPE WINDOWS
STUB WINSTUB.EXE'
CODE MOVEABLE DISCARDABLE SHARED PRELOAD
DATA MOVEABLE SINGLE PRELOAD
HEAPSIZE 1024
EXPORTS
WEP @1 RESIDENTNAME
XxxxInfo() @2
XxxxStyle() @3
XxxxFlags() @4
XxxxWndFn() @5
XxxxDlgFn() @6
You should always use the same predefined ordinal values for each of these exported functions. The XxxxDlgFn function associated with the XxxxStyle function must also be exported since it is used directly by Windows.
Library Entry Points In addition to the module definition file, you will need to create several different source code modules, each containing one or more of the exported functions discussed below. These functions must be present for the Windows 3.0 Dialog Editor to use the control. The next section describes each of these required functions in detail. Note that all the required data structures are defined in CONTROL.H (see Figure 1), one of the files used to build the sample control discussed later in this article.
BOOL FAR PASCAL XxxxInit( hInstance )
The XxxxInit function is responsible for all the initialization needed to use the DLL. This function is normally called by the assembly language entry point to the library. After saving the library instance handle using a global static variable, this function should register the control class and initialize the local heap (if necessary) by calling LocalInit.
Parameter Type Description
hInstance HANDLE Library instance handle
The return value indicates the success or failure of the library initialization. A return value of FALSE indicates that the initialization process has failed; TRUE indicates that the control class has been registered and initialization has succeeded.
HANDLE FAR PASCAL XxxxInfo()
The XxxxInfo function provides the calling application with information about the control library. Based on the information returned, the application can create instances of the control using one of the supported styles. If the control was used with the Windows Dialog Editor, for example, this function could be used to query the library about the different control styles it can support.
The return value is a handle to a CTLINFO data structure. This information becomes the property of the caller and must be explicitly released using the GlobalFree function when it is no longer needed. If there is insufficient memory available to define this structure, a NULL handle is returned.
The CTLINFO structure, defined in CONTROL.H (see Figure 1), specifies the class name and version number and lists each of the supported control styles with suggested size information and a short description.
/* general style & size definitions */
#define CT_STD 0x0000
#define CTLTYPES 12
#define CTLDESCR 22
#define CTLCLASS 20
#define CTLTITLE 94
typedef struct {
WORD wType;
WORD wWidth;
WORD wHeight;
DWORD dwStyle;
char szDescr[CTLDESCR];
} CTLTYPE;
typedef struct {
WORD wVersion;
WORD wCtlTypes;
char szClass[CTLCLASS];
char szTitle[CTLTITLE];
char szReserved[10];
CTLTYPE Type[CTLTYPES];
} CTLINFO;
typedef CTLINFO * PCTLINFO;
typedef CTLINFO FAR * LPCTLINFO;
The CTLTYPE structure has the following fields:
Field Description
wType Defines the type of control. While only the CT_STD option is currently supported, other types may be defined in the future.
wWidth Defines the suggested width of the control. If the most significant bit is zero, the remaining bits specify the default width in RC coordinates; otherwise, the remaining bits specify the default width in pixels. One horizontal RC unit is the width of the system font divided by four.
wHeight Defines the suggested height of the control. If the most significant bit is zero, the remaining bits specify the default height in RC coordinates; otherwise, the remaining bits specify the default height in pixels. One vertical RC unit is the height of the system font divided by eight.
dwStyle Specifies the initial style flags used to obtain this particular control variant. This value includes both the control-defined style flags in the low-order word and the Windows-defined flags in the high-order word.
szDescr Defines the name to be used by other applications when referencing this particular variant of the base control class.
The CTLINFO structure contains the following fields:
Field Description
wVersion Specifies the control version number. Although you can start your numbering scheme from 1, most implementations use the lower two digits to represent minor releases. Using this scheme, the first version would be numbered 100.
wCtlTypes Specifies the number of control types supported by this class. This value should always be greater than zero and less than or equal to CTLTYPES.
szClass Specifies with a NULL-terminated string the control class name supported by the DLL.
szTitle Specifies with a NULL-terminated string copyright or author information relating to the control library.
Type Specifies an array of CTLTYPE data structures containing information relating to each of the control types supported by the class.
BOOL FAR PASCAL XxxxStyle( hWnd, hCtlStyle, lpfnStrToId, lpfnIdToStr )
The XxxxStyle function enables the calling application to edit the style of a particular control. When this function is called, a modal dialog box should be displayed that permits the user to edit the CTLSTYLE parameters. The display of this dialog box should be consistent with Style dialog boxes provided by existing controls.
Parameter Type Description
hwnd HWND Handle to parent window
hCtlStyle HANDLE Handle to CTLSTYLE data
lpfnStrToId LPFNSTRTOID String-to-ID conversion function
lpfnIdToStr LPFNIDTOSTR ID-to-string conversion function
The XxxxStyle function returns a value of TRUE if the CTLSTYLE structure was changed. Otherwise, it returns FALSE.
The CTLSTYLE structure specifies the various attributes of the selected control, including the current style flags, location, dimensions, and associated text.
#define CTLCLASS 20
#define CTLTITLE 94
typedef struct {
WORD wX;
WORD wY;
WORD wCx;
WORD wCy;
WORD wId;
DWORD dwStyle;
char szClass[CTLCLASS];
char szTitle[CTLTITLE];
} CTLSTYLE;
typedef CTLSTYLE * PCTLSTYLE;
typedef CTLSTYLE FAR * LPCTLSTYLE;
The CTLSTYLE structure contains the following fields:
Field Description
wX Specifies in screen coordinates the X origin of the control relative to the client region of the parent window.
wY Specifies in screen coordinates the Y origin of the control relative to the client region of the parent window.
wCx Specifies the current width of the control in screen coordinates.
wCy Specifies the current height of the control in screen coordinates.
wId Specifies the current ID number of the control. Although you can allow the user to change this value, in most situations changes should be made in conjunction with the string-to-ID and ID-to-string conversion functions provided as parameters (see below).
dwStyle Specifies the current style of the control. The low-order word contains the control-specific flags; the high-order word contains the Windows-specific flags. You should let the user change these flags to any values supported by your control library.
szClass Specifies a NULL-terminated string representing the name of the current control class. Don't allow the user to edit this field-it is provided for informational purposes only.
szTitle Specifies the text associated with the control with a NULL-terminated string. This text is usually displayed inside the control or may be used to store other associated information required by the control.
The lpfnStrToId and lpfnIdToStr functions require a little further explanation. The primary purpose of the XxxxStyle function is to display a dialog box through which the user can edit the various parameters associated with the control. As you know, each control is associated with an ID value. This value is used by the parent window or dialog box to keep track of each of its child windows.
/* ID to string translation function prototypes */
typedef WORD (FAR PASCAL *LPFNIDTOSTR)( WORD, LPSTR, WORD );
typedef DWORD (FAR PASCAL *LPFNSTRTOID)( LPSTR );
The lpfnIdToStr function, an optional parameter provided by the calling application, enables you to translate the ID from the CTLSTYLE data structure to a text string. This text string can then be displayed in place of the numeric value. The first parameter is the control ID; the second, a long pointer to an empty text string; the third parameter is the maximum length of this string. The value returned by this function is the number of characters copied to the string. If a value of zero is returned, it can be assumed that the function call failed.
The lpfnStrToId function operates exactly in reverse. This function takes a string containing an ID value as a parameter and returns a doubleword result. If the LOWORD of the result is nonzero, then the HIWORD contains the new control ID value (which you can use to update the CTLSTYLE structure). If the LOWORD of the result is zero, the ID string was undefined.
The calling application (in this case, usually the Dialog Editor) keeps track of the ID strings and their corresponding values. The control style function accesses this information using the lpfnStrToId and lpfnIdToStr functions. The calling application can produce a header file containing these ID strings and values for use by the application under development.
WORD FAR PASCAL XxxxFlags( wStyle, lpszStyle, wMaxString )
The XxxxFlags function translates the control style flags provided into a corresponding text string for output to a text or RC file. Note that you should return the same control style definitions specified in the #include file that accompanies your control library.
Parameter Type Description
wStyle WORD Control style
lpszStyle LPSTR Style flags string
wMaxString WORD Maximum string length
The return value is the length of the resulting lpszStyle string. If an error occurs, zero is returned.
LONG FAR PASCAL XxxxWndFn( hwnd, wMsg, wParam, lParam )
The XxxxWndFn function is the conventional window procedure responsible for processing all the messages sent to the control. The messages that this function should be capable of handling were described earlier.
Parameter Type Description
hwnd HWND Handle to the control window
wMsg WORD Message identifier
wParam WORD Additional message data
lParam LONG Additional message data
The return value indicates the result of message processing. The possible return values depend on the actual message sent.
BOOL FAR PASCAL XxxxDlgFn( hDlg, wMsg, wParam, lParam )
The XxxxDlgFn function is a conventional window procedure that processes all the messages sent to the control style dialog box. This dialog box should display the control ID, title, and other related information. The user should be able to define different control styles and change the associated ID and title with this dialog box.
Parameter Type Description
hDlg HWND Handle to the style dialog box
wMsg WORD Message identifier
wParam WORD Additional message data
lParam LONG Additional message data
The return value indicates the result of message processing. If a nonzero value is returned, the dialog box manager will assume that the dialog function has processed the message. If zero is returned, the dialog box manager will process the message.
Building the Control Library After you have defined the control library components, you can create the installable control much as you would any other Windows application. Make sure you use the appropriate compilation flags, verifying especially that SS != DS.
To be consistent, consider structuring the various modules and components of your library in the following manner:
Name Description
Xxxx Make file
Xxxx.H Public header file
Xxxx.D Private header file
Xxxx.DEF Module definition file
Xxxx.RC Resource file
Xxxx0.ASM DLL entry module
Xxxx1.C Initialization module
Xxxx2.C Message processing module
Xxxx3.C Style dialog box module
Xxxx4.C Information module
Xxxx5.C Style flags module
Sample Control
To put all this in perspective, let's work through a sample control. The Page selection control is a relatively simple control that facilitates the selection of a numbered page within a small range of pages. You can select a particular page by clicking the mouse inside one of the control's page symbols or by using the cursor when the control has the keyboard input focus (see Figure 2). You might use this control to support the selection of a particular page in a document, specify a print range, or perhaps enable the user to select a particular page of a multipage data entry form.
The library PAGE.DLL completely defines the installable Page control. When an instance is created, the Page control responds to the following set of messages, as defined in the public header file, PAGE.H (see Figure 3).
PM_SETRANGE
Used to set the number of pages displayed inside the control (numbered from 1 to n). Note that the currently selected page is not reset. The default range is set to the maximum number of pages that can be displayed.
Parameter Description
wParam The new number of pages
lParam (not used)
The return value is nonzero if the new range was successfully defined. If an error occurs, zero is returned.
PM_GETRANGE
Used to retrieve the current number of pages displayed inside the control.
Parameter Description
wParam (not used)
lParam (not used)
The return value indicates the number of pages displayed.
PM_SETSELECT
Used to define the currently selected page.
Parameter Description
wParam An index for the newly selected page in the range of 0 to n1, where n is the current page range
lParam (not used)
The return value is the index of the newly selected page; in most cases this is identical to the wParam value.
PM_GETSELECT
Used to retrieve the index to the currently selected page.
Parameter Description
wParam (not used)
lParam (not used)
The return value is the index to the currently selected page (from 0 to n1).
PN_SELCHANGE
Whenever the user selects a particular page using the control, this notification message is sent to the parent window, indicating that the selected page has changed. Notification messages are used to inform the control's parent window of the control of changes that the parent may be interested in. As with all control notification messages, the selection change notification message is sent to the parent window in the following format:
Parameter Description
wMsg WM_COMMAND
wParam Page control ID
lParam Low-order word is the window handle
High-order word is the notification code (PN_SELCHANGE)
Page Control Components
The Page control library is constructed from the following files.
File Description Figure Number
PAGE Make file 4
PAGE.H Public header file 3
PAGE.D Private header file 5
PAGE.DEF Module definition file 6
PAGE.RC Resource file 7
PAGE0.ASM DLL entry module 8
PAGE1.C Initialization module 9
PAGE2.C Message processing module 10
PAGE3.C Style dialog box module 12
PAGE4.C Information module 13
PAGE5.C Style flags module 14
The make file, PAGE (see Figure 4), is almost identical to that used by any Windows application except that it uses the Aw and D_WINDLL compile flags. These flags instruct the C compiler to generate code in a DLL format with SS != DS.
The next file, PAGE.H (see Figure 3), is the public header file associated with the control library. Because this file is designed to be used by applications using the control, it contains only the control commands, notification codes, and function prototypes.
The private header file PAGE.D (see Figure 5) is associated with PAGE.H. This file defines the values used throughout the control library that do not need to be made public. For example, the following fragment from PAGE.D defines various constants that are used when registering the Page window class and when returning information about the control:
#define PAGE_CLASSEXTRA 0
#define PAGE_WNDEXTRA 18
#define PAGE_VERSION 100
#define PAGE_NAME "Page"
#define PAGE_COLOR NULL
#define PAGE_WNDSTYLE WS_CHILD|WS_VISIBLE|WS_TABSTOP
#define PAGE_CLASSSTYLE CS_HREDRAW|CS_VREDRAW|CS_DBLCLKS|CS_GLOBALCLASS
Note the specification of the CS_DBLCLKS and CS_GLOBALCLASS class style flags-they are extremely important. If you don't specify CS_DBLCLKS, you will not be able to bring up the control style dialog box inside the dialog editor by double-clicking inside the control. The CS_GLOBALCLASS flag is also necessary, as the control class must be made publicly available.
PAGE.DEF (see Figure 6), the module definition file, is almost identical to that described previously in the article, except that obviously Xxxx is replaced with the class name Page.
The library resource file, PAGE.RC (see Figure 7), defines the PageStyle dialog box used by the PageDlgFn function. This dialog box displays the information passed to the PageStyle function, allowing the user to define the control identifier when used in conjunction with the dialog editor.
PAGE0.ASM (see Figure 8) is the assembly language entry point to the DLL. Remember that in order to create DLLs, an assembly language entry point must call the control initialization function PageInit whenever the control is loaded.
The PAGE1.C module (see Figure 9) contains the PageInit function. If this is the first attempt to initialize the control, this function defines the Page control class and registers it. If the registration process succeeds, the library instance handle is saved and a value of TRUE is returned. The instance handle is saved because it will be used throughout the other modules in the library.
The next module, PAGE2.C (see Figure 10), is the most complex. It contains the window message processing function PageWndFn. Most of this function (and its associated helper functions) is quite typical, but two sections might need further explanation.
The first section requiring explanation is the update of the Page control window, which is performed when a WM_PAINT message is received or when the user selects a different page number. Although most of this code consists of straightforward Graphics Device Interface (GDI) calls, you should know how the update colors are defined.
The update colors involve the use of a new pen and background brush. The pen is created using the current system window frame color and selected into the display device context. The background brush is obtained from the parent window, enabling it to define the background color of the control, just as Windows does with the system buttons.
/* define appropriate pen */
hNewPen = CreatePen( PS_SOLID, 1, GetSysColor(COLOR_WINDOWFRAME) );
hOldPen = (hNewPen) ? SelectObject(Ps.hdc,hNewPen) : NULL;
/* define appropriate brush and text colors*/
hNewBrush = (HBRUSH)SendMessage(
PARENT (hwnd),
WM_CTLCOLOR,
Ps.hdc,
MAKELONG(hwnd,CTLCOLOR_BTN)
);
hOldBrush = hNewBrush ? SelectObject(Ps.hdc,hNewBrush) : NULL;
Creating the three-dimensional look of each page icon inside the control (see Figure 11), also requires some explanation. Unless this is done carefully, the result may not look right when viewed with different system colors and display drivers. Generally speaking, three-dimensional objects in Windows 3.0 are filled using a solid brush based on the COLOR_BTNFACE system color. The top and left edges are colored with a solid brush based on the COLOR_WINDOW system color, while the bottom and right edges are colored with a brush based on the COLOR_BTNSHADOW system color. These values must be used for the three-dimensional look to appear correctly on a monochrome display.
The DrawPage function uses these system colors when drawing each page icon in the following manner:
/* draw page outline */
hOldBrush = SelectObject( hDC, CreateSolidBrush(GetSysColor(COLOR_BTNFACE))
);
Polygon( hDC, ptPage, 7 );
DeleteObject( SelectObject(hDC,hOldBrush) );
/* draw white border */
hOldPen = SelectObject( hDC, CreatePen(PS_SOLID,1,GetSysColor(COLOR_WINDOW))
);
MoveTo( hDC, ptPage[5].x + 1, ptPage[5].y - 1 );
LineTo( hDC, ptPage[5].x + 1, ptPage[6].y + 1 );
LineTo( hDC, ptPage[1].x, ptPage[1].y + 1 );
DeleteObject( SelectObject(hDC,hOldPen) );
/* draw shadow */
hOldPen = SelectObject( hDC,
CreatePen(PS_SOLID,1,GetSysColor(COLOR_BTNSHADOW)) );
MoveTo( hDC, ptPage[5].x + 1, ptPage[5].y - 1 );
LineTo( hDC, ptPage[4].x - 1, ptPage[4].y - 1 );
LineTo( hDC, ptPage[3].x - 1, ptPage[3].y );
MoveTo( hDC, ptPage[5].x + 2, ptPage[5].y - 2 );
LineTo( hDC, ptPage[4].x - 2, ptPage[4].y - 2 );
LineTo( hDC, ptPage[3].x - 2, ptPage[3].y );
DeleteObject( SelectObject(hDC,hOldPen) );
The control style dialog box module, PAGE3.C (see Figure 12), contains the PageStyle and PageDlgFn functions. The PageStyle function is relatively simple; it sets a few temporary global variables and brings up the control style dialog box.
The PageDlgFn function is also quite simple, but further explanation might be necessary in two areas. The first involves the initial display of the control ID value. If the lpfnIdToStr and lpfnStrToId parameters have been defined, the control ID is passed to the lpfnIdToStr function and converted into a string. The resulting text string is then displayed in place of the numeric value.
/* define page id field */
if ( lpfnIdToStrTemp && lpfnStrToIdTemp ) {
(*lpfnIdToStrTemp)( lpCtlStyle->wId,
(LPSTR)szId, sizeof(szId) );
SetDlgItemText( hDlg, ID_IDENTIFER, szId );
}
When the user closes the style dialog box, the updated text string is retrieved and translated back into an identifier using the lpfnStrToId function. If the LOWORD of the returned double word is nonzero, then the HIWORD contains the new control ID.
/* retrieve identifier string */
wSize = GetDlgItemText( hDlg, ID_IDENTIFIER, szId,
sizeof(szId) );
szId[wSize] = NULL;
/* translate identifier */
dwResult = (*lpfnStrToId)( (LPSTR)szId );
if ( LOWORD( dwResult ) )
lpCtlStyle->wId = HIWORD( dwResult );
The PAGE4.C module (see Figure 13) contains the PageInfo function that is responsible for allocating and defining a CTLINFO data structure. Once this structure has been defined, a handle is returned to the calling application. It then becomes the calling application's responsibility to release the memory associated with this handle when it is no longer needed.
The last file, PAGE5.C (see Figure 14), contains the PageFlags function. As mentioned previously, this function is responsible for converting control style flags into their text string equivalents. In situations where multiple flags are appropriate, they can be combined with the bitwise OR operator. In this case, since the Page control does not define any private style flags, no action is taken.
To build the Page control library, you need the Windows 3.0 SDK, Microsoft C 5.1 or 6.0, and Microsoft Macro Assembler. The result will be PAGE.DLL, a DLL that you can experiment with using the Windows 3.0 Dialog editor or reference in your own application using the associated import library.
Using Installable Controls in Your Applications
Before you can use an installable control in an application, you must include the appropriate header file and register the control with the system. If your control is defined in a module linked into your application, you must ensure that the initialization function is called before the control class is used in a window or dialog box. If your control is incorporated into a library with the mechanisms described previously, you can load the library (and thus register the class) in one of three ways.
The first method is referencing the library initialization function explicitly in the import section of the module definition file (DEF) associated with your application. If you do this, Windows will automatically load all the referenced libraries when it loads your application. If you wanted to use the Page control in your application, for example, the following statement in your DEF file would cause the library to be loaded:
IMPORTS
Page.PageInit
The second method involves an explicit reference to one of the control library functions inside your application. For example, in certain situations you might want to call the control information or style functions. If you create an import library for your installable control (using the IMPLIB facility) and include it with the list of object libraries searched during the linking of your application, Windows will automatically load the control library when resolving these external references. In the process of loading the library, the control initialization function will be called, and your control will be registered with the system.
The third approach is a manual one in which you explicitly load and free the desired control library. The advantage of this approach is that you completely control how the library is used, even to the extent of enabling the user to determine which library is used (much as is done by the Dialog Editor).
The following code fragment demonstrates this:
/* definitions */
HANDLE hCtlLibrary;
/* load the library */
hCtlLibrary = LoadLibrary( "PAGE.DLL" );
if ( hCtlLibrary ) {
/* USE THE CONTROL HERE */
/* release the library */
FreeLibrary( hCtlLibrary );
}
Using the Control
Once you have made sure that your library will be loaded successfully, you can use the new control throughout your application, even as a child of an existing window. If you wish, you can create child windows based on the control class using an explicit CreateWindow function call. The following code fragment shows how an instance of the Page control can be created:
hWnd = CreateWindow(
"Page",
"",
WS_CHILD | WS_TABSTOP,
8,
8,
64,
64,
hWndParent,
ID_PAGE,
hInstance,
NULL
);
Note the specification of the control class ("Page"), the use of appropriate style flags, and the definition of a unique ID value that will be associated with the control.
Once the control has been created, the parent window will be responsible for managing many of its activities, such as displaying or hiding the control, repositioning it whenever the parent window size changes, and processing all notification messages generated by the control.
Using installable controls in dialog boxes is easier than creating them yourself, because Windows performs many of the routine tasks for you. Windows creates dialog boxes from templates defined in your resource file. Once your control library has been loaded, all you need to do to use the control is reference the public header file associated with it in your resource file, and use the following control placement statement inside each dialog box template:
CONTROL <text>, <id>, <class>, <flags>,
<x>, <y>, <width>, <height>
For example, the following statements define two instances of the Page control:
CONTROL "", ID_PAGE1, "Page",
WS_CHILD|WS_TABSTOP, 8, 8, 64, 64
CONTROL "", ID_PAGE2, "Page",
WS_CHILD, 24, 24, 64, 64
Using Installable Controls with the Dialog Editor
Another way to use installable controls in your applications is with the new Windows 3.0 Dialog Editor. Before you can use an installable control in the Dialog Editor, you must formally make it part of the Windows environment. One way to do this is to edit your WIN.INI file manually, creating a section that specifies the association between each control class name and the DLL responsible for it. To add the Page control to Windows this way, you would add a statement such as the following to your WIN.INI file:
PAGE=C:\WINDOWS\SYSTEM\PAGE.DLL
The second and perhaps easier method is to load the Dialog Editor and select Add Custom Control under the File menu. When you do this, the dialog box in Figure 15 will be displayed. Using this dialog box you can specify installable controls with either of two options. The first option specifies controls permanently and requires the use of a DLL. First, copy the DLL to your SYSTEM subdirectory. Then select the first option in the dialog box and enter the full pathname of the library supporting the control. This option causes an entry to be made automatically in your WIN.INI file associating the control class with this DLL (as previously described). From then on, this control can be used in the Dialog Editor in the same manner as any predefined system control. The advantage of this process is that the control is represented accurately on the screen and you can select the control styles defined by the library.
The second option is temporary and does not require the use of a DLL to define the control. If you choose this option, simply enter the name of the control class in the edit field provided. The Dialog Editor will then let you work with this control class. It will be represented on the screen simply as a rectangle with the class name centered inside (see Figure 16). Temporary control classes must be redefined each time you run the Dialog Editor.
Once you have defined an installable control, you can start using it inside your dialog boxes. To place a custom control in the new dialog box, select the Custom option under the Control menu. This will display the dialog box shown in Figure 17. Select a control class from this dialog box. A preview of the control you have selected is displayed on the right side of the dialog box. You will then be returned to the editor to place the control in the same manner used for predefined system controls. Or you can simply use the Dialog Editor toolbox. Make the toolbox visible by selecting the Toolbox option under the Options menu. Select the tool in the lower right corner to display the control selection dialog box (see Figure 17).
You can change the style of a custom control after it has been placed. Select the control, then either double-click inside the control or select the Styles option under the Edit menu. A dialog box, defined by the control's DLL itself, will be displayed. For example, when the Page control is selected, the dialog box in Figure 18 is displayed. Using this dialog box you can define the identifier associated with the control. For more sophisticated controls you can experiment with the various styles supported by the control class. In some cases, this dialog box will also display a small sample of the current control style in the lower right corner. When you have specified the new control style, click OK. The control in your dialog box will be updated automatically to reflect the changes you have specified.
You can also use the Dialog Editor to remove temporary or permanent control classes. Select the Remove Custom Control option under the File menu; a dialog box that lets you select the control class to be removed will be displayed. If the control was permanent, the Dialog Editor will release the associated DLL and update your WIN.INI file. However, the library may remain in memory if other applications are still using this control class. Also, you cannot remove a class (either permanent or temporary) in the dialog box currently being edited. When you remove custom controls from the Dialog Editor, the DLL associated with the removed control is not deleted from your hard disk.
Conclusion
Although the process of designing, creating, and using an installable control can be quite complicated, the end result is well worth the trouble. With a little effort, you should be able to create a custom collection of installable controls that you can use over and over again when building Windows applications.
1 For ease of reading, "Windows" refers to the Microsoft Windows graphical environment. "Windows" refers only to this Microsoft product and is not intended to refer to such products generally.