О ресурре... Главная
The Belarus Internet the Resource about Linux
 
   
Ж. Бланшетт, М. Саммерфильд, "Глава 6. Управление размещением виджетов" - 01/12/2004

Предыдущая глава | Содержание | Следующая глава

Часть II. Углубленные сведения

Содержание
6. Управление размещением виджетов.
6.1. Основы компоновки виджетов.
6.2. Разделители.
6.3. Многостраничные виджеты.
6.4. Области просмотра с прокруткой.
6.5. Стыкуемые окна.
6.6. Многодокументный интерфейс.
7. Обработка событий.
7.1. Обработчики событий.
7.2. Установка фильтров событий.
7.3. Сокращение времени отклика при длительной обработке данных.
8. Двух- и трехмерная графика.
8.1. Рисование средствами QPainter.
8.2. Рисование средствами QCanvas.
8.3. Вывод на печать.
8.4. Графика OpenGL.
9. Drag and Drop.
9.1. Реализация механизма 'drag and drop' в приложениях.
9.2. Поддержка нестандартных типов данных при перетаскивании.
9.3. Расширенные возможности буфера обмена.
10. Ввод/вывод.
10.1. Чтение и запись двоичных данных.
10.2. Чтение и запись текста.
10.3. Работа с файлами и каталогами.
10.4. Взаимодействия между процессами.
11. Контейнерные классы.
11.1. Векторы.
11.2. Списки.
11.3. Словари (map).
11.4. Контейнеры указателей.
11.5. Классы QString и QVariant.
12. Базы данных.
12.1. Установление соединения и выполнение запроса.
12.2. Представление данных в табличной форме.
12.3. Разработка форм, ориентированных на работу с базами данных.
13. Работа с сетью.
13.1. Класс QFtp.
13.2. Класс QHttp.
13.3. Класс QSocket.
13.4. Протокол UDP и класс QSocketDevice.
14. XML
14.1. Чтение XML-документов с помощью SAX.
14.2. Чтение XML-документов с помощью DOM.
14.3. Запись в XML-документы.
15. Интернационализация
15.1. Unicode.
15.2. Разработка приложений, подготовленных к переводу.
15.3. Динамическое переключение языков.
15.4. Перевод существующих приложений.
16. Разработка справочной системы приложения.
16.1. Всплывающие подсказки и справка "What's This?".
16.2. Использование QTextBrowser для отображения текста справки.
16.3. Использование Qt Assistant для отображения текста справки.
17. Многопоточность.
17.1. Потоки.
17.2. Взаимодействие с главным потоком приложения.
17.3. Работа с классами Qt вне главного потока.
18. Платформо-зависимые особенности.
18.1. Взаимодействие с API операционной системы.
18.2. ActiveX.
18.3. Управление сеансами.
19. Об авторах.
19.1. Jasmin Blanchette
19.2. Mark Summerfield

Глава 6. Управление размещением виджетов.

Каждый из виджетов, помещаемый на форму, должен быть размещен в нужном месте и с соответствующими размерами. Виджеты, размеры которых превышают размер формы, могут снабжаться полосами прокрутки, чтобы пользователь мог просмотреть все его содержимое. В этой главе мы рассмотрим различные способы размещения виджетов на форме и покажем, как реализовать отстыковываемые (dockable) окна и многодокументный интерфейс (MDI).


6.1. Основы компоновки виджетов.

Qt предоставляет три основных способа управления размещением подчиненных виджетов на форме: абсолютное позиционирование, ручное управление размещением и менеджеры компоновки. Мы рассмотрим каждый из них, на примере диалога "Find File", показанный на рисунке 6.1.

Рисунок 6.1. Диалог "Find File".


Абсолютное позиционирование -- это самый "неблагодарный" способ размещения виджетов. При таком подходе положение и размеры виджетов жестко зашиваются в программу, что, как правило, влечет за собой фиксированные размеры самой формы. Взглянем на конструктор диалога FindFileDialog, который строится по принципу абсолютного позиционирования:
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. Они обеспечивают разумные размеры по-умолчанию для каждого типа виджетов и учитывают "идеальные" размеры каждого из них, которые, в свою очередь, зависят от выбранного размера шрифта, стиля отображения и объема содержимого. Кроме того, менеджеры компоновки учитываю минимальные и максимальные размеры, и автоматически корректируют расположение виджетов, в ответ на изменение шрифта, содержимого или размеров окна.

В 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".


QGridLayout работает как плоская сетка ячеек. QLabel, в верхнем левом углу области, занимает ячейку (0, 0), а соответствующий ей QLineEdit -- (0, 1). QCheckBox объединяет две колонки и занимает ячейки (2, 0) и (2, 1). QListView и QLabel, расположенные снизу, так же занимают по две ячейки. Вызов addMultiCellWidget() имеет следующий синтаксис:
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 подытоживает все, что было сказано выше о политиках изменения размеров, на примере QLabel, отображающей текст "Some Text".

Рисунок 6.4. Различные политики изменения размеров.


Когда изменяется размер формы, которая включает в себя компоненты с политиками Preferred и Expanding, то дополнительное пространство отдается виджету с политикой Expanding, в то время, как виджет Preferred сохраняет "идеальные" размеры.

Существует еще две политики изменения размеров: MinimumExpanding и Ignored. Первая из них использовалась в ранних версиях Qt, хотя и довольно редко, в настоящее время не играет большой роли, поскольку лучший результат дает назначение политики Expanding и повторная реализация (перекрытие) метода minimumSizeHint(). Вторая -- во многом похожа на Expanding, но при этом игнорирует "идеальные" размеры виджета.

В дополнение к политикам изменения размера, горизонтальная и вертикальная составляющие визуального компонента, QSizePolicy хранят факторы растяжения. Они используются для задания степени растяжимости. Например, предположим, что на форме находятся QListView, а под ним -- QTextEdit. Нам необходимо, чтобы при растягивании формы QTextEdit рос в два раза быстрее, чем QListView. Для этого, фактор растягивания по вертикали (verticalStretch) компонента QTextEdit устанавливаем равным 2, а QListView -- 1.

Еще один способ воздействовать на порядок расположения -- изменять минимальный и максимальный размеры подчиненных виджетов. Менеджер компоновки будет учитывать значения этих параметров.


6.2. Разделители.

Разделитель (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 распределит пространство между подчиненными виджетами таким образом, что относительные их размеры останутся без изменения. Но в случае с почтовым клиентом нам необходимо, чтобы два QListView сохранили свои размеры, а все дополнительное пространство было отдано компоненту QTextEdit. Достигается это парой вызовов setResizeMode().

На запуске приложения, 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).


6.3. Многостраничные виджеты.

Еще один виджет, которые может оказаться полезным, в смысле компоновки -- это QWidgetStack. Он может содержать наборы виджетов, объединяемых в "страницы", и всегда показывает только одну страницу, скрывая остальные. Нумерация страниц начинается с 0. Чтобы сделать определенный подчиненный виджет-страницу видимым, необходимо вызвать функцию raiseWidget(), передав ей либо номер страницы, либо указатель на подчиненный виджет.

Рисунок 6.7. QWidgetStack.


Сам по себе QWidgetStack невидим и не предоставляет никаких дополнительных визуальных элементов, с помощью которых пользователь мог бы переходить от страницы к странице. Маленькие стрелочки и темно-серая рамка, которые вы можете наблюдать на рисунке 6.7, предоставляются визуальным построителем Qt Designer для удобства разработчика.

Рисунок 6.8. Диалог Configure.


Диалог Configure, изображенный на рисунке 6.8, может служить примером использования QWidgetStack. В левой части окна диалога находится QListBox, а в правой -- QWidgetStack. Каждому элементу в QListBox соответствует своя страница в QWidgetStack. Формы подобного рода очень просто создаются в Qt Designer:
  1. Создается новая форма из шаблона "Dialog" или "Widget".

  2. На форму добавляются QListBox и QWidgetStack.

  3. Каждая страница QWidgetStack заполняется необходимыми виджетами. (Чтобы создать новую страницу -- щелкните правой кнопкой мыши и выберите из контекстного меню пункт Add Page. Чтобы перейти к другой странице -- щелкните мышкой по одной из кнопок, расположенных в правом верхнем углу.)

  4. Объедините QListBox и QWidgetStack менеджером горизонтального размещения.

  5. Соедините сигнал highlighted(int), от QListBox, со слотом raiseWidget(int), компонента QWidgetStack.

  6. Установите свойство currentItem (QListBox) равным 0.

Поскольку мы реализовали механизм смены страниц стандартными сигналами и слотами, диалог будет способен выполнять корректный переход от страницы к странице, даже во время предварительного просмотра в Qt Designer.


6.4. Области просмотра с прокруткой.

Класс QScrollView представляет собой область просмотра с двумя полосами прокрутки и "угловым" компонентом, находящимся в правом нижнем углу (обычно -- пустой QWidget). Если необходимо добавить полосы прокрутки к своему виджету, то намного проще воспользоваться готовым QScrollView, чем добавлять компоненты QScrollBar к своему виджету и писать код, реализующий их функциональность.

Рисунок 6.9. Виджеты, составляющие QScrollView.


Самый простой способ добавить визуальный компонент в QScrollView -- это вызвать метод addChild(), указав необходимый подчиненный виджет в качестве аргумента. QScrollView автоматически переподчинит визуальный компонент, став его владельцем. Например, пусть необходимо окружить компонент IconEditor, который был разработан нами в Главе 5, полосами прокрутки. Для этого можно было бы написать следующий код:
#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.


Еще один способ добавить полосы прокрутки к своему виджету -- использовать QScrollView в качестве класса-предка и перекрыть метод drawContents(). Такой подход реализован в классах QIconView, QListBox, QListView, QTable и QTextEdit. Если вашему виджету необходимы полосы прокрутки, то лучшим решением будет породить класс виджета от 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-браузер, в котором большая часть содержимого может отрисовываться непосредственно в области просмотра, но кнопки и поля ввода на формах должны быть представлены в виде виджетов.


6.5. Стыкуемые окна.

Стыкуемые окна -- это окна, которые могут отстыковываться и пристыковываться к специальным областям стыковки. Самый яркий пример, пожалуй, это панели инструментов.

Объекты класса QMainWindow предоставляют в распоряжение программиста четыре области стыковки: вверху, внизу, слева и справа от центрального виджета. Когда создаются экземпляры класса QToolBar, они автоматически пристыковываются к верхней области окна-владельца.

Рисунок 6.11. "Плавающие" пристыковываемые окна.


Каждое из таких окон имеет "рукоятку". Она отображается в виде двух серых линий в левой или в верхней части окна, как это показано на рисунке 6.12. Ухватив мышью за "рукоятку", пользователь может перемещать стыкуемые окна из одной области стыковки в другую. Он так же может отделять стыкуемые окна от главного окна приложения. Отстыкованные окна могут свободно перемещаться по всей поверхности экрана, они имеют свою полосу заголовка и могут иметь собственную кнопку закрытия окна. Отстыкованные окна всегда отображаются поверх родительского окна.

Рисунок 6.12. QMainWindow с пятью стыкуемыми окнами.


Чтобы кнопка закрытия отображалась на отстыкованном окне, необходимо вызвать setCloseMode():
dockWindow->setCloseMode(QDockWindow::Undocked);      
      
Область стыковки -- QDockArea, имеет свое контекстное меню, со списком всех пристыкованных окон и панелей инструментов. После того, как отстыкованное окно было закрыто пользователем, оно может быть восстановлено с помощью этого меню.

Рисунок 6.13. Контекстное меню QDockArea.


Стыкуемые окна должны быть потомками класса QDockWindow. Если вам нужна панель инструментов с кнопками и некоторыми другими виджетами, то для этой цели прекрасно подойдет QToolBar, который является наследником QDockWindow. Ниже приводится пример создания экземпляра класса QToolBar, на который помещаются QComboBox, QSpinBox и ряд дополнительных кнопок. Полученная панель инструментов размещается в нижней области стыковки:
  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 очень широко используют стыкуемые окна, чтобы сделать интерфейс с пользователем более гибким.


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.


Предыдущая глава | Содержание | Следующая глава


Ж. Бланшетт, М. Саммерфильд, "Глава 6. Управление размещением виджетов" - 01/12/2004

г.ВИТЕБСК
РЕСПУБЛИКА БЕЛАРУСЬ

 
  О ресурре... Главная
[ ГЛАВНАЯ ] [ СОДЕРЖАНИЕ ]

E-mail : БИРЛ "Ombrello".

Hosted by uCoz