ээдд. Прохоренок_Н_А__Дронов_В_А_Python_3_и_PyQt_5_Разработка_приложен. Николай Прохоренок Владимир Дронов
Скачать 7.92 Mb.
|
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 350 Часть II. Библиотека PyQt 5 def run(self): self.exec_() # Запускаем цикл обработки сигналов def on_start(self): self.count += 1 self.s1.emit(self.count) class Thread2(QtCore.QThread): s2 = QtCore.pyqtSignal(str) def __init__(self, parent=None): QtCore.QThread.__init__(self, parent) def run(self): self.exec_() # Запускаем цикл обработки сигналов def on_change(self, i): i += 10 self.s2.emit("%d" % i) 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.button = QtWidgets.QPushButton("Сгенерировать сигнал") self.vbox = QtWidgets.QVBoxLayout() self.vbox.addWidget(self.label) self.vbox.addWidget(self.button) self.setLayout(self.vbox) self.thread1 = Thread1() self.thread2 = Thread2() self.thread1.start() self.thread2.start() self.button.clicked.connect(self.thread1.on_start) self.thread1.s1.connect(self.thread2.on_change) self.thread2.s2.connect(self.on_thread2_s2) def on_thread2_s2(self, s): self.label.setText(s) if __name__ == "__main__": import sys app = QtWidgets.QApplication(sys.argv) window = MyWindow() window.setWindowTitle("Обмен сигналами между потоками") window.resize(300, 70) window.show() sys.exit(app.exec_()) В этом примере мы создали классы Thread1 , Thread2 и MyWindow . Первые два класса пред- ставляют собой потоки. Внутри них в методе run() вызывается метод exec_() , который за- пускает цикл обработки событий. В конструкторе класса MyWindow производится создание надписи, кнопки и экземпляров классов Thread1 и Thread2 . Далее выполняется запуск сразу двух потоков. Глава 17. Знакомство с PyQt 5 351 В следующей инструкции сигнал нажатия кнопки соединяется с методом on_start() перво- го потока. Внутри этого метода производится какая-либо операция (в нашем случае — уве- личение значения атрибута count ), а затем с помощью метода emit() генерируется сигнал s1 , и в параметре передается результат выполнения метода. Сигнал s1 соединен с методом on_change() второго потока. Внутри этого метода также производится какая-либо операция, а затем генерируется сигнал s2 , и передается результат выполнения метода. В свою очередь сигнал s2 соединен со слотом on_thread2_s2 объекта окна, который выводит в надпись зна- чение, переданное с этим сигналом. Таким образом, при нажатии кнопки Сгенерировать сигнал вначале будет вызван метод on_start() из класса Thread1 , затем метод on_change() из класса Thread2 , а потом метод on_thread2_s2 класса MyWindow , который выведет результат выполнения на экран. 17.9.3. Модуль queue: создание очереди заданий В предыдущем разделе мы рассмотрели возможность обмена сигналами между потоками. Теперь предположим, что запущены десять потоков, которые ожидают задания в бесконеч- ном цикле. Как передать задание одному потоку, а не всем сразу? И как определить, какому потоку передать задание? Можно, конечно, создать список в глобальном пространстве имен и добавлять задания в этот список, но в этом случае придется решать вопрос о совместном использовании одного ресурса сразу десятью потоками. Ведь если потоки будут получать задания одновременно, то одно задание могут получить сразу несколько потоков, и какому- либо потоку не хватит заданий, — возникнет исключительная ситуация. Попросту говоря, возникает ситуация, когда вы пытаетесь сесть на стул, а другой человек одновременно пы- тается вытащить его из-под вас. Думаете, что успеете сесть? Модуль queue , входящий в состав стандартной библиотеки Python, позволяет решить эту проблему. Модуль содержит несколько классов, которые реализуют разного рода потоко- безопасные очереди. Опишем эти классы: Queue — очередь (первым пришел, первым вышел). Формат конструктора: <Объект> = Queue([maxsize=0]) Пример: >>> import queue >>> q = queue.Queue() >>> q.put_nowait("elem1") >>> q.put_nowait("elem2") >>> q.get_nowait() 'elem1' >>> q.get_nowait() 'elem2' LifoQueue — стек (последним пришел, первым вышел). Формат конструктора: <Объект> = LifoQueue([maxsize=0]) Пример: >>> q = queue.LifoQueue() >>> q.put_nowait("elem1") >>> q.put_nowait("elem2") >>> q.get_nowait() 'elem2' 352 Часть II. Библиотека PyQt 5 >>> q.get_nowait() 'elem1' PriorityQueue — очередь с приоритетами. Элементы очереди должны быть кортежами, в которых первым элементом является число, означающее приоритет, а вторым — значе- ние элемента. При получении значения возвращается элемент с наивысшим приоритетом (наименьшим значением в первом параметре кортежа). Формат конструктора класса: <Объект> = PriorityQueue([maxsize=0]) Пример: >>> q = queue.PriorityQueue() >>> q.put_nowait((10, "elem1")) >>> q.put_nowait((3, "elem2")) >>> q.put_nowait((12, "elem3")) >>> q.get_nowait() (3, 'elem2') >>> q.get_nowait() (10, 'elem1') >>> q.get_nowait() (12, 'elem3') Параметр maxsize во всех трех случаях задает максимальное количество элементов, которое может содержать очередь. Если параметр равен нулю (значение по умолчанию) или отрица- тельному значению, то размер очереди не ограничен. Эти классы поддерживают следующие методы: put(<Элемент>[, block=True][, timeout=None]) — добавляет элемент в очередь. Если в параметре block указано значение True , поток будет ожидать возможности добавления элемента, — при этом в параметре timeout можно указать максимальное время ожи- дания в секундах. Если элемент не удалось добавить, возбуждается исключение queue.Full . В случае передачи параметром block значения False очередь не будет ожи- дать, когда появится возможность добавить в нее новый элемент, и в случае невозмож- ности сделать это возбудит исключение queue.Full немедленно; put_nowait(<Элемент>) — добавление элемента без ожидания. Эквивалентно: put(<Элемент>, False) get([block=True][, timeout=None]) — возвращает элемент, при этом удаляя его из оче- реди. Если в параметре block указано значение True , поток будет ожидать возможности извлечения элемента, — при этом в параметре timeout можно указать максимальное время ожидания в секундах. Если элемент не удалось получить, возбуждается исключе- ние queue.Empty . В случае передачи параметром block значения False очередь не будет ожидать, когда появится возможность извлечь из нее элемент, и в случае невозможности сделать это возбудит исключение queue.Empty немедленно; get_nowait() |