ээдд. Прохоренок_Н_А__Дронов_В_А_Python_3_и_PyQt_5_Разработка_приложен. Николай Прохоренок Владимир Дронов
Скачать 7.92 Mb.
|
( • Копировать компактно — копирование головоломки в буфер обмена в компактном формате (аналогичном тому, в котором сохраняется файл при выборе пункта Сохра- нить компактно меню Файл); • Копировать для Excel — копирование головоломки в буфер обмена в формате, предназначенном для вставки в таблицу Microsoft Excel; • Вставить ( Если головоломка была скопирована в компактном формате, все ячейки, содержащие цифры, будут автоматически заблокированы; • Вставить из Excel — вставка из буфера обмена головоломки, скопированной из таб- лицы Microsoft Excel; • Блокировать ( Глава 32. Приложение «Судоку» 755 • Блокировать все ( • Разблокировать ( • Разблокировать все ( меню Справка: • О программе — вывод окна со сведениями о приложении «Судоку»; • О Qt — вывод окна со сведениями о самой библиотеке Qt. При наведении курсора мыши на любой пункт меню в строке состояния появляется его раз- вернутое описание. В панели инструментов присутствуют кнопки (в порядке слева направо): Новый, Открыть, Сохранить , Печать, Предварительный просмотр, Копировать, Вставить, Блокировать все и Разблокировать все. Они выполняют те же действия, что и одноименные пункты меню. При наведении курсора мыши на кнопку панели инструментов в строке состояния появляется ее развернутое описание, а рядом с кнопкой спустя небольшой промежуток времени появляется всплывающая подсказка (на рис. 32.2 курсор мыши наведен на кнопку Сохранить ). Сохранение головоломок выполняется в текстовых файлах с расширением svd. Форматы, в которых хранятся головоломки, будут описаны позже. Окно приложения при закрытии сохраняет свое местоположение и впоследствии, после следующего запуска, восстанавливает его. 32.3. Программирование приложения Определившись, что должно делать наше приложение, мы можем начать его разработку. Но сначала выполним некоторые подготовительные действия. 32.3.1. Подготовительные действия Создадим где-либо каталог, в котором будут находиться все файлы нашего будущего при- ложения. Назовем его, скажем, sudoku . В этом каталоге создадим два вложенных каталога: images — для хранения значков, которые будут выводиться в пунктах меню и кнопках панели инструментов, а также значки самого приложения; modules — для хранения файлов с программными модулями Python, которые напишем впоследствии. Найдем в Интернете значки для представления самого приложения, пунктов меню и кнопок панели инструментов. Сохраним их в каталоге images под именами svd.png (значок прило- жения), а также new.png , open.png , save.png , print.png , preview.png , copy.png , paste.png , lock.png и unlock.png (значки кнопок). 32.3.2. Класс MyLabel: ячейка поля судоку Начнем мы разработку приложения с создания класса MyLabel , который будет представлять отдельную ячейку поля судоку, — 81 такой компонент и составят это поле. Наш класс должен «уметь» выводить на экран цифру, занесенную в ячейку, отображать обычное, активное и заблокированное состояния, реагировать на щелчки мышью, чтобы 756 Часть II. Библиотека PyQt 5 сообщить компоненту поля судоку, что та или иная ячейка стала активной. Помимо этого, ячейка должна иметь возможность принимать фоновый цвет — ведь именно разными цве- тами фона мы будем выделять группы ячеек на поле. Визуально ячейка должна иметь размеры 30 × 30 пикселов и выводить цифру, выровненную по середине без отступов от границ компонента. Бо´льшую часть необходимой функциональности мы можем получить, просто сделав класс MyLabel производным от класса надписи QLabel . В самом деле, чтобы вывести на экран цифру, можно просто задать ее в качестве содержимого надписи, воспользовавшись унасле- дованным методом setText() , — также несложно будет указать необходимые размеры и выравнивание. А задать для текста и фона нужные цвета мы сможем, привязав к ячейке таблицу стилей, для чего воспользуемся методом setStyleSheet() , опять же, унаследо- ванным. Теперь подумаем, какие атрибуты должен поддерживать класс MyLabel : colorYellow , colorOrange , colorGrey , colorBlack и colorRed — будут хранить RGB-коды, соответственно, желтого, оранжевого, светло-серого, черного и красного цветов. По- скольку значения этих атрибутов будут одинаковыми для всех экземпляров класса ячей- ки, мы сделаем их атрибутами объекта класса. (Если же сделать их атрибутами экземп- ляра класса, каждый экземпляр будет хранить свой собственный набор этих атрибутов, что приведет к избыточному расходу оперативной памяти.); isCellChange — значение True сообщит о том, что ячейка разблокирована, а значение False — что она заблокирована; fontColorCurrent — текущий цвет текста: черный или красный; bgColorDefault — заданный при создании ячейки цвет фона: оранжевый или светло- серый. Он понадобится нам, чтобы перевести ячейку из активного в неактивное состоя- ние; bgColorCurrent — текущий цвет фона: желтый, оранжевый или светло-серый; id — порядковый номер ячейки. Он понадобится нам, чтобы при щелчке мышью на ячейке сообщить компоненту поля судоку, какая ячейка стала активной. Компонент MyLabel будет входить в состав компонента поля, который мы напишем потом. Он должен уведомлять компонент поля, когда текущая ячейка становится активной после щелчка мышью. Наилучший способ сделать это — объявить сигнал, который будет генери- роваться при щелчке. Обрабатывая этот сигнал, поле всегда будет «в курсе», на какой ячей- ке был выполнен щелчок. Мы объявим в ячейке сигнал cellChangeFocus . Он будет передавать обработчику единст- венный параметр целочисленного типа — номер ячейки, на которой пользователь щелкнул мышью. Теперь определим набор необходимых методов нашего класса и представим в общих чер- тах, что должен делать каждый из них: mousePressEvent() — этот метод следует переопределить, чтобы получить возможность обрабатывать щелчки мышью. Внутри него мы будем генерировать сигнал cellChangeFocus ; showColorCurrent() — задаст для ячейки цвета текста и фона, взятые из атрибутов fontColorCurrent и bgColorCurrent соответственно, и тем самым обновит визуальное состояние ячейки, или, как говорят программисты, перерисует ее. Этот метод мы будем Глава 32. Приложение «Судоку» 757 вызывать после перевода ячейки из неактивного состояния в активное, из разблокиро- ванного — в заблокированное и наоборот; setCellFocus() — переведет ячейку из неактивного состояния в активное. Сделать это очень просто — мы занесем в атрибут bgColorCurrent код желтого цвета (который хранится в атрибуте объекта класса colorYellow ) и вызовем метод showColorCurrent() , чтобы обновить визуальное представление ячейки; clearCellFocus() — переведет ячейку из активного состояния в неактивное. Мы занесем в атрибут bgColorCurrent значение цвета фона, взятое из атрибута bgColorDefault (он, как мы помним, хранит код изначального цвета фона), и вызовем метод showColorCurrent() , который перерисует ячейку; setCellBlock() — переведет ячейку из разблокированного состояния в заблокирован- ное. Здесь мы занесем в атрибут isCellChange значение False , тем самым указывая, что ячейка заблокирована, присвоим атрибуту fontColorCurrent код красного цвета, храня- щийся в атрибуте объекта класса colorRed , и перерисуем ячейку вызовом метода showColorCurrent() ; clearCellBlock() — переведет ячейку из заблокированного состояния в разблокирован- ное. Для этого достаточно присвоить атрибуту isCellChange значение True , занести в ат- рибут fontColorCurrent значение черного цвета из атрибута объекта класса colorBlack и не забыть перерисовать ячейку с помощью метода showColorCurrent() ; setNewText() — занесет в ячейку новое число, переданное ему в качестве единственного параметра в виде строки. Здесь нужно предварительно проверить, не заблокирована ли ячейка (не хранится ли в атрибуте isCellChange значение False ), и уже потом вызывать унаследованный от класса QLabel метод setText() ; конструктор должен принимать в качестве обязательных параметров номер создаваемой ячейки (он будет занесен в атрибут id ), цвет ее фона (его мы присвоим атрибутам bgColorDefault и bgColorCurrent ), а также необязательный параметр родителя parent Еще он должен присвоить атрибуту isCellChange значение True — вновь созданная ячейка изначально должна быть разблокирована. Напоследок он перерисует ячейку, вы- звав метод showColorCurrent() Код класса MyLabel относительно невелик, несложен и полностью приведен в листинге 32.1. Его следует сохранить в файле mylabel.py в каталоге modules Э ЛЕКТРОННЫЙ АРХИВ Напомним, что файлы с кодами разрабатываемого в этой главе приложения находятся в папке sudoku сопровождающего книгу электронного архива (см. приложение). Листинг 32.1. Класс MyLabel from PyQt5 import QtCore, QtWidgets class MyLabel(QtWidgets.QLabel): colorYellow = "#FFFF90" colorOrange = "#F5D8C1" colorGrey = "#E8E8E8" colorBlack = "#000000" colorRed = "#D77A38" 758 Часть II. Библиотека PyQt 5 changeCellFocus = QtCore.pyqtSignal(int) def __init__(self, id, bgColor, parent=None): QtWidgets.QLabel.__init__(self, parent) self.setAlignment(QtCore.Qt.AlignCenter) self.setFixedSize(30, 30) self.setMargin(0) self.setText("") if id < 0 or id > 80: id = 0 self.id = id self.isCellChange = True self.fontColorCurrent = self.colorBlack self.bgColorDefault = bgColor self.bgColorCurrent = bgColor self.showColorCurrent() def mousePressEvent(self, evt): self.changeCellFocus.emit(self.id) QtWidgets.QLabel.mousePressEvent(self, evt) def showColorCurrent(self): self.setStyleSheet("background-color:" + self.bgColorCurrent + ";color:" + self.fontColorCurrent + ";") def setCellFocus(self): self.bgColorCurrent = self.colorYellow self.showColorCurrent() def clearCellFocus(self): self.bgColorCurrent = self.bgColorDefault self.showColorCurrent() def setCellBlock(self): self.isCellChange = False self.fontColorCurrent = self.colorRed self.showColorCurrent() def clearCellBlock(self): self.isCellChange = True self.fontColorCurrent = self.colorBlack self.showColorCurrent() def setNewText(self, text): if self.isCellChange: self.setText(text) В принципе, объяснять здесь более особо нечего — все было рассказано ранее. Нужно лишь отметить пару чисто технических деталей: в методе mousePressEvent() , выполнив все необходимые действия, а именно, сгенериро- вав сигнал changeCellFocus , мы в обязательном порядке вызываем тот же метод супер- класса. Если этого не сделать, возможны проблемы; Глава 32. Приложение «Судоку» 759 в методе showColorCurrent() мы вызовом метода setStyleSheet() привязываем к наше- му компоненту таблицу стилей, которая установит для ячейки цвета текста и фона. (Прочие параметры для компонента мы установим в таблице стилей, привязанной к компоненту основного окна, которую создадим позже.) 32.3.3. Класс Widget: поле судоку Класс Widget представит само поле судоку, составленное из 81-го компонента MyLabel , написанного ранее, и набора из 10-ти обычных кнопок, с помощью которых пользователь будет вставлять цифры в ячейки. Сразу после создания компонент Widget должен вывести на экран поле судоку и набор кно- пок. Он даст пользователю возможность делать ячейки активными с помощью клавиш- стрелок (активизация посредством щелчка мышью уже реализована нами в классе MyLabel ), устанавливать в ячейки цифры либо клавишами, либо кнопками из набора и удалять цифры, опять же, клавишами или специальной кнопкой. Это основная функциональность компо- нента. Что касается дополнительной функциональности, то ее мы будем реализовывать по частям. И сейчас мы сделаем лишь очистку поля, блокировку и разблокировку его ячеек. Поскольку функциональности, специфической для какого-либо уже имеющегося в библио- теке компонента, нам не требуется, класс Widget мы сделаем производным от класса QWidget Необходимый нам набор атрибутов класса очень невелик: cells — массив ячеек поля судоку — экземпляров класса MyLabel . Мы сохраним этот массив в атрибуте, поскольку в других методах этого класса нам понадобится получать к нему доступ; idCellInFocus — порядковый номер ячейки, являющейся активной в настоящий момент. Его нужно знать в любом случае — хотя бы для того, чтобы визуально выделить актив- ную ячейку, вызвав у нее метод setCellFocus() (см. разд. 32.3.2). Набор методов, поддерживаемый классом Widget , будет более объемным (даже с учетом того, что мы еще не реализовали в классе дополнительную функциональность). Поскольку эти методы сложнее таковых у класса MyLabel , мы рассмотрим их вкратце, не углубляясь в технические детали — полное их описание будет приведено далее, вместе с их кодом: onChangeCellFocus() — обработчик сигналов changeCellFocus всех ячеек, что имеются в поле. (Упомянутый здесь сигнал, как мы помним, генерируется при щелчке на ячейке мышью.) Он получит с единственным параметром номер ячейки, на которой был выпол- нен щелчок, и активизирует ее; keyPressEvent() — этот метод следует переопределить, чтобы получить возможность обрабатывать нажатия клавиш клавиатуры. Здесь мы будем, в зависимости от нажатой клавиши, перемещать по полю фокус выделения, ставить цифры в ячейки и очищать их; onBtn (где — число от 0 до 8 или буква X ) — обработчики щелчков на кнопках 1...9 и Х. Они будут ставить в ячейку соответствующую цифру или же очищать ячейку; onClearAllCells() — очистит поле судоку. Будет вызываться при выборе пункта Новый меню Файл или нажатии кнопки Новый панели инструментов; onBlockCell() — заблокирует активную ячейку, если она содержит цифру и еще не за- блокирована. Будет вызываться при выборе пункта Заблокировать меню Правка; 760 Часть II. Библиотека PyQt 5 onBlockCells() — заблокирует все ячейки, содержащие цифры и не заблокированные. Будет вызываться при выборе пункта Заблокировать все меню Правка или нажатии кнопки Заблокировать все панели инструментов; onClearBlockCell() — разблокирует активную ячейку, если она заблокирована. Будет вызываться при выборе пункта Разблокировать меню Правка; onClearBlockCells() — разблокирует все заблокированные ячейки. Будет вызываться при выборе пункта Разблокировать все меню Правка или нажатии кнопки Разблоки- ровать все панели инструментов; конструктор, который создаст все необходимые компоненты и выполнит привязку обра- ботчиков к сигналам. Весь код класса Widget мы сохраним в файле widget.py в каталоге modules . Поскольку этот код довольно велик, мы рассмотрим его по частям. 32.3.3.1. Конструктор класса Widget Конструктор — самый сложный метод класса Widget . Поэтому мы не станем приводить его полный листинг, а рассмотрим его по фрагментам. from PyQt5 import QtCore, QtGui, QtWidgets Помимо модулей QtCore и QtWidgets , которые понадобятся нам уже сейчас, мы импортиру- ем модуль QtGui . Объявленные в нем классы пригодятся нам позже, при реализации печати. from modules.mylabel import MyLabel Не забываем импортировать из модуля mylabel.py , что хранится в каталоге modules , класс MyLabel , представляющий отдельную ячейку и написанный нами ранее. class Widget(QtWidgets.QWidget): def __init__(self, parent=None): QtWidgets.QWidget.__init__(self, parent) self.setFocusPolicy(QtCore.Qt.StrongFocus) По умолчанию экземпляр класса QWidget или производного от него класса не может прини- мать фокус ввода. Чтобы дать ему возможность принимать фокус ввода при щелчке мышью и переходе нажатием клавиши , передав ему в качестве параметра атрибут StrongFocus vBoxMain = QtWidgets.QVBoxLayout() Поскольку само поле и набор кнопок будут располагаться друг над другом, мы используем для их размещения контейнер QVBoxLayout frame1 = QtWidgets.QFrame() frame1.setStyleSheet( "background-color:#9AA6A7;border:1px solid #9AA6A7;") Поле (которое будет создано контейнером-сеткой QGridLayout ) мы поместим в панель с рамкой QFrame . Для этой панели с помощью таблицы стилей укажем тонкую рамку и фон одинакового темно-серого цвета. Панель с рамкой займет все выделенное под него про- странство контейнера, а поле судоку поместится в центре этой панели. В результате этого будет казаться, что поле окружено толстой темно-серой рамкой, что выглядит весьма эффектно. grid = QtWidgets.QGridLayout() grid.setSpacing(0) Глава 32. Приложение «Судоку» 761 Создаем сетку QGridLayout , которая сформирует само поле. idColor = (3, 4, 5, 12, 13, 14, 21, 22, 23, 27, 28, 29, 36, 37, 38, 45, 46, 47, 33, 34, 35, 42, 43, 44, 51, 52, 53, 57, 58, 59, 66, 67, 68, 75, 76, 77) Объявляем массив, хранящий номера ячеек, которые должны быть выделены светло-серым фоном. self.cells = [MyLabel(i, MyLabel.colorGrey if i in idColor else MyLabel.colorOrange) for i in range(0, 81)] Создаем список из 81-й ячейки MyLabel , сохранив его в атрибуте cells класса Widget . Здесь мы используем выражение генератора списка, которое позволит нам радикально упростить код. Если номер создаваемой ячейки имеется в объявленном ранее массиве, задаем для нее светло-серый цвет фона, в противном случае — оранжевый. self.cells[0].setCellFocus() self.idCellInFocus = 0 |