본문 바로가기
Software/Python

Pyinstaller 패키징 때 PyQt ui 파일 포함시키는 방법(여러개도 됨)

by lovey25 2021. 7. 22.
반응형

문제점

파이썬으로 GUI 프로그램을 만들 때 PyQt를 애용하고 있습니다. PyQt는 ".ui"라는 확장자를 사용하는 별도의 GUI 리소스 파일이 있어서 파이썬에서 이 UI파일을 읽어오기만 하면 되기 때문에 아주 편리하게 사용할 수 있습니다. 그래서 UI가 약간 수정이 있다 해도 메인 소스를 손댈 필요가 없어서 생산성 측면에서 큰 장점이 있습니다.

그런데 이렇게 별도의 UI 파일이 있는 프로그램은 Pyinstaller로 패키징 할 때 리소스 파일이 누락되어서 패키징이 제대로 되지 않을 때가 있습니다. 

제가 경험해본 바로는 UI 파일을 스크립트에서 불러올 때 상대 경로로 접근하는 경우 Pyinstaller로 패키징 할 때 UI가 누락되는 걸 경험했습니다. 그래서 이런 경우는 UI파일을 참조할 때 절대 경로를 사용하면 해결할 수 있습니다.

import os

Ui_MainWindow, QtBaseClass = uic.loadUiType('C:\myproject\myfolder\GUI.ui')

class MyWindow(QMainWindow):
    def __init__(self):
        super(MyWindow, self).__init__()
        self.ui = Ui_MainWindow()

그런데 이렇게 되면, 컴퓨터를 여기저기 옮겨가면서 코딩을 할 때 문제가 됩니다. 사용하는 컴퓨터마다 프로젝트 저장 위치가 다를 수 있기 때문에 테스트하려면 사용 환경에 따라서 매번 3행의 UI 절대 경로를 수정해야 합니다.

그렇다면 UI파일을 불러올 때 파이썬 코드가 실행되는 위치를 확인하고 그걸 절대 경로로 변환한 다음 UI 파일을 불러오면 되겠죠. 그래서 "__file__" 키워드를 사용해서 현재 코드가 실행되는 위치를 확인하고 UI파일 앞에 경로로 붙여줌으로써 절대 경로를 만들어서 사용을 해 봤습니다.

import os

## python실행파일 디렉토리
BASE_DIR = os.path.dirname(os.path.abspath(__file__)) 
Ui_MainWindow, QtBaseClass = uic.loadUiType(BASE_DIR + r'\GUI.ui')

class MyWindow(QMainWindow):
    def __init__(self):
        super(MyWindow, self).__init__()
        self.ui = Ui_MainWindow()

그런데 이 방법으로는 UI파일을 제대로 접근할 수가 없었습니다. 처음에는 "__file__"이라는 키워드가 스크립트로 실행될 때와 onefile 패키징 상태에서 실행될 때 반환되는 값이 다르다는 걸 얼마 전 확인했었기 때문에 이와 관련한 문제인 줄 알았고 이걸 잘 이용하면 해결할 수 있을 거라 생각했습니다.

 

Pyinstaller로 변환한 exe 파일의 실행 경로 찾기

Pyinstaller로 파이선 스크립트를 실행파일로 변환했을 때 겪을 수 있는 경로 문제에 대한 이야기입니다. 프로그램 동작중에 데이터 파일을 가져오거나 저장해야 할 일이 있을 때 파이썬 스크립트

kwonkyo.tistory.com

 

이전에 공부했던 패키징 여부에 따라서 프로그램 실행 경로가 달라질 수 있음을 생각해서 절대 경로를 만들어주는 함수를 만들어 봤습니다.

def resource_path(relative_path): 
    if getattr(sys, 'frozen', False):
        # 패키징 상태로 실행될 때
        base_path = os.getcwd()
        return os.path.join(base_path, relative_path)
    else:
        # 스크립트로 실행될 때
        base_path = getattr(sys, '_MEIPASS', os.path.dirname(os.path.abspath(__file__)))
        return os.path.join(base_path, relative_path)

 

그런데 아쉽게도 이 방법도 안되더군요. 스크립트로 실행하는 데는 문제가 없고 패키징 한 상태에서 UI파일의 경로도 잘 나오지만 UI파일이 패키징에 포함되지 않아서 파일을 찾을 수 없다는 에러가 발생했습니다.

해결책

결국 구글링으로 해결책을 찾았습니다. 이 방법은 pyinstaller가 패키징을 할 때 UI파일을 수동으로 포함하도록 지정해 주는 방법으로 UI파일이 2개 이상인 경우에도 사용할 수 있는 유용한 방법입니다. 

먼저 파이썬 스크립트는 처음 구상했던 대로 실행 위치를 기준으로 UI파일의 절대 경로를 찾아서 실행할 수 있도록 만들어 줍니다.

import os

## python실행파일 디렉토리
BASE_DIR = os.path.dirname(os.path.abspath(__file__)) 
Ui_MainWindow, QtBaseClass = uic.loadUiType(BASE_DIR + r'\GUI.ui')

class MyWindow(QMainWindow):
    def __init__(self):
        super(MyWindow, self).__init__()
        self.ui = Ui_MainWindow()

이렇게 되면 스크립트 실행 상태에서는 문제없지만 패키징 상태에서는 문제가 되죠. 그래서 이제 패키징에 UI파일을 포함시키기 위해서 아래 과정을 추가로 진행합니다.

pyinstaller.exe -w -F .\MyCode.py

일단 스크립트를 pyinstaller로 한번 패키징 합니다. 이때 최종 결과에 반영할 옵션을 걸어서 실행합니다. 저는 커맨드 창을 숨기고(-w) 하나의 파일로(-F) 패키징을 하도록 했습니다. 이렇게 하고 나면 실행파일(.exe)도 만들어지지만 작업 폴더에 보면 ".spec"이라는 확장자의 파일이 만들어져 있습니다. (예: MyCode.spec)

pyinstaller가 작업을 하는데 필요한 각종 설정이 저장된 파일인데요. 이 파일을 열어서 아래 그림처럼 수정해 줍니다. Analysis(datas) 부분이 UI 과련 리소스를 지정하는 부분인데 여기에 UI파일을 명기하면 됩니다. 

그리고 또 하나 유용한 게 만약 UI파일이 여러 개인 경우라면 대괄호 안에 리스크 형태로 포맷을 맞춰서 추가할 파일을 더 써주면 됩니다.

이렇게 수정했으면 이 .spec파일을 이용해서 다시 한번 패키징을 해 줍니다.

pyinstaller.exe .\MyCode.spec

이때는 앞서 사용했던 -w -F 옵션이 이미 .spec파일에 반영되어 있기 때문에 다시 지정할 필요는 없습니다.

이렇게 하면 UI파일이 패키징에 포함되어서 하나의 파일만으로도 GUI 프로그램이 잘 실행됩니다. UI파일 2개도 해봤는데 잘 되네요.

 

끝!

반응형

댓글