1 using System;
  2 using System.Collections.Generic;
  3 using System.Diagnostics;
  4 using System.Linq;
  5 using System.Text;
  6 using System.Threading.Tasks;
  7 using System.Windows;
  8 using System.Windows.Controls;
  9 using System.Windows.Controls.Primitives;
 10 using System.Windows.Media;
 11 
 12 namespace Cal.Wpf.Controls
 13 {
 14     /// <summary>
 15     /// 方块模式的布局,支持虚拟化
 16     /// ScrollOffset 每次滚动的距离
 17     /// <local:VirtualizingWrapPanel ScrollOffset="50" ChildHeight="200" ChildWidth="200"/>
 18     /// </summary>
 19     public class VirtualizingWrapPanel : VirtualizingPanel, IScrollInfo
 20     {
 21         #region 变量
 22         private TranslateTransform trans = new TranslateTransform();
 23         #endregion
 24 
 25         #region 构造函数
 26         public VirtualizingWrapPanel()
 27         {
 28             this.RenderTransform = trans;
 29         }
 30         #endregion
 31 
 32         #region 附加属性
 33         public static readonly DependencyProperty ChildWidthProperty = DependencyProperty.RegisterAttached("ChildWidth", typeof(double), typeof(VirtualizingWrapPanel), new FrameworkPropertyMetadata(200.0, FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.AffectsArrange));
 34 
 35         public static readonly DependencyProperty ChildHeightProperty = DependencyProperty.RegisterAttached("ChildHeight", typeof(double), typeof(VirtualizingWrapPanel), new FrameworkPropertyMetadata(200.0, FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.AffectsArrange));
 36 
 37         public static readonly DependencyProperty ScrollOffsetProperty = DependencyProperty.RegisterAttached("ScrollOffset", typeof(int), typeof(VirtualizingWrapPanel), new PropertyMetadata(10));
 38 
 39         /// <summary>
 40         /// 鼠标每一次滚动 UI上的偏移
 41         /// </summary>
 42         public int ScrollOffset
 43         {
 44             get { return Convert.ToInt32(GetValue(ScrollOffsetProperty)); }
 45             set { SetValue(ScrollOffsetProperty, value); }
 46         }
 47 
 48         /// <summary>
 49         /// 元素宽度
 50         /// </summary>
 51         public double ChildWidth
 52         {
 53             get => Convert.ToDouble(GetValue(ChildWidthProperty));
 54             set => SetValue(ChildWidthProperty, value);
 55         }
 56 
 57         /// <summary>
 58         /// 元素高度
 59         /// </summary>
 60         public double ChildHeight
 61         {
 62             get => Convert.ToDouble(GetValue(ChildHeightProperty));
 63             set => SetValue(ChildHeightProperty, value);
 64         }
 65         #endregion
 66 
 67         #region 私有方法
 68 
 69 
 70         int GetItemCount(DependencyObject element)
 71         {
 72             var itemsControl = ItemsControl.GetItemsOwner(element);
 73             return itemsControl.HasItems ? itemsControl.Items.Count : 0;
 74         }
 75 
 76         int CalculateChildrenPerRow(Size availableSize)
 77         {
 78             int childPerRow = 0;
 79             if (availableSize.Width == double.PositiveInfinity)
 80                 childPerRow = this.Children.Count;
 81             else
 82                 childPerRow = Math.Max(1, Convert.ToInt32(Math.Floor(availableSize.Width / this.ChildWidth)));
 83             return childPerRow;
 84         }
 85 
 86         /// <summary>
 87         /// width不超过availableSize的情况下,自身实际需要的Size(高度可能会超出availableSize)
 88         /// </summary>
 89         /// <param name="availableSize"></param>
 90         /// <param name="itemsCount"></param>
 91         /// <returns></returns>
 92         Size CalculateExtent(Size availableSize, int itemsCount)
 93         {
 94             int childPerRow = CalculateChildrenPerRow(availableSize);//现有宽度下 一行可以最多容纳多少个
 95             return new Size(childPerRow * this.ChildWidth, this.ChildHeight * Math.Ceiling(Convert.ToDouble(itemsCount) / childPerRow));
 96         }
 97 
 98         /// <summary>
 99         /// 更新滚动条
100         /// </summary>
101         /// <param name="availableSize"></param>
102         void UpdateScrollInfo(Size availableSize)
103         {
104             var extent = CalculateExtent(availableSize, GetItemCount(this));//extent 自己实际需要
105             if (extent != this.extent)
106             {
107                 this.extent = extent;
108                 this.ScrollOwner.InvalidateScrollInfo();
109             }
110             if (availableSize != this.viewPort)
111             {
112                 this.viewPort = availableSize;
113                 this.ScrollOwner.InvalidateScrollInfo();
114             }
115         }
116 
117         /// <summary>
118         /// 获取所有item,在可视区域内第一个item和最后一个item的索引
119         /// </summary>
120         /// <param name="firstIndex"></param>
121         /// <param name="lastIndex"></param>
122         void GetVisiableRange(ref int firstIndex, ref int lastIndex)
123         {
124             int childPerRow = CalculateChildrenPerRow(this.extent);
125             firstIndex = Convert.ToInt32(Math.Floor(this.offset.Y / this.ChildHeight)) * childPerRow;
126             lastIndex = Convert.ToInt32(Math.Ceiling((this.offset.Y + this.viewPort.Height) / this.ChildHeight)) * childPerRow - 1;
127             int itemsCount = GetItemCount(this);
128             if (lastIndex >= itemsCount)
129                 lastIndex = itemsCount - 1;
130 
131         }
132 
133         /// <summary>
134         /// 将不在可视区域内的item 移除
135         /// </summary>
136         /// <param name="startIndex">可视区域开始索引</param>
137         /// <param name="endIndex">可视区域结束索引</param>
138         void CleanUpItems(int startIndex, int endIndex)
139         {
140             var children = this.InternalChildren;
141             var generator = this.ItemContainerGenerator;
142             for (int i = children.Count - 1; i >= 0; i--)
143             {
144                 var childGeneratorPosi = new GeneratorPosition(i, 0);
145                 int itemIndex = generator.IndexFromGeneratorPosition(childGeneratorPosi);
146 
147                 if (itemIndex < startIndex || itemIndex > endIndex)
148                 {
149 
150                     generator.Remove(childGeneratorPosi, 1);
151                     RemoveInternalChildRange(i, 1);
152                 }
153             }
154         }
155 
156         /// <summary>
157         /// scroll/availableSize/添加删除元素 改变都会触发  edit元素不会改变
158         /// </summary>
159         /// <param name="availableSize"></param>
160         /// <returns></returns>
161         protected override Size MeasureOverride(Size availableSize)
162         {
163             this.UpdateScrollInfo(availableSize);//availableSize更新后,更新滚动条
164             int firstVisiableIndex = 0, lastVisiableIndex = 0;
165             GetVisiableRange(ref firstVisiableIndex, ref lastVisiableIndex);//availableSize更新后,获取当前viewport内可放置的item的开始和结束索引  firstIdnex-lastIndex之间的item可能部分在viewport中也可能都不在viewport中。
166 
167             UIElementCollection children = this.InternalChildren;//因为配置了虚拟化,所以children的个数一直是viewport区域内的个数,如果没有虚拟化则是ItemSource的整个的个数
168             IItemContainerGenerator generator = this.ItemContainerGenerator;
169             //获得第一个可被显示的item的位置
170             GeneratorPosition startPosi = generator.GeneratorPositionFromIndex(firstVisiableIndex);
171             int childIndex = (startPosi.Offset == 0) ? startPosi.Index : startPosi.Index + 1;//startPosi在chilren中的索引
172             using (generator.StartAt(startPosi, GeneratorDirection.Forward, true))
173             {
174                 int itemIndex = firstVisiableIndex;
175                 while (itemIndex <= lastVisiableIndex)//生成lastVisiableIndex-firstVisiableIndex个item
176                 {
177                     bool newlyRealized = false;
178                     var child = generator.GenerateNext(out newlyRealized) as UIElement;
179                     if (newlyRealized)
180                     {
181                         if (childIndex >= children.Count)
182                             base.AddInternalChild(child);
183                         else
184                         {
185                             base.InsertInternalChild(childIndex, child);
186                         }
187                         generator.PrepareItemContainer(child);
188                     }
189                     else
190                     {
191                         //处理 正在显示的child被移除了这种情况
192                         if (!child.Equals(children[childIndex]))
193                         {
194                             base.RemoveInternalChildRange(childIndex, 1);
195                         }
196                     }
197                     child.Measure(new Size(this.ChildWidth, this.ChildHeight));
198                     //child.DesiredSize//child想要的size
199                     itemIndex++;
200                     childIndex++;
201                 }
202             }
203             CleanUpItems(firstVisiableIndex, lastVisiableIndex);
204             return new Size(double.IsInfinity(availableSize.Width) ? 0 : availableSize.Width, double.IsInfinity(availableSize.Height) ? 0 : availableSize.Height);//自身想要的size
205         }
206 
207         protected override Size ArrangeOverride(Size finalSize)
208         {
209             Debug.WriteLine("----ArrangeOverride");
210             var generator = this.ItemContainerGenerator;
211             UpdateScrollInfo(finalSize);
212             int childPerRow = CalculateChildrenPerRow(finalSize);
213             double availableItemWidth = finalSize.Width / childPerRow;
214             for (int i = 0; i <= this.Children.Count - 1; i++)
215             {
216                 var child = this.Children[i];
217                 int itemIndex = generator.IndexFromGeneratorPosition(new GeneratorPosition(i, 0));
218                 int row = itemIndex / childPerRow;//current row
219                 int column = itemIndex % childPerRow;
220                 double xCorrdForItem = 0;
221 
222                 xCorrdForItem = column * availableItemWidth + (availableItemWidth - this.ChildWidth) / 2;
223 
224                 Rect rec = new Rect(xCorrdForItem, row * this.ChildHeight, this.ChildWidth, this.ChildHeight);
225                 child.Arrange(rec);
226             }
227             return finalSize;
228         }
229 
230         protected override void OnRenderSizeChanged(SizeChangedInfo sizeInfo)
231         {
232             base.OnRenderSizeChanged(sizeInfo);
233             this.SetVerticalOffset(this.VerticalOffset);
234         }
235 
236         protected override void OnClearChildren()
237         {
238             base.OnClearChildren();
239             this.SetVerticalOffset(0);
240         }
241 
242         protected override void BringIndexIntoView(int index)
243         {
244             if (index < 0 || index >= Children.Count)
245                 throw new ArgumentOutOfRangeException();
246             int row = index / CalculateChildrenPerRow(RenderSize);
247             SetVerticalOffset(row * this.ChildHeight);
248         }
249 
250         #endregion
251 
252         #region IScrollInfo Interface
253         public bool CanVerticallyScroll { get; set; }
254         public bool CanHorizontallyScroll { get; set; }
255 
256         private Size extent = new Size(0, 0);
257         public double ExtentWidth => this.extent.Width;
258 
259         public double ExtentHeight => this.extent.Height;
260 
261         private Size viewPort = new Size(0, 0);
262         public double ViewportWidth => this.viewPort.Width;
263 
264         public double ViewportHeight => this.viewPort.Height;
265 
266         private Point offset;
267         public double HorizontalOffset => this.offset.X;
268 
269         public double VerticalOffset => this.offset.Y;
270 
271         public ScrollViewer ScrollOwner { get; set; }
272 
273         public void LineDown()
274         {
275             this.SetVerticalOffset(this.VerticalOffset + this.ScrollOffset);
276         }
277 
278         public void LineLeft()
279         {
280             throw new NotImplementedException();
281         }
282 
283         public void LineRight()
284         {
285             throw new NotImplementedException();
286         }
287 
288         public void LineUp()
289         {
290             this.SetVerticalOffset(this.VerticalOffset - this.ScrollOffset);
291         }
292 
293         public Rect MakeVisible(Visual visual, Rect rectangle)
294         {
295             return new Rect();
296         }
297 
298         public void MouseWheelDown()
299         {
300             this.SetVerticalOffset(this.VerticalOffset + this.ScrollOffset);
301         }
302 
303         public void MouseWheelLeft()
304         {
305             throw new NotImplementedException();
306         }
307 
308         public void MouseWheelRight()
309         {
310             throw new NotImplementedException();
311         }
312 
313         public void MouseWheelUp()
314         {
315             this.SetVerticalOffset(this.VerticalOffset - this.ScrollOffset);
316         }
317 
318         public void PageDown()
319         {
320             this.SetVerticalOffset(this.VerticalOffset + this.viewPort.Height);
321         }
322 
323         public void PageLeft()
324         {
325             throw new NotImplementedException();
326         }
327 
328         public void PageRight()
329         {
330             throw new NotImplementedException();
331         }
332 
333         public void PageUp()
334         {
335             this.SetVerticalOffset(this.VerticalOffset - this.viewPort.Height);
336         }
337 
338         public void SetHorizontalOffset(double offset)
339         {
340             throw new NotImplementedException();
341         }
342 
343         public void SetVerticalOffset(double offset)
344         {
345             if (offset < 0 || this.viewPort.Height >= this.extent.Height)
346                 offset = 0;
347             else
348                 if (offset + this.viewPort.Height >= this.extent.Height)
349                 offset = this.extent.Height - this.viewPort.Height;
350 
351             this.offset.Y = offset;
352             this.ScrollOwner?.InvalidateScrollInfo();
353             this.trans.Y = -offset;
354             this.InvalidateMeasure();
355             //接下来会触发MeasureOverride()
356         }
357         #endregion
358     }
359 }