十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

用GDI+自绘控件:打造AntDesign风格的WinForm工具栏

用GDI+自绘控件:打造AntDesign风格的WinForm工具栏 1. 为什么WinForm界面始终逃不过丑这个字WinForm的界面美化一直是国内开发者绕不开的话题。你翻看GitHub上的C#项目十有八九还是那副老样子白色的窗体、呆板的ToolStrip、毫无反馈的按钮一运行起来就有种上个时代的产物的感觉。这些年我也做过不少客户项目经常听到的一句话是功能挺好的就是界面看起来不够高级。功能和颜值之间的矛盾在WinForm上尤其突出。其实WinForm本身并不是不能做好看只是默认控件的外观由系统主题接管你很难通过改几个属性就获得类似Web前端那种设计感。比如AntDesign这类组件库按钮有主色、有圆角、有悬停变深的反馈、有按下时的涟漪感这些体验在原生ToolStrip里是完全不存在的。ToolStripButton顶多换个背景色悬停状态由系统绘制风格跟现代设计语言差着一大截。这篇文章想做的事情很简单抛开第三方UI库纯用WinForm自带的GDI绘制能力把AntDesign的设计语言移植到工具栏这个场景上。最终做出来的效果是按钮扁平化、有圆角、有主色、鼠标悬停时有自然的颜色过渡按下时有下沉感支持图标加文字的组合排列整体风格干净清爽跟Web端的AntDesign组件观感接近但跑起来就是一个标准的Native WinForm程序不需要引入任何重量级依赖。这套方案适合谁参考给WinForm项目做界面升级的人、想从零理解GDI自绘控件的读者、以及不想引入大型第三方UI框架但又想让程序至少看起来不过时的开发者。代码量不大核心就是一个自定义Button控件加一小组配套的工具栏容器控件但整个思路吃透了你可以扩展到卡片、面板、表单等几乎所有界面元素上。2. 从AntDesign身上我们能抄到什么AntDesign的设计语言并不是玄学它的核心规则很容易拆解。工具栏这种场景来回就那几类元素普通按钮、图标按钮、带下拉的复合按钮、分割线、以及头部容器。把这些元素用AntDesign的视觉规则统一起来整体效果就出来了。2.1 关键视觉要素拆解主色贯穿AntDesign的默认主色是#1890FF按钮、选中态、链接、焦点框全部围绕这个颜色做明暗变化。悬停时颜色变深#40a9ff按下时更深#096dd9禁用时变成低饱和度的灰色。扁平化处理去掉渐变、去掉边框的厚重感取而代之的是1像素细边框和干净的留白。现代工具栏不再使用纯灰的立体按钮而是通过颜色层次区分状态。圆角体系AntDesign的基础圆角是2px到4px。WinForm默认按钮是直角矩形看起来生硬加上2到4像素的圆角之后整个控件的气质立刻变了。状态反馈鼠标悬停时颜色渐变或瞬间切换按下时内容有一个轻微的向下位移Active状态剥离开来。这对Web前端是CSS几行代码的事但在WinForm里需要你手动在绘制逻辑里实现。间距与对齐AntDesign的按钮内边距Padding为左右16px上下0或4px工具栏一般高32到40像素内部图标和文字的间距有明确规范。工具栏中多个按钮之间通常用8px的间距分隔。2.2 状态优先级是精髓工具栏按钮最核心的交互逻辑不是画个圆角矩形这么简单而是要正确处理好多个布尔状态叠加时到底画成什么样。比如一个按钮同时处于Hover和Selected比如当前所在页面的菜单项那它是显示悬停色还是选中色一个按钮处在Disabled状态鼠标悬停在它上面到底给不给反馈这些规则如果不定清楚做出来的工具栏交互会很飘。我的个人约定是Disabled Pressed Selected Hover Normal。禁用优先级最高不论什么状态都以灰色呈现且不响应鼠标按下状态用来画最深的颜色这在视觉上模拟按钮被按下去选中态用主色填充表示这是当前激活的功能悬停态只是稍微提升背景亮度或透明度普通态就是白底黑字加细边框。把这个优先级用if-else链写清楚后面不会乱。2.3 工具栏容器的取舍在WinForm里做工具栏的承载容器有三种可走的路。一是直接用ToolStrip把Renderer换成自定义绘制的ToolStripRenderer好处是系统帮你处理溢出按钮和布局坏处是ToolStrip的渲染机制比较绕细节控制起来很费劲。二是用FlowLayoutPanel把每个按钮当作独立控件往里丢简单直接但溢出、对齐、间距控制都需要自己写。三是做一个自带布局逻辑的自定义ToolstripContainer重写OnPaint和鼠标消息处理。我建议选择第二种思路FlowLayoutPanel 一组自定义控件。这样每个按钮是独立控件状态管理和事件处理都很自然FlowLayoutPanel天然支持换行和间距配合Dock Top和AutoSize已经能满足绝大多数工具栏排列需求。后续你想把工具栏做成固定顶部、可拖拽、可折叠也是在容器层继续扩展的事。3. 开始动工自定义工具栏控件的核心实现现在进入正题。我会把整个实现拆成三个类AToolbarButton核心所有控制按钮的载体、AToolbarSeparator垂直分割线、以及一个简单的AToolbarContainer容器。代码以C# WinForms环境为基础默认开启DoubleBuffered全部手写绘制逻辑。3.1 基础属性与样式枚举首先要确定按钮对外暴露什么样的属性。颜色相关参数全部做成可配置的不要写到绘制方法里写死。这样别人用你的控件只需要改几个属性就能换主题色。public enum ToolbarButtonState { Normal, Hover, Pressed, Selected, Disabled } public class AToolbarButton : Control { public string IconText { get; set; } string.Empty; public bool IsSelected { get; set; } public bool IsPressed { get; set; } public bool IsHovering { get; set; } public bool IsDisabled { get; set; } public Color ThemeColor { get; set; } Color.FromArgb(24, 144, 255); public Color HoverColor { get; set; } Color.FromArgb(230, 244, 255); public Color PressedColor { get; set; } Color.FromArgb(214, 235, 255); public Color NormalForeColor { get; set; } Color.FromArgb(51, 51, 51); public int CornerRadius { get; set; } 4; public int ImageTextGap { get; set; } 8; private bool _mouseDown; private bool _mouseEnter; public AToolbarButton() { SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer | ControlStyles.UserPaint | ControlStyles.ResizeRedraw, true); Size new Size(96, 36); Font new Font(Microsoft YaHei UI, 9F); Cursor Cursors.Hand; } }ThemeColor是AntDesign风格的主色HoverColor和PressedColor是预计算好的浅色背景中文环境常用的悬停底色是主色10%到20%的透明混合。这里直接给出具体的RGB值效果比较贴近AntDesign的#e6f7ff。3.2 绘制逻辑圆角、背景与文字图标绘制过程集中在OnPaint里。为了保证圆角在高DPI下不出现锯齿必须设置Graphics.SmoothingMode AntiAlias在绘制正方形、矩形这类没有斜边的图形时影响不大但涉及到圆角路径关闭抗锯齿会看到明显的阶梯状边缘。圆角路径的生成可以用GraphicsPath实现这里用AddArc组合四个角private static GraphicsPath CreateRoundedRectangle(Rectangle bounds, int radius) { int diameter radius * 2; GraphicsPath path new GraphicsPath(); if (radius 0) { path.AddRectangle(bounds); return path; } Rectangle arc new Rectangle(bounds.Location, new Size(diameter, diameter)); path.AddArc(arc, 180, 90); arc.X bounds.Right - diameter; path.AddArc(arc, 270, 90); arc.Y bounds.Bottom - diameter; path.AddArc(arc, 0, 90); arc.X bounds.Left; path.AddArc(arc, 90, 90); path.CloseFigure(); return path; }这里有个细节bounds如果是控件完整的显示区域圆角会把底边的像素裁掉一点导致背景和容器之间有1像素的缝隙。更稳妥的做法是把bounds压缩1到2个像素即Rectangle(1, 1, Width - 2, Height - 2)。否则在深色容器背景下按钮四周会出现一圈细线看起来非常不专业。背景颜色根据当前状态切换。状态优先级已经确定所以用if-else写出来protected override void OnPaint(PaintEventArgs e) { Graphics g e.Graphics; g.SmoothingMode System.Drawing.Drawing2D.SmoothingMode.AntiAlias; g.TextRenderingHint System.Drawing.Text.TextRenderingHint.ClearTypeGridFit; Rectangle rect new Rectangle(1, 1, Width - 2, Height - 2); Color bgColor GetBackgroundColor(); Color borderColor Color.Transparent; if (IsDisabled) { bgColor Color.FromArgb(245, 245, 245); borderColor Color.FromArgb(217, 217, 217); } else if (IsPressed || _mouseDown) { bgColor PressedColor; } else if (IsSelected) { bgColor ThemeColor; // 选中态前景色变为白色更接近AntDesign主按钮 } else if (IsHovering || _mouseEnter) { bgColor HoverColor; borderColor Color.FromArgb(64, 169, 255); } using (GraphicsPath path CreateRoundedRectangle(rect, CornerRadius)) { using (SolidBrush brush new SolidBrush(bgColor)) { g.FillPath(brush, path); } if (borderColor ! Color.Transparent) { using (Pen pen new Pen(borderColor)) { g.DrawPath(pen, path); } } } // 绘制图标与文字 DrawIconAndText(g, rect); }选中态为什么不用SelectedColor而直接用ThemeColor因为工具栏里的选中语义对应AntDesign中的主按钮风格直接全填充主色前景色切换为白色和顶部导航的当前项一致。Hover色是主色的浅色调不会覆盖选中态符合状态优先级。3.3 悬停和按下时的一点点动画AntDesign的按钮悬停时颜色是渐变的在WinForm里做连续动画会引入一个Timer循环增加代码复杂度。我的做法是两点改进一是颜色直接切换而不是渐变视觉上已经足够干净二是按下时让文字和图标向下位移1像素。位移的实现很简单在DrawIconAndText方法里判断状态后对绘制原点做偏移private void DrawIconAndText(Graphics g, Rectangle rect) { bool isPressedVisual IsPressed || _mouseDown; int offsetY isPressedVisual ? 1 : 0; Rectangle contentRect new Rectangle(rect.Left, rect.Top offsetY, rect.Width, rect.Height); Color foreColor NormalForeColor; if (IsDisabled) { foreColor Color.FromArgb(180, 180, 180); } else if (IsSelected) { foreColor Color.White; } string text Text; if (string.IsNullOrEmpty(IconText)) { TextRenderer.DrawText(g, text, Font, contentRect, foreColor, TextFormatFlags.HorizontalCenter | TextFormatFlags.VerticalCenter); return; } int iconWidth 18; int totalWidth iconWidth ImageTextGap TextRenderer.MeasureText(text, Font).Width; int startX contentRect.Left (contentRect.Width - totalWidth) / 2; // 这里简化处理用IconText的第一个字符模拟图标 Rectangle iconRect new Rectangle(startX, contentRect.Top (contentRect.Height - 16) / 2, 18, 16); using (SolidBrush iconBrush new SolidBrush(foreColor)) { g.DrawString(IconText, new Font(Segoe MDL2 Assets, 14F), iconBrush, iconRect); } TextRenderer.DrawText(g, text, Font, new Rectangle(startX iconWidth ImageTextGap, contentRect.Top, contentRect.Right - startX - iconWidth - ImageTextGap, contentRect.Height), foreColor, TextFormatFlags.Left | TextFormatFlags.VerticalCenter); }Segoe MDL2 Assets是Windows自带的一套图标字体里面包含设置、主页、刷新、搜索等常见图标。用这个字体画图标不需要引入任何图片资源工具栏跑到哪里都带着一套矢量图标Dpi缩放也不会失真。DrawString配合字体家族名称传入类似的Unicode字符点如\uE74E代表文件夹就能显示对应图标。如果你不想用这个字体也可以给控件加一个Image属性绘制时调用g.DrawImage注意保持图标大小为16到20像素太大会让整个工具栏变得笨重。3.4 垂直分割线控件分割线在AntDesign中有两种一种是菜单组之间的细线一种是带空格的弱分割线。工具栏上最常用的是高度比按钮短、居中对齐的一条1像素竖线public class AToolbarSeparator : Control { public AToolbarSeparator() { SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer | ControlStyles.UserPaint, true); Size new Size(12, 28); } protected override void OnPaint(PaintEventArgs e) { base.OnPaint(e); using (Pen pen new Pen(Color.FromArgb(232, 232, 232))) { int midX Width / 2; e.Graphics.DrawLine(pen, midX, 6, midX, Height - 6); } } }高度设置为28而不是控件满高这样视觉上更精致。宽度给12像素形成一个左右各6像素的空白间隔分隔效果既有区分度又不至于产生压迫感。3.5 容器与布局水平排布和自动换行容器选择FlowLayoutPanel最省事但要关闭它的滚动条避免纵向溢出时出现难看的滚动条。工具栏按钮的总数通常不多手动排列完全够用。对容器开一个Padding属性默认设置为(12, 8, 12, 8)让按钮不顶到边缘。我在封装AToolbarContainer时只是继承FlowLayoutPanel然后写死若干布局参数public class AToolbarContainer : FlowLayoutPanel { public AToolbarContainer() { Dock DockStyle.Top; AutoSize true; FlowDirection FlowDirection.LeftToRight; WrapContents true; Padding new Padding(12, 8, 12, 8); BackColor Color.White; } public void AddButton(AToolbarButton button, int marginRight 8) { button.Margin new Padding(0, 0, marginRight, 0); Controls.Add(button); } public void AddSeparator(int marginLeft 4, int marginRight 4) { AToolbarSeparator sep new AToolbarSeparator(); sep.Margin new Padding(marginLeft, 0, marginRight, 0); Controls.Add(sep); } }这里有个细节Margin是FlowLayoutPanel里控制控件间距的唯一可靠手段。如果在AddButton里不设置Margin所有按钮会紧贴在一起视觉上非常拥挤。AntDesign的工具栏按钮间距一般在8px左右按钮之间也保持统一间隔。中间需要分组时先加一个Separator它自带左右Margin自然完成分组。3.6 鼠标事件状态维护自绘控件的鼠标状态必须在OnMouseEnter、OnMouseLeave、OnMouseDown、OnMouseUp中维护并在状态变化后调用Invalidate()。细节多了容易漏比如鼠标按下后移出控件再松开_mouseDown可能不会被重置导致按钮卡在按下态。解决方式是增加一个Capture判断鼠标离开控件时如果按着左键则依然保持按下态直到鼠标松开或捕获结束。protected override void OnMouseEnter(EventArgs e) { base.OnMouseEnter(e); _mouseEnter true; Invalidate(); } protected override void OnMouseLeave(EventArgs e) { base.OnMouseLeave(e); _mouseEnter false; if (!_mouseDown) Invalidate(); } protected override void OnMouseDown(MouseEventArgs e) { base.OnMouseDown(e); if (e.Button MouseButtons.Left) { _mouseDown true; Invalidate(); } } protected override void OnMouseUp(MouseEventArgs e) { base.OnMouseUp(e); _mouseDown false; Invalidate(); } protected override void OnEnabledChanged(EventArgs e) { base.OnEnabledChanged(e); Invalidate(); }复选框选中态类似点击按钮时切换IsSelected并触发SelectedChanged事件用于外部主窗体切换页面或执行不同的工具栏命令。注意Invalidate不要连续调用太多次否则GDI绘制开销会耗掉一两个点的CPU。每次事件触发只调用一次完全够。4. 把工具栏搬到窗体上完整接入方案控件都封装好之后接入到主窗体非常顺手。普通Windows窗体项目里你要做的就是把容器放到窗体顶部然后往里加按钮。下面这段是标准用法public partial class MainForm : Form { private AToolbarContainer _toolbar; private Panel _contentHost; public MainForm() { InitializeComponent(); SetupToolbar(); } private void SetupToolbar() { _toolbar new AToolbarContainer(); Controls.Add(_toolbar); AToolbarButton btnHome new AToolbarButton(); btnHome.Text 首页; btnHome.IconText \uE80F; // 主页图标 btnHome.IsSelected true; _toolbar.AddButton(btnHome); AToolbarButton btnRefresh new AToolbarButton(); btnRefresh.Text 刷新; btnRefresh.IconText \uE72C; // 刷新图标 _toolbar.AddButton(btnRefresh); AToolbarButton btnSettings new AToolbarButton(); btnSettings.Text 设置; btnSettings.IconText \uE713; // 齿轮图标 _toolbar.AddButton(btnSettings); _toolbar.AddSeparator(); AToolbarButton btnAbout new AToolbarButton(); btnAbout.Text 关于; _toolbar.AddButton(btnAbout); btnHome.Click (s, e) SwitchPage(home); btnRefresh.Click (s, e) RefreshData(); } }这里有三个要点第一容器的Dock Top要放在Controls.Add之前设置否则Dock属性不会立即生效你会看到容器撑满整个窗体。具体原因是WinForms的Dock在控件加入窗体时根据当前值执行一次布局之后修改Dock在某些版本里只对下一个添加到同位置的控件生效。第二容器作为窗体第一个添加的子控件时Padding内侧12px让按钮不贴边顶部8px和底部8px保证按钮的36px高度加上内边距正好是52px左右对普通标题栏窗体来说比例协调。如果你用的是自绘无边框窗体这部分高度可以适当再增加一些让视觉重心更稳定。第三按钮的Click事件不需要特殊处理。标准WinForms的控件点击事件在鼠标按下并抬起于同一控件上时触发我们已经正确维护了状态Click事件自然工作正常。5. 按钮分组与下拉功能工具栏的进阶用法工具栏不只是放几个按钮那么简单实际项目中常见的是新增按钮旁边带一个下拉小三角点击弹出更多操作比如新增用户、批量导入、导出模板。AntDesign里的Dropdown.Button体现了这种复合交互。WinForm里实现这个也不难。思路是这样的创建一个AToolbarDropDownButton继承已有的AToolbarButton在绘制右侧区域时多画一个箭头三角然后重写OnMouseClick判断点击位置是否落在右侧箭头区域如果是就弹出一个ContextMenuStrip。箭头三角的绘制需要判断点击左侧主按钮还是右侧箭头两个区域对应不同的交互。箭头区域宽度固定为20像素左侧是按钮主体。所谓带下拉的复合按钮本质上就是一个控件模拟了两个点击区域。public class AToolbarDropDownButton : AToolbarButton { public ContextMenuStrip DropDownMenu { get; set; } private const int ARROW_WIDTH 20; public AToolbarDropDownButton() { Size new Size(120, 36); } protected override void OnPaint(PaintEventArgs e) { base.OnPaint(e); // 在右侧绘制下拉箭头 Rectangle arrowRect new Rectangle(Width - ARROW_WIDTH, 0, ARROW_WIDTH, Height); Point center new Point(arrowRect.Left arrowRect.Width / 2, arrowRect.Top arrowRect.Height / 2); using (SolidBrush brush new SolidBrush(IsSelected ? Color.White : Color.FromArgb(102, 102, 102))) { Point[] triangle { new Point(center.X - 4, center.Y - 1), new Point(center.X 4, center.Y - 1), new Point(center.X, center.Y 3) }; e.Graphics.FillPolygon(brush, triangle); } } protected override void OnMouseClick(MouseEventArgs e) { if (e.Button MouseButtons.Left e.X Width - ARROW_WIDTH) { DropDownMenu?.Show(this, new Point(0, Height 2)); return; } base.OnMouseClick(e); } }三角箭头是用FillPolygon填充一个等腰三角形坐标相对中心点偏移这样可以保证箭头在任何DPI下都居中。弹菜单时使用DropDownMenu.Show(this, new Point(...))可以让菜单出现在按钮的正下方而不是鼠标位置这也更贴近AntDesign的展示方式。6. 提升质感的小细节主题色切换与高DPI适配做完整体的工具栏再打磨几个容易被忽略的小细节观感会有明显提升。6.1 主题色跟随WinForm程序通常在窗体加载时读取用户配置或配置文件把主色动态注入到工具栏按钮上。可以给容器增加一个ApplyTheme(Color primaryColor)方法遍历所有子控件把AToolbarButton上的ThemeColor统一更新。这样换肤只需要改一处配置全界面的按钮颜色跟着变。public void ApplyTheme(Color primaryColor) { foreach (Control ctrl in Controls) { if (ctrl is AToolbarButton button) { button.ThemeColor primaryColor; button.HoverColor Color.FromArgb(230, 244, 255); button.PressedColor Color.FromArgb(214, 235, 255); button.Invalidate(); } } }注意HoverColor和PressedColor如果直接由主色动态生成需要写一个ColorHelper.Lighten(Color color, float percent)方法这样换任何主色都不用手动配两套辅助色。比如HoverColor就是主色加白90%混合PressedColor是主色加白80%。用颜色线性插值实现。6.2 高DPI与字体渲染WinForm默认在系统DPI为150%时如果不做任何处理所有坐标都会被系统拉伸导致文字模糊。工具栏这类自绘控件建议在Program.cs入口处添加[STAThread] static void Main() { Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); Application.SetHighDpiMode(HighDpiMode.PerMonitorV2); Application.Run(new MainForm()); }SetHighDpiMode(HighDpiMode.PerMonitorV2)是.NET Framework 4.7及以上版本支持的API。启用后窗体会按实际DPI缩放字体会使用系统正确的缩放比例绘制。此时自绘控件的Width、Height会自动按DPI缩放绘制时Invaldate也要跟着走否则会出现控件区域缩放但内容没有重绘的残影。字体渲染抗锯齿使用TextRenderingHint.ClearTypeGridFit在Windows系统上是最清晰的文本渲染方式。如果界面背景是深色建议换成AntiAliasClearType在深色背景下会出现彩色边缘。6.3 容器背景色与窗体背景统一工具栏通常置于窗体顶部。Windows窗体默认背景色是Control也就是浅灰色。AntDesign的风格是白色或极浅灰的背景所以工具栏容器和窗体背景需要统一设置成接近的色调。常见做法是窗体背景设置为Color.FromArgb(245, 245, 245)工具栏容器背景设置为Color.White这样一深一浅工具栏自然浮出来。如果你用的是白底窗体工具栏背景也用白色中间加一条DockStyle.Top的1像素细线分隔即可。我实际做下来纯白底加细线的方式更接近AntDesign的头部风格。7. 常见问题与排查实录自绘控件从写完到真正上线跑起来总有一堆预想不到的小毛病。这里挑几个频繁踩坑点说说。7.1 控件闪烁严重症状鼠标滑过按钮时整个工具栏区域疯狂闪烁甚至留下拖影。原因默认的WinForms控件不支持双缓冲反复Invalidate时背景先被擦除再重新绘制就会产生闪烁。对策在构造函数里确保设置OptimizedDoubleBuffer和AllPaintingInWmPaint两个Style标志。这两个标志是双缓冲绘制的关键。注意UserPaint也要设置否则系统默认绘制背景双缓冲优化会被绕过。设置完这三个标志绝大多数闪烁都会消失。如果还有轻微闪烁检查是否在OnPaint里手动调用了base.OnPaint(e)这会先绘制默认背景导致双重绘制。7.2 高DPI下图标和文字错位症状在100%缩放下一切正常换到125%或150%缩放时图标偏上、文字贴底或者控件高度明显不够。原因绘制逻辑中使用了固定的像素值比如图标矩形高度写死16DPI缩放后固定值不会跟着放大。对策在绘制时统一读取DeviceDpi属性然后把固定像素换算为缩放后的值。更省事的做法是设置AutoScaleMode AutoScaleMode.Dpi并在设计器里把Font设定为9pt让字体驱动控件整体缩放。图标用字体绘制的好处是它随Font变化一起缩放不会出现错位。7.3 圆角边缘有锯齿症状圆角路径的边缘明显呈阶梯状放大看很粗糙。原因SmoothingMode没设置或者设置了HighSpeed。对策OnPaint第一行务必设置g.SmoothingMode AntiAlias。还有一个容易被忽略的点如果同时设置了不透明度小于255的画刷填充圆角部分边缘区域可能还是会出现半透明像素叠加建议背景色不要使用带透明通道的颜色。7.4 工具栏按钮变成竖排有不少网友遇到过工具栏按钮莫名其妙变成竖排的问题通常发生在窗体Resize或移动之后。FlowLayoutPanel的WrapContents默认为true当容器宽度不够的时候按钮就会换行视觉上就成了竖排或错乱。工具栏场景下我建议设置WrapContents false; AutoSize true;然后让容器Dock在顶部。若按钮数量过多超出窗体宽度可以在容器内嵌一个Panel把按钮放进去并开启横向滚动或者动态折叠次要按钮到更多下来菜单里。7.5 按钮点击后没有视觉反馈症状鼠标按下能触发事件但界面没有任何变化。原因按下状态虽然变化了但没有触发Invalidate或者触发了但OnPaint里的状态判断逻辑没有覆盖按下态。对策在OnMouseDown和OnMouseUp方法中调用Invalidate。同时检查GetBackgroundColor方法里的if-else顺序Pressed分支必须放在Selected之前否则选中态永远是蓝色按下效果被覆盖。8. 工具栏最佳实践从能用变成好用到这里一个AntDesign风格的工具栏已经完整落地了。但工具栏这类基础组件跟业务代码结合时还有一些值得注意的经验我这里一并分享。一个是按钮语义的区分。工具栏上的操作通常分为导航和动作两类。导航类操作如切换页面应该使用IsSelected来高亮当前项动作类操作如刷新、删除不应该设置IsSelected。很多项目把这两类混在一起做完后用户不知道当前在哪个页面交互体验大打折扣。工具栏上的选中态在主色背景上使用白色文字视觉权重远高于普通悬停态所以务必只让导航类按钮使用Selected。另一个是合理利用ToolTip。AntDesign的IconButton只显示图标时悬停会有一个气泡提示说明用途。WinForm原生ToolTip组件就能实现ToolTip toolTip new ToolTip(); toolTip.SetToolTip(btnRefresh, 刷新列表数据);文字按钮建议不设置ToolTip因为文字已经表达了语义加了反而冗余。纯图标按钮必须设置。还有一个细节是按钮的快捷键和焦点。工具栏按钮一般设置为TabStop false因为用户操作工具栏的路径通常是用鼠标不应当占用Tab键焦点。某些高频操作如刷新建议在窗体级别增加快捷键比如在Form的KeyPreview中监听F5刷新工具栏按钮只作为可视化入口。禁用状态的处理也很考验功力。AntDesign的禁用按钮是灰底灰字不去响应任何点击。在业务上保存、删除这类操作在无数据时应该置灰否则用户点下去会弹错误提示体验较差。你可以在更新数据后遍历工具栏按钮设置Enabled属性或者用IsDisabled属性视觉置灰但保留点击事件后者适合用来做权限控制点击后弹窗提示无权限。从性能角度说一个窗体中同时存在10个以内的自绘按钮完全无压力。如果某个工具栏需要放几十个按钮建议考虑用ListView或虚模式绘制优化不能硬堆Control。这个场景在现实项目中并不多见但一旦遇到不要觉得奇怪。9. 我的一点收尾建议我最早写这个工具栏是因为接了一个内部管理系统的改造客户验收时原话是功能都行但看起来像教学软件。后来用这套自定义绘制方案把登录框、顶部工具栏、侧边栏全部统一成了AntDesign风格客户没说界面不好看只说你们升了个版本。这就是工具栏改造的意义它不动业务逻辑但会让整套系统的印象分上一个台阶。你做的时候如果能坚持把状态优先级画对、把圆角抗锯齿处理干净、把高DPI验证到位这三件事做到位交付出去的界面质感基本不会差。剩下的就是把你自己的业务图标、主题色、字体配进去而已。这套控件的代码完全可以抽成一个独立的UI.dll后续其他WinForm项目直接引用比引整个第三方UI框架轻量得多。如果以后有时间我打算再把导航侧边栏的自绘版也整理出来。工具栏和侧边栏放在一起WinForm大户系统的现代化改造基本就能全覆盖了。
返回列表