실행중인 프로그램과 사용자 사이의 교감이 지금까지는 터미널 콘솔창을 통해서만 가능하였다. 즉, 사용자가 프로그램에 값을 입력할 때, 또는 프로그램이 결과를 사용자에게 보여줄 때 모두 터미널 콘솔창을 이용하였다.
하지만 요즘 사용되는 애플리케이션은 사용자와 다양한 방식으로 교감한다. 사용자는 키보드 입력 이외에 마우스, 터치 등의 수단으로 애플리케이션에게 정보를 전달하고, 애플리케이션은 팝업 윈도우 창, 소리, 진동 등 다양한 방식으로 반응한다.
여기서는 티비 질문/답변 게임 진행자가 윈도우 창, 소리, 마우스 클릭 등의 다양한 방식으로 교감하는 GUI(graphic user interface, 그래픽 사용자 인터페이스) 애플리케이션을 구현하고자 한다.
아래 내용을 만족하는 GUI 애프리케이션을 생성하고자 한다.
애플리케이션의 시작 모습은 다음과 같다.
게임 시작 후에 참여자가 5개의 정답과 2개의 오답 버튼을 말했다면 애플리케이션 모습이 아래 모습으로 변하였어야만 한다.
"파이썬 퀴즈 게임" GUI 애플리케이션 구성요소는 다음과 같다.
창(screen), 버튼 등 애플리케이션을 구성하는 요소들을 위젯(widget)이라 부른다. GUI 프로그래밍을 위해 다양한 위젯이 제공되며, 여기서는 가장 기본적인 위젯만을 사용하여 퀴즈 게임 애플리케이션을 구현한다.
앞서 언급한 구성 요소 네 개를 하나씩 구현해 보자.
윈도우 창은 tkinter
모듈에 포함된 Tk
클래스의 인스턴스로 구할 수 있다.
from tkinter import *
# 윈도우 팝업창 인스턴스 생성
app = Tk()
# 윈도우 팝업창 타이틀 지정
app.title("파이썬 퀴즈 게임")
# 윈도우 팝업창 크기와 위치 지정
app.geometry('300x120+200+100')
# 윈도우 팝업창 유지
# X 버튼을 누를 때가지 유지됨.
# 사용하지 않을 경우 생성된 후 바로 사라짐.
app.mainloop()
위 프로그램을 실행하면 지정된 타이틀과 크기를 갖는, 하지만 다른 요소를 전혀 포함하지 않는 윈도우 창이 아래와 같이 생성된다.
mainloop
메소드는 생성된 윈도우 창이 사용자가 닫기 버튼(X)을 누를 때까지 유지되도록 하는 기능을 갖는다.
버튼은 tkinter
모듈의 Button
클래스의 인스턴스로 생성할 수 있다.
from tkinter import *
app = Tk()
app.title("파이썬 퀴즈 게임")
app.geometry('300x120+200+100')
# 정답 버튼 생성
b1 = Button(app, text = "정답", width = 7)
b1.pack(side = 'left', padx = 10, pady = 10)
# 오답 버튼 생성
b2 = Button(app, text = "오답", width = 7)
b2.pack(side = 'right', padx = 10, pady = 10)
app.mainloop()
위 프로그램을 실행하면 앞서 생성한 app
윈도우 창에 정답/오답 두 개의 버튼이 생성된다.
Button
생성자의 첫째 인자는 생성된 버튼을 위치시킬 창을 가리키는 값이어야 한다. 여기서는 app
이 사용되었다.Button
생성자의 text
와 width
키워드 인자의 역할은 상식적으로 이해될 수 있다.Button
클래스의 pack
메소드는 생성된 버튼을 지정된 윈도우 창에 위치시키는 기능을 갖는다.pack
메소드의 side
키워드 변수의 역할은 버튼 위치를 지정하기 위해 사용된다.pack
메소드의 padx
와 pady
의 역할은 버튼 주의에 여백(패딩)을 주기 위해 사용된다.윈도우 창에 문장을 입력하기 위해서는 원하는 문장을 Label
클래스의 인스턴스로 생성한 후에
지정된 윈도우 창에 위치시켜야 한다.
from tkinter import *
app = Tk()
app.title("파이썬 퀴즈 게임")
app.geometry('300x120+200+100')
# 첫째 문장
lab1 = Label(app, text = "정답 또는 오답 버튼을 누르세요!", fg='blue', height=3)
lab1.pack()
# 둘째 문장
lab2 = Label(app, text='총 문제 개수:')
lab2.pack()
b1 = Button(app, text = "정답", width = 7)
b1.pack(side = 'left', padx = 10, pady = 10)
b2 = Button(app, text = "오답", width = 7)
b2.pack(side = 'right', padx = 10, pady = 10)
app.mainloop()
위 프로그램을 실행하면 앞서 생성한 app
윈도우 창에 지정한 두 개의 문장이 차례대로 표시된다.
Label
생성자에 사용된 인자들의 역할은 자연스럽게 이해될 수 있다.Label
, Button
클래스 모두 pack
메소드를 갖고 있으며
pack
메소드가 실행되는 순서대로 각 요소가 자리를 잡는다.윈도우 창에 변하는 숫자를 입력하기 위해서도 Label
클래스의 인스턴스를 사용한다.
다만 정답/오답 버튼을 누를 때마다 변하는 값을 바로바로 애플리케이션 창에 보여주기 위해서는
숫자를 담는 특별한 자료형이 필요하다.
from tkinter import *
app = Tk()
app.title("파이선 퀴즈 게임")
app.geometry('300x120+200+100')
# 저장된 정수를 앱에 자동으로 전달하는 자료형 활용: IntVar 클래스
num_good = IntVar() # 정답 개수 저장
num_good.set(0) # 정답 개수 0으로 초기화
num_bad = IntVar() # 오답 개수 저장
num_bad.set(0) # 오답 개수 0으로 초기화
num_total = IntVar() # 총 문제 개수 정장
num_total.set(0) # 총 문제 개수 0으로 초기화
lab1 = Label(app, text = "정답 또는 오답 버튼을 누르세요!", fg='blue', height=3)
lab1.pack()
lab2 = Label(app, text='총 문제 개수:')
lab2.pack()
# IntVar 인스턴스에 저장된 값을 Label 클래스의 인스턴스로 전달하여 보여주기
lab3 = Label(app, textvariable = num_total)
lab3.pack()
lab4 = Label(app, textvariable = num_good)
lab4.pack(side = 'left')
lab5 = Label(app, textvariable = num_bad)
lab5.pack(side = 'right')
b1 = Button(app, text = "정답", width = 7)
b1.pack(side = 'left', padx = 10, pady = 10)
b2 = Button(app, text = "오답", width = 7)
b2.pack(side = 'right', padx = 10, pady = 10)
app.mainloop()
위 프로그램을 실행하면 앞서 생성한 num_total
, num_good
, num_bad
변수에 저장된 값이
지정된 위치에 표시된다.
원래 아래 모양을 기대했지만
아래 모양으로 만들어진다. 이유는 app
을 생성할 때 윈도우 창의 크기를 지정했기 때문이다.
IntVar
클래스의 인스턴스는 정수를 담는 그릇 역할을 수행한다.IntVar
클래스의 set
메소드는 생성된 인스턴스에 담길 정수값을 지정한다.Label
생성자에 사용된 textvariable
키워드 매개변수가 text
대신에 사용되었다.총 문제 개수가 "총 문제 개수:" 문장 옆에 위치시켜서 다음 모양을 만들었으면 한다.
그러기 위해서는 lab2
와 lab3
를 하나로 묶어야 하며
이를 위해 Frame
클래스를 활용한다.
from tkinter import *
app = Tk()
app.title("파이선 퀴즈 게임")
app.geometry('300x120+200+100')
num_good = IntVar()
num_good.set(0)
num_bad = IntVar()
num_bad.set(0)
num_total = IntVar()
num_total.set(0)
lab1 = Label(app, text = "정답 또는 오답 버튼을 누르세요!", fg='blue', height=3)
lab1.pack()
# 프레임 인스턴스 생성: 여러 요소를 하나로 묶는 기능 수행
# 윈도우 창을 여러 개의 프레임으로 나누는 기능 가잠
# 프레임을 먼저 윈도우 창에 위치시킴
frame = Frame(app)
frame.pack()
# lab2를 app이 아니라 frame 내부의 왼편에 위치시킴
lab2 = Label(frame, text='총 문제 개수:')
lab2.pack(side='left')
# lab3를 app이 아니라 frame 내부의 오른편에 위치시킴
lab3 = Label(frame, textvariable = num_total)
lab3.pack(side='right')
lab4 = Label(app, textvariable = num_good)
lab4.pack(side = 'left')
lab5 = Label(app, textvariable = num_bad)
lab5.pack(side = 'right')
b1 = Button(app, text = "정답", width = 7)
b1.pack(side = 'left', padx = 10, pady = 10)
b2 = Button(app, text = "오답", width = 7)
b2.pack(side = 'right', padx = 10, pady = 10)
app.mainloop()
위 프로그램을 실행하면 lab2
와 lab3
가 하나의 프레임으로 묶여서 각각 한 줄의 좌우편에 위치한다.
Frame
클래스의 pack
메소드 역시 호출 순서대로 윈도우 창에 위친한다.Frame
활용하기¶lab2
와 lab3
를 하나로 묶은 것처럼 다른 줄도 Frame
으로 묶어서 처리하면 보다 편하다.
따라서 각각의 줄을 하나의 프레임으로 묶기 위해 Frame
인스턴스를 세 개 사용한다.
이를 위해 Frame
클래스를 활용한다.
from tkinter import *
app = Tk()
app.title("파이선 퀴즈 게임")
app.geometry('300x120+200+100')
num_good = IntVar()
num_good.set(0)
num_bad = IntVar()
num_bad.set(0)
num_total = IntVar()
num_total.set(0)
# 세 개의 프레임 생성 후 차례대로 app에 위치 시키기
frame1 = Frame(app)
frame1.pack()
frame2 = Frame(app)
frame2.pack()
frame3 = Frame(app)
frame3.pack()
# lab1을 frame1에 넣기
lab1 = Label(frame1, text = "정답 또는 오답 버튼을 누르세요!", fg='blue', height=3)
lab1.pack()
# lab2와 lab3를 frame2에 넣기
lab2 = Label(frame2, text='총 문제 개수:')
lab2.pack(side='left')
lab3 = Label(frame2, textvariable = num_total)
lab3.pack(side='right')
# lab4, lab5, b1, b2를 frame3에 넣기
lab4 = Label(frame3, textvariable = num_good)
lab4.pack(side = 'left')
lab5 = Label(frame3, textvariable = num_bad)
lab5.pack(side = 'right')
b1 = Button(frame3, text = "정답", width = 7)
b1.pack(side = 'left', padx = 10, pady = 10)
b2 = Button(frame3, text = "오답", width = 7)
b2.pack(side = 'right', padx = 10, pady = 10)
app.mainloop()
위 프로그램을 실행하면 애초에 원했던 모양이 완성되어 보인다.
버튼을 누를 때 소리가 나거나 숫자가 변해야 하는 일이 발생하도록 하려면 원하는 기능을 수행하는 함수와 버튼을 서로 연결해야 한다.
예를 들어, 정답 버튼을 누를 때마다 정답 수가 1씩 증가해야 한다면 아래 함수를 호출하도록 하면 된다.
def play_correct_sound():
num_good.set(num_good.get() + 1)
num_total.set(num_total.get() + 1)
또한 오답 버튼을 누를 때마다 오답 수가 1씩 증가해야 한다면 아래 함수를 호출하도록 하면 된다.
def play_wrong_sound():
num_bad.set(num_bad.get() + 1)
num_total.set(num_total.get() + 1)
주의사항
IntVar
의 get
메소드는 현재 저장된 정수값을 리턴한다.따라서 예를 들어 아래 코드는 현재 정답수에 1을 더해 다시 num_good
에 저장하라는 명령문이다.
num_good.set(num_good.get() + 1)
num_bad
와 num_total
도 동일하게 작동한다.
이제 버튼을 누를 때 숫자가 변하도록 만들 수 있으며,
Button
클래스 생성자의 command
키워드 매개변수를 활용하면 된다.
from tkinter import *
app = Tk()
app.title("파이선 퀴즈 게임")
app.geometry('300x120+200+100')
num_good = IntVar()
num_good.set(0)
num_bad = IntVar()
num_bad.set(0)
num_total = IntVar()
num_total.set(0)
# 정답/오답 버튼을 누를 때 실행되는 함수 선언하기
def play_correct_sound():
num_good.set(num_good.get() + 1)
num_total.set(num_total.get() + 1)
def play_wrong_sound():
num_bad.set(num_bad.get() + 1)
num_total.set(num_total.get() + 1)
frame1 = Frame(app)
frame1.pack()
frame2 = Frame(app)
frame2.pack()
frame3 = Frame(app)
frame3.pack()
lab1 = Label(frame1, text = "정답 또는 오답 버튼을 누르세요!", fg='blue', height=3)
lab1.pack()
lab2 = Label(frame2, text='총 문제 개수:')
lab2.pack(side='left')
lab3 = Label(frame2, textvariable = num_total)
lab3.pack(side='right')
lab4 = Label(frame3, textvariable = num_good)
lab4.pack(side = 'left')
lab5 = Label(frame3, textvariable = num_bad)
lab5.pack(side = 'right')
# command 키워드 매개 변수에 특정 기능 추가하기
b1 = Button(frame3, text = "정답", width = 7, command = play_correct_sound)
b1.pack(side = 'left', padx = 10, pady = 10)
b2 = Button(frame3, text = "오답", width = 7, command = play_wrong_sound)
b2.pack(side = 'right', padx = 10, pady = 10)
app.mainloop()
위 프로그램을 실행하면 정답/오답 버튼을 누를 때 마다 세 개의 숫자가 적절하게 변한는 것을 확인할 수 있다.
play_correct_sound
와 play_wrong_sound
본문에 소리내는 부분은 포함되어 있지 않다.애플리케이션에서 소리를 내는 일은 간단하지 않다.
무엇보다도 윈도우, 맥, 리눅스 등 사용하는 운영체제마다 소리를 관리하는 방식이 다르기 때문이다.
다른 프로그래밍언어들도 비슷한 문제를 갖고 있으며,
파이썬의 경우에는 pygame
이라는 패키지를 추가로 설치해서 활용하여 소리를 낼 수 있는 방법이
가장 많이 활용된다.
주의: 패키지란?
pygame
이라는 디렉토리 안에 많은 파이썬 코드 파일이 저장되어 있다고 생각하면 된다.pygame
패키지 설치¶pygame
은 파이썬에서 기본적으로 제공하지 않는다.
따라서 파이썬 설치 이후에 추가로 설치해야 하며 pip
이라는 파이썬 패키지 관리자를 이용하면
설치가 매우 쉽다.
기본적으로 아래 과정을 따르면 된다.
pip install pygame
pygame.mixer
모듈 활용하기¶pygame
패키지(디렉토리) 안에 mixer
라는 모듈이 포함되어 있는데,
바로 이 모듈이 소리를 내는 다양한 기능을 담고 있다.
mixer
모듈을 활용하기 위해서는 아래 과정을 기본적으로 따르면 된다.
여기서는 굳이 다른 설명을 첨부하지 않는다.
import pygame.mixer as sounds
sounds.init()
그런 다음에 정답/오답 버튼을 누를 때 사용할 음악을 아래와 같이 지정한다.
correct_s = sounds.Sound("codes/hfprog/sounds/correct.wav")
wrong_s = sounds.Sound("codes/hfprog/sounds/wrong.wav")
주의:
correct.wav
와 wrong.wav
두 음원파일이 현재 작업 디렉토리(cwd)의
하위 디렉토리인 codes/hfprog/sounds
에 저장되어 있다고 가정한다.
다른 곳에 저장되어 있을 경우 경로를 적절하게 수정해야 한다.이제 play_correct_sound
와 play_wrong_sound
함수에
소리내는 기능도 추가할 수 있다.
def play_correct_sound():
num_good.set(num_good.get() + 1)
num_total.set(num_total.get() + 1)
correct_s.play()
def play_wrong_sound():
num_bad.set(num_bad.get() + 1)
num_total.set(num_total.get() + 1)
wrong_s.play()
이제 최종 코드를 살펴보자.
from tkinter import *
# pygame.mixer를 sounds라는 애칭으로 불러오기
# 애칭은 임의로 지정 가능
import pygame.mixer as sounds
# 정답/오답 소리 지정하기
sounds.init()
correct_s = sounds.Sound("codes/sounds/correct.wav")
wrong_s = sounds.Sound("codes/sounds/wrong.wav")
app = Tk()
app.title("파이선 퀴즈 게임")
app.geometry('300x120+200+100')
num_good = IntVar()
num_good.set(0)
num_bad = IntVar()
num_bad.set(0)
num_total = IntVar()
num_total.set(0)
# 정답/오답 버튼을 누를 때 실행되는 함수에 소리기능 추가
def play_correct_sound():
num_good.set(num_good.get() + 1)
num_total.set(num_total.get() + 1)
correct_s.play()
def play_wrong_sound():
num_bad.set(num_bad.get() + 1)
num_total.set(num_total.get() + 1)
wrong_s.play()
frame1 = Frame(app)
frame1.pack()
frame2 = Frame(app)
frame2.pack()
frame3 = Frame(app)
frame3.pack()
lab1 = Label(frame1, text = "정답 또는 오답 버튼을 누르세요!", fg='blue', height=3)
lab1.pack()
lab2 = Label(frame2, text='총 문제 개수:')
lab2.pack(side='left')
lab3 = Label(frame2, textvariable = num_total)
lab3.pack(side='right')
lab4 = Label(frame3, textvariable = num_good)
lab4.pack(side = 'left')
lab5 = Label(frame3, textvariable = num_bad)
lab5.pack(side = 'right')
# command 키워드 매개 변수에 특정 기능 추가하기
b1 = Button(frame3, text = "정답", width = 7, command = play_correct_sound)
b1.pack(side = 'left', padx = 10, pady = 10)
b2 = Button(frame3, text = "오답", width = 7, command = play_wrong_sound)
b2.pack(side = 'right', padx = 10, pady = 10)
app.mainloop()