Making a WPF TextBlock be only grow and not shrink? - c#

In my app I set the Text of the TextBlock named tbkStatus many times.
How can I make the TextBlock be just grow auto to fit the text but not shrink when the text changed?
The StatusText changes every few seconds, There are statuses with long text and short text.
I want my TextBlock to fit itself to the size of the longest text that was, and even when there is a short text the TextBlock should not shrink
<Window x:Class="WpfApp1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Height="200" Width="400"
WindowStartupLocation="CenterScreen"
ResizeMode="CanMinimize" Topmost="True">
<Window.Resources>
</Window.Resources>
<Grid >
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="AUTO" />
<RowDefinition Height="AUTO" />
<RowDefinition Height="AUTO" />
<RowDefinition Height="AUTO" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<TextBlock Text="Please wait ..." Grid.Row="1" Margin="6"/>
<TextBlock Name="tbkStatus" Grid.Row="2" Margin="6" TextWrapping="Wrap" Text="{Binding StatusText}"/>
<ProgressBar Grid.Row="3" Margin="6" Height="20"/>
<Button Grid.Row="4" HorizontalAlignment="Center" Padding="24,3" Margin="6" Content="Stop"/>
</Grid>
</Window>

Xaml only solution:
<TextBlock Text="{Binding StatusText}"
MinWidth="{Binding RelativeSource={RelativeSource Self}, Path=ActualWidth}"
HorizontalAlignment="Left"/>
This way whenever the Width growths, MinWidth growths too. Hence, the control can't shrink back.

I guess you could just listen to Layout Events like SizeChanged or LayoutUpdated or write some sort of behavior
In the below example, the basic premise is to listen to either of these events, and force your control to never shrink
Note this is totally untested and was just an idea, maybe you could set the MinWidth Property instead
Xaml
<TextBlock x:Name="tbkStatus" SizeChanged="OnSizeChanged"/>
Code Behind
private double _lastSize;
private void OnSizeChanged(object sender, SizeChangedEventArgs e)
{
var textBlock = sender as TextBlock;
if (textBlock == null)
{
return;
}
if (e.WidthChanged)
{
if (textBlock.Width < _lastSize)
{
textBlock.Width = _lastSize;
}
_lastSize = textBlock.Width;
}
}
Also Note
The SizeChangedEventArgs Class has many properties that you might be able to take advantage of

You can do something like this:
<TextBlock Name="tbkStatus" Grid.Row="2" Margin="6" TextWrapping="Wrap" Text="{Binding StatusText}"/>
Since TextBlock doesn't have TextChange event this will do that job
DependencyPropertyDescriptor dp = DependencyPropertyDescriptor.FromProperty(TextBlock.TextProperty, typeof(TextBlock));
int textLength =0
dp.AddValueChanged(tbkStatus, (object a, EventArgs b) =>
{
if (textLength < tbkStatus.Text.Length)
{
textLength = tkbStatus.Text.Length;
tbkStatus.Width = textLength * SomeValue; //You have to play around and see what value suits you best since it depends on font and it's size
}
});
Alternatively, you can use a TextBox and make it read only and use the TextChanged event.

Related

How to place any UI element at the end of text that is trimmed

