Меню
Конспекты

Создание собственного виджета

Урок 35. Основы алгоритмизации и программирования на языке Python

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

Конспект урока "Создание собственного виджета"

Вопросы:

·     Программирование собственного виджета.

·     Создание виджета для побитового отображения целых чисел.

С чего же нужно начать создание собственного элемента управления? Как обычно, прежде чем перейти к программированию, необходимо продумать несколько моментов:

·     Для чего нам нужен виджет?

·     Какие у него должны быть свойства?

·     Как он должен выглядеть?

Только после того, как мы ответили на эти вопросы, можно переходить к программированию. При программировании собственного виджета нужно знать, что все элементы управления графическим интерфейсом являются объектами некоторых классов. То есть нам нужно запрограммировать класс, который будет описывать наш виджет. Можно создать собственный виджет с нуля, а можно изменить уже существующий. В библиотеке PyQt5 базовым классом для всех элементов управления является класс QWidget. Это означает, что если мы решили создать собственный виджет с нуля, то наш класс должен быть его наследником.  Если мы решили изменить уже существующий виджет, то стоит унаследовать новый класс от класса именно этого виджета. В конструкторе класса нужно определить свойства, необходимые для работы виджета.

Чтобы запрограммировать внешний вид нашего виджета, нужно переопределить в его классе метод paintEvent. Как мы помним, этот метод отвечает за низкоуровневое рисование.  В этом методе мы должны запрограммировать рисование внешнего вида нашего виджета.

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

Ответим на вопросы, которые мы узнали ранее. Ответ на первый вопрос нам даёт само условие задачи. Наш виджет нужен для побитового отображения целых чисел в прямом коде с двухбайтовой разрядностью. Что это означает? Виджет должен отображать состояние двухбайтовой ячейки памяти, в которой хранится целое число в прямом коде. Так как ячейка двухбайтовая, в ней 16 бит. Как мы помним, каждый бит может быть в одном из двух состояний, соответствующих 1 или 0. Как же получить двухбайтовый прямой код числа? Сперва мы должны перевести модуль числа из десятичной системы счисления в двоичную. Таким образом, мы получим состояние последних битов ячейки памяти. Так как число необязательно положительное, то его первый бит будет знаковым. Он должен содержать 0, если заданное число неотрицательное, а в противном случае – 1. Биты, оставшиеся свободными, заполняются нулями. Именно так можно получить прямой код числа.

Какие же свойства должны быть у нашего виджета? Единственным его свойством, которое не определено в базовом классе, будет число, представление которого наш виджет отображает. Назовём это свойство value.

Теперь подумаем, как будет выглядеть наш виджет. Наиболее простой способ побитово отобразить состояние двухбайтовой ячейки памяти – это представить её как таблицу битов. Сделаем в ней две строки и восемь столбцов. Каждая ячейка таблицы будет соответствовать одному биту. Пронумеруем ячейки слева направо, сверху вниз. Таким образом, первая строка таблицы будет отображать состояние первого байта, а вторая – второго. Разделим таблицу на ячейки сплошными линиями чёрного цвета в 3 пикселя толщиной. При этом ячейки, отображающие биты и заполненные единицами, закрасим красным цветом, а те, которые отображают нулевые биты, – белым.

Мы ответили на все перечисленные вопросы, теперь можно приступать к программированию. Как обычно, вначале загрузим в наш модуль необходимые для его работы классы из библиотеки PyQt5. Прежде всего загрузим из модуля QtWidgets базовый класс, который необходим для описания нашего виджета – QWidget. Из модуля QtCore загрузим класс Qt. В нём содержатся стили, необходимые для описания нашего виджета. И, наконец, из модуля QtGui загрузим классы QPainter, QPen, QBrush и QColor. Эти классы понадобятся нам при описании внешнего вида виджета. Также ранее мы описали функцию DecToBin для перевода целых положительных чисел из десятичной системы счисления в двоичную. Загрузим её из модуля Bin, который находится в нашей рабочей папке.

from PyQt5.QtWidgets import QWidget

from PyQt5.QtCore import Qt

from PyQt5.QtGui import QPainter, QPen, QBrush, QColor

from Bin import DecToBin

Теперь создадим класс BinFrame, который будет описывать поведение нашего виджета, унаследуем его от класса QWidget. Опишем конструктор нашего класса. Помимо обычного параметра self, у него будет ещё один параметр – ссылка на форму, на которой будет размещаться виджет. Назовём этот параметр parent. Прежде всего в этом методе вызовем конструктор класса-предка с параметром parent. Таким образом, при создании виджета мы будем указывать, в каком окне он будет размещаться. Теперь опишем уникальное свойство нашего виджета. Создадим внутреннее поле __value. Это поле будет содержать число, представление которого будет отображать виджет. По умолчанию сделаем его равным нулю. Теперь укажем минимальный размер нашего виджета, вызвав у него метод setMinimumSize. Укажем минимальный размер 80´20 пикселей. Описание конструктора класса завершено.

class BinFrame (QWidget):

    def __init__ (self, parent):

        super ().__init__ (parent)

        self.__value = 0

        self.setMinimumSize (80, 20)

Теперь нам нужно сделать метод-сэттэр, который позволит записывать значения в поле __value. Назовём этот метод setValue. Помимо параметра self, он будет принимать на вход новое значение поля __value. Назовём его val. В этом методе мы должны проверять, является ли новое значение поля корректным. Значение поля должно быть целым числом. Поэтому в блоке try проверим, можно ли преобразовать аргумент val в целочисленный тип int. Если при попытке преобразования возникло исключение – присвоим val ноль. Если преобразование прошло успешно, то мы должны проверить, можно ли новое число представить шестнадцатиразрядным двоичным кодом. Для этого запишем ветвление с условием, что значение параметра 215 > val > -215  и больше этого же числа, взятого со знаком минус. Если это условие выполняется, то мы присвоим полю value значение параметра val, в противном случае – 0. После этого мы должны обновить внешний вид нашего виджета, так как изменилось число, представление которого он отображает. Для этого вызовем у него метод repaint без параметров. Он вызывает у класса метод paintEvent для новой прорисовки виджета, если этот метод описан.

def setValue (self, val):

    try:

        val = int (val)

    except:

        val = 0

    if 2 ** 15 > val > -2 ** 15:

        self.__value = val

    else:

        self.__value = 0

    self.repaint ()

Теперь опишем в классе метод, который будет возвращать шестнадцатиразрядный прямой код числа. Назовём его getBinCode. В методе создадим пустую символьную строку, которую назовём code. В ней мы будем записывать код числа. Вначале проверим, не записан ли в поле value ноль, так как функция DecToBin работает только для положительных чисел. Если это условие выполняется, присвоим переменной code преобразованное в строку значение функиции DecToBin с модулем поля value в качестве аргумента. Мы получили в строке code двоичное представление модуля числа value. Теперь мы должны дополнить недостающую длину кода нулями. Для этого напишем цикл while, который будет работать до тех пор, пока длина строки code будет меньше пятнадцати символов. В этом цикле, в начало кода, будем добавлять символ 0. Теперь добавим к коду числа знаковый разряд. Для этого запишем ветвление с условием, что value < 0. В этом случае добавим в начало кода 1, иначе – 0. Если число положительное, то в знаковом разряде будет храниться 0, а если отрицательное – 1. Завершим работу метода, вернув значение переменной code.

def getBinCode (self):

    code = ''

    if self.__value != 0:

        code = str (DecToBin (abs (self.__value)))

    while len (code) < 15:

        code = '0' + code

    if self.__value < 0:

        code = '1' + code

    else:

        code = '0' + code

    return code

Теперь переопределим в нашем классе метод paintEvent. Он отвечает за рисование внешнего вида виджета. Создадим в нём, в переменной pnt, объект класса QPainter. Дальше вызовем у этого объекта методы begin с параметром self и end без параметров. Дилегируем рисование виджета пользовательскому методу drawWidget, которому в качестве параметра передадим ссылку на объект pnt.

def paintEvent (self, event):

    pnt = QPainter ()

    pnt.begin (self)

    self.drawWidget (pnt)

    pnt.end ()

Далее опишем метод drawWidget с параметрами self и pnt. Он будет содержать команды для рисования внешнего вида виджета. Как мы помним, наш виджет будет представлять собой таблицу из двух строк и восьми столбцов. Вычислим размеры одной ячеки. Для этого в переменной w сохраним без дробной части результат деления ширины виджета на 8, а в переменной h – без дробной части высоту виджета, делённую на 2. В переменной code сохраним результат метода getBinCode, то есть шестнадцатиразрядный двоичный код числа. Далее зададим ручку для рисования. Для этого вызовем у объекта pnt метод setPen, а в нём – конструктор класса QPen. Зададим ручку чёрного цвета со сплошной линией толщиной три пикселя. Теперь создадим кисти, которыми будем закрашивать ячейки таблицы. Сначала в переменной br0 создадим кисть белого цвета со стилем SolidPattern. После чего точно так же в переменной br1 создадим кисть красного цвета.

Теперь начнём рисовать ячейки таблицы. Для этого запишем цикл с параметром i, изменяющимся от 0 до 8, не включая последнее. Таким образом, в i мы будем перебирать индексы столбцов таблицы. В цикле запишем ветвление с условием, что i-тый символ кода равен 0. Если это условие выполняется, то назначим объекту pnt кисть br0 белого цвета, в противном случае – br1 красного цвета. После чего изобразим прямоугольник с координатами левого верхнего угла i  * w и 0, а также шириной w и высотой h. Скопируем код из цикла и изменим его для рисования второй строки ячеек. В ветвлении будет проверяться символ кода с номером i + 8, а изображаемый прямоугольник будет находиться на h пикселей ниже.

def drawWidget (self, pnt):

    w = int (self.width () / 8)

    h = int (self.height () / 2)

    code = self.getBinCode ()

    pnt.setPen (QPen (QColor (0, 0, 0), 3, Qt.SolidLine))

    br0 = QBrush (QColor (255, 255, 255), Qt.SolidPattern)

    br1 = QBrush (QColor (255, 0, 0), Qt.SolidPattern)

    for i in range (8):

        if code[i] == '0':

            pnt.setBrush (br0)

        else:

            pnt.setBrush (br1)

        pnt.drawRect (w * i, 0, w, h)

        if code[i + 8] == '0':

            pnt.setBrush (br0)

        else:

            pnt.setBrush (br1)

        pnt.drawRect (w * i, h, w, h)

На этом описание нашего класса завершено. Сохраним его в модуле MyWidget в рабочей папке. Теперь откроем заранее созданный модуль Test. В нём уже заранее описан код для запуска приложения с графическим интерфейсом, а также создан класс, который будет описывать работу его формы.

import sys

from PyQt5.QtWidgets import *

 

class Test (QWidget):

    def __init__ (self):

        super ().__init__ ()

        self.setupUI ()

 

    def setupUI (self):

        self.move (700, 350)

        self.setFixedSize (240, 200)

        self.setWindowTitle ('Test')

 

app = QApplication (sys.argv)

ex = Test ()

ex.show ()

sys.exit (app.exec ())

В начале модуля загрузим класс нашего виджета. Создадим в классе формы в его поле txtNum объект класса QLineEdit, то есть текстовое поле ввода. Разместим его на форме в точке с координатами 20, 20 и зададим ему размер 200´30 пикселей. В это поле введём число, представление которого будет отображать наш виджет. Теперь в поле frm создадим объект класса BinFrame, который мы описали ранее. Разместим созданный виджет в точке с координатами 20, 100 и зададим ему размер 200´50 пикселей.

self.txtNum = QLineEdit (self)

self.txtNum.setGeometry (20, 20, 200, 30)

self.frm = BinFrame (self)

self.frm.setGeometry (20, 100, 200, 50)

Теперь в классе формы опишем метод changeValue. Он будет обработчиком события, изменения текста в поле ввода. В этом методе запишем вызов у виджета frm метода setValue, в котором в качестве параметра зададим текст из поля ввода txNum. То есть в этом методе мы передаём число, заданное в поле ввода в наш виджет. В методе setupUI у поля txtNum назначим событию textChanged в качестве обработчика метод changeValue.

class Test(QWidget):

    def __init__ (self):

        super ().__init__ ()

        self.setupUI ()

 

    def setupUI (self):

        self.move (700, 350)

        self.setFixedSize (240, 200)

        self.setWindowTitle ('Test')

        self.txtNum = QLineEdit (self)

        self.txtNum.setGeometry (20, 20, 200, 30)

        self.frm = BinFrame (self)

        self.frm.setGeometry (20, 100, 200, 50)

        self.txtNum.textChanged.connect (self.changeValue)

 

    def changeValue (self):

        self.frm.setValue (self.txtNum.text ())

Запустим модуль на выполнение. На экране появилось окно, в котором находится поле ввода и созданный нами виджет. Попробуем задать в поле ввода цифру 2. В нашем виджете предпоследняя ячейка изменила цвет на красный. Что соответствует цифре два в двоичной системе счисления. Добавим перед цифрой два знак «-». Первая ячейка виджета, которая отображает состояние знакового бита, изменила цвет на красный. Зададим в поле ввода число 31841. Было закрашено сразу несколько ячеек виджета. Если мы поставим перед числом знак «-», то будет закрашена первая ячейка. Теперь введём число 64573, его нельзя представить в двухбайтовом коде, поэтому ни одна из ячеек не была закрашена и даже если мы добавим перед ним знак минус, этого не изменится. Наш виджет работает правильно. Задача решена.

Мы узнали:

·     Все виджеты в библиотеке PyQt5 являются наследниками класса QWidget.

·     Для того, чтобы задать рисование внешнего вида виджета нужно переопределить у его класса метод painEvent.

0
977

Комментарии 0

Чтобы добавить комментарий зарегистрируйтесь или на сайт