修复流程图编辑器界面及初步的功能

This commit is contained in:
zhengxuan.zhang
2026-04-20 11:07:00 +08:00
parent b16d592087
commit 1c6c2ac675
19 changed files with 363 additions and 32 deletions
@@ -1,4 +1,7 @@
using Moq; using Moq;
using System;
using System.IO;
using System.Windows.Media.Imaging;
using XP.Common.Logging.Interfaces; using XP.Common.Logging.Interfaces;
using XplorePlane.Models; using XplorePlane.Models;
using XplorePlane.Services; using XplorePlane.Services;
@@ -104,6 +107,36 @@ namespace XplorePlane.Tests.Pipeline
Assert.Equal(i, vm.PipelineNodes[i].Order); Assert.Equal(i, vm.PipelineNodes[i].Order);
} }
[Fact]
public void LoadImageFromFile_SetsSourceImage()
{
var vm = CreateVm();
var tempPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid() + ".png");
try
{
var bitmap = TestHelpers.CreateTestBitmap(8, 8);
var encoder = new PngBitmapEncoder();
encoder.Frames.Add(BitmapFrame.Create(bitmap));
using (var stream = File.Create(tempPath))
{
encoder.Save(stream);
}
vm.LoadImageFromFile(tempPath);
Assert.NotNull(vm.SourceImage);
Assert.NotNull(vm.PreviewImage);
Assert.Contains(Path.GetFileName(tempPath), vm.StatusMessage);
}
finally
{
if (File.Exists(tempPath))
File.Delete(tempPath);
}
}
// ── 6.2 RemoveOperatorCommand ───────────────────────────────── // ── 6.2 RemoveOperatorCommand ─────────────────────────────────
[Fact] [Fact]
@@ -0,0 +1,28 @@
using Moq;
using XP.Common.Logging.Interfaces;
using XplorePlane.Services;
using Xunit;
namespace XplorePlane.Tests.Services
{
public class ImageProcessingServiceTests
{
[Fact]
public void DiscoverProcessors_LoadsKnownProcessors()
{
var logger = new Mock<ILoggerService>();
logger.Setup(l => l.ForModule<ImageProcessingService>()).Returns(logger.Object);
using var service = new ImageProcessingService(logger.Object);
var processors = service.GetAvailableProcessors();
Assert.Contains("GaussianBlur", processors);
Assert.Contains("ShockFilter", processors);
Assert.Contains("BandPassFilter", processors);
Assert.Contains("Division", processors);
Assert.Contains("Contour", processors);
Assert.True(processors.Count >= 20, $"Expected many discovered processors, got {processors.Count}.");
}
}
}
-1
View File
@@ -268,7 +268,6 @@ namespace XplorePlane
// 注册视图和视图模型 // 注册视图和视图模型
containerRegistry.RegisterForNavigation<MainWindow>(); containerRegistry.RegisterForNavigation<MainWindow>();
containerRegistry.RegisterForNavigation<MainWindowB>();
containerRegistry.Register<MainViewModel>(); containerRegistry.Register<MainViewModel>();
containerRegistry.RegisterSingleton<NavigationPropertyPanelViewModel>(); containerRegistry.RegisterSingleton<NavigationPropertyPanelViewModel>();
+3
View File
@@ -0,0 +1,3 @@
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("XplorePlane.Tests")]
@@ -0,0 +1,161 @@
using System;
namespace XplorePlane.Services
{
internal static class ProcessorUiMetadata
{
private static readonly (string Category, string CategoryIcon, int Order)[] CategoryDefinitions =
{
("滤波与平滑", "🌀", 0),
("图像增强", "✨", 1),
("图像变换", "🔁", 2),
("数学运算", "➗", 3),
("形态学处理", "⬚", 4),
("边缘检测", "📐", 5),
("检测分析", "🔎", 6),
("其他", "⚙", 99),
};
internal static (string Category, string CategoryIcon, string OperatorIcon) Get(string operatorKey)
{
var category = GetCategory(operatorKey);
return (category, GetCategoryIcon(category), GetOperatorIcon(operatorKey, category));
}
internal static string GetCategory(string operatorKey)
{
if (string.IsNullOrWhiteSpace(operatorKey))
return "其他";
if (ContainsAny(operatorKey, "Blur", "Filter", "Shock"))
return "滤波与平滑";
if (ContainsAny(operatorKey, "Contrast", "Gamma", "Retinex", "Histogram", "Sharpen", "Layer",
"SubPixel", "SuperResolution", "HDR", "Effect", "PseudoColor", "Color"))
return "图像增强";
if (ContainsAny(operatorKey, "Mirror", "Rotate", "Grayscale", "Threshold"))
return "图像变换";
if (ContainsAny(operatorKey, "Division", "Multiplication", "Difference", "Integral", "Or"))
return "数学运算";
if (ContainsAny(operatorKey, "Morphology"))
return "形态学处理";
if (ContainsAny(operatorKey, "Edge"))
return "边缘检测";
if (ContainsAny(operatorKey, "Measurement", "Detection", "Contour", "FillRate", "Void", "Line", "PointToLine", "Ellipse", "Bga"))
return "检测分析";
return "其他";
}
internal static int GetCategoryOrder(string category)
{
foreach (var definition in CategoryDefinitions)
{
if (string.Equals(definition.Category, category, StringComparison.Ordinal))
return definition.Order;
}
return 99;
}
internal static string GetCategoryIcon(string category)
{
foreach (var definition in CategoryDefinitions)
{
if (string.Equals(definition.Category, category, StringComparison.Ordinal))
return definition.CategoryIcon;
}
return "⚙";
}
internal static string GetOperatorIcon(string operatorKey) => GetOperatorIcon(operatorKey, GetCategory(operatorKey));
private static string GetOperatorIcon(string operatorKey, string category)
{
if (string.IsNullOrWhiteSpace(operatorKey))
return GetCategoryIcon(category);
if (ContainsAny(operatorKey, "Shock"))
return "⚡";
if (ContainsAny(operatorKey, "BandPass"))
return "📶";
if (ContainsAny(operatorKey, "GaussianBlur", "MeanFilter", "MedianFilter", "BilateralFilter", "LowPassFilter", "HighPassFilter"))
return "🌀";
if (ContainsAny(operatorKey, "Contrast"))
return "🌗";
if (ContainsAny(operatorKey, "Gamma"))
return "γ";
if (ContainsAny(operatorKey, "Retinex"))
return "🎛";
if (ContainsAny(operatorKey, "Histogram"))
return "📊";
if (ContainsAny(operatorKey, "Sharpen"))
return "✦";
if (ContainsAny(operatorKey, "SubPixel", "SuperResolution"))
return "🔬";
if (ContainsAny(operatorKey, "HDR"))
return "💡";
if (ContainsAny(operatorKey, "PseudoColor"))
return "🎨";
if (ContainsAny(operatorKey, "FilmEffect"))
return "🎞";
if (ContainsAny(operatorKey, "ColorLayer"))
return "🧪";
if (ContainsAny(operatorKey, "Mirror"))
return "↔";
if (ContainsAny(operatorKey, "Rotate"))
return "⟳";
if (ContainsAny(operatorKey, "Grayscale"))
return "◻";
if (ContainsAny(operatorKey, "Threshold"))
return "▣";
if (ContainsAny(operatorKey, "Division"))
return "➗";
if (ContainsAny(operatorKey, "Multiplication"))
return "✕";
if (ContainsAny(operatorKey, "Difference"))
return "Δ";
if (ContainsAny(operatorKey, "Integral"))
return "∫";
if (ContainsAny(operatorKey, "Or"))
return "";
if (ContainsAny(operatorKey, "Morphology"))
return "⬚";
if (ContainsAny(operatorKey, "Sobel", "Kirsch", "HorizontalEdge"))
return "📐";
if (ContainsAny(operatorKey, "Contour"))
return "✏";
if (ContainsAny(operatorKey, "Measurement"))
return "📏";
if (ContainsAny(operatorKey, "FillRate"))
return "🧮";
if (ContainsAny(operatorKey, "Void"))
return "⚪";
if (ContainsAny(operatorKey, "Ellipse"))
return "⭕";
if (ContainsAny(operatorKey, "PointToLine"))
return "📍";
if (ContainsAny(operatorKey, "Edge"))
return "📐";
return GetCategoryIcon(category);
}
private static bool ContainsAny(string value, params string[] terms)
{
foreach (var term in terms)
{
if (value.IndexOf(term, StringComparison.OrdinalIgnoreCase) >= 0)
return true;
}
return false;
}
}
}
@@ -60,6 +60,7 @@ namespace XplorePlane.ViewModels
SaveAsPipelineCommand = new DelegateCommand(async () => await SaveAsPipelineAsync()); SaveAsPipelineCommand = new DelegateCommand(async () => await SaveAsPipelineAsync());
DeletePipelineCommand = new DelegateCommand(async () => await DeletePipelineAsync()); DeletePipelineCommand = new DelegateCommand(async () => await DeletePipelineAsync());
LoadPipelineCommand = new DelegateCommand(async () => await LoadPipelineAsync()); LoadPipelineCommand = new DelegateCommand(async () => await LoadPipelineAsync());
LoadImageCommand = new DelegateCommand(LoadImage);
OpenToolboxCommand = new DelegateCommand(OpenToolbox); OpenToolboxCommand = new DelegateCommand(OpenToolbox);
MoveNodeUpCommand = new DelegateCommand<PipelineNodeViewModel>(MoveNodeUp); MoveNodeUpCommand = new DelegateCommand<PipelineNodeViewModel>(MoveNodeUp);
MoveNodeDownCommand = new DelegateCommand<PipelineNodeViewModel>(MoveNodeDown); MoveNodeDownCommand = new DelegateCommand<PipelineNodeViewModel>(MoveNodeDown);
@@ -88,6 +89,7 @@ namespace XplorePlane.ViewModels
if (SetProperty(ref _sourceImage, value)) if (SetProperty(ref _sourceImage, value))
{ {
ExecutePipelineCommand.RaiseCanExecuteChanged(); ExecutePipelineCommand.RaiseCanExecuteChanged();
RaisePropertyChanged(nameof(DisplayImage));
TriggerDebouncedExecution(); TriggerDebouncedExecution();
} }
} }
@@ -96,9 +98,15 @@ namespace XplorePlane.ViewModels
public BitmapSource PreviewImage public BitmapSource PreviewImage
{ {
get => _previewImage; get => _previewImage;
set => SetProperty(ref _previewImage, value); set
{
if (SetProperty(ref _previewImage, value))
RaisePropertyChanged(nameof(DisplayImage));
}
} }
public BitmapSource DisplayImage => PreviewImage ?? SourceImage;
public string PipelineName public string PipelineName
{ {
get => _pipelineName; get => _pipelineName;
@@ -142,6 +150,7 @@ namespace XplorePlane.ViewModels
public DelegateCommand SaveAsPipelineCommand { get; } public DelegateCommand SaveAsPipelineCommand { get; }
public DelegateCommand DeletePipelineCommand { get; } public DelegateCommand DeletePipelineCommand { get; }
public DelegateCommand LoadPipelineCommand { get; } public DelegateCommand LoadPipelineCommand { get; }
public DelegateCommand LoadImageCommand { get; }
public DelegateCommand OpenToolboxCommand { get; } public DelegateCommand OpenToolboxCommand { get; }
@@ -316,6 +325,45 @@ namespace XplorePlane.ViewModels
} }
} }
private void LoadImage()
{
var dialog = new OpenFileDialog
{
Title = "加载图像",
Filter = "图像文件|*.bmp;*.png;*.jpg;*.jpeg;*.tif;*.tiff|所有文件|*.*"
};
if (dialog.ShowDialog() != true)
return;
try
{
LoadImageFromFile(dialog.FileName);
}
catch (Exception ex)
{
StatusMessage = $"加载图像失败:{ex.Message}";
_logger.Error(ex, "加载图像失败:{Path}", dialog.FileName);
}
}
internal void LoadImageFromFile(string filePath)
{
if (string.IsNullOrWhiteSpace(filePath))
throw new ArgumentException("图像路径不能为空", nameof(filePath));
var bitmap = new BitmapImage();
bitmap.BeginInit();
bitmap.UriSource = new Uri(filePath, UriKind.Absolute);
bitmap.CacheOption = BitmapCacheOption.OnLoad;
bitmap.EndInit();
bitmap.Freeze();
SourceImage = bitmap;
PreviewImage = bitmap;
StatusMessage = $"已加载图像:{Path.GetFileName(filePath)}";
}
private void CancelExecution() private void CancelExecution()
{ {
_executionCts?.Cancel(); _executionCts?.Cancel();
@@ -3,7 +3,7 @@
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="库版本信息" Title="库版本信息"
Width="850" Width="400"
Height="600" Height="600"
ResizeMode="CanResizeWithGrip" ResizeMode="CanResizeWithGrip"
WindowStartupLocation="CenterOwner"> WindowStartupLocation="CenterOwner">
@@ -4,11 +4,9 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:prism="http://prismlibrary.com/"
xmlns:telerik="http://schemas.telerik.com/2008/xaml/presentation" xmlns:telerik="http://schemas.telerik.com/2008/xaml/presentation"
d:DesignHeight="700" d:DesignHeight="700"
d:DesignWidth="350" d:DesignWidth="350"
prism:ViewModelLocator.AutoWireViewModel="True"
mc:Ignorable="d"> mc:Ignorable="d">
<UserControl.Resources> <UserControl.Resources>
@@ -48,13 +46,14 @@
<Grid.RowDefinitions> <Grid.RowDefinitions>
<!-- Row 0: 工具栏 --> <!-- Row 0: 工具栏 -->
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />
<!-- Row 1: 流水线节点列表 -->
<RowDefinition Height="3*" /> <!-- Row 2: 流水线节点列表 -->
<!-- Row 2: 分隔线 --> <RowDefinition Height="2*" MinHeight="180" />
<!-- Row 3: 分隔线 -->
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />
<!-- Row 3: 参数面板 --> <!-- Row 4: 参数面板 -->
<RowDefinition Height="2*" MinHeight="80" /> <RowDefinition Height="2*" MinHeight="80" />
<!-- Row 4: 状态栏 --> <!-- Row 5: 状态栏 -->
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />
</Grid.RowDefinitions> </Grid.RowDefinitions>
@@ -90,6 +89,12 @@
Content="加载" Content="加载"
Style="{StaticResource ToolbarBtn}" Style="{StaticResource ToolbarBtn}"
ToolTip="加载流水线" /> ToolTip="加载流水线" />
<Button
Width="64"
Command="{Binding LoadImageCommand}"
Content="加载图像"
Style="{StaticResource ToolbarBtn}"
ToolTip="加载输入图像" />
<Button <Button
Command="{Binding ExecutePipelineCommand}" Command="{Binding ExecutePipelineCommand}"
Content="▶" Content="▶"
@@ -109,6 +114,9 @@
</Grid> </Grid>
</Border> </Border>
<!-- 流水线节点列表(拖拽目标) --> <!-- 流水线节点列表(拖拽目标) -->
<ListBox <ListBox
x:Name="PipelineListBox" x:Name="PipelineListBox"
@@ -1,3 +1,4 @@
using System;
using Prism.Ioc; using Prism.Ioc;
using System.Windows; using System.Windows;
using System.Windows.Controls; using System.Windows.Controls;
@@ -22,6 +23,18 @@ namespace XplorePlane.Views
private void OnLoaded(object sender, RoutedEventArgs e) private void OnLoaded(object sender, RoutedEventArgs e)
{ {
if (DataContext == null)
{
try
{
DataContext = ContainerLocator.Current?.Resolve<PipelineEditorViewModel>();
}
catch (Exception ex)
{
_logger?.Error(ex, "PipelineEditorViewModel 解析失败");
}
}
_logger?.Info("PipelineEditorView DataContext 类型={Type}", _logger?.Info("PipelineEditorView DataContext 类型={Type}",
DataContext?.GetType().Name); DataContext?.GetType().Name);
@@ -50,7 +63,7 @@ namespace XplorePlane.Views
if (!e.Data.GetDataPresent(OperatorToolboxView.DragFormat)) if (!e.Data.GetDataPresent(OperatorToolboxView.DragFormat))
{ {
_logger?.Warn("Drop 事件触发但数据中 {Format}", OperatorToolboxView.DragFormat); _logger?.Warn("Drop 事件触发但数据中没有 {Format}", OperatorToolboxView.DragFormat);
return; return;
} }
@@ -69,4 +82,4 @@ namespace XplorePlane.Views
e.Handled = true; e.Handled = true;
} }
} }
} }
@@ -1,19 +1,46 @@
<Window x:Class="XplorePlane.Views.PipelineEditorWindow" <Window x:Class="XplorePlane.Views.PipelineEditorWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:roi="clr-namespace:XP.ImageProcessing.RoiControl.Controls;assembly=XP.ImageProcessing.RoiControl"
xmlns:views="clr-namespace:XplorePlane.Views" xmlns:views="clr-namespace:XplorePlane.Views"
Title="流水线编辑器" Title="流水线编辑器"
Width="700" Height="750" Width="1200"
Height="750"
WindowStartupLocation="CenterOwner" WindowStartupLocation="CenterOwner"
ShowInTaskbar="False"> ShowInTaskbar="False">
<Grid> <Grid Background="#F3F3F3">
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="200" /> <ColumnDefinition Width="240" MinWidth="200" />
<ColumnDefinition Width="5" /> <ColumnDefinition Width="*" MinWidth="400" />
<ColumnDefinition Width="*" /> <ColumnDefinition Width="250" MinWidth="250" />
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<views:OperatorToolboxView Grid.Column="0" />
<GridSplitter Grid.Column="1" Width="5" HorizontalAlignment="Stretch" Background="#E0E0E0" /> <Border Grid.Column="0"
<views:PipelineEditorView Grid.Column="2" /> Margin="8"
Background="White"
BorderBrush="#D0D0D0"
BorderThickness="1"
CornerRadius="4">
<views:OperatorToolboxView />
</Border>
<Border Grid.Column="1"
Margin="8,8,4,8"
Background="White"
BorderBrush="#D0D0D0"
BorderThickness="1"
CornerRadius="4">
<roi:PolygonRoiCanvas ImageSource="{Binding DisplayImage}"
Background="White" />
</Border>
<Border Grid.Column="2"
Margin="4,8,8,8"
Background="White"
BorderBrush="#D0D0D0"
BorderThickness="1"
CornerRadius="4">
<views:PipelineEditorView />
</Border>
</Grid> </Grid>
</Window> </Window>
@@ -1,4 +1,7 @@
using Prism.Ioc;
using System;
using System.Windows; using System.Windows;
using XplorePlane.ViewModels;
namespace XplorePlane.Views namespace XplorePlane.Views
{ {
@@ -7,6 +10,15 @@ namespace XplorePlane.Views
public PipelineEditorWindow() public PipelineEditorWindow()
{ {
InitializeComponent(); InitializeComponent();
try
{
DataContext = ContainerLocator.Current?.Resolve<PipelineEditorViewModel>();
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine(ex);
}
} }
} }
} }
@@ -5,6 +5,7 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:prism="http://prismlibrary.com/" xmlns:prism="http://prismlibrary.com/"
xmlns:roi="clr-namespace:XP.ImageProcessing.RoiControl.Controls;assembly=XP.ImageProcessing.RoiControl"
prism:ViewModelLocator.AutoWireViewModel="True" prism:ViewModelLocator.AutoWireViewModel="True"
d:DesignHeight="400" d:DesignHeight="400"
d:DesignWidth="600" d:DesignWidth="600"
@@ -22,14 +23,10 @@
FontWeight="SemiBold" Foreground="#333333" Text="实时图像" /> FontWeight="SemiBold" Foreground="#333333" Text="实时图像" />
</Border> </Border>
<!-- 图像显示区域,支持滚动 --> <!-- 图像显示区域,支持滚动、缩放和ROI -->
<ScrollViewer Grid.Row="1" HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto"> <roi:PolygonRoiCanvas Grid.Row="1"
<Image Source="{Binding ImageSource}" ImageSource="{Binding ImageSource}"
Stretch="None" Background="White" />
HorizontalAlignment="Center"
VerticalAlignment="Center"
RenderOptions.BitmapScalingMode="NearestNeighbor" />
</ScrollViewer>
<!-- 图像信息栏 --> <!-- 图像信息栏 -->
<Border Grid.Row="2" Background="#F0F0F0" BorderBrush="#DDDDDD" BorderThickness="0,1,0,0"> <Border Grid.Row="2" Background="#F0F0F0" BorderBrush="#DDDDDD" BorderThickness="0,1,0,0">
+2
View File
@@ -14,6 +14,7 @@
<ItemGroup> <ItemGroup>
<Page Remove="MainWindow.xaml" /> <Page Remove="MainWindow.xaml" />
<Page Remove="Views\Main\MainWindowB.xaml" />
</ItemGroup> </ItemGroup>
<!-- NuGet 包引用 --> <!-- NuGet 包引用 -->
@@ -144,6 +145,7 @@
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<Link>Libs\Hardware\zh-TW\%(Filename)%(Extension)</Link> <Link>Libs\Hardware\zh-TW\%(Filename)%(Extension)</Link>
</None> </None>
<Compile Remove="Views\Main\MainWindowB.xaml.cs" />
<Resource Include="XplorerPlane.ico" /> <Resource Include="XplorerPlane.ico" />
+2 -2
View File
@@ -67,8 +67,8 @@ CNC及矩阵功能的设计与评审,包含以下功能: √
2026.4.20 2026.4.20
---------------------- ----------------------
1、图像算子工具箱的图标 √ 1、图像算子工具箱的图标 √
2、最新的图像算子集成到图像工具箱 2、最新的图像算子集成到图像工具箱
3、修复流程图编辑器 3、修复流程图编辑器界面及初步的功能 √
4、主页面加载图像的功能 4、主页面加载图像的功能
5、 5、