private static void OnPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { PropertyChangeNotifier notifier = (PropertyChangeNotifier)d; if (null != notifier.ValueChanged) { notifier.ValueChanged(notifier, EventArgs.Empty); } }
// the meat and potatos...lays out the grid private void ArrangeChildren() { // the layout strategy is: // 1. Fill all available rows // 2. Reduce the amount of free space at the end of each row // 3. Fill the top rows completely // this can be called before the control has been instantiated so _numberOfRows could be zero if (_numberOfRows == 0) return; #if DEBUG // pedantic checking of performance... System.Diagnostics.Stopwatch st = new System.Diagnostics.Stopwatch(); st.Start(); #endif // cache these for performance double itemWidth = this.ItemWidth; double itemHeight = this.ItemHeight; Thickness itemMargin = this.ItemMargin; int itemsCount = this.Children.Count; // no children, no layout if (itemsCount == 0) return; // cache for the children List<RowLayoutItem> items = new List<RowLayoutItem>(); // do we need to attach a notifier to the children? bool createItemWidthSpanChangeNotifier = false; if (this.TrackItemWidthSpanChanges && _itemWidthSpanChangeNotifier == null) { createItemWidthSpanChangeNotifier = true; _itemWidthSpanChangeNotifier = new PropertyChangeNotifier[itemsCount]; } int totalWidth = 0; int maxWidth = 0; int calculatedItemWidth; // first pass calculate the total width (in item units) and find the maximum item width // as the total width cannot be less than this int index = 0; foreach (FrameworkElement item in this.Children) { items.Add(new RowLayoutItem() { ItemWidth = calculatedItemWidth = GridPanel.GetItemWidthSpan(item), Element = item }); if (calculatedItemWidth > maxWidth) maxWidth = calculatedItemWidth; totalWidth += calculatedItemWidth; if (createItemWidthSpanChangeNotifier) { PropertyChangeNotifier pcn = new PropertyChangeNotifier(item, GridPanel.ItemWidthSpanProperty); pcn.ValueChanged += this.ItemWidthSpanChanged; _itemWidthSpanChangeNotifier[index++] = pcn; } } // second pass layout the items with a width of totalWidth / _numberOfRows // we also want to reduce the 'jaggedness' of the right hand side and so // an optimal layout is defined where the space on the right hand side is a minimum // where all rows have been laid out (otherwise it'll always lay it out in a single line!) RowLayout[] optimalRows = null; int minimumSpace = int.MaxValue; bool allRowsFilled; // width of one row int rowWidth = Math.Min(maxWidth, (totalWidth / _numberOfRows) + (totalWidth % _numberOfRows > 0 ? 1 : 0)); // temporary variables held outside the loop to reuce stack allocations RowLayout[] rows; RowLayout rowItem; RowLayoutItem rowLayoutItem; #if DEBUG int numberOfIterations = 0; #endif maxWidth = 0; int space = 0; // do this until all the elements have been laid out do { // elements in each row rows = new RowLayout[_numberOfRows]; // index into elements array index = 0; // check to see all rows in the layout are filled allRowsFilled = false; // lay out the items for (int row = 0; row < _numberOfRows; row++) { // allocate the row layout rows[row] = rowItem = new RowLayout(); // allocate elements to the row layout until they won't fit into the space or we run out of items while (index < itemsCount && rowItem.TotalRowWidth < rowWidth) { // is the item to be added greater than the allowed row width if (rowItem.TotalRowWidth + (calculatedItemWidth = (rowLayoutItem = items[index]).ItemWidth) > totalWidth) break; // if we're on the last row then all rows have been filled if (!allRowsFilled && row == _numberOfRows - 1) allRowsFilled = true; // we can fit the item so add to the row list and increment the width rowItem.RowItems.Add(rowLayoutItem); index++; // keep a copy of the maximum width if ((rowItem.TotalRowWidth += calculatedItemWidth) > maxWidth) maxWidth = rowItem.TotalRowWidth; } } #if DEBUG numberOfIterations++; #endif // if we couldn't lay out all the elements, increment the row width by 1 and try again. if (index < itemsCount) rowWidth++; // if all rows filled, check for a minimum of space // but also weigh fill from the top so that it'll look neat by multiplying by the inverse row position squared else if (allRowsFilled) { // check the free space space = 0; // calculate the free space and add the weighting for (int i = 0; i < _numberOfRows; i++) space += (maxWidth - rows[i].TotalRowWidth) * (_numberOfRows - i) * (_numberOfRows - i); // if the row has less free space, cache it if (space < minimumSpace) { minimumSpace = space; optimalRows = rows; } // increment the row width to test the next solution rowWidth++; } } // keep going until we have a blank last row (i.e. fails the criterion that all rows are filled) while (allRowsFilled); // it IS possible not to find a solution (may need to fix this) but unlikely for ItemWidthSpan = 1 or 2 if (optimalRows == null) return; // get the actual row width (not the guesstimate) rowWidth = 0; foreach (RowLayout r in optimalRows) if (r.TotalRowWidth > rowWidth) rowWidth = r.TotalRowWidth; // set the Width of the canvas (obviously, don't set the height!!) if (rowWidth > 0) { this.Width = (rowWidth * itemWidth) + ((itemMargin.Left + itemMargin.Right) * (rowWidth)); } else this.Width = 0; // layout the items double heightOffset = itemMargin.Top, widthOffset; FrameworkElement elem; foreach (RowLayout row in optimalRows) { widthOffset = itemMargin.Left; foreach (RowLayoutItem item in row.RowItems) { Canvas.SetLeft(elem = item.Element, widthOffset); Canvas.SetTop(elem, heightOffset); elem.Width = ((calculatedItemWidth = item.ItemWidth) * itemWidth) + ((itemMargin.Left + itemMargin.Right) * (calculatedItemWidth - 1)); widthOffset += (calculatedItemWidth * itemWidth) + ((itemMargin.Left + itemMargin.Right) * calculatedItemWidth); elem.Height = itemHeight; } heightOffset += itemHeight + itemMargin.Top + itemMargin.Bottom; } #if DEBUG st.Stop(); System.Diagnostics.Debug.WriteLine("GridPanel.ArrangeChildren: Number of items=" + itemsCount + ", Number of iterations=" + numberOfIterations + ", Time=" + st.Elapsed.TotalMilliseconds + "ms"); #endif }
// CTOR public GridPanel() { _actualHeightChangeNotifier = new PropertyChangeNotifier(this, GridPanel.ActualHeightProperty); _actualHeightChangeNotifier.ValueChanged += this.ActualHeightChanged; }
// the meat and potatos...lays out the grid private void ArrangeChildren() { // the layout strategy is: // 1. Fill all available rows // 2. Reduce the amount of free space at the end of each row // 3. Fill the top rows completely // this can be called before the control has been instantiated so _numberOfRows could be zero if (_numberOfRows == 0) { return; } #if DEBUG // pedantic checking of performance... System.Diagnostics.Stopwatch st = new System.Diagnostics.Stopwatch(); st.Start(); #endif // cache these for performance double itemWidth = this.ItemWidth; double itemHeight = this.ItemHeight; Thickness itemMargin = this.ItemMargin; int itemsCount = this.Children.Count; // no children, no layout if (itemsCount == 0) { return; } // cache for the children List <RowLayoutItem> items = new List <RowLayoutItem>(); // do we need to attach a notifier to the children? bool createItemWidthSpanChangeNotifier = false; if (this.TrackItemWidthSpanChanges && _itemWidthSpanChangeNotifier == null) { createItemWidthSpanChangeNotifier = true; _itemWidthSpanChangeNotifier = new PropertyChangeNotifier[itemsCount]; } int totalWidth = 0; int maxWidth = 0; int calculatedItemWidth; // first pass calculate the total width (in item units) and find the maximum item width // as the total width cannot be less than this int index = 0; foreach (FrameworkElement item in this.Children) { items.Add(new RowLayoutItem() { ItemWidth = calculatedItemWidth = GridPanel.GetItemWidthSpan(item), Element = item }); if (calculatedItemWidth > maxWidth) { maxWidth = calculatedItemWidth; } totalWidth += calculatedItemWidth; if (createItemWidthSpanChangeNotifier) { PropertyChangeNotifier pcn = new PropertyChangeNotifier(item, GridPanel.ItemWidthSpanProperty); pcn.ValueChanged += this.ItemWidthSpanChanged; _itemWidthSpanChangeNotifier[index++] = pcn; } } // second pass layout the items with a width of totalWidth / _numberOfRows // we also want to reduce the 'jaggedness' of the right hand side and so // an optimal layout is defined where the space on the right hand side is a minimum // where all rows have been laid out (otherwise it'll always lay it out in a single line!) RowLayout[] optimalRows = null; int minimumSpace = int.MaxValue; bool allRowsFilled; // width of one row int rowWidth = Math.Min(maxWidth, (totalWidth / _numberOfRows) + (totalWidth % _numberOfRows > 0 ? 1 : 0)); // temporary variables held outside the loop to reuce stack allocations RowLayout[] rows; RowLayout rowItem; RowLayoutItem rowLayoutItem; #if DEBUG int numberOfIterations = 0; #endif maxWidth = 0; int space = 0; // do this until all the elements have been laid out do { // elements in each row rows = new RowLayout[_numberOfRows]; // index into elements array index = 0; // check to see all rows in the layout are filled allRowsFilled = false; // lay out the items for (int row = 0; row < _numberOfRows; row++) { // allocate the row layout rows[row] = rowItem = new RowLayout(); // allocate elements to the row layout until they won't fit into the space or we run out of items while (index < itemsCount && rowItem.TotalRowWidth < rowWidth) { // is the item to be added greater than the allowed row width if (rowItem.TotalRowWidth + (calculatedItemWidth = (rowLayoutItem = items[index]).ItemWidth) > totalWidth) { break; } // if we're on the last row then all rows have been filled if (!allRowsFilled && row == _numberOfRows - 1) { allRowsFilled = true; } // we can fit the item so add to the row list and increment the width rowItem.RowItems.Add(rowLayoutItem); index++; // keep a copy of the maximum width if ((rowItem.TotalRowWidth += calculatedItemWidth) > maxWidth) { maxWidth = rowItem.TotalRowWidth; } } } #if DEBUG numberOfIterations++; #endif // if we couldn't lay out all the elements, increment the row width by 1 and try again. if (index < itemsCount) { rowWidth++; } // if all rows filled, check for a minimum of space // but also weigh fill from the top so that it'll look neat by multiplying by the inverse row position squared else if (allRowsFilled) { // check the free space space = 0; // calculate the free space and add the weighting for (int i = 0; i < _numberOfRows; i++) { space += (maxWidth - rows[i].TotalRowWidth) * (_numberOfRows - i) * (_numberOfRows - i); } // if the row has less free space, cache it if (space < minimumSpace) { minimumSpace = space; optimalRows = rows; } // increment the row width to test the next solution rowWidth++; } } // keep going until we have a blank last row (i.e. fails the criterion that all rows are filled) while (allRowsFilled); // it IS possible not to find a solution (may need to fix this) but unlikely for ItemWidthSpan = 1 or 2 if (optimalRows == null) { return; } // get the actual row width (not the guesstimate) rowWidth = 0; foreach (RowLayout r in optimalRows) { if (r.TotalRowWidth > rowWidth) { rowWidth = r.TotalRowWidth; } } // set the Width of the canvas (obviously, don't set the height!!) if (rowWidth > 0) { this.Width = (rowWidth * itemWidth) + ((itemMargin.Left + itemMargin.Right) * (rowWidth)); } else { this.Width = 0; } // layout the items double heightOffset = itemMargin.Top, widthOffset; FrameworkElement elem; foreach (RowLayout row in optimalRows) { widthOffset = itemMargin.Left; foreach (RowLayoutItem item in row.RowItems) { Canvas.SetLeft(elem = item.Element, widthOffset); Canvas.SetTop(elem, heightOffset); elem.Width = ((calculatedItemWidth = item.ItemWidth) * itemWidth) + ((itemMargin.Left + itemMargin.Right) * (calculatedItemWidth - 1)); widthOffset += (calculatedItemWidth * itemWidth) + ((itemMargin.Left + itemMargin.Right) * calculatedItemWidth); elem.Height = itemHeight; } heightOffset += itemHeight + itemMargin.Top + itemMargin.Bottom; } #if DEBUG st.Stop(); System.Diagnostics.Debug.WriteLine("GridPanel.ArrangeChildren: Number of items=" + itemsCount + ", Number of iterations=" + numberOfIterations + ", Time=" + st.Elapsed.TotalMilliseconds + "ms"); #endif }