Rendering Text with SDL_ttf

Learn to render and manipulate text in SDL2 applications using the official SDL_ttf extension

Ryan McCombe
Updated

In this lesson, we'll see how we can render text within our programs. We'll use the official SDL_ttf extension we installed earlier in the course.

We'll build upon the concepts we introduced in the previous chapters. Our main.cpp looks like below.

The key thing to note is that we have created a Text class, and instantiated an object from it called TextExample. This object is being asked to Render() onto the window surface every frame:

#include <iostream>
#include <SDL.h>
#include "Text.h"

class Window {/*...*/}; int main(int argc, char** argv) { SDL_Init(SDL_INIT_VIDEO); SDL_Event Event; bool shouldQuit{false}; Window GameWindow; Text TextExample{"Hello World"}; while (!shouldQuit) { while (SDL_PollEvent(&Event)) { if (Event.type == SDL_QUIT) { shouldQuit = true; } } GameWindow.Render(); TextExample.Render(GameWindow.GetSurface()); GameWindow.Update(); } SDL_Quit(); return 0; }

Our Text class currently looks like this, but we'll expand it throughout this lesson:

#pragma once
#include <SDL.h>
#include <SDL_ttf.h>

class Text {
 public:
  Text(std::string Content) {}

  void Render(SDL_Surface* DestinationSurface) {
    // ...
  }
};

We will also need a TrueType (.ttf) file stored in the same location as our executable. The code examples and screenshots in this lesson use Roboto-Medium.ttf, available from Google Fonts.

Initializing and Quitting SDL_ttf

To use SDL_ttf in our application, we #include the SDL_ttf.h header file. We then call TTF_Init() to initialize the library, and TTF_Quit() to close it down:

#include <SDL.h>
#include <SDL_ttf.h>
#include "Text.h"

class Window {/*...*/}; int main(int argc, char** argv) { SDL_Init(SDL_INIT_VIDEO); TTF_Init(); SDL_Event Event; bool shouldQuit{false}; Window GameWindow; Text Example{"Hello World"};
while (!shouldQuit) {/*...*/} TTF_Quit(); SDL_Quit(); return 0; }

Error Checking

TTF_Init() returns 0 if it succeeds, and a negative error code if it fails. We can check for this error state, and call SDL_GetError() for an explanation:

if (TTF_Init() < 0) {
  std::cout << "Error initializing SDL_ttf: "
    << SDL_GetError();
}

Loading and Freeing Fonts

To create text in our application, we first need to load a font. To do this, we call TTF_OpenFont(), passing the path to our font file, and the font size we want to use:

TTF_OpenFont("Roboto-Medium.ttf", 50);

This function creates a TTF_Font object and returns a pointer to it. To prevent memory leaks, we pass this pointer to TTF_CloseFont() when we no longer need it.

To simplify memory management, we'll also prevent our Text objects from being copied by deleting their copy constructor and copy assignment operator. Our updated Text class looks like this:

#pragma once
#include <SDL.h>
#include <SDL_ttf.h>

class Text {
 public:
  Text(std::string Content) : Font {
    TTF_OpenFont("Roboto-Medium.ttf", 50)
  } {

  }

  void Render(SDL_Surface* DestinationSurface) {

  }

  ~Text() {
    TTF_CloseFont(Font);
  }
  Text(const Text&) = delete;
  Text& operator=(const Text&) = delete;

private:
  TTF_Font* Font;
};

Checking Initialization using TTF_WasInit()

If we call TTF_CloseFont() when SDL_ttf is not initialized, our program can crash. Issues like this are most common during the "cleanup" phase of our application, where we're trying to shut everything down cleanly.

In our example, the TextExample object's destructor isn't called until main() ends. However, before main() ends, we call TTF_Quit(), meaning by the time ~TextExample() calls TTF_CloseFont(), SDL_ttf has already been shut down.

We can test for this scenario using TTF_WasInit(). The TTF_WasInit() function returns a positive integer if SDL_ttf is currently initialized, so we can make our class more robust like this:

// Text.h
class Text {
  // ...

  ~Text() {
    if (TTF_WasInit()) {
      TTF_CloseFont(Font);
    }
  }
  
  // ...
};

Error Checking

If TTF_OpenFont() failed, it will return a nullptr. We can check for this, and log out the error for an explanation:

// Text.h
class Text {
 public:
  Text(std::string Content) : Font {
    TTF_OpenFont("Roboto-Medium.ttf", 50)
  } {
    if (!Font) {
      std::cout << "Error loading font: "
        << SDL_GetError();
    }
  }
  
  // ...
};

Rendering Text

From a high level, the process of rendering text is very similar to rendering images:

  • We generate an SDL_Surface containing the pixel data representing our text
  • We blit that surface onto another surface, using functions like SDL_BlitSurface()

There are many options we can use to generate our surface. We'll explain their differences in the next lesson. For now, we'll use TTF_RenderUTF8_Blended(). This function requires three arguments:

  • The font we want to use, as an SDL_Font pointer
  • The text we want to render, as a C-style string
  • The color we want to use, as an SDL_Color

For example:

TTF_RenderUTF8_Blended(
  Font,
  "Hello World",
  SDL_Color{255, 255, 255} // White
)

SDL will render our text onto SDL_Surface, containing the pixel data we need to display the text. It will return a pointer to that surface, or a nullptr if the process failed. We can therefore check for errors in the usual way:

SDL_Surface* TextSurface{
  TTF_RenderUTF8_Blended(
    Font, "Hello World", {255, 255, 255}
  )
};

if (!TextSurface) {
  std::cout << "Error creating TextSurface: "
    << SDL_GetError();
}

Let's add this capability to our class as a private CreateSurface() method. We'll additionally:

  • Add a TextSurface member to store a pointer to the generated surface
  • Call SDL_FreeSurface() from the destructor to release the surface when we no longer need it.
  • Call SDL_FreeSurface() before we replace the existing TextSurface with a new one. This is not currently necessary as we're only calling CreateSurface() once per object lifecycle, but we'll expand our class later.
  • Call CreateSurface() from our constructor to ensure our Text objects are initialized with a TextSurface
// Text.h
class Text {
 public:
  Text(std::string Content) : Font {
    TTF_OpenFont("Roboto-Medium.ttf", 50)
  } {
    if (!Font) {
      std::cout << "Error loading font: "
        << SDL_GetError();
    }
    CreateSurface(Content);
  }

  void Render(SDL_Surface* DestinationSurface) {

  }

  ~Text() {
    SDL_FreeSurface(TextSurface);
    TTF_CloseFont(Font);
  }
  Text(const Text&) = delete;
  Text& operator=(const Text&) = delete;

private:
  void CreateSurface(std::string Content) {
    SDL_Surface* newSurface = TTF_RenderUTF8_Blended(
      Font, Content.c_str(), {255, 255, 255}
    );
    if (newSurface) {
      SDL_FreeSurface(TextSurface);
      TextSurface = newSurface;
    } else {
      std::cout << "Error creating TextSurface: "
        << SDL_GetError() << '\n';
    }
  }

  TTF_Font* Font{nullptr};
  SDL_Surface* TextSurface{nullptr};
};

Surface Blitting

After creating an SDL_Surface containing our rendered text, we can display it on the screen using a process called blitting. Blitting involves copying the pixel data from one surface (our text surface) to another (the window surface).

We covered blitting in much more detail when working with images earlier in the chapter.

In our current setup, the Text::Render() method receives the window surface as a parameter. By blitting our TextSurface onto this window surface, we draw the text onto the window:

// Text.h
class Text {
 public:
  // ...

  void Render(SDL_Surface* DestinationSurface) {
    SDL_BlitSurface(
      TextSurface, nullptr,
      DestinationSurface, nullptr
    );
  }
  
  // ...
};

Source Rectangle

As with any call to SDL_BlitSurface(), we can pass pointers to the 2nd and 4th arguments. These are pointers to SDL_Rect objects representing the source and destination rectangles respectively.

When working with text, it is fairly unusual that we would specify a source rectangle. The text we generated fills the entire SDL_Surface created by SDL_ttf. However, we can still crop it if we have some reason to:

// Text.h
class Text {
 public:
  // ...

  void Render(SDL_Surface* DestinationSurface) {
    SDL_BlitSurface(
      TextSurface, &SourceRectangle,
      DestinationSurface, nullptr
    );
  }
  
  // ...

private:
  void CreateSurface(std::string Content) {
    SDL_Surface* newSurface = TTF_RenderUTF8_Blended(
      Font, Content.c_str(), {255, 255, 255}
    );
    if (newSurface) {
      SDL_FreeSurface(TextSurface);
      TextSurface = newSurface;
    } else {
      std::cout << "Error creating TextSurface: "
        << SDL_GetError() << '\n';
    }
    
    SourceRectangle.w = TextSurface->w - 50;
    SourceRectangle.h = TextSurface->h - 20;
  }

  TTF_Font* Font{nullptr};
  SDL_Surface* TextSurface{nullptr};
  SDL_Rect SourceRectangle{0, 0, 0, 0};
};

Destination Rectangle

More commonly, we'll want to provide a destination rectangle with x and y values, controlling where the text is placed within the destination surface:

// Text.h
class Text {
 public:
  // ...
  void Render(SDL_Surface* DestinationSurface) {
    SDL_BlitSurface(
      TextSurface, nullptr,
      DestinationSurface, &DestinationRectangle
    );
  }

private:
  // ...
  SDL_Rect DestinationRectangle{50, 50, 0, 0}; 
};

As we covered in our image rendering lessons, SDL_BlitSurface() will update the w and h values of this rectangle with the size of our output within the destination surface.

