728x90

1. 목표

더보기
  1. 메뉴/툴바의 [Save] 메뉴(actionSave) 를 클릭했을 때
  2. QFileDialog 로 경로를 선택하고,
  3. QFile + QIODevice + QTextStream 을 사용해서
    QTextEdit 의 내용을 텍스트 파일로 저장한다.
  4. Qt 파일 I/O 흐름을 익히고
    QTextStream 의 기본 인코딩이 UTF-8 이라는 점도 같이 이해한다.
  5. 성공 / 실패 메시지를 보여준다.

 

아직은 “현재 파일 이름 기억하기”, “수정됨 표시” 같은 복잡한 부분은 생략하고, 

“일단 저장이 된다” 에만 집중하는 단계라고 생각하시면 좋습니다.

 

2. 전체 로직

더보기
# mainwindow.py

from PySide6.QtWidgets import QMainWindow, QFileDialog, QMessageBox
from PySide6.QtCore import QFile, QIODevice, QDir, QTextStream

from ui_form import Ui_MainWindow


class MainWindow(QMainWindow):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.ui = Ui_MainWindow()
        self.ui.setupUi(self)

        self.ui.actionSave.triggered.connect(self.save_file)

    def save_file(self) -> bool:
        filename, _ = QFileDialog.getSaveFileName(
            self,
            "파일 저장",
            QDir.homePath(),
            "Text Files (*.txt);;All Files (*)"
        )

        if not filename:
            return False

        file = QFile(filename)
        if not file.open(QIODevice.WriteOnly | QIODevice.Text):
            QMessageBox.critical(self, "오류", f"파일을 열 수 없습니다:\n{file.errorString()}")
            return False

        stream = QTextStream(file)
        stream << self.ui.textEdit.toPlainText()
        file.close()

        QMessageBox.information(self, "저장 성공", f"파일이 저장되었습니다.\n\n{filename}")
        return True
# main.py

import sys
from PySide6.QtWidgets import QApplication

from mainwindow import MainWindow

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

 

3. 필요한 import + 주석 설명

더보기
# QApplication : PySide6 애플리케이션과 메인 윈도우의 기본 클래스
# QFileDialog : 파일 저장/열기 표준 대화상자
# QMessageBox : 정보/경고/오류 메시지 창
from PySide6.QtWidgets import QMainWindow, QFileDialog, QMessageBox

# QFile       : Qt 의 파일 클래스, open()/close() 를 통해 QIODevice 로 동작
# QIODevice   : 읽기/쓰기 모드를 지정하기 위한 플래그(ReadOnly, WriteOnly 등)
# QDir        : 디렉터리 유틸리티, homePath(), currentPath() 등 제공
# QTextStream : QFile 과 연결해서 텍스트를 읽고 쓰는 스트림 클래스
from PySide6.QtCore import QFile, QIODevice, QDir, QTextStream

# Qt Designer 로 만든 form.ui 를 pyside6-uic 로 변환한 코드
# 이 안에 textEdit, actionSave 등 위젯과 액션이 정의되어 있다
from ui_form import Ui_MainWindow

Ui_MainWindow 는 이미 9.1, 9.2 단계에서 만들어 둔 메뉴와 아이콘들을 포함한 완성된 GUI 클래스입니다.

파일 I/O 를 Qt 쪽 세트(QFile, QIODevice, QTextStream)로 통일한다 는 점입니다.
그래야 나중에 Qt 네트워크, 소켓, 리소스 등과 연결할 때도 흐름이 동일해집니다.


 

4. 메뉴 액션과 슬롯 연결하기 (actionSave ↔ save_file)

더보기
class MainWindow(QMainWindow):
    def __init__(self, parent=None):
        super().__init__(parent)

        self.ui = Ui_MainWindow()
        self.ui.setupUi(self)

        # 9.3 파일 저장하기 기능 연결
        # actionSave 가 클릭되거나 단축키(Ctrl+S)가 눌리면 save_file 메서드를 호출한다
        self.ui.actionSave.triggered.connect(self.save_file)
  • self.ui.actionSave
    • .ui 에서 변환 ui_form.py 안에서 생성된 QAction 객체입니다.
    • [File] → [Save] 메뉴, 툴바의 디스크 아이콘, 단축키 Ctrl+S 에 모두 연결되어 있습니다.
  • .triggered
    • QAction 이 실행될 때(클릭/단축키) 발생하는 시그널입니다.
  • .connect(self.save_file)
    • 이 시그널이 발생하면 MainWindow.save_file() 메서드를 호출해 달라는 의미입니다.
  • 이렇게 <디자이너에서 만든 UI 액션>과 <파이썬 로직>을 연결하는 것이 GUI 프로그래밍의 핵심 패턴입니다.

 

5. 실제 파일 저장하기 기능 구현 함수

더보기
class MainWindow(QMainWindow):
    ...

    def save_file(self) -> bool:
        """
        [9.3] 파일 저장하기 기능
        - 파일 저장 대화상자를 띄워서 파일 이름을 선택하고
        - Qt 의 QFile + QTextStream 을 사용해 textEdit 내용을 저장한다.
        - 성공 여부를 bool 로 반환한다.
        """

        # 1) QFileDialog 를 이용해 저장할 파일 이름을 선택한다
        #    QDir.homePath() 를 초기 폴더로 사용하면 사용자의 홈 디렉터리가 기본으로 열린다
        filename, _ = QFileDialog.getSaveFileName(
            self,
            "파일 저장",
            QDir.homePath(),
            "Text Files (*.txt);;All Files (*)"
        )

        # 2) 사용자가 '취소'를 눌렀다면 filename 이 빈 문자열("") 이므로 저장을 중단한다
        if not filename:
            return False

        # 3) QFile 객체를 생성하고, 지정한 경로에 쓰기 모드로 연다
        file = QFile(filename)

        # WriteOnly | Text 플래그로 열기
        # - WriteOnly : 쓰기 전용 모드
        # - Text      : 텍스트 모드(윈도우의 \r\n 을 \n 으로 변환하는 등 텍스트 처리)
        if not file.open(QIODevice.WriteOnly | QIODevice.Text):
            # open 이 실패하면 errorString() 에 원인이 들어 있다
            QMessageBox.critical(
                self,
                "오류",
                f"파일을 열 수 없습니다.\n\n{file.errorString()}"
            )
            return False

        # 4) QTextStream 을 QFile 과 연결해서 텍스트를 기록한다
        #    QFile 을 직접 write() 해도 되지만, QTextStream 을 쓰면
        #    유니코드 처리와 줄 단위, 숫자 포맷 등 다양한 기능을 편하게 쓸 수 있다
        stream = QTextStream(file)

        # Qt6 기준으로 QTextStream 은 기본적으로 UTF-8 인코딩을 사용하므로
        # 별도 인코딩 설정 없이 한글을 포함한 텍스트를 안전하게 저장할 수 있다
        # 필요하다면 stream.setEncoding(...) 으로 변경할 수 있다

        # 5) QTextEdit 의 전체 텍스트를 가져와 스트림에 쓴다
        text = self.ui.textEdit.toPlainText()
        stream << text

        # 6) 작업이 끝나면 QFile 을 닫아준다
        file.close()

        # 7) 사용자에게 저장이 완료되었음을 알린다
        QMessageBox.information(
            self,
            "저장 성공",
            f"파일이 저장되었습니다.\n\n{filename}"
        )

        return True

이 함수는 이후 단계(예: 9.4 새 파일, 9.5 기존 파일 열기)에서도 재사용될 수 있습니다.
그때는 self.current_file 같은 멤버 변수를 추가해서 “이미 한 번 저장한 문서는 같은 파일로 저장”하는 로직을 확장할 수 있습니다.



6. 실행 테스트

 

7. 학습 주요 포인트

더보기

파이썬 open() 대신 Qt 파일 I/O 사용

  • 단순 예제에서는 open() 이 더 간단하지만,
    Qt 애플리케이션에서는 QFile + QIODevice + QTextStream 을 사용하는 쪽이
    다른 Qt 기능들과 연동할 때 훨씬 자연스럽다
  • 네트워크, 소켓, 메모리 버퍼 등도 모두 QIODevice 기반이라,
    장치(QIODevice) + 스트림(QTextStream)” 패턴을 익혀두면 재사용성이 올라간다.

 

QFileDialog.getSaveFileName() 의 반환값 처리

  • (filename, filter) 튜플을 반환한다.
  • filename 이 빈 문자열이면 사용자가 대화상자를 취소한 것이므로,
    저장을 진행하면 안 된다.

 

QFile.open(QIODevice.WriteOnly | QIODevice.Text)

  • WriteOnly : 쓰기 전용
  • Text : 텍스트 줄바꿈 처리 등 텍스트 모드로 사용
  • 실패하면 False 를 반환하고, file.errorString() 으로 원인 문자열을 확인 가능.

 

QTextStream 의 역할

  • QFile 에 연결된 텍스트 스트림으로,
    stream << "문자열"; 형식으로 쉽게 쓰기 가능.
  • 내부적으로 유니코드/인코딩을 처리해 주기 때문에
    한글 포함 텍스트도 안전하게 저장할 수 있다.

 

UI 액션과 로직 연결 패턴

  • self.ui.actionSave.triggered.connect(self.save_file)
  • Qt Designer 에서 만든 메뉴/툴바 액션과 Python 메서드를 연결하는
    기본 패턴을 이 단계에서 확실히 익히는 것이 중요하다.

 

9.3 단계의 역할

  • 이후 9.4(새 파일), 9.5(기존 파일 열기)에서
    self.current_file 를 도입하고,
    “이미 저장된 파일은 바로 덮어쓰기” 같은 로직을 추가할 수 있는 기반이 된다.
  • 나중에 “종료 전 변경 내용 저장 여부 확인(maybe_save)” 로 확장할 때도
    이 save_file() 함수가 그대로 재사용된다.

 

단계별 완성 파일