GraphicsBlast
The Basics | C++ | Linux | Command Line

Lesson 3: Window Mastery

Setting our window title, icon and toggling fullscreen!
Making our windows look great: We learn how to set the window's icon, alter it's title, and toggle fullscreen mode!

At this point I'm sure you're itching to start coding some actual graphics, but I want to spend a tiny bit more time looking at setting our window up before we start, while it's still fresh in our minds!

In this lesson, we're going to look at how to load an icon and display it on our window. We'll also look at changing the window's title, and allowing the user to toggle fullscreen mode too!

Icons

We'll start by setting the icon of our program.

Most desktop environments display an icon for each program either in the titlebar of the window, in the taskbar, or both. We can set this icon from within our code by first loading an image from the hard drive, and then passing it to an SDL function.

SDL is capable of reading image files from disk, but SDL itself is only capable of loading bitmap (.bmp) images, which is quite limiting and they're quite a memory intensive format. However, there is a separate but closely related SDL_image library made by the same developers, which contains functions for loading a large number of common image formats. To get access to SDL_image, we need to install the library, add it to our makefile, and then include it's header file just as we did for SDL itself.

If this feels a bit like overkill just to set an icon, don't worry! This library will be extremely useful to us later on, so why not set it up now?

First we need to get a copy of the code, either by downloading it from the library's GitHub page, or by cloning it directly using git:

git clone --depth=1 https://github.com/libsdl-org/SDL_image.git

After downloading the code, we then compile and install it with exactly the same commands as we used for SDL before:

cd SDL_image
cmake -S . -B ./build
cmake --build ./build
sudo cmake --install ./build --prefix /usr/local
sudo ldconfig

We configure the build directory, compile it, and then install it system-wide. We finish up with a call to ldconfig to update the system's cache.

Next, we need to add SDL_image to our Makefile:

3.
OBJS = main.cpp
4.
+ 5.
LIBRARIES = -lSDL3 -lSDL3_image -lGL -lGLEW
6.
7.
FLAGS = -Wall

...and lastly to use it we need to include its header file in our main.cpp:

1.
#include <SDL3/SDL.h>
2.
#include <SDL3/SDL_main.h>
+ 3.
#include <SDL3_image/SDL_image.h>
4.
#include <GL/glew.h>
5.
6.
#include <string>
7.
#include <stdio.h>

Now with SDL_image ready, we can start loading images into our program. After we create our window in our init function, let's load in a PNG image to use as our logo.

To do that, the first thing we'll need to do is create a logo. That's your job! I've created one and placed it inside a new folder called "resources" in my workspace, just to keep these files separate from my source code. The relative file path to my image from my executable is therefore resources/icon.png.

Usually, using this path is completely sufficient to load the image. However it's possible that your users run the program from a different directory. For example compare starting the compiled program using ./my_program versus being in the folder above and running ./some_path/my_program. In the second case, the program will complain that the image could not be found.

What happens is that the program's "working directory" is not where the executable file is, but where the user ran the program from. Relative paths are actually relative to this working directory, and therefore fail to work. Well that's frustrating.

There are two approaches to solving this to make all relative paths relative to the executable's location. The first is to alter the current working directory. This is possible through a system call, but can have weird side effects and the call is not cross-platform.

The second approach, and the one recommended by SDL itself, is to use exclusively absolute file paths. Of course, the reason we don't use absolute file paths in the first place is that we don't necessarily know the full filesystem structure of the user in the first place. But fortunately SDL provides a helper function to get the path of the executable.

We can then append our icon's relative path to the executable's absolute path to get a safe and fully portable way to locate the icon on disk. When you distribute your application, you can then copy your executable and resources folder to give people a program which works wherever and however they run it on their filesystem.

Let's code it!

44.
        printf("Unable to create a window: %s\n", SDL_GetError());
45.
        return false;
46.
    }
47.
+ 48.
    std::string iconPath(SDL_GetBasePath());
+ 49.
    iconPath.append("resources/icon.png");
+ 50.
    SDL_Surface* icon = IMG_Load(iconPath.c_str());
51.
    ...

The executable's absolute path can be obtained by calling the SDL_GetBasePath() function. This gives us the path as a const char*, which we convert to an std::string called iconPath. We perform this conversion as it is much easier to work with especially for things like appending the rest of the icon's path.

We then do the appending, passing in the rest icon's relative path from the executable as a constant. The call to SDL_GetBasePath() is guaranteed to end with a file path separator (the "/" denoting levels of folders), so we can just directly append it.

We pass this now absolute path to the function IMG_Load from SDL_image, which will load the requested image and return us a pointer to an SDL_Surface, the SDL variable type for holding images. Regardless of what format the image was on disk, the returned SDL_Surface will always look the same. For example, you can use SDL_Surface->w and SDL_Surface->h to get the image's width and height, and SDL_Surface->pixels to access the decompressed pixel values.

Should our code be unable to find the image and load it for any reason, the pointer will be NULL. So as is tradition at this point, we perform a test to see if it failed to load, and if so show an error message and exit:

48.
    std::string iconPath(SDL_GetBasePath());
49.
    iconPath.append("resources/icon.png");
50.
    SDL_Surface* icon = IMG_Load(iconPath.c_str());
+ 51.
    if(!icon)
+ 52.
    {
+ 53.
        printf("Unable to load icon: %s\n", SDL_GetError());
+ 54.
        return false;
+ 55.
    }
56.
    ...

If the image loads correctly, we can then set it as our icon:

51.
    if(!icon)
52.
    {
53.
        printf("Unable to load icon: %s\n", SDL_GetError());
54.
        return false;
55.
    }
+ 56.
    if(!SDL_SetWindowIcon(window, icon))
+ 57.
    {
+ 58.
        SDL_DestroySurface(icon);
+ 59.
        printf("Unable to set window icon: %s\n", SDL_GetError());
+ 60.
        return false;
+ 61.
    }
+ 62.
    SDL_DestroySurface(icon);
63.
64.
    context = SDL_GL_CreateContext(window);
65.
    if(!context)
