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

Далее в тексте p обозначает объект класса QPainter

Систему координат объектов класса QPainter можно изменять с помощью сдвига и масштабирования:

p.translate(dx, dy)
сдвинуть систему координат на вектор (dx, dy).
p.scale(sx, sy)
масштабировать систему координат с коэффициентами sx по оси X и sy по Y. sx > 1 “приближает” изображение, 0 < sx < 1 отдаляет изображение. Отрицательное значение sx отражает ось.

Пример, который переводит стандартную систему координат PyQt5 с началом в верхнем левом угле и направлением оси Y вниз в математическую систему с началом в левом нижнем угле и осью Y вверх.

p = QPainter()
p.begin(self)

p.translate(0, self.height())
p.scale(1, -1)

# Здесь должно происходить всё рисование

p.end()

Далее идёт список большинства методов, которые есть в QPainter для рисования. Надо сразу оговориться, что каждый метод рисования есть сразу в нескольких вариантах c разными наборами аргументов. Здесь сделан выбор в пользу использования QPointF и QRectF в аргументах. Буква F на конце имён этих классов говорит о том, что координаты в них описываются как числа с плавающей точкой. QPointF – координата точки на плоскости, QRectF – прямоугольник на плоскости (задаётся координатами левого верхнего угла и шириной и высотой).

Так же существуют классы QPoint и QRect, которые описываются целыми числами, но их использование как правило только приводит к проблемами с округлением.

p.drawPoint(QPointF(x, y))
нарисовать точку с координатами x, y
p.drawLine(QPointF(x0, y0), QPointF(x1, y1))
нарисовать линию от точки (x0, y0) до точки (x1, y1)
p.drawRect(QRectF(x0, y0, w, h))
нарисовать прямоугольник с левым верхним углом в точке (x0, y0), шириной w и высотой h.
p.drawEllipse(QPointF(x0, y0), rx, ry)
нарисовать эллипс с центром в точке (x0, y0). rx, ry - радиусы главных осей. Главные оси параллельны осям координат.
p.drawText(x, y, text)
нарисовать текст text в точке (x, y)
p.drawPolygon(polygon)
нарисовать многоугольник. polygon должен быть объектом класса QPolygonF, например QPolygonF([QPointF(-0.5, 0), QPointF(0, 0.866), QPointF(0.5, 0)]) – это равносторонний треугольник со стороной 1.
p.drawImage(QPointF(x, y), image)
нарисовать изображение image с левым верхним углом в точке x, y
p.drawImage(QRectF(x, y, w, h), image)
нарисовать изображение image, вписав его в прямоугольник с левым верхним углом (x, y), шириной w, высотой h.

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

#!/usr/bin/python3
'''
Программа для демонстрация различных способов рисования с помощью QPainter
'''
import sys
from PyQt5.QtWidgets import QWidget, QApplication
from PyQt5.QtGui import QPainter, QPen, QBrush, QColor, QPolygonF, QImage
from PyQt5.QtCore import Qt, QRectF, QPointF
from math import sin, cos, pi

class MyWidget(QWidget):
    def __init__(self):
        super().__init__()
        self.resize(500, 500)

    def paintEvent(self, event):
        # Создаём QPainter и начинаем рисование на виджете.
        p = QPainter()
        p.begin(self)
        # необязательная настройка для рисования линий. Рисование будет идти
        # чуть медленнее, но сами линии будут более сглаженными.
        p.setRenderHint(QPainter.Antialiasing)

        # Устанавливаем красную ручку шириной 2 пикселя для рисования контура
        p.setPen(QPen(QColor(255, 0, 0), 2))
	# Рисуем точку в координате 30, 40.
        p.drawPoint(QPointF(30, 40))

	# Рисуем две параллельные линии, одну зелёным, другую синим цветом
        p.setPen(QPen(QColor(0, 255, 0), 2))
        p.drawLine(QPointF(50, 10), QPointF(100, 110))
        p.setPen(QPen(QColor(0, 0, 255), 2))
        p.drawLine(QPointF(70, 10), QPointF(120, 110))

	# Чёрная ручка шириной 2 для контура и светло-красная кисть
        # для внутренней части фигуры
        p.setPen(QPen(QColor(0, 0, 0), 2))
        p.setBrush(QBrush(QColor(200, 100, 100)))
	# Рисуем прямоугольник 50 на 50 с левой верхней точкой (150, 40).
        p.drawRect(QRectF(150, 40, 50, 50))
	# Рисуем эллипс с центром в точке (270, 65) и полуосями
        # 50 по X и 30 по Y. 
        p.drawEllipse(QPointF(270, 65), 50, 30)

        # Рисуем небольшой прямогуольный треугольник с помощью
	# drawPolygon
        points = [QPointF(50, 350), QPointF(70, 370), QPointF(50, 390)]
        polygon = QPolygonF(points)
        p.drawPolygon(polygon)

        p.drawText(50, 200, "Hello world!")

        # Загружаем рисунок из файла. Загрузка рисунка достаточно
        # долгая операция, поэтому в более сложных программах лучше
        # выполнять загрузки не при каждой перерисовке окна, как здесь,
        # а один раз загружать картинку в свойство и потом пользоваться им. 
        # Например, можно перенести загрузку картинок в метод __init__.
        img = QImage("grumpycat.jpg")
        p.drawImage(QPointF(200, 200), img)
        p.drawImage(QRectF(70, 250, 50, 50), img)

        # Заканчиваем рисование
        p.end()

def main():
    app = QApplication(sys.argv)
    w = MyWidget()
    w.show()
    sys.exit(app.exec_())

if __name__ == "__main__":
    main()

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

С такими ситуациями вполне можно справиться и в рамках переопределения методов-обработчиков событий, однако часто будет получаться, что для определения реакции всего на одно событие (например, нажатие на кнопку), нужно определить новый класс, унаследовав его от базового виджета. Это приведёт к слишком большому количеству классов. Поэтому для таких ситуаций в PyQt5 придуман специальный механизм, позволяющий не определять новых классов, а привязывать к событиям объектов стандартных классов различные функции- или методы-обработчики.

У каждого события своё имя, подключить обработчик к событию можно с помощью кода вида

виджет.событие.connect(обработчик)

При этом аргументы обработчика должны совпадать с теми аргументами, которые передаёт обработчику событие.

Пример на подключение обработчиков к событиям:

#!/usr/bin/python3
'''
Программа для демонстрации обработки некоторых событий с помощью подключения
обработчиков. Обработчики, в отличие от шаблонных методов, могут иметь любые
имена, но их всегда нужно подключать к событиям с помощью метода connect.
'''
import sys
from PyQt5.QtWidgets import (
    QWidget, QApplication, QSlider, QSpinBox,
    QLabel, QCheckBox, QPushButton
)
from PyQt5.QtGui import QPainter, QPen
from PyQt5.QtCore import Qt, QTimer

class MyWidget(QWidget):
    def __init__(self):
        super().__init__()
        self.button = QPushButton(self)
        self.button.setText('Push me!')
        self.button.move(100, 100)
        # Подключаем обработчик нажатия кнопки.
        self.button.clicked.connect(
            self.button_clicked
        )

        self.slider = QSlider(Qt.Horizontal, self)
        self.slider.move(100, 150)
        # Подключаем обработчик новых значений слайдера
        self.slider.valueChanged.connect(
            self.slider_moved
        )

        self.label = QLabel(self)
        self.label.move(100, 200)

        self.checkbox = QCheckBox(self)
        self.checkbox.move(100, 250)
        self.checkbox.setText('Check me!')
        # Подключаем обработчик новых значений слайдера
        self.checkbox.stateChanged.connect(
            self.checkbox_changed
        )

    def checkbox_changed(self, new_state):
        # Метод, реагирующий на изменение состояния чекбокса.
        if new_state:
            self.label.setText('Ok')
        else:
            self.label.setText('Fail')
        # Изменим размер метки так, чтобы помещался текст.
        # В дальнейшем мы будем использовать менеджеры компоновки,
        # которые это будут делать автоматически.
        self.label.resize(self.label.sizeHint())

    def button_clicked(self):
        # Метод, реагирующий на нажатие на кнопку.
        print('Hello, world!')
        self.label.setText(
            'slider: ' + str(self.slider.value()) +
            ' checkbox: ' + str(self.checkbox.checkState())
        )
        self.label.resize(self.label.sizeHint())

    def slider_moved(self, new_value):
        # Метод, реагирующий на движение слайдера.
        self.label.setText(str(new_value))
        self.label.resize(self.label.sizeHint())


def main():
    app = QApplication(sys.argv)
    w = MyWidget()
    w.show()
    sys.exit(app.exec_())

if __name__ == "__main__":
    main()