vtExtender: ToolStrip 扩展、渲染器和自定义类






4.96/5 (33投票s)
创建具有渐变按钮、自定义工具提示的 ToolStrip,并暴露渲染器样式。
起初..
曾几何时,有了工具栏,开发者说:“工具栏很好,但是…”为什么我们只能选择几种样式?我怎么才能做出像 IE7 那样的渐变按钮?我能自定义工具提示吗?这个Renderer
类是怎么工作的,我又能用它做什么?
首先,我想感谢 Phil Wright 的 Office 2007 渲染器示例;没有他的示例,这项工作将花费更长的时间。
创建此类时的第一个目标被证明是最困难的,即如何创建具有渐变按钮的工具栏。解决方案需要大量的实验才能正确实现(但稍后会详细介绍)。我还想创建一个按钮,当选中时看起来是凸起的,当按下时是凹陷的;就此而言,我想要一种灵活性,而这种灵活性在通过设计器提供的预制实现中是缺乏的,并且我想公开允许即时自定义工具栏外观的属性。我开始研究Renderer
类(实际上帮助不大),然后寻找此处和其他开发人员网站上的更多代码示例。我找到的唯一好的示例是 Office 2007 渲染器,所以我把它拆开,逐行分析。
与 .NET 的其他方面一样,我开始喜欢渲染器类的想法,但对其实现感到失望。绘图过程中有太多方面没有得到充分暴露。如果重做一遍,我可能会从头开始编写一个工具栏;工作量更少,甚至代码也可能更少。
好了,前言就到这里..
继续表演..
这不仅是渲染器类的实现,还包括几个嵌入式类,用于实现渐变按钮、创建自定义工具提示和支持自定义绘制。然而,核心是一个渲染器类,我认为它可以被视为在工具栏元素的各种绘制阶段发送的一系列事件。根据状态,您可以处理事件,或者选择将其传递给默认处理程序。
protected override void OnRenderImageMargin(ToolStripRenderEventArgs e)
{
// draw the margin area on a menu
if ((e.ToolStrip is ContextMenuStrip) ||
(e.ToolStrip is ToolStripDropDownMenu))
drawImageMargin(e.Graphics, e.AffectedBounds);
else
base.OnRenderImageMargin(e);
}
事件参数包含有关项目和绘制阶段的信息;这些信息可以包括大小和坐标、图形对象、对象所有者以及特定于该事件的信息。您可以通过不包含基类部分来绕过默认处理程序,或者在某些情况下,事件参数中包含一个已处理的标志。
困难的部分可能是弄清楚这些事件中的一些实际上做了什么,以及如何处理事件参数。在上面的片段中,如果工具栏所有者是菜单,则调用会被转发到绘制菜单图像边距的例程。
这是一个在调用OnRenderButtonBackground
事件时(通过中间例程drawButton
)如何绘制玻璃风格按钮的示例。
private void drawGlassButton(Graphics g, RectangleF bounds, int opacity)
{
// initial bounds
bounds.Inflate(-1, -1);
// draw using anti alias
using (GraphicsMode mode = new GraphicsMode(g, SmoothingMode.AntiAlias))
{
// draw the border around the button
using (GraphicsPath buttonPath = createRoundRectanglePath(
g,
bounds.X, bounds.Y,
bounds.Width, bounds.Height,
1f))
{
using (LinearGradientBrush borderBrush = new LinearGradientBrush(
bounds,
Color.FromArgb(opacity * 20, ButtonGradientEnd),
Color.FromArgb(opacity * 20, ButtonGradientBegin),
90f))
{
borderBrush.SetSigmaBellShape(0.5f);
using (Pen borderPen = new Pen(borderBrush, .5f))
g.DrawPath(borderPen, buttonPath);
}
// create a clipping region
RectangleF clipBounds = bounds;
clipBounds.Inflate(-1, -1);
using (GraphicsPath clipPath = createRoundRectanglePath(
g,
clipBounds.X, clipBounds.Y,
clipBounds.Width, clipBounds.Height,
1f))
{
using (Region region = new Region(clipPath))
g.SetClip(region, CombineMode.Exclude);
}
// fill in the edge accent
using (LinearGradientBrush edgeBrush = new LinearGradientBrush(
bounds,
Color.FromArgb(opacity * 15, ButtonBorderColor),
Color.FromArgb(opacity * 5, Color.Black),
90f))
{
edgeBrush.SetBlendTriangularShape(0.1f);
g.FillPath(edgeBrush, buttonPath);
g.ResetClip();
bounds.Inflate(-1, -1);
}
// fill the button with a subtle glow
using (LinearGradientBrush fillBrush =
new LinearGradientBrush(
bounds,
Color.FromArgb(opacity * 10, Color.White),
Color.FromArgb(opacity * 5, ButtonGradientBegin),
LinearGradientMode.ForwardDiagonal))
{
fillBrush.SetBlendTriangularShape(0.4f);
g.FillPath(fillBrush, buttonPath);
g.ResetClip();
}
}
}
}
正如您所见,大部分代码用于处理绘制事件,并使用它们来创建所需的视觉样式。上面的示例使用剪裁、混合和渐变来创建具有镜像边缘的按钮。
亮点
示例应用程序中有五种样式,涵盖了该类的部分范围。Carbon 和 System Plus 样式使用图像作为工具栏背景,其余的则使用渐变。我尽可能多地将属性从类中暴露出来,为用户提供了广泛的样式选项。
玻璃
这种样式使用白色和银色的混合渐变。混合以及渐变类型通过属性暴露,或者可以通过SetGlobalStyle
方法设置,或者通过SetStyle
方法为工具栏类型(例如SetStatusbarStyle()
)设置。此示例还使用了上述代码中的玻璃按钮样式。
Chrome
这种样式使用居中的垂直渐变,带有扁平风格的菜单和“发光”按钮样式。发光效果是通过将不同透明度的帧嵌入圆角图形路径来实现的。
碳
Carbon 使用背景图像,带有玻璃风格的按钮和 Office 风格连接的菜单。
系统增强版
显示在页面顶部。这也使用背景图像来模仿 IE 风格,尽管也可以通过渐变来实现。但我更喜欢图像..处理量更少。您知道控件(例如此表单或按钮…)上的许多视觉样式都是通过将图像绘制到控件上来实现的吗?
黑出局
另一种使用垂直渐变和自定义混合的样式示例。
菜单样式
我在类中放入了四种不同的菜单样式选项:Vista、Flat、Office 和 Custom。样式还决定了菜单栏按钮的样式。自定义选项将使用该工具栏的按钮样式选项来绘制按钮。
工具提示
ToolTip
类实际上并不是一个工具提示,而是一个带有渐变计时器的静态窗口。
public ToolTip(IntPtr hParentWnd)
{
Type t = typeof(ToolTip);
Module m = t.Module;
_hInstance = Marshal.GetHINSTANCE(m);
_hParentWnd = hParentWnd;
// create window
_hTipWnd = CreateWindowEx(WS_EX_TOPMOST | WS_EX_TOOLWINDOW,
"STATIC", "",
SS_OWNERDRAW | WS_CHILD | WS_CLIPSIBLINGS | WS_OVERLAPPED,
0, 0,
0, 0,
GetDesktopWindow(),
IntPtr.Zero, _hInstance, IntPtr.Zero);
// set starting position
SetWindowPos(_hTipWnd, HWND_TOP,
0, 0,
0, 0,
SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE | SWP_NOOWNERZORDER);
createFonts();
this.AssignHandle(_hTipWnd);
}
请注意,您可以通过更改使用的颜色的 alpha 值来创建透明工具提示。这之所以可行,是因为静态窗口本身是透明的。
按钮渐变器
正如我在文章开头提到的,我真的很想要渐变按钮……让工具栏更有品位,你知道吗?我开始编写一个计时器类,并为每个需要渐变的工具项创建一个该类的实例。计时器类通过将计时器与父窗口同步来保持线程安全,以便从计时器线程发出的调用在父线程上退出。
_aTimer.SynchronizingObject = (ISynchronizeInvoke)sender;
该类包含一个计时器,该计时器根据离开状态向上或向下计数,但它还包含对所有者项的引用以及该项的图像设备上下文。这是因为您不能简单地在按钮从透明到不透明的循环之间擦除按钮,否则它会像坏的荧光灯一样闪烁。相反,您必须先绘制干净按钮的图像,然后绘制半透明按钮掩码。这提供了无闪烁的渐变效果。绘制是通过计时器类触发的事件发起的;事件还管理重置计时器并在渐变计时器用完后使工具项无效。
然而,我发现按钮在鼠标移出项目时会使自身失效,导致严重的闪烁。我尝试了多种方法来解决这个问题,包括对Paint
和Invalidate
方法进行子类化,但都没有成功……这是某些子类化方法的问题之一,如果这些任务以任何方式超出了预期的规范,它们似乎就无法完成某些任务。您被迫转向非托管代码并构思卑鄙的技巧。在这里就是这样……在用尽了所有内置方法后,我转向了 API 来找到解决方案。首先,我覆盖了消息处理程序并拦截了WM_PAINT
。
protected override void WndProc(ref Message m)
{
switch (m.Msg)
{
// bypasses an invalidation thrown by parent when mouse leaves
// a button, this causes a slight flicker when drawing fader
// if someone knows a better way, post a message..
case WM_PAINT:
if (!bypassPaint())
base.WndProc(ref m);
else
m.Result = new IntPtr(1);
break;
default:
base.WndProc(ref m);
break;
}
}
bypassPaint
方法首先获取更新矩形,然后如果它与具有活动计时器的项相交,它会使项边界有效化并绕过绘制调用。
internal Rectangle updateRegion()
{
RECT updateRect;
GetUpdateRect(ToolStrip.Handle, out updateRect, false);
return new Rectangle(updateRect.Left, updateRect.Top,
updateRect.Right - updateRect.Left,
updateRect.Bottom - updateRect.Top);
}
internal bool bypassPaint()
{
Rectangle updateRect = updateRegion();
foreach (ToolStripItem item in ToolStrip.Items)
{
if ((_fader.ContainsKey(item)) &&
(_fader[item].TickCount > 0) &&
(!item.IsOnOverflow) &&
(!_fader[item].Invalidating) &&
(updateRect.IntersectsWith(item.Bounds)))
{
if (((_fader[item].FadeStyle == FadeType.FadeOut) ||
(_fader[item].FadeStyle == FadeType.FadeFast)))
{
RECT validRect = new RECT(updateRect.Left, updateRect.Top,
updateRect.Right, updateRect.Bottom);
ValidateRect(ToolStrip.Handle, ref validRect);
return true;
}
}
}
return false;
}
嗯……就这些了,好好享受代码,并远离麻烦……