diff --git a/.gitignore b/.gitignore
index d01f5dd7..3a993721 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,3 +7,4 @@
/HandyControl/obj/Debug
/HandyControlDemo/bin/Debug
/HandyControlDemo/obj/Debug
+/HandyControl.sln.DotSettings.user
diff --git a/HandyControl/Controls/ColorPicker.xaml b/HandyControl/Controls/ColorPicker.xaml
new file mode 100644
index 00000000..3aa4cecb
--- /dev/null
+++ b/HandyControl/Controls/ColorPicker.xaml
@@ -0,0 +1,128 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/HandyControl/Controls/ColorPicker.xaml.cs b/HandyControl/Controls/ColorPicker.xaml.cs
new file mode 100644
index 00000000..22350188
--- /dev/null
+++ b/HandyControl/Controls/ColorPicker.xaml.cs
@@ -0,0 +1,558 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Input;
+using System.Windows.Media;
+using HandyControl.Data;
+using HandyControl.Tools.Extension;
+
+namespace HandyControl.Controls
+{
+ ///
+ /// 颜色拾取器
+ ///
+ public partial class ColorPicker
+ {
+ ///
+ /// 当前显示的颜色类型
+ ///
+ private int _colorType;
+
+ ///
+ /// 是否已经加载控件
+ ///
+ private bool _isLoaded;
+
+ ///
+ /// 是否需要更新小球位置
+ ///
+ private bool _isNeedUpdatePicker = true;
+
+ ///
+ /// 是否在拖动小球
+ ///
+ private bool _isOnDragging;
+
+ ///
+ /// 是否需要更新信息
+ ///
+ private bool IsNeedUpdateInfo { get; set; } = true;
+
+ ///
+ /// 颜色分值
+ ///
+ private const double ColorInterval = 1 / 6.0;
+
+ ///
+ /// 颜色选取面板宽度
+ ///
+ private const double ColorPanelWidth = 230;
+
+ ///
+ /// 颜色选取面板高度
+ ///
+ private const double ColorPanelHeight = 122;
+
+ ///
+ /// 预设的颜色(一共18个,两行)
+ ///
+ private readonly List _colorPresetList = new List
+ {
+ "#f44336",
+ "#e91e63",
+ "#9c27b0",
+ "#673ab7",
+ "#3f51b5",
+ "#2196f3",
+ "#03a9f4",
+ "#00bcd4",
+ "#009688",
+
+ "#4caf50",
+ "#8bc34a",
+ "#cddc39",
+ "#ffeb3b",
+ "#ffc107",
+ "#ff9800",
+ "#ff5722",
+ "#795548",
+ "#9e9e9e"
+ };
+
+ ///
+ /// 颜色范围集合
+ ///
+ private readonly List _colorRangeList = new List
+ {
+ new ColorRange
+ {
+ Color1 = Color.FromRgb(255, 0, 0),
+ Color2 = Color.FromRgb(255, 0, 255)
+ },
+ new ColorRange
+ {
+ Color1 = Color.FromRgb(255, 0, 255),
+ Color2 = Color.FromRgb(0, 0, 255)
+ },
+ new ColorRange
+ {
+ Color1 = Color.FromRgb(0, 0, 255),
+ Color2 = Color.FromRgb(0, 255, 255)
+ },
+ new ColorRange
+ {
+ Color1 = Color.FromRgb(0, 255, 255),
+ Color2 = Color.FromRgb(0, 255, 0)
+ },
+ new ColorRange
+ {
+ Color1 = Color.FromRgb(0, 255, 0),
+ Color2 = Color.FromRgb(255, 255, 0)
+ },
+ new ColorRange
+ {
+ Color1 = Color.FromRgb(255, 255, 0),
+ Color2 = Color.FromRgb(255, 0, 0)
+ }
+ };
+
+ ///
+ /// 颜色分隔集合
+ ///
+ private readonly List _colorSeparateList = new List
+ {
+ Color.FromRgb(255, 0, 0),
+ Color.FromRgb(255, 0, 255),
+ Color.FromRgb(0, 0, 255),
+ Color.FromRgb(0, 255, 255),
+ Color.FromRgb(0, 255, 0),
+ Color.FromRgb(255, 255, 0)
+ };
+
+ internal static readonly DependencyProperty CornerRadiusProperty = DependencyProperty.Register(
+ "CornerRadius", typeof(CornerRadius), typeof(ColorPicker), new PropertyMetadata(new CornerRadius(2)));
+
+ internal CornerRadius CornerRadius
+ {
+ get => (CornerRadius)GetValue(CornerRadiusProperty);
+ set => SetValue(CornerRadiusProperty, value);
+ }
+
+ ///
+ /// 颜色改变事件
+ ///
+ public static readonly RoutedEvent ColorSelectedEvent =
+ EventManager.RegisterRoutedEvent("ValueChanged", RoutingStrategy.Bubble,
+ typeof(EventHandler>), typeof(ColorPicker));
+
+ ///
+ /// 颜色改变事件
+ ///
+ public event EventHandler> ColorSelected
+ {
+ add => AddHandler(ColorSelectedEvent, value);
+ remove => RemoveHandler(ColorSelectedEvent, value);
+ }
+
+ ///
+ /// 取消事件
+ ///
+ public static readonly RoutedEvent CanceledEvent =
+ EventManager.RegisterRoutedEvent("Canceled", RoutingStrategy.Bubble,
+ typeof(EventHandler), typeof(ColorPicker));
+
+ ///
+ /// 取消事件
+ ///
+ public event EventHandler Canceled
+ {
+ add => AddHandler(CanceledEvent, value);
+ remove => RemoveHandler(CanceledEvent, value);
+ }
+
+ public static readonly DependencyProperty SelectedBrushProperty = DependencyProperty.Register(
+ "SelectedBrush", typeof(SolidColorBrush), typeof(ColorPicker), new PropertyMetadata(Brushes.White,
+ (o, args) =>
+ {
+ var ctl = (ColorPicker)o;
+ var v = (SolidColorBrush)args.NewValue;
+
+ if (ctl.ChannelR == null) return;
+
+ if (ctl.IsNeedUpdateInfo)
+ {
+ ctl.IsNeedUpdateInfo = false;
+ ctl.ChannelR.Value = v.Color.R;
+ ctl.ChannelG.Value = v.Color.G;
+ ctl.ChannelB.Value = v.Color.B;
+ ctl.SliderOpacity.Value = v.Color.A;
+ ctl.IsNeedUpdateInfo = true;
+ }
+ ctl.UpdateStatus(v.Color);
+ ctl.SelectedBrushWithoutOpacity = new SolidColorBrush(Color.FromRgb(v.Color.R, v.Color.G, v.Color.B));
+ }));
+
+ ///
+ /// 当前选中的颜色
+ ///
+ public SolidColorBrush SelectedBrush
+ {
+ get => (SolidColorBrush)GetValue(SelectedBrushProperty);
+ set => SetValue(SelectedBrushProperty, value);
+ }
+
+ private static readonly DependencyProperty SelectedBrushWithoutOpacityProperty = DependencyProperty.Register(
+ "SelectedBrushWithoutOpacity", typeof(SolidColorBrush), typeof(ColorPicker), new PropertyMetadata(Brushes.White));
+
+ private SolidColorBrush SelectedBrushWithoutOpacity
+ {
+ get => (SolidColorBrush)GetValue(SelectedBrushWithoutOpacityProperty);
+ set => SetValue(SelectedBrushWithoutOpacityProperty, value);
+ }
+
+ private static readonly DependencyProperty BackColorProperty = DependencyProperty.Register(
+ "BackColor", typeof(SolidColorBrush), typeof(ColorPicker), new PropertyMetadata(Brushes.Red));
+
+ private SolidColorBrush BackColor
+ {
+ get => (SolidColorBrush)GetValue(BackColorProperty);
+ set => SetValue(BackColorProperty, value);
+ }
+
+ private static readonly DependencyProperty ShowListProperty = DependencyProperty.Register(
+ "ShowList", typeof(List), typeof(ColorPicker), new PropertyMetadata(new List
+ {
+ true, false, false
+ }));
+
+ private List ShowList
+ {
+ get => (List)GetValue(ShowListProperty);
+ set => SetValue(ShowListProperty, value);
+ }
+
+ ///
+ /// 当前显示的颜色类型
+ ///
+ private int ColorType
+ {
+ get => _colorType;
+ set
+ {
+ if (value < 0)
+ {
+ _colorType = 1;
+ }
+ else if (value > 1)
+ {
+ _colorType = 0;
+ }
+ else
+ {
+ _colorType = value;
+ }
+
+ var list = new List();
+ for (int i = 0; i < 2; i++)
+ {
+ list.Add(false);
+ }
+ list[_colorType] = true;
+ ShowList = list;
+ }
+ }
+
+ public ColorPicker()
+ {
+ InitializeComponent();
+
+ UpdateStatus(SelectedBrush.Color);
+
+ Loaded += (s, e) =>
+ {
+ if (!_isLoaded)
+ {
+ Init();
+ _isLoaded = true;
+ }
+ };
+ }
+
+ ///
+ /// 初始化
+ ///
+ private void Init()
+ {
+ foreach (var item in _colorPresetList)
+ {
+ PanelColor.Children.Add(CreateColorButton(item));
+ }
+ }
+
+ ///
+ /// 创建颜色按钮
+ ///
+ ///
+ private Button CreateColorButton(string colorStr)
+ {
+ var colorObj = ColorConverter.ConvertFromString(colorStr);
+ var color = default(Color);
+ if (colorObj != null)
+ {
+ color = (Color)colorObj;
+ }
+ var brush = new SolidColorBrush(color);
+
+ var button = new Button
+ {
+ Margin = new Thickness(0, 0, 12, 12),
+ Style = FindResource("ButtonOpacityStyle") as Style,
+ Content = new Border
+ {
+ Background = brush,
+ Width = 12,
+ Height = 12,
+ CornerRadius = new CornerRadius(2)
+ }
+ };
+
+ button.Click += (s, e) =>
+ {
+ SelectedBrush = brush;
+ SliderOpacity.Value = SliderOpacity.Maximum;
+ };
+
+ return button;
+ }
+
+ ///
+ /// 内部更新
+ ///
+ private void UpdateStatus(Color color)
+ {
+ if (_isOnDragging) return;
+
+ var r = color.R;
+ var g = color.G;
+ var b = color.B;
+ var list = new List
+ {
+ r,
+ g,
+ b
+ };
+
+ var max = list.Max();
+ var min = list.Min();
+
+ if (min == max)
+ {
+ if (!(r == g && b == g))
+ {
+ BackColor = Brushes.Red;
+ IsNeedUpdateInfo = false;
+ if (!SliderColor.IsMouseOver && !SliderOpacity.IsMouseOver)
+ {
+ SliderColor.Value = 0;
+ }
+ IsNeedUpdateInfo = true;
+ }
+ }
+ else
+ {
+ var maxIndex = list.IndexOf(max);
+ var minIndex = list.IndexOf(min);
+ var commonIndex = 3 - maxIndex - minIndex;
+ if (commonIndex == 3)
+ {
+ BackColor = Brushes.Red;
+ IsNeedUpdateInfo = false;
+ if (!SliderColor.IsMouseOver && !SliderOpacity.IsMouseOver)
+ {
+ SliderColor.Value = 0;
+ }
+ IsNeedUpdateInfo = true;
+ }
+ else
+ {
+ var common = list[commonIndex];
+ list[maxIndex] = 255;
+ list[minIndex] = 0;
+ list[commonIndex] = (byte)(255 * max * (min - common) / (double)(max * (min - max)));
+ BackColor = new SolidColorBrush(Color.FromRgb(list[0], list[1], list[2]));
+
+ list[commonIndex] = 0;
+ var cIndex = _colorSeparateList.IndexOf(Color.FromRgb(list[0], list[1], list[2]));
+ int sub;
+ var direc = 0;
+ if (cIndex < 5 && cIndex > 0)
+ {
+ var nextColorList = _colorSeparateList[cIndex + 1].ToList();
+ var prevColorList = _colorSeparateList[cIndex - 1].ToList();
+ if (nextColorList[minIndex] > 0)
+ {
+ var target = prevColorList[commonIndex];
+ direc = 1;
+ sub = target - common;
+ }
+ else
+ {
+ sub = common;
+ }
+ }
+ else if (cIndex == 0)
+ {
+ sub = common;
+ if (minIndex == 2)
+ {
+ sub = 255 - common;
+ direc = -5;
+ }
+ }
+ else
+ {
+ sub = 255 - common;
+ }
+ var scale = sub / 255.0;
+ var scaleTotal = ColorInterval * (cIndex - direc) + scale * ColorInterval;
+ IsNeedUpdateInfo = false;
+ if (!SliderColor.IsMouseOver && !SliderOpacity.IsMouseOver)
+ {
+ SliderColor.Value = SliderColor.Maximum * scaleTotal;
+ }
+ IsNeedUpdateInfo = true;
+ }
+ }
+
+ var matrix = BorderPicker.RenderTransform.Value;
+ var x = max == 0 ? 0 : (1 - min / (double)max) * ColorPanelWidth;
+ var y = (1 - max / 255.0) * ColorPanelHeight;
+ if (_isNeedUpdatePicker)
+ {
+ BorderPicker.RenderTransform = new MatrixTransform(matrix.M11, matrix.M12, matrix.M21, matrix.M22, x, y);
+ }
+ }
+
+ private void SliderColor_OnValueChanged(object sender, RoutedPropertyChangedEventArgs e)
+ {
+ if (!_isLoaded || !IsNeedUpdateInfo) return;
+ var newValue = e.NewValue;
+ var div = newValue / ColorInterval;
+ var index = Math.Min(5, (int)Math.Floor(newValue / ColorInterval));
+ var sub = div - index;
+ var range = _colorRangeList[index];
+
+ var color = range.GetColor(sub);
+ BackColor = new SolidColorBrush(color);
+ var matrix = BorderPicker.RenderTransform.Value;
+ _isNeedUpdatePicker = false;
+ UpdateColorWhenDrag(new Point(matrix.OffsetX, matrix.OffsetY));
+ _isNeedUpdatePicker = true;
+ }
+
+ private void SliderOpacity_OnValueChanged(object sender, RoutedPropertyChangedEventArgs e)
+ {
+ if (!_isLoaded || !IsNeedUpdateInfo) return;
+ var color = SelectedBrush.Color;
+ SelectedBrush = new SolidColorBrush(Color.FromArgb((byte)SliderOpacity.Value, color.R, color.G, color.B));
+ }
+
+ private void MouseDragElementBehavior_OnDragging(object sender, MouseEventArgs e)
+ {
+ var p = e.GetPosition(BorderColor);
+ _isOnDragging = true;
+ UpdateColorWhenDrag(p);
+ _isOnDragging = false;
+ }
+
+ ///
+ /// 拖动时更新颜色
+ ///
+ private void UpdateColorWhenDrag(Point p)
+ {
+ var matrix = BorderPicker.RenderTransform.Value;
+
+ if (p.X < 0)
+ {
+ p.X = 0;
+ }
+ else if (p.X > ColorPanelWidth)
+ {
+ p.X = ColorPanelWidth;
+ }
+
+ if (p.Y < 0)
+ {
+ p.Y = 0;
+ }
+ else if (p.Y > ColorPanelHeight)
+ {
+ p.Y = ColorPanelHeight;
+ }
+
+ if (_isNeedUpdatePicker)
+ {
+ BorderPicker.RenderTransform = new MatrixTransform(matrix.M11, matrix.M12, matrix.M21, matrix.M22, p.X, p.Y);
+ }
+
+ var scaleX = p.X / ColorPanelWidth;
+ var scaleY = 1 - p.Y / ColorPanelHeight;
+
+ var colorYLeft = Color.FromRgb((byte)(255 * scaleY), (byte)(255 * scaleY), (byte)(255 * scaleY));
+ var colorYRight = Color.FromRgb((byte)(BackColor.Color.R * scaleY), (byte)(BackColor.Color.G * scaleY), (byte)(BackColor.Color.B * scaleY));
+
+ var subR = colorYLeft.R - colorYRight.R;
+ var subG = colorYLeft.G - colorYRight.G;
+ var subB = colorYLeft.B - colorYRight.B;
+
+ var color = Color.FromArgb((byte)SliderOpacity.Value, (byte)(colorYLeft.R - subR * scaleX),
+ (byte)(colorYLeft.G - subG * scaleX), (byte)(colorYLeft.B - subB * scaleX));
+ SelectedBrush = new SolidColorBrush(color);
+ }
+
+ private void MouseDragElementBehavior_OnDragFinished(object sender, MouseEventArgs e) => BorderDrag.RenderTransform = new MatrixTransform();
+
+ private void ButtonSwitch_OnClick(object sender, RoutedEventArgs e) => ColorType++;
+
+ private void InfoNumericUpDownRgb_OnValueChanged(object sender, FunctionEventArgs e)
+ {
+ if (!_isLoaded || !IsNeedUpdateInfo) return;
+ if (e.OriginalSource is InfoNumericUpDown ctl && ctl.Tag is string tag)
+ {
+ var color = SelectedBrush.Color;
+ IsNeedUpdateInfo = false;
+
+ switch (tag)
+ {
+ case "R":
+ {
+ SelectedBrush = new SolidColorBrush(Color.FromArgb(color.A, (byte)e.Info, color.G, color.B));
+ break;
+ }
+ case "G":
+ {
+ SelectedBrush = new SolidColorBrush(Color.FromArgb(color.A, color.R, (byte)e.Info, color.B));
+ break;
+ }
+ case "B":
+ {
+ SelectedBrush = new SolidColorBrush(Color.FromArgb(color.A, color.R, color.G, (byte)e.Info));
+ break;
+ }
+ }
+
+ IsNeedUpdateInfo = true;
+ }
+ }
+
+ private void ButtonSure_OnClick(object sender, RoutedEventArgs e)
+ => RaiseEvent(new FunctionEventArgs(ColorSelectedEvent, this)
+ {
+ Info = SelectedBrush.Color
+ });
+
+ private void ButtonCancel_OnClick(object sender, RoutedEventArgs e) => RaiseEvent(new RoutedEventArgs(CanceledEvent));
+ }
+}
\ No newline at end of file
diff --git a/HandyControl/Controls/IconElement.cs b/HandyControl/Controls/IconElement.cs
new file mode 100644
index 00000000..3cc778b5
--- /dev/null
+++ b/HandyControl/Controls/IconElement.cs
@@ -0,0 +1,35 @@
+using System.Windows;
+using System.Windows.Media;
+
+namespace HandyControl.Controls
+{
+ public class IconElement : DependencyObject
+ {
+ public static readonly DependencyProperty GeometryProperty = DependencyProperty.RegisterAttached(
+ "Geometry", typeof(Geometry), typeof(IconElement), new PropertyMetadata(default(Geometry)));
+
+ public static void SetGeometry(DependencyObject element, Geometry value)
+ {
+ element.SetValue(GeometryProperty, value);
+ }
+
+ public static Geometry GetGeometry(DependencyObject element)
+ {
+ return (Geometry)element.GetValue(GeometryProperty);
+ }
+
+ public static readonly DependencyProperty AngleProperty = DependencyProperty.RegisterAttached(
+ "Angle", typeof(double), typeof(IconElement), new PropertyMetadata(default(double)));
+
+ public static void SetAngle(DependencyObject element, double value)
+ {
+ element.SetValue(AngleProperty, value);
+ }
+
+ public static double GetAngle(DependencyObject element)
+ {
+ return (double)element.GetValue(AngleProperty);
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/HandyControl/Controls/Info/InfoControl.cs b/HandyControl/Controls/Info/InfoControl.cs
new file mode 100644
index 00000000..bbff8d29
--- /dev/null
+++ b/HandyControl/Controls/Info/InfoControl.cs
@@ -0,0 +1,374 @@
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Data;
+using System.Windows.Input;
+using System.Windows.Media.Animation;
+using HandyControl.Data.Enum;
+using HandyControl.Tools;
+using HandyControl.Tools.Converter;
+using HandyControl.Tools.Extension;
+
+// ReSharper disable once CheckNamespace
+namespace HandyControl.Controls
+{
+ public abstract class InfoControl : UserControl
+ {
+ private bool _isFocused;
+
+ protected TextBlock ErrorTextBlock;
+
+ protected TextBox ContentTextBox;
+
+ protected StackPanel TitleStackPanel;
+
+ ///
+ /// 标题
+ ///
+ public static readonly DependencyProperty TitleProperty = DependencyProperty.Register(
+ "Title", typeof(string), typeof(InfoControl), new PropertyMetadata(default(string)));
+
+ ///
+ /// 标题
+ ///
+ public string Title
+ {
+ get => (string)GetValue(TitleProperty);
+ set => SetValue(TitleProperty, value);
+ }
+
+ ///
+ /// 文本
+ ///
+ public static readonly DependencyProperty TextProperty = DependencyProperty.Register(
+ "Text", typeof(string), typeof(InfoControl), new FrameworkPropertyMetadata(default(string), FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));
+
+ ///
+ /// 文本
+ ///
+ public string Text
+ {
+ get => (string)GetValue(TextProperty);
+ set => SetValue(TextProperty, value);
+ }
+
+ ///
+ /// 是否为必填项
+ ///
+ public static readonly DependencyProperty IsNeedlyProperty = DependencyProperty.Register(
+ "IsNeedly", typeof(bool), typeof(InfoControl), new PropertyMetadata(true));
+
+ ///
+ /// 是否为必填项
+ ///
+ public bool IsNeedly
+ {
+ get => (bool)GetValue(IsNeedlyProperty);
+ set => SetValue(IsNeedlyProperty, value);
+ }
+
+ ///
+ /// 占位符
+ ///
+ public static readonly DependencyProperty PlaceHolderProperty = DependencyProperty.Register(
+ "PlaceHolder", typeof(string), typeof(InfoControl), new PropertyMetadata(default(string)));
+
+ ///
+ /// 占位符
+ ///
+ public string PlaceHolder
+ {
+ get => (string)GetValue(PlaceHolderProperty);
+ set => SetValue(PlaceHolderProperty, value);
+ }
+
+ ///
+ /// 错误信息
+ ///
+ public static readonly DependencyProperty ErrorStrProperty = DependencyProperty.Register(
+ "ErrorStr", typeof(string), typeof(InfoControl), new PropertyMetadata(default(string)));
+
+ ///
+ /// 错误信息
+ ///
+ public string ErrorStr
+ {
+ get => (string)GetValue(ErrorStrProperty);
+ set => SetValue(ErrorStrProperty, value);
+ }
+
+ ///
+ /// 控件是否包含错误
+ ///
+ public static readonly DependencyProperty IsErrorProperty = DependencyProperty.Register(
+ "IsError", typeof(bool), typeof(InfoControl), new PropertyMetadata(default(bool)));
+
+ ///
+ /// 控件是否包含错误
+ ///
+ public bool IsError
+ {
+ get => (bool)GetValue(IsErrorProperty);
+ set => SetValue(IsErrorProperty, value);
+ }
+
+ ///
+ /// 标题宽度
+ ///
+ public static readonly DependencyProperty TitleWidthProperty = DependencyProperty.RegisterAttached(
+ "TitleWidth", typeof(GridLength), typeof(InfoControl),
+ new FrameworkPropertyMetadata(GridLength.Auto, FrameworkPropertyMetadataOptions.Inherits));
+
+ ///
+ /// 设置标题宽度
+ ///
+ public static void SetTitleWidth(DependencyObject element, GridLength value)
+ {
+ element.SetValue(TitleWidthProperty, value);
+ }
+
+ ///
+ /// 标题宽度
+ ///
+ public static GridLength GetTitleWidth(DependencyObject element)
+ {
+ return (GridLength)element.GetValue(TitleWidthProperty);
+ }
+
+ ///
+ /// 标题宽度
+ ///
+ public GridLength TitleWidth
+ {
+ get => (GridLength)GetValue(TitleWidthProperty);
+ set => SetValue(TitleWidthProperty, value);
+ }
+
+ ///
+ /// 标题对齐方式
+ ///
+ public static readonly DependencyProperty TitleAlignmentProperty = DependencyProperty.Register(
+ "TitleAlignment", typeof(TitleAlignment), typeof(InfoControl), new PropertyMetadata(TitleAlignment.Left,
+ (o, args) =>
+ {
+ var ctl = (InfoControl)o;
+ var v = (TitleAlignment)args.NewValue;
+ if (v == TitleAlignment.Left)
+ {
+ Grid.SetColumn(ctl.TitleStackPanel, 0);
+ Grid.SetRow(ctl.TitleStackPanel, 1);
+ ctl.HorizontalAlignment = HorizontalAlignment.Right;
+ ctl.Height = ctl.MaxHeight;
+ }
+ else
+ {
+ Grid.SetColumn(ctl.TitleStackPanel, 1);
+ Grid.SetRow(ctl.TitleStackPanel, 0);
+ ctl.TitleStackPanel.HorizontalAlignment = HorizontalAlignment.Left;
+ ctl.TitleStackPanel.Margin = new Thickness(4, 0, 0, 4);
+ ctl.Height += 18;
+ }
+ }));
+
+ ///
+ /// 标题对齐方式
+ ///
+ public TitleAlignment TitleAlignment
+ {
+ get => (TitleAlignment)GetValue(TitleAlignmentProperty);
+ set => SetValue(TitleAlignmentProperty, value);
+ }
+
+ ///
+ /// 文本的水平对齐方式
+ ///
+ public static readonly DependencyProperty HorizontalTextAlignmentProperty = DependencyProperty.Register(
+ "HorizontalTextAlignment", typeof(HorizontalAlignment), typeof(InfoControl), new PropertyMetadata(HorizontalAlignment.Left));
+
+ ///
+ /// 文本的水平对齐方式
+ ///
+ public HorizontalAlignment HorizontalTextAlignment
+ {
+ get => (HorizontalAlignment)GetValue(HorizontalTextAlignmentProperty);
+ set => SetValue(HorizontalTextAlignmentProperty, value);
+ }
+
+ ///
+ /// 是否显示清除按钮
+ ///
+ public static readonly DependencyProperty ShowClearButtonProperty = DependencyProperty.Register(
+ "ShowClearButton", typeof(bool), typeof(InfoControl), new PropertyMetadata(true, (o, args) =>
+ {
+ var ctl = (InfoControl)o;
+ var v = (bool)args.NewValue;
+ if (!(ctl.FindName("ButtonClear") is Button button)) return;
+ BindingOperations.ClearBinding(button, VisibilityProperty);
+ button.IsEnabled = v;
+ if (v)
+ {
+ button.SetBinding(VisibilityProperty, new Binding("Text")
+ {
+ Source = ctl,
+ Converter = new String2VisibilityConverter()
+ });
+ }
+ else
+ {
+ button.Collapse();
+ }
+ }));
+
+ ///
+ /// 是否显示清除按钮
+ ///
+ public bool ShowClearButton
+ {
+ get => (bool)GetValue(ShowClearButtonProperty);
+ set => SetValue(ShowClearButtonProperty, value);
+ }
+
+ ///
+ /// 是否显示小圆角
+ ///
+ public static readonly DependencyProperty ShowSmallCornerRadiusProperty = DependencyProperty.Register(
+ "ShowSmallCornerRadius", typeof(bool), typeof(InfoControl), new PropertyMetadata(default(bool), (o, args) =>
+ {
+ var ctl = (InfoControl)o;
+ var v = (bool)args.NewValue;
+ ctl.CornerRadius = v ? new CornerRadius(2, 2, 2, 2) : new CornerRadius(4, 4, 4, 4);
+ }));
+
+ ///
+ /// 是否显示小圆角
+ ///
+ public bool ShowSmallCornerRadius
+ {
+ get => (bool)GetValue(ShowSmallCornerRadiusProperty);
+ set => SetValue(ShowSmallCornerRadiusProperty, value);
+ }
+
+ ///
+ /// 圆角
+ ///
+ protected static readonly DependencyProperty CornerRadiusProperty = DependencyProperty.Register(
+ "CornerRadius", typeof(CornerRadius), typeof(InfoControl), new PropertyMetadata(new CornerRadius(4, 4, 4, 4)));
+
+ ///
+ /// 圆角
+ ///
+ protected CornerRadius CornerRadius
+ {
+ get => (CornerRadius)GetValue(CornerRadiusProperty);
+ set => SetValue(CornerRadiusProperty, value);
+ }
+
+ ///
+ /// 文本内边距
+ ///
+ public static readonly DependencyProperty TextPaddingProperty = DependencyProperty.Register(
+ "TextPadding", typeof(Thickness), typeof(InfoControl), new PropertyMetadata(new Thickness(6, 0, 0, 0)));
+
+ ///
+ /// 文本内边距
+ ///
+ public Thickness TextPadding
+ {
+ get => (Thickness)GetValue(TextPaddingProperty);
+ set => SetValue(TextPaddingProperty, value);
+ }
+
+ ///
+ /// 更新状态
+ ///
+ public virtual void Update()
+ {
+ if (!IsError)
+ {
+ if (!_isFocused)
+ {
+ (TryFindResource("Storyboard2") as Storyboard)?.Begin();
+ }
+ else
+ {
+ (TryFindResource("Storyboard3") as Storyboard)?.Begin();
+ }
+ ErrorTextBlock.BeginAnimation(MarginProperty, AnimationHelper.CreateAnimation(new Thickness(4, 0, 0, 1)));
+ return;
+ }
+ (TryFindResource("Storyboard4") as Storyboard)?.Begin();
+ ErrorTextBlock.BeginAnimation(MarginProperty, AnimationHelper.CreateAnimation(new Thickness(4, 0, 0, -ErrorTextBlock.ActualHeight - 2)));
+ }
+
+ protected virtual void InputBox_OnLostFocus(object sender, RoutedEventArgs e)
+ {
+ _isFocused = false;
+ CheckInfo();
+ }
+
+ protected virtual void InputBox_OnGotFocus(object sender, RoutedEventArgs e)
+ {
+ _isFocused = true;
+ if (!IsError)
+ {
+ (TryFindResource("Storyboard3") as Storyboard)?.Begin();
+ }
+ }
+
+ protected virtual void InputBox_OnMouseEnter(object sender, MouseEventArgs e)
+ {
+ if (!IsError && !ContentTextBox.IsFocused)
+ {
+ (TryFindResource("Storyboard1") as Storyboard)?.Begin();
+ }
+ }
+
+ protected virtual void InputBox_OnMouseLeave(object sender, MouseEventArgs e)
+ {
+ if (!IsError && !ContentTextBox.IsFocused)
+ {
+ (TryFindResource("Storyboard2") as Storyboard)?.Begin();
+ }
+ }
+
+ protected virtual void InputBox_OnTextChanged(object sender, TextChangedEventArgs e) => CheckInfo();
+
+ protected virtual void ButtonClear_OnClick(object sender, RoutedEventArgs e) => Text = string.Empty;
+
+ ///
+ /// 文本类型
+ ///
+ public TextType TextType { get; set; }
+
+ ///
+ /// 正则字符串(当文本类型为Common时,此属性才生效)
+ ///
+ public string RegularStr { get; set; }
+
+ ///
+ /// 用于监测数据的正确性
+ ///
+ ///
+ public virtual bool CheckInfo()
+ {
+ bool result;
+ if (TextType == TextType.Common)
+ {
+ if (string.IsNullOrEmpty(RegularStr))
+ {
+ result = !IsNeedly || !string.IsNullOrEmpty(Text);
+ }
+ else
+ {
+ result = Text.IsKindOf(RegularStr);
+ }
+ }
+ else
+ {
+ result = Text.IsKindOf(TextType);
+ }
+ IsError = !result;
+ Update();
+ return result;
+ }
+ }
+}
\ No newline at end of file
diff --git a/HandyControl/Controls/Info/InfoNumericUpDown.xaml b/HandyControl/Controls/Info/InfoNumericUpDown.xaml
new file mode 100644
index 00000000..9609429f
--- /dev/null
+++ b/HandyControl/Controls/Info/InfoNumericUpDown.xaml
@@ -0,0 +1,65 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/HandyControl/Controls/Info/InfoNumericUpDown.xaml.cs b/HandyControl/Controls/Info/InfoNumericUpDown.xaml.cs
new file mode 100644
index 00000000..2195a6e9
--- /dev/null
+++ b/HandyControl/Controls/Info/InfoNumericUpDown.xaml.cs
@@ -0,0 +1,191 @@
+using System;
+using System.Windows;
+using System.Windows.Input;
+using System.Windows.Media.Animation;
+using HandyControl.Data;
+
+// ReSharper disable once CheckNamespace
+namespace HandyControl.Controls
+{
+ ///
+ /// InfoNumericUpDown.xaml 的交互逻辑
+ ///
+ public partial class InfoNumericUpDown
+ {
+ public InfoNumericUpDown()
+ {
+ InitializeComponent();
+
+ ErrorTextBlock = TextBlockError;
+ ContentTextBox = MyTextBox;
+ TitleStackPanel = StackPanelTitle;
+
+ (TryFindResource("Storyboard2") as Storyboard)?.Begin();
+
+ Loaded += (s, e) =>
+ {
+ StackPanelTitle.Focusable = true;
+ StackPanelTitle.Focus();
+ StackPanelTitle.Focusable = false;
+ };
+ }
+
+ ///
+ /// 最大值
+ ///
+ public static readonly DependencyProperty MaximumProperty = DependencyProperty.Register(
+ "Maximum", typeof(double), typeof(InfoNumericUpDown), new PropertyMetadata(100.0, (o, args) =>
+ {
+ o.CoerceValue(MinimumProperty);
+ o.CoerceValue(ValueProperty);
+ }, (o, value) =>
+ {
+ var info = (InfoNumericUpDown)o;
+ var max = (double)value;
+ if (max < info.Minimum) max = info.Minimum;
+ return max;
+ }), ValidateValueCallback);
+
+ ///
+ /// 最大值
+ ///
+ public double Maximum
+ {
+ get => (double)GetValue(MaximumProperty);
+ set => SetValue(MaximumProperty, value);
+ }
+
+ ///
+ /// 最小值
+ ///
+ public static readonly DependencyProperty MinimumProperty = DependencyProperty.Register(
+ "Minimum", typeof(double), typeof(InfoNumericUpDown), new PropertyMetadata(0.0, (o, args) =>
+ {
+ o.CoerceValue(MaximumProperty);
+ o.CoerceValue(ValueProperty);
+ }, (o, value) =>
+ {
+ var info = (InfoNumericUpDown)o;
+ var min = (double)value;
+ if (min > info.Maximum) min = info.Maximum;
+ return min;
+ }), ValidateValueCallback);
+
+ ///
+ /// 最小值
+ ///
+ public double Minimum
+ {
+ get => (double)GetValue(MinimumProperty);
+ set => SetValue(MinimumProperty, value);
+ }
+
+ ///
+ /// 当前值
+ ///
+ public static readonly DependencyProperty ValueProperty = DependencyProperty.Register(
+ "Value", typeof(double), typeof(InfoNumericUpDown), new PropertyMetadata(1.0, (o, args) =>
+ {
+ o.CoerceValue(MinimumProperty);
+ o.CoerceValue(MaximumProperty);
+ var ctl = (InfoNumericUpDown)o;
+ ctl.RaiseEvent(new FunctionEventArgs(ValueChangedEvent, ctl)
+ {
+ Info = (double)args.NewValue
+ });
+ }, (o, value) =>
+ {
+ var info = (InfoNumericUpDown)o;
+ var current = (double)value;
+
+ if (current < info.Minimum) current = info.Minimum;
+ if (current > info.Maximum) current = info.Maximum;
+
+ return current;
+ }), ValidateValueCallback);
+
+ ///
+ /// 值验证回调
+ ///
+ ///
+ ///
+ private static bool ValidateValueCallback(object value)
+ {
+ var v = (double)value;
+ return !(double.IsNaN(v) || double.IsInfinity(v));
+ }
+
+ ///
+ /// 当前值
+ ///
+ public double Value
+ {
+ get => (double)GetValue(ValueProperty);
+ set => SetValue(ValueProperty, value);
+ }
+
+ ///
+ /// 最小改变值
+ ///
+ public static readonly DependencyProperty SmallChangeProperty = DependencyProperty.Register(
+ "SmallChange", typeof(double), typeof(InfoNumericUpDown), new PropertyMetadata(1.0), ValidateValueCallback);
+
+ ///
+ /// 最小改变值
+ ///
+ public double SmallChange
+ {
+ get => (double)GetValue(SmallChangeProperty);
+ set => SetValue(SmallChangeProperty, value);
+ }
+
+ ///
+ /// 是否显示上下调整按钮
+ ///
+ public static readonly DependencyProperty ShowUpDownButtonProperty = DependencyProperty.Register(
+ "ShowUpDownButton", typeof(bool), typeof(InfoNumericUpDown), new PropertyMetadata(true));
+
+ ///
+ /// 是否显示上下调整按钮
+ ///
+ public bool ShowUpDownButton
+ {
+ get => (bool)GetValue(ShowUpDownButtonProperty);
+ set => SetValue(ShowUpDownButtonProperty, value);
+ }
+
+ ///
+ /// 值改变事件
+ ///
+ public static readonly RoutedEvent ValueChangedEvent =
+ EventManager.RegisterRoutedEvent("ValueChanged", RoutingStrategy.Bubble,
+ typeof(EventHandler>), typeof(InfoNumericUpDown));
+
+ ///
+ /// 值改变事件
+ ///
+ public event EventHandler> ValueChanged
+ {
+ add => AddHandler(ValueChangedEvent, value);
+ remove => RemoveHandler(ValueChangedEvent, value);
+ }
+
+ private void ButtonUp_OnClick(object sender, RoutedEventArgs e) => Value += SmallChange;
+
+ private void ButtonDown_OnClick(object sender, RoutedEventArgs e) => Value -= SmallChange;
+
+ private void MyTextBox_OnMouseWheel(object sender, MouseWheelEventArgs e)
+ {
+ if (e.Delta > 0)
+ {
+ Value += SmallChange;
+ }
+ else
+ {
+ Value -= SmallChange;
+ }
+ MyTextBox.Select(MyTextBox.Text.Length, 0);
+ e.Handled = true;
+ }
+ }
+}
\ No newline at end of file
diff --git a/HandyControl/Controls/Info/InfoTextBox.xaml b/HandyControl/Controls/Info/InfoTextBox.xaml
new file mode 100644
index 00000000..9cf02129
--- /dev/null
+++ b/HandyControl/Controls/Info/InfoTextBox.xaml
@@ -0,0 +1,61 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/HandyControl/Controls/Info/InfoTextBox.xaml.cs b/HandyControl/Controls/Info/InfoTextBox.xaml.cs
new file mode 100644
index 00000000..1e3c114d
--- /dev/null
+++ b/HandyControl/Controls/Info/InfoTextBox.xaml.cs
@@ -0,0 +1,45 @@
+using System.Windows;
+using System.Windows.Media.Animation;
+
+// ReSharper disable once CheckNamespace
+namespace HandyControl.Controls
+{
+ ///
+ /// InfoTextBox.xaml 的交互逻辑
+ ///
+ public partial class InfoTextBox
+ {
+ ///
+ /// 可输入的最大字符数
+ ///
+ public static readonly DependencyProperty MaxLengthProperty = DependencyProperty.Register(
+ "MaxLength", typeof(int), typeof(InfoTextBox), new PropertyMetadata(default(int)));
+
+ ///
+ /// 可输入的最大字符数
+ ///
+ public int MaxLength
+ {
+ get => (int)GetValue(MaxLengthProperty);
+ set => SetValue(MaxLengthProperty, value);
+ }
+
+ public InfoTextBox()
+ {
+ InitializeComponent();
+
+ ErrorTextBlock = TextBlockError;
+ ContentTextBox = MyTextBox;
+ TitleStackPanel = StackPanelTitle;
+
+ (TryFindResource("Storyboard2") as Storyboard)?.Begin();
+
+ Loaded += (s, e) =>
+ {
+ StackPanelTitle.Focusable = true;
+ StackPanelTitle.Focus();
+ StackPanelTitle.Focusable = false;
+ };
+ }
+ }
+}
\ No newline at end of file
diff --git a/HandyControl/Data/ColorRange.cs b/HandyControl/Data/ColorRange.cs
new file mode 100644
index 00000000..3db15cb3
--- /dev/null
+++ b/HandyControl/Data/ColorRange.cs
@@ -0,0 +1,75 @@
+using System.Windows.Media;
+
+
+namespace HandyControl.Data
+{
+ ///
+ /// 颜色范围
+ ///
+ public class ColorRange
+ {
+ ///
+ /// 颜色1
+ ///
+ private Color _color1;
+
+ ///
+ /// 颜色1
+ ///
+ public Color Color1
+ {
+ get => _color1;
+ set
+ {
+ _color1 = value;
+ Update();
+ }
+ }
+
+ ///
+ /// 颜色2
+ ///
+ private Color _color2;
+
+ ///
+ /// 颜色2
+ ///
+ public Color Color2
+ {
+ get => _color2;
+ set
+ {
+ _color2 = value;
+ Update();
+ }
+ }
+
+ ///
+ /// 颜色差值
+ ///
+ private readonly int[] _subColorArr = new int[4];
+
+ ///
+ /// 更新
+ ///
+ private void Update()
+ {
+ _subColorArr[0] = Color1.A - Color2.A;
+ _subColorArr[1] = Color1.R - Color2.R;
+ _subColorArr[2] = Color1.G - Color2.G;
+ _subColorArr[3] = Color1.B - Color2.B;
+ }
+
+ ///
+ /// 获取指定比例处的颜色
+ ///
+ /// 范围(0-1)
+ ///
+ public Color GetColor(double range)
+ {
+ if (range < 0 || range > 1) return default(Color);
+ return Color.FromArgb((byte)(_color1.A - _subColorArr[0] * range), (byte)(_color1.R - _subColorArr[1] * range),
+ (byte)(_color1.G - _subColorArr[2] * range), (byte)(_color1.B - _subColorArr[3] * range));
+ }
+ }
+}
\ No newline at end of file
diff --git a/HandyControl/Data/Enum/IpType.cs b/HandyControl/Data/Enum/IpType.cs
new file mode 100644
index 00000000..1551d04c
--- /dev/null
+++ b/HandyControl/Data/Enum/IpType.cs
@@ -0,0 +1,29 @@
+namespace HandyControl.Data.Enum
+{
+ ///
+ /// IP类型枚举
+ ///
+ public enum IpType
+ {
+ ///
+ /// A类IP地址
+ ///
+ A = 0,
+ ///
+ /// B类IP地址
+ ///
+ B,
+ ///
+ /// C类IP地址
+ ///
+ C,
+ ///
+ /// D类IP地址
+ ///
+ D,
+ ///
+ /// E类IP地址
+ ///
+ E
+ }
+}
\ No newline at end of file
diff --git a/HandyControl/Data/Enum/TextType.cs b/HandyControl/Data/Enum/TextType.cs
new file mode 100644
index 00000000..867e5d3e
--- /dev/null
+++ b/HandyControl/Data/Enum/TextType.cs
@@ -0,0 +1,26 @@
+namespace HandyControl.Data.Enum
+{
+ ///
+ /// 文本类型枚举
+ ///
+ public enum TextType
+ {
+ Common = 0,
+ Phone,
+ Mail,
+ Url,
+ Chinese,
+ Number,
+ Digits,
+ PInt,
+ NInt,
+ Int,
+ NnInt,
+ NpInt,
+ PDouble,
+ NDouble,
+ Double,
+ NnDouble,
+ NpDouble,
+ }
+}
\ No newline at end of file
diff --git a/HandyControl/Data/Enum/TitleAlignment.cs b/HandyControl/Data/Enum/TitleAlignment.cs
new file mode 100644
index 00000000..af88faab
--- /dev/null
+++ b/HandyControl/Data/Enum/TitleAlignment.cs
@@ -0,0 +1,11 @@
+namespace HandyControl.Data.Enum
+{
+ ///
+ /// 标题对齐方式
+ ///
+ public enum TitleAlignment
+ {
+ Left,
+ Top
+ }
+}
\ No newline at end of file
diff --git a/HandyControl/Data/FunctionEventArgs!1.cs b/HandyControl/Data/FunctionEventArgs!1.cs
new file mode 100644
index 00000000..239f367a
--- /dev/null
+++ b/HandyControl/Data/FunctionEventArgs!1.cs
@@ -0,0 +1,14 @@
+using System.Windows;
+
+
+namespace HandyControl.Data
+{
+ public class FunctionEventArgs : RoutedEventArgs
+ {
+ public FunctionEventArgs(RoutedEvent routedEvent, object source) : base(routedEvent, source)
+ {
+ }
+
+ public T Info { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/HandyControl/HandyControl.csproj b/HandyControl/HandyControl.csproj
index f4166407..6d854d8e 100644
--- a/HandyControl/HandyControl.csproj
+++ b/HandyControl/HandyControl.csproj
@@ -31,9 +31,174 @@
4
+
+ ColorPicker.xaml
+
+
+
+
+ InfoNumericUpDown.xaml
+
+
+ InfoTextBox.xaml
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Code
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Designer
+ MSBuild:Compile
+
+
+ Designer
+ MSBuild:Compile
+
+
+ Designer
+ MSBuild:Compile
+
+
+ Designer
+ MSBuild:Compile
+
+
+ Designer
+ MSBuild:Compile
+
+
+ Designer
+ MSBuild:Compile
+
+
+ Designer
+ MSBuild:Compile
+
+
+ Designer
+ MSBuild:Compile
+
+
+ Designer
+ MSBuild:Compile
+
+
+ Designer
+ MSBuild:Compile
+
+
+ Designer
+ MSBuild:Compile
+
+
+ Designer
+ MSBuild:Compile
+
+
+ Designer
+ MSBuild:Compile
+
+
+ Designer
+ MSBuild:Compile
+
+
+ Designer
+ MSBuild:Compile
+
+
+ Designer
+ MSBuild:Compile
+
+
+ Designer
+ MSBuild:Compile
+
+
+ Designer
+ MSBuild:Compile
+
+
+ Designer
+ MSBuild:Compile
+
+
+ Designer
+ MSBuild:Compile
+
+
+ Designer
+ MSBuild:Compile
+
+
+ Designer
+ MSBuild:Compile
+
+
+ Designer
+ MSBuild:Compile
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/HandyControl/Interactivity/AdornerContainer.cs b/HandyControl/Interactivity/AdornerContainer.cs
new file mode 100644
index 00000000..4ffc4f93
--- /dev/null
+++ b/HandyControl/Interactivity/AdornerContainer.cs
@@ -0,0 +1,40 @@
+using System.Windows;
+using System.Windows.Documents;
+using System.Windows.Media;
+
+namespace HandyControl.Interactivity
+{
+ public class AdornerContainer : Adorner
+ {
+ private UIElement _child;
+
+ public AdornerContainer(UIElement adornedElement) : base(adornedElement)
+ {
+ }
+
+ public UIElement Child
+ {
+ get =>
+ _child;
+ set
+ {
+ AddVisualChild(value);
+ _child = value;
+ }
+ }
+
+ protected override int VisualChildrenCount => _child != null ? 1 : 0;
+
+ protected override Size ArrangeOverride(Size finalSize)
+ {
+ _child?.Arrange(new Rect(finalSize));
+ return finalSize;
+ }
+
+ protected override Visual GetVisualChild(int index)
+ {
+ if (index == 0 && _child != null) return _child;
+ return base.GetVisualChild(index);
+ }
+ }
+}
\ No newline at end of file
diff --git a/HandyControl/Interactivity/AttachableCollection!1.cs b/HandyControl/Interactivity/AttachableCollection!1.cs
new file mode 100644
index 00000000..edcfdb83
--- /dev/null
+++ b/HandyControl/Interactivity/AttachableCollection!1.cs
@@ -0,0 +1,168 @@
+using System;
+using System.Collections.ObjectModel;
+using System.Collections.Specialized;
+using System.ComponentModel;
+using System.Globalization;
+using System.Windows;
+
+namespace HandyControl.Interactivity
+{
+ public abstract class AttachableCollection : FreezableCollection, IAttachedObject
+ where T : DependencyObject, IAttachedObject
+ {
+ private DependencyObject _associatedObject;
+ private Collection _snapshot;
+
+ internal AttachableCollection()
+ {
+ INotifyCollectionChanged changed = this;
+ changed.CollectionChanged += OnCollectionChanged;
+ _snapshot = new Collection();
+ }
+
+ public DependencyObject AssociatedObject
+ {
+ get
+ {
+ ReadPreamble();
+ return _associatedObject;
+ }
+ }
+
+ DependencyObject IAttachedObject.AssociatedObject => AssociatedObject;
+
+ public void Attach(DependencyObject dependencyObject)
+ {
+ if (Equals(dependencyObject, AssociatedObject))
+ return;
+ if (AssociatedObject != null)
+ throw new InvalidOperationException();
+ if (Interaction.ShouldRunInDesignMode || !(bool) GetValue(DesignerProperties.IsInDesignModeProperty))
+ {
+ WritePreamble();
+ _associatedObject = dependencyObject;
+ WritePostscript();
+ }
+ OnAttached();
+ }
+
+ public void Detach()
+ {
+ OnDetaching();
+ WritePreamble();
+ _associatedObject = null;
+ WritePostscript();
+ }
+
+ protected abstract void OnAttached();
+
+ protected abstract void OnDetaching();
+
+ internal abstract void ItemAdded(T item);
+
+ internal abstract void ItemRemoved(T item);
+
+ private void VerifyAdd(T item)
+ {
+ if (item == null) throw new ArgumentNullException(nameof(item));
+ if (_snapshot.Contains(item))
+ throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture,
+ ExceptionStringTable.DuplicateItemInCollectionExceptionMessage, new object[]
+ {
+ typeof(T).Name,
+ GetType().Name
+ }));
+ }
+
+ private void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
+ {
+ switch (e.Action)
+ {
+ case NotifyCollectionChangedAction.Add:
+ var enumerator1 = e.NewItems.GetEnumerator();
+ try
+ {
+ while (enumerator1.MoveNext())
+ {
+ var current = (T) enumerator1.Current;
+ try
+ {
+ VerifyAdd(current);
+ ItemAdded(current);
+ }
+ finally
+ {
+ _snapshot.Insert(IndexOf(current), current);
+ }
+ }
+ break;
+ }
+ finally
+ {
+ if (enumerator1 is IDisposable disposable)
+ disposable.Dispose();
+ }
+ case NotifyCollectionChangedAction.Remove:
+ var enumerator2 = e.OldItems.GetEnumerator();
+ try
+ {
+ while (enumerator2.MoveNext())
+ {
+ var current = (T) enumerator2.Current;
+ ItemRemoved(current);
+ _snapshot.Remove(current);
+ }
+ break;
+ }
+ finally
+ {
+ if (enumerator2 is IDisposable disposable)
+ disposable.Dispose();
+ }
+ case NotifyCollectionChangedAction.Replace:
+ foreach (T oldItem in e.OldItems)
+ {
+ ItemRemoved(oldItem);
+ _snapshot.Remove(oldItem);
+ }
+ var enumerator3 = e.NewItems.GetEnumerator();
+ try
+ {
+ while (enumerator3.MoveNext())
+ {
+ var current = (T) enumerator3.Current;
+ try
+ {
+ VerifyAdd(current);
+ ItemAdded(current);
+ }
+ finally
+ {
+ _snapshot.Insert(IndexOf(current), current);
+ }
+ }
+ break;
+ }
+ finally
+ {
+ if (enumerator3 is IDisposable disposable)
+ disposable.Dispose();
+ }
+ case NotifyCollectionChangedAction.Reset:
+ foreach (var obj in _snapshot)
+ ItemRemoved(obj);
+ _snapshot = new Collection();
+ using (var enumerator4 = GetEnumerator())
+ {
+ while (enumerator4.MoveNext())
+ {
+ var current = enumerator4.Current;
+ VerifyAdd(current);
+ ItemAdded(current);
+ }
+ break;
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/HandyControl/Interactivity/Behavior!1.cs b/HandyControl/Interactivity/Behavior!1.cs
new file mode 100644
index 00000000..bb1b7a14
--- /dev/null
+++ b/HandyControl/Interactivity/Behavior!1.cs
@@ -0,0 +1,13 @@
+using System.Windows;
+
+namespace HandyControl.Interactivity
+{
+ public abstract class Behavior : Behavior where T : DependencyObject
+ {
+ protected Behavior() : base(typeof(T))
+ {
+ }
+
+ protected new T AssociatedObject => (T)base.AssociatedObject;
+ }
+}
\ No newline at end of file
diff --git a/HandyControl/Interactivity/Behavior.cs b/HandyControl/Interactivity/Behavior.cs
new file mode 100644
index 00000000..a8487a15
--- /dev/null
+++ b/HandyControl/Interactivity/Behavior.cs
@@ -0,0 +1,87 @@
+using System;
+using System.Globalization;
+using System.Windows;
+using System.Windows.Media.Animation;
+
+namespace HandyControl.Interactivity
+{
+ public abstract class Behavior : Animatable, IAttachedObject
+ {
+ private DependencyObject _associatedObject;
+ private readonly Type _associatedType;
+
+ internal Behavior(Type associatedType)
+ {
+ _associatedType = associatedType;
+ }
+
+ protected DependencyObject AssociatedObject
+ {
+ get
+ {
+ ReadPreamble();
+ return _associatedObject;
+ }
+ }
+
+ protected Type AssociatedType
+ {
+ get
+ {
+ ReadPreamble();
+ return _associatedType;
+ }
+ }
+
+ public void Attach(DependencyObject dependencyObject)
+ {
+ if (!Equals(dependencyObject, AssociatedObject))
+ {
+ if (AssociatedObject != null)
+ throw new InvalidOperationException(ExceptionStringTable
+ .CannotHostBehaviorMultipleTimesExceptionMessage);
+ if (dependencyObject != null && !AssociatedType.IsInstanceOfType(dependencyObject))
+ throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture,
+ ExceptionStringTable.TypeConstraintViolatedExceptionMessage,
+ new object[] {GetType().Name, dependencyObject.GetType().Name, AssociatedType.Name}));
+ WritePreamble();
+ _associatedObject = dependencyObject;
+ WritePostscript();
+ OnAssociatedObjectChanged();
+ OnAttached();
+ }
+ }
+
+ public void Detach()
+ {
+ OnDetaching();
+ WritePreamble();
+ _associatedObject = null;
+ WritePostscript();
+ OnAssociatedObjectChanged();
+ }
+
+ DependencyObject IAttachedObject.AssociatedObject =>
+ AssociatedObject;
+
+ internal event EventHandler AssociatedObjectChanged;
+
+ protected override Freezable CreateInstanceCore()
+ {
+ return (Freezable) Activator.CreateInstance(GetType());
+ }
+
+ private void OnAssociatedObjectChanged()
+ {
+ AssociatedObjectChanged?.Invoke(this, new EventArgs());
+ }
+
+ protected virtual void OnAttached()
+ {
+ }
+
+ protected virtual void OnDetaching()
+ {
+ }
+ }
+}
\ No newline at end of file
diff --git a/HandyControl/Interactivity/BehaviorCollection.cs b/HandyControl/Interactivity/BehaviorCollection.cs
new file mode 100644
index 00000000..125384f9
--- /dev/null
+++ b/HandyControl/Interactivity/BehaviorCollection.cs
@@ -0,0 +1,42 @@
+using System.Windows;
+
+namespace HandyControl.Interactivity
+{
+ public sealed class BehaviorCollection : AttachableCollection
+ {
+ internal BehaviorCollection()
+ {
+ }
+
+ protected override void OnAttached()
+ {
+ foreach (var behavior in this)
+ behavior.Attach(AssociatedObject);
+ }
+
+ protected override void OnDetaching()
+ {
+ foreach (var behavior in this)
+ behavior.Detach();
+ }
+
+ internal override void ItemAdded(Behavior item)
+ {
+ if (AssociatedObject == null)
+ return;
+ item.Attach(AssociatedObject);
+ }
+
+ internal override void ItemRemoved(Behavior item)
+ {
+ if (((IAttachedObject) item).AssociatedObject == null)
+ return;
+ item.Detach();
+ }
+
+ protected override Freezable CreateInstanceCore()
+ {
+ return new BehaviorCollection();
+ }
+ }
+}
\ No newline at end of file
diff --git a/HandyControl/Interactivity/DefaultTriggerAttribute.cs b/HandyControl/Interactivity/DefaultTriggerAttribute.cs
new file mode 100644
index 00000000..2eb1cf97
--- /dev/null
+++ b/HandyControl/Interactivity/DefaultTriggerAttribute.cs
@@ -0,0 +1,49 @@
+using System;
+using System.Collections;
+using System.Globalization;
+
+namespace HandyControl.Interactivity
+{
+ [AttributeUsage(AttributeTargets.Property | AttributeTargets.Class, AllowMultiple = true)]
+ public sealed class DefaultTriggerAttribute : Attribute
+ {
+ private readonly object[] _parameters;
+
+ public DefaultTriggerAttribute(Type targetType, Type triggerType, object parameter) : this(targetType,
+ triggerType, new[] {parameter})
+ {
+ }
+
+ public DefaultTriggerAttribute(Type targetType, Type triggerType, params object[] parameters)
+ {
+ if (!typeof(TriggerBase).IsAssignableFrom(triggerType))
+ throw new ArgumentException(string.Format(CultureInfo.CurrentCulture,
+ ExceptionStringTable.DefaultTriggerAttributeInvalidTriggerTypeSpecifiedExceptionMessage,
+ new object[] {triggerType.Name}));
+ TargetType = targetType;
+ TriggerType = triggerType;
+ _parameters = parameters;
+ }
+
+ public IEnumerable Parameters =>
+ _parameters;
+
+ public Type TargetType { get; }
+
+ public Type TriggerType { get; }
+
+ public TriggerBase Instantiate()
+ {
+ object obj2 = null;
+ try
+ {
+ obj2 = Activator.CreateInstance(TriggerType, _parameters);
+ }
+ catch
+ {
+ // ignored
+ }
+ return (TriggerBase) obj2;
+ }
+ }
+}
\ No newline at end of file
diff --git a/HandyControl/Interactivity/EventTrigger.cs b/HandyControl/Interactivity/EventTrigger.cs
new file mode 100644
index 00000000..7df44802
--- /dev/null
+++ b/HandyControl/Interactivity/EventTrigger.cs
@@ -0,0 +1,29 @@
+using System.Windows;
+
+namespace HandyControl.Interactivity
+{
+ public class EventTrigger : EventTriggerBase