In this lesson, we’ll explore how to manage multiple windows using SDL2. We’ll also introduce one of the primary use cases for these techniques, which is creating menus and tooltips.
As we might expect, our program can manage multiple windows by performing multiple invocations to SDL_CreateWindow()
:
#include <SDL.h>
#include "Window.h"
int main(int argc, char** argv) {
SDL_Init(SDL_INIT_VIDEO);
Window GameWindow1;
Window GameWindow2;
SDL_Event E;
while (true) {
while (SDL_PollEvent(&E)) {
if (E.type == SDL_QUIT) {
SDL_Quit();
return 0;
}
}
GameWindow1.Update();
GameWindow2.Update();
GameWindow1.Render();
GameWindow2.Render();
}
}
Our windows do not need to be created at the same time. We can open additional windows in response to user events. Below, our program opens a second window when the user presses their space bar, and closes it when they press escape:
#include <SDL.h>
#include "Window.h"
SDL_Window* ExtraWindow{nullptr};
void HandleKeyboardEvent(SDL_KeyboardEvent& E) {
if (E.keysym.sym == SDLK_SPACE && !ExtraWindow) {
ExtraWindow = SDL_CreateWindow(
"Extra Window", 100, 200, 300, 400, 0);
} else if (E.keysym.sym == SDLK_ESCAPE) {
SDL_DestroyWindow(ExtraWindow);
ExtraWindow = nullptr;
}
}
int main(int argc, char** argv) {
SDL_Init(SDL_INIT_VIDEO);
Window GameWindow;
SDL_Event E;
while (true) {
while (SDL_PollEvent(&E)) {
if (E.type == SDL_KEYDOWN) {
HandleKeyboardEvent(E.key);
} else if (E.type == SDL_QUIT) {
SDL_Quit();
return 0;
}
}
GameWindow.Update();
GameWindow.Render();
}
}
When our program is managing multiple windows, it is often helpful to understand which window is associated with an event that shows up in our event loop.
To help with this, most SDL event types, such as SDL_MouseButtonEvent
and SDL_WindowEvent
, include a windowID
member.
This is a basic integer variable that SDL uses to uniquely identify each window. We can get the SDL_Window
pointer corresponding to a window ID by passing it to the SDL_GetWindowFromID()
function:
void HandleWindowEvent(SDL_WindowEvent& E) {
SDL_Window* EventWindow{
SDL_GetWindowFromID(E.windowID)};
// ...
}
This technique becomes increasingly important when our program is managing multiple windows, as it is the primary way we identify which window is associated with an event:
#include <SDL.h>
void HandleWindowEvent(SDL_WindowEvent& E) {
if (E.event != SDL_WINDOWEVENT_ENTER) return;
SDL_Window* EventWindow{
SDL_GetWindowFromID(E.windowID)};
std::cout << "\nMouse entered "
<< SDL_GetWindowTitle(EventWindow);
}
int main(int argc, char** argv) {
SDL_Init(SDL_INIT_VIDEO);
SDL_CreateWindow(
"Window 1", 100, 200, 100, 100, 0);
SDL_CreateWindow(
"Window 2", 300, 200, 100, 100, 0);
SDL_Event E;
while (true) {
while (SDL_PollEvent(&E)) {
if (E.type == SDL_WINDOWEVENT) {
HandleWindowEvent(E.window);
} else if (E.type == SDL_QUIT) {
SDL_Quit();
return 0;
}
}
}
}
Mouse entered Window 1
Mouse entered Window 2
Mouse entered Window 1
We covered window events and window IDs in more detail in a dedicated lesson earlier in the course:
Previously, we saw when our program is managing a lot of objects, it can be helpful to introduce intermediate objects to manage that complexity. This might include a UIManager
for managing interface elements and a WorldMananger
for managing objects being simulated in our world.
Similarly, when our program manages multiple windows, creating a dedicated type to manage this complexity can be helpful.
This helps to remove logic from important parts of our code, such as the event loop and main
function, thereby keeping them as clear as possible:
#include <SDL.h>
#include "WindowManager.h"
#include "UIManager.h"
#include "WorldManager.h"
int main(int argc, char** argv) {
// Initialization
SDL_Init(SDL_INIT_VIDEO);
WindowManager Windows;
UIManager UI;
WorldManager World;
Windows.CreateWindow();
Windows.CreateWindow();
Windows.CreateWindow();
// Event Handling
SDL_Event E;
while (true) {
while (SDL_PollEvent(&E)) {
World.HandleEvent(E);
UI.HandleEvent(E);
Windows.HandleEvent(E);
if (E.type == SDL_QUIT) {
SDL_Quit();
return 0;
}
}
// Ticking
World.Tick();
UI.Tick();
Windows.Tick();
// Rendering
World.Render();
UI.Render();
Windows.Render();
}
}
A starting implementation of a window manager might look something like this:
// WindowManager.h
#pragma once
#include <iostream>
#include <SDL.h>
#include <unordered_map>
class WindowManager {
public:
WindowManager() = default;
Uint32 CreateWindow() {
SDL_Window* NewWindow{
SDL_CreateWindow(
"Window", 100, 100, 200, 200, 0)};
if (!NewWindow) {
std::cout << "Error creating window: "
<< SDL_GetError();
return 0;
}
Uint32 WindowID{SDL_GetWindowID(NewWindow)};
Windows[WindowID] = NewWindow;
return WindowID;
}
void DestroyWindow(Uint32 WindowID) {
if (Windows[WindowID]) {
SDL_DestroyWindow(Windows[WindowID]);
Windows.erase(WindowID);
}
}
void HandleEvent(SDL_Event& E) {
// ...
}
void Tick() {
// ...
}
void Render() {
// ...
}
// Prevent copying of WindowManager objects
WindowManager(const WindowManager&) = delete;
WindowManager& operator=(const WindowManager&) = delete;
~WindowManager() {
for (auto [id, Window] : Windows) {
if (Window) { SDL_DestroyWindow(Window); }
}
}
private:
std::unordered_map<Uint32, SDL_Window*> Windows;
};
Programs often support multiple windows to enhance user productivity, particularly in software with complex, customizable UIs. Managing multiple windows allows users to organize their workspaces freely, leveraging features like multi-monitor setups for greater flexibility.
However, there is a subtle and more common scenario where our program should consider creating additional windows. This is to support UI elements that may need to extend beyond the boundaries of our main window.
This is common for elements that are expected to be near the user’s cursor, such as tooltips or right-click menus:
We may not consider these elements to be windows at all, but this is typically how they’re implemented. The SDL API calls these Utility Windows. We can flag a window as a utility window through the SDL_WindowFlags
:
SDL_CreateWindow(
"Primary Window", 100, 100, 200, 200, 0);
SDL_CreateWindow(
"Utility Window", 100, 100, 200, 200,
SDL_WINDOW_UTILITY
);
Utility windows share the same capabilities as regular windows. The reason we flag them as utility windows is so the underlying platform understands they’re secondary, supporting windows, and can handle and style them appropriately.
The use-case for utility windows being used to create tooltips and popup menus is so common that SDL includes dedicated window flags for them: SDL_WINDOW_TOOLTIP
and SDL_WINDOW_POPUP_MENU
Using these flags to explain the intent of our window can help SDL manage them more effectively. For example, a tooltip window can’t grab input focus or be minimized:
SDL_CreateWindow(
"Utility Window", 100, 100, 200, 200,
SDL_WINDOW_TOOLTIP
);
SDL_CreateWindow(
"Utility Window", 100, 100, 200, 200,
SDL_WINDOW_DROPDOWN
);
When creating a utility window, we often combine it with other window flags to create behaviors that are appropriate to our use case. For example, we often do not want a utility window to be included in the user’s taskbar.
To do this, we can combine the SDL_WINDOW_UTILITY
flag with SDL_WINDOW_SKIP_TASKBAR
:
SDL_CreateWindow(
"Utility Window", 100, 100, 200, 200,
SDL_WINDOW_UTILITY | SDL_WINDOW_SKIP_TASKBAR
);
It’s also common that we would want utility windows to be borderless and/or always-on-top:
SDL_CreateWindow(
"Utility Window", 100, 100, 200, 200,
SDL_WINDOW_UTILITY | SDL_WINDOW_BORDERLESS
);
SDL_CreateWindow(
"Utility Window", 100, 100, 200, 200,
SDL_WINDOW_UTILITY | SDL_WINDOW_ALWAYS_ON_TOP
);
In most programs, we also want utility windows to be initially hidden. We then later display the utility window based on user actions:
// The utility window is initially hidden
SDL_Window* Window{SDL_CreateWindow(
"Utility Window", 100, 100, 200, 200,
SDL_WINDOW_UTILITY | SDL_WINDOW_HIDDEN
)};
// And later it becomes visible
SDL_ShowWindow(UtilityWindow);
Let’s use a utility window to create the basic foundations of a dropdown menu that opens at the user’s mouse position when they right-click. There are two main ways we can approach this:
The first option can be problematic for performance reasons. Creating a window and getting everything in place to start rendering its contents can be an expensive process. We don’t want to incur that cost at the exact moment the user is performing an interaction, as it can make our program feel less responsive.
The second option is usually preferred in most situations. Let’s create a basic class that implements the showing and hiding approach:
// Dropdown.h
#pragma once
#include <SDL.h>
#include <iostream>
class DropdownWindow {
public:
DropdownWindow() {
SDLWindow = SDL_CreateWindow(
"Dropdown", 100, 200, 120, 200,
SDL_WINDOW_POPUP_MENU |
SDL_WINDOW_ALWAYS_ON_TOP |
SDL_WINDOW_SKIP_TASKBAR |
SDL_WINDOW_BORDERLESS |
SDL_WINDOW_HIDDEN);
if (!SDLWindow) {
std::cout << SDL_GetError();
}
}
~DropdownWindow() {
SDL_DestroyWindow(SDLWindow);
}
DropdownWindow(const DropdownWindow&) = delete;
DropdownWindow& operator=(
const DropdownWindow&) = delete;
void Open() {
// Get the mouse position
int x, y;
SDL_GetGlobalMouseState(&x, &y);
// Move the window next to the mouse and show it
SDL_SetWindowPosition(SDLWindow, x, y);
SDL_ShowWindow(SDLWindow);
isOpen = true;
// Enable mouse button events for clicks that
// change input focus
SDL_SetHint(
SDL_HINT_MOUSE_FOCUS_CLICKTHROUGH, "1");
}
void Close() {
SDL_HideWindow(SDLWindow);
isOpen = false;
// Disable mouse button events for clicks that
// change input focus
SDL_SetHint(
SDL_HINT_MOUSE_FOCUS_CLICKTHROUGH, "0");
}
void HandleEvent(SDL_Event& Event) {
if (Event.type == SDL_MOUSEBUTTONDOWN) {
SDL_MouseButtonEvent E{Event.button};
if (E.button == SDL_BUTTON_RIGHT) {
Open();
} else if (E.button == SDL_BUTTON_LEFT) {
Close();
}
}
}
SDL_Surface* GetSurface() {
return SDL_GetWindowSurface(SDLWindow);
}
void Render() {
if (!isOpen) return;
SDL_FillRect(
GetSurface(), nullptr, SDL_MapRGB(
GetSurface()->format, 150, 50, 50
)
);
}
void Update() {
if (!isOpen) return;
SDL_UpdateWindowSurface(SDLWindow);
}
SDL_Window* SDLWindow;
private:
bool isOpen{false};
};
This class uses some more advanced mouse techniques than we’ve covered so far, including the SDL_GetGlobalMouseState()
function and SDL_HINT_MOUSE_FOCUS_CLICKTHROUGH
hint. We cover these in more detail in our dedicated mouse management chapter later in the course.
In our main
function, we can create a DropdownWindow
and connect it to our application and event loops in the usual way:
#include <SDL.h>
#include "Window.h"
#include "Dropdown.h"
int main(int argc, char** argv) {
SDL_Init(SDL_INIT_VIDEO);
Window GameWindow;
DropdownWindow Menu;
SDL_Event E;
while (true) {
while (SDL_PollEvent(&E)) {
Menu.HandleEvent(E);
if (E.type == SDL_QUIT) {
SDL_Quit();
return 0;
}
}
Menu.Update();
GameWindow.Update();
Menu.Render();
GameWindow.Render();
}
}
Right-clicking in our program will now open a dropdown menu. And, because the dropdown menu is a standalone window, it can expand beyond the boundaries of our primary window:
In this lesson, we explored techniques for managing multiple windows in SDL2, including creating, destroying, and handling events for multiple windows.
We introduced the concept of utility windows, such as tooltips and dropdown menus, and demonstrated how to leverage SDL’s window flags for specialized behaviors.
Additionally, we implemented a WindowManager
class to simplify window management and created a dropdown menu system as a practical example of using utility windows.
SDL_CreateWindow()
.SDL_WINDOW_TOOLTIP
.windowID
and helper classes like WindowManager
.SDL_GetWindowFromID()
.Learn how to manage multiple windows, and practical examples using utility windows.
Learn C++ and SDL development by creating hands on, practical projects inspired by classic retro games