Reusing a Toolbar's ToolTip control

By: Steve Waring
Home Pages: Journal Dolphinharbor
Created: 20021101
Last Update: 20021101

Overview

Most of the support code for displaying tooltips for controls other than just the toolbar, is present in the base image. It just needs a bit of tweaking to make it happen.

Background

I wanted to use a tooltip for a normal PushButton. A quick browse through the View classes looked encouraging; methods such as ControlView>>ttnNeedText: and the Presenter>>onTipTextRequired: pattern, suggested it could be done, however the base UI only seemed to display Tooltips for Toolbar buttons, and there is no ControlView subclass for the ToolTip control.

The key is on this MSDN page ... A ToolTip control can support any number of tools.

The lock is that a toolbar contains a ToolTip control, and it is this control that can be reused.

Workspace Example

With this information, it was time to start prodding and poking, using a workspace and the Flipper inspector. I created a quick Shell subclass, and using the ViewComposer, created a Shell with a Toolbar and a PushButton. To allow easier access from a workspace, I named the Toolbar "toolbar" and the PushButton "button".

tooltip

Firstly, setup a few temporary variables in a workspace. The important message is the base method Toolbar>>tbGetToolTips.


"Create and show the Shell"
shellPresenter := SWTooltipsSample show.

"Ask the Toolbar for a handle to it's ToolTip control"
toolTipHandle := (shellPresenter view viewNamed: 'toolbar') tbGetToolTips.

"Get a pointer to the Button view for which we want to show a tooltip"
buttonView := shellPresenter view viewNamed: 'button'.

The approach we need to take is described in the "Supporting Tools" section on this MSDN page. The TOOLINFO structure is already wrapped in the base image, so we dont have much work to do. To make things neater, you can add the constants in the CommCtrlConstants PoolDictionary to your workspace by using the Workspace/Pools menu command.


toolInfo := TOOLINFO new.
toolInfo uFlags: TTF_IDISHWND | TTF_SUBCLASS.
toolInfo uId: buttonView handle.

The above TOOLINFO settings look like the simplest thing that will work. The base image methods look like they expect the "uId" to be a window handle, and the TTF_SUBCLASS flag means that we can get Windows to do the work of TTM_RELAYEVENT.

Continuing on with the easiest way; we can set the tooltip text into the TOOLINFO instead of dealing with callbacks. If you look at the comment in the TOOLINFO>>text: helper method, you can see that it is important that we keep a reference to this string.


myTooltip := 'This is the tip'.
toolInfo text: myTooltip.

It's now time to join the dots, and tell Windows about our button. We can't use the typical View>>sendMessage: helper methods because we don't have a View instance for the ToolTip control, all we have is an external handle that the ToolBar gave us. We can use the UserLibrary DLL function directly.

Keep your fingers crossed, hold your breath, pray to the windows gods, or whatever it is you do before entering the unknown of the the Windows API and:

answer := UserLibrary default
sendMessage: toolTipHandle
msg: 1028 "TTM_ADDTOOL"
wParam: 0
lParam: toolInfo yourAddress

Hopefully nothing much happened, but if you move your mouse over to the button, sooner or later this should appear:

tooltip

Yippee! ... too easy :)

To test this technique on various Windows versions, I moved the workspace code into my Shell presenter's onViewOpened method, created an instance variable to hold the tooltip string, and deployed it as a ToGo test application (390k). This is released as TestWare; if you have any problems running this in your environment please let me know about it. It has been run successfully on WinXP, Win2k, and even works on my Win95 torture test machine.

Presenter example

Functionally, the disadvantage of the above approach is that the tooltip text is not easily changed. It would be nice to take advantage of the base ControlView>>ttnNeedText: method, which will use the handle we set into the TOOLINFO structure, to locate our presenter, and ask it for the text using #onTipTextRequired:.

From reading the TOOLINFO MSDN page it looks like we need to set lpszText to LPSTR_TEXTCALLBACK. A quick "Containing text ..." search through the image turns up a couple of hits ... what do you know, Objects-Arts already has a helper method in TOOLINFO that does this for us.

With the trusty debugger always ready to go, I use a policy of leaping before I look. I re-evaluated the workspace code, this time sending the toolInfo the #textCallback message. mmm, nothing happens ... where is that debugger when you need it? I put Transcript trace code in the #ttnNeedText: methods, but nothing was turning up.

An hour or two of teeth gnashing, Transcript tracing, MSDN searching and Messaging spying turned up the answer (in an Object-Art's comment of course!). I copied Toolbar>>wmNotify:wParam:lParam to PushButton, and Yippee! ... I have my debugger up asking for a PushButton>>toolTipText method. I implemented this as "^self commandDescription description" and Yippee again, it worked.

tooltip

The above screenshot is from a second ToGo test application (473k). This is released as TestWare; if you have any problems running this in your environment please let me know about it. It has been run successfully on WinXP, Win2k, and Win95 machines.

The following loose methods were added to PushButton, and were mostly cut and pastes:

This routes the windows notification to the pushButton (instead of looking for a non-Dolphin ToolTip control).

PushButton>>wmNotify: message wParam: wParam lParam: lParam

"SW: Copy and Paste from ToolBar>>wmNotify:wParam:lParam:"

| pnmhdr code |
pnmhdr := lParam asExternalAddress.
code := pnmhdr sdwordAtOffset: 8.
(code == TTN_NEEDTEXTW or: [code == TTN_NEEDTEXTA])
ifTrue: [^self nmNotify: pnmhdr].
^super wmNotify: message wParam: wParam lParam: lParam
PushButton>>toolTipText

^self commandDescription description

The next two methods enable similar behavior to a ToolBar in that the Presenter that implements the PushButton's command, gets the opportunity to set the tooltip text. In the Test ToGo, this is demonstrated by including a count of how many times #onTipTextRequired: is sent.

PushButton>>onTipTextRequired

"SW: Use the same approach as ToolbarButton>>onTipTextRequired"

| query |
query := self commandSource queryCommandRouteFor: self commandDescription.
^(query canPerform and: [query receiver respondsTo: #onTipTextRequired:])
ifTrue: [query receiver onTipTextRequired: self]
ifFalse: [self toolTipText]

PushButton>>ttnNeedText: aTOOLTIPTEXT

"Private - Generic handler for the TTN_NEEDTEXT(A/W) notification message.
SW: Unlike Toolbar which is its own presenter, we need to ask the receiver directly for onTipTextRequired.
Added a check that the tool is the receiver, but I dont see how that will ever be false"

| tool idOrHandle |

idOrHandle := aTOOLTIPTEXT idFrom.
tool := (aTOOLTIPTEXT uFlags anyMask: TTF_IDISHWND)
ifTrue: [View withHandle: idOrHandle]
ifFalse: [idOrHandle].
tool isNil ifTrue: [^nil].
aTOOLTIPTEXT text: (tool == self
ifTrue: [self onTipTextRequired]
ifFalse: [self presenter onTipTextRequired: tool]).
^0

The following is the onViewOpened method of the test Shell:

SWTooltipsSampleC>>onViewOpened

| toolTipHandle buttonView toolInfo |
super onViewOpened.
toolTipHandle := (self view viewNamed: 'toolbar') tbGetToolTips.
buttonView := self view viewNamed: 'button'.
(toolInfo := TOOLINFO new)
uFlags: TTF_IDISHWND | TTF_SUBCLASS;
hwnd: buttonView handle;
uId: buttonView handle;
textCallback.
UserLibrary default sendMessage: toolTipHandle msg: 1028 "TTM_ADDTOOL" wParam: 0 lParam: toolInfo yourAddress

Copyright Steve Waring 2002.