65.9K
CodeProject 正在变化。 阅读更多。
Home

WPF 颜色、颜色空间、颜色选择器和为普通人创建自己颜色的权威指南

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.91/5 (22投票s)

2021 年 3 月 15 日

CPOL

15分钟阅读

viewsIcon

40684

downloadIcon

483

深入涵盖 WPF 中与颜色相关的所有问题,包括色彩模型、取色器、颜色,以及用于混合颜色、使其变亮和变暗的有用方法。

引言

我不知道你们怎么样,但我多年来一直为 Colors 类中有限的颜色数量所困扰,试图用 ColorPickers 获得匹配的颜色并理解各种色彩模型。为了简化我的生活,我写了一些小程序,允许我将任何颜色变成白色和黑色,另一个小程序用于混合颜色。有了这些,我就能获得漂亮匹配的颜色,有点像 GradientBrush 中的渐变。

然后我犯了个愚蠢的错误,写了一篇 CodeProject 文章。为了解释我的方法是如何工作的,我别无选择,只能详细研究到底发生了什么。所以本文的很大一部分是关于颜色、色彩模型、色相、亮度等等,但以软件开发者可以理解的简单术语来说明,而无需数学或物理学学位。

如果您已经对颜色有了扎实的理解,可以直接跳转到 “精确生成您自己的颜色” 章节,其中包含实际代码。

目录

HSB 色彩空间:色相、饱和度和亮度

正如我们可能都知道的那样,计算机屏幕上的颜色是由像素创建的,每个像素由 3 个可以发出红、绿、蓝光的点组成,这解释了这些点的名称 R、G 和 B。然而,这里已经存在第一个误解,因为实际上 G 不是 Colors.Green 而是 Colors.Lime。人眼有三种不同的颜色感受器,R、G 和 B 的颜色(色相)选择与这些感受器很好地匹配,碰巧是亮绿色而不是“普通”绿色。使用 RGB 像素,显示器可以产生人眼可以区分的大多数颜色。嗯,几乎。当然,与其他光源(例如太阳)相比,这些颜色的“强度”(亮度)是相当有限的。

每种颜色由这 3 个点发出多少光来定义。一个点的值可以从 0(不发光)到 255(或十六进制的 0xFF,以最大强度发光)。要看到其中一种原色,例如红色,将 R 设置为 255,将 G 和 B 设置为零,这将产生最亮的红色。如果我们想要更暗的红色,只需降低 R 的值。一旦 R 为 0,结果颜色就是黑色,因为没有点发光。这是一个改变颜色亮度的例子,亮度是每种颜色具有的三种属性之一。有趣的是,亮度不是从 0 到 255 定义的,而是从 0 到 1 或 0% 到 100%。

颜色的另一个属性称为色相。色相为黄色、橙色、红色等分配值。当 3 个点中的一个为 255,一个为 0,而“中间”(第三个)点可以为任何值时,我们可以产生最纯净的色相。例如,R 225,G 255,B 0 结合了红色和绿色,结果是?奇怪的是,结果是黄色!原因是当 2 种光照射在同一点上时,该点会变亮而不是变暗。如果我们混合 R、G 和 B 以最大强度发光,我们将得到白光。这与油漆颜色相反。如果我们混合许多油漆颜色,我们会得到一种深灰色且难看的颜色。

R:FF G:FF B:00 = 黄色

R:00 G:FF B:FF = 青色

R:FF G:00 B:FF = 品红色

顺便说一下,这些是显示器能产生的最亮的颜色,因为它们使用了 2 个全开的点,而红色、绿色和蓝色只使用 1 个点。对于所有其他色相,第二个最强的点发出的强度都小于 255。正是这个“中间”点,既不是 255 也不是 0,它定义了色相。通过将这个中间点从 0 缓慢增加到 255,我们可以得到所有可能的色相,例如,介于红色 (FF0000) 和黄色 (FFFF00) 之间。总共有 6 种这样的过渡。

255 Red and some Green: Red to Yellow

255 Red and some Blue: Red to Violet

255 Green and some Red: Green to Yellow

255 Green and some Blue: Green to BlueGreen

255 Blue and some Green: Blue to BlueGreen

255 Blue and some Red: Blue to Violet

当我们以微小增量改变 R、G 或 B 时,我们会得到类似彩虹的东西。

在左端和右端,每次都有一个红色。因此,彩虹经常被画成一个圆圈。色相以度为单位定义,红色为 0 度,360 度。

第三种颜色属性称为饱和度。到目前为止,我们只处理了完全饱和的颜色,即最暗的点是 0。如果我们想让完全饱和的颜色变亮并最终使其接近白色,我们需要通过将所有三个点的强度按比例提高到更接近 255 来降低饱和度。要将饱和度从 100% 降低到 50%,我们必须将当前值与 255 之间的差值减半。例如。

Present value: R 255, G 128, B 0

Decrease saturation from 1 to .5

New R value = 255 + 0.5 * (255-255) = 255

New G value = 128 + 0.5 * (255-128) = 192 (or 191 depending on rounding)

New B value = 0 + 0.5 * (255-0) = 128 (or 127 depending on rounding)

Decrease saturation from 1 to 0 (=white)

New R value = 255 + 1 * (255-255) = 255

New G value = 128 + 1 * (255-128) = 255

New B value = 0 + 1 * (255-0) = 255 

颜色从 100% 饱和度绘制到 0% 饱和度

现在我们可以再次绘制彩虹,并包含一些饱和度和亮度变化。X 轴增加色相从 0 到 360。中间显示每个色相 100% 的饱和度和 100% 的亮度。在上半部分,亮度保持 100%,饱和度降低到 0%,结果是白色。在下半部分,饱和度保持 100%,亮度降低到 0%,结果是黑色。

请注意,黄色、青色和品红色比其他色相能更长时间地保持其颜色,然后才变成白色或黑色。它们是最强的颜色,因为有 2 个点以全强度发光。

饱和度接近 0% 且亮度为 100% 的色相看起来像白色。白色值为 FFFFFF,色相和饱和度未定义。

饱和度为 100% 且亮度接近 0% 的色相看起来像黑色。黑色值为 000000,色相和饱和度未定义。

所有 3 个点以相同强度发光的颜色看起来是灰色。可能的值是 808080。

注意:对于灰色,即 R、G 和 B 具有相同的值,色相和饱和度均未定义,只有亮度有意义的值。我们也可以说,黑色、灰色和白色不是颜色。黑色 000000 的亮度为 0,白色 FFFFFF 的亮度为 1。仅亮度控制黑色、灰色和白色外观的这种特性会产生一个奇怪的后果,正如我们在下图中所见。

我们现在是否涵盖了显示器可以显示的所有颜色?实际上,我们只显示了所有 R、G 和 B 组合中不到 1% 的颜色,即只有一个点是 255(100% 亮度)或一个点是 0(100% 饱和度)的颜色。假设我们首先将颜色 FF8000(一种橙红色)的饱和度更改为 50%,我们得到 FFC080。然后将亮度更改为 50%,我们得到 806040。色相仍然是橙红色,但颜色现在更接近深灰色。

这张图片展示了红色在所有可能的饱和度和亮度组合下的效果。

我猜这是本文中最令人困惑的图片。基本上,我想在 Y 轴(从上到下)上将颜色从红色变为黑色,这意味着亮度从 1 变为 0;在 X 轴(从左到右)上将颜色从红色变为白色,这意味着饱和度从 1 变为 0。我本以为白色和黑色也会混合,右下角会是灰色。但事与愿违。一旦 R、G、B 具有相同的值,色相和饱和度就会失去意义。只有亮度会影响白色、灰色和黑色(右侧边框)的颜色。更糟糕的是,整个底部边框都是黑色的,因为一旦亮度为 0,色相和饱和度就会再次变得没有意义。

如果您也感到困惑,欢迎加入我们。但这正是HSB 色彩模式的工作方式。当仅操作 RGB 值时,很难判断结果会是什么样子(还记得 R 和 G 混合会产生黄色吗?)。在 HSB 色彩空间中操作颜色时,只要您仅更改饱和度和亮度,黄色就会保持黄色,直到亮度变为 1(白色)或 0(黑色),此时色相和饱和度就会丢失。

HSL 色彩空间

