PySide6로 데스크톱 앱 만들기 #2 위젯과 레이아웃 — 화면을 조립하는 법

5 분 소요

지난 글에서 버튼 하나가 창을 꽉 채운 앱을 만들었습니다. 실제 앱의 화면은 입력창, 목록, 버튼 여러 개가 정해진 자기 위치에 놓여야 하고, 창 크기를 바꿔도 배치가 자연스럽게 따라와야 합니다. 이번 글은 그 배치를 담당하는 두 가지 재료, 위젯레이아웃을 다룹니다.

총 7편으로 구성됩니다.

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

이번 글이 끝나면 할 일 앱의 화면 뼈대가 완성됩니다. 동작은 아직 없지만, 다음 글에서 시그널을 연결하면 곧바로 살아 움직이는 구조입니다.

자주 쓰는 위젯 카탈로그 #

위젯은 화면을 구성하는 부품입니다. Qt에는 수십 종이 있지만, 업무 도구 수준의 앱은 다음 일곱 개로 대부분 조립됩니다.

위젯용도
QLabel텍스트나 이미지 표시. 편집 불가
QLineEdit한 줄 텍스트 입력
QPushButton클릭 버튼
QCheckBox켜고 끄는 체크박스
QComboBox드롭다운 선택 목록
QTextEdit여러 줄 텍스트 입력과 표시
QSpinBox위아래 화살표가 달린 숫자 입력

여기에 항목 목록을 보여 주는 QListWidget을 더하면 이번 글의 재료가 전부입니다. 각 위젯의 세부 옵션은 필요할 때 공식 문서에서 찾으면 되고, 지금 중요한 것은 어떤 부품이 존재하는지를 아는 것입니다.

좌표 배치의 함정과 레이아웃 매니저 #

위젯을 배치하는 가장 원시적인 방법은 좌표 지정입니다. button.move(50, 100)처럼 픽셀 위치를 직접 찍는 방식인데, 이 방식은 실전에서 금방 무너집니다. 창 크기를 바꾸면 위젯이 따라오지 않고, 글꼴 크기가 다른 환경에서는 위젯이 겹치거나 잘립니다.

그래서 Qt는 레이아웃 매니저에 배치를 맡깁니다. 위젯을 레이아웃에 추가하기만 하면, 창 크기가 바뀔 때마다 레이아웃이 각 위젯의 위치와 크기를 다시 계산합니다. 기본 레이아웃은 네 가지입니다.

네 가지 기본 레이아웃
from PySide6.QtWidgets import (
    QVBoxLayout,   # 위에서 아래로 쌓기
    QHBoxLayout,   # 왼쪽에서 오른쪽으로 나열
    QGridLayout,   # 행과 열의 격자에 배치
    QFormLayout,   # 라벨-입력 쌍을 두 열로 정렬
)

QVBoxLayoutQHBoxLayout이 기본입니다. 격자가 필요한 계산기 화면은 QGridLayout, 설정 화면처럼 “이름: 입력창” 쌍이 반복되는 폼은 QFormLayout이 맞습니다.

QGridLayout 과 QFormLayout 맛보기
# 격자 배치 — addWidget(위젯, 행, 열)
grid = QGridLayout()
grid.addWidget(QPushButton("7"), 0, 0)
grid.addWidget(QPushButton("8"), 0, 1)
grid.addWidget(QPushButton("9"), 0, 2)

# 폼 배치 — addRow(라벨, 입력 위젯)
form = QFormLayout()
form.addRow("이름", QLineEdit())
form.addRow("포트", QSpinBox())

중첩 — 레이아웃 안에 레이아웃 #

실전 화면은 레이아웃 하나로 끝나지 않습니다. 할 일 앱의 화면을 설계해 보면 이렇게 나뉩니다.

할 일 앱 화면 설계
┌─────────────────────────────┐
│ [ 입력창          ] [추가]   │  ← 가로 배치 (QHBoxLayout)
├─────────────────────────────┤
│                             │
│  할 일 목록 (QListWidget)     │  ← 중앙, 남는 공간 전부
│                             │
├─────────────────────────────┤
│        [선택 삭제] [전체 삭제] │  ← 가로 배치 (QHBoxLayout)
└─────────────────────────────┘
        ↑ 전체를 세로로 쌓기 (QVBoxLayout)

가로 줄 두 개와 목록 하나를 세로 레이아웃에 쌓는 구조입니다. 코드로 옮기면 다음과 같습니다.

main.py — 할 일 앱 화면 뼈대
import sys

from PySide6.QtWidgets import (
    QApplication, QMainWindow, QWidget,
    QVBoxLayout, QHBoxLayout,
    QLineEdit, QPushButton, QListWidget,
)


class TodoWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("할 일 관리")
        self.resize(400, 500)

        # 상단 입력줄 — 입력창 + 추가 버튼
        self.todo_input = QLineEdit()
        self.todo_input.setPlaceholderText("할 일을 입력하세요")
        self.add_button = QPushButton("추가")
        input_row = QHBoxLayout()
        input_row.addWidget(self.todo_input)
        input_row.addWidget(self.add_button)

        # 중앙 목록
        self.todo_list = QListWidget()

        # 하단 버튼줄 — 오른쪽 정렬
        self.delete_button = QPushButton("선택 삭제")
        self.clear_button = QPushButton("전체 삭제")
        button_row = QHBoxLayout()
        button_row.addStretch()
        button_row.addWidget(self.delete_button)
        button_row.addWidget(self.clear_button)

        # 전체 조립 — 세로로 쌓기
        layout = QVBoxLayout()
        layout.addLayout(input_row)
        layout.addWidget(self.todo_list)
        layout.addLayout(button_row)

        container = QWidget()
        container.setLayout(layout)
        self.setCentralWidget(container)


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

실행하면 설계 그림 그대로의 창이 뜨고, 창을 늘리면 목록 영역이 함께 늘어납니다. 조립 규칙은 세 가지입니다.

  • 위젯은 addWidget, 레이아웃은 addLayout — 레이아웃 안에 레이아웃을 넣을 때는 addLayout을 씁니다.
  • 레이아웃은 위젯에 실어야 화면에 나옵니다. QMainWindow의 중앙에는 위젯만 들어갈 수 있으므로, 빈 QWidget에 setLayout으로 레이아웃을 싣고 그 위젯을 setCentralWidget에 넘깁니다.
  • 위젯을 self 속성으로 보관합니다. self.todo_input처럼 붙잡아 둔 위젯만 다음 글에서 시그널을 연결하거나 값을 읽을 수 있습니다.

여백, 정렬, 남는 공간 #

배치를 다듬는 세 가지 도구를 확인합니다.

  • stretch — 하단 버튼줄의 addStretch()는 빈 공간을 차지하는 스프링입니다. 버튼들 앞에 넣었으므로 버튼이 오른쪽으로 밀려 정렬됩니다.
  • 여백과 간격layout.setContentsMargins(12, 12, 12, 12)는 레이아웃 바깥 여백, layout.setSpacing(8)은 위젯 사이 간격을 지정합니다.
  • 크기 비율layout.addWidget(self.todo_list, stretch=1)처럼 stretch 값을 주면 창이 커질 때 남는 공간을 그 위젯이 우선 가져갑니다.
레이아웃이 의도대로 나오지 않을 때는 구조를 종이에 먼저 그려 보는 것이 가장 빠릅니다. 화면을 가로 줄과 세로 묶음으로 쪼개는 설계가 끝나면, 코드는 그 그림을 그대로 옮겨 적는 작업이 됩니다.

마무리 #

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

  • 화면은 위젯이라는 부품과 레이아웃이라는 배치 규칙으로 조립하고, 좌표 지정은 쓰지 않습니다.
  • 실전 화면은 QHBoxLayout과 QVBoxLayout의 중첩으로 대부분 표현되고, 레이아웃은 빈 QWidget에 실어 setCentralWidget으로 창에 올립니다.
  • addStretch, 여백, stretch 비율로 배치를 다듬습니다.

지금의 할 일 앱은 버튼을 눌러도 아무 일도 일어나지 않습니다. 다음 글인 “PySide6로 데스크톱 앱 만들기 #3 시그널과 슬롯 — 이벤트 처리의 핵심"에서 Qt 이벤트 처리의 핵심 모델인 시그널과 슬롯을 배우고, 이 화면에 실제 동작을 연결하겠습니다.

X