ээдд. Прохоренок_Н_А__Дронов_В_А_Python_3_и_PyQt_5_Разработка_приложен. Николай Прохоренок Владимир Дронов
Скачать 7.92 Mb.
|
[^0-9] ), и выполняет поиск в строке с данными посредством быстро выполняющегося метода match() . (Более подробно о работе с регулярными выраже- ниями рассказывалось в главе 7.) Глава 32. Приложение «Судоку» 777 И только если все проверки выполнены, вызывается метод setDataAllCells() , и ему пере- дается строка с данными для вставки. После чего сразу же выполняется выход из метода. Если же какая-либо проверка завершилась неудачей, будет выполнено самое последнее вы- ражение — вызов метода dataErrorMsg() , выводящего сообщение об ошибке. Мы напишем этот метод потом. Листинг 32.20. Метод onPasteDataExcel() def onPasteDataExcel(self): data = QtWidgets.QApplication.clipboard().text() if data: data = data.replace("\r", "") r = re.compile(r"([0-9]?[\t\n]){81}") if r.match(data): result = [] if data[-1] == "\n": data = data[:-1] dl = data.split("\n") for sl in dl: dli = sl.split("\t") for sli in dli: if len(sli) == 0: result.append("00") else: result.append("0" + sli[0]) data = "".join(result) self.sudoku.setDataAllCells(data) return self.dataErrorMsg() Метод onPasteDataExcel() вставит из буфера обмена данные, представленные в формате для Excel, разумеется, также выполнив необходимые проверки (листинг 32.20). Сначала он убедится, что данные для вставки есть, и для удобства их дальнейшей проверки и обработки удалит из них символы возврата каретки (для этого можно использовать метод replace() класса str , указав у этого метода первым параметром удаляемый символ, а вторым — пус- тую строку). Полученная нами строка представляет собой набор из строго 81-й комбинации двух симво- лов: цифры от 0...9, которая может присутствовать в единственном числе или отсутство- вать, и символа табуляции или перевода строки. Это правило прекрасно формализуется регулярным выражением ([0-9]?[\t\n]){81} . Мы сравниваем с ним строку с данными и выполняем дальнейшие манипуляции, только если сравнение выполняется. Сначала подготавливаем пустой список, в который будем помещать отдельные строки — фрагменты вставляемой головоломки. Удаляем из полученной строки с данными завер- шающий символ перевода строки, если он там есть. Разбиваем эту строку по символам пе- ревода строки, воспользовавшись методом split() класса str , и получаем список строк, представляющих отдельные строки поля судоку. Перебираем этот список и каждую из имеющихся в нем строк тем же методом разбиваем по символам табуляции, получив список строк, каждая из которых представляет сведения об одной ячейке. Перебираем этот список. 778 Часть II. Библиотека PyQt 5 Если очередной его элемент-строка пуст (т. е. ячейка не имеет цифры), добавляем в список строку "00" , где первая цифра обозначает, что ячейка не заблокирована, а вторая — отсут- ствие цифры в ячейке. Если же элемент не пуст, значит, он представляет собой цифру, которую следует занести в ячейку, и мы добавляем в список строку вида "0<Эта цифра> Наконец, объединяем все элементы списка в строку, передаем ее методу setDataAllCells() класса поля судоку и выполняем возврат из метода. Последнее выражение, выполняющееся, если какая-либо проверка из описанных ранее за- вершилась неудачей, вызовет все тот же метод dataErrorMsg() , который выведет сообщение о неправильном формате данных. Осталось написать этот метод. Его код приведен в листинге 32.21 — как видим, он очень прост. Листинг 32.21. Метод dataErrorMsg() def dataErrorMsg(self): QtWidgets.QMessageBox.information(self, "Судоку", "Данные имеют неправильный формат") Запустим приложение и проверим, как работает копирование и вставка данных в разных форматах. Проще всего сделать это, занеся цифры в некоторые ячейки поля судоку, выпол- нив копирование в каком-либо формате, очистив поле и произведя вставку. После этого поле судоку должно выглядеть так же, как перед копированием. 32.3.7. Сохранение и загрузка данных Настала пора заняться средствами для сохранения головоломок в файлах и загрузки их оттуда. Сохранять головоломки мы будем в тех же форматах, в каких они копировались в буфер обмена, — это позволит нам использовать написанные в разд. 32.3.6.2 методы клас- са Widget , выполняющие копирование данных. Чтобы дать нашему приложению возможность сохранять и загружать данные, мы добавим в класс MainWindow следующие методы: onOpenFile() — загрузит сохраненную в файле головоломку; onSave() — сохранит головоломку в файл в полном формате; onSaveMini() — сохранит головоломку в файл в компактном формате; saveSVDFile() — этот метод будет вызываться обоими предыдущими методами для вы- полнения собственно сохранения данных в файл. Эти данные он будет получать с един- ственным параметром. И внесем исправления в код конструктора — их можно увидеть в листинге 32.22 (добавлен- ный код выделен полужирным шрифтом). Листинг 32.22. Конструктор (дополнения) def __init__(self, parent=None): action = myMenuFile.addAction(QtGui.QIcon(r"images/new.png"), "&Новый", self.sudoku.onClearAllCells, QtCore.Qt.CTRL + QtCore.Qt.Key_N) Глава 32. Приложение «Судоку» 779 toolBar.addAction(action) action.setStatusTip("Создание новой, пустой головоломки") action = myMenuFile.addAction(QtGui.QIcon(r"images/open.png"), "&Открыть...", self.onOpenFile, QtCore.Qt.CTRL + QtCore.Qt.Key_O) toolBar.addAction(action) action.setStatusTip("Загрузка головоломки из файла") action = myMenuFile.addAction(QtGui.QIcon(r"images/save.png"), "Со&хранить...", self.onSave, QtCore.Qt.CTRL + QtCore.Qt.Key_S) toolBar.addAction(action) action.setStatusTip("Сохранение головоломки в файле") action = myMenuFile.addAction("&Сохранить компактно...", self.onSaveMini) action.setStatusTip( "Сохранение головоломки в компактном формате") myMenuFile.addSeparator() toolBar.addSeparator() Этот код добавит в меню Файл, между пунктом Новый и разделителем, пункты Открыть, Сохранить и Сохранить компактно. В качестве обработчиков указаны описанные ранее методы. Также выполняется добавление еще двух кнопок в панель инструментов. Листинг 32.23. Метод onOpenFile() def onOpenFile(self): fileName = QtWidgets.QFileDialog.getOpenFileName(self, "Выберите файл", QtCore.QDir.homePath(), "Судоку (*.svd)")[0] if fileName: data = "" try: with open(fileName, newline="") as f: data = f.read() except: QtWidgets.QMessageBox.information(self, "Судоку", "Не удалось открыть файл") return if len(data) > 2: if data[-1] == "\n": data = data[:-1] if len(data) == 81 or len(data) == 162: r = re.compile(r"[^0-9]") 780 Часть II. Библиотека PyQt 5 if not r.match(data): self.sudoku.setDataAllCells(data) return self.dataErrorMsg() В методе onOpenFile() , загружающем данные из файла (листинг 32.23), мы выводим стан- дартное диалоговое окно открытия файла, указав в качестве начального каталог пользова- тельского профиля. Если пользователь выбрал файл и нажал кнопку Открыть, мы в блоке обработки исключения открываем этот файл для чтения и читаем его содержимое. Если файл прочитать не удалось, и было сгенерировано исключение, мы выводим соответствую- щее сообщение и выполняем возврат из метода. Если данные были прочитаны, мы проверяем, имеют ли они длину 81 или 162 символа и не включают ли в себя символы, отличные от цифр 0...9. Если это так, мы передаем загружен- ные данные все тому же методу setDataAllCells() класса поля судоку и выполняем воз- врат. Если же все эти проверки не увенчаются успехом, выполняется последнее выражение, которое вызовет метод dataErrorMsg() класса MainWindow , написанный нами ранее. Листинг 32.24. Методы onSave() и onSaveMini() def onSave(self): self.saveSVDFile(self.sudoku.getDataAllCells()) def onSaveMini(self): self.saveSVDFile(self.sudoku.getDataAllCellsMini()) Методы onSave() и onSaveMini() , сохраняющие данные в файл (листинг 32.24), очень про- сты — они лишь вызывают метод saveSVDFile() , передав ему результат, возвращенный методами, соответственно, getDataAllCells() и getDataAllCellsMini() класса поля судоку. Листинг 32.25. Метод saveSVDFile() def saveSVDFile(self, data): fileName = QtWidgets.QFileDialog.getSaveFileName(self, "Выберите файл", QtCore.QDir.homePath(), "Судоку (*.svd)")[0] if fileName: try: with open(fileName, mode="w", newline="") as f: f.write(data) self.statusBar().showMessage("Файл сохранен", 10000) except: QtWidgets.QMessageBox.information(self, "Судоку", "Не удалось сохранить файл") Метод saveSVDFile() , непосредственно выполняющий сохранение данных (листинг 32.25), также несложен — мы выводим стандартное диалоговое окно сохранения файла. Если пользователь задал имя файла для сохранения и нажал кнопку Сохранить, мы открываем Глава 32. Приложение «Судоку» 781 файл на запись (если файл с заданным именем отсутствует, он будет создан), записываем в него данные, полученные с параметром, и выводим в строке состояния сообщение об успехе. Открытие файла и запись в него мы выполняем в блоке обработки исключений — в случае возникновения исключения на экран будет выведено сообщение об ошибке записи. Запустим приложение, поставим в некоторые ячейки цифры, сохраним головоломку в пол- ном формате, очистим поле судоку и загрузим сохраненную головоломку. После чего по- пробуем сохранить и загрузить головоломку в компактном формате. 32.3.8. Печать и предварительный просмотр Последнее, что мы добавим в приложение «Судоку», — это средства для печати, предвари- тельного просмотра головоломок и настройки печатной страницы. Здесь нам понадобится внести изменения в классы Widget , MainWindow и определить новый класс PreviewDialog , представляющий диалоговое окно предварительного просмотра. Условимся, что головоломка будет печататься в том же виде, в каком представлена на экра- не. Ячейки будут иметь размеры 30 × 30 пикселов, иметь темно-серую рамку, оранжевый или светло-серый цвет фона. Цифры в ячейках будут выводиться черным цветом, шрифтом Verdana размером 14 пунктов и выравниваться по середине. 32.3.8.1. Реализация печати в классе Widget Все действия по формированию печатного представления головоломки мы будем выпол- нять в классе поля судоку Widget . Для этого мы определим в нем метод print() , который в качестве единственного параметра получит принтер, на котором должна быть выполнена печать и который представляется экземпляром класса QPrinter Код метода print() не очень велик, но требует развернутых пояснений. Мы рассмотрим его по частям. def print(self, printer): penText = QtGui.QPen(QtGui.QColor(MyLabel.colorBlack), 1) penBorder = QtGui.QPen(QtGui.QColor(QtCore.Qt.darkGray), 1) brushOrange = QtGui.QBrush(QtGui.QColor(MyLabel.colorOrange)) brushGrey = QtGui.QBrush(QtGui.QColor(MyLabel.colorGrey)) Сразу же создаем два пера: для вывода цифр (черное) и рамок ячеек (темно-серое) и две кисти: оранжевую и светло-серую. painter = QtGui.QPainter() painter.begin(printer) Начинаем печать. painter.setFont(QtGui.QFont("Verdana", pointSize=14)) Указываем шрифт для вывода цифр в ячейках. i = 0 Объявляем переменную, в которой будет храниться номер печатаемой в настоящий момент ячейки. for j in range(0, 9): Запускаем цикл, который будет перебирать числа из диапазона 0...8 включительно. Эти числа будут представлять номера печатаемых строк поля судоку. for k in range(0, 9): 782 Часть II. Библиотека PyQt 5 Внутри этого цикла запускаем другой, аналогичный, который будет перебирать номера яче- ек текущей строки. x = j * 30 y = k * 30 Вычисляем координаты левого верхнего угла печатаемой в настоящий момент ячейки. Горизонтальную координату мы можем получить, взяв номер текущей строки и умножив его на ширину ячейки (30 пикселов). Вертикальная координата вычисляется аналогично на основе номера текущей ячейки текущей строки и высоты ячейки (также 30 пикселов). painter.setPen(penBorder) Теперь нам нужно вывести сам квадратик, создающий ячейку. Задаем темно-серое перо для печати рамки этого квадратика. painter.setBrush(brushGrey if self.cells[i].bgColorDefault == MyLabel.colorGrey else brushOrange) Если для фона ячейки задан светло-серый цвет, задаем светло-серое перо, в противном слу- чае — оранжевое. painter.drawRect(x, y, 30, 30) Выводим квадратик. painter.setPen(penText) Задаем черное перо, которым будет выведена цифра. painter.drawText(x, y, 30, 30, QtCore.Qt.AlignCenter, self.cells[i].text()) Выводим поверх квадратика цифру, установленную в ячейку. i += 1 Увеличиваем значение номера текущей ячейки на единицу, чтобы на следующем проходе цикла напечатать следующую ячейку. painter.end() И завершаем печать. 32.3.8.2. Класс PreviewDialog: диалоговое окно предварительного просмотра Класс PreviewDialog реализует функциональность диалогового окна предварительного про- смотра головоломки перед печатью (рис. 32.3). Это окно позволит нам просматривать голо- воломку в масштабе 1:1, увеличивать, уменьшать масштаб и сбрасывать его к изначальному значению. Код класса PreviewDialog мы сохраним в файле previewdialog.py в каталоге modules . Он до- вольно велик и использует примечательные приемы программирования, о которых следует поговорить, поэтому давайте рассмотрим его по частям. from PyQt5 import QtCore, QtWidgets, QtPrintSupport class PreviewDialog(QtWidgets.QDialog): Глава 32. Приложение «Судоку» 783 Рис. 32.3. Диалоговое окно предварительного просмотра Окно предварительного просмотра мы делаем подклассом класса Dialog . Это позволит нам без особых проблем сделать размеры окна неизменяемыми, а само окно — модальным. def __init__(self, parent=None): QtWidgets.QDialog.__init__(self, parent) self.setWindowTitle("Предварительный просмотр") self.resize(600, 400) vBox = QtWidgets.QVBoxLayout() Интерфейс окна включит две горизонтальные группы элементов управления, расположен- ные друг над другом (см. рис. 32.3). Поэтому для размещения групп мы создадим верти- кальный контейнер QVBoxlayout hBox1 = QtWidgets.QHBoxLayout() Верхняя группа будет содержать три кнопки: для увеличения, уменьшения и сброса мас- штаба. Поскольку элементы в группе должны располагаться по горизонтали, используем для их расстановки контейнер QHBoxLayout btnZoomIn = QtWidgets.QPushButton("&+") btnZoomIn.setFocusPolicy(QtCore.Qt.NoFocus) hBox1.addWidget(btnZoomIn, alignment=QtCore.Qt.AlignLeft) btnZoomOut = QtWidgets.QPushButton("&-") btnZoomOut.setFocusPolicy(QtCore.Qt.NoFocus) hBox1.addWidget(btnZoomOut, alignment=QtCore.Qt.AlignLeft) btnZoomReset = QtWidgets.QPushButton("&Сброс") btnZoomReset.setFocusPolicy(QtCore.Qt.NoFocus) btnZoomReset.clicked.connect(self.zoomReset) hBox1.addWidget(btnZoomReset, alignment=QtCore.Qt.AlignLeft) 784 Часть II. Библиотека PyQt 5 Создаем все эти три кнопки и добавляем их в контейнер. Для каждой кнопки указываем, что она не может принимать фокус ввода, вызвав у нее метод setFocusPolicy() с параметром NoFocus , — таким образом, мы создадим в нашем диалоговом окне подобие панели инстру- ментов. Также для всех трех кнопок указываем выравнивание по левому краю. У кнопки сброса масштаба мы сразу же указываем в качестве обработчика сигнала clicked метод zoomReset() класса PreviewDialog . У остальных кнопок мы пока не указываем обра- ботчики этого сигнала. hBox1.addStretch() Добавляем в горизонтальный контейнер растягивающуюся область, чтобы все кнопки ока- зались прижатыми к левому краю контейнера. vBox.addLayout(hBox1) Добавляем сам горизонтальный контейнер в вертикальный. hBox2 = QtWidgets.QHBoxLayout() Создаем еще один горизонтальный контейнер, в котором будут выводиться панель предва- рительного просмотра и кнопка Закрыть. self.ppw = QtPrintSupport.QPrintPreviewWidget(parent.printer) self.ppw.paintRequested.connect(parent.sudoku.print) hBox2.addWidget(self.ppw) Создаем панель предварительного просмотра (экземпляр класса QPrintPreviewWidget ) и связываем его сигнал paintRequested с методом print() компонента поля судоку, иначе эта панель ничего не выведет. Компонент поля судоку хранится в атрибуте sudoku основно- го окна приложения, а основное окно мы без проблем получим с параметром parent конст- руктора. Напоследок добавляем панель просмотра во второй горизонтальный контейнер. btnZoomIn.clicked.connect(self.ppw.zoomIn) btnZoomOut.clicked.connect(self.ppw.zoomOut) Создав панель предварительного просмотра, связываем с ее методами zoomIn() и zoomOut() сигналы clicked кнопок увеличения и уменьшения масштаба. box = QtWidgets.QDialogButtonBox( QtWidgets.QDialogButtonBox.Close, QtCore.Qt.Vertical) Создаем контейнер для кнопок, которые обычно выводятся в диалоговом окне, добавляем в него кнопку закрытия и располагаем по вертикали. btnClose = box.button(QtWidgets.QDialogButtonBox.Close) btnClose.setText("&Закрыть") btnClose.setFixedSize(96, 64) btnClose.clicked.connect(self.accept) Получаем только что созданную в контейнере кнопку, задаем для нее надпись Закрыть, увеличенные размеры и связываем ее сигнал clicked с методом accept() , унаследованным нашим диалоговым окном от класса Dialog hBox2.addWidget(box, alignment=QtCore.Qt.AlignRight | QtCore.Qt.AlignTop) Добавляем контейнер с кнопками во второй горизонтальный контейнер, указав выравнива- ние по правой и верхней границам, т. е. расположение в верхнем правом углу. vBox.addLayout(hBox2) self.setLayout(vBox) Глава 32. Приложение «Судоку» 785 Добавляем второй горизонтальный контейнер в вертикальный контейнер и помещаем по- следний в окно. self.zoomReset() Указываем масштаб по умолчанию — 1:1, вызвав метод zoomReset() окна. def zoomReset(self): self.ppw.setZoomFactor(1) И, не откладывая дела в долгий ящик, определим этот метод. 32.3.8.3. Реализация печати в классе MainWindow Теперь внесем необходимые дополнения в код класса MainWindow , чтобы наше приложение наконец-то овладело печатным мастерством. Нам понадобится добавить в конструктор код, создающий необходимые пункты меню и кнопки панели инструментов, и три метода: |