Playing with the GUI in Visual FoxPro 7
ComboBox Item ToolTip Control
Predrag Bosnic
In this article, Predrag Bosnic explores implementing the ComboBox Item ToolTip control in Visual FoxPro 7 and shares his experiences along the way.
I remember when I started using Visual FoxPro 7 for the first time, and I wanted to write a few lines of code in the Init method of my form. The code window was open, but the Procedure combo box was showing the Load method. I clicked on the combo box, and then I saw for the first time the Visual FoxPro 7 implementation of the ComboBox Item ToolTip control. I have to say I was very surprised (positively) because I think, sometimes, this type of help is very useful.
Analysis
As you can see in Figure 1, Visual FoxPro 7 uses the Multi-Line ToolTip control as a basis for this control. I have previous experience implementing a Multi-Line ToolTip control and had confidence that I could implement this one also.
Figure 1. Visual FoxPro implementation of ComboBox Item ToolTip.
Let's see how this control works in Visual FoxPro 7. A click on the combo box opens it, but you don't see the ToolTip. When you move the mouse pointer from the selected item, the ToolTip appears and will stay open until you close the combo box or click outside the ComboBox control. The ToolTip can have one or more lines of text and can have the position on the left or on the right side of the ComboBox control. Once more, this ToolTip control doesn't have a timer control and will stay open until you perform any of the previously mentioned actions.
It's difficult to say what's the maximum number of characters Visual FoxPro 7 can show in this control, but mine will show 254. For maximum width, I think 60 characters is a good choice. It means the ToolTip can have up to five rows.
Design requirements
From everything I said in the analysis, the design requirements are:
• ComboBox Item ToolTip must show ToolTip text of up to 254 characters. This is in line with the Multi-Line ToolTip control.
• Maximum line length has to be up to 60 characters. This is also in line with the Multi-Line ToolTip control.
• ComboBox Item ToolTip must be activated/deactivated independently of the native Visual FoxPro ToolTip.
• The control must be easy to use. If necessary, an appropriate builder has to be supplied.
• The ToolTip has to have ForeColor/BackColor properties.
• The control has to be compatible with existing applications.
Design and implementation
Each item in the combo box has to have text associated with it. Having that in mind, I can't see the solution with a native Visual FoxPro 7 ComboBox control. If I add on top of that the fact that I have to calculate the item under the mouse pointer, then I don't like the idea of using the native Visual FoxPro 7 combo box at all. For a lazy programmer like me, there must be another solution somewhere around the corner! It seems the problem is how to determine which item is under the mouse pointer. If I can get that, the rest—finding the text associated with the item and showing it in the ToolTip window—will be easy.
I remembered something very interesting: The MS TreeView control has a method called HitTest and enables programmers to know the node under the mouse pointer. The Drag-n-Drop operation uses this feature. If a native Visual FoxPro 7 ComboBox or ListBox control could have something like this, it would be ideal. Unfortunately, I can't find anything like that.
Then again, something else struck my mind: I read about a Grid control and its GridHitTest method while using Visual FoxPro 7 beta. Indeed, the Grid control has that method and can return the cell position under the mouse. Eureka! This could be a solution. If you don't see how, then imagine a container with a combo box above a grid. The grid only has one column; the header is hidden; the horizontal scroll bar is hidden; the record marker is hidden; and the horizontal lines are hidden. Figure 2 shows this idea.
Figure 2. Idea for a ComboBox Item ToolTip.
How does the control work? The container shows only the ComboBox control. When I click the combo box, it doesn't open; instead, the container size (height) is changed, and the container shows the combo box and the grid. When I move the mouse pointer over the grid, the GridHitTest method returns the cell position; I can highlight that cell, find the ToolTip text, and show the ToolTip control. At the end, when I click on any grid row to select an option, I deactivate the ToolTip control; I again change the container size (height) and show only the combo box. As you know, the Grid control is based on a table, and I have to put my combo box items in that table. This isn't bad, because during design time I can create a table with combo box items in the first column, and then I can add the second column with the ToolTip text and the third column with numbers to keep the item order. Table 1 shows this idea.
Table 1. ComboBox Item ToolTip metadata.
xName / xOrder / xDescOption-1 / 1 / This is ToolTip text for Option-1.
Option-2 / 2 / This is ToolTip text for Option-2.
Option-3 / 3 / This is ToolTip text for Option-3.
Similarly, I can use a ListBox control and calculate the position under the mouse pointer. The ListBox control doesn't have a HitTest method, but it has a TopIndex property. Using that property, the specific font for the ListBox and the MouseMove event for the ListBox, I can calculate the item under the mouse pointer. However, I need a table to keep my ComboBox Item ToolTip text, so I'm using the Grid control as a solution.
Let's create a library called wbComboToolTip.vcx and create a new class based on the form called ttScr1. Set the following properties:
TitleBar = 0
BorderStyle = 1
AlwaysOnTop = .T.
Add an EditBox control to the form and set the following properties:
BackColor = rgb(239,241,194)
DisabledBackColor = rgb(239,241,194)
DisabledForeColor = rgb(0,0,0)
FontName = Tahoma
FontSize = 9
ScrollBars = 0
SpecialEffect = 1
Left = -1
Top = -1
The last two properties will produce the shadow effect because the form has a single-line border. The Init method of the ToolTip form contains the following code:
LPARAMETERS toParentCbo, tcToolTipText, tnLeft, ;
tnTop, tnForeColor, tnBackColor
* toParentCbo - reference to the parent combobox
* tcToolTipText- ToolTip text
* tnLeft - xCoord of the ToolTip object
* tnTop - yCoord of the ToolTip object
* tnForeColor - fore color
* tnBackColor - back color
WITH thisform
.oParentCbo = toParentCbo
.comment = ALLTRIM(tcToolTipText)
.Edit1.Value = ALLTRIM(this.comment)
IF ! EMPTY(tnForeColor)
.Edit1.DisabledForeColor = tnForeColor
ENDIF
IF !EMPTY(tnBackColor)
.Edit1.DisabledBackColor = tnBackColor
Endif
ENDWITH
LOCAL jnTTwidth as Integer
jnTTwidth = thisform.CalcSize()
IF (tnLeft+toParentCbo.Width+jnTwidth)> ;
_screen.Width
this.Left = tnLeft - jnTTwidth
ELSE
this.Left= tnLeft + this.width
ENDIF
thisform.Top = tnTop
As you can see, the Init method calculates the left position of the ToolTip because it depends on the width of the ToolTip control. The CalcSize method calculates the width. The calling code calculates the top position of the ToolTip control because it has all the necessary information for this calculation.
Now, I can construct the ComboBox control. As I said, I'm using the container, a native ComboBox, and a Grid control. Figure 3 shows how it will look at the end.
I use the same library, wbComboToolTip.vcx, but this time I create a new class with the same name, wbComboToolTip, based on the container class. Before I start with coding, let me add a few properties that I need.
• cTable—Table name to keep all combo items and ToolTip text
• lEscape—User left the control without changing the selection
• Wb_ComboToolTipBackColor—Combo ToolTip BackColor
• Wb_ComboToolTipForeColor—Combo ToolTip ForeColor
• Wb_LcboTTactive—Combo ToolTip already active
• Wb_oCboToolTip—Object reference to the ToolTip control
• xStyle—2D/3D style
Figure 3. The wbComboToolTip control inside the Class Designer.
Now, I add a combo box to the container and set a few properties.
Name = wbcTT_combobox1
DisabledBackColor = 255,255,255
Left = 0
Top = 0
Now for the ComboBox.MouseDown method:
SELECT (this.Parent.cTable)
with this.parent
.height = .nDesignHeight
.Timer1.enabled = .t.
.wbcTT_grdList.setFocus()
endwith
this.Enabled = .f.
As you see, the Container control changes the height; the timer becomes active; the combo box is disabled; and the grid gets the focus.
The next step is to add a Grid control to the container. Adjust the grid width to be the same as the combo box width and set the next properties:
Name = wbcTT_grdList
AllowHeaderSizing = .F.
AllowRowSizing = .F.
ColumnCount = 1
DeleteMark = .F.
GridLines = 0
HeaderHeight = 0
HiglihtRowLineWidth = 0
RecordMark = 0
ScrollBars = 2 (Vertical)
SplitBar = .F.
Left = 0
Top = 24
RecordSourceType = 1
RecordSource = (None)
Of course, the most important method is Column.MouseMove, and here's the code:
* Column.MouseMove
LOCAL jnWhere_Out as Integer
Local jnRelRow_Out as integer, joParent as Object
joParent = .F.
jnWhere_Out = 0
jnRelRow_Out= 0
this.parent.GridHitTest(nXCoord, nYCoord, ;
@jnWhere_Out, @jnRelRow_Out)
this.parent.ActivateCell(jnRelRow_Out, 1)
joParent = this.Parent.parent
SELECT (joParent.cTable)
LOCATE FOR xname = Alltrim(this.wbcTT_text1.value)
jcToolTipText = Alltrim(xDesc)
LOCAL jnLeft, jnTop, jlCreateToolTip
jnLeft = 0
jnTop = 0
joParent.RetPosition(joParent, @jnLeft, @jnTop)
jnTop = jnTop + joParent.wbcTT_combobox1.Height + ;
(jnRelRow_out - 1)* this.parent.RowHeight
*------
jlCreateToolTip = !joForm.wb_lCboTTactive
joParent.showtooltip(jnLeft, jnTop, jcToolTipText, ;
jlCreateToolTip)
joParent.timer1.enabled = .t.
As I said, the most important part is to determine the item under the mouse pointer. Variable jnRelRow_Out keeps this value. Knowing this position, I know the row in the grid, I know the record in the table, and I can get the ToolTip text. The RetPosition method calculates the position of the ComboBox ToolTip control and returns values in the jnLeft and jnTop variables.
* wbToolTip.RetPosition
LPARAMETERS toObject, tnLeft, TnTop
* toObject - reference to the object
* tnLeft - left position to return
* tnTop - right position to return
IF Upper(toObject.Parent.BaseClass) = 'FORM'
tnLeft = tnLeft + toObject.Left
TnTop = tnTop + toObject.Top
tnLeft = tnLeft + Sysmetric(3) + ;
toObject.Parent.Left
tnTop = tnTop + Sysmetric(9) + Sysmetric(4) + ;
toObject.Parent.Top
IF Upper(toObject.BaseClass) = 'PAGEFRAME'
tnTop = tnTop + (toObject.Height - ;
toObject.PageHeight)
ENDIF
RETURN .t.
ELSE
IF PemStatus(toObject,'Left',5)
tnLeft = tnLeft + toObject.Left
TnTop = tnTop + toObject.Top
ENDIF
IF Upper(toObject.BaseClass) = 'PAGEFRAME'
tnTop = tnTop + (toObject.Height - ;
toObject.PageHeight)
ENDIF
joObject = toObject.Parent
RETURN this.RetPosition(joObject, ;
@tnLeft, @TnTop)
ENDIF
Having all of the needed values, I call the ShowToolTip method:
* wbToolTip.ShowToolTip
LPARAMETERS tnLeft, tnTop, tcToolTipText, ;
tlCreateToolTip
* tnLeft - ToolTip Left position
* tnTop - ToolTip Top position
* tcToolTipText - ToolTip text to show
* tlCreateToolTip - flag, create ToolTip or not
LOCAL joObject, jnLeft, jnTop, jcToolTip, ;
jnHeightBalonForme, jnCorection, jnTTwidth
IF EMPTY(tcToolTipText)
return
ENDIF
*--- Activate a ToolTip screen ---
IF tlCreateToolTip = .t.
this.wb_oCboToolTip =CREATEOBJECT('ttscr1',;
this,tcToolTipText,tnLeft,tnTop, ;
this.wb_ComboToolTipForeColor,;
this.wb_ComboToolTipBackColor)
WITH this.wb_oCboToolTip
.visible = .t.
.Show()
ENDWITH
this.wb_lcbottactive = .t.
ELSE
WITH this.wb_oCboToolTip
.Top = tnTop
.Comment = tcToolTipText
.Edit1.Value = tcToolTipText
jnTTwidth = .CalcSize()
IF (tnLeft + this.Width + jnTTwidth) > ;
_screen.Width
.Left = tnLeft - jnTTwidth
ELSE
.Left= tnLeft + this.width
ENDIF
ENDWITH
ENDIF
Using flags, wb_lCboActive and tlCreateToolTip, I can control the ToolTip behavior. It's created only once, and a movement of the mouse pointer will move the ToolTip and show the appropriate text.
Go back to the Column object of the Grid control. When the mouse pointer leaves the column, the MouseLeave event is triggered.
* Column.MouseLeave
LOCAL ox as Object
ox = Sys(1270)
IF Type('oX') = 'O' AND !IsNull(oX)
IF Upper(Left(oX.name,6)) # 'WBCTT_'
this.parent.parent.lEscape = .t.
this.wbcTT_Text1.Click()
ENDIF
ENDIF
In reality, it's possible to move the mouse pointer very quickly, and this event won't be triggered. For that reason, I added a Timer control to the container. The Timer method will test whether the mouse pointer is over the control or not.
The ToolTip form Activate method contains the following code:
* Form.Activate
joX = this.RetTopForm(thisform.oParentCbo)
joX.Show()
I can't allow the ToolTip form to get the focus. To suppress that, the ToolTip form must activate the main form (the form with our ComboBox control). In order to find out the form reference, I'll call the RetTopForm method.
* ToolTipForm.RetTopForm
LPARAMETERS toObject
IF Upper(toObject.Parent.BaseClass) = 'FORM'
RETURN toObject.Parent
ELSE
joObject = toObject.Parent
RETURN this.RetTopForm(joObject)
ENDIF
In addition, I can't allow the mouse pointer to enter the ToolTip area. To suppress that, the MouseEnter event on the edit box of the ToolTip control is used, and the code looks like this:
thisform.oParentCbo.lEscape = .t.
thisform.oParentCbo.wbcTT_Text1.Click()
For this concept to work, I need to create a table with the following structure:
• xName, char (25)—Item name
• xOrder I—Order number (it enables us to sort items)
• xDesc char (254)—ToolTip text
If I populate this table with a few records, it seems I have everything to test my control. All I have to do is create a form, add my ComboBox control on it, and set a property cTable with my table name. Figure 4 shows a more complicated example I used to test the control. As you can see, the control is on the form but also inside of the two-level deep containers and on the page of the PageFrame control. Every control has its own table to keep all necessary details straight.
Figure 4. An example of a ComboBox Item ToolTip in action.
wbComboToolTip library
This library contains two classes, ttScr1 (see Figure 5 and Tables 2 and 3) and wbComboToolTip (depicted in Figure 6 and Tables 4 and 5).