ВУЗ: Не указан
Категория: Не указан
Дисциплина: Не указана
Добавлен: 05.12.2023
Просмотров: 850
Скачиваний: 3
ВНИМАНИЕ! Если данный файл нарушает Ваши авторские права, то обязательно сообщите нам.
СОДЕРЖАНИЕ
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
onPrint()
— выполняет печать головоломки;
onPreview()
— выводит на экран созданное ранее окно предварительного просмотра;
onPageSetup()
— производит настройку параметров страницы.
Поскольку предварительный просмотр у нас будет выполняться в только что созданном окне
PreviewDialog
, нам следует добавить в самое начало кода класса
MainWindow выраже- ние, выполняющее импорт класса
PreviewDialog
: from modules.previewdialog import PreviewDialog
Дополнения, которые следует внести в код конструктора класса
MainWindow
, показаны в листинге 32.26 (добавленный код, как обычно, выделен полужирным шрифтом).
Листинг 32.26. Конструктор (дополнения) def __init__(self, parent=None): action = myMenuFile.addAction("&Сохранить компактно...", self.onSaveMini) action.setStatusTip(
"Сохранение головоломки в компактном формате") myMenuFile.addSeparator() toolBar.addSeparator() action = myMenuFile.addAction(QtGui.QIcon(r"images/print.png"),
"&Печать...", self.onPrint,
QtCore.Qt.CTRL + QtCore.Qt.Key_P) toolBar.addAction(action) action.setStatusTip("Печать головоломки") action = myMenuFile.addAction(QtGui.QIcon(r"images/preview.png"),
"П&редварительный просмотр...", self.onPreview) toolBar.addAction(action) action.setStatusTip("Предварительный просмотр головоломки")
786
Часть II. Библиотека PyQt 5 action = myMenuFile.addAction("П&араметры страницы...", self.onPageSetup) action.setStatusTip("Задание параметров страницы") myMenuFile.addSeparator() toolBar.addSeparator() action = myMenuFile.addAction("&Выход", QtWidgets.qApp.quit,
QtCore.Qt.CTRL + QtCore.Qt.Key_Q)
Между пунктами главного меню, вызывающими файловые операции, мы вставляем разде- литель и пункты Печать, Предварительный просмотр и Параметры страницы. Не забы- ваем добавить соответствующие кнопки на панель инструментов.
Листинг 32.27. Методы onPrint(), onPreview() и onPageSetup() def onPrint(self): pd = QtPrintSupport.QPrintDialog(self.printer, parent=self) pd.setOptions(QtPrintSupport.QAbstractPrintDialog.PrintToFile |
QtPrintSupport.QAbstractPrintDialog.PrintSelection) if pd.exec() == QtWidgets.QDialog.Accepted: self.sudoku.print(self.printer) def onPreview(self): pd = PreviewDialog(self) pd.exec() def onPageSetup(self): pd = QtPrintSupport.QPageSetupDialog(self.printer, parent=self) pd.exec()
Код методов onPrint()
, onPreview()
и onPageSetup()
, производящих печать, предваритель- ный просмотр и настройку страницы, очень прост (листинг 32.27). Единственная деталь, достойная рассмотрения: в диалоговом окне печати мы задаем только возможность указа- ния печати в файл и выбора принтера, на котором будет выполняться печать. Остальные параметры, в частности выбор печатаемых страниц, в нашем случае не нужны.
Запустим приложение, создадим какую-либо головоломку и проверим, как работает печать, предварительный просмотр и настройка параметров страницы.
На этом работу над приложением «Судоку» можно считать завершенной. Собственно, по- дошел конец и книге, посвященной замечательному языку Python и не менее замечательной библиотеке PyQt.