Open verelpode opened 5 years ago
You are correct that manually calling Measure or Arrange on a TextBlock (or any other element) can cause problems if done at the wrong time. The primary use, of course, is to call these in MeasureOverride/ArrangeOverride of a Panel subclass on that Panel's direct children elements. Any other use should be careful to avoid the issues you mentioned.
One safe use is to call Measure() on a TextBlock which is not in the tree, so its measure/arrange dirty state doesn't matter.
We agree that having an explicit mechanism to measure text would be useful, and we have come close a couple of times to adding a feature for this. There are a couple challenges which come up:
Your proposal to MeasureTheoretically is an interesting idea to allow measuring size while avoiding the two challenges above (at least without the second text parameter).
It would be great if you could open a Proposal to add a text measurement API. Your MeasureTheoretically idea can be the initial proposal for that, and we can see if the community has other ideas or other scenarios for text measurement.
How about we just add the label and edit the title to morph this into a feature proposal?
How do we keep the API simple yet still support full flexibility so it isn't too limited?
I like this balanced attitude very much, so I've quoted it to highlight it for the benefit of everyone who may happen to read this issue.
Your proposal to MeasureTheoretically is an interesting idea to allow measuring size while avoiding the two challenges above (at least without the second text parameter).
OK, if your opinion is that the text override parameter is troublesome, then let's remove it from the proposal. It's a non-essential parameter.
Re the Size
parameter, it could be one of these:
Size availableSize
with identical meaning as in Measure
.Size elementSize
as override for FrameworkElement.Width
and Height
.Size maxSize
as override for FrameworkElement.MaxWidth
and MaxHeight
, and MeasureTheoretically ignores FrameworkElement.Width, Height, MinWidth, MinHeight
.Size
parameter at all. Always operates equivalent to Measure(new Size(double.PositiveInfinity, double.PositiveInfinity))
. However this doesn't support the case when TextBlock.Width = NaN
(auto).Without being able to see the source code of TextBlock
, I'm having difficulty deciding which one of the above is best. Which one do you think is probably the best?
One safe use is to call Measure() on a TextBlock which is not in the tree, so its measure/arrange dirty state doesn't matter.
Although that's a very good tip, it's not always practical. For example, my real scenario faced this week requires that a TextBlock be in the tree because the text needs to be displayed to the user. Thus I cannot safely call Measure
because it is in the tree and cannot be removed. To work around this problem, I'm forced to create 2 identical TextBlocks. Identical except that one is in the tree and the other is unparented. The following properties in the in-tree TextBlock must be copied to the unparented TextBlock:
Text
or Inlines
(perhaps also Language
)Padding
and Margin
(DesiredSize
includes Margin
, unlike ActualWidth/Height
)Width, Height, MinWidth, MaxWidth, MinHeight, MaxHeight
TextAlignment, TextDecorations, TextEffects, TextTrimming, TextWrapping
FontFamily, FontSize, FontWeight, FontStyle, FontStretch
LineHeight, LineStackingStrategy, IsHyphenationEnabled, BaselineOffset
These properties are not only copied once, but rather it requires that the two TextBlocks be synchronized/mirrored. Each time one of those properties is changed in the in-tree TextBlock, it must also be changed in the unparented TextBlock. Impractical. The MeasureTheoretically
proposal would eliminate the duplicate TextBlock.
In addition to MeasureTheoretically
(not instead of), what do you think of the idea of creating the following static/non-instance method in TextBlock
or other suitable class?
public static Size MeasureText(string text, Size maxSize, TextWrapping wrapping, FontFamily font, FontSize fontSize, FontWeight? weight = null, FontStyle? style = null, FontStretch? stretch = null);
When the wrapping is parameter is Wrap
, the line breaks are placed according to maxSize.Width
. When the text is very long and the lines wrap beyond maxSize.Height
, then the method stops measuring (doesn't need to process the remainder of the text) and limits the returned height to maxSize.Height
.
As you can see, this non-instance method is simple and easy-to-use, but supports fewer options than MeasureTheoretically
, but it supports the most common cases. This method would allow apps to easily measure text in the most common cases, without requiring the creation of a TextBlock
or System.Windows.Media.FormattedText
instance or any other instance. In cases where uncommon options (such as BaselineOffset
) are required, it is reasonable that more work is required, and apps can use MeasureTheoretically
in those cases.
The WinUI team could also decide whether or not to support 2 overloads of the non-instance method: One for string text
, and one for InlineCollection
:
public static Size MeasureText(string text, Size maxSize, ...);
public static Size MeasureText(System.Windows.Documents.InlineCollection inlines, Size maxSize, ...);
I wouldn't expect the non-instance method to cache the resulting Size
. It's reasonable to expect the app to cache the Size
by itself by storing it in a field. Does any other significant caching/performance problem exist in the non-instance method idea? I'm referring to where you wrote:
TextBlock caches measurement information in the TextBlock specific to that TextBlock's text and font properties. [...] If we create a separate API to measure text, then either it won't take advantage of this caching, or we need more complex API changes to ensure the target TextBlock can cache the measurement info.
I haven't seen the source code but I guess that this issue of cached measurement info might mean that the non-instance method should only support string text
and not InlineCollection
? Even if the non-instance method only supports string text
, it would still be quite convenient in many places. Whenever the app's requirements are more complex, or whenever a TextBlock
instance already exists anyway, MeasureTheoretically
could be used.
@codendone
One safe use is to call Measure() on a TextBlock which is not in the tree, so its measure/arrange dirty state doesn't matter.
But does a multi-monitor environment break the unparented TextBlock technique? (Admittedly my description following is possibly inaccurate or outright incorrect because I might be mixing up the behavior of WPF versus UWP/WinUI, and because I don't have the considerable time necessary to fully research the multi-monitor issue. So please just tell me if I'm incorrect.)
For example, a comparison with WPF: System.Windows.Media.FormattedText.Width/Height
is akin to measuring text in an unparented manner. However, IIRC this broke compatibility with multi-monitor environments (correct?). In order to solve this problem, the FormattedText.PixelsPerDip property was added starting in .NET Framework 4.6.2. This property was also added as a parameter to the constructors. Furthermore, the older constructors that don't have a pixelsPerDip
parameter are now marked with ObsoleteAttribute
and display a warning in Visual Studio and in the online documentation:
public class FormattedText
{
[Obsolete("Use the PixelsPerDip override", false)]
public FormattedText(string textToFormat, CultureInfo culture, FlowDirection flowDirection,
Typeface typeface, double emSize, Brush foreground)
{ ... }
// New constructor with the necessary pixelsPerDip parameter:
public FormattedText(string textToFormat, CultureInfo culture, FlowDirection flowDirection,
Typeface typeface, double emSize, Brush foreground,
double pixelsPerDip)
{ ... }
}
In order to get the correct PixelsPerDip for a given FrameworkElement in the tree, while being compatible with multi-monitor, you can do:
WinXaml.FrameworkElement element = ...;
double ppd = System.Windows.Media.VisualTreeHelper.GetDpi(element).PixelsPerDip;
var ft = new FormattedText(..., pixelsPerDip: ppd);
However, that's WPF. Is this a non-issue in UWP/WinUI even when the computer has multiple monitors? In UWP, the following is true: PixelsPerDip == Windows.Graphics.Display.DisplayInformation.RawPixelsPerViewPixel
But that's a per-instance property. To read it, you can do:
double ppd = Windows.Graphics.Display.DisplayInformation.GetForCurrentView().RawPixelsPerViewPixel;
Thus a bug would occur if you measured an unparented TextBlock
and then used this measurement info in a different CoreApplicationView
that happens to be displayed on a different monitor with different RawPixelsPerViewPixel
. Although my proposed MeasureTheoretically
eliminates this problem with unparented TextBlocks, maybe this multi-monitor bug doesn't occur anyway because if you try to access a TextBlock
created in a different CoreApplicationView
(different thread), it throws an exception, regardless of whether the TextBlock is unparented or not.
BUT WAIT! What about the new Windows.UI.WindowManagement.AppWindow API? It allows apps to create multiple windows in the same thread. Does AppWindow break the unparented TextBlock technique when run in a multi-monitor environment? If the answer is Yes, then again the proposed MeasureTheoretically
is advantageous because it works with a TextBlock in the tree, without disrupting Measure/Arrange passes, and with multi-monitor compatibility.
Re my other proposed method; the static/non-instance MeasureText
method: Does this method need to be given a double rawPixelsPerViewPixel
parameter akin to FormattedText.PixelsPerDip
? The parameter could be any one of the following ways of providing direct or indirect access to the correct RawPixelsPerViewPixel
value:
double rawPixelsPerViewPixel
with same meaning as Windows.Graphics.Display.DisplayInformation.RawPixelsPerViewPixel
DisplayInformation display = null
. If null, it defaults to Windows.Graphics.Display.DisplayInformation.GetForCurrentView()
.ApplicationView view = null
. If null, it defaults to Windows.UI.ViewManagement.ApplicationView.GetForCurrentView()
.AppWindow window = null
. It uses AppWindow to determine which DisplayInformation to use. If AppWindow is null, it defaults to DisplayInformation.GetForCurrentView().RawPixelsPerViewPixel
.What do you think of the idea of creating MeasureTheoretically
in UIElement
(or FrameworkElement
) instead of only in TextBlock
? Although TextBlocks are the most common scenario/need for this feature, it would also be useful to have the ability to measure other elements.
Currently Measure
is implemented approximately (in principle) like this:
public void Measure(Size availableSize)
{
...
this._desiredSize = MeasureOverride(availableSize);
this.MeasureDirty = false;
...
}
protected virtual Size MeasureOverride(Size availableSize)
{ ... }
This means that MeasureTheoretically
could be implemented in FrameworkElement
similar to this:
// Note virtual to allow subclasses to override if necessary.
public virtual Size MeasureTheoretically(Size availableSize)
{
// Don't change this._desiredSize nor this.MeasureDirty.
return MeasureOverride(availableSize);
}
That would instantly work correctly with nearly all subclasses of FrameworkElement
, wouldn't it? I guess that only a small number of classes would need to be changed to be compatible with MeasureTheoretically
-- that is any classes that implement MeasureOverride
in a way that modifies their internal fields in an unsafe manner (unsafe meaning producing a side-effect that disrupts the normal Measure/Arrange passes). Those classes would need to be changed -- they would need to override MeasureTheoretically
and provide a safe implementation.
Alternatively, if you prefer, UIElement
could define it in an opt-in manner for the backwards-compatibility reasons, like this:
public virtual Size MeasureTheoretically(Size availableSize)
{
throw new System.NotSupportedException();
}
Would this MeasureTheoretically method take account of Margins + Padding, Transparent backgrounds, Null Backgrounds, Focus Rectangles with negative margins, clipped elements, etc
@mdtauk -- I'd propose that all of those factors be handled in the exact same manner as the existing Measure
/DesiredSize
. Both Margin
and Padding
are included in UIElement.DesiredSize
, therefore MeasureTheoretically
would be the same. Actually in my mind it makes more sense to exclude Margin
and include Padding
, but it'd be confusing if Measure
and MeasureTheoretically
behave differently in regards to Margin
, therefore I think it's overall better to keep it the same as the preexisting behavior.
As for the other proposed method -- the simple non-instance MeasureText
method -- no margin or padding parameters would be supported in that method because they're unnecessary.
The proposed MeasureTheoretically for all UIElements would be absolutely wonderful for my usage! Coming from WPF, I was not able to call UpdateLayout and check the DesiredSize for an element b/c UpdateLayout can result in a message loop and queued events could run when it was not appropriate. So, I created code to do my own (usually buggy) MeasureTheoretically for Buttons, etc. So, considering porting to WinUI3, I would LOVE to be able to avoid possible reentrancy issues here and also measure controls for layout (we built UI in code, not XAML). So please create this feature!!!
Please correct me if there is something I don't know: The current version of WinUI seems to have a problem when apps need to measure the width and height of text. People suggest using
Windows.UI.Xaml.UIElement.Measure
in order to measure text, like this:AFAIK, the above technique is buggy because it disrupts or interferes with the measure & arrange process/system (depending on when/where you invoke
Measure
and what value you supply for theavailableSize
parameter ofMeasure
). For example, this can happen:Windows.UI.Xaml.UIElement.InvalidateMeasure
and/orInvalidateArrange
is invoked in the normal manner at the normal time.InvalidateMeasure
sets its internal "IsMeasureValid" field/flag to false (the WPF equivalent isSystem.Windows.UIElement.IsMeasureValid
).InvalidateMeasure
schedules a measure pass (an update). The measure pass is not performed immediately, rather it is scheduled to be performed later-but-soon.Windows.UI.Xaml.UIElement.Measure
is invoked in the normal manner in response to the scheduling initiated byInvalidateMeasure
, the app decides that it needs to measure text and so it invokesmyTextBlock.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
Measure
,Measure
sets the internal "IsMeasureValid" field/flag to true.Measure
is invoked in the normal manner in response to the scheduling initiated byInvalidateMeasure
, but it does nothing because "IsMeasureValid" is already true.myTextBlock.DesiredSize
(UIElement.DesiredSize
) now contains an incorrect size becauseMeasure
was invoked with sizedouble.PositiveInfinity
instead of the realavailableSize
value calculated during a normal measure pass.myTextBlock.DesiredSize
.In WPF, one way of solving this problem is to stop using
Measure
and instead use System.Windows.Media.FormattedText to measure text, butFormattedText
does not exist in UWP/WinUI. Am I unaware of an alternative or equivalent toFormattedText
? Does there exist a recommended way to measure text in UWP/WinUI?I suggest that
Windows.UI.Xaml.Controls.TextBlock
be given a new method named "MeasureTheoretically" that operates likeWindows.UI.Xaml.UIElement.Measure
except that it performs the measurement only "in theory" and does not change any properties or internal fields inTextBlock
. Thus it would not changeUIElement.DesiredSize
nor the internal "IsMeasureValid" field/flag. It could be defined like this:availableSize
parameter has same meaning as inWindows.UI.Xaml.UIElement.Measure(availableSize)
.UIElement.DesiredSize
except thatDesiredSize
is not changed.To make it even better, I suggest adding a "text" parameter as follows:
text
parameter is supplied (non-null), then it temporarily overridesTextBlock.Text
without actually changingTextBlock.Text
.text
parameter is null (NOTSystem.String.Empty
), then it usesTextBlock.Text
orTextBlock.Inlines
as normal.Alternatively, the
availableSize
parameter could be removed entirely and it would act ifavailableSize
is alwaysdouble.PositiveInfinity
. In this case, it would be useful to have a parameter that overrides the values ofWindows.UI.Xaml.FrameworkElement.Width
andHeight
, such as:elementSize.Width
always overridesWindows.UI.Xaml.FrameworkElement.Width
.double.NaN
is acceptable (means automatic Width).elementSize.Height
always overridesWindows.UI.Xaml.FrameworkElement.Height
.double.NaN
is acceptable (means automatic Height).In some cases, apps don't need to perform a full measurement, rather they just need to get the line height. This means they need to get the actual/effective value of
Windows.UI.Xaml.Controls.TextBlock.LineHeight
but they cannot read theLineHeight
property in order to get this value, because usuallyLineHeight
returns zero. The default value ofLineHeight
is zero and it means that the line height is determined automatically. Thus I suggest that TextBlock be given a get-only "ActualLineHeight" or "EffectiveLineHeight" property like this: