Bubble Help

Written by Eric Slaats

Introduction
In EDM/2 volume 3 Issue 8, I wrote a small article about a very easy way to implement a toolbar. A more complex but also more flexible way to implement a toolbar was described in Building Custom Controls in EDM/2 issue 3-4. The samples described in both articles work just fine as toolbars. However, most toolbars are hard to use if you don't know the meaning of the buttons. Even if you use a certain toolbar very often buttons can be confusing, especially if a lot of buttons are used. To be really useful a toolbar needs an on- line help facility. In this article I'll describe one way to add a bubble help facility to a toolbar. This kind of help is very common in Windows applications. In OS/2, however, this isn't implemented very often. The only application I know that has this feature is Describe 5.0.

In this article I'll use a slightly altered version of the Easy Buttonbars example to show how the bubble help works. However the code can be attached to the EDITOR examples with few code changes.

Some Choices
There are several ways to add help to a button bar. Usually it depends on the personal taste of the user which way is preferred. What I want in a button bar help has at least the following characteristics: This calls for something called bubble help. What is bubble help? Bubble help is a method which shows a small window with a short help text under a button when you mouse over the button. This has the advantage that the user doesn't have to take an extra action to display help. (Normally an OS/2 applet will shows a help page when a toolbar or menu button is depressed and F1 is pressed. Check SMALLED on this behaviour and you'll see what I mean.) Bubble help looks something like this:
 * 1) It will show when moused over (no extra actions needed).
 * 2) It can be viewed without taking your eyes of the pointer.
 * 3) It has a toggle (can be turned on/off).



Figure 1: Bubble help in action.

The disadvantage of the bubble help method is that a window has to be created and handled. A number of applications use an existing window, like the status bar (Borland C++ 2.0) or the title bar (WordPerfect), to show help text.

Besides bubble help we'll take a look at displaying help in the title bar (another existing window). The example used doesn't have a status bar, but if you employ a status bar it won't be difficult to change the code so that the help text will be displayed in the status bar. (Displaying help in the status or title bar has the disadvantage that the help is displayed far from the button were the help is wanted. So it violates the second characteristic!)

If the help function can't be turned off, it leads to situations where the user will be annoyed by the help display. Of course this should be avoided. In the examples presented in this article the help will have a toggle function. If this is done really well, this setting should be saved in the OS2.INI file or a private INI file! In this article saving settings in an INI file won't be covered. If information is needed on how to accomplish this, read Building an Editor, Part 3 in EDM/2 issue 3-9.

Another method to avoid user annoyance is to display the help after a certain time. So the mouse pointer has to be over a button a certain time, before the help will be displayed.

Having said all this what will be covered in this article? I will describe a way to add bubble help and a help display in the title bar. Also a way to toggle this help will be added. I will add pointers on how to implement a time triggered help and how to add help displayed in a title/status-bar. Actual code on the last two items won't be added.

To Catch a Mouse
If a mouse-pointer is moved over a window, a WM_MOUSEMOVE message is sent to this window. So if the mouse is moved over the menu bar (or toolbar) window, this message is sent to the menu window. We have to capture this message. We'll look at a way to find out above what button the mouse pointer resides in a minute. But first let's take a look at the WM_MOUSEMOVE message and how to capture it.

If the WM_MOUSEMOVE message is sent, it's sent to the window the mouse pointer mouses over. Normally, PM will take no action on this message; however, if there's a need, a program can act on this message. In our case we're interested in the WM_MOUSEMOVE messages sent to the menu window. If we want to act on messages sent to this window, we have to subclass it. Subclassing the menu window is done in the same way as described in the Building Custom Controls and The Infinitely Floating Spinbutton articles, in EDM/2 issues 3-4 and 3-2 respectively.

The menu window is subclassed by calling the WinSubclassWindow function with the menu window handle. The menu window handle can be obtained by invoking WinWindowFromID with the frame control identifier for the menu control, FID_MENU. The WinSubclassWindow function needs a pointer to the new window procedure and returns a pointer to the original window procedure. pfnwOldToolProc = WinSubclassWindow(WinWindowFromID(hwndFrame, FID_MENU),            NewToolProc); Figure 2: The WinSubclassWindow call

If a window is subclassed, the subclass procedure needs to know the pointer to the original window procedure so it can call this procedure for the messages that aren't handled. The easiest way to pass this pointer to the subclass procedure is to create a global variable which contains this pointer. This is a rather crude method, especially when there is a much more elegant way.

Every window has a space in which you can put values that are connected to that window. This space is known as window words. If you know the handle of a certain window, you're able to access these window words. So window words are very suitable to pass the pointer to the original window procedure to the subclass function. The following code subclasses a window and puts the pointer in the window-word. Figure 3: Subclassing the menu window.

Next we have to supply the window procedure for the menu window called NewToolProc in our example. As mentioned above, it has to call the original window procedure for all the messages it doesn't handle. So the minimal subclass window procedure looks like this: Figure 4: Simple subclass routine.

Now we have to react to the WM_MOUSEMOVE message. But first we take a look at when the WM_MOUSEMOVE occurs. This message is sent to a window over which the mouse pointer resides even if this window isn't active! If we display a bubble if a WM_MOUSEMOVE occurs, chances are that the window this message is sent to isn't active. If we display a bubble at that moment, it means setting up a window as a child of the window that received the WM_MOUSEMOVE. This would result in activating that window and thus being displayed on top. Normally this is unwanted behaviour. (However, you may want to implement this as a feature, the application will become active the mouse pointer moves over the menu window.)

To prevent this, we have to query the if the frame window is active as a first action on the WM_MOUSEMOVE message. if (hwndFrame == WinQueryActiveWindow(HWND_DESKTOP)) Figure 5: Is the frame active?

If this is the case, we can proceed with further handling of this message. We want to know above which button the pointer resides, so we need to know the pointer position as well as the rectangle position of every menubutton.

At this point we have to make a slight detour and modify the original code of the Easy Buttonbars example. In this example the buttons as well as the normal menuitems had the same resource ID. If we want to query the rectangle of a button in the menu, it must have a unique ID. If this isn't the case, the message used for querying the menubutton rectangle simply takes the first menuitem it encounters with the right ID. This can be a normal menuitem and not a button. So in the example added with this article, the buttons have their own ID. In the main window procedure this leads to using two ID's for one action. For example: Figure 6: New WM_COMMAND processing

Well back to business. To get the position (rectangles) of the buttons we'll use the MM_QUERYITEMRECT message. This message returns the bounding rectangle of a menu item relative to the menu window.

At this point we'll use a little trick. We can send this message to the menu control using WinSendMsg. However this would cause extra overhead because PM has to call the subclassed window procedure again which will pass it on to the 'normal window procedure' for menu windows. In the subclassed window procedure we have the pointer to this 'normal window procedure'. So it can be called directly. Figure 7: Query a button rectangle

In this example hwnd is the window handle passed to the subclass window procedure. So in this case this is the menu window handle.

Now we know how to obtain the size and place of a button, we have to check if the mouse pointer resides above a button and which button it is. To do this we need the position of the mouse pointer and this preferably in coordinates relative to the menu window. This way we don't have to remap them so that they can be compared with the values in the returned which are relative to the menu window.

We're in luck - the WM_MOUSEMOVE message passes the coordinates of the mouse pointer, relative to the window it is over, in message parameter 1.

SHORT1FROMMP(mpParm1) - pointing device x-coordinate. SHORT2FROMMP(mpParm1) - pointing device y-coordinate.

We can use the SHORT1FROMMP and SHORT2FROMMP macros to extract the coordinates, as shown above. However, extracting the coordinates from a WM_MOUSEMOVE is a so common action that MOUSEMSG macro is included in PMWIN.H. The pointer position can be obtained like this: case WM_MOUSEMOVE SHORT x = MOUSEMSG(&msg)->x; SHORT y = MOUSEMSG(&msg)->y; Figure 8: Using the MOUSEMSG macro

