| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318 |
- using System;
- using System.Collections.Generic;
- using System.ComponentModel;
- using System.Linq;
- using System.Threading;
- using System.Threading.Tasks;
- using System.Windows;
- using System.Windows.Controls;
- using System.Windows.Controls.Primitives;
- using System.Windows.Input;
- using System.Windows.Media;
- using HandyControl.Controls;
- using PgItem = PropertyGridLib.Controls.PropertyItem;
- namespace TeamAAS.Dialogs.Dialogs
- {
- /// <summary>
- /// 基于 PropertyGridLib 的通用属性编辑对话框。
- /// 将任意对象传入 SelectedObject,PropertyGrid 自动生成编辑界面。
- /// 可选执行按钮:传入 ExecuteAction 后显示"执行"按钮,点击后台执行不卡UI,执行中变为"停止"可取消。
- /// </summary>
- public partial class PropertyGridDialog
- {
- /// <summary>用户是否点击了确定</summary>
- public bool Result { get; private set; }
- /// <summary>执行回调(对象, 取消令牌),传入则显示执行按钮</summary>
- public Action<object, CancellationToken> ExecuteAction { get; set; }
- private CancellationTokenSource _cts;
- private bool _isExecuting;
- private bool _refreshingProperties;
- /// <summary>最近一次持久提示。悬停提示结束后回落到这里。</summary>
- private string _lastTip = string.Empty;
- /// <summary>当前编辑的对象</summary>
- public object SelectedObject
- {
- get => PropertyGrid.SelectedObject;
- set
- {
- PropertyGrid.SelectedObject = value;
- Title = $"属性编辑 - {value?.GetType().Name ?? ""}";
- }
- }
- /// <summary>自定义窗口标题</summary>
- public string DialogTitle
- {
- get => Title;
- set => Title = value;
- }
- public PropertyGridDialog()
- {
- InitializeComponent();
- UiScaler.Attach(this); // 分辨率/DPI 自适应
- Closing += (s, e) => { if (_isExecuting) e.Cancel = true; };
- PropertyGrid.AddHandler(Selector.SelectionChangedEvent, new SelectionChangedEventHandler(OnComboSelectionChanged), true);
- PropertyGrid.AddHandler(UIElement.PreviewMouseLeftButtonDownEvent, new MouseButtonEventHandler(OnComboPreviewMouseDown), true);
- // 回车 = 确认(执行中除外)
- PreviewKeyDown += (s, e) =>
- {
- if (e.Key == System.Windows.Input.Key.Enter && !_isExecuting)
- {
- BtnOK.RaiseEvent(new RoutedEventArgs(System.Windows.Controls.Button.ClickEvent));
- e.Handled = true;
- }
- };
- }
- /// <summary>
- /// 设置编辑对象和标题。
- /// </summary>
- public void Setup(object obj, string title = null)
- {
- PropertyGrid.SelectedObject = obj;
- Title = title ?? $"属性编辑 - {obj?.GetType().Name ?? ""}";
- }
- /// <summary>
- /// 设置编辑对象、标题和执行回调。
- /// </summary>
- public void Setup(object obj, string title, Action<object, CancellationToken> executeAction)
- {
- PropertyGrid.SelectedObject = obj;
- Title = title ?? $"属性编辑 - {obj?.GetType().Name ?? ""}";
- ExecuteAction = executeAction;
- if (executeAction != null)
- BtnExecute.Visibility = Visibility.Visible;
- }
- /// <summary>
- /// 设置底部提示栏的持久提示(线程安全)。null/空 = 显示最近一次持久提示。
- /// </summary>
- public void ShowTip(string message)
- {
- void Apply()
- {
- if (string.IsNullOrWhiteSpace(message))
- {
- TxtTip.Text = _lastTip;
- return;
- }
- _lastTip = message;
- TxtTip.Text = message;
- }
- if (Dispatcher.CheckAccess()) Apply(); else Dispatcher.Invoke(Apply);
- }
- /// <summary>
- /// 显示鼠标悬停临时提示(线程安全)。message 为 null/空时结束悬停,回落到最近一次持久提示。
- /// </summary>
- public void ShowHoverHint(string message)
- {
- void Apply()
- {
- TxtTip.Text = string.IsNullOrWhiteSpace(message) ? _lastTip : message;
- }
- if (Dispatcher.CheckAccess()) Apply(); else Dispatcher.Invoke(Apply);
- }
- /// <summary>窗体自带按钮的悬停提示:把 Tag 文本写入底部提示栏</summary>
- private void BtnTip_MouseEnter(object sender, MouseEventArgs e)
- {
- if ((sender as FrameworkElement)?.Tag is string tip && !string.IsNullOrWhiteSpace(tip))
- TxtTip.Text = tip;
- }
- /// <summary>悬停结束:回落到最近一次持久提示</summary>
- private void BtnTip_MouseLeave(object sender, MouseEventArgs e)
- {
- TxtTip.Text = _lastTip;
- }
- private void BtnExecute_Click(object sender, RoutedEventArgs e)
- {
- if (_isExecuting)
- {
- // 正在执行 → 停止
- _cts?.Cancel();
- return;
- }
- // 开始执行
- _cts = new CancellationTokenSource();
- _isExecuting = true;
- BtnExecute.Content = "停止";
- BtnExecute.Style = FindResource("ButtonDanger") as System.Windows.Style;
- PropertyGrid.IsEnabled = false;
- BtnOK.IsEnabled = false;
- BtnCancel.IsEnabled = false;
- var token = _cts.Token;
- var obj = PropertyGrid.SelectedObject;
- Task.Run(() =>
- {
- try
- {
- ExecuteAction?.Invoke(obj, token);
- }
- catch (OperationCanceledException) { }
- catch (Exception ex)
- {
- Dispatcher.Invoke(() => Growl.Error($"执行失败: {ex.Message}"));
- }
- }).ContinueWith(t =>
- {
- Dispatcher.Invoke(() =>
- {
- _isExecuting = false;
- BtnExecute.Content = "执行";
- BtnExecute.Style = FindResource("ButtonInfo") as System.Windows.Style;
- PropertyGrid.IsEnabled = true;
- BtnOK.IsEnabled = true;
- BtnCancel.IsEnabled = true;
- if (t.IsCanceled)
- Growl.Info("已停止");
- });
- });
- }
- private void BtnOK_Click(object sender, RoutedEventArgs e)
- {
- _cts?.Cancel();
- // 强制让 PropertyGrid 中的所有编辑控件提交修改
- // 关键:PropertyGridLib 的 ComboBox 使用 LostFocus 作为 UpdateSourceTrigger
- // 所以需要在关闭前确保所有下拉框关闭且焦点移出
- // 1. 关闭所有打开的 ComboBox 下拉框
- CloseAllComboBoxes(PropertyGrid);
- // 2. 临时将焦点移到按钮上,这会触发 PropertyGrid 内部控件的 LostFocus 事件
- BtnOK.Focus();
- // 3. 等待一个 Dispatcher 帧,让绑定更新生效
- Dispatcher.Invoke(() => { }, System.Windows.Threading.DispatcherPriority.Input);
- Result = true;
- Close();
- }
- /// <summary>
- /// 递归关闭所有 ComboBox 的下拉框
- /// </summary>
- private static void CloseAllComboBoxes(DependencyObject root)
- {
- if (root == null) return;
- int count = VisualTreeHelper.GetChildrenCount(root);
- for (int i = 0; i < count; i++)
- {
- var child = VisualTreeHelper.GetChild(root, i);
- if (child is System.Windows.Controls.ComboBox combo && combo.IsDropDownOpen)
- {
- combo.IsDropDownOpen = false;
- }
- CloseAllComboBoxes(child);
- }
- }
- private void BtnCancel_Click(object sender, RoutedEventArgs e)
- {
- _cts?.Cancel();
- Result = false;
- Close();
- }
- private void OnComboSelectionChanged(object sender, SelectionChangedEventArgs e)
- {
- if (_refreshingProperties || e.AddedItems == null || e.AddedItems.Count == 0) return;
- var combo = e.OriginalSource as System.Windows.Controls.ComboBox
- ?? FindComboBox(e.OriginalSource as DependencyObject);
- if (combo?.DataContext is not PgItem item) return;
- var owner = PropertyGrid.SelectedObject;
- if (owner == null || string.IsNullOrEmpty(item.Name)) return;
- var pd = TypeDescriptor.GetProperties(owner)[item.Name];
- if (pd == null || pd.IsReadOnly) return;
- object val = combo.SelectedItem ?? combo.Text;
- object current = pd.GetValue(owner);
- if (val != null && !Equals(current, val))
- {
- try { pd.SetValue(owner, val); }
- catch { /* 类型转换失败时仍走 LostFocus 写回 */ }
- }
- var refresh = pd.Attributes[typeof(RefreshPropertiesAttribute)] as RefreshPropertiesAttribute;
- if (refresh == null || refresh.RefreshProperties == RefreshProperties.None) return;
- if (Equals(current, val)) return;
- _refreshingProperties = true;
- try
- {
- PropertyGrid.RefreshProperties();
- }
- finally
- {
- _refreshingProperties = false;
- }
- }
- /// <summary>
- /// PropertyGridLib 只在创建属性项时抓一次 TypeConverter 下拉项。展开前按当前对象重取,
- /// 这样「先选卡再选轴」能看到该卡的轴列表。
- /// </summary>
- private void OnComboPreviewMouseDown(object sender, MouseButtonEventArgs e)
- {
- var combo = FindComboBox(e.OriginalSource as DependencyObject);
- if (combo == null || combo.DataContext is not PgItem item) return;
- RefreshComboStandardValues(combo, item);
- }
- private void RefreshComboStandardValues(System.Windows.Controls.ComboBox combo, PgItem item)
- {
- var owner = PropertyGrid.SelectedObject;
- if (owner == null || string.IsNullOrEmpty(item.Name)) return;
- var pd = TypeDescriptor.GetProperties(owner)[item.Name];
- var conv = pd?.Converter;
- if (conv == null || !conv.GetStandardValuesSupported()) return;
- var values = conv.GetStandardValues(new EditorTypeDescriptorContext(owner, pd));
- if (values == null || values.Count == 0) return;
- var list = values.Cast<object>().ToList();
- combo.ItemsSource = list;
- }
- private static System.Windows.Controls.ComboBox FindComboBox(DependencyObject start)
- {
- for (var d = start; d != null; d = VisualTreeHelper.GetParent(d))
- {
- if (d is System.Windows.Controls.ComboBox combo) return combo;
- }
- return null;
- }
- private sealed class EditorTypeDescriptorContext : ITypeDescriptorContext
- {
- public EditorTypeDescriptorContext(object instance, PropertyDescriptor property)
- {
- Instance = instance;
- PropertyDescriptor = property;
- }
- public IContainer Container => null;
- public object Instance { get; }
- public PropertyDescriptor PropertyDescriptor { get; }
- public object GetService(Type serviceType) => null;
- public void OnComponentChanged() { }
- public bool OnComponentChanging() => true;
- }
- }
- }
|