Previous Page TOC Next Page



- 16 -
Creating OLE Controls with Visual C++ 4


This chapter describes the development of OLE controls and is divided into two parts. The first part covers the OLE control development tools that Visual C++ offers the database programmer, and the second part takes you through the process of creating your own OLE control.

This book isn't about writing OLE controls. One or two chapters can't cover everything you need to know about writing (or using) OLE controls. If you're thinking about developing OLE controls, you should consider a book oriented toward OLE controls. (The next chapter teaches you how to use OLE controls, something that most database programmers will find useful.)

This chapter doesn't take into consideration who you're developing an OLE control for. You could be developing OLE controls for your own internal use, for others in your company or organization, or for sale to other programmers. There will be a large market for OLE controls in the future. This could be a field that smart programmers could exploit to their advantage.

OLE Control Development Tools


Supplied with Visual C++ 1.5x and Visual C++ 2.x is a separate development kit called the OLE Controls Development Kit (CDK). This facility is integrated into Visual C++ 4, making Visual C++ 4 an easier-to-use system. Since it can be assumed that some developers will have to develop 16-bit OLE controls using Visual C++ 1.5x (which is included with Visual C++ 4), I've included some information about the CDK and other related tools in this chapter.

This section describes the development tools that are supplied with the OLE control CDK and are built into Visual C++ 4. These tools make the process of developing OLE controls easy and (hopefully) painless, and they vary depending on the platform under which you're developing. For instance, the Make TypeLib (MKTYPLIB) utility isn't called by the programmer directly, except when you're using 16-bit versions of Visual C++. Visual C++ 2's WPS (Windows Process Status) utility, used to view process threads (specifically, to see what DLLs are loaded), isn't used under Windows NT because the PStat and PView utilities are part of the Visual C++ 2.0 package. With Visual C++ 4, the Windows NT system has both PView and PStat, neither of which can be used under Windows 95. The PView95 program is available for Windows 95 users of Visual C++ 4. It can be found on the Visual C++ 4 CD in the directory \MSDEV\BIN\WIN95.



CAUTION

When you're converting OLE controls created with the CDK supplied with Visual C++ 1.5x and 2.x, you should read the topic OLE Controls: Converting a CDK Project to a Visual C++ Project in the Programming with MFC Encyclopedia, which is part of the Visual C++ Books Online documentation.


Unless otherwise noted, the descriptions of the tools in this chapter are for how they run under Visual C++ 4. When a tool works significantly differently under other versions of Visual C++ (including the 16-bit version, 1.5x), the differences are noted.

ControlWizard


This section introduces you to ControlWizard. You use ControlWizard to develop the shell for your OLE controls. Its user interface looks and feels very much like that of AppWizard in Visual C++, because it's an integrated AppWizard in Visual C++ 4. This is a major improvement to earlier versions of Visual C++, in which ControlWizard was a separate application, external to Visual C++'s Developer Studio.

To create a new OLE control, you first start App Wizard and select an OLE control project type, as shown in Figure 16.1. This figure shows that an OLE control is being created; the name for the control has been entered. After it's started, AppWizard's ControlWizard displays the main dialog box, shown in Figure 16.2.

Figure 16.1. AppWizard showing the OLE control project type.

Figure 16.2. ControlWizard's Step 1 of 2 dialog box.

Before you name the new OLE control, you must select the directory in which the new control will be created. When you specify the control's name, that name is used to create the control's subdirectory. If you like, either before or after specifying the control's name, you can change the path to the project's directory.

After you name your new control and specify its location, you can specify its options and configuration. These options and configurations are grouped using two Wizard dialogs. The first is shown in Figure 16.2, and the second is shown in Figure 16.3. These Wizard dialogs are described in the following sections.

Figure 16.3. ControlWizard's Step 2 of 2 dialog box.

OLE ControlWizard's Step 1 of 2 Dialog Box

The following options are available in OLE ControlWizard's first step:

As soon as you've set the desired options for the first step in ControlWizard, you can move to the second step.



NOTE

Remember when using ControlWizard that you can go back and change settings made in previous steps. You aren't locked into any choice until you actually create your project. You can even back up to the initial selection of the project's name and location if you want to.



OLE ControlWizard's Step 2 of 2 Dialog Box

The following options are available in OLE ControlWizard's second and final step. Each control in the project may have different options that can be set in the second step. Also, the second step is where you can modify the names for your project's controls.



NOTE

Regardless of the number of controls in a project, you can rename the controls at the second step. A control's name doesn't have to be the project's name, for example.


Each of the options in the second step is associated with a specific control. That way, you can have controls with different attributes in the same project. The following options may be set for each control.


Table 16.1. Windows controls that an OLE control may subclass.

Control Description
BUTTON The standard button control.
COMBOBOX The standard combo-box control.
EDIT The standard edit control (not the RTF edit control).
LISTBOX The standard list box control.
mscrls_hotkey32 The Win32 hot key control.
mscrls_progress32 The Win32 progress bar control.
mscrls_status32 The Win32 status control.
mscrls_trackbar32 The Win32 track bar control.
mscrls_updown32 The Win32 up-down control.
SCROLLBAR The standard scrollbar control.
STATIC The standard static control.
SysAnimate32 The Win32 animation control.
SysHeader32 The Win32 header control.
SysListView32 The Win32 list view control.
SysTabControl32 The Win32 tab control.
SysTreeView32 The Win32 tree view control.

The Edit Names Dialog Box

To access the Edit Names dialog box, shown in Figure 16.4, click the Edit Names button in ControlWizard's Step 2 of 2 dialog box. It lets you define a number of names associated with each of the controls in your project.

Figure 16.4. ControlWizard's Edit Names dialog box.

The following names and attributes may be changed:

For the control itself, the following names may be specified:

For the control's property page, the following may be specified:


Generating Your OLE Control

After you've defined your new OLE control, you can click the Finish button in ControlWizard's Step 2 of 2 dialog box. When you click Finish, the New Product Information dialog box, shown in Figure 16.5, appears. This dialog shows the various options, names, and configurations that you selected in the OLE control creation stage.

Figure 16.5. ControlWizard's New Product Information dialog box.

Review this dialog box and, if all is in order, click OK. Visual C++ 4 will then open the new control in Developer Studio.

Sometimes you'll want to create both 16-bit and 32-bit versions of your OLE control. To create a 16-bit version of your control, you must use Visual C++ 1.5x and its 16-bit OLE Control Development Kit. This might prove to be difficult, and I don't recommend it unless you have no alternative and must create 16-bit versions of your OLE controls. If you try to use Visual C++ 4 to create a 16-bit control, you must make sure that the control's project name is no more than four characters long, because the project's filenames must be short under Visual C++ 1.5x. You also need to create a new .MAK file for the control, a nontrivial task.

Register/Unregister Control


Register Control and Unregister Control are two separate menu options on the Tools menu in versions of Visual C++ prior to 4. (If you don't have an Unregister Control option, you can add it to your Tools menu using the Customize option and the REGSVR32 program.) However, both options call the registration program (REGSVR.EXE for Windows 3.x and REGSVR32.EXE for Windows NT and Windows 95) that is found in Visual C++'s \MSDEV\BIN directory. Visual C++ 4's controls automatically register themselves when created (as part of the project build process), but you can have a Register Control menu option on your Visual C++ 4 Tools menu too.

The REGSVR32 program takes as an argument the name of the control to be registered or unregistered. This name generally is the OLE control's DLL filename with the file extension. REGSVR32 also takes the following optional parameters, which aren't case-specific:

REGSVR32 is both small and efficient. It has no features or options other than those just described. REGSVR32 can't be redistributed. Other developers who are using your controls must use their own tools to register and install controls. The source for REGSVR32 is available on the Visual C++ CD in the directory \MSDEV\SAMPLES\MFC\CONTROLS\REGSVR.

Test Container


Test Container (TSTCON16 or TSTCON32, depending on the version of Windows to which you're targeting your OLE controls) is a very useful utility that lets you embed OLE controls and test their functionality. Test Container doesn't have all the usability of a full-fledged application, but it does let you test most of the aspects of your control. However, you still must test your control in its final environment, such as in an Access form.

Test Container runs externally to Visual C++ and therefore doesn't come in a debugging version. Generally, when you debug your control, you load it into Visual C++ and then start OLE Control Test Container from Visual C++'s Tools menu.

When Test Container is loaded and running, the first thing you'll do is insert an OLE control. After you insert a control into Test Container's document space, you can work with the control's interface. (You'll need to test both the user interface and the OLE control's interface with the container.)

Test Container's user interface is both a menu bar and a toolbar. The menu bar offers the options described in Table 16.2.

Table 16.2. Test Container's menu options.

Menu Option Description
File Save to Stream Saves the currently selected OLE control to a stream. A stream can hold one or more OLE controls, and an OLE control can be saved to the stream more than once. After an OLE control has been saved to a stream, the Save to Substorage selection is disabled until the stream has been cleared.

Save to Substorage Saves the currently selected OLE control to substorage. Substorage can hold one or more OLE controls, and an OLE control can be saved to substorage more than once. After an OLE control has been saved to substorage, the Save to Stream selection is disabled until substorage has been cleared.

Load Loads the currently saved stream or substorage.

Save Property Set Saves the properties of the currently selected OLE control to a document file.

Load Property Set Creates a new OLE control and initializes it from the previously saved document file.

Register Controls Registers a new OLE control. Performs the same function as Visual C++'s Tools | Register Control menu option.

Exit Ends the Test Container program.
Edit Insert OLE Control... Opens the Insert OLE control dialog box and lets you select a new control to be added to Test Container's document space.

Delete Deletes the currently selected OLE control.

Delete All Deletes all the OLE controls that are in Test Container's document.

Set Ambient Properties The Ambient Properties dialog box sets the container properties that affect all OLE controls. Properties such as UserMode, BackColor, Font, and so on can be set in this dialog box.

View Event List Lets you specify the logging of events, such as the clicking of the mouse button.

Invoke Methods Lets you test the OLE control's methods. All OLE controls that are created using ControlWizard at least have an About method, which displays the control's About box.

Draw Metafile Draws the control's metafile so that you can see the effects of metafile drawing of your control's client area.

Embedded Object Functions Provides a submenu of choices:


Hide:


Hides the control and puts it in the Loaded state. Calls COleClientItem::Activate(OLEIVERB _HIDE, ...).


Primary Verb:


Invokes the control's primary verb.


Activate:


Activates the control and puts it in the Loaded state. Calls COleClientItem::Activate(OLEIVERB_PRIMARY, ...).


UI Activate:


Puts the control in the UI Active state.


Close:


Closes the control and puts it in the Loaded state. Calls COleClientItem::Close().


Deactivate:


Deactivates the control and puts it in the Loaded state. Calls COleClientItem::Deactivate(). Also discards the contents of the Undo buffer.


Deactivate UI Only:


Restores Test Container's user interface to its original state. Calls COleClientItem::DeactivateUI().


Open:


Opens the control in stand-alone mode and puts it in the Open state. Calls COleClientItem::Activate(OLEIVERB_OPEN, ...).


Reactivate and Undo:


Reactivates a control and puts it in the Loaded state. Calls COleClientItem::ReactivateAndUndo().


Run:


Runs the control and puts it in the Loaded state. Calls COleClientItem::Run().


Show:


Activates the control and puts it in the UI Active state. Calls COleClientItem::Activate(OLEIVERB_SHOW, ...).

Properties Shows the currently active OLE control's property sheet.
View Toolbar Shows or hides the toolbar.

Status Bar Shows or hides the status bar.

Event Log Displays the event log.

Notification Log Displays the notification log.

Saved Control Stream Displays a dump of the currently saved stream. This dump is divided into sections, one per saved control.

Properties Shows or hides the Properties dialog box for the selected control.
Options Passive Container Mode Tells the container not to automatically change the control's state. Selecting Passive Container Mode automatically deselects Simulated Design Mode.

Simulated Design Mode Tells the container to automatically change the control's state. Selecting Simulated Design Mode automatically deselects Passive Container Mode.

Freeze Events Freezes or releases the on-event firing for all controls.

Honor ACTIVEATEWHENVISIBLE Turns support on or off for the OLEMISC_ACTIVATEWHENVISIBLE flag.

Honor INVISIBLEATRUNTIME Turns support on or off for the OLEMISC_INVISIBLEATRUNTIME flag. This option is supported only by TSTCON16.
Help Contents Provides help on Test Container by displaying the Contents help page.

About Test Container Displays Test Container's About box.

Test Container also has a customized status bar that provides information about the currently selected control and Test Container. The following is a list of the panes in the status bar from left to right:

Figure 16.6 shows Test Container's main window and its client area with the calendar custom control. Notice how the calendar looks in Test Container. This is almost exactly how it would appear when embedded into an application.

Figure 16.6. Test Container's main window, with Access 7's calendar control loaded.

Test Container also provides a dialog box to configure the logging of events, as shown in Figure 16.7. You can tell Test Container which events you want to log (see the description of the Event Log that follows). You can select each event and choose to have it logged or not logged. Also, there are buttons to turn event logging on or off for all events. Compare the events shown in Figure 16.7 with the Event Log window shown in Figure 16.8 (Test Container's logging windows).

Figure 16.7. Test Container's Events for... dialog box.

Figure 16.8. Test Container's logging windows.

With Test Container's status windows, you can see the OLE control's Event Log and Notification Log. Both the Event Log and the Notification Log dialog boxes are modeless and can be left displayed for the entire Test Container session.

The Event Log window shows the events for the currently active control. For example, the Circ3 control posts events for mouse clicks, both inside and outside the circle. For mouse clicks inside the circle, the event routines are configured to show the relative coordinates of the mouse cursor.

The Notification Log window notifies you of changes in the controls' properties. Figure 16.9 shows the notifications received when the Calendar control has had its Today property changed.

Figure 16.9. Test Container's Notification Log with changes to Calendar.

Also part of the Notification Log dialog box are radio buttons to configure the response to the OnRequestEdit() call. Choices include the following:


WPS


WPS is a utility—part of the 16-bit version of the CDK (supplied with Visual C++ 1.5x and found on the MSDN Level I CD)—that lets you view both running tasks and loaded modules. You can perform a number of functions with WPS, such as freeing a task or a module, saving the list of currently running tasks and loaded modules in a file, or forcing the loading of a module. Although this utility is intended for use with Windows 3.x, it will run under Windows 95.



NOTE

Programmers using Windows 95 should use the PView95 utility instead of WPS. PView95 works in the same manner as Windows NT's PView program.


Programmers who are developing OLE controls can use WPS in several ways. First, when a container program (such as Access) uses an OLE control, the container program won't unload the control when the control terminates. You can't make changes to a DLL file while it's loaded, so you must force Windows to unload the DLL before rebuilding it with Visual C++.

As a side benefit, WPS is useful as both a general process viewer and as a method to delete an undesirable process.

WARNING

As with any system-level utility, WPS can bring Windows to a screeching halt. If you free a module or task that Windows needs to run, the world as Windows knows it will come to an end. Processes and tasks to steer clear of include those that have the names kernel and KRNL, as well as other names that you can associate with the Windows system components.

WPS presents itself as a window split horizontally into two unequal parts. Processes appear in the top third of the window, and loaded modules appear in the lower two-thirds. One small flaw is that you can't change the size of the two parts of the window.

WPS offers several menus, which are listed in Table 16.3.

Table 16.3. WPS menu options.

Menu Option Description
File Load Module... Lets you force the loading of a module (an .EXE or a .DLL file).

Dump Lets you save the main window's contents in a standard text file format. The saved file's contents can then be reviewed later.

About WPS Displays the About dialog box, giving the authors' names and their company.

Exit Ends the current WPS session.
Options Free Module Frees the module that is currently selected.

Free Task Frees the task that is currently selected.

Font Lets you change the font that WPS uses. Selecting a smaller font lets you display more lines of information.
Edit Copy Copies the contents of the WPS window to the clipboard.
Update!
Tells WPS to update its display.

Figure 16.10 shows WPS running on a system. Notice that there are more tasks running than will fit into the Tasks part of the window (the top part), and many more modules loaded than will fit into the Modules part (the bottom part). However, because both of these lists are sorted by name, it isn't difficult to find a specific task or module.

Figure 16.10. WPS's main window.

When you look at Figure 16.10 you can see several columns. Table 16.4 describes the columns and how they're used.

Table 16.4. Columns in WPS.

Column Description
Tasks Section
Name The name of the running task. Generally, this is the eight-character program name. Often it's the same as the task's eight-character filename.
hTask The handle for the task (see hParent).
hParent The hTask for the parent task of this task. If the parent task is zero, the task is a 32-bit task owned by Windows.
nEvents The count of the hardware resources (such as communications ports) for the task.
hInst The instance handle for the task.
Version The version number from the task's version resource (if the task has a version resource). If there is no version resource, this field is blank.
Exe The fully qualified filename for the task. Some versions of Windows use UNC names for files that are on shared, nonlocal network drives.
Modules Section
Name The name of the loaded module. This name may be the same as the module's filename.
hModule The handle for the module.
Usage The usage count (lock count) that indicates the number of references to this module. Most nonsystem modules have a usage count of 1 or 2. System modules may have usage counts of 50 or more.
Version The version number from the task's version resource (if the task has a version resource). If there is no version resource, this field is blank.
Exe The fully qualified filename for the task. Some versions of Windows use UIC names for files that are on shared, nonlocal network drives.

All in all, I've found WPS to be a very useful tool for looking at what's loaded and running under Windows. I find it amazing how much is going on in Windows that I wouldn't be aware of otherwise.

PView95


The PView95 program is used to view processes while running under Windows 95. This program was developed to complement the Windows NT program PView. Using PView95 is easy, because it has a simple user interface that includes the menus listed in Table 16.5.

Table 16.5. PView95 menu options.

Menu Option Description
File Exit Ends the current PView95 session.
Process Refresh Updates the list of current tasks.

Kill Kills the selected task.
Help About Displays the PView95 About box.

Figure 16.11 shows PView95 running on a system. Notice that there are both 16-bit and 32-bit applications running under Windows 95. Both of these lists are sorted by name, so it isn't difficult to find a specific task or module.

Figure 16.11. PView95's main window.

Figure 16.11 has several columns. Table 16.6 lists these columns and describes how they're used.

Table 16.6. Columns in PView95.

Column Description
Process (Top) Section
Process The name of the running process. Generally, this is the eight-character program name, often with an extension of .EXE or .DLL.
PID The process identifier, a 32-bit value assigned to identify this process.
Base Priority A priority value, typically 8 for most processes (KERNEL32 has a base priority of 13). Threads of a process can have a lower or higher priority as needed. A value of 28 usually indicates a critical thread or process.
Num. Threads The number of threads owned by this process.
Type The program's type: 16-bit or 32-bit. Windows 95 is a mixed-type operating system that has both 16-bit and 32-bit components.
Full Path The full path to the executable file (either .EXE or .DLL) for this process.
Threads (Bottom) Section
TID The thread identifier.
Owning PID The process identifier for this thread's owner.
Thread Priority The priority for this thread, relative to the owning process's priority. The priority value is based on the process priority and can be either lower or higher than the process's priority. Each of a process's threads may have a lower or higher priority as needed. A value of 28 usually indicates a critical thread or process.

All in all, I've found PView95 to be a useful tool for looking at what processes and threads are loaded and running under Windows 95.

Make TypeLib


Make TypeLib is a utility that is run only as a separate step (by you, the programmer) under 16-bit versions of Visual C++. You don't use it when you're developing OLE controls using Visual C++ 2 or Visual C++ 4 under Windows 95 or Windows NT. Instead, the 32-bit versions of Visual C++ create the typelib as part of the project's build process by calling MKTYPLIB directly as part of the project's make process. The Make TypeLib command is found on Developer Studio's Tools menu when you're using Visual C++ 1.5. Make TypeLib invokes the MKTYPLIB program.

A typelib lets other applications determine which properties, methods, and events your OLE control will support. MKTYPLIB's input files have a file type of ODL, whereas output typelib files have a file type of TLB.

When you use ControlWizard to create an OLE control, an initial ODL file is created for you. ClassWizard updates this file as you add new properties, methods, or events to your OLE control. When you're developing under Visual C++ 1.5, you must use Tools | Make TypeLib to update the typelib file.

The MKTYPLIB program has a number of options, which are described in Table 16.7. These options are specified when you start MKTYPLIB from the Visual C++ 1.5 Tools menu. MKTYPLIB can also be started from a DOS prompt. These options may then be specified in the command line. The current version of MKTYPLIB is 2.01.

Table 16.7. MKTYPLIB options.

Option Description
/help or /? Displays a message specifying the options for MKTYPLIB.
/tlb <filename> Specifies the name of the output type library file. If it's not specified, the output file defaults to the same name as the input file, with a file type of TLB.
/h [filename] Specifies the output .H filename.
/<system> Available in both versions of MKTYPLIB. Use this option to specify which type of TLB is produced. Valid types of typelibs include WIN16, WIN32, MAC, MIPS, ALPHA, PPC, and PPC32. Defaults to WIN32 for the 32-bit version of MKTYPLIB and to WIN16 for the 16-bit version.
/align <#> Available in the 32-bit version of MKTYPLIB only. Use this option to override the default alignment setting.
/o filename Tells MKTYPLIB to redirect its output to the specified file. Normally, MKTYPLIB sends the output to the stdout device.
/nologo Tells MKTYPLIB not to display the startup logo or copyright message.
/w0 Tells MKTYPLIB to disable all warnings.
/nocpp Tells MKTYPLIB not to spawn the C preprocessor.
/cpp_cmd <path> Specifies the path for the C preprocessor, which is part of the C/C++ compiler. Defaults to CL.EXE. If MKTYPLIB is to be used with compilers other than Visual C++, this option might have to be changed to reflect the actual name of the preprocessor.
/cpp_opt "<opt>" Specifies the C/C++ preprocessor's options. The default options are /C /E /D__MKTYPLIB. The actions taken with the default options are as follows:

C Doesn't strip any comments from the preprocessor output.

/E Performs a preprocessor pass only, writing the output to stdout.

/D__MKTYPLIB Defines the identifier MKTYPLIB that is referenced in OLECTL.H.
/Ddefine[=value] Defines additional C/C++ preprocessor identifiers. This option is used in addition to the /cpp_opt "<opt>" option.
/I includepath Specifies paths for any include files.

The default installation for Visual C++ 1.5 (as completed by the CDK setup program) uses the following option list. Comments have been added.


/cpp_cmd D:\MSVC15\BIN\cl   // Defines the preprocessor command

/W0                         // Disables all warnings

/I D:\MSVC15\CDK16\INCLUDE  // Sets the include path

/nologo                     // Disables the startup logo

$Proj.odl                   // The input filename

/tlb tlb16\$Proj.tlb        // The output filename (and directory)

You could modify these options, but you probably won't need to.

The following code fragment shows the default commands for MKTYPLIB for Visual C++ 4. Notice that there are four different calls to MKTYPLIB—ANSI debug and release and Unicode debug and release. This code fragment is set up to create a 32-bit ANSI Windows release version.


SOURCE=.\clock.odl

!IF  "$(CFG)" == "clock - Win32 Release"

"$(OUTDIR)\clock.tlb" : $(SOURCE) "$(OUTDIR)"

   $(MTL) /nologo /D "NDEBUG" /tlb "$(OUTDIR)/clock.tlb" /win32 $(SOURCE)

!ELSEIF  "$(CFG)" == "clock - Win32 Debug"

"$(OUTDIR)\clock.tlb" : $(SOURCE) "$(OUTDIR)"

   $(MTL) /nologo /D "_DEBUG" /tlb "$(OUTDIR)/clock.tlb" /win32 $(SOURCE)

!ELSEIF  "$(CFG)" == "clock - Win32 Unicode Debug"

"$(OUTDIR)\clock.tlb" : $(SOURCE) "$(OUTDIR)"

   $(MTL) /nologo /D "_DEBUG" /tlb "$(OUTDIR)/clock.tlb" /win32 $(SOURCE)

!ELSEIF  "$(CFG)" == "clock - Win32 Unicode Release"

"$(OUTDIR)\clock.tlb" : $(SOURCE) "$(OUTDIR)"

   $(MTL) /nologo /D "NDEBUG" /tlb "$(OUTDIR)/clock.tlb" /win32 $(SOURCE)

!ENDIF

With Visual C++ 4, you set the typelib options by accessing the Project Settings dialog box. In this dialog, select the .ODL file in the Settings For list box. You see two tabs, General and OLE Types. Under the General tab, you can choose to exclude the typelib from the build. If you do so, you must build the typelib manually if you make any changes to the control's properties, methods, or events.

Figure 16.12 shows the General tab of Visual C++ 4's Project Settings for CLOCK's typelib generation.

Figure 16.12. The General tab of CLOCK.ODL's project settings.

Under the OLE Types tab, shown in Figure 16.13, you can specify the output TBL filename, output header filenames, additional include directories, and preprocessor definitions. You also can specify whether MKTYPLIB's startup banner is displayed. In addition, there is a Reset button so that you can reset the typelib options to their default values.

Figure 16.13. The OLE Types tab of CLOCK.ODL's project settings.

Creating an OLE Control


In this chapter you will create an OLE Custom Control and learn how an OLE control handles events, properties, and methods. The control created in this chapter can be embedded in an Access 7 form or report or in any other container that supports OLE controls, including a Visual C++ 4 MFC 4 program dialog box.

There is little reason to create an OLE control without using ControlWizard. If you create an OLE control manually, you can't use ClassWizard to manage the control's classes, and you'll have difficulty creating a project file that is compatible with Visual C++. If you convert an existing VBX control to an OLE control, ControlWizard offers an option to assist you in automating the conversion process.



NOTE

Remember, Windows NT and OLE are Unicode-compatible. You should always code string literals using the _T() macro. For example, the string "Peter D. Hipson" should be written as _T("Peter D. Hipson"). The _T() macro takes care of the conversions to Unicode when necessary.


If you're using Visual C++ 2, you must have the OLE control CDK installed before you create an OLE control. Versions of Visual C++ earlier than 1.50.01 (including 1.5) must be upgraded before you install the CDK. Visual C++ 4 has OLE control development support built in, not as a separate component.

Creating an OLE Control Shell


To create your new OLE control, which you'll call Clock, first you must start AppWizard's ControlWizard by choosing File | New from Visual C++. Then follow these steps:

  1. Select the directory under which the new OLE control's project directory will be created. Make sure that this directory doesn't already have a subdirectory with the same name.

  2. Name your new project in the Project Name edit box. For your sample Clock OLE control, use the name clock. In earlier versions of ControlWizard, you were restricted to lowercase letters. This restriction doesn't apply to Visual C++ 4's ControlWizard.

  3. Select OLE Control Wizard as the type of project.

  4. Click the Create button to display ControlWizard's first dialog box. Make sure that the all the default options are selected (one control, no runtime license, comments, and no help files).

  5. Click Next to move to the Step 2 of 2 dialog box, shown in Figure 16.14.

    Figure 16.14. ControlWizard's Step 2 of 2 dialog for the clock project.

  6. Accept the default control name (Clock), and again accept the default options (Activate when visible and Has an "About" box). Don't select any Windows control to subclass.

  7. Click the Edit Names button.

  8. In the Edit Names dialog box, shown in Figure 16.15, make sure that the Short Name is Clock and that the class is CClockCtrl. Change the Type Name to Digital Clock Control. Accept the default names for the Header File, Implementation File, and Type ID.

    Figure 16.15. ControlWizard's Edit Names dialog box for Clock.

  9. Click OK in the Edit Names dialog box.

  10. Click OK in the OLE ControlWizard - Step 2 of 2 dialog box.

  11. The New Project Information dialog box appears. Compare the results in your session with those shown in Figure 16.16. The only difference should be the Install Directory field (located at the bottom of the dialog box), because you won't be using the same directory structure as I am.

    Figure 16.16. The New Project Information dialog box.

  12. Click OK.

In 12 simple steps, you've created your OLE control. You didn't have to know a single thing about OLE, controls, containers, embedding, or linking to create the control.

After you've created your OLE control, you should perform a full build. If you're building a Windows 95 control, you need to select the Win32 Debug version. If you're building a Windows NT control, use the default, the Win32 Unicode Debug version. Building a new project ensures you that the project will build before you've made any changes. When the build completes successfully, your control will be registered by Visual C++ 4, and you can test it using Test Container. Because your control has no real functionality, it simply shows up in Test Container as an ellipse inside the control's user area. Your clock control should look like the one shown in Figure 16.17, which shows Clock running in Test Container.

Figure 16.17. Clock, fresh from ControlWizard, in Test Container.

Now that you have a basic OLE control that you can use, the next step in the development process is to add some properties.



NOTE

In this chapter, properties, events, and methods are added—in that order. You don't need to follow a certain order when you add features to your OLE control. I followed the order described simply because it made this chapter easier to write!



Adding Properties to an OLE Control


Properties are divided into two categories: stock (which are part of the OLE control system) and custom (which are specific to a given OLE control). You will learn about stock properties first. After you create your stock properties, you will add a custom property to your clock.

Stock Properties: Colors

The OLE Custom Control system lets you rely on a set of stock properties for your control. With stock properties, you don't have to design a dialog box to set the property, because the stock property dialog boxes are already included in the OLE control support DLL file. OLE supports the stock properties shown in Table 16.8.

Table 16.8. Stock properties supported by OLE.

Property Description
Appearance The control's appearance.
BackColor The control's background color. The default is white.
BorderStyle The style of the border around the control.
Caption The control's caption.
Enabled The control's enabled state.
Font The font used for text in the control.
ForeColor The control's foreground color.
hWnd The control's hWnd.
Text The control's text.

The first properties that you add are for foreground and background colors. These properties are supported with a stock color selection dialog box. The process of adding a stock property in an OLE control isn't very difficult.



NOTE

In Visual C++ 1.5 you must manually call the Make TypeLib utility (MKTYPLIB) to build the type library before you rebuild your project. In the 32-bit versions of Visual C++, Developer Studio takes care of building the type library. When you build OLE controls in Visual C++ 1.5, you should always select Tools | Make TypeLib after you've changed properties, events, or methods and before you rebuild the OLE control.


The following is the process to add a stock property:

  1. Start ClassWizard by pressing Ctrl-W. Select the OLE Automation tab, shown in Figure 16.18. Make sure that the class listed in the Class name combo box is the OLE control class (CClockCtrl for the clock control).

    Figure 16.18. The OLE Automation tab in ClassWizard.

  2. Click the Add Property button to display the Add Property dialog box, shown in Figure 16.19. You must provide the property's external name in the External name combo box. You can select one of the stock property names (see Table 16.8), or you can enter the name of a custom property (see the section "Custom Properties" later in this chapter).

    Figure 16.19. The Add Property dialog box.

    The first stock property you will add in your Clock OLE control is BackColor (which can be found in the External name combo box). After you've selected the name of a stock property, you must make sure that the Stock radio button in the Implementation group is selected. You can override a stock property by changing the Implementation.

  3. When you're finished with the Add Property dialog box, click OK. You're returned to the OLE Automation tab, where in the External names list box you see the new property that you've added (BackColor), with an S preceding it to indicate that BackColor is a stock property.

  4. The changes that you've made with ClassWizard have added an interface that lets you change the background color. However, there still is no code in the control's drawing function to actually implement this.

    At this stage, you must make whatever changes are necessary to implement your stock property. For example, the BackColor property is the background for the control (most Windows applications' controls are a light gray color). To fill the OLE control's background, you must add a few lines to the OLE control's OnDraw() function. This function exists as a basic shell that you modify to draw whatever the control must display for the user. The original OnDraw() function (in CLOCKCTL.CPP) supplied by ControlWizard is as follows:


    
    /////////////////////////////////////////////////////////////
    
    // CClockCtrl::OnDraw - Drawing function
    
    void CClockCtrl::OnDraw(
    
        CDC* pdc, const CRect& rcBounds, const CRect& rcInvalid)
    
    {
    
    // TODO: Replace the following code with your own drawing code.
    
        pdc->FillRect(rcBounds, CBrush::FromHandle(
    
            (HBRUSH)GetStockObject(WHITE_BRUSH)));
    
        pdc->Ellipse(rcBounds);
    
    }
    You must modify this function because it has been hard-coded to fill the OLE control's background using the stock WHITE_BRUSH. This raises a question: ClassWizard added the interface to set the background color, but how does the program find out what color the user selected? Easy enough: There is a set of functions you can call to obtain the necessary attributes for a given property.

    To obtain the color of the background, you use the GetBackColor() function, which retrieves the current background color. The format that GetBackColor() returns must then be processed by the TranslateColor() function, after which the value from TranslateColor() can be used to create a new brush. The following code shows in bold the changes necessary to implement a background color property:

    
    /////////////////////////////////////////////////////////////
    
    // CClockCtrl::OnDraw - Drawing function
    
    void CClockCtrl::OnDraw(
    
        CDC* pdc, const CRect& rcBounds, const CRect& rcInvalid)
    
    {
    
    // TODO: Replace the following code with your own drawing code.
    
    //  pdc->FillRect(rcBounds, CBrush::FromHandle(
    
    //      (HBRUSH)GetStockObject(WHITE_BRUSH)));
    
        CBrush bkBrush(TranslateColor(GetBackColor()));
    
        pdc->FillRect(rcBounds, &bkBrush);
    
        pdc->Ellipse(rcBounds);
    
    }
    You must also add the necessary property page information to the BEGIN_PROPPAGEIDS() section. This change tells OLE which dialog boxes to display. Property pages are displayed when the user selects properties for the control. Originally, the BEGIN_PROPPAGEIDS() section looked like the following:

    
    ///////////////////////////////////////////////////////////////
    
    // Property pages
    
    // TODO: Add more property pages as needed.
    
    // Remember to increase the count!
    
    BEGIN_PROPPAGEIDS(CClockCtrl, 1)
    
        PROPPAGEID(CClockPropPage::guid)
    
    END_PROPPAGEIDS(CClockCtrl)
    You must add a new property page to the list. To do this, you must make two changes. First, the number of property page sheets will change from 1 (the default page for the clock control) to 2 (the default page and a color page). The page count is in the opening macro:


    
    BEGIN_PROPPAGEIDS(CClockCtrl, 1)
    In the BEGIN_PROPPAGEIDS() macro, the second parameter specifies the count and must be changed from 1 to 2.

    
    BEGIN_PROPPAGEIDS(CClockCtrl, 2)
    Second, you must add a new PROPPAGEID() macro to the list, changing the BEGIN_PROPPAGEIDS() block as follows:

    
    ///////////////////////////////////////////////////////////////
    
    // Property pages
    
    // TODO: Add more property pages as needed.
    
    // Remember to increase the count!
    
    BEGIN_PROPPAGEIDS(CClockCtrl, 2)
    
          PROPPAGEID(CClockPropPage::guid)
    
        PROPPAGEID(CLSID_CColorPropPage)
    
    END_PROPPAGEIDS(CClockCtrl)
  5. After making these changes, you must rebuild the OLE control.

  6. After successfully rebuilding your control, try it out in the Test Container application. You should be able to select the control's property sheets, and there should be two tabs: General and Colors. If you select the Colors tab, you'll see a dialog box similar to the one shown in Figure 16.20.

Figure 16.20. The Colors tab of the Digital Clock Control Properties dialog box.

Adding Functionality to the Clock Control

Now that you've added a stock property, it's time to add some functionality to your clock control. You need to do the following:

  1. Set up a timer loop with a one-second resolution.

  2. Add code in your OnDraw() function to display the time.

  3. Add code to kill the timer when the control ceases to run.

Setting up the timer isn't very difficult. You need to add code to the OnCreate() function (which you will create with ClassWizard) to start the timer. To create your timer, follow these steps:

  1. Start ClassWizard and go to the Message Maps tab. Select the CClockCtrl class and CClockCtrl in the Object Ids list. You will see the Messages list box filled with the various WM_ messages. Select WM_CREATE and then click the Add Function button.

  2. The Member Functions list box shows a new member, OnCreate. It will have the handler ON_WM_CREATE.

  3. Click the Edit Code button, which ends ClassWizard and places the cursor in the OnCreate() member function in CLOCKCTL.CPP.

You must add some code to OnCreate() to set up the timer. The following code fragment shows in bold the changes that are needed in the OnCreate() function:


int CClockCtrl::OnCreate(LPCREATESTRUCT lpCreateStruct)

{

    TRACE(_T("OnCreate() called\n"));

    if (COleControl::OnCreate(lpCreateStruct) == -1)

        return -1;

    m_IDTimer = SetTimer(999, 1000, NULL);

    if (m_IDTimer == 0)

    {

        AfxMessageBox(_T("Couldn't set the timer in OnCreate()\n"));

    }

    return 0;

}

Notice that in OnCreate() you reference a new member variable, m_IDTimer. This member variable is used to hold the timer's ID, which SetTimer() returns. The timer ID is needed later to kill the timer, and it should be checked whenever a timer interrupt occurs so that you know which timer's interval has expired. In CLOCKCTL.H, you must add a declaration for m_IDTimer:


class CClockCtrl : public COleControl

{

       DECLARE_DYNCREATE(CClockCtrl)

// Constructor

public:

       CClockCtrl();

// Overrides

       // Drawing function

       virtual void OnDraw(

            CDC* pdc, const CRect& rcBounds, const CRect& rcInvalid)

       // Persistence

       virtual void DoPropExchange(CPropExchange* pPX);

       // Reset control state

       virtual void OnResetState();

// Implementation

protected:

       ~CClockCtrl();

       UINT  m_IDTimer;

As with any member function, in the constructor for the CClockCtl object you must initialize your member variable m_IDTimer:


CClockCtrl::CClockCtrl()

{

       InitializeIIDs(&IID_DClock, &IID_DClockEvents);

       // TODO: Initialize your control's instance data here

       m_IDTimer = 0;

}

Now you have the code to create a timer. You also need code to kill the timer when the OLE control ends execution. This is important, because timers are a limited system resource (especially under 16-bit versions of Windows). You get rid of your timer in the WM_DESTROY message handler. You need to create this handler in the same manner that you did when you created the WM_CREATE handler. Start ClassWizard, select WM_DESTROY, and then click the Add Function button to create the function handler. When creation is complete, click the Edit Code button to end ClassWizard and begin editing your function.

The following is the changed OnDestroy() function. The changes to kill the timer are in bold:


void CClockCtrl::OnDestroy()

{

    COleControl::OnDestroy();

    // TODO: Add your message handler code here

    if (m_IDTimer > 0)

    {// We have allocated a timer, so let's kill it:

        KillTimer(m_IDTimer);

        m_IDTimer = 0;

    }

}

Take a moment to take stock of what you've done. You now have a timer that is started when the OLE control starts and that ends when the OLE control ends. It's up to you to make use of this timer. To do so, you must make two more changes.

First, you need a handler for the WM_TIMER messages that will be sent to the application whenever the timer's interval expires. Again, with ClassWizard you need to create a handler, this time for WM_TIMER. In your OnTimer() function, you need to tell the control to update its display:


void CClockCtrl::OnTimer(UINT nIDEvent)

{

    // TODO: Add your message handler code here and/or call default

    if (nIDEvent == m_IDTimer)

    {// It is our timer, so we'll handle it now!

        InvalidateControl();

    }

    COleControl::OnTimer(nIDEvent);

}

The process of telling a control to update itself is very similar to the process of telling a window to update itself. A window is updated whenever a call is made to InvalidateRect(), and a control is updated whenever a call is made to InvalidateControl(). Check to see whether the timer message is the correct one so that you don't do more updates than you need to. Once a second is sufficient for a displayed clock.

Second, you must update the control's display of the time. Until now, your Clock OLE control has simply displayed the default ellipse. You now need to delete the ellipse drawing code and add whatever functionality is needed to display the time.

With the advent of strftime(), it has become easy to format a time value. However, because an OLE control is in a DLL, you can't call the strftime() function. You must manually format the time for display. Fortunately, this isn't too difficult. First, you add an #include to the CLOCKCTL.CPP file to include the TIME.H file.

After you have the header file for the standard time functions, you can add the time display code to the OnDraw() function:


void CClockCtrl::OnDraw(

                   CDC* pdc, const CRect& rcBounds, const CRect& rcInvalid)

{

   // TODO: Replace the following code with your own drawing code

//  pdc>FillRect(rcBounds,

//        CBrush::FromHandle((HBRUSH)GetStockObject(WHITE_BRUSH)));

   CBrush  bkBrush(TranslateColor(GetBackColor()));

   pdc->FillRect(rcBounds, &bkBrush);

//  pdc->Ellipse(rcBounds);

   struct  tm *newtime;

   char    am_pm[] = _T("AM");

   time_t long_time;

   char    szBuffer[80];

   time(&long_time);

   newtime = localtime(&long_time);

   if (newtime->tm_hour > 12)

   {

       strcpy(am_pm, _T("PM"));

       newtime->tm_hour -= 12;

   }

   sprintf(szBuffer, _T("%2.2d:%2.2d:%2.2d %s"),

       newtime->tm_hour,

       newtime->tm_min,

       newtime->tm_sec,

       am_pm);

   pdc->SetTextAlign(TA_LEFT | TA_TOP);

   pdc->ExtTextOut(rcBounds.left, rcBounds.top,

       ETO_CLIPPED, rcBounds,

       szBuffer, strlen(szBuffer), NULL);

}

After you have made these changes, you should rebuild your control and test it. If all goes well, you'll get a display like the one shown in Figure 16.21. It's not fancy, but it does display the time.

Figure 16.21. Test Container with Clock showing the time.

You still need one more stock color property. You can set the background color; wouldn't it be nice if you could set the color of the text? No sooner said than done. Again, you must follow a process very similar to the one you followed to add the BackColor property. The main difference is that you don't need to add a stock dialog page because you have one already, created for the BackColor property.

In ClassWizard, select the OLE Automation tab. Click the Add Property button and, in the External Name combo box, select ForeColor. Make sure that Stock Implementation is selected, and then click OK. Close ClassWizard by clicking OK in the main ClassWizard dialog box.

Next, you must modify the OnDraw() function to utilize the new color. You will use the ForeColor property for the text that will display the time:


void CClockCtrl::OnDraw(

                   CDC* pdc, const CRect& rcBounds, const CRect& rcInvalid)

{

   // TODO: Replace the following code with your own drawing code

//  pdc>FillRect(rcBounds,

//        CBrush::FromHandle((HBRUSH)GetStockObject(WHITE_BRUSH)));

   CBrush  bkBrush(TranslateColor(GetBackColor()));

   pdc->FillRect(rcBounds, &bkBrush);

//  pdc->Ellipse(rcBounds);

   struct  tm *newtime;

   char    am_pm[] = _T("AM");

   time_t long_time;

   char    szBuffer[80];

   time(&long_time);

   newtime = localtime(&long_time);

   if (newtime->tm_hour > 12)

   {

       strcpy(am_pm, _T("PM"));

       newtime->tm_hour -= 12;

   }

   sprintf(szBuffer, _T("%2.2d:%2.2d:%2.2d %s"),

       newtime->tm_hour,

       newtime->tm_min,

       newtime->tm_sec,

       am_pm);

   pdc->SetTextAlign(TA_LEFT | TA_TOP);

   pdc->SetTextColor(TranslateColor(GetForeColor()));

   pdc->SetBkMode(TRANSPARENT);

   pdc->ExtTextOut(rcBounds.left, rcBounds.top,

       ETO_CLIPPED, rcBounds,

       szBuffer, strlen(szBuffer), NULL);

}

You added only two new lines to set the text color. Now rebuild your clock control and try it. When you do so, you see that the colors' property page now has two selections: BackColor and ForeColor. When you change the ForeColor property, the color of the displayed time changes to match.

More Stock Properties: Fonts

The OLE Custom Control system lets you rely on a set of stock properties for your control. With stock properties, you don't have to design a dialog box to set the property, because the stock property dialog boxes are already included in the OLE control support DLL file.

Now that your clock has color, the next logical addition is the ability to change the font of the time display. Many digital clock users will appreciate the ability to change the font. Nothing beats having a digital clock in a font that looks digital.

To add a font property, first start ClassWizard. Select the OLE Automation tab and then click the Add Property button. The Add Property dialog box appears. In the External name combo box, select Font. Make sure that Stock Implementation is also selected, and then click OK to close the Add Property dialog box. Click OK again to close ClassWizard.

You haven't yet installed a stock property dialog box for fonts. You can fix this by making an addition to the BEGIN_PROPPAGEIDS() block:


// TODO: Add more property pages as needed. Remember to increase the count!

BEGIN_PROPPAGEIDS(CClockCtrl, 3)

    PROPPAGEID(CClockPropPage::guid)

    PROPPAGEID(CLSID_CColorPropPage)

    PROPPAGEID(CLSID_CFontPropPage)

END_PROPPAGEIDS(CClockCtrl)

Don't forget to change the number of property page IDs from 2 to 3 (BEGIN_PROPPAGEIDS (CClockCtrl, 3)).

Next, you must again make a change to the OnDraw() function to utilize the correct font. In the previous version, you simply used the default font that was already selected. This time you want to use the font that the user selects, draw the text, and then restore the original default font:


void CClockCtrl::OnDraw(

                   CDC* pdc, const CRect& rcBounds, const CRect& rcInvalid)

{

   // TODO: Replace the following code with your own drawing code

//  pdc>FillRect(rcBounds,

//        CBrush::FromHandle((HBRUSH)GetStockObject(WHITE_BRUSH)));

   CBrush  bkBrush(TranslateColor(GetBackColor()));

   pdc->FillRect(rcBounds, &bkBrush);

//  pdc->Ellipse(rcBounds);

   struct  tm *newtime;

   char    am_pm[] = _T("AM");

   time_t long_time;

   char    szBuffer[80];

   time(&long_time);

   newtime = localtime(&long_time);

   if (newtime->tm_hour > 12)

   {

       strcpy(am_pm, _T("PM"));

       newtime->tm_hour -= 12;

   }

   sprintf(szBuffer, _T("%2.2d:%2.2d:%2.2d %s"),

       newtime->tm_hour,

       newtime->tm_min,

       newtime->tm_sec,

       am_pm);

   pdc->SetTextAlign(TA_LEFT | TA_TOP);

   pdc->SetTextColor(TranslateColor(GetForeColor()));

   pdc->SetBkMode(TRANSPARENT);

   CFont* pOldFont;

   pOldFont = SelectStockFont(pdc);

   pdc->ExtTextOut(rcBounds.left, rcBounds.top,

       ETO_CLIPPED, rcBounds,

       szBuffer, strlen(szBuffer), NULL);

   pdc->SelectObject(pOldFont);

}

Notice that a call to SelectStockFont() has been added, a function that inserts into the specified device context the font that the user selects. The SelectStockFont() function returns a CFont pointer to the previous font that was selected. After drawing your text, you can restore the original font by calling SelectObject() with the pointer that was returned by SelectStockFont().

Figure 16.22 shows the stock Fonts property tab. This dialog box lets you select any installed font available on the system. The Effects group box offers strikeout and underline fonts. You also can select bold, italic, or both.

Figure 16.22. The Fonts tab of the Digital Clock Control Properties dialog box.

Again, rebuild the clock control. When the rebuild is done (without errors), try the clock control again. You'll notice an immediate change—the default font is now different. This is caused by the call to SelectStockFont(), which returns a different font than was originally selected into the device context. Using the clock's properties sheet, change to a new font. To see the effect of the change, look at your clock control, shown in Figure 16.23, for an example of the clock with a different font.

Figure 16.23. A new font in Clock.

You should keep several things in mind when you're working with stock properties. First, whenever a new stock property page is selected, the changes made on the previous property page are implemented. When the stock property pages exit (when you click OK or a different tab), an InvalidateControl() call is made.

As you can see, it's very easy to add stock properties to an OLE control. Next you will add a few customized properties.

Custom Properties

Not everything in an OLE control can be configured with the stock properties. Some things (sometimes many things) can only be set using a custom properties sheet.

Fortunately, Microsoft decided to make custom properties easy. First, the ControlWizard applet creates a default properties dialog box for you, to which you can add dialog controls that you can use to customize the operation of your OLE control.

For the clock control, the first change you want is the ability to configure the time display format. In your original format for Clock, the time was formatted with the string "%2.2d:%2.2d:%2.2d %s". There is always a leading zero when the time is earlier than 10:00. Also, you might want to display the hours, minutes, and seconds separated by dashes rather than colons.

You need to have an edit control in your default properties page with which you can edit the display format. This could be a difficult change, but ClassWizard does most of the work for you.

Here are the steps for creating a custom property:

  1. Start ClassWizard and click the OLE Automation tab.

  2. Click the Add Property button to display the Add Property dialog box, shown in Figure 16.24.

    Figure 16.24. TimeFormat in the Add Property dialog box.

  3. In the External Name combo box, enter the name TimeFormat. The Implementation should be member variable. In the Variable name field, the default name of m_timeFormat is displayed. In the Notification function field, the default name of OnTimeFormatChanged is displayed. The Type box should have a CString selected. When you're satisfied with the names for the name, variable, and function, click OK.

  4. Figure 16.25 shows ClassWizard's main dialog box and the implementation of the TimeFormat property. Notice that the TimeFormat property is prefixed with a C. This indicates that TimeFormat is a custom property. When you've finished reviewing the TimeFormat property, click OK to end ClassWizard.

    Figure 16.25. ClassWizard showing the TimeFormat property.

  5. After you end ClassWizard, you must implement whatever code is necessary to make the TimeFormat property work. ClassWizard has created a member variable in CClockCtrl called m_timeFormat. This variable must be initialized and used where necessary. To initialize m_timeFormat, you must change the CClockCtrl() constructor, adding an assignment for the m_timeFormat variable:

    
    CClockCtrl::CClockCtrl()
    
    {
    
            InitializeIIDs(&IID_DClock, &IID_DClockEvents);
    
            // TODO: Initialize your control's instance data here
    
            m_IDTimer = -1;
    
            m_timeFormat = _T("%2.2d:%2.2d:%2.2d %s");
    
    }
    Now that you have your default format in the m_timeFormat variable, you must use it. This means changing your OnDraw() function so that the sprintf() function that you use to format the time uses m_timeFormat rather than a character constant. Notice the cast on m_timeFormat. This casting is necessary because sprintf() expects a pointer to a character string, and just passing m_timeFormat would pass a CString object, making sprintf() very confused:

    
    if (newtime->tm_hour > 12)
    
    {
    
            strcpy(am_pm, _T("PM"));
    
            newtime->tm_hour -= 12;
    
    }
    
    sprintf(szBuffer, (const char *)m_timeFormat,
    
            newtime->tm_hour,
    
            newtime->tm_min,
    
            newtime->tm_sec,
    
            am_pm);
    
    pdc->SetTextAlign(TA_LEFT | TA_TOP);
    At this point you've done everything except allow the user to actually change the time format. You still need to add an edit control to the default property page dialog box and connect this edit control to the m_timeFormat variable in CClockCtrl.

  6. To add an edit control to your default property page dialog box, you must edit the dialog box by selecting the IDD_PROPPAGE_CLOCK dialog box into an edit window. Next, locate a group box at the left side of the property page dialog box and label it Display Format. Inside this group box, add an edit control named IDC_TIME_FORMAT (or something equally meaningful). When you have finished, the IDD_PROPPAGE_CLOCK dialog box will look like the one shown in Figure 16.26.

    Figure 16.26. IDD_PROPPAGE_CLOCK with IDC_TIME_FORMAT.

  7. When you've added the IDC_TIME_FORMAT control to the default property page dialog (and saved the dialog box), you must link the control to the m_timeFormat variable that's in CClockCtrl.

  8. Again, start ClassWizard. Click the Member Variables tab, and then select the IDC_TIME_FORMAT line in the Controls IDs list box. Click the Add Variable button. The Add Member Variable dialog box, shown in Figure 16.27, appears.

    Figure 16.27. ClassWizard's Add Member Variable dialog box.

  9. You must supply a variable name. You can reuse the name m_timeFormat, because this class (CClockPropPage) can't see the m_timeFormat variable in CClockCtrl. In the Category combo box, select Value. In the Variable type combo box, select CString. To link this control in the property page dialog box with the m_timeFormat variable in CClockCtrl, you must provide the Optional OLE property name. This is the name you entered in the External name field when you created the custom property in step 3. With this information, ClassWizard can create all the necessary linkages to manage the time display format. Click OK.

    There is no need in your clock control to have length validation for your format string. In a more finished project, you would want to make sure that the results from using the format string would fit in the buffers. However, in your clock control, you will omit this error-checking for clarity.

  10. After closing ClassWizard's main dialog box, you must rebuild your clock control. Visual C++ should build the control for you with no errors (if you have errors, find and correct them). Next, try the control in the Test Container application. After you embed the clock control, access the control's properties dialog. Your dialog box should look like the one shown in Figure 16.28, in which a new format string has already been set and the Apply Now button has been clicked, thereby updating the control's appearance.

Figure 16.28. Test Container showing custom properties for Clock.

You will make one final change to your clock control before you move on to other things (such as events). To make your clock have a 24-hour format in which 6 p.m. is displayed as 18:00, you will add a set of radio buttons to change the format of the displayed time.

To make this change, you first must add a set of radio buttons to your properties dialog box. I chose radio buttons because they seem to convey more information about the exclusive nature of the a.m./p.m. versus 24-hour display format than a check box could.



NOTE

Remember to use the Group property to allow MFC to properly manage the radio buttons. If you don't see your radio buttons in ClassWizard, you've probably forgotten to set the Group property. Remember: A group box is only a cosmetic object in a dialog box. The Group property is invisible but affects the operation of controls in a dialog box. Be careful not to confuse the two.


To enclose the radio buttons, you can use the group box that you created when you added the TimeFormat property. Follow these steps to make changes to the clock control:

  1. In ClassWizard, create a new custom property called Display24Hour, which is created as a type short. In the constructor, initialize the new variable (m_display24Hour) to 0:

    
    CClockCtrl::CClockCtrl()
    
    {
    
        InitializeIIDs(&IID_DClock, &IID_DClockEvents);
    
        // TODO: Initialize your control's instance data here.
    
        m_IDTimer = 0;
    
        m_display24Hour = 0;
    
        m_timeFormat = "%2.2d:%2.2d:%2.2d %s";
    
    }
    Add two radio buttons, IDC_AM_PM and IDC_24_HOUR, to the IDD_PROPPAGE_CLOCK dialog box. Figure 16.29 shows these two new dialog controls. When you're finished with the IDD_PROPPAGE_CLOCK dialog box, save it using File | Save.

    Figure 16.29. IDD_PROPPAGE_CLOCK with AM/PM and 24 Hour radio buttons.

  2. Invoke ClassWizard and select the Member Variables tab. Select the IDD_AM_PM control and select Add Member Variable. Use the name m_display24Hour for the variable, and make the variable's type int. In Optional OLE Property Name, use the name you entered in step 1. Click OK in the Add Member Variable dialog box, and then click OK in ClassWizard's main dialog box.

  3. Next, you must add code to the OnDraw() function to change the format of the time being displayed. First, in the CClockCtrl constructor, you must add an initializer for the m_display24Hour variable. Initialize this variable to 0. Next, modify the OnDraw() function to test the m_display24Hour variable. Replacing an if() block with a switch() block is easy, as the following code fragment shows:

Now your clock has all the properties you need. You can add more properties (such as an alarm function) later by simply following the preceding steps as a guideline.

Next you will add some events to your clock control.

Adding Events to an OLE Control


Your birthday, my birthday, the day the cow jumped over the moon—all are examples of events, but not the events that OLE controls are interested in. The term events, when used with OLE controls, refers to the process in which the control notifies the container that an event of some significance has occurred. This event might be as simple as the user clicking in the control's user area or (using your clock control as an example) as complex as the expiration of a time period.

When a container is notified that an event has occurred, the event has been fired. All event functions are called firing functions and usually are prefixed with the word Fire, as in FireClick().

Like properties, events come in two flavors: stock and custom. You will learn about stock events first, and then you will add a custom event to your clock.

Stock Events

Table 16.9 lists the stock events that are available to an OLE control. You can add these events by simply clicking the Add Event button in ClassWizard's OLE Events tab. The stock events have default functions defined for them. Each function has zero or more parameters and never has a return value.

Table 16.9. OLE control stock events.

Event Firing Function When It Gets Fired
Click which has no parameters the control, and a button-up message (WM_LBUTTONUP, WM_RBUTTONUP, or WM_MBUTTONUP) is received when the mouse is located over the control's user area. Before this event, the stock MouseDown and MouseUp events are fired (if defined).
DblClick void FileDblClick(), which has no parameters The mouse has been captured by the control, and any button has been double-clicked. A WM_LDBLCLICK, WM_RDBLCLICK, or WM_MDBLCLICK message is received when the mouse is located over the control's user area. Before this event, the stock Click, MouseDown, and MouseUp events are fired (if defined).
Error void FireError( SCODE scode, LPCSTR lpszErrorDescription, UINT nHelpID = 0) This event is fired whenever an error condition occurs in the control. The FireError() function has parameters to describe the actual error.
KeyDown void FireKeyDown( USHORT *pnChar, short nShiftState) This event is fired whenever a key (either WM_SYSKEYDOWN or WM_KEYDOWN) is pressed and the control has input focus. The FireKeyDown() function has parameters to tell which key was pressed and the state of the Shift keys.
KeyPress void FireKeyPress( USHORT * pnChar) This event is fired whenever a WM_CHAR message has been received. The FireKeyPress() function has a parameter that points to the character for the key that was pressed.
KeyUp void FireKeyUp( USHORT *pnChar, short nShiftState) This event is fired whenever a key (either WM_SYSKEYUP or WM_KEYUP) is pressed and the control has input focus. The FireKeyUp() function has parameters to tell which key was pressed and the state of the Shift keys.
MouseDown void FireMouseDown( short nButton, short nShiftState, OLE_XPOS_PIXELS x, OLE_YPOS_PIXELS y) This event is fired when a WM_LBUTTONDOWN, WM_RBUTTONDOWN, or WM_MBUTTONDOWN message is received. The mouse is captured just before the MouseDown event is fired. The FireMouseDown() function has parameters to indicate which mouse button was


pressed, the state of the Shift keys, and the mouse's x- and y- coordinates.
MouseMove void FireMouseMove(short nButton, short nShiftState, OLE_XPOS_PIXELS x, OLE_YPOS_PIXELS y) This event is fired when a WM_MOUSEMOVE message is received. The FireMouseDown() function has parameters to indicate which mouse button was pressed, the state of the Shift keys, and the mouse's x- and y- coordinates.
MouseUp void FireMouseUp (short nButton, short nShiftState, OLE_XPOS_PIXELS x, OLE_YPOS_PIXELS y) This event is fired when a WM_LBUTTONUP, WM_RBUTTONUP, or WM_MBUTTONUP message is received. The mouse is released from capture just before the MouseUp event is fired. The FireMouseUp() function has parameters to indicate which mouse button was pressed, the state of the Shift keys, and the mouse's x- and y- coordinates.

Your control can't offer a great deal of functionality in implementing a stock event. You'll add the stock event DblClick to your clock control as an exercise in adding stock events.

To add a stock event, start ClassWizard and then click the OLE Events tab. You will see a dialog box in which you can view currently defined events and add new events. To add an event, click the Add Event button. ClassWizard displays the Add Event dialog box, shown in Figure 16.30, which lets you select the event's external name. The External name combo box lets you select stock events from the drop-down list box or create a custom event by entering the event name.

Figure 16.30. The Add Event dialog box.

In the External name list box, select DblClick. Stock implementation should be selected. The Internal name field changes to FireDblClick and becomes read-only so that the name of the event-firing function can't be changed. If you're defining the DblClick event as a Custom implementation, you can edit the function's name if you want to.

The Parameter list box also is disabled because the stock DblClick event doesn't take any parameters.

After you've defined the DblClick event, click OK in the Add Event dialog box. You're returned to the OLE Events tab in ClassWizard. You should see a single event defined—DblClick, which is prefixed with an S symbol. This indicates that DblClick is a stock event. See Figure 16.31.

Figure 16.31. OLE Events in ClassWizard with the DblClick event displayed.

After you've reviewed your stock event, close ClassWizard by clicking OK, and rebuild the control. When you've successfully rebuilt the control, you can test it using Test Container, in which you can view the event log by selecting View | Event Log. Double-clicking the clock control causes an event notification to be logged in the Event Log dialog box.

Custom Events

Stock events are defined for you, but a custom event is totally up to your imagination. Take a trip back to the days of the first digital watches. Almost as soon as digital watches appeared, some smart engineer designed one with an alarm. Hey, what a concept—a digital alarm clock!

If you added a field to your clock's property page for the alarm time (a simple edit field, for example), which you could then parse out to an alarm time, you could compare this alarm time with the current time whenever you received a WM_TIMER message.

First, add a set alarm section to your property page. Adding a set alarm time field presents a minor problem: You really could use a custom time control that would validate your alarm time. However, to keep your clock custom control simple, you will use a set of simple combo box fields with the hours, minutes, and seconds entered in three separate combo boxes. You can use combo boxes to force the user to enter a valid time value.



NOTE

The MFC implementation of IDispatch lets you use a maximum of 15 parameters (the alarm function uses three). Be careful not to exceed this limitation.


The process of defining the alarm property is exactly the same as adding the properties that you added earlier. First, using ClassWizard, add three new properties: AlarmHours, AlarmMinutes, and AlarmSeconds. After adding these properties, you will have three new property variables: m_alarmHours, m_alarmMinutes, and m_alarmSeconds. When the clock control starts, you need to initialize the alarm to the current time:


CClockCtrl::CClockCtrl()

{

       InitializeIIDs(&IID_DClock, &IID_DClockEvents);

       // TODO: Initialize your control's instance data here

       m_IDTimer = -1;

       m_timeFormat = _T("%2d:%2.2d:%2.2d %s");

       m_display24Hour = 0;

       struct  tm *newtime;

       time_t long_time;

       time(&long_time);

       newtime = localtime(&long_time);

       m_alarmHours   = newtime->tm_hour;

       m_alarmMinutes = newtime->tm_min;

       m_alarmSeconds = newtime->tm_sec;

}

Next you must add to the stock property page a set of controls to allow the user to set the alarm time. In the sample OLE control, you do this using a set of combo boxes that are initialized to the desired values. Each of these combo boxes is a DropList style with the sort attribute set to off. If you don't specify DropList, you can't bind an integer variable to the combo box.

Figure 16.32 shows the properties dialog box with the new controls installed. Each of these controls has a fixed set of data. The hours control is 0 to 23, and the minutes and seconds controls have values of 0 to 59. I've used the dialog control editor to initialize the controls. (See the sample program in the CHAPTR16\clock folder on the CD that comes with this book.)

Figure 16.32. IDD_PROPPAGE_CLOCK with an alarm feature added.

You also need a way to disable the alarm feature. To do this, you will add a property called Alarmed using ClassWizard.



NOTE

Throughout this chapter you've bound variables to the property page dialog box that were identical to the property variable names in the clock project. Why? To keep the names consistent. The critical name is the optional OLE property name, which OLE uses to communicate your changes in the property dialog to the control itself.


After the clock control has started, each time the WM_TIMER message is received in the OnTimer() function, the time must be compared with the alarm time. If the clock time and the alarm time match, your alarm event is fired.

The container application determines what happens when the alarm event fires. If the clock control is embedded in an Access form, the alarm might be used to remind the user to perform a task, such as backing up or saving data.

Next, you must add the alarm event handler. To add a custom event handler, you use ClassWizard's OLE Events tab. Select CClockCtrl in the Class Name combo box. Click the Add Event button to display the Add Event dialog box, shown in Figure 16.33.

Figure 16.33. The Add Event dialog box.

Specify an external name of Alarm. The internal name (which Visual C++ will generate for you) should be FireAlarm (you couldn't have picked a better name if you tried). In the Parameter list combo box, double-click the left side of the top (current) line. An edit field appears, in which you can enter a variable name. Enter the first variable as nHour. Next, either tab forward or double-click the right side of the current line. You're presented with a drop-down list from which you can select nHour's variable type. Use short. After creating nHour, create nMinute (double-click the line under nHour) and nSecond (double-click the line under nMinute).

When the FireAlarm event is fired, it will pass the time (using the nHour, nMinute, and nSecond parameters) that the alarm occurred. Since Windows is a multitasking operating system, it's likely that the container will receive the FireAlarm event long after (in computer time) the actual event occurred. Click OK.

The final step in adding your alarm event is adding the actual alarm code. You must take into consideration several factors when you're comparing the alarm time with the current time. First and foremost is that there is no guarantee that there will be a WM_TIMER message every second. If Windows is busy, there might be one (or more) skipped WM_TIMER messages. This presents a problem, because you're testing for an exact time. Probably the best thing to do is to find out whether the alarm time has passed. If it has and an alarm hasn't yet been sounded, fire the alarm. You do this by setting a flag that signals that an alarm is set and hasn't been sounded. Of course, if the user sets an alarm time that is earlier than the current time, this generates an immediate alarm, which is a minor problem in this sample program.

First, take a look at the final OnDraw() function. You've added code to OnDraw() to find out whether the alarm has been set (if m_alarmed is TRUE) and whether the current time is later than the alarm time. If both tests are true, an alarm event is triggered.


void CClockCtrl::OnDraw(

           CDC* pdc, const CRect& rcBounds, const CRect& rcInvalid)

{

   // TODO: Replace the following code with your own drawing code

   CBrush  bkBrush(TranslateColor(GetBackColor()));

   pdc->FillRect(rcBounds, &bkBrush);

   struct    tm *newtime;

   char    am_pm[] = _T("AM");

   time_t long_time;

   char    szBuffer[80];

   time(&long_time);

   newtime = localtime(&long_time);

//    Check for alarms. If past alarm time, sound it!

   if (m_alarmed &&

       m_alarmHours   <= newtime->tm_hour &&

       m_alarmMinutes <= newtime->tm_min &&

       m_alarmSeconds <= newtime->tm_sec)

   {//    It's an alarming event!

       m_alarmed = FALSE;

       FireAlarm(m_alarmHours, m_alarmMinutes, m_alarmSeconds);

   }

//    Format time for display

   switch (m_display24Hour)

   {  // Shows how to handle radio buttons:

       case 0:  // First radio button, AM/PM format, being used

           if (newtime->tm_hour > 12)

           {

               strcpy(am_pm, _T("PM"));

               newtime->tm_hour -= 12;

           }

           break;

       case 1:  // Second radio button, 24 Hour format

           am_pm[0] = '\0';

           break;

       default:  // ERROR: An unhandled radio button selected!

           break;

   }

   sprintf(szBuffer, (const char *)m_timeFormat,

       newtime->tm_hour,

       newtime->tm_min,

       newtime->tm_sec,

       am_pm);

//    Set up display of time

   pdc->SetTextAlign(TA_LEFT | TA_TOP);

   pdc->SetTextColor(TranslateColor(GetForeColor()));

   pdc->SetBkMode(TRANSPARENT);

   CFont* pOldFont;

   pOldFont = SelectStockFont(pdc);

   pdc->ExtTextOut(rcBounds.left, rcBounds.top,

       ETO_CLIPPED, rcBounds,

       szBuffer, strlen(szBuffer), NULL);

//    Restore device context

   pdc->SelectObject(pOldFont);

}

When the alarm time has passed, a call is made to FireAlarm() with the alarm time. The FireAlarm() function is created by ClassWizard in the CLOCKCTL.H file as a single-line function that calls the OLE controls function FireEvent() with the correct parameters:


// Event maps

   //{{AFX_EVENT(CClockCtrl)

   void FireAlarm(short nHour, short nMinute, short nSecond)

       {FireEvent(eventidAlarm,EVENT_PARAM(VTS_I2  VTS_I2  VTS_I2),

           nHour, nMinute, nSecond);}

   //}}AFX_EVENT

   DECLARE_EVENT_MAP()

The m_alarmed variable is tied to a property that is, in turn, mapped to a check box in Clock's property page dialog box. This lets the user turn the alarm function on or off. Also, whenever an alarm occurs, the alarm function is turned off.

Adding this alarm functionality was a bit complex. Let's recap in a step-by-step manner:

  1. Add four properties—AlarmHours, AlarmMinutes, AlarmSeconds, and Alarmed—to your clock.

  2. Add an event called Alarm to your clock. It should have the parameters nHour, nMinute, and nSecond.

  3. Add to the properties dialog box a group box (cosmetic), three DropList combo boxes, and a single check box. Bind the variables (for simplicity, you can use the same variable names created in step 1) and the properties created in step 1 to these controls.

  4. Add code to the control's constructor to initialize the alarm time (set it to the current time) and set the alarm function to off. You set the alarm function to off because there would be a risk of having an immediate alarm as soon as the control were initialized.

  5. Add code to the OnDraw() function (or, if you wish, the OnTimer() function, but this would take more code) to compare the current time with the alarm time and to check to see if the alarm function is turned on. If the times match and the alarm function is on, fire the event using the FireAlarm() function.

There, in five easy steps, is what is necessary to add alarm functionality to the digital clock OLE control. The actual process of adding an event is simple: Use ClassWizard's OLE Events tab to design the event and add whatever data the event requires.

Adding Methods to an OLE Control


Along with properties, methods are another way that a control's container can communicate with the control. As with properties, there are both stock methods and custom methods. Methods fall within the realm of OLE Automation, allowing the container application to communicate with the OLE control.

A method can do everything that a property can do, plus the following:

In the next two sections, you will add both a stock method and a custom method to your clock control. First you will add the stock method Refresh, which tells the control to update its user area. Because a stock method is implemented by the OLE control's base class, most of the work is performed using ClassWizard.

Adding a Stock Method

To add a method to an OLE control, you must start ClassWizard, choose the OLE Automation tab, and click the Add Method button. You will see the Add Method dialog box, shown in Figure 16.34, in which you define the method's external name. For a stock method, you must select one of the names available in the External name combo box: DoClick or Refresh. For your stock method, choose Refresh. Note that the implementation must be Stock and that all other data entry controls in the Add Method dialog box are disabled.

Figure 16.34. The Add Method dialog box in ClassWizard.

Click OK to add the new method. You are returned to ClassWizard's main dialog box, which should look like Figure 16.35. The new method, Refresh (which is highlighted), is preceded by an M, indicating that it is a method.

Figure 16.35. ClassWizard showing the Refresh method.

Next, rebuild the clock control. After the control has been built, you can test your new method. Start the Test Container program and load the clock control into it. In Test Container, select Edit | Invoke Methods to display the Invoke Control Method dialog box, shown in Figure 16.36. Your clock control actually has two methods: your new stock Refresh method and the preexisting AboutBox method that ControlWizard created.

Figure 16.36. Test Container's Invoke Control Method dialog box.

With Refresh selected, you can click the Invoke button to update the clock's display. The effects of the Refresh method probably will be a bit difficult to see, because the clock is updated automatically. However, if you set the timer interrupt interval from one second to one minute, the effect of the Refresh method is much more visible.

Next you will create a custom method for your clock control.

Adding a Custom Method

Besides stock methods, there are custom methods, which let your control be manipulated in ways that are unique to it. For example, you'll create a method to let the container set the alarm.

First, make a minor modification to your alarm function. Until now, the alarm function simply fired an event. Next, tell the user that the alarm is ringing. To do this, you simply add a call to MessageBeep() in your alarm handler:


//    Check for alarms. If past alarm time, sound it!

   if (m_alarmed &&

       m_alarmHours   <= newtime->tm_hour &&

       m_alarmMinutes <= newtime->tm_min &&

       m_alarmSeconds <= newtime->tm_sec)

   {//    It's an alarming event!

       m_alarmed = FALSE;

       FireAlarm(m_alarmHours, m_alarmMinutes, m_alarmSeconds);

       MessageBeep(MB_ICONEXCLAMATION);

   }

With this audible alarm, the user will know when a container initiates an alarm.

Now design your custom method. This method (call it SetAlarm) takes four parameters: hours, minutes, seconds, and a flag called alarmed that specifies whether the alarm is on or off.

To create your custom method, start ClassWizard. Click the Add Methods button in the OLE Automation tab to display the Add Method dialog box, shown in Figure 16.37. In this dialog, you must specify the external name (SetAlarm). ClassWizard provides the internal name, which you can modify if you like. You also can specify a return value for the method. In SetAlarm, I specified the return value as a short, which is the previous value for the alarm on/off flag. You also need to specify the hours, minutes, seconds, and alarmed parameters for your method.

Figure 16.37. The Add Method dialog box with the SetAlarm method shown.

When you're finished with the Add Method dialog box, click OK to add the new method, and then click OK to end ClassWizard. ClassWizard then adds a member to the DISPATCH_MAP, describing the new method, and also creates an empty function that you, the programmer, can fill with whatever code is necessary to perform the method's function.

In Clock, you must set the alarm's time and the flag that specifies whether the alarm is on or off. The following code fragment shows how this might be done. The changes appear in bold:


short CClockCtrl::SetAlarm(short Hours, short Minutes, short Seconds,

    short Alarmed)

{

    // TODO: Add your dispatch handler code here

    short nReturnCode = m_alarmed;

    m_alarmHours = Hours;

    m_alarmMinutes = Minutes;

    m_alarmSeconds = Seconds;

    m_alarmed = Alarmed;

    return (nReturnCode);

//    return 0;

}

You need only to save the new alarm time and the state of the alarm on/off flag. After you've added the SetAlarm method and the preceding changes to your SetAlarm() function, rebuild the clock control.

Figure 16.38 shows Test Container with the clock control installed. The Invoke Control Method dialog box lets the user set the alarm time and turn the alarm on or off.

Figure 16.38. Test Container's Invoke Control Method dialog box with SetAlarm.

A custom method lets the container application set virtually any possible attribute or control almost any aspect of an OLE control's operation. Don't be limited by the examples shown here. Let your imagination run wild.

Interfacing with the Clock OLE Control from a Visual C++ 4 Application


Prior to Visual C++ 4, there were few opportunities to actually use the Clock OLE control. You could include it in an Access form, but that was of little use to the typical Visual C++ programmer. Visual C++ programmers want to write their applications using Visual C++, not Access!

With Visual C++ 4, you can add OLE controls to dialog boxes. This support is independent of the actual control's design. (You don't need the source for the control. Visual C++ 4 determines what functionality is needed to interface with the control and creates a wrapper class to create this interface.)

For example, Figure 16.39 shows the Clock OLE control inserted into a dialog box (the application Clock Container's About box). It also shows the properties page for the Clock OLE control that Visual C++ 4 displays. Also, notice that the toolbox has a button for inserting the Clock OLE control.

Figure 16.39. The Clock OLE Control in an application.

When an OLE control is inserted into a project (and each OLE control must be explicitly inserted into each project that it will be used in), Visual C++ creates a wrapper class. (Some OLE controls use more than one class, but the Clock OLE control uses both the basic class and a class for the font.) The wrapper class source is generated automatically. You shouldn't modify it, because Visual C++ might need to regenerate the wrapper occasionally. (If you must modify the OLE control, you can reinsert it into the project and update the wrapper class.) Listing 16.1 shows the Clock OLE control's wrapper class.

Listing 16.1. The CClock wrapper class (CLOCK.CPP) created by Visual C++ 4.


//Machine-generated IDispatch wrapper class(es) created by Microsoft Visual C++

//NOTE: Do not modify the contents of this file. If this class is regenerated by

//  Microsoft Visual C++, your modifications will be overwritten.

#include "stdafx.h"

#include "clock.h"

// Dispatch interfaces referenced by this interface

#include "font.h"

/////////////////////////////////////////////////////////////////////////////

// CClock

IMPLEMENT_DYNCREATE(CClock, CWnd)

/////////////////////////////////////////////////////////////////////////////

// CClock properties

OLE_COLOR CClock::GetBackColor()

{

    OLE_COLOR result;

    GetProperty(DISPID_BACKCOLOR, VT_I4, (void*)&result);

    return result;

}

void CClock::SetBackColor(OLE_COLOR propVal)

{

    SetProperty(DISPID_BACKCOLOR, VT_I4, propVal);

}

OLE_COLOR CClock::GetForeColor()

{

    OLE_COLOR result;

    GetProperty(DISPID_FORECOLOR, VT_I4, (void*)&result);

    return result;

}

void CClock::SetForeColor(OLE_COLOR propVal)

{

    SetProperty(DISPID_FORECOLOR, VT_I4, propVal);

}

COleFont CClock::GetFont()

{

    LPDISPATCH pDispatch;

    GetProperty(DISPID_FONT, VT_DISPATCH, (void*)&pDispatch);

    return COleFont(pDispatch);

}

void CClock::SetFont(LPDISPATCH propVal)

{

    SetProperty(DISPID_FONT, VT_DISPATCH, propVal);

}

CString CClock::GetTimeFormat()

{

    CString result;

    GetProperty(0x1, VT_BSTR, (void*)&result);

    return result;

}

void CClock::SetTimeFormat(LPCTSTR propVal)

{

    SetProperty(0x1, VT_BSTR, propVal);

}

short CClock::GetDisplay24Hour()

{

    short result;

    GetProperty(0x2, VT_I2, (void*)&result);

    return result;

}

void CClock::SetDisplay24Hour(short propVal)

{

    SetProperty(0x2, VT_I2, propVal);

}

short CClock::GetAlarmHours()

{

    short result;

    GetProperty(0x3, VT_I2, (void*)&result);

    return result;

}

void CClock::SetAlarmHours(short propVal)

{

    SetProperty(0x3, VT_I2, propVal);

}

short CClock::GetAlarmMinutes()

{

    short result;

    GetProperty(0x4, VT_I2, (void*)&result);

    return result;

}

void CClock::SetAlarmMinutes(short propVal)

{

    SetProperty(0x4, VT_I2, propVal);

}

short CClock::GetAlarmSeconds()

{

    short result;

    GetProperty(0x5, VT_I2, (void*)&result);

    return result;

}

void CClock::SetAlarmSeconds(short propVal)

{

    SetProperty(0x5, VT_I2, propVal);

}

short CClock::GetAlarmed()

{

    short result;

    GetProperty(0x6, VT_I2, (void*)&result);

    return result;

}

void CClock::SetAlarmed(short propVal)

{

    SetProperty(0x6, VT_I2, propVal);

}

/////////////////////////////////////////////////////////////////////////////

// CClock operations

void CClock::Refresh()

{

    InvokeHelper(DISPID_REFRESH, DISPATCH_METHOD, VT_EMPTY, NULL, NULL);

}

BOOL CClock::SetAlarm(short Hours, short Minutes, short Seconds, short Alarmed)

{

    BOOL result;

    static BYTE parms[] =

        VTS_I2 VTS_I2 VTS_I2 VTS_I2;

    InvokeHelper(0x7, DISPATCH_METHOD, VT_BOOL, (void*)&result, parms,

        Hours, Minutes, Seconds, Alarmed);

    return result;

}

void CClock::AboutBox()

{

    InvokeHelper(0xfffffdd8, DISPATCH_METHOD, VT_EMPTY, NULL, NULL);

}

For example, your application could call the CClock::SetAlarm() function to set an alarm time. This function maps the SetAlarm custom method that you created earlier. In fact, all of the Clock OLE control's methods, events, and properties can be accessed using the CClock class!

A Few More Bells and Whistles


What else could your Clock OLE control have? One enhancement is to expand what the user sees when the clock is running: You could display both the current time and the alarm time. It also might be a good idea to have some visual indicator that there is an active alarm.

Perhaps a digital clock isn't the best display. After all, this is a graphical environment. You could make the time display an analog format.

Most digital clocks offer some kind of date display. Your clock control might benefit from having a date display along with the time. Perhaps the date display could be optional, or the time could change to the date whenever the user clicks on the control.

A dynamic calendar display, in which previous and future months could be viewed, would enhance the clock control. Perhaps the date could be displayed when the left mouse button is pressed and the calendar could be displayed when the right mouse button is pressed.

Enhancements to the clock control are almost limitless. Adding a simple reminder system (to remind the user to perform a certain task, such as backing up or saving data) isn't difficult. Of course, you would want multiple reminders and multiple alarms. If you have reminders and multiple alarms, why not add an address book feature as well? The possibilities are endless, and all within a single OLE control. How endless? Take a look at the Calendar OLE control that is supplied with Access 7 to see what you can do with an OLE control. (The Calendar OLE control is discussed in Chapter 17, "Using OLE Controls and Automation with Visual C++ Applications.")

License Validation


The OLE control system includes a feature called license validation, which lets you determine who can distribute or use your OLE control at design time. To use license validation, you ship your control with the .LIC license file to other developers. These other developers then ship the control embedded in their application but don't ship the .LIC file. This prevents the users of the application in which your control is embedded from reusing the control on their own, because the OLE control can't be used in design mode without the license file. However, you must make sure that the people you've licensed to use your control and to whom you've given the .LIC file never distribute the .LIC file.

WARNING

You shouldn't rely on the default implementation of the licensing validation routines, because it can easily be defeated. You should write your own validation routines, keeping security in mind. Simple comparisons with text files probably won't prove sufficiently secure.

Adding License Validation to a New Control

To add license validation to a new OLE control, you simply check the License validation check box in ControlWizard. This adds the necessary code to check license validation.

With license validation, Windows looks for a specific string in the .LIC file. For example, if you had generated the Clock OLE control with license validation, the Clock OLE control's license file would resemble Listing 16.2.

Listing 16.2. CLOCK.LIC: The license file for Clock.


Copyright  1995

Warning:  This product is licensed to you pursuant to the terms of the

license agreement included with the original software, and is

protected by copyright law and international treaties.  Unauthorized

reproduction or distribution may result in severe civil and criminal

penalties, and will be prosecuted to the maximum extent possible under

the law.

To actually verify the license file, Windows looks at the first line in the file. This line is compared to the license string that is contained in the program.

Adding License Validation to Clock

To add licensing to an existing control, you need to make several changes to the control. First, you must have a .LIC file. If you can't come up with your own, you can copy the file shown in Listing 16.2 and change its name to the name you're using.

Next, in the control's ?????CTL.H file, you must change the class factor and guid macro to include license checking. The following code fragment shows the necessary changes:


// DECLARE_OLECREATE_EX(CClockCtrl)  // Class factory and guid

BEGIN_OLEFACTORY(CClockCtrl)         // Class factory and guid

       virtual BOOL VerifyUserLicense();

       virtual BOOL GetLicenseKey(DWORD, BSTR FAR*);

   END_OLEFACTORY(CClockCtrl)

First you must comment out the original class factory line:


// DECLARE_OLECREATE_EX(CClockCtrl)   // Class factory and guid

Then you must add a new class factor macro:


BEGIN_OLEFACTORY(CClockCtrl)         // Class factory and guid

       virtual BOOL VerifyUserLicense();

       virtual BOOL GetLicenseKey(DWORD, BSTR FAR*);

   END_OLEFACTORY(CClockCtrl)

The final change is to add a single parameter to the control's .ODL file. This file contains the control's typelib information.

The change is simply the addition of the keyword licensed to the help file line. In the following code fragment, I've used the Clock OLE control's name and uuid. In other controls, the control's name and uuid will be different than the values shown in these fields. The keyword you must add appears in bold:


[ uuid(D0CA5D3C-2F3C-11CF-A0CB-444553540000), licensed,

          helpstring("Digital Clock Control"), control ]

        coclass Clock

        {

                [default] dispinterface _DClock;

                [default, source] dispinterface _DClockEvents;

        };


NOTE

Gotcha! When Visual C++ 2.0 and 4 compile a project, they create subdirectories for the output. Creating subdirectories lets you have both debugging and release versions of the same project at the same time. In Visual C++ 1.5 and earlier versions, creating a release version when a debugging version exists and vice versa overwrites the debugging version. Visual C++ 2.0 and 4 create a separate subdirectory for each executable type. (An OLE control has four types: Unicode release, Unicode debug, ANSI release, and ANSI debug.)

The .LIC file must be in the same directory as the executable for the control. This means that you must manually copy the .LIC file to the directory that the control's .OCX file is in. If you don't have the .LIC file in the same directory as the control's executable file, the license verification will fail.

Time lost finding this problem: two hours.


How does licensing work? The call to AfxVerifyLicFile() opens the specified license file and reads the first line. If the first line in the license file matches the provided license string, the license verification is deemed a success.

Now that you know this, there are a few issues that you need to keep in mind:

WARNING

Microsoft really expects you to write your own validation function and not rely on the Visual C++ supplied function, which simply compares the first line in the license file with a constant found in the program. Your license validation routine should be a bit more sophisticated than this.

Overall, the default license validation provides a weak method of ensuring that a user or developer has actually licensed your control. It's easily defeated by anyone who is reasonably skilled, so don't depend on it for absolute security. If you rewrite the validation routine, you can enhance the OLE control's security substantially.

Using Your OLE Control Outside Visual C++ Programs


It's relatively easy to use your OLE control. Earlier you saw how to use an OLE control in a Visual C++ program. However, OLE controls have value in other applications as well, such as Visual Basic and Access, for example.

First, you must have an application that can use an OLE control. At the time this book was written, the most common nonprogramming platform application that could use OLE controls was Access. Access has been able to use OLE controls since version 2.

Inserting an OLE control into an Access form or report is simple. When you're designing the form, decide where you want to insert the OLE control. Then, from Access, select Insert | Object.

Figure 16.40 shows your clock control installed in an Access form. An appropriate font has been set, the digits are colored dark blue, and the background is set to match the form's background. Notice how well the custom control blends in with the form.

Figure 16.40. Your clock control in an Access form.

Shipping Your OLE Control


After you've developed your new OLE control, you must distribute it to your users. The installation process can be either a separate step (in which your product is only the OLE control) or part of another application's installation process.

For the 16-bit versions of your OLE controls, you should change the extension of the control's file from .DLL to .OCX. This will cause all the OLE controls to have the same file extension. Make this change to the DLL's name before you register the control, because the system won't find a control that is renamed after registration. OLE controls that are created using one of the 32-bit versions of Visual C++ are already named with the .OCX extension.

Microsoft recommends that you install your OLE controls in the Windows system directory on the user's machine. Your OLE control installation program can obtain the name of the system directory by using the GetSystemDirectory() function. Make sure that you don't install your OLE controls in the main Windows directory.

A number of redistributable DLL files might be needed with your OLE control. These files should be placed on the target system if either of the following is true:

Using a proper installation program such as InstallShield can make version checking much easier. You don't have to distribute the entire OLE set of DLL files. Your OLE control will be used only in an OLE container application that will already have installed the necessary OLE DLL files.

Before a user can use your OLE control, the control must be registered. This is done by having the container application call the CCtrlFactory::UpdateRegistry(BOOL bRegister) function. This function takes one parameter, bRegister, which, if TRUE, registers the control, and if FALSE, unregisters the control. In order to let all developers use your control (and insert the control into a program), there must be a method to register the control. For example, Visual C++ users can use the REGSVR32.EXE program to register a control if necessary.

Although some OLE control container applications have a way to register an OLE control, you shouldn't depend on the availability of such a feature. A developer could register a control by calling either REGSVR.EXE or REGSVR32.EXE (as appropriate for the target version of Windows).

If your control is licensed, you must make sure that the license control file (the .LIC file) is available to any developer who will use your control, and that the license file is installed in the same directory that the control has been installed in. You and the developers using your control should take care not to inadvertently distribute the .LIC file to end users if you intend to enforce licensing!

Summary


This chapter introduced the OLE control development tools, which let the developer create OLE controls with a minimum of effort. The following topics were covered:

You also learned that it isn't difficult to develop OLE controls. Even if you haven't had any experience in using OLE, you can develop OLE controls using the ControlWizard and ClassWizard applets that are supplied with Visual C++ 4.0.

You also developed the Clock OLE control, which can be embedded into any container application that supports OLE controls. It also can be embedded into an OLE container application, where it will function as an OLE Automation Server.

The following topics also were covered:

Previous Page Page Top TOC Next Page