《盗梦空间》
最近写的一些东西需要一些按钮和一些抛光,我开始考虑为我的小Wav记录器写一个按钮类。我首先想到的是WMC的按钮。我不喜欢WMC的很多地方(那个愚蠢的总结页面——为什么那里有一个删除按钮?), UI选项的缺乏——其他两种样式怎么样了?你炒了那个制图的吗?为什么他们不把该死的缩略图藏起来?但有一件事他们做得很不错,那就是用户界面。当第一次看到它时,它肯定有一些令人惊叹的因素。所以. .我在我的开发箱中打开WMC,并仔细查看。在一些轻快的内心对话之后(天啊,这需要一个星期才能做对!),我(你)不计后果地利用我的空闲时间,现在我的战斗箱里有了一个媒体按钮。
风格
WMC中有三种不同风格的按钮:主页上的“菜单”按钮,它会将你发送到选项的类别;没有动画的“媒体”按钮控件;以及在整个选项页面中可以找到的“自定义”按钮。本文将主要关注这三种样式中比较复杂的一种:菜单样式。
菜单风格
在创建精灵或启动计时器等等之前,我尽力接近按钮的视觉风格。WMC框架有一个1px宽的黑色外部框架(没有它看起来更好),带有一个对角渐变的2px框架。这是通过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(); } } }
现在我们有了像这样的东西:
然后,我们在这里画一个几乎透明的蒙版,使用线性渐变笔刷和半透明的白色和几乎透明的银色之间的混合,调整混合和因素,以达到接近顶部边缘的撕裂效果:
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#图形随着速度的提高(图形过载30次)还有很多需要改进的地方。画,难怪它这么慢;顺便说一下,它调用的API只接受一个整数..),我们需要缓冲绘图,并创建精灵只需要一次时,控件加载。这个控件使用我的StoreDc类,用于绘制控件到一个静态缓冲区;第二个缓冲区用于精灵,实际上,三倍缓冲控制,但也节省了大量的CPU时间,并创建一个无缝动画。
动画状态存储在自定义类型中,用于保存精灵大小、计时器和相对坐标等状态。这都是通过自定义计时器类FadeTimer运行的,它在升序/降序计数或循环中从同步计时器触发事件。我将这个动画片段命名为“漩涡”,用于旋转边缘精灵。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,但我发现图形颜色矩阵/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,而图形裁剪方法由于某些原因在该上下文中不起作用,所以我编写了一个基于API的裁剪类ClippingRegion,它似乎在这两个实例中都能很好地工作。
在聚焦状态下的其他精灵动作与第一个类似,但是这个按钮也使用了第二个效果,当按下时它会调整大小。当WMC首次加载时,它的大小也会增大。这是一个问题,因为它在控件可见之前正在调整大小。您可以看到,可见属性只是表明设置了WS_VISIBLE样式位,而不是表示控件已经绘制了自己。我在MSDN上提出了这个问题,但是没有得到答案(你不能在MSDN上不击中MVP就挥出一只死猫,但是没有人知道如何确定控件是否真的可见?!)我用一个简单的等待计时器解决了这个问题,并为ResizeOnLoad属性提供了使用这个效果的选项,这个属性应该分配给表单加载时获得焦点的按钮。调整大小的效果是通过将蒙版图像片段blitting到一个临时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的使用。我们告诉窗户,所有的画都画好了(不要管它;o)。
自定义按钮
现在,你不会认为我忘记了这个;它几乎和菜单按钮一样复杂(嗯,不完全是…)。自定义风格有两个动画:聚焦时的脉冲效果(这是非常微妙的;当我在WMC中第一次注意到它的时候,我还以为是电视里的强光,太阳黑子,或者是我应该不去管的烤了三天的炖肉…),以及按下按钮时的“Swish”效果。,那个小斑点飞过…我认为创建这些效果最困难的部分是匹配渐变纹理。我相当确定他们使用了图像的一些效果,菜单按钮下的辉光,和飞行的斑点似乎是最烦/烦扰我,但不是在这里扔下一些更无聊的代码,我将给你一个提示。在创建渐变的时候,我没有过多地关注颜色,而是关注形状;把形状做好,然后调整颜色。例如,当为自定义按钮创建环境辉光时,它的微妙让它很难把握,所以我使用红色而不是白色来获得形状和纹理的概念;我调整了颜色。
本文转载于:http://www.diyabc.com/frontweb/news14551.html