파이썬으로 구현한 윈도우 파일탐색기

개요

이번 시간에는 Python, PyQt5 QTreeWidget을 이용해 윈도우 탐색기를 만들어 보겠습니다.

Windows API 함수를 이용해 논리 드라이브를 찾고, 찾은 드라이브를 더블 클릭하면 하위 디렉토리, 파일을 검색해 트리 위젯에 추가합니다.

찾은 경로가 디렉토리인 경우에는 하위 디렉토리를 다시 검색하고, 파일인 경우 기본 연결 프로그램으로 연결해 파일을 열어서 보여줍니다.

[윈도우 탐색기 앱 실행화면]

완성된 앱의 동작은 아래 동영상을 참조 바라며, 기능은 다음과 같습니다.

1. 윈도우 운영제체의 드라이브 찾기

2. 드라이브의 하위 디렉토리 or 파일 탐색 및 구분 (더블 클릭시)

3. 파일의 경우 더블 클릭시 기본 연결프로그램으로 연결 (*.txt는 메모장 등)


개발 환경

  • Windows 10 Pro 64bit

  • Python 3.8.8 64bit, Pycharm

  • PyQt5 5.15.3


소스코드

main.py 단일 파일로 구성되어 있으며, 소스코드를 먼저 살펴보고 설명을 이어가겠습니다.

from PyQt5.QtWidgets import QApplication, QWidget, QLineEdit, QTreeWidget, QTreeWidgetItem,QVBoxLayout
from PyQt5.QtCore import Qt
from ctypes import windll
import sys
import socket
import string
import os

QApplication.setAttribute(Qt.AA_EnableHighDpiScaling, True)

class Form(QWidget):

    def __init__(self):
        super().__init__()
        self.setWindowTitle('Ocean Coding School')
        self.resize(640, 480)
        self.initUi()

    def initUi(self):
        self.le = QLineEdit()
        self.le.setReadOnly(True)

        self.tree = QTreeWidget()
        label = ('PC Name', 'Path', 'Type')
        self.tree.setColumnCount(len(label))
        self.tree.setHeaderLabels(label)

        pc_name = socket.gethostname()
        self.root = QTreeWidgetItem(self.tree, type=0)
        self.root.setText(0, pc_name)
        self.tree.expandItem(self.root)

        self.drives = []
        for d in self.get_drives():
            item = QTreeWidgetItem(self.root, type=1)
            item.setText(1, d)
            self.typeCheck(item, d)

        vbox = QVBoxLayout()
        vbox.addWidget(self.le)
        vbox.addWidget(self.tree)
        self.setLayout(vbox)

        # signal
        self.tree.currentItemChanged.connect(self.onItemChanged)
        self.tree.itemDoubleClicked.connect(self.onDbClickTree)

    def get_drives(self):
        drives = []
        bitmask = windll.kernel32.GetLogicalDrives()
        for letter in string.ascii_uppercase:
            if bitmask & 1:
                drives.append(letter+':')
            bitmask >>= 1
        return drives

    def onItemChanged(self, current, previous):
        path = self.findPath(current)
        self.le.setText(path)

    def onDbClickTree(self, item, col):
        path = self.findPath(item)
        if os.path.isdir(path):
            self.search(item, path + '/')
        else:
            os.startfile(path)

    def findPath(self, item):
        path = [item.text(item.type())]
        self.findParent(item, path)
        path.reverse()
        path = '/'.join(path)
        return path

    def findParent(self, item, path):
        parent = item.parent()
        if parent and parent.type()!=0:
            path.append(parent.text(parent.type()))
            self.findParent(item.parent(), path)

    def search(self, target_item, dir):
        path = os.listdir(dir)
        for p in path:
            item = QTreeWidgetItem(target_item, type=1)
            item.setText(1, p)
            self.typeCheck(item, dir+p)

    def typeCheck(self, item, path):
        if os.path.isdir(path):
            item.setText(2, 'Dir')
        else:
            item.setText(2, 'File')

if __name__ == '__main__':
    app = QApplication(sys.argv)
    w = Form()
    w.show()
    sys.exit(app.exec_())


[라인 1~9]

필요한 모듈을 불러오고, 4K 모니터용 해상도를 조정합니다.


[라인 11~17]

QWidget에서 상속받은 Form Class 선언부의 생성자 함수입니다.

super class (QWidget)의 생성자를 호출하고, UI를 초기화하는 initUi() 함수를 호출합니다.


[라인 19~46]

위젯에 컨트롤을 생성하고 추가하는 역할을 담당합니다.

경로를 보여주는 QLineEdit 컨트롤을 생성하고, 읽기 전용으로 설정합니다.

이어서 QTreeWidget 컨트롤 (3개의 열을 갖는 구조) 을 생성합니다.

트리 최상위 루트를 컴퓨터 이름으로 정하기 위해 socket 모듈의 gethostname() 함수를 호출해 컴퓨터의 이름을 알아낸 후 QTreeWidgetItem 을 트리위젯에 추가합니다.

참고로 트리위젯, 트리웨젯아이템 클래스의 차이점은 QTreeWidget은 트리 컨트롤 전체를 의미하며, QTreeWidgetItem은 트리위젯에 추가되는 하나의 아이템 (행) 이라고 생각하면 됩니다.

[앱 GUI 모습]

QTreeWidgetItem의 생성자는 type (int) 전달인자를 통해 트리에 추가되는 아이템을 구분할 수 있도록 구성되어 있으며 예제에서 드라이브(C:, D: 등) 레터는 type = 0, 나머지는 type = 1 로 구분하도록 구성하였습니다.

이는 나중에 현재 경로의 상위 경로를 재귀호출을 통해 찾아가게 되는데 이때, 드라이브의 상위 경로인 컴퓨터 이름은 굳이 필요하지 않기 때문에 이를 구분하기 위함입니다.

이어서 수직 레이아웃박스에 QLineEdit, QTreeWidget을 추가하고 메인위젯에 배치합니다.

마지막으로 트리 위젯의 아이템이 변경되거나 더블클릭시 호출되는 시그널을 슬롯함수와 연결시킵니다.

 

[라인 48~55]

Windows 운영체제의 논리드라이브 문자를 찾는 코드입니다.

아래 코드를 참조해 구성하였으며, 다른 점이 있다면 Python string 모듈의 대문자 아스키 코드가 string.uppercase 에서 string.ascii_uppercase로 수정한 것 외에는 동일합니다.

Windows 논리 드라이브 찾기

ctypes.windll 모듈을 이용, Windows dll 파일 내부 GetLogicalDrives() 함수를 호출해 논리 드라이브를 정수형 타입의 비트구조로 리턴 받습니다.

Windows Doc. 에 다음과 같이 설명되어 있습니다.

[출처 : Microsoft Windows API Doc.]

논리 드라이브의 존재 여부를 비트값에 매핑하는 형태이며, 해당 비트가 1이면 그 순서의 논리 드라이브 문자가 존재한다는 의미입니다.

즉 아래와 같이 정수형 타입에 비트가 세트되어 리턴됩니다.

[GetLogicalDirves()  함수 리턴값]

이제 남은 일은 string 모듈의 ascii_uppercase (A~Z, 26회) 만큼 돌며 위에서 구한 리턴값과 숫자 1을 비트 AND(&) 연산합니다.

비트 AND 연산은 둘 다 1인 경우만 1(참)이 되므로 그때의 Drive letter를 리스트에 추가하고 비트 쉬프트 연산(>>)을 통해 LSB를 제거해 갑니다.

최종적으로 리스트에는 비트가 1인 드라이브 문자가 저장되게 됩니다. (저는 C: D: E:)


[라인 57~59]

트리위젯의 아이템이 마우스나 키보드 등으로 변경되면 호출되는 슬롯함수 입니다.

트리아이템의 상위 트리로 이동(재귀)하며 전체 경로를 완성한 후, 이를 QLineEidt에 추가합니다.


[라인 61~66]

트리 아이템을 더블 클릭했을때 호출되는 슬롯함수입니다.

위와 마찬가지로 트리아이템의 전체 경로를 찾아낸 후, 디렉토리인지 파일인지를 구분해 디렉토리라면 선택트리의 하위에 추가하고 파일이라면 해당 파일의 기본연결프로그램으로 파일을 열도록 합니다.


[라인 68~73]

선택된 경로(상대)에서 상위 전체 디렉토리(절대)를 찾아 문자열로  함수입니다.

만약 다음과 같은 경로가 있다면,

C:/Foo/Bar.pdf

현재 트리는 Bar.pdf 파일을 클릭한 경우이므로 상위 경로인 Foo, C: 를 찾아야 합니다.

이어서 설명할 findParent() 함수는 재귀호출을 통해 상위트리의 이름(경로)를 리스트로 다음과 같이 리턴합니다.

path = ['Bar.pdf', 'Foo', 'C:']

이제 이 리스트를 역전하면 아래와 같이 변경되고,

path = ['C:', 'Foo', Bar.pdf']

이를 문자열의 join() 함수를 활용하면 최종적으로 아래와 같은 문자열이 완성됩니다.

path = 'C:/Foo/Bar.pdf'

 

[라인 75~79]

선택된 QTreeWidgetItem의 부모트리를 찾아 반복(재귀호출)하며, 찾은 부모의 이름을 전달인자로 넘어온 path 리스트에 추가합니다. 

 

[라인 81~86]

os모듈의 listdir() 함수를 이용, 전달인자로 받은 경로의 하위 디렉토리 or 파일을 검색해 트리에 추가하는 역할입니다.


[라인 88~92]

전달인자로 받은 경로가 디렉토리라면 'Dir' 파일이라면 'File' 을 구분해 트리위젯의 2번 열에 추가합니다.


[라인 94~98]

파이썬의 메인함수이며, Qt의 QApplication 객체와 QWidget에서 상속받은 Form 객체를 생성해 앱을 실행합니다.

QApplication 객체의 exec_() 함수로 앱을 실행하고, 앱이 종료되기 전까지 리턴되지 않다가 앱이 종료되면 리턴값 0 (문제없이 종료된 경우) 를 sys모듈의 exit()로 전달합니다.


이상으로 모든 설명을 마칩니다.

감사합니다.

댓글

  1. 파일인 경우 기본 연결 프로그램으로 연결해 파일을 열어서 보여줍니다.'
    라고 했는데
    코드를 추적해 보면 더블클릭해서 파일을 열 때
    75행의
    def findParent(self, item, path):
    함수에서 끝이 납니다.
    그런데 이 함수의 어디에 파일을 여는 기능인지 모르겠네요?
    알려주시면 감사 하겠습니다.

    답글삭제
    답글
    1. 네, 아래 순서로 호출됩니다.

      1. 더블클릭 슬롯함수 호출, 61라인
      2. findPath() 호출, 62라인
      3. findParent() 호출, 70라인
      4. 경로 탐색 후 62번라인으로 복귀
      5. 디렉토리가 아니면(파일이면) os.startfile()로 열기, 66라인

      삭제
    2. 내가 왜 66라인을 못 봤을까요? 감사!!! 합니다!!!

      삭제

댓글 쓰기

이 블로그의 인기 게시물

Qt Designer 설치하기

PyQt5 기반 동영상 플레이어앱 만들기