#0020 浮动图像处理工具箱调研
This commit is contained in:
@@ -65,7 +65,7 @@ XplorePlane/
|
||||
- [x] 日志库的引用(通过 XplorePlane.Common.dll)
|
||||
- [x] 按推荐的 DLL 目录结构进行修改
|
||||
- [x] 通过库依赖的方式调用日志功能
|
||||
- [ ] 界面的布局
|
||||
- [x] 界面的布局
|
||||
- [ ] 打通与硬件层的调用流程
|
||||
- [ ] 打通与图像层的调用流程
|
||||
|
||||
|
||||
+27
-1
@@ -1,20 +1,46 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.14.36811.4 d17.14
|
||||
VisualStudioVersion = 17.14.36811.4
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "XplorePlane", "XplorePlane\XplorePlane.csproj", "{07978DB9-4B88-4F42-9054-73992742BD6A}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "XplorePlane.Tests", "XplorePlane.Tests\XplorePlane.Tests.csproj", "{6234B622-8DF2-4A8D-AF93-B17774019555}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Debug|x64 = Debug|x64
|
||||
Debug|x86 = Debug|x86
|
||||
Release|Any CPU = Release|Any CPU
|
||||
Release|x64 = Release|x64
|
||||
Release|x86 = Release|x86
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{07978DB9-4B88-4F42-9054-73992742BD6A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{07978DB9-4B88-4F42-9054-73992742BD6A}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{07978DB9-4B88-4F42-9054-73992742BD6A}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{07978DB9-4B88-4F42-9054-73992742BD6A}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{07978DB9-4B88-4F42-9054-73992742BD6A}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{07978DB9-4B88-4F42-9054-73992742BD6A}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{07978DB9-4B88-4F42-9054-73992742BD6A}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{07978DB9-4B88-4F42-9054-73992742BD6A}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{07978DB9-4B88-4F42-9054-73992742BD6A}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{07978DB9-4B88-4F42-9054-73992742BD6A}.Release|x64.Build.0 = Release|Any CPU
|
||||
{07978DB9-4B88-4F42-9054-73992742BD6A}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{07978DB9-4B88-4F42-9054-73992742BD6A}.Release|x86.Build.0 = Release|Any CPU
|
||||
{6234B622-8DF2-4A8D-AF93-B17774019555}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{6234B622-8DF2-4A8D-AF93-B17774019555}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{6234B622-8DF2-4A8D-AF93-B17774019555}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{6234B622-8DF2-4A8D-AF93-B17774019555}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{6234B622-8DF2-4A8D-AF93-B17774019555}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{6234B622-8DF2-4A8D-AF93-B17774019555}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{6234B622-8DF2-4A8D-AF93-B17774019555}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{6234B622-8DF2-4A8D-AF93-B17774019555}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{6234B622-8DF2-4A8D-AF93-B17774019555}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{6234B622-8DF2-4A8D-AF93-B17774019555}.Release|x64.Build.0 = Release|Any CPU
|
||||
{6234B622-8DF2-4A8D-AF93-B17774019555}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{6234B622-8DF2-4A8D-AF93-B17774019555}.Release|x86.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
||||
@@ -174,6 +174,14 @@ namespace XplorePlane
|
||||
containerRegistry.Register<ImageProcessingViewModel>();
|
||||
containerRegistry.RegisterForNavigation<ImageProcessingPanelView>();
|
||||
|
||||
// 注册流水线服务(单例,共享 IImageProcessingService)
|
||||
containerRegistry.RegisterSingleton<IPipelineExecutionService, PipelineExecutionService>();
|
||||
containerRegistry.RegisterSingleton<IPipelinePersistenceService, PipelinePersistenceService>();
|
||||
|
||||
// 注册流水线 ViewModel(每次解析创建新实例)
|
||||
containerRegistry.Register<PipelineEditorViewModel>();
|
||||
containerRegistry.Register<OperatorToolboxViewModel>();
|
||||
|
||||
Log.Information("依赖注入容器配置完成");
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace XplorePlane.Models
|
||||
{
|
||||
public class PipelineModel
|
||||
{
|
||||
public Guid Id { get; set; } = Guid.NewGuid();
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string DeviceId { get; set; } = string.Empty;
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
|
||||
public List<PipelineNodeModel> Nodes { get; set; } = new();
|
||||
}
|
||||
|
||||
public class PipelineNodeModel
|
||||
{
|
||||
public Guid Id { get; set; } = Guid.NewGuid();
|
||||
public string OperatorKey { get; set; } = string.Empty;
|
||||
public int Order { get; set; }
|
||||
public bool IsEnabled { get; set; } = true;
|
||||
public Dictionary<string, object> Parameters { get; set; } = new();
|
||||
}
|
||||
|
||||
public class PipelineReorderArgs
|
||||
{
|
||||
public int OldIndex { get; set; }
|
||||
public int NewIndex { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Media.Imaging;
|
||||
using XplorePlane.ViewModels;
|
||||
|
||||
namespace XplorePlane.Services
|
||||
{
|
||||
public record PipelineProgress(int CurrentStep, int TotalSteps, string CurrentOperator);
|
||||
|
||||
public interface IPipelineExecutionService
|
||||
{
|
||||
Task<BitmapSource> ExecutePipelineAsync(
|
||||
IEnumerable<PipelineNodeViewModel> nodes,
|
||||
BitmapSource source,
|
||||
IProgress<PipelineProgress> progress = null,
|
||||
CancellationToken cancellationToken = default);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using XplorePlane.Models;
|
||||
|
||||
namespace XplorePlane.Services
|
||||
{
|
||||
public interface IPipelinePersistenceService
|
||||
{
|
||||
Task SaveAsync(PipelineModel pipeline, string filePath);
|
||||
Task<PipelineModel> LoadAsync(string filePath);
|
||||
Task<IReadOnlyList<PipelineModel>> LoadAllAsync(string directory);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
using System;
|
||||
|
||||
namespace XplorePlane.Services
|
||||
{
|
||||
public class PipelineExecutionException : Exception
|
||||
{
|
||||
public int FailedNodeOrder { get; }
|
||||
public string FailedOperatorKey { get; }
|
||||
|
||||
public PipelineExecutionException(string message, int failedNodeOrder, string failedOperatorKey)
|
||||
: base(message)
|
||||
{
|
||||
FailedNodeOrder = failedNodeOrder;
|
||||
FailedOperatorKey = failedOperatorKey;
|
||||
}
|
||||
|
||||
public PipelineExecutionException(string message, int failedNodeOrder, string failedOperatorKey, Exception innerException)
|
||||
: base(message, innerException)
|
||||
{
|
||||
FailedNodeOrder = failedNodeOrder;
|
||||
FailedOperatorKey = failedOperatorKey;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
using XplorePlane.ViewModels;
|
||||
|
||||
namespace XplorePlane.Services
|
||||
{
|
||||
public class PipelineExecutionService : IPipelineExecutionService
|
||||
{
|
||||
private const int PreviewMaxHeight = 1080;
|
||||
private const int UhdThreshold = 3840;
|
||||
|
||||
private readonly IImageProcessingService _imageProcessingService;
|
||||
|
||||
public PipelineExecutionService(IImageProcessingService imageProcessingService)
|
||||
{
|
||||
_imageProcessingService = imageProcessingService ?? throw new ArgumentNullException(nameof(imageProcessingService));
|
||||
}
|
||||
|
||||
public async Task<BitmapSource> ExecutePipelineAsync(
|
||||
IEnumerable<PipelineNodeViewModel> nodes,
|
||||
BitmapSource source,
|
||||
IProgress<PipelineProgress> progress = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (source == null) throw new ArgumentNullException(nameof(source));
|
||||
|
||||
var enabledNodes = (nodes ?? Enumerable.Empty<PipelineNodeViewModel>())
|
||||
.Where(n => n.IsEnabled)
|
||||
.OrderBy(n => n.Order)
|
||||
.ToList();
|
||||
|
||||
if (enabledNodes.Count == 0)
|
||||
return source;
|
||||
|
||||
// 大图像预览缩放
|
||||
var current = ScaleForPreview(source);
|
||||
|
||||
int total = enabledNodes.Count;
|
||||
for (int step = 0; step < total; step++)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var node = enabledNodes[step];
|
||||
var parameters = node.Parameters
|
||||
.Where(p => p.IsValueValid)
|
||||
.ToDictionary(p => p.Name, p => p.Value);
|
||||
|
||||
try
|
||||
{
|
||||
current = await _imageProcessingService.ProcessImageAsync(
|
||||
current, node.OperatorKey, parameters, null, cancellationToken);
|
||||
|
||||
if (current == null)
|
||||
throw new PipelineExecutionException(
|
||||
$"算子 '{node.OperatorKey}' 返回了空图像",
|
||||
node.Order, node.OperatorKey);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (PipelineExecutionException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new PipelineExecutionException(
|
||||
$"算子 '{node.DisplayName}' 执行失败:{ex.Message}",
|
||||
node.Order, node.OperatorKey, ex);
|
||||
}
|
||||
|
||||
progress?.Report(new PipelineProgress(step + 1, total, node.DisplayName));
|
||||
}
|
||||
|
||||
if (!current.IsFrozen)
|
||||
current.Freeze();
|
||||
|
||||
return current;
|
||||
}
|
||||
|
||||
private static BitmapSource ScaleForPreview(BitmapSource source)
|
||||
{
|
||||
if (source.PixelWidth <= UhdThreshold && source.PixelHeight <= UhdThreshold)
|
||||
return source;
|
||||
|
||||
double scale = (double)PreviewMaxHeight / source.PixelHeight;
|
||||
if (source.PixelWidth * scale > UhdThreshold)
|
||||
scale = (double)UhdThreshold / source.PixelWidth;
|
||||
|
||||
var scaled = new TransformedBitmap(source, new ScaleTransform(scale, scale));
|
||||
scaled.Freeze();
|
||||
return scaled;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading.Tasks;
|
||||
using XplorePlane.Models;
|
||||
|
||||
namespace XplorePlane.Services
|
||||
{
|
||||
public class PipelinePersistenceService : IPipelinePersistenceService
|
||||
{
|
||||
private readonly IImageProcessingService _imageProcessingService;
|
||||
private readonly string _baseDirectory;
|
||||
|
||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||
{
|
||||
WriteIndented = true,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
||||
Converters = { new JsonStringEnumConverter() }
|
||||
};
|
||||
|
||||
public PipelinePersistenceService(IImageProcessingService imageProcessingService, string baseDirectory = null)
|
||||
{
|
||||
_imageProcessingService = imageProcessingService ?? throw new ArgumentNullException(nameof(imageProcessingService));
|
||||
_baseDirectory = baseDirectory ?? Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
|
||||
"XplorePlane", "Pipelines");
|
||||
}
|
||||
|
||||
public async Task SaveAsync(PipelineModel pipeline, string filePath)
|
||||
{
|
||||
if (pipeline == null) throw new ArgumentNullException(nameof(pipeline));
|
||||
ValidatePath(filePath);
|
||||
|
||||
pipeline.UpdatedAt = DateTime.UtcNow;
|
||||
var json = JsonSerializer.Serialize(pipeline, JsonOptions);
|
||||
await File.WriteAllTextAsync(filePath, json);
|
||||
}
|
||||
|
||||
public async Task<PipelineModel> LoadAsync(string filePath)
|
||||
{
|
||||
ValidatePath(filePath);
|
||||
|
||||
if (!File.Exists(filePath))
|
||||
throw new FileNotFoundException("流水线文件不存在", filePath);
|
||||
|
||||
var json = await File.ReadAllTextAsync(filePath);
|
||||
var model = JsonSerializer.Deserialize<PipelineModel>(json, JsonOptions)
|
||||
?? throw new InvalidDataException("流水线文件格式无效");
|
||||
|
||||
// 白名单验证:过滤未注册的算子键
|
||||
var available = _imageProcessingService.GetAvailableProcessors().ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||
model.Nodes = model.Nodes
|
||||
.Where(n => available.Contains(n.OperatorKey))
|
||||
.OrderBy(n => n.Order)
|
||||
.ToList();
|
||||
|
||||
// 重新编号确保 Order 连续
|
||||
for (int i = 0; i < model.Nodes.Count; i++)
|
||||
model.Nodes[i].Order = i;
|
||||
|
||||
return model;
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<PipelineModel>> LoadAllAsync(string directory)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(directory))
|
||||
directory = _baseDirectory;
|
||||
|
||||
ValidateDirectory(directory);
|
||||
|
||||
if (!Directory.Exists(directory))
|
||||
return Array.Empty<PipelineModel>();
|
||||
|
||||
var files = Directory.GetFiles(directory, "*.pipeline.json");
|
||||
var results = new List<PipelineModel>();
|
||||
|
||||
foreach (var file in files)
|
||||
{
|
||||
try
|
||||
{
|
||||
var model = await LoadAsync(file);
|
||||
results.Add(model);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 跳过无效文件
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
private void ValidatePath(string filePath)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(filePath))
|
||||
throw new ArgumentException("文件路径不能为空", nameof(filePath));
|
||||
|
||||
if (filePath.Contains(".."))
|
||||
throw new UnauthorizedAccessException($"不允许路径遍历:{filePath}");
|
||||
}
|
||||
|
||||
private void ValidateDirectory(string directory)
|
||||
{
|
||||
if (directory.Contains(".."))
|
||||
throw new UnauthorizedAccessException($"不允许路径遍历:{directory}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,7 @@ namespace XplorePlane.ViewModels
|
||||
public DelegateCommand ClearCommand { get; set; }
|
||||
public DelegateCommand EditPropertiesCommand { get; set; }
|
||||
public DelegateCommand OpenImageProcessingCommand { get; set; }
|
||||
public DelegateCommand OpenPipelineEditorCommand { get; set; }
|
||||
|
||||
public MainViewModel(ILogger logger)
|
||||
{
|
||||
@@ -45,6 +46,13 @@ namespace XplorePlane.ViewModels
|
||||
_logger.Information("图像处理窗口已打开");
|
||||
});
|
||||
|
||||
OpenPipelineEditorCommand = new DelegateCommand(() =>
|
||||
{
|
||||
var window = new Views.PipelineEditorWindow();
|
||||
window.Show();
|
||||
_logger.Information("流水线编辑器窗口已打开");
|
||||
});
|
||||
|
||||
_logger.Information("MainViewModel 已初始化");
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
using System;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using Prism.Mvvm;
|
||||
using XplorePlane.Services;
|
||||
|
||||
namespace XplorePlane.ViewModels
|
||||
{
|
||||
public class OperatorDescriptor
|
||||
{
|
||||
public string Key { get; }
|
||||
public string DisplayName { get; }
|
||||
public string IconPath { get; }
|
||||
public string Category { get; }
|
||||
|
||||
public OperatorDescriptor(string key, string displayName, string iconPath = "", string category = "通用")
|
||||
{
|
||||
Key = key;
|
||||
DisplayName = displayName;
|
||||
IconPath = iconPath;
|
||||
Category = category;
|
||||
}
|
||||
}
|
||||
|
||||
public class OperatorToolboxViewModel : BindableBase
|
||||
{
|
||||
private readonly IImageProcessingService _imageProcessingService;
|
||||
private string _searchText = string.Empty;
|
||||
|
||||
public OperatorToolboxViewModel(IImageProcessingService imageProcessingService)
|
||||
{
|
||||
_imageProcessingService = imageProcessingService ?? throw new ArgumentNullException(nameof(imageProcessingService));
|
||||
AvailableOperators = new ObservableCollection<OperatorDescriptor>();
|
||||
FilteredOperators = new ObservableCollection<OperatorDescriptor>();
|
||||
LoadOperators();
|
||||
}
|
||||
|
||||
public ObservableCollection<OperatorDescriptor> AvailableOperators { get; }
|
||||
public ObservableCollection<OperatorDescriptor> FilteredOperators { get; }
|
||||
|
||||
public string SearchText
|
||||
{
|
||||
get => _searchText;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _searchText, value))
|
||||
ApplyFilter();
|
||||
}
|
||||
}
|
||||
|
||||
private void LoadOperators()
|
||||
{
|
||||
AvailableOperators.Clear();
|
||||
foreach (var key in _imageProcessingService.GetAvailableProcessors())
|
||||
{
|
||||
var displayName = _imageProcessingService.GetProcessorDisplayName(key);
|
||||
AvailableOperators.Add(new OperatorDescriptor(key, displayName ?? key));
|
||||
}
|
||||
ApplyFilter();
|
||||
}
|
||||
|
||||
private void ApplyFilter()
|
||||
{
|
||||
FilteredOperators.Clear();
|
||||
var filtered = string.IsNullOrWhiteSpace(SearchText)
|
||||
? AvailableOperators
|
||||
: AvailableOperators.Where(o =>
|
||||
o.DisplayName.Contains(SearchText, StringComparison.OrdinalIgnoreCase));
|
||||
foreach (var op in filtered)
|
||||
FilteredOperators.Add(op);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,403 @@
|
||||
using System;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Media.Imaging;
|
||||
using Microsoft.Win32;
|
||||
using Prism.Commands;
|
||||
using Prism.Mvvm;
|
||||
using XplorePlane.Models;
|
||||
using XplorePlane.Services;
|
||||
|
||||
namespace XplorePlane.ViewModels
|
||||
{
|
||||
public class PipelineEditorViewModel : BindableBase
|
||||
{
|
||||
private const int MaxPipelineLength = 20;
|
||||
private const int DebounceDelayMs = 300;
|
||||
|
||||
private readonly IImageProcessingService _imageProcessingService;
|
||||
private readonly IPipelineExecutionService _executionService;
|
||||
private readonly IPipelinePersistenceService _persistenceService;
|
||||
|
||||
private PipelineNodeViewModel _selectedNode;
|
||||
private BitmapSource _sourceImage;
|
||||
private BitmapSource _previewImage;
|
||||
private string _pipelineName = "新建流水线";
|
||||
private string _selectedDevice = string.Empty;
|
||||
private bool _isExecuting;
|
||||
private string _statusMessage = string.Empty;
|
||||
private string _currentFilePath;
|
||||
|
||||
private CancellationTokenSource _executionCts;
|
||||
private CancellationTokenSource _debounceCts;
|
||||
|
||||
public PipelineEditorViewModel(
|
||||
IImageProcessingService imageProcessingService,
|
||||
IPipelineExecutionService executionService,
|
||||
IPipelinePersistenceService persistenceService)
|
||||
{
|
||||
_imageProcessingService = imageProcessingService ?? throw new ArgumentNullException(nameof(imageProcessingService));
|
||||
_executionService = executionService ?? throw new ArgumentNullException(nameof(executionService));
|
||||
_persistenceService = persistenceService ?? throw new ArgumentNullException(nameof(persistenceService));
|
||||
|
||||
PipelineNodes = new ObservableCollection<PipelineNodeViewModel>();
|
||||
AvailableDevices = new ObservableCollection<string>();
|
||||
|
||||
AddOperatorCommand = new DelegateCommand<string>(AddOperator, CanAddOperator);
|
||||
RemoveOperatorCommand = new DelegateCommand<PipelineNodeViewModel>(RemoveOperator);
|
||||
ReorderOperatorCommand = new DelegateCommand<PipelineReorderArgs>(ReorderOperator);
|
||||
ExecutePipelineCommand = new DelegateCommand(async () => await ExecutePipelineAsync(), () => !IsExecuting && SourceImage != null);
|
||||
CancelExecutionCommand = new DelegateCommand(CancelExecution, () => IsExecuting);
|
||||
NewPipelineCommand = new DelegateCommand(NewPipeline);
|
||||
SavePipelineCommand = new DelegateCommand(async () => await SavePipelineAsync());
|
||||
SaveAsPipelineCommand = new DelegateCommand(async () => await SaveAsPipelineAsync());
|
||||
DeletePipelineCommand = new DelegateCommand(async () => await DeletePipelineAsync());
|
||||
}
|
||||
|
||||
// ── State Properties ──────────────────────────────────────────
|
||||
|
||||
public ObservableCollection<PipelineNodeViewModel> PipelineNodes { get; }
|
||||
public ObservableCollection<string> AvailableDevices { get; }
|
||||
|
||||
public PipelineNodeViewModel SelectedNode
|
||||
{
|
||||
get => _selectedNode;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _selectedNode, value) && value != null)
|
||||
LoadNodeParameters(value);
|
||||
}
|
||||
}
|
||||
|
||||
public BitmapSource SourceImage
|
||||
{
|
||||
get => _sourceImage;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _sourceImage, value))
|
||||
{
|
||||
ExecutePipelineCommand.RaiseCanExecuteChanged();
|
||||
TriggerDebouncedExecution();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public BitmapSource PreviewImage
|
||||
{
|
||||
get => _previewImage;
|
||||
set => SetProperty(ref _previewImage, value);
|
||||
}
|
||||
|
||||
public string PipelineName
|
||||
{
|
||||
get => _pipelineName;
|
||||
set => SetProperty(ref _pipelineName, value);
|
||||
}
|
||||
|
||||
public string SelectedDevice
|
||||
{
|
||||
get => _selectedDevice;
|
||||
set => SetProperty(ref _selectedDevice, value);
|
||||
}
|
||||
|
||||
public bool IsExecuting
|
||||
{
|
||||
get => _isExecuting;
|
||||
private set
|
||||
{
|
||||
if (SetProperty(ref _isExecuting, value))
|
||||
{
|
||||
ExecutePipelineCommand.RaiseCanExecuteChanged();
|
||||
CancelExecutionCommand.RaiseCanExecuteChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public string StatusMessage
|
||||
{
|
||||
get => _statusMessage;
|
||||
set => SetProperty(ref _statusMessage, value);
|
||||
}
|
||||
|
||||
// ── Commands ──────────────────────────────────────────────────
|
||||
|
||||
public DelegateCommand<string> AddOperatorCommand { get; }
|
||||
public DelegateCommand<PipelineNodeViewModel> RemoveOperatorCommand { get; }
|
||||
public DelegateCommand<PipelineReorderArgs> ReorderOperatorCommand { get; }
|
||||
public DelegateCommand ExecutePipelineCommand { get; }
|
||||
public DelegateCommand CancelExecutionCommand { get; }
|
||||
public DelegateCommand NewPipelineCommand { get; }
|
||||
public DelegateCommand SavePipelineCommand { get; }
|
||||
public DelegateCommand SaveAsPipelineCommand { get; }
|
||||
public DelegateCommand DeletePipelineCommand { get; }
|
||||
|
||||
// ── Command Implementations ───────────────────────────────────
|
||||
|
||||
private bool CanAddOperator(string operatorKey) =>
|
||||
!string.IsNullOrWhiteSpace(operatorKey) && PipelineNodes.Count < MaxPipelineLength;
|
||||
|
||||
private void AddOperator(string operatorKey)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(operatorKey))
|
||||
{
|
||||
StatusMessage = "算子键不能为空";
|
||||
return;
|
||||
}
|
||||
|
||||
var available = _imageProcessingService.GetAvailableProcessors();
|
||||
if (!available.Contains(operatorKey))
|
||||
{
|
||||
StatusMessage = $"算子 '{operatorKey}' 未注册";
|
||||
return;
|
||||
}
|
||||
|
||||
if (PipelineNodes.Count >= MaxPipelineLength)
|
||||
{
|
||||
StatusMessage = $"流水线节点数已达上限({MaxPipelineLength})";
|
||||
return;
|
||||
}
|
||||
|
||||
var displayName = _imageProcessingService.GetProcessorDisplayName(operatorKey) ?? operatorKey;
|
||||
var node = new PipelineNodeViewModel(operatorKey, displayName)
|
||||
{
|
||||
Order = PipelineNodes.Count
|
||||
};
|
||||
LoadNodeParameters(node);
|
||||
PipelineNodes.Add(node);
|
||||
StatusMessage = $"已添加算子:{displayName}";
|
||||
TriggerDebouncedExecution();
|
||||
}
|
||||
|
||||
private void RemoveOperator(PipelineNodeViewModel node)
|
||||
{
|
||||
if (node == null || !PipelineNodes.Contains(node)) return;
|
||||
|
||||
PipelineNodes.Remove(node);
|
||||
RenumberNodes();
|
||||
|
||||
if (SelectedNode == node)
|
||||
SelectedNode = null;
|
||||
|
||||
StatusMessage = $"已移除算子:{node.DisplayName}";
|
||||
TriggerDebouncedExecution();
|
||||
}
|
||||
|
||||
private void ReorderOperator(PipelineReorderArgs args)
|
||||
{
|
||||
if (args == null) return;
|
||||
int oldIndex = args.OldIndex;
|
||||
int newIndex = args.NewIndex;
|
||||
|
||||
if (oldIndex < 0 || oldIndex >= PipelineNodes.Count) return;
|
||||
if (newIndex < 0 || newIndex >= PipelineNodes.Count) return;
|
||||
if (oldIndex == newIndex) return;
|
||||
|
||||
var node = PipelineNodes[oldIndex];
|
||||
PipelineNodes.RemoveAt(oldIndex);
|
||||
PipelineNodes.Insert(newIndex, node);
|
||||
RenumberNodes();
|
||||
TriggerDebouncedExecution();
|
||||
}
|
||||
|
||||
private void RenumberNodes()
|
||||
{
|
||||
for (int i = 0; i < PipelineNodes.Count; i++)
|
||||
PipelineNodes[i].Order = i;
|
||||
}
|
||||
|
||||
private void LoadNodeParameters(PipelineNodeViewModel node)
|
||||
{
|
||||
var paramDefs = _imageProcessingService.GetProcessorParameters(node.OperatorKey);
|
||||
if (paramDefs == null) return;
|
||||
|
||||
// 保留已有参数快照
|
||||
var snapshot = node.Parameters.ToDictionary(p => p.Name, p => p.Value);
|
||||
node.Parameters.Clear();
|
||||
|
||||
foreach (var paramDef in paramDefs)
|
||||
{
|
||||
var vm = new ProcessorParameterVM(paramDef);
|
||||
if (snapshot.TryGetValue(paramDef.Name, out var savedValue))
|
||||
vm.Value = savedValue;
|
||||
vm.PropertyChanged += (_, e) =>
|
||||
{
|
||||
if (e.PropertyName == nameof(ProcessorParameterVM.Value))
|
||||
TriggerDebouncedExecution();
|
||||
};
|
||||
node.Parameters.Add(vm);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ExecutePipelineAsync()
|
||||
{
|
||||
if (SourceImage == null || IsExecuting) return;
|
||||
|
||||
_executionCts?.Cancel();
|
||||
_executionCts = new CancellationTokenSource();
|
||||
var token = _executionCts.Token;
|
||||
|
||||
IsExecuting = true;
|
||||
StatusMessage = "正在执行流水线...";
|
||||
|
||||
try
|
||||
{
|
||||
var progress = new Progress<PipelineProgress>(p =>
|
||||
StatusMessage = $"执行中:{p.CurrentOperator} ({p.CurrentStep}/{p.TotalSteps})");
|
||||
|
||||
var result = await _executionService.ExecutePipelineAsync(
|
||||
PipelineNodes, SourceImage, progress, token);
|
||||
|
||||
PreviewImage = result;
|
||||
StatusMessage = "流水线执行完成";
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
StatusMessage = "流水线执行已取消";
|
||||
}
|
||||
catch (PipelineExecutionException ex)
|
||||
{
|
||||
StatusMessage = $"节点 '{ex.FailedOperatorKey}' 执行失败:{ex.Message}";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
StatusMessage = $"执行错误:{ex.Message}";
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsExecuting = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void CancelExecution()
|
||||
{
|
||||
_executionCts?.Cancel();
|
||||
}
|
||||
|
||||
private void TriggerDebouncedExecution()
|
||||
{
|
||||
if (SourceImage == null) return;
|
||||
|
||||
_debounceCts?.Cancel();
|
||||
_debounceCts = new CancellationTokenSource();
|
||||
var token = _debounceCts.Token;
|
||||
|
||||
Task.Delay(DebounceDelayMs, token).ContinueWith(t =>
|
||||
{
|
||||
if (!t.IsCanceled)
|
||||
_ = ExecutePipelineAsync();
|
||||
}, TaskScheduler.FromCurrentSynchronizationContext());
|
||||
}
|
||||
|
||||
private void NewPipeline()
|
||||
{
|
||||
PipelineNodes.Clear();
|
||||
SelectedNode = null;
|
||||
PipelineName = "新建流水线";
|
||||
PreviewImage = null;
|
||||
_currentFilePath = null;
|
||||
StatusMessage = "已新建流水线";
|
||||
}
|
||||
|
||||
private async Task SavePipelineAsync()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(PipelineName) || PipelineName.Length > 100)
|
||||
{
|
||||
StatusMessage = "流水线名称不能为空且长度不超过 100 个字符";
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(_currentFilePath))
|
||||
{
|
||||
await SaveAsPipelineAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
await SaveToFileAsync(_currentFilePath);
|
||||
}
|
||||
|
||||
private async Task SaveAsPipelineAsync()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(PipelineName) || PipelineName.Length > 100)
|
||||
{
|
||||
StatusMessage = "流水线名称不能为空且长度不超过 100 个字符";
|
||||
return;
|
||||
}
|
||||
|
||||
var dialog = new SaveFileDialog
|
||||
{
|
||||
Filter = "流水线文件 (*.pipeline.json)|*.pipeline.json",
|
||||
FileName = PipelineName,
|
||||
InitialDirectory = GetPipelineDirectory()
|
||||
};
|
||||
|
||||
if (dialog.ShowDialog() == true)
|
||||
{
|
||||
_currentFilePath = dialog.FileName;
|
||||
await SaveToFileAsync(_currentFilePath);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SaveToFileAsync(string filePath)
|
||||
{
|
||||
try
|
||||
{
|
||||
var model = BuildPipelineModel();
|
||||
await _persistenceService.SaveAsync(model, filePath);
|
||||
StatusMessage = $"流水线已保存:{Path.GetFileName(filePath)}";
|
||||
}
|
||||
catch (IOException ex)
|
||||
{
|
||||
StatusMessage = $"保存失败:{ex.Message}";
|
||||
}
|
||||
}
|
||||
|
||||
private async Task DeletePipelineAsync()
|
||||
{
|
||||
if (string.IsNullOrEmpty(_currentFilePath)) return;
|
||||
|
||||
try
|
||||
{
|
||||
if (File.Exists(_currentFilePath))
|
||||
File.Delete(_currentFilePath);
|
||||
|
||||
NewPipeline();
|
||||
StatusMessage = "流水线已删除";
|
||||
}
|
||||
catch (IOException ex)
|
||||
{
|
||||
StatusMessage = $"删除失败:{ex.Message}";
|
||||
}
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
private PipelineModel BuildPipelineModel()
|
||||
{
|
||||
return new PipelineModel
|
||||
{
|
||||
Name = PipelineName,
|
||||
DeviceId = SelectedDevice,
|
||||
UpdatedAt = DateTime.UtcNow,
|
||||
Nodes = PipelineNodes.Select(n => new PipelineNodeModel
|
||||
{
|
||||
Id = n.Id,
|
||||
OperatorKey = n.OperatorKey,
|
||||
Order = n.Order,
|
||||
IsEnabled = n.IsEnabled,
|
||||
Parameters = n.Parameters.ToDictionary(p => p.Name, p => p.Value)
|
||||
}).ToList()
|
||||
};
|
||||
}
|
||||
|
||||
private static string GetPipelineDirectory()
|
||||
{
|
||||
var dir = Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
|
||||
"XplorePlane", "Pipelines");
|
||||
Directory.CreateDirectory(dir);
|
||||
return dir;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
using System;
|
||||
using System.Collections.ObjectModel;
|
||||
using Prism.Mvvm;
|
||||
|
||||
namespace XplorePlane.ViewModels
|
||||
{
|
||||
public class PipelineNodeViewModel : BindableBase
|
||||
{
|
||||
private string _displayName;
|
||||
private string _iconPath;
|
||||
private int _order;
|
||||
private bool _isSelected;
|
||||
private bool _isEnabled = true;
|
||||
|
||||
public PipelineNodeViewModel(string operatorKey, string displayName, string iconPath = null)
|
||||
{
|
||||
Id = Guid.NewGuid();
|
||||
OperatorKey = operatorKey;
|
||||
_displayName = displayName;
|
||||
_iconPath = iconPath ?? string.Empty;
|
||||
Parameters = new ObservableCollection<ProcessorParameterVM>();
|
||||
}
|
||||
|
||||
public Guid Id { get; }
|
||||
public string OperatorKey { get; }
|
||||
|
||||
public string DisplayName
|
||||
{
|
||||
get => _displayName;
|
||||
set => SetProperty(ref _displayName, value);
|
||||
}
|
||||
|
||||
public string IconPath
|
||||
{
|
||||
get => _iconPath;
|
||||
set => SetProperty(ref _iconPath, value);
|
||||
}
|
||||
|
||||
public int Order
|
||||
{
|
||||
get => _order;
|
||||
set => SetProperty(ref _order, value);
|
||||
}
|
||||
|
||||
public bool IsSelected
|
||||
{
|
||||
get => _isSelected;
|
||||
set => SetProperty(ref _isSelected, value);
|
||||
}
|
||||
|
||||
public bool IsEnabled
|
||||
{
|
||||
get => _isEnabled;
|
||||
set => SetProperty(ref _isEnabled, value);
|
||||
}
|
||||
|
||||
public ObservableCollection<ProcessorParameterVM> Parameters { get; }
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
using System;
|
||||
using ImageProcessing.Core;
|
||||
using Prism.Mvvm;
|
||||
|
||||
@@ -6,6 +7,7 @@ namespace XplorePlane.ViewModels
|
||||
public class ProcessorParameterVM : BindableBase
|
||||
{
|
||||
private object _value;
|
||||
private bool _isValueValid = true;
|
||||
|
||||
public ProcessorParameterVM(ProcessorParameter parameter)
|
||||
{
|
||||
@@ -21,6 +23,7 @@ namespace XplorePlane.ViewModels
|
||||
"boolean" or "bool" => "bool",
|
||||
_ => "enum"
|
||||
};
|
||||
ValidateValue(_value);
|
||||
}
|
||||
|
||||
public string Name { get; }
|
||||
@@ -29,10 +32,40 @@ namespace XplorePlane.ViewModels
|
||||
public object MaxValue { get; }
|
||||
public string ParameterType { get; }
|
||||
|
||||
public bool IsValueValid
|
||||
{
|
||||
get => _isValueValid;
|
||||
private set => SetProperty(ref _isValueValid, value);
|
||||
}
|
||||
|
||||
public object Value
|
||||
{
|
||||
get => _value;
|
||||
set => SetProperty(ref _value, value);
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _value, value))
|
||||
ValidateValue(value);
|
||||
}
|
||||
}
|
||||
|
||||
private void ValidateValue(object value)
|
||||
{
|
||||
if (value == null || MinValue == null || MaxValue == null)
|
||||
{
|
||||
IsValueValid = true;
|
||||
return;
|
||||
}
|
||||
try
|
||||
{
|
||||
double dVal = Convert.ToDouble(value);
|
||||
double dMin = Convert.ToDouble(MinValue);
|
||||
double dMax = Convert.ToDouble(MaxValue);
|
||||
IsValueValid = dVal >= dMin && dVal <= dMax;
|
||||
}
|
||||
catch
|
||||
{
|
||||
IsValueValid = true; // 非数值类型不做范围校验
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1714,6 +1714,10 @@
|
||||
Command="{Binding OpenImageProcessingCommand}"
|
||||
Size="Large"
|
||||
Text="图像处理" />
|
||||
<telerik:RadRibbonButton
|
||||
Command="{Binding OpenPipelineEditorCommand}"
|
||||
Size="Large"
|
||||
Text="流水线编辑器" />
|
||||
</telerik:RadRibbonGroup>
|
||||
</telerik:RadRibbonTab>
|
||||
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
<UserControl x:Class="XplorePlane.Views.OperatorToolboxView"
|
||||
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:telerik="http://schemas.telerik.com/2008/xaml/presentation"
|
||||
mc:Ignorable="d"
|
||||
d:DesignHeight="600" d:DesignWidth="200">
|
||||
|
||||
<UserControl.Resources>
|
||||
<SolidColorBrush x:Key="PanelBg" Color="White"/>
|
||||
<SolidColorBrush x:Key="PanelBorder" Color="#cdcbcb"/>
|
||||
<FontFamily x:Key="CsdFont">Microsoft YaHei UI</FontFamily>
|
||||
</UserControl.Resources>
|
||||
|
||||
<Border Background="{StaticResource PanelBg}"
|
||||
BorderBrush="{StaticResource PanelBorder}"
|
||||
BorderThickness="1" CornerRadius="4">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="*"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- 标题 -->
|
||||
<Border Grid.Row="0" Background="#F0F0F0" BorderBrush="{StaticResource PanelBorder}"
|
||||
BorderThickness="0,0,0,1" Padding="8,6">
|
||||
<TextBlock Text="算子工具箱" FontFamily="{StaticResource CsdFont}"
|
||||
FontWeight="Bold" FontSize="12" Foreground="#1c1c1b"/>
|
||||
</Border>
|
||||
|
||||
<!-- 搜索框 -->
|
||||
<Border Grid.Row="1" Padding="6,4" BorderBrush="{StaticResource PanelBorder}"
|
||||
BorderThickness="0,0,0,1">
|
||||
<TextBox Text="{Binding SearchText, UpdateSourceTrigger=PropertyChanged}"
|
||||
FontFamily="{StaticResource CsdFont}" FontSize="11"
|
||||
Padding="4,2" BorderBrush="#cdcbcb" BorderThickness="1">
|
||||
<TextBox.Style>
|
||||
<Style TargetType="TextBox">
|
||||
<Style.Triggers>
|
||||
<Trigger Property="Text" Value="">
|
||||
<Setter Property="Background">
|
||||
<Setter.Value>
|
||||
<VisualBrush AlignmentX="Left" AlignmentY="Center" Stretch="None">
|
||||
<VisualBrush.Visual>
|
||||
<TextBlock Text="搜索算子..." Foreground="#aaa"
|
||||
FontFamily="Microsoft YaHei UI" FontSize="11"
|
||||
Margin="4,0,0,0"/>
|
||||
</VisualBrush.Visual>
|
||||
</VisualBrush>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</TextBox.Style>
|
||||
</TextBox>
|
||||
</Border>
|
||||
|
||||
<!-- 算子列表(拖拽源) -->
|
||||
<telerik:RadListBox Grid.Row="2"
|
||||
ItemsSource="{Binding FilteredOperators}"
|
||||
BorderThickness="0"
|
||||
Background="Transparent"
|
||||
telerik:RadDragAndDropManager.AllowDrag="True">
|
||||
<telerik:RadListBox.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<StackPanel Orientation="Horizontal" Margin="4,3">
|
||||
<Border Width="24" Height="24" Background="#E8F0FE"
|
||||
CornerRadius="3" Margin="0,0,6,0">
|
||||
<TextBlock Text="⚙" HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center" FontSize="12"/>
|
||||
</Border>
|
||||
<TextBlock Text="{Binding DisplayName}"
|
||||
FontFamily="Microsoft YaHei UI" FontSize="11"
|
||||
VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</telerik:RadListBox.ItemTemplate>
|
||||
</telerik:RadListBox>
|
||||
</Grid>
|
||||
</Border>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,19 @@
|
||||
using System.Windows.Controls;
|
||||
using Prism.Ioc;
|
||||
using XplorePlane.ViewModels;
|
||||
|
||||
namespace XplorePlane.Views
|
||||
{
|
||||
public partial class OperatorToolboxView : UserControl
|
||||
{
|
||||
public OperatorToolboxView()
|
||||
{
|
||||
InitializeComponent();
|
||||
Loaded += (_, _) =>
|
||||
{
|
||||
if (DataContext == null)
|
||||
DataContext = ContainerLocator.Container.Resolve<OperatorToolboxViewModel>();
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
<UserControl x:Class="XplorePlane.Views.PipelineEditorView"
|
||||
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:telerik="http://schemas.telerik.com/2008/xaml/presentation"
|
||||
mc:Ignorable="d"
|
||||
d:DesignHeight="700" d:DesignWidth="400">
|
||||
|
||||
<UserControl.Resources>
|
||||
<SolidColorBrush x:Key="PanelBg" Color="White"/>
|
||||
<SolidColorBrush x:Key="PanelBorder" Color="#cdcbcb"/>
|
||||
<SolidColorBrush x:Key="SeparatorBrush" Color="#E0E0E0"/>
|
||||
<SolidColorBrush x:Key="AccentBlue" Color="#E3F0FF"/>
|
||||
<FontFamily x:Key="CsdFont">Microsoft YaHei UI</FontFamily>
|
||||
|
||||
<!-- 节点项样式 -->
|
||||
<Style x:Key="PipelineNodeItemStyle" TargetType="telerik:RadListBoxItem">
|
||||
<Setter Property="Padding" Value="0"/>
|
||||
<Setter Property="Margin" Value="0"/>
|
||||
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
|
||||
</Style>
|
||||
|
||||
<!-- 工具栏按钮样式 -->
|
||||
<Style x:Key="ToolbarBtn" TargetType="Button">
|
||||
<Setter Property="Width" Value="28"/>
|
||||
<Setter Property="Height" Value="28"/>
|
||||
<Setter Property="Margin" Value="2,0"/>
|
||||
<Setter Property="Background" Value="Transparent"/>
|
||||
<Setter Property="BorderBrush" Value="#cdcbcb"/>
|
||||
<Setter Property="BorderThickness" Value="1"/>
|
||||
<Setter Property="FontFamily" Value="Microsoft YaHei UI"/>
|
||||
<Setter Property="FontSize" Value="11"/>
|
||||
<Setter Property="Cursor" Value="Hand"/>
|
||||
</Style>
|
||||
</UserControl.Resources>
|
||||
|
||||
<Border Background="{StaticResource PanelBg}"
|
||||
BorderBrush="{StaticResource PanelBorder}"
|
||||
BorderThickness="1" CornerRadius="4">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="*"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="180"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- 标题栏:流水线名称 + 设备选择 -->
|
||||
<Border Grid.Row="0" Background="#F0F0F0"
|
||||
BorderBrush="{StaticResource PanelBorder}"
|
||||
BorderThickness="0,0,0,1" Padding="8,5">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="120"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Grid.Column="0" Text="图像处理 ▾"
|
||||
FontFamily="{StaticResource CsdFont}"
|
||||
FontWeight="Bold" FontSize="12"
|
||||
Foreground="#1c1c1b" VerticalAlignment="Center"
|
||||
Margin="0,0,8,0"/>
|
||||
<TextBox Grid.Column="1"
|
||||
Text="{Binding PipelineName, UpdateSourceTrigger=PropertyChanged}"
|
||||
FontFamily="{StaticResource CsdFont}" FontSize="11"
|
||||
BorderThickness="0" Background="Transparent"
|
||||
VerticalAlignment="Center"/>
|
||||
<TextBlock Grid.Column="2" Text="设备:"
|
||||
FontFamily="{StaticResource CsdFont}" FontSize="11"
|
||||
VerticalAlignment="Center" Margin="0,0,4,0"/>
|
||||
<telerik:RadComboBox Grid.Column="3"
|
||||
ItemsSource="{Binding AvailableDevices}"
|
||||
SelectedItem="{Binding SelectedDevice}"
|
||||
FontFamily="{StaticResource CsdFont}" FontSize="11"
|
||||
Height="22"/>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- 流水线节点列表(拖拽目标) -->
|
||||
<telerik:RadListBox Grid.Row="1"
|
||||
x:Name="PipelineListBox"
|
||||
ItemsSource="{Binding PipelineNodes}"
|
||||
SelectedItem="{Binding SelectedNode, Mode=TwoWay}"
|
||||
BorderThickness="0"
|
||||
Background="Transparent"
|
||||
telerik:RadDragAndDropManager.AllowDrop="True"
|
||||
ItemContainerStyle="{StaticResource PipelineNodeItemStyle}">
|
||||
<telerik:RadListBox.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="20"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<!-- 竖线连接 -->
|
||||
<Canvas Grid.Column="0" Width="20">
|
||||
<Line X1="10" Y1="0" X2="10" Y2="40"
|
||||
Stroke="#5B9BD5" StrokeThickness="2"/>
|
||||
<Rectangle Canvas.Left="3" Canvas.Top="12"
|
||||
Width="14" Height="14"
|
||||
Fill="White" Stroke="#5B9BD5" StrokeThickness="1.5"
|
||||
RadiusX="2" RadiusY="2"/>
|
||||
</Canvas>
|
||||
|
||||
<!-- 节点行 -->
|
||||
<Border Grid.Column="1" Padding="6,8"
|
||||
BorderBrush="Transparent" BorderThickness="0">
|
||||
<Border.Style>
|
||||
<Style TargetType="Border">
|
||||
<Setter Property="Background" Value="Transparent"/>
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding IsSelected}" Value="True">
|
||||
<Setter Property="Background" Value="{StaticResource AccentBlue}"/>
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</Border.Style>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<Border Width="28" Height="28" Background="#E8F0FE"
|
||||
CornerRadius="3" Margin="0,0,8,0">
|
||||
<TextBlock Text="⚙" HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center" FontSize="13"/>
|
||||
</Border>
|
||||
<TextBlock Text="{Binding DisplayName}"
|
||||
FontFamily="Microsoft YaHei UI" FontSize="12"
|
||||
VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</telerik:RadListBox.ItemTemplate>
|
||||
</telerik:RadListBox>
|
||||
|
||||
<!-- 分隔线 -->
|
||||
<Rectangle Grid.Row="2" Height="1" Fill="{StaticResource SeparatorBrush}"/>
|
||||
|
||||
<!-- 参数面板 -->
|
||||
<ScrollViewer Grid.Row="3" VerticalScrollBarVisibility="Auto"
|
||||
HorizontalScrollBarVisibility="Disabled">
|
||||
<StackPanel Margin="8,6">
|
||||
<TextBlock Text="参数配置" FontFamily="{StaticResource CsdFont}"
|
||||
FontWeight="Bold" FontSize="11" Foreground="#555"
|
||||
Margin="0,0,0,4"/>
|
||||
<ItemsControl ItemsSource="{Binding SelectedNode.Parameters}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Grid Margin="0,3">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="100"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Grid.Column="0" Text="{Binding DisplayName}"
|
||||
FontFamily="Microsoft YaHei UI" FontSize="11"
|
||||
VerticalAlignment="Center" TextTrimming="CharacterEllipsis"/>
|
||||
<TextBox Grid.Column="1"
|
||||
Text="{Binding Value, UpdateSourceTrigger=PropertyChanged}"
|
||||
FontFamily="Microsoft YaHei UI" FontSize="11"
|
||||
Padding="4,2" BorderBrush="#cdcbcb" BorderThickness="1">
|
||||
<TextBox.Style>
|
||||
<Style TargetType="TextBox">
|
||||
<Setter Property="BorderBrush" Value="#cdcbcb"/>
|
||||
<Setter Property="Background" Value="White"/>
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding IsValueValid}" Value="False">
|
||||
<Setter Property="BorderBrush" Value="Red"/>
|
||||
<Setter Property="Background" Value="#FFF0F0"/>
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</TextBox.Style>
|
||||
</TextBox>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
|
||||
<!-- 工具栏 -->
|
||||
<Border Grid.Row="4" Background="#F5F5F5"
|
||||
BorderBrush="{StaticResource PanelBorder}"
|
||||
BorderThickness="0,1,0,1" Padding="6,4">
|
||||
<Grid>
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Left">
|
||||
<Button Content="新" Style="{StaticResource ToolbarBtn}"
|
||||
Command="{Binding NewPipelineCommand}"
|
||||
ToolTip="新建流水线"/>
|
||||
<Button Content="存" Style="{StaticResource ToolbarBtn}"
|
||||
Command="{Binding SavePipelineCommand}"
|
||||
ToolTip="保存流水线"/>
|
||||
<Button Content="另" Style="{StaticResource ToolbarBtn}"
|
||||
Command="{Binding SaveAsPipelineCommand}"
|
||||
ToolTip="另存为"/>
|
||||
<Button Content="▶" Style="{StaticResource ToolbarBtn}"
|
||||
Command="{Binding ExecutePipelineCommand}"
|
||||
ToolTip="执行流水线"/>
|
||||
<Button Content="■" Style="{StaticResource ToolbarBtn}"
|
||||
Command="{Binding CancelExecutionCommand}"
|
||||
ToolTip="取消执行"/>
|
||||
</StackPanel>
|
||||
<Button Content="🗑" Style="{StaticResource ToolbarBtn}"
|
||||
HorizontalAlignment="Right"
|
||||
Command="{Binding DeletePipelineCommand}"
|
||||
ToolTip="删除流水线"/>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- 状态栏 -->
|
||||
<Border Grid.Row="5" Background="#F5F5F5" Padding="8,3">
|
||||
<TextBlock Text="{Binding StatusMessage}"
|
||||
FontFamily="{StaticResource CsdFont}" FontSize="11"
|
||||
Foreground="#555" TextTrimming="CharacterEllipsis"/>
|
||||
</Border>
|
||||
</Grid>
|
||||
</Border>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,37 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using Prism.Ioc;
|
||||
using Telerik.Windows.Controls.DragDrop;
|
||||
using XplorePlane.Models;
|
||||
using XplorePlane.ViewModels;
|
||||
|
||||
namespace XplorePlane.Views
|
||||
{
|
||||
public partial class PipelineEditorView : UserControl
|
||||
{
|
||||
public PipelineEditorView()
|
||||
{
|
||||
InitializeComponent();
|
||||
Loaded += OnLoaded;
|
||||
}
|
||||
|
||||
private void OnLoaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (DataContext == null)
|
||||
DataContext = ContainerLocator.Container.Resolve<PipelineEditorViewModel>();
|
||||
|
||||
// 配置拖拽目标:从工具箱拖入算子
|
||||
RadDragAndDropManager.AddDropInfoHandler(PipelineListBox, OnOperatorDropped);
|
||||
}
|
||||
|
||||
private void OnOperatorDropped(object sender, DragDropEventArgs e)
|
||||
{
|
||||
if (DataContext is PipelineEditorViewModel vm
|
||||
&& e.Options.Payload is OperatorDescriptor descriptor)
|
||||
{
|
||||
vm.AddOperatorCommand.Execute(descriptor.Key);
|
||||
e.Handled = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<Window x:Class="XplorePlane.Views.PipelineEditorWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:views="clr-namespace:XplorePlane.Views"
|
||||
Title="流水线编辑器"
|
||||
Width="700" Height="750"
|
||||
WindowStartupLocation="CenterOwner"
|
||||
ShowInTaskbar="False">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="200"/>
|
||||
<ColumnDefinition Width="5"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<views:OperatorToolboxView Grid.Column="0"/>
|
||||
<GridSplitter Grid.Column="1" Width="5" HorizontalAlignment="Stretch" Background="#E0E0E0"/>
|
||||
<views:PipelineEditorView Grid.Column="2"/>
|
||||
</Grid>
|
||||
</Window>
|
||||
@@ -0,0 +1,12 @@
|
||||
using System.Windows;
|
||||
|
||||
namespace XplorePlane.Views
|
||||
{
|
||||
public partial class PipelineEditorWindow : Window
|
||||
{
|
||||
public PipelineEditorWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -15,3 +15,5 @@
|
||||
1、主页面的布局与拆分 √
|
||||
2、硬件层射线源的集成 √
|
||||
3、图像层集成,包括复刻一个示例界面,优化界面布局及算子中文 √
|
||||
4、浮动图像处理工具箱调研
|
||||
5、各窗体间数据流的传递,全局数据结构的设计
|
||||
Reference in New Issue
Block a user