Horizontal dashed line stretched to container width - wpf

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>

Related

WPF DockPanel not Docking / Default fixed width and minimum width

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..

wpf - Binding issue ActualWidth

I have the following XAML code:
<Grid Grid.Row="2" Name="grid_StatusBar">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="{Binding ElementName=wrapPanel, Path=ActualWidth}" />
<ColumnDefinition Width="30" />
</Grid.ColumnDefinitions>
<ProgressBar Grid.Column="0" HorizontalAlignment="Stretch" Margin="5,5,5,5" Name="progressBar1" VerticalAlignment="Stretch" />
<WrapPanel Grid.Column="1" Name="wrapPanel" HorizontalAlignment="Right">
<Label Content="1" Height="28" HorizontalAlignment="Right" Margin="0,0,0,0" Name="label_Dataset" VerticalAlignment="Stretch" Visibility="Collapsed" />
<Label Content="/20" Height="28" HorizontalAlignment="Right" Margin="0,0,0,0" Name="label_TotalDatasets" VerticalAlignment="Stretch" Visibility="Collapsed" />
<Label Content="ID:" Height="28" HorizontalAlignment="Right" Margin="0,0,0,0" Name="label_IDText" VerticalAlignment="Stretch" />
<Label Content="no id" Height="28" HorizontalAlignment="Right" Margin="0,0,0,0" Name="labelID" VerticalAlignment="Stretch" />
</WrapPanel>
<Button Grid.Column="2" Name="button_Help" Height="30" Width="30" Content="?" HorizontalAlignment="Right" VerticalAlignment="Stretch" Click="button_Help_Click" >
</Grid>
What i am trying to show the progressbar as wide as possible while changing the visibility of some of the labels and setting different texts (and therefore different lengths/widths.
I have then different functions:
At program start the "no id" text of the labelID Label is replaced with an internal value. The width is updated: OK
With the code running I change the visibility of the first two labels, and the ColumnDefinition.Width is not updated and only the first two labels are shown (because there is not place enough for all the 4 of them to fit in the wrapPanel ActualWidth: ERROR!
If I change the Visibility property from the first two Labels from Collapsed to Visible, from start the visibility of all the Labels are Visible, and all the Labels are shown: OK
From the previous state I change visibility of the first two Labels to Collapsed, the ColumnDefinition.Width is updated and the ProgressBar is as wide as possible and all the text is shown: OK
Could anyone please help me? I do not understand why the width is not updated...
NOTE: The visibility is changed using two buttons in the window running the following code:
label_Dataset.Visibility = System.Windows.Visibility.Visible;
label_TotalDatasets.Visibility = System.Windows.Visibility.Visible;
EDIT: My target is to show the Visible Labels using the minimum space (in one line) in order to have the ProgressBar using the maximum width possible.
It is not possible to change the width of a parent element based on the width of a child. In your case, the column is the parent of the WrapPanel, so the ActualWidth of the WrapPanel is not available until after the grid/column has already been sized.
Even if you wrote code to try and circumvent this, you would still run into an issue, as the measure/layout sequence would become re-entrant. Whenever the size of the column changed, it would re-layout all the child controls, one of which is the WrapPanel. This would cause the ActualWidth of the WrapPanel to be changed -- effectively causing an infinite loop. To prevent this stack overflow scenario, the framework immediately detects any re-entrancy in the layout/measure cycle and throws an exception.
I'm not exactly sure what you are trying to achieve. Why do would you want the Labels in a WrapPanel? Surely you would always want the 4 labels to be on the same line, or at least on two lines.
If this is the case, I would set the width of the second Column to "Auto" and put the labels in another container, i.e. one of the following:
If you want all the labels to wrap, as in a WrapPanel being forced to its minimum width, use:
<StackPanel Grid.Column="1" Orientation="Vertical">
... your labels here ...
</StackPanel>
If you want all the labels to stay on the same line, use:
<StackPanel Grid.Column="1" Orientation="Horizontal">
... your labels here ...
</StackPanel>
If you want two lines use:
<StackPanel Grid.Column="1" Orientation="Vertical">
<StackPanel Orientation="Horizontal">
... 1st 2 labels here ...
</StackPanel>
<StackPanel Orientation="Horizontal">
... 2nd 2 labels here ...
</StackPanel>
</StackPanel>

Validation.ErrorTemplate size

I've got the following control template which I use as a Validation.ErrorTemplate for TextBoxes:-
<ControlTemplate x:Key="ControlValidationErrorTemplate">
<DockPanel LastChildFill="True">
<Border Background="Red"
DockPanel.Dock="right"
Padding="2,0,2,0"
ToolTip="{Binding ElementName=valAdorner, Path=AdornedElement.(Validation.Errors), Converter={x:Static val:ValidationErrorsConverter.Instance}}">
<TextBlock Text="!"
VerticalAlignment="center"
HorizontalAlignment="center"
FontWeight="Bold"
Foreground="white" />
</Border>
<AdornedElementPlaceholder x:Name="valAdorner"
VerticalAlignment="Center">
<Border BorderBrush="red"
BorderThickness="1" />
</AdornedElementPlaceholder>
</DockPanel>
</ControlTemplate>
When a TextBox contains invalid content, the above template applies a red border and adds a red box containing an exclamation mark immediately to the right of the TB.
The problem is, the exclamation mark overlaps anything immediately to the right of the TB, rather than the layout changing to accomomodate the exclamation mark. I have a similar problem in DataGrids - the exclamation mark overlaps the right-hand edge of the containing cell, rather than the column width increasing to accommodate it.
Using Snoop, it appears that the template is being displayed in an "adorner layer" which I assume is a separate visual tree? This would explain why the window's layout isn't recalculated to take into account the exclamation mark. Can anyone suggest a way to achieve what I want?
As I suspected, it's due to the error template being rendered on the adorner layer, so it doesn't affect the layout of the window. See: http://social.msdn.microsoft.com/Forums/en-US/wpf/thread/9de3c9e5-5759-4f88-9184-175d3eaabdad/
I'm now using this control template instead:-
<ControlTemplate x:Key="ControlValidationErrorTemplate">
<Grid>
<Polygon Points="9,9 9,0 0,0"
Stroke="Red"
StrokeThickness="1"
Fill="Red"
HorizontalAlignment="Right"
VerticalAlignment="Top"
ToolTip="{Binding ElementName=valAdorner, Path=AdornedElement.(Validation.Errors), Converter={x:Static val:ValidationErrorsConverter.Instance}}" />
<AdornedElementPlaceholder x:Name="valAdorner"
VerticalAlignment="Center">
<Border BorderBrush="red"
BorderThickness="1" />
</AdornedElementPlaceholder>
</Grid>
</ControlTemplate>
This draws a red border around the control, with a small red triangle overlapping the top-right corner of the control - hovering over this displays a tooltip containing the error message.

VerticalAlignment="Stretch" for label inside canvas doesn't work

How to use VerticalAlignment="Stretch" with a Label inside a Canvas? I'm trying to center the text "Cancel" in the button as in the code below. To use fixed height and width for the label isn't a desired option.
<Button Name="buttonCancel" Width="80" Height="40" IsCancel="True" Padding="0" HorizontalContentAlignment="Stretch" VerticalContentAlignment="Stretch">
<Canvas>
<Label Canvas.Top="0" Canvas.Left="0" Padding="0" FontSize="10">Esc</Label>
<Label VerticalContentAlignment="Center" HorizontalContentAlignment="Center" VerticalAlignment="Stretch" HorizontalAlignment="Stretch">Cancel</Label>
</Canvas>
</Button>
Use a binding to the Canvas's ActualWidth:
<Canvas>
<Label Width="{Binding RelativeSource={RelativeSource AncestorType={x:Type Canvas}}, Path=ActualWidth}">...</Label>
</Canvas>
But as mentioned above, if you are interested in dynamic stretching layouts, the Canvas is not the ideal choice of control.
A Canvas does not perform any scaling layout of its contents; if you want to scale the contents, you could use Grid in this case, which will, by default, scale both Label elements to fill the Content space.
Assuming you need the canvas for other objects that are of a fixed nature, you could overlay the Canvas on a Grid, and then put the labels in the grid. You can put the labels before the canvas to make them background z-index (overwritten by canvas objects) or after the canvas to make them higher z-index (will overwrite canvas objects). For example:
<Button Name="buttonCancel" Width="80" Height="40" IsCancel="True" Padding="0" HorizontalContentAlignment="Stretch" VerticalContentAlignment="Stretch">
<Grid>
<Label Padding="0" FontSize="10">Esc</Label>
<Label VerticalContentAlignment="Center" HorizontalContentAlignment="Center" VerticalAlignment="Stretch" HorizontalAlignment="Stretch">Cancel</Label>
<Canvas>
<!-- Your Canvas content here -->
</Canvas>
</Grid>
</Button>
Repeating my solution from the comments, since (a) you really don't want a Canvas and (b) it sounds like this solved your problems, so I'll make it an answer where it will be more visible to others.
Canvas is meant for fixed-pixel-size layouts, which is probably the least common case. You should replace your Canvas with a Grid as shown below, so that both Labels are laid out dynamically (and independently) within the available space:
<Grid>
<Label Padding="0" FontSize="10">Esc</Label>
<Label VerticalAlignment="Center" HorizontalAlignment="Center">Cancel</Label>
</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>

Resources