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.
SendMessage
CreateWindow
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:
ToolTip
TTM_GETTEXT
null
string
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.
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.
SafeHandle
IntPtr
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.
NM_CUSTOMDRAW
WM_NOTIFY
TTN_SHOW
TTN_POP
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.
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.
GetValue
SetValue
try
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.
BitBlt
Graphics DrawImage
There are seven style options in this version:
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(); }
PathGradient
BlendTrianglar
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(); }
BitBblt
StretchBlt
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); } }
ForwardDiagonal
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(); }
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:
PropertyDescriptorCollection
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; } } } }
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
This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)
General News Suggestion Question Bug Answer Joke Rant Admin
Old Japanese Man Creates Amazing Art Using Excel (Wait, Excel?)