VirtualizingWrapPanel.cs 18 KB

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