VirtualizingWrapPanel.cs 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Collections.ObjectModel;
  4. using System.ComponentModel;
  5. using System.Diagnostics;
  6. using System.Linq;
  7. using System.Text;
  8. using System.Threading.Tasks;
  9. using System.Windows.Controls.Primitives;
  10. using System.Windows.Controls;
  11. using System.Windows.Input;
  12. using System.Windows.Media;
  13. using System.Windows;
  14. using GeekDesk.CustomComponent.VirtualizingWrapPanel;
  15. namespace GeekDesk.CustomComponent.VirtualizingWrapPanel
  16. {
  17. public class VirtualizingWrapPanel : VirtualizingPanelBase
  18. {
  19. #region Deprecated properties
  20. [Obsolete("Use SpacingMode")]
  21. public static readonly DependencyProperty IsSpacingEnabledProperty = DependencyProperty.Register(nameof(IsSpacingEnabled), typeof(bool), typeof(VirtualizingWrapPanel), new FrameworkPropertyMetadata(true, FrameworkPropertyMetadataOptions.AffectsMeasure));
  22. [Obsolete("Use IsSpacingEnabled")]
  23. public bool SpacingEnabled { get => IsSpacingEnabled; set => IsSpacingEnabled = value; }
  24. /// <summary>
  25. /// Gets or sets a value that specifies whether the items are distributed evenly across the width (horizontal orientation)
  26. /// or height (vertical orientation). The default value is true.
  27. /// </summary>
  28. [Obsolete("Use SpacingMode")]
  29. public bool IsSpacingEnabled { get => (bool)GetValue(IsSpacingEnabledProperty); set => SetValue(IsSpacingEnabledProperty, value); }
  30. [Obsolete("Use ItemSize")]
  31. public Size ChildrenSize { get => ItemSize; set => ItemSize = value; }
  32. #endregion
  33. public static readonly DependencyProperty SpacingModeProperty = DependencyProperty.Register(nameof(SpacingMode), typeof(SpacingMode), typeof(VirtualizingWrapPanel), new FrameworkPropertyMetadata(SpacingMode.Uniform, FrameworkPropertyMetadataOptions.AffectsMeasure));
  34. public static readonly DependencyProperty OrientationProperty = DependencyProperty.Register(nameof(Orientation), typeof(Orientation), typeof(VirtualizingWrapPanel), new FrameworkPropertyMetadata(Orientation.Vertical, FrameworkPropertyMetadataOptions.AffectsMeasure, (obj, args) => ((VirtualizingWrapPanel)obj).Orientation_Changed()));
  35. public static readonly DependencyProperty ItemSizeProperty = DependencyProperty.Register(nameof(ItemSize), typeof(Size), typeof(VirtualizingWrapPanel), new FrameworkPropertyMetadata(Size.Empty, FrameworkPropertyMetadataOptions.AffectsMeasure));
  36. public static readonly DependencyProperty StretchItemsProperty = DependencyProperty.Register(nameof(StretchItems), typeof(bool), typeof(VirtualizingWrapPanel), new FrameworkPropertyMetadata(false, FrameworkPropertyMetadataOptions.AffectsArrange));
  37. /// <summary>
  38. /// Gets or sets the spacing mode used when arranging the items. The default value is <see cref="SpacingMode.Uniform"/>.
  39. /// </summary>
  40. public SpacingMode SpacingMode { get => (SpacingMode)GetValue(SpacingModeProperty); set => SetValue(SpacingModeProperty, value); }
  41. /// <summary>
  42. /// Gets or sets a value that specifies the orientation in which items are arranged. The default value is <see cref="Orientation.Vertical"/>.
  43. /// </summary>
  44. public Orientation Orientation { get => (Orientation)GetValue(OrientationProperty); set => SetValue(OrientationProperty, value); }
  45. /// <summary>
  46. /// Gets or sets a value that specifies the size of the items. The default value is <see cref="Size.Empty"/>.
  47. /// If the value is <see cref="Size.Empty"/> the size of the items gots measured by the first realized item.
  48. /// </summary>
  49. public Size ItemSize { get => (Size)GetValue(ItemSizeProperty); set => SetValue(ItemSizeProperty, value); }
  50. /// <summary>
  51. /// Gets or sets a value that specifies if the items get stretched to fill up remaining space. The default value is false.
  52. /// </summary>
  53. /// <remarks>
  54. /// The MaxWidth and MaxHeight properties of the ItemContainerStyle can be used to limit the stretching.
  55. /// In this case the use of the remaining space will be determined by the SpacingMode property.
  56. /// </remarks>
  57. public bool StretchItems { get => (bool)GetValue(StretchItemsProperty); set => SetValue(StretchItemsProperty, value); }
  58. protected Size childSize;
  59. protected int rowCount;
  60. protected int itemsPerRowCount;
  61. private void Orientation_Changed()
  62. {
  63. MouseWheelScrollDirection = Orientation == Orientation.Vertical ? ScrollDirection.Vertical : ScrollDirection.Horizontal;
  64. }
  65. protected override Size MeasureOverride(Size availableSize)
  66. {
  67. UpdateChildSize(availableSize);
  68. return base.MeasureOverride(availableSize);
  69. }
  70. private void UpdateChildSize(Size availableSize)
  71. {
  72. if (ItemsOwner is IHierarchicalVirtualizationAndScrollInfo groupItem
  73. && VirtualizingPanel.GetIsVirtualizingWhenGrouping(ItemsControl))
  74. {
  75. if (Orientation == Orientation.Vertical)
  76. {
  77. availableSize.Width = groupItem.Constraints.Viewport.Size.Width;
  78. availableSize.Width = Math.Max(availableSize.Width - (Margin.Left + Margin.Right), 0);
  79. }
  80. else
  81. {
  82. availableSize.Height = groupItem.Constraints.Viewport.Size.Height;
  83. availableSize.Height = Math.Max(availableSize.Height - (Margin.Top + Margin.Bottom), 0);
  84. }
  85. }
  86. if (ItemSize != Size.Empty)
  87. {
  88. childSize = ItemSize;
  89. }
  90. else if (InternalChildren.Count != 0)
  91. {
  92. childSize = InternalChildren[0].DesiredSize;
  93. }
  94. else
  95. {
  96. childSize = CalculateChildSize(availableSize);
  97. }
  98. if (double.IsInfinity(GetWidth(availableSize)))
  99. {
  100. itemsPerRowCount = Items.Count;
  101. }
  102. else
  103. {
  104. itemsPerRowCount = Math.Max(1, (int)Math.Floor(GetWidth(availableSize) / GetWidth(childSize)));
  105. }
  106. rowCount = (int)Math.Ceiling((double)Items.Count / itemsPerRowCount);
  107. }
  108. private Size CalculateChildSize(Size availableSize)
  109. {
  110. if (Items.Count == 0)
  111. {
  112. return new Size(0, 0);
  113. }
  114. var startPosition = ItemContainerGenerator.GeneratorPositionFromIndex(0);
  115. using (ItemContainerGenerator.StartAt(startPosition, GeneratorDirection.Forward, true))
  116. {
  117. var child = (UIElement)ItemContainerGenerator.GenerateNext();
  118. AddInternalChild(child);
  119. ItemContainerGenerator.PrepareItemContainer(child);
  120. child.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
  121. return child.DesiredSize;
  122. }
  123. }
  124. protected override Size CalculateExtent(Size availableSize)
  125. {
  126. double extentWidth = IsSpacingEnabled && SpacingMode != SpacingMode.None && !double.IsInfinity(GetWidth(availableSize))
  127. ? GetWidth(availableSize)
  128. : GetWidth(childSize) * itemsPerRowCount;
  129. if (ItemsOwner is IHierarchicalVirtualizationAndScrollInfo groupItem)
  130. {
  131. if (Orientation == Orientation.Vertical)
  132. {
  133. extentWidth = Math.Max(extentWidth - (Margin.Left + Margin.Right), 0);
  134. }
  135. else
  136. {
  137. extentWidth = Math.Max(extentWidth - (Margin.Top + Margin.Bottom), 0);
  138. }
  139. }
  140. double extentHeight = GetHeight(childSize) * rowCount;
  141. return CreateSize(extentWidth, extentHeight);
  142. }
  143. protected void CalculateSpacing(Size finalSize, out double innerSpacing, out double outerSpacing)
  144. {
  145. Size childSize = CalculateChildArrangeSize(finalSize);
  146. double finalWidth = GetWidth(finalSize);
  147. double totalItemsWidth = Math.Min(GetWidth(childSize) * itemsPerRowCount, finalWidth);
  148. double unusedWidth = finalWidth - totalItemsWidth;
  149. SpacingMode spacingMode = IsSpacingEnabled ? SpacingMode : SpacingMode.None;
  150. switch (spacingMode)
  151. {
  152. case SpacingMode.Uniform:
  153. innerSpacing = outerSpacing = unusedWidth / (itemsPerRowCount + 1);
  154. break;
  155. case SpacingMode.BetweenItemsOnly:
  156. innerSpacing = unusedWidth / Math.Max(itemsPerRowCount - 1, 1);
  157. outerSpacing = 0;
  158. break;
  159. case SpacingMode.StartAndEndOnly:
  160. innerSpacing = 0;
  161. outerSpacing = unusedWidth / 2;
  162. break;
  163. case SpacingMode.None:
  164. default:
  165. innerSpacing = 0;
  166. outerSpacing = 0;
  167. break;
  168. }
  169. }
  170. protected override Size ArrangeOverride(Size finalSize)
  171. {
  172. double offsetX = GetX(Offset);
  173. double offsetY = GetY(Offset);
  174. /* When the items owner is a group item offset is handled by the parent panel. */
  175. if (ItemsOwner is IHierarchicalVirtualizationAndScrollInfo groupItem)
  176. {
  177. offsetY = 0;
  178. }
  179. Size childSize = CalculateChildArrangeSize(finalSize);
  180. CalculateSpacing(finalSize, out double innerSpacing, out double outerSpacing);
  181. for (int childIndex = 0; childIndex < InternalChildren.Count; childIndex++)
  182. {
  183. UIElement child = InternalChildren[childIndex];
  184. int itemIndex = GetItemIndexFromChildIndex(childIndex);
  185. int columnIndex = itemIndex % itemsPerRowCount;
  186. int rowIndex = itemIndex / itemsPerRowCount;
  187. double x = outerSpacing + columnIndex * (GetWidth(childSize) + innerSpacing);
  188. double y = rowIndex * GetHeight(childSize);
  189. if (GetHeight(finalSize) == 0.0)
  190. {
  191. /* When the parent panel is grouping and a cached group item is not
  192. * in the viewport it has no valid arrangement. That means that the
  193. * height/width is 0. Therefore the items should not be visible so
  194. * that they are not falsely displayed. */
  195. child.Arrange(new Rect(0, 0, 0, 0));
  196. }
  197. else
  198. {
  199. child.Arrange(CreateRect(x - offsetX, y - offsetY, childSize.Width, childSize.Height));
  200. }
  201. }
  202. return finalSize;
  203. }
  204. protected Size CalculateChildArrangeSize(Size finalSize)
  205. {
  206. if (StretchItems)
  207. {
  208. if (Orientation == Orientation.Vertical)
  209. {
  210. double childMaxWidth = ReadItemContainerStyle(MaxWidthProperty, double.PositiveInfinity);
  211. double maxPossibleChildWith = finalSize.Width / itemsPerRowCount;
  212. double childWidth = Math.Min(maxPossibleChildWith, childMaxWidth);
  213. return new Size(childWidth, childSize.Height);
  214. }
  215. else
  216. {
  217. double childMaxHeight = ReadItemContainerStyle(MaxHeightProperty, double.PositiveInfinity);
  218. double maxPossibleChildHeight = finalSize.Height / itemsPerRowCount;
  219. double childHeight = Math.Min(maxPossibleChildHeight, childMaxHeight);
  220. return new Size(childSize.Width, childHeight);
  221. }
  222. }
  223. else
  224. {
  225. return childSize;
  226. }
  227. }
  228. private T ReadItemContainerStyle<T>(DependencyProperty property, T fallbackValue)
  229. {
  230. var value = ItemsControl.ItemContainerStyle?.Setters.OfType<Setter>()
  231. .FirstOrDefault(setter => setter.Property == property)?.Value;
  232. return (T)(value ?? fallbackValue);
  233. }
  234. protected override ItemRange UpdateItemRange()
  235. {
  236. if (!IsVirtualizing)
  237. {
  238. return new ItemRange(0, Items.Count - 1);
  239. }
  240. int startIndex;
  241. int endIndex;
  242. if (ItemsOwner is IHierarchicalVirtualizationAndScrollInfo groupItem)
  243. {
  244. if (!VirtualizingPanel.GetIsVirtualizingWhenGrouping(ItemsControl))
  245. {
  246. return new ItemRange(0, Items.Count - 1);
  247. }
  248. var offset = new Point(Offset.X, groupItem.Constraints.Viewport.Location.Y);
  249. int offsetRowIndex;
  250. double offsetInPixel;
  251. int rowCountInViewport;
  252. if (ScrollUnit == ScrollUnit.Item)
  253. {
  254. offsetRowIndex = GetY(offset) >= 1 ? (int)GetY(offset) - 1 : 0; // ignore header
  255. offsetInPixel = offsetRowIndex * GetHeight(childSize);
  256. }
  257. else
  258. {
  259. offsetInPixel = Math.Min(Math.Max(GetY(offset) - GetHeight(groupItem.HeaderDesiredSizes.PixelSize), 0), GetHeight(Extent));
  260. offsetRowIndex = GetRowIndex(offsetInPixel);
  261. }
  262. double viewportHeight = Math.Min(GetHeight(Viewport), Math.Max(GetHeight(Extent) - offsetInPixel, 0));
  263. rowCountInViewport = (int)Math.Ceiling((offsetInPixel + viewportHeight) / GetHeight(childSize)) - (int)Math.Floor(offsetInPixel / GetHeight(childSize));
  264. startIndex = offsetRowIndex * itemsPerRowCount;
  265. endIndex = Math.Min(((offsetRowIndex + rowCountInViewport) * itemsPerRowCount) - 1, Items.Count - 1);
  266. if (CacheLengthUnit == VirtualizationCacheLengthUnit.Pixel)
  267. {
  268. double cacheBeforeInPixel = Math.Min(CacheLength.CacheBeforeViewport, offsetInPixel);
  269. double cacheAfterInPixel = Math.Min(CacheLength.CacheAfterViewport, GetHeight(Extent) - viewportHeight - offsetInPixel);
  270. int rowCountInCacheBefore = (int)(cacheBeforeInPixel / GetHeight(childSize));
  271. int rowCountInCacheAfter = ((int)Math.Ceiling((offsetInPixel + viewportHeight + cacheAfterInPixel) / GetHeight(childSize))) - (int)Math.Ceiling((offsetInPixel + viewportHeight) / GetHeight(childSize));
  272. startIndex = Math.Max(startIndex - rowCountInCacheBefore * itemsPerRowCount, 0);
  273. endIndex = Math.Min(endIndex + rowCountInCacheAfter * itemsPerRowCount, Items.Count - 1);
  274. }
  275. else if (CacheLengthUnit == VirtualizationCacheLengthUnit.Item)
  276. {
  277. startIndex = Math.Max(startIndex - (int)CacheLength.CacheBeforeViewport, 0);
  278. endIndex = Math.Min(endIndex + (int)CacheLength.CacheAfterViewport, Items.Count - 1);
  279. }
  280. }
  281. else
  282. {
  283. double viewportSartPos = GetY(Offset);
  284. double viewportEndPos = GetY(Offset) + GetHeight(Viewport);
  285. if (CacheLengthUnit == VirtualizationCacheLengthUnit.Pixel)
  286. {
  287. viewportSartPos = Math.Max(viewportSartPos - CacheLength.CacheBeforeViewport, 0);
  288. viewportEndPos = Math.Min(viewportEndPos + CacheLength.CacheAfterViewport, GetHeight(Extent));
  289. }
  290. int startRowIndex = GetRowIndex(viewportSartPos);
  291. startIndex = startRowIndex * itemsPerRowCount;
  292. int endRowIndex = GetRowIndex(viewportEndPos);
  293. endIndex = Math.Min(endRowIndex * itemsPerRowCount + (itemsPerRowCount - 1), Items.Count - 1);
  294. if (CacheLengthUnit == VirtualizationCacheLengthUnit.Page)
  295. {
  296. int itemsPerPage = endIndex - startIndex + 1;
  297. startIndex = Math.Max(startIndex - (int)CacheLength.CacheBeforeViewport * itemsPerPage, 0);
  298. endIndex = Math.Min(endIndex + (int)CacheLength.CacheAfterViewport * itemsPerPage, Items.Count - 1);
  299. }
  300. else if (CacheLengthUnit == VirtualizationCacheLengthUnit.Item)
  301. {
  302. startIndex = Math.Max(startIndex - (int)CacheLength.CacheBeforeViewport, 0);
  303. endIndex = Math.Min(endIndex + (int)CacheLength.CacheAfterViewport, Items.Count - 1);
  304. }
  305. }
  306. return new ItemRange(startIndex, endIndex);
  307. }
  308. private int GetRowIndex(double location)
  309. {
  310. int calculatedRowIndex = (int)Math.Floor(location / GetHeight(childSize));
  311. int maxRowIndex = (int)Math.Ceiling((double)Items.Count / (double)itemsPerRowCount);
  312. return Math.Max(Math.Min(calculatedRowIndex, maxRowIndex), 0);
  313. }
  314. protected override void BringIndexIntoView(int index)
  315. {
  316. if (index < 0 || index >= Items.Count)
  317. {
  318. throw new ArgumentOutOfRangeException(nameof(index), $"The argument {nameof(index)} must be >= 0 and < the number of items.");
  319. }
  320. if (itemsPerRowCount == 0)
  321. {
  322. throw new InvalidOperationException();
  323. }
  324. var offset = (index / itemsPerRowCount) * GetHeight(childSize);
  325. if (Orientation == Orientation.Horizontal)
  326. {
  327. SetHorizontalOffset(offset);
  328. }
  329. else
  330. {
  331. SetVerticalOffset(offset);
  332. }
  333. }
  334. protected override double GetLineUpScrollAmount()
  335. {
  336. return -Math.Min(childSize.Height * ScrollLineDeltaItem, Viewport.Height);
  337. }
  338. protected override double GetLineDownScrollAmount()
  339. {
  340. return Math.Min(childSize.Height * ScrollLineDeltaItem, Viewport.Height);
  341. }
  342. protected override double GetLineLeftScrollAmount()
  343. {
  344. return -Math.Min(childSize.Width * ScrollLineDeltaItem, Viewport.Width);
  345. }
  346. protected override double GetLineRightScrollAmount()
  347. {
  348. return Math.Min(childSize.Width * ScrollLineDeltaItem, Viewport.Width);
  349. }
  350. protected override double GetMouseWheelUpScrollAmount()
  351. {
  352. return -Math.Min(childSize.Height * MouseWheelDeltaItem, Viewport.Height);
  353. }
  354. protected override double GetMouseWheelDownScrollAmount()
  355. {
  356. return Math.Min(childSize.Height * MouseWheelDeltaItem, Viewport.Height);
  357. }
  358. protected override double GetMouseWheelLeftScrollAmount()
  359. {
  360. return -Math.Min(childSize.Width * MouseWheelDeltaItem, Viewport.Width);
  361. }
  362. protected override double GetMouseWheelRightScrollAmount()
  363. {
  364. return Math.Min(childSize.Width * MouseWheelDeltaItem, Viewport.Width);
  365. }
  366. protected override double GetPageUpScrollAmount()
  367. {
  368. return -Viewport.Height;
  369. }
  370. protected override double GetPageDownScrollAmount()
  371. {
  372. return Viewport.Height;
  373. }
  374. protected override double GetPageLeftScrollAmount()
  375. {
  376. return -Viewport.Width;
  377. }
  378. protected override double GetPageRightScrollAmount()
  379. {
  380. return Viewport.Width;
  381. }
  382. /* orientation aware helper methods */
  383. protected double GetX(Point point) => Orientation == Orientation.Vertical ? point.X : point.Y;
  384. protected double GetY(Point point) => Orientation == Orientation.Vertical ? point.Y : point.X;
  385. protected double GetWidth(Size size) => Orientation == Orientation.Vertical ? size.Width : size.Height;
  386. protected double GetHeight(Size size) => Orientation == Orientation.Vertical ? size.Height : size.Width;
  387. protected Size CreateSize(double width, double height) => Orientation == Orientation.Vertical ? new Size(width, height) : new Size(height, width);
  388. protected Rect CreateRect(double x, double y, double width, double height) => Orientation == Orientation.Vertical ? new Rect(x, y, width, height) : new Rect(y, x, width, height);
  389. }
  390. }