C# 使用 WPF 实现日志查看器

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/16743804/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-10 01:49:05  来源:igfitidea点击:

Implementing a log viewer with WPF

c#wpfperformanceuser-interfacescroll

提问by pixtur

I seek advice for the best approach to implement a console-log viewer with WPF.

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

It should match the following criteria:

它应该符合以下条件:

  • fast scrolling with 100.000+ lines
  • Some entries (like stacktraces) should be foldable
  • long items wrap
  • the list can be filtered by different criteria (searching, tags, etc)
  • when at the end, it should keep scrolling when new items are added
  • Line-elements can contain some sort of addition formatting like hyperlinks and occurrence counter
  • 快速滚动 100.000+ 行
  • 一些条目(如堆栈跟踪)应该是可折叠的
  • 长物品包裹
  • 该列表可以通过不同的条件(搜索、标签等)进行过滤
  • 最后,它应该在添加新项目时继续滚动
  • 行元素可以包含某种附加格式,如超链接和出现计数器

In general I have something in mind like the console window of FireBug and Chrome.

总的来说,我有一些类似 FireBug 和 Chrome 的控制台窗口的想法。

I played around with thisbut I didn't make much progress, because... - the datagrid can't handle different item heights - the scroll position is only updated after releasing the scrollbar (which is completely unacceptable).

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

I'm pretty sure, I need some form of virtualization and would love to follow the MVVM pattern.

我很确定,我需要某种形式的虚拟化,并且很想遵循 MVVM 模式。

Any help or pointers are welcome.

欢迎任何帮助或指示。

采纳答案by Federico Berasategui

I should start selling these WPF samples instead of giving them out for free. =P

我应该开始销售这些 WPF 示例,而不是免费分发它们。=P

enter image description here

在此处输入图片说明

  • Virtualized UI (Using VirtualizingStackPanel) which provides incredibly good performance (even with 200000+ items)
  • Fully MVVM-friendly.
  • DataTemplates for each kind of LogEntrytype. These give you the ability to customize as much as you want. I only implemented 2 kinds of LogEntries (basic and nested), but you get the idea. You may subclass LogEntryas much as you need. You may even support rich text or images.
  • Expandable (Nested) Items.
  • Word Wrap.
  • You can implement filtering, etc by using a CollectionView.
  • WPF Rocks, just copy and paste my code in a File -> New -> WPF Applicationand see the results for yourself.

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

Code Behind: (Notice that most of it is just boileplate to support the example (generate random entries)

代码隐藏:(请注意,大部分只是支持示例的样板(生成随机条目)

 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()
                       };

        }
    }

Data Items:

数据项:

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; }
}

PropertyChangedBase:

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));
                                                                     }));
        }
    }

回答by drizin

HighCore answer is perfect, but I guess it's missing this requirement:"when at the end, it should keep scrolling when new items are added".

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

According to thisanswer, you can do this:

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

In the main ScrollViewer (inside the DockPanel), add the event:

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

<ScrollViewer CanContentScroll="True" ScrollChanged="ScrollViewer_ScrollChanged">

Cast the event source to do the auto scroll:

投射事件源以进行自动滚动:

    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);
        }
    }
}