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()
{
tagINITCOMMONCONTROLSEX tg =
new tagINITCOMMONCONTROLSEX(ICC_TAB_CLASSES);
InitCommonControlsEx(ref tg);
Type t = typeof(Tooltip);
Module m = t.Module;
_hInstance = Marshal.GetHINSTANCE(m);
_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);
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:
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);
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());
StretchBlt(cDraw.Hdc, 0, 3, 3, (rDmn.Height - 6), cImage.Hdc,
0, 3, 3, (cImage.Height - 6), 0xCC0020);
StretchBlt(cDraw.Hdc, (rDmn.Width - 3), 3, 3,
(rDmn.Height - 6), cImage.Hdc,
(cImage.Width - 3), 3, 3, (cImage.Height - 6), 0xCC0020);
StretchBlt(cDraw.Hdc, 0, 0, 3, 3, cImage.Hdc, 0, 0, 3, 3, 0xCC0020);
StretchBlt(cDraw.Hdc, 3, 0, (rDmn.Width - 3), 3, cImage.Hdc, 3,
0, (cImage.Width - 3), 3, 0xCC0020);
StretchBlt(cDraw.Hdc, 3, (rDmn.Height - 3), (rDmn.Width - 3), 3,
cImage.Hdc, 3, (cImage.Height - 3),
(cImage.Width - 3), 3, 0xCC0020);
StretchBlt(cDraw.Hdc, 0, (rDmn.Height - 3), 3, 3, cImage.Hdc, 0,
(cImage.Height - 3), 3, 3, 0xCC0020);
StretchBlt(cDraw.Hdc, 3, 3, (rDmn.Width - 6), (rDmn.Height - 6),
cImage.Hdc, 3, 3, (cImage.Width - 6),
(cImage.Height - 6), 0xCC0020);
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);
BitBlt(hdc, 0, 0, rDmn.Width, rDmn.Height,
_cBgDc.Hdc, 0, 0, 0xCC0020);
Graphics g = Graphics.FromHdc(hdc);
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();
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);
Rectangle rBv = new Rectangle(1, 1, 4, rDmn.Height);
LinearGradientBrush hB = new LinearGradientBrush(
rDmn,
c1,
c2,
LinearGradientMode.Horizontal);
g.FillRectangle(hB, rBv);
rBv = new Rectangle(1, rDmn.Height - 1, rDmn.Width, 4);
hB = new LinearGradientBrush(
rDmn,
c1,
c2,
LinearGradientMode.Vertical);
g.FillRectangle(hB, rBv);
rBv = new Rectangle(rDmn.Width, 2, 4, rDmn.Height + 1);
hB = new LinearGradientBrush(
rDmn,
c1,
c2,
LinearGradientMode.Horizontal);
g.FillRectangle(hB, rBv);
rBv = new Rectangle(1, 1, rDmn.Width, 4);
hB = new LinearGradientBrush(
rDmn,
c1,
c2,
LinearGradientMode.Vertical);
g.FillRectangle(hB, rBv);
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);
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;
}
}
}
}
_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