Python tkinter自定义消息窗口messagebox

tkinter模块python图形编程中常用的库,最近想要用tk中的messagebox实现一些简单的功能。

首先,我们看到tkinter中messagebox对像的使用。

from tkinter import messagebox

在pycharm中选中messagebox直接“ctrl+B"进入到messagebox文件,或者找到安装tkinter目录.\Lib\tkinter下的messagebox.py文件。翻到最下面,发现有几行测试代码:
在这里插入图片描述
运行python message.py
依次弹出
在这里插入图片描述
一共设计了七种常用窗口。来看看是如何实现的吧:
messagebox.py
通过看代码发现,这七个方法通过对 "_show" 这个函数传入不同的参数达到不同的显示效果。

def _show(title=None, message=None, _icon=None, _type=None, **options):
    if _icon and "icon" not in options:    options["icon"] = _icon
    if _type and "type" not in options:    options["type"] = _type
    if title:   options["title"] = title
    if message: options["message"] = message
    res = Message(**options).show()
    # In some Tcl installations, yes/no is converted into a boolean.
    if isinstance(res, bool):
        if res:
            return YES
        return NO
    # In others we get a Tcl_Obj.
    return str(res)

而负责显示的代码主要就一句:
res = Message(**options).show()
Message是这个文件的自定义类,理论上如果我们需要自定义一些功能只需要自定义一个类继承Message类,就可以增加自己的功能了,这个类就定义了一个command属性:

from tkinter.commondialog import Dialog
class Message(Dialog):
    "A message box"

    command  = "tk_messageBox"

于是我们去看父类Dialog的代码,

from tkinter import *

class Dialog:

    command  = None

    def __init__(self, master=None, **options):
        self.master  = master
        self.options = options
        if not master and options.get('parent'):
            self.master = options['parent']

    def _fixoptions(self):
        pass # hook

    def _fixresult(self, widget, result):
        return result # hook

    def show(self, **options):

        # update instance options
        for k, v in options.items():
            self.options[k] = v

        self._fixoptions()

        # we need a dummy widget to properly process the options
        # (at least as long as we use Tkinter 1.63)
        w = Frame(self.master)

        try:

            s = w.tk.call(self.command, *w._options(self.options))

            s = self._fixresult(w, s)

        finally:

            try:
                # get rid of the widget
                w.destroy()
            except:
                pass

        return s

找到show方法。找到用于显示的代码
w = Frame(self.master)
s = w.tk.call(self.command, *w._options(self.options))
发现似乎和想象的不一样,Dialog类自己并不用于显示,而是新建了一个Frame对象w,对这个调用了这个w.tk.call方法。
现在我想在消息窗口中增加一个倒计时功能,使窗口弹出后5秒钟自动关闭,我们自定义一个类:

from tkinter import messagebox
import threading
import time

class TimeMessage(messagebox.Message):

    def __init__(self, **options):
        super().__init__(**options)
        self.signal = True
        self.w = tk.Frame(self.master)
        t = threading.Thread(target=self.count_time)
        t.start()

    def count_time(self):

        _time = 5
        while _time:
            print(_time)
            if not self.signal:
                return
            time.sleep(1)
            _time -= 1
        self.w.destroy()

    def show(self, **options):

        # update instance options
        for k, v in options.items():
            self.options[k] = v

        self._fixoptions()

        # we need a dummy widget to properly process the options
        # (at least as long as we use Tkinter 1.63)

        try:

            s = self.w.tk.call(self.command, * self.w._options(self.options))
            s = self._fixresult(self.w, s)
            self.signal = False

        finally:

            try:
                # get rid of the widget
                self.w.destroy()
            except:
                pass

        return s

添加了一个计时的线程count_time(),一秒钟循环一次当时间减为0,则销毁w窗口,值得注意的是为了让计时函数使用w,这里将w写进init方法中,改为self.w。为了防止窗口点击关闭后,计时线程仍在运行,用一个信号量self.signal判断窗口是否已经关闭。
然后复制messagebox.py中的七个函数和"_show"函数,将res = Message(**options).show()改为res = TimeMessage(**options).show(),测试代码:

if __name__ == "__main__":
	showinfo(title="123", message="456")
运行

在这里插入图片描述
倒计时五秒后,报错。大概是说主进程不在主窗口循环中,也就是说,消息窗进入了阻塞状态,必须点击后,才能回到主窗口循环中。没办法,只能重头开始写了,其实Messagebox也就是一个普通的窗口,可贵在好看的图标和整齐的布局,这也不难,开始搞起吧。

效果图

在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述

下载图标

首先搜索到类似的图标,下载过来,类似这样:
在这里插入图片描述

自定义类

class TimeMessage(tk.Tk):

    def __init__(self, **kwargs):
        tk.Tk.__init__(self)

        self._title = kwargs["title"]
        self.message = kwargs["message"]
        self.timing = kwargs["timing"]
        self.detail = kwargs["detail"]

        self.details_expanded = False
        self.d_signal = False
        # self.withdraw()

        self.height = 0
        self.width = 0
        self.textbox = None  # 详细文本
        self.scrollbar = None  # 滚动条
        self.pic_frame = None  # 图片区域
        self.top = None

    def show(self):

        if self.timing:
            count_thread = threading.Thread(target=self.count_time)
            count_thread.start()
        if not self.detail:
            self.detail = self.message
        self.top_window(self.detail)

这个类继承了tk.TK类
输入参数包括
title:标题
message:显示信息
timing:倒计时秒数,为0则无倒计时功能
detail:详细信息(增加了一个详细信息显示功能)
看到上面代码的中的show函数最后一行会渲染一个TopLevel类,用于消息显示。

    def top_window(self, detail):
        self.top = tk.Toplevel()
        self.top.title(self._title)
      
        self.top.resizable(False, False)
        
        self.top.rowconfigure(2, weight=1)
        self.top.columnconfigure(0, weight=1)

        self.pic_frame = tk.Frame(self.top)
        self.pic_frame.grid(row=0, column=0, sticky="nsew")

        text_frame = tk.Frame(self.top)
        text_frame.grid(row=0, column=1, columnspan=2, sticky="nsew", padx=(0, 10))
        text_frame.columnconfigure(0, weight=1)
        text_frame.columnconfigure(1, weight=1)
        ttk.Label(text_frame, text=self.message[:100]).grid(row=0, column=0, columnspan=2, pady=(20, 7))

        button_frame = tk.Frame(self.top)
        button_frame.grid(row=1, column=1, columnspan=2, sticky="nsew", padx=(0, 10))
        button_frame.columnconfigure(0, weight=1)

        ttk.Button(button_frame, text="OK", command=self.destroy).grid(row=0, column=1, sticky="e")
        ttk.Button(button_frame, text="<<<Details", command=self.toggle_details).grid(row=0, column=2, sticky="w")

        detail_frame = tk.Frame(self.top)
        detail_frame.grid(row=2, column=0, columnspan=3, padx=(7, 7), pady=(7, 7), sticky="nsew")

        self.textbox = tk.Text(detail_frame, height=6)
        self.textbox.insert("1.0", detail)
        self.textbox.config(state="disabled")
        self.scrollbar = tk.Scrollbar(detail_frame, command=self.textbox.yview)

        self.top.protocol("WM_DELETE_WINDOW", self.destroy)

大部分代码都用于窗口的布局,这里采用的grid栅格布局的方法。包括左侧图片区域、文本区域、按钮区域,以及详细信息区域(新增)。
在这里插入图片描述
需要注意的是两个按钮,一个OK按钮绑定的是"窗口关闭事件"command=self.destory,另一个Details按钮绑定的是"弹出/隐藏详细信息事件"command=self.toggle_details下面我们码出这两个函数:

    def toggle_details(self):
        if not self.details_expanded:
            print(self.width)
            self.textbox["width"] = int((self.width-10)//7.7)
            self.textbox.grid(row=0, column=0)
            self.scrollbar.grid(row=0, column=1, sticky='nsew')
            self.top.geometry("{}x{}".format(self.width, self.height+105))
            self.details_expanded = True

        else:
            self.textbox.grid_forget()
            self.scrollbar.grid_forget()
            self.top.geometry("{}x{}".format(self.width, self.height))
            self.details_expanded = False

    def destroy(self):

        super().destroy()
        self.d_signal = True

由于destory是TK类的方法,所以重写该方法需要先调用父类的方法,然后将"计时线程的信号量"设置为True。然后toggle_details函数的主要原理是通过变量"self.details_expanded"判断详细信息是否弹出,设置窗口的大小达到显示/隐藏详细信息区域的目的。

加载图片

写到这里,你会有疑问,加载图片在哪里呢?别急,这里有一个bug,就是图片的加载必须要在类实例化后进行。很遗憾,由于笔者能力有限,没有发现这个bug的原因,(如果哪位大佬知道,请留言告诉我,不胜感激)。于是,只好吧这部分代码写入"_show"函数中:

def _show(title, message, detail, icon, timing, _type):

    if message is None:
        message = ""
    else:
        message = str(message)

    if title is None:
        title = _type
    window = TimeMessage(title=title, message=message, detail=detail, timing=timing)
    window.show()
    if icon:
        window.top.iconbitmap(icon)
    path = PATH[_type]
    img = Image.open(path)
    photo = ImageTk.PhotoImage(img)  # 在root实例化后创建,否则会报错
    label = tk.Label(window.pic_frame, image=photo)
    label.grid(row=0, column=0, pady=(15, 0), sticky="w", padx=(15, 5))

    window.top.update()
    window.height = window.top.winfo_height()
    window.width = window.top.winfo_width()

    window.mainloop()

可以看到,window就是实例化的TimeMessage类,调用"window.show()"函数后,Toplevel就渲染完成,这个时候再给显示区域铺上icon(左上角图标)和photo(左侧图片)。
注意下面有三行代码:

	window.top.update()
    window.height = window.top.winfo_height()
    window.width = window.top.winfo_width()

由于窗口没有设置固定大小,所以会根据文字多少自适应的窗口大小,而上面代码就是用来获取此时的窗口高宽的,来方便设置详细信息区域的宽度与初始宽度一致性。

好了,以上就是自定义消息窗口messagebox的全部内容了,觉得有用的话就点个赞吧。
代码放这里了:https://download.csdn.net/download/okfu_DL/12268051

  • 13
    点赞
  • 37
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值