规范类名及命名空间名称

This commit is contained in:
李伟
2026-04-13 14:35:37 +08:00
parent c430ec229b
commit ace1c70ddf
217 changed files with 1271 additions and 1384 deletions
@@ -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>