Главная страница

ээдд. Прохоренок_Н_А__Дронов_В_А_Python_3_и_PyQt_5_Разработка_приложен. Николай Прохоренок Владимир Дронов


Скачать 7.92 Mb.
НазваниеНиколай Прохоренок Владимир Дронов
Дата05.05.2023
Размер7.92 Mb.
Формат файлаpdf
Имя файлаПрохоренок_Н_А__Дронов_В_А_Python_3_и_PyQt_5_Разработка_приложен.pdf
ТипДокументы
#1111379
страница77 из 83
1   ...   73   74   75   76   77   78   79   80   ...   83
[^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
, чтобы наше приложение наконец-то овладело печатным мастерством. Нам понадобится добавить в конструктор код, создающий необходимые пункты меню и кнопки панели инструментов, и три метода:
1   ...   73   74   75   76   77   78   79   80   ...   83


написать администратору сайта