PySide6로 데스크톱 앱 만들기 #4 Qt Designer와 UI 파일 — 화면을 그려서 불러오기

7 분 소요

“PySide6로 데스크톱 앱 만들기 #2 위젯과 레이아웃 — 화면을 조립하는 법"에서는 레이아웃을 전부 코드로 조립했습니다. 위젯 몇 개짜리 화면이라면 그 방식으로 충분하지만, 폼이 커지면 코드만 읽어서는 화면이 어떻게 생겼는지 그리기 어려워지고, 배치를 조금 바꾸는 일에도 코드 여러 곳을 고치게 됩니다. Qt는 이 문제를 위해 화면을 마우스로 그리는 전용 도구를 제공합니다. 이번 글의 주제인 Qt Designer입니다.

총 7편으로 구성됩니다.

  • #1 PySide6란 — Qt와 파이썬으로 데스크톱 앱
  • #2 위젯과 레이아웃 — 화면을 조립하는 법
  • #3 시그널과 슬롯 — 이벤트 처리의 핵심
  • #4 Qt Designer와 UI 파일 — 화면을 그려서 불러오기 ← 이번 글
  • #5 모델과 뷰 — 리스트·테이블에 데이터 연결
  • #6 스레드와 타이머 — 멈추지 않는 UI
  • #7 패키징과 배포 — PyInstaller로 실행 파일 만들기

이번 글은 Designer로 할 일 앱 화면을 다시 그리고, 그 결과물인 .ui 파일을 파이썬 코드에서 불러오는 두 가지 방식을 비교한 뒤, 시그널 연결을 코드에 남기는 워크플로로 마무리하겠습니다.

Qt Designer 실행 — 설치가 이미 끝나 있습니다 #

Qt Designer는 별도 설치가 필요 없습니다. PySide6를 설치하면 명령행 도구로 함께 들어옵니다.

Qt Designer 실행
pyside6-designer

실행하면 새 폼 선택 대화상자가 열립니다. 독립 창을 만들 것이므로 Main Window를 선택합니다. 대화상자 하나만 필요한 화면이라면 Dialog 계열 템플릿을 골라도 됩니다.

화면 구성 — 세 개의 패널만 알면 됩니다 #

Designer 화면은 처음 보면 복잡하지만, 실제 작업에 쓰는 패널은 세 개입니다.

  • 위젯 박스(왼쪽) — 사용할 수 있는 위젯 목록입니다. 여기서 폼으로 드래그해 배치합니다.
  • 오브젝트 인스펙터(오른쪽 위) — 폼에 올라간 위젯의 트리입니다. 부모 자식 관계와 레이아웃 구조가 한눈에 보입니다.
  • 프로퍼티 에디터(오른쪽 아래) — 선택한 위젯의 속성을 편집합니다. 이 중 가장 중요한 속성이 objectName입니다.

objectName을 강조하는 이유가 있습니다. 뒤에서 .ui 파일을 코드로 불러오면 각 위젯이 objectName 그대로 파이썬 속성이 되기 때문입니다. 기본값인 pushButton, lineEdit 같은 이름을 그대로 두면 코드에서 어느 버튼인지 구분할 수 없으므로, 위젯을 올릴 때마다 addButton, todoInput처럼 역할이 드러나는 이름을 바로 지정하는 습관이 필요합니다.

폼 만들기 실습 — 할 일 앱 화면 #

#2에서 코드로 만들었던 화면을 Designer로 다시 만듭니다. 구성은 세 가지입니다.

  1. 위젯 박스에서 Line Edit를 드래그해 폼 상단에 놓고, objectName을 todoInput으로 지정합니다.
  2. Push Button을 그 옆에 놓고 objectName을 addButton, text 속성을 “추가"로 지정합니다.
  3. List Widget을 아래에 놓고 objectName을 todoList로 지정합니다.

이 시점에는 위젯들이 폼 위의 절대 좌표에 떠 있는 상태입니다. 창 크기를 바꾸면 위젯이 따라오지 않습니다. 레이아웃을 적용해야 합니다.

레이아웃 적용과 스페이서 #

Designer의 레이아웃은 코드의 QHBoxLayout, QVBoxLayout과 같은 개념을 시각적으로 적용하는 것입니다.

  1. todoInputaddButton을 함께 선택하고, 툴바의 Lay Out Horizontally를 누릅니다. 두 위젯이 가로 레이아웃으로 묶입니다.
  2. 폼의 빈 곳을 클릭해 창 자체를 선택한 뒤 Lay Out Vertically를 누릅니다. 방금 묶은 가로 묶음과 todoList가 세로로 배치되고, 창 크기를 바꾸면 위젯이 따라 늘어납니다.

위젯 사이의 빈 공간이 필요하면 위젯 박스의 Horizontal Spacer, Vertical Spacer를 드래그해 넣습니다. 스페이서는 화면에는 보이지 않지만 남는 공간을 흡수해, 버튼을 오른쪽 끝으로 밀어붙이는 식의 배치를 만들 수 있습니다.

작업이 끝나면 main_window.ui로 저장합니다.

.ui 파일의 정체 — XML #

저장된 파일을 에디터로 열어 보면 특별한 것이 없습니다. 위젯 트리와 속성을 그대로 담은 XML 문서입니다.

main_window.ui 일부
<widget class="QLineEdit" name="todoInput"/>
<widget class="QPushButton" name="addButton">
 <property name="text">
  <string>추가</string>
 </property>
</widget>
<widget class="QListWidget" name="todoList"/>

핵심은 이 파일에 화면 구조만 있고 동작이 없다는 점입니다. 무엇을 눌렀을 때 무슨 일이 일어나는지는 여전히 파이썬 코드의 몫입니다. 화면과 로직이 파일 단위로 분리되므로, 배치 수정은 Designer에서, 동작 수정은 코드에서 각각 진행할 수 있습니다.

불러오기 방식 1 — pyside6-uic로 코드 생성 #

첫 번째 방식은 .ui 파일을 파이썬 코드로 변환해 두고 임포트하는 것입니다.

ui 파일을 파이썬 코드로 변환
pyside6-uic main_window.ui -o ui_main_window.py

생성된 ui_main_window.py 안에는 Ui_MainWindow 클래스가 들어 있고, setupUi 메서드가 Designer에서 그린 위젯 트리를 코드로 재현합니다. 사용 패턴은 다음과 같습니다.

setupUi 패턴
import sys
from PySide6.QtWidgets import QApplication, QMainWindow
from ui_main_window import Ui_MainWindow


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.ui = Ui_MainWindow()
        self.ui.setupUi(self)
        self.ui.addButton.clicked.connect(self.add_todo)

    def add_todo(self):
        text = self.ui.todoInput.text().strip()
        if text:
            self.ui.todoList.addItem(text)
            self.ui.todoInput.clear()


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

self.ui.addButton처럼 Designer에서 지정한 objectName이 그대로 속성이 됩니다. 생성된 파일은 임포트 대상일 뿐이므로 직접 수정하지 않습니다. Designer에서 폼을 고치면 변환 명령을 다시 실행해 갱신합니다.

불러오기 방식 2 — QUiLoader로 런타임 로딩 #

두 번째 방식은 변환 단계 없이 실행 시점에 .ui 파일을 직접 읽는 것입니다.

QUiLoader 패턴
import sys
from PySide6.QtWidgets import QApplication
from PySide6.QtUiTools import QUiLoader

app = QApplication(sys.argv)
loader = QUiLoader()
window = loader.load("main_window.ui")
window.addButton.clicked.connect(
    lambda: window.todoList.addItem(window.todoInput.text())
)
window.show()
app.exec()

파일을 고치고 다시 실행하면 바로 반영되므로 손이 덜 갑니다. 대신 window가 어떤 위젯을 가졌는지 코드만으로는 알 수 없어 에디터 자동완성과 타입 검사의 도움을 받지 못하고, load가 완성된 위젯을 반환하는 구조라 창 클래스를 상속으로 확장하는 패턴과도 잘 맞지 않습니다.

두 방식 비교 #

구분pyside6-uic (코드 생성)QUiLoader (런타임 로딩)
변환 단계필요 (.ui 수정 때마다 재실행)없음
자동완성·타입 검사생성 코드 기준으로 동작지원 안 됨
클래스 상속 확장setupUi 패턴으로 자연스러움어색함
배포생성된 .py만 포함하면 됨.ui 파일을 함께 배포
어울리는 상황규모가 있는 앱, 팀 작업빠른 실험, 소규모 도구

이 시리즈의 남은 편은 setupUi 패턴을 기준으로 진행합니다. 자동완성이 되는 환경이 학습에도 유리하기 때문입니다.

변환 명령을 매번 손으로 치는 대신, 프로젝트에 스크립트로 등록해 두면 편합니다. .ui 파일이 여러 개라면 전부 변환하는 한 줄짜리 셸 스크립트나 Makefile 타깃을 만들어 두는 방식이 일반적입니다.

시그널 연결은 코드에서 #

Designer에도 시그널과 슬롯을 화면에서 연결하는 편집 모드가 있습니다. F4를 누르면 위젯을 드래그해 시그널을 잇는 화면으로 바뀝니다. 다만 이 시리즈에서는 사용하지 않습니다. 연결 관계가 .ui 파일 안에 숨으면 어떤 버튼이 어떤 동작을 부르는지 코드 검색으로 찾을 수 없게 되기 때문입니다. 화면 구조는 Designer가, 동작과 연결은 코드가 담당하도록 경계를 유지하는 편이 앱이 커질수록 유리합니다.

노트
Designer가 만든 폼에서도 시그널과 슬롯의 동작 원리는 #3에서 정리한 그대로입니다. clicked, textChanged 같은 시그널을 코드의 메서드에 connect로 잇는다는 점은 화면을 어떻게 만들었는지와 무관하게 동일합니다.

마무리 #

이번 글의 핵심은 세 가지입니다.

  • Qt Designer는 PySide6 설치에 포함된 화면 편집 도구이고, 결과물인 .ui 파일은 화면 구조만 담은 XML입니다. objectName이 코드에서 위젯을 부르는 이름이 됩니다.
  • 불러오기는 두 방식입니다. pyside6-uic로 코드를 생성하는 setupUi 패턴이 자동완성과 확장에 유리하고, QUiLoader는 변환 없이 바로 읽는 대신 타입 정보를 잃습니다.
  • 시그널 연결은 Designer가 아니라 코드에 둡니다. 화면과 동작의 경계가 분명해야 규모가 커져도 관리할 수 있습니다.

지금까지의 할 일 목록은 QListWidget에 문자열을 직접 넣는 방식이었습니다. 항목에 마감일과 완료 여부까지 담으려면 데이터와 표시를 분리하는 구조가 필요합니다. 다음 글인 “PySide6로 데스크톱 앱 만들기 #5 모델과 뷰 — 리스트·테이블에 데이터 연결"에서 Qt의 모델/뷰 구조를 다루겠습니다.

X