重命名

This commit is contained in:
GG Z
2026-01-02 17:30:30 +08:00
parent 0e9db9a2b9
commit fa0d280130
245 changed files with 4405 additions and 4236 deletions

View File

@@ -0,0 +1,25 @@
namespace VariaStudio.Controls;
// 定义颜色输入模式的枚举
/// <summary>
/// 定义了颜色输入模式的枚举。此枚举用于指定在颜色选择器中用户可以选择的颜色表示方式。
/// </summary>
public enum ColorInputMode
{
/// <summary>
/// 表示颜色输入模式为十六进制格式。在这种模式下,用户可以使用十六进制代码(如 #FF5733来指定颜色。
/// </summary>
HEX,
/// <summary>
/// 表示颜色输入模式为HSB色相、饱和度、亮度格式。在这种模式下用户可以通过调整色相、饱和度和亮度来指定颜色。
/// </summary>
HSB,
/// <summary>
/// 表示颜色输入模式为RGB格式。在这种模式下用户可以通过分别设置红色、绿色和蓝色的分量值来指定颜色。
/// </summary>
RGB,
}

View File

@@ -0,0 +1,234 @@
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:assists="clr-namespace:VariaStudio.Assists"
xmlns:controls="clr-namespace:VariaStudio.Controls"
xmlns:converters="clr-namespace:VariaStudio.Converters"
xmlns:internal="clr-namespace:VariaStudio.Converters.Internal"
xmlns:markup="clr-namespace:VariaStudio.Markup"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<!-- 1. ColorPanel 样式 -->
<Style TargetType="{x:Type controls:ColorPanel}">
<Setter Property="Padding" Value="4" />
<Setter Property="assists:ControlAssist.CornerRadius" Value="8" />
<Setter Property="Background" Value="{DynamicResource ControlBackgroundNormalBrush}" />
<Setter Property="Width" Value="340" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type controls:ColorPanel}">
<ControlTemplate.Resources>
<!-- 为每种模式定义一个数据模板 -->
<DataTemplate x:Key="HexInputTemplate">
<TextBox HorizontalContentAlignment="Center" Text="{Binding SelectedColor, RelativeSource={RelativeSource AncestorType=controls:ColorPanel}, Converter={x:Static internal:ColorToHexConverter.Instance}, UpdateSourceTrigger=PropertyChanged}" />
</DataTemplate>
<DataTemplate x:Key="HsbInputTemplate">
<UniformGrid Rows="1">
<TextBox HorizontalContentAlignment="Center" Text="{Binding Hue, RelativeSource={RelativeSource AncestorType=controls:ColorPanel}, UpdateSourceTrigger=PropertyChanged}" />
<TextBox HorizontalContentAlignment="Center" Text="{Binding Saturation, RelativeSource={RelativeSource AncestorType=controls:ColorPanel}, Converter={x:Static converters:PercentageConverter.Instance}, UpdateSourceTrigger=PropertyChanged}" />
<TextBox HorizontalContentAlignment="Center" Text="{Binding Brightness, RelativeSource={RelativeSource AncestorType=controls:ColorPanel}, Converter={x:Static converters:PercentageConverter.Instance}, UpdateSourceTrigger=PropertyChanged}" />
</UniformGrid>
</DataTemplate>
<DataTemplate x:Key="RgbInputTemplate">
<UniformGrid Rows="1">
<TextBox HorizontalContentAlignment="Center" Text="{Binding Red, RelativeSource={RelativeSource AncestorType=controls:ColorPanel}, UpdateSourceTrigger=PropertyChanged}" />
<TextBox HorizontalContentAlignment="Center" Text="{Binding Green, RelativeSource={RelativeSource AncestorType=controls:ColorPanel}, UpdateSourceTrigger=PropertyChanged}" />
<TextBox HorizontalContentAlignment="Center" Text="{Binding Blue, RelativeSource={RelativeSource AncestorType=controls:ColorPanel}, UpdateSourceTrigger=PropertyChanged}" />
</UniformGrid>
</DataTemplate>
</ControlTemplate.Resources>
<Grid>
<Border
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{TemplateBinding assists:ControlAssist.CornerRadius}" />
<!-- Effect="{DynamicResource PopupShadow}" -->
<StackPanel Margin="{TemplateBinding Padding}">
<Canvas
ClipToBounds="True"
Height="150"
x:Name="PART_ColorCanvas">
<Rectangle
Fill="{Binding Path=Hue, RelativeSource={RelativeSource TemplatedParent}, Converter={x:Static internal:HueToBrushConverter.Instance}}"
Height="{Binding ActualHeight, ElementName=PART_ColorCanvas}"
Width="{Binding ActualWidth, ElementName=PART_ColorCanvas}" />
<Rectangle Height="{Binding ActualHeight, ElementName=PART_ColorCanvas}" Width="{Binding ActualWidth, ElementName=PART_ColorCanvas}">
<Rectangle.Fill>
<LinearGradientBrush EndPoint="1,0" StartPoint="0,0">
<GradientStop Color="White" Offset="0" />
<GradientStop Color="Transparent" Offset="1" />
</LinearGradientBrush>
</Rectangle.Fill>
</Rectangle>
<Rectangle Height="{Binding ActualHeight, ElementName=PART_ColorCanvas}" Width="{Binding ActualWidth, ElementName=PART_ColorCanvas}">
<Rectangle.Fill>
<LinearGradientBrush EndPoint="0,1" StartPoint="0,0">
<GradientStop Color="Transparent" Offset="0" />
<GradientStop Color="Black" Offset="1" />
</LinearGradientBrush>
</Rectangle.Fill>
</Rectangle>
<Thumb
Height="16"
Margin="-8,-8,0,0"
Width="16"
x:Name="PART_ColorSelector">
<Thumb.Template>
<ControlTemplate TargetType="Thumb">
<Grid>
<Ellipse
Fill="Transparent"
Stroke="{DynamicResource BackgroundFloatingBrush}"
StrokeThickness="2" />
<Ellipse
Fill="Transparent"
Margin="1"
Stroke="{DynamicResource ControlBackgroundNormalBrush}"
StrokeThickness="1" />
</Grid>
</ControlTemplate>
</Thumb.Template>
</Thumb>
</Canvas>
<Grid Margin="0,12,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<StackPanel
Grid.Column="0"
Margin="0,0,12,0"
VerticalAlignment="Center">
<Slider
Maximum="360"
Minimum="0"
Style="{DynamicResource ColorSliderStyle}"
Value="{Binding Hue, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}">
<Slider.Background>
<LinearGradientBrush EndPoint="1,0" StartPoint="0,0">
<GradientStop Color="Red" Offset="0.0" />
<GradientStop Color="Yellow" Offset="0.166" />
<GradientStop Color="Green" Offset="0.333" />
<GradientStop Color="Cyan" Offset="0.5" />
<GradientStop Color="Blue" Offset="0.666" />
<GradientStop Color="Magenta" Offset="0.833" />
<GradientStop Color="Red" Offset="1.0" />
</LinearGradientBrush>
</Slider.Background>
</Slider>
<Slider
Margin="0,8,0,0"
Maximum="255"
Minimum="0"
Style="{DynamicResource ColorSliderStyle}"
Value="{Binding Alpha, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}">
<Slider.Background>
<LinearGradientBrush EndPoint="1,0" StartPoint="0,0">
<GradientStop Color="Transparent" Offset="0.0" />
<GradientStop Offset="1.0">
<GradientStop.Color>
<Binding
Converter="{x:Static internal:ColorToOpaqueColorConverter.Instance}"
Path="SelectedColor"
RelativeSource="{RelativeSource TemplatedParent}" />
</GradientStop.Color>
</GradientStop>
</LinearGradientBrush>
</Slider.Background>
</Slider>
</StackPanel>
<Border
Background="{DynamicResource CheckerboardBrushLarge}"
BorderBrush="{DynamicResource BorderNormalBrush}"
BorderThickness="1"
CornerRadius="4"
Grid.Column="1"
Height="36"
Width="36">
<Rectangle RadiusX="3" RadiusY="3">
<Rectangle.Fill>
<SolidColorBrush Color="{Binding SelectedColor, RelativeSource={RelativeSource TemplatedParent}}" />
</Rectangle.Fill>
</Rectangle>
</Border>
</Grid>
<Grid Margin="0,12,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="64" />
</Grid.ColumnDefinitions>
<ComboBox
Grid.Column="0"
HorizontalContentAlignment="Center"
ItemsSource="{markup:EnumSource EnumType=controls:ColorInputMode}"
SelectedItem="{Binding ColorInputMode, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}"
x:Name="ColorModeComboBox" />
<ContentControl Grid.Column="1">
<ContentControl.Style>
<Style TargetType="ContentControl">
<Style.Triggers>
<!-- 使用DataTrigger来切换ContentTemplate -->
<DataTrigger Binding="{Binding ColorInputMode, RelativeSource={RelativeSource AncestorType=controls:ColorPanel}}" Value="{x:Static controls:ColorInputMode.HEX}">
<Setter Property="ContentTemplate" Value="{StaticResource HexInputTemplate}" />
</DataTrigger>
<DataTrigger Binding="{Binding ColorInputMode, RelativeSource={RelativeSource AncestorType=controls:ColorPanel}}" Value="{x:Static controls:ColorInputMode.HSB}">
<Setter Property="ContentTemplate" Value="{StaticResource HsbInputTemplate}" />
</DataTrigger>
<DataTrigger Binding="{Binding ColorInputMode, RelativeSource={RelativeSource AncestorType=controls:ColorPanel}}" Value="{x:Static controls:ColorInputMode.RGB}">
<Setter Property="ContentTemplate" Value="{StaticResource RgbInputTemplate}" />
</DataTrigger>
</Style.Triggers>
</Style>
</ContentControl.Style>
</ContentControl>
<TextBox
Grid.Column="2"
HorizontalContentAlignment="Center"
Text="{Binding Alpha, RelativeSource={RelativeSource TemplatedParent}, Converter={x:Static internal:AlphaToPercentConverter.Instance}, UpdateSourceTrigger=PropertyChanged}"
x:Name="AlphaTextBox" />
</Grid>
<ItemsControl ItemsSource="{Binding PresetColors, RelativeSource={RelativeSource TemplatedParent}}" Margin="0,12,0,-6">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Button
Command="{Binding SelectPresetColorCommand, RelativeSource={RelativeSource AncestorType=controls:ColorPanel}}"
CommandParameter="{Binding}"
Height="22"
Margin="0,0,8,8"
ToolTip="{Binding}"
Width="22">
<Button.Template>
<ControlTemplate TargetType="Button">
<Border
Background="{DynamicResource CheckerboardBrushLarge}"
BorderBrush="{DynamicResource BorderNormalBrush}"
BorderThickness="1"
CornerRadius="2">
<Rectangle RadiusX="1" RadiusY="1">
<Rectangle.Fill>
<SolidColorBrush Color="{Binding}" />
</Rectangle.Fill>
</Rectangle>
</Border>
</ControlTemplate>
</Button.Template>
</Button>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>

View File

@@ -0,0 +1,441 @@
using System.Diagnostics;
using System.Windows.Controls.Primitives;
using System.Windows.Input;
using VariaStudio.Utilities;
namespace VariaStudio.Controls;
/// <summary>
/// ColorPanel 控件提供了一个用于选择颜色的界面。用户可以通过此控件来选取特定的颜色支持多种颜色输入模式如HEX、RGB和HSV。
/// </summary>
/// <remarks>
/// 该控件定义了多个依赖属性来表示颜色的不同组成部分如Alpha透明度、Hue色调、Saturation饱和度、Brightness亮度以及RGB值
/// 并且允许通过绑定或其他方式动态地改变这些属性。此外,还支持预设颜色列表的选择功能。
/// </remarks>
[TemplatePart(Name = "PART_ColorCanvas", Type = typeof(Canvas))]
[TemplatePart(Name = "PART_ColorSelector", Type = typeof(Thumb))]
public class ColorPanel : Control
{
private Thumb? colorSelector;
private Canvas? colorCanvas;
private bool isUpdating;
#region Events
/// <summary>
/// 当颜色被选中时触发的路由事件。此事件允许监听者响应颜色选择的变化。
/// </summary>
public static readonly RoutedEvent ColorSelectedEvent =
EventManager.RegisterRoutedEvent(nameof(ColorSelected), RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(ColorPanel));
/// <summary>
/// 当颜色被选中时触发的事件。此事件允许监听者响应颜色选择的变化通常用于更新UI或执行其他逻辑。
/// </summary>
public event RoutedEventHandler ColorSelected
{
add => AddHandler(ColorSelectedEvent, value);
remove => RemoveHandler(ColorSelectedEvent, value);
}
#endregion
#region Commands
/// <summary>
/// 选择预设颜色的命令。此命令允许用户从预定义的颜色列表中选取颜色。
/// </summary>
public ICommand SelectPresetColorCommand { get; }
private void ExecuteSelectPresetColor(object parameter)
{
if (parameter is Color color)
{
SelectedColor = color;
RaiseEvent(new RoutedEventArgs(ColorSelectedEvent));
}
}
#endregion
#region Dependency Properties
/// <summary>
/// 颜色模式
/// </summary>
public ColorInputMode ColorInputMode
{
get => (ColorInputMode)GetValue(ColorInputModeProperty);
set => SetValue(ColorInputModeProperty, value);
}
/// <summary>
///
/// </summary>
public static readonly DependencyProperty ColorInputModeProperty =
DependencyProperty.Register(nameof(ColorInputMode), typeof(ColorInputMode), typeof(ColorPanel), new PropertyMetadata(ColorInputMode.HEX));
//private static void OnColorInputModeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
//{
//}
/// <summary>
/// 选定颜色的依赖属性。此属性用于获取或设置当前选中的颜色值,并支持双向数据绑定。
/// </summary>
public static readonly DependencyProperty SelectedColorProperty = DependencyProperty.Register(nameof(SelectedColor), typeof(Color),
typeof(ColorPanel), new FrameworkPropertyMetadata(Colors.Red, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnSelectedColorChanged));
/// <summary>
/// 获取或设置当前选中的颜色。此属性支持双向数据绑定,并在颜色更改时触发相关事件。
/// </summary>
public Color SelectedColor
{
get => (Color)GetValue(SelectedColorProperty);
set => SetValue(SelectedColorProperty, value);
}
/// <summary>
/// 表示颜色透明度的依赖属性。此属性允许绑定和双向数据更新其默认值为255完全不透明
/// </summary>
public static readonly DependencyProperty AlphaProperty = DependencyProperty.Register(nameof(Alpha), typeof(byte), typeof(ColorPanel),
new FrameworkPropertyMetadata((byte)255, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnAlphaChanged));
/// <summary>
/// 表示颜色的透明度Alpha通道值。此属性支持双向数据绑定允许用户通过UI控件直接修改颜色的透明度。
/// </summary>
public byte Alpha
{
get => (byte)GetValue(AlphaProperty);
set => SetValue(AlphaProperty, value);
}
/// <summary>
/// 用于表示颜色面板中色调值的依赖属性。此属性允许用户设置或获取当前选定颜色的色调部分取值范围为0到360的整数。
/// </summary>
public static readonly DependencyProperty HueProperty =
DependencyProperty.Register(nameof(Hue), typeof(int), typeof(ColorPanel), new PropertyMetadata(0, OnHsvChanged));
/// <summary>
/// 获取或设置颜色的色调值范围为0到360的整数。此属性用于指定当前选定颜色的色相部分并在颜色模型中与其他属性如饱和度和亮度共同定义一个特定的颜色。
/// </summary>
public int Hue
{
get => (int)GetValue(HueProperty);
set => SetValue(HueProperty, value);
}
/// <summary>
/// 表示颜色饱和度的依赖属性。此属性允许设置或获取颜色的饱和度值范围通常在0.0到1.0之间。
/// </summary>
public static readonly DependencyProperty SaturationProperty =
DependencyProperty.Register(nameof(Saturation), typeof(double), typeof(ColorPanel), new PropertyMetadata(1.0, OnHsvChanged));
/// <summary>
/// 表示颜色的饱和度。饱和度是HSV颜色模型中的一个分量用于描述颜色的纯度或灰度。
/// 值为0表示灰色无色值为1表示该颜色最纯的状态。
/// </summary>
public double Saturation
{
get => (double)GetValue(SaturationProperty);
set => SetValue(SaturationProperty, value);
}
/// <summary>
/// 用于控制颜色亮度的依赖属性。此属性允许用户设置或获取颜色面板中的亮度值范围通常在0.0到1.0之间。
/// </summary>
public static readonly DependencyProperty BrightnessProperty =
DependencyProperty.Register(nameof(Brightness), typeof(double), typeof(ColorPanel), new PropertyMetadata(1.0, OnHsvChanged));
/// <summary>
/// 表示颜色的亮度。此属性的值范围从0.0到1.0其中0.0表示完全黑暗1.0表示最大亮度。
/// </summary>
public double Brightness
{
get => (double)GetValue(BrightnessProperty);
set => SetValue(BrightnessProperty, value);
}
/// <summary>
/// RGB颜色模式下的红色分量依赖属性。此属性允许用户设置或获取当前选定颜色的红色分量值取值范围为0到255。
/// </summary>
public static readonly DependencyProperty RedProperty =
DependencyProperty.Register(nameof(Red), typeof(byte), typeof(ColorPanel), new FrameworkPropertyMetadata((byte)255, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnRgbChanged));
/// <summary>
/// 获取或设置颜色的红色分量值范围为0到255。此属性用于RGB颜色模式下指定当前选定颜色的红色部分。
/// </summary>
public byte Red
{
get => (byte)GetValue(RedProperty);
set => SetValue(RedProperty, value);
}
/// <summary>
/// RGB颜色模式下的绿色分量依赖属性。此属性允许用户设置或获取当前选定颜色的绿色分量值取值范围为0到255。
/// </summary>
public static readonly DependencyProperty GreenProperty =
DependencyProperty.Register(nameof(Green), typeof(byte), typeof(ColorPanel), new FrameworkPropertyMetadata((byte)0, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnRgbChanged));
/// <summary>
/// 获取或设置颜色的绿色分量值范围为0到255。此属性用于RGB颜色模式下指定当前选定颜色的绿色部分。
/// </summary>
public byte Green
{
get => (byte)GetValue(GreenProperty);
set => SetValue(GreenProperty, value);
}
/// <summary>
/// RGB颜色模式下的蓝色分量依赖属性。此属性允许用户设置或获取当前选定颜色的蓝色分量值取值范围为0到255。
/// </summary>
public static readonly DependencyProperty BlueProperty =
DependencyProperty.Register(nameof(Blue), typeof(byte), typeof(ColorPanel), new FrameworkPropertyMetadata((byte)0, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnRgbChanged));
/// <summary>
/// 获取或设置颜色的蓝色分量值范围为0到255。此属性用于RGB颜色模式下指定当前选定颜色的蓝色部分。
/// </summary>
public byte Blue
{
get => (byte)GetValue(BlueProperty);
set => SetValue(BlueProperty, value);
}
/// <summary>
/// 预设颜色的依赖属性。此属性用于获取或设置预定义的颜色列表,这些颜色可以被用户选择。
/// </summary>
public static readonly DependencyProperty PresetColorsProperty =
DependencyProperty.Register(nameof(PresetColors), typeof(List<Color>), typeof(ColorPanel), new PropertyMetadata(null));
/// <summary>
/// 预设颜色列表。此属性允许用户定义一组预设的颜色,这些颜色可以在颜色选择器中直接使用。
/// </summary>
public List<Color> PresetColors
{
get => (List<Color>)GetValue(PresetColorsProperty);
set => SetValue(PresetColorsProperty, value);
}
#endregion
static ColorPanel()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(ColorPanel), new FrameworkPropertyMetadata(typeof(ColorPanel)));
}
/// <summary>
/// 用于选择颜色的面板控件。此控件允许用户通过HSV颜色模型调整颜色同时也支持预设颜色的选择。
/// </summary>
public ColorPanel()
{
SelectPresetColorCommand = new RelayCommand(ExecuteSelectPresetColor);
Loaded += OnPanelLoaded;
}
/// <summary>
/// 当控件模板应用到此控件时调用。在此方法中,通过模板获取颜色选择器和颜色画布的引用,并为这些元素添加事件处理程序。
/// </summary>
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
colorCanvas = (Canvas?)GetTemplateChild("PART_ColorCanvas");
colorSelector = (Thumb?)GetTemplateChild("PART_ColorSelector");
if (colorSelector == null || colorCanvas == null) return;
colorSelector.DragDelta += ColorSelector_DragDelta;
colorCanvas.MouseLeftButtonDown += ColorCanvas_MouseLeftButtonDown;
colorCanvas.SizeChanged += ColorCanvas_SizeChanged;
}
private void OnPanelLoaded(object sender, RoutedEventArgs e)
{
UpdateHsvFromColor(this.SelectedColor);
UpdateRgbFromColor(this.SelectedColor);
UpdateSelectorPosition();
}
private void ColorCanvas_SizeChanged(object sender, SizeChangedEventArgs e)
{
if (e.NewSize is not { Width: > 0, Height: > 0 }) return;
UpdateSelectorPosition();
if (colorCanvas != null) colorCanvas.SizeChanged -= ColorCanvas_SizeChanged;
}
#region Callbacks
private static void OnSelectedColorChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var p = (ColorPanel)d;
if (p.isUpdating) return;
p.isUpdating = true;
var newColor = (Color)e.NewValue;
p.Alpha = newColor.A;
p.UpdateHsvFromColor(newColor);
p.UpdateRgbFromColor(newColor);
p.UpdateSelectorPosition();
p.isUpdating = false;
}
private static void OnAlphaChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var p = (ColorPanel)d;
if (p.isUpdating) return;
p.isUpdating = true;
p.SelectedColor = Color.FromArgb((byte)e.NewValue, p.Red, p.Green, p.Blue);
p.isUpdating = false;
}
private static void OnHsvChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var p = (ColorPanel)d;
if (p.isUpdating) return;
p.isUpdating = true;
var newColor = HsvToRgb(p.Hue, p.Saturation, p.Brightness, p.Alpha);
p.SelectedColor = newColor;
p.UpdateRgbFromColor(newColor);
p.isUpdating = false;
p.UpdateSelectorPosition();
}
private static void OnRgbChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var p = (ColorPanel)d;
if (p.isUpdating) return;
p.isUpdating = true;
var newColor = Color.FromArgb(p.Alpha, p.Red, p.Green, p.Blue);
p.SelectedColor = newColor;
p.UpdateHsvFromColor(newColor);
p.isUpdating = false;
p.UpdateSelectorPosition();
}
#endregion
#region UI Handlers
private void ColorCanvas_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
UpdateSaturationAndBrightness(e.GetPosition(colorCanvas));
Debug.Assert(colorCanvas != null, nameof(colorCanvas) + " != null");
if (colorCanvas == null) return;
colorCanvas.CaptureMouse();
colorCanvas.MouseMove += ColorCanvas_MouseMove;
colorCanvas.MouseUp += ColorCanvas_MouseUp;
}
private void ColorCanvas_MouseUp(object sender, MouseButtonEventArgs e)
{
Debug.Assert(colorCanvas != null, nameof(colorCanvas) + " != null");
if (colorCanvas == null) return;
colorCanvas.ReleaseMouseCapture();
colorCanvas.MouseMove -= ColorCanvas_MouseMove;
colorCanvas.MouseUp -= ColorCanvas_MouseUp;
}
private void ColorCanvas_MouseMove(object sender, MouseEventArgs e)
{
if (e.LeftButton == MouseButtonState.Pressed) UpdateSaturationAndBrightness(e.GetPosition(colorCanvas));
}
private void ColorSelector_DragDelta(object sender, DragDeltaEventArgs e)
{
if (colorSelector != null)
UpdateSaturationAndBrightness(new Point(Canvas.GetLeft(colorSelector) + e.HorizontalChange,
Canvas.GetTop(colorSelector) + e.VerticalChange));
}
private void UpdateSaturationAndBrightness(Point position)
{
switch (colorCanvas)
{
case { ActualWidth: 0 }:
case null:
return;
default:
Saturation = Math.Max(0, Math.Min(1, position.X / colorCanvas.ActualWidth));
Brightness = Math.Max(0, Math.Min(1, 1 - position.Y / colorCanvas.ActualHeight));
break;
}
}
private void UpdateHsvFromColor(Color color)
{
var hsv = RgbToHsv(color.R, color.G, color.B);
Hue = (int)Math.Round(hsv.Hue); // 转换为整数
Saturation = hsv.Saturation;
Brightness = hsv.Brightness;
}
private void UpdateRgbFromColor(Color color)
{
Red = color.R;
Green = color.G;
Blue = color.B;
}
private void UpdateSelectorPosition()
{
if (colorCanvas is not { ActualWidth: > 0 }) return;
if (colorSelector == null) return;
Canvas.SetLeft(colorSelector, Saturation * colorCanvas.ActualWidth);
Canvas.SetTop(colorSelector, (1 - Brightness) * colorCanvas.ActualHeight);
}
#endregion
#region Color Conversion
private static HSVColor RgbToHsv(byte r, byte g, byte b)
{
double redNormalized = r / 255.0, greenNormalized = g / 255.0, blueNormalized = b / 255.0;
double max = Math.Max(redNormalized, Math.Max(greenNormalized, blueNormalized)),
min = Math.Min(redNormalized, Math.Min(greenNormalized, blueNormalized));
var delta = max - min;
double h = 0;
if (delta != 0)
{
if (Math.Abs(max - redNormalized) < 10e-6) h = 60 * (((greenNormalized - blueNormalized) / delta) % 6);
else if (Math.Abs(max - greenNormalized) < 10e-6) h = 60 * (((blueNormalized - redNormalized) / delta) + 2);
else h = 60 * (((redNormalized - greenNormalized) / delta) + 4);
}
if (h < 0) h += 360;
var s = max == 0 ? 0 : delta / max;
return new HSVColor(h, s, max);
}
internal static Color HsvToRgb(double h, double s, double v, byte a)
{
double r = 0, g = 0, b = 0, c = v * s, x = c * (1 - Math.Abs((h / 60) % 2 - 1)), m = v - c;
switch (h)
{
case < 60:
r = c;
g = x;
break;
case < 120:
r = x;
g = c;
break;
case < 180:
g = c;
b = x;
break;
case < 240:
g = x;
b = c;
break;
case < 300:
r = x;
b = c;
break;
default:
r = c;
b = x;
break;
}
return Color.FromArgb(a, (byte)((r + m) * 255), (byte)((g + m) * 255), (byte)((b + m) * 255));
}
#endregion
}

