#статьи
-
0
Знакомимся с библиотекой Tkinter — пишем на Python кросс-платформенный калькулятор, который рассчитывает вес человека.
Иллюстрация: Merry Mary для Skillbox Media
Изучает Python, его библиотеки и занимается анализом данных. Любит путешествовать в горах.
Десктопные приложения пишут на разных языках программирования: C++, C#, C, Python и других. Начинающим разработчикам проще всего использовать Python и его библиотеки для работы над графическими интерфейсами.
Одна из таких библиотек — Tkinter. Она входит в стандартный пакет Python и позволяет создавать приложения для Windows, mac OS и Linux. Давайте разберёмся, как устроена эта библиотека, и напишем десктопный калькулятор, помогающий рассчитать вес человека.
GUI (Graphical User Interface) — это графический интерфейс пользователя, оболочка программы, с которой мы взаимодействуем с помощью клавиатуры и мыши. На современных операционных системах почти все программы работают с графическим интерфейсом, и мы каждый день сталкиваемся с GUI: читаем статьи в браузере, набираем текст в редакторе или играем в игры.
Противоположность графическому интерфейсу — командная строка, позволяющая управлять приложением с помощью текстовых команд. Такой интерфейс реализован в терминале macOS и командной строке Windows.
Для работы с GUI в Python есть четыре библиотеки:
- Tkinter;
- Kivy;
- Python QT;
- wxPython.
Мы выбрали Tkinter, потому что она не требует дополнительной установки и позволяет быстро создавать приложения с простым графическим интерфейсом.
Tkinter — это удобный интерфейс для работы со средствами Tk. Приложения, созданные на основе этой библиотеки, кросс-платформенные, то есть могут запускаться на разных операционных системах.
Схематично работу с Tkinter можно представить в виде четырёх шагов:
Что здесь происходит:
- Мы подключаем библиотеку Tkinter с помощью директивы import.
- Создаём главное окно приложения, в котором будут размещаться все графические элементы.
- Добавляем виджеты — визуальные элементы, выполняющие определённые действия.
- Создаём главный цикл событий — он включает в себя все события, происходящие при взаимодействии пользователя с интерфейсом.
Ключевые объекты в работе с Tkinter — виджеты. Это аналоги тегов из HTML, которые позволяют создавать интерактивные и неинтерактивные элементы, например надписи или кнопки. Всего их 18, но чаще всего используют следующие:
- Button — кнопки;
- Canvas — «холст», на котором рисуют графические фигуры;
- Entry — виджет для создания полей ввода;
- Label — контейнер для размещения текста или изображения;
- Menu — виджет для создания пунктов меню.
Понять работу с виджетами легче всего на практике. Но прежде чем к ней приступить, обсудим идею нашего первого десктопного приложения.
Мы напишем калькулятор индекса массы тела. ИМТ — это важный медицинский показатель, который позволяет оценить, есть ли у человека избыточный вес или ожирение. Он рассчитывается по следующей формуле:
Результаты расчётов оценивают с помощью специальной таблицы. У врачей она имеет много градаций, мы же воспользуемся упрощённой версией:
Писать код на Python лучше всего в специальной IDE, например в PyCharm или Visual Studio Code. Они подсвечивают синтаксис и предлагают продолжение кода — это сильно упрощает работу программиста. Весь код из этой статьи мы писали в Visual Studio Code.
Библиотека Tkinter предустановлена в Python. Поэтому её нужно только импортировать:
import tkinter as tk
Теперь мы можем использовать любые модули из этой библиотеки.
Прежде чем писать код, необходимо ответить на несколько вопросов:
- Какие данные мы хотим получить от пользователя и в каком виде?
- Какое событие будет запускать расчёт ИМТ: нажатие кнопки, получение приложением всех необходимых данных или что-то другое?
- Как будем показывать результат?
В нашем случае необходимо получить от пользователя вес и рост в виде целых чисел. При этом вес должен быть введён в килограммах, а рост — в сантиметрах. ИМТ будет рассчитываться по нажатии кнопки, а результат — выводиться во всплывающем окне в виде значения ИМТ и категории, к которой он относится.
Схематично графический интерфейс нашего калькулятора будет выглядеть так:
Теперь попробуем реализовать интерфейс и работу калькулятора с помощью Python и Tkinter.
После импорта библиотеки в Python загрузим её методы:
from tkinter import * from tkinter import messagebox
Первая строка позволяет нам загрузить все методы Tkinter и использовать их в коде без ссылки на их наименование. Второй строкой мы явно импортируем метод messagebox, который будем использовать для вывода всплывающего окна с результатом. Это удобно, так как метод потребуется нам несколько раз.
Теперь создадим окно нашего приложения. Для этого воспользуемся модулем Tk. Приложение назовём «Калькулятор индекса массы тела (ИМТ)»:
window = Tk() #Создаём окно приложения. window.title("Калькулятор индекса массы тела (ИМТ)") #Добавляем название приложения.
После запуска кода ничего не произойдёт. Это не ошибка. На самом деле код выполнился и окно закрылось. Необходимо явно указать, что окно приложения не должно закрываться до тех пор, пока пользователь сам не сделает этого. Для этого к коду добавим функцию window.mainloop (), указывающую на запуск цикла событий:
window.mainloop()
Запустив код, увидим экран приложения:
Мы не указали размер окна, поэтому название приложения не помещается в него полностью. Исправим это с помощью метода geometry:
window.geometry('400x300')
Теперь название приложения видно полностью:
В окне приложения необходимо разместить несколько элементов с нашего эскиза: два поля ввода информации с подписями и одну кнопку. Важно, чтобы поля не накладывались друг на друга и не уходили за пределы окна. В Tkinter для этого есть несколько методов:
- pack — используется, когда мы работаем с контейнерами для элементов. Позволяет позиционировать кнопки, надписи или другие элементы внутри контейнеров.
- place — позволяет позиционировать элементы, указывая точные координаты.
- grid — размещает элементы по ячейкам условной сетки, разделяющей окно приложения.
Мы воспользуемся комбинацией методов pack и grid. Для начала создадим виджет Frame для размещения надписей, полей ввода и кнопок. Подробное описание работы виджета есть в документации. Мы же используем только два свойства: padx и pady.
Обозначим отступы по вертикали и горизонтали в 10 пикселей для элементов, которые будут расположены внутри Frame:
frame = Frame( window, #Обязательный параметр, который указывает окно для размещения Frame. padx = 10, #Задаём отступ по горизонтали. pady = 10 #Задаём отступ по вертикали. ) frame.pack(expand=True) #Не забываем позиционировать виджет в окне. Здесь используется метод pack. С помощью свойства expand=True указываем, что Frame заполняет весь контейнер, созданный для него.
В окне приложения нам необходимо добавить три вида виджетов: поле для ввода информации (Entry), текстовые надписи (Label) и кнопку (Button).
Начнём с надписей. Воспользуемся виджетом Label:
height_lb = Label( frame, text="Введите свой рост (в см) " ) height_lb.grid(row=3, column=1)
Мы передаём виджету Label два параметра:
- frame — используем заготовку виджета Frame, в которой уже настроены отступы по вертикали и горизонтали.
- text — текст, который должен быть выведен на экран.
Для позиционирования виджета используем метод grid. Укажем, что текст должен располагаться в ячейке с координатами «3-я строка, 1-й столбец». Если запустим код, то увидим там единственный элемент:
Сейчас элемент расположен в центре окна, но он займёт правильное положение, когда мы напишем другие элементы.
Добавим вторую надпись о весе аналогичным образом, но при позиционировании в grid укажем следующую, четвёртую строку:
weight_lb = Label( frame, text="Введите свой вес (в кг) ", ) weight_lb.grid(row=4, column=1)
Запускаем код и смотрим на результат:
Теперь добавим поля для ввода пользовательской информации, используя виджет Entry:
height_tf = Entry( frame, #Используем нашу заготовку с настроенными отступами. ) height_tf.grid(row=3, column=2)
Для позиционирования мы также воспользовались методом grid. Обратите внимание, что наш элемент должен быть расположен напротив надписи «Введите свой рост (в см)». Поэтому мы используем ячейку в той же строке, но уже во втором столбце. Запустим код и посмотрим на результат:
Всё получилось. Остаётся по аналогии добавить поле ввода веса:
weight_tf = Entry( frame, ) weight_tf.grid(row=4, column=2, pady=5)
Посмотрим на результат:
Теперь добавим кнопку, которая будет запускать расчёт ИМТ. Сделаем это с помощью виджета Button:
cal_btn = Button( frame, #Заготовка с настроенными отступами. text='Рассчитать ИМТ', #Надпись на кнопке. ) cal_btn.grid(row=5, column=2) #Размещаем кнопку в ячейке, расположенной ниже, чем наши надписи, но во втором столбце, то есть под ячейками для ввода информации.
Посмотрим на результат:
Теперь в приложении есть все графические элементы. Остаётся лишь написать код, который будет получать информацию из виджетов Entry и рассчитывать индекс массы тела.
Напишем простую функцию и разберём её построчно:
def calculate_bmi(): #Объявляем функцию. kg = int(weight_tf.get()) #С помощью метода .get получаем из поля ввода с именем weight_tf значение веса, которое ввёл пользователь и конвертируем в целое число с помощью int(). m = int(height_tf.get())/100 #С помощью метода .get получаем из поля ввода с именем height_tf значение роста и конвертируем в целое число с помощью int(). Обязательно делим его на 100, так как пользователь вводит рост в сантиметрах, а в формуле для расчёта ИМТ используются метры. bmi = kg/(m*m)#Рассчитываем значение индекса массы тела. bmi = round(bmi, 1) #Округляем результат до одного знака после запятой.
Функция готова. Но теперь нам необходимо оценить полученный результат расчёта и вывести сообщение для пользователя.
Дополним нашу функцию calculate_bmi. Воспользуемся условным оператором if, чтобы учесть полученные значения ИМТ, и методом Tkinter messagebox для отображения сообщения во всплывающем окне:
if bmi < 18.5: messagebox.showinfo('bmi-pythonguides', f'ИМТ = {bmi} соответствует недостаточному весу') elif (bmi > 18.5) and (bmi < 24.9): messagebox.showinfo('bmi-pythonguides', f'ИМТ = {bmi} соответствует нормальному весу') elif (bmi > 24.9) and (bmi < 29.9): messagebox.showinfo('bmi-pythonguides', f'ИМТ = {bmi} соответствует избыточному весу') else: messagebox.showinfo('bmi-pythonguides', f'ИМТ = {bmi} соответствует ожирению')
Остаётся последний шаг — наша функция должна запускаться при нажатии на кнопку «Рассчитать ИМТ». Для этого добавим свойство command в виджет Button:
cal_btn = Button( frame, text='Рассчитать ИМТ', command=calculate_bmi #Позволяет запустить событие с функцией при нажатии на кнопку. ) cal_btn.grid(row=5, column=2)
Запустим код и посмотрим на результат:
Всё работает. Функция получает данные из полей ввода и рассчитывает индекс массы тела, показывая результат на экране.
from tkinter import *
from tkinter import messagebox
def calculate_bmi():
kg = int(weight_tf.get())
m = int(height_tf.get())/100
bmi = kg/(m*m)
bmi = round(bmi, 1)
if bmi < 18.5:
messagebox.showinfo('bmi-pythonguides', f'ИМТ = {bmi} соответствует недостаточному весу')
elif (bmi > 18.5) and (bmi < 24.9):
messagebox.showinfo('bmi-pythonguides', f'ИМТ = {bmi} соответствует нормальному весу')
elif (bmi > 24.9) and (bmi < 29.9):
messagebox.showinfo('bmi-pythonguides', f'ИМТ = {bmi} соответствует избыточному весу')
else:
messagebox.showinfo('bmi-pythonguides', f'ИМТ = {bmi} соответствует ожирению')
window = Tk()
window.title('Калькулятор индекса массы тела (ИМТ)')
window.geometry('400x300')
frame = Frame(
window,
padx=10,
pady=10
)
frame.pack(expand=True)
height_lb = Label(
frame,
text="Введите свой рост (в см) "
)
height_lb.grid(row=3, column=1)
weight_lb = Label(
frame,
text="Введите свой вес (в кг) ",
)
weight_lb.grid(row=4, column=1)
height_tf = Entry(
frame,
)
height_tf.grid(row=3, column=2, pady=5)
weight_tf = Entry(
frame,
)
weight_tf.grid(row=4, column=2, pady=5)
cal_btn = Button(
frame,
text='Рассчитать ИМТ',
command=calculate_bmi
)
cal_btn.grid(row=5, column=2)
window.mainloop()
Узнать о возможностях Tkinter и особенностях работы с виджетами можно в официальной документации. А если хотите найти больше реальных примеров для практики, советуем две книги:
- Python GUI Programming with Tkinter. Develop responsive and powerful GUI applications with Tkinter, Алан Мур.
- Tkinter GUI Programming by Example, Дэвид Лав.
Курс с трудоустройством: «Профессия Python-разработчик»
Узнать о курсе
Пройдите тест, узнайте какой профессии подходите
Работать самостоятельно и не зависеть от других
Работать в команде и рассчитывать на помощь коллег
Организовывать и контролировать процесс работы
Введение в разработку приложений для ПК на Python
Python — это мощный и гибкий язык программирования, который идеально подходит для разработки приложений для ПК. Благодаря своей простоте и обширной библиотеке, Python позволяет создавать как простые утилиты, так и сложные приложения с графическим интерфейсом. В этой статье мы рассмотрим основные шаги, необходимые для разработки приложений для ПК на Python, начиная с установки окружения и заканчивая сборкой и распространением готового продукта.
Python имеет множество преимуществ, которые делают его отличным выбором для разработки приложений. Во-первых, это кроссплатформенность: приложения, написанные на Python, могут работать на Windows, macOS и Linux без значительных изменений в коде. Во-вторых, Python обладает богатой экосистемой библиотек и фреймворков, которые упрощают разработку и позволяют сосредоточиться на логике приложения, а не на низкоуровневых деталях.
Кроме того, Python известен своей читаемостью и простотой синтаксиса, что делает его отличным выбором для новичков. Даже если вы только начинаете свой путь в программировании, вы сможете быстро освоить основы Python и приступить к созданию своих первых приложений.

Установка и настройка окружения
Перед тем как начать разработку, необходимо установить Python и настроить рабочее окружение. Для этого выполните следующие шаги:
-
Скачайте и установите Python: Перейдите на официальный сайт Python (https://www.python.org/) и скачайте последнюю версию. Установите Python, следуя инструкциям установщика. Убедитесь, что вы установили Python с опцией добавления в PATH, чтобы иметь возможность запускать его из командной строки.
-
Установите виртуальное окружение: Виртуальные окружения позволяют изолировать зависимости вашего проекта. Это особенно полезно, если вы работаете над несколькими проектами, которые требуют разных версий библиотек. Для создания виртуального окружения выполните команду:
Активируйте виртуальное окружение:
– На Windows:
– На macOS и Linux: -
Установите необходимые библиотеки: Для разработки графических интерфейсов на Python часто используется библиотека Tkinter, которая входит в стандартную библиотеку Python. Дополнительно можно установить PyInstaller для сборки приложения:
Настройка виртуального окружения также позволяет вам легко управлять зависимостями вашего проекта. Вы можете создать файл requirements.txt
, в котором будут перечислены все необходимые библиотеки, и затем установить их с помощью команды:
Создание простого графического интерфейса с Tkinter
Tkinter — это стандартная библиотека Python для создания графических интерфейсов. Она проста в использовании и позволяет быстро создавать окна, кнопки, текстовые поля и другие элементы интерфейса. Tkinter является частью стандартной библиотеки Python, поэтому вам не нужно устанавливать дополнительные пакеты для его использования.
Пример простого окна
Создадим простое окно с кнопкой и текстовым полем. Этот пример поможет вам понять основные принципы работы с Tkinter и создать базовый интерфейс для вашего приложения:
Этот код создает окно с заголовком «Простое приложение», меткой и кнопкой. При нажатии на кнопку текст метки изменяется на «Привет, мир!». Основные элементы интерфейса, такие как окна, метки и кнопки, создаются с помощью соответствующих классов Tkinter. Метод pack()
используется для размещения элементов в окне.
Дополнительные элементы интерфейса
Помимо меток и кнопок, Tkinter предоставляет множество других элементов интерфейса, таких как текстовые поля, флажки, радиокнопки и списки. Рассмотрим пример с добавлением текстового поля и флажка:
В этом примере добавлено текстовое поле для ввода имени пользователя и флажок. При нажатии на кнопку приложение выводит приветствие с именем пользователя и состоянием флажка.
Работа с событиями и обработчиками
Важной частью любого приложения с графическим интерфейсом является обработка событий, таких как нажатия кнопок, ввод текста и т.д. В Tkinter события обрабатываются с помощью функций-обработчиков, которые связываются с элементами интерфейса.
Пример обработки событий
Добавим обработку ввода текста в наше приложение. Это позволит нам реагировать на действия пользователя и изменять интерфейс в зависимости от введенных данных:
Теперь приложение запрашивает у пользователя имя и выводит приветствие с этим именем при нажатии на кнопку. Обработка событий в Tkinter осуществляется с помощью связывания функций-обработчиков с элементами интерфейса. В данном случае функция on_button_click
вызывается при нажатии на кнопку.
Обработка дополнительных событий
Помимо нажатий кнопок, Tkinter позволяет обрабатывать множество других событий, таких как движение мыши, нажатие клавиш и изменение состояния элементов интерфейса. Рассмотрим пример обработки события нажатия клавиши:
В этом примере приложение реагирует на нажатие любой клавиши и выводит символ нажатой клавиши в метке. Метод bind
используется для связывания события с функцией-обработчиком.
Сборка и распространение приложения
После завершения разработки приложения его необходимо собрать в исполняемый файл, чтобы пользователи могли запускать его без установки Python и зависимостей. Для этого можно использовать PyInstaller.
Сборка приложения с PyInstaller
- Установите PyInstaller (если еще не установили):
bash
pip install pyinstaller - Соберите приложение:
bash
pyinstaller --onefile --windowed your_script.py
Параметры--onefile
и--windowed
создают один исполняемый файл без консольного окна. PyInstaller анализирует ваш скрипт и все его зависимости, а затем упаковывает их в один исполняемый файл, который можно запускать на целевой системе без необходимости установки Python.
Настройка сборки
PyInstaller предоставляет множество опций для настройки процесса сборки. Вы можете указать дополнительные файлы, которые должны быть включены в сборку, настроить иконку приложения и многое другое. Рассмотрим пример настройки иконки и включения дополнительных файлов:
-
Создайте файл спецификации:
В этом примере используется параметр `—icon` для указания иконки приложения и `—add-data` для включения папки с данными.
-
Редактирование файла спецификации: После первого запуска PyInstaller создаст файл спецификации (
.spec
), который можно отредактировать для более тонкой настройки сборки. Откройте файл.spec
и внесите необходимые изменения.
Распространение приложения
Для распространения приложения можно использовать различные методы:
-
Распределение через файлообменники: Просто загрузите исполняемый файл на файлообменник и поделитесь ссылкой с пользователями. Это простой и быстрый способ распространения, но он может быть неудобен для пользователей, если приложение требует установки дополнительных файлов или настроек.
-
Создание установщика: Используйте инструменты, такие как Inno Setup (для Windows) или Platypus (для macOS), чтобы создать удобный установщик для вашего приложения. Установщик позволяет пользователям легко установить и настроить ваше приложение, а также может включать дополнительные файлы и зависимости.
-
Публикация на GitHub: Создайте репозиторий на GitHub и загрузите туда исходный код и исполняемый файл. Это позволит пользователям легко найти и скачать ваше приложение. Вы также можете использовать GitHub Releases для публикации новых версий приложения и предоставления пользователям удобного интерфейса для загрузки.
-
Использование платформ для распространения ПО: Рассмотрите возможность публикации вашего приложения на платформах, таких как Microsoft Store, Mac App Store или Linux Package Managers. Это может потребовать дополнительных усилий для соответствия требованиям платформы, но обеспечит более широкую аудиторию и удобство для пользователей.
Заключение
Разработка приложений для ПК на Python — это увлекательный и полезный процесс. С помощью Python и библиотек, таких как Tkinter и PyInstaller, можно быстро создавать и распространять приложения с графическим интерфейсом. Надеюсь, это руководство помогло вам сделать первые шаги в этой области. Удачи в разработке! 😉
Читайте также
Считается, что Python не лучший выбор для десктопных приложений. Однако, когда в 2016 году я собирался переходить от разработки сайтов к программному обеспечению, Google подсказал мне, что на Python можно создавать сложные современные приложения. Например blender3d, который написан на Python.
Мы будем использовать PyQt (произносится «Пай-Кьют»). Это фреймворк Qt, портированный с C++. Qt известен тем, что необходим C++ разработчикам. С помощью этого фреймворка сделаны blender3d, Tableau, Telegram, Anaconda Navigator, Ipython, Jupyter Notebook, VirtualBox, VLC и другие. Мы будем использовать его вместо удручающего Tkinter.
Требования
- Вы должны знать основы Python
- Вы должны знать, как устанавливать пакеты и библиотеки с помощью pip.
- У вас должен быть установлен Python.
Установка
Вам нужно установить только PyQt. Откройте терминал и введите команду:
Мы будем использовать 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 отображается в середине окна.
Теперь давайте запустим приложение:
Вы увидите такое окно:
Давайте немного обновим 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
...
}
После запуска вы увидите следующее:
Показываем текущее время
Модуль 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:
Настройки файла 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 читайте в нашей статье.
К старту курса по разработке на Python делимся детальным руководством по работе с современным PyQt. Чтобы читать было удобнее, мы объединили несколько статей в одну:
-
Первое приложение
-
Слоты и сигналы
-
Виджеты
За подробностями приглашаем под кат.
Простое приложение Hello World! на Python и Qt6
PyQt — это библиотека Python для создания приложений с графическим интерфейсом с помощью инструментария Qt. Созданная в Riverbank Computing, PyQt является свободным ПО (по лицензии GPL) и разрабатывается с 1999 года. Последняя версия PyQt6 — на основе Qt 6 — выпущена в 2021 году, и библиотека продолжает обновляться. Это руководство можно также использовать для PySide2, PySide6 и PyQt5.
Сегодня используются две основные версии: PyQt5 на основе Qt5 и PyQt6 на основе Qt6. Обе почти полностью совместимы, за исключением импорта и отсутствия поддержки некоторых продвинутых модулей из Qt6. В PyQt6 вносятся изменения в работу пространств имён и флагов, но ими легко управлять. В этом руководстве мы узнаем, как использовать PyQt6 для создания настольных приложений.
Сначала создадим несколько простых окон на рабочем столе, чтобы убедиться, что PyQt работает, и разберём базовые понятия. Затем кратко изучим цикл событий и то, как он связан с программированием графического интерфейса на Python. В заключение поговорим о QMainWindow с полезными элементами интерфейса, такими как панели инструментов и меню. Подробно я расскажу о них в следующих руководствах.
Создание приложения
Установка PyQt:
pip install pyqt6
# и на будущее
pip install pyqt-tools
Сначала создадим новый файл Python с любым названием (например app.py) и сохраним его. Исходный код приложения показан ниже. Введите его полностью и постарайтесь не ошибиться. Если что-то напутаете, Python укажет, что именно:
from PyQt6.QtWidgets import QApplication, QWidget
import sys # Только для доступа к аргументам командной строки
# Приложению нужен один (и только один) экземпляр QApplication.
# Передаём sys.argv, чтобы разрешить аргументы командной строки для приложения.
# Если не будете использовать аргументы командной строки, QApplication([]) тоже работает
app = QApplication(sys.argv)
# Создаём виджет Qt — окно.
window = QWidget()
window.show() # Важно: окно по умолчанию скрыто.
# Запускаем цикл событий.
app.exec()
# Приложение не доберётся сюда, пока вы не выйдете и цикл
# событий не остановится.
Запускаем приложение из командной строки, как и любой скрипт Python:
python3 app.py
Выполнив его, мы увидим окно. В Qt автоматически создаётся окно с обычным оформлением, возможностью его перетаскивать и менять размер. То, что вы увидите, зависит от платформы, где этот пример выполняется. Вот как отображается это окно на Windows, macOS и Linux (Ubuntu):
Разбор кода
Пройдём код построчно, чтобы понять, что именно происходит. Сначала мы импортируем классы PyQt для приложения: здесь это обработчик приложения QApplication и базовый пустой виджет графического интерфейса QWidget (оба из модуля QtWidgets):
from PyQt6.QtWidgets import QApplication, QWidget
Основные модули для Qt: QtWidgets, QtGui и QtCore.
Возможен ещё from import *
, но этот вид импорта обычно не приветствуется в Python. Дальше создаём экземпляр QApplication и передаём sys.arg (список Python с аргументами командной строки, передаваемыми приложению):
app = QApplication(sys.argv)
Если не будете использовать аргументы командной строки для управления Qt, передайте пустой список:
app = QApplication([])
Затем создаём экземпляр QWidget, используя имя переменной window:
window = QWidget()
window.show()
В Qt все виджеты верхнего уровня — окна, то есть у них нет родительского элемента и они не вложены в другой виджет или макет. В принципе, окно можно создать, используя любой виджет.
Виджеты без родительского элемента по умолчанию невидимы. Поэтому после создания объекта window необходимо всегда вызывать функцию .show(), чтобы сделать его видимым. .show() можно удалить, но тогда, запустив приложение, вы не сможете выйти из него!
В окне находится пользовательский интерфейс приложения. У каждого приложения он как минимум один. Приложение (по умолчанию) завершает работу при закрытии последнего окна.
Наконец, вызываем app.exec(), чтобы запустить цикл события.
Что такое «цикл событий»?
Прежде чем вывести окно на экран, разберём ключевые понятия, касающиеся организации приложений в мире Qt. Если вам уже знакомы циклы событий, можете пропустить эту часть статьи.
Основной элемент всех приложений в Qt — класс QApplication. Для работы каждому приложению нужен один — и только один — объект QApplication, который содержит цикл событий приложения. Это основной цикл, управляющий всем взаимодействием пользователя с графическим интерфейсом:
При каждом взаимодействии с приложением — будь то нажатие клавиши, щелчок или движение мыши — генерируется событие, которое помещается в очередь событий. В цикле событий очередь проверяется на каждой итерации: если найдено ожидающее событие, оно вместе с управлением передаётся определённому обработчику этого события. Последний обрабатывает его, затем возвращает управление в цикл событий и ждёт новых событий. Для каждого приложения выполняется только один цикл событий.
Класс QApplication содержит цикл событий Qt (нужен один экземпляр QApplication). Приложение ждёт в цикле событий новое событие, которое будет сгенерировано при выполнении действия. Всегда выполняется только один цикл событий.
QMainWindow
Итак, в Qt любые виджеты могут быть окнами. Например, если заменить QtWidget на QPushButton. В этом примере получается окно с одной нажимаемой кнопкой:
import sys
from PyQt6.QtWidgets import QApplication, QPushButton
app = QApplication(sys.argv)
window = QPushButton("Push Me")
window.show()
app.exec()
Классно, но не очень полезно на самом деле: редко когда нужен пользовательский интерфейс, состоящий только из одного элемента управления. Зато возможность с помощью макетов вкладывать одни виджеты в другие позволяет создавать сложные пользовательские интерфейсы внутри пустого QWidget.
В Qt уже есть решение для окна — виджет QMainWindow, имеющий стандартные функции окна для использования в приложениях, который содержит панели инструментов, меню, строку состояния, закрепляемые виджеты и многое другое. Рассмотрим эти расширенные функции позже, а пока добавим в приложение простой, пустой QMainWindow:
import sys
from PyQt6.QtWidgets import QApplication, QMainWindow
app = QApplication(sys.argv)
window = QMainWindow()
window.show()
# Запускаем цикл событий.
app.exec()
Запускаем и видим главное окно. Точно такое же, как и раньше.
QMainWindow пока не очень интересный. Добавим контент. Чтобы сделать настраиваемое окно, лучше создать подкласс QMainWindow, а затем настроить окно в блоке __init__. Так окно станет независимым в плане поведения. Итак, добавляем подкласс QMainWindow — MainWindow:
import sys
from PyQt6.QtCore import QSize, Qt
from PyQt6.QtWidgets import QApplication, QMainWindow, QPushButton
# Подкласс QMainWindow для настройки главного окна приложения
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("My App")
button = QPushButton("Press Me!")
# Устанавливаем центральный виджет Window.
self.setCentralWidget(button)
app = QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec()
Для этого демо используем QPushButton. Основные виджеты Qt всегда импортируются из пространства имён QtWidgets, как и классы QMainWindow и QApplication. При использовании QMainWindow задействуем .setCentralWidget для размещения виджета (здесь виджет — QPushButton) в QMainWindow, по умолчанию он занимает всё окно. Как добавлять в окна несколько виджетов? Об этом поговорим рассмотрим в руководстве по макетам.
При создании подкласса из класса Qt, чтобы разрешить Qt настраивать объект, всегда нужно вызывать функцию super __init__.
В блоке __init__ сначала используем .setWindowTitle(), чтобы поменять заголовок главного окна. Затем добавляем первый виджет — QPushButton — в середину окна. Это один из основных виджетов Qt. При создании кнопки можно ввести текст, который будет на ней отображаться. Вызываем .setCentralWidget() в окне. Это специальная функция QMainWindow, которая позволяет установить виджет на середину окна.
Запускаем и снова видим окно, но на этот раз с виджетом QPushButton в центре. Нажатие кнопки ничего не даст — с этим мы разберёмся после:
Скоро мы подробно рассмотрим другие виджеты, но, если вам не терпится и хочется забежать вперёд, можете заглянуть в документацию QWidget. Попробуйте добавить различные виджеты в окно.
Изменение размеров окон и виджетов
Сейчас размер окна можно свободно поменять: щёлкните мышью на любой угол и перетаскивайте, меняя таким образом размер. Можно дать возможность пользователям самим менять размер приложений, а можно установить ограничения на минимальные или максимальные размеры или фиксированный размер окна.
В Qt размеры определяются с помощью объекта QSize. Он принимает параметры ширины и высоты. Например, так создаётся окно фиксированного размера 400 x 300 пикселей:
import sys
from PyQt6.QtCore import QSize, Qt
from PyQt6.QtWidgets import QApplication, QMainWindow, QPushButton
# Подкласс QMainWindow для настройки главного окна приложения
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("My App")
button = QPushButton("Press Me!")
self.setFixedSize(QSize(400, 300))
# Устанавливаем центральный виджет Window.
self.setCentralWidget(button)
app = QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec()
Запускаем и видим окно фиксированного размера. Поменять его размер не получится.
Элемент управления maximize отключён на Windows и Linux. На macOS можно развернуть приложение на весь экран, но размер центрального виджета не изменится.
Кроме .setFixedSize() можно также вызвать .setMinimumSize() и .setMaximumSize(), чтобы установить минимальный и максимальный размеры соответственно. Попробуйте сами! Эти методы регулирования размеров работают в любом виджете. Продолжить изучение Python вы сможете на наших курсах:
-
Курс Python-разработчик
-
Профессия Fullstack-разработчик на Python
-
Курс «Python для веб-разработки»
А ещё вы можете приобрести книгу автора этих уроков или продолжить чтение.
Слоты и сигналы
Ранее мы рассмотрели классы QApplication и QMainWindow, цикл событий и добавили в окно простой виджет. А теперь изучим механизмы Qt для взаимодействия виджетов и окон друг с другом. В статью внесены изменения, связанные с PyQt6.
Мы создали окно и добавили в него простой виджет push button, но кнопка пока бесполезна. Нужно связать действие нажатия кнопки с происходящим. В Qt это делается с помощью сигналов и слотов или событий.
Сигналы — это уведомления, отправляемые виджетами, когда что-то происходит. Этим «чем-то» может быть что угодно — нажатие кнопки, изменение текста в поле ввода или изменение текста в окне. Многие сигналы инициируются в ответ на действия пользователя, но не только: в сигналах могут отправляться данные с дополнительным контекстом произошедшего.
Можно также писать собственные сигналы, их мы рассмотрим позже.
Слоты в Qt — это приёмники сигналов. Слотом в приложении на Python можно сделать любую функцию (или метод), просто подключив к нему сигнал. Принимающая функция получает данные, отправляемые ей в сигнале. У многих виджетов Qt есть встроенные слоты, а значит, виджеты можно подключать друг к другу напрямую.
Рассмотрим основные сигналы Qt и их использование для подключения виджетов в приложениях. Сохраните эту заготовку приложения в файле app.py:
import sys
from PyQt6.QtWidgets import QApplication, QMainWindow, QPushButton
class MainWindow(QMainWindow):
def __init__(self):
super(MainWindow, self).__init__()
self.setWindowTitle("My App")
app = QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec()
Сигналы QPushButton
Сейчас у нас есть QMainWindow с центральным виджетом QPushButton. Подключим эту кнопку к пользовательскому методу Python. Создадим простой настраиваемый слот the_button_was_clicked, принимающий сигнал clicked от QPushButton:
import sys
from PyQt6.QtWidgets import QApplication, QMainWindow, QPushButton
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("My App")
button = QPushButton("Press Me!")
button.setCheckable(True)
button.clicked.connect(self.the_button_was_clicked)
# Устанавливаем центральный виджет Window.
self.setCentralWidget(button)
def the_button_was_clicked(self):
print("Clicked!")
app = QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec()
Запускаем. Если нажать на кнопку, в консоли появится текст Clicked! («Нажата!»):
Clicked!
Clicked!
Clicked!
Clicked!
Получение данных
В сигналах может отправляться дополнительная информация о произошедшем. И сигнал .clicked — не исключение: с его помощью сообщается о нажатом (или переключенном) состоянии кнопки. Для обычных кнопок это значение всегда False, поэтому первый слот проигнорировал эти данные. Включим возможность нажатия кнопки, чтобы увидеть этот эффект. Ниже добавляется второй слот и выводится состояние нажатия:
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("My App")
button = QPushButton("Press Me!")
button.setCheckable(True)
button.clicked.connect(self.the_button_was_clicked)
button.clicked.connect(self.the_button_was_toggled)
self.setCentralWidget(button)
def the_button_was_clicked(self):
print("Clicked!")
def the_button_was_toggled(self, checked):
print("Checked?", checked)
Запускаем! Если нажать на кнопку, она подсветится и станет checked («Нажатой»). Чтобы отключить её, нажимаем ещё раз. Найдите состояние нажатия в консоли:
Clicked!
Checked? True
Clicked!
Checked? False
Clicked!
Checked? True
Clicked!
Checked? False
Clicked!
Checked? True
К сигналу подключается сколько угодно слотов, в которых можно реагировать сразу на несколько версий сигналов.
Хранение данных
Текущее состояние виджета на Python часто хранят в переменной, что позволяет работать со значениями без доступа к исходному виджету. Причём для их хранения используются отдельные переменные или словарь. В следующем примере сохраняем значение кнопки checked («Нажата») в переменной button_is_checked в self:
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.button_is_checked = True
self.setWindowTitle("My App")
button = QPushButton("Press Me!")
button.setCheckable(True)
button.clicked.connect(self.the_button_was_toggled)
button.setChecked(self.button_is_checked)
self.setCentralWidget(button)
def the_button_was_toggled(self, checked):
self.button_is_checked = checked
print(self.button_is_checked)
Сначала устанавливаем переменной значение по умолчанию True, а затем используем это значение, чтобы установить исходное состояние виджета. Когда состояние виджета меняется, получаем сигнал и соответственно обновляем переменную.
Эта же схема применима к любым виджетам PyQt. Если в виджете нет сигнала, которым отправляется текущее состояние, нужно получить значение из виджета прямо в обработчике. Например, здесь мы проверяем состояние checked («Нажата») в нажатом обработчике:
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.button_is_checked = True
self.setWindowTitle("My App")
self.button = QPushButton("Press Me!")
self.button.setCheckable(True)
self.button.released.connect(self.the_button_was_released)
self.button.setChecked(self.button_is_checked)
self.setCentralWidget(self.button)
def the_button_was_released(self):
self.button_is_checked = self.button.isChecked()
print(self.button_is_checked)
Сохраним ссылку на кнопку в self, чтобы получить к ней доступ в слоте.
Сигнал released срабатывает, когда кнопка отпускается, при этом состояние нажатия не отправляется. Его получают из кнопки в обработчике, используя .isChecked().
Изменение интерфейса
Мы уже видели, как принимаются сигналы и выводятся на консоль результаты. Но что происходит с интерфейсом, когда нажимают на кнопку? Обновим метод слота, чтобы изменить кнопку, поменяв текст, отключив её и сделав её недоступной. И отключим пока состояние, допускающее нажатие:
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("My App")
self.button = QPushButton("Press Me!")
self.button.clicked.connect(self.the_button_was_clicked)
self.setCentralWidget(self.button)
def the_button_was_clicked(self):
self.button.setText("You already clicked me.")
self.button.setEnabled(False)
# Также меняем заголовок окна.
self.setWindowTitle("My Oneshot App")
Снова нужен доступ к кнопке в методе the_button_was_clicked, поэтому сохраняем ссылку на неё в self. Чтобы поменять текст кнопки, передаём str в .setText(). Чтобы отключить кнопку, вызываем .setEnabled() с аргументом False. И запускаем программу. Если нажать на кнопку, текст изменится и кнопка станет недоступной.
В методах слота можно не только менять кнопку, которая активирует сигнал, но и делать всё что угодно. Например, поменять заголовок окна, добавив в метод the_button_was_clicked эту строку:
self.setWindowTitle("A new window title")
Большинство виджетов, в том числе QMainWindow, имеют свои сигналы. В следующем, более сложном примере подключим сигнал .windowTitleChanged в QMainWindow к пользовательскому методу слота. А также сделаем для этого слота новый заголовок окна:
from PyQt6.QtWidgets import QApplication, QMainWindow, QPushButton
import sys
from random import choice
window_titles = [
'My App',
'My App',
'Still My App',
'Still My App',
'What on earth',
'What on earth',
'This is surprising',
'This is surprising',
'Something went wrong'
]
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.n_times_clicked = 0
self.setWindowTitle("My App")
self.button = QPushButton("Press Me!")
self.button.clicked.connect(self.the_button_was_clicked)
self.windowTitleChanged.connect(self.the_window_title_changed)
# Устанавливаем центральный виджет Window.
self.setCentralWidget(self.button)
def the_button_was_clicked(self):
print("Clicked.")
new_window_title = choice(window_titles)
print("Setting title: %s" % new_window_title)
self.setWindowTitle(new_window_title)
def the_window_title_changed(self, window_title):
print("Window title changed: %s" % window_title)
if window_title == 'Something went wrong':
self.button.setDisabled(True)
app = QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec()
Сначала создаём список заголовков окна и выбираем один из них наугад, используя встроенную функцию Python random.choice(). Подключаем пользовательский метод слота the_window_title_changed к сигналу окна .windowTitleChanged.
При нажатии на кнопку заголовок окна случайным образом изменится. Если новый заголовок окна изменится на Something went wrong («Что-то пошло не так»), кнопка отключится.
Запускаем! Нажимайте на кнопку, пока заголовок не изменится на Something went wrong. В этом примере стоит обратить внимание вот на что:
-
Сигнал windowTitleChanged при установке заголовка окна выдаётся не всегда. Он срабатывает, только если новый заголовок отличается от предыдущего: если один и тот же заголовок устанавливается несколько раз, сигнал срабатывает только в первый раз. Чтобы избежать неожиданностей, важно перепроверять условия срабатывания сигналов при их использовании в приложении.
-
С помощью сигналов создаются цепочки. Одно событие — нажатие кнопки — может привести к тому, что поочерёдно произойдут другие. Эти последующие эффекты отделены от того, что их вызвало. Они возникают согласно простым правилам. И это отделение эффектов от их триггеров — один из ключевых принципов, которые учитываются при создании приложений с графическим интерфейсом. Возвращаться к этому будем на протяжении всего курса.
Мы рассмотрели сигналы и слоты, показали простые сигналы и их использование для передачи данных и состояния в приложении. Теперь переходим к виджетам Qt, которые будут использоваться в приложениях вместе с сигналами.
Подключение виджетов друг к другу напрямую
Мы уже видели примеры подключения сигналов виджетов к методам Python. Когда сигнал из виджета срабатывает, вызывается метод Python, из сигнала он получает данные. Но для обработки сигналов не всегда нужна функция Python — можно подключать виджеты друг к другу напрямую.
Добавим в окно виджеты QLineEdit и QLabel. В __init__ для окна и подключим сигнал редактирования строки .textChanged к методу .setText в QLabel. Когда в QLineEdit меняется текст, он сразу будет поступать в QLabel (в метод .setText):
from PyQt6.QtWidgets import QApplication, QMainWindow, QLabel, QLineEdit, QVBoxLayout, QWidget
import sys
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("My App")
self.label = QLabel()
self.input = QLineEdit()
self.input.textChanged.connect(self.label.setText)
layout = QVBoxLayout()
layout.addWidget(self.input)
layout.addWidget(self.label)
container = QWidget()
container.setLayout(layout)
# Устанавливаем центральный виджет Window.
self.setCentralWidget(container)
app = QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec()
Внимание: чтобы подключить входные данные к метке, нужно определить и эти данные, и метку. В этом коде в макет добавляются два виджета и устанавливаются в окне. Подробно рассмотрим макеты позже, а пока не обращайте на них внимания.
Запускаем программу:
Введите текст в верхнем поле — он сразу появится в виде метки.
У большинства виджетов Qt есть доступные слоты, к которым подключается любой сигнал, возврощающий тот же тип, что он принимает. В документации по виджетам, в разделе Public Slots («Общедоступные слоты»), имеются слоты для каждого виджета. Посмотрите документацию для QLabel.
События
Любое взаимодействие пользователя с приложением Qt — это событие. Есть много типов событий, каждое из которых — это отдельный тип взаимодействия. В Qt события представлены объектами событий, в которые упакована информация о произошедшем. События передаются определённым обработчикам событий в виджете, где произошло взаимодействие.
Определяя пользовательские или расширенные обработчики событий, можно менять способ реагирования виджетов на них. Обработчики событий определяются так же, как и любой другой метод, но название обработчика зависит от типа обрабатываемого события.
QMouseEvent — одно из основных событий, получаемых виджетами. События QMouseEvent создаются для каждого отдельного нажатия кнопки мыши и её перемещения в виджете. Вот обработчики событий мыши:
Обработчик |
Событие |
---|---|
|
Мышь переместилась |
|
Кнопка мыши нажата |
|
Кнопка мыши отпущена |
|
Обнаружен двойной клик |
Например, нажатие на виджет приведёт к отправке QMouseEvent в обработчик событий .mousePressEvent в этом виджете.
События можно перехватывать, создав подкласс и переопределив метод обработчика в этом классе. Их можно фильтровать, менять или игнорировать, передавая обычному обработчику путём вызова функции суперкласса методом super(). Они добавляются в класс главного окна следующим образом (в каждом случае в e будет получено входящее событие):
import sys
from PyQt6.QtCore import Qt
from PyQt6.QtWidgets import QApplication, QLabel, QMainWindow, QTextEdit
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.label = QLabel("Click in this window")
self.setCentralWidget(self.label)
def mouseMoveEvent(self, e):
self.label.setText("mouseMoveEvent")
def mousePressEvent(self, e):
self.label.setText("mousePressEvent")
def mouseReleaseEvent(self, e):
self.label.setText("mouseReleaseEvent")
def mouseDoubleClickEvent(self, e):
self.label.setText("mouseDoubleClickEvent")
app = QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec()
Запускаем! В окне переместите мышь и нажмите на кнопку, затем дважды нажмите на кнопку и посмотрите, какие появляются события.
События перемещения мыши регистрируются только при нажатой кнопке. Чтобы это изменить, вызовите в окне self.setMouseTracking(True). События press («Нажатие кнопки»), click («Клика») и double-click («Двойного клика») срабатывают при нажатии кнопки. Событие release («Отпускание») срабатывает, только когда кнопка отпускается. Клик пользователя регистрируется обычно при нажатии кнопки мыши и её отпускании.
Внутри обработчиков события есть доступ к объекту этого события. Он содержит информацию о событии и используется, чтобы реагировать по-разному в зависимости от того, что произошло. Рассмотрим объекты событий управления мышью.
События управления мышью
В Qt все события управления мышью отслеживаются с помощью объекта QMouseEvent. При этом информация о событии считывается из следующих методов событий:
Метод |
Возвращает |
---|---|
|
Конкретную кнопку, вызвавшую данное событие |
|
Состояние всех кнопок мыши (флаги OR) |
|
Относительную позицию виджета в виде целого |
Эти методы используются в обработчике событий, чтобы на разные события реагировать по-разному или полностью их игнорировать. Через методы позиционирования в виде объектов QPoint предоставляется глобальная и локальная (касающаяся виджета) информация о местоположении. Сведения о кнопках поступают с использованием типов кнопок мыши из пространства имён Qt. Например, в этом коде показаны разные реакции на нажатие левой, правой или средней кнопки мыши в окне:
def mousePressEvent(self, e):
if e.button() == Qt.LeftButton:
# здесь обрабатываем нажатие левой кнопки
self.label.setText("mousePressEvent LEFT")
elif e.button() == Qt.MiddleButton:
# здесь обрабатываем нажатие средней кнопки.
self.label.setText("mousePressEvent MIDDLE")
elif e.button() == Qt.RightButton:
# здесь обрабатываем нажатие правой кнопки.
self.label.setText("mousePressEvent RIGHT")
def mouseReleaseEvent(self, e):
if e.button() == Qt.LeftButton:
self.label.setText("mouseReleaseEvent LEFT")
elif e.button() == Qt.MiddleButton:
self.label.setText("mouseReleaseEvent MIDDLE")
elif e.button() == Qt.RightButton:
self.label.setText("mouseReleaseEvent RIGHT")
def mouseDoubleClickEvent(self, e):
if e.button() == Qt.LeftButton:
self.label.setText("mouseDoubleClickEvent LEFT")
elif e.button() == Qt.MiddleButton:
self.label.setText("mouseDoubleClickEvent MIDDLE")
elif e.button() == Qt.RightButton:
self.label.setText("mouseDoubleClickEvent RIGHT")
Идентификаторы кнопок определяются в пространстве имён Qt:
Код |
Бинарное значение |
Описание |
---|---|---|
|
0 ( |
Кнопка не нажата, или событие не связано с нажатием кнопки |
|
1 ( |
Левая кнопка нажата |
|
2 ( |
Правая кнопка нажата |
|
4 ( |
Средняя кнопка [обычно это колёсико мыши] нажата |
В мышках для правшей положения левой и правой кнопок меняются местами, то есть при нажатии правой кнопки вернётся Qt.LeftButton. Иными словами, учитывать ориентацию мыши не нужно.
Контекстные меню
Контекстные меню — это небольшие контекстно-зависимые меню, которые обычно появляются в окне при нажатии правой кнопкои мыши. В Qt у виджетов есть определённое событие для активации таких меню.
В примере ниже мы будем перехватывать событие .contextMenuEvent в QMainWindow. Это событие запускается всякий раз перед самым появлением контекстного меню, а передаётся событие с одним значением типа QContextMenuEvent.
Чтобы перехватить событие, просто переопределяем метод объекта с помощью нового метода с тем же именем. В нашем случае создаём метод в подклассе MainWindow с именем contextMenuEvent, и он будет получать все события этого типа:
import sys
from PyQt6.QtCore import Qt
from PyQt6.QtGui import QAction
from PyQt6.QtWidgets import QApplication, QLabel, QMainWindow, QMenu
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
def contextMenuEvent(self, e):
context = QMenu(self)
context.addAction(QAction("test 1", self))
context.addAction(QAction("test 2", self))
context.addAction(QAction("test 3", self))
context.exec(e.globalPos())
app = QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec()
Если запустить этот код и нажать правой кнопкой в окне, появится контекстное меню. В действиях меню можно настроить слоты .triggered как обычные (и повторно использовать действия, определённые для меню и панелей инструментов).
При передаче исходного положения в функцию exec оно должно соответствовать родительскому элементу, переданному при определении. В нашем случае в качестве родительского элемента передаётся self, поэтому используем глобальное положение. Для полноты картины нужно сказать, что создавать контекстные меню можно и при помощи сигналов:
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.show()
self.setContextMenuPolicy(Qt.CustomContextMenu)
self.customContextMenuRequested.connect(self.on_context_menu)
def on_context_menu(self, pos):
context = QMenu(self)
context.addAction(QAction("test 1", self))
context.addAction(QAction("test 2", self))
context.addAction(QAction("test 3", self))
context.exec(self.mapToGlobal(pos))
Что выбрать — решать только вам.
Иерархия событий
В PyQt каждый виджет — это часть двух различных иерархий: иерархии объектов в Python и иерархии макета в Qt. Реакция на события или их игнорирование влияет на поведение пользовательского интерфейса.
Перенаправление наследования Python
Часто бывает нужно перехватить событие, что-то с ним сделать, запустив при этом поведение обработки событий по умолчанию. Если объект унаследован от стандартного виджета, у него наверняка будет поведение по умолчанию. Запустить его можно, методом super() вызвав реализацию из суперкласса. Будет вызван именно суперкласс в Python, а не .parent() из PyQt:
def mousePressEvent(self, event):
print("Mouse pressed!")
super(self, MainWindow).contextMenuEvent(event)
Интерфейс ведёт себя как раньше, при этом мы добавили новое поведение, которое не вмешивается в прежнее.
Передача вверх по иерархии макета
Когда в приложение добавляется виджет, он также получает из макета другой родительский элемент. Чтобы найти родительский элемент виджета, надо вызвать функцию .parent(). Иногда эти родительские элементы указываются вручную (и часто автоматически), например для QMenu или QDialog. Когда в главное окно добавляется виджет, оно становится родительским элементом виджета.
Когда события создаются для взаимодействия пользователя с пользовательским интерфейсом, они передаются в его самый верхний виджет. Если в окне есть кнопка и вы нажмёте её, она получит событие первой.
Если первый виджет не может обработать событие, оно перейдёт к следующему по очереди родительскому виджету. Эта передача вверх по иерархии вложенных виджетов продолжится, пока событие не будет обработано или не достигнет главного окна. В обработчиках событий событие помечают как обработанное через вызов .accept():
class CustomButton(QPushButton)
def mousePressEvent(self, e):
e.accept()
Вызвав в объекте события .ignore(), помечают его как необработанное. В этом случае событие будет передаваться по иерархии вверх:
class CustomButton(QPushButton)
def event(self, e):
e.ignore()
Чтобы виджет пропускал события, вы можете спокойно игнорировать те, на которые каким-то образом реагировали. Аналогично вы можете отреагировать на одни события, заглушая другие. А продолжить изучение Python вы сможете на наших курсах:
-
Курс Python-разработчик
-
Профессия Fullstack-разработчик на Python
-
Курс «Python для веб-разработки»
Напомним также о книге автора этих уроков.
Виджеты
В большинстве пользовательских интерфейсов и в Qt «виджет» — это компонент, с которым взаимодействует пользователь. Пользовательские интерфейсы состоят из нескольких виджетов, расположенных внутри окна. В Qt большой выбор виджетов и можно создать собственные виджеты.
Краткое демо
Посмотрим на самые распространённые виджеты PyQt. Они создаются и добавляются в макет окна с помощью этого кода (работу макетов в Qt рассмотрим в следующем руководстве):
import sys
from PyQt6.QtCore import Qt
from PyQt6.QtWidgets import (
QApplication,
QCheckBox,
QComboBox,
QDateEdit,
QDateTimeEdit,
QDial,
QDoubleSpinBox,
QFontComboBox,
QLabel,
QLCDNumber,
QLineEdit,
QMainWindow,
QProgressBar,
QPushButton,
QRadioButton,
QSlider,
QSpinBox,
QTimeEdit,
QVBoxLayout,
QWidget,
)
# Подкласс QMainWindow для настройки основного окна приложения
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("Widgets App")
layout = QVBoxLayout()
widgets = [
QCheckBox,
QComboBox,
QDateEdit,
QDateTimeEdit,
QDial,
QDoubleSpinBox,
QFontComboBox,
QLCDNumber,
QLabel,
QLineEdit,
QProgressBar,
QPushButton,
QRadioButton,
QSlider,
QSpinBox,
QTimeEdit,
]
for w in widgets:
layout.addWidget(w())
widget = QWidget()
widget.setLayout(layout)
# Устанавливаем центральный виджет окна. Виджет будет расширяться по умолчанию,
# заполняя всё пространство окна.
self.setCentralWidget(widget)
app = QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec()
Запускаем! Появится окно со всеми созданными виджетами:
Посмотрим внимательно на все эти виджеты:
Класс виджета |
Описание виджета |
---|---|
|
Чекбокс |
|
Окно выпадающего списка |
|
Для редактирования даты и времени |
|
Для редактирования даты и времени |
|
Поворотный циферблат |
|
Спиннер для чисел с плавающей точкой |
|
Список шрифтов |
|
Довольно неприятный дисплей LCD |
|
Просто метка, не интерактивная |
|
Поле ввода со строкой |
|
Индикатор выполнения |
|
Кнопка |
|
Переключаемый набор, в котором активен только один элемент |
|
Слайдер |
|
Спиннер для целых чисел |
|
Поле редактирования времени |
Их гораздо больше, но они не поместятся в статью. Полный список есть в документации Qt.
Разберём подробно самые используемые виджеты и поэкспериментируем с ними в простом приложении. Сохраните следующий код в файле app.py и, запустив его, убедитесь, что он работает:
import sys
from PyQt6.QtWidgets import (
QMainWindow, QApplication,
QLabel, QCheckBox, QComboBox, QListBox, QLineEdit,
QLineEdit, QSpinBox, QDoubleSpinBox, QSlider
)
from PyQt6.QtCore import Qt
class MainWindow(QMainWindow):
def __init__(self):
super(MainWindow, self).__init__()
self.setWindowTitle("My App")
app = QApplication(sys.argv)
w = MainWindow()
w.show()
app.exec()
Выше мы импортировали несколько виджетов Qt. Разберём каждый по очереди. Добавим их в приложение и посмотрим, как они себя ведут.
QLabel
Начнём с QLabel, одного из простейших виджетов в Qt. Он состоит из строчки текста и устанавливается в приложении. Текст задаётся аргументом QLabel:
widget = QLabel("Hello")
Или с помощью метода .setText():
widget = QLabel("1") # Создана метка с текстом 1.
widget.setText("2") # Создана метка с текстом 2.
Параметры шрифта, например его размер или выравнивание в виджете, настраиваются так:
class MainWindow(QMainWindow):
def __init__(self):
super(MainWindow, self).__init__()
self.setWindowTitle("My App")
widget = QLabel("Hello")
font = widget.font()
font.setPointSize(30)
widget.setFont(font)
widget.setAlignment(Qt.AlignmentFlag.AlignHCenter | Qt.AlignmentFlag.AlignVCenter)
self.setCentralWidget(widget)
Рекомендация по шрифту: чтобы изменить свойства шрифта виджета, лучше получить текущий шрифт, обновить его и применить снова. Так начертание шрифта будет соответствовать ограничениям рабочего стола. Выравнивание указывается с помощью флага из пространства имён Qt. Вот флаги для горизонтального выравнивания:
Флаг PyQt6 (полный код) |
Поведение |
---|---|
|
Выравнивает по левому краю |
|
Выравнивает по правому краю |
|
Центрирует по горизонтали в доступном пространстве |
|
Выравнивает текст в доступном пространстве |
Флаги используются вместе с помощью каналов (|), но флаги вертикального и горизонтального выравнивания не применяются одновременно:
Флаг PyQt6 (полный код) |
Поведение |
---|---|
|
Выравнивается по верху |
|
Выравнивается по низу |
|
Центрирует вертикально в доступном пространстве |
align_top_left = Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignTop
При этом для совместного применения двух флагов (не A & B) используется канал | с операцией логического «ИЛИ». Эти флаги — неперекрывающиеся битовые маски. Например, у Qt.AlignmentFlag.AlignLeft шестнадцатеричное значение 0x0001, а у Qt.AlignmentFlag.AlignBottom — 0x0040. С помощью операции логического «ИЛИ» получаем значение «внизу слева» — 0x0041. Этот принцип применим и ко всем остальным парам флагов Qt. Если вам что-то здесь непонятно, смело пропускайте эту часть и переходите к следующей. Только не забывайте использовать |. Наконец, есть флаг, с помощью которого выполняется выравнивание по центру в обоих направлениях одновременно:
Флаг PyQt6 |
Поведение |
---|---|
|
Центрирует горизонтально и вертикально. |
Как ни странно, QLabel используется и для показа изображения с помощью .setPixmap(). Этот метод принимает QPixmap, создаваемый передачей имени файла изображения в QPixmap. Среди примеров из этой книги есть файл otje.jpg, который отображается в окне так:
widget.setPixmap(QPixmap('otje.jpg'))
Какая мордочка! По умолчанию изображение масштабируется, сохраняя соотношение ширины и высоты. Если нужно растянуть и подогнать его по размерам окна, установите .setScaledContents(True) в QLabel:
widget.setScaledContents(True)
QCheckBox
А этот виджет, как следует из названия, предоставляет пользователю чекбокс с флажками. Как и во всех виджетах Qt, здесь есть настраиваемые параметры, изменяющие его поведение:
class MainWindow(QMainWindow):
def __init__(self):
super(MainWindow, self).__init__()
self.setWindowTitle("My App")
widget = QCheckBox()
widget.setCheckState(Qt.CheckState.Checked)
# Включение трёх состояний: widget.setCheckState(Qt.PartiallyChecked)
# Или: widget.setTriState(True)
widget.stateChanged.connect(self.show_state)
self.setCentralWidget(widget)
def show_state(self, s):
print(s == Qt.CheckState.Checked)
print(s)
Состояние чекбокса устанавливается программно с помощью .setChecked или .setCheckState. Первый принимает True или False, то есть чекбокс с галочкой или без неё соответственно. В случае с .setCheckState с помощью флага пространства имён Qt также указывается конкретное состояние чекбокса:
Флаг PyQt6 (Полный код) |
Состояние |
---|---|
|
Элемент не отмечен |
|
Элемент отмечен частично |
|
Элемент отмечен |
Чекбокс, поддерживающий состояние частичной отмеченности (Qt.CheckState.PartiallyChecked) галочками, обычно называют «имеющим третье состояние», т. е. он и отмечен, и не отмечен. Чекбокс в этом состоянии часто отображается в виде серого флажка и используется в иерархических структурах чекбоксов, где дочерние чекбоксы связаны с родительскими.
Установив значение Qt.CheckState.PartiallyChecked, вы переведёте чекбокс в третье состояние. То же самое, но без перевода текущего состояния в состояние частичной отмеченности галочками, делается с помощью .setTriState(True).
При запуске скрипта номер текущего состояния отображается в виде значения целочисленного типа int. Причём состоянию, отмеченному галочками, соответствует 2, не отмеченному — 0, а состоянию частичной отмеченности — 1. Запоминать эти значения не нужно: К примеру, переменная пространства имён Qt.Checked равна 2. Это значение соответствующих флагов состояния. То есть вы можете проверить состояние с помощью state == Qt.Checked.
QComboBox
QComboBox — это выпадающий список, закрытый по умолчанию, с кнопкой, чтобы открыть его. Можно выбрать один элемент из списка, при этом выбранный в данный момент элемент отображается в виджете в виде метки. Поле со списком подходит для выбора варианта из длинного списка вариантов. Такое поле используется при выборе начертания или размера шрифта в текстовых редакторах. В Qt для выбора шрифта есть специальное поле со списком шрифтов — QFontComboBox.
В QComboBox можно добавлять элементы, передавая список строк в .addItems(). Элементы добавляются в порядке расположения соответствующих элементам строк кода.
class MainWindow(QMainWindow):
def __init__(self):
super(MainWindow, self).__init__()
self.setWindowTitle("My App")
widget = QComboBox()
widget.addItems(["One", "Two", "Three"])
# Отправляет текущий индекс (позицию) выбранного элемента.
widget.currentIndexChanged.connect( self.index_changed )
# Есть альтернативный сигнал отправки текста.
widget.textChanged.connect( self.text_changed )
self.setCentralWidget(widget)
def index_changed(self, i): # i — это int
print(i)
def text_changed(self, s): # s — это str
print(s)
Сигнал .currentIndexChanged срабатывает при обновлении выбранного в данный момент элемента, индекс которого в списке передаётся по умолчанию. Кроме того, есть сигнал .currentTextChanged, при срабатывании которого вместо индекса предоставляется метка выбранного в данный момент элемента. Это часто оказывается полезнее.
Также QComboBox можно редактировать, вводя значения, которых в данный момент нет в списке, — вставлять их или просто использовать как значения. Поле делается редактируемым так:
widget.setEditable(True)
Чтобы определить, как обрабатываются вставляемые значения, устанавливается флаг. Флаги хранятся в самом классе QComboBox. Вот их список:
Флаг PyQt6 (полный код) |
Поведение |
---|---|
|
Не вставлять |
|
Вставить как первый элемент |
|
Заменить текущий выбранный элемент |
|
Вставить после последнего элемента |
|
Вставить после текущего элемента |
|
Вставить перед текущим элементом |
|
Вставить в алфавитном порядке |
Чтобы использовать их, применяется флаг:
widget.setInsertPolicy(QComboBox.InsertPolicy.InsertAlphabetically)
Кроме того, можно ограничить количество элементов поля, например при помощи .setMaxCount:
widget.setMaxCount(10)
Подробнее о QComboBox рассказывается здесь.
QListWidget
Этот виджет похож на QComboBox, но варианты здесь представлены в виде прокручиваемого списка элементов и возможен выбор нескольких элементов одновременно. В QListWidget есть сигнал currentItemChanged для отправки QListItem (элемента виджета списка) и сигнал currentTextChanged для отправки текста текущего элемента:
class MainWindow(QMainWindow):
def __init__(self):
super(MainWindow, self).__init__()
self.setWindowTitle("My App")
widget = QListWidget()
widget.addItems(["One", "Two", "Three"])
widget.currentItemChanged.connect(self.index_changed)
widget.currentTextChanged.connect(self.text_changed)
self.setCentralWidget(widget)
def index_changed(self, i); # i — не индекс, а сам QList
print(i.text())
def text_changed(self, s): # s — это строка
print(s)
QLineEdit
Виджет QLineEdit — это простое однострочное текстовое поле для редактирования вводимых пользователями данных. Он используется для полей, где нет ограничений на входные данные. Например, при вводе адреса электронной почты или имени компьютера:
class MainWindow(QMainWindow):
def __init__(self):
super(MainWindow, self).__init__()
self.setWindowTitle("My App")
widget = QLineEdit()
widget.setMaxLength(10)
widget.setPlaceholderText("Enter your text")
#widget.setReadOnly(True) # раскомментируйте, чтобы сделать доступным только для чтения
widget.returnPressed.connect(self.return_pressed)
widget.selectionChanged.connect(self.selection_changed)
widget.textChanged.connect(self.text_changed)
widget.textEdited.connect(self.text_edited)
self.setCentralWidget(widget)
def return_pressed(self):
print("Return pressed!")
self.centralWidget().setText("BOOM!")
def selection_changed(self):
print("Selection changed")
print(self.centralWidget().selectedText())
def text_changed(self, s):
print("Text changed...")
print(s)
def text_edited(self, s):
print("Text edited...")
print(s)
Как показано в этом коде, при однострочном редактировании вы можете установить максимальную длину текста.
В QLineEdit есть ряд сигналов для различных событий редактирования, в том числе при нажатии клавиши return (пользователем), когда пользователь изменил свой выбор. Также есть два сигнала редактирования: один — на случай, когда текст в поле отредактирован, другой — когда он изменён. Здесь различаются два вида изменений: пользовательские и выполненные программой. Сигнал textEdited отправляется только при редактировании текста пользователем.
Кроме того, с помощью маски ввода можно выполнить проверку вводимых данных, чтобы определить, какие символы поддерживаются и где. Применяется к соответствующему полю так:
widget.setInputMask('000.000.000.000;_')
Это позволяет использовать серию трёхзначных чисел, разделённых точками, для проверки адресов IPv4.
QSpinBox и QDoubleSpinBox
QSpinBox — это небольшое поле ввода чисел со стрелками для изменения значения. Оно поддерживает целые числа, а QDoubleSpinBox — числа с плавающей точкой:
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("My App")
widget = QSpinBox()
# Или: widget = QDoubleSpinBox()
widget.setMinimum(-10)
widget.setMaximum(3)
# Или: widget.setRange(-10,3)
widget.setPrefix("$")
widget.setSuffix("c")
widget.setSingleStep(3) # Или, например, 0.5 в QDoubleSpinBox
widget.valueChanged.connect(self.value_changed)
widget.textChanged.connect(self.value_changed_str)
self.setCentralWidget(widget)
def value_changed(self, i):
print(i)
def value_changed_str(self, s):
print(s)
Запустив код, вы увидите поле для ввода чисел. Значение в нём показывает префиксные и постфиксные блоки и ограничено диапазоном от +3 до –10:
В этом коде показан разнообразный функционал виджета. Чтобы задать диапазон допустимых значений, используются setMinimum и setMaximum. Одновременно они устанавливаются с помощью SetRange. Аннотация типов значений поддерживается префиксами и суффиксами, добавляемыми к числу. Например, для обозначений валюты или денежных единиц используются .setPrefix и .setSuffix соответственно.
Нажав на стрелки «вверх» и «вниз» на виджете, можно увеличить или уменьшить значение на величину, устанавливаемую с помощью .setSingleStep. Но это не повлияет на допустимые для виджета значения.
В QSpinBox и QDoubleSpinBox есть сигнал .valueChanged, срабатывающий при изменении их значений. С помощью необработанного сигнала .valueChanged отправляется числовое значение (целочисленного типа int или типа числа с плавающей точкой float), а с помощью .textChanged — значение в виде строки, включающей символы префикса и суффикса.
QSlider
QSlider — это ползунок, очень схожий по внутреннему функционалу с QDoubleSpinBox. Текущее значение представлено не числами, а в виде маркера ползунка, располагающегося по всей длине виджета. Этот виджет удобен для указания значения в промежутке между максимумом и минимумом, где абсолютная точность не требуется. Чаще всего этот тип виджетов используется при регулировке громкости. Есть и другие сигналы: .sliderMoved срабатывает при перемещении ползунка а .sliderPressed — при нажатии на ползунок:
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("My App")
widget = QSlider()
widget.setMinimum(-10)
widget.setMaximum(3)
# Или: widget.setRange(-10,3)
widget.setSingleStep(3)
widget.valueChanged.connect(self.value_changed)
widget.sliderMoved.connect(self.slider_position)
widget.sliderPressed.connect(self.slider_pressed)
widget.sliderReleased.connect(self.slider_released)
self.setCentralWidget(widget)
def value_changed(self, i):
print(i)
def slider_position(self, p):
print("position", p)
def slider_pressed(self):
print("Pressed!")
def slider_released(self):
print("Released")
Запустив код, вы увидите ползунок. Переместите его, чтобы изменить значение:
Также можно сделать вертикальный или горизонтальный ползунок, задав соответствующее расположение при его создании. Флаги расположения определены в пространстве имён Qt, например:
widget.QSlider(Qt.Orientiation.Vertical)
Или так:
widget.QSlider(Qt.Orientiation.Horizontal)
QDial
Наконец, QDial — это круговой виджет, схожий по функционалу с механическим ползунком из реального мира. Красиво, но с точки зрения пользовательского интерфейса не очень удобно. Он часто используется в аудиоприложениях:
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("My App")
widget = QDial()
widget.setRange(-10, 100)
widget.setSingleStep(0.5)
widget.valueChanged.connect(self.value_changed)
widget.sliderMoved.connect(self.slider_position)
widget.sliderPressed.connect(self.slider_pressed)
widget.sliderReleased.connect(self.slider_released)
self.setCentralWidget(widget)
def value_changed(self, i):
print(i)
def slider_position(self, p):
print("position", p)
def slider_pressed(self):
print("Pressed!")
def slider_released(self):
print("Released")
Запустив код, вы увидите круговой ползунок. Поверните его, чтобы выбрать число:
На сегодня мы заканчиваем, но это далеко не всё. Мы также расскажем о макете в PyQt, о работе с QAction, тулбарами, диалогами и не только. А пока ещё раз напоминаем о книге автора этих уроков и приглашаем на наши курсы:
-
Курс Python-разработчик
-
Профессия Fullstack-разработчик на Python
-
Курс «Python для веб-разработки»
Узнайте подробности здесь.
Профессии и курсы
Создаём свое Desktop приложение на Python
LOLZTEAMРазбираемся в основах Python на примере музыкального плеера!
Статья носит образовательный характер, мы ни к чему не призываем и не обязываем. Информация представлена исключительно в ознакомительных целях.
Больше интересных статей на нашем форуме: https://lolz.guru/articles/
Подписывайтесь на канал и делитесь ссылкой на статью с друзьями!
Всем привет! В этой статье мы напишем небольшое приложение на Python — плеер для прослушивания музыки.
Для достижения нашей цели можно использовать несколько разных и самых известных модулей:
- PyQt5
- Tkinter (встроенный модуль)
- Kivy
В данной статье мы остановимся на самом популярном PyQt.
PyQt содержит не только удобные методы для создания приложений, но и инструменты для их дизайна — Qt Designer, что не предоставляет ни один другой модуль. PyQt доступен для Windows, MacOSX, Linux, Android iOS и Raspberry Pi и различных версий Python от v2 до Qt v5.
Также мы будем использовать модуль pygame для манипуляций с музыкой.
Установка Python
Изначально нам нужно установить сам язык программирования Python. Переходим на официальный сайт языка Python. Нажимаем на вкладку Downloads. Далее на кнопку Download Python 3.9.5. У меня стоит Python 3.9, думаю, у вас всё должно работать.
Получаем обычный установщик и запускаем его после скачки.
Для безошибочного выполнения работы следуем этой инструкции:
- При установке обязательно жмём Add Python 3.9 to PATH
- Жмём Install Now
- Не трогаем Customize Location
Установка среды программирования
Лучше не использовать блокнот или Python Idle, для такого рода проектов. Это максимально неудобно!
Поэтому я советую лучшую IDE — PyCharm. Зацикливаться на ней я не буду, в интернете полно гайдов по его установке и настройке.
Создание проекта
Заходим в PyCharm -> File -> New Project -> Заполняем имя проекта. Жмём Inherit global site-packages. Создаём проект.
Открываем cmd или терминал на вашем компьютере (если проект в PyCharm можно и терминал в PyCharm):
Вписываем pip install pyqt5 -> Вписываем pip install pyqt5-tools-> Вписываем pip install pygame.
Настройка QtDesigner
Теперь программа находится на вашем компьютере по адресу:
Путь_к_папке_где_установлен_Python\Lib\site-packages\pyqt5_tools\Qt\bin\designer.exe
или (для старых версий):
Путь_к_папке_где_установлен_Python\Lib\site-packages\pyqt5_tools\designer.exe
Если программа отсутствует (частая проблема), то можно скачать здесь.
Создайте ярлык к этой программе, поскольку использовать ее придется часто.
Работа с QtDesigner
Давайте запустим программу и посмотрим, что в ней можно сделать.
Выбираем MainWindow и жмём Create.
Теперь мы видим сетку и Widget Box слева.
Давайте разберёмся, что где находится:
- Widget Box — место, где присутствуют все виджеты
- Центральная область — место для размещения виджетов
- Object Inspector — место для просмотра всех созданных виджетов
- Property Editor — место для изменения и просмотра характеристик выбранного виджета
Накидаем начальный интерфейс. Примерно вот так:
Здесь мы видим:
- ListWidget посередине
- несколько кнопок (PushButton)
- LcdNumber для таймера
- Horizontal Slider для увеличения громкости
- PlainText сверху для названия песни
Лучше переименовывать все виджеты в удобные названия:
- Проигрывание — play
- Пауза — stop
- Добавить — add
- Удалить — delete
- Таймер — timer
- Остальное оставил как есть
Иконки подключаются к кнопке через характеристику icon у PushButton.
Все файлы в pyqt для дизайна сохраняются в ui формате.
Если вы скачали файл от меня, то надо для каждой кнопки переопределить путь к иконкам. Чтобы это сделать, откройте ui файл через designer.exe, и для каждой кнопки укажите в характеристике icon путь до картинки.
Вот мой дизайн с несколькими иконками: https://disk.yandex.ru/d/8BKxMkJ28_en2g
Работа с Pyqt5
Для начала создадим файл main.py и вставим в него полный код, который находится ниже в статье.
Мы создаём класс MyWidget и подключаем к нему файл good.ui(наш дизайн) и для каждой кнопки создаём функцию и подключаем к ней.
Названия виджетов такие же как мы их создавали, но пишутся так self.имя_виджета.
Функции
__init__
Данная функция запускается при включении приложения. Давайте посмотрим, что за что отвечает.
- uic.loadUi() — подключение дизайна к программе, у меня файл с дизайном называется good.ui
- self.playing — отвечает за проигрывание музыки, если False — музыка не играет, True — играет
- pygame.mixer.init() — подключает музыку к программе, пока что она не загружена и не включена
- подключение всех событий методом clicked.connect() в к определённым функциям
- self.lcdtimer — таймер, который каждую минуту обновляет время
lcd_timer
Данная функция — таймер который отсчитывает длительность музыки. Обновляется каждую секунду.
- self.timer — объект QLcdNumber (дисплей). Методом display() выводится время.
- далее идёт условие, проверяющее длительность музыки на совпадение с таймером.
add_button
Данная функция добавляет выбранный музыкальный файл, используя QFileDialog. Затем добавляет в таблицу методом addItem().
delete_button
Данная функция удаляет выбранный музыкальный файл методом takeItem().
playbutton
Данная функция отвечает за кнопку проигрывания. При нажатии берётся выбранный элемент методом selectedItems()
Сравнивается не изменил ли музыку пользователь. Если изменил, то мы проигрываем заново методом load(), обновляем счётчик.
Если не изменил, то музыку уходит с паузы, если была остановлена, методом unpause()
stopbutton
Данная функция отвечает за кнопку паузы. Если музыка играет (за это отвечает self.playing), таймер останавливается, музыка останавливается методом pause().
backwardbutton
Данная функция отвечает за кнопку отматывания назад на 10 секунд. Если музыка идёт дольше 10 секунд, то методом set_pos() устанавливаем позицию self.time_counter — 10 (эквивалентно времени 10 секунд назад).
forwardbutton
Данная функция отвечает за кнопку отматывания вперёд на 10 секунд. Всё аналогично прошлой функции.
scrolled
Данная функция отвечает за событие изменения звука. Методом value() получаем новое значение звука. Методом set_value() ставим новое значение.
Полный код программы
import sys
import pygame
from PyQt5 import uic # Импортируем uic
from PyQt5.QtWidgets import QApplication, QMainWindow, QFileDialog
from PyQt5.QtCore import QTimer, QTime
class MyWidget(QMainWindow):
def __init__(self):
super().__init__()
uic.loadUi(‘good.ui’, self) # Загружаем дизайн
pygame.mixer.init()
self.playing = False
self.last_url = None
self.add_music.clicked.connect(self.addbutton)
self.delete_music.clicked.connect(self.deletebutton)
self.play.clicked.connect(self.playbutton)
self.stop.clicked.connect(self.stopbutton)
self.backward.clicked.connect(self.backwardbutton)
self.forward.clicked.connect(self.forwardbutton)
self.horizontalSlider.valueChanged.connect(self.scrolled)
self.lcdtimer = QTimer()
self.lcdtimer.timeout.connect(self.lcd_timer)
self.time_counter = 0
def lcd_timer(self):
time = QTime(self.time_counter // 3600, self.time_counter // 60, self.time_counter % 60)
text = time.toString(‘hh:mm:ss’)
self.timer.display(text)
self.time_counter += 1
if self.time_counter == round(self.sound.get_length()):
self.time_counter = 0
self.playing = False
self.last_url = None
self.lcdtimer.stop()
def addbutton(self):
fname = QFileDialog.getOpenFileName(
self, ‘Выбрать музыку’, »,
‘Аудио (*.mp3)’)[0]
self.listWidget.addItem(fname)
def deletebutton(self):
item = self.listWidget.selectedItems()[0]
if item:
self.listWidget.takeItem(self.listWidget.row(item))
def playbutton(self):
music = self.listWidget.selectedItems()
if music:
new_url = music[0].text()
if new_url != self.last_url:
pygame.mixer.music.load(new_url)
self.last_url = new_url
pygame.mixer.music.play()
self.sound = pygame.mixer.Sound(new_url)
self.time_counter = 0
self.plainTextEdit.setPlainText(new_url)
else:
pygame.mixer.music.unpause()
self.lcdtimer.start(1000)
self.playing = True
def stopbutton(self):
if self.playing:
self.lcdtimer.stop()
pygame.mixer.music.pause()
def backwardbutton(self):
if self.playing:
if self.time_counter > 10:
pygame.mixer.music.pause()
pygame.mixer.music.set_pos(self.time_counter — 10)
self.time_counter -= 10
pygame.mixer.music.unpause()
else:
pygame.mixer.music.rewind()
self.time_counter = 0
def forwardbutton(self):
if self.playing:
if self.time_counter + 10 < self.sound.get_length():
pygame.mixer.music.pause()
pygame.mixer.music.set_pos(self.time_counter + 10)
self.time_counter += 10
pygame.mixer.music.unpause()
else:
pass
def scrolled(self):
new_value = round(self.horizontalSlider.value() / 100, 3)
pygame.mixer.music.set_volume(new_value)
if __name__ == ‘__main__’:
pygame.init()
app = QApplication(sys.argv)
ex = MyWidget()
ex.show()
sys.exit(app.exec_())