news 2026/9/7 5:41:06

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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。也就是说,你不能直接把界面上的 Hue=200 当成一个 byte 塞进去,必须先除以 360 再乘 255,或者自己写独立转换。

另一个容易被忽略的边界情况:当 Saturation=0 时,无论 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.RColor.GColor.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.ColorColor.FromArgb(a, r, g, b),内存布局通常是 BGRA(小端序)。但 OpenCV 的Mat默认是三通道 BGR,Halcon 的HImage是 RGBA 或 RGB 又不一样。

实际测试的结论是:

  • 如果你用 OpenCVSharp 给 Mat 的某个区域赋值颜色,直接传Color.RColor.GColor.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); }

这段代码确实能跑,但实测有两个问题:

  1. CopyFromScreen每次都会从 GPU / 屏幕驱动做一次全屏数据拷贝,频繁调用(比如鼠标每移动一个像素就取一次)不仅卡,而且偶尔会拿到黑屏截图,尤其是在远程桌面或锁屏状态下。
  2. 高 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模拟 Ctrl+V,把十六进制颜色值粘贴到对方窗口的输入框中。我在 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# 做这类小工具的上限,其实不取决于语言本身,而取决于你想把它嵌进什么样的工作流里。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/7 5:39:54

手写原生JS响应式悬浮在线客服插件,轻量可定制

简介&#xff1a;这是一份用于网站右侧悬浮在线客服的前端插件&#xff0c;主要面向前端开发者、网站运维人员以及快速接入在线咨询功能的项目使用者&#xff0c;兼容电脑与移动设备。压缩包共三个文件&#xff0c;包含页面结构、前端交互脚本和配套图片资源&#xff0c;整体仅…

作者头像 李华
网站建设 2026/9/7 5:39:21

代码镜像化重构实践:寒冰西瓜尊提升可读性与团队协作

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/7 5:34:14

AI Agent工作台WorkBuddy上手教程:从聊天工具到干活同事

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/7 5:30:53

OpenMAIC实测:一句话生成AI课堂的部署与生成链路拆解

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/7 5:29:55

2.4G私有协议领夹麦方案:JL6976M单芯片一拖二全双工设计实践

简介&#xff1a;这是一份基于杰理JL6976M单芯片方案的2.4G无线麦克风领夹麦一拖二全双工SDK资源包&#xff0c;版本为v1.4.0_2t1&#xff0c;含软件与硬件设计资料。面向无线音频产品开发工程师、方案商及嵌入式学习者&#xff0c;适用于直播领夹麦、访谈麦克风等一对二全双工…

作者头像 李华