If these values are smaller than the width and height of our source surface (or source rectangle, if we're using one) that indicates that our text did not fit within the destination surface at the location we specified:

// Text.h
class Text {
 public:
  Text(std::string Content) : Font {
    TTF_OpenFont("Roboto-Medium.ttf", 150)
  } {
    if (!Font) {
      std::cout << "Error loading font: "
        << SDL_GetError();
    }
    CreateSurface(Content);
  }

  void Render(SDL_Surface* DestinationSurface) {
    SDL_BlitSurface(
      TextSurface, nullptr,
      DestinationSurface, &DestinationRectangle
    );
    if (DestinationRectangle.w < TextSurface->w) {
      std::cout << "Clipped horizontally\n";
    }
    if (DestinationRectangle.h < TextSurface->h) {
      std::cout << "Clipped vertically\n";
    }
  }

  // ...
};
Clipped horizontally
Clipped horizontally
Clipped horizontally
...

Scaling Text

As with any surface, we can add scaling to the blitting process using SDL_BlitScaled(). However, we should avoid doing this where possible. Scaling an image causes a loss of quality, and this can be particularly noticeable with text.

Let's temporarily update Render() and CreateSurface() to increase the size of our text using scaled blitting, and note the quality loss:

// Text.h
class Text {
 public:
  Text(std::string Content) : Font {
    TTF_OpenFont("Roboto-Medium.ttf", 25)
  } {
    if (!Font) {
      std::cout << "Error loading font: "
        << SDL_GetError();
    }
    CreateSurface(Content);
  }

  void Render(SDL_Surface* DestinationSurface) {
    SDL_BlitScaled(
      TextSurface, nullptr,
      DestinationSurface, &DestinationRectangle
    );
  }

  ~Text() {
    SDL_FreeSurface(TextSurface);
    if (TTF_WasInit()) {
      TTF_CloseFont(Font);
    }
  }
  Text(const Text&) = delete;
  Text& operator=(const Text&) = delete;

private:
  void CreateSurface(std::string Content) {
    SDL_Surface* newSurface = TTF_RenderUTF8_Blended(
      Font, Content.c_str(), {255, 255, 255}
    );
    if (newSurface) {
      SDL_FreeSurface(TextSurface);
      TextSurface = newSurface;
    } else {
      std::cout << "Error creating TextSurface: "
        << SDL_GetError() << '\n';
    }
    
    DestinationRectangle.w = TextSurface->w * 4;
    DestinationRectangle.h = TextSurface->h * 4;
  }

  TTF_Font* Font{nullptr};
  SDL_Surface* TextSurface{nullptr};
  SDL_Rect DestinationRectangle{50, 50, 0, 0};
};

Instead, we can just render the text at the correct size we want, by setting the font size. Our constructor is currently setting the font size to 20. Let's change that to be a constructor argument:

// Text.h
class Text {
 public:
  Text(std::string Content, int Size = 25)
  : Font {
    TTF_OpenFont("Roboto-Medium.ttf", Size)
  } {
    if (!Font) {
      std::cout << "Error loading font: "
        << SDL_GetError();
    }
    CreateSurface(Content);
  }
  // ...
};

The TTF_SetFontSize() function also lets us change the size of a font we've already loaded. Let's use this in a new SetFontSize() method, allowing consumers to change the font size without needing to create an entirely new object:

// Text.h
class Text {
 public:
  // ...
  void SetFontSize(int NewSize) {
    TTF_SetFontSize(Font, NewSize);
  }
  // ...
};

Let's increase our font size, and notice the quality improvement over using SDL_BlitScaled():

// main.cpp

class Window {/*...*/}; int main(int argc, char** argv) { SDL_Init(SDL_INIT_VIDEO); TTF_Init(); SDL_Event Event; bool shouldQuit{false}; Window GameWindow; Text TextExample{"Hello World", 100};
while (!shouldQuit) {/*...*/} TTF_Quit(); SDL_Quit(); return 0; }

We cover more techniques related to scaling in the next lesson.

Complete Code

Complete versions of the files we created in this lesson are available below:

Summary

In this lesson, we've explored the fundamentals of rendering text in SDL2 using the SDL_ttf extension. We've covered:

  • Initializing and quitting SDL_ttf
  • Loading and freeing fonts
  • Creating text surfaces
  • Rendering text to the screen
  • Handling potential errors in text rendering
  • Basic text positioning and scaling

In the next lesson, we'll expand on these concepts further, covering more advanced use cases.

Next Lesson
Lesson 32 of 129

Text Performance, Fitting and Wrapping

Explore advanced techniques for optimizing and controlling text rendering when using SDL_ttf

Questions & Answers

Answers are generated by AI models and may not have been reviewed. Be mindful when running any code on your device.

Changing Text Color
How can I change the color of the rendered text in SDL2 using SDL_ttf?
Adding Outlines and Shadows to Text
Is it possible to render text with an outline or shadow in SDL2 using SDL_ttf?
Rendering Multi-line Text
How do I handle multi-line text rendering in SDL2 with SDL_ttf?
Text Rendering Performance
What's the performance impact of rendering text every frame vs. caching text surfaces?
Implementing Text Wrapping
How can I implement text wrapping within a specific width?
Rendering Text Along a Curved Path
Is it possible to render text along a curved path?
Mixing Fonts and Styles in Text
Can I mix different fonts and styles within the same text surface?
Applying Color Gradients to Text
Is it possible to apply color gradients to rendered text?
Efficient Dynamic Text Rendering
How do I implement efficient text rendering for large amounts of dynamic text?
Text Transparency and Blending Modes
Is it possible to render text with transparency or blending modes?
Or Ask your Own Question
Purchase the course to ask your own questions