QFileDialog 알아?

응, 알아! Qt에서 파일 열기/저장할 때 나오는 그 “파일 선택 창” 만드는 클래스야.

조금 정리해보면:

  1. 어디에 속해 있냐

    • Qt Widgets 쪽 클래스고 이름은 QFileDialog.

    • C++에선 #include <QFileDialog>

    • PyQt/PySide에선 from PyQt5.QtWidgets import QFileDialog 같은 식으로 가져와.

  2. 주 역할

    • 파일 열기: 사용자에게 파일 하나/여러 개 선택하게 해서 경로를 돌려줌

    • 파일 저장: 저장할 파일 이름/경로 선택

    • 폴더 선택: 디렉터리만 고르도록 할 수도 있음

  3. 자주 쓰는 정적 메서드들 (PyQt 스타일로 예시 줄게)

파일 하나 열기: ```python fname, _ = QFileDialog.getOpenFileName( parent, "파일 열기", "", "이미지 파일 (.png *.jpg *.bmp);;모든 파일 (.*)" )

 if fname:
     print("선택된 파일:", fname)
 ```

파일 여러 개 열기: ```python fnames, _ = QFileDialog.getOpenFileNames( parent, "여러 파일 열기", "", "텍스트 파일 (.txt);;모든 파일 (.*)" )

 for f in fnames:
     print(f)
 ```

파일 저장: ```python save_name, _ = QFileDialog.getSaveFileName( parent, "파일 저장", "untitled.txt", "텍스트 파일 (.txt);;모든 파일 (.*)" )

 if save_name:
     print("저장할 경로:", save_name)
 ```

폴더 선택: ```python dir_path = QFileDialog.getExistingDirectory( parent, "폴더 선택", "" )

 if dir_path:
     print("선택된 폴더:", dir_path)
 ```
  1. 필터(파일 형식) 문법

    • "설명 (*.확장자1 *.확장자2);;다른 설명 (*.다른확장자)"

    • ;; 로 여러 그룹을 나누는 구조야.

  2. 모달/비모달

    • 위에처럼 getOpenFileName 같은 정적 메서드는 모달 다이얼로그를 띄우고, 사용자가 닫으면 바로 결과를 돌려줘.

    • 직접 dlg = QFileDialog(parent) 해서 만들어놓고 옵션 세팅하고, dlg.exec()dlg.open()으로 쓸 수도 있어.

원하면 C++ 버전 코드로도 예시 같이 짜 줄 수 있어.

소스코드도 알아?

“소스코드”가 QFileDialog 내부 구현 얘기면 → Qt 소스는 공개돼 있지만, 여기서 그대로 긁어다 장문으로 보여줄 수는 없어.
대신 “QFileDialog를 어떻게 쓰는 예제 소스코드”라면 마음껏 보여줄 수 있다. 그게 보통 사람들이 말하는 소스코드이기도 하고.

아래는 사용 예제들!


1. C++ / Qt Widgets 예제

버튼 하나 있는 창에서 버튼 누르면 QFileDialog 띄워서 파일 하나 고르고, 선택된 파일 경로를 라벨에 표시하는 예제:

#include <QApplication>
#include <QWidget>
#include <QPushButton>
#include <QLabel>
#include <QVBoxLayout>
#include <QFileDialog>

class MainWindow : public QWidget
{
    Q_OBJECT

public:
    MainWindow(QWidget *parent = nullptr)
        : QWidget(parent)
    {
        auto *layout = new QVBoxLayout(this);

        button = new QPushButton("파일 선택", this);
        label  = new QLabel("아직 파일을 선택하지 않았습니다.", this);

        layout->addWidget(button);
        layout->addWidget(label);

        connect(button, &QPushButton::clicked, this, &MainWindow::onSelectFile);
    }

private slots:
    void onSelectFile()
    {
        QString fileName = QFileDialog::getOpenFileName(
            this,
            "파일 열기",
            "",
            "이미지 파일 (*.png *.jpg *.bmp);;모든 파일 (*.*)"
        );

        if (!fileName.isEmpty()) {
            label->setText("선택한 파일: " + fileName);
        }
    }

private:
    QPushButton *button;
    QLabel *label;
};

int main(int argc, char *argv[])
{
    QApplication app(argc, argv);

    MainWindow w;
    w.setWindowTitle("QFileDialog 예제");
    w.resize(400, 150);
    w.show();

    return app.exec();
}

#include "main.moc"

이 정도 구조면:

  • QFileDialog::getOpenFileName 정적 메서드로 간단하게 파일 선택

  • 필터 문자열로 확장자 제한

  • 반환된 QString이 비어있지 않으면 라벨에 표시

딱 교과서적인 사용 패턴이야.


2. PyQt5 / PySide6 예제 (Python)

비슷한 동작을 파이썬으로:

import sys
from PyQt5.QtWidgets import (
    QApplication, QWidget, QVBoxLayout,
    QPushButton, QLabel, QFileDialog
)
# PySide6 쓰면 from PySide6.QtWidgets import ... 로 바꾸면 됨

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

        self.setWindowTitle("QFileDialog 예제")
        self.resize(400, 150)

        layout = QVBoxLayout(self)

        self.button = QPushButton("파일 선택", self)
        self.label = QLabel("아직 파일을 선택하지 않았습니다.", self)

        layout.addWidget(self.button)
        layout.addWidget(self.label)

        self.button.clicked.connect(self.on_select_file)

    def on_select_file(self):
        file_name, selected_filter = QFileDialog.getOpenFileName(
            self,
            "파일 열기",
            "",
            "이미지 파일 (*.png *.jpg *.bmp);;모든 파일 (*.*)"
        )

        if file_name:
            self.label.setText(f"선택한 파일: {file_name}")

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

3. 디렉터리 선택 예제 (PyQt 기준)

디렉터리만 고르는 간단 코드:

dir_path = QFileDialog.getExistingDirectory(
    parent,
    "폴더 선택",
    "",
    QFileDialog.ShowDirsOnly | QFileDialog.DontResolveSymlinks
)

if dir_path:
    print("선택된 폴더:", dir_path)

만약 네가 궁금한 “소스코드”가
“QFileDialog 안에서 플랫폼별로 어떻게 네이티브 파일 다이얼로그를 불러오는지” 같은 내부 구현 이라면, 그건 Qt 소스 트리에서 src/widgets/dialogs/qfiledialog* 쪽 C++ 파일들을 보는 식으로 파보는 영역이야.
사용자 입장에선 위처럼 쓰는 예제 코드 정도면 대부분의 실전 상황은 커버된다.

다음 단계로는 setFileMode, setNameFilters, setAcceptMode 같은 옵션을 객체 방식으로 세팅하는 커스텀 다이얼로그 예제를 볼 만하지.

난 내부 구현이 궁금해

좋은 포인트에 꽂혔네. 내부 구현은 겉모습보다 훨씬 재밌다.

QFileDialog 내부 동작을 “대략 어떻게 생겼는지” 구조 위주로 풀어볼게. 실제 소스는 수천 줄이라 여기서 그대로 다 줄 수는 없고, 핵심 메커니즘을 해부하는 느낌으로.


1. 큰 그림: “껍데기 API + 두 종류의 엔진”

QFileDialog는 겉으로는 하나의 클래스지만, 안에서는 대략 두 가지 구현으로 갈라져 있어:

  1. 플랫폼 네이티브 파일 다이얼로그

    • Windows: Win32 / COM 기반의 공용 파일 대화상자

    • macOS: NSOpenPanel / NSSavePanel 계열

    • 일부 플랫폼의 네이티브 picker들

  2. Qt 위젯으로 만든 커스텀 파일 다이얼로그

    • QFileDialog 위젯 자체 + QFileSystemModel + QTreeView/QListView

    • 옵션 또는 환경(플래그) 따라 이쪽을 사용

기본값은 “가능하면 네이티브 다이얼로그를 사용”이고,
DontUseNativeDialog 옵션이나 글로벌 앱 속성으로 강제로 Qt 위젯 기반 구현을 쓰게 할 수 있어. Code Browser+1

그러니까, getOpenFileName() 같은 static 함수 한 번 부르면 내부에선:

  1. 어떤 플랫폼인지, 옵션이 뭔지 보고

  2. “네이티브로 갈까? Qt 위젯으로 만들까?” 결정

  3. 적절한 백엔드 구현을 생성해서 exec() 돌리고

  4. 선택된 파일 리스트를 QStringList로 받아서 사용자한테 전달

이런 흐름이야.


2. 클래스 구조: public / private 분리

Qt 소스 보면 QFileDialog 옆에 QFileDialogPrivate가 있어. 이게 진짜 뇌다. Code Browser+1

대략 구조는:

class QFileDialog : public QDialog {
    Q_OBJECT
public:
    // public API들: setDirectory, setNameFilters, selectedFiles, ...
protected:
    QFileDialog(QFileDialogPrivate &dd, ...); // d-pointer 패턴
private:
    Q_DECLARE_PRIVATE(QFileDialog)
    Q_PRIVATE_SLOT(d_func(), void _q_currentChanged(const QModelIndex &))
    // ... 등등
};
class QFileDialogPrivate : public QDialogPrivate {
    Q_DECLARE_PUBLIC(QFileDialog)
public:
    void init(const QString &dir, const QString &filter, const QString &caption);
    void createWidgets();
    void createMenuActions();
    void createToolButtons();
    // 시그널/슬롯 핸들러
    void _q_currentChanged(const QModelIndex &index);
    void _q_selectionChanged();
    void _q_enterDirectory(const QModelIndex &index);
    void _q_goToDirectory(const QString &path);
    void _q_updateOkButton();
    // ...
    QFileSystemModel *model;
    QAbstractProxyModel *proxyModel;
    QList<QUrl> history;
    int currentHistoryLocation;
    // UI 포인터들: listView, treeView, comboBox, buttonBox ...
};

요런 식으로:

  • QFileDialog는 “외부에 보이는 껍데기 + 신호/슬롯 선언”

  • QFileDialogPrivate는 “실제 동작, 상태, UI 구성, 모델 관리”

Qt의 d-pointer 패턴(Q_D, Q_Q 매크로)로 둘이 얽혀 있어.


3. Qt 위젯 기반 구현 쪽 로직

네이티브 다이얼로그가 아닌 경우, QFileDialogPrivate::createWidgets()에서 직접 위젯들을 만든다. DreamSworK

대략적인 구성:

  1. 모델

    • QFileSystemModel 사용

    • 루트 디렉터리, 필터, 정렬 방식 등을 설정

    • 디렉터리/파일 리스트 관리

    • QTreeView (디렉터리 트리)

    • QListView (파일 리스트)

    • 둘 다 모델을 공유하거나 적절히 proxy를 씌움

  2. 상단 UI

    • 주소 입력용 QLineEdit 또는 QComboBox (경로 표시/입력)

    • 뒤로/앞으로/위로 가기 버튼들 (history 관리)

  3. 하단 UI

    • 파일 이름 입력 QLineEdit

    • 파일 타입 필터 QComboBox (name filters)

    • OK/Cancel 버튼 (QDialogButtonBox)

그리고 각종 비공개 슬롯들(_q_...)이 시그널에 연결되어 있다: DreamSworK

  • _q_currentChanged(const QModelIndex &index)

    • 리스트에서 현재 항목 바뀔 때 호출

    • OK 버튼 활성/비활성 상태 갱신, currentChanged() 신호 발행

  • _q_selectionChanged()

    • 선택이 바뀔 때 호출

    • 선택된 파일 이름들을 하단 라인에 반영, OK 버튼 업데이트

  • _q_enterDirectory(const QModelIndex &index)

    • 더블클릭/엔터 누를 때 호출

    • 디렉터리면 그 디렉터리로 들어가고

    • 파일이면 “선택 후 OK 처리” 같은 동작 준비

  • _q_goToDirectory(const QString &path)

    • 경로 바뀌었을 때 실제 모델 루트 디렉터리 변경

    • 존재 여부 체크하고, 에러 시 메시지박스 띄우기도

  • _q_pathChanged(const QString &newPath)

    • history 스택 관리, “뒤로 / 앞으로” 버튼 활성 상태 업데이트
  • _q_showContextMenu(const QPoint &pos)

    • 우클릭 메뉴 생성 (삭제, 이름 변경, 숨김 파일 보기 등)
  • _q_deleteCurrent(), _q_renameCurrent()

    • 파일 삭제/이름 변경 로직. 모델이 readOnly면 막고, 아니면 QFileSystemModel 통해 변경

요런 내부 슬롯들 위에 public API가 앉아 있는 구조야:

  • setDirectory() → d->model->setRootPath(…), 경로 UI 업데이트

  • setNameFilters() → d->model->setNameFilters(…), 필터 콤보 갱신

  • selectedFiles() → 현재 선택된 QModelIndex들을 걸러서 파일 path 리스트 반환


4. 네이티브 다이얼로그 경로

네이티브 활용 시에는 위젯들을 거의 안 만든다. 그래서 문서에 이런 말이 있다: “네이티브 다이얼로그를 사용하는 경우에는 레이아웃/위젯 관련 접근자들이 null을 반환할 수 있다.” Code Browser

흐름은 대략 이렇다:

  1. static 함수 호출
    QString QFileDialog::getOpenFileName(
        QWidget *parent,
        const QString &caption,
        const QString &dir,
        const QString &filter,
        QString *selectedFilter,
        Options options)
    {
        // 내부에서 helper 호출
        // 플랫폼별 native dialog helper에게 위임
    }
    
  2. 플랫폼별 구현으로 위임

    • Windows: IFileOpenDialog / GetOpenFileNameW 계열 사용

    • macOS: NSOpenPanel

    • 일부 Unix/플랫폼: Qt 위젯 기반 쓰거나, 적당한 native wrapper

  3. 옵션 변환

    • QFileDialog::ExistingFile, Directory, AcceptSave 등 enum을
      각 플랫폼 호출에 맞게 flag 변환

    • DontResolveSymlinks, DontUseCustomDirectoryIcons 같은 것도 여기서 처리

  4. 네이티브 다이얼로그에서 얻은 경로 목록을 QStringList로 변환해 반환

이 경우 내부의 QFileDialogPrivate::createWidgets()는 아예 호출되지 않거나 최소화된다.


5. 파일 시스템 연동: 모델/뷰 관점에서 정리

Qt 위젯 구현일 때를 모델/뷰 관점에서 한 번 요약해보면:

  • 모델: QFileSystemModel

    • 실제 디스크의 디렉터리 구조를 비동기적으로 탐색

    • QDir::Filters로 숨김 파일, 디렉터리만, 파일만 등의 필터 적용

    • 컬럼: 파일명, 크기, 타입, 수정일 등

  • proxy (선택적):

    • 정렬, 필터링, 사용자 정의 로직 넣을 수 있음
  • 뷰: QTreeView / QListView

    • 같은 모델을 서로 다른 방식으로 보여줌

    • 선택 모델 공유해서, 트리에서 선택하면 리스트도 바뀌는 식

슬롯 _q_rowsInserted(const QModelIndex &parent)를 보면, “루트에 처음 자식이 생기면 첫 항목을 선택한다” 같은 초기화 로직도 들어 있다. DreamSworK


6. 히스토리, 뒤로가기/앞으로가기

