Learning SDL2 - How to Create a SDL2 Window

In this tutorial we’ll learn how to create a SDL2 window that reacts to events and can be closed by pressing ESC or clicking on the X icon on the window’s top frame.

A SDL2 basic window
A SDL2 basic window

Step 1: Initialize the main window

In order to have a window showing up in SDL2, you have to have 3 key components:

  1. An initialized window.
  2. A renderer.
  3. Listen to SDL2 events.

Initializing a window in SLD2

Let’s start by defining constants to represent the WIDTH and HEIGHT of our window.

const int WINDOW_WIDTH = 512;
const int WINDOW_HEIGHT = 284;

Let’s declare the two pointer handles, one for the main window and another for the main renderer.

SDL_Window* g_main_window;
SDL_Renderer* g_main_renderer;

For this program, we want to display the window background in BLACK, so let’s define an enumerator for this color.

namespace Colors {
  const SDL_Color BLACK = { 0, 0, 0, SDL_ALPHA_OPAQUE };
}

Now we can move onto the SDL2 initialization, which is required in order to be able to initialize a SLD_Window.

Tip: If you are using the sdl2-starter project from Github, you’ll have already a sample code of how this initialization works.

Let’s create a function responsible for performing the SDL2 initialization. This function returns true in case the initialization succeeds.

static bool Init() {
  return true;
}

Now, let’s initialize SDL2 and check for any errors. If we can’t initialize it properly, we return with an EXIT_FAILURE.

For the sake of simplicity, here we will initialize all the SDL2 subsystems by using the SDL_INIT_EVERYTHING flag.

After calling the SDL_Init() function, we check if its return is less than 0, which will indicate that the initialization failed, and in that case we write a mesage to the debug console including the error message obtained from the SDL_GetError() function.

We can access the last error that happened in SDL2 by calling the SDL_GetError() function.

static bool Init()
{
  if (SDL_Init(SDL_INIT_EVERYTHING) < 0) {
    std::cout << "SDL_Init failed with error: " << SDL_GetError() << std::endl;
    return EXIT_FAILURE;
  }

  return true;
}

If the initialization succeeds, we can now initialize our main window. We will do that by using the SDL_CreateWindow() function.

This function, takes some arguments, like the window’s title, position, width, height, and type. For the WIDTH and HEIGHT we will use the previously defined constants. For the positions, we will create a window that’s centered on the screen by using the pre-defined SDL2 constans SDL_WINDOWPOS_CENTERED. And the type of the window will be SDL_WINDOW_OPEGNGL.

The window type will indicate wich type of context you’ll have on that window. This context will give you some option in regards to what you can do with it. In this case, an SDL_WINDOW_OPEGNGL will allow us to use OpenGL on that context, which is what we need in order to draw elements on the screen later on.

static bool Init()
{
  if (SDL_Init(SDL_INIT_EVERYTHING) < 0) {
    std::cout << "SDL_Init failed with error: " << SDL_GetError() << std::endl;
    return EXIT_FAILURE;
  }

  g_main_window = SDL_CreateWindow(
    "A SDL2 Basic Window (512x284)",
    SDL_WINDOWPOS_CENTERED,
    SDL_WINDOWPOS_CENTERED,
    WINDOW_WIDTH,
    WINDOW_HEIGHT,
    SDL_WINDOW_OPENGL
  );

  return true;
}

Initializing a window might fail for many reasons, therefore we should also check if the window was properly intialized.

If a window initialization fails, the return will be null, and our pointer will be pointing to nothing. We should handle this situation and in case the window handle is a null pointer we should log the error to the debug console and exit with EXIT_FAILURE.

static bool Init()
{
  if (SDL_Init(SDL_INIT_EVERYTHING) < 0) {
    std::cout << "SDL_Init failed with error: " << SDL_GetError() << std::endl;
    return EXIT_FAILURE;
  }

  g_main_window = SDL_CreateWindow(
    "A SDL2 Basic Window (512x284)",
    SDL_WINDOWPOS_CENTERED,
    SDL_WINDOWPOS_CENTERED,
    WINDOW_WIDTH,
    WINDOW_HEIGHT,
    SDL_WINDOW_OPENGL
  );

  if (g_main_window == nullptr) {
    std::cout << "Unable to crete the main window. Erro: " << SDL_GetError() << std::endl;
    SDL_Quit();
    return EXIT_FAILURE;
  }

  return true;
}

