【WPF】打造Blend风格的BrushPicker(1)

在贴出来上一篇文章后,感觉那个ColorPicker太简单了,于是决定搞个Blend中的那种ColorPicker。由于工作量比较大,所以打算分成几次来完成。

 【WPF】打造Blend风格的BrushPicker(1) 

首先说明一下,这个Demo还是属于未完成的阶段,比如ColorPicker的属性只是简单地设置了一个SelectedColor属性,而实际上分为A,R,G,B四个属性比较合适,这样可以在右边直接修改值;又比如,样式实在是很难看……(太累了,明天再说吧……)不过整体来看,还是能说明问题了。

 

【分析】

 

    Blend中的ColorPicker其实应该分为好几个部分。首先是中间那个彩条状选择基色的Bar。我称之为ColorRainbowBar。然后是左侧基于给定的基色精确调节颜色的Box。我称之为ColorAdjuster。

 

    这两个控件都是基于偏移量(Offset)来确定颜色的,因此,我首先定义了一个基类ColorBoxBase来提供偏移量,以及通过鼠标点击和拖拽改变偏移量的逻辑。

 

【控件的实现】

 

ColorBoxBase

 

    这个是颜色选择器的基础。他提供了偏移量属性和鼠标事件改变偏移量的逻辑。

 

【WPF】打造Blend风格的BrushPicker(1)【WPF】打造Blend风格的BrushPicker(1)Code
    public class ColorBoxBase : Control
    {
        
#region HorizontalOffset

        
/// <summary>
        
/// Gets HorizontalOffset
        
/// </summary>
        public double HorizontalOffset
        {
            
get { return (double)GetValue(HorizontalOffsetProperty); }
            
protected set { SetValue(HorizontalOffsetPropertyKey, value); }
        }

        
// Using a DependencyProperty as the backing store for HorizontalOffset.  This enables animation, styling, binding, etc【WPF】打造Blend风格的BrushPicker(1)
        private static readonly DependencyPropertyKey HorizontalOffsetPropertyKey = DependencyProperty.RegisterReadOnly(
            
"HorizontalOffset",
            
typeof(double),
            
typeof(ColorBoxBase),
            
new FrameworkPropertyMetadata(0.0, OnHorizontalOffsetChanged, OnCoerceHorizontalOffset));

        
public static readonly DependencyProperty HorizontalOffsetProperty = HorizontalOffsetPropertyKey.DependencyProperty;

        
private static void OnHorizontalOffsetChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
        {
            ColorBoxBase box 
= sender as ColorBoxBase;
            
if (box != null)
            {
                box.OnHorizontalOffsetChanged((
double)e.OldValue, (double)e.NewValue);
            }
        }

        
private static object OnCoerceHorizontalOffset(DependencyObject sender, object value)
        {
            ColorBoxBase box 
= sender as ColorBoxBase;
            
if (box != null)
            {
                
double x = (double)value;

                
if (x < 0)
                    x 
= 0;
                
else if (x > box.ActualWidth)
                    x 
= box.ActualWidth;
                
return x;
            }
            
return value;
        }

        
#endregion

        
#region VerticalOffset

        
/// <summary>
        
/// Gets VerticalOffset
        
/// </summary>
        public double VerticalOffset
        {
            
get { return (double)GetValue(VerticalOffsetProperty); }
            
protected set { SetValue(VerticalOffsetPropertyKey, value); }
        }

        
// Using a DependencyProperty as the backing store for VerticalOffsett.  This enables animation, styling, binding, etc【WPF】打造Blend风格的BrushPicker(1)
        private static readonly DependencyPropertyKey VerticalOffsetPropertyKey = DependencyProperty.RegisterReadOnly(
            
"VerticalOffset",
            
typeof(double),
            
typeof(ColorBoxBase),
            
new FrameworkPropertyMetadata(0.0, OnVerticalOffsetChanged, OnCoerceVerticalOffset));

        
public static readonly DependencyProperty VerticalOffsetProperty = VerticalOffsetPropertyKey.DependencyProperty;

        
private static void OnVerticalOffsetChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
        {
            ColorBoxBase box 
= sender as ColorBoxBase;
            
if (box != null)
            {
                box.OnVerticalOffsetChanged((
double)e.OldValue, (double)e.NewValue);
            }
        }

        
private static object OnCoerceVerticalOffset(DependencyObject sender, object value)
        {
            ColorBoxBase box 
= sender as ColorBoxBase;
            
if (box != null)
            {
                
double y = (double)value;

                
if (y < 0)
                    y 
= 0;
                
else if (y > box.ActualHeight)
                    y 
= box.ActualHeight;
                
return y;
            }
            
return value;
        }

        
#endregion       

        
#region SelectedColor

        
/// <summary>
        
/// Gets/Sets SelectedColor
        
/// </summary>
        public Color SelectedColor
        {
            
get { return (Color)GetValue(SelectedColorProperty); }
            
set { SetValue(SelectedColorProperty, value); }
        }

        
private static readonly DependencyProperty SelectedColorProperty = DependencyProperty.Register(
            
"SelectedColor",
            
typeof(Color),
            
typeof(ColorBoxBase),
            
new FrameworkPropertyMetadata(Colors.Red, OnSelectedColorChanged));

        
private static void OnSelectedColorChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
        {
            ColorBoxBase box 
= sender as ColorBoxBase;
            
if (box != null)
            {
                box.OnSelectedColorChanged(e);
                box.RaiseEvent(
new ColorChangedEventArgs(SelectedColorChangedEvent, box, (Color)e.OldValue, (Color)e.NewValue));
            }
        }

        
#endregion

        
#region SelectedColorChangedEvent

        
public static RoutedEvent SelectedColorChangedEvent =
            EventManager.RegisterRoutedEvent(
"SelectedColorChanged", RoutingStrategy.Bubble, typeof(ColorChangedEventHandler), typeof(ColorBoxBase));

        
public event ColorChangedEventHandler SelectedColorChanged
        {
            add { AddHandler(SelectedColorChangedEvent, value); }
            remove { RemoveHandler(SelectedColorChangedEvent, value); }
        }

        
#endregion
        
        
#region Methods for override

        
protected virtual void OnHorizontalOffsetChanged(double oldValue, double newValue)
        {
            
//
        }

        
protected virtual void OnVerticalOffsetChanged(double oldValue, double newValue)
        {
            
//
        }

        
protected virtual void OnSelectedColorChanged(DependencyPropertyChangedEventArgs e)
        {

        }

        
#endregion

        
#region "Drag" the pointer

        
protected override void OnRenderSizeChanged(SizeChangedInfo sizeInfo)
        {
            
base.OnRenderSizeChanged(sizeInfo);

            
double previousHeight = sizeInfo.PreviousSize.Height;
            
double ratio = this.VerticalOffset / previousHeight;
            
this.VerticalOffset = sizeInfo.NewSize.Height * ratio;

            
double previousWidth = sizeInfo.PreviousSize.Width;
            ratio 
= this.HorizontalOffset / previousWidth;
            
this.HorizontalOffset = sizeInfo.NewSize.Width * ratio;
        }

        
protected override void OnPreviewMouseLeftButtonDown(MouseButtonEventArgs e)
        {
            
base.OnPreviewMouseLeftButtonDown(e);

            
this.HorizontalOffset = e.GetPosition(this).X;
            
this.VerticalOffset = e.GetPosition(this).Y;

            
if (!this.IsMouseCaptured)
            {
                
this.CaptureMouse();
            }
        }

        
protected override void OnPreviewMouseMove(MouseEventArgs e)
        {
            
base.OnPreviewMouseMove(e);
            
if (e.LeftButton == MouseButtonState.Pressed)
            {
                
this.HorizontalOffset = e.GetPosition(this).X;
                
this.VerticalOffset = e.GetPosition(this).Y;
            }
        }

        
protected override void OnPreviewMouseLeftButtonUp(MouseButtonEventArgs e)
        {
            
base.OnPreviewMouseLeftButtonUp(e);
            
if (this.IsMouseCaptured)
            {
                
this.ReleaseMouseCapture();
            }
        }

        
#endregion
    }

 

    代码有点长,但是大部分都是在定义依赖属性。注意,其中的HorizontalOffset和VerticalOffset都是只读属性。并且,实际上的“拖拽”逻辑并不直接去控制UI,而仅仅是改变了Offset,这种思路是WPF里面很常见的。稍后我们就会看到如何在模板里面通过绑定来实现拖拽的效果。

 

ColorRainbowBar

 

    ColorRainbowBar由ColorBoxBase继承而来,比较有意思的是它的背景色,其实是一个有7个GradientStop的LinearGradientBrush

 

【WPF】打造Blend风格的BrushPicker(1)【WPF】打造Blend风格的BrushPicker(1)Code
    <LinearGradientBrush x:Key="ColorRainbowBarBrush" StartPoint="0,0" EndPoint="0 1">
        
<GradientStop Color="#FFFF0000" Offset="0.0"/>
        
<GradientStop Color="#FFFFFF00" Offset="0.166"/>
        
<GradientStop Color="#FF00FF00" Offset="0.333"/>
        
<GradientStop Color="#FF00FFFF" Offset="0.5"/>
        
<GradientStop Color="#FF0000FF" Offset="0.666"/>
        
<GradientStop Color="#FFFF00FF" Offset="0.833"/>
        
<GradientStop Color="#FFFF0000" Offset="1.0"/>
    
</LinearGradientBrush>

 

    其中,最主要的地方是根据Offset来确定颜色的代码。主要的原理在注释中写的比较清楚了。

 

【WPF】打造Blend风格的BrushPicker(1)【WPF】打造Blend风格的BrushPicker(1)Code
       // 在位移变化时重新构建颜色
        protected override void OnVerticalOffsetChanged(double oldValue, double newValue)
        {
            
//////////////////////////////////////////////////////////////////
            // 0: R:255  G:0     B:0         0 / 3 --- 0, 0  
            
// 1: R:255  G:255   B:0         1 / 3 --- 0, 1      0  +1   0
            
// 2: R:0    G:255   B:0         2 / 3 --- 0, 2     -1   0   0
            
// 3: R:0    G:255   B:255       3 / 3 --- 1, 0      0   0  +1
            
// 4: R:0    G:0     B:255       4 / 3 --- 1, 1      0  -1   0
            
// 5: R:255  G:0     B:255       5 / 3 --- 1, 2     +1   0   0
            
// 6: R:255  G:0     B:0         6 / 3 --- 2, 0      0   0  -1
            //////////////////////////////////////////////////////////////////

            
// 计算总的颜色数
            
// 一共6个区间
            int totalCount = 256 * 6;
            
// 计算颜色的位置偏移
            int colorOffset = (int)(this.VerticalOffset / this.ActualHeight * totalCount);
            
if (colorOffset < 0)
            {
                colorOffset 
= 0;
            }
            
else if (colorOffset > totalCount)
            {
                colorOffset 
= totalCount;
            }

            
// 计算属于哪个区间
            int offsetBase = colorOffset / 256;
            
// 计算相对于该区间的偏移
            int relativeOffset = colorOffset - 256 * offsetBase;

            
byte a = 255;
            
byte r = 0, g = 0, b = 0;
            
int dr = 0, dg = 0, db = 0;

            
// 设置各个区间的数据
            switch (offsetBase)
            {
                
case 0:
                    r 
= 255;
                    g 
= 0;
                    b 
= 0;
                    dg 
= +1;
                    
break;
                
case 1:
                    r 
= 255;
                    g 
= 255;
                    b 
= 0;
                    dr 
= -1;
                    
break;
                
case 2:
                    r 
= 0;
                    g 
= 255;
                    b 
= 0;
                    db 
= +1;
                    
break;
                
case 3:
                    r 
= 0;
                    g 
= 255;
                    b 
= 255;
                    dg 
= -1;
                    
break;
                
case 4:
                    r 
= 0;
                    g 
= 0;
                    b 
= 255;
                    dr 
= +1;
                    
break;
                
case 5:
                    r 
= 255;
                    g 
= 0;
                    b 
= 255;
                    db 
= -1;
                    
break;
                
case 6:
                    r 
= 255;
                    g 
= 0;
                    b 
= 0;
                    
break;
                
default:
                    r 
= 255;
                    g 
= 0;
                    b 
= 0;
                    
break;
            }

            
// 构建颜色
            r += (byte)(relativeOffset * dr);
            g 
+= (byte)(relativeOffset * dg);
            b 
+= (byte)(relativeOffset * db);

            
this.SelectedColor = Color.FromArgb(a, r, g, b);
        }

 

    首先我们要根据偏移量算出属于哪个区间,然后算出相对于该区间的偏移量,接着根据最前面注释中的表中的分析,确定每个颜色的改变方向(FF还是00)。最后计算颜色。

    它的模板如下: 

 

