规范类名及命名空间名称
This commit is contained in:
@@ -0,0 +1,43 @@
|
||||
using System;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls.Primitives;
|
||||
|
||||
namespace XP.ImageProcessing.RoiControl
|
||||
{
|
||||
/// <summary>
|
||||
/// ROI控制�?
|
||||
/// </summary>
|
||||
public class ControlThumb : Thumb
|
||||
{
|
||||
private static readonly Style? thumbStyle;
|
||||
|
||||
static ControlThumb()
|
||||
{
|
||||
try
|
||||
{
|
||||
ResourceDictionary dictionary = new ResourceDictionary();
|
||||
dictionary.Source = new Uri("pack://application:,,,/XP.ImageProcessing.RoiControl;component/Themes/Generic.xaml", UriKind.Absolute);
|
||||
thumbStyle = (Style?)dictionary["AreaControlThumbStyle"];
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 如果样式加载失败,使用默认样�?
|
||||
thumbStyle = null;
|
||||
}
|
||||
}
|
||||
|
||||
public ControlThumb()
|
||||
{
|
||||
if (thumbStyle != null)
|
||||
{
|
||||
Style = thumbStyle;
|
||||
}
|
||||
else
|
||||
{
|
||||
// 默认样式
|
||||
Width = 12;
|
||||
Height = 12;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,270 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
using XP.ImageProcessing.RoiControl.Models;
|
||||
|
||||
namespace XP.ImageProcessing.RoiControl.Controls
|
||||
{
|
||||
/// <summary>
|
||||
/// 图像ROI画布控件,支持图像显示、ROI编辑、缩放和平移
|
||||
/// </summary>
|
||||
public class ImageROICanvas : Control
|
||||
{
|
||||
private Canvas? roiCanvas;
|
||||
private bool isDragging = false;
|
||||
private Point mouseDownPoint = new Point();
|
||||
private const double ZoomStep = 1.2;
|
||||
private const double MinZoom = 0.1;
|
||||
private const double MaxZoom = 10.0;
|
||||
|
||||
static ImageROICanvas()
|
||||
{
|
||||
DefaultStyleKeyProperty.OverrideMetadata(typeof(ImageROICanvas),
|
||||
new FrameworkPropertyMetadata(typeof(ImageROICanvas)));
|
||||
}
|
||||
|
||||
public ImageROICanvas()
|
||||
{
|
||||
Loaded += OnLoaded;
|
||||
}
|
||||
|
||||
private void OnLoaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
roiCanvas = GetTemplateChild("PART_Canvas") as Canvas;
|
||||
}
|
||||
|
||||
#region Dependency Properties
|
||||
|
||||
public static readonly DependencyProperty ImageSourceProperty =
|
||||
DependencyProperty.Register(nameof(ImageSource), typeof(ImageSource), typeof(ImageROICanvas),
|
||||
new PropertyMetadata(null, OnImageSourceChanged));
|
||||
|
||||
public ImageSource? ImageSource
|
||||
{
|
||||
get => (ImageSource?)GetValue(ImageSourceProperty);
|
||||
set => SetValue(ImageSourceProperty, value);
|
||||
}
|
||||
|
||||
private static void OnImageSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
var control = (ImageROICanvas)d;
|
||||
if (e.NewValue is BitmapSource bitmap && control.roiCanvas != null)
|
||||
{
|
||||
control.ImageWidth = bitmap.PixelWidth;
|
||||
control.ImageHeight = bitmap.PixelHeight;
|
||||
}
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty ROIItemsProperty =
|
||||
DependencyProperty.Register(nameof(ROIItems), typeof(ObservableCollection<ROIShape>), typeof(ImageROICanvas),
|
||||
new PropertyMetadata(null));
|
||||
|
||||
public ObservableCollection<ROIShape>? ROIItems
|
||||
{
|
||||
get => (ObservableCollection<ROIShape>?)GetValue(ROIItemsProperty);
|
||||
set => SetValue(ROIItemsProperty, value);
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty ZoomScaleProperty =
|
||||
DependencyProperty.Register(nameof(ZoomScale), typeof(double), typeof(ImageROICanvas),
|
||||
new PropertyMetadata(1.0));
|
||||
|
||||
public double ZoomScale
|
||||
{
|
||||
get => (double)GetValue(ZoomScaleProperty);
|
||||
set => SetValue(ZoomScaleProperty, value);
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty ZoomCenterProperty =
|
||||
DependencyProperty.Register(nameof(ZoomCenter), typeof(Point), typeof(ImageROICanvas),
|
||||
new PropertyMetadata(new Point()));
|
||||
|
||||
public Point ZoomCenter
|
||||
{
|
||||
get => (Point)GetValue(ZoomCenterProperty);
|
||||
set => SetValue(ZoomCenterProperty, value);
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty PanningOffsetXProperty =
|
||||
DependencyProperty.Register(nameof(PanningOffsetX), typeof(double), typeof(ImageROICanvas),
|
||||
new PropertyMetadata(0.0));
|
||||
|
||||
public double PanningOffsetX
|
||||
{
|
||||
get => (double)GetValue(PanningOffsetXProperty);
|
||||
set => SetValue(PanningOffsetXProperty, value);
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty PanningOffsetYProperty =
|
||||
DependencyProperty.Register(nameof(PanningOffsetY), typeof(double), typeof(ImageROICanvas),
|
||||
new PropertyMetadata(0.0));
|
||||
|
||||
public double PanningOffsetY
|
||||
{
|
||||
get => (double)GetValue(PanningOffsetYProperty);
|
||||
set => SetValue(PanningOffsetYProperty, value);
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty ImageWidthProperty =
|
||||
DependencyProperty.Register(nameof(ImageWidth), typeof(double), typeof(ImageROICanvas),
|
||||
new PropertyMetadata(800.0));
|
||||
|
||||
public double ImageWidth
|
||||
{
|
||||
get => (double)GetValue(ImageWidthProperty);
|
||||
set => SetValue(ImageWidthProperty, value);
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty ImageHeightProperty =
|
||||
DependencyProperty.Register(nameof(ImageHeight), typeof(double), typeof(ImageROICanvas),
|
||||
new PropertyMetadata(600.0));
|
||||
|
||||
public double ImageHeight
|
||||
{
|
||||
get => (double)GetValue(ImageHeightProperty);
|
||||
set => SetValue(ImageHeightProperty, value);
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty SelectedROIProperty =
|
||||
DependencyProperty.Register(nameof(SelectedROI), typeof(ROIShape), typeof(ImageROICanvas),
|
||||
new PropertyMetadata(null));
|
||||
|
||||
public ROIShape? SelectedROI
|
||||
{
|
||||
get => (ROIShape?)GetValue(SelectedROIProperty);
|
||||
set => SetValue(SelectedROIProperty, value);
|
||||
}
|
||||
|
||||
#endregion Dependency Properties
|
||||
|
||||
#region Mouse Events
|
||||
|
||||
protected override void OnMouseWheel(MouseWheelEventArgs e)
|
||||
{
|
||||
base.OnMouseWheel(e);
|
||||
|
||||
Point mousePos = e.GetPosition(this);
|
||||
|
||||
if (e.Delta > 0)
|
||||
{
|
||||
ZoomIn(mousePos);
|
||||
}
|
||||
else
|
||||
{
|
||||
ZoomOut(mousePos);
|
||||
}
|
||||
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e)
|
||||
{
|
||||
base.OnMouseLeftButtonDown(e);
|
||||
mouseDownPoint = e.GetPosition(this);
|
||||
isDragging = false;
|
||||
}
|
||||
|
||||
protected override void OnMouseMove(MouseEventArgs e)
|
||||
{
|
||||
base.OnMouseMove(e);
|
||||
|
||||
if (e.LeftButton == MouseButtonState.Pressed)
|
||||
{
|
||||
Point mousePoint = e.GetPosition(this);
|
||||
double mouseMoveLength = (mousePoint - mouseDownPoint).Length;
|
||||
|
||||
if (mouseMoveLength > 10 / ZoomScale)
|
||||
{
|
||||
isDragging = true;
|
||||
PanningOffsetX += mousePoint.X - mouseDownPoint.X;
|
||||
PanningOffsetY += mousePoint.Y - mouseDownPoint.Y;
|
||||
mouseDownPoint = mousePoint;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnMouseLeftButtonUp(MouseButtonEventArgs e)
|
||||
{
|
||||
base.OnMouseLeftButtonUp(e);
|
||||
|
||||
if (!isDragging)
|
||||
{
|
||||
// 处理点击事件(如添加多边形顶点)
|
||||
Point clickPoint = e.GetPosition(roiCanvas);
|
||||
OnCanvasClicked(clickPoint);
|
||||
}
|
||||
|
||||
isDragging = false;
|
||||
}
|
||||
|
||||
#endregion Mouse Events
|
||||
|
||||
#region Zoom Methods
|
||||
|
||||
public void ZoomIn(Point center)
|
||||
{
|
||||
double newZoom = ZoomScale * ZoomStep;
|
||||
if (newZoom <= MaxZoom)
|
||||
{
|
||||
ZoomCenter = center;
|
||||
ZoomScale = newZoom;
|
||||
}
|
||||
}
|
||||
|
||||
public void ZoomOut(Point center)
|
||||
{
|
||||
double newZoom = ZoomScale / ZoomStep;
|
||||
if (newZoom >= MinZoom)
|
||||
{
|
||||
ZoomCenter = center;
|
||||
ZoomScale = newZoom;
|
||||
}
|
||||
}
|
||||
|
||||
public void ResetZoom()
|
||||
{
|
||||
ZoomScale = 1.0;
|
||||
PanningOffsetX = 0;
|
||||
PanningOffsetY = 0;
|
||||
ZoomCenter = new Point();
|
||||
}
|
||||
|
||||
#endregion Zoom Methods
|
||||
|
||||
#region Events
|
||||
|
||||
public static readonly RoutedEvent CanvasClickedEvent =
|
||||
EventManager.RegisterRoutedEvent(nameof(CanvasClicked), RoutingStrategy.Bubble,
|
||||
typeof(RoutedEventHandler), typeof(ImageROICanvas));
|
||||
|
||||
public event RoutedEventHandler CanvasClicked
|
||||
{
|
||||
add { AddHandler(CanvasClickedEvent, value); }
|
||||
remove { RemoveHandler(CanvasClickedEvent, value); }
|
||||
}
|
||||
|
||||
protected virtual void OnCanvasClicked(Point position)
|
||||
{
|
||||
var args = new CanvasClickedEventArgs(CanvasClickedEvent, position);
|
||||
RaiseEvent(args);
|
||||
}
|
||||
|
||||
#endregion Events
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 画布点击事件参数
|
||||
/// </summary>
|
||||
public class CanvasClickedEventArgs : RoutedEventArgs
|
||||
{
|
||||
public Point Position { get; }
|
||||
|
||||
public CanvasClickedEventArgs(RoutedEvent routedEvent, Point position) : base(routedEvent)
|
||||
{
|
||||
Position = position;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
<UserControl x:Class="XP.ImageProcessing.RoiControl.Controls.PolygonRoiCanvas"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:local="clr-namespace:XP.ImageProcessing.RoiControl.Controls"
|
||||
xmlns:models="clr-namespace:XP.ImageProcessing.RoiControl.Models"
|
||||
xmlns:converters="clr-namespace:XP.ImageProcessing.RoiControl.Converters"
|
||||
xmlns:behaviors="clr-namespace:XP.ImageProcessing.RoiControl"
|
||||
mc:Ignorable="d"
|
||||
d:DesignHeight="450" d:DesignWidth="800"
|
||||
x:Name="root"
|
||||
Background="White">
|
||||
<UserControl.Resources>
|
||||
<behaviors:PointListToPointCollectionConverter x:Key="PointListToPointCollectionConverter" />
|
||||
<converters:ROITypeToVisibilityConverter x:Key="ROITypeToVisibilityConverter" />
|
||||
</UserControl.Resources>
|
||||
<Border BorderBrush="Transparent" BorderThickness="1" ClipToBounds="True">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<!-- 左侧控制按钮 -->
|
||||
<Border Grid.Column="0" Background="White" Padding="5">
|
||||
<StackPanel Orientation="Vertical" VerticalAlignment="Top">
|
||||
<Button x:Name="btnZoomIn" Content="+" Background="White" BorderBrush="LightGray" Width="40" Height="40" Margin="2" Click="BtnZoomIn_Click" />
|
||||
<Button x:Name="btnZoomOut" Content="-" Background="White" BorderBrush="LightGray" Width="40" Height="40" Margin="2" Click="BtnZoomOut_Click" />
|
||||
<Button x:Name="btnReset" Content="适应" Background="White" BorderBrush="LightGray" Width="40" Height="40" Margin="2" Click="BtnReset_Click" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- 图像显示区域 -->
|
||||
<Grid Grid.Column="1" x:Name="imageDisplayGrid" ClipToBounds="True">
|
||||
<Grid x:Name="transformGrid"
|
||||
RenderTransformOrigin="0,0"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center">
|
||||
<Grid.RenderTransform>
|
||||
<TransformGroup>
|
||||
<ScaleTransform x:Name="scaleTransform"
|
||||
ScaleX="{Binding ZoomScale, ElementName=root}"
|
||||
ScaleY="{Binding ZoomScale, ElementName=root}" />
|
||||
<TranslateTransform x:Name="translateTransform"
|
||||
X="{Binding PanOffsetX, ElementName=root}"
|
||||
Y="{Binding PanOffsetY, ElementName=root}" />
|
||||
</TransformGroup>
|
||||
</Grid.RenderTransform>
|
||||
|
||||
<Canvas x:Name="mainCanvas"
|
||||
Width="{Binding CanvasWidth, ElementName=root}"
|
||||
Height="{Binding CanvasHeight, ElementName=root}"
|
||||
Background="Transparent"
|
||||
MouseWheel="Canvas_MouseWheel"
|
||||
MouseLeftButtonDown="Canvas_MouseLeftButtonDown"
|
||||
MouseLeftButtonUp="Canvas_MouseLeftButtonUp"
|
||||
MouseMove="Canvas_MouseMove"
|
||||
MouseRightButtonDown="Canvas_MouseRightButtonDown">
|
||||
|
||||
<!-- 背景图像 -->
|
||||
<Image x:Name="backgroundImage"
|
||||
Source="{Binding ImageSource, ElementName=root}"
|
||||
Width="{Binding CanvasWidth, ElementName=root}"
|
||||
Height="{Binding CanvasHeight, ElementName=root}"
|
||||
Stretch="Fill" />
|
||||
|
||||
<!-- ROI显示 - 只支持多边形 -->
|
||||
<ItemsControl ItemsSource="{Binding ROIItems, ElementName=root}">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<Canvas />
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<!-- 多边形ROI -->
|
||||
<Polygon x:Name="polygonShape"
|
||||
behaviors:PolygonPointsBehavior.PointsSource="{Binding Points}"
|
||||
Stroke="{Binding Color}"
|
||||
StrokeThickness="1"
|
||||
Fill="Transparent"
|
||||
MouseLeftButtonDown="ROI_MouseLeftButtonDown" />
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</Canvas>
|
||||
</Grid>
|
||||
|
||||
<!-- 缩放比例显示 -->
|
||||
<TextBlock Text="{Binding ZoomScale, ElementName=root, StringFormat=Zoom Scale: {0:P0}}"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Bottom"
|
||||
Margin="10"
|
||||
Padding="5"
|
||||
Background="#AA000000"
|
||||
Foreground="White"
|
||||
FontSize="10" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Border>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,527 @@
|
||||
using System;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Collections.Specialized;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Documents;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Shapes;
|
||||
using XP.ImageProcessing.RoiControl.Models;
|
||||
|
||||
namespace XP.ImageProcessing.RoiControl.Controls
|
||||
{
|
||||
public partial class PolygonRoiCanvas : UserControl
|
||||
{
|
||||
private bool isDragging = false;
|
||||
private Point lastMousePosition;
|
||||
private const double ZoomStep = 1.2;
|
||||
private Adorner? currentAdorner;
|
||||
|
||||
public PolygonRoiCanvas()
|
||||
{
|
||||
InitializeComponent();
|
||||
Loaded += PolygonRoiCanvas_Loaded;
|
||||
}
|
||||
|
||||
private void PolygonRoiCanvas_Loaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
// 监听ROI集合变化
|
||||
if (ROIItems != null)
|
||||
{
|
||||
ROIItems.CollectionChanged += ROIItems_CollectionChanged;
|
||||
foreach (var roi in ROIItems)
|
||||
{
|
||||
roi.PropertyChanged += ROI_PropertyChanged;
|
||||
|
||||
// 如果是多边形ROI,监听Points集合变化
|
||||
if (roi is PolygonROI polygonROI)
|
||||
{
|
||||
polygonROI.Points.CollectionChanged += Points_CollectionChanged;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ROIItems_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
|
||||
{
|
||||
if (e.NewItems != null)
|
||||
{
|
||||
foreach (ROIShape roi in e.NewItems)
|
||||
{
|
||||
roi.PropertyChanged += ROI_PropertyChanged;
|
||||
|
||||
// 如果是多边形ROI,监听Points集合变化
|
||||
if (roi is PolygonROI polygonROI)
|
||||
{
|
||||
polygonROI.Points.CollectionChanged += Points_CollectionChanged;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (e.OldItems != null)
|
||||
{
|
||||
foreach (ROIShape roi in e.OldItems)
|
||||
{
|
||||
roi.PropertyChanged -= ROI_PropertyChanged;
|
||||
|
||||
// 取消监听Points集合变化
|
||||
if (roi is PolygonROI polygonROI)
|
||||
{
|
||||
polygonROI.Points.CollectionChanged -= Points_CollectionChanged;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void Points_CollectionChanged(object? sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
|
||||
{
|
||||
// 只在删除或添加顶点时更新Adorner,拖拽时的Replace操作不触发更�?
|
||||
if (e.Action == System.Collections.Specialized.NotifyCollectionChangedAction.Remove ||
|
||||
e.Action == System.Collections.Specialized.NotifyCollectionChangedAction.Add)
|
||||
{
|
||||
// Points集合变化时,如果当前选中的是多边形ROI,更新Adorner
|
||||
if (SelectedROI is PolygonROI polygonROI && sender == polygonROI.Points)
|
||||
{
|
||||
// 使用Dispatcher延迟更新,确保UI已经处理完Points的变�?
|
||||
Dispatcher.BeginInvoke(new Action(() =>
|
||||
{
|
||||
UpdateAdorner();
|
||||
}), System.Windows.Threading.DispatcherPriority.Render);
|
||||
}
|
||||
}
|
||||
// Replace操作(拖拽时)不需要重建Adorner,只需要让现有Adorner重新布局
|
||||
}
|
||||
|
||||
private void ROI_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
|
||||
{
|
||||
if (e.PropertyName == nameof(ROIShape.IsSelected))
|
||||
{
|
||||
UpdateAdorner();
|
||||
}
|
||||
// 监听Points属性变化(整个集合替换的情况)
|
||||
else if (e.PropertyName == "Points" && sender is PolygonROI)
|
||||
{
|
||||
UpdateAdorner();
|
||||
}
|
||||
}
|
||||
|
||||
#region Dependency Properties
|
||||
|
||||
public static readonly DependencyProperty ImageSourceProperty =
|
||||
DependencyProperty.Register(nameof(ImageSource), typeof(ImageSource), typeof(PolygonRoiCanvas),
|
||||
new PropertyMetadata(null, OnImageSourceChanged));
|
||||
|
||||
public ImageSource? ImageSource
|
||||
{
|
||||
get => (ImageSource?)GetValue(ImageSourceProperty);
|
||||
set => SetValue(ImageSourceProperty, value);
|
||||
}
|
||||
|
||||
private static void OnImageSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
var control = (PolygonRoiCanvas)d;
|
||||
if (e.NewValue is ImageSource imageSource)
|
||||
{
|
||||
control.CanvasWidth = imageSource.Width;
|
||||
control.CanvasHeight = imageSource.Height;
|
||||
}
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty ROIItemsProperty =
|
||||
DependencyProperty.Register(nameof(ROIItems), typeof(ObservableCollection<ROIShape>), typeof(PolygonRoiCanvas),
|
||||
new PropertyMetadata(null));
|
||||
|
||||
public ObservableCollection<ROIShape>? ROIItems
|
||||
{
|
||||
get => (ObservableCollection<ROIShape>?)GetValue(ROIItemsProperty);
|
||||
set => SetValue(ROIItemsProperty, value);
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty ZoomScaleProperty =
|
||||
DependencyProperty.Register(nameof(ZoomScale), typeof(double), typeof(PolygonRoiCanvas),
|
||||
new PropertyMetadata(1.0, OnZoomScaleChanged));
|
||||
|
||||
public double ZoomScale
|
||||
{
|
||||
get => (double)GetValue(ZoomScaleProperty);
|
||||
set => SetValue(ZoomScaleProperty, Math.Max(0.1, Math.Min(10.0, value)));
|
||||
}
|
||||
|
||||
private static void OnZoomScaleChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
var control = (PolygonRoiCanvas)d;
|
||||
// 缩放变化时更新Adorner以调整控制点大小
|
||||
control.UpdateAdorner();
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty PanOffsetXProperty =
|
||||
DependencyProperty.Register(nameof(PanOffsetX), typeof(double), typeof(PolygonRoiCanvas),
|
||||
new PropertyMetadata(0.0, OnPanOffsetChanged));
|
||||
|
||||
public double PanOffsetX
|
||||
{
|
||||
get => (double)GetValue(PanOffsetXProperty);
|
||||
set => SetValue(PanOffsetXProperty, value);
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty PanOffsetYProperty =
|
||||
DependencyProperty.Register(nameof(PanOffsetY), typeof(double), typeof(PolygonRoiCanvas),
|
||||
new PropertyMetadata(0.0, OnPanOffsetChanged));
|
||||
|
||||
public double PanOffsetY
|
||||
{
|
||||
get => (double)GetValue(PanOffsetYProperty);
|
||||
set => SetValue(PanOffsetYProperty, value);
|
||||
}
|
||||
|
||||
private static void OnPanOffsetChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
var control = (PolygonRoiCanvas)d;
|
||||
|
||||
// 平移时重建Adorner,确保控制点位置正确
|
||||
if (control.SelectedROI != null && control.SelectedROI.IsSelected)
|
||||
{
|
||||
control.UpdateAdorner();
|
||||
}
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty CanvasWidthProperty =
|
||||
DependencyProperty.Register(nameof(CanvasWidth), typeof(double), typeof(PolygonRoiCanvas),
|
||||
new PropertyMetadata(800.0));
|
||||
|
||||
public double CanvasWidth
|
||||
{
|
||||
get => (double)GetValue(CanvasWidthProperty);
|
||||
set => SetValue(CanvasWidthProperty, value);
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty CanvasHeightProperty =
|
||||
DependencyProperty.Register(nameof(CanvasHeight), typeof(double), typeof(PolygonRoiCanvas),
|
||||
new PropertyMetadata(600.0));
|
||||
|
||||
public double CanvasHeight
|
||||
{
|
||||
get => (double)GetValue(CanvasHeightProperty);
|
||||
set => SetValue(CanvasHeightProperty, value);
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty SelectedROIProperty =
|
||||
DependencyProperty.Register(nameof(SelectedROI), typeof(ROIShape), typeof(PolygonRoiCanvas),
|
||||
new PropertyMetadata(null, OnSelectedROIChanged));
|
||||
|
||||
public ROIShape? SelectedROI
|
||||
{
|
||||
get => (ROIShape?)GetValue(SelectedROIProperty);
|
||||
set => SetValue(SelectedROIProperty, value);
|
||||
}
|
||||
|
||||
private static void OnSelectedROIChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
var control = (PolygonRoiCanvas)d;
|
||||
|
||||
// 更新IsSelected状�?
|
||||
if (e.OldValue is ROIShape oldROI)
|
||||
{
|
||||
oldROI.IsSelected = false;
|
||||
}
|
||||
if (e.NewValue is ROIShape newROI)
|
||||
{
|
||||
newROI.IsSelected = true;
|
||||
}
|
||||
|
||||
control.UpdateAdorner();
|
||||
}
|
||||
|
||||
#endregion Dependency Properties
|
||||
|
||||
#region Adorner Management
|
||||
|
||||
private void UpdateAdorner()
|
||||
{
|
||||
// 移除旧的Adorner
|
||||
if (currentAdorner != null)
|
||||
{
|
||||
var adornerLayer = AdornerLayer.GetAdornerLayer(mainCanvas);
|
||||
if (adornerLayer != null)
|
||||
{
|
||||
adornerLayer.Remove(currentAdorner);
|
||||
}
|
||||
currentAdorner = null;
|
||||
}
|
||||
|
||||
// 为选中的ROI添加Adorner
|
||||
if (SelectedROI != null && SelectedROI.IsSelected)
|
||||
{
|
||||
// 查找对应的UI元素
|
||||
var container = FindROIVisual(SelectedROI);
|
||||
if (container != null)
|
||||
{
|
||||
var adornerLayer = AdornerLayer.GetAdornerLayer(mainCanvas);
|
||||
if (adornerLayer != null)
|
||||
{
|
||||
double scaleFactor = 1.0 / ZoomScale;
|
||||
|
||||
if (SelectedROI is PolygonROI polygonROI)
|
||||
{
|
||||
currentAdorner = new PolygonAdorner(container, scaleFactor, polygonROI);
|
||||
}
|
||||
|
||||
if (currentAdorner != null)
|
||||
{
|
||||
adornerLayer.Add(currentAdorner);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private UIElement? FindROIVisual(ROIShape roi)
|
||||
{
|
||||
// 在ItemsControl中查找对应的视觉元素
|
||||
var itemsControl = FindVisualChild<ItemsControl>(mainCanvas);
|
||||
if (itemsControl != null)
|
||||
{
|
||||
for (int i = 0; i < itemsControl.Items.Count; i++)
|
||||
{
|
||||
if (itemsControl.Items[i] == roi)
|
||||
{
|
||||
// 尝试获取容器
|
||||
var container = itemsControl.ItemContainerGenerator.ContainerFromIndex(i) as ContentPresenter;
|
||||
|
||||
// 如果容器还没生成,尝试强制生�?
|
||||
if (container == null)
|
||||
{
|
||||
// 强制生成容器
|
||||
itemsControl.UpdateLayout();
|
||||
container = itemsControl.ItemContainerGenerator.ContainerFromIndex(i) as ContentPresenter;
|
||||
}
|
||||
|
||||
if (container != null)
|
||||
{
|
||||
// 查找实际的形状元素(只支持多边形�?
|
||||
if (roi is PolygonROI)
|
||||
{
|
||||
return FindVisualChild<Polygon>(container);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private T? FindVisualChild<T>(DependencyObject parent) where T : DependencyObject
|
||||
{
|
||||
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(parent); i++)
|
||||
{
|
||||
var child = VisualTreeHelper.GetChild(parent, i);
|
||||
if (child is T result)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
var childOfChild = FindVisualChild<T>(child);
|
||||
if (childOfChild != null)
|
||||
{
|
||||
return childOfChild;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
#endregion Adorner Management
|
||||
|
||||
#region Mouse Events
|
||||
|
||||
private void Canvas_MouseWheel(object sender, MouseWheelEventArgs e)
|
||||
{
|
||||
// 获取鼠标�?imageDisplayGrid 中的位置
|
||||
Point mousePos = e.GetPosition(imageDisplayGrid);
|
||||
|
||||
// 获取鼠标�?Canvas 中的位置(缩放前�?
|
||||
Point mousePosOnCanvas = e.GetPosition(mainCanvas);
|
||||
|
||||
double oldZoom = ZoomScale;
|
||||
double newZoom = oldZoom;
|
||||
|
||||
if (e.Delta > 0)
|
||||
{
|
||||
newZoom = oldZoom * ZoomStep;
|
||||
}
|
||||
else
|
||||
{
|
||||
newZoom = oldZoom / ZoomStep;
|
||||
}
|
||||
|
||||
// 限制缩放范围
|
||||
newZoom = Math.Max(0.1, Math.Min(10.0, newZoom));
|
||||
|
||||
if (Math.Abs(newZoom - oldZoom) > 0.001)
|
||||
{
|
||||
// 计算缩放比例变化
|
||||
double scale = newZoom / oldZoom;
|
||||
|
||||
// 更新缩放
|
||||
ZoomScale = newZoom;
|
||||
|
||||
// 调整平移偏移,使鼠标位置保持不变
|
||||
// 新的偏移 = 旧偏�?+ 鼠标位置 - 鼠标位置 * 缩放比例
|
||||
PanOffsetX = mousePos.X - (mousePos.X - PanOffsetX) * scale;
|
||||
PanOffsetY = mousePos.Y - (mousePos.Y - PanOffsetY) * scale;
|
||||
}
|
||||
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
private void Canvas_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
lastMousePosition = e.GetPosition(imageDisplayGrid);
|
||||
isDragging = false;
|
||||
mainCanvas.CaptureMouse();
|
||||
}
|
||||
|
||||
private void Canvas_MouseMove(object sender, MouseEventArgs e)
|
||||
{
|
||||
if (e.LeftButton == MouseButtonState.Pressed && mainCanvas.IsMouseCaptured)
|
||||
{
|
||||
Point currentPosition = e.GetPosition(imageDisplayGrid);
|
||||
Vector delta = currentPosition - lastMousePosition;
|
||||
|
||||
if (delta.Length > 5)
|
||||
{
|
||||
isDragging = true;
|
||||
PanOffsetX += delta.X;
|
||||
PanOffsetY += delta.Y;
|
||||
lastMousePosition = currentPosition;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void Canvas_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
mainCanvas.ReleaseMouseCapture();
|
||||
|
||||
if (!isDragging)
|
||||
{
|
||||
// 处理点击事件
|
||||
Point clickPosition = e.GetPosition(mainCanvas);
|
||||
OnCanvasClicked(clickPosition);
|
||||
}
|
||||
|
||||
isDragging = false;
|
||||
}
|
||||
|
||||
private void Canvas_MouseRightButtonDown(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
// 右键点击完成多边�?
|
||||
OnRightClick();
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
private void ROI_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
// 选择ROI
|
||||
if (sender is FrameworkElement element && element.DataContext is ROIShape roi)
|
||||
{
|
||||
SelectedROI = roi;
|
||||
e.Handled = true;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion Mouse Events
|
||||
|
||||
#region Public Methods
|
||||
|
||||
public void ResetView()
|
||||
{
|
||||
// 自动适应显示窗口 (类似 PictureBox SizeMode.Zoom)
|
||||
ZoomScale = 1.0;
|
||||
PanOffsetX = 0;
|
||||
PanOffsetY = 0;
|
||||
|
||||
if (imageDisplayGrid != null && CanvasWidth > 0 && CanvasHeight > 0)
|
||||
{
|
||||
// 使用 Dispatcher 延迟执行,确保布局已完�?
|
||||
Dispatcher.BeginInvoke(new Action(() =>
|
||||
{
|
||||
// 获取图像显示区域的实际尺�?
|
||||
double viewportWidth = imageDisplayGrid.ActualWidth;
|
||||
double viewportHeight = imageDisplayGrid.ActualHeight;
|
||||
|
||||
if (viewportWidth > 0 && viewportHeight > 0)
|
||||
{
|
||||
// 计算宽度和高度的缩放比例
|
||||
double scaleX = viewportWidth / CanvasWidth;
|
||||
double scaleY = viewportHeight / CanvasHeight;
|
||||
|
||||
// 选择较小的缩放比例,确保图像完全显示在窗口内(保持宽高比�?
|
||||
ZoomScale = Math.Min(scaleX, scaleY);
|
||||
|
||||
// 居中显示�?Grid �?HorizontalAlignment �?VerticalAlignment 自动处理
|
||||
PanOffsetX = 0;
|
||||
PanOffsetY = 0;
|
||||
}
|
||||
}), System.Windows.Threading.DispatcherPriority.Loaded);
|
||||
}
|
||||
}
|
||||
|
||||
private void BtnZoomIn_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
double newZoom = ZoomScale * 1.2;
|
||||
if (newZoom <= 10.0)
|
||||
{
|
||||
ZoomScale = newZoom;
|
||||
}
|
||||
}
|
||||
|
||||
private void BtnZoomOut_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
double newZoom = ZoomScale / 1.2;
|
||||
if (newZoom >= 0.1)
|
||||
{
|
||||
ZoomScale = newZoom;
|
||||
}
|
||||
}
|
||||
|
||||
private void BtnReset_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
ResetView();
|
||||
}
|
||||
|
||||
#endregion Public Methods
|
||||
|
||||
#region Events
|
||||
|
||||
public static readonly RoutedEvent CanvasClickedEvent =
|
||||
EventManager.RegisterRoutedEvent(nameof(CanvasClicked), RoutingStrategy.Bubble,
|
||||
typeof(RoutedEventHandler), typeof(PolygonRoiCanvas));
|
||||
|
||||
public event RoutedEventHandler CanvasClicked
|
||||
{
|
||||
add { AddHandler(CanvasClickedEvent, value); }
|
||||
remove { RemoveHandler(CanvasClickedEvent, value); }
|
||||
}
|
||||
|
||||
protected virtual void OnCanvasClicked(Point position)
|
||||
{
|
||||
var args = new CanvasClickedEventArgs(CanvasClickedEvent, position);
|
||||
RaiseEvent(args);
|
||||
}
|
||||
|
||||
public static readonly RoutedEvent RightClickEvent =
|
||||
EventManager.RegisterRoutedEvent(nameof(RightClick), RoutingStrategy.Bubble,
|
||||
typeof(RoutedEventHandler), typeof(PolygonRoiCanvas));
|
||||
|
||||
public event RoutedEventHandler RightClick
|
||||
{
|
||||
add { AddHandler(RightClickEvent, value); }
|
||||
remove { RemoveHandler(RightClickEvent, value); }
|
||||
}
|
||||
|
||||
protected virtual void OnRightClick()
|
||||
{
|
||||
RaiseEvent(new RoutedEventArgs(RightClickEvent));
|
||||
}
|
||||
|
||||
#endregion Events
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Windows;
|
||||
|
||||
namespace XP.ImageProcessing.RoiControl.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// ROI形状类型
|
||||
/// </summary>
|
||||
public enum ROIType
|
||||
{
|
||||
Polygon
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ROI基类
|
||||
/// </summary>
|
||||
public abstract class ROIShape : INotifyPropertyChanged
|
||||
{
|
||||
private bool _isSelected;
|
||||
private string _id = Guid.NewGuid().ToString();
|
||||
private string _color = "Red";
|
||||
|
||||
public string Id
|
||||
{
|
||||
get => _id;
|
||||
set { _id = value; OnPropertyChanged(); }
|
||||
}
|
||||
|
||||
public bool IsSelected
|
||||
{
|
||||
get => _isSelected;
|
||||
set { _isSelected = value; OnPropertyChanged(); }
|
||||
}
|
||||
|
||||
public string Color
|
||||
{
|
||||
get => _color;
|
||||
set { _color = value; OnPropertyChanged(); }
|
||||
}
|
||||
|
||||
public abstract ROIType ROIType { get; }
|
||||
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
|
||||
public void OnPropertyChanged([CallerMemberName] string? propertyName = null)
|
||||
{
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 多边形ROI
|
||||
/// </summary>
|
||||
public class PolygonROI : ROIShape
|
||||
{
|
||||
private ObservableCollection<Point> _points = new ObservableCollection<Point>();
|
||||
|
||||
public override ROIType ROIType => ROIType.Polygon;
|
||||
|
||||
public ObservableCollection<Point> Points
|
||||
{
|
||||
get => _points;
|
||||
set { _points = value; OnPropertyChanged(); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 用于JSON序列化的Points列表(不参与UI绑定�?
|
||||
/// </summary>
|
||||
[System.Text.Json.Serialization.JsonPropertyName("PointsList")]
|
||||
public List<Point> PointsList
|
||||
{
|
||||
get => new List<Point>(_points);
|
||||
set
|
||||
{
|
||||
_points = new ObservableCollection<Point>(value ?? new List<Point>());
|
||||
OnPropertyChanged(nameof(Points));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Globalization;
|
||||
using System.Windows;
|
||||
using System.Windows.Data;
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace XP.ImageProcessing.RoiControl
|
||||
{
|
||||
/// <summary>
|
||||
/// 将Point列表转换为PointCollection,支持ObservableCollection变化通知
|
||||
/// </summary>
|
||||
public class PointListToPointCollectionConverter : IValueConverter
|
||||
{
|
||||
public object? Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
if (value is List<Point> pointList)
|
||||
{
|
||||
return new PointCollection(pointList);
|
||||
}
|
||||
else if (value is ObservableCollection<Point> observablePoints)
|
||||
{
|
||||
var pointCollection = new PointCollection(observablePoints);
|
||||
return pointCollection;
|
||||
}
|
||||
else if (value is IEnumerable<Point> enumerable)
|
||||
{
|
||||
return new PointCollection(enumerable);
|
||||
}
|
||||
return new PointCollection();
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
if (value is PointCollection pointCollection)
|
||||
{
|
||||
var list = new ObservableCollection<Point>();
|
||||
foreach (Point p in pointCollection)
|
||||
{
|
||||
list.Add(p);
|
||||
}
|
||||
return list;
|
||||
}
|
||||
return new ObservableCollection<Point>();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 索引转换为位置标�?
|
||||
/// </summary>
|
||||
public class IndexToPositionConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
if (value is int index)
|
||||
{
|
||||
return (index + 1).ToString();
|
||||
}
|
||||
return "0";
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls.Primitives;
|
||||
using System.Windows.Documents;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Shapes;
|
||||
|
||||
namespace XP.ImageProcessing.RoiControl
|
||||
{
|
||||
/// <summary>
|
||||
/// 多边形装饰器,用于编辑多边形顶点
|
||||
/// </summary>
|
||||
public class PolygonAdorner : Adorner
|
||||
{
|
||||
private List<ControlThumb> vertexThumbs = new List<ControlThumb>(); // 顶点控制�?
|
||||
private VisualCollection visualChildren;
|
||||
private double scaleFactor = 1;
|
||||
private Models.PolygonROI? polygonROI;
|
||||
|
||||
public PolygonAdorner(UIElement adornedElement, double scaleFactor = 1, Models.PolygonROI? roiModel = null)
|
||||
: base(adornedElement)
|
||||
{
|
||||
visualChildren = new VisualCollection(this);
|
||||
this.scaleFactor = scaleFactor;
|
||||
this.polygonROI = roiModel;
|
||||
|
||||
// 使用ROI模型的Points数量而不是Polygon的Points
|
||||
int pointCount = polygonROI?.Points.Count ?? 0;
|
||||
|
||||
// 创建顶点控制�?
|
||||
for (int i = 0; i < pointCount; i++)
|
||||
{
|
||||
var thumb = new ControlThumb();
|
||||
thumb.DragDelta += HandleDrag;
|
||||
thumb.DragCompleted += HandleDragCompleted;
|
||||
thumb.MouseRightButtonDown += HandleRightClick;
|
||||
thumb.Tag = i;
|
||||
thumb.Cursor = Cursors.Hand;
|
||||
vertexThumbs.Add(thumb);
|
||||
visualChildren.Add(thumb);
|
||||
}
|
||||
|
||||
// 不再创建边中点控制点 - 使用智能插入算法代替
|
||||
// 用户可以直接点击画布,系统会自动找到最近的边并插入顶点
|
||||
}
|
||||
|
||||
private void HandleDrag(object sender, DragDeltaEventArgs args)
|
||||
{
|
||||
Thumb? hitThumb = sender as Thumb;
|
||||
if (hitThumb == null || polygonROI == null) return;
|
||||
|
||||
int index = (int)hitThumb.Tag;
|
||||
|
||||
// 直接修改ROI模型的Points
|
||||
if (index < polygonROI.Points.Count)
|
||||
{
|
||||
Point currentPoint = polygonROI.Points[index];
|
||||
Point newPoint = new Point(
|
||||
currentPoint.X + args.HorizontalChange,
|
||||
currentPoint.Y + args.VerticalChange
|
||||
);
|
||||
|
||||
// 使用索引器修改ObservableCollection中的元素
|
||||
polygonROI.Points[index] = newPoint;
|
||||
}
|
||||
|
||||
// 强制重新布局
|
||||
InvalidateArrange();
|
||||
}
|
||||
|
||||
private void HandleDragCompleted(object sender, DragCompletedEventArgs args)
|
||||
{
|
||||
// 拖拽完成后通知模型更新
|
||||
if (polygonROI != null)
|
||||
{
|
||||
polygonROI.OnPropertyChanged(nameof(polygonROI.Points));
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleRightClick(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
// 右键删除顶点(至少保�?个顶点)
|
||||
if (polygonROI != null && polygonROI.Points.Count > 3)
|
||||
{
|
||||
Thumb? hitThumb = sender as Thumb;
|
||||
if (hitThumb != null)
|
||||
{
|
||||
int index = (int)hitThumb.Tag;
|
||||
|
||||
// 删除顶点 - ObservableCollection会自动触发CollectionChanged事件
|
||||
// PolygonRoiCanvas会监听到这个变化并自动更新Adorner
|
||||
polygonROI.Points.RemoveAt(index);
|
||||
|
||||
e.Handled = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override Size ArrangeOverride(Size finalSize)
|
||||
{
|
||||
// 使用ROI模型的Points而不是Polygon的Points
|
||||
if (polygonROI != null)
|
||||
{
|
||||
double thumbSize = 12 * scaleFactor;
|
||||
|
||||
// 布局顶点控制�?
|
||||
for (int i = 0; i < vertexThumbs.Count && i < polygonROI.Points.Count; i++)
|
||||
{
|
||||
vertexThumbs[i].Arrange(new Rect(
|
||||
polygonROI.Points[i].X - (thumbSize / 2),
|
||||
polygonROI.Points[i].Y - (thumbSize / 2),
|
||||
thumbSize,
|
||||
thumbSize));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// 备用方案:使用Polygon的Points
|
||||
Polygon poly = (Polygon)AdornedElement;
|
||||
double thumbSize = 12 * scaleFactor;
|
||||
|
||||
for (int i = 0; i < vertexThumbs.Count && i < poly.Points.Count; i++)
|
||||
{
|
||||
vertexThumbs[i].Arrange(new Rect(
|
||||
poly.Points[i].X - (thumbSize / 2),
|
||||
poly.Points[i].Y - (thumbSize / 2),
|
||||
thumbSize,
|
||||
thumbSize));
|
||||
}
|
||||
}
|
||||
|
||||
return finalSize;
|
||||
}
|
||||
|
||||
protected override int VisualChildrenCount
|
||||
{ get { return visualChildren.Count; } }
|
||||
|
||||
protected override Visual GetVisualChild(int index)
|
||||
{ return visualChildren[index]; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Collections.Specialized;
|
||||
using System.Windows;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Shapes;
|
||||
|
||||
namespace XP.ImageProcessing.RoiControl
|
||||
{
|
||||
/// <summary>
|
||||
/// 多边形Points附加行为,支持ObservableCollection绑定
|
||||
/// </summary>
|
||||
public static class PolygonPointsBehavior
|
||||
{
|
||||
private static readonly Dictionary<Polygon, ObservableCollection<Point>> _attachedCollections
|
||||
= new Dictionary<Polygon, ObservableCollection<Point>>();
|
||||
|
||||
public static ObservableCollection<Point> GetPointsSource(DependencyObject obj)
|
||||
{
|
||||
return (ObservableCollection<Point>)obj.GetValue(PointsSourceProperty);
|
||||
}
|
||||
|
||||
public static void SetPointsSource(DependencyObject obj, ObservableCollection<Point> value)
|
||||
{
|
||||
obj.SetValue(PointsSourceProperty, value);
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty PointsSourceProperty =
|
||||
DependencyProperty.RegisterAttached(
|
||||
"PointsSource",
|
||||
typeof(ObservableCollection<Point>),
|
||||
typeof(PolygonPointsBehavior),
|
||||
new PropertyMetadata(null, OnPointsSourceChanged));
|
||||
|
||||
private static void OnPointsSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
if (!(d is Polygon polygon))
|
||||
return;
|
||||
|
||||
// 清理旧的订阅
|
||||
if (e.OldValue is ObservableCollection<Point> oldCollection)
|
||||
{
|
||||
oldCollection.CollectionChanged -= GetCollectionChangedHandler(polygon);
|
||||
_attachedCollections.Remove(polygon);
|
||||
}
|
||||
|
||||
// 设置新的订阅
|
||||
if (e.NewValue is ObservableCollection<Point> newCollection)
|
||||
{
|
||||
// 初始化Points
|
||||
UpdatePolygonPoints(polygon, newCollection);
|
||||
|
||||
// 监听集合变化
|
||||
NotifyCollectionChangedEventHandler handler = (s, args) => UpdatePolygonPoints(polygon, newCollection);
|
||||
newCollection.CollectionChanged += handler;
|
||||
|
||||
// 保存引用以便后续清理
|
||||
_attachedCollections[polygon] = newCollection;
|
||||
|
||||
// 监听Polygon卸载事件以清理资�?
|
||||
polygon.Unloaded += (s, args) =>
|
||||
{
|
||||
if (_attachedCollections.TryGetValue(polygon, out var collection))
|
||||
{
|
||||
collection.CollectionChanged -= handler;
|
||||
_attachedCollections.Remove(polygon);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private static NotifyCollectionChangedEventHandler GetCollectionChangedHandler(Polygon polygon)
|
||||
{
|
||||
return (s, args) =>
|
||||
{
|
||||
if (s is ObservableCollection<Point> collection)
|
||||
{
|
||||
UpdatePolygonPoints(polygon, collection);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static void UpdatePolygonPoints(Polygon polygon, ObservableCollection<Point> points)
|
||||
{
|
||||
// 使用Dispatcher确保在UI线程更新
|
||||
polygon.Dispatcher.BeginInvoke(new Action(() =>
|
||||
{
|
||||
// 创建新的PointCollection以触发UI更新
|
||||
polygon.Points = new PointCollection(points);
|
||||
}), System.Windows.Threading.DispatcherPriority.Render);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Windows;
|
||||
using XP.ImageProcessing.RoiControl.Models;
|
||||
|
||||
namespace XP.ImageProcessing.RoiControl
|
||||
{
|
||||
/// <summary>
|
||||
/// ROI序列化工具类
|
||||
/// </summary>
|
||||
public static class ROISerializer
|
||||
{
|
||||
private static readonly JsonSerializerOptions Options = new JsonSerializerOptions
|
||||
{
|
||||
WriteIndented = true,
|
||||
Converters = { new PointConverter(), new ROIShapeConverter() }
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// 保存ROI列表到JSON文件
|
||||
/// </summary>
|
||||
public static void SaveToFile(IEnumerable<ROIShape> roiList, string filePath)
|
||||
{
|
||||
var json = JsonSerializer.Serialize(roiList, Options);
|
||||
File.WriteAllText(filePath, json);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从JSON文件加载ROI列表
|
||||
/// </summary>
|
||||
public static List<ROIShape> LoadFromFile(string filePath)
|
||||
{
|
||||
var json = File.ReadAllText(filePath);
|
||||
return JsonSerializer.Deserialize<List<ROIShape>>(json, Options) ?? new List<ROIShape>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 序列化ROI列表为JSON字符�?
|
||||
/// </summary>
|
||||
public static string Serialize(IEnumerable<ROIShape> roiList)
|
||||
{
|
||||
return JsonSerializer.Serialize(roiList, Options);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从JSON字符串反序列化ROI列表
|
||||
/// </summary>
|
||||
public static List<ROIShape> Deserialize(string json)
|
||||
{
|
||||
return JsonSerializer.Deserialize<List<ROIShape>>(json, Options) ?? new List<ROIShape>();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Point类型的JSON转换�?
|
||||
/// </summary>
|
||||
public class PointConverter : JsonConverter<Point>
|
||||
{
|
||||
public override Point Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
if (reader.TokenType != JsonTokenType.StartObject)
|
||||
throw new JsonException();
|
||||
|
||||
double x = 0, y = 0;
|
||||
|
||||
while (reader.Read())
|
||||
{
|
||||
if (reader.TokenType == JsonTokenType.EndObject)
|
||||
return new Point(x, y);
|
||||
|
||||
if (reader.TokenType == JsonTokenType.PropertyName)
|
||||
{
|
||||
string? propertyName = reader.GetString();
|
||||
reader.Read();
|
||||
|
||||
switch (propertyName)
|
||||
{
|
||||
case "X":
|
||||
x = reader.GetDouble();
|
||||
break;
|
||||
|
||||
case "Y":
|
||||
y = reader.GetDouble();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw new JsonException();
|
||||
}
|
||||
|
||||
public override void Write(Utf8JsonWriter writer, Point value, JsonSerializerOptions options)
|
||||
{
|
||||
writer.WriteStartObject();
|
||||
writer.WriteNumber("X", value.X);
|
||||
writer.WriteNumber("Y", value.Y);
|
||||
writer.WriteEndObject();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ROIShape多态类型的JSON转换�?
|
||||
/// </summary>
|
||||
public class ROIShapeConverter : JsonConverter<ROIShape>
|
||||
{
|
||||
public override ROIShape? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
using (JsonDocument doc = JsonDocument.ParseValue(ref reader))
|
||||
{
|
||||
var root = doc.RootElement;
|
||||
|
||||
if (!root.TryGetProperty("ROIType", out var typeElement))
|
||||
throw new JsonException("Missing ROIType property");
|
||||
|
||||
var roiType = (ROIType)typeElement.GetInt32();
|
||||
|
||||
return roiType switch
|
||||
{
|
||||
ROIType.Polygon => JsonSerializer.Deserialize<PolygonROI>(root.GetRawText(), options),
|
||||
_ => throw new JsonException($"Unknown ROIType: {roiType}")
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public override void Write(Utf8JsonWriter writer, ROIShape value, JsonSerializerOptions options)
|
||||
{
|
||||
JsonSerializer.Serialize(writer, value, value.GetType(), options);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Windows;
|
||||
using System.Windows.Data;
|
||||
using XP.ImageProcessing.RoiControl.Models;
|
||||
|
||||
namespace XP.ImageProcessing.RoiControl.Converters
|
||||
{
|
||||
public class ROITypeToVisibilityConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
if (value is ROIType roiType && parameter is string targetTypeName)
|
||||
{
|
||||
bool match = roiType.ToString() == targetTypeName;
|
||||
return match ? Visibility.Visible : Visibility.Collapsed;
|
||||
}
|
||||
return Visibility.Collapsed;
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
<ResourceDictionary
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:local="clr-namespace:XP.ImageProcessing.RoiControl.Controls">
|
||||
|
||||
<!-- ControlThumb样式 - 14*14灰色矩形 -->
|
||||
<Style x:Key="AreaControlThumbStyle" TargetType="{x:Type Thumb}">
|
||||
<Setter Property="Width" Value="14" />
|
||||
<Setter Property="Height" Value="14" />
|
||||
<Setter Property="Cursor" Value="Hand" />
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="{x:Type Thumb}">
|
||||
<Grid>
|
||||
<!-- 灰色矩形 -->
|
||||
<Rectangle x:Name="ThumbRect"
|
||||
Fill="#FF808080"
|
||||
Stroke="White"
|
||||
StrokeThickness="1"
|
||||
Width="14"
|
||||
Height="14" />
|
||||
</Grid>
|
||||
<ControlTemplate.Triggers>
|
||||
<!-- 鼠标悬停效果 -->
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter TargetName="ThumbRect" Property="Fill" Value="#FF999999" />
|
||||
<Setter TargetName="ThumbRect" Property="StrokeThickness" Value="2" />
|
||||
</Trigger>
|
||||
<!-- 拖拽时效果 -->
|
||||
<Trigger Property="IsDragging" Value="True">
|
||||
<Setter TargetName="ThumbRect" Property="Fill" Value="#FFAAAAAA" />
|
||||
<Setter TargetName="ThumbRect" Property="StrokeThickness" Value="2" />
|
||||
<Setter Property="Cursor" Value="SizeAll" />
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<!-- ImageROICanvas控件模板 -->
|
||||
<Style TargetType="{x:Type local:ImageROICanvas}">
|
||||
<Setter Property="Background" Value="LightGray" />
|
||||
<Setter Property="ClipToBounds" Value="True" />
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="{x:Type local:ImageROICanvas}">
|
||||
<Border Background="{TemplateBinding Background}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
ClipToBounds="True">
|
||||
<Grid>
|
||||
<ScrollViewer HorizontalScrollBarVisibility="Auto"
|
||||
VerticalScrollBarVisibility="Auto"
|
||||
x:Name="PART_ScrollViewer">
|
||||
<Grid>
|
||||
<Grid.RenderTransform>
|
||||
<TransformGroup>
|
||||
<ScaleTransform ScaleX="{TemplateBinding ZoomScale}"
|
||||
ScaleY="{TemplateBinding ZoomScale}"
|
||||
CenterX="{Binding ZoomCenter.X, RelativeSource={RelativeSource TemplatedParent}}"
|
||||
CenterY="{Binding ZoomCenter.Y, RelativeSource={RelativeSource TemplatedParent}}" />
|
||||
<TranslateTransform X="{TemplateBinding PanningOffsetX}"
|
||||
Y="{TemplateBinding PanningOffsetY}" />
|
||||
</TransformGroup>
|
||||
</Grid.RenderTransform>
|
||||
|
||||
<AdornerDecorator x:Name="PART_Adorner">
|
||||
<Canvas x:Name="PART_Canvas"
|
||||
Width="{TemplateBinding ImageWidth}"
|
||||
Height="{TemplateBinding ImageHeight}"
|
||||
Background="White">
|
||||
|
||||
<!-- 图像显示 -->
|
||||
<Image Source="{TemplateBinding ImageSource}"
|
||||
Width="{TemplateBinding ImageWidth}"
|
||||
Height="{TemplateBinding ImageHeight}"
|
||||
Stretch="Fill" />
|
||||
|
||||
<!-- ROI项目容器 - 只支持多边形 -->
|
||||
<ItemsControl ItemsSource="{TemplateBinding ROIItems}">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<Canvas />
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<!-- 多边形ROI -->
|
||||
<Polygon Points="{Binding Points}"
|
||||
Stroke="{Binding Color}"
|
||||
StrokeThickness="1"
|
||||
Fill="Transparent" />
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</Canvas>
|
||||
</AdornerDecorator>
|
||||
</Grid>
|
||||
</ScrollViewer>
|
||||
</Grid>
|
||||
</Border>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
</ResourceDictionary>
|
||||
@@ -0,0 +1,22 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0-windows</TargetFramework>
|
||||
<UseWPF>true</UseWPF>
|
||||
<Nullable>enable</Nullable>
|
||||
<RootNamespace>XP.ImageProcessing.RoiControl</RootNamespace>
|
||||
<AssemblyName>XP.ImageProcessing.RoiControl</AssemblyName>
|
||||
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
|
||||
<PackageId>XP.ImageProcessing.RoiControl</PackageId>
|
||||
<Version>1.0.0</Version>
|
||||
<Authors>Your Name</Authors>
|
||||
<Company>Your Company</Company>
|
||||
<Description>WPF图像ROI编辑控件库,支持矩形、椭圆、多边形ROI的创建、编辑、缩放和平移功能</Description>
|
||||
<PackageTags>WPF;ROI;Image;Polygon;Rectangle;Ellipse;Zoom;Pan</PackageTags>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Xaml.Behaviors.Wpf" Version="1.1.39" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
Reference in New Issue
Block a user