Video Displays

Learn how to handle multiple monitors in SDL3, including creating windows on specific displays.

Ryan McCombe
Updated

Modern games and applications often require precise control over display management. In this lesson, you'll learn to retrieve monitor counts, display names, and manage window placement across different displays using SDL3.

In the context of games, the concepts we cover in this lesson are primarily useful for letting players choose which monitor they want our game to run on:

Screenshot of the Dishonored 2 options menu

Starting Point

This lesson is the first in a new chapter focused on advanced windowing and display topics. To keep the examples clear and focused, we'll start with a minimal project structure similar to what we used at the beginning of the course.

This includes a main.cpp with a basic application loop and a simplified Window.h class:

Files

src
Select a file to view its content

Getting Display Information

SDL associates an SDL_DisplayID with every connected display. This type is a simple integer, which we pass to various other SDL functions when we're querying or configuring our displays.

We can get the SDL_DisplayID of the primary display using the SDL_GetPrimaryDisplay() function:

SDL_DisplayID DisplayID{SDL_GetPrimaryDisplay()};

An SDL_DisplayID is a simple integer, and the SDL API uses the 0 value to represent errors. So, if SDL_GetPrimaryDisplay() fails, it will return 0:

SDL_DisplayID DisplayID{SDL_GetPrimaryDisplay()};

if (!DisplayID) {
  std::cout << "Couldn't get primary display: "
    << SDL_GetError();
}

Working with Multiple Displays

To understand how many displays we're working with, and what IDs we use to identify them, we can call the SDL_GetDisplays() function.

The SDL_GetDisplays() function returns a pointer to a C-style array of SDL_DisplayIDs and provides the count of displays through an output parameter.

We need to prompt SDL to free this memory when we no longer require it, by passing the pointer to SDL_free():

src/main.cpp

#include <SDL3/SDL.h>
#include <SDL3/SDL_main.h>
#include <iostream>
#include "Window.h"

int main(int, char**) {
  SDL_Init(SDL_INIT_VIDEO);

  int DisplayCount{0};
  SDL_DisplayID* Displays{
    SDL_GetDisplays(&DisplayCount)
  };

  if (Displays) {
    std::cout << "Displays: " << DisplayCount;
    
    // Remember to free the memory!
    SDL_free(Displays);
  }

SDL_Quit(); return 0; }

We can see how many IDs are in the array - that is, how many monitors are detected on the system we're running on - by examining the output parameter.

If we have 4 connected displays, for example, our program should detect that:

Displays: 4

Handling SDL_GetDisplays() Errors

If SDL_GetDisplays() fails, it will return a nullptr. This typically indicates an error, which can be further investigated using SDL_GetError(). Below, we attempt to get the displays before we have initialized the video subsystem, which results in an error:

src/main.cpp

#include <SDL3/SDL.h>
#include <SDL3/SDL_main.h>
#include <iostream>
#include "Window.h"

int main(int, char**) {
  // SDL_Init(SDL_INIT_VIDEO); 

  int DisplayCount{0};
  SDL_DisplayID* Displays{
    SDL_GetDisplays(&DisplayCount)
  };

  if (Displays) {
    std::cout << "Displays: " << DisplayCount;
    SDL_free(Displays);
  } else {
    std::cout << "Could not get displays: "
      << SDL_GetError();
  }
  
SDL_Quit(); return 0; }
Could not get displays: Video subsystem has not been initialized

Getting a Display Name

When presenting options for our player to select a monitor, it can be helpful to identify the displays by name. We can do this by passing an SDL_DisplayID to the SDL_GetDisplayName() function.

This will return a const char* string containing the name of the display.

#include <SDL3/SDL.h>
#include <SDL3/SDL_main.h>
#include <iostream>
#include "Window.h"

int main(int, char**) {
  SDL_Init(SDL_INIT_VIDEO);

  int DisplayCount{0};
  SDL_DisplayID* Displays{
    SDL_GetDisplays(&DisplayCount)
  };

  if (Displays) {
    std::cout << "First Display Name: "
      << SDL_GetDisplayName(Displays[0]);
    SDL_free(Displays);
  }
  
SDL_Quit(); return 0; }
First Display Name: DELL S2721DGF

Listing All Display Names

Knowing the names of all connected displays can be useful for games or applications that allow players to select a monitor. To achieve this, we can iterate over the array of SDL_DisplayIDs returned by SDL_GetDisplays() and pass each of their SDL_DisplayIDs to SDL_GetDisplayName():

src/main.cpp

#include <SDL3/SDL.h>
#include <SDL3/SDL_main.h>
#include <iostream>
#include "Window.h"

int main(int, char**) {
  SDL_Init(SDL_INIT_VIDEO);

  int DisplayCount{0};
  SDL_DisplayID* Displays{
    SDL_GetDisplays(&DisplayCount)
  };

  if (Displays) {
    for (int i = 0; i < DisplayCount; ++i) {
      const char* DisplayName{
        SDL_GetDisplayName(Displays[i])
      };
      if (DisplayName) {
        std::cout << "Display " << Displays[i]
          << ": " << DisplayName << '\n';
      } else {
        std::cout << "Display " << Displays[i]
          << ": Unknown (" << SDL_GetError() << ")\n";
      }
    }
    SDL_free(Displays);
  } else {
    std::cout << "Error: " << SDL_GetError();
    SDL_Quit();
    return 1;
  }
  
SDL_Quit(); return 0; }
Display 1: DELL S2721DGF
Display 2: DELL S2721DGF
Display 3: DELL U2515H
Display 4: DELL U2515H

Handling SDL_GetDisplayName() Errors

The SDL_GetDisplayName() function will return a nullptr if it is unable to get the name of the display with the ID we provide. We can call SDL_GetError() for an explanation of this failure. Below, we provide an invalid display ID of 0:

src/main.cpp

#include <SDL3/SDL.h>
#include <SDL3/SDL_main.h>
#include <iostream>
#include "Window.h"

int main(int, char**) {
  SDL_Init(SDL_INIT_VIDEO);

  const char* Name{SDL_GetDisplayName(0)};
  if (Name) {
    std::cout << "Display 0: " << Name;
  } else {
    std::cout << "Cannot get display name: "
      << SDL_GetError();
  }
  
SDL_Quit(); return 0; }
Cannot get display name: Invalid display

Creating Windows on Specific Monitors

As before, to set the position of our window, we use the more flexible properties system with SDL_CreateWindowWithProperties().

To create a window on a specific monitor, we can explicity chose x and y values that are within the bounds of that display. We cover how to do that in the next section.

A simpler alternative is to use two helper macros that SDL provides:

  • SDL_WINDOWPOS_CENTERED_DISPLAY(x) to center the window on a specific display , where x is the SDL_DisplayID.
  • SDL_WINDOWPOS_UNDEFINED_DISPLAY(x) to let the platform choose the exact position on that display , where x is the SDL_DisplayID of that display.

For example, the following program creates two windows, each centered on a different monitor. We'll get the list of displays, and then use the ID of the first two displays to create our windows:

src/main.cpp

#include <SDL3/SDL.h>
#include <SDL3/SDL_main.h>
#include <iostream>
#include "Window.h"

