WPF Zoom + Scrollbar? - wpf

I'm trying to zoom some contents within scrollviewer.
The zoom behavior I'm looking for is that of a RenderTransform+ScaleTransform. But this does not work with the ScrollViewer.
Using LayoutTransform+ScaleTransform, the scrollviewer does get affected (ContentTemplate1 only), but does not behave like a zoom.
Assuming ContentTemplate1/ContentTemplate2 cannot be changed (ie, 3rd party controls), how can I get zoom to work with a scrollviewer?
<Grid>
<Grid.Resources>
<!-- Content type 1 -->
<DataTemplate x:Key="ContentTemplate1">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="150"/>
<ColumnDefinition />
</Grid.ColumnDefinitions>
<TextBlock Background="DodgerBlue" Text="Left"/>
<TextBlock Grid.Column="1" Background="DarkGray" Text="Right"/>
</Grid>
</DataTemplate>
<!-- Content type 2 -->
<DataTemplate x:Key="ContentTemplate2">
<Viewbox>
<TextBlock Background="DodgerBlue" Text="Scale to fit" Width="100" Height="70" Foreground="White" TextAlignment="Center"/>
</Viewbox>
</DataTemplate>
</Grid.Resources>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TabControl>
<!-- Content 1 -->
<TabControl.Resources>
<ScaleTransform x:Key="ScaleTransform"
ScaleX="{Binding ElementName=ZoomSlider,Path=Value}"
ScaleY="{Binding ElementName=ZoomSlider,Path=Value}" />
</TabControl.Resources>
<TabItem Header="Content 1">
<ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto">
<ContentControl ContentTemplate="{StaticResource ContentTemplate1}" Margin="10" RenderTransformOrigin=".5,.5">
<!-- Affects scrollviewer, but does not behave like a zoom -->
<!--<FrameworkElement.LayoutTransform>
<StaticResource ResourceKey="ScaleTransform" />
</FrameworkElement.LayoutTransform>-->
<!-- Expected zoom behavior, but doesn't affect scrollviewer -->
<FrameworkElement.RenderTransform>
<StaticResource ResourceKey="ScaleTransform" />
</FrameworkElement.RenderTransform>
</ContentControl>
</ScrollViewer>
</TabItem>
<!-- Content 2 -->
<TabItem Header="Content 2">
<ContentControl ContentTemplate="{StaticResource ContentTemplate2}" Margin="10" RenderTransformOrigin=".5,.5">
<!-- Affects scrollviewer, but does not behave like a zoom -->
<!--<FrameworkElement.LayoutTransform>
<StaticResource ResourceKey="ScaleTransform" />
</FrameworkElement.LayoutTransform>-->
<!-- Expected zoom behavior, but doesn't affect scrollviewer -->
<FrameworkElement.RenderTransform>
<StaticResource ResourceKey="ScaleTransform" />
</FrameworkElement.RenderTransform>
</ContentControl>
</TabItem>
</TabControl>
<StackPanel Grid.Row="1" Orientation="Horizontal">
<!-- Zoom -->
<Slider x:Name="ZoomSlider"
Width="100"
Maximum="5"
Minimum="0.1"
Orientation="Horizontal"
Value="1" />
<!-- Autofit -->
<CheckBox Content="Autofit?" x:Name="AutoFitCheckBox" />
</StackPanel>
</Grid>

If I understand correctly:
You want to zoom with the ZoomSlider slider?
You want scrollbars to appear if the content is too large to fit within its tab?
If so, it's LayoutTransform you want. That transformation is done before all elements are measured and laid out, and the ScrollViewer will be able to tell whether scrollbars are needed.
On my machine, the "Content 1" tab works as expected if you just switch to LayoutTransform (note that you have to zoom a lot before "Right" disappears off-screen, toggling the scrollbar):
"Content 2" requires a little more work. First of all, there's no ScrollViewer in that tab, so that needs to be added. Secondly, ContentTemplate2 uses a ViewBox, which stretches by default, so zooming won't have an effect until you zoom in really close. To disable the ViewBox' built-in "zooming", you can center the ContentControl container (using HorizontalAlignment/VerticalAlignment), which forces it to take up as little space as possible:
<TabItem Header="Content 2">
<ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto">
<ContentControl ContentTemplate="{StaticResource ContentTemplate2}" ...
HorizontalAlignment="Center" VerticalAlignment="Center" >
<FrameworkElement.LayoutTransform>
...

To make the zoomed elements get the exact RenderTransform look, we may as well stick with RenderTransform, and instead tell the ScrollViewer how to behave by implementing our own scrolling logic. This approach is based on this excellent tutorial:
https://web.archive.org/web/20140809230047/http://tech.pro/tutorial/907/wpf-tutorial-implementing-iscrollinfo
We create our own custom "ZoomableContentControl" which implements IScrollInfo and tell the ScrollViewer to get its scrolling logic from there (ScrollViewer.CanContentScroll = True). The magic happens in ArrangeOverride() where we play with ExtentWidth/ExtentHeight and RenderTransformOrigin.
public class ZoomableContentControl : ContentControl, IScrollInfo
{
public ZoomableContentControl()
{
this.RenderTransformOrigin = new Point(0.5, 0.5);
}
private ScaleTransform _scale = null;
private ScaleTransform Scale
{
get
{
if (_scale == null)
{
_scale = this.RenderTransform as ScaleTransform;
//RenderTransforms don't update the layout, so we need to trigger that ourselves:
_scale.Changed += (s, e) => { InvalidateArrange(); };
}
return _scale;
}
}
protected override Size ArrangeOverride(Size arrangeBounds)
{
Statics.MessageIfDebug("Arranging");
var layout = base.ArrangeOverride(arrangeBounds);
var scale = this.Scale;
if (scale != null)
{
//Because RenderTransforms don't update the layout,
//we need to pretend we're bigger than we are to make room for our zoomed content:
_extent = new Size(layout.Width * scale.ScaleX, layout.Height * scale.ScaleY);
_viewport = layout;
//Coerce offsets..
var maxOffset = new Vector(ExtentWidth - ViewportWidth, ExtentHeight - ViewportHeight);
_offset.X = Math.Max(0, Math.Min(_offset.X, maxOffset.X));
_offset.Y = Math.Max(0, Math.Min(_offset.Y, maxOffset.Y));
//..and move the zoomed content within the ScrollViewer:
var renderOffsetX = (maxOffset.X > 0) ? (_offset.X / maxOffset.X) : 0.5;
var renderOffsetY = (maxOffset.Y > 0) ? (_offset.Y / maxOffset.Y) : 0.5;
this.RenderTransformOrigin = new Point(renderOffsetX, renderOffsetY);
if (ScrollOwner != null)
{
ScrollOwner.InvalidateScrollInfo();
}
}
return layout;
}
#region IScrollInfo
//This is the boilerplate IScrollInfo implementation,
//which can be found in *the first half* of this tutorial:
//https://web.archive.org/web/20140809230047/http://tech.pro/tutorial/907/wpf-tutorial-implementing-iscrollinfo
//(down to and including SetHorizontalOffset()/SetVerticalOffset()).
//Note the bug reported by "Martin" in the comments.
...
Usage:
<TabItem Header="Content 1">
<ScrollViewer CanContentScroll="True"
HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto">
<v:ZoomableContentControl ContentTemplate="{StaticResource ContentTemplate1}" Margin="10" >
<FrameworkElement.RenderTransform>
<StaticResource ResourceKey="ScaleTransform" />
</FrameworkElement.RenderTransform>
</v:ZoomableContentControl>
</ScrollViewer>
</TabItem>
<TabItem Header="Content 2">
<ScrollViewer CanContentScroll="True"
HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto">
<v:ZoomableContentControl ContentTemplate="{StaticResource ContentTemplate2}" Margin="10" >
<FrameworkElement.RenderTransform>
<StaticResource ResourceKey="ScaleTransform" />
</FrameworkElement.RenderTransform>
</v:ZoomableContentControl>
</ScrollViewer>
</TabItem>

My first advice is to check what kind of zooming functionality you can with using a commercial third party zoom control that already have support for ScrollViewer and also has many additional zooming and panning features.
Now to the solution of your problem:
You can make your code work with using LayoutTransform, but you will need to set the size of the ScrollViewer's content to a fixed value.
Currently you have a Grid inside a ScrollViewer. The Grid does not have its size defined, so it takes all the space it can get. So if you now scale the Grid, for example by factor 2, this means that the content of the Grid is scaled by factor 2, but the Grid will still take all the space it can get. If you would specify the width of the Grid to be 500, then scaling it by 2, would make the Grid's width 1000. But if you say: Grid, you can take all space that the parent gives you and then scale the Grid, it will be still the same. This means that scaling the auto-sized content of ScrollViewer will not show scrollbars.
In your sample this is true until the content of the Grid (the first column with width = 150 + width of the "Right" text in the second column) exceed the available size - at that point the DesiredSize of the Grid would be bigger than the size that ScrollViewer can provide and ScrollViewer will show scrollbars.
For example:
1) let's say that when you start your application, scale is set to 1 and ScrollViewer provides 500 points horizontally for the Grid. The Grid shows first column with 150 points width and shows "Right" text without any scale. The second column is set to fill the remaining space - so: 500 - 150 = 350 points is used by second column.
2) Now user sets the scale to 2. The Grid scales the first column to 300 points. This means that the second column can now take only 200 points. The Grid also scales the "Right" text, but the content (300 of first column + width of text) still does not exceed 500 point that are provided by the ScrollViewer.
3) User now sets scale to 3. Now the total width of the content of the grid exceed 500 points and this means that ScrollViewer will show scroll bars.
So having autosized controls, ScrollViewer and scaling does not work well.
But if you would fix the size of the Grid to 500, than you would get much more predictable results when scaling and using ScrollViewer. For example if you would scale by 10%, than the Grid's size would be 550 and would already exceed the size of the ScrollViewer - so ScrollViewer would show scrollbars. This would also give you the expected behaviour when you would increase the size of the window - the size of the Grid would remain the same and at some point the scrollbars would disappear (when the window would be big enough to show the whole content of the scaled Grid).
To conclude: my advice to you is to set the fixed size to the content of ScrollViewer controls. If you have a fixed size window, that you can set the Width and Height based on that size. Otherwise you can set it dynamically when the control is first loaded:
You can change the XAML for the content control into:
<ContentControl Name="ContentControl1"
ContentTemplate="{StaticResource ContentTemplate1}"
Margin="10"
Loaded="ContentControl1_OnLoaded" >
And also add the ContentControl1_OnLoaded handled that would just set the size to the initial size:
private void ContentControl1_OnLoaded(object sender, RoutedEventArgs e)
{
ContentControl1.Width = ContentControl1.ActualWidth;
ContentControl1.Height = ContentControl1.ActualHeight;
}
Zooming and panning may seem like a very simple task to do. But my experience shows (I am the author of ZoomPanel control) that this task can quickly become very complicated.

Related

How to adjust a canvas that child with position (-1,-1) is displayed completely

NEW INFORMATION!!!
Meanwhile i have found a solution but there is a new problem.
The solution is to set the margin of the canvas in code-behind to a new object of type Thickness with top and left 1 or 2.
But the canvas is lying on a tabcontrol.
When i switch between tabs or make a mousedown on the canvas the margin is lost.
I'm working with VS2015 on a WPF-application and have a very curious problem.
I got in one of my WPF windows a canvas as parent for some child elements.
One of these elements is a rectangle which shall show the user the size of a DIN-A4 page.
It is added in code-behind to the children collection of the canvas.
Normally i would place that rectangle at position (0,0).
But because of some problems i have to trick and set the position to (-1,-1) like that:
public static System.Windows.Shapes.Rectangle GetRectangle(double top, double left, double width, double height)
{
var rectangle = new System.Windows.Shapes.Rectangle();
System.Windows.Controls.Canvas.SetLeft(rectangle, -1);
System.Windows.Controls.Canvas.SetTop(rectangle, -1);
rectangle.Width = Math.Round(GetSizeInPoint(width)) + 2;
rectangle.Height = Math.Round(GetSizeInPoint(height));
rectangle.StrokeThickness = 1;
rectangle.Stroke = System.Windows.Media.Brushes.Red;
return rectangle;
}
But the result of it is that just a small part of the top and left border of the rectangle can be seen.
Do i have a chance to "move" the canvas so that the rectangle is displayed completely?
Hereby another important problematic point is that the Grid named "grdProtocolDesigner" can be serialized to XML and saved in the database.
So a complete restructuring would be a big problem.
Here the relevant part of my XAML including the canvas:
<ContentPresenter x:Name="protocolContainer"
Grid.Row="1"
Grid.Column="0">
<ContentPresenter.Content>
<Grid x:Name="grdProtocolDesigner"
Grid.Row="1"
Grid.Column="0">
<ScrollViewer>
<!--Important! The background of this panel has to be "Transparent" so that drag'n'drop-events can work.
Otherwise the events are not fired.-->
<Canvas x:Name="protocolDesignerPanel"
AllowDrop="True"
Visibility="{Binding DesignerPanelsVisibility}"
Width="2200" Height="4000"
MouseEnter="designerpanel_MouseEnter"
MouseLeave="designerpanel_MouseLeave"
MouseDown="designerpanel_MouseDown"
MouseMove="designerpanel_MouseMove"
MouseUp="designerPanel_MouseUp">
<Canvas.RenderTransform>
<ScaleTransform x:Name="scaleProtocolLayout"/>
</Canvas.RenderTransform>
<Canvas.Background>
<ImageBrush ImageSource="../../pictures/CanvasBackground.png"
TileMode="Tile"
Stretch="None"
Viewport="0, 0, 10, 10"
ViewportUnits="Absolute" />
</Canvas.Background>
</Canvas>
</ScrollViewer>
</Grid>
</ContentPresenter.Content>
</ContentPresenter>

Alternate between "Stretch.Uniform" and "Stretching.None" for ViewBox with ScrollViewer

I want to achieve a very well known behavior seen in the browser when you have an image to display that is larger then the monitor:
Originally, the image is displayed fitting inside the window area, and the mouse cursor is a magnifying glass with a "+" icon;
If you click, two things happen:
a. The image is displayed with its native pixel size;
b. Scroll bars appear;
I want this effect with a larger-than-screen UniformGrid. For that, I can use ViewBox. I have already got what I want putting the control inside a ViewBox with Stretch.Uniform property, and upon MouseLeftButtonDown event it toggles between Stretch.None and Stretch.Uniform, just like the large image in browser analogy, only without scroll bars.
Now if I add the ScrollViewer (ViewBox -> ScrollViewer -> UniformGrid), the effect doesn't work anymore, because the ScrollViewer always displays the (larger than window) MyUserControl with its native resolution, that is, clipped and with scroll bars activated, while I would like to alternate between this and a "fitting in ViewBox" version.
Here is how I get the resizing, but the ScrollViewer never displays:
<Viewbox x:Name="vbox" Stretch="None">
<ScrollViewer x:Name="scroll" VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Auto" >
<UniformGrid x:Name="ugrid" Columns="2" MouseLeftButtonDown="UniformGrid_MouseLeftButtonDown">
<local:AtlasMasculinoAnterior/>
<local:AtlasMasculinoPosterior/>
</UniformGrid>
</ScrollViewer>
</Viewbox>
And if change the order, then the Scroll bars always display and the zoom doesn't toggle upon mouse click (although the event fires):
<ScrollViewer x:Name="scroll" VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Auto" >
<Viewbox x:Name="vbox" Stretch="None">
<UniformGrid x:Name="ugrid" Columns="2" MouseLeftButtonDown="UniformGrid_MouseLeftButtonDown">
<local:AtlasMasculinoAnterior/>
<local:AtlasMasculinoPosterior/>
</UniformGrid>
</Viewbox>
</ScrollViewer>
And here the code behind event:
private void UniformGrid_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
if (vbox.Stretch == Stretch.None)
{
vbox.Stretch = Stretch.Uniform;
}
else
vbox.Stretch = Stretch.None;
}
So what am I doing wrong, or what should I do so that the intended behavior works?
The way I see it, I would like to alternate between having the control in a ViewBox (Stretch.Uniform) and having the control inside a ScrollViewer, but I wonder how to have the same effect with both elements being part of the layout tree (one inside another), or even if I should, move the UniformGrid in and out of containers I would manipulate programmatically in code behind.
Got it to work in sort of a hackish way, by having a Grid with both a ViewBox and a ScrollViewer, and putting the UniformGrid inside one of them in XAML. Then, in code-behind, I programmatically detach the UniformGrid from its present container, and attach it to the other (using a boolean flag to control where it is, but that is debatable):
<Grid x:Name="grid">
<ScrollViewer x:Name="scroll" HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto"/>
<Viewbox x:Name="viewbox" Stretch="Uniform">
<UniformGrid x:Name="ugrid" Columns="2" MouseLeftButtonDown="UniformGrid_MouseLeftButtonDown">
<local:AtlasMasculinoAnterior/>
<local:AtlasMasculinoPosterior/>
</UniformGrid>
</Viewbox>
</Grid>
and
bool atlasfullscreen = false;
private void UniformGrid_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
UniformGrid ug = sender as UniformGrid;
if (atlasfullscreen)
{
scroll.Content = null;
viewbox.Child = ug;
atlasfullscreen = false;
}
else
{
viewbox.Child = null;
scroll.Content = ug;
atlasfullscreen = true;
}
}
I had a similar use case where I had an item that I needed to alternate between Stretch.None and Stretch.Uniform, and when Stretch.None, I needed the scrollbars to be visible.
What I finally figured out was that when I set Stretch.None, I needed to set the ScrollViewer's Width & Height to the ViewBox's parent ActualWidth / Height, and when Stretch.Uniform, I needed to clear the ScollViewer's width and height.
So using your original XAML, plus the new Grid, here's the new XAML:
<Grid x:Name="grid">
<Viewbox x:Name="vbox"
Stretch="Uniform">
<ScrollViewer x:Name="scroll"
HorizontalScrollBarVisibility="Auto"
VerticalScrollBarVisibility="Auto">
<UniformGrid x:Name="ugrid"
Columns="2"
MouseLeftButtonDown="UniformGrid_MouseLeftButtonDown">
<local:AtlasMasculinoAnterior />
<local:AtlasMasculinoPosterior />
</UniformGrid>
</ScrollViewer>
</Viewbox>
</Grid>
New code behind:
private void UniformGrid_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
if (vbox.Stretch == Stretch.None)
{
vbox.Stretch = Stretch.Uniform;
scroll.Width = double.NaN;
scroll.Height = double.NaN;
}
else
{
vbox.Stretch = Stretch.None;
scroll.Width = grid.ActualWidth;
scroll.Height = grid.ActualHeight;
}
}
You might need to tweak the above example for how the Viewbox now being in a grid - but for my use case with similar XAML / code I got mine working without having to constantly move the child from the Viewbox to another control and back again.
So in summary: when Viewbox.Stretch = Uniform, set scrollviewer's width / height to double.NaN, and when Viewbox.Stretch = None, set scrollviewer's width / height to Viewbox.Parent.ActualWidth / Height.

