Десктопное приложение на Python: UI и сигналы

Kate

Administrator
Команда форума
Считается, что Python не лучший выбор для десктопных приложений. Однако, когда в 2016 году я собирался переходить от разработки сайтов к программному обеспечению, Google подсказал мне, что на Python можно создавать сложные современные приложения. Например blender3d, который написан на Python.

Скриншот программы Blender
Но люди, не по своей вине используют уродливые примеры графического интерфейса, которые выглядят слишком старыми, и не понравятся молодёжи. Я надеюсь изменить это мнение в своем туториале. Давайте начнём.

Мы будем использовать PyQt (произносится «Пай-Кьют‎»‎). Это фреймворк Qt, портированный с C++. Qt известен тем, что необходим C++ разработчикам. С помощью этого фреймворка сделаны blender3d, Tableau, Telegram, Anaconda Navigator, Ipython, Jupyter Notebook, VirtualBox, VLC и другие. Мы будем использовать его вместо удручающего Tkinter.

Требования​

  1. Вы должны знать основы Python
  2. Вы должны знать, как устанавливать пакеты и библиотеки с помощью pip.
  3. У вас должен быть установлен Python.

Установка​

Вам нужно установить только PyQt. Откройте терминал и введите команду:

>>> pip install PyQt5
Мы будем использовать PyQt версии 5.15. Дождитесь окончания установки, это займёт пару минут.

Hello, World!​

Создайте папку с проектом, мы назовём его helloApp. Откройте файл main.py, лучше сделать это vscode, и введите следующий код:

import sys

from PyQt5.QtGui import QGuiApplication
from PyQt5.QtQml import QQmlApplicationEngine

app = QGuiApplication(sys.argv)

engine = QQmlApplicationEngine()
engine.quit.connect(app.quit)
engine.load('./UI/main.qml')

sys.exit(app.exec())
Этот код вызывает QGuiApplication и QQmlApplicationEngine которые используют Qml вместо QtWidget в качестве UI слоя в Qt приложении. Затем, мы присоединяем UI функцию выхода к главной функции выхода приложения. Теперь они оба закроются одновременно, когда пользователь нажмёт выход. Затем, загружаем qml файл для Qml UI. Вызов app.exec(), запускает приложение, он находится внутри sys.exit, потому что возвращает код выхода, который передается в sys.exit.

Добавьте этот код в main.qml:

import QtQuick 2.15
import QtQuick.Controls 2.15

ApplicationWindow {
visible: true
width: 600
height: 500
title: "HelloApp"

Text {
anchors.centerIn: parent
text: "Hello, World"
font.pixelSize: 24
}

}
Этот код создает окно, делает его видимым, с указанными размерами и заголовком. Объект Text отображается в середине окна.

Интенсив «Напишите первую модель машинного обучения за 3 дня»
11–13 марта, Онлайн, Беcплатно
tproger.ru

События и курсы на tproger.ru
Теперь давайте запустим приложение:

>>> python main.py
Вы увидите такое окно:

Десктопное приложение на python

Обновление UI​

Давайте немного обновим UI, добавим фоновое изображение и время:

import QtQuick 2.15
import QtQuick.Controls 2.15

ApplicationWindow {
visible: true
width: 400
height: 600
title: "HelloApp"

Rectangle {
anchors.fill: parent

Image {
sourceSize.width: parent.width
sourceSize.height: parent.height
source: "./images/playas.jpg"
fillMode: Image.PreserveAspectCrop
}

Rectangle {
anchors.fill: parent
color: "transparent"
Text {
text: "16:38:33"
font.pixelSize: 24
color: "white"
}
}
}
}
Внутри типа ApplicationWindow находится содержимое окна, тип Rectangle заполняет пространство окна. Внутри него находится тип Image и другой прозрачный Rectangle который отобразится поверх изображения.

Если сейчас запустить приложение, то текст появится в левом верхнем углу. Но нам нужен левый нижний угол, поэтому используем отступы:

Text {
anchors {
bottom: parent.bottom
bottomMargin: 12
left: parent.left
leftMargin: 12
}
text: "16:38:33"
font.pixelSize: 24
...
}
После запуска вы увидите следующее:

Десктопное приложение на python

Показываем текущее время​

Модуль gmtime позволяет использовать структуру со временем, а strftime даёт возможность преобразовать её в строку. Импортируем их:

import sys
from time import strftime, gmtime
Теперь мы можем получить строку с текущим временем:

curr_time = strftime("%H:%M:%S", gmtime())
Строка "%H:%M:%S" означает, что мы получим время в 24 часовом формате, с часами минутами и секундами (подробнее о strtime).

Давайте создадим property в qml файле, для хранения времени. Мы назовём его currTime.

property string currTime: "00:00:00"
Теперь заменим текст нашей переменной:

Text {
...
text: currTime // used to be; text: "16:38:33"
font.pixelSize: 48
color: "white"
}
Теперь, передадим переменную curr_time из pyhton в qml:

engine.load('./UI/main.qml')
engine.rootObjects()[0].setProperty('currTime', curr_time)
Это один из способов передачи информации из Python в UI.

Запустите приложение и вы увидите текущее время.

Обновление времени​

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

Чтобы использовать сигналы нам нужен подкласс QObject. Назовём его Backend.

...
from PyQt5.QtCore import QObject, pyqtSignal

class Backend(QObject):
def __init__(self):
QObject.__init__(self)
...
У нас уже имеется свойства для строки со временем curr_time, теперь создадим свойство backend типа QtObject в файле main.qml.

property string currTime: "00:00:00"
property QtObject backend
Передадим данные из Python в qml:

engine.load('./UI/main.qml')
back_end = Backend()
engine.rootObjects()[0].setProperty('backend', back_end)
В qml файле один объект QtObject может получать несколько функций (называемых сигналами) из Python.

Создадим тип Connections и укажем backend в его target. Теперь внутри этого типа может быть столько функций, сколько нам необходимо получить в backend.

...
Rectangle {
anchors.fill: parent
Image {
...
}
...
}
Connections {
target: backend
}
...
Таким образом мы свяжем qml и сигналы из Python.

Мы используем потоки, для того чтобы обеспечить своевременное обновление UI. Создадим две функции, одну для управления потоками, а вторую для выполнения действий. Хорошая практика использовать в названии одной из функций _.

...
import threading
from time import sleep
...
class Backend(QObject):

def __init__(self):
QObject.__init__(self)
def bootUp(self):
t_thread = threading.Thread(target=self._bootUp)
t_thread.daemon = True
t_thread.start()
def _bootUp(self):
while True:
curr_time = strftime("%H:%M:%S", gmtime())
print(curr_time)
sleep(1)
...
Создадим pyqtsignal и назовём его updated, затем вызовем его из функции updater.

...
from PyQt5.QtCore import QObject, pyqtSignal
...
def __init__(self):
QObject.__init__(self)
updated = pyqtSignal(str, arguments=['updater'])
def updater(self, curr_time):
self.updated.emit(curr_time)
...
В этом коде updated имеет параметр arguments, который является списком, содержащим имя функции «updater». Qml будет получать данные из этой функции. В функции updater мы вызываем метод emit и передаём ему данные о времени.

Обновим qml, получив сигнал, с помощью обработчика, название которого состоит из «on» и имени сигнала:

target: backend
function onUpdated(msg) {
currTime = msg;
}
Теперь нам осталось вызвать функцию updater. В нашем небольшом приложении, использовать отдельную функцию для вызова сигнала не обязательно. Но это рекомендуется делать в больших программах. Изменим задержку на одну десятую секунды.

curr_time = strftime("%H:%M:%S", gmtime())
self.updater(curr_time)
sleep(0.1)
Функция bootUp должна быть вызвана сразу же после загрузки UI:

engine.rootObjects()[0].setProperty('backend', back_end)
back_end.bootUp()
sys.exit(app.exec())

Всё готово​

Теперь можно запустить программу. Время будет обновляться корректно. Для того, чтобы убрать рамку, вы можете добавить в qml файл следующую строку:

flags: Qt.FramelessWindowHint | Qt.Window
Так должен выглядеть файл main.py:

import sys
from time import strftime, gmtime
import threading
from time import sleep
from PyQt5.QtGui import QGuiApplication
from PyQt5.QtQml import QQmlApplicationEngine
from PyQt5.QtCore import QObject, pyqtSignal

class Backend(QObject):

def __init__(self):
QObject.__init__(self)
updated = pyqtSignal(str, arguments=['updater'])
def updater(self, curr_time):
self.updated.emit(curr_time)
def bootUp(self):
t_thread = threading.Thread(target=self._bootUp)
t_thread.daemon = True
t_thread.start()
def _bootUp(self):
while True:
curr_time = strftime("%H:%M:%S", gmtime())
self.updater(curr_time)
sleep(0.1)

app = QGuiApplication(sys.argv)
engine = QQmlApplicationEngine()
engine.quit.connect(app.quit)
engine.load('./UI/main.qml')
back_end = Backend()
engine.rootObjects()[0].setProperty('backend', back_end)
back_end.bootUp()
sys.exit(app.exec())
Вот содержимое файла main.qml:

import QtQuick 2.15
import QtQuick.Controls 2.15
ApplicationWindow {
visible: true
width: 360
height: 600
x: screen.desktopAvailableWidth - width - 12
y: screen.desktopAvailableHeight - height - 48
title: "HelloApp"
flags: Qt.FramelessWindowHint | Qt.Window
property string currTime: "00:00:00"
property QtObject backend
Rectangle {
anchors.fill: parent
Image {
sourceSize.width: parent.width
sourceSize.height: parent.height
source: "./images/playas.jpg"
fillMode: Image.PreserveAspectFit
}
Text {
anchors {
bottom: parent.bottom
bottomMargin: 12
left: parent.left
leftMargin: 12
}
text: currTime
font.pixelSize: 48
color: "white"
}
}

Connections {
target: backend
function onUpdated(msg) {
currTime = msg;
}
}
}

Сборка приложения​

Для сборки десктопного приложения на Python нам понадобится pyinstaller.

>>> pip install pyinstaller
Чтобы в сборку добавились все необходимые ресурсы, создадим файл spec:

>>> pyi-makespec main.py

Настройки файла spec​

Параметр datas можно использовать для того, чтобы включить файл в приложение. Это список кортежей, каждый из которых обязательно должен иметь target path(откуда брать файлы) и destination path(где будет находится приложение). destination path должен быть относительным. Чтобы расположить все ресурсы в одной папке с exe-файлами используйте пустую строку.

Измените параметр datas, на путь к вашей папке с UI:

a = Analysis(['main.py'],
...
datas=[('I:/path/to/helloApp/UI', 'UI')],
hiddenimports=[],
...
exe = EXE(pyz,
a.scripts,
[],
...
name='main',
debug=False,
...
console=False )
coll = COLLECT(exe,
...
upx_exclude=[],
name='main')
Параметр console установим в false, потому что у нас не консольное приложение.

Параметр name внутри вызова Exe, это имя исполняемого файла. name внутри вызова Collect, это имя папки в которой появится готовое приложение. Имена создаются на основании файла для которого мы создали spec — main.py.

Теперь можно запустить сборку:

>>> pyinstaller main.spec
В папке dist появится папка main. Для запуска программы достаточно запустить файл main.exe.

Так будет выглядеть содержимое папки с десктопным приложением на Python:

Содержимое папки с готовым приложением

О том, как использовать Qt Designer для создания UI приложений на Python читайте в нашей статье.

Источник статьи: https://tproger.ru/translations/desktopnoe-prilozhenie-na-python-ui-i-signaly/
 
Сверху