int main(int, char**) {
  SDL_Init(SDL_INIT_VIDEO);

  int DisplayCount{0};
  SDL_DisplayID* Displays{
    SDL_GetDisplays(&DisplayCount)
  };
  
  if (!Displays || DisplayCount < 2) {
    std::cout << "Need at least two displays.\n";
    SDL_Quit();
    return 1;
  }

  // Properties for Window 1 on Displays[0]
  SDL_PropertiesID Props1{SDL_CreateProperties()};
  
  // Positioning Window 1 on Displays[0]
  SDL_SetNumberProperty(Props1,
    SDL_PROP_WINDOW_CREATE_X_NUMBER,
    SDL_WINDOWPOS_CENTERED_DISPLAY(Displays[0]));
  SDL_SetNumberProperty(Props1,
    SDL_PROP_WINDOW_CREATE_Y_NUMBER,
    SDL_WINDOWPOS_CENTERED_DISPLAY(Displays[0])
  );
  
  // Other Window 1 Properties
  SDL_SetStringProperty(Props1,
    SDL_PROP_WINDOW_CREATE_TITLE_STRING,
    "First Monitor");
  SDL_SetNumberProperty(Props1,
    SDL_PROP_WINDOW_CREATE_WIDTH_NUMBER, 400);
  SDL_SetNumberProperty(Props1,
    SDL_PROP_WINDOW_CREATE_HEIGHT_NUMBER, 400);
    
  // Create the window
  SDL_Window* Window1{
    SDL_CreateWindowWithProperties(Props1)};
  SDL_DestroyProperties(Props1);

  // Properties for Window 2 on Displays[1]
  SDL_PropertiesID Props2{SDL_CreateProperties()};
  
  SDL_SetNumberProperty(Props2,
    SDL_PROP_WINDOW_CREATE_X_NUMBER,
    SDL_WINDOWPOS_CENTERED_DISPLAY(Displays[1]));
  SDL_SetNumberProperty(Props2,
    SDL_PROP_WINDOW_CREATE_Y_NUMBER,
    SDL_WINDOWPOS_CENTERED_DISPLAY(Displays[1])
  );
  
  // Other Window 2 Properties
  SDL_SetStringProperty(Props2,
    SDL_PROP_WINDOW_CREATE_TITLE_STRING,
    "Second Monitor");
  SDL_SetNumberProperty(Props2,
    SDL_PROP_WINDOW_CREATE_WIDTH_NUMBER, 400);
  SDL_SetNumberProperty(Props2,
    SDL_PROP_WINDOW_CREATE_HEIGHT_NUMBER, 400);
    
  // Create the Window
  SDL_Window* Window2{
    SDL_CreateWindowWithProperties(Props2)};
  SDL_DestroyProperties(Props2);

  SDL_free(Displays);

SDL_Quit(); return 0; }

Getting Display Bounds

To understand the size and layout of our user's monitors, we can get their display bounds. This involves passing the SDL_DisplayID and a pointer to an SDL_Rect to the SDL_GetDisplayBounds() function:

SDL_Rect Bounds;
SDL_GetDisplayBounds(DisplayID, &Bounds);

As we've covered in the past, an SDL_Rect combines 4 integers to represent a rectangle. SDL_GetDisplayBounds() updates these members with values representing the position and size of a given display. These values are useful if we want some operations, such as moving a window, to target a specific monitor. We'll demonstrate this use case in the next section.

Below, we iterate through all of our displays, and log out their bounds:

src/main.cpp

#include <iostream>
#include <SDL3/SDL.h>
#include <SDL3/SDL_main.h>
#include "Window.h"

int main(int, char**) {
  SDL_Init(SDL_INIT_VIDEO);

  int DisplayCount{0};
  SDL_DisplayID* Displays{SDL_GetDisplays(&DisplayCount)};

  if (Displays) {
    SDL_Rect Bounds;
    for (int i = 0; i < DisplayCount; ++i) {
      SDL_GetDisplayBounds(Displays[i], &Bounds);
      std::cout << "[Display " << Displays[i]
        << "] Left: " << Bounds.x
        << ", Top: " << Bounds.y
        << ", Width: " << Bounds.w
        << ", Height: " << Bounds.h << '\n';
    }
    SDL_free(Displays);
  }

SDL_Quit(); return 0; }
[Display 1] Left: 0, Top: 0, Width: 2560, Height: 1440
[Display 2] Left: 2560, Top: 0, Width: 2560, Height: 1440
[Display 3] Left: 14, Top: -1440, Width: 2560, Height: 1440
[Display 4] Left: 2574, Top: -1440, Width: 2560, Height: 1440

Moving Windows to Specific Monitors

Once we've used SDL_GetDisplayBounds() to understand the size and position of a monitor, we can move a window onto that monitor by setting its position to within those bounds.

Below, we move the window to the top left of display 1 if the user presses 1 on their keyboard, and to the top left of display 2 if the user presses 2:

src/main.cpp

#include <iostream>
#include <SDL3/SDL.h>
#include <SDL3/SDL_main.h>
#include "Window.h"

void HandleKeydownEvent(
  const SDL_KeyboardEvent& E,
  SDL_DisplayID* Displays,
  int DisplayCount
) {
  SDL_Window* Window{
    SDL_GetWindowFromID(E.windowID)
  };

  SDL_Rect Bounds;
  if (E.key == SDLK_1 && DisplayCount > 0) {
    SDL_GetDisplayBounds(Displays[0], &Bounds);
    SDL_SetWindowPosition(Window, Bounds.x, Bounds.y);
  } else if (E.key == SDLK_2 && DisplayCount > 1) {
    SDL_GetDisplayBounds(Displays[1], &Bounds);
    SDL_SetWindowPosition(Window, Bounds.x, Bounds.y);
  }
}

int main(int, char**) {
  SDL_Init(SDL_INIT_VIDEO);
  Window GameWindow;

  int DisplayCount{0};
  SDL_DisplayID* Displays{
    SDL_GetDisplays(&DisplayCount)
  };

  SDL_Event E;
  bool IsRunning = true;
  while (IsRunning) {
    while (SDL_PollEvent(&E)) {
      if (E.type == SDL_EVENT_KEY_DOWN) {
        HandleKeydownEvent(E.key, Displays, DisplayCount);
      } else if (E.type == SDL_EVENT_QUIT) {
        IsRunning = false;
      }
    }
    GameWindow.Update();
  }

  if (Displays) {
    SDL_free(Displays);
  }
  SDL_Quit();
  return 0;
}

Window Borders

In a previous chapter, we introduced the notion of window decorations, which SDL refers to as borders. These can include elements like the title bar.

When setting a window position, we are setting the position of the window's top-left corner, excluding decorations. The borders are added outside this area, so we typically want to ensure we leave enough room for them.

We can do this using the SDL_GetWindowBordersSize() function, which we covered in our lesson on .

Get Display of Window

If we have a window and need to find out which display it is on, we can use the SDL_GetDisplayForWindow() function. We pass it an SDL_Window pointer, and retrieve an SDL_DisplayID as the return value:

src/main.cpp

#include <SDL3/SDL.h>
#include <SDL3/SDL_main.h>
#include <iostream>
#include "Window.h"

int main(int, char**) {
  SDL_Init(SDL_INIT_VIDEO);
    Window GameWindow;

  SDL_DisplayID Display{
    SDL_GetDisplayForWindow(GameWindow.GetRaw())
  };
  std::cout << "Window is on display " << Display
    << " (" << SDL_GetDisplayName(Display) << ")";

SDL_Quit(); return 0; }
Window is on display 1 (DELL S2721DGF)

In this example, we log out the name of the display the window is currently on when the player presses their spacebar:

src/main.cpp

#include <SDL3/SDL.h>
#include <SDL3/SDL_main.h>
#include <iostream>
#include "Window.h"

void HandleKeydownEvent(const SDL_KeyboardEvent& E) {
  if (E.key != SDLK_SPACE) return;

  SDL_Window* Window{
    SDL_GetWindowFromID(E.windowID)
  };
  SDL_DisplayID Display{
    SDL_GetDisplayForWindow(Window)
  };
  std::cout << "Window is on display " << Display
    << " (" << SDL_GetDisplayName(Display) << ")\n";
}

int main(int, char**) {
  SDL_Init(SDL_INIT_VIDEO);
  Window GameWindow;

  SDL_Event E;
  bool IsRunning = true;
  while (IsRunning) {
    while (SDL_PollEvent(&E)) {
      if (E.type == SDL_EVENT_KEY_DOWN) {
        HandleKeydownEvent(E.key);
      } else if (E.type == SDL_EVENT_QUIT) {
        IsRunning = false;
      }
    }
    GameWindow.Render();
    GameWindow.Update();
  }

  SDL_Quit();
  return 0;
}
Window is on display 1 (DELL S2721DGF)
Window is on display 3 (DELL U2515H)

Summary

This lesson covers detecting displays, fetching their properties, and dynamically creating and positioning windows across multiple screens. Key takeaways:

  • Access the number of connected monitors and their IDs with SDL_GetDisplays().
  • Use SDL_GetDisplayName() to display monitor names for user-friendly interfaces.
  • Create windows on specific displays using SDL_CreateWindowWithProperties() and the SDL_PROP_WINDOW_CREATE_DISPLAY_ID_NUMBER property.
  • Understand display dimensions and layout with SDL_GetDisplayBounds().
  • Manage window decorations with SDL_GetWindowBordersSize() for better placement accuracy.
Next Lesson
Lesson 67 of 69

Fullscreen Windows

Learn how to create and manage fullscreen windows in SDL3, including desktop and exclusive fullscreen modes.

Have a question about this lesson?
Answers are generated by AI models and may not be accurate