Why are ActualWidth and ActualHeight 0.0 in this case? - wpf

I have a Grid inside a Canvas defined like this:
<Canvas x:Name="outerCanvas">
<Grid Grid.Row="1" Name="cGrid" ShowGridLines="True" Width="{Binding Path=ActualWidth, RelativeSource={RelativeSource AncestorType={x:Type Canvas}}}" Height="{Binding Path=ActualHeight, RelativeSource={RelativeSource AncestorType={x:Type Canvas}}}">
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<Rectangle Name="rectangle1" Stroke="Black" Fill="AntiqueWhite" />
<Rectangle Grid.Row="0" Grid.Column="1" Grid.ColumnSpan="1" Grid.RowSpan="1" Name="rectangle2" Stroke="Black" Fill="AliceBlue" />
<Rectangle Grid.Row="0" Grid.Column="2" Grid.ColumnSpan="1" Grid.RowSpan="1" Name="rectangle3" Stroke="Black" Fill="Aqua" />
<Rectangle Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="3" Grid.RowSpan="1" Name="rectangle4" Stroke="Black" Fill="DarkViolet" />
</Grid>
</Canvas>
My problem is that, on the Window constructor, after InitializeComponents() either Grid.ColumnDefinitions[0].ActualWidth or "any rectangle".ActualWidth are all set to 0.0 (the same for heights). I'm not figuring out what to do to get this information. Any help?
Observations:
I'm not defining the outer canvas width and height but if I do, it doesn't solve my problem.
At runtime I can see that this Canvas/Grid occupies the entire window space, so every rectangle inside it has ActualWidths and ActualHeights
The grid's width/height is bound to the canvas but I tried removing this binding and my problem still persists.

ActualHeight and ActualWidth are not set until the control is measured and arranged. Usually there is nothing in InitializeComponent() that causes a measure, so when it returns these will still be zero.
You can force these to be computed earlier by simply calling the window's Measure() and Arrange() methods manually after the window's InitializeComponent() returns.
If you are sizing to content:
window.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
window.Arrange(new Rect(0, 0, window.DesiredSize.Width, window.DesiredSize.Height));
If you are using an explicit window size:
window.Measure(new Size(Width, Height));
window.Arrange(new Rect(0, 0, window.DesiredSize.Width, window.DesiredSize.Height));

Ray is correct (+1) that this is due to the fact that the measure and arrange pass has not executed yet. However, rather than force another layout pass (expensive), you can just wait until your control has loaded before accessing the ActualXxx properties:
public MyWindow()
{
Loaded += delegate
{
// access ActualWidth and ActualHeight here
};
}

In our case the solution was simple, as everybody said ActualWidth and ActualHeight needed to get called after the Loaded even completes,
So we just wrapped the code in a dispatcher and set the priority to Loaded as below:
Dispatcher.Invoke(new Action(() =>
{
graphHeight = ActualHeight;
graphWidth = ActualWidth;
}), DispatcherPriority.Loaded);

Related

How to bind Font Size to variable Grid Size

I have a grid with 2 columns and 2 rows. A single character (Unicode U+2699) was placed inside the bottom right grid field. It looks like this:
I´d like the character to automatically adjust its font size to fit the grid field it has been placed in (in this case it should be bound to the height of the second grid row, but since in some cases it could be unclear if the height or the width of the grid is smaller, it would be also nice to know how to bind to the lowest of those 2 values).
My implementation so far is something like the following (I simplified it a bit for this example):
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition x:Name="heightToBind" Height="40"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="14*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Button FontSize="{Binding ElementName=heightToBind, Path=Height.Value, Mode=OneWay}" Content="⚙" Grid.Row="1" Grid.Column="1" HorizontalAlignment="Right" />
</Grid>
The problem here is, that it only works if the height is a fixed value inside the RowDefinitions. I want it to work with the following definition:
<Grid.RowDefinitions>
<RowDefinition Height="4*"/>
<RowDefinition x:Name="heightToBind" Height="*"/>
</Grid.RowDefinitions>
As a bonus question I´d also be interested why it might be that the character is placed too low so it is getting cut off at the bottom (I tried VerticalAlignment="Center" for the button but with no effect).
You can try using a ViewBox as the button's content:
<Button Grid.Row="1" Grid.Column="1">
<Button.Content>
<Viewbox StretchDirection="Both" HorizontalAlignment="Stretch">
<TextBlock Text="⚙" />
</Viewbox>
</Button.Content>
</Button>
A ViewBox can stretch and scale his child to fill all the available space...
You could try binding to the ActualHeight instead of the Height:
<Button FontSize="{Binding ElementName=heightToBind, Path=ActualHeight.Value, Mode=OneWay}"
Content="⚙" Grid.Row="1" Grid.Column="1" HorizontalAlignment="Right" />
This should work.
The * on the grid definition means take the available space as the height so it's only determined when the page layout has been prepared for layout. If the height is either unset or changed then the real height is returned in the ActualHeight property.

Expander, Grid & ListBox = No Virtualization

I have a ListBox, inside a Grid, inside an Expander. The ListBox is bound to an IList.
When I expand the Expander control for the first time, the ListBox processes all of the items in the IList (which can be thousands) instead of only processing the items that would be visible on the screen.
If however I fix the height of the ListBox control, it behaves as expected and only accesses those items in the IList that will be visible.
Effectively, the Virtualization is not working, though I believe that this is more related to the ListBox not being able to determine a height when the content items are being prepared.
The XAML is basically as follows (some stuff removed for simplification)...
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Expander ExpandDirection="Right"
Grid.Column="0"
Grid.Row="0"
Grid.RowSpan="2"
Header="Documents"
IsExpanded="False">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid Grid.Row="0">
<Grid.RowDefinitions>
<RowDefinition Height="50" />
</Grid.RowDefinitions>
</Grid>
<ListBox Name="listBox"
Grid.Row="1"
ItemsSource="{Binding Path=Items}"
SelectedIndex="{Binding Path=SelectedIndex}"
SelectedItem="{Binding Path=SelectedItem}"
SelectionMode="Single"
Width="250">
<ListBox.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="auto" />
</Grid.RowDefinitions>
<TextBlock Grid.Column="0"
Style="{StaticResource prompt}">
<TextBlock.Text>
<MultiBinding StringFormat="{}{0}{1:00000}">
<Binding Path="..."
FallbackValue="0" />
<Binding Path="..." />
</MultiBinding>
</TextBlock.Text></TextBlock>
<TextBlock Grid.Column="1"
Style="{StaticResource prompt}">
<TextBlock.Text>
<Binding Path="ItemCount"
StringFormat="{}{0} Items"
FallbackValue="" />
</TextBlock.Text></TextBlock>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
</Expander>
<v:DocumentView x:Name="documentView"
Grid.Column="1"
Grid.Row="0"
DocumentID="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type v:BatchView}}, Path=ViewModel.SelectedItem.ID}"
IsActive="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type v:BatchView}}, Path=IsActive}" />
<StackPanel Grid.Column="1"
Grid.Row="1"
Style="{StaticResource buttonStackStyle}">
<Button Command="{Binding Path=PreviousCommand}"
Style="{StaticResource previousButtonStyle}" />
<Button Command="{Binding Path=NextCommand}"
Style="{StaticResource nextButtonStyle}" />
</StackPanel>
</Grid>
Can anybody suggest how I might set the Height of the ListBox to the ActualHeight of the Grid.Row parent? Alternatively, can anybody provide a better solution?
Thanks.
Short version: remove Grid.RowSpan from the expander.
Long version:
Background: (in really broad strokes)
When you define the height of RowDefinition three things can happen, depending what type of units you end up using:
Pixel - any UIElement that is placed in that row will have the defined row height passed to the Measure and Arrange methods of the element.
Auto - the grid will pass infinity as the height for Measure and then element.DesiredSize.Height for Arrange.
Star - the grid will consider the heights of all rows with units of pixels and auto; calculate the height that is left from its available height and divide it by the "total of stars" that were defined for all the rows - this is the height of one star; then, each row height is assigned depending on the multiplier for its star definition; this height is passed to the Measure and Arrange methods.
Same logic is applied to column definitions only regarding width instead of height.
So, star definition is "stopping" the element, pixel definition is also "stopping" but it can be outside of the rendered view and auto definition is "letting" the element to be what ever size it wants to be.
All this logic is recursive so you need to think in two directions (explanation below).
In your case
In one direction. The ListBox is in a star row so it'll be stopped. The parent grid is also stopped (since the template for expander uses DockPanel that is also a "stopping panel"). The expander is defined to begin in a star row but it spans to an auto row - this means that it will be allowed to grow in height 'till infinity. Oops...time to reverse.
Now the reverse direction. The expander is not stopped, the child grid is not stopped (since the grid assumes it has infinite height available), thus the list box is not stopped, the ScrollViewer in the template of list box is not stopped so it's ViewportHeight is infinite, for the VirtualizingStackPanel that arranges the items (and is a child of the scroll viewer) this means all items are in the view == render all elements.
For a WPF window with default template, you can always assume the window is stopping its child element. So if removing the row span definition has not resolved the issue, continue traversing up until you find another element that is not stopping its child height and change its definitions or change the panel to stop the height from growing to infinity (scroll viewers are notorious for creating these behaviours, especially the ones that are hidden in templates).