View File

@@ -0,0 +1,213 @@
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:assists="clr-namespace:VariaStudio.Assists"
xmlns:controls="clr-namespace:VariaStudio.Controls"
xmlns:effects="clr-namespace:VariaStudio.Effects"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<!-- 两种尺寸的棋盘格 -->
<DrawingBrush
TileMode="Tile"
Viewport="0,0,16,16"
ViewportUnits="Absolute"
x:Key="CheckerboardBrushLarge">
<DrawingBrush.Drawing>
<DrawingGroup>
<GeometryDrawing Brush="White">
<GeometryDrawing.Geometry>
<GeometryGroup>
<RectangleGeometry Rect="0,0,8,8" />
<RectangleGeometry Rect="8,8,8,8" />
</GeometryGroup>
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FFE0E0E0">
<GeometryDrawing.Geometry>
<GeometryGroup>
<RectangleGeometry Rect="8,0,8,8" />
<RectangleGeometry Rect="0,8,8,8" />
</GeometryGroup>
</GeometryDrawing.Geometry>
</GeometryDrawing>
</DrawingGroup>
</DrawingBrush.Drawing>
</DrawingBrush>
<DrawingBrush
TileMode="Tile"
Viewport="0,0,8,8"
ViewportUnits="Absolute"
x:Key="CheckerboardBrushSmall">
<DrawingBrush.Drawing>
<DrawingGroup>
<GeometryDrawing Brush="White">
<GeometryDrawing.Geometry>
<GeometryGroup>
<RectangleGeometry Rect="0,0,4,4" />
<RectangleGeometry Rect="4,4,4,4" />
</GeometryGroup>
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FFE0E0E0">
<GeometryDrawing.Geometry>
<GeometryGroup>
<RectangleGeometry Rect="4,0,4,4" />
<RectangleGeometry Rect="0,4,4,4" />
</GeometryGroup>
</GeometryDrawing.Geometry>
</GeometryDrawing>
</DrawingGroup>
</DrawingBrush.Drawing>
</DrawingBrush>
<Style TargetType="{x:Type RepeatButton}" x:Key="SliderRepeatButtonStyle">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type RepeatButton}">
<Grid Background="Transparent" />
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style TargetType="{x:Type Slider}" x:Key="ColorSliderStyle">
<Setter Property="Height" Value="16" />
<Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Slider}">
<Grid>
<Grid
Height="8"
Margin="6,0"
x:Name="TrackBackground">
<Rectangle
Fill="{StaticResource CheckerboardBrushSmall}"
RadiusX="4"
RadiusY="4" />
<Rectangle
Fill="{TemplateBinding Background}"
RadiusX="4"
RadiusY="4" />
</Grid>
<Track x:Name="PART_Track">
<Track.DecreaseRepeatButton>
<RepeatButton Command="{x:Static Slider.DecreaseLarge}" Style="{StaticResource SliderRepeatButtonStyle}" />
</Track.DecreaseRepeatButton>
<Track.IncreaseRepeatButton>
<RepeatButton Command="{x:Static Slider.IncreaseLarge}" Style="{StaticResource SliderRepeatButtonStyle}" />
</Track.IncreaseRepeatButton>
<Track.Thumb>
<Thumb
FocusVisualStyle="{x:Null}"
Height="12"
Width="12">
<Thumb.Template>
<ControlTemplate TargetType="Thumb">
<Grid>
<Ellipse
Fill="Transparent"
Stroke="White"
StrokeThickness="2">
<Ellipse.Effect>
<DropShadowEffect
BlurRadius="2"
Color="{DynamicResource DarkShadowColor}"
Opacity="0.8"
ShadowDepth="0" />
</Ellipse.Effect>
</Ellipse>
</Grid>
</ControlTemplate>
</Thumb.Template>
</Thumb>
</Track.Thumb>
</Track>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- 2. ColorPicker 样式 -->
<Style TargetType="{x:Type controls:ColorPicker}">
<Setter Property="Width" Value="32" />
<Setter Property="Height" Value="32" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type controls:ColorPicker}">
<Grid>
<ToggleButton x:Name="PART_Trigger">
<ToggleButton.Template>
<ControlTemplate TargetType="ToggleButton">
<Border
Background="{StaticResource CheckerboardBrushLarge}"
BorderBrush="{DynamicResource BorderNormalBrush}"
BorderThickness="1"
CornerRadius="4"
x:Name="border">
<Rectangle RadiusX="3" RadiusY="3">
<Rectangle.Fill>
<SolidColorBrush Color="{Binding Path=SelectedColor, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=controls:ColorPicker}}" />
</Rectangle.Fill>
</Rectangle>
<!--<Border.Effect>
<DropShadowEffect
BlurRadius="4"
Opacity="0.6"
ShadowDepth="1"
Color="{DynamicResource DarkShadowColor}" />
</Border.Effect>-->
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Effect">
<Setter.Value>
<effects:BrightnessContrastEffect Brightness="0.08" />
</Setter.Value>
</Setter>
<Setter Property="Effect" TargetName="border">
<Setter.Value>
<DropShadowEffect
BlurRadius="0"
Color="{DynamicResource DarkShadowColor}"
Opacity="0.4"
ShadowDepth="0" />
</Setter.Value>
</Setter>
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter Property="Effect">
<Setter.Value>
<effects:BrightnessContrastEffect Brightness="0.06" />
</Setter.Value>
</Setter>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</ToggleButton.Template>
</ToggleButton>
<Popup
AllowsTransparency="True"
IsOpen="{Binding IsChecked, ElementName=PART_Trigger, Mode=TwoWay}"
Placement="Bottom"
PlacementTarget="{Binding RelativeSource={RelativeSource TemplatedParent}}"
PopupAnimation="None"
StaysOpen="True"
assists:BehaviorAssist.SimulateNativeBehavior="True"
x:Name="PART_Popup">
<Border
Background="{TemplateBinding Background}"
BorderThickness="{TemplateBinding BorderThickness}"
Margin="4">
<controls:ColorPanel
Margin="8"
PresetColors="{Binding Path=PlacementTarget.PresetColors, RelativeSource={RelativeSource AncestorType=Popup}}"
SelectedColor="{Binding Path=PlacementTarget.SelectedColor, RelativeSource={RelativeSource AncestorType=Popup}, Mode=TwoWay}" />
</Border>
</Popup>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>

