ВУЗ: Не указан
Категория: Не указан
Дисциплина: Не указана
Добавлен: 05.12.2023
Просмотров: 858
Скачиваний: 3
ВНИМАНИЕ! Если данный файл нарушает Ваши авторские права, то обязательно сообщите нам.
СОДЕРЖАНИЕ
Глава 17. Знакомство с PyQt 5 337
Закончив, выберем в меню File пункт Save и сохраним готовую форму в файл
MyForm.ui
При необходимости внести в этот файл какие-либо изменения, его можно открыть в про- грамме Qt Designer, выбрав в меню File пункт Open.
17.5.2. Использование UI-файла в программе
Как вы можете убедиться, внутри UI-файла содержится текст в XML-формате, а не про- граммный код на языке Python. Следовательно, подключить файл с помощью инструкции import не получится. Чтобы использовать UI-файл внутри программы, следует воспользо- ваться модулем uic
, который входит в состав библиотеки PyQt. Прежде чем использовать функции из этого модуля, необходимо подключить модуль с помощью инструкции: from PyQt5 import uic
Для загрузки UI-файла предназначена функция loadUi()
. Формат функции: loadUi(
1 ... 27 28 29 30 31 32 33 34 ... 83
Если второй параметр не указан, функция возвращает ссылку на объект формы. С помощью этой ссылки можно получить доступ к компонентам формы и, например, назначить об- работчики сигналов (листинг 17.4). Имена компонентов задаются в программе Qt Designer в свойстве objectName.
Листинг 17.4. Использование функции loadUi(). Вариант 1
# -*- coding: utf-8 -*- from PyQt5 import QtWidgets, uic import sys app = QtWidgets.QApplication(sys.argv) window = uic.loadUi("MyForm.ui") window.btnQuit.clicked.connect(app.quit) window.show() sys.exit(app.exec_())
Если во втором параметре указать ссылку на экземпляр класса, то все компоненты формы будут доступны через указатель self
(листинг 17.5).
Листинг 17.5. Использование функции loadUi(). Вариант 2
# -*- coding: utf-8 -*- from PyQt5 import QtWidgets, uic class MyWindow(QtWidgets.QWidget): def __init__(self, parent=None):
QtWidgets.QWidget.__init__(self, parent) uic.loadUi("MyForm.ui", self) self.btnQuit.clicked.connect(QtWidgets.qApp.quit) if __name__ == "__main__": import sys app = QtWidgets.QApplication(sys.argv) window = MyWindow()
338
Часть II. Библиотека PyQt 5 window.show() sys.exit(app.exec_())
Загрузить UI-файл позволяет также функция loadUiType()
— она возвращает кортеж из двух элементов: ссылки на класс формы и ссылки на базовый класс. Так как функция воз- вращает ссылку на класс, а не на экземпляр класса, мы можем создать множество экземпля- ров класса. После создания экземпляра класса формы необходимо вызвать метод setupUi()
и передать ему указатель self
(листинг 17.6).
Листинг 17.6. Использование функции loadUiType(). Вариант 1
# -*- coding: utf-8 -*- from PyQt5 import QtWidgets, uic class MyWindow(QtWidgets.QWidget): def __init__(self, parent=None):
QtWidgets.QWidget.__init__(self, parent)
Form, Base = uic.loadUiType("MyForm.ui") self.ui = Form() self.ui.setupUi(self) self.ui.btnQuit.clicked.connect(QtWidgets.qApp.quit) if __name__ == "__main__": import sys app = QtWidgets.QApplication(sys.argv) window = MyWindow() window.show() sys.exit(app.exec_())
Загрузить UI-файл можно и вне класса, после чего указать класс формы во втором парамет- ре в списке наследования, — в этом случае наш класс унаследует все методы класса формы
(листинг 17.7).
Листинг 17.7. Использование функции loadUiType(). Вариант 2 from PyQt5 import QtWidgets, uic
Form, Base = uic.loadUiType("MyForm.ui") class MyWindow(QtWidgets.QWidget, Form): def __init__(self, parent=None):
QtWidgets.QWidget.__init__(self, parent) self.setupUi(self) self.btnQuit.clicked.connect(QtWidgets.qApp.quit) if __name__ == "__main__": import sys app = QtWidgets.QApplication(sys.argv) window = MyWindow() window.show() sys.exit(app.exec_())
Глава 17. Знакомство с PyQt 5 339 17.5.3. Преобразование UI-файла в PY-файл
Вместо подключения UI-файла можно сгенерировать на его основе программный код на языке Python. Для этого служит утилита pyuic5
, чей исполняемый файл располагается в каталоге
<путь, по которому установлен Python>\Scripts
. Запустим командную строку и перейдем в каталог, в котором находится UI-файл. Для генерации Python-программы выпол- ним команду: pyuic5 MyForm.ui -o ui_MyForm.py
В результате будет создан файл ui_MyForm.py
, который мы уже можем подключить с по- мощью инструкции import
. Внутри файла находится класс
Ui_MyForm с методами setupUi()
и retranslateUi()
. При использовании процедурного стиля программирования следует соз- дать экземпляр класса формы, а затем вызвать метод setupUi()
и передать ему ссылку на экземпляр окна (листинг 17.8).
Листинг 17.8. Использование класса формы. Вариант 1
# -*- coding: utf-8 -*- from PyQt5 import QtWidgets import sys, ui_MyForm app = QtWidgets.QApplication(sys.argv) window = QtWidgets.QWidget() ui = ui_MyForm.Ui_MyForm() ui.setupUi(window) ui.btnQuit.clicked.connect(QtWidgets.qApp.quit) window.show() sys.exit(app.exec_())
При использовании ООП-стиля программирования следует создать экземпляр класса фор- мы, а затем вызвать метод setupUi()
и передать ему указатель self
(листинг 17.9).
Листинг 17.9. Использование класса формы. Вариант 2
# -*- coding: utf-8 -*- from PyQt5 import QtWidgets import ui_MyForm class MyWindow(QtWidgets.QWidget): def __init__(self, parent=None):
QtWidgets.QWidget.__init__(self, parent) self.ui = ui_MyForm.Ui_MyForm() self.ui.setupUi(self) self.ui.btnQuit.clicked.connect(QtWidgets.qApp.quit) if __name__ == "__main__": import sys app = QtWidgets.QApplication(sys.argv) window = MyWindow() window.show() sys.exit(app.exec_())
340
Часть II. Библиотека PyQt 5
Класс формы можно указать во втором параметре в списке наследования — в этом случае он унаследует все методы класса формы (листинг 17.10).
Листинг 17.10. Использование класса формы. Вариант 3 from PyQt5 import QtWidgets import ui_MyForm class MyWindow(QtWidgets.QWidget, ui_MyForm.Ui_MyForm): def __init__(self, parent=None):
QtWidgets.QWidget.__init__(self, parent) self.setupUi(self) self.btnQuit.clicked.connect(QtWidgets.qApp.quit) if __name__ == "__main__": import sys app = QtWidgets.QApplication(sys.argv) window = MyWindow() window.show() sys.exit(app.exec_())
Как видите, в PyQt можно создавать формы, размещать компоненты с помощью мыши, а затем непосредственно подключать UI-файлы в программе или преобразовывать их в Python-код с помощью утилиты pyuic5
, — все это очень удобно. Тем не менее, чтобы пол- ностью овладеть программированием на PyQt, необходимо уметь создавать код вручную.
Поэтому в оставшейся части книги мы больше не станем задействовать программу Qt
Designer.
17.6. Модули PyQt 5
В состав библиотеки PyQt 5 входит множество модулей, объединенных в пакет
PyQt5
. Упо- мянем самые важные из них:
QtCore
— содержит классы, не связанные с реализацией графического интерфейса. От этого модуля зависят все остальные модули;
QtGui
— содержит классы, реализующие низкоуровневую работу с оконными элемента- ми, обработку сигналов, вывод двухмерной графики и текста и др.;
QtWidgets
— содержит классы, реализующие компоненты графического интерфейса: окна, диалоговые окна, надписи, кнопки, текстовые поля и др.;
QtWebEngineCore
— включает низкоуровневые классы для отображения веб-страниц;
QtWebEngineWidgets
— реализует высокоуровневые компоненты графического интер- фейса, предназначенные для вывода веб-страниц и использующие модуль
QtWebEngineCore
;
П
РИМЕЧАНИЕ
Ранее для вывода веб-страниц использовались модули QtWebKit и QtWebKitWidgets.
Однако в версии PyQt 5.5 они были объявлены нерекомендованными для использования, а в версии 5.6 удалены.
QtMultimedia
— включает низкоуровневые классы для работы с мультимедиа;
Глава 17. Знакомство с PyQt 5 341
QtMultimediaWidgets
— реализует высокоуровневые компоненты графического интер- фейса с мультимедиа, использующие модуль
QtMultimedia
;
QtPrintSupport
— содержит классы, обеспечивающие поддержку печати и предвари- тельного просмотра документов;
QtSql
— включает поддержку работы с базами данных, а также реализацию SQLite;
QtSvg
— позволяет работать с векторной графикой (SVG);
QtNetwork
— содержит классы, предназначенные для работы с сетью;
QtXml и
QtXmlPatterns
— предназначены для обработки XML;
QtHelp
— содержат инструменты для создания интерактивных справочных систем;
QtWinExtras
— включает поддержку специфических возможностей Microsoft Windows;
Qt
— включает классы из всех модулей сразу.
П
РИМЕЧАНИЕ
Модуль QtOpenGL, обеспечивающий поддержку OpenGL, в версии PyQt 5.9 был объявлен нерекомендованным к использованию и будет удален в одной из последующих версий этой библиотеки. Его функциональность перенесена в модуль QtGui.
Для подключения модулей используется следующий синтаксис: from PyQt5 import <Названия модулей через запятую>
Так, например, можно подключить модули
QtCore и
QtWidgets
: from PyQt5 import QtCore, QtWidgets
В этой книге мы не станем рассматривать все упомянутые модули — чтобы получить ин- формацию по не рассмотренным здесь модулям, обращайтесь к соответствующей докумен- тации.
17.7. Типы данных в PyQt
Библиотека PyQt является надстройкой над написанной на языке C++ библиотекой Qt. По- следняя содержит множество классов, которые расширяют стандартные типы данных языка
C++ и реализуют динамические массивы, ассоциативные массивы, множества и др. Все эти классы очень помогают при программировании на языке C++, но для языка Python они не представляют особого интереса, т. к. весь этот функционал содержат стандартные типы данных. Тем не менее, при чтении документации вы столкнетесь с ними, поэтому сейчас мы кратко рассмотрим основные типы:
QByteArray
— массив байтов. Преобразуется в тип bytes
:
>>> from PyQt5 import QtCore
>>> arr = QtCore.QByteArray(bytes("str", "cp1251"))
>>> arr
PyQt5.QtCore.QByteArray(b'str')
>>> bytes(arr) b'str'
QVariant
— может хранить данные любого типа. Создать экземпляр этого класса можно вызовом конструктора, передав ему нужное значение. А чтобы преобразовать данные, хранящиеся в экземпляре класса
QVariant
, в тип данных Python, нужно вызвать метод value()
:
342
Часть II. Библиотека PyQt 5
>>> from PyQt5 import QtCore
>>> n = QtCore.QVariant(10)
>>> n
>>> n.value()
10
Также можно создать «пустой» экземпляр класса
QVariant
, вызвав конструктор без па- раметров:
>>> QtCore.QVariant() # Пустой объект
Если какой-либо метод ожидает данные типа
QVariant
, ему можно передать данные лю- бого типа.
Еще этот класс поддерживает метод typeName()
, возвращающий наименование типа хра- нящихся в экземпляре данных:
>>> from PyQt5 import QtCore
>>> n = QtCore.QVariant(10)
>>> n.typeName()
'int'
Кроме того, PyQt 5 поддерживает классы
QDate
(значение даты),
QTime
(значение времени),
QDateTime
(значение даты и времени),
QTextStream
(текстовый поток),
QUrl
(URL-адрес) и некоторые другие.
17.8. Управление основным циклом приложения
Для взаимодействия с системой и обработки возникающих сигналов предназначен основной цикл приложения. После вызова метода exec_()
программа переходит в бесконечный цикл.
Инструкции, расположенные после вызова этого метода, будут выполнены только после завершения работы всего приложения. Цикл автоматически прерывается после закры- тия последнего открытого окна приложения. С помощью статического метода setQuitOnLastWindowClosed()
класса
QApplication это поведение можно изменить.
Чтобы завершить работу приложения, необходимо вызвать слот quit()
или метод exit([returnCode=0])
класса
QApplication
. Поскольку программа находится внутри цикла, вызвать эти методы можно лишь при наступлении какого-либо события, — например, при нажатии пользователем кнопки.
После возникновения любого сигнала основной цикл прерывается, и управление передается в обработчик этого сигнала. После завершения работы обработчика управление возвраща- ется основному циклу приложения.
Если внутри обработчика выполняется длительная операция, программа перестает реагиро- вать на события. В качестве примера изобразим длительный процесс с помощью функции sleep()
из модуля time
(листинг 17.11).
Листинг 17.11. Выполнение длительной операции
# -*- coding: utf-8 -*- from PyQt5 import QtWidgets import sys, time
Глава 17. Знакомство с PyQt 5 343 def on_clicked(): time.sleep(10) # "Засыпаем" на 10 секунд app = QtWidgets.QApplication(sys.argv) button = QtWidgets.QPushButton("Запустить процесс") button.resize(200, 40) button.clicked.connect(on_clicked) button.show() sys.exit(app.exec_())
В этом примере при нажатии кнопки Запустить процесс вызывается функция on_clicked()
, внутри которой мы приостанавливаем выполнение программы на десять секунд и тем са- мым прерываем основной цикл. Попробуйте нажать кнопку, перекрыть окно другим окном, а затем заново его отобразить, — вам не удастся это сделать, поскольку окно перестает реа- гировать на любые события, пока не закончится выполнение процесса. Короче говоря, про- грамма просто зависнет.
Длительную операцию можно разбить на несколько этапов и по завершении каждого этапа выходить в основной цикл с помощью статического метода processEvents([flags=AllEvents])
класса
QCoreApplication
, от которого наследуется класс
QApplication
. Переделаем преды- дущую программу, инсценировав с помощью цикла длительную операцию, которая выпол- няется 20 секунд (листинг 17.12).
Листинг 17.12. Использование метода processEvents()
# -*- coding: utf-8 -*- from PyQt5 import QtWidgets import sys, time def on_clicked(): button.setDisabled(True) # Делаем кнопку неактивной for i in range(1, 21):
QtWidgets.qApp.processEvents() # Запускаем оборот цикла time.sleep(1) # "Засыпаем" на 1 секунду print("step -", i) button.setDisabled(False) # Делаем кнопку активной app = QtWidgets.QApplication(sys.argv) button = QtWidgets.QPushButton("Запустить процесс") button.resize(200, 40) button.clicked.connect(on_clicked) button.show() sys.exit(app.exec_())
В этом примере длительная операция разбита на одинаковые этапы, после выполнения каж- дого из которых выполняется выход в основной цикл приложения. Теперь при перекрытии окна и повторном его отображении оно будет перерисовано — таким образом, приложение по-прежнему будет взаимодействовать с системой, хотя и с некоторой задержкой.
344
Часть II. Библиотека PyQt 5 17.9. Многопоточные приложения
При обработке больших объемов данных не всегда можно равномерно разбить операцию на небольшие по времени этапы, поэтому при использовании метода processEvents()
возмож- ны проблемы, и тогда имеет смысл вынести длительную операцию в отдельный поток, — в этом случае операция станет выполняться параллельно с основным циклом приложения и не будет его блокировать.
В одном процессе можно запустить сразу несколько независимых потоков, и если ваш ком- пьютер оснащен многоядерным процессором, потоки будут равномерно распределены по его ядрам. За счет этого можно не только избежать блокировки GUI-потока приложения, в котором выполняется обновление его интерфейса, но и значительно увеличить эффектив- ность выполнения кода. Завершение основного цикла приложения приводит к завершению работы всех потоков.
17.9.1. Класс QThread: создание потока
Для создания потока в PyQt предназначен класс
QThread
, который объявлен в модуле
QtCore и наследует класс
QObject
. Конструктор класса
QThread имеет следующий формат:
<Объект> = QThread([parent=None])
Чтобы использовать потоки, следует создать класс, который будет наследником класса
QThread
, и определить в нем метод run()
. Код, расположенный в методе run()
, будет вы- полняться в отдельном потоке, а после завершения выполнения метода run()
этот поток прекратит свое существование. Затем нужно создать экземпляр класса и вызвать метод start()
, который после запуска потока вызовет метод run()
. Обратите внимание, что если напрямую вызвать метод run()
, то код станет выполняться в основном, а не в отдельном потоке. Метод start()
имеет следующий формат: start([priority=QThread.InheritPriority])
Параметр priority задает приоритет выполнения потока по отношению к другим потокам.
Следует учитывать, что при наличии потока с самым высоким приоритетом поток с самым низким приоритетом в некоторых операционных системах может быть просто проигнори- рован. Приведем допустимые значения параметра (в порядке увеличения приоритета) и со- ответствующие им атрибуты из класса
QThread
:
0
—
IdlePriority
— самый низкий приоритет;
1
—
LowestPriority
;
2
—
LowPriority
;
3
—
NormalPriority
;
4
—
HighPriority
;
5
—
HighestPriority
;
6
—
TimeCriticalPriority
— самый высокий приоритет;
7
—
InheritPriority
— автоматический выбор приоритета (значение по умолчанию).
Задать приоритет потока также позволяет метод setPriority(<Приоритет>)
. Узнать, какой приоритет использует запущенный поток, можно с помощью метода priority()
После запуска потока генерируется сигнал started()
, а после завершения — сигнал finished()
. Назначив обработчики этим сигналам, можно контролировать статус потока из
Глава 17. Знакомство с PyQt 5 345 основного цикла приложения. Если необходимо узнать текущий статус, следует воспользо- ваться методами isRunning() и isFinished()
: метод isRunning()
возвращает значение
True
, если поток выполняется, а метод isFinished()
— значение
True
, если поток закончил выполнение.
Потоки выполняются внутри одного процесса и имеют доступ ко всем глобальным пере- менным. Однако следует учитывать, что из потока нельзя изменять что-либо в GUI-потоке приложения, — например, выводить текст на надпись. Для изменения данных в GUI-потоке нужно использовать сигналы. Внутри потока у нужного сигнала вызывается метод emit()
, который, собственно, и выполняет его генерацию. В параметрах метода emit()
можно ука- зать данные, которые будут переданы обработчику сигнала. А внутри GUI-потока назна- чаем обработчик этого сигнала и в обработчике пишем код, который и будет обновлять ин- терфейс приложения.
Рассмотрим использование класса
QThread на примере (листинг 17.13).
Листинг 17.13. Использование класса QThread
# -*- coding: utf-8 -*- from PyQt5 import QtCore, QtWidgets class MyThread(QtCore.QThread): mysignal = QtCore.pyqtSignal(str) def __init__(self, parent=None):
QtCore.QThread.__init__(self, parent) def run(self): for i in range(1, 21): self.sleep(3) # "Засыпаем" на 3 секунды
# Передача данных из потока через сигнал self.mysignal.emit("i = %s" % i) class MyWindow(QtWidgets.QWidget): def __init__(self, parent=None):
1 ... 28 29 30 31 32 33 34 35 ... 83
QtWidgets.QWidget.__init__(self, parent) self.label = QtWidgets.QLabel("Нажмите кнопку для запуска потока") self.label.setAlignment(QtCore.Qt.AlignHCenter) self.button = QtWidgets.QPushButton("Запустить процесс") self.vbox = QtWidgets.QVBoxLayout() self.vbox.addWidget(self.label) self.vbox.addWidget(self.button) self.setLayout(self.vbox) self.mythread = MyThread() # Создаем экземпляр класса self.button.clicked.connect(self.on_clicked) self.mythread.started.connect(self.on_started) self.mythread.finished.connect(self.on_finished) self.mythread.mysignal.connect(self.on_change, QtCore.Qt.QueuedConnection) def on_clicked(self): self.button.setDisabled(True) # Делаем кнопку неактивной self.mythread.start() # Запускаем поток def on_started(self): # Вызывается при запуске потока self.label.setText("Вызван метод on_started()")
346
Часть II. Библиотека PyQt 5 def on_finished(self): # Вызывается при завершении потока self.label.setText("Вызван метод on_finished()") self.button.setDisabled(False) # Делаем кнопку активной def on_change(self, s): self.label.setText(s) if __name__ == "__main__": import sys app = QtWidgets.QApplication(sys.argv) window = MyWindow() window.setWindowTitle("Использование класса QThread") window.resize(300, 70) window.show() sys.exit(app.exec_())
Здесь мы создали класс
MyThread
, который является наследником класса
QThread
. В нем мы определили свой собственный сигнал mysignal
, для чего создали атрибут с таким же име- нем и занесли в него значение, возвращенное функцией pyqtSignal()
из модуля
QtCore
Функции pyqtSignal()
мы передали в качестве параметра тип str
(строка Python), тем са- мым указав PyQt, что вновь определенный сигнал будет принимать единственный параметр строкового типа: mysignal = QtCore.pyqtSignal(str)
В том же классе мы определили обязательный для потоков метод run()
— в нем произво- дится имитация процесса с помощью цикла for и метода sleep()
: каждые три секунды вы- полняется генерация сигнала mysignal и передача текущего значения переменной i
в соста- ве строки: self.mysignal.emit("i = %s" % i)
Внутри конструктора класса
MyWindow мы назначили обработчик этого сигнала с помощью выражения: self.mythread.mysignal.connect(self.on_change, QtCore.Qt.QueuedConnection)
Здесь все нам уже знакомо: у свойства mysignal потока, которое представляет одноименный сигнал, вызывается метод connect()
, и ему первым параметром передается обработчик. Во втором параметре метода connect()
с помощью атрибута
QueuedConnection указывается, что сигнал помещается в очередь обработки событий, и обработчик должен выполняться в по- токе приемника сигнала, т. е. в GUI-потоке. Из GUI-потока мы можем смело изменять свой- ства компонентов интерфейса.
Теперь рассмотрим код метода класса
MyWindow
, который станет обработчиком сигнала mysignal
: def on_change(self, s): self.label.setText(s)
Второй параметр этого метода служит для приема параметра, переданного этому сигналу.
Значение этого параметра будет выведено в надписи с помощью метода setText()
Еще в конструкторе класса
MyWindow производится создание надписи и кнопки, а затем их размещение внутри вертикального контейнера. Далее выполняется создание экземпляра класса
MyThread и сохранение его в атрибуте mythread
. С помощью этого атрибута мы мо-
Глава 17. Знакомство с PyQt 5 347 жем управлять потоком и назначить обработчики сигналов started()
, finished()
и mysignal
. Запуск потока производится с помощью метода start()
внутри обработчика на- жатия кнопки. Чтобы исключить повторный запуск потока, мы с помощью метода setDisabled() делаем кнопку неактивной, а после окончания работы потока внутри обра- ботчика сигнала finished()
опять делаем кнопку активной.
Обратите внимание, что для имитации длительного процесса мы использовали статический метод sleep()
из класса
QThread
, а не функцию sleep()
из модуля time
. Вообще, приоста- новить выполнение потока позволяют следующие статические методы класса
QThread
:
sleep()
— продолжительность задается в секундах:
QtCore.QThread.sleep(3) # "Засыпаем" на 3 секунды
msleep()
— продолжительность задается в миллисекундах:
QtCore.QThread.msleep(3000) # "Засыпаем" на 3 секунды
usleep()
— продолжительность задается в микросекундах:
QtCore.QThread.usleep(3000000) # "Засыпаем" на 3 секунды
Еще один полезный статичный метод класса
QThread
— yieldCurrentThread()
— немедлен- но приостанавливает выполнение текущего потока и передает управление следующему ожидающему выполнения потоку, если таковой есть:
QtCore.QThread.yieldCurrentThread()
17.9.2. Управление циклом внутри потока
Очень часто внутри потока одни и те же инструкции выполняются многократно. Например, при осуществлении мониторинга серверов в Интернете на каждой итерации цикла посыла- ется запрос к одному и тому же серверу. При этом внутри метода run()
используется беско- нечный цикл, выход из которого производится после окончания опроса всех серверов.
В некоторых случаях этот цикл необходимо прервать преждевременно по нажатию кнопки пользователем. Чтобы это стало возможным, в классе, реализующем поток, следует создать атрибут, который будет содержать флаг текущего состояния. Далее на каждой итерации цикла проверяется состояние флага и при его изменении прерывается выполнение цикла.
Чтобы изменить значение атрибута, создаем обработчик и связываем его с сигналом clicked()
соответствующей кнопки. При нажатии кнопки внутри обработчика производим изменение значения атрибута. Пример запуска и остановки потока с помощью кнопок при- веден в листинге 17.14.
Листинг 17.14. Запуск и остановка потока
# -*- coding: utf-8 -*- from PyQt5 import QtCore, QtWidgets class MyThread(QtCore.QThread): mysignal = QtCore.pyqtSignal(str) def __init__(self, parent=None):
QtCore.QThread.__init__(self, parent) self.running = False # Флаг выполнения self.count = 0 def run(self): self.running = True
348
Часть II. Библиотека PyQt 5 while self.running: # Проверяем значение флага self.count += 1 self.mysignal.emit("count = %s" % self.count) self.sleep(1) # Имитируем процесс class MyWindow(QtWidgets.QWidget): def __init__(self, parent=None):
QtWidgets.QWidget.__init__(self, parent) self.label = QtWidgets.QLabel("Нажмите кнопку для запуска потока") self.label.setAlignment(QtCore.Qt.AlignHCenter) self.btnStart = QtWidgets.QPushButton("Запустить поток") self.btnStop = QtWidgets.QPushButton("Остановить поток") self.vbox = QtWidgets.QVBoxLayout() self.vbox.addWidget(self.label) self.vbox.addWidget(self.btnStart) self.vbox.addWidget(self.btnStop) self.setLayout(self.vbox) self.mythread = MyThread() self.btnStart.clicked.connect(self.on_start) self.btnStop.clicked.connect(self.on_stop) self.mythread.mysignal.connect(self.on_change, QtCore.Qt.QueuedConnection) def on_start(self): if not self.mythread.isRunning(): self.mythread.start() # Запускаем поток def on_stop(self): self.mythread.running = False # Изменяем флаг выполнения def on_change(self, s): self.label.setText(s) def closeEvent(self, event): # Вызывается при закрытии окна self.hide() # Скрываем окно self.mythread.running = False # Изменяем флаг выполнения self.mythread.wait(5000) # Даем время, чтобы закончить event.accept() # Закрываем окно if __name__ == "__main__": import sys app = QtWidgets.QApplication(sys.argv) window = MyWindow() window.setWindowTitle("Запуск и остановка потока") window.resize(300, 100) window.show() sys.exit(app.exec_())
В этом примере в конструкторе класса
MyThread создается атрибут running
, и ему присваи- вается значение
False
. При запуске потока внутри метода run()
значение атрибута изменя- ется на
True
. Затем запускается цикл, в котором атрибут указывается в качестве условия.
Как только значение атрибута станет равным значению
False
, цикл будет остановлен.
Внутри конструктора класса
MyWindow производится создание надписи, двух кнопок и эк- земпляра класса
MyThread
. Далее назначаются обработчики сигналов. При нажатии кнопки
Глава 17. Знакомство с PyQt 5 349
Запустить поток запустится метод on_start()
, внутри которого с помощью метода isRunning()
производится проверка текущего статуса потока. Если поток не запущен, выполняется его запуск вызовом метода start()
. При нажатии кнопки Остановить поток запустится метод on_stop()
, в котором атрибуту running присваивается значение
False
. Это значение является условием выхода из цикла внутри метода run()
Путем изменения значения атрибута можно прервать выполнение цикла только в том слу- чае, если закончилось выполнение очередной итерации. Если поток длительное время ожи- дает какого-либо события (например, ответа сервера), можно так и не дождаться заверше- ния потока. Чтобы принудительно прервать выполнение потока, следует воспользоваться методом terminate()
. Однако к этому методу рекомендуется прибегать только в крайнем случае, поскольку прерывание производится в любой части кода. При этом блокировки ав- томатически не снимаются, а кроме того, можно повредить данные, над которыми произво- дились операции в момент прерывания. После вызова метода terminate()
следует вызвать метод wait()
При закрытии окна приложение завершает работу, что также приводит к завершению всех потоков. Чтобы предотвратить повреждение данных, следует перехватить событие закрытия окна и дождаться окончания выполнения. Чтобы перехватить событие, необходимо внутри класса создать метод с предопределенным названием, в нашем случае — с названием closeEvent()
. Этот метод будет автоматически вызван при попытке закрыть окно. В качест- ве параметра метод принимает объект события event
, через который можно получить до- полнительную информацию о событии. Чтобы закрыть окно внутри метода closeEvent()
, следует вызвать метод accept()
объекта события. Если необходимо предотвратить закрытие окна, то следует вызвать метод ignore()
Внутри метода closeEvent()
мы присваиваем атрибуту running значение
False
. Далее с по- мощью метода wait()
даем возможность потоку нормально завершить работу. В качестве параметра метод wait()
принимает количество миллисекунд, по истечении которых управ- ление будет передано следующей инструкции. Необходимо учитывать, что это максималь- ное время: если поток закончит работу раньше, то и метод закончит выполнение раньше.
Метод wait()
возвращает значение
True
, если поток успешно завершил работу, и
False
— в противном случае. Ожидание завершения потока занимает некоторое время, в течение которого окно будет по-прежнему видимым. Чтобы не вводить пользователя в заблуждение, в самом начале метода closeEvent()
мы скрываем окно вызовом метода hide()
Каждый поток может иметь собственный цикл обработки сигналов, который запускается с помощью метода exec_()
. В этом случае потоки могут обмениваться сигналами между собой. Чтобы прервать цикл, следует вызвать слот quit()
или метод exit([returnCode=0])
Рассмотрим обмен сигналами между потоками на примере (листинг 17.15).
Листинг 17.15. Обмен сигналами между потоками
# -*- coding: utf-8 -*- from PyQt5 import QtCore, QtWidgets class Thread1(QtCore.QThread): s1 = QtCore.pyqtSignal(int) def __init__(self, parent=None):
QtCore.QThread.__init__(self, parent) self.count = 0