With SDL2 successfully installed and added to our project, we should now be able to create and manipulate windows.
This lesson goes over the steps in how we can do this. We break down and explain every function in the process, as well as describe the most common options we have for customizing our window.
After installing SDL in the previous chapter, we can now compile and run the following program, and confirm that it creates a window:
#include <SDL.h>
#include <SDL_image.h>
#include <SDL_ttf.h>
int main(int argc, char** argv) {
SDL_Init(SDL_INIT_VIDEO);
IMG_Init(IMG_INIT_PNG);
TTF_Init();
SDL_CreateWindow(
"Hello Window",
100, 100, 800, 300, 0
);
while(true) {
SDL_PumpEvents();
}
return 0;
}
We’ll explain every line of this program throughout the first few chapters of this course. For now, let’s delete the SDL_image
and SDL_ttf
lines to keep things simple. SDL_image
and SDL_ttf
are for rendering images and text respectively. We’ll cover that soon, but we don’t need it now:
#include <SDL.h>
#include <SDL_image.h>
#include <SDL_ttf.h>
int main(int argc, char** argv) {
SDL_Init(SDL_INIT_VIDEO);
IMG_Init(IMG_INIT_PNG);
TTF_Init();
SDL_CreateWindow(
"Hello Window",
100, 100, 800, 300, 0
);
while(true) {
SDL_PumpEvents();
}
return 0;
}
Note that this example program does not allow itself to be closed. We’ll add that capability later in the course. For now, we can close it using our IDE if possible, or force it to close through our operating system (Ctrl + Alt + Del on Windows or Cmd + Option + Esc on macOS).
We’ll update our program to support quitting later in this chapter.
In the code examples in this course, we’ll be using #include
directives in the following form:
#include <SDL.h>
However, you may need to use an alternative format, such as #include <SDL2/SDL.h>
. This depends on how your include directories (sometimes also called search paths) were configured in the previous chapter.
Before we use any SDL functions, we need to initialize the library. SDL has a lot of features, and few programs need all of them. As such, the features are broken into subsystems, which allows us to load only the things our program needs.
The first thing we do in our main
function is call SDL_Init()
, which receives the subsystems we want to initialize as an argument:
SDL_Init(SDL_INIT_VIDEO);
Here, we’re just initializing the video subsystem, which we’ll need in almost every lesson in this course. Checking the official documentation, we see that SDL_INIT_VIDEO
will also initialize the events subsystem, which we’ll be using later in this chapter.
The documentation claims we can initialize multiple subsystems by "OR-ing them together". This refers to the bitwise OR operator, |
So, for example, we could initialize 3 subsystems like this:
SDL_Init(
SDL_INIT_VIDEO |
SDL_INIT_AUDIO |
SDL_INIT_TIMER
);
Note, the |
operator is not the same as the more common ||
operator. The single |
refers to the bitwise or, and it’s used quite a lot in SDL, and game development more generally. We can just treat it as a way to combine multiple options but, if you want to go a little deeper on how it works, we cover in more detail here:
It’s also a best practice to shut down SDL when we no longer need it. We can do this using the SDL_Quit()
function.
In most applications that use SDL, we need to keep it running through the entire lifecycle of our program, so we’d only call SDL_Quit()
just before our program ends.
Our program doesn’t currently end because of the infinite loop, which we’ll address soon, but let’s just add an unreachable call to SDL_Quit()
in the typical place for now:
#include <SDL.h>
int main(int argc, char** argv) {
SDL_Init(SDL_INIT_VIDEO);
SDL_CreateWindow(
"Hello Window",
100, 100, 800, 300, 0
);
while(true) {
SDL_PumpEvents();
}
SDL_Quit();
return 0;
}
As we’ve likely noticed, programs running on our computer can be opened within their own, dedicated windows. The companies that created the platforms (such as Apple for macOS and Microsoft for Windows) provide APIs to create and manage those windows.
Unfortunately, these APIs are quite complex, and they’re also completely different. If we create an application for Windows by directly using a Windows API, and then wanted to release on macOS too, we’d have a lot of work to do.
One of the main reasons for using SDL that it is cross-platform. If we write code using the SDL APIs then, behind the scenes, SDL can interpret that to work with the Windows API, the macOS API, and about 20 other platforms.
For the rest of this lesson, we’ll focus on window creation. Currently, that’s done through the SDL_CreateWindow()
invocation in our main
function. Let’s add a dedicated Window
class to take care of this, and to keep our main
function organized:
// Window.h
#pragma once
#include <SDL.h>
class Window {
public:
Window(){
SDL_CreateWindow(
"Hello Window",
100, 100, 800, 300, 0
);
}
};
Our Window
constructor is using an SDL function, so ensure SDL is initialized before it runs:
// main.cpp
#include <SDL.h>
#include "Window.h"
int main(int argc, char** argv) {
SDL_Init(SDL_INIT_VIDEO);
Window GameWindow;
SDL_CreateWindow(
"Hello Window",
100, 100, 800, 300, 0
);
while(true) {
SDL_PumpEvents();
}
SDL_Quit();
return 0;
}
The Window
constructor contains the code we previously had in our main
function, so our program should still behave as it did before:
SDL_CreateWindow()
FunctionWe’re passing six arguments to our SDL_CreateWindow()
function call:
// Window.h
#pragma once
#include <SDL.h>
class Window {
public:
Window(){
SDL_CreateWindow(
"Hello Window",
100, 100, 800, 300, 0
);
}
};
Let’s break down what these arguments are:
100
in this example, indicating we want our window to be opened 100 units from the left-most edge of the user’s screen100
in this example, indicating we want it to be opened 100 units from the top edge of the user’s screen800
units wide300
units tall0
indicating we want to use the default behavior, but we cover this in more detail later in this sectionThe position and size values (such as 100
, 700
, and 300
that we provide to SDL are in somewhat abstract units called screen coordinates.
Behind the scenes, these screen coordinates will be converted to pixel values based on the display our window is being shown on. The conversion rate between screen coordinates and pixels depends on the pixel density - also known as dots per inch (DPI) - of the display. We cover screen coordinates and pixel density in a dedicated lesson later in the course.
In our previous example, we’re opening our window 100
pixels from the left edge of the screen, and 100
pixels from the top edge. These values were chosen somewhat arbitrarily, just so we could get something on the screen.
In most cases, we don’t have any specific preference on where our window should open - we’d rather let the platform decide the best place. We can do that by passing SDL_WINDOWPOS_UNDEFINED
as the horizontal or vertical position. We’d typically use it for both:
// Window.h
#pragma once
#include <SDL.h>
class Window {
public:
Window(){
SDL_CreateWindow(
"Hello Window",
// Horizontal position
SDL_WINDOWPOS_UNDEFINED,
// Vertical position
SDL_WINDOWPOS_UNDEFINED,
800, 300, 0
);
}
};
If we want our window to be centered on the screen, we can pass SDL_WINDOWPOS_CENTERED
as the horizontal and/or vertical position:
// Window.h
#pragma once
#include <SDL.h>
class Window {
public:
Window(){
SDL_CreateWindow(
"Hello Window",
// Horizontal position
SDL_WINDOWPOS_CENTERED,
// Vertical position
SDL_WINDOWPOS_CENTERED,
800, 300, 0
);
}
};
Unless we have a compelling reason to specify the position, letting the platform decide using SDL_WINDOWPOS_UNDEFINED
is recommended.
They’ve often put some thought into what makes for a good user experience. For example, they might open the window near the user’s cursor, or they might remember where the user moved the window to the last time they ran our program, and open it in that same position.
The last argument to SDL_CreateWindow()
, which we’re currently passing 0
to, is for SDL window flags. Window flags give us some control over how our window should behave.
For example, we can make our window resizable by passing SDL_WINDOW_RESIZABLE
:
// Window.h
#pragma once
#include <SDL.h>
class Window {
public:
Window(){
SDL_CreateWindow(
"Hello Window",
SDL_WINDOWPOS_UNDEFINED,
SDL_WINDOWPOS_UNDEFINED,
800, 300,
SDL_WINDOW_RESIZABLE
);
}
};
If we compile and run our program at this stage, we might see some graphical issues after resizing the window. That is normal at this stage. After completing the double buffering lesson at the end of this chapter, these issues will be gone.
Window flags are a bit mask, so we can combine multiple flags using the bitwise |
operator. Below, we make our window both resizable and minimized.
// Window.h
#pragma once
#include <SDL.h>
class Window {
public:
Window(){
SDL_CreateWindow(
"Hello Window",
SDL_WINDOWPOS_UNDEFINED,
SDL_WINDOWPOS_UNDEFINED,
800, 300,
SDL_WINDOW_RESIZABLE
| SDL_WINDOW_MINIMIZED
);
}
};
The flags we pass to SDL_CreateWindow()
only apply to the initial state of the window.
These flags can be changed later - sometimes using specific SDL functions that we’ll cover throughout the course, or sometimes SDL will change them automatically.
Whilst our previous window will start minimized, the player can indirectly remove that flag later by, for example, clicking on their window in the taskbar.
SDL_WindowFlags
TypeWe can store a set of window flags using the SDL_WindowFlags
type if needed:
SDL_WindowFlags Flags{
SDL_WINDOW_RESIZABLE | SDL_WINDOW_MINIMIZED
};
The official documentation lists all of the available flags, and we cover most of them throughout this course.
SDL_Window
TypeThe SDL_CreateWindow()
function returns a pointer to the SDL_Window
it created. We should store this pointer, as we need it for future window management. Let’s add it as a member variable to our class:
// Window.h
#pragma once
#include <SDL.h>
class Window {
public:
Window(){
SDLWindow = SDL_CreateWindow(
"Hello Window",
SDL_WINDOWPOS_UNDEFINED,
SDL_WINDOWPOS_UNDEFINED,
800, 300, 0
);
}
private:
SDL_Window* SDLWindow{nullptr};
};
SDL_Window
Memory ManagementUnfortunately, SDL_Window
objects require us to do some memory management. This section may be a little annoying, but don’t get too disheartened - it’s somewhat uncommon that this level of manual intervention is required.
Behind the scenes, SDL allocates dynamic memory to support each SDL_Window
. When we no longer need a window, we need to notify SDL so it can release that memory and prevent a memory leak.
To do this, we call SDL_DestroyWindow()
, passing a pointer to the SDL_Window
we want to get rid of. Let’s add a destructor to our Window
class for this:
// Window.h
#pragma once
#include <SDL.h>
class Window {
public:
Window(){
SDLWindow = SDL_CreateWindow(
"Hello Window",
SDL_WINDOWPOS_UNDEFINED,
SDL_WINDOWPOS_UNDEFINED,
800, 300, 0
);
}
~Window() {
SDL_DestroyWindow(SDLWindow);
}
private:
SDL_Window* SDLWindow{nullptr};
};
Even though we won’t be directly managing memory in this course using keywords like new
and delete
, SDL still requires us to perform indirect memory management in a few scenarios.
We’ll call out these scenarios when we encounter them, and window creation is one such example.
Conceptually, we can consider SDL_CreateWindow()
to be equivalent to using new
, and SDL_DestroyWindow()
being the corresponding delete
. If this doesn’t make sense, or you want to learn more, we cover manual memory management in more detail in a lesson in our introductory course:
As we’re adding a destructor to our class, the rule of three should remind us that we need to consider our copy constructor and assignment operators too. In this case, we don’t need our Window
objects to be copyable, so we’ll simplify things and just delete the copy operations:
// Window.h
#pragma once
#include <SDL.h>
class Window {
public:
Window(){
SDLWindow = SDL_CreateWindow(
"Hello Window",
SDL_WINDOWPOS_UNDEFINED,
SDL_WINDOWPOS_UNDEFINED,
800, 300, 0
);
}
Window(const Window&) = delete;
Window& operator=(const Window&) = delete;
~Window() {
SDL_DestroyWindow(SDLWindow);
}
private:
SDL_Window* SDLWindow{nullptr};
};
We covered object copying and the rule of three in more detail in our introductory course:
There are two further scenarios we should consider in our Window
memory management, specifically within our destructor:
SDL_Window
pointer may be a nullptr
. It is technically safe to pass a nullptr
to SDL_DestroyWindow()
, but SDL will report it as an error, so let’s not do it. We will introduce SDL errors and why our SDL_Window
might be a nullptr
later in this chapter.SDL_Quit()
has been called. SDL_Quit()
will delete all the resources SDL is managing, including windows, and will leave our GameWindow
with a dangling pointer. It is not safe to call SDL_DestroyWindow()
on a dangling pointer, so we should check for this.We can check if an SDL system is currently intialized using SDL_WasInit()
. We pass the same initialization flag we passed to SDL_Init()
. For example, if we wanted to check if the video subsystem is initialized, we’d use this:
SDL_WasInit(SDL_INIT_VIDEO);
If we want to check if any subsystem is initialized, we can pass 0
as the argument:
SDL_WasInit(SDL_INIT_VIDEO);
Let’s update our destructor to include a nullptr
and SDL_WasInit()
check:
// Window.h
// ...
class Window {
public:
// ...
~Window() {
if (SDLWindow && SDL_WasInit(SDL_INIT_VIDEO)) {
SDL_DestroyWindow(SDLWindow);
} else {
std::cout << "Skipping SDL_DestroyWindow\n";
}
}
};
In our current program, SDL_Quit()
is called near the end of our main
function, and our GameWindow
is destroyed slightly later, once main
ends. As such, we should see our safety check being triggered when we close our program:
Skipping SDL_DestroyWindow
SDL_Window
using a Smart PointerIf we prefer, we can automate the management of our SDL_Window
using a smart pointer, such as a std::unique_ptr
. We covered std::unique_ptr
in our introductory course:
Using it for an SDL_Window
is a little more complex, as std::unique_ptr
doesn’t know how to delete an SDL_Window
- that is, it doesn’t know about the SDL_DestroyWindow()
function.
However, std::unique_ptr
has an alternative constructor that allows us to supply a custom deleter. A custom deleter is something that behaves like a function. When the std::unique_ptr
wants to delete the resource, it will call this function and pass the memory address as an argument. Within the function body, we implement our custom deletion logic.
We cover custom deleters in more detail later in the course but, as an example, changing our Window
class to apply this technique would look like the following:
#include <SDL.h>
#include <memory> // for std::unique_ptr
// Define a custom deleter for SDL_Window*
struct SDLWindowDeleter {
void operator()(SDL_Window* Ptr) const {
if (Ptr && SDL_WasInit(SDL_INIT_VIDEO)) {
SDL_DestroyWindow(Ptr);
}
}
};
// Alias for convenience
using UniqueSDLWindow = std::unique_ptr<
SDL_Window, SDLWindowDeleter>;
class Window {
public:
Window() {
// Get the raw pointer
SDL_Window* Ptr{SDL_CreateWindow(
"Smart Pointer Window",
SDL_WINDOWPOS_UNDEFINED,
SDL_WINDOWPOS_UNDEFINED,
800, 300, 0
)};
// Store in the smart pointer
SDLWindow = static_cast<UniqueSDLWindow>(Ptr);
}
// We still need a way to get the raw pointer
// for interactions with other SDL functions
SDL_Window* GetRaw() const {
return SDLWindow.get();
}
// No longer need copy semantics or destructor
// as the std::unique_ptr takes care of it
Window(const Window&) = delete;
Window& operator=(const Window&) = delete;
~Window() {
if (SDLWindow && SDL_WasInit(SDL_INIT_VIDEO)) {
SDL_DestroyWindow(SDLWindow);
}
}
private:
// Store the window resource in a unique_ptr
UniqueSDLWindow SDLWindow{nullptr};
};
To keep things simple, we’ll continue to use the raw pointer approach for now.
The state of our program at the end of this lesson is as follows. We’ll continue to build on this program throughout the rest of the chapter:
#include <SDL.h>
#include "Window.h"
int main(int argc, char** argv) {
SDL_Init(SDL_INIT_VIDEO);
Window GameWindow;
while(true) {
SDL_PumpEvents();
}
SDL_Quit();
return 0;
}
// Window.h
#pragma once
#include <SDL.h>
class Window {
public:
Window(){
SDLWindow = SDL_CreateWindow(
"Hello Window",
SDL_WINDOWPOS_UNDEFINED,
SDL_WINDOWPOS_UNDEFINED,
800, 300, 0
);
}
Window(const Window&) = delete;
Window& operator=(const Window&) = delete;
~Window() {
if (SDLWindow && SDL_WasInit(SDL_INIT_VIDEO)) {
SDL_DestroyWindow(SDLWindow);
}
}
private:
SDL_Window* SDLWindow{nullptr};
};
In this lesson, we introduced the following topics:
SDL_Init()
and understanding subsystem initialization.SDL_CreateWindow()
and understanding the parameters involved.Learn how to create and customize windows, covering initialization, window management, and rendering
Learn C++ and SDL development by creating hands on, practical projects inspired by classic retro games