还有一个名为 HSL(色相、饱和度和亮度)的色彩空间。色相与 HSB 中的相同,饱和度不是朝向白色而是灰色,而亮度从 0=黑色,0.5=灰色到 1=白色。HSL 在从黑白电视到彩色电视的过渡中很有用。黑白电视仅显示 L 值,而彩色电视使用 HSL。

取色器

过去,我一直很难理解取色器是如何工作的,以及为什么它们有时会失败。现在理解了色相、饱和度和亮度以及它们与 RGB 颜色的关系,取色器就更容易理解了。

PowerPoint 2010 取色器

PowerPoint 使用 HSL 色彩空间。在颜色选择区域,它们水平显示所有色相,垂直显示饱和度。在 HSL 色彩空间中,饱和度为 0 是灰色,因此整个底部边框是灰色的。右侧有一个滚动条,用于更改亮度,0 表示黑色,128 表示灰色,255 表示白色(它不使用 0-100%,而是 0-255)。在 0 和 255 时,色相和饱和度会失去意义。最纯净的颜色亮度为 128。

例如,选择一种蓝色(HSL),然后将亮度降低到 0 并切换到 RGB 显示,正确显示为 0,0,0。稍微增加 R,然后将其再次设置为 0,然后切换回 HSV 会显示色相和饱和度均为 0,这应该是未定义的。色相为 0 表示红色,但黑色没有色相。颜色区域中的指针仍然显示最初选择的蓝色,而不是红色(色相为 0)。这不仅让我感到困惑,而且当我反复尝试黑色、灰色和白色,并在 RGB 和 HSL 视图之间切换时,PowerPoint 最终崩溃了。

Paint.net 4.2 取色器

使用 paint.net 的取色器证明更容易。它们使用 HSV 色彩模型,与 HSB 色彩模型相同,它们只是将亮度一词更改为音量。我赞赏它们在同一个窗口中显示 RGB 值和 HSV 值,这使得更容易理解更改一个 HSV 值如何影响 RGB 值。它在颜色圆的边框上显示所有可用的色相,中间是白色,表示 0% 饱和度。要使颜色变暗,必须更改音量(亮度)参数的滑块。当然,它也为黑色、灰色和白色显示色相为零,但至少当我尝试这些值时它没有崩溃。

WinUI ColorPicker

令人遗憾的事实是 WPF 没有取色器。这就像微软多年来放弃了 WPF,试图强迫我们改用 UWP 应用程序。许多开发人员说“不必了”,并继续使用 WPF。所以现在微软正在引入 XAML Islands,允许 WPF 项目使用“较新”的控件,如取色器,而这本应一开始就包含在 WPF 中。我还没有在项目中使用过 WinUI ColorPicker,但我在 XAML Controls Gallery 中运行过它。

它的工作方式与 PowerPoint 的取色器有点相似,但使用的是 HSV(HSB)而不是,这意味着颜色区域的下部是白色,而不是灰色。下面的滚动条允许更改 V 值(亮度)。当设置为黑色时,色相和饱和度会保留其最新值,即使稍后在颜色区域中选择了不同的颜色。当值(亮度)增加时,颜色区域中的圆会跳回到旧的色相。奇怪,但至少没有崩溃。

Colors 类

Colors 类提供了一些标准颜色。它们是由委员会混合不同的色彩方案选择的,有时结果很奇怪。例如,Colors.GrayColors.DarkGray 更暗。奇怪,对吧?

或者 2 个不同的名称实际上代表同一种颜色,例如 Aqua (00FFFF) 和 Cyan (00FFFF),或者 Fuchsia (FF00FF) 和 Magenta (FF00FF)。不幸的是,Colors 帮助页面按字母顺序显示颜色,如果您知道名称,很容易找到它们,但很难分辨哪些颜色彼此接近或匹配。

所以我花了一些时间,按色相垂直排序,然后按亮度水平排序,再按饱和度水平排序。这是列表结果,与 Colors 帮助页面中的颜色完全相同。

精确生成您自己的颜色(本文代码)

使颜色变亮或变暗(降低饱和度和/或亮度)

当我在设计新应用程序并决定要使用的配色方案时,我通常无法使用 Colors 类提供的调色板。我经常需要相同色相但有不同饱和度和亮度的阴影。这只需要很少的代码行。这是降低任何颜色的饱和度(使其变亮)或降低亮度(使其变暗)的方法。

