Click here to Skip to main content
Email Password   helpLost your password?

Rave Against the Machine

Why write a tooltip class, when there is a perfectly good class built in to .NET? When I first looked at the tip class, my initial impression was, this will make things so much easier! So, I hooked up the draw event handler to custom draw my tips, then typed e. to look through the parameters, but wait a sec.. where's the handle property? I took a closer look at the tip properties and.. seems like they forgot a couple of little things when they wrote the class. Little stuff, that we will never need, like umm.. the font, balloon style, position, dozens of other properties, and of course, the handle (they don't want you using that nasty SendMessage, oh no!). I could have extracted the handle from the attributes and created a wrapper class, but better to use CreateWindow and do it from scratch, I think.

This leads me to the obvious question.. why did the designers cripple the tooltip class like this? I can only speculate, but I think it has something to do with the 'Redmond patch-it methodology' i.e., why fix it, when you can patch it instead?

The ToolTip class has not changed a whole lot since Win98, and throughout has had several security problems that were never addressed, most notably, some potential buffer vulnerabilities. Essentially, you cannot get the size of the caption string before requesting it with TTM_GETTEXT, so you cannot size the return buffer accordingly, but instead, are stuck with an 80 char limit on text. The fix for this would have been ridiculously simple, pass null for the string, and SendMessage returns the size of the buffer (3 lines of code?). Instead, they blitz us with KB alerts, and cripple the ToolTip class. Again, just a guess, but I kind of envision the framework designers as hedge hogging over their cubicles, doing a star trek shtick:

Jim: Bones, there's.. a security problem.. with the ToolTips.. they have to be.. 'contained'.

McCoy: Dammit Jim, I'm a programmer, not a miracle worker!

Jim: snort

McCoy: snort, snort..

Whatever the grim reality of it might be, here we are, I need nice tooltips, and the built in jobbies simply won't do.

Begin at the Begin'

The first thing to be done is create the tooltip window and pass the needed style flags. I inherit the native window class for the subclassing.

public Tooltip()
{
    // initialize class
    tagINITCOMMONCONTROLSEX tg = 
      new tagINITCOMMONCONTROLSEX(ICC_TAB_CLASSES);
    InitCommonControlsEx(ref tg);

    // get app instance
    Type t = typeof(Tooltip);
    Module m = t.Module;
    _hInstance = Marshal.GetHINSTANCE(m);

    // create window
    _hTipWnd = CreateWindowEx(WS_EX_TOPMOST | WS_EX_TOOLWINDOW,
        TOOLTIPS_CLASS, "",
        WS_POPUP | TTS_NOPREFIX | TTS_ALWAYSTIP,
        0, 0,
        0, 0,
        IntPtr.Zero, 
        IntPtr.Zero, _hInstance, IntPtr.Zero);

    // set position
    SetWindowPos(_hTipWnd, HWND_TOP,
        0, 0, 
        0, 0,
        SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE | SWP_NOOWNERZORDER);

    SendMessage(_hTipWnd, WM_SETFONT, _oTipFont.ToHfont(), 0);
    windowStyle(_hTipWnd, GWL_STYLE, 0, WS_BORDER);
    useUnicode(IsUnicode);
    base.AssignHandle(_hTipWnd);
}

A couple of things to note here: I remove the border style from the tip, which otherwise would paint an ugly black border in XP, test for Unicode, then assign the handle to the class. In my own implementation, I'll use SafeHandle rather than IntPtr for the window handle (why didn't they just fix the garbage collector?), but in an attempt to keep this available to pre 2.0 versions of C#, I am using IntPtr here.

The next thing to do is create the various window style and SendMessage macros. To change the window style, I use a method:

private void windowStyle(IntPtr handle, int type, int style, int stylenot)
{
    int nStyle = GetWindowLong(handle, type);
    nStyle = ((nStyle & ~stylenot) | style);
    SetWindowLong(handle, type, nStyle);

    SetWindowPos(handle,
        HWND_TOP,
        0, 0, 
        0, 0,
        (SWP_NOMOVE | SWP_NOSIZE | SWP_NOOWNERZORDER | 
         SWP_NOZORDER | SWP_FRAMECHANGED | SWP_NOACTIVATE));
}

To change the class attributes and implement methods, use a property:

/// <summary>
/// bool ToolTip.Active Gets/Sets a value indicating
/// whether the ToolTip is currently active.
/// </summary>

public bool Active
{
    get { return _bActive; }
    set { _bActive = value;
        SendMessage(_hTipWnd, TTM_ACTIVATE, value ? 1 : 0, 0); }
}

One of the first 'challenges' I faced at this point was with the custom draw message, there was none. No matter what style options I used, I just couldn't get the NM_CUSTOMDRAW message to show itself. What I did get, through WM_NOTIFY, was the TTN_SHOW and TTN_POP messages; the drawing though would have to be done through WM_PAINT.

