对于一个项目,我们遇到了Grid
的需求,它可以快速加载数千个用户控件。在google方式的同时,我遇到了很多关于使用WPF和VirtualizingStackPanel
进行虚拟化的文章。
最终我到了这个stackoverflow post,它使用ItemsControl
和VirtualizingStackPanel
组合来虚拟化垂直方向上的所有项目。对于我们的项目,我们需要它在两个方向上进行虚拟化,因为输入数据可以扩展数万行,数万列或两者。我试图从堆栈帖子中调整代码,结果是:
<ItemsControl ItemsSource="{Binding Rows}"
ScrollViewer.HorizontalScrollBarVisibility="Auto"
ScrollViewer.VerticalScrollBarVisibility="Auto"
ScrollViewer.CanContentScroll="true"
ScrollViewer.PanningMode="Both">
<ItemsControl.Template>
<ControlTemplate>
<ScrollViewer>
<ItemsPresenter/>
</ScrollViewer>
</ControlTemplate>
</ItemsControl.Template>
<ItemsControl.ItemTemplate>
<DataTemplate>
<ItemsControl ItemsSource="{Binding Items}"
ScrollViewer.HorizontalScrollBarVisibility="Auto"
ScrollViewer.VerticalScrollBarVisibility="Auto"
ScrollViewer.CanContentScroll="true"
ScrollViewer.PanningMode="Both">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Label Content="{Binding Text}"
Width="{Binding Width}"
Height="{Binding Height}">
</Label>
</DataTemplate>
</ItemsControl.ItemTemplate>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<VirtualizingStackPanel Orientation="Horizontal" VirtualizationMode="Standard" IsVirtualizing="True"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
</DataTemplate>
</ItemsControl.ItemTemplate>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<VirtualizingStackPanel Orientation="Vertical" VirtualizationMode="Standard" IsVirtualizing="True"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
这似乎不起作用,因为每次向下滚动时都会加载水平方向上的所有项目。换句话说,水平方向的项目不会虚拟化。
在尝试创建VirtualizingGrid的过程中,我还考虑了实施VirtualizingPanel
和IScrollInfo
以及所做的here。但似乎ItemContainerGenerator
类不是为了使用2D源而构建的,它只给出了下一个项目&#34;。根据我的理解,这意味着它只能一次水平或垂直工作。所以我认为嵌套两个VirtualizingPanel
是唯一的选择。
我的问题是,为什么上面代码段中的嵌套VirtualizingPanel
不起作用?我究竟做错了什么?有没有办法实现一个完全虚拟化的网格&#34;不知何故?
P.S。我们不是在寻找虚拟化DataGrid
,因为每个单元格都可以有三种不同尺寸和一种边框颜色中的一种。换句话说,细胞需要自由风格。另外,如上所述,我们既需要行也需要列虚拟化。
答案 0 :(得分:0)
这是我的VirtualizingPanel的旧实现,它看起来像一个基于区域大小的网格:
class VirtualizingTilePanel : VirtualizingPanel, IScrollInfo, INotifyPropertyChanged
{
private readonly Logger _log = LogManager.GetCurrentClassLogger();
public VirtualizingTilePanel()
{
// For use in the IScrollInfo implementation
this.RenderTransform = _trans;
}
// Dependency property that controls the size of the child elements
public static readonly DependencyProperty ChildSizeProperty
= DependencyProperty.RegisterAttached("ChildSize", typeof(double), typeof(VirtualizingTilePanel),
new FrameworkPropertyMetadata(Double.PositiveInfinity, FrameworkPropertyMetadataOptions.AffectsMeasure |
FrameworkPropertyMetadataOptions.AffectsArrange));
public static readonly DependencyProperty ItemHeightProperty = DependencyProperty.Register("ItemHeight", typeof(double), typeof(VirtualizingTilePanel), new FrameworkPropertyMetadata(double.PositiveInfinity));
public static readonly DependencyProperty ItemWidthProperty = DependencyProperty.Register("ItemWidth", typeof(double), typeof(VirtualizingTilePanel), new FrameworkPropertyMetadata(double.PositiveInfinity));
public static readonly DependencyProperty OrientationProperty = StackPanel.OrientationProperty.AddOwner(typeof(VirtualizingTilePanel), new FrameworkPropertyMetadata(Orientation.Horizontal));
public static readonly DependencyProperty HorizontalContentAlignmentProperty = ListBox.HorizontalContentAlignmentProperty.AddOwner(typeof(VirtualizingTilePanel), new FrameworkPropertyMetadata(HorizontalAlignment.Center));
// Accessor for the child size dependency property
public double ChildSize
{
get { return (double)GetValue(ChildSizeProperty); }
set { SetValue(ChildSizeProperty, value); }
}
// Accessor for the child size dependency property
private double _childDesiredWidth = double.PositiveInfinity;
public double ChildDesiredWidth
{
get { return _childDesiredWidth; }
set { _childDesiredWidth = value; }
}
// Accessor for the child size dependency property
private double _childDesiredHeigth = double.PositiveInfinity;
public double ChildDesiredHeigth
{
get { return _childDesiredHeigth; }
set { _childDesiredHeigth = value; }
}
// Current First visible item
private int _firstVisibleItem = 0;
public int FirstVisibleItem
{
get { return _firstVisibleItem; }
set
{
_firstVisibleItem = value;
OnPropertyChanged("FirstVisibleItem");
}
}
// Current Last visible item
private int _lastVisibleItem = 0;
public int LastVisibleItem
{
get { return _lastVisibleItem; }
set
{
_lastVisibleItem = value;
OnPropertyChanged("LastVisibleItem");
}
}
/// <summary>
/// Measure the children
/// </summary>
/// <param name="availableSize">Size available</param>
/// <returns>Size desired</returns>
protected override Size MeasureOverride(Size availableSize)
{
UpdateScrollInfo(availableSize);
// Figure out range that's visible based on layout algorithm
int firstVisibleItemIndex, lastVisibleItemIndex;
GetVisibleRange(out firstVisibleItemIndex, out lastVisibleItemIndex);
FirstVisibleItem = firstVisibleItemIndex;
LastVisibleItem = lastVisibleItemIndex;
// We need to access InternalChildren before the generator to work around a bug
UIElementCollection children = this.InternalChildren;
IItemContainerGenerator generator = this.ItemContainerGenerator;
if (children == null)
throw new ArgumentNullException("InternalChildren");
else if (generator == null)
throw new ArgumentNullException("ItemContainerGenerator");
// Get the generator position of the first visible data item
GeneratorPosition startPos = generator.GeneratorPositionFromIndex(firstVisibleItemIndex);
if (startPos == null)
throw new ArgumentNullException("GeneratorPositionFromIndex");
// Get index where we'd insert the child for this position. If the item is realized
// (position.Offset == 0), it's just position.Index, otherwise we have to add one to
// insert after the corresponding child
int childIndex = (startPos.Offset == 0) ? startPos.Index : startPos.Index + 1;
this.IsItemsHost = true;
ItemsControl itemsControl = ListBox.GetItemsOwner(this);
if (itemsControl == null)
{
_log.Error("class VirtualizingTilePanel, method MeasureOverride ->ListBox.GetItemsOwner(this) returned null");
return availableSize;
}
int itemCount = itemsControl.HasItems ? itemsControl.Items.Count : 0;
int current = firstVisibleItemIndex;
using (generator.StartAt(startPos, GeneratorDirection.Forward, true))
{
bool stop = false;
double currentX = 0;
double currentY = 0;
double maxItemSize = 0;
while (current < itemCount)
{
bool newlyRealized;
// Get or create the child
UIElement child = generator.GenerateNext(out newlyRealized) as UIElement;
if (newlyRealized)
{
// Figure out if we need to insert the child at the end or somewhere in the middle
if (childIndex >= children.Count)
{
base.AddInternalChild(child);
}
else
{
base.InsertInternalChild(childIndex, child);
}
generator.PrepareItemContainer(child);
}
else
{
// The child has already been created, let's be sure it's in the right spot
Debug.Assert(child == children[childIndex], "Wrong child was generated");
//_log.Warn("Wrong child was generated: {0}", childIndex);
}
// Measurements will depend on layout algorithm
child.Measure(GetChildSize());
var childDesiredSize = child.DesiredSize;
ChildDesiredHeigth = childDesiredSize.Height;
ChildDesiredWidth = childDesiredSize.Width;
Rect childRect = new Rect(new Point(currentX, currentY), childDesiredSize);
maxItemSize = Math.Max(maxItemSize, childRect.Height);
if (childRect.Right > availableSize.Width) //wrap to a new line
{
currentY = currentY + maxItemSize;
currentX = 0;
maxItemSize = childRect.Height;
childRect.X = currentX;
childRect.Y = currentY;
}
if (currentY > (availableSize.Height + ChildDesiredHeigth))
stop = true;
currentX = childRect.Right;
if (stop)
break;
current++;
childIndex++;
}
}
// Note: this could be deferred to idle time for efficiency
CleanUpItems(firstVisibleItemIndex, current - 1);
return availableSize;
}
/// <summary>
/// Arrange the children
/// </summary>
/// <param name="finalSize">Size available</param>
/// <returns>Size used</returns>
protected override Size ArrangeOverride(Size finalSize)
{
IItemContainerGenerator generator = this.ItemContainerGenerator;
UpdateScrollInfo(finalSize);
for (int i = 0; i < this.Children.Count; i++)
{
UIElement child = this.Children[i];
// Map the child offset to an item offset
int itemIndex = generator.IndexFromGeneratorPosition(new GeneratorPosition(i, 0));
ArrangeChild(itemIndex, child, finalSize);
}
return finalSize;
}
/// <summary>
/// Revirtualize items that are no longer visible
/// </summary>
/// <param name="minDesiredGenerated">first item index that should be visible</param>
/// <param name="maxDesiredGenerated">last item index that should be visible</param>
private void CleanUpItems(int minDesiredGenerated, int maxDesiredGenerated)
{
UIElementCollection children = this.InternalChildren;
IItemContainerGenerator generator = this.ItemContainerGenerator;
for (int i = children.Count - 1; i >= 0; i--)
{
GeneratorPosition childGeneratorPos = new GeneratorPosition(i, 0);
int itemIndex = generator.IndexFromGeneratorPosition(childGeneratorPos);
if (itemIndex < minDesiredGenerated || itemIndex > maxDesiredGenerated)
{
generator.Remove(childGeneratorPos, 1);
RemoveInternalChildRange(i, 1);
}
}
}
/// <summary>
/// When items are removed, remove the corresponding UI if necessary
/// </summary>
/// <param name="sender"></param>
/// <param name="args"></param>
protected override void OnItemsChanged(object sender, ItemsChangedEventArgs args)
{
switch (args.Action)
{
case NotifyCollectionChangedAction.Remove:
case NotifyCollectionChangedAction.Replace:
case NotifyCollectionChangedAction.Move:
RemoveInternalChildRange(args.Position.Index, args.ItemUICount);
break;
}
}
#region Layout specific code
// I've isolated the layout specific code to this region. If you want to do something other than tiling, this is
// where you'll make your changes
/// <summary>
/// Calculate the extent of the view based on the available size
/// </summary>
/// <param name="availableSize">available size</param>
/// <param name="itemCount">number of data items</param>
/// <returns></returns>
private Size CalculateExtent(Size availableSize, int itemCount)
{
int childrenPerRow = CalculateChildrenPerRow(availableSize);
// See how big we are
return new Size(childrenPerRow * ChildDesiredWidth,
ChildDesiredHeigth * Math.Ceiling((double)itemCount / childrenPerRow));
}
/// <summary>
/// Get the range of children that are visible
/// </summary>
/// <param name="firstVisibleItemIndex">The item index of the first visible item</param>
/// <param name="lastVisibleItemIndex">The item index of the last visible item</param>
private void GetVisibleRange(out int firstVisibleItemIndex, out int lastVisibleItemIndex)
{
int childrenPerRow = CalculateChildrenPerRow(_extent);
if (ChildDesiredWidth == double.PositiveInfinity)
firstVisibleItemIndex = 0;
else
firstVisibleItemIndex = (int)Math.Floor(_offset.Y / ChildDesiredHeigth) * childrenPerRow;
lastVisibleItemIndex = (int)Math.Ceiling((_offset.Y + _viewport.Height) / ChildDesiredHeigth) * childrenPerRow - 1;
ItemsControl itemsControl = ItemsControl.GetItemsOwner(this);
int itemCount = itemsControl.HasItems ? itemsControl.Items.Count : 0;
if (lastVisibleItemIndex >= itemCount)
lastVisibleItemIndex = itemCount - 1;
}
/// <summary>
/// Get the size of the children. We assume they are all the same
/// </summary>
/// <returns>The size</returns>
private Size GetChildSize()
{
return new Size(ChildDesiredWidth, ChildDesiredHeigth);
}
/// <summary>
/// Position a child
/// </summary>
/// <param name="itemIndex">The data item index of the child</param>
/// <param name="child">The element to position</param>
/// <param name="finalSize">The size of the panel</param>
private void ArrangeChild(int itemIndex, UIElement child, Size finalSize)
{
int childrenPerRow = CalculateChildrenPerRow(finalSize);
int row = itemIndex / childrenPerRow;
int column = itemIndex % childrenPerRow;
//coorrect horizontal content
var itemRect = new Rect(column * ChildDesiredWidth, row * ChildDesiredHeigth, ChildDesiredWidth, ChildDesiredHeigth);
ItemsControl itemsControl = ItemsControl.GetItemsOwner(this);
int itemCount = itemsControl.HasItems ? itemsControl.Items.Count : 0;
if(itemCount > 1)
{
var correction = Math.Ceiling((finalSize.Width - childrenPerRow * ChildDesiredWidth) / 2);
if (correction < child.DesiredSize.Width)
{
itemRect.X += correction;
}
}
child.Arrange(itemRect);
}
/// <summary>
/// Helper function for tiling layout
/// </summary>
/// <param name="availableSize">Size available</param>
/// <returns></returns>
private int CalculateChildrenPerRow(Size availableSize)
{
// Figure out how many children fit on each row
//int childrenPerRow;
//if (availableSize.Width == Double.PositiveInfinity)
// childrenPerRow = 1;
//else
// childrenPerRow = Math.Max(1, (int)Math.Floor(availableSize.Width / ChildDesiredWidth));
return Math.Max(1, (int)Math.Floor(availableSize.Width / ChildDesiredWidth)); ;
}
#endregion
#region IScrollInfo implementation
// See Ben Constable's series of posts at http://blogs.msdn.com/bencon/
private void UpdateScrollInfo(Size availableSize)
{
// See how many items there are
ItemsControl itemsControl = ItemsControl.GetItemsOwner(this);
int itemCount = itemsControl.HasItems ? itemsControl.Items.Count : 0;
Size extent = CalculateExtent(availableSize, itemCount);
// Update extent
if (extent != _extent)
{
_extent = extent;
}
//// Update viewport
if (availableSize != _viewport)
{
_viewport = availableSize;
}
if (_owner != null)
{
_owner.InvalidateScrollInfo();
}
}
public ScrollViewer ScrollOwner
{
get { return _owner; }
set { _owner = value; }
}
public bool CanHorizontallyScroll
{
get { return _canHScroll; }
set { _canHScroll = value; }
}
public bool CanVerticallyScroll
{
get { return _canVScroll; }
set { _canVScroll = value; }
}
public double HorizontalOffset
{
get { return _offset.X; }
}
public double VerticalOffset
{
get { return _offset.Y; }
}
public double ExtentHeight
{
get { return _extent.Height; }
}
public double ExtentWidth
{
get { return _extent.Width; }
}
public double ViewportHeight
{
get { return _viewport.Height; }
}
public double ViewportWidth
{
get { return _viewport.Width; }
}
public void LineUp()
{
SetVerticalOffset(this.VerticalOffset - SCROLL_STEP);
}
public void LineDown()
{
SetVerticalOffset(this.VerticalOffset + SCROLL_STEP);
}
public void PageUp()
{
SetVerticalOffset(this.VerticalOffset - _viewport.Height);
}
public void PageDown()
{
SetVerticalOffset(this.VerticalOffset + _viewport.Height);
}
public void MouseWheelUp()
{
SetVerticalOffset(this.VerticalOffset - SCROLL_STEP);
}
public void MouseWheelDown()
{
SetVerticalOffset(this.VerticalOffset + SCROLL_STEP);
}
public void LineLeft()
{
throw new InvalidOperationException();
}
public void LineRight()
{
throw new InvalidOperationException();
}
public Rect MakeVisible(Visual visual, Rect rectangle)
{
return new Rect();
}
public void MouseWheelLeft()
{
throw new InvalidOperationException();
}
public void MouseWheelRight()
{
throw new InvalidOperationException();
}
public void PageLeft()
{
throw new InvalidOperationException();
}
public void PageRight()
{
throw new InvalidOperationException();
}
public void SetHorizontalOffset(double offset)
{
throw new InvalidOperationException();
}
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;
}
}
_offset.Y = offset;
if (_owner != null)
_owner.InvalidateScrollInfo();
_trans.Y = -offset;
// Force us to realize the correct children
InvalidateMeasure();
}
private TranslateTransform _trans = new TranslateTransform();
private ScrollViewer _owner;
private bool _canHScroll = false;
private bool _canVScroll = false;
private Size _extent = new Size(0, 0);
private Size _viewport = new Size(0, 0);
private Point _offset;
private const int SCROLL_STEP = 60;
#endregion
public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyChanged(string name)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(name));
}
}
}
这是为列表框中的方形元素设计的 - 您可以修改它以使用矩形。
用法是:
<Style x:Key="ListBoxStyleVirtualize" BasedOn="{StaticResource ListBoxMainStyle}" TargetType="{x:Type ListBox}">
<Setter Property="ScrollViewer.CanContentScroll" Value="True"/>
<Setter Property="ItemsPanel">
<Setter.Value>
<ItemsPanelTemplate>
<custom:VirtualizingTilePanel Name="VirtHost" IsItemsHost="True" HorizontalAlignment="Center" VirtualizingPanel.VirtualizationMode="Recycling" Margin="0 0 0 23"/>
</ItemsPanelTemplate>
</Setter.Value>
</Setter>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ListBox}">
<Grid>
<Border CornerRadius="0" x:Name="Bd" BorderBrush="Transparent" BorderThickness="0" SnapsToDevicePixels="true">
<ScrollViewer Focusable="false" MouseLeftButtonDown="ScrollViewer_MouseLeftButtonDown" Padding="{TemplateBinding Padding}" Template="{DynamicResource ScrollViewerControlTemplate1}">
<ItemsPresenter SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/>
</ScrollViewer>
</Border>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsEnabled" Value="false">
<Setter Property="Background" TargetName="Bd" Value="Transparent"/>
</Trigger>
<Trigger Property="IsGrouping" Value="true">
<Setter Property="ScrollViewer.CanContentScroll" Value="false"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
答案 1 :(得分:0)
试试这个开源项目 - Virtualizing WrapPanel。它开箱即用。
答案 2 :(得分:0)
经过大量的搜索和抓挠后,我们发现了一个名为VirtualCanvas
的东西,它是Canvas
与IScrollInfo
实现相结合的“简单”示例(以及更多)。这允许滚动以两种方式虚拟化。唯一的缺点是它基于坐标而不是行/列。但是,由于它似乎是唯一的解决方案,我们将代码库转换为点和大小。
使用VirtualCanvas
相当容易,因为您只需要为要放在画布上的对象实现IVirtualChild
。然后,您可以通过在画布上调用AddVirtualChild
在画布上添加这些对象的实例。与VirtualCanvas
打包在一起的示例实际上解释了您需要了解的大部分内容。