SDL & Fonts Part 2: PrintStrings
by Doug Manley


ADVERTISEMENT

Download the source code for this article here.

Prerequisites

Really, all you need is to understand that we're rendering text in this example. If you want to have the code working, you'll need SDL, SDL_TTF, and a compiler :-)

A special thank-you to Ernest Pazera for all his friendly input.

SDL_TTF

The reason why I wrote SDL & Fonts was because I found SDL_TTF very confusing to use. I talked to Sam about it, and he gave me some help, but I know that reading an article is much nicer than cursing at .C files, looking for some help (from experience).

Last time, on GameDev...

Last time, we set up SDL_TTF and wrote a few functions to make SDL a little easier. Still, rendering text is a mess. To address this (and because SDL_TTF doesn't like the '\n' character), we'll be making PrintStrings() today.

PrintStrings()

Okay, let's recap on how to draw some text:

// Step 1: Load a font and give it a point-size:
TTF_Font fntArial12 = TTF_OpenFont( "arial.ttf", 12 );
// Step 2: Render to an SDL_Surface:
SDL_Color clrBlack = { 0,0,0, 0 };
// (or use newSDL_Color( 0,0,0 );)
SDL_Surface *sText =
  TTF_RenderText_Solid( fntArial12, "Hello.", clrBlack );
// Step 3: Blit that to the screen:
SDL_Rect rcDest = { 20,50, 0,0 };
// (or use newSDL_Rect( 20,50, 0,0 );)
SDL_BlitSurface( sText,NULL, screen,&rcDest );

So, PrintStrings() should do something like that. So, for our first version of PrintStrings, we'd need to pass it a destination, a font, the text, where to draw it, and a color. Also, I'm using "standard strings" (std::string) for the text so that we can do some convenient operations on the text.

void PrintStrings( SDL_Surface *sDest, TTF_Font *fnt, String str,
                   SDL_Rect &rc, SDL_Color clr ) {
  SDL_Surface *sText = TTF_RenderText_Solid( fnt, str.c_str(), clr );
  SDL_BlitSurface( sText,NULL, sDest,&rc );
  SDL_FreeSurface( sText );
}

Right now, all that does is save about 2 lines of code. But if we want to do multiple lines, it'll save a lot more than that. To this effect, SDL_TTF provides a nice function called TTF_FontLineSkip(). If you don't use this function for determining spacing and blit the lines one under the other, the text may be spaced really far apart. It sort of looks like double-spacing on some fonts (some fonts don't have this problem at all).

So, if you draw a line at (0,20), you should draw the next line at (0,20+TTF_FontLineSkip(fnt)). That makes the text look normal. With that in mind, let's make another PrintStrings() that can handle multiple lines. Let's also add another color for the background.

void PrintStrings( SDL_Surface *sDest, TTF_Font *fnt, String str,
                   SDL_Rect &rc, SDL_Color clrFg, SDL_Color clrBg ) {
  int lineSkip = TTF_FontLineSkip( font );  // Get the line-skip value.
  int width = 0, height = 10;
  std::vector<String> vLines; // these are the individual lines.
  
  // Break the String into its lines:
  int n = 0;
  while( n != -1 ) {
    // Get until either '\n' or '\0':
    String strSub;
    n = str.find( '\n', 0 ); // Find the next '\n'
    strSub = str.substr( 0,n );
    if( n != -1 ) {
      str = str.substr( n+1, -1 );
    }
    vLines.push_back(strSub);
    
    // Get the size of the rendered text:
    int w = 0;
    TTF_SizeText( font, strSub.c_str(), &w,&height );
    if( w > width ) {  width = w;  }
    // (really, we just want to see how wide this is going to be)
  }

  // Since the width was determined earlier, get the height:
  // (vLines.size() == Number of Lines)
  height = (vLines.size()-1) * lineSkip + height; // plus the first line
  // (we assume that height is the same for all lines.)

  // Make the surface to which to blit the text:
  sText = SDL_CreateRGBSurface( SDL_SWSURFACE, width,height, 
      sDest->format->BitsPerPixel, 
      sDest->format->Rmask,
      sDest->format->Gmask,
      sDest->format->Bmask,
      sDest->format->Amask
  );
  // (it's compatible with the destination Surface

  // Now, fill it with the background color:
  SDL_FillRect( sText,NULL, clrBg );

  // Actually render the text:
  SDL_Surface *sTemp = NULL;
  for( int i = 0; i < vLines.size(); i++ ) {
    // The rendered text:
    sTemp = TTF_RenderText_Solid( font, vLines[i].c_str(), clrFg );
    
    // Put it on the surface (sText):
    SDL_BlitSurface( sTemp,NULL,
      sText,&(SDL_Rect)newSDL_Rect(0,i*lineSkip,0,0) );
  // Clean up:
  SDL_FreeSurface( sTemp );
  }
  // So, now we have a nice bitmap (sText) that has all the text on it.

  // Draw the text on the destination:
  SDL_BlitSurface( sText,NULL, dest,&rc );
  SDL_FreeSurface( sText );
}

That's a lot longer, and it saves you a lot of time to call PrintStrings() instead of manually printing each line. This is a medium-strength version of PrintStrings(). The one that I use has one extra parameter: "int flags." You can OR a bunch of parameters to PrintStrings() this way. One of them doesn't draw to the destination; it returns the surface instead. Another aligns the text (like left, centered, right). Another draws transparently.

I've copied the almost-full-power version here (I haven't needed some things implemented yet, so they're still un-coded):

const int PS_ALIGN_LEFT =  1;
const int PS_ALIGN_CENTER =  2;
const int PS_ALIGN_RIGHT =  4;
const int PS_CREATE_SURFACE =  8;
const int PS_TRANSPARENT_BG =  16;
const int PS_BLENDED =    32;
SDL_Surface* PrintStrings( SDL_Surface *dest, TTF_Font *font,
                           String str, SDL_Rect &rc, SDL_Color clrFg,
                           SDL_Color clrBg, int flags ) {
/* This function prints "str" with font "font" and color "clrFg"
 * onto a rectangle of color "clrBg".
 * It does not pad the text.
 * If CREATE_SURFACE is NOT passed, the function returns NULL,
 *otherwise, it returns an SDL_Surface * pointer.
 */
  // If there's nothing to draw, return NULL:
  if( str.length() == 0 || font == NULL ) {
    return( NULL );
  }

  // This is the surface that everything is printed to.
  SDL_Surface *sText = NULL;
  int lineSkip = TTF_FontLineSkip( font );
  int width = 10, height = 10;
  std::vector<String> vLines;
  
  // Break the String into its lines:
  int n = 0;
  while( n != -1 ) {
    // Get until either "\n" or "\0":
    String strSub;
    n = str.find( '\n', 0 );
    strSub = str.substr( 0,n );
    if( n != -1 ) {
      str = str.substr( n+1, -1 );
    }
    vLines.push_back(strSub);
    
    int w = 0;
    // Get the size of the rendered text.
    TTF_SizeText( font, strSub.c_str(), &w,&height );
    if( w > width ) {  width = w;  }
  }
  
  // vLines.size() == Number of Lines.
  // we assume that height is the same for all lines.
  height = (vLines.size()-1) * lineSkip + height;
  
  // dest CAN'T be NULL if you're creating the surface!
  if( dest != NULL && (flags&PS_CREATE_SURFACE) ) {
    printf("dest != NULL with PS_CREATE_SURFACE!\n");
    return(NULL);
  }
  if( dest == NULL ) {
    if( flags&PS_CREATE_SURFACE ) {
      // Create a "dest" to which to print:
      dest = SDL_CreateRGBSurface( SDL_SWSURFACE, width,
          height, BPP, 0,0,0,0 );
    } else {
      printf("There was no surface.\n(Exiting function...)\n");
      return(NULL);
    }
  }
  
  sText = SDL_CreateRGBSurface( SDL_SWSURFACE, width,height,
    dest->format->BitsPerPixel, dest->format->Rmask,dest->format->Gmask,
    dest->format->Bmask,dest->format->Amask );

  // Color in the rectangle on the destination:
  if( flags&PS_TRANSPARENT_BG ) {
    // If the fg & bg colors are the same, we need to fix it:
    if( clrFg == clrBg ) {
      if( clrFg == BLACK ) {
        clrBg = WHITE;
      } else {
        clrBg = BLACK;
      }
    }
    SDL_FillRect( sText,NULL,
      SDL_MapRGB(sText->format,clrBg.r,clrBg.g,clrBg.b) );
    SDL_SetColorKey( sText, SDL_SRCCOLORKEY|SDL_RLEACCEL,
         SDL_MapRGB(sText->format,clrBg.r,clrBg.g,clrBg.b) );
  } else {
    SDL_FillRect( sText,NULL, clrBg );
  }
  
  // Actually render the text:
  SDL_Surface *sTemp = NULL;
  for( int i = 0; i < vLines.size(); i++ ) {
    // The rendered text:
    if( flags & PS_BLENDED ) {
      sTemp = TTF_RenderText_Blended( font, vLines[i].c_str(), clrFg );
    } else {
      sTemp = TTF_RenderText_Solid( font, vLines[i].c_str(), clrFg );
    }
    
    // Put it on the surface:
    if( (flags&PS_ALIGN_LEFT) ||
         !(flags&PS_ALIGN_CENTER ||
         flags&PS_ALIGN_RIGHT) ) {
      // If it's specifically LEFT or none of the others:
      SDL_BlitSurface( sTemp,NULL, sText,
          &(SDL_Rect)newSDL_Rect(0,i*lineSkip,0,0) );
    }
    if( flags & PS_ALIGN_CENTER ) {
      int w = 0, h = 0; TTF_SizeText( font, vLines[i].c_str(), &w,&h );
      SDL_BlitSurface( sTemp,NULL, sText,
          &(SDL_Rect)newSDL_Rect(width/2-w/2,i*lineSkip,0,0) );
    }
    if( flags & PS_ALIGN_RIGHT ) {
      printf("ERROR: PrintStrings()::PS_ALIGN_RIGHT:"
          "Oops, this hasn't been implemented yet\n");
    }
    // Clean up:
    SDL_FreeSurface( sTemp );
  }
  
  if( flags & PS_CREATE_SURFACE ) {
    SDL_FreeSurface( dest );
    return( sText );
  }
  
  // Draw the text on top of that:
  SDL_BlitSurface( sText,NULL, dest,&rc );
  SDL_FreeSurface( sText );
  return(NULL);
}

Closing

Thanks for your support, and if you need anything, feel free to e-mail me at ssvegeta7@yahoo.com.

Next Time

Possible topics: GetString(), RenderClass, SuperTimer.

Discuss this article in the forums


Date this article was posted to GameDev.net: 7/9/2003
(Note that this date does not necessarily correspond to the date the article was written)

See Also:
Simple DirectMedia Layer
Sweet Snippets

© 1999-2011 Gamedev.net. All rights reserved. Terms of Use Privacy Policy
Comments? Questions? Feedback? Click here!