WPF 虚拟化 VirtualizingWrapPanel 和 VirtualLizingTilePanel
一、 UI 上两个扩展
public class VirtualizingWrapPanel : VirtualizingPanel, IScrollInfo { #region Fields UIElementCollection _children; ItemsControl _itemsControl; IItemContainerGenerator _generator; private Point _offset = new Point(0, 0); private Size _extent = new Size(0, 0); private Size _viewport = new Size(0, 0); private int firstIndex = 0; private Size childSize; private Size _pixelMeasuredViewport = new Size(0, 0); Dictionary<UIElement, Rect> _realizedChildLayout = new Dictionary<UIElement, Rect>(); WrapPanelAbstraction _abstractPanel; #endregion #region Properties private Size ChildSlotSize { get { return new Size(ItemWidth, ItemHeight); } } #endregion #region Dependency Properties [TypeConverter(typeof(LengthConverter))] public double ItemHeight { get { return (double)base.GetValue(ItemHeightProperty); } set { base.SetValue(ItemHeightProperty, value); } } [TypeConverter(typeof(LengthConverter))] public double ItemWidth { get { return (double)base.GetValue(ItemWidthProperty); } set { base.SetValue(ItemWidthProperty, value); } } public Orientation Orientation { get { return (Orientation)GetValue(OrientationProperty); } set { SetValue(OrientationProperty, value); } } public static readonly DependencyProperty ItemHeightProperty = DependencyProperty.Register("ItemHeight", typeof(double), typeof(VirtualizingWrapPanel), new FrameworkPropertyMetadata(double.PositiveInfinity)); public static readonly DependencyProperty ItemWidthProperty = DependencyProperty.Register("ItemWidth", typeof(double), typeof(VirtualizingWrapPanel), new FrameworkPropertyMetadata(double.PositiveInfinity)); public static readonly DependencyProperty OrientationProperty = StackPanel.OrientationProperty.AddOwner(typeof(VirtualizingWrapPanel), new FrameworkPropertyMetadata(Orientation.Horizontal)); #endregion #region Methods public void SetFirstRowViewItemIndex(int index) { SetVerticalOffset((index) / Math.Floor((_viewport.Width) / childSize.Width)); SetHorizontalOffset((index) / Math.Floor((_viewport.Height) / childSize.Height)); } private void Resizing(object sender, EventArgs e) { if (_viewport.Width != 0) { int firstIndexCache = firstIndex; _abstractPanel = null; MeasureOverride(_viewport); SetFirstRowViewItemIndex(firstIndex); firstIndex = firstIndexCache; } } public int GetFirstVisibleSection() { int section; var maxSection = _abstractPanel.Max(x => x.Section); if (Orientation == Orientation.Horizontal) { section = (int)_offset.Y; } else { section = (int)_offset.X; } if (section > maxSection) section = maxSection; return section; } public int GetFirstVisibleIndex() { int section = GetFirstVisibleSection(); var item = _abstractPanel.Where(x => x.Section == section).FirstOrDefault(); if (item != null) return item._index; return 0; } private void CleanUpItems(int minDesiredGenerated, int maxDesiredGenerated) { 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); } } } private void ComputeExtentAndViewport(Size pixelMeasuredViewportSize, int visibleSections) { if (Orientation == Orientation.Horizontal) { _viewport.Height = visibleSections; _viewport.Width = pixelMeasuredViewportSize.Width; } else { _viewport.Width = visibleSections; _viewport.Height = pixelMeasuredViewportSize.Height; } if (Orientation == Orientation.Horizontal) { _extent.Height = _abstractPanel.SectionCount + ViewportHeight - 1; } else { _extent.Width = _abstractPanel.SectionCount + ViewportWidth - 1; } _owner.InvalidateScrollInfo(); } private void ResetScrollInfo() { _offset.X = 0; _offset.Y = 0; } private int GetNextSectionClosestIndex(int itemIndex) { var abstractItem = _abstractPanel[itemIndex]; if (abstractItem.Section < _abstractPanel.SectionCount - 1) { var ret = _abstractPanel. Where(x => x.Section == abstractItem.Section + 1). OrderBy(x => Math.Abs(x.SectionIndex - abstractItem.SectionIndex)). First(); return ret._index; } else return itemIndex; } private int GetLastSectionClosestIndex(int itemIndex) { var abstractItem = _abstractPanel[itemIndex]; if (abstractItem.Section > 0) { var ret = _abstractPanel. Where(x => x.Section == abstractItem.Section - 1). OrderBy(x => Math.Abs(x.SectionIndex - abstractItem.SectionIndex)). First(); return ret._index; } else return itemIndex; } private void NavigateDown() { var gen = _generator.GetItemContainerGeneratorForPanel(this); UIElement selected = (UIElement)Keyboard.FocusedElement; int itemIndex = gen.IndexFromContainer(selected); int depth = 0; while (itemIndex == -1) { selected = (UIElement)VisualTreeHelper.GetParent(selected); itemIndex = gen.IndexFromContainer(selected); depth++; } DependencyObject next = null; if (Orientation == Orientation.Horizontal) { int nextIndex = GetNextSectionClosestIndex(itemIndex); next = gen.ContainerFromIndex(nextIndex); while (next == null) { SetVerticalOffset(VerticalOffset + 1); UpdateLayout(); next = gen.ContainerFromIndex(nextIndex); } } else { if (itemIndex == _abstractPanel._itemCount - 1) return; next = gen.ContainerFromIndex(itemIndex + 1); while (next == null) { SetHorizontalOffset(HorizontalOffset + 1); UpdateLayout(); next = gen.ContainerFromIndex(itemIndex + 1); } } while (depth != 0) { next = VisualTreeHelper.GetChild(next, 0); depth--; } (next as UIElement).Focus(); } private void NavigateLeft() { var gen = _generator.GetItemContainerGeneratorForPanel(this); UIElement selected = (UIElement)Keyboard.FocusedElement; int itemIndex = gen.IndexFromContainer(selected); int depth = 0; while (itemIndex == -1) { selected = (UIElement)VisualTreeHelper.GetParent(selected); itemIndex = gen.IndexFromContainer(selected); depth++; } DependencyObject next = null; if (Orientation == Orientation.Vertical) { int nextIndex = GetLastSectionClosestIndex(itemIndex); next = gen.ContainerFromIndex(nextIndex); while (next == null) { SetHorizontalOffset(HorizontalOffset - 1); UpdateLayout(); next = gen.ContainerFromIndex(nextIndex); } } else { if (itemIndex == 0) return; next = gen.ContainerFromIndex(itemIndex - 1); while (next == null) { SetVerticalOffset(VerticalOffset - 1); UpdateLayout(); next = gen.ContainerFromIndex(itemIndex - 1); } } while (depth != 0) { next = VisualTreeHelper.GetChild(next, 0); depth--; } (next as UIElement).Focus(); } private void NavigateRight() { var gen = _generator.GetItemContainerGeneratorForPanel(this); UIElement selected = (UIElement)Keyboard.FocusedElement; int itemIndex = gen.IndexFromContainer(selected); int depth = 0; while (itemIndex == -1) { selected = (UIElement)VisualTreeHelper.GetParent(selected); itemIndex = gen.IndexFromContainer(selected); depth++; } DependencyObject next = null; if (Orientation == Orientation.Vertical) { int nextIndex = GetNextSectionClosestIndex(itemIndex); next = gen.ContainerFromIndex(nextIndex); while (next == null) { SetHorizontalOffset(HorizontalOffset + 1); UpdateLayout(); next = gen.ContainerFromIndex(nextIndex); } } else { if (itemIndex == _abstractPanel._itemCount - 1) return; next = gen.ContainerFromIndex(itemIndex + 1); while (next == null) { SetVerticalOffset(VerticalOffset + 1); UpdateLayout(); next = gen.ContainerFromIndex(itemIndex + 1); } } while (depth != 0) { next = VisualTreeHelper.GetChild(next, 0); depth--; } (next as UIElement).Focus(); } private void NavigateUp() { var gen = _generator.GetItemContainerGeneratorForPanel(this); UIElement selected = (UIElement)Keyboard.FocusedElement; int itemIndex = gen.IndexFromContainer(selected); int depth = 0; while (itemIndex == -1) { selected = (UIElement)VisualTreeHelper.GetParent(selected); itemIndex = gen.IndexFromContainer(selected); depth++; } DependencyObject next = null; if (Orientation == Orientation.Horizontal) { int nextIndex = GetLastSectionClosestIndex(itemIndex); next = gen.ContainerFromIndex(nextIndex); while (next == null) { SetVerticalOffset(VerticalOffset - 1); UpdateLayout(); next = gen.ContainerFromIndex(nextIndex); } } else { if (itemIndex == 0) return; next = gen.ContainerFromIndex(itemIndex - 1); while (next == null) { SetHorizontalOffset(HorizontalOffset - 1); UpdateLayout(); next = gen.ContainerFromIndex(itemIndex - 1); } } while (depth != 0) { next = VisualTreeHelper.GetChild(next, 0); depth--; } (next as UIElement).Focus(); } #endregion #region Override protected override void OnKeyDown(KeyEventArgs e) { switch (e.Key) { case Key.Down: NavigateDown(); e.Handled = true; break; case Key.Left: NavigateLeft(); e.Handled = true; break; case Key.Right: NavigateRight(); e.Handled = true; break; case Key.Up: NavigateUp(); e.Handled = true; break; default: base.OnKeyDown(e); break; } } protected override void OnItemsChanged(object sender, ItemsChangedEventArgs args) { base.OnItemsChanged(sender, args); _abstractPanel = null; ResetScrollInfo(); } protected override void OnInitialized(EventArgs e) { this.SizeChanged += new SizeChangedEventHandler(this.Resizing); base.OnInitialized(e); _itemsControl = ItemsControl.GetItemsOwner(this); _children = InternalChildren; _generator = ItemContainerGenerator; } protected override Size MeasureOverride(Size availableSize) { if (_itemsControl == null || _itemsControl.Items.Count == 0) return availableSize; if (_abstractPanel == null) _abstractPanel = new WrapPanelAbstraction(_itemsControl.Items.Count); _pixelMeasuredViewport = availableSize; _realizedChildLayout.Clear(); Size realizedFrameSize = availableSize; int itemCount = _itemsControl.Items.Count; int firstVisibleIndex = GetFirstVisibleIndex(); GeneratorPosition startPos = _generator.GeneratorPositionFromIndex(firstVisibleIndex); int childIndex = (startPos.Offset == 0) ? startPos.Index : startPos.Index + 1; int current = firstVisibleIndex; int visibleSections = 1; using (_generator.StartAt(startPos, GeneratorDirection.Forward, true)) { bool stop = false; bool isHorizontal = Orientation == Orientation.Horizontal; double currentX = 0; double currentY = 0; double maxItemSize = 0; int currentSection = GetFirstVisibleSection(); 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); child.Measure(ChildSlotSize); } 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"); } childSize = child.DesiredSize; Rect childRect = new Rect(new Point(currentX, currentY), childSize); if (isHorizontal) { maxItemSize = Math.Max(maxItemSize, childRect.Height); if (childRect.Right > realizedFrameSize.Width) //wrap to a new line { currentY = currentY + maxItemSize; currentX = 0; maxItemSize = childRect.Height; childRect.X = currentX; childRect.Y = currentY; currentSection++; visibleSections++; } if (currentY > realizedFrameSize.Height) stop = true; currentX = childRect.Right; } else { maxItemSize = Math.Max(maxItemSize, childRect.Width); if (childRect.Bottom > realizedFrameSize.Height) //wrap to a new column { currentX = currentX + maxItemSize; currentY = 0; maxItemSize = childRect.Width; childRect.X = currentX; childRect.Y = currentY; currentSection++; visibleSections++; } if (currentX > realizedFrameSize.Width) stop = true; currentY = childRect.Bottom; } _realizedChildLayout.Add(child, childRect); _abstractPanel.SetItemSection(current, currentSection); if (stop) break; current++; childIndex++; } } CleanUpItems(firstVisibleIndex, current - 1); ComputeExtentAndViewport(availableSize, visibleSections); return availableSize; } protected override Size ArrangeOverride(Size finalSize) { if (_children != null) { foreach (UIElement child in _children) { var layoutInfo = _realizedChildLayout[child]; child.Arrange(layoutInfo); } } return finalSize; } #endregion #region IScrollInfo Members private bool _canHScroll = false; public bool CanHorizontallyScroll { get { return _canHScroll; } set { _canHScroll = value; } } private bool _canVScroll = false; public bool CanVerticallyScroll { get { return _canVScroll; } set { _canVScroll = value; } } public double ExtentHeight { get { return _extent.Height; } } public double ExtentWidth { get { return _extent.Width; } } public double HorizontalOffset { get { return _offset.X; } } public double VerticalOffset { get { return _offset.Y; } } public void LineDown() { if (Orientation == Orientation.Vertical) SetVerticalOffset(VerticalOffset + 20); else SetVerticalOffset(VerticalOffset + 1); } public void LineLeft() { if (Orientation == Orientation.Horizontal) SetHorizontalOffset(HorizontalOffset - 20); else SetHorizontalOffset(HorizontalOffset - 1); } public void LineRight() { if (Orientation == Orientation.Horizontal) SetHorizontalOffset(HorizontalOffset + 20); else SetHorizontalOffset(HorizontalOffset + 1); } public void LineUp() { if (Orientation == Orientation.Vertical) SetVerticalOffset(VerticalOffset - 20); else SetVerticalOffset(VerticalOffset - 1); } public Rect MakeVisible(Visual visual, Rect rectangle) { var gen = (ItemContainerGenerator)_generator.GetItemContainerGeneratorForPanel(this); var element = (UIElement)visual; int itemIndex = gen.IndexFromContainer(element); while (itemIndex == -1) { element = (UIElement)VisualTreeHelper.GetParent(element); itemIndex = gen.IndexFromContainer(element); } int section = _abstractPanel[itemIndex].Section; Rect elementRect = _realizedChildLayout[element]; if (Orientation == Orientation.Horizontal) { double viewportHeight = _pixelMeasuredViewport.Height; if (elementRect.Bottom > viewportHeight) _offset.Y += 1; else if (elementRect.Top < 0) _offset.Y -= 1; } else { double viewportWidth = _pixelMeasuredViewport.Width; if (elementRect.Right > viewportWidth) _offset.X += 1; else if (elementRect.Left < 0) _offset.X -= 1; } InvalidateMeasure(); return elementRect; } public void MouseWheelDown() { PageDown(); } public void MouseWheelLeft() { PageLeft(); } public void MouseWheelRight() { PageRight(); } public void MouseWheelUp() { PageUp(); } public void PageDown() { SetVerticalOffset(VerticalOffset + _viewport.Height * 0.8); } public void PageLeft() { SetHorizontalOffset(HorizontalOffset - _viewport.Width * 0.8); } public void PageRight() { SetHorizontalOffset(HorizontalOffset + _viewport.Width * 0.8); } public void PageUp() { SetVerticalOffset(VerticalOffset - _viewport.Height * 0.8); } private ScrollViewer _owner; public ScrollViewer ScrollOwner { get { return _owner; } set { _owner = value; } } public void SetHorizontalOffset(double offset) { if (offset < 0 || _viewport.Width >= _extent.Width) { offset = 0; } else { if (offset + _viewport.Width >= _extent.Width) { offset = _extent.Width - _viewport.Width; } } _offset.X = offset; if (_owner != null) _owner.InvalidateScrollInfo(); InvalidateMeasure(); firstIndex = GetFirstVisibleIndex(); } 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; InvalidateMeasure(); firstIndex = GetFirstVisibleIndex(); } public double ViewportHeight { get { return _viewport.Height; } } public double ViewportWidth { get { return _viewport.Width; } } #endregion #region helper data structures class ItemAbstraction { public ItemAbstraction(WrapPanelAbstraction panel, int index) { _panel = panel; _index = index; } WrapPanelAbstraction _panel; public readonly int _index; int _sectionIndex = -1; public int SectionIndex { get { if (_sectionIndex == -1) { return _index % _panel._averageItemsPerSection - 1; } return _sectionIndex; } set { if (_sectionIndex == -1) _sectionIndex = value; } } int _section = -1; public int Section { get { if (_section == -1) { return _index / _panel._averageItemsPerSection; } return _section; } set { if (_section == -1) _section = value; } } } class WrapPanelAbstraction : IEnumerable<ItemAbstraction> { public WrapPanelAbstraction(int itemCount) { List<ItemAbstraction> items = new List<ItemAbstraction>(itemCount); for (int i = 0; i < itemCount; i++) { ItemAbstraction item = new ItemAbstraction(this, i); items.Add(item); } Items = new ReadOnlyCollection<ItemAbstraction>(items); _averageItemsPerSection = itemCount; _itemCount = itemCount; } public readonly int _itemCount; public int _averageItemsPerSection; private int _currentSetSection = -1; private int _currentSetItemIndex = -1; private int _itemsInCurrentSecction = 0; private object _syncRoot = new object(); public int SectionCount { get { int ret = _currentSetSection + 1; if (_currentSetItemIndex + 1 < Items.Count) { int itemsLeft = Items.Count - _currentSetItemIndex; ret += itemsLeft / _averageItemsPerSection + 1; } return ret; } } private ReadOnlyCollection<ItemAbstraction> Items { get; set; } public void SetItemSection(int index, int section) { lock (_syncRoot) { if (section <= _currentSetSection + 1 && index == _currentSetItemIndex + 1) { _currentSetItemIndex++; Items[index].Section = section; if (section == _currentSetSection + 1) { _currentSetSection = section; if (section > 0) { _averageItemsPerSection = (index) / (section); } _itemsInCurrentSecction = 1; } else _itemsInCurrentSecction++; Items[index].SectionIndex = _itemsInCurrentSecction - 1; } } } public ItemAbstraction this[int index] { get { return Items[index]; } } #region IEnumerable<ItemAbstraction> Members public IEnumerator<ItemAbstraction> GetEnumerator() { return Items.GetEnumerator(); } #endregion #region IEnumerable Members System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { return GetEnumerator(); } #endregion } #endregion }
来源:http://www.codeproject.com/Articles/75847/Virtualizing-WrapPanel
// class from: https://github.com/samueldjack/VirtualCollection/blob/master/VirtualCollection/VirtualCollection/VirtualizingWrapPanel.cs // MakeVisible() method from: http://www.switchonthecode.com/tutorials/wpf-tutorial-implementing-iscrollinfo public class VirtualLizingTilePanel : VirtualizingPanel, IScrollInfo { private const double ScrollLineAmount = 16.0; private Size _extentSize; private Size _viewportSize; private Point _offset; private ItemsControl _itemsControl; private readonly Dictionary<UIElement, Rect> _childLayouts = new Dictionary<UIElement, Rect>(); public static readonly DependencyProperty ItemWidthProperty = DependencyProperty.Register("ItemWidth", typeof(double), typeof(VirtualLizingTilePanel), new PropertyMetadata(1.0, HandleItemDimensionChanged)); public static readonly DependencyProperty ItemHeightProperty = DependencyProperty.Register("ItemHeight", typeof(double), typeof(VirtualLizingTilePanel), new PropertyMetadata(1.0, HandleItemDimensionChanged)); private static readonly DependencyProperty VirtualItemIndexProperty = DependencyProperty.RegisterAttached("VirtualItemIndex", typeof(int), typeof(VirtualLizingTilePanel), new PropertyMetadata(-1)); private IRecyclingItemContainerGenerator _itemsGenerator; private bool _isInMeasure; private static int GetVirtualItemIndex(DependencyObject obj) { return (int)obj.GetValue(VirtualItemIndexProperty); } private static void SetVirtualItemIndex(DependencyObject obj, int value) { obj.SetValue(VirtualItemIndexProperty, value); } public double ItemHeight { get { return (double)GetValue(ItemHeightProperty); } set { SetValue(ItemHeightProperty, value); } } public double ItemWidth { get { return (double)GetValue(ItemWidthProperty); } set { SetValue(ItemWidthProperty, value); } } public VirtualLizingTilePanel() { if (!DesignerProperties.GetIsInDesignMode(this)) { Dispatcher.BeginInvoke((Action)Initialize); } } private void Initialize() { _itemsControl = ItemsControl.GetItemsOwner(this); _itemsGenerator = (IRecyclingItemContainerGenerator)ItemContainerGenerator; InvalidateMeasure(); } protected override void OnItemsChanged(object sender, ItemsChangedEventArgs args) { base.OnItemsChanged(sender, args); InvalidateMeasure(); } protected override Size MeasureOverride(Size availableSize) { if (_itemsControl == null) { return availableSize; } _isInMeasure = true; _childLayouts.Clear(); var extentInfo = GetExtentInfo(availableSize, ItemHeight); EnsureScrollOffsetIsWithinConstrains(extentInfo); var layoutInfo = GetLayoutInfo(availableSize, ItemHeight, extentInfo); RecycleItems(layoutInfo); // Determine where the first item is in relation to previously realized items var generatorStartPosition = _itemsGenerator.GeneratorPositionFromIndex(layoutInfo.FirstRealizedItemIndex); var visualIndex = 0; var currentX = layoutInfo.FirstRealizedItemLeft; var currentY = layoutInfo.FirstRealizedLineTop; using (_itemsGenerator.StartAt(generatorStartPosition, GeneratorDirection.Forward, true)) { for (var itemIndex = layoutInfo.FirstRealizedItemIndex; itemIndex <= layoutInfo.LastRealizedItemIndex; itemIndex++, visualIndex++) { bool newlyRealized; var child = (UIElement)_itemsGenerator.GenerateNext(out newlyRealized); SetVirtualItemIndex(child, itemIndex); if (newlyRealized) { InsertInternalChild(visualIndex, child); } else { // check if item needs to be moved into a new position in the Children collection if (visualIndex < Children.Count) { if (Children[visualIndex] != child) { var childCurrentIndex = Children.IndexOf(child); if (childCurrentIndex >= 0) { RemoveInternalChildRange(childCurrentIndex, 1); } InsertInternalChild(visualIndex, child); } } else { // we know that the child can't already be in the children collection // because we've been inserting children in correct visualIndex order, // and this child has a visualIndex greater than the Children.Count AddInternalChild(child); } } // only prepare the item once it has been added to the visual tree _itemsGenerator.PrepareItemContainer(child); child.Measure(new Size(ItemWidth, ItemHeight)); _childLayouts.Add(child, new Rect(currentX, currentY, ItemWidth, ItemHeight)); if (currentX + ItemWidth * 2 >= availableSize.Width) { // wrap to a new line currentY += ItemHeight; currentX = 0; } else { currentX += ItemWidth; } } } RemoveRedundantChildren(); UpdateScrollInfo(availableSize, extentInfo); var desiredSize = new Size(double.IsInfinity(availableSize.Width) ? 0 : availableSize.Width, double.IsInfinity(availableSize.Height) ? 0 : availableSize.Height); _isInMeasure = false; return desiredSize; } private void EnsureScrollOffsetIsWithinConstrains(ExtentInfo extentInfo) { _offset.Y = Clamp(_offset.Y, 0, extentInfo.MaxVerticalOffset); } private void RecycleItems(ItemLayoutInfo layoutInfo) { foreach (UIElement child in Children) { var virtualItemIndex = GetVirtualItemIndex(child); if (virtualItemIndex < layoutInfo.FirstRealizedItemIndex || virtualItemIndex > layoutInfo.LastRealizedItemIndex) { var generatorPosition = _itemsGenerator.GeneratorPositionFromIndex(virtualItemIndex); if (generatorPosition.Index >= 0) { _itemsGenerator.Recycle(generatorPosition, 1); } } SetVirtualItemIndex(child, -1); } } protected override Size ArrangeOverride(Size finalSize) { foreach (UIElement child in Children) { child.Arrange(_childLayouts[child]); } return finalSize; } private void UpdateScrollInfo(Size availableSize, ExtentInfo extentInfo) { _viewportSize = availableSize; _extentSize = new Size(availableSize.Width, extentInfo.ExtentHeight); InvalidateScrollInfo(); } private void RemoveRedundantChildren() { // iterate backwards through the child collection because we're going to be // removing items from it for (var i = Children.Count - 1; i >= 0; i--) { var child = Children[i]; // if the virtual item index is -1, this indicates // it is a recycled item that hasn't been reused this time round if (GetVirtualItemIndex(child) == -1) { RemoveInternalChildRange(i, 1); } } } private ItemLayoutInfo GetLayoutInfo(Size availableSize, double itemHeight, ExtentInfo extentInfo) { if (_itemsControl == null) { return new ItemLayoutInfo(); } // we need to ensure that there is one realized item prior to the first visible item, and one after the last visible item, // so that keyboard navigation works properly. For example, when focus is on the first visible item, and the user // navigates up, the ListBox selects the previous item, and the scrolls that into view - and this triggers the loading of the rest of the items // in that row var firstVisibleLine = (int)Math.Floor(VerticalOffset / itemHeight); var firstRealizedIndex = Math.Max(extentInfo.ItemsPerLine * firstVisibleLine - 1, 0); var firstRealizedItemLeft = firstRealizedIndex % extentInfo.ItemsPerLine * ItemWidth - HorizontalOffset; var firstRealizedItemTop = (firstRealizedIndex / extentInfo.ItemsPerLine) * itemHeight - VerticalOffset; var firstCompleteLineTop = (firstVisibleLine == 0 ? firstRealizedItemTop : firstRealizedItemTop + ItemHeight); var completeRealizedLines = (int)Math.Ceiling((availableSize.Height - firstCompleteLineTop) / itemHeight); var lastRealizedIndex = Math.Min(firstRealizedIndex + completeRealizedLines * extentInfo.ItemsPerLine + 2, _itemsControl.Items.Count - 1); return new ItemLayoutInfo { FirstRealizedItemIndex = firstRealizedIndex, FirstRealizedItemLeft = firstRealizedItemLeft, FirstRealizedLineTop = firstRealizedItemTop, LastRealizedItemIndex = lastRealizedIndex, }; } private ExtentInfo GetExtentInfo(Size viewPortSize, double itemHeight) { if (_itemsControl == null) { return new ExtentInfo(); } var itemsPerLine = Math.Max((int)Math.Floor(viewPortSize.Width / ItemWidth), 1); var totalLines = (int)Math.Ceiling((double)_itemsControl.Items.Count / itemsPerLine); var extentHeight = Math.Max(totalLines * ItemHeight, viewPortSize.Height); return new ExtentInfo { ItemsPerLine = itemsPerLine, TotalLines = totalLines, ExtentHeight = extentHeight, MaxVerticalOffset = extentHeight - viewPortSize.Height, }; } public void LineUp() { SetVerticalOffset(VerticalOffset - ScrollLineAmount); } public void LineDown() { SetVerticalOffset(VerticalOffset + ScrollLineAmount); } public void LineLeft() { SetHorizontalOffset(HorizontalOffset + ScrollLineAmount); } public void LineRight() { SetHorizontalOffset(HorizontalOffset - ScrollLineAmount); } public void PageUp() { SetVerticalOffset(VerticalOffset - ViewportHeight); } public void PageDown() { SetVerticalOffset(VerticalOffset + ViewportHeight); } public void PageLeft() { SetHorizontalOffset(HorizontalOffset + ItemWidth); } public void PageRight() { SetHorizontalOffset(HorizontalOffset - ItemWidth); } public void MouseWheelUp() { SetVerticalOffset(VerticalOffset - ScrollLineAmount * SystemParameters.WheelScrollLines); } public void MouseWheelDown() { SetVerticalOffset(VerticalOffset + ScrollLineAmount * SystemParameters.WheelScrollLines); } public void MouseWheelLeft() { SetHorizontalOffset(HorizontalOffset - ScrollLineAmount * SystemParameters.WheelScrollLines); } public void MouseWheelRight() { SetHorizontalOffset(HorizontalOffset + ScrollLineAmount * SystemParameters.WheelScrollLines); } public void SetHorizontalOffset(double offset) { if (_isInMeasure) { return; } offset = Clamp(offset, 0, ExtentWidth - ViewportWidth); _offset = new Point(offset, _offset.Y); InvalidateScrollInfo(); InvalidateMeasure(); } public void SetVerticalOffset(double offset) { if (_isInMeasure) { return; } offset = Clamp(offset, 0, ExtentHeight - ViewportHeight); _offset = new Point(_offset.X, offset); InvalidateScrollInfo(); InvalidateMeasure(); } public Rect MakeVisible(Visual visual, Rect rectangle) { if (rectangle.IsEmpty || visual == null || visual == this || !IsAncestorOf(visual)) { return Rect.Empty; } rectangle = visual.TransformToAncestor(this).TransformBounds(rectangle); var viewRect = new Rect(HorizontalOffset, VerticalOffset, ViewportWidth, ViewportHeight); rectangle.X += viewRect.X; rectangle.Y += viewRect.Y; viewRect.X = CalculateNewScrollOffset(viewRect.Left, viewRect.Right, rectangle.Left, rectangle.Right); viewRect.Y = CalculateNewScrollOffset(viewRect.Top, viewRect.Bottom, rectangle.Top, rectangle.Bottom); SetHorizontalOffset(viewRect.X); SetVerticalOffset(viewRect.Y); rectangle.Intersect(viewRect); rectangle.X -= viewRect.X; rectangle.Y -= viewRect.Y; return rectangle; } private static double CalculateNewScrollOffset(double topView, double bottomView, double topChild, double bottomChild) { var offBottom = topChild < topView && bottomChild < bottomView; var offTop = bottomChild > bottomView && topChild > topView; var tooLarge = (bottomChild - topChild) > (bottomView - topView); if (!offBottom && !offTop) return topView; if ((offBottom && !tooLarge) || (offTop && tooLarge)) return topChild; return bottomChild - (bottomView - topView); } public ItemLayoutInfo GetVisibleItemsRange() { return GetLayoutInfo(_viewportSize, ItemHeight, GetExtentInfo(_viewportSize, ItemHeight)); } public bool CanVerticallyScroll { get; set; } public bool CanHorizontallyScroll { get; set; } public double ExtentWidth { get { return _extentSize.Width; } } public double ExtentHeight { get { return _extentSize.Height; } } public double ViewportWidth { get { return _viewportSize.Width; } } public double ViewportHeight { get { return _viewportSize.Height; } } public double HorizontalOffset { get { return _offset.X; } } public double VerticalOffset { get { return _offset.Y; } } public ScrollViewer ScrollOwner { get; set; } private void InvalidateScrollInfo() { if (ScrollOwner != null) { ScrollOwner.InvalidateScrollInfo(); } } private static void HandleItemDimensionChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { var wrapPanel = (d as VirtualLizingTilePanel); if (wrapPanel != null) wrapPanel.InvalidateMeasure(); } private double Clamp(double value, double min, double max) { return Math.Min(Math.Max(value, min), max); } internal class ExtentInfo { public int ItemsPerLine; public int TotalLines; public double ExtentHeight; public double MaxVerticalOffset; } public class ItemLayoutInfo { public int FirstRealizedItemIndex; public double FirstRealizedLineTop; public double FirstRealizedItemLeft; public int LastRealizedItemIndex; } }
来源:
// class from: https://github.com/samueldjack/VirtualCollection/blob/master/VirtualCollection/VirtualCollection/VirtualizingWrapPanel.cs
// MakeVisible() method from: http://www.switchonthecode.com/tutorials/wpf-tutorial-implementing-iscrollinfo
二、数据方面的一个处理
/// <summary> /// 为ListBox支持数据虚拟化技术 /// </summary> public class VirtualDataForListBox<T> : IDisposable, INotifyPropertyChanged where T : class { public event PropertyChangedEventHandler PropertyChanged; private DelayHelper delay; private ListBox listBox; /// <summary> /// 垂直滚动条 /// </summary> private ScrollBar bar; /// <summary> /// 滚动视图 /// </summary> private ScrollViewer viewer; /// <summary> /// 数据源 /// </summary> private ObservableCollection<T> sources; /// <summary> /// 是否已初始化完毕 /// </summary> protected bool Inited { get; set; } /// <summary> /// 偏移量 /// </summary> protected double Offset { get; set; } /// <summary> /// 偏移数量 /// </summary> protected int OffsetCount { get; set; } /// <summary> /// 偏移方向 /// <para>True:向上</para> /// <para>False:向下</para> /// </summary> protected bool OffsetDirection { get; set; } public Func<bool> CheckCanScrollToBottom; #region 数据绑定 private ObservableCollection<T> virtualData; /// <summary> /// 虚拟数据 /// </summary> public ObservableCollection<T> VirtualData { get { return virtualData; } protected set { virtualData = value; if (this.PropertyChanged != null) { this.PropertyChanged(this, new PropertyChangedEventArgs(nameof(VirtualData))); } } } #endregion #region 配置参数 /// <summary> /// 初始化时最多加载的数据量 /// <para>需要保证:如果数据未完全加载,ListBox一定可以出现滚动条</para> /// </summary> [DefaultValue(20)] public int InitLoadCount { get; set; } /// <summary> /// 递增的数量值 /// <para>滚动条滚动到两端时,每次自动加载的数据量</para> /// <para>子项数量超过容器的最大数量<paramref name="MaxCount"/>时,自动减少的数量</para> /// </summary> [DefaultValue(20)] public int IncreasingCount { get; set; } /// <summary> /// 子项的最大数量 /// </summary> [DefaultValue(60)] public int MaxCount { get; set; } #endregion /// <summary> /// 当前显示的虚拟数据起始索引 /// </summary> protected int StartVirtualIndex { get; set; } /// <summary> /// 当前显示的虚拟数据的终止索引 /// </summary> protected int EndVirtualIndex { get; set; } /// <summary> /// 忽略滚动条滚动事件 /// </summary> protected bool IgnoreBarChanged { get; set; } public VirtualDataForListBox(ListBox listBox, ObservableCollection<T> sources) { if (listBox == null || sources == null) throw new ArgumentException(" listBox or sources is null "); this.delay = new DelayHelper(25, DelayLayout); this.Inited = false; this.Offset = 0; this.listBox = listBox; this.sources = sources; this.InitLoadCount = 20; this.IncreasingCount = 20; this.MaxCount = 60; this.EndVirtualIndex = -1; this.StartVirtualIndex = -1; this.VirtualData = new ObservableCollection<T>(); } /// <summary> /// 初始化 /// </summary> public void Init() { if (this.Inited) return; if (this.listBox == null) { LogHelper.Warning("数据虚拟化-初始化失败"); return; } // 监控滚动条 this.bar = this.listBox.GetFirstChildT<ScrollBar, ListBoxItem>(t => t.Orientation == Orientation.Vertical); this.viewer = this.listBox.GetFirstChildT<ScrollViewer, ListBoxItem>(null); if (this.bar == null || this.viewer == null) { LogHelper.Warning("数据虚拟化-初始化失败"); return; } // 绑定数据源 this.listBox.SetBinding(ListBox.ItemsSourceProperty, new Binding(nameof(this.VirtualData)) { Source = this, }); this.ReloadEndData(); // 监控滚动条 this.bar.ValueChanged += Bar_ValueChanged; // 监控滚动视图 this.viewer.LayoutUpdated += Viewer_LayoutUpdated; // 监控数据源 this.sources.CollectionChanged += Sources_CollectionChanged; Inited = true; } private void Viewer_LayoutUpdated(object sender, EventArgs e) { if (!this.Inited) return; Console.WriteLine(" Viewer_LayoutUpdated "); if (this.Offset == 0 || this.IgnoreBarChanged) return; this.delay.DelayAction(); } private void DelayLayout() { if (!this.Inited) return; var view = new ViewDecorate(this.viewer); view.DispatcherAction(() => { if (this.Offset == 0) return; try { this.IgnoreBarChanged = true; double temp = 0; // 向上 if (this.OffsetDirection) { for (int i = 0; i < this.OffsetCount && i < this.VirtualData.Count; i++) { temp += (this.listBox.ItemContainerGenerator.ContainerFromIndex(i) as ListBoxItem).ActualHeight; } } this.viewer.ScrollToVerticalOffset(this.Offset + temp); Console.WriteLine(" Viewer_LayoutUpdated ----------------------- Over "); } finally { this.Offset = 0; this.IgnoreBarChanged = false; } }); } /// <summary> /// 滚动条滚动 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void Bar_ValueChanged(object sender, System.Windows.RoutedPropertyChangedEventArgs<double> e) { if (!this.Inited) return; if (this.IgnoreBarChanged || this.Offset != 0) { e.Handled = true; return; } try { this.IgnoreBarChanged = true; const int count = 100; // 向下滚动到端部 if (e.NewValue > e.OldValue && e.NewValue + count >= this.bar.Maximum) { TryScrollDown(e.NewValue - e.OldValue); } // 向上滚动到端部 else if (e.NewValue < e.OldValue && e.NewValue - count <= 0) { TryScrollUp(e.OldValue - e.NewValue); } } finally { e.Handled = true; this.IgnoreBarChanged = false; } } /// <summary> /// 数据源发生变化 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void Sources_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e) { if (!this.Inited) return; if (e.Action == NotifyCollectionChangedAction.Add) { // 新消息到达、尝试将滚动条滚动到底部 this.MoveToBottom(); } else if (e.Action == NotifyCollectionChangedAction.Remove) { this.IgnoreBarChanged = true; // 移除旧数据 foreach (var item in e.OldItems) { if (item is T) this.VirtualData.Remove(item as T); } this.ReCalIndex(); if (this.StartVirtualIndex == -1 || this.EndVirtualIndex == -1) { this.ReloadEndData(); } else { if (this.VirtualData.Count < this.InitLoadCount) { // 数量过少、尝试填充数据 this.LoadMoreData(); } } this.IgnoreBarChanged = false; } // 撤回消息 else if (e.Action == NotifyCollectionChangedAction.Replace) { if (e.OldItems != null && e.OldItems.Count == 1 && e.NewItems != null && e.NewItems.Count == 1) { var oldT = e.OldItems[0] as T; var newT = e.NewItems[0] as T; int index = this.VirtualData.IndexOf(oldT); if (index > -1) { this.VirtualData[index] = newT; } } } else if (e.Action == NotifyCollectionChangedAction.Reset) { this.IgnoreBarChanged = true; this.ReloadEndData(); this.IgnoreBarChanged = false; } } /// <summary> /// 将视图移动到某个索引的位置 /// </summary> /// <param name="index"></param> public void MoveToIndex(int index) { if (!this.Inited) return; if (index < 0 || index >= this.sources.Count) return; var t = this.sources[index]; if (this.VirtualData.IndexOf(t) > -1) { listBox.ScrollIntoView(t); return; } int start = index - this.InitLoadCount; if (start < 0) start = 0; int end = index + this.InitLoadCount; if (end >= this.sources.Count) end = this.sources.Count - 1; int count = end - start + 1; if (count == 0) return; try { this.IgnoreBarChanged = true; var list = this.sources.Skip(start).Take(count); this.VirtualData.Clear(); foreach (var item in list) { this.VirtualData.Add(item); } this.ReCalIndex(); listBox.ScrollIntoView(t); } finally { this.IgnoreBarChanged = false; } } /// <summary> /// 将视图移动到底部 /// </summary> public void MoveToBottom() { if (!this.Inited) return; try { this.IgnoreBarChanged = true; // 询问是否可以将滚动条滚动到底部 if (this.CheckCanScrollToBottom != null && !this.CheckCanScrollToBottom()) return; // 超过最大显示容量、则重新加载末端数据 if (this.StartVirtualIndex == -1 || this.sources.Count == 0 || this.sources.Count - this.StartVirtualIndex > this.MaxCount) { this.ReloadEndData(); return; } // 没有需要加载的数据 if (this.EndVirtualIndex == this.sources.Count - 1) { this.listBox.ScrollViewToBottom(); return; } // 平滑加载 var count = this.EndVirtualIndex + 1; if (this.sources.Count > count) { var list = this.sources.Skip(count).ToList(); foreach (var item in list) { this.VirtualData.Add(item); } this.ReCalIndex(); this.listBox.ScrollViewToBottom(); } } catch (Exception ex) { LogHelper.Execption(ex, "数据虚拟化"); } finally { this.IgnoreBarChanged = false; } } /// <summary> /// 重新计算索引值 /// </summary> private void ReCalIndex() { if (this.VirtualData.Count > 0) { this.StartVirtualIndex = this.sources.IndexOf(this.VirtualData[0]); this.EndVirtualIndex = this.sources.IndexOf(this.VirtualData[this.VirtualData.Count - 1]); if (this.StartVirtualIndex == -1 || this.EndVirtualIndex == -1 || this.EndVirtualIndex < this.StartVirtualIndex) { this.StartVirtualIndex = -1; this.EndVirtualIndex = -1; LogHelper.Warning("数据虚拟化-逻辑错误"); } } else { this.StartVirtualIndex = -1; this.EndVirtualIndex = -1; } } /// <summary> /// 重新初始化数据 /// </summary> private void ReloadEndData() { if (this.VirtualData.Count > 0) { this.VirtualData.Clear(); this.EndVirtualIndex = -1; this.StartVirtualIndex = -1; } if (this.sources != null && this.sources.Count > 0) { var list = this.sources.ListLastMaxCount(this.InitLoadCount); if (list.Count > 0) { foreach (var item in list) { this.VirtualData.Add(item); } this.ReCalIndex(); // 滚动条滚动到最底部 this.listBox.ScrollViewToBottom(); } } } /// <summary> /// 删除数据时加载更多数据 /// </summary> private void LoadMoreData() { List<T> data = this.sources.ListFindRangeWithMaxCount(this.StartVirtualIndex, this.InitLoadCount); if (data.Count <= this.VirtualData.Count) { // 没有加载到更多数据 return; } int start = data.IndexOf(this.VirtualData[0]); int end = data.LastIndexOf(this.VirtualData[this.VirtualData.Count - 1]); if (start == -1 || end == -1 || end < start) { LogHelper.Warning("数据虚拟化-逻辑错误"); return; } for (int i = 0; i < data.Count; i++) { if (i < start) { this.VirtualData.Insert(i, data[i]); } else if (i > end) { this.VirtualData.Add(data[i]); } } this.ReCalIndex(); } /// <summary> /// 向上滚动 /// </summary> private void TryScrollUp(double offset) { // 没有数据了 if (this.StartVirtualIndex == -1 || this.StartVirtualIndex == 0) return; double tempOffset = this.viewer.ContentVerticalOffset; // 释放捕获的鼠标 this.bar.Track.Thumb.ReleaseMouseCapture(); this.bar.Track.DecreaseRepeatButton.ReleaseMouseCapture(); int tempCount = 0; var list = this.sources.ListLastMaxCount(this.StartVirtualIndex, this.IncreasingCount, false); // list 为反序结果 foreach (var item in list) { this.VirtualData.Insert(0, item); tempCount++; } if (this.VirtualData.Count > this.MaxCount) { for (int i = 0; i < this.IncreasingCount; i++) { this.VirtualData.RemoveAt(this.VirtualData.Count - 1); } } this.ReCalIndex(); this.OffsetDirection = true; this.OffsetCount = tempCount; this.Offset = tempOffset - offset; if (this.Offset == 0) this.Offset = 1; } /// <summary> /// 向下滚动 /// </summary> private void TryScrollDown(double offest) { // 没有数据了 if (this.EndVirtualIndex == -1 || this.EndVirtualIndex == this.sources.Count - 1) return; // 释放捕获的鼠标 this.bar.Track.Thumb.ReleaseMouseCapture(); this.bar.Track.IncreaseRepeatButton.ReleaseMouseCapture(); double tempOffset = this.viewer.ContentVerticalOffset; var list = this.sources.Skip(this.EndVirtualIndex + 1).Take(this.IncreasingCount); foreach (var item in list) { this.VirtualData.Add(item); } if (this.VirtualData.Count > this.MaxCount) { for (int i = 0; i < this.IncreasingCount; i++) { tempOffset -= (this.listBox.ItemContainerGenerator.ContainerFromIndex(0) as ListBoxItem).ActualHeight; this.VirtualData.RemoveAt(0); } } this.ReCalIndex(); this.OffsetDirection = false; this.OffsetCount = 0; this.Offset = tempOffset + offest; if (this.Offset == 0) this.Offset = 1; } public void Dispose() { if (!this.Inited) return; this.Inited = false; this.VirtualData.Clear(); // 监控滚动条 this.bar.ValueChanged -= Bar_ValueChanged; // 监控滚动视图 this.viewer.LayoutUpdated -= Viewer_LayoutUpdated; // 监控数据源 this.sources.CollectionChanged -= Sources_CollectionChanged; this.CheckCanScrollToBottom = null; this.delay.Dispose(); } }
该处理方式相当于根据滚动条的滚动适时增减items,当然该类的应用有一定的局限性,不过操作滚动条的方式还是具有借鉴意义的。
源码出处不详。
三、补充
启用UI虚拟化的两个附加属性:
1、ScrollViewer.CanContentScroll="True"
2、VirtualizingStackPanel.IsVirtualizing="True"