The OpenNET Project / Index page

[ новости /+++ | форум | wiki | теги | ]

Каталог документации / Раздел "Программирование, языки" / Оглавление документа

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.




Спонсоры:
Inferno Solutions
Hosting by Hoster.ru
Хостинг:

Закладки на сайте
Проследить за страницей
Created 1996-2021 by Maxim Chirkov
Добавить, Поддержать, Вебмастеру