ээдд. Прохоренок_Н_А__Дронов_В_А_Python_3_и_PyQt_5_Разработка_приложен. Николай Прохоренок Владимир Дронов
Скачать 7.92 Mb.
|
Делаем активной ячейку с номером 0 и заносим тот же номер в атрибут idCellInFocus клас- са Widget . В результате изначально активной станет самая первая ячейка поля. i = 0 for j in range(0, 9): for k in range(0, 9): grid.addWidget(self.cells[i], j, k) i += 1 Помещаем все созданные ячейки в сетку. for cell in self.cells: cell.changeCellFocus.connect(self.onChangeCellFocus) У всех ячеек задаем для сигнала changeCellFocus обработчик — метод onChangeCellFocus() класса Widget , пока еще не объявленный. frame1.setLayout(grid) vBoxMain.addWidget(frame1, alignment=QtCore.Qt.AlignHCenter) Помещаем сетку в панель с рамкой и добавляем последнюю в контейнер VBoxLayout , указав для нее горизонтальное выравнивание по середине. frame2 = QtWidgets.QFrame() frame2.setFixedSize(272, 36) Набор кнопок, с помощью которых будет выполняться занесение цифр в ячейки, мы помес- тим в другую панель с рамкой QFrame . Это позволит нам привязать к кнопкам таблицу сти- лей, задающую для них представление. Для панели мы обязательно зададим фиксированные размеры — иначе их установит сам PyQt согласно своему разумению, которое вряд ли сов- падет с нашим. hbox = QtWidgets.QHBoxLayout() hbox.setSpacing(1) Кнопки у нас будут выстроены по горизонтали, следовательно, наилучший вариант — по- местить их в контейнер QHBoxLayout btns = [] for i in range(1, 10): btn = QtWidgets.QPushButton(str(i)) 762 Часть II. Библиотека PyQt 5 btn.setFixedSize(27, 27) btn.setFocusPolicy(QtCore.Qt.NoFocus) btns.append(btn) Создаем кнопки 1...9, задаем для них размеры 27 × 27 пикселов и добавляем в специально созданный для этого список. Также для каждой кнопки мы указываем, что она не должна принимать фокус ввода (для чего вызовем у нее метод setFocusPolicy() с параметром NoFocus ), — это нужно для того, чтобы поле судоку при нажатии любой из этих кнопок не теряло фокус, и пользователь смог продолжать манипулировать в нем с помощью клавиш. btn = QtWidgets.QPushButton("X") btn.setFixedSize(27, 27) btns.append(btn) Таким же образом создаем кнопку Х, которая уберет цифру из ячейки. for btn in btns: hbox.addWidget(btn) Помещаем все кнопки в контейнер QHBoxLayout btns[0].clicked.connect(self.onBtn0Clicked) btns[1].clicked.connect(self.onBtn1Clicked) btns[2].clicked.connect(self.onBtn2Clicked) btns[3].clicked.connect(self.onBtn3Clicked) btns[4].clicked.connect(self.onBtn4Clicked) btns[5].clicked.connect(self.onBtn5Clicked) btns[6].clicked.connect(self.onBtn6Clicked) btns[7].clicked.connect(self.onBtn7Clicked) btns[8].clicked.connect(self.onBtn8Clicked) btns[9].clicked.connect(self.onBtnXClicked) Привязываем к сигналам clicked всех этих кнопок соответствующие обработчики — мето- ды класса поля, которые объявим потом. frame2.setLayout(hbox) vBoxMain.addWidget(frame2, alignment=QtCore.Qt.AlignHCenter) Помещаем контейнер с кнопками в панель с рамкой, а ее — во «всеобъемлющий» контей- нер VBoxLayout , не забыв указать горизонтальное выравнивание по середине. self.setLayout(vBoxMain) И помещаем этот контейнер в компонент поля. 32.3.3.2. Прочие методы класса Widget Теперь напишем код остальных методов класса Widget . Они существенно проще конструк- тора, и мы можем не рассматривать их по частям. Листинг 32.2. Метод onChangeCellFocus() def onChangeCellFocus(self, id): if self.idCellInFocus != id and not (id < 0 or id > 80): self.cells[self.idCellInFocus].clearCellFocus() self.idCellInFocus = id self.cells[id].setCellFocus() Глава 32. Приложение «Судоку» 763 Метод onChangeCellFocus() станет обработчиком сигнала changeCellFocus ячейки MyLabel В качестве единственного параметра он получит номер ячейки, ставшей активной. Сначала мы проверим, не совпадает ли полученный номер с тем, что хранится в атрибуте idCellInFocus (не щелкнул ли пользователь на активной ячейке), не меньше ли он 0 и не больше ли 80 (т. е. не вышел ли он за диапазон номеров ячеек). Если это так, мы выполняем операцию по переносу фокуса на ячейку с полученным в параметре номером. Предварительно нам следует деактивировать ячейку, бывшую активной ранее (номер этой ячейки в настоящий момент хранится в атрибуте idCellInFocus ). Извлекаем номер, получа- ем из списка ячеек (он хранится в атрибуте cells ) саму эту ячейку и переводим ее в неак- тивное состояние вызовом метода clearCellFocus() . Далее мы заносим полученный с пара- метром номер в атрибут idCellInFocus , тем самым указывая, что ячейка с этим номером сейчас активна, и делаем ее активной, вызвав у нее метод setCellFocus() Листинг 32.3. Метод keyPressEvent() def keyPressEvent(self, evt): key = evt.key() if key == QtCore.Qt.Key_Up: tid = self.idCellInFocus - 9 if tid < 0: tid += 81 self.onChangeCellFocus(tid) elif key == QtCore.Qt.Key_Right: tid = self.idCellInFocus + 1 if tid > 80: tid -= 81 self.onChangeCellFocus(tid) elif key == QtCore.Qt.Key_Down: tid = self.idCellInFocus + 9 if tid > 80: tid -= 81 self.onChangeCellFocus(tid) elif key == QtCore.Qt.Key_Left: tid = self.idCellInFocus - 1 if tid < 0: tid += 81 self.onChangeCellFocus(tid) elif key >= QtCore.Qt.Key_1 and key <= QtCore.Qt.Key_9: self.cells[self.idCellInFocus].setNewText(chr(key)) elif key == QtCore.Qt.Key_Delete or key == QtCore.Qt.Key_Backspace or key == QtCore.Qt.Key_Space: self.cells[self.idCellInFocus].setNewText("") QtWidgets.QWidget.keyPressEvent(self, evt) Переопределенный метод keyPressEvent() будет обрабатывать нажатия клавиш. В качестве параметра он получит экземпляр класса, представляющий событие клавиатуры. Мы сразу же вызовем у этого экземпляра метод key() , чтобы получить код нажатой клавиши. После чего начнем последовательно сравнивать его с кодами различных клавиш, чтобы выяснить, какая из них была нажата. 764 Часть II. Библиотека PyQt 5 Если была нажата клавиша <↑>, следует сделать активной ячейку, расположенную строкой выше. Мы можем с легкостью получить номер этой ячейки, вычтя из номера активной ячейки (он, как мы знаем, хранится в атрибуте idCellInFocus ) число 9 — т. е. количество ячеек, помещающихся в строке поля. Если получившаяся разность меньше 0 (фокус выде- ления вышел за пределы верхней границы поля), мы прибавляем к разности 81 (количество ячеек в поле), в результате чего фокус окажется на самой последней строке поля, в том же столбце. И, наконец, вызываем метод onChangeCellFocus() класса поля, передав ему резуль- тирующий номер ячейки, чтобы сделать ее активной. Теперь рассмотрим случай, когда была нажата клавиша <→>. Нам нужно сделать активной ячейку, расположенную правее. Понятно, что для получения ее номера нам следует приба- вить к номеру активной ячейки единицу. Если же полученная сумма оказалась больше 80 (фокус выделения вышел за пределы верхней границы диапазона имеющихся ячеек), мы вычтем из нее то же число 81 — тогда фокус окажется на самой первой ячейке в поле. И не забываем напоследок вызвать метод onChangeCellFocus() Обработка нажатия клавиш <↓> и <←> производится аналогично. Вы можете сами разо- браться, как работает выполняющий ее код. Если была нажата клавиша <1>...<9>, мы формируем на основе ее кода соответствующий символ, воспользовавшись функцией chr() , вызываем у активной ячейки метод setNewText() и передаем ему этот символ. Так мы занесем в активную ячейку цифру, соот- ветствующую нажатой клавише. Случай нажатия клавиш , передав ему пустую строку — так мы уберем цифру с ячейки. В самом конце, какая бы ни была нажата клавиша, мы в обязательном порядке вызываем метод keyPressEvent() базового класса. Если этого не сделать, приложение может повести себя непредсказуемо. Листинг 32.4. Методы onBtn0Clicked()...onBtn8Clicked() и onBtnXClicked() def onBtn0Clicked(self): self.cells[self.idCellInFocus].setNewText("1") def onBtn1Clicked(self): self.cells[self.idCellInFocus].setNewText("2") def onBtn2Clicked(self): self.cells[self.idCellInFocus].setNewText("3") def onBtn3Clicked(self): self.cells[self.idCellInFocus].setNewText("4") def onBtn4Clicked(self): self.cells[self.idCellInFocus].setNewText("5") def onBtn5Clicked(self): self.cells[self.idCellInFocus].setNewText("6") def onBtn6Clicked(self): self.cells[self.idCellInFocus].setNewText("7") Глава 32. Приложение «Судоку» 765 def onBtn7Clicked(self): self.cells[self.idCellInFocus].setNewText("8") def onBtn8Clicked(self): self.cells[self.idCellInFocus].setNewText("9") def onBtnXClicked(self): self.cells[self.idCellInFocus].setNewText("") Методы onBtn0Clicked() onBtn8Clicked() и onBtnXClicked() выполнят занесение в ячейку цифры, соответствующей нажатой кнопке 1...9, или удаление цифры, если была нажата кнопка Х. Как они работают, понятно без дополнительных пояснений. Листинг 32.5. Метод onClearAllCells() def onClearAllCells(self): for cell in self.cells: cell.setText("") cell.clearCellBlock() Метод onClearAllCells() очистит поле судоку. В нем мы перебираем все имеющиеся в по- ле ячейки, каждую очищаем от занесенной в нее цифры и переводим в разблокированное состояние вызовом метода clearCellBlock() класса MyLabel Листинг 32.6. Метод onBlockCell() def onBlockCell(self): cell = self.cells[self.idCellInFocus] if cell.text() == "": QtWidgets.QMessageBox.information(self, "Судоку", "Нельзя блокировать пустую ячейку") else: if cell.isCellChange: cell.setCellBlock() Метод onBlockCell() будет блокировать активную ячейку. Сначала он проверит, есть ли в ней цифра (получить ее можно вызовом унаследованного от суперкласса QLabel метода text() ), поскольку блокировать можно только ячейки с цифрами. Если в ячейке нет цифры, на экран будет выведено информационное окно с соответствующим предупреждением. В противном случае будет выполнена проверка, хранится ли в атрибуте isCellChange бло- кируемой ячейки значение True (т. е. не заблокирована ли уже эта ячейка), и, если это так, ячейка блокируется вызовом метода setCellBlock() класса MyLabel В этом методе мы предварительно извлекаем из списка активную ячейку, сохраняем ее в переменной и в дальнейшем используем для доступа к активной ячейке именно эту пере- менную. Такой подход в случае, если нужно несколько раз обращаться к значению какого- либо атрибута класса или элемента списка, позволяет несколько повысить быстродействие, поскольку обращение к переменной выполняется быстрее, чем к атрибуту класса или эле- менту списка. 766 Часть II. Библиотека PyQt 5 Листинг 32.7. Метод onBlockCells() def onBlockCells(self): for cell in self.cells: if cell.text() and cell.isCellChange: cell.setCellBlock() Метод onBlockCells() , блокирующий все ячейки, выполняет перебор всех ячеек и блокиру- ет любую из них, если она содержит цифру и еще не заблокирована. Листинг 32.8. Метод onClearBlockCell() def onClearBlockCell(self): cell = self.cells[self.idCellInFocus] if not cell.isCellChange: cell.clearCellBlock() Метод onClearBlockCell() , предназначенный для разблокирования активной ячейки, пред- варительно проверит, хранится ли в ее атрибуте isCellChange значение False (т. е. заблоки- рована ли эта ячейка). И только после этого он вызывает метод clearCellBlock() класса MyLabel , чтобы разблокировать ячейку. Листинг 32.9. Метод onClearBlockCells() def onClearBlockCells(self): for cell in self.cells: if not cell.isCellChange: cell.clearCellBlock() Метод onClearBlockCells() , разблокирующий все ячейки поля, очень прост, и вы, уважае- мые читатели, сами поймете, как он работает. 32.3.4. Класс MainWindow: основное окно приложения Класс MainWindow представляет основное окно приложения «Судоку». Это окно включает компонент Widget , т. е. поле судоку, главное меню, панель инструментов и строку состоя- ния. Класс основного окна, в силу его сложности, мы также будем писать по частям. В настоя- щий момент мы реализуем в нем только часть всех функций приложения: операции очистки поля, выхода из приложения, блокировки и разблокировки ячеек и получения справочных сведений. Мы также реализуем сохранение и восстановление местоположения окна и нач- нем разработку функции печати. Остальная функциональность будет добавлена потом. Класс MainWindow мы сделаем производным от класса главного окна MainWindow . Это позво- лит нам без проблем разместить в окно поле судоку, создать главное меню, панель инстру- ментов и строку состояния. Подумаем, какие атрибуты мы объявим в классе основного окна: sudoku — экземпляр класса Widget , представляющий компонент поля судоку. Нам при- дется работать с этим компонентом в других методах класса MainWindow ; Глава 32. Приложение «Судоку» 767 settings — экземпляр класса QSettings , предназначенный для загрузки и сохранения настроек приложения. Мы применим здесь подход, показанный в листинге 31.2, при ко- тором сохранение настроек выполняется в методе closeEvent() ; printer — экземпляр класса QPrinter , представляющий принтер. Мы сразу же создадим его в конструкторе класса окна, чтобы потом не вносить в код конструктора слишком много правок. Что касается методов класса MainWindow , то мы объявим всего три: closeEvent() — этот метод следует переопределить, если нужно выполнять какие-либо действия непосредственно перед закрытием окна. Внутри его мы выполним сохранение местоположения окна; aboutInfo() — выведет на экран стандартное диалоговое окно со сведениями о прило- жении «Судоку»; конструктор сформирует интерфейс приложения, загрузит и установит сохраненное ранее местоположение окна, создаст принтер, а также привяжет к окну таблицу стилей, которая укажет специфическое оформление для ячеек поля судоку и набора кнопок, который находится ниже поля. Весь код класса MainWindow мы сохраним в файле mainwindow.py в каталоге modules . Его мы также рассмотрим по частям. 32.3.4.1. Конструктор класса MainWindow И в этом случае конструктор — самый сложный метод рассматриваемого класса. Разберем его по фрагментам. from PyQt5 import QtCore, QtGui, QtWidgets, QtPrintSupport Не забываем импортировать все нужные модули, включая модуль QtPrintSupport from modules.widget import Widget Импортируем класс поля судоку Widget из модуля widget.py , который мы сохранили в ката- логе modules class MainWindow(QtWidgets.QMainWindow): def __init__(self, parent=None): QtWidgets.QMainWindow.__init__(self, parent, flags=QtCore.Qt.Window | QtCore.Qt.MSWindowsFixedSizeDialogHint) self.setWindowTitle("Судоку 2.0.0") Для создаваемого окна указываем флаг MSWindowsFixedSizeDialogHint , запрещающий изме- нение его размеров. В самом деле, поле судоку у нас имеет фиксированный размер, и, если пользователь получит возможность изменять размеры окна, это будет выглядеть странно. self.setStyleSheet( "QFrame QPushButton {font-size:10pt;font-family:Verdana;" "color:black;font-weight:bold;}" "MyLabel {font-size:14pt;font-family:Verdana;" "border:1px solid #9AA6A7;}") Указываем для окна таблицу стилей, которая задаст представление для следующих элемен- тов управления: для кнопок из набора, расположенного под полем судоку, — черный полужирный шрифт Verdana размером 10 пунктов (так мы сделаем эти кнопки заметнее); 768 Часть II. Библиотека PyQt 5 для ячеек поля судоку — шрифт Verdana размером 14 пунктов и темно-серую сплошную рамку толщиной в 1 пиксел. self.settings = QtCore.QSettings("Прохоренок и Дронов", "Судоку") self.printer = QtPrintSupport.QPrinter() Создаем объекты хранилища настроек и принтера. self.sudoku = Widget() self.setCentralWidget(self.sudoku) Создаем экземпляр класса Widget , сохраняем его в атрибуте sudoku и помещаем в окно в качестве центрального. menuBar = self.menuBar() toolBar = QtWidgets.QToolBar() Получаем доступ к уже имеющемуся в окне главному меню и создаем панель инструмен- тов. myMenuFile = menuBar.addMenu("&Файл") Создаем меню Файл. action = myMenuFile.addAction(QtGui.QIcon(r"images/new.png"), "&Новый", self.sudoku.onClearAllCells, QtCore.Qt.CTRL + QtCore.Qt.Key_N) Создаем пункт Новый меню Файл. Для создания пунктов меню мы используем разновидность метода addAction() класса QMenu , которая в качестве параметров принимает значок, название пункта, обработчик и комбинацию клавиш. На основе всего этого метод формирует действие (экземпляр класса QAction ), создает связанный с ним пункт меню и возвращает это действие в качестве ре- зультата (подробности — в разд. 27.2.2). Мы сохраним полученное действие в переменной, чтобы впоследствии создать на панели инструментов связанную с ним кнопку и задать для него текст подсказки, выводящийся в строке состояния. В качестве обработчика для этого действия мы указываем метод onClearAllCells() компо- нента поля судоку (класса Widget ). toolBar.addAction(action) action.setStatusTip("Создание новой, пустой головоломки") Создаем на основе только что подготовленного действия кнопку Новый панели инструмен- тов и задаем для действия текст подсказки, выводящейся в строке состояния. myMenuFile.addSeparator() toolBar.addSeparator() action = myMenuFile.addAction("&Выход", QtWidgets.qApp.quit, QtCore.Qt.CTRL + QtCore.Qt.Key_Q) action.setStatusTip("Завершение работы приложения") Добавляем в меню и на панель инструментов разделители и точно таким же образом созда- ем пункт Выход меню Файл. В качестве обработчика указываем метод quit() приложения. myMenuEdit = menuBar.addMenu("&Правка") action = myMenuEdit.addAction("&Блокировать", self.sudoku.onBlockCell, QtCore.Qt.Key_F2) action.setStatusTip("Блокирование активной ячейки") Глава 32. Приложение «Судоку» 769 action = myMenuEdit.addAction(QtGui.QIcon(r"images/lock.png"), "Б&локировать все", self.sudoku.onBlockCells, QtCore.Qt.Key_F3) toolBar.addAction(action) action.setStatusTip("Блокирование всех ячеек") action = myMenuEdit.addAction("&Разблокировать", self.sudoku.onClearBlockCell, 9>1> |