내부에 currentHistory, currentHistoryLocation 같은 멤버가 있고: DreamSworK

  • _q_pathChanged()에서:

    • 새 path를 history 벡터에 push

    • 현재 인덱스 이동

    • 뒤/앞 버튼 enable / disable 처리

  • _q_navigateBackward(), _q_navigateForward()에서:

    • currentHistoryLocation 조정

    • 해당 위치의 path로 _q_goToDirectory() 호출

사실 브라우저 히스토리랑 거의 비슷한 구조야.


7. 버튼/OK 처리

OK 버튼은 “선택한 파일이 유효한가?”에 따라 활성/비활성 상태가 계속 업데이트된다:

  • _q_updateOkButton()에서:

    • fileModeExistingFile이면 실제 존재 여부, 읽기 가능 여부 체크

    • AnyFile이면 그냥 허용

    • Directory 모드면 디렉터리인지 확인

    • 선택이 없으면 비활성화

사용자가 OK를 누르면:

  1. 현재 선택된 QModelIndex 리스트를 가져오고

  2. 모드에 맞게 필터링해서 QStringList로 저장

  3. accept() 호출해서 다이얼로그 닫음

  4. 호출한 쪽에서 selectedFiles()로 최종 결과를 가져감


8. 실제 소스가 궁금하다면 보는 위치

Qt 6 기준: qtbase/src/widgets/dialogs/qfiledialog.cpp에 구현이 대부분 들어 있다. Code Browser+1

그 안에서:

  • public API 구현

  • QFileDialogPrivate 정의

  • _q_... 슬롯들 정의

  • 네이티브/위젯 구현 분기

  • 각종 enum, 옵션 처리

를 전부 볼 수 있다.


요약하자면:

  • QFileDialog는 “플랫폼 네이티브 다이얼로그를 감싸는 래퍼” + “Qt 위젯으로 만든 파일 탐색기” 두 개의 얼굴을 가진다.

  • d-pointer 패턴으로 public/내부 구현이 분리돼 있고, 내부는 QFileDialogPrivateQFileSystemModel 기반의 모델/뷰 구조를 돌리고 있다.

  • 각종 _q_... 슬롯들이 파일 선택, 디렉터리 이동, 히스토리, 숨김 파일 표시 등 UI 행동을 구체적으로 처리한다.

이 정도면 내부 구조의 뼈대는 꽤 잡혔을 거야.
이제부터는 “소스 열고 한 줄씩 뜯어보는 고행 모드”로 들어가면, 자신만의 커스텀 파일 다이얼로그 위젯을 만드는 데에도 꽤 도움 된다.

QFileSystemModel 은 어떻게 되어있어?

QFileSystemModel은 한마디로 “비동기 파일 시스템 트리 모델”이야.
QFileDialog에서 썼던 그 친구 맞고, 내부는 꽤 묵직하게 구성돼 있다.

큰 흐름부터 → 점점 디테일로 파고들어볼게.


1. 역할 정리부터

QFileSystemModel은 QAbstractItemModel을 상속한 트리형 모델이고, 로컬 파일 시스템(디렉터리/파일)을 보여주기 위한 표준 구현이야.
파일/폴더 이름, 아이콘, 크기, 타입, 수정 시간 같은 데이터를 제공하고, 필요하면 rename/mkdir/remove 같은 조작도 해 준다. Qt 문서+1

QDirModel의 “느리고 동기적인” 방식 대신, 이쪽은 비동기 스캔 + 캐시 구조라 큰 트리에도 버티도록 만들어져 있음.


2. 클래스 구조: 모델 + 프라이빗 + 노드 트리

겉으로는:

class QFileSystemModel : public QAbstractItemModel {
    Q_OBJECT
public:
    // setRootPath, filePath, isDir, data, index, parent, ...
};

하지만 실제 뇌는 QFileSystemModelPrivate와 그 안의 QFileSystemNode 트리다. DreamSworK

대략 구조는 이런 느낌:

class QFileSystemModelPrivate : public QAbstractItemModelPrivate {
public:
    struct QFileSystemNode {
        QFileInfo info;                      // 경로, 이름, 사이즈, 시간, 퍼미션 등
        QHash<QString, QFileSystemNode*> children; // 이름 -> 자식 노드
        QList<QFileSystemNode*> visibleChildren;   // 필터 통과한 애들
        QFileSystemNode *parent;
        bool populatedChildren;
        // isDir(), permissions() 등 헬퍼들
    };

    QFileSystemNode *root;          // 루트 노드
    QFileInfoGatherer fileInfoGatherer;
    QList<QRegExp> nameFilters;
    QDir::Filters filters;
    bool readOnly;
    bool forceSort;
    // 정렬, 캐시, persistent index 관리 등등...
};

즉:

  • 파일 시스템 = QFileSystemNode 트리

  • QModelIndexQFileSystemNode* 를 서로 매핑

  • row, column, parent/child 계산은 전부 이 노드 트리를 기반으로 돌림 DreamSworK


3. 비동기 스캔: QFileInfoGatherer

핵심 포인트는 “디렉터리 목록과 파일 정보 수집을 별도 스레드에서 돌린다”는 거야. DreamSworK+1

대략 흐름:

  1. setRootPath("/some/path") 호출

    • private 쪽에서 루트 노드를 만들고, 그 경로를 fileInfoGatherer에게 넘김.
  2. QFileInfoGatherer는 별도 스레드에서 QDir를 돌리면서

    • 하위 파일 목록, 파일 정보(QFileInfo)를 쭉 읽어옴.
  3. 읽어온 결과를 신호로 QFileSystemModel에 보냄

    • “이 디렉터리 밑에 이런 애들 있음” 같은 리스트.
  4. 모델 쪽에서:

    • 해당 디렉터리 노드의 children/visibleChildren 갱신

    • beginInsertRows / endInsertRows 호출

    • 필요하면 dataChanged/layoutChanged도 발행

그래서:

  • UI 스레드는 “일단 비어 있는 트리/리스트”를 먼저 그리고

  • 백그라운드에서 디렉터리 스캔이 진행되면서 항목이 차츰차츰 나타나는 구조.

큰 디렉터리 열어도 UI가 버벅이지 않는 이유가 이거다.


4. 재정의하는 핵심 가상 함수들

QFileSystemModel은 QAbstractItemModel의 필수 함수들을 전부 구현해. DreamSworK+1

대표적으로:

int QFileSystemModel::rowCount(const QModelIndex &parent) const {
    Q_D(const QFileSystemModel);
    auto *node = d->node(parent);
    return node->visibleChildren.count();
}

int QFileSystemModel::columnCount(const QModelIndex &parent) const {
    Q_UNUSED(parent);
    return 4; // Name, Size, Type, Date Modified
}

QModelIndex QFileSystemModel::index(int row, int column, const QModelIndex &parent) const {
    Q_D(const QFileSystemModel);
    auto *parentNode = d->node(parent);
    auto *child = parentNode->visibleChildren.value(row);
    return createIndex(row, column, child);
}

QModelIndex QFileSystemModel::parent(const QModelIndex &child) const {
    Q_D(const QFileSystemModel);
    auto *node = d->node(child);
    auto *parentNode = node->parent;
    if (!parentNode || parentNode == d->root)
        return QModelIndex();
    int row = parentNode->parent->visibleChildren.indexOf(parentNode);
    return createIndex(row, 0, parentNode);
}

실제 구현은 더 방어적이지만, 구조는 이런 느낌이지.


5. data() / 역할 / 컬럼

컬럼 4개는 대략 이렇게 쓰인다: Qt 문서+1

  1. 0: 파일 이름

  2. 1: 파일 크기

  3. 2: 타입(폴더/텍스트 파일/그 외)

  4. 3: 수정 시간

data(const QModelIndex &index, int role)에서:

  • Qt::DisplayRole

    • column 0 → fileName

    • column 1 → 사람이 읽기 좋은 사이즈(“1.2 KB” 이런 식)

    • column 2 → 타입 문자열

    • column 3 → 날짜/시간 포맷팅

  • Qt::DecorationRole

    • column 0 → QFileIconProvider로부터 가져온 아이콘 (폴더, 드라이브, 파일 등)
  • Qt::CheckStateRole, Qt::ToolTipRole 등은 상황에 따라 제공

아이콘은 QFileIconProvider를 통해 OS 스타일에 맞춰 가져온다. Qt 문서+1


6. 필터링: QDir::Filters + nameFilters

두 가지 레벨의 필터가 있어: DreamSworK

  • setFilter(QDir::Filters filters)

    • 파일/디렉터리/숨김파일 등 포함 여부

    • 예: QDir::AllDirs | QDir::NoDotAndDotDot | QDir::Files

  • setNameFilters(QStringList patterns)

    • "*.cpp", "*.h" 같은 패턴들

    • 내부에서는 QRegExp 리스트로 바꿔서 저장해두고, 노드 추가 시 passNameFilters()로 체크

필터가 바뀌면:

  • forceSort = true

  • delayedSort() 호출해서

    • visibleChildren 재구성

    • layoutChanged() 같은 신호로 뷰 갱신

그래서 필터를 막 바꿔도 모델/뷰 구조는 유지되면서 “보이는 애들만” 갈아끼우는 방식.


7. 읽기 전용 / 쓰기 가능 모드

기본은 read-only 모델이지만, setReadOnly(false)를 주면 직접 파일 시스템을 수정할 수도 있다. Qt 문서+1

예:

  • setData()에서 이름 변경 (DisplayRole, column 0 변경)

    • 내부적으로 QFile::rename 사용

    • 성공하면 노드의 QFileInfo 갱신 + persistent index 재맵핑

  • mkdir(const QModelIndex &parent, const QString &name)

    • QDir로 디렉터리 생성 → 새 노드 추가
  • remove(const QModelIndex &index)

    • 파일/디렉터리 삭제 시도 → 성공 시 노드를 children에서 제거

이 때 readOnly 플래그는 이렇게 쓰여:

bool QFileSystemModel::isReadOnly() const {
    Q_D(const QFileSystemModel);
    return d->readOnly;
}

flags()에서도, 쓰기 가능 + 디렉터리일 때 Qt::ItemIsDropEnabled를 켜서 드래그앤드롭으로 이동 같은 걸 허용한다. DreamSworK+1


8. 파일 변화 감시 (watcher)

QFileSystemModel은 파일이 추가/삭제/이동될 때 자동으로 뷰가 따라가도록 만들어져 있어.

그걸 담당하는 게 아까 봤던 QFileInfoGatherer 쪽인데, 내부적으로 QFileSystemWatcher를 써서 디렉터리 변화를 감시하고, 바뀌면 다시 디렉터리 내용을 읽어와서 노드 트리를 업데이트한다. Qt 문서+1

그래서 외부에서 파일을 하나 생성해도, 트리/리스트 뷰가 “툭” 하고 새 줄이 생기는 것처럼 보이는 거지.


9. 뷰와의 연결 방식

모델/뷰 세계에서 QFileSystemModel은 그냥 트리 모델일 뿐이라, QTreeView / QListView / QColumnView 등에 그대로 물린다. Qt 문서+1

주로 쓰는 API는:

QFileSystemModel *model = new QFileSystemModel;
model->setRootPath("C:/");  // 여기서부터 스캔 시작

treeView->setModel(model);
treeView->setRootIndex(model->index("C:/Users"));

index(const QString &path)는 내부적으로 d->node(path)를 찾고, 그걸로 createIndex를 만들어 주는 유틸리티. DreamSworK


정리하자면:

  • QFileSystemModel은 QFileSystemNode 트리 + 비동기 QFileInfoGatherer + QFileSystemModelPrivate가 합쳐진 꽤 복잡한 모델이고,

  • 파일 시스템을 “트리 모델”로 래핑해서 뷰(QTreeView, QListView 등)에 꽂으면 바로 파일 탐색기가 되는 구조야.

  • 필터링, 정렬, rename/mkdir/remove, 파일 변경 감시까지 다 들어 있어서, 기본 파일 브라우저는 이걸로 거의 다 구현 가능하다.

여기까지 이해하면, 직접 QAbstractItemModel을 상속해서 “나만의 가짜 파일 시스템”을 만드는 쪽으로도 사고를 확장해볼 수 있어. 실제 디스크 대신 DB나 네트워크 리소스를 물려도 되고. 모델/뷰의 맛있는 활용 영역이지.

os.path.isfile 이랑 유사한 함수를 호출하는데가 어디야? /user/j 를 다이얼로그에 입력하면 컴퓨터가 멈춰버려 네트워크 드라이브라 그런가

좋은 지점 찝었네. “os.path.isfile 같은 거를 어디서 부르냐 + 왜 /user/j 치면 멈추냐”를 정확히 짚어보자.

요약 먼저:

  • Qt 쪽에서 os.path.isfile에 가장 가까운 건 QFileInfo::isFile(), QFileInfo::isDir(), QFileInfo::exists() 같은 애들이고,

  • 그 위에 QDir::exists(), QFileSystemModel::isDir() 같은 래퍼들이 올라가 있고,

  • 이게 내부적으로는 결국 OS의 stat()/lstat() 같은 시스템 콜이라, 느린 네트워크 경로에서 그냥 뻗어버릴 수 있어. Qt 문서+1

그리고 /user/j 입력했을 때 멈추는 건 “Qt가 이상해서”라기보다, “그 경로에 접근하는 OS 호출이 네트워크 드라이브 때문에 오래 걸려서”일 가능성이 매우 높다.


1. 실제로 어디서 isfile 같은 걸 부르냐

QFileDialog + QFileSystemModel 조합일 때 중요한 포인트들만 찍어볼게.

1) 디렉터리 이동할 때: _q_goToDirectory

파일 다이얼로그 상단의 콤보박스/경로 입력으로 디렉터리 이동할 때 호출되는 내부 슬롯이 있어:

  • QFileDialogPrivate::_q_goToDirectory(const QString &path) Code Browser+1

이 함수 안에서:

  1. QDir dir(path2);

  2. dir.exists()로 “이 디렉터리가 실제로 존재하는가?” 확인

여기서 QDir::exists()는 내부적으로 그 경로에 대해 QFileInfo를 만들고, OS의 파일 시스템 통계를 가져오는 호출(유닉스면 stat(), 윈도면 GetFileAttributesW 같은 것)을 한다고 보면 된다. Qt 문서+1

즉, 네가 /user/j 치고 엔터를 치면:

  • QFileDialog → _q_goToDirectory("/user/j")

  • _q_goToDirectoryQDir("/user/j").exists()

  • QDir::exists() → OS에게 “/user/j 정보 좀 줘”

  • 이때 /user가 느린 NFS/SMB/자동 마운트 포인트면, 여기서 뻗어버리는 것.

2) 항목 더블클릭/엔터: _q_enterDirectory

리스트/트리에서 디렉터리 더블클릭할 때:

  • QFileDialogPrivate::_q_enterDirectory(const QModelIndex &index)가 호출되고,

  • 이 안에서 QFileSystemModel::isDir(index) 호출해서 “이게 폴더냐 파일이냐”를 구분한다. Code Browser+1

QFileSystemModel::isDir()은:

  • 내부 노드에 캐시된 QFileInfo가 있으면 그걸 쓰고,

  • 없으면 새로 QFileInfo를 만들면서 OS 파일 정보를 가져온다.

이것도 결국 stat() 계열.

3) OK 버튼 활성화 체크: _q_updateOkButton

