Top Banner
1 Алексей Иванович Горожанов PyQt 5 для лингвистов: профессионально ориентированное программирование Электронное учебное пособие для студентов лингвистических вузов и факультетов (бакалавриат и магистратура) версия в формате PDF Москва, 2014
201

Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

Jul 20, 2020

Download

Documents

dariahiddleston
Welcome message from author
This document is posted to help you gain knowledge. Please leave a comment to let me know what you think about it! Share it to your friends and learn new things together.
Transcript
Page 1: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

1

Алексей Иванович Горожанов

PyQt 5 для лингвистов:

профессионально ориентированное

программирование

Электронное учебное пособие

для студентов лингвистических вузов и факультетов

(бакалавриат и магистратура)

версия в формате PDF

Москва, 2014

Page 2: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

2

Оглавление Вступление ....................................................................................................... 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

Page 3: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

3

Вступление

Уважаемые читатели!

Цель этого учебного пособия – помочь лингвистам (в первую очередь

будущим и настоящим преподавателям и исследователям) научиться создавать

современные программные инструменты, с помощью которых можно решать

профессиональные задачи.

Работа с представленным материалом требует некоторых предварительных

знаний из области информатики. Для того, чтобы получить (или освежить) эти

знания, можно прочитать замечательную книгу 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

Для Вашего удобства архив файлов разбит по главам.

Page 4: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

4

Рубрика Проверьте и расширьте свое понимание содержит

практические задачи, которые предлагается решить. Ко всем задачам в

Приложении 1 Вы найдете ключи.

Настоятельно рекомендуется работать с главами в порядке их следования.

т.к. каждое практическое задание является развитием предыдущего и опирается

на его программный код.

Приложение 2 содержит список дополнительной литературы,

рекомендуемой для изучения.

По профессиональным вопросам, связанным с этим учебным пособием, Вы

можете написать мне на электронную почту: [email protected].

Page 5: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

5

Глава 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). В любом случае, я

Page 6: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

6

рекомендую изучить документы лицензий, чтобы быть полностью

информированным.

Если Вы читали 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

Page 7: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

7

Например, у нас есть элементарная программа hello.py, которая состоит из

одной строки:

print('Hello, world!')

После открытия программы, нажмите F5. Программа будет запущена, в

оболочке IDLE, имитирующей консоль, появится результат работы программы

или вывод (output) (см. Рис. 1.3):

Рис. 1.3 Результат работы программы hello.py

Page 8: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

8

Если у Вас все получилось именно так, то значит, 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="Русский и белорусский языки

Page 9: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

9

являются родственными.")

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. Во-первых, можно

разделить весь код на два части. Первая часть строит основной графический

Page 10: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

10

интерфейс пользователя (GUI – graphical user interface), а вторая – оперирует

данными и, если нужно, изменяет основной графический интерфейс и строит

дополнительные графические элементы (эти элементы называются виджетами).

В нашем случае и то, и другое находится в одном файле: prog1_1.py. Также

можно выделить блок импорта, в нашем случае он состоит из одной записи:

from tkinter import *

В переводе на человеческий язык это означает «Из модуля tkinter

возьми всё». Так мы говорим программе, к какие модули будут ей нужны,

чтобы реализовать последующий код. Поэтому импорт располагается всегда в

начале выполнения алгоритма. Строки 1 и 2 являются комментариями и

игнорируются программой. Они нужны людям, чтобы помещать в программу

что-то еще кроме кода. Все правее от знака «решетки» (hash-sign или number-

sign) и до конца строки считается комментарием. Впрочем, благодаря

прочтению книги Think Python: How To Think Like a Computer Scientist, Вы уже

все это знаете.

Итак, программа начинается со строки 3. Дальше она переходит сразу к

строке 15, т.к. в строках 4-14 находится функция, которая еще не вызвана. В

переменную root помещается главное окно нашего приложения (главный

виджет). Пока еще это только пустая оболочка, которую нужно наполнить

другими виджетами. В терминологии программистов главный виджет

называется родительским элементом (parent), а все зависящие от него –

дочерними или детьми (children). В свою очередь, дети могут включать другие

виджеты и являться по отношению к ним родителями.

Родительский элемент root содержит пять дочерних виджетов: три надписи

(label), флажок (checkbox) и кнопку (button). Строки 17-27 создают

переменные этих виджетов и помещают их в родительский. Строка 28

закрывает графический цикл. Если ее убрать, то программа будет исполнена до

строки 27 и выключится.

Page 11: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

11

В этом общий принцип графических приложений: программа существует

в бесконечном цикле, постоянно ожидая от пользователя каких-то действий.

При выборе действия, связанного с выходом из программы, цикл прерывается.

В программе 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 это тип шрифта и его размер. Параметров может

быть очень много, и не обязательно указывать все их сразу. Благодаря

созданию переменных виджетов, их параметры можно добавлять или изменять

Page 12: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

12

в других частях программы. Если Вы хотите узнать побольше о параметрах

виджетов модуля tkinter, то один из наилучших способов сделать это –

посетить сайт tutorialspoint, раздел Python, подраздел Python-GUI Programming

[tutorialspoint-pythonGUI].

Метод grid() выводит виджеты на экран. Здесь не лишним будет

напомнить, что методом называют функцию, по умолчанию свойственную

объектам определенного класса. Например, для объектов класса String в

Python разработчиками заложено множество методов, и в частности такие

полезные для лингвиста как split() или endswith() [tutorialspoint-

pythonStrings].

Однако, вернемся к визуализации виджетов. Дело в том, что внутри

главного окна все объекты (виджеты) располагаются не произвольно, а

согласно установленному порядку. В коде программы prog1_1.py все виджеты

вписаны в матрицу или таблицу, в которой есть какое-то число строк и

столбцов. Как принято у программистов, их счет начинается с нуля, а не с

единицы. В каждой ячейке объект может быть расположен также по-разному,

относительно сторон (по центру, слева, справа, вверху, внизу и т.д.).

В параметрах метода grid() указывается, в какую ячейку размещать

виджет. В программе prog1_1.py матрица главного окна состоит из одного

столбца и пяти рядов. Все виджеты выровнены по западной стороне (т.е. по

левому краю) (см. Рис. 1.5):

Рис. 1.5 Матрица главного окна программы prog1_1.py

lab_01

lab_02

lab_03

ch_box

button

Page 13: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

13

Параметры метода 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) существует в своем бесконечном

цикле, выйти из которого можно только закрыв его кнопкой «Х». Если флажок

Page 14: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

14

не выключен (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 в меню «Пуск»

Page 15: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

15

Как видно из рисунка, в этой книге будет использована свободно

распространяемая версия PyQt5.2.1.

3. Проверить правильность установки. Для этого открыть IDLE и набрать в

оболочке Python Shell следующее:

import PyQt5

Нажать Enter. Если оболочка не выводит ошибок, а просто переходит на

следующую строку, то все правильно, Python и PyQt «знают» друг о друге (см.

Рис. 1.7):

Рис. 1.7 Проверка установки PyQt5

***

В этой главе Вы получили предварительные сведения о PyQt5.

Вы установили Python's IDLE, PyQt5 и использовали свои знания Python

для написания простого графического приложения.

Page 16: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

16

Глава 2. Первые приложения на PyQt5

Технология создания каждой программы с использованием PyQt5 будет

одинакова (по крайней мере, в этой книге). Работа начинается с построения

графического интерфейса пользователя, а затем к нему добавляются функции

обработки данных. Уже на этом этапе Вы увидите одно из важнейших

достоинств PyQt: наличие среды разработки Qt Designer. Как видно из Рис. 1.6,

Qt Designer находится в группе, созданной при установке PyQt. После открытия

среды на экране появится диалоговое окно (см. Рис. 2.1):

Рис. 2.1 Диалоговое окно при запуске Qt Designer

Здесь нам предлагается выбрать, какой виджет будет главным в

создаваемой программе. Мы выберем установленный по умолчанию виджет

MainWindow. Для этого нужно нажать кнопку «Создать». На экране

появляется чистая заготовка для дальнейшей работы (см. Рис. 2.2):

Рис. 2.2 Заготовка для создания нового графического интерфейса пользователя

Page 17: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

17

Здесь нужно ненадолго остановиться и разобраться с панелями Qt

Designer. Слева находится панель виджетов. Виджеты просто перетаскиваются

мышью в нужное место заготовки. Справа располагаются Инспектор объектов,

Редактор свойств, Редактор действий и Обозреватель ресурсов. Сразу под

меню, вверху экрана, находится панель инструментов. Если активировать меню

Вид, то будут видны все доступные панели. Все эти панели необходимы нам

уже сейчас. Мы разберемся в них, создавая программы.

Создадим интерфейс программы, которая будет получать от пользователя

текст и выводить на экран длину этого текста в символах – вполне

лингвистическая задача.

Мы остановились на чистой заготовке. В нее надо добавить три виджета:

строку ввода (QLineEdit), кнопку (QPushButton) и надпись (QLabel).

Перетаскивая эти виджеты один за другим получим следующее (см. Рис. 2.3):

Рис. 2.3 Заполнение заготовки

Page 18: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

18

Очень хорошо. Теперь измените текст надписи. Для этого выделите

мышью надпись, чтобы она оказалась в синей рамке (см. Рис. 2.4):

Рис. 2.4 Выделенный виджет

Теперь в Редакторе свойств (панель в правой части экрана) отобразились

свойства выделенного виджета. Надо найти свойство text, которое по

умолчанию имеет значение TextLabel (см. Рис. 2.5):

Рис. 2.5 Редактор свойств для виджета QLabel

Page 19: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

19

Это значение нужно изменить на «Длина Вашего текста». Теперь виджет

отражает нужный нам текст. Его самого можно немного растянуть, чтобы текст

был виден целиком (см. Рис. 2.6):

Рис. 2.6 Измененная надпись

Интерфейс готов. Его можно сохранить в файл myinterface.ui (место

расположение файла надо оставить как есть, по умолчанию, т.е. в папке PyQt5).

Однако сам по себе файл с расширением .ui не представляет никакой ценности,

пока он не используется в нашей будущей программе. Есть несколько способов

превратить файл ui в полноценный компонент программы на Python. Один из

них – это конвертировать файл ui в файл py. Для этого Вам придется

поработать в командной строке.

Page 20: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

20

Если Вы работаете в Windows, то для выхода в командную строку нужно

нажать на меню Пуск и ввести в появившееся окно ввода команду «cmd» (см.

Рис. 2.7):

Рис. 2.7 Выход в командную строку

Нажмите Enter. Если все получилось правильно, то вы увидите следующее

(см. Рис. 2.8):

Рис. 2.8 Командная строка

Работа в командной строке возвращает нас во времена операционной

системы DOS, когда еще не было Windows, и людям приходилось общаться с

компьютером посредством набора команд в строке. Можно даже немного

преувеличенно сказать, что тогда каждый пользователь ПК должен был быть

немного программистом.

Но вернемся к нашей задаче, посмотрим внимательно, что представляет

собой командная строка. C:\Users\lenovo> означает, что мы находимся

именно в этой папке (во времена DOS говорили не папка, а директория). Нам

Page 21: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

21

нужно выйти в директорию, в которой расположен файл 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 Переход в нужную директорию

Page 22: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

22

В этой же директории располагается файл 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

Page 23: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

23

Программа работает! Хотя она пока и не считает длину текста в строке

ввода.

Подведем предварительный итог:

1. В среде разработки Qt Designer достаточно просто строить графический

интерфейс пользователя.

2. Затем полученный файл .ui легко конвертируется в работающий файл

.py.

3. Полученный файл .py НЕЛЬЗЯ конвертировать обратно в файл .ui и

вносить в него изменения.

4. Дальше остается только добавить в файл .py функции обработки данных,

но интерфейс изменять (если это необходимо) придется вручную, без Qt

Designer.

Здесь у внимательного читателя может возникнуть справедливый вопрос:

Неужели никто из программистов не видит неудобства в том, что после

конвертации среда разработки Qt Designer остается недоступной? Ведь иногда

приходится вносить в интерфейс программы значительные изменения, и лучше

Page 24: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

24

всего было бы это сделать мышью, а не кодом. Или придется переделывать всю

программу заново?

Ответ: решение есть, мы его обсудим, но немного позже. В 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. # by: PyQt5 UI code generator 5.2.1

7. #

8. # WARNING! All changes made in this file will be lost!

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,

Page 25: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

25

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()

Page 26: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

26

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 Изменение кнопки

Page 27: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

27

Перейдем к функции setupUi(self, MainWindow). Строка 15

устанавливает размер главного окна. Вы наверное заметили, что под надписью

«Длина вашего текста» остается много свободного места. Указанные в методе

resize(x, y) величины задают ширину и высоту окна соответственно.

Система координат при этом имеет следующую направленность (см. Рис. 2.13):

Рис. 2.13 Система координат виджетов PyQt

Установите значение высоты равное 180, чтобы строка 15 приняла вид

MainWindow.resize(240, 180)

Page 28: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

28

и запустите программу. Геометрия окна стала более логичной (см. Рис.

2.14):

Рис. 2.14 Изменение размера главного окна

Чтобы еще потренироваться с размерами, сделаем поле ввода шире. Пусть

оно тянется от одного края главного окна до другого. Вы наверняка догадались,

что для этого нужно изменить строку 19. Метод

setGeometry(QtCore.Qrect(x, y, width, height)) задает

прямоугольник, в котором находится виджет. Параметры x и y устанавливают

координаты левого верхнего угла виджета относительно родительского

виджета (не экрана компьютера!), а width и height отвечают за ширину и

высоту. Мы не будем менять измерения по вертикали, поэтому установим x на

5, а width на 230. Эти координаты я установил путем нескольких

экспериментов. Запустите программу и посмотрите на результат (см. Рис. 2.15):

Рис. 2.15 Изменение размеров строки ввода

Page 29: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

29

Справедливо будет сказать, что все эти операции лучше было бы сделать в

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)

Page 30: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

30

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.

Page 31: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

31

Настало время испытать программу. Запустите ее, введите какой-нибудь

текст (например, «эксперимент») и нажмите кнопку; результат – 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")

Page 32: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

32

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. def myFunction(self):

40. self.label.setText("Длина Вашего текста %d" %

len(self.lineEdit.text()))

41.

42. if __name__ == "__main__":

43. import sys

Page 33: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

33

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 получилась достаточно

интересной. Несмотря на небольшой объем в программе есть все основные

блоки, которые будут представлены и в более крупных проектах. Вернемся к

вопросу об удобстве изменения интерфейса.

Секрет в том, чтобы держать в отдельных файлах класс интерфейса и

функции обработки данных.

Page 34: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

34

Конвертируйте файл интерфейса 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. class MyWin(QtWidgets.QMainWindow):

6. 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 создают

Page 35: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

35

бесконечный цикл графического интерфейса. В принципе, приведенный выше

код можно использовать в качестве универсального, изменяя только строку 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. class MyWin(QtWidgets.QMainWindow):

6. 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. def myFunction(self):

14. self.ui.label.setText("Длина Вашего текста %d"

% len(self.ui.lineEdit.text()))

15.

16. if __name__ == "__main__":

17. app = QtWidgets.QApplication(sys.argv)

18. myapp = MyWin()

Page 36: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

36

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 при

помощи командной строки. Вы познакомились с двумя способами построения

программ: в одном файле и разделяя графический интерфейс и функции

обработки данных.

Page 37: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

37

Глава 3. Приложение Guess Word и его вариации

Сделаем еще один шаг вперед и решим более сложную задачу. Для этого

необходимо сформулировать техническое задание, оно должно содержать

требования, обязательные для разработчика. Итак, программа (назовем ее Guess

Word) должна:

Иметь графический (оконный) интерфейс пользователя.

Получать от пользователя некоторое количество букв русского

алфавита.

Выводить в специальном окне русские слова длинной 4 или 5

символов, которые можно составить из введенных букв.

Использовать каждую из введенных букв в одном слове столько раз,

сколь она была введена.

Давать возможность пользователю выбирать параметр длины 4 или 5

(определяет, какие слова искать).

Использовать для поиска список слов русского языка из отдельного

файла.

Выводить количество проверенных комбинаций.

Выводить время исполнения в секундах.

Иметь индикатор хода процесса.

Перечисленные выше общие пункты помогут нам подойти к решению

задачи более конструктивно.

Для начала нужно построить графический интерфейс пользователя в Qt

Designer. Главным виджетом, как обычно, будет виджет QMainWindow.

Внутри него все остальные виджеты (его дети) будут скомпонованы по сетке

(Grid Layout). Компоновке нужно уделить повышенное внимание.

Возможно, Вы заметили, что в программе myinterface.py при увеличении

главного окна на весь экран виджеты пропорционально не меняли своих

размеров. Каким бы большим не было главное окно, строка ввода, кнопка и

надпись оставались фиксированного размера. Для учебной программы это не

Page 38: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

38

является недостатком, но в профессиональных приложениях такого быть не

должно. Если пользователь увеличивает главное окно, то закономерно, чтобы

увеличивалось и все внутри него. В Qt Designer сделать это несложно. Но для

начала нужно перетащить мышью в главное окно все используемые виджеты и

расположить относительно друг друга так, как нам нужно.

В программе Guess Word будут использованы следующие виджеты (см.

Рис. 3.1):

Рис. 3.1 Инспектор объектов интерфейса программы Guess Word

Новыми для нас являются многострочное текстовое поле

(QPlainTextEdit) и выпадающий список (QComboBox). Весь интерфейс

примет следующий вид (см. Рис. 3.2):

Рис. 3.2 Интерфейс guess.ui

Page 39: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

39

Виджеты располагаются так (сверху вниз и слева направо): QLabel,

QLineEdit, QComboBox, QPushButton, QPlainTextEdit.

На рисунках 3.1 и 3.2 все виджеты уже скомпонованы по сетке. И если Вы

сами строите интерфейс, то в инспекторе объектов (см. Рис. 3.1) нажмите на

объект MainWindow правой кнопкой мыши, в появившемся контекстном меню

выберите пункт Компоновка, затем Скомпоновать по сетке (см. Рис. 3.3):

Рис. 3.3 Установка компоновки по сетке

Page 40: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

40

Если виджеты немного сдвинулись, то подправьте их мышью. А теперь

самое главное: какой все это даст эффект? Выберите в меню Qt Designer Форма

–> Предпросмотр или нажмите Ctrl + R. Таким Ваш интерфейс увидит

пользователь. Попробуйте изменить размеры главного окна. Если Вы

правильно выполнили все инструкции, то все виджеты будут пропорционально

изменяться. Достаточно трудоемкая задача решена нами в один клик. Если Вам

вдруг захочется отказаться от компоновки, то для этого есть команда

Компоновка –> Удалить компоновщик.

Page 41: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

41

Вы можете поэкспериментировать с интерфейсом, открыв в 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')

Также в строку состояния часто помещают информацию и последних

действиях, совершенных программой (открыт/записан файл, ошибка ввода,

Page 42: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

42

процесс занял столько-то времени и мн. др.). Последней строкой в функции

__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. class MyWin(QtWidgets.QMainWindow):

6. 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. def start1(self):

18. pass

19.

20. if __name__ == "__main__":

21. app = QtWidgets.QApplication(sys.argv)

22. myapp = MyWin()

23. myapp.show()

24. sys.exit(app.exec_())

Page 43: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

43

Из функции start1() будут запускаться все остальные функции

программы. Поскольку поиск слов осуществляется по параметру, который

выбирает пользователь, для каждого из этих параметров стоит предусмотреть

отдельную функцию. Поэтому вставим в код еще две пустые функции:

wordFour() и wordFive(). Файл, из которого программа будет брать слова

для проверки, находится в архиве программ под названием dict.txt. В нем

собрано более 30000 слов русского языка, слова отделены друг от друга одним

пробелом. Чтение файла будет происходить внутри каждой из двух функций.

Алгоритм, используемый в wordFour() и wordFive(), будет

отличаться только в количестве вложенных циклов проверки, поэтому мы

разберем для примера только функцию wordFour(). Вот ее основные блоки:

1. Проводится замер текущего времени и помещение полученной

величины в переменную (для этого импортируется модуль time).

2. Происходит считывание содержимого файла dict.txt в список

(массив).

3. С помощью четырех вложенных циклов for по очереди перебираются

все возможные комбинации длинной 4 символа из введенных пользователем

букв.

4. Внутри последнего вложенного цикла очередная составленная

комбинация сравнивается с каждым словом из файла dict.txt. Если есть

совпадение, то комбинация помещается в список (массив) результата.

5. Также внутри последнего вложенного цикла производится счет

комбинаций (инкремент накапливающей переменной).

6. Еще раз происходит замер текущего времени, полученная величина

вычитается из первоначальной, разница дает время выполнения функции.

6. Производится вывод списка результата, количества проверенных

комбинаций и времени выполнения.

В качестве аргумента функция будет получать текст, введенный

пользователем.

Page 44: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

44

Внутри функции 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. class MyWin(QtWidgets.QMainWindow):

7. 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.

Page 45: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

45

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] =

Page 46: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

46

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. self.c += 1

57. 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. def wordFive(self):

61. pass

62.

63. def arrOutput(self, arr):

64. arr.sort()

65. self.str = ''

66. for i in range(0, len(arr)):

67. if i != len(arr) - 1:

68. self.str += arr[i] + ', '

69. else:

70. self.str += arr[i] + '.'

71. return self.str

72.

73. if __name__ == "__main__":

74. app = QtWidgets.QApplication(sys.argv)

Page 47: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

47

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)

У виджета пять параметров: надпись, надпись на кнопке, первоначальное

значение, конечное значение и родительский виджет). Конечное значение не

Page 48: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

48

обязательно должно равняться ровно ста. 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(), будет перебирать, как

и в оригинальной программе, все сочетания из четырех символов, но

Page 49: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

49

сравнивать только с теми словами из словаря, которые имеют длину равную

также четырем символам. При этом скорость выполнения программы

существенно увеличится.

(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». По истечении

трех секунд в строке состояния снова должна появиться информация о

Page 50: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

50

правообладателе. Для решения этой задачи рекомендуется воспользоваться

форумом Stack Overflow или документацией разработчика [QtProject-

QStatusBar].

***

В этой главе Вы построили достаточно сложное приложение, которое

включает чтение из файла и запись в файл. Вы использовали такие виджеты как

главное окно (QMainWindow), строка ввода (QLineEdit), кнопка

(QPushButton), надпись (QLabel), выпадающий список (QComboBox),

многострочное текстовое поле (QPlainTextEdir) и строка состояния

(QStatusBar). Вы научились базовым приемам использования индикатора

хода процесса (QProgressDialog), и это позволяет Вашим программам

выглядеть весьма достойно.

Page 51: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

51

Глава 4. Меню и диалоговые окна

В рассмотренных нами приложениях пока еще очень мало виджетов. В

крупных приложениях количество кнопок, вызывающих различные функции,

может достигать десятков и сотен. Располагать их на экране, внутри главного

окна непрактично. Для того, чтобы компактно разместить сколько угодно

управляющих кнопок, были придуманы панели меню. Самая привычная из них

располагается тонкой полосой вдоль верхней границы (на севере) интерфейса.

В меню можно определить группы и подгруппы, что делает тонкую полоску

практически бесконечным вместилищем, позволяющим избавиться от

большого числа рассеивающих внимание кнопок (например, см. Рис. 3.3).

Диалоговые окна уже частично знакомы Вам в лице виджета

QProgressDialog. Вы даже уже знаете, что диалоговые окна могут быть

модальными и немодальными. Диалоговые окна позволяют избавиться от

большого числа строк ввода и помогают выстроить некоторую линию

поведения программы. Действие, предусмотренное в модальном диалоговом

окне, должно быть выполнено пользователем обязательно, его невозможно

обойти стороной. Например, в задании 3.2 Вы добавили кнопку для сохранения

содержимого текстового поля. Но сохранение происходило всегда в файл,

название которого было задано кодом программы. В профессиональных

приложениях в подобном случае обычно появляется диалоговое окно выбора

файла, в котором можно выбрать название файла и место его расположения, а

можно и просто отменить действие.

Это все нам и предстоит попробовать реализовать. Что касается меню, то

здесь мы полностью можем положиться на Qt Designer, а диалоговые окна

нужно будет писать вручную, т.к. они все будут находиться внутри функции.

Тем не менее это не является проблемой, поскольку PyQt5 имеет очень удобные

заготовки на все случаи жизни (ну, или почти на все).

Для начала создадим в Qt Designer простой интерфейс, состоящий из

главного окна размером 320х240. Внутрь поместите многострочное текстовое

Page 52: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

52

поле и скомпонуйте содержимое главного окна по сетке, по вертикали или по

горизонтали. Значения это не имеет, т.к. в окне будет только одни виджет (см.

Рис. 4.1):

Рис. 4.1 Интерфейс с одним виджетом во всю величину главного окна

Все управление будет происходить из меню. Сделать его очень просто.

Дважды нажмите левой кнопкой мыши на надпись «Пишите здесь». Напишите

имя группы меню. Пусть она называется File. Нажмите Enter. Qt Designer

построит группу меню под названием File (см. Рис. 4.2):

Рис. 4.2 Создание группы меню

Теперь таким же образом, двойным нажатием мыши, создайте пункт меню

Save, добавьте разделитель, а под ним – пунк меню Exit (см. Рис. 4.3):

Page 53: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

53

Рис. 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. class Ui_MainWindow(object):

4. def setupUi(self, MainWindow):

5. MainWindow.setObjectName("MainWindow")

Page 54: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

54

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.

Page 55: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

55

5. class MyWin(QtWidgets.QMainWindow):

6. 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__() достаточно добавить строку:

Page 56: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

56

self.ui.actionExit.triggered.connect(self.close)

Теперь по функциональности выбор пункта меню Exit и нажатие на

красный крестик идентичны. Код программы находится в файле menumain1.py.

Обратите внимание, что нам не понадобилось даже создавать отдельную

функцию. Метод connect() в качестве аргумента может содержать

стандартную команду. Поскольку через self мы вызываем главное окно,

self.close означает на человеческом языке: «Закрой главное окно».

Пусть при нажатии на крестик и при выборе пункта меню Exit программа

выводит диалоговое окно для подтверждения выхода из программы. Самым

простым способом было бы деактивировать красный крестик, но в текущей

версии PyQt5 это сделать невозможно. При нажатии на крестик происходит

событие closeEvent. Можно перехватить это событие и при его наступлении

проигнорировать. Изучите функцию (см. Код 4.4):

Код 4.4 Функция закрытия окна

1. def closeEvent(self, e):

2. result = QtWidgets.QMessageBox.question(self,

"Confirm Dialog", "Really quit?", QtWidgets.QMessageBox.Yes |

QtWidgets.QMessageBox.No, QtWidgets.QMessageBox.No)

3. if result == QtWidgets.QMessageBox.Yes:

4. e.accept()

5. else:

6. e.ignore()

Здесь e – переменная события closeEvent. Методы accept() и

ignore() соответственно принимают событие или игнорируют его. При

наступлении события выводится диалоговое окно класса QMessageBox (его

вариант для вывода вопроса). У этого диалогового окна пять аргументов:

родительский виджет (self, т.е. главное окно); заголовок окна; текст внутри

окна; через знак | кнопки, которые будут у окна (в нашем окне кнопки Yes и

No); кнопка по умолчанию (No). Задача диалогового окна вопроса – отговорить

Page 57: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

57

пользователя совершать действие, дать ему время одуматься. Это диалоговое

окно является модальным по умолчанию.

Поместите функцию в класс MyWin(), ниже функции __init__().

Измененный файл получит имя menumain2.py. Запустите программу. Если все

сделано правильно, то при нажатии красного крестика Вы увидите это (см. Рис.

4.5):

Рис. 4.5 Диалоговое окно при закрытии программы

При нажатии No событие закрытия игнорируется, а диалоговое окно

закрывается. При нажатии Yes программа закрывается.

То же самое должно происходить и при выбора пункта меню Exit.

Казалось бы, нужно написать отдельную функцию, которая дублировала бы

содержание функции closeEvent(). Но секрет в том, что ничего делать не

надо. Выбирая пункт меню Exit Вы вызываете то же самое событие close. И

следовательно, также вызывается функция closeEvent().

Поэкспериментируйте с закрытием программы двумя способами. В обоих

случаях вид и поведение диалогового совершенно одинаково.

***

Проверьте и расширьте свое понимание (4.1): Как изменить код

программы menumain2.py, чтобы при нажатии на крестик не происходило бы

Page 58: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

58

ничего, а диалоговое окно вопроса выводилось бы только при выбора пункта

меню 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. После сохранения в строку состояния

Page 59: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

59

выводится соответствующая надпись, которая исчезает, как только

пользователь наводит мышь на меню 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. if self.fileName:

5. 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 объявляет переменную диалогового окна. В ней

хранится название файла, который выберет пользователь. Пока не обращайте

Page 60: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

60

внимания на запятую и подчеркивание. У диалогового окна пять аргументов:

родительский виджет; заголовок (Save To File); имя файла по умолчанию (у нас

ничего не выбрано); фильтр (окно будет показывать только файлы с

расширением .txt; опции (установлены по умолчанию). Выглядит диалоговое

окно следующим образом (см. Рис. 4.7):

Рис. 4.7 Интерфейс диалогового окна сохранения файла

Как Вы можете заметить, открывается стандартный файловый диалог

Windows. Если ввести имя файла, которое уже есть в папке, появится еще одно

диалоговое окно, требующее подтверждения перезаписи файла. Это делается

автоматически, никакого дополнительного кода писать не надо.

После нажатия кнопки «Сохранить» программа переходит к строке 4. Если

пользователь ввел какое-то имя файла, то происходит запись в файл, а в строку

состояния выводится соответствующая надпись. Если ничего не было введено,

то функция saveToFile() завершается.

Проведем еще одну модификацию. Пусть программа сохраняет

содержимое многострочного текстового поля не в текстовый файл, а как веб-

Page 61: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

61

страницу, т.е. в формате .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 и проведите соответствующие модификации. Не забудьте,

что файл интерфейса изменился на menu1.py. В функции

saveToFile()четвертый аргумент изменится на "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. class MyWin(QtWidgets.QMainWindow):

6. def __init__(self, parent=None):

7. QtWidgets.QWidget.__init__(self, parent)

8. self.ui = Ui_MainWindow()

9. self.ui.setupUi(self)

Page 62: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

62

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. if self.fileName:

18. 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. def closeEvent(self, e):

24. result = QtWidgets.QMessageBox.question(self,

"Confirm Dialog", "Really quit?", QtWidgets.QMessageBox.Yes

| QtWidgets.QMessageBox.No, QtWidgets.QMessageBox.No)

25. if result == QtWidgets.QMessageBox.Yes:

26. e.accept()

27. else:

28. e.ignore()

29.

30. if __name__ == "__main__":

31. app = QtWidgets.QApplication(sys.argv)

32. myapp = MyWin()

33. myapp.show()

34. sys.exit(app.exec_())

Page 63: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

63

Запустите программу. Напишите что-нибудь в текстовом поле, например

следующее (см. Рис. 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">

Page 64: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

64

<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; margin-

left:0px; margin-right:0px; -qt-block-indent:0; text-

indent:0px;">Это предложение.</p>

<p style=" margin-top:0px; margin-bottom:0px; margin-

left:0px; margin-right:0px; -qt-block-indent:0; text-

indent: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, при выборе которого выводилось бы

диалоговое окно открытия текстового файла, а содержимое открываемого

файла помещалось бы в текстовое поле для редактирования.

***

В этой главе Вы поработали с диалоговыми окнами подтверждения выхода

из программы, сохранения и открытия файла. То, что получилось в итоге,

Page 65: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

65

можно даже назвать очень простым текстовым редактором. Одной их

важнейших частей главы было сохранение содержимого текстового поля в виде

веб-страницы.

Page 66: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

66

Глава 5. Первые программы учебного назначения

В этой главе мы приступим к написанию программ для ЭВМ учебного

назначения. Для этого будут задействованы новые виджеты, позволяющие

разворачивать на малой площади интерфейса большое количество информации.

Поставим перед собой задачу создать карточки с небольшим количеством

теоретического материала, за которым сразу следует контрольный вопрос.

Вопросы будут выполнены в виде кнопок радио, т.е. допускать выбор одного

варианта из нескольких.

Интерфейс программы мы сделаем в Qt Designer, и состоять он будет из

большого количества виджетов, вложенных друг в друга. Инспектор объектов

Qt Designer примет вид (см. Рис. 5.1):

Рис. 5.1 Инспектор объектов интерфейса learn0.ui

Page 67: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

67

Обратите внимание на новые виджеты, которые используются в

интерфейсе и на их иерархическую структуру. Внутри главного окна все

виджеты компонуются по горизонтали. И так происходит со всеми

вложенными виджетами, имеющими компоновку. В главное окно помещается

QTabWidget – панель с вкладками. Всего в интерфейсе присутствуют три

вкладки, которые имеют абсолютно одинаковое наполнение. Поэтому

рассмотрим подробно только первую из них.

Внутрь вкладки помещен виджет QScrollArea. Его предназначение –

быть контейнером для какого-то числа других виджетов. Даже если в него

поместить сотни и тысячи виджетов, то все они будут доступны для

Page 68: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

68

пользователя через полосу прокрутки. Очень похожее происходит в веб-

браузере, когда мы открываем большую веб-страницу, и чтобы добраться до ее

конца, нам нужно прокрутить содержимое, в то время как на экране может быть

видна только его часть. Если не использовать QScrollArea, то полоса

прокрутки не появится, и доступны будут только те виджеты, которые

помещаются в видимую часть главного окна. До сих пор это не было для нас

проблемой. Но представьте, что Вам предстоит написать тест, состоящий из 30

вопросов. Уже в этом случае без контейнера с полосой прокрутки обойтись

будет крайне затруднительно.

Внутри контейнера QScrollArea находятся два виджета: поле ввода

QTextArea и еще один контейнер QFrame, в котором находятся четыре

виджета: одна надпись и три кнопки радио. Три кнопки радио объединены в

группу, что позволяет программе воспринимать их как единое целое. Смысл

группы в том, что внутри нее может быть одновременно нажата только одна

кнопка радио (раньше были такие радиоприемники, у которых было три

кнопки, и при нажатии очередной кнопки другая нажатая кнопка

автоматически отскакивала). Для того, чтобы объединить несколько кнопок

радио в группу, нужно выделить их и нажать правую кнопку мыши, в

появившемся контекстном меню выбрать «Назначить группу кнопок».

Интерфейс, соответствующий Рис. 5.1 находится в файле learn0.ui (и learn0.py).

Обратите внимание на большой объем и высокую сложность кода. Я думаю,

что достоинства работы в Qt Designer уже стали для Вас очевидными.

На первом этапе разработки можно сразу поместить в интерфейс тексты

(теорию и вопросы). Этот интерфейс будет называться learn1.py. Пусть это пока

будут примитивный текст и примитивные вопросы. Займемся заполнением

полей ввода. Если дважды кликнуть левой кнопкой мыши на виджете

QTextEdit, то появится окно редактора HTML, в котором не просто можно

набрать текст, но и произвести его форматирование, вставить картинку и

гиперссылку, а также перейти в режим просмотра кода HTML (см. Рис. 5.2):

Page 69: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

69

Рис. 5.2 Окно редактора содержимого виджета QTextEdit

Также заполним формулировку вопроса и надписи кнопок радио (см. Рис.

5.3):

Рис. 5.3 Заполненная первая вкладка интерфейса learn1.py

Page 70: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

70

Полученный интерфейс сохранен в файле learn1.py, главным файлом

является learnmain1.py. В нем еще нет ни одной функции, созданием которых

нам и предстоит сейчас заняться. Но перед этим нужно поставить перед собой

цель. Чтобы наш материал имел какую-то методическую ценность, он должен

быть правильно организован. Сильная сторона электронных учебных

материалов заключается в том, что они структурируют работу студента,

направляют его на путь, который разработал преподаватель. Наш интерфейс

состоит из трех карточек. Пользователь читает текстовую информацию, а для

того, чтобы запомнить ее лучше – выполняет задание. Разумно начинать с

первой карточки, затем переходить ко второй, а завершать третьей. При работе

с бумажной книгой можно читать все что угодно, начиная с чего угодно. Мы

сделаем по-другому: нельзя будет переходить к последующей карточке, если не

сделано задание предыдущей. Сделанным оно будет считаться только в случае

правильного ответа на задание. Само собой разумеется, что наша программа –

Page 71: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

71

очень простая, но принцип организации материала в ней можно считать

типовым, т.е. переносимым на сколь угодно сложные учебные материалы.

В программе 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

Page 72: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

72

2. from learn1 import *

3. from PyQt5 import QtCore, QtGui, QtWidgets

4.

5. class MyWin(QtWidgets.QMainWindow):

6. 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.

Page 73: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

73

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). В исполняющей программе

изменяться функции, а также добавятся некоторые переменные.

Последовательность действий программы изложена в шести пунктах:

Page 74: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

74

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, "любая

Page 75: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

75

формальная знаковая система"]

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.

Page 76: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

76

self.ui.buttonGroup_2.buttonClicked.connect(self.correctAns

2)

42.

self.ui.buttonGroup_3.buttonClicked.connect(self.correctAns

3)

43.

44. def correctAns1(self):

45. for rb in self.ui.buttonGroup.buttons():

46. 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. def correctAns2(self):

53. for rb in self.ui.buttonGroup_2.buttons():

54. 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. def correctAns3(self):

61. for rb in self.ui.buttonGroup_3.buttons():

62. 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)

Page 77: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

77

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, которыми оперирует Qt Designer,

представляют собой не что иное как базы данных XML.

Page 78: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

78

В 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

смыслоразличительную роль играет количество отступов от

левого края">Особенностью синтаксиса языка программирования

Page 79: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

79

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

Page 80: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

80

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.

Page 81: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

81

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. def correctAns1(self):

61. for rb in self.ui.buttonGroup.buttons():

Page 82: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

82

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. def correctAns2(self):

69. for rb in self.ui.buttonGroup_2.buttons():

70. 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. def correctAns3(self):

77. for rb in self.ui.buttonGroup_3.buttons():

78. 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. Строки 9-

12 создают четыре списка для хранения текстов, вопросов, вариантов ответа и

правильного варианта. Эти списки заполнятся информацией из файла

learn.xml. Строка 19 создает переменную объектной модели документа

Page 83: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

83

(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?

***

В этой главе Вы поработали с новыми виджетами (кнопки радио),

группами кнопок, а также применили алгоритм перемешивания, используя базу

Page 84: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

84

данных на основе XML. Теперь можно переходить к более сложным

программам для ЭВМ учебного назначения.

Page 85: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

85

Глава 6. Программные тренажеры – основы

Программные тренажеры являются важной частью самостоятельной

работы студентов. Они помогают организовать учебный материал, позволяют

чувствовать прогрессию в изучении дисциплины, а это значит – усилить

мотивацию к дальнейшему обучению. (Программными они называются потому,

что представляют собой программу для ЭВМ).

Мы напишем программный тренажер, улучшив программы предыдущей

главы. Задача будет состоять в том, чтобы разработать графическую оболочку,

которая бы заполнялась материалом из базы данных и удовлетворяла бы

следующим требованиям:

1. Все учебное содержание должно загружаться из файла XML.

2. Интерфейс должен иметь поле, с полосой прокрутки, в котором будут

располагаться вопросы с вариантами ответов в виде кнопок радио.

3. Вопросов может быть сколько угодно, вариантов ответов для каждого

вопроса может быть произвольное количество (не одинаковое для всех

вопросов).

4. Всякий раз при загрузке программа должна перемешивать не только

вопросы, но и варианты ответов в них, чтобы у пользователя не происходило

запоминания расположения правильных ответов.

5. Интерфейс должен иметь кнопку, запускающую процесс проверки,

результатом которого должен быть процент правильного выполнения всех

заданий. Результат должен высчитываться точно, не зависимо от количества

вопросов в тренажере.

Для решения задачи нужно разработать структуру файла XML, интерфейс

тренажера и исполняющую программу. Интерфейс будет минималистичен (т.к.

все заполнение материалом будет происходить в исполняющем файле),

включая несколько виджетов (см. Рис. 6.1):

Рис. 6.1 Инспектор объектов интерфейса shell.ui

Page 86: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

86

Внешне он также будет прост (см. Рис. 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>

Page 87: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

87

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">Сколько будет 12-

6?</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 – правильный ответ. Обратите

внимание на то, что вариантов ответов не одинаковое количество, и с этой

проблемой надо будет справиться.

Самой сложной будет основная программа, потому что он должна

построить абстрактный интерфейс (такой, которого еще нет), причем с каждым

его элементом надо будет впоследствии работать для проверки результата.

Здесь нам помогут списки (массивы). Мы не знаем, сколько надо создать

переменных для формулировок вопросов и кнопок радио, поэтому не можем

просто заранее создать пустые виджеты, как в предыдущей главе, а потом

просто заполнить их. Виджетов может быть сколь угодно много, поэтому без

списков, которые будут состоять из объектов, решить проблему нельзя.

Page 88: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

88

В 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)

Page 89: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

89

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="utf-

8"?>\n<content>\n''')

46. for line in self.linesMixed:

47. 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. for line in range(0, len(self.linesArr)):

57. # label's text

58.

Page 90: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

90

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. for v in range(0, len(self.variants[line])):

78. self.rbs[line][v] =

QtWidgets.QRadioButton(self.ui.scrollAreaWidgetContents)

79.

Page 91: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

91

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; font-

weight: 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 является двухмерным, и его пришлось заполнить

Page 92: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

92

пустыми строками, потому что оказалось, что вложить кнопку в кнопку не

получается.

Код функции __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. Внутри заполняются списки виджетов, и эти виджеты

выводятся на экран. Тут важно еще раз напомнить, что вывод должен

Page 93: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

93

производиться в том порядке, каком пользователь должен видеть объекты.

Строки 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). Значение

Page 94: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

94

округляется до двух знаков после запятой. Результат выводится в строку

состояния с присвоением стиля (строки 92-93).

В результате тренажер выглядит следующим образом (см. Рис. 6.3):

Рис. 6.3 Тренажер после запуска программы shellmain.py

Все вопросы построились правильно, варианты ответов перемешались,

кнопка проверки работает.

***

Проверьте и расширьте свое понимание (6.1): В программе shellmain.py

результат выводится всегда синим цветом. Для того, чтобы создать

дополнительный показатель прогресса, сделайте так, чтобы цвет строки

состояния зависел от количества набранных баллов. Используйте схему

светофора: 0-50%: красный цвет, 51-75%: желтый цвет, 76-100%: зеленый цвет.

Page 95: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

95

(6.2): В строке состояния при изменении цвета вся строка закрашивается

одинаково. Как сделать так, чтобы цветом выделялось только число?

(6.3): Для хранения кнопок радио был объявлен двухмерный список 15 на

10. Но если пользователь захочет сделать тренажер с 16-ю и более вопросами,

то программа выдаст ошибку. Как гибко настроить размер списка, чтобы он

всегда соответствовал параметрам базы данных XML?

(6.4): Тренажер может включать любое количество заданий, и это

количество может быть довольно большим. Пользователь может делать задания

не по порядку, пропуская сложные вопросы. В этом случае ему нужно будет

возвращаться к нерешенным заданиям. Если заданий несколько десятков, то

может оказаться так, что при нажатии кнопки проверки некоторые задания

останутся нетронутыми только потому, что пользователь забыл к ним

вернуться. Чтобы облегчить человеку задачу сделайте индикатор, который бы

показывал, что в тренажере еще остались задания, в которых не отмечен еще ни

один вариант ответов. Для этого подойдет виджет QProgressBar, который

можно добавить в интерфейс, а потом работать с ним из основного файла.

Индикатор хода процесса должен показывать процент заданий с отмеченными

вариантами. Заметьте, индикатор показывает не процент правильно решенных

заданий, а процент хоть как-то решенных заданий. Например, если у Вас шесть

вопросов, и пользователь как-то ответил на три из них (не обязательно

правильно, это пока никак не проверяется), а к остальным трем вообще не

приступал, то должно отобразиться следующее (см. Рис. 6.4):

Рис. 6.4 Тренажер с индикатором, показывающим 50% решенных заданий

Page 96: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

96

На рисунке задания выполнены неверно, но индикатор показывает 50%,

т.к. отмеченные кнопки есть в половине из всех вопросов.

***

Важным показателем при проверке работы студента является время

выполнения заданий. Пока рассматриваемый тренажер никак его не

ограничивает, но мы сейчас это исправим.

Данные о времени программа будет получать из базы данных, поэтому

нужно ввести еще один дополнительный тег time, содержание которого будет

означать время на выполнение задания в секундах (см. Код 6.3):

Код 6.3 Файл db1.xml

1. <?xml version="1.0" encoding="utf-8"?>

2. <content>

Page 97: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

97

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">Сколько будет 12-

6?</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(). Возможно, понимание того, как он работает, придет не

сразу, но к этому нужно стремиться. Важность многопоточности ни в коем

случае нельзя недооценивать, потому что такие вещи как, например, таймер,

Page 98: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

98

очень важны для электронных учебных материалов. В любом случае, можно

использовать приведенный ниже код как шаблон и стараться в нем со временем

разобраться (см. Код 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 =

Page 99: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

99

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="utf-

8"?>\n<content>\n''')

53. for line in self.linesMixed:

54. 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 =

Page 100: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

100

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])

Page 101: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

101

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; font-

weight: bold;')

101. self.ui.statusbar.showMessage(message)

102.

103. def updater(self, val):

104. 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))

Page 102: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

102

109.

110. def start1(self):

111. self.mythread1.start()

112.

113. # Additional function, if you need to terminate the

thread

114. def stop1(self):

115. 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. str1 += "0%d:" % m

130. else:

131. str1 += "%d:" % m

132. if s < 10:

133. str1 += "0%d" % s

134. else:

135. str1 += "%d" % s

136. 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):

Page 103: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

103

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() предназначена для

Page 104: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

104

немедленной остановки второго потока, в программе она не используется, но

приведена для полноты картины. Функция 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.

Page 105: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

105

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. def mixXml(self):

39. # read xml and mix the lines

Page 106: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

106

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="utf-

8"?>\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. for line in range(0, len(self.linesArr)):

64. # 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'

Page 107: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

107

).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. for v in range(0, len(self.variants[line])):

85. 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.

Page 108: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

108

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; font-

weight: 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:

Page 109: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

109

119. m = num // 60

120. num = num % 60

121. s = num

122. str1 = "%d." % h

123. if m < 10:

124. str1 += "0%d:" % m

125. else:

126. str1 += "%d:" % m

127. if s < 10:

128. str1 += "0%d" % s

129. else:

130. str1 += "%d" % s

131. 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 уменьшается на единицу.

Итак, у Вас есть два варианта того, как сделать таймер обратного отсчета.

Вы можете выбрать любой из них, когда будете писать свои программы.

***

Page 110: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

110

В этой главе Вы сделали по-настоящему универсальный тренажер,

который является только графической оболочкой и получает всю информацию

из файла XML. Ваш тренажер имеет таймер обратного отсчета, перемешивает

вопросы и задания в каждом вопросе.

Теперь можно переходить к проблеме протоколирования.

Page 111: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

111

Глава 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")

Page 112: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

112

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

Page 113: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

113

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()

Page 114: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

114

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="utf-

8"?>\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)

Page 115: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

115

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

Page 116: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

116

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-

Page 117: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

117

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. str1 += "0%d:" % m

141. else:

142. str1 += "%d:" % m

143. if s < 10:

144. str1 += "0%d" % s

145. else:

146. str1 += "%d" % s

Page 118: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

118

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 в накапливающую

переменную записываются по очереди правильный ответ на вопрос (строка

Page 119: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

119

107) и тот ответ, который дал пользователь (строка 108). При следующем

проходе цикла строки 96 переменная correctThis обнулится, чтобы

получить очередное значение. Если пользователь не приступал к выполнению

задания, т.е. ни одна из кнопок радио в текущей группе не отмечена, то

correctThis запишет в накапливающую переменную logStr значение по

умолчанию (два дефиса). В строке 159 переменная logStr обнуляется.

Теперь протокол получил развернутый вид (см. Рис. 7.2):

Рис. 7.2 Фрагмент файла протокола программы logmain4.py

Теперь виден не только общий балл, но и то, какие ответы были даны на

вопросы. Причем сразу показан правильный ответ, чтобы было видно,

правильный ли дан ответ. Конечно, можно было бы просто писать после

каждого вопроса «правильно» или «неправильно», но когда мы видим, как

Page 120: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

120

отвечал пользователь, мы можем лучше понять, как он думал, в чем состоят

ошибки. А отсюда уже будет легче помочь студенту их исправить.

Представьте, что два студента выполняют одно и то же задание, но один

работает в нашей программе, а другой с листом бумаги. В чем будут состоять

отличия? Во-первых, работа в программе гарантирует абсолютно точный

контроль времени выполнения. Таймер просто выключит интерфейс после

определенного времени. При работе с бумагой такая точность невозможна. Во-

вторых, результаты первого студента формируются автоматически. Их можно

распечатать, их легко хранить на электронном носителе. Проверка всегда имеет

стопроцентную точность, что сложно достижимо при ручной проверке, когда

проверяющий устает, отвлекается и т.д. Кроме того, компьютер абсолютно

объективен и никогда не увеличит и не уменьшит балл по своему усмотрению.

А теперь представьте, что работают две группы по 20 человек. Конечно,

тут уже необходим целый компьютерный класс, но если он есть, то мы точно

уверены, что всем студентам будет дано абсолютно одинаковое время

выполнения и у каждого вопросы и варианты ответов будут перемешены в

своем порядке. Заготовить вручную перемешенные варианты для каждого

очень затратно по времени. Не стоит и говорить о количестве потраченной

бумаги, которая тут же превратится в макулатуру.

Тем не менее, будем честны. При текущем виде протокола мы однозначно

выигрываем во времени (и существенно в качестве), но все-таки не делаем

ничего такого, чего можно добиться традиционной работой с бумагой. Мы

внесем в протокол еще один параметр, учесть который никак невозможно при

работе с бумажными заданиями.

Этим параметром будет количество предпринятых действий или

количество нажатий на кнопки радио, и именно он не может быть вычислен при

выполнении работы на бумаге. Представьте разные ситуации. Два студента

получили равный балл за одну и ту же работу. С точки зрения традиционных

измерений они владеют материалом в равной степени. Однако первый решил

Page 121: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

121

все задания с первого раза (говоря техническим языком, выполнил столько же

включений кнопок, сколько в тренажере имеется вопросов), а другой менял

много раз свои ответы (произвел над интерфейсом больше действий, чем

имеется вопросов в тренажере). Мне видятся несколько оснований того, почему

действий было ровно столько, сколько и вопросов, или больше. Первый студент

твердо знал ответы и не сомневался в своем выборе. Если он к тому же

выполнил задания за очень краткий срок, то или он очень хорошо владеет

материалом, или знал ответы на вопросы заранее. Будем оптимистами и

примем первый вариант. Второй студент сомневался и переменял свои

решения; возможно, он где-то даже угадал. Но также нельзя исключать того,

что сыграла роль неуверенность в себе, что является чертой характера, а не

показателем знаний.

Как бы то ни было, можно с уверенностью утверждать, что параметр

количества изменений дает богатый материал для анализа и что получить этот

материал можно только используя электронные учебные материалы. Подобный

тренажер сохранен под именем logmain5.py. Для подсчета действий создается

накапливающая переменная triggeredNum (строка 19). В функцию

addWidgetsToInterface() добавляется строка (номер 89):

self.bgrs[line].buttonClicked.connect(self.increaseNum)

В классе MyWin() создается функция increaseNum():

def increaseNum(self):

self.triggeredNum += 1

Накапливающая переменная обнуляется в конце функции log() (строка

165). Также в этой функции производится соответствующий вывод в протокол

(строка 163). В итоге путем небольшой модификации мы получили

дополнительный инструмент оценки.

Продолжим следовать по пути усовершенствования тренажера. Задания, к

какой бы дисциплине они не относились, не могут быть все одинаковой

сложности. Какие-то всегда сложнее, а какие-то легче. Поэтому мы внесем в

Page 122: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

122

задания коэффициент сложности. В файл базы данных добавится атрибут 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>Сколько будет 12-

6?</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

Page 123: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

123

<?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, в который помещается картинка под именем

Page 124: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

124

picName (строка 101). Если значение равно 'empty', то создается просто

пустой виджет QLabel, чтобы не нарушать целостность списка lblPic.

Картинка выводится между формулировкой вопроса и вариантами ответа

(кнопками радио), в масштабе 100%, поэтому перед размещением картинки

придавайте ей подходящий размер. Для масштабирования графических файлов

можно воспользоваться редактором Paint, который входит в стандартный набор

программ ОС Windows (см. Рис. 7.3):

Рис. 7.3 Масштабирование картинки в редакторе Paint

Проверьте и расширьте свое понимание (7.3): Придайте тренажеру

больше персональности. Пусть во время запуска программы, до начала отсчета

таймера, на экран будет выведено диалоговое меню, которое запросит у

пользователя имя и номер учебной группы, например (см. Рис. 7.4):

Рис. 7.4 Диалоговое окно с двумя строками ввода

Page 125: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

125

Если пользователь не введет ничего хотя бы в одну строку и при этом

попытается закрыть окно кнопками или крестиками, то программа не должна

пускать его к тренажеру, а выводить диалоговое окно снова и снова. Введенные

имя и группа должны отразиться в протоколе.

(7.4) Наш тренажер пока разрешает многочисленное нажатие кнопки

проверки в течении одного сеанса работы. Сделайте так, чтобы при нажатии

кнопки Check производить действия с интерфейсом уже было бы нельзя.

Модифицируйте версию logmain7.py.

Теперь мы сделаем следующий шаг и добавим в тренажер еще один очень

важный виджет: флажок (QCheckBox).

Кнопки радио позволяли делать задания на множественный выбор, где был

возможен только один вариант ответа. Такие задания имеют невысокую

степень сложности, т.к. всегда существует высокий шанс угадать ответ.

Намного сложнее справиться с заданиями, в которых возможны различные

комбинации ответов. Рассмотрим два варианта. В первом из трех вариантов

нужно выбрать только один правильный ответ (и пользователь это знает). Шанс

угадать при этом 1 к 3. Теперь заменим кнопки радио флажками (см. Рис. 7.5):

Рис. 7.5 Вопрос с тремя флажками

Page 126: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

126

При таком задании известно заранее, что правильная комбинация может

быть какой угодно. Например, как на Рис. 7.5, когда ни один ответ не отмечен,

но это является правильным вариантом. Также все три варианта могут быть

правильными (см. Рис. 7.6):

Рис. 7.6 Вопрос с тремя отмеченными флажками

А возможны и другие комбинации, общее количество которых равняется

уже восьми, а не трем. Если увеличить количество флажков, то возрастет и

количество комбинаций. В этом случае угадать правильный вариант

практически невозможно, а значит задание станет интереснее и ответственнее.

Этим и замечательный флажки. Конечно и преподаватель должен будет

проявить больше фантазии при составлении таких заданий.

Теперь создадим тренажер, который будет сочетать задания с кнопками

радио и флажками, производить перемешивание, позволять присваивать

заданиям коэффициент сложности и вести подробный протокол.

В файл базы данных нужно ввести дополнительный атрибут тега q,

который будет говорить программе, какой тип виджета нужно использовать для

данного вопроса. Изменится и атрибут правильного ответа. Вообще,

существуют различные точки зрения на то, что считать правильным ответом на

задание с флажками. Допустим, что из трех вариантов правильными являются

два первых. Пользователь отметил первый вариант, а второй и третий не

отметил. Получается, что вроде бы половина ответа сделана правильно, т.е.

можно поставить полбалла. Но лучше я приведу пример (см. 7.7):

Рис. 7.7 Вопрос с одним отмеченным флажком

Page 127: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

127

Можно ли вообще поставить что-нибудь за такой ответ? Выполняя таким

образом тренажер, студент получит 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% за

Page 128: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

128

задание будет стараться понять ошибку и пытаться ее исправить. А в этом и

есть предназначение тренажеров.

Этот подход к оценке обязательно должен быть объяснен студенту в

инструкции к тренажеру, чтобы он понимал, как нужно работать (чтобы не

было раздражения от получения нуля баллов при частично правильно

решенном задании). Зато последующее превращение 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**?**60-

6**?**22,5*3" cor = "none" pnt = '3'>Какое выражение в итоге

равно 55?</q>

Page 129: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

129

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. def __init__(self, parent=None):

26. QtWidgets.QWidget.__init__(self, parent)

Page 130: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

130

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()

Page 131: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

131

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="utf-

8"?>\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. for line in range(0, len(self.linesArr)):

77. # 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.

Page 132: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

132

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))

Page 133: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

133

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])

Page 134: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

134

128.

self.verticalLayout.addWidget(self.rbs[line][v])

129.

130. def increaseNum(self):

131. 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. if len(chbCor) == 0:

158. chbCor.append('none')

159. chbCor.sort()

160. self.correct[group].sort()

Page 135: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

135

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; font-

weight: 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:

Page 136: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

136

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. str1 += "0%d:" % m

199. else:

200. str1 += "%d:" % m

201. if s < 10:

202. str1 += "0%d" % s

203. else:

204. str1 += "%d" % s

205. 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__":

Page 137: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

137

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

секунд несколько раз. Вы увидите, что он стал сложнее, и это сделало его

полезнее.

***

Page 138: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

138

Проверьте и расширьте свое понимание (7.5): Когда мы оперировали

только кнопками радио, показатель количества действий можно было

соотносить с количеством вопросов. Например, если в тренажере 10 заданий и

количество предпринятых действий равняется тоже 10, то можно заключить,

что задание выполнено в минимальное количество интеракций. С введением

флажков становится непонятно минимальное количество возможных действий

для достижения 100%. Каждое задание имеет свое количество правильных

ответов, поэтому при тех же 10 заданиях минимальное количество действий

может быть и 10, и 15, и 40 и т.д. Добавьте в протокол параметр «Коэффициент

интеракций», который будет отображать процентное отношение предпринятых

действий к минимальному количеству действий, которые нужно совершить для

достижения 100%.

(7.6): При формировании протокола данные о правильном ответе и ответе

пользователя для заданий с флажками выводятся как списки (в кавычках и

квадратных скобках). Это абсолютно не мешает их восприятию, но все же

измените вывод протокола, чтобы эти данные выводились без кавычек и

