Measuring text in WPF - wpf

Using WPF, what is the most efficient way to measure a large number of short strings? Specifically, I'd like to determine the display height of each string, given uniform formatting (same font, size, weight, etc.) and the maximum width the string may occupy?

The most low-level technique (and therefore giving the most scope for creative optimisations) is to use GlyphRuns.
It's not very well documented but I wrote up a little example here:
http://smellegantcode.wordpress.com/2008/07/03/glyphrun-and-so-forth/
The example works out the length of the string as a necessary step before rendering it.

In WPF:
Remember to call Measure() on the TextBlock before reading the DesiredSize property.
If the TextBlock was created on-the-fly, and not yet shown, you have to call Measure() first, like so:
MyTextBlock.Measure(new Size(Double.PositiveInfinity, Double.PositiveInfinity));
return new Size(MyTextBlock.DesiredSize.Width, MyTextBlock.DesiredSize.Height);
In Silverlight:
No need to measure.
return new Size(TextBlock.ActualWidth, TextBlock.ActualHeight);
The complete code looks like this:
public Size MeasureString(string s) {
if (string.IsNullOrEmpty(s)) {
return new Size(0, 0);
}
var TextBlock = new TextBlock() {
Text = s
};
#if SILVERLIGHT
return new Size(TextBlock.ActualWidth, TextBlock.ActualHeight);
#else
TextBlock.Measure(new Size(Double.PositiveInfinity, Double.PositiveInfinity));
return new Size(TextBlock.DesiredSize.Width, TextBlock.DesiredSize.Height);
#endif
}

It is very simple and done by FormattedText class!
Try it.

You can use the DesiredSize property on a rendered TextBox to get the height and width
using System.Windows.Threading;
...
Double TextWidth = 0;
Double TextHeight = 0;
...
MyTextBox.Text = "Words to measure size of";
this.Dispatcher.BeginInvoke(
DispatcherPriority.Background,
new DispatcherOperationCallback(delegate(Object state) {
var size = MyTextBox.DesiredSize;
this.TextWidth = size.Width;
this.TextHeight = size.Height;
return null;
}
) , null);
If you have a large number of strings it may be quicker to first pre-calualte the height and width of every indiviudal letter and symbol in a given font, and then do a calculation based on the string chars. This may not be 100% acurate due to kerning etc

Related

Place an Image scaled at the width available space

I created a code that works, but I'm not sure that it's the best way to place an Image scaled automatically to the available width space. I need to put some content over that image, so I have a LayeredLayout: in the first layer there is the Label created with the following code, on the second layer there is a BorderLayout that has the same size of the Image.
Is the following code fine or is it possible to do better?
Label background = new Label(" ", "NoMarginNoPadding") {
boolean onlyOneTime = false;
#Override
public void paint(Graphics g) {
int labelWidth = this.getWidth();
int labelHeight = labelWidth * bgImage.getHeight() / bgImage.getWidth();
this.setPreferredH(labelHeight);
if (!onlyOneTime) {
onlyOneTime = true;
this.getParent().revalidate();
}
super.paint(g);
}
};
background.getAllStyles().setBackgroundType(Style.BACKGROUND_IMAGE_SCALED_FIT);
background.getAllStyles().setBgImage(bgImage);
Shorter code:
ScaleImageLabel sl = new ScaleImageLabel(bgImage);
sl.setUIID("Container");
You shouldn't override paint to set the preferred size. You should have overriden calcPreferredSize(). For ScaleImageLabel it's already set to the natural size of the image which should be pretty big.

PreferredSize property doesn't match GetPreferredSize

I am trying to write a menu item control that will AutoSize based on the length of text it contains (like the Label control).
To do this, I overrided the GetPreferredSize method to calculate the length of the text:
public override Size GetPreferredSize(Size proposedSize)
{
Size size = TextRenderer.MeasureText(this.Text, this.Font);
int w = size.Width + this.Padding.Left + this.Padding.Right;
int h = size.Height + this.Padding.Top + this.Padding.Bottom;
return new Size(w, h);
}
I then add a bunch of these controls to a containing menu control and try to position them based on the size above:
if (item.AutoSize)
{
item.Size = item.PreferredSize;
}
item.Left = _Left;
item.Top = _Top;
if (this.MenuOrientation == Orientation.Vertical)
{
_Top += item.Size.Height;
}
else
{
_Left += item.Size.Width;
}
this.Controls.Add(item);
However, the sizes returned by PreferredSize and GetPreferredSize aren't the same. For one string, GetPreferredSize returns {Width=147, Height=27}, but PreferredSize returns {Width=105, Height=21}. Because of this the controls overlap instead of appearing beside one another.
I tried overriding MinimumSize instead of GetPreferredSize, but that also got scaled down from what I calculated.
So my question is, what is the correct way to do this? I'd also like to understand the way that AutoSize, PreferredSize, MinimumSize, and MaximumSize are meant to interact. MSDN is little help on this.