66.
    {
67.
        printf("Unable to create an OpenGL context: %s\n", SDL_GetError());

The call to set the window's icon is fairly straight-forward, you just pass in the window and the icon image, and check the return value to see if it set correctly or not. It will return true on success, else false if anything bad happened.

Regardless of if it worked or not, we immediately call SDL_DestroySurface to free the image's memory. If we failed to set the icon then we no longer need the image, and if the call was successful then our icon gets internally copied so we don't need to retain it.

Operating systems can be a bit picky over what kind of image you use as your icon. As I mentioned, SDL_image will bring any image you pass in into a common decompressed format, so whether you're loading a PNG or a JPEG, the operating system won't know, so that won't matter. But what does matter is size; passing large icon images can cause this function to fail, especially on other platforms.

For best compatibility, your icons should be small perfectly square images. If my memory is correct, I've seen sizes as low as 64x64 pixels fail on some systems. Obviously, too small and your icon will look awful as well. I think a 48x48 pixel image is about the largest that is reliably displayed. You can always load several images and if the high-res fails fall back to a lower resolution!

Changing the title

OK, this probably doesn't warrant an entire section, but I just want to show you another window modification at your disposal before we move on. You can use the command SDL_SetWindowTitle to change the title of your window.

I'm not actually going to use it in our code, because we already set the title when we created our window, and I'm happy with what we set. However, I know that some people like to use the titlebar of their windows to display information. In the past, I've definitely seen people display how many lives or how many hitpoints the character has left in the titlebar, and even the number of frames per second rendered.

This isn't something I'm a fan of, I think the titlebar should be for the title only, and any additional information should be either displayed in the terminal if it's debug information, or in a user interface if the user should be aware of it. But that's just me. The full function call is fairly straight-forward:

SDL_SetWindowTitle(window, "My window's new title");

Use it as you please!

Fullscreen mode

Next, let's have a look at making our window fullscreen. Especially for applications like games, fullscreening allows you to use the maximum possible amount of screen space and fully immerse your users in what you are rendering, without any distracting clutter around the sides.

We're going to alter our code so that when the user presses the 'f' key, our program will toggle between fullscreen and windowed mode.

To keep track of the current state of the window, we'll create a boolean variable at the start of our code:

12.
SDL_Window* window = NULL;
13.
SDL_GLContext context = NULL;
14.
15.
bool programRunning = true;
+ 16.
bool isFullscreen = false;
17.
18.
bool init()
19.
{

We can now adjust our event handler code to listen if the 'f' key was pressed:

116.
        else if(event.type == SDL_EVENT_KEY_DOWN)
117.
        {
118.
            printf("Key pressed down: %s\n", SDL_GetKeyName(event.key.key));
119.
            if(event.key.key == SDLK_ESCAPE)
120.
            {
121.
                programRunning = false;
122.
                return;
123.
            }
+ 124.
            else if(event.key.key == SDLK_F)
+ 125.
            {
+ 126.
                isFullscreen = !isFullscreen;
+ 127.
                SDL_SetWindowFullscreen(window, isFullscreen);
+ 128.
            }
129.
        }

We flip the value of isFullscreen and pass the result in to SDL_SetWindowFullscreen, along with the window. This function simply takes a boolean and will make the window fullscreen if true, and revert it back to a window if false. This means the code here will cause the window to toggle between being fullscreen and a window each time the key is pressed.

Viewport

To wrap up this lesson, let's talk about viewports. A viewport is the area of the window which OpenGL is rendering to. By default, when you open a window, the viewport is set to be the entire window, so we haven't had to deal with this up to now.

When our window is fullscreened, maximised, or otherwise changes size on our screen, OpenGL does not automatically adjust the viewport as perhaps you've set it to a particular size for a reason. It is fairly common in some programs not to set the viewport to the whole window, as they might have a certain portion of the window occupied by a user interface from another library for example. Therefore it's up to the developer to control which portion of the window OpenGL will draw to.

If the window is shrunk and the viewport isn't altered, the image will essentially be cropped to fit the window. Likewise if the window is made larger, all the pixels in the new area of the window will be in an undefined state. So it's important we take the viewport into consideration whenever the window changes size.

To know when we need to update our viewport, we'll listen for the event SDL_EVENT_WINDOW_RESIZED in our event handling function. When this is detected, we'll then get the new window size and update the viewport accordingly:

107.
            programRunning = false;
108.
            return;
109.
        }
+ 110.
        else if(event.type == SDL_EVENT_WINDOW_RESIZED)
+ 111.
        {
+ 112.
            windowWidth = event.window.data1;
+ 113.
            windowHeight = event.window.data2;
+ 114.
            glViewport(0, 0, windowWidth, windowHeight);
+ 115.
        }
116.
        else if(event.type == SDL_EVENT_KEY_DOWN)
117.
        {

When a window related event happens, additional relevant information can be found in event.window, and the data1 and data2 fields may get populated depending on what kind of event happened. For example, if the event was that the window moved, these fields contain the new x and y position of the window. On a window resize event, they contain the new size of the window. A full list of window events can be found here.

We update our windowWidth and windowHeight variables with the new window size so we have some record of how big the window now is. We then make a call to glViewport to define the new size of the viewport. In this case, the parameters indicate that we will render from the origin of the window (0, 0) and cover the full width and height of the newly resized window, the third and fourth parameters. Note that the origin for the coordinate system in OpenGL is the bottom left of the window, as a consequence of it's mathematical roots (the x and y axes run like those of graphs), unlike the more common top-left origin used for most desktop windows.

When we enter fullscreen mode, the SDL_EVENT_WINDOW_RESIZED event will be triggered, and we'll now be rendering to the full screen in OpenGL. When we change back to windowed mode, we'll switch back to only rendering to the size of the window. This will affect regular window resizing too, if the user clicks and drags an edge of our window to make the window larger or smaller.

Compile and run your code, and you should now see a beautiful window with icons in the title and taskbar. If you press 'f' on your keyboard, you will toggle between fullscreen and windowed mode, and regardless of which mode you're in, you'll be rendering to the full area.

With our window fully fleshed out, we can finally switch to doing some actual graphics in the next lesson. See you there!