sourcetip

WPF를 사용한 로그뷰어 구현

fileupload 2023. 4. 13. 21:05
반응형

WPF를 사용한 로그뷰어 구현

WPF를 사용하여 콘솔로그 뷰어를 구현하기 위한 최선의 접근법에 대한 조언을 구합니다.

다음 기준과 일치해야 합니다.

  • 100.000 이상의 행을 사용한 고속 스크롤
  • 일부 엔트리(스택트레이스 등)는 접을 수 있어야 합니다.
  • 긴 아이템 포장
  • 리스트는 다른 기준(예: 태그, 태그 등)으로 필터링할 수 있습니다.
  • 마지막에 새로운 아이템이 추가되었을 때 스크롤이 계속됩니다.
  • 라인 요소에는 하이퍼링크나 오카렌스 카운터 등의 추가 포맷이 포함될 수 있습니다.

일반적으로 FireBug나 Chrome의 콘솔 창과 같은 것을 생각하고 있습니다.

이걸로 장난을 쳤지만 별로 진전이 없었어요. 왜냐하면...- 데이터 그리드가 다른 항목 높이를 처리할 수 없음 - 스크롤 막대를 해제한 후에만 스크롤 위치가 업데이트됩니다(완전히 허용되지 않음).

분명 어떤 형태로든 가상화가 필요하며 MVVM 패턴을 따르고 싶습니다.

어떤 도움말이나 포인터라도 환영합니다.

저는 이 WPF 샘플을 공짜로 주는 것이 아니라 판매를 시작해야 합니다.=P

여기에 이미지 설명 입력

  • 가상화된 UI(사용방법)VirtualizingStackPanel200000개 이상의 아이템을 사용해도 놀라운 퍼포먼스를 발휘합니다.
  • MVVM에 매우 적합합니다.
  • DataTemplate의 종류별로LogEntry원하는 만큼 커스터마이즈할 수 있습니다.LogEntries는 2종류(기본과 네스트)밖에 실장하지 않았습니다만, 이해하실 수 있습니다.서브클래스를 사용할 수 있습니다.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; }
}

Property Changed Base:

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

언급URL : https://stackoverflow.com/questions/16743804/implementing-a-log-viewer-with-wpf

반응형