Вы находитесь на странице: 1из 489

in Russian ;)

Обращение переводчика к читателю

Книга была удачно стянута с неопознанного ресурса и переведена с


божьей помощью на русский язык :)

Переводчик, желая изучить питон и PyQt версии 6, не нашёл годной


литературы на русском языке, в связи с чем решил перевести эту книгу.
Одновременно с переводом тестировался листинг кода в этой книге и
оставлялись немногочисленные комментарии от переводчика.

Выражаю благодарность за помощь с переводом компании DeepL

Все мыслимые и немыслимые права были жесточайше нарушены :(

Дельные замечания по переводу или ошибкам присылайте на:


perevodchik@tuta.io
Есть малый шанс, что будут исправлены :)

К сожалению, из-за недостаточного качества иллюстраций этот файл не


пригоден для профессиональной печати
PyQt для начинающих
Практический подход к графическому
интерфейсу
Программирование с PyQt6

Второе издание

Джошуа М Уиллман
Начало работы с PyQt: Практический подход к программированию графических интерфейсов с PyQt6

ISBN-13 (pbk): 978-1-4842-7998-4 ISBN-13 (электронный): 978-1-4842-7999-1


https://doi.org/10.1007/978-1-4842-7999-1
Copyright © 2022 by Joshua M Willman
Данная работа является объектом авторского права. Все права защищены издательством, независимо от
того, идет ли речь о целом или части в частности, права на перевод, перепечатку, повторное использование
иллюстраций, декламацию, вещание, воспроизведение на микрофильмах или любым другим физическим
способом, а также передача или хранения и поиска, электронной адаптации, компьютерного программного
обеспечения, или по аналогичной или несхожей методике, известной в настоящее время или разработанной в
будущем.
В этой книге могут встречаться названия, логотипы и изображения, защищенные товарными знаками. Вместо
того чтобы использовать символ товарного знака мы используем названия, логотипы и изображения только в
редакционных целях и на благо читателей.
редакционной манере и в интересах владельца товарного знака, без намерения нарушить его права.
товарного знака.
Использование в данной публикации торговых названий, торговых марок, знаков обслуживания и
аналогичных терминов, даже если они не указаны в качестве таковых
даже если они не обозначены как таковые, не должно восприниматься как выражение мнения о том, являются
ли они объектом права собственности. Использование общих описательных названий, зарегистрированных
названий, торговых марок, знаков обслуживания и т.д. в данной публикации не означает, даже если они
идентифицированы как таковые.
в данной публикации не подразумевает, даже при отсутствии конкретного заявления, что такие названия
освобождены от соответствующих защитных законов и правил и поэтому свободны для общего использования.
Хотя советы и информация в этой книге считаются верными и точными на дату публикации, ни авторы, ни
редакторы, ни издатель не могут нести никакой юридической ответственности за любые ошибки или упущения,
которые могут быть допущены. Издатель не дает никаких гарантий, явных или подразумеваемых, в отношении
материала, содержащегося в настоящем издании.

Управляющий директор, Apress Media LLC: Велмоед Спар


Редактор по закупкам: Селестин Суреш Джон
Редактор по развитию: Джеймс Маркхэм
Координирующий редактор: Дивия Моди
Дизайн обложки разработан eStudioCalamar
Изображение на обложке разработано Pixabay

Распространяется в книжной торговле по всему миру издательством Springer Science+Business Media New
York, 1 New York Plaza, New York, NY 10004. Телефон 1-800-SPRINGER, факс (201) 348-4505, электронная
почта orders-ny@springer-sbm.com или посетите сайт www.springeronline.com. Apress Media, LLC является
калифорнийским ООО, а единственным участником (владельцем) является Springer Science + Business Media
Finance Inc (SSBM Finance Inc). SSBM Finance Inc является корпорацией штата Делавэр корпорация.
Для получения информации о переводах, пожалуйста, пишите по адресу booktranslations@springernature.com;
для получения прав на перепечатку, права на издание в мягкой обложке или аудио, пожалуйста, пишите по
адресу bookpermissions@springernature.com.
Издания Apress можно приобрести оптом для академического, корпоративного или рекламного использования.
Лицензии на большинство изданий также доступны. За дополнительной информацией обращайтесь к нашей
странице “Оптовые продажи печатных изданий и электронных книг” на сайте веб-странице по адресу
http://www.apress.com/bulk-sales.
Любой исходный код или другой дополнительный материал, на который автор ссылается в этой книге,
доступен для читателям на GitHub через страницу продукта книги, расположенную по адресу https://github.
com/Apress/Beginning-PyQt--second-edition. Для получения более подробной информации посетите сайт http://
www.apress.com/source-code.

Отпечатано на бескислотной бумаге (вот за это спасибо :) прим. переводчика)


Оглавление
Об авторе............................................................................................... XIV
О технических рецензентах..................................................................... XV
Благодарности....................................................................................... XVI
Введение.............................................................................................. XVII

Глава 1: Начало работы с PyQt................................................................................. 1


Фреймворк PyQt......................................................................................................................... 1
Почему стоит выбрать PyQt?..................................................................................................2
PyQt5 против PyQt6................................................................................................................3
Установка Python 3 и PyQt6.........................................................................................................3
Проверка версии Python........................................................................................................4
Установка PyQt6.....................................................................................................................4
Введение в пользовательские интерфейсы................................................................................5
Что такое графический интерфейс пользователя?.................................................................5
Концепции создания хорошего дизайна интерфейса.............................................................6
Создание первого графического интерфейса: Пустое окно..........................................................7
Объяснение создания пустого окна........................................................................................8
Резюме..................................................................................................................................... 11

Глава 2: Создание простого графического интерфейса......................................... 12


Виджет QLabel........................................................................................................................... 13
Объяснение использования QLabel...................................................................................... 14
Проект 2.1 - Графический интефейс профиля пользователя...................................................... 17
Проектирование графического интерфейса профиля пользователя.................................... 18
Пояснения к графическому интерфейсу профиля пользователя.......................................... 18
Резюме..................................................................................................................................... 22

VI ОГЛАВЛЕНИЕ
Глава 3: Добавление дополнительной функциональности с помощью виджетов.......23
Обработчики событий, сигналы и слоты................................................................................... 23
Виджет QPushButton................................................................................................................. 24
Объяснение использования QPushButton............................................................................ 25
Виджет QLineEdit...................................................................................................................... 28
Объяснение использования QLineEdit.................................................................................. 28
Виджет QCheckBox.................................................................................................................... 31
Объяснение использования QCheckBox................................................................................ 32
Диалоговое окно QMessageBox.................................................................................................. 34
Окна и диалоги..................................................................................................................... 35
Объяснение использования QMessageBox............................................................................ 35
Проект 3.1 - Графический интерфейс входа в систему и диалог регистрации............................. 41
Проектирование графического интерфейса входа в систему и диалогового окна регистрации.. 42
Объяснение создания графического интерфейса входа в систему....................................... 43
Пояснения к созданию главного окна.................................................................................. 52
Объяснение создания диалогового окна регистрации......................................................... 53
Резюме..................................................................................................................................... 57

Глава 4: Изучение управления макетом........................................................... 58


Использование менеджеров компоновки в PyQt...................................................................... 59
Абсолютное позиционирование................................................................................................ 60
Горизонтальная и вертикальная компоновка с помощью макетов блоков.............................. 61
Пояснение для QHBoxLayout................................................................................................ 61
Пояснение для QVBoxLayout................................................................................................. 64
Создание вложенных макетов.................................................................................................. 67
Пояснения к вложенным макетам....................................................................................... 68
Размещение виджетов в сетках с помощью QGridLayout.......................................................... 72
Пояснения к QGridLayout...................................................................................................... 74
Создание форм с помощью QFormLayout.................................................................................. 80
Пояснение для QFormLayout................................................................................................. 81
Управление страницами с помощью QStackedLayout................................................................ 87
Объяснение для QStackedLayout.......................................................................................... 89
Дополнительные советы по управлению пространством........................................................................ 92
Пояснения по управлению пространством.......................................................................................... 93
Резюме.......................................................................................................................................................... 95

ОГЛАВЛЕНИЕ VII
Глава 5: Меню, панели инструментов и многое другое..................................................96
Общая практика создания меню................................................................................................................ 97
Создание простой панели меню.................................................................................................................. 97
Пояснения к созданию панели меню.................................................................................................... 98
Использование значков и класса QIcon..................................................................................................... 102
Пояснения к использованию значков................................................................................................. 102
Установка центрального виджета....................................................................................................... 106
Встроенные классы диалогов в PyQt........................................................................................................ 106
Класс QFileDialog................................................................................................................................... 106
Класс QInputDialog................................................................................................................................ 107
Класс QFontDialog.................................................................................................................................. 108
Класс QColorDialog................................................................................................................................. 109
О QMessageBox...................................................................................................................................... 110
Проект 5.1 - Графический интерфейс блокнота с насыщенным текстом................................................. 111
Проектирование графического интерфейса блокнота Rich Text Notepad.......................................... 112
Пояснения к графическому интерфейсу блокнота Rich Text Notepad................................................. 113
Расширение возможностей главного окна............................................................................................... 122
Пояснение к расширению возможностей........................................................................................... 123
Проект 5.2 - Простой графический интерфейс редактора фотографий................................................... 131
Проектирование графического интерфейса редактора фотографий................................................ 132
Пояснения к графическому интерфейсу редактора фотографий...................................................... 132
Резюме........................................................................................................................................................ 146

Глава 6: Стилизация графических интерфейсов...........................................................148


Что такое стили в PyQt?.............................................................................................................................. 148
Изменение стиля по умолчанию......................................................................................................... 149
Изменение внешнего вида виджетов...................................................................................................... 150
Использование HTML для изменения внешнего вида текста............................................................ 150
Использование таблиц стилей Qt для изменения внешнего вида виджетов .................................. 153
Объяснение использования встроенных (inline) таблиц стилей Qt................................................... 155
Объяснение использования встроенных (embedded) таблиц стилей Qt........................................... 158
Организация виджетов с помощью контейнеров и вкладок.................................................................. 161
Виджет QRadioButton........................................................................................................................... 162
Класс QGroupBox................................................................................................................................... 162
Класс QTabWidget.................................................................................................................................. 163
Объяснение использования контейнеров и вкладок......................................................................... 163
Проект 6.1 - графический интерфейс заказа еды..................................................................................... 167
Разработка графического интерфейса пользователя для заказа еды............................................. 169
Пояснения к графическому интерфейсу заказа еды.......................................................................... 170
Справочник свойств CSS............................................................................................................................. 182
Резюме........................................................................................................................................................ 183

VIII ОГЛАВЛЕНИЕ
Глава 7: Обработка событий в PyQt...............................................................................184
Обработка событий в PyQt......................................................................................................................... 184
Использование сигналов и слотов....................................................................................................... 185
Использование обработчиков событий для обработки событий...................................................... 185
Разница между сигналами, слотами и обработчиками событий...................................................... 186
Обработка ключевых событий.................................................................................................................. 186
Объяснение обработки ключевых событий....................................................................................... 187
Обработка событий мыши........................................................................................................................ 188
Объяснение обработки событий мыши.............................................................................................. 189
Создание пользовательских сигналов...................................................................................................... 193
Пояснения к созданию пользовательских сигналов.......................................................................... 193
Резюме........................................................................................................................................................ 196

Глава 8: Создание графических интерфейсов с помощью Qt Designer.........................197


Начало работы с Qt Designer...................................................................................................................... 198
Установка Qt Designer........................................................................................................................... 198
Изучение пользовательского интерфейса Qt Designer....................................................................... 199
Режимы редактирования в Qt Designer.............................................................................................. 205
Создание приложения в Qt Designer.......................................................................................................... 206
Проект 8.1 - графический интерфейс клавиатуры.................................................................................... 207
Пояснения к графическому интерфейсу клавиатуры........................................................................ 207
Дополнительные советы по использованию Qt Designer........................................................................ 230
Настройка главного окна и меню........................................................................................................ 230
Отображение изображений в Qt Designer........................................................................................... 232
Добавление таблиц стилей в Qt Designer............................................................................................ 232
Резюме........................................................................................................................................................ 233

Глава 9: Работа с буфером обмена................................................................................234


Класс QClipboard.......................................................................................................................................... 234
Пояснения к использованию QClipboard............................................................................................. 236
Проект 9.1 - графический интерфейс Sticky Notes..................................................................................... 239
Пояснения к графическому интерфейсу Sticky Notes.......................................................................... 240
Резюме........................................................................................................................................................ 244

ОГЛАВЛЕНИЕ IX
Глава 10: Представление данных в PyQt......................................................................245
Быстрая работа с данными в PyQt............................................................................................................ 245
Класс QListWidget........................................................................................................................................ 246
Пояснения к использованию QListWidget........................................................................................... 247
Перетаскивание в PyQt.............................................................................................................................. 250
Пояснения к перетаскиванию.............................................................................................................. 251
Класс QTableWidget..................................................................................................................................... 253
Пояснения к использованию QTableWidget........................................................................................ 254
Класс QTreeWidget....................................................................................................................................... 262
Пояснения к использованию QTreeWidget.......................................................................................... 263
Резюме........................................................................................................................................................ 266

Глава 11: Графика и анимация в PyQt...........................................................................267


Введение в класс QPainter.......................................................................................................................... 267
Пояснения к использованию класса QPainter..................................................................................... 269
Проект 11.1 - графический интерфейс Painter........................................................................................... 279
Пояснения к графическому интерфейсу Painter................................................................................. 280
Создание класса Canvas........................................................................................................................ 281
Создание графического интерфейса класса MainWindow для Painter............................................... 286
Анимация сцен с помощью QPropertyAnimation...................................................................................... 291
Пояснения к анимации сцен................................................................................................................ 292
Введение в анимацию виджетов.............................................................................................................. 296
Пояснение к анимации виджетов....................................................................................................... 297
Резюме........................................................................................................................................................ 301

Глава 12: Создание пользовательских виджетов........................................................302


Проект 12.1 - Пользовательский виджет RGB-ползунок.......................................................................... 302
Классы PyQt для работы с изображениями........................................................................................ 304
Виджет QSlider...................................................................................................................................... 304
Пояснения к виджету RGB-ползунка................................................................................................... 305
Демонстрация RGB-ползунка.................................................................................................................... 315
Пояснения к демонстрации RGB-ползунка......................................................................................... 316
Резюме........................................................................................................................................................ 317

X ОГЛАВЛЕНИЕ
Глава 13: Работа с Qt Quick.............................................................................................318
Описания QtQuick и QML............................................................................................................................. 319
Элементы в QtQuick.............................................................................................................................. 320
Введение в язык и синтаксис QML........................................................................................................ 321
Создание и запуск QML-компонентов........................................................................................................ 324
Создание и загрузка QML-компонентов.............................................................................................. 325
Создание многократно используемых компонентов........................................................................ 330
Обработка макетов в QML.................................................................................................................... 332
Создание и загрузка QML-окон............................................................................................................. 336
Использование трансформаций для анимации объектов...................................................................... 342
Пояснения к простым преобразованиям........................................................................................... 342
Пояснения к использованию преобразований для анимации объектов......................................... 344
Резюме........................................................................................................................................................ 347

Глава 14: Введение в работу с базами данных.............................................................348


Размышления о данных............................................................................................................................ 348
Введение в программирование на основе Model/View........................................................................... 349
Компоненты архитектуры Model/View............................................................................................... 349
Классы Model/View в PyQt.................................................................................................................... 350
Пояснения к введению в Model/View.................................................................................................. 352
Работа с базами данных SQL в PyQt........................................................................................................... 356
Что такое SQL?....................................................................................................................................... 356
Проект 14.1 - Графический интерфейс управления учетной записью..................................................... 358
Пояснения к работе с модулем QtSql................................................................................................... 360
Пояснения к запросу к базе данных с помощью QSqlQuery............................................................... 366
Работа с классом QSqlTableModel......................................................................................................... 368
Работа с классом QSqlRelationalTableModel......................................................................................... 372
Пояснения к графическому интерфейсу управления учетными записями...................................... 376
Резюме........................................................................................................................................................ 383

Глава 15: Управление потоками...................................................................................384


Введение в управление потоками............................................................................................................ 384
Управление потоками в PyQt............................................................................................................... 385
Методы обработки длинных событий в PyQt..................................................................................... 386
Проект 15.1 - графический интерфейс переименования файлов............................................................ 387
Виджет QProgressBar............................................................................................................................ 387
Пояснения к графическому интерфейсу переименования файлов................................................... 388
Резюме........................................................................................................................................................ 395

ОГЛАВЛЕНИЕ XI
Глава 16: Дополнительные проекты............................................................................396
Проект 16.1 - Графический интерфейс просмотра каталогов................................................................... 397
Пояснения к графическому интерфейсу просмотра каталогов.......................................................... 398
Проект 16.2 - Графический интерфейс камеры......................................................................................... 400
Пояснения к графическому интерфейсу камеры............................................................................... 402
Проект 16.3 - П Project 16.7 – Tri-state ростой графический интерфейс часов.......................................... 407
Пояснения к графическому интерфейсу часов................................................................................... 407
Проект 16.4 - Графический интерфейс календаря.................................................................................... 410
Пояснения к графическому интерфейсу календаря........................................................................... 410
Проект 16.5 - Графический интерфейс "Виселица"................................................................................... 414
Пояснения к графическому интерфейсу "Виселица".......................................................................... 415
Проект 16.6 - Графический интерфейс веб-браузера................................................................................ 423
Пояснения к графическому интерфейсу веб-браузера...................................................................... 425
Проект 16.7 - Трехсоставной QComboBox................................................................................................... 434
Пояснения к трехсоставному QComboBox........................................................................................... 434
Резюме ....................................................................................................................................................... 437

Приложение: Справочное руководство по PyQt6.........................................................438


Избранные модули PyQt6.......................................................................................................................... 438
Избранные классы PyQt............................................................................................................................. 439
Классы для построения окна графического интерфейса.................................................................... 440
QPainter................................................................................................................................................. 446
Менеджеры компоновки..................................................................................................................... 447
Виджеты кнопок.................................................................................................................................. 449
Виджеты ввода.................................................................................................................................... 450
Виджеты отображения........................................................................................................................ 457
Представления элементов.................................................................................................................. 459
Виджеты-контейнеры......................................................................................................................... 461
QtQuick и QML.............................................................................................................................................. 464
Таблицы стилей Qt...................................................................................................................................... 464
Пространство имен Qt................................................................................................................................ 467
Резюме........................................................................................................................................................ 467
Указатель......................................................................................................................468

XII ОГЛАВЛЕНИЕ
ОГЛАВЛЕНИЕ XIII
Об авторе
Джошуа Уиллман - инженер-программист с более чем 12-летним опытом
разработки приложений преимущественно на Python и C++. Его карьера позволила
ему принять участие в многих различных областях, от робототехники, машинного
обучения и компьютерного зрения до разработки пользовательского интерфейса,
разработки игр, и многое другое. Его первым опытом работы с PyQt было создание
интерфейса для упрощения процесса маркировки наборов данных для машинного
обучения. С тех пор он просто влюбился!
В последние годы его страсть к программированию
и всему визуальным эффектам позволила ему
принять участие в многочисленных проектах.
Среди них разработка образовательных курсов по
мобильной робототехнике и компьютерному зрению
с использованием Arduino и Raspberry Pi, создание
приложений с графическим интерфейсом, а также
работа в качестве одиночного разработчика инди-
игр. В настоящее время он работает инженером по
робототехнике, техническим писателем и создателем
контента (в свободное время изучает веб-разработку,
чтобы создать свою собственную платформу
redhuli.io). Когда он не работает, ему нравится возиться с проектами по робототехнике
и проводить время со своей замечательной женой и дочерью.
Он также является автором двух книг, выпущенных издательством Apress:
• Beginning PyQt: Практический подход к программированию графических
интерфейсов (1-е издание).
• Современный PyQt: Создание приложений с графическим интерфейсом для
управления проектами, компьютерное зрение и анализ данных

XIV
О технических рецензентах
Викас Кумар имеет более семи лет совокупного опыта
работы в авионике, аэрокосмической, автомобильной и в
сфере здравоохранения в научно-исследовательской де-
ятельности и разработке программного обеспечения. Он
программировал и разрабатывал настольные приложения
с использованием C++, Qt, PyQt/PySide, Python, Java и SQL
с самого начала. Имеет степень бакалавра технологий в об-
ласти компьютерных наук и инженерии от Биджу Patnaik
University of Technology, Одиша, Индия.
Он работал с различными индийскими оборонными
заказчиками для разработки тестового пакета для авионики для тестирования ком-
понентов различных военных самолетов. Сотрудничал с компанией Airbus в каче-
стве клиента для разработки различного программного обеспечения, отвечающего
за структурный и вычислительный анализ различных коммерческих самолетов. Со-
трудничал с компанией Mercedes-Benz для разработки программного обеспечения,
отвечающего за анализ данных, моделирование и тестирование различных коммер-
ческих самолетов, анализ данных, моделирование и валидацию высоковольтных
электрических батарей, используемых в электромобилях Mercedes-Benz. Его техни-
ческие навыки включают C++, Qt, PyQt/PySide, Python, Java, MySQL и разработку на-
стольных приложений в Linux и Windows. В настоящее время он работает старшим
инженером-программистом в компании GE Healthcare India, занимаясь разрабаткой
программного обеспечения для МРТ-сканеров.
Саумитра Джагдале является основателем компании
Open Cloudware и Посол глобального искусственного интел-
лекта, уделяющий особое внимание современным тенденци-
ям в области технологий. Он является признанным техни-
ческим автором для различных известных медиа-компаний,
таких как OpenSystems Media, CNX Software, AspenCore,
Electronics-Lab и IoT Tech Trends. Кроме того, он является
старшим инженером - облачных сервисов и программно-
го обеспечения в компании L&T Infotech с опытом работы
в области CRM и ERP-приложений. Будучи разработчиком
Python с открытым исходным кодом, он также возглавляет
сообщество TensorFlow Community Индия для продвижения
методологии глубокого обучения в сообществе.

XV
Благодарности
Я безмерно благодарен Дивии Моди, Селестину Сурешу Джону и замечательному
коллективу издательства «Apress» за предоставленную мне возможность написать
второе издание этой книги. Моя глубочайшая признательность Дивии за то, что она
была рядом на каждом шагу.
Особая благодарность Андреа Касадеи, чьи вопросы очень помогли улучшить это
издание.
Огромная благодарность сообществам Python, PyQt и Qt. Я бы также очень
поблагодарил Фила Томпсона, создателя PyQt.
Я очень благодарен Ричарду Броноски за то, что он дал мне шанс, когда я больше
всего в нем нуждался.
Ашиш Наик, спасибо тебе за поддержку, которую ты оказывал моей семье и мне
с самого начала.
Спасибо моей маме, Валори, и моим сестрам, Тише и Джазмин, за поддержку,
которую вы всегда оказывали мне.
Я глубоко благодарен моей жене, Эвелин Йе, чье постоянное терпение по
отношению ко мне делает эти книги возможными.
Калани, ты продолжаешь быть моей мотивацией и вдохновением.
Еще раз спасибо читателям. Я искренне надеюсь, что идеи, найденные в этой книге
подстегнут ваше творчество и принесут вам какую-то пользу.

XVI
Введение
С новыми версиями PyQt появляются новые инструменты для игры. Последнее
издание направлено на изучение некоторых из этих идей, при этом ориентируясь
на новичков. Вы узнаете, как использовать язык программирования Python вместе
с набором инструментов PyQt6 для создания графических пользовательских
интерфейсов (GUI).
Важнее всего просто начать. Кодирование графического интерфейса
пользователя можно рассматривать как сочетание навыков программирования
и графического дизайна. Осознание понимания потребностей пользователя
также имеет решающее значение для улучшения удобства использования и
графического вида. Программирование GUI часто сводится к выбору подходящего
компонента, называемого в PyQt виджетами, для выполнения задачи, а затем
применить необходимые навыки программирования, чтобы заставить их
работать.
Одна из целей состоит в том, чтобы сбалансировать теорию, лежащую в основе
хороших методов проектирования, с более практическими занятиями, примерами
кодирования в стиле “учись как хочешь”. Новые концепции и классы PyQt вводятся
в каждой главе, а последующие главы иногда опираются на предыдущие.

Кому стоит прочитать эту книгу


Эта книга предназначена для разработчиков на Python, которые хотят начать
создавать графические пользовательские интерфейсы и хотят использовать
последнюю версию PyQt для начала работы. Наличие предварительных знаний
PyQt или других наборов инструментов графического интерфейса пользователя Py-
thon не является необходимым для начала использования этой книги. Тем не менее,
рекомендуется понимать основы Python и синтаксиса Python и уметь использовать
объектно-ориентированное программирование (ООП).

XVII
Как организована эта книга
Последнее издание книги “Знакомство с PyQt” начинается со знакомства с
основными идеями разработки графических интерфейсов пользователя. Глава 1
поможет вам установить и понять, как использовать последнюю версию PyQt.
Главы 2 и 3 учат, как добавлять виджеты в ваши приложения, тем самым
добавляя все больше и больше функциональности в ваши проекты. В обеих главах
представлены различные виджеты, такие как QLabel, QCheckBox и QLineEdit, а также
приводятся примеры и идеи их использования.Глава 3 также познакомит вас с
механизмом сигналов и слотов PyQt для обработки событий.
Глава 4 посвящена менеджерам компоновки для расположения виджетов.
После изучения различных виджетов и макетов, Глава 5 проведет вас через
примеры, которые помогут вам создать классический графический интерфейс с
меню и панелями инструментов.
В Главе 6 представлены таблицы стилей для изменения внешнего вида ваших
приложений.
В Главе 7 рассматривается обработка событий, происходящих в графическом
интерфейсе, например, нажатие пользователем кнопки мыши. Вы также узнаете, как
создавать собственные сигналы и как повторно реализовать обработчики событий.
Поскольку Qt также включает свой собственный графический интерфейс
пользователя, помогающий создавать графические интерфейсы, мы рассмотрим, как
использовать Qt Designer в Главе 8.
Отсюда мы начнем изучать более продвинутые концепции.
Глава 9 познакомит вас с использованием буфера обмена для копирования и
вставки информации между приложениями.
В Главе 10 показано, как работать с данными, используя классы удобства PyQt,
основанные на элементах. Вы также узнаете, как добавить базовую функциональность
перетаскивания к виджетам.
Глава 11 знакомит вас с рисованием, графикой и анимацией.
Настройка очень важна в PyQt. Глава 12 показывает, как создавать и использовать
свои собственные виджеты.
В Главе 13 рассказывается о Qt Quick для создания текучих и динамичных
приложений.
В Главе 14 показано, как использовать для создания пользовательских интерфейсов,
работающих с данными с помощью SQL баз данных и архитектуры PyQt Model/View.
В Главе 15 обсуждается многопоточное программирование, позволяющее
избежать «зависания» ваших приложений.
Глава 16 содержит дополнительные примеры проектов, которые помогут вам
получить дополнительную практику и понимание создания приложений с помощью
PyQt.
Приложение содержит дополнительную информацию о различных модулях
и классах PyQt. В приложении также можно найти дополнительный пример
кодирования.

XVIII
Понимание структуры и кода глав
Код в последнем издании, как правило, разбит на более мелкие части (если
только программа не представляет собой короткий пример). Это делает код более
легким для усвоения и понимания, а также избавляет пользователей от дампов
кода (копирования и вставки целых разделов). Вы всегда можете обратиться к
репозиторию GitHub (ссылка находится в разделе “Ссылки на исходный код”), чтобы
увидеть примеры кода целиком.
Кроме того, следуя примеру, обратите внимание на заголовки листингов над
каждым фрагментом кода. Они дают подсказки по каждому разделу, а также помогают
понять, какой пример кода вы просматриваете. Например, в Главе 2 представлены
метки. Код, объясняющий, как использовать метки, разбит на две части, листинг 1 и
листинг 2. Заголовок первого листинга выглядит следующим образом:
Листинг 1. Настройка главного окна для демонстрации использования виджетов
QLabel
# labels.py

Вторая часть приложения имеет следующий вид


Листинг 2. Метод setUpMainWindow() для отображения текстовых и графических
меток
# labels.py

Если фрагмент кода не содержит номера листинга, значит, этот код предоставляет
дополнительную информацию, но не находится ни в одном из файлов.
Кроме того, при необходимости в тексте указываются важные различия между
PyQt5 и PyQt6.
Еще одним важным замечанием является то, что PyQt разработан как
кроссплатформенный. Однако мир не совершенен, и иногда разработчикам даются
дополнительные пояснения для того, чтобы их код работал на Windows, macOS или
Linux (Ubuntu). Обязательно посмотрите на комментарии или примечания, чтобы
получить ясность при запуске приложения.
Наконец, по мере чтения ключевые слова выделяются жирным шрифтом.
Имена файлов, имена модулей и классов Python и PyQt, а также фрагменты кода,
упоминаемые в тексте, отображаются другим шрифтом, например, QPushButton.

XIX
Ссылки на исходный код
Исходный код для Beginning PyQt: практический подход к
программированию GUI с помощью PyQt6 можно найти на GitHub по адресу
https://github.com/Apress/Beginning-PyQt--second-edition

Отзывы читателей
Ваши отзывы, вопросы и идеи всегда приветствуются. Если у вас есть какие-либо
вопросы об этой книге, PyQt версии 5 или 6, разработке графического интерфейса
или вы просто хотите оставить комментарий, вы всегда можете найти меня по адресу
redhuli.comments@gmail.com.

XX
ГЛАВА 1

Начало работы с PyQt


Здравствуйте! Добро пожаловать в раздел Начало работы с PyQt: Практический
подход к программированию графических интерфейсов. Возможно, вы здесь потому,
что хотите научиться создавать приложения и вам нужна помощь в начале работы.
Возможно, у вас есть личная программа, которую нужно создать, или вы хотите
создать пользовательское приложение для использования другими. Какой бы ни
была ваша причина, вам нужно понять, с чего лучше всего начать
Целью этой книги является практический подход к кодированию пользовательских
интерфейсов. Вы сможете проследить за ходом работы и написать множество
примеров, как простых, так и сложных. Вы также получите знания благодаря
наглядности и практике, которые покажут, как использовать фундаментальные
концепции, необходимые для создания собственных приложений. Во многих случаях
эти концепции затем будут применяться в более крупных проектах.
В этой главе вы:
• Узнаете о наборе инструментов PyQt для создания пользовательских
интерфейсов
• Установите Python 3 и загрузите последнюю версию PyQt6
• Рассмотрите некоторые фундаментальные концепции для создания удобных
пользовательских интерфейсов
• Создадите свое первое приложение с помощью PyQt
Давайте начнем это путешествие, узнав больше о PyQt.

Фреймворк PyQt
Инструментарий PyQt - это набор привязок Python для кроссплатформенного
инструментария виджетов и приложений Qt. Что это значит?
Во-первых, Qt используется для разработки пользовательских интерфейсов и
других приложений и разрабатывается компанией The Qt Company. Эта платформа
важна тем, что она может работать на многочисленных программных и аппаратных
системах, таких как Windows, macOS, Linux, Android или встроенные системы,
практически не изменяя базовый код и сохраняя возможности и скорость
системы, на которой она работает. В общем, вы сможете создавать удивительные
кроссплатформенные приложения, не беспокоясь о платформе пользователя.
Во-вторых, все это означает, что PyQt сочетает в себе все преимущества
кроссплатформенного набора виджетов Qt C++ и Python, мощного и простого,
кроссплатформенного интерпретируемого языка. Стоит отметить, что хотя PyQt
имеет свою собственную документацию, документация Qt, как правило, более
полная. Если вы никогда раньше не использовали C++, это может показаться сложной
ГЛАВА 1 НАЧАЛО РАБОТЫ С PYQT 1
задачей. Мы обсудим эту тему немного подробнее в первом приложении этой главы.
Для получения дополнительной информации о Qt загляните на www.qt.io.
Более подробную информацию о PyQt можно найти на сайте
www.riverbankcomputing.com/news.

Почему стоит выбрать PyQt?


PyQt способен не только создавать графические интерфейсы, так как он также
имеет доступ к классам Qt, которые охватывают такие механики, как обработка
XML, базы данных SQL, сетевое взаимодействие, графика и анимация, а также
многие другие технологии. Возьмите возможности Qt и объедините их с количеством
модулей расширения, которые предоставляет Python, и вы получите возможность
создавать новые приложения, которые могут опираться на эти уже существующие
библиотеки.
PyQt также включает Qt Designer, который позволяет любому человеку намного
быстрее собрать графический интерфейс, используя простой конструктор
графических интерфейсов с перетаскиванием. Qt Designer подробно рассматривается
в Главе 8.
Используя механизм сигналов и слотов PyQt, вы можете создать чрезвычайно
интерактивный интерфейс и настроить взаимодействие различных компонентов
PyQt. Более подробно об этом будет рассказано в Главах 3 и 7.
Конечно, существуют и другие наборы инструментов для создания приложений с
графическим интерфейсом с помощью Python, такие как Tkinter, wxPython и PySide,
и все они имеют свои преимущества. Например, Tkinter поставляется в комплекте с
Python, что означает, что вы можете найти множество полезных ресурсов, выполнив
быстрый поиск в Интернете. PySide - это привязка Qt к Python, управляемая самой
компанией The Qt Company. Несмотря на это, PyQt все еще имеет больше поклонников
благодаря своему возрасту и замечательному сообществу разработчиков.
Стоит отметить, что если вы решите использовать PyQt для создания коммерческих
приложений, вам придется получить лицензию. Приложения, созданные в этой
книге, будут использовать Стандартную общественную лицензию GNU (GPL).
В конечном итоге, все сводится к выбору инструментария, который лучше всего
подходит для вашего проекта.

2 ГЛАВА 1 НАЧАЛО РАБОТЫ С PYQT


PyQt5 против PyQt6
В последней версии Qt больше внимания уделяется 2D и 3D возможностям. В
последней версии PyQt также довольно много изменений. Поскольку целью этой
книги является введение в основы разработки приложений с помощью PyQt, мы
рассмотрим те изменения, которые окажут на вас наибольшее влияние на данном
этапе вашего пути:
• По мере погружения в PyQt вы, безусловно, будете использовать
перечисления, флаги и другие идентификаторы в пространстве имен
Qt. Они полезны для задания свойств различных классов в PyQt. Важно
понимать, что теперь для этих элементов используются полные имена, а не
сокращенные, как в PyQt5. Например, PyQt5 использовал Qt.AlignCenter для
центрирования текста, но PyQt6 использует полное имя Qt.AlignmentFlag.
Center. Вы увидите множество примеров этого на протяжении всей книги, и
мы обсудим это подробнее в последующих главах.
• Некоторые классы и методы были устаревшими, например, QDesktopWidget, а
другие были перемещены, например, класс QAction теперь находится в модуле
QtGui.
• Метод exec() теперь используется в PyQt6 для запуска цикла событий вашего
приложения, а не exec_().
Это неполный список, и вы узнаете больше об этих и других темах по мере
изучения. Если вам интересно узнать больше о новых изменениях в PyQt6, загляните
на www.riverbankcomputing.com/static/Docs/PyQt6/pyqt5_differences.html.

Установка Python 3 и PyQt6


Чтобы все читатели были на одной волне, давайте начнем с установки или
обновления вашей версии Python.

ГЛАВА 1 НАЧАЛО РАБОТЫ С PYQT 3


Проверка версии Python
Для того чтобы использовать PyQt, вам необходимо установить Python 3.7 или
выше.

Примечание. Когда PyQt6 только был выпущен, он был совместим с Python


3.6.1 или выше. Однако в будущем планируется прекратить его использование.
На всякий случай вам следует установить Python 3.7 или выше.

Чтобы проверить, какая версия Python 3 установлена в вашей системе, откройте


оболочку системы и выполните команду
$ python3 --version
Измените python3 на python в Windows. Это вернет версию Python 3 в вашей
системе. Если ваша версия ниже Python 3.7 или у вас не установлен Python, загляните
на сайт www.python.org/downloads/, чтобы получить последнюю версию.

Совет. Для тех читателей, которые не хотят удалять текущую версию Python
и хотят управлять несколькими версиями Python в своей системе, обратите
внимание на инструмент управления версиями Python, pyenv.

Установка PyQt6
Поскольку PyQt не входит в комплект поставки Python, следующим шагом будет
использование pip для установки пакета PyQt6 из Python Package Index (PyPI). Для
создания связки между Python и C++ используется инструмент SIP binding generator.
При загрузке PyQt6 из PyPI автоматически загружается и модуль sip.
Чтобы установить PyQt6, введите в командную оболочку следующую команду:
$ pip3 install PyQt6
Если вы используете Windows, вам, вероятно, придется изменить pip3 на pip.
Чтобы убедиться, что PyQt загружен правильно, откройте интерпретатор Python 3,
введя python3 (python для Windows) в командной строке. Затем введите следующую
команду:
>>> import PyQt6

4 ГЛАВА 1 НАЧАЛО РАБОТЫ С PYQT


Совет. На протяжении всего курса этой книги вы создадите несколько
графических интерфейсов PyQt. Для тех читателей, которые заинтересованы
в управлении различными проектами PyQt и их зависимостями, обратите
внимание на использование виртуальных сред и модуля Python venv.

Если ошибок не возникло, мы можем двигаться дальше и узнать немного больше


о пользовательских интерфейсах.

Введение в пользовательские интерфейсы


Пользовательский интерфейс (UI) стал ключевой частью нашей повседневной
жизни, став посредником между нами и постоянно растущим количеством машин.
Пользовательский интерфейс предназначен для облегчения взаимодействия
человека и компьютера. Человеку необходимо управлять машиной для достижения
определенных целей; в то же время машина должна одновременно предоставлять
обратную связь и средства взаимодействия с ней, чтобы помочь человеку в
процессе принятия решений. Пользовательские интерфейсы встречаются повсюду
- от мобильных приложений на наших телефонах до веб-браузеров, управления
тяжелой техникой и даже бытовой техникой на наших кухнях. Конечно, способы
взаимодействия с технологиями не ограничиваются только руками, поскольку
многие пользовательские интерфейсы позволяют взаимодействовать и с другими
органами чувств.
Задача хорошего пользовательского интерфейса - помочь человеку получить
желаемый результат, а также обеспечить более простую, эффективную и
дружелюбную работу машины. Подумайте о приложениях для редактирования
фотографий на вашем телефоне. Редактирование размера, цвета или экспозиции
практически не требует усилий: вы проводите пальцами по экрану и наблюдаете,
как изображение меняется практически мгновенно. Для достижения желаемого
результата пользователь прикладывает минимум усилий.

Что такое графический интерфейс пользователя?


В этой книге мы сосредоточимся на создании графических пользовательских
интерфейсов (GUI), которые используют графические возможности компьютера
для создания визуальных приложений. Десятилетия назад для взаимодействия
с компьютером пользователям приходилось использовать командную строку и
текстовые команды. Такие задачи, как открытие, удаление и перемещение файлов,
поиск в каталогах, выполнялись путем ввода определенных команд. Однако
такие интерфейсы были не очень удобными и простыми в использовании для
широкой публики. Поэтому были созданы графические интерфейсы, позволяющие
пользователям взаимодействовать с электронными устройствами с помощью

ГЛАВА 1 НАЧАЛО РАБОТЫ С PYQT 5


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

Концепции создания хорошего дизайна интерфейса


Прежде всего, это техническая книга, написанная в помощь тем из вас, кто хочет
научиться создавать и кодировать свои собственные графические интерфейсы
с помощью PyQt и Python. Тем не менее, если вы планируете разрабатывать
пользовательский интерфейс любого типа, которым будут пользоваться другие
люди, то вы больше не создаете пользовательский интерфейс только для решения
своих собственных проблем. Вы также должны начать учитывать интересы других
пользователей приложения. Подумайте о том, чего вы хотите, чтобы они достигли,
или как приложение может им помочь. Иногда, пытаясь решить проблему, мы
настолько увлекаемся созданием продукта, что забываем о людях, которым придется
с ним взаимодействовать.
Ниже приведен список концепций, которые следует учитывать при разработке
собственного пользовательского интерфейса. Это не установленные правила и ни
в коем случае не полный список, а скорее идеи, которые следует учитывать при
разработке собственных приложений.
1. Ясность - использование четкого языка, иерархии и потока визуальных
элементов, чтобы избежать двусмысленности. Одним из способов
достижения этого является учет визуальной важности для человеческого
глаза, размещение виджетов большего размера, более темных цветов и
т.д. таким образом, чтобы мы могли визуально понять пользовательский
интерфейс.
2. Краткость - упрощение макета, чтобы включить только то, что пользователь
должен видеть или с чем взаимодействовать в данный момент времени,
чтобы быть кратким, но в то же время исчерпывающим. Добавление
дополнительных ярлыков или кнопок в окно только для того, чтобы дать
пользователю больше возможностей, не всегда лучше.
3. Последовательность - Разработайте пользовательский интерфейс так, чтобы
он был последовательным во всем приложении. Это помогает пользователям
распознавать закономерности в визуальных элементах и макете; это может
проявляться в типографике, которая улучшает навигацию и читабельность
приложения, стилях изображений или даже цветовых схемах.
4. Эффективность - Использование хорошего дизайна и быстрых клавиш
помогает пользователю повысить производительность. Если задачу можно
выполнить за два шага, зачем создавать графический интерфейс так, чтобы
работу пришлось выполнять за пять?
5. Знакомость - подумайте об элементах, которые пользователи обычно

6 ГЛАВА 1 НАЧАЛО РАБОТЫ С PYQT


видят в других пользовательских интерфейсах, и о том, как они ожидают
их увидеть в вашем приложении. Например, подумайте о том, как странно
было бы вводить данные для входа в систему и обнаружить, что поле ввода
пароля находится над полем ввода имени пользователя. В этом нет ничего
неправильного, но теперь вы без необходимости заставляете пользователей
обдумывать свои действия и замедляете их.
6. Отзывчивость - Предоставьте пользователю обратную связь, например,
тумблер, меняющий цвет на “вкл” или “выкл”, небольшое сообщение,
уведомляющее пользователя о правильности или неправильности ввода,
или даже звуковой эффект для подтверждения выполненного действия.
Пользователь никогда не должен оставаться в недоумении, было ли его
действие успешным или нет.

Создание первого графического интерфейса: Пустое


окно
Приложение с графическим интерфейсом обычно состоит из главного окна
и, возможно, одного или нескольких диалоговых окон. Главное окно - это место,
где пользователь будет проводить большую часть времени при работе с вашим
приложением, и оно может состоять из строки меню, строки состояния и других
виджетов. Диалоговые окна обычно состоят из текста, одного или нескольких
виджетов для сбора информации и кнопок. Они появляются перед пользователем,
когда это необходимо, чтобы передать информацию и попросить его ввести данные.
Примером диалогового окна является всплывающее окно с вопросом, хотите ли
вы сохранить изменения в документе. Диалоговые окна будут рассмотрены далее в
Главе 3.
Для вашего первого проекта, показанного на Рисунке 1-1, мы рассмотрим
следующие вопросы
• Как создать пустое окно в PyQt6
• Основные классы и модули, необходимые для создания графического
интерфейса пользователя
• Как изменить размер и заголовок главного окна.

ГЛАВА 1 НАЧАЛО РАБОТЫ С PYQT 7


Рисунок 1-1. Пустое окно, созданное с помощью PyQt6 (вид в macOs)
Это приложение послужит основой для всех остальных программ, описанных в
этой книге.

Объяснение создания пустого окна


Код, приведенный в листинге 1-1, - это все, что вам нужно для создания окна
в PyQt6. Примеры в этой книге будут использовать преимущества объектно-
ориентированного программирования (ООП), парадигмы программирования,
которая фокусируется на использовании классов для создания экземпляров
этих классов, или объектов, с их собственными свойствами и поведением, и
моделировании отношений между другими объектами.
Листинг 1-1. Создание пустого окна в PyQt
# basic_window.py
# Импортируйте необходимые модули
import sys
from PyQt6.QtWidgets import QApplication, QWidget

class EmptyWindow(QWidget):
def __init__(self):
"""Конструктор для класса "Пустое окно"""
super().__init__()
self.initializeUI()

def initializeUI(self):
"""Настройка приложения."""
self.setGeometry(200, 100, 400, 300)
self.setWindowTitle("Пустое окно в PyQt")
self.show() # Отображение окна на экране

8 ГЛАВА 1 НАЧАЛО РАБОТЫ С PYQT


# Запустить программу
if __name__ == '__main__':
app = QApplication(sys.argv)
window = EmptyWindow()
sys.exit(app.exec())

Ваше начальное окно должно выглядеть так же, как на Рисунке 1-1, в зависимости
от вашей операционной системы.
Проходя по коду, мы сначала начнем с импорта модулей sys и PyQt6, которые нам
нужны для создания окна. Модуль sys может использоваться в PyQt для передачи
аргументов командной строки нашим приложениям и для их закрытия.
Модуль QtWidgets предоставляет классы виджетов, которые понадобятся для
создания графических интерфейсов в стиле рабочего стола. Из модуля QtWidgets
мы импортируем два класса: QApplication и QWidget. Вам нужно создать только один
экземпляр класса QApplication, независимо от того, сколько окон или диалоговых окон
существует в приложении. QApplication отвечает за управление главным циклом
событий приложения, а также за инициализацию и завершение работы виджетов.
Главный цикл событий - это место, где происходит управление взаимодействием
пользователя с окном графического интерфейса, например, нажатие на кнопку.
Взгляните на
app = QApplication(sys.argv)
QApplication принимает в качестве аргумента sys.argv. Вы также можете передать
пустой список, если вы знаете, что ваша программа не будет принимать никаких
аргументов командной строки, используя
app = QApplication([])

Совет. Всегда создавайте объект QApplication вашего графического


интерфейса перед любым другим объектом, принадлежащим графическому
интерфейсу, включая главное окно. Эта концепция продемонстрирована в
Листинге 1-2.

ГЛАВА 1 НАЧАЛО РАБОТЫ С PYQT 9


Далее мы создаем объект окна, который наследуется от созданного нами класса
EmptyWindow. Наш класс фактически наследуется от QWidget, который является
базовым классом, от которого происходят все остальные объекты пользовательского
интерфейса, такие как виджеты и окна.

Совет. При создании окон в PyQt вы обычно создаете основной класс, который
наследуется от QMainWindow, QWidget или QDialog. Подробнее о каждом из
этих классов и о том, когда их использовать для создания окон и диалоговых
окон, вы узнаете в последующих главах.

Нам нужно вызвать метод show() для объекта окна, чтобы вывести его на экран.
Он находится внутри функции initializeUI() в нашем классе EmptyWindow. Вы можете
видеть, как app.exec() передается в качестве аргумента в sys.exit() в последней строке
Листинга 1-1. Метод exec() запускает цикл событий приложения и будет находиться
в нем до тех пор, пока вы не выйдете из приложения. Функция sys.exit() обеспечивает
чистый выход.
Шаги по созданию окна лучше проиллюстрированы в Листинге 1-2 с помощью
процедурного программирования - парадигмы программирования, в которой
компьютер выполняет набор последовательных команд для выполнения задачи.
Листинг 1-2. Минимальный код, необходимый для создания пустого окна в PyQt
без ООП
# procedural.py
# 1. Импортируйте необходимые модули
import sys # использовать sys для приема аргументов командной строки
from PyQt6.QtWidgets import QApplication, QWidget

app = QApplication(sys.argv) # 2. Создайте объект QApplication


window = QWidget() # 3. Создать окно из объекта QWidget
window.show() # 4. Вызываем show для отображения окна графического интерфейса
пользователя
# 5. Запустите цикл событий. Используйте sys.exit для закрытия программы
sys.exit(app.exec())

В следующем разделе показано, как использовать встроенные методы PyQt для


изменения размера главного окна и установки заголовка окна.

Изменение окна
Класс EmptyWindow в Листинге 1-1 содержит метод initializeUI(), который создает
окно на основе заданных нами параметров. Функция initializeUI() воспроизводится в
следующем фрагменте кода:

10 ГЛАВА 1 НАЧАЛО РАБОТЫ С PYQT


def initializeUI(self):
"""Инициализируем окно и выводим его содержимое на экран."""
self.setGeometry(200, 100, 400, 300)
self.setWindowTitle('Пустое окно в PyQt')
self.show()

Метод setGeometry() определяет расположение окна на экране вашего компьютера и


его размеры, ширину и высоту. Так, окно, которое мы только что создали, расположено
в окне x=200, y=100 и имеет ширину=400 и высоту=300. Метод setWindowTitle()
используется для установки заголовка окна. Уделите время изменению значений
геометрии или текста заголовка и посмотрите, как ваши изменения повлияют на
окно. Вы также можете закомментировать эти два метода и посмотреть, как PyQt
использует параметры по умолчанию для размера и заголовка окна.
Мы рассмотрим дальнейшую настройку макета окна в Главе 4 и внешнего вида в
Главе 6.

Резюме
В этой главе мы рассмотрели, как подготовиться к созданию графических
интерфейсов с помощью PyQt6. Создание удобных графических интерфейсов
очень важно, и есть несколько понятий, о которых следует помнить при разработке
приложений, таких как последовательность и ясность, чтобы помочь пользователям
понять назначение и возможности вашего приложения. Наконец, мы рассмотрели
основные классы и методы, необходимые для создания и модификации простого
главного окна.
В следующей главе вы узнаете больше о разработке графического интерфейса
пользователя. Вы узнаете, как добавлять текст и изображения в графические
интерфейсы с помощью виджета QLabel, а также изучите один из методов
расположения виджетов в окнах.

ГЛАВА 1 НАЧАЛО РАБОТЫ С PYQT 11


ГЛАВА 2

Создание простого
графического
интерфейса
И снова здравствуйте! Графические интерфейсы предназначены для выполнения
конкретных задач, таких как написание и редактирование документов или
воспроизведение видео. Создание любого пользовательского интерфейса может
показаться сложной задачей, поскольку необходимо учитывать всевозможные
виджеты. Виджеты - это кнопки, флажки, ползунки и другие компоненты, с помощью
которых пользователи взаимодействуют с графическим интерфейсом.
Для того чтобы помочь вам узнать о различных видах виджетов и понять, когда вы
можете захотеть их использовать, в каждой главе мы будем использовать различные
виджеты и применять их в одном или нескольких проектах GUI. Каждый раз, когда
будет представлен новый виджет, вы также будете применять этот компонент в
небольших практических приложениях, прежде чем использовать их в более крупных
графических интерфейсах. Для некоторых больших приложений мы также обсудим
процесс проектирования и расположения виджетов в окне приложения.
В этой главе вы
• Начнете изучать виджеты в PyQt и узнаете, как использовать их в своих
графических интерфейсах
• Рассмотрите процесс проектирования простого приложения с графическим
интерфейсом пользователя
• Построите базовые графические интерфейсы, используя в основном виджет
QLabel
• Узнайте, как организовать виджеты в графическом интерфейсе с помощью
метода move().
Давайте начнем с изучения очень фундаментального виджета.

12 ГЛАВА 2 СОЗДАНИЕ ПРОСТОГО ГРАФИЧЕСКОГО ИНТЕРФЕЙСА


Виджет QLabel
После того как в Главе 1 мы научились создавать окно, мы можем двигаться
дальше и добавлять больше функциональности с помощью виджетов. В этой главе
мы сосредоточимся в основном на использовании QLabel, поскольку это виджет,
который вы будете использовать почти в каждом разрабатываемом вами графическом
интерфейсе. Объект QLabel действует как нередактируемое место для отображения
обычного или насыщенного текста, гиперссылок, изображений или GIF. Он также
полезен для создания меток вокруг других виджетов, чтобы указать их роли или дать
им названия.

GUI, который вы создадите, показанный на Рисунке 2-1, демонстрирует, как


использовать QLabel для создания текстовых и графических меток, и будет служить
в качестве Hello World для добавления виджетов в PyQt.

Рисунок 2-1. Пример использования виджетов QLabel для размещения изображений


и текста в окне

Примечание. Для этого и других примеров в этой главе вам потребуется


загрузить папку images и ее содержимое из репозитория GitHub.

ГЛАВА 2 СОЗДАНИЕ ПРОСТОГО ГРАФИЧЕСКОГО ИНТЕРФЕЙСА 13


Объяснение использования QLabel
Давайте начнем со сценария пустого окна, который вы создали в Главе 1, и
используем его в качестве основы для создания Листинга 2-1.

Примечание. По мере усложнения приложений в этой книге, программы


будут разбиваться на более управляемые части и собираться по частям,
чтобы способствовать обучению и пониманию кода. Многие программы будут
начинаться с использования сценария пустого окна из главы 1 в качестве
отправной точки. Если во время выполнения какого-либо участка кода вы
получите ошибку из-за отсутствия метода или переменной, не бойтесь.
Продолжайте кодировать и следовать за каждым участком кода, чтобы
создать полное приложение. Например, для завершения этой программы
вам понадобится код из листингов 2-1 - 2-2.

Листинг 2-1. Настройка главного окна для демонстрации использования


виджетов QLabel
# labels.py
# Импортируйте необходимые модули
import sys
from PyQt6.QtWidgets import QApplication, QWidget, QLabel
from PyQt6.QtGui import QPixmap

class MainWindow(QWidget):
def __init__(self):
super().__init__()
self.initializeUI()

def initializeUI(self):
"""Настройте графический интерфейс приложения."""
self.setGeometry(100, 100, 250, 250)
self.setWindowTitle("Пример QLabel")

self.setUpMainWindow()
self.show()

if __name__ == '__main__':
app = QApplication(sys.argv)
window = MainWindow()
sys.exit(app.exec())

14 ГЛАВА 2 СОЗДАНИЕ ПРОСТОГО ГРАФИЧЕСКОГО ИНТЕРФЕЙСА


Сначала импортируем необходимые нам модули. Для создания окна нам нужно
импортировать еще один класс PyQt из модуля QtWidgets, класс QLabel.
На этот раз нам также нужно импортировать модуль QtGui. Модуль QtGui работает
с многочисленными графическими элементами, используемыми в графических
интерфейсах. QPixmap - это класс Qt, который оптимизирован для показа
изображений на экране и полезен для отображения изображения на объекте QLabel.
Вам нужно будет создать класс MainWindow, который наследуется от QWidget. Если
вы скопировали сценарий из Главы 1, просто измените имя класса EmptyWindow
на MainWindow. Далее инициализируйте размер окна с помощью setGeometry() и
установите заголовок нашего графического интерфейса с помощью setWindowTitle().

Примечание. Qt наполнен многочисленными методами класса, называемыми


accessors, также называемыми getters, для получения значений и mutators,
также называемыми setters, для изменения значений. Вы уже видели два
примера с сеттерами. Чтобы изменить размер виджета или виджет, вы можете
использовать setter setGeometry(). Если бы вы хотели получить это значение
в любое время, вы могли бы использовать getter geometry(). Методы setter
и getter следуют в PyQt такому шаблону, когда setter имеет в имени метода
слово set, а getter убирает слово set и заменяет первую букву на строчную.

Далее вызовите метод setUpMainWindow(), который используется не только для


настройки и расположения виджетов в главном окне, но и для структурирования
кода. Этот метод создан в Листинге 2-2. Затем мы используем метод show() для
отображения окна. Чтобы настроить приложение, сначала создайте объект
QApplication. Затем инициируйте окно. Далее, используйте exec() для начала цикла
событий. Наконец, sys.exit() используется для безопасного закрытия программы.
Следующее, что нужно сделать, это создать метод MainWindow, setUpMainWindow().

Примечание. Согласно PEP 8, Руководству по стилю кода Python, имена


функций должны быть в нижнем регистре и разделяться подчеркиванием.
В руководстве также говорится, что смешанный регистр также допускается,
если это преобладающий стиль. В этой книге для имен функций и методов
будет использоваться смешанный регистр, чтобы следовать стилю,
используемому в Qt и PyQt. Более подробную информацию о PEP 8 можно
найти на сайте www.python.org/dev/peps/pep-0008/#prescriptive-naming-conventions.

ГЛАВА 2 СОЗДАНИЕ ПРОСТОГО ГРАФИЧЕСКОГО ИНТЕРФЕЙСА 15


Листинг 2-2. Метод setUpMainWindow() для отображения текстовых и графических
меток
# labels.py
def setUpMainWindow(self):
"""Создайте QLabel для отображения в главном окне."""
hello_label = QLabel(self)
hello_label.setText("Прювет!")
hello_label.move(105, 15)
image = "images/world.png"
try:
with open(image):
world_label = QLabel(self)
pixmap = QPixmap(image)
world_label.setPixmap(pixmap)
world_label.move(25, 40)
except FileNotFoundError as error:
print(f"Image not found.\nError: {error}")

Сначала необходимо создать объект QLabel. Передавая self в качестве параметра


в QLabel, вы устанавливаете класс MainWindow в качестве родителя метки. Это
будет полезно для отображения и расположения виджета в родительском объекте.
Далее укажите, что будет написано на ярлыке, используя setText(). Здесь для текста
установлено значение "Hello". В следующей строке мы используем метод move(),
чтобы расположить ярлык в окне.
В PyQt есть несколько методов компоновки, включая горизонтальную компоновку,
компоновку сеткой и абсолютное позиционирование. Мы подробно рассмотрим
эти классы в Главе 4. В программах, созданных в этой главе, мы будем использовать
абсолютное позиционирование с помощью метода move(). В методе move() вам
нужно указать только значения x и y в пикселях, где вы хотите разместить виджеты.
Представьте главное окно в виде графика, левый верхний угол которого - точка
(0,0). Значения x и y, которые вы выбираете в move(), относятся к точке, которая
находится в левом верхнем углу виджета в главном окне. Для нашей текстовой метки
мы задаем значения x=105 и y=15. Это определенно не лучший метод расположения
виджетов в окне по ряду причин. Во-первых, он сложен и предполагает использование
метода проб и ошибок для установки положения виджета. Другая причина связана
с изменением размера окна. Если вы измените размер окна, потянув за правый
нижний угол, вы заметите, что виджеты не перемещаются и не растягиваются.
Классы компоновки Qt отлично подходят для решения этой и других проблем. Мы
обсудим использование классов компоновки в Главе 4.
Вы можете подумать, что изучение использования move() - пустая трата времени,
но это может быть очень полезно для понимания того, как использовать пиксельные
значения для манипулирования виджетами, особенно когда мы начнем работать с
более продвинутыми темами, такими как анимация и графические классы.
Изображение загружается аналогичным образом, создавая объект world_label,

16 ГЛАВА 2 СОЗДАНИЕ ПРОСТОГО ГРАФИЧЕСКОГО ИНТЕРФЕЙСА


который будет помещен в главное окно. Затем мы создаем QPixmap изображения и
используем setPixmap() для установки изображения, отображаемого на world_label.
Абсолютное местоположение изображения устанавливается с помощью функции
move(). Если изображение не может быть найдено, возникает исключение.
Каждый из различных классов PyQt имеет свои методы, которые можно
использовать для настройки и изменения внешнего вида и функциональности. В
Приложении вы можете найти список виджетов, используемых в этой книге, а также
некоторые из наиболее распространенных методов, которые вы, скорее всего, будете
использовать для их изменения.
После запуска программы на экране должно появиться окно, как на Рисунке 2-1. В
следующем разделе вы построите несколько более сложный графический интерфейс
пользователя, используя виджеты QLabel.

Проект 2.1 - Графический интерфейс профиля


пользователя
Профиль пользователя используется для визуального отображения личных
данных. Данные в профиле помогают ассоциировать определенные характеристики
с пользователем и помогают другим узнать больше о нем. В зависимости от цели
приложения, информация и внешний вид профиля будут меняться.
Профили пользователей, подобные представленному на Рисунке 2-2, часто
имеют ряд параметров, которые являются обязательными или необязательными
и допускают определенный уровень настройки в соответствии с предпочтениями
пользователя, например, изображение профиля или цвета фона. Многие из них
содержат аналогичные функции, такие как имя пользователя или раздел "О себе".

Рисунок 2-2. Графический интерфейс профиля пользователя, отображающий


информацию о пользователе

ГЛАВА 2 СОЗДАНИЕ ПРОСТОГО ГРАФИЧЕСКОГО ИНТЕРФЕЙСА 17


В следующем разделе мы разберем Рисунок 2-2 и подумаем о том, как виджеты
ярлыков будут расположены в окне.

Проектирование графического интерфейса


профиля пользователя
Типичные приложения профиля пользователя часто используют комбинацию
различных элементов, как интерактивных, так и статических. Схема на Рисунке 2-3
сосредоточена на использовании исключительно статических виджетов QLabel для
отображения информации в окне.
Если вы сравните Рисунок 2-3 с Рисунком 2-2, вы заметите сходство в том, как
они расположены. Пользовательский интерфейс можно разделить на две части. В
верхней части используются объекты QLabel, которые отображают изображение
профиля, расположенное поверх фонового изображения.

Рисунок 2-3. Схема графического интерфейса пользовательского профиля


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

Пояснения к графическому интерфейсу профиля


пользователя
Как и в предыдущем приложении, мы начнем с использования шаблона GUI из
Главы 1 в качестве основы для главного окна профиля пользователя в Листинге 2-3.

18 ГЛАВА 2 СОЗДАНИЕ ПРОСТОГО ГРАФИЧЕСКОГО ИНТЕРФЕЙСА


Листинг 2-3. Код для настройки главного окна GUI пользовательского профиля
# user_profile.py
# Import necessary modules
import sys
from PyQt6.QtWidgets import QApplication, QWidget, QLabel
from PyQt6.QtGui import QFont, QPixmap
class MainWindow(QWidget):
def __init__(self):
super().__init__()
self.initializeUI()

def initializeUI(self):
"""Настройки графического интерфейса приложения."""
self.setGeometry(50, 50, 250, 400)
self.setWindowTitle("2.1 - User Profile GUI")
self.setUpMainWindow()
self.show()

# Run program
if __name__ == '__main__':
app = QApplication(sys.argv)
window = MainWindow()
sys.exit(app.exec())

Для GUI пользовательского профиля импортируйте те же классы и модули, что и


в предыдущем приложении, добавив один новый класс, класс QFont из модуля QtGui,
который позволяет изменять размер и типы шрифтов в нашем приложении. Это
идеально подходит для создания различных размеров заголовков.
Прежде чем создавать setUpMainWindow(), давайте создадим отдельный метод
в MainWindow, показанный в Листинге 2-4, который будет обрабатывать загрузку
различных изображений и создание объектов QLabel для их отображения.
Листинг 2-4. Код для функции createImageLabels() в графическом интерфейсе
профиля пользователя
# user_profile.py
def createImageLabels(self):
"""Открывает файлы изображений и создаёт метки изображений."""
images = ["images/skyblue.png",
"images/profile_image.png"]
for image in images:
try:
with open(image):
label = QLabel(self)
pixmap = QPixmap(image)

ГЛАВА 2 СОЗДАНИЕ ПРОСТОГО ГРАФИЧЕСКОГО ИНТЕРФЕЙСА 19


label.setPixmap(pixmap)
if image == "images/profile_image.png":
label.move(80, 20)
except FileNotFoundError as error:
print(f"Image not found.\nError: {error}")

Список изображений(images) содержит определенные местоположения файлов,


которые будут использоваться как для синего фона, так и для изображения
профиля пользователя в верхней части окна. Используя цикл for, пройдитесь по
элементам списка, создайте объект QLabel для каждого, инстанцируйте объект
QPixmap, установите pixmap для метки(label), и если изображение(image) является
изображением профиля, отцентрируйте его в окне с помощью move(). Используя
move() и абсолютное позиционирование, вы можете легко перекрывать изображения,
но вам нужно будет загружать изображения в порядке от самого нижнего к самому
верхнему.
Теперь мы можем создать метод MainWindow setUpMainWindow() в Листинге 2-5,
где будет вызываться createImageLabels().
Listing 2-5. Code for the User Profile GUI’s setUpMainWindow() method
# user_profile.py
def setUpMainWindow(self):
"""Создайте метки, которые будут отображаться в окне."""
self.createImageLabels()
user_label = QLabel(self)
user_label.setText("Иван Драго")
user_label.setFont(QFont("Arial", 20))
user_label.move(85, 140)
bio_label = QLabel(self)
bio_label.setText("Биография")
bio_label.setFont(QFont("Arial", 17))
bio_label.move(15, 170)
about_label = QLabel(self)
about_label.setText("Я инженер-программист с 10-летним\
опытом создания потрясающего кода.")
about_label.setWordWrap(True)
about_label.move(15, 190)

После создания меток изображений инстанцируются несколько объектов QLabel


для отображения текста. Например, метка user_label отображает имя пользователя с
помощью setText() в окне. Вы можете установить шрифт виджета QLabel с помощью
метода setFont(). Обязательно передайте объект QFont и укажите тип шрифта и его
размер. Затем метка user_label центрируется в окне с помощью функции move().
Другие метки создаются аналогичным образом.
Листинг 2-6 продолжает создание и расположение виджетов QLabel в главном
окне.
20 ГЛАВА 2 СОЗДАНИЕ ПРОСТОГО ГРАФИЧЕСКОГО ИНТЕРФЕЙСА
Листинг 2-6. Размещение дополнительных меток в методе setUpMainWindow()
# user_profile.py
skills_label = QLabel(self)
skills_label.setText("Уменияs")
skills_label.setFont(QFont("Arial", 17))
skills_label.move(15, 240)
languages_label = QLabel(self)
languages_label.setText("Python | PHP | SQL | JavaScript")
languages_label.move(15, 260)

Создаются новые метки. Обратите внимание, что значение x в move() остается


равным 15, оставляя небольшое пространство в левой части окна, а значение
y постепенно увеличивается, размещая каждую последующую метку ниже. В
Листинге 2-7 к графическому интерфейсу добавлены дополнительные метки.
Листинг 2-7. Размещение еще большего количества меток в методе
setUpMainWindow()
# user_profile.py
experience_label = QLabel(self)
experience_label.setText("Опыт")
experience_label.setFont(QFont("Arial", 17))
experience_label.move(15, 290)

developer_label = QLabel(self)
developer_label.setText("Python Разработчик")
developer_label.move(15, 310)

dev_dates_label = QLabel(self)
dev_dates_label.setText("Март 2011 - настоящее время")
dev_dates_label.setFont(QFont("Arial", 10))
dev_dates_label.move(15, 330)

driver_label = QLabel(self)
driver_label.setText("Водитель доставки пиццы")
driver_label.move(15, 350)

driver_dates_label = QLabel(self)
driver_dates_label.setText("Aug 2015 - Dec 2017")
driver_dates_label.setFont(QFont("Arial", 10))
driver_dates_label.move(15, 370)

Запустив приложение, вы увидите окно, как показано на Рисунке 2-2.


Прим. переводчика. В код программы внесены небольшие изменения в координаты и
размеры шрифтов.

ГЛАВА 2 СОЗДАНИЕ ПРОСТОГО ГРАФИЧЕСКОГО ИНТЕРФЕЙСА 21


Резюме
В этой главе мы узнали, как добавлять и располагать виджеты в окне графического
интерфейса. Виджет QLabel является фундаментальным классом и не только отлично
подходит для отображения текста, но также может использоваться с другими
классами PyQt, такими как QPixmap для отображения изображений или QFont для
изменения стиля или размера текста ярлыка. Каждый из классов PyQt включает
различные методы для расширения своих возможностей и внешнего вида. Примеры
этих методов можно найти в Приложении.
В следующей главе мы рассмотрим несколько различных классов виджетов,
включая QPushButton и QLineEdit, которые позволят пользователям взаимодействовать
с приложениями, которые вы разрабатываете.

22 ГЛАВА 2 СОЗДАНИЕ ПРОСТОГО ГРАФИЧЕСКОГО ИНТЕРФЕЙСА


ГЛАВА 3

Добавление
дополнительной
функциональности с
помощью виджетов
Что хорошего в пользовательском интерфейсе, если он не интерактивен?
Эта глава посвящена изучению того, как использовать виджеты для создания
отзывчивых пользовательских интерфейсов, которые реагируют на взаимодействие с
пользователем, обрабатывают различные события и передают важную информацию
обратно пользователю. Мы рассмотрим несколько распространенных виджетов
и увидим, как использовать их для проектирования и создания приложений с
графическим интерфейсом.
В этой главе вы
• Познакомитесь с обработкой событий и механизмом сигналов и слотов Qt
• Создадите графические интерфейсы, используя новые классы виджетов,
включая QPushButton, QLineEdit, QCheckBox и QMessageBox
• Узнайте о полезных методах выравнивания текста и настройки размеров
виджетов.
• Узнайте больше об окнах и диалоговых окнах и посмотрите, как создавать
классы, наследующие от QDialog
• Создайте приложение, которое научит работать с несколькими окнами.
Прежде чем перейти к коду, давайте узнаем немного об обработке событий в PyQt.

Обработчики событий, сигналы и слоты


Графические интерфейсы управляются событиями, то есть они реагируют на
события, создаваемые пользователем, с клавиатуры или мыши, или на события,
вызываемые системой, например, таймером или при подключении к Bluetooth.
В Qt даже генерируются специальные виды событий для взаимодействия между

ГЛАВА 3 ДОБАВЛЕНИЕ ФУНКЦИОНАЛЬНОСТИ С ПОМОЩЬЮ ВИДЖЕТОВ 23


виджетами. Независимо от того, как они генерируются, приложение должно
прослушивать эти события и реагировать на них соответствующим образом. Это
известно как обработка событий. Когда вызывается exec(), приложение начинает
прослушивать события до тех пор, пока программа не будет закрыта.
В PyQt обработка событий осуществляется одним из двух способов - либо с
помощью обработчиков событий, либо с помощью сигналов и слотов. Обработчики
событий заботятся о событиях. Существуют различные типы событий, которые
можно обрабатывать, например paintEvent() для перекрашивания внешнего вида
виджета или keyPressEvent() для обработки нажатия клавиш. В Qt события - это
объекты, созданные на основе класса QEvent.
Связь между объектами в Qt, такими как виджеты, обрабатывается с помощью
сигналов и слотов. Сигналы генерируются всякий раз, когда происходит событие,
например, когда нажимается кнопка или переключается флажок. Затем эти
сигналы необходимо каким-то образом обработать. Слоты - это методы, которые
подключаются к событию и выполняются в ответ на сигнал. Слоты могут быть либо
встроенными функциями PyQt, либо функциями Python, которые вы создаете сами.
Каждый класс PyQt имеет свой собственный набор сигналов, и многие из них
наследуются от родительских классов. Давайте рассмотрим пример. Каждый раз,
когда пользователь нажимает на кнопку в окне, эта кнопка посылает сигнал, или
излучает сигнал:
button.clicked.connect(self.buttonClicked)
Здесь button - это виджет, а clicked - сигнал. Чтобы использовать этот сигнал, мы
должны с помощью connect() вызвать некоторую функцию, которой в данном случае
является buttonClicked(), являющаяся слотом. Метод buttonClicked() может выполнить
какое-то действие, например, открыть новое окно. Многие сигналы также передают
в слот дополнительную информацию, например, булево значение, которое говорит о
том, была ли нажата кнопка или нет.
Сигналы и слоты, а также создание собственных сигналов будут рассмотрены
в Главе 7. А пока давайте рассмотрим виджет, который идеально подходит для
демонстрации сигналов и слотов.

Виджет QPushButton
Виджет QPushButton можно использовать для выполнения действий и выбора.
Когда вы нажимаете на виджет QPushButton, он посылает сигнал, который может
быть подключен к функции. Хотя вы обычно встречаете кнопки с текстом: OK, Next,
Cancel, Close, Yes или No, вы также можете создавать свои собственные кнопки с
описательным текстом или значками.

24 ГЛАВА 3 ДОБАВЛЕНИЕ ФУНКЦИОНАЛЬНОСТИ С ПОМОЩЬЮ ВИДЖЕТОВ


Примечание. Существуют различные типы классов кнопок с разными
назначениями, например QToolButton для выбора элементов на панелях
инструментов и QRadioButton для создания групп кнопок, где можно сделать
только один выбор.

В этом первом примере вы создадите кнопку QPushButton, которая при нажатии


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

Объяснение использования QPushButton


Откройте новый файл и скопируйте в него код из сценария пустого окна из
Главы 1. Как вы видите в Листинге 3-1, вам потребуется импортировать еще
несколько классов, включая класс QPushButton из QtWidgets. Модуль QtCore содержит
множество классов, не связанных с пользовательским интерфейсом. Класс Qt
относится к пространству имен Qt, которое содержит множество идентификаторов,
используемых для задания свойств виджетов и других классов.
Листинг 3-1. Настройка главного окна для использования виджетов QPushButton
# buttons.py
# Импортируйте необходимые модули
import sys
from PyQt6.QtWidgets import (QApplication, QWidget, QLabel, QPushButton)
from PyQt6.QtCore import Qt

class MainWindow(QWidget):
def __init__(self):
super().__init__()
self.initializeUI()

def initializeUI(self):
"""Set up the application's GUI."""
self.setGeometry(100, 100, 250, 150)
self.setWindowTitle("QPushButton Example")
self.setUpMainWindow()
self.show()

if __name__ == '__main__':
app = QApplication(sys.argv)
window = MainWindow()
sys.exit(app.exec())

ГЛАВА 3 ДОБАВЛЕНИЕ ФУНКЦИОНАЛЬНОСТИ С ПОМОЩЬЮ ВИДЖЕТОВ 25


Обязательно установите начальные позиции x и y главного окна и его размер с
помощью функции setGeometry(). Затем установите заголовок окна и вызовите метод
setUpMainWindow(), который мы создадим в Листинге 3-2.
Листинг 3-2. Метод setUpMainWindow() для использования кнопок
# buttons.py
def setUpMainWindow(self):
"""Создание и расположение виджетов в главном окне."""
self.times_pressed = 0
self.name_label = QLabel("Не нажимайте кнопку.", self)
self.name_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.name_label.move(60, 30)
self.button = QPushButton("Нажми меня", self)
self.button.move(80, 70)
self.button.clicked.connect(self.buttonClicked)

Переменная times_pressed будет использоваться для отслеживания того, сколько


раз была нажата кнопка. Окно этого приложения содержит только QLabel и
QPushButton. Вместо того чтобы использовать setText() для назначения текста для
name_label, мы можем передать текст, который хотим отобразить, в качестве первого
аргумента при инстанцировании объекта QLabel.

Можно выровнять содержимое виджетов, отображающих текст. Для этого


используйте setAlignment(), и поскольку мы используем PyQt6, убедитесь, что
передали полный тип перечисления, Qt.AlignmentFlag. Существуют различные типы
флагов выравнивания, вот некоторые из них
• AlignLeft - выравнивает текст по левому краю
• AlignRight - выравнивание текста по правому краю
• AlignHCenter и AlignVCenter - выравнивает текст по горизонтали и вертикали,
соответственно.
• AlignTop и AlignBottom - выравнивание текста по верху и по низу, соответственно.
Здесь мы используем AlignCenter, который является комбинацией AlignVCenter и
AlignHCenter. Используйте move() для установки абсолютной позиции виджета.

Примечание. Вместо того чтобы использовать сеттеры, многие свойства


виджетов можно установить, передав их в качестве аргументов экземпляру
виджета. Например, вместо использования функции setAlignment()
можно установить выравнивание для метки, передав аргумент
alignment=Qt.AlignmentFlag.AlignCenter после self.

Далее создайте объект QPushButton и передайте в качестве аргументов текст кнопки


и self, ссылку на класс MainWindow. При нажатии на кнопку будет выдан сигнал clicked,
который подключается к слоту buttonClicked() (показано в Листинге 3-3).

26 ГЛАВА 3 ДОБАВЛЕНИЕ ФУНКЦИОНАЛЬНОСТИ С ПОМОЩЬЮ ВИДЖЕТОВ


Листинг 3-3. Код для слота buttonClicked()
# buttons.py
def buttonClicked(self):
"""Обработка нажатия на кнопку. Демонстрирует, как изменить текст для виджетов,
обновлять их размеры и расположение, а также как закрывать окно в результате
событий."""
self.times_pressed += 1
if self.times_pressed == 1:
self.name_label.setText("Почему ты надавил на меня?")
if self.times_pressed == 2:
self.name_label.setText("Я предупреждаю вас.")
self.button.setText("Почувствовал себя счастливчиком?")
self.button.adjustSize()
self.button.move(70, 70)
if self.times_pressed == 3:
print("Окно было закрыто.")
self.close()
В buttonClicked() мы сначала обновим переменную times_pressed. Далее следует
ряд операторов if, которые зависят от значения times_pressed. Вы можете обновлять
текстовые значения виджетов даже после их создания. Если times_pressed равно 1,
изменится текст для name_label с помощью setText().
Если значение равно 2, изменится текст и для name_label, и для button. Для
button, вам также потребуется изменить ее размер, чтобы она соответствовала более
длинному значению текста. Поскольку QPushButton наследует QWidget, мы можем
использовать метод QWidget adjustSize() для изменения размера метки name_label,
чтобы вместить более длинный текст. Поскольку для расположения виджетов
используется абсолютное позиционирование, вам также потребуется использовать
move() для центрирования button, в окне. Примеры изменения текста показаны на
Рисунке 3-1.

Рисунок 3-1. Нажатие на кнопку QPushButton приводит к изменению текста


метки и, в конечном итоге, текста кнопки
Наконец, для 3, метод QWidget close() используется для закрытия виджетов. В
данном случае self.close() относится к главному окну и закрывает приложение. Мы
рассмотрим более подробно события закрытия позже в разделе "Проект 3.1 - GUI
входа в систему и диалог регистрации".
Далее мы рассмотрим виджет, который полезен для сбора пользовательского ввода.

ГЛАВА 3 ДОБАВЛЕНИЕ ФУНКЦИОНАЛЬНОСТИ С ПОМОЩЬЮ ВИДЖЕТОВ 27


Виджет QLineEdit
Часто пользователю необходимо ввести одну строку текста, например, имя
пользователя или пароль. С помощью виджета QLineEdit вы можете собирать данные
от человека. QLineEdit также поддерживает обычные функции редактирования текста,
такие как вырезание, копирование и вставка, а также повтор и отмена. Есть также
дополнительные возможности для скрытия текста при его вводе, использования
текста-заставки или даже установки ограничения на длину текста.

Совет. Если вам нужно несколько строк для ввода текста пользователем,
используйте виджет QTextEdit.

GUI, который вы создадите на Рисунке 3-2, демонстрирует, как настроить и


использовать виджеты QLineEdit. Вы можете использовать другие виджеты, такие как
QPushButton, вместе с сигналами и слотами для получения текста в объекте QLineEdit
или очистки его текста.

Рисунок 3-2. Виджеты QLineEdit и QPushButton, используемые для сбора и очистки


текста
В следующем разделе вы узнаете, как использовать QLineEdit.

Объяснение использования QLineEdit


Листинг 3-4 устанавливает главное окно, показанное на Рисунке 3-2. Вам
потребуется импортировать различные классы виджетов, включая QLabel, QLineEdit
и QPushButton, а также Qt из модуля QtCore в сценарий пустого окна из Главы 1.

28 ГЛАВА 3 ДОБАВЛЕНИЕ ФУНКЦИОНАЛЬНОСТИ С ПОМОЩЬЮ ВИДЖЕТОВ


Листинг 3-4. Настройка главного окна для использования виджетов QLineEdit
# line_edits.py
# Импортируйте необходимые модули
import sys
from PyQt6.QtWidgets import (QApplication, QWidget,
QLabel, QLineEdit, QPushButton)
from PyQt6.QtCore import Qt

class MainWindow(QWidget):

def __init__(self):
super().__init__()
self.initializeUI()

def initializeUI(self):
"""Настройте графический интерфейс приложения."""
self.setMaximumSize(310, 130)
self.setWindowTitle("Пример QLineEdit")

self.setUpMainWindow()
self.show()

if __name__ == '__main__':
app = QApplication(sys.argv)
window = MainWindow()
sys.exit(app.exec())

В предыдущих примерах использовалась функция setGeometry() для установки


расположения и размера окна на экране. Можно также ограничить размер окна.
Здесь давайте воспользуемся методом QWidget setMaximumSize() и передадим
максимальную ширину и высоту для MainWindow. Некоторые другие методы для
установки размеров окна включают следующие:
• setMinimumSize() - устанавливает минимальный размер виджета
• setMinimumHeight() и setMinimumWidth() - устанавливает минимальную
высоту и ширину виджета, соответственно
• setMaximumHeight() и setMaximumWidth() - устанавливает максимальную
высоту и ширину виджета, соответственно
• setFixedSize() - устанавливает максимальный и минимальный размеры для
виджета, не позволяя ему менять размеры

Совет. Эти методы полезны не только для установки ограничений на размер


окон, но и для объектов виджетов, поскольку все они наследуют QWidget.

ГЛАВА 3 ДОБАВЛЕНИЕ ФУНКЦИОНАЛЬНОСТИ С ПОМОЩЬЮ ВИДЖЕТОВ 29


Чтобы настроить виджеты в методе setUpMainWindow() в Листинге 3-5, мы сначала
создадим два объекта QLabel, затем виджет QLineEdit и два объекта QPushButton.
Листинг 3-5. Метод setUpMainWindow() для использования виджетов
редактирования строк
# line_edits.py
def setUpMainWindow(self):
"""Создайте и расположите виджеты в главном окне."""
QLabel("Пожалуйста, введите ваше имя ниже.",
self).move(70, 10)
name_label = QLabel("Имя:", self)
name_label.move(20, 50)

self.name_edit = QLineEdit(self)
self.name_edit.resize(210, 20)
self.name_edit.move(70, 50)

clear_button = QPushButton("Clear", self)


clear_button.move(140, 90)
clear_button.clicked.connect(self.clearText)

accept_button = QPushButton("OK", self)


accept_button.move(210, 90)
accept_button.clicked.connect(self.acceptText)

Два объекта QLabel - это просто примеры создания виджетов. Вы можете создавать
виджеты и располагать их в своем графическом интерфейсе без необходимости
присваивать их переменной.
Объект QLineEdit, name_edit, является примером того, как изменить размер
виджета с помощью метода resize(). Вам потребуется указать желаемые значения
высоты и ширины виджета.
Объекты clear_button и accept_button связаны со слотами clearText() и acceptText(),
созданными в Листинге 3-6.
Листинг 3-6. Код для слотов clearText() и acceptText()
# line_edits.py
def clearText(self):
"""Очистить поле ввода QLineEdit."""
self.name_edit.clear()

def acceptText(self):
"""Принять ввод пользователя в виджете QLineEdit
и закрыть программу."""
print(self.name_edit.text())
self.close()
30 ГЛАВА 3 ДОБАВЛЕНИЕ ФУНКЦИОНАЛЬНОСТИ С ПОМОЩЬЮ ВИДЖЕТОВ
Когда нажимается кнопка clear_button, она издает сигнал, который подключается
к слоту clearText(), и виджет name_edit реагирует на этот сигнал и очищает свой
текущий текст. Если пользователь нажимает кнопку accept_button, текст в name_edit
считывается с помощью геттера text() и выводится в оболочку компьютера. После
этого приложение закрывается.
Давайте рассмотрим еще один часто встречающийся виджет в настольных
приложениях.

Виджет QCheckBox
Виджет QCheckBox представляет собой выбираемую кнопку, которая обычно
имеет два состояния: включено и выключено.
Это делает их идеальными для представления функций в вашем графическом
интерфейсе, которые могут быть включены или выключены, или для выбора из
списка вариантов, как в опросе.
Приложение на Рисунке 3-3 показывает базовый графический интерфейс анкеты.
Пользователю разрешается выбрать все флажки, которые к нему относятся, и каждый
раз, когда пользователь нажимает на флажок, мы вызываем метод, чтобы показать,
как определить текущее состояние виджета.

Рисунок 3-3. Пример, использующий виджеты QCheckBox

Примечание. Флажки в QCheckBox не являются взаимоисключающими, то


есть вы можете выбрать более одного флажка одновременно. Чтобы сделать
их взаимоисключающими, добавьте флажки в объект QButtonGroup или
рассмотрите возможность использования QRadioButton.

Класс QCheckBox также можно использовать в динамических приложениях, где


серия виджетов флажков может быть использована для выбора или изменения
текста, внешнего вида или даже состояния графического интерфейса (включение
или отключение интерактивности).

ГЛАВА 3 ДОБАВЛЕНИЕ ФУНКЦИОНАЛЬНОСТИ С ПОМОЩЬЮ ВИДЖЕТОВ 31


Объяснение использования QCheckBox
Начните с создания класса MainWindow, как и раньше, используя в качестве
шаблона сценарий пустого окна из Главы 1. Для этого приложения импортируйте
QCheckBox и другие классы, показанные в листинге 3-7.
Листинг 3-7. Настройка главного окна для использования виджетов QCheckBox
# checkboxes.py
# Импортируйте необходимые модули
import sys
from PyQt6.QtWidgets import (QApplication, QWidget, QCheckBox,
QLabel)
from PyQt6.QtCore import Qt

class MainWindow(QWidget):

def __init__(self):
super().__init__()
self.initializeUI()

def initializeUI(self):
"""Настройте графический интерфейс приложения."""
self.setGeometry(100, 100, 250, 150)
self.setWindowTitle("Пример QCheckBox")
self.setUpMainWindow()
self.show()

if __name__ == '__main__':
app = QApplication(sys.argv)
window = MainWindow()
sys.exit(app.exec())

В Листинге 3-8 мы настроили метод для расположения виджетов в окне.

32 ГЛАВА 3 ДОБАВЛЕНИЕ ФУНКЦИОНАЛЬНОСТИ С ПОМОЩЬЮ ВИДЖЕТОВ


Листинг 3-8. Метод setUpMainWindow() для использования checkboxes
# checkboxes.py
def setUpMainWindow(self):
"""Создаем и располагаем виджеты в главном окне."""
header_label = QLabel("В какие смены вы можете работать? \
(Пожалуйста, отметьте все, что применимо)", self)
header_label.setWordWrap(True)
header_label.move(20, 10)

# Установите флажки
morning_cb = QCheckBox("Утро [8 утра-14 дня]", self)
morning_cb.move(40, 60)
#morning_cb.toggle() # Удалите комментарий, чтобы начать проверять
morning_cb.toggled.connect(self.printSelected)

after_cb = QCheckBox("День [13 дня - 20 вечера]", self)


after_cb.move(40, 80)
after_cb.toggled.connect(self.printSelected)

night_cb = QCheckBox("Ночь [19 вечера-3 утра]", self)


night_cb.move(40, 100)
night_cb.toggled.connect(self.printSelected)

Виджет QLabel используется для отображения вопроса пользователю, помогая


ему понять цель графического интерфейса. Для меток с длинным текстом, который
не помещается в одной строке, используйте метод setWordWrap().
Также создаются три объекта QCheckBox, каждый из которых имеет имя
переменной, отражающее назначение виджета. Текст, отображаемый рядом с каждым
флажком, передается в качестве первого аргумента. Метод QCheckBox toggle() может
быть использован для включения или выключения флажка. Когда флажок выбран,
вместо использования сигнала clicked, как в случае с QPushButton, используйте toggled
для выдачи сигнала, который подключается к слоту printSelected(), как показано в
Листинге 3-9.

Совет. Можно подключить несколько сигналов к одному и тому же слоту.

ГЛАВА 3 ДОБАВЛЕНИЕ ФУНКЦИОНАЛЬНОСТИ С ПОМОЩЬЮ ВИДЖЕТОВ 33


Листинг 3-9. Код для слота printSelected()
# checkboxes.py
def printSelected(self, checked):
"""Печатать текст объекта QCheckBox, когда он выбран
или отменен. Используйте sender(), чтобы определить, какой виджет
посылает сигнал."""
sender = self.sender()
if checked:
print(f"{sender.text()} Выбран.")
else:
print(f"{sender.text()} Не выбран.")

Сигнал toggled() также передает дополнительную информацию, checked, которая


возвращает True, если флажок выбран. В противном случае возвращается False.
При таком количестве виджетов, подключенных к одному слоту, бывает трудно
определить, с каким виджетом взаимодействуют и какой из них издает сигнал. К
счастью, метод QObject sender() возвращает, какой объект (виджет) посылает сигнал.
(Все виджеты наследуют класс QObject.) Для этого примера используйте геттер text(),
чтобы получить текст объекта checkbox и вывести его значение в оболочку. Пример
вывода на терминал показан на Рисунке 3-4.

Рисунок 3-4. Вывод в терминал при установке или снятии флажков с различных
checkbox-ов
Давайте рассмотрим еще один очень важный класс для создания интерактивных
и удобных графических интерфейсов.

Диалоговое окно QMessageBox


Когда пользователь закрывает приложение или сохраняет свою работу, или
возникает ошибка, он обычно видит всплывающее диалоговое окно, в котором
отображается какая-либо ключевая информация. Затем пользователь может
взаимодействовать с этим диалоговым окном, часто нажимая на кнопку, чтобы
ответить на запрос. Диалоговые окна являются очень важной формой обратной
связи, или методом контроля и передачи изменений пользователю.
Класс QMessageBox можно использовать не только для предупреждения
пользователя о ситуации, но и для принятия решения о том, как поступить в данной
ситуации. Например, при закрытии только что измененного документа может
34 ГЛАВА 3 ДОБАВЛЕНИЕ ФУНКЦИОНАЛЬНОСТИ С ПОМОЩЬЮ ВИДЖЕТОВ
появиться диалоговое окно с кнопками, предлагающими сохранить, не сохранять или
отменить. Четыре распространенных типа предопределенных виджетов QMessageBox
в PyQt показаны в таблице 3-1.
Таблица 3-1. Четыре типа статических диалоговых окон QMessageBox в PyQt.
Изображения из www.riverbankcomputing.com

Значки QMessageBox Типы Подробнее

Вопрос Задать вопрос пользователю

Отображение информации во время общих


Информация
операций

Предупреждение Сообщить о некритических ошибках

Критический Сообщить о критических ошибках

Окна и диалоги
Приложения обычно состоят из одного главного окна. Окно используется для
визуального отделения приложений друг от друга и обычно состоит из меню, панели
инструментов и других видов виджетов, которые часто могут выступать в качестве
основного интерфейса GUI-приложения. Окна в Qt обычно считаются виджетами,
которые появляются на экране и не имеют родительского виджета.
Диалоговое окно, или просто диалог, всплывает и отображает опции или
информацию, пока пользователь работает в главном окне. Большинство видов
диалоговых окон имеют родительское окно, которое используется для определения
положения диалога относительно его владельца. Это также означает, что между
окном и диалоговым окном происходит обмен данными, и диалоговые окна могут
использоваться для обновления главного окна.
Существует два вида диалоговых окон. Модальные диалоги блокируют
взаимодействие пользователя с остальной частью программы до тех пор, пока
диалоговое окно не будет закрыто. Бесмодальные диалоги позволяют пользователю
взаимодействовать как с диалоговым окном, так и с остальной частью приложения.

Объяснение использования QMessageBox


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

ГЛАВА 3 ДОБАВЛЕНИЕ ФУНКЦИОНАЛЬНОСТИ С ПОМОЩЬЮ ВИДЖЕТОВ 35


Примечание. Для этого примера вам также понадобится файл authors.txt,
находящийся в папке files репозитория этой главы на GitHub.

Главное окно этого приложения показано на Рисунке 3-5, а пара диалогов


QMessageBox показана на Рисунке 3-6.

Рисунок 3-5. Главное окно для примера QMessageBox, в котором пользователь


может искать имя автора в текстовом файле

Рисунок 3-6. Информационный диалог (слева), позволяющий пользователю


узнать, что его поиск был успешным. Диалог вопроса (справа), который спрашивает
пользователя, хочет ли он продолжить поиск если автор не был найден
Для Листинга 3-10 вам потребуется импортировать несколько дополнительных
классов, включая класс QMessageBox из QtWidgets, в сценарий пустого окна из Главы 1.

36 ГЛАВА 3 ДОБАВЛЕНИЕ ФУНКЦИОНАЛЬНОСТИ С ПОМОЩЬЮ ВИДЖЕТОВ


Листинг 3-10. Настройка главного окна для использования диалоговых окон
QMessageBox
# message_boxes.py
# Импортируйте необходимые модули
import sys
from PyQt6.QtWidgets import (QApplication, QWidget, QLabel,
QMessageBox, QLineEdit, QPushButton)
from PyQt6.QtGui import QFont

class MainWindow(QWidget):
def __init__(self):
super().__init__()
self.initializeUI()

def initializeUI(self):
"""Настройте графический интерфейс приложения."""
self.setGeometry(100, 100, 340, 140)
self.setWindowTitle("Пример QMessageBox")

self.setUpMainWindow()
self.show()

if __name__ == '__main__':
app = QApplication(sys.argv)
window = MainWindow()
sys.exit(app.exec())

Чтобы создать окно, показанное на Рисунке 3-5, вам потребуется создать несколько
объектов QLabel, виджет QLineEdit для ввода пользователем имени автора и объект
QPushButton, который издает сигнал при нажатии и ищет текст в текстовом файле.
Все это реализовано в Листинге 3-11.

ГЛАВА 3 ДОБАВЛЕНИЕ ФУНКЦИОНАЛЬНОСТИ С ПОМОЩЬЮ ВИДЖЕТОВ 37


Листинг 3-11. Метод setUpMainWindow() для использования окон сообщений#
message_boxes.py
def setUpMainWindow(self):
"""Создайте и расположите виджеты в главном окне."""
catalog_label = QLabel("Каталог авторов", self)
catalog_label.move(100, 10)
catalog_label.setFont(QFont("Arial", 18))

search_label = QLabel(
"Поиск автора в индексе:", self)
search_label.move(20, 40)

# Создайте виджеты QLabel и QLineEdit автора


author_label = QLabel("Имя:", self)
author_label.move(20, 74)

self.author_edit = QLineEdit(self)
self.author_edit.move(70, 70)
self.author_edit.resize(240, 24)
self.author_edit.setPlaceholderText(
"Введите имя как: Имя Фамилия")

# Создание поисковой кнопки QPushButton


search_button = QPushButton("Поиск", self)
search_button.move(140, 100)
search_button.clicked.connect(self.searchAuthors)

Виджеты catalogue_label и search_label используются для передачи информации


пользователю. В PyQt виджеты QLabel часто размещаются рядом с QLineEdit и другими
виджетами ввода в качестве меток. Затем метки могут быть связаны с виджетами
ввода как друзья. Здесь метки author_label и author_edit просто размещены рядом
друг с другом.
Текст-заглушка может быть использован для предоставления пользователю
дополнительной информации о назначении виджета QLineEdit или о том, как
форматировать вводимый текст. Это делается с помощью функции setPlaceholderText().
Пример этого показан на Рисунке 3-5.
Наконец, search_button издает сигнал, который вызывает слот searchAuthors() в
Листингах 3-12 и 3-13.

38 ГЛАВА 3 ДОБАВЛЕНИЕ ФУНКЦИОНАЛЬНОСТИ С ПОМОЩЬЮ ВИДЖЕТОВ


Листинг 3-12. Первая часть кода для слота searchAuthors()
# message_boxes.py
def searchAuthors(self):
"""Поиск по каталогу имен.
Если имя найдено, выведите диалог "Автор найден".
В противном случае выведите диалог "Автор не найден"."""
file = "files/authors.txt"

try:
with open(file, "r") as f:
authors = [line.rstrip("\n") for line in f]

# Проверка наличия имени в списке авторов


if self.author_edit.text() in authors:
QMessageBox.information(self, "Автор найден",
"Автор найден в каталоге!",
QMessageBox.StandardButton.Ok)

Когда пользователь нажимает на кнопку search_button, программа попытается


открыть файл authors.txt и сохранить его содержимое в списке authors. Если
пользователь вводит в author_edit имя, которое содержится в файле authors.txt,
появляется information диалог, как показано на первом Рисунке 3-6.
Чтобы создать диалог QMessageBox из одного из предопределенных типов, сначала
создайте объект QMessageBox и вызовите одну из статических функций, в данном
случае information. Затем передайте родительский виджет. Затем задайте заголовок
диалога, "Автор найден", и текст, который будет появляться внутри диалога,
предоставляющий обратную связь, возможно, с информацией о действиях, которые
может предпринять пользователь. Далее следуют типы стандартных кнопок, которые
будут появляться в диалоге. Можно использовать несколько кнопок, разделяя их с
помощью клавиши "труба", |. Стандартные кнопки включают Open, Save, Cancel, Re-
set, Yes и No. В Приложении перечислены другие типы QMessageBox.StandardButton.

Примечание. В macOS, когда появляется окно сообщения, заголовок обычно


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

ГЛАВА 3 ДОБАВЛЕНИЕ ФУНКЦИОНАЛЬНОСТИ С ПОМОЩЬЮ ВИДЖЕТОВ 39


Листинг 3-13. Вторая часть кода для слота searchAuthors()
# message_boxes.py
else:
answer = QMessageBox.question(self,
"Автор не найден",
"""<p>Автор не найден в каталоге.</p>
<p>Вы хотите продолжить? </p>""",
QMessageBox.StandardButton.Yes | \
QMessageBox.StandardButton.No,
QMessageBox.StandardButton.No)

if answer == QMessageBox.StandardButton.No:
print("Закрытие приложения.")
self.close()
except FileNotFoundError as error:
QMessageBox.warning(self, "Ошибка",
f"""<p>Файл не найден.</p>
<p>Error: {error}</p>
Закрытие приложения.""",
QMessageBox.StandardButton.Ok)
self.close()

Если автор не найден, появляется диалоговое окно с вопросом (второе изображение


на Рисунке 3-6), спрашивающее пользователя, хочет ли он повторить поиск или
выйти из программы. В окне появляются стандартные кнопки Да и Нет. Последний
аргумент используется для указания кнопки, которую вы хотите выделить в диалоге
и установить в качестве кнопки по умолчанию.

Примечание. Текстовые виджеты PyQt способны отображать насыщенный


текст, используя HyperText Markup Language (HTML) и Cascading Style
Sheets (CSS). Эта тема более подробно рассматривается в Главе 6, а пока мы
будем использовать HTML для организации текста, размещенного между
HTML-тегами <p> и </p>, в абзацы.

40 ГЛАВА 3 ДОБАВЛЕНИЕ ФУНКЦИОНАЛЬНОСТИ С ПОМОЩЬЮ ВИДЖЕТОВ


Если произойдет ошибка и файл не будет найден, появится диалоговое окно
warning, показанное на Рисунке 3-7.

Рисунок 3-7. Диалоговое окно предупреждения, информирующее пользователя о


том, что файл authors.txt не найден
Учитывая все, чему вы научились до этого момента, самое время попрактиковаться
в создании более крупного проекта.

Проект 3.1 - Графический интерфейс входа в


систему и диалог регистрации
Пользовательский интерфейс входа в систему, вероятно, один из самых
распространенных интерфейсов, с которыми вы регулярно взаимодействуете -
входите в свой компьютер, банковский счет в интернете, электронную почту или
социальные сети; входите в свой телефон; или даже регистрируетесь в каком-нибудь
новом приложении. Интерфейс входа в систему присутствует везде.
В этом примере вы создадите три разных окна. Первое окно, которое появится, -
это графический интерфейс входа в систему, показанный на рисунке 3-8.

Рисунок 3-8. Окно входа в систему


Этот проект также демонстрирует, как открывать и закрывать другие окна и
диалоговые окна, и начинает рассматривать использование обработчиков событий.
В первый раз, когда кто-то использует ваше приложение, вы, возможно, захотите,
чтобы он зарегистрировался и создал свое собственное имя пользователя и пароль.

ГЛАВА 3 ДОБАВЛЕНИЕ ФУНКЦИОНАЛЬНОСТИ С ПОМОЩЬЮ ВИДЖЕТОВ 41


Диалог регистрации, показанный на Рисунке 3-9, появляется, когда пользователь
нажимает кнопку Sign Up (Регистрация) на Рисунке 3-8.

Рисунок 3-9. Диалоговое окно регистрации для создания нового пользователя


Если пользователь успешно вошел в систему, его приветствует окно на Рисунке
3-10, в котором просто отображается виджет QLabel с изображением зимородка.

Рисунок 3-10. Главное окно приложения, которое появляется при успешном входе
пользователя в систему. Изображение зимородка взято с сайта https://pixabay.com.
В следующем разделе обсуждаются различные окна, виджеты и их функциональные
возможности в этом приложении.

Проектирование графического интерфейса входа в


систему и диалогового окна регистрации
При разработке интерфейса входа в систему вы хотите создать графический
интерфейс, который четко обозначает свои виджеты, различает поля для входа и
регистрации и помогает пользователям лучше ориентироваться в потенциальных
ошибках, например, если включен caps lock или неправильно указано имя
пользователя. Хотя внешний вид и компоновка графического интерфейса входа
42 ГЛАВА 3 ДОБАВЛЕНИЕ ФУНКЦИОНАЛЬНОСТИ С ПОМОЩЬЮ ВИДЖЕТОВ
в систему могут меняться в зависимости от платформы, они, как правило, имеют
несколько общих ключевых компонентов, таких как
• Поля для ввода имени пользователя и пароля
• Флажки, которые могут запоминать регистрационную информацию
пользователя или раскрывать пароль при установке флажка
• Кнопки, которые пользователи могут нажимать, чтобы войти в систему или
даже зарегистрировать новые учетные записи.
Для окна Login на Рисунке 3-8, два отдельных виджета QLineEdit используются для
ввода пользователями имени пользователя и пароля. Под виджетом пароля QLineEdit
находится флажок, позволяющий установить, скрыт пароль или нет. Есть также два
виджета QPushButton: один, который пользователь может нажать для входа в систему,
а другой - для регистрации новой учетной записи.

Когда пользователь нажимает кнопку Login, подается сигнал. Подключенный слот


используется для проверки правильности ввода. Диалоги QMessageBox используются
для обратной связи, если пользователь существует или не существует, или в качестве
сообщения об ошибке, если файл users.txt не существует. Если успешный вход
произошел, то появится главное окно, показанное на Рисунке 3-10. Вы можете найти
файл users.txt в папке files репозитория GitHub этой главы.
Если пользователь хочет зарегистрировать новую учетную запись, он может нажать
кнопку Sign Up в окне Login, после чего появится модальный диалог, показанный
на Рисунке 3-9. Пользователь не сможет взаимодействовать с окном Login, пока не
будет закрыто диалоговое окно Registration.
Наконец, этот пример также демонстрирует, как обрабатывать событие, когда
пользователь закрывает окно. Вместо того чтобы просто закрыть приложение с
помощью close(), вы увидите, как использовать обработчик события closeEvent(),
чтобы настроить закрытие ваших программ.
Графический интерфейс входа в систему - это то, что пользователь видит первым,
поэтому давайте начнем с него.

Объяснение создания графического интерфейса


входа в систему
Виджеты и концепции, изученные в Главах 1, 2 и 3, будут применены в этом
проекте. Листинг 3-14 начинает проект с использования сценария пустого окна из
Главы 1, чтобы начать создание класса LoginWindow. Импортируйте многие виджеты
и другие классы, которые вы видели ранее, а также класс NewUserDialog, который вы
создадите позже в разделе "Объяснение создания регистрационного диалога".

ГЛАВА 3 ДОБАВЛЕНИЕ ФУНКЦИОНАЛЬНОСТИ С ПОМОЩЬЮ ВИДЖЕТОВ 43


Листинг 3-14. Настройка окна для графического интерфейса входа в систему
# login_gui.py
# Импортируйте необходимые модули
import sys
from PyQt6.QtWidgets import (QApplication, QWidget, QLabel,
QLineEdit, QPushButton, QCheckBox, QMessageBox)
from PyQt6.QtGui import QFont, QPixmap
from PyQt6.QtCore import Qt
from registration import NewUserDialog

class LoginWindow(QWidget):

def __init__(self):
super().__init__()
self.initializeUI()

def initializeUI(self):
"""Настройте графический интерфейс приложения."""
self.setFixedSize(360, 220)
self.setWindowTitle("3.1 – Login GUI")

self.setUpWindow()
self.show()

if __name__ == '__main__':
app = QApplication(sys.argv)
window = LoginWindow()
sys.exit(app.exec())

Класс LoginWindow не является главным окном этого приложения. Класс мог бы


наследовать класс QDialog, базовый класс для создания диалоговых окон, но нам не
нужно беспокоиться о модальности окна, поэтому подойдет просто наследование
QWidget. Кроме того, функция setFixedSize() используется для фиксированного
размера окна.
В Листинге 3-15 начинается настройка метода setUpWindow() для класса
LoginWindow.

44 ГЛАВА 3 ДОБАВЛЕНИЕ ФУНКЦИОНАЛЬНОСТИ С ПОМОЩЬЮ ВИДЖЕТОВ


Листинг 3-15. Первая часть метода setUpWindow() для графического интерфейса
входа в систему
# login_gui.py
def setUpWindow(self):
"""Создайте и расположите виджеты в главном окне."""
self.login_is_successful = False

login_label = QLabel("Login", self)


login_label.setFont(QFont("Arial", 20))
login_label.move(160, 10)

# Создайте виджеты для имени пользователя и пароля


username_label = QLabel("Имя пользователя:", self)
username_label.move(20, 54)

self.username_edit = QLineEdit(self)
self.username_edit.resize(250, 24)
self.username_edit.move(90, 50)

password_label = QLabel("Пароль:", self)


password_label.move(20, 86)

self.password_edit = QLineEdit(self)
self.password_edit.setEchoMode(QLineEdit.EchoMode.Password)
self.password_edit.resize(250, 24)
self.password_edit.move(90, 82)

Переменная login_is_successful отслеживает, вошел ли пользователь в систему или


нет. Для ввода имени пользователя и пароля создается пара виджетов ввода QLabel и
QLineEdit. Затем виджеты располагаются рядом друг с другом. Метод setEchoMode(),
предоставляемый QLineEdit, очень полезен для скрытия текста при вводе. Флаг
Password используется здесь для маскировки символов при вводе пароля.
Листинг 3-16 продолжает создание и расположение виджетов в LoginWindow.

ГЛАВА 3 ДОБАВЛЕНИЕ ФУНКЦИОНАЛЬНОСТИ С ПОМОЩЬЮ ВИДЖЕТОВ 45


Листинг 3-16. Вторая часть метода setUpWindow() для графического интерфейса
входа в систему
# login_gui.py
# Создаем QCheckBox для отображения пароля
self.show_password_cb = QCheckBox(
"Показать пароль", self)
self.show_password_cb.move(90, 110)
self.show_password_cb.toggled.connect(
self.displayPasswordIfChecked)

# Создайте QPushButton для входа в систему


login_button = QPushButton("Login", self)
login_button.resize(320, 34)
login_button.move(20, 140)
login_button.clicked.connect(self.clickLoginButton)

# Создайте QLabel и QPushButton для регистрации


not_member_label = QLabel("Не являетесь членом?", self)
not_member_label.move(20, 186)

sign_up_button = QPushButton("Зарегистрироваться", self)


sign_up_button.move(120, 180)
sign_up_button.clicked.connect(self.createNewUser)

Создайте QCheckBox с именем show_password_cb, который при переключении


(toggled) будет издавать сигнал, вызывающий слот displayPasswordIfChecked(). Кнопка
login_button использует сигнал clicked для вызова слота clickLoginButton(). Кнопка
sign_up_button открывает диалог для регистрации новых пользователей и при
нажатии вызывает слот createNewUser().
Следующим шагом будет создание различных слотов. Начнем в Листинге 3-17 с
clickLoginButton(), используемого login_button.

46 ГЛАВА 3 ДОБАВЛЕНИЕ ФУНКЦИОНАЛЬНОСТИ С ПОМОЩЬЮ ВИДЖЕТОВ


Листинг 3-17. Первая часть слота clickLoginButton()
# login_gui.py
def clickLoginButton(self):
"""Проверьте, совпадают ли имя пользователя и пароль с любыми
существующими записями в файле users.txt.
Если они найдены, покажите QMessageBox и закройте программу.
Если нет, покажите предупреждение QMessageBox.""""
users = {} # Словарь для хранения информации о пользователях
file = "files/users.txt"

try:
with open(file, "r") as f:
for line in f:
user_info = line.split(" ")
username_info = user_info[0]
password_info = user_info[1].strip("\n")
users[username_info] = password_info

# Соберите информацию о пользователе и пароле


username = self.username_edit.text()
password = self.password_edit.text()

Этот метод, который продолжается в Листинге 3-18, сначала проверяет, существует


ли файл users.txt. Если он существует, то информация о пользователе берется из
файла, а значения username_info и password_info добавляются в словарь users. Затем
с помощью text() собираются текстовые значения для username_edit и password_edit.

ГЛАВА 3 ДОБАВЛЕНИЕ ФУНКЦИОНАЛЬНОСТИ С ПОМОЩЬЮ ВИДЖЕТОВ 47


Листинг 3-18. Вторая часть слота clickLoginButton()
# login_gui.py
if (username, password) in users.items():
QMessageBox.information(self,
"Login Successful!",
"Login Successful!",
QMessageBox.StandardButton.Ok,
QMessageBox.StandardButton.Ok)
self.login_is_successful = True
self.close() # Закройте окно входа в систему
self.openApplicationWindow()
else:
QMessageBox.warning(self, "Сообщение об ошибке",
"Имя пользователя или пароль неверны.",
QMessageBox.StandardButton.Close,
QMessageBox.StandardButton.Close)
except FileNotFoundError as error:
QMessageBox.warning(self, "Ошибка",
f"""<p>Файл не найден.</p>
<p>Ошибка: {error}</p>"""",
QMessageBox.StandardButton.Ok)
# Создайте файл, если он не существует
f = open(file, "w")

Питоновский dict метод items() возвращает список пар ключ-значение в


виде кортежей, которые можно использовать для проверки совпадения пары
username-password у пользователей.
Если совпадение найдено, появляется верхний QMessageBox на Рисунке 3-11.
Затем login_is_successful устанавливается в True, и текущее окно закрывается. Главное
окно примера появляется при вызове openApplicationWindow(), которое создается в
разделе "Пояснения к созданию главного окна". Стоит отметить, что функция close()
на самом деле не закрывает окно, как вы можете подумать. Окно просто скрывается из
виду. Подробнее об этом рассказывается в подразделе "Использование обработчиков
событий для закрытия окна".

48 ГЛАВА 3 ДОБАВЛЕНИЕ ФУНКЦИОНАЛЬНОСТИ С ПОМОЩЬЮ ВИДЖЕТОВ


Рисунок 3-11. Информационный диалог (вверху), который информирует
пользователя о том, что его ввод был правильным. Диалоговое окно предупреждения
(внизу), информирующее пользователя об ошибке.
В противном случае, если имя пользователя или пароль введены неверно,
отображается предупреждение(warning) QMessageBox, нижнее изображение на
Рисунке 3-11.
В следующих подразделах завершается создание класса LoginWindow.
Hiding Input for QLineEdit (скрытие ввода данных)
Сигнал toggled, используемый show_password_cb в Листинге 3-16, подключен к
слоту displayPasswordIfChecked() в Листинге 3-19.
Листинг 3-19. Код для слота displayPasswordIfChecked()
# login_gui.py
def displayPasswordIfChecked(self, checked):
"""Если QCheckButton включен, просмотрите пароль.
В противном случае замаскируйте пароль, чтобы другие
не могли его увидеть."""
if checked:
self.password_edit.setEchoMode(
QLineEdit.EchoMode.Normal)
elif checked == False:
self.password_edit.setEchoMode(
QLineEdit.EchoMode.Password)

Если флажок show_password_cb установлен, то символы пароля можно увидеть,


используя setEchoMode() и передавая перечисление QLineEdit.EchoMode с флагом
Normal. В противном случае, если флажок не установлен, текст пароля маскируется,
чтобы другие не могли видеть символы. Пример этого можно увидеть на Рисунке 3-8.
Также существуют флаги для полного скрытия пароля, NoEcho, и для отображения
только вводимого символа и маскировки остальных, PasswordEchoOnEdit.

ГЛАВА 3 ДОБАВЛЕНИЕ ФУНКЦИОНАЛЬНОСТИ С ПОМОЩЬЮ ВИДЖЕТОВ 49


Как открыть новое окно или диалог
В PyQt можно иметь несколько окон и диалогов, открытых одновременно. Открыть
QMessageBox относительно просто - для этого создайте экземпляр QMessageBox,
когда это необходимо. Однако для пользовательских диалогов и окон вам также
потребуется вызвать метод для их отображения.
Кнопка sign_up_button при нажатии издает сигнал, который подключается к
методу createNewUser(), показанному в Листинге 3-20.
Листинг 3-20. Код для слота createNewUser() и метода openApplicationWindow()
# login_gui.py
def createNewUser(self):
"""Открыть диалог для создания новой учетной записи."""
self.create_new_user_window = NewUserDialog()
self.create_new_user_window.show()

def openApplicationWindow(self):
"""Открываем макет главного окна после входа пользователя в систему."""
self.main_window = MainWindow()
self.main_window.show()

В createNewUser() создайте экземпляр NewUserDialog из модуля регистрации


(registration). Чтобы отобразить модальный диалог, вызовите show(). Посмотрите
раздел "Объяснение создания диалогового окна регистрации" для создания класса.
Это аналогичная схема для открытия главного окна после входа пользователя в
систему.

Использование обработчиков событий для


закрытия окна
Хорошей практикой при выходе из программы является представление
диалогового окна, как показано на рисунке 3-12, подтверждающего, действительно
ли пользователь хочет выйти из программы или нет. В большинстве программ это
позволит пользователю не забыть сохранить последнюю работу.

50 ГЛАВА 3 ДОБАВЛЕНИЕ ФУНКЦИОНАЛЬНОСТИ С ПОМОЩЬЮ ВИДЖЕТОВ


Рисунок 3-12. QMessageBox, появляющийся перед выходом из приложения
Когда происходит событие, создается объект события в зависимости от типа
события, и этот объект передается соответствующему объекту, например, виджету.
Затем используются обработчики событий для обработки события, если оно принято.
В противном случае событие может быть проигнорировано.
Когда QWidget закрывается в PyQt, генерируется объект QCloseEvent. Однако
виджеты и окна на самом деле не закрываются. Скорее, они скрываются из виду, если
событие принято. Причина, по которой приложение фактически завершается при
закрытии экземпляра LoginWindow, заключается в сигнале, который испускается,
когда последнее главное окно (не имеющее родителя) больше не видно. Этот сигнал
- QApplication.lastWindowClosed(), который уже обрабатывается PyQt.
Для того чтобы изменить способ обработки метода closeEvent(), в данном случае
для класса LoginWindow, вам нужно будет повторно реализовать closeEvent(). Пример
этого показан в Листинге 3-21.
Листинг 3-21. Модификация обработчика события closeEvent()
# login_gui.py
def closeEvent(self, event):
"""Реализуйте событие закрытия для отображения
QMessageBox перед закрытием."""
if self.login_is_successful == True:
event.accept()
else:
answer = QMessageBox.question(
self, "Выйти из приложения?",
"Вы уверены, что хотите выйти из приложения?",
QMessageBox.StandardButton.No | \
QMessageBox.StandardButton.Yes,
QMessageBox.StandardButton.Yes)
if answer == QMessageBox.StandardButton.Yes:
event.accept()
if answer == QMessageBox.StandardButton.No:
event.ignore()

Если окно было закрыто после успешного входа в систему, событие(event)


принимается с помощью метода accept(). Если окно было закрыто по другой причине,

ГЛАВА 3 ДОБАВЛЕНИЕ ФУНКЦИОНАЛЬНОСТИ С ПОМОЩЬЮ ВИДЖЕТОВ 51


QMessageBox спрашивает пользователя, уверен ли он в выходе. Если ответ на вопрос
(question) QMessageBox - Да(Yes), событие(event) закрытия принимается, и программа
закрывается. В противном случае событие(event) игнорируется с помощью функции
ignore().
В следующем разделе создается класс MainWindow.

Пояснение к созданию главного окна


Главное окно, используемое в этом проекте, является очень простым примером,
но цель здесь - продемонстрировать, как работать с несколькими окнами. В том
же сценарии Python, что и класс LoginWindow, создаётся новый класс MainWindow,
который наследует QWidget. Этот новый класс можно увидеть в Листинге 3-22.
Листинг 3-22. Класс MainWindow
# login_gui.py
class MainWindow(QWidget):

def __init__(self):
super().__init__()
self.initializeUI()

def initializeUI(self):
"""Настройте графический интерфейс приложения."""
self.setMinimumSize(640, 426)
self.setWindowTitle('3.1 - Главное окно')
self.setUpMainWindow()

def setUpMainWindow(self):
"""Создайте и расположите виджеты в главном окне."""
image = "images/background_kingfisher.jpg"

try:
with open(image):
main_label = QLabel(self)
pixmap = QPixmap(image)
main_label.setPixmap(pixmap)
main_label.move(0, 0)
except FileNotFoundError as error:
print(f"Изображение не найдено.\nError: {error}")

Размер окна устанавливается с помощью setMinimumSize(). Следует отметить, что в


initializeUI() не вызывается метод show(). Это связано с тем, что окно появится только
после успешного входа в систему (см. Листинг 3-18). Главное окно на Рисунке 3-10

52 ГЛАВА 3 ДОБАВЛЕНИЕ ФУНКЦИОНАЛЬНОСТИ С ПОМОЩЬЮ ВИДЖЕТОВ


представляет простое окно с QLabel.
Классы, которые вы создаете, могут наследовать большинство классов PyQt,
включая классы для диалоговых окон, как вы увидите в следующем разделе.

Объяснение создания диалогового окна


регистрации
Настройка - одно из самых больших преимуществ использования PyQt для
создания графических интерфейсов. Когда пользователь хочет зарегистрировать
нового пользователя, появляется диалог, показанный на Рисунке 3-9. Для диалога
регистрации в Листингах 3-23 - 3-27 создайте отдельный Python-скрипт, чтобы
сохранить организованность кода и продемонстрировать, как импортировать
собственные пользовательские классы в свои проекты.
Листинг 3-23. Код для настройки диалога регистрации, который наследует QDialog
# registration.py
# Импортируйте необходимые модули
import sys
from PyQt6.QtWidgets import (QApplication, QDialog, QLabel,
QPushButton, QLineEdit, QMessageBox)
from PyQt6.QtGui import QFont, QPixmap

class NewUserDialog(QDialog):

def __init__(self):
super().__init__()
self.setModal(True)
self.initializeUI()

def initializeUI(self):
"""Настройте графический интерфейс приложения."""
self.setFixedSize(360, 320)
self.setWindowTitle("3.1 - Графический интерфейс регистрации")
self.setUpWindow()

Диалоговое окно регистрации содержит два виджета QLabel для заголовка и


изображения пользователя на Рисунке 3-10. Они созданы в Листинге 3-24.

ГЛАВА 3 ДОБАВЛЕНИЕ ФУНКЦИОНАЛЬНОСТИ С ПОМОЩЬЮ ВИДЖЕТОВ 53


Листинг 3-24. Добавление меток в метод setUpWindow() диалогового окна
регистрации
# registration.py
def setUpWindow(self):
"""Создайте и расположите виджеты в окне для
сбора информации о новом аккаунте."""
login_label = QLabel("Создать новый аккаунт", self)
login_label.setFont(QFont("Arial", 20))
login_label.move(90, 20)

# Создайте QLabel для изображения


user_image = "images/new_user_icon.png"

try:
with open(user_image):
user_label = QLabel(self)
pixmap = QPixmap(user_image)
user_label.setPixmap(pixmap)
user_label.move(150, 60)
except FileNotFoundError as error:
print(f "Изображение не найдено. Ошибка: {error}")

Далее в Листинге 3-25 создаются четыре виджета QLineEdit и соответствующие им


метки.
Листинг 3-25. Добавление меток и виджетов редактирования строк в методе
setUpWindow() диалога регистрации
# registration.py
# Создание виджетов QLabel и QLineEdit для имени
name_label = QLabel("Имя пользователя:", self)
name_label.move(20, 144)

self.name_edit = QLineEdit(self)
self.name_edit.resize(250, 24)
self.name_edit.move(90, 140)

full_name_label = QLabel("Полное имя:", self)


full_name_label.move(20, 174)

full_name_edit = QLineEdit(self)
full_name_edit.resize(250, 24)
full_name_edit.move(90, 170)

# Создайте виджеты QLabel и QLineEdit для пароля

54 ГЛАВА 3 ДОБАВЛЕНИЕ ФУНКЦИОНАЛЬНОСТИ С ПОМОЩЬЮ ВИДЖЕТОВ


new_pswd_label = QLabel("Пароль:", self)
new_pswd_label.move(20, 204)

self.new_pswd_edit = QLineEdit(self)
self.new_pswd_edit.setEchoMode(
QLineEdit.EchoMode.Password)
self.new_pswd_edit.resize(250, 24)
self.new_pswd_edit.move(90, 200)

confirm_label = QLabel("Подтвердить:", self)


confirm_label.move(20, 234)

self.confirm_edit = QLineEdit(self)
self.confirm_edit.setEchoMode(
QLineEdit.EchoMode.Password)
self.confirm_edit.resize(250, 24)
self.confirm_edit.move(90, 230)

Эти виджеты используются для сбора имени пользователя, полного имени, пароля
и дополнительного виджета QLineEdit для проверки правильности ввода пароля.
Кнопка для подтверждения данных настроена в Листинге 3-26.
Листинг 3-26. Добавление кнопки регистрации в метод setUpWindow() диалогового
окна регистрации
# registration.py
# Создаем кнопку регистрации QPushButton
sign_up_button = QPushButton("Регистрация", self)
sign_up_button.resize(320, 32)
sign_up_button.move(20, 270)
sign_up_button.clicked.connect(self.confirmSignUp)

При щелчке кнопка sign_up_button издает сигнал, который вызывает слот


confirmSignUp() в Листинге 3-27.

Совет. QDialog имеет свои собственные стандартные кнопки, которые


можно добавить в пользовательский класс диалога с помощью класса
QDialogButtonBox.

ГЛАВА 3 ДОБАВЛЕНИЕ ФУНКЦИОНАЛЬНОСТИ С ПОМОЩЬЮ ВИДЖЕТОВ 55


Листинг 3-27. Код для слота confirmSignUp()
# registration.py
def confirmSignUp(self):
"""Проверьте, правильно ли введена информация о пользователе.
Если да, добавьте текст имени пользователя и пароля в файл."""
name_text = self.name_edit.text()
pswd_text = self.new_pswd_edit.text()
confirm_text = self.confirm_edit.text()

if name_text == "" or pswd_text == "":


# Вывести QMessageBox, если пароли не совпадают
QMessageBox.warning(self, "Сообщение об ошибке",
"Пожалуйста, введите значения имени пользователя или пароля.",
QMessageBox.StandardButton.Close,
QMessageBox.StandardButton.Close)
elif pswd_text != confirm_text:
# Вывести QMessageBox, если пароли не совпадают.
QMessageBox.warning(self, "Сообщение об ошибке",
"Введенные вами пароли не совпадают.",
QMessageBox.StandardButton.Close,
QMessageBox.StandardButton.Close)
else:
# Возврат к окну входа в систему, если пароли совпадают
with open("files/users.txt", 'a+') as f:
f.write("\n" + name_text + " ")
f.write(pswd_text)
self.close()

Слот confirmSignUp() сначала считывает текст из name_edit, new_pswd_edit и


confirm_edit. Затем выполняется ряд проверок, чтобы проверить, пуст ли name_text
или pswd_text, а затем проверить, одинаковы ли pswd_text и confirm_text. Один из
двух диалогов QMessageBox на Рисунке 3-13 появится при выполнении любого из
условий.

Рисунок 3-13. Диалоговое окно предупреждения о том, что поля имени


пользователя или пароля пусты (слева), и другое, сообщающее пользователю, что
пароли не совпадают (справа).

56 ГЛАВА 3 ДОБАВЛЕНИЕ ФУНКЦИОНАЛЬНОСТИ С ПОМОЩЬЮ ВИДЖЕТОВ


Если пользователь нажимает кнопку sign_up_button и все данные введены
правильно, name_text и pswd_text сохраняются на новой строке в файле
users.txt, разделенные пробелом, как показано на Рисунке 3-14. Если пользователь
закроет диалог до заполнения формы, диалог закроется, но окно Login останется
открытым.

Рисунок 3-14. Оригинальный файл users.txt (слева) и обновленный файл (справа)


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

Резюме
В этой главе вы получили опыт создания графических интерфейсов с
использованием различных виджетов - QPushButton, QLineEdit, QCheckBox и
QMessageBox. Используя различные виджеты, вы также смогли узнать о других
важных концепциях для создания графических интерфейсов в PyQt, а именно:
обработка событий, взаимодействие между виджетами с помощью сигналов и слотов,
разница между окнами и диалогами, и как создавать приложения с несколькими
окнами. Все эти понятия в этой главе закладывают основу для создания больших,
более отзывчивых графических интерфейсов.
Однако есть еще несколько концепций, которые необходимо изучить для
создания GUI-приложений. В следующей главе вы узнаете еще об одной из этих
фундаментальных тем - управления макетами.

ГЛАВА 3 ДОБАВЛЕНИЕ ФУНКЦИОНАЛЬНОСТИ С ПОМОЩЬЮ ВИДЖЕТОВ 57


ГЛАВА 4

Изучение управления
макетом
По мере роста приложений, организация и управление расположением и
размерами всех виджетов в пользовательском интерфейсе может стать сложной
задачей. Хорошей новостью является то, что PyQt делает процесс организации
виджетов относительно простым благодаря встроенным менеджерам компоновки,
или классам, которые обрабатывают большинство тонкостей организации виджетов
в графических интерфейсах.
В этой главе вы
• узнаете о классах менеджера компоновки PyQt и примените их в
многочисленных графических интерфейсах пользователя
• рассмотрите, какой менеджер компоновки лучше всего подходит для вашего
приложения
• узнаете, как создавать сложные макеты с вложенными макетами
• изучите несколько различных методов управления виджетами в менеджерах
компоновки, таких как добавление пробелов или работа с политикой
размеров
• используйте множество новых классов, включая QComboBox, QSpinBox,
QDoubleSpinBox, QButtonGroup, QTextEdit, QDateEdit и другие, чтобы расширить
свой инструментарий и знания о создании невероятных графических
интерфейсов.
• расширите свой опыт работы с ранее изученными классами виджетов при
создании практических приложений
Давайте узнаем больше о менеджерах компоновки и о том, как использовать их в
PyQt.

58 ГЛАВА 4 ИЗУЧЕНИЕ УПРАВЛЕНИЯ МАКЕТОМ


Использование менеджеров компоновки в PyQt
Управление макетами (Layout management) - это полезная практика расположения
виджетов в графических интерфейсах. При организации виджетов необходимо
учитывать ряд ситуаций, включая размер и положение виджета относительно других
виджетов, что делать при изменении размера окна и как обращаться с виджетами при
их добавлении или удалении. Управление макетом также очень важно рассматривать
с точки зрения пользователя. Интуитивно понятное расположение виджетов может
помочь пользователю быстро ориентироваться в графическом интерфейсе и легче
выполнять задачи.
Классы менеджеров компоновки упрощают организацию виджетов и позволяют
дочерним и родительским виджетам взаимодействовать, обеспечивая более
эффективное использование пространства в окне при любых изменениях. Каждый
менеджер компоновки по-своему управляет виджетами и пространством, но
существует общая схема настройки и добавления виджетов в компоновку.
Давайте рассмотрим небольшой пример. Предположим, вы хотите расположить
виджет QLabel над виджетом QLineEdit. Сначала создайте два объекта виджетов:
label = QLabel("Name")
line_edit = QLineEdit()
Обратите внимание, что ни родительский виджет, ни параметр self не передаются
в качестве аргумента при создании виджетов. Это потому, что менеджер компоновки
автоматически позаботится о переназначении виджетов, чтобы они были связаны
с родительским виджетом. Проще говоря, менеджеры компоновки устанавливают
родителей для дочерних виджетов.
Далее создайте экземпляр менеджера компоновки для вертикального
расположения виджетов, QVBoxLayout:
v_box = QVBoxLayout() # Создаем экземпляр менеджера компоновки
v_box.addWidget(label) # Добавляем виджеты в макет
v_box.addWidget(line_edit)
parent_widget.setLayout(v_box) # Устанавливает макет для родителя.
Чтобы добавить виджеты в макет, используйте метод addWidget() и передайте
добавляемый виджет. Затем примените макет, используемый в родительском виджете,
вызвав метод QWidget setLayout(). Родительский виджет может быть виджетом, окном
или даже диалоговым окном. Менеджеры компоновки не могут быть родителями для
виджетов; только виджеты могут быть родителями для других виджетов.
Вместо использования setLayout() вы можете передать родительский виджет
менеджеру компоновки, как показано ниже:
v_box = QVBoxLayout(parent_widget)

ГЛАВА 4 ИЗУЧЕНИЕ УПРАВЛЕНИЯ МАКЕТОМ 59


Наконец, вы также можете добавлять макеты в другие макеты для создания
вложенного макета. Чтобы добавить макет, создайте новый экземпляр макета и
используйте метод addLayout() для передачи макета родительскому макету:
h_box = QHBoxLayout()
v_box.addLayout(h_box)
Здесь h_box является дочерним макетом, а v_box - родительским, и h_box
становится внутренним макетом, или дочерним макетом родительского макета. Эта
тема будет рассмотрена позже в разделе "Создание вложенных макетов".
Прежде чем двигаться дальше, давайте вспомним, что такое абсолютное
позиционирование и его назначение.

Абсолютное позиционирование
Одним из методов расположения виджетов в интерфейсе является абсолютное
позиционирование, которое подразумевает указание размеров и положения
дочернего виджета внутри родительского. Эта идея была представлена и использована
в Главах 2 и 3 с помощью метода QWidget move(). Это было сделано для того, чтобы вы
могли получить фундаментальное понимание того, как использовать пространство в
окне, а также для того, чтобы вы могли воочию убедиться в полезности использования
менеджеров компоновки.
Итак, почему вы хотите использовать абсолютное позиционирование? Абсолютное
позиционирование может быть наиболее полезным для установки положения и
размеров виджетов, которые находятся внутри других виджетов, или, возможно, для
изменения расположения окна на рабочем столе.
Если вы решите использовать абсолютное позиционирование, следует помнить о
нескольких недостатках. Во-первых, изменение размера главного окна не приведет
к изменению размера или положения виджетов в нем. Во-вторых, различия
между операционными системами, такие как шрифты и размеры шрифтов, могут
кардинально изменить внешний вид и расположение виджетов в интерфейсе на
разных платформах. Наконец, использование абсолютного позиционирования
требует больших затрат времени разработчика, поскольку ему придется вычислять
точный размер и положение каждого виджета, одновременно решая такие проблемы,
как изменение размеров окна, добавление или удаление виджетов в графическом
интерфейсе.
В следующем разделе вы рассмотрите свой первый менеджер компоновки для
расположения виджетов по горизонтали или вертикали.

60 ГЛАВА 4 ИЗУЧЕНИЕ УПРАВЛЕНИЯ МАКЕТОМ


Горизонтальная и вертикальная компоновка с
помощью макетов блоков
Представьте, что у вас есть группа виджетов, которые вы хотите расположить в
своем окне, либо укладывая их друг на друга в столбик, либо отображая их рядом
друг с другом в один ряд. Менеджер компоновки QBoxLayout отлично подходит для
любой из этих ситуаций. QBoxLayout использует пространство, предоставленное ему
от родителя, для назначения каждому виджету ячейки с определенным количеством
пространства. Количество пространства основано на ряде различных факторов,
таких как минимально допустимый размер виджета.
Хотя вы можете использовать QBoxLayout, PyQt также имеет два отдельных удобных
класса, которые происходят от QBoxLayout и предоставляют функциональность,
основанную на желаемой ориентации виджетов:
• QHBoxLayout - Располагает виджеты горизонтально слева направо или
наоборот.
• QVBoxLayout - Располагает виджеты вертикально сверху вниз или наоборот.
Необязательные параметры могут быть переданы в метод addWidget() для макетов
боксов, как показано в следующей строке:
addWidget(widget, stretch, alignment)
Параметр stretch означает коэффициент растяжения, или насколько виджет будет
растягиваться по отношению к другим виджетам в строке или столбце. Значение
для stretch - int, где 0 использует параметры виджета по умолчанию для установки
коэффициента растяжения. Виджеты располагаются пропорционально, и виджеты
с большими значениями растяжения будут занимать больше места. Виджеты также
можно выровнять по строке или столбцу с помощью аргумента alignment.
Следующие приложения предоставляют отдельные примеры использования
QHBoxLayout и QVBoxLayout. Они также используют сигналы и слоты для создания
полных, практичных программ.

Пояснение для QHBoxLayout


Для этого проекта графический интерфейс пользователя на Рисунке 4-1 состоит из
трех основных виджетов: QLabel, QLineEdit и QPushButton. Эти виджеты расположены
горизонтально с помощью QHBoxLayout.

ГЛАВА 4 ИЗУЧЕНИЕ УПРАВЛЕНИЯ МАКЕТОМ 61


Рисунок 4-1. Простой пример QHBoxLayout с каждым виджетом, расположенным
горизонтально в GUI
Приложение в Листингах 4-1 - 4-3 позволяет пользователю ввести имя пользователя
в виджет QLineEdit. При редактировании текста выдается сигнал, который проверяет,
что вводимый текст имеет определенную длину и содержит только алфавитные или
цифровые символы.
Чтобы начать работу, создайте новый сценарий и скопируйте код сценария
basic_window.py из главы 1 в свой новый файл.
Листинг 4-1. Настройка основного окна для использования QHBoxLayout
# horizontal_box.py
# Импортируйте необходимые модули
import sys
from PyQt6.QtWidgets import (QApplication, QWidget, QLabel,
QLineEdit, QPushButton, QHBoxLayout)

class MainWindow(QWidget):

def __init__(self):
super().__init__()
self.initializeUI()

def initializeUI(self):
"""Настройте графический интерфейс приложения."""
self.setMinimumWidth(500)
self.setFixedHeight(60)
self.setWindowTitle("Пример QHBoxLayout")
self.setUpMainWindow()
self.show()

if __name__ == '__main__':
app = QApplication(sys.argv)
window = MainWindow()
sys.exit(app.exec())

Обязательно импортируйте классы виджетов вместе с QHBoxLayout из QtWidgets.


Хотя классы менеджера компоновки находятся в модуле QtWidgets, они не являются
виджетами и не наследуют QWidget. Вместо этого они происходят от QLayout,
базового класса для менеджеров компоновки.

62 ГЛАВА 4 ИЗУЧЕНИЕ УПРАВЛЕНИЯ МАКЕТОМ


Вы можете использовать комбинацию методов, чтобы определить, как окно может
изменить размер. Здесь минимальная ширина окна устанавливается с помощью
setMinimumWidth(), чтобы его можно было изменять по горизонтали, а высота
фиксируется с помощью setFixedHeight().
В листинге 4-2 начните с создания трех виджетов в setUpMainWindow().
Листинг 4-2. Метод setUpMainWindow() для использования QHBoxLayout
# horizontal_box.py
def setUpMainWindow(self):
"""Создаем и располагаем виджеты в главном окне."""
name_label = QLabel("Новое имя пользователя:")
name_edit = QLineEdit()
name_edit.setClearButtonEnabled(True)
name_edit.textEdited.connect(self.checkUserInput)
self.accept_button = QPushButton("Подтвердить")
self.accept_button.setEnabled(False)
self.accept_button.clicked.connect(self.close)
main_h_box = QHBoxLayout()
main_h_box.addWidget(name_label)
main_h_box.addWidget(name_edit)
main_h_box.addWidget(self.accept_button)
self.setLayout(main_h_box)

Для виджетов QLineEdit метод setClearButtonEnabled() полезен для отображения


кнопки очистки, когда в поле редактора есть текст. Пример прозрачной кнопки
показан на рисунке 4-1.
Некоторые виджеты, такие как QPushButton, могут быть включены или выключены
с помощью метода QWidget setEnabled(). Объект accept_button в этой программе
становится отключенным. Затем можно использовать сигналы и слоты для проверки
выполнения определенных параметров, чтобы переключать состояния виджета.
Пример этого показан в Листинге 4-3, где слот checkUserInput() подключен к сигналу
clicked для accept_button.
Прежде чем двигаться дальше, посмотрите, как устроен экземпляр QHBoxLayout.
В менеджерах компоновки виджеты добавляются последовательно. Поэтому первый
виджет, добавленный с помощью addWidget() в QHBoxLayout, name_label, будет самым
левым виджетом. За ним следуют name_edit и accept_button.
Листинг 4-3. Код для слота checkUserInput()
# horizontal_box.py
def checkUserInput(self, text):
"""Проверьте длину и содержание name_edit."""
if len(text) > 0 \
and all(t.isalpha() or t.isdigit() for t in text):
self.accept_button.setEnabled(True)
else: self.accept_button.setEnabled(False)
ГЛАВА 4 ИЗУЧЕНИЕ УПРАВЛЕНИЯ МАКЕТОМ 63
Когда пользователь редактирует текст в name_edit, сигнал textEdited вызовет
checkUserInput(). Этот сигнал дает нам доступ к текущему тексту. Если длина текста
не меньше 1 и он содержит только буквы или цифры, то кнопка accept_button будет
включена.
Теперь можно запустить приложение, а в качестве теста следует изменить
размер окна и посмотреть, как виджеты также растягиваются, чтобы использовать
пространство. Далее рассмотрим QVBoxLayout.

Пояснение для QVBoxLayout


Создание опроса, подобного тому, что показан на Рисунке 4-2, для сбора данных
от пользователей может быть очень полезным для бизнеса или для исследований.
В следующей программе мы рассмотрим, как использовать класс QVBoxLayout для
создания простого окна, которое отображает вопрос для пользователя и позволяет
ему выбрать ответ.

Рисунок 4-2. Пример простого QVBoxLayout с каждым виджетом, расположенным


вертикально в GUI
При создании графического интерфейса пользователя в Листингах 4-4-4-6
мы также рассмотрим, как организовать и управлять группами кнопок. Для этого
графического интерфейса мы начнем со сценария basic_window.py из Главы 1.
Листинг 4-4. Настройка основного окна для использования QVBoxLayout
# vertical_box.py
# Импортируйте необходимые модули
import sys
from PyQt6.QtWidgets import (QApplication, QWidget, QLabel,
QCheckBox, QPushButton, QButtonGroup, QVBoxLayout)
from PyQt6.QtCore import Qt
from PyQt6.QtGui import QFont

class MainWindow(QWidget):

def __init__(self):
64 ГЛАВА 4 ИЗУЧЕНИЕ УПРАВЛЕНИЯ МАКЕТОМ
super().__init__()
self.initializeUI()

def initializeUI(self):
"""Настройте графический интерфейс приложения."""
self.setMinimumSize(350, 200)
self.setWindowTitle("Пример QVBoxLayout")

self.setUpMainWindow()
self.show()

if __name__ == '__main__':
app = QApplication(sys.argv)
window = MainWindow()
sys.exit(app.exec())

Обязательно импортируйте необходимые классы и настройте главное окно.


Каждый из виджетов на Рисунке 4-2 расположен в окне вертикально. Начнем с
того, что зададим текст и выравнивание для заголовка и меток вопросов в Листинге
4-5.
Листинг 4-5. Метод setUpMainWindow() для использования QVBoxLayout, часть 1
# vertical_box.py
def setUpMainWindow(self):
"""Создайте и расположите виджеты в главном окне."""
header_label = QLabel("Chez PyQt6")
header_label.setFont(QFont("Arial", 18))
header_label.setAlignment(
Qt.AlignmentFlag.AlignCenter)
question_label = QLabel(
"Как бы вы оценили свой сервис?")
question_label.setAlignment(Qt.AlignmentFlag.AlignTop)

Виджеты QCheckBox также располагаются в окне с помощью менеджера


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

Класс QButtonGroup
Часто у вас может быть несколько флажков или кнопок, которые необходимо
сгруппировать вместе, чтобы облегчить управление ими. К счастью, в PyQt есть
класс QButtonGroup, который помогает управлять связанными кнопками, делая их
взаимоисключающими. Это может быть полезно, если вы хотите, чтобы одновременно

ГЛАВА 4 ИЗУЧЕНИЕ УПРАВЛЕНИЯ МАКЕТОМ 65


был установлен только один флажок.
Чтобы добавить кнопки в QButtonGroup и в окно, вы можете использовать
следующий порядок:
1. Создайте экземпляр QButtonGroup и сделайте его членом класса, то есть
создайте переменную экземпляра класса, то есть self.
button_group, или передать родительский объект в качестве аргумента, т.е,
button_group = QButtonGroup(self).
2. Создайте объекты кнопки или флажка, которые будут добавлены в группу
кнопок из шага 1.
3. Добавьте кнопки из шага 2 в группу кнопок с помощью метода
QButtonGroup методом addButton().
4. Подключите все кнопки в группе к одному сигналу, например, к сигналу
QButtonGroup сигнал buttonClicked.
5. Добавьте виджеты, созданные в шаге 2, в менеджер компоновки.
QButtonGroup на самом деле не является виджетом, а представляет собой
абстрактный контейнер вокруг добавленных в него кнопок. Поэтому вы не можете
фактически добавить QButtonGroup в макет. Процедура добавления флажков в
QButtonGroup показана в Листинге 4-6.

Листинг 4-6. Метод setUpMainWindow() для использования QVBoxLayout, часть 2

# vertical_box.py
ratings = ["Удовлетворен", "Средне", "Не удовлетворен"]
ratings_group = QButtonGroup(self)
ratings_group.buttonClicked.connect(
self.checkboxClicked)

self.confirm_button = QPushButton("Подтвердить")
self.confirm_button.setEnabled(False)
self.confirm_button.clicked.connect(self.close)

main_v_box = QVBoxLayout()
main_v_box.addWidget(header_label)
main_v_box.addWidget(question_label)

for cb in range(len(ratings)):
rating_cb = QCheckBox(ratings[cb])
ratings_group.addButton(rating_cb)
main_v_box.addWidget(rating_cb)

main_v_box.addWidget(self.confirm_button)
self.setLayout(main_v_box)

66 ГЛАВА 4 ИЗУЧЕНИЕ УПРАВЛЕНИЯ МАКЕТОМ


def checkboxClicked(self, button):
"""Проверяет, был ли нажат QCheckBox в группе кнопок."""
print(button.text())
self.confirm_button.setEnabled(True)

Здесь мы создаем QButtonGroup, ratings_group, для управления тремя виджетами


QCheckBox. Если выбран любой из взаимоисключающих чекбоксов, он будет издавать
сигнал, вызывающий checkboxClicked(). Слот просто демонстрирует, как проверить,
какой флажок установлен, и включает кнопку confirm_button.
Объекты QCheckBox создаются путем итерации над значениями в списке оценок.
Они также добавляются и в группу кнопок, и в менеджер макетов окна, main_v_box,
в том же цикле.

Создание вложенных макетов


Иногда один менеджер компоновки не может удовлетворить все ваши потребности,
поскольку ваши интерфейсы становятся все более сложными. К счастью, в PyQt это
не так уж сложно, поскольку вы можете располагать макеты внутри других макетов
для решения сложных вопросов расположения.
В предыдущих примерах было показано, как применять макеты к виджету
с помощью метода setLayout(), создавая таким образом родительский макет.
Для графического интерфейса пользователя на Рисунке 4-3 вы заметите, что
некоторые виджеты расположены вертикально, а другие - горизонтально. Виджеты,
расположенные рядом, помещаются во внутренние макеты, а затем эти макеты
добавляются к родительскому макету с помощью метода addLayout() менеджера
макетов.

Рисунок 4-3. Виджеты, расположенные с помощью комбинации менеджеров


компоновки

ГЛАВА 4 ИЗУЧЕНИЕ УПРАВЛЕНИЯ МАКЕТОМ 67


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

Пояснения к вложенным макетам


В дополнение к созданию вложенных макетов в этой программе вы также узнаете
о двух новых типах виджетов, вращающемся поле и комбинированном поле, которые
полезны для выбора одного варианта из набора значений.
Листинг 4-7 начинается с импорта классов, которые нам понадобятся, и установки
класса MainWindow.
Листинг 4-7. Настройка главного окна для использования вложенных макетов
# nested.py
# Import necessary modules
import sys
from PyQt6.QtWidgets import (QApplication, QWidget, QLabel,
QComboBox, QSpinBox, QHBoxLayout, QVBoxLayout)
from PyQt6.QtCore import Qt
from PyQt6.QtGui import QFont

class MainWindow(QWidget):

def __init__(self):
super().__init__()
self.initializeUI()

def initializeUI(self):
"""Set up the application's GUI."""
self.setMinimumSize(400, 160)
self.setWindowTitle("Nested Layout Example")

self.setUpMainWindow()
self.show()

if __name__ == '__main__':
app = QApplication(sys.argv)
window = MainWindow()
sys.exit(app.exec())

Запустив класс MainWindow, давайте начнем создавать метод setUpMainWindow() и


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

68 ГЛАВА 4 ИЗУЧЕНИЕ УПРАВЛЕНИЯ МАКЕТОМ


Виджеты QSpinBox и QComboBox
Вместо того чтобы использовать виджет QLineEdit для ввода информации, иногда
вы можете захотеть, чтобы пользователю было позволено выбирать только из списка
заранее определенных значений или числовых диапазонов.
QSpinBox создает объект, который похож на текстовое поле, но позволяет
пользователю выбирать целочисленные значения, либо вводя значение в виджет,
либо нажимая на стрелки вверх и вниз. Вы также можете редактировать диапазон
значений, устанавливать размер шага при нажатии на стрелку, задавать начальное
значение или даже добавлять префиксы или суффиксы в поле. Существуют классы,
похожие на QSpinBox, которые обеспечивают аналогичную функциональность для
различных ситуаций. QDoubleSpinBox используется для выбора чисел с плавающей
точкой. QDateTimeEdit или один из его вариантов полезен для выбора значений даты
и времени.
Виджет QComboBox отображает выпадающий список опций для выбора, когда
пользователь нажимает на кнопку со стрелкой виджета. Комбобоксы удобны для
отображения большого количества вариантов на минимальном пространстве.
В Листинге 4-8 мы рассмотрим, как создать оба вида виджетов в методе
setUpMainWindow().
Листинг 4-8. Создание виджетов в методе setUpMainWindow()
# nested.py
def setUpMainWindow(self):
"""Создание и расположение виджетов в главном окне."""
info_label = QLabel(
"Выберите 2 блюда для обеда и их стоимость.")
info_label.setFont(QFont("Arial", 16))
info_label.setAlignment(Qt.AlignmentFlag.AlignCenter)

# Создайте список продуктов питания и два отдельных виджета


# QComboBox для отображения всех элементов
food_list = ["яйцо", "сэндвич с индейкой", "сэндвич с ветчиной",
"сыр", "хумус", "йогурт", "яблоко", "банан",
"апельсин", "вафля", "морковь", "хлеб", "макароны",
"крекеры", "крендельки", "кофе", "газировка", "вода"]

food_combo1 = QComboBox()
food_combo1.addItems(food_list)
food_combo2 = QComboBox()
food_combo2.addItems(food_list)

# Create two QSpinBox widgets to display prices


self.price_sb1 = QSpinBox()
self.price_sb1.setRange(0, 100)

ГЛАВА 4 ИЗУЧЕНИЕ УПРАВЛЕНИЯ МАКЕТОМ 69


self.price_sb1.setPrefix("₽")
self.price_sb1.valueChanged.connect(
self.calculateTotal)

self.price_sb2 = QSpinBox()
self.price_sb2.setRange(0, 100)
self.price_sb2.setPrefix("₽")
self.price_sb2.valueChanged.connect(
self.calculateTotal)

Мы создаем два отдельных комбобокса, food_combo1 и food_combo2, и добавляем


список товаров, которые мы хотим отобразить в каждом из них, используя метод
addItems(). После этого создаются два отдельных регулятора: price_sb1 и price_sb2.
Метод setRange() используется для установки верхней и нижней границ
регулятора, а setPrefix() можно использовать для отображения другого текста внутри
текстового поля, в данном случае знака рубля. Это может быть полезно, чтобы дать
пользователю больше информации о назначении виджета.
Наконец, когда мы изменяем значения в регуляторах, они оба посылают сигнал,
который подключается к методу calculateTotal(). Это динамически обновит значение
для totals_label, который был создан в Листинге 4-9.

Комбинирование макетов и расположение


виджетов
Процесс комбинирования макетов заключается в размещении одного типа
менеджера макетов внутри другого типа. В данном примере мы будем комбинировать
два макета боксов, чтобы получить преимущества как горизонтального, так и
вертикального расположения.
В Листинге 4-9 мы можем расположить регуляторы и комбобоксы из Листинга 4-8
в отдельных горизонтальных макетах, item1_h_box и item2_h_box.

Примечание. Поскольку два объекта QComboBox и два объекта QSpinBox


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

70 ГЛАВА 4 ИЗУЧЕНИЕ УПРАВЛЕНИЯ МАКЕТОМ


Листинг 4-9. Создание вложенного макета в setUpMainWindow()
# nested.py
# Создайте два горизонтальных макета для
# виджетов QComboBox и QSpinBox
item1_h_box = QHBoxLayout()
item1_h_box.addWidget(food_combo1)
item1_h_box.addWidget(self.price_sb1)

item2_h_box = QHBoxLayout()
item2_h_box.addWidget(food_combo2)
item2_h_box.addWidget(self.price_sb2)

self.totals_label = QLabel("Total Spent: $")


self.totals_label.setFont(QFont("Arial", 16))
self.totals_label.setAlignment(
Qt.AlignmentFlag.AlignRight)

# Упорядочить виджеты и макеты в главном окне


main_v_box = QVBoxLayout()
main_v_box.addWidget(info_label)
main_v_box.addLayout(item1_h_box)
main_v_box.addLayout(item2_h_box)
main_v_box.addWidget(self.totals_label)

# Установите макет для главного окна


self.setLayout(main_v_box)

Дополнительная метка, totals_label, будет отображать суммирование значений в


виджетах QSpinBox.
На данном этапе у вас есть две метки и два экземпляра QHBoxLayout, которые
необходимо добавить в окно. Вам нужно будет создать родительский макет для
хранения этих объектов. Здесь родительским макетом является main_v_box. Ярлыки
добавляются с помощью addWidget(), а макеты добавляются с помощью addLayout().
Эта схема изображена на Рисунке 4-4, где родительский QVBoxLayout представлен
сплошными линиями, а внутренние экземпляры QHBoxLayout изображены
пунктирными линиями.

ГЛАВА 4 ИЗУЧЕНИЕ УПРАВЛЕНИЯ МАКЕТОМ 71


Рисунок 4-4. Визуализация вложенного макета
Последняя задача в Листинге 4-10 - создать слот, который вычисляет значения из
регуляторов и обновляет текст для totals_label.
Листинг 4-10. Код для слота calculateTotal()
# nested.py
def calculateTotal(self, value):
"""Рассчитайте общую цену и обновите totals_label."""
total = self.price_sb1.value() + \
self.price_sb2.value()
self.totals_label.setText(f"Всего потрачено: ${total}")

На этом этапе вы закончили изучение основ работы с блочными макетами.


Давайте перейдем к рассмотрению другого встроенного менеджера компоновки.

Размещение виджетов в сетках с помощью


QGridLayout
Менеджер компоновки QGridLayout используется для расположения виджетов
в строках и столбцах, подобно электронной таблице или матрице. Этот менеджер
компоновки берет пространство в родительском окне или виджете и делит его в
соответствии с размерами виджетов в этой строке (или столбце). Также возможно
добавление пространства между виджетами, создание границы или растягивание
виджетов на несколько строк или столбцов.
Понимание того, как добавлять виджеты и управлять ими с помощью QGridLayout,
довольно простое. Значения индексов в сетке начинаются с (0,0), которая является
самой верхней левой ячейкой. Первое значение - это строка, а второе - столбец. Чтобы
добавить виджет под ним (следующий ряд), просто добавьте 1 к первому значению
(1,0). Чтобы продолжить движение вниз по строкам, продолжайте увеличивать
первое значение. Для перемещения по столбцам увеличивайте второе значение.
Метод addWidget() для QGridLayout имеет две формы. Первая показана в следующей
строке:

72 ГЛАВА 4 ИЗУЧЕНИЕ УПРАВЛЕНИЯ МАКЕТОМ


addWidget(widget, row, column, alignment)
Здесь виджет(widget) добавляется в указанную строку(row) и столбец(column) с
необязательным выравниванием(alignment). Другая форма позволяет виджетам
распространяться на несколько строк, столбцов или на оба столбца:
addWidget(widget, fromRow, fromColumn, rowSpan, columnSpan, alignment)
Аргументы fromRow и fromColumn задают начальную строку и столбец
соответственно. Аргументы rowSpan и columnSpan принимают целочисленные
значения и определяют, сколько строк и столбцов будет занимать виджет
соответственно.
В этом разделе вы узнаете, как использовать GridLayout для создания графического
интерфейса ежедневника, показанного на Рисунке 4-5.

Рисунок 4-5. Простой ежедневник, использующий QGridLayout для организации


виджетов
Некоторые списки дел разбиты по часам дня, по важности целей или по
приоритетности задач, которые необходимо выполнить в этот день, неделю или даже
месяц. Как только цель выполнена, нам нужен какой-то способ отметить задачу или
удалить ее.
Приложение состоит из трех частей: область для записи самой важной задачи на
сегодня, место для заметок и раздел для записи ежедневных задач. Мы также кратко
рассмотрим некоторые новые классы PyQt для работы с большими областями текста
и для работы с датами.

ГЛАВА 4 ИЗУЧЕНИЕ УПРАВЛЕНИЯ МАКЕТОМ 73


Пояснения к QGridLayout
Мы можем начать построение примера использования QGridLayout с импорта
необходимых нам классов и создания класса MainWindow в Листинге 4-11. Для начала
работы вы можете использовать сценарий basic_window.py из Главы 1.
Листинг 4-11. Настройка главного окна для использования QGridLayout
# grid.py
# Импортируйте необходимые модули
import sys, json
from PyQt6.QtWidgets import (QApplication, QWidget, QLabel,
QLineEdit, QCheckBox, QTextEdit, QGridLayout)
from PyQt6.QtCore import Qt, QDate
from PyQt6.QtGui import QFont

class MainWindow(QWidget):

def __init__(self):
super().__init__()
self.initializeUI()

def initializeUI(self):
"""Настройте графический интерфейс приложения."""
self.setMinimumSize(500, 300)
self.setWindowTitle("QGridLayout пример")

self.setUpMainWindow()
self.loadWidgetValuesFromFile()
self.show()

if __name__ == '__main__':
app = QApplication(sys.argv)
window = MainWindow()
sys.exit(app.exec())

Чтобы продолжить создание интерфейса ежедневника, нам понадобится изучить


класс виджета, который может работать с большими полями текста.

Виджет QTextEdit
Когда пользователю требуется область для ввода или редактирования более одной
строки текста за раз, класс QTextEdit хорошо подходит для изменения обычного
или насыщенного текста и включает встроенные функции редактирования, такие
74 ГЛАВА 4 ИЗУЧЕНИЕ УПРАВЛЕНИЯ МАКЕТОМ
как копирование, вставка и вырезание. Виджет может работать с символами или
абзацами текста. Параграфы - это просто длинные строки, которые заворачиваются
в виджет и заканчиваются символом новой строки. QTextEdit также полезен для
отображения списков, изображений и таблиц или для предоставления интерфейса
для отображения текста с помощью HTML.
Мы можем начать создание виджетов в левой части главного окна в Листинге 4-12,
начиная с ярлыка(label) заголовка, объектов QTextEdit и соответствующих им ярлыков.
Листинг 4-12. Метод setUpMainWindow() для использования QGridLayout, Часть 1
# grid.py
def setUpMainWindow(self):
"""Создать и расположить виджеты в главном окне."""
header_label = QLabel("Простой ежедневник")
header_label.setFont(QFont("Arial", 20))
header_label.setAlignment(Qt.AlignmentFlag.AlignLeft)

# Создайте виджеты для левой стороны окна


today_label = QLabel("•Сегодня сфокусироваться на")
today_label.setFont(QFont("Arial", 14))
self.today_tedit = QTextEdit()

notes_label = QLabel("•Заметки")
notes_label.setFont(QFont("Arial", 14))
self.notes_tedit = QTextEdit()

Теперь виджеты готовы к добавлению в менеджер компоновки.

Добавление виджетов и разнесение строк и


столбцов в QGridLayout
Поскольку виджеты будут размещаться в структуре, подобной сетке, при
добавлении нового объекта в макет необходимо указать значения строк и столбцов в
качестве параметров метода addWidget() или addLayout(). Посмотрите Листинг 4-13,
чтобы увидеть пример этого.

ГЛАВА 4 ИЗУЧЕНИЕ УПРАВЛЕНИЯ МАКЕТОМ 75


Листинг 4-13. Метод setUpMainWindow() для использования GridLayout, часть 2
# grid.py
# Организуем виджеты левой стороны в столбце 0 QGridLayout
self.main_grid = QGridLayout()
self.main_grid.addWidget(header_label, 0, 0)
self.main_grid.addWidget(today_label, 1, 0)
self.main_grid.addWidget(self.today_tedit, 2, 0, 3, 1)
self.main_grid.addWidget(notes_label, 5, 0)
self.main_grid.addWidget(self.notes_tedit, 6, 0, 3, 1)

Объект header_label добавляется в макет main_grid в позицию, где строка(row)


равна 0 и колонка(column) равна 0, что также является левым верхним углом.
Затем объект today_label добавляется непосредственно под header_label, увеличивая
значение строки до 1 и оставляя значение столбца равным 0. Объект today_tedit
добавляется в строку 2.
Что происходит, если в строке или столбце есть виджет, который должен занимать
больше места по вертикали или горизонтали? Давайте рассмотрим два различных
способа разрешения таких ситуаций.
Первый подход предполагает пропуск нескольких строк или столбцов,
чтобы позволить определенным видам виджетов, таким как QTextEdit, занять
дополнительное пространство. Если вы обратитесь к Рисунку 4-5, вы увидите, что
виджеты QTextEdit занимают больше места в окне, чем виджеты QLabel или QLineEdit.
Чтобы виджет today_tedit мог растянуться на три строки, следующий виджет,
notes_label, размещается в строке с индексом 5.
Второй способ использования пространства виджетами в QGridLayout заключается
в указании количества строк и столбцов, которые виджет должен охватывать.
Охватывание можно рассматривать как растягивание виджета по горизонтали
или вертикали, чтобы использовать пространство и помочь в организации окна.
Посмотрите на следующую строку:
self.main_grid.addWidget(self.today_tedit, 2, 0, 3, 1)
Дополнительные два параметра в конце, 3 и 1, сообщают менеджеру макета,
что виджет будет занимать три строки и один столбец. Это заставляет виджет
растягиваться по вертикали. Правая сторона окна обрабатывается в Листинге 4-14.

76 ГЛАВА 4 ИЗУЧЕНИЕ УПРАВЛЕНИЯ МАКЕТОМ


Листинг 4-14. Метод setUpMainWindow() для использования QGridLayout, часть 3
# grid.py
# Создание виджетов для правой стороны окна
today = QDate.currentDate().toString(
Qt.DateFormat.ISODate)
date_label = QLabel(today)
date_label.setFont(QFont("Arial", 18))
date_label.setAlignment(Qt.AlignmentFlag.AlignRight)

todo_label = QLabel("•Делать")
todo_label.setFont(QFont("Arial", 14))

# Организуйте виджеты правой стороны в


# столбцы 1 и 2 QGridLayout
self.main_grid.addWidget(date_label, 0, 2)
self.main_grid.addWidget(todo_label, 1, 1, 1, 2)

# Создайте 7 строк из индексов 2-8


for row in range(2, 9):
item_cb = QCheckBox()
item_edit = QLineEdit()
self.main_grid.addWidget(item_cb, row, 1)
self.main_grid.addWidget(item_edit, row, 2)

# Установите макет для главного окна


self.setLayout(self.main_grid)
Дата, которая появляется в правом верхнем углу окна на Рисунке 4-5, будет ме-
няться в зависимости от того, в какой день пользователь открывает приложение.
Это происходит благодаря классу QDate. Текущая дата извлекается с помощью
QDate.currentDate(), преобразуется в читаемую строку с помощью toString() и пере-
дается в переменную today. Перечисление Qt.DateFormat используется для установ-
ки формата представления даты пользователю. В PyQt существует несколько раз-
личных способов форматирования дат, например, TextDate для текстовых дат или
ISODate, использующий стандартный стиль ISO 8601. Строка today отображается в
QLabel, date_label.
Для группы из семи виджетов QCheckBox и QLineEdit, которые формируют список
дел, используется цикл for для создания каждого объекта и добавления их в нужную
строку и столбец main_grid.
На этом можно было бы остановиться, но давайте сделаем еще один шаг вперед
и выясним, как использовать сигналы и слоты для сохранения значений виджетов
редактирования текста и определения того, какие пункты дел нужно сохранить на
следующий день. Это можно сделать, посмотрев на состояния виджетов QCheckBox.
Для этого нам нужно выяснить, как найти дочерние виджеты после их добавления в
менеджер компоновки.
ГЛАВА 4 ИЗУЧЕНИЕ УПРАВЛЕНИЯ МАКЕТОМ 77
Поиск дочерних виджетов в макете
Часто возникает задача сбора значений из виджетов, возможно, для обновления
информации другого виджета или для сохранения данных при закрытии
приложения. Во многих случаях переменные экземпляра, созданные с помощью self,
могут использоваться во всем классе для изменения и обновления значений. Однако,
как мы видели в Листинге 4-14, виджеты QCheckBox и QLineEdit были итеративно
добавлены в макет без четкого имени переменной.
Класс QWidget уже имеет методы для поиска дочерних виджетов, но для
этого примера мы изучим, как дочерние элементы располагаются в менеджерах
компоновки, а именно в QGridLayout.
Для некоторых приложений может быть важно сохранить информацию в
окне. Когда пользователь закрывает ежедневник, сохраняется запись о заметках
предыдущего дня и незавершенных задачах. Метод MainWindow для обработки этих
операций, saveWidgetValues(), построен в Листинге 4-15.
Листинг 4-15. Код метода saveWidgetValues()
# grid.py
def saveWidgetValues(self):
"""Соберите и сохраните значения виджета."""
details = {"focus": self.today_tedit.toPlainText(),
"заметки": self.notes_tedit.toPlainText()}
remaining_todo = []

# Проверка значений виджетов QCheckBox


for row in range(2, 9):
# Получение объекта QLayoutItem
item = self.main_grid.itemAtPosition(row, 1)
# Получение виджета (QCheckBox)
widget = item.widget()
if widget.isChecked() == False:
# Получение объекта QLayoutItem
item = self.main_grid.itemAtPosition(row, 2)
# Получение виджета (QLineEdit)
widget = item.widget()
text = widget.text()
if text != "":
remaining_todo.append(text)
# Сохранение текста из виджетов QLineEdit
details["todo"] = remaining_todo

with open("details.txt", "w") as f:


f.write(json.dumps(details))

78 ГЛАВА 4 ИЗУЧЕНИЕ УПРАВЛЕНИЯ МАКЕТОМ


def closeEvent(self, event):
"""Сохранение значений виджета при закрытии окна."""
self.saveWidgetValues()

Значения для виджетов QTextEdit и QLineEdit хранятся в Python dict, что упрощает
процесс сохранения данных в формате JSON. Чтобы вернуть простой текст для
today_tedit и notes_tedit, используйте метод QTextEdit toPlainText(). Если вам нужно
вернуть насыщенный текст, используйте toHtml().
Когда виджеты добавляются в макеты, они добавляются как объекты QLayoutItem.
Метод QGridLayout itemAtPosition() используется для получения элементов по
заданным значениям строки и столбца. Перебирая индексы строк от 2 до 8, мы
можем узнать, какие виджеты QCheckBox отмечены с помощью функции isChecked().

Совет. Для макетов боксов и QFormLayout можно использовать метод


itemAt(index), где index - индекс виджета в макете. Обратите внимание, что
этот метод возвращает объект QWidgetItem, а не сам виджет. Поэтому для
взаимодействия с виджетом вам нужно будет вызвать метод widget() на
возвращенном элементе.

Задачи, которые завершены, должны быть отмечены. Поэтому если флажок не


установлен, мы сохраним текст из соответствующего QLinedEdit в столбце 2, если
поле QTextEdit не пусто. Наконец, словарь деталей(details) записывается в текстовый
файл. Все это произойдет, когда окно закроется с помощью closeEvent().
Загрузка данных в виджеты из предыдущей сессии выполняется в Листинге 4-16.
Если приложение запускается впервые, то предложение try пропускается, а файл
details.txt создается в предложении except.

ГЛАВА 4 ИЗУЧЕНИЕ УПРАВЛЕНИЯ МАКЕТОМ 79


Листинг 4-16. Код для метода loadWidgetValuesFromFile()
# grid.py
def loadWidgetValuesFromFile(self):
"""Получите предыдущие значения из последней сессии."""
# Сначала проверьте, существует ли файл
try:
with open("details.txt", "r") as f:
details = json.load(f)
# Получение и установка значений для виджетов
self.today_tedit.setText(details["focus"])
self.notes_tedit.setText(details["заметки"])

# Установите текст для виджетов QLineEdit


for row in range(len(details["todo"])):
# Получаем объект QLayoutItem
item = self.main_grid.itemAtPosition(
row + 2, 2)
# Получение виджета (QLineEdit)
widget = item.widget()
widget.setText(details["todo"][row])
except FileNotFoundError as error:
# Создаем файл, так как он не существует
f = open("details.txt", "w")

В противном случае текстовый файл загружается с помощью json.load(). Текстовые


значения для виджетов QTextEdit и QLineEdit из предыдущей сессии устанавливаются
в пункте try. Объекты QLineEdit находятся с помощью функции itemAtPosition().

Создание форм с помощью QFormLayout


Для ситуаций, когда вам нужно создать форму для сбора информации от
пользователя, PyQt предоставляет класс QFormLayout. Это менеджер компоновки,
который организует дочерние виджеты в двухколоночный макет, левая колонка
которого состоит из меток, а правая - из виджетов полей ввода, таких как QLineEdit
или QSpinBox. Класс QFormLayout делает проектирование такого рода графических
интерфейсов очень удобным.
Для приложения на Рисунке 4-6 давайте рассмотрим создание формы, которую
кто-то мог бы использовать для назначения встречи.

80 ГЛАВА 4 ИЗУЧЕНИЕ УПРАВЛЕНИЯ МАКЕТОМ


Рисунок 4-6. Форма приложения, созданная с помощью QFormLayout
В этом приложении вы также познакомитесь с классом для сопоставления строк
с помощью регулярных выражений в PyQt, который имеет функциональность,
аналогичную модулю re в Python или стороннему модулю regex.

Пояснение для QFormLayout


Форма приложения состоит из ряда различных виджетов, включая QLabel,
QLineEdit, QComboBox, QTextEdit и QPushButton. Новый класс, QDateEdit, похож
на QSpinBox, но обеспечивает специфическую функциональность для выбора и
редактирования дат.
Давайте используем сценарий basic_window.py для основы кода в Листинге 4-17 и
начнем с импорта необходимых классов PyQt. Этот графический интерфейс также содер­
жит несколько экземпляров вложенных макетов, поэтому в него включен QHBoxLayout.
ГЛАВА 4 ИЗУЧЕНИЕ УПРАВЛЕНИЯ МАКЕТОМ 81
Листинг 4-17. Настройка главного окна для использования QFormLayout
# form.py
# Импортируйте необходимые модули
import sys
from PyQt6.QtWidgets import (QApplication, QWidget, QLabel,
QPushButton, QDateEdit, QLineEdit, QTextEdit, QComboBox,
QFormLayout, QHBoxLayout)
from PyQt6.QtCore import Qt, QRegularExpression, QDate
from PyQt6.QtGui import QFont, QRegularExpressionValidator

class MainWindow(QWidget):

def __init__(self):
super().__init__()
self.initializeUI()

def initializeUI(self):
"""Настройте графический интерфейс приложения."""
self.setMinimumSize(500, 400)
self.setWindowTitle("QFormLayout пример")

self.setUpMainWindow()
self.show()

if __name__ == '__main__':
app = QApplication(sys.argv)
window = MainWindow()
sys.exit(app.exec())

При создании полей редактирования имейте в виду, что вам не нужно будет
создавать виджет QLabel, который обычно предшествует им. С помощью QFormLayout
можно добавить два виджета подряд. Пока мы просто установим виджеты
редактирования текста в Листинге 4-18.

82 ГЛАВА 4 ИЗУЧЕНИЕ УПРАВЛЕНИЯ МАКЕТОМ


Листинг 4-18. Метод setUpMainWindow() для использования QFormLayout, часть 1
# form.py
def setUpMainWindow(self):
"""Создание и расположение виджетов в главном окне."""
header_label = QLabel("Форма для приёма")
header_label.setFont(QFont("Arial", 18))
header_label.setAlignment(
Qt.AlignmentFlag.AlignCenter)

self.first_name_edit = QLineEdit()
self.first_name_edit.setPlaceholderText("Имя")
self.first_name_edit.textEdited.connect(
self.clearText)
self.last_name_edit = QLineEdit()
self.last_name_edit.setPlaceholderText("Фамилия")
self.last_name_edit.textEdited.connect(self.clearText)

# Создайте горизонтальное расположение имен


name_h_box = QHBoxLayout()
name_h_box.addWidget(self.first_name_edit)
name_h_box.addWidget(self.last_name_edit)

Несмотря на то, что перед виджетами first_name_edit и last_name_edit на Рисунке


4-6 существует метка Name, она еще не создана. Два виджета QLineEdit добавляются
в менеджер QHBoxLayout. Создание слота clearText() обрабатывается в Листинге 4-22.
Во второй части setUpMainWindow() в Листинге 4-19 добавляется больше виджетов
формы приложения.
Листинг 4-19. Метод setUpMainWindow() для использования QFormLayout, часть 2
# form.py
# Создание дополнительных виджетов для добавления в окно
gender_combo = QComboBox()
gender_combo.addItems(["Мужской", "Женский"])

self.phone_edit = QLineEdit()
self.phone_edit.setInputMask("(999) 999-9999;_")
self.phone_edit.textEdited.connect(self.clearText)

self.birthdate_edit = QDateEdit()
self.birthdate_edit.setDisplayFormat("yyyy/MM/dd")
self.birthdate_edit.setMaximumDate(
QDate.currentDate())
self.birthdate_edit.setCalendarPopup(True)
self.birthdate_edit.setDate(QDate.currentDate())

ГЛАВА 4 ИЗУЧЕНИЕ УПРАВЛЕНИЯ МАКЕТОМ 83


Виджеты в предыдущем блоке кода просят пользователя ввести свой пол,
номер телефона и дату рождения, используя различные классы виджетов. Объект
gender_combo представляет собой простой QComboBox.
В поле ввода QLineEdit можно ввести любой символ. Однако, если вы хотите
ограничить тип, размер или способ ввода символов, вы можете создать маску ввода,
вызвав метод setInputMask(). Символы маски в этом графическом интерфейсе поз­
воляют пользователю вводить только целые числа от 0 до 9. Конец последовательности,
;_, завершает маску ввода и устанавливает пустые символы на _.
Формат экземпляра QDateEdit для визуализации даты устанавливается с помощью
setDisplayFormat(). Максимальный диапазон виджета устанавливается на дату
открытия приложения пользователем с помощью QDate.currentDate(). QDateEdit имеет
удобную функцию setCalendarPopup(), которая позволяет отображать календарь
при нажатии на стрелку в виджете. Начальная дата, которая появляется в виджете,
устанавливается с помощью функции setDate().
Остальные виджеты графического интерфейса настроены в Листинге 4-20.
Листинг 4-20. Метод setUpMainWindow() для использования QFormLayout, часть 3
# form.py
self.email_edit = QLineEdit()
self.email_edit.setPlaceholderText(
"<username>@<domain>.com")
reg_opt = QRegularExpression()
regex = QRegularExpression(
"\\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\\.[com]{3}\\b",
reg_opt.PatternOption.CaseInsensitiveOption)
self.email_edit.setValidator(
QRegularExpressionValidator(regex))
self.email_edit.textEdited.connect(self.clearText)

extra_info_tedit = QTextEdit()
self.feedback_label = QLabel()
submit_button = QPushButton("ОТПРАВИТЬ")
submit_button.setMaximumWidth(140)
submit_button.clicked.connect(
self.checkFormInformation)

# Создание горизонтального расположения для последнего ряда виджетов


submit_h_box = QHBoxLayout()
submit_h_box.addWidget(self.feedback_label)
submit_h_box.addWidget(submit_button)

Другим типом информации, которую пользователю обычно нужно указать в


форме, является адрес электронной почты. Простого QLineEdit было бы достаточно,
но для сопоставления вводимой пользователем информации с общим форматом
адреса электронной почты мы можем использовать регулярные выражения.
84 ГЛАВА 4 ИЗУЧЕНИЕ УПРАВЛЕНИЯ МАКЕТОМ
Класс QRegularExpression предоставляет необходимые нам возможности шаблонов
и синтаксиса.

Примечание. В некоторой документации вы можете встретить класс Qt,


QRegExp, который имеет некоторое сходство с QRegularExpression. Однако
этот класс не включен в PyQt6, поскольку QRegularExpression содержит много
улучшений по сравнению с QRegExp.

Выражение, передаваемое экземпляру regex, является базовым


выражением для адресов электронной почты. Перечисление
QRegularExpression.PatternOption используется для указания способов интерпретации
строк, таких как CaseInsensitiveOption для нечувствительности к регистру или
DotMatchesEverythingOption, если вам нужно, чтобы точка, ., в строке шаблона
соответствовала любому символу.
Валидатор используется с некоторыми виджетами редактирования для
подтверждения того, что их содержимое соответствует ограничениям, заданным
разработчиком. QRegularExpressionValidator проверяет, соответствует ли переданная
ему строка заданному регулярному выражению, в данном случае экземпляру regex.
Наконец, нам нужно создать еще несколько виджетов. QLabel feedback_label
используется для уведомления пользователей о неправильной или отсутствующей
информации. Метка и кнопка submit_button добавляются в макет submit_h_box.

Добавление виджетов и макетов в QFormLayout


Добавление виджетов в QFormLayout отличается от компоновки боксов и
QGridLayout. Прежде всего, вместо addWidget() используется метод addRow(). Это также
происходит при добавлении других макетов. Во-вторых, несмотря на возможность
вложения макетов в QFormLayout, макет все равно вписывается в двухколоночную
структуру.
Давайте завершим создание setUpMainWindow() созданием экземпляра QFormLayout
и установкой некоторых параметров. Сначала создадим объект QFormLayout,
main_form, для главного окна.

ГЛАВА 4 ИЗУЧЕНИЕ УПРАВЛЕНИЯ МАКЕТОМ 85


Листинг 4-21. Размещение виджетов в макете формы в setUpMainWindow(), часть 4
# form.py
# Организуйте виджеты и макеты в QFormLayout
main_form = QFormLayout()
main_form.setFieldGrowthPolicy(
main_form.FieldGrowthPolicy.AllNonFixedFieldsGrow)
main_form.setFormAlignment(
Qt.AlignmentFlag.AlignHCenter | \
Qt.AlignmentFlag.AlignTop)
main_form.setLabelAlignment(
Qt.AlignmentFlag.AlignLeft)

main_form.addRow(header_label)
main_form.addRow("Имя", name_h_box)
main_form.addRow("Пол", gender_combo)
main_form.addRow("Дата рождения", self.birthdate_edit)
main_form.addRow("Телефон", self.phone_edit)
main_form.addRow("Email", self.email_edit)
main_form.addRow(QLabel("Комментарии или сообщения"))
main_form.addRow(extra_info_tedit)
main_form.addRow(submit_h_box)

# Установите макет для главного окна


self.setLayout(main_form)

Без указания каких-либо параметров или стилей класс QFormLayout будет


использовать родной стиль системы. Чтобы обойти это и гарантировать, что макет
формы будет выглядеть одинаково на разных системах, мы можем задать несколько
параметров.
Перечисление QFormLayout.FieldGrowthPolicy определяет, как виджеты
растягиваются в макете. Флаг AllNonFixedFieldsGrow гарантирует, что виджеты полей
будут расти горизонтально, чтобы заполнить дополнительное пространство. Метод
setFormAlignment() используется для задания выравнивания содержимого формы, а
setLabelAlignment() определяет выравнивание для меток.
Существует несколько различных способов добавления виджетов или макетов в
макет формы с помощью addRow(). Следующий список описывает их:
• addRow(QWidget(), QWidget()) - Добавляет два виджета в строку, где первый
виджет является меткой, а второй - виджетом поля. Первый QWidget обычно
является QLabel, но можно добавить и другие типы виджетов.
• addRow(QWidget(), QLayout()) - Добавляет виджет метки и макет в строку.
• addRow(string, QWidget()) - Добавляет строку и поле в строку.
• addRow(string, QLayout()) - Добавляет строку и макет в строку.
• addRow(QWidget()) - Добавляет один виджет в макет.
• addRow(QLayout()) - Вкладывает один макет в форму.

86 ГЛАВА 4 ИЗУЧЕНИЕ УПРАВЛЕНИЯ МАКЕТОМ


Примеры некоторых из них можно увидеть в Листинге 4-21. Добавляя QLabel и
виджет поля, например QLineEdit, в один ряд, можно создать приятеля для QLabel.
Слоты, к которым подключаются виджеты формы, созданы в Листинге 4-22. Слот
clearText() используется для очистки метки feedback_label всякий раз, когда текст
редактируется в одном из виджетов полей формы.
Листинг 4-22. Код для слотов clearText() и checkFormInformation()
# form.py
def clearText(self, text):
"""Очистить текст для QLabel, который предоставляет
обратную связь."""
self.feedback_label.clear()

def checkFormInformation(self):
"""Демонстрирует несколько случаев для проверки пользовательского
ввода."""
if self.first_name_edit.text() == "" or \
self.last_name_edit.text() == "":
self.feedback_label.setText(
"[INFO] Пропущенны имена.")
elif self.phone_edit.hasAcceptableInput() == False:
self.feedback_label.setText(
"[INFO] Неправильно введен номер телефона.")
elif self.email_edit.hasAcceptableInput() == False:
self.feedback_label.setText(
"[INFO] Email введен неверно.")

Обратная связь бывает разных форм. В функции checkFormInformation(), когда


пользователь нажимает кнопку submit_button в нижней части графического
интерфейса, проверяется ряд условий if. Если определенные поля не имеют
приемлемого ввода, то соответствующее сообщение будет выведено в feedback_label.
В следующем разделе рассматривается последний встроенный менеджер
компоновки.

Управление страницами с помощью QStackedLayout


Для некоторых интерфейсов может потребоваться показать некоторые виджеты
только до завершения определенных задач или организовать виджеты в группы и
устранить беспорядок. Обычным примером является веб-браузер, где вкладки в
верхней части окна помогают разделить различные веб-сайты.
Одним из способов создания такого типа макета является расположение
виджетов друг над другом с помощью QStackedLayout. Один виджет, добавленный в
QStackedLayout, служит страницей, а другие виджеты могут быть добавлены к этой

ГЛАВА 4 ИЗУЧЕНИЕ УПРАВЛЕНИЯ МАКЕТОМ 87


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

Рисунок 4-7. Пример нескольких страниц в одном окне. Изображение кошки взято
с сайта https://pixabay.com.

Совет. PyQt имеет удобный класс, QStackedWidget, который обеспечивает


ту же функциональность, что и QStackedLayout, с дополнительным преиму-
ществом методов QWidget. Другой виджет, QTabWidget (рассматривается в
главе 6), предоставляет вкладки.

Каждая страница добавляется в QStackedLayout с помощью addWidget().


Добавленные страницы управляются внутренним списком виджетов, и доступ
к каждой странице или виджету может быть осуществлен с помощью следующих
методов:
• currentIndex() - Возвращает индекс видимой страницы. Страницы имеют
значения индекса, начальный индекс равен 0.
• currentWidget() - Возвращает виджет видимой страницы.
Если страница также содержит несколько виджетов, доступ к дочерним виджетам
можно получить через виджет, который возвращает функция currentWidget().
И последнее замечание: можно создать динамический стековый макет, в котором
страницы вставляются с помощью insertWidget(index, widget) или удаляются с
помощью removeWidget(widget).

88 ГЛАВА 4 ИЗУЧЕНИЕ УПРАВЛЕНИЯ МАКЕТОМ


Объяснение для QStackedLayout
Обязательно загрузите папку images из репозитория GitHub. Начиная со сценария
basic_window.py, как и раньше, импортируйте классы, как в Листинге 4-23, из PyQt и
обновите класс MainWindow для этого примера.
Листинг 4-23. Настройка главного окна для использования QStackedLayout
# stacked.py
# Импортируйте необходимые модули
import sys
from PyQt6.QtWidgets import (QApplication, QWidget, QLabel,
QLineEdit, QTextEdit, QComboBox, QSpinBox, QDoubleSpinBox,
QStackedLayout, QFormLayout, QVBoxLayout)
from PyQt6.QtCore import Qt
from PyQt6.QtGui import QPixmap

class MainWindow(QWidget):

def __init__(self):
super().__init__()
self.initializeUI()

def initializeUI(self):
"""Настройте графический интерфейс приложения."""
self.setFixedSize(300, 340)
self.setWindowTitle("Пример QStackedLayout")

self.setUpMainWindow()
self.show()

if __name__ == '__main__':
app = QApplication(sys.argv)
window = MainWindow()
sys.exit(app.exec())

Первый виджет в Листинге 4-24 - это объект QComboBox для переключения


страниц. Комбобокс отображает заголовки для каждой страницы и подключен
к сигналу активизации. Сигнал подается всякий раз, когда пользователь выбирает
элемент в QComboBox, и сигнал передает индекс выбранной страницы. Метод
switchPage() настроен в Листинге 4-27.

ГЛАВА 4 ИЗУЧЕНИЕ УПРАВЛЕНИЯ МАКЕТОМ 89


Листинг 4-24. Метод setUpMainWindow() для использования QStackedLayout, часть 1
# stacked.py
def setUpMainWindow(self):
"""Создайте и расположите виджеты в главном окне."""
# Создайте и подключите комбобокс для переключения страниц
page_combo = QComboBox()
page_combo.addItems(["Изображение", "Описание",
"Дополнительная информация"])
page_combo.activated.connect(self.switchPage)

# Создайте страницу изображения (Страница 1)


profile_image = QLabel()
pixmap = QPixmap("images/norwegian.jpg")
profile_image.setPixmap(pixmap)
profile_image.setScaledContents(True)

Первая страница содержит единственную QLabel, которая отображает QPixmap.


Метод QLabel setScaledContents() указывает метке использовать все доступное
пространство для отображения ее содержимого.
На второй странице мы будем использовать некоторые виджеты для отображения
текстовой информации об изображении на первой странице. Виджеты организованы
в QFormLayout в Листинге 4-25.
Листинг 4-25. Метод setUpMainWindow() для использования QStackedLayout, часть 2
# stacked.py
# Создайте страницу профиля (Страница 2)
pg2_form = QFormLayout()
pg2_form.setFieldGrowthPolicy(
pg2_form.FieldGrowthPolicy.AllNonFixedFieldsGrow)
pg2_form.setFormAlignment(
Qt.AlignmentFlag.AlignHCenter |
Qt.AlignmentFlag.AlignTop)
pg2_form.setLabelAlignment(
Qt.AlignmentFlag.AlignLeft)
pg2_form.addRow("Порода:",
QLabel("Норвежская лесная кошка"))
pg2_form.addRow("Происхождение:", QLabel("Норвегия"))
pg2_form.addRow(QLabel("Описание:"))
default_text = """Имеют длинное, крепкое тело, длинные
ноги и кустистый хвост. Они дружелюбны, умны, и
вообще хорошо ладят с людьми."""
pg2_form.addRow(QTextEdit(default_text))
pg2_container = QWidget()
pg2_container.setLayout(pg2_form)

90 ГЛАВА 4 ИЗУЧЕНИЕ УПРАВЛЕНИЯ МАКЕТОМ


Обратитесь к разделу "Создание форм с помощью QFormLayout" за информацией о
том, как расположить виджеты в QFormLayout. Однако макет не может быть добавлен
в QStackedLayout. Вместо этого создается виджет-контейнер, чтобы сгруппировать
все эти виджеты вместе. QWidget может служить в качестве основного контейнера
для других виджетов. Затем макет применяется к контейнеру с помощью функции
setLayout().
Третья страница создается аналогично Листингу 4-26, но на этот раз позволяет
пользователю вводить информацию с помощью некоторых виджетов полей.
Листинг 4-26. Метод setUpMainWindow() для использования QStackedLayout, часть 3
# stacked.py
# Создайте страницу "О себе" (Страница 3)
pg3_form = QFormLayout()
pg3_form.setFieldGrowthPolicy(
pg3_form.FieldGrowthPolicy.AllNonFixedFieldsGrow)
pg3_form.setFormAlignment(
Qt.AlignmentFlag.AlignHCenter |
Qt.AlignmentFlag.AlignTop)
pg3_form.setLabelAlignment(
Qt.AlignmentFlag.AlignLeft)

pg3_form.addRow(QLabel("Введите информацию о вашей кошке."))


pg3_form.addRow("Имя:", QLineEdit())
pg3_form.addRow("Цвет:", QLineEdit())

age_sb = QSpinBox()
age_sb.setRange(0, 30)
pg3_form.addRow("Возраст:", age_sb)

weight_dsb = QDoubleSpinBox()
weight_dsb.setRange(0.0, 30.0)
pg3_form.addRow("Вес (кг):", weight_dsb)

pg3_container = QWidget()
pg3_container.setLayout(pg3_form)

Новый виджет, QDoubleSpinBox, позволяет пользователям выбирать из диапазона


значений с плавающей точкой. Три виджета, метка и два объекта QWidget,
выступающие в качестве контейнеров, добавлены в объект QStackedLayout в
Листинге 4-27.

ГЛАВА 4 ИЗУЧЕНИЕ УПРАВЛЕНИЯ МАКЕТОМ 91


Листинг 4-27. Упорядочение виджетов в setUpMainWindow() с помощью
QStackedLayout
# stacked.py
# Создайте стекированный макет и добавьте страницы
self.stacked_layout = QStackedLayout()
self.stacked_layout.addWidget(profile_image)
self.stacked_layout.addWidget(pg2_container)
self.stacked_layout.addWidget(pg3_container)
# Создание основного макета
main_v_box = QVBoxLayout()
main_v_box.addWidget(page_combo)
main_v_box.addLayout(self.stacked_layout)
# Установите макет для главного окна
self.setLayout(main_v_box)
def switchPage(self, index):
"""Слот для переключения между вкладками."""
self.stacked_layout.setCurrentIndex(index)

Наконец, слот switchPage() создан для использования index'а из QComboBox для


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

Дополнительные советы по управлению


пространством
В этом разделе вы рассмотрите очень короткий пример, демонстрирующий
несколько методов менеджера макета, которые могут быть полезны для обеспечения
наилучшего использования пространства окна виджетами. Когда вы добавляете
виджеты в макет, менеджер макетов выполняет ряд проверок. К ним относятся
следующие:
• Выделение пространства на основе sizeHint(), рекомендуемого размера
виджета, и sizePolicy(), определяющей поведение виджета при изменении
размера.
• Применение коэффициентов растяжения, если они указаны
• Дополнительные коэффициенты изменения размера, заданные
разработчиком, например, установка минимального или максимального
размера, высоты или ширины виджета.
Для макетов боксов можно настроить пространство между элементами с помощью
следующих методов:
92 ГЛАВА 4 ИЗУЧЕНИЕ УПРАВЛЕНИЯ МАКЕТОМ
• addSpacing(int) - Создает пустое пространство между виджетами, заданное
значением int (в пикселях).
• addStretch(int) - Добавляет растягиваемую область значения int между
виджетами, которая пропорциональна коэффициентам растяжения других
виджетов.
Для управления горизонтальным и вертикальным расстоянием между виджетами
в макетах сетки и форм используются различные методы, как показано в следующем
списке:
• setHorizontalSpacing(int) - Устанавливает горизонтальное расстояние между
виджетами (в пикселях)
• setVerticalSpacing(int) - Устанавливает вертикальное расстояние между
виджетами (в пикселях)
• setSpacing(int) - Устанавливает горизонтальное и вертикальное расстояние
между виджетами (в пикселях)
В следующем разделе мы рассмотрим пример управления пространством в
QVBoxLayout.

Пояснения по управлению пространством


Для этого приложения вы должны создать три виджета в классе MainWindow в
Листинге 4-28, которые организованы вертикально в окне с помощью QVBoxLayout.
Листинг 4-28. Краткая демонстрация того, как работает управление пространством
с помощью QVBoxLayout
# spacing.py
# Импортируйте необходимые модули
import sys
from PyQt6.QtWidgets import (QApplication, QWidget,
QLabel, QPushButton, QLineEdit, QVBoxLayout)

class MainWindow(QWidget):

def __init__(self):
super().__init__()
self.initializeUI()

def initializeUI(self):
"""Настройте графический интерфейс приложения."""
self.setMinimumSize(300, 200)
self.setWindowTitle("Пример расположения")

label = QLabel("Введите текст")


line_edit = QLineEdit()

ГЛАВА 4 ИЗУЧЕНИЕ УПРАВЛЕНИЯ МАКЕТОМ 93


button = QPushButton("Конец")

main_v_box = QVBoxLayout()
main_v_box.addWidget(label)
main_v_box.addSpacing(20)
main_v_box.addWidget(line_edit)
main_v_box.addStretch()
main_v_box.addWidget(button)

self.setLayout(main_v_box)
self.show()

if __name__ == '__main__':
app = QApplication(sys.argv)
window = MainWindow()
sys.exit(app.exec())

Без добавления каких-либо пробелов или растягивания вы получите левый


графический интерфейс, показанный на Рисунке 4-8. Чтобы разместить пустое,
растягиваемое пространство между виджетами QLineEdit и QPushButton, используется
метод addStretch(). Не нужно передавать аргумент в addStretch(), если вы хотите,
чтобы все промежутки были одинаковыми.
С помощью addSpacing() между экземплярами label и line_edit добавляется
фиксированное количество пространства. Посмотрите на разницу между интервалом
и коэффициентом растяжения на правом снимке экрана на Рисунке 4-8.

Рисунок 4-8. Графический интерфейс пользователя до добавления интервалов и


растягивания (слева) и после (справа)

94 ГЛАВА 4 ИЗУЧЕНИЕ УПРАВЛЕНИЯ МАКЕТОМ


Установка полей содержимого
В качестве последнего задания, оставленного для вас, добавьте следующую строку
кода в spacing.py, чтобы посмотреть, как это повлияет на макет:
main_v_box.setContentsMargins(40, 30, 40, 30)
Отступы добавляют пространство за пределами макета. Это можно сделать в
PyQt с помощью метода setContentMargins(). Каждое целое число в коде задает размер
границы в пикселях в виде (слева, сверху, справа, снизу).

Резюме
Уделив время изучению управления компоновкой, вы сэкономите время и усилия
при создании собственных приложений с графическим интерфейсом. В этой главе
мы рассмотрели множество способов организации виджетов в интерфейсе PyQt. В
зависимости от требований вашего приложения, существует множество вариантов,
включая абсолютное позиционирование и любой из встроенных менеджеров
компоновки Qt.
Боксовые макеты, QHBoxLayout и QVBoxLayout, используются для горизонтального
или вертикального расположения виджетов. QFormLayout отлично подходит для
построения форм. QGridLayout полезен для стратегической организации и определения
размеров виджетов в макете, напоминающем сетку. Наконец, QStackedLayout можно
использовать, когда вам нужно управлять пространством в окне, укладывая виджеты
друг на друга. Каждый класс имеет свой особый случай использования, но настоящая
сила заключается в том, насколько удобно их комбинировать для создания сложных
компоновок.
Преимущества использования менеджера компоновки включают контроль над
положением дочерних виджетов, возможность устанавливать размеры по умолчанию
для виджетов, простоту использования для изменения размеров виджетов и
упрощение обновления содержимого окна или родительского виджета, когда что-то
меняется, например, скрытие, отображение или удаление дочернего виджета. Кроме
того, вы можете создавать и компоновать свой интерфейс графически с помощью
Qt Designer. Мы рассмотрим, как это сделать в Главе 8.
В Главе 5 мы рассмотрим, как создавать главные окна, включающие панели меню,
док-виджеты и многое другое.

ГЛАВА 4 ИЗУЧЕНИЕ УПРАВЛЕНИЯ МАКЕТОМ 95


ГЛАВА 5

Меню, панели
инструментов и многое
другое
Настольные приложения предоставляют средства не только для организации
виджетов, но и для организации и представления интерактивных функций и
инструментов пользователям. Меню - это список команд, которые может выполнять
компьютерная программа, представленный в более удобном и организованном виде.
Многочисленные устройства и системы включают меню, помогающие пользователю
ориентироваться и выбирать различные опции и задачи, а хорошо организованное
меню сделает программу более удобной в использовании.
В этой главе вы
• Узнаете, как создавать главные окна, которые наследуются от QMainWindow
• Создавать меню, подменю и проверяемые пункты меню, используя классы
PyQt, такие как QMenuBar и QAction
• Узнайте, как устанавливать и изменять иконки виджетов и главного окна
• Узнайте о встроенных классах диалоговых окон PyQt, включая QFileDialog,
QInputDialog, QColorDialog и другие.
• Настраивать и использовать строку состояния приложения для
предоставления обратной связи
• Используйте класс QDockWidget для создания съемных виджетов, которые
могут отображать общие инструменты и операции.
• Создавайте практические приложения, обучающие дополнительным
навыкам, таким как манипулирование изображениями с помощью классов
QPixmap и QTransform и настройка приложений для печати изображений с
помощью класса QPrinter.
Эта глава создаст основу для полностью функционирующих программ, которые
можно использовать сразу или как отправную точку для ваших собственных
программ.
Давайте начнем с рассмотрения общих приемов, используемых при создании
меню.

96 ГЛАВА 5 МЕНЮ, ПАНЕЛИ ИНСТРУМЕНТОВ И МНОГОЕ ДРУГОЕ


Общая практика создания меню
Расположение меню GUI и пунктов меню, или опций в меню, обычно соответствует
списку стандартных практик, которые создавались на протяжении многих лет.
Различные платформы, такие как macOS и Windows, также имеют свои собственные
соглашения, и в этом разделе мы рассмотрим общие для всех них.
• Существует общая схема упорядочивания меню. Слева направо меню Файл
управляет общими взаимодействиями с файлами и приложениями, меню
Правка содержит операции редактирования и так далее. Меню "Справка"
обычно является последним и содержит информацию о приложении и
инструкции.
• Меню также содержат часто используемые пункты. Хотя они зависят от
типа разрабатываемого приложения, в меню есть и общие пункты. Наиболее
распространенные примеры: "Создать", "Открыть", "Сохранить" и "Печать"
в меню "Файл" и "Отменить", "Повторить", "Вырезать", "Копировать" и
"Вставить" в меню "Правка".
• Для выполнения задачи существуют общие сочетания клавиш. Общие
сочетания клавиш включают Ctrl+V для пункта Вставить (Command+V в
macOS) или Ctrl+X для пункта Вырезать (Command+X в macOS).
• Разделители используются для упорядочивания связанных элементов.
Они также используются для облегчения навигации по меню.
Давайте начнем с практического применения некоторых из этих идей в первом
примере этой главы.

Создание простой панели меню


Для программы на Рисунке 5-1 вы рассмотрите, как создать простую строку
меню, которая представляет собой набор выпадающих меню со списком команд для
взаимодействия с приложением. В этом графическом интерфейсе строка меню будет
содержать одно меню, File, и только одну команду, Quit.

ГЛАВА 5 МЕНЮ, ПАНЕЛИ ИНСТРУМЕНТОВ И МНОГОЕ ДРУГОЕ 97


Рисунок 5-1. Создается строка меню (слева), в которой отображается меню
Файл. Выпадающее меню (справа) содержит одну команду - "Выход".
Давайте создадим окно, которое станет шаблоном сценария для ряда проектов в
этой книге.

Пояснения к созданию панели меню


Чтобы начать создание окна на Рисунке 5-1, мы можем использовать сценарий
basic_window.py из Главы 1 в качестве основы этого графического интерфейса.
В Листинге 5-1 импортируйте класс QApplication, как обычно, но на этот раз
импортируйте класс QMainWindow, а не QWidget. Класс QMainWindow предоставляет
функциональные возможности для создания ключевых свойств главного окна, таких
как панели меню и панели инструментов. В PyQt6 класс QAction теперь находится в
модуле QtGui.
Листинг 5-1. Создание класса, наследующего QMainWindow
# main_window_template.py
# Импортируйте необходимые модули
import sys
from PyQt6.QtWidgets import (QApplication, QMainWindow)
from PyQt6.QtGui import QAction

class MainWindow(QMainWindow):

def __init__(self):
super().__init__()
self.initializeUI()

def initializeUI(self):
"""Настройте графический интерфейс приложения."""
self.setMinimumSize(450, 350)

98 ГЛАВА 5 МЕНЮ, ПАНЕЛИ ИНСТРУМЕНТОВ И МНОГОЕ ДРУГОЕ


self.setWindowTitle("Шаблон главного окна")
self.setUpMainWindow()
self.createActions()
self.createMenu()
self.show()

if __name__ == '__main__':
app = QApplication(sys.argv)
window = MainWindow()
sys.exit(app.exec())

Класс MainWindow в этой программе наследуется от QMainWindow, а не от QWidget.


Новые методы класса, createActions() и createMenu(), будут использованы для создания
строки меню в Листинге 5-2.
В следующем подразделе мы немного подробнее рассмотрим различия между
QMainWindow и QWidget.

QMainWindow против QWidget


Класс QMainWindow предназначен для создания и управления компоновкой
главного окна приложения. Он позволяет установить окно со строкой состояния,
панелью инструментов, док-виджетами или другими функциями меню в заранее
определенных местах.
Класс QWidget является базовым классом для всех объектов пользовательского
интерфейса в Qt, включая виджеты. Виджеты, которые вы использовали, такие
как QPushButton и QTextEdit, наследуют QWidget, предоставляя им доступ к
широкому набору методов для взаимодействия с интерфейсом или установки
параметров экземпляра виджета. Важно отметить, что классы QMainWindow и
QDialog также наследуют QWidget и являются классами специального назначения,
ориентированными на создание главных окон и диалогов, соответственно.
Окно в приложении - это просто виджет, который не помещен в родительский
виджет. Это означает, что класс, наследующий QWidget, может считаться окном, если
у него нет родителя. Окна обычно имеют свою собственную строку заголовка и рамку.
На Рисунке 5-2 показано, как различные виджеты, которые может использовать
QMainWindow, имеют специально отведенные для них области.

ГЛАВА 5 МЕНЮ, ПАНЕЛИ ИНСТРУМЕНТОВ И МНОГОЕ ДРУГОЕ 99


Рисунок 5-2. Структура макета для класса QMainWindow (Адаптировано из
https://doc.qt.io/).

Строка меню закреплена горизонтально в верхней части окна, а строка состояния


находится в нижней части. Панели инструментов могут быть расположены между
строкой меню и строкой состояния и могут располагаться вертикально или
горизонтально. Виджеты док-станции могут быть расположены аналогичным
образом. Центральный виджет в центре окна должен быть установлен, если вы
собираетесь использовать QMainWindow в качестве базового класса. Например, вы
можете использовать один виджет QTextEdit в качестве главного виджета окна или
создать объект QWidget с другими виджетами, расположенными внутри него.

Создание строки меню и добавление действий


В методе initializeUI() в Листинге 5-1 вы заметите три различных вызова метода
для настройки главного окна и меню. Это сделано для того, чтобы упорядочить и
облегчить управление кодом независимо от размера приложения. Первый метод,
созданный в Листинге 5-2, setUpMainWindow(), оставлен здесь как место для будущих
проектов.
Листинг 5-2. Базовая структура для создания строки меню в приложении
# main_window_template.py
def setUpMainWindow(self):
"""Создание и расположение виджетов в главном окне."""
pass

def createActions(self):
"""Создание действий в меню приложения."""
# Создание действий для меню "Файл
self.quit_act = QAction("&Выход")
self.quit_act.setShortcut("Ctrl+Q")
100 ГЛАВА 5 МЕНЮ, ПАНЕЛИ ИНСТРУМЕНТОВ И МНОГОЕ ДРУГОЕ
self.quit_act.triggered.connect(self.close)

def createMenu(self):
"""Создайте строку меню приложения."""
self.menuBar().setNativeMenuBar(False)

# Создание меню файлов и добавление действий


file_menu = self.menuBar().addMenu("&Файл")
file_menu.addAction(self.quit_act)

Следующий метод, createActions(), устанавливает действия для меню программы.


Действия используются для создания опций в меню или панели инструментов, таких
как Открыть, Закрыть и Сохранить. В PyQt эти действия создаются на основе класса
QAction. Посмотрите, как создается действие Quit, quit_act, и затем добавляется в
file_menu.
Действие Quit является экземпляром класса QAction. Действия также должны
иметь родителя, который обычно передается в качестве последнего параметра
экземпляра QAction. Поскольку в данном случае родителем является главное окно,
мы можем просто использовать ключевое слово self для привязки действия к
классу. В следующей строке ярлык для действия quit_act задается явно с помощью
метода setShortcut() с комбинацией клавиш Ctrl+Q. Другой способ задать ярлык -
использовать знак амперсанда, &, перед буквой, которую вы хотите использовать в
качестве ярлыка. В зависимости от вашей системы под буквой будет отображаться
знак подчеркивания, обозначающий клавишу быстрого доступа.

Примечание. В macOS ярлыки по умолчанию отключены. Лучший способ


настроить ярлыки - использовать setShortcut().

Действия в меню также используют сигналы и слоты. Когда действие в меню


выбрано, оно издает сигнал, который необходимо подключить к слоту, чтобы
выполнить операцию. Для этого используется сигнал triggered.
Иногда одна и та же опция отображается и в строке меню, и на панели
инструментов. Одним из распространенных примеров является операция Print в
приложении текстового процессора, где действие может быть выбрано либо из меню
File, либо из панели инструментов, либо с помощью ярлыка. Независимо от того, что
является причиной подачи сигнала, класс QAction обеспечит правильное выполнение
действия.
Наконец, метод createMenu() устанавливает строку меню. В данном примере
имеется только одно меню - Файл. Для того чтобы создать горизонтальную строку
меню, необходимо создать экземпляр класса QMenuBar. Поскольку класс MainWindow
наследует QMainWindow, это можно легко сделать, вызвав метод QMainWindow
menuBar().

ГЛАВА 5 МЕНЮ, ПАНЕЛИ ИНСТРУМЕНТОВ И МНОГОЕ ДРУГОЕ 101


Примечание. Из-за рекомендаций, установленных macOS, строка меню не
будет отображаться в графическом интерфейсе. Вы можете изменить это с
помощью self.menuBar().setNativeMenuBar(False). Для пользователей Windows
или Linux эту строку можно закомментировать или удалить.

Добавление меню в строку меню также очень просто в PyQt:


file_menu = self.menuBar().addMenu("Файл")
Здесь метод addMenu() используется для добавления меню с именем Файл в строку
меню. Использование addMenu() добавляет объект QMenu. И снова, так же просто
использовать функции, предоставляемые классом QMainWindow.
Поскольку меню, окна и некоторые виджеты могут отображать пиктограммы,
давайте немного больше узнаем о классе QIcon.

Использование значков и класса QIcon


Иконки могут использоваться как небольшие графические изображения в
графическом интерфейсе пользователя, или как символы, представляющие действия,
которые пользователь может выполнить в меню или на кнопке. Они помогают
пользователю быстро находить общие действия и ориентироваться в приложении.
Хорошим примером этой концепции являются панели инструментов в приложениях
для обработки текстов, содержащие большое количество инструментов, каждый из
которых имеет пиктограмму или текстовое описание.
В Главе 2 был кратко представлен класс Qt для работы с изображениями, QPixmap.
Класс QIcon предоставляет методы, которые могут использовать пиксмапы и изменять
их стиль или размер так, чтобы их можно было использовать в приложении. Одним
из действительно важных применений QIcon является установка вида пиктограммы,
представляющей действие, на активный или отключенный.

Пояснения к использованию значков


Пример, показанный на Рисунке 5-3, демонстрирует, как
• Динамически устанавливать и изменять значки на виджете QPushButton
• Сбросить значок приложения в строке заголовка окна
• Организовать и применить центральный виджет в экземпляре QMainWindow.

Примечание. Для этого и других примеров в этой главе вам потребуется


загрузить папку images и ее содержимое из репозитория GitHub.

102 ГЛАВА 5 МЕНЮ, ПАНЕЛИ ИНСТРУМЕНТОВ И МНОГОЕ ДРУГОЕ


Рисунок 5-3. Значок приложения не отображается в области заголовка в системах
macOS

Примечание. Для пользователей macOS иконка в окне приложения не


может быть применена из-за системных рекомендаций. Тем не менее, вам
стоит взглянуть на эту программу, поскольку она также показывает, как
устанавливать иконки для других виджетов в PyQt.

Этот графический интерфейс представляет собой простое упражнение,


показывающее, как переключать значок на кнопке QPushButton, что может пригодиться,
когда состояние или назначение кнопки изменилось. Каждый раз, когда пользователь
нажимает на кнопку, отображаемый фрукт выбирается случайным образом, а значок
меняется. В Листинге 5-3 устанавливается главное окно и применяется новый значок
к строке заголовка главного окна.
Листинг 5-3. Код для демонстрации установки значков для главного окна
# change_icons.py
# Импортируйте необходимые модули
import sys, random
from PyQt6.QtWidgets import (QApplication, QMainWindow,
QWidget, QLabel, QPushButton, QVBoxLayout)
from PyQt6.QtCore import Qt, QSize
from PyQt6.QtGui import QIcon

class MainWindow(QMainWindow):

def __init__(self):
super().__init__()
self.initializeUI()

ГЛАВА 5 МЕНЮ, ПАНЕЛИ ИНСТРУМЕНТОВ И МНОГОЕ ДРУГОЕ 103


def initializeUI(self):
"""Настройте графический интерфейс приложения."""
self.setMinimumSize(200, 200)
self.setWindowTitle("Пример смены иконок")
self.setWindowIcon(QIcon("images/pyqt_logo.png"))

self.setUpMainWindow()
self.show()

if __name__ == '__main__':
app = QApplication(sys.argv)
window = MainWindow()
sys.exit(app.exec())

Значок приложения, обычно отображаемый в левом верхнем углу окна на некоторых


системах, заменен на логотип PyQt на правом изображении на Рисунке 5-4. Значок
окна можно установить с помощью метода setWindowIcon(), передав ему объект QIcon.
QIcon принимает в качестве аргумента путь к расположению изображения. Обратите
внимание на Рисунке 5-3 на отсутствие значка приложения в macOS.

Рисунок 5-4. Оригинальный значок приложения в левом верхнем углу окна (слева)
может быть установлен на новый значок (справа) с помощью метода setWindowIcon().
Главное окно, созданное в Листинге 5-4, состоит из QLabel, которая предоставляет
инструкции пользователю, и QPushButton для выбора одного из изображений
фруктов, содержащихся в списке изображений.

104 ГЛАВА 5 МЕНЮ, ПАНЕЛИ ИНСТРУМЕНТОВ И МНОГОЕ ДРУГОЕ


Листинг 5-4. Метод setUpMainWindow() для использования пиктограмм
# change_icons.py
def setUpMainWindow(self):
"""Создайте и расположите виджеты в главном окне."""
info_label = QLabel(
"Нажмите на кнопку и выберите фрукт")
info_label.setAlignment(Qt.AlignmentFlag.AlignCenter)

self.images = [
"images/1_apple.png", "images/2_pineapple.png",
"images/3_watermelon.png", "images/4_banana.png"]

self.icon_button = QPushButton()
self.icon_button.setIcon(
QIcon(random.choice(self.images)))
self.icon_button.setIconSize(QSize(60, 60))
self.icon_button.clicked.connect(
self.changeButtonIcon)

Для виджетов, которые могут отображать значки, вызов метода setIcon() на


этом виджете позволит вам отобразить на нем значок. Здесь значок для icon_button
выбирается случайным образом и передается в качестве аргумента для обработки
QIcon. Вызов метода setIconSize() на виджете может быть использован для изменения
размера пиктограммы. Класс QSize используется для определения двумерного
размера виджета. PyQt будет обрабатывать размер и стиль виджета на основе
заданных вами параметров.
Наконец, кнопка подключается к слоту в Листинге 5-5. В классе MainWindow
создайте дополнительный метод changeButtonIcon() для установки нового значка.
Листинг 5-5. Создание слота changeButtonIcon()
# change_icons.py
def changeButtonIcon(self):
"""Когда кнопка нажата, измените иконку на
другой случайный значок из списка изображений."""
self.icon_button.setIcon(
QIcon(random.choice(self.images)))
self.icon_button.setIconSize(QSize(60, 60))

При каждом нажатии на кнопку icon_button используется функция setIcon() для


применения нового значка, а функция setIconSize() используется для обеспечения
одинакового размера всех значков, что позволяет избежать проблем с изменением
размера.

ГЛАВА 5 МЕНЮ, ПАНЕЛИ ИНСТРУМЕНТОВ И МНОГОЕ ДРУГОЕ 105


Установка центрального виджета
Виджеты не были упорядочены, и центральный виджет в главном окне еще
не установлен. Еще в методе setUpMainWindow() виджеты этикеток и кнопок
организованы в Листинге 5-6 с помощью QVBoxLayout.
Листинг 5-6. Установка центрального виджета в главном окне(setUpMainWindow)
# change_icons.py
# Создайте вертикальный макет и добавьте виджеты
main_v_box = QVBoxLayout()
main_v_box.addWidget(info_label)
main_v_box.addWidget(self.icon_button)
# Установите основной макет окна
container = QWidget()
container.setLayout(main_v_box)
self.setCentralWidget(container)

QWidget можно использовать в качестве контейнера, предоставляющего средства


для группировки и управления виджетами. В Qt есть несколько специфических
виджетов, которые специально служат контейнерами, но пока мы будем использовать
QWidget. Виджет для контейнера QWidget устанавливается с помощью setLayout(),
а метод QMainWindow setCentralWidget() используется для установки основного
виджета в окне.
Перед тем как создать более крупный проект с меню и QMainWindow, давайте
сначала рассмотрим некоторые встроенные классы диалоговых окон PyQt.

Встроенные классы диалогов в PyQt


Диалоги отлично подходят не только для предоставления обратной связи, но и для
взаимодействия с локальными файлами или для сбора информации от пользователя.
PyQt способен создавать окна, диалоговые окна и виджеты, выглядящие как родные,
независимо от системы. Давайте немного познакомимся с некоторыми встроенными
диалоговыми окнами и посмотрим, как их использовать.

Класс QFileDialog
Класс QFileDialog можно использовать для открытия и выбора файлов или
каталогов на вашем компьютере. Этот класс диалоговых окон полезен для открытия,
сохранения и именования файлов.
Чтобы открыть локальный файл, используется метод QFileDialog getOpenFileName().
Пример этого показан в следующем фрагменте кода:

106 ГЛАВА 5 МЕНЮ, ПАНЕЛИ ИНСТРУМЕНТОВ И МНОГОЕ ДРУГОЕ


file_name, ok = QFileDialog.getOpenFileName(self,
"Open File", "/Users/user_name/Desktop/",
"Image Files (*.png *.jpg *.bmp)")

Первый аргумент - это родитель диалогового окна. Если родителем является


главное окно, то диалог появится над ним на экране. Далее вы можете создать
заголовок для диалогового окна. За ним следует расположение каталога, который
будет отображаться при открытии диалога. Здесь будет отображаться Рабочий
стол пользователя. Последний передаваемый аргумент - это фильтр, используемый
для представления файлов с совпадающими шаблонами, заданными в строке. Для
предыдущего кода можно выбрать файлы изображений с расширениями .png, .jpg и
.bmp. Вы можете указать и другие типы файлов.
Сохранение файла осуществляется аналогичным образом с помощью функции
getSaveFileName():

file_name, ok = QFileDialog.getSaveFileName(self,
"Save File", "/Users/user_name/Desktop/",
"Text Files (*.txt)")

Здесь пользователю разрешено сохранять только текстовые файлы с расширением


.txt. Внешний вид появляющегося диалогового окна также будет отражать тип
используемой системы. Чтобы изменить это или другие свойства, можно включить
параметр options:

file_name, ok = QFileDialog.getOpenFileName(self,
"Open File", "/Users/user_name/Desktop/",
"Image Files (*.png *.jpg *.bmp)",
options = QFileDialog.Option.DontUseNativeDialog)

По умолчанию используется родной стиль системы.

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

find_text, ok = QInputDialog.getText(self, "Search Text", "Find:")

В этом примере объект диалога ввода создается вызовом QInputDialog.getText().


Использование функции getText() позволяет пользователю ввести одну строку
в виджет QLineEdit, как на Рисунке 5-5. Второй аргумент, "Search Text", является

ГЛАВА 5 МЕНЮ, ПАНЕЛИ ИНСТРУМЕНТОВ И МНОГОЕ ДРУГОЕ 107


заголовком для диалога. Третий аргумент - это метка, которая появляется рядом с
виджетом QLineEdit. Диалог ввода возвращает два значения: ввод от пользователя,
find_text, и булево значение, ok. Булево значение определяется тем, какой бюджет
нажат в диалоге: True для кнопки OK и False для Cancel.

Рисунок 5-5. Пример диалога QInputDialog из графического интерфейса блокнота


Rich Text Notepad GUI
Другие типы ввода могут быть собраны с помощью одного из следующих методов
QInputDialog:
• getMultiLineText() - Метод получения многострочной строки от пользователя
• getInt() - Метод получения целого числа от пользователя
• getDouble() - Метод получения от пользователя числа с плавающей точкой
• getItem() - Метод, позволяющий пользователю выбрать элемент из списка
строк.

Класс QFontDialog
QFontDialog предоставляет диалоговое окно, которое позволяет пользователю
выбирать и манипулировать различными типами шрифтов. Чтобы создать
диалоговое окно шрифта и выбрать шрифт, используйте метод getFont():
font, ok = QFontDialog.getFont()
Ключевое слово font - это конкретный шрифт, возвращаемый из getFont(), а ok -
это булева переменная для проверки того, выбрал ли пользователь шрифт и нажал
ли кнопку OK. Диалоговое окно шрифта на macOS показано на рисунке 5-6.

108 ГЛАВА 5 МЕНЮ, ПАНЕЛИ ИНСТРУМЕНТОВ И МНОГОЕ ДРУГОЕ


Рисунок 5-6. Пример диалогового окна QFontDialog
Когда пользователь нажимает кнопку OK, шрифт выбирается. Однако, если
нажать кнопку Отмена, то будет возвращен исходный шрифт. Если у вас есть шрифт
по умолчанию, который вы хотели бы использовать в случае, если пользователь не
выбрал OK, вы можете сделать следующее:

font, ok = QFontDialog.getFont(QFont("Helvetica", 10), self)


self.text_edit_widget.setCurrentFont(font)

Чтобы изменить шрифт, если был выбран новый, используйте метод setCurrentFont()
и измените его на новый шрифт.

Класс QColorDialog
Класс QColorDialog создает диалоговое окно для выбора цветов, как показано на
Рисунке 5-7. Выбор цветов может быть полезен для изменения цвета текста, цвета
фона окна и многих других задач.

ГЛАВА 5 МЕНЮ, ПАНЕЛИ ИНСТРУМЕНТОВ И МНОГОЕ ДРУГОЕ 109


Рисунок 5-7. Диалоговое окно QColorDialog
Чтобы создать диалоговое окно цвета и выбрать цвет, используйте следующую
строку кода:
color = QColorDialog.getColor()
Затем проверьте, выбрал ли пользователь правильный цвет и нажал ли он на
кнопку OK, используя метод isValid().
Если да, то вы можете использовать метод QTextEdit setTextColor() для изменения
цвета текста или setBackgroundColor() для изменения цвета фона:

if color.isValid():
self.text_field.setTextColor(color)

Последний диалог, который мы рассмотрим, является статическим методом из


QMessageBox.

О QMessageBox
Во многих приложениях в меню часто можно найти пункт "О программе". Щелчок
на этом пункте открывает диалоговое окно, в котором отображается информация
о приложении, такая как логотип программы, название, номер последней версии и
другая юридическая информация.
Класс QMessageBox, который мы рассматривали в Главе 3, также предоставляет
метод about() для создания диалогового окна для отображения заголовка и текста.

110 ГЛАВА 5 МЕНЮ, ПАНЕЛИ ИНСТРУМЕНТОВ И МНОГОЕ ДРУГОЕ


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

QMessageBox.about(self, "О блокноте",


"""<p>Практическое руководство новичка по PyQt</p>
<p>Проект 5.1 - графический интерфейс блокнота</p>""").

Это создает диалоговое окно, показанное на рисунке 5-8. Если вы никогда раньше
не использовали HTML, то <p> и </p> вокруг текста - это HTML-теги, которые
создают абзацы. Мы рассмотрим использование HTML с PyQt в главе 6.

Рисунок 5-8. Пример диалогового окна "О программе" из графического интерфейса


блокнота Rich Text Notepad
Вы также можете отобразить в окне значок приложения. Если значок не
предоставлен, метод about() попытается найти его в родительском виджете. Чтобы
предоставить значок, вызовите метод setWindowIcon() объекта QApplication в методе
main() программы.
app.setWindowIcon(QIcon("images/app_logo.png"))
Далее вы будете использовать то, что узнали о меню и диалогах, для создания
более крупного приложения.

Проект 5.1 - Графический интерфейс блокнота с


насыщенным текстом
Для первого проекта давайте используем концепции, изученные до сих пор в этой
главе, чтобы создать приложение блокнота, которое поддерживает насыщенный
текст с помощью QTextEdit. На Рисунке 5-9 показан пример готового приложения с
текстом различных размеров, цветов, шрифтов и выделения.

ГЛАВА 5 МЕНЮ, ПАНЕЛИ ИНСТРУМЕНТОВ И МНОГОЕ ДРУГОЕ 111


Рисунок 5-9. Графический интерфейс Блокнота со строкой меню и виджетом
QTextEdit
На этот раз мы добавим соответствующую строку меню с меню и действиями.
У пользователя также будет возможность открывать и сохранять свой текст, как в
формате насыщенный текст или обычный текст, а также редактировать шрифт, цвет
или размер текста, чтобы придать своим заметкам большую функциональность и
креативность.

Проектирование графического интерфейса


блокнота Rich Text Notepad
Лучше всего начинать создание графического интерфейса пользователя с
определения его ключевых особенностей, например, какие виджеты используются в
основном интерфейсе и какие опции можно найти в меню.
Для приложения для ведения заметок схема относительно проста - строка меню
в верхней части окна с различными меню для различных функций и инструментов
112 ГЛАВА 5 МЕНЮ, ПАНЕЛИ ИНСТРУМЕНТОВ И МНОГОЕ ДРУГОЕ
и область для отображения и редактирования текста. Для текстового поля мы
будем использовать виджет QTextEdit, который также будет служить центральным
виджетом для объекта QMainWindow. Посмотрите на Рисунок 5-10, чтобы увидеть
различные пункты меню, включенные в этот проект.

Рисунок 5-10. Схема, показывающая макет графического интерфейса Блокнота и


различные меню и действия
Это приложение имеет четыре меню в строке меню: File, Edit, Tools и Help. Наличие
различных меню в строке меню помогает организовать действия по различным
категориям, а также облегчает пользователю поиск нужных действий.

Пояснения к графическому интерфейсу блокнота


Rich Text Notepad
QTextEdit уже предоставляет функциональность для написания текста как в
обычном, так и в насыщенном текстовом формате. В этой программе вы изучите,
как использовать некоторые методы из QTextEdit, такие как отмена и повтор, а также
различные классы диалогов для создания приложения блокнота. Используйте
сценарий main_window_template.py из начала этой главы, чтобы установить
Листинг 5-7 и каркас(framework) для класса MainWindow.

ГЛАВА 5 МЕНЮ, ПАНЕЛИ ИНСТРУМЕНТОВ И МНОГОЕ ДРУГОЕ 113


Листинг 5-7. Настройка главного окна графического интерфейса RichText Notepad
# richtext_notepad.py
# Импортируйте необходимые модули
import sys
from PyQt6.QtWidgets import (QApplication, QMainWindow,
QMessageBox, QTextEdit, QFileDialog, QInputDialog,
QFontDialog, QColorDialog)
from PyQt6.QtCore import Qt
from PyQt6.QtGui import QIcon, QTextCursor, QColor, QAction

class MainWindow(QMainWindow):

def __init__(self):
super().__init__()
self.initializeUI()

def initializeUI(self):
"""Настройте графический интерфейс приложения."""
self.setMinimumSize(400, 500)
self.setWindowTitle("5.1 - Rich Text Notepad GUI")

self.setUpMainWindow()
self.createActions()
self.createMenu()
self.show()

if __name__ == "__main__":
app = QApplication(sys.argv)
window = MainWindow()
sys.exit(app.exec())

Для приложения блокнота необходимо импортировать довольно много классов.


Из модуля QtWidgets нам нужно импортировать QMainWindow для создания строки
меню и пунктов меню. Нам также нужно включить различные классы диалоговых
окон PyQt, такие как QFileDialog и QInputDialog. Из QtGui, QIcon используется для
работы с иконками, QTextCursor используется для получения информации о курсоре
в текстовых документах, QColor предоставляет методы для создания цветов в PyQt, а
QAction будет создавать действия, используемые меню.
Класс MainWindow наследует QMainWindow. В initializeUI() вызываются методы для
настройки главного окна и его строки меню. Виджет редактирования текста главного
окна создан в Листинге 5-8.

114 ГЛАВА 5 МЕНЮ, ПАНЕЛИ ИНСТРУМЕНТОВ И МНОГОЕ ДРУГОЕ


Листинг 5-8. Метод setUpMainWindow() для графического интерфейса
Rich Text Notepad
# richtext_notepad.py
def setUpMainWindow(self):
"""Создайте и расположите виджеты в главном окне."""
self.text_edit = QTextEdit()
self.text_edit.textChanged.connect(
self.removeHighlights)
self.setCentralWidget(self.text_edit)

Окно состоит из одного виджета QTextEdit и устанавливается в качестве центрального


виджета для MainWindow. Сигнал textChanged испускается при редактировании
или вставке текста в виджет и подключается к слоту removeHighlights(), который
установлен в Листинге 5-14.
Далее метод createActions() в Листингах 5-9 и 5-10 создает действия для меню.

Листинг 5-9. Метод createActions() для графического интерфейса блокнота


Rich Text Notepad, часть 1
# richtext_notepad.py
def createActions(self):
"""Создайте действия меню приложения."""
# Создаем действия для меню Файл
self.new_act = QAction(
QIcon("images/new_file.png"), "Новый")
self.new_act.setShortcut("Ctrl+N")
self.new_act.triggered.connect(self.clearText)

self.open_act = QAction(
QIcon("images/open_file.png"), "Открыть")
self.open_act.setShortcut("Ctrl+O")
self.open_act.triggered.connect(self.openFile)

self.save_act = QAction(
QIcon("images/save_file.png"), "Сохранить")
self.save_act.setShortcut("Ctrl+S")
self.save_act.triggered.connect(self.saveToFile)

self.quit_act = QAction(
QIcon("images/exit.png"), "Выход")
self.quit_act.setShortcut("Ctrl+Q")
self.quit_act.triggered.connect(self.close)

ГЛАВА 5 МЕНЮ, ПАНЕЛИ ИНСТРУМЕНТОВ И МНОГОЕ ДРУГОЕ 115


В строке меню блокнота есть четыре меню. Действия первого меню, Файл,
включают Новый для очистки всего текста, Открыть для открытия существующего
файла с расширенным или обычным текстом, Сохранить для сохранения текущего
текста виджета QTextEdit и Выход. Давайте рассмотрим первое из них, поскольку
остальные настроены аналогично.
Переменная new_act присваивается объекту QAction. QIcon используется для
установки пиктограммы рядом с текстом действия в меню. Затем действию
присваивается текст для отображения, Новый. Многим действиям в программе
блокнота присваивается текстовый ярлык с помощью функции setShortcut().
Наконец, мы подключаем сигнал, который испускается при нажатии на new_act, к
слоту, в данном случае clearText().
Листинг 5-10 создает действия для меню Правка, Инструменты и Помощь.
Листинг 5-10. Метод createActions() для графического интерфейса блокнота
Rich Text Notepad, часть 2
# richtext_notepad.py
# Создание действий для меню Правка
self.undo_act = QAction(
QIcon("images/undo.png"), "Отменить")
self.undo_act.setShortcut("Ctrl+Z")
self.undo_act.triggered.connect(self.text_edit.undo)

self.redo_act = QAction(
QIcon("images/redo.png"), "Повторить")
self.redo_act.setShortcut("Ctrl+Shift+Z")
self.redo_act.triggered.connect(self.text_edit.redo)

self.cut_act = QAction(QIcon("images/cut.png"), "Вырезать")


self.cut_act.setShortcut("Ctrl+X")
self.cut_act.triggered.connect(self.text_edit.cut)

self.copy_act = QAction(
QIcon("images/copy.png"), "Копировать")
self.copy_act.setShortcut("Ctrl+C")
self.copy_act.triggered.connect(self.text_edit.copy)

self.paste_act = QAction(
QIcon("images/paste.png"), "Вставить")
self.paste_act.setShortcut("Ctrl+V")
self.paste_act.triggered.connect(self.text_edit.paste)

self.find_act = QAction(
QIcon("images/find.png"), "Найти все")
self.find_act.setShortcut("Ctrl+F")
self.find_act.triggered.connect(self.searchText)
116 ГЛАВА 5 МЕНЮ, ПАНЕЛИ ИНСТРУМЕНТОВ И МНОГОЕ ДРУГОЕ
# Создание действий для меню "Инструменты
self.font_act = QAction(
QIcon("images/font.png"), "Шрифт")
self.font_act.setShortcut("Ctrl+T")
self.font_act.triggered.connect(self.chooseFont)

self.color_act = QAction(
QIcon("images/color.png"), "Цвет")
self.color_act.setShortcut("Ctrl+Shift+C")
self.color_act.triggered.connect(self.chooseFontColor)

self.highlight_act = QAction(
QIcon("images/highlight.png"), "Выделить")
self.highlight_act.setShortcut("Ctrl+Shift+H")
self.highlight_act.triggered.connect(
self.chooseFontBackgroundColor)

# Создайте действия для меню Справка


self.about_act = QAction("О программе")
self.about_act.triggered.connect(self.aboutDialog)

В QTextEdit уже есть предопределенные слоты, такие как cut(), copy() и paste(),
которые создают стандартные функции редактирования текста. Для большинства
действий в меню Правка их сигналы подключаются к этим специальным слотам, а
не создают новые, за исключением find_act.
Действия, созданные в меню Инструменты, вызывают слоты, открывающие
диалоги. Эти действия используются для изменения внешнего вида текста. Действие
about_act используется для отображения диалога О программе.
Когда действия созданы, следующим шагом будет создание соответствующих
пунктов меню в Листинге 5-11.
Листинг 5-11. Метод createMenu() для графического интерфейса Rich Text Notepad
# richtext_notepad.py
def createMenu(self):
"""Создаем строку меню приложения."""
self.menuBar().setNativeMenuBar(False)

# Создаем меню файла и добавляем действия


file_menu = self.menuBar().addMenu("Файл")
file_menu.addAction(self.new_act)
file_menu.addSeparator()
file_menu.addAction(self.open_act)
file_menu.addAction(self.save_act)
file_menu.addSeparator()
file_menu.addAction(self.quit_act)
ГЛАВА 5 МЕНЮ, ПАНЕЛИ ИНСТРУМЕНТОВ И МНОГОЕ ДРУГОЕ 117
# Создайте меню редактирования и добавьте действия
edit_menu = self.menuBar().addMenu("Правка")
edit_menu.addAction(self.undo_act)
edit_menu.addAction(self.redo_act)
edit_menu.addSeparator()
edit_menu.addAction(self.cut_act)
edit_menu.addAction(self.copy_act)
edit_menu.addAction(self.paste_act)
edit_menu.addSeparator()
edit_menu.addAction(self.find_act)

# Создайте меню Инструменты и добавьте действия


tool_menu = self.menuBar().addMenu("Инструменты")
tool_menu.addAction(self.font_act)
tool_menu.addAction(self.color_act)
tool_menu.addAction(self.highlight_act)

# Создайте меню справки и добавьте действия


help_menu = self.menuBar().addMenu("Помощь")
help_menu.addAction(self.about_act)

Различные действия добавляются в соответствующие меню в строке меню класса


MainWindow с помощью метода addMenu(). Чтобы добавить разделитель между
категориями в меню, используйте метод addSeparator(). Строка меню показана на
Рисунке 5-11.

Рисунок 5-11. Графический интерфейс Блокнота и его меню - Файл (слева), Правка
(посередине) и Инструменты (справа)
Существует ряд слотов, которые вызываются при нажатии на пункт меню.
Некоторые из них открывают диалоговое окно и возвращают какой-либо ввод от
пользователя, например, новый файл, цвет текста или фона, или ключевое слово для
поиска текста.
В Листинге 5-12 начинается конструирование слотов для меню Файл.

118 ГЛАВА 5 МЕНЮ, ПАНЕЛИ ИНСТРУМЕНТОВ И МНОГОЕ ДРУГОЕ


Листинг 5-12. Слоты clearText() и openFile() для меню Файл
# richtext_notepad.py
def clearText(self):
"""Очистите поле QTextEdit."""
answer = QMessageBox.question(self, "Очистить текст",
"Вы хотите очистить текст?",
QMessageBox.StandardButton.No | \
QMessageBox.StandardButton.Yes,
QMessageBox.StandardButton.Yes)
if answer == QMessageBox.StandardButton.Yes:
self.text_edit.clear()

def openFile(self):
"""Открыть текстовый или html файл и отобразить его содержимое
в поле редактирования текста."""
file_name, _ = QFileDialog.getOpenFileName(
self, "Открыть файл", "",
"Файлы HTML (*.html);;Текстовые файлы (*.txt)")

if file_name:
with open(file_name, "r") as f:
notepad_text = f.read()
self.text_edit.setText(notepad_text)

Вместо того чтобы закрывать окно и открывать новое, когда пользователь


выбирает Новый из меню Файл, текст просто очищается в clearText(). Появляется
окно QMessageBox, и метод QTextEdit clear() удаляет весь текущий текст, если
пользователь нажимает Да.
При нажатии кнопки Открыть пользователю показывается диалоговое окно
QFileDialog. Пользователь может выбрать HTML или текстовый файл на своем
компьютере. Когда файл выбран, используется QTextEdit.setText() для применения
текста в виджете QTextEdit.
Слот для сохранения текста, saveToFile(), создан в Листинге 5-13.

ГЛАВА 5 МЕНЮ, ПАНЕЛИ ИНСТРУМЕНТОВ И МНОГОЕ ДРУГОЕ 119


Листинг 5-13. Слот saveToFile() для меню Файл
# richtext_notepad.py
def saveToFile(self):
"""Если нажата кнопка сохранения, отобразится диалоговое окно
с вопросом, нужно ли сохранить текст в поле редактирования
текста в текстовый или форматированный текстовый файл."""
file_name, _ = QFileDialog.getSaveFileName(
self, "Сохранить файл", "",
"Файлы HTML (*.html);;Текстовые файлы (*.txt)")

if file_name.endswith(".txt"):
notepad_text = self.text_edit.toPlainText()
with open(file_name, "w") as f:
f.write(notepad_text)
elif file_name.endswith(".html"):
notepad_richtext = self.text_edit.toHtml()
with open(file_name, "w") as f:
f.write(notepad_richtext)
else:
QMessageBox.information(
self, "Не сохранено", "Текст не сохранен.",
QMessageBox.StandardButton.Ok)

Пользователь может выбрать одно из двух расширений файла: .html или .txt.
Если выбран .txt, то для преобразования текста в обычный текст используется
функция QTextEdit.toPlainText(). Затем текст записывается в файл. В противном случае
используется метод QTextEdit toHtml() для сохранения форматированного текста в
файл.
Единственный пункт меню Правка, который не вызывает встроенный слот
QTextEdit, - это опция Найти всё, созданная в Листинге 5-14.
Листинг 5-14. Слот searchText() для меню Edit
# richtext_notepad.py
def searchText(self):
"""Поиск текста."""
# Отображает диалог ввода, чтобы спросить пользователя о тексте для поиска
find_text, ok = QInputDialog.getText(
self, "Текст для поиска", "Найти:")
if ok:
extra_selections = []
# Установите курсор в начало
self.text_edit.moveCursor(
QTextCursor.MoveOperation.Start)
color = QColor(Qt.GlobalColor.gray)

120 ГЛАВА 5 МЕНЮ, ПАНЕЛИ ИНСТРУМЕНТОВ И МНОГОЕ ДРУГОЕ


while(self.text_edit.find(find_text)):
# Используйте ExtraSelection(), чтобы отметить текст,
# который вы ищете, как серый
selection = QTextEdit.ExtraSelection()
selection.format.setBackground(color)

# Установите курсор выделения


selection.cursor = self.text_edit.textCursor()
extra_selections.append(selection)

# Выделите все выделения в виджете QTextEdit


self.text_edit.setExtraSelections(
extra_selections)

def removeHighlights(self):
"""Сбросить дополнительные выделения после редактирования текста."""
self.text_edit.setExtraSelections([])

Диалоговое окно QInputDialog на Рисунке 5-5 позволяет пользователю ввести


строку. Если пользователь нажимает OK в диалоговом окне, курсор для text_edit
перемещается обратно к началу текста с помощью moveCursor() и флага QTextCursor
Start. В QTextEdit уже есть метод для поиска совпадений, find(). Однако он находит
только первое совпадение и останавливается. Слот searchText() будет итерационно
просматривать текст, отыскивая все совпадения, и выделять их серым цветом.
Когда совпадение найдено, структура QTextEdit ExtraSelection() используется
для задания формата символов и курсора для выделения текста в QTextEdit. Метод
setExtraSelections() позволяет выделить более одного совпадения.
Слот removeHighlights() используется для сброса дополнительных выделений в
пустой список, следовательно, для удаления серого выделения.
Листинг 5-15 устанавливает слоты для меню Инструменты. Каждый слот
открывает диалоговое окно для изменения внешнего вида текста.
Листинг 5-15. Различные слоты для меню Инструменты
# richtext_notepad.py
def chooseFont(self):
"""Выбираем шрифт из QFontDialog."""
current = self.text_edit.currentFont()

opt = QFontDialog.FontDialogOption.DontUseNativeDialog
font, ok = QFontDialog.getFont(current, self,
options=opt)
if ok:
self.text_edit.setCurrentFont(font)

ГЛАВА 5 МЕНЮ, ПАНЕЛИ ИНСТРУМЕНТОВ И МНОГОЕ ДРУГОЕ 121


def chooseFontColor(self):
"""Выберите шрифт из QColorDialog."""
color = QColorDialog.getColor()
if color.isValid():
self.text_edit.setTextColor(color)

def chooseFontBackgroundColor(self):
"""Выберите цвет для фона текста."""
color = QColorDialog.getColor()
if color.isValid():
self.text_edit.setTextBackgroundColor(color)

Обратитесь к разделу "Встроенные классы диалогов в PyQt" для объяснения


различных методов.
Последний шаг - создание диалога О программе в Листинге 5-16, который
появляется, когда пользователь выбирает опцию О программе в меню Помощь.
Листинг 5-16. Опция О программе в меню Помощь
# richtext_notepad.py
def aboutDialog(self):
"""Отображает диалог About."""
QMessageBox.about(self, "О блокноте",
"""<p>Практическое руководство новичка по PyQt</p>
<p>Проект 5.1 - графический интерфейс блокнота</p>""")

Обратитесь к подразделу "О QMessageBox" и Рисунку 5-8.


QMainWindow предоставляет методы удобства и другие средства для создания
профессиональных и хорошо продуманных графических интерфейсов.

Расширение возможностей главного окна


В этом разделе вы узнаете о некоторых дополнительных виджетах и функциях,
которые вы можете использовать при построении меню и главных окон в
приложениях с графическим интерфейсом пользователя, включая
• Класс QDockWidget
• Класс QStatusBar
• Функции меню, такие как подменю и проверяемые пункты меню
Эти темы будут продемонстрированы при построении графического интерфейса
пользователя на Рисунке 5-12.

122 ГЛАВА 5 МЕНЮ, ПАНЕЛИ ИНСТРУМЕНТОВ И МНОГОЕ ДРУГОЕ


Рисунок 5-12. Графический интерфейс пользователя с панелью инструментов,
строкой состояния и виджетом док-станции. В строке состояния внизу
отображается текст "Выход из программы" при наведении курсора мыши на значок
выхода на панели инструментов.

Пояснение к расширению возможностей


Скопируйте содержимое файла main_window_template.py в новый скрипт Python, и
давайте приступим к созданию главного окна в Листинге 5-17.
Листинг 5-17. Настройка главного окна с дополнительными функциональными
возможностями
# main_window_extras.py
# Импортируйте необходимые модули
import sys
from PyQt6.QtWidgets import (QApplication, QMainWindow,
QWidget, QCheckBox, QTextEdit, QDockWidget, QToolBar,
QStatusBar, QVBoxLayout)
from PyQt6.QtCore import Qt, QSize
from PyQt6.QtGui import QIcon, QAction

class MainWindow(QMainWindow):

def __init__(self):
super().__init__()
self.initializeUI()
ГЛАВА 5 МЕНЮ, ПАНЕЛИ ИНСТРУМЕНТОВ И МНОГОЕ ДРУГОЕ 123
def initializeUI(self):
"""Настройте графический интерфейс приложения."""
self.setMinimumSize(450, 350)
self.setWindowTitle("Добавление дополнительных возможностей окна")

self.setUpMainWindow()
self.createDockWidget()
self.createActions()
self.createMenu()
self.createToolBar()
self.show()

if __name__ == '__main__':
app = QApplication(sys.argv)
window = MainWindow()
sys.exit(app.exec())

Импортировано несколько новых классов из модуля QtWidgets, включая QStatusBar,


QToolBar и QDockWidget. Обязательно добавьте дополнительные вызовы методов
createDockWidget() и createToolBar().
Давайте установим главное окно и строку состояния в Листинге 5-18.
Листинг 5-18. Метод setUpMainWindow() для главного окна с дополнительными
функциональными возможностями
# main_window_extras.py
def setUpMainWindow(self):
"""Создание и расположение виджетов в главном окне."""
# Создание и установка центрального виджета
self.text_edit = QTextEdit()
self.setCentralWidget(self.text_edit)

# Создайте строку состояния


self.setStatusBar(QStatusBar())

Подобно Rich Text Notepad, центральным виджетом этого приложения также


является QTextEdit. Метод QMainWindow setStatusBar() позволяет быстро создать
строку состояния графического интерфейса. Давайте узнаем немного больше о
QStatusBar.

124 ГЛАВА 5 МЕНЮ, ПАНЕЛИ ИНСТРУМЕНТОВ И МНОГОЕ ДРУГОЕ


Класс QStatusBar
В нижней части графического интерфейса пользователя на Рисунке 5-12
находится горизонтальная полоса с текстом "Выход из программы". Эта полоса
известна как строка состояния и создается из класса QStatusBar. Этот виджет очень
полезен для отображения обратной связи, дополнительной информации о виджете
или результате процесса. При первом вызове метода setStatusBar() создается строка
состояния. Последующие вызовы будут возвращать объект строки состояния.
Чтобы отобразить сообщение в строке состояния при наведении курсора мыши
на иконку или виджет, необходимо вызвать метод setStatusTip() на объекте действия.
Например:
exit_act.setStatusTip("Выйти из программы")
Это отобразит текст "Выход из программы" при наведении мыши на иконку
exit_act или команду меню. Чтобы отобразить текст в строке состояния при запуске
программы или при вызове функции, используйте метод showMessage():
self.statusBar().showMessage("С возвращением!").
Подсказки состояния добавлены к пунктам меню в Листинге 5-19. Давайте
сконструируем меню и действия в следующем разделе.

Создание подменю с проверяемыми пунктами


меню
По мере усложнения приложения его меню могут превратиться в беспорядочный
беспорядок. С помощью подменю можно организовать похожие категории вместе и
упростить систему меню. На Рисунке 5-13 показан пример подменю.

ГЛАВА 5 МЕНЮ, ПАНЕЛИ ИНСТРУМЕНТОВ И МНОГОЕ ДРУГОЕ 125


Рисунок 5-13. Подменю также содержит проверяемое действие для перехода в
полноэкранный режим
Сначала нам нужно будет создать действия для меню в Листинге 5-19.
Листинг 5-19. Метод createActions() для главного окна с дополнительными
функциональными возможностями
# main_window_extras.py
def createActions(self):
"""Создаем действия меню приложения."""
# Создаем действия для меню Файл
self.quit_act = QAction(
QIcon("images/exit.png"), "Выход")
self.quit_act.setShortcut("Ctrl+Q")
self.quit_act.setStatusTip("Выход из программы")
self.quit_act.triggered.connect(self.close)

# Создание действий для меню "Вид


self.full_screen_act = QAction(
"Полный экран", checkable=True)
self.full_screen_act.setStatusTip(
"Переключиться в полноэкранный режим")
self.full_screen_act.triggered.connect(
self.switchToFullScreen)

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

126 ГЛАВА 5 МЕНЮ, ПАНЕЛИ ИНСТРУМЕНТОВ И МНОГОЕ ДРУГОЕ


пункт меню, включите аргумент checkable=True в параметрах QAction. Пример этого
можно увидеть в действии full_screen_act. Также возможно, чтобы проверяемые
пункты меню проверялись с самого начала, вызвав метод trigger() для действия.
Далее создайте строку меню в Листинге 5-20.
Листинг 5-20. Метод createMenu() для главного окна с дополнительными
функциональными возможностями
# main_window_extras.py
def createMenu(self):
"""Создаем строку меню приложения."""
self.menuBar().setNativeMenuBar(False)

# Создаем меню файлов и добавляем действия


file_menu = self.menuBar().addMenu("Файл")
file_menu.addAction(self.quit_act)

# Создайте меню "Вид", подменю "Внешний вид" и добавьте действия


view_menu = self.menuBar().addMenu("Просмотр")
appearance_submenu = view_menu.addMenu("Внешний вид")
appearance_submenu.addAction(self.full_screen_act)

def switchToFullScreen(self, state):


"""Если состояние равно True, отобразите главное окно в полноэкранном режиме.
В противном случае верните окно в нормальное состояние."""
if state: self.showFullScreen()
else: self.showNormal()

Создание подменю аналогично созданию обычного меню. Сначала используйте


метод addMenu() для создания меню Просмотр. Затем создается подменю
appearance_submenu и добавляется в меню Просмотр с помощью addMenu(), но на
этот раз вызывается из экземпляра view_menu. Не забудьте также добавить действие
в подменю с помощью метода addAction().

В подменю appearance_submenu в примере добавлено действие full_screen_act,


которое позволяет пользователю переключаться между полноэкранным и обычным
режимами экрана. Потратьте время, чтобы также закодировать слот switchToFullScreen()
в MainWindow.
Следующим шагом будет создание панели инструментов приложения.

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

ГЛАВА 5 МЕНЮ, ПАНЕЛИ ИНСТРУМЕНТОВ И МНОГОЕ ДРУГОЕ 127


класс QToolBar предоставляет возможность создать панель инструментов с
пиктограммами, текстом или стандартными виджетами Qt для быстрого доступа к
часто используемым командам.
Панели инструментов обычно располагаются под строкой меню, как на
Рисунке 5-12, но также могут быть размещены вертикально или в нижней части
главного окна над строкой состояния. Обратитесь к изображению на Рисунке 5-2,
чтобы получить представление о том, как располагать панели инструментов в
главном окне.
В графическом интерфейсе может быть только одна строка меню, но может быть
несколько панелей инструментов. Сначала создаётся объект панели инструментов с
помощью класса QToolBar, как в Листинге 5-21, и задаётся ему название.
Листинг 5-21. Метод createToolBar() для главного окна с дополнительными
функциональными возможностями
Листинг 5-21. Метод createToolBar() для главного окна с дополнительными
функциональными возможностями
# main_window_extras.py
def createToolBar(self):
"""Создаем панель инструментов приложения."""
toolbar = QToolBar("Главная панель инструментов")
toolbar.setIconSize(QSize(16, 16))
self.addToolBar(toolbar)

# Добавить действия на панель инструментов


toolbar.addAction(self.quit_act)

Добавьте его в главное окно с помощью метода QMainWindow addToolBar(). Вы


также можете установить размер значков на панели инструментов с помощью метода
setIconSize(), чтобы избежать лишней набивки, когда PyQt пытается самостоятельно
определить их расположение. Чтобы добавить действие на панель инструментов,
используйте метод addAction(). Один и тот же значок будет отображаться как на
панели инструментов, так и в меню. Чтобы добавить виджет на панель инструментов,
используйте addWidget().
В завершение этого раздела давайте узнаем, как создавать виджеты док-станции.

Класс QDockWidget
Класс QDockWidget используется для создания съемных или плавающих
палитр инструментов или панелей виджетов. Док-виджеты - это вторичные окна,
которые обеспечивают дополнительную функциональность для окон графического
интерфейса.
Чтобы создать объект док-виджета, создайте экземпляр QDockWidget и установите
заголовок виджета с помощью метода setWindowTitle(), как показано в Листинге 5-22.

128 ГЛАВА 5 МЕНЮ, ПАНЕЛИ ИНСТРУМЕНТОВ И МНОГОЕ ДРУГОЕ


Листинг 5-22. Метод createDockWidget() для главного окна с дополнительными
функциональными возможностями
# main_window_extras.py
def createDockWidget(self):
"""Создайте виджет док-станции приложения."""
dock_widget = QDockWidget()
dock_widget.setWindowTitle("Форматирование")
dock_widget.setAllowedAreas(
Qt.DockWidgetArea.AllDockWidgetAreas)

# Создайте примеры виджетов для добавления в док-станцию


auto_bullet_cb = QCheckBox("Автоматический маркированный список")
auto_bullet_cb.toggled.connect(
self.changeTextEditSettings)

# Создание макета для виджета дока


dock_v_box = QVBoxLayout()
dock_v_box.addWidget(auto_bullet_cb)
dock_v_box.addStretch(1)

# Создайте QWidget, который действует как контейнер, чтобы


# удерживать другие виджеты
dock_container = QWidget()
dock_container.setLayout(dock_v_box)

# Установите главный виджет для виджета дока


dock_widget.setWidget(dock_container)

# Установите начальное расположение док-виджета в главном окне


self.addDockWidget(
Qt.DockWidgetArea.LeftDockWidgetArea, dock_widget)

def changeTextEditSettings(self, checked):


"""Изменить возможности форматирования для QTextEdit."""
if checked:
self.text_edit.setAutoFormatting(
QTextEdit.AutoFormattingFlag.AutoBulletList)
else:
self.text_edit.setAutoFormatting(
QTextEdit.AutoFormattingFlag.AutoNone)

Когда виджет док-станции пристыкован внутри главного окна, Qt обрабатывает


изменение размеров виджета док-станции и центрального виджета. Вы также
можете указать области, в которых вы хотите разместить док-виджет в главном окне,

ГЛАВА 5 МЕНЮ, ПАНЕЛИ ИНСТРУМЕНТОВ И МНОГОЕ ДРУГОЕ 129


используя setAllowedAreas().
Виджет QDockWidget может быть размещен на любой из четырех сторон окна.
Чтобы ограничить допустимые области док-станции, укажите одну область или их
комбинацию, разделенную символом трубы:
• LeftDockWidgetArea - расположить док-виджет на левой стороне
• RightDockWidgetArea - расположить док-виджет с правой стороны
• TopDockWidgetArea - расположить док-виджет в верхней области
• BottomDockWidgetArea - расположить док-виджет в нижней области
Док-виджет этого проекта содержит один QCheckBox, который позволяет
пользователю добавлять маркированный текст в основной виджет QTextEdit.
Слот, который вызывается при переключении флажка - changeTextEditSettings().
Когда флажок установлен, пользователь может ввести звездочку, чтобы создать
маркированный список.
Чтобы разместить несколько виджетов в доке, необходимо использовать
контейнер, такой как QWidget, который будет служить родителем для нескольких
дочерних виджетов, и расположить их с помощью одного из менеджеров компоновки.
Затем передайте объект QWidget в качестве аргумента в setWidget(). Наконец,
установите док-станцию и ее начальное расположение в главном окне с помощью
addDockWidget().
В этом приложении, если виджет док-станции закрыт, мы не можем получить его
обратно. В Проекте 5.2 мы рассмотрим, как использовать проверяемые пункты меню
для скрытия или отображения виджета дока.

130 ГЛАВА 5 МЕНЮ, ПАНЕЛИ ИНСТРУМЕНТОВ И МНОГОЕ ДРУГОЕ


Проект 5.2 - Простой графический интерфейс
редактора фотографий
В наше время довольно часто приходится редактировать изображения. Некоторые
фоторедакторы очень просты, позволяя пользователю поворачивать, обрезать
или добавлять фигуры. Другие позволяют изменять контрастность и экспозицию,
уменьшать шум или даже добавлять специальные эффекты.
В этом проекте вы рассмотрите, как создать базовый фоторедактор, показанный
на Рисунке 5-14.

Рисунок 5-14. Графический интерфейс редактора фотографий. Пейзажное


изображение взято с сайта https://pixabay.com.
Графический интерфейс содержит строку меню вверху, панель инструментов с
пиктограммами под строкой меню, центральный виджет, отображающий изображе-
ние, строку состояния внизу и док-виджет справа, содержащий простые инструмен-
ты для редактирования фотографии.

ГЛАВА 5 МЕНЮ, ПАНЕЛИ ИНСТРУМЕНТОВ И МНОГОЕ ДРУГОЕ 131


Проектирование графического интерфейса
редактора фотографий
Подобно Проекту 5.1, этот графический интерфейс также имеет строку меню,
которая будет содержать различные меню - Файл, Правка и Вид. Посмотрите на
схему графического интерфейса пользователя на Рисунке 5-15. Под строкой меню
находится панель инструментов, созданная с помощью класса QToolBar и содержащая
пиктограммы, представляющие действия, которые пользователь может предпринять
для открытия файла, сохранения файла и печати.

Рисунок 5-15. Макет графического интерфейса редактора фотографий. Главное


окно гораздо более оживленное, содержит панель инструментов, виджет док-станции
и строку состояния.

Пояснения к графическому интерфейсу редактора


фотографий
Этот проект познакомит вас с рядом новых классов и послужит средством
для дальнейшего изучения и применения концепций для создания более

132 ГЛАВА 5 МЕНЮ, ПАНЕЛИ ИНСТРУМЕНТОВ И МНОГОЕ ДРУГОЕ


крупных и сложных графических интерфейсов. Как и раньше, используйте файл
main_window_template.py в качестве отправной точки для Листинга 5-23 и начните с
импорта некоторых дополнительных классов PyQt.
Листинг 5-23. Настройка главного окна графического интерфейса редактора
фотографий
# photo_editor.py
# Импортируйте необходимые модули
import sys
from PyQt6.QtWidgets import (QApplication, QMainWindow,
QWidget, QLabel, QPushButton, QDockWidget, QDialog,
QFileDialog, QMessageBox, QToolBar, QStatusBar,
QVBoxLayout)
from PyQt6.QtCore import Qt, QSize, QRect
from PyQt6.QtGui import (QIcon, QAction, QPixmap, QTransform,
QPainter)
from PyQt6.QtPrintSupport import QPrinter, QPrintDialog

class MainWindow(QMainWindow):

def __init__(self):
super().__init__()
self.initializeUI()

def initializeUI(self):
"""Настройте графический интерфейс приложения."""
self.setFixedSize(650, 650)
self.setWindowTitle("5.2 - графический интерфейс фоторедактора")

self.setUpMainWindow()
self.createToolsDockWidget()
self.createActions()
self.createMenu()
self.createToolBar()
self.show()

if __name__ == '__main__':
app = QApplication(sys.argv)
app.setAttribute(
Qt.ApplicationAttribute.AA_DontShowIconsInMenus, True)
window = MainWindow()
sys.exit(app.exec())

ГЛАВА 5 МЕНЮ, ПАНЕЛИ ИНСТРУМЕНТОВ И МНОГОЕ ДРУГОЕ 133


Приложение импортирует множество новых классов из различных модулей. Из
модуля QtGui мы используем QPixmap для работы с изображениями, QTransform
для выполнения преобразований изображений и QPainter для рисования и
раскрашивания виджетов.
QRect, из QtCore, используется для создания прямоугольной формы виджетов.
Она будет использоваться в методе printImage(), где модуль QtPrintSupport и его
классы обеспечивают кросс-платформенную поддержку доступа к принтерам и
печати документов.
Окно инициализируется, как и раньше, только на этот раз используется метод
setFixedSize() для установки геометрии окна, чтобы его размер нельзя было изменить.
Различные вызовы методов настроят меню, окно, панель инструментов и виджет
док-станции.
В отличие от предыдущих примеров, в меню этого приложения не будут
отображаться значки; они будут отображаться только на панели инструментов. Qt
имеет ряд специальных флагов в пространстве имен Qt для задания параметров
окон и меню. В частности, одним из них является AA_DontShowIconsInMenus, который
используется для скрытия пиктограмм в меню. Вы можете использовать метод
QApplication setAttribute(), чтобы указать флаги, которые вы хотите использовать.
Главное окно этого графического интерфейса, построенное в Листинге 5-24, будет
состоять из виджета QLabel для отображения изображений.
Листинг 5-24. Метод setUpMainWindow() для графического интерфейса
фоторедактора
# photo_editor.py
def setUpMainWindow(self):
"""Создание и расположение виджетов в главном окне."""
self.image = QPixmap()

self.image_label = QLabel()
self.image_label.setAlignment(
Qt.AlignmentFlag.AlignCenter)
self.setCentralWidget(self.image_label)

# Создайте строку состояния


self.setStatusBar(QStatusBar())

Экземпляр QPixmap, image, используется для отображения изображения на QLabel,


image_label. Пиксмап инстанцируется в setUpMainWindow(), чтобы избежать проблем
с кнопками, которые управляют изображением. Вы можете избежать этого случая,
если отключите все кнопки и действия редактирования при запуске приложения и
включите их только тогда, когда изображение будет установлено в метке image_label.
Действия для меню Файл - Открыть, Сохранить, Печать и Выход - обрабатываются
в Листинге 5-25.

134 ГЛАВА 5 МЕНЮ, ПАНЕЛИ ИНСТРУМЕНТОВ И МНОГОЕ ДРУГОЕ


Листинг 5-25. Метод createActions() для графического интерфейса редактора
фотографий, часть 1
# photo_editor.py
def createActions(self):
"""Создайте действия меню приложения."""
# Создаем действия для меню Файл
self.open_act = QAction(
QIcon("images/open_file.png"), "Открыть")
self.open_act.setShortcut("Ctrl+O")
self.open_act.setStatusTip("Открыть новое изображение")
self.open_act.triggered.connect(self.openImage)

self.save_act = QAction(
QIcon("images/save_file.png"), "Сохранить")
self.save_act.setShortcut("Ctrl+S")
self.save_act.setStatusTip("Сохранить изображение")
self.save_act.triggered.connect(self.saveImage)

self.print_act = QAction(
QIcon("images/print.png"), "Печать")
self.print_act.setShortcut("Ctrl+P")
self.print_act.setStatusTip("Печать изображения")
self.print_act.triggered.connect(self.printImage)
self.print_act.setEnabled(False)

self.quit_act = QAction(
QIcon("images/exit.png"), "Выход")
self.quit_act.setShortcut("Ctrl+Q")
self.quit_act.setStatusTip("Выход из программы")
self.quit_act.triggered.connect(self.close)

Установка метода setEnabled() для print_act в False показывает отключенный пункт


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

Действия для меню Правка созданы в Листинге 5-26. Эти действия также
появляются на панели инструментов приложения в Листинге 5-27.

ГЛАВА 5 МЕНЮ, ПАНЕЛИ ИНСТРУМЕНТОВ И МНОГОЕ ДРУГОЕ 135


Листинг 5-26. Метод createActions() для графического интерфейса редактора
фотографий, часть 2
# photo_editor.py
# Создание действий для меню редактирования
self.rotate90_act = QAction("Повернуть на 90°")
self.rotate90_act.setStatusTip(
"Повернуть изображение на 90° по часовой стрелке")
self.rotate90_act.triggered.connect(
self.rotateImage90)

self.rotate180_act = QAction("Повернуть на 180°")


self.rotate180_act.setStatusTip(
"Повернуть изображение на 180° по часовой стрелке")
self.rotate180_act.triggered.connect(
self.rotateImage180)

self.flip_hor_act = QAction("Перевернуть по горизонтали")


self.flip_hor_act.setStatusTip(
"Перевернуть изображение по горизонтальной оси")
self.flip_hor_act.triggered.connect(
self.flipImageHorizontal)

self.flip_ver_act = QAction("Перевернуть вертикально")


self.flip_ver_act.setStatusTip(
"Перевернуть изображение по вертикальной оси")
self.flip_ver_act.triggered.connect(
self.flipImageVertical)

self.resize_act = QAction("Изменить размер наполовину")


self.resize_act.setStatusTip(
"Изменение размера изображения до половины исходного размера")
self.resize_act.triggered.connect(
self.resizeImageHalf)

self.clear_act = QAction(
QIcon("images/clear.png"), "Очистить изображение")
self.clear_act.setShortcut("Ctrl+D")
self.clear_act.setStatusTip("Очистить текущее изображение")
self.clear_act.triggered.connect(self.clearImage)

Действия для меню Edit используются для поворота изображения, переворачивания


изображения, изменения размера изображения в два раза и очистки изображения.
За исключением clear_act, эти действия также добавлены в виджет док-станции в
Листинге 5-28.

136 ГЛАВА 5 МЕНЮ, ПАНЕЛИ ИНСТРУМЕНТОВ И МНОГОЕ ДРУГОЕ


Метод создания строки меню приведен в Листинге 5-27.
Листинг 5-27. Метод createMenu() для графического интерфейса редактора
фотографий
# photo_editor.py
def createMenu(self):
"""Создаем строку меню приложения."""
self.menuBar().setNativeMenuBar(False)

# Создаем меню файла и добавляем действия


file_menu = self.menuBar().addMenu("Файл")
file_menu.addAction(self.open_act)
file_menu.addAction(self.save_act)
file_menu.addSeparator()
file_menu.addAction(self.print_act)
file_menu.addSeparator()
file_menu.addAction(self.quit_act)

# Создайте меню редактирования и добавьте действия


edit_menu = self.menuBar().addMenu("Правка")
edit_menu.addAction(self.rotate90_act)
edit_menu.addAction(self.rotate180_act)
edit_menu.addSeparator()
edit_menu.addAction(self.flip_hor_act)
edit_menu.addAction(self.flip_ver_act)
edit_menu.addSeparator()
edit_menu.addAction(self.resize_act)
edit_menu.addSeparator()
edit_menu.addAction(self.clear_act)

# Создайте меню просмотра и добавьте действия


view_menu = self.menuBar().addMenu("Просмотр")
view_menu.addAction(self.toggle_dock_act)

Меню Вид содержит действие для переключения видимости виджета дока.


Если док-станция была закрыта, ее можно снова открыть с помощью этого меню.
Это действие было создано не в createActions(), а в методе createToolsDockWidget()
в Листинге 5-29. По этой же причине createToolsDockWidget() вызывается перед
createActions() в initializeUI(), поскольку createActions() полагается на то, что действие
toggle_dock_tools_act уже создано.
Панель инструментов и ее действия добавлены в приложение в Листинге 5-28.

ГЛАВА 5 МЕНЮ, ПАНЕЛИ ИНСТРУМЕНТОВ И МНОГОЕ ДРУГОЕ 137


Листинг 5-28. Создание панели инструментов для графического интерфейса
редактора фотографий
# photo_editor.py
def createToolBar(self):
"""Создаем панель инструментов приложения."""
tool_bar = QToolBar("Панель инструментов фоторедактора")
tool_bar.setIconSize(QSize(24,24))
self.addToolBar(tool_bar)

# Добавьте действия на панель инструментов


tool_bar.addAction(self.open_act)
tool_bar.addAction(self.save_act)
tool_bar.addAction(self.print_act)
tool_bar.addAction(self.clear_act)
tool_bar.addSeparator()
tool_bar.addAction(self.quit_act)

Виджет док-станции в Листинге 5-29 отображает опции редактирования


из меню Edit. Он ограничен только левой и правой сторонами приложения с
помощью setAllowedAreas(). После этого шага создайте виджеты QPushButton для
редактирования изображений.
Листинг 5-29. Создание виджета док-станции для графического интерфейса
редактора фотографий
# photo_editor.py
def createToolsDockWidget(self):
"""Создайте виджет док-станции приложения. Используйте View ->
меню Инструменты редактирования изображений для отображения/скрытия дока."""
dock_widget = QDockWidget()
dock_widget.setWindowTitle("Edit Image Tools")
dock_widget.setAllowedAreas(
Qt.DockWidgetArea.LeftDockWidgetArea |
Qt.DockWidgetArea.RightDockWidgetArea)

# Создайте кнопки для редактирования изображений


self.rotate90 = QPushButton("Повернуть на 90°")
self.rotate90.setMinimumSize(QSize(130, 40))
self.rotate90.setStatusTip(
"Повернуть изображение на 90° по часовой стрелке")
self.rotate90.clicked.connect(self.rotateImage90)

self.rotate180 = QPushButton("Повернуть на 180°")


self.rotate180.setMinimumSize(QSize(130, 40))
self.rotate180.setStatusTip(

138 ГЛАВА 5 МЕНЮ, ПАНЕЛИ ИНСТРУМЕНТОВ И МНОГОЕ ДРУГОЕ


"Повернуть изображение на 180° по часовой стрелке")
self.rotate180.clicked.connect(self.rotateImage180)

self.flip_horizontal = QPushButton("Перевернуть горизонтально")


self.flip_horizontal.setMinimumSize(QSize(130, 40))
self.flip_horizontal.setStatusTip(
"Перевернуть изображение по горизонтальной оси")
self.flip_horizontal.clicked.connect(
self.flipImageHorizontal)

self.flip_vertical = QPushButton("Перевернуть вертикально")


self.flip_vertical.setMinimumSize(QSize(130, 40))
self.flip_vertical.setStatusTip(
"Перевернуть изображение по вертикальной оси")
self.flip_vertical.clicked.connect(
self.flipImageVertical)

self.resize_half = QPushButton("Изменить размер наполовину")


self.resize_half.setMinimumSize(QSize(130, 40))
self.resize_half.setStatusTip(
"Изменение размера изображения до половины исходного размера")
self.resize_half.clicked.connect(self.resizeImageHalf)

# Создание макета для док-виджета


dock_v_box = QVBoxLayout()
dock_v_box.addWidget(self.rotate90)
dock_v_box.addWidget(self.rotate180)
dock_v_box.addStretch(1)
dock_v_box.addWidget(self.flip_horizontal)
dock_v_box.addWidget(self.flip_vertical)
dock_v_box.addStretch(1)
dock_v_box.addWidget(self.resize_half)
dock_v_box.addStretch(10)

# Создайте QWidget, который действует как контейнер и


# установите макет для дока
tools_container = QWidget()
tools_container.setLayout(dock_v_box)
dock_widget.setWidget(tools_container)

# Установите начальное местоположение виджета док-станции


self.addDockWidget(
Qt.DockWidgetArea.RightDockWidgetArea,
dock_widget)

ГЛАВА 5 МЕНЮ, ПАНЕЛИ ИНСТРУМЕНТОВ И МНОГОЕ ДРУГОЕ 139


# Обработка видимости док-виджета
self.toggle_dock_act = dock_widget.toggleViewAction()

Добавьте созданные вами виджеты в макет, добавьте этот макет в контейнер


QWidget, а затем добавьте его в док с помощью addDockWidget(). Чтобы обработать,
когда виджет док-станции отмечен или не отмечен в меню или если пользователь
закрыл виджет док-станции с помощью его кнопки закрытия, используйте метод
QDockWidget toggleViewAction() для создания действия.

Работа с изображениями в графическом интерфейсе фоторедактора


В этом разделе рассматриваются методы в MainWindow для взаимодействия
с локальными файлами изображений. В листинге 5-30, когда пользователь хочет
открыть или сохранить изображение, появляется QFileDialog.
Листинг 5-30. Создание слотов для загрузки и сохранения изображений в
графическом интерфейсе фоторедактора
# photo_editor.py
def openImage(self):
"""Открываем файл изображения и отображаем его содержимое на
виджете QLabel."""
image_file, _ = QFileDialog.getOpenFileName(
self, "Открыть изображение", "",
"JPG Файлы (*.jpeg *.jpg );;PNG Файлы (*.png);;\
растровые файлы (*.bmp);;GIF файлы (*.gif)")

if image_file:
self.image = QPixmap(image_file)

self.image_label.setPixmap(
self.image.scaled(self.image_label.size(),
Qt.AspectRatioMode.KeepAspectRatio,
Qt.TransformationMode.SmoothTransformation))
else:
QMessageBox.information(self, "Нет изображения",
"Изображение не выбрано",
QMessageBox.StandardButton.Ok)
self.print_act.setEnabled(True)

def saveImage(self):
"""Отображает QFileDialog для выбора местоположения изображения и
сохранить изображение."""
image_file, _ = QFileDialog.getSaveFileName(
self, "Сохранить изображение", "",
"JPG Файлы (*.jpeg *.jpg );;PNG Файлы (*.png);;\
растровые файлы (*.bmp);;GIF файлы (*.gif)")
140 ГЛАВА 5 МЕНЮ, ПАНЕЛИ ИНСТРУМЕНТОВ И МНОГОЕ ДРУГОЕ
if image_file and self.image.isNull() == False:
self.image.save(image_file)
else:
QMessageBox.information(self, "Не сохранено",
"Изображение не сохранено",
QMessageBox.StandardButton.Ok)

Если пользователь выбирает локальное изображение, экземпляр image


обновляется новым пиксмапом, image. Затем новое изображение применяется к
метке image_label с помощью setPixmap(), где изображение масштабируется, чтобы
соответствовать текущему размеру метки image_label. Его размер определяется с
помощью метода size() и зависит от того, открыт или закрыт виджет док-станции.
При изменении размера пиксмапа могут быть указаны и другие параметры. То, как
изображение будет использовать доступное пространство, можно определить с
помощью перечисления Qt.AspectRatioMode. Соотношение сторон может быть либо
• Игнорироваться, чтобы изображение занимало все доступное пространство
без учета соотношения сторон (IgnoreAspectRatio)
• Сохраняться, но также масштабироваться, чтобы поместиться в пределах
этикетки (KeepAspectRatio)
• Сохраняться, но может расширяться за пределы метки
(KeepAspectRatioByExpanding).
Когда изображение масштабируется, чтобы соответствовать текущему размеру
этикетки, текстуры изображения необходимо сгладить, чтобы избежать искажения
изображения. Сглаживание изображения задается с помощью параметра
SmoothTransformation. Другим параметром является FastTransformation, при котором
сглаживание не происходит.
Если пользователь хочет сохранить изображение, в функции saveImage()
используется метод QPixmap save().
Слоты для очистки и поворота изображений вызываются всякий раз, когда
операция выбрана в меню, панели инструментов или доке. Пример поворота
изображения показан на Рисунке 5-16. Слоты созданы в Листинге 5-31.

ГЛАВА 5 МЕНЮ, ПАНЕЛИ ИНСТРУМЕНТОВ И МНОГОЕ ДРУГОЕ 141


Рисунок 5-16. Пример поворота на 90° в графическом интерфейсе редактора
фотографий. Изображение растягивается по горизонтали, чтобы поместиться в
главном окне

Листинг 5-31. Создание слотов для очистки и поворота изображений в


графическом интерфейсе редактора фотографий
# photo_editor.py
def clearImage(self):
"""Очищает текущее изображение в виджете QLabel."""
self.image_label.clear()
self.image = QPixmap() # Сброс пиксмапа
self.print_act.setEnabled(False)

def rotateImage90(self):
"""Поверните изображение на 90° по часовой стрелке."""
if self.image.isNull() == False:
transform90 = QTransform().rotate(90)
pixmap = QPixmap(self.image)
mode = Qt.TransformationMode.SmoothTransformation
rotated = pixmap.transformed(transform90,
mode = mode)
142 ГЛАВА 5 МЕНЮ, ПАНЕЛИ ИНСТРУМЕНТОВ И МНОГОЕ ДРУГОЕ
self.image_label.setPixmap(
rotated.scaled(self.image_label.size(),
Qt.AspectRatioMode.KeepAspectRatio,
Qt.TransformationMode.SmoothTransformation))
self.image = QPixmap(rotated)
self.image_label.repaint() # Перекрашивание метки

def rotateImage180(self):
"""Поверните изображение на 180° по часовой стрелке."""
if self.image.isNull() == False:
transform180 = QTransform().rotate(180)
pixmap = QPixmap(self.image)
mode = Qt.TransformationMode.SmoothTransformation
rotated = pixmap.transformed(transform180,
mode = mode)

self.image_label.setPixmap(
rotated.scaled(self.image_label.size(),
Qt.AspectRatioMode.KeepAspectRatio,
Qt.TransformationMode.SmoothTransformation))
# Для того, чтобы не позволить вращать
# изображение, установите повернутое изображение как self.image
self.image = QPixmap(rotated)
self.image_label.repaint() # Перекрасить метку

Слот clearImage() очищает метку image_label, создает пустой пиксмап для


изображения и отключает print_act.
Преобразования изображения происходят, когда к изображению применяется
функция, изменяющая его каким-либо образом. Преобразования включают
вращение, масштабирование и сглаживание. QTransform используется для
манипулирования графикой в двухмерном пространстве. В функциях rotateImage90()
и rotateImage180() класс QTransform используется для поворота изображений. Метод
QPixmap transformed() используется для возврата преобразованного пиксмапа. Затем
повернутое изображение устанавливается на метку.
Метод repaint() важен, поскольку он гарантирует, что содержимое метки
image_label будет обновлено после преобразования.
В Листинге 5-32 продолжается использование QTransform для переворачивания
изображений.

ГЛАВА 5 МЕНЮ, ПАНЕЛИ ИНСТРУМЕНТОВ И МНОГОЕ ДРУГОЕ 143


Листинг 5-32. Создание слотов для переворачивания изображений в графическом
интерфейсе редактора фотографий
# photo_editor.py
def flipImageHorizontal(self):
"""Зеркальное отражение изображения по горизонтальной оси."""
if self.image.isNull() == False:
flip_h = QTransform().scale(-1, 1)
pixmap = QPixmap(self.image)
flipped = pixmap.transformed(flip_h)

self.image_label.setPixmap(
flipped.scaled(self.image_label.size(),
Qt.AspectRatioMode.KeepAspectRatio,
Qt.TransformationMode.SmoothTransformation))
self.image = QPixmap(flipped)
self.image_label.repaint()

def flipImageVertical(self):
"""Зеркально отразите изображение по вертикальной оси."""
if self.image.isNull() == False:
flip_v = QTransform().scale(1, -1)
pixmap = QPixmap(self.image)
flipped = pixmap.transformed(flip_v)

self.image_label.setPixmap(
flipped.scaled(self.image_label.size(),
Qt.AspectRatioMode.KeepAspectRatio,
Qt.TransformationMode.SmoothTransformation))
self.image = QPixmap(flipped)
self.image_label.repaint()

Процесс переворачивания изображений по горизонтальной и вертикальной


осям аналогичен коду для поворота. Ключевым отличием является использование
Transform.scale() для масштабирования изображений по горизонтальной и
вертикальной осям. Отрицательное значение, переданное в scale(), перевернет
изображение в этом направлении. Значение 1 означает, что размер изображения не
изменится, в то время как значение 0,5 уменьшит изображение вдвое. Это показано
в Листинге 5-33.

144 ГЛАВА 5 МЕНЮ, ПАНЕЛИ ИНСТРУМЕНТОВ И МНОГОЕ ДРУГОЕ


Листинг 5-33. Создание слотов для изменения размера изображений в
графическом интерфейсе редактора фотографий
# photo_editor.py
def resizeImageHalf(self):
"""Изменить размер изображения до половины его текущего размера."""
if self.image.isNull() == False:
resize = QTransform().scale(0.5, 0.5)
pixmap = QPixmap(self.image)
resized = pixmap.transformed(resize)

self.image_label.setPixmap(
resized.scaled(self.image_label.size(),
Qt.AspectRatioMode.KeepAspectRatio,
Qt.TransformationMode.SmoothTransformation))
self.image = QPixmap(resized)
self.image_label.repaint()

Последний шаг, о котором нужно позаботиться, это создание метода для печати.

Класс QPrinter
Фоторедактор включает метод для печати изображений. Класс QPrinter
используется для создания страницы для печати документов. Для страницы можно
задать ряд параметров, включая ее ориентацию и размер бумаги.
В данном примере мы хотим использовать QPrinter для печати изображения на
метке image_label. Для этого нам также потребуется использовать класс QPainter,
чтобы указать, что рисовать на странице, которую мы хотим напечатать, то есть на
изображении. Все это рассматривается в Листинге 5-34.
Листинг 5-34. Создание слота для печати изображений в графическом интерфейсе
редактора фотографий
# photo_editor.py
def printImage(self):
"""Печатаем изображение и используем QPrinter для выбора
родной системный формат для диалогового окна принтера."""
printer = QPrinter()
# Конфигурируем принтер
print_dialog = QPrintDialog(printer)
if print_dialog.exec() == QDialog.DialogCode.Accepted:
# Используйте QPainter для вывода PDF файла
painter = QPainter()
painter.begin(printer)
# Создайте объект QRect для хранения текущего видового экрана художника,
ГЛАВА 5 МЕНЮ, ПАНЕЛИ ИНСТРУМЕНТОВ И МНОГОЕ ДРУГОЕ 145
# которым является метка_изображения
rect = QRect(painter.viewport())

# Получите размер метки image_label и используйте его для установки


# размера области просмотра
size = QSize(self.image_label.pixmap().size())
size.scale(rect.size(),
Qt.AspectRatioMode.KeepAspectRatio)
painter.setViewport(rect.x(), rect.y(),
size.width(), size.height())
painter.setWindow(
self.image_label.pixmap().rect())
# Масштабируйте image_label, чтобы оно поместилось
# в исходный прямоугольник (0, 0)
painter.drawPixmap(0, 0,
self.image_label.pixmap())
painter.end()

Когда объект QPrinter определен, следующим шагом будет открытие диалогового


окна QPrintDialog, имеющего собственный вид, чтобы пользователь мог настроить
параметры принтера. Если пользователь нажимает кнопку Print в диалоге (который
ссылается на QDialog.DialogCode,Accepted), создается объект QPainter, которому
передается объект принтера. Отсюда мы можем получить размер этикетки и
масштабировать painter до размера пиксмапа.
Метод QPainter setViewport() задает размер координат печатающего устройства,
а setWindow() определяет логические координаты. Используя QPainter, мы должны
сопоставить логические координаты этикетки с физическими координатами перед
печатью. Изображение отправляется на принтер при вызове end().
Теперь пользователь может выполнять простые редактирования, сохранять и
печатать свои изображения с помощью Графического интерфейса фоторедактора.

Резюме
В этой главе вы смогли убедиться в преимуществах использования класса
QMainWindow для построения главного окна вашего приложения. QMainWindow
предоставляет функциональные возможности и взаимодействия, необходимые
для интеграции панелей инструментов, меню, док-виджетов и строк состояния в
ваши графические интерфейсы. Еще многое предстоит узнать об использовании
меню, например, о создании контекстных меню и отображении виджетов в строке
состояния. По мере изучения этой книги вы продолжите изучать и применять многие
из этих концепций на практических примерах.
Строка меню может состоять из нескольких меню, каждое из которых может
быть разбито на несколько команд. Каждая из этих команд сама по себе также может

146 ГЛАВА 5 МЕНЮ, ПАНЕЛИ ИНСТРУМЕНТОВ И МНОГОЕ ДРУГОЕ


быть выбираемой или даже подменю. Класс QAction обеспечивает выполнение
правильного действия независимо от того, что вызвало действие, будь то меню,
панель инструментов, клавиши быстрого доступа или виджет. Панели инструментов
часто состоят из пиктограмм, которые позволяют пользователю находить часто
используемые команды. Класс QDockWidget создает подвижные и плавающие панели
для организации и отображения различных инструментов, виджетов или команд
для пользователя. Строки состояния создают пространство для предоставления
дополнительной текстовой информации о виджетах или обратной связи.
В Главе 6 вы узнаете, как изменять внешний вид и стиль виджетов с помощью
таблиц стилей.

ГЛАВА 5 МЕНЮ, ПАНЕЛИ ИНСТРУМЕНТОВ И МНОГОЕ ДРУГОЕ 147


ГЛАВА 6

Стилизация графических
интерфейсов
В графических интерфейсах, которые вы создавали до сих пор, основное внимание
уделялось функциональности и меньше - внешнему виду и настройке. Создание
интерактивного, последовательного и профессионально выглядящего графического
интерфейса может быть достигнуто не только с помощью виджетов и менеджеров
компоновки, но и путем изменения внешнего вида и поведения каждого объекта в
интерфейсе. Выбор правильного стиля, цветов, шрифтов и тонких форм обратной
связи может помочь создать последовательный, простой в навигации и удобный для
пользователя интерфейс.
В этой главе вы
• Узнаете о стилизации приложений PyQt
• Узнаете, как настроить внешний вид виджетов с помощью Qt Style Sheets и
HTML
• Использовать новые виджеты и классы PyQt, включая QRadioButton,
QGroupBox и QTabWidget
• Используйте контейнеры и виджеты с вкладками для организации и
управления группами виджетов
Давайте начнем с изучения того, что такое стили в PyQt. После этого вы узнаете,
как настраивать внешний вид окон и виджетов приложения.

Что такое стили в PyQt?


Когда вы используете PyQt, внешний вид ваших приложений обрабатывается
классом Qt QStyle. QStyle содержит ряд подклассов, которые имитируют внешний вид
системы, на которой выполняется приложение. Это позволяет вашему графическому
интерфейсу выглядеть как родное приложение для macOS, Linux или Windows.
Пользовательские стили могут быть созданы либо путем изменения существующих
классов QStyle, либо путем создания собственных классов, либо с помощью Qt Style
Sheets.
Без указания стиля в коде PyQt автоматически выбирает стиль, который делает
графический интерфейс похожим на родное приложение. Существует также ряд
встроенных стилей. Вы можете использовать Листинг 6-1, чтобы узнать, какие стили
доступны в вашей операционной системе.
148 ГЛАВА 6 СТИЛИЗАЦИЯ ГРАФИЧЕСКИХ ИНТЕРФЕЙСОВ
Листинг 6-1. Выяснение того, какие стили доступны в вашей локальной системе
# styles.py
# Импортируйте необходимые модули
import sys
from PyQt6.QtWidgets import QApplication, QStyleFactory

# Выясните доступные стили для вашей ОС


print(f"Keys: {QStyleFactory.keys()}")

# Узнайте стиль по умолчанию, применяемый к приложению


app = QApplication(sys.argv)
print(f"Стиль по умолчанию: {app.style().name()}")
Выполнение этого короткого сценария выведет в оболочке macOS следующее:
Keys: ['macOS', 'Windows', 'Fusion'].
Default style: macos

В Windows, вероятно, вы получите другой набор ключей


(['windowsvista', 'Windows', 'Fusion']) и стиль (windowsvista). В Linux также должны
получиться разные результаты.
Класс QStyleFactory используется для создания объекта QStyle. Печать ключей
QStyleFactory вернет список всех возможных стилей, доступных в вашей ОС. Вывод
изменится, если вы находитесь в Windows или Linux. Стили Windows и Fusion обычно
включены во всех системах.

Изменение стиля по умолчанию


Можно изменить стиль, используемый приложением, с помощью метода
QApplication setStyle(). Обязательно передайте в качестве аргумента один из доступных
стилей. Например:
app.setStyle("Fusion")
Стили также можно указать в командной строке при запуске приложения,
включив опцию -style и тип стиля, например
$ python3 food_order.py -style Fusion
Уделите немного времени и попробуйте изменить стиль предыдущих программ.
Обязательно включите опцию -style или используйте метод setStyle() и обратите
внимание на разницу во внешнем виде.
В следующих разделах мы рассмотрим, как можно настраивать внешний вид вид
виджетов в пользовательских интерфейсах.

ГЛАВА 6 СТИЛИЗАЦИЯ ГРАФИЧЕСКИХ ИНТЕРФЕЙСОВ 149


Изменение внешнего вида виджетов
Если вы собираетесь изменить родные стили, заданные виджетам в PyQt, важно
учитывать несколько принципов:
1. Согласованность заключается в том, чтобы виджеты и другие компоненты
графического интерфейса выглядели и вели себя одинаково.
2. Визуальная иерархия может быть создана с помощью цвета, расположения,
размера или даже глубины.
3. Отношения между различными виджетами могут быть установлены
с помощью того, как они расположены или выровнены. Виджеты,
расположенные ближе друг к другу или расположенные вертикально или
горизонтально в линию, обычно воспринимаются как связанные.
4. Подчеркивание можно использовать для того, чтобы направить внимание
пользователя на определенные виджеты или части окна или диалога. Это
может быть достигнуто с помощью визуального контраста, возможно, с
помощью различных размеров или шрифтов.
5. Паттерны в дизайне графического интерфейса можно использовать для
сокращения времени, необходимого пользователю для выполнения задачи,
поддержания последовательности и создания единства в интерфейсе.
В PyQt можно использовать язык разметки гипертекста (HTML) для изменения
внешнего вида текста и каскадные таблицы стилей (CSS) для настройки внешнего
вида виджетов и текста. На момент публикации Qt все еще использует подмножество
HTML4. Мы рассмотрим эти языки подробнее в следующих разделах.

Использование HTML для изменения внешнего


вида текста
Для классов PyQt, которые могут отображать насыщенный текст, таких как
QLabel и QLineEdit, HTML можно использовать для изменения внешнего вида текста.
Для демонстрации мы создадим простое окно в Листинге 6-2. В графическом
интерфейсе отображаются два виджета QLabel - один, где текст не изменен, и другой
с изменениями текста. Вы можете использовать сценарий basic_window.py из Главы 1,
чтобы приступить к созданию этого примера.

150 ГЛАВА 6 СТИЛИЗАЦИЯ ГРАФИЧЕСКИХ ИНТЕРФЕЙСОВ


Листинг 6-2. Изменение стиля текста в виджете QLabel с помощью HTML
# html_ex.py
# Импортируем необходимые модули
import sys
from PyQt6.QtWidgets import (QApplication, QWidget, QLabel,
QVBoxLayout)

class MainWindow(QWidget):
def __init__(self):
super().__init__()
self.initializeUI()

def initializeUI(self):
"""Настройте графический интерфейс приложения."""
self.setMinimumSize(300, 100)
self.setWindowTitle("Пример HTML")
no_style_label = QLabel(
"""Не бойтесь совершенства.
- вы никогда его не достигнете.
- Сальвадор Дали""")
style_label = QLabel("""
<p><font color='#DB8D31' face='Times' size='+2'>
Не бойтесь совершенства -
вы никогда его не достигнете.</font></p>
<p align='right'>
<b> - <i>Сальвадор Дали</i></b></p>""")

v_box = QVBoxLayout()
v_box.addWidget(no_style_label)
v_box.addWidget(style_label)
self.setLayout(v_box)
self.show()

if __name__ == '__main__':
app = QApplication(sys.argv)
window = MainWindow()
sys.exit(app.exec())

Настройка главного окна аналогична предыдущим программам, поэтому в этом


примере мы больше сосредоточимся на двух экземплярах QLabel, no_style_label и
style_label. Экземпляр no_style_label похож на другие виджеты QLabel, которые мы
создавали ранее. Благодаря использованию тройных кавычек текст, отображаемый в
метке, может занимать несколько строк. Вы можете увидеть это на Рисунке 6-1.

ГЛАВА 6 СТИЛИЗАЦИЯ ГРАФИЧЕСКИХ ИНТЕРФЕЙСОВ 151


Рисунок 6-1. Два виджета QLabel отображают один и тот же текст, но нижняя
метка была изменена
Для style_label используются различные HTML-теги и атрибуты для описания
внешнего вида текста. Теги используются для определения отдельных участков
текста, а атрибуты - для описания дополнительных характеристик тега. Теги обычно
состоят из начального тега, например, <p>, и соответствующего конечного тега, </p>.
Тег p используется для определения отдельного блока текста в большом разделе.

Примечание. Поскольку Qt все еще использует HTML4, вы все еще можете


использовать некоторые теги, которые устарели в HTML5. Во многих случаях
может оказаться более эффективным использовать HTML-теги вместе с
CSS-форматированием. (Мы рассмотрим CSS более подробно в следующем
разделе.) Этот раздел просто предоставляет один из методов работы с
текстом.

Изменения стиля здесь задаются в строке, то есть HTML-код не загружается


из внешнего файла, а непосредственно указывается для каждого виджета. Такой
способ удобен для небольших изменений текста или виджетов. Однако, как мы
увидим в последующих примерах, создание отдельной переменной или даже файла
для хранения стилей является лучшей практикой. В Таблице 6-1 описаны теги и
атрибуты, используемые в Листинге 6-2.
Таблица 6-1. Некоторые теги и атрибуты HTML4, которые могут быть
использованы в PyQt

Тег Описание
p Определяет абзац. Для изменения тега можно использовать такие
атрибуты, как выравнивание
font Используется для задания внешнего вида шрифта с помощью атрибутов
color, face и size.
b Определяет полужирный текст
i Определяет курсивный текст

152 ГЛАВА 6 СТИЛИЗАЦИЯ ГРАФИЧЕСКИХ ИНТЕРФЕЙСОВ


Более подробную информацию об использовании
HTML и поддерживаемых тегах в Qt можно найти на сайте
https://doc.qt.io/qt-6/richtext-html-subset.html#using-html-markup-in-text-widgets.
В следующем разделе мы обсудим, как использовать подмножество свойств CSS,
доступных в Qt.

Использование таблиц стилей Qt для изменения


внешнего вида виджетов
CSS - это язык, который можно использовать наряду с HTML для определения
того, как должны быть оформлены различные компоненты приложения. Свойства
в таблицах стилей CSS применяются "каскадным" образом, то есть свойства
применяются последовательно в таблице стилей. Иногда могут возникать конфликты
в зависимости от порядка таблиц стилей или между родительскими и дочерними
виджетами, поэтому вам необходимо обратить внимание на то, как вы организуете
свои таблицы стилей. Вы также столкнетесь с проблемами, когда в окне есть несколько
объектов одного типа виджетов, но вы хотите применить разные стили.
С помощью таблиц стилей Qt можно настроить ряд различных свойств виджетов,
включая цвет фона, размер и цвет шрифта, тип, ширину или стиль границы, а также
добавить подложку к виджетам. Можно также изменять псевдосостояния, которые
определяют особые состояния виджета, например, когда мышь наводится на виджет
или когда виджет меняет состояние с активного на отключенное. Подконтроли также
могут быть изменены, что позволяет получить доступ к подэлементам виджета и
изменить их внешний вид, расположение или другие свойства. Например, вы можете
изменить внешний вид индикатора для QCheckBox, чтобы он имел другой цвет или
значок при установке или снятии флажка.
Настройки можно применять как к отдельным виджетам, так и к экземпляру
QApplication приложения с помощью функции setStyleSheet(). Для получения
списка виджетов, которые могут быть настроены, или для получения
ссылки на все различные свойства, поддерживаемые в Qt, загляните на
https://doc.qt.io/qt-6/stylesheet-reference.html. Примеры использования таблиц стилей
Qt можно найти на https://doc.qt.io/qt-6/stylesheet-examples.html.
Давайте рассмотрим несколько примеров, прежде чем перейти к созданию
приложения. Изменение цвета фона виджета - довольно распространенная задача.
Чтобы изменить цвет со стандартного серого на синий, можно использовать
следующую строку кода:
line_edit.setStyleSheet("background-color: blue")
Передайте свойство CSS и значение, разделенные двоеточием, в виде строки
в setStyleSheet(). Здесь цвет фона для line_edit установлен на синий с помощью
CSS-свойства background-color. Эта строка, определяющая изменения, называется
декларацией. Если вы корректируете несколько свойств в одном операторе, отделите

ГЛАВА 6 СТИЛИЗАЦИЯ ГРАФИЧЕСКИХ ИНТЕРФЕЙСОВ 153


каждое свойство точкой с запятой.
Цвета в таблице стилей могут быть заданы в шестнадцатеричном формате, RGB
или в формате ключевого слова color. Чтобы изменить цвет переднего плана (цвет
текста) виджета, посмотрите на следующий код:
line_edit.setStyleSheet("color: rgb(244, 160, 25") # оранжевый
Для окон и некоторых виджетов можно даже задать фоновое изображение.
Чтобы добавить фоновое изображение в класс главного окна, можно использовать
следующий код:
self.setStyleSheet("background-image: url(images/logo.png)")
Вам нужно будет использовать синтаксис url() и передать в качестве аргумента
местоположение файла. Полезную ссылку, касающуюся синтаксиса таблиц стилей,
можно найти на сайте https://doc.qt.io/qt-5/stylesheet-syntax.html.
Первый пример GUI, который вы создадите, показан на Рисунке 6-2. Приложение
состоит из виджетов QLabel и QPushButton, а стили применяются в режиме inline.

Рисунок 6-2. Настроенные виджеты QLabel и QPushButton


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

Рисунок 6-3. Графический интерфейс PyQt без таблиц стилей

154 ГЛАВА 6 СТИЛИЗАЦИЯ ГРАФИЧЕСКИХ ИНТЕРФЕЙСОВ


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

Объяснение использования встроенных (inline)


таблиц стилей Qt
В Листингах с 6-3 по 6-5 вы вкратце рассмотрите, как настраивать отдельные
свойства виджетов. Начнем с создания нового файла с помощью сценария
basic_window.py, включим дополнительные импорты QtWidgets в верхней части
и изменим настройки в initializeUI(). Этот графический интерфейс служит для
демонстрации того, как стилизовать виджеты, поэтому виджеты не связаны ни с
какими сигналами. Обязательно загрузите папку images из репозитория GitHub этой
главы.
Листинг 6-3. Настройка главного окна для использования таблиц стилей Qt
# style_sheet_ex.py
# Импортируйте необходимые модули
import sys
from PyQt6.QtWidgets import (QApplication, QWidget, QLabel,
QPushButton, QVBoxLayout)

class MainWindow(QWidget):

def __init__(self):
super().__init__()
self.initializeUI()

def initializeUI(self):
"""Настройте графический интерфейс приложения."""
self.setMinimumSize(200, 200)
self.setWindowTitle("Пример таблиц стилей")
self.show()

if __name__ == '__main__':
app = QApplication(sys.argv)
window = MainWindow()
sys.exit(app.exec())

Код в Листинге 6-4 размещен перед вызовом метода show() в Листинге 6-3.
Созданный ярлык использует комбинацию HTML и CSS для изменения своего
внешнего вида.

ГЛАВА 6 СТИЛИЗАЦИЯ ГРАФИЧЕСКИХ ИНТЕРФЕЙСОВ 155


Листинг 6-4. Настройка внешнего вида для виджета QLabel в initializeUI()
# style_sheet_ex.py
label = QLabel("<p align=center>Поставьте мне лайк!</p>")
label.setStyleSheet("""
background-color: skyblue;
color: white;
border-style: outset;
border-width: 3px;
border-radius: 5px;
font: bold 24px 'Times New Roman'""")

Текст этикетки располагается по центру с помощью HTML-атрибута align.


Для таблицы стилей фон установлен на skyblue, а цвет текста - white. Мы
можем задать различные стили границ, ширину и радиус углов с помощью
свойств CSS. Некоторые часто используемые стили границ включают
outset (внешняя), inset (внутренняя) и solid (сплошная). Наконец, можно задать стиль,
вес и размер шрифта. Таблицу типично используемых свойств можно найти в конце
главы в разделе "Справочник свойств CSS".
Попробуйте изменить различные значения пикселей и цветов и обратите внимание
на различия. Обратитесь к документации Qt Style Sheets за идеями о различных
свойствах, которыми вы можете манипулировать.

Настройка стилей для реагирования на


взаимодействия
Когда вы будете использовать общие настройки стилей для виджетов, вы заметите,
что они имеют свои собственные способы реагирования на взаимодействие с
пользователем. Однако, когда вы изменяете некоторые аспекты виджета с помощью
таблиц стилей, другие функции могут перестать работать должным образом. Во
многих случаях их также необходимо изменить. Одним из распространенных
примеров является обработка нажатия кнопки после редактирования стиля кнопки.
Давайте начнем с добавления после метки кнопки QPushButton, как в Листинге 6-5.

156 ГЛАВА 6 СТИЛИЗАЦИЯ ГРАФИЧЕСКИХ ИНТЕРФЕЙСОВ


Листинг 6-5. Настройка внешнего вида для виджета QPushButton в initializeUI()
# style_sheet_ex.py
like_button = QPushButton()
like_button.setStyleSheet("""
QPushButton {background-color: lightgrey;
padding: 5px;
border-style: inset;
border-width: 1px;
border-radius: 5px;
image: url(images/like_normal.png);
qproperty-iconSize: 20px 20px;}

QPushButton:pressed {background-color: grey;


padding: 5px;
border-style: outset;
border-width: 1px;
border-radius: 5px;
image: url(images/like_clicked.png);
qproperty-iconSize: 20px 20px;}""")
v_box = QVBoxLayout()
v_box.addWidget(label)
v_box.addWidget(like_button)
self.setLayout(v_box)

Мы хотим иметь возможность обрабатывать псевдосостояние, когда кнопка


нажата. В отличие от объекта QLabel этого GUI, для доступа к состоянию :pressed нам
нужно указать селектор, который является типом виджета, затронутым изменением
(здесь это QPushButton). Изменив обычный вид кнопки, а именно границы, кнопка
больше не будет отображать обратную связь при нажатии.
Несколько свойств, которые можно редактировать, являются общими для
многих виджетов, например, background-color, border и padding. Свойство padding
используется для добавления пространства вокруг текста или изображения внутри
виджета. Если вы хотите добавить дополнительное пространство за пределами
виджета, вы можете использовать свойство margin.
Для кнопки like_button также используется изображение, размер которого
настраивается с помощью свойства qproperty-iconSize. Свойство qproperty
используется для изменения определенных аспектов класса виджета. Простым
примером может быть геттер text() из QLabel. Если вы хотите использовать таблицы
стилей для задания текста метки, вы можете использовать следующий фрагмент
кода:
label.setStyleSheet("qproperty-text: 'example text'")

ГЛАВА 6 СТИЛИЗАЦИЯ ГРАФИЧЕСКИХ ИНТЕРФЕЙСОВ 157


Для состояния :pressed (нажата) используется более темный цвет фона, более
темное изображение и другой стиль границ, чтобы показать пользователю, что
кнопка нажата. Последним шагом является добавление виджетов в макет и установка
макета окна.
Список всех псевдосостояний можно найти в справочной таблице стилей Qt.
Давайте рассмотрим более эффективную альтернативу использованию встроенных
таблиц стилей в следующем разделе.

Объяснение использования встроенных


(embedded) таблиц стилей Qt
Встроенные таблицы стилей в CSS используются для определения стилей для
всего документа в одном месте, обычно в начале сценария. Мы можем следовать
аналогичной схеме при создании приложений PyQt. Это особенно полезно, когда у
вас есть несколько виджетов одного типа, которые имеют один и тот же стиль, что
позволяет задать все модификации за один раз. Например, следующий код установит
цвет фона для всех экземпляров QPushButton на красный:
app.setStyleSheet("QPushButton{background-color: #C92108}")
Обратите внимание, как изменение применяется к объекту QApplication, app. Для
примера GUI на Рисунке 6-4 мы рассмотрим, как использовать встроенные таблицы
стилей для применения изменений к определенным виджетам.

Рисунок 6-4. Графический интерфейс пользователя, демонстрирующий, как


применять стили к определенным виджетам
Окно содержит два виджета QPushButton - один с родным стилем, а другой с
измененным.

158 ГЛАВА 6 СТИЛИЗАЦИЯ ГРАФИЧЕСКИХ ИНТЕРФЕЙСОВ


Применение изменений к определенным
виджетам
Когда вы создаете объект в PyQt, например, виджет, вы можете дать ему имя с
помощью метода QObject setObjectName(). Это может быть полезно для поиска
конкретного дочернего объекта родительского виджета. При использовании таблиц
стилей это позволяет присвоить виджету ID Selector, или конкретное имя, для
идентификации определенного виджета.
В Листинге 6-6 показано, как использовать ID Selector для применения другого
стиля к указанной кнопке.
Листинг 6-6. Создание встроенной таблицы стилей
# style_sheet_ex2.py
# Импортируйте необходимые модули
import sys
from PyQt6.QtWidgets import (QApplication, QWidget, QLabel,
QPushButton, QVBoxLayout)

style_sheet = """
QPushButton#Warning_Button{
background-color: #C92108;
border-radius: 5px;
padding: 6px;
color: #FFFFFF
}
QPushButton#Warning_Button:pressed{
background-color: #F4B519;
}
"""

Окно состоит из QLabel и двух виджетов QPushButton. Чтобы выбрать конкретный


виджет, используйте ID Selector. В данном примере это #Warning_Button. Чтобы
обработать изменения при нажатии кнопки, добавьте псевдосостояние :pressed после
ID Selector. Эти изменения добавляются в переменную style_sheet.
В Листинге 6-7 показано, как настроить класс MainWindow, создать кнопки,
использовать setObjectName() для создания ID Selector и расположить виджеты в
макете.

ГЛАВА 6 СТИЛИЗАЦИЯ ГРАФИЧЕСКИХ ИНТЕРФЕЙСОВ 159


Листинг 6-7. Создание класса MainWindow и применение таблицы стилей
# style_sheet_ex2.py
class MainWindow(QWidget):

def __init__(self):
super().__init__()
self.initializeUI()

def initializeUI(self):
"""Настройте графический интерфейс приложения."""
self.setMinimumSize(230, 140)
self.setWindowTitle("Пример таблиц стилей 2")

label = QLabel("<p align=center>Нажмите кнопку.</p>")


normal_button = QPushButton("Нормальный")
warning_button = QPushButton("Внимание!")
# Установить идентификационный селектор
warning_button.setObjectName("Warning_Button")

v_box = QVBoxLayout()
v_box.addWidget(label)
v_box.addWidget(normal_button)
v_box.addWidget(warning_button)

self.setLayout(v_box)
self.show()

if __name__ == '__main__':
app = QApplication(sys.argv)
app.setStyleSheet(style_sheet) # Установить стиль приложения
window = MainWindow()
sys.exit(app.exec())

Последняя задача этой программы - применить таблицу стилей к объекту


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

160 ГЛАВА 6 СТИЛИЗАЦИЯ ГРАФИЧЕСКИХ ИНТЕРФЕЙСОВ


Организация виджетов с помощью контейнеров и
вкладок
Организация в графическом интерфейсе пользователя может быть достигнута
не только визуально, но и путем дальнейшего изучения новых инструментов для
структурирования виджетов. В Главе 5 вы видели, как использовать QWidget для
группировки виджетов. В этом разделе вы
• Узнаете, как создавать контейнеры, которые создают рамки вокруг
связанных виджетов
• Узнаете о радиокнопках, чтобы на практике увидеть, как можно создавать
и управлять отношениями при разработке графических интерфейсов
пользователя
• Изучите идею организации пользовательского интерфейса с помощью
интерфейсов с вкладками, позволяющих расположить больше содержимого
в графическом интерфейсе, не перегружая пользователя слишком большим
количеством визуальной информации за один раз.
Все это вы узнаете в процессе создания простого графического интерфейса
пользователя, показанного на Рисунке 6-5.

Рисунок 6-5. Графический интерфейс контактной формы. Вкладка "Сведения о


профиле" (слева) содержит два ярлыка и два виджета редактирования строки, а
также групповое поле с двумя радиокнопками. Вкладка "Фон" (справа) состоит из
группового поля с пятью радиокнопками.
В следующих нескольких разделах будут рассмотрены новые классы PyQt, которые
мы будем использовать для создания приложения на Рисунке 6-5.

ГЛАВА 6 СТИЛИЗАЦИЯ ГРАФИЧЕСКИХ ИНТЕРФЕЙСОВ 161


Виджет QRadioButton
Класс QRadioButton позволяет создавать кнопки опций, которые могут быть
включены, когда они отмечены, или выключены, когда они не отмечены. Радиокнопки
состоят из круглой кнопки и соответствующего ярлыка или значка и отлично
подходят для ситуаций, когда вам нужно предоставить пользователю несколько
вариантов выбора, но только один вариант может быть отмечен одновременно.
Когда пользователь выбирает новую радиокнопку, остальные радиокнопки снимают
флажки.
Для этого необходимо поместить несколько радиокнопок в родительский
виджет. Эти кнопки станут автоисключающими, то есть они автоматически станут
членами взаимоисключающей группы. Если одна радиокнопка будет отмечена
внутри родительского виджета, все остальные кнопки станут не отмеченными.
Эту функциональность можно изменить, установив значение метода QRadioButton
setAutoExclusive() на False.
Несколько эксклюзивных групп радиокнопок также можно поместить в один
родительский виджет, используя класс QButtonGroup для разделения и управления
различными группами. За информацией о QButtonGroup обратитесь к Главе 4.
Радиокнопки похожи на класс QCheckBox, когда издают сигналы. Радиокнопка
излучает сигнал переключения при включении или выключении и может быть
подключена к этому сигналу для запуска слота.

Класс QGroupBox
Контейнер QGroupBox представляет собой прямоугольную рамку, используемую
для группировки виджетов вместе. Групповой блок имеет рамку с заголовком в
верхней части. Заголовок также может быть проверяемым, так что дочерние виджеты
внутри группового блока могут быть включены или выключены при установке или
снятии флажка.
Объект группового блока может содержать любой виджет. Поскольку QGroupBox
автоматически не упорядочивает дочерние виджеты, вам также потребуется
применить менеджер компоновки.
Следующий блок кода является кратким примером использования QGroupBox:

# Заголовок для группового блока передается в качестве аргумента


effects_gb = QGroupBox("Эффекты")
# Создайте два объекта QRadioButton для размещения в групповом блоке
effect1_rb = QRadioButton("Зачеркивание")
effect2_rb = QRadioButton("Обводка")

# Создайте макет для группового блока


gb_h_box = QHBoxLayout()
gb_h_box.addWidget(effect1_rb)
162 ГЛАВА 6 СТИЛИЗАЦИЯ ГРАФИЧЕСКИХ ИНТЕРФЕЙСОВ
gb_h_box.addWidget(effect2_rb)

# Установите макет для группового блока


effects_gb.setLayout(gb_h_box)

Давайте посмотрим на последний класс для создания пользовательского


интерфейса с вкладками.

Класс QTabWidget
Иногда вам может понадобиться организовать связанную информацию на
отдельных страницах, чтобы не создавать беспорядочный графический интерфейс.
Класс QTabWidget предоставляет панель вкладок с областью под каждой вкладкой
(называемой страницей) для представления информации и виджетов, связанных с
каждой вкладкой. Одновременно отображается только одна страница, и пользователь
может просмотреть другую страницу, щелкнув на вкладке или используя ярлык (если
он установлен для вкладки).
Существует несколько различных способов взаимодействия и отслеживания
различных вкладок. Например, если пользователь переключается на другую вкладку,
индекс текущей вкладки может быть возвращен при подаче сигнала currentChanged. Вы
также можете вернуть индекс текущей страницы с помощью функции currentIndex()
или виджет текущей страницы с помощью функции currentWidget(). Вкладка также
может быть включена или отключена с помощью метода setTabEnabled().

Совет. Если вы хотите создать интерфейс с несколькими страницами, но без


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

Следующий пример создает простое приложение, включающее QRadioButton,


QGroupBox, QTabWidget и несколько других классов. Программа показывает, как
установить интерфейс с вкладками и как организовать другие виджеты на различных
страницах.

Объяснение использования контейнеров и вкладок


Для начала работы с этим приложением мы воспользуемся скриптом
basic_window.py. Начнем с импорта необходимых классов в Листинге 6-8, включая
QRadioButton, QTabWidget и QGroupBox из модуля QtWidgets. Затем настройте класс
MainWindow и инициализируйте его минимальный размер и заголовок.
ГЛАВА 6 СТИЛИЗАЦИЯ ГРАФИЧЕСКИХ ИНТЕРФЕЙСОВ 163
Листинг 6-8. Настройка главного окна для использования контейнеров и
виджетов с вкладками
# containers.py
# Импортируйте необходимые модули
import sys
from PyQt6.QtWidgets import (QApplication, QWidget, QLabel,
QRadioButton, QGroupBox, QLineEdit, QTabWidget,
QHBoxLayout, QVBoxLayout)

class MainWindow(QWidget):

def __init__(self):
super().__init__()
self.initializeUI()

def initializeUI(self):
"""Настройте графический интерфейс приложения."""
self.setMinimumSize(400, 300)
self.setWindowTitle("Пример контейнеров")

self.setUpMainWindow()
self.show()

if __name__ == '__main__':
app = QApplication(sys.argv)
window = MainWindow()
sys.exit(app.exec())

После этого шага нам нужно настроить виджет вкладок в setUpMainWindow().


Сначала необходимо создать экземпляр QTabWidget. Мы создадим объект, tab_bar, в
Листинге 6-9.
Листинг 6-9. Метод setUpMainWindow() для использования контейнеров и
виджетов с вкладками
# containers.py
def setUpMainWindow(self):
"""Создать и расположить виджеты в главном окне.
Настройте панель вкладок и различные виджеты вкладок."""
# Создайте панель вкладок и различные контейнеры страниц
tab_bar = QTabWidget(self)
self.prof_details_tab = QWidget()
self.background_tab = QWidget()

tab_bar.addTab(self.prof_details_tab, "Подробности профиля")

164 ГЛАВА 6 СТИЛИЗАЦИЯ ГРАФИЧЕСКИХ ИНТЕРФЕЙСОВ


tab_bar.addTab(self.background_tab, "Фон")

# Вызов методов для создания страниц


self.profileDetailsTab()
self.backgroundTab()

# Создаем макет для главного окна


main_h_box = QHBoxLayout()
main_h_box.addWidget(tab_bar)
self.setLayout(main_h_box)

Следующая задача - создать контейнер для каждой страницы. Вы можете


использовать QGroupBox или какой-либо другой класс контейнера. Для целей
данного графического интерфейса воспользуемся QWidget. Для этого проекта есть
две вкладки, prof_details_tab и background_tab. Вставьте эти две страницы в tab_bar с
помощью addTab(). Не забудьте также дать каждой вкладке соответствующую метку.
Нам понадобится создать два метода для создания различных страниц,
profileDetailsTab() и backgroundTab(), и вызвать их в setUpMainWindow(). Наконец,
расположите tab_bar в окне. В Листингах 6-10 и 6-11 будут установлены страницы.
Листинг 6-10. Код для страницы profileDetailsTab()
# containers.py
def profileDetailsTab(self):
"""Страница профиля позволяет пользователю ввести свое имя,
адрес и выбрать свой пол."""
# Устанавливаем метки и виджеты редактирования строк
name_label = QLabel("Имя")
name_edit = QLineEdit()
address_label = QLabel("Адрес")
address_edit = QLineEdit()

# Создайте радиокнопки и менеджер их расположения


male_rb = QRadioButton("Мужской")
female_rb = QRadioButton("Женский")

gender_h_box = QHBoxLayout()
gender_h_box.addWidget(male_rb)
gender_h_box.addWidget(female_rb)

# Создайте групповое поле для содержания радиокнопок


gender_gb = QGroupBox("Пол")
gender_gb.setLayout(gender_h_box)

# Добавьте все виджеты в макет страницы с подробной информацией о профиле


tab_v_box = QVBoxLayout()
ГЛАВА 6 СТИЛИЗАЦИЯ ГРАФИЧЕСКИХ ИНТЕРФЕЙСОВ 165
tab_v_box.addWidget(name_label)
tab_v_box.addWidget(name_edit)
tab_v_box.addStretch()
tab_v_box.addWidget(address_label)
tab_v_box.addWidget(address_edit)
tab_v_box.addStretch()
tab_v_box.addWidget(gender_gb)

# Установите макет для вкладки подробностей профиля


self.prof_details_tab.setLayout(tab_v_box)

Первая страница включает несколько виджетов для сбора общей информации о


пользователе. Вы можете обратиться к Рисунку 6-5, чтобы увидеть, как выглядит
каждая страница. Виджеты меток и редактирования строк настроены как обычно.
Для объектов QRadioButton, которые спрашивают пол пользователя, они добавляются
в QGroupBox, gender_gb, чтобы сделать их взаимоисключающими. Последним шагом
является расположение дочерних виджетов в макете и вызов метода setLayout() для
prof_details_tab для завершения создания страницы.
Метод backgroundTab() в Листинге 6-11 использует цикл for для инстанцирования
каждой кнопки QRadioButton и добавления их в макет страницы.
Листинг 6-11. Код для страницы backgroundTab()
# containers.py
def backgroundTab(self):
"""Фоновая страница позволяет пользователям выбрать
свой образовательныйфон."""
# Макет для education_gb
ed_v_box = QVBoxLayout()

# Создайте и добавьте радиокнопки в ed_v_box


education_list = ["Диплом о среднем образовании",
"Степень младшего специалиста", "Степень бакалавра",
"Степень магистра", "Докторская степень или выше"]
for ed in education_list:
self.education_rb = QRadioButton(ed)
ed_v_box.addWidget(self.education_rb)

# Установите групповое поле для размещения радиокнопок


self.education_gb = QGroupBox(
"Наивысший уровень образования")
self.education_gb.setLayout(ed_v_box)

# Создание и установка для фоновой вкладки


tab_v_box = QVBoxLayout()
tab_v_box.addWidget(self.education_gb)
166 ГЛАВА 6 СТИЛИЗАЦИЯ ГРАФИЧЕСКИХ ИНТЕРФЕЙСОВ
# Установите макет для фоновой вкладки
self.background_tab.setLayout(tab_v_box)

Имея базовое понимание таблиц стилей и несколько новых классов PyQt, пришло
время применить полученные знания для создания нового проекта GUI.

Проект 6.1 - графический интерфейс заказа еды


Приложения служб доставки еды повсюду. На вашем телефоне, в интернете и
даже в киосках, когда вы заходите в сами рестораны. Они упрощают процесс заказа и
одновременно дают пользователю ощущение контроля над своим выбором, предлагая
нам самим выбирать блюда и продукты, прокручивая список упорядоченных
категорий.
Такие графические интерфейсы, возможно, должны содержать сотни различных
товаров, объединенных в несколько групп. Вместо того чтобы просто вываливать
все товары в интерфейс и позволять пользователю тратить свое время на сортировку
товаров, товары обычно размещаются в категориях, часто разграниченных
вкладками. Эти вкладки содержат названия продуктов, которые можно найти на
соответствующих страницах, например, "Замороженные продукты" или "Фрукты/
овощи".
GUI в этом проекте позволяет пользователю разместить заказ на пиццу. Он
закладывает основу для приложения заказа еды, используя виджеты вкладок для
организации элементов на отдельных страницах. Проект также показывает, как
можно использовать таблицы стилей, чтобы придать графическому интерфейсу,
созданному с помощью PyQt, более эстетичный вид. Интерфейс с вкладками показан
на Рисунке 6-6.

ГЛАВА 6 СТИЛИЗАЦИЯ ГРАФИЧЕСКИХ ИНТЕРФЕЙСОВ 167


Рисунок 6-6. Графический интерфейс заказа блюд. Графический интерфейс
содержит две вкладки, Пицца (вверху) и Крылышки (внизу), чтобы разделить типы
блюд, которые клиент может видеть в одно время. Варианты, которые можно
выбрать, представляют собой виджеты QRadioButton и разделяются с помощью
виджетов QGroupBox. Главное окно имеет красный фон, а каждая вкладка - фон
цвета загара. Эти цвета и другие стили создаются с помощью таблицы стилей

168 ГЛАВА 6 СТИЛИЗАЦИЯ ГРАФИЧЕСКИХ ИНТЕРФЕЙСОВ


Разработка графического интерфейса пользователя
для заказа еды
Это приложение состоит из двух основных вкладок, как показано на Рисунке 6-7,
но можно легко добавить и другие. Каждая вкладка состоит из QWidget, который
действует как контейнер для всех остальных виджетов. Первая вкладка, "Пицца"
(Pizza), содержит изображение и текст, чтобы донести до пользователя назначение
вкладки. За ней следуют два виджета QGroupBox, каждый из которых состоит из
нескольких виджетов QRadioButton. В то время как радиокнопки в групповом блоке
"Корочка" (CRUST) являются взаимоисключающими, радиокнопки в групповом
блоке "Топпинги" (TOPPINGS) не являются таковыми. Это сделано для того, чтобы
пользователь мог выбрать несколько начинок за один раз.

Рисунок 6-7. Дизайн графического интерфейса пользователя для заказа еды


Вторая вкладка, "Крылышки" (Wings), устроена аналогичным образом, причем
радиокнопки "Вкус" (Flavor) являются взаимоисключающими.
Внизу каждой страницы находится кнопка "Добавить к заказу" (Add to Order)
QPushButton, которая обновляет заказ пользователя в виджете в правой части окна.

ГЛАВА 6 СТИЛИЗАЦИЯ ГРАФИЧЕСКИХ ИНТЕРФЕЙСОВ 169


Пояснения к графическому интерфейсу заказа еды
Этот графический интерфейс не содержит строки меню, поэтому мы снова
используем сценарий basic_window.py в качестве основы приложения и класс
MainWindow в Листинге 6-12.
Листинг 6-12. Настройка главного окна для графического интерфейса заказа еды
# food_order.py
# Импортируем необходимые модули
import sys
from PyQt6.QtWidgets import (QApplication, QWidget, QLabel,
QPushButton, QRadioButton, QButtonGroup, QTabWidget,
QGroupBox, QVBoxLayout, QHBoxLayout, QGridLayout)
from PyQt6.QtCore import Qt
from PyQt6.QtGui import QPixmap

class MainWindow(QWidget):

def __init__(self):
super().__init__()
self.initializeUI()

def initializeUI(self):
"""Настройте графический интерфейс приложения."""
self.setMinimumSize(700, 700)
self.setWindowTitle("6.1 - GUI заказа еды")

self.setUpMainWindow()
self.show()

if __name__ == '__main__':
app = QApplication(sys.argv)
app.setStyleSheet(style_sheet)
window = MainWindow()
sys.exit(app.exec())

Существует довольно много импортов для этого графического интерфейса, но


все они уже обсуждались в этой или предыдущих главах. Обратите внимание, как
встроенная таблица стилей для графического интерфейса заказа еды, который мы
создадим в следующем разделе, импортируется с app.
setStyleSheet(style_sheet).

170 ГЛАВА 6 СТИЛИЗАЦИЯ ГРАФИЧЕСКИХ ИНТЕРФЕЙСОВ


Создание таблицы стилей
Если таблица стилей не применена к графическому интерфейсу заказа еды, то для
оформления приложения будут использоваться собственные настройки системы.
На Рисунке 6-8 показано, как это выглядит на macOS.

Рисунок 6-8. Графический интерфейс пользователя для заказа еды до применения


таблицы стилей
В начале программы вам нужно будет создать экземпляр style_sheet, в котором
будут храниться все различные спецификации стилей для различных виджетов. Для
начала мы можем задать общий красный цвет фона, #C92108, который используется
для главного окна в Листинге 6-13.

ГЛАВА 6 СТИЛИЗАЦИЯ ГРАФИЧЕСКИХ ИНТЕРФЕЙСОВ 171


Листинг 6-13. Настройка таблицы стилей для графического интерфейса заказа
еды, часть 1
# food_order.py
# Настройка таблицы стилей для всего графического интерфейса пользователя
style_sheet = """
QWidget{
background-color: #C92108;
}
QWidget#Tabs{
background-color: #FCEBCD;
border-radius: 4px
}
QWidget#ImageBorder{
background-color: #FCF9F3;
border-width: 2px;
border-style: solid;
border-radius: 4px;
border-color: #FABB4C
}
QWidget#Side{
background-color: #EFD096;
border-radius: 4px
}

Но если QWidget имеет заданный ID Selector, например #Tabs, то он получит фон


цвета коричневый , #FCEBCD, и закругленные углы. Виджеты с такими свойствами
используются для стилизации страниц для каждой вкладки.
Экземпляры QWidget с селектором ID #ImageBorder создаются с небелым фоном
для размещения меток, которые отображают информацию для пользователя о
каждой странице.

Последний селектор QWidget с ID Selector #Side определяет настройки для боковой


панели.
В Листинге 6-14 создайте общий стиль для виджетов QLabel, а затем стиль для
меток, которые появляются в качестве заголовков на каждой странице. Обратите
внимание, что можно указать значение padding для всех четырех сторон виджета с
помощью padding, или для отдельных сторон с помощью padding-left, padding-top и
так далее.

172 ГЛАВА 6 СТИЛИЗАЦИЯ ГРАФИЧЕСКИХ ИНТЕРФЕЙСОВ


Листинг 6-14. Настройка таблицы стилей для графического интерфейса заказа
еды, часть 2
# food_order.py
QLabel{
background-color: #EFD096;
border-width: 2px;
border-style: solid;
border-radius: 4px;
border-color: #EFD096
}
QLabel#Header{
background-color: #EFD096;
border-width: 2px;
border-style: solid;
border-radius: 4px;
border-color: #EFD096;
padding-left: 10px;
color: #961A07
}
QLabel#ImageInfo{
background-color: #FCF9F3;
border-radius: 4px;
}
QGroupBox{
background-color: #FCEBCD;
color: #961A07
}
QRadioButton{
background-color: #FCF9F3
}
QPushButton{
background-color: #C92108;
border-radius: 4px;
padding: 6px;
color: #FFFFFF
}
QPushButton:pressed{
background-color: #C86354;
border-radius: 4px;
padding: 6px;
color: #DFD8D7
}
"""

ГЛАВА 6 СТИЛИЗАЦИЯ ГРАФИЧЕСКИХ ИНТЕРФЕЙСОВ 173


Селекторы QLabel с #ImageInfo предназначены для информационных изображений
и текста на каждой странице. В завершение таблицы стилей есть стили для объектов
QGroupBox, QRadioButton и QPushButton.
Теперь мы можем приступить к созданию MainWindow методом setUpMainWindow().

Построение главного окна (Main Window)


Чтобы начать работу, создайте структуру вкладок и макет главного окна в
Листинге 6-15. Создайте экземпляры объектов QTabWidget и QWidget, которые будут
использоваться для страниц вкладок. Две вкладки - pizza_tab, для отображения
выбора вариантов приготовления собственной пиццы, и wings_tab, для отображения
выбора вкуса крылышек.
Листинг 6-15. Настройка главного окна для графического интерфейса заказа еды,
часть 1
# food_order.py
def setUpMainWindow(self):
"""Создайте и расположите виджеты в главном окне."""
# Создайте панель вкладок, различные вкладки и задайте имена объектов
self.tab_bar = QTabWidget()
self.pizza_tab = QWidget()
self.pizza_tab.setObjectName("Tabs")
self.wings_tab = QWidget()
self.wings_tab.setObjectName("Tabs")

self.tab_bar.addTab(self.pizza_tab, "Пицца")
self.tab_bar.addTab(self.wings_tab, "Крылышки")

# Вызов методов, содержащих виджеты для каждой вкладки


self.pizzaTab()
self.wingsTab()

Некоторым виджетам в этом графическом интерфейсе присваивается ID Selector


с помощью метода setObjectName(). Например, виджету pizza_tab присвоен ID Selec-
tor #Вкладки. Это имя используется в таблице стилей приложения, чтобы отличать
этот виджет от других объектов QWidget с другим стилем.
В Листинге 6-16 показано построение боковой панели. Виджет side_widget
используется для обратной связи с пользователем о его выборе и может быть виден,
даже если пользователь переключает вкладки.

174 ГЛАВА 6 СТИЛИЗАЦИЯ ГРАФИЧЕСКИХ ИНТЕРФЕЙСОВ


Листинг 6-16. Настройка главного окна для графического интерфейса заказа еды,
часть 2
# food_order.py
# Создание боковой панели в главном окне
self.side_widget = QWidget()
self.side_widget.setObjectName("Tabs")

order_label = QLabel("ВАШ ЗАКАЗ")


order_label.setObjectName("Header")

items_box = QWidget()
items_box.setObjectName("Side")")
pizza_label = QLabel("Тип пиццы: ")
self.display_pizza_label = QLabel("")
toppings_label = QLabel("Начинки: ")
self.display_toppings_label = QLabel("")
extra_label = QLabel("Дополнительно: ")
self.display_wings_label = QLabel("")

# Установите расположение сетки для объектов в боковом виджете


items_grid = QGridLayout()
items_grid.addWidget(pizza_label, 0, 0,
Qt.AlignmentFlag.AlignRight)
items_grid.addWidget(self.display_pizza_label, 0, 1)
items_grid.addWidget(toppings_label, 1, 0,
Qt.AlignmentFlag.AlignRight)
items_grid.addWidget(self.display_toppings_label,
1, 1)
items_grid.addWidget(extra_label, 2, 0,
Qt.AlignmentFlag.AlignRight)
items_grid.addWidget(self.display_wings_label, 2, 1)
items_box.setLayout(items_grid)

Ярлыки, предназначенные для отображения выбора пользователя, изначально


отображают пустую строку. Все дочерние элементы для side_widget располагаются
во вложенном макете и добавляются к основному QHBoxLayout в Листинге 6-17.

ГЛАВА 6 СТИЛИЗАЦИЯ ГРАФИЧЕСКИХ ИНТЕРФЕЙСОВ 175


Листинг 6-17. Настройка главного окна для графического интерфейса заказа еды,
часть 3
# food_order.py
# Установите основной макет для бокового виджета
side_v_box = QVBoxLayout()
side_v_box.addWidget(order_label)
side_v_box.addWidget(items_box)
side_v_box.addStretch()
self.side_widget.setLayout(side_v_box)

# Добавьте виджеты в главное окно и установите макет


main_h_box = QHBoxLayout()
main_h_box.addWidget(self.tab_bar, 1)
main_h_box.addWidget(self.side_widget)
self.setLayout(main_h_box)

Метод pizzaTab(), построенный в Листингах 6-18 и 6-19, создает и упорядочивает


дочерние виджеты для первой вкладки, pizza_tab. Верхняя часть первой страницы
предоставляет пользователям информацию о назначении вкладки с помощью
изображений и текста. Также инстанцируются радиокнопки, отображающие
варианты коржей пиццы.
Листинг 6-18. Код для страницы pizzaTab(), часть 1
# food_order.py
def pizzaTab(self):
"""Создаем вкладку пиццы. Позволяет пользователю выбрать
тип пиццы и начинку с помощью радиокнопок."""
# Настройте виджеты и макеты для отображения информации
# пользователю о странице
tab_pizza_label = QLabel("СОЗДАЙТЕ СВОЮ СОБСТВЕННУЮ ПИЦЦУ")
tab_pizza_label.setObjectName("Header")
description_box = QWidget()
description_box.setObjectName("ImageBorder")
pizza_image_path = "images/pizza.png"
pizza_image = self.loadImage(pizza_image_path)
pizza_desc = QLabel()
pizza_desc.setObjectName("ImageInfo")
pizza_desc.setText(
"""<p>Создаем для вас пиццу на заказ. Начните с
вашего любимого коржа и добавьте любые начинки, а также
идеальное количество сыра и соуса.</p>""")
pizza_desc.setWordWrap(True)
pizza_desc.setContentsMargins(10, 10, 10, 10)

176 ГЛАВА 6 СТИЛИЗАЦИЯ ГРАФИЧЕСКИХ ИНТЕРФЕЙСОВ


pizza_h_box = QHBoxLayout()
pizza_h_box.addWidget(pizza_image)
pizza_h_box.addWidget(pizza_desc, 1)

description_box.setLayout(pizza_h_box)

# Создайте групповой блок, который будет содержать варианты корочки


crust_gbox = QGroupBox()
crust_gbox.setTitle("ВЫБОР ВАШЕГО КОРЖА")

# Групповое поле используется для группировки виджетов вместе,


# в то время как группа кнопок используется для получения информации
# о том, какая радиокнопка отмечена
self.crust_group = QButtonGroup()
gb_v_box = QVBoxLayout()
crust_list = ["Ручной", "Плоский", "Фаршированный"]
# Создайте радиокнопки для различных коржей и
# добавляем в макет
for cr in crust_list:
crust_rb = QRadioButton(cr)
gb_v_box.addWidget(crust_rb)
self.crust_group.addButton(crust_rb)

crust_gbox.setLayout(gb_v_box)

Обязательно следите за комментариями в Листинге 6-18, чтобы понять, как


структурирована страница. Виджеты QRadioButton объединены в группы с помощью
групповых блоков. Это позволяет каждой группе иметь заголовок. Класс QGroupBox
действительно обеспечивает эксклюзивность для радиокнопок, но чтобы получить
функциональность, позволяющую узнать, какие кнопки отмечены и вернуть
их текстовые значения, объекты QRadioButton также группируются с помощью
QButtonGroup. Более подробную информацию о QButtonGroup см. в Главе 4.
Код в Листинге 6-19 устанавливает объекты QRadioButton, которые отображают
варианты выбора начинки пиццы.

ГЛАВА 6 СТИЛИЗАЦИЯ ГРАФИЧЕСКИХ ИНТЕРФЕЙСОВ 177


Листинг 6-19. Код для страницы pizzaTab(), часть 2
# food_order.py
# Создайте групповой бокс, который будет содержать варианты начинок
toppings_gbox = QGroupBox()
toppings_gbox.setTitle("ВЫБЕРИТЕ НАЧИНКУ")

# Настройте группу кнопок для радиокнопок выбора начинок


self.toppings_group = QButtonGroup()
gb_v_box = QVBoxLayout()

toppings_list = ["Пепперони", "Колбаса", "Бекон",


"Канадский бекон", "Говядина", "Ананас",
"Оливки", "Томат", "Зеленый перец",
"Грибы", "Лук", "Шпинат", "Сыр"]
# Создайте радиокнопки для различных начинок и
# добавить в макет
for top in toppings_list:
toppings_rb = QRadioButton(top)
gb_v_box.addWidget(toppings_rb)
self.toppings_group.addButton(toppings_rb)
self.toppings_group.setExclusive(False)

toppings_gbox.setLayout(gb_v_box)

# Создайте кнопку для добавления информации


# в боковой виджет при нажатии
add_to_order_button1 = QPushButton("Добавить к заказу")
add_to_order_button1.clicked.connect(
self.displayPizzaInOrder)

# Создайте макет для вкладки пиццы (страница 1)


page1_v_box = QVBoxLayout()
page1_v_box.addWidget(tab_pizza_label)
page1_v_box.addWidget(description_box)
page1_v_box.addWidget(crust_gbox)
page1_v_box.addWidget(toppings_gbox)
page1_v_box.addStretch()
page1_v_box.addWidget(add_to_order_button1,
alignment=Qt.AlignmentFlag.AlignRight)

self.pizza_tab.setLayout(page1_v_box)

178 ГЛАВА 6 СТИЛИЗАЦИЯ ГРАФИЧЕСКИХ ИНТЕРФЕЙСОВ


Хотя в Листинге 6-18 в группе crust_group можно выбрать только одну радиокнопку,
пользователям необходимо иметь возможность выбрать более одного топпинга. Это
достигается с помощью метода setExclusive(), который устанавливает эксклюзивность
группы toppings_group на False.
Метод wingsTab() в Листингах 6-20 и 6-21 настроен аналогично pizzaTab().
Листинг 6-20. Код для страницы wingsTab(), часть 1
# food_order.py
def wingsTab(self):
"""Создайте вкладку "Крылышки". Позволяет пользователю выбрать
тип пиццы и начинку с помощью радиокнопок."""
# Настройте виджеты и макеты для отображения информации
# пользователю о странице
tab_wings_label = QLabel("ПРОБУЙТЕ НАШИ УДИВИТЕЛЬНЫЕ КРЫЛЫШКИ")
tab_wings_label.setObjectName("Header")
description_box = QWidget()
description_box.setObjectName("ImageBorder")
wings_image_path = "images/wings.png"
wings_image = self.loadImage(wings_image_path)
wings_desc = QLabel()
wings_desc.setObjectName("ImageInfo")
wings_desc.setText(
"""<p>6 кусочков белого мяса с насыщенным вкусом
курицы, которые заставят вас вернуться за добавкой.</p>""")
wings_desc.setWordWrap(True)
wings_desc.setContentsMargins(10, 10, 10, 10)
wings_h_box = QHBoxLayout()
wings_h_box.addWidget(wings_image)
wings_h_box.addWidget(wings_desc, 1)
description_box.setLayout(wings_h_box)

Виджеты для выбора крыльев организованы и добавлены в wings_tab в Листинге


6-21.
Листинг 6-21. Код для страницы wingsTab(), часть 2
# food_order.py
wings_gbox = QGroupBox()
wings_gbox.setTitle("ВЫБЕРИТЕ СВОЙ ВКУС")
self.wings_group = QButtonGroup()
gb_v_box = QVBoxLayout()
flavors_list = [
"Баффало", "Кисло-сладкий", "Терияки", "Барбекю"]

# Создайте радиокнопки для различных вкусов и


# добавить в макет
ГЛАВА 6 СТИЛИЗАЦИЯ ГРАФИЧЕСКИХ ИНТЕРФЕЙСОВ 179
for fl in flavors_list:
flavor_rb = QRadioButton(fl)
gb_v_box.addWidget(flavor_rb)
self.wings_group.addButton(flavor_rb)

wings_gbox.setLayout(gb_v_box)

# Создайте кнопку для добавления информации в боковой виджет.


# при нажатии
add_to_order_button2 = QPushButton("Добавить к заказу")
add_to_order_button2.clicked.connect(
self.displayWingsInOrder)

# создайте макет для вкладки "Крылышки" (страница 2)


page2_v_box = QVBoxLayout()
page2_v_box.addWidget(tab_wings_label)
page2_v_box.addWidget(description_box)
page2_v_box.addWidget(wings_gbox)
page2_v_box.addWidget(add_to_order_button2,
alignment=Qt.AlignmentFlag.AlignRight)
page2_v_box.addStretch()

self.wings_tab.setLayout(page2_v_box)

Если пользователь нажимает кнопку add_to_order_button на любой из страниц


(1 или 2), текст выбранных радиокнопок на этой странице отображается в side_widget
с помощью одного из двух методов в Листинге 6-22.

Листинг 6-22. Код для обновления боковой панели в графическом интерфейсе


заказа еды
# food_order.py
def displayPizzaInOrder(self):
"""Собираем текст с радиокнопок, которые отмечены
на странице пиццы. Отобразите текст в боковом виджете."""
if self.crust_group.checkedButton():
text = self.crust_group.checkedButton().text()
self.display_pizza_label.setText(text)
toppings = self.collectToppingsInList()
toppings_str = '\n'.join(toppings)
self.display_toppings_label.setText(toppings_str)
self.update()

180 ГЛАВА 6 СТИЛИЗАЦИЯ ГРАФИЧЕСКИХ ИНТЕРФЕЙСОВ


def displayWingsInOrder(self):
"""Собираем текст из радиокнопок, которые
отмечены на странице крыльев. Отобразите текст в боковом
виджете."""
if self.wings_group.checkedButton():
text = self.wings_group.checkedButton().text() +\
" Крылышки"
self.display_wings_label.setText(text)
self.update()

Для displayPizzaInOrder() мы проверяем, выбрана ли какая-либо из радиокнопок в


группе QButtonGroup crust_group. Если да, то текст выбранной кнопки собирается и
отображается в display_pizza_label с помощью setText(). Для метки display_toppings_label
все выбранные радиокнопки с топпингами собираются и возвращаются с помощью
функции collectToppingsInList() в Листинге 6-23. Затем топпинги отображаются
на этикетке. Метод update() используется для обеспечения соответствующего
обновления текста.
Листинг 6-23. Код для сбора информации о выбранных радиокнопках в
графическом интерфейсе заказа еды
# food_order.py
def collectToppingsInList(self):
"""Создать список всех отмеченных радиокнопок."""
toppings_list = [button.text() for i, button in \
enumerate(self.toppings_group.buttons()) if \
button.isChecked()]
return toppings_list

Последний метод в Листинге 6-24, loadImage(), загружает и масштабирует


изображения пиццы и крыльев, используемые на двух страницах.
Листинг 6-24. Код для загрузки изображений в графическом интерфейсе заказа
еды
# food_order.py
def loadImage(self, img_path):
"""Загружаем и масштабируем изображения."""
aspect = Qt.AspectRatioMode.KeepAspectRatioByExpanding
transform = Qt.TransformationMode.SmoothTransformation
try:
with open(img_path):
image = QLabel(self)
image.setObjectName("ImageInfo")
pixmap = QPixmap(img_path)
image.setPixmap(pixmap.scaled(image.size(), aspect, transform))
return image
ГЛАВА 6 СТИЛИЗАЦИЯ ГРАФИЧЕСКИХ ИНТЕРФЕЙСОВ 181
except FileNotFoundError as error:
print(f"Изображение не найдено. Ошибка: {error}")

Довольно длинный проект, графический интерфейс заказа еды демонстрирует,


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

Справочник свойств CSS


В Таблице 6-2 перечислены свойства CSS, встречающиеся в этой главе, а также
некоторые часто используемые свойства, которые могут понадобиться вам в ваших
ранних проектах.
Таблица 6-2. Часто используемые свойства CSS в PyQt

Объект Свойства
background-color Устанавливает цвет фона для виджета
border Сокращение для установки цвета, стиля и ширины границы QLabel
{border: 2px groove grey}
border-color Задает цвет границы для всех сторон виджета
border-style Определяет узор для рисования границы виджета. Некоторые из
них: пунктир (dashed), точка (dotted), желобок (groove), вставка
(inset), край (outset) и сплошная (solid).
border-width Задает ширину границы для всех сторон виджета (в пикселях).
border-radius Устанавливает радиус углов виджета (в пикселях).
color Задает цвет, используемый для текста
font Задает вес, стиль, размер и семейство шрифта QLabel
{font: bold italic small 'Times'}
image Задает изображение, используемое в виджете.
Обязательно укажите url(path_to_file)
margin Задает дополнительное пространство вокруг виджета (в пикселях)
padding Задает дополнительное пространство внутри виджета (в пикселях)

182 ГЛАВА 6 СТИЛИЗАЦИЯ ГРАФИЧЕСКИХ ИНТЕРФЕЙСОВ


Резюме
В этой главе мы рассмотрели, как использовать таблицы стилей Qt для изменения
внешнего вида виджетов, чтобы они лучше соответствовали назначению и внешнему
виду приложения. Мы также рассмотрели, как можно использовать HTML для
изменения внешнего вида текста.
Преимущества использования таблиц стилей включают более легкое обновление
кода, большую последовательность в дизайне, более простой способ форматирования
внешнего вида виджетов, повышение удобства использования и меньшие трудности
для разработчика в управлении цветами, макетами и другими эстетическими
аспектами дизайна пользовательского интерфейса.
В Главе 7 будет рассмотрена очень важная тема - обработка событий.

ГЛАВА 6 СТИЛИЗАЦИЯ ГРАФИЧЕСКИХ ИНТЕРФЕЙСОВ 183


ГЛАВА 7

Обработка событий в PyQt


Поскольку графические интерфейсы должны выполнять задачи, виджеты,
окна и другие аспекты приложения должны уметь реагировать на происходящие
события. Независимо от того, вызваны ли они пользователем или базовой системой,
события, а возможно и данные, должны быть доставлены в соответствующие места
и обработаны соответствующим образом.
В этой главе вы
• Узнаете больше о сигналах, слотах и обработке событий
• Узнаете, как модифицировать обработчики событий нажатия клавиш, ввода
и ввода мышью
• Изучите, как создавать пользовательские сигналы с помощью pyqtSignal.
Эта глава посвящена обработке событий и изменению поведения встроенных
функций PyQt.

Обработка событий в PyQt


События в Qt - это объекты, созданные на основе класса QEvent. Объекты
событий описывают различные типы взаимодействий, которые могут происходить
в графическом интерфейсе пользователя в результате того, что происходит либо
по вине пользователя, либо в результате какой-то системной активности вне
приложения. Эти события начинаются, как только запускается основной цикл
событий приложения.
Большинство событий, будь то нажатие клавиши, щелчок мыши, изменение
размера окна, перетаскивание виджета или данных, имеют свой собственный подкласс
QEvent, который генерирует объект события и передает его соответствующему
QObject, вызывая метод event(), который в свою очередь обрабатывается подходящим
обработчиком события. (Напомним, что QWidget наследует QObject.) Ответ от события
используется для определения того, было ли оно принято или проигнорировано.
Более подробную информацию об обработке событий можно найти на
https://doc.qt.io/qt-6/eventsandfilters.html.
Давайте рассмотрим сигналы, слоты и обработчики событий в следующих
подразделах и подумаем об их назначении и различиях.

184 ГЛАВА 7 ОБРАБОТКА СОБЫТИЙ В PYQT


Использование сигналов и слотов
Концепция сигналов и слотов в PyQt была кратко представлена в Главе 3. Виджеты
в PyQt используют сигналы и слоты для взаимодействия между объектами. Как и
события, сигналы могут генерироваться действиями пользователя или внутренней
системой. Слоты - это методы, которые выполняются в ответ на сигнал. Например,
когда нажимается кнопка QPushButton, она издает сигнал clicked. Этот сигнал может
быть подключен к встроенному слоту PyQt, например, close(), чтобы пользователь мог
выйти из приложения, или к пользовательскому слоту, который обычно представляет
собой функцию Python. Сигналы также полезны, поскольку их можно использовать
для отправки дополнительных данных в слот и предоставления дополнительной
информации о событии.
Сигнал clicked является одним из многих предопределенных сигналов Qt. Тип
сигналов, которые можно испускать, различается в зависимости от класса виджета.
PyQt передает события виджетам путем вызова определенных, предопределенных
функций обработки событий. Они могут варьироваться от функций, связанных с
операциями с окном, таких как show() или close(), до появления GUI с помощью
setStyleSheet(), до событий нажатия и отпускания мыши и т.д.
Посмотрите на www.riverbankcomputing.com/static/Docs/PyQt6/signals_slots.html
для получения дополнительной информации о сигналах и слотах в PyQt6.

Использование обработчиков событий для


обработки событий
Обработчики событий - это функции, которые реагируют на событие. В то время
как подкласс QEvent создается для доставки события, соответствующий метод QWidget
будет фактически обрабатывать событие. Если вы помните из Главы 3, обработчик
события closeEvent() использовался для закрытия окон. Класс, создающий объект
события закрытия, - QCloseEvent.

Примечание. Вы не всегда можете справиться со всеми функциями


обработчика события, который вы изменяете. В этом случае можно
использовать оператор if-else. В условии if укажите, как реагировать на
событие, а в условии else вызовите реализацию базового класса. Так, для
QCloseEvent вы включите super().closeEvent(event) в предложение else.
Эта часть позаботится о любом поведении по умолчанию, которое вы не
реализовали или могли пропустить.

ГЛАВА 7 ОБРАБОТКА СОБЫТИЙ В PYQT 185


Разница между сигналами, слотами и
обработчиками событий
Хотя между ними есть некоторое дублирование, сигналы и слоты обычно
используются для связи между различными виджетами и другими классами PyQt.
События генерируются внешней активностью и передаются через цикл событий
QApplication.
Еще одно важное различие заключается в том, что вы получаете уведомление
о появлении сигнала и предпринимаете соответствующие действия. События
необходимо обрабатывать каждый раз, когда они возникают.
Наконец, мы можем использовать сигналы с виджетами для улучшения их
возможностей, но при изменении функциональности виджета вам придется заново
реализовать обработчики событий.
Во многих случаях для выполнения задач вы будете использовать сигналы, слоты
и обработчики событий вместе.
В следующем разделе показан простой пример того, как переделать функцию
keyPressEvent().

Обработка ключевых событий


Когда клавиши нажимаются или отпускаются, создается событие QKeyEvent.
Клавишные события посылаются виджету, который в данный момент имеет фокус
клавиатуры. Затем мы можем реализовать следующие обработчики ключевых
событий QWidget для обработки этого события:
• keyPressEvent() - обрабатывает событие при нажатии клавиши
• keyReleaseEvent() - обрабатывает событие, когда клавиша отпущена.
На Рисунке 7-1 показан графический интерфейс пользователя, который мы будем
кодировать и который демонстрирует, как модифицировать keyPressEvent().

Рисунок 7-1. Окно, которое закрывается, когда пользователь нажимает клавишу


Escape
186 ГЛАВА 7 ОБРАБОТКА СОБЫТИЙ В PYQT
Некоторые названия клавиш включают Key_Escape, Key_Return,
Key_Up, Key_Down, Key_Space, Key_0, Key_1 и так далее. Полный список
клавиатурных кодов перечисления Qt.Key можно найти на сайте
https://doc.qt.io/qt-6/qt.html#Key-enum.

Объяснение обработки ключевых событий


В Листинге 7-1 мы создадим простой класс MainWindow, который наследует
QMainWindow. Импорт для этого приложения также достаточно прост. Класс
MainWindow наследует QMainWindow, поэтому нам не нужно импортировать никаких
менеджеров компоновки для единственного объекта QLabel.
Листинг 7-1. Код, демонстрирующий, как изменять ключевые обработчики
событий
# key_events.py
# Импортируйте необходимые модули
import sys
from PyQt6.QtWidgets import QApplication, QMainWindow, QLabel
from PyQt6.QtCore import Qt

class MainWindow(QMainWindow):

def __init__(self):
super().__init__()
self.initializeUI()

def initializeUI(self):
"""Настройте графический интерфейс приложения."""
self.setGeometry(100, 100, 300, 200)
self.setWindowTitle("Пример обработки событий")
info_label = QLabel(
"""<p align='center'>Нажмите <b>ESC</b> клавишу
чтобы закрыть окно.</p>""")
self.setCentralWidget(info_label)
self.show()

def keyPressEvent(self, event):


"""Реализуйте событие нажатия клавиши для
закрытия окна."""
if event.key() == Qt.Key.Key_Escape:
print("Приложение закрыто.")
self.close()

ГЛАВА 7 ОБРАБОТКА СОБЫТИЙ В PYQT 187


if __name__ == '__main__':
app = QApplication(sys.argv)
window = MainWindow()
sys.exit(app.exec())

Каждый раз, когда пользователь нажимает клавишу на клавиатуре, он посылает


сигнал компьютеру. Если вы хотите наделить определенные клавиши способностями,
то вам нужно использовать функцию keyPressEvent(). Функция keyPressEvent()
проверяет наличие событий, которые в данном случае являются сигналами,
посылаемыми клавишами. Если нажатой клавишей является клавиша Escape,
то приложение вызывает функцию close() для выхода из приложения. Доступ к
различным клавишам можно получить с помощью Qt.Key, и вы можете использовать
эти различные клавиши для выполнения любого количества действий.

Обработка событий мыши


События мыши обрабатываются классом QMouseEvent. Для событий мыши нам
нужно иметь возможность узнать, когда кнопка мыши нажата, отпущена, дважды
щелкнута, а также когда мышь перемещается во время щелчка. Существует также
класс событий QEnterEvent, который полезен для выяснения того, вошла ли мышь в
окно или определенный виджет или покинула его. События Enter также полезны для
сбора информации о положении курсора мыши.
Обработчики событий мыши QWidget, которые мы будем использовать, включают
следующие:
• mousePressEvent() - Обрабатывает события, когда кнопка мыши нажата.
• mouseReleaseEvent() - Обрабатывает события, когда кнопка мыши
отпускается.
• mouseMoveEvent() - обрабатывает события, когда кнопка мыши нажата
и перемещена. Включите отслеживание мыши, чтобы включить
события перемещения, даже если кнопка мыши не нажата, с помощью
QWidget.setMouseTracking(True).
• mouseDoubleClickEvent() - Обрабатывает события при двойном нажатии
кнопки мыши.
Для событий ввода мы будем использовать следующие обработчики событий:
• enterEvent() - обрабатывает события, когда курсор мыши входит в виджет.
• leaveEvent() - обрабатывает, когда курсор мыши покидает виджет.
Для графического интерфейса пользователя, показанного на Рисунке 7-2, при
первом запуске программы в левом окне отображается только изображение без какой-
либо текстовой информации. Когда мышь попадает в главное окно, изображение в
окне меняется на то, что показано на правом снимке экрана на Рисунке 7-2. Если
пользователь нажмет или отпустит кнопку мыши, метка в виджете обновится, чтобы
сообщить, какая кнопка мыши, левая или правая, была использована. Двойной

188 ГЛАВА 7 ОБРАБОТКА СОБЫТИЙ В PYQT


щелчок в окне изменит изображение. Наконец, при нажатии и перемещении мыши
на экране отображаются координаты x и y положения мыши.

Рисунок 7-2. Изображения и информация в окне меняются в зависимости от


событий мыши. Изображения с сайта https://pixabay.com
Обязательно загрузите папку images из репозитория GitHub для этого примера.

Объяснение обработки событий мыши


Для этого примера мы можем использовать сценарий basic_window.py из Главы 1.
В Листинге 7-2 настроим главное окно и метод setUpMainWindow(). Окно состоит из
трех объектов QLabel, один из которых предназначен для отображения изображений,
а два других - для передачи пользователю информации о событиях мыши.

ГЛАВА 7 ОБРАБОТКА СОБЫТИЙ В PYQT 189


Листинг 7-2. Код для установки главного окна в примере модификации
обработчиков событий мыши
# mouse_events.py
# Импортируйте необходимые модули
import sys
from PyQt6.QtWidgets import (QApplication, QWidget, QLabel,
QVBoxLayout)
from PyQt6.QtCore import Qt
from PyQt6.QtGui import QPixmap

class MainWindow(QWidget):

def __init__(self):
super().__init__()
self.initializeUI()

def initializeUI(self):
"""Настройте графический интерфейс приложения."""
self.setMinimumSize(400, 300)
self.setWindowTitle("Пример обработки событий")
self.setUpMainWindow()
self.show()

def setUpMainWindow(self):
self.image_label = QLabel()
self.image_label.setPixmap(QPixmap("images/back.png"))
self.image_label.setAlignment(
Qt.AlignmentFlag.AlignCenter)

self.info_label = QLabel("")
self.info_label.setAlignment(
Qt.AlignmentFlag.AlignCenter)

self.pos_label = QLabel("")
self.pos_label.setAlignment(
Qt.AlignmentFlag.AlignCenter)

main_h_box = QVBoxLayout()
main_h_box.addStretch()
main_h_box.addWidget(self.image_label)
main_h_box.addStretch()
main_h_box.addWidget(self.info_label)
main_h_box.addWidget(self.pos_label)
self.setLayout(main_h_box)

190 ГЛАВА 7 ОБРАБОТКА СОБЫТИЙ В PYQT


if __name__ == '__main__':
app = QApplication(sys.argv)
window = MainWindow()
sys.exit(app.exec())

Метод addStretch() используется до и после image_label в main_h_box для того,


чтобы изображения оставались по центру окна.
Всякий раз, когда курсор мыши входит в окно, image_label будет отображать
другое изображение. Чтобы изменить изображение обратно, мы можем использовать
функцию leaveEvent() для проверки того, когда мышь покинет виджет. Это сделано в
Листинге 7-3.
Листинг 7-3. Код для обработчиков событий enterEvent() и leaveEvent()
# mouse_events.py
def enterEvent(self, event):
self.image_label.setPixmap(
QPixmap("images/front.png"))

def leaveEvent(self, event):


self.image_label.setPixmap(QPixmap("images/back.png"))

В PyQt6 QMouseEvent наследует несколько методов от QPointerEvent, которые могут


предоставить больше информации о том, какие кнопки мыши были нажаты или где
мышь находится в окне или на экране компьютера. К ним относятся следующие:
• button() - Возвращает, какая кнопка вызвала событие.
• buttons() - возвращает состояние кнопок, давая доступ к тому, какая
комбинация кнопок вызвала событие с помощью оператора OR.
• globalPosition() - Возвращает координаты точки события на экране
компьютера.
• position() - Возвращает текущие координаты точки мыши относительно
виджета, вызвавшего событие. Возвращаемые значения относятся к точкам
внутри окна или виджета.
И globalPosition(), и position() имеют методы x() и y() для сбора горизонтальных или
вертикальных значений. В Листинге 7-4 мы используем несколько из этих методов.
Листинг 7-4. Код, показывающий, как модифицировать обработчики событий
мыши
# mouse_events.py
def mouseMoveEvent(self, event):
"""Печать положения мыши при нажатии и перемещении."""
if self.underMouse():
self.pos_label.setText(
f"""<p>X:{event.position().x()},
Y:{event.position().y()}</p>""")

ГЛАВА 7 ОБРАБОТКА СОБЫТИЙ В PYQT 191


def mousePressEvent(self, event):
"""Определите, какая кнопка была нажата."""
if event.button() == Qt.MouseButton.LeftButton:
self.info_label.setText("<b>Нажатие левой кнопки</b>")
if event.button() == Qt.MouseButton.RightButton:
self.info_label.setText("<b>Нажатие правой кнопки</b>")

def mouseReleaseEvent(self, event):


"""Определите, какая кнопка была отпущена."""
if event.button() == Qt.MouseButton.LeftButton:
self.info_label.setText(
"<b>Левая кнопка отпущена</b>")
if event.button() == Qt.MouseButton.RightButton:
self.info_label.setText(
"<b>Правая кнопка отпущена</b>")

def mouseDoubleClickEvent(self, event):


self.image_label.setPixmap(QPixmap("images/boom.png"))

Значения x и y мыши отображаются в метке pos_label с помощью функции


position() в mouseMoveEvent(). Для mousePressEvent() мы просто обновим текст
info_label в зависимости от того, какая кнопка мыши была нажата. Функция
mouseReleaseEvent() будет делать что-то подобное, но когда кнопка будет отпущена. В
случае mouseDoubleClickEvent(), pixmap обновляется, чтобы выглядеть, как показано
на Рисунке 7-3. Перемещение мыши из окна приводит к вызову функции leaveEvent(),
которая снова показывает изображения на Рисунке 7-2.

Рисунок 7-3. Изображение на экране изменяется при двойном щелчке мыши

192 ГЛАВА 7 ОБРАБОТКА СОБЫТИЙ В PYQT


После того, как мы увидели, как изменять обработчики событий, самое время
узнать, как создавать свои собственные сигналы.

Создание пользовательских сигналов


В предыдущих главах мы рассмотрели некоторые из предопределенных
сигналов и слотов PyQt. Для многих из этих приложений мы также видели, как
создавать собственные слоты для обработки сигналов, испускаемых виджетами.
Пользовательские слоты были просто функциями или методами Python.
Теперь давайте посмотрим, как можно создавать пользовательские сигналы с
помощью pyqtSignal для изменения таблицы стилей виджета. С помощью pyqtSignal
можно определить новые сигналы для класса. Подобно предопределенным сигналам,
в качестве аргументов создаваемого pyqtSignal можно передавать такие типы
информации, как строки, целые числа, словари или списки Python.
В графическом интерфейсе на Рисунке 7-4 пользователь может изменить цвет фона
нижнего виджета QLabel, нажимая на клавиатуре клавиши со стрелками вверх или
вниз. При нажатии клавиши будет выдаваться закрытый сигнал, не принимающий
аргументов.

Рисунок 7-4. Цвет метки будет меняться при нажатии стрелок вверх и вниз

Пояснение к созданию пользовательских сигналов


Этот пример создает простой графический интерфейс с объектом QLabel в
качестве центрального виджета главного окна. Фабрика pyqtSignal и классы QObject
импортируются из модуля QtCore. Модуль QtCore и класс QObject предоставляют
механику для сигналов и слотов.
Перед созданием класса MainWindow в Листинге 7-5 мы сначала создадим класс
SendSignal, который наследует QObject.

ГЛАВА 7 ОБРАБОТКА СОБЫТИЙ В PYQT 193


Листинг 7-5. Создание пользовательского сигнала для изменения цвета фона
виджета QLabel
# custom_signal.py
# Импортируйте необходимые модули
import sys
from PyQt6.QtWidgets import (QApplication, QMainWindow,
QWidget, QLabel, QVBoxLayout)
from PyQt6.QtCore import Qt, pyqtSignal, QObject

class SendSignal(QObject):

"""Определите сигнал, change_style, который не принимает никаких


аргументов."""
change_style = pyqtSignal()

class MainWindow(QMainWindow):

def __init__(self):
super().__init__()
self.initializeUI()

def initializeUI(self):
"""Настройте графический интерфейс приложения."""
self.setGeometry(100, 100, 300, 200)
self.setWindowTitle("Создание пользовательских сигналов")
self.setUpMainWindow()
self.show()

if __name__ == '__main__':
app = QApplication(sys.argv)
window = MainWindow()
sys.exit(app.exec())

Класс SendSignal создает новый сигнал под названием change_style из фабрики


pyqtSignal. Чтобы использовать этот сигнал, нам сначала нужно создать экземпляр
SendSignal, называемый просто sig, как показано в Листинге 7-6. Чтобы использовать
созданный вами пользовательский сигнал, вызовите экземпляр change_style из
sig и используйте connect() для подключения сигнала к слоту, в данном случае
changeBackground().

194 ГЛАВА 7 ОБРАБОТКА СОБЫТИЙ В PYQT


Листинг 7-6. Код для метода setUpMainWindow()
# custom_signal.py
def setUpMainWindow(self):
"""Создайте и расположите виджеты в главном окне."""
self.index = 0 # Индекс элементов в списке
self.direction = ""
# Создайте экземпляр класса SendSignal и
# подключите сигнал change_style к слоту
self.sig = SendSignal()
self.sig.change_style.connect(self.changeBackground)
header_label = QLabel(
"""<p align='center'>Нажмите <b>вверх</b> и
<b>вниз</b> стрелки.</p>""")
self.colors_list = ["red", "orange", "yellow",
"green", "blue", "purple"]
self.label = QLabel()
self.label.setStyleSheet(f"""background-color:
{self.colors_list[self.index]}""")
main_v_box = QVBoxLayout()
main_v_box.addWidget(header_label)
main_v_box.addWidget(self.label)
container = QWidget()
container.setLayout(main_v_box)
self.setCentralWidget(container)

Остальная часть setUpMainWindow() инстанцирует два виджета QLabel и создает


список цветов, которые будут использоваться меткой для задания фона в таблице
стилей.
Этот сигнал будет выдаваться каждый раз, когда пользователь нажимает клавишу
со стрелкой вверх или стрелку вниз в функции KeyPressEvent().
Когда пользователь нажимает клавишу Key_Up, направление устанавливается
равным "вверх", а сигнал change_style подается сигнал change_style. Чтобы издать
пользовательский сигнал, вам нужно вызвать emit() в том месте вашего приложения,
где сигнал должен быть вызван. Пример для sig показан в следующая строке:
self.sig.change_style.emit()
Этот сигнал подключен к слоту changeBackground(), который обновляет цвет
метки, проверяя индекс списка colors_list и обновляя цвет с помощью функции
setStyleSheet() в Листинге 7-7.

ГЛАВА 7 ОБРАБОТКА СОБЫТИЙ В PYQT 195


Листинг 7-7. Код для обработки keyPressEvent() и слот для изменения цвета фона
# custom_signal.py
def keyPressEvent(self, event):
"""Реализуем обработку события нажатия клавиши."""
if event.key() == Qt.Key.Key_Up:
self.direction = "up"
self.sig.change_style.emit()
elif event.key() == Qt.Key.Key_Down:
self.direction = "down"
self.sig.change_style.emit()

def changeBackground(self):
"""Изменение фона виджета этикетки, когда
подается сигнал KeyPressEvent."""
if self.direction == "up" and \
self.index < len(self.colors_list) - 1:
self.index = self.index + 1
self.label.setStyleSheet(f"""background-color:
{self.colors_list[self.index]}""")
elif self.direction == "down" and self.index > 0:
self.index = self.index - 1
self.label.setStyleSheet(f"""background-color:
{self.colors_list[self.index]}""")

Аналогично работает при нажатии клавиши "вниз". Помните, что пользовательские


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

Резюме
Обработка событий является важным компонентом разработки графического
интерфейса пользователя. В PyQt это можно сделать либо с помощью сигналов и
слотов, либо с помощью классов событий и соответствующих им обработчиков
событий. В любом случае, вы часто можете столкнуться с расширением возможностей
класса виджета путем создания пользовательских сигналов с помощью pyqtSignal или
повторной реализации базовой функциональности, предоставляемой различными
методами обработчиков событий Qt.
В этой главе мы рассмотрели обе эти концепции, изменив поведение обработчиков
событий нажатия клавиш и мыши и создав пользовательский сигнал для изменения
внешнего вида метки.
В следующей главе мы рассмотрим использование приложения Qt Designer для
создания приложений PyQt и упрощение процесса расположения виджетов в окне
графического интерфейса.
196 ГЛАВА 7 ОБРАБОТКА СОБЫТИЙ В PYQT
ГЛАВА 8

Создание графических
интерфейсов с
помощью Qt Designer
Хотя создание графических пользовательских интерфейсов программным
способом дает вам больше контроля над процессом проектирования, некоторые
задачи разработки могут потребовать более быстрого подхода. К счастью, Qt
предоставляет отличный интерфейс для расположения виджетов и проектирования
основных окон, виджетов или диалогов. Графический инструмент разработки,
Qt Designer, наполнен виджетами и другими инструментами для создания
графических интерфейсов. С помощью интерфейса перетаскивания приложения вы
можете создавать и настраивать свои собственные приложения Qt или PyQt.
Виджеты и другие приложения, которые вы создаете с помощью Qt Designer,
могут взаимодействовать с другими программами Qt с помощью сигналов и слотов,
что упрощает назначение поведения виджетов. Это означает, что больше ресурсов
можно направить на кодирование функциональности и меньше на компоновку и
дизайн.
В этой главе вы
• Установите приложение Qt Designer
• Рассмотрите различные компоненты, составляющие интерфейс Qt Designer
• Проследите за созданием приложения в Qt Designer, попутно изучая, как
применять макеты, редактировать свойства объектов, подключать сигналы и
слоты и генерировать код Python
• Узнайте о новых классах PyQt, таких как класс QFrame для группировки
виджетов.

Совет. Для получения ссылок или дополнительной помощи, выходящей за


рамки этой главы, ознакомьтесь с документацией Qt для Qt Designer на сайте
https://doc.qt.io/qt-6/qtdesignermanual.html.

ГЛАВА 8 СОЗДАНИЕ ГРАФИЧЕСКИХ ИНТЕРФЕЙСОВ С ПОМОЩЬЮ QT DESIGNER 197


Эта глава служит введением в Qt Designer, предоставляя вам основы, необходимые
для начала работы с приложением.

Начало работы с Qt Designer


В этом разделе мы сначала рассмотрим два метода установки Qt Designer на
ваш компьютер. После этого мы обсудим расположение графического интерфейса
Qt Designer.

Установка Qt Designer
На данный момент существует два подхода к установке последней версии
Qt Designer, и они могут различаться в зависимости от того, сколько памяти вы
готовы использовать.
Первый вариант - загрузить последнюю версию Qt Creator для Qt 6 с сайта
www.qt.io/download. Qt Designer поставляется в комплекте с Qt Creator, который
является официальной C++ IDE от Qt. Имейте в виду, что этот метод работает для
macOS, Windows и Linux, но также означает, что вам придется установить всю IDE
Qt Creator.
На веб-странице загрузки Qt вам нужно найти опцию загрузки Qt для создателей
с открытым исходным кодом. Оттуда прокрутите страницу в самый низ и найдите
кнопку с надписью Download the Qt Online Installer. Когда загрузка завершится,
вам нужно будет открыть программу установки Qt. Вам нужно будет создать
учетную запись Qt, а затем следовать подсказкам для установки Qt Creator. Следует
отметить, что если вы выберете выборочную установку, вы сможете вручную
выбрать необходимое программное обеспечение и сэкономить немного памяти.
После завершения установки выполните поиск на своем компьютере, чтобы найти
Qt Designer.
Другой способ установки Qt Designer - через PySide6. Сначала откройте окно
оболочки и введите следующую команду для установки PySide6:
$ pip3 install PySide6
В Windows используйте pip вместо pip3.
Затем выполните поиск Qt Designer на вашем компьютере и откройте приложение.
После открытия Qt Designer вы увидите графический пользовательский интерфейс
для создания собственных графических интерфейсов, как показано на рисунке 8-1.

198 ГЛАВА 8 СОЗДАНИЕ ГРАФИЧЕСКИХ ИНТЕРФЕЙСОВ С ПОМОЩЬЮ QT DESIGNER


Рисунок 8-1. Интерфейс Qt Designer

Совет. Вы можете изменить внешний вид окна Qt Designer. В строке меню


найдите пункт меню Preferences... и в появившемся диалоговом окне
найдите User Interface Mode. Вы можете выбрать два варианта внешнего
вида: Многоуровневые окна (Multi Top-Level Windows) или Докированные
окна (Docked Windows). Многоуровневая компоновка отлично подходит для
свободного расположения всех виджетов на больших экранах.

Прежде чем вы создадите свое первое приложение, давайте познакомимся с


различными меню, инструментами и режимами, которые отображаются в главном
окне на Рисунке 8-1.

Изучение пользовательского интерфейса Qt Designer


Когда вы впервые откроете Qt Designer, вы заметите диалог в центре окна
с заголовком Новая форма (New Form). Это диалоговое окно показано на
Рисунке 8-2. Отсюда вы можете выбрать шаблон для создания главного окна,
виджета или различных видов диалоговых окон. Вы также можете выбрать, какие
виды виджетов добавить в макет вашего проекта. После выбора шаблона и размера
приложения появится пустое окно, также известное как форма, которое можно
изменить.

ГЛАВА 8 СОЗДАНИЕ ГРАФИЧЕСКИХ ИНТЕРФЕЙСОВ С ПОМОЩЬЮ QT DESIGNER 199


Рисунок 8-2. Диалоговое окно Новая форма для выбора типа создаваемой формы
В верхней части главного окна на Рисунке 8-1 вы заметите строку меню Qt Designer
и панель инструментов для управления и редактирования вашего графического
интерфейса. В левой части главного окна находится док-виджет Widget Box,
показанный на Рисунке 8-3, который предоставляет упорядоченный список макетов
и виджетов, которые можно перетаскивать в нужные места вашего GUI. Другие
возможности для работы с формой можно получить, щелкнув правой кнопкой мыши
и открыв различные контекстные меню.
Еще одним очень полезным виджетом док-станции является редактор свойств,
показанный на Рисунке 8-4. Свойства окон, виджетов и макетов, такие как имя
объекта, ограничения по размеру, подсказки состояния и многое другое, могут быть
изменены с помощью редактора свойств. Каждый виджет, который вы добавляете
на форму, имеет свой собственный набор свойств, а также свойства, которые
виджет наследует от других классов. Чтобы выбрать конкретный виджет, вы можете
щелкнуть на объекте в форме или на имени виджета в док-виджетах Инспектора
объектов.
Инспектор объектов на Рисунке 8-5 позволяет вам просмотреть все объекты,
которые используются в данный момент, а также их иерархическое расположение.
Вы можете видеть, как MainWindow находится в списке первым, за ним следует
centralwidget и все его виджеты. Если у вашей формы есть меню или панель
инструментов, то они также будут перечислены в Инспекторе объектов вместе с
соответствующими действиями.

200 ГЛАВА 8 СОЗДАНИЕ ГРАФИЧЕСКИХ ИНТЕРФЕЙСОВ С ПОМОЩЬЮ QT DESIGNER


Примечание. Основной макет вашего графического интерфейса не
отображается в инспекторе объектов. Значок нарушенного макета (красный
круг с косой чертой) отображается на центральном виджете или на
контейнерах, если им не был назначен макет.

Рисунок 8-3. Док-виджет Widget Box для выбора макетов и виджетов

ГЛАВА 8 СОЗДАНИЕ ГРАФИЧЕСКИХ ИНТЕРФЕЙСОВ С ПОМОЩЬЮ QT DESIGNER 201


Рисунок 8-4. Виджет док-станции Property Editor для установки атрибутов
виджетов

202 ГЛАВА 8 СОЗДАНИЕ ГРАФИЧЕСКИХ ИНТЕРФЕЙСОВ С ПОМОЩЬЮ QT DESIGNER


Рисунок 8-5. Инспектор объектов отображает объекты виджетов, макетов и меню
В Qt Designer также можно создавать, редактировать и удалять сигналы и слоты
между объектами с помощью редактора сигналов/слотов. Вы должны знать, что хотя
вы можете подключать сигналы и слоты, вы не всегда сможете полностью настроить
свои виджеты, и иногда вам придется выполнять это самостоятельно в коде. Редактор
сигналов/слотов показан на рисунке 8-6. Qt Designer также предоставляет режим
редактирования для подключения виджетов.

Рисунок 8-6. Редактор сигналов/слотов для подключения сигналов и слотов


объектов
Пунктам меню, подменю или панели инструментов назначаются команды с
помощью действий. Этим действиям можно присвоить клавишу быстрого доступа,
сделать их проверяемыми и т. д. Редактор действий, показанный на Рисунке 8-7,
открывает доступ к работе с действиями. Более подробную информацию о назначении
действий см. в Главе 5.

ГЛАВА 8 СОЗДАНИЕ ГРАФИЧЕСКИХ ИНТЕРФЕЙСОВ С ПОМОЩЬЮ QT DESIGNER 203


Рисунок 8-7. Редактор действий используется для управления действиями
пунктов меню
Наконец, есть Браузер ресурсов (Resource Browser), который позволяет вам
указывать и управлять ресурсами, которые необходимо использовать в вашем
приложении. Эти ресурсы могут включать изображения и иконки. Виджет док-
станции Браузер ресурсов показан на рисунке 8-8.

Рисунок 8-8. Браузер ресурсов для работы с такими ресурсами, как изображения
и значки
Если вам нужно добавить ресурсы, сначала необходимо создать новый файл
ресурсов. Для этого нажмите на карандаш в левом верхнем углу док-виджета Браузер
ресурсов. Откроется диалог редактирования ресурсов, подобный показанному на
Рисунке 8-9.

204 ГЛАВА 8 СОЗДАНИЕ ГРАФИЧЕСКИХ ИНТЕРФЕЙСОВ С ПОМОЩЬЮ QT DESIGNER


Рисунок 8-9. Диалоговое окно редактирования ресурсов
Далее нажмите на кнопку Create New Resource, перейдите в нужную директорию и
введите имя файла ресурсов. Файл будет сохранен с расширением .qrc, что означает
Qt Resource Collection и содержит список всех ресурсов, используемых в вашей
программе. Отсюда создайте префикс для управления типами ресурсов и начните
добавлять файлы, такие как изображения и пиктограммы. Когда вы закончите,
нажмите кнопку OK, и файлы будут добавлены в Браузер ресурсов.

Примечание. Поддержка ресурсов и файлов .qrc в PyQt6 отличается от PyQt5.


Для доступа к ресурсам, возможно, придется использовать другие классы
PyQt, такие как QFile или QDir, или использовать путь к файлу ресурса.

Режимы редактирования в Qt Designer


В Qt Designer есть четыре различных режима редактирования, доступ к которым
можно получить либо в меню Edit, либо из панели инструментов Qt Designer.
Посмотрите на Рисунок 8-10, чтобы помочь вам найти виджеты на панели
инструментов.

1. Редактировать виджеты - виджеты можно перетаскивать на форму, применять


макеты и редактировать объекты как на форме, так и в редакторе свойств. Это
режим по умолчанию.
2. Редактировать сигналы/слоты - подключение сигналов и слотов для виджетов
и макетов. Чтобы создать соединения, щелкните объект и перетащите курсор
к объекту, который будет принимать сигнал. Объекты, которые могут быть со-
единены, будут выделены при наведении на них курсора мыши. Чтобы создать
соединение, отпустите кнопку мыши, когда линия со стрелкой соединит два
ГЛАВА 8 СОЗДАНИЕ ГРАФИЧЕСКИХ ИНТЕРФЕЙСОВ С ПОМОЩЬЮ QT DESIGNER 205
объекта. Затем настройте сигналы и слоты. Используйте вместе с док-видже-
том Signal/Slot Editor для редактирования соединений.
3. Edit Buddies - соединяет виджеты QLabel с виджетами ввода, такими как
QLineEdit или QTextEdit. Виджет ввода становится "приятелем" объекта QLabel.
Когда пользователь вводит клавишу быстрого доступа к ярлыку, фокус пере-
мещается на виджет ввода.
4. Порядок вкладок редактирования - установка порядка, в котором виджеты
получают фокус при нажатии клавиши табуляции. Это позволяет пользовате-
лю перемещаться по различным виджетам, улучшая удобство использования
приложения.

Рисунок 8-10. Режимы редактирования Qt Designer (выделены красным цветом).


(Слева направо) Редактирование виджетов, Редактирование сигналов/слотов,
Редактирование друзей, Редактирование порядка вкладок.

Создание приложения в Qt Designer


Когда вы создаете окна и виджеты вашего графического интерфейса, вы,
вероятно, будете продолжать вносить небольшие изменения в ваше приложение до
его завершения. К счастью, есть несколько шагов, которые можно выполнить, чтобы
упростить процесс создания.
1. Выберите форму - В диалоговом окне Новая форма (Рисунок 8-2) выберите
один из доступных шаблонов, Главное окно, Виджет или тип Диалога.
Вы также можете добавить и предварительно просмотреть виджеты для
включения в ваш графический интерфейс.
2. Размещение объектов на форме - Используйте механику перетаскивания
Qt Designer для размещения виджетов на форме. Затем назначьте макеты для
контейнеров и главного окна.
3. Редактирование свойств объектов - Щелкните объекты на форме и
отредактируйте их свойства в док-виджете Property Editor.
4. Соединить сигналы и слоты - Используйте режим редактирования сигналов/
слотов, чтобы соединить сигналы со слотами.
5. Предварительный просмотр графического интерфейса - Просмотрите
форму, прежде чем сохранить ее как файл пользовательского интерфейса с
расширением .ui.
6. Создание и редактирование кода Python - Используйте компилятор pyuic для
преобразования файла UI в читаемый и редактируемый код Python.
В следующем проекте будут рассмотрены эти шаги в дополнение ко многим
основным концепциям создания графических интерфейсов с помощью Qt Designer.

206 ГЛАВА 8 СОЗДАНИЕ ГРАФИЧЕСКИХ ИНТЕРФЕЙСОВ С ПОМОЩЬЮ QT DESIGNER


Проект 8.1 - графический интерфейс клавиатуры
GUI на Рисунке 8-11 должен быть знакомым - клавиатура.

Рисунок 8-11. Графический интерфейс клавиатуры


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

Пояснения к графическому интерфейсу клавиатуры


Приложение клавиатуры состоит из двух файлов Python: keypad_gui.py и
keypad_main.py. Keypad_gui.py содержит класс Python, сгенерированный из файла
пользовательского интерфейса, собранного в Qt Designer. Чтобы использовать этот
код, нам нужно создать настраиваемый класс в отдельном файле, keypad_main.py, для
импорта и настройки GUI.
Графический интерфейс клавиатуры состоит из четырех виджетов QLineEdit
для ввода только числовых значений, 12 виджетов QPushButton и одной QLabel для
отображения информации об использовании интерфейса. Кнопка со звездочкой
позволяет пользователю очистить текущий ввод, а кнопка с хэшем предназначена
для подтверждения введенного пользователем четырехзначного числа.
Мы начнем с создания окна в Qt Designer перед обсуждением кода.

ГЛАВА 8 СОЗДАНИЕ ГРАФИЧЕСКИХ ИНТЕРФЕЙСОВ С ПОМОЩЬЮ QT DESIGNER 207


Выбор формы
Для начала откройте Qt Designer. Выберите шаблон Widget в диалоговом окне
New Form. Мы будем использовать размер экрана по умолчанию. Выберите Создать.
Откроется пустая форма QWidget с сеткой точек внутри интерфейса Qt Designer, как
на Рисунке 8-1 (хотя на этом снимке экрана показана форма QMainWindow).

Размещение объектов на форме


Вы могли бы начать с настройки определенных характеристик формы, таких как
размер окна или цвет фона. Вместо этого давайте сначала добавим все необходимые
для проекта виджеты, перетаскивая их в главное окно из диалогового окна Widget
Box в левой части окна.
Найдите виджет QLabel (в диалоге он называется Label) и перетащите его на
форму. Затем перетащите на форму два контейнера QFrame (называемые Frame),
как показано на Рисунке 8-12. Вы можете изменить размер фреймов, щелкнув на
них и переместив края фрейма. Затем перетащите четыре виджета ввода QLineEdit
(называемые Line Edit) и расположите их в верхнем контейнере QFrame. Они будут
перекрываться, но это будет исправлено, когда вы примените макеты к фреймам
и главному окну. Когда объект будет перетащен поверх контейнера, в котором его
можно разместить, контейнер будет подсвечен, чтобы указать, что можно опустить
виджет внутрь. Кроме того, поместите 12 виджетов QPushButton (называемых Push
Button) в нижний фрейм.

Рисунок 8-12. Форма с меткой и двумя фреймами (слева) и с добавленными


виджетами редактирования строки и кнопками (справа)
Прежде чем двигаться дальше, давайте уделим немного времени изучению
контейнера QFrame, поскольку это очень полезный элемент в разработке графического
интерфейса.

208 ГЛАВА 8 СОЗДАНИЕ ГРАФИЧЕСКИХ ИНТЕРФЕЙСОВ С ПОМОЩЬЮ QT DESIGNER


Класс QFrame
Класс QFrame используется в качестве контейнера для группировки и окружения
виджетов, а также в качестве вставки в GUI-приложениях. Вы также можете
применить стиль рамки к экземпляру QFrame, чтобы визуально отделить его от
соседних виджетов. Следующий фрагмент кода показывает пример создания объекта
фрейма в главном окне, изменения его свойств и добавления виджета.

# Создать виджет для размещения во фрейме


button = QPushButton("Enter")

grid = QGridLayout()
grid.addWidget(button, 0, 0)

#Создание фрейма и установка его параметров


frame = QFrame() # Create a QFrame object
size_policy = QSizePolicy(
QSizePolicy.Policy.Expanding,
QSizePolicy.Policy.Preferred)
frame.setSizePolicy(size_policy)
frame.setFrameShape(QFrame.Shape.Box)
frame.setFrameShadow(QFrame.Shadow.Raised)
frame.setLineWidth(3)
frame.setMidLineWidth(5)

# Установка макета для объекта QFrame


frame.setLayout(grid)
self.setCentralWidget(frame)

С помощью метода setSizePolicy() мы можем определить, как фрейм должен


изменять размер. Объект фрейма может иметь несколько различных стилей
фреймов, включая Box, Panel, StyledPanel или NoFrame. Стиль рамки можно настроить
с помощью методов setFrameShadow(), setLineWidth() и setMidLineWidth(). Различные
типы тени включают в себя Plain, Raised и Sunken.
Для практики попробуйте создать простое окно из предыдущего кода.

Применение макетов (Layouts) в Qt Designer


Следующим шагом будет добавление макетов ко всем контейнерам и главному
окну. Это важный шаг, который обеспечивает правильное размещение и изменение
размеров элементов. Макеты можно добавлять как с панели инструментов, так и из
контекстного меню. Можно добавлять дополнительные виджеты к существующим
макетам после их установки.
ГЛАВА 8 СОЗДАНИЕ ГРАФИЧЕСКИХ ИНТЕРФЕЙСОВ С ПОМОЩЬЮ QT DESIGNER 209
Поскольку Qt Designer использует интерфейс перетаскивания, вам нужно
только разместить объекты на форме рядом с тем местом, где вы хотите их видеть,
а затем выбрать один из четырех макетов - QGridLayout, QHBoxLayout, QVBoxLayout
или QFormLayout - из диалога окна виджетов, и Qt Designer позаботится об их
расположении. Для получения дополнительной информации о типах макетов в PyQt,
обратитесь к Главе 4.
Щелкните правой кнопкой мыши на самой верхней рамке, чтобы открыть
контекстное меню (показано на Рисунке 8-13). Прокрутите вниз до последней опции,
Lay out, и выберите Lay Out Horizontally. Сделайте то же самое для нижнего фрейма,
но на этот раз выберите "Разложить по сетке" (Lay Out in a Grid).

Рисунок 8-13. Открытие контекстного меню для выбора макетов для контейнеров
и окон
Макет верхнего уровня формы можно установить, щелкнув правой кнопкой мыши
на самой форме в главном окне и найдя макет, который вы хотите использовать. Для
GUI клавиатуры щелкните правой кнопкой мыши и выберите Lay Out Vertically.
Ваш графический интерфейс должен выглядеть, как показано на Рисунке 8-14. Если
виджеты выровнены неправильно, вы также можете открыть контекстное меню,
выбрать Break Layout и переставить виджеты. Опция Simplify Grid Layout также
может помочь вам расположить элементы в макете сетки.

210 ГЛАВА 8 СОЗДАНИЕ ГРАФИЧЕСКИХ ИНТЕРФЕЙСОВ С ПОМОЩЬЮ QT DESIGNER


Рисунок 8-14. Графический интерфейс клавиатуры с макетами

Редактирование свойств объектов


После подготовки макетов следует приступить к редактированию свойств
объектов. Этот шаг также может быть выполнен ранее, когда вы размещаете объекты
на форме.
Редактор свойств показан на Рисунке 8-4. Он состоит из двух колонок: Свойство
и Значение. Свойства организованы по классам Qt.
Чтобы получить доступ и внести изменения в определенные контейнеры,
виджеты, макеты или даже главное окно, вы можете щелкнуть на них на форме или в
Инспекторе объектов. Если свойство редактируется в редакторе свойств, его можно
найти по следующей схеме:
Qt Класс (колонка Свойство) > Имя свойства > (подменю, если есть) > колонка
Значение > параметр.
Ниже перечислены шаги, которые вы можете выполнить для создания
графического интерфейса клавиатуры в Qt Designer:
1. Измените заголовок окна: QWidget > windowTitle > 8.1 - GUI клавиатура
2. Дважды щелкните на метке QLabel. Измените текст на Введите пароль.
3. Измените свойства QLabel:
a. QWidget > font > Point Size > 20
b. Для редактирования цветов палитры необходимо найти свойство
палитры, которое открывает диалоговое окно. Здесь можно изменить
цвета для различных частей объекта. Чтобы изменить цвет текста в
объекте метки: QWidget > palette > Change Palette > Window Text > white
c. QLabel > alignment > Horizontal > AlignHCenter
4. Измените свойства верхней рамки:
a. QWidget > sizePolicy > Vertical Stretch > 1
b. QFrame > frameShape > NoFrame
c. QFrame > frameShadow > Plain
5. Для каждого из четырех виджетов QLineEdit измените их свойства:
a. QWidget > sizePolicy > Vertical Policy > Expanding
b. QWidget > font > Point Size > 30
c. QLineEdit > alignment > Horizontal > AlignHCenter
ГЛАВА 8 СОЗДАНИЕ ГРАФИЧЕСКИХ ИНТЕРФЕЙСОВ С ПОМОЩЬЮ QT DESIGNER 211
6. Измените свойства нижней рамки:
a. QWidget > sizePolicy > Vertical Stretch > 2
b. QFrame > frameShape > Box
c. QFrame > frameShadow > Sunken
d. QFrame > lineWidth > 2
7. Дважды щелкните по каждой из кнопок и измените их текст на 0-9, * и #. (См.
рис. 8-11).
8. Отредактируйте свойства каждой кнопки:
a. QWidget > sizePolicy > Vertical Policy > Expanding
b. QWidget > font > Point Size > 36
9. Измените размер главного окна:
a. QWidget > geometry > Width > 302
b. QWidget > geometry > Height > 406
10. Щелкните форму и измените цвет ее фона:
QWidget > palette > Change Palette > Window > dark gray (тёмно серый)
11. В Инспекторе объектов дважды щелкните на каждом из имен объектов
по умолчанию для рамок, редактирования линий и кнопок и измените их.
Это будет полезно в дальнейшем, чтобы мы могли различать кнопки при
просмотре кода. Имя объекта используется для ссылки на объекты.
После того как вы выполнили все шаги, форма должна выглядеть так, как показано
на рисунке 8-11.

Предварительный просмотр графического


интерфейса
Часто бывает полезно просмотреть форму и взаимодействовать с ней, прежде
чем экспортировать ее в код. Это не только полезно для проверки внешнего вида
графического интерфейса, но предварительный просмотр также помогает убедиться,
что сигналы и слоты, изменение размера окна и другие функции работают правильно.
Для предварительного просмотра формы откройте меню Form и выберите
Preview или используйте горячие клавиши Ctrl+R для Windows или Command+R для
macOS. Если вы довольны своей формой, сохраните ее как UI-файл с расширением
.ui. Файлы UI Qt Designer записываются в формате XML и содержат представление
дерева виджетов для создания графического интерфейса.

Подключение сигналов и слотов в Qt Designer


Перейдите в режим редактирования сигналов/слотов (Edit Signals/Slots), выбрав
его на панели инструментов. Qt Designer имеет простой интерфейс для подключения
сигналов и слотов. Щелкните на объекте, который будет излучать сигнал, и перетащите
его на объект, который будет принимать сигнал. Для графического интерфейса
212 ГЛАВА 8 СОЗДАНИЕ ГРАФИЧЕСКИХ ИНТЕРФЕЙСОВ С ПОМОЩЬЮ QT DESIGNER
клавиатуры мы создаем только один набор соединений. Остальные сигналы и слоты
будут обрабатываться путем ручного кодирования.
При нажатии на кнопку "*" мы хотим очистить все четыре виджета редактирования
строк. Нажмите на кнопку и перетащите красную стрелку на первый объект
редактирования строки. Появится диалоговое окно (показано на рис. 8-15),
позволяющее выбрать методы для сигнала и слота.

Совет. При подключении сигналов и слотов обязательно установите флажок


"Показывать сигналы и слоты, унаследованные от QWidget", чтобы получить
доступ к большему количеству методов.

Рисунок 8-15. Диалоговое окно для подключения сигналов и слотов


Выберите clicked() для кнопки и clear() для редактирования строки. Завершите
подключение остальных трех виджетов редактирования строки. Обратитесь к
Рисунку 8-16 в качестве руководства по подключению виджетов. Не забудьте
сохранить свою работу, прежде чем двигаться дальше. В данном примере файл
сохраняется под именем keypad.ui.

ГЛАВА 8 СОЗДАНИЕ ГРАФИЧЕСКИХ ИНТЕРФЕЙСОВ С ПОМОЩЬЮ QT DESIGNER 213


Рисунок 8-16. Графический интерфейс клавиатуры с соединениями сигналов и
слотов

Создание кода Python из Qt Designer


Qt Designer использует утилиту Qt User Interface Compiler (uic) для генерации кода
и создания пользовательского интерфейса. Однако, поскольку вы используете PyQt6,
вы должны использовать модуль uic, pyuic6, для загрузки .ui файлов и преобразования
XML кода в Python код. Утилита pyuic6 представляет собой интерфейс командной
строки для взаимодействия с uic.
Откройте оболочку вашей системы и перейдите в каталог, содержащий файл UI.
Следующая строка показывает формат для преобразования XML в Python:
$ pyuic6 filename.ui -o filename.py
Чтобы сгенерировать файл Python, необходимо включить флаг -o и файл Python, в
который он будет записан, filename.py. Эта команда создаст один класс Python.
Создавая новый файл, лучше всего создать отдельный скрипт, который будет
наследоваться от созданного класса пользовательского интерфейса. Другой
вариант - создать исполняемый файл, который может отображать графический
интерфейс пользователя. Это можно сделать, включив флаг -x для выполнения, что
продемонстрировано в следующем коде:
pyuic6 -x filename.ui -o filename.py

214 ГЛАВА 8 СОЗДАНИЕ ГРАФИЧЕСКИХ ИНТЕРФЕЙСОВ С ПОМОЩЬЮ QT DESIGNER


Примечание. Если вы внесете изменения в графический интерфейс
пользователя в Qt Designer после создания сценария Python, вам придется
снова вызвать pyuic6 для обновления приложения.

И последнее замечание о выполнении команды pyuic6. Если вы обнаружите, что


pyuic6 не найден, вы можете попробовать использовать следующий формат:
$ python3 -m PyQt6.uic.pyuic filename.ui -o filename.py
Замените python3 на python в Windows.

Генерация кода с помощью pyuic6


Чтобы сгенерировать файл keypad_gui.py, перейдите к месту, где вы сохранили
файл keypad.ui, и выполните следующую строку:
$ python3 -m PyQt6.uic.pyuic keypad.ui -o keypad_gui.py
Следующий Python-код в Листингах от 8-1 до 8-10 получен в результате выполнения
команды pyuic6. Он не был изменен, чтобы мы могли посмотреть, что производит
pyuic6. Обратите внимание, что даже если вы следовали руководству по созданию
графического интерфейса клавиатуры, ваш код может выглядеть не совсем так же.
Листинг 8-1. Класс Python, созданный на основе keypad.ui
# keypad_gui.py
from PyQt6 import QtCore, QtGui, QtWidgets

class Ui_Keypad(object):
def setupUi(self, Keypad):
Keypad.setObjectName("Keypad")
Keypad.resize(302, 406)

Сначала импортируются модули PyQt6. Класс Ui_Keypad наследует object,


обозначая, что этот класс является корнем для всех остальных классов.
Отсюда функция setupUi() класса Ui_Keypad используется для построения дерева
виджетов на виджете Keypad. Дерево виджетов используется для представления
организации виджетов в пользовательском интерфейсе. Поэтому методу setupUi()
передается виджет, который будет отображать интерфейс (обычно QWidget, QDialog
или QMainWindow) и компоновать пользовательский интерфейс на основе виджетов
и связей, которые мы использовали для его создания, вместе с параметрами, которые
он наследует.
Каждый виджет в Qt имеет палитру, которая содержит информацию о том, как
он будет рисоваться в окне. Класс QPalette содержит группы цветов для каждого
виджета в одном из трех возможных состояний - Active, Inactive или Disabled.

ГЛАВА 8 СОЗДАНИЕ ГРАФИЧЕСКИХ ИНТЕРФЕЙСОВ С ПОМОЩЬЮ QT DESIGNER 215


Поскольку мы изменили цвет фона палитры для главного окна на темно-серый,
эти изменения отображаются в Листинге 8-2.
Листинг 8-2. Настройка палитры для графического интерфейса клавиатуры
# keypad_gui.py
palette = QtGui.QPalette()
brush = QtGui.QBrush(QtGui.QColor(255, 255, 255))
brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern)
palette.setBrush(QtGui.QPalette.ColorGroup.Active,
QtGui.QPalette.ColorRole.Base, brush)
brush = QtGui.QBrush(QtGui.QColor(52, 48, 47))
brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern)
palette.setBrush(QtGui.QPalette.ColorGroup.Active,
QtGui.QPalette.ColorRole.Window, brush)
brush = QtGui.QBrush(QtGui.QColor(255, 255, 255))
brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern)
palette.setBrush(QtGui.QPalette.ColorGroup.Inactive,
QtGui.QPalette.ColorRole.Base, brush)
brush = QtGui.QBrush(QtGui.QColor(52, 48, 47))
brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern)
palette.setBrush(QtGui.QPalette.ColorGroup.Inactive,
QtGui.QPalette.ColorRole.Window, brush)
brush = QtGui.QBrush(QtGui.QColor(52, 48, 47))
brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern)
palette.setBrush(QtGui.QPalette.ColorGroup.Disabled,
QtGui.QPalette.ColorRole.Base, brush)
brush = QtGui.QBrush(QtGui.QColor(52, 48, 47))
brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern)
palette.setBrush(QtGui.QPalette.ColorGroup.Disabled,
QtGui.QPalette.ColorRole.Window, brush)
Keypad.setPalette(palette)

Видно, что хотя мы изменили только цвет палитры, это изменение обрабатывается
для всех трех состояний автоматически. Класс QBrush используется для применения
цветов и узоров к виджетам. Метод setPalette() применяет палитру к классу Keypad.
Вертикальный макет для класса Keypad показан в Листинге 8-3.
Листинг 8-3. Создание менеджера компоновки главного окна
# keypad_gui.py
self.verticalLayout = QtWidgets.QVBoxLayout(Keypad)
self.verticalLayout.setObjectName("verticalLayout")

Изменения, внесенные в объект QLabel, отражены в Листинге 8-4. Они включают


изменение настроек палитры метки так, чтобы цвет шрифта был белым, и добавление
метки в verticalLayout.
216 ГЛАВА 8 СОЗДАНИЕ ГРАФИЧЕСКИХ ИНТЕРФЕЙСОВ С ПОМОЩЬЮ QT DESIGNER
Листинг 8-4. Создание ярлыка заголовка
# keypad_gui.py
self.label = QtWidgets.QLabel(Keypad)
palette = QtGui.QPalette()
brush = QtGui.QBrush(QtGui.QColor(255, 255, 255))
brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern)
palette.setBrush(QtGui.QPalette.ColorGroup.Active,
QtGui.QPalette.ColorRole.WindowText, brush)
brush = QtGui.QBrush(QtGui.QColor(255, 255, 255))
brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern)
palette.setBrush(QtGui.QPalette.ColorGroup.Inactive,
QtGui.QPalette.ColorRole.WindowText, brush)
brush = QtGui.QBrush(QtGui.QColor(127, 127, 127))
brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern)
palette.setBrush(QtGui.QPalette.ColorGroup.Disabled,
QtGui.QPalette.ColorRole.WindowText, brush)
self.label.setPalette(palette)
font = QtGui.QFont()
font.setPointSize(20)
self.label.setFont(font)
self.label.setAlignment(
QtCore.Qt.AlignmentFlag.AlignCenter)
self.label.setObjectName("label")
self.verticalLayout.addWidget(self.label)

Изменения шрифта и выравнивания экземпляра метки также отражены в коде.


Первый контейнер QFrame, frame, в Листинге 8-5 содержит четыре экземпляра
QLineEdit и использует QHBoxLayout для расположения виджетов.
Листинг 8-5. Создание рамки для виджетов QLineEdit в графическом интерфейсе
клавиатуры
# keypad_gui.py
self.frame = QtWidgets.QFrame(Keypad)
sizePolicy = QtWidgets.QSizePolicy(
QtWidgets.QSizePolicy.Policy.Preferred,
QtWidgets.QSizePolicy.Policy.Preferred)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(1)
sizePolicy.setHeightForWidth(
self.frame.sizePolicy().hasHeightForWidth())
self.frame.setSizePolicy(sizePolicy)
self.frame.setFrameShape(
QtWidgets.QFrame.Shape.NoFrame)
self.frame.setFrameShadow(

ГЛАВА 8 СОЗДАНИЕ ГРАФИЧЕСКИХ ИНТЕРФЕЙСОВ С ПОМОЩЬЮ QT DESIGNER 217


QtWidgets.QFrame.Shadow.Plain)
self.frame.setLineWidth(0)
self.frame.setObjectName("frame")
self.horizontalLayout = QtWidgets.QHBoxLayout(
self.frame)
self.horizontalLayout.setObjectName(
"horizontalLayout")
Корректировки рамки можно увидеть в предыдущем коде. Вертикальное
растяжение изменено на 1, форма рамки установлена на NoFrame, а тень установлена
на Plain. Редактирование линий, которые содержит рамка, построено в Листинге 8-6.
Листинг 8-6. Код для виджетов QLineEdit в графическом интерфейсе клавиатуры
# keypad_gui.py
self.line_edit1 = QtWidgets.QLineEdit(self.frame)
sizePolicy = QtWidgets.QSizePolicy(
QtWidgets.QSizePolicy.Policy.Expanding,
QtWidgets.QSizePolicy.Policy.Expanding)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(
self.line_edit1.sizePolicy().hasHeightForWidth())
self.line_edit1.setSizePolicy(sizePolicy)
font = QtGui.QFont()
font.setPointSize(30)
self.line_edit1.setFont(font)
self.line_edit1.setAlignment(
QtCore.Qt.AlignmentFlag.AlignCenter)
self.line_edit1.setObjectName("line_edit1")
self.horizontalLayout.addWidget(self.line_edit1)
self.line_edit2 = QtWidgets.QLineEdit(self.frame)
sizePolicy = QtWidgets.QSizePolicy(
QtWidgets.QSizePolicy.Policy.Expanding,
QtWidgets.QSizePolicy.Policy.Expanding)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(
self.line_edit2.sizePolicy().hasHeightForWidth())
self.line_edit2.setSizePolicy(sizePolicy)
font = QtGui.QFont()
font.setPointSize(30)
self.line_edit2.setFont(font)
self.line_edit2.setAlignment(
QtCore.Qt.AlignmentFlag.AlignCenter)
self.line_edit2.setObjectName("line_edit2")
self.horizontalLayout.addWidget(self.line_edit2)
218 ГЛАВА 8 СОЗДАНИЕ ГРАФИЧЕСКИХ ИНТЕРФЕЙСОВ С ПОМОЩЬЮ QT DESIGNER
self.line_edit3 = QtWidgets.QLineEdit(self.frame)
sizePolicy = QtWidgets.QSizePolicy(
QtWidgets.QSizePolicy.Policy.Expanding,
QtWidgets.QSizePolicy.Policy.Expanding)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(
self.line_edit3.sizePolicy().hasHeightForWidth())
self.line_edit3.setSizePolicy(sizePolicy)
font = QtGui.QFont()
font.setPointSize(30)
self.line_edit3.setFont(font)
self.line_edit3.setAlignment(
QtCore.Qt.AlignmentFlag.AlignCenter)
self.line_edit3.setObjectName("line_edit3")
self.horizontalLayout.addWidget(self.line_edit3)
self.line_edit4 = QtWidgets.QLineEdit(self.frame)
sizePolicy = QtWidgets.QSizePolicy(
QtWidgets.QSizePolicy.Policy.Expanding,
QtWidgets.QSizePolicy.Policy.Expanding)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(
self.line_edit4.sizePolicy().hasHeightForWidth())
self.line_edit4.setSizePolicy(sizePolicy)
font = QtGui.QFont()
font.setPointSize(30)
self.line_edit4.setFont(font)
self.line_edit4.setAlignment(
QtCore.Qt.AlignmentFlag.AlignCenter)
self.line_edit4.setObjectName("line_edit4")
self.horizontalLayout.addWidget(self.line_edit4)
self.verticalLayout.addWidget(self.frame)

В этом большом блоке кода много повторений. Это потому, что во всех четырех
строках внесены одни и те же изменения. Рассмотрев один экземпляр, вы сможете
понять остальные три.
Каждый из четырех виджетов редактирования строки имеет политики размеров,
которые позволяют им растягиваться, если окно изменяет размеры по вертикали
и горизонтали, используя QSizePolicy.Policy.Expanding. Изменения, внесенные в
размер шрифта и выравнивание, также отображаются. Затем виджеты QLineEdit
располагаются в horizontalLayout контейнера фрейма. Наконец, объект рамки
добавляется в verticalLayout главного окна.
В Листинге 8-7 показан нижний контейнер фрейма и устанавливается его политика
размера и атрибуты стиля.
ГЛАВА 8 СОЗДАНИЕ ГРАФИЧЕСКИХ ИНТЕРФЕЙСОВ С ПОМОЩЬЮ QT DESIGNER 219
Листинг 8-7. Создание рамки для виджетов QPushButton в графическом
интерфейсе клавиатуры
# keypad_gui.py
self.frame_2 = QtWidgets.QFrame(Keypad)
sizePolicy = QtWidgets.QSizePolicy(
QtWidgets.QSizePolicy.Policy.Preferred,
QtWidgets.QSizePolicy.Policy.Preferred)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(2)
sizePolicy.setHeightForWidth(
self.frame_2.sizePolicy().hasHeightForWidth())
self.frame_2.setSizePolicy(sizePolicy)
self.frame_2.setFrameShape(QtWidgets.QFrame.Shape.Box)
self.frame_2.setFrameShadow(
QtWidgets.QFrame.Shadow.Sunken)
self.frame_2.setLineWidth(2)
self.frame_2.setObjectName("frame_2")
self.gridLayout = QtWidgets.QGridLayout(self.frame_2)
self.gridLayout.setObjectName("gridLayout")

Нижняя рамка гарантированно займет больше места по вертикали, так как ее


коэффициент вертикального растяжения установлен на 2. Рамка имеет форму Box,
тень Sunken и ширину линии 2. Макет внутри рамки frame_2 содержит 12 кнопок и
использует сетчатый макет.
Имена кнопок и виджетов редактирования строки отражают изменения, которые
мы сделали в Qt Designer. Это облегчает в сценарии Python различение виджетов
в интерфейсе клавиатуры. Давайте рассмотрим код для 12 виджетов QPushButton в
Листинге 8-8.
Листинг 8-8. Создание виджетов QPushButton, расположенных в нижней рамке
# keypad_gui.py
self.button_7 = QtWidgets.QPushButton(self.frame_2)
sizePolicy = QtWidgets.QSizePolicy(
QtWidgets.QSizePolicy.Policy.Minimum,
QtWidgets.QSizePolicy.Policy.Expanding)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(
self.button_7.sizePolicy().hasHeightForWidth())
self.button_7.setSizePolicy(sizePolicy)
font = QtGui.QFont()
font.setPointSize(36)
self.button_7.setFont(font)
self.button_7.setObjectName("button_7")

220 ГЛАВА 8 СОЗДАНИЕ ГРАФИЧЕСКИХ ИНТЕРФЕЙСОВ С ПОМОЩЬЮ QT DESIGNER


self.gridLayout.addWidget(self.button_7, 0, 0, 1, 1)
self.button_8 = QtWidgets.QPushButton(self.frame_2)
sizePolicy = QtWidgets.QSizePolicy(
QtWidgets.QSizePolicy.Policy.Minimum,
QtWidgets.QSizePolicy.Policy.Expanding)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(
self.button_8.sizePolicy().hasHeightForWidth())
self.button_8.setSizePolicy(sizePolicy)
font = QtGui.QFont()
font.setPointSize(36)
self.button_8.setFont(font)
self.button_8.setObjectName("button_8")
self.gridLayout.addWidget(self.button_8, 0, 1, 1, 1)
self.button_9 = QtWidgets.QPushButton(self.frame_2)
sizePolicy = QtWidgets.QSizePolicy(
QtWidgets.QSizePolicy.Policy.Minimum,
QtWidgets.QSizePolicy.Policy.Expanding)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(
self.button_9.sizePolicy().hasHeightForWidth())
self.button_9.setSizePolicy(sizePolicy)
font = QtGui.QFont()
font.setPointSize(36)
self.button_9.setFont(font)
self.button_9.setObjectName("button_9")
self.gridLayout.addWidget(self.button_9, 0, 2, 1, 1)
self.button_4 = QtWidgets.QPushButton(self.frame_2)
sizePolicy = QtWidgets.QSizePolicy(
QtWidgets.QSizePolicy.Policy.Minimum,
QtWidgets.QSizePolicy.Policy.Expanding)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(
self.button_4.sizePolicy().hasHeightForWidth())
self.button_4.setSizePolicy(sizePolicy)
font = QtGui.QFont()
font.setPointSize(36)
self.button_4.setFont(font)
self.button_4.setObjectName("button_4")
self.gridLayout.addWidget(self.button_4, 1, 0, 1, 1)
self.button_5 = QtWidgets.QPushButton(self.frame_2)
sizePolicy = QtWidgets.QSizePolicy(
ГЛАВА 8 СОЗДАНИЕ ГРАФИЧЕСКИХ ИНТЕРФЕЙСОВ С ПОМОЩЬЮ QT DESIGNER 221
QtWidgets.QSizePolicy.Policy.Minimum,
QtWidgets.QSizePolicy.Policy.Expanding)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(
self.button_5.sizePolicy().hasHeightForWidth())
self.button_5.setSizePolicy(sizePolicy)
font = QtGui.QFont()
font.setPointSize(36)
self.button_5.setFont(font)
self.button_5.setObjectName("button_5")
self.gridLayout.addWidget(self.button_5, 1, 1, 1, 1)
self.button_6 = QtWidgets.QPushButton(self.frame_2)
sizePolicy = QtWidgets.QSizePolicy(
QtWidgets.QSizePolicy.Policy.Minimum,
QtWidgets.QSizePolicy.Policy.Expanding)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(
self.button_6.sizePolicy().hasHeightForWidth())
self.button_6.setSizePolicy(sizePolicy)
font = QtGui.QFont()
font.setPointSize(36)
self.button_6.setFont(font)
self.button_6.setObjectName("button_6")
self.gridLayout.addWidget(self.button_6, 1, 2, 1, 1)
self.button_3 = QtWidgets.QPushButton(self.frame_2)
sizePolicy = QtWidgets.QSizePolicy(
QtWidgets.QSizePolicy.Policy.Minimum,
QtWidgets.QSizePolicy.Policy.Expanding)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(
self.button_3.sizePolicy().hasHeightForWidth())
self.button_3.setSizePolicy(sizePolicy)
font = QtGui.QFont()
font.setPointSize(36)
self.button_3.setFont(font)
self.button_3.setObjectName("button_3")
self.gridLayout.addWidget(self.button_3, 2, 0, 1, 1)
self.button_2 = QtWidgets.QPushButton(self.frame_2)
sizePolicy = QtWidgets.QSizePolicy(
QtWidgets.QSizePolicy.Policy.Minimum,
QtWidgets.QSizePolicy.Policy.Expanding)
sizePolicy.setHorizontalStretch(0)
222 ГЛАВА 8 СОЗДАНИЕ ГРАФИЧЕСКИХ ИНТЕРФЕЙСОВ С ПОМОЩЬЮ QT DESIGNER
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(
self.button_2.sizePolicy().hasHeightForWidth())
self.button_2.setSizePolicy(sizePolicy)
font = QtGui.QFont()
font.setPointSize(36)
self.button_2.setFont(font)
self.button_2.setObjectName("button_2")
self.gridLayout.addWidget(self.button_2, 2, 1, 1, 1)
self.button_1 = QtWidgets.QPushButton(self.frame_2)
sizePolicy = QtWidgets.QSizePolicy(
QtWidgets.QSizePolicy.Policy.Minimum,
QtWidgets.QSizePolicy.Policy.Expanding)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(
self.button_1.sizePolicy().hasHeightForWidth())
self.button_1.setSizePolicy(sizePolicy)
font = QtGui.QFont()
font.setPointSize(36)
self.button_1.setFont(font)
self.button_1.setObjectName("button_1")
self.gridLayout.addWidget(self.button_1, 2, 2, 1, 1)
self.button_star = QtWidgets.QPushButton(self.frame_2)
sizePolicy = QtWidgets.QSizePolicy(
QtWidgets.QSizePolicy.Policy.Minimum,
QtWidgets.QSizePolicy.Policy.Expanding)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(
self.button_star.sizePolicy().hasHeightForWidth())
self.button_star.setSizePolicy(sizePolicy)
font = QtGui.QFont()
font.setPointSize(36)
self.button_star.setFont(font)
self.button_star.setObjectName("button_star")
self.gridLayout.addWidget(
self.button_star, 3, 0, 1, 1)
self.button_0 = QtWidgets.QPushButton(self.frame_2)
sizePolicy = QtWidgets.QSizePolicy(
QtWidgets.QSizePolicy.Policy.Minimum,
QtWidgets.QSizePolicy.Policy.Expanding)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(
ГЛАВА 8 СОЗДАНИЕ ГРАФИЧЕСКИХ ИНТЕРФЕЙСОВ С ПОМОЩЬЮ QT DESIGNER 223
self.button_0.sizePolicy().hasHeightForWidth())
self.button_0.setSizePolicy(sizePolicy)
font = QtGui.QFont()
font.setPointSize(36)
self.button_0.setFont(font)
self.button_0.setObjectName("button_0")
self.gridLayout.addWidget(self.button_0, 3, 1, 1, 1)
self.button_hash = QtWidgets.QPushButton(self.frame_2)
sizePolicy = QtWidgets.QSizePolicy(
QtWidgets.QSizePolicy.Policy.Minimum,
QtWidgets.QSizePolicy.Policy.Expanding)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(
self.button_hash.sizePolicy().hasHeightForWidth())
self.button_hash.setSizePolicy(sizePolicy)
font = QtGui.QFont()
font.setPointSize(36)
self.button_hash.setFont(font)
self.button_hash.setObjectName("button_hash")
self.gridLayout.addWidget(
self.button_hash, 3, 2, 1, 1)
self.verticalLayout.addWidget(self.frame_2)

Создано 12 виджетов QPushButton. Кнопки могут расширяться по вертикали с


помощью флага Expanding, а размер их шрифта установлен на 36. Затем каждая кнопка
добавляется в макет сетки frame_2, которая затем добавляется в вертикальный макет
главного окна.
Листинг 8-9 соединяет сигналы для экземпляра button_star с виджетами
редактирования строк, очищая текст при каждом нажатии кнопки. Это позволяет
пользователю удалить введенный текст и попробовать снова.
Листинг 8-9. Подключение сигналов и слотов для графического интерфейса
клавиатуры
# keypad_gui.py
self.retranslateUi(Keypad)
self.button_star.clicked.connect(
self.line_edit1.clear)
self.button_star.clicked.connect(
self.line_edit2.clear)
self.button_star.clicked.connect(
self.line_edit3.clear)
self.button_star.clicked.connect(
self.line_edit4.clear)
QtCore.QMetaObject.connectSlotsByName(Keypad)
224 ГЛАВА 8 СОЗДАНИЕ ГРАФИЧЕСКИХ ИНТЕРФЕЙСОВ С ПОМОЩЬЮ QT DESIGNER
Метод retranslateUi() в Листинге 8-10 определяет, как отображать текст в
графическом интерфейсе в случае использования другого языка.
Листинг 8-10. Код для метода retranslateUi()
# keypad_gui.py
def retranslateUi(self, Keypad):
_translate = QtCore.QCoreApplication.translate
Keypad.setWindowTitle(
_translate("Keypad", "8.1 - Keypad GUI"))
self.label.setText(
_translate("Keypad", "Enter a passcode"))
self.button_7.setText(_translate("Keypad", "7"))
self.button_8.setText(_translate("Keypad", "8"))
self.button_9.setText(_translate("Keypad", "9"))
self.button_4.setText(_translate("Keypad", "4"))
self.button_5.setText(_translate("Keypad", "5"))
self.button_6.setText(_translate("Keypad", "6"))
self.button_3.setText(_translate("Keypad", "3"))
self.button_2.setText(_translate("Keypad", "2"))
self.button_1.setText(_translate("Keypad", "1"))
self.button_star.setText(_translate("Keypad", "*"))
self.button_0.setText(_translate("Keypad", "0"))
self.button_hash.setText(_translate("Keypad", "#"))

QCoreApplication.translate возвращает переведенный текст второго аргумента,


переданного в метод.

Написание нового сценария для создания


графического интерфейса пользователя
В следующем разделе создается класс, наследующий Ui_Keypad, и настраивается
приложение GUI. Чтобы использовать класс Ui_Keypad, созданный с помощью
Qt Designer, мы создадим новый Python-файл, keypad_main.py. Класс KeypadGUI,
созданный в keypad_main.py, будет наследоваться от класса Ui_Keypad.
Мы начнем с импорта модулей, необходимых для этого проекта в Листинге 8-11,
включая класс Ui_Keypad и новый класс PyQt, QIntValidator. PyQt предоставляет
несколько классов, которые можно использовать для проверки типов вводимого
текста. QIntValidator будет использоваться для проверки того, являются ли значения,
вводимые в виджеты QLineEdit, целыми числами.

ГЛАВА 8 СОЗДАНИЕ ГРАФИЧЕСКИХ ИНТЕРФЕЙСОВ С ПОМОЩЬЮ QT DESIGNER 225


Листинг 8-11. Настройка главного окна для графического интерфейса клавиатуры
# keypad_main.py
# Import necessary modules
import sys
from PyQt6.QtWidgets import (QApplication, QWidget,
QMessageBox)
from PyQt6.QtCore import Qt
from PyQt6.QtGui import QIntValidator
from keypad_gui import Ui_Keypad

class MainWindow(QWidget):
def __init__(self):
super().__init__()
self.ui = Ui_Keypad()
self.ui.setupUi(self)
self.initializeUI()
self.show()

if __name__ == "__main__":
app = QApplication(sys.argv)
Keypad = MainWindow()
sys.exit(app.exec())

Класс MainWindow создан с использованием подхода единого наследования,


где он наследует свои свойства от единственного родительского класса QWidget.
Пользовательский интерфейс устанавливается в методе __init__() в следующих
строках:
self.ui = Ui_Keypad()
self.ui.setupUi(self)
В методе initializeUI() в Листинге 8-12 производятся локальные модификации
виджетов QLineEdit. Здесь политика фокуса виджета редактирования строки
установлена на NoFocus, чтобы пользователи могли вводить данные только в
правильном порядке, слева направо.
Листинг 8-12. Код для метода initializeUI() в графическом интерфейсе клавиатуры
# keypad_main.py
def initializeUI(self):
"""Настройка графического интерфейса приложения."""
#Обновление других функций line_edit
#Установите максимальное количество разрешенных символов
self.ui.line_edit1.setMaxLength(1)
# Пользователь может вводить только целые числа от 0-9
self.ui.line_edit1.setValidator(QIntValidator(0, 9))
226 ГЛАВА 8 СОЗДАНИЕ ГРАФИЧЕСКИХ ИНТЕРФЕЙСОВ С ПОМОЩЬЮ QT DESIGNER
# Виджет не принимает фокус
self.ui.line_edit1.setFocusPolicy(
Qt.FocusPolicy.NoFocus)
self.ui.line_edit2.setMaxLength(1)
self.ui.line_edit2.setValidator(QIntValidator(0, 9))
self.ui.line_edit2.setFocusPolicy(
Qt.FocusPolicy.NoFocus)
self.ui.line_edit3.setMaxLength(1)
self.ui.line_edit3.setValidator(QIntValidator(0, 9))
self.ui.line_edit3.setFocusPolicy(
Qt.FocusPolicy.NoFocus)
self.ui.line_edit4.setMaxLength(1)
self.ui.line_edit4.setValidator(QIntValidator(0, 9))
self.ui.line_edit4.setFocusPolicy(
Qt.FocusPolicy.NoFocus)

Затем мы соединяем сигналы и слоты для виджетов кнопок в Листинге 8-13. Когда
каждая кнопка нажимается, она посылает сигнал, который подключается к слоту
numberClicked(). Вместо того чтобы создавать отдельный метод для каждой кнопки,
функция lambda используется для повторного использования метода для сигналов.
lambda вызывает функцию numberClicked() и каждый раз передает ей новый параметр,
в данном случае конкретный текст каждой кнопки.
Листинг 8-13. Настройка сигналов для кнопок в графическом интерфейсе
клавиатуры
# keypad_main.py
# 4-значный код доступа
self.passcode = 8618

# Добавьте сигнальные/слотовые соединения для кнопок


self.ui.button_0.clicked.connect(
lambda: self.numberClicked(
self.ui.button_0.text()))
self.ui.button_1.clicked.connect(
lambda: self.numberClicked(
self.ui.button_1.text()))
self.ui.button_2.clicked.connect(
lambda: self.numberClicked(
self.ui.button_2.text()))
self.ui.button_3.clicked.connect(
lambda: self.numberClicked(
self.ui.button_3.text()))
self.ui.button_4.clicked.connect(
lambda: self.numberClicked(
self.ui.button_4.text()))
ГЛАВА 8 СОЗДАНИЕ ГРАФИЧЕСКИХ ИНТЕРФЕЙСОВ С ПОМОЩЬЮ QT DESIGNER 227
self.ui.button_5.clicked.connect(
lambda: self.numberClicked(
self.ui.button_5.text()))
self.ui.button_6.clicked.connect(
lambda: self.numberClicked(
self.ui.button_6.text()))
self.ui.button_7.clicked.connect(
lambda: self.numberClicked(
self.ui.button_7.text()))
self.ui.button_8.clicked.connect(
lambda: self.numberClicked(
self.ui.button_8.text()))
self.ui.button_9.clicked.connect(
lambda: self.numberClicked(
self.ui.button_9.text()))

self.ui.button_hash.clicked.connect(
self.checkPasscode)

Когда пользователь нажимает на кнопку, номер этой кнопки должен появиться


в правильной строке редактирования виджета слева направо. Виджет получает
фокус, если его значение text() пусто. Это обрабатывается в слоте numberClicked() в
Листинге 8-14.
Листинг 8-14. Создание слота NumberClicked()
# keypad_main.py
def numberClicked(self, text_value):
"""При нажатии кнопки с цифрой проверьте,
пуст ли текст для виджетов QLineEdit. Если пуст,
установите фокус на нужный виджет и введите
текстовое значение."""
if self.ui.line_edit1.text() == "":
self.ui.line_edit1.setFocus()
self.ui.line_edit1.setText(text_value)
self.ui.line_edit1.repaint()
elif (self.ui.line_edit1.text() != "") and \
(self.ui.line_edit2.text() == ""):
self.ui.line_edit2.setFocus()
self.ui.line_edit2.setText(text_value)
self.ui.line_edit2.repaint()
elif (self.ui.line_edit1.text() != "") and \
(self.ui.line_edit2.text() != "") and \
(self.ui.line_edit3.text() == ""):
self.ui.line_edit3.setFocus()
self.ui.line_edit3.setText(text_value)
228 ГЛАВА 8 СОЗДАНИЕ ГРАФИЧЕСКИХ ИНТЕРФЕЙСОВ С ПОМОЩЬЮ QT DESIGNER
self.ui.line_edit3.repaint()
elif (self.ui.line_edit1.text() != "") and \
(self.ui.line_edit2.text() != "") and \
(self.ui.line_edit3.text() != "") and \
(self.ui.line_edit4.text() == ""):
self.ui.line_edit4.setFocus()
self.ui.line_edit4.setText(text_value)
self.ui.line_edit4.repaint()

Метод repaint() используется для обеспечения обновления текста в виджетах


QLineEdit.
Наконец, если пользователь нажимает кнопку #, слот checkPasscode() в
Листинге 8-15 проверяет, ввел ли пользователь код, соответствующий passcode. Если
ввод не совпадает, виджеты редактирования строки сбрасываются. Этот проект
может быть разработан так, чтобы пароль считывался из файла или из базы данных.
Листинг 8-15. Создание слота checkPasscode()
# keypad_main.py
def checkPasscode(self):
"""Объедините текстовые значения из 4 виджетов
QLineEdit и проверьте, совпадает ли введенный
пользователем пароль с существующим."""
entered_passcode = self.ui.line_edit1.text() + \
self.ui.line_edit2.text() + \
self.ui.line_edit3.text() + \
self.ui.line_edit4.text()
if len(entered_passcode) == 4 and \
int(entered_passcode) == self.passcode:
QMessageBox.information(
self, "Верный пароль!", "Верный пароль!",
QMessageBox.StandardButton.Ok)
self.close()
else:
QMessageBox.warning(
self, "Сообщение об ошибке", "Неверный пароль.",
QMessageBox.StandardButton.Close)
self.ui.line_edit1.clear()
self.ui.line_edit2.clear()
self.ui.line_edit3.clear()
self.ui.line_edit4.clear()
self.ui.line_edit1.setFocus()

Появляется QMessageBox, информирующий пользователя о результате ввода


пароля. Когда вы запустите этот сценарий, ваш графический интерфейс должен
выглядеть так, как показано на Рисунке 8-11.
ГЛАВА 8 СОЗДАНИЕ ГРАФИЧЕСКИХ ИНТЕРФЕЙСОВ С ПОМОЩЬЮ QT DESIGNER 229
Мы рассмотрели лишь некоторые возможности Qt Designer при создании
графического интерфейса клавиатуры. В следующем разделе мы рассмотрим еще
несколько важных тем.

Дополнительные советы по использованию


Qt Designer
В следующем разделе кратко рассматриваются три дополнительные темы:
1. Создание графических интерфейсов с меню
2. Отображение изображений в Qt Designer
3. Использование таблиц стилей

Настройка главного окна и меню


Откройте Qt Designer и выберите шаблон Main Window из меню Form на
Рисунке 8-2. Это создаст главное окно со строкой меню и строкой состояния по
умолчанию. Вы можете увидеть форму главного окна, показанную на Рисунке 8-1.

Добавление меню и подменю в Qt Designer


Добавление меню в Qt Designer очень просто. Дважды щелкните на тексте-
заполнителе Type Here в строке меню и введите название (title) меню. Этот процесс
показан на рисунке 8-17. Если вы хотите создать ярлык, вы также можете добавить
амперсанд, &, в начало текста меню. Это обновит объект строки меню в диалоговом
окне "Инспектор объектов". Вы также можете редактировать свойства меню в
редакторе свойств.

Рисунок 8-17. Создание меню и пунктов меню. Введите здесь заполнитель (слева
вверху). Дважды щелкните заполнитель и введите название меню (вверху справа).
Добавьте новый пункт меню (слева внизу). Новый пункт меню (справа внизу)
230 ГЛАВА 8 СОЗДАНИЕ ГРАФИЧЕСКИХ ИНТЕРФЕЙСОВ С ПОМОЩЬЮ QT DESIGNER
Отсюда вы можете добавить дополнительные меню, подменю или действия. Чтобы
добавить подменю, сначала создайте пункт меню. Затем нажмите на символ плюса
рядом с новым пунктом меню. Это добавит новое меню, которое ответвляется от
существующего пункта меню. Дважды щелкните на заполнителе Type Here и введите
текст для нового пункта. Обратитесь за помощью к Рисунку 8-18.

Рисунок 8-18. Добавление подменю. Нажмите на символ плюса рядом с пунктом


меню (слева). Добавьте новый пункт (справа)

Добавление панелей инструментов в Qt Designer


Панели инструментов можно добавить в главное окно, щелкнув правой кнопкой
мыши на форме, чтобы открыть контекстное меню. Щелкните на опции Добавить
панель инструментов (Add Tool Bar).
Действия на панелях инструментов создаются как кнопки панели инструментов
и могут быть перетащены между меню и панелью инструментов. Вы также
можете добавить значки на панель инструментов. Эта тема рассматривается в
разделе Отображение изображений в Qt Designer. Пример панели инструментов с
пиктограммой показан на рисунке 8-19.

Рисунок 8-19. Панель инструментов с кнопкой Открыть файл

Добавление действий в Qt Designer


Когда элементы впервые создаются в меню и на панели инструментов, они
фактически являются действиями. Действия можно создавать, удалять, присваивать
ГЛАВА 8 СОЗДАНИЕ ГРАФИЧЕСКИХ ИНТЕРФЕЙСОВ С ПОМОЩЬЮ QT DESIGNER 231
им значок, назначать горячую клавишу и делать их проверяемыми в док-виджете
редактора действий (Рисунок 8-7). Действия также можно совместно использовать в
меню и на панели инструментов.
Чтобы разделить действие между меню и панелью инструментов так, чтобы
оба объекта содержали один и тот же элемент, перетащите действие из редактора
действий, уже существующего в меню, на панель инструментов.

Отображение изображений в Qt Designer


В этом разделе мы кратко рассмотрим, как вы можете включать изображения
и пиктограммы в ваше приложение. Независимо от того, хотите ли вы добавить
изображение в QLabel или пытаетесь добавить пиктограммы на панель инструментов,
процесс добавления изображения схож.
Например, если у вас есть виджет QLabel на форме, вы можете получить доступ
к его свойствам в редакторе свойств. Прокрутите вниз, пока не найдете свойство
pixmap. Щелкните на его значении, и отсюда вы сможете искать файл изображения.
Если вы хотите добавить значок, то используйте свойство icon, а не pixmap.
Вам предлагается два варианта: Выбрать ресурс... (Choose Resource) и Выбрать
файл... (Choose File). Если вы добавили ресурсы в свой проект, то выберите Choose
Resource.... В противном случае вы можете найти изображения на своем компьютере.

Рисунок 8-20. Добавление изображений в приложение с помощью свойства pixmap

Добавление таблиц стилей в Qt Designer


Таблицы стилей также можно легко добавить к каждому виджету, щелкнув правой
кнопкой мыши на виджете и выбрав опцию Change styleSheet... из контекстного
меню. Появится диалоговое окно, похожее на Рисунок 8-21.

232 ГЛАВА 8 СОЗДАНИЕ ГРАФИЧЕСКИХ ИНТЕРФЕЙСОВ С ПОМОЩЬЮ QT DESIGNER


Рисунок 8-21. Диалоговое окно для создания таблиц стилей
Здесь вы можете использовать выпадающие стрелки для выбора различных
свойств и изменения цветов, добавления ресурсов или изменения шрифтов.
На этом рассмотрение Qt Designer в этой главе заканчивается.

Резюме
Qt Designer, безусловно, является полезным инструментом для создания GUI-
приложений. Он предоставляет интерфейс перетаскивания, который облегчает
организацию виджетов; изменение параметров объектов; создание меню, панелей
инструментов и док-виджетов; добавление действий в меню; генерацию кода,
который можно использовать в Python, и многое другое. Qt Designer может сделать
процесс проектирования намного быстрее и проще.
Хотя в этой главе были рассмотрены некоторые основы использования
Qt Designer, существуют и другие возможности, например, создание собственных
пользовательских виджетов или построение диалоговых окон.
В следующих главах мы начнем рассматривать более специфические классы и темы
PyQt, которые можно использовать для дальнейшего улучшения пользовательских
интерфейсов. В следующей главе вы узнаете, как использовать класс QClipboard для
копирования и вставки данных между различными приложениями.

ГЛАВА 8 СОЗДАНИЕ ГРАФИЧЕСКИХ ИНТЕРФЕЙСОВ С ПОМОЩЬЮ QT DESIGNER 233


ГЛАВА 9

Работа с буфером
обмена
Одним из основных преимуществ графических интерфейсов является
возможность создавать программы, которые могут взаимодействовать с вашей
системой и другими приложениями. Эта концепция выходит за рамки открытия и
сохранения файлов или печати изображений.
Буфер обмена (clipboard) - это место в памяти компьютера, которое используется
для временного хранения данных, скопированных или вырезанных из приложения.
В буфере обмена могут храниться различные типы данных, включая текст,
изображения и GIF-файлы. Информация, хранящаяся в буфере обмена, может быть
вставлена в другие приложения, если только приложение знает, как работать с типом
данных, хранящихся в буфере обмена.
В этой главе вы узнаете
• Использовать классы QClipboard и QMimeData для перемещения данных между
приложениями
• Узнаете, как запустить несколько экземпляров приложения одновременно.
Чтобы начать, давайте узнаем о классе PyQt для взаимодействия с буфером обмена
вашего компьютера.

Класс QClipboard
Класс QClipboard делает доступным буфер обмена вашей системы, чтобы вы
могли копировать и вставлять данные, такие как текст, изображения и насыщенный
текст между приложениями. Виджеты Qt, которые можно использовать для работы с
текстовой информацией, такие как QLineEdit и QTextEdit, поддерживают использование
буфера обмена. Классы Model/View в Qt (о которых вы узнаете больше в Главе 10)
также поддерживают буфер обмена. Если вы хотите вставить изображение из буфера
обмена в приложение, обязательно используйте виджеты, поддерживающие графику,
такие как QLabel.
Включение буфера обмена в ваш проект довольно просто в PyQt. Чтобы получить
доступ к QClipboard приложения, сначала создайте экземпляр буфера обмена с
помощью следующей строки:
self.clipboard = QApplication.clipboard()
234 ГЛАВА 9 РАБОТА С БУФЕРОМ ОБМЕНА
Следующий блок кода показывает один из способов получения изображения,
которое было скопировано в буфер обмена, и применения его к метке:

label = QLabel() # Создание метки для хранения изображения


self.clipboard = QApplication.clipboard()
label.setPixmap(self.clipboard.pixmap())

Этот процесс работает только для изображений, поэтому если вы хотите вставить
текст или насыщенный текст, вам нужно будет использовать setText() для QLabel.
Другой способ получения данных - использовать класс QMimeData и описать, какие
именно данные перемещаются. Эта тема будет рассмотрена в разделе "Пояснения к
использованию QClipboard".
События, происходящие между вашей системой и приложением, созданным
с помощью PyQt, обрабатываются QApplication. Экземпляр буфера обмена дает
вам возможность отправлять или получать данные в вашем приложении. Однако
буфер обмена может одновременно хранить только один объект. Поэтому, если вы
скопируете изображение в буфер обмена, а затем скопируете текст, будет доступен
только текст, а изображение будет удалено.
В этом разделе вы создадите простой графический интерфейс, как на Рисунке 9-1,
который показывает, как получать текст из других приложений и затем вставлять
его в окно PyQt.

Рисунок 9-1. Пользователь может видеть содержимое буфера обмена в верхнем


виджете док-станции
ГЛАВА 9 РАБОТА С БУФЕРОМ ОБМЕНА 235
В верхнем виджете редактирования текста отображается текущее содержимое
буфера обмена. Затем пользователь может вставить его в главное окно, которое
является нижним виджетом редактирования текста. В некоторых приложениях вам
может понадобиться просмотреть содержимое буфера обмена в отдельном окне перед
тем, как вставить его в главное окно. Виджет док-станции, особенно тот, который
может плавать отдельно от главного окна, идеально подходит для использования в
качестве менеджера буфера обмена.

Пояснения к использованию QClipboard


В этом разделе вы увидите, как настроить буфер обмена и фактически иметь
возможность визуализировать его содержимое после копирования текста из другого
окна.
Поскольку класс MainWindow этого проекта наследует QMainWindow, вы можете
использовать сценарий main_window_template.py из Главы 5 для начала работы в
Листинге 9-1.
Листинг 9-1. Настройка главного окна для использования буфера обмена
# clipboard_ex.py
# Импортируйте необходимые модули
import sys
from PyQt6.QtWidgets import (QApplication, QMainWindow,
QPushButton, QTextEdit, QDockWidget, QFrame, QVBoxLayout)
from PyQt6.QtCore import Qt

class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.initializeUI()

def initializeUI(self):
"""Настройка графического интерфейса приложения."""
self.setMinimumSize(500, 300)
self.setWindowTitle("Пример буфера обмена")
self.setUpMainWindow()
self.createClipboardDock()
self.show()

if __name__ == "__main__":
app = QApplication(sys.argv)
window = MainWindow()
sys.exit(app.exec())

236 ГЛАВА 9 РАБОТА С БУФЕРОМ ОБМЕНА


Верхний QTextEdit на Рисунке 9-1 размещен в виджете QDockWidget. Он
устанавливается в методе createClipboardDock().
После импорта классов и настройки окна в предыдущем коде, давайте установим
центральный виджет главного окна как виджет QTextEdit в Листинге 9-2. Центральный
виджет является местом, где пользователь может редактировать текст, вставленный
из буфера обмена.
Листинг 9-2. Метод setUpMainWindow() для использования буфера обмена
# clipboard_ex.py
def setUpMainWindow(self):
"""Create and arrange widgets in the main window."""
self.central_tedit = QTextEdit()
self.setCentralWidget(self.central_tedit)
Далее мы установим виджет док-станции в Листинге 9-3, которая состоит из
QTextEdit и QPushButton, расположенных в QVBoxLayout. Контейнер QFrame содержит
виджеты clipboard_tedit и paste_button.
Листинг 9-3. Код для метода createClipboardDock()
# clipboard_ex.py
def createClipboardDock(self):
"""Настройка буфера обмена и виджета док-станции для
отображения текста из буфера обмена системы."""
self.clipboard_tedit = QTextEdit()
paste_button = QPushButton("Вставить")
paste_button.clicked.connect(self.pasteText)

dock_v_box = QVBoxLayout()
dock_v_box.addWidget(self.clipboard_tedit)
dock_v_box.addWidget(paste_button)

# Установите основной макет для виджета док-станции,


# затем установите основной виджет виджета док-станции
dock_frame = QFrame()
dock_frame.setLayout(dock_v_box)

# Создание виджета док-станции


clipboard_dock = QDockWidget()
clipboard_dock.setWindowTitle(
"Отображение содержимого буфера обмена")
clipboard_dock.setAllowedAreas(
Qt.DockWidgetArea.TopDockWidgetArea)
clipboard_dock.setWidget(dock_frame)

ГЛАВА 9 РАБОТА С БУФЕРОМ ОБМЕНА 237


# Установите начальное расположение виджета док-станции
self.addDockWidget(
Qt.DockWidgetArea.TopDockWidgetArea,
clipboard_dock)

# Создание экземпляра буфера обмена


self.clipboard = QApplication.clipboard()
self.clipboard.dataChanged.connect(
self.copyFromClipboard)

Виджет clipboard_dock устанавливается таким образом, чтобы он мог либо


плавать, либо быть прикрепленным к верхней части главного окна. Если новый
текст скопирован из другого приложения, то clipboard_tedit отобразит его. Если
пользователь хочет сохранить текст, то он может нажать кнопку paste_button и
скопировать его в central_tedit.
Метод QClipboard dataChanged() издает сигнал, если содержимое буфера обмена
изменилось. Если изменение произошло, то виджет clipboard_tedit обновляется для
отображения нового текста буфера обмена с помощью метода copyFromClipboard() в
Листинге 9-4.

Листинг 9-4. Код для слотов copyFromClipboard() и pasteText()


# clipboard_ex.py
def copyFromClipboard(self):
"""Получить содержимое системного буфера обмена
и вставить в окно, имеющее фокус."""
mime_data = self.clipboard.mimeData()
if mime_data.hasText():
self.clipboard_tedit.setText(mime_data.text())
self.clipboard_tedit.repaint()

def pasteText(self):
"""Вставка текста из буфера обмена при нажатии на кнопку."""
self.central_tedit.paste()
self.central_tedit.repaint()

Чтобы проверить, какие данные хранятся в буфере обмена, мы используем


класс QMimeData, который используется как для буфера обмена, так и для системы
drag-and-drop (перетаскивание) в PyQt. Формат Multipurpose Internet Mail Exten-
sions (MIME) поддерживает не только текст, но и HTML, URL, изображения и цветные
данные. Объекты, созданные на основе класса QMimeData, обеспечивают безопасное
перемещение информации между приложениями, а также между объектами в одном
и том же приложении.
Метод mimeData() возвращает информацию о данных, находящихся в данный
момент в буфере обмена. Чтобы проверить, может ли объект возвращать обычный
238 ГЛАВА 9 РАБОТА С БУФЕРОМ ОБМЕНА
текст, мы используем метод hasText(). Если данные являются текстом, то мы получаем
текст с помощью mime_data.text() и устанавливаем текст виджета QTextEdit с помощью
setText(). Аналогичный процесс используется и для доступа к другим видам данных
с помощью QMimeData.
Наконец, метод QTextEdit paste() вызывается в функции pasteText() для получения
текста в буфере обмена при нажатии кнопки. Метод repaint() используется для
принудительного обновления текста виджета.

Проект 9.1 - графический интерфейс Sticky Notes


Иногда у вас возникает идея, заметка или небольшая информация, которую
необходимо быстро записать. Возможно, вам нужно напомнить себе о встрече
и написать записку. Вам нужно лишь небольшое, временное, может быть, даже
красочное место, чтобы помочь провести мозговой штурм и упорядочить эти идеи.
Липкие заметки идеально подходят для этих и других целей.
Графический интерфейс "Липкие заметки", показанный на рис. 9-2, позволяет
открывать сколько угодно окон. Вы можете редактировать текст каждой заметки в
отдельности, изменять цвет заметки, а также вставлять текст из буфера обмена. Этот
проект демонстрирует практическое использование класса буфера обмена и служит
основой для создания собственного приложения для работы с липкими заметками.

Рисунок 9-2. Графический интерфейс Sticky Notes (Липкие заметки)

ГЛАВА 9 РАБОТА С БУФЕРОМ ОБМЕНА 239


Графический интерфейс Sticky Notes является хорошим проектом для ознакомления
с концепцией Single Document Interface (SDI). SDI - это метод организации
графических интерфейсов в отдельные окна, которые обрабатываются отдельно.
Несмотря на то, что приложение для работы с липкими записками позволяет
создавать несколько экземпляров графического интерфейса одновременно, каждое
окно является отдельным и независимым от других. Противоположностью является
Multiple Document Interface (MDI), в котором одно родительское окно содержит и
управляет несколькими вложенными друг в друга дочерними окнами. Пример MDI
приведен в Главе 12.

Пояснения к графическому интерфейсу Sticky Notes


Окно Sticky Notes (липкие заметки) относительно простое и состоит из одного
виджета QTextEdit, который служит центральным виджетом. Строка меню позволяет
создать новую заметку, очистить текст в виджете QTextEdit, выйти из приложения,
изменить цвет фона и вставить текст из буфера обмена.
Начиная со сценария main_window_template.py из Главы 5, мы импортируем
необходимые нам классы в Листинге 9-5. Поскольку это окно содержит строку меню,
не забудьте импортировать QAction для создания действий для меню.
Для этой программы класс MainWindow заменен на StickyNote. Менять его не
обязательно, но это лишь тонкое указание на то, что каждый экземпляр StickyNote
является собственным окном.
Листинг 9-5. Настройка главного окна для графического интерфейса Sticky Notes
# sticky_notes.py
# Импорт необходимых модулей
import sys
from PyQt6.QtWidgets import (QApplication, QMainWindow,
QTextEdit)
from PyQt6.QtGui import QAction

class StickyNote(QMainWindow):
# Переменные класса, общие для всех экземпляров
note_id = 1
notes = []

def __init__(self, note_ref=str()):


super().__init__()
self.initializeUI()

def initializeUI(self):
"""Настройка графического интерфейса приложения."""
self.setMinimumSize(250, 250)

240 ГЛАВА 9 РАБОТА С БУФЕРОМ ОБМЕНА


self.setWindowTitle("9.1 - Графический интерфейс Sticky Notes")
self.setUpMainWindow()
self.createActions()
self.createMenu()
self.createClipboard()
self.show()

if __name__ == "__main__":
app = QApplication(sys.argv)
window = StickyNote()
sys.exit(app.exec())

Класс StickyNote включает в себя две переменные класса: note_id, используемую


для присвоения уникального имени и ссылки на каждое новое окно, и notes,
позволяющую отслеживать новые открываемые окна путем добавления их в список.
Переменные класса являются общими для всех экземпляров класса и управляются
QApplication.
В Листинге 9-6 в качестве центрального виджета установлен виджет QTextEdit.
Кроме того, при создании новой заметки ее экземпляр добавляется в список заметок.
Листинг 9-6. Код метода SetUpMainWindow() для графического интерфейса Sticky
Notes
# sticky_notes.py
def setUpMainWindow(self):
"""Создаем и располагаем виджеты в главном окне."""
self.notes.append(self)
self.central_tedit = QTextEdit()
self.setCentralWidget(self.central_tedit)

В Листинге 9-7 построены действия для экземпляра StickyNote. Строка меню


включает действия по созданию нового экземпляра StickyNote, изменению цвета фона
и вставке текста. За помощью в настройке строк меню и действий можно обратиться
к Главе 5.
Листинг 9-7. Код метода createActions() для графического интерфейса Sticky Notes
# sticky_notes.py
def createActions(self):
"""Создание действий меню приложения."""
# Создание действий для меню Файл
self.new_note_act = QAction("Новая запись", self)
self.new_note_act.setShortcut("Ctrl+N")
self.new_note_act.triggered.connect(self.newNote)
self.close_act = QAction("Очистить", self)
self.close_act.setShortcut("Ctrl+W")
self.close_act.triggered.connect(self.clearNote)
ГЛАВА 9 РАБОТА С БУФЕРОМ ОБМЕНА 241
self.quit_act = QAction("Выход", self)
self.quit_act.setShortcut("Ctrl+Q")
self.quit_act.triggered.connect(self.close)

# Создание действий для меню Цвет


self.yellow_act = QAction("Жёлтый", self)
self.yellow_act.triggered.connect(
lambda: self.changeBackground(
self.yellow_act.text()))
self.blue_act = QAction("Синий", self)
self.blue_act.triggered.connect(
lambda: self.changeBackground(
self.blue_act.text()))
self.green_act = QAction("Зелёный", self)
self.green_act.triggered.connect(
lambda: self.changeBackground(
self.green_act.text()))

# Создание действий для меню Вставить


self.paste_act = QAction("Вставить", self)
self.paste_act.setShortcut("Ctrl+V")
self.paste_act.triggered.connect(
self.pasteToClipboard)

В зависимости от системы в оболочке могут появляться предупреждения о


несовпадении ключей. Это связано с различными правилами использования
кодов виртуальных клавиш, установленными для каждой платформы. Также
предупреждения могут возникать из-за жестко закодированных привязок клавиш
для классов виджетов ввода Qt. Если вы видите ошибку, не стоит беспокоиться. В
данном примере действия все равно будут выполняться в соответствии с указанными
клавишами быстрого доступа.
В Листинге 9-8 создаются меню Файл, Цвет и Вставить и их опции.
Листинг 9-8. Код метода createMenu() для графического интерфейса Sticky Notes
# sticky_notes.py
def createMenu(self):
"""Создаем строку меню приложения."""
self.menuBar().setNativeMenuBar(False)

# Создаем меню File и добавляем действия


file_menu = self.menuBar().addMenu("Файл")
file_menu.addAction(self.new_note_act)
file_menu.addAction(self.close_act)
file_menu.addAction(self.quit_act)

242 ГЛАВА 9 РАБОТА С БУФЕРОМ ОБМЕНА


# Создание меню Color и добавление действий
color_menu = self.menuBar().addMenu("Цвет")
color_menu.addAction(self.yellow_act)
color_menu.addAction(self.blue_act)
color_menu.addAction(self.green_act)

# Создание меню Paste и добавление действий


paste_menu = self.menuBar().addMenu("Вставить")
paste_menu.addAction(self.paste_act)

Меню Цвет позволяет пользователю выбрать цвет фона для каждой заметки.
Если пользователь хочет вставить текст из буфера обмена в виджет, он может
воспользоваться либо пунктом меню Вставить, либо горячей клавишей Ctrl+V.
Метод createClipboard() создает объект буфера обмена, а слот copyToClipboard()
в Листинге 9-9 срабатывает при изменении данных в буфере обмена по сигналу
dataChanged.
Листинг 9-9. Код для настройки буфера обмена в графическом интерфейсе Sticky
Notes
# sticky_notes.py
def createClipboard(self):
"""Устанавливаем буфер обмена."""
self.clipboard = QApplication.clipboard()
self.clipboard.dataChanged.connect(
self.copyToClipboard)
self.mime_data = self.clipboard.mimeData()

def copyToClipboard(self):
"""Получить содержимое системного буфера обмена."""
self.mime_data = self.clipboard.mimeData()

def newNote(self):
"""Создаем новый экземпляр класса StickyNote."""
StickyNote().show()
self.note_id += 1

Переменная mime_data хранит текущие данные, скопированные в буфер обмена,


и обновляется при их изменении. В функции newNote() создается новый экземпляр
заметки. С помощью функции show() этот новый экземпляр StickyNote появляется на
экране. Каждой новой заметке при ее создании присваивается номер, note_id. Задача
состоит в том, чтобы найти метод PyQt, позволяющий указать двумерное место, где
будет появляться новое окно.
Другие слоты в Листинге 9-10 позволяют очистить текст, изменить цвет фона
central_tedit или проверить, есть ли в mime_data текст, и, если есть, вставить его в
central_tedit.
ГЛАВА 9 РАБОТА С БУФЕРОМ ОБМЕНА 243
Листинг 9-10. Дополнительные слоты для пунктов меню в графическом
интерфейсе Sticky Notes
# sticky_notes.py
def clearNote(self):
"""Удалить текст текущей заметки."""
self.central_tedit.clear()

def changeBackground(self, color_text):


"""Изменить цвет фона заметки."""
if color_text == "Жёлтый":
self.central_tedit.setStyleSheet(
"background-color: rgb(248, 253, 145)")
elif color_text == "Синий":
self.central_tedit.setStyleSheet(
"background-color: rgb(145, 253, 251)")
elif color_text == "Зелёный":
self.central_tedit.setStyleSheet(
"background-color: rgb(148, 253, 145)")

def pasteToClipboard(self):
"""Получает содержимое системного буфера обмена и
вставить в заметку."""
if self.mime_data.hasText():
self.central_tedit.paste()

Графический интерфейс Sticky Notes демонстрирует один из практических


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

Резюме
Класс QClipboard позволяет GUI-приложениям получать и отправлять данные из
системного буфера обмена. Класс QMimeData обрабатывает различные типы данных
как для буфера обмена, так и для систем перетаскивания, обеспечивая корректную
работу с данными.
Многие виджеты PyQt, предназначенные для редактирования текста, уже
содержат возможность взаимодействия с буфером обмена, поэтому часто включать
код для работы с буфером обмена в свою программу не потребуется.
В следующей главе вы познакомитесь с классами Qt для работы с данными. Вы
также узнаете, как применять в графических интерфейсах функцию drag-and-drop
(перетаскивание), чтобы можно было передавать данные между виджетами и
другими программами.

244 ГЛАВА 9 РАБОТА С БУФЕРОМ ОБМЕНА


ГЛАВА 10

Представление данных
в PyQt
Как разработчик графических интерфейсов, вы, вероятно, в какой-то момент
обнаружите, что ищете способ представления данных, текстовых или визуальных,
в своем интерфейсе. Прежде чем приступать к этой работе, необходимо помнить
о пользователе и цели приложения. Во многих случаях методы представления
графических данных уже соответствуют различным стандартам. Многие
пользователи уже имеют представление о том, как должны отображаться данные в
таблицах или как добавлять и удалять элементы в списке. К счастью, Qt уже учла это
и создала несколько классов, которые позволяют быстрее и проще решать задачу
представления данных.
В этой главе вы
• Начнете думать о том, как работать с данными в PyQt
• Создадите графический интерфейс для каждого из классов удобства на
основе элементов. - QListWidget, QTableWidget и QTreeWidget.
• Добавите в графический интерфейс функций перетаскивания
• Создадите контекстные меню для отображения ярлыков.
Начнем с того, что выясним, на что способны удобные классы.

Быстрая работа с данными в PyQt


Изучение и сбор данных - большое дело, тем более что информация способна
улучшить жизнь людей, помочь в принятии решений, найти пути решения и т.д.
Процесс организации и визуализации данных упрощается еще больше благодаря
паттерну проектирования Qt "Model/View". Более подробно эта тема рассматривается
в Главе 14, но здесь важно понять, что модель и представление работают вместе
для организации, управления и представления данных в графическом интерфейсе.
Модели используются для управления данными, а представления - для их
отображения в графическом интерфейсе.
В этой главе мы сосредоточимся на удобных классах, которые являются
производными от классов Model/View. Хотя это означает, что у вас будет меньше
возможностей для настройки и гибкости, удобные классы все же предоставляют
общие функциональные возможности, которые вам понадобятся прямо из коробки.

ГЛАВА 10 ПРЕДСТАВЛЕНИЕ ДАННЫХ В PYQT 245


Это может быть особенно полезно, когда настройка не требуется для вашего
приложения.
Три удобных класса используют предопределенные модели и представления
со всеми стандартными стилями, возможностями и функциями, которые можно
ожидать от общего интерфейса, основанного на элементах.
Информацию о программировании на основе моделей и представлений можно
найти на сайте https://doc.qt.io/qt-6/model-view-programming.html.

Класс QListWidget
Класс QListWidget создает виджет, отображающий один столбец элементов, что
упрощает добавление и удаление элементов. Элементы могут быть добавлены либо
при создании виджета в коде, либо вставлены позже через графический интерфейс.
Элементы во всех трех удобных классах создаются с помощью специальных
классов. Класс QListWidgetItem используется совместно с QListWidget в качестве
элемента, который может быть использован со списком. На рис. 10-1 показан
графический интерфейс пользователя, который мы собираемся создать для этого
примера.

Рисунок 10-1. QListWidget может быть использован для отображения объектов в


инвентаре или элементов в каталоге
Класс QListWidget включает в себя различные методы для создания и
манипулирования данными в списке, в том числе
• addItem(QListWidgetItem) - Добавляет элемент в конец списка
• currentRow() - Возвращает значение индекса текущей выбранной строки
• insertItem(row, QListWidgetItem) - Вставляет элемент в указанную строку
• takeItem(row) - Удаляет элемент из указанной строки
• clear() - Удаление всех элементов из списка

246 ГЛАВА 10 ПРЕДСТАВЛЕНИЕ ДАННЫХ В PYQT


Пояснения к использованию QListWidget
Следующий пример кратко демонстрирует, как добавлять, вставлять, удалять
и очищать все элементы из виджета QListWidget. Для начала с помощью сценария
basic_window.py из Главы 1 создайте новый сценарий и установите класс MainWindow
и метод initializeUI() в Листинге 10-1.
Листинг 10-1. Настройка класса MainWindow для примера QListWidget
# list_widget.py
# Импорт необходимых модулей
import sys
from PyQt6.QtWidgets import (QApplication, QWidget,
QPushButton, QListWidget, QListWidgetItem, QInputDialog,
QHBoxLayout, QVBoxLayout,)

class MainWindow(QWidget):

def __init__(self):
super().__init__()
self.initializeUI()

def initializeUI(self):
"""Настройка графического интерфейса приложения."""
self.setMinimumSize(400, 200)
self.setWindowTitle("Пример QListWidget")
self.setUpMainWindow()
self.show()

if __name__ == "__main__":
app = QApplication(sys.argv)
window = MainWindow()
sys.exit(app.exec())

Обязательно включите импорт QListWidget и QListWidgetItem из QtWidgets.


QInputDialog будет использоваться при добавлении или вставке новой строки в
QListWidget.
В Листинге 10-2 построен метод setUpMainWindow() для класса MainWindow.
QListWidget используется для управления элементами данных, отображаемыми в
окне графического интерфейса. Для чередования цветов строк установите значение
метода setAlternatingRowColors() равным True.

ГЛАВА 10 ПРЕДСТАВЛЕНИЕ ДАННЫХ В PYQT 247


Листинг 10-2. Создание метода setUpMainWindow() для примера QListWidget
# list_widget.py
def setUpMainWindow(self):
"""Создаем и располагаем виджеты в главном окне."""
self.list_widget = QListWidget()
self.list_widget.setAlternatingRowColors(True)
# Инициализация QListWidget с элементами
grocery_list = ["виноград", "брокколи", "чеснок",
"сыр", "бекон", "яйца", "вафли",
"рис", "газировка"]
for item in grocery_list:
list_item = QListWidgetItem()
list_item.setText(item)
self.list_widget.addItem(list_item)

# Создание кнопок для взаимодействия с элементами


add_button = QPushButton("Добавить")
add_button.clicked.connect(self.addListItem)
insert_button = QPushButton("Вставить")
insert_button.clicked.connect(self.insertItemInList)
remove_button = QPushButton("Удалить")
remove_button.clicked.connect(self.removeOneItem)
clear_button = QPushButton("Очистить")
clear_button.clicked.connect(self.list_widget.clear)

# Создание макетов
right_v_box = QVBoxLayout()
right_v_box.addWidget(add_button)
right_v_box.addWidget(insert_button)
right_v_box.addWidget(remove_button)
right_v_box.addWidget(clear_button)
main_h_box = QHBoxLayout()
main_h_box.addWidget(self.list_widget)
main_h_box.addLayout(right_v_box)
self.setLayout(main_h_box)

Для отображения строковых элементов из списка grocery_list в list_widget


необходимо создать объект QListWidgetItem для каждого элемента, задать его текст
с помощью setText() и использовать метод addItem() для добавления элемента в
list_widget. Эти элементы будут заполнять список при запуске программы. Здесь мы
просто передаем текст в QListWidgetItem, но можно передать и значок.
Далее создадим кнопки для каждого из действий, которые могут быть выполнены
над QListWidget, и подключим эти кнопки к сигналу щелчка. При нажатии на кнопку
будет выдаваться сигнал, который соединяется со слотом для редактирования
элементов данных в QListWidget.

248 ГЛАВА 10 ПРЕДСТАВЛЕНИЕ ДАННЫХ В PYQT


Кнопки добавляются в QVBoxLayout, который затем вместе с list_widget размещается
в QHBoxLayout главного окна.
В Листинге 10-3 позаботились о создании слота, вызываемого командой
add_button.
Листинг 10-3. Код для слота addListItem()
# list_widget.py
def addListItem(self):
"""Добавляем один элемент в виджет списка."""
text, ok = QInputDialog.getText(
self, "Новый элемент", "Добавить элемент:")
if ok and text != "":
list_item = QListWidgetItem()
list_item.setText(text)
self.list_widget.addItem(list_item)

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


QInputDialog, подобный тому, что показан на рис. 10-2.

Рисунок 10-2. Добавление нового элемента в QListWidget с помощью QInputDialog


Если текст был введен и пользователь нажал кнопку OK в диалоге, то новый
элемент добавляется в конец list_widget с помощью функции addItem().
С помощью кнопок, расположенных справа от главного окна, пользователь может
также вставлять, удалять или очищать элементы списка. Для этого нам потребуется
создать два дополнительных слота, insertItemInList() и removeOneItem(), о которых идет
речь в Листинге 10-4. Для кнопки clear_button сигнал нажатия просто подключается
к методу QListWidget clear().
Листинг 10-4. Оставшиеся слоты для класса MainWindow в примере QListWidget
# list_widget.py
def insertItemInList(self):
"""Вставляет один элемент в виджет списка под
текущей выбранной строкой. """
text, ok = QInputDialog.getText(
self, "Вставить элемент", "Вставить элемент:")
if ok and text != "":
ГЛАВА 10 ПРЕДСТАВЛЕНИЕ ДАННЫХ В PYQT 249
row = self.list_widget.currentRow()
row = row + 1 # Выбор строки ниже текущей строки
new_item = QListWidgetItem()
new_item.setText(text)
self.list_widget.insertItem(row, new_item)

def removeOneItem(self):
"""Удаление одного элемента из виджета списка."""
row = self.list_widget.currentRow()
item = self.list_widget.takeItem(row)
del item

Когда пользователь захочет вставить новый элемент данных, появится диалоговое


окно QInputDialog. Если пользователь нажимает кнопку OK, то с помощью функции
currentRow() определяется текущий выбранный ряд. Затем значение row увеличивается
на 1, создается новый QListWidgetItem и этот новый элемент вставляется под текущей
выбранной строкой. При удалении ряда снова используется функция currentRow(),
чтобы определить, какой ряд выбран. Для удаления элемента из QListWidget
используется метод takeItem(). Ключевое слово del используется для окончательного
удаления элемента, поскольку takeItem() не удаляет элементы.
В следующем разделе мы продолжим использовать QListWidget и узнаем, как
расширить возможности виджета с помощью функции перетаскивания.

Перетаскивание в PyQt
Механизм drag-and-drop (перетаскивание) позволяет пользователю выполнять
задачи в графическом интерфейсе, выбирая элементы, например, пиктограммы
или изображения, и перемещая их в другое окно или на другой объект. PyQt также
позволяет легко включить это поведение в приложение. Для того чтобы виджеты
обладали базовой функциональностью перетаскивания, достаточно установить
значения методов setAcceptDrops() и setDragEnabled() в True.
В PyQt включив функцию перетаскивания, можно перемещать элементы из одного
объекта текстового редактора, списка или таблицы в другой. QMimeData также
может быть использован для управления тем, какие данные можно перемещать,
перетаскивать или опускать.
На рис. 10-3 показан графический интерфейс, который вы будете создавать в этом
разделе. Элементы в окне можно перетаскивать туда-сюда между двумя экземплярами
QListWidget. Обязательно загрузите папку images из репозитория GitHub для этого
проекта.

250 ГЛАВА 10 ПРЕДСТАВЛЕНИЕ ДАННЫХ В PYQT


Рисунок 10-3. Два объекта QListWidget, используемые для демонстрации
перетаскивания

Совет. Механика перетаскивания может быть применена к множеству


различных виджетов, а не только к QListWidget. Чтобы узнать, какие виджеты
уже имеют встроенные возможности перетаскивания, следует обратиться к
документации PyQt или Qt.

Пояснения к перетаскиванию
Начнем со сценария basic_window.py и модифицируем класс MainWindow в
Листинге 10-5. В этом примере мы продолжим использовать классы QListWidget и
QListWidgetItem для демонстрации перетаскивания.
Листинг 10-5. Создание класса MainWindow для примера с перетаскиванием
# drag_drop.py
# Импорт необходимых модулей
import sys, os
from PyQt6.QtWidgets import (QApplication, QWidget, QLabel,
QListWidget, QListWidgetItem, QGridLayout)
from PyQt6.QtCore import QSize
from PyQt6.QtGui import QIcon

ГЛАВА 10 ПРЕДСТАВЛЕНИЕ ДАННЫХ В PYQT 251


class MainWindow(QWidget):

def __init__(self):
super().__init__()
self.initializeUI()

def initializeUI(self):
"""Настройка графического интерфейса приложения."""
self.setMinimumSize(500, 300)
self.setWindowTitle("Пример перетаскивания")
self.setUpMainWindow()
self.show()

if __name__ == "__main__":
app = QApplication(sys.argv)
window = MainWindow()
sys.exit(app.exec())

В Листинге 10-6 создано два экземпляра класса QListWidget - icon_widget и


list_widget. Элементы в QListWidget могут отображаться либо в виде пиктограмм,
либо в виде текста в списке. Объект icon_widget отображает пиктограммы с помощью
флага IconMode. По умолчанию отображаются элементы в виде списка.
Листинг 10-6. Код метода setUpMainWindow() в примере с перетаскиванием
def setUpMainWindow(self):
"""Создание и расположение виджетов в главном окне."""
icon_label = QLabel("ЗНАЧКИ", self)
icon_widget = QListWidget()
icon_widget.setAcceptDrops(True)
icon_widget.setDragEnabled(True)
icon_widget.setViewMode(
QListWidget.ViewMode.IconMode)
image_path = "images"
for img in os.listdir(image_path):
list_item = QListWidgetItem()
list_item.setText(img.split(".")[0])
list_item.setIcon(QIcon(os.path.join(image_path,
"{0}").format(img)))
icon_widget.setIconSize(QSize(50, 50))
icon_widget.addItem(list_item)

list_label = QLabel("СПИСОК", self)


list_widget = QListWidget()
list_widget.setAlternatingRowColors(True)
list_widget.setAcceptDrops(True)
252 ГЛАВА 10 ПРЕДСТАВЛЕНИЕ ДАННЫХ В PYQT
list_widget.setDragEnabled(True)

# создание макета сетки


grid = QGridLayout()
grid.addWidget(icon_label, 0, 0)
grid.addWidget(list_label, 0, 1)
grid.addWidget(icon_widget, 1, 0)
grid.addWidget(list_widget, 1, 1)
self.setLayout(grid)

Чтобы настроить возможность перетаскивания для виджета icon_widget,


установите для методов setAcceptDrops() и setDragEnabled() значения True. Метод
setAcceptDrops() позволяет принимать события падения на виджет, а setDragEnabled()
позволяет перетаскивать элементы внутрь и наружу виджета. Эти методы фактически
наследуются от QWidget, поэтому большинство классов, наследующих QWidget, также
будут иметь доступ к функциям перетаскивания.
В начале работы программы элементами из папки images будет заполнен только
виджет QListWidget icon_widget. Хотя методы setText() и setIcon() вызываются для
применения текста и пиктограмм к экземплярам QListWidgetItem, эти значения
также могут быть переданы в качестве аргументов при инстанцировании объекта
QListWidgetItem.
Далее повторим процесс для list_widget, но не будем менять режим просмотра
виджета. Когда один из значков, загруженных в icon_widget, перетаскивается
на list_widget, список обновляет свое содержимое, включая новый элемент.
Перетаскивание элемента из одного QListWidget в другой добавляет новый элемент в
этот список.
В следующем разделе будет рассмотрен удобный класс для создания таблиц в
PyQt.

Класс QTableWidget
Класс QTableWidget предоставляет средства для отображения и организации
данных в табличной форме, представляя информацию в виде строк и столбцов.
Использование таблиц позволяет упорядочить данные в более удобном для чтения
формате. Пример таблиц PyQt показан на рис. 10-4.

ГЛАВА 10 ПРЕДСТАВЛЕНИЕ ДАННЫХ В PYQT 253


Рисунок 10-4. Пример таблицы из класса QTableWidget
QTableWidget предоставляет стандартные средства, необходимые для создания
таблиц, включая возможность редактирования ячеек, установки количества строк и
столбцов, добавления вертикальных и горизонтальных надписей-заголовков. Можно
также скрыть заголовки, если они не должны быть видны. QTableWidget также имеет
ряд сигналов для проверки того, что ячейки или элементы были щелкнуты, дважды
щелкнуты или даже изменены.
В этом первом примере мы рассмотрим, как использовать QTableWidget для
создания основы приложения для редактирования электронных таблиц. Кроме того,
это приложение научит вас создавать контекстное меню для работы с содержимым
виджета таблицы.

Пояснения к использованию QTableWidget


Для этого приложения начните с создания класса MainWindow в Листинге 10-7,
используя сценарий main_window_template.py из Главы 5. Обязательно импортируйте
QTableWidget и QTableWidgetItem, который используется для создания элементов
виджета таблицы. Класс QMenu будет использоваться для создания контекстных
меню в графическом интерфейсе.
Листинг 10-7. Настройка класса MainWindow для примера QTableWidget
# table_widget.py
# Импорт необходимых модулей
import sys
from PyQt6.QtWidgets import (QApplication, QMainWindow,
QTableWidget, QTableWidgetItem, QMenu, QInputDialog)
from PyQt6.QtGui import QAction

254 ГЛАВА 10 ПРЕДСТАВЛЕНИЕ ДАННЫХ В PYQT


class MainWindow(QMainWindow):

def __init__(self):
super().__init__()
self.initializeUI()

def initializeUI(self):
"""Настройка графического интерфейса приложения."""
self.setMinimumSize(1000, 500)
self.setWindowTitle(
"Электронная таблица - пример QTableWidget")

# Используется для копирования и вставки


self.item_text = None
self.setUpMainWindow()
self.createActions()
self.createMenu()
self.show()

if __name__ == "__main__":
app = QApplication(sys.argv)
window = MainWindow()
sys.exit(app.exec())

Обязательно создайте переменную экземпляра item_text, в которой будет


храниться текст для действий копирования и вставки. Виджет QTableWidget,
создающий электронную таблицу графического интерфейса, создан в Листинге 10-8
в функции setUpMainWindow().
Листинг 10-8. Создание метода setUpMainWindow() для примера QTableWidget
# table_widget.py
def setUpMainWindow(self):
"""Создаем и располагаем виджеты в главном окне."""
self.table_widget = QTableWidget()
# Установить начальные значения строк и столбцов
self.table_widget.setRowCount(10)
self.table_widget.setColumnCount(10)
# Установить фокус на ячейку в таблице
self.table_widget.setCurrentCell(0, 0)
# При двойном щелчке на горизонтальных заголовках,
# выдается сигнал
h_header = self.table_widget.horizontalHeader()
h_header.sectionDoubleClicked.connect(
self.changeHeader)
self.setCentralWidget(self.table_widget)
ГЛАВА 10 ПРЕДСТАВЛЕНИЕ ДАННЫХ В PYQT 255
При инстанцировании объекта QTableWidget можно передать ему в качестве
параметров количество строк и столбцов, как это сделано в следующей строке:
table_widget = QTableWidget(10, 10)
Или можно построить таблицу с помощью методов setRowCount() и
setColumnCount(). Экземпляр table_widget будет начинаться с десяти строк и десяти
столбцов.
Метод setCurrentCell() может быть использован для установки фокуса на
определенную ячейку таблицы.
Классы QTableWidget и QTreeWidget имеют заголовки, а QListWidget - нет. Для
доступа к заголовкам таблицы можно вызвать либо функцию horizontalHeader() для
горизонтальных заголовков, либо verticalHeader() для вертикальных. Изменение
меток заголовков в QTableWidget может быть выполнено либо непосредственно в коде,
либо с помощью несколько косвенного подхода. Заголовки для таблиц создаются
с помощью QHeaderView в классе QTableView. Более подробно мы рассмотрим
QTableView и другие классы представлений в Главе 14.
Поскольку QTableWidget наследуется от класса QTableView, мы также имеем доступ
к его функциям. Зная это, мы можем получить объект QHeaderView с помощью
table_widget.horizontalHeader(). Отсюда мы можем подключиться к сигналу класса
QHeaderView - sectionDoubleClicked. Этот сигнал может быть использован для проверки
того, дважды ли пользователь щелкнул на секции заголовка. Если это так, то сигнал
запускает слот changeHeader() (созданный в Листинге 10-12).
Строка меню, показанная на рис. 10-4, содержит два меню: Файл и Таблица. Файл
содержит действие выхода из приложения. Таблица содержит действия по добавлению
и удалению строк и столбцов. Мы настроим эти действия в Листинге 10-9.
Листинг 10-9. Код метода createActions() в примере QTableWidget
# table_widget.py
def createActions(self):
"""Создать действия меню приложения."""
# Создание действий для меню Файл
self.quit_act = QAction("Выход", self)
self.quit_act.setShortcut("Ctrl+Q")
self.quit_act.triggered.connect(self.close)
# Создание действий для меню Таблица
self.add_row_above_act = QAction(
"Добавить строку выше", self)
self.add_row_above_act.triggered.connect(
self.addRowAbove)
self.add_row_below_act = QAction(
"Добавить строку ниже", self)
self.add_row_below_act.triggered.connect(
self.addRowBelow)
self.add_col_before_act = QAction(
"Добавить столбец перед", self)
256 ГЛАВА 10 ПРЕДСТАВЛЕНИЕ ДАННЫХ В PYQT
self.add_col_before_act.triggered.connect(
self.addColumnBefore)
self.add_col_after_act = QAction(
"Добавить столбец после", self)
self.add_col_after_act.triggered.connect(
self.addColumnAfter)
self.delete_row_act = QAction("Удалить строку", self)
self.delete_row_act.triggered.connect(self.deleteRow)
self.delete_col_act = QAction("Удалить столбец", self)
self.delete_col_act.triggered.connect(
self.deleteColumn)
self.clear_table_act = QAction("Очистить все", self)
self.clear_table_act.triggered.connect(
self.clearTable)

Слоты, к которым подключается каждое действие, созданы в Листинге 10-14.


Пункты меню созданы в Листинге 10-10. Дополнительные сведения о создании
действий и меню см. в главе 5.
Листинг 10-10. Код метода createMenu() в примере QTableWidget
# table_widget.py
def createMenu(self):
"""Создаем строку меню приложения."""
self.menuBar().setNativeMenuBar(False)
# Создаем файловое меню и добавляем действия
file_menu = self.menuBar().addMenu('File')
file_menu.addAction(self.quit_act)
# Создание меню таблицы и добавление действий
table_menu = self.menuBar().addMenu('Table')
table_menu.addAction(self.add_row_above_act)
table_menu.addAction(self.add_row_below_act)
table_menu.addSeparator()
table_menu.addAction(self.add_col_before_act)
table_menu.addAction(self.add_col_after_act)
table_menu.addSeparator()
table_menu.addAction(self.delete_row_act)
table_menu.addAction(self.delete_col_act)
table_menu.addSeparator()
table_menu.addAction(self.clear_table_act)

Контекстное меню и его действия будут сформированы в следующем разделе.

ГЛАВА 10 ПРЕДСТАВЛЕНИЕ ДАННЫХ В PYQT 257


Создание контекстных меню
В этом приложении также рассказывается о создании контекстного меню, иногда
называемого всплывающим, которое появляется в окне в результате взаимодействия
с пользователем, например, при нажатии правой кнопки мыши. Контекстное меню
отображает список общих команд, таких как Back Page (Вернуться на страницу) или
Reload Page(Перезагрузить страницу). Контекстные меню могут быть настроены и
для управления конкретными виджетами.
Поскольку контекстные меню вызываются событиями, мы можем реализовать
обработчик события contextMenuEvent(). Простой пример показан в следующем
блоке кода:

def contextMenuEvent(self, event):


context_menu = QMenu(self)
context_menu.addAction(self.add_row_above_act)

Контекстное меню обычно создается с помощью QMenu. При этом можно либо
использовать существующие действия, созданные в строке меню или на панели
инструментов, либо создать новые. Пример контекстного меню данного приложения
показан на рис. 10-5.

Рисунок 10-5. Пример контекстного меню, в котором отображаются действия


по редактированию виджета таблицы
Для контекстного меню на рис. 10-5 включены все действия меню из Листинга 10-9
(кроме quit_act). Кроме того, специально для этого контекстного меню создаются
два дополнительных действия: copy_act и paste_act. Они обрабатываются в
Листинге 10-11.

258 ГЛАВА 10 ПРЕДСТАВЛЕНИЕ ДАННЫХ В PYQT


Листинг 10-11. Код обработчика события contextMenuEvent() в примере
QTableWidget
# table_widget.py
def contextMenuEvent(self, event):
"""Создание контекстного меню и дополнительных действий."""
context_menu = QMenu(self)
context_menu.addAction(self.add_row_above_act)
context_menu.addAction(self.add_row_below_act)
context_menu.addSeparator()
context_menu.addAction(self.add_col_before_act)
context_menu.addAction(self.add_col_after_act)
context_menu.addSeparator()
context_menu.addAction(self.delete_row_act)
context_menu.addAction(self.delete_col_act)
context_menu.addSeparator()
# Создание действий, специфичных для контекстного меню
copy_act = context_menu.addAction("Копировать")
paste_act = context_menu.addAction("Вставить")
context_menu.addSeparator()
context_menu.addAction(self.clear_table_act)
# Выполнение context_menu и возврат действия
# mapToGlobal() переводит положение координат окна в
# глобальные координаты экрана. Таким образом, мы можем
# определить, что щелчок правой кнопкой мыши произошел
# внутри графического интерфейса, и отобразить контекстное меню
action = context_menu.exec(
self.mapToGlobal(event.pos()))
# Проверка наличия выбранных в контекстном меню действий, которые
# не были созданы в строке меню
if action == copy_act:
self.copyItem()
if action == paste_act:
self.pasteItem()

Вывод контекстного меню осуществляется с помощью функции exec().


Возвращаемое им значение action может быть использовано для определения того,
были ли нажаты дополнительные действия в контекстном меню. Для получения
координат мыши на экране мы передаем в качестве аргумента self.mapToGlobal().
Положение мыши определяется с помощью event.pos().
Если action равно copy_act, то мы вызываем метод copyItem(). Для paste_act
вызывается метод pasteItem(). Они созданы в Листинге 10-13.

ГЛАВА 10 ПРЕДСТАВЛЕНИЕ ДАННЫХ В PYQT 259


Использование встроенных методов QTableWidget
для редактирования данных
В оставшихся листингах будут созданы различные слоты и методы в MainWindow.
В Листинге 10-12 мы создадим метод changeHeader(), который срабатывает при
двойном щелчке на заголовке столбца.
Чтобы получить текст для выбранного заголовка столбца, отображается
QInputDialog для получения от пользователя текста метки заголовка. Наконец,
элемент горизонтального заголовка устанавливается с помощью функции
setHorizontalHeaderItem().
Листинг 10-12. Код для слота changeHeader() в примере QTableWidget
# table_widget.py
def changeHeader(self):
"""Изменяем горизонтальные заголовки, возвращая текст
из диалога ввода."""
col = self.table_widget.currentColumn()
text, ok = QInputDialog.getText(
self, "Введите заголовок", "Текст заголовка:")
if ok and text != "":
self.table_widget.setHorizontalHeaderItem(
col, QTableWidgetItem(text))

Установка горизонтальных меток заголовка может быть выполнена либо


с помощью функции setHorizontalHeaderItem(), либо с помощью функции
setHorizontalHeaderLabels(). В вызовах методов для вертикальных заголовков можно
изменить Horizontal на Vertical.
С дополнительными методами в контекстном меню мы разберемся далее в
Листинге 10-13. Если выделенная ячейка не пуста, то мы копируем текст в item_text.
В методе pasteItem() происходит сбор текущей строки и столбца выделенной ячейки.
Затем мы вставляем данные с помощью функции setItem(). Действия копирования и
вставки можно также реализовать с помощью QClipboard.
Листинг 10-13. Код для методов copyItem() и pasteItem(), используемых
контекстным меню
# table_widget.py
def copyItem(self):
"""Если текущая выделенная ячейка не пуста,
сохраните текст."""
if self.table_widget.currentItem() != None:
text = self.table_widget.currentItem().text()
self.item_text = text

260 ГЛАВА 10 ПРЕДСТАВЛЕНИЕ ДАННЫХ В PYQT


def pasteItem(self):
"""Установить элемент для выделенной ячейки."""
if self.item_text != None:
row = self.table_widget.currentRow()
column = self.table_widget.currentColumn()
self.table_widget.setItem(
row, column, QTableWidgetItem(self.item_text))

Элементы в таблицу можно добавлять и программно, используя метод setItem().


Это позволяет указать значения строки и столбца, а также элемент для ячейки с
помощью QTableWidgetItem. В следующем коде элемент Kalani вставляется в строку
0 и столбец 0.
self.table_widget.setItem(1, 0, QTableWidgetItem("Kalani"))
QTableWidget включает несколько методов для манипулирования объектами
таблицы. В меню Таблица создаются действия, которые используют эти методы.
Эти действия вызывают слоты, использующие встроенные методы QTableWidget. В
следующем списке описано, как эти методы используются в графическом интерфейсе
и в Листинге 10-14:
• Добавление строк выше или ниже текущей выбранной строки с помощью
функции insertRow()
• Добавление столбцов до или после текущего выбранного столбца с помощью
insertColumn()
• Удаление текущей строки или столбца с помощью removeRow() или
removeColumn()
• Очистка всей таблицы, включая элементы и заголовки, с помощью функции
clear()
Листинг 10-14. Код для слотов, модифицирующих данные в примере QTableWidget
# table_widget.py
def addRowAbove(self):
current_row = self.table_widget.currentRow()
self.table_widget.insertRow(current_row)

def addRowBelow(self):
current_row = self.table_widget.currentRow()
self.table_widget.insertRow(current_row + 1)

def addColumnBefore(self):
current_col = self.table_widget.currentColumn()
self.table_widget.insertColumn(current_col)

def addColumnAfter(self):
current_col = self.table_widget.currentColumn()
self.table_widget.insertColumn(current_col + 1)
ГЛАВА 10 ПРЕДСТАВЛЕНИЕ ДАННЫХ В PYQT 261
def deleteRow(self):
current_row = self.table_widget.currentRow()
self.table_widget.removeRow(current_row)

def deleteColumn(self):
current_col = self.table_widget.currentColumn()
self.table_widget.removeColumn(current_col)

def clearTable(self):
self.table_widget.clear()

Доступ к элементам таблицы осуществляется по значениям их строк и столбцов.


Сначала нам нужно узнать, какая строка или столбец выбрана в данный момент.
Например, при нажатии кнопки add_row_above_act срабатывает сигнал, вызывающий
функцию addRowAbove(). Сначала мы узнаем выбранную строку с помощью функции
currentRow(). Затем на место текущей строки вставляется новая строка, в результате
чего все остальные строки смещаются вниз. Методы, работающие со столбцами,
используют метод currentColumn().
В последней программе будет представлен удобный класс QTreeWidget.

Класс QTreeWidget
Класс QTreeWidget имеет общие черты с QListWidget и QTableWidget. С одной
стороны, элементы данных могут отображаться в виде списка, аналогичного
QListWidget. С другой стороны, QTreeWidget также может отображать несколько
столбцов данных, но не в табличном формате.
Отличительной особенностью QTreeWidget является то, что класс может визуально
представлять взаимосвязи между данными в виде древовидной структуры. При этом
элемент дерева может быть родителем других элементов.
Графический интерфейс, который мы создадим в этом разделе, показан на рис. 10-6.

262 ГЛАВА 10 ПРЕДСТАВЛЕНИЕ ДАННЫХ В PYQT


Рисунок 10-6. Виджет QTreeWidget используется для представления данных в виде
древовидной структуры
Элементы, добавляемые в QTreeWidget, создаются из QTreeWidgetItem. Для элементов
на рис. 10-6 будет два родительских элемента для типов фруктов и несколько дочерних
элементов с пиктограммами, которые расположены под родительскими элементами.
Подобно QTableWidget, QTreeWidget также содержит горизонтальные заголовки
для каждого столбца. Однако вертикальные заголовки отсутствуют.
Сортировка элементов столбцов также возможна с помощью QTreeWidget.
Перед началом работы с этим приложением обязательно загрузите папку с
иконками с GitHub.

Пояснения к использованию QTreeWidget


Начнем со сценария basic_window.py из Главы 1, обновим импорты, установим
минимальный размер окна и заголовок для класса MainWindow в Листинге 10-15.
Листинг 10-15. Настройка класса MainWindow для примера QTreeWidget
# tree_widget.py
# Импорт необходимых модулей
import sys
from PyQt6.QtWidgets import (QApplication, QWidget,
QTreeWidget, QTreeWidgetItem, QVBoxLayout)
from PyQt6.QtGui import QIcon

class MainWindow(QWidget):

def __init__(self):
""" Конструктор для класса "Пустое окно" """
super().__init__()
self.initializeUI()

ГЛАВА 10 ПРЕДСТАВЛЕНИЕ ДАННЫХ В PYQT 263


def initializeUI(self):
"""Настраиваем графический интерфейс приложения."""
self.setMinimumSize(500, 300)
self.setWindowTitle("Пример QTreeWidget")
self.setUpMainWindow()
self.show()

if __name__ == '__main__':
app = QApplication(sys.argv)
window = MainWindow()
sys.exit(app.exec())

После создания объекта QTreeWidget в Листинге 10-16 нам необходимо задать


количество столбцов с помощью сеттера setColumnCount(). Далее зададим метки
для каждого из столбцов. Метод setHeaderLabels() принимает в качестве аргумента
итерируемый объект. Метод setColumnWidth() используется для установки
минимальной ширины указанного столбца, обеспечивающей четкое отображение
всех элементов. Здесь для столбца 0 установлена ширина 160 пикселей.
Листинг 10-16. Создание метода setUpMainWindow() для примера QTreeWidget,
часть 1
# tree_widget.py
def setUpMainWindow(self):
"""Создание и расположение виджетов в главном окне."""
tree_widget = QTreeWidget()
tree_widget.setColumnCount(2)
tree_widget.setHeaderLabels(
["Тип фрукта", "Описание"])
tree_widget.setColumnWidth(0, 160)
category_1 = QTreeWidgetItem(tree_widget, ["Яблоки", \
"Съедобные плоды, производимые яблоней"])
apple_list = [
["Braeburn", "Желтый с красными полосами или румянцем", \
"icons/braeburn.png"],
["Empire", "Сплошной красный", "icons/empire.png"],
["Имбирное золото", "Зелено-желтый", \
"icons/ginger_gold.png"]]
for i in range(len(apple_list)):
category_1_child = QTreeWidgetItem(
apple_list[i][:2])
category_1_child.setIcon(
0, QIcon(apple_list[i][2]))
category_1.addChild(category_1_child)

264 ГЛАВА 10 ПРЕДСТАВЛЕНИЕ ДАННЫХ В PYQT


Экземпляр category_1 представляет собой QTreeWidgetItem для первого
родительского элемента в дереве. В элемент передается родительский виджет
tree_widget, а также список информации для двух колонок. Далее следует apple_list
- список списков. Каждый список содержит тип яблока, соответствующий первому
столбцу, описание, которое будет отображаться во втором столбце, и иконку,
которая отображается рядом с названием элемента. Затем каждый category_1_child
превращается в QTreeWidgetItem, его иконка устанавливается и, наконец, добавляется
к родительскому элементу category_1 в цикле Python for.
Аналогичным образом создается второй родительский элемент и его дочерние
элементы в Листинге 10-17.
Листинг 10-17. Создание метода setUpMainWindow() для примера QTreeWidget,
часть 2
# tree_widget.py
category_2 = QTreeWidgetItem(tree_widget,
["Апельсины", "Вид цитрусовых"])
orange_list = [
["Navel", "Сладкий и слегка горький", \
"icons/navel.png"],
["Кровавый апельсин", "Сочный и терпкий", \
"icons/blood_orange.png"],
["Клементин", "Обычно без косточек", \
"icons/clementine.png"]]
for i in range(len(orange_list)):
category_2_child = QTreeWidgetItem(
orange_list[i][:2])
category_2_child.setIcon(
0, QIcon(orange_list[i][2]))
category_2.addChild(category_2_child)
main_v_box = QVBoxLayout()
main_v_box.addWidget(tree_widget)
self.setLayout(main_v_box)

После создания элементов tree_widget добавляется в макет главного окна.


После создания этого графического интерфейса вы получили опыт создания
приложений для каждого из удобных классов Model/View.

ГЛАВА 10 ПРЕДСТАВЛЕНИЕ ДАННЫХ В PYQT 265


Резюме
В этой главе мы рассмотрели классы удобства, основанные на элементах,
которые следуют стандартным методам представления данных. Элементы обычно
представляются в виде списка, таблицы или дерева. Мы познакомились с QListWidget,
QTableWidget и QTreeWidget и узнали, как использовать некоторые из их возможностей
для создания уникальных и практичных графических интерфейсов. Все классы
виджетов, основанных на элементах, а также многие другие классы, наследующие
QWidget, имеют возможность перетаскивания. Эта система очень полезна, поскольку
позволяет еще больше упростить перемещение данных между графическими
элементами графического интерфейса.
В следующей главе мы рассмотрим графические аспекты графических интерфейсов
и начнем разбираться с тем, как можно добавить анимацию и цвет в приложения.

266 ГЛАВА 10 ПРЕДСТАВЛЕНИЕ ДАННЫХ В PYQT


ГЛАВА 11

Графика и анимация в
PyQt
Разрабатывая графические интерфейсы, вы обнаружите, что можете проявить
свою творческую и художественную сторону, рисуя и анимируя виджеты. Поскольку
графика является очень важной частью графических приложений, в этой главе
мы рассмотрим только темы, связанные с двумерной графикой, включая линии,
формы, анимацию и рисование. Если вы заинтересованы в создании графических
интерфейсов, работающих с 3D-изображениями, то в Qt также имеется поддержка
различных графических API, включая OpenGL и Vulkan. Если вас действительно
интересует графика в Qt6, то посмотрите на https://doc.qt.io/qt-6/topics-graphics.html.
В этой главе вы
• Познакомитесь с QPainter и другими классами, используемыми для рисования
в Qt
• Создадите подсказки с помощью QToolTip
• Воспользуетесь QPropertyAnimation для анимации виджетов
• Анимируете объекты с помощью QPropertyAnimation и pyqtProperty
• Узнаете, как использовать QGraphicsView для построения графической сцены
Давайте ныачнем с одного из самых важных классов в PyQt.

Введение в класс QPainter


Графика в Qt создается в основном с помощью API QPainter. Система рисования
Qt управляет рисованием текста, изображений и векторной графики и может быть
выполнена на различных поверхностях, таких как QImage, QWidget и QPrinter. С по-
мощью QPainter можно улучшить внешний вид существующих виджетов или даже
создать свой собственный.
Основными компонентами системы рисования в PyQt являются классы QPainter,
QPaintDevice и QPaintEngine. QPainter выполняет операции рисования; QPaintDevice -
это абстракция двумерного пространства, выступающая в качестве поверхности, на
которой может рисовать QPainter; QPaintEngine - это внутренний интерфейс, исполь-
зуемый классами QPainter и QPaintDevice для рисования.
Всякий раз, когда вам нужно нарисовать что-либо в PyQt, вам, скорее всего, при-
дется работать с классом QPainter. QPainter предоставляет функции для рисования
ГЛАВА 11 ГРАФИКА И АНИМАЦИЯ В PYQT 267
простых точек и линий, сложных фигур, текста и пиксельных карт. Мы уже рассма-
тривали пиксмапы в предыдущих главах в приложениях, где требовалось отобра-
жать изображения. QPainter также позволяет настраивать различные параметры,
например, качество рендеринга или изменение системы координат рисования. Ри-
сование может осуществляться на устройстве рисования (paint device), которое
представляет собой двумерный объект, созданный из различных классов PyQt. Эти
объекты можно рисовать с помощью QPainter.
Рисование опирается на систему координат для задания положения точек и фигур
и обычно выполняется в событии paint виджета. Система координат по умолчанию
для устройства рисования имеет начало координат в левом верхнем углу, начиная с
точки (0, 0). Значения x увеличиваются вправо, а значения y - вниз. Каждая коорди-
ната (x, y) определяет местоположение одного пикселя.
Графический интерфейс, созданный на рис. 11-1, иллюстрирует некоторые функ-
ции и инструменты рисования класса QPainter.

Рисунок 11-1. Некоторые из различных функций рисования класса QPainter.


В первом ряду графического интерфейса представлены точки, линии и текст;
во втором ряду - фигуры и узоры, включая прямоугольники, многоугольники и
прямоугольники со скругленными углами; в последнем ряду - кривые, окружности и
рисование с градиентами.

268 ГЛАВА 11 ГРАФИКА И АНИМАЦИЯ В PYQT


Пояснения к использованию класса QPainter
Начнем с использования сценария basic_window.py из Главы 1. В этой програм
ме представлено довольно много новых классов, большинство из которых
импортировано из модуля QtGui. QtGui предоставляет нам инструменты, необходимые
для работы с двумерной графикой, изображениями и шрифтами. Классы QPoint и
QRect, импортированные из QtCore в Листинге 11-1, используются для определения
точек и прямоугольников, задаваемых значениями координат в плоскости окна.
Листинг 11-1. Создание класса MainWindow для примера QPainter
# paint_basics.py
# Импорт необходимых модулей
import sys
from PyQt6.QtWidgets import QApplication, QWidget
from PyQt6.QtCore import Qt, QPoint, QRect
from PyQt6.QtGui import (QPainter, QPainterPath, QColor,
QBrush, QPen, QFont, QPolygon, QLinearGradient)

class MainWindow(QWidget):

def __init__(self):
super().__init__()
self.initializeUI()

def initializeUI(self):
"""Настраиваем графический интерфейс приложения."""
self.setFixedSize(600, 600)
self.setWindowTitle("Основы QPainter")

# Создаем несколько цветов пера


self.black = '#000000'
self.blue = '#2041F1'
self.green = '#12A708'
self.purple = '#6512F0'
self.red = '#E00C0C'
self.orange = '#FF930A'
self.show()

if __name__ == '__main__':
app = QApplication(sys.argv)
window = MainWindow()
sys.exit(app.exec())

ГЛАВА 11 ГРАФИКА И АНИМАЦИЯ В PYQT 269


Класс MainWindow наследуется от QWidget, и всё рисование будет происходить
на поверхности виджета. Определенно полезно думать о виджетах как о холстах, на
которых вы будете рисовать и раскрашивать узоры и цвета. Хотя в пространстве
имен Qt имеется перечисление Qt.GlobalColor с некоторыми стандартными цветами,
мы создадим несколько собственных цветов в initializeUI().
Вы, вероятно, также заметили, что здесь нет метода setUpMainWindow() или каких-
либо других вызовов методов для рисования. Это связано с тем, что обработчик
события paintEvent() будет выполнять все действия по рисованию в окне.

Обработчик события paintEvent()


Для общих целей рисование осуществляется внутри функции paintEvent().
Рассмотрим пример настройки QPainter в следующем коде для рисования простой
линии:

def paintEvent(self, event):


painter = QPainter() # Конструируем объект painter
painter.begin(self)
painter.drawLine(260, 20, 260, 180)
painter.end()

Рисование происходит между методами begin() и end() на устройстве рисования, на


которое ссылается self. Рисование обрабатывается в промежутке между этими двумя
методами. Использование методов begin() и end() не является обязательным. Можно
сконструировать рисовальщика, принимающего в качестве параметра устройство
рисования. Однако begin() и end() можно использовать для отлова ошибок, если
закрашивающее устройство не сработает.
В листинге 11-2 создан обработчик события paintEvent() для MainWindow.
Листинг 11-2. Код обработчика события paintEvent() в примере QPainter
# paint_basics.py
def paintEvent(self, event):
"""Реализуйте обработчик событий для создания объекта QPainter
который используется во всем примере."""
painter = QPainter()
painter.begin(self)
# Использование сглаживания для сглаживания изогнутых краев
painter.setRenderHint(
QPainter.RenderHint.Antialiasing)

self.drawPoints(painter)
self.drawDiffLines(painter)
self.drawText(painter)

270 ГЛАВА 11 ГРАФИКА И АНИМАЦИЯ В PYQT


self.drawRectangles(painter)
self.drawPolygons(painter)
self.drawRoundedRects(painter)
self.drawCurves(painter)
self.drawCircles(painter)
self.drawGradients(painter)
painter.end()

Во время события paint могут быть вызваны и другие методы. Поскольку


одновременно разрешено использовать только один объект painter, в Листинге 11-2
мы вызываем различные методы, которые принимают в качестве аргумента объект
painter.
Одним из параметров, которые мы можем изменять в QPainter, является качество
рендеринга с помощью подсказок рендеринга. QPainter.RenderHint.Antialiasing создает
более плавные изогнутые края.

Классы QColor, QPen и QBrush


К числу параметров, которые можно изменить, относятся цвет, ширина и
стили, используемые для рисования линий и фигур. Класс QColor предоставляет
доступ к различным цветовым схемам, например, к значениям RGB, HSV и CMYK.
Для задания цветов можно использовать либо шестнадцатеричные строки RGB,
"#112233"; предопределенные имена цветов, например Qt.GlobalColor.blue или
Qt.GlobalColor.darkBlue; либо значения RGB, (233, 12, 43). QColor также включает
альфа-канал, используемый для придания цветам прозрачности, где 0 - полностью
прозрачный, а 255 - полностью непрозрачный.
QPen используется для рисования линий и контуров фигур. Следующая строка
создает черное перо шириной 2 пиксела, которое рисует пунктирные линии:

pen = QPen(QColor("#000000"), 2, Qt.PenStyle.DashLine)


painter.setPen(pen)

По умолчанию используется стиль Qt.PenStyle.SolidLine.


Кисть QBrush определяет, как рисовать, точнее, заполнять, фигуры. Кисти могут
иметь цвет, узор, градиент или текстуру. В следующем блоке создается пурпурная
кисть со стилем Dense5Pattern:

brush = QBrush(Qt.darkMagenta,
Qt.BrushStyle.Dense5Pattern)
painter.setBrush(brush)

ГЛАВА 11 ГРАФИКА И АНИМАЦИЯ В PYQT 271


По умолчанию используется стиль Qt.BrushStyle.SolidPattern.
Если вы хотите создать несколько линий или фигур с разными перьями и кистями,
обязательно вызывайте setPen() и/или setBrush() каждый раз, когда их нужно
изменить. В противном случае QPainter будет продолжать использовать настройки
пера и кисти, заданные при предыдущем вызове.
Все эти концепции демонстрируются в следующих разделах.

Примечание. Вызов QPainter.begin() приводит к сбросу всех настроек


рисовальщика к значениям по умолчанию.

Рисование точек и линий


Рассмотрим, как рисовать точки и линии на виджете в Листинге 11-3.
Листинг 11-3. Код для методов drawPoints() и drawDiffLines()
# paint_basics.py
def drawPoints(self, painter):
"""Пример того, как рисовать точки с помощью QPainter."""
pen = QPen(QColor(self.black))
for i in range(1, 9):
pen.setWidth(i * 2)
painter.setPen(pen)
painter.drawPoint(i * 20, i * 20)

def drawDiffLines(self, painter):


"""Примеры рисования линий с помощью QPainter."""
pen = QPen(QColor(self.black), 2)
painter.setPen(pen)
painter.drawLine(230, 20, 230, 180)
pen.setStyle(Qt.PenStyle.DashLine)
painter.setPen(pen)
painter.drawLine(260, 20, 260, 180)
pen.setStyle(Qt.PenStyle.DotLine)
painter.setPen(pen)
painter.drawLine(290, 20, 290, 180)
pen.setStyle(Qt.PenStyle.DashDotLine)
painter.setPen(pen)
painter.drawLine(320, 20, 320, 180)
# Изменение цвета и толщины пера
blue_pen = QPen(QColor(self.blue), 4)
painter.setPen(blue_pen)
painter.drawLine(350, 20, 350, 180)
272 ГЛАВА 11 ГРАФИКА И АНИМАЦИЯ В PYQT
blue_pen.setStyle(Qt.PenStyle.DashDotDotLine)
painter.setPen(blue_pen)
painter.drawLine(380, 20, 380, 180)

Метод drawPoint() может использоваться для рисования отдельных пикселей.


Изменяя ширину пера, можно рисовать более широкие точки. Значения x и y могут
быть заданы явно или указаны с помощью QPoint. Простейший пример рисования
одиночной точки шириной 3 пикселя в точке (10, 15) приведен в следующем коде:

pen.setWidth(3)
painter.setPen(pen)
painter.drawPoint(10, 15)

Примечание. Метод drawPoint() и другие методы задаются с использованием


целочисленных значений. Некоторые из методов рисования позволяют
также использовать значения с плавающей точкой. Вместо импорта классов
QPoint и QRect следует использовать QPointF и QRectF.

Результаты работы методов drawPoints() и drawDiffLines() показаны на рис. 11-2.

Рисунок 11-2. Пример точек и линий, нарисованных с помощью QPainter


Для рисования линий существуют методы drawLine() и drawLines(). Каждая из
линий, показанных на рис. 11-2, отображается различными стилями или цветами.
Линии создаются путем указания набора точек, а именно начальных значений x1 и
y1 и конечных значений x2 и y2. Это демонстрируется в следующем коде:

pen.setStyle(Qt.DashLine) # Specify a style


painter.setPen(pen) # Set the pen
painter.drawLine(260, 20, 260, 180) # x1, y1, x2, y2

ГЛАВА 11 ГРАФИКА И АНИМАЦИЯ В PYQT 273


Рисование текста
Метод drawText() в Листинге 11-4 используется для рисования текста на
устройстве рисования, а для применения различных настроек шрифта мы можем
воспользоваться функцией setFont().
Листинг 11-4. Код метода drawText()
# paint_basics.py
def drawText(self, painter):
"""Пример того, как рисовать текст с помощью QPainter."""
text = "Не оглядывайся назад."
pen = QPen(QColor(self.red))
painter.setFont(QFont("Helvetica", 15))
painter.setPen(pen)
painter.drawText(420, 110, text)

Для рисования текста сначала задаются координаты верхнего левого угла на


устройстве рисования (считайте, что текст находится внутри прямоугольника).
Это самый простой способ рисования текста. Для рисования нескольких строк или
для обертывания текста используйте объект QRect (прямоугольник), в который
помещается текст. Результаты рисования текста показаны на рис. 11-3.

Рисунок 11-3. Простой пример рисования текста с помощью QPainter

Рисование двумерных фигур


Существует несколько различных способов рисования четырехугольников с
помощью метода drawRect(). В данном примере мы укажем координаты левого
верхнего угла, а затем ширину и высоту фигуры.
painter.drawRect(120, 220, 80, 80)
Для каждого из квадратов, изображенных в левом верхнем углу рис. 11-4, мы
начинаем с установки значений пера и кисти, а затем вызываем функцию drawRect()
для рисования фигуры. В первой фигуре используется черное перо без кисти; во
второй вызывается функция setBrush() для заливки квадрата. В следующей фигуре

274 ГЛАВА 11 ГРАФИКА И АНИМАЦИЯ В PYQT


используется красное перо с зеленой кистью. Наконец, в последнем квадрате показан
пример установки прозрачности цвета объекта пера на 100.

Рисунок 11-4. Различные фигуры, нарисованные с помощью QPainter


Эти прямоугольники рисуются с помощью метода drawRectangles() в Листинге 11-5.
Листинг 11-5. Код метода drawRectangles()
# paint_basics.py
def drawRectangles(self, painter):
"""Примеры того, как рисовать прямоугольники с помощью
QPainter."""
pen = QPen(QColor(self.black))
brush = QBrush(QColor(self.black))
painter.setPen(pen)
painter.drawRect(20, 220, 80, 80)

painter.setPen(pen)
painter.setBrush(brush)
painter.drawRect(120, 220, 80, 80)

red_pen = QPen(QColor(self.red), 5)
green_brush = QBrush(QColor(self.green))
painter.setPen(red_pen)
painter.setBrush(green_brush)
painter.drawRect(20, 320, 80, 80)

# Демонстрация изменения альфа-канала


# для включения прозрачности

ГЛАВА 11 ГРАФИКА И АНИМАЦИЯ В PYQT 275


blue_pen = QPen(QColor(32, 85, 230, 100), 5)
blue_pen.setStyle(Qt.PenStyle.DashLine)
painter.setPen(blue_pen)
painter.setBrush(green_brush)
painter.drawRect(120, 320, 80, 80)

Для рисования неравномерных многоугольников можно использовать класс


QPolygon, указав координаты точек каждого угла. Эта операция выполняется в
Листинге 11-6. Порядок передачи точек в объект QPolygon является порядком их
отрисовки. Затем объект многоугольника рисуется с помощью метода QPainter
drawPolygon(). Многоугольник можно увидеть в середине верхней строки на рис. 11-4.
Листинг 11-6. Код для метода drawPolygons()
# paint_basics.py
def drawPolygons(self, painter):
"""Пример того, как рисовать многоугольники с помощью QPainter."""
pen = QPen(QColor(self.blue), 2)
brush = QBrush(QColor(self.orange))

points = QPolygon([QPoint(240, 240), QPoint(380, 250),


QPoint(230, 380), QPoint(370, 360)])

painter.setPen(pen)
painter.setBrush(brush)
painter.drawPolygon(points)

QPainter также может рисовать прямоугольники с закругленными углами. Процесс


их рисования аналогичен рисованию обычных прямоугольников, за исключением
того, что необходимо указать значения радиусов x и y для углов. Примеры можно
увидеть на рис. 11-4 в правом верхнем углу. В Листинге 11-7 показано, как создать
скругленный прямоугольник, сначала создав координаты объекта QRect, а затем
указав стиль кисти для заливки формы.
Листинг 11-7. Код метода drawRoundedRects()
# paint_basics.py
def drawRoundedRects(self, painter):
"""Примеры того, как рисовать прямоугольники с
скругленными углами с помощью QPainter."""
pen = QPen(QColor(self.black))
brush = QBrush(QColor(self.black))

rect_1 = QRect(420, 340, 40, 60)


rect_2 = QRect(480, 300, 50, 40)
rect_3 = QRect(540, 240, 40, 60)

276 ГЛАВА 11 ГРАФИКА И АНИМАЦИЯ В PYQT


painter.setPen(pen)
brush.setStyle(Qt.BrushStyle.Dense1Pattern)
painter.setBrush(brush)
painter.drawRoundedRect(rect_1, 8, 8)

brush.setStyle(Qt.BrushStyle.Dense5Pattern)
painter.setBrush(brush)
painter.drawRoundedRect(rect_2, 5, 20)

brush.setStyle(Qt.BrushStyle.BDiagPattern)
painter.setBrush(brush)
painter.drawRoundedRect(rect_3, 15, 15)

Для рисования абстрактных фигур нам необходимо использовать QPainterPath.


Объекты, состоящие из различных компонентов, таких как линии, прямоугольники и
кривые, называются путями художника (painter paths). Пример контура рисования
можно увидеть в левом нижнем углу рис. 11-4.
В методе drawCurves() в Листинге 11-8 мы сначала создаем черное перо и белую
кисть, а также экземпляр QPainterPath. Метод moveTo() перемещает в определенную
позицию в окне без рисования других компонентов. Мы начнем рисовать в этой
позиции, (30, 420).
Листинг 11-8. Код метода drawCurves()
# paint_basics.py
def drawCurves(self, painter):
"""Примеры того, как рисовать кривые с помощью
QPainterPath."""
pen = QPen(Qt.GlobalColor.black, 3)
brush = QBrush(Qt.GlobalColor.white)

tail_path = QPainterPath()
tail_path.moveTo(30, 420)
tail_path.cubicTo(30, 420, 65, 500, 30, 560)
tail_path.lineTo(163, 540)
tail_path.cubicTo(125, 360, 110, 440, 30, 420)
tail_path.closeSubpath()

painter.setPen(pen)
painter.setBrush(brush)
painter.drawPath(tail_path)

ГЛАВА 11 ГРАФИКА И АНИМАЦИЯ В PYQT 277


Метод cubicTo() используется для построения параметрической кривой,
называемой также кривой Безье, от начальной точки (30, 420) до конечной (30, 560).
Первые две точки, (30, 420) и (65, 500), в функции cubicTo() используются для того,
чтобы повлиять на изгиб линии между начальной и конечной точками. Следующими
компонентами tail_path являются линия, построенная с помощью lineTo(), и еще
одна кривая, построенная с помощью cubicTo(). Абстрактная фигура закрывается с
помощью функции closeSubpath(), а путь рисуется с помощью функции drawPath().
Последняя фигура, которую мы рассмотрим, - это эллипс, который рисуется с
помощью метода QPainter drawEllipse(). Для эллипса нам нужны четыре значения:
местоположение центра и два значения радиусов для направлений x и y. Если
значения радиусов равны, то можно нарисовать окружность, как в правом нижнем
углу рис. 11-4.
В Листинге 11-9 показано, как нарисовать эллипс с QPoint в качестве координаты
центра, за которой следуют значения радиусов x и y. Фигуру также можно нарисовать,
передав QRect конструктору QPoint.
Листинг 11-9. Код метода drawCircles()
# paint_basics.py
def drawCircles(self, painter):
"""Пример того, как рисовать эллипсы с помощью QPainter."""
height, width = self.height(), self.width()

center_x, center_y = (width / 2), height - 100


radius_x, radius_y = 60, 60
pen = QPen(Qt.GlobalColor.black, 2,
Qt.PenStyle.SolidLine)
brush = QBrush(Qt.GlobalColor.darkMagenta,
Qt.BrushStyle.Dense5Pattern)

painter.setPen(pen)
painter.setBrush(brush)
painter.drawEllipse(QPoint(int(center_x),
int(center_y)), radius_x, radius_y)

Рисование градиентов
Градиенты можно использовать вместе с QBrush для заливки внутренней части
фигур. В PyQt существует три различных типа стилей градиента: линейный,
радиальный и конический. В данном примере мы будем использовать класс
QLinearGradient для интерполяции цветов между двумя начальными и конечными
точками. Результат можно увидеть на рис. 11-5.

278 ГЛАВА 11 ГРАФИКА И АНИМАЦИЯ В PYQT


Рисунок 11-5. Применение градиента к квадрату
Конструктор QLinearGradient принимает в качестве аргументов область устройства
закраски, в которой будет происходить градиент, заданную координатами x1, y1, x2,
y2. Пример этого показан в Листинге 11-10.
Листинг 11-10. Код метода drawGradients()
def drawGradients(self, painter):
"""Пример заливки фигур с помощью градиентов."""
pen = QPen(QColor(self.black), 2)
gradient = QLinearGradient(450, 480, 520, 550)

gradient.setColorAt(0.0, Qt.GlobalColor.blue)
gradient.setColorAt(0.5, Qt.GlobalColor.yellow)
gradient.setColorAt(1.0, Qt.GlobalColor.cyan)

painter.setPen(pen)
painter.setBrush(QBrush(gradient))
painter.drawRect(420, 420, 160, 160)

Мы можем создавать точки начала закрашивания и смешивания цветов с помощью


функции setColorAt(). Этот метод определяет позицию, с которой начинается цвет, и
какой цвет используется для заливки этой области. Значения позиции должны быть
от 0.0 до 1.0.
Следующий проект объединяет то, что мы только что узнали о рисовании в
PyQt, с тем, что мы знаем о виджетах и окнах, и позволяет создать приложение для
рисования.

Проект 11.1 - графический интерфейс Painter


Существует множество приложений для создания цифровых картин, которые до
отказа заполнены инструментами для рисования, раскрашивания, редактирования
и создания собственных произведений искусства на компьютере. В QPainter можно
было бы вручную кодировать каждую отдельную линию и фигуру по очереди. Однако

ГЛАВА 11 ГРАФИКА И АНИМАЦИЯ В PYQT 279


вместо того, чтобы проходить через этот кропотливый процесс создания цифровых
произведений искусства, проект графического интерфейса Painter закладывает
основу для создания приложения для рисования, которое может проложить путь к
более плавному процессу рисования. Интерфейс можно увидеть на рис. 11-6.

Рисунок 11-6. Графический интерфейс художника с панелью инструментов в левой


части окна и текущими координатами мыши, отображаемыми в строке состояния
В этом первом проекте мы попытаемся объединить многие концепции, изученные в
предыдущих главах, включая строки меню, панели инструментов, строки состояния,
диалоговые окна, создание пиктограмм и повторную реализацию обработчиков
событий, и объединить их с классом QPainter. Кроме того, мы добавим несколько
новых идей, сосредоточившись на создании подсказок и отслеживании положения
мыши.
Обязательно возьмите папку icons с GitHub для этого проекта.

Пояснения к графическому интерфейсу Painter


В графическом интерфейсе художника пользователи смогут рисовать, используя
карандаш или маркер, стирать и выбирать цвета с помощью QColorDialog. Пункты
меню позволяют очистить текущий холст, сохранить рисунок, выйти из программы,
а также включить или выключить сглаживание.
Для начала работы в Листинге 11-11 воспользуемся сценарием
main_window_template.py из Главы 5. Мы импортируем множество классов из QtWidgets,
QtCore и QtGui. Обязательно включим класс QToolTip, чтобы иметь возможность
создавать информативные подсказки для элементов панели инструментов.
280 ГЛАВА 11 ГРАФИКА И АНИМАЦИЯ В PYQT
Листинг 11-11. Импорт классов PyQt для графического интерфейса художника
# painter.py
# Импорт необходимых модулей
import os, sys
from PyQt6.QtWidgets import (QApplication, QMainWindow,
QLabel, QToolBar, QStatusBar, QToolTip, QColorDialog,
QFileDialog)
from PyQt6.QtCore import Qt, QSize, QPoint, QRect
from PyQt6.QtGui import (QPainter, QPixmap, QPen, QColor,
QIcon, QFont, QAction)

Графический интерфейс painter позволяет пользователям рисовать изображения


на области холста. В отличие от предыдущего примера, где рисование происходило
на основном виджете, в этом примере показано, как подклассифицировать QLabel
и повторно реализовать его обработчики событий рисования и мыши. Обработка
некоторых обработчиков событий в этом приложении была адаптирована с сайта
документа Qt. (Ссылка: https://doc.qt.io/qt-6/qtwidgets-widgets-scribble-example.html).
Программа содержит два класса: класс Canvas для рисования и класс MainWindow
для создания строки меню и панели инструментов.

Создание класса Canvas


Большинство классов в PyQt могут быть подклассами для модификации или
расширения существующих функциональных возможностей. Слово большинство
использовано потому, что не все классы предназначены для подклассификации,
например, такие удобные классы, как QListWidget и QTableWidget. В данном проекте
подкласс QLabel и переработка его обработчика события paintEvent() позволяют
создать поверхность для рисования.
В Листинге 11-12 класс Canvas наследует QLabel, что означает, что мы можем создать
собственный класс QLabel. Мы также передадим родителя класса, чтобы иметь
доступ к его строке состояния. Другой подход может заключаться в использовании
pyqtSignal для обновления строки состояния.
Листинг 11-12. Код для пользовательского класса Canvas
# painter.py
# Создает виджет, на котором будет производиться рисование
class Canvas(QLabel):
def __init__(self, parent):
super().__init__()
self.parent = parent
width, height = parent.width(), parent.height()
# Создаем объект pixmap, который будет выступать в качестве холста
self.pixmap = QPixmap(width, height)
ГЛАВА 11 ГРАФИКА И АНИМАЦИЯ В PYQT 281
self.pixmap.fill(Qt.GlobalColor.white)
self.setPixmap(self.pixmap)

# Следим за мышью для получения ее координат


self.mouse_track_label = QLabel()
self.setMouseTracking(True)

# Инициализация переменных
self.antialiasing_status = False
self.eraser_selected = False

self.last_mouse_pos = QPoint()
self.drawing = False
self.pen_color = Qt.GlobalColor.black
self.pen_width = 2

Далее мы создадим объект pixmap и передадим его в функцию setPixmap().


Поскольку QPixmap можно использовать в качестве устройства QPaintDevice,
использование pixmap значительно упрощает работу с рисованием и отображением
пикселей. Кроме того, использование QPixmap означает, что мы можем задать
начальный цвет фона с помощью функции fill().
Далее нам необходимо инициализировать несколько переменных и объектов.
• mouse_track_label - метка для отображения текущего положения мыши
• eraser_selected - True, если ластик выбран.
• antialiasing_status - True, если пользователь выбрал пункт меню для
использования сглаживания
• last_mouse_pos - Отслеживает последнее положение мыши при нажатии
левой кнопки мыши или при ее перемещении
• drawing - True, если нажата левая кнопка мыши, что указывает на то, что
пользователь может рисовать
• pen_color, pen_width - Переменные, в которых хранятся начальные значения
пера и кисти
Поскольку пользователь будет использовать мышь для рисования в окне
графического интерфейса, нам необходимо обрабатывать события нажатия и
отпускания кнопки мыши, а также перемещения мыши. Мы можем использовать
функцию setMouseTracking() для отслеживания курсора мыши и возврата его
координат в mouseMoveEvent(). Возвращенные координаты будут отображаться в
строке состояния.
В Листинге 11-13 создан слот selectDrawingTool(), определяющий, какой инструмент
рисования был выбран на панели инструментов. На панели инструментов
пользователь может выбрать четыре инструмента: карандаш, маркер, ластик
и селектор цвета. Кроме того, слот также заботится об определении значения
eraser_selected и установке ширины или цвета пера.

282 ГЛАВА 11 ГРАФИКА И АНИМАЦИЯ В PYQT


Листинг 11-13. Код для слота selectDrawingTool() в пользовательском классе Canvas
# painter.py
def selectDrawingTool(self, tool):
"""Определяет, какой инструмент на панели инструментов был
выбран."""
if tool == "pencil":
self.eraser_selected = False
self.pen_width = 2
elif tool == "marker":
self.eraser_selected = False
self.pen_width = 8
elif tool == "eraser":
self.eraser_selected = True
elif tool == "color":
self.eraser_selected = False
color = QColorDialog.getColor()
if color.isValid():
self.pen_color = color

В Листинге 11-14, если пользователь нажимает левую кнопку мыши, когда курсор
находится в окне, мы устанавливаем значение drawing равным True и сохраняем
текущее значение мыши в last_mouse_pos. Нам также потребуется проверить, когда
мышь была отпущена, чтобы остановить рисование, и обновить значения для draw
и eraser_selected.
Листинг 11-14. Реализация функций mousePressEvent() и mouseReleaseEvent() в
классе Canvas
# painter.py
def mousePressEvent(self, event):
"""Обработка нажатия кнопки мыши."""
if event.button() == Qt.MouseButton.LeftButton:
self.last_mouse_pos = event.pos()
self.drawing = True

def mouseReleaseEvent(self, event):


"""Обрабатываем момент отпускания мыши.
Проверяется, когда ластик больше не используется."""
if event.button() == Qt.MouseButton.LeftButton:
self.drawing = False
elif self.eraser_selected == True:
self.eraser_selected = False

ГЛАВА 11 ГРАФИКА И АНИМАЦИЯ В PYQT 283


Обработка событий движения мыши
В этом проекте в строке состояния отображаются текущие координаты x и y
мыши. Возможно, вам не нужна такая функциональность, поэтому в следующем
коде показаны основы включения отслеживания мыши и настройки функции
mouseMoveEvent() для возврата значений x и y:

# Включить слежение за мышью


self.setMouseTracking(True)
def mouseMoveEvent(self, event):
mouse_pos = event.pos()
pos_text = "Mouse Coordinates: (
{}, {})".format(mouse_pos.x(), mouse_pos.y())
print(pos_text)

События перемещения мыши возникают при каждом перемещении мыши, а


также при нажатии или отпускании кнопки мыши.
Для функции mouseMoveEvent(), используемой классом Canvas в Листинге 11-15,
мы вызовем функцию drawOnCanvas(), если была нажата только левая кнопка мыши.
Координаты mouse_pos мы передадим в mouse_track_label и отобразим их в строке
состояния.
Листинг 11-15. Реализация событий mousePressEvent() и mouseReleaseEvent() в
классе Canvas
# painter.py
def mouseMoveEvent(self, event):
"""Обработка движений мыши. Отслеживаем координаты
мыши в окне и отображение их в строке состояния."""
mouse_pos = event.pos()
if (event.buttons() and Qt.MouseButton.LeftButton)\
and self.drawing:
self.drawOnCanvas(mouse_pos)

self.mouse_track_label.setVisible(True)
sb_text = f"""<p>Координаты мыши: ({mouse_pos.x()},
{mouse_pos.y()})</p>"""
self.mouse_track_label.setText(sb_text)
self.parent.status_bar.addWidget(
self.mouse_track_label)

def drawOnCanvas(self, points):


"""Выполняет рисование на холсте."""
painter = QPainter(self.pixmap)

284 ГЛАВА 11 ГРАФИКА И АНИМАЦИЯ В PYQT


if self.antialiasing_status:
painter.setRenderHint(
QPainter.RenderHint.Antialiasing)
if self.eraser_selected == False:
pen = QPen(QColor(self.pen_color), self.pen_width)
painter.setPen(pen)
painter.drawLine(self.last_mouse_pos, points)

# Обновление положения мыши для следующего перемещения


self.last_mouse_pos = points
elif self.eraser_selected == True:
# Использовать ластик
eraser = QRect(points.x(), points.y(), 12, 12)
painter.eraseRect(eraser)
self.update()

Собственно рисование выполняется в функции drawOnCanvas(). Создается


экземпляр QPainter, который рисует на пиксельной карте. Мы также проверяем
несколько условий. В частности, проверяется наличие сглаживания и значение
параметра eraser_selected - True или False. Если его значение равно False, то пользователь
может рисовать. В противном случае он может стирать.
Класс Canvas также включает методы для очистки и сохранения пиксмапа,
приведенные в Листинге 11-16.
Листинг 11-16. Методы очистки и сохранения в классе Canvas
# painter.py
def newCanvas(self):
"""Очищает текущий холст."""
self.pixmap.fill(Qt.GlobalColor.white)
self.update()

def saveFile(self):
"""Сохраняет файл изображения .png текущей области пиксмапа."""
file_format = "png"
default_name = os.path.curdir + "/untitled." + \
file_format
file_name, _ = QFileDialog.getSaveFileName(
self, "Сохранить как",
default_name, "PNG Формат (*.png)")
if file_name:
self.pixmap.save(file_name, file_format)

Для сохранения файла откроем текущий каталог и выведем в QFileDialog имя по


умолчанию. Для сохранения изображения используется метод QPixmap save().
Реализация события paintEvent() в Листинге 11-17 создает закрашивающее
ГЛАВА 11 ГРАФИКА И АНИМАЦИЯ В PYQT 285
устройство для области холста и рисует пиксмап с помощью функции drawPixmap().
Если сначала нарисовать на QPixmap в методе drawOnCanvas(), а затем скопировать
QPixmap на экран в paintEvent(), то можно быть уверенным, что рисунок не будет
потерян при сворачивании окна.
Листинг 11-17. Код обработчика события paintEvent() в классе Canvas
# painter.py
def paintEvent(self, event):
"""Создать объект QPainter. Это сделано для того, чтобы
исключить возможность потери рисунка при смене окна.."""
painter = QPainter(self)

target_rect = QRect()
target_rect = event.rect()
painter.drawPixmap(target_rect,
self.pixmap, target_rect)
painter.end()

На этом создание класса Canvas завершено. Перейдем к созданию класса


MainWindow.

Создание графического интерфейса класса


MainWindow для Painter
Класс MainWindow в Листингах 11-18 - 11-22 создает главное меню, панель
инструментов, подсказки для каждой из кнопок панели инструментов, а также
экземпляр класса Canvas. Основа класса MainWindow задана в Листинге 11-18.
Листинг 11-18. Код класса MainWindow графического интерфейса painter
# painter.py
class MainWindow(QMainWindow):

def __init__(self):
super().__init__()
self.initializeUI()

def initializeUI(self):
"""Настройка графического интерфейса приложения."""
self.setMinimumSize(900, 600)
self.setWindowTitle("11.1 – Painter GUI")
# Установите стиль шрифта, используемый всеми подсказками
QToolTip.setFont(QFont("Helvetica", 12))

286 ГЛАВА 11 ГРАФИКА И АНИМАЦИЯ В PYQT


self.setUpMainWindow()
self.createActions()
self.createMenu()
self.createToolbar()
self.show()

if __name__ == '__main__':
app = QApplication(sys.argv)
app.setAttribute(
Qt.ApplicationAttribute.AA_DontShowIconsInMenus, True)
window = MainWindow()
sys.exit(app.exec())

Центральным виджетом приложения является один объект Canvas, представленный


в Листинге 11-19.
Листинг 11-19. Код метода setUpMainWindow() графического интерфейса painter
# painter.py
def setUpMainWindow(self):
"""Создайте объект холст, который наследуется от QLabel."""
self.canvas = Canvas(self)
self.setCentralWidget(self.canvas)

В Листинге 11-20 созданы действия и меню, расположенные в строке меню


графического интерфейса.
Листинг 11-20. Код методов createActions() и createMenu() графического
интерфейса painter
# painter.py
def createActions(self):
"""Создать действия меню приложения."""
# Создание действий для меню Файл
self.new_act = QAction("Новый холст")
self.new_act.setShortcut("Ctrl+N")
self.new_act.triggered.connect(self.canvas.newCanvas)
self.save_file_act = QAction("Сохранить файл")
self.save_file_act.setShortcut("Ctrl+S")
self.save_file_act.triggered.connect(
self.canvas.saveFile)
self.quit_act = QAction("Выход")
self.quit_act.setShortcut("Ctrl+Q")
self.quit_act.triggered.connect(self.close)

# Создание действий для меню Инструменты


self.anti_al_act = QAction(
ГЛАВА 11 ГРАФИКА И АНИМАЦИЯ В PYQT 287
"AntiAliasing", checkable=True)
self.anti_al_act.triggered.connect(
self.turnAntialiasingOn)

def createMenu(self):
"""Создаем строку меню приложения."""
self.menuBar().setNativeMenuBar(False)

# Создаем меню "Файл" и добавляем действия


file_menu = self.menuBar().addMenu("Файл")
file_menu.addAction(self.new_act)
file_menu.addAction(self.save_file_act)
file_menu.addSeparator()
file_menu.addAction(self.quit_act)

# Создать меню Инструменты и добавить действия


file_menu = self.menuBar().addMenu("Инструменты")
file_menu.addAction(self.anti_al_act)
self.status_bar = QStatusBar()
self.setStatusBar(self.status_bar)

Меню Файл в createMenu() содержит действия по очистке холста, сохранению


изображения и выходу из приложения. Меню Инструменты содержит проверяемый
пункт меню, который включает или выключает сглаживание.
Метод createToolbar() создает действия и пиктограммы для инструментов
рисования на панели инструментов приложения. При нажатии кнопки выдается
сигнал срабатывания, который подключается к слоту selectDrawingTool() класса
Canvas. Используя лямбда-функцию, мы можем передавать в слот дополнительную
информацию. Для каждого действия мы будем передавать в selectDrawingTool()
строку.
Листинг 11-21. Код метода createToolbar() графического интерфейса painter
# painter.py
def createToolbar(self):
"""Создать панель инструментов приложения, содержащую
инструменты рисования."""
tool_bar = QToolBar("Painting Toolbar")
tool_bar.setIconSize(QSize(24, 24))
# Установить ориентацию панели инструментов на левую сторону
self.addToolBar(Qt.ToolBarArea.LeftToolBarArea, tool_bar)
tool_bar.setMovable(False)

# Создание действий и всплывающих подсказок и добавление


# их на панель инструментов
pencil_act = QAction(QIcon("icons/pencil.png"),
288 ГЛАВА 11 ГРАФИКА И АНИМАЦИЯ В PYQT
"Карандаш", tool_bar)
pencil_act.setToolTip("Это <b>Карандаш</b>.")
pencil_act.triggered.connect(
lambda: self.canvas.selectDrawingTool("pencil"))

marker_act = QAction(QIcon("icons/marker.png"),
"Маркер", tool_bar)
marker_act.setToolTip("Это <b>Маркер</b>.")
marker_act.triggered.connect(
lambda: self.canvas.selectDrawingTool("marker"))

eraser_act = QAction(QIcon("icons/eraser.png"),
"Ластик", tool_bar)
eraser_act.setToolTip(
"Используйте <b>Ластик</b>, чтобы все исчезло.")
eraser_act.triggered.connect(
lambda: self.canvas.selectDrawingTool("eraser"))

color_act = QAction(QIcon("icons/colors.png"),
"Цвета", tool_bar)
color_act.setToolTip(
"Выберите <b>Цвет</b> в диалоговом окне Цвет.")
color_act.triggered.connect(
lambda: self.canvas.selectDrawingTool("color"))

tool_bar.addAction(pencil_act)
tool_bar.addAction(marker_act)
tool_bar.addAction(eraser_act)
tool_bar.addAction(color_act)

Создание подсказок рассматривается в следующем подразделе, "Создание


подсказок для виджетов".
Слот turnAntialiasingOn() в Листинге 11-22 обновляет переменную класса Canvas
- antialiasing_status. Реализованная функция leaveEvent() обрабатывает перемещение
курсора мыши за пределы главного окна и устанавливает видимость метки
mouse_track_label в значение False.
Листинг 11-22. Дополнительные методы, используемые в классе MainWindow
графического интерфейса painter
# painter.py
def turnAntialiasingOn(self, state):
"""Включить или выключить сглаживание."""
if state:
self.canvas.antialiasing_status = True
else:
self.canvas.antialiasing_status = False
ГЛАВА 11 ГРАФИКА И АНИМАЦИЯ В PYQT 289
def leaveEvent(self, event):
"""Класс QEvent, который вызывается, когда мышь покидает
пространство экрана. Скрывает координаты мыши в строке состояния,
если мышь покидает окно."""
self.canvas.mouse_track_label.setVisible(False)

Создание подсказок для виджетов


Часто пользователь может задаться вопросом, что на самом деле делает тот или
иной виджет или действие в меню или на панели инструментов в приложении.
Возможно, требуется дополнительная информация, чтобы помочь пользователю
понять, как взаимодействовать с тем или иным инструментом.
Инструментальные подсказки - это полезные небольшие фрагменты текста,
которые могут быть выведены на экран для информирования пользователя о
назначении виджета. Инструментальные подсказки могут быть применены к
любому виджету с помощью метода setToolTip(). Подсказки могут отображать строки
с форматированным текстом, как показано в примере кода из Листинга 11-21 и на
рис. 11-7.
eraser_act.setToolTip("Use the <b>Eraser</b> to make it all disappear.")

Рисунок 11-7. Подсказка, которая отображается при наведении курсора на кнопку


ластика
Стиль шрифта и внешний вид подсказки могут быть изменены в соответствии с
вашими предпочтениями.
В следующих разделах мы рассмотрим, как анимировать виджеты и другие
объекты в графических интерфейсах.

290 ГЛАВА 11 ГРАФИКА И АНИМАЦИЯ В PYQT


Анимация сцен с помощью QPropertyAnimation
Следующий проект служит введением в фреймворк графических представлений
Qt и класс QAnimationProperty. С помощью этого фреймворка можно создавать
приложения, позволяющие пользователям взаимодействовать с элементами в окне.
Графическое представление состоит из трех компонентов:
1. Сцена, созданная на основе класса QGraphicsScene. Сцена создает
поверхность для управления двумерными графическими элементами и
должна быть создана вместе с представлением для визуализации сцены.
2. QGraphicsView предоставляет виджет представления для визуализации
элементов сцены, создавая область прокрутки, которая позволяет
пользователю перемещаться по сцене.
3. Элементы сцены основаны на классе QGraphicsItem. Пользователи могут
взаимодействовать с графическими элементами с помощью событий
мыши и клавиш, а также перетаскивания. Элементы также поддерживают
обнаружение столкновений.
QAnimationProperty используется для анимирования свойств виджетов и
элементов. Анимация в графических интерфейсах может быть использована для
анимации виджетов. Например, можно анимировать кнопку, которая увеличивается,
уменьшается или поворачивается, или текст, который плавно перемещается в
окне, или создать виджеты, которые затухают или меняют цвет. QAnimationProperty
работает только с объектами, наследующими класс QObject. QObject - это базовый
класс для всех объектов, создаваемых в Qt.
Qt предоставляет ряд простых объектов, наследующих QGraphicsItem, включая
базовые фигуры, текст и пиксмапы. Эти элементы уже обеспечивают поддержку
взаимодействия с мышью и клавиатурой. Однако QGraphicsItem не наследует
QObject. Поэтому, если вы хотите анимировать графический элемент с помощью
QPropertyAnimation, вы должны сначала создать новый класс, который наследуется
от QObject, и определить новые свойства для элемента.
На рис. 11-8 показан пример сцены, которую мы будем создавать в этом проекте.

Рисунок 11-8. Сцена с автомобилем и движущимися в окне деревьями


Перед началом работы убедитесь, что вы загрузили папку images из репозитория
GitHub.
ГЛАВА 11 ГРАФИКА И АНИМАЦИЯ В PYQT 291
Пояснения к анимации сцен
В следующем приложении вы узнаете, как создавать новые свойства для элементов
с помощью pyqtProperty, научитесь анимировать объекты с помощью класса
QPropertyAnimation и создадите графическое представление Qt для отображения
элементов и анимации.
Поскольку мы собираемся создать графическую сцену, нам необходимо
импортировать QGraphicsScene, QGraphicsView и один из классов QGraphicsItem в
Листинге 11-23. Для данной программы мы импортируем QGraphicsPixmapItem,
поскольку будем работать с пиксмапами. Хотя мы можем использовать
QPropertyAnimation для анимации свойств виджетов, некоторые классы не имеют
доступа к некоторым свойствам Qt. Например, не существует метода для изменения
цвета текста QLabel. В таких ситуациях можно создать новые свойства Qt с помощью
pyqtProperty. Более подробно мы рассмотрим эту тему в разделе "Введение в анимацию
виджетов".
Листинг 11-23. Импорт классов для примера анимации
# animation.py
# Импорт необходимых модулей
import sys
from PyQt6.QtWidgets import (QApplication, QGraphicsView,
QGraphicsScene, QGraphicsPixmapItem)
from PyQt6.QtCore import (QObject, QPointF, QRectF,
QPropertyAnimation, pyqtProperty)
from PyQt6.QtGui import QPixmap

Также нам не потребуется импортировать QMainWindow или QWidget для создания


главного окна, поскольку в качестве окна для представления анимации будет
выступать QGraphicsView.
QObject не имеет свойства position. Поэтому нам придется определить его с
помощью pyqtProperty в классе Objects в Листинге 11-24.
Листинг 11-24. Создание класса Objects, наследующего QObject
# animation.py
# Создать класс Objects, определяющий свойство position для
# экземпляров класса с помощью pyqtProperty.
class Objects(QObject):

def __init__(self, image_path):


super().__init__()

item_pixmap = QPixmap(image_path)
resize_item = item_pixmap.scaledToWidth(150)
self.item = QGraphicsPixmapItem(resize_item)
292 ГЛАВА 11 ГРАФИКА И АНИМАЦИЯ В PYQT
def _set_position(self, position):
self.item.setPos(position)

position = pyqtProperty(QPointF, fset=_set_position)

QGraphicsPixmapItem создает графический элемент из pixmap, который может быть


добавлен в сцену QGraphicsScene. Мы создаем свойство position, которое позволяет
нам устанавливать и обновлять положение объекта с помощью fset. Параметр
_set_position() передает позицию методу QGraphicsItem.setPos(), устанавливая
положение элемента как координаты, заданные QPointF. Знаки подчеркивания перед
именами переменных, методов или классов используются для обозначения частных
экземпляров.
Цель данного проекта - анимировать два объекта, автомобиль и дерево, в сцене
QGraphicsScene. Код в Листинге 11-25 похож на GUI-приложения, которые мы
делали ранее. Однако вместо QWidget мы будем использовать QGraphicsView для
представления объектов в окне.
Листинг 11-25. Создание класса AnimationScene для визуализации анимации
# animation.py
class AnimationScene(QGraphicsView):

def __init__(self):
super().__init__()
self.initializeView()

def initializeView(self):
"""Инициализация графического представления и отображение
его содержимое на экран."""
self.setMaximumSize(700, 450)
self.setWindowTitle("Пример анимации")

self.createObjects()
self.createScene()
self.show()

if __name__ == '__main__':
app = QApplication(sys.argv)
window = AnimationScene()
sys.exit(app.exec())

ГЛАВА 11 ГРАФИКА И АНИМАЦИЯ В PYQT 293


Метод createObjects() используется для создания объектов, а createScene() - для
настройки сцены.
Сначала создадим объекты и анимации, которые будут помещены в сцену. Для
данной сцены два объекта будут двигаться одновременно. Qt предоставляет другие
классы для работы с группами анимаций, но в Листинге 11-26 для отслеживания
нескольких анимаций используется QPropertyAnimation и список анимаций.
Листинг 11-26. Код метода createObjects() в классе AnimationScene
# animation.py
def createObjects(self):
"""Создаем экземпляры класса Objects и устанавливаем
анимации объектов."""
# Список, содержащий все анимации.
animations = []

# Создаем объект автомобиля и анимацию автомобиля.


self.car = Objects("images/car.png")

self.car_anim = QPropertyAnimation(self.car, b "position")


self.car_anim.setDuration(6000)

self.car_anim.setStartValue(QPointF(-50, 350))
self.car_anim.setKeyValueAt(0.3, QPointF(150, 350))
self.car_anim.setKeyValueAt(0.6, QPointF(170, 350))
self.car_anim.setEndValue(QPointF(750, 350))

# Создайте объект дерева и анимацию дерева.


self.tree = Objects("images/trees.png")

self.tree_anim = QPropertyAnimation(self.tree, b "position")


self.tree_anim.setDuration(6000)

self.tree_anim.setStartValue(QPointF(750, 150))
self.tree_anim.setKeyValueAt(0.3, QPointF(170, 150))
self.tree_anim.setKeyValueAt(0.6, QPointF(150, 150))
self.tree_anim.setEndValue(QPointF(-150, 150))

# Добавьте анимации в список анимаций и запустите их


# анимации после начала работы программы.
animations.append(self.car_anim)
animations.append(self.tree_anim)

for anim in animations:


anim.start()

294 ГЛАВА 11 ГРАФИКА И АНИМАЦИЯ В PYQT


Мы создадим элемент car как экземпляр класса Objects и передадим car и setter
position в QPropertyAnimation. QPropertyAnimation будет обновлять значение position,
чтобы автомобиль перемещался по сцене. Для анимации элементов используйте
функцию setDuration(), чтобы задать время перемещения объекта в миллисекундах,
и укажите начальное и конечное значения свойства с помощью setStartValue() и
setEndValue(). Анимация автомобиля длится шесть секунд и начинается за пределами
экрана, двигаясь с левой стороны вправо. Дерево устроено аналогичным образом, но
движется в противоположном направлении.
Метод setKeyValueAt() позволяет создать ключевые кадры на заданных шагах с
указанными значениями QPointF. Используя ключевые кадры, автомобиль и дерево
будут как бы замедляться по мере прохождения сцены. Метод start() запускает
анимацию.
Настройка сцены проста. Создайте экземпляр сцены, задайте ее размер, добавьте
объекты и их анимацию с помощью addItem(), а затем вызовите setScene(). Эта
процедура описана в Листинге 11-27.
Листинг 11-27. Код метода createScene() в классе AnimationScene
# animation.py
def createScene(self):
"""Создайте графическую сцену и добавьте в
нее экземпляры Objects."""
self.scene = QGraphicsScene(self)
self.scene.setSceneRect(0, 0, 700, 450)
self.scene.addItem(self.car.item)
self.scene.addItem(self.tree.item)
self.setScene(self.scene)

def drawBackground(self, painter, rect):


"""Реализация метода QGraphicsView drawBackground()."""
scene_rect = self.scene.sceneRect()

background = QPixmap("images/highway.jpg")
bg_rectf = QRectF(background.rect())
painter.drawPixmap(scene_rect, background, bg_rectf)

Наконец, сцене можно придать фон с помощью QBrush. Если вы хотите использовать
фоновое изображение, то вам необходимо реализовать метод drawBackground()
класса QGraphicView, как показано в Листинге 11-27.

ГЛАВА 11 ГРАФИКА И АНИМАЦИЯ В PYQT 295


Введение в анимацию виджетов
Мы познакомились с использованием виджетов и настройкой их параметров. Но
что если бы вы могли также анимировать свойства виджетов, такие как размер, цвет,
текст и положение? С помощью класса QPropertyAnimation мы можем анимировать
такие свойства Qt, как geometry (геометрия), size (размер) и text (текст). Эти свойства
относятся к методам-получателям (getter), находящимся внутри каждого класса.
В этом вводном примере мы рассмотрим, как анимировать размер кнопки
QPushButton и цвет текста QCheckBox. Если размер является встроенным свойством
всех виджетов PyQt, то цвет - нет. Для QCheckBox мы посмотрим, как использовать
pyqtProperty для создания нового свойства.
На рис. 11-9 вы видите два виджета. При нажатии кнопки на левом снимке
экрана ее размер будет увеличиваться и уменьшаться. При этом кнопка становится
неактивной, а текст виджета QCheckBox мигает красным цветом. Когда флажок будет
установлен, состояние окна вернется в нормальное.

Рисунок 11-9. Анимированные виджеты, использующие сигналы и слоты для


взаимного изменения состояний
Следующий список содержит несколько полезных методов из QPropertyAnimation:
• start() - Запускает анимацию
• stop() - Остановка анимации
• setStartValue(value) - Устанавливает начальное значение анимации
• setEndValue(value) - Устанавливает конечное значение анимации
• setDuration(int) - Устанавливает длительность анимации (в миллисекундах)
• setKeyValueAt(step, value) - Создает ключевой кадр с заданным шагом (от 0.0 до
1.0) с заданным значением
• setLoopCount(int) - Устанавливает количество повторений анимации; для
бесконечного количества повторений используйте -1.
Многие из этих методов наследуются от QVariantAnimation, одного из базовых
классов для классов анимации.

296 ГЛАВА 11 ГРАФИКА И АНИМАЦИЯ В PYQT


Пояснение к анимации виджетов
Для этого приложения мы можем использовать сценарий basic_window.py из
Главы 1 и импортировать множество новых классов. QAbstractAnimation - это базовый
класс для всех классов анимации. QEasingCurve используется для определения и
управления плавностью анимации.
Также существуют два типа классов для группировки нескольких анимаций
вместе:
• QParallelAnimationGroup - параллельный запуск анимации
• QSequentialAnimationGroup - последовательный запуск анимации.
Для этой программы мы импортируем QSequentialAnimationGroup в Листинге 11-28.
Листинг 11-28. Код для импорта и пользовательского QCheckBox в примере с
анимирующими виджетами
# animate_widgets.py
# Import necessary modules
import sys
from PyQt6.QtWidgets import (QApplication, QWidget,
QPushButton, QCheckBox, QVBoxLayout)
from PyQt6.QtCore import (QAbstractAnimation, QRect, QSize,
QPoint, QEasingCurve, pyqtProperty,
QPropertyAnimation, QSequentialAnimationGroup)
from PyQt6.QtGui import QColor

class AnimatedCheckbox(QCheckBox):

def __init__(self, text):


"""Пользовательский QCheckBox с анимированным текстом."""
super().__init__(text)

def _set_color(self, color):


"""Метод для свойства цвета текста с использованием таблиц стилей."""
self.setStyleSheet(
f"""color: rgb({color.red()}, {color.green()},
{color.blue()})""")

color = pyqtProperty(QColor, fset=_set_color)

В то время как QCheckBox отображает текст и имеет свойство text, класс не


имеет свойства для изменения цвета текста. Поэтому создадим приватный метод
_set_color(), в котором определим свойство, которое хотим изменить. В этом методе
мы можем использовать таблицы стилей для обновления цвета текста. Свойство
pyqtProperty, color, является именем нашего нового свойства.

ГЛАВА 11 ГРАФИКА И АНИМАЦИЯ В PYQT 297


В Листинге 11-29 устанавливается класс MainWindow и строится метод
setUpMainWindow(). Главное окно состоит из двух виджетов - флажка и кнопки,
расположенных в QVBoxLayout.
Листинг 11-29. Настройка класса MainWindow и метода setUpMainWindow() в
примере с анимацией виджетов
# animate_widgets.py
class MainWindow(QWidget):

def __init__(self):
super().__init__()
self.initializeUI()

def initializeUI(self):
"""Настройка графического интерфейса приложения."""
self.setFixedSize(240, 120)
self.setWindowTitle("Анимированные виджеты")

self.setUpMainWindow()
self.show()

def setUpMainWindow(self):
"""Создание и расположение виджетов в главном окне."""
self.update_cb = AnimatedCheckbox("Нормальный")
self.update_cb.stateChanged.connect(self.stopFlashing)
self.status_button = QPushButton("Статус изменен")
self.status_button.clicked.connect(
self.startAnimations)

# Создание экземпляров анимации


self.cb_anim = QPropertyAnimation(
self.update_cb, b"color")
self.button_anim = QPropertyAnimation(
self.status_button, b"geometry")
self.seq_group = QSequentialAnimationGroup()

main_v_box = QVBoxLayout()
main_v_box.addWidget(self.update_cb)
main_v_box.addWidget(self.status_button)
self.setLayout(main_v_box)

if __name__ == '__main__':
app = QApplication(sys.argv)
window = MainWindow()
sys.exit(app.exec())
298 ГЛАВА 11 ГРАФИКА И АНИМАЦИЯ В PYQT
Объект update_cb вначале отображает Normal. Его сигнал stateChanged подключен
к слоту stopFlashing(), код которого мы приведем в Листинге 11-31.
Для status_button щелчок на кнопке запускает различные анимации. В
setUpMainWindow() инстанцируются два объекта анимации свойств и одна группа
для управления анимациями.
Объект cb_anim анимирует цвет update_cb, а button_anim - свойство geometry для
кнопки status_button. С помощью геометрии мы получим координаты x и y, а также
размер кнопки. Наконец, seq_group будет управлять анимациями и проигрывать их
по порядку.
Геометрия виджета может быть получена с помощью метода geometry().
Переменная start_geometry в Листинге 11-30 содержит начальные значения QPoint и
QSize кнопки status_button. (Геттер geometry() фактически возвращает объект QRect,
а прямоугольник можно разбить на значения положения и размера верхнего левого
угла).
Листинг 11-30. Код для слота startAnimations() в примере с анимирующими
виджетами
# animate_widgets.py
def startAnimations(self):
"""Воспроизведение анимации и обновление состояний виджетов."""
# Соберите начальные значения геометрии кнопки.
# start_geometry - объект QRect
start_geometry = self.status_button.geometry()

# Настройка анимации изменения размера кнопки


self.button_anim.setEasingCurve(
QEasingCurve.Type.InOutSine)
self.button_anim.setDuration(1000)
self.button_anim.setStartValue(start_geometry)
self.button_anim.setKeyValueAt(0.5, QRect(QPoint(
start_geometry.x() - 4, start_geometry.y() - 4),
QSize(start_geometry.width() + 8,
start_geometry.height() + 8)))
self.button_anim.setEndValue(start_geometry)

# Отключить флажок, если он переключен


if self.update_cb.isChecked():
self.update_cb.toggle()
self.update_cb.setText("КРАСНАЯ ТРЕВОГА!")

# Настройте анимацию изменения цвета флажка цвета


self.cb_anim.setDuration(500)
self.cb_anim.setLoopCount(-1)
self.cb_anim.setStartValue(QColor(0, 0, 0))
self.cb_anim.setEndValue(QColor(255, 0, 0))
ГЛАВА 11 ГРАФИКА И АНИМАЦИЯ В PYQT 299
# Запуск последовательной последовательности
self.seq_group.addAnimation(self.button_anim)
self.seq_group.addAnimation(self.cb_anim)
self.seq_group.start()

# Наконец, отключите кнопку


self.status_button.setEnabled(False)

Получив геометрию кнопки, зададим несколько параметров для ее анимации.


Сглаживающие кривые в анимации позволяют сделать более плавными визуальные
переходы от одной анимации к другой. QEasingCurve используется вместе со
значениями start, stop или ключевого кадра для управления переходом анимации.
Здесь мы будем использовать перечисление QEasingCurve.Type для задания кривой
смягчения. Флаг InOutSine использует синусоидальную кривую для анимации кнопки.
Полный список кривых смягчения можно найти на сайте
https://doc.qt.io/qt-6/qeasingcurve.html#Type-enum.
Анимация будет происходить в течение одной секунды. Первый аргумент
функции setKeyValue() используется для задания ключевого кадра, который является
точкой половины анимации. Объект QRect, переданный в setKeyValue(), используется
для увеличения размера кнопки status_button. Это поможет сохранить центр кнопки
во время анимации. Через полсекунды виджет снова уменьшится до исходной
геометрии.
Для одиночной анимации следующим шагом будет вызов start(). Однако, поскольку
cb_anim будет следовать за анимацией кнопки, запуск анимации будет выполняться
экземпляром QSequentialAnimationGroup.
Также выполняется несколько проверок для обновления состояния и текста
QCheckBox. Используя cb_anim и счетчик циклов, равный -1, текст флажка будет
мигать красным цветом до тех пор, пока пользователь не установит флажок. Мы
отключим кнопку, чтобы пользователь был вынужден это сделать.
Когда флажок будет установлен, сигнал stateChanged вызовет слот stopFlashing() в
Листинге 11-31.
Листинг 11-31. Код для слота stopFlashing() и closeEvent() в примере с
анимирующими виджетами
# animate_widgets.py
def stopFlashing(self):
"""Остановка анимации при установленном флажке."""
self.seq_group.stop()
# Обновление виджетов
self.update_cb.setText("Normal")
self.update_cb.setStyleSheet("color: rgb(0, 0, 0)")
self.status_button.setEnabled(True)

300 ГЛАВА 11 ГРАФИКА И АНИМАЦИЯ В PYQT


def closeEvent(self, event):
"""Во избежание ошибок убедитесь, что анимация
останавливается при закрытии окна."""
running = QAbstractAnimation.State.Running
if self.seq_group.state == running:
self.seq_group.stop()
event.accept()

Сначала объект seq_group останавливает обе анимации. Затем значения и


состояния виджетов возвращаются в нормальное состояние. Наконец, событие
closeEvent() останавливает анимацию, если она еще запущена.
Хотя это всего лишь простой пример, использование рисования и анимации
может быть использовано для привлечения внимания, создания увлекательных
интерфейсов, а также для четкого и эффективного донесения информации о цели
лучше, чем это может сделать любой стандартный или статичный графический
интерфейс.

Резюме
Система графики и рисования в PyQt6 - это обширная тема, которая сама по
себе могла бы стать целой книгой. Класс QPainter важен для выполнения рисования
на виджетах и других устройствах рисования. QPainter работает вместе с классами
QPaintEngine и QPaintDevice, предоставляя инструменты, необходимые для создания
приложений двумерного рисования.
Мы рассмотрели некоторые функции класса QPainter для рисования линий,
примитивных и абстрактных фигур. Вместе с QPen, QBrush и QColor QPainter позволяет
создавать довольно красивые цифровые изображения. Чтобы материализовать
эту концепцию, мы создали простое приложение для рисования. Надеемся, что
вы усовершенствуете это приложение и добавите еще больше возможностей для
рисования.
Мы также рассмотрели, как создавать свойства для объектов, созданных на основе
класса QObject, и затем анимировать эти объекты в Qt Graphics View Framework.
Это не рассматривается в данной книге, но вы можете использовать графическое
представление для создания графического интерфейса с интерактивными
элементами. Мы также использовали QPropertyAnimation для анимирования свойств
виджетов.
В Главе 12 мы рассмотрим, как создавать пользовательские виджеты в PyQt.

ГЛАВА 11 ГРАФИКА И АНИМАЦИЯ В PYQT 301


ГЛАВА 12

Создание
пользовательских
виджетов
Хотя большинство задач разработки можно решить с помощью кнопок, виджетов
редактирования текста и других компонентов, предоставляемых PyQt, в какой-то
момент вы можете оказаться в ситуации, когда ни один виджет не обеспечивает
вас необходимыми инструментами или функциональностью. Вы даже можете
столкнуться с необходимостью использовать созданный вами виджет в других
графических интерфейсах, и тогда вам понадобится способ легко импортировать
созданный вами виджет в другие приложения. К счастью, PyQt позволяет
разработчикам создавать и импортировать собственные виджеты для решения
новых и непредвиденных задач.
В этой главе вы
• Узнаете о создании собственных пользовательских виджетов в PyQt
• Увидите, как применить созданный пользовательский виджет в небольшом
примере графического интерфейса
• Познакомитесь с четырьмя классами Qt для работы с изображениями
• Использовать новый виджет, QSlider, для выбора значений в ограниченном
диапазоне.
Давайте познакомимся с пользовательским виджетом, который мы построим в
следующих разделах.

Проект 12.1 - Пользовательский виджет RGB-ползунок


В проекте этой главы мы рассмотрим создание пользовательского
функционального виджета в PyQt. Хотя PyQt предлагает множество виджетов
для создания графических интерфейсов, время от времени может возникнуть
необходимость разработать и создать свой собственный. Одно из преимуществ
создания собственного виджета заключается в том, что можно либо создать общий
виджет, который можно использовать во многих различных приложениях, либо
сделать виджет, ориентированный на конкретное приложение и позволяющий
решить определенную задачу.
302 ГЛАВА 12 СОЗДАНИЕ ПОЛЬЗОВАТЕЛЬСКИХ ВИДЖЕТОВ
Для создания собственных виджетов можно использовать довольно много
приемов, большинство из которых мы уже рассматривали в предыдущих примерах.
• Изменение свойств виджетов PyQt с помощью встроенных методов, таких
как setAlignment(), setTextColor() и setRange().
• Создание таблиц стилей для изменения поведения и внешнего вида
виджетов.
• Подклассификация виджетов и повторная реализация обработчиков
событий, а также динамическое добавление свойств в классы QWidget
• Создание составных виджетов, состоящих из двух других типов виджетов и
расположенных вместе с помощью макета
• Разработка совершенно нового виджета, который является подклассом
QWidget и имеет свои собственные уникальные свойства и внешний вид.
RGB-ползунок, показанный на рис. 12-1, фактически создан путем комбинирования
нескольких из перечисленных ранее приемов. Виджет использует виджеты Qt
QSlider и QSpinBox для выбора RGB-значений и отображает цвет на виджетах QLabel.
Внешний вид ползунков изменяется с помощью таблиц стилей. Затем все виджеты
собираются в родительский виджет, который мы можем импортировать в другие
приложения PyQt.

Рисунок 12-1. Пользовательский виджет, используемый для выбора цвета с


помощью ползунков и регуляторов
Прежде чем приступить к созданию виджета RGB-ползунка, нам необходимо
узнать немного больше о некоторых классах, которые понадобятся нам для создания
приложения.

ГЛАВА 12 СОЗДАНИЕ ПОЛЬЗОВАТЕЛЬСКИХ ВИДЖЕТОВ 303


Классы PyQt для работы с изображениями
В предыдущих примерах мы работали с QPixmap для работы с данными
изображения. На самом деле Qt предоставляет четыре различных класса для работы
с изображениями, каждый из которых имеет свое особое назначение.
QPixmap является основным классом для отображения изображений на экране.
Пиксмапы могут быть представлены на различных виджетах, способных отображать
пиктограммы, включая QLabel и QPushButton. QImage оптимизирован для чтения,
записи и работы с изображениями и очень полезен, если необходимо напрямую
обращаться к пиксельным данным изображения и изменять их. QImage также может
выступать в качестве устройства рисования. Устройство рисования (создаваемое
классом QPaintDevice) представляет собой двумерную поверхность, на которой
можно рисовать с помощью QPainter. Следует также отметить, что QImage наследует
QPaintDevice.
Также возможна конвертация между QImage и QPixmap. Один из вариантов
совместного использования этих двух классов - загрузка файла изображения с
помощью QImage, манипулирование данными изображения, а затем преобразование
изображения в пиксмап перед выводом его на экран. В качестве примера конвертации
между двумя классами можно привести виджет ползунка RGB.
QBitmap является подклассом QPixmap и предоставляет монохромные (с глубиной
1 бит) пиксмапы. QPicture - это устройство рисования, которое воспроизводит
команды QPainter, то есть вы можете создать картинку из любого формата
изображения, на котором вы рисуете. Изображения, созданные с помощью QPicture,
не зависят от разрешения и выглядят одинаково независимо от используемого
формата, например, png, svg или pdf.
Ползунок RGB использует два типа виджетов для выбора значений RGB: QSpinBox,
который был представлен в Главе 4, и новый виджет.

Виджет QSlider
Класс QSlider предоставляет разработчику инструмент для выбора целочисленных
значений в ограниченном диапазоне. Ползунки предоставляют пользователям
удобное средство для быстрого выбора значений или изменения настроек всего лишь
простым движением ручки. По умолчанию ползунки располагаются вертикально
(задается флагом Qt.Orientation.Vertical), но это можно изменить, передав конструктору
флаг Qt.Orientation.Horizontal.
Следующий блок кода демонстрирует, как создать экземпляр QSlider, установить
максимальное значение диапазона ползунка и подключиться к сигналу valueChanged,
который выдается при изменении значения ползунка.

304 ГЛАВА 12 СОЗДАНИЕ ПОЛЬЗОВАТЕЛЬСКИХ ВИДЖЕТОВ


slider = QSlider(Qt.Horizontal, self)
# Значения по умолчанию - от 0 до 99
slider.setMaximum(200)
slider.valueChanged.connect(self.printSliderValue)

def printSliderValue(self, value):


print(value)

Здесь максимальный диапазон ползунка равен 200, и его значение выводится на


экран при каждом изменении положения ползунка.

Пояснения к виджету RGB-ползунка


RGB-ползунок - это пользовательский виджет, созданный путем объединения не-
скольких встроенных виджетов Qt: QLabel, QSlider и QSpinBox. Внешний вид ползунков
настраивается с помощью таблиц стилей таким образом, чтобы они давали визуаль-
ную обратную связь пользователю о том, какое значение RGB регулируется. Ползун-
ки и регуляторы связаны между собой, чтобы их значения были синхронизированы
и чтобы пользователь мог видеть целое значение на шкале RGB. Значения RGB также
преобразуются в шестнадцатеричный формат и отображаются на виджете.
Ползунки и регуляторы могут использоваться как для поиска RGB или шест-
надцатеричных значений цвета, так и для использования переделанного метода
mousePressEvent(), чтобы пользователь мог щелкнуть на пикселе изображения и уз-
нать его RGB-значение. Пример этого показан в разделе "Демонстрация RGB-ползун-
ка", где также показано, как импортировать RGB-ползунок в демонстрационное при-
ложение.
В Листинге 12-1 нам необходимо импортировать достаточно много классов. Клас-
сы для работы с изображениями в PyQt находятся в модуле QtGui. Еще один класс,
заслуживающий упоминания, qRgb, фактически является типизацией, создающей
unsigned int (Тип данных unsigned int - беззнаковое целое число, также как и тип int
(знаковое) занимает в памяти 2 байта. Но в отличие от int, тип unsigned int может
хранить только положительные целые числа в диапазоне от 0 до 65535 (2^16)-1).),
представляющий триплет значений RGB (r, g, b). typedef в C++ - это ключевое слово,
которое используется для создания нового имени типа данных, в данном случае для
представления значения RGB.
Листинг 12-1. Импорт для ползунка RGB
# rgb_slider.py
# Импорт необходимых модулей
import sys
from PyQt6.QtWidgets import (QApplication, QWidget, QLabel,
QSlider, QSpinBox, QHBoxLayout, QVBoxLayout, QGridLayout)
from PyQt6.QtGui import QImage, QPixmap, QColor, qRgb, QFont
from PyQt6.QtCore import Qt
ГЛАВА 12 СОЗДАНИЕ ПОЛЬЗОВАТЕЛЬСКИХ ВИДЖЕТОВ 305
Таблица стилей, приведенная в Листинге 12-2, используется для изменения
внешнего вида ползунков. Мы хотим изменить их внешний вид таким образом,
чтобы они давали пользователю больше информации о том, какие значения RGB
изменяются. Каждому ползунку присваивается идентификационный селектор с
помощью метода setObjectName() в методе setUpMainWindow(). Если в таблице стилей
не используется селектор ID, то этот стиль применяется ко всем объектам QSlider.
Листинг 12-2. Таблица стилей для RGB-ползунка, часть 1
# rgb_slider.py
style_sheet = """
QSlider:groove:horizontal{
border: 1px solid #000000;
background: white;
height: 10 px;
border-radius: 4px
}

QSlider#Red:sub-page:horizontal{
background: qlineargradient(x1:1, y1:0, x2:0, y2:1,
stop: 0 #FF4242, stop: 1 #1C1C1C);
background: qlineargradient(x1:0, y1:1, x2:1, y2:1,
stop: 0 #1C1C1C, stop: 1 #FF0000);
border: 1px solid #4C4B4B;
height: 10px;
border-radius: 4px;
}

QSlider::add-page:horizontal {
background: #FFFFFF;
border: 1px solid #4C4B4B;
height: 10px;
border-radius: 4px;
}

QSlider::handle:horizontal {
background: qlineargradient(x1:0, y1:0, x2:1, y2:1,
stop: 0 #EEEEEE, stop: 1 #CCCCCC);
border: 1px solid #4C4B4B;
width: 13px;
margin-top: -3px;
margin-bottom: -3px;
border-radius: 4px;
}

306 ГЛАВА 12 СОЗДАНИЕ ПОЛЬЗОВАТЕЛЬСКИХ ВИДЖЕТОВ


QSlider::handle:horizontal:hover {
background: qlineargradient(x1:0, y1:0, x2:1, y2:1,
stop: 0 #FFFFFF, stop: 1 #DDDDDD);
border: 1px solid #393838;
border-radius: 4px;
}

Ползунки используют линейные градиенты, чтобы пользователи могли получить


наглядное представление о том, какая часть красного, зеленого и синего цветов
используется. При использовании линейных градиентов цвет интерполируется от
x1, y1 до x2, y2. Псевдосостояние horizontal используется для указания того, что стили
будут применяться к горизонтальным объектам QSlider.
Подконтроль groove (паз) относится к длинной прямоугольной части ползунка,
которая перед перемещением ручки ползунка имеет сплошной белый цвет.
Подконтроль add-page обозначает цвет частей ползунка до ручки, а sub-page - после.
Для ручки цвет будет меняться при наведении на нее мыши.
Единственные изменения, которые необходимо внести для Green и Blue ползунков,
касаются подконтролей sub-page. Эти изменения выполняются в Листинге 12-3. Для
ознакомления с таблицами стилей можно также обратиться к Главе 6.
Листинг 12-3. Таблица стилей для ползунка RGB, часть 2
# rgb_slider.py
QSlider#Green:sub-page:horizontal{
background: qlineargradient(x1:1, y1:0, x2:0, y2:1,
stop: 0 #FF4242, stop: 1 #1C1C1C);
background: qlineargradient(x1:0, y1:1, x2:1, y2:1,
stop: 0 #1C1C1C, stop: 1 #00FF00);
border: 1px solid #4C4B4B;
height: 10px;
border-radius: 4px;
}

QSlider#Blue:sub-page:horizontal{
background: qlineargradient(x1:1, y1:0, x2:0, y2:1,
stop: 0 #FF4242, stop: 1 #1C1C1C);
background: qlineargradient(x1:0, y1:1, x2:1, y2:1,
stop: 0 #1C1C1C, stop: 1 #0000FF);
border: 1px solid #4C4B4B;
height: 10px;
border-radius: 4px;
}
"""
Класс RGBSlider наследует QWidget в Листинге 12-4. Для этого класса пользователь
может передавать в конструктор изображение и другие аргументы в качестве
параметров.
ГЛАВА 12 СОЗДАНИЕ ПОЛЬЗОВАТЕЛЬСКИХ ВИДЖЕТОВ 307
Листинг 12-4. Код для начала построения класса RGBSlider
# rgb_slider.py
class RGBSlider(QWidget):

def __init__(self, _image=None, *args, **kwargs):


super().__init__(*args, **kwargs)
self._image = _image
self.initializeUI()

def initializeUI(self):
"""Настройка графического интерфейса приложения."""
self.setMinimumSize(225, 300)
self.setWindowTitle("12.1 - RGB Ползунок")

# Сохранить текущее значение пикселя


self.current_val = QColor()

self.setUpMainWindow()
self.show()

if __name__ == '__main__':
app = QApplication(sys.argv)
app.setStyleSheet(style_sheet)
window = RGBSlider()
sys.exit(app.exec())

Переменная экземпляра current_val будет использоваться для отслеживания


текущего значения цвета RGB. Цвет, конечно же, будет складываться из значений
ползунка и поля вращения.
В функции setUpMainWindow() в Листинге 12-5 создается объект QImage, который
будет отображать цвет, созданный из значений RGB. При использовании метода
QImage fill() первым цветом, который отобразится при запуске приложения, будет
черный. Для отображения изображения в виджете сначала преобразуем QImage в
QPixmap с помощью метода QPixmap fromImage() и передаем ему экземпляр QImage.
Затем с помощью функции setPixmap() установите пиксмап виджета QLabel.

308 ГЛАВА 12 СОЗДАНИЕ ПОЛЬЗОВАТЕЛЬСКИХ ВИДЖЕТОВ


Листинг 12-5. Код метода setUpMainWindow() в классе RGBSlider, часть 1
# rgb_slider.py
def setUpMainWindow(self):
"""Создание и расположение виджетов в главном окне."""
# Изображение, на котором будет отображаться текущий цвет,
# заданный значениями ползунка/регулятора
self.color_display = QImage(
100, 100, QImage.Format.Format_RGBX64)
self.color_display.fill(Qt.GlobalColor.black)

self.cd_label = QLabel()
self.cd_label.setPixmap(
QPixmap.fromImage(self.color_display))
self.cd_label.setScaledContents(True)

Затем содержимое cd_label масштабируется в соответствии с размерами окна.

Обновление Ползунков и Регуляторов


Далее мы создаем красный, зеленый и синий виджеты QSlider и QSpinBox в
Листинге 12-6. Максимальные значения ползунков установлены на 255, поскольку
значения RGB находятся в диапазоне 0-255. Каждому ползунку также присваивается
имя объекта, которое используется для его идентификации в таблице стилей.
Листинг 12-6. Код метода setUpMainWindow() в классе RGBSlider, часть 2
# rgb_slider.py
# Создание RGB-ползунков и регуляторов
red_label = QLabel("Красный")
red_label.setFont(QFont("Helvetica", 14))
self.red_slider = QSlider(Qt.Orientation.Horizontal)
self.red_slider.setObjectName("Red")
self.red_slider.setMaximum(255)

self.red_spinbox = QSpinBox()
self.red_spinbox.setMaximum(255)

green_label = QLabel("Зелёный")
green_label.setFont(QFont("Helvetica", 14))
self.green_slider = QSlider(Qt.Orientation.Horizontal)
self.green_slider.setObjectName("Green")
self.green_slider.setMaximum(255)

ГЛАВА 12 СОЗДАНИЕ ПОЛЬЗОВАТЕЛЬСКИХ ВИДЖЕТОВ 309


self.green_spinbox = QSpinBox()
self.green_spinbox.setMaximum(255)

blue_label = QLabel("Синий")
blue_label.setFont(QFont("Helvetica", 14))
self.blue_slider = QSlider(Qt.Orientation.Horizontal)
self.blue_slider.setObjectName("Blue")
self.blue_slider.setMaximum(255)

self.blue_spinbox = QSpinBox()
self.blue_spinbox.setMaximum(255)

Две метки, инстанцированные в Листинге 12-7, будут отображать


шестнадцатеричное значение цвета. Затем они располагаются в QHBoxLayout, который
задается в качестве макета для hex_container.
Листинг 12-7. Код метода setUpMainWindow() в классе RGBSlider, часть 3
# rgb_slider.py
# Используйте шестнадцатеричные метки для отображения
# значений цвета в шестнадцатеричном формате
hex_label = QLabel("Hex Color ")
self.hex_values_label = QLabel()

hex_h_box = QHBoxLayout()
hex_h_box.addWidget(
hex_label, Qt.AlignmentFlag.AlignRight)
hex_h_box.addWidget(self.hex_values_label,
Qt.AlignmentFlag.AlignRight)
hex_container = QWidget()
hex_container.setLayout(hex_h_box)

# Создание макета сетки для ползунков и регуляторов


grid = QGridLayout()
grid.addWidget(
red_label, 0, 0, Qt.AlignmentFlag.AlignLeft)
grid.addWidget(self.red_slider, 1, 0)
grid.addWidget(self.red_spinbox, 1, 1)
grid.addWidget(
green_label, 2, 0, Qt.AlignmentFlag.AlignLeft)
grid.addWidget(self.green_slider, 3, 0)
grid.addWidget(self.green_spinbox, 3, 1)
grid.addWidget(
blue_label, 4, 0, Qt.AlignmentFlag.AlignLeft)
grid.addWidget(self.blue_slider, 5, 0)
grid.addWidget(self.blue_spinbox, 5, 1)
grid.addWidget(hex_container, 6, 0, 1, 0)
310 ГЛАВА 12 СОЗДАНИЕ ПОЛЬЗОВАТЕЛЬСКИХ ВИДЖЕТОВ
После этого ползунки, регуляторы и контейнер для меток организуются в
QGridLayout.

Обновление цветов
Ползунки QSlider и QSpinBox могут выдавать сигнал valueChanged. Мы можем
связать ползунки и регуляторы так, чтобы их значения изменялись относительно друг
друга. Например, когда red_slider издает сигнал, он запускает слот updateRedSpinBox(),
который затем обновляет значение red_spinbox с помощью setValue(). Аналогичный
процесс происходит и для red_spinbox. Этот процесс также происходит для ползунков
и регуляторов, управляющих значениями синего и зеленого цветов.
Посмотрите на сигналы valueChanged в Листинге 12-8 для ползунка и
соответствующего ему регулятора, и вы заметите, что они запускают слоты, которые
обновляют друг друга.
Листинг 12-8. Код метода setUpMainWindow() в классе RGBSlider, часть 4
# rgb_slider.py
# Ползунки и регуляторы для каждого цвета должны
# отображать одинаковые значения и обновляться
# одновременно
self.red_slider.valueChanged.connect(
self.updateRedSpinBox)
self.red_spinbox.valueChanged.connect(
self.updateRedSlider)
self.green_slider.valueChanged.connect(
self.updateGreenSpinBox)
self.green_spinbox.valueChanged.connect(
self.updateGreenSlider)
self.blue_slider.valueChanged.connect(
self.updateBlueSpinBox)
self.blue_spinbox.valueChanged.connect(
self.updateBlueSlider)

# Создание контейнера для rgb-виджетов


rgb_widgets = QWidget()
rgb_widgets.setLayout(grid)

main_v_box = QVBoxLayout()
main_v_box.addWidget(self.cd_label)
main_v_box.addWidget(rgb_widgets)

self.setLayout(main_v_box)

ГЛАВА 12 СОЗДАНИЕ ПОЛЬЗОВАТЕЛЬСКИХ ВИДЖЕТОВ 311


Все виджеты вместе с cd_label из Листинга 12-5 содержатся в rgb_widgets и
расположены в основном макете.
Рассмотрим в Листинге 12-9 слоты для обновления значений виджетов.
Листинг 12-9. Код для слотов, обновляющих значения ползунка и регулятора
# rgb_slider.py
# Следующие слоты обновляют красный, зеленый
# и синий ползунки и регуляторы
def updateRedSpinBox(self, value):
self.red_spinbox.setValue(value)
self.redValue(value)

def updateRedSlider(self, value):


self.red_slider.setValue(value)
self.redValue(value)

def updateGreenSpinBox(self, value):


self.green_spinbox.setValue(value)
self.greenValue(value)

def updateGreenSlider(self, value):


self.green_slider.setValue(value)
self.greenValue(value)

def updateBlueSpinBox(self, value):


self.blue_spinbox.setValue(value)
self.blueValue(value)

def updateBlueSlider(self, value):


self.blue_slider.setValue(value)
self.blueValue(value)

Когда сигнал valueChanged запускает слот, он использует значение для обновления


соответствующего ползунка или регулятора, а затем вызывает функцию, которая
создает новый цвет из значений красного, зеленого или синего.
Мы рассмотрим один пример, поскольку остальные организованы аналогичным
образом. При изменении значения red_slider будет вызван слот updateRedSpinBox(), а
значение red_spinbox будет установлено в value. Далее перейдем к Листингу 12-10 для
обработки создания новых цветов.

312 ГЛАВА 12 СОЗДАНИЕ ПОЛЬЗОВАТЕЛЬСКИХ ВИДЖЕТОВ


Листинг 12-10. Код методов, создающих и обновляющих цвет
# rgb_slider.py
# Создание новых цветов на основе изменения значений RGB
def redValue(self, value):
new_color = qRgb(value,
self.current_val.green(), self.current_val.blue())
self.updateColorInfo(new_color)

def greenValue(self, value):


new_color = qRgb(self.current_val.red(),
value, self.current_val.blue())
self.updateColorInfo(new_color)

def blueValue(self, value):


new_color = qRgb(self.current_val.red(),
self.current_val.green(), value)
self.updateColorInfo(new_color)

def updateColorInfo(self, color):


"""Обновить цвет, отображаемый на изображении, и установить
соответствующие шестнадцатеричные значения."""
self.current_val = QColor(color)
self.color_display.fill(color)

self.cd_label.setPixmap(QPixmap.fromImage(
self.color_display))
self.hex_values_label.setText(
f"{self.current_val.name()}")

Продолжая работу с красным цветом, функция redValue() создает новый цвет


qRgb, используя новое значение красного цвета, а также цвета green() и blue()
переменной current_val. Переменная current_val является экземпляром QColor. Класс
QColor содержит функции, которые мы можем использовать для доступа к значениям
RGB (или другого цветового формата) изображения.
Значение new_color передается в функцию updateColorInfo(). Аналогичным
образом обрабатываются зеленый и синий цвета. Далее необходимо создать QColor
из значения qRgb и сохранить его в current_val. С помощью fill() обновляется QImage
color_display, который затем преобразуется в QPixmap и отображается на метке
cd_label.
В последнюю очередь обновляются шестнадцатеричные метки с помощью
QColor.name(). (Помните, что current_val - это объект QColor.) Эта функция возвращает
имя цвета в формате "#RRGGBB".

ГЛАВА 12 СОЗДАНИЕ ПОЛЬЗОВАТЕЛЬСКИХ ВИДЖЕТОВ 313


Добавление методов в пользовательский виджет
Варианты методов, которые можно создать для пользовательского виджета,
многочисленны. Одним из вариантов является создание методов, позволяющих
пользователю изменять поведение или внешний вид пользовательского виджета.
Другой вариант - использовать обработчики событий для проверки событий
клавиатуры или мыши, которые могут быть использованы для взаимодействия с
графическим интерфейсом.
Метод getPixelValue() в Листинге 12-11 представляет собой реализацию обработ
чика события mousePressEvent(). Если в конструктор RGBSlider передается
изображение, то _image не является None, и пользователь может щелкнуть на точках
изображения, чтобы получить соответствующие им значения пикселей. QColor.pixel()
получает RGB-значения пикселей. Затем значение параметра current_val обновляется,
чтобы использовать значения красного, синего и зеленого цветов выбранного
пикселя. Эти значения затем передаются обратно в функции, которые обновляют
ползунки, регуляторы, метки и QImage.
Листинг 12-11. Код для метода getPixelValues()
# rgb_slider.py
def getPixelValues(self, event):
"""Метод переимитирует метод mousePressEvent.
Для его использования необходимо установить
mousePressEvent виджета равным getPixelValues,
например: image_label.mousePressEvent = rgbslider.getPixelValues
Если _image != None, то пользователь может выбрать пиксель
на изображении и обновить ползунки для просмотра цвета,
а также получить rgb и hex значения."""
x = int(event.position().x())
y = int(event.position().y())

# valid() возвращает true, если выбранная точка является


# допустимой парой координат на растре
if self._image.valid(x, y):
self.current_val = QColor(self._image.pixel(x, y))
red_val = self.current_val.red()
green_val = self.current_val.green()
blue_val = self.current_val.blue()

self.updateRedSpinBox(red_val)
self.updateRedSlider(red_val)
self.updateGreenSpinBox(green_val)
self.updateGreenSlider(green_val)
self.updateBlueSpinBox(blue_val)
self.updateBlueSlider(blue_val)

314 ГЛАВА 12 СОЗДАНИЕ ПОЛЬЗОВАТЕЛЬСКИХ ВИДЖЕТОВ


Запустите скрипт и посмотрите, как он работает. Сейчас приложение
представляет собой просто небольшой графический интерфейс. Давайте посмотрим,
как использовать класс пользовательского виджета в другом приложении для
использования функции выбора цвета.

Демонстрация RGB-ползунка
Одна из причин создания пользовательского виджета заключается в том, что его
можно использовать в других приложениях. Следующая программа представляет
собой краткий пример импорта и настройки RGB-ползунка, встроенного в
Проект 12.1. В данном примере в окне рядом с ползунком RGB отображается
изображение. Пользователи могут щелкать по точкам изображения и видеть, как в
реальном времени изменяются RGB и шестнадцатеричные значения.
Графический интерфейс этой короткой программы показан на рис. 12-2.

Рисунок 12-2. Пример графического интерфейса с пользовательским ползунком


RGB. Изображение с сайта www.pixilart.com/

ГЛАВА 12 СОЗДАНИЕ ПОЛЬЗОВАТЕЛЬСКИХ ВИДЖЕТОВ 315


Пояснения к демонстрации RGB-ползунка
Обязательно загрузите изображение из папки images в репозитории GitHub.
Для начала работы с этой программой можно использовать класс basic_window.py
из Главы 1. Начните с импорта нескольких классов, включая RGB-ползунок и таблицу
стилей из rgb_slider.py, в Листинге 12-12.
Листинг 12-12. Код, показывающий пример использования виджета RGB-ползунка
# rgb_demo.py
# Import necessary modules
import sys
from PyQt6.QtWidgets import (QApplication, QWidget, QLabel, QHBoxLayout)
from PyQt6.QtGui import QPixmap, QImage
from PyQt6.QtCore import Qt
from rgb_slider import RGBSlider, style_sheet

class MainWindow(QWidget):

def __init__(self):
super().__init__()
self.initializeUI()

def initializeUI(self):
"""Настройка графического интерфейса приложения."""
self.setMinimumSize(225, 300)
self.setWindowTitle("12.2 - Пример пользовательского виджета")

# Загрузить изображение
image = QImage("images/duck_pic.png")

# Создание экземпляра виджета RGB-ползунка


rgbslider = RGBSlider(image)
image_label = QLabel()
image_label.setAlignment(Qt.AlignmentFlag.AlignTop)
image_label.setPixmap(QPixmap().fromImage(image))

# Реализация события mousePressEvent метки


image_label.mousePressEvent = rgbslider.getPixelValues
h_box = QHBoxLayout()
h_box.addWidget(rgbslider)
h_box.addWidget(image_label)
self.setLayout(h_box)
self.show()

316 ГЛАВА 12 СОЗДАНИЕ ПОЛЬЗОВАТЕЛЬСКИХ ВИДЖЕТОВ


if __name__ == '__main__':
app = QApplication(sys.argv)
app.setStyleSheet(style_sheet)
window = MainWindow()
sys.exit(app.exec())

В классе MainWindow в initializeUI() настраиваем окно, загружаем изображение и


создаем экземпляр ползунка RGB. Затем в окне располагаются виджеты.
В данном приложении мы по-прежнему создаем изображение как экземпляр
QImage, а затем преобразуем его в QPixmap. QImage используется для того, чтобы мы
имели доступ к информации о пикселях изображения.
Чтобы использовать метод getPixelValues() в классе RGBSlider, нам необходимо
переделать обработчик события мыши объекта QLabel. Когда пользователь щелкает
на пикселе изображения, координаты x и y из этого события используются для
обновления значений в виджете RGB-ползунка с помощью метода getPixelValues().
Если вы хотите использовать ползунок только для получения различных RGB-
или шестнадцатеричных значений, то на этом приложение закончено. Но вы можете
продолжить добавлять другие функциональные возможности к RGB-ползунку для
использования в своих собственных проектах.

Резюме
Не все проблемы можно решить с помощью виджетов, предоставляемых Qt.
В ситуациях, когда требуется проявить изобретательность, PyQt очень удобен,
поскольку позволяет разработчикам создавать и настраивать собственные виджеты.
Это можно сделать различными способами, например, создать новый виджет на
основе уже существующих или создать совершенно новый виджет с нуля. После
этого новый виджет может быть легко импортирован в другие приложения.
В Главе 13 вы узнаете, как создавать современные графические интерфейсы с
помощью Qt Quick.

ГЛАВА 12 СОЗДАНИЕ ПОЛЬЗОВАТЕЛЬСКИХ ВИДЖЕТОВ 317


ГЛАВА 13

Работа с Qt Quick
Графические пользовательские интерфейсы сегодня можно встретить на мно
жестве устройств, включая настольные компьютеры, мобильные устройства
и небольшие сенсорные экраны, управляемые микроконтроллерами. Хотя
пользовательские интерфейсы разрабатываются в соответствии с функциональными
и техническими потребностями каждого устройства, требования этих платформ
побудили компанию Qt продолжить создание набора масштабируемых, отточенных,
динамичных и визуально ошеломляющих инструментов для построения
пользовательских интерфейсов.
В этой главе вы
• Получите обзор модулей QtQuick и QtQml, а также языка программирования QML
• Узнаете, как писать и запускать простые приложения с использованием QML и
PyQt
• Узнаете, как создавать компоненты QML и использовать их в других файлах .qml
• Использовать различные методы расположения элементов QtQuick с помощью
QML
• Экспериментировать с различными типами QML для создания приложений
• Использовать простые преобразования для анимации объектов
Цель этой главы - дать общее представление о QtQuick и языке Qt QML. Если ваша
цель - дальнейшее использование QtWidgets, то эта глава может не дать информации,
полезной на данном этапе разработки. Однако мы надеемся, что для начинающих
разработчиков PyQt эта глава может дать несколько полезных идей и представлений
о том, что могут предложить последние версии Qt и PyQt.

Примечание. Для читателей, использующих macOS, могут возникнуть


проблемы с запуском приложений, описанных в этой главе, если вы
используете оболочку Z, известную также как zsh. До недавнего времени в
macOS по умолчанию использовалась оболочка bash. Если у вас возникли
проблемы с zsh, вы можете переключиться на оболочку bash, введя в
командной строке команду chsh -s /bin/bash/. Для обратного переключения
на zsh по завершении работы необходимо ввести команду chsh -s /bin/zsh.
Следует помнить, что при переключении между оболочками необходимо
будет либо установить PyQt6 из PyPI, либо изменить пути в bash, чтобы найти
PyQt6 и другие пакеты Python.

318 ГЛАВА 13 РАБОТА С QT QUICK


Описания QtQuick и QML
Хотя настольные приложения по-прежнему составляют большую часть Qt, в Qt 6
была проделана значительная работа по созданию инструментария, напоминающего
более плавные, динамичные и анимированные пользовательские интерфейсы
мобильных и встраиваемых устройств. По мере изучения этой главы (или по ссылкам
в этой главе, которые ведут к еще большему количеству информации о создании
графических интерфейсов с помощью QtQuick) вы заметите ряд сходств с QtWidgets.
Вы заметите такие визуальные элементы, как кнопки и комбобоксы. Вы увидите окна,
диалоги и меню. Для расположения элементов в QtQuick, помимо прочих методов,
также используются макеты. Мы даже вкратце рассмотрим анимацию объектов.
Что же представляет собой QtQuick? Прежде чем ответить на этот вопрос, давайте
попробуем устранить некоторую путаницу, которая может возникнуть в самом
начале. Для создания приложений нам потребуется прояснить три важных термина:
QML, QtQuick и QtQml.
При использовании QtQuick вы также услышите о языке моделирования Qt
(QML). QML - это декларативный язык разметки, специально разработанный для
пользовательских интерфейсов, на котором построен QtQuick. Этот язык используется
для создания очень подвижных и динамичных интерфейсов и визуальных эффектов,
аналогичных тем, которые можно наблюдать на мобильных устройствах. Файл QML,
также называемый документом (более подробную информацию о документах QML
можно найти на сайте https://doc.qt.io/qt-6/qtqml-documents-topic.html.), состоит из
декларативного иерархического дерева элементов и имеет поддержку различных
выражений JavaScript (информацию о выражениях JavaScript в QML см. в разделе
https://doc.qt.io/qt-6/qtqml-javascript-expressions.html). Документ QML может
создавать окно приложения или создавать многократно используемый элемент,
называемый компонентом. По сути, QML используется для создания объектов
пользовательского интерфейса и определения их взаимосвязи друг с другом.
Построив взаимосвязи, модуль QtQuick используется для описания внешнего
вида и поведения элементов графического интерфейса. Стоит отметить, что, как и
в QtWidgets, QtQuick также содержит классы для визуального холста, графических
элементов, макетов, моделей данных и представлений, анимации, графики и многого
другого!
В дополнение к QtQuick мы будем использовать модуль QtQml, который
предоставляет объект QQmlEngine для доступа к создаваемому нами QML-контенту.
Движок вместе с объектом QQmlContext для передачи данных компонентам QML
используется для того, чтобы открыть Python для создаваемого нами QML-кода.
К счастью, QtQml предоставляет удобный класс QQmlApplicationEngine, который
объединяет движок и контекст. Мы будем использовать QQmlApplicationEngine для
загрузки наших QML-файлов.
Открыв Python для компонентов QML, мы можем использовать QML для
написания внешнего кода и использовать Python и PyQt для построения внутренней
логики.

ГЛАВА 13 РАБОТА С QT QUICK 319


Примечание. Небольшое замечание о структуре этой главы. Чтобы
сосредоточиться на эффективности, мы сначала расскажем о том, как создать
базовый QML-документ, а затем добавим простые визуальные элементы,
такие как текст и изображения, чтобы вы почувствовали себя комфортно.
Затем мы обсудим, как организовать элементы в графическом интерфейсе.
Далее мы узнаем, как создать окно QML, дополненное строкой меню, а
также познакомимся с сигналами и слотами в QML. Наконец, мы вкратце
рассмотрим анимацию элементов.

Если после завершения этой главы вы поймете, что QtQuick и QML вас
заинтересовали, или если у вас возникнут вопросы по ходу работы, то следующие
ссылки помогут вам получить дополнительную информацию и рекомендации:
• Краткий учебник по QML: https://doc.qt.io/qt-6/qml-tutorial.html
• Онлайновая книга по QML, предоставленная компанией The Qt Company:
www.qt.io/product/qt6/qml-book
• Информация по работе с PyQt и QML:
www.riverbankcomputing.com/static/Docs/PyQt6/qml.html
• Информация о Qt Quick в Qt 6: https://doc.qt.io/qt-6/qtquick-index.html
Прежде чем приступить к созданию кода, давайте рассмотрим некоторые общие
элементы и свойства, которые мы будем использовать при создании приложений
QtQuick.

Элементы в QtQuick
Элементы, называемые также типами, являются встроенными строительными
блоками, используемыми для создания графических интерфейсов в QtQuick. Термин
"элементы" включает в себя как визуальные, так и невизуальные типы. Визуальные
элементы имеют геометрию и могут быть расположены в графическом интерфейсе,
а невизуальные элементы обычно используются для управления визуальными
элементами.
Чтобы создать экземпляр типа в QML, достаточно вызвать элемент, за которым
следует пара скобок. Пример создания элемента Rectangle показан в следующей
строке:
Rectangle {...}
Типы объектов всегда начинаются с заглавной буквы. Каждый элемент также имеет
ряд определенных свойств, таких как width (ширина), height (высота), color (цвет)
и text (текст), которые используются для задания различных аспектов элемента.
Свойства также могут использоваться для позиционирования элементов, задания
геометрических преобразований и обработки изменений состояния. Эти свойства
задаются между скобками.
В табл. 13-1 описаны некоторые распространенные и интересные элементы,
320 ГЛАВА 13 РАБОТА С QT QUICK
встречающиеся в QtQuick. Полный список типов QML можно найти на сайте
https://doc.qt.io/qt-6/qmltypes.html.

Таблица 13-1. Выбор распространенных типов QtQuick


Элемент Описание
Item Базовый элемент, от которого отталкиваются все визуальные
элементы. Сам по себе не создает визуальный элемент. Вместо
этого элемент определяет свойства других типов. Может также
использоваться в качестве контейнера для других элементов
Rectangle Наследует Item и добавляет визуальные свойства, включая color
(цвет), border (границы) и radius (радиус)
Image Отображает изображения. Предоставляет свойство source для
указания URL-адреса изображения и fillMode для управления
изменением размера
Text Отображает текст. Включает свойства text (текста), font (шрифта),
style (стиля) и alignment (выравнивания)
MouseArea Невизуальный тип, необходимый для захвата событий мыши.
MouseArea также включает такие свойства, как width (ширина) и
height (высота)
Flickable () Невизуальный тип, выполняющий роль перетаскиваемой и
пролистываемой поверхности для своих дочерних объектов.
Идеально подходит для отображения большого количества дочерних
объектов на прокручиваемой поверхности
Component Используется вместе с типом Loader для динамического создания
и загрузки компонентов в компонентном документе. Компоненты
обычно создаются и инстанцируются с помощью отдельных QML-
файлов

В следующем разделе рассмотрим, как использовать несколько элементов из


табл. 13-1 для создания QML-документа с расширением .qml.

Введение в язык и синтаксис QML


Один из лучших способов начать понимать новый язык - это сразу же перейти
к написанию кода. В этом разделе мы рассмотрим QML-документ и разберем
древовидную структуру QML. Если вы никогда не писали на JavaScript или любом
другом декларативном языке, не стоит беспокоиться. Существует очень простая
схема написания QML-кода. Создав простой графический интерфейс, показанный
на рис. 13-1, вы узнаете несколько очень важных концепций, в том числе как
• Импортировать типы объектов QML в документ (также можно
импортировать ресурсы JavaScript)
• Создавать родительские и дочерние элементы QML
ГЛАВА 13 РАБОТА С QT QUICK 321
• Определять свойства для различных типов элементов
• Вручную располагать элементы, используя систему координат визуального
холста
• Создание простого компонента многократного использования
• Понимание простых понятий синтаксиса QML

Рисунок 13-1. Пользовательский компонент QtQuick с текстом


После работы с QtWidgets вы можете заметить, что в строке заголовка окна нет
заголовка. Это связано с тем, что в данном примере не создается окно, а создается
QML-компонент, родительским объектом которого является Rectangle. У Rectangle
не только нет свойства title, но, создав компонент, мы можем при желании
инстанцировать этот объект в другом QML-документе. Пример этого мы увидим в
разделе "Обработка макетов в QML".

Пояснения к языку и синтаксису QML


Первым шагом является импорт типов объектов в Листинге 13-1. Если вы когда-
либо использовали QtQuick, то знаете, что в Qt 5 операторы импорта также требовали
указания номера версии. К счастью, в Qt 6 в этом больше нет необходимости, и
версия QtQuick будет соответствовать версии PyQt6.
В однострочных комментариях в QML используются двойные прямые косые
черты, //. Многострочные комментарии начинаются с /* и заканчиваются */.
Для начала создайте новый файл с расширением .qml.
Листинг 13-1. Простой QML-документ, демонстрирующий основные принципы
синтаксиса
# qml_intro.qml
// Import necessary modules
import QtQuick

Rectangle {
id: rect
width: 155; height: 80
color: "skyblue"

Text {
text: "Small Component"
322 ГЛАВА 13 РАБОТА С QT QUICK
x: 10; y: 30
font.pixelSize: 16
font.weight: Font.DemiBold
color: "black"
}
}

QML-документ содержит иерархию объектов, где каждый объект может иметь


идентификатор и имя, свойства, методы и даже сигналы и обработчики сигналов.
Корневой объект - это объект верхнего уровня в документе.
В данном примере имеется два объекта. Первый - элемент Rectangle, который
является родителем для второго - элемента Text. Одним из преимуществ
использования скобок и отступов в QML является то, что иерархию объектов можно
понять, просто взглянув на код. Создание отношений "родитель-ребенок" в QML -
это просто инстанцирование объекта внутри скобок родительского объекта.
Но что произойдет, если вы захотите, чтобы дочерний объект имел доступ к
значениям других элементов, отличных от родительского? Чтобы выяснить это,
давайте рассмотрим, как задавать значения свойств.

Определение свойств элемента


Когда в QML свойству присваивается значение, оно обозначается двоеточием, :,
где левая часть двоеточия - это имя свойства, а правая - его значение. Например,
свойство width прямоугольника Rectangle равно 155.
Можно заметить, что первым атрибутом, определенным для Rectangle, является
id - идентификатор, который может использоваться во всем QML-документе для
взаимодействия с этим объектом. Здесь элемент Rectangle идентифицируется как
rect. Во избежание путаницы между элементами QtQuick и другими компонентами
всегда используйте нижний регистр для первой буквы идентификаторов.
Для Rectangle укажем его ширину и высоту и зададим цвет, отличный от белого
по умолчанию. Свойства, задаваемые в одной строке, разделяются точкой с запятой.
Для элемента Text задаются свойства текста, шрифта и цвета. Как и в QtWidgets, текст
можно оформлять с помощью HTML-тегов.

Система координат
Визуальный холст в QtQuick представляет собой двумерную поверхность для
расположения объектов. Левый верхний пиксель окна находится в точке (0, 0). Хотя
холст является двумерным, он также имеет z-размещение для упорядочивания
объектов при их наложении друг на друга.
Интересно, что дочерние элементы являются относительными по отношению к
своим родителям, то есть дочерний элемент наследует систему координат родителя
ГЛАВА 13 РАБОТА С QT QUICK 323
и располагается на основе его левого верхнего угла. Пример этого можно найти на
сайте https://doc.qt.io/qt-6/qtquickvisualcanvas-coordinates.html.
Существует множество способов организации элементов в окне графического
интерфейса, многие из которых будут рассмотрены в разделе "Обработка макетов в
QML". В данном примере мы можем вручную расположить объекты в окне, указав
значения x и y для объекта Text.

Совет. Если вы загрузили программу Qt Creator еще в главе 8, то на этом


этапе вы сможете визуализировать документ. Можно либо открыть файл в
Qt Creator, либо найти исполняемый файл qml, который находится в каталоге
Qt на вашем компьютере. Далее выполните в оболочке следующую команду:
$ <Qt_dir>/Qt/<path-to-qml>/qml qml_intro.qml.
Ваш <путь к qml> может быть похож на 6.0.0/clang_64/bin, где 6.0.0 обозначает
версию Qt. (Ваш путь и версия могут быть и другими). Вы можете заменить
qml_intro.qml на любой QML-документ, который хотите запустить.

Используя полученные знания, в следующих разделах мы рассмотрим, как


продолжить добавлять новые возможности в пользовательские интерфейсы QtQuick
и научиться представлять документы с помощью Python.

Создание и запуск QML-компонентов


Этот раздел разбит на четыре основные части:
1. Создание и визуализация QML-компонентов с помощью QQuickView
2. Создание многократно используемых компонентов
3. Позиционирование элементов в QML
4. Создание и визуализация окон QtQuick с помощью QQmlApplicationEngine
В каждом из примеров для загрузки QML-файлов используется один из двух
классов. Первый - это QQuickView, представляющий собой удобный класс, который
загружает QML-файл и предоставляет окно для отображения QML-сцен. QQuickView
работает для визуализации компонентов.
Но что делать, если вы хотите создать приложение QtQuick с окном, строкой
меню и другими элементами пользовательского интерфейса? Вот тут-то и приходит
на помощь QQmlApplicationEngine. В разделе "Создание и загрузка QML-окон" мы
рассмотрим, как создавать приложения с окнами.

324 ГЛАВА 13 РАБОТА С QT QUICK


Попутно вы также узнаете, как
• Добавлять изображения в приложения QtQuick
• Позиционировать объекты с помощью якорей
• Включать обработку мыши с помощью MouseArea
• Узнайте, как использовать выражения JavaScript
• Использовать QtQuick Controls для создания окон и добавления в графические
интерфейсы дополнительных компонентов, таких как кнопки и флажки.
• Узнайте, как использовать сигналы и обработчики сигналов в QML.
Для всех примеров, приведенных в этой главе, нам потребуется как минимум
два файла: один документ .qml для разработки пользовательского интерфейса и
один скрипт .py, выполняющий загрузку файла QML и, возможно, внутреннюю
функциональность. Более сложные приложения могут иметь несколько компонентов,
которые вызываются в файле main.qml.

Создание и загрузка QML-компонентов


Для создания графического интерфейса, показанного на рис. 13-2, нам потребуется
создать следующие два файла:
1. images_and_text.qml - QML-компонент, состоящий из изображений и текста
2. quick_loader.py - Python-скрипт для быстрой загрузки общих QML-
компонентов

Рисунок 13-2. Компонент QtQuick, содержащий изображения и текст


Перед началом работы обязательно загрузите папку images из репозитория GitHub.

ГЛАВА 13 РАБОТА С QT QUICK 325


Пояснения к созданию QML-компонентов
Сначала создайте новый QML-документ. Элемент Item в Листинге 13-2 служит
корнем для этого компонента. Ему назначен идентификатор id. Затем задаются
свойства width и height. Поскольку типы Item не отображают визуальное содержимое,
нам необходимо присвоить объекту дочерний элемент, возможно, Rectangle или
Image.
Листинг 13-2. Создание QML-документа с изображениями и текстом
# images_and_text.qml
// Import necessary modules
import QtQuick

Item {
id: root
width: 340; height: 420

// Create an Image that will serve as the background


Image {
anchors.fill: root
source: "images/background.jpg"
fillMode: Image.PreserveAspectCrop
}

// Create a container Rectangle to hold text and images


Rectangle {
id: container
width: 300; height: 120
y: 40 // Vertical offset
/* Comment out the following line and uncomment the
line after to view the Rectangle */
color: "transparent"
//color: "lightgrey"

anchors.horizontalCenter: root.horizontalCenter
anchors.topMargin: 40

Image {
id: image
anchors.centerIn: container
source: "images/qtquick_text.png"
sourceSize.width: container.width
sourceSize.height: container.height
}

326 ГЛАВА 13 РАБОТА С QT QUICK


Text {
text: "It's amazing!"
anchors {
top: image.bottom
horizontalCenter: image.horizontalCenter
}
font.pixelSize: 24
font.weight: Font.DemiBold
color: "#3F5674"
}
}
}
Добавив элемент Image в Item в качестве дочернего, изображение можно легко
установить в качестве фона компонента. QtQuick делает этот процесс легким с
помощью якорей.

Позиционирование элементов с помощью якорей


Якоря - это свойства, которые позволяют расположить объекты в графическом
интерфейсе, задавая отношение одного элемента к его родительским или дочерним
объектам. Представьте себе объект, у которого есть линии вдоль левой, правой,
верхней и нижней сторон, а также линии, проходящие вертикально и горизонтально
через его середину. Мы можем использовать якоря для определения связи между
элементами и этими линиями.
В следующем списке описаны часто используемые свойства якорей:
• anchors.fill - удобное свойство, позволяющее одному элементу иметь ту же
геометрию, что и другой, тем самым заполняя пространство другого элемента
(при этом сохраняется соотношение сторон и кадрирование)
• anchors.centerIn - позиционирует объект в центре другого объекта
• anchors.left, anchors.right - Располагает объект слева или справа от другого
объекта
• anchors.top, anchors.bottom - Позиционирует объект сверху или снизу другого
объекта
• anchors.verticalCenter, anchors.horizontalCenter - Устанавливает объект в центр
другого объекта по вертикали или горизонтали

Существуют также способы добавления полей между объектами с помощью


якорей.
В первом типе Image, созданном в Листинге 13-2, anchors.fill: root привязывает
свойство Image.anchors к размеру корневого объекта.
Привязка задает значение свойства в QML и обозначается двоеточием, :,
аналогично присвоению обычного значения. Разница заключается в том, что
привязка создает зависимость между свойством и другим объектом. Привязки в

ГЛАВА 13 РАБОТА С QT QUICK 327


QML могут использоваться для доступа к встроенным свойствам, вызовам функций
и даже для использования встроенных объектов JavaScript, таких как Math.
Более подробную информацию о привязках можно найти на сайте
https://doc.qt.io/qt-6/qtquick-positioning-anchors.html.

Добавление изображений в QtQuick


Свойство source в Image используется для указания пути к нужному файлу
изображения. Свойство fillMode определяет, что происходит с изображением,
когда его размер не совпадает с размером элемента. Значение PreserveAspectCrop
сохраняет соотношение сторон изображения и при необходимости обрезает его.
Другие значения fillMode включают Stretch, PreserveAspectFit, Tile и Pad (которое не
трансформирует изображение).
Тип Rectangle служит контейнером для остальных объектов Image и Text. Свойству
color может быть присвоена строка "transparent". Это небольшой трюк для удаления
фона, если вы используете PNG-изображения с прозрачным фоном. Если вы хотите
посмотреть, как типы Image и Text размещаются в объекте-контейнере, то можете
переключить комментарии к цветовым линиям.
Параметр sourceSize можно использовать для принудительного уменьшения
или увеличения масштаба изображения до определенного размера. Здесь
размер изображения qtquick_text.png принудительно сохраняется исходным, но
центрируется в контейнере. Это позволяет избежать искажения текста.
Следует отметить, что свойства можно группировать. Это видно на примере
объекта Text. В данном примере группировку свойств можно было бы выполнить
также с помощью свойств font и sourceSize.
После создания пользовательского интерфейса нам необходимо загрузить QML-
документ.

Пояснения к загрузке QML-компонентов


QQuickView предоставляет окно для отображения пользовательского интерфейса
QtQuick, в котором все, что вам нужно сделать, - это передать QQuickView URL-адрес
файла .qml.

Для того чтобы сделать общий Python-скрипт, которому можно передавать QML-
файлы в качестве аргументов при запуске приложения, мы также воспользуемся
модулем Python argparse в Листинге 13-3 (Более подробную информацию об argparse
можно найти на сайте https://docs.python.org/3.9/howto/argparse.html).
Для начала импортируем несколько классов PyQt6 в новый Python-скрипт.
Поскольку мы не используем QtWidgets, нет необходимости импортировать
QApplication. Вместо этого для приложений, связанных с графическим интерфейсом
и не использующих виджеты, используется QGuiApplication.

328 ГЛАВА 13 РАБОТА С QT QUICK


Листинг 13-3. Код для загрузки общего QML-компонента с использованием
QQuickView
# quick_loader.py
# Import necessary modules
import sys, argparse
from PyQt6.QtCore import QUrl
from PyQt6.QtGui import QGuiApplication
from PyQt6.QtQuick import QQuickView

def parseCommandLine():
"""Use argparse to parse the command line for specifying
a path to a QML file."""
parser = argparse.ArgumentParser()
parser.add_argument("-f", "--file", type=str,
help="A path to a .qml file to be the source.",
required=True)
args = vars(parser.parse_args())
return args

class MainView(QQuickView):

def __init__(self):
""" Constructor for loading QML files """
super().__init__()
self.setSource(QUrl(args["file"]))
# Get the Status enum's value and check for an error
if self.status().name == "Error":
sys.exit(1)
else:
self.show()

if __name__ == "__main__":
args = parseCommandLine() # Return command line arguments
app = QGuiApplication(sys.argv)
view = MainView()
sys.exit(app.exec())

Для загрузки QML-файла используется метод QQuickView setSource(). Если ошибок


не обнаружено, то для отображения графического интерфейса используется метод
show().
Вы можете загрузить Листинг 13-1 или 13-2 и визуализировать компоненты.
Чтобы загрузить файл, выполните в оболочке следующую команду:
$ python3 quick_loader.py -f images_and_text.qml

ГЛАВА 13 РАБОТА С QT QUICK 329


Пользователи Windows могут использовать python вместо python3.
Чтобы загрузить Листинг 13-1, выполните следующую команду:
$ python3 quick_loader.py -f qml_intro.qml
Мы только что рассмотрели, как создать простой компонент. Теперь давайте
разберемся, как создавать многократно используемые и интерактивные компоненты.

Создание многократно используемых компонентов


Возможность создавать пользовательские компоненты многократного
использования является неотъемлемой частью разработки графических интер
фейсов. Это справедливо даже для QtQuick. На рис. 13-3 показан простой
пользовательский Rectangle, который мы создадим для демонстрации использования
обработчиков событий мыши.

Рисунок 13-3. Многоразовый компонент QtQuick, меняющий цвет при нажатии


кнопки мыши
Это лишь пример того, какие компоненты можно создавать. Компоненты могут
состоять из классических элементов пользовательского интерфейса, представлений
данных, анимации и многого другого.

Пояснения к созданию пользовательских


компонентов
Листинг 13-4 представляет собой новый QML-документ, содержащий тип Rectangle
с одним дочерним компонентом Text. Отличие этого компонента от Листинга 13-1
заключается в добавлении типа MouseArea.
Для этого примера создайте файл с именем ColorRect.qml.

Совет. Обязательно используйте camelCasing при именовании компонентов,


которые планируется повторно использовать.

330 ГЛАВА 13 РАБОТА С QT QUICK


Листинг 13-4. Код для компонента ColorRect
# ColorRect.qml
import QtQuick

Rectangle {
id: root
width: 80; height: 80
color: "#1FC6DE" // Cyan-like color
border.color: "#000000"
border.width: 4
radius: 5

Text {
text: root.color
anchors.centerIn: root
}

// Click on Rectangle to change the color


MouseArea {
anchors.fill: parent
onClicked: {
color = '#' + (0x1000000 + Math.random()
* 0xffffff).toString(16).substr(1, 6);
// Uncomment the following line for Listing 13-9
//root.clicked()
}
}
}
Текущий цвет прямоугольника отображается на Text путем привязки свойства
text к root.color.

Создание интерактивного элемента с помощью


обработки мыши
В QtQuick возможна поддержка различных устройств ввода, включая клавиатуру,
мышь, сенсорные устройства и стилус.
MouseArea - это невизуальный элемент, который используется совместно
с визуальными типами. Щелчок на элементе, который также включает объект
MouseArea, может использоваться для запуска сигналов, проверки местоположения
курсора или перетаскивания элементов, если перетаскивание включено.
Свойство anchors.fill используется для того, чтобы пользователь мог щелкнуть
в любом месте родительского объекта (которым является Rectangle). Если
ГЛАВА 13 РАБОТА С QT QUICK 331
прямоугольник щелкнут, то раздается сигнал clicked и вызывается обработчик
сигнала onClicked. (Обработчики сигналов - это методы, которые обрабатывают
сигналы. Их имена - это просто сигнал с добавлением on и camelCased.) Подробнее о
сигналах мы поговорим в подразделе "Сигналы и обработчики сигналов".
В onClicked выражение JavaScript используется для выбора случайного цвета и
преобразования его в шестнадцатеричную строку, представляющую новый цвет.
Значение цвета используется для обновления значения ColorRect.
Используя Листинг 13-3 (quick_loader.py), можно запустить просмотр компонента
ColorRect, выполнив в оболочке следующую строку:
$ python3 quick_loader.py -f ColorRect.qml
В следующем разделе ColorRect будет использован для создания нескольких
примеров приложений, демонстрирующих способы организации элементов в
QtQuick.

Обработка макетов в QML


Организация визуальных элементов в графическом интерфейсе очень важна для
создания целостности. Объекты в QtQuick могут быть расположены несколькими
различными способами. В этом разделе мы рассмотрим четыре из них. Также будет
создано несколько примеров документов, помогающих наглядно представить, как
использовать их в коде.
Существует несколько различных подходов к упорядочиванию элементов в QML.
Ниже приводится описание каждого из них:
• Ручное позиционирование может быть использовано для явного указания
координат x и y типов QtQuick. Этот метод чрезвычайно эффективен для
графических интерфейсов, которые не являются динамическими. Этот метод
был продемонстрирован в Листинге 13-1.
• Якорение использует границы и относительное положение родительских
и сиблинговых элементов для расположения объектов. Эта тема была
рассмотрена в подразделе "Позиционирование элементов с помощью якорей".
• Позиционеры - это контейнеры, которые используются для расположения
дочерних элементов в столбцах, строках или сетках. В разделе
"Использование позиционеров для размещения элементов" приведен обзор
позиционеров.
• Менеджеры компоновки используются для организации элементов в
пользовательском интерфейсе. Основное различие между менеджерами
макетов и позиционерами заключается в том, что макеты также
обрабатывают изменение размеров. Макеты могут быть импортированы в
QML-документ с помощью команды import QtQuick.Layouts. Более подробную
информацию о менеджерах компоновки в QtQuick можно найти на сайте
https://doc.qt.io/qt-6/qtquicklayouts-index.html.

332 ГЛАВА 13 РАБОТА С QT QUICK


Использование позиционеров для
позиционирования элементов
Позиционеры имеют схожее поведение с менеджерами компоновки. Как и
менеджеры компоновки, позиционеры используются для организации элементов в
определенной форме, например, в виде строки или столбца. Однако, в отличие от
менеджеров компоновки, позиционеры действуют как контейнеры для дочерних
виджетов и не управляют размерами дочерних элементов.
В табл. 13-2 перечислены четыре часто используемых позиционера.
Таблица 13-2. Четыре стандартных типа позиционеров

Позиционер Описание
Столбец Размещает дочерние элементы в одном столбце
Строка Размещает дочерние элементы в одной строке
Сетка Позиционирует дочерние элементы в сетке
Поток Располагает дочерние элементы рядом друг с другом, причем дочер
ние элементы могут быть обернуты сверху вниз или слева направо

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


расстоянием между элементами, применять padding и задавать направление
расположения элементов.
Более подробную информацию о позиционерах можно найти на сайте
https://doc.qt.io/qt-6/qtquickpositioning-layouts.html.

Пояснения к использованию позиционеров


столбцов и сеток
На рис. 13-4 для расположения нескольких компонентов ColorRect из Листинга 13-4
(ColorRect.qml) используется позиционер Column. Элементы укладываются друг на
друга, а расстояние между каждым ColorRect задается с помощью свойства spacing.

ГЛАВА 13 РАБОТА С QT QUICK 333


Рисунок 13-4. Расположение элементов в позиционере Column
Листинг 13-5 создает в QML-документе простой родительский Rectangle,
содержащий три экземпляра ColorRect, расположенных в позиционере Column.
Листинг 13-5. Код для примера позиционера Column
# columns.qml
import QtQuick

Rectangle {
width: 200; height: 300
color: "grey"

Column {
id: column
anchors.centerIn: parent
spacing: 6
// Add custom components to Column
ColorRect { }
ColorRect { }
ColorRect { color: "pink"}
}
}
Кроме того, если вы посмотрите на третий экземпляр ColorRect, то заметите,
что свойства компонентов все еще можно изменять при их инстанцировании.
Попробуйте переключить Column на Row или Flow в Листинге 13-5 и посмотрите на
разницу в графическом интерфейсе.
На рис. 13-5 приведен пример позиционера Grid.

334 ГЛАВА 13 РАБОТА С QT QUICK


Рисунок 13-5. Элементы, расположенные в позиционере Grid
Листинг 13-6 лишь незначительно отличается от Листинга 13-5. Обратите
внимание, что для позиционера Grid необходимо указать количество строк и столбцов
в сетке.
Листинг 13-6. Код для примера позиционера Grid
# grids.qml
import QtQuick

Rectangle {
width: 200; height: 200
color: "grey"

Grid {
id: grid
rows: 2; columns: 2
anchors.centerIn: parent
spacing: 6
// Add custom components to Column
ColorRect { }
ColorRect { }
ColorRect { radius: 20 }
ColorRect { }
}
}
Несмотря на то, что в этих двух примерах для иллюстрации использования
позиционеров используются типы Rectangle, они более эффективны при расположении
кнопок, циферблатов и других элементов пользовательского интерфейса.
В следующем разделе вы приблизитесь к созданию полноценных классических
настольных приложений, узнав, как создавать строки меню и отображать диалоговые
окна в QtQuick.

ГЛАВА 13 РАБОТА С QT QUICK 335


Создание и загрузка QML-окон
Хотя QQuickView полезен для отображения компонентов, класс QQmlApplicationEngine
представляет собой более удобный способ загрузки единого QML-документа,
корневым объектом которого является окно. Это означает, что вместо использования
в качестве корневого объекта типов Rectangle или Item мы будем использовать
элемент управления ApplicationWindow. Это позволит нам получить дополнительные
инструменты, такие как строка меню и панели инструментов.

Примечание. QQuickView не поддерживает использование в качестве


корневого элемента оконных типов типа ApplicationWindow. Для отображения
сцены в окне необходимо использовать QQmlApplicationEngine.

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


локальные изображения. Выбор изображений осуществляется через экземпляр
FileDialog, который создается при выборе пункта меню Open. Это показано на
рис. 13-6.

В предыдущих документах по QML импортировался только класс QtQuick. Для того


чтобы включить в приложение QtQuick общие элементы графического интерфейса,
нам потребуется импортировать новый класс.

Элементы управления QtQuick


Элементы управления похожи на виджеты в QtWidgets. Это кнопки, флажки,
ползунки и другие графические элементы пользовательского интерфейса, которые
мы привыкли использовать для взаимодействия с приложениями.
336 ГЛАВА 13 РАБОТА С QT QUICK
В табл. 13-3 перечислена лишь часть элементов управления, доступных в QtQuick.
Таблица 13-3. Выбор распространенных типов QtQuick.Controls

Средства
Описание
управления
Action Описываются действия, которые могут быть назначены пунктам
меню и панелям инструментов
ApplicationWindow Окно с дополнительной функциональностью для добавления
строки меню, верхнего и нижнего колонтитулов
Button Кнопка, на которую может нажать пользователь для выполнения
действия
CheckBox Кнопка проверки, которую можно включать и выключать
ComboBox Представляет выпадающий список для выбора опций
Dial Круговой диск, вращая который можно выбрать значение
Dialog Всплывающее диалоговое окно со стандартными кнопками и
заголовком
DialogButtonBox Кнопочное поле, которое используется для задания кнопок в
диалоговом окне
Frame Обеспечивает визуальную рамку для организации других
элементов управления
GroupBox Обеспечивает визуальную рамку с заголовком для организации
других элементов управления
MenuBar Создает в окне строку меню
RadioButton Радиокнопки, которые являются автоисключающими и могут быть
включены или выключены
Slider Используется для выбора значения с помощью подвижной
рукоятки на направляющей
TabBar Создает панель вкладок для переключения между различными
представлениями
Tumbler Колесо, которое можно вращать для выбора значений

Полный список типов Controls можно найти на сайте


https://doc.qt.io/qt-6/qtquick-controls2-qmlmodule.html.

Пояснения к созданию QML-окон


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

ГЛАВА 13 РАБОТА С QT QUICK 337


Чтобы получить доступ к этим средствам, нам потребуется импортировать
несколько новых классов QtQuick в новый QML-документ, как показано в
Листинге 13-7. Controls предоставляет нам доступ к элементам пользовательского
интерфейса, а Dialogs используется для создания FileDialog.
Листинг 13-7. Создание графического интерфейса просмотра изображений
QtQuick для иллюстрации использования окон и элементов управления
# windows_and_controls.qml
// Import necessary modules
import QtQuick
import QtQuick.Controls
import QtQuick.Dialogs

ApplicationWindow {
title: "QtQuick Image Viewer"
width: 800; height: 500
visible: true

// Create the menu bar and its actions


menuBar: MenuBar {
Menu {
title: "&File"
Action {
text: "&Open"
onTriggered: openImage()
}
MenuSeparator {}
Action {
text: "&Quit"
onTriggered: Qt.quit()}
}
}

// Define the signal for opening images


signal openImage()

// Define the slot for opening images


onOpenImage: {
fileDialog.open()
}

// Define a FileDialog for selecting local images


FileDialog {
id: fileDialog
title: "Choose an image file"
338 ГЛАВА 13 РАБОТА С QT QUICK
nameFilters: ["Image files (*.png *.jpg)"]
onAccepted: {
// Update displayed image
image.source = fileDialog.selectedFile
}
onRejected: {
fileDialog.close()
}
}

/* Create a container Rectangle for the image


in order to add margins around the image’s edges */
Rectangle {
id: container
anchors {
fill: parent
margins: 10
}

Image {
id: image
anchors.fill: container
source: "images/open_image.png"
fillMode: Image.PreserveAspectFit
}
}
}
ApplicationWindow является корневым объектом приложения и будет загружен
в следующем разделе с помощью QQmlApplicationEngine. ApplicationWindow также
включает свойство title. Очень важно, чтобы при использовании QQmlApplicationEngine
вы не забыли включить строку visible: true. Если забыть об этом свойстве, то окно
останется скрытым, так как по умолчанию ApplicationWindow не видно.
Элемент Image используется в окне ApplicationWindow для отображения выбранного
изображения пользователю. Хотя изображение может быть непосредственно
помещено в окно, использование свойства anchors.margins элемента Rectangle
позволяет сделать тонкую границу вокруг изображения.

Создание строки меню


Для создания строки меню окна используется элемент управления MenuBar.
Затем Menu используется для создания меню File, и, наконец, элементы управления
Action добавляются в меню File вместе с элементом управления MenuSeparator для
разделения действий Open и Quit.

ГЛАВА 13 РАБОТА С QT QUICK 339


Сигналы и обработчики сигналов
Элементы управления в QtQuick.Controls, как и виджеты, взаимодействуют между
собой с помощью сигналов и слотов, которые в QtQuick называются обработчиками
сигналов. По их названиям легко определить, какие сигналы связаны с какими
обработчиками сигналов. Обработчики сигналов имеют дополнительную приставку
on и заключены в верблюжий корпус. Например, элемент управления Button имеет
сигнал clicked, который при нажатии на кнопку запускает обработчик сигнала
onClicked.
Посмотрите на действия в menuBar. Для создания пользовательского сигнала в
типе QML необходимо использовать ключевое слово signal. Здесь мы создаем новый
сигнал openImage, не имеющий параметров.
В данном примере сигнал openImage элемента управления ApplicationWindow будет
выдаваться каждый раз, когда срабатывает пункт меню Open. Затем он подключается
к обработчику сигнала onOpenImage, в котором открывается экземпляр fileDialog.
Чтобы вы знали, обработчик сигнала onTriggered может быть напрямую подключен
к fileDialog.open().
Пункт меню Quit закрывает все приложение с помощью Qt.quit().

Использование FileDialog для открытия файлов


Диалоги используются либо для сбора, либо для представления информации
пользователю. В данном примере открывается диалог FileDialog, чтобы пользователь
мог выбрать файлы изображений .png или .jpg.
В правом нижнем углу диалога расположены две кнопки: OK и Cancel. Если
пользователь нажимает кнопку OK, то сигнал о принятии обрабатывается
командой onAccepted. При этом обновляется URL-адрес image.source и изображение,
использующее значение fileDialog.selectedFile. В противном случае сигнал отклонения
от кнопки Cancel подключается к onRejected и закрывает диалог.

Пояснение для загрузки окон QML


Аналогично Листингу 13-3, Листинг 13-8 представляет собой Python-скрипт для
загрузки общих QML-документов. Отличие заключается в том, что в Листинге 13-8
файлы загружаются с помощью QQmlApplicationEngine. Это означает, что в качестве
аргументов можно передавать только пути к QML-документам, в которых элементом
верхнего уровня является окно, например ApplicationWindow.
Начнем с импорта классов из PyQt6 в новый Python-скрипт, включая
QQmlApplicationEngine из модуля QtQml.

340 ГЛАВА 13 РАБОТА С QT QUICK


Листинг 13-8. Код для загрузки общего QML-окна с помощью QQmlApplicationEngine
# qml_loader.py
# Import necessary modules
import sys, argparse
from PyQt6.QtCore import Qt, QUrl
from PyQt6.QtGui import QGuiApplication
from PyQt6.QtQml import QQmlApplicationEngine

def parseCommandLine():
"""Use argparse to parse the command line for specifying
a path to a QML file."""
parser = argparse.ArgumentParser()
parser.add_argument("-f", "--file", type=str,
help="A path to a .qml file to be the source.",
required=True)
args = vars(parser.parse_args())
return args

class MainView(QQmlApplicationEngine):

def __init__(self):
super().__init__()
# Order matters here; need to check if the object was
# created before loading the QML file
self.objectCreated.connect(self.checkIfObjectsCreated,
Qt.ConnectionType.QueuedConnection)
self.load(QUrl(args["file"]))

def checkIfObjectsCreated(self, object, url):


"""Check if QML objects have loaded without errors.
Otherwise, exit the program."""
if object is None:
QGuiApplication.exit(1)

if __name__ == "__main__":
args = parseCommandLine() # Return command line arguments
app = QGuiApplication(sys.argv)
engine = MainView()
sys.exit(app.exec())

Класс MainView наследует QQmlApplicationEngine. После передачи QML-файла в


метод load() QQmlApplicationEngine будет выдан сигнал objectCreated, когда все объекты
будут загружены. Перечисление Qt.ConnectionType.QueuedConnection гарантирует, что
сигнал будет поставлен в очередь до тех пор, пока цикл событий не сможет доставить

ГЛАВА 13 РАБОТА С QT QUICK 341


его в слот. Это сделано для того, чтобы мы могли проверить наличие ошибок перед
загрузкой файла.
Если загрузка прошла успешно, то откроется окно. В противном случае при
ошибке в слот checkIfObjectsCreated() будет возвращен объект со значением None.
Чтобы загрузить Листинг 13-7, выполните в оболочке следующую команду:
$ python3 qml_loader.py -f windows_and_controls.qml
В последнем разделе мы немного развлечемся и заставим некоторые объекты
вращаться и менять размеры с помощью трансформаций.

Использование трансформаций для анимации


объектов
Трансформация - это общий термин, обозначающий манипулирование формой,
размером и/или положением точки, линии или геометрической фигуры. В Главе 11
было показано, как использовать трансформации для анимации объектов в QtWidgets.
Теперь мы начнем выяснять, как выполнять некоторые базовые преобразования в
QtQuick.

Пояснения к простым преобразованиям


В первом примере, показанном на рис. 13-7, мы продемонстрируем, как
использовать свойства поворота и масштаба типа Item для выполнения базовых
преобразований объектов.
• rotation - Передаются значения в градусах.
• scale - При значениях меньше 1,0 объект отображается в меньшем размере, а
при значениях больше 1,0 - в большем.

Рисунок 13-7. Повернутые и масштабированные объекты

342 ГЛАВА 13 РАБОТА С QT QUICK


Примечание. В этом примере повторно используется компонент ColorRect из
Листинга 13-4. Для того чтобы этот пример заработал, необходимо вернуться
в Листинг 13-4 и убрать символы комментария из строки root.clicked(). Это
позволит ColorRect получать сигналы от ApplicationWindow из Листинга 13-8,
создавая компонент ColorRect с возможностью щелчка. При использовании
ColorRect.qml в других примерах не забудьте закомментировать эту строку еще
раз.

Сначала создайте новый QML-файл, импортирующий QtQuick и QtQuick.Controls.


Объектом верхнего уровня в Листинге 13-9 является ApplicationWindow. Убедитесь,
что при загрузке этого QML-файла используется Листинг 13-8. Далее настройте
свойства окна и убедитесь, что visible имеет значение true.
Листинг 13-9. Код для преобразования объектов по щелчку мыши
# rotate_and_move.qml
import QtQuick
import QtQuick.Controls

ApplicationWindow {
title: "Simple Transformations"
width: 300; height: 300
visible: true

MouseArea {
id: windowMouse
anchors.fill: parent
onClicked: {
// Reset the values of the ColorRect objects
rect1.rotation = 0
rect2.scale = 1.0
}
}

ColorRect {
id: rect1
x: 20; y: 20
antialiasing: true
signal clicked

onClicked:{
// Rotate the rect 20˚ when clicked
rotation += 20
}

ГЛАВА 13 РАБОТА С QT QUICK 343


}
ColorRect {
id: rect2;
x: 200; y: 200
antialiasing: true
signal clicked

onClicked:{
// Scale the rect when clicked
scale += .1
}
}
}
Далее мы создадим элемент MouseArea для обработки нажатий на окно
приложения. Поскольку в коде QML порядок имеет значение, создание MouseArea
перед другими элементами гарантирует, что окно также будет получать щелчки, а не
только элементы ColorRect.
Два объекта ColorRect располагаются в окне вручную. Для каждого из объектов
определяется новый сигнал - clicked. Затем мы можем использовать обработчик
сигнала onClicked в каждом из элементов ColorRect для поворота или масштабирования
элементов с помощью встроенных свойств Item.
Щелкните в любом месте окна, чтобы сбросить значения ColorRect.
Чтобы загрузить Листинг 13-9, выполните в оболочке следующую команду:
$ python3 qml_loader.py -f rotate_and_move.qml
Рассмотрим последний пример, демонстрирующий использование трансформаций
и некоторых других классов QtQuick для анимации элементов.

Пояснения к использованию преобразований для


анимации объектов
Графический интерфейс на рис. 13-8 развивает рассмотренные в предыдущем
разделе понятия трансформаций и показывает, как можно создавать анимацию с
помощью QML-типа Behavior. Тип Behavior определяет анимацию по умолчанию,
которая будет происходить при изменении значения определенного свойства.

344 ГЛАВА 13 РАБОТА С QT QUICK


Рисунок 13-8. Колесо вращается при каждом щелчке мыши в окне.
Мы также рассмотрим, как использовать JavaScript для создания функций,
добавляющих случайность в наше приложение.
Перед началом работы с этим приложением убедитесь, что вы загрузили папку
images с GitHub.
Начните с создания нового QML-документа. В Листинге 13-10 нам потребуется
импортировать QtQuick.Controls, чтобы получить доступ к элементу управления
ApplicationWindow. Обязательно установите значение параметра visible равным true.
Листинг 13-10. Код для графического интерфейса QtQuick с вращающимся колесом
# transforms.qml
import QtQuick
import QtQuick.Controls

ApplicationWindow {
title: "Spin Wheel Transformations"
width: 500; height: 500
visible: true

/* Get a random number where both the minimum and maximum


values are inclusive */
function getRandomIntInclusive(min, max) {
min = Math.ceil(min);
max = Math.floor(max);
return Math.floor(Math.random() * (max - min + 1) + min);
}

Image {
id: pointer
source: "images/pointer.png"

ГЛАВА 13 РАБОТА С QT QUICK 345


x: parent.width / 2 - width / 2; y: 0; z: 1
}

Image {
id: spinwheel
anchors.centerIn: parent
source: "images/spin_wheel.png"
sourceSize.width: parent.width - 30
sourceSize.height: parent.height - 30

// Create a behavior for rotating the spinwheel Image


Behavior on rotation {
NumberAnimation {
duration: getRandomIntInclusive(500, 3000)
easing.type: Easing.OutSine
}
}

/* Enable mouse handling and define how the image rotates


when clicked */
MouseArea {
anchors.fill: parent
onClicked: spinwheel.rotation += getRandomIntInclusive(
360, 360 * 4)
}
}
}
Отдельные функции JavaScript могут быть добавлены в документы QML для
придания им дополнительной функциональности. Ключевое слово function
обозначает функцию getRandomIntInclusive(), которая принимает в качестве
аргументов два целочисленных значения, представляющих собой максимальное
и предельное ограничения. Используя эти два значения, возвращается случайное
целое число.
Функция getRandomIntInclusive() будет использоваться дважды. Первый раз -
в обработчике сигнала onClicked элемента MouseArea. Возвращаемое случайное
значение будет задавать величину вращения колеса, добавляя реалистичности
графическому интерфейсу, чтобы ни одно вращение не казалось одинаковым.
Во второй раз, когда используется getRandomIntInclusive(), мы хотим описать
анимацию поведения вращения. При нажатии на окно значение вращения спин-
колеса будет меняться. Строка Behavior on rotation означает, что всякий раз, когда
значение вращения будет меняться, будет запускаться анимация NumberAnimation.
Значение свойства duration указывает, как долго будет происходить вращение: от
половины секунды до трех секунд.
Наконец, свойства Easing type и easing.type указывают, какой тип кривой смягчения
мы хотим использовать. Смягчающие кривые обеспечивают более реалистичную
346 ГЛАВА 13 РАБОТА С QT QUICK
анимацию объектов. Чтобы узнать, какие виды кривых Easing доступны в QtQuick,
следует обратиться к сайту
https://doc.qt.io/qt-5/qml-qtquick-propertyanimation.html#easing-prop.
Чтобы загрузить Листинг 13-10, выполните в оболочке следующую команду:
$ python3 qml_loader.py -f transforms.qml
Мы только начали знакомиться с теми видами трансформации и анимации,
которые существуют в QtQuick. Настоятельно рекомендуется попробовать найти
другие примеры и поэкспериментировать с ними.

Резюме
С появлением Qt 6 все больше внимания уделяется созданию динамических
графических интерфейсов, которые являются кроссплатформенными,
масштабируемыми, простыми в обслуживании и призваны максимально
использовать возможности графического оборудования на любой платформе.
QtQuick, инструментарий, основанный на языке Qt QML, определяет широкий
спектр удивительных графических инструментов, которые отлично подходят для
настольных, мобильных и встраиваемых приложений.
О QtQuick и QML можно узнать гораздо больше. Цель этой главы - дать вам
основные сведения для начала работы с QtQuick, а затем направить вас в нужное
русло с помощью ссылок и другой полезной информации.
В этой главе мы рассмотрели, как создать базовое окно, добавить QML-элементы
и элементы управления в приложение и даже обсудили, как создавать многократно
используемые QML-компоненты, которые можно использовать в других
приложениях. Затем эти компоненты были использованы для демонстрации других
концепций QtQuick, таких как макеты и трансформации. Мы увидели, как загружать
QML-файлы с помощью QQuickView и QQmlApplicationEngine. Последнее приложение,
которое мы создали, продемонстрировало, как анимировать объекты, используя как
типы Behavior, так и JavaScript.
Откровенно говоря, для того чтобы охватить и описать все, что можно предложить
в QML, необходимо полное руководство по QtQuick. Так много содержания и так
много возможностей для использования QtQuick и QML при создании графических
интерфейсов.
В Главе 14 мы вернемся к использованию QtWidgets и узнаем, как начать создавать
приложения, взаимодействующие с базами данных SQL и управляющие ими.

ГЛАВА 13 РАБОТА С QT QUICK 347


ГЛАВА 14

Введение в работу с
базами данных
Данные являются основополагающим фактором, определяющим изменения
в современном бизнесе, коммуникациях, науке и даже в нашей личной жизни.
Информация, которую мы получаем в результате покупок в интернете, сообщений
в социальных сетях, запросов в поисковых системах и данных о местоположении,
собирается, управляется и анализируется и может использоваться по разным
причинам, в том числе для отслеживания потребительских моделей, обучения
алгоритмов искусственного интеллекта или даже для изучения географического
распределения определенных событий, например, заболеваний.
В этой главе мы
• Познакомимся с архитектурой Qt "Model/View" для создания графических
интерфейсов, работающих с данными
• Используем класс QTableView для создания приложений, работающих с
данными
• Посмотрим, как работать с CSV-файлами в PyQt
• Представим модуль QtSql для создания и управления реляционными базами
данных SQL.
Прежде чем начать, давайте немного поразмышляем о пользе данных.

Размышления о данных
Анализ данных, или процесс организации, модификации и моделирования
данных, является важным процессом, и в этой главе мы рассмотрим работу
со структурированными данными для разработки графических интерфейсов.
Данные могут храниться в различных форматах, включая текстовые, визуальные и
мультимедийные.
Для анализа данных необходимо организовать их в виде структур, которые можно
хранить и затем получать к ним доступ в электронном виде через компьютерную
систему. Иногда приходится работать с небольшим набором данных, состоящим из
одного-двух файлов. В других случаях может потребоваться доступ к определенным
фрагментам целой базы данных, содержащей конфиденциальную информацию.
База данных - это организованная коллекция из нескольких наборов данных.

348 ГЛАВА 14 ВВЕДЕНИЕ В РАБОТУ С БАЗАМИ ДАННЫХ


Данные из файлов и баз данных обычно представляются в виде таблиц. Строки
и столбцы таблицы обычно лучше всего подходят для работы со стилем данных в
файлах данных. Если у нас есть набор данных о сотрудниках компании, то каждая
строка может представлять отдельного сотрудника компании, а каждый столбец
- различные типы атрибутов каждого сотрудника, такие как возраст, зарплата и
идентификационный номер сотрудника.
В этой главе мы рассмотрим использование классов таблиц PyQt для отображения
и манипулирования данными. Мы рассмотрим, как использовать таблицы для
работы с файлами CSV, а также для построения и взаимодействия с языком
управления базами данных SQL. Конечно, существуют и другие форматы, которые
можно использовать для просмотра данных, а именно списки и деревья, если они
лучше соответствуют требованиям вашего приложения.

Введение в программирование на основе


Model/View
Qt, а следовательно, и PyQt, нуждается в системе доступа, отображения и
управления данными, которые могут быть представлены пользователю. Старая
техника, используемая для управления связями между данными и их визуальным
представлением в пользовательских интерфейсах, - это паттерн проектирования
Model-View-Controller (MVC). MVC разделяет логику программы на три
взаимосвязанных компонента: модель, представление и контроллер.
В Qt используется аналогичный шаблон проектирования, основанный на MVC:
парадигма Model/View.

Компоненты архитектуры Model/View


Программирование Model/View, как и MVC, также разделяет логику между тремя
компонентами, но при этом объединяет объекты представления и контроллера и
вводит новый элемент - делегат. Диаграмма этой архитектуры приведена на рис. 14-1.

ГЛАВА 14 ВВЕДЕНИЕ В РАБОТУ С БАЗАМИ ДАННЫХ 349


Рисунок 14-1. Архитектура Model/View
• Модель - класс, который взаимодействует с источником данных, получает
доступ к данным и обеспечивает точку соединения между данными и
представлением и делегатом.
• Представление - класс, отвечающий за отображение данных пользователю
в виде списка, таблицы или дерева, а также за извлечение элементов
данных из модели с помощью индексов модели. Представление также имеет
функциональность, аналогичную контроллеру в паттерне MVC, который
обрабатывает входные данные от взаимодействия пользователя с элементами,
отображаемыми в представлении.
• Делегат - класс, отвечающий за рисование элементов и предоставление
редакторов в представлении. Делегат также осуществляет обратную связь с
моделью, если элемент был отредактирован.
Использование структуры Model/View имеет ряд преимуществ, в частности,
идеально подходит для разработки крупномасштабных приложений, обеспечивает
большую гибкость и контроль над внешним видом и редактированием элементов
данных, упрощает структуру отображения данных, а также предоставляет
возможность одновременного отображения нескольких представлений модели.

Классы Model/View в PyQt


Как мы видели в Главе 10, Qt предоставляет несколько удобных классов для работы
с данными. Эти классы значительно упрощают работу разработчика и предоставляют
всю функциональность, необходимую для базовых приложений работы с данными.
Ниже приведен краткий обзор:
• QTreeWidget - создает таблицу элементов
• QListWidget - отображает список элементов
• QTreeWidget - предоставляет иерархическую древовидную структуру.
Эти виджеты предоставляют все необходимые инструменты для работы с данными
и уже включают в себя классы представления, модели и делегата, объединенные в
350 ГЛАВА 14 ВВЕДЕНИЕ В РАБОТУ С БАЗАМИ ДАННЫХ
один класс. Однако эти классы в большей степени ориентированы на интерфейсы,
основанные на элементах, и являются менее гибкими, чем работа со структурой
Model/View. Следует также отметить, что каждый из этих виджетов наследует
поведение от класса абстрактного представления элемента, QAbstractItemView,
создавая поведение для выбора элементов и управления заголовками.
Абстрактный класс обеспечивает точки соединения, называемые интерфейсом,
между другими компонентами, предоставляя методы класса, функциональность
и реализацию функций по умолчанию. На их основе создаются другие классы.
Абстрактные классы данных Qt также могут быть использованы для создания
пользовательских моделей, представлений или делегатов.
Давайте попробуем немного подробнее разобраться с классами моделей,
представлений и делегатов, которые предоставляет Qt:
• Модели - Все модели основаны на классе QAbstractItemModel, определяющем
интерфейс, используемый как представлениями, так и делегатами для
доступа к данным. Они могут использоваться для работы со списками,
таблицами или деревьями. Данные могут иметь различную форму, включая
структуры данных Python, отдельные классы, файлы или базы данных.
Другими классами моделей являются QStandardItemModel, QFileSystemModel
и модели, связанные с SQL.
• Представления - Все представления основаны на QAbstractItemView и
используются для отображения элементов данных из источника данных,
включая QListView, QTableView и QTreeView.
• Делегаты - Базовым классом является QAbstractItemDelegate, отвечающий
за отрисовку элементов из модели и предоставляющий виджет редактора для
модификации элементов. Например, при редактировании ячейки в таблице
виджет редактора, такой как QLineEdit, размещается непосредственно над
элементом.

В этом разделе мы создадим графический интерфейс, демонстрирующий


использование классов Model/View для отображения данных в таблицах. Для
графического интерфейса на рис. 14-2 данные, содержащиеся в CSV-файле,
будут загружены и отображены в таблице. В этом примере мы также рассмотрим
использование класса QStandardItemModel, который предоставляет общую модель
для хранения данных. В этом примере также будет показано, как можно связать
модель для управления данными с представлением, которое будет отображать эти
данные.

ГЛАВА 14 ВВЕДЕНИЕ В РАБОТУ С БАЗАМИ ДАННЫХ 351


Рисунок 14-2. Таблица, созданная с использованием архитектуры Model/View
Связь между моделями, представлениями и делегатами осуществляется с помощью
сигналов и слотов. Модель использует сигналы для уведомления представления
об изменениях данных. Представление генерирует сигналы, предоставляющие
информацию о том, как пользователь взаимодействует с элементами. Для простого
графического интерфейса взаимодействие с делегатом может и не понадобиться, но
важно знать, что при редактировании элемента в представлении выдаются сигналы
от делегата. Это, в свою очередь, информирует модель и представление о состоянии
виджета редактора.

Пояснения к введению в Model/View


Перед началом работы над этой программой обязательно загрузите папку
files из репозитория GitHub. Листинги 14-1 - 14-3 иллюстрируют использование
программирования Model/View для отображения содержимого небольшого CSV-
файла в табличном представлении. В листинге 14-1 мы используем сценарий
basic_window.py из Главы 1, чтобы начать настройку класса MainWindow.

352 ГЛАВА 14 ВВЕДЕНИЕ В РАБОТУ С БАЗАМИ ДАННЫХ


Листинг 14-1. Код для настройки класса MainWindow в вводном примере
Model/View
# model_view_ex.py
# Import necessary modules
import sys, csv
from PyQt6.QtWidgets import (QApplication, QWidget,
QTableView, QAbstractItemView, QVBoxLayout)
from PyQt6.QtGui import (QStandardItemModel, QStandardItem)

class MainWindow(QWidget):

def __init__(self):
super().__init__()
self.initializeUI()

def initializeUI(self):
"""Настройка графического интерфейса приложения."""
self.setGeometry(100, 100, 450, 300)
self.setWindowTitle("Пример модели и представления")

self.setupMainWindow()
self.loadCSVFile()
self.show()

if __name__ == '__main__':
app = QApplication(sys.argv)
window = MainWindow()
sys.exit(app.exec())
Таблицы отлично подходят для организации и отображения различных типов
текстовых (а иногда и графических) данных, например, информации о сотрудниках
или инвентаризации.
Начнем с импорта классов, в том числе QTableView из модуля QtWidgets и классов
QStandardItemModel и QStandardItem из QtGui. QStandardItemModel предоставляет
модель на основе элементов, необходимую для работы с данными; QStandardItem
создает элементы, которые используются в модели.

Настройка модели, представления и режимов


выбора
Для метода setUpMainWindow() в Листинге 14-2 создаются экземпляры как модели
с использованием QStandardItemModel, так и класса QTableView. Метод loadCSVFile()
для загрузки данных в таблицу обрабатывается в Листинге 14-3.
ГЛАВА 14 ВВЕДЕНИЕ В РАБОТУ С БАЗАМИ ДАННЫХ 353
Листинг 14-2. Код метода setUpMainWindow() во вводном примере Model/View
# model_view_ex.py
def setupMainWindow(self):
"""Создаем и располагаем виджеты в главном окне."""
self.model = QStandardItemModel()

table_view = QTableView()
table_view.setSelectionMode(
QAbstractItemView.SelectionMode.ExtendedSelection)
table_view.setModel(self.model)

# Установите начальные значения строк и столбцов


self.model.setRowCount(3)
self.model.setColumnCount(4)

main_v_box = QVBoxLayout()
main_v_box.addWidget(table_view)
self.setLayout(main_v_box)

Пользователи могут выбирать элементы в табличном представлении


различными способами. Сеттер setSelectionMode(), а также перечисление
QAbstractItemView.SelectionMode определяют, как представление реагирует на выбор
пользователя. Ниже приводится описание различных флагов:
• SingleSelection - пользователь может выбрать только один элемент в любой
момент времени. Выбранный ранее элемент становится невыбранным.
• ExtendedSelection - Позволяет выполнять обычный выбор, а также выбирать
несколько элементов, нажимая клавишу Ctrl (Cmd в MacOS) при щелчке
на элементе в представлении или выделяя несколько элементов с помощью
клавиши Shift.
• ContiguousSelection - позволяет выполнять обычное выделение, а также
выделять несколько элементов нажатием клавиши Shift.
• MultiSelection - пользователь может выбирать и отменять выбор нескольких
элементов, щелкая и перетаскивая мышь в таблице.
• NoSelection - выбор элементов отключен.
Чтобы настроить представление на отображение данных из модели, необходимо
вызвать метод setModel() и передать ему инстанцированную модель. В данном
примере используется модель QStandardItemModel.
В главе 10, где мы рассматривали QTableWidget, у виджета таблицы вызывались
методы setRowCount() и setColumnCount(). При использовании QTableView эти методы
не являются встроенными и вызываются на модели, как в следующей строке кода из
Листинга 14-2:
self.model.setRowCount(3)
Виджет table_view добавляется в QVBoxLayout.
354 ГЛАВА 14 ВВЕДЕНИЕ В РАБОТУ С БАЗАМИ ДАННЫХ
Работа с CSV-файлами
В initializeUI() из Листинга 14-1 следующим шагом будет вызов loadCSVFile() и чтение
содержимого файла данных. Затем элементы добавляются в модель для отображения
в представлении. Содержимое файла показано на рис. 14-3.

Рисунок 14-3. Пример данных, хранящихся в CSV-файле


В методе loadCSVFile() в Листинге 14-3 показано, как считывать заголовки и
данные из CSV-файла. Формат CSV (Comma-Separated Values (Значения, разделенные
запятыми)) является очень распространенным форматом, используемым для
хранения данных электронных таблиц и наборов данных.
Листинг 14-3. Код метода loadCSVFile() во вводном примере Model/View
# model_view_ex.py
def loadCSVFile(self):
"""Загрузить заголовок и строки из CSV-файла."""
file_name = "files/parts.csv"

with open(file_name, "r") as csv_f:


reader = csv.reader(csv_f)
header_labels = next(reader)
self.model.setHorizontalHeaderLabels(
header_labels)
for i, row in enumerate(csv.reader(csv_f)):
items = [QStandardItem(item) for item in row]
self.model.insertRow(i, items)
Мы откроем файл, настроим объект reader для чтения последовательностей в
файле, получения заголовочных меток и перехода к следующей строке с помощью
функции next(). В данном примере мы предположим, что CSV-файл будет содержать
метки заголовка. Горизонтальные метки для модели задаются с помощью списка
элементов из первой строки файла, который хранится в header_labels.
Для оставшихся строк мы используем понимание списка, чтобы считать элементы
для каждой строки в список. Элементы, созданные для QStandardItemModel, должны
быть экземплярами QStandardItem. Метод insertRow() используется для вставки
списка элементов в i-й ряд.
ГЛАВА 14 ВВЕДЕНИЕ В РАБОТУ С БАЗАМИ ДАННЫХ 355
Получив фундаментальное представление о том, как создавать модель и
представление, мы можем перейти к созданию графических интерфейсов,
работающих с большими наборами данных. Хотя в остальной части этой главы
основное внимание будет уделено классам и моделям на основе SQL, использовать
SQL не обязательно. Для использования классов Model/View достаточно иметь
некоторую форму структурированных данных.

Работа с базами данных SQL в PyQt


Теперь, когда мы рассмотрели архитектуру Model/View в PyQt и класс QTableView,
давайте посмотрим, как использовать SQL для работы со структурированными
данными.

Что такое SQL?


Язык структурированных запросов (SQL) - это язык программирования,
предназначенный для работы с базами данных. Данные, хранящиеся в базах данных,
организованы в виде набора таблиц. Строки таблиц называются записями, а столбцы
- полями. Каждый столбец может хранить только определенный вид информации,
например, имена, даты или числа.
С помощью SQL можно выполнять запросы к данным, хранящимся в реляционных
базах данных - совокупности элементов данных, которые имеют предопределенные
связи между несколькими таблицами, обозначенные уникальным идентификатором,
называемым внешним ключом. В реляционной базе данных несколько таблиц
составляют схему, несколько схем составляют базу данных, и эти базы данных
хранятся на сервере. Реляционные базы данных позволяют одновременно работать
с данными нескольким пользователям. По этой причине доступ к базе данных часто
требует от пользователя ввода имени и пароля для подключения к ней.
В данном разделе речь пойдет исключительно об использовании SQL и классов из
модуля QtSql в PyQt для создания базового интерфейса системы управления базами
данных.

356 ГЛАВА 14 ВВЕДЕНИЕ В РАБОТУ С БАЗАМИ ДАННЫХ


Работа с системами управления базами данных
Модуль QtSql предоставляет драйверы для ряда систем управления
реляционными базами данных (РСУБД), включая MySQL, Oracle, Microsoft SQL
Server, PostgreSQL и SQLite версий 2 и 3. РСУБД - это программное обеспечение,
позволяющее пользователям взаимодействовать с реляционными базами данных
с помощью языка SQL. Более подробную информацию о драйверах Qt SQL можно
найти на сайте https://doc-snapshots.qt.io/qt6-dev/sql-driver.html.
В следующих примерах мы будем использовать SQLite 3, поскольку эта библиотека
уже поставляется с Python и входит в состав Qt. SQLite не является клиент-серверным
механизмом баз данных, поэтому нам не нужен сервер баз данных. Кроме того, SQLite
работает с одним файлом и используется в основном для небольших настольных
приложений.

Знакомство с командами SQL


Язык SQL уже имеет свои команды для формирования запросов к базам данных.
С помощью этих команд пользователь может выполнять ряд различных действий по
взаимодействию с таблицами базы данных. Например, оператор SQL SELECT может
быть использован для получения записей из таблицы. Если бы у вас была база данных
для реестра идентификации собак, содержащая таблицу dog_registry, вы могли бы
выбрать все записи в этой таблице с помощью следующего оператора:
SELECT * FROM dog_registry
Звездочка, *, означает все столбцы таблицы. При создании запроса необходимо
учитывать, откуда берутся данные, в том числе из какой базы данных или таблицы.
Следует помнить о том, какие поля будут использоваться. Также следует помнить
об условиях отбора. Например, нужно ли отобразить всех домашних животных
в базе данных или только определенную породу собак? Пример такого выбора с
использованием реестра dog_registry показан в следующей строке:
SELECT name FROM dog_registry WHERE breed = 'shiba inu'
Использование различных драйверов, скорее всего, повлечет за собой
использование различного синтаксиса SQL, но PyQt может справиться с этими
различиями. В табл. 14-1 перечислены несколько распространенных команд SQLite 3,
которые будут использоваться в примерах этой главы.

ГЛАВА 14 ВВЕДЕНИЕ В РАБОТУ С БАЗАМИ ДАННЫХ 357


Таблица 14-1. Список общих ключевых слов и функций SQLite, которые
встречаются в этой главе

Ключевые слова
Описание
SQLite
AUTOINCREMENT Автоматически генерирует уникальный номер при вставке новой
записи в таблицу
CREATE TABLE Создает новую таблицу в базе данных
DELETE Удаляет строку из таблицы
DROP TABLE Удаляет таблицу, которая уже существует в базе данных
FOREIGN KEY Ограничение, связывающее две таблицы между собой
FROM Указание таблицы, с которой необходимо взаимодействовать при
выборе или удалении данных
INTEGER Целочисленный тип данных со знаком
INSERT INTO Вставляет новые строки в таблицу
MAX() Функция, находящая максимальное значение указанного столбца
NOT NULL Ограничение, гарантирующее, что столбец не будет принимать
значения NULL
PRIMARY KEY Ограничение, однозначно идентифицирующее запись в таблице
REFERENCES Используется с FOREIGN KEY для указания другой таблицы,
имеющей связь с первой таблицей.
SELECT Выборка данных из базы данных
SET Определяет, какие столбцы и значения должны быть обновлены
UNIQUE Ограничение, обеспечивающее уникальность всех значений в
столбце
UPDATE Обновление существующих значений в строке
VALUES Определяет значения оператора INSERT INTO
VARCHAR Переменный символьный тип данных для строк
WHERE Фильтрует результаты запроса, чтобы включить в него только
записи, удовлетворяющие определенным условиям

В следующих разделах мы будем работать над созданием пользовательского


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

Проект 14.1 - Графический интерфейс управления


учетной записью
В этом проекте мы будем использовать другой подход к разработке графического
интерфейса управления учетной записью. В этом разделе мы рассмотрим несколько
358 ГЛАВА 14 ВВЕДЕНИЕ В РАБОТУ С БАЗАМИ ДАННЫХ
небольших примеров программ, которые позволят подготовить окончательный
вариант проекта. Приходится распаковывать большой объем информации, и если
вы впервые работаете с SQL, особенно для создания интерфейса в PyQt, то процесс
работы с базами данных может стать немного непонятным.
Представьте, что у вас есть предприятие, и вы хотите создать базу данных для
хранения информации о своих сотрудниках. Вы хотите включить в нее такие
сведения, как фамилии, имена и отчества, идентификаторы сотрудников, адреса
электронной почты, отделы и страны, в которых они работают. (Эту базу можно
расширить, включив в нее дополнительную информацию, например, о зарплате,
номерах телефонов и датах приема на работу). На начальном этапе можно обойтись
небольшой базой данных. Однако по мере расширения штата сотрудников будет
расти и количество информации. Некоторые сотрудники могут иметь одинаковые
имена и фамилии или даже работать в одной стране. Необходимо найти способ
управления всеми этими сотрудниками, чтобы поля базы данных заполнялись
правильной информацией и типами данных.
Использование реляционной базы данных позволяет избежать проблем с
целостностью данных. Мы можем создать несколько таблиц: одну для счетов
различных сотрудников и одну для стран. В данном примере мы используем только
повторяющиеся названия стран, чтобы продемонстрировать, как использовать
классы PyQt для работы с реляционными базами данных. На рис. 14-4 показан
графический интерфейс управления счетами.

Рисунок 14-4. Графический интерфейс управления учетными записями. В последней


строке таблицы отображается добавление новой записи в базу данных

ГЛАВА 14 ВВЕДЕНИЕ В РАБОТУ С БАЗАМИ ДАННЫХ 359


Данный проект разбит на следующие части:
1. Знакомство с использованием QSqlDatabase для подключения к базам данных
и QSqlQuery для создания запросов.
2. Несколько примеров использования QSqlQuery для редактирования
элементов базы данных.
3. Представление QSqlTableModel для создания редактируемых моделей данных,
работающих с таблицами, не содержащими внешних ключей.
4. Показать, как использовать QSqlRelationalTableModel для создания
редактируемых моделей данных, работающих с таблицами, в которых есть
поддержка внешних ключей.
5. Создать графический интерфейс управления учетной записью.
Приступим к работе!

Пояснения к работе с модулем QtSql


В этом первом примере мы рассмотрим, как использовать QSqlQuery для создания
небольшой базы данных, которую мы сможем просматривать в графическом
интерфейсе управления счетами. База данных состоит из двух таблиц - "Счета" и
"Страны". Эти две таблицы связаны между собой через поле country_id в счетах и поле
id в странах.

Примечание. Данная программа не создает графический интерфейс


пользователя. Она демонстрирует, как начать работу с QSqlDatabase для
подключения к базе данных и как использовать QSqlQuery для создания
записей в базе данных. Кроме того, создается база данных accounts.db,
которая используется в оставшейся части главы.

Создание соединения с базой данных


Поскольку в этой программе не создается графический интерфейс, нам потребуется
только импортировать классы QSqlDatabase и QSqlQuery из QtSql. QSqlDatabase
мы будем использовать для создания соединения, обеспечивающего доступ к базе
данных, а QSqlQuery - для выполнения SQL-запросов в PyQt.
Соединение с базой данных выполнено в Листинге 14-4.

360 ГЛАВА 14 ВВЕДЕНИЕ В РАБОТУ С БАЗАМИ ДАННЫХ


Листинг 14-4. Подключение к базе данных с помощью QSqlDatabase
# create_database.py
# Импорт необходимых модулей
import sys, random
from PyQt6.QtSql import QSqlDatabase, QSqlQuery

class CreateEmployeeData:
"""Создаем пример базы данных для проекта.
Класс демонстрирует, как подключаться к базе данных, создавать
запросы, создавать таблицы и записи в этих таблицах."""
# Создаем соединение с базой данных. Если файл db
# не существует, будет создан новый файл db.
# Использовать драйвер SQLite версии 3
database = QSqlDatabase.addDatabase("QSQLITE")
database.setDatabaseName("files/accounts.db")
if not database.open():
print("Невозможно открыть файл источника данных.")
sys.exit(1) # Код ошибки 1 - означает ошибку

Начнем с создания соединения с базой данных в классе CreateEmployeeData.


Функция addDatabase() позволяет указать используемый SQL-драйвер. В примерах
этой главы используется SQLite 3, поэтому мы передаем QSQLITE. После создания
объекта базы данных мы можем задать другие параметры подключения, в том числе,
какую базу данных мы будем использовать, имя пользователя, пароль, имя хоста и
порт подключения. Для SQLite 3 достаточно указать имя базы данных с помощью
функции setDatabaseName(). Можно также создать несколько соединений с базой
данных, передав в addDatabase() после аргумента driver дополнительный аргумент -
имя соединения.

Примечание. Ссылка на соединение осуществляется по его имени, а не по


имени базы данных. Если вы хотите присвоить базе данных имя, передайте
его в качестве аргумента после драйвера в методе addDatabase(). Если имя
не указано, то будет использовано соединение по умолчанию.

Если файл accounts.db еще не существует, то он будет создан. После установки


параметров необходимо вызвать open() для активации соединения с базой данных.
Соединение не может быть использовано до тех пор, пока оно не будет открыто.

ГЛАВА 14 ВВЕДЕНИЕ В РАБОТУ С БАЗАМИ ДАННЫХ 361


Построение набора данных с помощью QSqlQuery
Теперь, когда в CreateEmployeeData установлены соединения, можно приступить к
формированию запросов к базе данных. Как правило, можно начать с баз данных, в
которых уже есть данные, но в данном примере мы рассмотрим, как можно создать
базу данных с помощью команд SQL. Чтобы выполнить запрос к базе данных
с помощью PyQt, сначала нужно создать экземпляр QSqlQuery. Это делается в
Листинге 14-5.
Листинг 14-5. Создание набора данных с помощью QSqlQuery в классе
CreateEmployeeData
# create_database.py
query = QSqlQuery()
# Стирание содержимого базы данных
query.exec("DROP TABLE accounts")
query.exec("DROP TABLE countries")

query.exec("""CREATE TABLE accounts (


id INTEGER PRIMARY KEY AUTOINCREMENT UNIQUE NOT NULL,
employee_id INTEGER NOT NULL,
first_name VARCHAR(30) NOT NULL,
last_name VARCHAR(30) NOT NULL,
email VARCHAR(40) NOT NULL,
department VARCHAR(20) NOT NULL,
country_id VARCHAR(20) REFERENCES countries(id))""")

# Позиционная привязка для вставки записей в базу данных


query.prepare("""INSERT INTO accounts (
employee_id, first_name, last_name,
email, department, country_id)
VALUES (?, ?, ?, ?, ?, ?)""")

Метод exec() используется для выполнения SQL-запросов в PyQt. В следующих


строках мы хотим создать объект запроса и удалить accounts из таблицы:
query = QSqlQuery()
query.exec("DROP TABLE accounts")
Далее создадим новую таблицу счетов с помощью exec() и SQL-команды
CREATE TABLE accounts. Каждая запись таблицы будет иметь свой уникальный
идентификатор с помощью AUTOINCREMENT. Таблица accounts будет содержать
информацию об идентификаторе сотрудника, его имени, фамилии, электронной
почте, отделе и стране, в которой он находится. Мы также создадим таблицу countries,
которая будет содержать названия стран сотрудников и будет связана с таблицей
accounts с помощью следующей строки:

362 ГЛАВА 14 ВВЕДЕНИЕ В РАБОТУ С БАЗАМИ ДАННЫХ


country_id VARCHAR(20) REFERENCES countries(id))
В строке country_id содержится ссылка на id таблицы countries. На рис. 14-5
показана связь между двумя таблицами.

Рисунок 14-5. Связь между таблицами "Счета" и "Страны"


Следующей задачей является вставка записей в таблицы. Мы могли бы продолжать
использовать exec() для выполнения запросов, но при большом объеме базы данных
это стало бы утомительным. Для одновременной вставки нескольких записей мы
отделяем запрос от собственно вставляемых значений с помощью заполнителей и
метода prepare(). Заполнитель будет выступать в роли временной переменной, что
позволит пользователям вводить различные данные в один и тот же SQL-запрос. В
приведенном ниже коде позиционные заполнители - это "?". PyQt поддерживает два
варианта синтаксиса заполнителей: стиль ODBC, в котором используется "?", и стиль
Oracle, в котором используется :field_name.
query.prepare("""INSERT INTO accounts (
employee_id, first_name, last_name,
email, department, country_id)
VALUES (?, ?, ?, ?, ?, ?, ?)""")
Каждое поле, например, employee_id или first_name, связано с одним из
заполнителей. Поскольку мы использовали AUTOINCREMENT для id, нам не нужно
включать в запрос ни поле, ни заполнитель.
Метод prepare() подготавливает запрос к выполнению. Если запрос подготовлен
успешно, то с помощью метода addBindValue() можно привязать значения к
полям. Информацию о выполнении SQL-запросов в Qt можно найти на сайте
https://doc.qt.io/qt-6/sql-sqlstatements.html. Различные подходы к привязке значений
можно найти на https://doc.qt.io/qt-6/sql-sqlstatements.html.
Далее мы создадим значения для полей first_name, last_name и других полей в
таблицах SQL, используя списки и словари Python в Листингах 14-6 и 14-7.

ГЛАВА 14 ВВЕДЕНИЕ В РАБОТУ С БАЗАМИ ДАННЫХ 363


Листинг 14-6. Создание значений для примера набора данных в классе
CreateEmployeeData, часть 1
# create_database.py
first_names = ["Emma", "Olivia", "Ava", "Isabella", "Sophia",
"Mia", "Charlotte", "Amelia", "Evelyn", "Abigail",
"Valorie", "Teesha", "Jazzmin", "Liam", "Noah",
"William", "James", "Logan", "Benjamin", "Mason",
"Elijah", "Oliver", "Jason", "Lucas", "Michael"]

last_names = ["Smith", "Johnson", "Williams", "Brown", "Jones",


"Garcia", "Miller", "Davis", "Rodriguez", "Martinez",
"Hernandez", "Lopez", "Gonzalez", "Wilson", "Anderson",
"Thomas", "Taylor", "Moore", "Jackson", "Martin", "Lee",
"Perez", "Thompson", "White", "Harris"]

# Создайте данные для первой таблицы, account


employee_ids = random.sample(
range(1000, 2500), len(first_names))
countries = {"USA": 1, "India": 2, "China": 3,
"France": 4, "Germany": 5}
country_names = list(countries.keys())
country_codes = list(countries.values())
departments = ["Production", "R&D", "Marketing", "HR",
"Finance", "Engineering", "Managerial"]

for f_name in first_names:


l_name = last_names.pop()
email = (l_name + f_name[0]).lower() + "@job.com"
country_id = random.choice(country_codes)
dept = random.choice(departments)
employee_id = employee_ids.pop()
query.addBindValue(employee_id)
query.addBindValue(f_name)
query.addBindValue(l_name)
query.addBindValue(email)
query.addBindValue(dept)
query.addBindValue(country_id)
query.exec()

Затем используется цикл for, в котором мы привязываем значения к заполнителям. В


конце каждой итерации вызывается метод exec() для вставки значений в таблицу счетов.
Аналогичным образом подготавливается таблица стран, приведенная в Листинге 14-7.

364 ГЛАВА 14 ВВЕДЕНИЕ В РАБОТУ С БАЗАМИ ДАННЫХ


Листинг 14-7. Создание значений для набора данных примера в классе
CreateEmployeeData, часть 2
# create_database.py
# Создайте данные для второй таблицы, стран
country_query = QSqlQuery()
country_query.exec("""CREATE TABLE countries (
id INTEGER PRIMARY KEY AUTOINCREMENT UNIQUE NOT NULL,
country VARCHAR(20) NOT NULL)""")

country_query.prepare(
"INSERT INTO countries (country) VALUES (?)")

for name in country_names:


country_query.addBindValue(name)
country_query.exec()

print("[INFO] Database successfully created.")

if __name__ == "__main__":
CreateEmployeeData()
sys.exit(0)

После заполнения таблиц мы вызываем sys.exit(0) для выхода из программы.


Экземпляр QApplication отсутствует, поскольку нет графического интерфейса.

Визуализация данных SQL с помощью SQLite


Если вы хотите создать из файла accounts.db читаемый файл SQLite для
визуализации данных, то у вас есть несколько вариантов. Первый - использовать
доступные инструменты, например SQLiteStudio на сайте https://sqlitestudio.pl/, для
просмотра баз данных.
Другой вариант - использовать библиотеку SQLite. Если вы используете macOS или
Linux, она уже должна быть установлена в вашей системе. Для Windows, возможно,
придется выполнить следующие дополнительные шаги (пользователи macOS и Linux
могут пропустить этот список):
1. На странице загрузки SQLite, www.sqlite.org/download.html, загрузите
предварительно скомпилированные двоичные файлы для Windows. Найдите
вариант, включающий инструменты командной строки. Найдите на своем
компьютере место, куда загрузились файлы. Внутри папки вы обнаружите три
файла, один из которых - sqlite3.exe.
2. Откройте окно оболочки и перейдите по адресу C:\>. Далее, mkdir sqlite.
3. Переместите три файла из шага 1 в новую папку sqlite. Один из способов
сделать это - открыть папку с начальным именем .\sqlite и перетащить файлы.
ГЛАВА 14 ВВЕДЕНИЕ В РАБОТУ С БАЗАМИ ДАННЫХ 365
4. Для того чтобы вы могли использовать SQLite независимо от того, в какой папке
вы находитесь, вам необходимо добавить созданную папку sqlite в переменную
окружения PATH. В командной строке введите $env:Path +=";C:\sqlite".
5. Наконец, запустите sqlite3 в командной строке, и вы должны войти в среду
оболочки SQLite. Для выхода наберите .quit.
Убедившись, что SQLite установлен, перейдите в папку files в каталоге приложения,
где находится файл accounts.db, и выполните в оболочке следующую команду sqlite3:
$ sqlite3 accounts.db .dump >> accounts.sql
Вы увидите, что в папке files создан новый файл accounts.sql. Также стоит
отметить, что SQLite 3 входит в состав стандартной библиотеки Python, поэтому при
необходимости вы можете импортировать sqlite в свои приложения.
В следующем разделе мы рассмотрим, как использовать QSqlQuery не только для
создания таблиц.

Пояснения к запросу к базе данных с помощью


QSqlQuery
Программа, созданная в Листингах 14-8-14-10, не является необходимой для
графического интерфейса менеджера учета, но она дает еще несколько примеров
для понимания того, как вводить, обновлять и удалять записи с помощью SQL в
приложении PyQt. Цель раздела - продемонстрировать, как открыть существующую
базу данных и изменить ее содержимое. Мы сделаем это для базы данных, созданной
в разделе "Пояснения к работе с модулем QtSql".
Для Листинга 14-8 снова импортируем классы QSqlDatabase и QSqlQuery. Также
создадим новый класс QueryExamples и создадим два метода класса:
• createConnection() - устанавливает соединение с базой данных
• exampleQueries() - запрашивает базу данных для получения и изменения
существующих записей.
Листинг 14-8. Создание соединения для класса QueryExamples
# query_examples.py
# Импорт необходимых модулей
import sys
from PyQt6.QtSql import QSqlDatabase, QSqlQuery

class QueryExamples:

def __init__(self):
super().__init__()
self.createConnection()
self.exampleQueries()

366 ГЛАВА 14 ВВЕДЕНИЕ В РАБОТУ С БАЗАМИ ДАННЫХ


def createConnection(self):
"""Создание соединения с базой данных."""
database = QSqlDatabase.addDatabase("QSQLITE")
database.setDatabaseName("files/accounts.db")
if not database.open():
print("Невозможно открыть файл источника данных.")
sys.exit(1) # Код ошибки 1 - означает ошибку

Начнем с добавления базы данных, используя драйвер SQLite 3 и соединение


по умолчанию, поскольку в функции addDatabase() имя соединения не передается.
Далее установим базу данных, созданную в предыдущей программе, accounts.db.
Далее завершим подключение с помощью функции open().
В exampleQueries() в Листинге 14-9 рассмотрим, как использовать класс QSqlQuery
и SQL-команду SELECT для запроса к базе данных.
Листинг 14-9. Демонстрация доступа к базам данных SQL в PyQt
# query_examples.py
def exampleQueries(self):
"""Примеры работы с базой данных."""
# Конструктор QSqlQuery принимает необязательный
# объект QSqlDatabase, который указывает, какое
# соединение с базой данных следует использовать.
# В данном примере мы не указываем никакого соединения,
# поэтому используется соединение по умолчанию.
# При возникновении ошибки функция exec() возвращает false.
# Ошибка затем доступна в виде SqlQuery::lastError()
# Выполнение простого запроса
query = QSqlQuery()
query.exec("SELECT first_name, last_name FROM \
accounts WHERE employee_id > 2000")

# Навигация по набору результатов


while (query.next()):
f_name = str(query.value(0))
l_name = str(query.value(1))
print(f_name, l_name)

Мы создаем новый экземпляр QSqlQuery для поиска имен и фамилий сотрудников,


чьи идентификаторы сотрудников больше 2000.
С помощью этого запроса мы можем использовать значения first_name и last_name
для обновления или удаления записей. Для циклического просмотра результатов
запроса мы используем метод QSqlQuery next(). Другие методы, которые можно
использовать для навигации по результатам, включают next(), previous(), first() и last().
Дополнительные запросы показаны в Листинге 14-10.

ГЛАВА 14 ВВЕДЕНИЕ В РАБОТУ С БАЗАМИ ДАННЫХ 367


Листинг 14-10. Демонстрация вставки, обновления и удаления записей с помощью
SQL и PyQt
# query_examples.py
# Вставка одной новой записи в базу данных
query.exec("""INSERT INTO accounts (
employee_id, first_name, last_name,
email, department, country_id)
VALUES (2134, 'Robert', 'Downey',
'downeyr@job.com', 'Managerial', 1)""")

# Обновление записи в базе данных


query.exec("UPDATE accounts SET department = 'R&D' \
WHERE employee_id = 2134")

# Удалить запись из базы данных


query.exec("DELETE FROM accounts WHERE \
employee_id <= 1500")

if __name__ == "__main__":
QueryExamples()
sys.exit(0)

Для вставки одной записи мы можем использовать SQL-команду INSERT. В этом


запросе мы вставляем определенные значения для каждого поля. Можно также
добавить в базу данных несколько записей. Обратитесь к разделу "Пояснения к
работе с модулем QtSql", чтобы понять, как это сделать.
Для обновления записей используется функция UPDATE. Мы обновляем значение
отдела для сотрудника, который был только что вставлен. Наконец, для удаления
записи используется DELETE.
Этот пример также не имеет окна графического интерфейса. Чтобы увидеть
изменения, можно запустить эту программу после выполнения программы из
раздела "Пояснения к работе с модулем QtSql", а затем воспользоваться графическим
интерфейсом из следующего раздела для визуализации результатов в таблице.

Работа с классом QSqlTableModel


Наконец-то мы создадим графический интерфейс для визуализации содержимого
базы данных. В таблице на рис. 14-6 мы будем визуализировать только таблицу счетов,
чтобы продемонстрировать работу класса QSqlTableModel - интерфейса, удобного
для чтения и записи записей базы данных, когда требуется использовать только одну
таблицу без связей с другими таблицами. Следующая программа продемонстрирует,
как использовать программирование Model/View для просмотра содержимого базы
данных SQL.
368 ГЛАВА 14 ВВЕДЕНИЕ В РАБОТУ С БАЗАМИ ДАННЫХ
Рисунок 14-6. Таблица, созданная с помощью QSqlTableModel
Мы могли бы использовать QSqlQuery для выполнения всей работы с базой данных,
но сочетание этого класса с парадигмой Model/View позволяет нам разрабатывать
графические интерфейсы, которые упрощают процесс управления данными.

Пояснения к работе с QSqlTableModel


Начнем с использования сценария basic_window.py из Главы 1, а затем
импортируем необходимые нам классы PyQt, включая QSqlTableModel, как показано
в Листинге 14-11. QHeaderView - это класс, который обеспечивает горизонтальные и
вертикальные заголовки для классов представления элементов.
Далее создадим класс MainWindow для отображения содержимого базы данных.
Листинг 14-11. Код класса MainWindow с использованием QSqlTableModel
# table_model.py
# Импорт необходимых модулей
import sys
from PyQt6.QtWidgets import (QApplication, QWidget,
QTableView, QHeaderView, QMessageBox, QVBoxLayout)
from PyQt6.QtSql import QSqlDatabase, QSqlTableModel

class MainWindow(QWidget):

def __init__(self):
super().__init__()
self.initializeUI()

def initializeUI(self):
ГЛАВА 14 ВВЕДЕНИЕ В РАБОТУ С БАЗАМИ ДАННЫХ 369
"""Настройка графического интерфейса приложения."""
self.setMinimumSize(1000, 500)
self.setWindowTitle("Модель таблицы SQL")

self.createConnection()
self.setUpMainWindow()
self.show()

if __name__ == "__main__":
app = QApplication(sys.argv)
window = MainWindow()
sys.exit(app.exec())

Мы вызываем createConnection() перед setUpMainWindow(), так как объекты


модели и представления в главном окне зависят от данных из базы. В методе
createConnection() в Листинге 14-12 мы подключаемся к базе данных и активизируем
соединение с помощью open(). На этот раз проверим, что таблицы, которые мы
хотим использовать, находятся в базе данных. Если их не удастся найти, то на экран
будет выведено диалоговое окно, подобное приведенному на рис. 14-7, о чем будет
сообщено пользователю, и программа закроется.

Рисунок 14-7. QMessageBox, сообщающий пользователям, что таблица, которую


они хотят просмотреть, отсутствует
Листинг 14-12. Код метода createConnection() в примере QSqlTableModel
# table_model.py
def createConnection(self):
"""Установите соединение с базой данных.
Проверьте наличие необходимых таблиц."""
database = QSqlDatabase.addDatabase("QSQLITE")
database.setDatabaseName("files/accounts.db")

if not database.open():
print("Невозможно открыть файл источника данных.")
sys.exit(1) # Код ошибки 1 - означает ошибку

# Проверить, существуют ли в базе данных нужные нам таблицы


tables_needed = {"accounts"}

370 ГЛАВА 14 ВВЕДЕНИЕ В РАБОТУ С БАЗАМИ ДАННЫХ


tables_not_found = tables_needed - \
set(database.tables())
if tables_not_found:
QMessageBox.critical(None, "Error",
f"""<p>The following tables are missing
from the database: {tables_not_found}</p>""")
sys.exit(1) # Код ошибки 1 - означает ошибку

Экземпляры QSqlTableModel и QTableView создаются в методе setUpMainWindow()


в Листинге 14-13. Для QSqlTableModel необходимо задать таблицу базы данных,
которую мы хотим использовать, с помощью функции setTable(). В данном случае мы
будем использовать таблицу счетов.
Листинг 14-13. Код метода setUpMainWindow() в примере QSqlTableModel
# table_model.py
def setUpMainWindow(self):
"""Создание и расположение виджетов в главном окне."""
# Создание модели
model = QSqlTableModel()
model.setTable("accounts")

table_view = QTableView()
table_view.setModel(model)
table_view.horizontalHeader().setSectionResizeMode(
QHeaderView.ResizeMode.Stretch)

# Наполнение модели данными


model.select()

main_v_box = QVBoxLayout()
main_v_box.addWidget(table_view)
self.setLayout(main_v_box)

Далее создаем объект QTableView и задаем его модель с помощью функции


setModel(). Для того чтобы таблица растягивалась и помещалась в представление по
горизонтали, используем следующую строку:
table_view.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
Эта строка также обрабатывает растягивание таблицы при изменении размеров
окна.
Наконец, модель заполняется данными с помощью функции select(). Если вы
внесли изменения в таблицу, но не представили их, то функция select() приведет к
возврату отредактированных элементов в прежнее состояние.
На рис. 14-6 показано содержимое базы данных в виде таблицы. Обратите
внимание, что метки заголовков отображают имена полей, использованные при
ГЛАВА 14 ВВЕДЕНИЕ В РАБОТУ С БАЗАМИ ДАННЫХ 371
создании базы данных. Мы увидим, как устанавливать метки заголовков, когда
будем создавать графический интерфейс управления счетами. Кроме того, в столбце
country_id в настоящее время отображаются только числа, связанные с различными
названиями в таблице стран. Если вы хотите отобразить только определенные
столбцы, то следующий код позволяет выбрать, какие именно столбцы вы хотите
отобразить:
model.setQuery(QSqlQuery("SELECT id, employee_id, first_name, last_name FROM accounts"))
В следующем разделе вы узнаете, как создавать и отображать в представлении
таблицы отношения, заданные внешними ключами.

Работа с классом QSqlRelationalTableModel


Далее мы рассмотрим, как использовать класс QSqlRelationalTableModel для работы
с реляционными базами данных. Класс QSqlRelationalTableModel предоставляет
модель для управления и редактирования данных в таблице SQL с дополнительной
поддержкой использования внешних ключей. Внешний ключ - это SQL-ограничение,
используемое для связи таблиц между собой.
Приложение на рис. 14-8 построено на основе графического интерфейса,
описанного в разделе "Работа с классом QSqlTableModel".

Рисунок 14-8. Таблица, созданная с помощью QSqlRelationalTableModel

372 ГЛАВА 14 ВВЕДЕНИЕ В РАБОТУ С БАЗАМИ ДАННЫХ


Пояснения к работе с QSqlRelationalTableModel
Начнем со сценария basic_window.py из Главы 1. На этот раз нам необходимо
импортировать QSqlRelationalTableModel, поскольку мы работаем с реляционными
базами данных и внешними ключами. Также в состав включен класс QSqlRelation,
поскольку нам потребуется использовать его для хранения информации о внешних
ключах SQL. QSqlRelationalDelegate также необходим, поскольку нам нужно будет
отображать виджеты редактора в столбцах, относящихся к внешним ключам.
В листинге 14-14 все это выполняется, а также устанавливается класс MainWindow.
Листинг 14-14. Код класса MainWindow, использующего QSqlRelationalTableModel
# relational_model.py
# Импорт необходимых модулей
import sys
from PyQt6.QtWidgets import (QApplication, QWidget,
QTableView, QMessageBox, QHeaderView, QVBoxLayout)
from PyQt6.QtSql import (QSqlDatabase, QSqlRelation,
QSqlRelationalTableModel, QSqlRelationalDelegate)

class MainWindow(QWidget):

def __init__(self):
super().__init__()
self.initializeUI()

def initializeUI(self):
"""Настройка графического интерфейса приложения."""
self.setMinimumSize(1000, 500)
self.setWindowTitle("Модель реляционной таблицы")

self.createConnection()
self.setUpMainWindow()
self.show()

if __name__ == "__main__":
app = QApplication(sys.argv)
window = MainWindow()
sys.exit(app.exec())

В Листинге 14-15 мы подключаемся к базе данных точно так же, как и в примере
QSqlTableModel, только в этот раз мы проверяем обе таблицы - accounts и countries.

ГЛАВА 14 ВВЕДЕНИЕ В РАБОТУ С БАЗАМИ ДАННЫХ 373


Листинг 14-15. Код метода createConnection() в примере QSqlRelationalTableModel
# relational_model.py
def createConnection(self):
"""Установите соединение с базой данных.
Проверьте наличие необходимых таблиц."""
database = QSqlDatabase.addDatabase("QSQLITE")
database.setDatabaseName("files/accounts.db")

if not database.open():
print("Невозможно открыть файл источника данных.")
sys.exit(1) # Код ошибки 1 - означает ошибку

# Проверить, существуют ли в базе данных нужные нам таблицы


tables_needed = {"accounts", "countries"}
tables_not_found = tables_needed - \
set(database.tables())
if tables_not_found:
QMessageBox.critical(None, "Error",
f"""<p>The following tables are missing
from the database: {tables_not_found}</p>""")
sys.exit(1) # Код ошибки 1 - означает ошибку

Для функции setUpMainWindow() в Листинге 14-16 создайте экземпляры классов


QSqlRelationalTableModel и QTableView. Метод setTable() заставляет модель получить
информацию о таблице счетов.
Листинг 14-16. Код метода setUpMainWindow() в примере QSqlRelationalTableModel
# relational_model.py
def setUpMainWindow(self):
"""Создание и расположение виджетов в главном окне."""
# Создание модели
model = QSqlRelationalTableModel()
model.setTable("accounts")

# Настройка отношений для внешних ключей


model.setRelation(model.fieldIndex("country_id"),
QSqlRelation("countries", "id", "country"))
table_view = QTableView()
table_view.setModel(model)
table_view.horizontalHeader().setSectionResizeMode(
QHeaderView.ResizeMode.Stretch)

# Наполнение модели данными


model.select()

374 ГЛАВА 14 ВВЕДЕНИЕ В РАБОТУ С БАЗАМИ ДАННЫХ


Поле country_id в accounts сопоставлено с полем id таблицы countries. Для метода
QSqlRelationalTableModel setRelation() нам необходимо передать индекс столбца,
содержащего внешний ключ (это делается с помощью функции fieldIndex()), и
объект QSqlRelation, определяющий связь. Для QSqlRelation поле id таблицы countries
сопоставляется с country_id в таблице accounts. Последний аргумент, country,
указывает, какое поле должно быть отображено в таблице accounts.
Если сравнить рис. 14-8 с рис. 14-6, то можно заметить, что данные в последнем
столбце, country, были обновлены для отображения названий стран, а заголовок
также был изменен на country.

Добавление делегатов для редактирования


реляционных данных
Назначение делегатов при использовании классов Model/View становится более
очевидным, когда вы начинаете создавать собственные пользовательские классы
или когда вам необходимо использовать реляционные классы для выбора значений
полей с внешними ключами. При использовании делегата виджет редактора,
такой как QLineEdit или QComboBox, будет появляться при редактировании данных
пользователем. Возможно, вы даже не осознаете, что каждый раз, когда вы
редактируете значения в ячейках QTableView, вы все время используете делегат. Это
объясняется тем, как Qt органично сочетает представление и делегаты.
Для реляционных баз данных SQL можно просматривать и редактировать данные
QSqlRelationalDelegate из модели QSqlRelationalTableModel.
В заключительной части setUpMainWindow() создадим экземпляр
QSqlRelationalDelegate и добавим его к table_view с помощью setItemDelegate() в
Листинге 14-17.
Листинг 14-17. Добавление делегатов в примере QSqlRelationalTableModel
# relational_model.py
# Инстанцирование делегата
delegate = QSqlRelationalDelegate()
table_view.setItemDelegate(delegate)

main_v_box = QVBoxLayout()
main_v_box.addWidget(table_view)
self.setLayout(main_v_box)

Теперь, если дважды щелкнуть мышью в колонке стран, появится QComboBox,


содержащий список стран. Пример этого показан на рис. 14-9.

ГЛАВА 14 ВВЕДЕНИЕ В РАБОТУ С БАЗАМИ ДАННЫХ 375


Рисунок 14-9. Виджет редактора (QComboBox), отображаемый в столбце с
внешними ключами
Последний шаг - создание и установка макета главного окна.
К этому моменту вы должны иметь фундаментальное представление о том,
как использовать классы моделей и представлений, использовать классы QtSql
для выполнения запросов и отображать отношения внешних ключей в таблице
с помощью реляционных классов. Теперь мы готовы к созданию графического
интерфейса управления счетами.

Пояснения к графическому интерфейсу управления


учетными записями
Графический интерфейс управления счетами использует модель
QSqlRelationalTableModel для управления таблицами accounts и countries. Мы будем
использовать концепции, изученные в предыдущих разделах, для разработки
графического интерфейса с возможностями прямого, а не программного управления
базой данных. Обратитесь к рис. 14-4, чтобы увидеть интерфейс.
Приложение позволяет пользователю добавлять, удалять и сортировать
содержимое таблицы. Добавленные или удаленные строки также обновляют базу
данных.
Также не забудьте скачать с GitHub папку icons для этого проекта.
Для выполнения Листинга 14-18 начнем со сценария basic_window.py из Главы 1 и
импортируем различные классы.
Листинг 14-18. Код класса MainWindow в графическом интерфейсе управления
учетной записью
# account_manager.py
# Импорт необходимых модулей
import sys, os
from PyQt6.QtWidgets import (QApplication, QWidget, QLabel,
QPushButton, QComboBox, QTableView, QHeaderView,
QAbstractItemView, QMessageBox, QHBoxLayout, QVBoxLayout,

376 ГЛАВА 14 ВВЕДЕНИЕ В РАБОТУ С БАЗАМИ ДАННЫХ


QSizePolicy)
from PyQt6.QtCore import Qt
from PyQt6.QtGui import QIcon
from PyQt6.QtSql import (QSqlDatabase, QSqlQuery,
QSqlRelation, QSqlRelationalTableModel,
QSqlRelationalDelegate)

class MainWindow(QWidget):

def __init__(self):
super().__init__()
self.initializeUI()

def initializeUI(self):
"""Настройка графического интерфейса приложения."""
self.setMinimumSize(1000, 600)
self.setWindowTitle("14.1 - Графический интерфейс управления счетами")
self.createConnection()
self.createModel()
self.setUpMainWindow()
self.show()

if __name__ == '__main__':
app = QApplication(sys.argv)
window = MainWindow()
sys.exit(app.exec())

Для данного графического интерфейса отдельный метод createModel() будет


создавать модель QSqlRelationalTableModel, которая используется объектом QTableView
в setUpMainWindow(). Это сделано для упорядочивания кода.
Следующей задачей является подключение к accounts.db, как мы уже делали ранее
в createConnections(). Эта задача решается в Листинге 14-19.
Листинг 14-19. Код метода createConnection() в графическом интерфейсе
управления счетами
# account_manager.py
def createConnection(self):
"""Установите соединение с базой данных.
Проверьте наличие необходимых таблиц."""
database = QSqlDatabase.addDatabase("QSQLITE")
database.setDatabaseName("files/accounts.db")

if not database.open():
print("Невозможно открыть файл источника данных.")
sys.exit(1) # Код ошибки 1 - означает ошибку
ГЛАВА 14 ВВЕДЕНИЕ В РАБОТУ С БАЗАМИ ДАННЫХ 377
# Проверить, существуют ли в базе данных нужные нам таблицы
tables_needed = {"accounts", "countries"}
tables_not_found = tables_needed - \
set(database.tables())
if tables_not_found:
QMessageBox.critical(None, "Ошибка",
f"""<p>The following tables are missing
from the database: {tables_not_found}</p>""")
sys.exit(1) # Код ошибки 1 - означает ошибку

Метод createModel() в Листинге 14-20 инстанцирует и устанавливает модель,


устанавливая связь по внешнему ключу между двумя таблицами с помощью функции
setRelation(). Метод setHeaderData() накладывает метки на каждый из столбцов. Для
указания индекса имени поля и изменения его значения мы можем использовать
метод fieldIndex(), который наследует QSqlTableModel - fieldRelationalTableModel.
Листинг 14-20. Код метода createModel() для графического интерфейса управления
счетами
# account_manager.py
def createModel(self):
"""Настройка модели и заголовков, заполнение модели."""
self.model = QSqlRelationalTableModel()
self.model.setTable("accounts")
self.model.setRelation(
self.model.fieldIndex("country_id"),
QSqlRelation("countries", "id", "country"))
self.model.setHeaderData(
self.model.fieldIndex("id"),
Qt.Orientation.Horizontal, "ID")
self.model.setHeaderData(
self.model.fieldIndex("employee_id"),
Qt.Orientation.Horizontal, "Employee ID")
self.model.setHeaderData(
self.model.fieldIndex("first_name"),
Qt.Orientation.Horizontal, "First")
self.model.setHeaderData(
self.model.fieldIndex("last_name"),
Qt.Orientation.Horizontal, "Last")
self.model.setHeaderData(
self.model.fieldIndex("email"),
Qt.Orientation.Horizontal, "E-mail")
self.model.setHeaderData(
self.model.fieldIndex("department"),
Qt.Orientation.Horizontal, "Dept.")
self.model.setHeaderData(
378 ГЛАВА 14 ВВЕДЕНИЕ В РАБОТУ С БАЗАМИ ДАННЫХ
self.model.fieldIndex("country_id"),
Qt.Orientation.Horizontal, "Country")

# Наполнение модели данными


self.model.select()

В Листинге 14-21 в методе setUpMainWindow() создается объект QTableView,


table_view. Также инстанцируются метки, кнопки и комбобокс графического
интерфейса.
Листинг 14-21. Код метода setUpMainWindow() в графическом интерфейсе
управления счетами, часть 1
# account_manager.py
def setUpMainWindow(self):
"""Создание и расположение виджетов в главном окне."""
icons_path = "icons"

title = QLabel("Account Management System")


title.setSizePolicy(QSizePolicy.Policy.Fixed,
QSizePolicy.Policy.Fixed)
title.setStyleSheet("font: bold 24px")

add_product_button = QPushButton("Добавить сотрудника")


add_product_button.setIcon(QIcon(os.path.join(
icons_path, "add_user.png")))
add_product_button.setStyleSheet("padding: 10px")
add_product_button.clicked.connect(self.addItem)

del_product_button = QPushButton("Удалить")
del_product_button.setIcon(QIcon(os.path.join(
icons_path, "trash_can.png")))
del_product_button.setStyleSheet("padding: 10px")
del_product_button.clicked.connect(self.deleteItem)

# Настройка сортировочного комбобокса


sorting_options = [
"Sort by ID", "Sort by Employee ID",
"Sort by First Name", "Sort by Last Name",
"Sort by Department", "Sort by Country"]
sort_combo = QComboBox()
sort_combo.addItems(sorting_options)
sort_combo.currentTextChanged.connect(
self.setSortingOrder)

buttons_h_box = QHBoxLayout()
ГЛАВА 14 ВВЕДЕНИЕ В РАБОТУ С БАЗАМИ ДАННЫХ 379
buttons_h_box.addWidget(add_product_button)
buttons_h_box.addWidget(del_product_button)
buttons_h_box.addStretch()
buttons_h_box.addWidget(sort_combo)

# Виджет, содержащий кнопки редактирования


edit_container = QWidget()
edit_container.setLayout(buttons_h_box)

Объекты add_product_button и del_product_button используются для добавления


и удаления элементов из таблицы и модели. Каждая кнопка связана со слотом с
помощью сигнала clicked. Слоты созданы в Листинге 14-23.
Элементы в представлении можно также сортировать. В sort_combo предусмотрены
различные способы упорядочивания данных таблицы на основе имен столбцов. Для
QComboBox при изменении выбора виджет может выдать сигнал currentTextChanged.
В этом сигнале также передается текст, который мы можем использовать для
определения порядка отображения записей в слоте setSortingOrder(). Эта операция
выполняется в Листинге 14-24. Затем виджеты располагаются в buttons_h_box и
добавляются в edit_container.
Для table_view в Листинге 14-22 задается его модель и несколько параметров. В
частности, мы задаем вертикальные и горизонтальные заголовки таблицы, чтобы
они растягивались и заполняли пространство в окне. В данном примере пользователь
может выбирать только отдельные элементы в таблице, используя флаг SingleSelection.
Листинг 14-22. Код метода setUpMainWindow() в графическом интерфейсе
управления счетами, часть 2
# account_manager.py
# Создание табличного представления и установка модели
self.table_view = QTableView()
self.table_view.setModel(self.model)
horizontal = self.table_view.horizontalHeader()
horizontal.setSectionResizeMode(
QHeaderView.ResizeMode.Stretch)
vertical = self.table_view.verticalHeader()
vertical.setSectionResizeMode(
QHeaderView.ResizeMode.Stretch)
self.table_view.setSelectionMode(
QAbstractItemView.SelectionMode.SingleSelection)
self.table_view.setSelectionBehavior(
QAbstractItemView.SelectionBehavior.SelectRows)

# Инстанцирование делегата
delegate = QSqlRelationalDelegate()
self.table_view.setItemDelegate(delegate)

380 ГЛАВА 14 ВВЕДЕНИЕ В РАБОТУ С БАЗАМИ ДАННЫХ


# Основной макет
main_v_box = QVBoxLayout()
main_v_box.addWidget(
title, Qt.AlignmentFlag.AlignLeft)
main_v_box.addWidget(edit_container)
main_v_box.addWidget(self.table_view)
self.setLayout(main_v_box)

Для классов представлений поведение при выборе строк, столбцов или отдельных
элементов может быть определено с помощью QAbstractItemView.SelectionBehavior. В
следующем списке описаны различные флаги:
• SelectRows - могут быть выбраны только строки
• SelectColumns - можно выбрать только столбцы
• SelectItems - можно выбрать только отдельные элементы
При программировании по принципу Model/View делегат предоставляет средства
по умолчанию для рисования данных элемента в представлении и для предоставления
виджетов редактора для моделей элементов. Внешний вид и виджеты редактора
делегата элемента могут быть настроены. Для графического интерфейса управления
счетами используется делегат QSqlRelationalDelegate. Этот класс предоставляет
комбобокс для редактирования данных в полях, являющихся внешними ключами
для других таблиц.
Пример комбобокса, используемого делегатом, можно увидеть в правом нижнем
углу рис. 14-4. Виджет появляется всякий раз, когда пользователю необходимо
выбрать страну из таблицы стран, которая будет отображаться в представлении.
Для функции addItem() в Листинге 14-23 мы проверяем количество строк в таблице
с помощью функции rowCount() и с помощью функции insertRow() вставляем пустую
строку в конец представления таблицы. Мы запрашиваем базу данных, чтобы узнать
наибольшее значение id в таблице. Если пользователь не ввел в строку значение id, то
id новой записи будет равен наибольшему значению id плюс единица. Следует также
отметить, что если в новой строке не заполнены все элементы, то при закрытии
приложения новая запись не будет сохранена в модели.
Листинг 14-23. Код для слотов addItem() и deleteItem() в графическом интерфейсе
управления счетами
# account_manager.py
def addItem(self):
"""Добавить новую запись в последнюю строку таблицы."""
last_row = self.model.rowCount()
self.model.insertRow(last_row)

query = QSqlQuery()
query.exec("SELECT MAX (id) FROM accounts")
if query.next():
int(query.value(0))
ГЛАВА 14 ВВЕДЕНИЕ В РАБОТУ С БАЗАМИ ДАННЫХ 381
def deleteItem(self):
"""Удаление всей строки из таблицы."""
current_item = self.table_view.selectedIndexes()
for index in current_item:
self.model.removeRow(index.row())
self.model.select()

Для функции deleteItem() мы получаем индекс текущей выбранной строки и


удаляем ее с помощью функции removeRow(). Затем мы обновляем модель с помощью
select().
Последний слот, который необходимо создать в классе MainWindow, - это
setSortingOrder(). Мы будем использовать текст, передаваемый по сигналу
currentTextChanged, для определения способа сортировки данных. Например, если
пользователь хочет упорядочить элементы по номерам сотрудников, он сначала
выберет в QComboBox опцию Sort by Employee ID. Затем выдается сигнал, и значение
текста сравнивается в различных условиях в методе setSortingOrder(). Затем метод
setSort() используется для упорядочивания поля employee_id по возрастанию.
Листинг 14-24. Код для слота setSortingOrder() в графическом интерфейсе
управления счетами
# account_manager.py
def setSortingOrder(self, text):
"""Сортировка строк в таблице."""
if text == "Sort by ID":
self.model.setSort(self.model.fieldIndex("id"),
Qt.SortOrder.AscendingOrder)
elif text == "Sort by Employee ID":
self.model.setSort(
self.model.fieldIndex("employee_id"),
Qt.SortOrder.AscendingOrder)
elif text == "Sort by First Name":
self.model.setSort(
self.model.fieldIndex("first_name"),
Qt.SortOrder.AscendingOrder)
elif text == "Sort by Last Name":
self.model.setSort(
self.model.fieldIndex("last_name"),
Qt.SortOrder.AscendingOrder)
elif text == "Sort by Department":
self.model.setSort(
self.model.fieldIndex("department"),
Qt.SortOrder.AscendingOrder)
elif text == "Sort by Country":
self.model.setSort(
self.model.fieldIndex("country"),
382 ГЛАВА 14 ВВЕДЕНИЕ В РАБОТУ С БАЗАМИ ДАННЫХ
Qt.SortOrder.AscendingOrder)
self.model.select()
Наконец, вызывается функция select() для обновления данных в модели и
представлении.
На этом этапе следует запустить приложение и протестировать его. Если вы
хотите поработать с кодом, то сначала посмотрите на различные режимы выбора
и поведение выбора. Затем можно вернуться к базе данных SQL и попробовать
реализовать дополнительные поля или создать новые внешние ключи для проверки
реляционных классов.

Резюме
PyQt предоставляет удобные классы для работы со списками, таблицами и
деревьями. QListWidget, QTableWidget и QTreeWidget полезны при необходимости
просмотра данных в общих ситуациях. Хотя они удобны для создания быстрых
интерфейсов для редактирования данных, если в приложении необходимо иметь
более одного виджета для отображения набора данных, то необходимо также создать
процесс согласования наборов данных и виджетов. Лучшим вариантом является
использование архитектуры Model/View PyQt.
Существуют различные форматы для хранения и управления данными. Одним
из примеров является формат CSV, который удобен для чтения, разбора и хранения
небольших наборов данных. Однако для больших баз данных, содержащих
множество таблиц с реляционными характеристиками, более предпочтительным
вариантом управления данными является реляционная система управления базами
данных, использующая язык SQL. Язык SQL позволяет пользователям выбирать
нужную информацию, которая может быть разделена между таблицами, а также
легко вставлять, обновлять и удалять существующие записи.
Для работы с базами данных SQL очень удобны модели Model/View, предостав
ляющие средства, необходимые для подключения к базе данных и просмотра ее
содержимого. Qt предоставляет три модели для работы с базами данных SQL.
Для редактируемой модели данных без поддержки внешних ключей используйте
QSqlTableModel. Если у вас есть таблицы с реляционными свойствами, используйте
QSqlRelationalTableModel. Наконец, модель QSqlQueryModel удобна в тех случаях,
когда требуется только прочитать результаты запроса без их редактирования.
В ходе работы над этой книгой мы рассмотрели несколько приложений, которые
могли бы извлечь большую пользу из управления данными. Графический интерфейс
входа в систему, описанный в Главе 3, мог бы подключаться к базе данных для
получения имен пользователей и паролей. Есть также графический интерфейс заказа
пиццы из Главы 6. Вы могли бы реализовать базу данных для хранения информации
о клиентах, используя реляционную базу данных для добавления новых клиентов,
обновления существующих и предотвращения дублирования данных.
В Главе 15 мы кратко рассмотрим многопоточность в PyQt.

ГЛАВА 14 ВВЕДЕНИЕ В РАБОТУ С БАЗАМИ ДАННЫХ 383


ГЛАВА 15

Управление потоками
Каждый из нас сталкивался с ситуацией, когда выполнение какого-либо процесса,
например копирование файлов между каталогами или запуск нового экземпляра
приложения, приводит к кратковременной задержке программы, а в некоторых
случаях и к ее полному зависанию. В этом случае мы вынуждены либо ждать
завершения текущей задачи, либо удалять Ctrl+Alt+Delete, чтобы освободиться.
При создании графических интерфейсов необходимо знать, как действовать в таких
ситуациях, и предусмотрительно избегать их.
В этой главе мы
• Рассмотрим методы обработки трудоемких процессов в PyQt
• Узнаем, как реализовать многопоточность в графических интерфейсах с
помощью QThread
• Используем виджет QProgressBar для визуального отображения хода
выполнения задачи.
Эта глава преследует две цели: помочь вам разработать более надежные GUI-
приложения, а также рассказать о том, как вы можете справиться с ситуациями,
когда вашим приложениям необходимо запускать длительные процессы. Любое
действие, приводящее к остановке обработки событий, плохо сказывается на работе
пользователя.

Введение в управление потоками


Производительность компьютера можно определить по точности, эффективности
и скорости выполнения программных инструкций. Современные компьютеры могут
использовать преимущества своих многоядерных процессоров для параллельного
выполнения этих инструкций, что повышает производительность компьютерных
приложений, написанных с учетом многоядерной архитектуры.
Идея синхронного выполнения задач, когда за один раз обрабатывается только одна
задача до ее завершения, а затем переходят к следующей, может быть неэффективной,
особенно для больших операций. Необходим способ одновременного выполнения
операций. Именно в этом случае на помощь приходят потоки и процессы.
Потоки и процессы - это не одно и то же. Не углубляясь в технический жаргон,
поп­робуем понять разницу между ними. Процесс - это экземпляр приложения, для
рабо­ты которого требуется память и ресурсы компьютера. Открытие текстового про­
цес­сора на компьютере для написания эссе - это один из процессов. Во время написа­
ния эссе вам необходимо найти информацию в Интернете. Теперь на вашем ком-
384 ГЛАВА 15 УПРАВЛЕНИЕ ПОТОКАМИ
пьютере независимо и параллельно выполняются два отдельных процесса. То, что
происходит в одном процессе, не влияет на другой. Конечно, в веб-браузере откры­то
несколько вкладок, и каждая из них загружает и обновляет информацию; эти вклад-
ки работают бок о бок с веб-браузером. Вот здесь-то и становится важен поток.
Поток необходим для обеспечения параллелизма внутри отдельного процесса.
Когда процесс начинается, он имеет только один поток, но в рамках одного процесса
может быть запущено несколько потоков. Этими потоками, как и процессами,
управляет центральный процессор (CPU). Многопоточность возникает тогда, когда
центральный процессор может одновременно обрабатывать несколько потоков
выполнения в рамках одного процесса. Эти потоки независимы, но при этом
совместно используют ресурсы процесса. Использование многопоточности позволяет
приложениям более оперативно реагировать на ввод данных пользователем, в то
время как другие операции выполняются в фоновом режиме, и более эффективно
использовать ресурсы системы.
В системе с процессором, имеющим только одно ядро, истинный параллелизм
фактически недостижим. В таких случаях процессор делится между процессами
или потоками. Для переключения между потоками используются контекстные
переключатели, которые прерывают текущий поток, сохраняют его состояние, а
затем восстанавливают состояние следующего потока. Это создает у пользователя
ложную видимость параллелизма.
Для достижения истинного параллелизма и создания действительно параллельной
системы многоядерный процессор позволяет назначать потоки в многопоточном
приложении на разные процессоры.

Управление потоками в PyQt


Приложения на базе Qt основаны на событиях. При запуске цикла событий
с помощью функции exec() создается поток. Этот поток называется главным
потоком графического интерфейса. Все события, происходящие в главном потоке,
включая сам графический интерфейс, выполняются синхронно в главном цикле
событий. Чтобы воспользоваться преимуществами многопоточности, необходимо
создать вторичный поток, который будет разгружать основной поток от операций
обработки.
PyQt упрощает взаимодействие между главным и вторичными потоками,
называемыми также рабочими потоками, с помощью сигналов и слотов. Это
может быть полезно для передачи обратной связи, для того чтобы пользователь мог
прервать процесс, а также для того, чтобы сообщить главному потоку о завершении
процесса. Поскольку потоки используют одно и то же адресное пространство, они
могут легко обмениваться данными.
Однако будьте осторожны. Если несколько потоков пытаются одновременно
получить доступ к общим данным или ресурсам, это может привести к сбоям или
повреждению памяти. Еще одна проблема - тупик, который может возникнуть, если
два потока заблокированы из-за ожидания ресурсов. PyQt предоставляет несколько

ГЛАВА 15 УПРАВЛЕНИЕ ПОТОКАМИ 385


классов, например, QMutex, QReadWriteLock и QSemaphore, для предотвращения
подобных проблем.

Примечание. В Python также имеется ряд модулей для обработки потоков


и задач обработки, включая _thread, threading, asyncio и multiprocessing.
Хотя вы можете использовать и эти модули, QThread и другие классы PyQt
позволяют передавать сигналы между основным и рабочим потоками.

Методы обработки длинных событий в PyQt


Хотя в этой главе основное внимание уделено использованию QThread, не стоит
забывать, что существуют и другие способы, которые вы можете попробовать,
прежде чем пытаться использовать потоковую обработку в своем графическом
интерфейсе. Реализация потоков может привести к проблемам с параллелизмом
и выявлением ошибок. В сочетании с сигналами и слотами PyQt предоставляет
несколько различных способов обработки трудоемких операций.
Выбор метода, который лучше всего подходит для вашего приложения, сводится к
рассмотрению конкретной ситуации. Ниже перечислены основные методы, включая
потоковые, для обработки такого рода событий:
1. Если в приложении есть процесс, вызывающий зависание, проверьте,
можно ли разбить этот процесс на более мелкие шаги и выполнять их
последовательно. Вручную обрабатывайте длинные операции и явно
вызывайте QApplication.processEvents() для обработки ожидающих событий.
Этот способ лучше всего подходит, если операции можно обрабатывать в
одном потоке.
2. С помощью QTimer и сигналов и слотов можно запланировать выполнение
операций через определенные промежутки времени в будущем.
3. Используйте QThread для создания рабочего потока, который будет
выполнять длительные операции в отдельном потоке. Создайте класс на
основе QThread, повторно реализуйте run() и используйте сигналы и слоты
для связи с основным потоком. Этот метод поможет избежать блокировки
главного цикла событий.
4. Классы QThreadPool и QRunnable можно использовать для разделения
работы между ядрами процессора на компьютере. Создайте подкласс
QRunnable и переделайте функцию run(); затем экземпляр QRunnable можно
передавать потокам, управляемым QThreadPool. QThreadPool обрабатывает
очередь и выполнение экземпляров QRunnable за вас.
Существуют и другие варианты, которые могут зависеть от требований вашего
приложения. Следует помнить, что использование потоков может принести пользу
вашему приложению, но может и замедлить его работу или привести к ошибкам при
неправильном использовании.
386 ГЛАВА 15 УПРАВЛЕНИЕ ПОТОКАМИ
Проект 15.1 - Графический интерфейс
переименования файлов
При создании и маркировке наборов данных часто приходится писать сценарии
на языке Python для маркировки тысяч изображений и файлов данных. Эти
сценарии, как правило, пишутся с учетом того, что в командной строке пользователю
предоставляется некоторая визуальная обратная связь о том, как идет процесс.
Для графического интерфейса, представленного на рис. 15-1, мы создадим
графический интерфейс, который позволит нам выбрать локальную директорию и
редактировать имена файлов в ней с указанным расширением. Интерфейс включает
виджеты QTextEdit и QProgressBar в качестве двух различных средств обратной связи о
процессе маркировки файлов. В этом приложении также используется класс QThread,
чтобы пользователи могли взаимодействовать с интерфейсом, пока операции
выполняются в фоновом режиме.

Рисунок 15-1. Интерфейс для переименования файлов в выбранном каталоге

Виджет QProgressBar
Виджет QProgressBar визуально передает пользователю информацию о ходе
выполнения расширенной операции. Эта обратная связь может также использоваться
в качестве подтверждения того, что такой процесс, как загрузка, установка или
передача файлов, все еще выполняется. К числу параметров, которыми можно
управлять, относятся ориентация и дальность действия виджета.
Для настройки индикатора выполнения обратитесь к проекту в следующих
разделах.
ГЛАВА 15 УПРАВЛЕНИЕ ПОТОКАМИ 387
Пояснения к графическому интерфейсу
переименования файлов
Окно GUI содержит различные кнопки и виджеты редактора, которые позволяют
пользователю управлять переименованием файлов. Пользователь может выбрать
каталог с помощью кнопки QPushButton и появившегося диалогового окна QFileDialog.
Новое имя файла может быть введено в виджет QLineEdit. С помощью QComboBox
можно также выбрать расширение файлов, которые необходимо изменить.
Приложение использует потоки для обновления индикатора выполнения,
отображения информации об изменяемых файлах в текстовом редакторе и
выполнения самой операции переименования. Для этого используются сигналы и
слоты.
Начнем с использования в качестве шаблона сценария basic_window.py из Главы 1.
Далее импортируем классы Python и PyQt в Листинге 15-1. Класс QThread является
частью QtCore.
Листинг 15-1. Код для импорта и таблицы стилей, используемых в графическом
интерфейсе переименования файлов
# file_rename_threading.py
# Импорт необходимых модулей
import os, sys, time
from PyQt6.QtWidgets import (QApplication, QWidget, QLabel,
QProgressBar, QLineEdit, QPushButton, QTextEdit,
QComboBox, QFileDialog, QGridLayout)
from PyQt6.QtCore import pyqtSignal, QThread

style_sheet = """
QProgressBar{
background-color: #C0C6CA;
color: #FFFFFF;
border: 1px solid grey;
padding: 3px;
height: 15px;
text-align: center;
}

QProgressBar::chunk{
background: #538DB8;
width: 5px;
margin: 0.5px
}
"""

388 ГЛАВА 15 УПРАВЛЕНИЕ ПОТОКАМИ


Таблица стилей используется для изменения внешнего вида QProgressBar. Кроме
изменения внешнего вида прогресс-бара, мы также можем изменить внешний вид
подконтрольного блока, чтобы создать блочный вид баров при их обновлении.
Для этого графического интерфейса создадим класс, наследующий QThread.
Класс Worker в Листинге 15-2 будет использоваться для обновления индикатора
выполнения, обновления виджета редактирования текста и собственно выполнения
задачи переименования файлов изображений, освобождая тем самым главный
цикл обработки событий для выполнения других задач. Экземпляр класса QThread
управляет только одним потоком.
Для обновления прогресс-бара и виджетов редактирования текста создаются три
пользовательских сигнала:
• update_value_signal - Выдает сигнал, который используется для обновления
целочисленного значения индикатора прогресса
• update_text_edit_signal - Используется для обновления содержимого виджета
QTextEdit. Передается строковая информация о старом имени файла и новом
имени файла
• clear_text_edit_signal - Сигнал, используемый для очистки виджета
редактирования текста, если пользователь прекращает запуск рабочего
потока.
Листинг 15-2. Создание класса Worker, который является подклассом QThread
# file_rename_threading.py
# Создать рабочий поток для выполнения таких задач,
# как обновление прогресс-бара, переименование фотографий,
# отображение информации в виджете редактирования текста.
class Worker(QThread):

update_value_signal = pyqtSignal(int)
update_text_edit_signal = pyqtSignal(str, str)
clear_text_edit_signal = pyqtSignal()

def __init__(self, dir, ext, prefix):


super().__init__()
self.dir = dir
self.ext = ext
self.prefix = prefix

def stopRunning(self):
"""Прервать поток."""
self.terminate()
self.wait()
self.update_value_signal.emit(0)
self.clear_text_edit_signal.emit()

ГЛАВА 15 УПРАВЛЕНИЕ ПОТОКАМИ 389


def run(self):
"""Отсюда начинается выполнение потока.
run() вызывается только после start()."""
for (i, file) in enumerate(os.listdir(self.dir)):
_, file_ext = os.path.splitext(file)
if file_ext == self.ext:
new_file_name = self.prefix + str(i) + \
self.ext
src_path = os.path.join(self.dir, file)
dst_path = os.path.join(
self.dir, new_file_name)

# os.rename(src, dst): src - исходный адрес


# переименовываемого файла, dst - место назначения
# с новым именем
os.rename(src_path, dst_path)
# Отмените комментарий, если процесс слишком
# быстрый и вы хотите видеть обновления
# time.sleep(1.0)

self.update_value_signal.emit(i + 1)
self.update_text_edit_signal.emit(
file, new_file_name)
else:
pass
# Сброс значения индикатора выполнения
self.update_value_signal.emit(0)
Реализованный метод QThread, run(), начинает выполнение потока. В run()
выполняются трудоемкие операции - обход каталога, переименование файлов, выдача
сигналов для обновления QProgressBar и QTextEdit. Однако этот метод не вызывается
напрямую. Метод QThread start() используется для связи с рабочим потоком и начала
его выполнения путем вызова run(). Метод start() вызывается из метода renameFiles()
класса MainWindow в Листинге 15-8.
Слот stopRunning() используется для завершения процессов потока, когда
пользователь нажимает кнопку Stop в главном окне. Метод terminate() используется
для завершения потока, а wait() - для того, чтобы убедиться в его завершении,
заблокировав поток.
Листинг 15-3 начинает создание класса MainWindow, который наследует QWidget.
Листинг 15-3. Базовый код класса MainWindow
# file_rename_threading.py
class MainWindow(QWidget):

def __init__(self):

390 ГЛАВА 15 УПРАВЛЕНИЕ ПОТОКАМИ


super().__init__()
self.initializeUI()
def initializeUI(self):
"""Настройка графического интерфейса приложения."""
self.setMinimumSize(600, 250)
self.setWindowTitle("15.1 - Графический интерфейс изменения имен файлов")

self.directory = ""
self.combo_value = ""

self.setUpMainWindow()
self.show()

if __name__ == "__main__":
app = QApplication(sys.argv)
app.setStyleSheet(style_sheet)
window = MainWindow()
sys.exit(app.exec())

Переменная directory используется для хранения значения выбранного каталога, а


combo_value - для значения расширения файла, выбранного в QComboBox.
В Листинге 15-4 функция setUpMainWindow() используется для создания метки,
строки редактирования и кнопки выбора каталога. В различных виджетах этой
программы также используются всплывающие подсказки для предоставления
пользователю дополнительной информации о назначении или функциональности
виджета.
Листинг 15-4. Создание функции setUpMainWindow() для графического
интерфейса переименования файлов, часть 1
# file_rename_threading.py
def setUpMainWindow(self):
"""Создание и расположение виджетов в главном окне."""
dir_label = QLabel(
"""<p>Используйте кнопку для выбора каталога и
Изменения имен файлов:</p>""")

self.dir_edit = QLineEdit()
dir_button = QPushButton("Выберите каталог")
dir_button.setToolTip("Выберите каталог файлов.")
dir_button.clicked.connect(self.chooseDirectory)

Для задания новых имен файлов можно ввести текст в change_name_edit в


Листинге 15-5. QComboBox используется для определения типов файлов, которые
необходимо изменить в выбранном каталоге. В процессе переименования будут
изменены только файлы с выбранным расширением.
ГЛАВА 15 УПРАВЛЕНИЕ ПОТОКАМИ 391
Листинг 15-5. Создание функции setUpMainWindow() для графического
интерфейса переименования файлов, часть 2
# file_rename_threading.py
self.change_name_edit = QLineEdit()
self.change_name_edit.setToolTip(
"""<p>К файлам будут добавлены числовые значения.
Например: filename<b>01</b>.jpg</p>""")
self.change_name_edit.setPlaceholderText(
"Измените имена файлов на…")

file_exts = [".jpg", ".jpeg", ".png", ".gif", ".txt"]


self.combo_value = file_exts[0]

# Создание комбинированного окна для выбора расширений файлов


ext_combo = QComboBox()
ext_combo.setToolTip(
"Будут изменены только файлы с этим расширением.")
ext_combo.addItems(file_exts)
ext_combo.currentTextChanged.connect(
self.updateComboValue)

rename_button = QPushButton("Переименование файлов")


rename_button.setToolTip(
"Начать переименование файлов в каталоге.")
rename_button.clicked.connect(self.renameFiles)

Экземпляр rename_button используется для запуска процесса переименования


файлов. При нажатии на кнопку выдается сигнал, вызывающий слот renameFiles().
Листинг 15-6 завершает настройку главного окна созданием виджетов
редактирования текста и индикатора выполнения, которые обеспечивают обратную
связь с процессом переименования. Кроме того, создается кнопка QPushButton,
которая будет включаться после нажатия кнопки rename_button и во время
переименования файлов.
Листинг 15-6. Создание функции setUpMainWindow() для графического
интерфейса переименования файлов, часть 3
# file_rename_threading.py
# Редактирование текста предназначено для отображения
# имен файлов по мере их обновления
self.display_files_tedit = QTextEdit()
self.display_files_tedit.setReadOnly(True)

self.progress_bar = QProgressBar()
self.progress_bar.setValue(0)

392 ГЛАВА 15 УПРАВЛЕНИЕ ПОТОКАМИ


self.stop_button = QPushButton("Остановить")
self.stop_button.setEnabled(False)

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


grid = QGridLayout()
grid.addWidget(dir_label, 0, 0)
grid.addWidget(self.dir_edit, 1, 0, 1, 2)
grid.addWidget(dir_button, 1, 2)
grid.addWidget(self.change_name_edit, 2, 0)
grid.addWidget(ext_combo, 2, 1)
grid.addWidget(rename_button, 2, 2)
grid.addWidget(self.display_files_tedit, 3, 0, 1, 3)
grid.addWidget(self.progress_bar, 4, 0, 1, 2)
grid.addWidget(self.stop_button, 4, 2)

self.setLayout(grid)
Затем виджеты организуются в QGridLayout.
Слот chooseDirectory() в Листинге 15-7 вызывается при нажатии кнопки dir_button
и открывает QFileDialog для выбора директорий. После выбора каталога пользователь
может ввести новые имена файлов в change_name_edit и выбрать расширение для
типов изменяемых файлов в комбинированном окне.
Листинг 15-7. Создание слота chooseDirectory()
# file_rename_threading.py
def chooseDirectory(self):
"""Выбор каталога файлов."""
file_dialog = QFileDialog(self)
file_dialog.setFileMode(
QFileDialog.FileMode.Directory)
self.directory = file_dialog.getExistingDirectory(
self, "Open Directory", "",
QFileDialog.Option.ShowDirsOnly)
if self.directory:
self.dir_edit.setText(self.directory)

# Установить максимальное значение индикатора


# прогресса, равное максимальному количеству
# файлов в каталоге
num_of_files = len(
[name for name in os.listdir(self.directory)])
self.progress_bar.setRange(0, num_of_files)
Выбор каталогов в этом приложении возможен только с помощью слота
chooseDirectory(). Мы также можем установить максимальный диапазон QProgressBar,
используя общее количество файлов в каталоге.
ГЛАВА 15 УПРАВЛЕНИЕ ПОТОКАМИ 393
Переименование файлов может происходить в главном потоке. Это не будет
проблемой для нескольких файлов. Однако если пользователь захочет работать
с большим количеством файлов, то это приведет к блокировке графического
интерфейса до завершения операций. Поэтому процесс переименования файлов,
а также обновление индикатора выполнения и виджетов редактирования текста
выполняется в рабочем потоке. В Листинге 15-8 создан экземпляр класса Worker.
Листинг 15-8. Код слота renameFiles(), создающего рабочий поток
# file_rename_threading.py
def renameFiles(self):
"""Создание экземпляра рабочего потока для
обработки процесса переименования файлов."""
prefix_text = self.change_name_edit.text()
if self.directory != "" and prefix_text != "":
self.worker = Worker(
self.directory, self.combo_value, prefix_text)
self.worker.clear_text_edit_signal.connect(
self.display_files_tedit.clear)

self.stop_button.setEnabled(True)
self.stop_button.repaint()
self.stop_button.clicked.connect(
self.worker.stopRunning)

self.worker.update_value_signal.connect(
self.updateProgressBar)
self.worker.update_text_edit_signal.connect(
self.updateTextEdit)
self.worker.finished.connect(
self.worker.deleteLater)
self.worker.start()

В Листинге 15-9 вновь созданному рабочему потоку передаются directory,


combo_value и prefix_text. Затем сигнал worker_clear_text_signal подключается к методу
clear() экземпляра display_files_text.
Если в этот момент нажать кнопку stop_button, то будет вызван слот stopRunning()
класса Worker, что приведет к завершению потока и сбросу индикатора выполнения
и редактирования текста. Остальные сигналы Worker также связаны со слотами в
Листинге 15-9.
QThread также имеет сигнал finished, который выдается при завершении чтения.
Сигнал finished связан с методом QObject deleteLater(), который используется для
удаления объекта worker и освобождения объектов, которые были созданы во время
работы потока.

394 ГЛАВА 15 УПРАВЛЕНИЕ ПОТОКАМИ


Листинг 15-9. Код для слотов, обновляющих значения виджетов
# file_rename_threading.py
def updateComboValue(self, text):
"""Изменение значения комбинированного окна.
Значения представляют собой различные расширения файлов."""
self.combo_value = text
print(self.combo_value)

def updateProgressBar(self, value):


self.progress_bar.setValue(value)

def updateTextEdit(self, old_text, new_text):


self.display_files_tedit.append(
f"[INFO] {old_text} changed to {new_text}.")

Слоты updateProgressBar() и updateTextEdit() подключены к сигналам рабочего


потока.
Теперь можно запустить программу и найти локальную папку. Если вы обнаружите,
что процесс идет слишком быстро, и захотите увидеть реально выполняющиеся
процессы, то можете откомментировать time.sleep(1.0) в классе Worker, чтобы
замедлить процесс.

Резюме
Предотвращение зависания графического интерфейса при обработке длительных
операций очень важно для удобства пользователя. Существует несколько вариантов
эффективной обработки блокировки в приложении, включая использование
таймеров и потоков. Qt предоставляет класс QThread, который в сочетании с сигналами
и слотами может быть использован для обработки дополнительных процессов в
GUI-приложениях. Однако при использовании QThread следует быть осторожным,
чтобы гарантировать, что потоки защищают доступ к своим собственным данным.
Хотя в коротком проекте этой главы QThread не показан, он также имеет методы,
такие как started(), finished(), wait() и quit(), для управления потоками.
В Главе 16 мы построим несколько примеров проектов для изучения и отработки
различных концепций, не рассмотренных в предыдущих главах.

ГЛАВА 15 УПРАВЛЕНИЕ ПОТОКАМИ 395


ГЛАВА 16

Дополнительные
проекты
На протяжении всей этой книги мы стремились использовать практический
подход к созданию приложений, чтобы помочь вам освоить основы разработки
графических интерфейсов. По мере дальнейшего использования PyQt6 и Python вы
сможете узнать о других модулях и классах, которые также окажутся полезными.
В некоторых случаях эта книга лишь поверхностно коснулась того, что можно
сделать с помощью PyQt. Благодаря большому количеству модулей, классов и
возможностей настройки, предоставляемых Qt, потенциал создания графических
интерфейсов безграничен. Чтобы расширить ваш опыт работы с PyQt, в этой главе
мы рассмотрим некоторые дополнительные классы Qt, которые не смогли уместить
в предыдущих главах.
В этой главе вы создадите проекты для
1. Отображения каталогов и файлов с помощью QFileSystemModel и QTreeView
2. Создание графического интерфейса, который делает фотографии с помощью
QCamera и демонстрирует, как создавать пользовательские диалоги с помощью
QDialog
3. Создание простого графического интерфейса часов с использованием QDate и
QTime
4. Изучение класса QCalendarWidget
5. Создание "Виселицы" с помощью QPainter и других классов PyQt
6. Построение framework для веб-браузера с помощью модуля QtWebEngineWidgets
7. Создание трехпозиционных виджетов QComboBox
Пояснения к каждому проекту не будут слишком подробными. Скорее, они
сфокусированы на объяснении ключевых моментов каждой программы и оставляют
за вами право изучить детали, в которых вы не уверены, либо найдя ответы в другой
главе, либо обратившись за помощью к Интернету.

396 ГЛАВА 16 ДОПОЛНИТЕЛЬНЫЕ ПРОЕКТЫ


Проект 16.1 - Графический интерфейс просмотра
каталогов
Для каждой операционной системы необходим определенный способ доступа
пользователя к данным и файлам, расположенным в ней. Диски, каталоги и файлы
хранятся в иерархической файловой системе и представляются пользователю таким
образом, чтобы он видел только те файлы, которые его интересуют.
Независимо от того, используете ли вы интерфейс командной строки или
графический интерфейс пользователя, необходимо каким-то образом создавать,
удалять и переименовывать файлы и каталоги. Однако если вы уже взаимодействуете
с одним интерфейсом, то может оказаться удобнее находить нужные файлы
или каталоги в главном окне приложения, а не открывать новые окна или другие
программы.
В данном проекте показано, как настроить интерфейс для просмотра файлов
на локальной системе. В этом проекте будут представлены два ключевых класса:
QFileSystemModel, предоставляющий доступ к файловой системе компьютера,
и QTreeView, обеспечивающий визуальное представление данных с помощью
древовидной структуры. Приложение для просмотра каталогов показано на рис. 16-1.

Рисунок 16-1. Средство просмотра каталогов, отображающее каталоги


локальной системы
ГЛАВА 16 ДОПОЛНИТЕЛЬНЫЕ ПРОЕКТЫ 397
Пояснения к графическому интерфейсу просмотра
каталогов
Начните с использования сценария main_window_template.py из Главы 5, а затем
импортируйте необходимые модули для этого графического интерфейса. В данном
проекте для просмотра данных на компьютере нам потребуется использовать
парадигму Model/View. Более подробную информацию о программировании в
парадигме Model/View см. в Главе 10. Код программы просмотра каталога приведен
в Листинге 16-1.
Листинг 16-1. Код графического интерфейса программы просмотра каталогов
# directory_viewer.py
# Импорт необходимых модулей
import sys
from PyQt6.QtWidgets import (QApplication, QMainWindow,
QTreeView, QFrame, QFileDialog, QVBoxLayout)
from PyQt6.QtGui import QFileSystemModel, QAction

class MainWindow(QMainWindow):

def __init__(self):
super().__init__()
self.initializeUI()

def initializeUI(self):
"""Настройка графического интерфейса приложения."""
self.setMinimumSize(500, 400)
self.setWindowTitle("16.1 – Графический интерфейс просмотра каталога")

self.setUpMainWindow()
self.createActions()
self.createMenu()
self.show()

def setUpMainWindow(self):
"""Настройте QTreeView в главном окне для отображения
содержимого локальной файловой системы."""
self.model = QFileSystemModel()
self.model.setRootPath("")

self.tree = QTreeView()
self.tree.setIndentation(10)
self.tree.setModel(self.model)

398 ГЛАВА 16 ДОПОЛНИТЕЛЬНЫЕ ПРОЕКТЫ


# Установка контейнера и компоновка
frame = QFrame()
frame_v_box = QVBoxLayout()
frame_v_box.addWidget(self.tree)
frame.setLayout(frame_v_box)

self.setCentralWidget(frame)

def createActions(self):
"""Создание действий меню приложения."""
# Создание действий для меню Каталоги
self.open_dir_act = QAction("Открыть каталог...")
self.open_dir_act.triggered.connect(self.chooseDirectory)

self.root_act = QAction("Вернуться в корневой каталог")


self.root_act.triggered.connect(self.returnToRootDirectory)

def createMenu(self):
"""Создать панель меню приложения."""
self.menuBar().setNativeMenuBar(False)

# Создание файлового меню и добавление действий


dir_menu = self.menuBar().addMenu("Каталоги")
dir_menu.addAction(self.open_dir_act)
dir_menu.addAction(self.root_act)

def chooseDirectory(self):
"""Слот выбора каталога для отображения."""
file_dialog = QFileDialog(self)
file_dialog.setFileMode(QFileDialog.FileMode.Directory)
directory = file_dialog.getExistingDirectory(self, "Открыть каталог",
"", QFileDialog.Option.ShowDirsOnly)

self.tree.setRootIndex(self.model.index(directory))

def returnToRootDirectory(self):
"""Слот для повторного отображения содержимого корневого каталога."""
self.tree.setRootIndex(self.model.index(""))

if __name__ == '__main__':
app = QApplication(sys.argv)
window = MainWindow()
sys.exit(app.exec())

ГЛАВА 16 ДОПОЛНИТЕЛЬНЫЕ ПРОЕКТЫ 399


Класс QFileSystemModel предоставляет модель, необходимую для доступа к данным
в локальной файловой системе. Для PyQt6 этот класс теперь находится в QtGui. Хотя
он не включен в данный проект, вы также можете использовать QFileSystemModel для
переименования или удаления файлов и каталогов, создания новых каталогов или
использовать его с другими виджетами отображения как часть браузера.
Класс QTreeView будет использоваться для отображения содержимого модели в
виде иерархического древовидного представления.
Для этого графического интерфейса мы создадим меню Каталоги с действиями,
которые позволят пользователю либо просмотреть определенный каталог, либо
вернуться в корневой каталог. Снимок строки меню приведен на рис. 16-2.

Рисунок 16-2. Меню графического интерфейса просмотра каталога


Создаём экземпляр класса QFileSystemModel, model, и устанавливаем каталог
в качестве корневого пути, передав в setRootPath() пустую строку. Можно задать
другой каталог, передав в setRootPath() другой путь.
Наконец, зададим модель объекта tree для отображения содержимого файловой
системы с помощью функции setModel(). Чтобы выбрать другой каталог, пользователь
может выбрать в меню пункт Open Directory..., после чего появится диалог выбора
файла. Новый каталог может быть выбран в слоте chooseDirectory() и установлен
в качестве нового корневого пути для отображения в объекте дерева с помощью
метода QTreeView setRootIndex().
Если был выбран новый каталог, то для повторного отображения корневого
каталога можно воспользоваться слотом returnToRootDirectory(), который вызывается
функцией root_act.

Проект 16.2 - Графический интерфейс камеры


PyQt умеет работать не только с изображениями, в нем есть модули для работы с
видео, аудио и другими видами мультимедиа. Для данного графического интерфейса
мы создадим простое окно, которое открывает веб-камеру компьютера и отображает
её содержимое в окне. При нажатии на клавишу пробела появляется пользовательский
QDialog, отображающий снимок экрана и предлагающий сохранить или отклонить
видео. Главное окно показано на рис. 16-3.

400 ГЛАВА 16 ДОПОЛНИТЕЛЬНЫЕ ПРОЕКТЫ


Рисунок 16-3. Графический интерфейс камеры
Перед началом работы над этим проектом убедитесь, что ваша версия PyQt6 -
это версия 6.2 или выше. В версии 6.1 и более ранних классы мультимедиа не были
включены. Чтобы проверить версию PyQt6, откройте оболочку Python и введите
следующие команды:
>>> import PyQt6
>>> from PyQt6.QtCore import PYQT_VERSION_STR
>>> print(PYQT_VERSION_STR)
6.2.1
>>> help(PyQt6)
В оболочке должно появиться сообщение, по крайней мере, о версии 6.2.1. Если
этого не произошло, можно обновить версию PyQt6, выполнив следующую команду:
$ pip3 install PyQt6 --upgrade
В Windows вместо pip3 можно использовать pip. Вы можете воспользоваться
функцией Python help(), чтобы получить список всех модулей PyQt6. Просмотрите
его, и вы увидите QtMultimedia в списке различных модулей.
Другой способ убедиться в том, что классы мультимедиа были установлены, -
открыть оболочку Python и выполнить следующий код:
>>> from PyQt6 import QtMultimedia

ГЛАВА 16 ДОПОЛНИТЕЛЬНЫЕ ПРОЕКТЫ 401


Примечание. Для читателей, использующих macOS, могут возникнуть
проблемы с запуском этого приложения, если вы используете оболочку
Z, также известную как zsh. До недавнего времени в macOS по умолчанию
использовалась оболочка bash. Если у вас возникли проблемы из-за
использования zsh, вы можете переключиться на использование bash, введя
в командной строке команду chsh -s /bin/bash/. Если по окончании работы вы
захотите вернуться к использованию zsh, то вам потребуется ввести команду
chsh -s /bin/zsh. Следует помнить, что при переключении между оболочками
необходимо будет также установить PyQt6 из PyPI или изменить пути в bash
для размещения PyQt6 и других пакетов Python.

Пояснения к графическому интерфейсу камеры


Давайте рассмотрим, как использовать мультимедийные классы для создания
графического интерфейса камеры для фотографирования в Листинге 16-2. Для
начала мы воспользуемся файлом basic_window.py из Главы 1.
Для создания пользовательского диалога, в котором будут отображаться
изображения, полученные с помощью веб-камеры, нам понадобятся QDialog и
QDialogButtonBox из QtWidgets. Виджет QDialogButtonBox используется для удобного
создания и расположения стандартных кнопок в диалоговых окнах. Более подробную
информацию о типах кнопок можно найти в Приложении в подразделе "QDialog".
Многочисленные обновления коснулись мультимедийных классов в PyQt6. Модуль
QtMultimedia предоставляет доступ к ряду мультимедийных инструментов, которые
могут работать с аудио, видео и камерами. Класс QCamera предоставляет интерфейс
для работы с устройствами камеры. Мы можем использовать QImageCapture для записи
или съемки мультимедийных объектов, таких как QCamera. Класс QMediaDevices
предоставляет информацию о доступных камерах или аудиоустройствах.
Из модуля QtMultimediaWidgets класс QVideoWidget настраивает и отображает
вывод камеры или видеообъекта.
Листинг 16-2. Пример кода, показывающий, как использовать класс QCamera и
строить пользовательские диалоговые окна
# camera.py
# Импорт необходимых модулей
import os, sys
from PyQt6.QtWidgets import (QApplication, QWidget, QLabel,
QDialog, QDialogButtonBox, QVBoxLayout)
from PyQt6.QtCore import Qt, QDate
from PyQt6.QtGui import QPixmap
from PyQt6.QtMultimedia import (QCamera, QImageCapture,

402 ГЛАВА 16 ДОПОЛНИТЕЛЬНЫЕ ПРОЕКТЫ


QMediaDevices, QMediaCaptureSession)
from PyQt6.QtMultimediaWidgets import QVideoWidget

class ImageDialog(QDialog):

def __init__(self, id, image):


"""Пользовательский QDialog, отображающий сделанное изображение."""
super().__init__()
self.id = id
self.setWindowTitle(f"Image #{id}")
self.setMinimumSize(400, 300)

self.pixmap = QPixmap().fromImage(image)
image_label = QLabel()
image_label.setPixmap(self.pixmap)

# Создание кнопок, появляющихся в диалоге


self.button_box = QDialogButtonBox(
QDialogButtonBox.StandardButton.Save | \
QDialogButtonBox.StandardButton.Cancel)
self.button_box.accepted.connect(self.accept)
self.button_box.rejected.connect(self.reject)

dialog_v_box = QVBoxLayout()
dialog_v_box.addWidget(image_label)
dialog_v_box.addWidget(self.button_box)
self.setLayout(dialog_v_box)

def accept(self):
"""Реализация метода accept() для сохранения файла
изображения в каталоге images."""
file_format = "png"
today = QDate().currentDate().toString(Qt.DateFormat.ISODate)

file_name = f"images/image{self.id}_{today}.png"
self.pixmap.save(file_name, file_format)
super().accept()

class MainWindow(QWidget):

def __init__(self):
super().__init__()
self.initializeUI()

def initializeUI(self):
ГЛАВА 16 ДОПОЛНИТЕЛЬНЫЕ ПРОЕКТЫ 403
"""Настройка графического интерфейса приложения."""
self.setMinimumSize(500, 400)
self.setWindowTitle("16.2 - Графический интерфейс камеры")

self.setUpMainWindow()
self.show()

def setUpMainWindow(self):
"""Создание и расположение виджетов в главном окне."""
# Создание каталога вывода изображения
exists = os.path.exists("images")
if not exists:
os.makedirs("images")

info_label = QLabel("Нажмите 'Пробел', чтобы сделать снимок.")


info_label.setAlignment(Qt.AlignmentFlag.AlignCenter)

# Создание камеры, использующей камеру компьютера по умолчанию


self.camera = QCamera(QMediaDevices.defaultVideoInput())

# Создать экземпляр класса, используемого для захвата изображений


self.image_capture = QImageCapture(self.camera)
self.image_capture.imageCaptured.connect(self.viewImage)

video_widget = QVideoWidget(self)

# QMediaCaptureSession обрабатывает воспроизведение


# и захват видео и аудио
self.media_capture_session = QMediaCaptureSession()
self.media_capture_session.setCamera(self.camera)
self.media_capture_session.setImageCapture(self.image_capture)
self.media_capture_session.setVideoOutput(video_widget)

self.camera.start()

main_v_box = QVBoxLayout()
main_v_box.addWidget(info_label)
main_v_box.addWidget(video_widget, 1)
self.setLayout(main_v_box)

def viewImage(self, id, preview):


"""Открыть диалоговое окно для предварительного
просмотра изображения."""
self.image_dialog = ImageDialog(id, preview)
self.image_dialog.open()
404 ГЛАВА 16 ДОПОЛНИТЕЛЬНЫЕ ПРОЕКТЫ
def keyPressEvent(self, event):
"""Реализация захвата изображения при нажатии клавиши пробела."""
if event.key() == Qt.Key.Key_Space:
self.image_capture.capture()

def closeEvent(self, event):


if self.camera.isActive():
self.camera.stop()
event.accept()

if __name__ == '__main__':
app = QApplication(sys.argv)
window = MainWindow()
sys.exit(app.exec())

Для создания настраиваемого диалога необходимо создать новый класс,


наследующий QDialog. Экземпляр ImageDialog будет отображать изображение,
полученное с камеры, на QLabel. Аргумент id указывает на значение id, возвращаемое
QImageCapture при получении снимка. Поскольку QImageCapture также возвращает
объект QImage, то перед установкой изображения на метку нам потребуется изменить
его на пиксмап с помощью метода QPixmap fromImage().
Экземпляр button_box представляет собой QDialogButtonBox, содержащий кнопки
Save и Cancel. Две кнопки добавляются в button_box и разделяются клавишей pipe, |.
Когда кнопка нажимается, она издает сигнал. Как правило, при работе с диалоговыми
кнопками эти сигналы принимаются или отклоняются. Мы прикрепляем эти
сигналы к встроенным слотам accept() и reject(). Хотя это стандартные слоты, можно
подключить принятый или отклоненный сигнал к собственным слотам и выполнить
другие операции. Мы сделаем именно это для accept(), а для reject() воспользуемся
стандартной функциональностью.
Для данного примера мы переделаем функцию accept() для сохранения пиксмапа в
папку images. Расширение файла .png все же необходимо включить, чтобы избежать
проблем с сохранением изображений (особенно в Windows).
Пример ImageDialog с его кнопками показан на рис. 16-4.

ГЛАВА 16 ДОПОЛНИТЕЛЬНЫЕ ПРОЕКТЫ 405


Рисунок 16-4. Пользовательский экземпляр QDialog, отображающий изображение,
сделанное камерой
Последний шаг - расположение виджетов в макете, как это обычно делается с
другими окнами.
Переходя к функции setUpMainWindow() в классе MainWindow, сначала создадим
каталог images для сохранения изображений, если он еще не существует. Окно состоит
из метки для предоставления инструкций и объекта QVideoWidget для отображения
содержимого камеры.
QMediaDevices может использоваться для указания используемой камеры или
предоставления пользователю списка возможных устройств. Для камер нам
потребуется использовать QCameraDevice для обнаружения доступных камер.
Метод QMediaDevices defaultVideoInput() определяет местоположение устройства
QCameraDevice, используемого по умолчанию на компьютере. Затем это устройство
можно передать в QCamera при создании экземпляра камеры.
С помощью QImageCapture пользователь может делать снимки. Для определения
момента съемки используется сигнал imageCaptured. Захват изображения для
данного графического интерфейса обрабатывается функцией keyPressEvent().
При нажатии клавиши пробела QImageCapture.capture() делает снимок, выдавая
тем самым сигнал imageCaptured. После этого вызывается функция ViewImage(), в
которой идентификатор изображения и объект QImage - предварительный просмотр
- передаются экземпляру ImageDialog. Для открытия диалога используется метод
open(). Если пользователь нажимает кнопку Save, то изображение преобразуется в
pixmap и сохраняется в папке images (обрабатывается в слоте accept() ImageDialog).
Вернувшись в setUpMainWindow(), QMediaCaptureSession будет управлять захватом
изображения с камеры, которое отображается в video_widget.

406 ГЛАВА 16 ДОПОЛНИТЕЛЬНЫЕ ПРОЕКТЫ


Проект 16.3 - Простой графический интерфейс часов
PyQt6 также предоставляет классы для работы с датами, QDate, и временем,
QTime. Класс QDateTime предоставляет функции для работы как с датами, так и со
временем. Все три класса содержат методы для работы с функциями, связанными со
временем.
Рассмотрим кратко класс QDateTime. Следующий фрагмент кода создает
экземпляр QDateTime, который выводит текущую дату и время с помощью метода
currentDateTime():
now = QDateTime.currentDateTime()
print(now.toString("MMMM dd, yyyy hh:mm:ss AP"))
Текущая дата и время выводятся на экран в следующем формате:
November 07, 2021 03:34:11 PM
Этот формат используется для отображения времени в графическом интерфейсе
на рис. 16-5.

Рисунок 16-5. Графический интерфейс часов, отображающий текущую дату


календаря и время часов
В PyQt6 также можно использовать перечисление Qt.DateFormat для использования
стандартных типов формата даты и времени. К ним относятся формат ISO 8601 (с
использованием флага ISODate) и RFC 2822 (с использованием флага RFC2822Date).
Метод toString() возвращает дату и время в виде строки. QDateTime также обрабатывает
переход на летнее время, различные часовые пояса, а также манипулирует временем
и датой, например, добавляет или вычитает месяцы, дни или часы.
Если вам нужно работать только с отдельными датами и временем, то QDate и
QTime также предоставляют аналогичные функции, как вы увидите в следующем
примере.

Пояснения к графическому интерфейсу часов


В качестве основы для этой программы мы будем использовать файл basic_window.py
из Главы 1. Начнем с импорта необходимых модулей, включая QDate, QTime и QTimer
из модуля QtCore в Листинге 16-3.

ГЛАВА 16 ДОПОЛНИТЕЛЬНЫЕ ПРОЕКТЫ 407


Класс QTimer будет использоваться для создания объекта таймера, который будет
отслеживать прошедшее время и соответствующим образом обновлять метки,
содержащие дату и время. Таймер устанавливается в initializeUI(), а его сигнал
таймаута подключается к слоту updateDateTime(). Сигнал таймаута выдается каждую
секунду.
Листинг 16-3. Код для графического интерфейса часов
# clock.py
# Импорт необходимых модулей
import sys
from PyQt6.QtWidgets import (QApplication, QWidget, QLabel,
QVBoxLayout)
from PyQt6.QtCore import Qt, QDate, QTime, QTimer

class DisplayTime(QWidget):

def __init__(self):
super().__init__()
self.initializeUI()

def initializeUI(self):
"""Настройка графического интерфейса приложения."""
self.setGeometry(100, 100, 250, 100)
self.setWindowTitle("16.3 – Пример QDateTime")
self.setStyleSheet("background-color: black")

self.setUpMainWindow()

# Создание объекта таймера


timer = QTimer(self)
timer.timeout.connect(self.updateDateTime)
timer.start(1000)

self.show()

def setUpMainWindow(self):
"""Создание меток, отображающих текущую дату и время в главном
окне программы."""
current_date, current_time = self.getDateTime()

self.date_label = QLabel(current_date)
self.date_label.setStyleSheet("color: white; font: 16px Courier")
self.time_label = QLabel(current_time)
self.time_label.setStyleSheet("""color: white;
border-color: white;
408 ГЛАВА 16 ДОПОЛНИТЕЛЬНЫЕ ПРОЕКТЫ
border-width: 2px;
border-style: solid;
border-radius: 4px;
padding: 10px;
font: bold 24px Courier""")

# Создание макета и добавление виджетов


v_box = QVBoxLayout()
v_box.addWidget(self.date_label, alignment=Qt.AlignmentFlag.AlignCenter)
v_box.addWidget(self.time_label, alignment=Qt.AlignmentFlag.AlignCenter)

self.setLayout(v_box)

def getDateTime(self):
"""Возвращает текущую дату и время."""
date = QDate.currentDate().toString("MMMM dd, yyyy")
time = QTime.currentTime().toString("hh:mm:ss AP")
return date, time

def updateDateTime(self):
"""Слот, обновляющий значения даты и времени."""
date = QDate.currentDate().toString("MMMM dd, yyyy")
time = QTime.currentTime().toString("hh:mm:ss AP")

self.date_label.setText(date)
self.time_label.setText(time)
return date, time

if __name__ == '__main__':
app = QApplication(sys.argv)
window = DisplayTime()
sys.exit(app.exec())

Для получения текущей даты и времени значения извлекаются с помощью


методов currentDate() и currentTime() в методе getDateTime(). Затем они возвращаются
и устанавливаются как current_date и current_time.
Значения date и time задаются с помощью последовательности символов для
создания строки формата. Для даты мы представим полное название месяца
(MMMM), день (dd) и полный год (yyyy). Экземпляр времени будет отображать часы
(hh), минуты (mm), секунды (ss), а также время AM или PM (AP).
Метки, отображающие дату и время, инстанцируются, стилизуются и добавляются
в макет в функции setUpMainWindow(). Значения меток обновляются с помощью
слота updateDateTime(), подключенного к timer.

ГЛАВА 16 ДОПОЛНИТЕЛЬНЫЕ ПРОЕКТЫ 409


Проект 16.4 - Графический интерфейс календаря
В этом проекте рассматривается настройка класса QCalendarWidget и
использование некоторых его функций. PyQt позволяет без особых усилий добавить
в приложение календарь на месяц.
Календарь показан на рис. 16-6.

Рисунок 16-6. Графический интерфейс календаря, отображающий календарь,


текущую дату и виджеты, позволяющие пользователю искать даты в заданном
диапазоне времени
Класс QCalendarWidget предоставляет календарь, в который уже встроен ряд других
полезных виджетов и функций. Например, календарь уже содержит горизонтальный
заголовок, включающий виджеты для изменения месяца и года, и вертикальный
заголовок, отображающий номер недели. В класс также встроены сигналы, которые
выдаются при изменении даты, месяца и года в календаре. Внешний вид календаря
будет зависеть от платформы, на которой запущено приложение.
Виджет QDateEdit используется в данном приложении для ограничения диапазона
дат, который может выбрать пользователь, задавая минимальное и максимальное
значения.

Пояснения к графическому интерфейсу календаря


Начнем со сценария basic_window.py из Главы 1. После импорта модулей,
необходимых для графического интерфейса календаря в Листинге 16-4, с помощью
style_sheet подготавливаются стили для виджетов QLabel и QGroupBox.

410 ГЛАВА 16 ДОПОЛНИТЕЛЬНЫЕ ПРОЕКТЫ


Листинг 16-4. Код графического интерфейса календаря
# calendar.py
# Импорт необходимых модулей
import sys
from PyQt6.QtWidgets import (QApplication, QWidget, QLabel,
QCalendarWidget, QDateEdit, QGroupBox, QHBoxLayout,
QGridLayout)
from PyQt6.QtCore import Qt, QDate

style_sheet = """
QLabel{
padding: 5px;
font: 18px
}

QLabel#DateSelected{
font: 24px
}

QGroupBox{
border: 2px solid gray;
border-radius: 5px;
margin-top: 1ex;
font: 14px
}
"""

class MainWindow(QWidget):

def __init__(self):
super().__init__()
self.initializeUI()

def initializeUI(self):
"""Настройка графического интерфейса приложения."""
self.setMinimumSize(500, 400)
self.setWindowTitle("16.4 – Графический интерфейс календаря")

self.setUpMainWindow()
self.show()

def setUpMainWindow(self):
"""Создание и расположение виджетов в главном окне."""
self.calendar = QCalendarWidget()

ГЛАВА 16 ДОПОЛНИТЕЛЬНЫЕ ПРОЕКТЫ 411


self.calendar.setGridVisible(True)
self.calendar.setMinimumDate(QDate(1900, 1, 1))
self.calendar.setMaximumDate(QDate(2200, 1, 1))

# Подключение к слоту newDateSelection() при изменении текущей выбранной даты


self.calendar.selectionChanged.connect(self.newDateSelection)

current = QDate.currentDate().toString("MMMM dd, yyyy")


self.current_label = QLabel(current)
self.current_label.setObjectName("DateSelected")

# Создание виджетов текущего, минимального и максимального значений QDateEdit


min_date_label = QLabel("Минимальная дата:")
self.min_date_edit = QDateEdit()
self.min_date_edit.setDisplayFormat("MMM d yyyy")
self.min_date_edit.setDateRange(self.calendar.minimumDate(), self.calendar.maximum-
Date())
self.min_date_edit.setDate(self.calendar.minimumDate())
self.min_date_edit.dateChanged.connect(self.minDatedChanged)

current_date_label = QLabel("Текущая дата:")


self.current_date_edit = QDateEdit()
self.current_date_edit.setDisplayFormat("MMM d yyyy")
self.current_date_edit.setDate(self.calendar.selectedDate())
self.current_date_edit.setDateRange(self.calendar.minimumDate(), self.calendar.maxi-
mumDate())
self.current_date_edit.dateChanged.connect(self.selectionDateChanged)

max_date_label = QLabel("Максимальная дата:")


self.max_date_edit = QDateEdit()
self.max_date_edit.setDisplayFormat("MMM d yyyy")
self.max_date_edit.setDateRange(self.calendar.minimumDate(), self.calendar.maximum-
Date())
self.max_date_edit.setDate(self.calendar.maximumDate())
self.max_date_edit.dateChanged.connect(self.maxDatedChanged)

# Добавление виджетов в групповой блок и добавление в макет сетки


dates_gb = QGroupBox("Установленные даты")
dates_grid = QGridLayout()
dates_grid.addWidget(self.current_label, 0, 0, 1, 2, Qt.AlignmentFlag.AlignAbsolute)
dates_grid.addWidget(min_date_label, 1, 0)
dates_grid.addWidget(self.min_date_edit, 1, 1)
dates_grid.addWidget(current_date_label, 2, 0)
dates_grid.addWidget(self.current_date_edit, 2, 1)
dates_grid.addWidget(max_date_label, 3, 0)
412 ГЛАВА 16 ДОПОЛНИТЕЛЬНЫЕ ПРОЕКТЫ
dates_grid.addWidget(self.max_date_edit, 3, 1)
dates_gb.setLayout(dates_grid)

# Создание и установка макета главного окна


main_h_box = QHBoxLayout()
main_h_box.addWidget(self.calendar)
main_h_box.addWidget(dates_gb)
self.setLayout(main_h_box)

def selectionDateChanged(self, date):


"""Обновление current_date_edit при изменении выбранной даты календаря. """
self.calendar.setSelectedDate(date)

def minDatedChanged(self, date):


"""Обновление минимальной даты календаря. Обновите max_date_edit,
чтобы избежать конфликтов с максимальной и минимальной датами."""
self.calendar.setMinimumDate(date)
self.max_date_edit.setDate(self.calendar.maximumDate())

def maxDatedChanged(self, date):


"""Обновление максимальной даты календаря. Обновите min_date_edit,
чтобы избежать конфликтов с минимальной и максимальной датами."""
self.calendar.setMaximumDate(date)
self.min_date_edit.setDate(self.calendar.minimumDate())

def newDateSelection(self):
"""Обновление даты в виджетах current_label и current_date_edit
при выборе новой даты."""
date = self.calendar.selectedDate().toString("MMMM dd, yyyy")
self.current_date_edit.setDate(self.calendar.selectedDate())
self.current_label.setText(date)

if __name__ == '__main__':
app = QApplication(sys.argv)
app.setStyleSheet(style_sheet)
window = MainWindow()
sys.exit(app.exec())

Создать экземпляр QCalendarWidget очень просто.


self.calendar = QCalendarWidget()

ГЛАВА 16 ДОПОЛНИТЕЛЬНЫЕ ПРОЕКТЫ 413


Далее мы зададим несколько параметров объекта календаря. Установив значение
параметра setGridVisible() в True, мы сделаем линии сетки видимыми. Для того
чтобы задать диапазон дат, которые пользователь может выбрать в календаре, мы
устанавливаем минимальное и максимальное значения дат с помощью методов
QCalendar setMinimumDate() и setMaximumDate().
При выборе даты в виджете календаря выдается сигнал selectionChanged. Этот
сигнал подключается к слоту newDateSelection(), который обновляет дату на метке
current_label и в current_date_edit. При выборе значения в виджете current_date_edit
изменяются и другие значения.
Класс QCalendarWidget также имеет ряд функций, позволяющих настраивать его
поведение и внешний вид. Для данного проекта мы создадим три виджета QDateEdit,
которые позволят пользователю изменять минимальное и максимальное значения
диапазона дат, а также текущую дату, выбранную в календаре. Эти виджеты можно
увидеть в правой части графического интерфейса на рис. 16-6.
Формат отображения даты в виджете QDateEdit может быть задан с помощью
функции setDisplayFormat(). Объектам редактирования даты также задается диапазон
дат с помощью функции setDateRange(). В следующей строке кода приведен пример
установки диапазона дат виджета min_date_edit с использованием диапазонов,
установленных ранее для объекта календаря:
self.min_date_edit.setDateRange(
self.calendar.minimumDate(),
self.calendar.maximumDate())
При изменении даты в виджете редактирования даты генерируется сигнал
dateChanged. Каждый из виджетов QDateEdit подключен к соответствующему слоту,
который будет обновлять минимальное, максимальное или текущее значение
даты календаря в зависимости от того, в каком виджете редактирования даты
происходит изменение. Метод изменения дат адаптирован с сайта документа Qt
(https://doc.qt.io/qt-6/qtwidgets-widgets-calendarwidget-example.html).
Наконец, виджеты этикеток и редактирования даты размещаются в QGroupBox,
добавляются к экземпляру QGridLayout и вставляются в макет главного окна в
функции setUpMainWindow().

Проект 16.5 - Графический интерфейс "Виселица"


PyQt может быть использован для создания множества различных типов
приложений. На протяжении всей этой книги мы рассмотрели довольно много идей
по созданию графических интерфейсов. В следующем проекте мы рассмотрим, как
использовать QPainter и несколько других классов для создания игры "Виселица".
Несмотря на то, что "Виселица" - простая игра, ее можно использовать для обучения
нескольким фундаментальным концепциям использования PyQt для создания игр.
Интерфейс "Виселица" показан на рис. 16-7.

414 ГЛАВА 16 ДОПОЛНИТЕЛЬНЫЕ ПРОЕКТЫ


Рисунок 16-7. Приложение "Виселица". Сможете ли вы спасти его?
В этом приложении игрок может выбрать одну из 26 английских букв, чтобы угадать
букву в неизвестном слове. По мере выбора каждой буквы она будет отключаться в
окне. Если буква правильная, то она будет открыта игроку. В противном случае на
экране рисуется часть тела фигуры Палача. Если все буквы угаданы правильно, то
игрок выигрывает. Всего в игре шесть ходов. Независимо от того, выиграл игрок или
проиграл, на экран выводится диалог, информирующий игрока и позволяющий ему
выйти из игры или продолжить ее.
Перед началом работы над проектом обязательно загрузите файл words.txt из
папки files в репозитории GitHub.

Пояснения к графическому интерфейсу "Виселица"


В графическом интерфейсе "Виселица" в Листинге 16-5 используется множество
классов, включая различные виджеты из QtWidgets, а также классы для рисования
из QtCore и QtGui. Таблица стилей используется для изменения стилевых свойств
виджетов и для обработки внешнего вида кнопок при их нажатии.
Листинг 16-5. Код для графического интерфейса "Виселица"
# hangman.py
# Импорт необходимых модулей
import sys, random
from PyQt6.QtWidgets import (QApplication, QMainWindow, QWidget, QPushButton,
QLabel, QFrame, QButtonGroup, QHBoxLayout, QVBoxLayout,
QMessageBox, QSizePolicy)
from PyQt6.QtCore import Qt, QRect, QLine
from PyQt6.QtGui import QPainter, QPen, QBrush, QColor

style_sheet = """

ГЛАВА 16 ДОПОЛНИТЕЛЬНЫЕ ПРОЕКТЫ 415


QWidget{
background-color: #FFFFFF
}

QLabel#Word{
font: bold 20px;
qproperty-alignment: AlignCenter
}

QPushButton#Letters{
background-color: #1FAEDE;
color: #D2DDE1;
border-style: solid;
border-radius: 3px;
border-color: #38454A;
font: 28px
}

QPushButton#Letters:pressed{
background-color: #C86354;
border-radius: 4px;
padding: 6px;
color: #DFD8D7
}

QPushButton#Letters:disabled{
background-color: #BBC7CB
}
"""

class DrawingLabel(QLabel):

def __init__(self):
"""Палач рисуется не в главном окне, а на объекте QLabel.
Этот класс обрабатывает рисование."""
super().__init__()
self.height = 200
self.width = 300

self.incorrect_letter = False
self.incorrect_turns = 0

self.wrong_parts_list = []

def drawHangmanBackground(self, painter):


416 ГЛАВА 16 ДОПОЛНИТЕЛЬНЫЕ ПРОЕКТЫ
"""Нарисовать виселицу для графического интерфейса пользователя."""
painter.setBrush(QBrush(QColor("#000000")))
# drawRect(x, y, width, height)
painter.drawRect(int(self.width / 2) - 40, self.height, 150, 4)
painter.drawRect(int(self.width / 2), 0, 4, 200)
painter.drawRect(int(self.width / 2), 0, 60, 4)
painter.drawRect(int(self.width / 2) + 60, 0, 4, 40)

def drawHangmanBody(self, painter):


"""Создание и рисование частей тела для палача."""
if "head" in self.wrong_parts_list:
head = QRect(int(self.width / 2) + 42, 40, 40, 40)
painter.setPen(QPen(QColor("#000000"), 3))
painter.setBrush(QBrush(QColor("#FFFFFF")))
painter.drawEllipse(head)
if "body" in self.wrong_parts_list:
body = QRect(int(self.width / 2) + 60, 80, 2, 55)
painter.setBrush(QBrush(QColor("#000000")))
painter.drawRect(body)
if "right_arm" in self.wrong_parts_list:
right_arm = QLine(int(self.width / 2) + 60, 85,
int(self.width / 2) + 50, int(self.height / 2) + 30)
pen = QPen(Qt.GlobalColor.black, 3, Qt.PenStyle.SolidLine)
painter.setPen(pen)
painter.drawLine(right_arm)
if "left_arm" in self.wrong_parts_list:
left_arm = QLine(int(self.width / 2) + 62, 85,
int(self.width / 2) + 72, int(self.height / 2) + 30)
painter.drawLine(left_arm)
if "right_leg" in self.wrong_parts_list:
right_leg = QLine(int(self.width / 2) + 60, 135,
int(self.width / 2) + 50, int(self.height / 2) + 75)
painter.drawLine(right_leg)
if "left_leg" in self.wrong_parts_list:
left_leg = QLine(int(self.width / 2) + 62, 135,
int(self.width / 2) + 72, int(self.height / 2) + 75)
painter.drawLine(left_leg)

# Сброс переменной
self.incorrect_letter = False

def paintEvent(self, event):


"""Создание QPainter и обработка событий рисования."""
painter = QPainter()
painter.begin(self)
ГЛАВА 16 ДОПОЛНИТЕЛЬНЫЕ ПРОЕКТЫ 417
self.drawHangmanBackground(painter)
if self.incorrect_letter == True:
self.drawHangmanBody(painter)

painter.end()

class Hangman(QMainWindow):

def __init__(self):
super().__init__()
self.initializeUI()

def initializeUI(self):
"""Настройка графического интерфейса приложения."""
self.setFixedSize(400, 500)
self.setWindowTitle("16.5 - Графический интерфейс 'Виселица'")

self.newGame()
self.show()

def newGame(self):
"""Создание новой игры Виселица. Устанавливает объекты
для главного окна."""
self.setUpHangmanBoard()
self.setUpWord()
self.setUpBoard()

def setUpHangmanBoard(self):
"""Настройте объект метки для отображения палача."""
self.hangman_label = DrawingLabel()
self.hangman_label.setSizePolicy(
QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)

def setUpWord(self):
"""Откройте файл words и выберите произвольное слово.
Создать метки, которые будут отображать '_' в зависимости
от длины слова."""
words = self.openFile()
self.chosen_word = random.choice(words).upper()
#print(self.chosen_word)

# Вести учет правильных догадок


self.correct_counter = 0

# Следит за объектами меток.


418 ГЛАВА 16 ДОПОЛНИТЕЛЬНЫЕ ПРОЕКТЫ
# Используется для обновления текста на метках
self.labels = []

word_h_box = QHBoxLayout()

for letter in self.chosen_word:


self.letter_label = QLabel("___")
self.labels.append(self.letter_label)
self.letter_label.setObjectName("Word")
word_h_box.addWidget(self.letter_label)

self.word_frame = QFrame()
self.word_frame.setLayout(word_h_box)

def setUpBoard(self):
"""Настройка объектов и раскладок для клавиатуры и главного окна."""
top_row_list = ["A", "B", "C", "D", "E",
"F", "G", "H"]
mid_row_list = ["I", "J", "K", "L", "M",
"N", "O", "P", "Q"]
bot_row_list = ["R", "S", "T", "U", "V",
"W", "X", "Y", "Z"]

# Создание группы кнопок для отслеживания букв


self.keyboard_bg = QButtonGroup()

# Настройка клавиш в верхнем ряду


top_row_h_box = QHBoxLayout()

for letter in top_row_list:


button = QPushButton(letter)
button.setObjectName("Letters")
top_row_h_box.addWidget(button)
self.keyboard_bg.addButton(button)

top_frame = QFrame()
top_frame.setLayout(top_row_h_box)

# Настройка клавиш в среднем ряду


mid_row_h_box = QHBoxLayout()

for letter in mid_row_list:


button = QPushButton(letter)
button.setObjectName("Letters")
mid_row_h_box.addWidget(button)
ГЛАВА 16 ДОПОЛНИТЕЛЬНЫЕ ПРОЕКТЫ 419
self.keyboard_bg.addButton(button)

mid_frame = QFrame()
mid_frame.setLayout(mid_row_h_box)

# Настройка клавиш в нижнем ряду


bot_row_h_box = QHBoxLayout()

for letter in bot_row_list:


button = QPushButton(letter)
button.setObjectName("Letters")
bot_row_h_box.addWidget(button)
self.keyboard_bg.addButton(button)

bot_frame = QFrame()
bot_frame.setLayout(bot_row_h_box)

# Подключите кнопки в группе кнопок к слоту


self.keyboard_bg.buttonClicked.connect(self.buttonPushed)

keyboard_v_box = QVBoxLayout()
keyboard_v_box.addWidget(top_frame)
keyboard_v_box.addWidget(mid_frame)
keyboard_v_box.addWidget(bot_frame)

keyboard_frame = QFrame()
keyboard_frame.setLayout(keyboard_v_box)

# Создание основного макета и добавление виджетов


main_v_box = QVBoxLayout()
main_v_box.addWidget(self.hangman_label)
main_v_box.addWidget(self.word_frame)
main_v_box.addWidget(keyboard_frame)

# Создание центрального виджета для главного окна


central_widget = QWidget()
central_widget.setLayout(main_v_box)
self.setCentralWidget(central_widget)

def buttonPushed(self, button):


"""Работа с кнопками из группы кнопок и игровая логика."""
button.setEnabled(False)

body_parts_list = ["head", "body", "right_arm",


"left_arm", "right_leg", "left_leg"]
420 ГЛАВА 16 ДОПОЛНИТЕЛЬНЫЕ ПРОЕКТЫ
# Если пользователь угадывает неверно и количество
# неверных ответов не равно 6 (количество частей тела)
if button.text() not in self.chosen_word and self.hangman_label.incorrect_turns <= 5:
self.hangman_label.incorrect_turns += 1
index = self.hangman_label.incorrect_turns - 1
self.hangman_label.wrong_parts_list.append(body_parts_list[index])
self.hangman_label.incorrect_letter = True

# Если выбрана правильная буква,


# обновить метки и скорректировать счетчик
elif button.text() in self.chosen_word and self.hangman_label.incorrect_turns <= 5:
self.hangman_label.incorrect_letter = True
for i in range(len(self.chosen_word)):
if self.chosen_word[i] == button.text():
self.labels[i].setText(button.text())
self.correct_counter += 1

# Вызов обновления перед проверкой условий выигрыша


self.update()

# Пользователь выигрывает, если количество правильных букв


# равно длине слова
if self.correct_counter == len(self.chosen_word):
self.displayDialogs("win")

# Игра заканчивается, если количество неправильных ответов


# равно количеству частей тела. Раскрыть слово пользователю
if self.hangman_label.incorrect_turns == 6:
for i in range(len(self.chosen_word)):
self.labels[i].setText(self.chosen_word[i])
self.displayDialogs("game_over")

def openFile(self):
"""Open words.txt file."""
try:
with open("files/words.txt", 'r') as f:
word_list = f.read().splitlines()
return word_list
except FileNotFoundError:
print("File Not Found.")
ex_list = ["nofile"]
return ex_list

def displayDialogs(self, text):


"""Отображение диалоговых окон победы и завершения игры."""
ГЛАВА 16 ДОПОЛНИТЕЛЬНЫЕ ПРОЕКТЫ 421
if text == "win":
message = QMessageBox().question(self, "Win!",
"Вы победили!\nNEW GAME?",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
QMessageBox.StandardButton.No)
elif text == "game_over":
message = QMessageBox().question(self, "Game Over",
"Игра окончена\nNEW GAME?",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
QMessageBox.StandardButton.No)

if message == QMessageBox.StandardButton.No:
self.close()
else:
self.newGame()

if __name__ == '__main__':
app = QApplication(sys.argv)
app.setStyleSheet(style_sheet)
window = Hangman()
sys.exit(app.exec())

Эта программа содержит два класса: DrawingLabel и Hangman.

Создание класса Drawing


Класс DrawingLabel наследуется от QLabel и обрабатывает различные события
рисования, которые будут нарисованы на объекте метки в главном окне. Функция
paintEvent() вызывается в классе, который наследуется от QLabel, чтобы события
рисования происходили на метке и не закрывались главным окном.
Для того чтобы использовать класс DrawingLabel(), в методе setupHangmanBoard()
класса Hangman создается его экземпляр:
self.hangman_label = DrawingLabel()
Функция paintEvent() устанавливает QPainter и обрабатывает два метода
рисования: drawHangmanBackground(), который рисует виселицу игры Hangman на
ярлыке, и drawHangmanBody(), который рисует части тела только в том случае, если
они содержатся в списке part_list.

422 ГЛАВА 16 ДОПОЛНИТЕЛЬНЫЕ ПРОЕКТЫ


Создание класса главного окна
Работа класса Hangman начинается с инициализации окна GUI и вызова метода
newGame(). Сначала создается доска Hangman как экземпляр класса DrawingLabel.
Затем в функции setUpBoard() выбирается случайное слово из файла words.txt. Метки,
которые будут представлять буквы выбранного слова, заменяются символами
подчеркивания, добавляются в список меток и добавляются к горизонтальной
разметке объекта word_frame.
Наконец, в функции setUpBoard() необходимо настроить кнопки клавиатуры,
раскладку и логику игры. Три ряда кнопок, обозначающих буквы алфавита,
управляются одним объектом QButtonGroup, keyboard_bg.
При нажатии одной кнопки генерируется сигнал, вызывающий слот buttonPushed().
Когда кнопка нажата, она отключается путем передачи False в setEnabled().
Список частей тела, body_parts_list, содержит шесть названий частей тела.
Если игрок выбирает неправильную букву, то это название добавляется в список
wrong_parts_list и проверяется в функции drawHangmanBody() метода DrawingLabel.
Использование этого метода гарантирует, что при вызове функции paintEvent() все
необходимые части будут нарисованы различными стилями. В противном случае
метки обновляются, отображая правильные буквы в соответствующих позициях,
если игрок угадал правильно.
Если игрок выиграл или проиграл, появляется окно QMessageBox, позволяющее
пользователю закрыть приложение или продолжить игру. Если выбрано Yes, то
вызывается newGame().

Проект 16.6 - Графический интерфейс веб-браузера


Веб-браузер - это графический интерфейс пользователя, обеспечивающий доступ
к информации во Всемирной паутине. Пользователь может ввести в адресную строку
унифицированный локатор ресурса (URL) и запросить у веб-сервера содержимое
сайта для отображения на локальном устройстве, включая текст, изображения
и видеоданные. URL обычно имеет префикс http - протокол, используемый для
получения и передачи запрашиваемых веб-страниц, или https - протокол для
шифрованной связи между браузером и веб-сайтом.
В Qt имеется достаточно много классов для сетевого взаимодействия, WebSockets,
поддержка доступа к World Wide Web и многое другое. Этот проект знакомит с
классами PyQt для добавления веб-интеграции в графические интерфейсы.
В следующем проекте мы рассмотрим основные классы Qt WebEngine, в частности
модуль QtWebEngineWidgets для создания веб-приложений на основе виджетов.
Классы ядра WebEngine предоставляют механизм веб-браузера, который можно
использовать для встраивания веб-контента в приложения. Модуль QtWebEngineCore
использует Chromium в качестве "движка". Chromium - это программное обеспечение
с открытым исходным кодом от Google, которое может быть использовано для
создания веб-браузеров.
ГЛАВА 16 ДОПОЛНИТЕЛЬНЫЕ ПРОЕКТЫ 423
Графический интерфейс браузера, который мы создадим на рис. 16-8, служит
основой для создания собственного браузера и включает в себя следующие
возможности:
• Возможность открытия нескольких окон и вкладок с помощью меню
приложения или горячих клавиш.
• Навигационная панель, состоящая из кнопок "Назад", "Вперед", "Обновить",
"Стоп" и "Домой", а также адресная строка для ввода URL-адресов
• Виджет представления веб-движка, созданный с помощью QWebEngineView
• Строка состояния
• Строка прогресса, с помощью которой пользователь получает информацию о
загрузке веб-страниц

Рисунок 16-8. Графический интерфейс веб-браузера, отображающий строку меню,


панель инструментов, различные вкладки, логотип моего блога redhuli.io и индикатор
прогресса внизу

Примечание. Необходимо установить модуль QtWebEngineWidgets.


Для этого введите в командную строку следующую команду:
pip3 install PyQt6-WebEngine (для Windows используйте pip).

Кроме того, убедитесь, что вы загрузили папку icons из репозитория GitHub этой
главы.

424 ГЛАВА 16 ДОПОЛНИТЕЛЬНЫЕ ПРОЕКТЫ


Пояснения к графическому интерфейсу веб-браузера
В качестве основы для этого приложения можно использовать файл
main_window_template.py из Главы 5. В Листинге 16-6 представлены два новых класса:
QUrl используется для управления и построения URL-адресов, а QWebEngineView
- для создания основного компонента для рендеринга содержимого из Web,
представления веб-движка (в коде обозначается как web_view).
Листинг 16-6. Код для графического интерфейса веб-браузера
# web_browser.py
# Импорт необходимых модулей
import os, sys
from PyQt6.QtWidgets import (QApplication, QMainWindow,
QWidget, QLabel, QLineEdit, QTabWidget, QToolBar,
QProgressBar, QStatusBar, QVBoxLayout)
from PyQt6.QtCore import QSize, QUrl
from PyQt6.QtGui import QIcon, QAction
from PyQt6.QtWebEngineWidgets import QWebEngineView

style_sheet = """
QTabWidget:pane{
border: none
}
"""

class WebBrowser(QMainWindow):
def __init__(self):
super().__init__()
# Создайте списки, в которых будут отслеживаться новые окна,
# вкладки и ссылки
self.window_list = []
self.list_of_web_pages = []
self.list_of_urls = []

self.initializeUI()

def initializeUI(self):
self.setMinimumSize(300, 200)
self.setWindowTitle("16.6 – Web-браузер")
self.setWindowIcon(QIcon(os.path.join("icons", "pyqt_logo.png")))

self.sizeMainWindow()
self.createToolbar()
self.setUpMainWindow()

ГЛАВА 16 ДОПОЛНИТЕЛЬНЫЕ ПРОЕКТЫ 425


self.createActions()
self.createMenu()
self.show()

def setUpMainWindow(self):
"""Создайте объект QTabWidget и различные страницы для главного окна.
Обработка закрытия вкладки."""
self.tab_bar = QTabWidget()
self.tab_bar.setTabsClosable(True) # Добавление кнопок закрытия на вкладки
self.tab_bar.setTabBarAutoHide(True) # Скрывает панель вкладок, если в ней менее 2
вкладок
self.tab_bar.tabCloseRequested.connect(self.closeTab)

# Создать вкладку
self.main_tab = QWidget()
self.tab_bar.addTab(self.main_tab, "New Tab")

# Вызов метода, который устанавливает каждую страницу


self.setUpTab(self.main_tab)
self.setCentralWidget(self.tab_bar)

self.status_bar = QStatusBar()
self.setStatusBar(self.status_bar)

def createActions(self):
"""Создание действий меню приложения."""
# Создание действий для меню Файл
self.new_window_act = QAction("Новое окно", self)
self.new_window_act.setShortcut("Ctrl+N")
self.new_window_act.triggered.connect(self.openNewWindow)

self.new_tab_act = QAction("Новая вкладка", self)


self.new_tab_act.setShortcut("Ctrl+T")
self.new_tab_act.triggered.connect(self.openNewTab)

self.quit_act = QAction("Выход из браузера", self)


self.quit_act.setShortcut("Ctrl+Q")
self.quit_act.triggered.connect(self.close)

def createMenu(self):
"""Создание строки меню приложения."""
self.menuBar().setNativeMenuBar(False)

# Создание меню "Файл" и добавление действий


file_menu = self.menuBar().addMenu("Файл")
426 ГЛАВА 16 ДОПОЛНИТЕЛЬНЫЕ ПРОЕКТЫ
file_menu.addAction(self.new_window_act)
file_menu.addAction(self.new_tab_act)
file_menu.addSeparator()
file_menu.addAction(self.quit_act)

def createToolbar(self):
"""Настройка панели инструментов навигации."""
tool_bar = QToolBar("Адресный бар")
tool_bar.setIconSize(QSize(30, 30))
self.addToolBar(tool_bar)

# Создание действий на панели инструментов


back_page_button = QAction(QIcon(os.path.join("icons", "back.png")), "Back", self)
back_page_button.triggered.connect(self.backPageButton)

forward_page_button = QAction(QIcon(os.path.join("icons", "forward.png")),


"Forward", self)
forward_page_button.triggered.connect(self.forwardPageButton)

refresh_button = QAction(QIcon(os.path.join("icons", "refresh.png")), "Refresh", self)


refresh_button.triggered.connect(self.refreshButton)

home_button = QAction(QIcon(os.path.join("icons", "home.png")), "Home", self)


home_button.triggered.connect(self.homeButton)

stop_button = QAction(QIcon(os.path.join("icons", "stop.png")), "Stop", self)


stop_button.triggered.connect(self.stopButton)

# Настройка адресной строки


self.address_line = QLineEdit()
# addAction() здесь используется для простого отображения иконки
# в строке редактирования.
self.address_line.addAction(QIcon("icons/search.png"),
QLineEdit.ActionPosition.LeadingPosition)
self.address_line.setPlaceholderText("Введите адрес сайта")
self.address_line.returnPressed.connect(self.searchForUrl)

tool_bar.addAction(home_button)
tool_bar.addAction(back_page_button)
tool_bar.addAction(forward_page_button)
tool_bar.addAction(refresh_button)
tool_bar.addWidget(self.address_line)
tool_bar.addAction(stop_button)

def setUpWebView(self):
ГЛАВА 16 ДОПОЛНИТЕЛЬНЫЕ ПРОЕКТЫ 427
"""Создайте объект QWebEngineView, который
используется для просмотра веб-документов.
Настройте главную страницу и обработайте сигналы web_view."""
web_view = QWebEngineView()
web_view.setUrl(QUrl("https://google.com"))

# Создать индикатор хода загрузки страницы,


# отображаемый в строке состояния.
self.page_load_pb = QProgressBar()
self.page_load_label = QLabel()
web_view.loadProgress.connect(self.updateProgressBar)

# Отображение url в адресной строке


web_view.urlChanged.connect(self.updateUrl)

ok = web_view.loadFinished.connect(self.updateTabTitle)
if ok:
# Загрузка веб-страницы
return web_view
else:
print("Запрос завершился по таймеру.")

def setUpTab(self, tab):


"""Создайте отдельные вкладки и виджеты.
Добавьте url вкладки и веб-представление в соответствующий список.
Обновление адресной строки при переключении пользователем вкладок."""
# Создайте веб-представление, которое будет отображаться на странице.
self.web_page = self.setUpWebView()

# Добавьте новые web_page и url в соответствующие списки


self.list_of_web_pages.append(self.web_page)
self.list_of_urls.append(self.address_line)
self.tab_bar.setCurrentWidget(self.web_page)

# Если пользователь переключает страницу, обновить url в адресе,


# чтобы он отражал текущую страницу.
self.tab_bar.currentChanged.connect(self.updateUrl)

tab_v_box = QVBoxLayout()
# Устанавливает левое, верхнее, правое и нижнее поля,
# используемые для макета.
tab_v_box.setContentsMargins(0,0,0,0)
tab_v_box.addWidget(self.web_page)
tab.setLayout(tab_v_box)

428 ГЛАВА 16 ДОПОЛНИТЕЛЬНЫЕ ПРОЕКТЫ


def openNewWindow(self):
"""Создание нового экземпляра класса WebBrowser."""
new_window = WebBrowser()
new_window.show()
self.window_list.append(new_window)

def openNewTab(self):
"""Создание новой веб-вкладки."""
new_tab = QWidget()
self.tab_bar.addTab(new_tab, "Новая вкладка")
self.setUpTab(new_tab)

# Обновление индекса tab_bar для отслеживания новой вкладки.


# Загрузите url для новой страницы.
tab_index = self.tab_bar.currentIndex()
self.tab_bar.setCurrentIndex(tab_index + 1)
self.list_of_web_pages[self.tab_bar.currentIndex()].load(QUrl("https://google.com"))

def updateProgressBar(self, progress):


"""Обновление индикатора выполнения в строке состояния.
Это дает пользователю информацию о том, что страница
все еще загружается."""
if progress < 100:
self.page_load_pb.setVisible(progress)
self.page_load_pb.setValue(progress)
self.page_load_label.setVisible(progress)
self.page_load_label.setText(f"Загрузка страницы... ({str(progress)}/100)")
self.status_bar.addWidget(self.page_load_pb)
self.status_bar.addWidget(self.page_load_label)
else:
self.status_bar.removeWidget(self.page_load_pb)
self.status_bar.removeWidget(self.page_load_label)

def updateTabTitle(self):
"""Обновите название вкладки, чтобы оно соответствовало названию веб-сайта."""
tab_index = self.tab_bar.currentIndex()
title = self.list_of_web_pages[self.tab_bar.currentIndex()].page().title()
self.tab_bar.setTabText(tab_index, title)

def updateUrl(self):
"""Обновление url в адресе для отражения текущей отображаемой страницы."""
url = self.list_of_web_pages[self.tab_bar.currentIndex()].page().url()
formatted_url = QUrl(url).toString()
self.list_of_urls[self.tab_bar.currentIndex()].setText(formatted_url)

ГЛАВА 16 ДОПОЛНИТЕЛЬНЫЕ ПРОЕКТЫ 429


def searchForUrl(self):
"""Выполнить запрос на загрузку url."""
url_text = self.list_of_urls[self.tab_bar.currentIndex()].text()

# Добавить http к url


url = QUrl(url_text)
if url.scheme() == "":
url.setScheme("http")

# Запрос url
if url.isValid():
self.list_of_web_pages[self.tab_bar.currentIndex()].page().load(url)
else:
url.clear()

def backPageButton(self):
tab_index = self.tab_bar.currentIndex()
self.list_of_web_pages[tab_index].back()

def forwardPageButton(self):
tab_index = self.tab_bar.currentIndex()
self.list_of_web_pages[tab_index].forward()

def refreshButton(self):
tab_index = self.tab_bar.currentIndex()
self.list_of_web_pages[tab_index].reload()

def homeButton(self):
tab_index = self.tab_bar.currentIndex()
self.list_of_web_pages[tab_index].setUrl(QUrl("https://google.com"))

def stopButton(self):
tab_index = self.tab_bar.currentIndex()
self.list_of_web_pages[tab_index].stop()

def closeTab(self, tab_index):


"""Слот выдается при нажатии кнопки закрытия вкладки.
Индекс указывает на вкладку, которая должна быть удалена."""
self.list_of_web_pages.pop(tab_index)
self.list_of_urls.pop(tab_index)

self.tab_bar.removeTab(tab_index)

def sizeMainWindow(self):
"""Используйте QApplication.primaryScreen() для
430 ГЛАВА 16 ДОПОЛНИТЕЛЬНЫЕ ПРОЕКТЫ
доступа к информации об экране и использовать ее
для определения размера окна приложения
при запуске нового приложения."""
desktop = QApplication.primaryScreen()
size = desktop.availableGeometry()
screen_width = size.width()
screen_height = size.height()
self.setGeometry(0, 0, screen_width, screen_height)

if __name__ == '__main__':
app = QApplication(sys.argv)
app.setStyleSheet(style_sheet)
window = WebBrowser()
app.exec()

Перед вызовом функции initializeUI() необходимо создать несколько списков,


которые будут содержать новые окна, просмотренные веб-страницы и URL-адреса
для каждой вкладки. В данном проекте также вызывается setWindowIcon() для
включения иконки приложения, но на macOS она не будет отображаться из-за
системных рекомендаций.
В initializeUI() вызывается несколько методов. Первый - sizeMainWindow(),
демонстрирующий использование QApplication для получения информации о размере
экрана компьютера. Второй, createToolbar(), устанавливает панель инструментов для
навигации по веб-страницам. Методы createActions() и createMenu() устанавливают
главное меню. Меню включает в себя действия и ярлыки для создания новых
окон и новых вкладок, а также для закрытия приложения. В setUpMainWindow()
создается строка состояния приложения, а также виджет QTabWidget для управления
открытыми веб-страницами.
В методе createToolbar() экземпляр tool_bar включает кнопки для навигации между
веб-страницами и виджет QLineEdit для ввода и отображения URL-адресов. Каждая
кнопка при срабатывании издает сигнал, который подключается к соответствующему
слоту. Например, при нажатии кнопки back_page_button будет вызван слот
backPageButton(), что мы видим в следующем блоке кода:
def backPageButton(self):
tab_index = self.tab_bar.currentIndex()
self.list_of_web_pages[tab_index].back()
В tab_index хранится текущий индекс просматриваемой вкладки. Затем вызывается
метод back() на объекте web_view для этой текущей вкладки. Если tab_index не
равен 0, то пользователь может перемещаться назад по ранее просмотренным веб-
страницам. Метод back() является одной из нескольких функций, входящих в класс
QWebEngineView. К другим методам навигации относятся forward(), reload() и stop(),
которые используются и для других кнопок панели инструментов.
Когда пользователь вводит веб-адрес в виджете QLineEdit и нажимает клавишу
возврата, в функции searchForUrl() проверяется, начинается ли URL с правильной

ГЛАВА 16 ДОПОЛНИТЕЛЬНЫЕ ПРОЕКТЫ 431


схемы (например, http, https или file). Если правильная схема отсутствует, то к началу
URL добавляется http. Если URL соответствует стандартным правилам кодировки,
то отправляется запрос на загрузку load() сайта.

Создание вкладок для веб-браузера


Функция setUpMainWindow() используется для создания виджета вкладки и
объектов веб-представления. Сначала необходимо создать виджет QTabWidget,
который будет отображать веб-представление каждой отдельной вкладки. За более
подробной информацией о настройке виджетов вкладок обратитесь к Главе 6.
Несколько параметров виджета tab_bar изменяются таким образом, чтобы на
каждой вкладке была кнопка закрытия, и если остается только одна вкладка, то
панель вкладок не отображается. Это позволяет убедиться, что в главном окне
всегда есть хотя бы одна вкладка. При закрытии вкладки вызывается слот closeTab().
Соответствующие URL-адреса и элементы веб-просмотра для этой вкладки также
удаляются из списков list_of_urls и list_of_web_pages.
Первая вкладка, main_tab, создается, добавляется на панель вкладок
tab_bar и передается в метод setUpTab(). Виджет tab_bar устанавливается в качестве
центрального виджета главного окна. Чтобы настроить вкладку на отображение
веб-страницы, сначала необходимо создать объект веб-представления.

Создание веб-вида
Метод setUpWebView() создает экземпляр класса QWebEngineView, web_view, и
устанавливает URL веб-представления для отображения веб-страницы Google:
web_view.setUrl(QUrl("https://google.com"))
Для создания базового экземпляра веб-представления в приложении достаточно
создать объект QWebEngineView, использовать метод load() для загрузки веб-
страницы в виджет веб-представления и вызвать show(). В следующем коде показан
процесс настройки простого виджета веб-представления.
web_view = QWebEngineView()
web_view.load(QUrl("https://google.com"))
web_view.show()
После загрузки веб-страницы сигнал urlChanged, подключенный к функции
updateUrl(), изменяет URL, отображаемый в адресной_строке. С помощью сигнала
loadFinished() мы можем сообщить текущей вкладке об обновлении ее заголовка с
помощью слота updateTabTitle() и вернуть виджет web_view.
Далее создадим макет для размещения виджета web view, добавим URL текущей
вкладки и объект web_page в списки list_of_urls и list_of_web_pages, а также установим
макет для страницы текущей вкладки. Объект web_page - это виджет web_view,

432 ГЛАВА 16 ДОПОЛНИТЕЛЬНЫЕ ПРОЕКТЫ


который возвращается из функции setUpWebView() и отображается на странице в
функции setUpTab().
Наконец, для обработки переключения пользователя между вкладками в
QTabWidget предусмотрен сигнал currentChanged. Если выбрана другая вкладка, то
подключенный слот updateUrl() изменит отображаемый URL в address_line.
Добавление QProgressBar в строку состояния
В функции setUpWebView() также создаются индикатор и метка, которые будут
использоваться для отображения хода загрузки веб-страницы в строке состояния
браузера. Когда генерируется сигнал loadProgress, вызывается слот updateProgressBar().
Слот loadProgress содержит целочисленную информацию, которую мы можем
использовать для отслеживания степени загрузки страницы. Если прогресс меньше
100, то отображаются и индикатор прогресса, и метка, и устанавливаются их значения.
Код отображения индикатора прогресса представлен в следующих строках:
self.page_load_pb.setVisible(progress)
self.page_load_pb.setValue(progress)
Затем виджеты добавляются в строку состояния:
self.status_bar.addWidget(self.page_load_pb)
После завершения загрузки страницы мы вызываем функцию removeWidget() для
удаления индикатора прогресса и метки. Пример индикатора выполнения можно
увидеть в нижней части рис. 16-8.

Примечание. Создание веб-браузера - очень объемная задача. Существует


множество тем, которые не включены в данный проект, например, доступ
к HTTP-куки с помощью Qt WebEngine Core, работа с историей браузера с
помощью QWebEngineHistory, управление соединениями и клиентскими
сертификатами, поддержка прокси с помощью QNetworkProxy, работа с
JavaScript, загрузка содержимого с сайтов и другие. Вам обязательно стоит
изучить эти темы, если вы захотите использовать Qt WebEngine для более
сложных проектов.

ГЛАВА 16 ДОПОЛНИТЕЛЬНЫЕ ПРОЕКТЫ 433


Проект 16.7 - Трехсоставной QComboBox
Хотя обычно вы работаете с флажками, имеющими два состояния - отмеченное
и неотмеченное, существует и третье состояние - частично отмеченное. На этот
тип состояния обычно влияют дочерние виджеты комбобокса или группа, которой
управляет QComboBox. На рис. 16-9 показан простой пример комбобокса с тремя
состояниями, когда не все его дочерние виджеты выбраны.

Рисунок 16-9. Окно, содержащее частично установленный флажок QComboBox


Если все дочерние элементы выбраны, то родительский флажок установлен.
Если выбраны некоторые дочерние элементы, то родительский флажок установлен
частично.

Пояснения к трехсоставному QComboBox


Для этого примера начнем со сценария basic_window.py из Главы 1. В этом разделе
нет новых виджетов или других классов. Здесь мы сосредоточимся на изучении того,
что уже было изучено ранее, чтобы освоить новый навык. Посмотрите на экземпляр
tristate_cb в функции setUpMainWindow() в Листинге 16-7. Вы заметите, что мы хотим
ожидать сигнала при изменении состояния виджета с помощью stateChanged.
Листинг 16-7. Код для трехсоставного QComboBox
# tristate.py
# Импорт необходимых модулей
import sys
from PyQt6.QtWidgets import (QApplication, QWidget,
QCheckBox, QGroupBox, QButtonGroup, QVBoxLayout)
from PyQt6.QtCore import Qt

class MainWindow(QWidget):

def __init__(self):
434 ГЛАВА 16 ДОПОЛНИТЕЛЬНЫЕ ПРОЕКТЫ
super().__init__()
self.initializeUI()

def initializeUI(self):
"""Настройка графического интерфейса приложения."""
self.setMinimumSize(300, 200)
self.setWindowTitle("Трехпозиционный QComboBox")

self.setUpMainWindow()
self.show()

def setUpMainWindow(self):
"""Создание и расположение виджетов в главном окне."""
self.tristate_cb = QCheckBox("Выберите все начинки")
self.tristate_cb.stateChanged.connect(self.updateTristateCb)

# Создание флажков с отступом с помощью таблиц стилей


topping1_cb = QCheckBox("Шоколадная стружка")
topping1_cb.setStyleSheet("padding-left: 20px")
topping2_cb = QCheckBox("Жевательные мишки")
topping2_cb.setStyleSheet("padding-left: 20px")
topping3_cb = QCheckBox("Орео, арахис")
topping3_cb.setStyleSheet("padding-left: 20px")

# Создание неисключающей группы флажков


self.button_group = QButtonGroup(self)
self.button_group.setExclusive(False)
self.button_group.addButton(topping1_cb)
self.button_group.addButton(topping2_cb)
self.button_group.addButton(topping3_cb)
self.button_group.buttonToggled.connect(self.checkButtonState)

gb_v_box = QVBoxLayout()
gb_v_box.addWidget(self.tristate_cb)
gb_v_box.addWidget(topping1_cb)
gb_v_box.addWidget(topping2_cb)
gb_v_box.addWidget(topping3_cb)
gb_v_box.addStretch()

group_box = QGroupBox("Выбор начинки для мороженого")


group_box.setLayout(gb_v_box)

main_v_box = QVBoxLayout()
main_v_box.addWidget(group_box)
self.setLayout(main_v_box)
ГЛАВА 16 ДОПОЛНИТЕЛЬНЫЕ ПРОЕКТЫ 435
def updateTristateCb(self, state):
"""Используйте QCheckBox для установки или снятия всех флажков."""
for button in self.button_group.buttons():
if state == 2: # Qt.CheckState.Checked
button.setChecked(True)
elif state == 0: # Qt.CheckState.Unchecked
button.setChecked(False)

def checkButtonState(self, button, checked):


"""Определите, какие кнопки выбраны, и установите
состояние трехпозиционного QCheckBox."""
button_states = []

for button in self.button_group.buttons():


button_states.append(button.isChecked())

if all(button_states):
self.tristate_cb.setCheckState(Qt.CheckState.Checked)
self.tristate_cb.setTristate(False)
elif any(button_states) == False:
self.tristate_cb.setCheckState(Qt.CheckState.Unchecked)
self.tristate_cb.setTristate(False)
else:
self.tristate_cb.setCheckState(Qt.CheckState.PartiallyChecked)

if __name__ == '__main__':
app = QApplication(sys.argv)
window = MainWindow()
sys.exit(app.exec())

Если tristate_cb установлен, то для установки всех флажков в checkButtonState() мы


будем использовать значение state, переданное с сигналом stateChanged. В против-
ном случае все виджеты не будут отмечены.
Далее мы создадим остальную часть окна, инстанцируем дочерние объек-
ты QCheckBox и расположим их в группе QButtonGroup. Сигнал QButtonGroup
buttonToggled будет выдаваться всякий раз, когда любой из виджетов будет отмечен
или снят. Если состояние одного из флажков в группе кнопок изменяется, то исполь-
зуется слот checkButtonState(), чтобы узнать, какие кнопки отмечены или неотмече-
ны. Мы можем получить доступ ко всем кнопкам в QButtonGroup с помощью метода
buttons().
Затем эти значения добавляются в список button_states. Именно здесь мы забо-
тимся об обновлении параметров tristate_cb. Если все значения равны True, то ис-
пользуется функ­ция setCheckState(), чтобы убедиться, что tristate_cb имеет только
два состояния. Ес­ли все кнопки имеют значение False, то tristate_cb снимается с про-
верки. И, наконец, ес­ли в состоянии button_states присутствует смесь значений True
436 ГЛАВА 16 ДОПОЛНИТЕЛЬНЫЕ ПРОЕКТЫ
и False, то tristate_cb пе­ре­водится в трехпозиционный режим с помощью функции
setCheckState() и флага PartiallyChecked.

Резюме
В этой главе вы познакомились с различными GUI-приложениями, которые
создают структуру для более крупных проектов, таких как графический интерфейс
камеры или графический интерфейс веб-браузера. В других проектах были
представлены компоненты, которые можно включить в другие программы,
например, графический интерфейс просмотра каталогов, графический интерфейс
часов и графический интерфейс календаря. На примере графического интерфейса
"Виселица" мы продемонстрировали, как понимание QPainter полезно для рисования
и настройки внешнего вида виджетов. Наконец, трехпозиционные виджеты
QComboBox полезны для управления дочерними элементами.
На протяжении всей книги мы рассматривали различные темы проектирования
графических пользовательских интерфейсов с использованием PyQt6 и Python -
различные типы виджетов, классы и макеты. Мы рассмотрели, как стилизовать
интерфейсы, как добавлять меню и как упростить приложение с помощью Qt Designer.
Также были рассмотрены такие продвинутые темы, как работа с буфером обмена,
SQL и многопоточные приложения.
В Приложении вы найдете более подробную информацию о некоторых классах
PyQt6, используемых в этой книге, а также о некоторых других классах, которые не
были включены в предыдущие главы.
Ваши отзывы и вопросы всегда приветствуются. Большое спасибо за то, что
присоединились ко мне в этом путешествии и позволили поделиться с вами своими
знаниями о разработке графических интерфейсов.

ГЛАВА 16 ДОПОЛНИТЕЛЬНЫЕ ПРОЕКТЫ 437


ПРИЛОЖЕНИЕ

Справочное
руководство по PyQt6
PyQt - это связка языка Python для прикладного фреймворка Qt, поддерживаемого
компанией Riverbank Computing Limited. Связка - это интерфейс прикладного
программирования (API), который предоставляет код, позволяющий языку
программирования использовать другие библиотеки, не являющиеся родными для
этого языка. Qt - это набор библиотек C++ и средств разработки, обеспечивающих
доступ к сетевым технологиям, потокам, базам данных SQL, OpenGL и другим
графическим инструментам, XML, разработке графических интерфейсов и ряду
других возможностей. Данная глава посвящена только PyQt6, однако многие
концепции и методы доступны и в PyQt5.
Приложение содержит справочник по некоторым инструментам, модулям и
классам, изученным в этой книге, в том числе
• Обзор модулей и классов PyQt
• Обзор таблиц стилей Qt
• Обсуждение пространства имен Qt
Дополнительную информацию о компании Riverbank Computing Limited и PyQt6
можно найти на сайте https://riverbankcomputing.com/software/pyqt/intro.

Избранные модули PyQt6


PyQt предоставляет ряд модулей, которые дают доступ к широкому набору
инструментов, включая основы разработки графических интерфейсов, 2D-
и 3D-рендеринг, мультимедийный контент, работу с сетями, глобальное
позиционирование и многое другое. Для разработки базового графического
интерфейса пользователя в основном используются модули QtWidgets, QtGui и QtCore.
В Таблице A-1 перечислены модули, рассматриваемые в книге, а также несколько
дополнительных, с которыми вам стоит ознакомиться.
Полный список модулей верхнего уровня PyQt6 можно найти по следующей
ссылке: www.riverbankcomputing.com/static/Docs/PyQt6/module_index.html

438 ПРИЛОЖЕНИЕ СПРАВОЧНОЕ РУКОВОДСТВО ПО PYQT6


Таблица A-1. Таблица избранных модулей PyQt

Название модуля Описание


QtWidgets Предоставляет виджеты и другие классы для создания
пользовательских интерфейсов в стиле рабочего стола
QtCore Содержит множество дополнительных классов, включая
основные классы, не относящиеся к графическому интерфейсу,
например, классы для системы сигналов и слотов Qt
QtGui Содержит классы для работы с двумерной графикой и
изображениями, обработки событий и интеграции с оконными
системами
QtPrintSupport Обеспечивает кроссплатформенную поддержку конфигурирова
ния и подключения к принтерам
QtNetwork Предоставляет классы для написания коммуникационных
протоколов с использованием UDP или TCP
QtQuick Содержит классы для создания QML-приложений на Python
QtMultimedia Содержит классы для мультимедийного контента, включая
камеры, изображения и аудио.
QtMultimediaWidgets Предоставляет дополнительные классы, расширяющие
функциональность виджетов, связанных с мультимедиа
QtWebEngineCore Содержит основные классы, используемые другими модулями
Web Engine
QtWebEngineWidgets Классы, которые могут быть использованы для создания веб-
браузера на базе Chromium
QtSql Предоставляет классы для работы с базами данных SQL
sip Средства, используемые для создания привязок на Python для
библиотек C ++ (на этом языке написан Qt)
uic Содержит классы, используемые для работы с файлами .ui,
созданными Qt Designer

Избранные классы PyQt


Существуют сотни классов PyQt. В следующем разделе перечислены классы и
виджеты, которые можно встретить в этой книге. В каждом подразделе приводятся
либо таблицы с часто используемыми методами и сигналами, либо ссылка на сайт,
где можно найти более подробную информацию о классе.
Список всех классов PyQt можно найти на сайте
www.riverbankcomputing.com/static/Docs/PyQt6/sip-classes.html
Хотя документация по классам Qt написана на языке C++, она, как правило, более
подробна. Если вы хотите получить более подробную информацию о классах Qt, вы
также можете посмотреть https://doc.qt.io/qt-6/classes.html
ПРИЛОЖЕНИЕ СПРАВОЧНОЕ РУКОВОДСТВО ПО PYQT6 439
Следует иметь в виду, что некоторые классы, существующие в Qt, недоступны
в PyQt. Во многих случаях это связано с тем, что Python уже включает в себя
функциональность, которую мог бы предоставить удаленный класс. Одним из
распространенных примеров является QList, который существует в Qt, но не включен
в Python, поскольку включает в себя структуру данных list.

Классы для построения окна графического


интерфейса
В PyQt можно создать новый класс, который наследуется от любого из классов
виджетов. Однако для общего GUI-приложения необходимо создать только один
экземпляр QApplication и создать класс, который наследуется от QWidget, QMainWindow
или QDialog для создания главного окна приложения.

QApplication
QApplication отвечает за инициализацию и завершение работы виджетов в
графических интерфейсах пользователя. Если вы создаете приложения на основе
QWidget, то перед созданием любых других объектов, связанных с графическим
интерфейсом, вам необходимо создать экземпляр QApplication.
Среди обязанностей класса QApplication - инициализация приложения в
соответствии с настройками рабочего стола пользователя, обработка событий,
определение стиля графического интерфейса, работа с буфером обмена и учет всех
окон приложения.
Если вы создаете приложение, которое не нуждается в графическом интерфейсе
и может запускаться через командную строку, то вместо него следует использовать
QCoreApplication.

440 ПРИЛОЖЕНИЕ СПРАВОЧНОЕ РУКОВОДСТВО ПО PYQT6


QWidget
Класс QWidget является базовым классом для всех объектов графического
интерфейса пользователя PyQt. Виджет, созданный на основе класса QWidget, может
получать ввод от мыши, клавиатуры и других событий, а также может рисовать себя
на экране. Виджеты, не встроенные в родительский виджет, рассматриваются как
окна, имеющие строку заголовка и рамку. Класс QWidget является подклассом QObject
и QPaintDevice (класс, определяющий двумерное пространство для рисования с
помощью QPainter). Некоторые полезные методы QWidget приведены в таблице A-2.
Таблица A-2. Избранные методы из QWidget

Метод Описание
addAction(action) Добавляет действие к виджету
close() Закрытие виджета
height() Получение высоты виджета
width() Получение ширины виджета
move(x, y) Устанавливает местоположение виджета в (x, y)
rect() Получает геометрию виджета за вычетом рамки
setDisabled(bool) Если значение True, то виджет отключен
setEnabled(bool) Если значение True, то виджет включен
setFont(font) Устанавливает шрифт текста виджета (если виджет может
отображать текст)
setLayout(layout) Устанавливает менеджер компоновки для виджета
setGeometry(x, y, Устанавливает местоположение виджета, (x, y), и его размер,
width, height) ширину и высоту.
setStyleSheet(styleSheet) Устанавливает таблицу стилей для виджета
setToolTip(text) Устанавливает подсказку виджета
repaint() Перекрашивает виджет немедленно, вызывая функцию
paintEvent().
showFullScreen() Отображение виджета в полноэкранном режиме
update() Обновляет виджет, планируя событие paint в основном цикле
событий

ПРИЛОЖЕНИЕ СПРАВОЧНОЕ РУКОВОДСТВО ПО PYQT6 441


Обработка событий
События обычно вызываются пользователями или базовой системой. Это
может быть перемещение мыши, нажатие клавиши, изменение размера окна или
срабатывание таймера. Виджеты в приложении должны соответствующим образом
реагировать на событие. В простейших приложениях события, как правило, уже
обрабатываются в фоновом режиме, но иногда может возникнуть необходимость
переделать обработчики событий, чтобы обеспечить дополнительное поведение или
содержание виджетов. В Таблице A-3 перечислены некоторые часто используемые
обработчики событий.
Таблица A-3. Некоторые обработчики событий, используемые для обеспечения
поведения объектов QWidget

Обработчик событий Описание


paintEvent() Вызывается всякий раз, когда виджет необходимо перекрасить
resizeEvent() Вызывается, когда размер виджета был изменен
mousePressEvent() Вызывается при нажатии кнопки мыши, когда курсор мыши
находится внутри виджета. Какая именно кнопка мыши была
нажата, можно указать в событии
mouseReleaseEvent() Вызывается при отпускании кнопки мыши. Виджет,
получающий это событие, зависит от получения события
нажатия кнопки мыши
mouseDoubleClickEvent() Вызывается при двойном щелчке на виджете
mouseMoveEvent() Вызывается, когда мышь перемещается при нажатой кнопке.
Если параметр setMouseTracking() равен True, то события
посылаются даже тогда, когда ни одна кнопка не нажата
enterEvent() Вызывается, когда мышь входит в пространство виджета
leaveEvent() Вызывается, когда мышь покидает пространство виджета
keyPressEvent() Вызывается при нажатии клавиши
keyReleaseEvent() Вызывается при отпускании клавиши
focusInEvent() Вызывается, когда виджет получает фокус клавиатуры
focusOutEvent() Вызывается, когда виджет теряет фокус клавиатуры
closeEvent() Вызывается, когда закрывается либо виджет, либо окно

442 ПРИЛОЖЕНИЕ СПРАВОЧНОЕ РУКОВОДСТВО ПО PYQT6


QMainWindow
Класс QMainWindow предоставляет основу для построения приложения,
включающую функции для добавления строки меню, панелей инструментов,
строки состояния и виджетов док-станции. Элементы меню и панели инструментов
создаются с помощью QAction. QMainWindow уже имеет свой собственный макет,
к которому необходимо добавить центральный виджет в качестве центральной
области окна приложения.
Некоторые методы класса QMainWindow приведены в Табл. A-4.
Таблица A-4. Выбор методов из QMainWindow

Метод Описание
addDockWidget(area, Создает виджет док-станции в главном окне в указанной
dockwidget) области
addToolBar(area, Создает панель инструментов для главного окна. Также
toolbar) может быть указана область
menuBar() Возвращает строку меню главного окна
setStatusBar(statusbar) Создает строку состояния для главного окна
setCentralWidget(widget) Устанавливает центральный виджет окна
setWindowIcon(icon) Устанавливает значок окна
setWindowTitle(text) Устанавливает заголовок окна. Этот метод унаследован от
QWidget

ПРИЛОЖЕНИЕ СПРАВОЧНОЕ РУКОВОДСТВО ПО PYQT6 443


QDialog
Диалоговые окна представляют собой окна верхнего уровня, которые обычно
используются для быстрого получения обратной связи от пользователя. Экземпляры
QDialog могут быть модальными или безмодальными. Модальные диалоги часто
используются при выборе в диалоге опции, которая возвращает значение. Это
значение может быть использовано для сохранения файла, закрытия документа или
отмены действия.
QDialog является базовым классом для других классов диалоговых окон, включая
QColorDialog, QFileDialog, QFontDialog, QInputDialog, QMessageBox, QProgressDialog и
QErrorMessage. Несколько методов для установки режима диалога и обработки его
результатов приведены в Табл. A-5.
Таблица A-5. Выбор методов для QDialog

Метод Описание
accept() Скрывает модальный диалог и возвращает True, принимая действия,
указанные диалогом
reject() Скрывает модальный диалог и возвращает False, отклоняя действия,
указанные диалогом
open() Диалог отображается в виде модального диалога и блокирует дальнейшие
действия пользователя до тех пор, пока диалог не будет закрыт
show() Диалог является бесмодельным, возвращая управление пользователю
немедленно

444 ПРИЛОЖЕНИЕ СПРАВОЧНОЕ РУКОВОДСТВО ПО PYQT6


В Таблице A-6 перечислены некоторые общие кнопки по умолчанию, входящие
в перечисления QMessageBox.StandardButton или QDialogButtonBox.StandardButton.
Эти флаги очень полезны при создании пользовательских диалоговых окон. Каждая
из кнопок возвращает определенный ButtonRole (роль кнопки), описывающий
поведение кнопки. Например, AcceptRole приводит к принятию диалога и его
содержимого. Это эквивалентно кнопке OK. Роль RejectRole отклоняет диалог, что
соответствует действию Cancel. Существуют и другие виды ролей. Более подробная
информация приведена в таблице.
Таблица A-6. Выбор стандартных кнопок для QDialogButtonBox и QMessageBox

Метод Описание
Ok Определяет кнопку OK с ролью AcceptRole
Open Определяет кнопку Open с параметром AcceptRole
Save Определяет кнопку Save с параметром AcceptRole
Cancel Определяет кнопку Cancel с параметром RejectRole
Close Определяет кнопку Close с ролью RejectRole
Yes Определяет кнопку Yes с ролью YesRole
No Определяет кнопку No с параметром NoRole
Reset Определяет кнопку Reset с ролью ResetRole

ПРИЛОЖЕНИЕ СПРАВОЧНОЕ РУКОВОДСТВО ПО PYQT6 445


QPainter
Класс QPainter отвечает за работу с рисунком в PyQt, позволяя рисовать простые
линии и сложные фигуры на виджетах и других устройствах рисования. Наиболее
часто QPainter используется в обработчике события paintEvent(), а также для работы
с пиксмапами и изображениями. В Табл. A-7 приведены некоторые методы класса
QPainter для рисования.
Таблица A-7. Методы, выбранные из QPainter

Метод Описание
begin(device) Начало окраски на устройстве окраски
end() Завершает рисование. Ресурсы, использованные во время
рисования, освобождаются
save() Сохраняет текущее состояние рисовальщика. За функцией
save() должна следовать функция restore(), которая
возвращает текущее состояние рисовальщика.
drawArc(QRectF, Рисует дугу, заданную прямоугольником QRectF, startAngle,
startAngle, spanAngle) и spanAngle
drawChord(QRectF, Рисует хорду, заданную прямоугольником QRectF, startAngle,
startAngle, spanAngle) и spanAngle
drawEllipse(QPointF, x_ Рисует эллипс с центром QPointF, радиусом x_rad и y_rad
rad, y_rad)
drawLine(x1, y1, x2, y2) Проводит прямую из точки (x1, y1) в точку (x2, y2)
drawPath(path) Рисует контур, заданный QPainterPath path
drawPie(QRectF, Рисует кружок, заданный прямоугольником QRectF,
startAngle, spanAngle) startAngle и spanAngle
drawPixmap(x, y, pixmap) Рисует пиксельное изображение в точке (x, y)
drawPoint(x, y) Строит точку в точке (x, y)
drawRect(x, y, width, Рисует прямоугольник в точке (x, y) с шириной и высотой
height)
drawRoundedRect(QRectF, Рисует прямоугольник со скругленными углами, заданный
x_rad, y_rad) QRectF, с радиусами x_rad и y_rad
drawText(QPointF, text) Рисует текст в точке QPointF
fillRect(QRectF, brush) Заливает прямоугольник QRectF цветом кисти
rotate(angle) Поворачивает систему координат по часовой стрелке на
угол (в градусах)
setBrush(brush) Устанавливает кисть художника
setPen(pen) Устанавливает перо художника
setFont() Устанавливает шрифт художника

446 ПРИЛОЖЕНИЕ СПРАВОЧНОЕ РУКОВОДСТВО ПО PYQT6


Менеджеры компоновки
Использование менеджеров компоновки PyQt значительно упрощает процесс
расположения виджетов по сравнению с ручным указанием размера, позиции или
обработчика события resizeEvent() каждого виджета. Использование менеджеров
компоновки, как правило, является хорошим началом для позиционирования
виджетов, хотя может потребоваться корректировка политики размеров виджета
или добавление растяжек или интервалов в компоновку.
Следующие классы наследуются от класса QLayout, который является базовым
классом для менеджеров компоновки:
1. QBoxLayout - Упорядочивает дочерние виджеты в ряд (по горизонтали) или в
столбец (по вертикали).
a. QHBoxLayout - Упорядочивает виджеты по горизонтали
b. QVBoxLayout - Упорядочивает виджетов по вертикали
2. QGridLayout - Упорядочивает виджеты в сетке строк и столбцов
3. QFormLayout - Выстраивает виджеты в структуру, подобную форме, с метками
и связанными с ними виджетами ввода
4. QStackedLayout - Упорядочивает виджеты в стопку, в которой одновременно
виден только один виджет. Удобный класс QStackedWidget построен поверх
QStackedLayout.
В Таблице A-8 перечислены часто используемые методы классов макетов.

ПРИЛОЖЕНИЕ СПРАВОЧНОЕ РУКОВОДСТВО ПО PYQT6 447


Таблица A-8. Выбранные методы для различных менеджеров компоновки

Метод Класс Описание


addWidget(widget, QBoxLayout Добавляет виджет в конец макета
stretch, alignment) с коэффициентом растяжения и
выравниванием
addWidget(widget, QGridLayout Добавляет виджет в строку, столбец с
row, column, rowSpan, (необязательно) rowSpan и columnSpan и
columnSpan alignment) выравниванием
addRow(label, field) QFormLayout Добавляет новую строку с заданными меткой
и полем (виджет ввода)
addWidget(widget) QStackedLayout Добавляет новый виджет в конец макета.
Метод возвращает индекс виджета в стеке
addLayout(layout, QBoxLayout Добавляет макет в конец блока. Создает
stretch) вложенный макет
addLayout(layout, row, QGridLayout Добавляет макет в позицию (строка, столбец).
column, alignment) Создает вложенный макет
addSpacing(int) QGridLayout, Добавляет в макет нерастяжимую область
QBoxLayout (QSpacerItem) со значением int
addStretch(int) QBoxLayout Добавляет в макет растягиваемую область
(QSpacerItem) со значением int
setSpacing(int) QLayout Устанавливает расстояние между виджетами
в макете. Наследуется от QLayout
setContentMargins(left, QLayout Устанавливает левое, верхнее, правое и
top, right, bottom) нижнее поля вокруг макета

448 ПРИЛОЖЕНИЕ СПРАВОЧНОЕ РУКОВОДСТВО ПО PYQT6


Виджеты кнопок
Кнопки являются одним из основных инструментов взаимодействия в
графическом интерфейсе, обеспечивая обратную связь приложения с решениями
пользователя. Кнопки в PyQt могут отображать текст или пиктограммы и являются
проверяемыми. Следующие классы наследуются от базового класса для виджетов
кнопок - QAbstractButton:
1. QPushButton - Командная кнопка, используемая для указания компьютеру
выполнить какое-либо действие.
2. QCheckBox - Кнопка с возможностью установки флажка, обычно используется
для включения/выключения функций в приложении.
3. QRadioButton - Аналогична флажкам, но является взаимоисключающей.
4. QToolButton - Обычно используется на панели инструментов и представляет
собой кнопки быстрого доступа для выбора команд или опций.
Для управления и организации нескольких кнопок в качестве контейнера для
создания эксклюзивных кнопок (по умолчанию) может выступать класс QButtonGroup.
В Таблице A-9 перечислены некоторые из наиболее часто используемых методов для
виджетов кнопок.
Таблица A-9. Избранные методы для различных виджетов кнопок

Метод Описание
setIcon(icon) Устанавливает значок виджета
setText(text) Устанавливает текст виджета
setAutoExclusive(bool) Включение автоисключения для кнопок в группе
setCheckable(bool) Устанавливает, является ли кнопка кнопкой переключения
или нет
setChecked(bool) Устанавливает, отмечена ли кнопка флажком или нет
isChecked() Указывает, отмечена ли кнопка флажком или нет (если
setCheckable() равен True)
text() Получает текст кнопки

Некоторые сигналы для классов виджетов кнопок приведены в таблице A-10.

ПРИЛОЖЕНИЕ СПРАВОЧНОЕ РУКОВОДСТВО ПО PYQT6 449


Таблица A-10. Сигналы для различных виджетов кнопок

Сигнал Класс Описание


clicked(bool) QAbstractButton Сигнал, издаваемый при нажатии и отпускании
кнопки
pressed() QAbstractButton Выдается при щелчке левой кнопкой мыши по
кнопке
released() QAbstractButton Сигнал, выдаваемый при отпускании левой
кнопки мыши
toggled(bool) QAbstractButton Выдается при изменении состояния проверяемой
кнопки
stateChanged(bool) QCheckBox Выдается при изменении состояния флажка
triggered(action) QToolButton Сигнал, выдаваемый при срабатывании действия

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

Комбобоксы
Класс QComboBox представляет пользователю список вариантов выбора в
виде компактного выпадающего меню. Некоторые методы класса приведены в
Таблице A-11. Когда с комбобоксом не взаимодействуют, все элементы, кроме
текущего выбранного, скрываются от глаз. Виджет QFontComboBox - это еще один
тип комбобокса, который наследует QComboBox и используется для выбора семейства
шрифтов.

450 ПРИЛОЖЕНИЕ СПРАВОЧНОЕ РУКОВОДСТВО ПО PYQT6


Таблица A-11. Выбор методов из класса QComboBox

Метод Описание
addItem(text) Добавляет элемент в список с текстом
addItems(list(text)) Добавляет список элементов в комбобокс
currentIndex() Получает индекс текущего выбранного элемента
currentText() Получает текст текущего выделенного элемента
insertItem(index, text) Вставляет текст в комбобокс по заданному индексу
setItemText(index, text) Устанавливает текст для элемента с заданным индексом
removeItem(index) Удаляет элемент с заданным индексом
clear() Очищает все элементы из комбобокса
setEditable(bool) Если значение True, то содержимое комбобокса можно
редактировать

В Таблице A-12 приведены сигналы выбора для классов комбобоксов.


Таблица A-12. Часто используемые сигналы классов QComboBox и QFontComboBox

Сигнал Описание
currentIndexChanged(index) Выдается, если текущий элемент в комбобоксе изменился
currentTextChanged(text) Сигнал, выдаваемый при изменении текущего элемента в
комбобоксе. Возвращает текст
activated(index) Выдается только при взаимодействии пользователя с
элементом
highlighted(index) Выдается при выделении элемента в комбобоксе
textActivated(text) Сигнал, выдаваемый при выборе пользователем элемента
currentFontChanged(font) Выдается при изменении текущего шрифта

ПРИЛОЖЕНИЕ СПРАВОЧНОЕ РУКОВОДСТВО ПО PYQT6 451


QLineEdit
Виджет QLineEdit предоставляет одну строку для ввода и редактирования
обычного текста. Несмотря на то, что они не перечислены в следующих таблицах, в
QLineEdit уже встроены слоты clear(), selectAll(), cut(), copy(), paste(), undo() и redo().
В Таблице A-13 приведены некоторые методы класса QLineEdit.
Таблица A-13. Методы класса QLineEdit

Метод Описание
text() Получает текущий текст в строке редактирования
setAlignment(alignment) Устанавливает выравнивание текста, отображаемого в
виджете
setPlaceholderText(text) Отображение текста-заместителя при пустой строке
редактирования
setEchoMode(mode) Параметр mode описывает способ отображения содержимого
строки. Установите для параметра mode значение QLineEdit.
Пароль для маскировки символов
setMaxLength(int) Устанавливает максимальную длину символов
setTextMargins(left, top, Устанавливает текстовые поля для текста, отображаемого в
right, bottom) режиме редактирования строки
setDragEnabled(bool) Если значение True, то перетаскивание выделенного текста в
строке редактирования разрешено

Несколько общих сигналов для QLineEdit приведены в Таблице A-14.


Таблица A-14. Часто используемые сигналы класса QLineEdit

Сигнал Описание
returnPressed() Выдается при нажатии клавиши Enter. Если установлен
валидатор, то сигнал подается только в том случае, если
текст принят
textChanged(text) Сигнал выдается при изменении текста

452 ПРИЛОЖЕНИЕ СПРАВОЧНОЕ РУКОВОДСТВО ПО PYQT6


Виджеты редактирования текста
Два класса редактирования текста, QTextEdit и QPlainTextEdit, предоставляют
инструменты и функциональность для отображения и редактирования больших
массивов текста. Дополнительным преимуществом QTextEdit является возможность
работы с насыщенным текстом, графикой и таблицами. Оба класса похожи на
QLineEdit, поскольку в них уже встроены функции редактирования. Несколько
методов текстовых редакторов приведены в Таблице A-15.
Следует также отметить класс QTextBrowser, который наследует QTextEdit.
QTextBrowser позволяет работать только в режиме чтения, но включает в себя
функции гипертекстовой навигации, чтобы пользователи могли щелкать на ссылках
и переходить по ним.
Таблица A-15. Выбор методов из QTextEdit и QPlainTextEdit

Метод Описание
find(text, flags) Находит следующее вхождение текста в текстовом редакторе
print(printer) Печать документа текстового редактора на принтер
setPlaceHolderText(text) Устанавливает текст-заполнитель для редактирования текста
setReadOnly(bool) Если значение True, то для редактирования текста
устанавливается режим "только для чтения".
toPlainText() Возвращает текст редактируемого текста в виде обычного
текста
zoomIn(range) Увеличение масштаба текста
zoomOut(range) Уменьшение масштаба текста

Часто используемые сигналы для виджетов редактирования текста приведены в


Таблице A-16.
Таблица A-16. Выбор сигналов из QTextEdit и QPlainTextEdit

Сигнал Описание
selectionChanged() Сигнал, выдаваемый при изменении текста, выделенного
в текстовом редакторе
textChanged() Выдается при изменении содержимого редактируемого
текста

ПРИЛОЖЕНИЕ СПРАВОЧНОЕ РУКОВОДСТВО ПО PYQT6 453


Виджеты со спин-боксом
Спин-боксы позволяют пользователям выбирать значения в заданном диапазоне,
нажимая кнопки вверх/вниз для циклического перебора значений виджета.
Пользователи также могут вручную вводить значения в предусмотренную строку
редактирования. Класс QAbstractSpinBox является базовым классом для следующих
классов:
1. QSpinBox - работает с целыми числами.
2. QDoubleSpinBox - аналогичен QSpinBox, но используется для значений с
плавающей точкой.
3. QDateTimeEdit - виджет спинбокса для выбора даты и времени. Для установки
формата, используемого для отображения даты и времени, используется
функция setDisplayFormat().
4. QDateEdit - виджет, отображающий только даты. Наследует QDateTimeEdit.
5. QTimeEdit - виджет, отображающий только время. Наследует QDateTimeEdit.
Некоторые методы классов QSpinBox и QDoubleSpinBox приведены в Таблице A-17.
Аналогичные методы есть у QDateTimeEdit и других виджетов спин-бокса.
Таблица A-17. Выбор сигналов классов QSpinBox и QDoubleSpinBox. Значение
val относится к целым числам для QSpinBox и к числам с плавающей точкой для
QDoubleSpinBox

Метод Описание
setValue(val) Устанавливает значение val для спин-бокса
setMinimum(val) Устанавливает минимальное значение val для спин-бокса
setMaximum(val) Устанавливает максимальное значение val для спин-бокса
setPrefix(str) Добавляет префикс к началу отображаемого значения
setSuffix(str) Добавляет суффикс к концу отображаемого значения
setRange(min, max) Устанавливает минимальное и максимальное значения
диапазона
setSingleStep(val) При нажатии клавиш со стрелками значение спин-бокса
увеличивается/уменьшается на величину val

454 ПРИЛОЖЕНИЕ СПРАВОЧНОЕ РУКОВОДСТВО ПО PYQT6


Некоторые сигналы QSpinBox и QDoubleSpinBox приведены в Таблице A-18.
Таблица A-18. Сигналы от QSpinBox и QDoubleSpinBox

Сигнал Описание
valueChanged(val) Сигнал, выдаваемый при изменении значения.
Предоставляет новое значение val
textChanged(text) Сигнал, выдаваемый при изменении значения.
Предоставляет новое значение текста

Виджеты ползунка
Приведенные ниже виджеты отличаются друг от друга внешне, но на самом деле
весьма схожи по функциональности. Виджеты, наследующие класс QAbstractSlider,
используются для выбора целочисленных значений в ограниченном диапазоне.
Классы, наследующие QAbstractSlider, включают следующие:
1. QDial - предоставляет контроллер округлого диапазона для выбора или
настройки значений. Пример QDial показан на рисунке A-1.
2. QScrollBar - предоставляет горизонтальные или вертикальные полосы
прокрутки, которые пользователь может использовать для доступа к
другим частям документа, которые шире, чем виджет, используемый для его
отображения.
3. QSlider - создает классические виджеты горизонтальных и вертикальных
ползунков для управления значениями в заданном диапазоне.

Рисунок A-1. Пример виджетов QLCDNumber и QDial. XML и Python-код этого


примера можно найти в папке Appendix репозитория GitHub.
ПРИЛОЖЕНИЕ СПРАВОЧНОЕ РУКОВОДСТВО ПО PYQT6 455
В Таблице A-19 приведены некоторые методы базового класса QAbstractSlider.
Таблица A-19. Выбор методов из QAbstractSlider

Метод Описание
value() Удерживает текущее значение ползунка
setMinimum(int) Устанавливает минимальное значение ползунка
setMaximum(int) Устанавливает максимальное значение ползунка
setOrientation(orientation) Устанавливает ориентацию, горизонтальную или
вертикальную (обеспечивается перечислением
Qt.Orientation).
setSingleStep(int) При нажатии клавиш со стрелками значение ползунка
увеличивается/уменьшается на int
setTracking(bool) Если значение True, то положение ползунка можно
отслеживать
setSliderPosition(int) Устанавливает текущее положение ползунка
setValue(int) Устанавливает текущее положение ползунка в int. Если
отслеживание включено, то это значение совпадает со
значением геттера value()

Сигналы класса QAbstractSlider приведены в Таблице A-20.


Таблица A-20. Сигналы класса QAbstractSlider

Сигнал Описание
valueChanged(val) Сигнал, выдаваемый при изменении значения.
Предоставляет новое значение val
rangeChanged(min, max) Сигнал, выдаваемый при изменении диапазона с новыми
минимальным и максимальным значениями
sliderMoved(val) Выдается при нажатии на ползунок и его перемещении
sliderPressed() Выдается при нажатии на ползунок
sliderReleased() Выдается при отпускании ползунка

456 ПРИЛОЖЕНИЕ СПРАВОЧНОЕ РУКОВОДСТВО ПО PYQT6


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

QLabel
QLabel - это универсальный виджет. Несмотря на то, что ярлык не предоставляет
никаких функций взаимодействия с пользователем, QLabel может отображать
обычный или насыщенный текст, пиксмапы и даже GIF-файлы. Для настройки
внешнего вида ярлыков существует ряд методов. В Таблице A-21 перечислены
некоторые из них.
Таблица A-21. Методы выбора из QLabel

Метод Описание
setPicture(picture) Устанавливает содержимое метки в виде изображения
setPixmap(pixmap) Устанавливает содержимое метки в виде pixmap
setMovie(movie) Устанавливает для содержимого метки значение movie
setText(text) Устанавливает содержимое метки в виде текста
setAlignment(alignment) Устанавливает выравнивание содержимого метки
setIndent(int) Устанавливает количество пикселей, на которое отступает
текст метки
setMargin(int) Устанавливает поля метки

ПРИЛОЖЕНИЕ СПРАВОЧНОЕ РУКОВОДСТВО ПО PYQT6 457


QProgressBar
Прогрессбары используются для визуальной обратной связи с пользователем
о ходе выполнения той или иной компьютерной операции. Прогрессбары могут
отображаться как вертикально, так и горизонтально. В Таблице A-22 приведены
некоторые методы класса QProgressBar.
Таблица A-22. Выбор методов для класса QProgressBar

Метод Описание
value() Получает текущее значение индикатора прогресса
setMinimum(int) Устанавливает минимальное значение индикатора
прогресса
setMaximum(int) Устанавливает максимальное значение индикатора
прогресса
setRange(min, max) Устанавливает минимальное и максимальное значения
setOrientation(orientation) Устанавливает ориентацию, горизонтальную или
вертикальную (обеспечивается перечислением
Qt.Orientation).
setTextVisible(bool) Если значение True, то отображается текущий процент
выполнения

QProgressBar имеет один сигнал, valueChanged(int), который выдается при


изменении значения, отображаемого на индикаторе прогресса.

QGraphicsView
Класс QGraphicsView предоставляет виджет для отображения содержимого сцены
QGraphicsScene. Как одна из частей Qt Graphics View Framework, класс QGraphicsView
отвечает за отображение элементов графической сцены в прокручиваемом окне. В
обязанности объекта QGraphicsScene входит управление элементами сцены. Объект
QGraphicsItem (или один из его подклассов) предоставляет элементы для сцены.
Если вы хотите узнать больше о Graphics View Framework, посетите сайт
https://doc.qt.io/qt-6/graphicsview.html.

QLCDNumber
Виджет QLCDNumber отображает числа на семисегментном ЖК-дисплее.
Его пример показан на рис. A-1. На дисплее могут отображаться десятичные,
шестнадцатеричные, восьмеричные и двоичные числа. ЖК-дисплей может

458 ПРИЛОЖЕНИЕ СПРАВОЧНОЕ РУКОВОДСТВО ПО PYQT6


отображать только определенные символы. Обратите внимание, что если передается
символ, который виджет не может отобразить, то вместо него будет показан пробел.
В Таблице A-23 перечислены некоторые методы класса QLCDNumber.
Таблица A-23. Выбор методов из класса QLCDNumber

Метод Описание
value() Получение отображаемого на ЖК-дисплее значения
intValue() Получает отображаемое значение, округленное до
ближайшего целочисленного значения
display(val) Отображает на дисплее значение val. val может быть
плавающей точкой, целым числом или строкой.
setMode(mode) Устанавливает режим отображения на ЖК-дисплее
значений Bin, Oct, Dec или Hex
setSmallDecimalPoint(bool) Если True, то десятичная дробь выводится между двумя
цифрами

QLCDNumber имеет сигнал overflow(), который выдается, когда виджету


предлагается вывести на экран слишком длинное число или строку.

Представления элементов
Следующие классы представлений модели предоставляют средства для
отображения элементов в списках, таблицах или древовидных структурах. Они
должны использоваться вместе с классом модели как часть фреймворка Qt
Model/View.
1. QListView - Предоставляет представление списка и пиктограмм для отображения
элементов из модели.
2. QTableView - Предоставляет таблицу для отображения элементов из модели.
3. QTreeView - Предоставляет иерархическую древовидную архитектуру для
отображения элементов из модели.
Все эти классы наследуются от класса QAbstractItemView. С помощью сигналов
и слотов представления элементов, созданные на основе QAbstractItemView, могут
взаимодействовать с моделями, использующими QAbstractItemModel. Каждое из
представлений элементов имеет свои методы для работы со строками, столбцами,
заголовками и элементами. Для управления элементами представления используют
индексы. Некоторые методы для QAbstractItemView приведены в табл. A-24.

ПРИЛОЖЕНИЕ СПРАВОЧНОЕ РУКОВОДСТВО ПО PYQT6 459


Таблица A-24. Выбор методов для базового класса QAbstractItemView

Метод Описание
clearSelection() Все выбранные элементы снимаются с выбора
selectAll() Выделяет все элементы в представлении
setCurrentIndex(index) Устанавливает элемент с индексом в качестве текущего
элемента
update(index) Обновление области по заданному индексу
setAlternatingRowColors(bool) Если True, то фон рисуется чередующимися цветами
setAcceptDrops(bool) Если значение True, то элементы могут быть
перетащены в представление
setDragEnabled(bool) Если значение True, элементы можно перетаскивать по
экрану
setIconSize(size) Устанавливает размер значков
setItemDelegate(delegate) Устанавливает делегат элемента для фреймворка
Model/View представления
setModel(model) Устанавливает модель для представления

PyQt также предлагает удобные классы, основанные на элементах, для каждого


из различных типов представлений - QListWidget, QTableWidget и QTreeWidget.
Элементы добавляются в виджеты с помощью QListWidgetItem, QTableWidgetItem или
QTreeWidgetItem.
Сигналы выбора для QAbstractItemView приведены в табл. A-25.
Таблица A-25. Методы Select для базового класса QAbstractItemView

Сигнал Описание
activated(index) Сигнал, выдаваемый при активации пользователем элемента с
индексом
clicked(index) Выдается при нажатии левой кнопки мыши на элемент в
представлении (указанный индексом)
doubleClicked(index) Выдается при двойном нажатии кнопки мыши на элементе в
представлении (указанном индексом)
entered(index) Сигнал, выдаваемый при попадании курсора мыши в элемент по
индексу. Включите слежение за мышью, чтобы использовать
pressed(index) Сигнал, выдаваемый при нажатии кнопки мыши на элементе с
индексом

460 ПРИЛОЖЕНИЕ СПРАВОЧНОЕ РУКОВОДСТВО ПО PYQT6


Виджеты-контейнеры
PyQt предоставляет несколько виджетов-контейнеров для управления группами
виджетов. Контейнеры могут использоваться для управления виджетами ввода,
упрощения процесса организации группы виджетов или просто в качестве
декоративного виджета для разделения групп виджетов. После создания контейнера
к самому виджету контейнера необходимо применить менеджер компоновки.

Контейнеры с фреймами
Виджеты QFrame могут заключать в себе и группировать виджеты, а также
выполнять роль вместилищ в окнах. С помощью фреймов можно задавать внешний
вид других виджетов: приподнятый, утопленный или плоский. Класс QFrame
используется в качестве базового класса для нескольких других классов-контейнеров,
включая QToolBox и QStackedWidget. В Таблице A-26 перечислены некоторые методы
класса QFrame.
Таблица A-26. Выбор методов для QFrame

Метод Описание
setFrameRect(QRect) Задает прямоугольник, в котором рисуется рамка
setFrameShadow(shadow) Устанавливает тень рамки, используя такие флаги,
как Plain(плоский), Raised(приподнятый) или
Sunken(утопленный)
setFrameShape(shape) Устанавливает форму рамки, используя такие флаги, как
Box, Panel, HLine и VLine
setLineWidth(int) Устанавливает ширину линии, рисуемой вокруг рамки

ПРИЛОЖЕНИЕ СПРАВОЧНОЕ РУКОВОДСТВО ПО PYQT6 461


Виджеты QToolBox представляют собой ряд страниц или отсеков в столбце. Для
навигации по каждой из страниц в верхней части каждой из них имеется вкладка.
Щелкнув на следующей вкладке, пользователь может просмотреть содержимое
новой вкладки. Некоторые методы QToolBox перечислены в табл. A-27.
Таблица A-27. Некоторые методы класса QToolBox

Метод Описание
addItem(widget, text) Добавляет виджет в новую вкладку в нижней части
панели инструментов
insertItem(index, widget, text) Вставляет виджет в новую вкладку по заданному индексу
indexOf(widget) Возвращает индекс указанного виджета
setCurrentIndex(index) Устанавливает индекс для нового элемента
setCurrentWidget(widget) Делает виджет текущим виджетом, отображаемым на
панели инструментов

При изменении элемента в QToolBox выдается сигнал currentChanged(index).


Виджет QStackedWidget имеет схожую с QToolBox функцию - отображение
нескольких виджетов, уложенных друг на друга для экономии места в окне. Однако
есть ключевое отличие: QStackedWidget не предоставляет пользователю возможности
переключения между вкладками. Поэтому для навигации по страницам используются
другие виджеты, такие как QComboBox или QListWidget.
QTabWidget - это еще один контейнерный класс, который похож на QStackedLayout,
но предоставляет вкладки, необходимые для переключения между страницами.
Наконец, виджеты QGroupBox обычно объединяют коллекции радиокнопок и
флажков. Основным визуальным отличием от класса QFrame является добавление
заголовка.

QScrollArea
Область прокрутки может быть добавлена к дочернему виджету для отображения
содержимого внутри фрейма. При изменении размера фрейма будут появляться
полосы прокрутки, что позволит пользователю по-прежнему просматривать весь
дочерний виджет. Несколько методов класса перечислены в Таблице A-28. То, как
появляются полосы прокрутки, можно контролировать с помощью политик размера
класса QAbstractScrollArea.

462 ПРИЛОЖЕНИЕ СПРАВОЧНОЕ РУКОВОДСТВО ПО PYQT6


Таблица A-28. Выбор методов для QScrollArea

Метод Описание
ensureVisible(x, y, Убеждается, что указанная координата (x, y) с полями
xmargin, ymargin) xmargin и ymargin остается видимой в области просмотра
setAlignment(alignment) Устанавливает выравнивание виджета области
прокрутки
setWidget(widget) Устанавливает виджет области прокрутки
setWidgetResizable(bool) Если False, то область прокрутки подчиняется размеру
дочернего виджета

QMdiArea
Для многооконных графических интерфейсов (MDI) класс QMdiArea
предоставляет контейнер для отображения нескольких окон внутри одного окна
приложения. Под-окна являются экземплярами класса QMdiSubWindow и могут
быть расположены в виде плиток или каскадов. Под-окна могут работать совместно,
передавая информацию туда и обратно. Для удобства переключения между окнами
в виджет MDI-области может быть добавлено контекстное меню. Некоторые методы
для виджета MDI приведены в Таблице A-29.
Таблица A-29. Список избранных методов QMdiArea

Метод Описание
addSubWindow(widget) Добавляет виджет в качестве нового под-окна в область
MDI
activeSubWindow() Возвращает активное под-окно
cascadeSubWindow() Упорядочивает под-окна по каскадному принципу
tileSubWindows() Располагает под-окна в виде черепицы
removeSubWindow(widget) Удаляет виджет из области MDI, где виджет является
под-окном
setBackground(background) Устанавливает фон QBrush для области MDI
subWindowList(subwindows) Возвращает список под-окна
setTabsClosable(bool) Если значение True, то кнопки закрытия размещаются на
каждой вкладке в табличном представлении
setTabsMovable() Если значение True, то вкладки в представлении с
вкладками будут подвижными

ПРИЛОЖЕНИЕ СПРАВОЧНОЕ РУКОВОДСТВО ПО PYQT6 463


QtQuick и QML
Поскольку Qt и PyQt продолжают развиваться с каждой новой версией, все больше
внимания уделяется созданию более динамичных и плавных пользовательских
интерфейсов. Особенно это касается Qt 6 и PyQt6.
С помощью модулей QtQuick и QtQml разработчики могут использовать язык
моделирования Qt (QML) для создания пользовательских интерфейсов и компонентов.
QtQuick включает в себя ряд классов для построения холста для визуализации
графических компонентов, обработки пользовательского ввода, работы с данными
и графических эффектов, напоминающих мобильные приложения.
Обратите внимание, что QtQuick отличается от QtWidgets API, который мы
использовали на протяжении большей части этой книги. Синтаксис QML, который
использует QtQuick, основан на встроенном JavaScript. Используя PyQt, мы можем
создавать приложения, которые подключаются к коду QML с помощью Python. Во
многих случаях можно даже использовать такие классы, как QtCore и QtGui, для
взаимодействия с интерфейсом, построенным с помощью QML.
Есть две ссылки, которые могут помочь вам начать работу с
QtQuick. Первая - это документация Qt Quick компании Qt по адресу
https://doc.qt.io/qt-6/qtquick-index.html. Вторая - документация по Riverbank на
www.riverbankcomputing.com/static/Docs/PyQt6/qml.html#ref-integrating-qml.

Таблицы стилей Qt
Большой справочник по виджетам и свойствам, которыми
можно управлять с помощью таблиц стилей Qt, находится на сайте
https://doc.qt.io/qt-6/stylesheet-reference.html.
Таблицы стилей позволяют настраивать многие аспекты и поведение виджетов.
В табл. A-30 перечислены многие свойства, которые можно изменять. Виджеты
поддерживают только определенные свойства, поэтому, если вы не уверены в том,
какие свойства можно изменять, обязательно ознакомьтесь с документацией Qt.

464 ПРИЛОЖЕНИЕ СПРАВОЧНОЕ РУКОВОДСТВО ПО PYQT6


Таблица A-30. Список свойств, на которые можно влиять с помощью таблиц
стилей Qt

Метод Описание
alternate-background-color Альтернативный цвет фона для виджетов QAbstractItemView
QListView{
alternate-background-color: blue;
background: grey
}
Background Сокращенное обозначение для установки фона
background-color Цвет фона, используемый для виджета
QPushButton{
background-color: #49DE1F
}
background-image Фоновое изображение, используемое для виджета
QFrame{
background-image: url(images/black_cat.png)
}
Border Сокращение для установки границы виджета
QComboBox{
border: 2px solid magenta
}
border-top, border-right, Сокращение для указания сторон границы виджета
border-bottom, border-left
border-color Цвет для всех сторон границы виджета
border-image Указание изображения для заполнения границы
border-radius Радиус углов границы
QTextEdit{
border-width: 1px;
border-style: groove;
border-radius: 3px
}

(продолжение следует)

ПРИЛОЖЕНИЕ СПРАВОЧНОЕ РУКОВОДСТВО ПО PYQT6 465


Таблица A-30. (продолжение)

Метод Описание
border-style Определяет стиль для всех краёв границы
border-width Задает ширину для всех краёв границы
color Цвет, используемый для отрисовки текста
font Сокращение для определения шрифта виджета
QRadioButton{
font: bold italic large “Helvetica”
}
font-family, font-size, fontstyle, Другие свойства, используемые для индивидуальной
font-weight настройки характеристик шрифта
height, width Высота и ширина виджета
icon-size Ширина и высота значка виджета
image Изображение, рисуемое на виджете. Можно
использовать url или svg
margin Задает поля виджета. Как и в случае с border, можно
задать конкретные стороны
max-height, max-width Максимальная высота или ширина виджета
min-height, min-width Минимальная высота или ширина виджета
outline Контур рисует границу виджета. Можно также задать
цвет, стиль и радиус
padding Задает отступы виджета. Как и в случае с border, можно
задать конкретные стороны
selection-color Цвет переднего плана выделенных элементов в текст
spacing Устанавливает внутренний интервал в виджете
text-align Определяет выравнивание текста и значков внутри
виджета
QPushButton{
text-align: right
}

466 ПРИЛОЖЕНИЕ СПРАВОЧНОЕ РУКОВОДСТВО ПО PYQT6


Пространство имен Qt
На протяжении всей книги вы встречали множество перечислений и флагов,
позволяющих описывать или изменять параметры, состояния и внешний вид
виджетов. Класс Qt в модуле QtCore организует множество идентификаторов в
пространстве имен Qt. Пространство имен в языке C++ используется для организации
имен функций и переменных в логические группы с целью предотвращения ошибок.
Чтобы получить представление о том, насколько обширно пространство имен
Qt, загляните на сайт https://doc.qt.io/qt-6/qt.html. Там вы найдете перечисления,
относящиеся к выравниванию, стилю курсора, формату даты, областям виджетов
док-станции, кнопкам клавиатуры, состояниям окна и т.д.

Резюме
В ходе работы над этой книгой вы уже использовали многие из основополагающих
классов PyQt для построения графических интерфейсов пользователя. В Приложении
приведены ссылки, которые помогут вам проанализировать программы, приведенные
в этой книге, и узнать больше о виджетах, макетах и таблицах стилей, используемых
для проектирования и создания приложений PyQt. Содержащиеся здесь классы
и методы служат руководством к действию, позволяющим задуматься о способах
создания и совершенствования собственных программ.
Просто не хватит места, чтобы включить в это руководство все классы, методы и
сигналы. По мере выполнения примеров используйте данное приложение как ресурс,
помогающий узнать больше о возможностях PyQt. Если вы не нашли здесь ответа на
свой вопрос, перейдите по ссылкам, найдите его в Интернете или напишите мне по
электронной почте (автору книги, а не переводчику :))).
Счастливого кодирования!

ПРИЛОЖЕНИЕ СПРАВОЧНОЕ РУКОВОДСТВО ПО PYQT6 467


468 УКАЗАТЕЛЬ
УКАЗАТЕЛЬ 469

Вам также может понравиться