WPF TextBox recalculate size - wpf

When using a wpf textbox without explicit height and width values, and when there is space available to expand, the textbox resizes as you type.
However when I change the border thickness, it does not recalculate it and for very thick borders, part of the text is covered by the border. How do I explicitly precipitate a recalc?
Coincidently I am using a derived custom textbox class so I should know when the border thickness changes.

This bug must be some optimization gone wrong
Overriding Metadata for BorderThickness or adding a Dependency Property that affects Measure, Arrange or Render don't help
Undocking and Redocking from the parent container had no effect either
Even Undocking from the parent container and Redocking into a new container won't help if the space it is given in the new container is exactly the same as the space that it had in the old container
It seems like the size is only re-calculated once Text, Width, Height or available space changes. I looked around with Reflector but things get pretty complex down there so I couldn't find the source for this.
Here is a small workaround that listens to changes in BorderThickness and in the changed event handler, make a small change to the Width and once it is updated, change it right back
public class MyTextBox : TextBox
{
public MyTextBox()
{
DependencyPropertyDescriptor borderThickness
= DependencyPropertyDescriptor.FromProperty(MyTextBox.BorderThicknessProperty, typeof(MyTextBox));
borderThickness.AddValueChanged(this, OnBorderThicknessChanged);
}
void OnBorderThicknessChanged(object sender, EventArgs e)
{
double width = this.Width;
SizeChangedEventHandler eventHandler = null;
eventHandler = new SizeChangedEventHandler(delegate
{
this.Width = width;
this.SizeChanged -= eventHandler;
});
this.SizeChanged += eventHandler;
this.Width = this.ActualWidth + 0.00000001;
}
}

First of all, this looks like a bug.
If the problem is that dynamic changes of the border thickness are not taken into account, you can perhaps make a workaround by creating a dependency property with AffectsMeasure in FrameworkPropertyMetadata, and bind it to the border thickness. Hope this quirk helps.
If the static setting of the border thickness are not taken into account, you can try to replace the TextBox's default template with your own (correct) version.

Related

`Window` `Width` and `Top` have local values

My main window's Height, Left, Top, and Width are all bound to their respective viewmodel properties through a style. I can confirm that these four properties in the view model are only ever set to 1920, 1920, 118, 1080 respectively.
But when I launch the app, the Top and Width properties on the main window are set to something else (Width will be 1440 and Top will be a random number usually less than 300). What would cause this?
Here's what I see when I Snoop the app. Notice how Top and Width come from a Local Value Source:
Strangely, when I right-click on those properties in Snoop and tell it to "Clear/Reset", then those properties begin behaving. What is Snoop doing that fixes this?
Other facts:
The getters for the Top and Width viewmodel properties are only called once while the main window is being initialized. The stack trace runs through framework binding initialization code.
The setters for the Top and Width viewmodel properties are only called once from the viewmodel constructor as it sets those properties to 118 and 1080 respectively.
The bindings for these four properties are all two-way.
None of these things cause the view's properties to change/be correct:
Changing the associated viewmodel properties at runtime, even after the view has been fully loaded.
Calling UpdateLayout() on the view.
Calling InvalidateArrange() on the view.
Calling InvalidateMeasure() on the view.
Calling InvalidateProperty(FrameworkElement.WidthProperty) on the view.
Calling InvalidateVisual() on the view.
I have searched and searched and do not see any code anywhere touching the view's Top or Width properties (other than the style bindings).
Here's the style:
Sorry I had to blank out type names and some other things—it's a company application. If it helps, the main window/the view is at the end of a long inheritance line with Window as its great-great grandaddy. I'm trying to make the main window more reusable by MVVM-ing it—formerly these layout properties were set in code-behind in the view, and the view had constructor parameters :'( That's related to why I need to key the style, and why the style is based on other stuff. But none of the inherited types manipulate layout properties.
P.S. I've seen other people complain about how hard it is to resize WPF's Window. The most commonly suggested solution is to bind MinWidth and MaxWidth as well as Width. When I do that then the Width is indeed forced to the value I want, but you can't resize the window, the Width property still has its Local Value Source, and Top is still incorrect.
Given that "[Top] cannot be set through a style", and given the complications with binding Window.Width, I solved this a different way.
I created this attached property called WindowLayout and bound it to a viewmodel property in my style:
public static class WindowLayoutBehavior
{
public static readonly DependencyProperty LayoutProperty = DependencyPropertyHelpers.RegisterAttached(
(Window x) => GetLayout(x),
new PropertyMetadata(HandleLayoutChanged));
private static void HandleLayoutChanged(
DependencyObject d,
DependencyPropertyChangedEventArgs e)
{
if (!(d is Window window))
return;
if (!(e.NewValue is Rect rect))
return;
window.Height = rect.Height;
window.Left = rect.Left;
window.Top = rect.Top;
window.Width = rect.Width;
}
[AttachedPropertyBrowsableForType(typeof(Window))]
public static Rect GetLayout(Window window) =>
window.GetValue(LayoutProperty) is Rect rect
? rect
: default;
public static void SetLayout(Window window, Rect rect) =>
window.SetValue(LayoutProperty, rect);
}
DependencyPropertyHelpers.RegisterAttached is a shorthand helper method for creating the attached property in the way you might expect.
Usage in the style:
<Setter
Property="WindowLayoutBehavior.Layout"
Value="{Binding WindowLayout, Mode=OneWay}"/>
Now when I Snoop the app, Height, Left, Top, and Width all show as having Local Value Sources, and they change when the viewmodel property changes, so that works for me.

Keeping aspect ratio of a control

I've looked for quite a while now for a way to be able to tell a WPF control (or window) to keep a certain aspect ratio.
For a Window I found this solution, that works quite well. But since it uses the Win32 API and window handles it's not working for any WPF Controls (because as far as I know in WPF only the window itself has a handle)
For a Control one usually gets the advice to put the Control in a ViewBox, but I don't want to scale my controls, I want them to resize (and keep any border width or font size).
Other "solutions" for a Control involve any form of binding the Width to the ActualHeight or the Height to the ActualWidth, or using the SizeChanged event, but this results in heavy flickering while resizing and it's not very reliable.
In case of binding the Width to the ActualHeight you can't resize only the Width (by dragging the right border) because the ActualHeight doesn't change.
In case of the event it gets tricky when width and height change at the same time, then you'd have to change the size inside the SizeChanged event... and did I mention the flickering?
After a lot of reading and searching I came to the conclusion that the best way to force any control to keep a certain aspect ratio would be to do that inside the Measure and Arrange functions.
I found this solution that creates a Decorator control with overridden Measure and Measure functions, but that would mean to put any control that's supposed to keep it's aspect ratio inside it's own Decorator. I could live with that if I had to, but I wonder if there's a better way to do it.
So, here's my question. Is it possible to create an attached property Ratio and an attached property KeepRatio and somehow override the Measure and Arrange functions of the controls in question in the OnKeepRatioChanged and RatioChanged callbacks of the attached properties?
If you want to override Arrange/Measure methods then there is no need in attached properties. This wrapper should be fine:
public partial class RatioKeeper : UserControl
{
public static readonly DependencyProperty VerticalAspectProperty = DependencyProperty.Register(
"VerticalAspect", typeof(double), typeof(RatioKeeper), new PropertyMetadata(1d));
public static readonly DependencyProperty HorizontalAspectProperty = DependencyProperty.Register(
"HorizontalAspect", typeof(double), typeof(RatioKeeper), new PropertyMetadata(1d));
public double HorizontalAspect
{
get { return (double) GetValue(HorizontalAspectProperty); }
set { SetValue(HorizontalAspectProperty, value); }
}
public double VerticalAspect
{
get { return (double) GetValue(VerticalAspectProperty); }
set { SetValue(VerticalAspectProperty, value); }
}
public RatioKeeper()
{
InitializeComponent();
}
//arrangeBounds provides size of a host.
protected override Size ArrangeOverride(Size arrangeBounds)
{
//Calculation of a content size that wont exceed host's size and will be of the desired ratio at the same time
var horizontalPart = arrangeBounds.Width / HorizontalAspect;
var verticalPart = arrangeBounds.Height / VerticalAspect;
var minPart = Math.Min(horizontalPart, verticalPart);
var size = new Size(minPart * HorizontalAspect, minPart * VerticalAspect);
//apply size to wrapped content
base.ArrangeOverride(size);
//return size to host
return size;
}
}

Popup to appear on the bottom-right corner of its parent

I'm trying to design a Popup which will appear on the bottom-right corner of its PlacementTarget
Let's admit that you set its PlacementTarget to a Window, well, the Popup will act as classic toaster notifications.
Given the fact that WPF is not smart enough to provide us a "corner" solution, I'm trying to implement a new control, inheriting from Popup , which will place itself at the appropriate location.
Here is my first idea: work on Loaded event to determine where should I place the Popup.
Problem? I don't want to give any fixed dimensions to the popup, which is supposed to size itself according to the text displayed.
However, I can't get the ActualWidth property when Loaded event is raised.
I can't have it either when Opened event is raised.
Here is the draft code so far:
public class ExceptionPopup : Popup
{
public ExceptionPopup()
{
InitializeComponent();
Loaded += new RoutedEventHandler(ExceptionPopup_Loaded);
}
void ExceptionPopup_Loaded(object sender, RoutedEventArgs e)
{
if (PlacementTarget != null)
{
if (PlacementTarget is FrameworkElement)
{
parentWidth = (PlacementTarget as FrameworkElement).ActualWidth;
parentHeight = (PlacementTarget as FrameworkElement).ActualHeight;
}
}
}
protected override void OnOpened(EventArgs e)
{
this.HorizontalOffset = parentWidth;
this.VerticalOffset = parentHeight;
base.OnOpened(e);
}
}
Is there any other event I could use to catch what I want here?
I'd basically like to set HorizontalOffset to parentWidth - ActualWidth/2 , same for height :)
Any idea?
Thanks!
Usually I set the PlacementTarget to either Bottom or Right, then apply a RenderTransform which shifts the Popup by the remaining value.
For example, I might use Placement=Bottom, then use a RenderTransform to shift the popup (Window.Width - Popup.Width) to the right, and Popup.Height upwards. You might not even need to re-adjust based on the Popup Height/Width becauase MSDN says that Popups are not allowed to be displayed off screen, and it will automatically adjust their placement to keep them visible
Be sure you use a RenderTransform instead of a LayoutTransform, because RenderTransforms get applied after the Popup gets Rendered, so the ActualHeight and ActualWidth will be greater than 0.

Why is a ListBoxItem not calling MeasureOverride when its width is changed?

Ok, for illustrative purposes, below I created a subclass of ListBoxItem and a subclass of ListBox which uses it as its container by overriding both IsItemItsOwnContainerOverride and GetContainerForItemOverride.
Now when the window first appears, as expected, MeasureOverride is called on every ListBoxItem (with Infinity,Infinity) followed by ArrangeOverride being called on every item.
However, when resizing the ListBox, only ArrangeOverride is called on the ListBoxItem, not MeasureOverride even though the metadata for the width property is set to AffectsMeasure.
NotE: I know I can get around this by setting ScrollViewer.HorizontalScrollbarVisibility to 'Disabled' in which case MeasureOverride does get called as expected because that scroll setting forces the items to match the width of the listbox and thus naturally would re-fire. However, I'm still trying to figure out why Measure isn't called by default anyway because the metadata for the Width property has the AffectsMeasure flag set and the width is changing via the ArrangeOverride step.
Is that flag just a hint for its container and in the case of a control placed in a ScrollViewer it's ignored? My guess is that unless you disable the scrolling, the controls have an infinite area available to them, so once they are measured, there's no need to re-measure them again. Disable the horizontal scrolling however and you're stating the width isn't unlimited, hence the MeasureOverride is called again. But that's just a guess, albeit a logical one.
Here's example code to play with. Create a new WPF project and paste this in the window's CodeBehind and look at the debug output. Next, set the HorizontalScrollbarVisibility flag and you'll see that it does get called.
public partial class MainWindow : Window
{
public MainWindow(){
InitializeComponent();
var items = new List<object>(){ "This is a really, really, really, really long sentence"};
var lbx = new ListBoxEx { ItemsSource = items };
this.Content = lbx;
}
}
public class ListBoxEx : ListBox
{
protected override bool IsItemItsOwnContainerOverride(object item){
return (item is ListBoxItemEx);
}
protected override DependencyObject GetContainerForItemOverride(){
return new ListBoxItemEx();
}
}
public class ListBoxItemEx : ListBoxItem
{
protected override Size MeasureOverride(Size availableSize){
Console.WriteLine("MeasureOverride called with " + availableSize);
return base.MeasureOverride(availableSize);
}
protected override Size ArrangeOverride(Size finalSize){
Console.WriteLine("ArrangeOverride called with " + finalSize);
return base.ArrangeOverride(finalSize);
}
}
Ok, I think the answer is it's not firing because the Measure pass is affecting the panel, not the items, and the panel itself hasn't resized so there's no reason to re-measure its children.
However, when setting ScrollViewer.HorizontalScrollbarVisibility to Disabled, the width of the panel tracks the width of the ListBox, not its contents, therefore the children do need to be re-measured, hence that's why they were in that case.

WPF stretched UserControl width doesn't match the width of its child horizontal StackPanel

first time post for me :)
I'm having this issue with the width of my UserControl that i just can't seem to figure out.
Essentially i have a UserControl that contains an ItemsControl (panle is a horizontal StackPanel). The item's DataTemplate is a standard Button.
I bind a List of strings to the ItemsControl, and each string gets bound to the Content property of the Button. Works fine till now.
What i need to do is to keep track of the width of each single item (Button with string inside) in the ItemsControl even if they are not rendered. I need to do this to dynamically (while resizing) remove items that exceed the maximum width of the UserControl, and add them back again if there is enough space.
To achieve this i measure the width of each string using the following function:
private double GetTextWidth(string text)
{
FormattedText ft = new FormattedText(text, CultureInfo.CurrentCulture, FlowDirection.LeftToRight, new Typeface(this.FontFamily, this.FontStyle, this.FontWeight, this.FontStretch), this.FontSize, this.Foreground);
ft.Trimming = TextTrimming.CharacterEllipsis;
return ft.WidthIncludingTrailingWhitespace;
}
I then manually add a margin of 6 to the returned value, this margin is the spacing needed by the Button.
Summing all the generated values from the strings in my List i should get almost the same width as my UserControl. Since all layouts are stretched (even the UserControl in the Window is), there is no margin or padding going on (as far as i can see) and there is only a single Border which has a width of 1.
Problem is that this doesn't work, it requires me to manually add a pretty large value (45-65 units) to the calculated width to get it to work "pixel-perfect".
I obviously tried to find the root of this problem, but was unable.
First i thought it's caused by a DIP <-> Pixel problem, but that doesn't seem the case.
I measured the width of a single string and then the width of the button containing the same string. In all scenarios the first and the second differ by 6 units.
There is no visible space between the buttons so i really can't explain where so much overhead is coming from.
The only thing i noticed is that the value i need to add changes if i change the FontSize, but that's pretty obvious...
Probably i'm missing something big, any ideas?
Thanks for reading!
If I understand you correctly, you want to hide items that are not completely visible on the screen.
I've done this in the past with a helper method that will tell me if a control is fully visible or not, and that sets the control's visibility to Hidden if the control is not fully visible. I implemented it in the Loaded and SizeChanged events.
Here's the helper class which returned the rendered control's visibility:
public enum ControlVisibility
{
Hidden,
Partial,
Full,
FullHeightPartialWidth,
FullWidthPartialHeight
}
/// <summary>
/// Checks to see if an object is rendered visible within a parent container
/// </summary>
/// <param name="child">UI element of child object</param>
/// <param name="parent">UI Element of parent object</param>
/// <returns>ControlVisibility Enum</returns>
public static ControlVisibility IsObjectVisibleInContainer(
FrameworkElement child, UIElement parent)
{
GeneralTransform childTransform = child.TransformToAncestor(parent);
Rect childSize = childTransform.TransformBounds(
new Rect(new Point(0, 0), new Point(child.Width, child.Height)));
Rect result = Rect.Intersect(
new Rect(new Point(0, 0), parent.RenderSize), childSize);
if (result == Rect.Empty)
{
return ControlVisibility.Hidden;
}
if (result.Height == childSize.Height && result.Width == childSize.Width)
{
return ControlVisibility.Full;
}
if (result.Height == childSize.Height)
{
return ControlVisibility.FullHeightPartialWidth;
}
if (result.Width == childSize.Width)
{
return ControlVisibility.FullWidthPartialHeight;
}
return ControlVisibility.Partial;
}
The code-behind the Loaded and SizeChanged events looked something like this:
ControlVisibility ctrlVisibility=
WPFHelpers.IsObjectVisibleInContainer(button, parent);
if (ctrlVisibility == ControlVisibility.Full
|| isVisible == ControlVisibility.FullWidthPartialHeight)
{
button.Visibility = Visibility.Visible;
}
else
{
button.Visibility = Visibility.Hidden;
}

Resources