And that’s the code for initializing a window. Check that everything works by building the app with $ make. You should see no error at this stage.

Initializing a SDL2 renderer

in SDL2, when you want to draw something on the screen, on don’t do that directly. You make use of a renderer wich renders whatever you defined in a context to a so called Back Buffer and when you decide, you flip the current buffer, brining the one on the back to the front. This is an over simplification of the process, but it’s important that you understand now why do a renderer exist and why do we need one here.

With that said, let’s initialize our main renderer inside our previously created function Init().

static bool Init()
{
  if (SDL_Init(SDL_INIT_EVERYTHING) < 0) {
    std::cout << "SDL_Init failed with error: " << SDL_GetError() << std::endl;
    return EXIT_FAILURE;
  }

  g_main_window = SDL_CreateWindow(
    "A SDL2 Basic Window (512x284)",
    SDL_WINDOWPOS_CENTERED,
    SDL_WINDOWPOS_CENTERED,
    WINDOW_WIDTH,
    WINDOW_HEIGHT,
    SDL_WINDOW_OPENGL
  );

  if (g_main_window == nullptr) {
    std::cout << "Unable to crete the main window. Erro: " << SDL_GetError() << std::endl;
    SDL_Quit();
    return EXIT_FAILURE;
  }

  g_main_renderer = SDL_CreateRenderer(g_main_window, -1, SDL_RENDERER_PRESENTVSYNC);

  return true;
}

That’s it! Here, we don’t need to check if the renderer initialization succeeded or not. For now, we can just assume that the rendere initialization will be always successful.

A renderer takes a SDL_Window reference, a render driver index, and some flags. For now, we will be using the first driver found that suits the renderer context, and we will be passing only the SDL_RENDERER_PRESENTVSYNC flag, which will take care of limiting our frame rate for this program.

In order to reduce the complexity of this program, we will not be manually controlling the frame rate, that’s why we are using the SDL_RENDERER_PRESENTVSYNC flag.

Listen to SDL2 events

Our next task is to start listening to SDL2 events so we can, for instance, close the window when you press the ESC key, move the square later on when we press the directional keys, or even reacting to events happning over the window like closing it by clicking on the X icon, resizing it, etc.

Here we start creating our main loop. The loop that will run while our application is running and on each pass will check for any SDL2 event, and in case an event happened, we will then handle it properly.

Lets start by creating the main function and use the previously created Init() function.

int main()
{
  if (Init() == false) { Shutdown(); }

  // Implement the main loop here

  Shutdown();
  return EXIT_SUCCESS;
}

Note that we have a call to a function called Shutdown() which we don’t have yet. The Shutdown() function will handle all the deallocation of memory and the release of any resource associated with the initialization of SDL2.

void Shutdown()
{
  if (g_main_window != nullptr) {
    SDL_DestroyWindow(g_main_window);
    g_main_window = nullptr;
  }

  if (g_main_renderer != nullptr) {
    SDL_DestroyRenderer(g_main_renderer);
    g_main_renderer = nullptr;
  }

  SDL_Quit();
}

Now we can proceed to the creation of the main loop and start listening for events.

The main loop has a certain sequence of tasks to perform every time it runs. In our program, the sequence goes like this:

  1. Clear the screen from any previously drawn shapes.
  2. Check for any SDL2 event and handle them accordingly.
  3. Render and present the new context on the screen.

Let’s start step-by-step.

We don’t have yet the function ClearScreen(), so let’s create it now. In this function we will define the renderer color using our previously declared Colors::BLACK and calling SDL_SetRendererDrawColor() and next we will clear the renderer buffer calling the function SDL_RenderClear(). This function will paint all pixels with the color we previously defined, so that we have a blank canvas to start painting/drawing again.

static void ClearScreen(SDL_Renderer* renderer)
{
  SDL_SetRenderDrawColor(renderer, Colors::BLACK.r, Colors::BLACK.g, Colors::BLACK.b, Colors::BLACK.a);
  SDL_RenderClear(renderer);
}

Now, let’s declare a SDL_Event variable called event that will be used later when we will check for events. Also, let’s create the running boolean variable to indicate when the program is running and when it’s not and init the main loop.

int main()
{
  if (Init() == false) { Shutdown(); }

  // Draw loop
  SDL_Event event;
  bool running = true;

  while(running)
  {
    // Implement the sequence of tasks here.
  }

  Shutdown();
  return EXIT_SUCCESS;
}

Let’s add the first task to the main loop, a call to the ClearScreen() function.

int main()
{
  if (Init() == false) { Shutdown(); }

  // Draw loop
  SDL_Event event;
  bool running = true;

  while(running)
  {
    ClearScreen(g_main_renderer);
  }

  Shutdown();
  return EXIT_SUCCESS;
}

Now, let’s add the second task to the main loop so that we can listen to SDL2 events. In order to achieve that, we use the SDL_PollEvent() function passing the address of our event variable so that SDL2 can assign to that variable any event in case one exists.

In case an event exists, we will have to verify which event that is. In order to do that we will be using a switch statement and will verify the event.type.

int main()
{
  if (Init() == false) { Shutdown(); }

  // Draw loop
  SDL_Event event;
  bool running = true;

  while(running)
  {
    ClearScreen(g_main_renderer);

    // Check and process I/O events
    if (SDL_PollEvent(&event)) {
      switch (event.type) {
        default:
          break;
      }
    }
  }

  Shutdown();
  return EXIT_SUCCESS;
}

We will be interested in two types of events in this programs: SDL_KEYDOWN and SDL_QUIT.

  • SDL_KEYDOWN: Will allow us to respond to a key down event and check which key was pressed.
  • SDL_QUIT: Will allow us to respond to the closing of the window by clicking on the X icon.

Checking which key was pressed

In order to know which key was pressed, we have to access the event.key.keysym.scancode property. We can use SDL2’s pre-defined key constants to compare the actual pressed key with the key we are expecting to handle.

In our program, we want the program to stop, which in this case will require us to set the running variable to false, when the user presses the ESC key.

Let’s implement that in our main loop.

int main()
{
  if (Init() == false) { Shutdown(); }

  // Draw loop
  SDL_Event event;
  bool running = true;

  while(running)
  {
    ClearScreen(g_main_renderer);

    // Check and process I/O events
    if (SDL_PollEvent(&event)) {
      switch (event.type) {
        case SDL_KEYDOWN:
        {
          running = event.key.keysym.scancode != SDL_SCANCODE_ESCAPE;
          break;
        }
        case SDL_QUIT:
        {
          running = false;
          break;
        }
        default:
          break;
      }
    }
  }

  Shutdown();
  return EXIT_SUCCESS;
}

Displaying the window

Now, let’s display our window by presending the content of our renderer (which at this point is just a black screen) using the function SDL_RenderPresent().

Note: If you don’t do that, the window will not be displayed on your monitor. You have to check at least once the SDL2 event queue in order to allow the window to be displayed.

int main()
{
  if (Init() == false) { Shutdown(); }

  // Draw loop
  SDL_Event event;
  bool running = true;

  while(running)
  {
    ClearScreen(g_main_renderer);

    // Check and process I/O events
    if (SDL_PollEvent(&event)) {
      switch (event.type) {
        case SDL_KEYDOWN:
        {
          running = event.key.keysym.scancode != SDL_SCANCODE_ESCAPE;
          break;
        }
        case SDL_QUIT:
        {
          running = false;
          break;
        }
        default:
          break;
      }
    }

    // Update the screen with the content rendered in the background
    SDL_RenderPresent(g_main_renderer);
  }

  Shutdown();
  return EXIT_SUCCESS;
}

And that’s it. Now, if you make your program and run it with ./build/debub/play, you should see the black window poping up on your monitor. You should be able to close it by pressing ESC on your keyboard or by clicking with your mouse on the X icon.

Well done! 👏👏👏

Here’s the final code: