WPF 控件庫——仿制Chrome的ColorPicker


WPF 控件庫系列博文地址:

WPF 控件庫——仿制Chrome的ColorPicker

WPF 控件庫——仿制Windows10的進度條

WPF 控件庫——輪播控件

WPF 控件庫——帶有慣性的ScrollViewer

WPF 控件庫——可拖動選項卡的TabControl

 

一、觀察

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

   

 

  看上去不錯,可以搞!搞之前得觀察一下這里面可能的一些坑。對WPF而言,圓角陰影等效果都是基本操作,這里就不說了。

  首先我們注意到上圖中有兩個拖動條,一個背景是可見光譜,另一個背景是顏色漸變和方塊平鋪的疊加。因為需求里沒有屏幕取色的功能,所以在拖動條左側的拾取圖標可以去掉,只保留當前顏色預覽,這樣會多出來一大塊空間,可以考慮將圓形的顏色預覽區域改成有圓角的矩形。而最上方的顏色拾取區域就比較復雜了,它其實是三層畫刷的疊加,第一層是洋紅色主色調,第二層是白色到透明的左右漸變,第三層是透明到黑色的上下漸變。由於WPF的帶透明通道的顏色漸變並非是標准的,舉個例子,假設有一個從透明到黑色的上下漸變層,在漸變層下方是純紅色背景,那么理論上漸變開始的顏色是#FFFF0000,漸變結束的顏色是#FF000000,那么在上下一半處的顏色應該是#FF7F0000(或者是#FF800000,就是簡單的相加除以2),但是在WPF中卻不是這個值(在專業的圖像處理軟件中比如PS中的確是#FF7F0000),如果你不信,我們現在就做個實驗。

 

二、實驗

  打開Blend,新建個WPF項目,設置窗口尺寸為400*300,為了方便定位中心點,我們需要設置 AllowsTransparency="True" , WindowStyle="None" ,接着把主窗口背景改成純紅色,再添加一層從透明到黑色的上下漸變層,用Border實現,如下圖:

  

  我們使用標尺,定位中心點(200,150),如下圖:

  

  我們看看在中心點處的顏色是什么,用Blend取色得到的顏色如下:

  

  下面我們在PS中重現以上操作,看看最后的顏色會是什么,打開PS新建400*300,分辨率為72的畫布:

  

  新建純紅色填充圖層和透明到黑色的上下漸變層,再用標尺定位一下中心點:

  

  最后得到的顏色如下:

  

  我們該相信誰?當然是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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM