| |
Если в двух предыдущих частях этого цикла было многовато абстрактных понятий, то теперь мы займемся вещами конкретными и красивыми. До сих пор наши программы-примеры обходились без одной важной вещи, без которой ни одна серьезная программа обойтись не может без главного окна. На этот раз мы создадим главное окно и для этого заново познакомимся с Qt Designer.
Вы наверняка обратили внимание, что в Qt версии 3 Qt Designer выступал в роли своего рода полу-IDE. Помимо самого визуального редактора в нем присутствовал генератор проектов и собственный редактор кода, который позволял, например, редактировать текст методов-слотов. В Qt 4 разработчики решительно пересмотрели функции Qt Designer. Теперь этот инструмент предназначен исключительно для визуального редактирования. Редактор текста и генератор проектов из нового Qt Designer удалены. Объясняется это тем, что, по мнению разработчиков, генерация проектов и редактирование текстов программ должны целиком переместиться в IDE, такие, например, как KDevelop или Microsoft Visual Studio, а Qt Designer (который интегрируется c указанными IDE) будет выполнять функцию вспомогательного инструмента. Впрочем, версия KDevelop, поддерживающая Qt Designer 4, есть пока далеко не у всех, а продукт Microsoft нас и вовсе не касается, поэтому мы рассмотрим работу с Qt Designer 4, как с самостоятельным средством. Лично мне в новом дизайнере не понравилось то, что он представляет собой набор независимых друг от друга окон (наподобие Glade). Возможно, такая структура упрощает встраивание Qt Designer в другие программные среды, но при работе с самим дизайнером постоянно переключаться между окнами не очень удобно.
При запуске Qt Designer 4 нас встречает диалоговое окно выбора заготовки для формы, которую мы будем редактировать. Можно создать новую форму или открыть уже существующую. В списке заготовок форм всего 4 пункта, из которых мы выбираем пункт Main Window (рис. 1).
Рисунок 1. Окно выбора формы Qt Designer
Само главное окно Qt-приложения (класс QMainWindow) также претерпело немало изменений. Теперь панели инструментов (объекты класса QToolBar) и стыкуемые окна (объекты класса QDockWidget) реализованы независимо друг от друга. Принципы работы с главным меню тоже несколько изменились (мы рассмотрим все это ниже). Пока что выберем в приветственном окне Qt Designer форму главного окна и щелкнем кнопку Create. Появится форма с заготовкой меню и строки состояния. Мы добавим в эту форму компонент QFrame, сделаем так, чтобы соответствующий объект заполнял всю форму (команда Lay Out Vertically контекстного меню) и сохраним форму в файле mainform.ui.
Для нашей первой программы с полноценным главным окном нам, разумеется, понадобится функция main(). Вот она:
#include <QApplication>
#include <QMainWindow>
#include "ui_mainform.h"
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
Ui::MainWindow mainWindow;
QMainWindow * window = new QMainWindow;
mainWindow.setupUi(window);
window->show();
return app.exec();
}
Эта простенькая функция демонстрирует много не очень заметных, но существенных отличий Qt 4 от Qt 3. Мы начнем разбирать ее с заголовочных файлов. Включение заголовочных файлов QApplication и QMainWindow не должно вызывать вопросов. А вот откуда взялся файл ui_mainform.h? Этот файл должен описывать форму, созданную нами в визуальном редакторе, на языке C++. Иначе говоря, файл ui_mainform.h генерируется на основе данных, содержащихся в файле mainform.ui. Но чем его генерировать? В Qt3 нечто подобное создавалось автоматически программой Qt Designer (в Qt 3 этот файл назывался бы mainform.ui.h), но Qt Designer 4 не генерирует сам никаких исходных текстов. Мы можем возложить эту задачу на утилиту qmake. Тут возникает одна неуклюжесть. Обычно мы сначала пишем функцию main(), а затем вызываем такие инструменты как qmake. Но функция main() нуждается в файле ui_mainform.h, который только будет создан qmake. Я включил ссылку на файл ui_mainform.h в текст файла main.cpp, поскольку знал, что такой файл у нас появится (схему построения имен файлов с объявлением класса формы разгадать нетрудно имя состоит из префикса ui_, имени файла, в котором вы сохранили форму, и расширения .h). Если вы не уверены в том, как будет называться созданный автоматически заголовочный файл, можете сначала запустить qmake, а потом редактировать файл main.cpp. После этого qmake, конечно, придется запускать еще раз.
В файле ui_mainform.h определено пространство имен Ui, а в нем класс MainWindow (так по умолчанию назван класс, соответствующий форме главного окна). В функции main() мы создаем (статически) объект этого класса. Если вы думаете, что класс MainWindow происходит от QMainWindow, то ошибаетесь. На самом деле этот класс, описывающий интерфейс нашей программы, вообще не является потомком QWidget. Можно сказать, что класс MainWindow содержит инструкции по построению интерфейса, предназначенные для объекта класса QMainWindow. Объект этого класса мы и создаем далее в нашей программе. Метод mainWindow.setupUi() настраивает внешний вид объекта QMainWindow, в том числе, создает и настраивает дочерние элементы главного окна, которые мы определили в процессе редактирования формы.
Выполнив команды qmake project и qmake, мы можем собрать нашу программу с помощью make. В результате у нас получается главное окно с пустой формой в клиентской области. Конечно, на этом этапе наша программа выглядит не очень привлекательно, но она послужит холстом, на котором мы будем творить шедевры.
Хотя изменения графической системы Qt 4 по сравнению с Qt 3 не очень заметны на первый взгляд, новая система предоставляет весьма широкие возможности. Напомню, что основные функции работы с графикой в Qt 3 были реализованы в классе QPainter, который изначально предназначался для вывода графики в растровые массивы. Для поддержки подсистем вывода, не основанных на растровых массивах (например, принтеров PostScript) специальные объекты-потомки класса QPaintDevice эмулировали растровое устройство. В Qt 4 разработчики пошли более простым и логичным путем. Классы QPainter и QPaintDevice остались, но к ним добавился абстрактный класс QPaintEngine. Теперь все функции, реализующие специфику графического вывода на разных устройствах, собраны в классах-потомках QPaintEngine, соответствующих этим устройствам. Классы QPainter и QPaintDevice используют методы QPaintEngine для доступа к графическим устройствам, а не обращаются к этим устройствам напрямую, как раньше. Вам же, наоборот, не придется иметь дела с потомками QPaintEngine, если только вы не захотите расширить функциональность Qt 4, реализовав графический вывод на каком-нибудь неподдерживаемом устройстве. На практике это означает, что сладкая парочка QPainter и QPaintDevice теперь может рисовать практически на всех графических устройствах, доступных на данной платформе, причем работа с разными устройствами, будь то принтер или окно OpenGL, в значительной степени унифицирована. Еще одно преимущество новой системы заключается в том, что многие графические операции, которые раньше были реализованы чисто программными средствами, теперь могут использовать аппаратное ускорение и другие функции, поддерживаемые железом (раньше это было невозможно потому, что между QPainter и устройством лежала прослойка эмулятора растрового массива).
Посмотрим, как все это работает на практике. В качестве примера рассмотрим визуальный компонент QGLWidget. Благодаря новой архитектуре мы можем создавать изображения средствами QPainter в рабочем окне QGLWidget точно так же, как и на поверхности любого другого виджета. Не могу не отметить некоторую диалектичность процесса: когда-то я демонстрировал, как выводить графику OpenGL на поверхности объекта-потомка QWidget. Теперь мы воспользуемся QGLWidget для вывода обычных изображений, которые не являются частью трехмерных сцен. Такое использование OpenGL не по назначению отражает популярную в последнее время тенденцию задействовать мощь 3D-ускорителей в традиционно не-трехмерных задачах, например при отрисовке окон или работе с растровыми картинками.
Вернемся к программе с главным окном. Для того чтобы заставить наше главное окно что-то делать, нам следует создать его потомка. Посмотрим на объявление класса OGLWindow, который представляет собой главное окно программы arthur-demo (полный текст программы вы, как всегда, найдетепо ссылке в коце страницы).
#include <QMainWindow>
#include "ui_mainform.h"
class GLWidget;
class OGLWindow : public QMainWindow, public Ui::MainWindow
{
public:
OGLWindow(QWidget *parent = 0);
private:
GLWidget * glWidget;
};
Класс OGLWindow наследует сразу двум классам QMainWindow и Ui::MainWindow (второй из этих классов объявлен в файле ui_mainform.h. Что дает нам двойное наследование? Вернемся к функции main(), приведенной выше. Нам пришлось создать объект класса Ui::MainWindow для того, чтобы настроить внешний вид объекта QMainWindow. Для объекта класса OGLWindow дополнительных объектов создавать не придется, так как этот объект уже знает все, что нужно для построения его графического интерфейса. Ниже приводится текст конструктора OGLWindow:
OGLWindow::OGLWindow(QWidget *parent):QMainWindow(parent)
{
setupUi(this);
glWidget = new GLWidget(frame);
frame->setLayout(new QHBoxLayout);
frame->layout()->addWidget(glWidget);
}
Поскольку класс OGLWindow наследует классу Ui::MainWindow, метод setupUi() становится доступен в конструкторе OGLWindow. Мы вызываем этот метод и передаем ему в качестве параметра указатель this. Таким образом, объект класса OGLWindow сам настраивает свой интерфейс, а программисту, который захочет работать с нашим классом, не придется беспокоиться о вызове setupUi(). С классом GLWidget, производным от QGLWidget, мы познакомимся ниже. Объект frame представляет собой панель QFrame, которую мы добавили в форму главного окна в процессе визуального редактирования. Мы делаем объект класса GLWidget дочерним объектом этой панели.
Как вы, конечно, догадались, объект glWidget это визуальный компонент, предназначенный для вывода графики средствами OpenGL. Мы хотим, чтобы он занимал все пространство панели frame. Для этого мы создаем новый объект-менеджер компоновки класса QHBoxLayout, назначаем его в качестве текущего менеджера компоновки объекту frame (с помощью метода setLayout()) и добавляем в коллекцию менеджера объект glWidget. Самый простой способ заставить виджет QGLWidget выводить графическое изображение заключается в том, чтобы перекрыть метод paintEvent() в классе потомке QGLWidget. Именно для этого нам и нужен класс GLWidget. Наш вариант метода paintEvent() приводится ниже:
void GLWidget::paintEvent(QPaintEvent *event)
{
QPainter painter;
QPen pen;
painter.begin(this);
painter.eraseRect(QRect(0, 0, width(), height()));
pen.setColor(QColor(0, 127, 0));
pen.setWidth(4);
painter.setPen(pen);
painter.drawLine(0, 0, width(), height());
painter.setRenderHint(QPainter::Antialiasing);
pen.setColor(QColor(255, 0, 0));
painter.setPen(pen);
painter.drawLine(0, height(), width(), 0);
painter.setBrush(QColor(255, 0, 0, 127));
painter.drawRect(0, 0, width()/2, height());
painter.setBrush(QColor(0, 0, 255, 127));
painter.drawRect(0, 0, width(), height()/2);
painter.setBrush(QColor(0, 255, 0, 127));
painter.drawRect(width()/2, 0, width(), height());
painter.end();
}
Рисование начинается с вызова метода begin() объекта painter() класса QPainter. Аргументом этого метода должен быть указатель на объект QPaintDevice, к каковому типу теперь приводится и объект класса QGLWidget (и, естественно, его потомки). Останавливаться на каждой инструкции вывода графики мы не будем. Обращу внимание читателей на поддержку сглаживания контуров, которую мы включаем с помощью вызова метода setRenderHint() и смешивания цветов alpha blending (обратите внимание на четвертый аргумент конструктора QColor()). Сглаживание и смешивание являются новыми возможностями QPainter и могут выполняться с использованием аппаратной поддержки (например, поддержки 3D-ускорителя), если аппаратная поддержка включена в вашей системе.
В приведенном выше примере я специально не использовал функции OpenGL (например, воспользовался методом eraseRect() вместо glClearColor()), чтобы показать, что в графической системе Arthur можно задействовать возможности OpenGL, не используя сами команды OpenGL. В результате один и тот же код может использоваться для вывода графики в окне с помощью 3D-ускорителя, для записи графики в растровое изображения или для вывода изображений на принтер PostScript.
Рассмотрим теперь функцию main() нашей программы. Наследование класса главного окна сразу от двух предков (QMainWindow и UI::MainWindow) позволило упростить код и этой функции:
#include <QApplication>
#include "oglform.h"
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
OGLWindow * window = new OGLWindow;
window->show();
return app.exec();
}
Для того чтобы собрать программу успешно, в файле pro нужно включить поддержку модуля QtOpenGL:
QT += opengl
Наиболее полно мощь OpenGL проявляется (помимо собственно 3D-графики), в тех приложениях, которым приходится выполнять различные преобразования изображений на экране. Возможность работы с QGLWidget как с обычным компонентом для вывода графики, позволяет, например, создать программу просмотра электронных фотографий, которая будет использовать аппаратно-ускоренные спецэффекты при показе изображений. При этом тот же самый код может быть использован, например, для сохранения результатов преобразований в растровом формате.
Рисунок 2. Arthur: не-трехмерная графика с помощью OpenGL
В заключение обзора этой программы (первой нашей программы, использующей главное окно) позволю себе дать вам один совет. Готовьтесь к тому, что при работе с главным окном (и другими окнами, спроектированными в Qt Designer 4) вам все время придется создавать их потомков. В Qt Designer 3 мы могли редактировать код обработчиков сигналов в процессе визуального редактирования. Qt Designer 4 тоже позволяет нам связывать сигналы и слоты и даже определять новые слоты, но, поскольку редактировать код в дизайнере все равно нельзя, лучше определять все новые слоты и связывать их с сигналами в классе-потомке того класса, который сгенерирован Qt Designer 4.
Система вывода форматированного текста претерпела в Qt 4 не меньше, а, пожалуй, даже больше изменений, чем система вывода графики. Основой для работы с текстовыми документами в новой системе вывода текста, получившей название Scribe, служит класс QTextDocument. Объект этого класса хранит всю информацию о структуре форматированного документа, а также предоставляет функции для его редактирования. Вы можете использовать класс QTextEdit для ручного редактирования текста, содержащегося в QTextDocument, и класс QTextBrowser для просмотра. Любопытно отметить, что хотя в документации к Qt 4 разработчики советуют использовать повсеместно объекты QTextDocument (а не QTextString) для хранения текста, у класса QTextEdit нет конструктора, которому можно было бы передать ссылку на объект QTextDocument, а вот конструктор со ссылкой на объект QString - есть. Для того чтобы назначить объекту класса QTextEdit объект класса QTextDocument, необходимо вызвать метод setDocument().
Важную роль в редактировании содержимого QTextDocument играет класс QTextCursor. Объекты QTextCursor используются как для указания текущей позиции в документе, так и для обозначения выбранных фрагментов текста. Кроме этого объекты QTextCursor предоставляют в распоряжение программиста ряд методов, предназначенных для редактирования текста и изменения форматирования, начиная с выбранной позиции.
Высшей единицей логической структуры документа QTextDocument является фрейм, представленный классом QTextFrame. Весь документ содержится в корневом фрейме (получить доступ к корневому фрейму можно с помощью метода rootFrame()). Перейти от корневого фрейма к дочерним фреймам можно с помощью метода childFrames() объекта QTextFrame (собственные фреймы положены таким элементам документа как таблица или изображение). На более низком уровне элементы документа представлены текстовыми блоками (объекты класса QTextBlock). Текстовым блоком в форматированном документе является любой массив текста, к символам которого применены одинаковые элементы форматирования (это может быть абзац, фраза или отдельное слово). Обычно программа получает доступ к текстовым блокам тогда, когда пользователь выделяет фрагмент текст или когда сама программа выделяет текстовый фрагмент по какому-либо признаку.
Работа со сложными документами не единственный козырь Scribe. Помимо прочего эта система позволяет выполнять фигурный вывод текста. Если вы хотите, чтобы вместо ровных полей слева и права выводимый вами текст был выровнен по контуру какой-нибудь сложной фигуры, воспользуйтесь классом QTextLayout. Класс QTextLayout управляет компоновкой неформатированного текста, то есть, текста, который выводится одним шрифтом. Объекты класса QTextLayout позволяют нам сделать две вещи: разбить текст на строки с учетом параметров выбранного шрифта и ширины каждой строки и задать расположение каждой строки относительно левого края виртуального листа. После этого вывести фигурно расположенный текст на экран (на принтер, или на другое устройство) можно одной командой.
В качестве демонстрации сложного расположения текста мы рассмотрим программу scribe-demo, полный исходный текст которой вы найдете на диске. Как и программа arthur-demo, наша новая программа добавляет в главное окно свой собственный виджет. На этот раз наш фирменный виджет реализуется классом Label. Класс Label происходит не от класса QLabel, а непосредственно от класса QWidget, ведь мы собираемся выводить текст нашими собственными средствами и функциональность класса QLabel нам ни к чему. Объявление класса Label выглядит просто:
class Label : public QWidget
{
public:
Label(QWidget *parent);
private:
QTextLayout * textLayout;
void makeLayout();
protected:
void paintEvent(QPaintEvent *event);
};
В классе Label, как и в классе GLWidget, мы перекрываем метод paintEvent() класса-предка. Кроме того мы водим вспомогательный метод makeLayout(). Рассмотрим определения всех трех методов класса Label (конструктора, makeLayout() и paintEvent()).
Label::Label(QWidget *parent) : QWidget(parent)
{
QFont font("Times", 22, -1, true);
QString text = QObject::trUtf8("Этот фрагмент текста выведен на экран с помощью системы Scribe...");
textLayout = new QTextLayout(text, font);
}
void Label::makeLayout()
{
int indent = 20;
qreal vertPos = 10;
QTextLine line;
textLayout->beginLayout();
line = textLayout->createLine();
while (line.isValid())
{
line.setLineWidth(width() - 2 * indent);
line.setPosition(QPointF(indent, vertPos));
vertPos += line.height();
indent += 20;
line = textLayout->createLine();
}
textLayout->endLayout();
}
void Label::paintEvent(QPaintEvent *event)
{
QPainter painter;
QPen pen;
makeLayout();
painter.begin(this);
painter.eraseRect(QRect(0, 0, width(), height()));
pen.setColor(QColor(0, 0, 127));
painter.setPen(pen);
textLayout->draw(&painter, QPoint(0,0));
painter.end();
}
В конструкторе Label мы создаем объект класса QTextLayout. Конструктору объекта textLayout передаются два аргумента ссылка на строку теста и ссылка на объект QFont, который определяет используемый шрифт. Все самое интересное сосредоточено в методе makeLayout(). Мы начинаем работу с компоновщиком текста с вызова метода beginLayout(). Для каждой строки выводимого текста мы создаем объект класса QTextLine с помощью метода createLine() объекта textLayout. Этот метод будет возвращать объекты QTextLine со значением isValid(), равным true, до тех пор, пока весь текст не будет распределен на строки (общее количество строк, разумеется, зависит от размеров шрифта и ширины каждой строки). Ширина строки устанавливается с помощью метода setLineWidth(), а ее позиция методом setPosition(). Для того, чтобы строки не наезжали друг на друга, мы смещаем отступ очередной строки от верхнего края на значение, равное высоте строки. В этом нам помогает метод height() объекта класса QTextLine. После того как создание и расположение строк закончены, мы вызываем метод endLayout().
Метод makeLayout создает своего рода шаблон, содержащий текст. Для вывода этого текста в виджет достаточно вызвать метод draw() объекта textLayout. Первым аргументом метода draw() должна быть ссылка на объект класса QPainter, вторым аргументом ссылка на объект QPoint, определяющий расположение левого верхнего угла той области, в которой выводится текст (фактическое расположение строк, заданное в методе makeLayout(), при выводе текста будет отсчитываться относительно этой точки).
Рисунок 3. Текст с полями в форме треугольника.
Обратите внимание на важную особенность QTextLayout. Каждый раз, когда мы вызываем метод beginLayout() информация о предыдущей компоновке текста (и всех созданных объектах QTextLine) теряется. Эта особенность может стать источником труднообъяснимых ошибок для новичка, но нам на позволяет создать объект класса QTextLayout один раз (в конструкторе Label), а затем использовать его многократно для генерации компоновок текста, зависящих от ширины виджета, в методе makeLayout(). В связи с этим любопытно отметь, что в релизе Qt 4.4 (на момент написания статьи он находился на стадии бета) у класса QTextLayout появился метод clearLayout (), который очищает список строк компоновщика текста. Лично я большой пользы от этого метода не вижу (разве что кому-то понадобится обнулить список строк между вызовами beginLayout() и endLayout()), а, учитывая то, что этот метод поддерживается не всеми релизами Qt 4, пользоваться им не советую.
На этом наше знакомство с изобразительными средствами Qt 4 не закончилось. В следующий раз мы познакомимся с системой Graphics View, которая появилась в Qt 4.2 и была серьезно дополнена и переработана в Qt 4.3 и Qt 4.4.
Исходные тексты примеров (Artuthur)
Исходные тексты примеров (Scribe)
Закладки на сайте Проследить за страницей |
Created 1996-2024 by Maxim Chirkov Добавить, Поддержать, Вебмастеру |