1. 학습 목표

더보기

 (1) Qt 리소스(qrc → py) 시스템 이해

  • 이미지 파일을 프로그램 내부에 포함하는 방법
  • :/res/normal 와 같은 리소스 경로 사용 방법

(2) Custom Widget 설계 원리 이해

  • QWidget을 상속하여 자신만의 위젯 만들기
  • paintEvent()에서 직접 이미지를 그리는 기법

 

2. 프로젝트 구조 만들기

더보기
PythonProject.zip
0.01MB
CustomImageButtonDemo/
 ├─ res                  # Qt 리소스 정의 파일
 ├─ res.qrc              # 리소스 컴파일 결과 (프로그램 내 포함됨)
 ├─ res.py               # pyside6-rcc로 컴파일된 리소스 파이썬 파일
 ├─ image_button.py      # 커스텀 위젯(ImageButton) 클래스
 ├─ widget.py            # 커스텀 버튼 둘을 배치한 화면
 └─ main.py              # 실행 진입점, 위젯 사용 예제
  • res.py : PNG 이미지를 파이썬 코드 안에 바이너리로 포함하고, :/res/xxx.png 경로로 접근 가능하게 해줌
  • image_button.py : QWidget을 상속한 커스텀 버튼 위젯 구현 파일
  • main.py : 커스텀 버튼을 실제로 생성해서 사용하는 예제

 

3. 이미지 리소스 준비 & 리소스 정의 파일 만들기

: STEP 1.  리소스 파일(res.qrc → res.py)

더보기

1. 프로젝트에 사용할 이미지가 압축된 파일을 프로젝트의 res 폴더 내에 준비합니다.

res.zip
0.00MB

 

 

 

 

2. res.qrc 파일을 만들고 아래 코드를 추가합니다.

<RCC>
  <qresource prefix="/res">
    <file alias="normal">res/normal.png</file>
    <file alias="enter">res/enter.png</file>
    <file alias="press">res/press.png</file>
    <file alias="disable">res/disable.png</file>
  </qresource>
</RCC>

 

 

 

 

3. < res.qrc > 파일을 리소스 컴파일러로 <  res.py > 파이썬 코드로 변환:

pyside6-rcc res.qrc -o res.py

그러면 res.py 안에 PNG 데이터가 통째로 들어가고, 코드에서 import res 하는 순간 qInitResources()가 호출되면서
:/res/normal.png 같은 경로로 이미지를 읽을 수 있게 됩니다.

 

 

 

 

# 코드에서 사용하는 방법은 아래와 같습니다.

import res     # 리소스 자동 등록
QImage(":/res/normal")
QImage(":/res/press")

 

4. 커스텀 위젯 ImageButton 만들기 (image_button.py)

: STEP 2. 상태에 따라 다른 이미지를 그려주는 버튼 위젯

더보기

일반 QPushButton 대신 QWidget을 상속하여 아래와 같이 구현합니다.

from PySide6.QtWidgets import QWidget
from PySide6.QtGui import QPainter, QImage, QMouseEvent
from PySide6.QtCore import Signal

import res  # 리소스 등록 (:/res/xxx.png 사용 가능)

# 상태 상수 정의
BEHAVIOUR_NORMAL = 0
BEHAVIOUR_ENTER = 1
BEHAVIOUR_LEAVE = 2
BEHAVIOUR_PRESS = 3
BEHAVIOUR_DISABLE = 4


class ImageButton(QWidget):
    # 사용자 정의 시그널
    clicked = Signal()
    hovering = Signal()

    def __init__(self, parent=None):
        super(ImageButton, self).__init__(parent)

        self._disabled = False  # 비활성화 여부
        self._behaviour = BEHAVIOUR_NORMAL
        self._iamgeFileName = ""

        # 기본 이미지 크기를 기준으로 위젯 크기 지정
        image = QImage(":/res/normal")
        self.setFixedWidth(image.width())
        self.setFixedHeight(image.height())

    def enterEvent(self, event):
        self._behaviour = BEHAVIOUR_ENTER
        self.update()
        self.hovering.emit()

    def leaveEvent(self, event):
        self._behaviour = BEHAVIOUR_NORMAL
        self.update()

    def mousePressEvent(self, event):
        self._behaviour = BEHAVIOUR_PRESS
        self.update()
        self.clicked.emit()

    def mouseReleaseEvent(self, event):
        self._behaviour = BEHAVIOUR_ENTER
        self.update()


    def setDisabled(self, val):
        self._disabled = val
        self.update()  # 다시 그리기 요청

    def paintEvent(self, event):
        painter = QPainter(self)

        # 상태에 따라 사용할 이미지 경로 결정
        if self._disabled:
            self._iamgeFileName = ":/res/disable"
        else:
            if self._behaviour == BEHAVIOUR_NORMAL:
                self._iamgeFileName = ":/res/normal"
            elif self._behaviour == BEHAVIOUR_ENTER:
                self._iamgeFileName = ":/res/enter"
            elif self._behaviour == BEHAVIOUR_LEAVE:
                self._iamgeFileName = ":/res/normal"
            elif self._behaviour == BEHAVIOUR_PRESS:
                self._iamgeFileName = ":/res/press"

        image = QImage(self._iamgeFileName)
        painter.drawImage(0, 0, image)

 

 

1. QWidget 내장된 이벤트 핸들러

 

1.1 이벤트 핸들러

  • QWidget 이 만들어 놓은 수십 가지 이벤트 핸들러(이벤트 처리 메서드)를 이미 가지고 있으며,
    위 코드에서는 필요한 이벤트 핸들러를 오버라이드(override) 하여 사용하고 있다.
이벤트 (트리거, 시작점) 이벤트 핸들러 메서드 이름 
위젯에 마우스 진입 enterEvent
위젯에 마우스 떠남 leaveEvent
위젯에 마우스 클릭 mousePressEvent
위젯에 마우스 클릭 해제 mouseReleaseEvent
(이벤트 핸들러가 아님, 호출되는 함수)  (이벤트 핸들러가 아님) update( )
update( ), repaint( )를 호출 paintEvent
  • 이벤트(위젯에 마우스 진입 동작) 발생하면, 이벤트 핸들러(enterEvent( ) 함수)가 동작한다.
  • 위 코드는 이벤트 핸들러(enterEvent( ) 함수)를 추가 수정(override) 하여 사용한다.

즉, 우리가 직접 정의하지 않아도:

  • 마우스 진입 이벤트 >> 이벤트 핸들러  enterEvent( ) 동작
  • 마우스 클릭 이벤트
  • 마우스 이동 이벤트
  • 키보드 이벤트

enterEvent() 등의 함수는 우리가 직접 호출하는 함수가 아니라 Qt가 자동으로 호출하는, 구조가 이미 탑재되어 있습니다. 

 

 

1.2 update( )

update()
  • PySide6.QtWidgets.QWidget 안에 정의된 메서드
  • “나중에 paintEvent() 다시 실행시켜줘!”
  • update()는 즉시 그리는 것이 아니라,
    이벤트 루프에 “이 위젯을 다시 그려라”라는 요청을 등록합니다.

 

 

 

 

2. 동작 구조

마우스 움직임 → 위젯 영역 안으로 들어감
   ▼
마우스 움직임 감지 (이벤트)
   ▼
enterEvent() 이벤트 핸들러 호출(실행)
   ▼
enterEvent() 안에서 update() 실행 
   ▼
update() 함수가 paintEvent() 실행 
   ▼
상태(Behaviour) 변경
   ├─ NORMAL
   ├─ ENTER (mouse hover)
   ├─ LEAVE
   ├─ PRESS (mouse down)
   └─ DISABLE
   ▼
painter.drawImage(0, 0, image)
   → 상태에 따라 이미지 선택

 

 

 

 

3. 상태(Behaviour) 상수 정의

BEHAVIOUR_NORMAL = 0
BEHAVIOUR_ENTER = 1
BEHAVIOUR_LEAVE = 2
BEHAVIOUR_PRESS = 3
BEHAVIOUR_DISABLE = 4
  • 버튼이 "어떤 상태인지" 표시하는 상태 변수로 사용됨
  • UI 상태를 숫자로 정의하면 코드 가독성이 좋아지고 비교가 쉬워짐

 

 

 

 

4. 사용자 정의 시그널 생성

clicked = Signal()
hovering = Signal()
  • PySide6에서 제공하지 않는 이벤트를 Button이 직접 전달할 때 사용
  • clicked.emit() 호출하면
    외부에서 button.clicked.connect(함수) 형태로 처리 가능

 

 

 

 

5. 초기화 및 이미지 기반 크기 설정

image = QImage(":/res/normal")
self.setFixedWidth(image.width())
self.setFixedHeight(image.height())
  • 버튼은 이미지 크기 기반으로 UI 크기를 결정함
  • QPushButton처럼 text + padding이 아니라
    이미지 1개 = 버튼의 전체 UI

 

 

 

 

6. 마우스 이벤트에 맞춰, 이벤트 핸들러 처리

 

6.1 마우스가 버튼 위젯 위에 올라가는 이벤트 처리 >> enterEvent( ) 이벤트 핸들러 동작

def enterEvent(self, event):
    self._behaviour = BEHAVIOUR_ENTER
    self.update()
    self.hovering.emit()
  1. 상태 변경 → ENTER
  2. update() → paintEvent 호출 요청
  3. hovering 시그널 emit (외부에서 슬롯 함수 동작 처리 연결)

 

 

6.2 마우스가 버튼 위젯 영역을 벗어남 이벤트 처리 >> leaveEvent( ) 이벤트 핸들러 동작

def leaveEvent(self, event):
    self._behaviour = BEHAVIOUR_NORMAL
    self.update()
  • 상태 NORMAL로 다시 변경
  • 다시 이미지 리프레시

 

 

6.3 마우스 버튼 위젯을 누름 이벤트 처리 >> mousePressEvent( ) 이벤트 핸들러 동작

def mousePressEvent(self, event):
    self._behaviour = BEHAVIOUR_PRESS
    self.update()
    self.clicked.emit()
  • 상태 → PRESS
  • 이미지 변경
  • clicked() 시그널 emit (외부에서 슬롯 함수 동작 처리 연결)

 

 

6.4 마우스 위젯을 떼면 ENTER 상태로 회복 이벤트 처리 >> mouseReleaseEvent( ) 이벤트 핸들러 동작

def mouseReleaseEvent(self, event):
    self._behaviour = BEHAVIOUR_ENTER
    self.update()
  • 버튼 영역 안에서 클릭 → 뗀 후에는 hover 상태가 유지되어야 함
  • mouseRelease에서 click을 emit하지 않는 이유
    → PRESS 때 이미 emit했기 때문

 

 

6.5 Disable 기능 구현

def setDisabled(self, val):
    self._disabled = val
    self.update()
  • 호출하면 disabled 이미지로 변경 
  • 클릭 동작하지만, 클릭하지 않는것처럼 보임

 


7. paintEvent: 상태에 따라 이미지 그림

def paintEvent(self, event):
    painter = QPainter(self)
    ...
    image = QImage(self._iamgeFileName)
    painter.drawImage(0, 0, image)
  • QWidget은 기본적으로 아무것도 그리지 않서 paintEvent()에서 직접 그림을 그려야 함
  • 버튼 UI 전체를 이미지로 그리는 구조

 

 

if self._disabled:
    self._iamgeFileName = ":/res/disable"
else:
    if self._behaviour == BEHAVIOUR_NORMAL:
        self._iamgeFileName = ":/res/normal"
    elif self._behaviour == BEHAVIOUR_ENTER:
        self._iamgeFileName = ":/res/enter"
    elif self._behaviour == BEHAVIOUR_PRESS:
        self._iamgeFileName = ":/res/press"
  • 상태별 이미지 선택 로직

 

 

image = QImage(self._iamgeFileName)
painter.drawImage(0, 0, image)
  • 선택된 이미지에 따른, 버튼 UI 전체를 이미지로 그리는 로직

 

 

5. 커스텀 위젯을 화면에 배치 (widget.py)

: STEP 3.

더보기

이제 만든 ImageButton 두 개를 하나의 창에 배치하고, 출력되는 슬롯을 연결합니다.

# widget.py
from PySide6.QtWidgets import QWidget, QHBoxLayout
from PySide6.QtCore import Slot

from image_button import ImageButton


class Widget(QWidget):
    """
    ImageButton 2개를 가로로 배치한 예제 위젯.
    왼쪽 버튼: 동작/출력 확인용
    오른쪽 버튼: 비활성화 상태
    """

    def __init__(self, parent=None):
        super().__init__(parent)

        # [1] 커스텀 이미지 버튼 2개 생성
        imgBtn1 = ImageButton(self)
        imgBtn2 = ImageButton(self)

        # [2] 수평 레이아웃에 버튼 추가
        layout = QHBoxLayout(self)
        layout.addWidget(imgBtn1)
        layout.addWidget(imgBtn2)
        self.setLayout(layout)

        # [3] 첫 번째 버튼 시그널 → 슬롯 연결
        imgBtn1.hovering.connect(self.on_hovering)
        imgBtn1.clicked.connect(self.on_clicked)

        # [4] 두 번째 버튼은 비활성화
        imgBtn2.setDisabled(True)

    # [슬롯1] 마우스를 올렸을 때
    @Slot()
    def on_hovering(self):
        print("이미지 버튼 1 위에 마우스가 올라갔습니다.")

    # [슬롯2] 버튼을 클릭(누름) 했을 때
    @Slot()
    def on_clicked(self):
        print("이미지 버튼 1이 클릭되었습니다.")

