├── 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 | --------------------------------------------------------------------------------