소개
파이썬 코드를 만들다보니
1) 해당 파일을 빠르게 실행하고 싶다든가 (VSCODE와 같은 편집기를 실행하지 않고)
2) EXE 프로그램으로 만들어서 어디든 가지고 다니면서 쓰고 다니고 싶다는
생각이 들었습니다.
그래서 배치 파일과 EXE 프로그램으로 빠르게 만들어주는 GUI 프로그램을 만들기로 하였습니다.
진행 방법
챗지피티 4o를 기본으로 사용하고
필요에 따라 o1-mini 를 사용하여 제작하였습니다.
생성형 인공지능에게
1) 파이썬 전문가로서 역할을 부여하고
2) 아이디어를 제공한뒤 (배치파일이나, exe 파일로 만드는 gui 프로그램을 만들고 싶다는 계획)
3) 해당 아이디어를 기반으로 챗지피티가 어느정도 잘 설계했는지 한번 확인을 받고 최종 코드를 요청하였습니다.
gui는 pyside6로 제작을 요청하였습니다.
# pip install Pyside6
import os
import subprocess
import json
from PySide6.QtWidgets import (QApplication, QWidget, QVBoxLayout, QPushButton, QMessageBox,
QListWidget, QInputDialog, QFileDialog, QHBoxLayout, QLabel,
QListWidgetItem, QGroupBox, QMenu)
from PySide6.QtCore import Qt
from datetime import datetime
class FileListItem(QWidget):
def __init__(self, filename, parent=None):
super().__init__(parent)
self.filename = filename
layout = QHBoxLayout(self)
self.label = QLabel(filename)
layout.addWidget(self.label)
layout.addStretch()
self.setLayout(layout)
class UtilityManager(QWidget):
def __init__(self):
super().__init__()
self.setWindowTitle("유틸리티 관리 프로그램")
self.setGeometry(100, 100, 1000, 600)
main_layout = QHBoxLayout()
left_layout = QVBoxLayout()
right_layout = QVBoxLayout()
# 버튼들 추가
self.open_current_folder_button = QPushButton('현재 실행 폴더 열기', self)
self.open_current_folder_button.clicked.connect(self.open_current_folder)
self.create_batch_button = QPushButton('배치파일 만들기', self)
self.create_batch_button.clicked.connect(self.create_batch_file)
self.add_startup_button = QPushButton('자동 실행 설정', self)
self.add_startup_button.clicked.connect(self.add_to_startup)
self.check_startup_button = QPushButton('등록 확인', self)
self.check_startup_button.clicked.connect(self.check_startup_registration)
self.delete_startup_button = QPushButton('등록 삭제', self)
self.delete_startup_button.clicked.connect(self.remove_from_startup)
self.create_exe_button = QPushButton('EXE 생성', self)
self.create_exe_button.clicked.connect(self.create_exe_file)
left_layout.addWidget(self.open_current_folder_button)
left_layout.addWidget(self.create_batch_button)
left_layout.addWidget(self.add_startup_button)
left_layout.addWidget(self.check_startup_button)
left_layout.addWidget(self.delete_startup_button)
left_layout.addWidget(self.create_exe_button)
# 배치 파일 목록
batch_group = QGroupBox("현재 폴더의 배치 파일:")
batch_layout = QVBoxLayout()
self.batch_list = QListWidget(self)
self.batch_list.itemDoubleClicked.connect(self.run_file)
self.batch_list.setContextMenuPolicy(Qt.CustomContextMenu)
self.batch_list.customContextMenuRequested.connect(self.show_context_menu)
batch_layout.addWidget(self.batch_list)
batch_group.setLayout(batch_layout)
# EXE 파일 목록
exe_group = QGroupBox("dist 폴더의 EXE 파일:")
exe_layout = QVBoxLayout()
self.exe_list = QListWidget(self)
self.exe_list.itemDoubleClicked.connect(self.run_file)
self.exe_list.setContextMenuPolicy(Qt.CustomContextMenu)
self.exe_list.customContextMenuRequested.connect(self.show_context_menu)
exe_layout.addWidget(self.exe_list)
exe_group.setLayout(exe_layout)
right_layout.addWidget(batch_group)
right_layout.addWidget(exe_group)
main_layout.addLayout(left_layout)
main_layout.addLayout(right_layout)
self.setLayout(main_layout)
self.update_file_lists()
def open_current_folder(self):
current_dir = os.getcwd()
os.startfile(current_dir)
def create_batch_file(self):
file_path, _ = QFileDialog.getOpenFileName(self, "배치 파일로 만들 파일 선택", "", "Python Files (*.py)")
if file_path:
current_dir = os.path.dirname(file_path)
python_file_name = os.path.splitext(os.path.basename(file_path))[0]
timestamp = datetime.now().strftime("%Y_%m_%d_%H_%M_%S")
batch_file_name = f"{python_file_name}_{timestamp}.bat"
batch_file_path = os.path.join(current_dir, batch_file_name)
with open(batch_file_path, 'w') as f:
f.write(f'@echo off\npython "{file_path}"\npause')
QMessageBox.information(self, "배치파일 생성", f"배치파일이 생성되었습니다: {batch_file_path}")
self.update_file_lists()
def add_to_startup(self):
file_path, _ = QFileDialog.getOpenFileName(
self,
"시작 프로그램으로 등록할 파일 선택",
"",
"Batch and Executable Files (*.bat *.exe);;Batch Files (*.bat);;Executable Files (*.exe)"
)
if file_path:
startup_path = os.path.join(os.environ['APPDATA'], 'Microsoft', 'Windows', 'Start Menu', 'Programs', 'Startup')
file_name = os.path.basename(file_path)
startup_file_path = os.path.join(startup_path, file_name)
try:
if file_path.endswith('.bat'):
with open(file_path, 'r') as source, open(startup_file_path, 'w') as target:
target.write(source.read())
else:
import shutil
shutil.copy2(file_path, startup_file_path)
QMessageBox.information(self, "자동 시작 설정", f"{file_name}이(가) 시작 프로그램으로 등록되었습니다.")
except Exception as e:
QMessageBox.warning(self, "오류", f"파일을 시작 프로그램 폴더에 복사하는 중 오류가 발생했습니다: {str(e)}")
def check_startup_registration(self):
startup_programs = self.get_startup_programs()
if startup_programs:
programs_list = "\n".join(startup_programs)
QMessageBox.information(self, "등록된 시작 프로그램", f"현재 등록된 시작 프로그램:\n{programs_list}")
else:
QMessageBox.information(self, "등록된 시작 프로그램", "등록된 시작 프로그램이 없습니다.")
def remove_from_startup(self):
startup_programs = self.get_startup_programs()
if not startup_programs:
QMessageBox.information(self, "등록 삭제", "삭제할 시작 프로그램이 없습니다.")
return
item, ok = QInputDialog.getItem(self, "등록 삭제", "삭제할 프로그램을 선택하세요:", startup_programs, 0, False)
if ok and item:
startup_path = os.path.join(os.environ['APPDATA'], 'Microsoft', 'Windows', 'Start Menu', 'Programs', 'Startup')
file_path = os.path.join(startup_path, item)
try:
os.remove(file_path)
QMessageBox.information(self, "등록 삭제", f"{item}이(가) 시작 프로그램에서 삭제되었습니다.")
except Exception as e:
QMessageBox.warning(self, "삭제 오류", f"삭제 중 오류가 발생했습니다: {str(e)}")
def create_exe_file(self):
file_path, _ = QFileDialog.getOpenFileName(self, "EXE로 변환할 Python 파일 선택", "", "Python Files (*.py)")
if file_path:
options = ["기본 설정", "JSON 설정 파일 선택"]
choice, ok = QInputDialog.getItem(self, "설정 선택", "EXE 생성 설정을 선택하세요:", options, 0, False)
if ok:
current_dir = os.getcwd()
timestamp = datetime.now().strftime("%Y_%m_%d_%H_%M_%S")
python_file_name = os.path.splitext(os.path.basename(file_path))[0]
if choice == "JSON 설정 파일 선택":
json_path, _ = QFileDialog.getOpenFileName(self, "JSON 설정 파일 선택", "", "JSON Files (*.json)")
if not json_path:
return
with open(json_path, 'r') as json_file:
config = json.load(json_file)
config_name = os.path.basename(json_path).split('.')[0]
else:
config = {"cmd_open": False, "one_file": True, "files_to_include": []}
config_name = "기본 설정"
exe_file_name = f"[{config_name}]_{python_file_name}_{timestamp}.exe"
pyinstaller_command = [
'pyinstaller',
'--name', exe_file_name,
'--distpath', os.path.join(current_dir, 'dist'),
'--workpath', os.path.join(current_dir, 'build'),
'--specpath', current_dir,
'--onefile',
file_path
]
if config.get('cmd_open', False):
pyinstaller_command.append('--console')
else:
pyinstaller_command.append('--windowed')
for file in config.get('files_to_include', []):
pyinstaller_command.extend(['--add-data', f'{file}:.'])
# 생성된 명령 출력
print("생성된 PyInstaller 명령:")
print(" ".join(pyinstaller_command))
try:
subprocess.run(pyinstaller_command, check=True, cwd=current_dir)
dist_path = os.path.join(current_dir, 'dist', exe_file_name)
QMessageBox.information(self, "EXE 생성", f"EXE 파일이 성공적으로 생성되었습니다: {dist_path}")
self.update_file_lists()
except subprocess.CalledProcessError as e:
QMessageBox.warning(self, "EXE 생성 오류", f"EXE 파일 생성 중 오류가 발생했습니다: {str(e)}")
except Exception as e:
QMessageBox.warning(self, "EXE 생성 오류", f"오류가 발생했습니다: {str(e)}")
def get_startup_programs(self):
startup_path = os.path.join(os.environ['APPDATA'], 'Microsoft', 'Windows', 'Start Menu', 'Programs', 'Startup')
return [f for f in os.listdir(startup_path) if f.endswith('.bat') or f.endswith('.lnk') or f.endswith('.exe')]
def update_file_lists(self):
# 배치 파일 목록 업데이트
self.batch_list.clear()
current_dir = os.getcwd()
for item in os.listdir(current_dir):
full_path = os.path.join(current_dir, item)
if os.path.isfile(full_path) and item.endswith('.bat'):
item_widget = FileListItem(item)
list_item = QListWidgetItem(self.batch_list)
list_item.setSizeHint(item_widget.sizeHint())
self.batch_list.addItem(list_item)
self.batch_list.setItemWidget(list_item, item_widget)
# EXE 파일 목록 업데이트
self.exe_list.clear()
dist_dir = os.path.join(current_dir, 'dist')
if os.path.exists(dist_dir):
for item in os.listdir(dist_dir):
full_path = os.path.join(dist_dir, item)
if os.path.isfile(full_path) and item.endswith('.exe'):
item_widget = FileListItem(item)
list_item = QListWidgetItem(self.exe_list)
list_item.setSizeHint(item_widget.sizeHint())
self.exe_list.addItem(list_item)
self.exe_list.setItemWidget(list_item, item_widget)
def run_file(self, item):
file_widget = item.listWidget().itemWidget(item)
if item.listWidget() == self.batch_list:
file_path = os.path.join(os.getcwd(), file_widget.filename)
else: # EXE list
file_path = os.path.join(os.getcwd(), 'dist', file_widget.filename)
try:
if file_path.endswith('.bat'):
subprocess.Popen(['cmd', '/c', file_path], creationflags=subprocess.CREATE_NEW_CONSOLE)
elif file_path.endswith('.exe'):
subprocess.Popen([file_path], shell=True)
except Exception as e:
QMessageBox.warning(self, "실행 오류", f"파일 실행 중 오류가 발생했습니다: {str(e)}")
def show_context_menu(self, position):
list_widget = self.sender()
item = list_widget.itemAt(position)
if item is not None:
context_menu = QMenu(self)
open_folder_action = context_menu.addAction("폴더 열기")
action = context_menu.exec(list_widget.mapToGlobal(position))
if action == open_folder_action:
self.open_file_folder(item)
def open_file_folder(self, item):
file_widget = item.listWidget().itemWidget(item)
if item.listWidget() == self.batch_list:
folder_path = os.getcwd()
else: # EXE list
folder_path = os.path.join(os.getcwd(), 'dist')
os.startfile(folder_path)
def main():
app = QApplication([])
window = UtilityManager()
window.show()
app.exec()
if __name__ == '__main__':
main()
vscode에서 실행하면 다음과 같은 gui가 뜹니다. 실제로 한번 테스트 해보세요!
결과와 배운 점
시작 프로그램으로 등록하는 기능도 추가했습니다만
파이썬 코드일 때와 exe 파일이 되었을 때 파일을 참고(텍스트 파일이나 json 파일)하는 부분이 달라서 해당 부분에 대한 고려가 없는 경우 오류가 나타나곤 했습니다.
241116 이후 기능 개선된 코드
# pip install Pyside6
import os
import subprocess
import json
from PySide6.QtWidgets import (QApplication, QWidget, QVBoxLayout, QPushButton, QMessageBox,
QListWidget, QInputDialog, QFileDialog, QHBoxLayout, QLabel,
QListWidgetItem, QGroupBox, QMenu)
from PySide6.QtCore import Qt
from datetime import datetime
class FileListItem(QWidget):
def __init__(self, filename, parent=None):
super().__init__(parent)
self.filename = filename
layout = QHBoxLayout(self)
self.label = QLabel(filename)
layout.addWidget(self.label)
layout.addStretch()
self.setLayout(layout)
class UtilityManager(QWidget):
def __init__(self):
super().__init__()
self.setWindowTitle("유틸리티 관리 프로그램")
self.setGeometry(100, 100, 1000, 600)
main_layout = QHBoxLayout()
left_layout = QVBoxLayout()
right_layout = QVBoxLayout()
# 버튼들 추가
self.open_current_folder_button = QPushButton('현재 실행 폴더 열기', self)
self.open_current_folder_button.clicked.connect(self.open_current_folder)
self.create_batch_button = QPushButton('배치파일 만들기', self)
self.create_batch_button.clicked.connect(self.create_batch_file)
self.add_startup_button = QPushButton('자동 실행 설정', self)
self.add_startup_button.clicked.connect(self.add_to_startup)
self.check_startup_button = QPushButton('등록 확인', self)
self.check_startup_button.clicked.connect(self.check_startup_registration)
self.delete_startup_button = QPushButton('등록 삭제', self)
self.delete_startup_button.clicked.connect(self.remove_from_startup)
self.create_exe_button = QPushButton('EXE 생성', self)
self.create_exe_button.clicked.connect(self.create_exe_file)
left_layout.addWidget(self.open_current_folder_button)
left_layout.addWidget(self.create_batch_button)
left_layout.addWidget(self.add_startup_button)
left_layout.addWidget(self.check_startup_button)
left_layout.addWidget(self.delete_startup_button)
left_layout.addWidget(self.create_exe_button)
# 배치 파일 목록
batch_group = QGroupBox("현재 폴더의 배치 파일:")
batch_layout = QVBoxLayout()
self.batch_list = QListWidget(self)
self.batch_list.itemDoubleClicked.connect(self.run_file)
self.batch_list.setContextMenuPolicy(Qt.CustomContextMenu)
self.batch_list.customContextMenuRequested.connect(self.show_context_menu)
batch_layout.addWidget(self.batch_list)
batch_group.setLayout(batch_layout)
# EXE 파일 목록
exe_group = QGroupBox("dist 폴더의 EXE 파일:")
exe_layout = QVBoxLayout()
self.exe_list = QListWidget(self)
self.exe_list.itemDoubleClicked.connect(self.run_file)
self.exe_list.setContextMenuPolicy(Qt.CustomContextMenu)
self.exe_list.customContextMenuRequested.connect(self.show_context_menu)
exe_layout.addWidget(self.exe_list)
exe_group.setLayout(exe_layout)
right_layout.addWidget(batch_group)
right_layout.addWidget(exe_group)
main_layout.addLayout(left_layout)
main_layout.addLayout(right_layout)
self.setLayout(main_layout)
self.update_file_lists()
def open_current_folder(self):
current_dir = os.getcwd()
os.startfile(current_dir)
def create_batch_file(self):
file_path, _ = QFileDialog.getOpenFileName(self, "배치 파일로 만들 파일 선택", "", "Python Files (*.py)")
if file_path:
current_dir = os.path.dirname(file_path)
python_file_name = os.path.splitext(os.path.basename(file_path))[0]
timestamp = datetime.now().strftime("%Y_%m_%d_%H_%M_%S")
batch_file_name = f"{python_file_name}_{timestamp}.bat"
batch_file_path = os.path.join(current_dir, batch_file_name)
with open(batch_file_path, 'w') as f:
f.write(f'@echo off\npython "{file_path}"\npause')
QMessageBox.information(self, "배치파일 생성", f"배치파일이 생성되었습니다: {batch_file_path}")
self.update_file_lists()
def add_to_startup(self):
file_path, _ = QFileDialog.getOpenFileName(
self,
"시작 프로그램으로 등록할 파일 선택",
"",
"Batch and Executable Files (*.bat *.exe);;Batch Files (*.bat);;Executable Files (*.exe)"
)
if file_path:
startup_path = os.path.join(os.environ['APPDATA'], 'Microsoft', 'Windows', 'Start Menu', 'Programs', 'Startup')
file_name = os.path.basename(file_path)
startup_file_path = os.path.join(startup_path, file_name)
try:
if file_path.endswith('.bat'):
with open(file_path, 'r') as source, open(startup_file_path, 'w') as target:
target.write(source.read())
else:
import shutil
shutil.copy2(file_path, startup_file_path)
QMessageBox.information(self, "자동 시작 설정", f"{file_name}이(가) 시작 프로그램으로 등록되었습니다.")
except Exception as e:
QMessageBox.warning(self, "오류", f"파일을 시작 프로그램 폴더에 복사하는 중 오류가 발생했습니다: {str(e)}")
def check_startup_registration(self):
startup_programs = self.get_startup_programs()
if startup_programs:
programs_list = "\n".join(startup_programs)
QMessageBox.information(self, "등록된 시작 프로그램", f"현재 등록된 시작 프로그램:\n{programs_list}")
else:
QMessageBox.information(self, "등록된 시작 프로그램", "등록된 시작 프로그램이 없습니다.")
def remove_from_startup(self):
startup_programs = self.get_startup_programs()
if not startup_programs:
QMessageBox.information(self, "등록 삭제", "삭제할 시작 프로그램이 없습니다.")
return
item, ok = QInputDialog.getItem(self, "등록 삭제", "삭제할 프로그램을 선택하세요:", startup_programs, 0, False)
if ok and item:
startup_path = os.path.join(os.environ['APPDATA'], 'Microsoft', 'Windows', 'Start Menu', 'Programs', 'Startup')
file_path = os.path.join(startup_path, item)
try:
os.remove(file_path)
QMessageBox.information(self, "등록 삭제", f"{item}이(가) 시작 프로그램에서 삭제되었습니다.")
except Exception as e:
QMessageBox.warning(self, "삭제 오류", f"삭제 중 오류가 발생했습니다: {str(e)}")
def create_exe_file(self):
file_path, _ = QFileDialog.getOpenFileName(self, "EXE로 변환할 Python 파일 선택", "", "Python Files (*.py)")
if file_path:
options = ["기본 설정", "JSON 설정 파일 선택"]
choice, ok = QInputDialog.getItem(self, "설정 선택", "EXE 생성 설정을 선택하세요:", options, 0, False)
if ok:
current_dir = os.getcwd()
timestamp = datetime.now().strftime("%Y_%m_%d_%H_%M_%S")
python_file_name = os.path.splitext(os.path.basename(file_path))[0]
if choice == "JSON 설정 파일 선택":
json_path, _ = QFileDialog.getOpenFileName(self, "JSON 설정 파일 선택", "", "JSON Files (*.json)")
if not json_path:
return
with open(json_path, 'r') as json_file:
config = json.load(json_file)
config_name = os.path.basename(json_path).split('.')[0]
else:
config = {"cmd_open": False, "one_file": True, "files_to_include": []}
config_name = "기본 설정"
exe_file_name = f"[{config_name}]_{python_file_name}_{timestamp}.exe"
pyinstaller_command = [
'pyinstaller',
'--name', exe_file_name,
'--distpath', os.path.join(current_dir, 'dist'),
'--workpath', os.path.join(current_dir, 'build'),
'--specpath', current_dir,
'--onefile',
file_path
]
if config.get('cmd_open', False):
pyinstaller_command.append('--console')
else:
pyinstaller_command.append('--windowed')
for file in config.get('files_to_include', []):
pyinstaller_command.extend(['--add-data', f'{file}:.'])
# 생성된 명령 출력
print("생성된 PyInstaller 명령:")
print(" ".join(pyinstaller_command))
try:
subprocess.run(pyinstaller_command, check=True, cwd=current_dir)
dist_path = os.path.join(current_dir, 'dist', exe_file_name)
QMessageBox.information(self, "EXE 생성", f"EXE 파일이 성공적으로 생성되었습니다: {dist_path}")
self.update_file_lists()
except subprocess.CalledProcessError as e:
QMessageBox.warning(self, "EXE 생성 오류", f"EXE 파일 생성 중 오류가 발생했습니다: {str(e)}")
except Exception as e:
QMessageBox.warning(self, "EXE 생성 오류", f"오류가 발생했습니다: {str(e)}")
def get_startup_programs(self):
startup_path = os.path.join(os.environ['APPDATA'], 'Microsoft', 'Windows', 'Start Menu', 'Programs', 'Startup')
return [f for f in os.listdir(startup_path) if f.endswith('.bat') or f.endswith('.lnk') or f.endswith('.exe')]
def update_file_lists(self):
# 배치 파일 목록 업데이트
self.batch_list.clear()
current_dir = os.getcwd()
for item in os.listdir(current_dir):
full_path = os.path.join(current_dir, item)
if os.path.isfile(full_path) and item.endswith('.bat'):
item_widget = FileListItem(item)
list_item = QListWidgetItem(self.batch_list)
list_item.setSizeHint(item_widget.sizeHint())
self.batch_list.addItem(list_item)
self.batch_list.setItemWidget(list_item, item_widget)
# EXE 파일 목록 업데이트
self.exe_list.clear()
dist_dir = os.path.join(current_dir, 'dist')
if os.path.exists(dist_dir):
for item in os.listdir(dist_dir):
full_path = os.path.join(dist_dir, item)
if os.path.isfile(full_path) and item.endswith('.exe'):
item_widget = FileListItem(item)
list_item = QListWidgetItem(self.exe_list)
list_item.setSizeHint(item_widget.sizeHint())
self.exe_list.addItem(list_item)
self.exe_list.setItemWidget(list_item, item_widget)
def run_file(self, item):
file_widget = item.listWidget().itemWidget(item)
if item.listWidget() == self.batch_list:
file_path = os.path.join(os.getcwd(), file_widget.filename)
else: # EXE list
file_path = os.path.join(os.getcwd(), 'dist', file_widget.filename)
try:
if file_path.endswith('.bat'):
subprocess.Popen(['cmd', '/c', file_path], creationflags=subprocess.CREATE_NEW_CONSOLE)
elif file_path.endswith('.exe'):
subprocess.Popen([file_path], shell=True)
except Exception as e:
QMessageBox.warning(self, "실행 오류", f"파일 실행 중 오류가 발생했습니다: {str(e)}")
def show_context_menu(self, position):
list_widget = self.sender()
item = list_widget.itemAt(position)
if item is not None:
context_menu = QMenu(self)
open_folder_action = context_menu.addAction("폴더 열기")
action = context_menu.exec(list_widget.mapToGlobal(position))
if action == open_folder_action:
self.open_file_folder(item)
def open_file_folder(self, item):
file_widget = item.listWidget().itemWidget(item)
if item.listWidget() == self.batch_list:
folder_path = os.getcwd()
else: # EXE list
folder_path = os.path.join(os.getcwd(), 'dist')
os.startfile(folder_path)
def main():
app = QApplication([])
window = UtilityManager()
window.show()
app.exec()
if __name__ == '__main__':
main()
우선 간편하게 배치파일화 시켜서 파이썬 코드를 즉시 실행시킬 수 있어서 만족스럽습니다.
exe 파일로 만드는 과정도 쉬운 편이라 잘 쓰고 있습니다.
발표를 위한 패들렛: https://padlet.com/sangsang24/padlet-ofjj6km6uodvmfae
도움 받은 글 (옵션)
pyinstaller를 써야 한다는 점을 블로그와 유튜브 등을 참고하였습니다.