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

[PySide6] QLabel을 클릭가능하게!?

by 일코 2021. 8. 11.

지금까지의 내용을 요약하면,

안녕하세요? QLabel에 대한 설명을 간단하게 세 개의 포스팅에 걸쳐서 설명드렸습니다.

지금까지의 골자는, 기본적으로 이미 만들어진 틀(클래스)을 가져다 쓰기만 하면 되기 때문에(사실 그런 예제 밖에 안 보여드렸고...), 어떻게 임포트하고 어떻게 상속해서 어떻게 배치하는지, 이 정도만 알면 Qt GUI를 짜는 데 큰 걸림돌이 없다는 것이었습니다. 그런데...

 

이번 시간에는

그런데 이번 시간에는 진지하게 나름 상급기술을 하나 짚고 넘어가려고 합니다.

바로 QLabel에 클릭이벤트 붙이기!

전문용어로는, "클릭시그널이 없는 위젯에 클릭시그널 넣기" 정도?

영어로는 깔끔하게 "Making non-clickable widgets clickable"이라고 표현하네요.

조금 다뤄보시면 아시겠지만, QLabel에 기본적으로 지정된 시그널은 linkActivated와, linkHovered 둘 밖에 없거든요. 그런데, clicked를 사용하고 싶으면 어떻게 해야 할까요? 예를 들어, QLabel에 pixmap을 사용해서 이미지를 넣었는데, 이미지를 클릭하면 다른 이미지가 뜨게, 이렇게는 어떻게 표현할 수 있을까요?

(이 튜토리얼을 진행하기 전에, 이미지 두 장을 미리 준비하시면 좋겠습니다.)

 

방법은 크게 두 가지입니다.

1. 꼼수 같지만 간단한 방법 : mousePressEvent 사용

우선 기본적인 QLabel 코드는 이렇습니다. 빈 줄과 주석 빼면 12줄 정도로 짧습니다.

from PySide6.QtWidgets import QApplication, QLabel, QMainWindow


class MainWindow(QMainWindow):
    def __init__(self):
        super(MainWindow, self).__init__()
        # self.setWindowTitle("아이들 사진")
        widget = QLabel()
        widget.setPixmap(r"C:\Users\smj02\Desktop\photo_change\이미지 400.png")
        widget.setScaledContents(True)
        self.setCentralWidget(widget)


app = QApplication()
window = MainWindow()
window.show()
app.exec()

실행결과는 아래와 같습니다.

제가 작성하려고 하는 건, 우측에 나타난 사진 클릭시 다른 사진이 뜨게 하는 코드입니다.

예전 포스팅에, 버튼 시그널 중에는 clicked가 있어서, self.button.clicked.connect(함수)로 간단히 작성했는데요(링크). 위의 경우 말고, clicked 시그널이 없는 위젯, 예를 들면 QLabel 등에서는 clicked 시그널을 어떻게 구현할 수 있을까요?

우선 완성된 코드와 실행결과를 먼저 보여드리고 부연설명 드리겠습니다.

아래는 코드입니다.

from PySide6.QtWidgets import QApplication, QLabel, QMainWindow


class MainWindow(QMainWindow):
    def __init__(self):
        super(MainWindow, self).__init__()
        self.widget = QLabel()
        self.widget.setPixmap(r"C:\Users\smj02\Desktop\photo_change\이미지 400.png")
        self.widget.setScaledContents(True)
        self.setCentralWidget(self.widget)
        self.origin_photo = True  # 추가
        self.widget.mousePressEvent = self.photo_toggle  # 추가

    def photo_toggle(self, event):  # 추가
        if self.origin_photo:
            self.widget.setPixmap(r"C:\Users\smj02\Desktop\photo_change\이미지 401.png")
            self.origin_photo = False
        else:
            self.widget.setPixmap(r"C:\Users\smj02\Desktop\photo_change\이미지 400.png")
            self.origin_photo = True


if __name__ == '__main__':
    app = QApplication()
    window = MainWindow()
    window.show()
    app.exec()

아래는 실행결과입니다.

mousePressEvent를 정의하여 시그널-슬롯 대체

아래 간단히 라인별로 부연설명 드리겠습니다.

[13] self.widget.mousePressEvent = self.photo_toggle

클릭(정확히는 누르는) 시점에 호출되는 이벤트를 설정하였습니다. (생각보다 간단한데 이게 거의 전부입니다.) 설정한 함수는 제가 임의로 작성한 photo_toggle 함수입니다.

참고로 마우스 관련한 이벤트는 아래와 같습니다. Press 외에도 Release, DoubleClick 심지어 Move 이벤트까지 있네요.

위젯의 마우스 관련 이벤트들

한 가지 눈여겨 봐야 할 것은, 우리가 사용할 예정인 mousePressEvent 메서드는 self 외에도 ev(이벤트)라는 인자를 받아야 한다고 표시되어 있네요? ev는 어차피 무시할 거지만, photo_toggle이라는 함수를 만들 때 인자도 두 개를 넣어줘야 오류가 발생하지 않습니다.

[15~21]

photo_toggle 정의

상단의 메인윈도우 생성자(__init__)에서 self에 origin_photo라는 bool 변수를 정의해놓았습니다. 원래사진이면 origin_photo == True, 바뀐 사진이면 False로 써두고, origin_photo 상태에 따라서 다른 사진을 불러오려고 합니다. 코드 자체는 간단하죠? 맨 윗줄 괄호 안에 (self, event): 라고 인자가 두 개 들어있습니다. 이건 mousePressEvent가 두 개의 인자를 받아야 하는 메서드이기 때문입니다. (실제로 photo_toggle 함수 어디에도 event 파라미터를 사용하지는 않았습니다.)

 

번외로 좀 더 고전적인(지난 포스팅에서 주로 활용한) 방법을 쓰자면, 푸쉬버튼을 이미지 하단에 넣는 방법이 있죠.

쿨하지 않은 방법이지만, 위 코드와 비교하며 시그널-슬롯 부분만 참고하시기 바랍니다.

from PySide6.QtWidgets import QApplication, QMainWindow, QLabel, QWidget, \
    QVBoxLayout, QPushButton


class MainWindow(QMainWindow):
    def __init__(self):
        super(MainWindow, self).__init__()
        self.widget = QLabel()
        self.widget.setPixmap(r"C:\Users\smj02\Desktop\photo_change\이미지 400.png")
        self.widget.setScaledContents(True)
        self.button = QPushButton(text="클릭")

        layout = QVBoxLayout()
        layout.addWidget(self.widget)
        layout.addWidget(self.button)
        container = QWidget()
        container.setLayout(layout)
        self.setCentralWidget(container)

        self.origin_photo = True
        self.button.clicked.connect(self.button_clicked)  # 여기만 참고

    def button_clicked(self):
        if self.origin_photo:
            self.widget.setPixmap(r"C:\Users\smj02\Desktop\photo_change\이미지 401.png")
            self.origin_photo = False
        else:
            self.widget.setPixmap(r"C:\Users\smj02\Desktop\photo_change\이미지 400.png")
            self.origin_photo = True


if __name__ == '__main__':
    app = QApplication()
    window = MainWindow()
    window.show()
    app.exec()

실행화면은 아래와 같습니다. 가급적 이렇게 시그널-슬롯으로 처리하면 코딩은 단순해지지만, 예외적인 경우에 어떻게 코딩을 하면 되는지 알아두시면 언젠가는 쓸 데가 있을 것입니다.

clicked 시그널이 있는 경우, 이렇게 간편합니다.


2. 어렵게 접근하는 방법 : QObject를 상속받아 이벤트필터를 정의하는 방법

"커스텀 시그널"을 생성하는 방법 중 하나인 "이벤트필터 정의"입니다. (시그널이나 슬롯 뿐 아니라 위젯도 모두 커스텀으로 작성할 수 있는데, 이건 추후에 기초부터 한 번 더 다루도록 하겠습니다.)

우선 코드와 시연화면을 보여드리겠습니다. 코드는,

from PySide6.QtCore import Signal, QObject, QEvent
from PySide6.QtWidgets import QApplication, QLabel, QMainWindow


def click(widget):  # 참고!
    class Filter(QObject):
        clicked = Signal()

        def eventFilter(self, obj, event):
            if obj == widget and event.type() == QEvent.MouseButtonPress:
                self.clicked.emit()
                return True
            return False

    filter = Filter(widget)
    widget.installEventFilter(filter)
    return filter.clicked


class MainWindow(QMainWindow):
    def __init__(self):
        super(MainWindow, self).__init__()
        self.setWindowTitle("아이들 사진")
        self.widget = QLabel()
        self.widget.setPixmap(r"C:\Users\smj02\Desktop\photo_change\이미지 400.png")
        self.widget.setScaledContents(True)
        self.setCentralWidget(self.widget)
        click(self.widget).connect(self.photo_toggle)  # click함수 사용법. 시그널과 비슷.
        self.origin_photo = True

    def photo_toggle(self):
        if self.origin_photo:
            self.widget.setPixmap(r"C:\Users\smj02\Desktop\photo_change\이미지 401.png")
            self.origin_photo = False
        else:
            self.widget.setPixmap(r"C:\Users\smj02\Desktop\photo_change\이미지 400.png")
            self.origin_photo = True


if __name__ == '__main__':
    app = QApplication()
    window = MainWindow()
    window.show()
    app.exec()

조금 간단히만 설명드리면, click이라는 함수가 리턴하는 filter 인스턴스는 installEventFilter라는 메서드를 통해 widget의 시그널을 슥 가로채고, if문 안에 있는 조건에 부합하는 시그널만 click함수를 통해 리턴해줍니다. 여기서 유의할 점은, 내부 eventFilter에서 보이는 clicked는 기존의 clicked시그널과는 다른 제가 그냥 임의로 작성한 시그널입니다. clicked라는 변수명은 "클릭"이나 "press" 등 아무 이름으로나 교체해도 무관합니다.(참고링크1, 참고링크2)

참고로 PySide가 아니라 PyQt 를 사용하시는 분들은 QtCore.Signal만 QtCore.pyqtSignal로 임포트하시면 동일하게 사용하실 수 있을 것입니다.

 

시연화면은,

click 함수를 짜버림

이밖에도 QtDesigner를 사용하는 방법 등, 커스텀이벤트를 짜는 다양한 방법에 대해서는 다른 포스팅에서 다루도록 하겠습니다.

 

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

행복한 하루 되세요!

 


국내 유일의 파이썬+한컴오피스 업무자동화 입문강의

 

움짤로 빠르게 배우는 파이썬-아래아한글 자동화 레시피 - 인프런 | 강의

파이썬으로 아래아한글을 다루는 짧은 예제코드들을 소개하고, 중간중간의 결과를 GIF로 보여드립니다. 동영상 강의가 아니지만 오히려 빠르게 배울 수 있고, 따라하기도 쉽습니다., - 강의 소개

www.inflearn.com

 

댓글