Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Length of string that will fit in a specific width

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

like image 976
Ian Avatar asked Dec 27 '10 23:12

Ian


2 Answers

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;
}
like image 187
BrokenGlass Avatar answered Nov 07 '22 03:11

BrokenGlass


@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;
}
like image 37
Rand Scullard Avatar answered Nov 07 '22 01:11

Rand Scullard