Today you learn ways to turn a good Windows application into a great Windows application. Specifically, I discuss the following:
This discussion continues tomorrow as you look at implementing advanced Windows programming features in your Delphi applications.
No, I'm not talking about pretty lights around the front window of your house. What I am talking about are features such as toolbars and status bars. These features are often called window decorations. This section deals with these types of decorations and how to implement them in your application.
A toolbar (also called a control bar or a speedbar) is almost standard equipment for Windows programs today. Users expect certain amenities, and a toolbar is one of them. A top quality toolbar should have the following features and capabilities:
Some of the features in this list are optional features that not every toolbar needs to have. With Delphi, implementing these toolbar features is easy to accomplish. A little later in the chapter I talk about command enabling in the section "Adding Functionality with Command Enabling." I'll save discussion of command enabling for toolbar buttons for that time.
NOTE: It is considered good practice to place on the toolbar only buttons that have corresponding menu items. The toolbar is an alternative to using the menu. It should not contain items found nowhere else in the program.
On Day 8, "Creating Applications in Delphi," I said that the easiest way to construct a toolbar is to use the Application Wizard. Even if you already have a partially written application, you can still use the Application Wizard to create a toolbar. Just generate an application with the Application Wizard, copy the panel with the toolbar to the Clipboard, reopen your original application (don't bother saving the Application Wizard application), and paste the toolbar into your application from the Clipboard. Slick and easy.
However, the Application Wizard doesn't give you everything you could possibly need in a toolbar. Most notably, the Application Wizard uses the old method of creating a toolbar--with a panel and speed buttons. The preferred way of creating a toolbar is to use the ToolBar and CoolBar components (found on the Win32 tab of the Component palette). Let's take a look at those components next.
NOTE: The CoolBar and ToolBar components require version 4.72.2106.4 or later of COMCTL32.DLL, which should have been installed as part of the Delphi installation. If you don't have the latest version of this DLL, you can find it at Microsoft's Web site (http://www.microsoft.com). When you deploy your applications using these components, you should install COMCTL32.DLL version 4.70 or later. Be sure you are using a good installation program so that you don't overwrite a new version of this DLL when you install your application.
The CoolBar component is an encapsulation of the Win32 cool bar (sometimes called a rebar). This component is a specialized container control. Most of the time the CoolBar is used as a container for toolbars, but its use isn't limited strictly to toolbars.
A cool bar has bands that can be moved and resized at runtime. The bands show a sizing grip on the left side, giving the user a visual cue that the band can be moved or sized. Cool bar bands are represented by the TCoolBand class. A cool bar band can contain only one component. Usually that component is a toolbar, but it can be a combo box or any other component. Let's do an exercise to better understand how the CoolBar component works:
Now your form looks like Figure 13.1.
FIGURE 13.1. The form with a cool bar and three bands.
Now run the program. Experiment with the cool bar bands. Drag them up or down or resize them. Notice that as you drag the bands up or down, the cool bar resizes as needed and the memo always fills the remaining client area.
Cool bar bands are accessed through the Bands property. This property is a TCoolBands, which is a list of TCoolBand components. If you want to hide the second band, you can do this:
CoolBar.Bands[1].Visible := False;
You can add bands in two ways. As you have already seen, you can create a band by dropping any component on the cool bar, but you can also use the Bands Editor. To invoke the Bands Editor, double-click the cool bar or click the ellipsis button next to the Bands property in the Object Inspector. You add bands by clicking the Add button; you delete bands by clicking the Delete button. The Move Up and Move Down buttons enable you to change the order of the bands.
NOTE: If the AutoSize property is set to True, you will have to turn it off temporarily if you want to add new bands by dropping components on the cool bar. Set the AutoSize property to False, make the cool bar higher, drop a component on the cool bar, and set the AutoSize property to True again.
NOTE: When you select a band in the Bands Editor, the Object Inspector displays the band's properties. Figure 13.2 shows the Bands Editor and the Object Inspector when a band is selected.
FIGURE 13.2. The cool bar Bands Editor.
The Bitmap property enables you to set a background bitmap for a band. To select an image that will appear to the left of the band, you use ImageIndex. ImageIndex requires the ImageList property of the cool bar to be set to a valid TImageList. You can set a band's minimum height and width through the MinHeight and MinWidth properties. To make a band immovable, set the FixedSize property to True.
A cool bar can be either vertical or horizontal. By default the Align property is set to alTop. To make a vertical cool bar, change the Align property to alRight or alLeft. Some components, when placed on a cool bar, automatically orient themselves based on whether the cool bar is vertical or horizontal. Another way to change the orientation of the cool bar is by setting the Vertical property.
The Bitmap property enables you to set a background bitmap for the cool bar. The bitmap you choose will be tiled to fill the cool bar's background. Note that this sets the background bitmap for the cool bar itself, not for any individual bands on the cool bar (as discussed in the previous section). You use the ImageList property to set the image list that the bands will use to display an image to the left of any band that has its ImageIndex property set.
The AutoSize property determines whether the cool bar will resize itself when bands are moved. You saw the effect of the AutoSize property in the preceding exercise.
NOTE: Check out the TControlBar component on the Additional tab of the Component palette. TControlBar is a native VCL component that works very much like a cool bar. This component does not rely on COMCTL32.DLL as does TCoolBar, so it is less susceptible to the whims of Microsoft.
The ToolBar component encapsulates the Win32 toolbar control. The toolbar will automatically arrange and size the controls placed on the toolbar so that they all have a consistent height. You can use the ToolBar component with or without a cool bar. If you have only a single toolbar, use the toolbar without a cool bar. If you have multiple toolbars that you want to enable the user to move, place two or more toolbars on a cool bar.
Creating a toolbar and adding buttons to it is very easy. If your toolbar buttons will have glyphs (and most do), you have to use an ImageList component for the glyphs. To illustrate how to build a toolbar with the ToolBar component, let's again go back to the ScratchPad program. You'll tear it apart and put it back together.
If you recall, the toolbar you created for ScratchPad originally was just a placeholder. The first thing you need to do is get rid of the old toolbar by performing these steps:
Now you can start adding components back again. The first thing to do is add a cool bar and a toolbar. You don't really need a cool bar at this stage because you have only one toolbar, but you might want to add another toolbar later, so it's best to plan ahead. Perform these steps:
Now you begin adding buttons to the toolbar; you will add several buttons and a few spacers. At first the buttons won't have glyphs on them, but you'll take care of that later. For now, follow these steps:
TIP: Buttons and spacers added to the toolbar always appear to the right of the toolbar's last control. You can't insert a button at a specific location in the toolbar, but after a button or spacer is added, you can drag it to a different location on the toolbar. The existing buttons will make room for the new button.
That finishes the first set of buttons (except for the glyphs). You are about to add a second set of buttons, but before you do, there needs to be a little separation between the first set of buttons and the second. Back to work:
Your form now looks like the one shown in Figure 13.3.
You now have a good start on the toolbar, but the toolbar buttons don't do anything because you haven't assigned any event handlers to their OnClick events. Let's do that next.
Obviously this toolbar is missing something. You need to add glyphs to the toolbar buttons. To do so, you must add an ImageList component to the form by following these steps:
FIGURE 13.4. The ImageList Editor after adding three images.
TIP: You can select multiple images in the ImageList Editor's Add Images dialog box and add them all to the image list at one time.
Now you are ready to hook the image list to the toolbar. Click on the toolbar. Locate the Images property in the Object Inspector and choose ImageList from the drop-down list. If you did everything right, your buttons now have glyphs. You probably didn't notice, but each time you added a toolbar button, Delphi automatically incremented the ImageIndex property for the button. Because you created the buttons and images in the same order, the glyphs on the buttons should be correct. If a button is wrong, you can either change the button's ImageIndex property or go back to the ImageList Editor and change the order of the images in the image list.
TIP: To rearrange the images in an image list, drag them to a new position in the ImageList Editor.
Right now you have glyphs only for the buttons in the enabled state. You also need glyphs that will be displayed when the buttons are disabled. You aren't disabling the toolbar buttons yet, but you will be before the day is done. There are two ways to implement the disabled button glyphs:
Certainly the easier of these two methods is to let the toolbar create the disabled state buttons automatically. Most of the time this is sufficient. Sometimes, however, the algorithm for creating the disabled buttons glyphs doesn't work well. (The glyphs end up losing definition and don't look quite right.) This happens with buttons that don't have enough contrasting colors. In that case, you can create a second image list that contains the disabled button glyphs. Set the DisabledImages property of the toolbar to the image list containing the disabled glyphs, and the rest is automatic. For ScratchPad you're going to go with the automatic disabling of toolbar buttons, so nothing further is required.
Poor old ScratchPad is back together again. This is a good time to save the project. After saving the project, click the Run button and give the program a workout. Click the buttons and see whether they do what they are supposed to do. If all went well, you have a working program again. If your program won't compile, review the steps and try to fix the problem. If all else fails, you can refer to the ScratchPad project of the book's code, available at http://www.mcp.com/info (Day 13).
I covered nearly everything there is to be said about tooltips and hints in the discussion of components on Day 7, "VCL Components"; again on Day 8, "Creating Applications in Delphi," when adding hint text support for the ScratchPad program; and again today when rebuilding the toolbar.
There is one issue I didn't talk about, and that is changing the tooltip properties. The TApplication class has four properties that control how the tooltips behave. Table 13.1 lists these properties and their descriptions.
Property | Description |
Hintcolor | Sets the background color of the Tooltip window. Default: CLINFOBK |
HintHidePause | Controls how long to wait (in milliseconds) before hiding the tooltip if the mouse cursor remains stationary over the component. Default: 2500 milliseconds |
HintPause | Controls the interval between the time the mouse cursor is paused over a component and the time the tooltip appears (in milliseconds). Default: 500 milliseconds |
HintShortPause | Controls how long to wait before showing tooltips after they have already been shown--for example, when the user is roaming over a group of toolbar buttons. Default: 50 milliseconds |
The default values for these properties are sufficient for most applications. Still, if you need to change any hint properties, you have that option.
HOUSE RULES: TOOLBARS AND HINTS
- Don't use tooltips on controls where they will be in the way when the user wants to read the text in the control. In particular, don't use tooltips for edit controls and combo boxes. At a minimum, give the user the option to turn off tooltips for those types of controls.
- Keep your tooltip hint (the short hint) brief and to the point.
- Make your status bar hint (the long hint) more descriptive and meaningful.
- Consider giving your users the option of turning off hints altogether.
Because the ToolBar component is so versatile, nothing special needs to be done to add other types of controls to your toolbar. The most common type of control to add to a toolbar is a combo box. You can use a toolbar combo box to select a font, a configuration option, a zoom setting. . . the possibilities are endless.
To add a component to the toolbar, select the component from the Component palette and drop it on the toolbar. The toolbar will take care of aligning the component. Add spacers as necessary to separate components visually. When the component is on the toolbar, you deal with it just as you would a component on a form. I could try to complicate this for you, but the truth is, it's just that simple. If you've never tried to implement a combo box on a toolbar using the Windows API, you cannot appreciate how much work Delphi saves you. Take my word for it--it's significant.
Toolbars come in many shapes and sizes, and Delphi makes creating and implementing toolbars very easy. With Delphi, you no longer have the excuse, "That's too hard!" In fact, you might even enjoy creating toolbars with Delphi.
Dockable toolbars are common in many Windows programs. Dockable toolbars are a paradox of sorts. On the one hand, they are a cool feature and most power users expect a good application to have dockable toolbars. On the other hand, I doubt anyone actually uses the docking capability of most dockable toolbars. Still, dockable toolbars are easy enough to implement in Delphi, so you might as well provide them.
NOTE: The docking features discussed in this section apply to any windowed control, not just to toolbars.
Making a toolbar dockable requires two steps:
After you set these two properties, you can drag your toolbar around the screen. Dragging the toolbar around the screen doesn't get you very much, though. In order for dockable toolbars to make sense, you have to have a target for the drop part of the drag-and-drop equation.
A dockable toolbar needs to have a place to dock. As Roger Waters said, "Any fool knows a dog needs a home." Home for a dockable toolbar is a dock site. A dock site is any windowed component that has its DockSite property set to True. Components that are typically used as dock sites are TCoolBar, TControlBar, TPageScroller, TPanel, and TPageControl. There are other controls that have a DockSite property, but those controls are less likely to be used as dock sites.
An exercise helps illustrate the use of dock sites. Follow these steps:
Now run the program. Drag the toolbar from dock site to dock site. Notice how the toolbar changes its orientation when you drag it to the cool bar on the left side of the form.
Let's experiment some more. Set each of the CoolBar components' AutoSize properties to True. This will cause each CoolBar to change its size based on the controls it contains. Now run the program again and move the toolbar to the individual dock sites. Notice that each cool bar is nearly invisible until the toolbar is docked to the site. Then the cool bar expands to contain the toolbar.
A toolbar can be made into a floating toolbox by simply dragging the toolbar off its dock site and dropping it anywhere (anywhere other than another dock site, that is). The toolbar becomes a floating window. You can specify the type of window that should host the floating dock site by setting the FloatingDockSiteClass property to the name of the class you want to act as the parent to the floating toolbar. For example, suppose that you design a form that has all the characteristics you want for a custom floating toolbox, and that the form is called MyToolBox. In that case, you could cause this form to be the host for a floating toolbar with this code:
ToolBar.FloatingDockSiteClass := TMyToolBox;
When the toolbar is undocked and the mouse button released, Delphi will automatically create an instance of the TMyToolBox class and place the toolbar in the form for that class. To dock the floating toolbox again, simply drop it on any dock site. To make a dock site accept a floating toolbox, you must respond to the OnDockOver and OnDockDrop events for the dock site. In the OnDockDrop event handler, call the ManualDock method of the toolbar to cause the toolbar to dock.
A status bar is another feature that makes an application more marketable. Not all applications benefit from a status bar, but many can. The VCL StatusBar component, which encapsulates the Win32 status bar control, makes creating status bars a breeze. First, take a quick look at the major properties of the StatusBar component, listed in Table 13.2.
Property | Description |
AutoHint | Automatically displays hints in the status bar when the mouse cursor passes over any component whose Hint property has been set. |
Panels | For status bars with multiple panels. This property defines the individual panels. |
SimplePanel | Determines whether the status bar shows a simple panel or multiple panels. |
SimpleText | The text for the status bar's simple panel. |
Property | Description |
SizeGrip | Determines whether the status bar displays the sizing grip in the lower-right corner. The sizing grip provides an area that the user can drag to size the window. The absence of the sizing grip doesn't prevent the window from being sized, but the presence of the size grip makes sizing a window easier. |
UseSystemFont | Always uses the current system font, overriding the settings of the Font property. This is particularly useful for users who use Plus pack themes. |
As you can see from this table, a status bar can be a simple status bar or have multiple panels. Let's discuss this choice next.
A status bar can be either a simple status bar or a complex status bar. A simple status bar has a single panel that occupies the entire status bar. If you want a simple status bar, set the SimplePanel property to True. The SimplePanel property acts as a toggle. You can switch between a simple and a complex status bar at runtime by setting SimplePanel to True or False, accordingly.
A complex status bar is one with multiple panels. If you elect to use a complex status bar, you can use the StatusBar Panels Editor to set up the panels you want to see on your status bar. To invoke the StatusBar Panels Editor, double-click on the Value column of the Panels property. To add a panel, click the Add New button on the StatusBar Panels Editor. To delete a panel, click the Delete Selected button. To edit a panel, select the panel and then make changes to the panel's properties in the Object Inspector. Figure 13.5 shows the StatusBar Panels Editor and Object Inspector when editing panels.
FIGURE 13.5. The StatusBar Panels Editor.
NOTE: The individual panels in a complex status bar are instances of the TStatusPanel class.
Most of the properties are self-explanatory, but a couple require further note. The Text property contains the text that will be displayed in the panel. You can also use the Text property at runtime to change the text in the panel. Setting the status bar text is discussed a little later; you don't need to supply text for the panel at design time if you are going to change the text at runtime.
You can set the Style property to either psText or psOwnerDraw. If the Style is set to psText (the default), the panel behaves as you would expect. The text is aligned in the panel according to the value of the Alignment property. If the Style is set to psOwnerDraw, it is up to you to draw any text or image that is displayed in the panel. Owner drawing of panel items is discussed later in the section "Owner-Drawn Status Bar Panels."
The Width, Bevel, and Alignment properties for the panel are straightforward. Experiment with these properties to see how they affect your status bar's appearance.
NOTE: In the Form Designer you immediately see the results of changes made to the status bar via the StatusBar Panels Editor. Position the StatusBar Panels Editor so that you can view the status bar as you work with the StatusBar Panels Editor. Each time you make a change, it will be reflected in the Form Designer.
When you are done adding panels to the status bar, close the StatusBar Panels Editor and return to the Form Designer.
NOTE: When you modify the Panels property of a StatusBar component, the Form Designer automatically sets the SimplePanel property to False. The assumption is that if you are using multiple panels, you don't want to have a simple status bar.
There are two ways to change text in a status bar:
Manually changing the text in the status bar is simple, particularly if you have a simple status bar. When the SimplePanel property is True, you can set the SimpleText property to the text you want displayed in the status bar:
StatusBar.SimpleText := `This shows up in the status bar.';
In the case of complex status bars, changing the text is only slightly more complicated. If you want to change the text for the first panel of a complex status bar, you would use something like this:
StatusBar.Panels[0].Text := `Status Bar Text';
The Panels property of the StatusBar component has a property called Items that is an array of panels in the status bar. Setting the Text property for an element in the Items array changes the text for that panel (because Items is the default array property for the Panels object, you don't have to specifically reference Items). As you can see, the array is 0-based. The first panel in the status bar is array element 0.
Automatic status bar hint text doesn't require much in the way of explanation. All you have to do is set the AutoHint property to True. The rest is, as the property name implies, automatic.
NOTE: You can still modify the status bar's text manually even when using AutoHint. There's nothing to stop you from changing the text manually, but remember that the text will be replaced the next time the mouse passes over a component with hint text.
Earlier I said that a panel's Style property can be either psText or psOwnerDraw. When you set a panel's style to psOwnerDraw, you must take the responsibility of drawing anything in the panel that needs to be displayed there. It is unlikely that you are going to go to the trouble of using an owner-drawn panel just to display text. Usually it means you are going to display some sort of icon or bitmap in the status bar. Regardless of what is being drawn on the panel, the steps are the same:
Obviously, the real work here is going to take place in the event handler for the OnDrawPanel event. The declaration for the OnDrawPanel event handler looks like this:
procedure TForm1.StatusBar1DrawPanel(StatusBar: TStatusBar; Panel: TStatusPanel; const Rect: TRect);
The StatusBar parameter is a pointer to the status bar. Usually you have a pointer to the status bar anyway (the Name property of the StatusBar component), so this parameter is not all that useful unless you are using multiple owner-drawn status bars. The Panel property is a pointer to the particular panel that currently needs drawing. You can use this parameter to determine which panel needs drawing if you have more than one owner-drawn panel in your status bar. The Rect parameter contains the panel's size and position. The Rect parameter is important because it tells you the exact dimensions of the drawing area.
The OnDrawPanel event handler is called once for each panel that has its Style property set to psOwnerDraw. If you have only one panel to draw, you don't have to worry about much except the Rect parameter. If you have multiple panels to draw, you must first determine which panel to draw and then do your drawing. An illustration might help to explain this. The book's code includes a program called StatBar that illustrates some of the things you can do with status bars. Run the program and examine its source for tips on implementing status bars in your applications. Figure 13.6 shows the StatBar program running.
FIGURE 13.6. The StatBar program with owner-drawn status bar panels.
As you can see, the status bar in this program has multiple panels. The middle three panels are owner-drawn. The panels marked OVR and EXT simulate the status bar on a word processing program or code editor. In those types of programs, the Overtype or Extended Selection modes might be on or off. If the mode is on, the text in the status bar panel shows in black. If the mode is off, the text has a 3D disabled-text appearance. The third owner-drawn panel displays a stock Windows icon to illustrate the use of a graphic on a status bar. Run the program and experiment with it to learn how it works.
Listing 13.1 shows the OnDrawPanel event handler from the StatBar program. Examine it and read the comments to understand what is going on in the code.
procedure TMainForm.StatusBarDrawPanel(StatusBar: TStatusBar; Panel: TStatusPanel; const Rect: TRect); var R : TRect; Icon : HIcon; begin with StatusBar.Canvas do begin { Create a temporary TRect object. The Rect parameter { is const so we can't change it. } R := Rect; { Check to see if panel 3 is the panel which needs { to be drawn. If so, draw an icon in the panel. } if Panel.Index = 3 then begin { Load one of the stock Windows icons. This time { using the API is easier than using VCL. } Icon := LoadIcon(0, IDI_HAND); { Draw the icon and shrink it down to 15 x 15 pixels. } { Center it in the panel, too. } DrawIconEx(Handle, Rect.Left + 6, 3, Icon, 15, 15, 0, 0, DI_NORMAL); { Nothing more to do. } Exit; end; { This rather lengthy if statement checks to see if { either the Overtype Mode or Extended Selection { check boxes are checked. If so, then what we need { to do is to draw the text twice. First, we draw it { in white. Then we draw it again, offset by 1 pixel, { in gray. The effect is a 3D disabled-text look. } if ((Panel.Index = 1) and (OvrMode.Checked = False)) or ((Panel.Index = 2) and (ExtendedSel.Checked = False)) then begin { Move over and down one pixel for the offset. } Inc(R.Left); Inc(R.Top, 2); { Change the text color to white. } Font.Color := clWhite; { Set the backround mode to transparent so the { text appears hollow and so that the white { text can be seen under the gray. } Brush.Style := bsClear; { Draw the text using the API function DrawText. } { I use DrawText because it allows me to center { the text both horizontally and vertically within { the given rectangle. } DrawText(Handle, PChar(Panel.Text), -1, R, DT_CENTER or DT_VCENTER or DT_SINGLELINE); { Set the color to gray because we're going to { draw the text in gray in a moment. } Font.Color := clGray;
{ Set the rect back to the original size. }
Dec(R.Left); Dec(R.Top, 2); end; { Display the text. If the item is not disabled then { the default color (black) is used to draw the text. } { If the item is disabled, then the text color has { been set to gray by the code above. } DrawText(Handle, PChar(Panel.Text), -1, R, DT_CENTER or DT_VCENTER or DT_SINGLELINE); end; end;
This code might seem intimidating, but most of it is comment lines. The code itself is relatively simple. The comment lines explain what is happening at each step: The 3D appearance for the disabled text is accomplished by drawing the text once in white and then drawing it again in gray with a slight offset. The result is that the text looks recessed. The icon is displayed using the Windows API functions LoadIcon and DrawIconEx.
Owner drawing of status bar panels is daunting at first, but you'll soon find out that it's not all that bad. You might write Windows applications for a long time and never need owner-drawn panels in your status bar. If you ever need them, however, you'll know that's not impossible to accomplish.
Command enabling is the process of enabling or disabling buttons depending on current conditions. For example, there's not much point of having the Cut or Copy button or menu item enabled for a text editor when no text is currently selected. Likewise, if there is no text in the Clipboard, the Paste button should be disabled.
Command enabling isn't difficult, especially with Delphi's new TActionList component. Still, it takes time to get right. It takes time because you have to pay attention to detail. (Sometimes it is attention to detail that separates the great applications from the mediocre applications.)
The TAction class provides a convenient way of performing command enabling. TActionList, the nonvisual component that manages actions, is found on the Additional tab of the Component palette. TActionList, as its name implies, contains a list of TAction objects. You create an action and then assign that action to any controls that need to be enabled or disabled based on that action. By controls I mean menu items, toolbar buttons, context menu items, and so on.
Let's take the Edit|Cut menu item, for example. You could have at least three objects associated with this particular task:
You create actions with the ActionList Editor. Given the Edit|Cut example, you would create an action for Cut called, say, CutAction. Then, using the Object Inspector, you assign CutAction to the Action property of each of the objects that correspond to the cut operation (toolbar buttons and menus, for example). At runtime, when you need to enable the Cut option, you can do so with just one line of code:
CutAction.Enabled := True;
This will enable all components with their Action properties set to CutAction. Disabling the Cut items is as simple as assigning False to the Enabled property of the action. The OnUpdate event of TAction and TActionList provides a convenient place to put your command-enabling code.
Command enabling with TAction is something that you have to experience to fully appreciate. You add command enabling to ScratchPad in the next section.
In this section you implement command enabling for the ScratchPad program. First you set up the ActionList component, and then you hook up the various components to the action list.
It all starts with the ActionList component, which is the heart of the command-enabling system in VCL. First you add the actions for the Edit menu items. After that, you add an action for the File menu's Save and Save As menu items. The following steps will take you through the ActionList setup process.
Creating the Edit Menu Actions
The following steps show you how to create actions for the Edit menu's Cut, Copy, and Paste items. Perform these steps:
You have now created the actions for the primary Edit menu items.
Creating the File Menu Actions
Next you need to create the actions for the File menu's Save and Save As menu items. Perform these steps:
NOTE: You can create categories of actions if you have several dozen actions to keep track of. To create an action category, simply set the Category property of one or more actions to any text you want. For example, to create a category for the edit actions created earlier, set each action's Category property to Edit. Action categories are simply for organization and don't have any bearing on the way actions operate.
The next step is to attach the actions you just created to the various menu items and toolbar buttons to which those actions correspond. Perform these steps:
You probably didn't notice, but when you assigned SaveAction to the Action property of a component, that component's Caption, Checked, Enabled, HelpContext, Hint, ImageIndex, ShortCut, and Visible properties all changed to the values of those properties in the SaveAction object. It is important to understand that the action's properties will overwrite the properties of any component that the action is assigned to. You must set up the action with that in mind. That is why I had you change the Hint and ItemIndex properties when you created the actions. Had you not done that, the hint text and toolbar button glyphs would not be correct.
Now each of the components just listed is hooked to an action. When a particular action changes, any components hooked to that action will change as well. Take the following code, for example:
SaveAction.Enabled := False;
When this code executes, any components with their Action property set to SaveAction will be disabled (the main menu Save item and the Save button on the toolbar). To enable all items associated with this action, use this code:
SaveAction.Enabled := True;
It's as simple as that. Because the main menu and toolbar Save components have their Action property set to SaveAction, the following two code snippets are equivalent:
{ One-shot using the Action. } SaveAction.Enabled := False; { The hard way. } FileSave.Enabled := False; FileSaveBtn.Enabled := False;
Granted you save only one line of code in this example, but if you have several components that need to be enabled or disabled, actions can save you a lot of time. After you create an action and associate that action with one or more components, command enabling is as simple as one line of code. The beauty of this system is that it doesn't matter how many components you need to enable or disable. It's still just one line of code.
Run the ScratchPad program. Notice that the Cut and Copy buttons are disabled. Type some text in the memo and highlight it. The Cut and Copy buttons on the toolbar are magically enabled. Click anywhere in the memo to deselect the selected text. The Cut and Copy buttons are disabled again. Is the Paste button enabled? If so, type Alt+Print Screen on the keyboard. (This copies the current window to the Clipboard as a bitmap.) When you press Alt+Print Screen, the Paste button should become disabled because you can't paste a bitmap into a memo. Select some text and click either the Cut or the Copy button. The Paste button is now enabled because the Clipboard contains text you can paste into a memo.
How does it work? The TEditCopy, TEditCut, and TEditPaste standard actions automatically know how to enable and disable their associated components when any type of edit control has input focus. It's not magic, but it's the next best thing to it! You only have to create the standard actions and the rest is automatic. You didn't have to write any code to get the Edit menu command enablers working. You can't beat that!
The Edit menu items were easy because you didn't have to write any code. The File menu items, Save and Save As, will take a little more work because there are no standard actions for these menu items. Not to worry, though--adding command enabling for those menu items doesn't take much time. To implement command enabling for these menu items, you will make use of the OnUpdate event. But first you need a little background information to put the OnUpdate event in perspective.
The OnUpdate event provides a convenient place to put your command-enabling code. When your application runs out of messages to process, Windows sends it a WM_ENTERIDLE message. Windows, in effect, tells your program, "I don't have anything for you to do right now, so relax and take it easy for a while." When a Delphi application receives a WM_ENTERIDLE message, it triggers a TAction's OnUpdate event. All you have to do is create an event handler for the OnUpdate event and do your command enabling there. You can use the event handler to check the state of the memo component and enable the Save and Save As items accordingly.
The only step remaining, then, is to create an event handler for the action's OnUpdate event. Perform these steps:
Creating the event handler is the simple part, of course. The more difficult aspect is writing the code that goes between the begin and end statements. Listing 13.2 shows the completed OnUpdate event handler. Switch to the Code Editor and enter the code shown in Listing 13.2 into your OnUpdate event handler.
procedure TMainForm.SaveActionUpdate(Sender: TObject); begin { Command enabler for Save and Save As. } SaveAction.Enabled := Memo.Modified and (Length(Memo.Lines.Text) > 0); SaveAsAction.Enabled := SaveAction.Enabled;
{ The following two command enablers don't use actions. }
{ Instead the Enabled property of the two menu items } { is accessed directly. } { Command enabler for Select All. } EditSelectAll.Enabled := Memo.Lines.Count > 0; { Command enabler for Undo. } EditUndo.Enabled := Memo.Modified; end;
The SaveAction's Enabled property is set based on whether or not the memo has been modified and whether or not the memo contains text. In a nutshell, the Save action is enabled if the memo has been modified since it was loaded and if it contains text. The same value is assigned to the Enabled property of the SaveAsAction. This enables both Save and Save As at the same time using the same criteria.
Notice that I slipped a couple of extra commands into the OnUpdate event handler. The command enablers for the Select All and Undo items of the Edit menu do not use actions. Instead, the Enabled property of the menu items is set directly. Using actions for these two items is overkill because there is only one line of code in each case. As long as you have an OnUpdate event handler, you can use it for any type of command enabling you want. Put another way, this OnUpdate event handler is not exclusively for the File menu items Save and Save As. Any command enabling can be done in the OnUpdate event.
NOTE: The OnUpdate event handler might be called thousands of times per second. For that reason, you must keep code in this method as short as possible.
NOTE: Debugging code in the OnUpdate event handler is tricky. The problem is that any breakpoints in the OnUpdate event handler will be hit as soon as you run the program. You will have to use debugging methods other than straight breakpoints when debugging code in the OnUpdate event handler. Two such methods are conditional breakpoints and using OutputDebugString to send messages to the Event Log.
There's an added benefit to command lists that I haven't mentioned yet. To see this hidden (up to now) benefit, perform these steps:
Now run the program and look at the File and Edit menus. Wow! Instant menu bitmaps! The menu items inherit the ImageIndex property of their associated action. All you have to do to enable the bitmaps is assign the same image list use for the actions to the menu's Images property. The rest is automatic.
Printing is an everyday necessity for most Windows users. Whereas plenty of programs do not have printing capability, the majority of Windows applications have some form of printing support. I'll cover the basics of printing in this section.
Providing printing capabilities in a DOS application used to be a real chore. A DOS program had to provide and install printer drivers for every type of printer that the program supported. That put a huge burden on software developers, especially on small companies or shareware developers. Windows changed all that. For the most part, Windows takes on the burden of dealing with different printers, printer drivers, and so on. All you have to do is send output to the printer just as you would send output to a window. I'll get to that soon.
Printing in Delphi applications comes in several flavors. You'll probably be relieved to learn that, in many cases, printing is built into VCL and comes nearly automatically. In other cases, though, you have to do some specialized printing. Before you learn how to go about that, let's look at the common dialog boxes that pertain to printing. After that I'll discuss the different ways you can print from a Delphi application.
Windows provides the common Print and Print Setup dialog boxes for use in your applications. You use the Print dialog box just before printing begins and the Print Setup dialog box to configure the printer. First, though, you must add the components to your form.
As I've mentioned, the Print dialog box is displayed just before printing begins, usually when the user chooses File|Print from the main menu. If the user clicks OK, printing begins; if the user clicks Cancel, printing is aborted. Figure 13.8 shows the Windows Print dialog box in its most basic form.
FIGURE 13.8. The Windows Print dialog box.
No doubt this is not the first time you have seen this particular dialog box. The combo box at the top of the dialog box enables you to choose the particular printer to which you want to print. The Properties button brings up a dialog box specific to the printer currently selected that enables you to set the orientation, resolution, and other properties specific to that printer. The Print Range section enables the user to print all pages, a page range, or any objects or text currently selected in the application. The Copies section enables the user to specify the number of copies to print as well as whether to collate the copies.
The Print dialog box is encapsulated in VCL in the PrintDialog component. As with the other common dialog boxes, you display the Print dialog box by calling its Execute method. It shouldn't disappoint you to learn that Windows carries out much of what the Print dialog box does. The printer selection, number of copies, and collating options are all handled by Windows, so you don't have to worry about them. Depending on your application, you might need to enable the user to print a specified range of pages or to print the current selection in the application. If you are providing that kind of support, you need to examine some of the PrintDialog properties before printing begins.
The PrintDialog component has the Execute method only and no events. All the functionality of the PrintDialog component takes place through its properties, as listed in Table 13.3.
Property | Description |
Collate | Specifies collated copies. If this is set to True, Windows will print so that the copies are collated. |
Copies | Specifies the number of copies to print. You can set this property before calling the Print dialog box if one of your application's options is the number of copies to print. Windows takes care of printing the correct number of copies. |
FromPage | Specifies the starting page when the option of printing a range of pages is enabled. Applications that support page-range printing should read this property to determine which pages to print. |
MaxPage | Specifies the maximum page number that can be specified in the To field when printing a range of pages. The Print dialog box takes care of validating entry in the From and To fields. |
MinPage | Specifies the minimum page number that can be specified in the From field when printing a range of pages. |
Options | Contains a set of options that control which features of the print dialog box are enabled. You can elect to have a help button, to display the print to file option, or to enable the page-range or print selection options. |
PrintRange | Controls which of the Print Range radio buttons is selected when the Print dialog box is initially displayed. |
PrintToFile | Indicates whether the user has chosen the Print to File option. It is up to the application to write the output to a file. |
ToPage | Specifies the ending page number when printing a range of pages. Applications that support page-range printing should read this property to determine which pages to print. |
The application doesn't have much to do in response to the Print dialog box closing unless the Print Range and Print to File options are enabled. For example, if your application enables printing a range of pages, you need to read the FromPage and ToPage properties to determine which pages to print. Other than that, you begin printing if the user clicks OK.
The Print Setup dialog box, shown in Figure 13.9, is used when the user wants to change printers, page size, paper source, or orientation.
FIGURE 13.9. The Print Setup dialog box.
The Print Setup dialog box isn't necessary in most applications because the user can always press the Properties button on the Print dialog box to change the setup options (refer to Figure 13.8). On the other hand, implementing the Print Setup dialog box is so easy that you might as well include it in your applications. How easy is it? Well, the PrinterSetup component has no properties, methods, or events specific to it. As with the PrintDialog component, the Execute method is the only method in which you are interested. To further simplify things, Windows handles everything that the Print Setup dialog box does. In fact, the Execute method doesn't even return a value. This is because Windows handles everything for you. If the user clicks Cancel, Windows does nothing. If the user clicks OK, Windows makes the appropriate changes in preparation for printing. All you have to do is display the Print Setup dialog box and forget about it. A typical event handler for the File|Printer Setup menu item would look like this:
procedure TMainForm.FilePrintSetupClick(Sender: TObject); begin PrinterSetupDialog.Execute; end;
That's all there is to it. As I said, implementing the Print Setup dialog box is so simple you might as well add it to your application.
Printing is an application-specific task. That might not sound profound, but it's true. Depending on what kind of application you develop, printing can be as simple as one line or it can entail hundreds of lines of code. Let me first discuss the easiest forms of printing, and then I'll progress to the more difficult printing operations.
The TForm class has a method called Print that can be used to print the contents of a form. Only the client area of the form is printed; the form's frame and menu bar are not. Although this method works well for a simple screen dump, it is limited in its implementation. You can choose from three print options, which are controlled through the PrintScale property. Table 13.4 lists the print scaling choices and their descriptions.
Option | Description |
poNone | No special scaling is applied. The printed output of the form varies from printer to printer. |
PoProportional | This option attempts to print the form in roughly the same size as it appears on the screen. |
poPrintToFit | This increases or reduces the size of the image to fit the current printer settings. |
You can set the PrintScaled property at runtime or at design time. The Print method's use is limited to simple screen dumps and isn't likely to be used for any serious printing.
The RichEdit component is powerful primarily due to the amount of work done by the underlying Windows memo control. Printing in the RichEdit component is accomplished via a call to the Print method. This method takes a single parameter called Caption that is used by the Print Manager when it displays the print job. Printing the contents of a RichEdit component is as simple as this:
RichEdit.Print(`MyApp.exe - readme.txt');
Everything is taken care of for you. Word wrapping and pagination are automatically implemented. If you are using a multiline edit control that requires printing, the RichEdit component is the way to go.
TIP: You can use the Windows API function ShellExecute to print a text file. ShellExecute is used, among other things, to run a program based on a filename extension. For example, by default Windows registers the .txt extension as belonging to Windows Notepad. If you double-click on a file with a .txt extension in Explorer, Windows will look up the .txt extension in the Registry, see that Notepad.exe is registered to handle .txt files, and will run Notepad. The file you double-clicked will be loaded automatically.
You can use this behavior to your advantage. Take this line of code, for example:
ShellExecute(Handle, `print', `readme.txt', nil, nil, SW_HIDE);
This code loads Notepad, prints the file called Readme.txt, and then exits Notepad. In fact, you never see the Notepad program's main window because the SW_HIDE style is specified for the Show parameter. Using this technique assumes that the user has not modified the default registration of the .txt extension and has not deleted Notepad from his or her system. If you use ShellExecute, you have to add ShellApi to your unit's uses list.
Database programs can use QuickReport to print reports. I mention QuickReport here because it obviously pertains to printing, but because QuickReport is one of the database components, I will postpone a detailed discussion about its actual implementation until Day 18, "Building Database Applications."
Don't let the title of this section put you off. Printing isn't all that difficult; it just takes some time and organization. First, let's look at some steps you need to know in order to implement printing in your applications. After that you delve into the actual code.
I talked about device contexts and TCanvas in detail yesterday, but a recap can't hurt. A device context (DC) is like a slate that Windows programs can draw on. A better word would be canvas. On this canvas you can draw text, lines, bitmaps, rectangles, ellipses, and so on. The type of line used when drawing on a device context depends on the current pen selected into the DC. The current fill color and fill pattern are taken from the brush that is currently selected into the device context. Device contexts must be carefully managed. There are a limited number of DCs available to Windows, and you have to be careful to release the device context as soon as you are finished with it. Also, if you don't properly delete the objects you select into the device context, your program will leak memory and perhaps even leave Windows itself in a precarious state. As you can imagine, working with DCs can be complicated.
The good news is that VCL shields you from having to know every detail of device contexts. VCL encapsulates Windows DCs in the TCanvas class. The Canvas property frees you from worrying about all the little details that can drive you nuts when dealing with Windows device contexts. VCL takes care of obtaining the DC, selecting the appropriate objects into the DC, and releasing the DC when it is no longer needed. All you have to do is draw on the canvas and let VCL worry about the rest.
So what does this have to do with printing? (Inquiring minds want to know.) Well, it's like this: Windows enables you to obtain a printer device context on which you can draw text, graphics, lines, and so on. In other words, you draw on a printer canvas just as you draw on a screen canvas. This concept represents quite a switch from the way printing was approached back in the good old days of DOS. In this case, it is Windows that comes to your rescue by enabling the use of a printer DC. VCL further aids you by encapsulating device contexts in the Canvas property. The bottom line is that printing is easier than it's ever been.
VCL aids in printing operations by providing the TPrinter class. This class encapsulates the whole of printing in Windows. TPrinter has a Canvas property that you can use to output lines, text, graphics, and other drawing objects to the printer. I don't want to make it sound too easy, but all you have to do to print in your Delphi programs is add Printers to your uses list and then do something like the following:
Printer.BeginDoc; Printer.Canvas.TextOut(20, 20, `Hello There!'); Printer.EndDoc;
In this code, Printer is a VCL function. This function enables you access to a TPrinter object that is set up and ready to go. All you have to do is put it to work.
Now let's take a quick look at TPrinter's properties and methods. Table 13.5 lists the primary TPrinter properties, and Table 13.6 shows the primary TPrinter methods.
Property | Description |
Aborted | This property is True if printing was started and then aborted before it was finished. |
Canvas | The mechanism through which you can draw on the printer (the printer device context). |
Capabilities | The current settings of a printer device driver. |
Copies | The number of copies printed. |
Fonts | A list of fonts supported by the current printer. |
Handle | The handle to the printer device context (HDC). Use this property when you have to call a Windows API function that requires a handle to a device context. |
Orientation | The printer orientation (poPortrait or poLandsacpe). This is automatically set when the user chooses a printer or modifies the printer setup, but you can also set it manually. |
PageHeight | The height of the current printer page in pixels. This value varies from printer to printer. In addition, this property can contain a different value based on the orientation of the printer. Some printers can use more than one resolution, which also causes this value to vary. |
PageNumber | The page number of the page currently printing. This property is incremented each time you call NewPage to begin printing a new page. |
PageWidth | The width of the page in pixels. As with the PageHeight property, this value varies depending on the printer resolution, paper orientation, and paper size. |
PrinterIndex | The index value of the currently selected printer in the list of available printers. Specify -1 to select the default printer. |
Printers | A list of available printers on the system. |
Printing | This property is True if the printer is currently printing. |
Title | The text that identifies this printing job in the print manager window. |
Method | Description |
Abort | Used to abort the printing before normal completion. |
BeginDoc | Begins the printing process. Sets up the printer with Windows in preparation for printing. |
EndDoc | Ends the printing process. Forces the current page to be printed and performs printing cleanup with Windows. |
GetPrinter | Retrieves the current printer. Use the Printers property instead of this method. (The Printers property is the preferred method for accessing printers because you can use it for both retrieving and setting the current printer.) |
NewPage | Used to force printing of the current page and start a new page. Increments the PageNumber property. |
SetPrinter | Sets the current printer. Use the Printers property instead of this method. |
The TPrinter class has no design-time interface. Everything is accomplished at runtime.
It's time to put your newly acquired knowledge to work. Once again it's time to dust off the ScratchPad program and spruce it up a bit. After all, what good is a text editor that doesn't print?
First, you need to modify the main form slightly. You already have menu items set up for the Print and Print Setup menu items, but you need to enable them and add the Print and Printer Setup dialog boxes to the form. Here goes:
Okay, now that you've completed the form, it's time to go to work modifying the code. To start, you have to add a couple items to the main form's declaration. Perform the following steps:
procedure PrintFooter(var R : TRect; LineHeight : Integer);
procedure TMainForm.FilePrintSetupClick(Sender: TObject); begin PrinterSetupDialog.Execute; end;
Okay, now you're ready to fill in the FilePrintClick and PrintFooter methods. Listing 13.3 shows the FilePrintClick method. You can enter the code in this method by hand, or you can load the ScratchPad project that comes with the book's code and examine it in the Delphi IDE. Listing 13.4 shows the PrintFooter method. Enter the code in these methods in your SPMain.pas file. You don't have to type the comment lines, of course.
procedure TMainForm.FilePrintClick(Sender: TObject); var I : Integer; LineHeight : Integer; LinesPerPage : Integer; LineCount : Integer; R : TRect; S : string; begin { Display the Print dialog. } if PrintDialog.Execute then begin { Set the title for the printer object. } Printer.Title := `ScratchPad - ` + OpenDialog.FileName; { Set the printer font to the same font as the memo. } Printer.Canvas.Font := Memo.Font; { Determine the line height. Take the Size of the { font and and use the MulDiv function to calculate { the line height taking into account the current { printer resolution. Use the Abs function to get { the absolute value because the result could be a { negative number. After that add 40% for leading. } LineHeight := Abs( MulDiv(Printer.Canvas.Font.Size, GetDeviceCaps(Printer.Handle, LOGPIXELSY), 72)); Inc(LineHeight, (LineHeight * 4) div 10); { Determine how many lines will fit on a page. Trim { it back by three lines to leave some bottom margin. } LinesPerPage := (Printer.PageHeight div lineHeight) - 4; { Start printing on line 4 rather than line 0 to leave { room for the header and to allow for some top margin. } LineCount := 4; { Tell Windows we're starting and print the header. } Printer.BeginDoc; R.Top := LineHeight; R.Left := 20; R.Right := Printer.PageWidth; R.Bottom := LineHeight * 2; DrawText(Printer.Handle, PChar(OpenDialog.FileName), -1, R, DT_CENTER); { Loop through all of the lines and print each one. } for I := 0 to Pred(Memo.Lines.Count) do begin { When we get to the bottom of the page reset the { line counter, eject the page, and start a new page. } Inc(LineCount); if LineCount = LinesPerPage then begin PrintFooter(R, LineHeight); LineCount := 4; Printer.NewPage; end; { Get the next string and print it using TextOut } S := Memo.Lines.Strings[I]; Printer.Canvas.TextOut(0, LineCount * LineHeight, S); end; { All done. } PrintFooter(R, LineHeight); Printer.EndDoc; end; end;
procedure TMainForm.PrintFooter(var R: TRect; LineHeight: Integer); var S : String; begin with Printer do begin { Build a string to display the page number. } S := Format(`Page %d', [PageNumber]); { Set up the rectangle where the footer will be drawn. } { Find the bottom of the page and come up a couple of { lines. } R.Top := PageHeight - (lineHeight * 2); R.Bottom := R.Top + lineHeight; { Display the text using DrawText so we can center the { text with no fuss. } DrawText(Handle, PChar(S), -1, R, DT_CENTER); { Draw a line across the page just above the `Page X' text. } Canvas.MoveTo(0, R.Top - 2); Canvas.LineTo(R.Right, R.Top - 2); end; end;
This code illustrates how you can print directly through Windows rather than rely on the built-in printing that VCL provides. Although I always opt to do something the easy way when possible, there are times when the easy way isn't flexible enough. In those times, it's good to have the knowledge to do the job without trouble.
Printing a bitmap is simple. All you need to do is create an instance of the TBitmap class, load a bitmap into the bitmap object, and send it to the printer using the Draw method of TCanvas. Here's the entire code:
procedure TForm1.Button1Click(Sender: TObject); var Bitmap : TBitmap; begin Bitmap := TBitmap.Create; Bitmap.LoadFromFile(`test.bmp'); Printer.BeginDoc; Printer.Canvas.Draw(20, 20, Bitmap); Printer.EndDoc; Bitmap.Free; end;
When you print a bitmap, be aware that the bitmap might turn out very small depending on the resolution of your printer. The bitmap might have to be stretched to look right. If you need to stretch the bitmap, use the StretchDraw method instead of Draw.
The use of cursors is not difficult, but I'll describe it here to give you a basic understanding of how it works. This section deals with cursors that you change at runtime. (To change a cursor at design time, select a new value for the component's Cursor property.) After a look at cursor basics, I will discuss how to load stock cursors and custom cursors.
First, you can change the cursor for a particular component or form or for the entire client area of your application. If you want to change the cursor for the entire application, you need to change the Cursor property of the Screen object. The Screen object represents your application's screen. By changing the Cursor property of the Screen object, you ensure that the cursor is the same regardless of which component the cursor is over. Let's say, for example, that you want to change the cursor to the hourglass cursor. If you change the Cursor property for the form only, the cursor will be an hourglass when it's over the form itself but will revert back to the default cursor when it's over any other components on the form. Changing the Cursor for the Screen object gives you the same cursor throughout.
Cursor management is the responsibility of the Screen object. All the cursors available for your use are contained in the Cursors property of the Screen object. Note that the name of this property is Cursors and is not the same as the Cursor property discussed in the previous paragraph. The Cursors property is an array that contains a list of available cursors for the application, and the Cursor property is the property that is used to display a particular cursor. Although this is confusing at first, you'll catch on quickly. All it takes is a little experience working with cursors.
Windows provides several built-in cursors for use in your applications. In addition to these built-in cursors, VCL adds a few cursors of its own. Together, these cursors are called the stock cursors. Each stock cursor has a named constant associated with it. For example, the arrow cursor is named crArrow, the hourglass cursor is named crHourGlass, and the drag cursor is named crDrag. All these cursors are held in the Cursors array and occupy positions -17 through 0 in the array; the default cursor is at array index 0 (crDefault), no cursor is -1 (crNone), the arrow cursor is -2 (crArrow), and so on. The online help for the Cursors property lists all the cursors and their constant names, so check Delphi help for a complete list of the available cursors.
To use one of the cursors in the Cursors array, assign the name of the cursor you want to use to the Cursor property of the Screen object:
Screen.Cursor := crHourGlass;
At this point the VCL magic kicks in and the correct cursor is loaded and displayed. The use of the Cursors property is transparent to you because you don't directly access the Cursors property when you use a cursor. Instead, you make an assignment to the Cursor property, and VCL takes care of looking up the proper cursor in the Cursors array and displaying it. The Cursor property of a component and the Cursors property of the Screen object work together to display different cursors in your application.
You can change cursors in your applications for any number of reasons--to display the wait cursor (the hourglass), if you have a drawing program that uses special cursors, or to implement a help cursor in your application.
Windows provides several built-in cursors for use in your applications. In addition to those, VCL adds a few more cursors from which you can choose. You can use these stock cursors anytime you like.
One of the most obvious times to change the cursor is when you have a lengthy process your application must perform. It is considered bad practice to tie up the program without giving the user some indication that the program is busy. Windows provides the hourglass cursor (Windows calls it the wait cursor) for exactly this purpose. Let's say, for example, that you have a processing loop in your application that might take a long time. You would do something like this:
procedure TMainForm.DoItClick(Sender: TObject); var I, Iterations : Integer; OldCursor : TCursor; begin Iterations := StrToInt(Time.Text); OldCursor := Screen.Cursor; Screen.Cursor := crHourGlass; for I := 0 to Iterations do begin Application.ProcessMessages; Time.Text := IntToStr(I); end; Screen.Cursor := OldCursor; end;
Because you don't always know which cursor your application is using at any given time, it is a good idea to save the current cursor before changing the Cursor property. After you have finished with a particular cursor, you can restore the old cursor.
NOTE: Sometimes you will change the Cursor property before a lengthy processing loop and nothing seems to happen. The reason is that Windows didn't get a chance to change your cursor before your program entered the loop. When in the loop, your application can't process any messages--including the message to change the cursor. The fix is to pump the Windows message loop for waiting messages while you are in your loop:Application.ProcessMessages;
Now your application enables messages to flow and the cursor changes when your loop starts.
You can, of course, change the cursor for an individual component. For example, a drawing program might change the client area cursor depending on the current drawing tool. You don't want to change the cursor for the Screen object in that case because you want the arrow cursor to appear when the cursor is moved over the toolbar, status bar, menu, or any other components that might be on the form. In that case, you set the cursor only for the component that represents the client window of your application:
PaintBox.Cursor := crCross;
Now the cursor is changed for just this one component and all other components use their own predefined cursor.
Loading custom cursors requires a little more work. As I mentioned earlier, the Cursors property of the TScreen class contains the list of cursors available to your application. To use a custom cursor requires several steps:
The first two steps were covered on Day 11 when I talked about the Image Editor and on Day 8 when I discussed resources. After you have the cursor bound to the .exe, you can load it with the LoadCursor function. Loading a cursor into the Cursors array is easy:
Screen.Cursors[1] := LoadCursor(HInstance, `MYCURSOR');
This code assumes that you have a cursor with a name of MYCURSOR and that you are assigning the cursor to position 1 in the cursors list (remember, positions -17 through 0 are used for the stock cursors). Loading the cursor has to be done only once, so you will probably do it in your main form's OnCreate event handler.
NOTE: All cursors are loaded into the Screen object's Cursors property regardless of whether you use the cursor with the Screen object, with a form, or with a component. There is only one Cursors array, and it belongs to the Screen object.
To use the cursor, then, just assign it to the Cursor property of the Screen object or of any component:
PaintBox.Cursor := 1;
If you have several cursors, you might want to create constants for each cursor so that you have meaningful names to use instead of integer values that are easy to mix up. Using that method, the preceding code would look like this:
const MyCursor = 1; { later... }
Screen.Cursors[MyCursor] := LoadCursor(HInstance, `MYCURSOR');
{ later still... } PaintBox.Cursor := MyCursor;
As you can see, loading and implementing custom cursors isn't difficult when you know how to do it. The book's code for today includes a sample program called CursTest that demonstrates the theories discussed in this section.
Today you learned about some features that make up a top-quality Windows application and how to implement them in your own programs. I should warn you that it's tempting to overdo it with features such as control bars, status bars, and cursors. Implement whatever decorations your application needs, but don't overuse them. You also learned about printing today. In some cases, printing support is built into a particular component, and in those cases printing is incredibly easy. In other cases you have to roll up your sleeves and go to work with the TPrinter class. Even at those times, though, printing is nothing to fear.
The Workshop contains quiz questions to help you solidify your understanding of the material covered and exercises to provide you with experience in using what you have learned. You can find the answers to the quiz questions in Appendix A, "Answers to the Quiz Questions."
© Copyright, Macmillan Computer Publishing. All rights reserved.