简体   繁体   English

如何加快垂直滚动条标记的渲染

[英]How to speed up rendering of vertical scrollbar markers

I have a customized vertical scrollbar which displays markers for selected items in a DataGrid. 我有一个自定义的垂直滚动条,它显示DataGrid中所选项目的标记。

在此处输入图片说明

The problem I'm facing is, when there are a great number of items (eg could be 5000 to 50000) there is a lag while it is rendering the markers. 我面临的问题是,当有大量项目(例如可能为5000至50000)时,在渲染标记时会出现滞后。

With the following code it basically renders as per the selected items index, number of items and height of the track. 使用以下代码,它基本上根据选定的项目索引,项目数和轨道高度进行渲染。 Obviously this is inefficient and am looking for other solutions. 显然这是低效的,正在寻找其他解决方案。

This is my customized vertical scrollbar 这是我自定义的垂直滚动条

<helpers:MarkerPositionConverter x:Key="MarkerPositionConverter"/>

<ControlTemplate x:Key="VerticalScrollBar" TargetType="{x:Type ScrollBar}">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition MaxHeight="18" />
            <RowDefinition Height="0.00001*" />
            <RowDefinition MaxHeight="18" />
        </Grid.RowDefinitions>
        <Border Grid.RowSpan="3"
                        CornerRadius="2"
                        Background="#F0F0F0" />
        <RepeatButton Grid.Row="0"
                              Style="{StaticResource ScrollBarLineButton}"
                              Height="18"
                              Command="ScrollBar.LineUpCommand"
                              Content="M 0 4 L 8 4 L 4 0 Z" />
        <!--START-->
        <ItemsControl VerticalAlignment="Stretch"  x:Name="ItemsSelected"
                                   ItemsSource="{Binding ElementName=GenericDataGrid, Path=SelectedItems}">
            <ItemsControl.ItemTemplate>
                <DataTemplate>
                    <Rectangle Fill="SlateGray" Width="9" Height="4">
                        <Rectangle.RenderTransform>
                            <TranslateTransform>
                                <TranslateTransform.Y>
                                    <MultiBinding Converter="{StaticResource MarkerPositionConverter}" FallbackValue="-1000">
                                        <Binding/>
                                        <Binding RelativeSource="{RelativeSource AncestorType={x:Type DataGrid}}" />
                                        <Binding Path="ActualHeight" ElementName="ItemsSelected"/>
                                        <Binding Path="Items.Count" ElementName="GenericDataGrid"/>
                                    </MultiBinding>
                                </TranslateTransform.Y>
                            </TranslateTransform>
                        </Rectangle.RenderTransform>
                    </Rectangle>
                </DataTemplate>
            </ItemsControl.ItemTemplate>
            <ItemsControl.ItemsPanel>
                <ItemsPanelTemplate>
                    <Canvas ClipToBounds="True"/>
                </ItemsPanelTemplate>
            </ItemsControl.ItemsPanel>
        </ItemsControl>
        <!--END-->
        <Track x:Name="PART_Track" Grid.Row="1" IsDirectionReversed="true">
            <Track.DecreaseRepeatButton>
                <RepeatButton Style="{StaticResource ScrollBarPageButton}"
                                      Command="ScrollBar.PageUpCommand" />
            </Track.DecreaseRepeatButton>
            <Track.Thumb>
                <Thumb Style="{StaticResource ScrollBarThumb}" Margin="1,0,1,0">
                    <Thumb.BorderBrush>
                        <LinearGradientBrush StartPoint="0,0" EndPoint="1,0">
                            <LinearGradientBrush.GradientStops>
                                <GradientStopCollection>
                                    <GradientStop Color="{DynamicResource BorderLightColor}" Offset="0.0" />
                                    <GradientStop Color="{DynamicResource BorderDarkColor}" Offset="1.0" />
                                </GradientStopCollection>
                            </LinearGradientBrush.GradientStops>
                        </LinearGradientBrush>
                    </Thumb.BorderBrush>
                    <Thumb.Background>
                        <LinearGradientBrush StartPoint="0,0"
                                 EndPoint="1,0">
                            <LinearGradientBrush.GradientStops>
                                <GradientStopCollection>
                                    <GradientStop Color="{DynamicResource ControlLightColor}" Offset="0.0" />
                                    <GradientStop Color="{DynamicResource ControlMediumColor}" Offset="1.0" />
                                </GradientStopCollection>
                            </LinearGradientBrush.GradientStops>
                        </LinearGradientBrush>
                    </Thumb.Background>
                </Thumb>
            </Track.Thumb>
            <Track.IncreaseRepeatButton>
                <RepeatButton Style="{StaticResource ScrollBarPageButton}" Command="ScrollBar.PageDownCommand" />
            </Track.IncreaseRepeatButton>
        </Track>
        <RepeatButton Grid.Row="3" Style="{StaticResource ScrollBarLineButton}" Height="18" Command="ScrollBar.LineDownCommand" Content="M 0 0 L 4 4 L 8 0 Z" />
    </Grid>
</ControlTemplate>

This is my converter that transforms the Y position and scales accordingly if the DataGrid height changes. 这是我的转换器,可以转换Y位置并在DataGrid高度更改时相应地缩放。

public class MarkerPositionConverter: IMultiValueConverter
{
    //Performs the index to translate conversion
    public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        try
        {
            //calculated the transform values based on the following
            object o = (object)values[0];
            DataGrid dg = (DataGrid)values[1];
            double itemIndex = dg.Items.IndexOf(o);
            double trackHeight = (double)values[2];
            int itemCount = (int)values[3];
            double translateDelta = trackHeight / itemCount;
            return itemIndex * translateDelta;
        }
        catch (Exception ex)
        {
            Console.WriteLine("MarkerPositionConverter error : " + ex.Message);
            return false;
        }
    }

    public object[] ConvertBack(object value, Type[] targetTypes, object parameter, System.Globalization.CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}


[RE-EDIT] I have tried to create a separate class for a marker canvas, for use with ObservableCollection's. [重新编辑]我试图为标记画布创建一个单独的类,以与ObservableCollection一起使用。 Note that at present, this does not work. 请注意,目前这不起作用。

XAML still the same as yesterday: XAML与昨天相同:

<helpers:MarkerCollectionCanvas 
         x:Name="SearchMarkerCanvas" 
         Grid.Row="1" 
         Grid="{Binding RelativeSource={RelativeSource AncestorType={x:Type DataGrid}}}"
         MarkerCollection="{Binding Source={x:Static helpers:MyClass.Instance}, Path=SearchMarkers}"/>

Canvas class, ObservableCollection changed to use object instead of double, there is a console.writeline in MarkerCollectionCanvas_CollectionChanged that never gets called: Canvas类的ObservableCollection更改为使用object而不是double,MarkerCollectionCanvas_CollectionChanged中有一个console.writeline,它永远不会被调用:

class MarkerCollectionCanvas : Canvas
{
    public DataGrid Grid
    {
        get { return (DataGrid)GetValue(GridProperty); }
        set { SetValue(GridProperty, value); }
    }

    public static readonly DependencyProperty GridProperty =
        DependencyProperty.Register("Grid", typeof(DataGrid), typeof(MarkerCollectionCanvas), new PropertyMetadata(null));

    public ObservableCollection<object> MarkerCollection
    {
        get { return (ObservableCollection<object>)GetValue(MarkerCollectionProperty); }
        set { SetValue(MarkerCollectionProperty, value); }
    }

    public static readonly DependencyProperty MarkerCollectionProperty =
        DependencyProperty.Register("MarkerCollection", typeof(ObservableCollection<object>), typeof(MarkerCollectionCanvas), new PropertyMetadata(null, OnCollectionChanged));

    private static void OnCollectionChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        MarkerCollectionCanvas canvas = d as MarkerCollectionCanvas;

        if (e.NewValue != null)
        {
            (e.NewValue as ObservableCollection<object>).CollectionChanged += canvas.MarkerCollectionCanvas_CollectionChanged;
        }
        if (e.OldValue != null)
        {
            (e.NewValue as ObservableCollection<object>).CollectionChanged -= canvas.MarkerCollectionCanvas_CollectionChanged;
        }
    }

    void MarkerCollectionCanvas_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
    {
        Console.WriteLine("InvalidateVisual");
        InvalidateVisual();
    }

    public Brush MarkerBrush
    {
        get { return (Brush)GetValue(MarkerBrushProperty); }
        set { SetValue(MarkerBrushProperty, value); }
    }

    public static readonly DependencyProperty MarkerBrushProperty =
        DependencyProperty.Register("MarkerBrush", typeof(Brush), typeof(MarkerCollectionCanvas), new PropertyMetadata(Brushes.DarkOrange));

    protected override void OnRender(System.Windows.Media.DrawingContext dc)
    {
        base.OnRender(dc);

        if (Grid == null || MarkerCollection == null)
            return;

        //Get all items
        object[] items = new object[Grid.Items.Count];
        Grid.Items.CopyTo(items, 0);

        //Get all selected items
        object[] selection = new object[MarkerCollection.Count];
        MarkerCollection.CopyTo(selection, 0);

        Dictionary<object, int> indexes = new Dictionary<object, int>();

        for (int i = 0; i < selection.Length; i++)
        {
            indexes.Add(selection[i], 0);
        }

        int itemCounter = 0;
        for (int i = 0; i < items.Length; i++)
        {
            object item = items[i];
            if (indexes.ContainsKey(item))
            {
                indexes[item] = i;
                itemCounter++;
            }
            if (itemCounter >= selection.Length)
                break;
        }

        double translateDelta = ActualHeight / (double)items.Length;
        double width = ActualWidth;
        double height = Math.Max(translateDelta, 4);
        Brush dBrush = MarkerBrush;
        double previous = 0;

        IEnumerable<int> sortedIndex = indexes.Values.OrderBy(v => v);

        foreach (int itemIndex in sortedIndex)
        {
            double top = itemIndex * translateDelta;
            if (top < previous)
                continue;

            dc.DrawRectangle(dBrush, null, new Rect(0, top, width, height));
            previous = (top + height) - 1;
        }
    }
}

This is my singleton class with SearchMarkers in it: 这是我的单身人士课程,其中包含SearchMarkers:

public class MyClass : INotifyPropertyChanged
{
    public static ObservableCollection<object> m_searchMarkers = new ObservableCollection<object>();
    public ObservableCollection<object> SearchMarkers
    {
        get
        {
            return m_searchMarkers;
        }
        set
        {
            m_searchMarkers = value;
            NotifyPropertyChanged();
        }
    }

    private static MyClass m_Instance;
    public static MyClass Instance
    {
        get
        {
            if (m_Instance == null)
            {
                m_Instance = new MyClass();
            }

            return m_Instance;
        }
    }

    private MyClass()
    {
    }

    public event PropertyChangedEventHandler PropertyChanged;

    private void NotifyPropertyChanged([CallerMemberName] String propertyName = "")
    {
        if (PropertyChanged != null)
        {
            PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

And this is a textbox text changed behavior. 而这是一个文本框的文本更改行为。 This is where the ObservableCollection SearchMarkers gets populated. 这是ObservableCollection SearchMarkers的填充位置。

public class FindTextChangedBehavior : Behavior<TextBox>
{
    protected override void OnAttached()
    {
        base.OnAttached();
        AssociatedObject.TextChanged += OnTextChanged;
    }

    protected override void OnDetaching()
    {
        AssociatedObject.TextChanged -= OnTextChanged;
        base.OnDetaching();
    }

    private void OnTextChanged(object sender, TextChangedEventArgs args)
    {
        var textBox = (sender as TextBox);
        if (textBox != null)
        {
            DataGrid dg = DataGridObject as DataGrid;
            string searchValue = textBox.Text;

            if (dg.Items.Count > 0)
            {
                var columnBoundProperties = new List<KeyValuePair<int, string>>();

                IEnumerable<DataGridColumn> visibleColumns = dg.Columns.Where(c => c.Visibility == System.Windows.Visibility.Visible);

                foreach (var col in visibleColumns)
                {
                    if (col is DataGridTextColumn)
                    {
                        var binding = (col as DataGridBoundColumn).Binding as Binding;
                        columnBoundProperties.Add(new KeyValuePair<int, string>(col.DisplayIndex, binding.Path.Path));
                    }
                    else if (col is DataGridComboBoxColumn)
                    {
                        DataGridComboBoxColumn dgcbc = (DataGridComboBoxColumn)col;
                        var binding = dgcbc.SelectedItemBinding as Binding;
                        columnBoundProperties.Add(new KeyValuePair<int, string>(col.DisplayIndex, binding.Path.Path));
                    }
                }

                Type itemType = dg.Items[0].GetType();
                if (columnBoundProperties.Count > 0)
                {
                    ObservableCollection<Object> tempItems = new ObservableCollection<Object>();
                    var itemsSource = dg.Items as IEnumerable;
                    Task.Factory.StartNew(() =>
                    {
                        ClassPropTextSearch.init(itemType, columnBoundProperties);
                        if (itemsSource != null)
                        {
                            foreach (object o in itemsSource)
                            {
                                if (ClassPropTextSearch.Match(o, searchValue))
                                {
                                    tempItems.Add(o);
                                }
                            }
                        }
                    })
                    .ContinueWith(t =>
                    {
                        Application.Current.Dispatcher.Invoke(new Action(() => MyClass.Instance.SearchMarkers = tempItems));
                    });
                }
            }
        }
    }

    public static readonly DependencyProperty DataGridObjectProperty =
        DependencyProperty.RegisterAttached("DataGridObject", typeof(DataGrid), typeof(FindTextChangedBehavior), new UIPropertyMetadata(null));

    public object DataGridObject
    {
        get { return (object)GetValue(DataGridObjectProperty); }
        set { SetValue(DataGridObjectProperty, value); }
    }
}

Here you go, I tried to attempt it for you. 在这里,我尝试为您尝试。

  • Created a class MarkerCanvas deriving Canvas with a property to bind with the data grid 创建了一个MarkerCanvas类,该类派生Canvas具有与数据网格绑定的属性
  • Attached SelectionChanged to listen to any change and requested the canvas to redraw itself by InvalidateVisual 附加了SelectionChanged以侦听任何更改,并请求InvalidateVisual重绘画布
  • overrided the method OnRender to take control of drawing and did the necessary check and calculation 重写了OnRender方法来控制图形,并进行了必要的检查和计算
  • finally rendered the rectangle on the calculated coordinates using the given brush 最后使用给定的画笔在计算的坐标上绘制矩形

MarkerCanvas class MarkerCanvas类

class MarkerCanvas : Canvas
{
    public DataGrid Grid
    {
        get { return (DataGrid)GetValue(GridProperty); }
        set { SetValue(GridProperty, value); }
    }

    // Using a DependencyProperty as the backing store for Grid.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty GridProperty =
        DependencyProperty.Register("Grid", typeof(DataGrid), typeof(MarkerCanvas), new PropertyMetadata(null, OnGridChanged));

    private static void OnGridChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        MarkerCanvas canvas = d as MarkerCanvas;

        if (e.NewValue != null)
        {
            (e.NewValue as DataGrid).SelectionChanged += canvas.MarkerCanvas_SelectionChanged;
        }
        if (e.OldValue != null)
        {
            (e.NewValue as DataGrid).SelectionChanged -= canvas.MarkerCanvas_SelectionChanged;
        }
    }

    void MarkerCanvas_SelectionChanged(object sender, SelectionChangedEventArgs e)
    {
        InvalidateVisual();
    }

    public Brush MarkerBrush
    {
        get { return (Brush)GetValue(MarkerBrushProperty); }
        set { SetValue(MarkerBrushProperty, value); }
    }

    // Using a DependencyProperty as the backing store for MarkerBrush.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty MarkerBrushProperty =
        DependencyProperty.Register("MarkerBrush", typeof(Brush), typeof(MarkerCanvas), new PropertyMetadata(Brushes.SlateGray));


    protected override void OnRender(System.Windows.Media.DrawingContext dc)
    {
        base.OnRender(dc);

        if (Grid==null || Grid.SelectedItems == null)
            return;

        object[] markers = Grid.SelectedItems.OfType<object>().ToArray();
        double translateDelta = ActualHeight / (double)Grid.Items.Count;
        double width = ActualWidth;
        double height = Math.Max(translateDelta, 4);
        Brush dBrush = MarkerBrush;

        for (int i = 0; i < markers.Length; i++)
        {
            double itemIndex = Grid.Items.IndexOf(markers[i]);
            double top = itemIndex * translateDelta;

            dc.DrawRectangle(dBrush, null, new Rect(0, top, width, height));
        }
    }
}

I have also adjusted the height of marker so it grows when there are less items, you can choose to fix it to specific value as per your needs 我还调整了标记的高度,以便当物品较少时它会增加,您可以根据需要选择将其固定为特定值

in XAML replace your items control with the new marker canvas with binding to the grid 在XAML中,将您的项目控件替换为具有绑定到网格的新标记画布

<helpers:MarkerCanvas Grid.Row="1" Grid="{Binding RelativeSource={RelativeSource AncestorType={x:Type DataGrid}}}"/>

helpers: is referring to WpfAppDataGrid.Helpers where I create the class, you can choose your own namespace helpers:指的是WpfAppDataGrid.Helpers,在我创建类的地方,您可以选择自己的名称空间

also you can bind the MarkerBrush property for your desired effet, which defaulted to SlateGray 您还可以为所需的效果绑定MarkerBrush属性,默认为SlateGray

rendering is pretty fast now, perhaps could make it more fast by doing some work on indexof method. 现在渲染速度非常快,也许可以通过对indexof方法做一些工作来使其更快。

Also to skip some of the overlapping rectangles to be rendered you can change the method like this. 此外,也可以跳过某些要渲染的重叠矩形,可以更改这种方法。 little buggy as of now

protected override void OnRender(System.Windows.Media.DrawingContext dc)
{
    base.OnRender(dc);

    if (Grid==null || Grid.SelectedItems == null)
        return;

    object[] markers = Grid.SelectedItems.OfType<object>().ToArray();
    double translateDelta = ActualHeight / (double)Grid.Items.Count;
    double width = ActualWidth;
    double height = Math.Max(translateDelta, 4);
    Brush dBrush = MarkerBrush;
    double previous = 0;

    for (int i = 0; i < markers.Length; i++)
    {
        double itemIndex = Grid.Items.IndexOf(markers[i]);
        double top = itemIndex * translateDelta;
        if (top < previous)
            continue;
        dc.DrawRectangle(dBrush, null, new Rect(0, top, width, height));
        previous = (top + height) - 1;
    }
}

Performance optimization 性能优化

I tried to optimize the performance using a slight different approach, specially for the select all button 我尝试使用略有不同的方法来优化性能,特别是针对全选按钮

protected override void OnRender(System.Windows.Media.DrawingContext dc)
{
    base.OnRender(dc);

    if (Grid == null || Grid.SelectedItems == null)
        return;

    object[] items = new object[Grid.Items.Count];
    Grid.Items.CopyTo(items, 0);

    object[] selection = new object[Grid.SelectedItems.Count];
    Grid.SelectedItems.CopyTo(selection, 0);

    Dictionary<object, int> indexes = new Dictionary<object, int>();

    for (int i = 0; i < selection.Length; i++)
    {
        indexes.Add(selection[i], 0);
    }

    int itemCounter = 0;
    for (int i = 0; i < items.Length; i++)
    {
        object item = items[i];
        if (indexes.ContainsKey(item))
        {
            indexes[item] = i;
            itemCounter++;
        }
        if (itemCounter >= selection.Length)
            break;
    }

    double translateDelta = ActualHeight / (double)items.Length;
    double width = ActualWidth;
    double height = Math.Max(translateDelta, 4);
    Brush dBrush = MarkerBrush;
    double previous = 0;

    IEnumerable<int> sortedIndex = indexes.Values.OrderBy(v => v);

    foreach (int itemIndex in sortedIndex)
    {
        double top = itemIndex * translateDelta;
        if (top < previous)
            continue;

        dc.DrawRectangle(dBrush, null, new Rect(0, top, width, height));
        previous = (top + height) - 1;
    }
}

Reflection approach 反思方式

in this I have tried to get the underlying selection list and attempted to retrieve the selected indexes from the same, also added even more optimization when doing select all 在这种情况下,我尝试获取基础选择列表,并尝试从中检索选择的索引,并且在执行全选时还添加了更多优化

protected override void OnRender(System.Windows.Media.DrawingContext dc)
{
    base.OnRender(dc);

    if (Grid == null || Grid.SelectedItems == null)
        return;

    List<int> indexes = new List<int>();
    double translateDelta = ActualHeight / (double)Grid.Items.Count;
    double height = Math.Max(translateDelta, 4);
    int itemInOneRect = (int)Math.Floor(height / translateDelta);
    itemInOneRect -= (int)(itemInOneRect * 0.2);
    if (Grid.SelectedItems.Count == Grid.Items.Count)
    {
        for (int i = 0; i < Grid.Items.Count; i += itemInOneRect)
        {
            indexes.Add(i);
        }
    }
    else
    {
        FieldInfo fi = Grid.GetType().GetField("_selectedItems", BindingFlags.NonPublic | BindingFlags.FlattenHierarchy | BindingFlags.Instance);
        IEnumerable<object> internalSelectionList = fi.GetValue(Grid) as IEnumerable<object>;
        PropertyInfo pi = null;
        int lastIndex = int.MinValue;
        foreach (var item in internalSelectionList)
        {
            if (pi == null)
            {
                pi = item.GetType().GetProperty("Index", BindingFlags.Instance | BindingFlags.NonPublic);
            }
            int newIndex = (int)pi.GetValue(item);
            if (newIndex > (lastIndex + itemInOneRect))
            {
                indexes.Add(newIndex);
                lastIndex = newIndex;
            }
        }
        indexes.Sort();
    }

    double width = ActualWidth;
    Brush dBrush = MarkerBrush;

    foreach (int itemIndex in indexes)
    {
        double top = itemIndex * translateDelta;
        dc.DrawRectangle(dBrush, null, new Rect(0, top, width, height));
    }
}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM