├── README.md
└── UniformGridPanel.cs
/README.md:
--------------------------------------------------------------------------------
1 | # UniformGridPanel
2 | Virtualized UniformGrid for use as a panel in WPF
3 |
4 | Example Usage:
5 | --------------
6 | ```
7 |
8 |
9 |
10 |
13 |
14 |
15 |
16 | ```
17 |
18 | Too easy!
19 |
--------------------------------------------------------------------------------
/UniformGridPanel.cs:
--------------------------------------------------------------------------------
1 | namespace Auxide.Controls
2 | {
3 | using System;
4 | using System.Collections.Specialized;
5 | using System.Diagnostics;
6 | using System.Windows;
7 | using System.Windows.Controls;
8 | using System.Windows.Controls.Primitives;
9 |
10 | public class UniformGridPanel : VirtualizingPanel, IScrollInfo
11 | {
12 | private Size _extent = new Size(0, 0);
13 | private Size _viewport = new Size(0, 0);
14 | private Point _offset = new Point(0, 0);
15 | private bool _canHorizontallyScroll = false;
16 | private bool _canVerticallyScroll = false;
17 | private ScrollViewer _owner;
18 | private int _scrollLength = 25;
19 |
20 | //-----------------------------------------
21 | //
22 | // Dependency Properties
23 | //
24 | //-----------------------------------------
25 |
26 | #region Dependency Properties
27 |
28 | ///
29 | /// Columns DependencyProperty
30 | ///
31 | public static readonly DependencyProperty ColumnsProperty = DependencyProperty.Register("Columns", typeof(int), typeof(UniformGridPanel),
32 | new FrameworkPropertyMetadata(1, FrameworkPropertyMetadataOptions.AffectsArrange | FrameworkPropertyMetadataOptions.AffectsMeasure));
33 |
34 | ///
35 | /// Rows DependencyProperty
36 | ///
37 | public static readonly DependencyProperty RowsProperty = DependencyProperty.Register("Rows", typeof(int), typeof(UniformGridPanel),
38 | new FrameworkPropertyMetadata(1, FrameworkPropertyMetadataOptions.AffectsArrange | FrameworkPropertyMetadataOptions.AffectsMeasure));
39 |
40 | ///
41 | /// Orientation DependencyProperty
42 | ///
43 | public static readonly DependencyProperty OrientationProperty = DependencyProperty.RegisterAttached("Orientation", typeof(Orientation), typeof(UniformGridPanel),
44 | new FrameworkPropertyMetadata(Orientation.Vertical, FrameworkPropertyMetadataOptions.AffectsArrange | FrameworkPropertyMetadataOptions.AffectsMeasure));
45 |
46 | #endregion Dependency Properties
47 |
48 |
49 | //-----------------------------------------
50 | //
51 | // Public Properties
52 | //
53 | //-----------------------------------------
54 |
55 | #region Public Properties
56 |
57 | ///
58 | /// Get/Set the amount of columns this grid should have
59 | ///
60 | public int Columns
61 | {
62 | get { return (int)this.GetValue(ColumnsProperty); }
63 | set { this.SetValue(ColumnsProperty, value); }
64 | }
65 |
66 | ///
67 | /// Get/Set the amount of rows this grid should have
68 | ///
69 | public int Rows
70 | {
71 | get { return (int)this.GetValue(RowsProperty); }
72 | set { this.SetValue(RowsProperty, value); }
73 | }
74 |
75 | ///
76 | /// Get/Set the orientation of the panel
77 | ///
78 | public Orientation Orientation
79 | {
80 | get { return (Orientation)this.GetValue(OrientationProperty); }
81 | set { this.SetValue(OrientationProperty, value); }
82 | }
83 |
84 | #endregion Public Properties
85 |
86 |
87 | //-----------------------------------------
88 | //
89 | // Overrides
90 | //
91 | //-----------------------------------------
92 |
93 | #region Overrides
94 |
95 | ///
96 | /// When items are removed, remove the corresponding UI if necessary
97 | ///
98 | ///
99 | ///
100 | protected override void OnItemsChanged(object sender, ItemsChangedEventArgs args)
101 | {
102 | switch (args.Action)
103 | {
104 | case NotifyCollectionChangedAction.Remove:
105 | case NotifyCollectionChangedAction.Replace:
106 | case NotifyCollectionChangedAction.Move:
107 | RemoveInternalChildRange(args.Position.Index, args.ItemUICount);
108 | break;
109 | }
110 | }
111 |
112 | ///
113 | /// Measure the children
114 | ///
115 | /// Size available
116 | /// Size desired
117 | protected override Size MeasureOverride(Size availableSize)
118 | {
119 | UpdateScrollInfo(availableSize);
120 |
121 | int firstVisibleItemIndex, lastVisibleItemIndex;
122 | GetVisibleRange(out firstVisibleItemIndex, out lastVisibleItemIndex);
123 |
124 | // We need to access InternalChildren before the generator to work around a bug
125 | UIElementCollection children = this.InternalChildren;
126 | IItemContainerGenerator generator = this.ItemContainerGenerator;
127 |
128 | // Get the generator position of the first visible data item
129 | GeneratorPosition startPos = generator.GeneratorPositionFromIndex(firstVisibleItemIndex);
130 |
131 | // Get index where we'd insert the child for this position. If the item is realized
132 | // (position.Offset == 0), it's just position.Index, otherwise we have to add one to
133 | // insert after the corresponding child
134 | int childIndex = (startPos.Offset == 0) ? startPos.Index : startPos.Index + 1;
135 |
136 | using (generator.StartAt(startPos, GeneratorDirection.Forward, true))
137 | {
138 | for (int itemIndex = firstVisibleItemIndex; itemIndex <= lastVisibleItemIndex; ++itemIndex, ++childIndex)
139 | {
140 | bool newlyRealized;
141 |
142 | // Get or create the child
143 | UIElement child = generator.GenerateNext(out newlyRealized) as UIElement;
144 |
145 | childIndex = Math.Max(0, childIndex);
146 |
147 | if (newlyRealized)
148 | {
149 | // Figure out if we need to insert the child at the end or somewhere in the middle
150 | if (childIndex >= children.Count)
151 | {
152 | base.AddInternalChild(child);
153 | }
154 | else
155 | {
156 | base.InsertInternalChild(childIndex, child);
157 | }
158 |
159 | generator.PrepareItemContainer(child);
160 | }
161 | else
162 | {
163 | // The child has already been created, let's be sure it's in the right spot
164 | Debug.Assert(child == children[childIndex], "Wrong child was generated");
165 | }
166 |
167 | // Measurements will depend on layout algorithm
168 | child.Measure(GetChildSize(availableSize));
169 | }
170 | }
171 |
172 | // Note: this could be deferred to idle time for efficiency
173 | CleanUpItems(firstVisibleItemIndex, lastVisibleItemIndex);
174 |
175 | if (availableSize.Height.Equals(double.PositiveInfinity))
176 | {
177 | Debug.WriteLine(_extent);
178 | return new Size(200, 200);
179 | }
180 |
181 | return availableSize;
182 | }
183 |
184 | ///
185 | /// Arrange the children
186 | ///
187 | /// Size available
188 | /// Size used
189 | protected override Size ArrangeOverride(Size finalSize)
190 | {
191 | IItemContainerGenerator generator = this.ItemContainerGenerator;
192 |
193 | UpdateScrollInfo(finalSize);
194 |
195 | for (int i = 0; i < this.Children.Count; i++)
196 | {
197 | UIElement child = this.Children[i];
198 |
199 | int itemIndex = generator.IndexFromGeneratorPosition(new GeneratorPosition(i, 0));
200 |
201 | ArrangeChild(itemIndex, child, finalSize);
202 | }
203 |
204 | return finalSize;
205 | }
206 |
207 | #endregion Overrides
208 |
209 | //-----------------------------------------
210 | //
211 | // Layout Specific Code
212 | //
213 | //-----------------------------------------
214 |
215 | #region Layout Specific Code
216 |
217 | ///
218 | /// Revisualizes items that are no longer visible
219 | ///
220 | /// first item index that should be visible
221 | /// last item index that should be visible
222 | private void CleanUpItems(int minDesiredGenerated, int maxDesiredGenerated)
223 | {
224 | UIElementCollection children = this.InternalChildren;
225 | IItemContainerGenerator generator = this.ItemContainerGenerator;
226 |
227 | for (int i = children.Count - 1; i >= 0; i--)
228 | {
229 | GeneratorPosition childGeneratorPos = new GeneratorPosition(i, 0);
230 | int itemIndex = generator.IndexFromGeneratorPosition(childGeneratorPos);
231 | if (itemIndex < minDesiredGenerated || itemIndex > maxDesiredGenerated)
232 | {
233 | generator.Remove(childGeneratorPos, 1);
234 | RemoveInternalChildRange(i, 1);
235 | }
236 | }
237 | }
238 |
239 | ///
240 | /// Calculate the extent of the view based on the available size
241 | ///
242 | ///
243 | ///
244 | private Size MeasureExtent(Size availableSize, int itemsCount)
245 | {
246 | Size childSize = GetChildSize(availableSize);
247 |
248 | if (this.Orientation == System.Windows.Controls.Orientation.Horizontal)
249 | {
250 | return new Size((this.Columns * childSize.Width) * Math.Ceiling((double)itemsCount / (this.Columns * this.Rows)), _viewport.Height);
251 | }
252 | else
253 | {
254 | var pageHeight = (this.Rows * childSize.Height);
255 |
256 | var sizeWidth = _viewport.Width;
257 | var sizeHeight = pageHeight * Math.Ceiling((double)itemsCount / (this.Rows * this.Columns));
258 |
259 | return new Size(sizeWidth, sizeHeight);
260 | }
261 | }
262 |
263 |
264 | ///
265 | /// Arrange the individual children
266 | ///
267 | ///
268 | ///
269 | ///
270 | private void ArrangeChild(int index, UIElement child, Size finalSize)
271 | {
272 | int row = index / this.Columns;
273 | int column = index % this.Columns;
274 |
275 | double xPosition, yPosition;
276 |
277 | int currentPage;
278 | Size childSize = GetChildSize(finalSize);
279 |
280 | if (this.Orientation == System.Windows.Controls.Orientation.Horizontal)
281 | {
282 | currentPage = (int)Math.Floor((double)index / (this.Columns * this.Rows));
283 |
284 | xPosition = (currentPage * this._viewport.Width) + (column * childSize.Width);
285 | yPosition = (row % this.Rows) * childSize.Height;
286 |
287 | xPosition -= this._offset.X;
288 | yPosition -= this._offset.Y;
289 | }
290 | else
291 | {
292 | xPosition = (column * childSize.Width) - this._offset.X;
293 | yPosition = (row * childSize.Height) - this._offset.Y;
294 | }
295 |
296 | child.Arrange(new Rect(xPosition, yPosition, childSize.Width, childSize.Height));
297 | }
298 |
299 | ///
300 | /// Get the size of the child element
301 | ///
302 | ///
303 | /// Returns the size of the child
304 | private Size GetChildSize(Size availableSize)
305 | {
306 | double width = availableSize.Width / this.Columns;
307 | double height = availableSize.Height / this.Rows;
308 |
309 | return new Size(width, height);
310 | }
311 |
312 | ///
313 | /// Get the range of children that are visible
314 | ///
315 | /// The item index of the first visible item
316 | /// The item index of the last visible item
317 | private void GetVisibleRange(out int firstVisibleItemIndex, out int lastVisibleItemIndex)
318 | {
319 | Size childSize = GetChildSize(this._extent);
320 |
321 | int pageSize = this.Columns * this.Rows;
322 | int pageNumber = this.Orientation == System.Windows.Controls.Orientation.Horizontal ?
323 | (int)Math.Floor((double)this._offset.X / this._viewport.Width) :
324 | (int)Math.Floor((double)this._offset.Y / this._viewport.Height);
325 |
326 | firstVisibleItemIndex = (pageNumber * pageSize);
327 | lastVisibleItemIndex = firstVisibleItemIndex + (pageSize * 2) - 1;
328 |
329 | ItemsControl itemsControl = ItemsControl.GetItemsOwner(this);
330 | int itemCount = itemsControl.HasItems ? itemsControl.Items.Count : 0;
331 |
332 |
333 | if (lastVisibleItemIndex >= itemCount)
334 | {
335 | lastVisibleItemIndex = itemCount - 1;
336 | }
337 | }
338 |
339 | #endregion
340 |
341 |
342 | //-----------------------------------------
343 | //
344 | // IScrollInfo Implementation
345 | //
346 | //-----------------------------------------
347 |
348 | #region IScrollInfo Implementation
349 |
350 | public bool CanHorizontallyScroll
351 | {
352 | get { return _canHorizontallyScroll; }
353 | set { _canHorizontallyScroll = value; }
354 | }
355 |
356 | public bool CanVerticallyScroll
357 | {
358 | get { return _canVerticallyScroll; }
359 | set { _canVerticallyScroll = value; }
360 | }
361 |
362 | ///
363 | /// Get the extent height
364 | ///
365 | public double ExtentHeight
366 | {
367 | get { return this._extent.Height; }
368 | }
369 |
370 | ///
371 | /// Get the extent width
372 | ///
373 | public double ExtentWidth
374 | {
375 | get { return this._extent.Width; }
376 | }
377 |
378 | ///
379 | /// Get the current horizontal offset
380 | ///
381 | public double HorizontalOffset
382 | {
383 | get { return this._offset.X; }
384 | }
385 |
386 | ///
387 | /// Get the current vertical offset
388 | ///
389 | public double VerticalOffset
390 | {
391 | get { return this._offset.Y; }
392 | }
393 |
394 | ///
395 | /// Get/Set the scrollowner
396 | ///
397 | public System.Windows.Controls.ScrollViewer ScrollOwner
398 | {
399 | get { return this._owner; }
400 | set { this._owner = value; }
401 | }
402 |
403 | ///
404 | /// Get the Viewport Height
405 | ///
406 | public double ViewportHeight
407 | {
408 | get { return _viewport.Height; }
409 | }
410 |
411 | ///
412 | /// Get the Viewport Width
413 | ///
414 | public double ViewportWidth
415 | {
416 | get { return _viewport.Width; }
417 | }
418 |
419 |
420 |
421 | public void LineLeft()
422 | {
423 | this.SetHorizontalOffset(this._offset.X - _scrollLength);
424 | }
425 |
426 | public void LineRight()
427 | {
428 | this.SetHorizontalOffset(this._offset.X + _scrollLength);
429 | }
430 |
431 | public void LineUp()
432 | {
433 | this.SetVerticalOffset(this._offset.Y - _scrollLength);
434 | }
435 | public void LineDown()
436 | {
437 | this.SetVerticalOffset(this._offset.Y + _scrollLength);
438 | }
439 |
440 | public Rect MakeVisible(System.Windows.Media.Visual visual, Rect rectangle)
441 | {
442 | return new Rect();
443 | }
444 |
445 | public void MouseWheelDown()
446 | {
447 | if (this.Orientation == System.Windows.Controls.Orientation.Horizontal)
448 | {
449 | this.SetHorizontalOffset(this._offset.X + _scrollLength);
450 | }
451 | else
452 | {
453 | this.SetVerticalOffset(this._offset.Y + _scrollLength);
454 | }
455 | }
456 |
457 | public void MouseWheelUp()
458 | {
459 | if (this.Orientation == System.Windows.Controls.Orientation.Horizontal)
460 | {
461 | this.SetHorizontalOffset(this._offset.X - _scrollLength);
462 | }
463 | else
464 | {
465 | this.SetVerticalOffset(this._offset.Y - _scrollLength);
466 | }
467 | }
468 |
469 | public void MouseWheelLeft()
470 | {
471 | return;
472 | }
473 |
474 | public void MouseWheelRight()
475 | {
476 | return;
477 | }
478 |
479 | public void PageDown()
480 | {
481 | this.SetVerticalOffset(this._offset.Y + _viewport.Width);
482 | }
483 |
484 | public void PageUp()
485 | {
486 | this.SetVerticalOffset(this._offset.Y - _viewport.Width);
487 | }
488 |
489 | public void PageLeft()
490 | {
491 | this.SetHorizontalOffset(this._offset.X - _viewport.Width);
492 | }
493 |
494 | public void PageRight()
495 | {
496 | this.SetHorizontalOffset(this._offset.X + _viewport.Width);
497 | }
498 |
499 |
500 | public void SetHorizontalOffset(double offset)
501 | {
502 | _offset.X = Math.Max(0, offset);
503 |
504 | if (_owner != null)
505 | {
506 | _owner.InvalidateScrollInfo();
507 | }
508 |
509 | InvalidateMeasure();
510 | }
511 |
512 | public void SetVerticalOffset(double offset)
513 | {
514 | _offset.Y = Math.Max(0, offset);
515 |
516 | if (_owner != null)
517 | {
518 | _owner.InvalidateScrollInfo();
519 | }
520 |
521 | InvalidateMeasure();
522 | }
523 |
524 | private void UpdateScrollInfo(Size availableSize)
525 | {
526 | // See how many items there are
527 | ItemsControl itemsControl = ItemsControl.GetItemsOwner(this);
528 | int itemCount = itemsControl.HasItems ? itemsControl.Items.Count : 0;
529 |
530 | Size extent = MeasureExtent(availableSize, itemCount);
531 | // Update extent
532 | if (extent != _extent)
533 | {
534 | _extent = extent;
535 | if (_owner != null)
536 | _owner.InvalidateScrollInfo();
537 | }
538 |
539 | // Update viewport
540 | if (availableSize != _viewport)
541 | {
542 | _viewport = availableSize;
543 | if (_owner != null)
544 | _owner.InvalidateScrollInfo();
545 | }
546 | }
547 |
548 | #endregion IScrollInfo Implementation
549 | }
550 | }
551 |
--------------------------------------------------------------------------------