Введение
Многие десктопные приложения работают не с единственным главным окном, а с несколькими независимыми окнами — палитры инструментов, отдельные окна документов, диалоги настроек, не привязанные к одному родителю. Управление жизненным циклом, видимостью и взаимодействием между несколькими окнами требует аккуратного подхода, особенно в части того, кто владеет памятью каждого окна и когда приложение должно завершиться. В статье разберём паттерны управления множественными окнами в 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).