Click here to Skip to main content
Click here to Skip to main content

VS Style Tooltips and A Whole Lot More...

, 6 Dec 2008 CPOL
Rate this:
Please Sign up or sign in to vote.
A tooltip replacement class

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:

  • Default: Draws a tooltip using the system defined methods and styles.

  • Solid: Paints a solid back color on the tip. This method uses a solid brush to draw the background:
    private void tipDrawSolid(Rectangle rDmn, IntPtr hdc)
    {
        Graphics g = Graphics.FromHdc(hdc);
        float o = _fOpacity * 255;
        Brush hB = new SolidBrush(Color.FromArgb((int)o, _oBackColor));
        g.FillRectangle(hB, rDmn);
        hB.Dispose();
        g.Dispose();
    }

  • Gradient: Gradient background with eight predefined gradients. The gradient shown was created with a PathGradient brush using the BlendTrianglar style.
    private void drawPathGradient(Rectangle rDmn, IntPtr hdc)
    {
        Graphics g = Graphics.FromHdc(hdc);
        GraphicsPath gP = new GraphicsPath();
        gP.AddRectangle(rDmn);
        PathGradientBrush pGp = new PathGradientBrush(gP);
        float o = _fOpacity * 255;
        Color c1 = Color.FromArgb((int)o, _oGradientStartColor);
        Color c2 = Color.FromArgb((int)o, _oGradientEndColor);
    
        switch (_eGradientStyle)
        {
            case GradientStyle.BlendTriangular:
                pGp.CenterPoint = new PointF(rDmn.Width / 2, rDmn.Height / 2);
                pGp.CenterColor = c2;
                pGp.SurroundColors = new Color[] { c1 };
                g.FillPath(pGp, gP);
                break;
    
            case GradientStyle.FloatingBoxed:
                pGp.FocusScales = new PointF(0f, 0f);
                pGp.CenterColor = c2;
                pGp.SurroundColors = new Color[] { c1 };
                Blend bP = new Blend();
                bP.Positions = new float[] { 0f, .2f, .4f, .6f, .8f, 1f };
                bP.Factors = new float[] { .2f, .5f, .2f, .5f, .2f, .5f };
                pGp.Blend = bP;
                g.FillPath(pGp, gP);
                break;
        }
    
        pGp.Dispose();
        gP.Dispose();
        g.Dispose();
    }

  • Graphical: A bitmap is used as the tip background. This method stores the bitmap image in a temporary DC, then using BitBblt for the edges and StretchBlt for the center, it alpha-blends to the window:
    private void drawGraphic(Rectangle rDmn, IntPtr hdc, 
            string caption, string title, IntPtr parent)
    {
        RECT tR = new RECT();
        GetWindowRect(_hTipWnd, ref tR);
    
        // blit the capture, simulating transparency 
        BitBlt(hdc, 0, 0, rDmn.Width, rDmn.Height, _cBgDc.Hdc, 0, 0, 0xCC0020);
    
        if (_bmGraphic != null)
        {
            cStoreDc cImage = new cStoreDc();
            cStoreDc cDraw = new cStoreDc();
            cImage.Height = _bmGraphic.Height;
            cImage.Width = _bmGraphic.Width;
            cDraw.Height = rDmn.Height;
            cDraw.Width = rDmn.Width;
            IntPtr hOld = SelectObject(cImage.Hdc, _bmGraphic.GetHbitmap());
    
            // left side
            StretchBlt(cDraw.Hdc, 0, 3, 3, (rDmn.Height - 6), cImage.Hdc, 
                       0, 3, 3, (cImage.Height - 6), 0xCC0020);
            // right side
            StretchBlt(cDraw.Hdc, (rDmn.Width - 3), 3, 3, 
    		(rDmn.Height - 6), cImage.Hdc, 
                      (cImage.Width - 3), 3, 3, (cImage.Height - 6), 0xCC0020);
            // top left corner
            StretchBlt(cDraw.Hdc, 0, 0, 3, 3, cImage.Hdc, 0, 0, 3, 3, 0xCC0020);
            // top
            StretchBlt(cDraw.Hdc, 3, 0, (rDmn.Width - 3), 3, cImage.Hdc, 3, 
                       0, (cImage.Width - 3), 3, 0xCC0020);
            // bottom
            StretchBlt(cDraw.Hdc, 3, (rDmn.Height - 3), (rDmn.Width - 3), 3, 
                       cImage.Hdc, 3, (cImage.Height - 3), 
                      (cImage.Width - 3), 3, 0xCC0020);
            // bottom left corner
            StretchBlt(cDraw.Hdc, 0, (rDmn.Height - 3), 3, 3, cImage.Hdc, 0, 
                      (cImage.Height - 3), 3, 3, 0xCC0020);
            // center
            StretchBlt(cDraw.Hdc, 3, 3, (rDmn.Width - 6), (rDmn.Height - 6), 
                       cImage.Hdc, 3, 3, (cImage.Width - 6), 
                      (cImage.Height - 6), 0xCC0020);
            // draw to buffer
            byte bt = (byte)(_fOpacity * 255);
            alphaBlit(hdc, 0, 0, rDmn.Width, rDmn.Height, cDraw.Hdc, 0, 0, 
                      rDmn.Width, rDmn.Height, bt);
            SelectObject(cImage.Hdc, hOld);
        }
    }

  • Mirror: A predefined style with a mirror effect. This is a bit more involved, using a combination of gradient styles to simulate a beveled edge, then drawing the center with a linear gradient using the ForwardDiagonal mode:
    private void drawMirror(ref Rectangle rDmn, IntPtr hdc, 
                 string caption, string title, IntPtr parent)
    {
        RECT tR = new RECT();
        GetWindowRect(_hTipWnd, ref tR);
    
        // blit the capture, simulating transparency
        BitBlt(hdc, 0, 0, rDmn.Width, rDmn.Height, 
               _cBgDc.Hdc, 0, 0, 0xCC0020);
        Graphics g = Graphics.FromHdc(hdc);
    
        // draw the frame
        Color c1 = Color.Silver;
        Color c2 = Color.SteelBlue;
        Pen p1 = new Pen(c1, .9f);
        Pen p2 = new Pen(c2, .9f);
    
        g.DrawLines(p1, new Point[] {
            new Point (0, rDmn.Height - 1), 
            new Point (0, 0), 
            new Point (rDmn.Width - 1, 0)
        });
    
        p1 = new Pen(c2, .1f);
    
        g.DrawLines(p2, new Point[] {
            new Point (0, rDmn.Height - 1), 
            new Point (rDmn.Width - 1, rDmn.Height - 1), 
            new Point (rDmn.Width - 1, 0)
        });
    
        p1.Dispose();
        p2.Dispose();
        // draw bevel
        rDmn.Inflate(-2, -2);
        rDmn.Offset(1, 1);
        float fO = _fOpacity * 255;
        c1 = Color.FromArgb((int)fO, Color.Snow);
        c2 = Color.FromArgb((int)fO, Color.Silver);
        // left
        Rectangle rBv = new Rectangle(1, 1, 4, rDmn.Height);
        LinearGradientBrush hB = new LinearGradientBrush(
            rDmn,
            c1,
            c2,
            LinearGradientMode.Horizontal);
        g.FillRectangle(hB, rBv);
        // bottom
        rBv = new Rectangle(1, rDmn.Height - 1, rDmn.Width, 4);
        hB = new LinearGradientBrush(
            rDmn,
            c1,
            c2,
            LinearGradientMode.Vertical);
        g.FillRectangle(hB, rBv);
    
        // right
        rBv = new Rectangle(rDmn.Width, 2, 4, rDmn.Height + 1);
        hB = new LinearGradientBrush(
            rDmn,
            c1,
            c2,
            LinearGradientMode.Horizontal);
        g.FillRectangle(hB, rBv);
        // top
        rBv = new Rectangle(1, 1, rDmn.Width, 4);
        hB = new LinearGradientBrush(
            rDmn,
            c1,
            c2,
            LinearGradientMode.Vertical);
        g.FillRectangle(hB, rBv);
    
        // fill
        hB = new LinearGradientBrush(
            rDmn,
            c1,
            c2,
            LinearGradientMode.ForwardDiagonal);
        rDmn.Inflate(1, 1);
        rDmn.Offset(-1, -1);
        hB.SetSigmaBellShape(1f, .5f);
        g.FillRectangle(hB, rDmn);
    
        hB.Dispose();
        g.Dispose();
    }

  • Glass: A predefined glass style effect. Almost a paste of the above, just with different colors and parameters.
  • OwnerDrawn: Drawing is up to the owner, handled through the draw event interface.

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

  • 2nd December, 2008: Initial post
  • 5th December, 2008: Updated source code

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

John Underhill
Network Administrator vtdev.com
Canada Canada
Network and programming specialist. Started in C, and have learned about 14 languages since then. Cisco programmer, and lately writing a lot of C# and WPF code, (learning Java too). If I can dream it up, I can probably put it to code. My software company, (VTDev), is on the verge of releasing a couple of very cool things.. keep you posted.

Comments and Discussions

 
GeneralMy vote of 5 Pinmembermajid torfi2-Nov-14 4:01 
GeneralUsing your tooltip control as a baloon tip PinmemberDude from backyard26-Apr-10 21:29 
GeneralDispose() doesn't release all GDI objects Pinmemberbugmenot123424-Aug-09 8:08 
GeneralNice done !! PinmemberMember 79085413-Jul-09 5:42 
GeneralRe: Nice done !! PinmemberSteppenwolfe25-Jul-09 6:56 
GeneralRe: Nice done !! PinmemberMember 79085426-Jul-09 11:30 
GeneralRe: Nice done !! PinmemberSteppenwolfe26-Jul-09 13:00 
GeneralRe: Nice done !! PinmemberMember 79085426-Jul-09 22:31 
GeneralRe: Nice done !! PinmemberSteppenwolfe27-Jul-09 4:03 
This would require serious modification to code, multiple text and forecolor properties.. good luck
GeneralRe: Nice done !! PinmemberMember 79085427-Jul-09 4:10 
GeneralFix when Converting project to 2008 PinmemberSteppenwolfe25-May-09 4:54 
GeneralRe: Fix when Converting project to 2008 PinmemberDaniel Stutzbach4-Sep-09 5:24 
GeneralProblem with Right Mouse Up event Pinmembergpgemini5-Apr-09 10:14 
QuestionHow to use this from VB.NET PinmemberMisterT9925-Mar-09 5:26 
AnswerRe: How to use this from VB.NET PinmemberSteppenwolfe28-Mar-09 3:17 
Questionare you from planet-source-code? PinmemberUnruled Boy12-Jan-09 4:00 
AnswerRe: are you from planet-source-code? PinmemberSteppenwolfe12-Jan-09 15:53 
GeneralRe: are you from planet-source-code? PinmemberUnruled Boy12-Jan-09 15:56 
GeneralCool PinmemberDr.Luiji9-Jan-09 11:20 
GeneralRe: Cool PinmemberMoak2-Sep-09 14:36 
Generalauto show PinmemberFrank Wagner15-Dec-08 1:19 
GeneralRe: auto show [modified] PinmemberSteppenwolfe15-Dec-08 10:19 
GeneralProblems PinmemberDmitri Nesteruk10-Dec-08 0:24 
GeneralRe: Problems PinmemberSteppenwolfe10-Dec-08 3:38 
GeneralRe: Problems PinmemberDmitri Nesteruk10-Dec-08 4:08 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Terms of Use | Mobile
Web04 | 2.8.1411019.1 | Last Updated 6 Dec 2008
Article Copyright 2008 by John Underhill
Everything else Copyright © CodeProject, 1999-2014
Layout: fixed | fluid