Creating complex calculations for Grid column width in WPF - wpf

Is it possible to create complex calculations for determining the widths of columns (or any control) in WPF? So, for instance, I want Grid.Column3.Width to equal Grid.Width - Grid.Column2.Width - 100.
Specifically, this is what I am working on:
I have a grid with three columns. The first column has a cancel button who may be collapsed. The second has a text box. And the third has a Progress Bar (that may be collapsed).
I want the button always have all the width it needs (if it is visible). I would like try to show all of the text box. And give all of the rest of the width to the progress bar (if it is visible). But I don't want the progress bar to be less than 200. The text box should be truncated if necessary to give the progress bar a minimum of 200.
So, I want something like this:
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*" MinWidth="200"/>
</Grid.ColumnDefinitions>
But that doesn't work (The progress bar will be pushed off of the side of the grid if there is not enough room).
What is the best way to represent this? Can I do it entirely in XAML, or am I going to need to use code-behind?

I can't come up with a way to do this strictly in Xaml.
You could use a MultiBinding for the MaxWidth of the TextBox where you use the following as Bindings
ActualWidth of Grid
ActualWidth of Button
MinWidth of ProgressBar
Xaml
<Grid Name="grid">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Button Name="button" Grid.Column="0" Content="Cancel"/>
<TextBox Grid.Column="1" Text="Some Text">
<TextBox.MaxWidth>
<MultiBinding Converter="{StaticResource MaxWidthMultiConverter}">
<Binding ElementName="grid" Path="ActualWidth"/>
<Binding ElementName="button" Path="ActualWidth"/>
<Binding ElementName="progressBar" Path="MinWidth"/>
</MultiBinding>
</TextBox.MaxWidth>
</TextBox>
<ProgressBar Name="progressBar" Grid.Column="2" Value="50" Maximum="100" MinWidth="200"/>
</Grid>
MaxWidthMultiConverter
public class MaxWidthMultiConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
double gridWidth = (double)values[0];
double buttonWidth = (double)values[1];
double progressBarMaxWidth = (double)values[2];
return gridWidth - (buttonWidth + progressBarMaxWidth);
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
}

Related

Texttrimming in Auto grid column

I want something very simple in WPF, but I don't get it to work:
I have a grid with 2 columns: one * and one Auto. The second column contains a TextBlock. I need texttrimming to work with this TextBlock. This doesn't work currently, because the TextBlock goes outside the bounds of the grid.
Extra info:
The second column should be juste wide enough to contain the TextBlock. The first column should contain all remaining space. If the Grid isn't wide enough to contain the desired width of the TextBlock, the text should be trimmed.
The width of the Grid changes when resizing the window.
Nothing is static (not the text, no sizes), so hardcoded values can not be used.
ClipToBounds property doesn't fix this issue.
I can't bind MaxWidth of the TextBlock to the width of the column, otherwise the TextBlock will only getting smaller, but never bigger when resizing the window.
Code to reproduce the issue (for example in Kaxaml):
<Page xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<DockPanel>
<Grid Height="20" Background="Blue" DockPanel.Dock="Top" Margin="100 0 100 0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto" MaxWidth="200"/>
</Grid.ColumnDefinitions>
<TextBlock
Grid.Column="1"
Background="Red"
Text="Test tralalalalalalalalalala long string this should be trimmed!"
TextTrimming="CharacterEllipsis"/>
</Grid>
</DockPanel>
</Page>
Any suggestions?
Second solution:
Use a Converter like this:
namespace StackStuff{
class WidthConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if(value is Double)
{
return (double)value - 200; // 200 = 100+100 form the grid margin
}
return value;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}
In the View, you will have:
xmlns:local="clr-namespace:StackStuff"
Then, you have to add the converter for it to be used:
<Window.Resources>
<local:WidthConverter x:Key="WidthConverter"/>
</Window.Resources>
And then you have to implement the converter:
<DockPanel Background="Green" x:Name="dock">
<Grid Height="20" Background="Blue" DockPanel.Dock="Top" Margin="100 0 100 0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBlock MaxWidth="{Binding ActualWidth, Converter={StaticResource WidthConverter}, ElementName=dock}"
Grid.Column="1"
Hope this is what you wanted.

Setting margin to controls outside Grid with reference to control inside Grid

Working on wpf solution
i have created a Grid with different columns, a control is added in column 2.
Now i have a control outside grid and the Left margin should be same as the control inside the grid.
Can this be done?
We can of course take the very simple way of changing Margin using code.
For pure MVVM approach, you can do that using AttachedProperty.
Binding with a Convertor won't work here as Thickness type is not a DependencyObject.
If we simply bind the Margin of outer Button to inner Button, Entire Margin of outer Button will change, which we dont want. So, we need to preserve whole Margin except Left Margin. Left Margin can be changed using Binding. But how ? Our outer Button needs to have two Margin values, one original, and another coming from inner Button, so that original one can be changed. For this another margin, we can take help of Attached Property, as they allow us to extend a control.
AttachedProperty
public static BindingExpression GetLefMargin(DependencyObject obj)
{
return (BindingExpression)obj.GetValue(LefMarginProperty);
}
public static void SetLefMargin(DependencyObject obj, BindingExpression value)
{
obj.SetValue(LefMarginProperty, value);
}
// Using a DependencyProperty as the backing store for LefMargin. This enables animation, styling, binding, etc...
public static readonly DependencyProperty LefMarginProperty =
DependencyProperty.RegisterAttached("LefMargin", typeof(BindingExpression), typeof(Window1), new PropertyMetadata(null, new PropertyChangedCallback(MarginCallback)));
private static void MarginCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
FrameworkElement elem = d as FrameworkElement;
BindingExpression exp = e.NewValue as BindingExpression;
// Create a new Binding to set ConverterParameter //
Binding b = new Binding();
b.Converter = exp.ParentBinding.Converter;
b.ConverterParameter = elem.Margin;
b.Path = exp.ParentBinding.Path;
b.ElementName = exp.ParentBinding.ElementName;
b.Mode = exp.ParentBinding.Mode;
elem.SetBinding(FrameworkElement.MarginProperty, b);
}
Converter
public class LeftMarginCnv : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
double gridCtrlLeftMargin = ((Thickness)value).Left;
Thickness tgtCtrlMargin = (Thickness)parameter;
return new Thickness(gridCtrlLeftMargin, tgtCtrlMargin.Top, tgtCtrlMargin.Right, tgtCtrlMargin.Bottom);
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
}
Usage
<Grid>
<Grid Background="Red" Margin="29,55,52,125" ShowGridLines="True">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="90*"/>
<ColumnDefinition Width="121*"/>
</Grid.ColumnDefinitions>
<Button x:Name="Btn" Content="Press" Margin="18,22,35,28" Grid.Column="1" Click="Btn_Click"/>
</Grid>
<Button local:Window1.LefMargin="{Binding Margin, ElementName=Btn, Converter={StaticResource LeftMarginCnvKey}}" Content="Button" HorizontalAlignment="Left" Margin="55,199,0,0" VerticalAlignment="Top" Width="75"/>
</Grid>
Outer Button will change its Left Margin if you change the Left margin of inner Button.
You can set the common style for both. Something like this.
<StackPanel>
<StackPanel.Resources>
<Style x:Key="commonstyle" TargetType="{x:Type FrameworkElement}">
<Setter Property="Margin" Value="10,0,0,0" />
</Style>
</StackPanel.Resources>
<TextBox x:Name="outside" Width="100" Height="70" Style="{StaticResource commonstyle}"/>
<Grid ShowGridLines="True">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Button Width="100" Height="70" x:Name="inside" Grid.Column="2" HorizontalAlignment="Left" Style="{StaticResource commonstyle}"/>
</Grid>
</StackPanel>
(or)
Make it simple
<StackPanel>
<TextBox x:Name="outside" Width="100" Height="70" Margin="{Binding ElementName=inside, Path=Margin}"/>
<Grid ShowGridLines="True">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Button Width="100" Height="70" x:Name="inside" Grid.Column="2" HorizontalAlignment="Left" Margin="20"/>
</Grid>
</StackPanel>
Hope that helps.

How can I have only a * notation wpf grid column resize when the containing control is resized and not clip other columns?

I have a grid with certain column settings:
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width=".25*" MinWidth="200"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="240" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="100*" />
</Grid.RowDefinitions>
....Omitting content to keep this simple....
</grid>
When I resize the width of the containing control, the center column resizes as expected, to a point. Then it will start to clip the third column for no apparent reason (there's still room for the center column to shrink). How can I force WPF to only resize the center column, and only clip the third column if the center column's width is at 0?
EDIT:
Try using a MultiValueConverter to minimize the MaxWidth of the second column to the available space in the grid. The code below should do the trick:
Xaml:
<Grid.ColumnDefinitions>
<ColumnDefinition Name="col1" Width=".25*" MinWidth="200"/>
<ColumnDefinition Name="col2" Width="*">
<ColumnDefinition.MaxWidth>
<MultiBinding Converter="{StaticResource GridWidthConverter}">
<Binding ElementName="col1" Path="MinWidth"/>
<Binding ElementName="col3" Path="Width"/>
<Binding ElementName="Control" Path="ActualWidth"/>
</MultiBinding>
</ColumnDefinition.MaxWidth>
</ColumnDefinition>
<ColumnDefinition Name="col3" Width="240" />
</Grid.ColumnDefinitions>
Converter:
public class GridWidthConverter : IMultiValueConverter
{
#region IMultiValueConverter Members
public object Convert(object[] values, System.Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
var col1 = System.Convert.ToDouble(values[0]);
var col2 = System.Convert.ToDouble(((GridLength)values[1]).Value);
var control = System.Convert.ToDouble(values[2]);
var maxWidth = control - (col1 + col2);
return maxWidth > 0 ? maxWidth : 0;
}
public object[] ConvertBack(object value, System.Type[] targetTypes, object parameter, System.Globalization.CultureInfo culture)
{
return null;
}
#endregion
}
You may want to add some error checking to the converter but it should give you the idea.
I ended up figuring out the problem. Richard E led me in the right direction. Apparently the act of setting a * notation on a column and a minimum width caused the behavior I was experiencing. Specifically, when column 1 stopped shrinking due to hitting the minimum width, column 2 only continued to shrink at a rate that it would have if column 1 continued to shrink as well. This version of the xaml works correctly:
<Grid.ColumnDefinitions>
<ColumnDefinition Width="200"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="240" />
</Grid.ColumnDefinitions>
I don't know if the behavior I encountered was intentional or not.
Thanks for the help!

Cannot bind Margin property?

I am trying to have a sort of animated film strip where only one of its fields is visible at the given moment. Like a film strip in a camera - only one film field is currently placed under the lens and prepared to be enlighted. I try to achieve it as a grid withing an another grid. "Film strip" is called pageContainer in the code below. In the code behind I do an animation which is changing the Margin property of pageContainer grid. Works perfectly, pageContainer is nicely sliding to the left or to the right as I wish. For example: To show the page no. 2 the margin is set to (-270,0,0,0) instead of (0,0,0,0). That moves pageContainer to the left and only the second field is visible instead of the first one.
However as soon as I start resizing the whole UI, this mechanism stops to work and user can see a border between the two pages (fields) instead of only the current one. If I put breakpoint to my pageWidthConverter it is always hit when resizing the whole UI. If I put another breakpoint to pageMarginConverter it is never hit during the resizing. Why? I would like to make the Margin property dependent on BackgroundRectangle ActualWidth similarly as I made it with the Width property. But that does not work. Why is Width dynamically set while resizing the window, but Margin is not???
PS: Instead of using Margin one could use TranslateTransform. I tried but no success either.
If anybody can help it would be so highly appreciated.
Cheers
Hans
... <Grid Grid.Row="3" Grid.Column="1" Margin="0,-4,0,0" ClipToBounds="True">
<Grid x:Name="pageContainer"
Width="{Binding Converter={StaticResource pageWidthConvertor}, ElementName=BackgroundRectangle, Path=ActualWidth}"
Margin="{Binding Converter={StaticResource pageMarginConvertor}, ElementName=BackgroundRectangle, Path=ActualWidth}" >
<Grid.ColumnDefinitions>
<ColumnDefinition Width="{Binding ElementName=BackgroundRectangle, Path=ActualWidth}"/>
<ColumnDefinition Width="40"/>
<ColumnDefinition Width="{Binding ElementName=BackgroundRectangle, Path=ActualWidth}"/>
<ColumnDefinition Width="40"/>
<ColumnDefinition Width="{Binding ElementName=BackgroundRectangle, Path=ActualWidth}"/>
<ColumnDefinition Width="40"/>
<ColumnDefinition Width="{Binding ElementName=BackgroundRectangle, Path=ActualWidth}"/>
<ColumnDefinition Width="40"/>
</Grid.ColumnDefinitions> ...
It sounds like you need your margin to be dependant on two things (1) The offset you desire and (2) some proportion of your ActualWidth, in order for your UI to scale nicely. Have you tried using a MultiBinding? Take a look at the following article:
http://www.codeproject.com/KB/WPF/WpfWinFormsBulletGraphs.aspx#layout
This uses a multi-binding to scale some value based on the size of the control. Here is the multi-binding:
class ScalingMultiConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType,
object parameter, CultureInfo culture)
{
if (!ValuesPopulated(values))
return 0.0;
double containerWidth = (double)values[2];
double valueToScale = (double)values[1];
double maximum = (double)values[0] ;
return valueToScale * containerWidth / maximum;
}
private bool ValuesPopulated(object[] values)
{
foreach (object value in values)
{
if (value==null || value.Equals(DependencyProperty.UnsetValue))
return false;
}
return true;
}
public object[] ConvertBack(object value, Type[] targetTypes,
object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
And an example of its usage:
<Rectangle.Width>
<MultiBinding Converter="{StaticResource ScalingMultiConverter}">
<Binding Path="(c:BulletGraphWithLegend.GraphRange)"/>
<Binding Path="(c:BulletGraphWithLegend.FeaturedMeasure)"/>
<Binding Path="ActualWidth"/>
</MultiBinding>
</Rectangle.Width>
You should be able to create a multi-binding that takes your desired offset and the ActualWidth and converts it into a Margin.
On another note, why not wrap your Grid in a Canvas and position it via the Canvas.Left property? This way you will not have to construct a Thickness in your converter.

WPF unwanted grid splitter behaviour

I have a simple grid with 3 columns (one of which contains a grid splitter). When resizing the grid and the left column reaches its minimum width, instead of doing nothing it increases the width of the right column. Could anyone help me stop this?
I can't set the max width of the right column, because the grid itself also resizes.
Here's some sample code that shows the problem. While resizing, move the mouse over the red area:
XAML:
<Grid DockPanel.Dock="Top" Height="200">
<Grid.ColumnDefinitions>
<ColumnDefinition MinWidth="200" Width="*" />
<ColumnDefinition Width="3" />
<ColumnDefinition MinWidth="120" Width="240" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Rectangle Fill="Red" Grid.Row="0" Grid.Column="0" />
<DockPanel LastChildFill="True" Grid.Row="0" Grid.Column="2" >
<Rectangle DockPanel.Dock="Right" Width="20" Fill="Blue" />
<Rectangle Fill="Green" />
</DockPanel>
<GridSplitter Background="LightGray" Grid.Row="0" Grid.Column="1" Height="Auto" Width="Auto" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" />
</Grid>
I know this is a bit late, but I just run into this problem, and here is my solution. Unfortunately it is not general enough, it only works for a grid with two columns, but it can probably be adapted farther. However, it solves the described problem and my own, so here goes:
The solution consists in a hack or workaround, however you want to call it. Instead of declaring MinWidth for both the left and right column, you declare a MinWidth and a MaxWidth for the first column. This means that the GridSplitter won't move right of a defined location. So far, so good.
The next problem is that if we have a resizable container (the window in my case), this is not enough. It means that we cannot enlarge the left column as much as we want, even though there might be plenty of space for the second one. Fortunately, there is a solution: binding on the Grid ActualWidth and using an addition converter. The converter parameter will actually be the desired MinWidth for the right column, obviously the negative value, since we need to subtract it from the Grid Width. You could also use a SubtractConvertor, but that is up to you.
Here goes the xaml and code:
<Grid Background="{DynamicResource MainBackground}" x:Name="MainGrid" >
<Grid.ColumnDefinitions>
<ColumnDefinition Width="200" MinWidth="100" MaxWidth="{Binding Path=ActualWidth, RelativeSource={RelativeSource AncestorType=Grid}, Converter={Converters:AdditionConverter}, ConverterParameter=-250}" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<GridSplitter Width="3" VerticalAlignment="Stretch" Grid.Column="0"/>
<!-- your content goes here -->
</Grid>
and the converter:
[ValueConversion(typeof(double), typeof(double))]
public class AdditionConverter : MarkupExtension, IValueConverter
{
#region IValueConverter Members
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
double dParameter;
if (targetType != typeof(double) ||
!double.TryParse((string)parameter, NumberStyles.Any, CultureInfo.InvariantCulture, out dParameter))
{
throw new InvalidOperationException("Value and parameter passed must be of type double");
}
var dValue = (double)value;
return dValue + dParameter;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotSupportedException();
}
#endregion
#region Overrides of MarkupExtension
/// <summary>
/// When implemented in a derived class, returns an object that is set as the value of the target property for this markup extension.
/// </summary>
/// <returns>
/// The object value to set on the property where the extension is applied.
/// </returns>
/// <param name="serviceProvider">Object that can provide services for the markup extension.
/// </param>
public override object ProvideValue(IServiceProvider serviceProvider)
{
return this;
}
#endregion
}
I hope this helps,
Mihai Drebot
This approach is a hack but can be used for the desired effect. Try putting an event handler on the Window.SizeChanged event to set the maxWidth of the first columndef. For example:
private void Window_SizeChanged(object sender, SizeChangedEventArgs e)
{
int borderBuffer = 9;
// AppWindow Size Splitter Width Far Column Min FudgeFactor
Col0.MaxWidth = e.NewSize.Width - Col1.Width.Value - Col2.MinWidth - borderBuffer;
}
I needed to use this method to prevent a third party control in the top row of the grid from having some undesirable wrapping resulting from when the gridSplitter disregards the min/max width of the column with width set to "Auto". The borderBuffer may need to be adjusted for your case. The borderBuffer in my case didn't make perfect sense based on the geometry/column/border widths - it was just the magic number that worked for my layout.
If someone can come up with a cleaner solution, I'd love to use it instead. This solution reminds me of painfully trying to force VB6 to resize controls - yuck. But for now, it beats having items on the top row of my grid from getting hidden due to unexpected wrapping.
I got this undesirable behaviour to stop by changing the * in the columndefinition to Auto and the 240 to *.

Resources