前言
对无外观控件的学习记录,自定义无外观控件,使用WPF纯MVVM模式实现屏幕取色器。
一、引入库
1.CommunityToolkit.Mvvm 包(详情)(又名 MVVM 工具包,以前称为 Microsoft.Toolkit.Mvvm)是一个现代、快速和模块化的 MVVM 库。
2.Microsoft.Xaml.Behaviors.Wpf(详情),可以实现复杂命令绑定,而不是单纯的点击触发的Command。
二、创建无外观控件库
项目创建好之后会自动创建一个Themes文件夹,在此文件夹下包含一个Generic.xaml文件。
1.Generic.xaml资源文件可以创建该控件的默认样式模版。General.xmal名称不能随意修改,该资源文件必须在项目的根目录下的Therems目录中,作用是全局管理所有的控件默认模版样式。
2.可以创建多个样式资源文件,但最终必须合并到全局的General.xaml(项目目录下的Themes文件夹中)中。
XAML代码
前台样式资源文件代码
使用了三种绑定方式:
TemplateBinding:轻量级,只能单向通知
Binding 的相对绑定方式 RelativeSource,可以实现双向通知
后台C#代码绑定:需要为前台空间命名,PART_名称
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:ColorLibrary">
<Style TargetType="Slider" x:Key="SliderStyle">
<Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="Maximum" Value="255" />
<Setter Property="Minimum" Value="0" />
<Setter Property="TickPlacement" Value="BottomRight"/>
</Style>
<Style TargetType="{x:Type local:ColorPicker}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:ColorPicker}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="auto"/>
<RowDefinition Height="auto"/>
<RowDefinition Height="auto"/>
<RowDefinition Height="auto"/>
<RowDefinition Height="auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition Width="auto"/>
</Grid.ColumnDefinitions>
<!-- Three Binding Method -->
<!--{TemplateBinding Green} same as {Binding RelativeSource={RelativeSource Mode=TemplatedParent }, Path=Blue}-->
<Slider
Grid.Row="0"
Grid.Column="0"
x:Name="PART_RedSlider"
Background="Red"
Margin="{TemplateBinding Padding}"
Style="{StaticResource SliderStyle}"
/>
<Slider
Grid.Row="1"
Grid.Column="0"
x:Name="PART_GreenSlider"
Margin="{TemplateBinding Padding}"
Value="{Binding RelativeSource={RelativeSource Mode=TemplatedParent }, Path=Green}"
Background="Green"
Style="{StaticResource SliderStyle}"
/>
<Slider
Grid.Row="2"
Grid.Column="0"
x:Name="PART_BlueSlider"
Margin="{TemplateBinding Padding}"
Value="{Binding RelativeSource={RelativeSource Mode=TemplatedParent }, Path=Blue}"
Background="Blue"
Style="{StaticResource SliderStyle}"
/>
<Slider
Grid.Row="3"
Grid.Column="0"
x:Name="PART_AlphaSlider"
Margin="{TemplateBinding Padding}"
Value="{Binding RelativeSource={RelativeSource Mode=TemplatedParent }, Path=Alpha}"
Style="{StaticResource SliderStyle}"
/>
<Border Grid.RowSpan="4" Grid.Column="1" Width="200" Margin="{TemplateBinding Padding}" CornerRadius="5" >
<Border.Background>
<SolidColorBrush x:Name="PART_Brush"/>
</Border.Background>
<StackPanel>
<TextBlock Text="Sample Text" Foreground="Black" FontSize="30"/>
<TextBlock Text="Sample Text" Foreground="White" FontSize="30"/>
</StackPanel>
</Border>
<StackPanel Grid.Row="4" Grid.ColumnSpan="2" Orientation="Horizontal" Margin="{Binding ElementName=PART_RedSlider, Path=Margin}">
<StackPanel Orientation="Horizontal" Margin="0 0 10 0">
<TextBlock Text="Red" Grid.Column="0" VerticalAlignment="Center" Margin="0 0 3 0"/>
<TextBox Width="30" Text="{Binding ElementName=PART_RedSlider, Path=Value,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}" HorizontalContentAlignment="Center" VerticalAlignment="Center" Grid.Column="1"/>
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="0 0 10 0">
<TextBlock Text="Green" Grid.Column="0" VerticalAlignment="Center" Margin="0 0 3 0"/>
<TextBox Width="30" Text="{Binding ElementName=PART_GreenSlider, Path=Value,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}" HorizontalContentAlignment="Center" VerticalAlignment="Center" Grid.Column="1"/>
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="0 0 10 0">
<TextBlock Text="Blue" Grid.Column="0" VerticalAlignment="Center" Margin="0 0 3 0"/>
<TextBox Width="30" Text="{Binding ElementName=PART_BlueSlider, Path=Value,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}" HorizontalContentAlignment="Center" VerticalAlignment="Center" Grid.Column="1"/>
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="0 0 10 0">
<TextBlock Text="Alpha" Grid.Column="0" VerticalAlignment="Center" Margin="0 0 3 0"/>
<TextBox Width="30" Text="{Binding ElementName=PART_AlphaSlider, Path=Value,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}" HorizontalContentAlignment="Center" VerticalAlignment="Center" Grid.Column="1"/>
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="0 0 10 0">
<TextBlock Text="ARGB" Grid.Column="0" VerticalAlignment="Center" Margin="0 0 3 0"/>
<TextBox Width="100" Text="{Binding ElementName=PART_Brush, Path=Color,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}" HorizontalContentAlignment="Center" VerticalAlignment="Center" Grid.Column="1"/>
</StackPanel>
</StackPanel>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
后台逻辑代码
public class ColorPicker : Control
{
#region 01依赖项属性
//1.声明依赖项属性
public static readonly DependencyProperty ColorProperty;
public static readonly DependencyProperty RedProperty;
public static readonly DependencyProperty GreenProperty;
public static readonly DependencyProperty BlueProperty;
public static readonly DependencyProperty AlphaProperty;
//2.包装依赖项属性,依赖项属性也是一种属性,所以要包装一下,暴露出一般属性的特性方便使用
public Color MyColor
{
get { return (Color)GetValue(ColorProperty); }
set { SetValue(ColorProperty, value); }
}
public byte Red
{
get { return (byte)GetValue(RedProperty); }
set { SetValue(RedProperty, value); }
}
public byte Green
{
get { return (byte)GetValue(GreenProperty); }
set { SetValue(GreenProperty, value); }
}
public byte Blue
{
get { return (byte)GetValue(BlueProperty); }
set { SetValue(BlueProperty, value); }
}
public byte Alpha
{
get { return (byte)GetValue(AlphaProperty); }
set { SetValue(AlphaProperty, value); }
}
#endregion
#region 02路由事件
//1.定义路由事件
public static readonly RoutedEvent ColorChangedEvent;
//2.包装路由事件
public event RoutedPropertyChangedEventHandler<Color> ColorChanged
{
add { AddHandler(ColorChangedEvent, value); }
remove { RemoveHandler(ColorChangedEvent, value); }
}
#endregion
#region 03注册依赖项属性与路由事件
//3.注册依赖项属性与路由事件
//注意:必须再使用依赖项属性之前注册依赖项属性,所以依赖项属性总是再所在类静态构造函数或则静态字段中注册
static ColorPicker()
{
//从样式中加载模板,固定写法
DefaultStyleKeyProperty.OverrideMetadata(typeof(ColorPicker), new FrameworkPropertyMetadata(typeof(ColorPicker)));
//注册依赖项属性
ColorProperty = DependencyProperty.Register(
name: "MyColor",
propertyType: typeof(Color),
ownerType: typeof(ColorPicker),
typeMetadata: new PropertyMetadata(propertyChangedCallback: OnColorChanged) { DefaultValue = Colors.Black },//属性改变时触发的回调
validateValueCallback: ValidateValue//验证
);
RedProperty = DependencyProperty.Register(
name: "Red",
propertyType: typeof(byte),
ownerType: typeof(ColorPicker),
typeMetadata: new PropertyMetadata(propertyChangedCallback: OnRGBChanged),//属性改变时触发的回调
validateValueCallback: ValidateValue//验证
);
GreenProperty = DependencyProperty.Register(
name: "Green",
propertyType: typeof(byte),
ownerType: typeof(ColorPicker),
typeMetadata: new PropertyMetadata(propertyChangedCallback: OnRGBChanged),
validateValueCallback: ValidateValue
);
BlueProperty = DependencyProperty.Register(
name: "Blue",
propertyType: typeof(byte),
ownerType: typeof(ColorPicker),
typeMetadata: new PropertyMetadata(propertyChangedCallback: OnRGBChanged),
validateValueCallback: ValidateValue
);
AlphaProperty = DependencyProperty.Register(
name: "Alpha",
propertyType: typeof(byte),
ownerType: typeof(ColorPicker),
typeMetadata: new PropertyMetadata(propertyChangedCallback: OnRGBChanged),
validateValueCallback: ValidateValue
);
//注册路由事件
ColorChangedEvent = EventManager.RegisterRoutedEvent(
name: "ColorChanged",//事件名
routingStrategy: RoutingStrategy.Bubble,//路由类型,冒泡
handlerType: typeof(RoutedPropertyChangedEventHandler<Color>),//事件类型
ownerType: typeof(ColorPicker)//属于哪个对象
);
}
#endregion
#region 04回调函数
public static void OnColorChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
ColorPicker picker = d as ColorPicker;
//MyColor属性改变后会触发OnColorChanged()回调函数,获得新值
Color newcolor = (Color)e.NewValue;
Color oldcolor = (Color)e.OldValue;
//解析Color解析到RGB
picker.Red = newcolor.R;
picker.Green = newcolor.G;
picker.Blue = newcolor.B;
picker.Alpha = newcolor.A;
//颜色改变后调用事件
RoutedPropertyChangedEventArgs<Color> args =
new RoutedPropertyChangedEventArgs<Color>(oldcolor, newcolor, ColorChangedEvent);
picker?.RaiseEvent(args);
}
public static void OnRGBChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
ColorPicker picker = d as ColorPicker;
Color color = picker.MyColor;
if (e.Property == RedProperty)
{
color.R = (byte)e.NewValue;
}
if (e.Property == GreenProperty)
{
color.G = (byte)e.NewValue;
}
if (e.Property == BlueProperty)
{
color.B = (byte)e.NewValue;
}
if (e.Property == AlphaProperty)
{
color.A = (byte)e.NewValue;
}
picker.MyColor = color;
}
#endregion
#region 05验证
public static bool ValidateValue(object ob)
{
return true;
}
#endregion
/// <summary>
/// 当 DefaultStyleKeyProperty.OverrideMetadata初始化完成Style时回调
/// </summary>
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
//在后台代码手动建立数据绑定,必须给元素命名,方式为:PART_开头,后跟元素名称
RangeBase redSlider = GetTemplateChild("PART_RedSlider") as RangeBase;
Binding binding = new Binding("Red") //Path = Red
{
Source = this, //源
Mode = BindingMode.TwoWay
};
redSlider?.SetBinding(RangeBase.ValueProperty, binding);
//建立Brush的绑定,由于Brush没有SetBinding对象,只能this当作目标,Brush当作源,OneWayToSource,目标改变时改变源
Brush brush = GetTemplateChild("PART_Brush") as Brush;
Binding binding1 = new Binding("Color")
{
Source = brush,
Mode = BindingMode.TwoWay
};
this.SetBinding(ColorProperty, binding1);
}
}
三、核心功能实现
获取鼠标所在位置颜色
鼠标按下时打开计时器,每隔一段时间获取光标所在的位置。
private DispatcherTimer timer = new DispatcherTimer();
public MainWindowViewMode()
{
timer.Interval = TimeSpan.FromSeconds(0.01);
timer.Tick += timer_Tick;
MouseDownCommand = new RelayCommand<Button>(MouseDown);
MouseUpCommand = new RelayCommand<Button>(MouseUp);
}
[DllImport("gdi32")]
private static extern int GetPixel(int hdc, int nXPos, int nYPos);
[DllImport("user32")]
private static extern int GetWindowDC(int hwnd);
[DllImport("user32")]
private static extern int ReleaseDC(int hWnd, int hDC);
[DllImport("user32.dll", CharSet = CharSet.Auto)]
public static extern bool GetCursorPos(out POINT pt);
private void timer_Tick(object sender, EventArgs e)
{
GetCursorPos(out var pt);
Point point = new Point(pt.X, pt.Y);
MyColor = GetPixelColor(point);
Red = MyColor.R;
Green = MyColor.G;
Blue = MyColor.B;
Alpha = MyColor.A;
}
private Color GetPixelColor(Point point)
{
int windowDC = GetWindowDC(0);
int pixel = GetPixel(windowDC, (int)point.X, (int)point.Y);
ReleaseDC(0, windowDC);
byte b = (byte)((ulong)(pixel >> 16) & 0xFFuL);
byte g = (byte)((ulong)(pixel >> 8) & 0xFFuL);
byte r = (byte)((ulong)pixel & 0xFFuL);
return Color.FromRgb(r, g, b);
}
引用无外观控件
xmlns:uc="clr-namespace:ColorLibrary;assembly=ColorLibrary"
<uc:ColorPicker x:Name="picker" Padding="10" Red="{Binding Red,Mode=TwoWay}" Green="{Binding Green,Mode=TwoWay}" Blue="{Binding Blue,Mode=TwoWay}" Alpha="{Binding Alpha,Mode=TwoWay}"/>
添加鼠标按下的命令
xmlns:I="http://schemas.microsoft.com/xaml/behaviors"
<I:Interaction.Triggers>
<I:EventTrigger EventName="MouseDown">
<I:InvokeCommandAction Command="{Binding MouseDownCommand}" CommandParameter="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=Button}}" />
</I:EventTrigger>
<I:EventTrigger EventName="MouseUp">
<I:InvokeCommandAction Command="{Binding MouseUpCommand}" CommandParameter="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=Button}}" />
</I:EventTrigger>
</I:Interaction.Triggers>
public MainWindowViewMode()
{
MouseDownCommand = new RelayCommand<Button>(MouseDown);
MouseUpCommand = new RelayCommand<Button>(MouseUp);
}
public RelayCommand<Button> MouseDownCommand { get; set; }
public RelayCommand<Button> MouseUpCommand { get; set; }
public void MouseDown(Button button)
{
button.CaptureMouse();
button.Content = "松开完成";
timer.Start();
}
public void MouseUp(Button button)
{
button.ReleaseMouseCapture();
button.Content = "按住取色";
timer.Stop();
}
总结
无外观控件灵活性高,控件的样式不是固定的,可以在使用时重写控件模板。完整代码