J4N
J4N

Reputation: 20761

WPF: Ensure that button have the same Width

I've currently a StackPanel with several Button inside. The content of the Button are dynamic, so I don't know their size.

Each Buttonhas different margins.

I would like to have all my buttons having the same width(without counting the Margin in it).

Here is an example:

        <StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
            <Button Margin="3" Content="{Binding PreviousButtonText, ElementName=CurrentControl}" Command="{Binding GoToPreviousPageCommand}"/>
            <Button Margin="3,3,8,3"  Content="{Binding NextButtonText, ElementName=CurrentControl}" Command="{Binding GoToNextPageCommand}" Visibility="{Binding IsLastPage, Converter={StaticResource BooleanToVisibleConverter}, ConverterParameter=true, UpdateSourceTrigger=PropertyChanged}"/>
            <Button Margin="3,3,8,3"  Content="{Binding FinishButtonText, ElementName=CurrentControl}" Command="{Binding FinishCommand}" Visibility="{Binding IsLastPage, Converter={StaticResource BooleanToVisibleConverter}, UpdateSourceTrigger=PropertyChanged}"/>
            <Button Margin="8,3,3,3" Content="{Binding CancelButtonText, ElementName=CurrentControl}" Command="{Binding CancelCommand}"/>
        </StackPanel>

Since I need to have the same width, but not count the Margin in it, I can't use UniformGrid(Or Grid with SharedSizeGroup).

The goal is to do have all buttons occupying the "max width" required by one of the button.

This has to be done in XAML, no code behind. Another thing, the Text inside the buttons can be changed on runtime.

Any idea how to do this?

Edit it seems that many people didn't understood that I need a Button with the same width, not a button+his margin having the same width.

All your solutions gives something like this: where you can clearly see that the button doesn't have the same size(I increased the "8" margin value to better show the issue). enter image description here

Here is what I would like to achieve(I cheated with putting some fixed values): enter image description here

Upvotes: 0

Views: 3217

Answers (5)

tomab
tomab

Reputation: 2151

Here is a small class which inherits the standard WPF Button control. It ensures that controls within same container share same width when any of the buttons changes its size and becomes the one with max width. However, this is only a prrof of concept and you should complete it accordingly to your containers (maybe are not just StackPanels), maybe to treat errors with exceptions, not return; statements and so on.

Later edit: you specified that no code-behind solution should be here. Are new control classes forbidden too in your requirements? If yes, this doesn't apply then.

internal class WidthButton : Button
{
    private static Dictionary<Panel, List<Button>> _containers = new Dictionary<Panel, List<Button>>();

    public WidthButton()
    {
        this.Initialized += WidthButton_Initialized;
        this.SizeChanged += WidthButton_SizeChanged;
    }

    void WidthButton_Initialized(object sender, EventArgs e)
    {
        var parent = VisualTreeHelper.GetParent(this) as Panel;
        if (parent == null) return;

        var thisButton = sender as Button;
        if (thisButton == null) return;

        if (!_containers.ContainsKey(parent))
        {
           _containers.Add(parent, new List<Button>()); 
        }

        if (!_containers[parent].Contains(thisButton))
        {
            _containers[parent].Add(thisButton);
        }
    }

    void WidthButton_SizeChanged(object sender, System.Windows.SizeChangedEventArgs e)
    {
        var thisButton = sender as Button;
        if (thisButton == null) return;

        var containerPair = _containers.FirstOrDefault(pair => pair.Value.Contains(thisButton));
        if (containerPair.Value == null) return;

        var maxWidth = containerPair.Value.Max(btn => btn.ActualWidth);
        containerPair.Value.ForEach(btn => btn.Width = maxWidth);
    }
}

Later later edit: here is an example which uses an attached behavior (potential memory leak as there is no unsubscribing to SizeChanged event)

internal class WidthBehavior
{
    private static Dictionary<string, List<FrameworkElement>> _scopes = new Dictionary<string, List<FrameworkElement>>();

    public static readonly DependencyProperty WidthShareScopeProperty = DependencyProperty.RegisterAttached(
        "WidthShareScope", typeof (string), typeof (WidthBehavior), new PropertyMetadata(default(string), PropertyChangedCallback));

    private static void PropertyChangedCallback(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs dependencyPropertyChangedEventArgs)
    {
        var elem = dependencyObject as FrameworkElement;
        if (elem == null) return;

        var scope = dependencyPropertyChangedEventArgs.NewValue as string;
        if (scope == null) return;

        if (!_scopes.ContainsKey(scope))
        {
            _scopes.Add(scope, new List<FrameworkElement>());
        }

        if (!_scopes[scope].Contains(elem))
        {
            _scopes[scope].Add(elem);
            elem.SizeChanged += elem_SizeChanged;
        }
    }

    static void elem_SizeChanged(object sender, SizeChangedEventArgs e)
    {
        var elem = sender as FrameworkElement;
        if (elem == null) return;

        var scope = GetWidthShareScope(elem);
        ArrangeScope(scope);
    }

    private static void ArrangeScope(string scope)
    {
        if (!_scopes.ContainsKey(scope)) return;

        var list = _scopes[scope];

        var maxWidth = list.Max(elem => elem.ActualWidth);
        list.ForEach(elem => elem.Width = maxWidth);
    }

    public static void SetWidthShareScope(DependencyObject element, string value)
    {
        element.SetValue(WidthShareScopeProperty, value);
    }

    public static string GetWidthShareScope(DependencyObject element)
    {
        return (string) element.GetValue(WidthShareScopeProperty);
    }
}

You can use it like this:

<StackPanel Orientation="Horizontal">

        <Button Margin="10 15 13 12"
                behavior:WidthBehavior.WidthShareScope="Scope1"
                Content="Text"/>
        <Button Margin="7 2 14 11"
                behavior:WidthBehavior.WidthShareScope="Scope1"
                Content="Longer Text"/>

    </StackPanel>

Upvotes: 3

Liero
Liero

Reputation: 27360

Do you want to share the same value for some property, width property in this case? create style for it:

<Style x:Key="DialogButton" BaseOn="{StaticResource YourDefaultButtonStyle}">
   <Setter Property="Width" Value="100" />
</Style>

Or use some layouting panel, e.g Grid:

<Grid IsSharedSizeScope="True">
   <Grid.ColumnDefinitions>
        <ColumnDefinition Width="Auto" SharedColumnSize="Button"/>
        <ColumnDefinition Width="10"/>
        <ColumnDefinition Width="Auto" SharedColumnSize="Button"/>
        <ColumnDefinition Width="50"/>
        <ColumnDefinition Width="Auto" SharedColumnSize="Button"/>
    </Grid.ColumnDefinitions>

    <Button Grid.Column="0" Margin="0" />
    <Button Grid.Column="2" Margin="0" />
    <Button Grid.Column="4" Margin="0" />

</Grid>

EDIT: one more suggestion: it's good if your buttons have the same width across entire app if possible, not just single view. Create explicit style with MinWidth specified.

<Style x:Key="DialogButton" BaseOn="{StaticResource YourDefaultButtonStyle}">
   <Setter Property="MinWidth" Value="100" />
</Style>

All buttons with this style will have the same width, except the text exceeds the minwidth. This is how buttons in most dialogs works (e.g. in Visual Studio Options dialog, Save dialog, etc..)

Upvotes: 1

aicu
aicu

Reputation: 13

Use implicit style?

But you need to know which Button has max width and binds it to the style.

    <StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
        <StackPanel.Resources>
            <Style TargetType="{x:Type Button}">
                <Setter Property="Width" Value="{Binding ButtonMaxWidth}"/>
            </Style>
        </StackPanel.Resources>
        <Button Margin="3" Content="11" />
        <Button Margin="3,3,8,3" Content="2222" />
        <Button Margin="3,3,8,3" Content="333333" />
        <Button Margin="8,3,3,3" Content="44444444" />
    </StackPanel>

Upvotes: 0

Kryptos
Kryptos

Reputation: 875

When I have Margin/Padding issues, I prefer to have an enclosing control element to deal with it independently of the original control. One solution would be to imitiate the Margin with a Border with no thickness, no brush and a Padding with the desired Margin.

Then if the following solution, you must know in advance which Button will have the maximum width. Because we need to copy the ActualWidth of this Button to the Width of all the other.

<StackPanel HorizontalAlignment="Right" Orientation="Horizontal">
    <Border BorderBrush="Transparent"
            BorderThickness="0"
            Padding="3">
        <Button x:Name="MaxWidthButton"
                Content="{Binding Path=PreviousButtonText, FallbackValue=VeryLongTextThatIKnowItsLength}" />
    </Border>
    <Border BorderBrush="Transparent"
            BorderThickness="0"
            Padding="3,3,8,3">
        <Button Width="{Binding ElementName=MaxWidthButton, Path=ActualWidth}" Content="{Binding Path=NextButtonText}" />
    </Border>
    <Border BorderBrush="Transparent"
            BorderThickness="0"
            Padding="3,3,8,3">
        <Button Width="{Binding ElementName=MaxWidthButton, Path=ActualWidth}" Content="{Binding Path=FinishButtonText}" />
    </Border>
    <Border BorderBrush="Transparent"
            BorderThickness="0"
            Padding="8,3,3,3">
        <Button Width="{Binding ElementName=MaxWidthButton, Path=ActualWidth}" Content="{Binding Path=CancelButtonText}" />
    </Border>
</StackPanel>

Upvotes: 0

Xiaoy312
Xiaoy312

Reputation: 14477

Alternatively, you can also use UniformGrid. Simply set UniformGrid.Rows to 1 to achieve the same behavior of Orientation="Horizontal" :

<UniformGrid Rows="1" HorizontalAlignment="Right">
    <Button Margin="3" Content="{Binding PreviousButtonText, ElementName=CurrentControl}" Command="{Binding GoToPreviousPageCommand}"/>
    <Button Margin="3,3,8,3"  Content="{Binding NextButtonText, ElementName=CurrentControl}" Command="{Binding GoToNextPageCommand}" Visibility="{Binding IsLastPage, Converter={StaticResource BooleanToVisibleConverter}, ConverterParameter=true, UpdateSourceTrigger=PropertyChanged}"/>
    <Button Margin="3,3,8,3"  Content="{Binding FinishButtonText, ElementName=CurrentControl}" Command="{Binding FinishCommand}" Visibility="{Binding IsLastPage, Converter={StaticResource BooleanToVisibleConverter}, UpdateSourceTrigger=PropertyChanged}"/>
    <Button Margin="8,3,3,3" Content="{Binding CancelButtonText, ElementName=CurrentControl}" Command="{Binding CancelCommand}"/>
</UniformGrid>

Upvotes: 2

Related Questions