11

I'm sure I'm missing something obvious, I have an area in which I intend to draw text. I know its (the area) height and width. I wish to know how many characters/Words will fit in the width, characters preferably. Second question, If the line is too long I'll want to draw a second line, so I guess I need to get the height of the text as well, including what ever it considers the right vertical padding?

I'd also rather like to know the inverse, i.e. how many characters I can fit in a specific width.

I assume the fact that WPF isn't constrained to pixels will have some bearing on the answer?

Ultimately I'm planning on wrapping text around irregular shaped images embedded in the text.

Any pointers in the right direction would be great.

Thanks

Robaticus
  • 22,857
  • 5
  • 54
  • 63
Ian
  • 4,885
  • 4
  • 43
  • 65

3 Answers3

19

For WPF you can use the FormattedText class to calculate how much width a given text string will use - it will depend on the actual text.

Example:

FormattedText formattedText = new FormattedText("Hello Stackoverflow", 
                                                System.Globalization.CultureInfo.GetCultureInfo("en-us"), 
                                                FlowDirection.LeftToRight, 
                                                new Typeface("Arial"), FontSize =14, Brushes.Black);
double textWidth = formattedText.Width;

Getting a sub string for a given width (simplified):

string text  = GetSubStringForWidth("Hello Stackoverflow", 55);
...
public string GetSubStringForWidth(string text, double width)
{
    if (width <= 0)
        return "";

    int length = text.Length;
    string testString;

    while(true)//0 length string will always fit
    {
        testString = text.Substring(0, length);
        FormattedText formattedText = new FormattedText(testString, 
                                                        CultureInfo.GetCultureInfo("en-us"), 
                                                        FlowDirection.LeftToRight, 
                                                        new Typeface("Arial"), 
                                                        FontSize = 14, 
                                                        Brushes.Black);
        if(formattedText.Width <= width)
            break;
        else
            length--;
    }
    return testString;
}
BrokenGlass
  • 158,293
  • 28
  • 286
  • 335
  • 1
    Excellent start, do we have any idea how to go about doing the opposite or do I need to apply some sort of divide and conquer strategy? i.e. Measure "Am I too long to fit in this space" then if > available width, measure "Am I too long" and if it's too short, try something in between? – Ian Dec 28 '10 at 13:27
  • @Ian: it should be straightforward to write a method that returns the number of characters that fit a given width, I'd start with something like my edit above (obviously simplified), and if performance is an issue go to something more complicated - I haven't done any measurements. – BrokenGlass Dec 28 '10 at 14:21
  • Yeah. How widde exactly is every char of an unknown string? Impssible, can only be "assumed" (Averaged). – TomTom Dec 28 '10 at 14:30
  • @Tom Tom: why the down vote? if you check the solution you will see it does depend on the actual text, I made that clear from the beginning. – BrokenGlass Dec 28 '10 at 14:35
  • the iterative algorithm is complete overkill, guys. all you need to do is calculate the percentage how much the string is too small or too large for the given space and scale the fontsize with that factor – henon May 05 '14 at 10:58
3

@BrokenGlass's answer is great, but depending on the characteristics of your application, you may find you get better performance with a binary search. If the majority of your strings fit in the available width, or generally only need to be trimmed by a character or two, then a linear search is best. However, if you have a lot of long strings that will be severely truncated, the following binary search will work well.

Note that both availableWidth and fontSize are specified in device-independent units (1/96ths of an inch). Also, use the TextFormattingMode that matches the way you draw your text.

public static string TruncateTextToFitAvailableWidth(
    string text, 
    double availableWidth, 
    string fontName, 
    double fontSize)
{
    if(availableWidth <= 0)
        return string.Empty;

    Typeface typeface = new Typeface(fontName);

    int foundCharIndex = BinarySearch(
        text.Length,
        availableWidth,
        predicate: (idxValue1, value2) =>
        {
            FormattedText ft = new FormattedText(
                text.Substring(0, idxValue1 + 1), 
                CultureInfo.CurrentCulture, 
                FlowDirection.LeftToRight, 
                typeface, 
                fontSize, 
                Brushes.Black,
                numberSubstitution: null,
                textFormattingMode: TextFormattingMode.Ideal);

            return ft.WidthIncludingTrailingWhitespace.CompareTo(value2);
        });

    int numChars = (foundCharIndex < 0) ? ~foundCharIndex : foundCharIndex + 1;

    return text.Substring(0, numChars);
}

/**
<summary>
See <see cref="T:System.Array.BinarySearch"/>. This implementation is exactly the same,
except that it is not bound to any specific type of collection. The behavior of the
supplied predicate should match that of the T.Compare method (for example, 
<see cref="T:System.String.Compare"/>).
</summary>
*/      
public static int BinarySearch<T>(
    int               length,
    T                 value,
    Func<int, T, int> predicate) // idxValue1, value2, compareResult
{
    return BinarySearch(0, length, value, predicate);
}

public static int BinarySearch<T>(
    int               index,
    int               length,
    T                 value,
    Func<int, T, int> predicate)
{
    int lo = index;
    int hi = (index + length) - 1;

    while(lo <= hi)
    {
        int mid = lo + ((hi - lo) / 2);

        int compareResult = predicate(mid, value);

        if(compareResult == 0)
            return mid;
        else if(compareResult < 0)
            lo = mid + 1;
        else
            hi = mid - 1;
    }

    return ~lo;
}
Rand Scullard
  • 3,145
  • 1
  • 22
  • 18
  • your answer is over-complicated, there is a much simpler solution. all you need to do is calculate the percentage how much the string is too small or too large for the given space and scale the font size with that factor. – henon May 05 '14 at 11:00
  • 1
    Changing the font size is not acceptable for either the OP or our application. – Rand Scullard May 05 '14 at 14:54
-1

This is impossible because characters have different length, for example W is much wider than i (unless you're using a font like Courier New).

Ilya Kogan
  • 21,995
  • 15
  • 85
  • 141
  • It isn't impossible. The whole of typography depends on being able to do this. Historically we've had things like MeasureText and MeasureString, and the Measure event on items. Think about it, look at the web page you're reading, notice how the text fits into the space available. – Ian Dec 28 '10 at 13:25
  • Ah,´but typography does not go like you do. THis is like building ahouse from the roof. You dont know how many chars fit into a width, you know how wide a SPECIFIC TEXT is. WIthout knowiing the text, calculating the width is not possible. Typogrpahy does not start with an "unknown random text" to be measured. – TomTom Dec 28 '10 at 14:29
  • Sorry, I didn't mean to give that impression. I know the text, and I know width of the line it has to go in. I know the font, the font size and so forth. So I have all the bits I need to be able to calculate it, I just didn't know how :) – Ian Dec 28 '10 at 16:45