Wpf Resize object with window

Looked around to find a way to resize bind with the windows resize without explicitly telling my object to grab the windows size.
Here is the code:
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<WindowsFormsHost Background="{x:Null}" Grid.Row="0" Grid.Column="0" HorizontalAlignment="Left" Name="windowsFormsHost1" VerticalAlignment="Top" Margin="-1,0,0,0">
<wf:Panel x:Name="pnlLivePreview" />
</WindowsFormsHost>
</Grid>
This was followed by the example showed here
Edit: Question: Why doesn't panel resize with the window ?
Simply remove the explicit Width and Height settings, and the HorizontalAlignment and VerticalAlignment settings, thus:
<WindowsFormsHost Background="{x:Null}"
Name="windowsFormsHost1"
Margin="-1,0,0,0">
<wf:Panel x:Name="pnlLivePreview" />
</WindowsFormsHost>
I'm going to throw a wild guess here, but since this is a WinForms panel, try setting it's Dock property to Fill thus:
<wf:Panel x:Name="pnlLivePreview" Dock="Fill" />
Really not sure it would work, if it doesn't work in markup, try doing it in code.
Bind your Height/Width to your window's height/width
<Window x:Name="Root_Window">
<Grid Height="{Binding ElementName=RootWindow, Path=ActualHeight}"
Width="{Binding ElementName=RootWindow, Path=ActualWidth}">
<!-- Content Here -->
</Grid>
</Window>
The answer: Problem is not the panel but the api used to create the content of it.

Stop Gridsplitter stretching content beyond window

