diff --git a/XP.ImageProcessing.Core/ImageProcessorBase.cs b/XP.ImageProcessing.Core/ImageProcessorBase.cs index bafd8f0..0ed4c1e 100644 --- a/XP.ImageProcessing.Core/ImageProcessorBase.cs +++ b/XP.ImageProcessing.Core/ImageProcessorBase.cs @@ -15,6 +15,7 @@ using Emgu.CV; using Emgu.CV.Structure; using Emgu.CV.Util; +using System.Globalization; namespace XP.ImageProcessing.Core; @@ -164,11 +165,45 @@ public abstract class ImageProcessorBase /// public T GetParameter(string name) { - if (Parameters.ContainsKey(name)) + if (!Parameters.ContainsKey(name)) + throw new ArgumentException($"参数 {name} 不存在"); + + var parameter = Parameters[name]; + + try { - return (T)Convert.ChangeType(Parameters[name].Value, typeof(T))!; + if (parameter.Value is T typedValue) + return typedValue; + + if (parameter.Value is string textValue) + { + var normalizedText = NormalizeText(textValue); + if (typeof(T) == typeof(string)) + return (T)(object)textValue; + + if (typeof(T) == typeof(int) && int.TryParse(normalizedText, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intValue)) + return (T)(object)intValue; + + if (typeof(T) == typeof(double) && double.TryParse(normalizedText, NumberStyles.Float | NumberStyles.AllowThousands, CultureInfo.InvariantCulture, out var doubleValue)) + return (T)(object)doubleValue; + + if (typeof(T) == typeof(bool) && bool.TryParse(normalizedText, out var boolValue)) + return (T)(object)boolValue; + } + + return (T)Convert.ChangeType(parameter.Value, typeof(T), CultureInfo.InvariantCulture)!; } - throw new ArgumentException($"参数 {name} 不存在"); + catch (Exception ex) + { + throw new ArgumentException( + $"参数 {name} 的值 '{parameter.Value}' 无法转换为 {typeof(T).Name}", + ex); + } + } + + private static string NormalizeText(string value) + { + return value.Trim().TrimEnd('、', ',', ',', '。', '.', ';', ';', ':', ':'); } /// @@ -178,4 +213,4 @@ public abstract class ImageProcessorBase { return Parameters.ContainsKey(name) ? Parameters[name] : null; } -} \ No newline at end of file +} diff --git a/XP.ImageProcessing.Processors/图像增强/SuperResolutionProcessor.cs b/XP.ImageProcessing.Processors/图像增强/SuperResolutionProcessor.cs index 25a2723..b26c2fe 100644 --- a/XP.ImageProcessing.Processors/图像增强/SuperResolutionProcessor.cs +++ b/XP.ImageProcessing.Processors/图像增强/SuperResolutionProcessor.cs @@ -70,7 +70,7 @@ public class SuperResolutionProcessor : ImageProcessorBase public override Image Process(Image inputImage) { string model = GetParameter("Model"); - int scale = int.Parse(GetParameter("Scale")); + int scale = GetParameter("Scale"); // 查找模型文件 string modelPath = FindModelFile(model, scale); @@ -317,4 +317,4 @@ public class SuperResolutionProcessor : ImageProcessorBase _logger.Warning("Model file not found: {Model}_x{Scale}.onnx", model, scale); return string.Empty; } -} \ No newline at end of file +} diff --git a/XplorePlane.Tests/Pipeline/PipelineEditorViewModelTests.cs b/XplorePlane.Tests/Pipeline/PipelineEditorViewModelTests.cs index 19eb3b6..9716bde 100644 --- a/XplorePlane.Tests/Pipeline/PipelineEditorViewModelTests.cs +++ b/XplorePlane.Tests/Pipeline/PipelineEditorViewModelTests.cs @@ -1,4 +1,8 @@ using Moq; +using Prism.Events; +using System; +using System.IO; +using System.Windows.Media.Imaging; using XP.Common.Logging.Interfaces; using XplorePlane.Models; using XplorePlane.Services; @@ -29,7 +33,7 @@ namespace XplorePlane.Tests.Pipeline } private PipelineEditorViewModel CreateVm() => - new PipelineEditorViewModel(_mockImageSvc.Object, _mockExecSvc.Object, _mockPersistSvc.Object, _mockLogger.Object); + new PipelineEditorViewModel(_mockImageSvc.Object, _mockExecSvc.Object, _mockPersistSvc.Object, new EventAggregator(), _mockLogger.Object); // ── 6.1 AddOperatorCommand ──────────────────────────────────── @@ -44,6 +48,18 @@ namespace XplorePlane.Tests.Pipeline Assert.Equal(0, vm.PipelineNodes[0].Order); } + [Fact] + public void AddOperator_KnownKey_SetsIconPath() + { + _mockImageSvc.Setup(s => s.GetAvailableProcessors()).Returns(new[] { "ShockFilter" }); + + var vm = CreateVm(); + vm.AddOperatorCommand.Execute("ShockFilter"); + + Assert.Single(vm.PipelineNodes); + Assert.Equal("⚡", vm.PipelineNodes[0].IconPath); + } + [Fact] public void AddOperator_InvalidKey_NodeNotAdded() { @@ -92,6 +108,36 @@ namespace XplorePlane.Tests.Pipeline Assert.Equal(i, vm.PipelineNodes[i].Order); } + [Fact] + public void LoadImageFromFile_SetsSourceImage() + { + var vm = CreateVm(); + var tempPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid() + ".png"); + + try + { + var bitmap = TestHelpers.CreateTestBitmap(8, 8); + var encoder = new PngBitmapEncoder(); + encoder.Frames.Add(BitmapFrame.Create(bitmap)); + + using (var stream = File.Create(tempPath)) + { + encoder.Save(stream); + } + + vm.LoadImageFromFile(tempPath); + + Assert.NotNull(vm.SourceImage); + Assert.NotNull(vm.PreviewImage); + Assert.Contains(Path.GetFileName(tempPath), vm.StatusMessage); + } + finally + { + if (File.Exists(tempPath)) + File.Delete(tempPath); + } + } + // ── 6.2 RemoveOperatorCommand ───────────────────────────────── [Fact] @@ -276,4 +322,4 @@ namespace XplorePlane.Tests.Pipeline Assert.True(vmMax.IsValueValid); } } -} \ No newline at end of file +} diff --git a/XplorePlane.Tests/Pipeline/PipelinePropertyTests.cs b/XplorePlane.Tests/Pipeline/PipelinePropertyTests.cs index 9bb7b1a..060b14f 100644 --- a/XplorePlane.Tests/Pipeline/PipelinePropertyTests.cs +++ b/XplorePlane.Tests/Pipeline/PipelinePropertyTests.cs @@ -2,6 +2,7 @@ using FsCheck; using FsCheck.Fluent; using FsCheck.Xunit; using Moq; +using Prism.Events; using System; using System.Collections.Generic; using System.IO; @@ -31,7 +32,7 @@ namespace XplorePlane.Tests.Pipeline var mockPersistSvc = new Mock(); var mockLogger = new Mock(); mockLogger.Setup(l => l.ForModule()).Returns(mockLogger.Object); - return new PipelineEditorViewModel(mockImageSvc.Object, mockExecSvc.Object, mockPersistSvc.Object, mockLogger.Object); + return new PipelineEditorViewModel(mockImageSvc.Object, mockExecSvc.Object, mockPersistSvc.Object, new EventAggregator(), mockLogger.Object); } /// @@ -277,4 +278,4 @@ namespace XplorePlane.Tests.Pipeline }); } } -} \ No newline at end of file +} diff --git a/XplorePlane.Tests/Services/ImageProcessingServiceTests.cs b/XplorePlane.Tests/Services/ImageProcessingServiceTests.cs new file mode 100644 index 0000000..6afc85d --- /dev/null +++ b/XplorePlane.Tests/Services/ImageProcessingServiceTests.cs @@ -0,0 +1,28 @@ +using Moq; +using XP.Common.Logging.Interfaces; +using XplorePlane.Services; +using Xunit; + +namespace XplorePlane.Tests.Services +{ + public class ImageProcessingServiceTests + { + [Fact] + public void DiscoverProcessors_LoadsKnownProcessors() + { + var logger = new Mock(); + logger.Setup(l => l.ForModule()).Returns(logger.Object); + + using var service = new ImageProcessingService(logger.Object); + + var processors = service.GetAvailableProcessors(); + + Assert.Contains("GaussianBlur", processors); + Assert.Contains("ShockFilter", processors); + Assert.Contains("BandPassFilter", processors); + Assert.Contains("Division", processors); + Assert.Contains("Contour", processors); + Assert.True(processors.Count >= 20, $"Expected many discovered processors, got {processors.Count}."); + } + } +} diff --git a/XplorePlane/App.xaml.cs b/XplorePlane/App.xaml.cs index d3e02c0..9005fb3 100644 --- a/XplorePlane/App.xaml.cs +++ b/XplorePlane/App.xaml.cs @@ -268,7 +268,6 @@ namespace XplorePlane // 注册视图和视图模型 containerRegistry.RegisterForNavigation(); - containerRegistry.RegisterForNavigation(); containerRegistry.Register(); containerRegistry.RegisterSingleton(); diff --git a/XplorePlane/Events/ManualImageLoadedEvent.cs b/XplorePlane/Events/ManualImageLoadedEvent.cs new file mode 100644 index 0000000..702e708 --- /dev/null +++ b/XplorePlane/Events/ManualImageLoadedEvent.cs @@ -0,0 +1,22 @@ +using Prism.Events; +using System.Windows.Media.Imaging; + +namespace XplorePlane.Events +{ + public sealed class ManualImageLoadedEvent : PubSubEvent + { + } + + public sealed class ManualImageLoadedPayload + { + public ManualImageLoadedPayload(BitmapSource image, string filePath) + { + Image = image; + FilePath = filePath; + } + + public BitmapSource Image { get; } + public string FilePath { get; } + public string FileName => System.IO.Path.GetFileName(FilePath); + } +} diff --git a/XplorePlane/Events/PipelinePreviewUpdatedEvent.cs b/XplorePlane/Events/PipelinePreviewUpdatedEvent.cs new file mode 100644 index 0000000..b4a15d8 --- /dev/null +++ b/XplorePlane/Events/PipelinePreviewUpdatedEvent.cs @@ -0,0 +1,21 @@ +using Prism.Events; +using System.Windows.Media.Imaging; + +namespace XplorePlane.Events +{ + public sealed class PipelinePreviewUpdatedEvent : PubSubEvent + { + } + + public sealed class PipelinePreviewUpdatedPayload + { + public PipelinePreviewUpdatedPayload(BitmapSource image, string statusMessage) + { + Image = image; + StatusMessage = statusMessage; + } + + public BitmapSource Image { get; } + public string StatusMessage { get; } + } +} diff --git a/XplorePlane/Properties/AssemblyInfo.cs b/XplorePlane/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..f2bf64e --- /dev/null +++ b/XplorePlane/Properties/AssemblyInfo.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("XplorePlane.Tests")] diff --git a/XplorePlane/Services/ImageProcessing/ImageProcessingService.cs b/XplorePlane/Services/ImageProcessing/ImageProcessingService.cs index a539c42..7025567 100644 --- a/XplorePlane/Services/ImageProcessing/ImageProcessingService.cs +++ b/XplorePlane/Services/ImageProcessing/ImageProcessingService.cs @@ -1,12 +1,15 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Runtime.Loader; using System.Threading; using System.Threading.Tasks; using System.Windows.Media.Imaging; using XP.Common.Logging.Interfaces; using XP.ImageProcessing.Core; -using XP.ImageProcessing.Processors; namespace XplorePlane.Services { @@ -18,40 +21,180 @@ namespace XplorePlane.Services public ImageProcessingService(ILoggerService logger) { _logger = logger?.ForModule() ?? throw new ArgumentNullException(nameof(logger)); - _processorRegistry = new ConcurrentDictionary(); - RegisterBuiltInProcessors(); + _processorRegistry = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); + DiscoverProcessors(); } - private void RegisterBuiltInProcessors() + private void DiscoverProcessors() { - // 8-bit processors - _processorRegistry["GaussianBlur"] = new GaussianBlurProcessor(); - _processorRegistry["Threshold"] = new ThresholdProcessor(); - _processorRegistry["Division"] = new DivisionProcessor(); - _processorRegistry["Contrast"] = new ContrastProcessor(); - _processorRegistry["Gamma"] = new GammaProcessor(); - _processorRegistry["Morphology"] = new MorphologyProcessor(); - _processorRegistry["Contour"] = new ContourProcessor(); - _processorRegistry["ShockFilter"] = new ShockFilterProcessor(); - _processorRegistry["BandPassFilter"] = new BandPassFilterProcessor(); + var assemblies = LoadCandidateAssemblies().ToList(); + var discoveredCount = 0; - _logger.Info("Registered {Count} built-in image processors", _processorRegistry.Count); + foreach (var assembly in assemblies) + { + foreach (var processorType in GetProcessorTypes(assembly)) + { + if (!TryCreateProcessor(processorType, out var processor)) + continue; + + var key = GetProcessorKey(processorType); + RegisterProcessorInternal(key, processor, discovered: true); + discoveredCount++; + } + } + + _logger.Info( + "Discovered {ProcessorCount} image processors from {AssemblyCount} assemblies", + discoveredCount, + assemblies.Count); } - public IReadOnlyList GetAvailableProcessors() => new List(_processorRegistry.Keys).AsReadOnly(); + private IReadOnlyList LoadCandidateAssemblies() + { + var assemblies = AppDomain.CurrentDomain.GetAssemblies() + .Where(a => !a.IsDynamic) + .ToList(); + + var loadedNames = new HashSet( + assemblies.Select(a => a.GetName().Name ?? string.Empty), + StringComparer.OrdinalIgnoreCase); + + foreach (var path in EnumerateProcessorAssemblyPaths()) + { + var assemblyName = Path.GetFileNameWithoutExtension(path); + if (string.IsNullOrWhiteSpace(assemblyName) || loadedNames.Contains(assemblyName)) + continue; + + try + { + var assembly = AssemblyLoadContext.Default.LoadFromAssemblyPath(Path.GetFullPath(path)); + loadedNames.Add(assembly.GetName().Name ?? assemblyName); + assemblies.Add(assembly); + _logger.Debug("Loaded processor assembly from {Path}", path); + } + catch (Exception ex) + { + _logger.Error(ex, "Failed to load processor assembly from {Path}", path); + } + } + + return assemblies; + } + + private static IEnumerable EnumerateProcessorAssemblyPaths() + { + var baseDirectory = AppContext.BaseDirectory; + var candidateDirectories = new[] + { + baseDirectory, + Path.Combine(baseDirectory, "Processors"), + Path.Combine(baseDirectory, "Plugins"), + Path.Combine(baseDirectory, "Libs", "ImageProcessing") + }; + + var paths = new List(); + + foreach (var directory in candidateDirectories.Distinct(StringComparer.OrdinalIgnoreCase)) + { + if (!Directory.Exists(directory)) + continue; + + if (string.Equals(directory, baseDirectory, StringComparison.OrdinalIgnoreCase)) + { + var processorDll = Path.Combine(directory, "XP.ImageProcessing.Processors.dll"); + if (File.Exists(processorDll)) + paths.Add(processorDll); + } + else + { + paths.AddRange(Directory.GetFiles(directory, "*.dll", SearchOption.TopDirectoryOnly)); + } + } + + return paths.Distinct(StringComparer.OrdinalIgnoreCase); + } + + private static IEnumerable GetProcessorTypes(Assembly assembly) + { + try + { + return assembly + .GetTypes() + .Where(type => type is { IsClass: true, IsAbstract: false } + && typeof(ImageProcessorBase).IsAssignableFrom(type) + && type.GetConstructor(Type.EmptyTypes) != null); + } + catch (ReflectionTypeLoadException ex) + { + return ex.Types + .Where(type => type != null + && type.IsClass + && !type.IsAbstract + && typeof(ImageProcessorBase).IsAssignableFrom(type) + && type.GetConstructor(Type.EmptyTypes) != null)!; + } + } + + private static string GetProcessorKey(Type processorType) + { + const string suffix = "Processor"; + return processorType.Name.EndsWith(suffix, StringComparison.OrdinalIgnoreCase) + ? processorType.Name[..^suffix.Length] + : processorType.Name; + } + + private bool TryCreateProcessor(Type processorType, out ImageProcessorBase processor) + { + try + { + processor = (ImageProcessorBase)Activator.CreateInstance(processorType)!; + return true; + } + catch (Exception ex) + { + processor = null; + _logger.Error(ex, "Failed to create processor instance for {ProcessorType}", processorType.FullName); + return false; + } + } + + private void RegisterProcessorInternal(string name, ImageProcessorBase processor, bool discovered) + { + if (string.IsNullOrWhiteSpace(name)) + throw new ArgumentException("Processor name cannot be empty", nameof(name)); + if (processor == null) + throw new ArgumentNullException(nameof(processor)); + + _processorRegistry.AddOrUpdate( + name, + processor, + (_, _) => processor); + + if (discovered) + _logger.Debug("Discovered processor: {ProcessorName}", name); + else + _logger.Info("Registered processor: {ProcessorName}", name); + } + + public IReadOnlyList GetAvailableProcessors() + { + return _processorRegistry.Keys + .OrderBy(key => ProcessorUiMetadata.GetCategoryOrder(ProcessorUiMetadata.Get(key).Category)) + .ThenBy(key => GetProcessorDisplayName(key), StringComparer.CurrentCultureIgnoreCase) + .ToList() + .AsReadOnly(); + } public void RegisterProcessor(string name, ImageProcessorBase processor) { - if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("Processor name cannot be empty", nameof(name)); - if (processor == null) throw new ArgumentNullException(nameof(processor)); - _processorRegistry[name] = processor; - _logger.Info("Registered processor: {ProcessorName}", name); + RegisterProcessorInternal(name, processor, discovered: false); } public IReadOnlyList GetProcessorParameters(string processorName) { if (_processorRegistry.TryGetValue(processorName, out var processor)) return processor.GetParameters().AsReadOnly(); + throw new ArgumentException($"Processor not registered: {processorName}", nameof(processorName)); } @@ -59,13 +202,15 @@ namespace XplorePlane.Services { if (_processorRegistry.TryGetValue(processorName, out var processor)) return processor; + throw new ArgumentException($"Processor not registered or is 16-bit only: {processorName}", nameof(processorName)); } public string GetProcessorDisplayName(string processorName) { - if (_processorRegistry.TryGetValue(processorName, out var p)) - return string.IsNullOrWhiteSpace(p.Name) ? processorName : p.Name; + if (_processorRegistry.TryGetValue(processorName, out var processor)) + return string.IsNullOrWhiteSpace(processor.Name) ? processorName : processor.Name; + return processorName; } @@ -81,7 +226,6 @@ namespace XplorePlane.Services if (!_processorRegistry.TryGetValue(processorName, out var processor)) throw new ArgumentException($"Processor not registered: {processorName}", nameof(processorName)); - // Extract pixels on the UI thread (BitmapSource / FormatConvertedBitmap are DependencyObjects) var rawPixels = ImageConverter.ExtractGray8Pixels(source, out int imgWidth, out int imgHeight); return await Task.Run(() => @@ -103,7 +247,7 @@ namespace XplorePlane.Services progress?.Report(0.9); var result = ImageConverter.ToBitmapSource(processedEmgu); - result.Freeze(); // must freeze before crossing thread boundary + result.Freeze(); progress?.Report(1.0); return result; @@ -131,7 +275,8 @@ namespace XplorePlane.Services if (processor is IDisposable disposable) disposable.Dispose(); } + _processorRegistry.Clear(); } } -} \ No newline at end of file +} diff --git a/XplorePlane/Services/ImageProcessing/ProcessorUiMetadata.cs b/XplorePlane/Services/ImageProcessing/ProcessorUiMetadata.cs new file mode 100644 index 0000000..69b40c3 --- /dev/null +++ b/XplorePlane/Services/ImageProcessing/ProcessorUiMetadata.cs @@ -0,0 +1,161 @@ +using System; + +namespace XplorePlane.Services +{ + internal static class ProcessorUiMetadata + { + private static readonly (string Category, string CategoryIcon, int Order)[] CategoryDefinitions = + { + ("滤波与平滑", "🌀", 0), + ("图像增强", "✨", 1), + ("图像变换", "🔁", 2), + ("数学运算", "➗", 3), + ("形态学处理", "⬚", 4), + ("边缘检测", "📐", 5), + ("检测分析", "🔎", 6), + ("其他", "⚙", 99), + }; + + internal static (string Category, string CategoryIcon, string OperatorIcon) Get(string operatorKey) + { + var category = GetCategory(operatorKey); + return (category, GetCategoryIcon(category), GetOperatorIcon(operatorKey, category)); + } + + internal static string GetCategory(string operatorKey) + { + if (string.IsNullOrWhiteSpace(operatorKey)) + return "其他"; + + if (ContainsAny(operatorKey, "Blur", "Filter", "Shock")) + return "滤波与平滑"; + + if (ContainsAny(operatorKey, "Contrast", "Gamma", "Retinex", "Histogram", "Sharpen", "Layer", + "SubPixel", "SuperResolution", "HDR", "Effect", "PseudoColor", "Color")) + return "图像增强"; + + if (ContainsAny(operatorKey, "Mirror", "Rotate", "Grayscale", "Threshold")) + return "图像变换"; + + if (ContainsAny(operatorKey, "Division", "Multiplication", "Difference", "Integral", "Or")) + return "数学运算"; + + if (ContainsAny(operatorKey, "Morphology")) + return "形态学处理"; + + if (ContainsAny(operatorKey, "Edge")) + return "边缘检测"; + + if (ContainsAny(operatorKey, "Measurement", "Detection", "Contour", "FillRate", "Void", "Line", "PointToLine", "Ellipse", "Bga")) + return "检测分析"; + + return "其他"; + } + + internal static int GetCategoryOrder(string category) + { + foreach (var definition in CategoryDefinitions) + { + if (string.Equals(definition.Category, category, StringComparison.Ordinal)) + return definition.Order; + } + + return 99; + } + + internal static string GetCategoryIcon(string category) + { + foreach (var definition in CategoryDefinitions) + { + if (string.Equals(definition.Category, category, StringComparison.Ordinal)) + return definition.CategoryIcon; + } + + return "⚙"; + } + + internal static string GetOperatorIcon(string operatorKey) => GetOperatorIcon(operatorKey, GetCategory(operatorKey)); + + private static string GetOperatorIcon(string operatorKey, string category) + { + if (string.IsNullOrWhiteSpace(operatorKey)) + return GetCategoryIcon(category); + + if (ContainsAny(operatorKey, "Shock")) + return "⚡"; + if (ContainsAny(operatorKey, "BandPass")) + return "📶"; + if (ContainsAny(operatorKey, "GaussianBlur", "MeanFilter", "MedianFilter", "BilateralFilter", "LowPassFilter", "HighPassFilter")) + return "🌀"; + if (ContainsAny(operatorKey, "Contrast")) + return "🌗"; + if (ContainsAny(operatorKey, "Gamma")) + return "γ"; + if (ContainsAny(operatorKey, "Retinex")) + return "🎛"; + if (ContainsAny(operatorKey, "Histogram")) + return "📊"; + if (ContainsAny(operatorKey, "Sharpen")) + return "✦"; + if (ContainsAny(operatorKey, "SubPixel", "SuperResolution")) + return "🔬"; + if (ContainsAny(operatorKey, "HDR")) + return "💡"; + if (ContainsAny(operatorKey, "PseudoColor")) + return "🎨"; + if (ContainsAny(operatorKey, "FilmEffect")) + return "🎞"; + if (ContainsAny(operatorKey, "ColorLayer")) + return "🧪"; + if (ContainsAny(operatorKey, "Mirror")) + return "↔"; + if (ContainsAny(operatorKey, "Rotate")) + return "⟳"; + if (ContainsAny(operatorKey, "Grayscale")) + return "◻"; + if (ContainsAny(operatorKey, "Threshold")) + return "▣"; + if (ContainsAny(operatorKey, "Division")) + return "➗"; + if (ContainsAny(operatorKey, "Multiplication")) + return "✕"; + if (ContainsAny(operatorKey, "Difference")) + return "Δ"; + if (ContainsAny(operatorKey, "Integral")) + return "∫"; + if (ContainsAny(operatorKey, "Or")) + return "∨"; + if (ContainsAny(operatorKey, "Morphology")) + return "⬚"; + if (ContainsAny(operatorKey, "Sobel", "Kirsch", "HorizontalEdge")) + return "📐"; + if (ContainsAny(operatorKey, "Contour")) + return "✏"; + if (ContainsAny(operatorKey, "Measurement")) + return "📏"; + if (ContainsAny(operatorKey, "FillRate")) + return "🧮"; + if (ContainsAny(operatorKey, "Void")) + return "⚪"; + if (ContainsAny(operatorKey, "Ellipse")) + return "⭕"; + if (ContainsAny(operatorKey, "PointToLine")) + return "📍"; + if (ContainsAny(operatorKey, "Edge")) + return "📐"; + + return GetCategoryIcon(category); + } + + private static bool ContainsAny(string value, params string[] terms) + { + foreach (var term in terms) + { + if (value.IndexOf(term, StringComparison.OrdinalIgnoreCase) >= 0) + return true; + } + + return false; + } + } +} diff --git a/XplorePlane/Services/Pipeline/PipelineExecutionService.cs b/XplorePlane/Services/Pipeline/PipelineExecutionService.cs index 4173732..6f5af09 100644 --- a/XplorePlane/Services/Pipeline/PipelineExecutionService.cs +++ b/XplorePlane/Services/Pipeline/PipelineExecutionService.cs @@ -46,6 +46,19 @@ namespace XplorePlane.Services cancellationToken.ThrowIfCancellationRequested(); var node = enabledNodes[step]; + var invalidParameters = node.Parameters + .Where(p => !p.IsValueValid) + .Select(p => p.DisplayName) + .ToList(); + + if (invalidParameters.Count > 0) + { + throw new PipelineExecutionException( + $"算子 '{node.DisplayName}' 存在无效参数:{string.Join("、", invalidParameters)}", + node.Order, + node.OperatorKey); + } + var parameters = node.Parameters .Where(p => p.IsValueValid) .ToDictionary(p => p.Name, p => p.Value); @@ -98,4 +111,4 @@ namespace XplorePlane.Services return scaled; } } -} \ No newline at end of file +} diff --git a/XplorePlane/ViewModels/ImageProcessing/OperatorToolboxViewModel.cs b/XplorePlane/ViewModels/ImageProcessing/OperatorToolboxViewModel.cs index b64ee03..2eb128c 100644 --- a/XplorePlane/ViewModels/ImageProcessing/OperatorToolboxViewModel.cs +++ b/XplorePlane/ViewModels/ImageProcessing/OperatorToolboxViewModel.cs @@ -37,21 +37,7 @@ namespace XplorePlane.ViewModels private readonly IImageProcessingService _imageProcessingService; private string _searchText = string.Empty; - // 算子 Key -> (分类名, 分类图标, 算子图标) 映射 - private static readonly Dictionary CategoryMap = new() - { - ["GaussianBlur"] = ("滤波与平滑", "🔵", "🌀"), - ["GaussianBlur16"] = ("滤波与平滑", "🔵", "🌀"), - ["BandPassFilter"] = ("滤波与平滑", "🔵", "📶"), - ["ShockFilter"] = ("滤波与平滑", "🔵", "⚡"), - ["Contrast"] = ("增强与校正", "🟡", "🔆"), - ["Gamma"] = ("增强与校正", "🟡", "🌗"), - ["FlatFieldCorrection16"] = ("增强与校正", "🟡", "📐"), - ["Threshold"] = ("分割与阈值", "🟢", "📊"), - ["Division"] = ("分割与阈值", "🟢", "➗"), - ["Morphology"] = ("形态学与轮廓", "🔴", "🔲"), - ["Contour"] = ("形态学与轮廓", "🔴", "✏️"), - }; + // UI 元数据(分类 + 图标)由 ProcessorUiMetadata 统一提供,保持工具箱与流水线图标一致 public OperatorToolboxViewModel(IImageProcessingService imageProcessingService) { @@ -82,10 +68,8 @@ namespace XplorePlane.ViewModels foreach (var key in _imageProcessingService.GetAvailableProcessors()) { var displayName = _imageProcessingService.GetProcessorDisplayName(key); - var (category, catIcon, opIcon) = CategoryMap.TryGetValue(key, out var info) - ? info - : ("其他", "⚙", "⚙"); - AvailableOperators.Add(new OperatorDescriptor(key, displayName ?? key, opIcon, category, catIcon)); + var (category, categoryIcon, operatorIcon) = ProcessorUiMetadata.Get(key); + AvailableOperators.Add(new OperatorDescriptor(key, displayName ?? key, operatorIcon, category, categoryIcon)); } ApplyFilter(); } @@ -122,10 +106,13 @@ namespace XplorePlane.ViewModels private static int GetCategoryOrder(string category) => category switch { "滤波与平滑" => 0, - "增强与校正" => 1, - "分割与阈值" => 2, - "形态学与轮廓" => 3, + "图像增强" => 1, + "图像变换" => 2, + "数学运算" => 3, + "形态学处理" => 4, + "边缘检测" => 5, + "检测分析" => 6, _ => 99 }; } -} \ No newline at end of file +} diff --git a/XplorePlane/ViewModels/ImageProcessing/PipelineEditorViewModel.cs b/XplorePlane/ViewModels/ImageProcessing/PipelineEditorViewModel.cs index c84bba7..6ba4b19 100644 --- a/XplorePlane/ViewModels/ImageProcessing/PipelineEditorViewModel.cs +++ b/XplorePlane/ViewModels/ImageProcessing/PipelineEditorViewModel.cs @@ -1,4 +1,5 @@ using Microsoft.Win32; +using Prism.Events; using Prism.Commands; using Prism.Mvvm; using System; @@ -9,6 +10,7 @@ using System.Threading; using System.Threading.Tasks; using System.Windows.Media.Imaging; using XP.Common.Logging.Interfaces; +using XplorePlane.Events; using XplorePlane.Models; using XplorePlane.Services; @@ -22,6 +24,7 @@ namespace XplorePlane.ViewModels private readonly IImageProcessingService _imageProcessingService; private readonly IPipelineExecutionService _executionService; private readonly IPipelinePersistenceService _persistenceService; + private readonly IEventAggregator _eventAggregator; private readonly ILoggerService _logger; private PipelineNodeViewModel _selectedNode; @@ -40,11 +43,13 @@ namespace XplorePlane.ViewModels IImageProcessingService imageProcessingService, IPipelineExecutionService executionService, IPipelinePersistenceService persistenceService, + IEventAggregator eventAggregator, ILoggerService logger) { _imageProcessingService = imageProcessingService ?? throw new ArgumentNullException(nameof(imageProcessingService)); _executionService = executionService ?? throw new ArgumentNullException(nameof(executionService)); _persistenceService = persistenceService ?? throw new ArgumentNullException(nameof(persistenceService)); + _eventAggregator = eventAggregator ?? throw new ArgumentNullException(nameof(eventAggregator)); _logger = logger?.ForModule() ?? throw new ArgumentNullException(nameof(logger)); PipelineNodes = new ObservableCollection(); @@ -60,9 +65,13 @@ namespace XplorePlane.ViewModels SaveAsPipelineCommand = new DelegateCommand(async () => await SaveAsPipelineAsync()); DeletePipelineCommand = new DelegateCommand(async () => await DeletePipelineAsync()); LoadPipelineCommand = new DelegateCommand(async () => await LoadPipelineAsync()); + LoadImageCommand = new DelegateCommand(LoadImage); OpenToolboxCommand = new DelegateCommand(OpenToolbox); MoveNodeUpCommand = new DelegateCommand(MoveNodeUp); MoveNodeDownCommand = new DelegateCommand(MoveNodeDown); + + _eventAggregator.GetEvent() + .Subscribe(OnManualImageLoaded); } // ── State Properties ────────────────────────────────────────── @@ -88,6 +97,7 @@ namespace XplorePlane.ViewModels if (SetProperty(ref _sourceImage, value)) { ExecutePipelineCommand.RaiseCanExecuteChanged(); + RaisePropertyChanged(nameof(DisplayImage)); TriggerDebouncedExecution(); } } @@ -96,9 +106,15 @@ namespace XplorePlane.ViewModels public BitmapSource PreviewImage { get => _previewImage; - set => SetProperty(ref _previewImage, value); + set + { + if (SetProperty(ref _previewImage, value)) + RaisePropertyChanged(nameof(DisplayImage)); + } } + public BitmapSource DisplayImage => PreviewImage ?? SourceImage; + public string PipelineName { get => _pipelineName; @@ -142,6 +158,7 @@ namespace XplorePlane.ViewModels public DelegateCommand SaveAsPipelineCommand { get; } public DelegateCommand DeletePipelineCommand { get; } public DelegateCommand LoadPipelineCommand { get; } + public DelegateCommand LoadImageCommand { get; } public DelegateCommand OpenToolboxCommand { get; } @@ -183,7 +200,8 @@ namespace XplorePlane.ViewModels } var displayName = _imageProcessingService.GetProcessorDisplayName(operatorKey) ?? operatorKey; - var node = new PipelineNodeViewModel(operatorKey, displayName) + var icon = ProcessorUiMetadata.GetOperatorIcon(operatorKey); + var node = new PipelineNodeViewModel(operatorKey, displayName, icon) { Order = PipelineNodes.Count }; @@ -296,6 +314,7 @@ namespace XplorePlane.ViewModels PreviewImage = result; StatusMessage = "流水线执行完成"; + PublishPipelinePreviewUpdated(result, StatusMessage); } catch (OperationCanceledException) { @@ -315,6 +334,84 @@ namespace XplorePlane.ViewModels } } + private void LoadImage() + { + var dialog = new OpenFileDialog + { + Title = "加载图像", + Filter = "图像文件|*.bmp;*.png;*.jpg;*.jpeg;*.tif;*.tiff|所有文件|*.*" + }; + + if (dialog.ShowDialog() != true) + return; + + try + { + LoadImageFromFile(dialog.FileName); + } + catch (Exception ex) + { + StatusMessage = $"加载图像失败:{ex.Message}"; + _logger.Error(ex, "加载图像失败:{Path}", dialog.FileName); + } + } + + internal void LoadImageFromFile(string filePath) + { + if (string.IsNullOrWhiteSpace(filePath)) + throw new ArgumentException("图像路径不能为空", nameof(filePath)); + + var bitmap = new BitmapImage(); + bitmap.BeginInit(); + bitmap.UriSource = new Uri(filePath, UriKind.Absolute); + bitmap.CacheOption = BitmapCacheOption.OnLoad; + bitmap.EndInit(); + bitmap.Freeze(); + + SourceImage = bitmap; + PreviewImage = bitmap; + StatusMessage = $"已加载图像:{Path.GetFileName(filePath)}"; + PublishManualImageLoaded(bitmap, filePath); + } + + internal void LoadImageFromBitmap(BitmapSource bitmap, string filePath, bool runPipeline = true) + { + if (bitmap == null) + throw new ArgumentNullException(nameof(bitmap)); + + SourceImage = bitmap; + PreviewImage = bitmap; + StatusMessage = $"已加载图像:{Path.GetFileName(filePath)}"; + PublishManualImageLoaded(bitmap, filePath); + + if (runPipeline) + TriggerDebouncedExecution(); + } + + private void PublishManualImageLoaded(BitmapSource bitmap, string filePath) + { + _eventAggregator.GetEvent() + .Publish(new ManualImageLoadedPayload(bitmap, filePath)); + } + + private void PublishPipelinePreviewUpdated(BitmapSource bitmap, string statusMessage) + { + if (bitmap == null) return; + + _eventAggregator.GetEvent() + .Publish(new PipelinePreviewUpdatedPayload(bitmap, statusMessage)); + } + + private void OnManualImageLoaded(ManualImageLoadedPayload payload) + { + if (payload?.Image == null) return; + if (ReferenceEquals(SourceImage, payload.Image)) return; + + SourceImage = payload.Image; + PreviewImage = payload.Image; + StatusMessage = $"已加载图像:{payload.FileName}"; + } + private void CancelExecution() { _executionCts?.Cancel(); @@ -442,7 +539,8 @@ namespace XplorePlane.ViewModels { var displayName = _imageProcessingService.GetProcessorDisplayName(nodeModel.OperatorKey) ?? nodeModel.OperatorKey; - var node = new PipelineNodeViewModel(nodeModel.OperatorKey, displayName) + var icon = ProcessorUiMetadata.GetOperatorIcon(nodeModel.OperatorKey); + var node = new PipelineNodeViewModel(nodeModel.OperatorKey, displayName, icon) { Order = nodeModel.Order, IsEnabled = nodeModel.IsEnabled @@ -515,4 +613,4 @@ namespace XplorePlane.ViewModels return dir; } } -} \ No newline at end of file +} diff --git a/XplorePlane/ViewModels/ImageProcessing/ProcessorParameterVM.cs b/XplorePlane/ViewModels/ImageProcessing/ProcessorParameterVM.cs index 4f01210..a5119df 100644 --- a/XplorePlane/ViewModels/ImageProcessing/ProcessorParameterVM.cs +++ b/XplorePlane/ViewModels/ImageProcessing/ProcessorParameterVM.cs @@ -1,5 +1,7 @@ -using Prism.Mvvm; +using Prism.Mvvm; using System; +using System.Globalization; +using System.Linq; using XP.ImageProcessing.Core; namespace XplorePlane.ViewModels @@ -16,7 +18,8 @@ namespace XplorePlane.ViewModels _value = parameter.Value; MinValue = parameter.MinValue; MaxValue = parameter.MaxValue; - ParameterType = parameter.ValueType?.Name?.ToLower() switch + Options = parameter.Options; + ParameterType = parameter.ValueType?.Name?.ToLowerInvariant() switch { "int32" or "int" => "int", "double" => "double", @@ -30,6 +33,7 @@ namespace XplorePlane.ViewModels public string DisplayName { get; } public object MinValue { get; } public object MaxValue { get; } + public string[]? Options { get; } public string ParameterType { get; } public bool IsValueValid @@ -43,29 +47,161 @@ namespace XplorePlane.ViewModels get => _value; set { - if (SetProperty(ref _value, value)) - ValidateValue(value); + var normalizedValue = NormalizeValue(value); + if (SetProperty(ref _value, normalizedValue)) + ValidateValue(normalizedValue); } } private void ValidateValue(object value) { - if (value == null || MinValue == null || MaxValue == null) + if (value == null) { - IsValueValid = true; + IsValueValid = false; return; } - try + + if (ParameterType == "int") { - double dVal = Convert.ToDouble(value); - double dMin = Convert.ToDouble(MinValue); - double dMax = Convert.ToDouble(MaxValue); - IsValueValid = dVal >= dMin && dVal <= dMax; + IsValueValid = TryConvertToInt(value, out var intValue) && IsWithinRange(intValue); + return; } - catch + + if (ParameterType == "double") { - IsValueValid = true; // 非数值类型不做范围校? + IsValueValid = TryConvertToDouble(value, out var doubleValue) && IsWithinRange(doubleValue); + return; + } + + if (ParameterType == "bool") + { + IsValueValid = TryConvertToBool(value, out _); + return; + } + + if (Options is { Length: > 0 }) + { + var stringValue = Convert.ToString(value, CultureInfo.InvariantCulture) ?? string.Empty; + IsValueValid = Options.Contains(stringValue, StringComparer.OrdinalIgnoreCase); + return; + } + + IsValueValid = true; + } + + private object NormalizeValue(object value) + { + if (value == null) + return value; + + if (ParameterType == "int" && TryConvertToInt(value, out var intValue)) + return intValue; + + if (ParameterType == "double" && TryConvertToDouble(value, out var doubleValue)) + return doubleValue; + + if (ParameterType == "bool" && TryConvertToBool(value, out var boolValue)) + return boolValue; + + return value; + } + + private bool IsWithinRange(double value) + { + if (MinValue != null && TryConvertToDouble(MinValue, out var minValue) && value < minValue) + return false; + + if (MaxValue != null && TryConvertToDouble(MaxValue, out var maxValue) && value > maxValue) + return false; + + return true; + } + + private static string NormalizeNumericText(string value) + { + return value.Trim().TrimEnd('、', ',', ',', '。', '.', ';', ';', ':', ':'); + } + + private static bool TryConvertToInt(object value, out int result) + { + switch (value) + { + case int intValue: + result = intValue; + return true; + case string stringValue: + stringValue = NormalizeNumericText(stringValue); + return int.TryParse(stringValue, NumberStyles.Integer, CultureInfo.InvariantCulture, out result) + || int.TryParse(stringValue, NumberStyles.Integer, CultureInfo.CurrentCulture, out result); + default: + try + { + result = Convert.ToInt32(value, CultureInfo.InvariantCulture); + return true; + } + catch + { + result = default; + return false; + } + } + } + + private static bool TryConvertToDouble(object value, out double result) + { + switch (value) + { + case double doubleValue: + result = doubleValue; + return true; + case float floatValue: + result = floatValue; + return true; + case int intValue: + result = intValue; + return true; + case long longValue: + result = longValue; + return true; + case string stringValue: + stringValue = NormalizeNumericText(stringValue); + return double.TryParse(stringValue, NumberStyles.Float | NumberStyles.AllowThousands, CultureInfo.InvariantCulture, out result) + || double.TryParse(stringValue, NumberStyles.Float | NumberStyles.AllowThousands, CultureInfo.CurrentCulture, out result); + default: + try + { + result = Convert.ToDouble(value, CultureInfo.InvariantCulture); + return true; + } + catch + { + result = default; + return false; + } + } + } + + private static bool TryConvertToBool(object value, out bool result) + { + switch (value) + { + case bool boolValue: + result = boolValue; + return true; + case string stringValue: + return bool.TryParse(stringValue.Trim(), out result); + default: + try + { + result = Convert.ToBoolean(value, CultureInfo.InvariantCulture); + return true; + } + catch + { + result = default; + return false; + } } } } -} \ No newline at end of file +} diff --git a/XplorePlane/ViewModels/Main/MainViewModel.cs b/XplorePlane/ViewModels/Main/MainViewModel.cs index 8854c19..448552b 100644 --- a/XplorePlane/ViewModels/Main/MainViewModel.cs +++ b/XplorePlane/ViewModels/Main/MainViewModel.cs @@ -1,11 +1,15 @@ using Prism.Commands; +using Prism.Events; using Prism.Ioc; using Prism.Mvvm; +using Microsoft.Win32; using System; using System.Collections.ObjectModel; using System.Configuration; using System.IO; using System.Windows; +using System.Windows.Media.Imaging; +using XplorePlane.Events; using XP.Common.Logging.Interfaces; using XP.Common.PdfViewer.Interfaces; using XP.Hardware.MotionControl.Abstractions; @@ -16,6 +20,7 @@ namespace XplorePlane.ViewModels { private readonly ILoggerService _logger; private readonly IContainerProvider _containerProvider; + private readonly IEventAggregator _eventAggregator; private string _licenseInfo = "当前时间"; public string LicenseInfo @@ -36,6 +41,7 @@ namespace XplorePlane.ViewModels // 窗口打开命令 public DelegateCommand OpenImageProcessingCommand { get; } + public DelegateCommand LoadImageCommand { get; } public DelegateCommand OpenPipelineEditorCommand { get; } public DelegateCommand OpenCncEditorCommand { get; } public DelegateCommand OpenMatrixEditorCommand { get; } @@ -64,10 +70,11 @@ namespace XplorePlane.ViewModels private Window _toolboxWindow; private Window _raySourceConfigWindow; - public MainViewModel(ILoggerService logger, IContainerProvider containerProvider) + public MainViewModel(ILoggerService logger, IContainerProvider containerProvider, IEventAggregator eventAggregator) { _logger = logger?.ForModule() ?? throw new ArgumentNullException(nameof(logger)); _containerProvider = containerProvider ?? throw new ArgumentNullException(nameof(containerProvider)); + _eventAggregator = eventAggregator ?? throw new ArgumentNullException(nameof(eventAggregator)); NavigationTree = new ObservableCollection(); @@ -81,6 +88,7 @@ namespace XplorePlane.ViewModels // 窗口打开命令 OpenImageProcessingCommand = new DelegateCommand(() => ShowWindow(new Views.ImageProcessingWindow(), "图像处理")); + LoadImageCommand = new DelegateCommand(ExecuteLoadImage); OpenPipelineEditorCommand = new DelegateCommand(() => ShowWindow(new Views.PipelineEditorWindow(), "流水线编辑器")); OpenCncEditorCommand = new DelegateCommand(() => ShowWindow(new Views.Cnc.CncEditorWindow(), "CNC 编辑器")); OpenMatrixEditorCommand = new DelegateCommand(() => ShowWindow(new Views.Cnc.MatrixEditorWindow(), "矩阵编排")); @@ -260,6 +268,36 @@ namespace XplorePlane.ViewModels () => new XP.Hardware.RaySource.Views.RaySourceConfigWindow(), "射线源配置"); } + private void ExecuteLoadImage() + { + var dialog = new OpenFileDialog + { + Title = "加载图像", + Filter = "图像文件|*.bmp;*.png;*.jpg;*.jpeg;*.tif;*.tiff|所有文件|*.*" + }; + + if (dialog.ShowDialog() != true) + return; + + try + { + var bitmap = new BitmapImage(); + bitmap.BeginInit(); + bitmap.UriSource = new Uri(dialog.FileName, UriKind.Absolute); + bitmap.CacheOption = BitmapCacheOption.OnLoad; + bitmap.EndInit(); + bitmap.Freeze(); + + _eventAggregator.GetEvent() + .Publish(new ManualImageLoadedPayload(bitmap, dialog.FileName)); + } + catch (Exception ex) + { + _logger.Error(ex, "加载图像失败:{Path}", dialog.FileName); + MessageBox.Show($"加载图像失败:{ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error); + } + } + private void ExecuteWarmUp() { var messageBoxResult = MessageBox.Show("确认执行射线源暖机操作?", "暖机", diff --git a/XplorePlane/ViewModels/Main/ViewportPanelViewModel.cs b/XplorePlane/ViewModels/Main/ViewportPanelViewModel.cs index 9d31dce..599b638 100644 --- a/XplorePlane/ViewModels/Main/ViewportPanelViewModel.cs +++ b/XplorePlane/ViewModels/Main/ViewportPanelViewModel.cs @@ -8,6 +8,7 @@ using System.Windows.Media.Imaging; using XP.Common.Logging.Interfaces; using XP.Hardware.Detector.Abstractions; using XP.Hardware.Detector.Abstractions.Events; +using XplorePlane.Events; namespace XplorePlane.ViewModels { @@ -39,6 +40,10 @@ namespace XplorePlane.ViewModels eventAggregator.GetEvent() .Subscribe(OnImageCaptured, ThreadOption.BackgroundThread); + eventAggregator.GetEvent() + .Subscribe(OnManualImageLoaded, ThreadOption.UIThread); + eventAggregator.GetEvent() + .Subscribe(OnPipelinePreviewUpdated, ThreadOption.UIThread); } private void OnImageCaptured(ImageCapturedEventArgs args) @@ -75,6 +80,22 @@ namespace XplorePlane.ViewModels } } + private void OnManualImageLoaded(ManualImageLoadedPayload payload) + { + if (payload?.Image == null) return; + + ImageSource = payload.Image; + ImageInfo = $"手动加载: {payload.FileName}"; + } + + private void OnPipelinePreviewUpdated(PipelinePreviewUpdatedPayload payload) + { + if (payload?.Image == null) return; + + ImageSource = payload.Image; + ImageInfo = payload.StatusMessage; + } + /// /// 16 位灰度数据线性拉伸为 8 位 BitmapSource(委托给 XP.Common 通用转换器) /// diff --git a/XplorePlane/Views/Main/LibraryVersionsView.xaml b/XplorePlane/Views/Help/LibraryVersionsView.xaml similarity index 100% rename from XplorePlane/Views/Main/LibraryVersionsView.xaml rename to XplorePlane/Views/Help/LibraryVersionsView.xaml diff --git a/XplorePlane/Views/Main/LibraryVersionsView.xaml.cs b/XplorePlane/Views/Help/LibraryVersionsView.xaml.cs similarity index 100% rename from XplorePlane/Views/Main/LibraryVersionsView.xaml.cs rename to XplorePlane/Views/Help/LibraryVersionsView.xaml.cs diff --git a/XplorePlane/Views/Main/LibraryVersionsWindow.xaml b/XplorePlane/Views/Help/LibraryVersionsWindow.xaml similarity index 99% rename from XplorePlane/Views/Main/LibraryVersionsWindow.xaml rename to XplorePlane/Views/Help/LibraryVersionsWindow.xaml index 55347bd..d6abfc1 100644 --- a/XplorePlane/Views/Main/LibraryVersionsWindow.xaml +++ b/XplorePlane/Views/Help/LibraryVersionsWindow.xaml @@ -3,7 +3,7 @@ xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="库版本信息" - Width="850" + Width="400" Height="600" ResizeMode="CanResizeWithGrip" WindowStartupLocation="CenterOwner"> diff --git a/XplorePlane/Views/Main/LibraryVersionsWindow.xaml.cs b/XplorePlane/Views/Help/LibraryVersionsWindow.xaml.cs similarity index 100% rename from XplorePlane/Views/Main/LibraryVersionsWindow.xaml.cs rename to XplorePlane/Views/Help/LibraryVersionsWindow.xaml.cs diff --git a/XplorePlane/Views/ImageProcessing/PipelineEditorView.xaml b/XplorePlane/Views/ImageProcessing/PipelineEditorView.xaml index 4c80928..f9c427c 100644 --- a/XplorePlane/Views/ImageProcessing/PipelineEditorView.xaml +++ b/XplorePlane/Views/ImageProcessing/PipelineEditorView.xaml @@ -4,11 +4,9 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" - xmlns:prism="http://prismlibrary.com/" xmlns:telerik="http://schemas.telerik.com/2008/xaml/presentation" d:DesignHeight="700" d:DesignWidth="350" - prism:ViewModelLocator.AutoWireViewModel="True" mc:Ignorable="d"> @@ -48,13 +46,14 @@ - - - + + + + - + - + @@ -90,6 +89,14 @@ Content="加载" Style="{StaticResource ToolbarBtn}" ToolTip="加载流水线" /> +