在贴出来上一篇文章后,感觉那个ColorPicker太简单了,于是决定搞个Blend中的那种ColorPicker。由于工作量比较大,所以打算分成几次来完成。
首先说明一下,这个Demo还是属于未完成的阶段,比如ColorPicker的属性只是简单地设置了一个SelectedColor属性,而实际上分为A,R,G,B四个属性比较合适,这样可以在右边直接修改值;又比如,样式实在是很难看……(太累了,明天再说吧……)不过整体来看,还是能说明问题了。
【分析】
Blend中的ColorPicker其实应该分为好几个部分。首先是中间那个彩条状选择基色的Bar。我称之为ColorRainbowBar。然后是左侧基于给定的基色精确调节颜色的Box。我称之为ColorAdjuster。
这两个控件都是基于偏移量(Offset)来确定颜色的,因此,我首先定义了一个基类ColorBoxBase来提供偏移量,以及通过鼠标点击和拖拽改变偏移量的逻辑。
【控件的实现】
ColorBoxBase
这个是颜色选择器的基础。他提供了偏移量属性和鼠标事件改变偏移量的逻辑。
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
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
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
<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来确定颜色的代码。主要的原理在注释中写的比较清楚了。
// 在位移变化时重新构建颜色
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)。最后计算颜色。
它的模板如下:
<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,这个也算是讨巧了。因为两个方向的渐变均是渐变到透明,如果背景色不是白色的话,会跟控件下面的颜色发生混淆,影像颜色的效果。
<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>
让我费了费脑子的是颜色的算法。为此,我专门画了一个图来分析。
颜色的变化其实是一个F(x,y)的形式,跟水平和垂直的偏移量都有关系。同时计算其实比较难想,于是我把计算过程拆成步:首先计算水平偏移后的结果,接着水平偏移结果的基础上,接着计算垂直偏移。
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。
它没什么代码,就是一个模板。
<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
代码下载https://files.cnblogs.com/RMay/ColorPickerSeries/RMay.Demos.rar