#c_sharp #wpf #xaml #toolbar
В WPF есть компонент ToolBar, по умолчанию используется ToolBarPanel для размещения кнопок, и ToolBarOverflowPanel -для кнопок которые не помещаются. Можно ли как то изменить ToolBarPanel на WrapPanel, чтобы кнопки размещались в 2 ряда или 3 ряда в зависимости от высоты или ширины тулбара, но если места не хватает переносились на панель переполнения? Читал, что по умолчанию ToolBarPanel использует StackPanel для компановки и ее как-то надо переопределить на WrapPanel.
Ответы
Ответ 1
Смотрите, ToolBarPanel просто унаследован от StackPanel. Поэтому единственная возможность перекрыть то, как он располагает дочерние элементы — это унаследоваться от ToolBarPanel и перекрыть MeasureOverride и ArrangeOverride. Тут на самом деле реально много работы. Проще всего подсмотреть логику во WrapPanel (оттуда я стащил код расположения контролов по строкам) и ToolBarPanel (глядя в него, я понял, как работать с MinLength, MaxLength и ToolBarOverflowPanel в MeasureOverride). Поскольку в коде используются internal-методы и свойства, мне пришлось выцепить их через рефлексию. Получился вот такой монстрик: class WrapToolBarPanel : ToolBarPanel { // for reflection static DependencyPropertyKey ToolBar_HasOverflowItemsPropertyKey; static DependencyPropertyKey ToolBar_IsOverflowItemPropertyKey; static PropertyInfo Toolbar_ToolBarOverflowPanel; static PropertyInfo ToolBarPanel_GeneratedItemsCollection; static PropertyInfo ToolBarPanel_MinLength, ToolBarPanel_MaxLength; static MethodInfo UIElementCollection_InsertInternal, UIElementCollection_AddInternal, UIElementCollection_RemoveNoVerify; static WrapToolBarPanel() { ToolBar_HasOverflowItemsPropertyKey = (DependencyPropertyKey)typeof(ToolBar) .GetField("HasOverflowItemsPropertyKey", BindingFlags.Static | BindingFlags.NonPublic) .GetValue(null); ToolBar_IsOverflowItemPropertyKey = (DependencyPropertyKey)typeof(ToolBar) .GetField("IsOverflowItemPropertyKey", BindingFlags.Static | BindingFlags.NonPublic) .GetValue(null); Toolbar_ToolBarOverflowPanel = typeof(ToolBar).GetProperty( "ToolBarOverflowPanel", BindingFlags.Instance | BindingFlags.NonPublic); ToolBarPanel_GeneratedItemsCollection = typeof(ToolBarPanel).GetProperty( "GeneratedItemsCollection", BindingFlags.Instance | BindingFlags.NonPublic); UIElementCollection_InsertInternal = typeof(UIElementCollection).GetMethod( "InsertInternal", BindingFlags.Instance | BindingFlags.NonPublic); UIElementCollection_AddInternal = typeof(UIElementCollection).GetMethod( "AddInternal", BindingFlags.Instance | BindingFlags.NonPublic); UIElementCollection_RemoveNoVerify = typeof(UIElementCollection).GetMethod( "RemoveNoVerify", BindingFlags.Instance | BindingFlags.NonPublic); ToolBarPanel_MinLength = typeof(ToolBarPanel).GetProperty( "MinLength", BindingFlags.Instance | BindingFlags.NonPublic); ToolBarPanel_MaxLength = typeof(ToolBarPanel).GetProperty( "MaxLength", BindingFlags.Instance | BindingFlags.NonPublic); } // adapted from https://referencesource.microsoft.com/, WrapPanel and ToolBarPanel private bool MeasureGeneratedItems(Listinfos, int numberOfAsNeededItems) { ToolBar toolBar = TemplatedParent as ToolBar; ToolBarOverflowPanel overflowPanel = null; if (toolBar != null) overflowPanel = (ToolBarOverflowPanel)Toolbar_ToolBarOverflowPanel .GetValue(toolBar); bool hasOverflowItems = false; bool overflowNeedsInvalidation = false; UIElementCollection children = InternalChildren; int childrenCount = children.Count; int childrenIndex = 0; int asNeededIndex = 0; foreach (var info in infos) { UIElement child = info.Item; OverflowMode overflowMode = info.Mode; bool sendToMain = false; if (overflowMode == OverflowMode.Never) sendToMain = true; if (overflowMode == OverflowMode.AsNeeded) { sendToMain = asNeededIndex < numberOfAsNeededItems; asNeededIndex++; } DependencyObject visualParent = VisualTreeHelper.GetParent(child); if (sendToMain) { // ensure it's this panel's child if (visualParent != this) { // if it's a child of overflow panel, reseat it if ((visualParent == overflowPanel) && (overflowPanel != null)) { overflowPanel.Children.Remove(child); } if (childrenIndex < childrenCount) { // InternalChildren.InsertInternal(childrenIndex, child); UIElementCollection_InsertInternal.Invoke( children, new object[] { childrenIndex, child }); } else { // InternalChildren.AddInternal(child); UIElementCollection_AddInternal.Invoke( children, new object[] { child }); } childrenCount++; } Debug.Assert(children[childrenIndex] == child, "InternalChildren is out of sync with _generatedItemsCollection."); childrenIndex++; } else { hasOverflowItems = true; child.SetValue(ToolBar_IsOverflowItemPropertyKey, true); // If the child is in this panel's visual tree, remove it. if (visualParent == this) { Debug.Assert(children[childrenIndex] == child, "InternalChildren is out of sync with _generatedItemsCollection."); // InternalChildren.RemoveNoVerify(child); UIElementCollection_RemoveNoVerify.Invoke( children, new object[] { child }); childrenCount--; overflowNeedsInvalidation = true; } // If the child isn't connected to the visual tree, // notify the overflow panel to pick it up. else if (visualParent == null) { overflowNeedsInvalidation = true; } } } // A child was added to the overflow panel, but since we don't add it // to the overflow panel's visual collection until that panel's measure // pass, we need to mark it as measure dirty. if (overflowNeedsInvalidation && (overflowPanel != null)) { overflowPanel.InvalidateMeasure(); } return hasOverflowItems; } struct ItemInfo { public UIElement Item; public OverflowMode Mode; public Size? SizeInMainPart; public int Index; } Size MeasureItemCollection(double maxExtent, IEnumerable items) { var curLineSize = new Size(); var panelSize = new Size(); foreach (var child in items) { var sz = child.SizeInMainPart.Value; if (DoubleUtil.GreaterThan(curLineSize.Width + sz.Width, maxExtent)) { // need to switch to another line panelSize.Width = Math.Max(curLineSize.Width, panelSize.Width); panelSize.Height += curLineSize.Height; curLineSize = sz; if (DoubleUtil.GreaterThan(sz.Width, maxExtent)) { // the element is wider then the constraint, arrangement not possible return new Size(maxExtent, double.NaN); } } else //continue to accumulate a line { curLineSize.Width += sz.Width; curLineSize.Height = Math.Max(sz.Height, curLineSize.Height); } } //the last line size, if any should be added panelSize.Width = Math.Max(curLineSize.Width, panelSize.Width); panelSize.Height += curLineSize.Height; return panelSize; } protected override Size MeasureOverride(Size constraint) { // workaround, otherwise generatedItemsCollection can be null var dummy = InternalChildren; var generatedItemsCollection = (List )ToolBarPanel_GeneratedItemsCollection.GetValue(this); Size layoutSlotSize = constraint; layoutSlotSize.Width = Double.PositiveInfinity; var infos = new List (generatedItemsCollection.Count); var sureInfos = new List (generatedItemsCollection.Count); var maybeInfos = new List (generatedItemsCollection.Count); int idx = 0; foreach (var child in generatedItemsCollection) { var ii = new ItemInfo() { Item = child, Mode = ToolBar.GetOverflowMode(child), Index = idx++ }; if (ii.Mode != OverflowMode.Always) { child.Measure(layoutSlotSize); ii.SizeInMainPart = child.DesiredSize; } if (ii.Mode == OverflowMode.AsNeeded) child.SetValue(ToolBar_IsOverflowItemPropertyKey, false); infos.Add(ii); if (ii.Mode == OverflowMode.Never) sureInfos.Add(ii); if (ii.Mode == OverflowMode.AsNeeded) maybeInfos.Add(ii); } Size minSize = MeasureItemCollection(constraint.Width, sureInfos); bool hasAlwaysOverflowItems = DoubleUtil.GreaterThan(minSize.Height, constraint.Height); // evaluate minimal vertical size and set MinLength double minWidth = 0; if (sureInfos.Count > 0) { var rows = new List >(sureInfos.Count) { sureInfos.Select(ii => ii.SizeInMainPart.Value).ToList() }; Size StackHor(Size s1, Size s2) => new Size(s1.Width + s2.Width, Math.Max(s1.Height, s2.Height)); Size StackVer(Size s1, Size s2) => new Size(Math.Max(s1.Width, s2.Width), s1.Height + s2.Height); minWidth = rows[0].Sum(size => size.Width); while (true) { var rowBoundingBoxes = rows.Select(r => r.Aggregate(StackHor)).ToList(); var boundingBox = rowBoundingBoxes.Aggregate(StackVer); if (DoubleUtil.GreaterThan(boundingBox.Height, constraint.Height)) break; minWidth = boundingBox.Width; var longestIndex = rowBoundingBoxes.IndexOfMaxBy(size => size.Width); var longestRow = rows[longestIndex]; if (longestRow.Count <= 1) break; // cannot make smaller if (longestIndex == rows.Count - 1) rows.Add(new List
()); rows[longestIndex + 1].Insert(0, longestRow.Last()); longestRow.RemoveAt(longestRow.Count - 1); } } ToolBarPanel_MinLength.SetValue(this, minWidth); var candidateList = infos.Where(ii => ii.Mode != OverflowMode.Always).ToList(); ToolBarPanel_MaxLength.SetValue( this, candidateList.Sum(ii => ii.SizeInMainPart.Value.Width)); Size candidateSize = new Size(); int asNeededCount = maybeInfos.Count; while (candidateList.Count > 0) { candidateSize = MeasureItemCollection(constraint.Width, candidateList); if (!double.IsNaN(candidateSize.Height) && !DoubleUtil.GreaterThan(candidateSize.Height, constraint.Height)) break; var asNeededIdx = candidateList.FindLastIndex(ii => ii.Mode == OverflowMode.AsNeeded); if (asNeededIdx == -1) break; candidateList.RemoveAt(asNeededIdx); asNeededCount--; } if (candidateList.Count == 0) candidateSize = new Size(); bool hasAsNeededOverflowItems = MeasureGeneratedItems(infos, asNeededCount); ToolBar toolbar = TemplatedParent as ToolBar; if (toolbar != null) toolbar.SetValue(ToolBar_HasOverflowItemsPropertyKey, hasAlwaysOverflowItems || hasAsNeededOverflowItems); return candidateSize; } // Arrange намного проще protected override Size ArrangeOverride(Size finalSize) { int firstInLine = 0; double accumulatedHeight = 0; var curLineSize = new Size(); var children = InternalChildren; for (int i = 0, count = children.Count; i < count; i++) { var child = children[i] as UIElement; if (child == null) continue; var sz = child.DesiredSize; if (DoubleUtil.GreaterThan(curLineSize.Width + sz.Width, finalSize.Width)) { // need to switch to another line arrangeLine(accumulatedHeight, curLineSize.Height, firstInLine, i); accumulatedHeight += curLineSize.Height; curLineSize = sz; if (DoubleUtil.GreaterThan(sz.Width, finalSize.Width)) { // the element is wider then the constraint - give it a separate line // switch to next line which only contain one element arrangeLine(accumulatedHeight, sz.Height, i, ++i); accumulatedHeight += sz.Height; curLineSize = new Size(); } firstInLine = i; } else //continue to accumulate a line { curLineSize.Width += sz.Width; curLineSize.Height = Math.Max(sz.Height, curLineSize.Height); } } //arrange the last line, if any if (firstInLine < children.Count) arrangeLine(accumulatedHeight, curLineSize.Height, firstInLine, children.Count); return finalSize; } private void arrangeLine(double h, double lineHeight, int start, int end) { double w = 0; var children = InternalChildren; for (int i = start; i < end; i++) { UIElement child = children[i] as UIElement; if (child != null) { var childSize = child.DesiredSize; child.Arrange(new Rect( w, h, childSize.Width, lineHeight)); w += childSize.Width; } } } } и служебный класс internal static class DoubleUtil { internal const double DBL_EPSILON = 2.2204460492503131E-16; public static bool AreClose(double value1, double value2) { if (value1 == value2) return true; double num = (Math.Abs(value1) + Math.Abs(value2) + 10.0) * DBL_EPSILON; double num2 = value1 - value2; if (0.0 - num < num2) return num > num2; return false; } public static bool GreaterThan(double value1, double value2) { if (value1 > value2) return !AreClose(value1, value2); return false; } } Смысл алгоритма таков: мы считаем размеры всех блоков, убираем те, которые можно, по одному, если блоки не влазят в отведённый квадрат, и убранные блоки переносим в выпадающий список. При расстановке мы идём по строкам, и когда строка оканчивается, переходим вниз на следующую строку. Но это ещё не всё, вопрос в том, как подключить наш класс вместо стандартного? Это довольно просто: вы создаёте шаблон для ToolBar'а (через меню Edit Style...), и в нём заменяете ToolBarPanel на local:WrapToolBarPanel. Заметьте, что просто поменять ToolBarPanel на WrapPanel не пойдёт, т. к. код ToolBar ожидает, что контрол будет иметь тип ToolBarPanel или совместимый по присваиванию. Запускаем, получаем вот что:
Комментариев нет:
Отправить комментарий