How do you use MeasureOverride in Silverlight?

I think I have understood how MeasureOverrride works, but I am trying to use it in a very simple case and It doesn't work... So now I'm not so sure... After using Measureoverride do I have to use Arrageoverride too or the system will do it for me? The situation is this one: I have a LinearLayout class inherited from Panel and it has two fields called wrapwidht and wrapheigh, if they are true the width or the height of the LinearLayout has to be as its children require. so my Measureoveride looks like:
protected override Size MeasureOverride(Size availableSize) {
Size panelDesiredSize = new Size();
if ((this.widthWrap) || (this.heightWrap))
{
foreach (UIElement elemento in this.Children)
{
System.Diagnostics.Debug.WriteLine(((FrameworkElement)elemento).DesiredSize.ToString());
if (this.Orientation.Equals(System.Windows.Controls.Orientation.Vertical))
{
if (this.widthWrap)
{
//the widest element will determine containers width
if (panelDesiredSize.Width < ((FrameworkElement)elemento).Width)
panelDesiredSize.Width = ((FrameworkElement)elemento).Width;
}
//the height of the Layout is determine by the sum of all the elment that it cointains
if (this.heightWrap)
panelDesiredSize.Height += ((FrameworkElement)elemento).Height;
}
else
{
if (this.heightWrap)
{
//The highest will determine the height of the Layout
if (panelDesiredSize.Height < ((FrameworkElement)elemento).Height)
panelDesiredSize.Height = ((FrameworkElement)elemento).Height;
}
//The width of the container is the sum of all the elements widths
if (this.widthWrap)
panelDesiredSize.Width += ((FrameworkElement)elemento).Width;
}
}
}
System.Diagnostics.Debug.WriteLine("desiredsizzeeeeeee" + panelDesiredSize);
return panelDesiredSize;
}
The children I am aading to the LinerLayout are 3 buttons, but nothing is drawn.. even if the panelDesiredSize filed is correct.. so maybe I didn't understand how it works very well. If anybody can help me would be very nice :-)
Check my answer on a previous post similar to yours: Two Pass Layout system in WPF and Silverlight
The answer is that no, you don't have to override ArrangeOverride, but what is the point of using MeasureOverride if you are not going to use ArrangeOverride?
You should call the Measure method on the child. See the example here: http://msdn.microsoft.com/en-us/library/system.windows.frameworkelement.measureoverride.aspx
You must call Measure on "elemento". It's during the Measure that Silverlight creates the UI elements declared in the elemento's template since they'll be needed to actually measure and come up with a desired size. You should then use the elemento.DesiredSize.Width and Height to come up with the desired size for your panelDesiredSize.

WPF Layout algorithm woes - control will resize, but not below some arbitrary value

