PropertyGridDialog.xaml.cs 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318
  1. using System;
  2. using System.Collections.Generic;
  3. using System.ComponentModel;
  4. using System.Linq;
  5. using System.Threading;
  6. using System.Threading.Tasks;
  7. using System.Windows;
  8. using System.Windows.Controls;
  9. using System.Windows.Controls.Primitives;
  10. using System.Windows.Input;
  11. using System.Windows.Media;
  12. using HandyControl.Controls;
  13. using PgItem = PropertyGridLib.Controls.PropertyItem;
  14. namespace TeamAAS.Dialogs.Dialogs
  15. {
  16. /// <summary>
  17. /// 基于 PropertyGridLib 的通用属性编辑对话框。
  18. /// 将任意对象传入 SelectedObject,PropertyGrid 自动生成编辑界面。
  19. /// 可选执行按钮:传入 ExecuteAction 后显示"执行"按钮,点击后台执行不卡UI,执行中变为"停止"可取消。
  20. /// </summary>
  21. public partial class PropertyGridDialog
  22. {
  23. /// <summary>用户是否点击了确定</summary>
  24. public bool Result { get; private set; }
  25. /// <summary>执行回调(对象, 取消令牌),传入则显示执行按钮</summary>
  26. public Action<object, CancellationToken> ExecuteAction { get; set; }
  27. private CancellationTokenSource _cts;
  28. private bool _isExecuting;
  29. private bool _refreshingProperties;
  30. /// <summary>最近一次持久提示。悬停提示结束后回落到这里。</summary>
  31. private string _lastTip = string.Empty;
  32. /// <summary>当前编辑的对象</summary>
  33. public object SelectedObject
  34. {
  35. get => PropertyGrid.SelectedObject;
  36. set
  37. {
  38. PropertyGrid.SelectedObject = value;
  39. Title = $"属性编辑 - {value?.GetType().Name ?? ""}";
  40. }
  41. }
  42. /// <summary>自定义窗口标题</summary>
  43. public string DialogTitle
  44. {
  45. get => Title;
  46. set => Title = value;
  47. }
  48. public PropertyGridDialog()
  49. {
  50. InitializeComponent();
  51. UiScaler.Attach(this); // 分辨率/DPI 自适应
  52. Closing += (s, e) => { if (_isExecuting) e.Cancel = true; };
  53. PropertyGrid.AddHandler(Selector.SelectionChangedEvent, new SelectionChangedEventHandler(OnComboSelectionChanged), true);
  54. PropertyGrid.AddHandler(UIElement.PreviewMouseLeftButtonDownEvent, new MouseButtonEventHandler(OnComboPreviewMouseDown), true);
  55. // 回车 = 确认(执行中除外)
  56. PreviewKeyDown += (s, e) =>
  57. {
  58. if (e.Key == System.Windows.Input.Key.Enter && !_isExecuting)
  59. {
  60. BtnOK.RaiseEvent(new RoutedEventArgs(System.Windows.Controls.Button.ClickEvent));
  61. e.Handled = true;
  62. }
  63. };
  64. }
  65. /// <summary>
  66. /// 设置编辑对象和标题。
  67. /// </summary>
  68. public void Setup(object obj, string title = null)
  69. {
  70. PropertyGrid.SelectedObject = obj;
  71. Title = title ?? $"属性编辑 - {obj?.GetType().Name ?? ""}";
  72. }
  73. /// <summary>
  74. /// 设置编辑对象、标题和执行回调。
  75. /// </summary>
  76. public void Setup(object obj, string title, Action<object, CancellationToken> executeAction)
  77. {
  78. PropertyGrid.SelectedObject = obj;
  79. Title = title ?? $"属性编辑 - {obj?.GetType().Name ?? ""}";
  80. ExecuteAction = executeAction;
  81. if (executeAction != null)
  82. BtnExecute.Visibility = Visibility.Visible;
  83. }
  84. /// <summary>
  85. /// 设置底部提示栏的持久提示(线程安全)。null/空 = 显示最近一次持久提示。
  86. /// </summary>
  87. public void ShowTip(string message)
  88. {
  89. void Apply()
  90. {
  91. if (string.IsNullOrWhiteSpace(message))
  92. {
  93. TxtTip.Text = _lastTip;
  94. return;
  95. }
  96. _lastTip = message;
  97. TxtTip.Text = message;
  98. }
  99. if (Dispatcher.CheckAccess()) Apply(); else Dispatcher.Invoke(Apply);
  100. }
  101. /// <summary>
  102. /// 显示鼠标悬停临时提示(线程安全)。message 为 null/空时结束悬停,回落到最近一次持久提示。
  103. /// </summary>
  104. public void ShowHoverHint(string message)
  105. {
  106. void Apply()
  107. {
  108. TxtTip.Text = string.IsNullOrWhiteSpace(message) ? _lastTip : message;
  109. }
  110. if (Dispatcher.CheckAccess()) Apply(); else Dispatcher.Invoke(Apply);
  111. }
  112. /// <summary>窗体自带按钮的悬停提示:把 Tag 文本写入底部提示栏</summary>
  113. private void BtnTip_MouseEnter(object sender, MouseEventArgs e)
  114. {
  115. if ((sender as FrameworkElement)?.Tag is string tip && !string.IsNullOrWhiteSpace(tip))
  116. TxtTip.Text = tip;
  117. }
  118. /// <summary>悬停结束:回落到最近一次持久提示</summary>
  119. private void BtnTip_MouseLeave(object sender, MouseEventArgs e)
  120. {
  121. TxtTip.Text = _lastTip;
  122. }
  123. private void BtnExecute_Click(object sender, RoutedEventArgs e)
  124. {
  125. if (_isExecuting)
  126. {
  127. // 正在执行 → 停止
  128. _cts?.Cancel();
  129. return;
  130. }
  131. // 开始执行
  132. _cts = new CancellationTokenSource();
  133. _isExecuting = true;
  134. BtnExecute.Content = "停止";
  135. BtnExecute.Style = FindResource("ButtonDanger") as System.Windows.Style;
  136. PropertyGrid.IsEnabled = false;
  137. BtnOK.IsEnabled = false;
  138. BtnCancel.IsEnabled = false;
  139. var token = _cts.Token;
  140. var obj = PropertyGrid.SelectedObject;
  141. Task.Run(() =>
  142. {
  143. try
  144. {
  145. ExecuteAction?.Invoke(obj, token);
  146. }
  147. catch (OperationCanceledException) { }
  148. catch (Exception ex)
  149. {
  150. Dispatcher.Invoke(() => Growl.Error($"执行失败: {ex.Message}"));
  151. }
  152. }).ContinueWith(t =>
  153. {
  154. Dispatcher.Invoke(() =>
  155. {
  156. _isExecuting = false;
  157. BtnExecute.Content = "执行";
  158. BtnExecute.Style = FindResource("ButtonInfo") as System.Windows.Style;
  159. PropertyGrid.IsEnabled = true;
  160. BtnOK.IsEnabled = true;
  161. BtnCancel.IsEnabled = true;
  162. if (t.IsCanceled)
  163. Growl.Info("已停止");
  164. });
  165. });
  166. }
  167. private void BtnOK_Click(object sender, RoutedEventArgs e)
  168. {
  169. _cts?.Cancel();
  170. // 强制让 PropertyGrid 中的所有编辑控件提交修改
  171. // 关键:PropertyGridLib 的 ComboBox 使用 LostFocus 作为 UpdateSourceTrigger
  172. // 所以需要在关闭前确保所有下拉框关闭且焦点移出
  173. // 1. 关闭所有打开的 ComboBox 下拉框
  174. CloseAllComboBoxes(PropertyGrid);
  175. // 2. 临时将焦点移到按钮上,这会触发 PropertyGrid 内部控件的 LostFocus 事件
  176. BtnOK.Focus();
  177. // 3. 等待一个 Dispatcher 帧,让绑定更新生效
  178. Dispatcher.Invoke(() => { }, System.Windows.Threading.DispatcherPriority.Input);
  179. Result = true;
  180. Close();
  181. }
  182. /// <summary>
  183. /// 递归关闭所有 ComboBox 的下拉框
  184. /// </summary>
  185. private static void CloseAllComboBoxes(DependencyObject root)
  186. {
  187. if (root == null) return;
  188. int count = VisualTreeHelper.GetChildrenCount(root);
  189. for (int i = 0; i < count; i++)
  190. {
  191. var child = VisualTreeHelper.GetChild(root, i);
  192. if (child is System.Windows.Controls.ComboBox combo && combo.IsDropDownOpen)
  193. {
  194. combo.IsDropDownOpen = false;
  195. }
  196. CloseAllComboBoxes(child);
  197. }
  198. }
  199. private void BtnCancel_Click(object sender, RoutedEventArgs e)
  200. {
  201. _cts?.Cancel();
  202. Result = false;
  203. Close();
  204. }
  205. private void OnComboSelectionChanged(object sender, SelectionChangedEventArgs e)
  206. {
  207. if (_refreshingProperties || e.AddedItems == null || e.AddedItems.Count == 0) return;
  208. var combo = e.OriginalSource as System.Windows.Controls.ComboBox
  209. ?? FindComboBox(e.OriginalSource as DependencyObject);
  210. if (combo?.DataContext is not PgItem item) return;
  211. var owner = PropertyGrid.SelectedObject;
  212. if (owner == null || string.IsNullOrEmpty(item.Name)) return;
  213. var pd = TypeDescriptor.GetProperties(owner)[item.Name];
  214. if (pd == null || pd.IsReadOnly) return;
  215. object val = combo.SelectedItem ?? combo.Text;
  216. object current = pd.GetValue(owner);
  217. if (val != null && !Equals(current, val))
  218. {
  219. try { pd.SetValue(owner, val); }
  220. catch { /* 类型转换失败时仍走 LostFocus 写回 */ }
  221. }
  222. var refresh = pd.Attributes[typeof(RefreshPropertiesAttribute)] as RefreshPropertiesAttribute;
  223. if (refresh == null || refresh.RefreshProperties == RefreshProperties.None) return;
  224. if (Equals(current, val)) return;
  225. _refreshingProperties = true;
  226. try
  227. {
  228. PropertyGrid.RefreshProperties();
  229. }
  230. finally
  231. {
  232. _refreshingProperties = false;
  233. }
  234. }
  235. /// <summary>
  236. /// PropertyGridLib 只在创建属性项时抓一次 TypeConverter 下拉项。展开前按当前对象重取,
  237. /// 这样「先选卡再选轴」能看到该卡的轴列表。
  238. /// </summary>
  239. private void OnComboPreviewMouseDown(object sender, MouseButtonEventArgs e)
  240. {
  241. var combo = FindComboBox(e.OriginalSource as DependencyObject);
  242. if (combo == null || combo.DataContext is not PgItem item) return;
  243. RefreshComboStandardValues(combo, item);
  244. }
  245. private void RefreshComboStandardValues(System.Windows.Controls.ComboBox combo, PgItem item)
  246. {
  247. var owner = PropertyGrid.SelectedObject;
  248. if (owner == null || string.IsNullOrEmpty(item.Name)) return;
  249. var pd = TypeDescriptor.GetProperties(owner)[item.Name];
  250. var conv = pd?.Converter;
  251. if (conv == null || !conv.GetStandardValuesSupported()) return;
  252. var values = conv.GetStandardValues(new EditorTypeDescriptorContext(owner, pd));
  253. if (values == null || values.Count == 0) return;
  254. var list = values.Cast<object>().ToList();
  255. combo.ItemsSource = list;
  256. }
  257. private static System.Windows.Controls.ComboBox FindComboBox(DependencyObject start)
  258. {
  259. for (var d = start; d != null; d = VisualTreeHelper.GetParent(d))
  260. {
  261. if (d is System.Windows.Controls.ComboBox combo) return combo;
  262. }
  263. return null;
  264. }
  265. private sealed class EditorTypeDescriptorContext : ITypeDescriptorContext
  266. {
  267. public EditorTypeDescriptorContext(object instance, PropertyDescriptor property)
  268. {
  269. Instance = instance;
  270. PropertyDescriptor = property;
  271. }
  272. public IContainer Container => null;
  273. public object Instance { get; }
  274. public PropertyDescriptor PropertyDescriptor { get; }
  275. public object GetService(Type serviceType) => null;
  276. public void OnComponentChanged() { }
  277. public bool OnComponentChanging() => true;
  278. }
  279. }
  280. }