=> Главная База Знаний Qt Многодокументный интерфейс


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

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

Приложения, которые обеспечивают работу со многими документами в центральной области главного окна, называются приложениями с многодокументным интерфейсом или MDI—приложениями. В Qt MDI—приложения создаются с использованием в качестве центрального виджета класса QWorkspace и путем представления каждого документа в виде дочернего окна QWorkspace.

Обычно MDI—приложения содержат пункт главного меню Windows (окна) с командами по управлению окнами и их списком. Активное окно отмечается галочкой. Пользователь может сделать любое окно активным, щелкая по его названию в меню Windows.

В данном разделе для демонстрации способов создания приложения с интерфейсом MDI и способов реализации его меню Windows мы разработаем MDI—приложение Editor (редактор), показанное на рис. 6.16.

Рис. 6.16. MDI—приложение Editor.

Это приложение состоит из двух классов: MainWindow и Editor. Его программный код находится на компакт-диске, и поскольку большая часть его либо совпадает, либо очень похожа на программный код приложения Электронная таблица из части I, здесь мы представим только новый программный код.

Рис. 6.17. Меню MDI—приложения Editor.

Давайте начнем с класса MainWindow.

01 MainWindow::MainWindow()

02 {

03 workspace = new QWorkspace;

04 setCentralWidget(workspace);

05 connect(workspace, SIGNAL(windowActivated(QWidget *)),

06 this, SLOT(updateMenus()));

07 createActions();

08 createMenus();

09 createToolBars();

10 createStatusBar();

11 setWindowTitle(tr("MDI Editor"));

12 setWindowIcon(QPixmap(":/images/icon.png"));

13 }

В конструкторе MainWindow мы создаем виджет QWorkspace и делаем его центральным виджетом. Мы связываем сигнал windowActivated() класса QWorkspace со слотом, который мы будем использовать для обеспечения актуального состояния меню Window.

01 void MainWindow::newFile()

02 {

03 Editor *editor = createEditor();

04 editor->newFile();

05 editor->show();

06 }

Слот newFile() соответствует пункту меню File | New. Он зависит от закрытой функции createEditor(), создающей дочерний виджет Editor.

01 Editor *MainWindow::createEditor()

02 {

03 Editor *editor = new Editor;

04 connect(editor, SIGNAL(copyAvailable(bool)),

05 cutAction, SLOT(setEnabled(bool)));

06 connect(editor, SIGNAL(copyAvailable(bool)),

07 copyAction, SLOT(setEnabled(bool)));

08 workspace->addWindow(editor);

09 windowMenu->addAction(editor->windowMenuAction());

10 windowActionGroup->addAction(editor->windowMenuAction());

11 return editor;

12 }

Функция createEditor() создает виджет Editor и устанавливает два соединения «сигнал—слот». Эти соединения обеспечивают включение или выключение пунктов меню Edit | Cut и Edit | Copy в зависимости от наличия выделенной области текста.

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

После настройки Editor мы добавляем QAction для представления окна в меню Window. Это действие обеспечивается классом Editor, который мы скоро рассмотрим. Мы также добавляем это действие в объект QActionGroup. QActionGroup гарантирует, что в любой момент времени оказывается отмеченной только одна строка меню Window.

01 void MainWindow::open()

02 {

03 Editor *editor = createEditor();

04 if (editor->open()) {

05 editor->show();

06 } else {

07 editor->close();

08 }

09 }

Функция open() соответствует пункту меню File | Open. Этот пункт меню создает Editor для нового документа и вызывает функцию open() для Editor. Имеет смысл выполнять файловые операции в классе Editor, а не в классе MainWindow, поскольку каждый Editor требует поддержки своего собственного состояния.

Если функция open() завершится неудачей, мы просто закроем редактор, поскольку пользователь уже будет уведомлен об ошибке. Мы не обязаны сами явно удалять объект Editor; это происходит автоматически при условии установки атрибута виджета Qt::WA_DeleteOnClose, что и делается в конструкторе Editor.

01 void MainWindow::save()

02 {

03 if (activeEditor()) {

04 activeEditor()->save();

05 }

06 }

Слот save() вызывает функцию Editor::save() для активного редактора, если таковой имеется. И снова программный код по выполнению реальной работы находится в классе Editor.

01 Editor *MainWindow::activeEditor()

02 {

03 return qobject_cast<Editor *>(workspace->activeWindow());

04 }

Закрытая функция activeEditor() возвращает активное дочернее окно в виде указателя типа Editor или нулевой указатель при отсутствии такого окна.

01 void MainWindow::cut()

02 {

03 if (activeEditor())

04 activeEditor()->cut();

05 }

Слот cut() вызывает функцию Editor::cut() для активного редактора. Мы не приводим слоты copy(), paste() и del(), потому что они имеют такой же вид.

01 void MainWindow::updateMenus()

02 {

03 bool hasEditor = (activeEditor() != 0);

04 bool hasSelection = activeEditor()

05 && activeEditor()->textCursor().hasSelection();

06 saveAction->setEnabled(hasEditor);

07 saveAsAction->setEnabled(hasEditor);

08 pasteAction->setEnabled(hasEditor);

09 cutAction->setEnabled(hasSelection);

10 copyAction->setEnabled(hasSelection);

11 closeAction->setEnabled(hasEditor);

12 closeAllAction->setEnabled(hasEditor);

13 tileAction->setEnabled(hasEditor);

14 cascadeAction->setEnabled(hasEditor);

15 nextAction->setEnabled(hasEditor);

16 previousAction->setEnabled(hasEditor);

17 separatorAction->setVisible (hasEditor);

18 if (activeEditor())

19 activeEditor()->windowMenuAction()->setChecked(true);

20 }

Слот updateMenus() вызывается всякий раз, когда окно становится активным (и когда закрывается последнее окно) для обновления системы меню благодаря помещенному нами в конструктор MainWindow соединению «сигнал—слот».

Большинство пунктов меню имеет смысл при существовании активного окна, поэтому мы их отключаем при отсутствии активного окна. В конце мы вызываем setChecked() для QAction, представляющего активное окно. Благодаря использованию QActionGroup нам не требуется явно сбрасывать флажок предьщущего активного окна.

01 void MainWindow::createMenus()

02 {

03 windowMenu = menuBar()->addMenu(tr("&Window"));

04 windowMenu->addAction(closeAction);

05 windowMenu->addAction(closeAllAction);

06 windowMenu->addSeparator();

07 windowMenu->addAction(tileAction);

08 windowMenu->addAction(cascadeAction);

09 windowMenu->addSeparator();

10 windowMenu->addAction(nextAction);

11 windowMenu->addAction(previousAction);

12 windowMenu->addAction(separatorAction);

13 }

Закрытая функция createMenus() заполняет меню Window командами. Здесь используются типичные для такого рода меню команды, и они легко реализуются с применением слотов closeActiveWindow(), closeAllWindows(), tile() и cascade() класса QWorkspace. Всякий раз, когда пользователь открывает новое окно, в меню Window добавляется список действий. (Это делается в функции createEditor(), которую мы видели.) При закрытии пользователем окна редактора соответствующий ему пункт в меню Window удаляется (поскольку его владельцем является это окно редактора), т.е. пункт меню удаляется из меню Window автоматически.

01 void MainWindow::closeEvent(QCloseEvent *event)

02 {

03 workspace->closeAllWindows();

04 if (activeEditor()) {

05 event->ignore();

06 } else {

07 event->accept();

08 }

09 }

Функция closeEvent() переопределяется для закрытия всех дочерних окон, обеспечивая получение всеми дочерними виджетами сигнала о возникновении события закрытия. Если один из дочерних виджетов «игнорирует» свое событие закрытия (прежде всего из-за того, что пользователь нажал кнопку отмены при выдаче соответствующего сообщения о «несохраненных изменениях»), мы игнорируем событие закрытия для MainWindow; в противном случае мы принимаем его, и в результате Qt закрывает окно. Если бы мы не переопределили функцию closeEvent() в MainWindow, у пользователя не было бы никакой возможности сохранения ни одного из несохраненных изменений.

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

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

01 class Editor : public QTextEdit

02 {

03 Q_OBJECT

04 public:

05 Editor(QWidget *parent = 0);

06 bool openFile(const QString &fileName);

07 bool save();

08 bool saveAs();

09 void newFile();

10 bool open();


11 protected:

12 QSize sizeHint() const;

13 QAction *windowMenuAction() const { return action; }

14 void closeEvent(QCloseEvent *event);

15 private slots:

16 void documentWasModified();


17 private:

18 bool okToContinue();

19 bool saveFile(const QString &fileName);

20 void setCurrentFile(const QString &fileName);

21 bool readFile(const QString &fileName);

22 bool writeFile(const QString &fileName);

23 QString strippedName(const QString &fullFileName);

24 QString curFile;

25 bool isUntitled;

26 QString fileFilters;

27 QAction *action;

28 }

Присутствующие в классе MainWindow приложения Электронная таблица четыре закрытые функции имеются также в классе Editor: okToContinue(), saveFile(), setCurrentFile() и strippedName().

01 Editor::Editor(QWidget *parent)

02 : QTextEdit(parent)

03 {

04 action = new QAction(this);

05 action->setCheckable(true);

06 connect(action, SIGNAL(triggered()), this, SLOT(show()));

07 connect(action, SIGNAL(triggered()), this, SLOT(setFocus()));

08 isUntitled = true;

09 fileFilters = tr("Text files (*.txt)\nAll files (*)");

10 connect(document(), SIGNAL(contentsChanged()),

11 this, SLOT(documentWasModified()));

12 setWindowIcon(QPixmap(":/images/document.png"));

13 setAttribute(Qt::WA_DeleteOnClose);

14 }

Сначала мы создаем действие QAction, представляющее редактор в меню приложения Window, и связываем его со слотами show() и setFocus().

Поскольку мы разрешаем пользователям создавать любое количество окон редактора, мы должны предусмотреть соответствующую систему их наименования, чтобы они отличались до первого их сохранения. Один из распространенных методов решения этой проблемы заключается в назначении имен с числами (например, document1.txt). Мы используем переменную isUntitled, чтобы отличить предоставляемые пользователем имена документов и сгенерированные программно.

Мы связываем сигнал текстового документа contentsChanged() c закрытым слотом documentWasModified(). Этот слот просто вызывает setWindowModified(true).

Наконец, мы устанавливаем атрибут Qt::WA_DeleteOnClose для предотвращения утечек памяти при закрытии пользователем окна Editor.

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

01 void Editor::newFile()

02 {

03 static int documentNumber = 1;

04 curFile = tr("document%1.txt").arg(documentNumber);

05 setWindowTitle(curFile + "[*]");

06 action->setText(curFile);

07 isUntitled = true;

08 ++documentNumber;

09 }

Функция newFile() генерирует для нового документа имя типа document1.txt. Этот программный код помещен в функцию newFile(), a не в конструктор, поскольку мы не хотим использовать числа при вызове функции open() для открытия существующего документа во вновь созданном редакторе Editor. Поскольку переменная documentNumber объявлена как статическая, она совместно используется всеми экземплярами Editor.

Маркер «[*]» в заголовке окна указывает место, где мы хотим выдавать звездочку при несохраненных изменениях файла для платформ, отличных от Mac OS X. Мы рассматривали этот маркер в главе 3.

01 bool Editor::open()

02 {

03 QString fileName = QFileDialog::getOpenFileName(

04 this, tr("Open"), fileFilters);

05 if(fileName.isEmpty())

06 return false;

07 return openFile(fileName);

08 }

Функция open() пытается открыть сущеcтвующий файл при помощи функции openFile().

01 bool Editor::save()

02 {

03 if (isUntitled) {

04 return saveAs();

05 } else {

06 return saveFile(curFile);

07 }

Функция save() используёт переменную isUntitled для определения вида вызываемой функции saveFile() или saveAs().

01 void Editor::closeEvent(QCloseEvent *event)

02 {

03 if (okToContinue()) {

04 event->accept();

05 } else {

06 event->ignore();

07 }

08 }

Функция closeEvent() переопределяется, чтобы разрешить пользователю сохранить несохраненные изменения. Вся логика содержится в функции okToContinue(), которая выводит сообщение «Do you want to save your changes?» (Сохранить изменения?). Если функция okToContinue() возвращает true, мы обрабатывам событие закрытия; в противном случае мы «игнорируем» его и окно оставляем прежним.

01 void Editor::setCurrentFile(const QString &fileName)

02 {

03 curFile = fileName;

04 isUntitled = false;

05 action->setText(strippedName(curFile));

06 document()->setModified(false);

07 setWindowTitle(strippedName(curFile) + "[*]");

08 setWindowModified(false);

09 }

Функция setCurrentFile() вызывается из openFile() и saveFile() для обновления переменных curFile и isUntitled, установки текста заголовка окна и пункта меню, а также для установки значения флажка модификации документа на false. Всякий раз, когда пользователь изменяет текст в редакторе, объект базового класса QTextDocument генерирует сигнал contentsChanged() и устанавливает свой внутренний флажок модификации на значение true.

01 QSize Editor::sizeHint() const

02 {

03 return QSize(72 * fontMetrics().width('x'),

04 25 * fontMetrics().lineSpacing());

05 }

Функция sizeHint() возвращает размер, рассчитанный на основе ширины буквы «x» и высоты строки текста. QWorkspace использует идеальный размер в качестве начального размера окна.

Ниже приводится файл main.cpp MDI—приложения Editor:

01 #include <QApplication>

02 #include "mainwindow.h"

03 int main(int argc, char *argv[])

04 {

05 QApplication app(argc, argv);

06 QStringList args = app.arguments();

07 MainWindow mainWin;

08 if (args.count() > 1) {

09 for (int i = 1; i < args.count(); ++i)

10 mainWin.openFile(args[i]);

11 } else {

12 mainWin.newFile();

13 }

14 mainWin.show();

15 return app.exec();

16 }

Еслй пользователь задает в командной строке какие-нибудь файлы, мы пытаемся их загрузить, в противном случае мы начинаем работу с пустым документом. Такие характерные для Qt опции командной строки, как —style и —font (стиль и шрифт), автоматически убираются из списка аргументов конструктором QApplication. Поэтому, если мы напишем в командной строке

mdieditor -style motif readme.txt

QApplication::arguments() возвратит QStringList с двумя элементами («mdieditor» и «readme.txt»), а МDI—приложение Editor запустится с документом readme.txt.

Интерфейс MDI представляет собой один из способов работы одновременно со многими документами. В системе MacOS Х более предпочтителен подход, связанный с применением нескольких окон верхнего уровня. Этот подход рассматривается в разделе «Работа со многими документами» главы З.