LXF104:Qt4

Материал из Linuxformat.

Перейти к: навигация, поиск
Программирование в стиле Qt Осваиваем технологии, лежащие в основе нашумевшего KDE4

Содержание

Перо короля Артура

Qt4
ЧАСТЬ 3 Qt4 позволяет делать все не только правильно, но и красиво. Андрей Боровский расскажет о двух технологиях, непосредственно причастных к этому визуальному великолепию.

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

Qt Designer – старый новый друг

Вы наверняка обратили внимание, что в Qt версии 3 Qt Designer выступал в роли своего рода «полу-IDE». Помимо самого визуального редактора, в нем присутствовал генератор проектов и собственный редактор кода, который позволяли, например, редактировать текст методов-слотов. В Qt 4 разработчики решительно пересмотрели функционал Qt Designer, и теперь этот инструмент предназначен исключительно для визуального проектирования. Редактор текста и генератор проектов из нового Qt Designer удалены.

Рис. 1 Рис. 1. Окно выбора формы 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 нас встречает диалог выбора заготовки для формы, которую мы будем редактировать. Можно создать новую форму или открыть уже существующую. В списке заготовок форм всего четыре пункта, из которых нас интересует Main Window (рис. 1).

Само главное окно 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, но каким образом?

В Qt 3 нечто подобное создавалось автоматически программой Qt Designer (там оно называлось бы 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. В результате у нас получается главное окно с пустой формой в клиентской области. Конечно, на данном этапе наша программа выглядит не очень привлекательно, но она послужит холстом, на котором мы будем творить шедевры.

Графическая система Arthur

Хотя изменения графической подсистемы 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.

Рис. 2Рис. 2. Arthur: не-трехмерная графика с помощью OpenGL.

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

В заключение обзора этой программы (первой, использующей главное окно!) позволю себе дать вам один совет. Готовьтесь к тому, что при работе с главным окном (и другими окнами, спроектированными в 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 позволяют нам сделать две вещи: разбить текст на строки с учетом параметров выбранного шрифта и ширины каждой строки и задать расположение каждой строки относительно левого края «виртуального листа». После этого вывести фигурно расположенный текст на экран (на принтер, или на другое устройство) можно одной командой.

Рис. 3Рис. 3. Текст с полями в форме треугольника.

В качестве демонстрации сложного расположения текста мы рассмотрим программу scribe-demo, полный исходный текст которой вы найдете на диске. Как и программа arthur-demo, наша новая программа добавляет в главное окно свой собственный виджет. На этот раз наш фирменный виджет реализуется классом 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(), при выводе текста будет отсчитываться относительно этой точки).

Обратите внимание на важную особенность QTextLayout. Каждый раз, когда мы вызываем метод beginLayout(), информация о предыдущей компоновке текста (и всех созданных объектах QTextLine) теряется. Эта особенность может стать источником труднообъяснимых ошибок для новичка, но нам она позволяет создать объект класса QTextLayout один раз (в конструкторе Label), а затем использовать его многократно для генерации компоновок текста, зависящих от ширины виджета, в методе makeLayout(). В связи с этим любопытно отметь, что в релизе Qt 4.4 (на момент написания статьи он находился на стадии «бета») у класса QTextLayout появился метод clearLayout(), который очищает список строк компоновщика текста. Лично я большой пользы от этого метода не вижу (разве что кому-то понадобится «обнулить» список строк между вызовами beginLayout() и endLayout()), а учитывая то, что этот метод поддерживается не всеми релизами Qt 4, пользоваться им не советую.

На этом наше знакомство с изобразительными средствами Qt 4 не закончилось. В следующий раз мы познакомимся с системой Graphics View, появившейся в Qt 4.2. LXF

Личные инструменты
  • Купить электронную версию
  • Подписаться на бумажную версию