Создание APP для самотестирования (Python)

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!

На базе GUI библиотеки Tkinter
На базе GUI библиотеки Tkinter

Недавно от знакомых прилетела задачка написать программу для самотестирования. Порылся в инете, думал в лёгкую найду наработки, но ничего кроме платных и бесплатных конструкторов тестов не нашёл (может плохо искал, кто знает…). Мне показалось, что устанавливать какие-то инородные проги, а потом ещё туда все вопросы ручками забивать - совсем некрасиво. Так родилось приложение для самотестирования, написанное на 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/post/656517/


Интересные статьи

Интересные статьи

В этом материале речь пойдёт о том, как собрать часы из обычных микросхем. Схема часов (оригинал) Как собрать часы?
Есть вопрос, который мне постоянно задают в Твиттере: как создавать приложения с крутым дизайном с помощью Xamarin.Forms? Это отличный вопрос, ведь любой может создавать ...
Часто при разговорах с клиентами мы спрашиваем, как они ведут учет различных данных и используют ли они CRM-систему? Популярный ответ — мы работаем с Excel-файлами, а пот...
Всем привет, на связи Лиза, UX-писатель, и Стася, UX-аналитик Центра Развития Финансовых Технологий Россельхозбанка. Ближе к релизу наших площадок в продуктовых кома...
Всем привет. Я Михаил Кравченко, дизайнер игровых интерфейсов. В этой статье опишу процесс создания интерфейса для игры. Статья будет полезна начинающим дизайнерам, художникам, которых просят...