Лекция 12, графические интерфейсы, модель событий, библиотека PyQt5

Особенности PyQt5

Библиотека PyQt5 – это “обёртка” на языке Python, предоставляющая доступ для программам на Python к библиотеке Qt5, написанной на C++ с применением объектно-ориентированного программирования. Qt5 является открытым проектом и имеет две важных особенности: кроссплатформенность и очень хорошую документацию. Кроссплатформенность – это возможность одному и тому же коду одинаково работать на разных операционных системах и иногда даже на разных семействах процессоров. Так, Qt5 и PyQt5 отлично работают под управлением операционных систем Windows, Linux, MacOs. Хорошая документация относится больше к самой Qt5, документация обёртки PyQt5 несколько беднее, однако так как имена функций и методов в них одинаковы, всегда можно посмотреть документацию к самой Qt5, которая является одной из лучших среди графических библиотек.

Установка PyQt5

При установке PyQt5 важно обратить внимание, чтобы, во-первых, это действительно была PyQt версии 5, потому что на сайте компании-разработчика сейчас доступны версии PyQt4 и PyQt5. Вам нужна версия 5. Во-вторых, есть различные версии PyQt5, скомпилированные для различных версий Python: для Python3.0, Python3.1 и так далее. Поэтому надо запустить установленный у вас Python и посмотреть версию которую он выведет при старте интерактивного режима. Например, у меня выводится следующая информация:

Python 3.5.2 (default, Nov 17 2016, 17:05:23)

Это означает, что мне нужен PyQt5, скомпилированный для Python3.5. Ваша версия может отличаться от моей.

Виджеты

Главное понятие в PyQt5 – это “виджет”. Виджетом называется графический элемент, который может быть отображён на поверхности окна и который может принимать события мыши и клавиатуры. Каждая разновидность виджетов реализована как отдельный класс, например, все кнопки принадлежат классу QPushButton, а все ползунки – к классу QSlider. Все виджеты PyQt5 имеют своим классом-предком QWidget либо непосредственно, либо через другие классы по цепочке. Мы будет как использовать уже готовые виджеты, так и наследовать класс QWidget, создавая свои виджеты с нужным нам поведением.

На рисунке ниже представлена иерархия наследования основных виджетов, которыми мы будем пользоваться. На самом деле готовых в виджетов в PyQt5 намного больше, несколько десятков. Но на рисунке приведены самые полезные. Так же на рисунке показано, что можно создать собственный виджет на основе QWidget.

_images/widgets.png

Модель событий (асинхронный ввод)

                 --- устройства ввода
Операционная  ---
система
              ---
                 --- графическая --- графическое приложение 1
                     подсистема  --- графическое приложение 2
                                 --- графическое приложение 3

TODO: про синхронную модель ввода.

Синхронная модель ввода хорошо работает, когда есть один источник ввода. Для графических приложений это совсем не так. На виджете на изображении ниже можно насчитать ? источников ввода:

TODO рисунок

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

В модели событий каждый ввод порождает событие, которое приводит к выполнению определённой функции или метода некоторого объекта. Такие функции или методы называются обработчиками событий. Обработчики событий устанавливает графическая библиотека или сам программист.

Важно: порядок выполнения обработчиков зависит от порядка событий и не всегда контролируется программистом. TODO пример

____________________________________________
|                                    _ o x |
--------------------------------------------
|+-------> X                               |
||                                         |
||                                         |
|| Y                                       |
|v                                         |
|                                          |
--------------------------------------------

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

w = self.width()
h = self.height()

Примеры событий:

  • Отрисовка виджета
  • изменение размера виджета
  • нажатие мыши
  • нажатие на клавиатуру
  • клик по кнопке

Старый текст

                 --- устройства ввода
Операционная  ---
система
              ---
                 --- графическая --- графическое приложение 1
                     подсистема  --- графическое приложение 2
                                 --- графическое приложение 3

Раньше в C и Python3 мы использовали синхронную модель ввода (ввод происходит только тогда, когда мы его просим у ОС с помощью соответствующих функций).

Синхронная модель ввода плохо работает, когда есть много источников ввода разного типа. В графических подсистемах производится асинхронный ввод через модель событий.

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

____________________________________________
|                                    _ o x |
--------------------------------------------
|+-------> X                               |
||                                         |
||                                         |
|| Y                                       |
|v                                         |
|                                          |
--------------------------------------------

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

Сейчас мы рассмотрим, как графическая библиотека PyQt5 передаёт эти события нашему коду.

QWidget – класс, реализующий элементарный графический элемент, который может встраиваться в другие элементы или быть отдельным окном.

QPushButton – потомок QWidget, реализует функциональность кнопки.

QLineEdit – потомок QWidget, поле ввода текста.

MyWidget – наш собственный класс-потомок QWidget-а, в котором мы переопределим некоторые методы, которые графическая библиотека будет вызывать, когда с нашим виждетом происходят определённые события.

Пример “Пустой виджет”

Показываем пустой виджет с серым фоном. Несмотря на то, что этот пример, на первый взгляд, не делает ничего полезного, в нём объяснены стандартные действия, которые необходимо выполнять в любой программе на PyQt5. Не пренебрегайте прочтением кода и комментариев.

_images/example12_1.png

Исходный код примера:

import sys
from PyQt5.QtWidgets import QApplication, QWidget


def main():  # Для удобства работы, определим главную функцию нашего примера

    app = QApplication(sys.argv)
    # Создаём объект класса QApplication, который управляет
    # всем приложением на PyQt5.

    w = QWidget()  # Создаём виджет.

    # Изменяем размер рабочей области виджета
    # на 250x150 по ширине и высоте.
    w.resize(250, 150)

    # Передвинем окно на 300 пикселей вниз и вправо
    # от левой верхней точки экрана.
    w.move(300, 300)

    # Установим заголовок окна, который отображается в рамке и панели задач.
    w.setWindowTitle('Hello, world!!!')
    w.show()  # Настройки окна закончены, можно показать его пользователю.

    sys.exit(app.exec_())
    # app.exec_() содержит в себе главный цикл обработки событий библиотеки
    # PyQt5, который завершится, когда пользователь закроет окно или мы
    # своей программе вызовем функцию завершения этого цикла. Только после
    # этого произойдёт выход из питона с помощью функции sys.exit.


if __name__ == '__main__':  # Если файл запущен как программа,
    main()                  # (а не импортирован как модуль), вызовем main.

Пример “Две кнопки с обработчиками”

Создаём виджет, внутри которых будет размещено две кнопки, при нажатию на верхнюю в консоль будет выводиться фраза “Hello, world!”, при нажатии на нижнюю приложение закроется. Интерес представляет создание собственного виджета с помощью наследования класса QWidget, добавление кнопок на поверхность виджета и привязка функций-обработчиков.

_images/example12_2.png

Исходный код примера:

import sys
from PyQt5.QtWidgets import QApplication, QWidget, QPushButton


def print_hello():
    print('Hello, world')


# Класс с виджетом, содержащим две кнопки,
# одна из которых печатает сообщение на терминал, другая завершает программу.
class Example(QWidget):
    def __init__(self):
        # Найти суперкласс, в данном случае QtGui.QWidget
        # и инициализировать текущий объект так,
        # как инициализируется суперкласс
        super().__init__()
        # Отличие в инициализации состоит в вызове метода initUI.
        self.initUI()

    def initUI(self):
        # Создаём кнопку, которая будет размещена на виджете self.
        button_quit = QPushButton('Hello', self)
        # При нажатии кнопки будет происходить вызов функции print_hello
        # которая напечатает в отладочную консоль 'Hello, world'.
        button_quit.clicked.connect(print_hello)
        # Первоначально кнопка создалась со стандартным размером, в который
        # наш текст может не влезть. Задаём для неё размер, который позволит
        # вместить весь текст.
        button_quit.resize(button_quit.sizeHint())
        # Переместить кнопку на 50 пикселей вниз и 50 пикселей вправо
        # от левого верхнего края рабочей области виджета self.
        button_quit.move(50, 50)

        # Аналогичным образом создаём вторую кнопку, за исключением того,
        # что на кнопке будет написано 'Quit', а функцией-обработчиком будет
        # sys.exit, которая завершит программу при нажатии кнопки.
        button_print = QPushButton('Quit', self)
        button_print.clicked.connect(sys.exit)
        button_print.resize(button_print.sizeHint())
        button_print.move(50, 100)

        # окно смещено на 300x300 от верхнего края экрана, размер окна 250x150.
        self.setGeometry(300, 300, 250, 150)

        self.setWindowTitle('Quit button')
        self.show()


def main():
    app = QApplication(sys.argv)
    ex = Example()
    sys.exit(app.exec_())

if __name__ == '__main__':
    main()

Пример “Рисование линий”

Пример на рисование на поверхности виджета линий различного цвета и стиля.

_images/example12_3.png

Исходный код примера:

import sys
from PyQt5.QtWidgets import QWidget, QApplication
from PyQt5.QtGui import QPainter, QPen, QColor
from PyQt5.QtCore import Qt


class Example(QWidget):
    def __init__(self):
        super().__init__()
        self.initUI()

    def initUI(self):
        self.setGeometry(300, 300, 400, 220)
        self.setWindowTitle('Test line drawing')
        self.show()

    # Внимание! Всё рисование происходит только в
    # методе paintEvent, потому что QApplication
    # вызывает именно его при поступлении события
    # отрисовки от графической подсистемы.
    def paintEvent(self, e):
        qp = QPainter()       # Создаём объект QPainter для рисования.
        qp.begin(self)        # Начинаем рисовать на self, то есть на виджете.

        # Само рисование проходит в методе drawLines.
        # Передаём туда объект qp, с помощью которого
        # и будем рисовать.
        self.drawLines(qp)

        # Чтобы результаты всегда корректно отображались,
        # у объектов класса QPainter надо вызвать метод end.
        qp.end()

    def drawLines(self, qp):

        # Создаём ручку для рисования линий.
        # Аргументы констуктора QPen: цвет, ширина, тип линии.
        # QColor(255, 0, 0) -- цвет в модели RBG (красный, синий, зелёный),
        # в котором красная компонента выставлена на максимум (255),
        # синяя и зелёная отсутствуют.
        # Ширина ручки 2 пикселя, линия сплошная (Qt.SolidLine).
        pen = QPen(QColor(255, 0, 0), 2, Qt.SolidLine)
        # Будем рисовать на qp с помощью объекта pen.
        qp.setPen(pen)
        # проведём линию от точки (50, 40) до точки (350, 40).
        # Координаты относительно левого верхнего угла, ось y
        # направлена вниз.
        qp.drawLine(50, 40, 350, 40)

        # Зелёная линия шириной 4 пикселя.
        pen = QPen(QColor(0, 255, 0), 4, Qt.SolidLine)
        qp.setPen(pen)
        qp.drawLine(50, 60, 350, 60)

        # Синяя линия шириной 8 пикселей.
        pen = QPen(QColor(0, 0, 255), 8, Qt.SolidLine)
        qp.setPen(pen)
        qp.drawLine(50, 80, 350, 80)


        # Рисование чёрных линий с разным типом штриха.
        pen = QPen(QColor(0, 0, 0), 2, Qt.DashLine)
        qp.setPen(pen)
        qp.drawLine(50, 120, 350, 120)

        pen = QPen(QColor(0, 0, 0), 2, Qt.DashDotLine)
        qp.setPen(pen)
        qp.drawLine(50, 140, 350, 140)

        pen = QPen(QColor(0, 0, 0), 2, Qt.DotLine)
        qp.setPen(pen)
        qp.drawLine(50, 160, 350, 160)

        pen = QPen(QColor(0, 0, 0), 2, Qt.DashDotDotLine)
        qp.setPen(pen)
        qp.drawLine(50, 180, 350, 180)


def main():
    app = QApplication(sys.argv)
    ex = Example()
    sys.exit(app.exec_())


if __name__ == '__main__':
    main()