본문 바로가기

PyQt GUI

PyQt GUI (12) 어플리케이션 만들기 (2) : 기억력 카드 게임 만들기

728x90

단순히 Layout이나 Widget의 사용법만을 익히면 좀 지루한 감이 있으니, 배운 내용을 바탕으로 의미있는 GUI 프로그램을 만들어 보는 시간을 갖도록 하겠습니다. 지난번에는 계산기 어플리케이션을 만들었는데, 이번 시간에는 기억력 카드 게임을 만들어 보겠습니다.

 

이 어플리케이션은 유투브 채널의 시청자님의 요청으로 만들게 되었습니다. 영상을 촬영하고 편집하는데는 시간이 좀 걸리니 우선, 바로 쓸 수 있는 블로그 포스팅 부터 하도록 하겠습니다. 

 

우선, 최종적으로 만든 어플리케이션을 보여드리면서 기억력 카드 게임이 무엇인지, 그리고 프로그램을 어떻게 사용하는지를 설명드리겠습니다. 

사용법은 아래와 같습니다.

 

(0) 첫째줄에는 6개의 동물 이미지가 있습니다. 사용자가 사용할 이미지를 준비해 놓고 해당 파일의 경로를 코드에 입력 합니다. 

(1) 첫째줄에 있는 6개의 이미지를 무작위로 선택합니다. 

(2) 이미지를 누를 때, 순서를 기억하고 있습니다. 

(3) 맨 아랫줄에 Solution 버튼을 클릭하면, 두 번째 줄에는 (1)에서 선택했던 동물 이미지가 순서대로 출력됩니다. 

    혹은 두 번째 줄에 빈 칸을 클릭하면, 해당 순서의 이미지가 출력됩니다. 

 

위와 같이 작동합니다. 영상이 아닌 단순히 이미지만을 보고 설명하긴 좀 어렵네요. 

(1)의 과정을 해 보도록 하겠습니다. 토끼 -> 사막여우 -> 코끼리 순서대로 클릭했습니다. 클릭이 된 사진은 다시 클릭 될 수 없기 때문에 흑백으로 변경 됩니다. 그리고 나머지 동물 사진을 선택해야 하는데, 고양이 -> 개 -> 호랑이 순으로 클릭 해 보겠습니다. 

네, 위와 같이 6개의 이미지의 선택이 끝났습니다. 선택이 끝나면, 어떤 순서대로 위 이미지를 선택했는지를 맞추면 되는데요, 기억력이 좋아야 맞출 수 있겠죠! 아래 하늘색으로 돼 있는 Solution을 클릭하면 두 번째 줄에 정답이 뜹니다. 

Reset, Solution 버튼에는 StyleSheet, hover 세팅을 해 두었기 때문에, 마우스 포인터를 해당 버튼 위에 올리면, 연한 노란색/하늘색이 진한 노란색/하늘색으로 하이라이트 표시 됩니다. Solution을 클릭하면,

위와 같이 두 번째 줄에 토끼 -> 사막여우 -> 코끼리 -> 고양이 -> 개 -> 호랑이 순서의 정답이 출력 됩니다. 문제를 다시 하고 싶을 때는 Reset 버튼을 클릭합니다. 그렇게 되면 프로그램의 첫 화면으로 돌아가게 됩니다.

 

Solution 버튼을 클릭하면 6개의 이미지가 한 번에 출력되는데요, 첫 번째 줄에서 6개의 이미지를 모두 선택하고 나서 두 번째 줄에서 빈칸을 클릭하면 해당 순서의 이미지가 하나 씩만 출력됩니다. 

위 예시는 두 번째 줄에서 앞에 있는 두 빈칸을 클릭하여 처음 두 개의 선택인 토끼 -> 사막여우 이미지만 출력이 된 것 입니다. Solution 버튼을 클릭하면 한 번에 답이 나오는 것이라면, 이렇게 하나씩 카드를 오픈 하여 일종의 힌트를 줄 수 도 있습니다. 

 

프로그램의 사용법 설명 및 실제 사용 예시 설명은 끝 났습니다. 이제 이 프로그램을 어떻게 PyQt를 통해서 만들 수 있는지를 확인해 보도록 하겠습니다. 우선 코드 전체는 아래와 같습니다. 

import sys
from PyQt5.QtGui import *
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *

class QPushButtonIcon(QPushButton):
    def __init__(self, parent = None):
        super().__init__(parent)
        self.setFixedHeight(200)
        self.setFixedWidth(200)
        self.setIconSize(QSize(192, 192))

class QPushButtonReset(QPushButton):
    def __init__(self, parent = None):
        super().__init__(parent)
        self.setFixedHeight(50)
        font = QFont("Helvetica", 12)
        font.setBold(True)
        self.setFont(font)

class QPushButtonSolution(QPushButton):
    def __init__(self, parent = None):
        super().__init__(parent)
        self.setFixedHeight(50)
        font = QFont("Helvetica", 12)
        font.setBold(True)
        self.setFont(font)

class Main(QDialog):
    def __init__(self):
        super().__init__()
        self.set_default()
        self.set_style()
        self.init_ui()

    def set_default(self):
        self.selection_list = []
        self.figures = ['cat.jpg', 'dog.jpg', 'rabbit.jpg', 'fox.jpg', 'elephant.jpg', 'tiger.jpg']

        self.icons = {}
        for index, filename in enumerate(self.figures):
            pixmap = QPixmap(filename)
            pixmap = pixmap.scaled(200, 200, Qt.IgnoreAspectRatio)
            icon = QIcon()
            icon.addPixmap(pixmap)
            self.icons[index] = icon

    def set_style(self):
        with open("style", 'r') as f:
            self.setStyleSheet(f.read())

    def init_ui(self):
        main_layout = QVBoxLayout()

        layout_1 = QHBoxLayout()
        layout_2 = QHBoxLayout()
        layout_3 = QVBoxLayout()

        self.qbuttons = {}
        for index, icon in self.icons.items():
            button = QPushButtonIcon()
            button.setIcon(icon)
            button.clicked.connect(lambda state, button = button, idx = index :
                                   self.qbutton_clicked(state, idx, button))
            layout_1.addWidget(button)
            self.qbuttons[index] = button

        self.sbuttons ={}
        for index in range(len(self.figures)):
            button = QPushButtonIcon()
            self.sbuttons[index] = button
            button.clicked.connect(lambda state, button = button, idx = index:
                                   self.sbutton_clicked(state, idx, button))
            layout_2.addWidget(button)

        self.button_reset = QPushButtonReset("Reset")
        self.button_reset.clicked.connect(self.action_reset)

        self.button_solution = QPushButtonSolution("Solution")
        self.button_solution.clicked.connect(self.action_solution)

        layout_3.addWidget(self.button_reset)
        layout_3.addWidget(self.button_solution)

        main_layout.addLayout(layout_1)
        main_layout.addLayout(layout_2)
        main_layout.addLayout(layout_3)

        main_layout.addLayout(main_layout)

        self.setLayout(main_layout)
        self.setFixedSize(main_layout.sizeHint())
        self.setWindowTitle("Memory Game")
        self.show()


    def qbutton_clicked(self, state, idx, button):
        self.selection_list.append(idx)
        button.setDisabled(True)

    def sbutton_clicked(self, state, idx, button):
        if len(self.selection_list) > idx:
            self.set_button_selected_index(button, idx)

    def set_button_selected_index(self, button, idx):
        sol_index = self.selection_list[idx]
        icon = self.icons[sol_index]
        button.setIcon(icon)

    def check_all_selected(self):
        return len(self.selection_list) == len(self.figures)

    def action_solution(self):
        if self.check_all_selected():
            for index, button in self.sbuttons.items():
                self.set_button_selected_index(button, index)

    def action_reset(self):
        self.selection_list = []
        for button in self.qbuttons.values():
            button.setDisabled(False)
        for button in self.sbuttons.values():
            button.setIcon(QIcon())

if __name__ == '__main__':
    app = QApplication(sys.argv)
    main = Main()
    sys.exit(app.exec_())

공백을 포함하여 128 라인으로 작성하였습니다. 좀 더 효율적으로 코딩을 하면 아마도 100 라인 안으로 작성할 수 있을 것 같습니다. 

 

지금까지 소개했던 위젯, 레이아웃, 스타일시트를 잘 적용하면 쉽게 만들 수 있는 프로그램 입니다. 물론 기억력 게임을 구현하기 위한 아주 약간의 알고리듬도 필요 합니다. 

 

이 프로그램에서 가장 중요한 구성 요소는 QPushButton 위젯 입니다. 위 프로그램 이미지만 봐서는 PushButton은 맨 아래에 있는 Reset, Solution 버튼이 전부인것 처럼 보이겠지만, 사실 동물 이미지를 표현하는 것은 모두 PushButton으로 구현한 것 입니다. 바로 이전 포스팅에서 소개했던 PushButton에 Icon을 적용하여 이미지를 표시하였습니다. 즉, Icon에 동물 이미지를 설정하고, PushButton에 Icon을 설정한 것 입니다. Icon의 크기와 PushButton의 크기를 같게 하여 (정확히는 PushButton의 테두리 굵기 만큼 크기가 다릅니다), 버튼 전체에 Icon, 즉 이미지가 표시 되도록 한 것 입니다. 

 

버튼을 클릭했을 때, 버튼에 설정된 아이콘의 이미지가 흑백으로 변하는 것은 Icon과 PushButton이 아주 단순한 기능인 setDisabled(True)를 이용한 것 입니다. 

 

아래에서 코드의 부분부분을 위에서 부터 차례로 설명하도록 하겠습니다. 

class QPushButtonIcon(QPushButton):
    def __init__(self, parent = None):
        super().__init__(parent)
        self.setFixedHeight(200)
        self.setFixedWidth(200)
        self.setIconSize(QSize(192, 192))

class QPushButtonReset(QPushButton):
    def __init__(self, parent = None):
        super().__init__(parent)
        self.setFixedHeight(50)
        font = QFont("Helvetica", 12)
        font.setBold(True)
        self.setFont(font)

class QPushButtonSolution(QPushButton):
    def __init__(self, parent = None):
        super().__init__(parent)
        self.setFixedHeight(50)
        font = QFont("Helvetica", 12)
        font.setBold(True)
        self.setFont(font)

Customized 위젯을 생성하는 부분입니다. QPushButtonIcon 위젯이 이 프로그램의 첫 번째, 두 번째 줄에 있는 이미지를 출력하는데 사용한 PushButton의 class 입니다. setFixedHeight/Width를 이용해서 버튼의 크기를 200px * 200px 로 고정하였습니다. setIconSize를 통해서 이 PushButton에 들어가는 Icon의 크기를 192px * 192px 로 설정하였습니다. 나중에도 설명하겠지만, PushButton에 hover 상태가 될 때, 마우스 포인터가 있는 PushButton을 강조하기 위해서 스타일시트에서 hover를 설정해 주는데, 이 때문에 Icon의 사이즈를 200px * 200px 로 하지 않고, 192px * 192px 로 설정하였습니다.

 

아래의 QPushButtonReset, QPushButtonSolution은 각각 프로그램의 아랫 부분에 있는 Reset, Solution 버튼을 위해 만들어진 class입니다. 단 한 번씩 밖에 사용되지 않기 때문에, 굳이 class를 만들 필요까지는 없지만, 나중에 스타일시트에서 쉽게 스타일을 변경하기 위해서 class를 따로 만들어 주었습니다. 위젯의 높이 설정과 폰트 설정을 추가로 해 주었습니다. 이것들은 이미 지난 포스팅에서 설명한 바 있습니다.

class Main(QDialog):
    def __init__(self):
        super().__init__()
        self.set_default()
        self.set_style()
        self.init_ui()

메인 클래스를 실행하면, set_default(), set_style(), init_ui() 메서드가 실행 됩니다. 각각

 

set_default() : 동물 이미지 load, 동물 이미지에 해당하는 Icon 위젯 생성

set_style() : 스타일시트 적용

init_ui() : 전체 UI 생성 (레이아웃, 위젯 생성)

 

의 역할을 합니다. 

    def set_default(self):
        self.selection_list = []
        self.figures = ['cat.jpg', 'dog.jpg', 'rabbit.jpg', 'fox.jpg', 'elephant.jpg', 'tiger.jpg']

        self.icons = {}
        for index, filename in enumerate(self.figures):
            pixmap = QPixmap(filename)
            pixmap = pixmap.scaled(200, 200, Qt.IgnoreAspectRatio)
            icon = QIcon()
            icon.addPixmap(pixmap)
            self.icons[index] = icon

set_default() 메서드 입니다. self.selected_list 는 첫 번째 라인에서 동물 이미지를 선택할 때 (정확히는 동물 이미지를 Icon으로 설정한 PushButton을 클릭할 때), 선택된 순서를 저장하기 위해 만들어진 list 입니다. 

 

self.figures는 동물 이미지 파일의 경로 입니다. self.figures의 순서대로 동물의 순서가 지정됩니다. 

 

0 : 개

1 : 고양이 

2 : 토끼

3 : 사막여우

4 : 코끼리 

5 : 호랑이

 

순서입니다. self.figures에 입력하는 이미지의 숫자 만큼이 프로그램에서 사용됩니다. 위 프로그램의 이미지에서 볼 수 있듯 이미지는 200px * 200px 크기의 정사각형으로 변형됩니다. 따라서 원래 이미지 파일 역시 정사각형 형태이어야 크기를 변경했을 때 모양이 이상해 지지 않습니다. 물론 전체 위젯이나 레이아웃의 크기를 변경하여 이미지를 크거나 작게 만들 수 있습니다. 

 

cat.jpg
0.03MB
dog.jpg
0.01MB
elephant.jpg
0.08MB
fox.jpg
0.05MB
rabbit.jpg
0.00MB
tiger.jpg
0.01MB

원하는 이미지를 아무거나 사용해도 되지만, 제가 사용한 이미지는 위와 같습니다. 

 

아래에서는 각 동물 이미지를 설정한 Icon을 생성합니다. 지난 포스팅에서 알아본 바와 같이 QPixmap과 QIcon을 이용하여 동물 이미지를 이용하는 Icon 위젯을 생성합니다. 이 Icon 위젯은 나중에 여러번 사용될 것 이기 때문에 "순서:Icon 위젯" 형식의 dictionary에 저장 해 놓습니다. 

    def set_style(self):
        with open("style", 'r') as f:
            self.setStyleSheet(f.read())

set_style() 메서드 입니다. 스타일시트가 정의된 style 이라는 파일을 읽어와서 스타일시트를 적용하는 역할을 합니다. 특별한건 없습니다. 

    def init_ui(self):
        main_layout = QVBoxLayout()

        layout_1 = QHBoxLayout()
        layout_2 = QHBoxLayout()
        layout_3 = QVBoxLayout()

        self.qbuttons = {}
        for index, icon in self.icons.items():
            button = QPushButtonIcon()
            button.setIcon(icon)
            button.clicked.connect(lambda state, button = button, idx = index :
                                   self.qbutton_clicked(state, idx, button))
            layout_1.addWidget(button)
            self.qbuttons[index] = button

    def qbutton_clicked(self, state, idx, button):
        self.selection_list.append(idx)
        button.setDisabled(True)

항상, 가장 중요한 init_ui() 메서드 입니다. 이 메서드에서 모든 위젯과 레이아웃을 생성하고 배치합니다. 

 

layout_1은 첫째 줄의 PushButton, layout_2는 둘째 줄의 PushButton, layout_3은 마지막 Reset Solution 버튼을 배치하기 위해서 준비된 레이아웃 입니다. 

 

이미지를 선택하는 첫 번째 줄의 6개의 PushButton을 생성하고, 순서대로 0 ~ 5 번에 해당하는 Icon을 버튼에 설정합니다. 그런 다음 각 버튼을 클릭했을 때, qbutton_clicked가 실행 되도록 설정합니다. 각 버튼들을 self.qbuttons dictionary에 담아 둡니다. 나중에도 사용하기 위함 입니다. 

 

qbutton_clicked() 메서드에서는 클릭된 버튼의 인덱스를 self.selection_list 리스트에 원소로 추가합니다. 예들들어서 토끼 그림 아이콘이 설정된 세 번째 PushButton이 클릭 되면 self.selection_list에는 숫자 토끼 그림 아이콘의 번호인 2가 추가 됩니다. 그런 다음에는 해당 PushButton을 Disabled상태로 설정합니다. 이 경우 앞에서 설명한대로, 이 PushButton에 설정된 Icon의 이미지가 흑백으로 변경되면고, 버튼은 클릭을 할 수 없는 비활성화 상태로 변경 됩니다. 

        self.sbuttons ={}
        for index in range(len(self.figures)):
            button = QPushButtonIcon()
            self.sbuttons[index] = button
            button.clicked.connect(lambda state, button = button, idx = index:
                                   self.sbutton_clicked(state, idx, button))
            layout_2.addWidget(button)

    def sbutton_clicked(self, state, idx, button):
        if len(self.selection_list) > idx:
            self.set_button_selected_index(button, idx)
            
    def set_button_selected_index(self, button, idx):
        sol_index = self.selection_list[idx]
        icon = self.icons[sol_index]
        button.setIcon(icon)

두 번째 줄의 6개의 PushButton을 생성하는 부분입니다. 첫 번째 줄의 PushButton에는 아이콘을 설정해 주었지만, 두 번째 줄에 있는 PushButton은 생성만 하고 아이콘 설정은 해 주지 않습니다. 나중에 답을 출력하는 단계에서 아이콘 설정을 통해 이미지를 출력하면 되는데, 이것은 sbutton_clicked() 메서드를 통해서 구현 됩니다. 

 