왼쪽 버튼

  • 마우스를 올리면 Hovering 출력, 이미지가 enter.png로 변경
  • 클릭하면 Clicked !! 출력, 이미지가 press.png로 변경 후 다시 enter.png로 변경

 

6. 실행 진입점 (main.py)

: STEP 4. 마지막으로, 애플리케이션을 실행하는 코드

더보기
# main.py
import sys
from PySide6.QtWidgets import QApplication

from widget import Widget


def main():
    app = QApplication(sys.argv)

    w = Widget()
    w.setWindowTitle("Custom Image Button Demo")
    w.resize(220, 100)
    w.show()

    sys.exit(app.exec())


if __name__ == "__main__":
    main()

 

7. 추가 실습 과제

: (1) 토글 버튼 만들기

더보기
  • 한 번 클릭 → “재생(Play)” / 다시 클릭 → “일시정지(Pause)” 이미지로 변경
  • 내부에 self._checked 같은 bool 상태 추가해 구현
from PySide6.QtWidgets import QWidget
from PySide6.QtGui import QPainter, QImage, QMouseEvent
from PySide6.QtCore import Signal
import res   # 리소스 등록 (:/res/xxx.png 사용 가능)

# 상태 상수 정의
BEHAVIOUR_NORMAL = 0
BEHAVIOUR_ENTER = 1
BEHAVIOUR_LEAVE = 2
BEHAVIOUR_PRESS = 3
BEHAVIOUR_DISABLE = 4


class ImageButton(QWidget):
    # 사용자 정의 시그널
    clicked = Signal()
    hovering = Signal()

    def __init__(self, parent = None):
        super(ImageButton, self).__init__(parent)

        self._disabled = False          # 비활성화 여부
        self._behaviour = BEHAVIOUR_NORMAL
        self._iamgeFileName = ""

        self._checked = False  # <추가 실습 과제 1> 재생/일시정지 토글 상태 (False=재생, True=일시정지)

        # 기본 이미지 크기를 기준으로 위젯 크기 지정
        image = QImage(":/res/normal")
        self.setFixedWidth(image.width())
        self.setFixedHeight(image.height())

    def setDisabled(self, val):
        self._disabled = val
        self.update()   # 다시 그리기 요청

    def paintEvent(self, event):
        painter = QPainter(self)

        # 상태에 따라 사용할 이미지 경로 결정
        if self._disabled:
            self._iamgeFileName = ":/res/disable"

        # <추가 실습 과제 1> 체크된 상태(일시정지 모드)일 때는 항상 다른 이미지 사용
        elif self._checked:
            # 여기서는 예시로 :/res/press 를 "Pause" 상태 이미지라고 가정
            # 실제로는 별도의 pause.png 를 리소스로 추가해서 사용해도 됩니다.
            self._iamgeFileName = ":/res/press"
        # </추가 실습 과제 1>

        else:
            if self._behaviour == BEHAVIOUR_NORMAL:
                self._iamgeFileName = ":/res/normal"
            elif self._behaviour == BEHAVIOUR_ENTER:
                self._iamgeFileName = ":/res/enter"
            elif self._behaviour == BEHAVIOUR_LEAVE:
                self._iamgeFileName = ":/res/normal"
            elif self._behaviour == BEHAVIOUR_PRESS:
                self._iamgeFileName = ":/res/press"

        image = QImage(self._iamgeFileName)
        painter.drawImage(0, 0, image)

    def enterEvent(self, event):
        self._behaviour = BEHAVIOUR_ENTER
        self.update()
        self.hovering.emit()

    def leaveEvent(self, event):
        self._behaviour = BEHAVIOUR_NORMAL
        self.update()

    def mousePressEvent(self, event):
        self._behaviour = BEHAVIOUR_PRESS
        self.update()
        self.clicked.emit()

    def mouseReleaseEvent(self, event: QMouseEvent) -> None:
        if self._disabled:
            return

        # 버튼 영역 안에서 마우스를 뗀 경우만 클릭으로 처리
        if self.rect().contains(event.position().toPoint()):
            # <추가 실습 과제 1> 재생/일시정지 상태 토글
            self._checked = not self._checked
            # </추가 실습 과제 1>

            # 체크 상태와 상관없이, 마우스는 여전히 버튼 위에 있다고 보고 ENTER 상태로
            self._behaviour = BEHAVIOUR_ENTER
            self.update()
            self.clicked.emit()
        else:
            self._behaviour = BEHAVIOUR_NORMAL
            self.update()
image_button.py
0.00MB