WPF 控件库系列博文地址:html
WPF 控件库——仿制Chrome的ColorPickergit
WPF 控件库——仿制Windows10的进度条github
1、观察google
项目中的一个新需求,须要往控件库中添加颜色拾取器控件,由于公司暂时尚未UI设计大佬入住,因此就从网上开始找各类模样的ColorPicker,找来找去我就看上了谷歌浏览器自带的,它长这个样:spa
看上去不错,能够搞!搞以前得观察一下这里面可能的一些坑。对WPF而言,圆角阴影等效果都是基本操做,这里就不说了。.net
首先咱们注意到上图中有两个拖动条,一个背景是可见光谱,另外一个背景是颜色渐变和方块平铺的叠加。由于需求里没有屏幕取色的功能,因此在拖动条左侧的拾取图标能够去掉,只保留当前颜色预览,这样会多出来一大块空间,能够考虑将圆形的颜色预览区域改为有圆角的矩形。而最上方的颜色拾取区域就比较复杂了,它实际上是三层画刷的叠加,第一层是洋红色主色调,第二层是白色到透明的左右渐变,第三层是透明到黑色的上下渐变。因为WPF的带透明通道的颜色渐变并不是是标准的,举个例子,假设有一个从透明到黑色的上下渐变层,在渐变层下方是纯红色背景,那么理论上渐变开始的颜色是#FFFF0000,渐变结束的颜色是#FF000000,那么在上下一半处的颜色应该是#FF7F0000(或者是#FF800000,就是简单的相加除以2),可是在WPF中却不是这个值(在专业的图像处理软件中好比PS中的确是#FF7F0000),若是你不信,咱们如今就作个实验。设计
2、实验
打开Blend,新建个WPF项目,设置窗口尺寸为400*300,为了方便定位中心点,咱们须要设置 AllowsTransparency="True" , WindowStyle="None" ,接着把主窗口背景改为纯红色,再添加一层从透明到黑色的上下渐变层,用Border实现,以下图:
咱们使用标尺,定位中心点(200,150),以下图:
咱们看看在中心点处的颜色是什么,用Blend取色获得的颜色以下:
下面咱们在PS中重现以上操做,看看最后的颜色会是什么,打开PS新建400*300,分辨率为72的画布:
新建纯红色填充图层和透明到黑色的上下渐变层,再用标尺定位一下中心点:
最后获得的颜色以下:
咱们该相信谁?固然是PS,毕竟人家是图像处理科班出身,因此咱们只要用PS作一张从透明到黑色的渐变png就ok了。
3、拖动条背景
我有个强迫症,那就是能不用png就不用png,除非是万不得已,好比上一节中颜色偏差问题。因此咱们这里谈谈那两个拖动条的背景该怎么实现。第一个是光谱,简单观察其实就是颜色渐变,只不过里面的 GradientStop 比较多罢了,光谱的XAML代码以下:
1 <LinearGradientBrush x:Key="ColorPickerRainbowBrush" StartPoint="0,1"> 2 <GradientStop Color="#ff0000"/> 3 <GradientStop Color="#ff00ff" Offset="0.167"/> 4 <GradientStop Color="#0000ff" Offset="0.334"/> 5 <GradientStop Color="#00ffff" Offset="0.501"/> 6 <GradientStop Color="#00ff00" Offset="0.668"/> 7 <GradientStop Color="#ffff00" Offset="0.835"/> 8 <GradientStop Color="#ff0000" Offset="1"/> 9 </LinearGradientBrush>
第二个背景也很简单,就是普通的 DrawingBrush ,不过可能接触过它的人很少,简单的来讲当设置属性 TileMode="Tile" 时,它会使用咱们提供的单位画笔来平铺整个画布,经过观察google的ColorPicker,咱们发现,这里的单位画笔是一深一浅的两个方块,和一条不太明显的分割线组成的,因此最后的代码以下:
1 <DrawingBrush x:Key="ColorPickerOpacityBrush" Viewport="0,0,12,11" ViewportUnits="Absolute" Stretch="None" TileMode="Tile"> 2 <DrawingBrush.Drawing> 3 <DrawingGroup> 4 <GeometryDrawing Brush="#d0cec7"> 5 <GeometryDrawing.Geometry> 6 <GeometryGroup> 7 <RectangleGeometry Rect="0,0,6,5" /> 8 <RectangleGeometry Rect="6,6,6,5" /> 9 </GeometryGroup> 10 </GeometryDrawing.Geometry> 11 </GeometryDrawing> 12 <GeometryDrawing Brush="#e7e7e2"> 13 <GeometryDrawing.Geometry> 14 <RectangleGeometry Rect="0,5,12,1" /> 15 </GeometryDrawing.Geometry> 16 </GeometryDrawing> 17 </DrawingGroup> 18 </DrawingBrush.Drawing> 19 </DrawingBrush>
至于拖动条的样式因为篇幅有限我就不贴出来了。
3、算法
一、颜色的进制转换
由于涉及到颜色的16进制和10进制的相互转换,因此须要写一个简单的算法加以处理。颜色的16进制转10进制.net已经给咱们封装在类型 ColorConverter 中了,只要给静态方法 ConvertFromString 传入一个颜色字符串,再将返回值转换为 Color 就能实现咱们想要的功能。而从10进制到16进制就太简单了,微软都不屑去作,那只能咱们去实现了,只要一行代码: $"#{color.A:X2}{color.R:X2}{color.G:X2}{color.B:X2}" 。要注意的是,在WPF中最好将涉及到UI的数据转换作成转换器,以便在XAML中使用。
二、根据拖动条在光谱上的位置,改变顶部颜色拾取区域的主色调
该算法用一张gif能简单的说明:
为了实现该算法咱们须要先搞清楚光谱的颜色分布,由于以前已经贴过光谱的画刷,因此咱们能够给它加个注释:
如上图,我把光谱分红了6块,数一数一共是7条竖线,它们分别对应光谱画刷中的7个 GradientStop ,如今咱们已知拖动条的位置和7处节点处对应的颜色,求拖动条所处位置的颜色就很是简单了,由于拖动条是个 Slider 控件,咱们能够把它的最大值设为6 Maximum="6" ,并从它的 OnValueChanged 事件中获知它此时的位置,假设此时的值为1.75,那么就至关因而落在了编号为1的方块中,并且是3/4位置处。这时该怎么计算此处的颜色呢?因为编号0和编号1的分割线(左起第二根)处的颜色刚好是第二个 GradientStop 的值#ff00ff(咱们用color1代替),又由于第三个 GradientStop 值为#0000ff(咱们用color2代替),因此3/4位置处的颜色应该是(color1 -(color1 - color2)* 3 / 4),至此该算法看似完成了,可是谷歌在这基础上多了一个步骤,详细请看最后一小节。
三、根据主色调来改变拖动条在光谱上的位置
对,这个算法就是2的逆过程。什么状况下会用到呢?仍是看一下gif吧:
既然是逆过程,咱们就要反过来思考,把重点放在颜色上。此次咱们要把光谱的10进制代码拿来分析,咱们已经知道光谱被7个节点拆分红6块颜色渐变区域,用代码来表示的话就是这样的:
稍加观察便可发现,每一块颜色渐变都只改变三色通道中的一个,好比从(0,0,255)到(0,255,255)改变的是G通道,它从0增长到了255。这说明了什么?这说明光谱上的颜色都是强迫症,它们的三色通道一定有一个值为255,也一定有一个值为0,只有一个通道的值在不停地改变。
假设咱们如今选中了一个颜色#4caf50,接下来该怎么分析它呢?16进制不适合观察,咱们先把它转换成10进制:(76,175,80),能够发现,G通道175的值最大,而R通道76的值最小,这说明这个颜色比较喜欢G通道,而讨厌R通道,对B通道则无所谓,那么它在光谱上的表现就是处于R通道值最小,G通道值最大,B通道值无所谓的颜色渐变区域,在哪里呢?经过上图的代码能够判断应该在(0,255,255)到(0,255,0)这块,也就是编号3的这块。至于在块内的相对位置在上一小节中已经给出了计算方法,这里再也不赘述。
这里须要注意的是,有可能咱们选取的颜色是形如(0,0,255)或(0,255,255)这种极值数量不惟一的状况,针对这种特殊样本,作好充足的验证便可,也再也不赘述。
四、根据鼠标位置来改变选取颜色
按照惯例,给张gif:
获取鼠标位置很简单,我就不说明了,如今又已知主色调,那么咱们能够作出以下示意图:
如图,此时主色调为(255,0,0),假设鼠标位置为中心点,那么选取的颜色是什么?若是不能一步算出,就分而算之。咱们先计算左右两边中点的颜色,很简单,利用以前贴出的算法计算后得出左侧中点的颜色为(127,127,127),右侧的为(127,0,0),故中心点的颜色为(127,63,63),或者是(127,64,64),主要看你舍入的规则。
五、根据主色调来改变拾取点位置
这里的gif和小节3中的同样:
能够看到,选取一个预置的颜色后,不只仅是光谱位置变了,颜色选取点的位置也变了。假设咱们选取了一个预置颜色#4caf50,它的10进制为:(76,175,80),再假设此时咱们也知道主色调(也就是颜色拾取区域右上角的颜色),如此一来就和小节3同样了,只不过从原来的一维变成了二维而已。
六、不太明白谷歌的逻辑
假如给定一个颜色(76,175,80),经过上面5小节的内容,你可能算出来右上角主色调为(0,255,80),但google的ColorPicker倒是(0,255,10),这不是个特殊状况,例如再点击一个预置颜色(244,67,54),根据咱们的算法主色调应该是(255,67,0),但google的结果是(255,17,0),有兴趣你能够多试试一些预置值。
因此google的答案究竟是如何计算而成的?只要尝试几组数据,你会发现谷歌是这么计算非极值通道的值的255*(min-common)/(min-max)。至于为何要这么计算,但愿了解的园友不吝赐教。
4、截图
5、源码
本文所讨论的颜色拾取器源码已经在github开源:https://github.com/NaBian/HandyControl