尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
用C#实现屏幕取色器:像素读取、DPI缩放与多屏适配实战
简介这份C#拾色器工程资源面向使用Windows窗体进行桌面应用开发的C#学习者和开发者适用于界面设计、图像处理或取色工具开发等需要快速获取屏幕特定点颜色的场景。压缩包大小约2.07MB文件明细暂未标注已有814人浏览学习。项目实现覆盖了屏幕像素取色、红绿蓝与HSV及CMYK等颜色模型的换算、可滑动的刻度尺调色、滑块调节、实时预览以及事件驱动的界面更新等关键技术同时遵循模型-视图-控制器或模型-视图-视图模型等架构来组织代码便于读者理解C#图形界面开发中的控件交互与事件处理机制。借助该工程可掌握通过图形设备接口获取指定坐标像素颜色、利用颜色结构进行格式转换以及Windows窗体界面同步更新的实战思路是一份适合上手研读的C#桌面应用示例。1. 用 C# 做颜色选择器真正意义上的取色器难点从来不在 GetPixel用 C# 做颜色选择器很多人的第一反应是拖一个 ColorDialog 出来但那是选色不是取色。取色器要从屏幕像素里把颜色真正读出来背后还牵动光标位置、DPI 缩放、多显示器负坐标、窗口自遮挡和颜色字节序这几个容易被忽略的环节。这套用 C# 实现的方案包含跟随鼠标的放大镜、全局快捷键和一键复制 HEX/RGB/HSL 的能力做成一个不依赖第三方控件的轻量组件可以直接嵌进设计辅助工具或 UI 自动化脚本。想自建取色器、又不想被开源库里一堆历史包袱捆住手脚的开发者顺着这篇文章的思路走一遍就能明白源码里每个文件为什么这么安排。2. 取色器里的颜色是怎么来的GDI 像素读取、坐标换算与刷新频率取色器要回答的问题只有一个屏幕上某个像素到底是什么颜色。可 Windows 的绘图模型里屏幕不是一张能随意访问的位图而是一组与窗口关联的 DC设备上下文。颜色得先从“桌面 DC”或“屏幕拷贝”里拿出来才能转成界面上常见的 Color 对象。实际的取色链路比我预想的长鼠标位置要先拿到再换算成物理像素坐标然后调 GDI 读取最后把 COLORREF 转成 RGB。任何一步的坐标系假设不同最终值都会差出好几个像素。2.1 先理解 COLORREF、DC 和像素字节顺序不同 UI 框架里颜色通常以 COLORREF 结构表示数字布局是 0x00BBGGRR。这里有个非常容易踩的坑低字节实际是 R高字节是 G再往上是 B。如果你按平时写字符串的习惯把 GetPixel 的返回值直接拆成 R、G、B红色按钮会取成蓝色蓝色按钮会取成红色。取色器能通过 GDI 的GetDC(IntPtr.Zero)拿到桌面表面的句柄再用GetPixel读取指定坐标的颜色。需要强调的是这里的坐标必须是物理像素不是窗口客户区坐标。很多刚开始写取色器的人把鼠标的Location直接塞给GetPixel在 125% 或 150% 缩放的屏幕上读到的总是隔壁像素这就是坐标换算出了问题。我一般会在工程里定义一个静态类ScreenColorReader专门负责 DC 的获取、读取和释放把所有 Win32 调用集中在一个文件里。这样后续排查句柄泄漏、做单元测试都方便比散落在窗体代码里好维护得多。2.2 两种读取实现GetPixel 单点读取与 CopyFromScreen 区域抓取取色最简单的做法是直接用 GetPixel代码量短适合高频采样少数几个点using System; using System.Drawing; using System.Runtime.InteropServices; public static class ScreenColorReader { [DllImport(user32.dll)] private static extern IntPtr GetDC(IntPtr hwnd); [DllImport(user32.dll)] private static extern int ReleaseDC(IntPtr hwnd, IntPtr hdc); [DllImport(gdi32.dll)] private static extern uint GetPixel(IntPtr hdc, int x, int y); public static Color ReadPixel(Point pt) { IntPtr desktopHdc GetDC(IntPtr.Zero); try { uint colorRef GetPixel(desktopHdc, pt.X, pt.Y); return ColorTranslator.FromWin32((int)colorRef); } finally { ReleaseDC(IntPtr.Zero, desktopHdc); } } }代码逻辑很直接GetDC(IntPtr.Zero)返回桌面 DCtry/finally 保证每次读取都会释放句柄GetPixel返回 COLORREF再交给ColorTranslator.FromWin32转成 System.Drawing.Color避免手拆字节序。参数上要注意pt必须是物理屏幕坐标主屏左上角是 (0,0)扩展屏在左侧时可能出现负坐标。GetPixel 速度快但在某些全屏独占、硬件覆盖窗口中会读到旧帧或黑块。如果需要一次取一个区域更稳妥的做法是先CopyFromScreen抓帧再从位图里读public static Bitmap CaptureRegion(Rectangle region) { var bmp new Bitmap(region.Width, region.Height); using (Graphics g Graphics.FromImage(bmp)) { g.CopyFromScreen(region.Location, Point.Empty, region.Size); } return bmp; }region.Location表示屏幕上的左上角起点Point.Empty是目标位图的插入原点region.Size是要复制的宽高。抓帧和取色分离适合“放大镜”这类需要连续看多个像素的场景也能减少临时对象分配拖拽时的卡顿感会明显改善。2.3 取色频率与 UI 节流用定时器代替 MouseMove 高频调用不建议在 MouseMove 事件里直接取色。鼠标每动一个像素就可能触发几十次事件每次再做一次 GDI 调用或抓帧UI 线程很快就被拖垮。常见做法是用一个 50ms 左右的 Timer 做节流在 Tick 里统一读光标位置、取色、刷新预览private Timer _sampler; private Point _cursorPos; private void InitSampler() { _sampler new Timer { Interval 50 }; _sampler.Tick (s, e) { _cursorPos Cursor.Position; Color c ScreenColorReader.ReadPixel(_cursorPos); previewBox.BackColor c; hexBox.Text $#{c.R:X2}{c.G:X2}{c.B:X2}; }; _sampler.Start(); }Interval 50表示每 50ms 采样一次大约是 20Hz。对鼠标移动预览来说20Hz 足够顺滑同时把 GDI 调用频率控制在安全范围内。把取色和 UI 刷新放在同一个 Tick 里逻辑上也简单不会出现取色结果和预览不同步的奇怪问题。3. 把取色器做成可用的工具无边框窗体、放大镜与锁存交互只有取色函数还不够。一个能真正日常使用的取色器至少得解决三个交互问题窗体怎么做到不挡路、放大镜里显示什么、按下快捷键时怎么避免取到自己窗体。这一部分做完取色器才有工具的样子而不是一个黑盒命令。3.1 无边框置顶窗体与鼠标穿透的取舍取色器需要一个跟随鼠标移动的提示窗体但又不能挡住用户看目标像素。常见做法是设置FormBorderStyle.None加TopMost true只显示取色区域的小块预览。若还想让鼠标直接穿透这个窗体可以在 CreateParams 里加一个扩展样式protected override CreateParams CreateParams { get { CreateParams cp base.CreateParams; cp.ExStyle | 0x00000020; // WS_EX_TRANSPARENT鼠标穿透 return cp; } }设置WS_EX_TRANSPARENT后窗体自身无法再接收鼠标事件因此“穿透预览模式”和“可拖动模式”之间必须有一个切换开关不能同时生效。实际项目中我通常默认不穿透因为用户还需要拖动取色器本身等到锁定取色那一刻再把窗体隐藏效果反而更干净。3.2 放大镜区域抓取、最近邻放大与中心十字标记放大镜是取色器里最直观的模块。把鼠标周围 24x24 像素的区域放大 8 倍画在约 384x384 的预览控件上让像素块边缘保持分明。这里必须使用 NearestNeighbor 插值其他插值方式会把像素边界模糊成过渡色拾色参考价值就没了。private void RenderZoomView(Point cursorPos) { int radius 24; int scale 8; Rectangle srcRect new Rectangle( cursorPos.X - radius, cursorPos.Y - radius, radius * 2, radius * 2); using Bitmap src ScreenColorReader.CaptureRegion(srcRect); using Bitmap zoomed new Bitmap(radius * 2 * scale, radius * 2 * scale); using (Graphics g Graphics.FromImage(zoomed)) { g.InterpolationMode InterpolationMode.NearestNeighbor; g.DrawImage(src, 0, 0, zoomed.Width, zoomed.Height); using var pen new Pen(Color.White, 2f); var center new Point(zoomed.Width / 2, zoomed.Height / 2); g.DrawLine(pen, center.X, 0, center.X, zoomed.Height); g.DrawLine(pen, 0, center.Y, zoomed.Width, center.Y); } zoomPictureBox.Image?.Dispose(); zoomPictureBox.Image (Bitmap)zoomed.Clone(); }srcRect决定放大镜的来源区域scale是放大倍数8 倍对高分屏来说比较合适。中心十字线帮助用户对准像素。代码最后用 Clone 把位图交给 PictureBox是因为zoomed离开 using 块就会被释放直接赋值会导致预览区域黑屏。这个细节不处理放大镜经常跑到一半就消失了。3.3 锁定取色先隐藏窗体再延时抓取一个常见翻车现场是用户按 CtrlShiftC 取色结果取到的是取色器自己的放大镜边框颜色。原因很简单取色器窗口盖住了目标像素。解决思路不是把窗体做透明而是拆成两步先隐藏能挡路的 UI等桌面刷新后再抓取。private async void TriggerPick() { _isPicking true; this.Hide(); await Task.Delay(120); var pos Cursor.Position; _lockedColor ScreenColorReader.ReadPixel(pos); Clipboard.SetText(_lockedColor.ToHex()); this.Show(); _isPicking false; }120ms 的延时差不多是 8 帧足够让 DWM 把无边框窗体从画面上移除。代价是界面会闪一下但比起每次都取到自己窗体的颜色这个体验好得多。从那以后凡是做悬浮取色类工具我都会把“锁存前先隐藏自己”当成默认习惯写进流程。4. 取色器避坑DPI 缩放、窗口自遮挡、字节序与负坐标这一章整理了我在几个仿制项目里遇到频率最高的四个问题全部按“现象、原因、解决”来记录。把这些坑提前排掉后面加功能才能安心。4.1 现象125% 缩放屏上取色结果偏出几个像素原因逻辑坐标系和物理像素没对齐解决声明 DPI 感知或改用物理坐标 API在系统缩放为 125% 或 150% 的屏幕上鼠标停在一个图标上取色器读到的却是附近背景色偏移量会随缩放比例变化。原因在于 Windows 默认对 WinForms 做 DPI 虚拟化Cursor.Position 返回的是逻辑坐标而 GetPixel 和 CopyFromScreen 需要物理像素坐标两边没有换算就出现偏差。解决方法是进程启动时先申请 DPI 感知让 WinForms 和 GDI 使用同一套物理像素[DllImport(user32.dll)] private static extern bool SetProcessDPIAware(); [STAThread] static void Main() { if (Environment.OSVersion.Version.Major 6) { SetProcessDPIAware(); } Application.EnableVisualStyles(); Application.Run(new PickerForm()); }另一种更可控的方式是在 app.manifest 里声明dpiAwaretrue/pm/dpiAware由系统在进程启动阶段完成初始化避免“代码已经跑起来但 DPI 感知还没生效”的竞态。两种方式选一种用不要重复调用。4.2 现象取到的颜色一直是自己窗体的颜色原因取色器自遮挡目标像素解决隐藏后延时取色或基于一帧快照取色按下快捷键后颜色值稳定地等于放大镜边框或半透明背景的颜色和鼠标指向的目标毫无关系。原因是目标像素被取色器自己的窗体盖住了屏幕表层显示的自然是这个工具自身。参考 3.3 隐藏窗体的方法可以解决单次取色。若想连续读取多个点可以先抓一帧全屏快照再统一从快照中取色private Bitmap _snapshot; private void CaptureSnapshot() { Rectangle bounds SystemInformation.VirtualScreen; _snapshot?.Dispose(); _snapshot new Bitmap(bounds.Width, bounds.Height); using (Graphics g Graphics.FromImage(_snapshot)) { g.CopyFromScreen(bounds.X, bounds.Y, 0, 0, bounds.Size); } }快照方案的优点是画面稳定允许用户取色过程中移动鼠标而不用反复抓屏缺点是一旦屏幕内容变化快照会过时适合锁定取色那一小段操作。4.3 现象复制出的 HEX 在别的软件里红蓝互换原因COLORREF 字节序被误拆解决区分 COLORREF 与 RGB 的存储顺序用取色器从红色按钮上取色如果自己写代码拆 GetPixel 的返回值红色经常会变成蓝色。原因是 GetPixel 返回的是 0x00BBGGRR 结构和你平常在字符串里写的 RGB 正好相反。如果按“高位是 R、低位是 B”的直觉去拆纯红和纯蓝一定会互换。最简单的处理是直接用ColorTranslator.FromWin32((int)colorRef)不要手拆。如果必须自己拆按这个顺序int r (int)(colorRef 0x000000FF); int g (int)((colorRef 8) 0x000000FF); int b (int)((colorRef 16) 0x000000FF);转换后用 Color.FromArgb(r, g, b) 组装才和屏幕实际颜色一致。取色函数写完后最好先用纯红、纯绿、纯蓝三个色值跑一遍单元测试确认通道没有颠倒。4.4 现象副屏在主屏左侧时取色区域变黑或整体偏移原因负坐标被裁剪解决用虚拟屏幕边界裁剪矩形双屏用户常把副屏放在左边从扩展屏取色时放大镜左侧一整片是黑边取到的颜色也不是鼠标所指的位置。原因是副屏左上角坐标可能是负值比如 (-1920, 0)取色矩形如果从 0 开始构造负坐标部分在 CopyFromScreen 里会被当成裁剪区域直接丢弃。解决方法是取区域前用 SystemInformation.VirtualScreen 做一次交集裁剪Rectangle region new Rectangle( cursor.X - radius, cursor.Y - radius, radius * 2, radius * 2); Rectangle virtualBounds SystemInformation.VirtualScreen; region.Intersect(virtualBounds); if (region.Width 0 || region.Height 0) return;radius决定取色范围半径。多显示器环境下建议把 radius 做成可配置项避免用户在超宽副屏上取色时裁剪后区域太小导致无法定位。5. 把取色器变成顺手工具格式互转、历史记录和全局快捷键取色器不能只是一个放大镜。要日常顺手还得把颜色无缝喂给设计软件、代码编辑器和配色文档。这一章补上三块我最后一定会加的扩展功能。5.1 HEX、RGB、HSL 互转取到一个颜色三种表达同步出现设计场景常用 HEX数值分析常用 RGB配色微调常用 HSL。取色后只显示一个 HEX 肯定不够至少要同步算出三种格式。这里给一个常用的 RGB 转 HSL 实现public static (double h, double s, double l) RgbToHsl(Color c) { double r c.R / 255.0; double g c.G / 255.0; double b c.B / 255.0; double max Math.Max(r, Math.Max(g, b)); double min Math.Min(r, Math.Min(g, b)); double l (max min) / 2.0; if (Math.Abs(max - min) 1e-9) return (0, 0, l); double delta max - min; double s l 0.5 ? delta / (2.0 - max - min) : delta / (max min); double h; if (max r) h (g - b) / delta (g b ? 6 : 0); else if (max g) h (b - r) / delta 2; else h (r - g) / delta 4; return (h * 60, s, l); }参数说明h范围是 0 到 360 度s和l是 0 到 1 的浮点数。灰色判断用了1e-9而不是直接等值比较因为浮点运算有误差直接等值判断会漏掉接近灰的颜色。反向做 HSL 转 RGB 时也要对接近 0 的饱和度和亮度做容差处理否则会产生不合理的 RGB 值。5.2 历史记录JSON 持久化比数据库更适合轻量取色器我习惯把最近几次取色记录保存成一个列表每条记录包含 HEX、时间戳和备注格式用 JSON 完全足够public class ColorRecord { public string Hex { get; set; } public DateTime PickedAt { get; set; } public static void Save(ListColorRecord records, string path) { var json JsonSerializer.Serialize(records, new JsonSerializerOptions { WriteIndented true }); File.WriteAllText(path, json); } public static ListColorRecord Load(string path) { if (!File.Exists(path)) return new ListColorRecord(); return JsonSerializer.DeserializeListColorRecord(File.ReadAllText(path)); } }WriteIndented true让记录文件可读性更好方便手动改备注序列化直接使用属性名作为字段名如果以后想增加一个Version字段升级时做一次数据迁移就能平滑过渡。取色器一天最多百来条记录JSON 完全撑得住。只有当记录量超过几千条或需要并发查询时才有必要换成 SQLite。5.3 注册全局快捷键把 CtrlShiftC 留给取色器没有全局快捷键的取色器谈不上效率。WinForms 里可以用 RegisterHotKey它需要一个窗口句柄来接收 WM_HOTKEY 消息[DllImport(user32.dll)] private static extern bool RegisterHotKey(IntPtr hWnd, int id, uint fsModifiers, uint vk); private const int HOTKEY_ID 0x3001; private const uint MOD_CONTROL 0x0002; private const uint MOD_SHIFT 0x0004; protected override void OnLoad(EventArgs e) { base.OnLoad(e); RegisterHotKey(Handle, HOTKEY_ID, MOD_CONTROL | MOD_SHIFT, 0x43); // 0x43 C } protected override void WndProc(ref Message m) { const int WM_HOTKEY 0x0312; if (m.Msg WM_HOTKEY m.WParam.ToInt32() HOTKEY_ID) { TriggerPick(); return; } base.WndProc(ref m); }0x43 是 C 键的虚拟键码MOD_CONTROL 和 MOD_SHIFT 对应 CtrlShift 组合。TriggerPick 在 UI 线程中执行不能做长时间阻塞如果取色前要先抓全屏快照建议开后台线程完成后回到 UI 线程更新预览。RegisterHotKey 有个坑快捷键注册后其他应用再按同一个组合不会触发自己的事件程序退出前要记得调用 UnregisterHotKey否则系统会残留注册信息。窗口句柄还没创建时注册也会失败所以放在 OnLoad 里最稳。6. 验证取色器的准头一张基准色卡把 RGB 通道差异量出来取色器最容易出问题的地方不是像素本身而是字节序和坐标换算。与其靠肉眼一次次试错不如做一张基准色卡把取色结果和期望值挨个对比用 RGB 距离把误差量化出来。var samples new[] { (Color.FromArgb(255, 0, 0), 纯红), (Color.FromArgb(0, 255, 0), 纯绿), (Color.FromArgb(0, 0, 255), 纯蓝), (Color.FromArgb(255, 255, 255), 纯白), (Color.FromArgb(0, 0, 0), 纯黑), (Color.FromArgb(128, 128, 128), 中性灰) }; foreach (var (expected, name) in samples) { Color picked PickAt(currentPosition); int distance Math.Abs(picked.R - expected.R) Math.Abs(picked.G - expected.G) Math.Abs(picked.B - expected.B); Console.WriteLine(${name}: distance{distance}); }判断标准我一般放宽到 3RGB 三个通道距离总和小于等于 3属于正常误差大于 3基本就是坐标偏移或字节序问题。跑测试时要做两组验证一组在主屏一组在副屏顺手把负坐标相关的代码也覆盖掉。另一种更干净的方式是用截图工具导出一张已知颜色的纯色图片再把取色器对准图片中心这样环境里没有其他窗口干扰定位也更精确。从我经手的几个工具型项目来看取色器的问题往往不在 GetPixel 本身而在“取的是哪一层画面、用的是哪个坐标系、前面有没有自遮挡”。把这些问题想清楚后再写 UI效率会高很多。从那以后我每次改完 DPI 感知或多屏相关代码都会强制把这张基准色卡从头跑一遍确认六个样本全部通过才继续加功能。希望帮到你。本文还有配套的精品资源点击获取
RELATED

相关推荐

AI编程助手行为审计:Gryph本地监控实践

AI编程助手行为审计:Gryph本地监控实践

Gryph最初是我们团队内部为了解决一个具体到让人频繁失眠的问题而动手写的:AI编程助手已经进入日常开发流程的骨髓,可是我们发现,没有人能清晰地回答“我的代码到底被发到哪里去了”“它读过的本地文件里有没有不该被读取的东西”“如果它哪天…

📅 2026/10/19 11:58:06
DeepSeek Infra仙人:从MoE到GRPO的全栈优化拆解

DeepSeek Infra仙人:从MoE到GRPO的全栈优化拆解

如果你这两年一直泡在 AI 社区,八成见过这个说法:DeepSeek 是 Infra 仙人。第一次听到我也觉得夸张,但把他们的技术路线从头到尾翻完,我反而觉得这个词挺精准——“仙人”不是玄学,而是把别人不愿意抠的系统级细节一个…

📅 2026/10/19 11:58:06
GPT-6免费实测:多模态、推理与上下文升级全解析

GPT-6免费实测:多模态、推理与上下文升级全解析

1. 这次升级到底改了啥:从"能用"到"好用"的三处关键变化GPT-6 全面上线,海外和国内技术圈都在刷屏。最让我这种普通用户兴奋的不是"又变聪明了"这种口号,而是免费用户这次真的能上手用了,不用开订阅…

