만들기

파이썬으로 문제 은행 프로그램 만들기

muya98 2023. 7. 25. 01:24

ADP 공부도 할 겸.. 겸사겸사 문제 은행 프로그램을 만들어 보았습니다. 이틀이 걸렸는데 중간중간 발생한 문제를 해결하고 발전시킨걸 순차적으로 블로깅 했어야했는데.. 다 완성해버리고 아차.. 기억을 더듬더듬... 만들어 봅시다...

 

먼저 프로그램을 구상하자면~ tkinter와 openpyxl 라이브러리를 사용하여 엑셀파일에 저장한 문제를 불러와서 tkinter로 인터페이스를 만들어 봅시닷!

 

< 프로그램 진전? 절차? >

1. 초기 구상 : 문제와 사지선다 보기가 화면에 뜨면 사지선다 버튼을 눌러 정답확인

2. 문제를 풀면 다음 문제로 넘어 갈 수 있도록 '이전 문제'와 '다음 문제' 버튼 추가

3. 해설이 바로 보여지지 않도록 해설 버튼 추가

4. 문제 순서가 랜덤으로 나오도록 수정

5. 각 버튼 단축키 기능 추가

6. 과목(시트)을 선택하는 기능과 초기 인터페이스 추가

7. 첫 인터페이스로 돌아가는 홈 버튼과 단축키 추가, 홈 키를 누르면 프로그램이 초기화되도록 수정

8. 엑셀 파일 경로를 프로그램과 같은 파일의 경로로 수정

등등 도중에 에러가 엄청 많이 떴습니다.. 특히 엑셀 파일을 불러 올 때 에러가 너무 많이 떴어요..

 

< 만들기 GO! >

1. 엑셀 파트

엑셀 파일에 문제를 적어줍시다. 저는 5과목이기 때문에 시트를 5개를 만들어서 각각 '(숫자)과목'이라 이름지었어요.

1열에는 문제의 회차, 2열에는 문제, 3~6열에는 사지선다 보기, 7열에는 사지선다 정답(숫자), 8열에는 풀이를 작성했어요.

 

2. 파이썬 파트

 

먼저 tkinter와 openpyxl이 없을수도 없으니 깔아줘야겠죠? 콘솔창으로 가서

 

pip install openpyxl  # openpyxl 설치 

pip install tk  # tkinter 설치 

 

라이브러리를 설치해줍시다! 

 

먼저 사용할 라이브러리와 모듈들을 불러옵시닷.

import tkinter as tk  
from tkinter import messagebox 
from openpyxl import load_workbook 
from openpyxl.styles import NamedStyle
import random  
import os  

'messagebox 모듈'은 오류창을 표시하는데 사용하고 'load_workbook 함수'는 Excel 파일을 읽고 쓰는 기능을 추가해줍니다. 'NamedStyle 클래스'는 엑셀 파일의 스타일을 Default 값으로 되돌릴때 사용했습니다. 'radom 모듈'은 문제 순서를 무작위로 바꿀 때 사용하였고, 'os 모듈'은 마지막에 파이썬의 파일 경로로 엑셀파일을 불러 올 때 사용했습니다!

class DefaultStyle(NamedStyle): 
    def __init__(self, name=None): 
        super().__init__(name) 

먼저 openpyxl의 NamedStyle 클래스를 새로운 클래스인 DefaultStyle 클래스에 정의합니다. 이후 스타일을 모두 일관성있게 처리되도록 해줍니다!

 

이번에는 초기화면 인터페이스를 만들어 봅시다.

class QuestionBankApp(tk.Tk):
    def __init__(self):
        super().__init__()
        self.title("문제은행")
        self.geometry("400x200")
        self.selected_sheet = None
        self.create_widgets()

    def create_widgets(self):
        self.sheet_label = tk.Label(self, text="과목 선택:", font=("Arial", 16))
        self.sheet_label.pack(pady=10)

        self.sheet_var = tk.StringVar()
        self.sheet_dropdown = tk.OptionMenu(self, self.sheet_var, "1과목", "2과목", "3과목", "4과목", "5과목")
        self.sheet_dropdown.config(font=("Arial", 14), width=12)
        self.sheet_dropdown.pack()

        self.start_button = tk.Button(self, text="시작", command=self.start_question_bank, font=("Arial", 20))
        self.start_button.pack(pady=10)

    def start_question_bank(self):
        self.selected_sheet = self.sheet_var.get()
        if self.selected_sheet:
            self.destroy()
            app = QuestionBank(self.selected_sheet)
        else:
            messagebox.showinfo("오류", "과목을 선택하세요.")

먼저 '__init__(self) ' 메소드로 클래스와 객체의 속성 초기화에 필요한 설정들을 만들어줍니다. 프로그램의 제목, 창 크기, 시트 초기값(None), 위젯 생성 메소드 호출.

다음은 버튼을 생성하고 버튼을 클릭하면 start_question_bank 메소드를 불러오도록 합니다.

start_question_bank 메소드에서는 '과목을 선택함'에 대해 True/False로 작업이 실행되도록 합니다. 

True이면 문제은행 프로그램 인터페이스를 실행하고, False이면 messagebox로 오류를 표시합니다.

 

이제 본 인터페이스를 만들어 봅시다.

class QuestionBank(tk.Tk):

초기 인터페이스와 같이 먼저 'tkinter.TK' 클래스를 상속해옵니다.

    def __init__(self, selected_sheet):
        super().__init__()
        self.title("문제은행")
        self.geometry("800x600")
        self.current_question_index = 0
        self.questions = []
        self.selected_sheet = selected_sheet
        self.load_question_data()
        self.create_widgets()

그 다음 초기 설정을 입력해줍니다.

    def create_widgets(self):
        self.question_label = tk.Label(self, wraplength=780, font=("Arial", 18), justify="left", anchor="w")
        self.question_label.pack(pady=10, padx=10, anchor="w")

        self.choice_buttons = []
        for idx in range(4):
            choice_frame = tk.Frame(self)
            choice_frame.pack(anchor="w")
            choice_text = f"{idx+1}."
            choice_button = tk.Button(choice_frame, text=choice_text, command=lambda ch=idx+1: self.check_answer(ch),
                                      font=("Arial", 15), wraplength=780, justify="left")
            self.choice_buttons.append(choice_button)
            choice_button.pack(side="left")

        self.result_label = tk.Label(self, font=("Arial", 30))
        self.result_label.pack()

        self.explanation_label = tk.Label(self, wraplength=780, font=("Arial", 12))
        self.explanation_label.pack()

        button_frame = tk.Frame(self)
        button_frame.pack(pady=10)

        self.previous_button = tk.Button(button_frame, text="이전 문제", command=self.show_previous_question, state=tk.DISABLED,
                                     font=("Arial", 15))
        self.previous_button.pack(side="left", padx=10)

        self.next_button = tk.Button(button_frame, text="다음 문제", command=self.show_next_question, state=tk.DISABLED,
                                     font=("Arial", 15))
        self.next_button.pack(side="left", padx=10)

        self.show_explanation_button = tk.Button(button_frame, text="풀이 보기", command=self.show_explanation, state=tk.DISABLED,
                                                 font=("Arial", 15))
        self.show_explanation_button.pack(side="left", padx=10)

        self.restart_button = tk.Button(button_frame, text="처음으로", command=self.restart_program, font=("Arial", 15))
        self.restart_button.pack(side="left", padx=10)

        self.show_question()
        self.bind('<Key>', self.handle_keypress)

위에서는 버튼들의 글꼴, 크기를 설정하고 각 버튼에 대한 단축키를 지정해주었습니다.

 

    def load_question_data(self):
        # Get the directory of the current script
        current_dir = os.path.dirname(os.path.realpath(__file__))
        # Combine directory with file name to get the path
        file_path = os.path.join(current_dir, "adp3.xlsx")

        wb = load_workbook(file_path)

        ws = wb[self.selected_sheet]
        default_style = DefaultStyle(name='normal')
        for row in ws.iter_rows(min_row=2, values_only=True):
            question_data = {
                'question': row[1],
                'choices': [str(row[2]), str(row[3]), str(row[4]), str(row[5])],
                'answer': int(row[6]),
                'explanation': row[7]
            }
            self.questions.append(question_data)
        random.shuffle(self.questions)

이번에는 문제 데이터를 엑셀로부터 가져오도록 해봅시다.  파일 경로를 아까 import한 os의 realpath를 통해 프로그램과 같은 폴더에 있는 엑셀 파일을 불러오도록 하였습니다. 그리고 random 모듈을 사용하여 문제가 무작위로 섞이도록 했습니다!

 

    def show_question(self):
        question_data = self.questions[self.current_question_index]

        question_number = self.current_question_index + 1
        question_text = f"{question_number}. {question_data['question']}"
        self.question_label.configure(text=question_text)

        for idx, choice_button in enumerate(self.choice_buttons):
            choice_text = f"{idx+1}. {question_data['choices'][idx]}"
            choice_button.configure(text=choice_text)

        self.result_label.configure(text="")
        self.explanation_label.configure(text="")
        self.show_explanation_button.configure(state=tk.DISABLED)
        self.next_button.configure(state=tk.DISABLED)
        self.previous_button.configure(state=tk.DISABLED)

이번에는 문제를 표시하는 창을 만들어봅시다. 먼저 문제 데이터를 불러 온 후 문제 앞에 '숫자.'가 표시되도록하고 사지선다의 앞에도 '숫자번호.'가 표시되도록 설정해주고 버튼들이 문제를 풀기 전에는 비활성화 되어있도록 합니다.

 

    def check_answer(self, selected_choice):
        question_data = self.questions[self.current_question_index]
        correct_answer = question_data['answer']

        if selected_choice == correct_answer:
            self.result_label.configure(text="정답입니다!", fg='green')
        else:
            self.result_label.configure(text="틀렸습니다.", fg='red')
        self.show_explanation_button.configure(state=tk.NORMAL)
        self.next_button.configure(state=tk.NORMAL)
        self.previous_button.con123figure(state=tk.NORMAL)

이번에는 문제가 틀렸는지 맞았는지 판단하는 부분입니다. 엑셀에 적힌 정답 데이터를 비교하고 정답과 오답을 판단하여 텍스트가 출력되도록 하였습니다. 문제를 풀었기 때문에 버튼이 다시 활성화되도록 합니다.

 

    def show_explanation(self):
        question_data = self.questions[self.current_question_index]
        self.explanation_label.configure(text=f"풀이: {question_data['explanation']}")

        self.show_explanation_button.configure(state=tk.DISABLED)
        self.next_button.configure(state=tk.NORMAL)
        self.previous_button.configure(state=tk.NORMAL)

문제의 풀이를 표시하는 부분입니다! 

 

    def show_next_question(self):
        self.current_question_index += 1
        if self.current_question_index >= len(self.questions):
            self.current_question_index = 0
        self.show_question()

    def show_previous_question(self):
        self.current_question_index -= 1
        if self.current_question_index < 0:
            self.current_question_index = len(self.questions) - 1
        self.show_question()

이전 문제와 다음 문제를 표시하는 부분입니다! 인덱스를 플러스마이너스 1하며 if 문을 사용해 문제가 보여지도록 했습니다!

 

    def handle_keypress(self, event):
        if event.keysym == 'Right':
            self.show_next_question()
        elif event.keysym == 'Left':
            self.show_previous_question()
        elif event.keysym == 'Up':
            self.show_explanation()
        elif event.char.isdigit() and int(event.char) in [1, 2, 3, 4]:
            self.check_answer(int(event.char))
        elif event.char.lower() == 'h':
            self.restart_program()
        else:
            messagebox.showinfo("오류", "단축키 - 숫자 키 1, 2, 3, 4 : 정답, 풀이 : 윗 방향키, 이전 문제 : 왼쪽 방향키, 다음 문제 : 오른쪽 방향키")

앞에 지정했던 각 버튼들의 키 입력을 처리하는 부분입니다! 미리 입력된 키 외에 키들이 입력되면 messagebox로 단축키 설명이 표시되도록 했습니다.

 

    def restart_program(self):
        self.destroy()
        app = QuestionBankApp()

홈키를 눌렀을 때 프로그램을 재시작하여 초기화시키는 부분입니다.

 

    app = QuestionBankApp()
    app.mainloop()

마지막으로 프로그램 화면을 생성하고 실행하는 부분입니다!

 

이렇게 문제 은행 프로그램을 실행하면!!

초기 화면
과목 선택~~
문제 푸는 창

 

아주 잘 작동합니다!! 당연하죠 오류 수정을 엄청 많이 했거든요...

 

뒤로 갈수록 설명이 부실하긴 한데.. 음 한줄 한줄 다쓰기엔 좀.. 귀찮자나요...

 

코드를 블로깅하는게 생각보다 쉬운 일이 아니네요 쩝.. 다음엔 또 재밌는걸 만들어 보겠어요