![]() |
![]() |
![]()
| ![]() |
Inside OP Hints / Fly-over Help / Tooltips / Delphi 2-5
Dropping Hints Part I: Taking Control of Fly-over Help
You're sure to be familiar with the small, yellow informational boxes that pop up when you move the mouse pointer over a control. These boxes - sometimes referred to as fly-over or fly-by help - are found in virtually all modern Windows applications. Delphi, of course, is no different, and fully supports ToolTips (their official Microsoft designation) for applications it develops. In Delphi parlance they're know as Help Hints, or - more often - simply as Hints.
Hints are a handy way to inform the user about a particular control's function - far more convenient than having to look up the information in the Help file. Microsoft Windows provides a standard ToolTip window class, TOOLTIPS_CLASS, in the common control library. Delphi, however, ignores the API and implements ToolTips as a simple window object type, named THintWindow. In this two-part series, we'll take a close look at creating hint windows for our Delphi applications.
The Hint Property The VCL defines a Hint property for TControl. If any descendant of TControl has a string in its Hint property, and its ShowHint property is set to True, a little yellow window containing the hint string will be displayed under the mouse pointer as the mouse pointer is moved over it.
If a control has child controls within it when its ShowHint property is changed, it will attempt to set the child control's ShowHint value to that of its own. You can prevent this by setting each control's ParentShowHint property to False.
To create a hint with multiple lines of text, add the newline character (#13) at the point where you want the text to split lines. This isn't possible using the standard Delphi Hint property editor in the Object Inspector.
TApplication's Hint-related Properties The global Application instance controls the display and position of the hint window, and has properties and methods that allow you to control the appearance and other aspects of the hint.
The Application.Hint property contains the text for the hint window. This is usually taken from the Hint property of the control over which the mouse pointer is located, but it can be changed, as we'll discuss later.
The Application.HintColor property specifies the background color of hint windows. By default, this will be clInfoBk, but you can change this property to specify the default color.
Waiting Periods Having the hint visible the whole time the mouse pointer is over a control can be irritating. Indeed, if a user already knows what the control does and has moved the mouse onto the control to activate it, why bother him/her with a flash of yellow at all? Or, after the user has already read the hint, why keep it there to block the view of the rest of the screen?
Therefore, a timing mechanism is used. When the mouse pointer is moved over a control, there's a small waiting period in which users who already know what they're doing can take action. The length of this waiting period can be set with the TApplication.HintPause property. After this amount of time, it's fair to assume the user isn't sure what the control does, so the hint window is displayed.
After a time specified by the Application.HintHidePause property, the hint window is hidden. However, there may be a time when the user is moving the mouse pointer over a row of buttons to see what each of them do. It would not be logical to make him/her wait a full HintPause time to see the information. Therefore, Delphi provides the HintShortPause property. If a hint is still visible and the cursor is moved to another control, the application will only wait Application.HintShortPause milliseconds before it displays the hint window for the new control. If the number is low enough, this will cut down on flickering as the hint will seem to jump to the new cursor position and not seem to blink off at the old location and on at the new.
All these properties are specified in milliseconds, and have default values that are adequate for most applications. However, you can change them to customize the behavior of your application. The table in Figure 1 provides a quick review of the Application object's hint-related properties.
Figure 1: Hint-related properties of TApplication.
The Extended Hint Property The Hint property can be divided into two parts: the first is called the "short hint," and the second is called the "long hint." The part of a Hint property string before a piped character ( | ) is considered the short hint string, and the latter is considered the long hint.
The short hint is usually very brief, something to simply remind the user what the control does. This is the portion of the hint that gets displayed in the hint window. The long hint usually provides more detailed information on what a control is or does. Some applications display this information in the status bar. For example, the following hint string might be used for an Open File button:
OpenBtn.Hint := 'Open a file|Open a file previously saved to disk';
Delphi provides two functions to get these portions of the hint: GetShortHint and GetLongHint. You pass the Hint property to these functions. If there's no piped character in the string, the functions will return the entire string. As a bonus, you can use these functions on any string with an embedded piped character; use GetShortHint for the first half, and GetLongHint for the second half.
The OnHint Event How do you use the long hint? You can assign the long hint to a label or status bar in the Application.OnHint event. The Delphi documentation says this occurs whenever the mouse cursor is moved over a control or menu item that doesn't have an empty hint string. The real trigger for this event, however, is when the Application.Hint property changes. Because moving the mouse cursor over a control or menu will change the Application.Hint property to the Hint property of that control, the OnHint event fires.
This event will also fire if you make a direct assignment to Application.Hint. We can use the long hint with code such as this:
procedure TForm1.FormCreate(Sender: TObject); begin Application.OnHint := HintFire ; end;
procedure TForm1.HintFire(Sender: TObject); begin StatusBar1.Panels[0].Text := GetLongHint(Application.Hint); // or StatusBar1.SimpleText := GetLongHint(Application.Hint); end;
The same results can be achieved in Delphi 4/5 by setting a StatusBar component's AutoHint property to True. This will only work if you don't have an event assigned to the Application.OnHint event. (This is the only way to use menu item hints, because no hint window pops up when you select a menu item.)
THintInfo and the OnShowHint Event Immediately before a hint window is displayed, Delphi fires the Application.OnShowHint event, and passes it a THintInfo record filled with information about how the hint window should be displayed. (Note: This event isn't fired for menu hints, because no hint window is displayed.) The following is the procedure type for the OnShowHint event:
TShowHintEvent = procedure(var HintStr: string; var CanShow: Boolean; var HintInfo: THintInfo) of object;
To use it, define a procedure like this:
procedure Form1.ShowHintProc(var HintStr: string; var CanShow: Boolean; var HintInfo: THintInfo);
and assign it in OnFormCreate (or any other convenient place):
Application.OnShowHint := ShowHintProc;
The HintStr parameter contains the string that will be shown in the hint window. You can modify this parameter to customize the hint text. The second parameter is named CanShow. It's a Boolean you can set to determine whether the hint window will be displayed. The third parameter is a record of type THintInfo. This record is filled with information that will eventually be used to specify the color, position, and other attributes of the hint window. The following is the definition of the THintInfo record:
THintInfo = record HintControl: TControl; HintWindowClass: THintWindowClass; HintPos: TPoint; HintMaxWidth: Integer; HintColor: TColor; CursorRect: TRect; CursorPos: TPoint; ReshowTimeout: Integer; HideTimeout: Integer; HintStr: string; HintData: Pointer; end;
Before displaying the hint window, Delphi gives you a chance to modify these values. Let's examine each field:
TopRect := Rect(0,0,100,10); BottomRect := Rect(0,10,100,20);
if HintInfo.HintControl = Button1 then // Hint testing for top part. if PtInRect(TopRect, HintInfo.CursorPos) then begin HintStr := 'Top Part'; HintInfo.CursorRect := TopRect; end else // It's part of the bottom. begin HintStr := 'Bottom Part'; HintInfo.CursorRect := BottomRect; end; Figure 2: You can use hint testing to create a multi-hint control.
The THintWindow Component The core of the Delphi hint system is the THintWindow component. THintWindow is a descendant of TControl that can exist by itself without a parent because it has the window style WS_POPUP, which makes it a top-level window, equal to a form. This is the object used in Delphi as a hint window.
You can create your own hint windows independent of the TApplication hinting mechanism. An example of this can be found in the Delphi Object Inspector. If the property name or value is too big to fit into its allotted area, and the mouse pointer is over it, a small yellow hint pops up with the full information. That window is not the Application's general hint window, but rather a hint window specifically created for this task.
To create a new hint window, call the THintWindow's constructor:
var HintWin : THintWindow; begin HintWin := THintWindow.Create(Self);
The window is now in memory. To display the window, you can use the ActivateHint method of THintWindow. This method takes a TRect that describes the coordinates of the hint window in screen coordinates, and a string to display in it. Note: The hint window will be the size specified in the TRect; even if it cuts off some of the words, it will not resize to accommodate the string. ActivateHint will also attempt to ensure the full hint rectangle is on the visible portion of the screen. ActivateHint also takes a pointer to an object, but this pointer has no use in the standard THintWindow and can be passed a nil.
THintWindow provides a procedure that can be used to measure the amount of space a string of text will require, named CalcHintRect. CalcHintRect allows you to specify a string and the maximum width in pixels allowed (similar to the HintMaxWidth member of the THintInfo record), and returns a TRect bounding that area.
In reality, it's possible to call ActivateHint and combine both functions:
HintWin.ActivateHint(HintWin.CalcHintRect( 10, 'Hello World', nil), 'Hello World');
However, we have to remember that the THintWindow is based on screen coordinates, and that because CalcHintRect returns a 0,0-based rectangle, this little snippet would put a yellow box at the top left of the screen. Another problem is that there are times when CalcHintRect isn't so accurate, and can underestimate the width required to display the text. So a direct assignment such as this isn't a good idea.
The way to do it is to assign the return value of CalcHintRect to a TRect, then add 2 or 3 to TRect.Right. Then you would add the correct top offset to Rect.Top and Rect.Bottom, and the correct left offset to Rect.Left and Rect.Right. An easier way to do this would be to use the API OffsetRect function:
rct := HintWin.CalcHintRect(10, 'Hello World', nil); OffsetRect(rct, 100, 100); // Or whatever x,y you want. rct.Right := rct.Right + 3;
The hint text is assigned to the window's Caption property, and you can change the property while the hint window is visible. The window will be resized to fit the new text.
The constructor of THintWindow automatically assigns the clYellow color to the hint window. (This is probably left over from Delphi 1, where the clInfoBk color wasn't available.) This is a very bright color that may irritate the user and probably doesn't match all other hints on the system, so set the hint's Color property to clInfoBk during its creation process.
Hint windows you create yourself aren't subject to the waiting periods discussed above, and will not disappear by themselves. The standard Visible property only works with parented controls, and THintWindow is a top-level window without a parent. To hide a THintWindow, you call its ReleaseHandle procedure. ReleaseHandle calls the TWinControl.DestroyHandle procedure, which destroys the window handle without destroying the Delphi control instance. When it's time to reshow the hint, the control will build a new window based on the existing Delphi control data.
As you can see in the Object Inspector and other places, when a hint window pops up, the hint window doesn't accept mouse clicks. Instead, it passes them on to the window under it, if it's part of the same application.
Windows sends a WM_NCHITTEST message to a window when the mouse moves over it. The window responds by specifying its type of area. For example, if the area is in the blue rectangular strip at the top of the window, the result value will be HTCAPTION. Windows will then interpret any mouse click at these coordinates to be the start of a window dragging operation. There are many other values that can be returned to this message, including: HTREDUCE, which indicates that the mouse is in a minimize box; and HTZOOM, which indicates that the mouse is in a maximize box. A control can control certain aspects of its behavior by subclassing this message. THintWindow does this, and returns a value of HTTRANSPARENT. When receiving this value, Windows will ignore the window and send all mouse events to the window under it.
Hint at Coordinates When you move a component in the Delphi IDE, a little hint window informs you of the top-left coordinates of the component. When you drag one of the resizing handles, the little window informs you of the component's height and width. Let's try to implement such functionality using an independent THintWindow. It may seem that a better way of implementing this would be to set the hint text in the OnShowHint event. However, this will only work for the top-left part of the function. Once the mouse button is down, no OnShowHint events are fired, so the user won't be informed of the width and height.
We will implement this on a form. In the form's declaration, declare four integers named mX and mY, fX and fY, a variable of type THintWindow named HintWin, and a Boolean named MDown (the full source code is included in the example provided; see end of article for details). In the form's OnCreate handler, create a variable of THintWindow and set its color to clInfoBk. To ensure full compliance with hint window styles, set the control's Canvas.Font.Color property to clInfoText.
The OnMouseDown event records the coordinates of the mouse click in the form's mX and mY variables. It also sets MDown to True. The OnMouseUp event sets MDown to False.
The OnMouseMove event shown in Figure 3 does the lion's share of work. When MDown is False, a small hint window showing the current mouse coordinates is displayed. When MDown is True, the event calculates the difference of the current coordinates to the coordinates described in the form's mX and mY variables, and displays and adds this as an extra line to the hint window. OnMouseMove also draws a rectangle displaying the selected area using the TCanvas.FocusRect method. We're going to take advantage of the fact that FocusRect draws using an XOR pen. That means that every pixel is drawn using the inverse color of the pixel that was there before, so we can redraw the rectangle to erase it.
procedure TForm1.FormMouseMove(Sender: TObject; Shift: TShiftState; X, Y: Integer); var Rct : TRect; S : string; begin // Compose the Hint text. S := ' x = ' + IntToStr(X) + ' , y = ' + IntToStr(Y); // If MouseDown then add the height and width. if MDown then begin S := ' Left = ' + IntToStr(X) + ' , Top = ' + IntToStr(Y); S := S + #13 + // Add a new line character. ' Width = ' + IntToStr(x-mX) + ' , Height = ' + IntToStr(y-mY); // Erase the previous FocusRect... Canvas.DrawFocusRect(Rect(mX,mY,fX,fY)); // and draw the new coordinates. Canvas.DrawFocusRect(Rect(mX,mY,X,Y)); // Save these for later. fX := X; fY := Y; end else S := ' x = ' + IntToStr(X) + ' , y = ' + IntToStr(Y); // Get the string's dimensions. rct := HintWin.CalcHintRect(Screen.Width, S, nil); // Add the Hint offsets to the rectangle. Note: This // can be problematic since the x, y represents the // cursor's hotspot which isn't always the top-left of // the cursor. The Application hint tests for this and // positions the window appropriately. However, this is // a complex test that cannot be implemented here. // Instead, we will risk it and position it at X and // Y + 16. OffsetRect(Rct, x, y + 20); Rct.Right := Rct.Right + 3; // Convert to Client Coordinates. Rct.TopLeft := ClientToScreen(Rct.TopLeft); Rct.BottomRight := ClientToScreen(Rct.BottomRight);
HintWin.ActivateHint(Rct,s); end;
procedure TForm1.FormMouseDown(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer); begin MDown := True; mX := x; mY := y; end;
procedure TForm1.FormMouseUp(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer); begin MDown := False; // Erase the previous FocusRect. Canvas.DrawFocusRect(Rect(mX, mY, fX, fY)); fX := 0; fY := 0; end; Figure 3: The OnMouseMove event.
We save the current mouse position as fX and fY so we have the coordinates next time (see Figure 4). Then we redraw. The remaining FocusRect is erased in OnMouseUp; fX and fY are also cleared.
One problem remains: When you move the mouse pointer off the form, the small hint window stops and stays stuck at the point that the mouse left. Even if you switch to another application, this will happen, because the OnMouseMove event doesn't get fired for coordinates that are out of the client area of the window. There are several approaches to fix this; the easiest involves the CM_MouseLeave message.
The CM_MouseLeave Message Messages are notifications that Windows sends to a window when a certain event has occurred, or when information is needed from the window. Not all messages received by VCL objects are from the Windows messaging system. The VCL itself has a few messages it passes back and forth to make things tick. Windows messages have a WM prefix, while Delphi messages usually have a CM prefix. When a message is received, it's handled in either the Windows general message handler, or a handler specific to the message. By overriding a message handler, we can customize the way an object responds to a message.
Delphi sends the CM_MouseEnter message to a window when the mouse first enters it, and the CM_MouseLeave message when the mouse leaves. We can take advantage of the CM_MouseLeave message to hide the hint window at the appropriate time. Note: TForm's response to the CM_MouseLeave message is erratic (sometimes it doesn't fire the handler), but I haven't seen any problems when dealing with other controls, which makes it perfect for our next example.
Conclusion In the second half of this two-part series, we'll look at yet another example using an independent window. We'll create a TListBox descendant that will display a little hint window over an item whenever the item text is too large to fit in the list box. The examples referenced in this article are available for download.
Motty Adler is a freelance programmer/consultant and President of WAISS Systems. He has been developing software for over five years, and has used Delphi for over two years. He can be reached at mailto:aisssoft@aol.com. WAISS Systems' Web site is at http://www.waiss.com/.
|
![]() |
|