WPF custom VirtualizingGridPanel inherited from VirtualizingPanel and IScrollInfo,MeasureOverride and ArrangeOverride

public class VirtualizingGridPanel : VirtualizingPanel, IScrollInfo
{
    public double ColumnWidth { get; set; } = 200;
    public double ItemHeight { get; set; } = 80;
    public int ColumnCount { get; set; } = 2;

    private double _verticalOffset;
    private Size _extent = new Size(0, 0);
    private Size _viewport = new Size(0, 0);
    private ScrollViewer _owner;
    private bool isInitialized = false;
    public VirtualizingGridPanel()
    {
        this.IsItemsHost = true;
        this.Loaded += VirtualizingGridPanel_Loaded;
    }

    private void VirtualizingGridPanel_Loaded(object sender, RoutedEventArgs e)
    {
        if(!isInitialized)
        {
            isInitialized = true;
            InvalidateMeasure();
            InvalidateArrange();
            _owner?.InvalidateArrange();
        }
    }

    #region IScrollInfo 
    public bool CanVerticallyScroll { get; set; } = true;
    public bool CanHorizontallyScroll { get; set; } = false;
    public double ExtentHeight => _extent.Height;
    public double ExtentWidth => _extent.Width;
    public double ViewportHeight => _viewport.Height;
    public double ViewportWidth => _viewport.Width;
    public double VerticalOffset => _verticalOffset;
    public double HorizontalOffset => 0;
    public ScrollViewer ScrollOwner 
    {
        get => _owner;
        set 
        {
            _owner = value;
            if(_owner!=null)
            {
                InvalidateMeasure();
            }
        }
    }

    public void SetVerticalOffset(double offset)
    {
        if (offset < 0 || _viewport.Height >= _extent.Height)
        {
            offset = 0;
        }
        else if (offset + _viewport.Height >= _extent.Height)
        {
            offset = _extent.Height - _viewport.Height;
        }

        _verticalOffset = offset;
        _owner?.InvalidateScrollInfo();
        InvalidateMeasure();
    }

    public void MouseWheelDown() => SetVerticalOffset(VerticalOffset + ItemHeight);
    public void MouseWheelUp() => SetVerticalOffset(VerticalOffset - ItemHeight);
    public void LineDown() => SetVerticalOffset(VerticalOffset + ItemHeight);
    public void LineUp() => SetVerticalOffset(VerticalOffset - ItemHeight);
    public void PageDown() => SetVerticalOffset(VerticalOffset + ViewportHeight);
    public void PageUp() => SetVerticalOffset(VerticalOffset - ViewportHeight);
    public void SetHorizontalOffset(double offset) { }
    public void LineLeft() { }
    public void LineRight() { }
    public void PageLeft() { }
    public void PageRight() { }
    public void MouseWheelLeft() { }
    public void MouseWheelRight() { }
    public Rect MakeVisible(Visual visual, Rect rectangle) => rectangle;
    #endregion

    protected override Size MeasureOverride(Size availableSize)
    {
        var itemsControl = ItemsControl.GetItemsOwner(this);
        if (itemsControl == null)
        {
            return availableSize;
        }

        int itemCount = itemsControl.Items.Count;
         
        _viewport = availableSize;
        if (double.IsInfinity(_viewport.Width))
        {
            _viewport.Width = ColumnWidth * ColumnCount;
        }

        if (double.IsInfinity(_viewport.Height))
        {
            _viewport.Height = 800;
        }

        int rowCount = (int)Math.Ceiling((double)itemCount / ColumnCount);
        _extent = new Size(ColumnCount * ColumnWidth, rowCount * ItemHeight);

        _owner?.InvalidateScrollInfo();
         
        int firstVisibleRow = (int)Math.Floor(VerticalOffset / ItemHeight);
        int lastVisibleRow = (int)Math.Ceiling((VerticalOffset + _viewport.Height) / ItemHeight);

        int startIndex = Math.Max(0, firstVisibleRow * ColumnCount);
        int endIndex = Math.Min(itemCount - 1, (lastVisibleRow + 1) * ColumnCount - 1);
                     
        RealizeItems(startIndex, endIndex);

        return new Size(
            Math.Min(availableSize.Width, _extent.Width),
            Math.Min(availableSize.Height, _extent.Height)
        );
    }

    private void RealizeItems(int startIndex, int endIndex)
    {
        if (startIndex>endIndex)
        {
            return;
        }

        IItemContainerGenerator generator = this.ItemContainerGenerator;
        if (generator == null)
        {
            return;
        }

        GeneratorPosition startPos = generator.GeneratorPositionFromIndex(startIndex);
        int childIndex = (startPos.Offset == 0) ? startPos.Index : startPos.Index + 1;

        using (generator.StartAt(startPos, GeneratorDirection.Forward, true))
        {
            for (int itemIndex = startIndex; itemIndex <= endIndex; itemIndex++, childIndex++)
            {
                UIElement child = generator.GenerateNext(out bool newlyRealized) as UIElement;
                if (child == null)
                {
                    continue;
                }

                if (newlyRealized)
                {
                    if (childIndex >= InternalChildren.Count)
                    {
                        AddInternalChild(child);
                    }                            
                    else
                    {
                        InsertInternalChild(childIndex, child);
                    }
                    generator.PrepareItemContainer(child);
                }
                child.Measure(new Size(ColumnWidth, ItemHeight));
            }
        }
         
        for (int i = InternalChildren.Count - 1; i >= 0; i--)
        {
            int itemIndex = generator.IndexFromGeneratorPosition(new GeneratorPosition(i, 0));
            if (itemIndex < startIndex || itemIndex > endIndex)
            {
                generator.Remove(new GeneratorPosition(i, 0), 1);
                RemoveInternalChildRange(i, 1);
            }
        }
    }

    protected override Size ArrangeOverride(Size finalSize)
    {
        IItemContainerGenerator generator = this.ItemContainerGenerator;

        for (int i = 0; i < InternalChildren.Count; i++)
        {
            UIElement child = InternalChildren[i];
            int itemIndex = generator.IndexFromGeneratorPosition(new GeneratorPosition(i, 0));

            if (itemIndex < 0)
            {
                continue;
            }

            int row = itemIndex / ColumnCount;
            int col = itemIndex % ColumnCount;

            double x = col * ColumnWidth;
            double y = row * ItemHeight - VerticalOffset;

            child.Arrange(new Rect(x, y, ColumnWidth, ItemHeight));
        }
        return finalSize;
    }

    protected override void OnItemsChanged(object sender, ItemsChangedEventArgs args)
    {
        switch (args.Action)
        {
            case System.Collections.Specialized.NotifyCollectionChangedAction.Remove:
            case System.Collections.Specialized.NotifyCollectionChangedAction.Replace:
            case System.Collections.Specialized.NotifyCollectionChangedAction.Reset:
                InvalidateMeasure();
                break;
        }
    }
}

 

 

 

image

 

image

 

 

 

<Window x:Class="WpfApp2.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfApp2"
        mc:Ignorable="d"
        Title="{Binding MainTitle}" WindowState="Maximized">
    <Window.DataContext>
        <local:MainVM/>
    </Window.DataContext>
    <Grid>
        <ListBox ItemsSource="{Binding BooksCollection}"
                 VirtualizingPanel.IsVirtualizing="True"
                 VirtualizingPanel.VirtualizationMode="Recycling"
                 ScrollViewer.CanContentScroll="True">
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <Border BorderBrush="Gray"
                            BorderThickness="0.5"
                            Padding="4">
                        <TextBlock Text="{Binding ISBN}" FontSize="30"/>
                    </Border>
                </DataTemplate>
            </ListBox.ItemTemplate>
            <ListBox.ItemsPanel>
                <ItemsPanelTemplate>
                    <local:VirtualizingGridPanel
                        ColumnCount="1"
                        ColumnWidth="1200"
                        ItemHeight="80"/>
                </ItemsPanelTemplate>
            </ListBox.ItemsPanel>
        </ListBox>
    </Grid>
</Window>


using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace WpfApp2
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }
    }

    public class MainVM : INotifyPropertyChanged
    {
        public MainVM()
        {
            BooksCollection = new ObservableCollection<Book>();
            for (int i = 1; i < 50000001; i++)
            {
                BooksCollection.Add(new Book() { ISBN = $"{i}_{Guid.NewGuid():N}" });
            }
            Task.Run(() =>
            {
                InitTimer();
            });
        }

        private void InitTimer()
        {
            System.Timers.Timer tmr = new System.Timers.Timer();
            tmr.Interval = 1000;
            tmr.Elapsed += Tmr_Elapsed;
            tmr.Start();
        }

        private void Tmr_Elapsed(object? sender, System.Timers.ElapsedEventArgs e)
        {
            MainTitle = GetMem();
        }

        private string GetMem()
        {
            System.Diagnostics.Process proc = Process.GetCurrentProcess();
            return $"{DateTime.Now},{proc.PrivateMemorySize64 / 1024 / 1024:F2} M";
        }

        private string mainTitle = "";
        public string MainTitle
        {
            get
            {
                return mainTitle;
            }
            set
            {
                if (value != mainTitle)
                {
                    mainTitle = value;
                    OnPropertyChanged();
                }
            }
        }

        private ObservableCollection<Book> booksCollection;
        public ObservableCollection<Book> BooksCollection
        {
            get => booksCollection;
            set { booksCollection = value; OnPropertyChanged(); }
        }

        public event PropertyChangedEventHandler PropertyChanged;
        public void OnPropertyChanged([CallerMemberName] string propertyName = "")
            => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }

    public class Book
    {
        public string ISBN { get; set; }
    }

    public class VirtualizingGridPanel : VirtualizingPanel, IScrollInfo
    {
        public double ColumnWidth { get; set; } = 200;
        public double ItemHeight { get; set; } = 80;
        public int ColumnCount { get; set; } = 2;

        private double _verticalOffset;
        private Size _extent = new Size(0, 0);
        private Size _viewport = new Size(0, 0);
        private ScrollViewer _owner;
        private bool isInitialized = false;
        public VirtualizingGridPanel()
        {
            this.IsItemsHost = true;
            this.Loaded += VirtualizingGridPanel_Loaded;
        }

        private void VirtualizingGridPanel_Loaded(object sender, RoutedEventArgs e)
        {
            if(!isInitialized)
            {
                isInitialized = true;
                InvalidateMeasure();
                InvalidateArrange();
                _owner?.InvalidateArrange();
            }
        }

        #region IScrollInfo 
        public bool CanVerticallyScroll { get; set; } = true;
        public bool CanHorizontallyScroll { get; set; } = false;
        public double ExtentHeight => _extent.Height;
        public double ExtentWidth => _extent.Width;
        public double ViewportHeight => _viewport.Height;
        public double ViewportWidth => _viewport.Width;
        public double VerticalOffset => _verticalOffset;
        public double HorizontalOffset => 0;
        public ScrollViewer ScrollOwner 
        {
            get => _owner;
            set 
            {
                _owner = value;
                if(_owner!=null)
                {
                    InvalidateMeasure();
                }
            }
        }

        public void SetVerticalOffset(double offset)
        {
            if (offset < 0 || _viewport.Height >= _extent.Height)
            {
                offset = 0;
            }
            else if (offset + _viewport.Height >= _extent.Height)
            {
                offset = _extent.Height - _viewport.Height;
            }

            _verticalOffset = offset;
            _owner?.InvalidateScrollInfo();
            InvalidateMeasure();
        }

        public void MouseWheelDown() => SetVerticalOffset(VerticalOffset + ItemHeight);
        public void MouseWheelUp() => SetVerticalOffset(VerticalOffset - ItemHeight);
        public void LineDown() => SetVerticalOffset(VerticalOffset + ItemHeight);
        public void LineUp() => SetVerticalOffset(VerticalOffset - ItemHeight);
        public void PageDown() => SetVerticalOffset(VerticalOffset + ViewportHeight);
        public void PageUp() => SetVerticalOffset(VerticalOffset - ViewportHeight);
        public void SetHorizontalOffset(double offset) { }
        public void LineLeft() { }
        public void LineRight() { }
        public void PageLeft() { }
        public void PageRight() { }
        public void MouseWheelLeft() { }
        public void MouseWheelRight() { }
        public Rect MakeVisible(Visual visual, Rect rectangle) => rectangle;
        #endregion

        protected override Size MeasureOverride(Size availableSize)
        {
            var itemsControl = ItemsControl.GetItemsOwner(this);
            if (itemsControl == null)
            {
                return availableSize;
            }

            int itemCount = itemsControl.Items.Count;
             
            _viewport = availableSize;
            if (double.IsInfinity(_viewport.Width))
            {
                _viewport.Width = ColumnWidth * ColumnCount;
            }

            if (double.IsInfinity(_viewport.Height))
            {
                _viewport.Height = 800;
            }

            int rowCount = (int)Math.Ceiling((double)itemCount / ColumnCount);
            _extent = new Size(ColumnCount * ColumnWidth, rowCount * ItemHeight);

            _owner?.InvalidateScrollInfo();
             
            int firstVisibleRow = (int)Math.Floor(VerticalOffset / ItemHeight);
            int lastVisibleRow = (int)Math.Ceiling((VerticalOffset + _viewport.Height) / ItemHeight);

            int startIndex = Math.Max(0, firstVisibleRow * ColumnCount);
            int endIndex = Math.Min(itemCount - 1, (lastVisibleRow + 1) * ColumnCount - 1);
                         
            RealizeItems(startIndex, endIndex);

            return new Size(
                Math.Min(availableSize.Width, _extent.Width),
                Math.Min(availableSize.Height, _extent.Height)
            );
        }

        private void RealizeItems(int startIndex, int endIndex)
        {
            if (startIndex>endIndex)
            {
                return;
            }

            IItemContainerGenerator generator = this.ItemContainerGenerator;
            if (generator == null)
            {
                return;
            }

            GeneratorPosition startPos = generator.GeneratorPositionFromIndex(startIndex);
            int childIndex = (startPos.Offset == 0) ? startPos.Index : startPos.Index + 1;

            using (generator.StartAt(startPos, GeneratorDirection.Forward, true))
            {
                for (int itemIndex = startIndex; itemIndex <= endIndex; itemIndex++, childIndex++)
                {
                    UIElement child = generator.GenerateNext(out bool newlyRealized) as UIElement;
                    if (child == null)
                    {
                        continue;
                    }

                    if (newlyRealized)
                    {
                        if (childIndex >= InternalChildren.Count)
                        {
                            AddInternalChild(child);
                        }                            
                        else
                        {
                            InsertInternalChild(childIndex, child);
                        }
                        generator.PrepareItemContainer(child);
                    }
                    child.Measure(new Size(ColumnWidth, ItemHeight));
                }
            }
             
            for (int i = InternalChildren.Count - 1; i >= 0; i--)
            {
                int itemIndex = generator.IndexFromGeneratorPosition(new GeneratorPosition(i, 0));
                if (itemIndex < startIndex || itemIndex > endIndex)
                {
                    generator.Remove(new GeneratorPosition(i, 0), 1);
                    RemoveInternalChildRange(i, 1);
                }
            }
        }

        protected override Size ArrangeOverride(Size finalSize)
        {
            IItemContainerGenerator generator = this.ItemContainerGenerator;

            for (int i = 0; i < InternalChildren.Count; i++)
            {
                UIElement child = InternalChildren[i];
                int itemIndex = generator.IndexFromGeneratorPosition(new GeneratorPosition(i, 0));

                if (itemIndex < 0)
                {
                    continue;
                }

                int row = itemIndex / ColumnCount;
                int col = itemIndex % ColumnCount;

                double x = col * ColumnWidth;
                double y = row * ItemHeight - VerticalOffset;

                child.Arrange(new Rect(x, y, ColumnWidth, ItemHeight));
            }
            return finalSize;
        }

        protected override void OnItemsChanged(object sender, ItemsChangedEventArgs args)
        {
            switch (args.Action)
            {
                case System.Collections.Specialized.NotifyCollectionChangedAction.Remove:
                case System.Collections.Specialized.NotifyCollectionChangedAction.Replace:
                case System.Collections.Specialized.NotifyCollectionChangedAction.Reset:
                    InvalidateMeasure();
                    break;
            }
        }
    }
}

 

posted @ 2026-03-03 22:13  FredGrit  阅读(7)  评论(0)    收藏  举报