原文: WPF 控件庫——仿制Chrome的ColorPicker
一、觀察
項目中的一個新需求,需要往控件庫中添加顔色拾取器控件,因為公司暫時還沒有UI設計大佬入住,是以就從網上開始找各種模樣的ColorPicker,找來找去我就看上了谷歌浏覽器自帶的,它長這個樣:

看上去不錯,可以搞!搞之前得觀察一下這裡面可能的一些坑。對WPF而言,圓角陰影等效果都是基本操作,這裡就不說了。
首先我們注意到上圖中有兩個拖動條,一個背景是可見光譜,另一個背景是顔色漸變和方塊平鋪的疊加。因為需求裡沒有螢幕取色的功能,是以在拖動條左側的拾取圖示可以去掉,隻保留目前顔色預覽,這樣會多出來一大塊空間,可以考慮将圓形的顔色預覽區域改成有圓角的矩形。而最上方的顔色拾取區域就比較複雜了,它其實是三層畫刷的疊加,第一層是洋紅色主色調,第二層是白色到透明的左右漸變,第三層是透明到黑色的上下漸變。由于WPF的帶透明通道的顔色漸變并非是标準的,舉個例子,假設有一個從透明到黑色的上下漸變層,在漸變層下方是純紅色背景,那麼理論上漸變開始的顔色是#FFFF0000,漸變結束的顔色是#FF000000,那麼在上下一半處的顔色應該是#FF7F0000(或者是#FF800000,就是簡單的相加除以2),但是在WPF中卻不是這個值(在專業的圖像處理軟體中比如PS中的确是#FF7F0000),如果你不信,我們現在就做個實驗。
二、實驗
打開Blend,建立個WPF項目,設定視窗尺寸為400*300,為了友善定位中心點,我們需要設定 AllowsTransparency="True" , WindowStyle="None" ,接着把主視窗背景改成純紅色,再添加一層從透明到黑色的上下漸變層,用Border實作,如下圖:
我們該相信誰?當然是PS,畢竟人家是圖像處理科班出身,是以我們隻要用PS做一張從透明到黑色的漸變png就ok了。
三、拖動條背景
我有個強迫症,那就是能不用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>
至于拖動條的樣式由于篇幅有限我就不貼出來了。
三、算法
1、顔色的進制轉換
因為涉及到顔色的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中使用。
2、根據拖動條在光譜上的位置,改變頂部顔色拾取區域的主色調
該算法用一張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),至此該算法看似完成了,但是谷歌在這基礎上多了一個步驟,詳細請看最後一小節。
3、根據主色調來改變拖動條在光譜上的位置
對,這個算法就是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)這種極值數量不唯一的情況,針對這種特殊樣本,做好充足的驗證即可,也不再贅述。
4、根據滑鼠位置來改變選取顔色
按照慣例,給張gif:
擷取滑鼠位置很簡單,我就不說明了,現在又已知主色調,那麼我們可以做出如下示意圖:
如圖,此時主色調為(255,0,0),假設滑鼠位置為中心點,那麼選取的顔色是什麼?如果不能一步算出,就分而算之。我們先計算左右兩邊中點的顔色,很簡單,利用之前貼出的算法計算後得出左側中點的顔色為(127,127,127),右側的為(127,0,0),故中心點的顔色為(127,63,63),或者是(127,64,64),主要看你舍入的規則。
5、根據主色調來改變拾取點位置
這裡的gif和小節3中的一樣:
可以看到,選取一個預置的顔色後,不僅僅是光譜位置變了,顔色選取點的位置也變了。假設我們選取了一個預置顔色#4caf50,它的10進制為:(76,175,80),再假設此時我們也知道主色調(也就是顔色拾取區域右上角的顔色),如此一來就和小節3一樣了,隻不過從原來的一維變成了二維而已。
6、不太明白谷歌的邏輯
假如給定一個顔色(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)。至于為什麼要這麼計算,希望了解的園友不吝賜教。
四、截圖
五、源碼
本文所讨論的顔色拾取器源碼已經在github開源:
https://github.com/NaBian/HandyControl