GraphicsBlast
The Basics | C++ | Windows | 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 our window 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.

Windows displays an icon for each program in the titlebar of the window, as well as in the taskbar, task manager, and various other places too. We can set this icon from within our code by first loading an image from the hard drive, and then passing it to SDL.

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?

To install it we'll again need to get a copy of the code from the repository's GitHub page.

Just like before, click the "Code" button to get a zip of the latest version of the code, and extract it somewhere convienient.

When we installed SDL itself before, we had to also download a compiled version of the library as the official version 3 of the library hasn't been released yet. Fortunately, that compiled version also includes everything we need for SDL_image as well, so we can skip having to update our linker for this library.

Therefore to get it working, after downloading the source code, we just need to include it's header files in our Makefile:

3.
OBJS = main.cpp
4.
+ 5.
INCLUDE_DIRS = -IC:\SDL3\include -IC:\SDL3_image\include -IC:\glew\include
6.
7.
LINKER_DIRS = -LC:\SDL3_precompiled\lib -LC:\glew\lib\Release\x64

Again we don't need to touch the LINKER_DIRS variable as that path already has the SDL_image library inside of it. We do need to specify the name of the library though:

7.
LINKER_DIRS = -LC:\SDL3_precompiled\lib -LC:\glew\lib\Release\x64
8.
+ 9.
LIBRARIES = -lmingw32 -lSDL3 -lSDL3_image -lopengl32 -lglew32
10.
11.
FLAGS = -Wall -Wl,-subsystem,console

Finally, to get SDL_image working, you need to make sure that it's DLL file is either placed alongside your executable, or installed system-wide.

With that in place, we should now be able to use the library in our code. To use it, we need to start by including it's header files in our main.cpp:

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

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

38.
    if(window == NULL)
39.
    {
40.
        printf("Unable to create a window: %s\n", SDL_GetError());
41.
        return -1;
42.
    }
43.
+ 44.
    SDL_Surface* icon = IMG_Load("resources/icon.png");
+ 45.
    if(icon == NULL)
+ 46.
    {
+ 47.
        printf("Unable to load icon: %s\n", SDL_GetError());
+ 48.
        return -1;
+ 49.
    }
50.
    ...

I've stored my icon image inside a new "resources" folder, just to keep these files separate from my source code. As this is a relative path to the image, you will need to make sure that however you distribute your compiled executable, the icon remains in a subfolder with the same name, or else the executable will not be able to find the image.

Calling the function IMG_Load from SDL_image 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. If the image loads, we can then set it as our icon:

44.
    SDL_Surface* icon = IMG_Load("resources/icon.png");
45.
    if(icon == NULL)
46.
    {
47.
        printf("Unable to load icon: %s\n", SDL_GetError());
48.
        return -1;
49.
    }
+ 50.
    if(SDL_SetWindowIcon(window, icon) != 0)
+ 51.
    {
+ 52.
        SDL_DestroySurface(icon);
+ 53.
        printf("Unable to set window icon: %s\n", SDL_GetError());
+ 54.
        return -1;
+ 55.
    }
+ 56.
    SDL_DestroySurface(icon);
57.
58.
    context = SDL_GL_CreateContext(window);
59.
    if(context == NULL)
60.
    {

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 worked or not. 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 it worked 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 into a common decompressed format, so whether you're loading from 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.

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.

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

We're going to finish up this lesson by looking at fullscreening our window. 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:

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

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

110.
        else if(event.type == SDL_EVENT_KEY_DOWN)
111.
        {
112.
            printf("Key pressed down: %s\n", SDL_GetKeyName(event.key.keysym.sym));
113.
            if(event.key.keysym.sym == SDLK_ESCAPE)
114.
            {
115.
                programRunning = false;
116.
                return;
117.
            }
+ 118.
            else if(event.key.keysym.sym == SDLK_f)
+ 119.
            {
+ 120.
                isFullscreen = !isFullscreen;
+ 121.
                SDL_SetWindowFullscreen(window, isFullscreen);
+ 122.
            }
123.
        }

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 we don't update it and enlarge our window, all the pixels in the new area will be in an undefined state.

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:

101.
            programRunning = false;
102.
            return;
103.
        }
+ 104.
        else if(event.type == SDL_EVENT_WINDOW_RESIZED)
+ 105.
        {
+ 106.
            windowWidth = event.window.data1;
+ 107.
            windowHeight = event.window.data2;
+ 108.
            glViewport(0, 0, windowWidth, windowHeight);
+ 109.
        }
110.
        else if(event.type == SDL_EVENT_KEY_DOWN)
111.
        {

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!