Profile picture

[Python / GUI] PySide6를 활용하여 GUI 개발해보기

JaehyoJJAng2024년 08월 14일

개요

PySide6는 파이썬에서 Qt 라이브러리를 사용할 수 있도록 도와주는 도구로, 다양한 플랫폼에서 GUI 애플리케이션 개발이 가능하다.

이번 글에서는 PySide6를 이용해 기본적인 GUI 프로그램을 만드는 법부터 멀티스레딩을 구현하는 방법까지 단계별로 기록해보려고 한다.

예제 코드를 작성하여 GUI 프로그램을 개발해보고, QThread를 활용해 긴 작업을 백그라운드에서 실행하는 법까지 실습해보자!


PySide6란 무엇인가?

PySide6는 Qt 라이브러리의 Python 바인딩으로, 크로스 플랫폼 애플리케이션을 개발할 수 있는 프레임워크이다.

PySide6를 사용하면 Python 코드로 윈도우, 버튼, 레이블 등의 GUI 요소를 손쉽게 구현할 수 있다.

또한, Qt의 강력한 기능을 활용하여 복잡한 애플리케이션도 빠르게 개발할 수 있다.


1. 개발 환경 설정

PySide6 설치

pip instasll Pyside6

2. Qt Designer

image


Qt Designer는 GUI를 시각적으로 디자인할 수 있는 도구이다.

코드를 직접 작성하지 않고도 마우스 클릭과 드래그 앤 드롭으로 인터페이스를 설계할 수 있어 매우 편리하다.


Qt Designer의 주요 기능

  • 위젯 배치: 다양한 위젯(버튼, 레이블, 입력 창 등)을 드래그 앤 드롭으로 배치함
  • 속성 편집: 각 위젯의 속성(텍스트, 폰트, 색상 등)을 편집할 수 있음
  • 레이아웃 관리: 윈도우 크기 변화에도 유연하게 대응할 수 있도록 레이아웃을 설정함
  • 시그널 및 슬롯 편집: 위젯 간의 이벤트 연결을 시각적으로 설정할 수 있음.

GUI 디자인 단계

  • Qt Designer 실행: 위의 명령어로 Qt Designer를 실행
  • 새 폼 선택: 'Main Window'를 선택하고 'Create'를 클릭
  • 위젯 추가: 좌측의 위젯 패널에서 필요한 위젯을 중앙의 작업 영역에 드래그
    • 예: QPushButton, QLabel, QLineEdit 등
  • 속성 설정: 우측 하단의 속성 창에서 선택한 위젯의 속성을 편집
    • 예: 버튼의 objectName을 startButton으로 설정
  • 레이아웃 적용: 위젯들을 선택하고 적절한 레이아웃(수평, 수직, 그리드 등)을 적용
  • 디자인 저장: 작업이 완료되면 .ui 확장자로 디자인을 저장.
    • 예: design.ui

3. PySide6로 기본 GUI 프로그램 작성하기

UI 파일을 파이썬 코드로 변환

저장한 .ui 파일을 파이썬 코드로 변환해야 한다. 터미널에서 다음 명령어를 실행하자.

pyside6-uic design.ui -o ui_design.py

파이썬 코드 작성

main.py 파일을 생성하고 다음과 같이 작성해보자.

import sys
from PySide6.QtWidgets import QApplication, QMainWindow
from ui_design import Ui_MainWindow  # 변환된 UI 파일 임포트

class MainWindow(QMainWindow, Ui_MainWindow):
    def __init__(self):
        super().__init__()
        self.setupUi(self)  # UI 설정 (업데이트)

        # 시그널 및 슬롯 연결
        self.startButton.clicked.connect(self.button_clicked)

    def button_clicked(self):
        print("버튼이 클릭되었습니다!")

if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = MainWindow()
    window.show()
    sys.exit(app.exec())

4. QThread를 사용한 멀티스레딩 구현

QThread는 PySide6에서 멀티스레딩을 구현하기 위한 핵심 클래스이다.

QThread를 사용하면 UI 응용 프로그램이 사용자 인터페이스를 원활하게 유지하면서 동시에 백그라운드에서 가볍거나 무거운 작업을 수행할 수 있다.


QThread는 OS 수준의 스레드를 관리하며, Qt의 이벤트 루프와 통합되어 신호(Signal)와 슬롯(Slot)을 통해 스레드 간 통신을 용이하게 한다.

QThread는 다음과 같은 두 가지 주요 방법으로 사용할 수 있다.

  • 서브클래싱 (Subclassing): QThread를 상속받아 run() 메소드를 재정의(Override)하여 스레드에서 실행할 코드를 정의한다.
  • 워커 객체 이동: 별도의 워커 클래스를 만들고, 이 객체를 QThread 인스턴스로 이동시켜 워커의 슬롯을 스레드에서 실행하도록 한다.

4-1. QThread 사용 방법

4-1-1. QThread 서브 클래싱

이 방법은 QThread를 상속받아 run() 메소드를 재정의하는 방식이다.

그러나 이 방법은 Qt의 권장 방식이 아니며, 신호(Signal)와 슬롯(Slot)을 활용하기 어렵기 때문에 피하는게 좋다.

from PySide6.QtCore import QThread, Signal
import time

class WorkerThread(QThread):
    progress = Signal(int)

    def run(self):
        for i in range(100):
            time.sleep(0.1)  # 긴 작업 시뮬레이션
            self.progress.emit(i + 1)

4-1-2. 워커 객체 이동

이 방법은 QObject를 상속받은 워커 클래스를 만들고. 이를 QThread에 이동시키는 방식이다.

이 방식이 권장되며, 신호와 슬롯을 통한 통신이 용이하다.

from PySide6.QtCore import QObject, QThread, Signal, Slot
import time

class Worker(QObject):
    progress = Signal(int)
    finished = Signal()

    @Slot()
    def do_work(self):
        for i in range(100):
            time.sleep(0.1)  # 긴 작업 시뮬레이션
            self.progress.emit(i + 1)
        self.finished.emit()

4-2. 신호(Signal)와 슬롯(Slot)

Qt의 핵심 기능 중 하나인 신호와 슬롯은 스레드 간 통신을 안전하게 수행할 수 있게 해준다.

QThread를 사용할 때, 워커 객체는 신호를 통해 진행 상태나 결과를 메인 스레드에 전달할 수 있게된다.


주요 개념

  • Signal (신호): 이벤트가 발생했을 때 방출되는 통지 메커니즘.
  • Slot (슬롯): 신호를 수신하여 특정 작업을 수행하는 메서드.

스레드 간 통신은 자동으로 스레드 세이프하게 이루어지므로, 직접 스레드 락을 관리할 필요가 없다.


4-3. 신호와 슬롯의 인자 전달 방식

QThread에서 신호(Signal)을 사용할 때, 신호에 정의된 인자들은 슬롯(Slot)에 연결된 메소드로 정확히 전달된다.


먼저 Worker 클래스에서 다음과 같이 신호를 정의했다고 가정해보자.

from PySide6.QtCore import QObject, Signal

class Worker(QObject):
    result = Signal(bool, str, list)
    
    def some_work(self):
        # 작업 수행 후 결과를 신호로 방출
        success = True
        message = "작업이 완료되었습니다."
        data = [1, 2, 3]
        self.result.emit(success, message, data)

위 코드에서 result 신호는 세 개의 인자를 전달하도록 정의되어 있다.

  • bool 타입 (success)
  • str 타입 (message)
  • list 타입 (data)

이 신호를 메인 UI에서 받아 처리할 슬롯 메소드를 정의할 때, 슬롯 메소드는 신호가 방출할 인자들과 동일한 수와 타입의 인자를 받아야 한다.


슬롯 메소드 정의 및 연결

다음은 메인 윈도우 클래스에서 Workerresult 신호를 처리하는 슬롯 메소드를 정의하고 연결하는 예제이다.

from PySide6.QtWidgets import QApplication, QWidget, QVBoxLayout, QPushButton, QLabel
from PySide6.QtCore import QObject, QThread, Signal, Slot
import sys

class Worker(QObject):
    result = Signal(bool, str, list)
    
    @Slot()
    def some_work(self):
        # 작업 수행 (예시)
        success = True
        message = "작업이 완료되었습니다."
        data = [1, 2, 3]
        self.result.emit(success, message, data)

class MainWindow(QWidget):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("QThread 신호 예제")
        self.layout = QVBoxLayout()
        self.setLayout(self.layout)
        
        self.button = QPushButton("작업 시작")
        self.label = QLabel("결과가 여기에 표시됩니다.")
        
        self.layout.addWidget(self.button)
        self.layout.addWidget(self.label)
        
        self.button.clicked.connect(self.start_thread)
    
    def start_thread(self):
        self.thread = QThread()
        self.worker = Worker()
        self.worker.moveToThread(self.thread)
        
        # 스레드 시작 시 워커의 some_work 메소드 호출
        self.thread.started.connect(self.worker.some_work)
        
        # 워커의 result 신호을 메인 윈도우의 handle_result 슬롯에 연결
        self.worker.result.connect(self.handle_result)
        
        # 작업 완료 후 스레드 정리
        self.worker.result.connect(self.thread.quit)
        self.worker.result.connect(self.worker.deleteLater)
        self.thread.finished.connect(self.thread.deleteLater)
        
        self.thread.start()
        self.button.setEnabled(False)
    
    @Slot(bool, str, list)
    def handle_result(self, success, message, data):
        if success:
            self.label.setText(f"성공: {message}\n데이터: {data}")
        else:
            self.label.setText(f"실패: {message}")
        self.button.setEnabled(True)

if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = MainWindow()
    window.show()
    sys.exit(app.exec())

Worker 클래스

  • result 신호는 세 개의 인자를 전달하도록 정의되었다.
  • some_work 메소드에서 작업을 수행한 후, result.emit(success, message, data)를 통해 신호를 방출한다.

MainWindows 클래스

  • 버튼을 클릭하면 start_thread 메소드가 호출됩니다.
  • 새로운 QThreadWorker 인스턴스를 생성하고, 워커를 스레드로 이동시킨다.
  • 스레드가 시작되면 워커의 some_work 메소드가 실행된다.
  • worker.result 신호는 handle_result 슬롯에 연결된다.
  • handle_result 슬롯은 신호에서 방출된 세 개의 인자 (success, message, data)를 받아 처리한다.
  • 작업이 완료되면 스레드를 정리하고, 버튼을 다시 활성화한다.

handle_result 슬롯

  • @Slot(bool, str, list) 데코레이터를 사용하여 슬롯 메소드가 세 개의 인자를 받을 것임을 명시한다.
  • 신호에서 전달된 success, message, data 인자를 사용하여 UI를 업데이트한다.

4-3. 예제 코드

아래는 워커 객체를 QThread에 이동시켜 긴 작업을 수행하고, 진행 상황을 UI에 업데이트하는 간단한 예제이다.

import sys
import time
from PySide6.QtWidgets import QApplication, QWidget, QVBoxLayout, QPushButton, QProgressBar
from PySide6.QtCore import QObject, QThread, Signal, Slot

class Worker(QObject):
    progress = Signal(int)
    finished = Signal()

    @Slot()
    def do_work(self):
        for i in range(1, 101):
            time.sleep(0.05)  # 긴 작업 시뮬레이션
            self.progress.emit(i)
        self.finished.emit()

class MainWindow(QWidget):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("QThread 예제")
        self.layout = QVBoxLayout()
        self.setLayout(self.layout)

        self.button = QPushButton("작업 시작")
        self.progress_bar = QProgressBar()
        self.progress_bar.setRange(0, 100)

        self.layout.addWidget(self.button)
        self.layout.addWidget(self.progress_bar)

        self.button.clicked.connect(self.start_work)

    def start_work(self):
        self.button.setEnabled(False)

        self.thread = QThread()
        self.worker = Worker()
        self.worker.moveToThread(self.thread)

        # 스레드 시작 시 워커의 do_work 슬롯 호출
        self.thread.started.connect(self.worker.do_work)
        # 워커의 progress 신호을 프로그레스 바에 연결
        self.worker.progress.connect(self.progress_bar.setValue)
        # 작업 완료 시 스레드 정리
        self.worker.finished.connect(self.thread.quit)
        self.worker.finished.connect(self.worker.deleteLater)
        self.thread.finished.connect(self.thread.deleteLater)
        self.thread.finished.connect(self.work_finished)

        self.thread.start()

    def work_finished(self):
        self.button.setEnabled(True)
        self.progress_bar.setValue(0)

if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = MainWindow()
    window.show()
    sys.exit(app.exec())

WorkerThread 클래스 생성

from PySide6.QtCore import QThread, Signal

class WorkerThread(QThread):
    progress = Signal(int)  # 진행 상황을 나타내는 시그널

    def run(self):
        for i in range(1, 101):
            # 긴 작업 수행 (예: 시간 지연)
            self.sleep(1)
            self.progress.emit(i)  # 진행 상황 업데이트

메인 윈도우에서 스레드 사용

class MainWindow(QMainWindow, Ui_MainWindow):
    def __init__(self):
        super().__init__()
        self.setupUi(self)
        self.startButton.clicked.connect(self.start_thread)
        self.worker = None

    def start_thread(self):
        self.worker = WorkerThread()
        self.worker.progress.connect(self.update_progress)
        self.worker.start()

    def update_progress(self, value):
        self.progressBar.setValue(value)

QThread 사용 시 주의할 점

  • GUI 업데이트는 메인 스레드에서만 가능: 백그라운드 스레드에서 직접 GUI를 업데이트하지 않고, 시그널과 슬롯을 이용해 메인 스레드에서 업데이트해야 한다.
  • 스레드 수명 관리: 작업이 끝난 후 스레드를 안전하게 종료하거나, 필요 시 재사용할 수 있도록 관리해야 한다.

5. 네이버 뉴스 크롤러 프로그램 만들기

기본기 연습에서 이해한 내용으로 간단한 네이버 뉴스 크롤러 프로그램을 만들어보자!

프로그램은 PySide6와 QThread를 활용해 뉴스 데이터를 실시간으로 크롤링하는 원리로 동작하도록 구현할 거다.


5-1. 필요 라이브러리 설치

크롤링을 하기 위해 필요한 라이브러리인 requests beautifulSoup를 설치하자.

pip install requests bs4 pyside6

5-2. UI 수정 (Qt Designer)

  • QLineEdit 추가: URL을 입력받을 입력 창 (objectName: urlInput)
  • QTextEdit 추가: 크롤링 결과를 표시할 영역 (objectName: resultText)
  • QProgressBar 추가: 진행 상황을 표시 (objectName: progressBar)

디자인을 저장한 후 .ui 파일을 다시 변환합니다

pyside6-uic design.ui -o ui_design.py

5-3. 크롤링을 위한 WorkerThread 수정

class CrawlerThread(QThread):
    progress = Signal(int)
    result = Signal(str)

    def __init__(self, url):
        super().__init__()
        self.url = url

    def run(self):
        import requests
        from bs4 import BeautifulSoup

        response = requests.get(self.url)
        soup = BeautifulSoup(response.content, 'html.parser')
        texts = list(soup.stripped_strings)

        total = len(texts)
        for i, text in enumerate(texts):
            self.progress.emit(int((i / total) * 100))
            # 실제로는 데이터를 처리하거나 저장 로직 구현
        self.result.emit('\n'.join(texts))

5-4. 메인 윈도우 코드 수정

class MainWindow(QMainWindow, Ui_MainWindow):
    def __init__(self):
        super().__init__()
        self.setupUi(self)
        self.startButton.clicked.connect(self.start_crawling)
        self.thread = None

    def start_crawling(self):
        url = self.urlInput.text()
        if url:
            self.thread = CrawlerThread(url)
            self.thread.progress.connect(self.update_progress)
            self.thread.result.connect(self.display_result)
            self.thread.start()

    def update_progress(self, value):
        self.progressBar.setValue(value)

    def display_result(self, data):
        self.resultText.setPlainText(data)

Loading script...