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 }