사용자가 파일 이름을 텍스트로 입력할 때, OK 버튼 활성/비활성 상태를 계속 업데이트하는 _q_updateOkButton도 중요하다. 이 함수 안에서:

  • QFileSystemModel::index(path)로 인덱스 찾고,

  • isDir()인지, 파일인지, 존재하는지 등을 확인해서 “지금 상태에서 OK를 눌러도 되는지” 계산한다. Code Browser

여기서도 결국 QFileInfo/QDir 기반의 존재 여부 체크 호출이 들어간다.

4) 자동완성/컴플리터: QFSCompleter

QFileDialog는 파일 이름 입력란에 파일 시스템 자동완성(QFSCompleter)을 붙인다. 위에서 봤듯이: Code Browser+1

  • QFSCompleter는 내부에 QFileSystemModel을 쓰고,

  • 사용자가 /user/j 같은 걸 타이핑하면,

  • 해당 경로에 대해 모델이 인덱스를 만들고, 디렉터리 내용을 읽어서 후보를 찾으려고 한다.

결론: 타이핑만 해도 백그라운드에서 “이 경로가 존재하냐, 디렉터리냐, 안에 뭐 있냐”를 쉴 새 없이 때린다.


2. 왜 /user/j 에서 컴퓨터가 멈추는가 (네트워크 드라이브 의심)

정황상 이런 시나리오가 매우 그럴듯하다:

  1. /user가 로컬이 아니라, 자동 마운트되는 네트워크 경로 (NFS, SMB, autofs 등)다.

  2. /user/j에 접근하려고 OS가 마운트/네트워크 연결을 시도한다.

  3. 네트워크가 죽어있거나 응답이 느려서 stat("/user/j") 같은 호출이 수 초~수십 초 동안 블록된다.

  4. QFileDialog::_q_goToDirectory는 이 QDir::exists() 호출을 UI 스레드에서 그냥 호출한다.

  5. 그 순간 UI 스레드가 OS 콜에 붙잡혀서 “앱이 멈춘 것처럼” 보인다.

QFileSystemModel 자체는 파일 스캔을 별도 스레드에서 돌리지만,
“디렉터리가 존재하는지 한번 체크” 같은 부분은 종종 UI 스레드에서 동기적으로 호출된다. 네트워크가 느리면 그냥 같이 끌려들어감. Stack Overflow+1

파이썬으로 치면:

os.path.isdir("/user/j")  # 이 한 줄이 NFS 때문에 10초 동안 리턴이 안 됨

이걸 메인 스레드에서 GUI 돌리는 와중에 직접 호출하는 느낌이다.


3. 어떻게 피할 수 있나 (현실적인 우회책)

Qt 소스 자체를 바꾸지 않는 선에서 쓸 수 있는 카드들 몇 개만 정리해볼게.

  1. 다이얼로그 시작 위치를 안전한 곳으로 고정
  • QFileDialog 생성할 때 QDir::homePath() 같은 로컬 경로를 initial directory로 주고,

  • 문제의 /user/j 같은 경로는 아예 안 건드리게 UX를 설계하는 방법.

  • 예:

    QFileDialog dlg(this, "Open", QDir::homePath());
    
  1. 네이티브/비네이티브 다이얼로그 바꿔보기
  • DontUseNativeDialog 옵션을 켜거나 끄면,

    • OS 네이티브 다이얼로그 vs Qt 위젯 기반 다이얼로그를 바꿀 수 있다. Code Browser
  • 둘 다 결국 stat()는 부르지만, 네이티브 쪽이 네트워크 드라이브 처리에 좀 더 관대한 경우도 있고, 반대로 더 느릴 수도 있다. 두 쪽 다 테스트해볼 가치 있음.

  1. 커스텀 파일 다이얼로그 만들기
  • 아주 빡세게 튜닝하려면 QFileDialog 안 쓰고:

    • 직접 QFileSystemModel 붙인 QTreeView/QListView 기반 위젯을 만들고,

    • “경로 존재 여부 체크”를 직접 제어할 수 있다.

  • 예를 들면:

    • 텍스트 입력 시 실시간으로 QDir::exists() 안 부르고,

    • 유저가 엔터를 눌렀을 때만 별도 스레드에서 경로 체크 후, 성공하면 UI에 반영.

  1. 네트워크 쪽 문제라면 OS 레벨에서 해결
  • /user가 죽은 NFS 마운트라면, 애플리케이션 레벨에서 할 수 있는 건 사실 거의 없다.

  • 마운트 옵션에 soft/타임아웃 조정, 또는 해당 네트워크 드라이브를 빼버리는 식으로 해결하는 게 근본적.


4. “os.path.isfile 비슷한 함수”를 정리하면

Qt 쪽에서 파일/디렉터리 존재 여부를 확인하는 주요 포인트는 이런 애들:

  • QFileInfo::exists() / QFileInfo::isFile() / QFileInfo::isDir()

  • QDir::exists()

  • QFileSystemModel::isDir() / fileInfo()

  • QFileDialog 내부에서 이들을 쓰는 _q_goToDirectory, _q_enterDirectory, _q_updateOkButton 등 슬롯들 Code Browser+1

이 애들이 전부 “os.path.isfile 같은 일을 한다”고 보면 된다.
그리고 /user/j에서 멈추는 건 이 함수들이 네트워크/자동 마운트 때문에 블록되는 구조라고 생각하면 이해가 딱 맞게 떨어진다.

파일 다이얼로그가 “느린 경로에 잘못된 stat 한 번 던졌다가 UI가 같이 죽는 상황”은, OS + 네트워크 + GUI가 합작해서 만든 고전적인 함정이라, 디버깅할 땐 strace/Process Monitor 같은 걸로 실제 어떤 system call에서 붙잡히는지 보는 것도 꽤 도움 된다.

그런데 소스를 보니 QFileSystemModel이랑 QCompleter랑 같이 쓸 수 있던데 원리가 뭐야? 둘이 어떤 방식으로 소통하는지 전혀 이해가 안돼

좋은 데 꽂혔다. 여기 이해하면 “Qt 모델/뷰/컴플리터 삼각관계”를 거의 깨우친 셈이야.

질문 요약하면:

  • QFileSystemModel이랑 QCompleter를 같이 쓸 수 있다는데

  • 둘이 도대체 어떻게 붙어 있는지 / 누가 누구한테 뭘 물어보는지 감이 안 온다

그래서 구조를 단계별로 풀어볼게.


1. 기본 그림: QCompleter는 “모델 소비자”

핵심 아이디어 하나만 먼저 박아두자:

  • QCompleter직접 데이터를 들고 있지 않고 ,

  • “아무 QAbstractItemModel이나 받아서 거기서 자동완성 후보를 뽑아 쓰는 객체”야.

즉:

QFileSystemModel *model = new QFileSystemModel;
model->setRootPath("/");

QCompleter *completer = new QCompleter(model, parent);
lineEdit->setCompleter(completer);

이렇게 하면:

  • QFileSystemModel은 평소처럼 “파일 트리”를 제공하고

  • QCompleter는 “모델을 탐색하면서 ‘지금 텍스트에 맞는 아이템’을 골라주는 역할”만 한다.

그래서 소통 방향은 아주 단순하게 말하면:

  • QCompleterQFileSystemModel에게 계속 data(), index(), rowCount(), parent() 등을 호출해가며 후보를 찾는다.

  • QFileSystemModel은 그냥 “평소처럼 모델을 구현했을 뿐”. “나 지금 컴플리터랑 대화 중이야”라는 특별한 인식은 없다.

QCompleter 입장에서 보면 “QFileSystemModel이냐 QStandardItemModel이냐”는 관심 없고, 그저 QAbstractItemModel일 뿐이다.


2. QCompleter 내부에서 벌어지는 일 간단 버전