How do I configure a TextBox control to automatically resize itself vertically when text no longer fits on one line?

How do I configure a TextBox control to automatically resize itself vertically when text no longer fits on one line?
For example, in the following XAML:
<DockPanel LastChildFill="True" Margin="0,0,0,0">
<Border Name="dataGridHeader"
DataContext="{Binding Descriptor.Filter}"
DockPanel.Dock="Top"
BorderThickness="1"
Style="{StaticResource ChamelionBorder}">
<Border
Padding="5"
BorderThickness="1,1,0,0"
BorderBrush="{DynamicResource {ComponentResourceKey TypeInTargetAssembly=dc:NavigationPane,
ResourceId={x:Static dc:NavigationPaneColors.NavPaneTitleBorder}}}">
<StackPanel Orientation="Horizontal">
<TextBlock
Name="DataGridTitle"
FontSize="14"
FontWeight="Bold"
Foreground="{DynamicResource {ComponentResourceKey
TypeInTargetAssembly=dc:NavigationPane,
ResourceId={x:Static dc:NavigationPaneColors.NavPaneTitleForeground}}}"/>
<StackPanel Margin="5,0" Orientation="Horizontal"
Visibility="{Binding IsFilterEnabled, FallbackValue=Collapsed, Mode=OneWay, Converter={StaticResource BooleanToVisibility}}"
IsEnabled="{Binding IsFilterEnabled, FallbackValue=false}" >
<TextBlock />
<TextBox
Name="VerticallyExpandMe"
Padding="0, 0, 0, 0"
Margin="10,2,10,-1"
AcceptsReturn="True"
VerticalAlignment="Center"
Text="{Binding QueryString}"
Foreground="{DynamicResource {ComponentResourceKey
TypeInTargetAssembly=dc:NavigationPane,
ResourceId={x:Static dc:NavigationPaneColors.NavPaneTitleForeground}}}">
</TextBox>
</StackPanel>
</StackPanel>
</Border>
</Border>
</DockPanel>
The TextBox control named "VerticallyExpandMe" needs to automatically expand vertically when the text bound to it does not fit on one line. With AcceptsReturn set to true, TextBox expands vertically if I press enter within it, but I want it do do this automatically.
Although Andre Luus's suggestion is basically correct, it won't actually work here, because your layout will defeat text wrapping. I'll explain why.
Fundamentally, the problem is this: text wrapping only does anything when an element's width is constrained, but your TextBox has unconstrained width because it's a descendant of a horizontal StackPanel. (Well, two horizontal stack panels. Possibly more, depending on the context from which you took your example.) Since the width is unconstrained, the TextBox has no idea when it is supposed to start wrapping, and so it will never wrap, even if you enable wrapping. You need to do two things: constrain its width and enable wrapping.
Here's a more detailed explanation.
Your example contains a lot of detail irrelevant to the problem. Here's a version I've trimmed down somewhat to make it easier to explain what's wrong:
<StackPanel Orientation="Horizontal">
<TextBlock Name="DataGridTitle" />
<StackPanel
Margin="5,0"
Orientation="Horizontal"
>
<TextBlock />
<TextBox
Name="VerticallyExpandMe"
Margin="10,2,10,-1"
AcceptsReturn="True"
VerticalAlignment="Center"
Text="{Binding QueryString}"
>
</TextBox>
</StackPanel>
</StackPanel>
So I've removed your containing DockPanel and the two nested Border elements inside of that, because they're neither part of the problem nor relevant to the solution. So I'm starting at the pair of nested StackPanel elements in your example. And I've also removed most of the attributes because most of them are also not relevant to the layout.
This looks a bit weird - having two nested horizontal stack panels like this looks redundant, but it does actually make sense in your original if you need to make the nested one visible or invisible at runtime. But it makes it easier to see the problem.
(The empty TextBlock tag is also weird, but that's exactly as it appears in your original. That doesn't appear to be doing anything useful.)
And here's the problem: your TextBox is inside some horizontal StackPanel elements, meaning its width is unconstrained - you have inadvertently told the text box that it is free to grow to any width, regardless of how much space is actually available.
A StackPanel will always perform layout that is unconstrained in the direction of stacking. So when it comes to lay out that TextBox, it'll pass in a horizontal size of double.PositiveInfinity to the TextBox. So the TextBox will always think it has more space than it needs. Moreover, when a child of a StackPanel asks for more space than is actually available, the StackPanel lies, and pretends to give it that much space, but then crops it.
(This is the price you pay for the extreme simplicity of StackPanel - it's simple to the point of being bone-headed, because it will happily construct layouts that don't actually fit. You should only use StackPanel if either you really do have unlimited space because you're inside a ScrollViewer, or you are certain that you have sufficiently few items that you're not going to run out of space, or if you don't care about items running off the end of the panel when they get too large and you don't want the layout system to try to do anything more clever than simply cropping the content.)
So turning on text wrapping won't help here, because the StackPanel will always pretend that there's more than enough space for the text.
You need a different layout structure. Stack panels are the wrong thing to use because they will not impose the layout constraint you need to get text wrapping to kick in.
Here's a simple example that does roughly what you want:
<Grid VerticalAlignment="Top">
<DockPanel>
<TextBlock
x:Name="DataGridTitle"
VerticalAlignment="Top"
DockPanel.Dock="Left"
/>
<TextBox
Name="VerticallyExpandMe"
AcceptsReturn="True"
TextWrapping="Wrap"
Text="{Binding QueryString}"
>
</TextBox>
</DockPanel>
</Grid>
If you create a brand new WPF application and paste that in as the content of the main window, you should find it does what you want - the TextBox starts out one line tall, fills the available width, and if you type text in, it'll grow one line at a time as you add more text.
Of course, layout behaviour is always sensitive to context, so it may not be enough to just throw that into the middle of your existing application. That will work if pasted into a fixed-size space (e.g. as the body of a window), but will not work correctly if you paste it into a context where width is unconstrained. (E.g., inside a ScrollViewer, or inside a horizontal StackPanel.)
So if this doesn't work for you, it'll be because of other things wrong elsewhere in your layout - possibly yet more StackPanel elements elsewhere. From the look of your example, it's probably worth spending some time thinking about what you really need in your layout and simplifying it - the presence of negative margins, and elements that don't appear to do anything like that empty TextBlock are usually indicative of an over-complicated layout. And unnecessary complexity in a layout makes it much hard to achieve the effects you're looking for.
Alternatively, you could constrain your TextBlock's Width by binding it to a parent's ActualWidth, for example:
<TextBlock Width="{Binding ElementName=*ParentElement*, Path=ActualWidth}"
Height="Auto" />
This will force it to resize its height automatically too.
Use MaxWidth and TextWrapping="WrapWithOverflow".
I'm using another simple approach that allows me not to change the document layout.
The main idea is not to set the control Width before it starts changing. For TextBoxes, I handle the SizeChanged event:
<TextBox TextWrapping="Wrap" SizeChanged="TextBox_SizeChanged" />
private void TextBox_SizeChanged(object sender, SizeChangedEventArgs e)
{
FrameworkElement box = (FrameworkElement)sender;
if (e.PreviousSize.Width == 0 || box.Width < e.PreviousSize.Width)
return;
box.Width = e.PreviousSize.Width;
}
You can use this class which extends TextBlock. It does auto-shrinking and takes MaxHeight / MaxWidth into consideration:
public class TextBlockAutoShrink : TextBlock
{
private double _defaultMargin = 6;
private Typeface _typeface;
static TextBlockAutoShrink()
{
TextBlock.TextProperty.OverrideMetadata(typeof(TextBlockAutoShrink), new FrameworkPropertyMetadata(new PropertyChangedCallback(TextPropertyChanged)));
}
public TextBlockAutoShrink() : base()
{
_typeface = new Typeface(this.FontFamily, this.FontStyle, this.FontWeight, this.FontStretch, this.FontFamily);
base.DataContextChanged += new DependencyPropertyChangedEventHandler(TextBlockAutoShrink_DataContextChanged);
}
private static void TextPropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args)
{
var t = sender as TextBlockAutoShrink;
if (t != null)
{
t.FitSize();
}
}
void TextBlockAutoShrink_DataContextChanged(object sender, DependencyPropertyChangedEventArgs e)
{
FitSize();
}
protected override void OnRenderSizeChanged(SizeChangedInfo sizeInfo)
{
FitSize();
base.OnRenderSizeChanged(sizeInfo);
}
private void FitSize()
{
FrameworkElement parent = this.Parent as FrameworkElement;
if (parent != null)
{
var targetWidthSize = this.FontSize;
var targetHeightSize = this.FontSize;
var maxWidth = double.IsInfinity(this.MaxWidth) ? parent.ActualWidth : this.MaxWidth;
var maxHeight = double.IsInfinity(this.MaxHeight) ? parent.ActualHeight : this.MaxHeight;
if (this.ActualWidth > maxWidth)
{
targetWidthSize = (double)(this.FontSize * (maxWidth / (this.ActualWidth + _defaultMargin)));
}
if (this.ActualHeight > maxHeight)
{
var ratio = maxHeight / (this.ActualHeight);
// Normalize due to Height miscalculation. We do it step by step repeatedly until the requested height is reached. Once the fontsize is changed, this event is re-raised
// And the ActualHeight is lowered a bit more until it doesnt enter the enclosing If block.
ratio = (1 - ratio > 0.04) ? Math.Sqrt(ratio) : ratio;
targetHeightSize = (double)(this.FontSize * ratio);
}
this.FontSize = Math.Min(targetWidthSize, targetHeightSize);
}
}
}

How to toggle a WPF Grid column visibility

I'm having some trouble getting this to work in a WPF app I'm working on. Basically, what I'm after is something like the Task pane in an MMC:
The app has three columns in the main part of the display. I need a column on the right side which is resizable. I presume this means using a Grid with a GridSplitter but anything that works will do.
I want to be able to save the width of the right-side column when the app is closed and load it when the app is opened but this should be an initial size: the user should be able to resize it.
When I resize the window, I want the left- and right-side columns to stay the same size and the middle column to resize with the window width.
The left- and right-side columns need to have a minimum width. When I resize the right-side column I want the centre column to get smaller but not the left-side column.
I also want to be able to toggle the visibility of the right-side column with a toggle button which is outside the column and when it returns to visibility I want it to be the same width it was before.
I'm trying to do as much as possible in XAML and with binding.
And can I have it topped with cream, ice cream and chocolate chips, please? :-)
As I read your requirements, instead of thinking of a Grid, I think of a DockPanel.
<DockPanel>
<Grid Name="right"
DockPanel.Dock="Right" MinWidth="100" />
<Grid Name="Left"
DockPanel.Dock="Left" MinWidth="100" />
<Grid Name="middle" />
</DockPanel>
If you make a way to resize right, then middle will change as right is resized. If you resize the window, only middle will change. Storing and setting the Width of right is up to you, but shouldn't be hard.
As for allowing the user to resize right, that will a bit trickier, but I found this article that should help. This other article might help even more.
For the visibility of right, you can set its Visibility to Collapsed to hide it and restore it by setting it to Visible.
Note: The panels inside don't have to be Grids, but you will want to use some sort of Panel for each. Whatever you have inside your current Grid columns should work just fine.
I used a Grid with GridSplitters since this made it really easy to resize the middle column while maintaining the widths of the left and right columns.
XAML:
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="MainWindow"
Title="Main Window"
Width="640" Height="480">
<Grid>
<Grid.ColumnDefinitions>
<!-- Left column -->
<ColumnDefinition Width="200" MinWidth="100"/>
<!-- Left GridSplitter column -->
<ColumnDefinition Width="5"/>
<!-- Center column. A width of * means the column will fill
any remaining space. -->
<ColumnDefinition Width="*"/>
<!-- Right GridSplitter column -->
<ColumnDefinition x:Name="RightSplitterColumn" Width="5"/>
<!-- Right column -->
<ColumnDefinition x:Name="RightColumn" Width="200"
MinWidth="100"/>
</Grid.ColumnDefinitions>
<GridSplitter Grid.Column="1" HorizontalAlignment="Stretch" />
<GridSplitter Grid.Column="3" HorizontalAlignment="Stretch" />
<Button x:Name="ToggleButton" Grid.Column="2"
Content="Toggle Right Column" Width="150" Height="25"
Click="ToggleButton_Click" />
</Grid>
</Window>
Code-Behind
When hiding the right column, I just set the column width to 0 since grid columns don't have a visibility property.
public partial class MainWindow : Window
{
private double rightColumnWidth;
private double rightColumnMinWidth;
private bool rightColumnHidden;
public MainWindow()
{
this.InitializeComponent();
}
private void ToggleButton_Click(object sender, RoutedEventArgs e)
{
if (rightColumnHidden)
{
// Restore the widths.
RightColumn.MinWidth = rightColumnMinWidth;
RightColumn.Width = new GridLength(rightColumnWidth);
RightSplitterColumn.Width = new GridLength(5);
}
else
{
// Remember the user-set widths for the columns.
rightColumnWidth = RightColumn.Width.Value;
rightColumnMinWidth = RightColumn.MinWidth;
// Remember to set the minimum width to 0 before changing the actual
// width.
RightColumn.MinWidth = 0;
RightColumn.Width = new GridLength(0);
RightSplitterColumn.Width = new GridLength(0);
}
rightColumnHidden = !rightColumnHidden;
}
}
As for saving and restoring the column widths on startup, I would just store the width variables to a settings file and then apply them when your app is reopened.
Set the columndefinition Width to Auto and put a control inside that column and give Star for the other columns . Whenever you want to hide the column with content, set the control.Visibility=Collapsed and since column width is Auto, you wont see that column and the remaining columns will take the space.
3 years later you can find another approach on CodeProject.
http://www.codeproject.com/Articles/437237/WPF-Grid-Column-and-Row-Hiding
It adds a "Visible" property to custom Column definitions.

Reset Expander to default collapse behavior

I'm using an expander inside a Resizer (a ContentControl with a resize gripper), and it expands/collapses properly when the control initially comes up. Once I resize it, the Expander won't properly collapse, as documented below. I ran Snoop on my application, and I don't see any heights set on Expander or its constituents.
How would I go about convincing Expander to collapse properly again? Or modifying Resizer to not make Expander sad would work as well.
Expander documentation says:
"For an Expander to work correctly, do not specify a Height on the Expander control when the ExpandDirection property is set to Down or Up. Similarly, do not specify a Width on the Expander control when the ExpandDirection property is set to Left or Right. When you set a size on the Expander control in the direction that the expanded content is displayed, the area that is defined by the size parameter is displayed with a border around it. This area displays even when the window is collapsed. To set the size of the expanded window, set size dimensions on the content of the Expander control or the ScrollViewer that encloses the content."
I resolved the problem by moving the Resizer inside the Expander, but I've run into the Expander issue elsewhere, so would still like an answer if someone has it.
thanks
I haven't had a chance to mock up this particular issue since then, but I recently discovered that setting Height or Width to Double.NaN resets it to its default free-spirited behavior.
Ironically, this was from reading the code of the Resizer control I was using in the first place.
Answering this a bit late (2+ years), but, hey, better late than never, right?
Anyway, I ran into this exact problem and was able to solve it with some code-behind to save and reset column widths.
I have a 3 columned Grid, with some content in the first column, the GridSplitter in the second column, and the Expander in the third column. It looks like what is happening is that after the GridSplitter is moved the width of the column containing the Expander is altered from Auto to a fixed size. This causes the Expander to no longer collapse as expected.
So, I added a private variable and two event handlers:
private GridLength _columnWidth;
private void Expander_Expanded (object sender, RoutedEventArgs e)
{
// restore column fixed size saved in Collapse event
Column2.Width = _columnWidth;
}
private void Expander_Collapsed (object sender, RoutedEventArgs e)
{
// save current column width so we can restore when expander is expanded
_columnWidth = Column2.Width;
// reset column width to auto so the expander will collapse properly
Column2.Width = GridLength.Auto;
}
When the Expander is collapsed I save Column2's fixed width (which was altered from Auto auto-magically in the background somewhere) then reset the width to Auto.
Then, when the expander is expanded, I restore the column back to the fixed width so it expands to the same width it was before it was collapsed.
Here's the XAML for reference:
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="2*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition x:Name="Column2" Width="Auto" />
</Grid.ColumnDefinitions>
<ScrollViewer Grid.Column="0" VerticalScrollBarVisibility="Auto">
<!-- some content goes here -->
</ScrollViewer>
<GridSplitter HorizontalAlignment="Right" VerticalAlignment="Stretch"
Grid.Column="1" ResizeBehavior="PreviousAndNext" Width="5"
Background="Black" />
<Expander Grid.Column="2" ExpandDirection="Left"
IsExpanded="True" Style="{StaticResource LeftExpander}"
Expanded="Expander_Expanded" Collapsed="Expander_Collapsed">
<Grid>
<TextBox TextWrapping="Wrap" Height="Auto" Margin="0 5 5 5" />
</Grid>
</Expander>
</Grid>

Resources