给cmd控制台程序套壳美化,可以获取程序的标准输出和报错信息。
# _*_ coding: utf-8 _*_
""" 控制台程序启动器,杜绝黑窗口。
Time: 2023/10/18 15:28
Author: Jyun
Version: V 0.1
File: main.py
Blog: https://ctrlcv.blog.csdn.net
"""
import os
import subprocess
import threading
import time
import tkinter as tk
# 设置 Python 的标准输出编码[配置项]
os.environ['PYTHONIOENCODING'] = 'gbk'
# 窗口标题[配置项]
TITLE_BAR_TEXT = "Demo v1.0"
# 程序名称[配置项]
MAIN_TITLE = "Program Name"
# 程序停止时的状态提示[配置项]
STOPPED_STATE_TEXT = "Program stopped."
# 程序运行时的状态提示[配置项]
RUNNING_STATE_TEXT = "Program running."
# 程序启动脚本(可以是打包后的exe程序)[配置项]
COMMAND = ['python', 'test_program.py']
THREADS_LIST = []
def async_way(func):
def wrapper(*args, **kwargs):
t = threading.Thread(target=func, args=args, kwargs=kwargs)
t.setDaemon(True)
t.start()
THREADS_LIST.append(t)
return t # 返回线程对象用于后续操作
return wrapper
class GUI(tk.Tk):
def __init__(self):
tk.Tk.__init__(self)
self.process = None
self.title(TITLE_BAR_TEXT)
self.configure(bg="white")
self.resizable(False, False)
self.protocol("WM_DELETE_WINDOW", self.destroy)
# 主标题
self.label = tk.Label(self, text=MAIN_TITLE, font=("微软雅黑", 16), anchor="w", justify="left", bg="white")
self.label.pack(fill="x", padx=25, pady=5)
# 状态、操作栏块
self.div = tk.Frame(self, bg="white")
self.div.pack(fill="x", padx=25, pady=5)
# 当前状态显示TXT
self.state = tk.Label(self.div, text="Status Action Bar", font=("微软雅黑", 12), anchor="w", justify="left",
bg="white")
self.state.pack(side="left")
# 日志显示区
self.log_text = tk.Text(self, wrap=tk.WORD, width=60, height=20, bg="#f6f6f6", borderwidth=0, padx=10, pady=10)
self.log_text.pack(padx=25, pady=(5, 25))
# 停止按钮(如不需要 注释即可)
self.stop_button = tk.Button(self.div, text="停止", width=10, command=self.stop, bd=0, bg="#ff8787")
self.stop_button.pack(side="right")
# 启动按钮(如不需要 注释即可)
self.start_button = tk.Button(self.div, text="启动", width=10, command=self.start, bd=0, bg="#69db7c")
self.start_button.pack(side="right")
def log(self, message, color="black", autowrap=True):
""" 输出日志
:param message: 内容(行)
:param color: 文本颜色
:param autowrap: 是否自动换行
:return:
"""
message = str(message)
if autowrap:
message = message + "\n"
self.log_text.tag_configure("custom_color", foreground=color)
self.log_text.insert(tk.END, message, "custom_color")
self.log_text.see(tk.END)
# @async_way
def start(self):
""" 启动子程序
:return:
"""
self.state["text"] = RUNNING_STATE_TEXT
if self.process is not None:
self.log("程序正在运行,若要重新启动,请先停止", "red")
return
self.process = subprocess.Popen(COMMAND, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
self.read_child_output()
self.read_child_error()
def stop(self):
""" 停止子程序
:return:
"""
# 程序正常运行时`self.process.poll()`为None
if self.process is not None and self.process.poll() is None:
self.process.terminate()
self.log(f"等待进程结束:{self.process.pid}")
self.wait_process_exit()
else:
self.process = None # !
self.state["text"] = STOPPED_STATE_TEXT
self.log("程序已经停止")
self.log(f'辅助线程: {len(THREADS_LIST)}')
self.release_thread()
@async_way
def read_child_output(self):
""" 获取子进程的标准输出
:return:
"""
while True:
try:
output = self.process.stdout.readline()
except UnicodeDecodeError as e:
self.log(f'主线程输出解码错误 UnicodeDecodeError: {e}', color="red")
continue
if not output:
break
self.log(output, autowrap=False)
@async_way
def read_child_error(self):
""" 获取子进程的错误信息
:return:
"""
while True:
output = self.process.stderr.readline()
if not output:
break
self.log(output, color="red", autowrap=False)
@async_way
def wait_process_exit(self):
""" 等待进程结束
:return:
"""
while self.process.poll() is None:
time.sleep(1)
self.log(f"进程已结束,退出代码为 {self.process.poll()}")
self.process = None
self.state["text"] = STOPPED_STATE_TEXT
def release_thread(self):
""" 释放已结束的线程
:return:
"""
for thread in THREADS_LIST:
if not thread.is_alive():
thread.join()
THREADS_LIST.remove(thread)
self.log(f"释放线程:{thread}")
def run(self):
self.mainloop()
if __name__ == '__main__':
gui = GUI()
gui.run()
# TODO: 待实现
# 启动后自动运行子程序
# 添加日志长度限制
# 添加标题栏图标
# 添加程序图标
GUI示例: