학습 로그/Python3

D+36 [Layout] .py 파일분리 왜 할까 ? 레이아웃 종합 예제 학습(2)

goodjop79 님의 블로그 2026. 5. 28. 21:15

ㅇ 일 자 : 2026년 5월 28일(목)

ㅇ 내용 : Layout 가상환경에서 종합 예제 학습 하기

 

1. QtabWidget을 통해 파일분리를 실습하였다. 왜 .py 파일분리를 해야 하는가? 

위젯의 통합 코드를 여러 파일(main.py,  Widget.py,  from_tab.py,  buttons_tab.py 등)로 분리하여 사용하는 가장 큰 이유는 
유지보수의 편의성과 코드의 재사용성 때문이다. 코드가 한 파일에 모두 모여 있으면 초기 개발은 빠를 수 있지만, 프로그램이 커질수록 "수정"과 "관리"가 불가능해 진다. 그러므로 분야별 분담과 유지보수 편의성을 위해 class부분을 분리하여 관리한다.

 

1.1. 파일별 역할 분담 구조
- main.py : 프로그램의 시작점. 전체 애플리케이션을 실행하고 메인 윈도우를 띄우는 진입로 역할
- Widget.py : 메인 화면의 큰 틀이나 공통 레이아웃을 정의하고 조각 파일들을 하나로 조립하는 본체 역할
- form_tab.py : 입력창, 콤보박스 등 데이터 입력과 관련된 UI와 로직만 집중적으로 관리
- buttons_tab.py: 버튼의 배치와 버튼을 눌렀을 때 실행될 기능(이벤트)만 따로 모아 관리

 

1.2. 소스코드를 분리하는 핵심 이유
- 가독성 향상 : 한 파일이 수천 줄이 되는 것을 방지하여 코드를 쉽게 읽을 수 있다.
- 유지보수 용이 : 버튼 기능을 수정할 때 buttons_tab.py만 열어서 고치면 되므로 다른 코드를 건드릴 위험이 줄어든다.
- 코드 재사용 : form_tab.py에서 만든 입력 양식을 다른 화면이나 다른 프로젝트에서 그대로 가져다 쓸 수 있다.
- 협업 수월 : 여러 개발자가 동시에 작업할 때, 파일이 분리되어 있으면 깃(Git) 충돌 없이 각자 맡은 화면을 개발할 수 있다.

 

※ 예시 : main.py 설명 
   from PyQt6.QtWidgets import QWidget   # 또는 PySide6.QtWidgets
   class Widget(QWidget) :   # QWidget(기본 위젯 클래스)을 상속받아 새로운 위젯 클래스를 정의함
   def __init__(self, parent=None) :  # 클래스가 생성될 때 자동으로 실행되는 초기화 메서드(생성자)
        super().__init__(parent) :   # 부모 클래스(QWidget)의 생성자를 호출해 위젯을 초기화, 부모-자식 관계 설정

 

2. Layout (QTabWidget) 예시 문제 실습

(예시 1)  간단한 계산기 만들기 (계산기 화면 결과값 오른쪽 정렬)

import sys
from PySide6.QtWidgets import QApplication, QWidget, QLabel, QPushButton, QGridLayout
from PySide6.QtCore import Qt


class CalculatorWindow(QWidget):
    def __init__(self):
        super().__init__()

        self.setWindowTitle("계산기")
        self.setMinimumSize(336, 541)

        # [1] 그리드 레이아웃 생성
        layout = QGridLayout(self)  # 위젯을 행과 열을 기준으로 배치하는 레이아웃이다.
        layout.setSpacing(4)
        layout.setContentsMargins(4, 4, 4, 4)

        # [2] 결과 라벨
    ## 계산 결과를 표시하는 리절트 라벨
        self.result_label = QLabel("0")
    ## 계산기 화면 결과값 오른쪽 정렬 한다.(AlignRight, AlignVCenter)
        self.result_label.setAlignment(Qt.AlignRight | Qt.AlignVCenter)
        self.result_label.setStyleSheet("font-size: 36px; padding: 8px;")
        layout.addWidget(self.result_label, 0, 0, 1, 4)

        # [3] 1줄: %, CE, C, ⌫
    ## 열은 0열 부터 3열까지 총 4개를 사용한다.
        btn_percent = QPushButton("%")
        btn_ce = QPushButton("CE")
        btn_c = QPushButton("C")
        btn_del = QPushButton("⌫")

        for b in (btn_percent, btn_ce, btn_c, btn_del):
            b.setMinimumHeight(50)
    ## 반복 코드 (버튼을 하나씩 변수로 만들고 하나씩 배치함, 계속 반복해서 코드가 길어진 단점)
        layout.addWidget(btn_percent, 1, 0)
        layout.addWidget(btn_ce, 1, 1)
        layout.addWidget(btn_c, 1, 2)
        layout.addWidget(btn_del, 1, 3)

    ## 결과 라벨이 0행을 차지하므로 버튼 배치는 1행부터 배치한다.
        # [4] 2줄: 1/x, x², √x, ÷
        btn_inv = QPushButton("1/x")
        btn_sq = QPushButton("x²")
        btn_sqrt = QPushButton("√x")
        btn_div = QPushButton("÷")

        for b in (btn_inv, btn_sq, btn_sqrt, btn_div):
            b.setMinimumHeight(50)

        layout.addWidget(btn_inv, 2, 0)
        layout.addWidget(btn_sq, 2, 1)
        layout.addWidget(btn_sqrt, 2, 2)
        layout.addWidget(btn_div, 2, 3)

        # [5] 3줄: 7 8 9 ×
        btn_7 = QPushButton("7")
        btn_8 = QPushButton("8")
        btn_9 = QPushButton("9")
        btn_mul = QPushButton("×")

        for b in (btn_7, btn_8, btn_9, btn_mul):
            b.setMinimumHeight(50)

        layout.addWidget(btn_7, 3, 0)
        layout.addWidget(btn_8, 3, 1)
        layout.addWidget(btn_9, 3, 2)
        layout.addWidget(btn_mul, 3, 3)

        # [6] 4줄: 4 5 6 −
        btn_4 = QPushButton("4")
        btn_5 = QPushButton("5")
        btn_6 = QPushButton("6")
        btn_sub = QPushButton("−")

        for b in (btn_4, btn_5, btn_6, btn_sub):
            b.setMinimumHeight(50)

        layout.addWidget(btn_4, 4, 0)
        layout.addWidget(btn_5, 4, 1)
        layout.addWidget(btn_6, 4, 2)
        layout.addWidget(btn_sub, 4, 3)

        # [7] 5줄: 1 2 3 +
        btn_1 = QPushButton("1")
        btn_2 = QPushButton("2")
        btn_3 = QPushButton("3")
        btn_add = QPushButton("+")

        for b in (btn_1, btn_2, btn_3, btn_add):
            b.setMinimumHeight(50)

        layout.addWidget(btn_1, 5, 0)
        layout.addWidget(btn_2, 5, 1)
        layout.addWidget(btn_3, 5, 2)
        layout.addWidget(btn_add, 5, 3)

        # [8] 6줄: +/- 0 . =
        btn_neg = QPushButton("+/-")
        btn_0 = QPushButton("0")
        btn_point = QPushButton(".")
        btn_eq = QPushButton("=")

        for b in (btn_neg, btn_0, btn_point, btn_eq):
            b.setMinimumHeight(50)

        layout.addWidget(btn_neg, 6, 0)
        layout.addWidget(btn_0, 6, 1)
        layout.addWidget(btn_point, 6, 2)
        layout.addWidget(btn_eq, 6, 3)

        # [9] 행 비율 설정
        layout.setRowStretch(0, 1)
        layout.setRowStretch(1, 2)
        layout.setRowStretch(2, 2)
        layout.setRowStretch(3, 2)
        layout.setRowStretch(4, 2)
        layout.setRowStretch(5, 2)
        layout.setRowStretch(6, 2)


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

 

(예시 1-1)

- 계산기 수정된 사항 : 앞에서 길게 반복해서 사용하던 코드를 간결하게 사용하였음을 확인함

-  계산기 만들기 코드 : 개선된 코드는 클린해진다. 클레스 상속을 여러번 받아서 사용하던 것을 2차원 리스트로 하여 코드가 간결하게 작성된다.  

 

(예시 1-2)  계산기 레이아웃 버튼 설명 (주석)

 

class CalculatorLayoutDemo(QWidget):
    def __init__(self):
        super().__init__()

        self.setWindowTitle("계산기 레이아웃 데모")
        self.setMinimumSize(336, 541)

        # [1] QGridLayout 생성
        layout = QGridLayout(self)
        layout.setSpacing(4)
        layout.setContentsMargins(4, 4, 4, 4)

        # [2] 결과 표시 라벨
        self.result_label = QLabel("0")
        self.result_label.setAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter)
        # self.result_label.setAlignment(Qt.AlignRight | Qt.AlignVCenter)
        self.result_label.setStyleSheet("font-size: 36px; padding: 8px;")
        layout.addWidget(self.result_label, 0, 0, 1, 4)

### 개선된 코드는 클린하다. 클래스 상속을 여러번 받아서 사용하던 것을 버튼 2차원 리스트로 하여 코드가 간결해진다.
        # [3] 계산기 버튼 텍스트를 2차원 리스트로 정의
        buttons = [
            ["%", "CE", "C", "⌫"],
            ["1/x", "x²", "√x", "÷"],
            ["7", "8", "9", "×"],
            ["4", "5", "6", "−"],
            ["1", "2", "3", "+"],
            ["+/-", "0", ".", "="],
        ]

        # [4] 버튼 생성 및 그리드에 배치
        row_offset = 1
### 버튼 반복문 안에서 만들때 개별 변수명을 안만들고 대신 딕셔너리에 저장해서 나중에 특정 버튼을 찾는다
        self.button_map = {}

        for row_index, row in enumerate(buttons):
            for col_index, text in enumerate(row):
                btn = QPushButton(text)
                btn.setMinimumHeight(50)
#### 인덱스 번호도 함께 얻는다 (row_index는 버튼 줄번호), (col_index는 열 번호)
                grid_row = row_offset + row_index
                grid_col = col_index
                layout.addWidget(btn, grid_row, grid_col)

                self.button_map[text] = btn

        # [5] 특정 버튼에 스타일 지정
        if "=" in self.button_map:
            self.button_map["="].setStyleSheet("font-weight: bold; font-size: 18px;")

        # [6] 행 스트레치 설정
        layout.setRowStretch(0, 1)
        for r in range(1, 7):
            layout.setRowStretch(r, 2)


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

 

(예시 1-3) 계산기 덧셈만 가능한 레이아웃

# 덧셈 기능 전체 코드
### 그런데 덧셈기능만 구현되는 계산기는 c버튼 클릭시 초기화가 되지 않는 문제가 있다.
import sys
from PySide6.QtWidgets import (
    QApplication, QWidget, QLabel, QPushButton, QGridLayout
)
from PySide6.QtCore import Qt


class CalculatorLayoutDemo(QWidget):
    def __init__(self):
        super().__init__()

        self.setWindowTitle("계산기 (덧셈 기능만)")
        self.setMinimumSize(336, 541)

        # ======== 계산 상태 변수 ========
        self.current_input = "0"  # 화면에 표시되는 현재 입력
        self.saved_value = None  # 이전 값
        self.is_add_mode = False  # + 버튼이 눌러진 상태 여부

        # ======== [1] 레이아웃 ========
        layout = QGridLayout(self)
        layout.setSpacing(4)
        layout.setContentsMargins(4, 4, 4, 4)

        # ======== [2] 결과 표시 ========
        self.result_label = QLabel("0")
        self.result_label.setAlignment(Qt.AlignRight | Qt.AlignVCenter)
        self.result_label.setStyleSheet("font-size: 36px; padding: 8px;")
        layout.addWidget(self.result_label, 0, 0, 1, 4)

        # ======== [3] 버튼 행렬 ========
        buttons = [
            ["%", "CE", "C", "⌫"],
            ["1/x", "x²", "√x", "÷"],
            ["7", "8", "9", "×"],
            ["4", "5", "6", "−"],
            ["1", "2", "3", "+"],
            ["+/-", "0", ".", "="],
        ]

        self.button_map = {}
        row_offset = 1

        for r, row in enumerate(buttons):
            for c, text in enumerate(row):
                btn = QPushButton(text)
                btn.setMinimumHeight(50)
                layout.addWidget(btn, row_offset + r, c)
                self.button_map[text] = btn

        # ======== 버튼 기능 연결 ========
        # 숫자 버튼
        for num in ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"]:
            self.button_map[num].clicked.connect(self.on_number_clicked)

        # 덧셈
        self.button_map["+"].clicked.connect(self.on_plus_clicked)

        # =
        self.button_map["="].clicked.connect(self.on_equal_clicked)

        # 초기값 표시
        self.update_display()

    # 숫자 입력 처리
    def on_number_clicked(self):
        text = self.sender().text()

        if self.current_input == "0":
            self.current_input = text
        else:
            self.current_input += text

        self.update_display()

    # + 동작
    def on_plus_clicked(self):
        self.saved_value = float(self.current_input)
        self.current_input = "0"
        self.is_add_mode = True
        self.update_display()

    # = 동작
    def on_equal_clicked(self):
        if self.is_add_mode and self.saved_value is not None:
            result = self.saved_value + float(self.current_input)
            self.current_input = str(result)

        self.is_add_mode = False
        self.saved_value = None
        self.update_display()

    # 화면 업데이트
    def update_display(self):
        self.result_label.setText(self.current_input)


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

 

(예시 1-4) 계산기 사칙연산이 가능하게 레이아웃 수정하기 

#### [사칙연산]이 가능한 구조개선 과제 (덧셈과 여러연산자 상태변수 일반화 하기)
#### 덧셈기능만 구현되는 계산기는 초기화 기능 및 사칙연산 계산 기능을 추가하여 문제를 해결함
import sys
from PySide6.QtWidgets import QApplication, QWidget, QGridLayout, QLabel, QPushButton
from PySide6.QtCore import Qt

class CalculatorLayoutDemo(QWidget):
    def __init__(self):
        super().__init__()
### 타이틀 제목을 수정했다. 사칙연산 구조 개선
        self.setWindowTitle("계산기(사칙연산 구조개선)")
        self.setMinimumSize(336, 541)

        # ======== 데이터 초기화 ========
        self.current_input = "0"      # 현재 입력 중인 숫자
        self.saved_value = None       # 저장된 이전 값
        self.current_operator = None  # 현재 선택된 연산자 (+, -, *, /)

        # ======== [1] 레이아웃 ========
        layout = QGridLayout(self)
        layout.setSpacing(4)
        layout.setContentsMargins(4, 4, 4, 4)

        # ======== [2] 결과 창 ========
        self.result_label = QLabel("0")
        # PySide6에서는 Qt.AlignmentFlag 형식을 권장합니다.
        self.result_label.setAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter)
        self.result_label.setStyleSheet("font-size: 36px; padding: 8px;")
        layout.addWidget(self.result_label, 0, 0, 1, 4)

        # ======== [3] 버튼 생성 ========
        buttons = [
            ["%", "CE", "C", "back"],
            ["1/x", "x²", "√x", "/"],
            ["7", "8", "9", "*"],
            ["4", "5", "6", "-"],
            ["1", "2", "3", "+"],
            ["+/-", "0", ".", "="],
        ]

        self.button_map = {}
        row_offset = 1

        for r, row in enumerate(buttons):
            for c, text in enumerate(row):
                btn = QPushButton(text)
                btn.setMinimumHeight(50)
                layout.addWidget(btn, row_offset + r, c)
                self.button_map[text] = btn

        # ======== 이벤트 연결 ========
        # 숫자 및 소수점 버튼
        for num in ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "."]:
            self.button_map[num].clicked.connect(self.on_number_clicked)

        # 사칙연산 버튼 (+, -, *, /)
### 사칙연산이 가능하도록 버튼을 눌렀을 때 값이 계산되도록 한다
        for op in ["+", "-", "*", "/"]:
            self.button_map[op].clicked.connect(self.on_operator_clicked)
### 기능버튼을 사용하여 값을 계산하도록 한다.
        # 기능 버튼
        self.button_map["="].clicked.connect(self.on_equal_clicked)
        self.button_map["C"].clicked.connect(self.on_clear_clicked)

### 화면 업데이트
        self.update_display()

    # 숫자 버튼 클릭 이벤트
    def on_number_clicked(self):
        text = self.sender().text()

        if text == ".":
            if "." in self.current_input:
                return  # 이미 소수점이 있으면 무시
            self.current_input += text
        elif self.current_input == "0":
            self.current_input = text
        else:
            self.current_input += text

        self.update_display()

    # 연산자 버튼 클릭 이벤트 (+, -, *, /)
    def on_operator_clicked(self):
        op = self.sender().text()
        self.saved_value = float(self.current_input)
        self.current_operator = op
        self.current_input = "0"
        self.update_display()

    # = 버튼 클릭 이벤트 (결과 계산)
    def on_equal_clicked(self):
        if self.current_operator is None or self.saved_value is None:
            return

        current_val = float(self.current_input)
        result = 0

        # 연산자별 계산
        if self.current_operator == "+":
            result = self.saved_value + current_val
        elif self.current_operator == "-":
            result = self.saved_value - current_val
        elif self.current_operator == "*":
            result = self.saved_value * current_val
        elif self.current_operator == "/":
            if current_val == 0:
                self.current_input = "Error (0으로 나눌 수 없음)"
                self.update_display()
                self.saved_value = None
                self.current_operator = None
                return
            result = self.saved_value / current_val

        # 소수점 아래가 0이면 정수로 변환, 아니면 실수 그대로 표시
        if result.is_integer():
            self.current_input = str(int(result))
        else:
            self.current_input = str(result)

        self.current_operator = None
        self.saved_value = None
        self.update_display()

    # C 버튼 클릭 이벤트 (초기화)
    def on_clear_clicked(self):
        self.current_input = "0"
        self.saved_value = None
        self.current_operator = None
        self.update_display()

    # 디스플레이 갱신
    def update_display(self):
        self.result_label.setText(self.current_input)


if __name__ == "__main__":
    app = QApplication(sys.argv)
    w = CalculatorLayoutDemo()
    w.show()
    sys.exit(app.exec())  # PySide6에서는 exec_() 대신 exec()을 사용합니다.

 

(예시 1-5) QTab Widget  버튼틀, 정보입력창 만들기 

 

main.py

from PySide6.QtWidgets import QApplication
from widget import Widget
import sys

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

 

widget.py

from PySide6.QtWidgets import (
    QWidget, QHBoxLayout, QVBoxLayout,
    QTabWidget, QPushButton, QLabel, QLineEdit
)

class Widget(QWidget):
    def __init__(self):
        super().__init__()

        # 윈도우 제목
        self.setWindowTitle("QTabWidget 데모")

        # [1] 탭 위젯 생성
        tab_widget = QTabWidget(self)

        # [2] 첫 번째 탭 : 정보 입력 탭
        widget_form = QWidget()
        label_full_name = QLabel("이름 :")
        line_edit_full_name = QLineEdit()
        line_edit_full_name.setPlaceholderText("이름을 입력하세요")

        form_layout = QHBoxLayout()
        form_layout.addWidget(label_full_name)
        form_layout.addWidget(line_edit_full_name)
        widget_form.setLayout(form_layout)

        # [3] 두 번째 탭 : 버튼 탭
        widget_buttons = QWidget()
        button_1 = QPushButton("버튼 1")
        button_2 = QPushButton("버튼 2")
        button_3 = QPushButton("버튼 3")

        # 버튼 1 클릭 시 슬롯 함수 호출
        button_1.clicked.connect(self.button_1_clicked)

        buttons_layout = QVBoxLayout()
        buttons_layout.addWidget(button_1)  # 버튼 크기 결정 값
        buttons_layout.addWidget(button_2)  # 버튼 크기 결정 값
        buttons_layout.addWidget(button_3)  # 버튼 크기 결정 값
        widget_buttons.setLayout(buttons_layout)

        # [4] 탭 위젯에 탭 추가
    ### 처음의 위젯 버튼에 "정보입력" 버튼이 있고, 추가로 두번째 클릭 되는 '버튼틀'을 추가한다.
        tab_widget.addTab(widget_form, "정보 입력")
        tab_widget.addTab(widget_buttons, "버튼들")

        # [5] 위젯 전체 레이아웃 설정
        layout = QVBoxLayout()
        layout.addWidget(tab_widget)
        self.setLayout(layout)

    # [6] 슬롯 함수 : 버튼 1 클릭 시 동작
    def button_1_clicked(self):
        print("버튼 1이 클릭되었습니다.")

 

(예시 1-6) QTab Widget  버튼틀 누르기

            버튼 1동작, 버튼틀 추가  수정하기

 

main.py

from PySide6.QtWidgets import QApplication
from widget import Widget
import sys

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

 

widget.py

from PySide6.QtWidgets import (
    QWidget, QHBoxLayout, QVBoxLayout,
    QTabWidget, QPushButton, QLabel, QLineEdit
)

class Widget(QWidget):
    def __init__(self):
        super().__init__()

        # 윈도우 제목
        self.setWindowTitle("QTabWidget 데모")

        # [1] 탭 위젯 생성
        tab_widget = QTabWidget(self)

        # [2] 첫 번째 탭 : 정보 입력 탭
        widget_form = QWidget()
        label_full_name = QLabel("이름 :")
        line_edit_full_name = QLineEdit()
        line_edit_full_name.setPlaceholderText("이름을 입력하세요")

        form_layout = QHBoxLayout()
        form_layout.addWidget(label_full_name)
        form_layout.addWidget(line_edit_full_name)
        widget_form.setLayout(form_layout)

        # [3] 두 번째 탭 : 버튼 탭
        widget_buttons = QWidget()
        button_1 = QPushButton("버튼 1")
        button_2 = QPushButton("버튼 2")
        button_3 = QPushButton("버튼 3")

        # 버튼 1 클릭 시 슬롯 함수 호출
        button_1.clicked.connect(self.button_1_clicked)

        buttons_layout = QVBoxLayout()
        buttons_layout.addWidget(button_1)  # 버튼 크기 결정 값
        buttons_layout.addWidget(button_2)  # 버튼 크기 결정 값
        buttons_layout.addWidget(button_3)  # 버튼 크기 결정 값
        widget_buttons.setLayout(buttons_layout)

        # [4] 탭 위젯에 탭 추가
    ### 처음의 위젯 버튼에 "정보입력" 버튼이 있고, 추가로 두번째 클릭 되는 '버튼틀'을 추가한다.
        tab_widget.addTab(widget_form, "정보 입력")
        tab_widget.addTab(widget_buttons, "버튼들")

        # [5] 위젯 전체 레이아웃 설정
        layout = QVBoxLayout()
        layout.addWidget(tab_widget)
        self.setLayout(layout)

    # [6] 슬롯 함수 : 버튼 1 클릭 시 동작
    def button_1_clicked(self):
        print("버튼 1이 클릭되었습니다.")

 

 

