|
||
![]() |
||
Каждый из виджетов, помещаемый на форму, должен быть размещен в нужном месте и с соответствующими размерами. Виджеты, размеры которых превышают размер формы, могут снабжаться полосами прокрутки, чтобы пользователь мог просмотреть все его содержимое. В этой главе мы рассмотрим различные способы размещения виджетов на форме и покажем, как реализовать отстыковываемые (dockable) окна и многодокументный интерфейс (MDI).
Qt предоставляет три основных способа управления размещением подчиненных виджетов на форме: абсолютное позиционирование, ручное управление размещением и менеджеры компоновки. Мы рассмотрим каждый из них, на примере диалога "Find File", показанный на рисунке 6.1.
Рисунок 6.1. Диалог "Find File".
FindFileDialog::FindFileDialog(QWidget *parent, const char *name) : QDialog(parent, name) { ... namedLabel->setGeometry(10, 10, 50, 20); namedLineEdit->setGeometry(70, 10, 200, 20); lookInLabel->setGeometry(10, 35, 50, 20); lookInLineEdit->setGeometry(70, 35, 200, 20); subfoldersCheckBox->setGeometry(10, 60, 260, 20); listView->setGeometry(10, 85, 260, 100); messageLabel->setGeometry(10, 190, 260, 20); findButton->setGeometry(275, 10, 80, 25); stopButton->setGeometry(275, 40, 80, 25); closeButton->setGeometry(275, 70, 80, 25); helpButton->setGeometry(275, 185, 80, 25); setFixedSize(365, 220); }Абсолютное позиционирование имеет массу недостатков. Самый главный недостаток -- невозможность изменить размеры окна. Другой недостаток: текст меток может не умещаться в заданные размеры, если пользователь выбрал большой размер шрифта или, если интерфейс приложения был переведен на другой язык. Кроме того, этот подход требует от нас выполнения кропотливой работы по вычислению положения и размеров виджетов.
При ручном управлении размещением виджетов, мы по прежнему должны задавать положение компонентов на форме, но их размеры устанавливаются пропорционально размерам окна. Добиться этого можно за счет перекрытия обработчика события resizeEvent() формы, в котором можно пересчитывать и задавать новые размеры подчиненных виджетов:
FindFileDialog::FindFileDialog(QWidget *parent, const char *name) : QDialog(parent, name) { ... setMinimumSize(215, 170); resize(365, 220); } void FindFileDialog::resizeEvent(QResizeEvent *) { int extraWidth = width() - minimumWidth(); int extraHeight = height() - minimumHeight(); namedLabel->setGeometry(10, 10, 50, 20); namedLineEdit->setGeometry(70, 10, 50 + extraWidth, 20); lookInLabel->setGeometry(10, 35, 50, 20); lookInLineEdit->setGeometry(70, 35, 50 + extraWidth, 20); subfoldersCheckBox->setGeometry(10, 60, 110 + extraWidth, 20); listView->setGeometry(10, 85, 110 + extraWidth, 50 + extraHeight); messageLabel->setGeometry(10, 140 + extraHeight, 110 + extraWidth, 20); findButton->setGeometry(125 + extraWidth, 10, 80, 25); stopButton->setGeometry(125 + extraWidth, 40, 80, 25); closeButton->setGeometry(125 + extraWidth, 70, 80, 25); helpButton->setGeometry(125 + extraWidth, 135 + extraHeight, 80, 25); }В конструкторе мы установили минимальные размеры формы 215 X 170 и начальный размер 365 X 220. В обработчике resizeEvent() устанавливаются новые размеры виджетов при изменении размеров окна.
Как и в случае с абсолютным позиционированием, ручное управление размещением требует от программиста предварительного вычисления некоторых констант, которые потом жестко зашиваются в код программы. Написание таких программ очень утомительное занятие, особенно если потом потребуется внести изменения в дизайн формы. По прежнему сохраняется риск того, что какие-то надписи на форме не поместятся в отведенное им пространство. Избежать этого можно, если учитывать "идеальные" размеры виджетов, но это еще больше усложнит код.
Рисунок 6.2. Диалог "Find File" с изменяемыми размерами.
В Qt имеется три вида менеджеров компоновки: QHBoxLayout, QVBoxLayout и QGridLayout. Это классы-потомки от QLayout, который реализует основные методы управления размещением. Все три класса полностью поддерживаются Qt Designer-ом, а так же могут использоваться при написании кода вручную. Оба варианта использования были рассмотрены в Главе 2.
Ниже приводится конструктор FindFileDialog, в котором используются менеджеры размещения:
FindFileDialog::FindFileDialog(QWidget *parent, const char *name) : QDialog(parent, name) { ... QGridLayout *leftLayout = new QGridLayout; leftLayout->addWidget(namedLabel, 0, 0); leftLayout->addWidget(namedLineEdit, 0, 1); leftLayout->addWidget(lookInLabel, 1, 0); leftLayout->addWidget(lookInLineEdit, 1, 1); leftLayout->addMultiCellWidget(subfoldersCheckBox, 2, 2, 0, 1); leftLayout->addMultiCellWidget(listView, 3, 3, 0, 1); leftLayout->addMultiCellWidget(messageLabel, 4, 4, 0, 1); QVBoxLayout *rightLayout = new QVBoxLayout; rightLayout->addWidget(findButton); rightLayout->addWidget(stopButton); rightLayout->addWidget(closeButton); rightLayout->addStretch(1); rightLayout->addWidget(helpButton); QHBoxLayout *mainLayout = new QHBoxLayout(this); mainLayout->setMargin(11); mainLayout->setSpacing(6); mainLayout->addLayout(leftLayout); mainLayout->addLayout(rightLayout); }Размещением компонентов на форме управляют один QHBoxLayout, один QGridLayout и один QVBoxLayout. QGridLayout и QVBoxLayout расположены рядом друг с дружкой, внутри QHBoxLayout. Рамка вокруг формы имеет ширину 11 пикселей, промежутки между подчиненными виджетами -- 6 пикселей.
Рисунок 6.3. Раскладка диалога "Find File".
leftLayout->addMultiCellWidget(widget, row1, row2, col1, col2);где widget -- это подчиненный виджет, передаваемый этому менеджеру компоновки, row1, col1 -- верхняя левая ячейка, которую занимает виджет и row2, col2 -- правая нижняя ячейка.
Тот же самый диалог может быть создан с помощью визуального построителя Qt Designer. Пример работы с визуальным построителем, мы рассматривали в Главе 2.
Использование менеджеров размещения дает определенные преимущества, которые мы уже обсуждали ранее. Если в область компоновки добавляется виджет или удаляется из нее, менеджер автоматически адаптируется под изменившиеся условия. То же самое применимо и к случаю, когда вызываются методы подчиненного компонента -- hide() и show(). Если подчиненный виджет изменит "идеальный" размер, то раскладка изменится, с учетом изменившихся обстоятельств. Кроме того, менеджеры размещения автоматически установят минимальный размер формы в целом, основываясь на минимальных и "идеальных" размерах дочерних виджетов.
Во всех примерах, которые мы до сих пор рассматривали, мы просто объединяли виджеты менеджерами размещения и добавляли дополнительные распорки, для утилизации свободного пространства. Но иногда, чтобы расположение компонентов полностью соответствовало нашим желаниям, этого бывает недостаточно. В таких ситуациях необходимо дополнительно настраивать политики изменения размеров и "идеальные" размеры виджетов.
Политика изменения размеров сообщает менеджеру компоновки, как виджет должен растягиваться или сжиматься. Qt по-умолчанию дает неплохие значения политики изменения размеров для всех стандартных виджетов, но никакое значение по-умолчанию не может идеально подходить под все случаи жизни. Поэтому, до сих пор обычной практикой считается дополнительная настройка политик изменения размеров для одного-двух виджетов на форме. Политика изменения размеров назначается для каждого из двух направлений (по вертикали и по горизонтали). Наиболее часто используются значения Fixed, Minimum, Maximum, Preferred и Expanding:
Fixed -- виджет имеет фиксированные размеры, т.е. он не может ни растягиваться, ни сжиматься. Он всегда должен иметь "идеальный" ( sizeHint() ) размер.
Minimum -- "идеальный" размер виджета, это минимально возможный его размер. Виджет не может сжиматься меньше этого размера, но может растягиваться и занимать все доступное пространство, если это потребуется.
Maximum -- "идеальный" размер виджета, это максимально возможный его размер, т.е. виджет может сжиматься до минимально возможного размера, но не может растягиваться больше "идеального".
Preferred -- "идеальный" размер виджета, это предпочтительный его размер, но в случае необходимости виджет может как растягиваться, так и сжиматься.
Expanding -- виджет может и растягиваться, и сжиматься, но он предпочитает растягиваться.
Рисунок 6.4. Различные политики изменения размеров.
Существует еще две политики изменения размеров: MinimumExpanding и Ignored. Первая из них использовалась в ранних версиях Qt, хотя и довольно редко, в настоящее время не играет большой роли, поскольку лучший результат дает назначение политики Expanding и повторная реализация (перекрытие) метода minimumSizeHint(). Вторая -- во многом похожа на Expanding, но при этом игнорирует "идеальные" размеры виджета.
В дополнение к политикам изменения размера, горизонтальная и вертикальная составляющие визуального компонента, QSizePolicy хранят факторы растяжения. Они используются для задания степени растяжимости. Например, предположим, что на форме находятся QListView, а под ним -- QTextEdit. Нам необходимо, чтобы при растягивании формы QTextEdit рос в два раза быстрее, чем QListView. Для этого, фактор растягивания по вертикали (verticalStretch) компонента QTextEdit устанавливаем равным 2, а QListView -- 1.
Еще один способ воздействовать на порядок расположения -- изменять минимальный и максимальный размеры подчиненных виджетов. Менеджер компоновки будет учитывать значения этих параметров.
Разделитель (splitter) -- это виджет, который используется для размещения других виджетов и их разделения вертикальной или горизонтальной полосой. Пользователь может изменять размеры виджетов, перемещая разделитель. Они зачастую используются вместо менеджеров размещения, чтобы дать пользователю возможность самому управлять размерами виджетов.
Разделители в Qt реализованы в виде класса QSplitter. Подчиненные виджеты автоматически размещаются друг за дружкой, в порядке их создания, в смежных областях, разделителя. Ниже приводится код, который создает окно, изображенное на рисунке 6.5.
#include <qapplication.h> #include <qsplitter.h> #include <qtextedit.h> int main(int argc, char *argv[]) { QApplication app(argc, argv); QSplitter splitter(Qt::Horizontal); splitter.setCaption(QObject::tr("Splitter")); app.setMainWidget(&splitter); QTextEdit *firstEditor = new QTextEdit(&splitter); QTextEdit *secondEditor = new QTextEdit(&splitter); QTextEdit *thirdEditor = new QTextEdit(&splitter); splitter.show(); return app.exec(); }На форме находятся три компонента QTextEdit, выровненных по горизонтали виджетом QSplitter. В отличие от менеджера размещения, который отвечает только за размещение подчиненных виджетов, QSplitter является потомком класса QWidget и может использоваться как любой другой виджет.
Рисунок 6.5. Разделитель в приложении.
QSplitter может размещать подчиненные виджеты как по горизонтали, так и по вертикали. За счет вкладывания одного разделителя в другой, могут быть достигнуты весьма замысловатые комбинации. Например, приложение - почтовый клиент, главное окно которого изображено на рисунке 6.6, содержит горизонтальный разделитель и, вложенный в него, вертикальный разделитель.
Ниже приводится код конструктора подкласса QMainWindow:
MailClient::MailClient(QWidget *parent, const char *name) : QMainWindow(parent, name) { horizontalSplitter = new QSplitter(Horizontal, this); setCentralWidget(horizontalSplitter); foldersListView = new QListView(horizontalSplitter); foldersListView->addColumn(tr("Folders")); foldersListView->setResizeMode(QListView::AllColumns); verticalSplitter = new QSplitter(Vertical, horizontalSplitter); messagesListView = new QListView(verticalSplitter); messagesListView->addColumn(tr("Subject")); messagesListView->addColumn(tr("Sender")); messagesListView->addColumn(tr("Date")); messagesListView->setAllColumnsShowFocus(true); messagesListView->setShowSortIndicator(true); messagesListView->setResizeMode(QListView::AllColumns); textEdit = new QTextEdit(verticalSplitter); textEdit->setReadOnly(true); horizontalSplitter->setResizeMode(foldersListView, QSplitter::KeepSize); verticalSplitter->setResizeMode(messagesListView, QSplitter::KeepSize); ... readSettings(); }Здесь сначала создается горизонтальный разделитель, после чего он назначается центральным виджетом. Затем создаются подчиненные виджеты.
Рисунок 6.6. Почтовый клиент в Mac OS X.
На запуске приложения, QSplitter устанавливает размеры подчиненных виджетов, основываясь на их начальных размерах. Передвинуть разделитель можно не только вручную, но и программно, вызвав QSplitter::setSizes(). Кроме того, QSplitter предоставляет возможность сохранить свое положение, при завершении работы приложения, и восстановить его на следующем запуске. Ниже приводится функция, которая сохраняет настройки приложения - почтового клиента:
void MailClient::writeSettings() { QSettings settings; settings.setPath("software-inc.com", "MailClient"); settings.beginGroup("/MailClient"); QString str; QTextOStream out1(&str); out1 << *horizontalSplitter; settings.writeEntry("/horizontalSplitter", str); QTextOStream out2(&str); out2 << *verticalSplitter; settings.writeEntry("/verticalSplitter", str); settings.endGroup(); }И, соответствующая ей, функция readSettings().
void MailClient::readSettings() { QSettings settings; settings.setPath("software-inc.com", "MailClient"); settings.beginGroup("/MailClient"); QString str1 = settings.readEntry("/horizontalSplitter"); QTextIStream in1(&str1); in1 >> *horizontalSplitter; QString str2 = settings.readEntry("/verticalSplitter"); QTextIStream in2(&str2); in2 >> *verticalSplitter; settings.endGroup(); }Вся файловые операции, в этих функциях, выполняются через классы QTextIStream и QTextOStream -- потомки класса QTextStream.
По-умолчанию, во время перетаскивания, разделитель отображается в виде рамки. А размеры виджетов, с обеих сторон разделителя, изменяют размер только тогда, когда пользователь отпустит кнопку мыши. Чтобы изменения размеров происходили в реальном времени, необходимо вызвать setOpaqueResize(true).
Разделители QSplitter полностью поддерживаются визуальным построителем Qt Designer. Чтобы поместить виджеты в разделитель -- разместите подчиненные виджеты на форме примерно так, как вы желаете, затем выделите их и выберите пункт меню Layout|Lay Out Horizontally (in Splitter) или Layout|Lay Out Vertically (in Splitter).
Еще один виджет, которые может оказаться полезным, в смысле компоновки -- это QWidgetStack. Он может содержать наборы виджетов, объединяемых в "страницы", и всегда показывает только одну страницу, скрывая остальные. Нумерация страниц начинается с 0. Чтобы сделать определенный подчиненный виджет-страницу видимым, необходимо вызвать функцию raiseWidget(), передав ей либо номер страницы, либо указатель на подчиненный виджет.
Рисунок 6.7. QWidgetStack.
Рисунок 6.8. Диалог Configure.
Создается новая форма из шаблона "Dialog" или "Widget".
На форму добавляются QListBox и QWidgetStack.
Каждая страница QWidgetStack заполняется необходимыми виджетами. (Чтобы создать новую страницу -- щелкните правой кнопкой мыши и выберите из контекстного меню пункт Add Page. Чтобы перейти к другой странице -- щелкните мышкой по одной из кнопок, расположенных в правом верхнем углу.)
Объедините QListBox и QWidgetStack менеджером горизонтального размещения.
Соедините сигнал highlighted(int), от QListBox, со слотом raiseWidget(int), компонента QWidgetStack.
Установите свойство currentItem (QListBox) равным 0.
Класс QScrollView представляет собой область просмотра с двумя полосами прокрутки и "угловым" компонентом, находящимся в правом нижнем углу (обычно -- пустой QWidget). Если необходимо добавить полосы прокрутки к своему виджету, то намного проще воспользоваться готовым QScrollView, чем добавлять компоненты QScrollBar к своему виджету и писать код, реализующий их функциональность.
Рисунок 6.9. Виджеты, составляющие QScrollView.
#include <qapplication.h> #include <qscrollview.h> #include "iconeditor.h" int main(int argc, char *argv[]) { QApplication app(argc, argv); QScrollView scrollView; scrollView.setCaption(QObject::tr("Icon Editor")); app.setMainWidget(&scrollView); IconEditor *iconEditor = new IconEditor; scrollView.addChild(iconEditor); scrollView.show(); return app.exec(); }По-умолчанию, полосы прокрутки отображаются только в том случае, когда подчиненный виджет не умещается в область просмотра (viewport). Однако, следующий код вынудит QScrollView всегда показывать их:
scrollView.setHScrollBarMode(QScrollView::AlwaysOn); scrollView.setVScrollBarMode(QScrollView::AlwaysOn);Когда изменяется "идеальный" размер подчиненного виджета, QScrollView автоматически адаптируется под новые условия.
Рисунок 6.10. Изменение размеров QScrollView.
Чтобы продемонстрировать это на примере, попробуем написать новую версию класса IconEditor, породив его от QScrollView. Назовем новый класс ImageEditor, поскольку полосы прокрутки дают нам возможность работать с изображениями большого размера.
#ifndef IMAGEEDITOR_H #define IMAGEEDITOR_H #include <qimage.h> #include <qscrollview.h> class ImageEditor : public QScrollView { Q_OBJECT Q_PROPERTY(QColor penColor READ penColor WRITE setPenColor) Q_PROPERTY(QImage image READ image WRITE setImage) Q_PROPERTY(int zoomFactor READ zoomFactor WRITE setZoomFactor) public: ImageEditor(QWidget *parent = 0, const char *name = 0); void setPenColor(const QColor &newColor); QColor penColor() const { return curColor; } void setZoomFactor(int newZoom); int zoomFactor() const { return zoom; } void setImage(const QImage &newImage); const QImage &image() const { return curImage; } protected: void contentsMousePressEvent(QMouseEvent *event); void contentsMouseMoveEvent(QMouseEvent *event); void drawContents(QPainter *painter, int x, int y, int width, int height); private: void drawImagePixel(QPainter *painter, int i, int j); void setImagePixel(const QPoint &pos, bool opaque); void resizeContents(); QColor curColor; QImage curImage; int zoom; }; #endifЗаголовочный файл очень похож на предыдущий. Основное отличие состоит в том, что теперь предком является не QWidget, а QScrollView. Другие, менее значимые отличия, мы рассмотрим в процессе описания реализации класса.
ImageEditor::ImageEditor(QWidget *parent, const char *name) : QScrollView(parent, name, WStaticContents | WNoAutoErase) { curColor = black; zoom = 8; curImage.create(16, 16, 32); curImage.fill(qRgba(0, 0, 0, 0)); curImage.setAlphaBuffer(true); resizeContents(); }Родительскому конструктору передаются флаги WStaticContents и WNoAutoErase. Они необходимы для области просмотра. Мы не назначаем политики изменения размеров, поскольку значения по-умолчанию (Expanding, Expanding) нас вполне устраивают. В конструкторе ранней версии мы не вызывали updateGeometry(), поскольку начальные размеры виджета могли зависеть от действий менеджеров размещения. Однако в данном случае, нам необходимо задать начальные размеры компонента, что мы и делаем вызовом resizeContents().
void ImageEditor::resizeContents() { QSize size = zoom * curImage.size(); if (zoom >= 3) size += QSize(1, 1); QScrollView::resizeContents(size.width(), size.height()); }Приватная функция resizeContents() вызывает унаследованный метод QScrollView::resizeContents(), передавая ему начальные размеры содержимого QScrollView, который в свою очередь отображает полосы прокрутки, в зависимости от размеров содержимого и области просмотра.
Нам нет необходимости перекрывать функцию sizeHint(). Компонент QScrollView автоматически вычисляет "идеальный" размер, отталкиваясь от размера содержимого области просмотра.
void ImageEditor::setImage(const QImage &newImage) { if (newImage != curImage) { curImage = newImage.convertDepth(32); curImage.detach(); resizeContents(); updateContents(); } }В большинстве случаев, в оригинальном IconEditor, когда необходимо было послать компоненту событие "paint", мы вызывали методы update() и updateGeometry() -- чтобы объявить об изменении "идеальных" размеров. В новой версии, эти вызовы заменены на updateContents() и resizeContents(), соответственно.
void ImageEditor::drawContents(QPainter *painter, int, int, int, int) { if (zoom >= 3) { painter->setPen(colorGroup().foreground()); for (int i = 0; i <= curImage.width(); ++i) painter->drawLine(zoom * i, 0, zoom * i, zoom * curImage.height()); for (int j = 0; j <= curImage.height(); ++j) painter->drawLine(0, zoom * j, zoom * curImage.width(), zoom * j); } for (int i = 0; i < curImage.width(); ++i) { for (int j = 0; j < curImage.height(); ++j) drawImagePixel(painter, i, j); } }QScrollViewвызывает функцию drawContents(), чтобы перерисовать содержимое области просмотра. Объект QPainter уже инициализирован, в соответствии с позициями движков в полосах прокрутки, поэтому мы просто "рисуем", точно так же как в обработчике события paintEvent().
Второй, третий, четвертый и пятый аргументы определяют координаты прямоугольника, который должен быть перерисован. Мы могли бы использовать их, чтобы перерисовывать только видимую часть изображения, но для упрощения примера мы перерисовыаем все изображение.
Функция drawImagePixel(), обращение к которой стоит в конце drawContents(), осталась без изменений (см. оригинальную версию), поэтому здесь мы ее рассматривать не будем.
void ImageEditor::contentsMousePressEvent(QMouseEvent *event) { if (event->button() == LeftButton) setImagePixel(event->pos(), true); else if (event->button() == RightButton) setImagePixel(event->pos(), false); } void ImageEditor::contentsMouseMoveEvent(QMouseEvent *event) { if (event->state() & LeftButton) setImagePixel(event->pos(), true); else if (event->state() & RightButton) setImagePixel(event->pos(), false); }События от мыши, направляемые содержимому QScrollView, обрабатываются специальными функциями обработчиками, имена которых начинаются со слова contents. Прежде, чем события будут переданы обработчикам, QScrollView выполнит преобразование координат из системы координат области просмотра в систему координат содержимого, поэтому у нас не возникает необходимости в написании дополнительного кода, выполняющего эти действия.
void ImageEditor::setImagePixel(const QPoint &pos, bool opaque) { int i = pos.x() / zoom; int j = pos.y() / zoom; if (curImage.rect().contains(i, j)) { if (opaque) curImage.setPixel(i, j, penColor().rgb()); else curImage.setPixel(i, j, qRgba(0, 0, 0, 0)); QPainter painter(viewport()); painter.translate(-contentsX(), -contentsY()); drawImagePixel(&painter, i, j); } }Функция setImagePixel() вызывается из contentsMousePressEvent() и contentsMouseMoveEvent(), для закрашивания и очистки пикселей. Код функций, по большей части, остался без изменений, за исключением способа инициализации объекта QPainter. В данном случае, мы передаем ему viewport(), в качестве владельца, поскольку рисование будет производиться на поверхности области просмотра, а затем выполняем преобразование системы координат, чтобы учесть положение движков на полосах прокрутки.
Последние три строки, которые работают с QPainter, можно было бы заменить одной строкой:
updateContents(i * zoom, j * zoom, zoom, zoom);Которая сообщила бы QScrollView о необходимости перерисовать один квадратик, который соответствует текущему пикселю. Но поскольку у нас функция drawContents() не оптимизирована, то приходится создавать QPainter и рисовать изображение пикселя самостоятельно.
Если теперь мы попробуем поработать с ImageEditor, то мы практически не заметим разницы с оригинальным IconEditor, вставленным в QScrollView. Однако, другие виджеты, порожденные от QScrollView, используют дополнительные преимущества родительского класса. Например, QTextEdit выполняет перенос текста по словам.
Обратите внимание: вам наверняка придется использовать класс QScrollView, в качестве предка, если размеры отображаемого содержимого очень велики, поскольку некоторые оконные подсистемы не в состоянии отобразить виджеты, размеры которых превышают величину 32767 пикселей.
Еще один важный момент, которого мы не коснулись здесь: мы можем вставлять подчиненные виджеты в область просмотра, вызовом функции addWidget(), и перемещать вызовом moveWidget(). Всякий раз, когда пользователь перемещается по области просмотра, с помощью полос прокрутки, QScrollView автоматически перемещает подчиненные виджеты на экране. (Если подчиненных виджетов слишком много, то прокрутка может существенно замедляться. Чтобы оптимизировать этот процесс, можно вызвать enableClipper(true).) В качестве примера, использующего подобный подход, можно привести web-браузер, в котором большая часть содержимого может отрисовываться непосредственно в области просмотра, но кнопки и поля ввода на формах должны быть представлены в виде виджетов.
Стыкуемые окна -- это окна, которые могут отстыковываться и пристыковываться к специальным областям стыковки. Самый яркий пример, пожалуй, это панели инструментов.
Объекты класса QMainWindow предоставляют в распоряжение программиста четыре области стыковки: вверху, внизу, слева и справа от центрального виджета. Когда создаются экземпляры класса QToolBar, они автоматически пристыковываются к верхней области окна-владельца.
Рисунок 6.11. "Плавающие" пристыковываемые окна.
Рисунок 6.12. QMainWindow с пятью стыкуемыми окнами.
dockWindow->setCloseMode(QDockWindow::Undocked);Область стыковки -- QDockArea, имеет свое контекстное меню, со списком всех пристыкованных окон и панелей инструментов. После того, как отстыкованное окно было закрыто пользователем, оно может быть восстановлено с помощью этого меню.
Рисунок 6.13. Контекстное меню QDockArea.
QToolBar *toolBar = new QToolBar(tr("Font"), this); QComboBox *fontComboBox = new QComboBox(true, toolBar); QSpinBox *fontSize = new QSpinBox(toolBar); boldAct->addTo(toolBar); italicAct->addTo(toolBar); underlineAct->addTo(toolBar); moveDockWindow(toolBar, DockBottom);Эта панель будет выглядеть просто отвратительно, если пользователь переместит ее в левую или правую область стыковки, из-за QComboBox и QSpinBox. Чтобы предотвратить такую возможность, мы можем запретить стыковку к левой и правой областям, вызовом QMainWindow:: setDockEnabled():
setDockEnabled(toolBar, DockLeft, false); setDockEnabled(toolBar, DockRight, false);Если необходимо создать нечто более похожее на плавающее окно или палитру инструментов, то можно напрямую обращаться к QDockWindow, вызывая метод setWidget(), чтобы добавить виджет в окно. Если необходимо предоставить пользователю возможность изменять размеры пристыкованного окна, то для этого можно воспользоваться функцией setResizeEnabled().
Если виджет должен изменять свой вид, в зависимости от того, к какой из областей стыковки он присоединен, то для этого необходимо перекрыть метод QDockWindow:: setOrientation() и выполять все необходимые действия в нем.
Если необходимо сохранять положение всех панелей инструментов и других стыкуемых окон, чтобы потом, на следующем запуске приложения восстанавливать его, можно написать код, который очень похож на тот, который мы разбирали ранее, используя оператор "<<" класса QMainWindow, для записи в файл, и ">>" -- для восстановления из файла.
Приложения, подобные Microsoft Visual Studio и Qt Designer очень широко используют стыкуемые окна, чтобы сделать интерфейс с пользователем более гибким.
Приложения, которые могут работать с несколькими документами, открываемыми в отдельных окнах и расположенных внутри главного окна, называют 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.
|