본문 바로가기
GUI 튜토리얼/PySide6 # Qt의 원조가 돌아왔다!

1. 시그널-슬롯 응용예제(버튼에 클릭횟수 표시)와 클래스 기본개념

by 일코 2021. 3. 26.

지난 포스팅에서는 Signal&Slot의 기본 개념을 짧은 설명과 최소한의 코드를 통해 가볍게 설명을 드렸습니다. 지난 포스팅 마지막에, 시그널을 통해 변수의 값을 바꾼다든지, 인터페이스를 변경하거나, 혹은 위젯에서 슬롯을 거치지 않고 직접 다른 위젯에 영향을 주는 등의 응용방법이 있다는 설명을 드렸습니다.

 

1. PySide6의 Signal과 Slot에 관하여

시그널과 슬롯 짐작컨대 이 몇 개 안 되는 튜토리얼 시리즈를 따라오시면서, 여러분이 가장 많이 하셨던 생각 중 하나는, "그래서 이 버튼이 뭔가 액션을 하게 하려면 어떻게 해야 하나?" 일 거라

www.martinii.fun

이번 시간에는 그 활용예제 중 하나를 클래스를 곁들여 가볍게 다뤄보도록 하겠습니다. (메인 윈도우 디자인하다가 갑자기 Signal&Slot이 튀어나오더니 무슨 클래스냐는 분도 계실 것 같습니다. 기초이면서 중요한 문법 중 하나이므로 다소 생소하더라도 여기서 꼭 익혀두셔야 이어지는 포스팅에 적응하실 수 있습니다.)


1. 버튼 텍스트에 클릭횟수 넣어보기

이번 포스팅에서 첫 번째로 만들어 보고 싶은 기능은, 아래처럼 버튼 텍스트에 클릭횟수를 넣어보는 것입니다.

버튼 텍스트에 클릭횟수 삽입

button.setText 메서드를 사용할 예정이며, 클릭횟수는 click_count라는 속성을 미리 0으로 정의해놓습니다. 아래 코드를 참고해 주시기 바랍니다.

from PySide6.QtWidgets import QApplication, QMainWindow, QPushButton


class MainWindow(QMainWindow):
    def __init__(self):
        super(MainWindow, self).__init__()
        self.button = QPushButton(parent=self)
        self.setCentralWidget(self.button)
        self.button.setCheckable(True)
        self.button.clicked.connect(self.button_clicked)
        self.click_count = 0

    def button_clicked(self):
        self.click_count += 1
        self.button.setText(f"{self.click_count}번 클릭함")


app = QApplication()
window = MainWindow()
window.show()
app.exec_()
파이썬의 클래스 문법이 다소 생소하신 분도 있겠지만, 전체적으로 쭉 한 번 직접 타이핑하면서 코딩해보시고 나서 실행하신 후 아래 설명을 읽어주시기 바랍니다.

 

사실 PySide6 튜토리얼 시리즈를 시작하던 그 순간부터 우리는 클래스 문법을 사용해 오고 있었습니다. 아마 파이썬 기초문법을 배우던 시점부터였을 거라고 생각해요. 단지 우리가 "클래스 정의"를 하지 않았을 뿐이지요. 기존에 어떤 부분이 클래스 문법이었냐면,

 

window = QMainWindow() 라거나,

button = QPushButton(text="Click me!") 라든지,

window.show() 등의 코드입니다. (기억나시나요?)

 

위의 두 코드는 "인스턴스(붕어빵) 생성"이라는 문법으로, 비유하자면 일종의 "붕어빵틀"QMainWindowQPushButton으로부터 "붕어빵"windowbutton을 생성한 것입니다. 특히 button 붕어빵에는 기존 팥앙금에다 "text"라는 양념을 더 첨가했다고 보셔도 무방하겠습니다.

마지막으로 세 번째 코드인 window.show() 는 window 인스턴스의 show 메서드를 실행하는 코드입니다.

 

위에서 예로 보여드린 클래스나 메서드는 모두 누군가 이미 만들어놓은 "틀""기능"입니다. 여기다 우리가 원하는 기능을 하나 추가하고 싶을 때(기존의 모든 기능은 그대로 이용하면서 말이죠.) 종종 들어보셨을 "상속"이라는 개념을 도입하면 됩니다. 처음에 보여드린 예제코드를 다시 한 번 보시겠습니다.

QMainWindow를 상속한 MainWindow 클래스 정의


4번라인 : class MainWindow(QMainWindow):

이 부분이 기존의 클래스를 상속하여 새로운 클래스를 정의하는 코드입니다. 이 라인의 정확한 의미는 "QMainWindow라는 클래스를 상속하여 새롭게 MainWindow클래스를 정의하려고 한다." 입니다.

QMainWindow가 대체 어떤 부자;;이길래 얘를 상속한다는 거지?

QMainWindow의 실체를 확인하시려면 QMainWindow에다 캐럿을 놓고 Ctrl-b (Navigate) 를 눌러봅시다.

파이참의 Navigate(Ctrl-b)를 사용해 QMainWindow를 정의한 코드 읽어보기

꽤 많은 재산을 보유하고 있는 것 같긴 하네요.. 그럼 이 중에 예전에도 사용한 적이 있는, setCentralWidget 메서드를 찾아볼까요?

setCentralWidget이 135번 라인부터 정의되어 있습니다.

135~137번 라인이 setCentralWidget 메서드에 대한 정의입니다. 소스는 C++로 정의되어 있어서, 파이썬 코드로는 확인할 수 없지만 어떤 인자가 들어가는지 정도의 정보는 확인 가능합니다. 기왕 여기까지 들어온 거 메서드 하나만 더 찾아볼까요? 윈도우 타이틀을 정의하는 메서드인 setWindowTitle("My App")를 지난 포스팅에서 사용해봤는데, 기억나시나요? 그것도 한 번 찾아봅시다. 찾기창을 열어서(Ctrl-f) setWindowsTitle을 입력하면,

"0 results" ??? setWindowTitle이 정의되어 있지 않네요??

setWindowTitle이 정의되어 있지 않습니다. 왜 코드를 찾을 수 없을까요? 힌트는 바로 QMainWindow.py의 15번 라인에 있습니다.

QMainWindow도 QWidget을 상속한 클래스이기 때문입니다.

QMainWindow가 QWidget을 상속한 코드라는 걸 확인할 수 있습니다. 그러면 setWindowTitle 메서드는 QWidget(혹은 그 상위)클래스에서 상속받았겠군요? 한 번 확인해볼까요?

QWidget.py 안에 setWindowTitle이 정의되어 있는 걸 확인해봤습니다.

여기까지 설명드린 내용을 세 줄 요약하면,

1. 클래스는 붕어빵틀, 인스턴스는 붕어빵.

2. 일반적인 인스턴스 메서드는 인스턴스.메서드명()으로 접근할 수 있다.

3. QMainWindow는 (QWidget을 상속했기 때문에) QWidget 클래스의 모든 기능을 동일하게 사용할 수 있다.

 

이 아래부터는 다시 우리가 작성한 코드에 대한 설명을 이어가겠습니다.


5번라인: def __init__(self):

5번 라인은 "생성자" 라는 문법입니다. 붕어빵틀로 붕어빵을 만드는 시점에 무조건 실행해야 하는 액션이 있죠. 붕어빵으로 비유하자면, "뚜껑을 열어서, 반죽을 붓고, 앙금을 넣은 후 뚜껑을 닫는 액션" 정도가 되겠습니다. 이건 붕어빵을 만들 때마다 항상 실행해야 하니까, 아예 생성자 안에다가 넣어놓고 붕어빵 만들 때마다 반복해서 실행하는 겁니다. 그런 맥락에서, 이 라인 아래로 들여쓰기 된 6줄의 코드는 인스턴스를 생성하는 시점(19번라인)에 모두 실행됩니다. 참고로 괄호 안의 self나중에 생성될 인스턴스를 가리키는 변수명이 됩니다. 어떤 변수명이 와도 무방하지만, 파이썬에서는 관례적으로 self를 사용합니다. self라는 변수 덕분에 

MainWindow 생성자 내부의 6줄의 코드


6번 라인: super().__init__()

클래스를 상속받았을 때 신경써줘야 하는 부분 중 하나가 바로 상속해준 클래스의 생성자(__init__)를 실행해야 하냐는 부분입니다. 필요없는 경우도 물론 있겠지만, 대부분의 경우 상위클래스에서 정의된 __init__를 실행해줘야 정상적으로 프로그램이 구동됩니다. 여기서는 super(MainWindow, self).__init__()라고 작성되었지만, 짧게 super().__init__() 이라고 실행하셔도 무방합니다. 이 때 super()는 상위클래스를 의미합니다.


7번 라인: self.button = QPushButton()

self가 다시 등장했네요. MainWindow 인스턴스인 self(여기서는 window) 안에 QPushButton 인스턴스인 button 을 정의한다는 의미입니다. 참고로 여기에서 정의한 self.button이나 11번 라인의 self.click_count 등의 속성은 프로그램이 실행된 이후에는 내부적으로 각각 window.button과 window.click_count로 해석됩니다. 동일한 맥락에서 13번 라인, button_clicked 메서드 정의를 읽어보시면 self의 의미를 더 정확히 이해하실 겁니다.


9번라인 : self.button.setCheckable(True)

일반적인 버튼을 토글방식의 버튼으로 변경하는 코드입니다. 한 번 클릭하면 체크된 채로 유지되고, 다시 한 번 클릭하면 체크해제되는 방식으로요.


10번라인 : self.button.clicked.connect(self.button_clicked)

시그널-슬롯 및 연결(connect) 개념의 핵심입니다. 앞의 포스팅에서 설명드렸지만, self.button을 클릭할 때 발생하는 신호를 self.button_clicked로 전송하라는 의미입니다. 한 가지 재미있는 팁을 드리면, setCheckable이 실행된 상태에서는 .clicked 시그널 안에 True/False가 같이 전송됩니다. (다음 포스팅에서 이를 활용한 예제를 소개해 드리겠습니다.)


11번라인: self.click_count = 0

click_count는 인스턴스 변수입니다. 그냥 변수와 인스턴스변수의 차이를 설명드릴 때 다음의 예를 가장 많이 사용하는 것 같습니다. window라는 인스턴스창이 생성된 이후에, print(window.click_count)를 실행하면 0이 출력됩니다. 만약 window2라는 인스턴스를 MainWindow클래스를 통해 하나 더 생성하더라도, window2.click_count와 window.click_count는 별개입니다. window를 100개 만들더라도, 100개 창의 click_count는 모두 별개입니다. 인스턴스 개념이 아니라 전역변수 click_count를 정의한다면 이런 scalability를 구현하기는 거의 불가능할 것입니다. 


13번라인: def button_clicked(self):

button_clicked라는 이름의 인스턴스메서드를 정의합니다. 인스턴스메서드의 첫 번째 인자는 항상 self입니다. 이건 문법이므로 숙지하셔야 합니다. 또한 self.click_count나 self.button 등 인스턴스 속성이나 메서드에 자유롭게 접근할 수 있다는 특징이 있습니다. 클래스를 정의하지 않고는 구현할 수 없는 기능이라고 생각하셔도 좋습니다.


14번라인: self.click_count += 1

"+=" 연산자가 생소한 분도 있을 것 같습니다. a += 1 은 a = a + 1 이라는 의미와 동일합니다. 10번 라인에서 버튼을 클릭할 때마다 button_clicked 메서드가 실행되는데, 이를 통해 click_count의 숫자가 1씩 올라가는 겁니다.


15번라인: self.button.setText(f"{self.click_count}번 클릭함")

다시 한 번 self에 대해 말씀드리지만, 만약 이 부분에 "self."가 붙지 않고 그냥 button.setText라고 코드가 작성되어 있으면 창을 띄우는 데까지는 문제가 없지만, 버튼을 클릭하자마자 에러가 발생합니다. 왜냐면 우리가 접근하고 싶은 버튼은 self.button, 즉 window.button인데, 전역변수인 "button"은 정의하지 않았기 때문이죠.

name 'button' is not defined

이어서 f스트링 문법이 나오는데 1을 더한 self.click_count를 따옴표 안의 중괄호 안에다가 집어넣습니다.

지금까지의 코드를 다시 한 번 실행해보겠습니다.

코드 실행화면

이번 포스팅에서는 시그널-슬롯 개념에 대해 말씀드리면서 클래스와 인스턴스 개념을 곁들여 설명드려보았습니다. 객체지향(클래스) 문법이 절대 어렵거나 복잡하지 않으며, 오히려 코드를 쉽게 만들기 위해서 도입된 개념이라는 걸 생각하면, 아직 좀 생소하더라도 부담을 내려놓으시고 조금씩 클래스 문법을 사용해 보시는 것도 추천을 드립니다.

마치며(부록)

참고로 이번 예제에서는 메인윈도우 인스턴스 안에서 버튼 인스턴스를 생성했기 때문에 접근이 어려워 클래스를 사용할 수밖에 없었는데, QPushButton만 생성한다면 또 클래스를 정의하지 않고도 이런 기능은 간단히 구현이 가능합니다. 참고하시라고 아래에 영양가 없는 코드 및 실행화면을 보여드리면서 마칩니다.

사실 클래스를 정의하지 않아도 이런 기능 정도는 실행할 수 있다.

긴 글 읽어주셔서 감사합니다.

행복한 하루 보내시고, 다음 포스팅에서 뵙겠습니다.

 

 


donaricano-btn

댓글