VS 样式工具提示及更多...






4.99/5 (79投票s)
一个工具提示替换类

Rave Against the Machine
既然 .NET 中已经有了一个很好的类,为什么还要写一个工具提示类呢?当我第一次看到工具提示类时,我最初的印象是,这将使事情变得容易得多!于是,我连接了绘制事件处理程序来自定义绘制我的工具提示,然后输入e. 来查看参数,但等等……句柄属性在哪里?我仔细查看了工具提示的属性,发现……他们写这个类的时候似乎忘了几个小东西。一些我们永远不会用到的东西,比如……字体、气球风格、位置、几十个其他属性,当然,还有句柄(他们不想让你使用那个讨厌的SendMessage
,哦不!)。我可以从属性中提取句柄并创建一个包装类,但我想,最好还是使用CreateWindow
并从头开始做。
这让我提出了一个显而易见的问题……为什么设计者要把工具提示类搞成这样?我只能猜测,但我认为这与“雷德蒙德补丁方法”有关,也就是说,为什么要在可以打补丁的时候去修复它呢?
ToolTip
类自 Win98 以来变化不大,并且一直存在一些从未得到解决的安全问题,最值得注意的是一些潜在的缓冲区漏洞。本质上,你在请求字符串大小之前无法获取字符串大小,因为无法在请求时确定返回缓冲区的大小,但只能接受 80 个字符的文本限制。对此的修复会非常简单,将string
传递为null
,SendMessage
返回缓冲区的大小(3行代码?)。相反,他们用 KB 警报轰炸我们,并阉割了ToolTip
类。再说一遍,这只是一个猜测,但我有点设想框架设计者们在他们的隔间里像刺猬一样横冲直撞,做着星际迷航式的表演
Jim:Bones,工具提示……有一个安全问题……它们必须被……“控制”。
McCoy:该死,Jim,我是一名程序员,不是奇迹创造者!
Jim:哼哧
McCoy:哼哧,哼哧……
无论其严峻的现实是什么,我们现在身处其中,我需要漂亮的工具提示,而内置的工具提示根本不行。
从头开始
首先要做的是创建工具提示窗口并传递所需的样式标志。我继承了原生窗口类来进行子类化。
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);
}
这里有几点需要注意:我移除了工具提示的边框样式,否则在 XP 中会绘制一个丑陋的黑色边框,测试 Unicode,然后将句柄分配给类。在我自己的实现中,我将使用SafeHandle
而不是IntPtr
作为窗口句柄(他们为什么不修复垃圾收集器?),但为了使此功能在 C# 2.0 之前的版本中可用,我在这里使用了IntPtr
。
接下来要做的是创建各种窗口样式和SendMessage
宏。要更改窗口样式,我使用一个方法
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));
}
要更改类属性并实现方法,请使用属性
/// <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); }
}
我在这里遇到的第一个“挑战”之一是自定义绘制消息,根本没有。无论我使用什么样式选项,我都无法让NM_CUSTOMDRAW
消息显示出来。通过WM_NOTIFY
,我确实收到了TTN_SHOW
和TTN_POP
消息;但是绘制必须通过WM_PAINT
来完成。
TTN_SHOW
消息让我们知道一个工具提示即将显示。通过此消息可以完成三件事:可以调整窗口大小、定位,并且可以将背景复制到“假”透明度。为什么不使用分层窗口样式?我试过了,但在 XP 中,当工具提示首次绘制时,你会看到一个黑色的空 DC 闪烁。我的方法是将桌面背景绘制到一个临时 DC 中,然后将其“blit”到窗口上,然后将工具提示的内容绘制在这个背景之上,并具有可调的透明度。这样做的优点是文本和图标完全不透明,在背景透明度淡化时保持可读。
为什么使用 API?我使用 C# 编程的时间不长(刚过一个月),但我看到了很大的潜力。我还注意到一些 C# 开发人员非常不愿意使用 API。
框架 vs. API
我记得,在我使用 VB6 的时候,看到一个项目帖子吹嘘“纯 VB,无 API!”时,我感到很有趣。作者坚信内置方法优于一切,并自豪地战胜了那些乏味的旧 Windows API。但在大多数情况下,作者仍然使用了 API,只是调用是通过运行时模块进行的,并通过添加这个间接层,他们制造出了速度更慢、灵活性更差的软件。
.NET Framework 也不例外。许多类及其方法仍然使用 API 来完成繁重的工作,只是它们为你屏蔽了调用设置的复杂性,提供了简化的、有组织的方法的便利性。我说便利性,是因为这正是它所提供的,而且与许多便利形式一样,需要付出代价。我最近用 C# 写了一个注册表类。我做的第一件事就是比较 API(advapi.dll)和框架GetValue
/SetValue
方法的执行时间。API 调用始终比嵌入式方法快 2 倍。这并不奇怪,因为它遵循“编程黄金法则”……堆栈跟踪显示,嵌入式方法本身也在调用advapi.dll,只是在执行了许多任务之后。有趣的是,在初始调用和内核执行之间的实际层数,因为通过advapi.dll 的调用本身就是简化方法。让我解释一下,这有时被称为环或间接层,介于对任务的调用及其实际执行之间。你是一名谨慎的程序员,所以……你会测试你的变量,将你的SetValue
调用放在一个try
块中,然后执行调用。然后检查对框架方法的调用:安全令牌会被检查以获取所需的访问级别,会测试调用参数,类型会被转换,然后调用会被转发到 -> advapi.dll。在advapi 中,会再次测试调用参数,检查安全令牌,转换类型,然后会设置一个新的调用到ntdll.dll。如果你运行在用户模式下(当然,你就是),在调用最终转发到内核执行之前,还会再次测试调用参数和安全令牌。这有很多间接的环节!每次增加一个间接环节,都会对执行时间产生严重影响。
现在,我确信不乏顽固的 .NET 爱好者会告诉我我错了,并向我推销“构建块”编程方法的诸多优势,如果你相信这一点,那就随你便。但我相信,API 和框架的结合是最佳选择。在框架提供简化方法但通过 API 实现会需要大量繁琐编程的情况下,我倾向于框架(例如此类的渐变)。在 API 方法在速度和灵活性上远远超过框架的情况下,选择 API。我包含了一个示例项目,比较了BitBlt
API 和Graphics DrawImage
。在我的机器上,BitBlt
快 5 倍(你怎么反驳这一点?)。它也与上下文有关。如果你正在编写一个超级间谍扫描程序,需要在扫描期间访问注册表一万次,那么你希望间接环节越少越好,因此你应该考虑编写一个直接调用nt_ API 的类,或者更好的是一个调用zw_ API 的内核模式驱动程序。如果你只在程序关闭时设置几个应用程序默认值,嵌入式方法就足够了,但如果你问我,在编写软件时应该始终牢记那个黄金法则:最快的代码就是……没有代码。
继续表演……
此版本中有七种样式选项
- 默认:使用系统定义的と方法和样式绘制工具提示。
- 纯色:在工具提示上绘制纯背景色。此方法使用纯色画笔绘制背景
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(); }
- 渐变:渐变背景,带有八种预定义渐变。显示的渐变是通过使用
BlendTrianglar
样式创建的PathGradient
画笔。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(); }
- 图形:使用位图作为工具提示背景。此方法将位图图像存储在一个临时 DC 中,然后使用
BitBblt
处理边缘,使用StretchBlt
处理中心,将其 alpha 混合到窗口上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(); }
- 玻璃:预定义的玻璃效果。几乎与上面的相同,只是颜色和参数不同。
- OwnerDrawn:绘制由所有者决定,通过绘制事件接口处理。
我一直在想……他们是怎么制作那些 VS 样式提示的?小心你的愿望 ~嘿嘿~。花了一些功夫才得到外观和感觉,但示例应该能让你入门。
属性是通过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;
}
}
}
}
生之沉重(一个 Windows 开发者)
这些天,我正在 Vista 下编码,然后在 XP 中测试类。W2K 是我愿意回溯的最低版本,一个应用程序是否能在 10 年前的操作系统上运行已不再困扰我(你认为运行 Win98 的人真的会买软件吗?)。我正要发布这篇文章,但我的理智战胜了我,我决定先在 XP 上进行测试。我注意到的第一件事是工具提示周围有一个黑色的边框。你在 Vista 中看不到这个,因为系统是用 Vista 样式绘制它的。很简单,只需从工具提示中删除边框样式。
我注意到的第二件事是工具提示没有淡入淡出。在 Vista 中触发淡入淡出计时器 (6) 的 UID 在 XP 中不存在。尝试了大约一个小时来解决这个问题,我暂时放弃了在 XP 中实现淡入淡出(目前)。
下一个 bug 是可点击的工具提示不再工作(由于计时器差异),因此我为计时器(悬停:3)添加了一个新案例来处理鼠标悬停并防止窗口过早关闭。
最后一个(也是最奇怪的)问题是,当工具提示位置改变时(但仅当工具提示位于光标上方时),窗口会变成灰色。我认为这是操作系统将工具提示视为失去焦点并更改背景颜色。这通过在更改样式时“提醒”工具提示其背景颜色来解决。
case WM_STYLECHANGED:
if (_eCustomStyle == TipStyle.Default)
SendMessage(_hTipWnd, TTM_SETTIPBKCOLOR,
ColorTranslator.ToWin32(Color.LightYellow), 0);
base.WndProc(ref m);
break;
请注意,我没有使用系统颜色“Info”,因为它也会显示为灰色。
暂时就到这里(毕竟这只是我网格控件的一个成员类)。希望大家觉得有用。
历史
- 2008年12月2日:首次发布
- 2008年12月5日:更新源代码