Files
XplorePlane/XplorePlane/ViewModels/ImageProcessing/ProcessorParameterVM.cs
T
wei.lw.li 3677bb85a4 feat: 新增高级参数折叠面板(IsAdvanced)框架支持
- ProcessorParameter新增IsAdvanced属性
- ProcessorParameterVM导出IsAdvanced供XAML绑定
- PipelineNodeViewModel新增HasAdvancedParameters
- PipelineEditorView增加'高级参数'Expander(默认折叠)
- 修复Expander内FontWeight继承导致的字体样式不一致
2026-08-12 09:24:36 +08:00

459 lines
16 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ── 算子参数 ViewModel ──
// 将 ProcessorParameter 模型包装为可绑定的 ViewModel,
// 支持参数值编辑/校验/重置/格式化和类型转换。
using Prism.Mvvm;
using System;
using System.Globalization;
using System.Linq;
using XP.ImageProcessing.Core;
using XP.ImageProcessing.Processors;
namespace XplorePlane.ViewModels.ImageProcessing
{
/// <summary>算子参数 ViewModel,可绑定参数名/值/范围/类型</summary>
public class ProcessorParameterVM : BindableBase
{
private object _value;
private bool _isValueValid = true;
public ProcessorParameterVM(ProcessorParameter parameter)
{
Name = parameter.Name;
DisplayName = parameter.DisplayName;
_value = parameter.Value;
MinValue = parameter.MinValue;
MaxValue = parameter.MaxValue;
Options = parameter.Options;
LocalizedOptions = parameter.Options?
.Select(option => LocalizeOption(option))
.ToArray();
IsVisible = parameter.IsVisible;
IsAdvanced = parameter.IsAdvanced;
ParameterType = parameter.ValueType?.Name?.ToLowerInvariant() switch
{
"int32" or "int" => "int",
"double" => "double",
"boolean" or "bool" => "bool",
_ => "enum"
};
ValidateValue(_value);
}
public string Name { get; }
public string DisplayName { get; }
public object MinValue { get; }
public object MaxValue { get; }
public string[]? Options { get; }
public string[]? LocalizedOptions { get; }
public bool IsVisible { get; }
public bool IsAdvanced { get; }
public string ParameterType { get; }
public bool HasOptions => Options is { Length: > 0 };
public bool IsBool => ParameterType == "bool";
public bool IsNumeric => ParameterType is "int" or "double";
public bool HasRange => IsNumeric && MinValue != null && MaxValue != null;
public bool IsSliderInput => HasRange;
public bool IsTextInput => !IsBool && !HasOptions && !IsSliderInput;
/// <summary>是否为输出控制参数(Name 以 "Output" 开头的 bool 参数)</summary>
public bool IsOutputControl => IsBool && Name.StartsWith("Output", StringComparison.Ordinal);
public double SliderMinimum => TryConvertToDouble(MinValue, out var minValue) ? minValue : 0d;
public double SliderMaximum => TryConvertToDouble(MaxValue, out var maxValue) ? maxValue : 100d;
public double SliderTickFrequency => ResolveTickFrequency();
/// <summary>键盘/滚轮细调步距(等于 TickFrequency</summary>
public double SliderSmallChange => ResolveTickFrequency();
/// <summary>PageUp/PageDown 或点击轨道的粗调步距(约为范围的 10%,对齐到 SmallChange 的整数倍)</summary>
public double SliderLargeChange => ResolveLargeChange();
public bool IsIntegerSlider => ParameterType == "int";
public bool IsValueValid
{
get => _isValueValid;
private set => SetProperty(ref _isValueValid, value);
}
public object Value
{
get => _value;
set
{
var normalizedValue = NormalizeValue(value);
if (!Equals(_value, normalizedValue))
{
_value = normalizedValue;
ValidateValue(normalizedValue);
RaisePropertyChanged(nameof(Value));
RaisePropertyChanged(nameof(BoolValue));
RaisePropertyChanged(nameof(SelectedOption));
RaisePropertyChanged(nameof(SliderValue));
RaisePropertyChanged(nameof(DisplayValueText));
}
}
}
public double SliderValue
{
get => TryConvertToDouble(_value, out var sliderValue) ? sliderValue : SliderMinimum;
set
{
if (!IsSliderInput)
{
return;
}
Value = ParameterType == "int"
? (object)Convert.ToInt32(Math.Round(value, MidpointRounding.AwayFromZero), CultureInfo.InvariantCulture)
: Math.Round(value, ResolveDecimalPlaces(), MidpointRounding.AwayFromZero);
}
}
public string DisplayValueText
{
get
{
if (ParameterType == "int" && TryConvertToInt(_value, out var intValue))
{
return intValue.ToString(CultureInfo.InvariantCulture);
}
if (ParameterType == "double" && TryConvertToDouble(_value, out var doubleValue))
{
return doubleValue.ToString($"F{ResolveDecimalPlaces()}", CultureInfo.InvariantCulture);
}
return Convert.ToString(_value, CultureInfo.InvariantCulture) ?? string.Empty;
}
}
public bool BoolValue
{
get => ParameterType == "bool" && TryConvertToBool(_value, out var boolValue) && boolValue;
set
{
if (ParameterType == "bool")
{
Value = value;
}
}
}
public string SelectedOption
{
get => HasOptions
? GetLocalizedOption(Convert.ToString(_value, CultureInfo.InvariantCulture) ?? string.Empty)
: string.Empty;
set
{
if (HasOptions)
Value = GetRawOption(value);
}
}
private void ValidateValue(object value)
{
if (value == null)
{
IsValueValid = false;
return;
}
if (ParameterType == "int")
{
IsValueValid = TryConvertToInt(value, out var intValue) && IsWithinRange(intValue);
return;
}
if (ParameterType == "double")
{
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 double ResolveTickFrequency()
{
if (!HasRange)
{
return 1d;
}
double range = SliderMaximum - SliderMinimum;
if (ParameterType == "int")
{
// 整数步距:范围越大步距越大,避免刻度过密
if (range <= 20) return 1d;
if (range <= 100) return 5d;
if (range <= 500) return 10d;
if (range <= 2000) return 50d;
return Math.Ceiling(range / 40d / 10d) * 10d; // 约 40 步,对齐到 10 的倍数
}
// 浮点步距:按范围量级决定精度
if (range <= 1d) return 0.01d;
if (range <= 10d) return 0.1d;
if (range <= 100d) return 1d;
return Math.Ceiling(range / 100d); // 约 100 步
}
private double ResolveLargeChange()
{
if (!HasRange)
{
return 10d;
}
double range = SliderMaximum - SliderMinimum;
double small = ResolveTickFrequency();
// 粗调约为范围的 10%,并对齐到 SmallChange 的整数倍(至少 1 步)
double rough = range * 0.1d;
double steps = Math.Max(1d, Math.Round(rough / small));
return steps * small;
}
private int ResolveDecimalPlaces()
{
if (ParameterType == "int")
{
return 0;
}
double tick = SliderTickFrequency;
if (tick >= 1d)
{
return 0;
}
if (tick >= 0.1d)
{
return 1;
}
if (tick >= 0.01d)
{
return 2;
}
return 3;
}
private static string NormalizeNumericText(string value)
{
return value.Trim().TrimEnd('、', '', ',', '。', '.', ';', '', ':', '');
}
private static string LocalizeOption(string option)
{
if (string.IsNullOrWhiteSpace(option))
return option;
// 先允许资源覆盖通用名称;没有资源时使用内置通用翻译,保证历史算子
// 即使尚未补齐专属资源,也不会把内部英文枚举直接暴露到中文界面。
var localized = LocalizationHelper.GetString($"ProcessorOption_{option}");
if (!string.Equals(localized, $"ProcessorOption_{option}", StringComparison.Ordinal))
return localized;
if (!CultureInfo.CurrentUICulture.Name.StartsWith("zh", StringComparison.OrdinalIgnoreCase))
return SplitWords(option);
return option switch
{
"Uniform" => "均匀分层",
"Otsu" => "大津法",
"Peaks" => "波峰分层",
"EqualSpaced" => "等间距",
"MidValue" => "层中值",
"Spectrum" => "光谱",
"Traffic" => "交通灯",
"Heat" => "热力图",
"Fixed" => "固定阈值",
"TopHat" => "顶帽",
"LocalContrast" => "局部对比度",
"AdaptiveStatistics" => "自适应统计",
"Bright" => "亮区域",
"Dark" => "暗区域",
"Both" => "两者",
"Horizontal" => "水平",
"Vertical" => "垂直",
"None" => "无",
"Polygon" => "多边形",
"White" => "白色",
"Black" => "黑色",
"Nearest" => "最近邻",
"Bilinear" => "双线性",
"Bicubic" => "双三次",
"Lanczos" => "Lanczos",
"Global" => "全局",
"CLAHE" => "CLAHE",
"Linear" => "线性",
"Logarithmic" => "对数",
"Exponential" => "指数",
_ => option
};
}
private string GetLocalizedOption(string rawOption)
{
if (Options == null || LocalizedOptions == null)
return rawOption;
var index = Array.IndexOf(Options, rawOption);
return index >= 0 && index < LocalizedOptions.Length ? LocalizedOptions[index] : rawOption;
}
private string GetRawOption(string displayOption)
{
if (Options == null || LocalizedOptions == null)
return displayOption;
var index = Array.IndexOf(LocalizedOptions, displayOption);
return index >= 0 && index < Options.Length ? Options[index] : displayOption;
}
private static string SplitWords(string value)
{
var chars = value.Select((ch, index) => index > 0 && char.IsUpper(ch) ? $" {ch}" : ch.ToString());
return string.Concat(chars);
}
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;
}
}
}
}
}