The TTN_SHOW messages let us know that a tooltip is about to be shown. Three things are accomplished through this message, the window can be sized, positioned, and the background can be copied to a 'fake' transparency. Why not use the layered window style? I tried that, but in XP, you get a flash of a black empty DC as the tip first draws. My way draws the desktop background into a temporary DC, then blits it onto the window, the tips contents are then drawn over this with an adjustable opacity. One advantage to this is that the text and icon are drawn completely opaque, remaining readable, as the background opacity is faded.

Why use API? I haven't been programming with C# very long (a little over a month), but what I see is a lot of potential. What I have also noticed is the real reluctance of some C# developers to use APIs.

Framework vs. API

I remember, during my VB6 days, the amusement I felt when seeing a project posting that touted 'Pure VB, No API!'. The author was absolutely convinced of the superiority of inbuilt methods, and proud to have prevailed against that tedious old Windows APIs. In most cases, however, the author had still used APIs, only the calls were made through the runtime module, and by adding this layer of indirection, they had made a slower and less flexible software.

The .NET Framework is no different. The many classes and their methods still use APIs to do the heavy lifting, only they shield you from the complexities of the call setup, offering the convenience of simplified and regimented methods. I say convenience, because that is exactly what this is, and as with many forms of convenience, there is a tradeoff involved. I recently wrote a Registry class in C#. One of the first things I did was to compare the execution times between the API (advapi.dll) and the framework GetValue/SetValue methods. The API calls consistently merited a 2:1 speed advantage over the embedded methods. This is not really surprising as it goes to the 'golden rule of programming'.. A stack trace revealed that the embedded method was itself calling advapi.dll, only after a number of tasks were performed. What is interesting though is the actual layers involved between the initial call and the execution in the kernel, because the calls through advapi.dll are themselves simplified methods. Let me explain, this is sometimes referred to as rings, or layers of indirection between a call to a task and its actual execution. You are a careful programmer, so.. you test your variables, put your SetValue call in a try block, and execute the call. The call into the framework method is then examined: the security token is checked for the level of access required, the call parameters are tested, types are converted, then the call is forwarded to -> advapi.dll. In advapi, the call parameters are tested, the security token is checked, and types are converted, and a new call is then setup to ntdll.dll. If you are running in user mode (which, of course, you are), the call parameters and security token are tested again, before the call is finally forwarded to the kernel for execution. That is a lot of indirection! Each time you add a layer of indirection, it has a serious impact on execution time.

Now, I am sure that there is no shortage of diehard .NET enthusiasts waiting to tell me how wrong I am, and sell me on the many advantages of the 'building block' method of programming, and if you believe that, all the power to you. I believe though, that a marriage between the API and the framework is the best option. In situations where the framework offers simplified methods that would require a lot of tedious programming to achieve via API, I side with the framework (the gradients in this class as an example). In cases where the API method far outstrips the framework in speed and flexibility, choose the API. I have included a sample project that compares the BitBlt API with the Graphics DrawImage. On my box, BitBlt is 5 times faster (how do you argue with that?). It is also about context. If you are writing some super-duper spyware scanner that needs to access the Registry 10 thousand times during a scan, then you want as little indirection as possible, so you should consider writing a class that calls the nt_ API directly, or better yet a kernel mode driver that calls the zw_ API. If you are only setting a couple of application defaults as the program closes, the embedded methods should suffice, but if you ask me, you should always consider that golden rule when writing software: the fastest code is... no code.

On With the Show..

There are seven style options in this version:

I was wondering... how do they make those VS style tips? Careful what you wish for ~smirk~. Took some doing getting the look and feel, but the example should get you started.

The properties were extracted using the PropertyDescriptorCollection and parsing out valid entries:

private void createVals()
{
    int nC = 0;
    PropertyDescriptorCollection properties = TypeDescriptor.GetProperties(lb18);

    // get valid prop count
    for (int i = 0; i < properties.Count; i++)
    {
        if (properties[i].GetValue(lb18) != null)
        {
            if (properties[i].Name != null)
            {
                if (properties[i].GetValue(lb18).ToString() != null)
                {
                    nC += 1;
                }
            }
        }
    }

    // build the prop array
    _aVal = new string[nC, 2];

    nC = 0;

    for (int i = 0; i < properties.Count; i++)
    {
        if (properties[i].GetValue(lb18) != null)
        {
            if (properties[i].Name != String.Empty)
            {
                _aVal[nC, 0] = properties[i].Name;
                _aVal[nC, 1] = properties[i].GetValue(lb18).ToString();
                nC += 1;
            }
        }
    }
}

The Impossible Heaviness of Being (a Windows Developer)

These days, I am coding in Vista, then testing classes in XP. W2K is about as far back as I am willing to go, and whether an app works on a 10 year old Operating System no longer concerns me (do you think someone running Win98 actually buys software?). I was just about to publish this when my better judgment prevailed, and I decided to test it on XP first. The first thing I noticed was a black border around the tips. You don't see this in Vista, because the system is painting it with the Vista style. Simple enough, just remove the border style from the tip.

The next thing I noticed was that the tips were not fading. The UID that signals the fade timer (6) in Vista was absent in XP. After about an hour of trying to workaround this, I'm leaving it with no fader in XP (for now).

The next bug was that clickable tips were no longer working (because of the timer differences), so I added a new case for the timer (hover: 3) to handle the mouse over and prevent a premature closure of the window.

The last (and strangest) issue was that when the tip position was changed (but only when the tip was placed above the cursor), the window would turn gray. I take this to be that the OS sees the tip as out of focus and changes the backcolor. This was fixed by 'reminding' the tip of its backcolor when the style changes:

case WM_STYLECHANGED:
    if (_eCustomStyle == TipStyle.Default)
    SendMessage(_hTipWnd, TTM_SETTIPBKCOLOR, 
                ColorTranslator.ToWin32(Color.LightYellow), 0);
    base.WndProc(ref m);
    break;

Note that I did not use the system color 'Info' because that will also paint gray.

That's it for now (was just a member class for my grid control after all). Hope you all find this useful.

History

You must Sign In to use this message board.
 
 
Per page   
 FirstPrevNext
GeneralDispose() doesn't release all GDI objects
bugmenot1234
8:08 24 Aug '09  
I noticed that Dispose() doesn't release all GDI objects.

Try creating a new ToolTip in a loop, while also calling Dispose and GC.Collect() and you'll see that GDI objects are rising without being released.
I think it has something to do with windows messages processing used in the Dispose method.

Can anyone confirm this ?

Is there a fix for this issue ?
GeneralNice done !!
Member 790854
5:42 13 Jul '09  
I love this peace of code, wonderfull job, my complements !!

But there is always something extra I need Smile

Is there a possibility to make textlines in the tooltip box with different colors ?
(except the header, that is already possible)

Best Regards, Frank
GeneralRe: Nice done !!
Steppenwolfe
6:56 25 Jul '09  
Just add the properties..
GeneralRe: Nice done !!
Member 790854
11:30 26 Jul '09  
can you give a example ... Wink
GeneralRe: Nice done !!
Steppenwolfe
13:00 26 Jul '09  
There is a Forecolor property, is this what you mean?
GeneralRe: Nice done !!
Member 790854
22:31 26 Jul '09  
my mistake... I explain it a little more in detail...

I can set the color with ForeColor, like this:

_ttPicBox1.ForeColor = Color.White;

then the whole text line will be the same color. What i want to do is different lines
with different colors, example:

_ttPicBox1.SetToolTip(pictureBox1, "text of line 1, with color 1" + LINE +
"text of line2, with color 2"+ LINE + LINE +
"text of line 3 with color 3" + LINE +
"text of line 4 with color 4"
);

is there a possibility to do that ?

Thnx in advance

Regards, Frank
GeneralRe: Nice done !!
Steppenwolfe
4:03 27 Jul '09  
This would require serious modification to code, multiple text and forecolor properties.. good luck
GeneralRe: Nice done !!
Member 790854
4:10 27 Jul '09  
I was hoping that it was simple ....

but thnx for the nice code.

Regards, Frank
GeneralFix when Converting project to 2008
Steppenwolfe
4:54 25 May '09  
If you use the auto upgrade to convert this to the 2008 format, you will find it no longer works properly. This has nothing to do with the code, but rather a change to the tooltip control implementation. The WM_NOTIFY message in the wndproc uses the NMHDR struct to get notifications on tooltip state changes via the 'code' struct member, in 2008, the 'code' integer always returns 0. I have no idea why this is, or what internal alteration precipitated the change, but I do have a simple fix for the code.
In the WndProc: WM_NOTIFY | WM_REFLECT case, add the following code:

if (IsVisible())
nM.code = TTN_POP;
else
nM.code = TTN_SHOW;

So the beginning of the switch should now look like this:

