给cmd控制台程序 套壳 美化

给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()

    def _destroy(self):
        """ 关闭主窗口时,结束子进程
        :return:
        """
        if self.process is not None and self.process.poll() is None:
            self.process.kill()
        self.destroy()

    @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示例:
请添加图片描述

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

CtrlCV工程师

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值