ARTICLE DETAIL

资讯详情

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

C#自定义颜色选择器开发实战:从HSV模型到屏幕取色

C#自定义颜色选择器开发实战:从HSV模型到屏幕取色 简介一款基于C#编写的颜色选择器项目面向C#初学者或需要桌面取色功能的开发者用于实现从屏幕任意位置取色并完成颜色显示、拷贝与多种模型转换。工程围绕Windows Forms窗体展开涵盖PictureBox区域捕获、GetPixel方法读取指定坐标像素、Color结构解析及Alpha通道处理并给出了RGB与HSV模型之间相互转换的完整逻辑帮助理解颜色数据的底层表示。同时引入TrackBar刻度尺调节色相、饱和度和明度结合鼠标点击事件与实时预览控件形成即时反馈使取色、调整和结果展示三个环节能够良好配合。资源压缩包约2.07MB以源码工程文件为主体积轻量方便本地打开目前已有813人学习适合结合博客逐步跟进与二次开发。通过阅读源码读者可以掌握C#窗口程序的事件处理、图形绘制与颜色转换等核心技巧并可将相关逻辑迁移到图像处理或其他Windows小工具中。 做上位机界面或者视觉调试工具的时候我经常要给某些标记、区域、曲线选颜色。刚开始图省事直接用控件库自带的 ColorDialog用了几次就受不了了——不能直接输入 Hex 值不能吸屏幕上的颜色自定义色板操作繁琐高 DPI 下那个系统的取色对话框还会糊。后来我干脆用 C# 自己写了一个颜色选择器把日常开发里常见的选色交互全做进去了现在它已经成了我工具箱里离不开的一个小部件。这篇文章就把这个 C# 颜色选择器的完整实现过程拆开讲一遍先聊聊为什么非要自己写再按“HSV 颜色模型 → 拾色器面板绘制 → 数据转换细节 → 屏幕取色 → 与上位机/视觉场景联动”这条线把关键代码和踩坑记录写出来。适合 C# 开发者、上位机工控开发、机器视觉调试这类经常要跟颜色打交道的场景如果你正准备自己做一个类似的控件可以直接参考里面的思路和代码。1. 为什么要自己写一个颜色选择器而不是用系统自带的 ColorDialog我见过不少 C# 项目里选颜色就直接 new ColorDialog()确实省事但只要你的使用场景稍微复杂一点它的问题就会一个个冒出来。不能直接输入 Hex。做界面调试的人习惯直接贴一个 #FF8800 或者 0xFF8800 这样的值ColorDialog 没有这个入口你得先自己把 Hex 转成 RGB 再填进去。自定义颜色区体验差。ColorDialog 的自定义色块需要你先“添加到自定义颜色”然后在同一个对话框里来回切换鼠标点错一步就重置。而且这个自定义色板不会跨项目保存每次开发都要重新调。没有屏幕取色。很多时候颜色不是自己“调”出来的而是从某个截图、某个网页、某个第三方软件窗口里“吸”出来的。系统对话框完全不支持吸管操作。高 DPI 下的显示问题。在 150%、200% 缩放的 Windows 上部分旧版本系统 ColorDialog 的内部布局会错位字体发虚看着非常难受。我白天做 C# 上位机开发经常要配合海康相机、VisionMaster 这类视觉软件调参数。让操作员在一个取色工具里选一个颜色标记再把这个颜色值发给视觉软件的关键区域框这种需求用系统对话框根本没法顺畅完成。所以一个可扩展、可定制、能对接业务逻辑的 C# 颜色选择器对这类场景的价值是实打实的。自己做还有一个额外的好处——选色逻辑可以深度嵌入项目。比如在图像处理模块里你可以直接用这个控件选一个阈值范围、选一个 ROI 框的颜色、选一个缺陷标记的颜色选完的同时把 RGB/HSV/Hex 三种格式一并拿到传递给相机 SDK 或者算法层。这些是 ColorDialog 给不了你的。2. 先搞清楚 HSV拾色器才能做出“人话”交互2.1 RGB 是给机器看的HSV 是给人用的在做颜色选择器之前先要统一一个认知在 UI 交互层面HSV 远比 RGB 好用。RGB 描述的是“三种发光强度”相当于告诉屏幕“红色打多少、绿色打多少、蓝色打多少”。这种方式适合计算机处理但对人来说反直觉。你想找一个“偏亮一点的橙色”靠 RGB 就只能凭感觉试255, 155, 0改一点 G 值颜色就完全变了。HSV 的三维坐标更贴近人的感知Hue 是色相大概什么颜色Saturation 是饱和度颜色鲜艳还是灰Value 是明度整体有多亮。你可以把它理解成“在色盘上选位置再调整浓淡和亮度”。我实现的选色器交互是这样的主区域是一个二维面板横轴是 Hue纵轴是 Saturation右侧一个竖条滑块控制 Value预览框实时显示当前颜色并提供 Hex/RGB/HSV 三种数据。这种交互方式就是参考了浏览器开发者工具里的取色器用户上手零成本。2.2 HSV 与 RGB 转换中的边界细节C# 里做 HSV 转 RGB我一开始自己写分段公式后来发现System.Drawing.Color其实提供了基础方法但有一个坑必须留意Color.GetHue()返回的是 0 到 360 之间的 float而Color.FromArgb接收的是 0-255 的 int。也就是说你不能直接把界面上的 Hue200 当成一个 byte 塞进去必须先除以 360 再乘 255或者自己写独立转换。另一个容易被忽略的边界情况当 Saturation0 时无论 Hue 是多少颜色都是灰色。这意味着界面上的色相值此时对颜色没有任何影响。很多实现没处理这种边界在灰阶区域拖拽色相条预览框颜色却没有任何变化用户会以为程序坏了。核心转换代码大概长这样public static Color FromHsv(double h, double s, double v) { h (h % 360 360) % 360; // 负数归一化 s Math.Clamp(s, 0, 1); v Math.Clamp(v, 0, 1); int hi (int)(h / 60) % 6; double f h / 60 - Math.Floor(h / 60); double p v * (1 - s); double q v * (1 - f * s); double t v * (1 - (1 - f) * s); double r, g, b; switch (hi) { case 0: r v; g t; b p; break; case 1: r q; g v; b p; break; case 2: r p; g v; b t; break; case 3: r p; g q; b v; break; case 4: r t; g p; b v; break; default: r v; g p; b q; break; } return Color.FromArgb( (int)Math.Round(r * 255), (int)Math.Round(g * 255), (int)Math.Round(b * 255)); }注意代码里我加了h (h % 360 360) % 360这一行。实测会遇到外部传入负值的情况比如某些联动逻辑里 Hue 被减到了 -30如果不做归一化颜色就会突然跳到黑色或白色。3. 拾色器面板的实现从画色板到鼠标反算坐标3.1 直接 OnPaint 画渐变性能上不可行二维色板的绘制我一开始走了弯路直接在OnPaint里调用Graphics.DrawRectangle一个像素一个像素地画或者用LinearGradientBrush左上右下渐变组合。结果在高分屏和快速拖动鼠标的时候CPU 占用飙升UI 明显卡顿。后来换成了预生成 Bitmap 方案在控件尺寸固定后创建一张Bitmap用LockBits锁定像素区域遍历每个坐标根据当前 Hue 值计算对应 Saturation/Value 下的 RGB写入像素数组只在 Hue 变化或控件尺寸变化时重新生成这张图鼠标移动时只负责画一个小圆圈光标不重建色板。色板生成的核心思路类似于for (int y 0; y height; y) { for (int x 0; x width; x) { double saturation (double)x / (width - 1); double value 1.0 - (double)y / (height - 1); Color c ColorFromHsv(currentHue, saturation, value); // 写入像素数组 } }这里上下方向我把 Value 从上到下从 1 降到 0符合大多数人的直觉上面亮下面暗。如果你喜欢 Photoshop 那种上下布局可以把 Saturation 和 Value 互换没有绝对标准但一定要符合你自己的使用习惯。3.2 坐标反算把鼠标位置还原成颜色值画出色板只是完成了一半。用户用鼠标在色板上点击、拖动时我们需要从屏幕坐标反推颜色值。反算逻辑非常简单private Color PickColorFromPanel(Point p) { if (panelSize.Width 0 || panelSize.Height 0) return Color.Black; double saturation Math.Clamp((double)p.X / panelSize.Width, 0, 1); double value 1.0 - Math.Clamp((double)p.Y / panelSize.Height, 0, 1); return ColorFromHsv(currentHue, saturation, value); }但这里有个细节很多人第一次写都会漏鼠标按下和鼠标移动必须连起来处理。正确的做法是在MouseDown时调用this.Capture true;WinForms或者Mouse.Capture(this)WPF然后在MouseMove里持续更新。否则当用户按下鼠标并快速拖到色板区域之外时控件会丢失鼠标事件颜色值就停住不动了。光标圈的位置也要在Paint事件里根据当前颜色值重新绘制否则会出现光标和实际颜色不匹配的情况。画的时候我直接用反色或者黑色圆环保证在任何背景色下都能看清。4. 材质细节byte、string 与 Hex 互相折腾的常见坑4.1 byte 到 string 的精度陷阱C# 的Color.R、Color.G、Color.B都是byte类型这是一个从底层就定死的类型设计。但在颜色选择器里用户输入的是 Hex 字符串或者 0-255 的整数你必须在 int、double、string 之间反复转换。有一个非常容易犯的错直接把 double 转 int 时会丢失小数点后面的精度导致颜色值来回调整时出现可见跳变。比如你从色板上点到的颜色算出来 RGB 是 (243.8, 127.2, 11.6)如果不加四舍五入直接用(int)243.8得到 243输入框显示的颜色和实际颜色就会有一点点偏差。更麻烦的是用户手动在输入框里输入 244再切回色板发现光标位置往回跳了 0.2 个像素。这种误差虽然小但在设计对色彩要求高的界面时会让人很烦躁。所以我在所有转换位置都做了统一封装public static string RgbToHex(byte r, byte g, byte b) { return # r.ToString(X2) g.ToString(X2) b.ToString(X2); } public static bool TryParseHex(string hex, out byte r, out byte g, out byte b) { r g b 0; if (string.IsNullOrEmpty(hex)) return false; hex hex.Trim().Replace(#, ).Replace(0x, ).Replace(0X, ); if (hex.Length ! 6) return false; bool ok byte.TryParse(hex.Substring(0, 2), NumberStyles.HexNumber, null, out r) byte.TryParse(hex.Substring(2, 2), NumberStyles.HexNumber, null, out g) byte.TryParse(hex.Substring(4, 2), NumberStyles.HexNumber, null, out b); return ok; }顺带说一句ToString(X2)这个格式化串非常关键。如果直接写r.ToString(X)当颜色值小于 16 时比如红色分量是 3会输出单个字符 “3”拼出来就是 “#3FF00”长度不对再解析回来就崩了。X2保证输出两位不足补零这是 Hex 字符串拼接的基础操作。4.2 AARRGGBB 还是 RGB跟视觉 SDK 对接时更容易翻车的点C# 开发者在做上位机、视觉项目时经常要跟 OpenCVSharp、Halcon、海康相机 SDK 打交道。不同 SDK 对颜色数据的组织方式差异很大这也是颜色选择器的“下游”最容易出问题的地方。Windows 上System.Drawing.Color用Color.FromArgb(a, r, g, b)内存布局通常是 BGRA小端序。但 OpenCV 的Mat默认是三通道 BGRHalcon 的HImage是 RGBA 或 RGB 又不一样。实际测试的结论是如果你用 OpenCVSharp 给 Mat 的某个区域赋值颜色直接传Color.R、Color.G、Color.B会反色因为 OpenCV 默认是 BGR 顺序。海康相机 SDK 通过 C# 回调拿到的图像数据通常也是直接裸数据你按 RGB 顺序显示时画面会偏蓝红互换。VisionMaster 保存的配置文件里颜色字段不同版本格式不同有的是255,0,0有的是FF0000需要在外部做一个适配层。我建议在颜色选择器控件内部统一用 RGB 作为标准数据模型只在输出时根据目标端做转换。不要试图让一个颜色模型适配所有 SD K否则会把控件内部逻辑搞得很乱。5. 吸管功能屏幕取色与跨程序取色实战5.1 从 GetPixel 开始但别止步于 GetPixel吸管功能是颜色选择器的灵魂。实现屏幕取色最直观的方案是using (Bitmap bmp new Bitmap(1, 1)) { using (Graphics g Graphics.FromImage(bmp)) { g.CopyFromScreen(Cursor.Position, Point.Empty, new Size(1, 1)); } return bmp.GetPixel(0, 0); }这段代码确实能跑但实测有两个问题CopyFromScreen每次都会从 GPU / 屏幕驱动做一次全屏数据拷贝频繁调用比如鼠标每移动一个像素就取一次不仅卡而且偶尔会拿到黑屏截图尤其是在远程桌面或锁屏状态下。高 DPI 环境下Cursor.Position返回的是物理像素坐标而CopyFromScreen的参数以及窗口坐标受 DPI 缩放影响坐标会偏取到的颜色差一个甚至几个像素。我的改进方案是鼠标进入吸管模式后一次性截取整个屏幕到一张 Bitmap然后只做内存中的 GetPixel。这样鼠标移动时只是从内存 Bitmap 里读像素只有按一次才截一次屏性能完全够用。_bitmap new Bitmap(Screen.PrimaryScreen.Bounds.Width, Screen.PrimaryScreen.Bounds.Height); using (Graphics g Graphics.FromImage(_bitmap)) { g.CopyFromScreen(0, 0, 0, 0, _bitmap.Size); } // 鼠标移动到某点时 Color pixel _bitmap.GetPixel(point.X, point.Y);如果你要在多显示器环境下使用建议用Screen.AllScreens把每个屏幕截成一张或者用SystemInformation.VirtualScreen计算整个虚拟桌面范围再截取整个区域。不然副屏上的颜色取不到。5.2 全局取色钩子与自动粘贴吸管模式下需要捕获全局鼠标点击事件在 WinForms 里可以简单用MouseHook也就是SetWindowsHookEx(WH_MOUSE_LL)监听鼠标左键点击时立刻拿到当前屏幕坐标并取色同时把颜色值以指定格式复制到剪贴板。这里有一个实用的扩展取色后自动拼好目标格式并发送到焦点窗口。方法是用SendKeys.Send模拟 CtrlV把十六进制颜色值粘贴到对方窗口的输入框中。我在 VisionMaster 的 ROI 颜色配置界面里经常这么干省去了在工具和视觉软件之间来回切换的操作。Clipboard.SetText(hexString); // 通知目标窗口点击取色器上的“发送”按钮后 SendKeys.SendWait(^v);但注意SendKeys.SendWait执行时要求目标窗口处于激活状态。如果用户只是把颜色值复制到剪贴板而没有切换到目标程序发送就会落到取色器自己的窗口上然后就乱了。所以我的设计里“自动粘贴”是一个显式按钮而不是取色后的默认动作。这个习惯很重要别偷懒。6. 把选择器做成“上位机/视觉调试工具链”的一部分6.1 界面细节置顶、放大镜、历史色板基础功能做完之后我加了一些提升使用效率的细节这里挑几个值得参考的说窗口置顶。调试视觉软件的时候取色器需要一直停留在最上层。加一个TopMost切换按钮比每次都去任务栏找窗口快得多。屏幕放大镜。屏幕取色模式下主窗口旁边显示一个小窗把鼠标周围 5x5 像素放大显示并画出中心十字线。这个对精确取色太有用了尤其是目标区域只有一两个像素时比如一个细小的轮廓线没有放大镜很难选准。历史色板。把最近用过的 16 个颜色存成列表用列表控件展示点一下自动恢复。这个功能我用 JSON 保存到本地下次重启工具还能接着用反复调试 ROI 区域太方便了。6.2 典型使用场景从 VisionMaster 与海康相机调试说起有一次去客户现场调试视觉项目需要在 VisionMaster 的方案里设置缺陷区域的显示颜色还要统一相机图像里某个特征的颜色阈值。当时没有这个工具只能在 VisionMaster 自带的小色块里手动调效率极低。后来我用自己写的 C# 颜色选择器截屏取色直接把 Hex 转成 VisionMaster 的 RGB 数组格式用 UI 自动粘贴进参数框整个过程不到一分钟。后来又结合海康相机的 SDK做了一个批量设置的小工具程序启动时读取相机采集到的当前帧用这个选择器选定一个颜色区间生成灰度阈值范围内的掩膜并叠加显示在图像上。这里的颜色选择器本质上已经从“选一个展示色”变成了“选一个图像处理参数”——而这正是自定义控件能带来的最大价值它不只是 UI 组件还是业务逻辑的入口。如果你也在做 C# 上位机、机器视觉、工业软件这一类产品我建议你在早期就封装一个这样的颜色选择器控件它可以复用到项目里的很多地方日志标注、区域框选、视觉参数配置、甚至是操作界面主题定制。我最后踩过的一个小坑也要提醒一下取色器显示预览颜色时不要直接在预览控件上用BackColor color因为在高 DPI 下 BackColor 的刷子渲染会和实际颜色有一点点色差。要精确预览就自己在 OnPaint 里用Graphics.FillRectangle(new SolidBrush(color), rect)去画或者使用ColorTranslator.FromHtml得到完全等价的颜色对象。这个控件我至今还在持续迭代最近计划加入的功能是从图片文件里直接取样一个区域并统计平均色方便做视觉模板匹配时设定初始颜色阈值。C# 做这类小工具的上限其实不取决于语言本身而取决于你想把它嵌进什么样的工作流里。本文还有配套的精品资源点击获取
返回列表