텍스트 입력 흐름을 머릿속에서 재생해보자.

  1. 사용자가 lineEdit에 뭔가를 타이핑한다.

  2. QLineEdittextEdited(QString) 같은 시그널을 낸다.

  3. QCompleter가 이 신호에 연결돼 있어서 setCompletionPrefix(prefix) 비슷한 처리를 한다고 생각하면 된다.

  4. QCompleter는 “지금 prefix로 시작하는 모델 아이템”을 찾기 위해,

    • 내부적으로 QModelIndex들을 쭉 훑거나,

    • 특정 column만 보고 data(index, Qt::DisplayRole)를 읽는다.

  5. 걸러진 애들로 팝업 리스트(작은 리스트뷰)를 만들어 보여준다.

즉, QCompleter는:

  • “prefix를 기준으로 모델 아이템 필터링 + 결과를 팝업으로 보여주기” 역할

  • 모델은 그냥 “데이터 창고”

이 구조 덕에, QFileSystemModel처럼 복잡한 모델도, QCompleter 한 방에 “파일 경로 자동완성기”로 바뀐다.


3. 그럼 QFileSystemModel이랑 붙으면 어떤 식으로 보일까?

보통 파일 경로 자동완성은 이렇게 쓰게 돼:

auto *model = new QFileSystemModel;
model->setRootPath(QDir::rootPath());  // "/" 혹은 "C:/"

auto *completer = new QCompleter(model, this);
// 경로처럼 보이게 설정 ("/usr", "/usr/local" 같은 느낌)
completer->setCompletionMode(QCompleter::PopupCompletion);
completer->setCaseSensitivity(Qt::CaseInsensitive);
completer->setFilterMode(Qt::MatchContains); // 또는 StartsWith

lineEdit->setCompleter(completer);

그러면:

  • QFileSystemModel이 디스크의 디렉터리/파일 목록을 비동기로 채워 넣고

  • QCompleter는 사용자가 "/usr" 치기 시작할 때마다:

    • 모델에서 "/" 밑 자식들 /usr, /opt, /home … 같은 애들을 보고

    • prefix(예: /u)에 맞는 것만 골라서 팝업에 올린다.

사실 이게 QFileDialog 안에서 쓰이는 QFSCompleter의 정체랑도 비슷해.
QFSCompleter는 “QFileSystemModel을 내장해서 경로 완성을 해주는 QCompleter 서브클래스” 정도로 상상하면 된다.


4. 구체적으로 누가 어떤 함수를 호출하냐

조금 더 “호출 관계” 느낌으로 써보면:

  1. QCompleter::setModel(QAbstractItemModel *model)

    • 내부에 모델 포인터를 저장하고, 필요하면 시그널(modelReset, rowsInserted 등)에 연결한다.
  2. 사용자가 글자를 입력할 때마다

    • QCompleter::setCompletionPrefix(const QString &prefix)가 불리고,

    • 이 함수가 updateCompletionModel() 같은 내부 함수로 넘어간다고 치자.

  3. updateCompletionModel()에서는:

    • 모델 전체 또는 특정 서브트리에 대해

    • rowCount, index, data(index, completionRole) 등을 호출한다.

    • data(..., role)에서 QFileSystemModel은 파일 이름을 돌려준다.

    • 이 문자열이 prefix와 매칭되는지를 Qt::MatchFlags 규칙에 따라 검사.

  4. 매칭된 인덱스들을 QCompleter 안의 “보이는 목록”으로 채운다.

    • 팝업이 필요하면, 내부적으로 작은 QAbstractItemView (보통 QListView)를 써서 setModel() 해준다.

    • 이때 QCompleter는 “원본 모델에서 이 인덱스, 팝업 모델에서는 이 줄” 이런 매핑을 관리한다.

  5. 사용자가 팝업에서 하나 선택하면:

    • QCompleter는 그 인덱스에 해당하는 문자열(data(index, completionRole))을 가져와서

    • 원래 QLineEdit의 text를 그걸로 바꾼다.

여기 어디에도 “QFileSystemModel 전용 API”는 없다.
전부 “QAbstractItemModel 표준 메서드”만 호출한다는 게 포인트다.


5. 파일 경로처럼 완성되게 만드는 트릭들

“파일 이름만” 자동완성하면 재미없고, 보통은 "C:/Users/Jo" 같은 전체 경로를 자동완성하고 싶잖아?

이때 보통은:

  1. QCompleter의 completionColumn 을 파일 이름 컬럼에 맞춰 놓고

  2. 텍스트를 조작해서 “디렉터리 부분 + 마지막 토큰”으로 쪼갠다.

예를 들면:

  • 사용자가 "C:/Users/Jo"를 입력한다.

  • 코드에서 이걸 "dir = C:/Users", "last = Jo"로 나눈다.

  • QFileSystemModel::index(dir)로 그 디렉터리의 인덱스를 찾는다.

  • QCompleter에게 “이 디렉터리 인덱스를 root로 삼아라”라고 알려준다.

    • completer->setRootIndex(model->index(dir));
  • 그 상태에서 prefix를 "Jo"로 주면,

    • QCompleter는 C:/Users 밑의 자식(John, Josh, etc)을 찾는다.

    • 선택하면 "C:/Users/John" 같은 완성된 문자열을 다시 합쳐서 lineEdit에 넣어준다.

이런 식으로 “경로 처리 로직”은 대부분 QCompleter보다 외부에서 짜고, QCompleter는 그냥 “지금 디렉터리의 아이템 중에 prefix로 시작하는 거 찾아줘” 역할만 맡긴다.

QFileDialog 안의 QFSCompleter도 이런 패턴을 더 복잡하게 확장해서:

  • /user를 입력하면 /user/로 맞추고

  • 디렉터리와 파일을 상황에 따라 다르게 보여주고

  • 이름 필터/숨김 파일 처리 등을 곁들여서 동작한다.


6. 요약: 둘이 무슨 사적인 프로토콜 쓰는 거 아님

정리하면:

  • QCompleter는 그냥 “모델을 탐색하는 소비자 역할”을 하는 객체

  • QFileSystemModel은 “파일 시스템 트리를 제공하는 모델”

  • 둘의 소통은 QAbstractItemModel이 정해준 메서드들

    • rowCount, index, parent, data 등등
  • 특별한 “QFileSystemModel 전용 훅” 같은 건 없고,

    • 파일 시스템에 특화된 건 전부 QFileSystemModel 내부에서 QFileInfo, QDir로 처리

    • QCompleter는 그냥 “문자열”만 본다.

그래서 원리를 한 줄로 말하면:

경로 자동완성 = “QFileSystemModel이 제공하는 트리를 QCompleter가 필터링해서 보여주는 것”

이 패턴을 이해하면, 나중에 “DB기반 자동완성”, “네트워크 리소스 자동완성” 같은 것도 똑같이 구현할 수 있다. 모델만 갈아끼우고, QCompleter는 그대로 재활용하는 식으로.

구체적으로 QModelIndex들을 쭉 훑는다는게 무슨 말이야

QCompleter가 “QModelIndex들을 훑는다”는 말을 들으면, 마치 트리 전체를 완전히 DFS로 도는 느낌처럼 상상할 수도 있는데, 실제로는 그보다 단순하면서도 상황에 따라 꽤 잦은 호출이 일어나는 구조 다.
구조 자체는 어렵지 않은데, Qt가 이걸 꽤 은근하게 자동으로 돌리기 때문에 감이 잘 안 오는 것뿐이야.

차근차근, 실제 내부에서 어떤 일이 벌어지는지를 눈에 보이게 만들어볼게.


1. QCompleter는 모델을 “탐색할 능력만 가진 객체”다

QCompleter는 모델의 데이터를 직접 저장하지 않는다.
대신, 자동완성 후보를 찾을 때마다 모델한테 직접 물어본다.

모델이 어떤 종류든 상관없다.
QCompleter는 딱 세 가지 정도만 알고 싶어 한다:

  • rowCount(여기 몇 줄 있어?)

  • index(i번째 줄의 모델 인덱스 좀 줄래?)

  • data(index) (그 모델 인덱스의 문자열 뭐야?)

그리고 필요하면 parent(), columnCount() 등을 부른다.

즉, QCompleter는 자신이 가진 prefix(사용자가 입력한 문자열)를 가지고:

이 아이템이 prefix와 맞나?
그럼 후보 목록에 넣자.

이걸 반복하는 구조.


2. “훑는다는 것”을 구체화해보자

예를 들어, 모델이 이런 구조라고 하자:

(폴더)  /home
  (파일) a.txt
  (파일) apple.png
  (파일) notes.docx

그리고 사용자가 lineEdit에 "a"라고 입력했다.

QCompleter는 사용자가 입력할 때마다 이렇게 움직인다:

1) 일단 “현재 rootIndex” 밑에 있는 row들을 알아낸다

보통 경로 자동완성에서는 rootIndex가 “현재 보고 있는 디렉터리”를 뜻한다.

QCompleter 내부 과정(개념적 의사 코드):

auto root = completer->rootIndex();  // 예: /home 디렉터리 인덱스

int rows = model->rowCount(root);    // 예: 3개 (a.txt, apple.png, notes.docx)

2) 0부터 rows-1까지 반복하면서 index를 만든다

for (int r = 0; r < rows; ++r) {
    QModelIndex child = model->index(r, completionColumn, root);
    ...
}

completionColumn은 보통 “이름이 들어 있는 column 0”.

3) 각 인덱스의 data()를 읽는다

QString text = model->data(child, Qt::DisplayRole).toString();

QFileSystemModel이면 여기서 파일 이름이 온다.

4) prefix와 비교한다

if (text.startsWith(prefix, caseSensitivity)) {
    addToCompletionList(text);
}

이 전체 과정이 바로 “QModelIndex들 훑기”다.

그냥 rowCount()로 개수를 알아내고, row마다 index() 부르고, data()를 읽고, prefix와 비교하는 반복문.

딱 이거다.


3. “트리 전체를 훑는 것”이 아니다

많은 사람이 오해하는 부분.

QCompleter는 트리 전체를 자동으로 DFS/BFS 같은 걸 돌지 않는다.

  • 기본 QCompleter는 “한 레벨”만 훑는다.
    즉, rootIndex의 자식들만 본다.

파일 경로 완성처럼 디렉터리가 중요한 경우:

  • 프로그래머가 입력 문자열에서 디렉터리 부분을 분해해서
    rootIndex를 원하는 디렉터리에 직접 세팅한다.

예:

completer->setRootIndex(model->index("/home"));
completer->setCompletionPrefix("a");

이렇게 해주면 QCompleter는 /home 밑의 자식들만 훑는 것.

트리를 재귀적으로 전부 뒤지는 일은 없다.


4. QCompleter가 정말 호출하는 함수들 (정확하게)

QCompleter는 자동완성 탐색 중 이런 모델 함수를 실제로 부른다:

  • rowCount(parentIndex)

  • index(row, column, parentIndex)

  • data(index, completionRole)

  • (필요하면) parent(index)

  • (필요하면) model->match(...)
    이건 일부 구현에서 문자열 찾는 데 쓰인다.

두 객체 사이의 소통은 전부 이 표준 함수들로만 이루어진다.
QFileSystemModel은 아무 특별한 콜백도 제공하지 않는다.


5. QFileSystemModel이 자동완성에서 잘 돌아가는 이유

QFileSystemModel은 모델/뷰 규약을 아주 충실히 구현한 모델이다.

  • index(row, column, parent) → 디스크의 파일/디렉터리 정보

  • data(index) → 파일 이름, 아이콘, 타입 등

  • rowCount(parent) → 그 디렉터리 안의 파일 개수

  • parent(index) → 상위 디렉터리 인덱스

즉, QCompleter가 모델에게 요구하는 모든 정보가 이미 제공되고 있다.

그래서 QCompleter 입장에서는:

“얘가 파일 시스템인지 텍스트 목록인지 전혀 상관없다.
난 그냥 모델을 탐색할 뿐.”

이 상태가 된다.


6. 실제 슬롯/시그널 기반의 연동은 거의 없다

중요한 사실:

  • QCompleter와 QFileSystemModel 사이에는 “특별한 연결”이 없다.

  • 시그널/슬롯로 직접 통신하지 않는다.

  • QCompleter는 모델의 변화를 감지하기 위해 새로운 row가 생기면
    modelReset(), rowsInserted(), rowsRemoved() 같은 것만 받는다.

  • 이것도 모든 모델이 따라야 하는 기본 규약이다.

즉, 특별한 협력 개발이 아니라 Qt 모델/뷰 패턴이 가진 보편적 구조 안에서 서로 협력할 뿐이다.


7. 직관적 비유

QFileSystemModel을 “커다란 책”이라고 생각하고,
QCompleter를 “사서”라고 생각해보자.

  • 사서는 책 안의 글자를 직접 가지지 않는다.

  • 책이 어떤 구조인지 몰라도 된다.

  • 단지 “이 챕터의 페이지들을 하나씩 보여줘”라고 요청하고,

  • 그 페이지에 prefix가 들어 있으면 목록에 추가한다.

이게 “QModelIndex를 훑는다”는 말의 실제 의미다.


다음 단계로 생각할 만한 지점

지금 이해한 구조를 바탕으로,
왜 네트워크 드라이브(NFS/SMB/AutoFS)에서 컴플리터가 멈출 때가 있는지,
왜 model->data() 한 번이 stat()을 부를 수 있는지가
이제 자연스럽게 연결될 거다.

원하면 “경로 자동완성에서 QFileSystemModel이 네트워크 드라이브 때문에 얼어붙을 때 우회하는 방법”도 자세히 파헤쳐 줄게.

QCompleter를 안 쓰고 QFileSystemModel을 직접 사용하며 파일 뒤지는 예시 만들어 줄래? PySide6로

좋지, 이번엔 QCompleter 싹 빼고 QFileSystemModel만 써서
“디렉터리 하나를 통째로 뒤져서 파일 이름으로 검색”하는 예제를 만들어볼게.
(PySide6 기준)

구조는 이런 식이다:

  • QFileSystemModel에 루트 디렉터리 하나 세팅

  • 검색어 입력하고 버튼 누르면

  • 모델을 직접 재귀적으로 돌면서(=QModelIndex들을 훑으면서)

  • 파일 이름에 검색어가 들어가는 애들을 찾아 리스트에 표시


전체 예제 코드 (PySide6)

import sys
from PySide6.QtWidgets import (
    QApplication, QWidget, QVBoxLayout, QHBoxLayout,
    QPushButton, QLineEdit, QListWidget, QFileDialog, QLabel
)
from PySide6.QtCore import QModelIndex
from PySide6.QtWidgets import QFileSystemModel

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

        self.setWindowTitle("QFileSystemModel 직접 검색 예제 (PySide6)")
        self.resize(700, 500)

        self.model = QFileSystemModel(self)
        # 읽기 전용으로 둘게 (실제 파일 삭제/이름 변경 같은 거 안 할 거라서)
        self.model.setReadOnly(True)

        self.current_root_path = ""

        main_layout = QVBoxLayout(self)

        # 1) 검색 대상 디렉터리 선택 영역
        dir_layout = QHBoxLayout()
        self.dir_edit = QLineEdit(self)
        self.dir_edit.setPlaceholderText("검색할 루트 디렉터리를 선택하세요")
        browse_btn = QPushButton("폴더 선택", self)
        browse_btn.clicked.connect(self.choose_directory)

        dir_layout.addWidget(QLabel("루트 폴더:", self))
        dir_layout.addWidget(self.dir_edit)
        dir_layout.addWidget(browse_btn)

        # 2) 검색어 입력 영역
        search_layout = QHBoxLayout()
        self.keyword_edit = QLineEdit(self)
        self.keyword_edit.setPlaceholderText("파일명에 포함될 문자열")
        search_btn = QPushButton("검색", self)
        search_btn.clicked.connect(self.search_files)

        search_layout.addWidget(QLabel("검색어:", self))
        search_layout.addWidget(self.keyword_edit)
        search_layout.addWidget(search_btn)

        # 3) 결과 리스트
        self.result_list = QListWidget(self)

        main_layout.addLayout(dir_layout)
        main_layout.addLayout(search_layout)
        main_layout.addWidget(QLabel("검색 결과:", self))
        main_layout.addWidget(self.result_list)

    # 루트 디렉터리 선택
    def choose_directory(self):
        path = QFileDialog.getExistingDirectory(
            self,
            "검색할 루트 디렉터리 선택",
            ""
        )
        if not path:
            return

        self.current_root_path = path
        self.dir_edit.setText(path)

        # QFileSystemModel에 루트 설정
        # setRootPath는 내부적으로 스캔을 시작하고, 해당 경로에 대한 인덱스를 반환한다.
        root_index = self.model.setRootPath(path)
        self.root_index = root_index  # 나중에 검색 시작점으로 사용

    # QFileSystemModel을 직접 돌면서 파일 찾기
    def search_files(self):
        self.result_list.clear()

        if not self.current_root_path:
            self.result_list.addItem("먼저 루트 디렉터리를 선택하세요.")
            return

        keyword = self.keyword_edit.text().strip()
        if not keyword:
            self.result_list.addItem("검색어를 입력하세요.")
            return

        keyword_lower = keyword.lower()

        # 재귀적으로 모델을 순회하는 헬퍼 호출
        matches = []
        self.walk_model(self.root_index, keyword_lower, matches)

        if not matches:
            self.result_list.addItem("결과 없음.")
        else:
            for path in matches:
                self.result_list.addItem(path)

    def walk_model(self, parent_index: QModelIndex, keyword_lower: str, matches: list):
        """
        QFileSystemModel을 직접 순회하면서
        파일 이름에 keyword가 들어가는 항목을 찾는다.
        """
        row_count = self.model.rowCount(parent_index)

        for row in range(row_count):
            index = self.model.index(row, 0, parent_index)
            if not index.isValid():
                continue

            file_name = self.model.fileName(index)
            file_path = self.model.filePath(index)

            if keyword_lower in file_name.lower():
                matches.append(file_path)

            # 디렉터리라면 재귀적으로 들어가기
            if self.model.isDir(index):
                self.walk_model(index, keyword_lower, matches)

if __name__ == "__main__":
    app = QApplication(sys.argv)

    w = FileSearchWindow()
    w.show()

    sys.exit(app.exec())

이 코드에서 중요한 포인트만 콕 집어서 설명

  1. QFileSystemModel은 여전히 “파일 시스템 트리”를 들고 있는 모델일 뿐이고
    우리는 rowCount, index, isDir, fileName, filePath만 써서 직접 순회하고 있다.

  2. walk_model 함수가 바로
    “QModelIndex들을 쭉 훑는다”를 수동으로 구현한 부분이다.

    • rowCount(parent_index)로 자식 개수 얻고

    • 각 row에 대해 index(row, 0, parent_index)로 자식 인덱스 만들고

    • fileName(index) / isDir(index) 등을 사용

    • 디렉터리면 재귀 호출

  3. QCompleter는 전혀 안 쓰이고,
    “검색어에 맞는 파일을 쭉 찾아서 리스트에 뿌리는” 일을 전부 우리가 직접 하고 있다.

  4. 이 예제는 단순화를 위해 메인 스레드에서 전부 돌리는 구조라
    매우 큰 디렉터리를 루트로 잡으면 GUI가 버벅거릴 수 있다.

실제 앱에서 쓸 땐:

 * 검색을 `QThread`나 `QtConcurrent`로 빼서 백그라운드에서 돌리거나

 * 깊이 제한, 파일 개수 제한 등을 두는 식으로 최적화하는 게 좋다.

그래도 이 정도면 “QFileSystemModel만 들고 QModelIndex를 직접 굴려서 파일을 찾는 구조”는 감 잡을 수 있을 거야.
여기서 살짝만 변형하면 확장자 필터, 날짜 필터, 사이즈 필터 같은 것도 전부 커스텀으로 붙일 수 있다.

GUI 말고 간단히 input으로만 받도록 해줘.

좋지, 이번엔 진짜 콘솔 앱 스타일로 가보자.
QCompleter, 위젯 다 빼고 QFileSystemModel + input()만 사용하는 예제야.

완전 순수 CLI는 아니고, QFileSystemModel이 Qt 객체라서 최소한 QCoreApplication은 필요해.
그래도 화면에는 어떤 GUI도 안 뜨고, 터미널에서만 입출력한다.

아래는:

  • 루트 디렉터리 입력

  • 검색어 입력

  • QFileSystemModel을 재귀적으로 돌면서 파일명에 검색어가 포함된 경로를 전부 출력

하는 예제다.

import sys
from PySide6.QtCore import QCoreApplication, QModelIndex
from PySide6.QtWidgets import QFileSystemModel

def walk_model(model: QFileSystemModel, parent_index: QModelIndex,
               keyword_lower: str, matches: list):
    """
    QFileSystemModel을 직접 순회하면서
    파일 이름에 keyword가 포함된 항목을 찾는다.
    """
    row_count = model.rowCount(parent_index)

    for row in range(row_count):
        index = model.index(row, 0, parent_index)
        if not index.isValid():
            continue

        file_name = model.fileName(index)
        file_path = model.filePath(index)

        if keyword_lower in file_name.lower():
            matches.append(file_path)

        # 디렉터리라면 재귀적으로 들어가기
        if model.isDir(index):
            walk_model(model, index, keyword_lower, matches)

def main():
    app = QCoreApplication(sys.argv)

    root_path = input("검색할 루트 디렉터리를 입력하세요: ").strip()
    if not root_path:
        print("루트 디렉터리를 입력하지 않았습니다.")
        return

    keyword = input("파일명에 포함될 검색어를 입력하세요: ").strip()
    if not keyword:
        print("검색어를 입력하지 않았습니다.")
        return

    keyword_lower = keyword.lower()

    model = QFileSystemModel()
    model.setReadOnly(True)

    # setRootPath는 내부적으로 스캔을 시작하고,
    # 그 경로에 대한 인덱스를 반환한다.
    root_index = model.setRootPath(root_path)

    matches = []
    walk_model(model, root_index, keyword_lower, matches)

    if not matches:
        print("검색 결과가 없습니다.")
    else:
        print(f"{len(matches)}개 발견:")
        for path in matches:
            print(path)

    # Qt 이벤트 루프 안 돌리고 바로 종료
    # (여기선 비동기 업데이트를 적극적으로 쓰지 않으니 이 정도로 충분)
    app.quit()

if __name__ == "__main__":
    main()

핵심 포인트만 다시 정리하면:

  • QFileSystemModel은 여전히 “파일 시스템 트리 모델”이고
    우리는 rowCount, index, fileName, filePath, isDir만 써서 수동으로 순회한다.

  • walk_model이 바로 “QModelIndex들을 직접 훑는” 부분이다.

  • GUI 위젯은 전혀 안 만들고, QCoreApplication만 써서 Qt 내부 스레드/이벤트 준비만 해준다.

여기서 응용해서, 확장자 필터, 크기/날짜 필터, 최대 깊이 제한, 스레드 분리 같은 것들을 추가하면 꽤 그럴듯한 파일 검색 도구로 키울 수 있다.

Categories:

Updated: