Mega Code Archive

 
Categories / Delphi / VCL
 

Adding tooltips to a TListBox

Title: Adding tooltips to a TListBox Question: Have you ever wanted to see the full contents of a ListBox entry if the entry is wider than the ListBox? Answer: Normally if you are using listboxes in your programs and the items that populate the listbox are wider than the width of the listbox, those items will be clipped. This could cause important information to be visually lost. This acrticle provides a method to prevent this from being a major problem, by displaying a tooltip-like hint over items that are wider than the listbox. The key to this problem is the THintWindow object. This is the same object that is used on the Windows Taskbar or System Tray to pop up a hint if the mouse hovers over an icon for a short amount of time. The TTreeView VCL component provides this capability built-in, while the TListBox component does not provide it at all. You will be responsible for creating, displaying and destroying the THintWindow yourself. To begin, start a new project and add a single TListBox (named lstProducts) and a single TButton (named btnOK) component to it. You may also use the source code provided at the end of this document. Rearrange and resize the components as needed. You will need to manually add the following definition to the private section of the main forms definition: ThisHintWindow : THintWindow; This is the object we will be using to provide the list item hints. In the main forms OnCreate() event handler, we will need to do three things: (a) define a method to ensure we are display the hint over the correct component, (b) create the hint window, and (c) set the color of the hint window. a. Define a method to ensure we are display the hint over the correct component. We do this by setting the OnShowHint() event handler to the procedure we want to use: Application.OnShowHint := CheckHint; The definition of the CheckHint() procedure is below: procedure TfrmMain.CheckHint(var HintStr: string; var CanShow: Boolean; var HintInfo: THintInfo); begin if (HintInfo.HintControl = lstProducts) then HintInfo.HintPos.y := HintInfo.HintPos.y - 24; end; We want to make sure we are showing the hint over the correct component, the lstProducts listbox. b. Create the hint window. Simple call the Create() constructor for the hint window, using the main form as the owner: ThisHintWindow := THintWindow.Create(Self); c. Set the color of the hint window. When the hint window is displayed, we want to make sure it is displayed in the colors defined by the user using the Appearance display properties on the Windows desktop. This is done by using Delphis predefined clInfoBk constant: ThisHintWindow.Color := clInfoBk; We will now want to display the hint window when the mouse is over items in the listbox that are wider than the listbox. The other, shorter items are OK as they are. We will take care of displaying the hint window in the OnMouseMove() event handler of the listbox, and below are the basic steps. First we need to get the item that is under the mouse cursor, and the following lines accomplish this: ThePoint.x := X; ThePoint.y := Y; Index := ListBox.ItemAtPos(ThePoint, true); Index will return the zero-based index of the listbox item or 1 if it is not over an item. If we are over an item wider than the listbox, we will need to define the upper-left and lower-right corners of a rectangle that will be used as the dimensions of the hint window. if ListBox.Canvas.TextWidth(ListBox.Items[Index]) ListBox.Width then begin ScreenPointUpperLeft.x := ListBox.ItemRect(Index).left - 1; ScreenPointUpperLeft.y := ListBox.ItemRect(Index).top - 3; ScreenPointLowerRight.x := ScreenPointUpperLeft.x + ThisHintWindow.Canvas.TextWidth(ListBox.Items[Index]) + 7; ScreenPointLowerRight.y := ScreenPointUpperLeft.y + ThisHintWindow.Canvas.TextHeight(ListBox.Items[Index]) + 2; ScreenRect.TopLeft := ListBox.ClientToScreen(ScreenPointUpperLeft); ScreenRect.BottomRight := ListBox.ClientToScreen(ScreenPointLowerRight); Once we have the dimensions of the rectangle defined, we can finally display the hint window that is using it: ThisHintWindow.ActivateHint(ScreenRect, ListBox.Items[Index]); end; As long as the mouse cursor remains hovering over the item, the hint will be displayed. If the mouse cursor moves to another long item, the window will still be displayed, but the contents of it will be replaced by the text of the item the cursor is over. You may be asking: How and when does the hint window go away? Making the hint go away is the easy part. There are two conditions in which we want the hint window to not be displayed: when we move the mouse cursor to an item that is shorter than the width of the listbox, and also when we move the mouse cursor totally out of the listbox. In order to do this, however, we need some additional code that will tell us if the mouse cursor is over a specific component on the form: function TfrmMain.IsMouseOverControl(Control: TWinControl): Boolean; var P: TPoint; begin // Get the screen coordinates of the current mouse position GetCursorPos(P); // The mouse is over the control if : (a) the control is defined AND created, (b) // it is a WINDOWED control, and (c) the handle of the window the mouse is // currently over is the same as the handle of the control we passed in Result := Assigned(Control) and IsWindow(Control.Handle) and (WindowFromPoint(P) = Control.Handle); end; We can use this function to determine whether or not the mouse cursor is currently over the listbox. If we are not over the listbox, we want to destroy the hint window. We do this by calling the ReleaseHandle() procedure of the hint window: if not IsMouseOverControl(lstProducts) and (ThisHintWindow nil) then ThisHintWindow.ReleaseHandle; We can do this because this procedure is used specifically for hint windows that are activated calling the ActivateHint() procedure, which we did in the OnMouseMove() event handler above. In closing... As you can see, displaying a hint for any listbox items that are too wide is quite trivial by using the techniques above. The default size of a listbox when it is dropped on a form is pretty small. If you do have space constraints, this method can be used to prevent information in your programs from being lost. ------------------------ SNIP ----------------------------- Project1.dpr: program Project1; uses Forms, Unit1 in 'Unit1.pas' {frmMain}; {$R *.RES} begin Application.Initialize; Application.CreateForm(TfrmMain, frmMain); Application.Run; end. Unit1.dfm: object frmMain: TfrmMain Left = 270 Top = 396 Width = 238 Height = 351 Caption = 'Hints for long listbox items' Color = clBtnFace Font.Charset = DEFAULT_CHARSET Font.Color = clWindowText Font.Height = -11 Font.Name = 'MS Sans Serif' Font.Style = [] OldCreateOrder = False OnCreate = FormCreate OnMouseMove = FormMouseMove PixelsPerInch = 96 TextHeight = 13 object lstProducts: TListBox Left = 30 Top = 9 Width = 170 Height = 263 ItemHeight = 13 Items.Strings = ( 'This is a short listbox item' 'This is a longer listbox item' 'This is an even longer listbox item' 'This is a really really really long listbox item' 'This is a stupendously long listbox item (Amazing, ain'#39't it?)') TabOrder = 0 OnMouseMove = lstProductsMouseMove end object btnOK: TButton Left = 78 Top = 286 Width = 75 Height = 25 Caption = '&Ok' TabOrder = 1 OnClick = btnOKClick end end Unit1.pas unit Unit1; interface uses Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls; type TfrmMain = class(TForm) lstProducts: TListBox; btnOK: TButton; procedure lstProductsMouseMove(Sender: TObject; Shift: TShiftState; X, Y: Integer); procedure FormCreate(Sender: TObject); procedure FormMouseMove(Sender: TObject; Shift: TShiftState; X, Y: Integer); procedure btnOKClick(Sender: TObject); private { Private declarations } ThisHintWindow : THintWindow; procedure CheckHint(var HintStr: string; var CanShow: Boolean; var HintInfo: THintInfo); function IsMouseOverControl(Control: TWinControl): Boolean; public { Public declarations } end; var frmMain: TfrmMain; implementation {$R *.DFM} procedure TfrmMain.CheckHint(var HintStr: string; var CanShow: Boolean; var HintInfo: THintInfo); begin // If the control the hint window is over is our listbox ... if (HintInfo.HintControl = lstProducts) then // Back up the position of the hint window so it's not "stepping on" the text HintInfo.HintPos.y := HintInfo.HintPos.y - 24; end; function TfrmMain.IsMouseOverControl(Control: TWinControl): Boolean; var P: TPoint; begin // Get the screen coordinates of the current mouse position GetCursorPos(P); // The mouse is over the control if : (a) the control is defined AND created, (b) it is a WINDOWED // control, and (c) the handle of the window the mouse is currently over is the same as the handle // of the control we passed in Result := Assigned(Control) and IsWindow(Control.Handle) and (WindowFromPoint(P) = Control.Handle); end; procedure TfrmMain.lstProductsMouseMove(Sender: TObject; Shift: TShiftState; X, Y: Integer); var ThePoint : TPoint; ScreenPointUpperLeft : TPoint; ScreenPointLowerRight : TPoint; ScreenRect : TRect; ListBox : TListBox; Index : integer; begin // Local variable so we don't have to typecast every time ListBox := (Sender as TListBox); // Get the current location of the mouse cursor ThePoint.x := X; ThePoint.y := Y; // Get the index of the listbox item the mouse cursor is currently over Index := ListBox.ItemAtPos(ThePoint, true); // Make sure we're over a listbox item if (Index -1) then begin // If the text of the item the mouse is over is longer than the width of the listbox, we // well want to pop up the hint window if ListBox.Canvas.TextWidth(ListBox.Items[Index]) ListBox.Width then begin // Find a decent place for the upper-left corner of the hint window to be displayed ScreenPointUpperLeft.x := ListBox.ItemRect(Index).left - 1; ScreenPointUpperLeft.y := ListBox.ItemRect(Index).top - 3; // Find a decent place for the lower-right corner of the hint window to be displayed ScreenPointLowerRight.x := ScreenPointUpperLeft.x + ThisHintWindow.Canvas.TextWidth(ListBox.Items[Index]) + 7; ScreenPointLowerRight.y := ScreenPointUpperLeft.y + ThisHintWindow.Canvas.TextHeight(ListBox.Items[Index]) + 2; // Define the boundaries of the hint windows rectangle using the two corners from above ScreenRect.TopLeft := ListBox.ClientToScreen(ScreenPointUpperLeft); ScreenRect.BottomRight := ListBox.ClientToScreen(ScreenPointLowerRight); // Show the hint using the listbox item the mouse cursor is currently over ThisHintWindow.ActivateHint(ScreenRect, ListBox.Items[Index]); end else ThisHintWindow.ReleaseHandle; end else ThisHintWindow.ReleaseHandle; end; procedure TfrmMain.FormCreate(Sender: TObject); begin // Defines the method to use when an application is to display the hint window Application.OnShowHint := CheckHint; // Create our own hint window ThisHintWindow := THintWindow.Create(Self); // Use the Windows-defined color. We don't want to force the user to use our color ThisHintWindow.Color := clInfoBk; end; procedure TfrmMain.FormMouseMove(Sender: TObject; Shift: TShiftState; X, Y: Integer); begin // This code is only used to 'disable' the hint window over long list items. We // want to turn this off if (a) the mouse cursor is NOT currently over the list, // and (b) the hint window is active if not IsMouseOverControl(lstProducts) and (ThisHintWindow nil) then ThisHintWindow.ReleaseHandle; end; procedure TfrmMain.btnOKClick(Sender: TObject); begin Close; end; end.