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 }