Разработка графического интерфейса с помощью библиотеки Qt3 | ||
---|---|---|
Пред. | Глава 6. Управление размещением виджетов. | След. |
Приложения, которые могут работать с несколькими документами, открываемыми в отдельных окнах и расположенных внутри главного окна, называют MDI-приложениями (MDI -- от англ. Multiple Document Interface). В Qt подобный интерфейс создается с помощью класса QWorkspace, назначаемого центральным виджетом. Каждое окно с открытым документом становится подчиненным, по отношению к QWorkspace.
В этом разделе мы создадим приложение Editor (текстовый редактор), изображенное на рисунке 6.14, чтобы продемонстрировать принципы создания MDI-приложений и оконных меню.
Рисунок 6.14. Внешний вид приложения Editor.
Рисунок 6.15. Меню приложения Editor.
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.
Пред. | В начало | След. |
Стыкуемые окна. | На уровень выше | Обработка событий. |