前面的三篇文章,已经把核心部分的代码贴出来了。
有些同学可能还不太理解自动以控件怎么写,我这就把完整的代码贴出来。
这里需要注意一下几点:
1. 依赖属性有点多,可以根据自己的需要取舍;
2. AccordingPixel()方法,是将第1篇文章和第2篇文章的代码融合而来。因为在一个类中,这样可以减少创建Color对象的过程。实测提升效率40%。半径越大,提升越大。
3. HSB模式的代码,是我从网上抄的,不是我写的。我只是做了个整合。
using System;
using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
namespace RayLib.WPF
{
/// <summary>
/// 绘制ColorWheel的方法
/// </summary>
public enum ColorWheelMethod
{
HSB, Pixel
}
/// <summary>
/// 颜色选择器
/// </summary>
[TemplatePart(Name = elementName, Type = typeof(Image))]
public class ColorPicker : Control
{
const string elementName = "Part_Image";
private Image? image;
#region 依赖项属性
internal static readonly DependencyProperty SourceProperty = DependencyProperty.Register(
"Source", typeof(WriteableBitmap), typeof(ColorPicker), new PropertyMetadata(default));
internal static readonly DependencyProperty RedPorperty = DependencyProperty.Register(
"Red", typeof(byte), typeof(ColorPicker), new PropertyMetadata(byte.MinValue, OnValueChanged));
internal static readonly DependencyProperty GreenProperty = DependencyProperty.Register(
"Green", typeof(byte), typeof(ColorPicker), new PropertyMetadata(byte.MinValue, OnValueChanged));
internal static readonly DependencyProperty BlueProperty = DependencyProperty.Register(
"Blue", typeof(byte), typeof(ColorPicker), new PropertyMetadata(byte.MinValue, OnValueChanged));
internal static readonly DependencyProperty AlphaProperty = DependencyProperty.Register(
"Alpha", typeof(byte), typeof(ColorPicker), new PropertyMetadata(byte.MaxValue, OnValueChanged));
internal static readonly DependencyProperty SelectColorProperty = DependencyProperty.Register(
"SelectColor", typeof(Color), typeof(ColorPicker), new PropertyMetadata(Colors.LimeGreen));
internal static readonly DependencyProperty SelectBrushProperty = DependencyProperty.Register(
"SelectBrush", typeof(SolidColorBrush), typeof(ColorPicker), new PropertyMetadata(Brushes.LimeGreen));
internal static readonly DependencyProperty ColorStringProperty = DependencyProperty.Register(
"ColorString", typeof(string), typeof(ColorPicker), new PropertyMetadata(string.Empty));
internal static readonly DependencyProperty HexStringProperty = DependencyProperty.Register(
"HexString", typeof(string), typeof(ColorPicker), new PropertyMetadata(string.Empty));
internal static readonly DependencyProperty MethodProperty = DependencyProperty.Register(
"Method", typeof(ColorWheelMethod), typeof(ColorPicker), new PropertyMetadata(ColorWheelMethod.Pixel, OnMethodChange));
internal static readonly DependencyProperty RadiusProperty = DependencyProperty.Register(
"Radius", typeof(int), typeof(ColorPicker), new PropertyMetadata(256, OnRadiusChanged));
internal static readonly DependencyProperty OrientationProperty = DependencyProperty.Register(
"Orientation", typeof(Orientation), typeof(ColorPicker), new PropertyMetadata(Orientation.Horizontal));
internal static readonly DependencyProperty VerticalOffsetProperty = DependencyProperty.Register(
"VerticalOffset", typeof(double), typeof(ColorPicker), new FrameworkPropertyMetadata(0.0, FrameworkPropertyMetadataOptions.AffectsMeasure));
internal static readonly DependencyProperty HorizontalOffsetProperty = DependencyProperty.Register(
"HorizontalOffset", typeof(double), typeof(ColorPicker), new FrameworkPropertyMetadata(0.0, FrameworkPropertyMetadataOptions.AffectsMeasure));
// 当R\G\B\A的值发生改变时,调用该方法,会设置SelectColor、SelectBrush、ColorString、HexString等属性的值
private static void OnValueChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
ColorPicker instance = (ColorPicker)d;
Color newColor = new() { A = instance.Alpha, B = instance.Blue, G = instance.Green, R = instance.Red };
instance.SelectColor = newColor;
instance.SelectBrush = new SolidColorBrush(newColor);
instance.ColorString = $"( R:{newColor.R}, G:{newColor.G}, B:{newColor.B}, A:{newColor.A} )";
instance.HexString = $"#{newColor.R:X2}{newColor.G:X2}{newColor.B:X2}{newColor.A:X2}";
instance.OnSelectedColor();
}
// 当渲染方法发生改变时
private static void OnMethodChange(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
ColorPicker instance = (ColorPicker)d;
instance.Drawing();
}
// 当Radius属性的值发生改变时,调用该方法
private static void OnRadiusChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
ColorPicker instance = (ColorPicker)d;
instance.Drawing();
}
public override void OnApplyTemplate()
{
if (image == null) return;
image.MouseDown -= Image_MouseDown; // 卸载事件
if (GetTemplateChild(elementName) is Image temp)
{
image = temp;
image.MouseDown += Image_MouseDown;
}
}
private void Image_MouseDown(object sender, MouseButtonEventArgs e)
{
if (image == null) return;
Point mouse = e.GetPosition(image);
int x = (int)(image.Source.Width / image.ActualWidth * mouse.X);
int y = (int)(image.Source.Height / image.ActualHeight * mouse.Y);
unsafe
{
Source.Lock();
IntPtr pointer = Source.BackBuffer;
int offset = (x + y * Source.PixelWidth) * Source.Format.BitsPerPixel / 8;
Blue = Marshal.ReadByte(pointer, offset);
Green = Marshal.ReadByte(pointer, offset + 1);
Red = Marshal.ReadByte(pointer, offset + 2);
Alpha = Marshal.ReadByte(pointer, offset + 3);
Source.Unlock();
}
}
#endregion
#region 依赖性属性包装器
/// <summary>
/// 调色盘图像
/// </summary>
public WriteableBitmap Source
{
get => (WriteableBitmap)GetValue(SourceProperty);
set => SetValue(SourceProperty, value);
}
/// <summary>
/// 红色值
/// </summary>
public byte Red
{
get => (byte)GetValue(RedPorperty);
set => SetValue(RedPorperty, value);
}
/// <summary>
/// 绿色值
/// </summary>
public byte Green
{
get => (byte)GetValue(GreenProperty);
set => SetValue(GreenProperty, value);
}
/// <summary>
/// 绿色值
/// </summary>
public byte Blue
{
get => (byte)GetValue(BlueProperty);
set => SetValue(BlueProperty, value);
}
/// <summary>
/// 透明度
/// </summary>
public byte Alpha
{
get => (byte)GetValue(AlphaProperty);
set => SetValue(AlphaProperty, value);
}
/// <summary>
/// 选择的颜色
/// </summary>
public Color SelectColor
{
get => (Color)GetValue(SelectColorProperty);
set => SetValue(SelectColorProperty, value);
}
/// <summary>
/// 选择的颜色构成的画刷
/// </summary>
public SolidColorBrush SelectBrush
{
get => (SolidColorBrush)GetValue(SelectBrushProperty);
set => SetValue(SelectBrushProperty, value);
}
/// <summary>
/// 颜色值的字符串表示,含Alpha值
/// </summary>
public string ColorString
{
get => (string)GetValue(ColorStringProperty);
set => SetValue(ColorStringProperty, value);
}
/// <summary>
/// 颜色值的十六进制表示,含Alpha值
/// </summary>
public string HexString
{
get => (string)GetValue(HexStringProperty);
set => SetValue(HexStringProperty, value);
}
/// <summary>
/// 生成调色盘的方法
/// </summary>
public ColorWheelMethod Method
{
get => (ColorWheelMethod)GetValue(MethodProperty);
set => SetValue(MethodProperty, value);
}
/// <summary>
/// 调色盘图像的像素点半径:与控件的宽度无关
/// </summary>
public int Radius
{
get => (int)GetValue(RadiusProperty);
set => SetValue(RadiusProperty, value);
}
/// <summary>
/// 排列方向
/// </summary>
public Orientation Orientation
{
get => (Orientation)GetValue(OrientationProperty);
set => SetValue(OrientationProperty, value);
}
/// <summary>
/// 垂直方向偏移量
/// </summary>
public double VerticalOffset
{
get => (double)GetValue(VerticalOffsetProperty);
set => SetValue(VerticalOffsetProperty, value);
}
/// <summary>
/// 水平方向偏移量
/// </summary>
public double HorizontalOffset
{
get => (double)GetValue(HorizontalOffsetProperty);
set => SetValue(HorizontalOffsetProperty, value);
}
#endregion
#region 路由事件
internal static readonly RoutedEvent SelectedColorEvent = EventManager.RegisterRoutedEvent(
"SelectedColor", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(ColorPicker));
/// <summary>
/// 选择了颜色事件
/// </summary>
public event RoutedEventHandler SelectedColor
{
add => AddHandler(SelectedColorEvent, value);
remove => RemoveHandler(SelectedColorEvent, value);
}
// 触发SelectedColor事件
protected void OnSelectedColor()
{
RoutedEventArgs e = new(SelectedColorEvent, this);
RaiseEvent(e);
}
#endregion
static ColorPicker()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(ColorPicker), new FrameworkPropertyMetadata(typeof(ColorPicker)));
}
public ColorPicker()
{
Drawing();
}
// 根据Method的值绘制色轮
private void Drawing()
{
switch (Method)
{
case ColorWheelMethod.HSB:
AccordingHSB();
break;
case ColorWheelMethod.Pixel:
AccordingPixel();
break;
}
}
#region 采用逐像素点的模式
// 采用计算逐个像素点的颜色的方法绘制色轮
private void AccordingPixel()
{
int radius = Radius;
Vector o = new(radius, 0); // 定义X轴向量
WriteableBitmap bitmap = new(radius * 2 + 1, radius * 2 + 1, 96, 96, PixelFormats.Pbgra32, null);
unsafe
{
bitmap.Lock();
IntPtr pointer = bitmap.BackBuffer; // 找到内存中的图像的首地址
byte r, g, b;
for (int y = 0; y < bitmap.PixelHeight; y++)
{
for (int x = 0; x < bitmap.PixelWidth; x++)
{
double distance = Math.Sqrt(Math.Pow(x - radius, 2) + Math.Pow(y - radius, 2)); // 到圆心的距离
if (distance > radius) continue;
Vector v1 = new(x - radius, radius - y); // 坐标换算后的向量
double angel = Vector.AngleBetween(o, v1); // 计算两个向量的夹角
if (angel <= -60)
{
// 落在蓝色区间
r = CalculateSpread(distance, -angel - 60);
g = CalculateSpread(distance, 180 + angel);
b = 255;
}
else if (angel <= 60)
{
// 落在红色区间
r = 255;
g = CalculateSpread(distance, 60 - angel);
b = CalculateSpread(distance, 60 + angel);
}
else
{
// 落在绿色区间
r = CalculateSpread(distance, angel - 60);
g = 255;
b = CalculateSpread(distance, 180 - angel);
}
int offset = (x + y * bitmap.PixelWidth) * bitmap.Format.BitsPerPixel / 8;
Marshal.WriteByte(pointer, offset, b);
Marshal.WriteByte(pointer, offset + 1, g);
Marshal.WriteByte(pointer, offset + 2, r);
Marshal.WriteByte(pointer, offset + 3, 255);
}
}
Int32Rect rect = new(0, 0, bitmap.PixelWidth, bitmap.PixelHeight); // 要更新的区域
bitmap.AddDirtyRect(rect);
bitmap.Unlock();
}
Source = bitmap;
}
/// <summary>
/// 计算某个颜色的分量。distance为点到圆心的距离;angel为点与圆心的连线,与颜色的边界之间的夹角
/// </summary>
private byte CalculateSpread(double distance, double angel)
{
// 计算圆周分量:如果两个向量的夹角大于60度,则圆周分量为0;否则,计算比例递减
double circum = angel > 60 ? 0 : 255 - angel / 60 * 255;
return (byte)(255 - distance / Radius * (255 - circum));
}
#endregion
#region 采用HSB模式
private void AccordingHSB()
{
int radius = Radius;
Vector o = new(radius, 0); // 圆心的向量:x轴正向为0°
WriteableBitmap bitmap = new(radius * 2 + 1, radius * 2 + 1, 96, 96, PixelFormats.Pbgra32, null);
unsafe
{
bitmap.Lock();
IntPtr pointer = bitmap.BackBuffer; // 找到内存中的图像的首地址
for (int y = 0; y < bitmap.PixelHeight; y++)
{
for (int x = 0; x < bitmap.PixelWidth; x++)
{
double s = Math.Sqrt(Math.Pow(x - radius, 2) + Math.Pow(y - radius, 2)) / radius; // (x,y)点到圆心的距离,与r的比值
if (s > 1) continue; // 在圆之外,跳过
Vector v1 = new(x - radius, radius - y); // 点(x,y)以圆心为坐标系原点的坐标
double h = Vector.AngleBetween(o, v1); // 从o->v1的夹角,逆时针为正,顺时针为负
if (h <= 0) h += 360;
Color c = HsbToColor(h, s, 1.0); // 将 HSB模式转换为RGB颜色模式
int offset = (x + y * bitmap.PixelWidth) * bitmap.Format.BitsPerPixel / 8;
Marshal.WriteByte(pointer, offset, c.B);
Marshal.WriteByte(pointer, offset + 1, c.G);
Marshal.WriteByte(pointer, offset + 2, c.R);
Marshal.WriteByte(pointer, offset + 3, c.A);
}
}
Int32Rect rect = new(0, 0, bitmap.PixelWidth, bitmap.PixelHeight); // 要更新的区域
bitmap.AddDirtyRect(rect);
bitmap.Unlock();
}
Source = bitmap;
}
// HSB颜色模式:h:色相(相位),s:饱和度,brightness:明度
private static Color HsbToColor(double h, double s, double brightness)
{
if (h == 360) h = 0;
double r = 0;
double g = 0;
double b = 0;
if (s == 0)
{
r = g = b = brightness;
}
else
{
double sectorPos = h / 60f; // 将圆划分为6等分,每等分60°。
int sectorNum = (int)Math.Floor(sectorPos); // 取整数部分
double fractionalSector = sectorPos - sectorNum; // 取小数部分
double p = brightness * (1 - s);
double q = brightness * (1 - s * fractionalSector);
double t = brightness * (1 - s * (1 - fractionalSector));
switch (sectorNum)
{
case 0:
r = brightness;
g = t;
b = p;
break;
case 1:
r = q;
g = brightness;
b = p;
break;
case 2:
r = p;
g = brightness;
b = t;
break;
case 3:
r = p;
g = q;
b = brightness;
break;
case 4:
r = t;
g = p;
b = brightness;
break;
case 5:
r = brightness;
g = p;
b = q;
break;
}
}
byte byteR = (byte)(r * 255), byteG = (byte)(g * 255), byteB = (byte)(b * 255);
return Color.FromRgb(byteR, byteG, byteB);
}
#endregion
}
}
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:rayCtl ="clr-namespace:RayLib.WPF" >
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="/RayLib;component/Themes/Public.xaml" />
</ResourceDictionary.MergedDictionaries>
<Style TargetType="{x:Type rayCtl:ColorPicker}">
<Setter Property="Width" Value="100" />
<Setter Property="Height" Value="20"/>
<Setter Property="Background" Value="White"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type rayCtl:ColorPicker}">
<StackPanel Orientation="Horizontal">
<Rectangle Name="rect"
Width="{TemplateBinding Width}"
Height="{TemplateBinding Height}"
Fill="{TemplateBinding SelectBrush}"/>
<Popup Name="popup" StaysOpen="False" PlacementTarget="{Binding ElementName=rect}" AllowsTransparency="True"
Placement="Bottom"
HorizontalOffset="{TemplateBinding HorizontalOffset}"
VerticalOffset="{TemplateBinding VerticalOffset}">
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{Binding Path=(rayCtl:CornerElement.CornerRadius), RelativeSource={RelativeSource Mode=Self}}">
<Border.Resources>
<Style TargetType="Rectangle">
<Setter Property="Width" Value="30"/>
<Setter Property="Height" Value="15" />
<Setter Property="Margin" Value="5"/>
</Style>
<Style TargetType="Slider">
<Setter Property="Minimum" Value="0"/>
<Setter Property="Maximum" Value="255"/>
<Setter Property="Width" Value="100"/>
<Setter Property="Grid.Column" Value="1"/>
<Setter Property="VerticalAlignment" Value="Center"/>
<Setter Property="Template" Value="{StaticResource SliderBase}"/>
</Style>
<Style TargetType="TextBox">
<Setter Property="Grid.Column" Value="2"/>
<Setter Property="VerticalContentAlignment" Value="Center"/>
<Setter Property="HorizontalContentAlignment" Value="Center"/>
<Setter Property="Margin" Value="5"/>
</Style>
</Border.Resources>
<Grid HorizontalAlignment="Left" VerticalAlignment="Top">
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="auto"/>
</Grid.ColumnDefinitions>
<Image Name="Part_Image" Stretch="Uniform" Cursor="Cross" Width="200" Height="200"
Source="{TemplateBinding Source}" />
<Grid Name="valuePanel" Grid.Column="1" HorizontalAlignment="Left" VerticalAlignment="Center">
<Grid.RowDefinitions>
<RowDefinition Height="auto"/>
<RowDefinition Height="auto"/>
<RowDefinition Height="auto"/>
<RowDefinition Height="auto"/>
<RowDefinition Height="auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Rectangle Fill="{TemplateBinding SelectBrush}" Grid.Row="0"/>
<Rectangle Fill="Red" Grid.Row="1"/>
<Rectangle Fill="Green" Grid.Row="2"/>
<Rectangle Fill="Blue" Grid.Row="3"/>
<Rectangle Grid.Row="4">
<Rectangle.Fill>
<LinearGradientBrush>
<GradientStop Color="Transparent" Offset="0"/>
<GradientStop Color="Black" Offset="1"/>
</LinearGradientBrush>
</Rectangle.Fill>
</Rectangle >
<TextBox Text="{TemplateBinding HexString}" Grid.Row="0" IsReadOnly="True"/>
<Slider Name="sliderR" Grid.Row="1" Value="{Binding Red, Mode=TwoWay,RelativeSource={RelativeSource Mode=TemplatedParent}, Converter={StaticResource byte2Double}}" />
<Slider Name="sliderG" Grid.Row="2" Value="{Binding Green, Mode=TwoWay,RelativeSource={RelativeSource Mode=TemplatedParent}, Converter={StaticResource byte2Double}}" />
<Slider Name="sliderB" Grid.Row="3" Value="{Binding Blue, Mode=TwoWay,RelativeSource={RelativeSource Mode=TemplatedParent}, Converter={StaticResource byte2Double}}" />
<Slider Name="sliderA" Grid.Row="4" Value="{Binding Alpha, Mode=TwoWay,RelativeSource={RelativeSource Mode=TemplatedParent}, Converter={StaticResource byte2Double}}" />
</Grid>
</Grid>
</Border>
</Popup>
</StackPanel>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="popup" Property="IsOpen" Value="True"/>
</Trigger>
<Trigger Property="Orientation" Value="Vertical">
<Setter TargetName="valuePanel" Property="Grid.Row" Value="1"/>
<Setter TargetName="valuePanel" Property="Grid.Column" Value="0"/>
<Setter TargetName="valuePanel" Property="HorizontalAlignment" Value="Center"/>
<Setter TargetName="valuePanel" Property="VerticalAlignment" Value="Top"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>