/// <summary>
/// Makes the color lighter if factor>0 and darker if factor<0. 1 returns white, -1 returns 
/// black.
/// </summary>
public static Color GetBrighterOrDarker(this Color color, double factor) {
  if (factor<-1) throw new Exception($"Factor {factor} must be greater equal -1.");
  if (factor>1) throw new Exception($"Factor {factor} must be smaller equal 1.");

  if (factor==0) return color;

  if (factor<0) {
    //make color darker, changer brightness
    factor += 1;
    return Color.FromArgb(
      color.A, 
      (byte)(color.R*factor), 
      (byte)(color.G*factor), 
      (byte)(color.B*factor));
  } else {
    //make color lighter, change saturation
    return Color.FromArgb(
      color.A,
      (byte)(color.R + (255-color.R)*factor),
      (byte)(color.G + (255-color.G)*factor),
      (byte)(color.B + (255-color.B)*factor));
  }
}

令人惊讶的是,只需几行代码就可以改变饱和度和亮度。要正确计算饱和度有点挑战,为此此方法会很有用。

红、绿、蓝,因子为 -1 到 1

要获得更强的发光颜色,请不要使用绿色(它不是 100% 饱和),而是使用黄色、品红色和青色。

请注意,先应用因子 0.5 然后应用因子 -0.5 并不会得到原始颜色。第一次调用改变了饱和度,第二次调用改变了亮度。

我喜欢使用此方法的是

  1. 我可以以小的、可控的步长增加、减少变化,并在 GUI 中看到结果。
  2. 我可以轻松创建阴影和高光,它们应该具有相同的色相,但饱和度和亮度不同。

混合色相

通常,用户界面不应使用太多色相,但少量色相可能还可以,并且可以从它们混合出一些色相。这两种方法可以满足此目的,第一种方法将两种颜色混合一半一半,第二种方法允许使用一种颜色多于另一种颜色。

/// <summary>
/// Mixes 2 colors equally
/// </summary>
public static Color Mix(this Color color1, Color color2) {
  return Mix(color1, 0.5, color2);
}

/// <summary>
/// Mixes factor*color1 with (1-factor)*color2.
/// </summary>
public static Color Mix(this Color color1, double factor, Color color2) {
  if (factor<0) throw new Exception($"Factor {factor} must be greater equal 0.");
  if (factor>1) throw new Exception($"Factor {factor} must be smaller equal 1.");

  if (factor==0) return color2;
  if (factor==1) return color1;

  var factor1 = 1 - factor;
  return Color.FromArgb(
    (byte)((color1.A * factor + color2.A * factor1)),
    (byte)((color1.R * factor + color2.R * factor1)),
    (byte)((color1.G * factor + color2.G * factor1)),
    (byte)((color1.B * factor + color2.B * factor1)));
}

这就是生成漂亮匹配颜色所需的一切。第一张图展示了每种“主要”颜色如何与每种“主要”颜色以不同程度混合,再次是红色、绿色和蓝色。

但是,如果您使用黄色、品红色和青色代替可能会更好。

在这里,我真的觉得颜色比混合红色、绿色和蓝色更漂亮。当然,它们可能太纯了。GUI 通常使用一种有点灰的色调,混合后可以通过使用 GetBrighterOrDarker() 轻松制成。

获取 RGB 颜色的色相、饱和度和亮度

我写了一些我为写这篇文章而需要的额外方法,这些方法可能也很有用。第一个计算 RGB 颜色的 `hue`、`saturation` 和 `brightness`。

/// <summary>
/// Returns the hue, saturation and brightness of color
/// </summary>
public static (int Hue, double Saturation, double Brightness)GetHSB(this Color color) {
  int max = Math.Max(color.R, Math.Max(color.G, color.B));
  int min = Math.Min(color.R, Math.Min(color.G, color.B));
  int hue = 0;//for black, gray or white, hue could be actually any number, but usually 0 is 
              //assign, which means red
  if (max-min!=0) {
    //not black, gray or white
    int maxMinDif = max-min;
    if (max==color.R) {
      #pragma warning disable IDE0045 // Convert to conditional expression
      if (color.G>=color.B) {
      #pragma warning restore IDE0045
        hue = 60 * (color.G-color.B)/maxMinDif;
      } else {
        hue = 60 * (color.G-color.B)/maxMinDif + 360;
      }
    } else if (max==color.G) {
      hue = 60 * (color.B-color.R)/maxMinDif + 120;
    } else if(max == color.B) {
      hue = 60 * (color.R-color.G)/maxMinDif + 240;
    }
  }

  double saturation = (max == 0) ? 0.0 : (1.0-((double)min/(double)max));

  return (hue, saturation, (double)max/0xFF);
}

