Компановка кнопок в тулбар WPF

223
21 ноября 2018, 00:00

В WPF есть компонент ToolBar, по умолчанию используется ToolBarPanel для размещения кнопок, и ToolBarOverflowPanel -для кнопок которые не помещаются. Можно ли как то изменить ToolBarPanel на WrapPanel, чтобы кнопки размещались в 2 ряда или 3 ряда в зависимости от высоты или ширины тулбара, но если места не хватает переносились на панель переполнения? Читал, что по умолчанию ToolBarPanel использует StackPanel для компановки и ее как-то надо переопределить на WrapPanel.

Answer 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(List<ItemInfo> infos, 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)
                    {
                        UIElementCollection_InsertInternal.Invoke(
                            children, new object[] { childrenIndex, child });
                    }
                    else
                    {
                        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.");
                    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<ItemInfo> 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<UIElement>)ToolBarPanel_GeneratedItemsCollection.GetValue(this);
        Size layoutSlotSize = constraint;
        layoutSlotSize.Width = Double.PositiveInfinity;
        var infos = new List<ItemInfo>(generatedItemsCollection.Count);
        var sureInfos = new List<ItemInfo>(generatedItemsCollection.Count);
        var maybeInfos = new List<ItemInfo>(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<List<Size>>(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<Size>());
                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 или совместимый по присваиванию.

Запускаем, получаем вот что:

<DockPanel>
    <ToolBarTray DockPanel.Dock="Top">
        <ToolBar Height="52" Width="90" Style="{DynamicResource WrapToolBarStyle}">
            <Button Command="Cut" Content="Cut" />
            <Button Command="Copy" Content="Copy" />
            <Button Command="Paste" Content="Paste" />
        </ToolBar>
        <ToolBar Style="{DynamicResource WrapToolBarStyle}">
            <Button Command="Cut" Content="Cut Long Long Name" ToolBar.OverflowMode="AsNeeded"/>
            <Button Command="Copy" Content="Copy Long Name" ToolBar.OverflowMode="Never"/>
            <Button Command="Paste" Content="Paste Long Name" ToolBar.OverflowMode="Always"/>
        </ToolBar>
    </ToolBarTray>
    <TextBox AcceptsReturn="True" />
</DockPanel>

READ ALSO
Не запускается файл .exe

Не запускается файл .exe

В приложении WPF есть необходимость открыть файл с расширением exe в фоновом режимеОткрыл диспетчер задач во время нажатия кнопки, crypto_parser

188
Оптимизация c# кода

Оптимизация c# кода

Как можно оптимизировать этот код?

164
Имитация файла в потоке из строки

Имитация файла в потоке из строки

Может быть вы можете помочь или натолкнуть на правильное понимание вопроса

160
ACF как вывести метаданные рубрики?

ACF как вывести метаданные рубрики?

У меня на сайте Wordpress установлен плагин ACFЯ создал поля для рубрик и ввел туда данные

160