How to speed up rendering of vertical scrollbar markers

十年热恋 提交于 2020-01-20 07:26:40

问题


I have a customized vertical scrollbar which displays markers for selected items in a DataGrid.

The problem I'm facing is, when there are a great number of items (e.g. could be 5000 to 50000) there is a lag while it is rendering the markers.

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.

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. Note that at present, this does not work.

XAML still the same as yesterday:

<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:

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:

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.

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

回答1:


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
  • Attached SelectionChanged to listen to any change and requested the canvas to redraw itself by InvalidateVisual
  • overrided the method OnRender to take control of drawing and did the necessary check and calculation
  • finally rendered the rectangle on the calculated coordinates using the given brush

MarkerCanvas class

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

<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

also you can bind the MarkerBrush property for your desired effet, which defaulted to SlateGray

rendering is pretty fast now, perhaps could make it more fast by doing some work on indexof method.

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


来源:https://stackoverflow.com/questions/24318971/how-to-speed-up-rendering-of-vertical-scrollbar-markers

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!