LogView.xaml.cs 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181
  1. using System;
  2. using System.Collections.ObjectModel;
  3. using System.ComponentModel;
  4. using System.Windows;
  5. using System.Windows.Controls;
  6. using System.Windows.Data;
  7. using System.Windows.Input;
  8. using System.Windows.Media;
  9. using TeamAAS.Dialogs.Dialogs;
  10. namespace TeamAAS.Logging.Controls
  11. {
  12. /// <summary>
  13. /// 实时日志控件:订阅 <see cref="LogStream.Published"/> 联动显示 AppLogger / PluginLogger 的日志。
  14. /// - 类别筛选:Debug/Info/Warning/Error/Critical 多选开关;
  15. /// - 显示判定:公共日志恒显;插件日志由各节点「显示详细度」闸门决定是否进流(与是否落盘无关);
  16. /// - 插件条目上的 L2/L4 为归一化档位标签,仅用于区分常规/详细,不参与显隐。
  17. /// </summary>
  18. public partial class LogView : UserControl
  19. {
  20. /// <summary>显示上限依赖属性的默认值(可在 XAML 用 MaxEntries 覆盖)。</summary>
  21. private const int DefaultMaxEntries = 500;
  22. /// <summary>显示上限:控件最多保留的最近条目数,超出移除最旧。可在 XAML 用 MaxEntries 设置。</summary>
  23. public static readonly DependencyProperty MaxEntriesProperty =
  24. DependencyProperty.Register(nameof(MaxEntries), typeof(int), typeof(LogView),
  25. new PropertyMetadata(DefaultMaxEntries, OnMaxEntriesChanged));
  26. /// <summary>显示上限(最近 N 条),默认 500,&lt;=0 视为 1。</summary>
  27. public int MaxEntries
  28. {
  29. get => (int)GetValue(MaxEntriesProperty);
  30. set => SetValue(MaxEntriesProperty, value);
  31. }
  32. private static void OnMaxEntriesChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
  33. => (d as LogView)?.TrimToMax();
  34. private readonly ObservableCollection<LogEntry> _items = new ObservableCollection<LogEntry>();
  35. private readonly ICollectionView _view;
  36. private ScrollViewer _scrollViewer;
  37. private bool _subscribed;
  38. public LogView()
  39. {
  40. InitializeComponent();
  41. _view = CollectionViewSource.GetDefaultView(_items);
  42. _view.Filter = o => IsCategoryEnabled(((LogEntry)o).Category);
  43. LogList.ItemsSource = _view;
  44. Loaded += OnLoaded;
  45. Unloaded += OnUnloaded;
  46. }
  47. private void OnLoaded(object sender, RoutedEventArgs e)
  48. {
  49. if (_subscribed) return;
  50. // 首次加载回填历史缓冲(新建控件实例时 _items 为空)
  51. if (_items.Count == 0)
  52. {
  53. foreach (var entry in LogStream.Snapshot())
  54. AppendInternal(entry);
  55. }
  56. LogStream.Published += OnPublished;
  57. _subscribed = true;
  58. ScrollToEnd();
  59. }
  60. private void OnUnloaded(object sender, RoutedEventArgs e)
  61. {
  62. if (!_subscribed) return;
  63. LogStream.Published -= OnPublished;
  64. _subscribed = false;
  65. }
  66. private void OnPublished(LogEntry entry)
  67. {
  68. var dispatcher = Application.Current?.Dispatcher;
  69. if (dispatcher != null && !dispatcher.CheckAccess())
  70. dispatcher.BeginInvoke(new Action(() => Append(entry)));
  71. else
  72. Append(entry);
  73. }
  74. private void Append(LogEntry entry)
  75. {
  76. bool atBottom = IsScrolledToEnd();
  77. AppendInternal(entry);
  78. if (atBottom) ScrollToEnd();
  79. }
  80. private void AppendInternal(LogEntry entry)
  81. {
  82. _items.Add(entry);
  83. TrimToMax();
  84. }
  85. private void TrimToMax()
  86. {
  87. int max = MaxEntries <= 0 ? 1 : MaxEntries;
  88. while (_items.Count > max)
  89. _items.RemoveAt(0);
  90. }
  91. private void CategoryToggle_Changed(object sender, RoutedEventArgs e)
  92. {
  93. // 初始化阶段(ToggleButton 尚未就绪)不刷新
  94. if (_view != null && LogList != null)
  95. _view.Refresh();
  96. }
  97. private void BtnClear_Click(object sender, RoutedEventArgs e)
  98. {
  99. _items.Clear();
  100. }
  101. // 双击一条日志 → 弹出详情窗(含来源/时间/类别/完整内容,支持复制)
  102. private void LogList_MouseDoubleClick(object sender, MouseButtonEventArgs e)
  103. {
  104. var entry = FindEntry(e.OriginalSource as DependencyObject) ?? LogList.SelectedItem as LogEntry;
  105. if (entry == null) return;
  106. var window = new LogDetailWindow(entry) { Owner = Window.GetWindow(this) };
  107. window.ShowDialog();
  108. }
  109. // 从双击命中点沿(视觉/逻辑)树上溯,找到 DataContext 为 LogEntry 的元素
  110. private static LogEntry FindEntry(DependencyObject node)
  111. {
  112. while (node != null)
  113. {
  114. if (node is FrameworkElement fe && fe.DataContext is LogEntry entry)
  115. return entry;
  116. node = node is Visual
  117. ? VisualTreeHelper.GetParent(node)
  118. : LogicalTreeHelper.GetParent(node);
  119. }
  120. return null;
  121. }
  122. private bool IsCategoryEnabled(LogLevel category)
  123. {
  124. switch (category)
  125. {
  126. case LogLevel.Warning: return TgWarning == null || TgWarning.IsChecked == true;
  127. case LogLevel.Error: return TgError == null || TgError.IsChecked == true;
  128. default: return TgInfo == null || TgInfo.IsChecked == true; // Info
  129. }
  130. }
  131. private bool IsScrolledToEnd()
  132. {
  133. if (_scrollViewer == null) _scrollViewer = FindScrollViewer(LogList);
  134. if (_scrollViewer == null) return true;
  135. return _scrollViewer.VerticalOffset >= _scrollViewer.ScrollableHeight - 4;
  136. }
  137. private void ScrollToEnd()
  138. {
  139. if (LogList.Items.Count > 0)
  140. LogList.ScrollIntoView(LogList.Items[LogList.Items.Count - 1]);
  141. }
  142. private static ScrollViewer FindScrollViewer(DependencyObject root)
  143. {
  144. if (root == null) return null;
  145. int count = VisualTreeHelper.GetChildrenCount(root);
  146. for (int i = 0; i < count; i++)
  147. {
  148. var child = VisualTreeHelper.GetChild(root, i);
  149. if (child is ScrollViewer sv) return sv;
  150. var result = FindScrollViewer(child);
  151. if (result != null) return result;
  152. }
  153. return null;
  154. }
  155. }
  156. }