我从 CodeProject: Manipulating colors in .NET Part 1 复制了这段代码,该代码曾获得“2007 年 5 月最佳 C# 文章”奖,并对其进行了些许“改进”。例如,我认为整数 0 到 360 足够枚举所有色相。原始代码使用浮点数,您可以在其中拥有无限数量的色相。这可能会使计算不易受到舍入误差的影响,但我猜人眼看不出区别。

将任何颜色的饱和度和亮度提高到 100%

在我选择匹配颜色的方法中,从“纯”颜色开始很有帮助,这些颜色具有 100% 的饱和度和亮度,之后再用于混合和使其变暗或变亮。以下方法接受任何 RGB 颜色并返回具有相同色相但饱和度和亮度为 100% 的 RGB 颜色。

/// <summary>
/// Returns a color with the same hue, but brightness and saturation increased to 100%.
/// </summary>
public static Color ToFullColor(this Color color) {
  //step 1: increase brightness to 100%
  var max = Math.Max(color.R, Math.Max(color.G, color.B));
  var min = Math.Min(color.R, Math.Min(color.G, color.B));
  if (max==min) {
    //for black, gray or white return white
    return Color.FromArgb(color.A, 0xFF, 0xFF, 0xFF);
  }

  double rBright = (double)color.R * 255 / max;
  double gBright = (double)color.G * 255 / max;
  double bBright = (double)color.B * 255 / max;

  //step2: increase saturation to 100%
  //lower smallest R, G, B component to zero and adjust second smallest color accordingly
  //p = (smallest R, G, B component) / 255
  //(255-FullColor.SecondComponent) * p + FullColor.SecondComponent = color.SecondComponent
  //FullColor.SecondComponent = (color.SecondComponent-255p)/(1-p)
  if (color.R==max) {
    if (color.G==min) {
      double p = gBright / 255;
      return Color.FromArgb(color.A, 0xFF, 0, (byte)((bBright-gBright)/(1-p)));
    } else {
      double p = bBright / 255;
      return Color.FromArgb(color.A, 0xFF, (byte)((gBright-bBright)/(1-p)), 0);
    }
  } else if (color.G==max) {
    if (color.R==min) {
      double p = rBright / 255;
      return Color.FromArgb(color.A, 0, 0xFF, (byte)((bBright-rBright)/(1-p)));
    } else {
      double p = bBright / 255;
      return Color.FromArgb(color.A, (byte)((rBright-bBright)/(1-p)), 0xFF, 0);
    }
  } else {
    if (color.R==min) {
      double p = rBright / 255;
      return Color.FromArgb(color.A, 0, (byte)((gBright-rBright)/(1-p)), 0xFF);
    } else {
      double p = bBright / 255;
      return Color.FromArgb(color.A, (byte)((rBright-bBright)/(1-p)), 0, 0xFF);
    }
  }
}

这段方法是我自己写的。这里的数学要求稍微高一些,我希望我做得没错。您可以使用任何取色器轻松验证它。如果您发现任何差异,请告诉我。

下载示例代码

本文已包含帮助您创建自己颜色的方法。下载示例代码,了解它们如何使用,并以原始大小查看本文中的图形闪耀光芒。它们看起来很不错,您可以根据自己的需求进行调整。

推荐阅读

如果您一直读到这里,您可能对我在 CodeProject 上写的其他 WPF 相关文章感兴趣。最后两篇与 WPF 无关,但电子邮件POP3/MIME 文章是我最受欢迎的,而实时调试(!) 我认为是我最惊人的文章。

历史

  • 2021 年 3 月 15 日:初始版本
© . All rights reserved.