Python 사용자 정의 신호 (User defined signal)

개요

코드를 만들다 보면 버튼을 눌렀을때 어떤 함수가 호출되는 경험을 해 본 적이 있을 것입니다. 

여기서 버튼을 누르는 행위는 이벤트 발생이고, 이때 호출되는 함수는 콜백함수 (Callback Function)라고 합니다. 

신호, 콜백함수?

Python, PyQt6 코드 예시를 볼까요.

from PyQt6.QtWidgets import QApplication, QWidget, QPushButton                           
import sys

class Window(QWidget):

    def __init__(self):
        super().__init__()
        btn = QPushButton('Hi', self)

        #signal
        btn.clicked.connect(self.onClick)

    def onClick(self):
        print('Hello')

if __name__ == '__main__':
    app = QApplication(sys.argv)
    w = Window()
    w.show()
    sys.exit(app.exec())

이 코드는 "Hi" 버튼을 누르면 "Hello"라는 문구가 출력됩니다.

여기서 11번째 줄 "clicked" 라는 친구가 신호(Signal)이고 "self.onClick" 은 신호가 발생했을 때 호출되는 콜백함수 입니다.

그럼 11번 라인의 "btn.clicked.connect(self.onClick)" 은 "버튼이 클릭되면 self.onClick함수를 호출해 줄께" 라는 의미이죠.

Qt에서는 이 콜백함수를 Slot 이라고 표현합니다.

 

Siganl 연결위치

위 코드를 잘 살펴보면 "btn.clicked.connect(self.onClick)" 신호연결 코드가 생성자 함수에 위치함을 알 수 있습니다.

그 이유는 생성자함수는 해당 클래스의 객체가 생성될때 최초 1회만 호출되므로 무언가를 초기화하기에 가장 좋은 곳입니다.

따라서 버튼과 콜백함수를 연결하는 작업은 이때 1회만 일어나야 하므로 이곳이 가장 적절합니다. 보통 UI 관련 객체들이 생성될 때 시그널을 연결해 줍니다.

만약 신호연결코드를 2회 작성하면 버튼을 한번 눌러도 "Hello" 는 2회 출력되게 됩니다.


사전등록된 콜백함수

앞서 콜백함수는 신호가 발생되었을때 호출되는 함수라고 설명하였습니다.

그런데 "clicked"라는 신호는 이미 Qt에 정의되어 있는 신호 (predefined signal) 이며 "self.onClick" 콜백 함수는 제가 정의한 함수입니다.

"clicked" 라는 신호는 이미 Qt QPushbutton Class에 선언되어 있으므로 다른 이름을 쓸 수 없지만, self.onClick 함수는 내맘대로 이름을 바꾸어도 됩니다.

Qt QPushbutton 도움말을 한번 볼까요.

(정확히는 QPushButton의 부모클래스 QAbstractButton에 존재)

출처 : Qt Doc. QAQAbstractButton

이미 QPushbutton class에 "clicked" 라는 신호가 미리 선언되어 있음을 알 수 있습니다.

그래서 우리가 바로 사용이 가능했던 것입니다. 

그럼, 내가 신호를 만들 수는 없을까요? 🤔

예를 들면 사용자가 1을 입력하면 사용자정의신호인 "번역"을 발생시켜 one으로 출력(콜백함수)하는 느낌이라고 할까요.

이제 신호와 콜백함수의 개념이 조금 이해가 되었다면 이 글의 주제인 "사용자 정의 신호" 에 대해 살펴볼 차례입니다.


 

사용자 정의 신호

말 그대로 이미 선언된 or 만들어진 신호가 아니라 내가 필요한 신호를 만들고 필요할때 이벤트를 발생시키는 것을 의미합니다.

아래 상황을 가정해 보겠습니다.

버튼이 눌러지면 "작아져" 라는 사용자 정의 신호가 발생되고, 위젯이 작아지는 콜백함수를 호출해 창을 작게 만드는 것이죠.

먼저 코드부터 살펴보겠습니다.

from PyQt6.QtWidgets import QApplication, QWidget, QVBoxLayout, QPushButton        
from PyQt6.QtCore import pyqtSignal
import sys

class Window(QWidget):

    #1.define user-defined signal
    my_signal = pyqtSignal(str)

    def __init__(self):
        super().__init__()
        btn = QPushButton('Hi', self)

        #signal
        btn.clicked.connect(self.onClick)

        #2.connect user defined siganl
        self.my_signal.connect(self.onMySlot)

    def onClick(self):
        print('Hello')
        #3.emit user defined signal
        self.my_signal.emit("This is callback by my_siganl")

    def onMySlot(self, msg):
        print(msg)
        self.resize(200,100)