【WPF】打造Blend风格的BrushPicker(1)【WPF】打造Blend风格的BrushPicker(1)Code
    <Style TargetType="{x:Type local:ColorRainbowBar}">
        
<Setter Property="Background" Value="{StaticResource ColorRainbowBarBrush}"/>
        
<Setter Property="Foreground" Value="#FFC4C4C4"/>
        
<Setter Property="Template">
            
<Setter.Value>
                
<ControlTemplate TargetType="{x:Type local:ColorRainbowBar}">
                    
<Border Background="{TemplateBinding Background}" 
                            BorderBrush
="{TemplateBinding BorderBrush}"
                            BorderThickness
="{TemplateBinding BorderThickness}">
                        
<Canvas x:Name="ThumbHolder" ClipToBounds="True">
                            
<Thumb x:Name="Thumb" Height="4" Width="{Binding ElementName=ThumbHolder, Path=ActualWidth}"
                                   Canvas.Top
="{TemplateBinding VerticalOffset}"
                                   Style
="{StaticResource ColorRainbowBarPointerThumbStyle}"/>
                        
</Canvas>                        
                    
</Border>
                
</ControlTemplate>
            
</Setter.Value>
        
</Setter>
    
</Style>

 

    注意,里面放了个Thumb,但我却没有写任何拖拽的逻辑,只是简单的绑定到Offset而已。这样当我们用鼠标拖拽的时候,感觉上就是在拖拽这个Thumb了。

 

ColorAdjuster

 

    ColorAdjuster比较麻烦,一个是它的颜色,并不是简单的LinearGradientBrush,而是包含了两个方向(水平和垂直)的复杂渐变。为了解决这个问题,我放置了两个Rectangle,一个在水平方向渐变填充,一个在垂直方向渐变填充。然后两者的颜色合成为最终的效果。需要特别注意的是,必须把LinearGradientBrush的ColorInterpolationMode设置为ScRgbLinearInterpolation,否则你会发现颜色叠加之后,中间有一条混合带的颜色特别明显。 

另外,我将它的背景色固定为White,这个也算是讨巧了。因为两个方向的渐变均是渐变到透明,如果背景色不是白色的话,会跟控件下面的颜色发生混淆,影像颜色的效果。

 

【WPF】打造Blend风格的BrushPicker(1)【WPF】打造Blend风格的BrushPicker(1)Code
    <Style TargetType="{x:Type local:ColorAdjuster}">
        
<Setter Property="Template">
            
<Setter.Value>
                
<ControlTemplate TargetType="{x:Type local:ColorAdjuster}">
                    
<Border Background="White"
                            BorderBrush
="{TemplateBinding BorderBrush}"
                            BorderThickness
="{TemplateBinding BorderThickness}">
                        
<Grid>
                            
<Rectangle x:Name="HorizontalColor">
                                
<Rectangle.Fill>
                                    
<LinearGradientBrush StartPoint="1,0.5" EndPoint="0,0.5" ColorInterpolationMode="ScRgbLinearInterpolation">
                                        
<GradientStop Color="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=BaseColor}" Offset="0"/>
                                        
<GradientStop Color="#00000000" Offset="1"/>                                          
                                    
</LinearGradientBrush>
                                
</Rectangle.Fill>
                            
</Rectangle>
                            
<Rectangle x:Name="VerticalColor">
                                
<Rectangle.Fill>
                                    
<LinearGradientBrush StartPoint="0.5,1" EndPoint="0.5,0" ColorInterpolationMode="ScRgbLinearInterpolation">
                                        
<GradientStop Color="#FF000000" Offset="0"/>
                                        
<GradientStop Color="#00000000" Offset="1"/>
                                    
</LinearGradientBrush>
                                
</Rectangle.Fill>
                            
</Rectangle>
                            
<Canvas ClipToBounds="True">
                                
<Thumb x:Name="Thumb" Height="10" Width="10"
                                       Canvas.Left
="{TemplateBinding HorizontalOffset}"
                                       Canvas.Top
="{TemplateBinding VerticalOffset}"
                                       Style
="{StaticResource ColorAdjusterPointerThumbStyle}"/>
                            
</Canvas>
                        
</Grid>
                    
</Border>
                
</ControlTemplate>
            
</Setter.Value>
        
</Setter>
    
</Style>

 

让我费了费脑子的是颜色的算法。为此,我专门画了一个图来分析。

【WPF】打造Blend风格的BrushPicker(1)

    颜色的变化其实是一个F(x,y)的形式,跟水平和垂直的偏移量都有关系。同时计算其实比较难想,于是我把计算过程拆成步:首先计算水平偏移后的结果,接着水平偏移结果的基础上,接着计算垂直偏移。

 

【WPF】打造Blend风格的BrushPicker(1)【WPF】打造Blend风格的BrushPicker(1)Code
        private void CreateColor()
        {
            Color baseColor 
= this.BaseColor;
            
double xRatio = 1.0 - this.HorizontalOffset / this.ActualWidth;
            
double yRatio = 1.0 - this.VerticalOffset / this.ActualHeight;

            
// 计算差量
            
// 首先是X方向差量,然后以此为基准,计算Y方向差量
            byte currentR = (byte)(baseColor.R + (255 - baseColor.R) * xRatio);
            currentR 
= (byte)(currentR * yRatio);

            
byte currentG = (byte)(baseColor.G + (255 - baseColor.G) * xRatio);
            currentG 
= (byte)(currentG * yRatio);

            
byte currentB = (byte)(baseColor.B + (255 - baseColor.B) * xRatio);
            currentB 
= (byte)(currentB * yRatio);

            
this.SelectedColor = Color.FromArgb(255, currentR, currentG, currentB);
        }

 

ColorPicker

 

    最后的工作就是把前面做的控件组合起来,做一个ColorPicker。我这里使用的是CustomControl,但其实UserControl也没什么问题。

我原来的打算是做一个SolidColorBrushPicker,一个GradientBrushPicker,可以直接返回Brush,但今天实在太累了,也就凑合着做了个ColorPicker,主要是为了测试一下。明天开始继续进军BrushPicker。

它没什么代码,就是一个模板。

 

【WPF】打造Blend风格的BrushPicker(1)【WPF】打造Blend风格的BrushPicker(1)Code
    <Style TargetType="{x:Type local:ColorPicker}">
        
<Setter Property="Background" Value="Gray"/>
        
<Setter Property="Template">
            
<Setter.Value>
                
<ControlTemplate TargetType="{x:Type local:ColorPicker}">
                    
<Border Background="{TemplateBinding Background}"
                            BorderBrush
="{TemplateBinding BorderBrush}"
                            BorderThickness
="{TemplateBinding BorderThickness}">
                        
<DockPanel Margin="4">
                            
<Grid DockPanel.Dock="Right" Width="100">
                                
<Grid.RowDefinitions>
                                    
<RowDefinition Height="24"/>
                                    
<RowDefinition Height="24"/>
                                    
<RowDefinition Height="24"/>
                                    
<RowDefinition Height="24"/>
                                    
<RowDefinition Height="*"/>
                                
</Grid.RowDefinitions>
                                
<Grid.ColumnDefinitions>
                                    
<ColumnDefinition Width="24"/>
                                    
<ColumnDefinition Width="*"/>
                                
</Grid.ColumnDefinitions>
                                
<TextBlock Grid.Row="0" Grid.Column="0" Text="R" VerticalAlignment="Center"/>
                                
<TextBlock Grid.Row="1" Grid.Column="0" Text="G" VerticalAlignment="Center"/>
                                
<TextBlock Grid.Row="2" Grid.Column="0" Text="B" VerticalAlignment="Center"/>
                                
<TextBlock Grid.Row="3" Grid.Column="0" Text="A" VerticalAlignment="Center"/>
                                
<Border Grid.Row="0" Grid.Column="1"
                                        BorderBrush
="Gray" BorderThickness="1" Background="Red">
                                    
<TextBlock HorizontalAlignment="Center" VerticalAlignment="Center" 
                                               Text
="{Binding ElementName=Adjuster, Path=SelectedColor.R}"/>
                                
</Border>
                                
<Border Grid.Row="1" Grid.Column="1"
                                        BorderBrush
="Gray" BorderThickness="1" Background="Green">
                                    
<TextBlock HorizontalAlignment="Center" VerticalAlignment="Center" 
                                               Text
="{Binding ElementName=Adjuster, Path=SelectedColor.G}"/>
                                
</Border>
                                
<Border Grid.Row="2" Grid.Column="1"
                                        BorderBrush
="Gray" BorderThickness="1" Background="Blue">
                                    
<TextBlock HorizontalAlignment="Center" VerticalAlignment="Center" 
                                               Text
="{Binding ElementName=Adjuster, Path=SelectedColor.B}"/>
                                
</Border>
                                
<Border Grid.Row="3" Grid.Column="1"
                                        BorderBrush
="Gray" BorderThickness="1" Background="Red">
                                    
<TextBlock HorizontalAlignment="Center" VerticalAlignment="Center" 
                                               Text
="{Binding ElementName=Adjuster, Path=SelectedColor.A}"/>
                                
</Border>
                            
</Grid>
                            
<DockPanel>
                                
<local:ColorRainbowBar x:Name="RainbowBar" DockPanel.Dock="Right" Width="20"/>
                                
<local:ColorAdjuster x:Name="Adjuster" 
                                                     BaseColor
="{Binding ElementName=RainbowBar, Path=SelectedColor}"
                                                     SelectedColor
="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=SelectedColor, Mode=TwoWay}"/>
                            
</DockPanel>
                        
</DockPanel>
                    
</Border>
                
</ControlTemplate>
            
</Setter.Value>
        
</Setter>
    
</Style>

 

【下篇预告】

 

    好了,其实到此为止,我们需要攻坚的两个东东,ColorRainbowBar和ColorAdjuster已经完成了,剩下的任务就是把选出的Color转换成需要的Brush,下一篇里我打算讲讲怎么搞个SolidColorBrushPicker出来,如果快的话,估计GradientBrushPicker也能出来:)

 

注:我的开发环境是Vista SP1 + .Net 3.5 SP1 + VS2008 SP1

代码下载http://files.cnblogs.com/RMay/ColorPickerSeries/RMay.Demos.rar

转载于:https://www.cnblogs.com/RMay/archive/2008/09/08/1286879.html