74 votes

Mise en œuvre d'une visionneuse de journaux avec WPF

Je cherche des conseils sur la meilleure approche pour mettre en œuvre un visualiseur de journal de console avec WPF.

Il doit correspondre aux critères suivants :

  • défilement rapide avec plus de 100.000 lignes
  • Certaines entrées (comme les stacktraces) devraient être pliables.
  • emballage des articles longs
  • la liste peut être filtrée selon différents critères (recherche, tags, etc.)
  • lorsqu'à la fin, il doit continuer à défiler lorsque de nouveaux éléments sont ajoutés.
  • Les éléments de ligne peuvent contenir une sorte de mise en forme supplémentaire, comme des hyperliens et un compteur d'occurrences.

En général, j'ai en tête quelque chose comme la fenêtre de console de FireBug et Chrome.

J'ai joué avec este mais je n'ai pas beaucoup avancé, parce que... - la grille de données ne peut pas gérer différentes hauteurs d'éléments - la position de défilement n'est mise à jour qu'après avoir relâché la barre de défilement (ce qui est totalement inacceptable).

Je suis pratiquement sûr que j'ai besoin d'une forme de virtualisation et que j'aimerais suivre le modèle MVVM.

Toute aide ou indication est la bienvenue.

0 votes

Êtes-vous sûr de devoir mettre en place votre propre visionneuse de journaux ? C'est un peu réinventer la roue... Pouvez-vous utiliser des outils tiers pour visualiser vos journaux ? Par exemple, vous pouvez ouvrir DbgView et il capturera les journaux qui sont envoyés via l'API Windows. Vous pouvez ensuite diffuser les journaux qui seront capturés dans l'outil, pour une navigation et un filtrage faciles.

1 votes

Excellente question. J'ai besoin de ce composant dans le cadre d'une application WPF existante. Nous disposons déjà d'une "console" qui est implémentée sous la forme d'une TextBox d'une lenteur frustrante. Mais nous avons maintenant besoin des fonctionnalités supplémentaires que j'ai décrites. Je suis très heureux de réutiliser des composants commerciaux ou gratuits non-GPL existants.

205voto

HighCore Points 23088

Je devrais commencer à vendre ces échantillons WPF au lieu de les donner gratuitement. =P

enter image description here

  • Interface utilisateur virtualisée (Utilisation VirtualizingStackPanel ) qui offre des performances incroyables (même avec plus de 200 000 éléments).
  • Entièrement compatible avec MVVM.
  • DataTemplate pour chaque type de LogEntry type. Ceux-ci vous donnent la possibilité de les personnaliser autant que vous le souhaitez. Je n'ai implémenté que 2 types de LogEntries (basic et nested), mais vous comprenez l'idée. Vous pouvez sous-classer LogEntry autant que vous le souhaitez. Vous pouvez même prendre en charge le texte riche ou les images.
  • Éléments extensibles (imbriqués).
  • Word Wrap.
  • Vous pouvez mettre en œuvre le filtrage, etc. en utilisant un fichier CollectionView .
  • WPF Rocks, il suffit de copier et coller mon code dans une File -> New -> WPF Application et voyez les résultats par vous-même.

    <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 : (Remarquez que la plupart de ces éléments ne sont que du texte standard pour soutenir l'exemple (générer des entrées aléatoires).

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

Éléments de données :

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 votes

Wow ! Est-ce que tu viens d'écrire ça ? ! C'est vraiment incroyable. Je viens de le tester et c'est à peu près la réponse parfaite à ma question. Il semble que l'étoffement des détails devrait être direct. Je suis époustouflé. Merci beaucoup !

1 votes

Si vous pouviez être loué pour répondre à ce genre de questions de temps en temps, je serais très heureux de payer :-)

1 votes

Un problème que j'ai rencontré est que lorsque je faisais défiler la molette de la souris sur un élément imbriqué, la vue ne défilait pas.

25voto

drizin Points 637

La réponse de HighCore est parfaite, mais je pense qu'il manque cette exigence : "lorsqu'il est à la fin, il devrait continuer à défiler lorsque de nouveaux éléments sont ajoutés".

Selon este réponse, vous pouvez le faire :

Dans le ScrollViewer principal (à l'intérieur du DockPanel), ajoutez l'événement :

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

Coulez la source de l'événement pour effectuer le défilement automatique :

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

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X