(예시 ) QTab Widget 정보 입력하고 이름 확인 클릭시 출력이 된다.

 

main.py

from PySide6.QtWidgets import QApplication
from widget import Widget
import sys

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

 

widget.py

from PySide6.QtWidgets import (
    QWidget, QHBoxLayout, QVBoxLayout,
    QTabWidget, QPushButton, QLabel, QLineEdit
)

class Widget(QWidget):
    def __init__(self):
        super().__init__()

        # 윈도우 제목
        self.setWindowTitle("QTabWidget 데모")

        # [1] 탭 위젯 생성
        tab_widget = QTabWidget(self)

        # [2] 첫 번째 탭 : 정보 입력 탭
        widget_form = QWidget()
        label_full_name = QLabel("이름 :")

        # <추가 실습 과제> 인스턴스 변수로 변경
        self.line_edit_full_name = QLineEdit()
        self.line_edit_full_name.setPlaceholderText("이름을 입력하세요")

        # <추가 실습 과제> 이름 확인 버튼 추가
        info_button = QPushButton("이름 확인")
        info_button.clicked.connect(self.info_button_clicked)

        # 기존: HBoxLayout → 수정: 이름 입력 + 버튼 수직 배치
        row_layout = QHBoxLayout()
        row_layout.addWidget(label_full_name)
        row_layout.addWidget(self.line_edit_full_name)

        form_layout = QVBoxLayout()   # <추가 실습 과제> 탭 구성 변경
        form_layout.addLayout(row_layout)
        form_layout.addWidget(info_button)  # <추가 실습 과제>
        widget_form.setLayout(form_layout)

        # [3] 두 번째 탭 : 버튼 탭
        widget_buttons = QWidget()
        button_1 = QPushButton("버튼 1")
        button_2 = QPushButton("버튼 2")
        button_3 = QPushButton("버튼 3")

        # 버튼 클릭 시 슬롯 함수 호출
        button_1.clicked.connect(self.button_1_clicked)
        button_2.clicked.connect(self.button_2_clicked)
        button_3.clicked.connect(self.button_3_clicked)

        buttons_layout = QVBoxLayout()
        buttons_layout.addWidget(button_1)
        buttons_layout.addWidget(button_2)
        buttons_layout.addWidget(button_3)
        widget_buttons.setLayout(buttons_layout)

        # [4] 탭 위젯에 탭 추가
        tab_widget.addTab(widget_form, "정보 입력")
        tab_widget.addTab(widget_buttons, "버튼들")

        # [5] 위젯 전체 레이아웃 설정
        layout = QVBoxLayout()
        layout.addWidget(tab_widget)
        self.setLayout(layout)

    # [6] 슬롯 함수 : 버튼 1 클릭 시 동작
    def info_button_clicked(self):
        # <추가 실습 과제> 이름 입력 여부 확인 후 출력
        name = self.line_edit_full_name.text().strip()
#### 탭에서 이름을 입력한 후 "이름확인"을 클릭하면 클릭했다는 메세지가 출력된다.
        if not name:
            print("이름을 먼저 입력해 주세요.")
        else:
            print(f"{name}님, info 출력 클릭하셨습니다.")

    def button_1_clicked(self):
        print("버튼 2가 클릭되었습니다.")

    def button_2_clicked(self):
        print("버튼 2가 클릭되었습니다.")

    def button_3_clicked(self):
        print("버튼 3이 클릭되었습니다.")

 

 

(예시 ) QTab Widget 레이아웃 버튼을 눌러서 출력결과 확인 하기

 

 

(예시 ) QTab Widget 버튼 클릭시 클릭되었다는 메세지 출력 확인 

 

main.py

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

 

widget.py

from PySide6.QtWidgets import (
    QWidget, QHBoxLayout, QVBoxLayout,
    QTabWidget, QPushButton, QLabel, QLineEdit
)

class Widget(QWidget):
    def __init__(self):
        super().__init__()

        # 윈도우 제목
        self.setWindowTitle("QTabWidget 데모")

        # [1] 탭 위젯 생성
        tab_widget = QTabWidget(self)

        # <추가 실습 과제3> 탭 클릭 시 시그널 연결
        tab_widget.currentChanged.connect(self.tab_changed)

        # [2] 첫 번째 탭 : 정보 입력 탭
        widget_form = QWidget()
        label_full_name = QLabel("이름 :")

        # <추가 실습 과제> 인스턴스 변수로 변경
        self.line_edit_full_name = QLineEdit()
        self.line_edit_full_name.setPlaceholderText("이름을 입력하세요")

        # <추가 실습 과제> 이름 확인 버튼 추가
        info_button = QPushButton("이름 확인")
        info_button.clicked.connect(self.info_button_clicked)

        # 기존: HBoxLayout → 수정: 이름 입력 + 버튼 수직 배치
        row_layout = QHBoxLayout()
        row_layout.addWidget(label_full_name)
        row_layout.addWidget(self.line_edit_full_name)

        form_layout = QVBoxLayout()   # <추가 실습 과제> 탭 구성 변경
        form_layout.addLayout(row_layout)
        form_layout.addWidget(info_button)  # <추가 실습 과제>
        widget_form.setLayout(form_layout)

        # [3] 두 번째 탭 : 버튼 탭
        widget_buttons = QWidget()
        button_1 = QPushButton("버튼 1")
        button_2 = QPushButton("버튼 2")
        button_3 = QPushButton("버튼 3")

        # 버튼 클릭 시 슬롯 함수 호출
        button_1.clicked.connect(self.button_1_clicked)
        button_2.clicked.connect(self.button_2_clicked)
        button_3.clicked.connect(self.button_3_clicked)

        buttons_layout = QVBoxLayout()
        buttons_layout.addWidget(button_1)
        buttons_layout.addWidget(button_2)
        buttons_layout.addWidget(button_3)
        widget_buttons.setLayout(buttons_layout)

        # [4] 탭 위젯에 탭 추가
        tab_widget.addTab(widget_form, "정보 입력")
        tab_widget.addTab(widget_buttons, "버튼들")

        # [5] 위젯 전체 레이아웃 설정
        layout = QVBoxLayout()
        layout.addWidget(tab_widget)
        self.setLayout(layout)

    # [6] 슬롯 함수 : 버튼 1 클릭 시 동작
    def info_button_clicked(self):
        # <추가 실습 과제> 이름 입력 여부 확인 후 출력
        name = self.line_edit_full_name.text().strip()

        if not name:
            print("이름을 먼저 입력해 주세요.")
        else:
            print(f"{name}님, info 출력 클릭하셨습니다.")

    def button_1_clicked(self):
        print("버튼 2가 클릭되었습니다.")

    def button_2_clicked(self):
        print("버튼 2가 클릭되었습니다.")

    def button_3_clicked(self):
        print("버튼 3이 클릭되었습니다.")

### 웹 하나의 창에서 Layout의 버튼 중복 배치를 통해 탭을 클릭하면 동작이 출력하는 코드이다.

    # <추가 실습 과제3> 탭을 클릭하면 탭 이름 출력
    def tab_changed(self, index):
        tab_widget = self.findChild(QTabWidget)  # 현재 탭 위젯 찾기
        tab_text = tab_widget.tabText(index)     # 클릭한 탭의 이름 가져오기
        print(f"'{tab_text}' 탭을 클릭했습니다.")

        # <추가 실습 과제4> 첫 번째 탭에서 두 번째 탭으로 이동하는 슬롯
        def go_to_second_tab(self):
            # 현재 위젯(self) 안에서 QTabWidget 타입의 자식 위젯을 찾아온다.
            tab_widget = self.findChild(QTabWidget)

            # 탭 위젯을 정상적으로 찾았는지 확인한다.
            if tab_widget is not None:
                # 인덱스 1번 탭으로 이동한다.
                # 0 → 첫 번째 탭: "정보 입력"
                # 1 → 두 번째 탭: "버튼들"
                tab_widget.setCurrentIndex(1)

#### 탭에서 이동할 때 슬롯 함수를 사용해 버튼 누름이 첫번째 탭으로 이동하는 것을 구현해 본다.
#### 처음 실행 화면부터 정보입력 창이 실행되는 메세지가 출력된다. 이후 클릭 순서에 따라 메세지가 출력된다
        # <추가 실습 과제4> 두 번째 탭에서 첫 번째 탭으로 이동하는 슬롯
        def go_to_first_tab(self):
            # 현재 위젯(self) 안에서 QTabWidget 타입의 자식 위젯을 찾아온다.
            tab_widget = self.findChild(QTabWidget)

            # 탭 위젯을 정상적으로 찾았는지 확인한다.
            if tab_widget is not None:
                # 인덱스 0번 탭으로 이동한다.
                # 0 → 첫 번째 탭: "정보 입력"
                # 1 → 두 번째 탭: "버튼들"
                tab_widget.setCurrentIndex(0)

 

 

(예시 ) QTab Widge 레이아웃 첫화면 탭 

 

 

(예시 ) QTab Widge 스크롤 레이아웃 항목 추가 하기

 

main.py

# 위젯파일과 메인파일로 분리해서 메인파일을 실행시키면 엡창이 실행된다.
#### tab_scroll_area.py 에서 메인에 분리할 참조 내용들 ####
#from PySide6.QtWidgets import (
#    QWidget, QVBoxLayout, QScrollArea,
#    QLabel, QPushButton
#)
#from PySide6.QtCore import Qt
#class ScrollAreaTab(QWidget):
#    def __init__(self, parent=None):
#        super().__init__(parent)

#### Widget에서 분리 생성된 main.py 내용들 ####
import sys
from PySide6.QtWidgets import QApplication, QMainWindow

# widget.py 파일에서 class 부분 ScrollAreaTab 클래스를 가져오기
from widget import ScrollAreaTab

class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("Scroll Area Tab Example")
        self.resize(400, 500)

        # 위젯 생성 후 메인 윈도우의 중앙 위젯으로 설정
        self.tab_widget = ScrollAreaTab()
        self.setCentralWidget(self.tab_widget)

if __name__ == "__main__":   # 스크립트가 직접 실행될 때만 코드 실행하기
    app = QApplication(sys.argv)   # 애플리케이션 객체를 생성, 명령행 인수를 전달해 Qt 환경을 초기화 함
    w = MainWindow()  # 위젯파이에서 클래스 위젯을 가리키며, 위젯 객체(윈도우 창)를 생성 한다.
    w.show()          # 생성한 위젯 창을 화면에 표시 한다.
    sys.exit(app.exec())   # Qt의 메인 이벤트 루프를 시작, 프로그램 종료시 안전하게 리소스를 해제한다

 

widget.py

# tab_scroll_area.py
from PySide6.QtWidgets import (
    QWidget, QVBoxLayout, QScrollArea,
    QLabel, QPushButton
)
from PySide6.QtCore import Qt


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

        layout = QVBoxLayout(self)

        self.add_button = QPushButton("항목 추가")
        self.add_button.clicked.connect(self.add_item)
        layout.addWidget(self.add_button)

# QScrollArea 사용하기
        self.scroll_area = QScrollArea()
        self.scroll_area.setWidgetResizable(True)
        layout.addWidget(self.scroll_area)

 # 스크롤 안에서 실제 내용은 별도의 QWidget + QVBoxLayout  실행한다. 내용이 많으면 자동스크롤바 생성
        self.content_widget = QWidget()
        self.content_layout = QVBoxLayout(self.content_widget)
        self.content_layout.setAlignment(Qt.AlignTop)

        self.scroll_area.setWidget(self.content_widget)

        self.item_count = 0
        for i in range(5):
            self.add_label(f"초기 항목 {i + 1}")
        self.item_count = 5

    def add_label(self, text: str):
        label = QLabel(text)
        self.content_layout.addWidget(label)

    def add_item(self):
        self.item_count += 1
        new_text = f"추가된 항목 {self.item_count}"
        self.add_label(new_text)
        print(new_text)
# 항목 추가시 자동으로 맨 아래로 스크롤바 생성
        bar = self.scroll_area.verticalScrollBar()
        bar.setValue(bar.maximum())

 

 

(예시 ) QTab Widge  스크롤 바 생성하기 

 

 

 

 

(예시 ) QTab Widge 스크롤 로그 추가 하기

 

main.py

# 스크롤을 사용자가 편집하지 못하게 전형적인 로그창 스타일 실습하기
import sys
from PySide6.QtWidgets import QApplication, QMainWindow

# widget.py 파일에서 class 부분 ScrollAreaTab 클래스를 가져오기
from widget import LogTextEditTab

class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("LogTextEditTab")
        self.resize(400, 500)

        # 위젯 생성 후 메인 윈도우의 중앙 위젯으로 설정
        self.tab_widget = LogTextEditTab()
        self.setCentralWidget(self.tab_widget)

if __name__ == "__main__":   # 스크립트가 직접 실행될 때만 코드 실행하기
    app = QApplication(sys.argv)   # 애플리케이션 객체를 생성, 명령행 인수를 전달해 Qt 환경을 초기화 함
    w = MainWindow()  # 위젯파이에서 클래스 위젯을 가리키며, 위젯 객체(윈도우 창)를 생성 한다.
    w.show()          # 생성한 위젯 창을 화면에 표시 한다.
    sys.exit(app.exec())   # Qt의 메인 이벤트 루프를 시작, 프로그램 종료시 안전하게 리소스를 해제한다

 

widget.py

# tab_log_textedit.py
from PySide6.QtWidgets import (
    QWidget, QVBoxLayout,
    QPushButton, QTextEdit
)
from PySide6.QtGui import QTextCursor


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

        layout = QVBoxLayout(self)

        self.log_textedit = QTextEdit()
        self.log_textedit.setReadOnly(True)
        layout.addWidget(self.log_textedit)

        self.log_button = QPushButton("로그 추가")
        self.log_button.clicked.connect(self.add_log)
        layout.addWidget(self.log_button)

        self.log_count = 0

    def add_log(self):
        self.log_count += 1
        message = f"[로그] {self.log_count}번째 메시지입니다."
        self.log_textedit.append(message)
        print(message)

        cursor = self.log_textedit.textCursor()
        cursor.movePosition(QTextCursor.End)
        self.log_textedit.setTextCursor(cursor)
        self.log_textedit.ensureCursorVisible()

 

 

(예시 ) QTab Widge  스크롤 통합 (스크롤 영역)

 

 

(예시 ) QTab Widge 스크롤 통합 리스트 작성 (리스트 스크롤)

 

 

(예시 ) QTab Widge 스크롤 통합 로그 실습 (로그 스크롤)

 

 

(예시 ) QTab Widge  스크롤 통합 로그 

 

main.py

# 스크롤을 사용자가 편집하지 못하게 전형적인 로그창 스타일 실습하기
import sys
from PySide6.QtWidgets import QApplication, QMainWindow

# widget.py 파일에서 class 부분 ScrollAreaTab 클래스를 가져오기
from widget import LogTextEditTab

class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("LogTextEditTab")
        self.resize(400, 500)

        # 위젯 생성 후 메인 윈도우의 중앙 위젯으로 설정
        self.tab_widget = LogTextEditTab()
        self.setCentralWidget(self.tab_widget)

if __name__ == "__main__":   # 스크립트가 직접 실행될 때만 코드 실행하기
    app = QApplication(sys.argv)   # 애플리케이션 객체를 생성, 명령행 인수를 전달해 Qt 환경을 초기화 함
    w = MainWindow()  # 위젯파이에서 클래스 위젯을 가리키며, 위젯 객체(윈도우 창)를 생성 한다.
    w.show()          # 생성한 위젯 창을 화면에 표시 한다.
    sys.exit(app.exec())   # Qt의 메인 이벤트 루프를 시작, 프로그램 종료시 안전하게 리소스를 해제한다

 

widget.py

# tab_log_textedit.py
from PySide6.QtWidgets import (
    QWidget, QVBoxLayout,
    QPushButton, QTextEdit
)
from PySide6.QtGui import QTextCursor


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

        layout = QVBoxLayout(self)

        self.log_textedit = QTextEdit()
        self.log_textedit.setReadOnly(True)
        layout.addWidget(self.log_textedit)

        self.log_button = QPushButton("로그 추가")
        self.log_button.clicked.connect(self.add_log)
        layout.addWidget(self.log_button)

        self.log_count = 0

    def add_log(self):
        self.log_count += 1
        message = f"[로그] {self.log_count}번째 메시지입니다."
        self.log_textedit.append(message)
        print(message)

        cursor = self.log_textedit.textCursor()
        cursor.movePosition(QTextCursor.End)
        self.log_textedit.setTextCursor(cursor)
        self.log_textedit.ensureCursorVisible()

 

 

(예시 ) Layout Widget 위에서 실습한 파일들 분리하기 

※ 소스코드를 분리하는 핵심 이유
- 가독성 향상 : 한 파일이 수천 줄이 되는 것을 방지하여 코드를 쉽게 읽을 수 있다.
- 유지보수 용이 : 버튼 기능을 수정할 때 buttons_tab.py만 열어서 고치면 되므로 다른 코드를 건드릴 위험이 줄어든다.
- 코드 재사용 : form_tab.py에서 만든 입력 양식을 다른 화면이나 다른 프로젝트에서 그대로 가져다 쓸 수 있다.
- 협업 수월 : 여러 개발자가 동시에 작업할 때, 파일이 분리되어 있으면 깃(Git) 충돌 없이 각자 맡은 화면을 개발할 수 있다.

# __init__.py """ Layout 복합적인 레이아웃 예제 학습 내용이다."""
# main.py

import sys

if __name__ == "__main__":
    app = QApplication(sys.argv)
    w = Widget()
    w.show()
    sys.exit(app.exec())
# tab_list_widget.py 클래스에서 리스트 분리

from PySide6.QtWidgets import (
    QWidget, QVBoxLayout,
    QPushButton, QListWidget
)


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

        layout = QVBoxLayout(self)

        self.add_button = QPushButton("항목 추가")
        self.add_button.clicked.connect(self.add_item)
        layout.addWidget(self.add_button)

        self.list_widget = QListWidget()
        layout.addWidget(self.list_widget)

        self.item_count = 0
        for i in range(10):
            self.list_widget.addItem(f"초기 항목 {i + 1}")
        self.item_count = 10

    def add_item(self):
        self.item_count += 1
        text = f"추가 항목 {self.item_count}"
        self.list_widget.addItem(text)
        print(text)

        self.list_widget.scrollToBottom()
# tab_log_textedit.py  클래스에서 로그 텍스트에디트 부분 분리

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

        layout = QVBoxLayout(self)

        self.log_textedit = QTextEdit()
        self.log_textedit.setReadOnly(True)
        layout.addWidget(self.log_textedit)

        self.log_button = QPushButton("로그 추가")
        self.log_button.clicked.connect(self.add_log)
        layout.addWidget(self.log_button)

        self.log_count = 0

    def add_log(self):
        self.log_count += 1
        message = f"[로그] {self.log_count}번째 메시지입니다."
        self.log_textedit.append(message)
        print(message)

        cursor = self.log_textedit.textCursor()
        cursor.movePosition(QTextCursor.End)
        self.log_textedit.setTextCursor(cursor)
        self.log_textedit.ensureCursorVisible()
# tab_scroll_area.py 클래스에서 스크롤 부분 분리

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

        layout = QVBoxLayout(self)

        self.add_button = QPushButton("항목 추가")
        self.add_button.clicked.connect(self.add_item)
        layout.addWidget(self.add_button)

        self.scroll_area = QScrollArea()
        self.scroll_area.setWidgetResizable(True)
        layout.addWidget(self.scroll_area)

        self.content_widget = QWidget()
        self.content_layout = QVBoxLayout(self.content_widget)
        self.content_layout.setAlignment(Qt.AlignTop)

        self.scroll_area.setWidget(self.content_widget)

        self.item_count = 0
        for i in range(5):
            self.add_label(f"초기 항목 {i + 1}")
        self.item_count = 5

    def add_label(self, text: str):
        label = QLabel(text)
        self.content_layout.addWidget(label)

    def add_item(self):
        self.item_count += 1
        new_text = f"추가된 항목 {self.item_count}"
        self.add_label(new_text)
        print(new_text)

        bar = self.scroll_area.verticalScrollBar()
        bar.setValue(bar.maximum())
# widget.py 위젯 클래스(부모) 
# from PyQt6.QtWidgets import QWidget  # 또는 PySide6.QtWidgets

class Widget(QWidget):  # QWidget(기본 위젯 클래스)을 상속받아 새로운 위젯 클래스를 정의함

    def __init__(self, parent=None):  # 클래스가 생성될 때 자동으로 실행되는 초기화 메서드(생성자)
    
       # 부모 클래스(QWidget)의 생성자를 호출하여 위젯을 올바르게 초기화하고 부모-자식 관계를 설정함
        super().__init__(parent)  


        self.setWindowTitle("스크롤 예제 통합 (QTabWidget)")
        self.resize(420, 340)

        layout = QVBoxLayout(self)

        self.tab_widget = QTabWidget()
        layout.addWidget(self.tab_widget)

        scroll_tab = ScrollAreaTab(self)
        list_tab = ListWidgetTab(self)
        log_tab = LogTextEditTab(self)

        self.tab_widget.addTab(scroll_tab, "스크롤 영역")
        self.tab_widget.addTab(list_tab, "리스트 스크롤")
        self.tab_widget.addTab(log_tab, "로그 스크롤")

 

※ widget.py 위젯 클래스(부모) 
   from PyQt6.QtWidgets import QWidget   # 또는 PySide6.QtWidgets
   class Widget(QWidget):   # QWidget(기본 위젯 클래스)을 상속받아 새로운 위젯 클래스를 정의함
   def __init__(self, parent=None):  # 클래스가 생성될 때 자동으로 실행되는 초기화 메서드(생성자)
        super().__init__(parent)   # 부모 클래스(QWidget)의 생성자를 호출해 위젯을 초기화 하고 부모-자식 관계를 설정함

end.