If the rectangle of a button is known, we can check if the mouse pointer resides over the button using the following lines: Figure 9: Is the pointer over a button?

What's left is the check for each button if the pointer resides over it. We can do this by using MM_QUERYITEMRECT for every button and compare it with the pointer position. However, this is a very crude way. Besides, if we expand our button bar, a lot of extra programming is required to handle the extra buttons (this is also true for the code that will display the help message in the bubble)! We will look for a more universal approach so that the button bar can be expanded without much effort.

To accomplish this, we need an array with all the button ID's and a value that contains the number of button in the toolbar. Figure 10: Defining the bubble array

The array's last element is a 0. We will use this value if the pointer resides over the menu bar, but when it isn't over a button. The precise approach for this value will be described in the next section. For now all we want to know is which button is under the mouse pointer. To accomplish this task we have to check each value in the array until one of these turns out to be under the mouse pointer, or until the 0 value is used. We want to remember the menuitem under the mouse pointer, because it will be used to display the help bubble. The code to accomplish this looks like this. Figure 11: Find the moused over button

If this piece of code is executed, we know if the mouse pointer resides over a button (sCurrentItem != 0) and we know the resource ID of that button. We'll use this knowledge in the next section to set up a bubble.

Displaying The Bubble
In the bubble window text should be displayed and it should have a thin border (the sizing or dialog border will look ugly on a small window). Besides that it should be displayed right under the current button.

To achieve this we'll create a frame window with a small border of which the client area consist of a WC_STATIC window that can display text. This can be done by two consecutive calls of WinCreateWindow. One for the frame, one for the static. However OS/2 provides a function that handles both in one call, WinCreateStdWindow. In this the properties of the frame window as well of the client window can be passed. The syntax of the WinCreateStdWindow looks something like this. Figure 12: WinCreateStdWindow

We have to figure out how all of these arguments must be filled to create the bubble we want. Here are the values for the most important fields when we create a bubble.

hwndParent - well, the bubble must be on top off all windows, besides that it must have the ability to show outside the main frame window. This is for the situation that a button resides on the far right side of the menu against the side of the window. If a bubble is called in that situation, it shouldn't be clipped to the main window, but shown partly outside the main window. So for the parent window we'll choose the desktop.

flStyle - the frame window style isn't really a concern here. The size and place of the window is determined elsewhere. For the style we use a 0.

pflCreateFlags - the bubble wants only one thing from the bubbleframe window, a thin border. This can be obtained by the creation flag FCF_BORDER. The frame-creation flags must be put into a ULONG of which the pointer is passed to WinCreateStdWindow.

pszClassClient - we want to display text in the bubble. The best suited window class to display text in is the WC_STATIC class. This means that the WC_STATIC window has to be formatted properly in the client window style flags.

flStyleClient - in this field we have to format the WC_STATIC window. We want to use this window to display text. The style flag that indicates this is SS_TEXT. Besides that we can set flags for aligning and justification. If we want the text to be verticaly centered and left justified. This gives us two more style flags, DT_LEFT and DT_VCENTER.

This leads to the following call to WinCreateStdWindow: Figure 13: Creating the bubble-window

We've created a bubble window, but after this call it has no size or place. We have to size and position the bubble with a call to WinSetWindowPos. With this call a number of things can be accomplished at once. Some tricks are needed to determine the place where the bubble has to be positioned. Our goal is to position the bubble right under the current button. Therefore, we need the position of bottom-left corner of the current button. We already have this value because we queried the button rectangles to check if the mouse pointer is over it. So the RECTL variable still contains the rectangle of the current button. However, this rectangle are relative to the menu window and we want to display the bubble relative to the desktop! There is a nice function that converts window coordinates from one window to another, WinMapWindowPoints. This function takes two window handles - a source handle and a destination handle - and an array of window points that have to be converted.
 * The window will be placed where we want it (under a button)
 * The window will get the size we want (matter of taste)
 * The window will be placed on top (float)

The array of points that WinMapWindowPoints takes are of the POINTL type. But the rectangle coordinates are of the RECTL type so we have to convert them. The left bottom point of the rectangle can be converted to desktop coordinates by the following call: Figure 14: Remapping coordinates

Now we know were the button in the desktop space resides, we can place the button with a call to WinSetWindowPos. The only thing that needs a decision is how large should the bubble be and how many pixels should there be between the button and the bubble. I chose the size (22,100) for the bubble size and I want two pixel rows between the bubble and the button. However, this is a question of taste. The button can be placed with the following call (using the above values): Figure 15: Size/place the bubble.

We now know were and how to display a bubble. But there is also the question of when a bubble should be displayed. A bubble should not be displayed when another already exists or when the pointer doesn't reside over a button but is elsewhere over the menu window.

To determine if a bubble exists, we check on the existence of the window handle of the bubble frame. Of course, this value has to be saved somehow so that it contains a valid value every time a WM_MOUSEMOVE message is received. In order to achieve this, the bubble frame handle will be declared as a static. If this variable contains a non-zero value, a bubble exists; if it contains a NULL value, a bubble doesn't exist.

Next, if we want to create a bubble, the pointer has to be over a button and not elsewhere over the menu. To check this we consult the sCurrentitem variable (see previous section). If this contains a non-zero value, the pointer is above a button.

So, if the pointer is above a button and there is no bubble active a bubble must be created. if (!hwndBubble && sCurrentitem) Figure 16: Do we have to create a bubble?

We will take a look at how we make sure the right bubble will be displayed in a while (destroying the bubble), but let us first take a look how to fill the bubble.

What's In A Bubble
With the code discussed so far, we're able to display a bubble. However, there's nothing in the bubble yet. In this section we'll discuss a way to put text in the bubble and to set the desired fonts and colours for the bubble.

The bubble is designed to show a short piece of text for every button in the toolbar. In order to do that we have to store a short line of text for every bubble somewhere in our application. Most suitable is the STRINGTABLE resource. We can attach a short string to every button identifier. For the buttons in our example application, the stringtable would look something like this: Figure 17: STRINGTABLE

What we have to do now is load the appropriate string if a bubble is created. Because the strings have the same ID as the buttons, it's very easy to determine which on we need, the ID for the current button is contained in the sCurrentitem variable. A string can be extracted from a STRINGTABLE with the WinLoadString function. The syntax for this function looks like this: Figure 18: Load a resource string

If we create a small buffer (32 characters should be enough), we can load the appropriate string. This string can be displayed in the client of button with a call to WinSetWindowText. The following piece of code takes care of this: Figure 19: Filling the bubble with text

If we leave it this way, then we have some problems. Because the bubble area has no font attributes set, the help text will be displayed using the default system font (10 point, "System Proportional"). This font is probably too large for the bubble previously defined; also, it doesn't look attractive. So we should change the font. The easiest way to do this is set a presentation parameter for the bubble client. If we want to change a font using presentation parameters we have to use the PP_FONTNAMESIZE presentation parameter. (For more on presentation parameters see Building a Editor, Part 3 in EDM/2 issue 3-9.) The following line of code will set the font used for the bubble to 8 point, "Helv". WinSetPresParam(hwndBubbleClient, PP_FONTNAMESIZE, 7, "8.Helv"); Figure 20: Setting the bubble font

While we're at it, we could also change the colour of the bubble-client by using the WinSetPresParam function. If we don't, the bubble will show the standard window background colour (again, rather dull). Changing the background colour of a static window through presentation parameters can be done in two ways. We can use an indexed (solid) colour, or we can use an RGB (composite) colour. I prefer the last one because it has much more possibilities.

If we're going to use RGB colours, the following include should be added to our program.
 * 1) define INCL_GPIBITMAPS

It's in that part that the RGB structure is defined. What is an RGB structure? It is, in fact, just three different byte values, the first one is for red; the second one is for green and the last one is for blue. Each value may vary between 0 and 255. This way we can create the colour mix we want. The following code makes sure the bubble is shown in a sort of turquoise. (No, we won't discuss good taste here. [Grin] With a little effort the colour can be made user-configurable.) RGB rgb = {200,200,0}; WinSetPresParam(hwndBubbleClient, PP_BACKGROUNDCOLOR, sizeof(RGB),&rgb); Figure 21: Set bubble colour

Destroying The Bubble
Besides displaying the bubble we have to remove it when the current bubble isn't valid any more. There are several situations in which this will be the case. The first item can be tackled very quickly. If the pointer resides over the menu, but not over a button, the sCurrentitem value will contain a zero value. Before we'll take action on this let's examine the next item. (In fact the next item is the same as what we're trying to cover here.)
 * If the pointer resides over a menu, but not over a button.
 * If the pointer resides above another button then for what is displayed in the bubble.
 * If the pointer leaves the menu-area.

To check if the pointer resides over another button then where a bubble is created for we have to remember the sCurrentitem when a button is created. To remember a value for the next time the window procedure is called we have to declare it as a static (just like the bubble window handle) at the beginning of the window procedure. static short sLastitem; Figure 22: Remember the last item

This sLastitem variable gets its value when the bubble is built. At the moment we know for what button the bubble must be displayed we fill the sLastitem value.

To check if the pointer changed position to another place on the menu bar, we'll compare the sCurrentitem with the sLastitem value. This also takes care of item one, the pointer does not reside over a button. If the sCurrentitem becomes 0 because the pointer leaves a button, this value will be different from the sLastitem value.

Do not forget that there has to be a bubble to destroy. This can be checked by looking at the hwndBubble value. Once the bubble is destroyed, we must reset it to 0.

For now we know enough to destroy the bubble during a WM_MOUSEMOVE if the pointer isn't above the right button. The following code will take care of business: Figure 23: Kill the bubble

This code has to be placed before the code in the last section, so that the bubble will be destroyed before a new one is created in another location!

Right now we can handle the destruction of bubbles if the pointer moves to another button or the pointer is over a buttonless area of the menu window. However, with this code we can't destroy a bubble if the pointer leaves the menu- area. To take care of this we have to use a little trick.

There isn't a message that is sent when the pointer leaves a frame window control. So we can't use that. Besides that if the pointer leaves the menu window, no more WM_MOUSEMOVE messages are sent to this window. We have to design a method that checks if the pointer is over the menu bar as long as a bubble is displayed. To accomplish this we'll start a timer at the moment a bubble is created. In the WM_TIMER message, sent by the timer to the menu window, we will check if the mouse pointer still resides over the menu bar. If this isn't the case, the bubble as well as the timer will be destroyed. This also means the timer has to be killed when a bubble is killed.

The timer has to be started when a bubble is created. The following line of code takes care of that and makes sure the WM_TIMER message is sent to the menu window. WinStartTimer(hab, hwnd, 1, 50); Figure 24: Start a timer

In this case the timer sends a WM_TIMER every 20th second (50/1000) to the menu window. The timer ID is 1. If you're using this example in an applet that has already an active timer you may want to alter this value.

How do we know the pointer is still over the menu or toolbar area? Well, we first have to query the size and place of the menu bar and the position of the mouse pointer. For querying the mouse pointer we'll use the WinQueryPointerPos function. This function takes the address of a POINTL structure and the handle of the desktop window and returns the coordinates of the mouse pointer relative to the desktop. (This is a pity, having the coordinates relative to the menu bar would avoid some code.) Figure 25: Query the mouse pointer

Next we'll query the menu bar size and place by using the WinQueryWindowPos function. This function takes a window handle and an the pointer to an SWP structure. (For more information on SWP structures see Building Custom Controls in EDM/2 3-4.) On return, the SWP structure will contain the size/place information. SWP  swpMenu; WinQueryWindowPos(hwnd, &swpMenu); Figure 26: Query the menu window size and place

It would be nice if we could just compare the values of the pointer position with the values in the swpMenu structure. Well, we can't! The problem is that the pointer position coordinate returned by WinQueryPointerPos is relative to the desktop and the values in the swpMenu structure are relative to the frame window of which the menu bar is a control. So before a comparison can be made, we have to convert the pointer postion to the menu size/place information.

This can be done by querying the frame position and doing some arithmetic on the returned SWP information. There is an easier way, though. We will use the WinMapWindowPoints function (as we did in placing the bubble) to map the pointer position relative to the frame window (not relative to the menu bar!) WinMapWindowPoints(HWND_DESKTOP, hwndFrame, &ptl, 1); Figure 27: Remap coordinates

We know the pointer is outside the menu bar if one of the following conditions is met: If one of these conditions is met, the bubble has to be destroyed and the timer has to be stopped. Remember, if the bubble is destroyed, it isn't enough to simply destroy the bubble. The code that sets up new bubbles uses the hwndBubble variable to check if there is a valid bubble. So if the bubble is destroyed, the (static) hwndBubble variable should be set to 0.
 * swpMenu.x > ptl.x
 * swpMenu.x + swpMenu.cx < ptl.x
 * swpMenu.y > ptl.y
 * swpMenu.y + swpMenu.cy < ptl.y)

This about rounds it up. The complete code for WM_TIMER looks like this: Figure 28: Kill the bubble when it leaves the menu window

Putting It All Together
If you read all this, it seems like a lot of code, it isn't. Let's end any confusion and display the complete code for the WM_MOUSEMOVE message. (The WM_TIMER message is handled complete in the previous section.) Before we do this we have to discuss one last item that has to be handled - the ability to toggle the bubble on and off.

To do this we introduce a BOOL variable that will be declared as a static in the subclass so its value will be remembered every time the subclass is used. static BOOL bBubble = TRUE; // Bubble toggle Figure 29: The bubble toggle variable

Here we set it default to TRUE, so at the start of the bubble applet, bubble help will be on. It's this flag we have to check when processing the WM_MOUSEMOVE message. So the if-statement at the start of the WM_MOUSEMOVE message will change to: if (hwndFrame == WinQueryActiveWindow(HWND_DESKTOP) && bBubble) Figure 30: Check the toggle

Next we have to build something to toggle this switch. Of course, we can use a menu or a popup, but I like to take another approach once in a while. The bubble should toggle on a double click with mouse button 2 on the menu/tool bar. Beside the fact that this is a neat way to do things, it's very easy to implement. All that has to be done is capture the WM_BUTTON2DBLCLK message as it is sent to the menu bar and invert the value of bBubble. Figure 31: Toggling the bubble

And now as promised the complete handling of the WM_MOUSEMOVE message. Figure 32: The complete handling of WM_MOUSEMOVE

Concluding Notes
It should be fairly easy to convert this code so that the help is displayed in the title bar or in the status bar (it took me about 10 minutes to get it working). Also, it isn't a big hassle to work things around to a time delayed bubble. To accomplish this, some more work is needed. The key to this lies in starting another timer that will display the bubble after a certain time. If the pointer moves to a new button the timer should be restarted. Personally, I don't like time delayed buttons. Help is wanted or it's not, so adding the toggle as a double click on the menu bar is - in my opinion - a better solution.

The sample code for this article is build and compiled with the Borland OS/2 C++ 2.0 compiler. If you have problems recompiling the RC code with another compiler add the #include  statement to the RC file. The Borland compiler assumes it is there!

This article should shed some light on how I did the bubble in the SMALLED 0.96 application. (Although the article turned out larger then I intended!) Feel free to use this code anyway you like. Of course I'm open to suggestions, kudo's, criticism, bug-reports and postcards (keep `em coming).