View File

@@ -0,0 +1,99 @@
namespace VariaStudio.Controls
{
/// <summary>
/// ColorPicker 控件允许用户从一系列预设颜色中选择一个颜色,或者自定义颜色。该控件支持双向数据绑定,并且在颜色值发生更改时会自动更新显示。
/// </summary>
/// <remarks>
/// 本控件定义了三个主要的依赖属性SelectedColor、IsOpen 和 PresetColors。通过这些属性可以控制当前选中的颜色、控件的打开/关闭状态以及可用的颜色列表。
/// </remarks>
[TemplatePart(Name = "PART_ColorPanel", Type = typeof(ColorPanel))]
public class ColorPicker : Control
{
/// <summary>
/// 代表 ColorPicker 控件中用于显示和选择颜色的面板。此组件允许用户从预设的颜色列表中选取颜色,同时也支持自定义颜色的选择。
/// 在控件模板中通过 "PART_ColorPanel" 名称引用,并且在颜色被选中时会触发相关事件来关闭颜色选择器。
/// </summary>
private ColorPanel? colorPanel;
#region Dependency Properties
/// <summary>
/// 表示 ColorPicker 控件中当前选中的颜色的依赖属性。
/// 该属性为 Color 类型用于获取或设置控件内用户选定的颜色。默认值设置为红色Colors.Red
/// 此属性支持双向数据绑定,并且在颜色值发生更改时会触发控件重绘。
/// </summary>
public static readonly DependencyProperty SelectedColorProperty =
DependencyProperty.Register(nameof(SelectedColor), typeof(Color), typeof(ColorPicker),
new FrameworkPropertyMetadata(
Colors.Red,
FrameworkPropertyMetadataOptions.BindsTwoWayByDefault | FrameworkPropertyMetadataOptions.AffectsRender
));
/// <summary>
/// 表示 ColorPicker 控件中当前选中的颜色的依赖属性。
/// 该属性为 Color 类型用户可以通过此属性获取或设置当前选定的颜色。默认值为红色Colors.Red
/// 属性支持双向数据绑定,并且任何运行时的颜色变化都会触发控件重绘。
/// </summary>
public Color SelectedColor
{
get => (Color)GetValue(SelectedColorProperty);
set => SetValue(SelectedColorProperty, value);
}
/// <summary>
/// 表示 ColorPicker 控件是否处于打开状态的依赖属性。
/// 该属性为布尔类型,当设置为 true 时,表示控件已打开;设置为 false 时,表示控件已关闭。
/// </summary>
public static readonly DependencyProperty IsOpenProperty =
DependencyProperty.Register(nameof(IsOpen), typeof(bool), typeof(ColorPicker), new PropertyMetadata(false));
/// <summary>
/// 获取或设置一个布尔值,指示 ColorPicker 控件是否处于打开状态。
/// 当该属性为 true 时,表示控件已打开;当该属性为 false 时,表示控件已关闭。
/// </summary>
public bool IsOpen
{
get => (bool)GetValue(IsOpenProperty);
set => SetValue(IsOpenProperty, value);
}
/// <summary>
/// 表示 ColorPicker 控件中预设颜色列表的依赖属性。
/// 该属性包含一个颜色列表,用户可以从这些预设的颜色中进行选择。默认情况下,它包含了多种常用的颜色。
/// </summary>
public static readonly DependencyProperty PresetColorsProperty =
DependencyProperty.Register(nameof(PresetColors), typeof(List<Color>), typeof(ColorPicker),
new PropertyMetadata(new List<Color> { Color.FromRgb(244, 67, 54), Color.FromRgb(233, 30, 99), Color.FromRgb(156, 39, 176), Color.FromRgb(103, 58, 183), Color.FromRgb(63, 81, 181), Color.FromRgb(33, 150, 243), Color.FromRgb(0, 188, 212), Color.FromRgb(0, 150, 136), Color.FromRgb(76, 175, 80), Color.FromRgb(205, 220, 57), Color.FromRgb(255, 235, 59), Color.FromRgb(255, 152, 0) }));
/// <summary>
/// 表示 ColorPicker 控件中预设颜色列表的依赖属性。
/// 该属性为 类型,包含了一系列预定义的颜色选项,用户可以从这些颜色中选择。
/// </summary>
public List<Color> PresetColors
{
get => (List<Color>)GetValue(PresetColorsProperty);
set => SetValue(PresetColorsProperty, value);
}
#endregion
static ColorPicker()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(ColorPicker), new FrameworkPropertyMetadata(typeof(ColorPicker)));
}
/// <summary>
/// 当控件的模板被应用时调用此方法。在此方法中ColorPicker 控件会尝试获取其模板中的 ColorPanel 部分,并为其 ColorSelected 事件添加处理程序。
/// 如果成功获取到 ColorPanel则设置一个事件处理器当用户从 ColorPanel 中选择了一个颜色后,该处理器将关闭 ColorPicker 的弹出窗口。
/// </summary>
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
colorPanel = GetTemplateChild("PART_ColorPanel") as ColorPanel;
if (colorPanel != null)
{
// 这个事件处理依然是必要的用于点击预设颜色时关闭Popup
colorPanel.ColorSelected += (_, _) => IsOpen = false;
}
}
}
}

