使用WPF实现日志查看器

pix*_*tur 71 c# wpf performance user-interface scroll

我寻求有关使用WPF实现控制台日志查看器的最佳方法的建议.

它应符合以下标准:

  • 快速滚动100.000+行
  • 某些条目(如stacktraces)应该是可折叠的
  • 长项包裹
  • 列表可以按不同的标准(搜索,标签等)进行过滤
  • 在最后,它应该在添加新项目时继续滚动
  • 线元素可以包含某种添加格式,如超链接和出现计数器

总的来说,我有一些想法,如FireBug和Chrome的控制台窗口.

我玩这个但是我没有取得多大进展,因为...... - 数据网格无法处理不同的项目高度 - 滚动位置仅在释放滚动条后更新(这是完全不可接受的).

我很确定,我需要某种形式的虚拟化,并希望遵循MVVM模式.

欢迎任何帮助或指示.

Fed*_*gui 178

我应该开始销售这些WPF样品,而不是免费赠送.= P

在此输入图像描述

  • 虚拟化UI(使用VirtualizingStackPanel),提供令人难以置信的良好性能(即使有200000多个项目)
  • 完全MVVM友好.
  • DataTemplates适用于LogEntry各种类型.这些使您能够根据需要进行自定义.我只实现了2种LogEntries(基本和嵌套),但你明白了.您可以LogEntry根据需要进行子类化.您甚至可以支持富文本或图像.
  • 可扩展(嵌套)项.
  • 自动换行.
  • 您可以使用a实现过滤等CollectionView.
  • WPF Rocks,只需将我的代码复制并粘贴到一个File -> New -> WPF Application并自己查看结果.

    <Window x:Class="MiscSamples.LogViewer"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:MiscSamples"
        Title="LogViewer" Height="500" Width="800">
    <Window.Resources>
        <Style TargetType="ItemsControl" x:Key="LogViewerStyle">
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate>
                        <ScrollViewer CanContentScroll="True">
                            <ItemsPresenter/>
                        </ScrollViewer>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
    
            <Setter Property="ItemsPanel">
                <Setter.Value>
                    <ItemsPanelTemplate>
                        <VirtualizingStackPanel IsItemsHost="True"/>
                    </ItemsPanelTemplate>
                </Setter.Value>
            </Setter>
        </Style>
    
        <DataTemplate DataType="{x:Type local:LogEntry}">
            <Grid IsSharedSizeScope="True">
                <Grid.ColumnDefinitions>
                    <ColumnDefinition SharedSizeGroup="Index" Width="Auto"/>
                    <ColumnDefinition SharedSizeGroup="Date" Width="Auto"/>
                    <ColumnDefinition/>
                </Grid.ColumnDefinitions>
    
                <TextBlock Text="{Binding DateTime}" Grid.Column="0"
                           FontWeight="Bold" Margin="5,0,5,0"/>
    
                <TextBlock Text="{Binding Index}" Grid.Column="1"
                           FontWeight="Bold" Margin="0,0,2,0" />
    
                <TextBlock Text="{Binding Message}" Grid.Column="2"
                           TextWrapping="Wrap"/>
            </Grid>
        </DataTemplate>
    
        <DataTemplate DataType="{x:Type local:CollapsibleLogEntry}">
            <Grid IsSharedSizeScope="True">
                <Grid.ColumnDefinitions>
                    <ColumnDefinition SharedSizeGroup="Index" Width="Auto"/>
                    <ColumnDefinition SharedSizeGroup="Date" Width="Auto"/>
                    <ColumnDefinition/>
                </Grid.ColumnDefinitions>
    
                <Grid.RowDefinitions>
                    <RowDefinition Height="Auto"/>
                    <RowDefinition/>
                </Grid.RowDefinitions>
    
                <TextBlock Text="{Binding DateTime}" Grid.Column="0"
                           FontWeight="Bold" Margin="5,0,5,0"/>
    
                <TextBlock Text="{Binding Index}" Grid.Column="1"
                           FontWeight="Bold" Margin="0,0,2,0" />
    
                <TextBlock Text="{Binding Message}" Grid.Column="2"
                           TextWrapping="Wrap"/>
    
                <ToggleButton x:Name="Expander" Grid.Row="1" Grid.Column="0"
                              VerticalAlignment="Top" Content="+" HorizontalAlignment="Right"/>
    
                <ItemsControl ItemsSource="{Binding Contents}" Style="{StaticResource LogViewerStyle}"
                              Grid.Row="1" Grid.Column="1" Grid.ColumnSpan="2"
                              x:Name="Contents" Visibility="Collapsed"/>
    
            </Grid>
            <DataTemplate.Triggers>
                <Trigger SourceName="Expander" Property="IsChecked" Value="True">
                    <Setter TargetName="Contents" Property="Visibility" Value="Visible"/>
                    <Setter TargetName="Expander" Property="Content" Value="-"/>
                </Trigger>
            </DataTemplate.Triggers>
        </DataTemplate>
    </Window.Resources>
    
    <DockPanel>
        <TextBlock Text="{Binding Count, StringFormat='{}{0} Items'}"
                   DockPanel.Dock="Top"/>
    
        <ItemsControl ItemsSource="{Binding}" Style="{StaticResource LogViewerStyle}">
            <ItemsControl.Template>
                <ControlTemplate>
                    <ScrollViewer CanContentScroll="True">
                        <ItemsPresenter/>
                    </ScrollViewer>
                </ControlTemplate>
            </ItemsControl.Template>
            <ItemsControl.ItemsPanel>
                <ItemsPanelTemplate>
                    <VirtualizingStackPanel IsItemsHost="True"/>
                </ItemsPanelTemplate>
            </ItemsControl.ItemsPanel>
        </ItemsControl>
    </DockPanel>
    </Window>
    
    Run Code Online (Sandbox Code Playgroud)

代码背后:( 请注意,大多数只是支持示例的boileplate(生成随机条目)

 public partial class LogViewer : Window
    {
        private string TestData = "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum";
        private List<string> words;
        private int maxword;
        private int index;

        public ObservableCollection<LogEntry> LogEntries { get; set; }

        public LogViewer()
        {
            InitializeComponent();

            random = new Random();
            words = TestData.Split(' ').ToList();
            maxword = words.Count - 1;

            DataContext = LogEntries = new ObservableCollection<LogEntry>();
            Enumerable.Range(0, 200000)
                      .ToList()
                      .ForEach(x => LogEntries.Add(GetRandomEntry()));

            Timer = new Timer(x => AddRandomEntry(), null, 1000, 10);
        }

        private System.Threading.Timer Timer;
        private System.Random random;
        private void AddRandomEntry()
        {
            Dispatcher.BeginInvoke((Action) (() => LogEntries.Add(GetRandomEntry())));
        }

        private LogEntry GetRandomEntry()
        {
            if (random.Next(1,10) > 1)
            {
                return new LogEntry()
                {
                    Index = index++,
                    DateTime = DateTime.Now,
                    Message = string.Join(" ", Enumerable.Range(5, random.Next(10, 50))
                                                         .Select(x => words[random.Next(0, maxword)])),
                };
            }

            return new CollapsibleLogEntry()
                       {
                           Index = index++,
                           DateTime = DateTime.Now,
                           Message = string.Join(" ", Enumerable.Range(5, random.Next(10, 50))
                                                        .Select(x => words[random.Next(0, maxword)])),
                           Contents = Enumerable.Range(5, random.Next(5, 10))
                                                .Select(i => GetRandomEntry())
                                                .ToList()
                       };

        }
    }
Run Code Online (Sandbox Code Playgroud)

数据项:

public class LogEntry: PropertyChangedBase
{
    public DateTime DateTime { get; set; }

    public int Index { get; set; }

    public string Message { get; set; }
}

public class CollapsibleLogEntry: LogEntry
{
    public List<LogEntry> Contents { get; set; }
}
Run Code Online (Sandbox Code Playgroud)

PropertyChangedBase:

 public class PropertyChangedBase:INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;

        protected virtual void OnPropertyChanged(string propertyName)
        {
            Application.Current.Dispatcher.BeginInvoke((Action) (() =>
                                                                     {
                                                                         PropertyChangedEventHandler handler = PropertyChanged;
                                                                         if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
                                                                     }));
        }
    }
Run Code Online (Sandbox Code Playgroud)

  • 哇!你刚刚写了这个吗?!这真是太神奇了.我只是测试了它,它几乎是我的问题的完美答案.看起来充实细节应该是直截了当的.我被吹走了.非常感谢! (8认同)
  • @SinJeong-hun 那是因为嵌套了 `ScrollViewer`s,你可以尝试为嵌套的 `ItemsControl`s 设置一个不同的 `ControlTemplate`。 (3认同)
  • 或者您可以将嵌套的ScrollViewer滚动连接到父滚动查看器.不应该那么难.这里唯一的问题是你不能复制粘贴日志,因为它是一个文本块.但很容易修复它使它成为一个文本框和readonly.:P (3认同)
  • @user1034912 如果您每秒有 10k 条日志,那么您显然遇到的问题比仅在日志查看器应用程序中显示它们要大得多。 (3认同)

dri*_*zin 20

HighCore答案是完美的,但我想它缺少这个要求:"当最后,它应该在添加新项目时继续滚动".

根据这个答案,你可以这样做:

在主ScrollViewer(DockPanel内)中,添加事件:

<ScrollViewer CanContentScroll="True" ScrollChanged="ScrollViewer_ScrollChanged">
Run Code Online (Sandbox Code Playgroud)

转换事件源以执行自动滚动:

    private bool AutoScroll = true;
    private void ScrollViewer_ScrollChanged(object sender, ScrollChangedEventArgs e)
    {
        // User scroll event : set or unset autoscroll mode
        if (e.ExtentHeightChange == 0)
        {   // Content unchanged : user scroll event
            if ((e.Source as ScrollViewer).VerticalOffset == (e.Source as ScrollViewer).ScrollableHeight)
            {   // Scroll bar is in bottom
                // Set autoscroll mode
                AutoScroll = true;
            }
            else
            {   // Scroll bar isn't in bottom
                // Unset autoscroll mode
                AutoScroll = false;
            }
        }

        // Content scroll event : autoscroll eventually
        if (AutoScroll && e.ExtentHeightChange != 0)
        {   // Content changed and autoscroll mode set
            // Autoscroll
            (e.Source as ScrollViewer).ScrollToVerticalOffset((e.Source as ScrollViewer).ExtentHeight);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

  • 谢谢。现在固定。 (3认同)
  • AutoScroll 变量是一个例外 (2认同)