PySide6로 데스크톱 앱 만들기 #4 Qt Designer와 UI 파일 — 화면을 그려서 불러오기
“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를 설치하면 명령행 도구로 함께 들어옵니다.
pyside6-designer실행하면 새 폼 선택 대화상자가 열립니다. 독립 창을 만들 것이므로 Main Window를 선택합니다. 대화상자 하나만 필요한 화면이라면 Dialog 계열 템플릿을 골라도 됩니다.
화면 구성 — 세 개의 패널만 알면 됩니다 #
Designer 화면은 처음 보면 복잡하지만, 실제 작업에 쓰는 패널은 세 개입니다.
- 위젯 박스(왼쪽) — 사용할 수 있는 위젯 목록입니다. 여기서 폼으로 드래그해 배치합니다.
- 오브젝트 인스펙터(오른쪽 위) — 폼에 올라간 위젯의 트리입니다. 부모 자식 관계와 레이아웃 구조가 한눈에 보입니다.
- 프로퍼티 에디터(오른쪽 아래) — 선택한 위젯의 속성을 편집합니다. 이 중 가장 중요한 속성이
objectName입니다.
objectName을 강조하는 이유가 있습니다. 뒤에서 .ui 파일을 코드로 불러오면 각 위젯이 objectName 그대로 파이썬 속성이 되기 때문입니다. 기본값인 pushButton, lineEdit 같은 이름을 그대로 두면 코드에서 어느 버튼인지 구분할 수 없으므로, 위젯을 올릴 때마다 addButton, todoInput처럼 역할이 드러나는 이름을 바로 지정하는 습관이 필요합니다.
폼 만들기 실습 — 할 일 앱 화면 #
#2에서 코드로 만들었던 화면을 Designer로 다시 만듭니다. 구성은 세 가지입니다.
- 위젯 박스에서 Line Edit를 드래그해 폼 상단에 놓고, objectName을
todoInput으로 지정합니다. - Push Button을 그 옆에 놓고 objectName을
addButton, text 속성을 “추가"로 지정합니다. - List Widget을 아래에 놓고 objectName을
todoList로 지정합니다.
이 시점에는 위젯들이 폼 위의 절대 좌표에 떠 있는 상태입니다. 창 크기를 바꾸면 위젯이 따라오지 않습니다. 레이아웃을 적용해야 합니다.
레이아웃 적용과 스페이서 #
Designer의 레이아웃은 코드의 QHBoxLayout, QVBoxLayout과 같은 개념을 시각적으로 적용하는 것입니다.
todoInput과addButton을 함께 선택하고, 툴바의 Lay Out Horizontally를 누릅니다. 두 위젯이 가로 레이아웃으로 묶입니다.- 폼의 빈 곳을 클릭해 창 자체를 선택한 뒤 Lay Out Vertically를 누릅니다. 방금 묶은 가로 묶음과
todoList가 세로로 배치되고, 창 크기를 바꾸면 위젯이 따라 늘어납니다.
위젯 사이의 빈 공간이 필요하면 위젯 박스의 Horizontal Spacer, Vertical Spacer를 드래그해 넣습니다. 스페이서는 화면에는 보이지 않지만 남는 공간을 흡수해, 버튼을 오른쪽 끝으로 밀어붙이는 식의 배치를 만들 수 있습니다.
작업이 끝나면 main_window.ui로 저장합니다.
.ui 파일의 정체 — XML #
저장된 파일을 에디터로 열어 보면 특별한 것이 없습니다. 위젯 트리와 속성을 그대로 담은 XML 문서입니다.
<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 파일을 파이썬 코드로 변환해 두고 임포트하는 것입니다.
pyside6-uic main_window.ui -o ui_main_window.py생성된 ui_main_window.py 안에는 Ui_MainWindow 클래스가 들어 있고, setupUi 메서드가 Designer에서 그린 위젯 트리를 코드로 재현합니다. 사용 패턴은 다음과 같습니다.
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 파일을 직접 읽는 것입니다.
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가, 동작과 연결은 코드가 담당하도록 경계를 유지하는 편이 앱이 커질수록 유리합니다.
clicked, textChanged 같은 시그널을 코드의 메서드에 connect로 잇는다는 점은 화면을 어떻게 만들었는지와 무관하게 동일합니다.마무리 #
이번 글의 핵심은 세 가지입니다.
- Qt Designer는 PySide6 설치에 포함된 화면 편집 도구이고, 결과물인
.ui파일은 화면 구조만 담은 XML입니다. objectName이 코드에서 위젯을 부르는 이름이 됩니다. - 불러오기는 두 방식입니다.
pyside6-uic로 코드를 생성하는 setupUi 패턴이 자동완성과 확장에 유리하고,QUiLoader는 변환 없이 바로 읽는 대신 타입 정보를 잃습니다. - 시그널 연결은 Designer가 아니라 코드에 둡니다. 화면과 동작의 경계가 분명해야 규모가 커져도 관리할 수 있습니다.
지금까지의 할 일 목록은 QListWidget에 문자열을 직접 넣는 방식이었습니다. 항목에 마감일과 완료 여부까지 담으려면 데이터와 표시를 분리하는 구조가 필요합니다. 다음 글인 “PySide6로 데스크톱 앱 만들기 #5 모델과 뷰 — 리스트·테이블에 데이터 연결"에서 Qt의 모델/뷰 구조를 다루겠습니다.