View File

@@ -0,0 +1,43 @@
namespace VariaStudio.Controls;
/// <summary>
/// 表示HSV颜色空间中的颜色。HSV颜色模型通过色调Hue、饱和度Saturation和亮度Brightness来描述颜色。
/// 该结构体提供了这三个属性的只读访问并且通过构造函数接收这三个参数来初始化一个HSV颜色实例。
/// 色调Hue定义了颜色在色轮上的位置取值范围为0到360度饱和度Saturation表示颜色的纯度取值范围从0.0灰度到1.0(全彩);
/// 亮度Brightness决定了颜色的明暗程度同样地其取值范围也是从0.0黑色到1.0(最亮)。
/// </summary>
public struct HSVColor
{
/// <summary>
/// 获取或设置当前颜色的色调值。色调定义了颜色在色轮上的位置取值范围为0到360度。
/// 通过调整此属性可以改变颜色的基本外观,例如从红色过渡到黄色等。
/// </summary>
public double Hue { get; }
/// <summary>
/// 获取或设置颜色的饱和度值。饱和度定义了颜色的纯度取值范围从0.0灰度到1.0全彩。此属性用于描述HSV颜色模型中的饱和度分量。
/// </summary>
public double Saturation { get; }
/// <summary>
/// 表示HSV颜色模型中的亮度值。亮度决定了颜色的明暗程度取值范围从0.0黑色到1.0(最亮)。此属性为只读。
/// </summary>
public double Brightness { get; }
/// <summary>
/// 表示HSV颜色空间中的颜色。该结构体用于存储和操作基于色调Hue、饱和度Saturation和亮度Brightness定义的颜色。
/// 通过构造函数初始化一个HSVColor实例其中色调范围为0到360度饱和度和亮度的取值范围均为0.0到1.0。
/// </summary>
public HSVColor(double hue,
double saturation,
double value)
{
Hue = hue;
Saturation = saturation;
Brightness = value;
}
}