
Недавно от знакомых прилетела задачка написать программу для самотестирования. Порылся в инете, думал в лёгкую найду наработки, но ничего кроме платных и бесплатных конструкторов тестов не нашёл (может плохо искал, кто знает…). Мне показалось, что устанавливать какие-то инородные проги, а потом ещё туда все вопросы ручками забивать — совсем некрасиво. Так родилось приложение для самотестирования, написанное на Python с помощью GUI библиотеки Tkinter.
В первую очередь нужно было составить требования к структуре файла, чтобы не пришлось писать всеобъемлющий парсер на регулярках.
Я обозначил следующие требования:
-
Каждый вопрос должен начинаться с числа и точки (метка для идентификации вопросов)
-
Каждый ответ может начинаться с любого символа, но затем обязательно должна идти скобка (метка для идентификации ответов)
-
Правильные варианты ответа должны быть отмечены знаком «+» (метка для идентификации правильных ответов)
-
Вариантов ответа должно быть ровно 5:
!Если ответов меньше ПЯТИ, то их НУЖНО добить пустыми, например, если их три то добавить 4) и 5)
!Если вариантов ответа более ПЯТИ, то приложение не будет корректно работать
Ограничения по количеству ответов можно обойти, если при запуске программы попросить пользователя ввести максимальное кол-во ответов в вопросе и в автомате добивать недостающие ответы пустышками посредством алгоритмических ухищрений. Однако пока я не стал этого реализовывать, ибо не увидел великой надобности 🙂
Пример содержимого txt файла:

Напомню, что построение программы с помощью Tkinter основывается на СОП (событийно-ориентированном программировании). При таком подходе в коде явным образом выделяется главный цикл приложения, тело которого состоит из двух частей: выборки события и обработки события.
В моём случае главный цикл представляет из себя class Block, который вызывается в mainloop():
######################### # Блок обработки событий. ######################### class Block: # Инициализация объектов def __init__(self, master): # счетчик количества вопросов self.qc = 0 # счетчик количества правильных ответов self.true_points = 0 # *Здесь отображено лишь начало Block
################# # Основной цыкл. ################# window = tk.Tk() window.title('Конструктор тестов (VladislavSoren)') window.resizable(width=False, height=False) window.geometry('720x480+400+100') window['bg'] = 'grey' first_block = Block(window) window.mainloop()
Выборки события представляют из себя интерфейс взаимодействия с пользователем, т.е. виджеты (кнопки, чекбоксы и т.п.). Обработчиками событий являются функции класса (методы), которые вызываются, когда произошло то или иное событие, например, пользователь нажал кнопку.
Для большего понимания происходящего пробежимся по алгоритму работы программы, а потом рассмотрим каждый этап в отдельности:
1. Пользователь указывает путь к txt файлу с тестом.

2. Производится парсинг содержимого теста, т.е. формируются список вопросов и список ответов.
3. Пользователь выбирает режим работы (Рандомный порядок вопросов vs обычный порядок).

4. Генерируется список с порядком вопросов.
5. «Правильные ответы» пользователь отмечает в чекбоксах.

6. Затем пользователь нажимает кнопку «Ответить». После данного события система переходит в состояние проверки, где:
-
Истинно-правильные ответы подсвечиваются зелёным
-
Изменяется статус индикатора. Если ошибок НЕТ, то индикатор примет значение «Всё верно», иначе «Есть ошибки»
-
Если «Всё верно», то увеличивается счётчик верных ответов на единицу

7. После того, как пользователь проанализировал свой ответ, он нажимает кнопку «Следующий». После данного события система переходит в состояние смены вопроса, где:
-
Счётчик вопросов увеличивается на единицу
-
Меняется вопрос и ответы
-
Статус индикатора изменяется на исходный «Выберите ответы:»

8. Когда пользователь ответил на последний вопрос и нажал «Следующий», то высвечивается кол-во правильных ответов за весь тест.

А теперь немного поподробнее:
1. Пользователь указывает путь к txt файлу с тестом.
################################ # Загрузка теста и его парсинг ################################ window = tk.Tk() # Пользователь указывает путь к txt файлу text_path = filedialog.askopenfilename(title='Выберите тест') window.destroy()
2. Производится парсинг содержимого теста, т.е. формируются список вопросов, список ответов и вектор меток.
# разделяем файл на строки по пробелам Text = Text.split(sep='\n') # отделяем вопросы pattern = r'^\d{1,3}\.' # строка начинается с 1 или 3 цыфр, а затем идёт точка (ограничение на 999 вопросов!!!) Text_q = [i for i in Text if len(re.findall(pattern, i)) != 0] print('Всего вопросов:', len(Text_q)) # отделяем ответы pattern = r'^.\)' # строка начинается с одного любого символа, а затем идёт скобка Text_a = [i for i in Text if len(re.findall(pattern, i)) != 0] print('Всего ответов:', len(Text_a))
Далее проходимся по ответам и создаём отдельный вектор с метками правильных ответов формата [1 0 0 0 1 0…], как раз для этого и были нужны «+» при разметке файла.
После данного шага получаем список вопросов Test_q, список ответов БЕЗ меток Test_a и вектор меток flags.
3. Пользователь выбирает режим работы (Рандомный порядок вопросов vs обычный порядок).
################################################################ # Выбор режима (Рандомный порядок вопросов vs обычный порядок). ################################################################ window = tk.Tk() window.title('Конструктор тестов (VladislavSoren)') window.resizable(width=False, height=False) window.geometry('240x60+600+300') window['bg'] = 'white' # функция закрытия окна при выборе рандомного режима def accept(): window.destroy() RandomState = tk.IntVar() # в данную переменную записывается состояние box (1 или 0) box = Checkbutton(window, text='Включить случайный порядок?', variable=RandomState, font=('Arial Bold', 10), relief='solid', bd='1' ) box['command']=accept box.place(x=12, y=20) window.mainloop()
Здесь главным действующим лицом выступает CheckButton (Чекбокс). Если пользователь ставит в боксе галочку, то переменная RandomState примет значение 1, иначе будет равна 0.
Как только пользователь поставил галку – окно закрывается, а RandomState=1. Если же нам НЕ нужен рандомный режим, то просто можно закрыть всплывающее окно.
4. Генерируется список с порядком вопросов.
####################################### # Получение списка с порядком вопросов. ####################################### Text_q_dict = {} for i, q in enumerate(Text_q): Text_q_dict[i] = q np1 = np.arange(len(Text_q)) order_list = np1.tolist() # Если выбран рандомный режим, то перемешиваем порядок вопросов if RandomState.get(): random.shuffle(order_list)
При выборе рандомного режима order_list будет вида [2 0 3 1 5 4].
5. «Правильные ответы» пользователь отмечает в чекбоксах.
Тут комментарии излишни.
6. Затем пользователь нажимает кнопку «Ответить». Система переходит в состояние проверки.
Кратко опишу структуру class Block, который вызывается в главном цикле mainloop().
# Инициализация объектов def __init__(self, master): # счетчик количества вопросов self.qc = 0 # счетчик количества правильных ответов self.true_points = 0 # Инициализация вопроса и ответов self.quest = scrolledtext.ScrolledText(window, width=75,height=5) index = order_list[self.qc] # индекс вопроса определяем по order_list self.quest.insert(tk.INSERT, Text_q[index])
Метод __inite__ выполняется единожды при создании объекта класса, т.е. в нём происходит инициализация всех объектов, создаются виджеты и все необходимые привязки событий.
Вот основные привязки:
# Инициализация лэйблов и кнопок self.mark = tk.Label(window, text='Выберите ответы: ', font=('Arial Bold', 12), fg='Green', bg='white') self.ButGiveAns = Button(text='Ответить', font=('Arial Bold', 12)) # кнопка перехода в состояние "ПРОВЕРКА" self.ButGiveAns['command'] = self.show_res self.ButNext = Button(text='Следующий', font=('Arial Bold', 12)) # кнопка перехода в состояние "СМЕНА ВОПРОСА" self.ButNext['command'] = self.next_q
При нажатии кнопки «Ответить» вызовется метод show_res:
# Функция обработки события "ПРОВЕРКА" (нажатие кнопки "Ответить") def show_res(self): # определяем текущий индекс вопроса index = order_list[self.qc] # создаем вектор таргетов и ответов targets = flags[5*index : 5*index + 5] answers = np.zeros(5) answers[0] = self.check1.get() # записываем состояние box1 (0 или 1) в нулевой бит вектора answers answers[1] = self.check2.get() answers[2] = self.check3.get() answers[3] = self.check4.get() answers[4] = self.check5.get() # подсвечиваем истинно верные ответы зелёным цветом (задний фон чекбоксов) for i, box in enumerate([self.box1, self.box2, self.box3, self.box4, self.box5]): if targets[i] == 1: box['bg'] = 'green' # проверка ответа пользователя (сравнение вектора ответа с вектором таргета) if (targets == answers).sum() == 5: self.mark['text'] = 'Всё верно' # меняем текст метки на статус "Всё верно" self.true_points += 1 # исли всё верно, то накидываем очко else: self.mark['text'] = 'Есть ошибки'
Главное на что стоит обратить внимание — за индекс мы берём значение из списка order_list. Например, ели мы на втором вопросе (self.qc=1), а order_list = [2 0 3 1 5 4], то индекс будет равен 0.
При нажатии кнопки «Следующий» вызовется метод next_q. О нём в следующем пункте.
7. Пользователь нажимает кнопку «Следующий». Система переходит в состояние смены вопроса и вызывается метод next_q.
Первый важный момент:
# инкрементируем счётчик вопросов self.qc += 1
Затем удаляем подсветку боксов и обновляем поля вопросов и ответов:
# определяем текущий индекс вопроса index = order_list[self.qc] # удаляем подсветку чекбоксов for i, box in enumerate([self.box1, self.box2, self.box3, self.box4, self.box5]): box['bg'] = 'white' box.deselect() # смена вопроса self.quest.delete('1.0', 'end') # очищаем всё поле с индекса "1" до последнего "end" self.quest.insert(tk.INSERT, Text_q[index]) # выводим следующий вопрос
И не забываем про index!
8. Пользователь ответил на последний вопрос и нажал «Следующий» — высвечивается кол-во правильных ответов за весь тест.
# когда ответили на все вопросы -> подводим итоги if self.qc >= len(Text_q): self.FinalScore = tk.Label(window, text=f'Всего правильных ответов: {self.true_points}', font=('Arial Bold', 15), fg='white', bg='grey') self.FinalScore.place(x=360, y=210)
С логикой работы разобрались)
Подытожим:
-
Были обозначены требования к структуре файлов, для их корректной обработки приложением
-
Рассмотрены принципы событийно-ориентированного программирования
-
Написан GUI на базе библиотеки Tkinter
-
Описана логика работы приложения
Всем успехов в написании собственных интересных и полезных APP. Делитесь ими, и кто-то обязательно их заценит 😉
ссылка на оригинал статьи https://habr.com/ru/articles/656517/
Добавить комментарий