скобок, через точку с запятой. Для полноты картины пусть в протоколе для

каждого задания отражается и его коэффициент сложности.

***

В этой главе Вы написали мощный и разносторонний тренажер, который

может включать в себя картинку и два типа заданий – с кнопками радио и

флажками. Задания получили коэффициент сложности. Теперь Ваш тренажер

формирует подробный протокол, включая важный параметр количества

совершенных действий.

Page 139: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

139

Глава 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.

Это сделано для того, чтобы анализирующей программе не пришлось

брать эту информацию из файла. Работа только с заголовками ускорит процесс

обработки данных.

Теперь можно приступать к созданию анализирующей программы. Она

должна открывать папку с файлами протоколов, которые относятся к одному и

тому же тренажеру. Создать такую папку должен человек, это очень несложно.

Из заголовков файлов программа должна извлечь всю информацию и

представить ее в виде таблицы. Также автоматически должны быть рассчитаны

и показаны максимальный балл, минимальный балл, средний балл, среднее

время выполнения и среднее количество интеракций. Таблица должна иметь

возможность сортировки по каждому из пяти параметров.

Page 140: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

140

В 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):

Page 141: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

141

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. def openFunction(self):

26. options = QtWidgets.QFileDialog.DontResolveSymlinks

| QtWidgets.QFileDialog.ShowDirsOnly

27. directory =

QtWidgets.QFileDialog.getExistingDirectory(self,

28. "Choose Folder with Logfiles",

29. "some text", options=options)

30. if directory:

31. # 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])

Page 142: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

142

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' %

Page 143: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

143

(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. В принципе, от этой проверки

Page 144: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

144

толку мало, но для тренировки она полезна. В список dataFromName

помещаются данные из имени файла. Это пять переменных типа String. В

строке 42 создаются ячейки для текущего ряда таблицы. В них помещается

текст, но по-разному. Мы поставили перед собой задачу сортировки данных в

таблице. Для того, чтобы сортировать правильно, программа должна знать тип

данных, которые находятся в одном столбце. В первых двух столбцах это

строковые данные (String), в остальных трех – данные типа float. Эти

данные помещаются в ячейки таблицы разными методами (строки 44 и 46). В

строке 47 ячейки выводятся в интерфейс. Теперь таблица видна пользователю.

В строках 48-50 заполняются списки, объявленные в самом начале класса.

Заметьте, что переменная dataFromName обнуляется каждое прохождение

внешнего цикла for. В строках 51-56 высчитываются и помещаются в надписи

минимальные, максимальные и средние значения.

Для проверки работы программы можно воспользоваться заранее

заготовленными протоколами для воображаемых двух групп студентов (папка

results). Если после запуска программы выбрать пункт меню File -> Open и

в открывшемся диалоговом окне выбрать папку results, то результат будет

таким (см. Рис. 8.2):

Рис. 8.2 Результат работы программы analyzermain.py

Page 145: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

145

Замечательно то, что таблица автоматически создаст полосы прокрутки,

если ячейки не будет помещаться в экран. Ряды автоматически нумеруются.

Под таблицей выведена статистическая информация. Столбцы таблицы можно

сортировать от меньшего к большему или от большего к меньшему нажатием

левой кнопки мыши на нужный заголовок, например, на результат (см. Рис.

8.3):

Рис. 8.3 Сортировка результата по возрастанию

Page 146: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

146

Теперь можно двигаться дальше. У нас есть очень хорошая таблица со

статистикой. Но на практике бывает полезно сохранить и распечатать результат

в различных вариантах сортировки. Надо написать функцию сохранения

данных. Сохранение будет происходить в файл. Осталось выбрать формат.

Самое простое решение – это текстовый файл, но читать такой документ

неудобно, в нем нельзя сделать таблицу, выделение полужирным или цветом.

Мы остановимся на формате 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()

Page 147: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

147

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 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>\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()

Page 148: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

148

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

Page 149: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

149

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, открытого браузером

Page 150: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

150

Такой вывод выглядит очень профессионально и современно.

Дополнительно в программу добавлена активация и деактивация пункта меню

Save. Обратите внимание на то, что сразу после запуска программы пункт меню

Save является неактивным. И это понятно, ведь сохранять еще нечего.

Активация происходит только в конце функции openFunction(). Все это

сделано для удобства пользователя, чтобы он понимал, какие действия

являются доступными в текущий момент времени. Можно сказать, что с

помощью этого интерфейс программы стал «более дружеским» (more user

friendly).

***

Проверьте и расширьте свое понимание (8.1): Это задание на

повторение материала. У изученного нами анализатора остался неактивным

пункт меню Exit. модифицируйте программу таким образом, чтобы при

нажатии на красный крестик ничего не происходило, а при выборе пункта меню

Exit выводилось бы диалоговое окно, спрашивающее подтверждение выхода из

программы.

Page 151: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

151

***

Будем двигаться дальше. Допустим, что несколько человек работали над

одним и тем же тренажером некоторое время. С помощью нашей программы

мы можем получить данные о среднем балле, времени и т.д. Но что если мы

захотим узнать, какие вопросы были самыми сложными, а какие самыми

легкими? Этот параметр чрезвычайно важен для педагога и автора учебных

материалов, т.к. помогает понять, что именно вызывает трудности. К тому же

интересно проверить, правильно ли присвоены коэффициенты сложности. Пока

наша программа не дает такого ответа, и это нужно исправить.

Для анализа такого рода мы еще расширим файл протокола, чтобы в него

добавились коэффициент сложности для каждого вопроса и строка со

значением TRUE или FALSE. Протокол получится таким (см. Рис. 8.5):

Рис. 8.5 Фрагмент протокола с новыми данными

Файл тренажера, который составляет протокол указанного образца

сохранен под именем logmain14.py. Он изменился не очень сильно, но все же

изменился. Обратите внимание на функции check() и log(). Строка 264

делает файл протокола доступным только для чтения. Это важно, поскольку

анализатор будет читать файл целиком, и если его структура будет случайно

изменена, то программа выдаст ошибку. В строках 192-195 в протокол

Page 152: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

152

добавляется значение 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.

Page 153: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

153

self.ui.actionQuestions_Statistics.triggered.connect(self.qu

estFunction)

23.

24. def closeProg(self):

25. 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. e.accept()

33. else:

34. e.ignore()

35.

36. def questFunction(self):

37. options = QtWidgets.QFileDialog.DontResolveSymlinks

| QtWidgets.QFileDialog.ShowDirsOnly

38. directory =

QtWidgets.QFileDialog.getExistingDirectory(self,

39. "Choose Folder with Logfiles",

40. "some text", options=options)

41. if directory:

42. 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')

Page 154: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

154

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)

Page 155: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

155

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,

Page 156: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

156

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=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>\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())

Page 157: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

157

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. if directory:

148. # set row quantity

Page 158: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

158

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('Интеракции')

Page 159: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

159

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)))

Page 160: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

160

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 – еще один пустой список для показателя сложности

Page 161: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

161

вопроса. Индексы списков будут синхронизированы, например, в 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. Именно поэтому накопление

Page 162: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

162

правильных ответов происходит корректно. Т.е. для 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 Интерфейс с таблицей анализа вопросов

Page 163: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

163

На приведенном рисунке вопросы отсортированы от самого сложного к

самому легкому. Таблица изменилась так, как мы и ожидали. Безусловно,

пользователь захочет сохранить этот результат в файл, но функция Save

сработает некорректно, т.к. она настроена на сохранение конкретной таблицы, а

не любой таблицы, которая находится в текущий момент на экране. Надо

произвести изменения. Вернемся назад к коду программы.

Теперь функция saveFunction() считает количество колонок в

текущей таблице и только после этого записывает их заголовки (строки 124-

125). Далее происходит вывод исходя из текущего количества рядов (строка

127) и колонок (строка 130). Теперь функция стала универсальной, она будет

сохранять то, что есть в интерфейсе, какая бы таблица там не была.

Функция openFunction() также модифицирована. Перед выводом

ячеек в таблицу она устанавливает количество строк и колонок, создает и

называет заголовки (строки 149-171).

Page 164: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

164

Попробуйте сохранить данные статистики вопросов в файл HTML. Он

будет выглядеть следующим образом (см. Рис. 8.7):

Рис. 8.7 Файл HTML с сохраненным анализом вопросов

Таблица построена верно.

***

Проверьте и расширьте свое понимание (8.2): При вызове функции

questFunction() программа стирает все статистические данные под

таблицей, кроме названия тренажера. Исправьте это. Пусть при вызове любой

функции программа правильно выводит в интерфейс название тренажера,

данные которого обрабатывает в настоящее время.

***

В этой главе Вы сделали очень важный и большой шаг вперед. Вы

написали программу, анализирующую двумя способами протоколы

тренажеров. Результаты анализа представлены в виде таблицы, их можно

сортировать и сохранять в файл HTML. Очень важен анализ вопросов, который

очень быстро позволяет понять, что вызывает сложности у студентов при

Page 165: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

165

прохождении тренажера. Можно сказать, что в Вашем распоряжении целый

программный комплекс, с которым уже можно полноценно работать.

Page 166: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

166

Глава 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):

Page 167: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

167

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

Page 168: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

168

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

Page 169: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

169

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.

Page 170: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

170

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)

Page 171: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

171

126. url =

QtCore.QUrl.fromLocalFile(fileInfo.absoluteFilePath())

127. self.playlist.addMedia(QMediaContent(url))

128. self.pushButton.setEnabled(True)

129.

130. def play1(self):

131. if self.playerState in (QMediaPlayer.StoppedState,

QMediaPlayer.PausedState):

132. self.player.play()

133. elif self.playerState == QMediaPlayer.PlayingState:

134. self.player.pause()

135.

136. def stop1(self):

137. 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);

Page 172: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

172

155. format1 = 'hh:mm:ss' if duration > 3600 else

'mm:ss'

156. tStr = currentTime.toString(format1) + " / " +

totalTime.toString(format1)

157. else:

158. tStr = ''

159. self.labelDuration.setText(tStr)

160.

161. def setState(self,state):

162. 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. def changePosition(self, seconds):

178. if self.playerState == QMediaPlayer.PausedState:

179. pass

180. elif self.playerState == QMediaPlayer.PlayingState:

Page 173: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

173

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="utf-

8"?>\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

Page 174: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

174

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'))

Page 175: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

175

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]))

Page 176: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

176

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. def increaseNum(self):

276. self.triggeredNum += 1

277.

Page 177: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

177

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. elif self.tp[group] == 'chb':

296. if self.correct[group][0] != 'none':

297. 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. if len(chbCor) == 0:

307. chbCor.append('none')

308. chbCor.sort()

309. self.correct[group].sort()

310. if self.correct[group] == chbCor:

Page 178: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

178

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; font-

weight: bold;')

325. self.ui.statusbar.showMessage(message)

326. # creating log file

327. self.log()

328.

329. def listToString(self, getlist):

330. if type(getlist).__name__ == 'list':

331. string = ''

332. for i in getlist:

333. string += i + '; '

334. string = string[:-2] + '.'

335. return string

336. else:

337. return getlist

338.

Page 179: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

179

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. str1 += "0%d:" % m

362. else:

363. str1 += "%d:" % m

364. if s < 10:

365. str1 += "0%d" % s

366. else:

367. str1 += "%d" % s

368. return str1 # returns time as a string

369.

370. def log(self):

371. logFileName = self.name1 + '_' + self.group1 +

'_%.2f' % self.result + '_' + str(self.timeConstant -

Page 180: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

180

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()

Page 181: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

181

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(), updateDurationInfo(), setState() и

changePosition(). Эти функции регулируют отношения между

воспроизводимым аудиопотоком, кнопками и слайдером.

Функция open1() открывает нужный аудиофайл и помещает его в список

воспроизведения. Функция play1() вызывается при нажатии кнопки play.

Если статус плейера имеет значение остановки или паузы, то плейер начинает

играть, если плейер играл, то он устанавливается на паузу. Функция stop1()

останавливает плейер. Функция positionChanged1() двигает слайдер во

время воспроизведения. Функция durationChanged1() связана с сигналом

плейера durationChanged. Функция updateDurationInfo() отвечает за

Page 182: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

182

отображение времени воспроизведения в надписи labelDuration. Функция

setState() регулирует установки статуса плейера. При этом изображение на

кнопке воспроизведения меняется (play/pause), слайдер и кнопки активируются

и дезактивируются для того, чтобы пользователю удобно было управлять

плейером. Функция changePosition() реагирует на изменение позиции

слайдера пользователем при помощи мыши. Если плейер находится на паузе, то

пользователь не может изменять позицию слайдера. Если плейер играет, то

возможность перемотки включена.

Если Вы не до конца поняли код программы, то отчаиваться не стоит.

Повторяю, Вы можете пользоваться программой как готовым шаблоном.

При запуске программы тренажер выглядит следующим образом (см. Рис.

9.1):

Рис. 9.1 Интерфейс программы audmain.py сразу после запуска

Плейер помещается в верхнюю часть области прокрутки. Пользователю

предлагается нажать кнопку воспроизведения, т.к. другие элементы плейера

Page 183: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

183

деактивированы. После нажатия кнопки воспроизведения плейер приходит в

движение (см. Рис. 9.2):

Рис. 9.2 Интерфейс программы audmain.py во время воспроизведения плейера

Кнопка воспроизведения изменилась на кнопку паузы. Кнопка остановки

активировалась. Надпись времени воспроизведения показывает сколько

проиграно аудиопотока по отношению к общей длине файла в минутах и

секундах (одна секунда из четырех). По окончании воспроизведения плейер

возвращается в свое первоначальное состояние.

Таким образом, у пользователя есть возможность послушать запись

несколько раз, при необходимости проматывая ее вперед и назад, насколько

позволяет время, отведенное на выполнение тренажера.

Плейер тестировался на аудиофайлах с расширением .wav и .mp3. Файлы

.mp3 предпочтительнее, т.к. имеют меньший объем. Помните, что применяя в

учебных материалах сторонние аудиозаписи, Вы можете нарушать чьи-то

авторские права. Самый лучший вариант – использовать аудиозаписи, которые

Page 184: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

184

были записаны лично Вами, например, с помощью знакомых носителей

иностранного языка, имея в распоряжении обычный цифровой диктофон,

смартфон, ноутбук и другие бытовые устройства. Качество записи при этом

может быть довольно высокое, во всяком случае достаточное для учебных

целей.

***

Проверьте и расширьте свое понимание (9.1): Повысьте степень

универсальности тренажера. Пусть при считывании базы данных программа

определяет наличие в ней тега aud и в зависимости от этого выводит или не

выводит в интерфейс аудио плейер.

(9.2): Иногда задания требуют прослушать текст целиком, не пользуясь

перемоткой. Модифицируйте программу так, чтобы перематывать плейер было

нельзя.

(9.3): Также иногда требуется ограничить число полных прослушиваний

(например, 2 или 3). Пусть программа считывает число прослушиваний из

файла XML и выводит в интерфейс число оставшихся прослушиваний.

***

В этой, заключительной, главе Вы познакомились с возможностью

встраивать в тренажер задание на аудирование, а это значит, что Ваши

электронные учебные материалы уже оперируют тремя видами представления

информации: текстом, картинкой и звуком.

Page 185: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

185

Заключение

В предыдущих главах мы с Вами проделали очень большую работу. Мы

установили Python 3.3 и PyQt 5, научились работать в Qt Designer и начали

писать программы для ЭВМ учебного назначения.

Конечно, мы рассмотрели только очень небольшую часть возможностей

библиотеки PyQt 5. В ней есть еще много полезных виджетов и классов,

которые вряд ли можно описать все в одной книге. Это учебное пособие было

нацелено на то, чтобы помочь сделать первые шаги, которые затем могут

превратиться в уверенное следование по пути профессионального развития.

Программирование – это такая область, в которой самостоятельное

обучение играет ключевую роль, потому что языки программирования и

библиотеки к ним обновляются очень часто, добавляя новые возможности. На

момент написания этого учебного пособия версия 5 библиотеки PyQt является

актуальной, но так не будет всегда. Параллельно с ней существует и активно

используется версия 4, которая, правда, имеет существенные отличия от 5-й.

Поэтому тем, кого заинтересовала тема профессионально

ориентированного программирования, я рекомендую внимательно следить за

развитием версий, пользоваться официальной документацией и

профессиональным форумом Stack Overflow.

Удачи Вам и терпения!

Page 186: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

186

Список литературы

Ответы на вопросы о лицензиях 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/pythonlearn-

001 (Дата обращения: 05.05.2014)

QStatusBar Class / QtProject [Электронный ресурс]. – URL: http://qt-

project.org/doc/qt-5/qstatusbar.html#permanent-message (Дата обращения:

16.04.2014)

Page 187: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

187

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.python-

projects.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)

Page 188: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

188

Приложение 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

Page 189: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

189

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).

Page 190: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

190

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. class MyWin(QtWidgets.QMainWindow):

6. 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. def closeProg(self):

15. 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. e.accept()

23. else:

24. e.ignore()

25.

Page 191: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

191

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>"

Page 192: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

192

Код с такой надписью находится в файле 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])

Page 193: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

193

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; font-

weight: bold;')

elif percentage > 76:

self.ui.statusbar.setStyleSheet('color: green; font-

weight: bold;')

else:

self.ui.statusbar.setStyleSheet('color: orange; font-

weight: bold;')

self.ui.statusbar.showMessage(message)

Поскольку желтый плохо заметен на сером фоне, то лучше заменить его на

оранжевый или какой-нибудь другой цвет. Полный файл с решением сохранен

под именем shellmain1.py.

6.2 Строка состояния не работает с кодом HTML, но в нее можно добавить

виджет QLabel. Поэтому для решения задачи необходимо модифицировать

функцию __init__(), добавив в нее строки:

self.labelStatus = QtWidgets.QLabel(self.ui.statusbar)

Page 194: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

194

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)

Page 195: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

195

Она привязывает к каждой группе кнопок функцию, которая запускается

каждый раз, когда пользователь щелкает мышью по одной из кнопок радио

этой группы. Теперь нужно написать эту функцию. Вот ее код:

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))))

Page 196: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

196

Программа работает. Однако не всякое техническое решение полезно с

точки зрения методики. Интерфейс с убывающим индикатором прогресса внизу

и таймером обратного отсчета вверху отвлекает студента от самого главного: от

вопросов тренажера. Помните, что для электронных учебных материалов

минималистичность интерфейса – это скорее плюс, чем минус. Поэтому если

выбирать между зеленым мигающим индикатором и маленьким серым

таймером, то выбор однозначно в пользу второго.

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):

Page 197: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

197

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_():

Page 198: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

198

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 и 150-

151). Вывод производится в процентах, округленных до двух знаков после

запятой (строка 223).

7.6 Решение находится в файле logmain12.py. В строке 179 создается новая

функция. Ее задача – представить список как последовательность его элементов

через точку с запятой:

def listToString(self, getlist):

Page 199: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

199

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). В нем будет храниться число допустимых

прослушиваний. Все изменения в файле основной программы (по сравнению с

Page 200: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

200

файлом audmain2.py) отмечены комментарием # new. Кнопка остановки

воспроизведения дезактивирована. Возможность ставить плейер на паузу также

убрана. Кнопка воспроизведения может быть нажата только если слайдер

находится на отметке «0». Переменная setTimes, в которую записано число

воспроизведений, уменьшается на единицу при каждом нажатии кнопки

воспроизведения. Значение этой переменной выводится в формулировку

задания на аудирование.

Page 201: Алексей Иванович Горожанов1.pyqtforlinguists.appspot.com/book.pdf · PyQt, как более мощной и удобной для пользования библиотеке.

© А.И.Горожанов

201

Приложение 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.