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