Рисование в Qt5. Введение. Трансформации. Трансляция. Часть 1. - АлтунинВВ.Блог - всё об IT-технологиях!
Понедельник, 20 апреля 2020 16:12

Рисование в Qt5. Введение. Трансформации. Трансляция. Часть 1.

Россия

Для рисования и вывода графики в Qt5 существует класс QPainter. С его помощью вы можете отрисовывать примитивы (линии, квадраты, арки и т.д.) и производить над ними трансформации (вращение, перемещение, масштабирование и т.д.). Так же он позволяет выводить текст с выравниванием и растровые изображения.

В данной статье мы рассмотрим основы рисования на Qt5 с помощью класса QPainter.

 

Введение в 2D рисование.

QPainter предоставляет богатый выбор методов для отрисовки разнообразной графики. Прежде всего давайте разберемся, как осуществляется вывод графических примитивов.

При рисовании графических примитивов всегда очень важно знать, где располагается начало координат, так как рисование всегда начинается с точки с координатами (0,0). По умолчанию QPainter использует в качестве этой точки верхний левый угол виджета. Давайте проверим это на практике.

В QtCreator Создадим новый проект Qt Widgets Application, назовем его QtDrawing.

Все настройки оставим по-умолчанию.

Откроем форму mainwindow.ui и изменим свойство Geometry выставив ширину и высоту равными 300 сохраним и закроем редактор форм.

Откроем mainwindow.cpp нажмем F4 и добавим после Ui::MainWindow *ui; строку

void paintEvent(QPaintEvent *);

Нажмем еще раз F4 и добавим в конец файла метод:

void MainWindow::paintEvent(QPaintEvent *)
{
    QPainter painter(this);
    QColor coordLineColor(255, 0, 0, 255);

    QPen apen = QPen(coordLineColor);
    apen.setWidth(5);
    painter.setPen(apen);

    painter.drawLine(QLine(0,0,300,0));
    painter.drawLine(QLine(0,0,0,300));

    painter.drawText(QPoint(5,13), "0,0");
    painter.drawText(QPoint(280,13), "300");
    painter.drawText(QPoint(5,295), "300");
}

Запустим сборку Ctrl+R и увидим на экране такое окно:

И так, что же мы сделали? Мы добавили новый обработчик события paintEvent. Он вызывается каждый раз когда возникает необходимост в отрисовке содержимого нашего виджета. В данном случае нашей формы. Согласно документации QPainter painter(this); должен вызываться только один раз в paintEvent, но об этом мы поговорим позже.

Далее мы задаем цвет, которым будут нарисованы линии и текст.

QColor coordLineColor(255, 0, 0, 255);

Мы используем класс QPen, он позволяет нам контролировать цвет или толщину линий. Мы задаем цвет линии и её толщину, чтобы оси координат были заметнее.

QPen apen = QPen(coordLineColor);
apen.setWidth(5);
painter.setPen(apen);

В самом конце мы просто вызываем painter.setPen(apen); и, тем самым, задаем цвет и толщину линий для рисования.

Далее все очень просто, строки:

painter.drawLine(QLine(0,0,300,0));
painter.drawLine(QLine(0,0,0,300));

отрисовывают оси X и Y, от начала координат.

Для задании координат линий используется класс QLine, в качестве параметров в который мы передаем координаты начала и конца отрезка.

Следующие строки:

painter.drawText(QPoint(5,13), "0,0");
painter.drawText(QPoint(280,13), "300");
painter.drawText(QPoint(5,295), "300");

Выводят текст по заданным координатам. Для их задания используется класс QPoint, в качестве параметров которому передается координаты точки, в которой должен быть выведен текст.

При выводе текста в QPainter есть целый ряд особенностей, но о них мы поговорим в следующих статьях.

Итак, мы убедились, что рисование начинается из начала координат. Вы можете всегда рисовать от начала координат, но тогда вам каждый раз придется заново вычислят координаты каждой точки, если потребуется передвинуть примитив на новую позицию, и это в целом неверный подход.

Глобальный объект класса для QPainter

Прежде чем мы продолжим, внесем изменения в механизм инициализации самого QPainter. В документации отдельно указывается, что строка QPainter painter(this); должна вызываться только один раз. Таким образом производится инициализация всех необходимых для работы данных. Но в таком подходе, есть небольшая проблема — объект класса painter существует только до конца метода

void MainWindow::paintEvent(QPaintEvent *).

Если мы хотим использовать отдельные функции для рисования целых частей или убрать в них весь повторяющийся код, нам придется передавать painter как параметр, что не очень хорошая идея. Мы поступим по другому:

Откроем mainwindow.cpp нажмем F4 и добавим после

void paintEvent(QPaintEvent *);

строку

QPainter *painter;

Таким образом мы объявили приватный указатель на объект класса QPainter Теперь осталось его инициализировать, изменим предыдущий код:

void MainWindow::paintEvent(QPaintEvent *)
{
    painter= new QPainter(this);
    QColor coordLineColor(255, 0, 0, 255);
    QPen apen = QPen(coordLineColor);
    apen.setWidth(5);
    painter->setPen(apen);
    painter->drawLine(QLine(0,0,300,0));
    painter->drawLine(QLine(0,0,0,300));
    painter->drawText(QPoint(5,13), "0,0");
    painter->drawText(QPoint(280,13), "300");
    painter->drawText(QPoint(5,295), "300");
}

Обратите внимание, так как мы используем ссылку на объект класса, то вместо «.» для доступа к методу класса, мы теперь используем «->». В принципе ничего сильно не изменилось, а теперь давайте уберем код для вывода осей координат в отдельный метод.

В конец файла добавим код:

void MainWindow::drawMainAxis()
{
    QColor coordLineColor(255, 0, 0, 255);
    QColor outlineColor(0, 255, 0, 255);
    QPen apen = QPen(coordLineColor);
    apen.setWidth(5);
    painter->setPen(apen);
    painter->drawLine(QLine(0,0,300,0));
    painter->drawLine(QLine(0,0,0,300));
    painter->drawText(QPoint(5,13), "0,0");
    painter->drawText(QPoint(280,13), "300");
    painter->drawText(QPoint(5,295), "300");
}

Нажмем F4 и добавим в блок private стоку

void drawMainAxis();

И изменим метод:

void MainWindow::paintEvent(QPaintEvent *)
{
    painter= new QPainter(this);
    drawMainAxis();

    painter->end();	
}

Обратите внимание мы добавили

painter→end();

в конец метода.

Это нужно сделать для того, чтобы painter знал, когда мы закончили рисование. Иначе в консоли вы можете увидеть подобные ошибки:

QBackingStore::endPaint() called with active painter; did you forget to destroy it or call QPainter::end() on it?

Запустим сборку Ctrl+R. Как видите ничего не поменялась но код основного метода стал более компактным.

Рисование примитивов

Давайте рассмотрим какие основные примитивы можно рисовать с помощью QPainter, для этого нарисуем такой вот детский рисунок:

Для этого добавим новый метод:

void MainWindow::drawChildPic()
{
    QColor linesColor(0, 0, 255, 255);
    QPen apen = QPen(linesColor);
    apen.setWidth(3);
    painter->setPen(apen);
  
//Квадрат - дом painter->drawRect(100,100,100,100);
//Квадрат - окно painter->drawRect(120,120,60,60); //Полигон - крыша и дымоход static const QPointF points[7] = { QPointF(100.0, 100.0), QPointF(150.0, 50.0), QPointF(170.0, 70.0), QPointF(170.0, 50.0), QPointF(180.0, 50.0), QPointF(180.0, 80.0), QPointF(200.0, 100.0) }; painter->drawConvexPolygon(points, 7);
//Линия - крышка дымохода painter->drawLine(QLine(165,50,185,50)); painter->drawLine(QLine(165,50,175,40)); painter->drawLine(QLine(175,40,185,50));
//Дуга - Радуга apen = QPen(QColor("red")); apen.setWidth(2); painter->setPen(apen); painter->drawArc(100,10,100,100,30*16,120*16); apen = QPen(QColor("orange")); apen.setWidth(2); painter->setPen(apen); painter->drawArc(100,12,100,100,30*16,120*16); apen = QPen(QColor("yellow")); apen.setWidth(2); painter->setPen(apen); painter->drawArc(100,14,100,100,30*16,120*16); apen = QPen(QColor("green")); apen.setWidth(2); painter->setPen(apen); painter->drawArc(100,16,100,100,30*16,120*16); apen = QPen(QColor("lightblue")); apen.setWidth(2); painter->setPen(apen); painter->drawArc(100,18,100,100,30*16,120*16); apen = QPen(QColor("blue")); apen.setWidth(2); painter->setPen(apen); painter->drawArc(100,20,100,100,30*16,120*16); apen = QPen(QColor("violet")); apen.setWidth(2); painter->setPen(apen); painter->drawArc(100,22,100,100,30*16,120*16);
//Эллипс - круг - солнце apen = QPen(QColor("yellow")); apen.setWidth(2); painter->setPen(apen); painter->drawEllipse(220, 30, 50,50); }

Для того чтобы нарисовать эту простую картинку мне пришлось подгонять координаты каждой точки, таким образом, чтобы все было на своих местах. Что не очень удобно, а что если нужно будет передвинуть дом вниз на 40 пикселей? Менять все координаты? Вводить две переменные и добавлять их значение ко всем координатам? А что если я хочу увеличить размер окна или добавить дверь. В общем придется вносить огромное число правок.

Чтобы избежать всего этого и существуют методы трансформации.

Трансформация примитивов

Трансляция

Трансляция — процесс при котором к каждой точке добавляется указанное смещение. Зачем нужна трансляция? Ответ очень прост, она позволяет не вычислять каждый раз смещение для каждой точки примитива, когда требуется переместить его в другую позицию. При этом координаты точек этого примитива не меняются! Давайте напишем простой пример, чтобы лучше разобраться:

void MainWindow::paintEvent(QPaintEvent *)
{
    painter= new QPainter(this);
    drawMainAxis();
    painter->translate(QPoint(150,150));
    drawMainAxis();
    painter->end();
}

Здесь всё просто, мы рисуем оси координат на своем месте, а потом производим трансляцию в середину экрана и снова отрисовываем оси, в центре экрана. Если мы немного расширим размер окна, то убедимся, что на самом деле оси отрисовываются полностью, просто рисунок выходит за пределы экрана.

При это трансляция каждый раз идет от последней точки начала координат, например, мы нарисовали оси в центре экрана и теперь хотим нарисовать их из точки 50,50:

void MainWindow::paintEvent(QPaintEvent *)
{
    painter= new QPainter(this);
    drawMainAxis();
    painter->translate(QPoint(150,150));
    drawMainAxis();
    painter->translate(QPoint(50,50));
    drawMainAxis();

    painter->end();
}

Мы, конечно ожидаем, что новые оси будут нарисованы в точке с координатами 50,50, но на практике у нас получается следующее:

Это происходит потому, что мы уже один раз применили трансляцию в точку 150,150. Так что теперь, когда мы вновь делаем трансляцию в точку 50,50 на самом деле мы переносим начало координат в точку 200,200. Что же делать? Есть 2 способа, можно просто сделать так:

painter->translate(QPoint(-100,-100));

или же сделать так:

painter->resetTransform();
painter->translate(QPoint(50,50));

resetTransform(); - сбрасывает все трансформации произведенные с помощью QPainter при этом не затрагиваются уже отрисованные примитивы.

Результат будет одинаков:

Теперь изменим наш детский рисунок с использованием трансляции:

void MainWindow::drawChildPicRel()
{
    QColor linesColor(0, 0, 255, 255);
    QPen apen = QPen(linesColor);
    apen.setWidth(3);
    painter->setPen(apen);
    painter->translate(QPoint(150,220));

//Квадрат - дом painter->drawRect(-50,-50,100,100);
//Квадрат - окно painter->drawRect(-30,-30,60,60);
//Полигон - крыша и дымоход static const QPointF points[7] = { QPointF(-50.0, -50.0), QPointF(0.0, -100.0), QPointF(20.0, -80.0), QPointF(20.0, -110.0), QPointF(30.0, -110.0), QPointF(30.0, -70.0), QPointF(50.0, -50.0) }; painter->drawConvexPolygon(points, 7);
//Линия - крышка дымохода painter->drawLine(QLine(15,-110,35,-110)); painter->drawLine(QLine(15,-110,25,-120)); painter->drawLine(QLine(25,-120,35,-110)); painter->resetTransform(); painter->translate(QPoint(50,50)); int rainbowWidth = 200; int rainbowHeight = 200; int rainbowInRad = 30 * 16; int rainbowOutRad = 120 * 16; int rainbowArcHeight = 4;
//Дуга - Радуга apen = QPen(QColor("red")); apen.setWidth(rainbowArcHeight); painter->setPen(apen); painter->drawArc(0,0,rainbowWidth,rainbowHeight,rainbowInRad,rainbowOutRad); apen.setWidth(rainbowArcHeight); apen = QPen(QColor("orange")); apen.setWidth(rainbowArcHeight); painter->setPen(apen); painter->drawArc(0,rainbowArcHeight,rainbowWidth,rainbowHeight,rainbowInRad,rainbowOutRad); apen = QPen(QColor("yellow")); apen.setWidth(rainbowArcHeight); painter->setPen(apen); painter->drawArc(0,rainbowArcHeight*2,rainbowWidth,rainbowHeight,rainbowInRad,rainbowOutRad); apen = QPen(QColor("green")); apen.setWidth(rainbowArcHeight); painter->setPen(apen); painter->drawArc(0,rainbowArcHeight*3,rainbowWidth,rainbowHeight,rainbowInRad,rainbowOutRad); apen = QPen(QColor("lightblue")); apen.setWidth(rainbowArcHeight); painter->setPen(apen); painter->drawArc(0,rainbowArcHeight*4,rainbowWidth,rainbowHeight,rainbowInRad,rainbowOutRad); apen = QPen(QColor("blue")); apen.setWidth(rainbowArcHeight); painter->setPen(apen); painter->drawArc(0,rainbowArcHeight*5,rainbowWidth,rainbowHeight,rainbowInRad,rainbowOutRad); apen = QPen(QColor("violet")); apen.setWidth(rainbowArcHeight); painter->setPen(apen); painter->drawArc(0,rainbowArcHeight*6,rainbowWidth,rainbowHeight,rainbowInRad,rainbowOutRad); painter->resetTransform(); painter->translate(QPoint(240,30));
//Эллипс - круг - солнце apen = QPen(QColor("yellow")); apen.setWidth(2); painter->setPen(apen); painter->setBrush(Qt::yellow); painter->drawEllipse(0, 0, 50,50); }

Запустим и получим вот такой рисунок:

Сначала мы делаем трансляцию в точку 150,220, затем рисуем домик, крышу и трубу, обратите внимание, для того чтобы наш домик был симметричен относительно начала координат мы используем отрицательные координаты. Мы слегка модифицировали код отрисовки радуги, таким образом, чтобы проще было изменять её размер и она так же выводится от начала координат, но перед этим мы делаем:

painter->resetTransform();
painter->translate(QPoint(50,50));

Т.е начинаем рисовать из точки 50,50

Затем отрисовывается солнце. Мы используем вызов

painter→setBrush(Qt::yellow);

чтобы залить эллипс желтым цветом.

Заключение

Мы, на примерах, разобрали основы рисования 2D графики и рассмотрели первую трансформацию примитивов — трансляцию.

Вот и всё на сегодня, в следующей части мы рассмотрим вращение и масштабирование в Qt5.

Исходный код проекта вы можете скачать с github.com.

Прочитано 649 раз Последнее изменение Вторник, 28 июля 2020 11:23