📅 2026/10/19 11:58:06
MORE NEWS

更多资讯

📰

2026年本科生降AIGC率指南:10个实用工具与组合策略

1. 写在前面:为什么本科生都在讨论“降AIGC率”先说个今年我特别有感触的场景。某高校课程群里,期中论文提交前夜,好几个同学同时在问同一个问题:“用AI写初稿到底行不行?查出来会不会出事?”底下有人回复“…

📰

从零搭建个人技能图谱:技能资产管理、熟练度评估与缺口量化实战

1. 从“skills”这个标题说起:它到底在指什么第一次看到“skills”这个标题,很多人会下意识觉得太宽泛——技能?能力?技巧?这也能做成一个项目?但恰恰是这种看似“什么都能装”的标题,背后往往藏…

📰

图表设计实战:从需求澄清到版本管理的完整指南

1. 从“diagram-design”这个标题里,我读出了什么第一次看到“diagram-design”这个标题时,我脑子里蹦出来的不是某个具体工具,而是一类非常典型的工作场景:手里有一堆零散的信息——流程节点、系统模块、数据流向、组织关系——需…

📰

基于Spark的地铁客流分析与预测系统设计与实现

简介:基于Spark的地铁大数据客流分析系统毕业设计源码包,面向计算机专业学生及大数据初学者,可支撑课程设计或毕设中的客流统计、预测与优化分析。压缩包内主目录按文档、源码、配置、数据等模块组织,共194个文件、约42.6MB&#…

📰

STM32F103C8T6入门:从点灯到精准控舵机的嵌入式实战

简介:本资源是面向嵌入式初学者与STM32开发者的STM32F103C8T6全栈学习资料包,聚焦ARM Cortex-M3平台快速入门与工程实践。内容涵盖芯片核心特性解析、最小系统设计原理、外设驱动(USART/SPI/I2C/ADC/TIM等)应用示例、低功耗模式配…

📰

百考通AI搭起科研第一步:从模糊想法到可执行方案

手头正好有个读研的朋友跟我吐槽,说自己坐在电脑前两个小时,文档标题写了又删,最后只敲下一句话:“我想研究社交媒体对青少年的影响。”这个场景太典型了——科研新人并不缺想法,缺的是把“我想研究……”翻译成“我将…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