Announcement

Collapse
No announcement yet.

How to Measure Rendered Size of a String?

Collapse
X
 
  • Filter
  • Time
  • Show
Clear All
new posts

    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() --

    #2
    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...

    Comment


      #3
      I'm aware - In this case I have the UMG widget and its UFont object / FSlateFontInfo structure and the canvas size is an absolutely static, unscaled 280 x 120 pixels.

      As I discovered in the sources, 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:

      Code:
      // --------------------------------------------------------------------------------------------- //
      
      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('\t')) || (character == TEXT('\n'))
            );
      
            // 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;
      }
      
      // --------------------------------------------------------------------------------------------- //

      Comment

      Working...
      X