|By Rik Brooks||
|January 1, 2000 12:00 AM EST||
A tool tip, which appears in several modern applications, is a small text area that floats over an object when the cursor comes to rest on it. Microsoft Word has it - in fact, PowerBuilder itself incorporates tool tips. Rest your cursor on any item in the toolbar and a moment later a tip will appear. In Figure 1, which shows this in action, the cursor was placed over the Library menu button, and after a moment the word Library appeared (in black letters on a yellow background).
A couple of months ago in PBDJ (Vol. 6, issue 11) I showed you how to implement an automatic microhelp in your ancestor DataWindow object. This month we're going to build on that theme, making your ancestor DataWindow even more powerful. We're going to add these tool tips, and in the process we'll use some of the code we've already used. I'll provide that code again in case you missed the previous article.
Clearly, we need several pieces to implement this. We need a window on which to put our text, and we need text on that window. The window needs to be smart enough to know the size of the text and resize itself so it won't be too large and leave unattractive spaces at the end for short words. We'll just make the text very long from the start so it can accommodate long phrases. That way we don't have to worry about expanding the width of both the window and the text. We also need a way of knowing when to open that window and when to close it. And we need to know how to position the window when we do open it.
There's a lot to cover here so let's get right to it. The first thing is a riddle that's plagued PowerBuilder programmers for years. How do we know the actual size of a text object?
A quick search of the help files tells you that a pbu (PowerBuilder Unit) is approximately 1/32 the space of a single character. This won't work for us, though. First of all, we don't know what font the user will be using - a "small font" or "Wide Latin." So how can such a simplistic formula work? It can't. For this we need to turn to the Windows API.
We know we're going to modify the size of a text object. Where do we put that code? Remember, we always try to put any code as close as possible to the object on which it'll operate. That would mean in a text object. If you don't have an ancestor text object, create one now. I certainly have one in my libraries. In fact, I have one object inherited from each of the standard PowerBuilder objects just for this occasion. In case you don't, here's how you go about it.
First click on the New button, then go to the Object tab page. Double-click on the "Standard Visual" icon shown in Figure 2. This will give you a dialog with a listbox. Scroll down near the bottom, find and select StaticText - now you're in a user object painter for a static text.
Now we need to call a couple of API functions. One of them needs a structure as an argument. The structure has to have two long variables, one for the height of the text and the other for the width. Create a structure. I named mine struct_size. The two members of the structure I named width and height, respectively.
The next step is to declare two local external functions, as follows:
->Function boolean GetTextExtentPoint32A(ulong, string, long, REF struct_size) Library "GDI32" ->function ulong GetDC( ulong hWnd) LIBRARY "USER32"
Note that in the declaration of GetTextExtentPoint32A, our structure is the last argument. Not only that, it's a REF - a reference. The API function is going to load that for us. Actually, this is the function we call to get the "real" length of the text. It'll return both the width and the height of the text in our object. We don't need the height, of course, but the width will be important to us.
We now have the tools to find out how big the text is in our object, and we also know all we need to know to resize the object so that it's just big enough to hold our text (no matter how big). The problem is that we don't always want to resize the text object. We need a flag to turn this on and off.
Declare an instance variable in your staticText object. It should look like the following line:
Public boolean ib_autoResize = FALSE
Okay, now we create a new function. Normally I prefix my functions with an of_ like everyone else. In this case I'll make an exception. I want a function that tells the object to resize itself. Since a resize function already exists, I want to use polymorphism and call mine resize as well. The difference is that mine doesn't need any arguments. It gets them from the text itself. This means that the definition won't clash with the current definition of the standard resize function. It also means that the programmer never needs to wonder if he or she wants to call resize or of_resize. Just call resize and let PowerBuilder figure out which one is appropriate.
Declare your function - its name is resize. It returns a long (the new width) and takes no arguments. The code for this function can be found in Listing 1.
We now have a StaticText that's just a little more intelligent than it was before. It knows how to resize itself based on the length of the text. Will this work? Let's check it out.
I created a window that has our new object. (The sample application with this window can be downloaded from www.PowerBuilderJournal.com.) It also has a single line edit where I can change the text in the new object. Finally, the window has a checkbox so I can turn the Auto Resize on and off for the fun of it. You can see that window at work in Figure 3.
The code in this window is simple. In the open event of the window I turn on the ib_autoResize for the StaticText. Maybe I should point out something that's probably obvious. If you implement new functionality in an object, you most likely won't want it to be default. To implement new functionality and make it default is to court confusion from other programmers who might be using your object. Can't you just hear them? "Why does my static text keep changing sizes?"
In the open event you have the following line:
st_tip.ib_autoResize = TRUE
You want to allow the user to turn it on and off from the checkbox, and the code for that is in the clicked event. Again, it's only one line:
st_tip.ib_autoResize = this.checked
So far, not too bad. Finally, we have to move to the event in which all of the code for this window lives - the modified event of the single line edit. The code for that follows:
st_tip.text = text
Okay, that window is done. We have a StaticText object that can resize itself. Now what? The next step is to create a special window that holds this object and responds to it. This is going to be a little more intense.
First, create a window. It should be a popup type with no title bar, Min Box, Max Box or Control Menu. In fact, we want the thing to be invisible. Remember, we want it to look like the static text is floating by itself.
Put the object we just created onto your window. By default it'll display the word "none," which you'll want to delete so it doesn't show anything at all. Then set the background color to yellow and the foreground to black.
You don't need much code in this window. Listing 2 shows the code you'll need in the open event. Clearly, from looking at this code, you should open the window with a parm. The call to resize is in this event. We use the return value of the call to resize, that is, to set the width of the window to equal the size of the text.
The only other code, in the MouseMove event, just closes the window.
The window is done. You can close and save it.
The next thing is to open your ancestor DataWindow control. I'm sure if you've been following my articles you already have one of these. If you don't, you have to create your own. Use the same basic method you used when creating your StaticText object.
We've finally come to the crux of the article. We have the tools and are ready to implement our tool tip DataWindow. This is where we get to some substantial code.
Before we get to that code, though, you'll need a few instance variables. Declare the following instance variables for your DataWindow control:
int ii_xAnchor, ii_yAnchor
int ii_mouseMoveDelta = 100
What we want to do is capture the movements of the mouse. Once it's moved a certain distance, we want to check the object under the mouse and see if it needs a tool tip. If it does, we want to open the tool tip window. The code for this is in Listing 3. I'd like to point out that this code calls another function that we wrote about in November in the "Implementing Automatic Microhelp" article. You'll find the code for that function in Listing 4 in case you missed it the first time around.
We do need to discuss Listing 3 just a little, though. It's pretty well commented, but still, let's consider it.
Take a look at the first block of code. I've handled it in a rather cumbersome fashion. In the last few years I've been moving more and more to a convention in which I do all the validity checks I can at the start of the function; once those are done, I proceed with the code. In the old, structured way of doing things I'd exit the function (return) at the end only. Thus the entire body of my function was often within an IF construct, something like this:
IF validation_conditionA and validation_conditionB then
// do your function
// handle validation errors
That doesn't seem too bad, but when I have to handle the validation errors, I often find myself repeating the original IF construct, similar to the code below:
IF validation_conditionA and validation_conditionB then
// do your function
// handle validation errors
if validation_conditionA then
// Notify user of condition A
if validation_conditionB then
// Notify user of condition B
This became cumbersome and difficult to maintain. The validations would be checked and then responses to them delayed until the end of the code. I've found what I think is a better way. I handle all the validations up front. If any of them fail, I return. Once these are over I can fall cleanly through to the code. The first block of code in this event follows that format, but I had to make the check for that validation a little odd in order to do it.
The idea of this first block is that if the mouse hasn't moved more than a given delta from its previous position, we should just return from the event. That's why you see the NOT.
What's left? We have to create a DataWindow object to test. I created one from an external data source just to keep things simple. Then we need to set the tag attribute for it and make sure this attribute has a tool tip token. Finally, put it into a window and retrieve it (or insert a row if it's external like mine).
Figure 4 shows where to set the tool tip and Figure 5 shows my window at work.
One final note: if you do download the code, you'll find a lot of objects that aren't referenced in this article. Some are from previous articles, some aren't. They're all required to make the application run though. For example, my ancestor DataWindow won't compile without the custom services I created for them. The ones you don't recognize come from my DojoTools (version 7) libraries where I keep reusable code I've created over the years.
- Where Are RIA Technologies Headed in 2008?
- PowerBuilder History - How Did It Evolve?
- Creation and Consumption of Web Services with PowerBuilder
- Cloud People: A Who's Who of Cloud Computing
- DDDW Tips and Tricks
- Working with SOA & Web Services in PowerBuilder
- Cloud Expo 2011 East To Attract 10,000 Delegates and 200 Exhibitors
- Dynamically Creating DataWindow Objects
- OLE - Extending the Capabilities of PowerBuilder
- DataWindow.NET How To: Data Entry Form