6.6. Многодокументный интерфейс.

Приложения, которые могут работать с несколькими документами, открываемыми в отдельных окнах и расположенных внутри главного окна, называют MDI-приложениями (MDI -- от англ. Multiple Document Interface). В Qt подобный интерфейс создается с помощью класса QWorkspace, назначаемого центральным виджетом. Каждое окно с открытым документом становится подчиненным, по отношению к QWorkspace.

В этом разделе мы создадим приложение Editor (текстовый редактор), изображенное на рисунке 6.14, чтобы продемонстрировать принципы создания MDI-приложений и оконных меню.

Рисунок 6.14. Внешний вид приложения Editor.


Приложение состоит из двух классов: MainWindow и Editor. Полный код приложения находится на CD, сопровождающем книгу, а поскольку он во многом похож на код, который мы писали в приложении Spreadsheet (в первой части книги), то мы будем описывать только ту часть реализации, которая является для нас еще незнакомой.

Рисунок 6.15. Меню приложения Editor.


Начнем с класса MainWindow. MainWindow::MainWindow(QWidget *parent, const char *name) : QMainWindow(parent, name) { workspace = new QWorkspace(this); setCentralWidget(workspace); connect(workspace, SIGNAL(windowActivated(QWidget *)), this, SLOT(updateMenus())); connect(workspace, SIGNAL(windowActivated(QWidget *)), this, SLOT(updateModIndicator())); createActions(); createMenus(); createToolBars(); createStatusBar(); setCaption(tr("Editor")); setIcon(QPixmap::fromMimeSource("icon.png")); } В конструкторе создается экземпляр класса QWorkspace и назначается центральным виджетом. Затем мы соединяем сигнал windowActivated(), класса QWorkspace, с двумя приватными слотами. Эти слоты гарантируют, что меню и строка состояния всегда будут соответствовать текущему активному окну. void MainWindow::newFile() { Editor *editor = createEditor(); editor->newFile(); editor->show(); } Слот newFile() соответствует пункту меню File|New. Он создает новое окно (класса Editor) с документом, вызывая приватную функцию createEditor(). Editor *MainWindow::createEditor() { Editor *editor = new Editor(workspace); connect(editor, SIGNAL(copyAvailable(bool)), this, SLOT(copyAvailable(bool))); connect(editor, SIGNAL(modificationChanged(bool)), this, SLOT(updateModIndicator())); return editor; } Функция createEditor() создает виджет класса Editor и устанавливает два соединения типа сигнал-слот. Первое соответствует пунктам меню Edit|Cut и Edit|Copy. Доступность этих пунктов меню разрешается или запрещается, в зависимости от наличия выделенного текста. Второе соединение отвечает за обновление индикатора MOD (признак наличия в документе несохраненных изменений), который находится в строке состояния.

Поскольку мы имеем дело с многодокументным интерфейсом, то вполне возможно, что одновременно могут оказаться открытыми несколько окон с документами. Вас может обеспокоить этот факт, поскольку интерес для нас представляют сигналы copyAvailable(bool) и modificationChanged(), исходящие только от активного окна. На самом деле это не может служить причиной для беспокойства, поскольку сигналы могут подавать только активные окна.

void MainWindow::open() { Editor *editor = createEditor(); if (editor->open()) editor->show(); else editor->close(); } Функция open() соответствует пункту меню File|Open. Она создает новое окно Editor и вызывает метод Editor::open(). Если функция Editor::open() завершается с ошибкой, то окно редактора просто закрывается, поскольку пользователь уже был извещен о возникших проблемах. void MainWindow::save() { if (activeEditor()) { activeEditor()->save(); updateModIndicator(); } } Слот save() вызывает функцию save() активного окна. Опять таки, весь код, который фактически сохраняет файл, находится в классе Editor. Editor *MainWindow::activeEditor() { return (Editor *)workspace->activeWindow(); } Приватная функция activeEditor() возвращает указатель на активное окно редактора. void MainWindow::cut() { if (activeEditor()) activeEditor()->cut(); } Слот cut() вызывает функцию cut() активного окна. Слоты copy(), paste() и del() реализованы аналогичным образом. void MainWindow::updateMenus() { bool hasEditor = (activeEditor() != 0); saveAct->setEnabled(hasEditor); saveAsAct->setEnabled(hasEditor); pasteAct->setEnabled(hasEditor); deleteAct->setEnabled(hasEditor); copyAvailable(activeEditor() && activeEditor()->hasSelectedText()); closeAct->setEnabled(hasEditor); closeAllAct->setEnabled(hasEditor); tileAct->setEnabled(hasEditor); cascadeAct->setEnabled(hasEditor); nextAct->setEnabled(hasEditor); previousAct->setEnabled(hasEditor); windowsMenu->clear(); createWindowsMenu(); } Слот updateMenus() вызывается всякий раз, когда активизируется другое окно (или когда закрывается последнее окно с документом), с целью обновления системы меню. Большинство из пунктов меню имеют смысл только при наличии активного дочернего окна, поэтому мы запрещаем некоторые пункты меню, если нет ни одного окна с открытым документом. Затем очищается меню Windows и вызывается функция createWindowsMenu(), которая обновляет список открытых дочерних окон. void MainWindow::createWindowsMenu() { closeAct->addTo(windowsMenu); closeAllAct->addTo(windowsMenu); windowsMenu->insertSeparator(); tileAct->addTo(windowsMenu); cascadeAct->addTo(windowsMenu); windowsMenu->insertSeparator(); nextAct->addTo(windowsMenu); previousAct->addTo(windowsMenu); if (activeEditor()) { windowsMenu->insertSeparator(); windows = workspace->windowList(); int numVisibleEditors = 0; for (int i = 0; i < (int)windows.count(); ++i) { QWidget *win = windows.at(i); if (!win->isHidden()) { QString text = tr("%1 %2") .arg(numVisibleEditors + 1) .arg(win->caption()); if (numVisibleEditors < 9) text.prepend("&"); int id = windowsMenu->insertItem( text, this, SLOT(activateWindow(int))); bool isActive = (activeEditor() == win); windowsMenu->setItemChecked(id, isActive); windowsMenu->setItemParameter(id, i); ++numVisibleEditors; } } } } Приватная функция createWindowsMenu() заполняет меню Windows действиями (action) и дополняет списком открытых окон. Перечень пунктов типичен для меню подобного рода и соответствующие им действия легко реализуются с помощью слотов QWorkspace -- closeActiveWindow(), closeAllWindows(), tile() и cascade().

Активное окно, в списке, отмечается маркером, напротив имени документа. Когда пользователь выбирает пункт меню, соответствующий открытому документу, вызывается слот activateWindow(), которому в качестве аргумента передается индекс в массиве windows. Это очень похоже на то, что мы делали в Главе 3, когда создавали список недавно открывавшихся документов.

Для первых девяти пунктов меню мы добавили символ амперсанда, перед порядковым номером пункта меню, чтобы можно было быстро перемещаться между открытыми документами, с помощью горячих клавиш.

void MainWindow::activateWindow(int param) { QWidget *win = windows.at(param); win->show(); win->setFocus(); } Функция activateWindow() вызывается, когда пользователь выбирает какое либо окно с документом, из меню Windows. Параметр param -- это индекс выбранного окна, в массиве windows. void MainWindow::copyAvailable(bool available) { cutAct->setEnabled(available); copyAct->setEnabled(available); } Слот copyAvailable() вызывается, когда выделяется какой либо текст (или наоборот, когда выделение снимается) в окне редактора. Он так же вызывается из updateMenus(). И разрешает или запрещает пункты меню Cut и Copy. void MainWindow::updateModIndicator() { if (activeEditor() && activeEditor()->isModified()) modLabel->setText(tr("MOD")); else modLabel->clear(); } Функция updateModIndicator() обновляет индикатор MOD в строке состояния. Вызывается при любом изменении текста в окне редактора, а так же при активации другого окна. void MainWindow::closeEvent(QCloseEvent *event) { workspace->closeAllWindows(); if (activeEditor()) event->ignore(); else event->accept(); } Функция closeEvent() закрывает все дочерние окна. Если какое либо из окон "проигнорирует" событие "close" (например в том случае, когда пользователь отменил закрытие окна, имевшее несохраненные данные), то это событие так же игнорируется и главным окном приложения MainWindow. В противном случае событие "принимается" и Qt закрывает окно. Если не перекрыть этот обработчик, то у пользователя не будет возможности записать на диск несохраненные данные.

На этом мы завершаем обзор класса MainWindow и переходим к реализации класса Editor. Этот класс представляет собой одно дочернее окно. Он порожден от класса QTextEdit, который реализует всю необходимую функциональность по редактированию текста. Так же, как и любой другой виджет Qt, QTextEdit может использоваться как дочернее окно в рабочей области MDI.

Ниже приводится определение класса:

class Editor : public QTextEdit { Q_OBJECT public: Editor(QWidget *parent = 0, const char *name = 0); void newFile(); bool open(); bool openFile(const QString &fileName); bool save(); bool saveAs(); QSize sizeHint() const; signals: void message(const QString &fileName, int delay); protected: void closeEvent(QCloseEvent *event); private: bool maybeSave(); void saveFile(const QString &fileName); void setCurrentFile(const QString &fileName); QString strippedName(const QString &fullFileName); bool readFile(const QString &fileName); bool writeFile(const QString &fileName); QString curFile; bool isUntitled; QString fileFilters; }; Четыре приватных функции, которые обсуждались нами при создании приложения Spreadsheet, аналогичным образом реализованы и в классе Editor. Это функции maybeSave(), saveFile(), setCurrentFile() и strippedName(). Editor::Editor(QWidget *parent, const char *name) : QTextEdit(parent, name) { setWFlags(WDestructiveClose); setIcon(QPixmap::fromMimeSource("document.png")); isUntitled = true; fileFilters = tr("Text files (*.txt)\n" "All files (*)"); } В конструкторе, с помощью функции setWFlags(), взводится флаг WDestructiveClose. Если конструктор класса не принимает флаги в качестве аргументов, как это имеет место быть в случае с QTextEdit, то мы можем установить флаги вызовом setWFlags().

Так как мы позволяем пользователям одновременно открывать несколько документов, необходимо предусмотреть какие либо характеристики окон, чтобы потом пользователи могли как-то их отличать между собой, до того, как вновь создаваемые документы будут сохранены. Самый распространенный способ -- присваивать документам имена по-умолчанию, которые включают в себя порядковый номер (например, document1.txt). Для этой цели мы используем переменную isUntitled, которая отличает имена документов, уже существующих, и имена документов, которым имя еще не было присвоено пользователем.

После вызова конструктора должна вызываться одна из двух функций -- либо newFile(), либо open().

void Editor::newFile() { static int documentNumber = 1; curFile = tr("document%1.txt").arg(documentNumber); setCaption(curFile); isUntitled = true; ++documentNumber; } Функция newFile() генерирует новое имя документа, например document2.txt. Этот код помещен в newFile(), а не в конструктор, потому что нет необходимости вести счетчик создаваемых документов для тех из них, которые после конструирования объекта будут открываться функцией open(). Поскольку переменная documentNumber объявлена как статическая, то она существует в единственном экземпляре, для всех объектов класса Editor. bool Editor::open() { QString fileName = QFileDialog::getOpenFileName(".", fileFilters, this); if (fileName.isEmpty()) return false; return openFile(fileName); } Функция open() пытается открыть существующий файл, с помощью вызова openFile(). bool Editor::save() { if (isUntitled) { return saveAs(); } else { saveFile(curFile); return true; } } Функция save() использует переменную isUntitled, чтобы определить -- какую функцию вызывать: saveFile() или saveAs(). void Editor::closeEvent(QCloseEvent *event) { if (maybeSave()) event->accept(); else event->ignore(); } За счет перекрытия родительского метода closeEvent() мы даем пользователю возможность сохранить имеющиеся изменения. Логика сохранения реализована в функции maybeSave(), которая выводит запрос перед пользователем: "Желаете ли вы сохранить имеющиеся изменения?". Если она возвращает true, то событие "close" принимается, в противном случае оно игнорируется и окно останется открытым. void Editor::setCurrentFile(const QString &fileName) { curFile = fileName; setCaption(strippedName(curFile)); isUntitled = false; setModified(false); } Функция setCurrentFile() вызывается из openFile() и saveFile(), чтобы изменить содержимое переменных curFile и isUntitled, обновить заголовок окна и сбросить признак "modified". Класс Editor наследует методы setModified() и isModified() от своего предка -- QTextEdit, поэтому у нас нет необходимости "тащить" свой признак модификации документа. Когда пользователь вносит какие либо изменения в документ, QTextEdit выдает сигнал modificationChanged() и устанавливает признак модификации. QSize Editor::sizeHint() const { return QSize(72 * fontMetrics().width( x ), 25 * fontMetrics().lineSpacing()); } Функция sizeHint() возвращает "идеальные" размеры виджета, основываясь на размере символа 'x'. Класс QWorkspace использует эти размеры, чтобы назначить начальные размеры для окна с документом.

И в заключение приведем исходный текст файла main.cpp:

#include <qapplication.h> #include "mainwindow.h" int main(int argc, char *argv[]) { QApplication app(argc, argv); MainWindow mainWin; app.setMainWidget(&mainWin); if (argc > 1) { for (int i = 1; i < argc; ++i) mainWin.openFile(argv[i]); } else { mainWin.newFile(); } mainWin.show(); return app.exec(); } Если пользователь задаст имена документов в командной строке, то приложение попытается загрузить их. В противном случае приложение создает пустой документ. Специфические ключи командной строки, такие как -style и -font, будут автоматически исключены из списка аргументов, конструктором QApplication. Так что, если мы дадим такую команду: editor -style=motif readme.txt То приложение на запуске откроет один единственный документ readme.txt.

Многодокументный интерфейс -- один из способов одновременной работы с несколькими документами. Другой способ состоит в том, чтобы использовать несколько окон верхнего уровня. Он был описан в разделе Работа с несколькими документами одновременно Главы 3.