I have a custom control that contains TextBlock and Ellipse.
I want to have this Ellipse placed in a position where the TextBlock ends. If there is lack of space text should be trimmed and Ellipse should remain at its position. I have created very simple sample to illustrate the problem.
MainPage.xaml.cs:
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}" x:Name="MainGrid" HorizontalAlignment="Stretch">
<Grid Height="50">
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<local:TextWithEllipse x:Name="Control1" Grid.Row="0"/>
<local:TextWithEllipse x:Name="Control2" Grid.Row="1"/>
</Grid>
</Grid>
MainPage.cs:
public sealed partial class MainPage : Page
{
public MainPage()
{
this.InitializeComponent();
SizeChanged += OnSizeChanged;
Loaded += OnLoaded;
}
private void OnLoaded(object sender, RoutedEventArgs routedEventArgs)
{
Control1.Text = "One two three four five six seven eight nine ten One two three four five six seven eight nine ten";
Control2.Text = "Short text";
}
private void OnSizeChanged(object sender, SizeChangedEventArgs e)
{
Control1.Width = e.NewSize.Width;
Control2.Width = e.NewSize.Width;
}
}
what is done: the TextWithEllipse controls are getting the same width as the app window has and some texts are written to them - short and long one.
Here is how this control looks like:
TextWithEllipse.xaml.cs:
<UserControl
x:Class="TextResize.TextWithEllipse"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
d:DesignHeight="300"
d:DesignWidth="400">
<Grid Height="20" HorizontalAlignment="Stretch" Background="MediumPurple">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="20" MinWidth="20"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" HorizontalAlignment="Stretch" x:Name="TestTitle"
TextTrimming="CharacterEllipsis" FontSize="16"/>
<Ellipse Width="20" Height="20" Fill="Red" Grid.Column="1" HorizontalAlignment="Left"/>
</Grid>
TextWithEllipse.cs:
public sealed partial class TextWithEllipse : UserControl
{
public TextWithEllipse()
{
InitializeComponent();
}
public string Text
{
get { return TestTitle.Text; }
set { TestTitle.Text = value; }
}
}
It works OK if the text width is less than controls width. If the text is long and takes more space it is going outside the window, Ellipse is not shown unless you the windows width is extended.
When the first Column width is changed to '*':
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="20" MinWidth="20"/>
the situation is than while the text takes more width than window, the text is being trimmed and Ellipse is in the right place. But if the text is short, the Ellipse will not be placed right after the text, but at the end of window.
How can I achieve that?
I played around with your code a bit to find a solution. As you found out setting ColumnDefinition Width="Auto" won't work because it tells the TextBlock that it has as much space as it needs, so it will never trim. So setting ColumnDefinition Width="*" is the right way to go, but as a little detail change the HorizontalAlignment property of the whole Grid to Left, because then the whole Grid will only stretch if it is required (which will happen when you have a lot of text). The whole code would be:
<Grid Height="20" HorizontalAlignment="Left" Background="MediumPurple">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="20" MinWidth="20"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" HorizontalAlignment="Stretch" x:Name="TestTitle"
TextTrimming="CharacterEllipsis" FontSize="16"/>
<Ellipse Width="20" Height="20" Fill="Red" Grid.Column="1" HorizontalAlignment="Left"/>
</Grid>
This at least in the XAML Designer gives me the behavior you would like to have.
You may try to use RelativePanel:
<RelativePanel HorizontalAlignment="Stretch" Height="20">
<TextBlock HorizontalAlignment="Stretch" x:Name="TestTitle" TextTrimming="CharacterEllipsis" FontSize="16" Padding="0,0,20,0"/>
<Ellipse Width="20" Height="20" Fill="Red" RelativePanel.AlignRightWith="TestTitle"/>
</RelativePanel>
You also don't need to change width in OnSizeChanged - it will change automatically even without this event.

Autosize to only some controls

I'm working on a WPF GUI, and I want the window to auto-size to the content, but not to everything: I have some various buttons & other controls, and I want to autosize the width to that. If I add a long item to the list box, I want the window to stay the same size.
Example code:
<Window x:Class="QuickieWPF.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" SizeToContent="WidthAndHeight" ResizeMode="CanMinimize">
<Grid>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<StackPanel Orientation="Horizontal">
<Button Width="100" Content="Foo" Click="Button_Click"/>
<Button Width="100" Content="Bar" Click="Button_Click"/>
<Button Width="100" Content="Baz" Click="Button_Click"/>
</StackPanel>
<ListBox Grid.Row="1" Name="TheList" Height="100"/>
</Grid>
</Window>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private void Button_Click(object sender, RoutedEventArgs e)
{
string str = "This is a really long text item that will be wider than the " +
"three buttons. I want a horizontal scrollbar on the listbox, not a wider " +
"window. I want the window to size to the buttons, not to this listbox.";
this.TheList.Items.Add(str);
}
}
Initially, the window is sized to the buttons:
After adding a long item to the list box, I currently get this:
But I'd rather get this:
(I did that last screenshot by setting MaxWidth on the list box, but that isn't a good solution for the full application: In the full application, it's more than just three buttons; it's buttons, icons, textboxes, labels, etc, and I want the window to autosize to the whole mess, but not to the listbox at the bottom.)
You can bind width of content you do not want to autosize to the actual width of content you do, for example:
<Grid>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<StackPanel Orientation="Horizontal" x:Name="panel">
<Button Width="100"
Content="Foo"
Click="Button_Click" />
<Button Width="100"
Content="Bar"
Click="Button_Click" />
<Button Width="100"
Content="Baz"
Click="Button_Click" />
</StackPanel>
<ListBox Grid.Row="1"
Name="TheList"
Height="100" Width="{Binding ElementName=panel, Path=ActualWidth}" />
</Grid>
Here I bound width of ListBox to the actual width of panel with buttons, which in this case achieves what you want.
You have to limit the width of the ListBox, else the parent window would just resize with whichever the control that take the most space due to SizeToContent="WidthAndHeight".
<StackPanel Orientation="Horizontal">
<Button Width="100" Content="Foo" Click="Button_Click"/>
<Button Width="100" Content="Bar" Click="Button_Click"/>
<Button Width="100" Content="Baz" Click="Button_Click"/>
</StackPanel>
<!-- set the width to 300 -->
<ListBox Grid.Row="1" Name="TheList" Height="100" Width="300" />
Give your stackPanel a name, e.g.
<StackPanel Name="MyPanel" Orientation="Horizontal">
Then in BOTH your list box and window properties add this;
Width="{Binding ElementName=MyPanel, Path=ActualWidth}"
You will need to include horizontal scrolling in your listbox.

WPF shrink other conent when Expander is Expanded

Whenever Expander is expanded I would like to shrink grid cell above containing ListBox, so that you can always access every ListItem(if the listbox's grid cell would not shrink, lowest part would be inacessible). To illustrate:
item item *scrollbar*
item -> item *scrollbar*
item expanderItems
expander expander
I found bunch of threads for resizable expander, but none mentioning resizing other content. The problem is, grid containing listbox in 1st row and expander in 2nd with 2nd row Height set to Auto do not resize itself when expander is expanded.
<Grid.RowDefinitions>
<RowDefinition Height="1*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
I managed to code an ugly workaround using Expanded/Collapsed events:
private void Expander_Expanded(object sender, System.Windows.RoutedEventArgs e)
{
//grid1.Height is named content of expander
this.LayoutRoot.RowDefinitions[1].Height = new GridLength(this.LayoutRoot.RowDefinitions[1].ActualHeight + grid1.Height, GridUnitType.Pixel);
this.LayoutRoot.RowDefinitions[0].Height = new GridLength(1, GridUnitType.Star);
}
What would be the proper way? Preferably with no code behind and more "automated".
EDIT: xaml
<Grid x:Name="LayoutRoot">
<Grid.RowDefinitions>
<RowDefinition Height="1*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Expander Header="Expander" Margin="0" Grid.Row="1" VerticalAlignment="Bottom" Height="23" Panel.ZIndex="1" ExpandDirection="Up" Grid.RowSpan="2" Expanded="Expander_Expanded" Collapsed="Expander_Collapsed">
<Grid x:Name="grid1" Background="#FF762CF7" Height="100" Margin="0,-100,0,0"/>
</Expander>
<ListBox Margin="0" Background="#19FFFFFF">
<Button Height="150" Width="100"/>
<Button Height="150" Width="100"/>
<Button Height="150" Width="100"/>
</ListBox>
<Grid Margin="0" Grid.Row="1" Background="#FFAEFFAE"/>
<Grid Margin="0" Background="#FFFFD8D8"/>
</Grid>
The main problem you have is setting the Height property of the Expander. It changes its height when you expand it. But if you are setting it, it has to respect the height you gave it and cannot properly expand.
Instead of setting the Height, I would set the MinHeight (or completely remove height constraints, depending on what you want to do).
Additionally you should remove the Margin of the grid inside the expander.

ScrollViewer is visible?

I'm using ComputedHorizontalScrollBarVisibility but this doesn't work when you set the HorizontalScrollBarVisibility to "Hidden".
What I'm trying to achieve is knowing if a ScrollViewer should be visible but without showing the ScrollViewer. Then bind that result to show the buttons that control the ScrollViewer (in this case the `StackPanel below).
XAML
<ScrollViewer HorizontalScrollBarVisibility="Auto" x:Name="Scroll">
.....
</ScrollViewer>
<StackPanel Visibility="{Binding ElementName=Scroll, Path=ComputedHorizontalScrollBarVisibility}">
<Button Grid.Column="0" Content="Left" HorizontalAlignment="Left" Click="..."/>
<Button Grid.Column="1" Content="Right" HorizontalAlignment="Right" Click="..."/>
</StackPanel >
If you need to control the way the ScrollViewer (or any control, really) is laid out, consider using a ControlTemplate, which is accesible in any Control's Template property. as this will allow you to bind to the object itself and values passed in to it, and provide such templating. This may, however, involve needing to deal with the computations to show the exact part of your control which is visible.
You can get what you want by simple adding up the width of content elements inside ScrollViewer, e.g. if you have a StackPanel (with Orientation=Horizontal) inside ScrollViewer then add up the width of each child element in the StackPanel and compare it with ActualWidth of ScrollViewer. if the sum is less greater than the ActualWidth of ScrollViewer then you need to scroll it.
For more details refer this link
In my experience, the scroll viewer property values can be stale until the next layout pass. It's code-behind in my simple example below but this does work the way you want.
I create a dependency property called "ShowScrollButtons". You can probably watch for extent and viewport size changes and automatically recompute the property.
When the scroll content size changes, I trigger a re-evaluate of ShowScrollButtons. Note the call to UpdateLayout to make sure the extent and viewport sizes are up-to-date. Again, it's a sample so I'm only checking the Width here for left/right scroll buttons
private void UpdateScrollButtonVis()
{
UpdateLayout();
ShowScrollButtons = (Scroll.ExtentHeight > Scroll.ViewportWidth);
}
In XAML...
<Window.Resources>
<BooleanToVisibilityConverter x:Key="boolvis"/>
</Window.Resources>
<Grid x:Name="theGrid">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"></RowDefinition>
<RowDefinition Height="*"></RowDefinition>
<RowDefinition Height="Auto"></RowDefinition>
</Grid.RowDefinitions>
<ScrollViewer Grid.Row="0" Width="100" Height="100" VerticalScrollBarVisibility="Hidden" HorizontalScrollBarVisibility="Hidden" x:Name="Scroll">
<Canvas x:Name="theCanvas" Width="300" Height="300" Background="Green"/>
</ScrollViewer>
<StackPanel Grid.Row="1" Visibility="{Binding ShowScrollButtons,Converter={StaticResource boolvis}}">
<Button Grid.Column="0" Content="Left" HorizontalAlignment="Left" />
<Button Grid.Column="1" Content="Right" HorizontalAlignment="Right" />
</StackPanel >
<Button x:Name="toggle" Grid.Row="2" Height="25" Width="100" Click="toggle_Click">Toggle</Button>
</Grid>
Update:
How about a new approach works with multiple scroll viewers and StackPanels without code-behind.
Use an Attached Property to control the external button visibility:
public class ScrollViewWatcher
{
public static readonly DependencyProperty HorizontalButtonVisibility = DependencyProperty.RegisterAttached(
"HorizontalButtonVisibility",
typeof(Visibility),
typeof(ScrollViewWatcher),
new FrameworkPropertyMetadata(Visibility.Visible,
FrameworkPropertyMetadataOptions.AffectsRender | FrameworkPropertyMetadataOptions.AffectsArrange | FrameworkPropertyMetadataOptions.AffectsMeasure)
);
public static Visibility GetHorizontalButtonVisiblity(UIElement element)
{
return (Visibility)element.GetValue(HorizontalButtonVisibility);
}
public static void SetHorizontalButtonVisibility(UIElement element, Visibility value)
{
element.SetValue(HorizontalButtonVisibility, value);
ScrollViewer sv = element as ScrollViewer;
if (sv != null)
{
sv.ScrollChanged -= sv_ScrollChanged;
sv.ScrollChanged += sv_ScrollChanged;
}
}
static void sv_ScrollChanged(object sender, ScrollChangedEventArgs e)
{
var sv = sender as ScrollViewer;
if (sv != null)
{
var vis = sv.ExtentHeight > sv.ViewportWidth ? Visibility.Visible : Visibility.Hidden;
sv.SetValue(HorizontalButtonVisibility, vis);
}
}
}
Then in XAML, bind to the appropriate ScrollViewer like this:
<ScrollViewer
x:Name="sv1" local:ScrollViewWatcher.HorizontalButtonVisibility="Visible"
Grid.Row="0" Width="100" Height="100" VerticalScrollBarVisibility="Hidden" HorizontalScrollBarVisibility="Hidden" >
<Canvas x:Name="theCanvas" Width="300" Height="300" Background="Green"/>
</ScrollViewer>
<StackPanel Grid.Row="1" Visibility="{Binding ElementName=sv1,Path=(local:ScrollViewWatcher.HorizontalButtonVisibility), Mode=OneWay}">
<Button Grid.Column="0" Content="Left" HorizontalAlignment="Left" />
<Button Grid.Column="1" Content="Right" HorizontalAlignment="Right" />
</StackPanel >
This works great in my test. This was a fun challenge. Maybe someone can enlighten us with a better approach but I'm pretty happy with this.
Thank you all for the answers but finally got a workaround a bit easier, instead of binding the visibility from the StackPanel of the buttons just call a ScrollChanged in the ScrollViewer and then in code check for the ComputedHorizontalScrollBarVisibility and change visibilities depending on the result.
XAML
<ScrollViewer HorizontalScrollBarVisibility="Auto" x:Name="Scroll" ScrollChanged="Scroll_ScrollChanged">
.....
</ScrollViewer>
<StackPanel x:Name="BPanel" Visibility="Hidden">
<Button Grid.Column="0" Content="Left" HorizontalAlignment="Left" Click="..."/>
<Button Grid.Column="1" Content="Right" HorizontalAlignment="Right" Click="..."/>
</StackPanel >
C#
private void Scroll_ScrollChanged(object sender, ScrollChangedEventArgs e)
{
ScrollViewer scroll = (ScrollViewer)sender;
if(scroll.HorizontalScrollBarVisibility == ScrollBarVisibility.Auto)
{
if (scroll.ComputedHorizontalScrollBarVisibility == Visibility.Visible)
{
scroll.HorizontalScrollBarVisibility = ScrollBarVisibility.Hidden;
BPanel.Visibility = Visibility.Visible;
}
}
}

Custom ListView Control

So, I have been looking for solution more than 12 hours(but without success). How should I change ListView ControlTemplate to get effect like this:
(This question is about this buttons that working like scrollview)
Have you another ideas how to create control like this?
It's vertical representation, but idea is understood: hide scrollbars and manipulate them manually. For more responsive UI you'll need to subscribe to MouseDown event instead of Click, also NullReference exceptions are possible on every line of Grid_Click().
XAML:
<ListView.Template>
<ControlTemplate>
<Grid ButtonBase.Click="Grid_Click">
<Grid.RowDefinitions>
<RowDefinition Height="16"/>
<RowDefinition Height="*"/>
<RowDefinition Height="16"/>
</Grid.RowDefinitions>
<Button Content="^" Grid.Row="0"/>
<Button Content="v" Grid.Row="2"/>
<ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Hidden">
<ItemsPresenter/>
</ScrollViewer>
</Grid>
</ControlTemplate>
</ListView.Template>
Code:
private void Grid_Click(object sender, RoutedEventArgs e) {
bool down = (e.OriginalSource as Button).Content as string == "v";
var scroller = VisualTreeHelper.GetChild((e.OriginalSource as Button).Parent, 2) as ScrollViewer;
scroller.ScrollToVerticalOffset(scroller.VerticalOffset + (down ? 1 : -1));
}
Magical number 2 in GetChild() is index of ScrollViewer inside its parent (Grid).

Categories

Resources