#0020 浮动图像处理工具箱调研
This commit is contained in:
@@ -65,7 +65,7 @@ XplorePlane/
|
|||||||
- [x] 日志库的引用(通过 XplorePlane.Common.dll)
|
- [x] 日志库的引用(通过 XplorePlane.Common.dll)
|
||||||
- [x] 按推荐的 DLL 目录结构进行修改
|
- [x] 按推荐的 DLL 目录结构进行修改
|
||||||
- [x] 通过库依赖的方式调用日志功能
|
- [x] 通过库依赖的方式调用日志功能
|
||||||
- [ ] 界面的布局
|
- [x] 界面的布局
|
||||||
- [ ] 打通与硬件层的调用流程
|
- [ ] 打通与硬件层的调用流程
|
||||||
- [ ] 打通与图像层的调用流程
|
- [ ] 打通与图像层的调用流程
|
||||||
|
|
||||||
|
|||||||
+27
-1
@@ -1,20 +1,46 @@
|
|||||||
|
|
||||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
# Visual Studio Version 17
|
# Visual Studio Version 17
|
||||||
VisualStudioVersion = 17.14.36811.4 d17.14
|
VisualStudioVersion = 17.14.36811.4
|
||||||
MinimumVisualStudioVersion = 10.0.40219.1
|
MinimumVisualStudioVersion = 10.0.40219.1
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "XplorePlane", "XplorePlane\XplorePlane.csproj", "{07978DB9-4B88-4F42-9054-73992742BD6A}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "XplorePlane", "XplorePlane\XplorePlane.csproj", "{07978DB9-4B88-4F42-9054-73992742BD6A}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "XplorePlane.Tests", "XplorePlane.Tests\XplorePlane.Tests.csproj", "{6234B622-8DF2-4A8D-AF93-B17774019555}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
|
Debug|x64 = Debug|x64
|
||||||
|
Debug|x86 = Debug|x86
|
||||||
Release|Any CPU = Release|Any CPU
|
Release|Any CPU = Release|Any CPU
|
||||||
|
Release|x64 = Release|x64
|
||||||
|
Release|x86 = Release|x86
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||||
{07978DB9-4B88-4F42-9054-73992742BD6A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{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|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.ActiveCfg = Release|Any CPU
|
||||||
{07978DB9-4B88-4F42-9054-73992742BD6A}.Release|Any CPU.Build.0 = 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
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
|
|||||||
@@ -174,6 +174,14 @@ namespace XplorePlane
|
|||||||
containerRegistry.Register<ImageProcessingViewModel>();
|
containerRegistry.Register<ImageProcessingViewModel>();
|
||||||
containerRegistry.RegisterForNavigation<ImageProcessingPanelView>();
|
containerRegistry.RegisterForNavigation<ImageProcessingPanelView>();
|
||||||
|
|
||||||
|
// 注册流水线服务(单例,共享 IImageProcessingService)
|
||||||
|
containerRegistry.RegisterSingleton<IPipelineExecutionService, PipelineExecutionService>();
|
||||||
|
containerRegistry.RegisterSingleton<IPipelinePersistenceService, PipelinePersistenceService>();
|
||||||
|
|
||||||
|
// 注册流水线 ViewModel(每次解析创建新实例)
|
||||||
|
containerRegistry.Register<PipelineEditorViewModel>();
|
||||||
|
containerRegistry.Register<OperatorToolboxViewModel>();
|
||||||
|
|
||||||
Log.Information("依赖注入容器配置完成");
|
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 ClearCommand { get; set; }
|
||||||
public DelegateCommand EditPropertiesCommand { get; set; }
|
public DelegateCommand EditPropertiesCommand { get; set; }
|
||||||
public DelegateCommand OpenImageProcessingCommand { get; set; }
|
public DelegateCommand OpenImageProcessingCommand { get; set; }
|
||||||
|
public DelegateCommand OpenPipelineEditorCommand { get; set; }
|
||||||
|
|
||||||
public MainViewModel(ILogger logger)
|
public MainViewModel(ILogger logger)
|
||||||
{
|
{
|
||||||
@@ -45,6 +46,13 @@ namespace XplorePlane.ViewModels
|
|||||||
_logger.Information("图像处理窗口已打开");
|
_logger.Information("图像处理窗口已打开");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
OpenPipelineEditorCommand = new DelegateCommand(() =>
|
||||||
|
{
|
||||||
|
var window = new Views.PipelineEditorWindow();
|
||||||
|
window.Show();
|
||||||
|
_logger.Information("流水线编辑器窗口已打开");
|
||||||
|
});
|
||||||
|
|
||||||
_logger.Information("MainViewModel 已初始化");
|
_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 ImageProcessing.Core;
|
||||||
using Prism.Mvvm;
|
using Prism.Mvvm;
|
||||||
|
|
||||||
@@ -6,6 +7,7 @@ namespace XplorePlane.ViewModels
|
|||||||
public class ProcessorParameterVM : BindableBase
|
public class ProcessorParameterVM : BindableBase
|
||||||
{
|
{
|
||||||
private object _value;
|
private object _value;
|
||||||
|
private bool _isValueValid = true;
|
||||||
|
|
||||||
public ProcessorParameterVM(ProcessorParameter parameter)
|
public ProcessorParameterVM(ProcessorParameter parameter)
|
||||||
{
|
{
|
||||||
@@ -21,6 +23,7 @@ namespace XplorePlane.ViewModels
|
|||||||
"boolean" or "bool" => "bool",
|
"boolean" or "bool" => "bool",
|
||||||
_ => "enum"
|
_ => "enum"
|
||||||
};
|
};
|
||||||
|
ValidateValue(_value);
|
||||||
}
|
}
|
||||||
|
|
||||||
public string Name { get; }
|
public string Name { get; }
|
||||||
@@ -29,10 +32,40 @@ namespace XplorePlane.ViewModels
|
|||||||
public object MaxValue { get; }
|
public object MaxValue { get; }
|
||||||
public string ParameterType { get; }
|
public string ParameterType { get; }
|
||||||
|
|
||||||
|
public bool IsValueValid
|
||||||
|
{
|
||||||
|
get => _isValueValid;
|
||||||
|
private set => SetProperty(ref _isValueValid, value);
|
||||||
|
}
|
||||||
|
|
||||||
public object Value
|
public object Value
|
||||||
{
|
{
|
||||||
get => _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}"
|
Command="{Binding OpenImageProcessingCommand}"
|
||||||
Size="Large"
|
Size="Large"
|
||||||
Text="图像处理" />
|
Text="图像处理" />
|
||||||
|
<telerik:RadRibbonButton
|
||||||
|
Command="{Binding OpenPipelineEditorCommand}"
|
||||||
|
Size="Large"
|
||||||
|
Text="流水线编辑器" />
|
||||||
</telerik:RadRibbonGroup>
|
</telerik:RadRibbonGroup>
|
||||||
</telerik:RadRibbonTab>
|
</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、主页面的布局与拆分 √
|
1、主页面的布局与拆分 √
|
||||||
2、硬件层射线源的集成 √
|
2、硬件层射线源的集成 √
|
||||||
3、图像层集成,包括复刻一个示例界面,优化界面布局及算子中文 √
|
3、图像层集成,包括复刻一个示例界面,优化界面布局及算子中文 √
|
||||||
|
4、浮动图像处理工具箱调研
|
||||||
|
5、各窗体间数据流的传递,全局数据结构的设计
|
||||||
Reference in New Issue
Block a user