Реализация средства просмотра журнала с помощью WPF
Я ищу совет для лучшего подхода к реализации средства просмотра журнала консоли с помощью WPF.
Он должен соответствовать следующим критериям:
- быстрая прокрутка с 100.000+ строки
- некоторые записи (например, stacktraces) должны быть складными
- длинные предметы обернуть
- список может быть отфильтрован по различным критериям (поиск, теги и т. д.)
- когда в конце, он должен продолжать прокрутку, когда новые элементы добавлено
- Line-элементы могут содержать какое-то дополнительное форматирование, такое как гиперссылки и счетчик встречаемости
В общем у меня есть что-то в виду, как окно консоли FireBug и Chrome.
Я играл вокруг с этой но я не сделал большого прогресса, потому что... - datagrid не может обрабатывать различные высоты элемента - положение прокрутки обновляется только после освобождения полосы прокрутки (которая полностью неприемлемый.)
Я уверен, что мне нужна какая-то форма виртуализации, и я хотел бы следовать шаблону MVVM.
любая помощь или указатели приветствуются.
2 ответа:
я должен начать продавать эти образцы WPF вместо того, чтобы раздавать их бесплатно. =P
- виртуализированный пользовательский интерфейс (с помощью
VirtualizingStackPanel
) который обеспечивает невероятно хорошую производительность (даже с 200000+ пунктов)- полностью MVVM-дружественный.
DataTemplate
S для каждого видаLogEntry
тип. Они дают вам возможность настроить как сколько хочешь. Я реализовал только 2 вида LogEntries (basic и nested), но вы получите идею. Вы можете подкласс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>
Код: (обратите внимание, что большинство из них просто шаблон для поддержки примера (generate случайные записи)
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); } } }