Grid column not collapsing to fit contents after width resize - wpf

I have a grid column that contains a treeview which can shrink to a width of 16 when a ToggleButton is pressed. The column is set to Width="Auto" which I assume is responsible for the shrinking behavior.
The problem is that to the right of the column, there is a grid splitter, so that the user can increase/decrease the width as they see fit. If the user does this, and then presses the ToggleButton, the contents of the column shrink to 16, but the column itself does not.
Does anyone know how I can ensure that the column shrinks in these scenarios? I'm guessing it has to do with the column width being changed from auto to a definite value, but I can't think of how to fix that once the contents shrink. Here's my code so far:
<Grid Name="ContentGrid" HorizontalAlignment="Stretch" Grid.Row="1" Grid.Column="0">
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" x:Name="TreeColumn"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*" MinWidth="400" />
</Grid.ColumnDefinitions>
<UserControl DockPanel.Dock="Left" Grid.Row="0" Grid.Column="0" Padding="0" x:Name="TreeControl" >
<local:TreeViewControl x:Name="mainTreeView" Height="Auto" />
</UserControl>
<GridSplitter Grid.Row="0" Grid.Column="1" Margin="0,0,0,0"
Width="4" Background="Transparent" VerticalAlignment="Stretch" HorizontalAlignment="Stretch" />
<Grid Name="LandingPageGrid" Grid.Row="0" Grid.Column="2">
EDIT: I've tried adding a DataTrigger to the column's definition, but this hasn't worked.
<ColumnDefinition Width="Auto" x:Name="TreeColumn">
<ColumnDefinition.Style>
<Style>
<Style.Triggers>
<DataTrigger {Binding Path=IsChecked, ElementName=CollapseIcon}" Value="True">
<Setter Property="ColumnDefinition.Width" Value="Auto"/>
</DataTrigger>
</Style.Triggers>
</Style>
</ColumnDefinition.Style>
</ColumnDefinition>

So after much debugging, many fruitless attempts, I finally resorted to setting the errant widths in the code-behind to the TreeViewControl. It's not pretty, so if someone would like to suggest a better answer, please do so, and I'll happily mark your code as the solution.
private void CollapseIcon_Checked(object sender, RoutedEventArgs e)
{
TreeControl.Width = 16;
TreeControl.HorizontalAlignment = HorizontalAlignment.Left;
((FrameworkElement) TreeControl.Parent).Width = 16;
var parentColumn = (((Grid) ((FrameworkElement) TreeControl.Parent).Parent).ColumnDefinitions)[0];
parentColumn.Width = GridLength.Auto;
}
private void CollapseIcon_Unchecked(object sender, RoutedEventArgs e)
{
TreeControl.Width = Double.NaN;
TreeControl.HorizontalAlignment = HorizontalAlignment.Stretch;
((FrameworkElement) TreeControl.Parent).Width = Double.NaN;
var parentColumn = (((Grid)((FrameworkElement)TreeControl.Parent).Parent).ColumnDefinitions)[0];
parentColumn.Width = GridLength.Auto;
}

Related

WPF centered text in stretched label fixed width

My goal is simpel, however i can't figure it out. Sorry for the Titel but couldn't come up with a better explanation...
I have usercontrol with an label that display's the current time (hooked up to a timer with 1 second interval). The label is the width of its parent and the text is aligned in the center. The format is DateTime.ToString("HH : mm : ss"), the FontFamily and size can be adjusted by the user. So far nothing strange... But, the text is aligned centered so when time is lets say 12:34:02 the pixel width different than 12:34:11. (of course depending on the font) This causes the label jump (because it auto centers itself)
The code below is an example of it. the canvas is used to draw stuff on it and the viewbox is used so it autosizes itself in his parent.
Code:
<Grid>
<Viewbox>
<Canvas Name="canv" Height="300" Width="300">
<StackPanel Name="stckpnlDateTime">
<Label Name="lblDateOrText"
Grid.Column="0"
Grid.Row="0"
Content = "------"
FontSize="25"
Foreground="GhostWhite"
HorizontalContentAlignment="Center"
VerticalAlignment="Bottom"
FontFamily="Arial"
Width="Auto"/>
<Label Name="lblTime"
Grid.Column="0"
Grid.Row="1"
Content = "-- : -- : --"
FontSize="25"
Foreground="GhostWhite"
HorizontalContentAlignment="Center"
VerticalAlignment="Top"
FontFamily="DS-Digital"
Width="Auto"/>
</StackPanel>
</Canvas>
</Viewbox>
</Grid>
Private Sub SystemTime_Tick(sender As Object, e As EventArgs) Handles tmrSystemTime.Tick
lblTime.Content = Now.ToString("HH : mm : ss")
End Sub
So i tried a different approach, great a grid with 10 columns and 8 labels, one for each char, and stretch the labels to its parent (cell). This works and keeps the chars on a fixed position. But the width of last column is smaller then the rest... In this image you can see hat i mean, the second purple column is what i mean. Example alignment
Code:
<UserControl.Resources>
<Style x:Key="LabelStyle" TargetType="Label">
<Setter Property="Foreground" Value="White" />
<Setter Property="FontFamily" Value="DS-Digital" />
<Setter Property="FontSize" Value="40"/>
<Setter Property="HorizontalAlignment" Value="Center"/>
<Setter Property="HorizontalContentAlignment" Value="Right"/>
<Setter Property="Background" Value="Green" />
</Style>
</UserControl.Resources>
<Grid HorizontalAlignment="Stretch">
<Viewbox HorizontalAlignment="Stretch">
<Canvas Name="canv" Height="300" Width="300" HorizontalAlignment="Stretch">
<StackPanel Name="stckpnlDateTime" HorizontalAlignment="Stretch">
<Label Name="lblDateOrText"
Grid.Column="0"
Grid.Row="0"
Content = ""
FontSize="25"
Foreground="GhostWhite"
HorizontalContentAlignment="Center"
VerticalAlignment="Bottom"
FontFamily="Arial"
Width="Auto"/>
<Grid Name="GridTimeLabel" HorizontalAlignment="Stretch" Width="Auto" Grid.Column="0" Grid.Row="1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="1*"/>
<ColumnDefinition Width="1*"/>
<ColumnDefinition Width="1*"/>
<ColumnDefinition Width="1*"/>
<ColumnDefinition Width="1*"/>
<ColumnDefinition Width="1*"/>
<ColumnDefinition Width="1*"/>
<ColumnDefinition Width="1*"/>
<ColumnDefinition Width="1*"/>
<ColumnDefinition Width="1*"/>
</Grid.ColumnDefinitions>
<Label Background="Purple" Grid.Column="0" Grid.Row="0"/>
<Label Name="lblTime1" Grid.Column="1" Grid.Row="0" Style="{StaticResource LabelStyle}" Content="-"/>
<Label Name="lblTime2" Grid.Column="2" Grid.Row="0" Style="{StaticResource LabelStyle}" Content="-"/>
<Label Name="lblTime3" Grid.Column="3" Grid.Row="0" Style="{StaticResource LabelStyle}" Content=":"/>
<Label Name="lblTime4" Grid.Column="4" Grid.Row="0" Style="{StaticResource LabelStyle}" Content="-"/>
<Label Name="lblTime5" Grid.Column="5" Grid.Row="0" Style="{StaticResource LabelStyle}" Content="-"/>
<Label Name="lblTime6" Grid.Column="6" Grid.Row="0" Style="{StaticResource LabelStyle}" Content=":"/>
<Label Name="lblTime7" Grid.Column="7" Grid.Row="0" Style="{StaticResource LabelStyle}" Content="-"/>
<Label Name="lblTime8" Grid.Column="8" Grid.Row="0" Style="{StaticResource LabelStyle}" Content="-"/>
<Label Background="Purple" Grid.Column="9" Grid.Row="0"/>
</Grid>
</StackPanel>
</Canvas>
</Viewbox>
</Grid>
Long story short, i'm stuk.... hopefully someone could point me in the right direction.
I have two methods in mind.
First method:
Change to a monospace font, this will ensure that no matter what time value you throw at it, the width will be constant. But you may not find a good/suitable font using this method, though.
Second method:
If you are not using MVVM, try this (C# code, I'm not too used to VB.net):
// Code-behind
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
var label = this.lblTime;
label.Text = "00:00:00";
label.Measure(); // Force it to measure
label.Width = label.DesiredSize.Width;
}
This will force the width to stay constant. Depending on the font, you need to manually set the time value that takes up the most space.
Also, to be sure it works properly, you may need to wrap the Label in a Grid. Make the Grid have 3 columns, set the label in column 1 (middle column), and set the columndefinitions' widths to *, auto and * respectively in that order.
Your Canvas has a width of 300. It looks to me as there is less than 300 px available. This is why the last column is smaller.
#Jai, thanks for pointing me in the direction of the .Measure() Sub. After fiddling around i ended up with the 3 column grid, measuring the size of the label with the new content, setting the size of the label. This causes the Column to realign, which holds the label in place.
The code: (created new WPF program for the test, The colors are to see the difference between child and parent)
<Grid Background="Tomato">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label Name="lblTime"
Grid.Column="1"
Grid.Row="0"
Content = "-- : -- : --"
FontSize="50"
Foreground="Black"
Background="Beige"
HorizontalContentAlignment="Left"
VerticalAlignment="Center"
FontFamily="DS-Digital"/>
</Grid>
And the code behind it:
Class MainWindow
''' <summary>
''' Timer for updating the time Clock
''' </summary>
Dim WithEvents tmrSystemTime As New DispatcherTimer With {.Interval = TimeSpan.FromSeconds(1)} 'Set Timer interval on every second.
Sub New()
' This call is required by the designer.
InitializeComponent()
' Add any initialization after the InitializeComponent() call.
tmrSystemTime.Start()
End Sub
Private Sub SystemTime_Tick(sender As Object, e As EventArgs) Handles tmrSystemTime.Tick
'Set the time in the label
lblTime.Content = Now.ToString("HH : mm : ss")
'Measure and set the size of the label
MeasureSizeTimeLabel()
End Sub
''' <summary>
''' Measure the Max Size of the label with a specific Format
''' </summary>
Private Sub MeasureSizeTimeLabel()
'Store the Max size of the Time Label in this variable
Dim MaxClockSize As Size
'Measure the Max size of the clock label and use this width
'lblTime.Content = "00 : 00 : 00"
lblTime.Measure(New Size(Double.PositiveInfinity, Double.PositiveInfinity))
MaxClockSize = lblTime.DesiredSize
'Now Set the size of the label
lblTime.Width = MaxClockSize.Width
lblTime.Height = MaxClockSize.Height
End Sub
Thanks all for the help, appreciate the effort and time! :-)

WPF GridSplitter behavior when resizing its parent Window

I have defined my GridSplittler XAML like the following.
<Window>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
</Grid>
<Border Grid.Column="0" Background="Red" />
<GridSplitter Grid.Column="1" ResizeDirection="Columns" Width="3" Height="Auto" HorizontalAlighment="Stretch" VerticalAlignment="Stretch" />
<Border Grid.Column="0" Background="Green" />
</Window>
This will create two columns with a grid splitter between them and it will correctly resize the columns as the grid splitter is dragged left and right. Now if you resize the whole Window I would like the width of left red column to remain fixed and have the width of the right green column change (as the Window is resized). Its the same effect as when you resize the whole Visual Studio application and the Solution Explorer width remains fixed but the width of a code tab changes. Also the same as the SQL Server Management Studio; the Object Explorer width remains fixed but the SQL tabs width changes. I realize that these two examples use a more complicated docking control but I was hoping to achieve the same result with using the WPF GridSplitter.
Edit: Based on mathieu suggestions all the was necessary was to give the first column definition an initial width (shown using 200 below).
<Grid Name="GridName">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="200" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Border Grid.Column="0" Background="Red" />
<GridSplitter Name="SplitterName" Grid.Column="1" ResizeDirection="Columns" Width="3" Height="Auto" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" />
<Border Grid.Column="2" Background="Green" />
</Grid>
Now when the Window is re-sized it only changes the width of the green border and the width of the red border remains fixed. Thanks!
If you don't mind some code behind, you can achieve this behaviour by reacting to the DragDelta event of the splitter, and adjusting the width of the first column, by removing the Star width, and fixing the width according to the drag delta.
Xaml :
<Grid Name="GridName">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Border Grid.Column="0" Background="Red" />
<GridSplitter Name="SplitterName" Grid.Column="1" ResizeDirection="Columns" Width="3" Height="Auto" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" />
<Border Grid.Column="2" Background="Green" />
</Grid>
Code behind
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
SplitterName.DragDelta += SplitterNameDragDelta;
}
private void SplitterNameDragDelta(object sender, System.Windows.Controls.Primitives.DragDeltaEventArgs e)
{
GridName.ColumnDefinitions[0].Width = new GridLength(GridName.ColumnDefinitions[0].ActualWidth + e.HorizontalChange);
}
}
I've used the snipet of mathieu - and modified it for code behind use.
In my case I need a dynamic number of Items resized by the GridSplitter.
I used the Thumb Control to have full control of the action
private Thumb GetNewThumbAsGridSplitter(int column)
{
var gs = new Thumb();
gs.SetValue(Grid.ColumnProperty, column);
// gs.SetValue(Grid.RowSpanProperty, 2);
// gs.SetValue(Grid.RowProperty, 1);
gs.Width = 5;
gs.MouseEnter += (o, i) =>
{
Mouse.OverrideCursor = Cursors.ScrollWE;
};
gs.MouseLeave += (o, i) =>
{
Mouse.OverrideCursor = Cursors.Arrow;
};
gs.DragDelta += (o, i) =>
{
var grid = (Grid)gs.Parent;
var previous = (YourControl)((grid.Children[column - 1]));
var next = (YourControl)(grid.Children[column + 1]);
if (next.MinWidth >= (next.ActualWidth - i.HorizontalChange))
{
return;
}
if (previous.MinWidth >= (previous.ActualWidth + i.HorizontalChange))
{
return;
}
previous.Width = previous.ActualWidth + i.HorizontalChange;
next.Width = next.ActualWidth - i.HorizontalChange;
};
return gs;
}

How to have a click-able button in my combo-box ItemTemplate?

This is what i have so far:
<dxe:ComboBoxEdit Name="cboUserCustomReports"
Width="300" Height="Auto"
Margin="0,5,0,5"
ItemsSource="{Binding Path=UserReportProfileList,Mode=OneWay,UpdateSourceTrigger=PropertyChanged}"
EditValue="{Binding Path=UserReportProfileID,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}"
ValueMember="UserReportProfileID"
DisplayMember="ReportName"
PopupClosed="cboUserCustomReports_PopupClosed">
<dxe:ComboBoxEdit.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="100*"/>
<ColumnDefinition Width="20"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0"
Text="{Binding ReportName, Mode=Default}"
VerticalAlignment="Stretch" HorizontalAlignment="Left"/>
<Button Name="btnDelete"
Grid.Column="1"
Width="20" Height="20"
VerticalAlignment="Center" HorizontalAlignment="Right"
Click="btnDelete_Click">
<Button.Template>
<ControlTemplate>
<Image Source="/RMSCommon;component/Resources/Delete.ico"></Image>
</ControlTemplate>
</Button.Template>
</Button>
</Grid>
</DataTemplate>
</dxe:ComboBoxEdit.ItemTemplate>
</dxe:ComboBoxEdit>
First, i want the two columns to be stand alone. The user must be able to select or delete the item.
Second, i would like to make my button in the ItemTemplate to be click-able.
What do i need to add to get this behavior?
This is what it looks like at the moment:
Click
I assume, that your button is clickable, and you want to know how to process the click event. Right?
For the click-handler, add the following code:
private void btnDelete_Click(object sender, RoutedEventArgs e) {
FrameworkElement fe = sender as FrameworkElement;
if(null == fe){
return;
}
UserReportProfile userReportProfile = fe.DataContext as UserReportProfile;
if (null == userReportProfile) {
return;
}
// Do here your deletion-operation
}
I assumed that your item-class is named UserReportProfile. Otherwise, change the declared type accordingly.
Layout
For the alignment, add the following declaration to your ComboBox:
HorizontalContentAlignment="Stretch"
This gives your DataTemplate-Grid the full width and you can layout then your items as you desire.
<dxe:ComboBoxEdit Name="cboUserCustomReports"
HorizontalContentAlignment="Stretch"
Width="300" Height="Auto"
Margin="0,5,0,5"
...>
Your question is not clear enough. But I guess you want to vertically align the text and images in your combobox. If so, then all you need to do this:
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
And I think your items are already clickable!

Setting ControlTemplate in code [WPF]

How can I set Grid object with some UI elements in ControlTemplate? I try use this code, but I don't understand how to set Grid object to FrameworkElementFactory object. Thanks
ControlTemplate CellControlTemplate = new ControlTemplate(); // my control template
Grid CellGrid = new Grid(); // my grid (I want add it to my control template
FrameworkElementFactory root = new FrameworkElementFactory(typeof(Grid));
// ???
CellControlTemplate.VisualTree = root;
I need it for replacing my xaml-designed style in code behind:
<Style TargetType="{x:Type igDP:CellValuePresenter}" x:Key="PStyle">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type igDP:CellValuePresenter}">
<Grid Margin="4">
<Grid.RowDefinitions>
<RowDefinition />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Row="0" Grid.RowSpan="2" Grid.Column="0"
FontWeight="DemiBold"
FontSize="18"
VerticalAlignment="Center"
Text="{Binding Path=DataItem.DINAMIC_COLUMN}"/>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
I must do it, because I want change binding path for textbox in runtime from code. Do you know others ways? Thanks.
Here is solution:
var grid = new FrameworkElementFactory(typeof(Grid));
// assign template to grid
CellControlTemplate.VisualTree = grid;
// define grid's rows
var r = new FrameworkElementFactory(typeof(RowDefinition));
grid.AppendChild(r);
// define grid's columns
var c = new FrameworkElementFactory(typeof(ColumnDefinition));
grid.AppendChild(c);
//... etc
Thanks all.

GridSplitter with min constraints

I want a Grid layout with two rows and splitter between them. Rows should have a minimum height of 80 pixels.
This code works great:
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*" MinHeight="80" />
<RowDefinition Height="5" />
<RowDefinition Height="*" MinHeight="80" />
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" Text="{Binding Path=ActualHeight, RelativeSource={RelativeSource Self}}" />
<GridSplitter Grid.Row="1" VerticalAlignment="Stretch" HorizontalAlignment="Stretch" Background="Red" />
<TextBlock Grid.Row="2" Text="{Binding Path=ActualHeight, RelativeSource={RelativeSource Self}}" />
</Grid>
But I want top row to have an Auto height until user manually change it using the splitter. So I changed the code to this:
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" MinHeight="80" />
<RowDefinition Height="5" />
<RowDefinition Height="*" MinHeight="80" />
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" Text="{Binding Path=ActualHeight, RelativeSource={RelativeSource Self}}" />
<GridSplitter Grid.Row="1" VerticalAlignment="Stretch" HorizontalAlignment="Stretch" Background="Red" />
<TextBlock Grid.Row="2" Text="{Binding Path=ActualHeight, RelativeSource={RelativeSource Self}}" />
</Grid>
And there is a problem. Splitter still satisfies row constraints, but it begins to increase top row's height infinitely if I drag splitter too low. This results in bottom row to be completely below window's bottom border.
I have done some Reflector on GridSplitter code and see that it uses different logic if rows has Auto or Star height.
Any suggestions how can I "fix" it?
I've run into this problem a few times myself. It appears as though the GridSplitter doesn't play well with Auto. That said, I have found a potential workaround.
You are able to specify the value of a GridLength object using "star coefficients". This acts as a multiplier for the length in question.
In your example, if you take the row you want to remain as star, and set the the star coefficient to a really large number, the row will take up all available space (forcing the other row to become its min-height). While this isn't the same behavior as "auto" (the height of the first row is not determined by its contents height), it might get you closer.
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*" MinHeight="80" />
<RowDefinition Height="5" />
<RowDefinition Height="10000*" MinHeight="80" />
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" Text="{Binding Path=ActualHeight, RelativeSource={RelativeSource Self}}" />
<GridSplitter Grid.Row="1" VerticalAlignment="Stretch" HorizontalAlignment="Stretch" Background="Red" />
<TextBlock Grid.Row="2" Text="{Binding Path=ActualHeight, RelativeSource={RelativeSource Self}}" />
</Grid>
I have developed a workaround for this problem. Point is to set MaxHeight for the top row while we are dragging splitter. Here the code:
public class FixedGridSplitter : GridSplitter
{
private Grid grid;
private RowDefinition definition1;
private double savedMaxLength;
#region static
static FixedGridSplitter()
{
new GridSplitter();
EventManager.RegisterClassHandler(typeof(FixedGridSplitter), Thumb.DragCompletedEvent, new DragCompletedEventHandler(FixedGridSplitter.OnDragCompleted));
EventManager.RegisterClassHandler(typeof(FixedGridSplitter), Thumb.DragStartedEvent, new DragStartedEventHandler(FixedGridSplitter.OnDragStarted));
}
private static void OnDragStarted(object sender, DragStartedEventArgs e)
{
FixedGridSplitter splitter = (FixedGridSplitter)sender;
splitter.OnDragStarted(e);
}
private static void OnDragCompleted(object sender, DragCompletedEventArgs e)
{
FixedGridSplitter splitter = (FixedGridSplitter)sender;
splitter.OnDragCompleted(e);
}
#endregion
private void OnDragStarted(DragStartedEventArgs sender)
{
grid = Parent as Grid;
if (grid == null)
return;
int splitterIndex = (int)GetValue(Grid.RowProperty);
definition1 = grid.RowDefinitions[splitterIndex - 1];
RowDefinition definition2 = grid.RowDefinitions[splitterIndex + 1];
savedMaxLength = definition1.MaxHeight;
double maxHeight = definition1.ActualHeight + definition2.ActualHeight - definition2.MinHeight;
definition1.MaxHeight = maxHeight;
}
private void OnDragCompleted(DragCompletedEventArgs sender)
{
definition1.MaxHeight = savedMaxLength;
grid = null;
definition1 = null;
}
}
Then just replace GridSplitter with FixedGridSplitter.
Note: this code is not general - it doesn't support columns and assume that bottom row has MinHeight specified.

Resources