Пользовательский интерфейс в Qt

Многооконные Qt-приложения: управление несколькими QWidget/QML-окнами

Введение

Многие десктопные приложения работают не с единственным главным окном, а с несколькими независимыми окнами — палитры инструментов, отдельные окна документов, диалоги настроек, не привязанные к одному родителю. Управление жизненным циклом, видимостью и взаимодействием между несколькими окнами требует аккуратного подхода, особенно в части того, кто владеет памятью каждого окна и когда приложение должно завершиться. В статье разберём паттерны управления множественными окнами в Qt Widgets.

Концепция

Каждое окно верхнего уровня в Qt Widgets — это QWidget/QMainWindow без родителя (или с флагом Qt::Window). По умолчанию QApplication завершает event loop, когда закрывается последнее видимое окно (поведение, управляемое QGuiApplication::quitOnLastWindowClosed, включённым по умолчанию) — это важно учитывать при проектировании приложений, которые должны продолжать работать в фоне (например, со значком в системном трее) даже при закрытии всех видимых окон.

Для управления коллекцией независимых окон (документов, инструментов) удобен централизованный менеджер окон, который отслеживает список открытых окон и обеспечивает их корректное закрытие/удаление без утечек памяти, особенно учитывая, что окна без родителя не удаляются автоматически системой parent-child.

Пример кода: Многооконные Qt-приложения

// windowmanager.h
#pragma once
#include <QObject>
#include <QMainWindow>
#include <QList>

class DocumentWindow : public QMainWindow
{
    Q_OBJECT
public:
    explicit DocumentWindow(const QString &documentName, QWidget *parent = nullptr)
        : QMainWindow(parent), m_documentName(documentName)
    {
        setWindowTitle("Документ: " + documentName);
        resize(600, 400);
        setAttribute(Qt::WA_DeleteOnClose); // автоудаление при закрытии окна
    }

signals:
    void windowClosing(DocumentWindow *window);

protected:
    void closeEvent(QCloseEvent *event) override
    {
        emit windowClosing(this);
        QMainWindow::closeEvent(event);
    }

private:
    QString m_documentName;
};

class WindowManager : public QObject
{
    Q_OBJECT
public:
    static WindowManager &instance()
    {
        static WindowManager instance;
        return instance;
    }

    DocumentWindow *openDocument(const QString &name)
    {
        auto *window = new DocumentWindow(name);
        connect(window, &DocumentWindow::windowClosing, this, &WindowManager::onWindowClosing);
        m_openWindows.append(window);
        window->show();
        return window;
    }

    int openWindowCount() const { return m_openWindows.size(); }

private slots:
    void onWindowClosing(DocumentWindow *window)
    {
        m_openWindows.removeOne(window);
        qDebug() << "Окно закрыто, осталось открытых:" << m_openWindows.size();
    }

private:
    WindowManager() = default;
    QList<DocumentWindow *> m_openWindows;
};
// main.cpp
#include <QApplication>
#include <QMenuBar>
#include "windowmanager.h"

int main(int argc, char *argv[])
{
    QApplication app(argc, argv);

    // Приложение продолжает работать, даже если все документы закрыты
    // (например, чтобы пользователь мог открыть новый через главное меню)
    app.setQuitOnLastWindowClosed(false);

    WindowManager::instance().openDocument("Отчёт.docx");
    WindowManager::instance().openDocument("Презентация.pptx");

    return app.exec();
}

Пояснения к коду

DocumentWindow устанавливает атрибут Qt::WA_DeleteOnClose, что заставляет Qt автоматически вызвать delete для объекта окна сразу после его закрытия — без этого атрибута закрытое окно осталось бы существовать в памяти (хотя и скрытым), создавая утечку при многократном открытии/закрытии документов. Переопределение closeEvent() испускает сигнал windowClosing перед стандартной обработкой закрытия — это даёт WindowManager шанс удалить указатель на закрываемое окно из своего списка до того, как объект будет физически уничтожен.

app.setQuitOnLastWindowClosed(false) в main.cpp явно отключает стандартное поведение автоматического завершения приложения при закрытии последнего окна — это нужно, если приложение должно оставаться «живым» в фоне (например, доступным через системный трей) даже без открытых окон документов.

Подводные камни

  • Использование указателя на окно после его закрытия с Qt::WA_DeleteOnClose, без подписки на сигнал об уничтожении. Любой код, хранящий «сырой» указатель на такое окно, должен либо отслеживать его уничтожение через QObject::destroyed или QPointer, либо, как в примере, явно удалять указатель из своих структур в ответ на сигнал о закрытии — иначе обращение к уже удалённому объекту приведёт к неопределённому поведению.
  • Забытое отключение quitOnLastWindowClosed приводит к неожиданному завершению приложения, если по архитектуре оно должно продолжать работать без видимых окон (фоновые сервисы, приложения с системным треем) — стандартное поведение Qt рассчитано на типичные приложения с единственным главным окном и без него легко «выстрелить себе в ногу».
  • Несогласованное владение окнами между разными частями кода. Если несколько разных модулей приложения могут создавать окна одного типа без единого центрального менеджера, отслеживание реального числа открытых окон и предотвращение дублирования (например, повторное открытие уже открытого документа в новом окне вместо фокусировки существующего) становится затруднительным без централизации логики управления окнами.
  • Модальные диалоги, блокирующие весь event loop приложения (exec() диалога), в многооконном приложении могут неожиданно заблокировать взаимодействие с другими, логически независимыми окнами — если это нежелательно, стоит использовать немодальные диалоги (show() вместо exec()) или модальность, ограниченную конкретным родительским окном (Qt::WindowModal), а не всем приложением (Qt::ApplicationModal).