WPF DockPanel not Docking / Default fixed width and minimum width - wpf

I'm trying to make a footer control that has a minimum height that it will shrink to before allowing the a window resize encroaches on its view-able area. But I want a fixed width that it will adhere to until a resize of the main window encroaches on its default bounds. Given the following code, what I'm not understanding is:
-On shrinking the window size after running the sample, why is the bottom anchor of the lower canvas not respected? Instead it's anchoring to the canvas above it.
-Why is the is minimum size of the bottom panel not shrunk too before the window encroaches on its area?
-Why do I have to add the bottom canvas before the top for this demo to even layout correctly?
-Lastly, is there a way to make a Window's minimum bounds just be the sum of all the horizontal and vertical minimum bounds of the controls it contains?
<Window x:Class="TestWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="TestWindow" Height="300" Width="1278" Background="{x:Null}">
<DockPanel Background="#FFE6AFAF" VerticalAlignment="Stretch" >
<Canvas DockPanel.Dock="Bottom" HorizontalAlignment="Stretch" MinHeight="100" Height="170" Margin="10,10,10,10" VerticalAlignment="Stretch" Width="Auto">
<Canvas.Background>
<SolidColorBrush Color="{DynamicResource {x:Static SystemColors.ControlLightColorKey}}"/>
</Canvas.Background>
</Canvas>
<Canvas DockPanel.Dock="Top,Bottom" HorizontalAlignment="Stretch" Height="Auto" Margin="10,10,10,10" VerticalAlignment="Stretch" Width="Auto">
<Canvas.Background>
<SolidColorBrush Color="{DynamicResource {x:Static SystemColors.ControlLightColorKey}}"/>
</Canvas.Background>
</Canvas>
</DockPanel>
</Window>

To start with question 3:
When using a DockPanel, the order in which you add elements matters. What happens here is that the "MinHeight=100" canvas is added first, and the DockPanel.Dock="Bottom" says: "Stretch this canvas across the bottom of the DockPanel and make it as tall as it needs to be, but no taller, because we need to keep as much space as possible available for the rest of the elements".
This process is then repeated for each consecutive element in the DockPanel until the very last element, which gets to use all the space that is left in the DockPanel (unless you set <DockPanel LastChildFill="False" ...). This example might help illustrate how the DockPanel works:
<DockPanel Width="200" Height="200" >
<Button Content="01" Background="#222" DockPanel.Dock="Bottom" />
<Button Content="02" Background="#333" DockPanel.Dock="Left" />
<Button Content="03" Background="#444" DockPanel.Dock="Top" />
<Button Content="04" Background="#555" DockPanel.Dock="Right" />
<Button Content="05" Background="#666" DockPanel.Dock="Bottom" />
<Button Content="06" Background="#777" DockPanel.Dock="Left" />
<Button Content="07" Background="#888" DockPanel.Dock="Top" />
<Button Content="08" Background="#999" DockPanel.Dock="Right" />
<Button Content="09" Background="#aaa" DockPanel.Dock="Bottom" />
<Button Content="10" Background="#bbb" DockPanel.Dock="Left" />
<Button Content="11" Background="#ccc" DockPanel.Dock="Top" />
<Button Content="12" Background="#ddd" />
</DockPanel>
So in your case, the first canvas is anchored to the bottom and stretches horizontally across the DockPanel. Its height will always be 100 pixels, because its MinHeight says that's the lowest height it will accept.
Then, the second canvas is added, and because it's the last element, it's allowed to use all the space that's left above the first canvas.
Question 3, part "-Lastly":
Try <Window ... SizeToContent="WidthAndHeight" />
Question 2:
You mean if you shrink the window to be less than 100 pixels tall? Elements will never accept to be smaller than their minimum size (in this case 100 pixels tall). The canvas renders itself at 100 pixels, and what doesn't fit inside the window simply gets clipped.
..and I'm not sure what you mean in Question 1..

Related

Horizontal dashed line stretched to container width

I have a layout contained within a ScrollViewer in which I need to draw a horizontal dashed line that stretches to the full width of the container. The closest I've managed is the following
<ScrollViewer HorizontalScrollBarVisibility="Auto">
<StackPanel>
<Button Width="400" Height="50" VerticalAlignment="Top" Margin="10" />
<Line HorizontalAlignment="Stretch" VerticalAlignment="Bottom" Stroke="Black"
X2="{Binding ActualWidth, RelativeSource={RelativeSource Self}}"
StrokeDashArray="2 2" StrokeThickness="1" />
</StackPanel>
</ScrollViewer>
This nearly works, however once the container (in my case a window) has been enlarged, the line doesn't shrink back down to the appropriate size when the container is sized back down. The below is the screenshot of the same window after I have horizontally sized the window up and down.
Note that the fact that the line is dashed is important as it means that solutions that involve stretching the line don't work (the dashes appear stretched).
I know that this is because of the X2="{Binding ActualWidth, RelativeSource={RelativeSource Self}}" binding (by design the line is always the widest thing in the scrollable region, so when I size the window down the scrollable region the line defines the width of the scrollable region), however I can't think of a solution.
How can I fix this problem?
Screenshot of why using ViewportWidth doesn't work
I realised that what I needed was for the Line to ask for zero space during the measure step of layout, but then use up all the available space during the arrange step. I happened to stumble across the question Make WPF/SL grid ignore a child element when determining size which introduced the approach of using a custom decorator which included this logic.
public class NoSizeDecorator : Decorator
{
protected override Size MeasureOverride(Size constraint) {
// Ask for no space
Child.Measure(new Size(0,0));
return new Size(0, 0);
}
}
(I was hoping that some existing layout control incorporated this logic to avoid having to write my own layout logic, however the logic here is so simple that I'm not really that fussed). The modified XAML then becomes
<ScrollViewer HorizontalScrollBarVisibility="Auto">
<StackPanel>
<Button Width="400" Height="50" VerticalAlignment="Top" Margin="10" />
<local:NoSizeDecorator Height="1">
<Line Stroke="Black" HorizontalAlignment="Stretch"
X2="{Binding ActualWidth, RelativeSource={RelativeSource Self}}"
StrokeDashArray="2 2" StrokeThickness="1" />
</local:NoSizeDecorator>
</StackPanel>
</ScrollViewer>
This works perfectly
You may put a very long Line in a left-aligned Canvas with zero Width and ClipToBounds set to false.
<ScrollViewer HorizontalScrollBarVisibility="Auto">
<StackPanel>
<Button Width="400" Height="50" VerticalAlignment="Top" Margin="10" />
<Canvas HorizontalAlignment="Left" Width="0" ClipToBounds="False">
<Line Stroke="Black" StrokeDashArray="2 2" X2="10000"/>
</Canvas>
</StackPanel>
</ScrollViewer>

TextBlock text wrapping pushes other components

I have a TextBlock and a couple Buttons in a StatusBar. The Buttons are effectively right-aligned (e: actually the StatusBarItem containing them is). When the window is shrunk horizontally, and the text wraps, the TextBlock pushes the Buttons off the window to varying degrees.
Why does this happen, and how can I fix the positions of the Buttons?
<Window x:Class="TextWrapping.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">
<DockPanel>
<StatusBar DockPanel.Dock="Bottom">
<StatusBarItem>
<TextBlock Name="statusText" TextWrapping="Wrap" Text="when this text wraps, it pushes the buttons off the window" />
</StatusBarItem>
<StatusBarItem HorizontalAlignment="Right">
<StackPanel Orientation="Horizontal">
<Button>one</Button>
<Button>two</Button>
</StackPanel>
</StatusBarItem>
</StatusBar>
<TextBlock Text="shink this window horizontally" />
</DockPanel>
</Window>
You can see this blog post for more information, but basically the StatusBar is using a DockPanel to present it's items. So for the code you have above, the statusText is being docked left, and the buttons are filling the remaining space (but aligned horizontally within that area).
So as you size smaller, the TextBlock will always take as much space as it needs (allowing the buttons to size to zero). When the text is wrappped, the buttons get back a little more space as the TextBlock doesn't need all the horizontal space.
To fix it you can change your code to:
<DockPanel>
<StatusBar DockPanel.Dock="Bottom">
<StatusBarItem DockPanel.Dock="Right">
<StackPanel Orientation="Horizontal">
<Button>one</Button>
<Button>two</Button>
</StackPanel>
</StatusBarItem>
<StatusBarItem>
<TextBlock Name="statusText" TextWrapping="Wrap" Text="when this text wraps, it pushes the buttons off the window" />
</StatusBarItem>
</StatusBar>
<TextBlock Text="shink this window horizontally" />
</DockPanel>
Or you can use the trick shown the blog post, to replace the DockPanel with a Grid.

Why ZIndex is not considered in arrange override of a stackpanel

Here is what I tried to to:
Added a stackpanel to my window (Orientation: Horizontal)
Added a set of buttons to it
Set the first button's ZIndex to be higher than the second one
Increased the width of the first button
What I expected:
I expected the first button to be on top of the second button (atlest overlay)
StackPanel's width should not change unless the width of the first button is no more sufficient
What is happening actually:
First button's width increases and the second button moves towards the right accordingly. They stay on the same plane
StackPanel's width increases with increase in the first button's width
Here is my question:
I know that stackpanel has not considered ZIndex while arranging the items within itself, BUT WHY?? Shouldn't it consider the ZIndex of its children while arranging them???
The Stackpanel 'stacks' its children based on their widths, i.e. if you increase the width of an item (or increase its margin), the stackpanel will simply expand to accomodate this. If you want to force items within a stackpanel to overlap, you will have to change their location after the layout has been computed. You can perform this using a RenderTransform. See the example below:
<StackPanel Orientation="Horizontal" VerticalAlignment="Top">
<Button Content="One" Canvas.ZIndex="10">
<Button.RenderTransform>
<TranslateTransform X="10"/>
</Button.RenderTransform>
</Button>
<Button Content="One"/>
<Button Content="One"/>
<Button Content="One"/>
</StackPanel>
And yes, the ZIndex is respected. This is an attached proepry of Canvas, however, it seems to be used by the rendering engine directly rather than by Canvas, hence it works in the above code.
I tried to find some relevant info about how to set the z index of wpf layout elements and panels. Using a Canvas comes with a different set of positioning issues which I simply hadn't the time to investigate. Here is a simple solution using the Panel.ZIndex property in xaml.
<Grid>
<Border Width="100" Height="100" Margin="0,0,50,50" Panel.ZIndex="1" Background="Navy" Opacity="0.3"
VerticalAlignment="Top" HorizontalAlignment="Left">
</Border>
<Border Width="100" Height="100" Margin="50,50,0,0" Background="Fuchsia" Opacity="0.3">
</Border>
The resulting two square border elements will overlap. One can use stackpanels instead of borders and use this logic to overlap anything easily.
Here is the same code adapted to the button problem:
<Grid>
<StackPanel Panel.ZIndex="10" Margin="20,20,0,0" HorizontalAlignment="Left">
<Button Content="One" Width="50" Height="40">
</Button>
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="50,0,0,0" >
<Button Content="Two" Width="50" Height="40"/>
<Button Content="Three" Width="50" Height="40"/>
<Button Content="Four" Width="50" Height="40"/>
</StackPanel>
</Grid>

TextBox expanding with surrounding Grid but not with text

A window has a Grid with two columns. The left column contains a control with a constant width but with a height that adapts. The right column contains a TextBox that takes up all remaining space in the Grid (and thereby in the Window).
The Grid is given a minimal width and height and is wrapped within a ScrollViewer. If the user resizes the window to be smaller than the minimal width/height of the Grid, scrollbars are displayed.
This is exactly how I want it to be. However, a problem occurs when the user starts typing text. If the text is to long to fit in one line in the TextBox, I want the text to wrap. Therefore I set TextWrapping="Wrap" on the TextBox. But since the TextBox has an automatic width and is wrapped in a ScrollViewer (its actually the whole Grid that is wrapped), the TextBox just keeps expanding to the right.
I do want the TextBox to expand if the window is expanded, but I don't want the TextBox to expand by the text. Rather the text should wrap inside the available TextBox. If the text don't fit within the TextBox height, a scrollbar should be displayed within the TextBox.
Is there a way to accomplish this?
Below is some code that shows my problem:
<Window x:Class="AdaptingTextBoxes.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="300" Width="400" Background="DarkCyan">
<Grid Margin="10" Name="LayoutRoot">
<ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto">
<Grid MinWidth="300" MinHeight="200">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Button Grid.Column="0" Margin="0,0,10,0" Content="Button" Width="100" />
<TextBox Grid.Column="1" AcceptsReturn="True" TextWrapping="Wrap" ScrollViewer.HorizontalScrollBarVisibility="Disabled" ScrollViewer.VerticalScrollBarVisibility="Auto" />
</Grid>
</ScrollViewer>
</Grid>
</Window>
You could use an invisible border (its hacky but it works - its how I tend to sort out dynamic textbox sizes in Xaml):
<Border BorderThickness="0" x:Name="border" Grid.Column="1" Margin="0.5" />
<TextBox Grid.Column="1" AcceptsReturn="True" TextWrapping="Wrap" Width="{Binding ActualWidth, ElementName=border}" Height="{Binding ActualHeight, ElementName=border}" />
Have you tried setting the MaxWidth property on just the TextBox?
Edit after OP's comment
I would try getting rid of the ScrollViewer. The sizing used in the Grid's layout should take care of re-sizing and the scroll bar settings on the TextBox should take care of the rest.
The answer is based on Leom's answer.
The solution works great when you enlarge the window, but the resizing is not smooth when you make the window smaller. As the textbox participates in the grid's layout, it has to perform layout process multiple times. You can fix that by putting the texbox in the canvas, so the change of the size of the textbox no longer triggers the grid's re-layout.
The updated code:
<Border BorderThickness="0" x:Name="border" Grid.Column="1" Margin="0.5" />
<Canvas Grid.Column="1">
<TextBox AcceptsReturn="True" TextWrapping="Wrap" Width="{Binding ActualWidth, ElementName=border}" Height="{Binding ActualHeight, ElementName=border}" />
</Canvas>

How to incorporate Canvas into a larger layout in WPF?

Canvas doesn't seem to play well together nicely with the other elements when you try to build it into a layout and have e.g. controls on the side and the canvas is the drawing area.
For instance, why can I put a border around every element except a canvas? In the following code, border wraps canvas but the canvas only has the border on the top but not on the left, right or bottom:
<Window x:Class="WpfApplication25.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window1" Height="300" Width="300">
<StackPanel>
<TextBlock DockPanel.Dock="Bottom" Text="Move the slider to reveal the answer:"/>
<Slider DockPanel.Dock="Bottom" Name="theSlider"
HorizontalAlignment="Left"
Width="200"
Minimum="0"
Maximum="1"
Value="1"
Cursor="Hand"/>
<Border BorderBrush="Tan" BorderThickness="2">
<Canvas>
<TextBlock Canvas.Left="45" Canvas.Top="50" Text="test" FontSize="16"/>
<Rectangle
Canvas.Left="10"
Canvas.Top="10"
Width="100"
Height="100"
Fill="Silver"
Opacity="{Binding ElementName=theSlider, Path=Value}"
/>
</Canvas>
</Border>
</StackPanel>
</Window>
From what I can tell in XamlPad, the problem appears to be that your Canvas does not have an explicit height/width, and that its HorizontalAlignment defaults to being in the middle of the Border. Without an explicit height and width the Border appears to collapse to 0 height and stretches on the width. My assumption is this is because your Border is in a StackPanel, as placing the Border in a Grid, causes it to behave as expected.
Your best bet is to give the Canvas an explicit Height and Width. Not sure that is what you're looking for though.
As far as I understand what you are trying to achieve, you should place your controls in one cell of a Grid and your Canvas in another.

Resources