Reputation: 20761
I've currently a StackPanel
with several Button
inside.
The content of the Button
are dynamic, so I don't know their size.
Each Button
has 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).
Here is what I would like to achieve(I cheated with putting some fixed values):
Upvotes: 0
Views: 3217
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
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
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
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
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