Multiple Windows and Utility Windows

Learn how to manage multiple windows, and practical examples using utility windows.
This lesson is part of the course:

Game Dev with SDL2

Learn C++ and SDL development by creating hands on, practical projects inspired by classic retro games

Free, Unlimited Access
Abstract art representing computer programming
Ryan McCombe
Ryan McCombe
Updated

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();
  }
}

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

Creating a Window Manager

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;
};

Utility 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:

Screenshot showing a utility window

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
);

Combining Utility Windows with Other Flags

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:

  1. Create a dropdown window when the user opens the menu, and destroy it when they’re done
  2. Keep a dropdown window alive, and switch it between being shown and hidden as appropriate

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:

Screenshot showing a dropdown window

Summary

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.

Key Takeaways

  • SDL2 supports multiple windows via SDL_CreateWindow().
  • Utility windows (e.g., tooltips, dropdown menus) are created with specific window flags like SDL_WINDOW_TOOLTIP.
  • Managing multiple windows efficiently involves using identifiers like windowID and helper classes like WindowManager.
  • Utility windows are often hidden instead of destroyed for performance reasons.
  • Event handling can identify specific windows using SDL_GetWindowFromID().

Was this lesson useful?

Next Lesson

Video Displays

Learn how to handle multiple monitors in SDL, including creating windows on specific displays.
Abstract art representing computer programming
Ryan McCombe
Ryan McCombe
Updated
Lesson Contents

Multiple Windows and Utility Windows

Learn how to manage multiple windows, and practical examples using utility windows.

sdl2-promo.jpg
This lesson is part of the course:

Game Dev with SDL2

Learn C++ and SDL development by creating hands on, practical projects inspired by classic retro games

Free, Unlimited Access
Window Management
sdl2-promo.jpg
This lesson is part of the course:

Game Dev with SDL2

Learn C++ and SDL development by creating hands on, practical projects inspired by classic retro games

Free, unlimited access

This course includes:

  • 67 Lessons
  • 100+ Code Samples
  • 91% Positive Reviews
  • Regularly Updated
  • Help and FAQ
Next Lesson

Video Displays

Learn how to handle multiple monitors in SDL, including creating windows on specific displays.
Abstract art representing computer programming
Contact|Privacy Policy|Terms of Use
Copyright © 2024 - All Rights Reserved