if __name__ == '__main__':
    app = QApplication(sys.argv)
    w = Window()
    w.show()
    sys.exit(app.exec())
    

이 코드는 버튼이 눌러지면 위젯의 크기가 변하고 "This is callback by my_siganl" 라는 메시지를 출력합니다.

어떻게 사용자 정의 신호를 만들고 연결하고 보내는지 살펴보겠습니다.

1. pyqtSiganl 클래스 불러오기

from PyQt6.QtCore import pyqtSignal

사용자 정의 신호를 생성할 수 있는 Class Import


2. 사용자정의신호 선언

my_signal = pyqtSignal(str)

클래스 변수로 my_signal을 선언하고 전달인자로 str 타입을 보낼 계획.

만약 숫자와 문자를 2개 보낸다면 아래와 같이 선언 (변수이름이 아닌 타입을 선언)

my_signal = pyqtSignal(int, str)

 

3. 신호생성 및 연결

self.my_signal.connect(self.onMySlot)

인스턴스(멤버) 변수로 self.my_signal을 만들고 onMySlot() 콜백, 즉 Slot 함수와 연결

 

4. 사용자정의신호 보내기

self.my_signal.emit("This is callback by my_siganl")

emit() 함수를 통해 신호를 보내면 3번에서 해당 신호와 연결된 콜백함수인 onMySlot() 이 호출


위 내용이 PyQt에서 User Defined Signal을 사용하는 일반적인 절차입니다.

사용자 정의 신호는 3단 콤보 이것만 기억하세요! 🤫

신호생성(pyqtSignal) ➡️ 신호연결(connect) ➡️ 신호보내기(emit)

 


왜 클래스변수로 선언하나?

앞서 사용자정의신호를 간단히 설명하기 위해 언급하지 않은 부분을 좀 더 자세히 설명해 보려 합니다.

잘 이해가지 않아도 위 예시를 보고 사용자정의신호를 "생성, 연결, 보내기" 하는 3단계를 구성할 수 있다면 그냥 넘어가도 무방합니다.

바로 아래의 두가지 부분 입니다.

my_signal = pyqtSignal(str) # 클래스 변수선언

self.my_signal.connect(self.onMySlot) # 인스턴스변수 선언

왜 클래스변수로 선언된 것을 사용하면 될터인데 멤버, 즉 인스턴스(객체) 변수로 따로 또 선언할까요.

그 이유는,

my_signal은 클래스 변수로 선언되었지만, 신호와 콜백(슬롯)을 연결하는 작업은 인스턴스의 상태에 맞게 각각의 객체에서 이루어져야 하기 때문에 생성자에서 따로 선언하는 것입니다.

 

좀 더 부연설명하면,

1. 신호 그 자체는 모든 객체가 공유

pyqtSignal이 클래스 변수로 대표 정의되면, 모든 객체가 공유하는 하나의 신호입니다. 즉, my_signal은 신호를 만들 수 있는 붕어빵의 틀 개념입니다 .

2. 슬롯 연결은 인스턴스별로 이루어져야 하기 때문

하지만 my_signal.connect(self.onMySlot)은 인스턴스에서 수행되어야 합니다. 각 인스턴스가 자신의 신호와 슬롯을 연결해야만 그 인스턴스에서 신호가 방출될 때 연결된 슬롯이 동작하기 때문입니다.

만약 my_signal을 클래스 변수로 선언하고, connect를 클래스 차원에서만 하게 된다면, 모든 인스턴스에서 동일한 신호를 처리하게 되어버립니다. 이는 의도하지 않은 결과를 초래할 수 있습니다.

3. 인스턴스별 동작을 보장하기 위함

생성자에서 self.my_signal.connect(self.onMySlot)을 호출하면, 이 신호와 슬롯 연결은 해당 인스턴스에 대해서만 유효하게 됩니다. 만약 클래스 차원에서 connect를 호출하게 된다면, 모든 인스턴스가 동일한 슬롯을 연결받게 되어 각기 다른 동작을 처리하는 데 문제가 발생할 수 있습니다.

이상으로 모든 설명을 마칩니다.

감사합니다.

댓글

이 블로그의 인기 게시물

Qt Designer 설치하기

PyQt5 기반 동영상 플레이어앱 만들기