74

WPF でコンソール ログ ビューアーを実装するための最善の方法についてアドバイスを求めます。

次の基準に一致する必要があります。

  • 100.000 行以上の高速スクロール
  • 一部のエントリ (スタック トレースなど) は折りたたみ可能にする必要があります
  • 長いアイテムのラップ
  • リストはさまざまな基準 (検索、タグなど) でフィルタリングできます。
  • 最後に、新しいアイテムが追加されたときにスクロールし続ける必要があります
  • 行要素には、ハイパーリンクやオカレンス カウンターなどの追加の書式設定を含めることができます

一般的には、FireBug や Chrome のコンソール ウィンドウのようなものを念頭に置いています。

私はこれをいじりましたが、あまり進歩しませんでした... - データグリッドはさまざまなアイテムの高さを処理できません - スクロール位置はスクロールバーを離した後にのみ更新されます (これは完全に受け入れられません)。

なんらかの形の仮想化が必要であり、MVVM パターンに従いたいと思っています。

どんな助けや指針も大歓迎です。

4

2 に答える 2

206

これらの WPF サンプルを無料で配布するのではなく、販売を開始する必要があります。=P

ここに画像の説明を入力

  • 信じられないほど優れたパフォーマンスを提供する仮想化された UI ( を使用VirtualizingStackPanel) (200000 以上のアイテムでも)
  • 完全に MVVM フレンドリー。
  • DataTemplateタイプの種類ごとに s LogEntry。これらにより、必要なだけカスタマイズすることができます。私は 2 種類の 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; }
}

プロパティ変更ベース:

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));
        }));
    }
}
于 2013-05-24T23:48:46.060 に答える
25

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);
        }
    }
}
于 2015-12-06T22:34:47.353 に答える