How to Measure Rendered Size of a String?

I’m trying to calculate the size a speech bubble needs to have to contain the (localized) text in it.

Every other engine I’ve used had something like a MeasureString() method, but I can’t seem to find an exposed function to do this in Unreal Engine

  • HUD::GetTextSize() does this (if you have a HUD)
  • There’s FCanvas::TextSize() which is used by the HUD but isn’t public/blueprintable,
  • an entire FontMeasureService with a Measure() method,
  • a ComputeTextSize() method

Is there a way to reach one of these methods from Blueprint (or maybe C++)?
Some of the engine code does FSlateApplication::Get().GetRenderer()->GetFontMeasureService()->Measure() –

Well, first of, this would highly depend on font in use.
are you aware of the types of fonts?
monospaced vs proportional?

Assuming the worse case you would be using a proportional font.

Then you need to account for font size - is it fixed, or scaled at viewport?

Assume the worse, its scaled by an unknown factor.

Assume a much worse case - the font size can be changed by the user at will.

Ergo, you’ll never actually know programmatically what size this text is unless you control/read all the variables.

You need to take those variables over from the engine (they exist somewhere, you just need to find where. They may not be exposed to blueprint).
and with those you can then affect your own calculations to determine the size of the container.

I believe you can break down the UMG system which already sort of does this with fitting to content button to get everything you need in one go.
To be able to do what it does it has to use all the variables you need that I mentioned and probably more.

I would look into extracting the function in its entirety and exposing it to blueprint - you’ll probably hit some roadblocks with not having access to the font type in use.
hard to say offhand. Definitely worth a look.

there are plugins that do this, so in would assume it possible.

Maybe you can lock your variables - all of them - with a custom monospaced font… this will give you easy math and 100% reliability…

I’m aware :slight_smile: - In this case I have the UMG widget and its [FONT=courier new]UFont object / [FONT=courier new]FSlateFontInfo structure and the canvas size is an absolutely static, unscaled 280 x 120 pixels.

As I discovered in the sources, [FONT=courier new]FSlateFontMeasure::MeasureStringInternal() does 100% of everything I need. It’s also private in C++ and not exposed to Blueprint.

Because I don’t want to run on modified engine sources, I’ve now written my own method and put it in a Blueprint function library. If anyone runs into the same issue, here’s a code snippet that calculates the unscaled text size:


[FONT=courier new]// --------------------------------------------------------------------------------------------- //

FVector2D UFontHelper::MeasureString(
  UTextBlock *textBlock,
  const FString &text
) {
  TSharedRef<FSlateFontMeasure> fontMeasureService = (
    FSlateApplication::Get().GetRenderer()->GetFontMeasureService()
  );

  return fontMeasureService->Measure(text, textBlock->Font);
}

// --------------------------------------------------------------------------------------------- //

FVector2D UFontHelper::MeasureWrappedString(
  UTextBlock *textBlock,
  const FString &text,
  float wrapWidth
) {
  FVector2D size(0.0f, 0.0f);

  // Everything we do here duplicates what FSlateFontMeasure::MeasureStringInternal()
  // already does. Sadly, the existing method is private and there's no exposed method
  // that happens to call it in a way we can work with...

  // Scan the entire string, keeping track of where lines begin and checking every time
  // a whitespace is encountered how long the line would be when wrapped there.
  // We could do this in a more fancy way with binary search and by guessing lengths,
  // but it's typically done just one when new text is displayed, so this is good enough.
  {
    TSharedRef<FSlateFontMeasure> fontMeasureService = (
      FSlateApplication::Get().GetRenderer()->GetFontMeasureService()
    );

    int32 lineCount = 1;
    bool lastWasWhitespace = true;

    bool foundLineStartIndex = false;
    int32 lineStartIndex = 0;

    int32 lastGoodWrapIndex = -1;
    float lastGoodWrapWidth = 0.0f;

    // Scanning loop, steps through the string character by character
    int32 textLength = text.Len();
    for(int32 index = 0; index < textLength; ++index) {

      // Check if the current character is a whitespace character (and thus, the line
      // can be broken at this point)
      TCHAR character = text[index];
      bool isWhitespace = (
        (character == TEXT(' ')) || (character == TEXT('	')) || (character == TEXT('
'))
      );

      // If we have a line start index (meaning there was a non-whitespace character),
      // do the line break checking
      if(foundLineStartIndex) {

        // Don't re-check line breaks on consecutive whitespaces
        if(isWhitespace && lastWasWhitespace) {
          continue;
        }
        lastWasWhitespace = isWhitespace;

        // If this is no whitespace, we can't wrap here, so continue scanning
        if(!isWhitespace) {
          continue;
        }

        // Measure the line up until the whitespace we just encountered
        FVector2D potentialLineSize = fontMeasureService->Measure(
          text, lineStartIndex, index - 1, textBlock->Font, false
        );

        // If it still fits in the line, remember this as the most recent good wrap ppoint
        if(potentialLineSize.X < wrapWidth) {
          lastGoodWrapIndex = index;
          lastGoodWrapWidth = potentialLineSize.X;
        } else {
          ++lineCount;

          if(lastGoodWrapIndex == -1) { // First whitespace and it's already too long...
            size.X = FMath::Max(size.X, potentialLineSize.X);
          } else { // Phew... we have a good wrapping position remembered
            size.X = FMath::Max(size.X, lastGoodWrapWidth);
          }

          // Reset all trackers to scan for a new line from here
          lastGoodWrapIndex = -1;
          lineStartIndex = index;
          foundLineStartIndex = false;
        }

      } else if(!isWhitespace) {
        foundLineStartIndex = true; // The first non-whitespace character marks the line start
        lineStartIndex = index;
      }

    } // for

    // If there are characters remaining on the last line, measure them, too
    // (we also know it doesn't end in a space/newline because otherwise this
    // property would have a value of false, thus this final check is really basic)
    if(foundLineStartIndex) {
      FVector2D finalLineSize = fontMeasureService->Measure(
        text, lineStartIndex, textLength - 1, textBlock->Font, false
      );
      size.X = FMath::Max(size.X, finalLineSize.X);
    }

    size.Y = (
      static_cast<float>(fontMeasureService->GetMaxCharacterHeight(textBlock->Font)) * lineCount
    );
  }

  return size;
}

// --------------------------------------------------------------------------------------------- //


2 Likes

I want to say I really appreciate cygon’s code up there, as it helped me a lot. However, unless I’m wrong, there were a couple bugs in it: it didn’t account for the final word exceeding the text box width (as there is no whitespace character at the end for a final check), and it didn’t properly start the new line index once a break was found. This code could probably be refactored, but this worked for me:


FVector2D UFontHelper::MeasureWrappedString(
    UTextBlock *textBlock,
    const FString &text,
    float wrapWidth,
    float linePercentageHeight
) {
    FVector2D size(0.0f, 0.0f);

    // Everything we do here duplicates what FSlateFontMeasure::MeasureStringInternal()
    // already does. Sadly, the existing method is private and there's no exposed method
    // that happens to call it in a way we can work with...

    // Scan the entire string, keeping track of where lines begin and checking every time
    // a whitespace is encountered how long the line would be when wrapped there.
    // We could do this in a more fancy way with binary search and by guessing lengths,
    // but it's typically done just one when new text is displayed, so this is good enough.
    {
        TSharedRef<FSlateFontMeasure> fontMeasureService = (
            FSlateApplication::Get().GetRenderer()->GetFontMeasureService()
            );

        int32 lineCount = 1;
        bool lastWasWhitespace = true;

        bool foundLineStartIndex = false;
        int32 lineStartIndex = 0;

        int32 lastGoodWrapIndex = -1;
        float lastGoodWrapWidth = 0.0f;

        // Scanning loop, steps through the string character by character
        int32 textLength = text.Len();
        for (int32 index = 0; index < textLength; ++index) {

            // Check if the current character is a whitespace character (and thus, the line
            // can be broken at this point)
            TCHAR character = text[index];
            bool isWhitespace = (
                (character == TEXT(' ')) || (character == TEXT('	')) || (character == TEXT('
'))
                );

            // If we have a line start index (meaning there was a non-whitespace character),
            // do the line break checking
            if (foundLineStartIndex) {

                // Don't re-check line breaks on consecutive whitespaces
                if (isWhitespace && lastWasWhitespace) {
                    continue;
                }
                lastWasWhitespace = isWhitespace;

                // If this is no whitespace, we can't wrap here, so continue scanning. Must include exception for end of text, however.
                if (!isWhitespace && index < textLength - 1) {
                    continue;
                }

                // Measure the line up until the whitespace we just encountered
                FVector2D potentialLineSize = fontMeasureService->Measure(
                    text, lineStartIndex, !isWhitespace ? index : index - 1, textBlock->Font, false
                );

                // If it still fits in the line, remember this as the most recent good wrap point
                if (potentialLineSize.X < wrapWidth) {
                    lastGoodWrapIndex = index;
                    lastGoodWrapWidth = potentialLineSize.X;

                }
                else {
                    ++lineCount;

                    if (lastGoodWrapIndex == -1) { // First whitespace and it's already too long...
                        size.X = FMath::Max(size.X, potentialLineSize.X);
                    }
                    else { // Phew... we have a good wrapping position remembered
                        size.X = FMath::Max(size.X, lastGoodWrapWidth);
                    }

                    // Reset all trackers to scan for a new line from here
                    lineStartIndex = lastGoodWrapIndex + 1;
                    lastGoodWrapIndex = -1;
                }

            }
            else if (!isWhitespace) {
                foundLineStartIndex = true; // The first non-whitespace character marks the line start
                lineStartIndex = index;
            }

        }

        // If there are characters remaining on the last line, measure them, too
        // (we also know it doesn't end in a space/newline because otherwise this
        // property would have a value of false, thus this final check is really basic)
        if (foundLineStartIndex) {
            FVector2D finalLineSize = fontMeasureService->Measure(
                text, lineStartIndex, textLength - 1, textBlock->Font, false
            );
            size.X = FMath::Max(size.X, finalLineSize.X);
        }

        size.Y = (
            static_cast<float>(fontMeasureService->GetMaxCharacterHeight(textBlock->Font)) * (lineCount - ((1 - linePercentageHeight) * (lineCount - 1)))
            );
    }

    return size;
}

Hi

first of all thank you for this code! I’m trying to get this into my own blueprint function but I am unsure what includes I need to get it working. I’m getting this:

1>MeasureTextSize.cpp.obj : error LNK2019: unresolved external symbol "__declspec(dllimport) public: class TSharedRef<class FSlateFontMeasure,0> __cdecl FSlateRenderer::GetFontMeasureService(void)const " (_imp?GetFontMeasureService@FSlateRenderer@@QEBA?AV?$TSharedRef@VFSlateFontMeasure@@$0A@@@XZ) referenced in function “private: struct FVector2D __cdecl UMeasureTextSize::MeasureWrappedString(class UTextBlock *,class FString const &,float,float)” (?MeasureWrappedString@UMeasureTextSize@@AEAA?AUFVector2D@@PEAVUTextBlock@@AEBVFString@@MM@Z)

1>MeasureTextSize.cpp.obj : error LNK2019: unresolved external symbol “__declspec(dllimport) public: static class FSlateApplication & __cdecl FSlateApplication::Get(void)” (_imp?Get@FSlateApplication@@SAAEAV1@XZ) referenced in function “private: struct FVector2D __cdecl UMeasureTextSize::MeasureWrappedString(class UTextBlock *,class FString const &,float,float)” (?MeasureWrappedString@UMeasureTextSize@@AEAA?AUFVector2D@@PEAVUTextBlock@@AEBVFString@@MM@Z)

MeasureTextSize.cpp.obj : error LNK2019: unresolved external symbol "__declspec(dllimport) public: unsigned short __cdecl FSlateFontMeasure::GetMaxCharacterHeight(struct FSlateFontInfo const &,float)const " (_imp?GetMaxCharacterHeight@FSlateFontMeasure@@QEBAGAEBUFSlateFontInfo@@M@Z) referenced in function “private: struct FVector2D __cdecl UMeasureTextSize::MeasureWrappedString(class UTextBlock *,class FString const &,float,float)” (?MeasureWrappedString@UMeasureTextSize@@AEAA?AUFVector2D@@PEAVUTextBlock@@AEBVFString@@MM@Z)

1>MeasureTextSize.gen.cpp.obj : error LNK2019: unresolved external symbol “__declspec(dllimport) class UClass * __cdecl Z_Construct_UClass_UTextBlock_NoRegister(void)” (_imp?Z_Construct_UClass_UTextBlock_NoRegister@@YAPEAVUClass@@XZ) referenced in function “void __cdecl `dynamic initializer for ‘public: static struct UE4CodeGen_Private::FObjectPropertyParams const Z_Construct_UFunction_UMeasureTextSize_MeasureWrappedString_Statics::NewProp_textBlock’'(void)” (??__E?NewProp_textBlock@Z_Construct_UFunction_UMeasureTextSize_MeasureWrappedString_Statics@@2UFObjectPropertyParams@UE4CodeGen_Private@@B@@YAXXZ)

Appreciate any help!

You need to add to the following file:
YourProjectName.Build.cs
the following line to the constructor:
PrivateDependencyModuleNames.AddRange(new string[] { “UMG”, “Slate”, “SlateCore” });

This code is very useful but setting lastGoodWrapIndex = -1 can lead to issues when the size of the box is smaller than the size of the element. I assigned index instead and it went fine.