На базе 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. Делитесь ими, и кто-то обязательно их заценит ;)

Комментарии (13)


  1. Pushkinist
    20.03.2022 14:38
    +1

    А чем не подходит Anki?

    Есть и на телефон и на все популярные ОС. Вопросы можно вбивать копипастом, ведь в любом случае надо их как-то добавлять. Есть и MCQ.

    P.S. приложение для Мака, на сколько помню, написано на Python.


    1. VladislavSoren Автор
      20.03.2022 15:57

      Не слышал, чекну ????


    1. VladislavSoren Автор
      21.03.2022 10:30

      Ознакомился с Anki:
      https://www.youtube.com/watch?v=MXLN9mekyrk
      Видео работы моего приложения:
      https://disk.yandex.ru/i/jQ3p6mLlNPTEdQ

      По моему мнению из плюсов:
      1. Автоматически перемешиваются ответы (однако они всегда перемешиваются, что не очень гибко). Данную функцию я не реализовал, но это не трудно, просто не было надобности.
      2. Кроссплатформенная.
      3. Есть возможность расширения количества вариантов ответа.

      По моему мнению из минусов:
      1. Подсвечивание правильных ответов не очень удобное. Я такой вариант тоже рассматривал, но понял что это лишний гемор для пользователя.
      2. Сам процесс добавления вопросов более сложный. В моём же варианте можно просто скопипастить с сайта и плюсы там где надо поставить в txt, что гораздо быстрее.
      3. Не смотря на то, что есть функция расширения количества вариантов ответа, достаточно неудобно пользователю заходить в исходник и что-то там править. Мой же вариант, описанный в статье, будет более дружелюбным пользователю (в код не надо будет лезть).


  1. kraken2018
    21.03.2022 09:13

    Госпади, практически такую же "систему" я писал почти 30 лет назад (да-да, с чтением вопросов из текстового файла, только сжатого и шифрованного), на ТурбоПаскале, с генератором тестов из текстовых баз, написанным с помощью TurboVision. С помощью нее несколько лет проводилось тестирование студентов кафедры "Истории, теории государства и права и социологии". Очень быстро и просто генерился набор вопросов, в параметрах запуска указывалось, сколько из них задавать.

    И вот кто-то на полном серьезе описывает создание подобной "системы". Даю ценный совет: введите в программу "тайную кнопочку", при нажатии на которую будут автоматически активироваться верные ответы ;) Это позволит вашим друзьям иметь приличные оценки всегда :)


    1. VladislavSoren Автор
      21.03.2022 09:26

      С помощью нее несколько лет проводилось тестирование студентов кафедры "Истории, теории государства и права и социологии".

      Красавчик

      Даю ценный совет: введите в программу "тайную кнопочку", при нажатии на которую будут автоматически активироваться верные ответы ;)

      Задача программы отличалась от вашей, т.к. она НЕ планировалась для массового тестирования в каких-то учреждениях. Её цель - помочь самому натаскаться на какой либо тест.
      И функционал устроен так, что ты можешь нажать "Ответить", появятся правильные ответы, и ты можешь их перевыбрать под правильные и нажать "Следующий" (счётчик правильных ответов срабатывает только при смене вопроса). Так что это кнопочка заложена в функционале.
      Повторюсь, результат зависит от поставленной задачи.


      1. c_pro_lang
        21.03.2022 10:09

        Её цель - помочь самому натаскаться на какой либо тест

        А откуда берете тесты, если не секрет? На экзамены готовитесь?


        1. VladislavSoren Автор
          21.03.2022 10:42

          Мои знакомые в колледже медицинском учатся и там преподы дают по какой либо теме самому подготовить тест. Так каждый студент (пусть 20 человек), каждый по своей теме готовит, например, по 30 вопросов.
          Далее всё это сдаётся хитрому преподу и он их перемешивает и даёт на экзах и зачётах им же :)
          И вот данная программа, её я назвал SelfCon, помогает в данном случае ребятам подготовиться.


  1. c_pro_lang
    21.03.2022 09:13

    Я тоже в студенчестве написал такую программу на Python (PyQt4), но часть пришлось переписать как веб сайт, чтобы использовать на телефоне.

    Но у нас правильные ответы тестов всегда были вариант А), по мне это лучше чем плюс в конце.


    1. VladislavSoren Автор
      21.03.2022 09:30

      Но у нас правильные ответы тестов всегда были вариант А), по мне это лучше чем плюс в конце.

      А не терялась ли тогда суть теста?
      Если всё время верный первый вариант ответа, то ленивому мозгу человеческому будет труднее вдумываться, ведь верный ответ всегда A).
      Поясните пожалуйста такое подход.


      1. c_pro_lang
        21.03.2022 09:58

        Не правильно выразился. Тест с правильным вариантом А) это только для программы университета. На экзамене программа перемешивает все ответы (и вопросы), и вариант А) уже может быть неправильным.

        Преподаватели давали нам этот Word документ с 300 вопросами, каждый из которых имеет 5 вариантов ответа (четыре из которых неправильный).

        Для программы, думаю, легче считать первый вариант правильным, чем в каждой строке искать плюс. Я это имел ввиду.


        1. VladislavSoren Автор
          21.03.2022 10:54

          Понял идею
          Но опять же, любой подход имеет право на жизнь
          В моём случае можно скопировать тест из интернета (где порядок как правило уже случайный) и просто метки добавить к правильным ответам.
          В вашем случае, если с нуля сам тест составляешь, то просто по умолчанию правильный вариант первым ставишь и всё, а дальше перетасовываешь. И парсер в проге никакой тогда не нужен.
          Однако главный минус такого подхода в том, что правильный ответ только один может быть при данном подходе.


  1. Z55
    21.03.2022 15:55

    цИкл


    1. VladislavSoren Автор
      21.03.2022 23:36

      Да да косяк ????

      С русским всегда были непростые отношения)