I'm working on an application for a client, and one of the requirements is the ability to make appointments, and display the current week's appointments in a visual format, much like in Google Calendar's or Microsoft Office. I found a great (3 part) article on codeproject, in which he builds a "RangePanel", and composes one for each "period" (for example, the work day.) You can find part 1 here:
http://www.codeproject.com/KB/WPF/OutlookWpfCalendarPart1.aspx
The code presents, but seems to choose an arbitrary height value overall (440.04), and won't resize below that without clipping. What I mean to say, is that the window/container will resize, but it just cuts off the bottom of the control instead of recalculating the height of the range panels, and the controls in the range panels representing the appointment. It will resize and recalculate for greater values, but not less.
Code-wise, what's happening is that when you resize below that value, first the MeasureOverride is called with the correct "new height". However, by the time the ArrangeOverride method is called, it's passing the same 440.04 value as the height to arrange to.
I need to find a solution/workaround, but any information that you can provide that might direct me for things to look into would also be greatly appreciated (I understand how frustrating it is to debug code when you don't have the codebase in front of you. :) )
The code for the various Arrange and Measure functions are provided below. The CalendarView control has a CalendarViewContentPresenter, which handles several periods. Then, the periods have a CalendarPeriodContentPresenter, which handles each "block" of appointments. Finally, the RangePanel has its own implementation. (To be honest, I'm still a bit hazy on how the control works, so if my explanations are a bit hazy, the article I linked probably has a more cogent explanation. :) )
CalendarViewContentPresenter:
protected override Size ArrangeOverride(Size finalSize)
{
int columnCount = this.CalendarView.Periods.Count;
Size columnSize = new Size(finalSize.Width / columnCount, finalSize.Height);
double elementX = 0;
foreach (UIElement element in this.visualChildren)
{
element.Arrange(new Rect(new Point(elementX, 0), columnSize));
elementX = elementX + columnSize.Width;
}
return finalSize;
}
protected override Size MeasureOverride(Size constraint)
{
this.GenerateVisualChildren();
this.GenerateListViewItemVisuals();
// If it's coming back infinity, just return some value.
if (constraint.Width == Double.PositiveInfinity)
constraint.Width = 10;
if (constraint.Height == Double.PositiveInfinity)
constraint.Height = 10;
return constraint;
}
CalendarViewPeriodPersenter:
protected override Size ArrangeOverride(Size finalSize)
{
foreach (UIElement element in this.visualChildren)
{
element.Arrange(new Rect(new Point(0, 0), finalSize));
}
return finalSize;
}
protected override Size MeasureOverride(Size constraint)
{
this.GenerateVisualChildren();
return constraint;
}
RangePanel:
protected override Size ArrangeOverride(Size finalSize)
{
double containerRange = (this.Maximum - this.Minimum);
foreach (UIElement element in this.Children)
{
double begin = (double)element.GetValue(RangePanel.BeginProperty);
double end = (double)element.GetValue(RangePanel.EndProperty);
double elementRange = end - begin;
Size size = new Size();
size.Width = (Orientation == Orientation.Vertical) ? finalSize.Width : elementRange / containerRange * finalSize.Width;
size.Height = (Orientation == Orientation.Vertical) ? elementRange / containerRange * finalSize.Height : finalSize.Height;
Point location = new Point();
location.X = (Orientation == Orientation.Vertical) ? 0 : (begin - this.Minimum) / containerRange * finalSize.Width;
location.Y = (Orientation == Orientation.Vertical) ? (begin - this.Minimum) / containerRange * finalSize.Height : 0;
element.Arrange(new Rect(location, size));
}
return finalSize;
}
protected override Size MeasureOverride(Size availableSize)
{
foreach (UIElement element in this.Children)
{
element.Measure(availableSize);
}
// Constrain infinities
if (availableSize.Width == double.PositiveInfinity)
availableSize.Width = 10;
if (availableSize.Height == double.PositiveInfinity)
availableSize.Height = 10;
return availableSize;
}
So, after much hunting this morning, I found a workaround. If this gives insight and someone else finds a solution, I'll still mark yours as the accepted solution, because this just seems so hacky to me.
Basically, this morning, I pulled out the proverbial bludgeon, and decided to see what would happen if I set the MaxHeight to some value (100), and see how it would render then. It turns out, it scaled the control properly, and there was no clipping in sight! So, I figured I would try databinding the MaxHeight property to the window's height in the XAML. This didn't work, so I tried doing it from the code behind, and still had no luck. So, I went back to verify the phenomenon, and set the MaxHeight to 100 again, and ran the program. It resized to the size of the window, which was odd since hadn't I just set it to 100? Then I realized it was setting it to 100, and then the codebehind was overriding that value allowing it to scale upwards. Resizing the window even had the desired no-clipping effect... right up until you got to 100 height. So, I set the MaxHeight to zero, and that apparently goes inside the control and breaks some psuedo-minimum-height property, and then the window allows it to expand up to it's full size.
It's extremely hacky, but it works for now. =/ It seems so odd to me that I enabled the shrinking of a control by setting the MaxHeight property. ><

WPF RichTextBox with no width set

I have the following XAML code:
<Window x:Class="RichText_Wrapping.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window1">
<Grid>
<RichTextBox Height="100" Margin="2" Name="richTextBox1">
<FlowDocument>
<Paragraph>
This is a RichTextBox - if you don't specify a width, the text appears in a single column
</Paragraph>
</FlowDocument>
</RichTextBox>
</Grid>
... If you create this window in XAML, you can see that when you don't specify a width for the window, it wraps the text in a single column, one letter at a time. Is there something I'm missing? If it's a known deficiency in the control, is there any workaround?
This is a confirmed bug with the WPF RichTextBox. To fix it, Bind the PageWidth of the FlowDocument to the RichTextBox width, i.e.
<RichTextBox Name="rtb">
<FlowDocument Name="rtbFlowDoc" PageWidth="{Binding ElementName=rtb, Path=ActualWidth}" />
</RichTextBox>
EDIT:
Give the FlowDocument a name so that you can access it in the code behind and never new the flow document in codebehind.
Try binding the FlowDocument's width (one way) to the width of the container RichTextBox.
Worked for me...
The approach in this article worked for me:
WPF RichTextBox doesn't provide the functionality to adjust its width
to the text. As far as I know, RichTextBox use a FlowDocumentView in
its visual tree to render the Flowdocument. It will take the available
space to render its content, so it won't adjust its size to the
content. Since this is an internal class, it seems we cannot override
the layout process to let a RichTextBox to adjust its size to the
text.
Therefore, I think your approach is in the right direction.
Unfortunelately, based on my research, there is no straightforward way
to measure the size of the rendered text in a RichTextBox.
There is a workaround we can try. We can loop through the flowdocument
in RichTextBox recursively to retrieve all Run and Paragraph objects.
Then we convert them into FormattedText to get the size.
This article demonstrates how to convert a FlowDocument to
FormattedText. I also write a simple sample using the
FlowDocumentExtensions class in that article.
public Window2()
{
InitializeComponent();
StackPanel layoutRoot = new StackPanel();
RichTextBox myRichTextBox = new RichTextBox() { Width=20};
this.Content = layoutRoot;
layoutRoot.Children.Add(myRichTextBox);
myRichTextBox.Focus();
myRichTextBox.TextChanged += new TextChangedEventHandler((o,e)=>myRichTextBox.Width=myRichTextBox.Document.GetFormattedText().WidthIncludingTrailingWhitespace+20);
}
public static class FlowDocumentExtensions
{
private static IEnumerable<TextElement> GetRunsAndParagraphs(FlowDocument doc)
{
for (TextPointer position = doc.ContentStart;
position != null && position.CompareTo(doc.ContentEnd) <= 0;
position = position.GetNextContextPosition(LogicalDirection.Forward))
{
if (position.GetPointerContext(LogicalDirection.Forward) == TextPointerContext.ElementEnd)
{
Run run = position.Parent as Run;
if (run != null)
{
yield return run;
}
else
{
Paragraph para = position.Parent as Paragraph;
if (para != null)
{
yield return para;
}
}
}
}
}
public static FormattedText GetFormattedText(this FlowDocument doc)
{
if (doc == null)
{
throw new ArgumentNullException("doc");
}
FormattedText output = new FormattedText(
GetText(doc),
CultureInfo.CurrentCulture,
doc.FlowDirection,
new Typeface(doc.FontFamily, doc.FontStyle, doc.FontWeight, doc.FontStretch),
doc.FontSize,
doc.Foreground);
int offset = 0;
foreach (TextElement el in GetRunsAndParagraphs(doc))
{
Run run = el as Run;
if (run != null)
{
int count = run.Text.Length;
output.SetFontFamily(run.FontFamily, offset, count);
output.SetFontStyle(run.FontStyle, offset, count);
output.SetFontWeight(run.FontWeight, offset, count);
output.SetFontSize(run.FontSize, offset, count);
output.SetForegroundBrush(run.Foreground, offset, count);
output.SetFontStretch(run.FontStretch, offset, count);
output.SetTextDecorations(run.TextDecorations, offset, count);
offset += count;
}
else
{
offset += Environment.NewLine.Length;
}
}
return output;
}
private static string GetText(FlowDocument doc)
{
StringBuilder sb = new StringBuilder();
foreach (TextElement el in GetRunsAndParagraphs(doc))
{
Run run = el as Run;
sb.Append(run == null ? Environment.NewLine : run.Text);
}
return sb.ToString();
}
}
I copy pasted your code and its not in a single column, Do you have a width somewhere that is small? Maybe defined on the code behind for instance.
I noticed that I only had this issue when my default ScrollViewer style explicitly set HorizontalScrollBarVisibility=Hidden. Removing this setter (default value is Hidden anyway) fixed the single column issue for me in my RichTextBox.
Just for the record as I think this thread is missing some explanations as per the why: RichTextBox MeasureOverride implementation is like that. I won't call that a bug, maybe just a poor design behavior justified by the fact that just like mentioned above the FlowDocument is not cheap to measure due to its complexity. Bottom line, avoid unlimited Width constraint by binding MinWidth or wrap it in a limiting container.
/// <summary>
/// Measurement override. Implement your size-to-content logic here.
/// </summary>
/// <param name="constraint">
/// Sizing constraint.
/// </param>
protected override Size MeasureOverride(Size constraint)
{
if (constraint.Width == Double.PositiveInfinity)
{
// If we're sized to infinity, we won't behave the same way TextBox does under
// the same conditions. So, we fake it.
constraint.Width = this.MinWidth;
}
return base.MeasureOverride(constraint);
}

Resources