Dunge
Dunge

Reputation: 89

WPF Toolbar line wrapping

I have a WPF application with a ToolBarTray composed of a few ToolBar with tons of buttons. The only thing I want is for them to always be visible, no matter the size of the window.

<ToolBarTray>
    <ToolBar OverflowMode="Never">
        <userControls:SearchUserControl x:Name="SearchControl" />
    </ToolBar>
    <ToolBar ItemsSource="{Binding CommonCommands}" OverflowMode="Never"/>
    <ToolBar ItemsSource="{Binding Type1Commands}" OverflowMode="Never"/>
    <ToolBar ItemsSource="{Binding Type2Commands}" OverflowMode="Never" />
    <ToolBar ItemsSource="{Binding Type3Commands}" OverflowMode="Never" />
</ToolBarTray>

I set OverflowMode="Never" to get rid of the little arrow on the right where buttons goes when there's not enough space since I always want all buttons visible. My "commands" list are RoutedCommand with a DataTemplate to show up as a button, but it react the same whatever I use.

If I put the ToolBarTray inside a StackPanel, their buttons/toolbars just continue past the window size. If I put the ToolBarTray inside a WrapPanel, instead of wrapping it hide the buttons completely (only the toolbar grip remains).

The comportment I would love to achieve is for toolbars to change their Band property dynamically so that if there's no space available, the toolbar switch to the second band (row) instead of hiding the buttons in their overflow panel.

Upvotes: 1

Views: 3007

Answers (2)

Daap
Daap

Reputation: 365

Four years after the original question, but if I understand the problem correctly it is possible by adding the ToolBar controls in a WrapPanel instead of a ToolBarTray. The downside is that the functionality of the ToolBarTray such as moving ToolBars within the tray is lost, but if that is not a problem it works well otherwise.

Upvotes: 1

Il Vic
Il Vic

Reputation: 5666

Wrapping a ToolBar's content is not easy, but of course it is possible. The point is that the standard ToolBar control wants a TemplatePart called "PART_ToolBarPanel", whose type must be ToolBarPanel. The ToolBarPanel inherits from StackPanel, but for achieving your target, you should use a WrapPanel. So since the ToolBarPanel inherits from StackPanel, it cannot inherit at the same time from WrapPanel.

My idea is to extend the ToolBarPanel and to disguise it so it looks like a WrapPanel. First of all let's see the code of our ToolBarPanel (I used ILSpy in order to retrive the WrapPanel code behavior):

public class ToolBarPanel : System.Windows.Controls.Primitives.ToolBarPanel
{
    private struct UVSize
    {
        internal double U;
        internal double V;
        private Orientation _orientation;
        internal double Width
        {
            get
            {
                if (this._orientation != Orientation.Horizontal)
                {
                    return this.V;
                }
                return this.U;
            }
            set
            {
                if (this._orientation == Orientation.Horizontal)
                {
                    this.U = value;
                    return;
                }
                this.V = value;
            }
        }
        internal double Height
        {
            get
            {
                if (this._orientation != Orientation.Horizontal)
                {
                    return this.U;
                }
                return this.V;
            }
            set
            {
                if (this._orientation == Orientation.Horizontal)
                {
                    this.V = value;
                    return;
                }
                this.U = value;
            }
        }
        internal UVSize(Orientation orientation, double width, double height)
        {
            this.U = (this.V = 0.0);
            this._orientation = orientation;
            this.Width = width;
            this.Height = height;
        }
        internal UVSize(Orientation orientation)
        {
            this.U = (this.V = 0.0);
            this._orientation = orientation;
        }
    }

    private static int _itemWidth = 80;
    private static int _itemHeight = 30;

    private static bool IsWidthHeightValid(object value)
    {
        double num = (double)value;
        return DoubleUtil.IsNaN(num) || (num >= 0.0 && !double.IsPositiveInfinity(num));
    }

    protected override Size ArrangeOverride(Size finalSize)
    {
        int num = 0;
        double itemWidth = _itemWidth;
        double itemHeight = _itemHeight;
        double num2 = 0.0;
        double itemU = (this.Orientation == Orientation.Horizontal) ? itemWidth : itemHeight;
        ToolBarPanel.UVSize uVSize = new ToolBarPanel.UVSize(this.Orientation);
        ToolBarPanel.UVSize uVSize2 = new ToolBarPanel.UVSize(this.Orientation, finalSize.Width, finalSize.Height);
        bool flag = !DoubleUtil.IsNaN(itemWidth);
        bool flag2 = !DoubleUtil.IsNaN(itemHeight);
        bool useItemU = (this.Orientation == Orientation.Horizontal) ? flag : flag2;
        UIElementCollection internalChildren = base.InternalChildren;
        int i = 0;
        int count = internalChildren.Count;
        while (i < count)
        {
            UIElement uIElement = internalChildren[i];
            if (uIElement != null)
            {
                ToolBarPanel.UVSize uVSize3 = new ToolBarPanel.UVSize(this.Orientation, flag ? itemWidth : uIElement.DesiredSize.Width, flag2 ? itemHeight : uIElement.DesiredSize.Height);
                if (DoubleUtil.GreaterThan(uVSize.U + uVSize3.U, uVSize2.U))
                {
                    this.ArrangeLine(num2, uVSize.V, num, i, useItemU, itemU);
                    num2 += uVSize.V;
                    uVSize = uVSize3;
                    if (DoubleUtil.GreaterThan(uVSize3.U, uVSize2.U))
                    {
                        this.ArrangeLine(num2, uVSize3.V, i, ++i, useItemU, itemU);
                        num2 += uVSize3.V;
                        uVSize = new ToolBarPanel.UVSize(this.Orientation);
                    }
                    num = i;
                }
                else
                {
                    uVSize.U += uVSize3.U;
                    uVSize.V = Math.Max(uVSize3.V, uVSize.V);
                }
            }
            i++;
        }
        if (num < internalChildren.Count)
        {
            this.ArrangeLine(num2, uVSize.V, num, internalChildren.Count, useItemU, itemU);
        }
        return finalSize;
    }

    private void ArrangeLine(double v, double lineV, int start, int end, bool useItemU, double itemU)
    {
        double num = 0.0;
        bool flag = this.Orientation == Orientation.Horizontal;
        UIElementCollection internalChildren = base.InternalChildren;
        for (int i = start; i < end; i++)
        {
            UIElement uIElement = internalChildren[i];
            if (uIElement != null)
            {
                ToolBarPanel.UVSize uVSize = new ToolBarPanel.UVSize(this.Orientation, uIElement.DesiredSize.Width, uIElement.DesiredSize.Height);
                double num2 = useItemU ? itemU : uVSize.U;
                uIElement.Arrange(new Rect(flag ? num : v, flag ? v : num, flag ? num2 : lineV, flag ? lineV : num2));
                num += num2;
            }
        }
    }

    protected override Size MeasureOverride(Size constraint)
    {
        ToolBarPanel.UVSize uVSize = new ToolBarPanel.UVSize(this.Orientation);
        ToolBarPanel.UVSize uVSize2 = new ToolBarPanel.UVSize(this.Orientation);
        ToolBarPanel.UVSize uVSize3 = new ToolBarPanel.UVSize(this.Orientation, constraint.Width, constraint.Height);
        double itemWidth = _itemWidth;
        double itemHeight = _itemHeight;
        bool flag = !DoubleUtil.IsNaN(itemWidth);
        bool flag2 = !DoubleUtil.IsNaN(itemHeight);
        Size availableSize = new Size(flag ? itemWidth : constraint.Width, flag2 ? itemHeight : constraint.Height);
        UIElementCollection internalChildren = base.InternalChildren;
        int i = 0;
        int count = internalChildren.Count;
        while (i < count)
        {
            UIElement uIElement = internalChildren[i];
            if (uIElement != null)
            {
                uIElement.Measure(availableSize);
                ToolBarPanel.UVSize uVSize4 = new ToolBarPanel.UVSize(this.Orientation, flag ? itemWidth : uIElement.DesiredSize.Width, flag2 ? itemHeight : uIElement.DesiredSize.Height);
                if (DoubleUtil.GreaterThan(uVSize.U + uVSize4.U, uVSize3.U))
                {
                    uVSize2.U = Math.Max(uVSize.U, uVSize2.U);
                    uVSize2.V += uVSize.V;
                    uVSize = uVSize4;
                    if (DoubleUtil.GreaterThan(uVSize4.U, uVSize3.U))
                    {
                        uVSize2.U = Math.Max(uVSize4.U, uVSize2.U);
                        uVSize2.V += uVSize4.V;
                        uVSize = new ToolBarPanel.UVSize(this.Orientation);
                    }
                }
                else
                {
                    uVSize.U += uVSize4.U;
                    uVSize.V = Math.Max(uVSize4.V, uVSize.V);
                }
            }
            i++;
        }
        uVSize2.U = Math.Max(uVSize.U, uVSize2.U);
        uVSize2.V += uVSize.V;
        return new Size(uVSize2.Width, uVSize2.Height);
    }
}

Just for the purpose of this sample, I used two static variables (_itemWidth and _itemHeight) to define the WrapPanel item size. Anyway they should be replaced with two dependecy properties. Moreover the extended ToolBar does not handle the Orientation changes (just use ILSpy to adapt it).

In order to make this code work, I added the DoubleUtil class too:

public static class DoubleUtil
{
    [StructLayout(LayoutKind.Explicit)]
    private struct NanUnion
    {
        [FieldOffset(0)]
        internal double DoubleValue;
        [FieldOffset(0)]
        internal ulong UintValue;
    }
    internal const double DBL_EPSILON = 2.2204460492503131E-16;
    internal const float FLT_MIN = 1.17549435E-38f;
    public static bool AreClose(double value1, double value2)
    {
        if (value1 == value2)
        {
            return true;
        }
        double num = (Math.Abs(value1) + Math.Abs(value2) + 10.0) * 2.2204460492503131E-16;
        double num2 = value1 - value2;
        return -num < num2 && num > num2;
    }
    public static bool LessThan(double value1, double value2)
    {
        return value1 < value2 && !DoubleUtil.AreClose(value1, value2);
    }
    public static bool GreaterThan(double value1, double value2)
    {
        return value1 > value2 && !DoubleUtil.AreClose(value1, value2);
    }
    public static bool LessThanOrClose(double value1, double value2)
    {
        return value1 < value2 || DoubleUtil.AreClose(value1, value2);
    }
    public static bool GreaterThanOrClose(double value1, double value2)
    {
        return value1 > value2 || DoubleUtil.AreClose(value1, value2);
    }
    public static bool IsOne(double value)
    {
        return Math.Abs(value - 1.0) < 2.2204460492503131E-15;
    }
    public static bool IsZero(double value)
    {
        return Math.Abs(value) < 2.2204460492503131E-15;
    }
    public static bool AreClose(Point point1, Point point2)
    {
        return DoubleUtil.AreClose(point1.X, point2.X) && DoubleUtil.AreClose(point1.Y, point2.Y);
    }
    public static bool AreClose(Size size1, Size size2)
    {
        return DoubleUtil.AreClose(size1.Width, size2.Width) && DoubleUtil.AreClose(size1.Height, size2.Height);
    }
    public static bool AreClose(Vector vector1, Vector vector2)
    {
        return DoubleUtil.AreClose(vector1.X, vector2.X) && DoubleUtil.AreClose(vector1.Y, vector2.Y);
    }
    public static bool AreClose(Rect rect1, Rect rect2)
    {
        if (rect1.IsEmpty)
        {
            return rect2.IsEmpty;
        }
        return !rect2.IsEmpty && DoubleUtil.AreClose(rect1.X, rect2.X) && DoubleUtil.AreClose(rect1.Y, rect2.Y) && DoubleUtil.AreClose(rect1.Height, rect2.Height) && DoubleUtil.AreClose(rect1.Width, rect2.Width);
    }
    public static bool IsBetweenZeroAndOne(double val)
    {
        return DoubleUtil.GreaterThanOrClose(val, 0.0) && DoubleUtil.LessThanOrClose(val, 1.0);
    }
    public static int DoubleToInt(double val)
    {
        if (0.0 >= val)
        {
            return (int)(val - 0.5);
        }
        return (int)(val + 0.5);
    }
    public static bool RectHasNaN(Rect r)
    {
        return DoubleUtil.IsNaN(r.X) || DoubleUtil.IsNaN(r.Y) || DoubleUtil.IsNaN(r.Height) || DoubleUtil.IsNaN(r.Width);
    }
    public static bool IsNaN(double value)
    {
        DoubleUtil.NanUnion nanUnion = default(DoubleUtil.NanUnion);
        nanUnion.DoubleValue = value;
        ulong num = nanUnion.UintValue & 18442240474082181120uL;
        ulong num2 = nanUnion.UintValue & 4503599627370495uL;
        return (num == 9218868437227405312uL || num == 18442240474082181120uL) && num2 != 0uL;
    }
}

Now we need just to define a couple of resources in the XAML (ILSpy again):

<Style x:Key="ToolbarThumb" TargetType="{x:Type Thumb}">
    <Setter Property="Control.Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type Thumb}">
                <Border Padding="{TemplateBinding Control.Padding}" Background="#00FFFFFF" SnapsToDevicePixels="True">
                    <Rectangle>
                        <Rectangle.Fill>
                            <DrawingBrush Viewbox="0,0,4,4" Viewport="0,0,4,4" TileMode="Tile" ViewportUnits="Absolute" ViewboxUnits="Absolute">
                                <DrawingBrush.Drawing>
                                    <DrawingGroup>
                                        <DrawingGroup.Children>
                                            <GeometryDrawing Brush="#FFFFFFFF" Geometry="M1,1L1,3 3,3 3,1z" />
                                            <GeometryDrawing Brush="#A0A0A0" Geometry="M0,0L0,2 2,2 2,0z" />
                                        </DrawingGroup.Children>
                                    </DrawingGroup>
                                </DrawingBrush.Drawing>
                            </DrawingBrush>
                        </Rectangle.Fill>
                    </Rectangle>
                </Border>
                <ControlTemplate.Triggers>
                    <Trigger Property="UIElement.IsMouseOver" Value="True">
                        <Setter Property="FrameworkElement.Cursor" Value="SizeAll" />
                    </Trigger>
                </ControlTemplate.Triggers>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>
<Style x:Key="NoOverflowItems" TargetType="ToolBar" BasedOn="{StaticResource {x:Type ToolBar}}">
    <Setter Property="Control.Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type ToolBar}">
                <Grid Name="Grid" Margin="3,1,1,1" SnapsToDevicePixels="True">
                    <Border Name="MainPanelBorder" Background="{TemplateBinding Control.Background}" BorderBrush="{TemplateBinding Control.BorderBrush}" BorderThickness="{TemplateBinding Control.BorderThickness}" Padding="{TemplateBinding Control.Padding}">
                        <DockPanel KeyboardNavigation.TabIndex="1" KeyboardNavigation.TabNavigation="Local">
                            <Thumb Name="ToolBarThumb" Style="{StaticResource ToolbarThumb}" Margin="-3,-1,0,0" Width="10" Padding="6,5,1,6" />
                            <ContentPresenter Name="ToolBarHeader" ContentSource="Header" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="4,0,4,0" SnapsToDevicePixels="{TemplateBinding UIElement.SnapsToDevicePixels}" />
                            <local:ToolBarPanel x:Name="PART_ToolBarPanel" IsItemsHost="True" Margin="0,1,2,2" SnapsToDevicePixels="{TemplateBinding UIElement.SnapsToDevicePixels}" />
                        </DockPanel>
                    </Border>
                </Grid>
                <ControlTemplate.Triggers>
                    <Trigger Value="{x:Null}" Property="HeaderedItemsControl.Header">
                        <Setter TargetName="ToolBarHeader" Property="UIElement.Visibility" Value="Collapsed" />
                    </Trigger>
                    <Trigger Property="ToolBarTray.IsLocked" Value="True">
                        <Setter TargetName="ToolBarThumb" Property="UIElement.Visibility" Value="Collapsed" />
                    </Trigger>
                    <Trigger Property="ToolBar.Orientation" Value="Vertical">
                        <Setter TargetName="Grid" Property="FrameworkElement.Margin" Value="1,3,1,1" />
                        <Setter TargetName="ToolBarThumb" Property="FrameworkElement.Height" Value="10" />
                        <Setter TargetName="ToolBarThumb" Property="FrameworkElement.Width" Value="Auto" />
                        <Setter TargetName="ToolBarThumb" Property="FrameworkElement.Margin" Value="-1,-3,0,0" />
                        <Setter TargetName="ToolBarThumb" Property="Control.Padding" Value="5,6,6,1" />
                        <Setter TargetName="ToolBarHeader" Property="FrameworkElement.Margin" Value="0,0,0,4" />
                        <Setter TargetName="PART_ToolBarPanel" Property="FrameworkElement.Margin" Value="1,0,2,2" />
                        <Setter TargetName="ToolBarThumb" Property="DockPanel.Dock" Value="Top" />
                        <Setter TargetName="ToolBarHeader" Property="DockPanel.Dock" Value="Top" />

                        <Setter TargetName="MainPanelBorder" Property="FrameworkElement.Margin" Value="0,0,0,11" />
                    </Trigger>
                    <Trigger Property="UIElement.IsEnabled" Value="False">
                        <Setter Value="{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}" Property="Control.Foreground" />
                    </Trigger>
                </ControlTemplate.Triggers>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

Now let's create a small Window (300x300) and add this XAML:

<ToolBarTray>
    <ToolBar OverflowMode="Never" Band="1">
        <Button Content="Button1" Width="76" Height="26" Margin="2" />
        <Button Content="Button2" Width="76" Height="26" Margin="2" />
        <Button Content="Button3" Width="76" Height="26" Margin="2" />
        <Button Content="Button4" Width="76" Height="26" Margin="2" />
        <Button Content="Button5" Width="76" Height="26" Margin="2" />
        <Button Content="Button6" Width="76" Height="26" Margin="2" />
    </ToolBar>
    <ToolBar Style="{StaticResource NoOverflowItems}" Band="2">
        <Button Content="ButtonA" Width="76" Height="26" Margin="2" />
        <Button Content="ButtonB" Width="76" Height="26" Margin="2" />
        <Button Content="ButtonC" Width="76" Height="26" Margin="2" />
        <Button Content="ButtonD" Width="76" Height="26" Margin="2" />
        <Button Content="ButtonF" Width="76" Height="26" Margin="2" />
        <Button Content="ButtonG" Width="76" Height="26" Margin="2" />
    </ToolBar>
</ToolBarTray>

I hope my code can help you.

Upvotes: 3

Related Questions