Given the below XAML, how do I have the gridsplitter respect the MinHeight given to the 3rd row and have the content remain inside my window?
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition MinHeight="40" />
</Grid.RowDefinitions>
<Expander Grid.Row="0" ExpandDirection="Down" VerticalAlignment="Top">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" MinHeight="40" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Border Grid.Row="0" MinHeight="100" Background="Black" />
<GridSplitter Grid.Row="1" Height="5" HorizontalAlignment="Stretch" VerticalAlignment="Bottom" Background="LightBlue" ResizeBehavior="PreviousAndCurrent" />
</Grid>
</Expander>
<Expander Grid.Row="1" ExpandDirection="Down" VerticalAlignment="Top">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" MinHeight="40" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Border Grid.Row="0" MinHeight="100" Background="Black" />
<GridSplitter Grid.Row="1" Height="5" HorizontalAlignment="Stretch" VerticalAlignment="Bottom" Background="LightBlue" ResizeBehavior="PreviousAndCurrent" />
</Grid>
</Expander>
<Border DockPanel.Dock="Bottom" Grid.Row="2" Background="Lime" MinHeight="30" >
<TextBlock Text="{Binding RelativeSource={RelativeSource Mode=FindAncestor,AncestorType=DockPanel},Path=ActualHeight,StringFormat={}{0:f0}}" />
</Border>
</Grid>
The way your code is, this cannot be done mate. This is due to how the GridSplitter works.
A few points
A GridSplitter will always work on directly adjacent rows/columns
In reality, your MinHeight IS being respected, but so is the GridSplitter's request to grow being respected, which results in the Grid growing larger than your Window
When sized to Auto, a row/column will always resize according to its content, not bigger, and not smaller
Therefore if a GridSplitter is sandwiched between two * sized rows/columns, then it would implicitly respect your MinHeight, since in reality, it would not be touching it
You have a few solutions
Add another row in the 3rd position which is * sized, and have your border on row 3 with a RowSpan of 2 (so the 3rd row is the one being really resized, and your 4th row isn't touched. Though this will also have side-effects.
Handle a mixture of DragEnter and PreviewMouseMove events on the GridSplitter, keeping track of focus, and cancelling (e.Handled = true) the event when a certain size is reached.
This is what I can think of mate, hope I've been of some help.
I created a custom grid splitter class that will not allow the grid splitter to go off the edge of a window (either the bottom or the side).
Public Class CustomGridSplitter
Inherits GridSplitter
Public Enum SplitterDirectionEnum
Horizontal
Vertical
End Enum
Public Property SplitterDirection As SplitterDirectionEnum
Public Property MinimumDistanceFromEdge As Integer
Private _originPoint As Point
Private Sub customSplitter_MouseDown(sender As Object, e As MouseButtonEventArgs) Handles MyBase.MouseDown
_originPoint = e.GetPosition(Window.GetWindow(Me))
End Sub
Private Sub customSplitter_PreviewMouseMove(sender As Object, e As MouseEventArgs) Handles MyBase.PreviewMouseMove
If e.LeftButton = MouseButtonState.Pressed Then
Dim pwindow As Window = Window.GetWindow(Me)
Dim newPoint As Point = e.GetPosition(pwindow)
If SplitterDirection = SplitterDirectionEnum.Horizontal Then
If newPoint.Y >= _originPoint.Y Then
If newPoint.Y >= pwindow.ActualHeight - MinimumDistanceFromEdge Then
e.Handled = True
End If
Else
If newPoint.Y > pwindow.ActualHeight - (MinimumDistanceFromEdge + 2) Then
e.Handled = True
End If
End If
Else
If newPoint.X >= _originPoint.X Then
If newPoint.X >= pwindow.ActualWidth - MinimumDistanceFromEdge Then
e.Handled = True
End If
Else
If newPoint.X > pwindow.ActualWidth - (MinimumDistanceFromEdge + 2) Then
e.Handled = True
End If
End If
End If
_originPoint = newPoint
End If
End Sub
End Class
To use it in XAML:
<CustomGridSplitter SplitterDirection="Vertical" MinimumDistanceFromEdge="100" x:Name="splitterCenter" ResizeDirection="Columns" Grid.Column="1" HorizontalAlignment="Center" VerticalAlignment="Stretch" Width="2" Margin="2,0,2,0"/>
The custom properties to set are the "SplitterDirection" and "MinimumDistanceFromEdge". Everything works like the base grid splitter.
This uses mouse events to determine where in the window the user is dragging the splitter and handles the events if they get too close to the edge.
I found another solution to this problem, though in a much more simple case where I just had two columns inside a window that I wanted to resize.
The solution that I came up with (described in more detail here: https://stackoverflow.com/a/46924893/6481970) was to add event callbacks for when the grid was resized, when the GridSplitter moved, and when the window was resized (to handle the case where you resize the window to no longer fit the content because the grid doesn't automatically resize itself to fit the smaller window).
Here is some simplified code:
XAML:
<Grid x:Name="ResizeGrid" SizeChanged="ResizeGrid_SizeChanged">
<Grid.ColumnDefinitions>
<ColumnDefinition x:Name="C0" Width="150" MinWidth="50" />
<ColumnDefinition Width="5" />
<ColumnDefinition x:Name="C2" Width="*" MinWidth="50" />
</Grid.ColumnDefinitions>
<Grid Grid.Column="0" Background="Green" />
<GridSplitter Grid.Column="1" Width="5" HorizontalAlignment="Stretch" DragCompleted="GridSplitter_DragCompleted" />
<Grid Grid.Column="2" Background="Red" />
</Grid>
C# Code Behind:
C0.MaxWidth = Math.Min(ResizeGrid.ActualWidth, ActualWidth) - (C2.MinWidth + 5);

Dynamic Clipping Width in Silverlight Within Canvas

Why does this throw an error and how can I fix... I need to set the clipping rect to be dynamic.
<Grid.ColumnDefinitions>
<ColumnDefinition Width="42"/>
<ColumnDefinition x:Name="ListBoxContainer" Width="*"/>
<ColumnDefinition Width="42"/>
</Grid.ColumnDefinitions>
<Canvas>
<Button x:Name="btnGalleryLeft"
Click="btnGalleryLeftClick"
Style="{StaticResource GalleryNavigationLeft}"
Canvas.Left="7"
Canvas.Top="50" />
</Canvas>
<Canvas Grid.Column="1" x:Name="ListboxWrapper">
<Canvas.Clip>
<RectangleGeometry>
<RectangleGeometry.Rect>
<Rect X="0" Y="0"
Width="{Binding ElementName=ListBoxContainer, Path=Width}"
Height="{Binding ElementName=ListBoxContainer, Path=Height}"/>
</RectangleGeometry.Rect>
</RectangleGeometry>
</Canvas.Clip>
<ListBox x:Name="ListBox1"
Margin="15, 18, 15, 0"
Style="{StaticResource GalleryListBoxStyle}"
ItemsSource="{Binding DocItemCollection}"
SelectionChanged="ListBox_SelectionChanged"
MouseLeftButtonUp="ListBox_MouseLeftButtonUp"
Canvas.Top="0"
Canvas.Left="0"
/>
</Canvas>
<Canvas Grid.Column="2">
<Button x:Name="btnGalleryRight"
Click="btnGalleryRightClick"
Style="{StaticResource GalleryNavigationRight}"
Margin="0, 0, 7, 0"
Canvas.Top="50" />
You can still use RectangleGeometry as clipping area. Just subscribe to the Loaded event, and in that create a new RectangleGeometry
void control_Loaded(object sender, RoutedEventArgs e)
{
LayoutRoot.DataContext = this;
Rect rect = new Rect(0, 0, yourWidth, yourHeight);
RectangleGeometry reo = new RectangleGeometry();
reo.Rect = rect;
this.canvas.Clip = reo;
}
Just to add little information
In addition, opacity and clip property settings are handled by the
composition thread. However, if an Opacity mask or non-rectangular
clip is used on the animated object, these operations will be passed
to the UI Thread. This means that even if the clip region is a
rectangular shape but uses the PathGeometry the operations will be
passed to the UI thread. So make sure to always use the
RectangleGeometry for clipping paths if possible.
Solution finally.... after much cursin and swearing. If only everything was as stright fwd as in CSS (overflow bloody hidden property).
Front-end:
<Canvas Grid.Column="1" x:Name="ListboxWrapper" Background="Black">
<ScrollViewer Background="Red"
FlowDirection="LeftToRight"
HorizontalScrollBarVisibility="Hidden"
VerticalScrollBarVisibility="Hidden"
x:Name="ScrollViewerClipper">
<Canvas x:Name="CarouselContainer">
<gallery:ImageCarousel x:Name="carousel" />
</Canvas>
</ScrollViewer>
Back-end:
public GalleryPanel()
{
InitializeComponent();
LayoutRoot.SizeChanged +=new SizeChangedEventHandler(LayoutRoot_SizeChanged);
}
private void LayoutRoot_SizeChanged(object s, SizeChangedEventArgs e)
{
ScrollViewerClipper.Width = ListboxWrapper.ActualWidth;
ScrollViewerClipper.Height = ListboxWrapper.ActualHeight;
}
I think you are over complicating it. Binding to Width/Height of ColumnDefinition not going to work. I'd simply create behavior that would subscribe to SizeChanged event and set clipping based on ActualWidth[Height].

Resources