资讯详情

资讯详情

WinForms皮肤库CSkin源码解析:GDI+自绘控件与界面美化

简介C# WinForm 界面美化与自定义控件开发常受限于原生控件样式单一、皮肤切换和绘制扩展成本高。这份 CSkin 源码面向中高级 C# 开发者、桌面应用界面设计者及希望研读控件绘制原理的学习者提供皮肤管理、自定义按钮与窗体、事件处理、布局管理和性能优化等完整实现可直接用于项目换肤或二次开发。压缩包共 2000 个文件、约 25.24MB包含 560 个 cs 源码、846 个 gif 与 230 个 png/213 个 jpg 界面素材、35 个 resx 资源以及 22 个 config、11 个 csproj 和 18 个 dll 等工程与依赖文件css/htm/js 示例页面也一并收录源码、资源与演示文件层次较完整。已有 1859 人学习下载。读者可从中梳理皮肤配置加载、控件状态绘制、双缓冲与资源复用思路也可借鉴目录组织、事件分发和 API 设计快速搭建美观且运行稳定的 WinForm 界面并积累可迁移的 UI 编程实践。1. 接手老 WinForms 项目时的界面改造接手一个跑了七八年的 WinForms 项目最扎眼的往往不是业务逻辑是界面——灰底方角的按钮、凹陷的输入框、系统默认的标题栏客户一句“看着像上个年代的”就能让验收卡住。C# UI 库 CSkin 源码要解决的就是这件事它把 WinForms 那套原生控件整套重画了一遍按钮、输入框、下拉框、选项卡、表格、窗体边框全部换成圆角、渐变、带悬停反馈的皮肤控件同时仍然待在 .NET Framework 的 WinForms 体系里不用把几万行代码迁到 WPF。适合两类人一类是手里有存量 WinForms 项目、只想换皮不想重构的开发者另一类是想搞清楚 GDI 自绘控件到底怎么写、拿一套现成代码当模板研究的人。下面按“能干什么 → 怎么跑 → 怎么改”的顺序拆。2. 技术选型与整体架构一套自绘皮肤库是怎么搭起来的2.1 为什么是 GDI 自绘而不是换框架老项目要美化摆在面前的路其实只有三条整体迁到 WPF、买一套第三方控件库、自己重写控件的绘制。第一条路的成本不在技术在存量代码——Form 继承链、事件绑定、第三方组件、打印和报表模块随便一个环节都能让工期翻倍第二条路对个人维护的项目来说预算不现实。剩下的就是自绘而 WinForms 恰好把这条路留得很宽Control类的OnPaint是虚方法SetStyle能开双缓冲GraphicsPath能画任意形状TextRenderer能拿到和系统一致的文字渲染效果。CSkin 走的就是第三条路。它的每个控件都继承自 WinForms 原生控件或者一套自己的基类公开的属性和事件尽量和原生保持一致Text、Enabled、Click、SelectedIndex这些都不改名所以替换成本很低把Button改成SkinButton代码里button1.Text 确定一行都不用动。代价也很清楚——自绘意味着 DPI 缩放、键盘导航、无障碍、设计器支持这些原本由系统兜底的东西全部要自己补。这套库的价值一半在“画得好看”另一半在“这些坑已经有人踩过一遍并写好了兜底逻辑”。2.2 三层结构绘制基类、控件层、主题资源层把源码摊开看整体是清晰的三层。最底下是基础绘制层放的是和具体控件无关的东西圆角矩形路径的生成、颜色明暗计算、字体测量、Win32 API 的 P/Invoke 声明ReleaseCapture、SendMessage、SetClassLong、DwmExtendFrameIntoClientArea这一类。这一层的代码基本是纯函数改起来风险最低也最值得单独抽出来复用。中间是控件层一个SkinBaseControl基类把OnPaint收口子类只实现“怎么画自己”这一件事。按钮、输入框、复选、单选、进度条、选项卡这些轻量控件直接继承它表格、列表、树这类需要承载子项的则继承原生DataGridView、ListView、TreeView再重写绘制。窗体是单独一条线SkinForm继承Form处理的是非客户区那套消息。最上面是主题资源层一个静态色表加一组图片资源。控件绘制时不写死颜色全部从色表里取换主题只要改一处。2.3 目录结构与改动频率按这套库常见的组织方式目录大致是这样分的目录主要内容二次开发时的改动频率Controls/全部皮肤控件的实现按控件类型拆文件高加控件、改绘制都在这Win32/窗口消息、非客户区、DWM 相关的 P/Invoke低除非要改窗体行为Utils/圆角路径、颜色计算、图像缩放等工具方法中画新形状时会加Themes/默认色表、字体、图片资源引用高换品牌色必改Resources/图标、背景图、按钮态图片中Demo/演示工程每个控件一个示例窗体低但排查时最先看它新手容易犯的一个错是直接改Controls/里的绘制代码来适配自己的配色改到第十个控件就会发现改不动了。正确顺序永远是先动Themes/色表解决不了再考虑继承某个控件覆写绘制方法最后才去改源码。3. 核心控件拆解从基类到按钮、窗体、输入框的绘制链路3.1 绘制基类把 OnPaint 收口到一处所有轻量控件的绘制入口都收在一个基类里这是整套库最值得先读的一个文件。它干的事只有三件打开正确的绘制样式、统一 Graphics 的渲染质量、把绘制动作转成一个虚方法交给子类。public class SkinBaseControl : Control { public SkinBaseControl() { // 双缓冲 自绘 不擦背景三件套缺一个就会闪 SetStyle(ControlStyles.UserPaint | ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer | ControlStyles.ResizeRedraw | ControlStyles.SupportsTransparentBackColor, true); BackColor Color.Transparent; // 交给父容器画背景自己只画前景 } protected override void OnPaint(PaintEventArgs e) { Graphics g e.Graphics; g.SmoothingMode SmoothingMode.AntiAlias; // 圆角不锯齿靠它 g.PixelOffsetMode PixelOffsetMode.HighQuality; g.TextRenderingHint TextRenderingHint.ClearTypeGridFit; // 文字不发虚靠它 OnPaintSkin(g); } // 子类只覆写这一个方法不用再关心缓冲和渲染质量 protected virtual void OnPaintSkin(Graphics g) { } // 屏蔽系统擦背景闪烁的另一半原因在这里 protected override void WndProc(ref Message m) { const int WM_ERASEBKGND 0x0014; if (m.Msg WM_ERASEBKGND) { m.Result (IntPtr)1; return; } base.WndProc(ref m); } }ControlStyles.OptimizedDoubleBuffer和手动屏蔽WM_ERASEBKGND是配套的前者让绘制发生在缓冲区里后者阻止系统在绘制前先把背景刷成控件色。少了任何一边拖动窗口或者快速悬停时就会出现那种一闪一闪的“玄学”抖动。SupportsTransparentBackColor加BackColor Transparent的组合让控件能嵌在带背景图的父容器上而不露出灰块这是皮肤库能不能“融合”的关键开关。OnPaintSkin这个命名是个小设计它把“画什么”和“在哪画”彻底分开。子类里写绘制代码时不需要再考虑e.Graphics会不会被释放、SmoothingMode是不是默认值这类问题全部在基类解决。3.2 SkinButton四态状态机与圆角路径按钮是皮肤库里用得最多的控件也是最容易看出实现水平的地方。一个合格的皮肤按钮要维护四个状态常态、悬停、按下、禁用每个状态对应一组背景色、边框色、文字色加上圆角、可选的渐变和图标。public class SkinButton : SkinBaseControl { private bool _hover, _pressed; public int Radius { get; set; } 4; // 圆角半径0 即直角 public Color NormalColor { get; set; } SkinTheme.Primary; public Color HoverColor { get; set; } SkinTheme.PrimaryHover; public Color PressedColor { get; set; } SkinTheme.PrimaryDark; protected override void OnMouseEnter(EventArgs e) { _hover true; Invalidate(); base.OnMouseEnter(e); } protected override void OnMouseLeave(EventArgs e) { _hover false; _pressed false; Invalidate(); base.OnMouseLeave(e); } protected override void OnMouseDown(MouseEventArgs e) { if (e.Button MouseButtons.Left) { _pressed true; Invalidate(); } base.OnMouseDown(e); } protected override void OnMouseUp(MouseEventArgs e) { _pressed false; Invalidate(); base.OnMouseUp(e); } protected override void OnPaintSkin(Graphics g) { Color back !Enabled ? SkinTheme.DisabledBack : _pressed ? PressedColor : _hover ? HoverColor : NormalColor; // 内缩 1 像素否则描边会被裁掉一半 var rect new Rectangle(0, 0, Width - 1, Height - 1); using (var path GraphicsHelper.CreateRoundRect(rect, Radius)) using (var brush new SolidBrush(back)) using (var pen new Pen(SkinTheme.Border, 1f)) { g.FillPath(brush, path); g.DrawPath(pen, path); } // 文字用 TextRenderer 而不是 Graphics.DrawString清晰度差一个档次 TextRenderer.DrawText(g, Text, Font, ClientRectangle, Enabled ? SkinTheme.TextOnPrimary : SkinTheme.DisabledText, TextFormatFlags.HorizontalCenter | TextFormatFlags.VerticalCenter | TextFormatFlags.EndEllipsis); } }几个细节值得说。Invalidate()而不是Refresh()前者只是标记区域无效、等消息循环合并后重绘后者会立刻同步重绘在鼠标移动这种高频事件里用Refresh会明显卡顿。矩形内缩 1 像素是因为 GDI 的Pen以路径为中心向两侧各画一半边框宽度 1 时外侧那一半会被控件边界裁掉看起来就是“右边和下边没框”。圆角路径统一由一个工具方法生成这里面有个典型的翻车点public static GraphicsPath CreateRoundRect(Rectangle rect, int radius) { var path new GraphicsPath(); if (radius 0) { path.AddRectangle(rect); return path; } // 直径不能超过短边否则弧线会反向翻出去形成鬼畜图形 int d Math.Min(radius * 2, Math.Min(rect.Width, rect.Height)); path.AddArc(rect.X, rect.Y, d, d, 180, 90); // 左上 path.AddArc(rect.Right - d, rect.Y, d, d, 270, 90); // 右上 path.AddArc(rect.Right - d, rect.Bottom - d, d, d, 0, 90); // 右下 path.AddArc(rect.X, rect.Bottom - d, d, d, 90, 90); // 左下 path.CloseFigure(); return path; }d的钳制是必须的。一个高度只有 22 像素的按钮如果把Radius设成 15不做钳制的话d 30超过高度四条弧的圆心位置会错乱画出来是个中间凹陷的怪形状。这类尺寸相关的边界条件在皮肤库里到处都是改半径、改高度时一定要拿最小尺寸的控件试一遍。3.3 SkinForm无边框窗体的拖动、缩放与阴影窗体这块和普通控件完全是两套逻辑。SkinForm首先要设FormBorderStyle FormBorderStyle.None去掉系统标题栏然后自己画标题栏、自己处理拖动和缩放。拖动的实现方式有两种一种是拦截WM_NCHITTEST把标题栏区域直接报告成HTCAPTION剩下的交给系统另一种是在MouseDown里调ReleaseCapture加SendMessage(WM_NCLBUTTONDOWN, HTCAPTION)。前者更干净因为它顺带把双击最大化、拖动到屏幕边缘贴边这些系统行为全保留了protected override void WndProc(ref Message m) { const int WM_NCHITTEST 0x0084; const int HTCLIENT 1, HTCAPTION 2; const int HTLEFT 10, HTRIGHT 11, HTTOP 12, HTTOPLEFT 13, HTTOPRIGHT 14, HTBOTTOM 15, HTBOTTOMLEFT 16, HTBOTTOMRIGHT 17; base.WndProc(ref m); if (m.Msg ! WM_NCHITTEST || (int)m.Result ! HTCLIENT) return; // LParam 低 16 位是屏幕 X高 16 位是屏幕 Y注意是有符号的 short int lp m.LParam.ToInt32(); var screen new Point((short)(lp 0xFFFF), (short)((lp 16) 0xFFFF)); Point p PointToClient(screen); int edge 6; // 缩放热区宽度太小用户抓不住太大又容易误触 if (p.Y CaptionHeight) { m.Result (IntPtr)HTCAPTION; return; } if (p.X edge p.Y edge) m.Result (IntPtr)HTTOPLEFT; else if (p.X Width - edge p.Y edge) m.Result (IntPtr)HTTOPRIGHT; else if (p.X edge p.Y Height - edge) m.Result (IntPtr)HTBOTTOMLEFT; else if (p.X Width - edge p.Y Height - edge) m.Result (IntPtr)HTBOTTOMRIGHT; else if (p.X edge) m.Result (IntPtr)HTLEFT; else if (p.X Width - edge) m.Result (IntPtr)HTRIGHT; else if (p.Y edge) m.Result (IntPtr)HTTOP; else if (p.Y Height - edge) m.Result (IntPtr)HTBOTTOM; }LParam的解析是这段代码里最容易写错的地方。低 16 位和高 16 位拼坐标时必须转成short再取直接按int位运算在负坐标多显示器左侧屏幕上会得到一串巨大的正数表现就是副屏上拖不动窗口。这个问题在单屏开发环境里永远复现不了一上双屏就翻车。阴影是另一半。两种常见做法轻量的用SetClassLong给窗口类加上CS_DROPSHADOW0x00020000系统会给一个固定样式的小阴影实现三行代码但没法调讲究一点的用 DWM 的DwmExtendFrameIntoClientArea把边框延伸到客户区配合一层半透明的边距做阴影效果接近现代应用代价是要处理WM_DWMCOMPOSITIONCHANGED消息在系统关闭特效时降级。标题栏上的最小化、最大化、关闭三个按钮是画在OnPaint里的自绘按钮热区判定用Rectangle.Contains。要注意最大化状态和普通状态的按钮图标要切换而WindowState变化的时机在Resize之前所以状态判断不能只写在OnPaint里得在OnResize里也刷一次。3.4 SkinTextBox 与 SkinComboBox组合式控件比纯自绘更划算输入框是个例外它不适合纯自绘。原因很实际输入法候选框定位、文本选区、右键菜单、密码掩码、CtrlA/C/V、拖动选中这一整套交互系统原生TextBox已经做得很完整自己重写一遍既费力又难对齐。这套库采用的是组合式做法——外面套一个负责画圆角背景和边框的容器里面塞一个去边框的原生TextBox。public class SkinTextBox : SkinBaseControl { private readonly TextBox _inner new TextBox(); public SkinTextBox() { _inner.BorderStyle BorderStyle.None; // 去掉系统边框 _inner.BackColor Color.White; _inner.Font SkinTheme.DefaultFont; Controls.Add(_inner); Padding new Padding(8, 6, 8, 6); // 内边距决定文字离边框多远 Height 30; // 单行输入框给个默认高度 } protected override void OnLayout(LayoutEventArgs e) { base.OnLayout(e); // 内层文本框严格贴合内边距多一个像素都会露出白边 _inner.Bounds new Rectangle(Padding.Left, Padding.Top, Width - Padding.Horizontal, Height - Padding.Vertical); } protected override void OnPaintSkin(Graphics g) { var rect new Rectangle(0, 0, Width - 1, Height - 1); using (var path GraphicsHelper.CreateRoundRect(rect, 4)) using (var brush new SolidBrush(Enabled ? Color.White : SkinTheme.DisabledBack)) using (var pen new Pen(_inner.Focused ? SkinTheme.Primary : SkinTheme.Border)) { g.FillPath(brush, path); g.DrawPath(pen, path); } } }焦点边框靠_inner.Focused判断但TextBox的焦点事件不会自动通知父容器所以还得挂_inner.GotFocus和_inner.LostFocus去调Invalidate()否则点进去以后边框颜色不变用户完全感知不到焦点在哪。下拉框比这复杂一层因为ComboBox的下拉列表是系统弹出的独立窗口样式改不了。常见做法是彻底放弃原生ComboBox用“一个只读输入框 一个自绘按钮 一个无边框Form承载ListBox”拼出来。这个无边框 Form 要设ShowInTaskbar false、TopMost true并且在失焦时自动关闭否则点到别处它不会消失。4. 把项目跑起来环境、编译与第一个皮肤窗体4.1 环境准备与工程打开这套源码是 .NET Framework 的 WinForms 工程目标框架一般落在 4.0 到 4.8 之间。开发机要装的是对应版本的 .NET Framework 开发包和 Visual Studio2017 及以上都能打开工程文件格式是老式的 csprojVS 会自动做一次单向升级提示选“确定”即可不影响源码。打开后先别急着 F5看一眼解决方案结构通常是一个类库工程加一个 Demo 工程。类库工程是要引用的本体Demo 是用来验证的示例集合。第一步先确认类库工程能单独编译通过这一步能过滤掉大部分环境问题——引用路径失效、资源文件丢失、目标框架版本不匹配都会在这一步暴露。需要注意的一点如果源码里带了Resources下的图片资源而工程文件用的是相对路径引用换一台机器解压后路径可能对不上。表现是编译报“找不到类型或命名空间”实际是资源没加载进来。解决办法是在解决方案资源管理器里点开Properties/Resources.resx看有没有红色报错项有就重新指定一下文件路径。4.2 编译顺序与第一次运行编译顺序是先类库后 Demo。命令行操作的话# 先还原并编译类库工程名按实际目录调整 msbuild CSkin\CSkin.csproj /p:ConfigurationRelease /p:PlatformAny CPU # 再编译 Demo它通过项目引用拿到上面那个 dll msbuild Demo\Demo.csproj /p:ConfigurationRelease用msbuild而不是dotnet build是因为老式 csproj 里的资源、设计器、COM 引用这些dotnet命令行工具链处理不完整很容易出现“VS 里能编、命令行编不过”的情况。第一次运行 Demo 时把每个控件示例窗体都点一遍重点看三件事窗体拖动是否跟手、按钮悬停有没有延迟、输入框能否正常输入中文。这三项分别对应消息处理、Invalidate调用频率和输入法兼容性是这套库最容易出问题的地方。4.3 把控件拖进自己的窗体工具箱与属性面板在类库编译通过之后把生成的 dll 添加到工具箱就能在设计器里拖拽使用了。操作路径是打开工具箱 → 右键任意分组 → “选择项” → 浏览到编译输出的 dll → 确定。之后所有Skin开头的控件会出现在工具箱里。如果拖进去的控件在设计器里显示成一个空白方块或者灰色矩形别慌这不一定是 bug。自绘控件在设计期和运行期走的是不同的绘制路径设计器为了性能会跳过一部分自绘逻辑所以设计视图里看起来“没样式”是正常的按 F5 跑起来才是真实效果。这套库通常会给控件加一个设计期判断DesignMode为真时走简化绘制避免设计器卡顿。实际在自己项目里用的时候窗体基类要改一下// 原来public partial class MainForm : Form public partial class MainForm : SkinForm { public MainForm() { InitializeComponent(); // 全局主题色一行改完全部控件 SkinTheme.Apply(Color.FromArgb(0, 153, 204)); CaptionHeight 36; // 标题栏高度影响拖动热区 RoundCap true; // 窗体圆角 skinButton1.Radius 4; } }继承关系一改窗体边框就没了标题栏、最小化最大化关闭按钮全部换成自绘版本原生Form的Text、Icon属性依然是生效的不用改调用方代码。这是这套库设计上最舒服的一点。4.4 部署阶段的三个必查项本地跑通和部署到客户机器上是两件事。第一项是目标框架客户机器上必须装了对应版本的 .NET Framework4.0 和 4.8 不通用装错了的表现是双击没反应或者弹一个语焉不详的初始化失败。第二项是高 DPI 清单不声明 DPI 感知的话在 125% 或 150% 缩放的笔记本上整个界面会被系统做位图拉伸圆角和文字糊成一片。第三项是图标和字体自绘标题栏的图标要单独设不能用Form.Icon自动缩放否则大尺寸下会拉花。高 DPI 需要在工程里加一个app.manifest?xml version1.0 encodingutf-8? assembly manifestVersion1.0 xmlnsurn:schemas-microsoft-com:asm.v1 application xmlnsurn:schemas-microsoft-com:asm.v3 windowsSettings !-- 声明 PerMonitorV2让系统不要做位图拉伸 -- dpiAwareness xmlnshttp://schemas.microsoft.com/SMI/2016/WindowsSettingsPerMonitorV2/dpiAwareness dpiAware xmlnshttp://schemas.microsoft.com/SMI/2005/WindowsSettingstrue/dpiAware /windowsSettings /application /assembly注意加了清单之后必须在代码里处理DpiChanged事件重新计算尺寸否则从 100% 屏幕拖到 150% 屏幕时控件不会自动放大界面会缩在左上角一小块。这是从“看着模糊”变成“看着错位”问题性质变了很多人到这一步才发现。提示PerMonitorV2只在 Windows 10 1703 及以上生效老系统会退回dpiAware的行为测试时最好找一台老系统机器过一遍。5. 避坑与排查皮肤控件的高频翻车现场5.1 拖进窗体后控件是一块灰方块或者黑块现象控件在设计器里是灰块运行起来依然是一整块纯色没有圆角、没有边框、没有文字。原因八成是BackColor被设成了不透明色并覆盖了整个客户区。自绘控件如果先被系统刷了一遍背景而OnPaint里又没把整个区域重画露出来的就是那层背景色。另一种可能是控件被Panel之类的父容器裁剪了Padding设得太大导致实际绘制区域是负尺寸。解决确认控件构造函数里有SetStyle(..., true)那一串以及BackColor Color.Transparent。如果是继承自原生控件的皮肤版比如SkinDataGridView检查有没有在OnPaint里调用base.OnPaint之后再绘制原生控件的绘制顺序和纯自绘控件是反的。5.2 窗体最大化后盖住了任务栏现象点最大化窗口铺满整个屏幕底部任务栏被压住开始菜单弹不出来。原因FormBorderStyle None之后系统不再按“工作区”计算最大化尺寸而是按整个屏幕算。有些实现里还会手动把Bounds设成Screen.PrimaryScreen.Bounds那就是彻底盖住了。解决不要手动设尺寸重写WM_GETMINMAXINFO0x0024消息把MaxPosition和MaxSize按当前显示器的工作区Screen.FromControl(this).WorkingArea填回去。多显示器场景下还要用MonitorFromWindow拿到窗口所在的那块屏而不是永远用主屏否则副屏最大化会跑到主屏尺寸上去。5.3 高 DPI 下圆角发虚、1 像素边框粗细不一现象125% 缩放下按钮圆角边缘有明显的灰色毛边左右两条边框一条粗一条细。原因GDI 的坐标是浮点GraphicsPath在非整数坐标上做抗锯齿会产生半透明像素。缩放比例不是 100% 时Width - 1这个计算出来的边界落在小数位置上描边就被分摊到了两个像素上。粗细不一是因为左边界在整数上、右边界在 .5 上。解决给Graphics设PixelOffsetMode.Half或者干脆所有坐标用Math.Round对齐到整像素边框宽度不要用 1用1f并且确认Pen.Alignment是Inset而不是默认的Center。这套库一般会在基类里统一做像素对齐自己加控件时别忘了这一步。5.4 设计器报“找不到类型”控件从工具箱消失现象打开窗体设计器弹一堆红字错误页之前拖进去的皮肤控件变成一个个报错框。原因设计器加载控件时会真实实例化它们如果控件的构造函数里有依赖运行环境的东西读配置文件、连数据库、访问网络路径的图片在设计期就会抛异常。另一种常见情况是类库的目标框架和主工程不一致设计器加载不了。解决构造函数里只做初始化任何外部依赖都推迟到OnHandleCreated或者第一次绘制时。必须区分设计期的用Control.DesignMode或者LicenseManager.UsageMode LicenseUsageMode.Designtime判断。框架版本不一致的把两个工程的 TargetFramework 对齐。5.5 表格滚动时闪成一片快速拖动滚动条尤其明显现象SkinDataGridView或皮肤列表滚动时整个控件白闪滚动条拖快了几乎看不清内容。原因原生DataGridView的双缓冲默认是关的SetStyle在构造之后调用也不一定生效因为控件内部有自己的绘制管线。另外单元格的Style.BackColor如果设成了透明每滚一行都要向父容器取背景开销翻倍。解决反射打开DataGridView的私有DoubleBuffered属性或者在自定义子类里重写CreateParams加上WS_EX_COMPOSITED。单元格背景给具体颜色不要留Color.Transparent。滚动时逐行重绘而不是整体重绘性能差距能到三四倍。注意WS_EX_COMPOSITED会让整个控件的绘制走另一条路在某些老显卡驱动上反而更卡上生产前在目标机器上实测一下再决定。6. 二次开发把 CSkin 改成自己产品的一套皮肤真正用起来改源码是迟早的事。最常见的需求是加一个库里没有的控件比如一个带左侧色条的标签或者一个开关按钮。这时候别从Control开始写直接继承SkinBaseControl只实现OnPaintSkin// 带左侧色条的列表项标签色条颜色可在属性面板里调 public class SkinAccentLabel : SkinBaseControl { public int AccentWidth { get; set; } 3; public Color AccentColor { get; set; } SkinTheme.Primary; public SkinAccentLabel() { Height 28; } protected override void OnPaintSkin(Graphics g) { // 先画左侧色条再留给文字区域顺序反了会被盖住 using (var brush new SolidBrush(AccentColor)) g.FillRectangle(brush, 0, 0, AccentWidth, Height); TextRenderer.DrawText(g, Text, Font, new Rectangle(AccentWidth 8, 0, Width - AccentWidth - 8, Height), SkinTheme.Text, TextFormatFlags.VerticalCenter | TextFormatFlags.EndEllipsis); } }写完在类库工程里重新编译从工具箱“选择项”重新加载一次 dll新控件就出现在工具箱里了属性面板里能看到AccentWidth和AccentColor和库自带的控件体验一致。这是继承基类的好处——设计期支持、双缓冲、透明背景这些东西全部继承下来了。改主题色的验证清单我一般会固定走一遍因为颜色这种改动最容易“改一处崩一处”检查项为什么容易漏悬停态、按下态、禁用态只改了常态色其余三态还是旧色鼠标一移就露馅文字对比度主色改浅了白字就看不清要连同TextOnPrimary一起调边框和分隔线硬编码在某个控件里的灰色边框不会跟着变图标资源图标是位图颜色改不了只能整套换那个对比度的问题我吃过一次亏把主题色从一个深蓝调成偏亮的青本地看着挺精神交付后客户反馈“按钮上的字看不清”。后来我把这条写进检查项每次换色都用一张四态截图逐个看。现在我的习惯是动源码之前先把Themes/里那个色表整个抄一份放自己工程的配置类里所有控件引用自己那份源码只当成绘制逻辑的参考。这样以后升级库、或者客户临时要第二套配色都不用再回头翻一遍所有控件的绘制代码。希望帮到你。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →