1. 为什么我们需要自己造一个取色器?
在数字设计和开发工作中,取色器(Color Picker)是最基础但使用频率极高的工具之一。市面上已经有Photoshop、Snipaste等成熟工具提供了取色功能,为什么我们还要用C#自己造轮子呢?
首先,商业软件往往功能过于复杂,启动缓慢。当我们需要快速获取屏幕上某个点的颜色值时,一个轻量级的专用工具会更加高效。其次,自定义开发的取色器可以完美适配个人工作流,比如直接输出特定格式的颜色代码(HEX、RGB、HSL等),或者与你的其他工具链集成。
我在实际开发中经常遇到需要精确获取界面元素颜色的场景,比如:
- 前端开发时需要提取设计稿中的色值
- UI调试时需要验证实际显示颜色与设计规范是否一致
- 自动化测试中需要验证界面元素的颜色状态
现有的工具要么功能过剩,要么缺少我需要的特定功能(如历史颜色记录、格式一键转换等),这就是我决定用C#开发TakeColor取色器的初衷。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 核心功能设计与技术选型
2.1 功能需求清单
一个实用的取色器至少需要实现以下核心功能:
- 屏幕取色:能够捕获屏幕上任意像素的颜色值
- 颜色表示:支持多种颜色格式的显示与转换(RGB、HEX、HSL等)
- 颜色存储:记录最近使用的颜色,方便重复调用
- 交互设计:简洁直观的UI,支持快捷键操作
进阶功能可以包括:
- 调色板管理
- 颜色对比度检查
- 颜色盲模拟
- 与设计工具的集成
2.2 技术实现方案
基于C#的Windows窗体应用是最合适的选择,原因如下:
- 系统API支持:通过
user32.dll可以方便地获取屏幕像素信息 - 开发效率:WinForms提供了快速构建GUI的能力
- 部署便利:编译为单个exe文件,无需复杂运行时环境
核心依赖的技术点包括:
GetPixelAPI:用于获取屏幕指定位置的颜色Color结构体:C#内置的颜色处理能力Clipboard类:实现颜色值一键复制Bitmap类:用于屏幕截图和放大镜效果实现
3. 关键代码实现详解
3.1 屏幕取色核心逻辑
取色的核心是通过Windows API获取屏幕指定坐标的像素值。以下是关键代码:
csharp复制[DllImport("user32.dll")]
public static extern IntPtr GetDC(IntPtr hwnd);
[DllImport("user32.dll")]
public static extern int ReleaseDC(IntPtr hwnd, IntPtr hdc);
[DllImport("gdi32.dll")]
public static extern uint GetPixel(IntPtr hdc, int x, int y);
public Color GetColorAt(Point location)
{
IntPtr hdc = GetDC(IntPtr.Zero);
uint pixel = GetPixel(hdc, location.X, location.Y);
ReleaseDC(IntPtr.Zero, hdc);
byte r = (byte)(pixel & 0x000000FF);
byte g = (byte)((pixel & 0x0000FF00) >> 8);
byte b = (byte)((pixel & 0x00FF0000) >> 16);
return Color.FromArgb(r, g, b);
}
这段代码的工作原理:
- 通过
GetDC获取屏幕设备上下文 - 使用
GetPixel获取指定坐标的像素值 - 释放设备上下文
- 将32位像素值分解为RGB分量
注意:在高DPI屏幕上使用时需要考虑DPI缩放问题,可以通过
Graphics.DpiX和Graphics.DpiY获取缩放比例并进行坐标转换。
3.2 颜色格式转换实现
取色器需要支持多种颜色表示格式的相互转换。以下是HEX和RGB格式的转换示例:
csharp复制public static string RgbToHex(Color color)
{
return $"#{color.R:X2}{color.G:X2}{color.B:X2}";
}
public static Color HexToRgb(string hex)
{
if (hex.StartsWith("#"))
hex = hex.Substring(1);
if (hex.Length != 6)
throw new ArgumentException("HEX颜色格式不正确");
int r = Convert.ToInt32(hex.Substring(0, 2), 16);
int g = Convert.ToInt32(hex.Substring(2, 2), 16);
int b = Convert.ToInt32(hex.Substring(4, 2), 16);
return Color.FromArgb(r, g, b);
}
对于HSL格式的转换稍微复杂一些,需要实现RGB到HSL的色彩空间转换算法:
csharp复制public static (float H, float S, float L) RgbToHsl(Color color)
{
float r = color.R / 255f;
float g = color.G / 255f;
float b = color.B / 255f;
float max = Math.Max(r, Math.Max(g, b));
float min = Math.Min(r, Math.Min(g, b));
float delta = max - min;
float h = 0f;
float s = 0f;
float l = (max + min) / 2f;
if (delta != 0)
{
s = l > 0.5f ? delta / (2f - max - min) : delta / (max + min);
if (max == r)
h = (g - b) / delta + (g < b ? 6f : 0f);
else if (max == g)
h = (b - r) / delta + 2f;
else
h = (r - g) / delta + 4f;
h /= 6f;
}
return (h * 360f, s * 100f, l * 100f);
}
3.3 放大镜效果实现
专业的取色器通常会提供放大镜功能,方便精确选取像素。实现这一功能的关键步骤:
- 捕获屏幕指定区域:
csharp复制public Bitmap CaptureScreenRegion(Rectangle region)
{
Bitmap bmp = new Bitmap(region.Width, region.Height);
using (Graphics g = Graphics.FromImage(bmp))
{
g.CopyFromScreen(region.Location, Point.Empty, region.Size);
}
return bmp;
}
- 创建放大后的视图:
csharp复制public Bitmap CreateMagnifier(Bitmap source, int zoomFactor)
{
int newWidth = source.Width * zoomFactor;
int newHeight = source.Height * zoomFactor;
Bitmap result = new Bitmap(newWidth, newHeight);
using (Graphics g = Graphics.FromImage(result))
{
g.InterpolationMode = InterpolationMode.NearestNeighbor;
g.PixelOffsetMode = PixelOffsetMode.Half;
g.DrawImage(source, 0, 0, newWidth, newHeight);
// 添加网格线
using (Pen pen = new Pen(Color.Black))
{
for (int x = 0; x <= newWidth; x += zoomFactor)
g.DrawLine(pen, x, 0, x, newHeight);
for (int y = 0; y <= newHeight; y += zoomFactor)
g.DrawLine(pen, 0, y, newWidth, y);
}
}
return result;
}
- 在UI中实时更新放大视图:
csharp复制private void UpdateMagnifier(Point mousePos)
{
// 计算捕获区域(以鼠标为中心的正方形)
int size = 15; // 捕获区域边长(像素)
Rectangle captureRect = new Rectangle(
mousePos.X - size / 2,
mousePos.Y - size / 2,
size, size);
// 捕获并放大
Bitmap captured = CaptureScreenRegion(captureRect);
Bitmap magnified = CreateMagnifier(captured, 10); // 放大10倍
// 显示在UI上
pictureBoxMagnifier.Image?.Dispose();
pictureBoxMagnifier.Image = magnified;
}
4. 完整项目架构与源码解析
4.1 项目结构设计
一个结构良好的取色器项目应该包含以下核心模块:
code复制TakeColor/
├── Models/
│ ├── ColorModel.cs // 颜色数据模型
│ └── Settings.cs // 用户配置
├── Services/
│ ├── ColorService.cs // 颜色相关服务
│ └── ScreenCapture.cs // 屏幕捕获服务
├── Utilities/
│ ├── ColorConverter.cs // 颜色格式转换
│ └── Extensions.cs // 扩展方法
├── Forms/
│ ├── MainForm.cs // 主界面
│ └── ColorPalette.cs // 调色板界面
└── Program.cs // 程序入口
4.2 主窗体实现要点
主窗体需要处理以下关键交互:
- 全局鼠标钩子:在取色模式下捕获鼠标移动和点击事件
csharp复制private void EnableColorPicking(bool enable)
{
if (enable)
{
// 注册全局鼠标钩子
hook = new MouseHook();
hook.MouseMove += OnGlobalMouseMove;
hook.MouseDown += OnGlobalMouseDown;
hook.Install();
this.Cursor = Cursors.Cross;
}
else
{
// 注销钩子
hook?.Uninstall();
hook = null;
this.Cursor = Cursors.Default;
}
}
- 颜色展示面板:实时显示当前颜色及其各种格式表示
csharp复制private void UpdateColorDisplay(Color color)
{
panelColorPreview.BackColor = color;
// 更新各种格式的颜色值
lblRgb.Text = $"RGB: {color.R}, {color.G}, {color.B}";
lblHex.Text = $"HEX: {ColorService.RgbToHex(color)}";
var hsl = ColorService.RgbToHsl(color);
lblHsl.Text = $"HSL: {hsl.H:F0}°, {hsl.S:F0}%, {hsl.L:F0}%";
// 根据亮度自动调整文本颜色
Color textColor = color.GetBrightness() > 0.5 ? Color.Black : Color.White;
lblRgb.ForeColor = textColor;
lblHex.ForeColor = textColor;
lblHsl.ForeColor = textColor;
}
- 历史颜色记录:保存最近使用的颜色
csharp复制private void AddToHistory(Color color)
{
// 避免重复添加相同颜色
if (colorHistory.Any(c => c.ToArgb() == color.ToArgb()))
return;
// 限制历史记录数量
if (colorHistory.Count >= MaxHistoryCount)
colorHistory.RemoveAt(0);
colorHistory.Add(color);
UpdateHistoryPanel();
}
private void UpdateHistoryPanel()
{
flowLayoutHistory.Controls.Clear();
foreach (var color in colorHistory)
{
var panel = new Panel
{
BackColor = color,
Size = new Size(30, 30),
Margin = new Padding(5),
Cursor = Cursors.Hand
};
panel.Click += (s, e) =>
{
currentColor = color;
UpdateColorDisplay(color);
};
flowLayoutHistory.Controls.Add(panel);
}
}
4.3 配置持久化
为了让用户设置能够保存,我们需要实现配置的序列化:
csharp复制public class AppSettings
{
public Point WindowLocation { get; set; }
public Size WindowSize { get; set; }
public bool AlwaysOnTop { get; set; }
public List<Color> ColorHistory { get; set; } = new List<Color>();
public static AppSettings Load()
{
string path = GetSettingsPath();
if (File.Exists(path))
{
string json = File.ReadAllText(path);
return JsonSerializer.Deserialize<AppSettings>(json);
}
return new AppSettings();
}
public void Save()
{
string json = JsonSerializer.Serialize(this);
File.WriteAllText(GetSettingsPath(), json);
}
private static string GetSettingsPath()
{
string appData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
return Path.Combine(appData, "TakeColor", "settings.json");
}
}
在窗体生命周期中调用保存和加载:
csharp复制private void MainForm_Load(object sender, EventArgs e)
{
settings = AppSettings.Load();
// 应用设置
this.Location = settings.WindowLocation;
this.Size = settings.WindowSize;
this.TopMost = settings.AlwaysOnTop;
// 恢复历史颜色
colorHistory = settings.ColorHistory ?? new List<Color>();
UpdateHistoryPanel();
}
private void MainForm_FormClosing(object sender, FormClosingEventArgs e)
{
// 保存当前设置
settings.WindowLocation = this.Location;
settings.WindowSize = this.Size;
settings.AlwaysOnTop = this.TopMost;
settings.ColorHistory = colorHistory;
settings.Save();
}
5. 高级功能扩展与实践技巧
5.1 颜色对比度检查
对于无障碍设计,检查两种颜色的对比度是否符合WCAG标准是非常有用的功能。实现原理如下:
csharp复制public static double GetContrastRatio(Color color1, Color color2)
{
// 计算相对亮度(sRGB颜色空间)
double luminance1 = GetLuminance(color1);
double luminance2 = GetLuminance(color2);
// 确保较亮的颜色在前
if (luminance1 < luminance2)
{
(luminance1, luminance2) = (luminance2, luminance1);
}
return (luminance1 + 0.05) / (luminance2 + 0.05);
}
private static double GetLuminance(Color color)
{
// sRGB颜色空间中的相对亮度计算
double r = color.R / 255.0;
double g = color.G / 255.0;
double b = color.B / 255.0;
r = r <= 0.03928 ? r / 12.92 : Math.Pow((r + 0.055) / 1.055, 2.4);
g = g <= 0.03928 ? g / 12.92 : Math.Pow((g + 0.055) / 1.055, 2.4);
b = b <= 0.03928 ? b / 12.92 : Math.Pow((b + 0.055) / 1.055, 2.4);
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
}
使用示例:
csharp复制double ratio = ColorService.GetContrastRatio(foreColor, backColor);
if (ratio >= 4.5)
{
Console.WriteLine("符合AA级标准");
}
else if (ratio >= 3)
{
Console.WriteLine("符合大型文本AA级标准");
}
else
{
Console.WriteLine("对比度不足");
}
5.2 颜色盲模拟
为了帮助设计师考虑色盲用户的体验,可以实现颜色盲模拟算法:
csharp复制public static Color SimulateColorBlindness(Color color, ColorBlindnessType type)
{
float[][] matrix;
switch (type)
{
case ColorBlindnessType.Protanopia: // 红色盲
matrix = new float[][]
{
new float[] {0.567f, 0.433f, 0.000f},
new float[] {0.558f, 0.442f, 0.000f},
new float[] {0.000f, 0.242f, 0.758f}
};
break;
case ColorBlindnessType.Deuteranopia: // 绿色盲
matrix = new float[][]
{
new float[] {0.625f, 0.375f, 0.000f},
new float[] {0.700f, 0.300f, 0.000f},
new float[] {0.000f, 0.300f, 0.700f}
};
break;
case ColorBlindnessType.Tritanopia: // 蓝色盲
matrix = new float[][]
{
new float[] {0.950f, 0.050f, 0.000f},
new float[] {0.000f, 0.433f, 0.567f},
new float[] {0.000f, 0.475f, 0.525f}
};
break;
default:
return color;
}
int r = (int)(color.R * matrix[0][0] + color.G * matrix[0][1] + color.B * matrix[0][2]);
int g = (int)(color.R * matrix[1][0] + color.G * matrix[1][1] + color.B * matrix[1][2]);
int b = (int)(color.R * matrix[2][0] + color.G * matrix[2][1] + color.B * matrix[2][2]);
r = Math.Clamp(r, 0, 255);
g = Math.Clamp(g, 0, 255);
b = Math.Clamp(b, 0, 255);
return Color.FromArgb(r, g, b);
}
5.3 性能优化技巧
在实现实时取色功能时,性能是关键。以下是几个优化建议:
- 减少不必要的屏幕捕获:只有在鼠标移动时才更新颜色,而不是持续轮询
- 使用双缓冲技术:避免UI闪烁
csharp复制public MainForm()
{
InitializeComponent();
this.DoubleBuffered = true; // 启用双缓冲
}
- 优化颜色计算:缓存常用计算结果
csharp复制private static readonly ConcurrentDictionary<int, string> HexCache = new();
public static string RgbToHexCached(Color color)
{
int argb = color.ToArgb();
return HexCache.GetOrAdd(argb, _ =>
{
return $"#{color.R:X2}{color.G:X2}{color.B:X2}";
});
}
- 异步UI更新:对于耗时的操作(如调色板生成),使用后台线程
csharp复制private async void GeneratePaletteAsync(Color baseColor)
{
btnGenerate.Enabled = false;
await Task.Run(() =>
{
var palette = ColorPaletteGenerator.Generate(baseColor);
this.Invoke((MethodInvoker)delegate
{
DisplayPalette(palette);
btnGenerate.Enabled = true;
});
});
}
6. 实际开发中的踩坑与解决方案
6.1 多显示器环境下的坐标问题
在多显示器配置中,屏幕坐标可能是负值或超出主显示器范围。直接使用鼠标位置可能会导致取色错误。解决方案:
csharp复制public static Rectangle GetVirtualScreenBounds()
{
int minX = int.MaxValue, minY = int.MaxValue;
int maxX = int.MinValue, maxY = int.MinValue;
foreach (Screen screen in Screen.AllScreens)
{
minX = Math.Min(minX, screen.Bounds.X);
minY = Math.Min(minY, screen.Bounds.Y);
maxX = Math.Max(maxX, screen.Bounds.Right);
maxY = Math.Max(maxY, screen.Bounds.Bottom);
}
return new Rectangle(minX, minY, maxX - minX, maxY - minY);
}
// 使用前检查坐标是否有效
Rectangle virtualScreen = GetVirtualScreenBounds();
if (!virtualScreen.Contains(mousePos))
{
// 处理无效坐标
}
6.2 高DPI缩放问题
在高DPI显示器上,获取的鼠标位置可能与实际屏幕像素不对应。解决方案:
csharp复制[DllImport("user32.dll")]
public static extern bool GetCursorPos(out Point lpPoint);
public static Point GetPhysicalCursorPosition()
{
GetCursorPos(out Point pt);
// 考虑DPI缩放
using (Graphics g = Graphics.FromHwnd(IntPtr.Zero))
{
float dpiScaleX = g.DpiX / 96f;
float dpiScaleY = g.DpiY / 96f;
return new Point((int)(pt.X * dpiScaleX), (int)(pt.Y * dpiScaleY));
}
}
6.3 颜色精度问题
在某些情况下,GetPixel API可能返回的颜色值与实际显示有细微差异。更精确的方法是捕获屏幕区域后分析位图:
csharp复制public Color GetPreciseColorAt(Point location)
{
using (Bitmap bmp = new Bitmap(1, 1))
using (Graphics g = Graphics.FromImage(bmp))
{
g.CopyFromScreen(location, Point.Empty, new Size(1, 1));
return bmp.GetPixel(0, 0);
}
}
6.4 快捷键冲突处理
为了避免取色器的全局快捷键与其他应用程序冲突,需要实现更健壮的快捷键注册:
csharp复制[DllImport("user32.dll")]
public static extern bool RegisterHotKey(IntPtr hWnd, int id, int fsModifiers, int vk);
[DllImport("user32.dll")]
public static extern bool UnregisterHotKey(IntPtr hWnd, int id);
protected override void WndProc(ref Message m)
{
const int WM_HOTKEY = 0x0312;
if (m.Msg == WM_HOTKEY)
{
int id = m.WParam.ToInt32();
if (id == HOTKEY_ID)
{
ToggleColorPicking();
}
}
base.WndProc(ref m);
}
private void RegisterGlobalHotKey()
{
// 使用Win+Shift+C作为快捷键
int MOD_WIN = 0x0008;
int MOD_SHIFT = 0x0004;
int VK_C = 0x43;
if (!RegisterHotKey(this.Handle, HOTKEY_ID, MOD_WIN | MOD_SHIFT, VK_C))
{
MessageBox.Show("无法注册全局快捷键,可能已被其他程序占用");
}
}
7. 项目部署与进阶方向
7.1 打包与分发
为了让取色器更易于使用,可以考虑以下打包方式:
-
独立EXE:最简单的分发方式,适合技术用户
- 在项目属性中设置"生成单个文件"
- 选择"独立"部署模式
-
安装程序:使用Inno Setup等工具创建专业安装包
- 添加桌面快捷方式
- 注册文件关联(如.takepalette格式)
- 添加开始菜单项
-
商店发布:打包为MSIX提交到Microsoft Store
- 支持自动更新
- 更好的分发渠道
7.2 进阶功能路线图
基于核心取色功能,可以考虑以下扩展方向:
-
调色板生成器:根据基础颜色自动生成协调色板
- 类似Adobe Color的功能
- 支持单色、互补色、三色等配色方案
-
设计系统集成:
- 导出到Sketch/Figma插件
- 与CSS预处理器集成
-
颜色命名服务:
- 根据颜色值提供语义化名称
- 如"珊瑚红"、"薄荷绿"等
-
历史记录云同步:
- 通过OneDrive/Dropbox同步颜色历史
- 多设备间共享调色板
-
命令行接口:
- 支持脚本化取色操作
- 与其他工具链集成
7.3 开源协作建议
如果决定开源项目,建议:
- 选择MIT许可证,最大化项目可用性
- 编写完善的README,包括:
- 功能特性列表
- 截图展示
- 构建说明
- 添加贡献指南
- 使用GitHub Actions设置CI/CD流水线
- 提供清晰的问题模板
我在实际开发中体会到,一个看似简单的工具背后往往有许多细节需要考虑。比如颜色格式转换时的舍入误差、高DPI环境下的坐标转换、全局快捷键的冲突处理等。这些经验教训很难在文档中找到,只有通过实际开发才能深刻理解。
