Balloon ToolTip Control

Predrag Bosnic

After the Multi-Line ToolTip control Predrag Bosnic created last month, most will say that this is just a variation, and it's easy to change the Multi-Line ToolTip control to the Balloon ToolTip control. So why a completely new article? Well, the truth is somewhere in the middle. Here, he'll use many basic principles he covered for the Multi-Line ToolTip, but don't forget he has to do something that's not natural for Visual FoxPro. This time, he'll introduce Windows API functions and show how they can help when help is needed.

First of all, let me see where I can find a Balloon ToolTip control. I use MS Word for writing, and the AutoShape/Callouts section contains a few different styles of balloons. Figure 1 shows these callouts, and I have to say each one has something interesting about it.

Figure 1. MS Word callouts.

The second application is MS Visio, but it uses the Balloon ToolTip to show the ToolTip text. Figure 2 shows the MS Visio implementation of the Balloon ToolTip control, and I have to say I like it; it's very elegant. In addition, the control can show a title and has a drop-shadow.

Figure 2. MS Visio Balloon ToolTip control.

On this occasion, I chose a simplified version of the MS Visio control. The title and drop-shadow will be skipped for the time being. At the end of this article, I'll offer some comments about possible improvements.

Figure 3 shows the design I want to achieve and the basic proportion I want to keep. It's possible to change the value of "X" inside the code. The maximum width of the control is about 363 pixels, and that would be sufficient for about 60 characters. Figure 4 shows four possible orientations that the ToolTip can have. Which one will be used depends on the mouse position on the screen. The default position is the position K1.

Figure 3. The basic proportion for the ToolTip control.

Figure 4. All four possible orientations for the Balloon ToolTip control.

Design requirements

Please see the requirements for the Multi-Line ToolTip in last month's issue. (Subscribers can access the article in the "View Past Issues" area of the FoxTalk Web site.)

Design and implementation

For this solution, I'll use Windows API functions. The reason is very simple; I don't see how I can do this with the native Visual FoxPro 7 controls. In my opinion, this is a good example of when I should use Windows API functions. When a programming language I use can't help to solve the problem (or at least there's not an easy solution), the Windows API should be used. Of course, the programming language has to have the ability to call the Windows API and pass adequate parameters. Visual FoxPro isn't ideal to call the Windows API, but, with every new release, it's getting better and better. The Visual FoxPro 7 release brings in hWnd (windows handler) as a property of the form control. The windows handler is used in hundreds of Windows API functions. Also, the DECLARE statement has improved, and, if I add on top of that a couple of good articles about using the Windows API in Visual FoxPro (Christof Lange's in the public domain and Doug Hennig's articles in FoxTalk), a lot can be done.

I'll create a class library wbToolTipB to be a container for my classes. This library has two classes: wbToolTipB and ttScr2. This time, I'll first describe the wbToolTipB class. This class is based on the Timer control and has a few user-defined properties.

•oObjRef—Reference to the object(for instance, button, combo box, and so on)

•ToolTipForeColor—ToolTip fore color

•ToolTipBackColor—ToolTip back color

The Init method contains only declarations for Windows API functions.

Declare Integer CreateRectRgn in "gdi32"

Declare Integer CreateRoundRectRgn in "gdi32"

...

It's worth mentioning that this class will be on the form that uses the Balloon ToolTip control, and using its Init method to declare Windows API functions is correct. The Init method is executed only once, and the user must not care about declaring necessary Windows API functions. I'll give you an explanation of using some of the API functions, but I have to say that I don't use all of the declared functions. I suggest you read about the following Windows API functions: CreateRectRgn, CreateRoundRectRgn, CreateEllipticRgn, CreatePolygonRgn, FillRgn, PaintRgn, CombineRgn, SetWindowRgn, DeleteObject, ReleaseCapture, GetDC, ReleaseDC, CreateSolidBrush, Polygon, SelectObject, CreateFontIndirect, TextOut, SetTextColor, GetTextColor, and FrameRgn.

The only important method is the Timer method, and its code follows:

* Timer event

LOCAL joObject, jnLeft, jnTop, jcToolTip, joX

joX = SYS(1270)

IF TYPE('joX') > 'O' or ISNULL(joX)

thisform.wb_LIsToolTipActive = .f.

thisform.KillToolTip()

this.Enabled = .f.

RETURN

ENDIF

*

IF joX # this.oObjRef

thisform.wb_LIsToolTipActive = .f.

thisform.KillToolTip()

this.Enabled = .f.

return

endif

*---

IF thisform.wb_LIsToolTipActive = .t.

RETURN .f.

endif

*

joObject = this.oObjRef

jcToolTip = ALLTRIM(joObject.wb_cToolTipText)

IF EMPTY(jcToolTip)

this.Enabled = .f.

return

ENDIF

*

jnLeft = Mcol('',3)

jnTop = Mrow('',3)

thisform.wb_oToolTip = CREATEOBJECT('ttscr2',thisform,;

thisform.wb_nToolTipType,jcToolTip,jnLeft,;

jnTop,this.ToolTipForeColor,this.ToolTipBackColor)

thisform.wb_LIsToolTipActive = .t.

Thisform.wb_oToolTip.Visible = .T.

thisform.wb_oToolTip.Show()

*------

*** Don't kill the timer here.

*** Timer must be active during the ToolTip session!!

Keep in mind that the timer is active only during the ToolTip session, while the ToolTip is shown. When the Timer method is running, it investigates the position of the mouse pointer.

The code in the first IF command deactivates the ToolTip, and then the timer deactivates itself if the mouse pointer isn't over an object

The second IF command investigates the object under the mouse pointer. If it's not the same object that's activated the ToolTip control (when the ToolTip control has been activated, the code sets the oObjRef property and now is using it), the code deactivates it because the user has moved the mouse pointer outside of the control that created the ToolTip.

The third IF statement tests the wb_LIsToolTipActive property. If the ToolTip is already active, it simply returns control from the Timer event code.

The fourth IF command tests the ToolTip text. If it's empty, it returns the control from the Timer event code. If the execution achieves this point, it means the ToolTip isn't active, and the code must activate the ToolTip control. At this point, I scan the mouse position, and then I call the code to create the Balloon ToolTip and set the flag:

wb_lIsToolTipActive = .T.

Now it's time to create the class definition for Balloon ToolTip—ttScr2.

As you can see in Figure 5, the class contains the same controls as the ttScr1 class used for the Multi-Line ToolTip control. However, the Init method is different.

Figure 5. The wbToolTipB class inside the Class Designer.

LPARAMETERS toParentForm, tnToolTipType, ;

tcToolTipText, tnLeft, tnTop, tnForeColor, ;

tnBackColor

* toParentForm - reference to the Parent form

* tnToolTipType - tooltip type, 1=rectangle

* tcToolTipText - ToolTip text

* tnLeft - xCoord of the mouse pointer

* tnTop - yCoord of the mouse pointer

* tnForeColor - fore color

* tnBackColor - back color

*------

WITH thisform

.oParentForm = toParentForm

.nObjCenterX = tnLeft

.nObjCenterY = tnTop

.comment = ALLTRIM(tcToolTipText)

.Edit1.Value = .comment

.ToolTipType = tnToolTipType

IF !EMPTY(tnForeColor)

.Edit1.ForeColor = tnForeColor

ENDIF

IF !EMPTY(tnBackColor)

.BackColor = tnBackColor

endif

ENDWITH

WITH thisform

.Left = tnLeft

.Top = tnTop

.Skin(this)

ENDWITH

The ToolTip form ttScr2 accepts a few parameters, sets the ToolTip color, and calls a Skin method. After that, the ToolTip is shown. If you compare this code with Multi-Line ToolTip code, the difference is in the Skin method.

* ttScr2, Skin method

LPARAMETERS tnWidth, tnHeight

* tnWidth - optional, ToolTip form width

* tnHeight - optional, ToolTip form height

#DEFINE RGN_AND 1

#DEFINE RGN_OR 2

#DEFINE RGN_XOR 3

#DEFINE RGN_DIFF 4

#DEFINE RGN_COPY 5

*

LOCAL W, H, NoOfLines, jaDots, EH, K, strDots,;

Delta, jnX, jnY, xMin, xMax

LOCAL yMin, yMax, nSkinRgn, jnDots, DeltaCut

*------Set DEFAULTS for Width ------

IF EMPTY(tnWidth) or tnWidth < 363

W = 363

ELSE

W = tnWidth

ENDIF

*

NoOfLines=1

DO CASE

CASE thisform.ToolTipType = 1

NoOfLines = Int(LEN(toForm.comment)/60) + 1

EH = 21 + 15*(NoOfLines - 1)

Delta = 10

Delta2= 2*Delta

thisform.Edit1.Height = EH - 4

*------

H = EH + Delta2

IF NoOfLines = 1

W = thisform.textwidth(thisform.Comment)+25

ELSE

W = W + Delta

ENDIF

WITH thisform

.width = W

.Height = H

.Edit1.Width = .width-6-Delta-8

.Edit1.Left = Delta + 3

ENDWITH

OTHERWISE

*

ENDCASE

*------

IF NoOfLines = 1

thisform.Edit1.Alignment = 0

ENDIF

*------

jnX = thisform.nObjCenterX

jnY = thisform.nObjCenterY

Xmin = W

Xmax = _screen.Width-W

Ymin = H

Ymax = _screen.Height - H

*

Xscr = _screen.Width

Yscr = _screen.Height

* I

* K2 I K1

* I

* ------

* K3 I K4

* I

* I

*

DO case

CASE jnX > Xmax and jnY < Yscr/2

K=3

WITH thisform

.Left = jnX - W

.Top = jnY + Delta2

.Edit1.Top = H - EH + 2

ENDWITH

thisform.edit1.Left = 3

CASE jnX > Xscr/2 and jnX < Xmax and jnY < Ymin

K=3

WITH thisform

.Left = jnX - W

.Top = jnY + Delta2

.Edit1.Top = H - EH + 2

ENDWITH

thisform.edit1.Left = 3

CASE jnX > Xmin and jnX < Xscr/2 and jnY < Ymin

K=4

WITH thisform

.Left = jnX

.Top = jnY + Delta2

.Edit1.Top = H - EH + 2

ENDWITH

thisform.Edit1.Width = thisform.width-6-Delta

CASE jnX < Xmin and jnY < Yscr/2

K=4

WITH thisform

.Left = jnX

.Top = jnY + Delta2

.Edit1.Top = H - EH + 2

ENDWITH

thisform.Edit1.Width = thisform.width-6-Delta

CASE jnX < Xmin and jnY > Yscr/2

K=1

thisform.Left = jnX

thisform.Top = jnY -H - Delta2

thisform.Edit1.Width = thisform.width-6-Delta

CASE jnX > Xmin and jnX < Xscr/2 and jnY > Ymax

K=1

toForm.Left = jnX

toForm.Top = jnY - H - Delta2

thisform.Edit1.Width = thisform.width-6-Delta

CASE jnX > Xscr/2 and jnX < Xmax and jnY > Ymax

K=2

thisform.Left = jnX - W

thisform.Top = jnY - H - Delta2

thisform.edit1.Left = 3

thisform.Edit1.Width = thisform.width-6-Delta

CASE jnX > Xmax and jnY > Yscr/2

K=2

thisform.Left = jnX - W

thisform.Top = jnY - H - Delta2

thisform.edit1.Left = 3

thisform.Edit1.Width = thisform.width-6-Delta

OTHERWISE

K=1

thisform.Left = jnX

thisform.Top = jnY -H - Delta2

thisform.Edit1.Width = thisform.width-6-Delta

ENDCASE

*

DO CASE

* Default: rectangle-Visio

CASE thisform.ToolTipType = 1

jnDots = 7

DIMENSION jaDots(jnDots,2) & Region

thisform.DotsRectangle_v(;

@jaDots, k, h, delta, eh, w, delta2 )

OTHERWISE

WAIT WINDOW "Error in ToolTip call."

RETURN

ENDCASE

*

strDots = this.CreateDots(@jaDots)

nSkinRgn = CreatePolygonRgn(strDots,jnDots,1)

*

With thisform

Local HDC, hRgnO1, jnRez

HDC = GetDC( .HWND )

Local lnPrevBrush, lnBrush

lnBrush = CreateSolidBrush(RGB( 0,0,0))

lnPrevBrush = SelectObject(m.HDC,m.lnBrush)

jRez = FrameRgn(HDC,nSkinRgn,lnBrush,1,1)

SelectObject( m.HDC, m.lnPrevBrush )

DeleteObject( m.lnBrush )

DeleteObject(hRgnO1)

ReleaseDC( .HWND, m.HDC )

ENDWITH

=SetWindowRgn(thisform.HWnd, nSkinRgn, .f.)

RETURN

Basically, I'm drawing the ToolTip control on the form. First, the code calculates the width and height of the ToolTip control and then the orientation of the ToolTip control (K1, K2, K3, or K4). The next important line is the call of the CreatePolygonRgn function. This function creates a region and returns a handler to it. In my case, the region is in the shape of the ToolTip control, and this shape is defined by the DotsRectangle_v method. The CreateDots method creates a hexadecimal interpretation of the ToolTip coordinates. The code inside the With...EndWith structure uses a few Windows API functions to draw the border around the ToolTip control, to paint it, and, finally, the SetWindowRegion function will combine the form and the ToolTip region. The last part means that the ToolTip is visible, but the rest of the form is transparent. Figure 6 shows an example of the Balloon ToolTip control in action.

Figure 6. Form controls using the Balloon ToolTip control.

It's important to note that the form must call the Skin method every time it's painted. The Paint method contains the following code:

thisform.Skin(this)

The wbToolTipB library

This library contains two classes: ttScr2 and wbToolTipB. The latter class must be added to the form. All controls on the form have to support the Balloon ToolTip.

The ttScr2 class

Figure 7 shows the ttScr2 class with it methods and properties; they're described in Table 1 and Table 2.

Figure 7. The ttScr2 class with its methods and properties.

Table 1. The ttScr2 methods.

Name / Type / Description
Skin / Private / Used internally. It calculates, draws, and repaints a ToolTip control.
CreateDots / Private / Used internally. It creates a dot array to draw a polygon.
DecToHex / Private / Performs conversion: Decimal to Hex.
DotsRectangle_v / Private / Defines seven significant points to draw a defined ToolTip shape.

Table 2. The ttScr2 properties.

Name / Type / Description
nObjCenterX / Int / Used internally. Mouse pointer position, X coord.
nObjCenterY / Int / Used internally. Mouse pointer position, Y coord.
oParentForm / Int / Reference to the parent form object.

The wbToolTipB class

Figure 8 shows the wbToolTip class with it methods and properties; the properties are described in Table 3.

Figure 8. The wbToolTip class with its methods and properties.

Table 3. The wbToolTip properties.

Name / Type / Description
nObjRef / Int / Used internally. Reference to the object on the form.
ToolTipBackColor / Int / ToolTip back color.
ToolTipForeColor / Int / ToolTip fore color.

Improvements

Apart from improving the current code, think about the following:

•A ToolTip with rounded corners

•A ToolTip with a title and possibly an icon (see Figure 2)

•A cloud-style ToolTip control

Conclusion

The Balloon ToolTip control is a very attractive control, and it can find a place in all applications. This solution isn't heavy, and the speed is satisfactory. Most probably, you can see many similarities between the Multi-Line ToolTip and Balloon ToolTip controls. If you're thinking about putting both classes together in one class, I advise you not to. You'll probably use only one type of ToolTip control, and the rest of the code is an unnecessary overload. Next time, I'll show you how to build a very specific ToolTip control—a ComboBox Item ToolTip control.

Download 07BOSNIC.ZIP

Predrag Bosnic is a senior developer for London's Westwood Forster Ltd. .