使用WPF实施日志查看器


71

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

它应符合以下条件:

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

总的来说,我想到的是FireBug和Chrome的控制台窗口。

我打得四处这个,但我并没有取得多大进展,因为... - DataGrid中不能处理不同项目的高度-滚动位置释放滚动条(这是完全不能接受的)后才会更新。

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

欢迎任何帮助或指点。


您确定需要实现自己的日志查看器吗?这是在重新发明轮子。您可以使用3rd party工具查看日志吗?例如,您可以打开DbgView,它将捕获通过Windows API发送的日志。然后,您可以广播将在该​​工具中捕获的日志,以便于浏览和过滤
-Liel

1
很好的问题。我需要此组件作为现有WPF应用程序的一部分。我们已经有一个“控制台”,它以令人沮丧的慢速TextBox实现。但是现在我们需要我描述的其他功能。我很高兴重用现有的商业或免费的非GPL组件。
pixtur

Answers:


189

我应该开始出售这些WPF样本,而不是免费分发。= P

在此处输入图片说明

  • 虚拟UI(使用VirtualizingStackPanel)提供了令人难以置信的良好性能(即使有200000多个项目)
  • 完全对MVVM友好。
  • DataTemplate每种LogEntry类型的。这些使您能够根据需要进行自定义。我只实现了两种LogEntries(基本和嵌套),但您明白了。您可以LogEntry根据需要子类化。您甚至可能支持富文本或图像。
  • 可扩展(嵌套)项目。
  • 换行。
  • 您可以使用来实现过滤等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>

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

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

数据项:

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:

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

12
哇!你刚写这个吗?这真是太神奇了。我刚刚对其进行了测试,它几乎可以完美回答我的问题。看来充实细节应该是直截了当的。我被吹走了。非常感谢!
pixtur

1
如果您不时被要求回答这样的问题,我将很高兴为您付款。:-)
pixtur

1
我发现的一个问题是,当我在嵌套项目上滚动鼠标滚轮时,视图没有滚动。
该死的蔬菜

3
@ SinJeong-hun是由于嵌套ScrollViewers引起的,您可以尝试设置ControlTemplate与嵌套ItemsControls不同的值。
2013年

4
或者,您也可以将嵌套的ScrollViewer的滚动连接到父scrollviewer。不应该那么难。唯一的问题是您无法复制粘贴日志,因为它是文本块。但是容易修复,使其成为文本框和只读格式。:P
123 456 789

22

HighCore的答案是完美的,但我想它没有满足以下要求:“最后,添加新项目时它应该继续滚动”。

根据答案,您可以执行以下操作:

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

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

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

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

2
AutoScroll变量是一个例外
alerya '16

2
谢谢。立即修复。
drizin

这不工作,偶尔给我上e.source空例外
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.