I have a set of buttons I am trying to make the same width, rather than the default width. However, the maximum width they should be is to make the button with the longest text display nicely.
First, I tried using a Grid and using columns with a * width, but that filled the entire window. It was suggested that I use a UniformGrid, but that had the same result. I cannot just set the width because the value in the button can change at any time by the user setting which language the button should display in.
I eventually set up a MultiBinding and got things to almost work, with the button resizing up when the locale is changed. My problem now is that it does not resize back down.
Here is my XAML content; I hard-coded the text in so this should be copy/pasteable as-is:
<StackPanel Orientation="Horizontal"
HorizontalAlignment="Right"
Margin="0,10,0,0">
<Button Name="DeleteBranchOkButton"
Content="Ok"
HorizontalAlignment="Left"
Padding="5">
<Button.MinWidth>
<MultiBinding Converter="{StaticResource EqualWidthConverter}">
<Binding ElementName="DeleteBranchCancelButton" Path="ActualWidth" />
</MultiBinding>
</Button.MinWidth>
</Button>
<Button Name="DeleteBranchCancelButton"
Content="Cancel"
HorizontalAlignment="Left"
Margin="10,0,0,0"
Padding="5">
<Button.MinWidth>
<MultiBinding Converter="{StaticResource EqualWidthConverter}">
<Binding ElementName="DeleteBranchOkButton" Path="ActualWidth" />
</MultiBinding>
</Button.MinWidth>
</Button>
</StackPanel>
My converter implementation is:
public class EqualWidthConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
return Math.Abs(values.Cast<double>().Max()) < .1 ? -1 : values.Cast<double>().Max();
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
return new object[] {};
}
}
The problem now is allowing the buttons to resize back down if the language is changed and the buttons are wider than necessary. One potential solution I identified is injecting the settings control into each view, handling the languageChanged event, and setting the MinWidth property to 0 in the code behind.
I would rather not do that, however, unless it is the only method possible; I would much rather have each control not need to know about the settings implementation. Is there a better way to do this, perhaps without even needing the converter?
This is what using a Grid results in:
This is what I want:
Grid.IsSharedSizeScope and SharedSizeGroup to the rescue! These two properties let you specify grid columns/rows to share the same width/height, even across multiple grids.
Solution without wrapping
<Grid Grid.IsSharedSizeScope="True">
<Grid.ColumnDefinitions>
<ColumnDefinition SharedSizeGroup="a"></ColumnDefinition>
<ColumnDefinition SharedSizeGroup="a"></ColumnDefinition>
<ColumnDefinition SharedSizeGroup="a"></ColumnDefinition>
</Grid.ColumnDefinitions>
<Button Grid.Column="0">New Branch</Button>
<Button Grid.Column="1">Merge</Button>
<Button Grid.Column="2">Delete Branch in French!</Button>
</Grid>
Solution with wrapping
<WrapPanel Grid.IsSharedSizeScope="True">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition SharedSizeGroup="a"></ColumnDefinition>
</Grid.ColumnDefinitions>
<Button>New Branch</Button>
</Grid>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition SharedSizeGroup="a"></ColumnDefinition>
</Grid.ColumnDefinitions>
<Button>Merge</Button>
</Grid>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition SharedSizeGroup="a"></ColumnDefinition>
</Grid.ColumnDefinitions>
<Button>Delete Branch in French!</Button>
</Grid>
</WrapPanel>
There are several possible solutions to your problem. The most trivial and straightforward would be using Buttons of equal size placed either within the layout Grid control with three equally-sized columns (use *) and Buttons' properties set to default Stretch mode, or place them into StackPanel (as per your sample); then encapsulate the Button's TexBlock into:
<Viewbox>
<TextBlock Text="YourText" Style="{Optional}"/>
</Viewbox>
in order to resize/fit the content nicely.
Related
My goal is to save a GridSplitter position for later recall. The splitter is inside a Grid control that has three columns defined thusly:
<Grid.ColumnDefinitions>
<ColumnDefinition Width="{Binding GridPanelWidth, Mode=TwoWay}" />
<ColumnDefinition Width="3" /> <!--splitter itself is in this column-->
<ColumnDefinition Width="2*" />
</Grid.ColumnDefinitions>
The property GridPanelWidth is defined this way in the view model:
private GridLength _gridPanelWidth = new GridLength(1, GridUnitType.Star);
public GridLength GridPanelWidth
{
get { return _gridPanelWidth; }
set
{
if (_gridPanelWidth != value)
SetProperty(ref _gridPanelWidth, value, () => GridPanelWidth);
}
}
The problem I am having is that when the splitter is moved, the binding updates only the Double (Value) component of the bound property, but not the GridUnitType part of it.
Example: the property defaults to 1*. User drags the splitter and the value becomes 354*, instead of just 354. On restoring the value, then, it's huge (354 times, not 354 pixels).
Why is this happening, and what would you do about it?
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="{Binding GridPanelWidth, Mode=TwoWay}" />
<ColumnDefinition Width="4" />
<!--splitter itself is in this column-->
<ColumnDefinition x:Name="RightColumn" Width="2*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Border
BorderBrush="Gray"
BorderThickness="1"
Grid.Column="0"
Grid.Row="0"
/>
<GridSplitter
Background="SteelBlue"
ResizeBehavior="PreviousAndNext"
ResizeDirection="Columns"
VerticalAlignment="Stretch"
HorizontalAlignment="Stretch"
ShowsPreview="False"
Grid.Column="1"
Grid.Row="0"
/>
<Border
BorderBrush="Gray"
BorderThickness="1"
Grid.Column="2"
Grid.Row="0"
/>
<StackPanel
Grid.Row="1"
Grid.ColumnSpan="3"
Grid.Column="0"
>
<TextBlock>
<Run>GridPanelWidth: </Run>
<Run Text="{Binding GridPanelWidth.Value, Mode=OneWay}" />
<Run Text="{Binding GridPanelWidth.GridUnitType, Mode=OneWay}" />
</TextBlock>
<TextBlock>
<Run>RightColumn.Width: </Run>
<Run Text="{Binding Width.Value, ElementName=RightColumn, Mode=OneWay}" />
<Run Text="{Binding Width.GridUnitType, ElementName=RightColumn, Mode=OneWay}" />
</TextBlock>
</StackPanel>
</Grid>
Screenshot 1:
Screenshot 2:
Screenshot 3:
Res ipsa loquitor, as far as I'm concerned, but just to be on the safe side:
Because the parent may be resized, the grid splitter changes the ratio between the two columns, while preserving the GridUnitType.Star for each one so that when the parent is resized, the ratio will naturally remain constant. This preserves the intent of the initial Width values in the XAML.
Width.Value for the left column turns out to be identical to the left Border's ActualWidth, and the same holds true for the right column. You'll have to grab both and save the ratio.
Update
I find Grid/GridSplitter a bit overengineered for everyday use when all I want is Yet Another Navigation Pane, so I recently wrote a SplitterControl that has two content properties and sets up the grid and splitter, with styling, in the template. I hadn't gotten around to making the split ratio persistent, so I did that just now.
What I did was rather painful because the control is configurable, and the code isn't all that great, but I can share if you're interested.
The business end is simple:
When a column resizes, set a flag to block recursion and
PaneRatio = _PART_ContentColumn.Width.Value / _PART_NavColumn.Width.Value;
When PaneRatio changes, if it wasn't set by the column size change handler
_PART_NavColumn.Width = new GridLength(1, GridUnitType.Star);
_PART_ContentColumn.Width = new GridLength(PaneRatio, GridUnitType.Star);
In practice, the navigator/content columns can be swapped, or they can be rows instead. Both of those are done by switching templates on a HeaderedContentControl that's a child of the split control template.
I am fairly new to WPF and MVVM and a newb in general, so thank you in advance for your patience.
I am using a custom class in my model, and I have an ObservableCollection of that custom object in my viewmodel. In the class' constructor, I am adding the object to the collection when it is instantiated. In my view, I am using a DataGrid that is bound to the collection to list all active instances of the class. I am trying to implement a drag-and-drop from the DataGrid onto a trash can icon that would allow a user to dispose of unneeded instances of the class.
The problem is that when you click anything in the DataGrid, the program immediately crashes with an ArgumentOutOfRange exception - ("The given DisplayIndex is out of range. DisplayIndex must be greater than or equal to 0 and less than Columns.Count." "Actual value was 0"). DisplayIndex seems to relate to the DataGrid column, so this exception is probably due to the fact that I am not displaying any columns in the traditional sense - in my DataGrid, AutoGenerateColumns is set to False, and I am displaying everything I need to display using a RowDetailsTemplate. (The reason for this is that the area where I am displaying the DataGrid is narrow, so I need a nested, item-specific grid to represent the item properly.) The DataGrid displays and syncs with the collection fine, but obviously has some issues. I have read dozens of links on DataGrid crashes, and haven't found anything involving this exception.
My desired behavior is to pass the custom object represented by the DataGrid item to a target when I drag and drop it. I don't care which "column" they clicked or anything else - I just need a way to pass either an object reference or a SelectedIndex (the items index in the collection) to a method in the viewmodel.
Thank you in advance for any help! The offending bit of code (XAML) seems to be:
<ScrollViewer DockPanel.Dock="Bottom" Margin="2" Width="180" ScrollViewer.VerticalScrollBarVisibility="Auto">
<DataGrid ItemsSource="{Binding Path=myCollection, Mode=OneWay}" AutoGenerateColumns="False" RowDetailsVisibilityMode="Visible" HeadersVisibility="None">
<DataGrid.RowDetailsTemplate>
<DataTemplate DataType="model:myClass">
<Border CornerRadius="10" Background="AliceBlue">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="50" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" Grid.Column="0" Text="{Binding MyString1}" FontSize="21" VerticalAlignment="Bottom" />
<TextBlock Grid.Row="0" Grid.Column="1" Text="{Binding MyCustomProperty, Converter={StaticResource MyIValueConverter}}" VerticalAlignment="Bottom" />
<TextBlock Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="2" Text="{Binding MyString2}" TextWrapping="Wrap" />
<Image Source="/Resources/image1.png" Grid.Column="2" Grid.Row="0">
<Image.DataContext>
<Properties:Resources/>
</Image.DataContext>
</Image>
<Image Source="/Resources/image2.png" Grid.Column="2" Grid.Row="1">
<Image.DataContext>
<Properties:Resources/>
</Image.DataContext>
</Image>
</Grid>
</Border>
</DataTemplate>
</DataGrid.RowDetailsTemplate>
</DataGrid>
</ScrollViewer>
The issue was indeed because I am not generating any "traditional" columns. This is apparently a known bug, documented here: Microsoft Bug Report
As a workaround, I just defined an invisible column within the DataGrid and it seems to behave properly now:
<DataGrid.Columns>
<DataGridTemplateColumn Visibility="Hidden" />
</DataGrid.Columns>
I have a Label & a Border(the line) which I want to show as follows
The problem is client's name can be of any size and then it overlaps on the line.
Is there any way to relate the line to the size of the label?
Note: Both components are in the same cell of Grid.
That's what Grid is for. You can put a grid inside the cell of a grid, or you could use the outer grid in combination with ColumnSpan:
<Grid MaxWidth="240">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label Content="Client Name" Grid.Column="0" />
<Border Grid.Column="1" ... />
</Grid>
The first column gets as much space as it needs, the second as much as it can get (which can be more, or less, than it needs). The MaxWidth which I put on the grid is optional. it makes sure the Client Name is cut off if it exceeds a certain length.
There are several other ways of doing this, but I find Grid is the most flexible and the easiest to maintain, despite requiring more characters to write.
The approach given by kidshaw:
<DockPanel LastChildFill="True">
<Label Content="Client Name" DockPanel.Dock="Left" />
<Border ... />
</DockPanel>
The next one will draw the label on top of the border, but requires knowing the background color, which won't work if the background is a gradient or image:
<Border ... />
<Label HorizontalAlignment="Left" Content="Client Name" Background="White" />
Here's a different question that, although the question asked is quite different, has the same answers: How to get StackPanel's children to fill maximum space downward?
I post this example although plenty of ones was mentioned already.
<StackPanel Orientation="Horizontal" VerticalAlignment="Center" Width="125">
<TextBox Text="Lie" Name="Label1" MaxWidth="{Binding RelativeSource={RelativeSource AncestorType=StackPanel}, Path=Width}"/>
<Border Background="CadetBlue" Height="5">
<Border.Width>
<MultiBinding Converter="{StaticResource Converter}">
<Binding RelativeSource="{RelativeSource AncestorType=StackPanel}" Path="Width"/>
<Binding ElementName="Label1" Path="ActualWidth"/>
</MultiBinding>
</Border.Width>
</Border>
</StackPanel>
[ValueConversion(typeof(double), typeof(double))]
public class MyConverterS : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
double panelWidth, elementWidth;
Double.TryParse(values[0].ToString(), out panelWidth);
Double.TryParse(values[1].ToString(), out elementWidth);
if (panelWidth - elementWidth <= 0)
return 0;
return panelWidth - elementWidth;
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
Peter offers a good answer +1.
An alternative if interested is the dock panel.
Set both controls to left and set the panel to last child fill...
<DockPanel LastChildFill="True">
<Label DockPanel.Dock="Left>
Label text
<\Label>
<Path dockpanel.dock="Left"/>
<\DockPanel>
I am trying to make a wpf program with an rectangle in it and two sliders at the top:
The first slider changes the left/right margin of the rectangle and the right slider changes the top/bottom margin, so it makes it look like the rectangle is getting bigger and smaller. However, if you change the window size, the rectangle is adjusting.
Here is my code:
<Window x:Class="Rechteck.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525"
>
<Grid>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="50*"/>
<ColumnDefinition Width="50*"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" HorizontalAlignment="Center">Rand top/bottom</TextBlock>
<Slider Grid.Column="0"
x:Name="MySlider"
VerticalAlignment="Top"
Minimum="10"
Maximum="300"
Value="150"
Margin="10" />
<TextBlock Grid.Column="1" HorizontalAlignment="Center">Rand left/right</TextBlock>
<Slider Grid.Column="1"
x:Name="MySlider2"
VerticalAlignment="Top"
Minimum="10"
Maximum="300"
Value="150"
Margin="10" />
</Grid>
<Grid>
<Rectangle x:Name="rechteck"
Width="300"
Height="300"
Margin="{Binding ElementName=MySlider2, Path=Value}"
Fill="black"
VerticalAlignment="Center"
HorizontalAlignment="Center" />
</Grid>
</Grid>
</Window>
If you want your Rectangle to maintain size when Window is resized then instead of binding Margin and keeping Width and Height fixed bind them to appropriate Slider:
<Rectangle
x:Name="rechteck"
Width="{Binding ElementName=MySlider2, Path=Value}"
Height="{Binding ElementName=MySlider, Path=Value}"
Fill="black"
VerticalAlignment="Center"
HorizontalAlignment="Center"/>
EDIT
If you want Rectangle to adjust to Window size then don't set fixed Width/Height and HorizontalAlignment/VerticalAlignment and only bind Margin via custom IMultiValueConverter:
public class MarginConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
return new Thickness((double)values[0], (double)values[1], (double)values[0], (double)values[1]);
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
and change your Grid as follows:
<Window ... xmlns:local="clr-namespace:Namespace.For.MarginConverter">
<Grid>
<Grid.Resources>
<local:MarginConverter x:Key="MarginConverter"/>
</Grid.Resources>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="50*"/>
<ColumnDefinition Width="50*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition/>
</Grid.RowDefinitions>
<TextBlock Grid.Column="0" HorizontalAlignment="Center">Rand top/bottom</TextBlock>
<Slider Grid.Column="0"
x:Name="MySlider"
VerticalAlignment="Top"
Minimum="10"
Maximum="300"
Value="150"
Margin="10" />
<TextBlock Grid.Column="1" HorizontalAlignment="Center">Rand left/right</TextBlock>
<Slider Grid.Column="1"
x:Name="MySlider2"
VerticalAlignment="Top"
Minimum="10"
Maximum="300"
Value="150"
Margin="10" />
<Rectangle Grid.Row="1" Grid.ColumnSpan="2" x:Name="rechteck" Fill="black">
<Rectangle.Margin>
<MultiBinding Converter="{StaticResource MarginConverter}">
<Binding ElementName="MySlider2" Path="Value"/>
<Binding ElementName="MySlider" Path="Value"/>
</MultiBinding>
</Rectangle.Margin>
</Rectangle>
</Grid>
</Window>
I've put Rectangle in the bottom row of the same Grid which has Slider as otherwise it may cover your controls when margin gets close to 0
In WPF, Controls will fill the space they are given. In your case, you are giving this rectangle an explicit size of 300x300. Margin is internal padding that reduces the size of the control. when you re-size and shrink the window, the rectangle re-sizes to fit inside its new given bounds.
If you want the size of the rectangle to remain constant regardless of window re-sizing, I would recommend binding directly to the height and width properties instead of the margin. Also it currently looks like you're not even using the MySlider, and only using MySlider2.
First add a convertor somewhere
public class IntToThicknessConvertor : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
return new Thickness((double)values[0], (double)values[1], (double)values[2], (double)values[3]);
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
Thickness t = (Thickness)value;
return new object[]{t.Left,t.Top,t.Right,t.Bottom};
}
}
In you xaml make a resource pointing to your converter:
<Window.Resources>
<local:IntToThicknessConvertor x:Key="theIntToThicknessConvertor"/>
</Window.Resources>
Then in your rectangle implement multibinding:
<Rectangle x:Name="rechteck"
Width="300"
Height="300"
Fill="black"
VerticalAlignment="Center"
HorizontalAlignment="Center" >
<Rectangle.Margin>
<MultiBinding Converter="{StaticResource theIntToThicknessConvertor}">
<Binding ElementName="MySlider" Path="Value"/>
<Binding ElementName="MySlider2" Path="Value"/>
<Binding ElementName="MySlider" Path="Value"/>
<Binding ElementName="MySlider2" Path="Value"/>
</MultiBinding>
</Rectangle.Margin>
</Rectangle>
You'll prolly have to do some more but I hope this should get you on the right path
I have a window split up by grids (left center right), where the center control I want to have multiple controls that will need to fill their heights as tall as possible while splitting it evenly amongst the other controls.
One way I was able to achieve this is through a grid like so:
<!-- Center -->
<Grid Grid.Row="0" Grid.Column="2" Height="Auto">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="*" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<WebBrowser Grid.Row="0" Name="browsc" />
<WebBrowser Grid.Row="1" Name="browsa" />
<WebBrowser Grid.Row="2" Name="browsb" />
</Grid>
This works, however I need to add and remove rows dynamically, where it's possible a row in the middle will need to be removed (causing the need to reorder the rows each control rests on which I can't imagine a simple solution for).
If there isn't a better way to split the controls than this, how would I add and remove rows via code (C#)?
If there's a better way to do it, how can I do this where all I have to worry about are adding and removing the controls themselves and not mess with row properties?
Thanks!
This is an example on how to add new RowDefinition to Grid programmatically and set control to a specific Grid Row :
//following line equal to XAML : <RowDefinition Height="*" />
var newrow = new RowDefinition {Height = new GridLength(1, GridUnitType.Star)};
//add new rowdefinition to grid
myGridName.RowDefinitions.Add(newrow);
//set webbrowser control to newly added row, or any row number you wish
Grid.SetRow(browsx, myGridName.RowDefinitions.Count-1);
You can access any RowDefinition from myGridName.RowDefinitions property to delete it later. But a better idea is to set RowDefinition's Height to zero instead of delete it. With that you don't have to rearrange other controls, moving control in row 3 to row 2 for example because the previous row 2 has been deleted.
This seems like a good case for UniformGrid:
<UniformGrid Grid.Row="0" Grid.Column="2" Columns="1">
<WebBrowser Name="browsc" />
<WebBrowser Name="browsa" />
<WebBrowser Name="browsb" />
</UniformGrid>
UniformGrid ensures that all items have the same size, both width and height. In your case you only want to constrain height, but since there is only one column, all items must have the same width anyway, so this is okay.
Instead of setting up rows, we can just set Columns="1" on the UniformGrid and it will auto-arrange each item on a new row. If you add or remove items, or toggle their Visibility between Visible and Collapsed, all the sizes will be adjusted to fit the space.
Another possibility still using a grid is to bind the height to a converter that uses the visibility of the control it contains to decide on the layout.
Here's an example:
In the XAML file:
<Grid Grid.Row="0" Grid.Column="2" Height="Auto" xmlns:local="clr-namespace:WpfApplication2">
<Grid.Resources>
<local:RowHeightConverter x:Key="RowHeightConverter" />
</Grid.Resources>
<Grid.RowDefinitions>
<RowDefinition Height="{Binding ElementName=browsc, Path=IsVisible, Converter={StaticResource RowHeightConverter}}" />
<RowDefinition Height="{Binding ElementName=browsa, Path=IsVisible, Converter={StaticResource RowHeightConverter}}" />
<RowDefinition Height="{Binding ElementName=browsb, Path=IsVisible, Converter={StaticResource RowHeightConverter}}" />
</Grid.RowDefinitions>
<WebBrowser Grid.Row="0" Name="browsc"></WebBrowser>
<WebBrowser Grid.Row="1" Name="browsa" Visibility="Collapsed"></WebBrowser>
<WebBrowser Grid.Row="2" Name="browsb"></WebBrowser>
</Grid>
You can also move the 'xmlns bit up to a top-level window or you may already have it.
In the code behind:
public class RowHeightConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
var isVisible = value as bool?;
if (isVisible.HasValue && isVisible.Value)
return new GridLength(1, GridUnitType.Star);
else
return new GridLength(0);
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
}
Once you've built the code once, you can change the 'Visibility' of the browsers in the XAML designer and see the changes reflected in the layout.