Multiple Windows and Utility Windows

Learn how to manage multiple windows in SDL3, with practical examples using utility windows like tooltips and menus.

Ryan McCombe
Updated

In this lesson, we'll explore how to manage multiple windows using SDL3. We'll also introduce one of the primary use cases for these techniques, which is creating menus and tooltips.

Starting Point

This lesson builds on our earlier work. To focus on multi-window management, we will start with a minimal project containing a Window class and a main function with a basic application loop.

Files

src
Select a file to view its content

Creating Multiple Windows

As we might expect, our program can manage multiple windows by performing multiple invocations to SDL_CreateWindow():

src/main.cpp

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

int main(int, char**) {
  SDL_Init(SDL_INIT_VIDEO);
  SDL_Window* Window1{SDL_CreateWindow(
    "Window 1", 600, 300, 0
  )};

  SDL_Window* Window2{SDL_CreateWindow(
    "Window 2", 600, 300, 0
  )};

  SDL_Event E;
  bool IsRunning = true;
  while (IsRunning) {
    while (SDL_PollEvent(&E)) {
      if (E.type == SDL_EVENT_QUIT) {
        IsRunning = false;
      }
    }
    
    if (Window1) {
      // Render and update...
    }

    if (Window2) {
      // Render and update...
    }
  }

  SDL_Quit();
  return 0;
}

Note that this program may be difficult to close. You can close the program through your IDE, or platform-specific techniques such at Ctrl + Alt + Delete on Windows, or Force Quit on macOS. We'll cover how to close multiple-window programs later in this lesson.

Creating Windows on Demand

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:

src/main.cpp

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

SDL_Window* ExtraWindow{nullptr};

void HandleKeyboardEvent(const SDL_KeyboardEvent& E) {
  if (E.key == SDLK_SPACE && !ExtraWindow) {
    ExtraWindow = SDL_CreateWindow(
      "Extra Window", 300, 400, 0);
  } else if (E.key == SDLK_ESCAPE && ExtraWindow) {
    SDL_DestroyWindow(ExtraWindow);
    ExtraWindow = nullptr;
  }
}

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) {
        HandleKeyboardEvent(E.key);
      } else if (E.type == SDL_EVENT_QUIT) {
        IsRunning = false;
      }
    }
    GameWindow.Render();
    GameWindow.Update();
  }

  SDL_Quit();
  return 0;
}

Events and Window IDs

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 unique identifier that SDL assigns to each window.

We can get the SDL_Window pointer corresponding to a window ID by passing it to the SDL_GetWindowFromID() function:

void HandleWindowEvent(const SDL_WindowEvent& E) {
  SDL_Window* EventWindow{
    SDL_GetWindowFromID(E.windowID)};
  // ...
}

This technique becomes increasingly important when our program is managing multiple windows. Here's an example that logs a message when the mouse enters one of our two windows:

src/main.cpp

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

void HandleWindowEvent(const SDL_WindowEvent& E) {
  if (E.type != SDL_EVENT_WINDOW_MOUSE_ENTER) return;

  SDL_Window* EventWindow{
    SDL_GetWindowFromID(E.windowID)};

  if (EventWindow) {
    std::cout << "\nMouse entered "
      << SDL_GetWindowTitle(EventWindow);
  }
}

int main(int, char**) {
  SDL_Init(SDL_INIT_VIDEO);
  SDL_CreateWindow(
    "Window 1", 300, 200, 0);
  SDL_CreateWindow(
    "Window 2", 300, 200, 0);

  SDL_Event E;
  bool IsRunning = true;
  while (IsRunning) {
    while (SDL_PollEvent(&E)) {
      if (E.type >= SDL_EVENT_WINDOW_FIRST &&
          E.type <= SDL_EVENT_WINDOW_LAST) {
        HandleWindowEvent(E.window);
      } else if (E.type == SDL_EVENT_QUIT) {
        IsRunning = false;
      }
    }
  }

  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 this chapter:

Window Close Events

When a window is closed (e.g., by the user clicking the 'X' button), an SDL_EVENT_WINDOW_CLOSE_REQUESTED event is pushed to the queue.

In single-window applications, we generally don't need to care about this event. This is because, by default, SDL also dispatches an SDL_EVENT_QUIT when the last (or only) window is closed, and we're currently shutting down our application when we detect that event.

However, in multi-window programs, we need to react to these individual SDL_EVENT_WINDOW_CLOSE_REQUESTED events, too.

In the following example, we open two windows and, when a user requests one be closed, we dutifully destroy it. We'll offload this complexity to a WindowManager in the next section, but a basic implementation might look something like this:

src/main.cpp

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

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

  SDL_Window* Window1{SDL_CreateWindow(
    "Window 1", 600, 300, 0
  )};

  SDL_Window* Window2{SDL_CreateWindow(
    "Window 2", 600, 300, 0
  )};

  SDL_Event E;
  bool IsRunning = true;
  while (IsRunning) {
    while (SDL_PollEvent(&E)) {
      if (E.type == SDL_EVENT_WINDOW_CLOSE_REQUESTED) {
        SDL_Window* WindowToDestroy{SDL_GetWindowFromID(
          E.window.windowID
        )};
        if (WindowToDestroy == Window1) {
          Window1 = nullptr;
        } else if (WindowToDestroy == Window2) {
          Window2 = nullptr;
        }
        SDL_DestroyWindow(WindowToDestroy);
      } else if (E.type == SDL_EVENT_QUIT) {
        IsRunning = false;
      }
    }


    if (Window1) {
      // Render and update...
    }

    if (Window2) {
      // Render and update...
    }
  }

  SDL_Quit();
  return 0;
}

Disabling QUIT_ON_LAST_WINDOW_CLOSE

In some programs, we may want to keep our application running even if it currently has no windows. Perhaps we want to continue running in the background, and we may open a new window at some point in the future.

To prevent SDL from sending an SDL_EVENT_QUIT when the last window closes, we can set the SDL_HINT_QUIT_ON_LAST_WINDOW_CLOSE hint to "0":

SDL_SetHint(
  SDL_HINT_QUIT_ON_LAST_WINDOW_CLOSE,
  "0"
);

Alternatively, we can continue to allow the SDL_EVENT_QUIT to be sent, but change how we react to it based on conditional checks.

We don't necessarily need to end our application loop in receipt of such an event - we can choose what the most appropriate reaction should be based on the current situation.

Creating a Window Manager

Previously, we saw that when our program is managing a lot of objects, it can be helpful to introduce intermediate objects to hide that complexity.

Similarly, when our program manages multiple windows, creating a dedicated type to manage this 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 <SDL3/SDL.h>
#include "WindowManager.h"
#include "UIManager.h"
#include "WorldManager.h"

int main(int, char**) {
  // Initialization
  SDL_Init(SDL_INIT_VIDEO);
  UIManager UI;
  WorldManager World;
  WindowManager Windows;
  Windows.CreateWindow();
  Windows.CreateWindow();

  // Event Handling
  SDL_Event E;
  bool IsRunning = true;
  while (IsRunning ) {
    while (SDL_PollEvent(&E)) {
      World.HandleEvent(E);
      UI.HandleEvent(E);
      Windows.HandleEvent(E);
      if (E.type == SDL_EVENT_QUIT) {
        IsRunning = false;
      }
    }

    // Ticking
    World.Tick();
    UI.Tick();
    Windows.Tick();

    // Rendering
    Windows.Render();
    World.Render();
    UI.Render();
    
    // Buffer Swap
    Windows.Update();
  }
  
  SDL_Quit();
  return 0;
}

A starting implementation of a window manager might look something like this:

src/WindowManager.h

#pragma once
#include <iostream>
#include <SDL3/SDL.h>
#include <vector>

class WindowManager {
public:
  WindowManager() = default;

  void CreateWindow() {
    SDL_Window* NewWindow{SDL_CreateWindow(
      "Window", 200, 200, 0
    )};
    if (!NewWindow) {
      std::cout << "Error creating window: "
        << SDL_GetError();
      return;
    }
    Windows.push_back(NewWindow);
  }

  void DestroyWindow(SDL_WindowID WindowID) {
    std::erase_if(Windows, [WindowID](SDL_Window* Window) {
      if (Window && SDL_GetWindowID(Window) == WindowID) {
        SDL_DestroyWindow(Window);
        return true;
      }
      return false;
    });
  }

  void HandleEvent(SDL_Event& E) {
    if (E.type == SDL_EVENT_WINDOW_CLOSE_REQUESTED) {
      DestroyWindow(E.window.windowID);
    }
  }

  void Tick() {
    for (SDL_Window* Window : Windows) {
      if (Window) {
        // ...
      }
    }
  }

  void Render() {
    for (SDL_Window* Window : Windows) {
      if (Window) {
        // ...
      }
    }
  }

  void Update() {
    for (SDL_Window* Window : Windows) {
      if (Window) {
        // ...
      }
    }
  }

  WindowManager(const WindowManager&) = delete;
  WindowManager& operator=(const WindowManager&) = delete;

  ~WindowManager() {
    for (SDL_Window* Window : Windows) {
      if (Window) {
        SDL_DestroyWindow(Window);
      }
    }
  }

private:
  std::vector<SDL_Window*> Windows;
};

Utility Windows

Programs often support multiple windows to enhance user productivity by, for example, letting our program span across multiple monitors.

However, there is a more common scenario where we might create additional windows: to support UI elements that extend beyond the boundaries of our main window, such as tooltips or right-click menus:

SDL refers to these as Utility Windows. We can flag a window as a utility window through the SDL_WindowFlags. In SDL3, SDL_WINDOW_UTILITY is the primary flag for this purpose, replacing the older SDL_WINDOW_SKIP_TASKBAR flag.

Tooltip and Popup Menu Windows

For common UI elements like tooltips and menus, SDL3 provides dedicated flags: SDL_WINDOW_TOOLTIP and SDL_WINDOW_POPUP_MENU. A significant change from SDL2 is that these types of windows must now be created with a parent window.

This is done using SDL_CreatePopupWindow(), which takes the parent SDL_Window* as its first argument:

SDL_Window* ParentWindow = SDL_CreateWindow(
  "Primary Window", 600, 300, 0);

// Create a popup menu attached to the parent
SDL_Window* Popup = SDL_CreatePopupWindow(
  ParentWindow,
  100, 100, // Position relative to parent
  200, 200, // Size
  SDL_WINDOW_POPUP_MENU
);

Using these specific flags helps SDL and the underlying platform manage the window's behavior appropriately. For example, a tooltip window can't grab input focus.

Combining Utility Windows with Other Flags

We often combine utility window flags with other flags to achieve the desired behavior. For example, a popup menu should typically be borderless and always-on-top:

SDL_Window* Popup = SDL_CreatePopupWindow(
  ParentWindow, 100, 100, 200, 200,
  SDL_WINDOW_POPUP_MENU 
  | SDL_WINDOW_BORDERLESS
  | 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* Tooltip = SDL_CreatePopupWindow(
  ParentWindow, 100, 100, 200, 50,
  SDL_WINDOW_TOOLTIP | SDL_WINDOW_HIDDEN
);

// And later it becomes visible
SDL_ShowWindow(Tooltip);

Example: Dropdown Menu

Let's use a utility window to create a dropdown menu that opens at the user's mouse position when they right-click. A performant approach is to create the window once and then show or hide it as needed.

Let's create a DropdownWindow class to manage this:

src/Dropdown.h

#pragma once
#include <SDL3/SDL.h>
#include <iostream>

class DropdownWindow {
public:
  DropdownWindow(SDL_Window* Parent) {
    SDLWindow = SDL_CreatePopupWindow(
      Parent, 100, 200, 120, 200,
      SDL_WINDOW_POPUP_MENU |
      SDL_WINDOW_ALWAYS_ON_TOP |
      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() {
    float MouseX, MouseY;
    SDL_GetGlobalMouseState(&MouseX, &MouseY);

    int ParentX, ParentY;
    SDL_GetWindowPosition(
      SDL_GetWindowParent(SDLWindow), &ParentX, &ParentY
    );

    int RelativeX{int(MouseX) - ParentX};
    int RelativeY{int(MouseY) - ParentY};

    SDL_SetWindowPosition(SDLWindow, RelativeX, RelativeY);
    SDL_ShowWindow(SDLWindow);

    isOpen = true;
  }

  void Close() {
    SDL_HideWindow(SDLWindow);
    isOpen = false;
  }

  void HandleEvent(const SDL_Event& Event) {
    if (Event.type == SDL_EVENT_MOUSE_BUTTON_DOWN) {
      if (Event.button.button == SDL_BUTTON_RIGHT) {
        Open();
      } else if (Event.button.button == SDL_BUTTON_LEFT) {
        Close();
      }
    }
  }

  SDL_Surface* GetSurface() const {
    return SDL_GetWindowSurface(SDLWindow);
  }

  void Render() {
    if (!isOpen) return;
    
    const auto* Fmt = SDL_GetPixelFormatDetails(
      GetSurface()->format
    );
    
    SDL_FillSurfaceRect(
      GetSurface(),
      nullptr,
      SDL_MapRGB(Fmt, nullptr, 50, 50, 255)
    );
  }

  void Update() {
    if (!isOpen) return;
    SDL_UpdateWindowSurface(SDLWindow);
  }

private:
  SDL_Window* SDLWindow{nullptr};
  bool isOpen{false};
};

This class combines many of the window management techniques we covered through the rest of this chapter. However, the Open() method has some new concepts:

void Open() {
  float MouseX, MouseY;
  SDL_GetGlobalMouseState(&MouseX, &MouseY);

  int ParentX, ParentY;
  SDL_GetWindowPosition(
    SDL_GetWindowParent(SDLWindow), &ParentX, &ParentY
  );

  int RelativeX{int(MouseX) - ParentX};
  int RelativeY{int(MouseY) - ParentY};

  SDL_SetWindowPosition(SDLWindow, RelativeX, RelativeY);
  SDL_ShowWindow(SDLWindow);

  isOpen = true;
}

We want to open the dropdown menu next to the mouse, so we eventually want to use SDL_SetWindowPosition() to update the position of our DropdownMenu. However, we need to do some work to get the positional arguments for this function.

  1. We use the SDL_GetGlobalMouseState() function to retrieve the mouse position. We cover this function, and mouse management more generally, later in the course.
  2. When we're setting the position of a popup window, the positional arguments are relative to its parent, not the overall screen. So, we now need to get the parent window's position. We do this by calling SDL_GetWindowParent(), and passing it to SDL_GetWindowPosition().
  3. Finally, we use our global mouse position and parent window position to calculate the mouse's position relative to the parent window. We then use this to update the position of our popup window.

In many use cases, this logic could be simplified by controlling how mouse focus is handled. We'll cover this in chapter dedicated to mouse handling later in the course.

Creating a DropdownWindow

In our main function, we can now create a DropdownWindow and pass it our main GameWindow's raw pointer:

src/main.cpp

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

int main(int, char**) {
  SDL_Init(SDL_INIT_VIDEO);
  Window GameWindow;
  DropdownWindow Menu(GameWindow.GetRaw());

  SDL_Event E;
  bool IsRunning = true;
  while (IsRunning) {
    while (SDL_PollEvent(&E)) {
      Menu.HandleEvent(E);
      if (E.type == SDL_EVENT_QUIT) {
        IsRunning = false;
      }
    }
    GameWindow.Render();
    GameWindow.Update();
    Menu.Render();
    Menu.Update();
  }

  SDL_Quit();
  return 0;
}

Right-clicking in our program will now open a dropdown menu. Because the dropdown is a standalone window, it can extend beyond the boundaries of our primary window, similar to the behavior we're familiar with from other programs:

Summary

In this lesson, we explored techniques for managing multiple windows, including creating, destroying, and handling events for them.

We introduced the concept of utility windows, such as tooltips and dropdown menus, and demonstrated how to use window flags and SDL_CreatePopupWindow() for specialized behaviors.

Key Takeaways:

  • SDL3 supports multiple windows via SDL_CreateWindow() and SDL_CreatePopupWindow().
  • Utility windows must be created with a parent window in SDL3.
  • Managing multiple windows often involves using identifiers like windowID and adding helper classes like WindowManager.
  • Utility windows are often hidden instead of destroyed for performance reasons.
  • Event handling can identify specific windows using SDL_GetWindowFromID().
  • The SDL_EVENT_WINDOW_CLOSE_REQUESTED event lets you handle window closures.
Next Lesson
Lesson 58 of 61

Snake Game Core Components

Introducing the foundational components for our game and setting up the project

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