set_button_selected_index() 메서드는 첫 번째 줄에서 선택된 동물 이미지의 숫자가, 두 번째 줄에서 클릭된 버튼의 순서보다 많은 경우, 두 번째 줄의 PushButton에 Icon을 설정하여 답이 되는 동물 이미지를 출력하는 부분입니다. 처음 보면 약간 복잡하다고 생각할 수 있는데, self.selection_list에 저장된 순서를 불러와서, 순서에 맞는 동물 Icon을 설정하는 것이 전부입니다. 

 

        self.button_reset = QPushButtonReset("Reset")
        self.button_reset.clicked.connect(self.action_reset)

        self.button_solution = QPushButtonSolution("Solution")
        self.button_solution.clicked.connect(self.action_solution)

        layout_3.addWidget(self.button_reset)
        layout_3.addWidget(self.button_solution)

        main_layout.addLayout(layout_1)
        main_layout.addLayout(layout_2)
        main_layout.addLayout(layout_3)

        main_layout.addLayout(main_layout)

        self.setLayout(main_layout)
        self.setFixedSize(main_layout.sizeHint())
        self.setWindowTitle("Memory Game")
        self.show()
        
    def check_all_selected(self):
        return len(self.selection_list) == len(self.figures)

    def action_solution(self):
        if self.check_all_selected():
            for index, button in self.sbuttons.items():
                self.set_button_selected_index(button, index)

    def action_reset(self):
        self.selection_list = []
        for button in self.qbuttons.values():
            button.setDisabled(False)
        for button in self.sbuttons.values():
            button.setIcon(QIcon())

Reset, Solution 버튼을 만드는 부분, 앞에서 만든 모든 위젯과 레이아웃을 main_layout에 추가하는 부분입니다. 위젯, 레이아웃을 만드는 것은 쉽고, 아래에 있는 메서드의 역할을 이해하면 됩니다. 

 

check_all_selected() 메서드는 첫 번째 줄에 있는 6개의 PushButton이 모두 선택되었는지 아닌지를 확인하는 메서드 입니다. Solution 버튼을 클릭하여 답을 출력하기 위해서는 우선 첫 번째 줄에서 모든 동물 이미지가 선택이 되었는지를 확인해야 합니다. 

 

action_solution() 메서드는 Solution 버튼을 클릭했을 때 호출 됩니다. 두 번째 줄에 있는 PushButton에 순서에 맞게 Icon을 설정하는 역할을 합니다. 

 

action_reset() 메서드는 Reset 버튼을 클릭했을 때 호출 됩니다. 모든것을 초기화 시키는 거라고 생각하면 되는데, 초기화 상태에서는 선택된 이미지가 없기 때문에, self.selection_list를 빈 list로 만들어 줍니다. 그리고 첫 번째 줄의 PushButton은 모두 활성화 상태로 설정을 하고, 두 번째 줄에 있는 PushButton은 모두 아이콘이 설정되지 않는 PushButton으로 만들어 줍니다. 아이콘이 설정되지 않는 PushButton은 빈 아이콘을 설정하면 됩니다. 

 

QDialog{
background:white;
}

QPushButtonIcon{
border:4px solid rgb(0, 255, 0, 0.3);
border-radius:8px;
background:white;
}

QPushButtonIcon:hover{
border:4px solid rgb(0, 255, 0, 1.0);
}

QPushButtonReset{
background-color:rgb(247, 230, 0, 0.5);
color:black;
border-radius:5px;
}

QPushButtonReset:hover{
background-color:rgb(247, 230, 0, 1.0);
}

QPushButtonSolution{
background-color:rgb(80, 188, 223, 0.5);
color:black;
border-radius:5px;
}

QPushButtonSolution:hover{
background-color:rgb(80, 188, 223, 1);
}

다음은 스타일시트 파일 style 입니다. 앞서 설명한 Customized된 PushButton 위젯의 스타일 설정입니다. 특별한 것은 없고, 마우스 포인터를 각 버튼에 가져갔을 때, 활성화를 표헌하기 위해서 hover 설정을 따로 해 주었습니다. 

 

이 프로그램은 기능성을 위주로 만들어졌습니다. 보다 이쁜 UI를 만들기 위해서는 위젯의 크기나 배치를 다시 해 주어야 할 것 입니다. 또한 스타일시트를 좀 더 다양하고 이쁘게 설정할 수 도 있을 것 입니다. 

728x90