介绍 本文探讨了几个different 实现一个颜色选择器的方法。 表的内容 空间背景下载预览版是什么颜色的?我们的模型颜色怎么样?XYZ,它的许多形式重构视图最后讲话的兴趣点对未来读者历史问题 背景 项目一开始修改一个由Ken Johnson和设计所以我想给他一个shoutout奠定了基础。不幸的是,他的项目的问题可重用性差,有许多事情可以改善。 目标是减少多余的代码,建立一致性,并定义基础logic 这样额外的颜色空间可以轻松地添加和删除。约翰逊定义a UserControl several 组件:一探究竟,你将notice 重复,根本没有必要和使它difficult 支持额外的模型XYZ和华尔街日报没有乏味的复制和粘贴。 现在,添加支持额外的颜色模型,您需要两个essential 组件: 一个视图模型封装了一个给定颜色空间和定义的逻辑相关的组件。结构表示一个颜色在一个给定的颜色空间和与其他颜色空间转换的能力。 这让我们有几个优点: 从逻辑的观点进一步分离。视图may 更容易安排和其他实现引入了一致性缺席。输入给定的颜色空间的方向可能会改变。例如,在Photoshop中,输入为CMYK安排水平而另一些垂直排列。你也可以指定一个颜色是否应该代表的视觉空间。c my RGB的倒数;因此,没有必要来表示出来。你可以添加和删除给定颜色空间在运行时支持。 还有一些缺点: 最初学习的逻辑有点复杂,不可能代表一个组件的价值不止一个方法(例如,Rgb 值表示在一个范围从0到255,但不能代表在其他范围,比如从0到1)。 下载 你可能会发现最新版本并在GitHub演示。 预览 空间是什么颜色的? 颜色空间,也称为a color 模型(一、color 系统),是一个抽象的数学模型,简单地描述了射程of colors 元组的数字,通常为3或4值或是color 组件[1]只颜色空间你可能最熟悉的RGB, HSB,实验室(由Adobe)。 颜色空间中实现这个解决方案是: CMYK * hsb奥软实验室禄luv rgb xyz yuv(或YXY) *逻辑表示只 也许最mysterious 是CIE系列,由of XYZ,,实验室,禄等等。非传统的他们在现实世界中很少使用,容易被误解又是;我从未见过的实际表示XYZ包括它在我的解决方案是必须的。 [1], http://www.arcsoft.com/topics/photostudio-darkroom/what-is-color-space.html 我们的模型颜色怎么样? 肯似乎正确的想法,乍看之下,但这个项目颜色空间的数量增加,所以我挫折和耐心。颜色对我来说最好的方法模型定义一个封装的数学和逻辑结构和相应的视图模型沟通这个逻辑视图。 结构 请注意,为了简便起见,我们省略了一些转换逻辑…… 隐藏,收缩,复制Code
/// <summary> /// Structure to define a color in <see cref="ColorSpace.Rgb"/>. /// </summary> [Serializable] public struct Rgb : IColor, IEquatable<Rgb> { #region Properties /// <summary> /// Specifies a <see cref="Rgb"/> component. /// </summary> public enum Component { /// <summary> /// Specifies the <see cref="Rgb.R"/> component. /// </summary> R, /// <summary> /// Specifies the <see cref="Rgb.G"/> component. /// </summary> G, /// <summary> /// Specifies the <see cref="Rgb.B"/> component. /// </summary> B } public const byte Maximum = byte.MaxValue; public const byte Minimum = byte.MinValue; public Color Color { get => Color.FromArgb(255, r, g, b); } readonly byte r; /// <summary> /// Gets the <see cref="Component.R"/> component (0 to 255). /// </summary> public byte R { get => r; } readonly byte g; /// <summary> /// Gets the <see cref="Component.G"/> component (0 to 255). /// </summary> public byte G { get => g; } readonly byte b; /// <summary> /// Gets the <see cref="Component.B"/> component (0 to 255). /// </summary> public byte B { get => b; } #endregion #region Rgb /// <summary> /// Initializes an instance of the <see cref="Rgb"/> structure. /// </summary> /// <param name="source"></param> public Rgb(Color source) : this(source.R, source.G, source.B) {} /// <summary> /// Initializes an instance of the <see cref="Rgb"/> structure. /// </summary> /// <param name="r"></param> /// <param name="g"></param> /// <param name="b"></param> public Rgb(int r, int g, int b) : this(r.Coerce(Maximum).ToByte(), g.Coerce(Maximum).ToByte(), b.Coerce(Maximum).ToByte()) {} /// <summary> /// Initializes an instance of the <see cref="Rgb"/> structure. /// </summary> /// <param name="_r"></param> /// <param name="_g"></param> /// <param name="_b"></param> public Rgb(byte _r, byte _g, byte _b) { r = _r; g = _g; b = _b; } /// <summary> /// Initializes an instance of the <see cref="Rgb"/> structure. /// </summary> /// <param name="source"></param> public Rgb(Cmyk source) { //Conversion logic... } /// <summary> /// Initializes an instance of the <see cref="Rgb"/> structure. /// </summary> /// <param name="source"></param> public Rgb(Hsb source) { //Conversion logic... } /// <summary> /// Initializes an instance of the <see cref="Rgb"/> structure. /// </summary> /// <param name="source"></param> public Rgb(Hsl source) { //Conversion logic... } /// <summary> /// Initializes an instance of the <see cref="Rgb"/> structure. /// </summary> /// <param name="source"></param> public Rgb(Lab source) : this(new Xyz(source)) {} /// <summary> /// Initializes an instance of the <see cref="Rgb"/> structure. /// </summary> /// <param name="source"></param> public Rgb(Lch source) : this(new Lab(source)) {} /// <summary> /// Initializes an instance of the <see cref="Rgb"/> structure. /// </summary> /// <param name="source"></param> public Rgb(Luv source) : this(new Xyz(source)) {} /// <summary> /// Initializes an instance of the <see cref="Rgb"/> structure. /// </summary> /// <param name="source"></param> public Rgb(Xyz source) { //Conversion logic... } /// <summary> /// Initializes an instance of the <see cref="Rgb"/> structure. /// </summary> /// <param name="source"></param> public Rgb(Yuv source) : this(new Xyz(source)) {} public static bool operator ==(Rgb left, Rgb right) { if (ReferenceEquals(left, null)) { if (ReferenceEquals(right, null)) return true; return false; } return left.Equals(right); } public static bool operator !=(Rgb left, Rgb right) => !(left == right); #endregion #region Methods public bool Equals(Rgb o) { if (ReferenceEquals(o, null)) return false; if (ReferenceEquals(this, o)) return true; if (GetType() != o.GetType()) return false; return (R == o.R) && (G == o.G) && (B == o.B); } public override bool Equals(object o) => Equals((Rgb)o); public override int GetHashCode() => new { R, G, B }.GetHashCode(); public override string ToString() => "R => {0}, G => {1}, B => {2}".F(r, g, b); public static double Linear(byte value) => Linear(value.ToInt32()); public static double Linear(int value) => value.ToDouble() / Maximum.ToDouble(); #endregion }
每个结构都必须做到以下几点: 为每个组件定义一个最大和最小值。强迫所有传入的值到适当的范围(例如,Rgb.R 应该强迫一系列[0,255])。转换为一个颜色of 任何给定的表示,然后回来。为平等提供了检查的能力。实现接口,IColor。 隐藏,复制Code
/// <summary> /// Specifies a color. /// </summary> public interface IColor { /// <summary> /// The color. /// </summary> Color Color { get; } }
这都是表示 这就是事情可能很快就会变得令人困惑。RGB值most 通常表示为一个整数范围[0,255]。字节类型商店RGB值完美;然而,RGB值也可以表示为一个无理数区间[0,1](255),除以值或范围(0,2.55),(价值除以100);后者也可能最稀有、最实用。 尽管有这些许多表示,应该认识到只有一个结构。替代表示有时是必要的视图模型时必须与结构,但应该没有完全结构内部,以避免混乱。 有两种主要的方式来表示给定组件的值: Logically 该值表示为一个整数范围由of 所有逻辑值由颜色空间定义。隐藏,Code 复印件; Mathematically 该值表示为基于其逻辑对应项的百分比。 我们如何表示一个组件的值? 我们逻辑地表示这些值。有些值是使用byte、int或double来存储的,这取决于精确性是否值得关注,甚至是否相关。只有在处理XYZ或XYZ派生的颜色空间(如LAB、LCH等)时,准确性才成为一个问题。 每个结构是否应该使用逻辑表示? 理想情况下,是的。这避免了在值从一个转换方法传递到下一个转换方法时,其表示形式出现混乱。 例如,在HSB中,色调分量有范围[0,359],它对应于一个给定的度数或弧度。直接表示度[0,359]而不是比例[0,1]更直观。 是否使用了数学表示? 以前的实现确实使用了这种表示,但后来被替换了。 , 隐藏,复制Code
H = 245 = 245 / 359 S = 60 = 60 / 100 B = 10 = 10 / 100
很容易意外地除以错误的值,所以在每一个与此有关的地方都要特别注意(注意,这应该只在模型中处理,而不是在原语中)。 关于原语需要注意的最后一点是,它们可以根据需要处理每个颜色空间之间的所有转换。那么其他的模型是什么呢?例如,绑定到视图,处理与转换无关的数学逻辑,而是与视图以及如何与视图交互。与视图的交互作用会随着颜色空间的变化而发生数学上的变化。这是因为对于每个颜色空间的每个颜色组件,我们必须用数学方法在图像上绘制它的颜色表示,这是基于它的值、值的范围,以及在某些情况下鼠标相对于给定UI元素的x/y坐标。 该模型指定了一些非常重要的事情: 它应该为色彩组件显示什么单位。每个组件的最小值和最大值以整数形式(不是原始形式!)例如,HSB中h分量的最小值为0.0,最大值为1.0;要找到整数形式的最大值,你必须知道h分量的可能最大值,并将其乘以h分量的值。色调的最大值是359,所以整型h分量的最大值是h分量的值* 359,这是有意义的,因为十进制形式的最大值是1.0,当它乘以359,等于整型的最大值。如何画出滑块,只选择组件的变化值如何绘制平面的颜色,这是由两个对立组件向着相反的方向如何得到一个颜色从一个点(例如,当您点击颜色平面上的任何地方,我们得到这一点,基于所选择的组件,转换成一种颜色显示所选的颜色。如何从一个颜色得到一个点 模型基类可能会让事情有点混乱,但它最终帮助解决了我在开始时提到的复制/粘贴困境。以前,每个表示颜色空间的用户控件都包含在一堆方法中,这些方法对每个控件都是完全相同的(我知道这是一场噩梦)。但我发现,尽管每个颜色空间有许多不同,但都可以以超级统一的方式绘制:因此,您可以看到对每个组件模型中的基方法的调用。 隐藏,复制Code
base.UpdatePlane(Bitmap, ComponentValue, new Func<RowColumn, int, Rgba>((RowColumn, Value) => { return new Rgba(RowColumn.Column.ToByte(), RowColumn.Row.ToByte(), ComponentValue.ToByte()); }), new RowColumn(255, 255));
基本方法负责实际的绘制部分,它只是枚举我们想要绘制的图像的行/列,并根据模型中指定的转换插入适当的颜色。位图参数得到我们想要绘制的位图:有两个主要的位图我们使用绘制,a)滑动位图,b)彩色平面位图。当其他两个组件以相反的方向增加时,componentvalueparameter保持静态。这种对立是通过为行和列指定最大值来实现的(依赖于它们对应的组件),而颜色是通过将这些值插入到一个基本类型中来创建的,该类型处理实际的颜色转换。我们用Func返回获得的颜色,它将公开当前行和列。一旦这个函数被返回,基方法处理分配这个颜色到它对应的像素位置。对我来说已经够好了! 现在让我们看看基本方法: 隐藏,收缩,复制Code
unsafe { Bitmap.Lock(); int CurrentPixel = -1; byte* Start = (byte*)(void*)Bitmap.BackBuffer; var u = Unit.Value; u.Row = u.Row / 256.0; u.Column = u.Column / 256.0; double CurrentRow = u.Row * 256.0; for (int Row = 0; Row < Bitmap.PixelHeight; Row++) { double CurrentCol = 0; for (int Col = 0; Col < Bitmap.PixelWidth; Col++) { var c = Action.Invoke(new RowColumn(CurrentRow, CurrentCol), ComponentValue); CurrentPixel++; *(Start + CurrentPixel * 3 + 0) = c.B; *(Start + CurrentPixel * 3 + 1) = c.G; *(Start + CurrentPixel * 3 + 2) = c.R; CurrentCol += u.Column; } CurrentRow -= u.Row; } Bitmap.AddDirtyRect(new Int32Rect(0, 0, Bitmap.PixelWidth, Bitmap.PixelHeight)); Bitmap.Unlock(); }
在处理指针时,我们被迫使用不安全的代码,这一点尤为重要。上面你会发现我发现的最快的方法之一,以绘制图像,甚至与可疑的转换算法。如您所见,有一些值保持静态,这是有原因的。 这个方法的最后一个参数是RowColumn类型的Unit,它指定要递增/十二月的单位从行/列和是唯一的颜色空间的对立组件值。起始行总是最后一行,行单元从它递减。这是因为图像是256个像素×256个像素的,相反分量的最大值是不一样的。考虑一下:如果颜色空间是RGB,甚至不需要这个额外的数学运算,因为任何RGB值的范围总共包含256个值!这可能是这个算法中最令人困惑的方面之一,但希望它的组织方式对每个人都有意义。 滑块的绘制非常相似,除了为一行找到的颜色对所有列重复,但对所有行不同。 XYZ,它的许多形式 如果有一件事是肯定的,那就是1931年让数学家们感到困惑。这是因为与其只开发一种颜色空间,他们认为最好是开发多种颜色空间,以及上述多种颜色空间的多种变体。其中之一就是CIE XYZ,有时被称为CIE 1931年或CIE 1964:认识到两者之间的差异(老实说,我在理解自己方面仍有问题)以及究竟是什么让两者如此不同是很重要的。 根据我的理解,XYZ是这样工作的: XYZ充当其他CIE模型(如LAB、LCH、LUV等)的“门户”;即。,为了从LAB计算RGB,您将首先将LAB转换为XYZ,然后将XYZ转换为RGB。XYZ本身有许多变体,这是由“光源”和“观察者”决定的。看起来有很多光源可供选择,但使用最广泛的是D65,我的解决方案默认使用它;为了方便起见,我定义了其他6个函数,其余的函数我找不到值。事实上,虽然F2和F7是最受欢迎的,但实际上有一个完整的系列从F1开始,到F11结束,而且可能会走得更远。有两个主要的观测者:1931年或2度,1964年或10度;1931年是我选择的观察者。光源本质上决定了X, Y的最大值和最小值。与Y 一致,几乎总是有一个最大值1.0。所有光源的最小值为0。据我所知,有一种“理论”光源是“E”,不清楚是否应该用它来表示。不确定是否所有光源都能准确地表示出来。我的XYZ转换似乎是正确的,但似乎是错误的,这在LCH实现. CIE实验室和Hunter实验室是完全不同的模型!在我的解决方案中使用了CIE LAB,需要与CIE XYZ进行转换;据我所知,Adobe和继续使用猎人lab.尚不清楚什么是爱情的最大和最小值,不过,我怀疑它是0到1 L以及1 - 1 U和v .为此,爱是排除在默认情况下,但可以包含在任何时候对于那些喜欢实验,如自己又是;大多数的XYZ变种看起来或多或少是一样的,但如果有一种方法可以快速和容易地改变光源和观察者,如果这样想的话。几乎没有什么数学研究来证明我的一些观察结果,所以有很多推论和假设。 重构视图 让我对Ken的解决方案不满意的一件事是,每个颜色空间都有一个单独的用户控件。我的意思是,为什么?它们都有相同的特性,尽管从技术上讲,有些与其他的有某种或那种不同,但我认为没有理由这样做。MVVM和绑定的存在是有原因的,毕竟。 说到这里,让我们来看看新的观点: 隐藏,收缩,复制Code
<ItemsControl ItemsSource="{Binding Models, ElementName=PART_ColorPicker}" VerticalAlignment="Center"> <ItemsControl.ItemsPanel> <ItemsPanelTemplate> <WrapPanel Orientation="Horizontal"/> </ItemsPanelTemplate> </ItemsControl.ItemsPanel> <ItemsControl.ItemTemplate> <DataTemplate DataType="{x:Type local:ColorSpaceModel}"> <DataTemplate.Resources> <Common.Mvvm:BindingProxy x:Key="ComponentProxy" Data="{Binding Mode=OneWay, RelativeSource={RelativeSource Self}}"/> </DataTemplate.Resources> <local:ColorSpaceView x:Name="PART_Components" ItemsSource="{Binding Components}" Margin="0,0,0,15"> <local:ColorSpaceView.ItemTemplate> <DataTemplate DataType="{x:Type local:ComponentModel}"> <DataTemplate.Resources> <Common.Data.Converters:BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter"/> </DataTemplate.Resources> <local:ComponentView ColorSpaceModel="{Binding DataContext, Mode=OneWay, RelativeSource={RelativeSource AncestorType={x:Type local:ColorSpaceView}}}" Color="{Binding SelectedColor, RelativeSource={RelativeSource AncestorType={x:Type local:ColorPicker}}}" ComponentModel="{Binding Mode=OneWay}" CurrentValue="{Binding CurrentValue}"> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition Width="Auto" /> <ColumnDefinition Width="50" /> <ColumnDefinition Width="15" /> </Grid.ColumnDefinitions> <RadioButton Checked="OnComponentChecked" Content="{Binding ComponentLabel}" GroupName="ColorSpace" IsChecked="{Binding IsEnabled}" HorizontalAlignment="Center" Margin="5,0" VerticalAlignment="Center" Tag="{Binding Mode=OneWay}" Visibility="{Binding CanSelect, Converter={StaticResource BooleanToVisibilityConverter}}"/> <TextBlock HorizontalAlignment="Center" VerticalAlignment="Center" Margin="5,0" Text="{Binding ComponentLabel}" Visibility="{Binding CanSelect, Converter={StaticResource BooleanToVisibilityConverter}, ConverterParameter=Inverted}"/> <Controls.Common:AdvancedTextBox Grid.Column="1" HorizontalAlignment="Center" HorizontalContentAlignment="Center" VerticalAlignment="Center" Text="{Binding CurrentValue, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Width="40"/> <TextBlock Grid.Column="2" Text="{Binding UnitLabel}" VerticalAlignment="Center"/> </Grid> </local:ComponentView> </DataTemplate> </local:ColorSpaceView.ItemTemplate> </local:ColorSpaceView> <DataTemplate.Triggers> <DataTrigger Binding="{Binding Orientation}" Value="Horizontal"> <Setter TargetName="PART_Components" Property="ItemsPanel"> <Setter.Value> <ItemsPanelTemplate> <Controls.Common:Spacer Spacing="0,0,5,0" Orientation="Horizontal"/> </ItemsPanelTemplate> </Setter.Value> </Setter> </DataTrigger> <DataTrigger Binding="{Binding Orientation}" Value="Vertical"> <Setter TargetName="PART_Components" Property="ItemsPanel"> <Setter.Value> <ItemsPanelTemplate> <Controls.Common:Spacer Spacing="0,0,0,10"/> </ItemsPanelTemplate> </Setter.Value> </Setter> </DataTrigger> </DataTemplate.Triggers> </DataTemplate> </ItemsControl.ItemTemplate> </ItemsControl>
该视图主要包括以下内容: 存放所有可用颜色模型的ItemsControl。为了达到和之前一样的效果,所有的颜色空间都放在一个包裹板中,也许会给人一种它们仍然在网格中的错觉。然后颜色空间由一个叫做ColorSpaceView的控件表示,它除了促进颜色选择器控件和颜色空间之间的交流之外什么也不做。每个颜色空间模型都有一个组件数组,这些组件是在初始化模型时添加的。这些绑定到ColorSpaceView。每个颜色组件都由控件组件视图表示,它既方便了颜色空间和它的每个单独组件之间的通信,也处理了大多数与视图相关的问题。其中一个问题是能够根据文本输入和文本选择的颜色改变颜色分量值,而不会导致任何一方陷入无限循环;例如,如果一个属性更改,我们必须更改另一个属性,但如果其他属性更改,将导致另一个属性再次更改。关注这些变化可以解决这个问题。如果一个颜色空间具有水平方向(到目前为止,只有CmykModel),那么它将显示水平方向,而不是默认的垂直方向。 最后的评论 除了原语、模型和重构之外看,其他的都是一样的你会发现在肯的项目。不同的是,在每个地方肯选择不使用绑定,我选择使用绑定。这需要使用few 转换器(如将十六进制转换成一个颜色,一个颜色一个SolidColorBrush等等),并带走了太多多余的代码我以前抱怨。 另一个重大变化是我选择去一个颜色选择器设计与选择隐藏和显示元素,而肯有能力显示不同类型的农户与α(例如,颜色选择器滑块和一个没有)。在我看来,我不希望看到任何原因所以many 种类的拾荒者和即使这样,似乎更多的逻辑只是使toggling 元素的可见性,而不是定义全新的控制又是; 的兴趣点 断开连接算法;基于这些发表提醒EasyRGB。 , 问题的读者 你认为它是不必要的,有这么多颜色空间模型在一个颜色选择器或越多越好?对我来说,它总是开心! 历史 2016年9月17日 最初的发布 未来 本文中的代码是开源项目的一部分,现在,Imagin.NET又是; 本文转载于:http://www.diyabc.com/frontweb/news8365.html