媒体按钮:一个动画 WMC 风格的按钮控件






4.85/5 (36投票s)
旋转、摆动、脉动、缩小,你懂的..
Inception
最近写了一些需要一些带有亮点的按钮的东西,我开始考虑为我的小型 Wav 录音机编写一个按钮类。我首先想到的是 WMC 的按钮。我对 WMC 有很多不满(那个愚蠢的摘要页面——为什么那里有个删除按钮?UI 选项的缺乏——其他两种风格怎么了?难道你们解雇了美工吗?为什么他们不缓存该死的小缩略图?!真是的……),但他们确实在用户界面方面做得不错。第一次看到时绝对有“哇”的因素。所以……我在我的开发机上打开了 WMC,仔细看了看……经过一番激烈的内心对话(哎呀,这得花一周时间才能弄好!),并且像我出名那样不顾一切地挥霍我的空闲时间,我(你)现在拥有了一个媒体按钮在我的武器库里。
样式
WMC 中有三种不同的按钮风格:主页面上的“菜单”按钮,它将你带到一个选项类别;没有动画的“媒体”按钮控件;以及遍布选项页面的“自定义”按钮。我将主要关注其中最复杂的一种:菜单风格。
菜单风格
在创建精灵或启动计时器之前,我尽力模仿按钮的视觉风格。WMC 框架有一个 1 像素宽的黑色外框(没有它看起来更好),还有一个用对角渐变绘制的 2 像素边框。这是通过使用 `BackwardDiagonal` 混合渐变画笔通过 `GraphicsPath` 绘制实现的。
private void DrawMenuButtonBorder(Graphics g, RectangleF bounds)
{
using (GraphicsPath borderPath = CreateRoundRectanglePath(
g,
bounds.X, bounds.Y,
bounds.Width, bounds.Height,
CornerRadius))
{
// top-left bottom-right -dark
using (LinearGradientBrush borderBrush = new LinearGradientBrush(
bounds,
Color.FromArgb(140, Color.DarkGray),
Color.FromArgb(140, Color.White),
LinearGradientMode.BackwardDiagonal))
{
Blend blnd = new Blend();
blnd.Positions = new float[] { 0f, .5f, 1f };
blnd.Factors = new float[] { 1f, 0f, 1f };
borderBrush.Blend = blnd;
using (Pen borderPen = new Pen(borderBrush, 2f))
g.DrawPath(borderPen, borderPath);
}
}
}
菜单风格有一个明显的阴影,也是通过渐变画笔实现的,并使用剪裁区域来控制效果。
private void DrawMenuButtonDropShadow(Graphics g, RectangleF bounds,
int depth, int opacity)
{
// offset shadow dimensions
RectangleF shadowBounds = bounds;
shadowBounds.Inflate(1, 1);
shadowBounds.Offset(depth, depth);
// create a clipping region
bounds.Inflate(1, 1);
using (GraphicsPath clipPath = CreateRoundRectanglePath(
g,
bounds.X, bounds.Y,
bounds.Width, bounds.Height,
CornerRadius))
{
// clip the interior
using (Region region = new Region(clipPath))
g.SetClip(region, CombineMode.Exclude);
}
// create a graphics path
using (GraphicsPath gp = CreateRoundRectanglePath(g, shadowBounds.X,
shadowBounds.Y, shadowBounds.Width, shadowBounds.Height, 8))
{
// draw with a path brush
using (PathGradientBrush borderBrush = new PathGradientBrush(gp))
{
borderBrush.CenterColor = Color.FromArgb(opacity, Color.Black);
borderBrush.SurroundColors = new Color[] { Color.Transparent };
borderBrush.SetBlendTriangularShape(.5f, 1.0f);
borderBrush.FocusScales = new PointF(.4f, .5f);
g.FillPath(borderBrush, gp);
g.ResetClip();
}
}
}
现在我们得到了类似这样的效果
然后,我们在其中绘制一个几乎透明的蒙版,使用 `LinearGradient` 画笔以及半透明白色和几乎透明银色之间的混合,调整混合和因子以在顶部边缘附近产生撕裂效果。
private void DrawMenuButtonMask(Graphics g, RectangleF bounds)
{
RectangleF maskRect = bounds;
int offsetX = (this.ImagePadding.Left + this.ImagePadding.Right) / 2;
int offsetY = (this.ImagePadding.Top + this.ImagePadding.Bottom) / 2;
maskRect.Inflate(offsetX, offsetY);
// draw using hq anti alias
using (GraphicsMode mode = new GraphicsMode(g, SmoothingMode.AntiAlias))
{
// draw the drop shadow 494 210
DrawMenuButtonDropShadow(g, maskRect, ShadowDepth, 120);
// draw the border
DrawMenuButtonBorder(g, maskRect);
maskRect.Inflate(-1, -1);
// create an interior path
using (GraphicsPath gp = CreateRoundRectanglePath(g, maskRect.X, maskRect.Y,
maskRect.Width, maskRect.Height, CornerRadius))
{
// fill the button with a subtle glow
using (LinearGradientBrush fillBrush = new LinearGradientBrush(
maskRect,
Color.FromArgb(160, Color.White),
Color.FromArgb(5, Color.Silver),
75f))
{
Blend blnd = new Blend();
blnd.Positions = new float[] { 0f, .1f, .2f, .3f, .4f, .5f, 1f };
blnd.Factors = new float[] { 0f, .1f, .2f, .4f, .7f, .8f, 1f };
fillBrush.Blend = blnd;
g.FillPath(fillBrush, gp);
}
}
// init storage
_tSwirlStage = new SwirlStage(0);
maskRect.Inflate(1, 1);
_tSwirlStage.mask = Rectangle.Round(maskRect);
}
}
这就得到了聚焦蒙版。
好了,说完了这些,现在是激动人心的部分(我->讽刺)。菜单按钮采用四阶段复合动画。第一阶段有两个同时进行的效果:线条精灵和一个似乎从按钮下方发出的环境光晕。在第一阶段,两条线条精灵从 (0,0) 开始,然后分成两个方向:一条沿着左侧向下,另一条沿着顶部横向。在移动过程中,它们的亮度也在减弱,直到大约三分之一处,它们就消失了。在此过程中,一个纹理光晕会沿着顶部边缘脉动一次。现在,我认为这类动画的关键在于微妙。如果动画过于繁忙或效果过于明显,观察者会在第一个周期就全部看到,并很快失去兴趣。所以,就像 WMC 中一样,效果非常微妙,需要仔细观察才能获得完整效果。
考虑到 C# 图形在速度方面还有很多不足之处(`Graphics.Draw` 有 30 个重载,难怪这么慢;顺便说一句,它调用一个只接受整数的 API……),我们需要缓冲绘图,并在控件加载时只创建一次所需的精灵。此控件使用了我的 `StoreDc` 类,用于将控件绘制到静态缓冲区;第二个缓冲区用于精灵,实际上是三重缓冲控件,但同时也节省了大量 CPU 时间,并实现了无缝动画。
动画状态存储在一个自定义类型中,该类型保存精灵大小、计时器滴答和相对坐标等状态。所有这些都由一个自定义计时器类 `FadeTimer` 运行,该计时器从同步计时器触发事件,计数递增/递减或循环。我将这个动画段命名为“Swirl”,因为边缘有旋转的精灵。`DrawSwirl()` 例程是核心,它太大无法直接放在此页面上,但这是到第一个动画阶段结束的片段。
private void DrawSwirl()
{
if (_cButtonDc != null)
{
int endX = 0;
int endY = this.Height / 2;
float alphaline = 0;
int offset = 0;
Rectangle cliprect;
Rectangle mistrect;
Rectangle linerect;
// copy unaltered image into buffer
BitBlt(_cTempDc.Hdc, 0, 0, _cTempDc.Width,
_cTempDc.Height, _cButtonDc.Hdc, 0, 0, 0xCC0020);
switch (_tSwirlStage.stage)
{
#region Stage 1 - Top/Left
case 0:
{
endX = _tSwirlStage.mask.Width / 2;
if (_tSwirlStage.tick == 0)
{
_tSwirlStage.linepos.X =
_tSwirlStage.mask.X + (int)CornerRadius;
_tSwirlStage.linepos.Y = _tSwirlStage.mask.Y;
_ftAlphaLine = .95f;
_ftAlphaGlow = .45f;
}
// just in case..
if (endX - _tSwirlStage.linepos.X > 0)
{
// get the alpha
_ftAlphaLine -= .02f;
if (_ftAlphaLine < .4f)
_ftAlphaLine = .4f;
linerect = new Rectangle(_tSwirlStage.linepos.X,
_tSwirlStage.linepos.Y, _bmpSwirlLine.Width,
_bmpSwirlLine.Height);
// draw first sprite -horz
DrawMenuButtonLine(_cTempDc.Hdc, _bmpSwirlLine,
linerect, _ftAlphaLine);
// second sprite -vert
// turn down the alpha to match border color
alphaline = _ftAlphaLine - .1f;
// draw second sprite
linerect = new Rectangle(_tSwirlStage.linepos.Y,
_tSwirlStage.linepos.X, _bmpSwirlLineVert.Width,
_bmpSwirlLineVert.Height);
DrawMenuButtonLine(_cTempDc.Hdc,
_bmpSwirlLineVert, linerect, alphaline);
}
// draw mist //
if (_tSwirlStage.linepos.X < endX / 3)
{
_ftAlphaGlow += .05f;
if (_ftAlphaGlow > .9f)
_ftAlphaGlow = .9f;
}
else
{
_ftAlphaGlow -= .05f;
if (_ftAlphaGlow < .1f)
_ftAlphaGlow = .1f;
}
// position
cliprect = _tSwirlStage.mask;
cliprect.Inflate(1, 1);
cliprect.Offset(1, 1);
mistrect = new Rectangle(_tSwirlStage.mask.Left,
_tSwirlStage.mask.Top, _bmpSwirlGlow.Width,
_bmpSwirlGlow.Height);
offset = (int)(ShadowDepth * .7f);
mistrect.Offset(-offset, -offset);
// draw _ftAlphaGlow
DrawMenuButtonMist(_cTempDc.Hdc, _bmpSwirlGlow,
cliprect, mistrect, _ftAlphaGlow);
// counters
_tSwirlStage.linepos.X++;
_tSwirlStage.tick++;
// reset
if (_tSwirlStage.linepos.X > (endX - _tSwirlStage.linepos.X))
{
_tSwirlStage.stage = 1;
_tSwirlStage.tick = 0;
_ftAlphaGlow = 0;
_ftAlphaLine = 0;
}
break;
}
#endregion
...
你可能已经注意到了 `DrawMenuButtonMist` 和 `DrawMenuButtonLine` 例程;这些调用 `AlphaBlend` 例程。最初,我使用了 `GdiAlphaBlend`,但我发现 `Graphics` 的 `ColorMatrix`/`ImageAttribute` 方法效果相当不错……
private void DrawMenuButtonLine(IntPtr destdc, Bitmap source,
Rectangle bounds, float intensity)
{
using (Graphics g = Graphics.FromHdc(destdc))
{
g.CompositingMode = CompositingMode.SourceOver;
AlphaBlend(g, source, bounds, intensity);
}
}
private void AlphaBlend(Graphics g, Bitmap bmp,
Rectangle bounds, float alpha)
{
if (alpha > 1f)
alpha = 1f;
else if (alpha < .01f)
alpha = .01f;
using (ImageAttributes ia = new ImageAttributes())
{
ColorMatrix cm = new ColorMatrix();
cm.Matrix00 = 1f;
cm.Matrix11 = 1f;
cm.Matrix22 = 1f;
cm.Matrix44 = 1f;
cm.Matrix33 = alpha;
ia.SetColorMatrix(cm);
g.DrawImage(bmp, bounds, 0, 0, bmp.Width,
bmp.Height, GraphicsUnit.Pixel, ia);
}
}
还请注意调用方法中 `CompositingMode` 被更改为 `SourceOver`。
从按钮顶部角落散发出来的光晕是通过将剪裁区域放置在蒙版和边框周围来渲染的。正如我所提到的,我最初使用的是 API `AlphaBlend`,并且 `Graphics` 的剪裁方法在这种情况下出于某种原因不起作用,所以我编写了一个基于 API 的剪裁类 `ClippingRegion`,它似乎在任何一种情况下都运行良好。
聚焦状态下的其他精灵运动与第一个类似,但该按钮还使用了第二个效果,按下时会调整大小。当 WMC 首次加载时,它也会增大。这是一个问题,因为它在控件可见之前就调整了大小。你看,`Visible` 属性只是 `WS_VISIBLE` 样式位已被设置的指示,而不是控件已自行绘制。我在 MSDN 上提出了这个问题,但没有得到答复(在 MSDN 上你随便扔块石头都能砸到 MVP,但没人知道如何确定控件是否真正可见?!)。我通过一个简单的等待计时器解决了这个问题,并提供了使用 `ResizeOnLoad` 属性使用此效果的选项,该属性应分配给窗体加载时获得焦点的按钮。通过将蒙版图像片段绘制到临时 DC 中,然后通过 `FadeTimer` 滴答计数分阶段调整图像大小来实现大小调整效果,如下所示。
private void DrawResized()
{
Rectangle canvas = new Rectangle(0, 0, this.Width, this.Height);
Rectangle maskRect = canvas;
// image axis
int offsetX = (this.ImagePadding.Left + this.ImagePadding.Right) / 2;
int offsetY = (this.ImagePadding.Top + this.ImagePadding.Bottom) / 2;
maskRect.Inflate(-offsetX, -offsetY);
// inflate by -tickcount
int sizediff = _cResizeTimer.TickCount;
maskRect.Inflate(-sizediff, -sizediff);
if (_cButtonDc != null)
{
using (Graphics g = Graphics.FromHdc(_cButtonDc.Hdc))
{
using (GraphicsMode mode =
new GraphicsMode(g, SmoothingMode.AntiAlias))
{
// set hq render
g.InterpolationMode = InterpolationMode.HighQualityBicubic;
// backfill
using (Brush br = new SolidBrush(this.BackColor))
g.FillRectangle(br, canvas);
// draw text in at normal size
if (this.Text.Length != 0)
{
SizeF sz = MeasureText(this.Text);
maskRect.Height -= (int)sz.Height +
TextPadding.Top + TextPadding.Bottom;
Rectangle textRect = GetTextRectangle(TextAlign, canvas,
new Rectangle(0, 0, (int)sz.Width, (int)sz.Height));
DrawText(g, this.Text, textRect);
}
// draw the sized image
g.DrawImage(_bmpResize, maskRect);
}
}
}
// draw to control
using (Graphics g = Graphics.FromHwnd(this.Handle))
{
BitBlt(g.GetHdc(), 0, 0, _cButtonDc.Width,
_cButtonDc.Height, _cButtonDc.Hdc, 0, 0, 0xCC0020);
g.ReleaseHdc();
}
// don't repaint
RECT r = new RECT(0, 0, canvas.Width, canvas.Height);
ValidateRect(this.Handle, ref r);
}
请注意上面代码中 `ValidateRect` 的使用。我们正在告知 Windows 所有绘制工作已完成(并让它保持原样;o)。
自定义按钮
你不会以为我忘了这个吧;它几乎和菜单按钮一样复杂(好吧,不完全是……)。自定义风格有两种动画:聚焦时有脉动效果(非常微妙;当我第一次在 WMC 中注意到它时,我以为是电视上的眩光,或者是太阳黑子,或者是我应该直接丢掉的放了三天的炖牛肉……),以及按下时的“Swish”效果,即那个小水滴飞过……我认为创建这些效果最难的部分是匹配渐变纹理。我相当确定他们使用了一些图像效果,菜单按钮下的光晕和飞行的水滴似乎最让我恼火/困扰,但与其在这里贴一些无聊的代码,我给你一个小贴士。在重新创建渐变时,我更关注形状而不是颜色;先调整好形状,然后调整颜色。例如,在创建自定义按钮的环境光晕时,它的微妙让我难以掌握,所以我使用了红色而不是白色来了解形状和纹理;这一点弄清楚后,我再调整颜色。