PropertyGridDialog.xaml.cs 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Threading;
  4. using System.Threading.Tasks;
  5. using System.Windows;
  6. using System.Windows.Controls;
  7. using System.Windows.Input;
  8. using System.Windows.Media;
  9. using HandyControl.Controls;
  10. namespace TeamAAS.Dialogs.Dialogs
  11. {
  12. /// <summary>
  13. /// 基于 PropertyGridLib 的通用属性编辑对话框。
  14. /// 将任意对象传入 SelectedObject,PropertyGrid 自动生成编辑界面。
  15. /// 可选执行按钮:传入 ExecuteAction 后显示"执行"按钮,点击后台执行不卡UI,执行中变为"停止"可取消。
  16. /// </summary>
  17. public partial class PropertyGridDialog
  18. {
  19. /// <summary>用户是否点击了确定</summary>
  20. public bool Result { get; private set; }
  21. /// <summary>执行回调(对象, 取消令牌),传入则显示执行按钮</summary>
  22. public Action<object, CancellationToken> ExecuteAction { get; set; }
  23. private CancellationTokenSource _cts;
  24. private bool _isExecuting;
  25. /// <summary>最近一次持久提示。悬停提示结束后回落到这里。</summary>
  26. private string _lastTip = string.Empty;
  27. /// <summary>当前编辑的对象</summary>
  28. public object SelectedObject
  29. {
  30. get => PropertyGrid.SelectedObject;
  31. set
  32. {
  33. PropertyGrid.SelectedObject = value;
  34. Title = $"属性编辑 - {value?.GetType().Name ?? ""}";
  35. }
  36. }
  37. /// <summary>自定义窗口标题</summary>
  38. public string DialogTitle
  39. {
  40. get => Title;
  41. set => Title = value;
  42. }
  43. public PropertyGridDialog()
  44. {
  45. InitializeComponent();
  46. UiScaler.Attach(this); // 分辨率/DPI 自适应
  47. Closing += (s, e) => { if (_isExecuting) e.Cancel = true; };
  48. // 回车 = 确认(执行中除外)
  49. PreviewKeyDown += (s, e) =>
  50. {
  51. if (e.Key == System.Windows.Input.Key.Enter && !_isExecuting)
  52. {
  53. BtnOK.RaiseEvent(new RoutedEventArgs(System.Windows.Controls.Button.ClickEvent));
  54. e.Handled = true;
  55. }
  56. };
  57. }
  58. /// <summary>
  59. /// 设置编辑对象和标题。
  60. /// </summary>
  61. public void Setup(object obj, string title = null)
  62. {
  63. PropertyGrid.SelectedObject = obj;
  64. Title = title ?? $"属性编辑 - {obj?.GetType().Name ?? ""}";
  65. }
  66. /// <summary>
  67. /// 设置编辑对象、标题和执行回调。
  68. /// </summary>
  69. public void Setup(object obj, string title, Action<object, CancellationToken> executeAction)
  70. {
  71. PropertyGrid.SelectedObject = obj;
  72. Title = title ?? $"属性编辑 - {obj?.GetType().Name ?? ""}";
  73. ExecuteAction = executeAction;
  74. if (executeAction != null)
  75. BtnExecute.Visibility = Visibility.Visible;
  76. }
  77. /// <summary>
  78. /// 设置底部提示栏的持久提示(线程安全)。null/空 = 显示最近一次持久提示。
  79. /// </summary>
  80. public void ShowTip(string message)
  81. {
  82. void Apply()
  83. {
  84. if (string.IsNullOrWhiteSpace(message))
  85. {
  86. TxtTip.Text = _lastTip;
  87. return;
  88. }
  89. _lastTip = message;
  90. TxtTip.Text = message;
  91. }
  92. if (Dispatcher.CheckAccess()) Apply(); else Dispatcher.Invoke(Apply);
  93. }
  94. /// <summary>
  95. /// 显示鼠标悬停临时提示(线程安全)。message 为 null/空时结束悬停,回落到最近一次持久提示。
  96. /// </summary>
  97. public void ShowHoverHint(string message)
  98. {
  99. void Apply()
  100. {
  101. TxtTip.Text = string.IsNullOrWhiteSpace(message) ? _lastTip : message;
  102. }
  103. if (Dispatcher.CheckAccess()) Apply(); else Dispatcher.Invoke(Apply);
  104. }
  105. /// <summary>窗体自带按钮的悬停提示:把 Tag 文本写入底部提示栏</summary>
  106. private void BtnTip_MouseEnter(object sender, MouseEventArgs e)
  107. {
  108. if ((sender as FrameworkElement)?.Tag is string tip && !string.IsNullOrWhiteSpace(tip))
  109. TxtTip.Text = tip;
  110. }
  111. /// <summary>悬停结束:回落到最近一次持久提示</summary>
  112. private void BtnTip_MouseLeave(object sender, MouseEventArgs e)
  113. {
  114. TxtTip.Text = _lastTip;
  115. }
  116. private void BtnExecute_Click(object sender, RoutedEventArgs e)
  117. {
  118. if (_isExecuting)
  119. {
  120. // 正在执行 → 停止
  121. _cts?.Cancel();
  122. return;
  123. }
  124. // 开始执行
  125. _cts = new CancellationTokenSource();
  126. _isExecuting = true;
  127. BtnExecute.Content = "停止";
  128. BtnExecute.Style = FindResource("ButtonDanger") as System.Windows.Style;
  129. PropertyGrid.IsEnabled = false;
  130. BtnOK.IsEnabled = false;
  131. BtnCancel.IsEnabled = false;
  132. var token = _cts.Token;
  133. var obj = PropertyGrid.SelectedObject;
  134. Task.Run(() =>
  135. {
  136. try
  137. {
  138. ExecuteAction?.Invoke(obj, token);
  139. }
  140. catch (OperationCanceledException) { }
  141. catch (Exception ex)
  142. {
  143. Dispatcher.Invoke(() => Growl.Error($"执行失败: {ex.Message}"));
  144. }
  145. }).ContinueWith(t =>
  146. {
  147. Dispatcher.Invoke(() =>
  148. {
  149. _isExecuting = false;
  150. BtnExecute.Content = "执行";
  151. BtnExecute.Style = FindResource("ButtonInfo") as System.Windows.Style;
  152. PropertyGrid.IsEnabled = true;
  153. BtnOK.IsEnabled = true;
  154. BtnCancel.IsEnabled = true;
  155. if (t.IsCanceled)
  156. Growl.Info("已停止");
  157. });
  158. });
  159. }
  160. private void BtnOK_Click(object sender, RoutedEventArgs e)
  161. {
  162. _cts?.Cancel();
  163. // 强制让 PropertyGrid 中的所有编辑控件提交修改
  164. // 关键:PropertyGridLib 的 ComboBox 使用 LostFocus 作为 UpdateSourceTrigger
  165. // 所以需要在关闭前确保所有下拉框关闭且焦点移出
  166. // 1. 关闭所有打开的 ComboBox 下拉框
  167. CloseAllComboBoxes(PropertyGrid);
  168. // 2. 临时将焦点移到按钮上,这会触发 PropertyGrid 内部控件的 LostFocus 事件
  169. BtnOK.Focus();
  170. // 3. 等待一个 Dispatcher 帧,让绑定更新生效
  171. Dispatcher.Invoke(() => { }, System.Windows.Threading.DispatcherPriority.Input);
  172. Result = true;
  173. Close();
  174. }
  175. /// <summary>
  176. /// 递归关闭所有 ComboBox 的下拉框
  177. /// </summary>
  178. private static void CloseAllComboBoxes(DependencyObject root)
  179. {
  180. if (root == null) return;
  181. int count = VisualTreeHelper.GetChildrenCount(root);
  182. for (int i = 0; i < count; i++)
  183. {
  184. var child = VisualTreeHelper.GetChild(root, i);
  185. if (child is System.Windows.Controls.ComboBox combo && combo.IsDropDownOpen)
  186. {
  187. combo.IsDropDownOpen = false;
  188. }
  189. CloseAllComboBoxes(child);
  190. }
  191. }
  192. private void BtnCancel_Click(object sender, RoutedEventArgs e)
  193. {
  194. _cts?.Cancel();
  195. Result = false;
  196. Close();
  197. }
  198. }
  199. }