用 WPF 实现日志查看器

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

它应符合以下标准:

  • 100.000行以上的快速滚动
  • 有些条目(比如堆栈跟踪)应该是可折叠的
  • 长物品包装
  • 可以根据不同的条件(搜索、标记等)对列表进行过滤
  • 当最后添加新项时,它应该继续滚动
  • 行元素可以包含某种添加格式,如超链接和出现计数器

一般来说,我脑子里想的是 FireBug 和 Chrome 的控制台窗口。

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

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

欢迎任何帮助或指点。

60142 次浏览

我应该开始出售这些 WPF 样品,而不是免费发放

enter image description here

  • 虚拟用户界面(使用 VirtualizingStackPanel) ,它提供了令人难以置信的良好性能(即使有200000多个项目)
  • 完全 MVVM 友好。
  • 每种 LogEntry类型的 DataTemplate。这些可以让你根据自己的需要进行定制。我只实现了2种 LogEntry (基本的和嵌套的) ,但是您已经明白我的意思了。您可以根据需要继承 LogEntry。您甚至可以支持富文本或图像。
  • 可扩展(嵌套)项。
  • 总结一下。
  • 您可以使用 CollectionView实现过滤等。
  • WPF 岩石,只是复制和粘贴我的代码在一个 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));
}));
}
}

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