case (WM_NOTIFY | WM_REFLECT):
NMHDR nM = new NMHDR(0);
RtlMoveMemory(ref nM, m.LParam, Marshal.SizeOf(nM));
if (nM.hwndFrom == _hTipWnd)
{
if (IsVisible())
nM.code = TTN_POP;
else
nM.code = TTN_SHOW;
switch (nM.code)
{
...
GeneralRe: Fix when Converting project to 2008
Daniel Stutzbach
5:24 4 Sep '09  
I converted to the 2008 format and did not run into this problem or need to apply your patch. Are you sure the conversion is what caused the problem you observed?
GeneralProblem with Right Mouse Up event
gpgemini
10:14 5 Apr '09  
A really nice control, I've began using it after the windows ToolTip failed me miserably (not too hard to achieve that).

But, I'm having a problem here - I've registered to the RightMouseUp event and:
A. You have a small bug where you actually raise the RightMouseDown event instead (found easily via a Null reference exception).
B. SOMETIMES, can't really figure out yet when - the DrawEventArgs have invalid information, i.e. the Caption and other fields hold junk instead of the info of the clicked ToolTip. I noticed you get this info via a SendMessage but couldn't trace the source of the error.

Any suggestions ?

Thanks,
GP.
GeneralHow to use this from VB.NET
MisterT99
5:26 25 Mar '09  
Fantastic article! I have been looking for something like this for a long time.

How can this be put in a DLL so it can be used from VB.NET?

Thank you
Tom
GeneralRe: How to use this from VB.NET
Steppenwolfe
3:17 28 Mar '09  
I really don't know much about vb.net.. but it could be translated, and that would be your best bet.
Generalare you from planet-source-code?
Unruled Boy
4:00 12 Jan '09  
are you the one from psc? Laugh

Regards,
unruledboy_at_gmail_dot_com
http://www.xnlab.com

GeneralRe: are you from planet-source-code?
Steppenwolfe
15:53 12 Jan '09  
That would be me.. (long time no see enmity) Big Grin
GeneralRe: are you from planet-source-code?
Unruled Boy
15:56 12 Jan '09  
I have been here for a long long time, finally got you hereRose codeproject is really a good place, enjoy yourself Laugh

Regards,
unruledboy_at_gmail_dot_com
http://www.xnlab.com

GeneralCool
Dr.Luiji
11:20 9 Jan '09  
I found the article very interesting and well written, have my 5!

Dr.Luiji
Trust and you'll be trusted.
Try iPhone UI [^] a new fresh face for your Windows Mobile, here on Code Project.

GeneralRe: Cool
Moak
14:36 2 Sep '09  
I second that! Interesting and well written, 5 points.


Generalauto show
Frank Wagner
1:19 15 Dec '08  
Hi,

first: thanks for this great control. I would like to to display the tooltip automatically from control load event handler. I tried Show method but it don't work. Is there any way to display control without hovering over bound control?

Regards,
Frank
GeneralRe: auto show [modified]
Steppenwolfe
10:19 15 Dec '08  
This is not what the 'show' command is for, (ex. would be: reshowing after a timeout, or manual mousemove trigger). Much of what is going on with timers and window itself, is being done inside (mfc)tooltip window class, not this hosting class, so mouse must be over control, and it must be visible in order to start timers. Not sure what you are trying to do, (intro window maybe?), but you could create a static window to do it.

modified on Tuesday, December 16, 2008 8:29 AM

GeneralProblems
Dmitri Nesteruk
0:24 10 Dec '08  
Hi, I'm trying to show a tooltip in a modal dialog window fired from a VS add-in, but the tooltips don't work. Also, on my machine (Vista Ultimate), some of the borders and shadows of the tooltip drift a pixel or two from the tooltip itself. Do you know how it can be fixed?
GeneralRe: Problems
Steppenwolfe
3:38 10 Dec '08  
Shadows are seperate windows drawn by the system not the tooltip or this class. I am also using Vista ultimate though, and it looks fine here. As for modal window from add in, are you passing in the handle of the dialog or the add in?
GeneralRe: Problems
Dmitri Nesteruk
4:08 10 Dec '08  
I am passing the handle of the target control to the tooltip, i.e., toolTip.SetToolTip(myButton.Handle, "text")
GeneralRe: Problems
Steppenwolfe
9:03 10 Dec '08  
I have no idea what control/code you are using so.. what I would do, is set a breakpoint within the wndproc and see what messages are coming through. If messages are absent, then window target message pump might be consuming necessary messages. Also, some windows can not be subclassed, they will return no messages, I can't remember if system dialog is one of them, check msdn.
General.NET vs. API
Ed Bouras
4:07 9 Dec '08  
You make a convincing argument for the case of API over .NET and I'm not experienced enough to argue effectively either way. However, given the success of .NET there must be some advantage that translates to an advantage for the developer (besides being lazy enough to not want to learn the API calls). Arguably that advantage comes when performance and speed are not a critical factor. There are probably tens of thousands of small, targeted business applications written in .NET that work perfectly for the task with no perceptible disadvantage by the user - perhaps the true litmus test. BTW: excellent article, very well composed -- a true 5.


Last Updated 6 Dec 2008 | Advertise | Privacy | Terms of Use | Copyright © CodeProject, 1999-2010