Читать в формате PDF - PyQt for Linguists Home

Алексей Иванович Горожанов
PyQt 5 для лингвистов:
профессионально ориентированное
программирование
Электронное учебное пособие
для студентов лингвистических вузов и факультетов
(бакалавриат и магистратура)
версия в формате PDF
Москва, 2014
1
© А.И.Горожанов
Оглавление
Вступление ....................................................................................................... 3
Глава 1. Установка и настройка ...................................................................... 5
Глава 2. Первые приложения на PyQt5 ......................................................... 16
Глава 3. Приложение Guess Word и его вариации ....................................... 37
Глава 4. Меню и диалоговые окна ................................................................ 51
Глава 5. Первые программы учебного назначения ...................................... 66
Глава 6. Программные тренажеры – основы ................................................ 85
Глава 7. Программные тренажеры с протоколированием ......................... 111
Глава 8. Автоматический анализ протоколов ............................................. 139
Глава 9. Программные тренажеры с заданием на аудирование ................ 166
Заключение ................................................................................................... 185
Список литературы ...................................................................................... 186
Приложение 1. Ключи к заданиям .............................................................. 188
Приложение 2. Литература, рекомендуемая для дополнительного изучения
............................................................................................................................... 201
2
© А.И.Горожанов
Вступление
Уважаемые читатели!
Цель этого учебного пособия – помочь лингвистам (в первую очередь
будущим и настоящим преподавателям и исследователям) научиться создавать
современные программные инструменты, с помощью которых можно решать
профессиональные задачи.
Работа с представленным материалом требует некоторых предварительных
знаний, как из области информатики. Для того, чтобы получить (или освежить)
эти знания можно прочитать замечательную книгу Think Python: How To Think
Like a Computer Scientist, которая доступна в Интернете по адресу:
http://www.greenteapress.com/thinkpython/thinkpython.html
Рекомендуется читать именно оригинальный английский вариант. С ним (в
виде интерактивного учебника онлайн) можно также работать по ссылке:
http://interactivepython.org/courselib/static/thinkcspy/index.html
Если Вы прочитали и поняли основные темы Think Python: How To Think
Like a Computer Scientist, то добро пожаловать в мир графических приложений
на языке программирования Python3 и библиотеке PyQt5!
Учебное пособие также может быть использовано в качестве самоучителя.
***
Учебное пособие состоит из вступления, девяти глав, заключения и двух
приложений. В конце приведен список цитируемых в тексте источников.
Код всех обсуждаемых программ приведен целиком (текстом или в файле).
Обязательно построчно изучайте и запускайте на своем компьютере все
рассматриваемые программы. Только так можно научиться практическому
программированию. Все файлы программ и интерфейсов можно скачать в
архиве программ по ссылке:
http://1.pyqtforlinguists.appspot.com
Для Вашего удобства архив файлов разбит по главам.
3
© А.И.Горожанов
Рубрика
Проверьте
и
расширьте
свое
понимание
содержит
практические задачи, которые предлагается решить. Ко всем задачам в
Приложении 1 Вы найдете ключи.
Настоятельно рекомендуется работать с главами в порядке их следования.
т.к. каждое практическое задание является развитием предыдущего и опирается
на его программный код.
Приложение
2
содержит
список
дополнительной
литературы,
рекомендуемой для изучения.
По профессиональным вопросам, связанным с этим учебным пособием, Вы
можете написать мне на электронную почту: [email protected]
4
© А.И.Горожанов
Глава 1. Установка и настройка
Перед тем, как начать работу важно понять, чем обусловлен выбор именно
Python как языка программирования и именно PyQt как библиотеки для
создания оконных приложений. Я думаю, что ответ на этот вопрос
сформируется у Вас сам собой по мере продвижения вглубь повествования. В
книге Think Python: How To Think Like a Computer Scientist говорится много о
достоинствах Python [Downey, cc. v-xi]. Упомянем только тот факт, что этот
язык используется сейчас во многих университетах мира (доказательством тому
служат курсы на Coursera [PfE, AItIPiP, LtP on Coursera]) Python используется
как самый первый язык программирования и позиционируется как наиболее
удобный для изучения непрограммистами. Это полностью отвечает нашим
целям – обучить лингвистов программированию.
Как Вы могли заметить по прочтению Think Python: How To Think Like a
Computer Scientist, авторы не затрагивают тему написания графических
(оконных) приложений, упоминая несколько графических библиотек, на
которые стоит обратить внимание, среди них модуль tkinter, который уже
встроен в стандартный установочный пакет Python [Downey, c. 237]. Далее мы
немного остановимся на tkinter, но все-таки дальше однозначно перейдем к
PyQt, как более мощной и удобной для пользования библиотеке. Достаточно
сказать, что библиотека Qt (PyQt – это ее воплощение для Python) широко
используется программистами, работающими на C/C++. В итоге Ваши
программы внешне ничем не будут отличаться от программ опытных
профессионалов – неплохой аргумент!
Кроме того, Python и PyQt распространяются на сегодняшний день
бесплатно, на условиях Универсальной общественной лицензии GNU (GNU
Public License) [Ответы GNU]. PyQt имеет также и коммерческую версию,
которая предоставляет пользователям некоторые дополнительные права (см.
http://www.riverbankcomputing.com/commercial/pyqt).
5
В
любом
случае,
я
© А.И.Горожанов
рекомендую
изучить
документы
лицензий,
чтобы
быть
полностью
информированным.
Если Вы читали Think Python: How To Think Like a Computer Scientist и уже
сами писали программы, то можно предположить, что Python уже установлен
на вашем компьютере. Тем не менее, рассмотрим несколько вариантов работы
над кодом.
После установки Python с официального сайта python.org в Программах
(для пользователей Windows) появится группа Python X.X, где X.X – номер
установленной версии. В нашем случае – это версия 3.3 (см. Рис. 1.1):
Рис. 1.1 Расположение Python в Windows
Запустите программу IDLE (Python GUI). Если все в порядке, то на экране
появится окно среды разработки. Если у Вас уже есть готовый файл, то Вы
можете открыть его с помощью меню File –> Open (см. Рис. 1.2):
Рис. 1.2 Открытие файла в среде разработки IDLE
6
© А.И.Горожанов
Например, у нас есть элементарная программа hello.py, которая состоит из
одной строки:
print('Hello, world!')
После открытия программы, нажмите F5. Программа будет запущена, в
оболочке IDLE, имитирующей консоль, появится результат работы программы
или вывод (output) (см. Рис. 1.3):
Рис. 1.3 Результат работы программы hello.py
7
© А.И.Горожанов
Если у Вас все получилось именно так, то значит, IDLE работает
правильно и Python установлен корректно.
Среда IDLE отличается удобством и простотой использования. Однако,
есть и более сложные (но в то же время и более удобные) среды разработки.
Например, The Erik Python IDE [The Erik]. В любом случае, пока наши
программы достаточно небольшие (не более нескольких сотен строк), мы будем
работать в IDLE. Кстати, на библиотеке tkinter выполнена сама IDLE.
Еще до установки PyQt мы можем написать простое оконное приложение,
используя встроенный модуль tkinter. Создадим маленький тест (см. Код
1.1):
Код 1.1 Программа prog1_1.py
1.
# Alexey Gorozhanov, 2014
2.
# prog1_1.py
3.
from tkinter import *
4.
def check():
5.
if var.get() == 1:
6.
master = Tk()
7.
message = Message(master, text="ПОЗДРАВЛЯЮ! Вы
ответили правильно!", width=300)
8.
message.pack()
9.
master.mainloop()
10.
else:
11.
master = Tk()
12.
message = Message(master, text="К сожалению,
пока неверно", width=300)
13.
message.pack()
14.
master.mainloop()
15. root = Tk()
16. root.title("Example")
17. lab_01 = Label(root, text="Подтвердите или
опровергните утверждение:", font=("Helvetica", 14))
18. lab_02 = Label(root, text="Русский и белорусский языки
8
© А.И.Горожанов
являются родственными.")
19. lab_03 = Label(root, text="(поставьте галочку, если
утверждение верно)")
20. lab_01.grid(row=0, column=0, sticky=W)
21. lab_02.grid(row=1, column=0, sticky=W)
22. lab_03.grid(row=3, column=0, sticky=W)
23. var = IntVar()
24. ch_box = Checkbutton(root, text="Да/Нет",
variable=var)
25. ch_box.grid(row=2, column=0, sticky=W)
26. button = Button(root, text="Проверить", command=check)
27. button.grid(row=4, column=0, sticky=W)
28. root.mainloop()
Если Вы набрали код верно, то при запуске программы получится
следующее (см. Рис. 1.4):
Рис. 1.4 Графический интерфейс программы prog1_1.py
Перед
нами
по-настоящему
интерактивное
оконное
приложение,
представляющее собой маленький тест, в котором нужно выбрать один из двух
вариантов ответа (Да/Нет). При нажатии на кнопку «Проверить» на экран
выводится окно оповещения – «классическая» обратная связь интерактивных
тестов.
Эти 28 строк кода помогут Вам разобраться, как в целом функционирует
оконное приложение не только на tkinter, но и на PyQt. Во-первых, можно
разделить весь код на два части. Первая часть строит основной графический
9
© А.И.Горожанов
интерфейс пользователя (GUI – graphical user interface), а вторая – оперирует
данными и, если нужно, изменяет основной графический интерфейс и строит
дополнительные графические элементы (эти элементы называются виджетами).
В нашем случае и то, и другое находится в одном файле: prog1_1.py. Также
можно выделить блок импорта, в нашем случае он состоит из одной записи:
from tkinter import *
В переводе на человеческий язык это означает «Из модуля tkinter
возьми всё». Так мы говорим программе, к какие модули будут ей нужны,
чтобы реализовать последующий код. Поэтому импорт располагается всегда в
начале выполнения алгоритма. Строки 1 и 2 являются комментариями и
игнорируются программой. Они нужны людям, чтобы помещать в программу
что-то еще кроме кода. Все правее от знака «решетки» (hash-sign или numbersign) и до конца строки считается комментарием. Впрочем, благодаря
прочтению книги Think Python: How To Think Like a Computer Scientist, Вы уже
все это знаете.
Итак, программа начинается со строки 3. Дальше она переходит сразу к
строке 15, т.к. в строках 4-14 находится функция, которая еще не вызвана. В
переменную root помещается главное окно нашего приложения (главный
виджет). Пока еще это только пустая оболочка, которую нужно наполнить
другими виджетами.
В терминологии программистов главный виджет
называется родительским элементом (parent), а все зависящие от него –
дочерними или детьми (children). В свою очередь, дети могут включать другие
виджеты и являться по отношению к ним родителями.
Родительский элемент root содержит пять дочерних виджетов: три надписи
(label), флажок (checkbox) и кнопку (button). Строки 17-27 создают
переменные этих виджетов и помещают их в родительский. Строка 28
закрывает графический цикл. Если ее убрать, то программа будет исполнена до
строки 27 и выключится.
10
© А.И.Горожанов
В этом общий принцип графических приложений: программа существует
в бесконечном цикле, постоянно ожидая от пользователя каких-то действий.
При выборе действия, связанного с выходом из программы, цикл прерывается.
В программе prog1_1.py такое действие – это закрытие главного окна.
Рассмотрим дочерние виджеты главного окна root. Надписи помещаются в
переменные lab_01, lab_02 и lab_03. В принципе, можно обойтись и без
переменных. Для этого нужно соединить строки объявления переменных и
вывода надписи на экран (визуализации виджета). Например, для первой
надписи при объединении строк 17 и 20 получится такой код (одна строка):
Label(root, text="Подтвердите или опровергните
утверждение:", font=("Helvetica", 14)).grid(row=0,
column=0, sticky=W)
С одной стороны, мы экономим строку. Но с другой – это неудобно, т.к.
если мы захотим что-то сделать с этой надписью (поменять текст, цвет текста,
шрифт и др.), то без переменной сделать это будет нельзя (или очень
затруднительно). Говоря более строгим языком, для того, чтобы легко
производить операции над объектом, нужно создать переменную для этого
объекта, чтобы к нему можно было легко обращаться.
В первоначальном коде так и сделано. Сначала объявлена переменная
lab_01 (строка 17), а затем объект, связанный с этой переменной, выведен на
экран (строка 20). Главный родительский элемент также помещен в
переменную. Благодаря этому мы можем запустить бесконечный цикл (строка
28) и дать главному окну заголовок (строка 17).
В скобках после слова Label приводится ряд параметров. Первый параметр
определяет родительский виджет надписи (это root). Параметр text определяет
текст надписи. Параметр font задает характеристики написания текста
надписи. У надписи lab_01 это тип шрифта и его размер. Параметров может
быть очень много, и не обязательно указывать все их сразу. Благодаря
созданию переменных виджетов, их параметры можно добавлять или изменять
11
© А.И.Горожанов
в других частях программы. Если Вы хотите узнать побольше о параметрах
виджетов модуля tkinter, то один из наилучших способов сделать это –
посетить сайт tutorialspoint, раздел Python, подраздел Python-GUI Programming
[tutorialspoint-pythonGUI].
Метод grid() выводит виджеты на экран. Здесь не лишним будет
напомнить, что методом называют функцию, по умолчанию свойственную
объектам определенного класса. Например, для объектов класса String в
Python разработчиками заложено множество методов, и в частности такие
полезные для лингвиста как split() или endswith() [tutorialspointpythonStrings].
Однако, вернемся к визуализации виджетов. Дело в том, что внутри
главного окна все объекты (виджеты) располагаются не произвольно, а
согласно установленному порядку. В коде программы prog1_1.py все виджеты
вписаны в матрицу или таблицу, в которой есть какое-то число строк и
столбцов. Как принято у программистов, их счет начинается с нуля, а не с
единицы. В каждой ячейке объект может быть расположен также по-разному,
относительно сторон (по центру, слева, справа, вверху, внизу и т.д.).
В параметрах метода grid() указывается, в какую ячейку размещать
виджет. В программе prog1_1.py матрица главного окна состоит из одного
столбца и пяти рядов. Все виджеты выровнены по западной стороне (т.е. по
левому краю) (см. Рис. 1.5):
Рис. 1.5 Матрица главного окна программы prog1_1.py
lab_01
lab_02
lab_03
ch_box
button
12
© А.И.Горожанов
Параметры метода grid() хорошо понятны: row – ряд, column – столбец,
sticky – выравнивание.
Флажок (checkbox) находится в переменной ch_box. Его параметры
становятся понятными, если сравнить их с параметрами виджета label. Нечто
новое представляет собой параметр variable. В нем указывается имя
переменной, которая будет хранить значение флажка. Эта переменная
принадлежит заложенному в tkinter классу IntVar(). При включенном
флажке переменная var имеет значение 1, а при выключенном устанавливается
на 0. Перед тем, как указывать переменную в параметре флажка, нужно создать
ее, что мы и сделали в строке 23.
Последний из виджетов в этой программе – это кнопка. Нажатие кнопки
всегда предполагает выполнение какой-то команды (функции). Указатель на
эту функцию определяется параметром command. Наша кнопка активирует
функцию check(). Обратите внимание на то, что в параметре command
функция пишется без скобок, иначе она выполнится сразу, а программа выдаст
ошибку.
Осталось разобраться в функции check(). Внутри нее проводится
проверка значения переменной var (т.е. включен ли флажок). Получить
значение переменной можно с помощью метода get(), поскольку это не
обычный тип Integer, а объект класса IntVar(). (Кстати, установить
значение можно с помощью метода set()). Если переменная var имеет
значение 1, то формируется еще одно главное окно master, а в него помещается
виджет message (сообщение). Сообщение мало чем отличается от надписи
[tutorialspoint-Message], мы употребляем его только для того, чтобы внести в
программу немного разнообразия. Для вывода сообщения используется метод
pack(), который по умолчанию располагает виджеты сверху вниз один за
другим, по центру. Окно master (как и root) существует в своем бесконечном
цикле, выйти из которого можно только закрыв его кнопкой «Х». Если флажок
13
© А.И.Горожанов
не выключен (else), то на экран также выводится окно, но уже с другим
текстом.
Мы
достаточно
подробно
рассмотрели
эту
простую
программу.
Программы с использованием PyQt будут строиться по сходному принципу, в
чем мы скоро и удостоверимся.
***
Проверьте и расширьте свое понимание (1.1): Как нужно изменить код,
чтобы при неправильном ответе и нажатии кнопки «Проверить», окно с текстом
«К сожалению, пока неверно» не выводилось?
(1.2): Что произойдет, если в коде программы заменить парные кавычки (")
на одинарные(')?
(1.3): Модифицируйте программу так, чтобы при ее запуске флажок уже
был включен.
***
Теперь можно переходить к установке PyQt5. Для этого надо проделать
следующие шаги:
1.
Скачать
с
официального
http://www.riverbankcomputing.co.uk/software/pyqt/download5
сайта
установочный
пакет.
2. Запустить скаченный пакет, следовать инструкциям. При этом
установщик сам найдет папку с установленным на Вашем компьютере Python и
впишет все нужные файлы в папку Lib/site-packages/PyQt5. В меню «Пуск»
будет создана отдельная группа (см. Рис. 1.6):
Рис. 1.6 Расположение PyQt в меню «Пуск»
14
© А.И.Горожанов
Как видно из рисунка, в этой книге будет использована свободно
распространяемая версия PyQt5.2.1.
3. Проверить правильность установки. Для этого открыть IDLE и набрать в
оболочке Python Shell следующее:
import PyQt5
Нажать Enter. Если оболочка не выводит ошибок, а просто переходит на
следующую строку, то все правильно, Python и PyQt «знают» друг о друге (см.
Рис. 1.7):
Рис. 1.7 Проверка установки PyQt5
***
В этой главе Вы получили предварительные сведения о PyQt5.
Вы установили Python's IDLE, PyQt5 и использовали свои знания Python
для написания простого графического приложения.
15
© А.И.Горожанов
Глава 2. Первые приложения на PyQt5
Технология создания каждой программы с использованием PyQt5 будет
одинакова (по крайней мере, в этой книге). Работа начинается с построения
графического интерфейса пользователя, а затем к нему добавляются функции
обработки данных. Уже на этом этапе Вы увидите одно из важнейших
достоинств PyQt: наличие среды разработки Qt Designer. Как видно из Рис. 1.6,
Qt Designer находится в группе, созданной при установке PyQt. После открытия
среды на экране появится диалоговое окно (см. Рис. 2.1):
Рис. 2.1 Диалоговое окно при запуске Qt Designer
Здесь нам предлагается выбрать, какой виджет будет главным в
создаваемой программе. Мы выберем установленный по умолчанию виджет
MainWindow. Для этого нужно нажать кнопку «Создать». На экране
появляется чистая заготовка для дальнейшей работы (см. Рис. 2.2):
Рис. 2.2 Заготовка для создания нового графического интерфейса пользователя
16
© А.И.Горожанов
Здесь нужно ненадолго остановиться и разобраться с панелями Qt
Designer. Слева находится панель виджетов. Виджеты просто перетаскиваются
мышью в нужное место заготовки. Справа располагаются Инспектор объектов,
Редактор свойств, Редактор действий и Обозреватель ресурсов. Сразу под
меню, вверху экрана, находится панель инструментов. Если активировать меню
Вид, то будут видны все доступные панели. Все эти панели необходимы нам
уже сейчас. Мы разберемся в них, создавая программы.
Создадим интерфейс программы, которая будет получать от пользователя
текст и выводить на экран длину этого текста в символах – вполне
лингвистическая задача.
Мы остановились на чистой заготовке. В нее надо добавить три виджета:
строку ввода (QLineEdit), кнопку (QPushButton) и надпись (QLabel).
Перетаскивая эти виджеты один за другим получим следующее (см. Рис. 2.3):
Рис. 2.3 Заполнение заготовки
17
© А.И.Горожанов
Очень хорошо. Теперь измените текст надписи. Для этого выделите
мышью надпись, чтобы она оказалась в синей рамке (см. Рис. 2.4):
Рис. 2.4 Выделенный виджет
Теперь в Редакторе свойств (панель в правой части экрана) отобразились
свойства выделенного виджета. Надо найти свойство text, которое по
умолчанию имеет значение TextLabel (см. Рис. 2.5):
Рис. 2.5 Редактор свойств для виджета QLabel
18
© А.И.Горожанов
Это значение нужно изменить на «Длина Вашего текста». Теперь виджет
отражает нужный нам текст. Его самого можно немного растянуть, чтобы текст
был виден целиком (см. Рис. 2.6):
Рис. 2.6 Измененная надпись
Интерфейс готов. Его можно сохранить в файл myinterface.ui (место
расположение файла надо оставить как есть, по умолчанию, т.е. в папке PyQt5).
Однако сам по себе файл с расширением .ui не представляет никакой ценности,
пока он не используется в нашей будущей программе. Есть несколько способов
превратить файл ui в полноценный компонент программы на Python. Один из
них – это конвертировать файл ui в файл py. Для этого Вам придется
поработать в командной строке.
19
© А.И.Горожанов
Если Вы работаете в Windows, то для выхода в командную строку нужно
нажать на меню Пуск и ввести в появившееся окно ввода команду «cmd» (см.
Рис. 2.7):
Рис. 2.7 Выход в командную строку
Нажмите Enter. Если все получилось правильно, то вы увидите следующее
(см. Рис. 2.8):
Рис. 2.8 Командная строка
Работа в командной строке возвращает нас во времена операционной
системы DOS, когда еще не было Windows, и людям приходилось общаться с
компьютером посредством набора команд в строке. Можно даже немного
преувеличенно сказать, что тогда каждый пользователь ПК должен был быть
немного программистом.
Но вернемся к нашей задаче, посмотрим внимательно, что представляет
собой командная строка. C:\Users\lenovo> означает, что мы находимся
именно в этой папке (во времена DOS говорили не папка, а директория). Нам
20
© А.И.Горожанов
нужно выйти в директорию, в которой расположен файл myinterface.ui. На моей
машине это C:\Python33\Lib\site-packages\PyQt5. Попасть туда не
так просто. Для начала надо просто выйти в директорию диска С, т.е. выйти из
директорий lenovo и Users. Наберите в строке команду cd.. (cd и две точки),
нажмите Enter, мы поднялись на один уровень (см. Рис. 2.9):
Рис. 2.9 Использование команды cd..
Проделайте то же самое еще один раз. Теперь мы на диске C. Дальше
можно или набрать сразу cd Python33\Lib\site-packages\PyQt5 или
двигаться к цели по одной директории: cd Python33, затем cd Lib, затем
cd site-packages, и наконец cd PyQt5. Разумеется, если у Вас другой
путь, то надо следовать ему. Обратите внимание на то, что Copy/Paste в режиме
командной строки не работает, все надо набирать руками. Итак, Вы в нужной
директории (см. Рис. 2.10):
Рис. 2.10 Переход в нужную директорию
21
© А.И.Горожанов
В этой же директории располагается файл pyuic5.bat. Именно он и нужен
для конвертации файла myinterface.ui в myinterface.py. Наберите в командной
строке следующее:
pyuic5 myinterface.ui -o myinterface.py -x
Тем самым мы указываем программе pyuic5, какой файл мы хотим
конвертировать и каким образом. На первый взгляд ничего не произошло, но
если посмотреть внимательно на содержание директории PyQt5, то будет
заметно появление в ней файла myinterface.py. Файл можно скопировать в
любое удобное для Вас место. Теперь откройте myinterface.py в IDLE и
запустите (F5). Вот результат (см. Рис. 2.11):
Рис. 2.11 Интерфейс программы myinterface.py
22
© А.И.Горожанов
Программа работает! Хотя она пока и не считает длину текста в строке
ввода.
Подведем предварительный итог:
1. В среде разработки Qt Designer достаточно просто строить графический
интерфейс пользователя.
2. Затем полученный файл .ui легко конвертируется в работающий файл
.py.
3. Полученный файл .py НЕЛЬЗЯ конвертировать обратно в файл .ui и
вносить в него изменения.
4. Дальше остается только добавить в файл .py функции обработки данных,
но интерфейс изменять (если это необходимо) придется вручную, без Qt
Designer.
Здесь у внимательного читателя может возникнуть справедливый вопрос:
Неужели никто из программистов не видит неудобства в том, что после
конвертации среда разработки Qt Designer остается недоступной? Ведь иногда
приходится вносить в интерфейс программы значительные изменения, и лучше
23
© А.И.Горожанов
всего было бы это сделать мышью, а не кодом. Или придется переделывать всю
программу заново?
Ответ: решение есть, мы его обсудим, но немного позже. В Qt Designer
можно работать на любом этапе разработки программы.
Но пока у нас есть файл myinterface.py (см. Код 2.1):
Код 2.1 Программа myinterface.py
1.
# -*- coding: utf-8 -*-
2.
3.
# Form implementation generated from reading ui file
'myinterface.ui'
4.
#
5.
# Created: Tue Apr 22 17:15:14 2014
6.
#
7.
#
8.
# WARNING! All changes made in this file will be lost!
by: PyQt5 UI code generator 5.2.1
9.
10. from PyQt5 import QtCore, QtGui, QtWidgets
11.
12. class Ui_MainWindow(object):
13.
def setupUi(self, MainWindow):
14.
MainWindow.setObjectName("MainWindow")
15.
MainWindow.resize(240, 320)
16.
self.centralwidget = QtWidgets.QWidget(MainWindow)
17.
self.centralwidget.setObjectName("centralwidget")
18.
self.lineEdit =
QtWidgets.QLineEdit(self.centralwidget)
19.
self.lineEdit.setGeometry(QtCore.QRect(60, 20, 113,
20))
20.
self.lineEdit.setText("")
21.
self.lineEdit.setObjectName("lineEdit")
22.
self.pushButton =
QtWidgets.QPushButton(self.centralwidget)
23.
self.pushButton.setGeometry(QtCore.QRect(80, 60,
24
© А.И.Горожанов
75, 23))
24.
self.pushButton.setObjectName("pushButton")
25.
self.label = QtWidgets.QLabel(self.centralwidget)
26.
self.label.setGeometry(QtCore.QRect(30, 120, 181,
20))
27.
self.label.setObjectName("label")
28.
MainWindow.setCentralWidget(self.centralwidget)
29.
self.menubar = QtWidgets.QMenuBar(MainWindow)
30.
self.menubar.setGeometry(QtCore.QRect(0, 0, 240,
21))
31.
self.menubar.setObjectName("menubar")
32.
MainWindow.setMenuBar(self.menubar)
33.
self.statusbar = QtWidgets.QStatusBar(MainWindow)
34.
self.statusbar.setObjectName("statusbar")
35.
MainWindow.setStatusBar(self.statusbar)
36.
37.
self.retranslateUi(MainWindow)
38.
QtCore.QMetaObject.connectSlotsByName(MainWindow)
39.
40.
def retranslateUi(self, MainWindow):
41.
_translate = QtCore.QCoreApplication.translate
42.
MainWindow.setWindowTitle(_translate("MainWindow",
"MainWindow"))
43.
self.pushButton.setText(_translate("MainWindow",
"PushButton"))
44.
self.label.setText(_translate("MainWindow", "Длина
Вашего текста"))
45.
46.
47. if __name__ == "__main__":
48.
import sys
49.
app = QtWidgets.QApplication(sys.argv)
50.
MainWindow = QtWidgets.QMainWindow()
51.
ui = Ui_MainWindow()
25
© А.И.Горожанов
52.
ui.setupUi(MainWindow)
53.
MainWindow.show()
54.
sys.exit(app.exec_())
Первые 9 строк являются комментариями (или вообще не заполнены). В
них содержится описательная информация. Строка 10 производит импорт
нужных модулей библиотеки PyQt5. В строках с 12 по 44 содержится класс
Ui_MainWindow(), который включает в себя две функции: setupUi(self,
MainWindow) и retranslateUi(self, MainWindow). Строки 47-54
инициализируют класс Ui_MainWindow(), в них строится бесконечный
графический цикл.
Если Вы прочитали и поняли в книге Think Python: How To Think Like a
Computer Scientist, что такое классы, то этот код не должен Вас испугать.
Разберем самое простое из приведенного кода. Функция setupUi(self,
MainWindow) (строки 13-38) строит графический интерфейс пользователя, а
функция retranslateUi(self,
MainWindow) (строки 40-44) создает
подписи для виджетов, которые могут иметь подписи. В нашей программе
подписи могут иметь главное окно MainWindow, кнопка pushButton и подпись
label. Чтобы быть уверенным в том, что Вы понимаете код, нужно попробовать
его изменить. Например, если заменить в строке 43 подпись "PushButton" на
"Кнопка", чтобы получилось
self.pushButton.setText(_translate("MainWindow",
"Кнопка"))
и запустить программу, то текст кнопки изменится (см. Рис. 2.12):
Рис. 2.12 Изменение кнопки
26
© А.И.Горожанов
Перейдем к функции setupUi(self,
MainWindow). Строка 15
устанавливает размер главного окна. Вы наверное заметили, что под надписью
«Длина вашего текста» остается много свободного места. Указанные в методе
resize(x, y) величины задают ширину и высоту окна соответственно.
Система координат при этом имеет следующую направленность (см. Рис. 2.13):
Рис. 2.13 Система координат виджетов PyQt
Установите значение высоты равное 180, чтобы строка 15 приняла вид
MainWindow.resize(240, 180)
27
© А.И.Горожанов
и запустите программу. Геометрия окна стала более логичной (см. Рис.
2.14):
Рис. 2.14 Изменение размера главного окна
Чтобы еще потренироваться с размерами, сделаем поле ввода шире. Пусть
оно тянется от одного края главного окна до другого. Вы наверняка догадались,
что
для
этого
нужно
setGeometry(QtCore.Qrect(x,
изменить
y,
строку
width,
19.
height))
Метод
задает
прямоугольник, в котором находится виджет. Параметры x и y устанавливают
координаты левого верхнего угла виджета относительно родительского
виджета (не экрана компьютера!), а width и height отвечают за ширину и
высоту. Мы не будем менять измерения по вертикали, поэтому установим x на
5, а width на 230. Эти координаты я установил путем нескольких
экспериментов. Запустите программу и посмотрите на результат (см. Рис. 2.15):
Рис. 2.15 Изменение размеров строки ввода
28
© А.И.Горожанов
Справедливо будет сказать, что все эти операции лучше было бы сделать в
Qt Designer еще до конвертации файла .ui в файл .py, но мы тренируемся, чтобы
начать понимать код, поэтому наши действия являются оправданными. Уже
понятно, что если мы захотим изменить расположение и размер кнопки, то
изменения нужно будет вносить в строку 23.
***
Проверьте и расширьте свое понимание (2.1): Измените кнопку так,
чтобы ее ширина и высота стали равны ширине и высоте строки ввода. При
этом координата y кнопки не должна измениться.
(2.2): Измените код главного окна так, чтобы при запуске программы в
заголовке появлялась надпись «My Window», а не «MainWindow».
***
Остановимся пока на этом и перейдем к написанию функций обработки
данных. Эти функции будут оставаться внутри класса Ui_MainWindow(),
прибавим к уже имеющимся двум функциям пустую заготовку. Для этого
между строками 44 и 47 надо написать следующее:
def myFunction(self):
pass
Не забудьте про отступы (indentations). Слово pass внутри функции
означает, что функция ничего не содержит. Однако программу можно запускать
без ошибок. То есть это заготовка, которая ничего не добавляет в программу, но
и не мешает ее функционированию.
Нам нужно, чтобы функция myFunction() активировалась при нажатии
кнопки pushButton. Сама функция должна выполнять три действия: брать
введенный текст из строки ввода, находить его длину и выводить результат в
надпись. Каждое действие может соответствовать одной строке функции:
def myFunction(self):
self.text = self.lineEdit.text()
self.length = len(self.text)
29
© А.И.Горожанов
self.label.setText("Длина Вашего текста %d" %
self.length)
Первая строка функции создает переменную self.text типа String и
помещает в нее содержимое строки ввода. Это делается при помощи метода
text(). Вторая строка создает переменную self.length типа Integer и
записывает в нее длину переменной self.text. Третья строка устанавливает текст
надписи self.label. В ней уже имеется текст «Длина Вашего текста», но при
использовании метода setText() он заменится на новый.
Функцию можно записать и в одну строку, просто она будет длиннее и
немного сложнее для чтения:
def myFunction(self):
self.label.setText("Длина
Вашего
текста
%d"
%
len(self.lineEdit.text()))
При такой записи нам не нужно создавать две переменные, поэтому мы
остановимся на этом варианте. Теперь у Вас есть функция, но она все еще
бесполезна, т.к. никак не связана с кнопкой. Добавьте в конец функции
setupUi() следующую строку:
self.pushButton.clicked.connect(self.myFunction)
Вы помните, что программа находится в бесконечном графическом цикле.
При этом она как будто ожидает от пользователя некоторых действий или
событий. Событие clicked (в терминологии PyQt эти события называются
сигналами), привязанное к кнопке означает нажатие этой кнопки. С помощью
метода connect() с событием соединяется некоторое действие (функция).
Таким образом, приведенную выше строку можно прочитать так: «Кнопка,
ожидай нажатия! Когда это произойдет, сразу вызывай указанную в скобках
функцию!» Обратите внимание на написание функции. Она пишется в этом
случае без скобок.
У каждого виджета есть свой определенный набор сигналов, многие из
которых являются универсальными как в случае с сигналом clicked.
30
© А.И.Горожанов
Настало время испытать программу. Запустите ее, введите какой-нибудь
текст (например, «эксперимент») и нажмите кнопку; результат – 11 символов
(см. Рис. 2.16):
Рис. 2.16 Результат работы программы при вводе слова «эксперимент»
В конце концов программа приняла следующий вид (см. Код 2.2):
Код. 2.2 Программа myinterface1.py, измененная версия программы myinterface.py
1.
from PyQt5 import QtCore, QtGui, QtWidgets
2.
3.
class Ui_MainWindow(object):
4.
def setupUi(self, MainWindow):
5.
MainWindow.setObjectName("MainWindow")
6.
MainWindow.resize(240, 180)
7.
self.centralwidget = QtWidgets.QWidget(MainWindow)
8.
self.centralwidget.setObjectName("centralwidget")
9.
self.lineEdit =
QtWidgets.QLineEdit(self.centralwidget)
10.
self.lineEdit.setGeometry(QtCore.QRect(5, 20, 230,
20))
11.
self.lineEdit.setText("")
12.
self.lineEdit.setObjectName("lineEdit")
13.
self.pushButton =
QtWidgets.QPushButton(self.centralwidget)
14.
self.pushButton.setGeometry(QtCore.QRect(80, 60,
75, 23))
15.
self.pushButton.setObjectName("pushButton")
31
© А.И.Горожанов
16.
self.label = QtWidgets.QLabel(self.centralwidget)
17.
self.label.setGeometry(QtCore.QRect(30, 120, 181,
20))
18.
self.label.setObjectName("label")
19.
MainWindow.setCentralWidget(self.centralwidget)
20.
self.menubar = QtWidgets.QMenuBar(MainWindow)
21.
self.menubar.setGeometry(QtCore.QRect(0, 0, 240,
21))
22.
self.menubar.setObjectName("menubar")
23.
MainWindow.setMenuBar(self.menubar)
24.
self.statusbar = QtWidgets.QStatusBar(MainWindow)
25.
self.statusbar.setObjectName("statusbar")
26.
MainWindow.setStatusBar(self.statusbar)
27.
28.
self.retranslateUi(MainWindow)
29.
QtCore.QMetaObject.connectSlotsByName(MainWindow)
30.
31.
self.pushButton.clicked.connect(self.myFunction)
32.
33.
def retranslateUi(self, MainWindow):
34.
_translate = QtCore.QCoreApplication.translate
35.
MainWindow.setWindowTitle(_translate("MainWindow",
"My Window"))
36.
self.pushButton.setText(_translate("MainWindow",
"Кнопка"))
37.
self.label.setText(_translate("MainWindow", "Длина
Вашего текста"))
38.
39.
40.
def myFunction(self):
self.label.setText("Длина Вашего текста %d" %
len(self.lineEdit.text()))
41.
42.
43.
if __name__ == "__main__":
import sys
32
© А.И.Горожанов
44.
app = QtWidgets.QApplication(sys.argv)
45.
MainWindow = QtWidgets.QMainWindow()
46.
ui = Ui_MainWindow()
47.
ui.setupUi(MainWindow)
48.
MainWindow.show()
49.
sys.exit(app.exec_())
***
Проверьте и расширьте свое понимание (2.3): В рассмотренной нами
программе функция вызывается нажатием кнопки. Однако пользователям
привычнее после набранного в строке ввода текста нажать клавишу Enter. Как
модифицировать программу, чтобы она работала именно таким образом?
Примечание: это сложное задание. Для его решения, попробуйте поискать
информацию в Интернете, в частности на форумах Stack Overflow [Stack
Overflow].
(2.4): Представьте, что пользователь ввел в строку ввода одни пробелы.
Модифицируйте программу так, чтобы в этом случае длина текста равнялась
нулю. Это не должно касаться случая, когда вместе с пробелами введены и
какие-то символы.
(2.5): Усовершенствуйте программу дальше. Сделайте так, чтобы вывод
принял форму «a / b», где a – количество всех символов кроме пробелов, b –
количество пробелов. Например, при вводе «я - лингвист» получился бы вывод
«10 / 2».
***
Итак,
наша
первая
программа
на
PyQt5
получилась
достаточно
интересной. Несмотря на небольшой объем в программе есть все основные
блоки, которые будут представлены и в более крупных проектах. Вернемся к
вопросу об удобстве изменения интерфейса.
Секрет в том, чтобы держать в отдельных файлах класс интерфейса и
функции обработки данных.
33
© А.И.Горожанов
Конвертируйте файл интерфейса myinterface.ui немного по-другому.
Уберите из командной строки параметр -x, чтобы получилось следующее:
pyuic5 myinterface.ui -o myinterface.py
Полученный файл переименуйте в myinterface2.py. Этот файл отличается
от предыдущего тем, что в нем нет графического цикла и его нельзя запустить
самостоятельно. Вы легко можете убедиться в этом сами. Поэтому нужно
создать главный исполняемый файл (не путайте с файлами с расширением
.exe!). Назовем его myintmain.py. Он должен иметь следующее содержание (см.
Код 2.3):
Код 2.3 Программа myintmain.py
1.
import sys
2.
from myinterface2 import *
3.
from PyQt5 import QtCore, QtGui, QtWidgets
4.
5.
6.
class MyWin(QtWidgets.QMainWindow):
def __init__(self, parent=None):
7.
QtWidgets.QWidget.__init__(self, parent)
8.
self.ui = Ui_MainWindow()
9.
self.ui.setupUi(self)
10.
11.
if __name__ == "__main__":
12.
app = QtWidgets.QApplication(sys.argv)
13.
myapp = MyWin()
14.
myapp.show()
15.
sys.exit(app.exec_())
Это достаточно небольшая но очень важная программа. Строка 2
импортирует все классы из файла myinterface2.py, в котором находится код
интерфейса. Строка 5 объявляет класс MyWin(). Вы можете назвать этот класс
и по-другому, главное, чтобы новое название было отражено и в строке 13.
Строка 6 запускает инициализирующую функцию. Строки 11-15 создают
34
© А.И.Горожанов
бесконечный цикл графического интерфейса. В принципе, приведенный выше
код можно использовать в качестве универсального, изменяя только строку 2.
Теперь нужно правильно добавить в программу myintmain.py функцию
myFunction() и привязать ее к кнопке. Можно скопировать прежний
фрагмент кода, но он выведет ошибки. И вот почему. Когда весь код находился
внутри одного файла, то мы ссылались на переменные виджетов напрямую,
используя self. Теперь кнопка и другие виджеты находятся в другом файле,
т.е. в другом классе, поэтому ссылаться на них нужно через переменную, в
которой находится их класс. Это переменная self.ui, которая создается в строке
8. Таким образом, self.pushButton превращается в self.ui.pushButton. Внесите
нужные изменения и сохраните полученный файл как myintmain1.py (см. Код
2.4):
1.
import sys
2.
from myinterface2 import *
3.
from PyQt5 import QtCore, QtGui, QtWidgets
4.
5.
6.
class MyWin(QtWidgets.QMainWindow):
def __init__(self, parent=None):
7.
QtWidgets.QWidget.__init__(self, parent)
8.
self.ui = Ui_MainWindow()
9.
self.ui.setupUi(self)
10.
11.
self.ui.pushButton.clicked.connect
(self.myFunction)
12.
13.
14.
def myFunction(self):
self.ui.label.setText("Длина Вашего текста %d"
% len(self.ui.lineEdit.text()))
15.
16.
if __name__ == "__main__":
17.
app = QtWidgets.QApplication(sys.argv)
18.
myapp = MyWin()
35
© А.И.Горожанов
19.
myapp.show()
20.
sys.exit(app.exec_())
Теперь можно снова открыть файл myinterface.ui в Qt Designer и изменить
с помощью мыши размеры виджетов, надписи, цветовую гамму и многое
другое. Измененный файл надо просто конвертировать в файл с расширением
.py и все. Изменения сразу вступят в силу при запуске программы главной
программы myintmain1.py. Убедитесь в этом сами, поэкспериментируя с
интерфейсом. Измените заголовок главного окна и размеры виджетов.
***
Проверьте и расширьте свое понимание (2.6): Выполните задание 2.3,
изменив файл myintmain1.py (Задание про клавишу Enter).
***
В этой главе Вы построили свое первое приложение на PyQt5. Вы
использовали такие виджеты как главное окно, строка ввода, кнопка и надпись.
Виджеты были соединены с функцией посредством сигналов clicked и
returnPressed. Также Вы научились конвертировать файлы ui в py при
помощи командной строки. Вы познакомились с двумя способами построения
программ: в одном файле и разделяя графический интерфейс и функции
обработки данных.
36
© А.И.Горожанов
Глава 3. Приложение Guess Word и его вариации
Сделаем еще один шаг вперед и решим более сложную задачу. Для этого
необходимо сформулировать техническое задание, оно должно содержать
требования, обязательные для разработчика. Итак, программа (назовем ее Guess
Word) должна:
 Иметь графический (оконный) интерфейс пользователя.
 Получать от пользователя некоторое количество букв русского
алфавита.
 Выводить в специальном окне русские слова длинной 4 или 5
символов, которые можно составить из введенных букв.
 Использовать каждую из введенных букв в одном слове столько раз,
сколь она была введена.
 Давать возможность пользователю выбирать параметр длины 4 или 5
(определяет, какие слова искать).
 Использовать для поиска список слов русского языка из отдельного
файла.
 Выводить количество проверенных комбинаций.
 Выводить время исполнения в секундах.
 Иметь индикатор хода процесса.
Перечисленные выше общие пункты помогут нам подойти к решению
задачи более конструктивно.
Для начала нужно построить графический интерфейс пользователя в Qt
Designer. Главным виджетом, как обычно, будет виджет QMainWindow.
Внутри него все остальные виджеты (его дети) будут скомпонованы по сетке
(Grid Layout). Компоновке нужно уделить повышенное внимание.
Возможно, Вы заметили, что в программе myinterface.py при увеличении
главного окна на весь экран виджеты пропорционально не меняли своих
размеров. Каким бы большим не было главное окно, строка ввода, кнопка и
надпись оставались фиксированного размера. Для учебной программы это не
37
© А.И.Горожанов
является недостатком, но в профессиональных приложениях такого быть не
должно. Если пользователь увеличивает главное окно, то закономерно, чтобы
увеличивалось и все внутри него. В Qt Designer сделать это несложно. Но для
начала нужно перетащить мышью в главное окно все используемые виджеты и
расположить относительно друг друга так, как нам нужно.
В программе Guess Word будут использованы следующие виджеты (см.
Рис. 3.1):
Рис. 3.1 Инспектор объектов интерфейса программы Guess Word
Новыми
для
нас
являются
многострочное
текстовое
поле
(QPlainTextEdit) и выпадающий список (QComboBox). Весь интерфейс
примет следующий вид (см. Рис. 3.2):
Рис. 3.2 Интерфейс guess.ui
38
© А.И.Горожанов
Виджеты располагаются так (сверху вниз и слева направо): QLabel,
QLineEdit, QComboBox, QPushButton, QPlainTextEdit.
На рисунках 3.1 и 3.2 все виджеты уже скомпонованы по сетке. И если Вы
сами строите интерфейс, то в инспекторе объектов (см. Рис. 3.1) нажмите на
объект MainWindow правой кнопкой мыши, в появившемся контекстном меню
выберите пункт Компоновка, затем Скомпоновать по сетке (см. Рис. 3.3):
Рис. 3.3 Установка компоновки по сетке
39
© А.И.Горожанов
Если виджеты немного сдвинулись, то подправьте их мышью. А теперь
самое главное: какой все это даст эффект? Выберите в меню Qt Designer Форма
–> Предпросмотр или нажмите Ctrl + R. Таким Ваш интерфейс увидит
пользователь. Попробуйте изменить размеры главного окна. Если Вы
правильно выполнили все инструкции, то все виджеты будут пропорционально
изменяться. Достаточно трудоемкая задача решена нами в один клик. Если Вам
вдруг захочется отказаться от компоновки, то для этого есть команда
Компоновка –> Удалить компоновщик.
40
© А.И.Горожанов
Вы можете поэкспериментировать с интерфейсом, открыв в Qt Designer
файл guess.ui из архива программ.
Конвертируйте интерфейс с помощью командной строки в файл guess.py.
Далее создайте главный файл под названием guessmain.py, для этого
воспользуйтесь Кодом 2.3, заменив в строке 2 импортируемый файл на guess.
Интерфейс готов, программа запускается из файла guessmain.py, но конечно же,
еще ничего не работает, а в выпадающем списке нет никаких значений. Значит,
самое время построить интерфейс из исполняемого файла и написать функции,
которые оживят программу.
Для того, чтобы добавить значения в выпадающий список, можно
воспользоваться методом addItems(). Параметром метода является массив
(питонисты говорят список) значений. Также полезно будет установить первое
(т.е. нулевое!) значение списка как значение по умолчанию. Добавьте в
функцию __init__()две строки:
self.ui.comboBox.setCurrentIndex(0)
self.ui.comboBox.addItems('4 5'.split())
Обратите внимание, что для создания списка используется строка и метод
split(). Вместо этого можно было бы записать (['4',
'5']). Так
проявляет себя синонимия в языках программирования.
Изучив Рис. 3.1, Вы наверное обратили внимание на объект statusbar
класса QStatusBar. Это очень полезный элемент программы, который еще
называют строкой состояния. Ее особенность состоит в том, что при любых
изменениях размера главного окна она всегда будет находится внизу
интерфейса. Я разместил в ней сведения об авторе программы, добавив в
функцию __init__()еще одну строку:
self.ui.statusbar.showMessage('© Alexey
Gorozhanov, 2014')
Также в строку состояния часто помещают информацию и последних
действиях, совершенных программой (открыт/записан файл, ошибка ввода,
41
© А.И.Горожанов
процесс занял столько-то времени и мн. др.). Последней строкой в функции
__init__()будет привязка функции к кнопке:
self.ui.pushButton.clicked.connect(self.start1)
Сама функция start1() еще не создана, поэтому на одном уровне с
функцией __init__() мы создадим пустую функцию start1(). На этот
момент исполняемый файл guessmain.py имеет вид (см. Код 3.1):
Код 3.1 Программа mainguess.py без исполняющих функций
1.
import sys
2.
from guess import *
3.
from PyQt5 import QtCore, QtGui, QtWidgets
4.
5.
6.
class MyWin(QtWidgets.QMainWindow):
def __init__(self, parent=None):
7.
QtWidgets.QWidget.__init__(self, parent)
8.
self.ui = Ui_MainWindow()
9.
self.ui.setupUi(self)
10.
11.
self.ui.comboBox.setCurrentIndex(0)
12.
self.ui.comboBox.addItems('4 5'.split())
13.
self.ui.statusbar.showMessage('© Alexey Gorozhanov,
2014')
14.
15.
self.ui.pushButton.clicked.connect(self.start1)
16.
17.
18.
def start1(self):
pass
19.
20.
if __name__ == "__main__":
21.
app = QtWidgets.QApplication(sys.argv)
22.
myapp = MyWin()
23.
myapp.show()
24.
sys.exit(app.exec_())
42
© А.И.Горожанов
Из функции start1() будут запускаться все остальные функции
программы. Поскольку поиск слов осуществляется по параметру, который
выбирает пользователь, для каждого из этих параметров стоит предусмотреть
отдельную функцию. Поэтому вставим в код еще две пустые функции:
wordFour() и wordFive(). Файл, из которого программа будет брать слова
для проверки, находится в архиве программ под названием dict.txt. В нем
собрано более 30000 слов русского языка, слова отделены друг от друга одним
пробелом. Чтение файла будет происходить внутри каждой из двух функций.
Алгоритм,
используемый
в
wordFour()
и
wordFive(),
будет
отличаться только в количестве вложенных циклов проверки, поэтому мы
разберем для примера только функцию wordFour(). Вот ее основные блоки:
1. Проводится замер текущего времени и помещение полученной
величины в переменную (для этого импортируется модуль time).
2. Происходит считывание содержимого файла dict.txt в список
(массив).
3. С помощью четырех вложенных циклов for по очереди перебираются
все возможные комбинации длинной 4 символа из введенных пользователем
букв.
4.
Внутри
последнего вложенного цикла
очередная
составленная
комбинация сравнивается с каждым словом из файла dict.txt. Если есть
совпадение, то комбинация помещается в список (массив) результата.
5. Также внутри последнего вложенного цикла производится счет
комбинаций (инкремент накапливающей переменной).
6. Еще раз происходит замер текущего времени, полученная величина
вычитается из первоначальной, разница дает время выполнения функции.
6. Производится вывод списка результата, количества проверенных
комбинаций и времени выполнения.
В качестве аргумента функция будет получать текст, введенный
пользователем.
43
© А.И.Горожанов
Внутри функции start1() будет проводиться проверка текущего
значения выпадающего списка. В зависимости от результата будет запускаться
функция wordFour() или wordFive().
Для того, чтобы элементы списка результата выводились через запятую, а
после
последнего
стояла
точка,
создадим
специальную
функция
arrOutput(), которая будет перерабатывать список в одну строку.
Замечание между строк. Вместо выпадающего списка технически можно
было бы использовать и просто строку ввода, т.е. пользователь бы сам вводил
цифру 4 или 5. Но в этом случае пришлось бы проводить многочисленные
проверки ввода. Например, была ли введена цифра, какая цифра и т.д.
Использование виджета QComboBox полностью исключает возможность
некорректного ввода, а потому является наилучшим выбором в этом случае.
Итак, на текущем этапе получаем следующий код (см. Код 3.2):
Код 3.2 Программа mainguess.py с одной исполняющей функцией
1.
import sys
2.
import time
3.
from guess import *
4.
from PyQt5 import QtCore, QtGui, QtWidgets
5.
6.
7.
class MyWin(QtWidgets.QMainWindow):
def __init__(self, parent=None):
8.
QtWidgets.QWidget.__init__(self, parent)
9.
self.ui = Ui_MainWindow()
10.
self.ui.setupUi(self)
11.
12.
self.ui.comboBox.setCurrentIndex(0)
13.
self.ui.comboBox.addItems('4 5'.split())
14.
self.ui.statusbar.showMessage('© Alexey Gorozhanov,
2014')
15.
16.
self.ui.pushButton.clicked.connect(self.start1)
17.
44
© А.И.Горожанов
18.
def start1(self):
19.
if self.ui.comboBox.currentIndex() == 0:
20.
self.wordFour(self.ui.lineEdit.text())
21.
elif self.ui.comboBox.currentIndex() == 1:
22.
self.wordFive(self.ui.lineEdit.text())
23.
24.
def wordFour(self, letters):
25.
self.t1 = time.time()
26.
self.c = 0
27.
self.resArr = []
28.
self.initW = letters
29.
self.res = ["", "", "", ""]
30.
self.r = open("dict.txt", 'r', encoding='utf-8')
31.
self.fileRead = self.r.read()
32.
self.fileSplit = self.fileRead.split()
33.
self.r.close()
34.
35.
for self.i in range(0, len(self.initW)):
36.
self.res[0] = self.initW[self.i]
37.
38.
for self.q in range(0, len(self.initW)):
39.
if (self.q != self.i):
40.
self.res[1] = self.initW[self.q]
41.
42.
for self.p in range(0, len(self.initW)):
43.
if (self.p != self.i) and (self.p !=
self.q):
44.
self.res[2] = self.initW[self.p]
45.
46.
for self.pp in range(0,
len(self.initW)):
47.
if (self.pp != self.i) and
(self.pp != self.q) and (self.pp != self.p):
48.
self.res[3] =
45
© А.И.Горожанов
self.initW[self.pp]
49.
50.
self.wordFor =
self.res[0] + self.res[1] + self.res[2] + self.res[3]
51.
if self.wordFor in
self.fileSplit:
52.
53.
if self.wordFor not
in self.resArr:
54.
self.resArr.append(self.wordFor)
55.
56.
57.
self.c += 1
self.str = "Найдено совпадений: " +
str(len(self.resArr)) + "\n" + self.arrOutput(self.resArr) +
"\n" + str(self.c) + " комбинаций проверено\nВремя
исполнения: " + str(time.time() - self.t1) + " с."
58.
self.ui.plainTextEdit.appendPlainText(self.str)
59.
60.
61.
def wordFive(self):
pass
62.
63.
def arrOutput(self, arr):
64.
arr.sort()
65.
self.str = ''
66.
for i in range(0, len(arr)):
67.
68.
69.
70.
71.
if i != len(arr) - 1:
self.str += arr[i] + ', '
else:
self.str += arr[i] + '.'
return self.str
72.
73. if __name__ == "__main__":
74.
app = QtWidgets.QApplication(sys.argv)
46
© А.И.Горожанов
75.
myapp = MyWin()
76.
myapp.show()
77.
sys.exit(app.exec_())
Аналогично строится функция wordFive(), с тем лишь отличием, что
вложенные циклы получат еще один цикл for. Но остается выполнить еще
одно важное условие – написать индикатор выполнения хода процесса.
Важность такого индикатора нельзя недооценивать, т.к. при достаточно
длительных процессах пользователь не сможет узнать, происходит ли вообще
что-то или программа просто зависла. В сложных приложениях бывают
процессы, которые даже при высокой мощности современных компьютеров
могут протекать многие минуты и даже часы, поэтому знать, на каком этапе
находится действие, а также иметь возможность прервать процесс в случае
необходимости чрезвычайно важно. PyQt имеет специально встроенный виджет
QProgressDialog, который позволяет отслеживать длительные процессы, в
частности циклы, как в нашем случае. Мы привяжем индикатор к внешнему
циклу for и сделаем так, чтобы всякий раз при увеличении переменной цикла
полоса индикатора обновляла значение. Также виджет QProgressDialog
имеет специальную кнопку, которую можно связать с событием, например,
выходом из цикла. Для начала создадим переменную типа bool и присвоим ей
значение False. Это удобно сделать в самом начале функции:
self.cancelled = False
Затем перед самым началом цикла создадим переменную виджета
QProgressDialog и поработаем с ней:
self.progress = QtWidgets.QProgressDialog("Searching...",
"Stop", 0, len(self.initW), self.ui.lineEdit)
self.progress.setWindowModality(QtCore.Qt.WindowModal)
self.progress.setMinimumDuration(1000)
У виджета пять параметров: надпись, надпись на кнопке, первоначальное
значение, конечное значение и родительский виджет). Конечное значение не
47
© А.И.Горожанов
обязательно должно равняться ровно ста. PyQt возьмет разницу между
максимальным и минимальным значением и самостоятельно пересчитает ее в
100 %. Метод устанавливает setWindowModality() модальность виджета.
При модальном значении основное окно программы будет недоступным, пока
не исчезнет виджет индикатора. И, наконец, метод setMinimumDuration()
устанавливает минимальное значение в миллисекундах. Если отслеживаемый
процесс занимает больше указанного времени, но индикатор появляется после
этого времени, если нет – то не появляется вовсе. В самом конце функции
добавьте строку
self.progress.deleteLater()
Если этого не сделать, то виджет не исчезнет при достижении 100 %.
Теперь внутри первого цикла for (но до начала второго) нужно добавить
установку значения полосы индикатора виджета и проверку нажатия кнопки
сброса:
self.progress.setValue(self.i)
if self.progress.wasCanceled():
self.cancelled = True
return
Здесь self.i – это переменная цикла. А return – выход из функции, т.е.
нажатие кнопки виджета индикатора хода процесса в данном случае прерывает
всю функцию, а не только цикл.
Можно сказать, что программа guessmain.py написана. Ее полный код (с
небольшими добавлениями, касающимися дезактивации и активации кнопки)
находится в архиве программ.
***
Проверьте и расширьте свое понимание (3.1): Модифицируйте
программу guessmain.py так, чтобы cравнение внутри цикла происходило не с
каждым словом в словаре, а только со словами подходящей длины. Например,
для слов длиной четыре символа функция wordFour(), будет перебирать, как
и в оригинальной программе, все сочетания из четырех символов, но
48
© А.И.Горожанов
сравнивать только с теми словами из словаря, которые имеют длину равную
также четырем символам. При этом скорость выполнения программы
существенно увеличится.
(3.2): Добавьте к интерфейсу программы Guess Word кнопку, которая бы
позволяла сохранить содержимое многострочного текстового поля в файл
text.txt. Файл должен иметь кодировку UTF-8. При этом интерфейс,
сохраняя выравнивание по сетке, может принять следующую форму (См. Рис.
3.4):
Рис. 3.4. Возможное размещение кнопки сохранения в файл
(3.3): В предыдущей программе всякий раз при нажатии на кнопку Save To
File файл text.txt перезаписывался, т.е. он терял прежнее наполнение.
Модифицируйте программу так, чтобы новый файл создавался только при
запуске программы, а при нажатии кнопки «Save To File» содержимое
многострочного текстового поля добавлялось бы в созданный файл. Также
пусть программа записывает в файл введенные пользователем буквы.
(3.4): Добавьте последний штрих. При сохранении в файл пусть в строке
состояния на три секунды появится надпись «Content Saved». По истечении
трех секунд в строке состояния снова должна появиться информация о
49
© А.И.Горожанов
правообладателе. Для решения этой задачи рекомендуется воспользоваться
форумом
Stack
Overflow
или
документацией
разработчика
[QtProject-
QStatusBar].
***
В этой главе Вы построили достаточно сложное приложение, которое
включает чтение из файла и запись в файл. Вы использовали такие виджеты как
главное
окно
(QPushButton),
(QMainWindow),
строка
ввода
(QLineEdit),
кнопка
надпись (QLabel), выпадающий список (QComboBox),
многострочное текстовое поле (QPlainTextEdir) и строка состояния
(QStatusBar). Вы научились базовым приемам использования индикатора
хода процесса (QProgressDialog), и это позволяет Вашим программам
выглядеть весьма достойно.
50
© А.И.Горожанов
Глава 4. Меню и диалоговые окна
В рассмотренных нами приложениях пока еще очень мало виджетов. В
крупных приложениях количество кнопок, вызывающих различные функции,
может достигать десятков и сотен. Располагать их на экране, внутри главного
окна непрактично. Для того, чтобы компактно разместить сколько угодно
управляющих кнопок, были придуманы панели меню. Самая привычная из них
располагается тонкой полосой вдоль верхней границы (на севере) интерфейса.
В меню можно определить группы и подгруппы, что делает тонкую полоску
практически
бесконечным
вместилищем,
позволяющим
избавиться
от
большого числа рассеивающих внимание кнопок (например, см. Рис. 3.3).
Диалоговые
окна
уже
частично
знакомы
Вам
в
лице
виджета
QProgressDialog. Вы даже уже знаете, что диалоговые окна могут быть
модальными и немодальными. Диалоговые окна позволяют избавиться от
большого числа строк ввода и помогают выстроить некоторую линию
поведения программы. Действие, предусмотренное в модальном диалоговом
окне, должно быть выполнено пользователем обязательно, его невозможно
обойти стороной. Например, в задании 3.2 Вы добавили кнопку для сохранения
содержимого текстового поля. Но сохранение происходило всегда в файл,
название которого было задано кодом программы. В профессиональных
приложениях в подобном случае обычно появляется диалоговое окно выбора
файла, в котором можно выбрать название файла и место его расположения, а
можно и просто отменить действие.
Это все нам и предстоит попробовать реализовать. Что касается меню, то
здесь мы полностью можем положиться на Qt Designer, а диалоговые окна
нужно будет писать вручную, т.к. они все будут находиться внутри функции.
Тем не менее это не является проблемой, поскольку PyQt5 имеет очень удобные
заготовки на все случаи жизни (ну, или почти на все).
Для начала создадим в Qt Designer простой интерфейс, состоящий из
главного окна размером 320х240. Внутрь поместите многострочное текстовое
51
© А.И.Горожанов
поле и скомпонуйте содержимое главного окна по сетке, по вертикали или по
горизонтали. Значения это не имеет, т.к. в окне будет только одни виджет (см.
Рис. 4.1):
Рис. 4.1 Интерфейс с одним виджетом во всю величину главного окна
Все управление будет происходить из меню. Сделать его очень просто.
Дважды нажмите левой кнопкой мыши на надпись «Пишите здесь». Напишите
имя группы меню. Пусть она называется File. Нажмите Enter. Qt Designer
построит группу меню под названием File (см. Рис. 4.2):
Рис. 4.2 Создание группы меню
Теперь таким же образом, двойным нажатием мыши, создайте пункт меню
Save, добавьте разделитель, а под ним – пунк меню Exit (см. Рис. 4.3):
52
© А.И.Горожанов
Рис. 4.3 Расширение группы меню
Если перейти в режим предпросмотра, то видимыми останутся только
созданные элементы (см. Рис. 4.4):
Рис. 4.4 Созданное меню в режиме предпросмотра
Теперь можно конвертировать файл manu0.ui в menu0.py. Должен
получиться следующий код (см. Код 4.1):
Код 4.1 Интерфейс menu0.py
1.
from PyQt5 import QtCore, QtGui, QtWidgets
2.
3.
4.
5.
class Ui_MainWindow(object):
def setupUi(self, MainWindow):
MainWindow.setObjectName("MainWindow")
53
© А.И.Горожанов
6.
MainWindow.resize(320, 240)
7.
self.centralwidget = QtWidgets.QWidget(MainWindow)
8.
self.centralwidget.setObjectName("centralwidget")
9.
self.verticalLayout =
QtWidgets.QVBoxLayout(self.centralwidget)
10.
self.verticalLayout.setObjectName("verticalLayout")
11.
self.plainTextEdit =
QtWidgets.QPlainTextEdit(self.centralwidget)
12.
self.plainTextEdit.setObjectName("plainTextEdit")
13.
self.verticalLayout.addWidget(self.plainTextEdit)
14.
MainWindow.setCentralWidget(self.centralwidget)
15.
self.menubar = QtWidgets.QMenuBar(MainWindow)
16.
self.menubar.setGeometry(QtCore.QRect(0, 0, 320, 21))
17.
self.menubar.setObjectName("menubar")
18.
MainWindow.setMenuBar(self.menubar)
19.
self.statusbar = QtWidgets.QStatusBar(MainWindow)
20.
self.statusbar.setObjectName("statusbar")
21.
MainWindow.setStatusBar(self.statusbar)
22.
23.
self.retranslateUi(MainWindow)
24.
QtCore.QMetaObject.connectSlotsByName(MainWindow)
25.
26.
def retranslateUi(self, MainWindow):
27.
_translate = QtCore.QCoreApplication.translate
28.
MainWindow.setWindowTitle(_translate("MainWindow",
"MainWindow"))
Постройте код исполняемой программы. Для этого, как обычно,
воспользуйтесь шаблоном (см. Код 4.2):
Код 4.3 Программа menumain0.py
1.
import sys
2.
from menu0 import *
3.
from PyQt5 import QtCore, QtGui, QtWidgets
4.
54
© А.И.Горожанов
5.
6.
class MyWin(QtWidgets.QMainWindow):
def __init__(self, parent=None):
7.
QtWidgets.QWidget.__init__(self, parent)
8.
self.ui = Ui_MainWindow()
9.
self.ui.setupUi(self)
10.
11.
if __name__ == "__main__":
12.
app = QtWidgets.QApplication(sys.argv)
13.
myapp = MyWin()
14.
myapp.show()
15.
sys.exit(app.exec_())
Запустите программу menumain0.py. Меню нажимается и открывается, в
поле можно что-то написать. Теперь можно переходить к настройкам меню.
Прежде всего разберитесь с Кодом 4.1. Найдите в нем переменные пунктов
меню – actionSave и actionExit. С ними мы будем работать в
исполняемом файле.
Начнем с пункта меню Exit. В принципе, выход из любого оконного
приложения на PyQt5 осуществляется нажатием на крестик в верхнем правом
углу экрана. В Windows 7 этот крестик по умолчанию красный. Но многие
программисты стараются деактивировать этот крестик, чтобы не случилось так,
что пользователь закроет программу по ошибке, не сохранив данные. В этом
случае пункт меню Exit необходим. Если деактивировать кнопку закрытия
(крестик) не получается, то стараются делать так, чтобы при нажатии на него
появлялось диалоговое окно, которое бы предупреждало бы пользователя о
возможном закрытии программы. И даже в этом случае пункт меню Exit не
является лишним, потому что к нему можно привязать сочетание клавиш или
просто потому что некоторые пользователи привыкли к его наличию.
Чтобы привязать функцию к пункту меню существует сигнал triggered.
Если Вы хотите, чтобы программа просто закрывалась при нажатии пункта
меню Exit, то в конец функции __init__() достаточно добавить строку:
55
© А.И.Горожанов
self.ui.actionExit.triggered.connect(self.close)
Теперь по функциональности выбор пункта меню Exit и нажатие на
красный крестик идентичны. Код программы находится в файле menumain1.py.
Обратите внимание, что нам не понадобилось даже создавать отдельную
функцию. Метод connect() в качестве аргумента может содержать
стандартную команду. Поскольку через self мы вызываем главное окно,
self.close означает на человеческом языке: «Закрой главное окно».
Пусть при нажатии на крестик и при выборе пункта меню Exit программа
выводит диалоговое окно для подтверждения выхода из программы. Самым
простым способом было бы деактивировать красный крестик, но в текущей
версии PyQt5 это сделать невозможно. При нажатии на крестик происходит
событие closeEvent. Можно перехватить это событие и при его наступлении
проигнорировать. Изучите функцию (см. Код 4.4):
Код 4.4 Функция закрытия окна
1.
2.
def closeEvent(self, e):
result = QtWidgets.QMessageBox.question(self,
"Confirm Dialog", "Really quit?", QtWidgets.QMessageBox.Yes |
QtWidgets.QMessageBox.No, QtWidgets.QMessageBox.No)
3.
4.
5.
6.
if result == QtWidgets.QMessageBox.Yes:
e.accept()
else:
e.ignore()
Здесь e – переменная события closeEvent. Методы accept() и
ignore() соответственно принимают событие или игнорируют его. При
наступлении события выводится диалоговое окно класса QMessageBox (его
вариант для вывода вопроса). У этого диалогового окна пять аргументов:
родительский виджет (self, т.е. главное окно); заголовок окна; текст внутри
окна; через знак | кнопки, которые будут у окна (в нашем окне кнопки Yes и
No); кнопка по умолчанию (No). Задача диалогового окна вопроса – отговорить
56
© А.И.Горожанов
пользователя совершать действие, дать ему время одуматься. Это диалоговое
окно является модальным по умолчанию.
Поместите функцию в класс MyWin(), ниже функции __init__().
Измененный файл получит имя menumain2.py. Запустите программу. Если все
сделано правильно, то при нажатии красного крестика Вы увидите это (см. Рис.
4.5):
Рис. 4.5 Диалоговое окно при закрытии программы
При нажатии No событие закрытия игнорируется, а диалоговое окно
закрывается. При нажатии Yes программа закрывается.
То же самое должно происходить и при выбора пункта меню Exit.
Казалось бы, нужно написать отдельную функцию, которая дублировала бы
содержание функции closeEvent(). Но секрет в том, что ничего делать не
надо. Выбирая пункт меню Exit Вы вызываете то же самое событие close. И
следовательно,
также
вызывается
функция
closeEvent().
Поэкспериментируйте с закрытием программы двумя способами. В обоих
случаях вид и поведение диалогового совершенно одинаково.
***
Проверьте и расширьте свое понимание (4.1): Как изменить код
программы menumain2.py, чтобы при нажатии на крестик не происходило бы
57
© А.И.Горожанов
ничего, а диалоговое окно вопроса выводилось бы только при выбора пункта
меню Exit?
(4.2): Добавьте в диалоговое окно третью кнопку со значением Cancel.
(4.3): Модифицируйте программу menumain2.py так, чтобы текст внутри
диалогового окна был размещен в несколько строк, например таким образом
(см. Рис. 4.6):
Рис. 4.6 Диалоговое окно с текстом из двух строк
***
Перейдем
к
пункту
меню
Save.
Для
сохранения
содержимого
многострочного текстового поля в файл нужно написать функцию и привязать
ее к пункту меню. Такая функция у Вас уже есть, если Вы выполнили задание
3.2:
def saveToFile(self):
self.writeFile = open("text.txt", 'w', encoding='utf-8')
self.writeFile.write(self.ui.plainTextEdit.toPlainText())
self.writeFile.close()
Для того, чтобы привязать функцию к пункту меню, добавьте в конец
функции __init__() строку:
self.ui.actionSave.triggered.connect(self.saveToFile)
Полный текст программы находится в файле menumain21.py. Она
сохраняет текст в файл text.txt. После сохранения в строку состояния
58
© А.И.Горожанов
выводится
соответствующая
надпись,
которая
исчезает,
как
только
пользователь наводит мышь на меню File.
Как Вы уже наверное догадались, программа, над которой мы работаем в
этой главе, является некоторым подобием самого простого текстового
редактора. Сохранение текста до сих пор происходило в файл, название
которого устанавливалось автоматически. Но пользователь может захотеть
назвать файл так, как ему нравится. Поэтому мы дополним функцию
saveToFile() диалоговым окном сохранения файла. Такое диалоговое окно
уже имеется в готовом виде, поэтому его код нужно просто выучить как
формулу. Для удобства приведем код функции целиком (см. Код 4.5):
Код 4.5 Функция saveToFile() с диалоговым окном сохранения файла
1.
def saveToFile(self):
2.
options = QtWidgets.QFileDialog.Options()
3.
self.fileName, _ =
QtWidgets.QFileDialog.getSaveFileName(self, "Save To File",
"", "Text Files (*.txt)", options=options)
4.
5.
if self.fileName:
self.writeFile = open(self.fileName, 'w',
encoding='utf-8')
6.
self.writeFile.write(self.ui.plainTextEdit.toPlainText())
7.
self.writeFile.close()
8.
self.ui.statusbar.showMessage('Saved to %s' %
self.fileName)
Код всей программы находится в файле manumain22.
Виджет QFileDialog достаточно сложен, но разобраться в нем можно.
Строка 2 создает переменную, в которой хранятся опции диалогового окна. В
таком написании все опции имеют значения по умолчанию. И этого пока
вполне достаточно. Строка 3 объявляет переменную диалогового окна. В ней
хранится название файла, который выберет пользователь. Пока не обращайте
59
© А.И.Горожанов
внимания на запятую и подчеркивание. У диалогового окна пять аргументов:
родительский виджет; заголовок (Save To File); имя файла по умолчанию (у нас
ничего не выбрано); фильтр (окно будет показывать только файлы с
расширением .txt; опции (установлены по умолчанию). Выглядит диалоговое
окно следующим образом (см. Рис. 4.7):
Рис. 4.7 Интерфейс диалогового окна сохранения файла
Как Вы можете заметить, открывается стандартный файловый диалог
Windows. Если ввести имя файла, которое уже есть в папке, появится еще одно
диалоговое окно, требующее подтверждения перезаписи файла. Это делается
автоматически, никакого дополнительного кода писать не надо.
После нажатия кнопки «Сохранить» программа переходит к строке 4. Если
пользователь ввел какое-то имя файла, то происходит запись в файл, а в строку
состояния выводится соответствующая надпись. Если ничего не было введено,
то функция saveToFile() завершается.
Проведем
еще
одну
модификацию.
Пусть
программа
сохраняет
содержимое многострочного текстового поля не в текстовый файл, а как веб60
© А.И.Горожанов
страницу, т.е. в формате .html. Причем сделать это нужно максимально
используя встроенные возможности PyQt5.
Итак, мы получали текст из текстового поля с помощью метода
toPlainText(). В PyQt5 есть замечательный метод toHtml(), который
превращает содержимое текстового поля в код html. Однако применить этот
метод к виджету QPlainTextEdit мы не можем. Сперва мы должны
превратить его в виджет QTextEdit. Для этого в файле menu0.py надо
исправить строку, объявляющую переменную textPlainEdit. Саму переменную
можно оставить такой же, а вот виджет, который в нее помещается нужно
исправить. Строка изменится с
self.plainTextEdit = QtWidgets.QPlainTextEdit(self.centralwidget)
на
self.plainTextEdit = QtWidgets.QTextEdit(self.centralwidget)
Сохраните файл как menu1.py. Внешне ничего не изменится, но теперь мы
можем применить к виджету метод toHtml(). Сохраните файл menumain22.py
как menumain23.py и проведите соответствующие модификации. Не забудьте,
что
файл
интерфейса
saveToFile()четвертый
изменился
аргумент
на
menu1.py.
изменится
на
В
"HTML
функции
Files
(*.html)". Строка, которая производит запись в файл, примет вид:
self.writeFile.write(self.ui.plainTextEdit.toHtml())
Вот что получается в итоге (см. Код 4.6):
Код 4.6 Программа menumain23.py
1.
import sys
2.
from menu1 import *
3.
from PyQt5 import QtCore, QtGui, QtWidgets
4.
5.
6.
class MyWin(QtWidgets.QMainWindow):
def __init__(self, parent=None):
7.
QtWidgets.QWidget.__init__(self, parent)
8.
self.ui = Ui_MainWindow()
9.
self.ui.setupUi(self)
61
© А.И.Горожанов
10.
11.
self.ui.actionExit.triggered.connect(self.close)
12.
self.ui.actionSave.triggered.connect(self.saveToFile)
13.
14.
def saveToFile(self):
15.
options = QtWidgets.QFileDialog.Options()
16.
self.fileName, _ =
QtWidgets.QFileDialog.getSaveFileName(self, "Save To File",
"", "HTML Files (*.html)", options=options)
17.
18.
if self.fileName:
self.writeFile = open(self.fileName, 'w',
encoding='utf-8')
19.
self.writeFile.write(self.ui.plainTextEdit.toHtml())
20.
self.writeFile.close()
21.
self.ui.statusbar.showMessage('Saved to %s' %
self.fileName)
22.
23.
24.
def closeEvent(self, e):
result = QtWidgets.QMessageBox.question(self,
"Confirm Dialog", "Really quit?", QtWidgets.QMessageBox.Yes
| QtWidgets.QMessageBox.No, QtWidgets.QMessageBox.No)
25.
26.
27.
28.
if result == QtWidgets.QMessageBox.Yes:
e.accept()
else:
e.ignore()
29.
30.
if __name__ == "__main__":
31.
app = QtWidgets.QApplication(sys.argv)
32.
myapp = MyWin()
33.
myapp.show()
34.
sys.exit(app.exec_())
62
© А.И.Горожанов
Запустите программу. Напишите что-нибудь в текстовом поле, например
следующее (см. Рис. 4.8):
Рис. 4.8 Ввод в текстовое поле нескольких строк
Теперь сохраните содержимое при помощи пункта меню Save и откройте
файл в браузере. Вы увидите абсолютно то же самое (см. Рис. 4.9):
Рис. 4.9 Вид сохраненного текста в браузере
Обратите внимание на то, что PyQt5 при преобразовании текста в вебстраницу сохранил не только его содержание и абзацы, но и внешний вид
(размер, цвет, шрифт). Если вы работаете в браузере Google Chrome, нажмите
правой кнопкой мыши на поле браузера и выберите в появившемся
контекстном меню пункт «Просмотр кода страницы». Вы увидите, как PyQt5
преобразовал текст (см. Код 4.6):
Код 4.6 Текст, преобразованный в код HTML (файл texthtml.html).
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN"
"http://www.w3.org/TR/REC-html40/strict.dtd">
63
© А.И.Горожанов
<html><head><meta name="qrichtext" content="1" /><style
type="text/css">
p, li { white-space: pre-wrap; }
</style></head><body style=" font-family:'MS Shell Dlg 2';
font-size:8.25pt; font-weight:400; font-style:normal;">
<p style=" margin-top:0px; margin-bottom:0px; marginleft:0px; margin-right:0px; -qt-block-indent:0; textindent:0px;">Это предложение.</p>
<p style=" margin-top:0px; margin-bottom:0px; marginleft:0px; margin-right:0px; -qt-block-indent:0; textindent:0px;">А это еще одно предложение на новой
строке.</p></body></html>
Метод toHtml() сэкономил нам очень много работы.
***
Проверьте и расширьте свое понимание (4.4): Представьте, что
пользователь набрал в текстовом поле свой текст, не сохранил его и нажал на
крестик. Модифицируйте программу menumain2.py так, чтобы вместо
диалогового окна, изображенного на Рис. 4.5 выводилось диалоговое окно с
текстом Save text before quit? и с тремя кнопками: Yes, No, Cancel. При нажатии
Yes будет выводиться диалоговое окно сохранения в текстовый файл, а затем
последует выход из программы. При нажатии No программа просто
завершится. Нажатие на Cancel просто вернет пользователя обратно в
программу без всяких изменений и сохранений.
(4.5): Любой текстовый редактор может не только сохранять, но и
открывать сохраненные файлы. Модифицируйте программу menumain22.py так,
чтобы в меню File добавился пункт Open, при выборе которого выводилось бы
диалоговое окно открытия текстового файла, а содержимое открываемого
файла помещалось бы в текстовое поле для редактирования.
***
В этой главе Вы поработали с диалоговыми окнами подтверждения выхода
из программы, сохранения и открытия файла. То, что получилось в итоге,
64
© А.И.Горожанов
можно даже назвать очень простым текстовым редактором. Одной их
важнейших частей главы было сохранение содержимого текстового поля в виде
веб-страницы.
65
© А.И.Горожанов
Глава 5. Первые программы учебного назначения
В этой главе мы приступим к написанию программ для ЭВМ учебного
назначения. Для этого будут задействованы новые виджеты, позволяющие
разворачивать на малой площади интерфейса большое количество информации.
Поставим перед собой задачу создать карточки с небольшим количеством
теоретического материала, за которым сразу следует контрольный вопрос.
Вопросы будут выполнены в виде кнопок радио, т.е. допускать выбор одного
варианта из нескольких.
Интерфейс программы мы сделаем в Qt Designer, и состоять он будет из
большого количества виджетов, вложенных друг в друга. Инспектор объектов
Qt Designer примет вид (см. Рис. 5.1):
Рис. 5.1 Инспектор объектов интерфейса learn0.ui
66
© А.И.Горожанов
Обратите внимание на новые виджеты, которые используются в
интерфейсе и на их иерархическую структуру. Внутри главного окна все
виджеты компонуются по горизонтали. И так происходит со всеми
вложенными виджетами, имеющими компоновку. В главное окно помещается
QTabWidget – панель с вкладками. Всего в интерфейсе присутствуют три
вкладки, которые имеют абсолютно одинаковое наполнение. Поэтому
рассмотрим подробно только первую из них.
Внутрь вкладки помещен виджет QScrollArea. Его предназначение –
быть контейнером для какого-то числа других виджетов. Даже если в него
поместить сотни и тысячи виджетов, то все они будут доступны для
67
© А.И.Горожанов
пользователя через полосу прокрутки. Очень похожее происходит в веббраузере, когда мы открываем большую веб-страницу, и чтобы добраться до ее
конца, нам нужно прокрутить содержимое, в то время как на экране может быть
видна только его часть. Если не использовать QScrollArea, то полоса
прокрутки не появится, и доступны будут только те виджеты, которые
помещаются в видимую часть главного окна. До сих пор это не было для нас
проблемой. Но представьте, что Вам предстоит написать тест, состоящий из 30
вопросов. Уже в этом случае без контейнера с полосой прокрутки обойтись
будет крайне затруднительно.
Внутри контейнера QScrollArea находятся два виджета: поле ввода
QTextArea и еще один контейнер QFrame, в котором находятся четыре
виджета: одна надпись и три кнопки радио. Три кнопки радио объединены в
группу, что позволяет программе воспринимать их как единое целое. Смысл
группы в том, что внутри нее может быть одновременно нажата только одна
кнопка радио (раньше были такие радиоприемники, у которых было три
кнопки,
и
при
нажатии
очередной
кнопки
другая
нажатая
кнопка
автоматически отскакивала). Для того, чтобы объединить несколько кнопок
радио в группу, нужно выделить их и нажать правую кнопку мыши, в
появившемся контекстном меню выбрать «Назначить группу кнопок».
Интерфейс, соответствующий Рис. 5.1 находится в файле learn0.ui (и learn0.py).
Обратите внимание на большой объем и высокую сложность кода. Я думаю,
что достоинства работы в Qt Designer уже стали для Вас очевидными.
На первом этапе разработки можно сразу поместить в интерфейс тексты
(теорию и вопросы). Этот интерфейс будет называться learn1.py. Пусть это пока
будут примитивный текст и примитивные вопросы. Займемся заполнением
полей ввода. Если дважды кликнуть левой кнопкой мыши на виджете
QTextEdit, то появится окно редактора HTML, в котором не просто можно
набрать текст, но и произвести его форматирование, вставить картинку и
гиперссылку, а также перейти в режим просмотра кода HTML (см. Рис. 5.2):
68
© А.И.Горожанов
Рис. 5.2 Окно редактора содержимого виджета QTextEdit
Также заполним формулировку вопроса и надписи кнопок радио (см. Рис.
5.3):
Рис. 5.3 Заполненная первая вкладка интерфейса learn1.py
69
© А.И.Горожанов
Полученный интерфейс сохранен в файле learn1.py, главным файлом
является learnmain1.py. В нем еще нет ни одной функции, созданием которых
нам и предстоит сейчас заняться. Но перед этим нужно поставить перед собой
цель. Чтобы наш материал имел какую-то методическую ценность, он должен
быть
правильно
организован.
Сильная
сторона
электронных
учебных
материалов заключается в том, что они структурируют работу студента,
направляют его на путь, который разработал преподаватель. Наш интерфейс
состоит из трех карточек. Пользователь читает текстовую информацию, а для
того, чтобы запомнить ее лучше – выполняет задание. Разумно начинать с
первой карточки, затем переходить ко второй, а завершать третьей. При работе
с бумажной книгой можно читать все что угодно, начиная с чего угодно. Мы
сделаем по-другому: нельзя будет переходить к последующей карточке, если не
сделано задание предыдущей. Сделанным оно будет считаться только в случае
правильного ответа на задание. Само собой разумеется, что наша программа –
70
© А.И.Горожанов
очень простая, но принцип организации материала в ней можно считать
типовым, т.е. переносимым на сколь угодно сложные учебные материалы.
В программе learnmain1.py мы закроем вторую и третью вкладки. Для
этого в функцию __init__() нужно добавить две строки:
self.ui.Tab3.setTabEnabled(1, False)
self.ui.Tab3.setTabEnabled(2, False)
Метод setTabEnabled() имеет два аргумента: номер вкладки (а мы
помним, что программисты считают от нуля) и переменную типа bool, которая
включает или выключает доступность вкладки.
По нашему сценарию, при правильном ответе на вопрос первой вкладки
станет доступна вторая вкладка. Самый простой способ сделать это – привязать
к кнопке радио с правильным ответом функцию, которая откроет доступ к
следующей вкладке. Также будет хорошо, если пользователь дополнительно
получит сообщение, подтверждающее ответ. Это не обязательно должно быть
окно оповещения. Можно вывести информацию в строку состояния. Напишите
функцию следующего содержания:
def correctAns1(self):
self.ui.statusbar.showMessage("Correct!
Tab
2
is
enabled
now.", 5000)
self.ui.tab_1.setEnabled(False)
self.ui.Tab3.setTabEnabled(1, True)
Свяжите ее с кнопкой радио radioButton_2 с помощью добавления к
функции __init__() строки:
self.ui.radioButton_2.clicked.connect(self.correctAns1)
Заметьте, что при выборе правильного ответа вся вкладка недоступна для
интеракции, т.е. ее можно читать, но изменить ничего уже нельзя.
То же самое проделайте и для двух остальных вкладок, чтобы получилось
следующее (см. Код 5.1):
Код 5.1 Полный код программы learnmain1.py
1.
import sys
71
© А.И.Горожанов
2.
from learn1 import *
3.
from PyQt5 import QtCore, QtGui, QtWidgets
4.
5.
6.
class MyWin(QtWidgets.QMainWindow):
def __init__(self, parent=None):
7.
QtWidgets.QWidget.__init__(self, parent)
8.
self.ui = Ui_MainWindow()
9.
self.ui.setupUi(self)
10.
self.ui.Tab3.setTabEnabled(1, False)
11.
self.ui.Tab3.setTabEnabled(2, False)
12.
self.ui.radioButton_2.clicked.connect(self.correctAns1)
13.
self.ui.radioButton_6.clicked.connect(self.correctAns2)
14.
self.ui.radioButton_9.clicked.connect(self.correctAns3)
15.
16.
def correctAns1(self):
17.
self.ui.statusbar.showMessage("Correct! Tab 2 is
enabled now.", 5000)
18.
self.ui.tab_1.setEnabled(False)
19.
self.ui.Tab3.setTabEnabled(1, True)
20.
21.
def correctAns2(self):
22.
self.ui.statusbar.showMessage("Correct! Tab 3 is
enabled now.", 5000)
23.
self.ui.tab_2.setEnabled(False)
24.
self.ui.Tab3.setTabEnabled(2, True)
25.
26.
def correctAns3(self):
27.
self.ui.statusbar.showMessage("Correct! Done!",
5000)
28.
self.ui.tab_3.setEnabled(False)
29.
72
© А.И.Горожанов
30.
if __name__ == "__main__":
31.
app = QtWidgets.QApplication(sys.argv)
32.
myapp = MyWin()
33.
myapp.show()
34.
sys.exit(app.exec_())
Мы
составили
хорошо
работающую
программу,
которая
решает
поставленную перед ней задачу. Однако на практике такие маленькие проекты
мало применимы. Вряд ли кто-то будет создавать отдельную программу, чтобы
выучить три небольших теоретических положения. Настоящие программы
должны быть более универсальными. К таким программам мы обязательно
придем, но немного позже. А пока сделаем еще один маленький шаг.
В рассмотренной программе вопросы на каждой вкладке никак не
меняются.
Формулировка,
конечно,
никак
и
не
изменится,
но
последовательность вариантов ответов всегда одна и та же. Если пользователь
будет проходить это задание несколько раз, то он запомнит в конце концов
последовательность правильных ответов (2-3-3). Он скорее всего даже не будет
больше читать варианты ответов, а просто нажимать 2-3-3 еще и еще. Это очень
плохо, и мы как профессиональные педагоги допустить такого не можем!
Бороться с этим можно, например, постоянно перемешивая варианты ответов.
Так, чтобы 2-3-3 запоминать не имело бы смысла, а всякий раз нужно было бы
читать ответы. Мы модифицируем программу learnmain1.py так, чтобы при
сохранении функциональности варианты ответов всякий раз появлялись бы в
другой последовательности, но программа бы все равно при проверке
реагировала бы только на правильный ответ.
Получается, что при каждом запуске программы интерфейс (текст кнопок
радио) будет выглядеть по-разному. Поэтому в самом файле интерфейса этот
текст можно просто удалить (файл learn2.py). В исполняющей программе
изменяться
функции,
а
также
добавятся
некоторые
переменные.
Последовательность действий программы изложена в шести пунктах:
73
© А.И.Горожанов
1. Правильные ответы помещаются в три переменные типа String.
2. Варианты ответов помещаются в списки (в каждом по три переменные
типа String).
3. Перед построением интерфейса списки перемешиваются в случайном
порядке при помощи метода shuffle() модуля random.
4. К кнопкам радио добавляется текст.
5. К группам кнопок привязывается сигнал buttonClicked, по
наступлении которого вызывается соответствующая функция.
5. Внутри каждой функции происходит проверка, которая – перебирая все
кнопки радио группы кнопок – устанавливает, какая именно кнопка нажата.
6. Если текст этой кнопки соответствует тексту правильного ответа (он
хранится в переменной, см. п. 1), то выполняются действия по выводу надписи
в строке состояния и т.д.
Получается следующий код (см. Код 5.2):
Код 5.2 Программа learnmain2.py
1.
import sys
2.
import random
3.
from learn2 import *
4.
from PyQt5 import QtCore, QtGui, QtWidgets
5.
6.
class MyWin(QtWidgets.QMainWindow):
7.
8.
correct1 = "формальная знаковая система,
предназначенная для записи компьютерных программ"
9.
correct2 = "и естественным языкам, и языкам
программирования"
10.
correct3 = "В программе на языке Python
смыслоразличительную роль играет количество отступов от
левого края"
11.
12.
variants1 = ["любая знаковая система", correct1, "любая
74
© А.И.Горожанов
формальная знаковая система"]
13.
variants2 = ["только естественным языкам", "только
языкам программирования", correct2]
14.
variants3 = ["В конце каждой строки программы на языке
Python должна стоять точка с запятой", "В программе на
языке Python не должно быть пустых строк", correct3]
15.
16.
def __init__(self, parent=None):
17.
QtWidgets.QWidget.__init__(self, parent)
18.
self.ui = Ui_MainWindow()
19.
self.ui.setupUi(self)
20.
21.
random.shuffle(self.variants1)
22.
random.shuffle(self.variants2)
23.
random.shuffle(self.variants3)
24.
25.
self.ui.radioButton.setText(self.variants1[0])
26.
self.ui.radioButton_2.setText(self.variants1[1])
27.
self.ui.radioButton_3.setText(self.variants1[2])
28.
29.
self.ui.radioButton_4.setText(self.variants2[0])
30.
self.ui.radioButton_5.setText(self.variants2[1])
31.
self.ui.radioButton_6.setText(self.variants2[2])
32.
33.
self.ui.radioButton_7.setText(self.variants3[0])
34.
self.ui.radioButton_8.setText(self.variants3[1])
35.
self.ui.radioButton_9.setText(self.variants3[2])
36.
37.
self.ui.Tab3.setTabEnabled(1, False)
38.
self.ui.Tab3.setTabEnabled(2, False)
39.
40.
self.ui.buttonGroup.buttonClicked.connect(self.correctAns1)
41.
75
© А.И.Горожанов
self.ui.buttonGroup_2.buttonClicked.connect(self.correctAns
2)
42.
self.ui.buttonGroup_3.buttonClicked.connect(self.correctAns
3)
43.
44.
45.
46.
def correctAns1(self):
for rb in self.ui.buttonGroup.buttons():
if rb.isChecked():
47.
if rb.text() == self.correct1:
48.
self.ui.statusbar.showMessage("Correct!
Tab 2 is enabled now.", 5000)
49.
self.ui.tab_1.setEnabled(False)
50.
self.ui.Tab3.setTabEnabled(1, True)
51.
52.
53.
54.
def correctAns2(self):
for rb in self.ui.buttonGroup_2.buttons():
if rb.isChecked():
55.
if rb.text() == self.correct2:
56.
self.ui.statusbar.showMessage("Correct!
Tab 3 is enabled now.", 5000)
57.
self.ui.tab_2.setEnabled(False)
58.
self.ui.Tab3.setTabEnabled(2, True)
59.
60.
61.
62.
def correctAns3(self):
for rb in self.ui.buttonGroup_3.buttons():
if rb.isChecked():
63.
if rb.text() == self.correct3:
64.
self.ui.statusbar.showMessage("Correct!
Done!", 5000)
65.
self.ui.tab_3.setEnabled(False)
66.
67. if __name__ == "__main__":
68.
app = QtWidgets.QApplication(sys.argv)
76
© А.И.Горожанов
69.
myapp = MyWin()
70.
myapp.show()
71.
sys.exit(app.exec_())
Здесь Python и PyQt работают как отличная команда: Python предоставляет
метод для перемешивания списка, а PyQt строит интерфейс по полученному
результату и удобно работает с группами кнопок. Правильные ответы
помещены в переменные для того, чтобы и внутри списка (строки 12-14), и при
последующей проверке (строки 47, 55, 63) не повторять дважды длинный текст
и не ошибиться при его написании. Это страховка, которая позволяет избежать
ошибок при написании кода.
Проведем еще одну важную модификацию. Вынесем все учебное
содержание (текст, формулировку вопроса и варианты ответов) в отдельный
файл. Таким образом, сама программа будет состоять только из пустого
интерфейса и исполняющей программы, которая сразу после запуска будет
считывать файл и работать дальше в соответствии с его содержимым.
Получится, что меняя файлы содержания, мы каждый раз будем получать
новые учебные материалы, не меняя ничего в программе, как мы не меняем
ничего, например, в браузере, когда просматриваем разные веб-страницы. Мы
только даем браузеру разные адреса, которые он обрабатывает.
Важно выбрать тип файла для хранения учебного наполнения. Это может
быть простой текстовый файл, но такой вариант не является наилучшим. По
сути мы создаем пусть небольшую, но все-таки базу данных, а здесь есть
общепринятые стандарты, которые нашли свое отражение и в языках
программирования, включая Python. Мы сделаем выбор в пользу файла типа
XML [What is XML]. Если вам знаком HTML, то работа с XML не доставит
особого труда. Главное помнить два правила: (1) ничего не должно быть вне
тегов и (2) теги могут иметь атрибуты.
Кстати, файлы интерфейса ui, которыми оперирует
представляют собой не что иное как базы данных XML.
77
Qt Designer,
© А.И.Горожанов
В Python предусмотрены несколько модулей для работы с базами данных
XML [tutorialspoint-Python XML], в частности xml.dom. Но для начала работы
с базой данных нужно ее создать и изучить (см. Код 5.3):
Код 5.3 Файл learn.xml
1.
<?xml version="1.0" encoding="utf-8"?>
2.
<content>
3.
<text question = "Язык программирования - это" answers =
"любая знаковая система**?**формальная знаковая система,
предназначенная для записи компьютерных программ**?**любая
формальная знаковая система" correct = "формальная знаковая
система, предназначенная для записи компьютерных
программ">Языком программирования называют формальную
знаковую систему, предназначенную для записи компьютерных
программ. Язык программирования определяет набор
лексических, синтаксических и семантических правил,
задающих внешний вид программы и действия, которые выполнит
исполнитель (компьютер) под её управлением.</text>
4.
<text question = "Явление синонимии свойственно" answers =
"только естественным языкам**?**только языкам
программирования**?**и естественным языкам, и языкам
программирования" correct = "и естественным языкам, и
языкам программирования">Так же как и в естественных
языках, во многих языках программирования одно и то же
содержание может быть выражено несколькими способами, что
позволяет говорить о наличии в них явления
синонимии.</text>
5.
<text question = "Выберите одно верное утверждение:"
answers = "В конце каждой строки программы на языке Python
должна стоять точка с запятой**?**В программе на языке
Python не должно быть пустых строк**?**В программе на языке
Python смыслоразличительную роль играет количество отступов
от левого края" correct = "В программе на языке Python
смыслоразличительную роль играет количество отступов от
левого края">Особенностью синтаксиса языка программирования
78
© А.И.Горожанов
Python является отсутствие знаков препинания в конце каждой
строки. Однако смыслоразличительную роль играет количество
отступов от левого края (абсолютно и относительно
количества отступов у предыдущей строки). Если количество
отступов будет нарушено, интерпретатор выдаст
ошибку.</text>
6.
</content>
Весь файл состоит из шести достаточно длинных строк. Строка 1 является
информационной, из нее компьютер понимает, что перед ним файл XML,
имеющий
кодировку
utf-8.
Строки
2
и
6
содержат
соответственно
открывающий и закрывающий тег content. В этом теге находится собственно
содержание базы данных, которое распределено между трех тегов text.
Содержанием тега является текст для текстового поля (теоретическая
информация), а в атрибутах находятся вопрос (question), варианты ответов
(answers) и правильный ответ (correct). Обратите особое внимание на то,
как записаны варианты ответов. Они являются одним атрибутом, но разделены
странной последовательностью символов **?**. Странной она сделана
намеренно, чтобы программа потом легко смогла отделить один вариант ответа
от другого. Для того, чтобы разделение было всегда корректным, выбираются
такие сочетания символов, которые не могут встретиться в естественном языке.
Можно было бы выбрать, например, «гжгжгЧгж» или «эигЙигэиг».
Теперь всю эту информацию из файла интерфейса можно убрать (см. файл
learn3.py). Здесь надо руководствоваться тем принципом, что более короткая
программа
предпочтительнее
более
длинной.
Кстати
сказать,
главная
программа получилась несколько длиннее (см. Код 5.4):
Код 5.4 Программа learnmain3.py
1.
import sys
2.
import random
3.
import xml.dom.minidom
79
© А.И.Горожанов
4.
from learn3 import *
5.
from PyQt5 import QtCore, QtGui, QtWidgets
6.
7.
class MyWin(QtWidgets.QMainWindow):
8.
9.
text = []
10.
questions = []
11.
variants = []
12.
correct = []
13.
14.
def __init__(self, parent=None):
15.
QtWidgets.QWidget.__init__(self, parent)
16.
self.ui = Ui_MainWindow()
17.
self.ui.setupUi(self)
18.
19.
self.dom = xml.dom.minidom.parse('learn.xml')
20.
self.collection = self.dom.documentElement
21.
self.linesArr =
self.collection.getElementsByTagName("text")
22.
23.
for line in self.linesArr:
24.
self.text.append(line.childNodes[0].data)
25.
self.questions.append(line.getAttribute('question'))
26.
self.variants.append(line.getAttribute('answers').split('**
?**'))
27.
self.correct.append(line.getAttribute('correct'))
28.
29.
random.shuffle(self.variants[0])
30.
random.shuffle(self.variants[1])
31.
random.shuffle(self.variants[2])
32.
80
© А.И.Горожанов
33.
self.ui.textEdit.setText(self.text[0])
34.
self.ui.textEdit_2.setText(self.text[1])
35.
self.ui.textEdit_3.setText(self.text[2])
36.
37.
self.ui.label.setText(self.questions[0])
38.
self.ui.label_2.setText(self.questions[1])
39.
self.ui.label_3.setText(self.questions[2])
40.
41.
self.ui.radioButton.setText(self.variants[0][0])
42.
self.ui.radioButton_2.setText(self.variants[0][1])
43.
self.ui.radioButton_3.setText(self.variants[0][2])
44.
45.
self.ui.radioButton_4.setText(self.variants[1][0])
46.
self.ui.radioButton_5.setText(self.variants[1][1])
47.
self.ui.radioButton_6.setText(self.variants[1][2])
48.
49.
self.ui.radioButton_7.setText(self.variants[2][0])
50.
self.ui.radioButton_8.setText(self.variants[2][1])
51.
self.ui.radioButton_9.setText(self.variants[2][2])
52.
53.
self.ui.Tab3.setTabEnabled(1, False)
54.
self.ui.Tab3.setTabEnabled(2, False)
55.
56.
self.ui.buttonGroup.buttonClicked.connect(self.correctAns1)
57.
self.ui.buttonGroup_2.buttonClicked.connect(self.correctAns
2)
58.
self.ui.buttonGroup_3.buttonClicked.connect(self.correctAns
3)
59.
60.
61.
def correctAns1(self):
for rb in self.ui.buttonGroup.buttons():
81
© А.И.Горожанов
62.
if rb.isChecked():
63.
if rb.text() == self.correct[0]:
64.
self.ui.statusbar.showMessage("Correct!
Tab 2 is enabled now.", 5000)
65.
self.ui.tab_1.setEnabled(False)
66.
self.ui.Tab3.setTabEnabled(1, True)
67.
68.
69.
70.
def correctAns2(self):
for rb in self.ui.buttonGroup_2.buttons():
if rb.isChecked():
71.
if rb.text() == self.correct[1]:
72.
self.ui.statusbar.showMessage("Correct!
Tab 3 is enabled now.", 5000)
73.
self.ui.tab_2.setEnabled(False)
74.
self.ui.Tab3.setTabEnabled(2, True)
75.
76.
77.
78.
def correctAns3(self):
for rb in self.ui.buttonGroup_3.buttons():
if rb.isChecked():
79.
if rb.text() == self.correct[2]:
80.
self.ui.statusbar.showMessage("Correct!
Done!", 5000)
81.
self.ui.tab_3.setEnabled(False)
82.
83.
if __name__ == "__main__":
84.
app = QtWidgets.QApplication(sys.argv)
85.
myapp = MyWin()
86.
myapp.show()
87.
sys.exit(app.exec_())
В строке 3 импортируется модуль для работы с файлами XML. Строки 912 создают четыре списка для хранения текстов, вопросов, вариантов ответа и
правильного варианта. Эти списки заполнятся информацией из файла
learn.xml. Строка 19 создает переменную объектной модели документа
82
© А.И.Горожанов
(DOM), с помощью которой можно извлекать информацию из кода XML.
Переменная collection получает в качестве значения содержимое главного
тега (content), а в следующей строке создается список linesArr, в который
записываются все теги text. В строке 23 цикл for перебирает по очереди все
теги text, т.е. объекты списка linesArr. У нас их три, но могло бы быть и
сто, и десять тысяч. В строках 24-27 заполняются объявленные в строках 9-12
списки. Обратите внимание на то, что список variants является двухмерным, т.е.
в каждый из трех его элементов представляет собой список из трех элементов.
Именно здесь варианты ответов из одного атрибута превращаются в три с
помощью разделения по последовательности **?**. Далее списки вариантов
ответов перемешиваются, в строках 33-51 заполняется интерфейс и происходит
все то же, что было в предыдущей версии программы.
Теперь, изменяя файл XML, мы будем получать разные учебные
материалы, впрочем строго ограниченные по структуре. При таком алгоритме
мы не можем поменять число вариантов ответов или количество вопросов
внутри вкладки. Если мы захотим добавить еще по одному варианту ответов к
имеющемся трем, то изменять надо будет и файл интерфейса, и основную
программу, и, конечно, файл XML.
***
Проверьте и расширьте свое понимание (5.1): В отличие от программы
learnmain2.py в программе learnmain3.py текст в виджете QTextEdit имеет
размер 8. Как с минимальными изменениями программы увеличить текст
виджета до 12?
(5.2): Кроме того, что в программе learnmain2.py текст был большего
размера, он имел выделение полужирным ключевых слов. Как сделать это для
программы learnmain3.py?
***
В этой главе Вы поработали с новыми виджетами (кнопки радио),
группами кнопок, а также применили алгоритм перемешивания, используя базу
83
© А.И.Горожанов
данных на основе XML. Теперь можно переходить к более сложным
программам для ЭВМ учебного назначения.
84
© А.И.Горожанов
Глава 6. Программные тренажеры – основы
Программные тренажеры являются важной частью самостоятельной
работы студентов. Они помогают организовать учебный материал, позволяют
чувствовать прогрессию в изучении дисциплины, а это значит – усилить
мотивацию к дальнейшему обучению. (Программными они называются потому,
что представляют собой программу для ЭВМ).
Мы напишем программный тренажер, улучшив программы предыдущей
главы. Задача будет состоять в том, чтобы разработать графическую оболочку,
которая бы заполнялась материалом из базы данных и удовлетворяла бы
следующим требованиям:
1. Все учебное содержание должно загружаться из файла XML.
2. Интерфейс должен иметь поле, с полосой прокрутки, в котором будут
располагаться вопросы с вариантами ответов в виде кнопок радио.
3. Вопросов может быть сколько угодно, вариантов ответов для каждого
вопроса может быть произвольное количество (не одинаковое для всех
вопросов).
4. Всякий раз при загрузке программа должна перемешивать не только
вопросы, но и варианты ответов в них, чтобы у пользователя не происходило
запоминания расположения правильных ответов.
5. Интерфейс должен иметь кнопку, запускающую процесс проверки,
результатом которого должен быть процент правильного выполнения всех
заданий. Результат должен высчитываться точно, не зависимо от количества
вопросов в тренажере.
Для решения задачи нужно разработать структуру файла XML, интерфейс
тренажера и исполняющую программу. Интерфейс будет минималистичен (т.к.
все заполнение материалом будет происходить в исполняющем файле),
включая несколько виджетов (см. Рис. 6.1):
Рис. 6.1 Инспектор объектов интерфейса shell.ui
85
© А.И.Горожанов
Внешне он также будет прост (см. Рис. 6.2):
Рис. 6.2 Интерфейс shell.ui
Все содержимое файла XML разместится в виджете QScrollArea,
который не имеет пока даже никакой компоновки. Кнопка Check будет
запускать процесс проверки, а результат будет выведен в строку состояния.
Теперь создадим файл XML. Не обращайте внимания на примитивные
вопросы, их можно в любой момент заменить на сколь угодно сложные (см.
Код 6.1):
Код 6.1 Файл bd.xml
1.
<?xml version="1.0" encoding="utf-8"?>
2.
<content>
3.
<q ans = "2**?**3**?**4**?**5" cor = "4">Сколько будет
2+2?</q>
86
© А.И.Горожанов
4.
<q ans = "5**?**6**?**7" cor = "7">Сколько будет 5+2?</q>
5.
<q ans = "7**?**8**?**9**?**10**?**11" cor = "9">Сколько
будет 2+7?</q>
6.
<q ans = "4**?**5**?**6**?**7" cor = "6">Сколько будет 126?</q>
7.
<q ans = "10**?**20**?**30**?**40**?**50" cor =
"20">Сколько будет 10*2</q>
8.
<q ans = "11**?**22**?**33**?**44**?**55" cor =
"33">Сколько будет 55-22</q>
9.
</content>
На строку 1 мы уже не обращаем внимания, потому что она содержит
стандартный код. Главным тегом является двойной тег content. Далее видно,
что в тренажере будет шесть вопросов, по количеству тегов q. Конечно, можно
было бы написать вместо одной буквы слово question, так было бы даже
понятнее, но представьте, что в базе данных не шесть записей, а 100 000.
Заменив question на q мы сэкономим 1 400 000 символов (!), а значит
затребуем меньше памяти для обработки базы данных. Тег q содержит вопрос,
атрибут ans – варианты ответов, а атрибут cor – правильный ответ. Обратите
внимание на то, что вариантов ответов не одинаковое количество, и с этой
проблемой надо будет справиться.
Самой сложной будет основная программа, потому что он должна
построить абстрактный интерфейс (такой, которого еще нет), причем с каждым
его элементом надо будет впоследствии работать для проверки результата.
Здесь нам помогут списки (массивы). Мы не знаем, сколько надо создать
переменных для формулировок вопросов и кнопок радио, поэтому не можем
просто заранее создать пустые виджеты, как в предыдущей главе, а потом
просто заполнить их. Виджетов может быть сколь угодно много, поэтому без
списков, которые будут состоять из объектов, решить проблему нельзя.
87
© А.И.Горожанов
В Python можно просто создать список без размера, а потом добавить в
него объекты. Или можно сразу создать пустой список какого-то размера (в том
числе и многомерный), а затем заполнить его уже чем надо. Мы воспользуемся
обеими возможностями. Сначала я приведу код программы, а потом буду его
объяснять (см. Код 6.2):
Код 6.2 Программа shellmain.py
1.
import sys
2.
import random
3.
import os
4.
import xml.dom.minidom
5.
from shell import *
6.
from PyQt5 import QtCore, QtGui, QtWidgets
7.
8.
class MyWin(QtWidgets.QMainWindow):
9.
10.
lbs = []
11.
rbs = [[''] * 10] * 15 # emply list 15x10
12.
bgrs = []
13.
labels = []
14.
variants = []
15.
correct = []
16.
17.
def __init__(self, parent=None):
18.
QtWidgets.QWidget.__init__(self, parent)
19.
self.ui = Ui_MainWindow()
20.
self.ui.setupUi(self)
21.
# xml handling (read & mix)
22.
self.mixXml()
23.
# read to DOM
24.
self.readToDom()
25.
# assigning layout to the scrollarea
26.
self.verticalLayout =
QtWidgets.QVBoxLayout(self.ui.scrollAreaWidgetContents)
88
© А.И.Горожанов
27.
self.verticalLayout.setObjectName("verticalLayout")
28.
# adding widgets to the scrollarea
29.
self.addWidgetsToInterface()
30.
31.
self.ui.pushButton.clicked.connect(self.check)
32.
33.
def mixXml(self):
34.
# read xml and mix the lines
35.
self.linesMixed = []
36.
self.r = open("db.xml", 'r', encoding='utf-8')
37.
self.fileRead = self.r.readlines()
38.
for line in range(2, len(self.fileRead)-1):
39.
self.linesMixed.append(self.fileRead[line])
40.
random.shuffle(self.linesMixed)
41.
self.r.close()
42.
43.
# write temporary xml with new mixed lines
44.
self.w = open("temp.xml", 'w', encoding='utf-8')
45.
self.w.write('''<?xml version="1.0" encoding="utf8"?>\n<content>\n''')
46.
47.
for line in self.linesMixed:
self.w.write('%s' % line)
48.
self.w.write('</content>')
49.
self.w.close()
50.
51.
def readToDom(self):
52.
# read to DOM
53.
self.dom = xml.dom.minidom.parse('temp.xml')
54.
self.collection = self.dom.documentElement
55.
self.linesArr =
self.collection.getElementsByTagName("q")
56.
57.
for line in range(0, len(self.linesArr)):
# label's text
58.
89
© А.И.Горожанов
self.labels.append(self.linesArr[line].childNodes[0].data)
59.
# variants' text
60.
self.variants.append(self.linesArr[line].getAttribute('ans'
).split('**?**'))
61.
# correct answer
62.
self.correct.append(self.linesArr[line].getAttribute('cor')
)
63.
# Mix variants
64.
for variant in self.variants:
65.
random.shuffle(variant)
66.
# Deleting temporary file
67.
os.remove('temp.xml')
68.
69.
def addWidgetsToInterface(self):
70.
# adding widgets to the scrollarea
71.
for line in range (0, len(self.labels)):
72.
self.lbs.append(QtWidgets.QLabel(self.ui.scrollAreaWidgetCo
ntents))
73.
self.lbs[line].setAlignment(QtCore.Qt.AlignLeading|QtCore.Q
t.AlignLeft|QtCore.Qt.AlignTop)
74.
self.lbs[line].setText('<b>%s</b>' %
self.labels[line])
75.
self.verticalLayout.addWidget(self.lbs[line])
76.
self.bgrs.append(QtWidgets.QButtonGroup(self.ui.centralwidg
et))
77.
78.
for v in range(0, len(self.variants[line])):
self.rbs[line][v] =
QtWidgets.QRadioButton(self.ui.scrollAreaWidgetContents)
79.
90
© А.И.Горожанов
self.bgrs[line].addButton(self.rbs[line][v])
80.
self.rbs[line][v].setText(self.variants[line][v])
81.
self.verticalLayout.addWidget(self.rbs[line][v])
82.
83.
def check(self):
84.
counter = 0
85.
for group in range(0, len(self.bgrs)):
86.
for rb in self.bgrs[group].buttons():
87.
if rb.isChecked():
88.
if rb.text() == self.correct[group]:
89.
counter += 1
90.
# And this is the result! Rounded to 2 decimal
points
91.
message = "Your result is " + "%.2f" %
float(counter/len(self.bgrs)*100) + "%"
92.
self.ui.statusbar.setStyleSheet('color: navy; fontweight: bold;')
93.
self.ui.statusbar.showMessage(message)
94.
95.
if __name__ == "__main__":
96.
app = QtWidgets.QApplication(sys.argv)
97.
myapp = MyWin()
98.
myapp.show()
99.
sys.exit(app.exec_())
Для программы такой сложности размер кода в 99 строк не представляется
большим, но некоторые строки стоят десяти благодаря возможностям Python
концентрировать содержание.
Строки 10-15 объявляют списки, которые делятся на две группы: списки
виджетов (lbs, rbs, bgrs) и списки текста виджетов (labels, variants, correct).
Список кнопок радио rbs является двухмерным, и его пришлось заполнить
91
© А.И.Горожанов
пустыми строками, потому что оказалось, что вложить кнопку в кнопку не
получается.
Код функции __init__() разделен между тремя функциями, каждая из
которых выполняет свою задачу: mixXml() перемешивает строки в файле
XML и создает временный файл temp.xml; readToDom() считывает
содержание временного файла в объектную модель документа и заполняет
списки
текста
виджетов;
addWidgetsToInterface()
достраивает
интерфейс, заполняя списки виджетов и выводя их в контейнер с полосой
прокрутки. Функция check() привязана к кнопке Check, в ней производится
проверка правильности выполнения заданий и вывод сообщения в строку
состояния. Также в программе есть небольшие комментарии, которые
помогают ориентироваться в происходящем.
Первой вызывается функция mixXml(). Она открывает файл db.xml,
считывает его целиком построчно в список (строка 37), а затем записывает уже
в другой список только строки с тегом q (строки 38-39). Этот список
перемешивается в строке 40. Далее, согласно комментарию, перемешанная база
данных записывается во временный файл temp.xml.
Далее вызывается функция readToDom(). Она работает уже со
временным файлом. С помощью объектной модели документа (DOM)
заполняются списки labels, variants и correct. Список variants
перемешивается (строки 64-65). Временный файл удаляется (строка 67).
Мы не должны забыть о том, что в контейнере с полосой прокрутки, в
который надо будет помещать все виджеты, пока еще нет никакой компоновки.
Ее нужно создать следующим шагом (строки 26-27). Этим мы гарантируем, что
добавляя виджеты они будут появляться один под другим, поэтому очень важна
последовательность их вывода в интерфейс.
Вызывается функция addWidgetsToInterface(). Она состоит из двух
вложенных циклов for. Внутри заполняются списки виджетов, и эти виджеты
выводятся на экран. Тут важно еще раз напомнить, что вывод должен
92
© А.И.Горожанов
производиться в том порядке, каком пользователь должен видеть объекты.
Строки 72-75 создают виджет QLabel, т.е. формулировку вопроса. Текст
выводится полужирным шрифтом помощью тега b. Дальше нужно вывести
кнопки радио для этого вопроса. И тут возникают две сложности. Во-первых,
количество вариантов ответов, т.е. кнопок радио, для каждого вопроса разное.
Во-вторых, кнопки нельзя просто вывести, их надо организовать в группу
кнопок. В Qt Designer это можно было бы сделать в два клика мыши, но такой
возможности в нашем случае нет. Вот алгоритм решения:
1. Запускается цикл for, который переберет все варианты ответов для
каждого вопроса (строка 77).
2. Каждая кнопка радио записывается в двухмерный список, который мы
объявили выше. Если представить этот массив как матрицу x*y, то количество
x будет равно количеству вопросов тренажера, а y в каждом x будет разный
(равный количеству вариантов ответов для текущего вопроса). Мы заготовили
матрицу с запасом, размером 15 на 10.
3. Далее текущая кнопка радио добавляется в группу кнопок, которая была
создана выше, в строке 76.
4. Кнопка радио снабжается текстом и выводится на экран.
Ключевым
решением
здесь
является
создание
заранее
пустого
двухмерного списка для кнопок радио. Первоначально список создается для
переменных типа String, но впоследствии объекты String заменяются на
объекты QRadioButton (строка 78). Здесь, как и всегда, Python и PyQt
работают в хорошо слаженной команде.
Функция проверки check() анализирует по очереди каждую группу
кнопок с помощью перебора (строка 85). Далее следует цикл for, который
находит нажатую кнопку и сверяет ее текст с текстом правильного ответа из
списка correct. Если значение совпадает (это значит, что студент ответил
правильно), то накапливающая переменная увеличивается на единицу. Далее
производится простой подсчет процента выполнения (строка 91). Значение
93
© А.И.Горожанов
округляется до двух знаков после запятой. Результат выводится в строку
состояния с присвоением стиля (строки 92-93).
В результате тренажер выглядит следующим образом (см. Рис. 6.3):
Рис. 6.3 Тренажер после запуска программы shellmain.py
Все вопросы построились правильно, варианты ответов перемешались,
кнопка проверки работает.
***
Проверьте и расширьте свое понимание (6.1): В программе shellmain.py
результат выводится всегда синим цветом. Для того, чтобы создать
дополнительный показатель прогресса, сделайте так, чтобы цвет строки
состояния зависел от количества набранных баллов. Используйте схему
светофора: 0-50%: красный цвет, 51-75%: желтый цвет, 76-100%: зеленый цвет.
94
© А.И.Горожанов
(6.2): В строке состояния при изменении цвета вся строка закрашивается
одинаково. Как сделать так, чтобы цветом выделялось только число?
(6.3): Для хранения кнопок радио был объявлен двухмерный список 15 на
10. Но если пользователь захочет сделать тренажер с 16-ю и более вопросами,
то программа выдаст ошибку. Как гибко настроить размер списка, чтобы он
всегда соответствовал параметрам базы данных XML?
(6.4): Тренажер может включать любое количество заданий, и это
количество может быть довольно большим. Пользователь может делать задания
не по порядку, пропуская сложные вопросы. В этом случае ему нужно будет
возвращаться к нерешенным заданиям. Если заданий несколько десятков, то
может оказаться так, что при нажатии кнопки проверки некоторые задания
останутся нетронутыми только потому, что пользователь забыл к ним
вернуться. Чтобы облегчить человеку задачу сделайте индикатор, который бы
показывал, что в тренажере еще остались задания, в которых не отмечен еще ни
один вариант ответов. Для этого подойдет виджет QProgressBar, который
можно добавить в интерфейс, а потом работать с ним из основного файла.
Индикатор хода процесса должен показывать процент заданий с отмеченными
вариантами. Заметьте, индикатор показывает не процент правильно решенных
заданий, а процент хоть как-то решенных заданий. Например, если у Вас шесть
вопросов, и пользователь как-то ответил на три из них (не обязательно
правильно, это пока никак не проверяется), а к остальным трем вообще не
приступал, то должно отобразиться следующее (см. Рис. 6.4):
Рис. 6.4 Тренажер с индикатором, показывающим 50% решенных заданий
95
© А.И.Горожанов
На рисунке задания выполнены неверно, но индикатор показывает 50%,
т.к. отмеченные кнопки есть в половине из всех вопросов.
***
Важным показателем при проверке работы студента является время
выполнения
заданий.
Пока рассматриваемый тренажер
никак
его
не
ограничивает, но мы сейчас это исправим.
Данные о времени программа будет получать из базы данных, поэтому
нужно ввести еще один дополнительный тег time, содержание которого будет
означать время на выполнение задания в секундах (см. Код 6.3):
Код 6.3 Файл db1.xml
1.
<?xml version="1.0" encoding="utf-8"?>
2.
<content>
96
© А.И.Горожанов
3.
<time>30</time>
4.
<q ans = "2**?**3**?**4**?**5" cor = "4">Сколько будет
2+2?</q>
5.
<q ans = "5**?**6**?**7" cor = "7">Сколько будет 5+2?</q>
6.
<q ans = "7**?**8**?**9**?**10**?**11" cor = "9">Сколько
будет 2+7?</q>
7.
<q ans = "4**?**5**?**6**?**7" cor = "6">Сколько будет 126?</q>
8.
<q ans = "10**?**20**?**30**?**40**?**50" cor = "20">Сколько
будет 10*2</q>
9.
<q ans = "11**?**22**?**33**?**44**?**55" cor = "33">Сколько
будет 55-22</q>
10.
</content>
В строке 3 содержится новый тег, согласно которому на выполнение
задания отводится 30 секунд.
В интерфейс надо куда-нибудь добавить надпись (QLabel), чтобы в ней
шел обратный отсчет (файл интерфейса сохранен как shell2.py).
Теперь важно понять логику выполнения программы. Получается, что в
ней будут совершенно независимо друг от друга происходить два действия (это
называется потоками). Первый поток отвечает за построение интерфейса и
нажатие кнопок. Это то, что в программе выполнялось до сих пор по
умолчанию, и мы это никак не акцентировали. Второй поток организует
обратный отсчет, причем так, что нажатие на кнопки радио никак не замедляет
таймер, потому что он сам по себе. Но как только время доходит до нуля, поток
№2 пошлет сигнал в первый поток, а сам прекратится.
Python позволяет работать с несколькими потоками, однако я рекомендую
воспользоваться здесь возможностями PyQt5, которая также имеет встроенный
класс QThread(). Возможно, понимание того, как он работает, придет не
сразу, но к этому нужно стремиться. Важность многопоточности ни в коем
случае нельзя недооценивать, потому что такие вещи как, например, таймер,
97
© А.И.Горожанов
очень важны для электронных учебных материалов. В любом случае, можно
использовать приведенный ниже код как шаблон и стараться в нем со временем
разобраться (см. Код 6.4):
Код 6.4 Программа shellmain5.py
1.
import sys
2.
import random
3.
import os
4.
import time
5.
import xml.dom.minidom
6.
from shell2 import *
7.
from PyQt5 import QtCore, QtGui, QtWidgets
8.
9.
class MyWin(QtWidgets.QMainWindow):
10.
11.
lbs = []
12.
rbs = [[''] * 10] * 15 # emply list 15x10
13.
bgrs = []
14.
labels = []
15.
variants = []
16.
correct = []
17.
18.
def __init__(self, parent=None):
19.
QtWidgets.QWidget.__init__(self, parent)
20.
self.ui = Ui_MainWindow()
21.
self.ui.setupUi(self)
22.
23.
self.mythread1 = AThread()
24.
25.
# xml handling (read & mix)
26.
self.mixXml()
27.
# read to DOM
28.
self.readToDom()
29.
# assigning layout to the scrollarea
30.
self.verticalLayout =
98
© А.И.Горожанов
QtWidgets.QVBoxLayout(self.ui.scrollAreaWidgetContents)
31.
self.verticalLayout.setObjectName("verticalLayout")
32.
# adding widgets to the scrollarea
33.
self.addWidgetsToInterface()
34.
35.
self.mythread1.partDone.connect(self.updater)
36.
self.start1()
37.
38.
self.ui.pushButton.clicked.connect(self.check)
39.
40.
def mixXml(self):
41.
# read xml and mix the lines
42.
self.linesMixed = []
43.
self.r = open("db1.xml", 'r', encoding='utf-8')
44.
self.fileRead = self.r.readlines()
45.
for line in range(2, len(self.fileRead)-1):
46.
self.linesMixed.append(self.fileRead[line])
47.
random.shuffle(self.linesMixed)
48.
self.r.close()
49.
50.
# write temporary xml with new mixed lines
51.
self.w = open("temp.xml", 'w', encoding='utf-8')
52.
self.w.write('''<?xml version="1.0" encoding="utf8"?>\n<content>\n''')
53.
54.
for line in self.linesMixed:
self.w.write('%s' % line)
55.
self.w.write('</content>')
56.
self.w.close()
57.
58.
def readToDom(self):
59.
# read to DOM
60.
self.dom = xml.dom.minidom.parse('temp.xml')
61.
self.collection = self.dom.documentElement
62.
self.mythread1.timeSeconds =
99
© А.И.Горожанов
self.collection.getElementsByTagName("time")[0].childNodes[
0].data
63.
self.linesArr =
self.collection.getElementsByTagName("q")
64.
for line in range(0, len(self.linesArr)):
65.
# label's text
66.
self.labels.append(self.linesArr[line].childNodes[0].data)
67.
# variants' text
68.
self.variants.append(self.linesArr[line].getAttribute('ans'
).split('**?**'))
69.
# correct answer
70.
self.correct.append(self.linesArr[line].getAttribute('cor')
)
71.
# Mix variants
72.
for variant in self.variants:
73.
random.shuffle(variant)
74.
# Deleting temporary file
75.
os.remove('temp.xml')
76.
77.
def addWidgetsToInterface(self):
78.
# adding widgets to the scrollarea
79.
for line in range (0, len(self.labels)):
80.
self.lbs.append(QtWidgets.QLabel(self.ui.scrollAreaWidgetCo
ntents))
81.
self.lbs[line].setAlignment(QtCore.Qt.AlignLeading|QtCore.Q
t.AlignLeft|QtCore.Qt.AlignTop)
82.
self.lbs[line].setText('<b>%s</b>' %
self.labels[line])
83.
self.verticalLayout.addWidget(self.lbs[line])
100
© А.И.Горожанов
84.
self.bgrs.append(QtWidgets.QButtonGroup(self.ui.centralwidg
et))
85.
for v in range(0, len(self.variants[line])):
86.
self.rbs[line][v] =
QtWidgets.QRadioButton(self.ui.scrollAreaWidgetContents)
87.
self.bgrs[line].addButton(self.rbs[line][v])
88.
self.rbs[line][v].setText(self.variants[line][v])
89.
self.verticalLayout.addWidget(self.rbs[line][v])
90.
91.
def check(self):
92.
counter = 0
93.
for group in range(0, len(self.bgrs)):
94.
for rb in self.bgrs[group].buttons():
95.
if rb.isChecked():
96.
if rb.text() == self.correct[group]:
97.
counter += 1
98.
# And this is the result! Rounded to 2 decimal
points
99.
message = "Your result is " + "%.2f" %
float(counter/len(self.bgrs)*100) + "%"
100.
self.ui.statusbar.setStyleSheet('color: navy; fontweight: bold;')
101.
self.ui.statusbar.showMessage(message)
102.
103.
104.
def updater(self, val):
if val == 0:
105.
self.check()
106.
self.ui.scrollArea.setEnabled(False)
107.
self.ui.pushButton.setEnabled(False)
108.
self.ui.label.setText(self.intToTime(val))
101
© А.И.Горожанов
109.
110.
def start1(self):
111.
self.mythread1.start()
112.
113.
# Additional function, if you need to terminate the
thread
114.
115.
def stop1(self):
self.mythread1.terminate()
116.
117.
def intToTime(self, num):
118.
h = 0
119.
m = 0
120.
if num >= 3600:
121.
h = num // 3600
122.
num = num % 3600
123.
if num >= 60:
124.
m = num // 60
125.
num = num % 60
126.
s = num
127.
str1 = "%d." % h
128.
if m < 10:
129.
130.
131.
132.
133.
134.
135.
136.
str1 += "0%d:" % m
else:
str1 += "%d:" % m
if s < 10:
str1 += "0%d" % s
else:
str1 += "%d" % s
return str1 # returns time as a string
137.
138. class AThread(QtCore.QThread):
139.
timeSeconds = 0
140.
partDone = QtCore.pyqtSignal(int)
141.
def run(self):
102
© А.И.Горожанов
142.
count = int(self.timeSeconds)
143.
while count > -1:
144.
time.sleep(1)
145.
self.partDone.emit(count)
146.
count -= 1
147.
148. if __name__ == "__main__":
149.
app = QtWidgets.QApplication(sys.argv)
150.
myapp = MyWin()
151.
myapp.show()
152.
sys.exit(app.exec_())
В строке 138 создается новых класс AThread(), который наследует
свойства класса QThread(). Далее создается переменная этого класса
timeSeconds, которая приравнивается к нулю. В переменную partDone
помещается сигнал, связанный с целочисленным значением (это и будут
секунды). В строке 141 объявляется функция run(). Создается переменная
count, которая получает значение переменной timeSeconds. В строке 143
запускается цикл while. Каждое прохождение цикла значение переменной
count будет уменьшаться на единицу, а весь поток будет приостанавливаться
(засыпать) на одну секунду. Здесь уже используется модуль time, который
встроен в Python. Чтобы поток не был «вещью в себе», каждый проход цикла
инициируется сигнал partDone, который получает текущее значение
переменной count.
Этот второй поток из класса AThread() запускается внутри первого
потока в строке 23, когда создается переменная mythread1. Это переменная
класса MyWin(). Теперь оба потока действуют параллельно. Внутри класса
MyWin() создаются четыре новых функции: updater(), start1(),
stop1() и intToTime(). Функция start1() фактически инициализирует
функцию run() класса AThread(). Функция stop1() предназначена для
103
© А.И.Горожанов
немедленной остановки второго потока, в программе она не используется, но
приведена
для
полноты
картины.
Функция
updater()
получает
целочисленный аргумент val, который выводится в интерфейс, в виджет
QLabel. Этот аргумент функция получает от сигнала partDone (см. строку
35). Для того, чтобы пользователь видел не просто уменьшающееся количество
секунд, а часы, минуты и секунды, функция intToTime() производит
соответствующее форматирование.
***
Проверьте и расширьте свое понимание (6.5): В приведенной выше
программе таймер обратного отсчета выводится в виде часов, минут и секунд.
Модифицируйте
программу
позывалось
виде
в
таким
образом,
уменьшающейся
чтобы
полоски.
оставшееся
Используйте
время
виджет
QProgressBar.
(6.6): Виджет QProgressBar по умолчанию имеет зеленый цвет (по
крайней мере в ОС Windows). Измените его цвет на более неприметный,
например серый.
***
Проблему таймера можно решить и по-другому. Вместо создания своего
класса на основе класса QThread() можно воспользоваться преимуществами
класса QTimer(). Он уже предусматривает создание отдельного потока,
который через определенный интервал времени будет вызывать указанную
функцию. Программа даже получится немного короче (см. Код 6.5):
Код 6.5 Программа shellmain8.py
1.
import sys
2.
import random
3.
import os
4.
import xml.dom.minidom
5.
from shell2 import *
6.
from PyQt5 import QtCore, QtGui, QtWidgets
7.
104
© А.И.Горожанов
8.
class MyWin(QtWidgets.QMainWindow):
9.
10.
lbs = []
11.
rbs = [[''] * 10] * 15 # emply list 15x10
12.
bgrs = []
13.
labels = []
14.
variants = []
15.
correct = []
16.
17.
def __init__(self, parent=None):
18.
QtWidgets.QWidget.__init__(self, parent)
19.
self.ui = Ui_MainWindow()
20.
self.ui.setupUi(self)
21.
# creating timer
22.
self.timer = QtCore.QTimer(self)
23.
# xml handling (read & mix)
24.
self.mixXml()
25.
# read to DOM
26.
self.readToDom()
27.
# assigning layout to the scrollarea
28.
self.verticalLayout =
QtWidgets.QVBoxLayout(self.ui.scrollAreaWidgetContents)
29.
self.verticalLayout.setObjectName("verticalLayout")
30.
# adding widgets to the scrollarea
31.
self.addWidgetsToInterface()
32.
self.timer.timeout.connect(lambda:
self.updater(self.timeSeconds))
33.
# starting timer
34.
self.timer.start(1000)
35.
36.
self.ui.pushButton.clicked.connect(self.check)
37.
38.
39.
def mixXml(self):
# read xml and mix the lines
105
© А.И.Горожанов
40.
self.linesMixed = []
41.
self.r = open("db1.xml", 'r', encoding='utf-8')
42.
self.fileRead = self.r.readlines()
43.
for line in range(2, len(self.fileRead)-1):
44.
self.linesMixed.append(self.fileRead[line])
45.
random.shuffle(self.linesMixed)
46.
self.r.close()
47.
48.
# write temporary xml with new mixed lines
49.
self.w = open("temp.xml", 'w', encoding='utf-8')
50.
self.w.write('''<?xml version="1.0" encoding="utf8"?>\n<content>\n''')
51.
for line in self.linesMixed:
52.
self.w.write('%s' % line)
53.
self.w.write('</content>')
54.
self.w.close()
55.
56.
def readToDom(self):
57.
# read to DOM
58.
self.dom = xml.dom.minidom.parse('temp.xml')
59.
self.collection = self.dom.documentElement
60.
# reading timeout to a variable
61.
self.timeSeconds =
int(self.collection.getElementsByTagName("time")[0].childNo
des[0].data)
62.
self.linesArr =
self.collection.getElementsByTagName("q")
63.
64.
for line in range(0, len(self.linesArr)):
# label's text
65.
self.labels.append(self.linesArr[line].childNodes[0].data)
66.
# variants' text
67.
self.variants.append(self.linesArr[line].getAttribute('ans'
106
© А.И.Горожанов
).split('**?**'))
68.
# correct answer
69.
self.correct.append(self.linesArr[line].getAttribute('cor')
)
70.
# Mix variants
71.
for variant in self.variants:
72.
random.shuffle(variant)
73.
# Deleting temporary file
74.
os.remove('temp.xml')
75.
76.
def addWidgetsToInterface(self):
77.
# adding widgets to the scrollarea
78.
for line in range (0, len(self.labels)):
79.
self.lbs.append(QtWidgets.QLabel(self.ui.scrollAreaWidgetCo
ntents))
80.
self.lbs[line].setAlignment(QtCore.Qt.AlignLeading|QtCore.Q
t.AlignLeft|QtCore.Qt.AlignTop)
81.
self.lbs[line].setText('<b>%s</b>' %
self.labels[line])
82.
self.verticalLayout.addWidget(self.lbs[line])
83.
self.bgrs.append(QtWidgets.QButtonGroup(self.ui.centralwidg
et))
84.
85.
for v in range(0, len(self.variants[line])):
self.rbs[line][v] =
QtWidgets.QRadioButton(self.ui.scrollAreaWidgetContents)
86.
self.bgrs[line].addButton(self.rbs[line][v])
87.
self.rbs[line][v].setText(self.variants[line][v])
88.
107
© А.И.Горожанов
self.verticalLayout.addWidget(self.rbs[line][v])
89.
90.
def check(self):
91.
counter = 0
92.
for group in range(0, len(self.bgrs)):
93.
for rb in self.bgrs[group].buttons():
94.
if rb.isChecked():
95.
if rb.text() == self.correct[group]:
96.
counter += 1
97.
# And this is the result! Rounded to 2 decimal
points
98.
message = "Your result is " + "%.2f" %
float(counter/len(self.bgrs)*100) + "%"
99.
self.ui.statusbar.setStyleSheet('color: navy; fontweight: bold;')
100.
self.ui.statusbar.showMessage(message)
101.
102.
def updater(self, val):
103.
val = self.timeSeconds
104.
if val == 0:
105.
self.timer.stop()
106.
self.check()
107.
self.ui.scrollArea.setEnabled(False)
108.
self.ui.pushButton.setEnabled(False)
109.
self.ui.label.setText(self.intToTime(val))
110.
self.timeSeconds -= 1
111.
112.
def intToTime(self, num):
113.
h = 0
114.
m = 0
115.
if num >= 3600:
116.
h = num // 3600
117.
num = num % 3600
118.
if num >= 60:
108
© А.И.Горожанов
119.
m = num // 60
120.
num = num % 60
121.
s = num
122.
str1 = "%d." % h
123.
if m < 10:
124.
125.
126.
127.
128.
129.
130.
131.
str1 += "0%d:" % m
else:
str1 += "%d:" % m
if s < 10:
str1 += "0%d" % s
else:
str1 += "%d" % s
return str1 # returns time as a string
132.
133.
if __name__ == "__main__":
134.
app = QtWidgets.QApplication(sys.argv)
135.
myapp = MyWin()
136.
myapp.show()
137.
sys.exit(app.exec_())
Из программы исчезает класс потока. Вместо этого в строке 22 создается
переменная класса QTimer(). В строке 32 с помощью метода timeout()
поток таймера связывается с функцией updater(). В строке 34 метод
start() задает интервал в миллисекундах, через который начиная с этого
момента будет вызываться функция updater(). Далее в строке 61 при чтении
файла XML время таймера считывается в переменную timeSeconds.
Некоторым изменениям подверглась функция updater(). При достижении
нулевой отметки таймер останавливается (строка 105), а в конце функции
значение переменной timeSeconds уменьшается на единицу.
Итак, у Вас есть два варианта того, как сделать таймер обратного отсчета.
Вы можете выбрать любой из них, когда будете писать свои программы.
***
109
© А.И.Горожанов
В этой главе Вы сделали по-настоящему универсальный тренажер,
который является только графической оболочкой и получает всю информацию
из файла XML. Ваш тренажер имеет таймер обратного отсчета, перемешивает
вопросы и задания в каждом вопросе.
Теперь можно переходить к проблеме протоколирования.
110
© А.И.Горожанов
Глава 7. Программные тренажеры с протоколированием
Протоколирование – важный элемент программ для ЭВМ учебного
назначения. Под этим термином понимается подробная запись процесса работы
над учебными материалами с целью установления прогрессии и дальнейшего
анализа для улучшения качества программ. Это не инструмент выставления и
хранения оценки (хотя и это тоже нужно), а возможность заглянуть внутрь
процесса самостоятельной работы студента, понять, что вызывает особые
трудности.
Протоколы многих студентов могут сводиться вместе для анализа работы
группы или курса, причем большое количество обрабатываемых протоколов не
представляет проблемы, т.к. процесс происходит автоматически, и неважно два
протокола должны быть обработаны или десять тысяч.
При хорошо построенном алгоритме создания и анализа протоколов
преподаватель полностью освобождается от рутинной механической работы,
получая больше времени для того, чтобы решать сложные методические
задачи, которые на сегодняшнее время пока не могут быть решены
компьютером (дидактизация материала, написание учебных программ (не
программ для ЭВМ), модерирование общения студентов в виртуальной
образовательной среде и мн. др.).
Первые протоколы будут представлять собой текстовые файлы, что
обусловлено простотой их создания и чтения. Самый простой протокол должен
фиксировать хотя бы время выполнения задания и полученный балл. За основу
мы возьмем тренажер из предыдущей главы. В код добавится функция log():
def log(self):
file = open("log.txt", 'w', encoding='utf-8')
file.write("Дата и время записи: ")
file.write(time.strftime("%Y-%m-%d %H:%M:%S"))
file.write('\n')
file.write("Результат: %.2f" % self.result + " %\n")
111
© А.И.Горожанов
file.write('Выполнено за %d секунд' %
(self.timeConstant - self.timeSeconds))
file.close()
Также в программу нужно импортировать модули time и io. Функция
будет вызываться в конце функции check(). Для удобства работы в
программе появились еще две переменные: result и timeConstant. В
результате программа создает файл протокола, который имеет следующий вид
(см. Рис. 7.1):
Рис. 7.1 Один из вариантов содержания файла протокола
Протокол фиксирует дату и время записи информации в файл (фактически
время нажатия кнопки Check), результат и время выполнения задания. При
каждом нажатии кнопки Check файл перезаписывается. Полностью файл
программы сохранен под именем logmain1.py.
Проверьте и расширьте свое понимание (7.1): Модифицируйте
программу так, чтобы протокол добавлялся в файл, а не перезаписывал его.
(7.2): Поработайте дальше с файлом протокола. Сделайте так, чтобы
программа не только добавляла новый протокол в файл, но и делала файл
защищенным от записи, т.е. в него нельзя было бы случайно что-то дописать
вручную.
Наш первый протокол хороший, но в принципе малополезный, т.к. из него
непонятно, что же делал пользователь. Главное преимущество электронных
протоколов именно в том, чтобы углубляться в детали. Надо добавить в
протокол информацию о заданиях. Изучите файл logmain4.py (см. Код 7.1):
Код 7.1 Программа logmain4.py
1.
import sys
112
© А.И.Горожанов
2.
import random
3.
import os
4.
import io
5.
import time
6.
import xml.dom.minidom
7.
from shell2 import *
8.
from PyQt5 import QtCore, QtGui, QtWidgets
9.
10.
class MyWin(QtWidgets.QMainWindow):
11.
12.
lbs = []
13.
rbs = [[''] * 10] * 15 # emply list 15x10
14.
bgrs = []
15.
labels = []
16.
variants = []
17.
correct = []
18.
logStr = ''
19.
20.
def __init__(self, parent=None):
21.
QtWidgets.QWidget.__init__(self, parent)
22.
self.ui = Ui_MainWindow()
23.
self.ui.setupUi(self)
24.
# creating timer
25.
self.timer = QtCore.QTimer(self)
26.
# xml handling (read & mix)
27.
self.mixXml()
28.
# read to DOM
29.
self.readToDom()
30.
# assigning layout to the scrollarea
31.
self.verticalLayout =
QtWidgets.QVBoxLayout(self.ui.scrollAreaWidgetContents)
32.
self.verticalLayout.setObjectName("verticalLayout")
33.
# adding widgets to the scrollarea
34.
self.addWidgetsToInterface()
113
© А.И.Горожанов
35.
self.timer.timeout.connect(lambda:
self.updater(self.timeSeconds))
36.
# starting timer
37.
self.timer.start(1000)
38.
39.
self.ui.pushButton.clicked.connect(self.check)
40.
41.
def mixXml(self):
42.
# read xml and mix the lines
43.
self.linesMixed = []
44.
self.r = open("db1.xml", 'r', encoding='utf-8')
45.
self.fileRead = self.r.readlines()
46.
for line in range(2, len(self.fileRead)-1):
47.
self.linesMixed.append(self.fileRead[line])
48.
random.shuffle(self.linesMixed)
49.
self.r.close()
50.
51.
# write temporary xml with new mixed lines
52.
self.w = open("temp.xml", 'w', encoding='utf-8')
53.
self.w.write('''<?xml version="1.0" encoding="utf8"?>\n<content>\n''')
54.
for line in self.linesMixed:
55.
self.w.write('%s' % line)
56.
self.w.write('</content>')
57.
self.w.close()
58.
59.
def readToDom(self):
60.
# read to DOM
61.
self.dom = xml.dom.minidom.parse('temp.xml')
62.
self.collection = self.dom.documentElement
63.
# reading timeout to a variable
64.
self.timeSeconds =
int(self.collection.getElementsByTagName("time")[0].childNod
es[0].data)
114
© А.И.Горожанов
65.
self.timeConstant = self.timeSeconds
66.
self.linesArr =
self.collection.getElementsByTagName("q")
67.
for line in range(0, len(self.linesArr)):
68.
# label's text
69.
self.labels.append(self.linesArr[line].childNodes[0].data)
70.
# variants' text
71.
self.variants.append(self.linesArr[line].getAttribute('ans')
.split('**?**'))
72.
# correct answer
73.
self.correct.append(self.linesArr[line].getAttribute('cor'))
74.
# Mix variants
75.
for variant in self.variants:
76.
random.shuffle(variant)
77.
# Deleting temporary file
78.
os.remove('temp.xml')
79.
80.
def addWidgetsToInterface(self):
81.
# adding widgets to the scrollarea
82.
for line in range (0, len(self.labels)):
83.
self.lbs.append(QtWidgets.QLabel(self.ui.scrollAreaWidgetCon
tents))
84.
self.lbs[line].setAlignment(QtCore.Qt.AlignLeading|QtCore.Qt
.AlignLeft|QtCore.Qt.AlignTop)
85.
self.lbs[line].setText('<b>%s</b>' %
self.labels[line])
86.
self.verticalLayout.addWidget(self.lbs[line])
87.
self.bgrs.append(QtWidgets.QButtonGroup(self.ui.centralwidge
115
© А.И.Горожанов
t))
88.
for v in range(0, len(self.variants[line])):
89.
self.rbs[line][v] =
QtWidgets.QRadioButton(self.ui.scrollAreaWidgetContents)
90.
self.bgrs[line].addButton(self.rbs[line][v])
91.
self.rbs[line][v].setText(self.variants[line][v])
92.
self.verticalLayout.addWidget(self.rbs[line][v])
93.
94.
def check(self):
95.
counter = 0
96.
for group in range(0, len(self.bgrs)):
97.
correctThis = '--'
98.
# adding questions
99.
self.logStr += self.labels[group] + '\n'
100.
for rb in self.bgrs[group].buttons():
101.
# adding variants
102.
self.logStr += '\t' + rb.text() + '\n'
103.
if rb.isChecked():
104.
if rb.text() == self.correct[group]:
105.
correctThis = rb.text()
106.
counter += 1
107.
self.logStr += 'Правильный ответ: %s' %
self.correct[group] + '\n'
108.
self.logStr += 'Ответ пользователя: %s' %
correctThis + '\n'
109.
self.logStr += '\n'
110.
# And this is the result! Rounded to 2 decimal
points
111.
self.result = float(counter/len(self.bgrs)*100)
112.
message = "Your result is " + "%.2f" % self.result +
"%"
113.
self.ui.statusbar.setStyleSheet('color: navy; font-
116
© А.И.Горожанов
weight: bold;')
114.
self.ui.statusbar.showMessage(message)
115.
# creating log file
116.
self.log()
117.
118.
def updater(self, val):
119.
val = self.timeSeconds
120.
if val == 0:
121.
self.timer.stop()
122.
self.check()
123.
self.ui.scrollArea.setEnabled(False)
124.
self.ui.pushButton.setEnabled(False)
125.
self.ui.label.setText(self.intToTime(val))
126.
self.timeSeconds -= 1
127.
128.
def intToTime(self, num):
129.
h = 0
130.
m = 0
131.
if num >= 3600:
132.
h = num // 3600
133.
num = num % 3600
134.
if num >= 60:
135.
m = num // 60
136.
num = num % 60
137.
s = num
138.
str1 = "%d." % h
139.
if m < 10:
140.
141.
142.
143.
144.
145.
146.
str1 += "0%d:" % m
else:
str1 += "%d:" % m
if s < 10:
str1 += "0%d" % s
else:
str1 += "%d" % s
117
© А.И.Горожанов
147.
return str1 # returns time as a string
148.
149.
def log(self):
150.
file = open("log.txt", 'w', encoding='utf-8')
151.
file.write("Дата и время записи: ")
152.
file.write(time.strftime("%Y-%m-%d %H:%M:%S"))
153.
file.write('\n\n')
154.
file.write(self.logStr)
155.
file.write('\n')
156.
file.write("Результат: %.2f" % self.result + " %\n")
157.
file.write('Выполнено за %d секунд' %
(self.timeConstant - self.timeSeconds))
158.
file.close()
159.
self.logStr = ''
160.
161. if __name__ == "__main__":
162.
app = QtWidgets.QApplication(sys.argv)
163.
myapp = MyWin()
164.
myapp.show()
165.
sys.exit(app.exec_())
В строке 18 объявлена пустая накапливающая переменная типа String.
Главные изменения происходят в функции check(). В строке 97 создается
накапливающая переменная correctThis, которой присваивается значение
«--». В строке 99 при каждом прохождении цикла, объявленного в строке 96, к
переменной прибавляется формулировка очередного вопроса. В строке 102, уже
в цикле, который перебирает варианты ответов внутри каждой группы кнопок,
в переменную записываются по очереди все варианты ответов – в том виде, как
их видит пользователь. Обратите внимание на знак табулятора \t. Если есть
отмеченный правильный ответ, то его формулировка помещается в переменную
correctThis. После выхода из цикла строки 100 в накапливающую
переменную записываются по очереди правильный ответ на вопрос (строка
118
© А.И.Горожанов
107) и тот ответ, который дал пользователь (строка 108). При следующем
проходе цикла строки 96 переменная correctThis обнулится, чтобы
получить очередное значение. Если пользователь не приступал к выполнению
задания, т.е. ни одна из кнопок радио в текущей группе не отмечена, то
correctThis запишет в накапливающую переменную logStr значение по
умолчанию (два дефиса). В строке 159 переменная logStr обнуляется.
Теперь протокол получил развернутый вид (см. Рис. 7.2):
Рис. 7.2 Фрагмент файла протокола программы logmain4.py
Теперь виден не только общий балл, но и то, какие ответы были даны на
вопросы. Причем сразу показан правильный ответ, чтобы было видно,
правильный ли дан ответ. Конечно, можно было бы просто писать после
каждого вопроса «правильно» или «неправильно», но когда мы видим, как
119
© А.И.Горожанов
отвечал пользователь, мы можем лучше понять, как он думал, в чем состоят
ошибки. А отсюда уже будет легче помочь студенту их исправить.
Представьте, что два студента выполняют одно и то же задание, но один
работает в нашей программе, а другой с листом бумаги. В чем будут состоять
отличия? Во-первых, работа в программе гарантирует абсолютно точный
контроль времени выполнения. Таймер просто выключит интерфейс после
определенного времени. При работе с бумагой такая точность невозможна. Вовторых, результаты первого студента формируются автоматически. Их можно
распечатать, их легко хранить на электронном носителе. Проверка всегда имеет
стопроцентную точность, что сложно достижимо при ручной проверке, когда
проверяющий устает, отвлекается и т.д. Кроме того, компьютер абсолютно
объективен и никогда не увеличит и не уменьшит балл по своему усмотрению.
А теперь представьте, что работают две группы по 20 человек. Конечно,
тут уже необходим целый компьютерный класс, но если он есть, то мы точно
уверены, что всем студентам будет дано абсолютно одинаковое время
выполнения и у каждого вопросы и варианты ответов будут перемешены в
своем порядке. Заготовить вручную перемешенные варианты для каждого
очень затратно по времени. Не стоит и говорить о количестве потраченной
бумаги, которая тут же превратится в макулатуру.
Тем не менее, будем честны. При текущем виде протокола мы однозначно
выигрываем во времени (и существенно в качестве), но все-таки не делаем
ничего такого, чего можно добиться традиционной работой с бумагой. Мы
внесем в протокол еще один параметр, учесть который никак невозможно при
работе с бумажными заданиями.
Этим
параметром
будет
количество
предпринятых
действий
или
количество нажатий на кнопки радио, и именно он не может быть вычислен при
выполнении работы на бумаге. Представьте разные ситуации. Два студента
получили равный балл за одну и ту же работу. С точки зрения традиционных
измерений они владеют материалом в равной степени. Однако первый решил
120
© А.И.Горожанов
все задания с первого раза (говоря техническим языком, выполнил столько же
включений кнопок, сколько в тренажере имеется вопросов), а другой менял
много раз свои ответы (произвел над интерфейсом больше действий, чем
имеется вопросов в тренажере). Мне видятся несколько оснований того, почему
действий было ровно столько, сколько и вопросов, или больше. Первый студент
твердо знал ответы и не сомневался в своем выборе. Если он к тому же
выполнил задания за очень краткий срок, то или он очень хорошо владеет
материалом, или знал ответы на вопросы заранее. Будем оптимистами и
примем первый вариант. Второй студент сомневался и переменял свои
решения; возможно, он где-то даже угадал. Но также нельзя исключать того,
что сыграла роль неуверенность в себе, что является чертой характера, а не
показателем знаний.
Как бы то ни было, можно с уверенностью утверждать, что параметр
количества изменений дает богатый материал для анализа и что получить этот
материал можно только используя электронные учебные материалы. Подобный
тренажер сохранен под именем logmain5.py. Для подсчета действий создается
накапливающая
переменная
triggeredNum
(строка
19).
В
функцию
addWidgetsToInterface() добавляется строка (номер 89):
self.bgrs[line].buttonClicked.connect(self.increaseNum)
В классе MyWin() создается функция increaseNum():
def increaseNum(self):
self.triggeredNum += 1
Накапливающая переменная обнуляется в конце функции log() (строка
165). Также в этой функции производится соответствующий вывод в протокол
(строка 163). В итоге путем небольшой модификации мы получили
дополнительный инструмент оценки.
Продолжим следовать по пути усовершенствования тренажера. Задания, к
какой бы дисциплине они не относились, не могут быть все одинаковой
сложности. Какие-то всегда сложнее, а какие-то легче. Поэтому мы внесем в
121
© А.И.Горожанов
задания коэффициент сложности. В файл базы данных добавится атрибут pnt
тега q (см. Код 7.2):
Код 7.2 Файл db2.xml
<?xml version="1.0" encoding="utf-8"?>
<content>
<time>30</time>
<q ans = "2**?**3**?**4**?**5" cor = "4" pnt = 1>Сколько будет
2+2?</q>
<q ans = "5**?**6**?**7" cor = "7" pnt = 1>Сколько будет 5+2?</q>
<q ans = "7**?**8**?**9**?**10**?**11" cor = "9" pnt = 1>Сколько
будет 2+7?</q>
<q ans = "4**?**5**?**6**?**7" cor = "6" pnt = 1>Сколько будет 126?</q>
<q ans = "10**?**20**?**30**?**40**?**50" cor = "20" pnt =
2>Сколько будет 10*2?</q>
<q ans = "11**?**22**?**33**?**44**?**55" cor = "33" pnt =
2>Сколько будет 55-22?</q>
</content>
Некоторые вопросы получили коэффициент сложности 1, а некоторые – 2.
Это нужно учесть при подсчете результата. Решение находится в файле
logmain6.py. Изменений всего несколько. В строке 18 объявлен пустой список
value. В строке 77 этот список заполняется. В функции check() при
правильном ответе накапливается не единица, а соответствующий коэффициент
(строка 144). Формула подсчета результата также изменилась (строка 199):
self.result = float(counter/sum(self.value)*100)
Теперь преподаватель может помещать в тренажер задания различного
уровня сложности.
До сих пор мы работали только с текстовой информацией. Теперь настало
время сделать наши тренажеры по-настоящему мультимедийными, добавив в
них картинку.
Введем в файл XML новый атрибут pic (см. Код 7.3):
Код 7.3 Файл db3.xml
122
© А.И.Горожанов
<?xml version="1.0" encoding="utf-8"?>
<content>
<time>30</time>
<q ans = "2**?**3**?**4**?**5" cor = "4" pnt = '1'>Сколько будет
2+2?</q>
<q ans = "5**?**6**?**7" cor = "7" pnt = '1'>Сколько будет
5+2?</q>
<q ans = "7**?**8**?**9**?**10**?**11" cor = "9" pnt = '1'>Сколько
будет 2+7?</q>
<q ans = "4**?**5**?**6**?**7" cor = "6" pnt = '1'>Сколько будет
12-6?</q>
<q ans = "10**?**20**?**30**?**40**?**50" cor = "20" pnt =
'2'>Сколько будет 10*2?</q>
<q ans = "Нотр-Дам**?**Аббатство Клюни**?**Аббатство Сен-Дени" cor
= "Нотр-Дам" pnt = '3' pic='pic.jpg'>Что изображено на
картинке?</q>
</content>
В последнем теге q появился атрибут pic, который содержит имя
графического файла. Поскольку не каждый вопрос может сопровождаться
картинкой, основная программа должна сначала проверять каждый тег q на
наличие атрибута pic, и соответственно этому строить интерфейс.
Файл logmain7.py реализует поставленную задачу. В строках 19 и 20
объявляются два пустых списка picName и lblPic, в которых будут
храниться название графического файла для вопроса и виджет QLabel, в
который будет помещен этот файл. В функции readToDom() в строках 81-84
производится проверка наличия в строке вопроса атрибута pic. В зависимости
от результата элемент списка picName принимает значение имени файла или
значение 'empty'. В функции addWidgetsToInterface() при выводе
вопросов в интерфейс (строки 98-104) перебираются соответствующие
элементы списка picName и если значение элемента не равно 'empty',
создается виджет QLabel, в который помещается картинка под именем
123
© А.И.Горожанов
picName (строка 101). Если значение равно 'empty', то создается просто
пустой виджет QLabel, чтобы не нарушать целостность списка lblPic.
Картинка выводится между формулировкой вопроса и вариантами ответа
(кнопками радио), в масштабе 100%, поэтому перед размещением картинки
придавайте ей подходящий размер. Для масштабирования графических файлов
можно воспользоваться редактором Paint, который входит в стандартный набор
программ ОС Windows (см. Рис. 7.3):
Рис. 7.3 Масштабирование картинки в редакторе Paint
Проверьте и расширьте свое понимание (7.3): Придайте тренажеру
больше персональности. Пусть во время запуска программы, до начала отсчета
таймера, на экран будет выведено диалоговое меню, которое запросит у
пользователя имя и номер учебной группы, например (см. Рис. 7.4):
Рис. 7.4 Диалоговое окно с двумя строками ввода
124
© А.И.Горожанов
Если пользователь не введет ничего хотя бы в одну строку и при этом
попытается закрыть окно кнопками или крестиками, то программа не должна
пускать его к тренажеру, а выводить диалоговое окно снова и снова. Введенные
имя и группа должны отразиться в протоколе.
(7.4) Наш тренажер пока разрешает многочисленное нажатие кнопки
проверки в течении одного сеанса работы. Сделайте так, чтобы при нажатии
кнопки Check производить действия с интерфейсом уже было бы нельзя.
Модифицируйте версию logmain7.py.
Теперь мы сделаем следующий шаг и добавим в тренажер еще один очень
важный виджет: флажок (QCheckBox).
Кнопки радио позволяли делать задания на множественный выбор, где был
возможен только один вариант ответа. Такие задания имеют невысокую
степень сложности, т.к. всегда существует высокий шанс угадать ответ.
Намного сложнее справиться с заданиями, в которых возможны различные
комбинации ответов. Рассмотрим два варианта. В первом из трех вариантов
нужно выбрать только один правильный ответ (и пользователь это знает). Шанс
угадать при этом 1 к 3. Теперь заменим кнопки радио флажками (см. Рис. 7.5):
Рис. 7.5 Вопрос с тремя флажками
125
© А.И.Горожанов
При таком задании известно заранее, что правильная комбинация может
быть какой угодно. Например, как на Рис. 7.5, когда ни один ответ не отмечен,
но это является правильным вариантом. Также все три варианта могут быть
правильными (см. Рис. 7.6):
Рис. 7.6 Вопрос с тремя отмеченными флажками
А возможны и другие комбинации, общее количество которых равняется
уже восьми, а не трем. Если увеличить количество флажков, то возрастет и
количество комбинаций. В этом случае угадать правильный вариант
практически невозможно, а значит задание станет интереснее и ответственнее.
Этим и замечательный флажки. Конечно и преподаватель должен будет
проявить больше фантазии при составлении таких заданий.
Теперь создадим тренажер, который будет сочетать задания с кнопками
радио и флажками, производить перемешивание, позволять присваивать
заданиям коэффициент сложности и вести подробный протокол.
В файл базы данных нужно ввести дополнительный атрибут тега q,
который будет говорить программе, какой тип виджета нужно использовать для
данного вопроса. Изменится и атрибут правильного ответа. Вообще,
существуют различные точки зрения на то, что считать правильным ответом на
задание с флажками. Допустим, что из трех вариантов правильными являются
два первых. Пользователь отметил первый вариант, а второй и третий не
отметил. Получается, что вроде бы половина ответа сделана правильно, т.е.
можно поставить полбалла. Но лучше я приведу пример (см. 7.7):
Рис. 7.7 Вопрос с одним отмеченным флажком
126
© А.И.Горожанов
Можно ли вообще поставить что-нибудь за такой ответ? Выполняя таким
образом тренажер, студент получит 66%, что говорит о том, что он усвоил
больше половины материала. Но это чистой воды обман, потому что нельзя
уметь вычитать 2 из 92, но не уметь вычитать 1 из 92. Скорее всего галочка
поставлена наугад, а студент даже не читал задание. Но при этом есть результат
– целых 66%!
Вы справедливо заметите, что пример слишком примитивен. Но при
усложнении мало что изменится. Если заменить арифметику на что-то из нашей
области, то получится то же самое (см. Рис. 7.8):
Рис. 7.8 Задание на определение правильности форм
Предположим, что студент вообще не делал задание, но пустой третий
флажок – это правильный флажок. Поэтому программа выдаст результат 33%.
И это просто за нажатие кнопки Check. Если поставить галочку в первом
флажке, то результат увеличивается уже до 66%. Но ни о каких знаниях речь не
может идти, потому что понимать I am a student, но не понимать I am a doctor
невозможно. Скорее всего галочка поставлена наугад.
Проблема решается просто: принять, что такой тип заданий может быть
решен или на 100%, или на 0%, без градаций. Решение на 100% с высокой
степенью достоверности показывает наличие знаний. Выполняя такой тренажер
дома, студент не будет обманываться мнимыми процентами, но получив 0% за
127
© А.И.Горожанов
задание будет стараться понять ошибку и пытаться ее исправить. А в этом и
есть предназначение тренажеров.
Этот подход к оценке обязательно должен быть объяснен студенту в
инструкции к тренажеру, чтобы он понимал, как нужно работать (чтобы не
было раздражения от получения нуля баллов при частично правильно
решенном задании). Зато последующее превращение 0% в 100% невероятно
мотивирует и приносит удовлетворение.
Итак, атрибут cor примет вид последовательности правильных ответов.
Если ответ только один, то это будет один ответ. Если никакой ответ не
правильный, то атрибут примет значение 'none'(см. Код 7.4):
Код 7.4 Файл db4.xml
1.
<?xml version="1.0" encoding="utf-8"?>
2.
<content>
3.
<time>30</time>
4.
<q type='rb' ans = "2**?**3**?**4**?**5" cor = "4" pnt =
'1'>Сколько будет 2+2?</q>
5.
<q type='rb' ans = "5**?**6**?**7" cor = "7" pnt =
'1'>Сколько будет 5+2?</q>
6.
<q type='rb' ans = "7**?**8**?**9**?**10**?**11" cor = "9"
pnt = '1'>Сколько будет 2+7?</q>
7.
<q type='rb' ans = "4**?**5**?**6**?**7" cor = "6" pnt =
'1'>Сколько будет 12-6?</q>
8.
<q type='rb' ans = "10**?**20**?**30**?**40**?**50" cor =
"20" pnt = '2'>Сколько будет 10*2?</q>
9.
<q type='chb' ans = "20+3**?**24-2**?**10+15**?**50-26" cor
= "20+3" pnt = '2'>Какое выражение в итоге равно 23?</q>
10.
<q type='chb' ans = "40+2**?**44-2**?**30+15**?**50-6" cor =
"40+2**?**44-2" pnt = '2'>Какое выражение в итоге равно
42?</q>
11.
<q type='chb' ans = "55+1**?**20+30**?**31+15**?**606**?**22,5*3" cor = "none" pnt = '3'>Какое выражение в итоге
равно 55?</q>
128
© А.И.Горожанов
12.
</content>
В этой базе данных есть и кнопки радио, и флажки (строки 9-11). В строке
9 один правильный ответ, в строке 10 два правильных ответа, а в строке 11 нет
правильных ответов. Все это должно восприниматься основной программой
корректно. Разберем ее ниже (см. Код 7.5):
Код 7.5 Программа logmain10.py
1.
import sys
2.
import random
3.
import os
4.
import io
5.
import time
6.
import xml.dom.minidom
7.
from shell2 import *
8.
from PyQt5 import QtCore, QtGui, QtWidgets
9.
10.
class MyWin(QtWidgets.QMainWindow):
11.
12.
lbs = []
13.
rbs = [[''] * 10] * 15 # emply list 15x10
14.
bgrs = []
15.
labels = []
16.
variants = []
17.
correct = []
18.
value = []
19.
tp = []
20.
picName = []
21.
lblPic = []
22.
logStr = ''
23.
triggeredNum = 0
24.
25.
26.
def __init__(self, parent=None):
QtWidgets.QWidget.__init__(self, parent)
129
© А.И.Горожанов
27.
self.ui = Ui_MainWindow()
28.
self.ui.setupUi(self)
29.
# creating timer
30.
self.timer = QtCore.QTimer(self)
31.
# xml handling (read & mix)
32.
self.mixXml()
33.
# read to DOM
34.
self.readToDom()
35.
# assigning layout to the scrollarea
36.
self.verticalLayout =
QtWidgets.QVBoxLayout(self.ui.scrollAreaWidgetContents)
37.
self.verticalLayout.setObjectName("verticalLayout")
38.
# adding widgets to the scrollarea
39.
self.addWidgetsToInterface()
40.
self.timer.timeout.connect(lambda:
self.updater(self.timeSeconds))
41.
# starting timer
42.
self.timer.start(1000)
43.
44.
self.ui.pushButton.clicked.connect(self.finish)
45.
46.
def finish(self):
47.
self.timeRes = self.timeSeconds
48.
self.timeSeconds = 0
49.
50.
def mixXml(self):
51.
# read xml and mix the lines
52.
self.linesMixed = []
53.
self.r = open("db4.xml", 'r', encoding='utf-8')
54.
self.fileRead = self.r.readlines()
55.
for line in range(2, len(self.fileRead)-1):
56.
self.linesMixed.append(self.fileRead[line])
57.
random.shuffle(self.linesMixed)
58.
self.r.close()
130
© А.И.Горожанов
59.
60.
# write temporary xml with new mixed lines
61.
self.w = open("temp.xml", 'w', encoding='utf-8')
62.
self.w.write('''<?xml version="1.0" encoding="utf8"?>\n<content>\n''')
63.
for line in self.linesMixed:
64.
self.w.write('%s' % line)
65.
self.w.write('</content>')
66.
self.w.close()
67.
68.
def readToDom(self):
69.
# read to DOM
70.
self.dom = xml.dom.minidom.parse('temp.xml')
71.
self.collection = self.dom.documentElement
72.
# reading timeout to a variable
73.
self.timeSeconds =
int(self.collection.getElementsByTagName("time")[0].childNod
es[0].data)
74.
self.timeConstant = self.timeSeconds
75.
self.linesArr =
self.collection.getElementsByTagName("q")
76.
77.
for line in range(0, len(self.linesArr)):
# label's text
78.
self.labels.append(self.linesArr[line].childNodes[0].data)
79.
# variants' text
80.
self.variants.append(self.linesArr[line].getAttribute('ans')
.split('**?**'))
81.
# correct answer
82.
self.correct.append(self.linesArr[line].getAttribute('cor'))
83.
# value
84.
131
© А.И.Горожанов
self.value.append(int(self.linesArr[line].getAttribute('pnt'
)))
85.
# reading type
86.
self.tp.append(self.linesArr[line].getAttribute('type'))
87.
# adding picture name if any
88.
if self.linesArr[line].hasAttribute('pic'):
89.
self.picName.append(self.linesArr[line].getAttribute('pic'))
90.
else:
91.
self.picName.append('empty')
92.
# Mix variants
93.
for variant in self.variants:
94.
random.shuffle(variant)
95.
# Deleting temporary file
96.
os.remove('temp.xml')
97.
98.
def addWidgetsToInterface(self):
99.
# adding widgets to the scrollarea
100.
for line in range (0, len(self.labels)):
101.
self.lbs.append(QtWidgets.QLabel(self.ui.scrollAreaWidgetCon
tents))
102.
self.lbs[line].setAlignment(QtCore.Qt.AlignLeading|QtCore.Qt
.AlignLeft|QtCore.Qt.AlignTop)
103.
self.lbs[line].setText('<b>%s</b>' %
self.labels[line])
104.
self.verticalLayout.addWidget(self.lbs[line])
105.
# adding picture
106.
if self.picName[line] != 'empty':
107.
self.lblPic.append(QtWidgets.QLabel(self.ui.scrollAreaWidget
Contents))
132
© А.И.Горожанов
108.
self.lblPic[line].setAlignment(QtCore.Qt.AlignLeading|QtCore
.Qt.AlignLeft|QtCore.Qt.AlignTop)
109.
self.lblPic[line].setPixmap(QtGui.QPixmap(os.getcwd() + "/"
+ self.picName[line]))
110.
self.verticalLayout.addWidget(self.lblPic[line])
111.
else:
112.
self.lblPic.append(QtWidgets.QLabel(self.ui.scrollAreaWidget
Contents))
113.
# adding button group
114.
self.bgrs.append(QtWidgets.QButtonGroup(self.ui.centralwidge
t))
115.
if self.tp[line] == 'chb':
116.
self.bgrs[line].setExclusive(False)
117.
self.correct[line] =
self.correct[line].split('**?**')
118.
# click counter
119.
self.bgrs[line].buttonClicked.connect(self.increaseNum)
120.
for v in range(0, len(self.variants[line])):
121.
# check br/chb
122.
if self.tp[line] == 'rb':
123.
self.rbs[line][v] =
QtWidgets.QRadioButton(self.ui.scrollAreaWidgetContents)
124.
elif self.tp[line] == 'chb':
125.
self.rbs[line][v] =
QtWidgets.QCheckBox(self.ui.scrollAreaWidgetContents)
126.
self.bgrs[line].addButton(self.rbs[line][v])
127.
self.rbs[line][v].setText(self.variants[line][v])
133
© А.И.Горожанов
128.
self.verticalLayout.addWidget(self.rbs[line][v])
129.
130.
131.
def increaseNum(self):
self.triggeredNum += 1
132.
133.
def check(self):
134.
counter = 0
135.
for group in range(0, len(self.bgrs)):
136.
# adding questions
137.
self.logStr += self.labels[group] + '\n'
138.
# check rb/chb
139.
if self.tp[group] == 'rb':
140.
correctThis = '--'
141.
for rb in self.bgrs[group].buttons():
142.
# adding variants
143.
self.logStr += '\t' + rb.text() + '\n'
144.
if rb.isChecked():
145.
if rb.text() == self.correct[group]:
146.
correctThis = rb.text()
147.
counter += self.value[group]
148.
elif self.tp[group] == 'chb':
149.
correctThis = []
150.
chbCor = []
151.
for rb in self.bgrs[group].buttons():
152.
# adding variants
153.
self.logStr += '\t' + rb.text() + '\n'
154.
if rb.isChecked():
155.
# marked checkboxes
156.
chbCor.append(rb.text())
157.
158.
if len(chbCor) == 0:
chbCor.append('none')
159.
chbCor.sort()
160.
self.correct[group].sort()
134
© А.И.Горожанов
161.
if self.correct[group] == chbCor:
162.
counter += self.value[group]
163.
correctThis = chbCor
164.
165.
self.logStr += 'Правильный ответ: %s' %
self.correct[group] + '\n'
166.
self.logStr += 'Ответ пользователя: %s' %
correctThis + '\n'
167.
self.logStr += '\n'
168.
# And this is the result! Rounded to 2 decimal
points
169.
self.result = float(counter/sum(self.value)*100)
170.
message = "Your result is " + "%.2f" % self.result +
"%"
171.
self.ui.statusbar.setStyleSheet('color: navy; fontweight: bold;')
172.
self.ui.statusbar.showMessage(message)
173.
# creating log file
174.
self.log()
175.
176.
def updater(self, val):
177.
val = self.timeSeconds
178.
if val == 0:
179.
self.timer.stop()
180.
self.check()
181.
self.ui.scrollArea.setEnabled(False)
182.
self.ui.pushButton.setEnabled(False)
183.
self.ui.label.setText(self.intToTime(val))
184.
self.timeSeconds -= 1
185.
186.
def intToTime(self, num):
187.
h = 0
188.
m = 0
189.
if num >= 3600:
135
© А.И.Горожанов
190.
h = num // 3600
191.
num = num % 3600
192.
if num >= 60:
193.
m = num // 60
194.
num = num % 60
195.
s = num
196.
str1 = "%d." % h
197.
if m < 10:
198.
199.
200.
201.
202.
203.
204.
205.
str1 += "0%d:" % m
else:
str1 += "%d:" % m
if s < 10:
str1 += "0%d" % s
else:
str1 += "%d" % s
return str1 # returns time as a string
206.
207.
def log(self):
208.
file = open("log.txt", 'w', encoding='utf-8')
209.
file.write("Дата и время записи: ")
210.
file.write(time.strftime("%Y-%m-%d %H:%M:%S"))
211.
file.write('\n\n')
212.
file.write(self.logStr)
213.
file.write('\n')
214.
file.write("Результат: %.2f" % self.result + " %\n")
215.
file.write('Выполнено за %d секунд\n' %
(self.timeConstant - self.timeRes))
216.
file.write('Количество действий: %d' %
self.triggeredNum)
217.
file.close()
218.
self.triggeredNum = 0
219.
self.logStr = ''
220.
221. if __name__ == "__main__":
136
© А.И.Горожанов
222.
app = QtWidgets.QApplication(sys.argv)
223.
myapp = MyWin()
224.
myapp.show()
225.
sys.exit(app.exec_())
В строке 19 объявлен пустой список tp. В строке 86 этот список
заполняется.
В
функции
addWidgetsToInterface()
начинается
разделение кода в зависимости от значения атрибута type. В строке 122
производится проверка. Если текущий вопрос содержит кнопки радио, то
выполняется уже знакомый нам алгоритм. Если там флажки (строка 124), то
формируется виджет QCheckBox. Заметьте, что флажки одного задания также
помещаются в группу кнопок. Если это было бы невозможно, то вся наша
работа была бы обречена на провал. В функции check() также произойдет
разделение кода. В строке 139 происходит проверка, выполняется код для
кнопок радио. В строке 148 начинается код для флажков. Переменная
correctThis объявляется как пустой список (строка 149). Создается еще
один пустой список chbCor (150). Перебираются все флажки (строка 151).
Если флажок отмечен, то его текст добавляется в список chbCor (строка 156).
Если ни один из флажков не отмечен, то в список chbCor помещается только
значение 'none' (строки 157-158). Списки ответов пользователя и правильных
ответов сортируются, чтобы в случае нахождения там одинаковых ответов,
быть одинаковыми (строки 159-160). Списки сравниваются (строка 161). В
случае равенства (т.е. задание решено верно) накапливающая переменная
counter увеличивается на баллы, отведенные этому заданию. Задача решена.
Теперь в тренажере представлены и кнопки радио, и флажки. Задания
стали более разнообразными. Попробуйте пройти тренажер за отведенные 30
секунд несколько раз. Вы увидите, что он стал сложнее, и это сделало его
полезнее.
***
137
© А.И.Горожанов
Проверьте и расширьте свое понимание (7.5): Когда мы оперировали
только кнопками радио, показатель количества действий можно было
соотносить с количеством вопросов. Например, если в тренажере 10 заданий и
количество предпринятых действий равняется тоже 10, то можно заключить,
что задание выполнено в минимальное количество интеракций. С введением
флажков становится непонятно минимальное количество возможных действий
для достижения 100%. Каждое задание имеет свое количество правильных
ответов, поэтому при тех же 10 заданиях минимальное количество действий
может быть и 10, и 15, и 40 и т.д. Добавьте в протокол параметр «Коэффициент
интеракций», который будет отображать процентное отношение предпринятых
действий к минимальному количеству действий, которые нужно совершить для
достижения 100%.
(7.6): При формировании протокола данные о правильном ответе и ответе
пользователя для заданий с флажками выводятся как списки (в кавычках и
квадратных скобках). Это абсолютно не мешает их восприятию, но все же
измените вывод протокола, чтобы эти данные выводились без кавычек и
скобок, через точку с запятой. Для полноты картины пусть в протоколе для
каждого задания отражается и его коэффициент сложности.
***
В этой главе Вы написали мощный и разносторонний тренажер, который
может включать в себя картинку и два типа заданий – с кнопками радио и
флажками. Задания получили коэффициент сложности. Теперь Ваш тренажер
формирует подробный протокол, включая важный параметр количества
совершенных действий.
138
© А.И.Горожанов
Глава 8. Автоматический анализ протоколов
При большом количестве протоколов, состоящих из десятков заданий,
провести их общий анализ вручную весьма затруднительно. Очевидно, что
необходимо
автоматизировать
процесс
анализа
большого
количества
протоколов, чтобы иметь возможность быстро систематизировать информацию
о результатах работы.
Прежде
чем
модифицировать
написать
файл
соответствующую
протокола,
чтобы
его
имя
программу
нужно
включало
больше
информации. Файл logmain13.py содержит нужные модификации. Обратите
внимание на строку 241:
logFileName = self.name1 + '_' + self.group1 + '_%.2f' %
self.result + '_' + str(self.timeConstant - self.timeRes) +
'_%.2f' % (self.triggeredNum/self.minNum*100) + '_' +
str(int(round(time.time() * 1000))) + '.txt'
Имя файла протокола состоит из имени, группы, результата, времени
выполнения,
коэффициента
интеракций
и
времени
записи,
например,
Антонова Е.С._124_46.15_18_375.00_1400015481874.txt.
Это сделано для того, чтобы анализирующей программе не пришлось
брать эту информацию из файла. Работа только с заголовками ускорит процесс
обработки данных.
Теперь можно приступать к созданию анализирующей программы. Она
должна открывать папку с файлами протоколов, которые относятся к одному и
тому же тренажеру. Создать такую папку должен человек, это очень несложно.
Из заголовков файлов программа должна извлечь всю информацию и
представить ее в виде таблицы. Также автоматически должны быть рассчитаны
и показаны максимальный балл, минимальный балл, средний балл, среднее
время выполнения и среднее количество интеракций. Таблица должна иметь
возможность сортировки по каждому из пяти параметров.
139
© А.И.Горожанов
В Qt Designer мы заготовим интерфейс, в котором кроме уже известных
нас виджетов появится новый – таблица (QTableWidget). Подробно о
виджете
можно
узнать
из
официальной
документации
разработчика
[QTableWidget / QtProject]. В результате получится следующее (см. Рис. 8.1):
Рис. 8.1 Интерфейс analyzer.ui
Таблица пока не заполнена ничем, но в ней есть заголовки столбцов. В
меню File расположены три пункта: Open, Save и Exit. Надписи внизу также
пока имеют нулевые значения. Эту оболочку заполнит основная программа, она
сохранена под именем analyzermain.py (см. Код 8.1):
Код 8.1 Программа analyzermain.py
1.
import sys
2.
import os
3.
import io
4.
from analyzer import *
5.
from PyQt5 import QtCore, QtGui, QtWidgets
6.
7.
class MyWin(QtWidgets.QMainWindow):
140
© А.И.Горожанов
8.
9.
resD = []
10.
timeD = []
11.
interD = []
12.
13.
def __init__(self, parent=None):
14.
QtWidgets.QWidget.__init__(self, parent)
15.
self.ui = Ui_MainWindow()
16.
self.ui.setupUi(self)
17.
18.
self.ui.actionOpen.triggered.connect(self.openFunction)
19.
self.ui.actionSave.triggered.connect(self.saveFunction)
20.
21.
# Save as HTML or TXT
22.
def saveFunction(self):
23.
pass
24.
25.
26.
def openFunction(self):
options = QtWidgets.QFileDialog.DontResolveSymlinks
| QtWidgets.QFileDialog.ShowDirsOnly
27.
directory =
QtWidgets.QFileDialog.getExistingDirectory(self,
28.
"Choose Folder with Logfiles",
29.
"some text", options=options)
30.
31.
if directory:
# set row quantity
32.
self.ui.tableWidget.setRowCount(len(os.listdir(directory)))
33.
self.item = [[''] * 6] *
len(os.listdir(directory))
34.
path = os.path.abspath(directory + '\\' +
os.listdir(directory)[0])
141
© А.И.Горожанов
35.
r = open(path, 'r', encoding='utf-8')
36.
title = r.readlines()[0].split(":")[1][1:]
37.
r.close()
38.
for file in range (0,
len(os.listdir(directory))):
39.
if
os.listdir(directory)[file].endswith('.txt'):
40.
dataFromName =
os.listdir(directory)[file].split('.txt')[0].split('_')
41.
for data in range (0,
len(dataFromName)):
42.
self.item[file][data] =
QtWidgets.QTableWidgetItem()
43.
if data == 0 or data == 1:
44.
self.item[file][data].setText(dataFromName[data])
45.
else:
46.
self.item[file][data].setData(QtCore.Qt.EditRole,
float(dataFromName[data]))
47.
self.ui.tableWidget.setItem(file,
data, self.item[file][data])
48.
self.resD.append(float(dataFromName[2]))
49.
self.timeD.append(float(dataFromName[3]))
50.
self.interD.append(float(dataFromName[4]))
51.
self.ui.minRes.setText(str(min(self.resD)))
52.
self.ui.maxRes.setText(str(max(self.resD)))
53.
self.ui.avgRes.setText('%.2f' %
(sum(self.resD)/len(self.resD)))
54.
self.ui.avgTime.setText('%.2f' %
(sum(self.timeD)/len(self.timeD)))
55.
self.ui.avgInter.setText('%.2f' %
142
© А.И.Горожанов
(sum(self.interD)/len(self.interD)))
56.
self.ui.trTitle.setText(title)
57.
58.
if __name__ == "__main__":
59.
app = QtWidgets.QApplication(sys.argv)
60.
myapp = MyWin()
61.
myapp.show()
62.
sys.exit(app.exec_())
Разберем программу построчно. В строках 9-11 создаются пустые списки,
в которые позже будут записаны данные о результате, времени выполнения и
интеракциях соответственно. Строки 18 и 19 связывают пункты меню с
функциями. Функция сохранения пока что является пустой (строки 22-23). В
строке 25 объявляется функция openFunction(), в которой и будет
разворачивается основное действие программы. Сразу вызывается диалоговое
окно выбора папки (строки 26 и 27). Никаких особенных настроек в нем делать
не нужно. Теперь выбранная папка находится в переменной directory. Если
эта переменная существует, т.е. если какая-то папка выбрана, то функция
продолжает свою работу. В строке 32 таблица достраивается строками по числу
файлов в папке. В строке 33 создается пустой двухмерный список, размер
которого также зависит от количества файлов, т.е. количества анализируемых
протоколов. В список item будут помещены ячейки таблицы (пока у таблицы
вообще нет ячеек, только заголовки столбцов). Цифра 6 будет неизменной при
любом количестве протоколов. В строке 34 в переменную path помещается
путь к первому файлу протокола (мы помним, что в папке собраны протоколы
для одного и того же тренажера). Из него надо извлечь название тренажера. Это
происходит в строках 35-37. Далее следуют два цикла for, которые вложены
один в другой. Первый перебирает по одному файлы из папки directory, это
ряды таблицы. Второй заполняет текущий ряд ячейками. Строка 39 проверяет,
является ли файл файлом с расширением .txt. В принципе, от этой проверки
143
© А.И.Горожанов
толку мало, но для тренировки она полезна. В список dataFromName
помещаются данные из имени файла. Это пять переменных типа String. В
строке 42 создаются ячейки для текущего ряда таблицы. В них помещается
текст, но по-разному. Мы поставили перед собой задачу сортировки данных в
таблице. Для того, чтобы сортировать правильно, программа должна знать тип
данных, которые находятся в одном столбце. В первых двух столбцах это
строковые данные (String), в остальных трех – данные типа float. Эти
данные помещаются в ячейки таблицы разными методами (строки 44 и 46). В
строке 47 ячейки выводятся в интерфейс. Теперь таблица видна пользователю.
В строках 48-50 заполняются списки, объявленные в самом начале класса.
Заметьте, что переменная dataFromName обнуляется каждое прохождение
внешнего цикла for. В строках 51-56 высчитываются и помещаются в надписи
минимальные, максимальные и средние значения.
Для проверки работы программы можно воспользоваться заранее
заготовленными протоколами для воображаемых двух групп студентов (папка
results). Если после запуска программы выбрать пункт меню File -> Open и
в открывшемся диалоговом окне выбрать папку results, то результат будет
таким (см. Рис. 8.2):
Рис. 8.2 Результат работы программы analyzermain.py
144
© А.И.Горожанов
Замечательно то, что таблица автоматически создаст полосы прокрутки,
если ячейки не будет помещаться в экран. Ряды автоматически нумеруются.
Под таблицей выведена статистическая информация. Столбцы таблицы можно
сортировать от меньшего к большему или от большего к меньшему нажатием
левой кнопки мыши на нужный заголовок, например, на результат (см. Рис.
8.3):
Рис. 8.3 Сортировка результата по возрастанию
145
© А.И.Горожанов
Теперь можно двигаться дальше. У нас есть очень хорошая таблица со
статистикой. Но на практике бывает полезно сохранить и распечатать результат
в различных вариантах сортировки. Надо написать функцию сохранения
данных. Сохранение будет происходить в файл. Осталось выбрать формат.
Самое простое решение – это текстовый файл, но читать такой документ
неудобно, в нем нельзя сделать таблицу, выделение полужирным или цветом.
Мы остановимся на формате HTML, т.к. файл в этом формате можно не только
просмотреть в любом браузере, но и сохранить как PDF или распечатать. Это
как раз то, что надо. Если Вы не очень уверенно чувствуете себя в общении с
кодом гипертекстовой разметки, Вы можете пополнить свои знания в
Интернете, в частности на сайте htmlbook.ru [htmlbook].
Функция сохранения программы analyzermain1.py будет выглядеть так (см.
Код 8.2):
Код 8.2 Функция сохранения программы analyzermain1.py
24.
# Save as HTML
25.
def saveFunction(self):
26.
options = QtWidgets.QFileDialog.Options()
146
© А.И.Горожанов
27.
fileName, _ =
QtWidgets.QFileDialog.getSaveFileName(self,
28.
"Save Data To HTML File", "", "HTML Files
(*.html)", options=options)
29.
if fileName:
30.
r = open(fileName, 'w', encoding='utf-8')
31.
r.write('''<!DOCTYPE HTML><html><head><META HPPTEQIUV="Content-Type" CONTENT="text/html; charset=utf8"><style>table {border: 1px solid black; border-collapse:
collapse;} td {border: 1px solid black;} th {border: 1px
solid black; background: #CCC;}</style></head>\n''')
32.
r.write('<h3>Тренажер: %s</h3>\n' %
self.ui.trTitle.text())
33.
r.write('<p>Время записи файла: ' +
time.strftime("%Y-%m-%d %H:%M:%S") + '</p>\n')
34.
r.write('<p>Результат: Мин. <b>%s</b>\tСредн.
<b>%s</b>\tМакс. <b>%s</b></p>\n' % (self.ui.minRes.text(),
self.ui.avgRes.text(), self.ui.maxRes.text()))
35.
r.write('<p>Средн. время выполнения:
<b>%s</b></p>\n' % self.ui.avgTime.text())
36.
r.write('<p>Средн. коэф. интеракций:
<b>%s</b></p><table>\n' % self.ui.avgInter.text())
37.
r.write('<tr><th>No.</th><th>Имя</th><th>Группа</th><th>Рез
ультат</th><th>Время (c)</th><th>Интеракции</th></tr>')
38.
strTbl = ''
39.
for row in range (0,
self.ui.tableWidget.rowCount()):
40.
strTbl += '<tr>'
41.
strTbl += '<td>%d.</td>' % (row+1)
42.
for col in range (0,
self.ui.tableWidget.columnCount()):
43.
strTbl += '<td>%s</td>' %
self.ui.tableWidget.item(row, col).text()
147
© А.И.Горожанов
44.
strTbl += '</tr>\n'
45.
r.write(strTbl)
46.
r.write('</table></body></html>')
47.
r.close()
В начале функции выводится диалоговое окно сохранения файла. Если
файл выбран, то создается файл (строка 30), в который записывается код
HTML. В строках 31-37 записываются служебные данные, название тренажера,
время записи в файл, статистика результата (минимум, средний, максимум),
среднее время выполнения, средний коэффициент интеракций и заголовок
таблицы. В строке 38 создается накапливающая переменная strTbl, которая
соберет практически всю оставшуюся информацию. Далее следуют два цикла
for (один вложен в другой), которые перебирают все ячейки таблицы – в том
виде как они выглядят на момент сохранения. Важно, что сохраняется именно
текущее состояние таблицы, над которой пользователь произвел нужные ему
процедуры сортировки. В строке 45 накапливающая переменная записывается в
файл. В строке 46 в файл дописываются финальные теги. Файл закрывается.
Сгенерированный код получится таким (см. Код 8.3):
Код 8.3 Автоматически сгенерированный файл HTML
1.
<!DOCTYPE HTML><html><head><META HPPT-EQIUV="Content-Type"
CONTENT="text/html; charset=utf-8"><style>table {border:
1px solid black; border-collapse: collapse;} td {border:
1px solid black;} th {border: 1px solid black; background:
#CCC;}</style></head>
2.
<h3>Тренажер: Math01
3.
</h3>
4.
<p>Время записи файла: 2014-05-14 22:18:41</p>
5.
<p>Результат: Мин. <b>7.69</b>
Средн. <b>50.00</b> Макс.
<b>100.0</b></p>
6.
<p>Средн. время выполнения: <b>16.00</b></p>
7.
<p>Средн. коэф. интеракций: <b>123.44</b></p><table>
8.
<tr><th>No.</th><th>Имя</th><th>Группа</th><th>Результат</t
148
© А.И.Горожанов
h><th>Время
(c)</th><th>Интеракции</th></tr><tr><td>1.</td><td>Сидорова
Е.У.</td><td>123</td><td>7.69</td><td>10</td><td>100</td></
tr>
9.
<tr><td>2.</td><td>Сидоров
Н.Е.</td><td>123</td><td>100</td><td>22</td><td>225</td></t
r>
10.
<tr><td>3.</td><td>Петров
Н.К.</td><td>123</td><td>23.08</td><td>2</td><td>0</td></tr
>
11.
<tr><td>4.</td><td>Иванов
П.И.</td><td>123</td><td>69.23</td><td>11</td><td>100</td><
/tr>
12.
<tr><td>5.</td><td>Ванина
Е.К.</td><td>123</td><td>84.62</td><td>21</td><td>87.5</td>
</tr>
13.
<tr><td>6.</td><td>Ванин
Н.Е.</td><td>124</td><td>23.08</td><td>30</td><td>0</td></t
r>
14.
<tr><td>7.</td><td>Антонова
Е.С.</td><td>124</td><td>46.15</td><td>18</td><td>375</td><
/tr>
15.
<tr><td>8.</td><td>Сидорова
Л.Д.</td><td>124</td><td>46.15</td><td>14</td><td>100</td><
/tr>
16.
</table></body></html>
Хотя читать код в таком написании и не очень удобно человеку, браузер
отлично интерпретирует его и выведет на экран правильно отформатированный
текст (см. Рис. 8.4):
Рис. 8.4 Вид файла HTML, открытого браузером
149
© А.И.Горожанов
Такой
вывод
выглядит
очень
профессионально
и
современно.
Дополнительно в программу добавлена активация и деактивация пункта меню
Save. Обратите внимание на то, что сразу после запуска программы пункт меню
Save является неактивным. И это понятно, ведь сохранять еще нечего.
Активация происходит только в конце функции openFunction(). Все это
сделано для удобства пользователя, чтобы он понимал, какие действия
являются доступными в текущий момент времени. Можно сказать, что с
помощью этого интерфейс программы стал «более дружеским» (more user
friendly).
***
Проверьте и расширьте свое понимание (8.1): Это задание на
повторение материала. У изученного нами анализатора остался неактивным
пункт меню Exit. модифицируйте программу таким образом, чтобы при
нажатии на красный крестик ничего не происходило, а при выборе пункта меню
Exit выводилось бы диалоговое окно, спрашивающее подтверждение выхода из
программы.
150
© А.И.Горожанов
***
Будем двигаться дальше. Допустим, что несколько человек работали над
одним и тем же тренажером некоторое время. С помощью нашей программы
мы можем получить данные о среднем балле, времени и т.д. Но что если мы
захотим узнать, какие вопросы были самыми сложными, а какие самыми
легкими? Этот параметр чрезвычайно важен для педагога и автора учебных
материалов, т.к. помогает понять, что именно вызывает трудности. К тому же
интересно проверить, правильно ли присвоены коэффициенты сложности. Пока
наша программа не дает такого ответа, и это нужно исправить.
Для анализа такого рода мы еще расширим файл протокола, чтобы в него
добавились коэффициент сложности для каждого вопроса и строка со
значением TRUE или FALSE. Протокол получится таким (см. Рис. 8.5):
Рис. 8.5 Фрагмент протокола с новыми данными
Файл тренажера, который составляет протокол указанного образца
сохранен под именем logmain14.py. Он изменился не очень сильно, но все же
изменился. Обратите внимание на функции check() и log(). Строка 264
делает файл протокола доступным только для чтения. Это важно, поскольку
анализатор будет читать файл целиком, и если его структура будет случайно
изменена, то программа выдаст ошибку. В строках 192-195 в протокол
151
© А.И.Горожанов
добавляется значение TRUE или FALSE, а строка 159 отвечает за добавление
параметра сложности.
В анализирующей программе изменений будет намного больше. Прежде
всего изменится сам интерфейс (см. файл analyzer1.py). В него добавится новая
группа меню Stats с единственным пунктом Questions Statistics. Названия
заголовков
таблицы
убраны.
Теперь
подробно
рассмотрим
файл
analyzermain3.py. Ввиду глобальности изменений приведу его код целиком (см.
Код 8.4):
Код 8.4 Программа analyzermain3.py
1.
import sys
2.
import os
3.
import io
4.
import time
5.
from analyzer1 import *
6.
from PyQt5 import QtCore, QtGui, QtWidgets
7.
8.
class MyWin(QtWidgets.QMainWindow):
9.
10.
checker = False
11.
12.
def __init__(self, parent=None):
13.
QtWidgets.QWidget.__init__(self, parent)
14.
self.ui = Ui_MainWindow()
15.
self.ui.setupUi(self)
16.
17.
self.ui.actionSave.setEnabled(False)
18.
19.
self.ui.actionOpen.triggered.connect(self.openFunction)
20.
self.ui.actionSave.triggered.connect(self.saveFunction)
21.
self.ui.actionExit.triggered.connect(self.closeProg)
22.
152
© А.И.Горожанов
self.ui.actionQuestions_Statistics.triggered.connect(self.qu
estFunction)
23.
24.
25.
def closeProg(self):
result = QtWidgets.QMessageBox.question(self,
"Confirm Dialog", "Really quit?", QtWidgets.QMessageBox.Yes
| QtWidgets.QMessageBox.No, QtWidgets.QMessageBox.No)
26.
if result == QtWidgets.QMessageBox.Yes:
27.
self.checker = True
28.
self.close()
29.
30.
def closeEvent(self, e):
31.
if self.checker:
32.
33.
34.
e.accept()
else:
e.ignore()
35.
36.
37.
def questFunction(self):
options = QtWidgets.QFileDialog.DontResolveSymlinks
| QtWidgets.QFileDialog.ShowDirsOnly
38.
directory =
QtWidgets.QFileDialog.getExistingDirectory(self,
39.
"Choose Folder with Logfiles",
40.
"some text", options=options)
41.
42.
if directory:
path = os.path.abspath(directory + '\\' +
os.listdir(directory)[0])
43.
r = open(path, 'r', encoding='utf-8')
44.
content = r.read()
45.
# counts how many questions there are in log
46.
occur = content.count("Сложность: ")
47.
questions = []
48.
valueQ = []
49.
content = content.split('\n')
153
© А.И.Горожанов
50.
for i in range(0, len(content)):
51.
if content[i] == '':
52.
questions.append(content[i+1])
53.
valueQ.append(content[i+2].split()[1])
54.
r.close()
55.
# here are the question
56.
questions = questions[0:-2]
57.
counterQ = [0] * occur
58.
# iterate files
59.
for i in range (0, len(os.listdir(directory))):
60.
path = os.path.abspath(directory + '\\' +
os.listdir(directory)[i])
61.
# open files one by one
62.
r = open(path, 'r', encoding='utf-8')
63.
content = r.read()
64.
content = content.split('\n')
65.
# iterate questions in a file
66.
for q in range (0, len(questions)):
67.
index = content.index(questions[q])
68.
while (content[index] != ''):
69.
index += 1
70.
if content[index-1] == 'TRUE':
71.
counterQ[q] += 1
72.
r.close()
73.
#for q in range (0, len(questions)):
74.
#print(questions[q] + ', ' +
str(counterQ[q]) + ', ' + valueQ[q] + '\n')
75.
# output tableview
76.
# set row quantity
77.
self.ui.tableWidget.setRowCount(occur)
78.
self.ui.tableWidget.setColumnCount(3)
79.
item = QtWidgets.QTableWidgetItem()
80.
self.ui.tableWidget.setHorizontalHeaderItem(0,
item)
154
© А.И.Горожанов
81.
header =
self.ui.tableWidget.horizontalHeaderItem(0)
82.
header.setText('Вопрос')
83.
item = QtWidgets.QTableWidgetItem()
84.
self.ui.tableWidget.setHorizontalHeaderItem(1,
item)
85.
header =
self.ui.tableWidget.horizontalHeaderItem(1)
86.
header.setText('Отвечено раз')
87.
item = QtWidgets.QTableWidgetItem()
88.
self.ui.tableWidget.setHorizontalHeaderItem(2,
item)
89.
header =
self.ui.tableWidget.horizontalHeaderItem(2)
90.
header.setText('Сложность')
91.
self.item = [[''] * 3] * occur
92.
for row in range (0,
self.ui.tableWidget.rowCount()):
93.
for col in range (0,
self.ui.tableWidget.columnCount()):
94.
self.item[row][col] =
QtWidgets.QTableWidgetItem()
95.
if col == 0:
96.
self.item[row][col].setText(questions[row])
97.
elif col == 1:
98.
self.item[row][col].setData(QtCore.Qt.EditRole,
int(counterQ[row]))
99.
else:
100.
self.item[row][col].setData(QtCore.Qt.EditRole,
int(valueQ[row]))
101.
self.ui.tableWidget.setItem(row, col,
155
© А.И.Горожанов
self.item[row][col])
102.
self.ui.avgInter.setText('')
103.
self.ui.avgRes.setText('')
104.
self.ui.avgTime.setText('')
105.
self.ui.maxRes.setText('')
106.
self.ui.minRes.setText('')
107.
self.ui.actionSave.setEnabled(True)
108.
109.
# Save as HTML
110.
def saveFunction(self):
111.
options = QtWidgets.QFileDialog.Options()
112.
fileName, _ =
QtWidgets.QFileDialog.getSaveFileName(self,
113.
"Save Data To HTML File", "", "HTML Files
(*.html)", options=options)
114.
if fileName:
115.
r = open(fileName, 'w', encoding='utf-8')
116.
r.write('''<!DOCTYPE HTML><html><head><META
HPPT-EQIUV="Content-Type" CONTENT="text/html; charset=utf8"><style>table {border: 1px solid black; border-collapse:
collapse;} td {border: 1px solid black;} th {border: 1px
solid black; background: #CCC;}</style></head>\n''')
117.
r.write('<h3>Тренажер: %s</h3>\n' %
self.ui.trTitle.text())
118.
r.write('<p>Время записи файла: ' +
time.strftime("%Y-%m-%d %H:%M:%S") + '</p>\n')
119.
r.write('<p>Результат: Мин. <b>%s</b>\tСредн.
<b>%s</b>\tМакс. <b>%s</b></p>\n' % (self.ui.minRes.text(),
self.ui.avgRes.text(), self.ui.maxRes.text()))
120.
r.write('<p>Средн. время выполнения:
<b>%s</b></p>\n' % self.ui.avgTime.text())
121.
r.write('<p>Средн. коэф. интеракций:
<b>%s</b></p><table>\n<tr><th>No.</th>' %
self.ui.avgInter.text())
156
© А.И.Горожанов
122.
headerT = ''
123.
for col in range (0,
self.ui.tableWidget.columnCount()):
124.
headerT += '<th>%s</th>' %
self.ui.tableWidget.horizontalHeaderItem(col).text()
125.
r.write(headerT)
126.
strTbl = ''
127.
for row in range (0,
self.ui.tableWidget.rowCount()):
128.
strTbl += '</tr><tr>'
129.
strTbl += '<td>%d.</td>' % (row+1)
130.
for col in range (0,
self.ui.tableWidget.columnCount()):
131.
strTbl += '<td>%s</td>' %
self.ui.tableWidget.item(row, col).text()
132.
strTbl += '</tr>\n'
133.
r.write(strTbl)
134.
r.write('</table></body></html>')
135.
r.close()
136.
137.
def openFunction(self):
138.
139.
self.resD = []
140.
self.timeD = []
141.
self.interD = []
142.
143.
options = QtWidgets.QFileDialog.DontResolveSymlinks
| QtWidgets.QFileDialog.ShowDirsOnly
144.
directory =
QtWidgets.QFileDialog.getExistingDirectory(self,
145.
"Choose Folder with Logfiles",
146.
"some text", options=options)
147.
148.
if directory:
# set row quantity
157
© А.И.Горожанов
149.
self.ui.tableWidget.setRowCount(len(os.listdir(directory)))
150.
self.ui.tableWidget.setColumnCount(5)
151.
152.
item = QtWidgets.QTableWidgetItem()
153.
self.ui.tableWidget.setHorizontalHeaderItem(0,
item)
154.
header =
self.ui.tableWidget.horizontalHeaderItem(0)
155.
header.setText('Имя')
156.
item = QtWidgets.QTableWidgetItem()
157.
self.ui.tableWidget.setHorizontalHeaderItem(1,
item)
158.
header =
self.ui.tableWidget.horizontalHeaderItem(1)
159.
header.setText('Группа')
160.
item = QtWidgets.QTableWidgetItem()
161.
self.ui.tableWidget.setHorizontalHeaderItem(2,
item)
162.
header =
self.ui.tableWidget.horizontalHeaderItem(2)
163.
header.setText('Результат')
164.
item = QtWidgets.QTableWidgetItem()
165.
self.ui.tableWidget.setHorizontalHeaderItem(3,
item)
166.
header =
self.ui.tableWidget.horizontalHeaderItem(3)
167.
header.setText('Время выполнения')
168.
item = QtWidgets.QTableWidgetItem()
169.
self.ui.tableWidget.setHorizontalHeaderItem(4,
item)
170.
header =
self.ui.tableWidget.horizontalHeaderItem(4)
171.
header.setText('Интеракции')
158
© А.И.Горожанов
172.
173.
self.item = [[''] * 6] *
len(os.listdir(directory))
174.
path = os.path.abspath(directory + '\\' +
os.listdir(directory)[0])
175.
r = open(path, 'r', encoding='utf-8')
176.
title = r.readlines()[0].split(":")[1][1:]
177.
r.close()
178.
for file in range (0,
len(os.listdir(directory))):
179.
if
os.listdir(directory)[file].endswith('.txt'):
180.
dataFromName =
os.listdir(directory)[file].split('.txt')[0].split('_')
181.
for data in range (0,
len(dataFromName)):
182.
self.item[file][data] =
QtWidgets.QTableWidgetItem()
183.
if data == 0 or data == 1:
184.
self.item[file][data].setText(dataFromName[data])
185.
else:
186.
self.item[file][data].setData(QtCore.Qt.EditRole,
float(dataFromName[data]))
187.
self.ui.tableWidget.setItem(file,
data, self.item[file][data])
188.
self.resD.append(float(dataFromName[2]))
189.
self.timeD.append(float(dataFromName[3]))
190.
self.interD.append(float(dataFromName[4]))
191.
self.ui.minRes.setText(str(min(self.resD)))
192.
self.ui.maxRes.setText(str(max(self.resD)))
159
© А.И.Горожанов
193.
self.ui.avgRes.setText('%.2f' %
(sum(self.resD)/len(self.resD)))
194.
self.ui.avgTime.setText('%.2f' %
(sum(self.timeD)/len(self.timeD)))
195.
self.ui.avgInter.setText('%.2f' %
(sum(self.interD)/len(self.interD)))
196.
self.ui.trTitle.setText(title)
197.
self.ui.actionSave.setEnabled(True)
198.
199. if __name__ == "__main__":
200.
app = QtWidgets.QApplication(sys.argv)
201.
myapp = MyWin()
202.
myapp.show()
203.
sys.exit(app.exec_())
В классе MyWin() шесть функций: __init__(), closeProg(),
closeEvent(),
questFunction(),
saveFunction()
и
openFunction(). В __init__() происходит привязка нового пункта меню
к функции questFunction() (строка 22). Функции closeProg()
и
closeEvent()знакомы Вам из предыдущих заданий. Они отвечают за то,
чтобы программу нельзя было закрыть красным крестиком, а выбор пункта
меню File -> Exit вызывал бы диалоговое окно подтверждения выхода из
программы. questFunction() – совершенно новая функция, которая и
производит анализ вопросов тренажера.
Сначала вызывается диалоговое меню выбора папки с протоколами для
анализа. Здесь налицо явное сходство с функцией openFunction(). В
строках 42-44 открывается и считывается в переменную содержание первого
протокола. Из него программа возьмет общую информацию о тренажере.
Строка 46 считает количество вопросов. Оно равно числу надписей
"Сложность: " в файле протокола. В строке 47 создается пустой список для
вопросов, а в строке 48 – еще один пустой список для показателя сложности
160
© А.И.Горожанов
вопроса. Индексы списков будут синхронизированы, например, в questions[0]
будет находиться один из вопросов тренажера (не важно какой), но в valueQ[0]
будет находиться сложность именно для вопроса questions[0].
В строке 49 содержание фала разбивается на строки. Теперь content – это
список строк. Цикл строки 50 перебирает все строки в файле и если строка
является пустой, то записывает следующую строку как вопрос, а следующую за
ней как сложность этого вопроса. Все это соотносится со структурой файла
протокола, в которой пустые строки отделяют вопросы. Алгоритм сбивается
только в конце файла, где после пустой строки следуют еще одна пустая строка
и общие данные. Поэтому в строке 56 последний элемент списка
отбрасывается. Со списком valueQ делать такое не обязательно, т.к. его длина
ни на что не влияет. В строке 57 создается список из нулей длиной равной
количеству вопросов тренажера. В него будут записаны данные о количестве
правильных ответов для вопросов. Список также будет синхронизирован с
списком question. Строка 59 запускает цикл for, который перебирает все
файлы протоколов в выбранной папке, включая и первый файл. Файлы
открываются, считываются и разбиваются на строки (строки 60-64). Далее
следует еще один цикл for, который перебирает все вопросы. В строке 67
находится позиция текущего вопроса (это номер строки в файле). Начиная с нее
цикл while спускается по строкам файла вниз и ищет ближайшую пустую
строку. Как только она найдена, происходит выход из цикла и значение строки
до найденной пустой строки проверяется. Если это 'TRUE', то накапливается
элемент списка counterQ. Файл закрывается (строка 72).
Перед нами алгоритм из цикла for, в который вложен еще одни цикл for,
а в него вложен цикл while. Немного упрощая, вся эта конструкция
перебирает по одному файлы протокола. Для каждого файла протокола
перебирает вопросы и проверяет, отвечены ли они правильно. И хотя порядок
следования вопросов в файлах разный, ищутся они все равно в том порядке, в
котором они располагаются в списке questions. Именно поэтому накопление
161
© А.И.Горожанов
правильных ответов происходит корректно. Т.е. для questions[x] всегда
накапливаются counterQ[x], где x – индекс вопроса в списке questions.
Теперь у нас есть данные в трех списках. Их можно вывести в консоль (эти
строки закомментированы). Но наша задача – вывести их в таблицу. В таблице
изначально пять колонок с заголовками, а здесь нам нужна таблица из трех
колонок с другими заголовками, поэтому таблицу нужно предварительно
настроить. Строки 77 и 78 задают нужно число строк и колонок таблицы. В
строках 79-90 создаются и называются заголовки. Как и в функции
openFunction(), создается двухмерный список элементов таблицы. Циклы
for в строках 92-93 перебирают все ячейки таблицы и устанавливают в них
значения из переменных questions, valueQ и counterQ. В первой колонке это
значение типа String, во второй и третьей это Integer. В строке 101 ячейка
выводится в интерфейс. В строках 102-106 обнуляется статистическая
информация под таблицей, при анализе вопросов она не нужна. В строке 107
пункт меню File -> Save активируется, как и в функции openFunction().
Для
проверки
текущего
результата
Вы
можете
воспользоваться
протоколами вымышленных студентов из папки results1. В результате
получается следующее (см. Рис. 8.6):
Рис 8.6 Интерфейс с таблицей анализа вопросов
162
© А.И.Горожанов
На приведенном рисунке вопросы отсортированы от самого сложного к
самому легкому. Таблица изменилась так, как мы и ожидали. Безусловно,
пользователь захочет сохранить этот результат в файл, но функция Save
сработает некорректно, т.к. она настроена на сохранение конкретной таблицы, а
не любой таблицы, которая находится в текущий момент на экране. Надо
произвести изменения. Вернемся назад к коду программы.
Теперь функция saveFunction() считает количество колонок в
текущей таблице и только после этого записывает их заголовки (строки 124125). Далее происходит вывод исходя из текущего количества рядов (строка
127) и колонок (строка 130). Теперь функция стала универсальной, она будет
сохранять то, что есть в интерфейсе, какая бы таблица там не была.
Функция openFunction() также модифицирована. Перед выводом
ячеек в таблицу она устанавливает количество строк и колонок, создает и
называет заголовки (строки 149-171).
163
© А.И.Горожанов
Попробуйте сохранить данные статистики вопросов в файл HTML. Он
будет выглядеть следующим образом (см. Рис. 8.7):
Рис. 8.7 Файл HTML с сохраненным анализом вопросов
Таблица построена верно.
***
Проверьте и расширьте свое понимание (8.2): При вызове функции
questFunction() программа стирает все статистические данные под
таблицей, кроме названия тренажера. Исправьте это. Пусть при вызове любой
функции программа правильно выводит в интерфейс название тренажера,
данные которого обрабатывает в настоящее время.
***
В этой главе Вы сделали очень важный и большой шаг вперед. Вы
написали
программу,
анализирующую
двумя
способами
протоколы
тренажеров. Результаты анализа представлены в виде таблицы, их можно
сортировать и сохранять в файл HTML. Очень важен анализ вопросов, который
очень быстро позволяет понять, что вызывает сложности у студентов при
164
© А.И.Горожанов
прохождении тренажера. Можно сказать, что в Вашем распоряжении целый
программный комплекс, с которым уже можно полноценно работать.
165
© А.И.Горожанов
Глава 9. Программные тренажеры с заданием на аудирование
В предыдущих главах мы оперировали текстовой информацией. Но при
полноценном изучении иностранного языка этого недостаточно, поэтому в этой
главе мы рассмотрим тренажер, позволяющий развивать умение аудирования.
PyQt5 способен решить эту задачу, тем более что на этой библиотеке (а именно
на Qt) написан популярнейший плейер VLC [VLC].
В файл базы данных добавится тег aud, который внутри себя будет
содержать задание к аудиотексту. В атрибуте src будет находится название
аудиофайла, например:
<aud src='audio.wav'>Прослушайте текст и выполните задания:</aud>
С внедрением в тренажер аудиоплейера код программы значительно
увеличится. (При написании кода использовались примеры, выпущенные
разработчиком PyQt5, о чем в файлах имеются соответствующие записи
[GitHub]). Тем не менее, приведу его целиком (см. Код 9.1):
Код 9.1 Программа audmain.py
1.
import sys
2.
import random
3.
import os
4.
import io
5.
import time
6.
import stat
7.
import xml.dom.minidom
8.
from shell2 import *
9.
from diatwo import Ui_Dialog
10.
from PyQt5.QtMultimedia import QMediaPlayer, QMediaPlaylist,
QMediaContent, QSound
11.
from PyQt5 import QtCore, QtGui, QtWidgets
12.
13.
class StartDialog(QtWidgets.QDialog, Ui_Dialog):
14.
15.
def __init__(self,parent=None):
166
© А.И.Горожанов
16.
QtWidgets.QDialog.__init__(self,parent)
17.
self.setupUi(self)
18.
19.
class MyWin(QtWidgets.QMainWindow):
20.
21.
lbs = []
22.
rbs = [[''] * 10] * 15 # emply list 15x10
23.
bgrs = []
24.
labels = []
25.
variants = []
26.
correct = []
27.
value = []
28.
tp = []
29.
picName = []
30.
lblPic = []
31.
logStr = ''
32.
triggeredNum = 0
33.
minNum = 0
34.
name1 = ''
35.
group1 = ''
36.
timeRes = 0
37.
38.
def __init__(self, parent=None):
39.
QtWidgets.QWidget.__init__(self, parent)
40.
self.ui = Ui_MainWindow()
41.
self.ui.setupUi(self)
42.
43.
while self.name1 == '' or self.group1 == '':
44.
dialog = StartDialog(self)
45.
if dialog.exec_():
46.
self.name1 = dialog.lineEdit.text()
47.
self.group1 = dialog.lineEdit_2.text()
48.
49.
self.duration = 0
167
© А.И.Горожанов
50.
self.playerState = QMediaPlayer.StoppedState
51.
self.timerAud = QtCore.QTimer(self)
52.
self.player = QMediaPlayer()
53.
self.playlist = QMediaPlaylist()
54.
self.player.setPlaylist(self.playlist)
55.
56.
# frame with buttons play and stop, and duration
label
57.
self.frame =
QtWidgets.QFrame(self.ui.scrollAreaWidgetContents)
58.
self.frame.setFrameShape(QtWidgets.QFrame.StyledPanel)
59.
self.frame.setFrameShadow(QtWidgets.QFrame.Raised)
60.
self.frame.setObjectName("frame")
61.
self.horizontalLayout =
QtWidgets.QHBoxLayout(self.frame)
62.
self.horizontalLayout.setObjectName("horizontalLayout")
63.
self.pushButton = QtWidgets.QPushButton(self.frame)
64.
self.pushButton.setObjectName("pushButton")
65.
self.horizontalLayout.addWidget(self.pushButton)
66.
self.pushButton_3 =
QtWidgets.QPushButton(self.frame)
67.
self.pushButton_3.setObjectName("pushButton_3")
68.
self.horizontalLayout.addWidget(self.pushButton_3)
69.
self.labelDuration = QtWidgets.QLabel(self.frame)
70.
self.labelDuration.setText('0')
71.
self.horizontalLayout.addWidget(self.labelDuration)
72.
73.
# creating timer
74.
self.timer = QtCore.QTimer(self)
75.
# xml handling (read & mix)
76.
self.mixXml()
77.
# read to DOM
168
© А.И.Горожанов
78.
self.readToDom()
79.
# assigning layout to the scrollarea
80.
self.verticalLayout =
QtWidgets.QVBoxLayout(self.ui.scrollAreaWidgetContents)
81.
self.verticalLayout.setObjectName("verticalLayout")
82.
# adding title
83.
self.trTitle =
QtWidgets.QLabel(self.ui.scrollAreaWidgetContents)
84.
self.trTitle.setAlignment(QtCore.Qt.AlignLeading|QtCore.Qt.A
lignLeft|QtCore.Qt.AlignTop)
85.
self.trTitle.setText('')
86.
# adding standart widgets for listening
87.
self.verticalLayout.addWidget(self.trTitle)
88.
self.lblAud =
QtWidgets.QLabel(self.ui.scrollAreaWidgetContents)
89.
self.lblAud.setAlignment(QtCore.Qt.AlignLeading|QtCore.Qt.Al
ignLeft|QtCore.Qt.AlignTop)
90.
self.lblAud.setText('')
91.
self.horizontalSlider =
QtWidgets.QSlider(self.ui.scrollAreaWidgetContents)
92.
self.horizontalSlider.setOrientation(QtCore.Qt.Horizontal)
93.
self.horizontalSlider.setObjectName("horizontalSlider")
94.
95.
# adding frame
96.
self.verticalLayout.addWidget(self.lblAud)
97.
self.verticalLayout.addWidget(self.horizontalSlider)
98.
self.verticalLayout.addWidget(self.frame)
99.
100.
self.pushButton.setIcon(self.style().standardIcon(QtWidgets.
169
© А.И.Горожанов
QStyle.SP_MediaPlay))
101.
self.pushButton_3.setIcon(self.style().standardIcon(QtWidget
s.QStyle.SP_MediaStop))
102.
self.pushButton_3.setEnabled(False)
103.
104.
# adding widgets to the scrollarea
105.
self.addWidgetsToInterface()
106.
self.timer.timeout.connect(lambda:
self.updater(self.timeSeconds))
107.
# starting timer
108.
self.timer.start(1000)
109.
110.
self.player.durationChanged.connect(self.durationChanged1)
111.
self.player.positionChanged.connect(self.positionChanged1)
112.
self.player.stateChanged.connect(self.setState)
113.
114.
self.horizontalSlider.setRange(0,
self.player.duration() / 1000)
115.
116.
self.pushButton.clicked.connect(self.play1)
117.
self.pushButton_3.clicked.connect(self.stop1)
118.
119.
self.horizontalSlider.sliderMoved.connect(self.changePositio
n)
120.
121.
self.ui.pushButton.clicked.connect(self.finish)
122.
123.
def open1(self, filename):
124.
self.audiofile = filename
125.
fileInfo = QtCore.QFileInfo(self.audiofile)
170
© А.И.Горожанов
126.
url =
QtCore.QUrl.fromLocalFile(fileInfo.absoluteFilePath())
127.
self.playlist.addMedia(QMediaContent(url))
128.
self.pushButton.setEnabled(True)
129.
130.
131.
def play1(self):
if self.playerState in (QMediaPlayer.StoppedState,
QMediaPlayer.PausedState):
132.
133.
134.
self.player.play()
elif self.playerState == QMediaPlayer.PlayingState:
self.player.pause()
135.
136.
137.
def stop1(self):
self.player.stop()
138.
139.
def positionChanged1(self, progress):
140.
progress = progress / 1000
141.
if not self.horizontalSlider.isSliderDown():
142.
self.horizontalSlider.setValue(progress)
143.
self.updateDurationInfo(progress)
144.
145.
def durationChanged1(self, duration):
146.
duration = duration / 1000
147.
self.duration = duration
148.
self.horizontalSlider.setMaximum(duration)
149.
150.
def updateDurationInfo(self, currentInfo):
151.
duration = self.duration
152.
if currentInfo or duration:
153.
currentTime =
QtCore.QTime((currentInfo/3600)%60, (currentInfo/60)%60,
currentInfo%60, (currentInfo*1000)%1000)
154.
totalTime = QtCore.QTime((duration/3600)%60,
(duration/60)%60, duration%60, (duration*1000)%1000);
171
© А.И.Горожанов
155.
format1 = 'hh:mm:ss' if duration > 3600 else
'mm:ss'
156.
tStr = currentTime.toString(format1) + " / " +
totalTime.toString(format1)
157.
158.
159.
else:
tStr = ''
self.labelDuration.setText(tStr)
160.
161.
162.
def setState(self,state):
if state != self.playerState:
163.
self.playerState = state
164.
if state == QMediaPlayer.StoppedState:
165.
self.horizontalSlider.setEnabled(False)
166.
self.pushButton_3.setEnabled(False)
167.
self.pushButton.setIcon(self.style().standardIcon(QtWidgets.
QStyle.SP_MediaPlay))
168.
elif state == QMediaPlayer.PlayingState:
169.
self.horizontalSlider.setEnabled(True)
170.
self.pushButton_3.setEnabled(True)
171.
self.pushButton.setIcon(self.style().standardIcon(QtWidgets.
QStyle.SP_MediaPause))
172.
elif state == QMediaPlayer.PausedState:
173.
self.horizontalSlider.setEnabled(False)
174.
self.pushButton_3.setEnabled(True)
175.
self.pushButton.setIcon(self.style().standardIcon(QtWidgets.
QStyle.SP_MediaPlay))
176.
177.
178.
179.
180.
def changePosition(self, seconds):
if self.playerState == QMediaPlayer.PausedState:
pass
elif self.playerState == QMediaPlayer.PlayingState:
172
© А.И.Горожанов
181.
self.player.setPosition(seconds * 1000)
182.
183.
def finish(self):
184.
self.timeRes = self.timeSeconds
185.
self.timeSeconds = 0
186.
187.
def mixXml(self):
188.
# read xml and mix the lines
189.
self.linesMixed = []
190.
self.r = open("db6.xml", 'r', encoding='utf-8')
191.
self.fileRead = self.r.readlines()
192.
for line in range(2, len(self.fileRead)-1):
193.
self.linesMixed.append(self.fileRead[line])
194.
random.shuffle(self.linesMixed)
195.
self.r.close()
196.
197.
# write temporary xml with new mixed lines
198.
self.w = open("temp.xml", 'w', encoding='utf-8')
199.
self.w.write('''<?xml version="1.0" encoding="utf8"?>\n<content>\n''')
200.
for line in self.linesMixed:
201.
self.w.write('%s' % line)
202.
self.w.write('</content>')
203.
self.w.close()
204.
205.
def readToDom(self):
206.
# read to DOM
207.
self.dom = xml.dom.minidom.parse('temp.xml')
208.
self.collection = self.dom.documentElement
209.
# reading timeout to a variable
210.
self.timeSeconds =
int(self.collection.getElementsByTagName("time")[0].childNod
es[0].data)
211.
# reading title
173
© А.И.Горожанов
212.
self.title =
self.collection.getElementsByTagName("ttl")[0].childNodes[0]
.data
213.
self.questAudio =
self.collection.getElementsByTagName("aud")[0].childNodes[0]
.data
214.
aud = self.collection.getElementsByTagName("aud")[0]
215.
audFile = aud.getAttribute('src')
216.
self.open1(audFile)
217.
self.timeConstant = self.timeSeconds
218.
self.linesArr =
self.collection.getElementsByTagName("q")
219.
for line in range(0, len(self.linesArr)):
220.
# label's text
221.
self.labels.append(self.linesArr[line].childNodes[0].data)
222.
# variants' text
223.
self.variants.append(self.linesArr[line].getAttribute('ans')
.split('**?**'))
224.
# correct answer
225.
self.correct.append(self.linesArr[line].getAttribute('cor'))
226.
# value
227.
self.value.append(int(self.linesArr[line].getAttribute('pnt'
)))
228.
# reading type
229.
self.tp.append(self.linesArr[line].getAttribute('type'))
230.
# adding picture name if any
231.
if self.linesArr[line].hasAttribute('pic'):
232.
self.picName.append(self.linesArr[line].getAttribute('pic'))
174
© А.И.Горожанов
233.
else:
234.
self.picName.append('empty')
235.
# Mix variants
236.
for variant in self.variants:
237.
random.shuffle(variant)
238.
# Deleting temporary file
239.
os.remove('temp.xml')
240.
241.
def addWidgetsToInterface(self):
242.
# adding widgets to the scrollarea
243.
self.trTitle.setText("Тренажер %s" % self.title)
244.
for line in range (0, len(self.labels)):
245.
self.lblAud.setText("<b>%s</b>" %
self.questAudio)
246.
self.lbs.append(QtWidgets.QLabel(self.ui.scrollAreaWidgetCon
tents))
247.
self.lbs[line].setAlignment(QtCore.Qt.AlignLeading|QtCore.Qt
.AlignLeft|QtCore.Qt.AlignTop)
248.
self.lbs[line].setText('<b>%s</b>' %
self.labels[line])
249.
self.verticalLayout.addWidget(self.lbs[line])
250.
# adding picture
251.
if self.picName[line] != 'empty':
252.
self.lblPic.append(QtWidgets.QLabel(self.ui.scrollAreaWidget
Contents))
253.
self.lblPic[line].setAlignment(QtCore.Qt.AlignLeading|QtCore
.Qt.AlignLeft|QtCore.Qt.AlignTop)
254.
self.lblPic[line].setPixmap(QtGui.QPixmap(os.getcwd() + "/"
+ self.picName[line]))
175
© А.И.Горожанов
255.
self.verticalLayout.addWidget(self.lblPic[line])
256.
else:
257.
self.lblPic.append(QtWidgets.QLabel(self.ui.scrollAreaWidget
Contents))
258.
# adding button group
259.
self.bgrs.append(QtWidgets.QButtonGroup(self.ui.centralwidge
t))
260.
if self.tp[line] == 'chb':
261.
self.bgrs[line].setExclusive(False)
262.
self.correct[line] =
self.correct[line].split('**?**')
263.
# click counter
264.
self.bgrs[line].buttonClicked.connect(self.increaseNum)
265.
for v in range(0, len(self.variants[line])):
266.
# check br/chb
267.
if self.tp[line] == 'rb':
268.
self.rbs[line][v] =
QtWidgets.QRadioButton(self.ui.scrollAreaWidgetContents)
269.
elif self.tp[line] == 'chb':
270.
self.rbs[line][v] =
QtWidgets.QCheckBox(self.ui.scrollAreaWidgetContents)
271.
self.bgrs[line].addButton(self.rbs[line][v])
272.
self.rbs[line][v].setText(self.variants[line][v])
273.
self.verticalLayout.addWidget(self.rbs[line][v])
274.
275.
276.
def increaseNum(self):
self.triggeredNum += 1
277.
176
© А.И.Горожанов
278.
def check(self):
279.
counter = 0
280.
for group in range(0, len(self.bgrs)):
281.
# adding questions
282.
self.logStr += self.labels[group] + '\n'
283.
self.logStr += 'Сложность: %d\n' %
self.value[group]
284.
# check rb/chb
285.
if self.tp[group] == 'rb':
286.
self.minNum += 1
287.
correctThis = '--'
288.
for rb in self.bgrs[group].buttons():
289.
# adding variants
290.
self.logStr += '\t' + rb.text() + '\n'
291.
if rb.isChecked():
292.
if rb.text() == self.correct[group]:
293.
correctThis = rb.text()
294.
counter += self.value[group]
295.
296.
297.
elif self.tp[group] == 'chb':
if self.correct[group][0] != 'none':
self.minNum += len(self.correct[group])
298.
correctThis = []
299.
chbCor = []
300.
for rb in self.bgrs[group].buttons():
301.
# adding variants
302.
self.logStr += '\t' + rb.text() + '\n'
303.
if rb.isChecked():
304.
# marked checkboxes
305.
chbCor.append(rb.text())
306.
307.
if len(chbCor) == 0:
chbCor.append('none')
308.
chbCor.sort()
309.
self.correct[group].sort()
310.
if self.correct[group] == chbCor:
177
© А.И.Горожанов
311.
counter += self.value[group]
312.
correctThis = chbCor
313.
314.
self.logStr += 'Правильный ответ: %s' %
self.listToString(self.correct[group]) + '\n'
315.
self.logStr += 'Ответ пользователя: %s' %
self.listToString(correctThis) + '\n'
316.
if self.listToString(self.correct[group]) ==
self.listToString(correctThis):
317.
self.logStr += 'TRUE\n'
318.
else:
319.
self.logStr += 'FALSE\n'
320.
self.logStr += '\n'
321.
# And this is the result! Rounded to 2 decimal
points
322.
self.result = float(counter/sum(self.value)*100)
323.
message = "Your result is " + "%.2f" % self.result +
"%"
324.
self.ui.statusbar.setStyleSheet('color: navy; fontweight: bold;')
325.
self.ui.statusbar.showMessage(message)
326.
# creating log file
327.
self.log()
328.
329.
330.
def listToString(self, getlist):
if type(getlist).__name__ == 'list':
331.
string = ''
332.
for i in getlist:
333.
string += i + '; '
334.
string = string[:-2] + '.'
335.
return string
336.
337.
else:
return getlist
338.
178
© А.И.Горожанов
339.
def updater(self, val):
340.
val = self.timeSeconds
341.
if val == 0:
342.
self.timer.stop()
343.
self.check()
344.
self.ui.scrollArea.setEnabled(False)
345.
self.ui.pushButton.setEnabled(False)
346.
self.ui.label.setText(self.intToTime(val))
347.
self.timeSeconds -= 1
348.
349.
def intToTime(self, num):
350.
h = 0
351.
m = 0
352.
if num >= 3600:
353.
h = num // 3600
354.
num = num % 3600
355.
if num >= 60:
356.
m = num // 60
357.
num = num % 60
358.
s = num
359.
str1 = "%d." % h
360.
if m < 10:
361.
362.
363.
364.
365.
366.
367.
368.
str1 += "0%d:" % m
else:
str1 += "%d:" % m
if s < 10:
str1 += "0%d" % s
else:
str1 += "%d" % s
return str1 # returns time as a string
369.
370.
371.
def log(self):
logFileName = self.name1 + '_' + self.group1 +
'_%.2f' % self.result + '_' + str(self.timeConstant -
179
© А.И.Горожанов
self.timeRes) + '_%.2f' %
(self.triggeredNum/self.minNum*100) + '_' +
str(int(round(time.time() * 1000))) + '.txt'
372.
file = open(logFileName, 'w', encoding='utf-8')
373.
file.write("Тренажер: %s\n" % self.title)
374.
file.write("Имя: %s\nГруппа: %s\n" % (self.name1,
self.group1))
375.
file.write("Дата и время записи: ")
376.
file.write(time.strftime("%Y-%m-%d %H:%M:%S"))
377.
file.write('\n\n')
378.
file.write(self.logStr)
379.
file.write('\n')
380.
file.write("Результат: %.2f" % self.result + " %\n")
381.
file.write('Выполнено за %d с\n' %
(self.timeConstant - self.timeRes))
382.
# Nubmer of iteractions in percent relative to
minimal number of interactions.
383.
# Minimal number of iteractions equals the quantity
of radio buttons
384.
# multiplied by the quantity of correct variants in
questions with check boxes
385.
# see line 140 and 150-151.
386.
file.write('Коэффициент интеракций: %.2f' %
(self.triggeredNum/self.minNum*100) + ' %')
387.
# makes log file read only
388.
os.chmod(os.path.abspath(logFileName), stat.S_IREAD)
389.
file.close()
390.
self.triggeredNum = 0
391.
self.logStr = ''
392.
393. if __name__ == "__main__":
394.
app = QtWidgets.QApplication(sys.argv)
395.
myapp = MyWin()
396.
myapp.show()
180
© А.И.Горожанов
397.
sys.exit(app.exec_())
Такое количество кода не должно Вас испугать. В конце концов, Вы
можете использовать программу как готовый шаблон. Но основные пункты
надо постараться понять, быть может не вдаваясь в детали. (Нумерация строк
кода приводится в соответствии с выше указанным кодом и может отличаться
от нумерации в файле на сайте поддержки данного издания).
В строке 10 импортируются классы модуля QtMultimedia. Этот модуль
отвечает за воспроизведение медиаресурсов, а нашем случае – аудиофайлов. В
интерфейс программно добавляются надпись с формулировкой задания на
аудирование, горизонтальный слайдер и контейнер (QFrame) с двумя кнопками
(воспроизведение/пауза и стоп) и надписью с временем воспроизведения
(строки 56-71). Немного выше, в строках 49-54, объявляются новые
переменные, среди которых таймер timerAud и переменные классов
QMediaPlayer() и QMediaPlaylist() – собственно плейер и его список
воспроизведения, в который мы поместим один единственный файл. В строках
100 и 101 кнопкам придается вид кнопок плейера – play и stop. В конце
функции __init__() плейер, слайдер и кнопки связываются с множеством
новых функций: open1(), play1(), stop1(), positionChanged1(),
durationChanged1(),
changePosition().
updateDurationInfo(),
Эти
функции
регулируют
setState()
отношения
и
между
воспроизводимым аудиопотоком, кнопками и слайдером.
Функция open1() открывает нужный аудиофайл и помещает его в список
воспроизведения. Функция play1() вызывается при нажатии кнопки play.
Если статус плейера имеет значение остановки или паузы, то плейер начинает
играть, если плейер играл, то он устанавливается на паузу. Функция stop1()
останавливает плейер. Функция positionChanged1() двигает слайдер во
время воспроизведения. Функция durationChanged1() связана с сигналом
плейера durationChanged. Функция updateDurationInfo() отвечает за
181
© А.И.Горожанов
отображение времени воспроизведения в надписи labelDuration. Функция
setState() регулирует установки статуса плейера. При этом изображение на
кнопке воспроизведения меняется (play/pause), слайдер и кнопки активируются
и дезактивируются для того, чтобы пользователю удобно было управлять
плейером. Функция changePosition() реагирует на изменение позиции
слайдера пользователем при помощи мыши. Если плейер находится на паузе, то
пользователь не может изменять позицию слайдера. Если плейер играет, то
возможность перемотки включена.
Если Вы не до конца поняли код программы, то отчаиваться не стоит.
Повторяю, Вы можете пользоваться программой как готовым шаблоном.
При запуске программы тренажер выглядит следующим образом (см. Рис.
9.1):
Рис. 9.1 Интерфейс программы audmain.py сразу после запуска
Плейер помещается в верхнюю часть области прокрутки. Пользователю
предлагается нажать кнопку воспроизведения, т.к. другие элементы плейера
182
© А.И.Горожанов
деактивированы. После нажатия кнопки воспроизведения плейер приходит в
движение (см. Рис. 9.2):
Рис. 9.2 Интерфейс программы audmain.py во время воспроизведения плейера
Кнопка воспроизведения изменилась на кнопку паузы. Кнопка остановки
активировалась. Надпись времени воспроизведения показывает сколько
проиграно аудиопотока по отношению к общей длине файла в минутах и
секундах (одна секунда из четырех). По окончании воспроизведения плейер
возвращается в свое первоначальное состояние.
Таким образом, у пользователя есть возможность послушать запись
несколько раз, при необходимости проматывая ее вперед и назад, насколько
позволяет время, отведенное на выполнение тренажера.
Плейер тестировался на аудиофайлах с расширением .wav и .mp3. Файлы
.mp3 предпочтительнее, т.к. имеют меньший объем. Помните, что применяя в
учебных материалах сторонние аудиозаписи, Вы можете нарушать чьи-то
авторские права. Самый лучший вариант – использовать аудиозаписи, которые
183
© А.И.Горожанов
были записаны лично Вами, например, с помощью знакомых носителей
иностранного языка, имея в распоряжении обычный цифровой диктофон,
смартфон, ноутбук и другие бытовые устройства. Качество записи при этом
может быть довольно высокое, во всяком случае достаточное для учебных
целей.
***
Проверьте и расширьте свое понимание (9.1): Повысьте степень
универсальности тренажера. Пусть при считывании базы данных программа
определяет наличие в ней тега aud и в зависимости от этого выводит или не
выводит в интерфейс аудио плейер.
(9.2): Иногда задания требуют прослушать текст целиком, не пользуясь
перемоткой. Модифицируйте программу так, чтобы перематывать плейер было
нельзя.
(9.3): Также иногда требуется ограничить число полных прослушиваний
(например, 2 или 3). Пусть программа считывает число прослушиваний из
файла XML и выводит в интерфейс число оставшихся прослушиваний.
***
В этой, заключительной, главе Вы познакомились с возможностью
встраивать в тренажер задание на аудирование, а это значит, что Ваши
электронные учебные материалы уже оперируют тремя видами представления
информации: текстом, картинкой и звуком.
184
© А.И.Горожанов
Заключение
В предыдущих главах мы с Вами проделали очень большую работу. Мы
установили Python 3.3 и PyQt 5, научились работать в Qt Designer и начали
писать программы для ЭВМ учебного назначения.
Конечно, мы рассмотрели только очень небольшую часть возможностей
библиотеки PyQt 5. В ней есть еще много полезных виджетов и классов,
которые вряд ли можно описать все в одной книге. Это учебное пособие было
нацелено на то, чтобы помочь сделать первые шаги, которые затем могут
превратиться в уверенное следование по пути профессионального развития.
Программирование – это такая область, в которой самостоятельное
обучение играет ключевую роль, потому что языки программирования и
библиотеки к ним обновляются очень часто, добавляя новые возможности. На
момент написания этого учебного пособия версия 5 библиотеки PyQt является
актуальной, но так не будет всегда. Параллельно с ней существует и активно
используется версия 4, которая, правда, имеет существенные отличия от 5-й.
Поэтому
тем,
кого
заинтересовала
тема
профессионально
ориентированного программирования, я рекомендую внимательно следить за
развитием
версий,
пользоваться
официальной
профессиональным форумом Stack Overflow.
Удачи Вам и терпения!
185
документацией
и
© А.И.Горожанов
Список литературы
Ответы на вопросы о лицензиях GNU [Электронный ресурс]. – URL:
http://www.gnu.org/licenses/gpl-faq.html#WhatDoesWrittenOfferValid (Дата
обращения: 28.04.2014)
An Introduction to Interactive Programming in Python by Rice University / on
Coursera [Электронный ресурс]. – URL:
https://www.coursera.org/course/interactivepython (Дата обращения: 05.05.2014)
Downey A., Elkner J., Meyers Ch. How to Think Like a Computer Scientist:
Learning with Python. – Wellesley, Massachusetts: Green Tea Press, 2002. – 290 pp.
– ISBN 0-9716775-0-6
GitHub // pyqt5 / examples / multimediawidgets / player.py [Электронный
ресурс]. – URL:
https://github.com/baoboa/pyqt5/blob/master/examples/multimediawidgets/player.py
(Дата обращения: 16.05.2014)
HTML5 - Entities Reference [Электронный ресурс]. – URL:
http://www.tutorialspoint.com/html5/html5_entities.htm (Дата обращения:
05.05.2014)
htmlbook.ru [Электронный ресурс]. – URL: http://htmlbook.ru (Дата
обращения: 15.05.2014)
Learn to Program: The Fundamentals by University of Toronto // Coursera
[Электронный ресурс]. – URL: https://www.coursera.org/course/programming1
(Дата обращения: 05.05.2014)
Programming for Everybody by Charles Severance, University of Michigan /
on Coursera [Электронный ресурс]. – URL: https://class.coursera.org/pythonlearn001 (Дата обращения: 05.05.2014)
QStatusBar Class / QtProject [Электронный ресурс]. – URL: http://qtproject.org/doc/qt-5/qstatusbar.html#permanent-message (Дата обращения:
16.04.2014)
186
© А.И.Горожанов
QTableWidget Class / Documentation / QtProject [Электронный ресурс]. –
URL: http://qt-project.org/doc/qt-5/QTableWidget.html (Дата обращения:
14.05.2014)
Stack Overflow // The flagship site of the Stack Exchange Network [Электронный ресурс]. – URL:http://stackoverflow.com (Дата обращения: 24.04.2014).
The Erik Python IDE [Электронный ресурс]. – URL: http://eric-ide.pythonprojects.org (Дата обращения: 16.04.2014)
Tutorialspoint / Python / GUI Programming/Message [Электронный ресурс]. –
URL: http://www.tutorialspoint.com/python/tk_message.htm (Дата обращения:
21.04.2014)
Tutorialspoint / Python / Python GUI Programming [Электронный ресурс]. –
URL: http://www.tutorialspoint.com/python/python_gui_programming.htm (Дата
обращения: 17.04.2014)
Tutorialspoint / Python / Python Strings [Электронный ресурс]. – URL:
http://www.tutorialspoint.com/python/python_strings.htm (Дата обращения:
20.04.2014)
Tutorialspoint / Python / XML Processing [Электронный ресурс]. – URL:
http://www.tutorialspoint.com/python/python_xml_processing.htm (Дата
обращения: 04.05.2014)
VLC media player / Wikipedia [Электронный ресурс]. – URL:
http://htmlbook.ru (Дата обращения: 16.05.2014)
What is XML? / HTMLGoodies [Электронный ресурс]. – URL:
http://www.htmlgoodies.com/beyond/xml/article.php/3473531 (Дата обращения:
04.05.2014)
187
© А.И.Горожанов
Приложение 1. Ключи к заданиям
1.1 Нужно просто удалить строки 10-14 включительно, т.е. весь блок
else.
1.2 Ничего не изменится. Python позволяет использовать как двойные, так
и одинарные кавычки. Главное, чтобы открывающие и закрывающие кавычки
были одинаковыми.
1.3 Для этого нужно добавить после строки 22 еще одну строку:
var.set(1)
Тем самым мы присвоим переменной флажка значение 1.
2.1 Строка 23 должна быть заменена на следующую:
self.pushButton.setGeometry(QtCore.QRect(5, 60, 230, 20))
2.2 Строка 42 примет вид:
MainWindow.setWindowTitle(_translate("MainWindow", "My
Window"))
2.3 Для того, чтобы строка ввода реагировала на нажатие клавиши Enter,
нужно использовать сигнал returnPressed. Добавьте в конце функции
setupUi() строку:
self.lineEdit.returnPressed.connect(self.myFunction)
2.4 Для решения этой задачи нужно дополнить функцию myFunction
проверкой введенного текста, функция примет следующий вид:
def myFunction(self):
s = self.lineEdit.text() # Создает переменную типа String
if s.isspace(): # Проверяет, является ли она ВСЯ пробелами
s = s.strip() # Фактически удаляет все пробелы
self.label.setText("Длина Вашего текста %d" % len(s)) #
Выводит результат
2.5 В этом случае надо будет проверить каждый символ текста и
установить, является ли он пробелом. Для подсчета пробелов создается
отдельная переменная. Код функции примет вид:
def myFunction(self):
s = self.lineEdit.text() # Создает переменную типа String
188
© А.И.Горожанов
counterSpace = 0 # Создает переменную типа Integer
for ch in s: # Перебирает символы введенного текста один
за другим
if ch.isspace(): # Проверяет, является ли символ
пробелом
counterSpace += 1 # Считает количество пробелов,
прибавляет единицу к имеющемуся значению.
self.label.setText("Длина Вашего текста %d / %d" %
(len(s)-counterSpace, counterSpace)) # Выводит результат
2.6 Как и в 2.3, используйте синал returnPressed, но не забудьте
добавить ui при ссылке на переменную строки ввода:
self.ui.lineEdit.returnPressed.connect(self.myFunction)
Строку нужно добавить после строки 11, т.е. в конце фунцкции
__init__().
3.1 Решение состоит в том, чтобы считывать из файла только слова длиной
4 и 5 символов для функций wordFour() и wordFive() соответственно.
Например, для функции wordFour(), строку
self.fileSplit = self.fileRead.split()
нужно заменить на строки
self.fileSplit0 = self.fileRead.split()
self.fileSplit = []
for word in self.fileSplit0:
if len(word) == 4:
self.fileSplit.append(word)
Решение отражено в файле guessmain1.py
3.2 Решение находится в файле guessmainsave.py, соответствующий
интерфейс – в файле guessmainsave.ui.
3.3 Решение находится в файле guessmainsave1.py. Вы можете заметить,
что в функции __init__() создается файл с параметром 'w' (write), затем в
функции saveToFile() он уже открывается с параметром 'a' (append).
189
© А.И.Горожанов
3.4 Решение находится в файле guessmainsave2.py. Обратите внимание на
то, что постоянная надпись, то есть информация о правообладателе, добавлена
в строку состояния в качестве виджета QLabel. Метод showMessage() на
время выключает этот виджет.
4.1 Здесь придется потрудиться и подойти к решению творчески. Изучите
код программы menumain3.py:
1.
import sys
2.
from menu0 import *
3.
from PyQt5 import QtCore, QtGui, QtWidgets
4.
5.
6.
class MyWin(QtWidgets.QMainWindow):
def __init__(self, parent=None):
7.
QtWidgets.QWidget.__init__(self, parent)
8.
self.ui = Ui_MainWindow()
9.
self.ui.setupUi(self)
10.
self.checker = False
11.
12.
self.ui.actionExit.triggered.connect(self.closeProg)
13.
14.
15.
def closeProg(self):
result = QtWidgets.QMessageBox.question(self,
"Confirm Dialog", "Really quit?", QtWidgets.QMessageBox.Yes
| QtWidgets.QMessageBox.No, QtWidgets.QMessageBox.No)
16.
if result == QtWidgets.QMessageBox.Yes:
17.
self.checker = True
18.
self.close()
19.
20.
def closeEvent(self, e):
21.
if self.checker:
22.
23.
24.
e.accept()
else:
e.ignore()
25.
190
© А.И.Горожанов
26.
if __name__ == "__main__":
27.
app = QtWidgets.QApplication(sys.argv)
28.
myapp = MyWin()
29.
myapp.show()
30.
sys.exit(app.exec_())
Решение может показаться не очевидным, но постарайтесь разобраться.
Прежде всего, функция closeEvent() лишается диалогового окна и
принимает по умолчанию значение события ignore(). Ключевой является
строка 10, в которой объявляется переменная checker типа bool,
получающая первоначальное значение False. Обратите внимание на проверку
в строке 21. Программа закрывается только если переменная имеет значение
True. Такое значение присваивается переменной в строке 17. Если
пользователь нажимает на крестик, то вызывается функция closeEvent(), но
переменная checker имеет значение False, поэтому событие закрытия
игнорируется (строка 24). Если пользователь выбирает пункт меню Exit, то
запускается функция closeProg() и выводится диалоговое окно. Если в
диалоговом окне нажимается No, то ничего не происходит. Но если нажимается
Yes, то переменная checker получает значение True (строка 17) и
запускается функция closeEvent() (строка 18), функция переходит к строке
21 и закрывает программу в строке 22.
4.2 Замените четвертый аргумент диалогового окна на следующий:
QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No |
QtWidgets.QMessageBox.Cancel
Правда в нашем приложении третья кнопка совершенно бесполезна, т.к. не
привязана ни к какому событию.
4.3 Замечательное свойство PyQt состоит в том, что многие строковые
переменные могут принимать код HTML. Чтобы получить надпись как на Рис.
4.6, нужно записать третий аргумент диалогового окна как
"<p>Это текст<br>из нескольких строк</p>"
191
© А.И.Горожанов
Код с такой надписью находится в файле menumain4.py.
4.4 Решение находится в файле menumain24.py.
4.5 Решение находится в файле menumain221.py. Файл интерфейса
называется
menu2.py.
За
его
основу
взят
файл
menu0.py.
В
файл
menumain221.py добавлена функция openFile() следующего содержания:
def openFile(self):
options = QtWidgets.QFileDialog.Options()
self.fileName, _ = QtWidgets.QFileDialog.getOpenFileName(self,
"Open File", "", "Text Files (*.txt)", options=options)
if self.fileName:
self.openF = open(self.fileName, 'r', encoding='utf-8')
self.ui.plainTextEdit.insertPlainText(self.openF.read())
self.openF.close()
self.ui.statusbar.showMessage('%s opened' % self.fileName)
В функции также используется виджет QFileDialog, но с методом
getOpenFileName(). Пять параметров метода соответствуют параметрам
метода getSaveFileName(). Содержимое открываемого файла помещается
в текстовое поле посредством метода insertPlainText().
5.1 Это можно сделать тремя строчками в главном файле learnmain3.py:
self.ui.textEdit.setFontPointSize(12)
self.ui.textEdit_2.setFontPointSize(12)
self.ui.textEdit_3.setFontPointSize(12)
Можно расположить этот код в начале функции __init__(), перед
считыванием файла XML.
5.2 Задача непростая, но выполнимая. Замените все символы, которые
XML считает символами разметки, их альтернативными вариантами [HTML5 Entities Reference]. Вы найдете такую запись в файле learn1.xml. Поскольку Вы
вставляете в поле не простой текст, а код HTML, то также надо заменить блок
кода
self.ui.textEdit.setText(self.text[0])
self.ui.textEdit_2.setText(self.text[1])
192
© А.И.Горожанов
self.ui.textEdit_3.setText(self.text[2])
на
self.ui.textEdit.setHtml(self.text[0])
self.ui.textEdit_2.setHtml(self.text[1])
self.ui.textEdit_3.setHtml(self.text[2])
6.1 Функция проверки примет вид:
def check(self):
counter = 0
for group in range(0, len(self.bgrs)):
for rb in self.bgrs[group].buttons():
if rb.isChecked():
if rb.text() == self.correct[group]:
counter += 1
# And this is the result! Rounded to 2 decimal points
percentage = float(counter/len(self.bgrs)*100)
message = "Your result is " + "%.2f" % percentage + "%"
if percentage < 50:
self.ui.statusbar.setStyleSheet('color: red; fontweight: bold;')
elif percentage > 76:
self.ui.statusbar.setStyleSheet('color: green; fontweight: bold;')
else:
self.ui.statusbar.setStyleSheet('color: orange; fontweight: bold;')
self.ui.statusbar.showMessage(message)
Поскольку желтый плохо заметен на сером фоне, то лучше заменить его на
оранжевый или какой-нибудь другой цвет. Полный файл с решением сохранен
под именем shellmain1.py.
6.2 Строка состояния не работает с кодом HTML, но в нее можно добавить
виджет QLabel. Поэтому для решения задачи необходимо модифицировать
функцию __init__(), добавив в нее строки:
self.labelStatus = QtWidgets.QLabel(self.ui.statusbar)
193
© А.И.Горожанов
self.ui.statusbar.addWidget(self.labelStatus)
Также изменится и функция проверки:
def check(self):
counter = 0
for group in range(0, len(self.bgrs)):
for rb in self.bgrs[group].buttons():
if rb.isChecked():
if rb.text() == self.correct[group]:
counter += 1
# And this is the result! Rounded to 2 decimal points
message = "Your result is " + "<b
style=color:'navy';>%.2f</b>" %
float(counter/len(self.bgrs)*100) + "%"
self.labelStatus.setText(message)
Полный файл с решением сохранен под именем shellmain2.py.
6.3 Вся проблема заключается в строке 11 программы shellmain.py. Размер
списка задается в самом начале и далее никак не меняется. Решение в том,
чтобы перенести объявление списка в конец функции readToDom(), когда мы
уже точно знаем, сколько вопросов будет в тренажере. Количество вариантов
ответа сделать разным не получится, но можно ориентироваться на
максимальное значение. Например, если в базе данных XML 60 вопросов, а
самое большое количество вариантов для одного вопроса равняется 10, то надо
создать список 60 на 10.
Полный файл с решением сохранен под именем shellmain3.py. Обратите
внимание на новую переменную в строке 15.
6.4 Виджет добавляется в интерфейс в Qt Designer. Это самая простая
часть решения. Дальше начинается работа в основном файле. Фактически нам
нужно следить за группами кнопок, проверять, нажаты ли в них кнопки и
представлять результат в виде количества процентов (от 0 до 100). В функцию
addWidgetsToInterface() перед вторым циклом for добавляется строка
self.bgrs[line].buttonClicked.connect(self.progressUpdate)
194
© А.И.Горожанов
Она привязывает к каждой группе кнопок функцию, которая запускается
каждый раз, когда пользователь щелкает мышью по одной из кнопок радио
этой группы. Теперь нужно написать эту функцию. Вот ее код:
def progressUpdate(self):
counterPrgr = 0
for bg in self.bgrs:
for rb in bg.buttons():
if rb.isChecked():
counterPrgr += 1
self.ui.progressBar.setValue(
int(counterPrgr/len(self.labels)*100) )
self.ui.progressBar.update()
Создается локальная накапливающая переменная counterPrgr равная
нулю (накапливающей переменная называется потому, что она постоянно будет
прибавлять к уже имеющимся какие-то новые значения). Перебираются все
группы кнопок, внутри этого цикла перебираются все кнопки радио внутри
текущей
группы.
Если
внутри
группы
есть
отмеченная
кнопка,
то
накапливающая переменная увеличивается на единицу. После полного
прохождения
всех
циклов
индикатору
присваивается
значение,
соответствующее проценту решенных заданий. Индикатор обновляется с
помощью метода update(). При этом программа выполняет очень много
действий при каждом нажатии одной из кнопок радио. А это требует
дополнительной памяти, поэтому я рекомендую применять такой способ
индикации только при необходимости.
Полный файл с решением сохранен под именем shellmain4.py. Интерфейс –
под именем shell1.py.
6.5 Файл интерфейса сохранен как shell3.py. Основная программа
называется shellmain6.py. В функцию updater() основной программы
добавляется строка:
self.ui.progressBar.setValue(int(val*(100/int(self.mythread
1.timeSeconds))))
195
© А.И.Горожанов
Программа работает. Однако не всякое техническое решение полезно с
точки зрения методики. Интерфейс с убывающим индикатором прогресса внизу
и таймером обратного отсчета вверху отвлекает студента от самого главного: от
вопросов тренажера. Помните, что для электронных учебных материалов
минималистичность интерфейса – это скорее плюс, чем минус. Поэтому если
выбирать между зеленым мигающим индикатором и маленьким серым
таймером, то выбор однозначно в пользу второго.
6.6 В PyQt 5 оформление виджетов, в том числе и индикатора прогресса,
можно изменить, присвоив этому виджету новую таблицу стилей. Решение
находится в файле shellmain7.py.
7.1 Делается это очень просто. Изменяются только несколько символов
(отмечено красным) в функции log():
def log(self):
file = open("log.txt", 'a', encoding='utf-8')
file.write("Дата и время записи: ")
file.write(time.strftime("%Y-%m-%d %H:%M:%S"))
file.write('\n')
file.write("Результат: %.2f" % self.result + " %\n")
file.write('Выполнено за %d секунд\n\n' %
(self.timeConstant - self.timeSeconds))
file.close()
Файл решения сохранен под именем logmain2.py.
7.2 Импортируйте в программу logmain1.py модуль stat.
Перед
закрытием файла можно сделать его доступным только для чтения. Но этого
мало. Программа создаст файл, запишет в него данные и сделает его доступным
только для чтения, но при следующем нажатии кнопки Check программа
обнаружит файл log.txt и не сможет его перезаписать (или дописать в него чтото), т.к. он теперь доступен только для чтения. В самом начале функции файл
придется делать доступным для записи, а перед закрытием – делать его
доступным только для чтения:
def log(self):
196
© А.И.Горожанов
os.chmod(os.path.abspath("log.txt"), stat.S_IWRITE)
file = open("log.txt", 'w', encoding='utf-8')
file.write("Дата и время записи: ")
file.write(time.strftime("%Y-%m-%d %H:%M:%S"))
file.write('\n')
file.write("Результат: %.2f" % self.result + " %\n")
file.write('Выполнено за %d секунд' % (self.timeConstant self.timeSeconds))
os.chmod(os.path.abspath("log.txt"), stat.S_IREAD)
file.close()
Тот же алгоритм действует и если Вы захотите модифицировать подобным
образом файл logmain2.py.
Файл решения сохранен под именем logmain3.py.
7.3 Для того, чтобы запросить у пользователя данные об имени и учебной
группе, нужно вызвать диалоговое окно еще до вывода основного виджета.
Проще всего было бы воспользоваться виджетом QInputDilog, но не все так
просто. В этом диалоговом окне только одна строка ввода, а для нашего случая
необходимо две надписи и две строки. Поэтому нужно изготовить диалоговое
окно самостоятельно в Qt Disigner (см. файл diatwo.py). Теперь нужно
правильно интегрировать его в основную программу. Для этого создайте еще
один класс:
class StartDialog(QtWidgets.QDialog, Ui_Dialog):
def __init__(self,parent=None):
QtWidgets.QDialog.__init__(self,parent)
self.setupUi(self)
Не забудьте произвести импорт по схеме:
from diatwo import Ui_Dialog
Теперь класс можно вызвать из функции __init__():
while self.name1 == '' or self.group1 == '':
dialog = StartDialog(self)
if dialog.exec_():
197
© А.И.Горожанов
self.name1 = dialog.lineEdit.text()
self.group1 = dialog.lineEdit_2.text()
Код помещен в цикл while, который будет выводить диалоговое окно
снова и снова, пока обе строки ввода не будут заполнены символами.
Переменные name1 и group1 нужно создать заранее. Далее данные об имени и
группе нужно будет внести в протокол. Полный код решения находится в
файле logmain8.py.
7.4 Решение кроется в хорошем понимании алгоритма программы
logmain7.py. Нужно привести таймер к нулевому состоянию. За отсчет времени
отвечает переменная timeSeconds. Как только она уменьшается до нуля,
функция updater() останавливает таймер, вызывает функцию check() и
блокирует интерфейс. Поэтому все дело в том, чтобы нажатием кнопки Check
обнулить переменную timeSeconds, предварительно сохранив ее значение в
переменную timeRes. Это нужно для правильного подсчета времени
выполнения в функции log(). Кнопка отсоединяется от функции check() и
соединяется с функцией finish(), которая выглядит следующим образом:
def finish(self):
self.timeRes = self.timeSeconds
self.timeSeconds = 0
Полный код решения находится в файле logmain9.py.
7.5 Решение находится в файле logmain11.py. В строке 24 объявляется
новая
накапливающая
минимальных
переменная
интеракций
minNum.
(действий)
с
Она
считает
интерфейсом
для
количество
достижения
результата 100%. В функции check() происходит подсчет (строки 140 и 150151). Вывод производится в процентах, округленных до двух знаков после
запятой (строка 223).
7.6 Решение находится в файле logmain12.py. В строке 179 создается новая
функция. Ее задача – представить список как последовательность его элементов
через точку с запятой:
def listToString(self, getlist):
198
© А.И.Горожанов
if type(getlist).__name__ == 'list':
string = ''
for i in getlist:
string += i + '; '
string = string[:-2] + '.'
return string
else:
return getlist
Функция получает в качестве аргумента некую переменную. Если
переменная является списком, то производится действие. Если нет, то
переменная выходит из функции в неизменном виде. В строках 168 и 169
каждая переменная правильного ответа и ответа пользователя перед записью в
протокол проходит через функцию listToString().
8.1 Вспомните код программы menumain3.py. Решение задачи сохранено в
файле analyzermain2.py.
8.2 Решение находится в файле analyzermain4.py. В код добавлены всего
две строки (50 и 108).
9.1
Решение
находится
в
файле
audmain1.py.
В
функции
addWidgetsToInterface() производится проверка на наличие в базе
данных тега aud. Если тег имеется, то плейер выводится в интерфейс. Для
этого почти весь код, связанный с инициализацией плейера, перемещен в
функцию addWidgetsToInterface(). Для проверки вариант базы данных
без тега aud сохранен в файле db7.xml.
9.2 Решение находится в файле audmain2.py. Самый простой способ убрать
перемотку – это дезактивировать слайдер с помощью метода
setEnabled(False)
Для этого нужно поменять True на False в строке 165.
9.3 Решение находится в файле audmain3.py. Введите в тег aud атрибут
times (см. файл bd8.xml). В нем будет храниться число допустимых
прослушиваний. Все изменения в файле основной программы (по сравнению с
199
© А.И.Горожанов
файлом audmain2.py) отмечены комментарием #
new. Кнопка остановки
воспроизведения дезактивирована. Возможность ставить плейер на паузу также
убрана. Кнопка воспроизведения может быть нажата только если слайдер
находится на отметке «0». Переменная setTimes, в которую записано число
воспроизведений, уменьшается на единицу при каждом нажатии кнопки
воспроизведения. Значение этой переменной выводится в формулировку
задания на аудирование.
200
© А.И.Горожанов
Приложение 2. Литература, рекомендуемая для дополнительного
изучения
Прохоренок Н.А. Python 3 и PyQt. Разработка приложений. – СПб.: БХВПетербург, 2012. – 704 с.: ил.
Dusty Phillips. Python 3 Object Oriented Programming. – Packt Publishing Ltd.:
Birmingham, 2010. – 405 p. – ISBN 978-1-849511-26-1.
Harwani B.M. Introduction to Python Programming and Developing GUI
Applications with PyQT – Course Technology, a part of Cengage Learning, 2012.–
423 p. – ISBN-13: 978-1-4354-6097-3.
Pilgrim
M.
Dive
Into Python
3
[Электронный ресурс].
–
URL:
http://www.diveintopython3.net (Дата обращения: 17.05.2014)
Tim Hall and J-P Stacey. Python 3 for Absolute Beginners. – NY: APRESS,
2009. – 314 p. – ISBN-13: 978-1-4302-1632-2.
201