实现自定义弹出tkinter对话框的正确方法

2024-05-12 18:52:55 发布

您现在位置:Python中文网/ 问答频道 /正文

我刚刚开始学习如何创建一个自定义弹出对话框;事实证明,tkinter messagebox非常容易使用,但也不会做太多。下面是我创建一个对话框的尝试,该对话框将接受输入,然后将其存储在用户名中。

我的问题是推荐的实现方式是什么?正如布莱恩·奥克利在this comment中所建议的。

I would advise against using a global variable. Instead of having the dialog destroy itself, have it destroy only the actual widget but leave the object alive. Then, call something like inputDialog.get_string() and then del inputDialog from your main logic.

也许使用全局变量返回我的字符串不是最好的主意,但是为什么呢?建议的方法是什么?我很困惑,因为我不知道如何在窗口被破坏后触发getstring,而且。。。关于销毁实际小部件的行,我不确定他是否指的是TopLevel

我问这个问题的原因是,我希望在按下submit按钮后弹出框被销毁;因为毕竟,我希望它恢复到主程序,更新一些东西等等。在这种情况下,button方法send应该怎么做?因为在这个特定的例子中,如果用户愿意的话,允许他一遍又一遍地做这件事。

import tkinter as tk

class MyDialog:
    def __init__(self, parent):
        top = self.top = tk.Toplevel(parent)
        self.myLabel = tk.Label(top, text='Enter your username below')
        self.myLabel.pack()

        self.myEntryBox = tk.Entry(top)
        self.myEntryBox.pack()

        self.mySubmitButton = tk.Button(top, text='Submit', command=self.send)
        self.mySubmitButton.pack()

    def send(self):
        global username
        username = self.myEntryBox.get()
        self.top.destroy()

def onClick():
    inputDialog = MyDialog(root)
    root.wait_window(inputDialog.top)
    print('Username: ', username)

username = 'Empty'
root = tk.Tk()
mainLabel = tk.Label(root, text='Example for pop up input box')
mainLabel.pack()

mainButton = tk.Button(root, text='Click me', command=onClick)
mainButton.pack()

root.mainloop()

Tags: thetextselfsendtkintertopdefusername
3条回答

我用了Honest Abe's 2nd part of the code标题:

code a dialog box that can be imported to use without a main GUI

并做了一些修改。我需要一个组合框而不是条目,所以我也实现了它。如果你需要其他东西,应该很容易修改。

以下是更改内容

  • 像个孩子一样
  • 对父对象是模态的
  • 在父对象的顶部居中
  • 不可调整大小
  • 组合框而不是条目
  • 单击交叉(X)关闭对话框

删除

  • 帧、计时器、剪贴板

将以下文件另存为mbox.py中的your_python_folder\Lib\site-packages或与主GUI文件位于同一文件夹中。

import tkinter
import tkinter.ttk as ttk

class MessageBox(object):

    def __init__(self, msg, b1, b2, parent, cbo, cboList):

        root = self.root = tkinter.Toplevel(parent)

        root.title('Choose')
        root.geometry('100x100')
        root.resizable(False, False)
        root.grab_set() # modal

        self.msg = str(msg)
        self.b1_return = True
        self.b2_return = False
        # if b1 or b2 is a tuple unpack into the button text & return value
        if isinstance(b1, tuple): b1, self.b1_return = b1
        if isinstance(b2, tuple): b2, self.b2_return = b2
        # main frame
        frm_1 = tkinter.Frame(root)
        frm_1.pack(ipadx=2, ipady=2)
        # the message
        message = tkinter.Label(frm_1, text=self.msg)
        if cbo: message.pack(padx=8, pady=8)
        else: message.pack(padx=8, pady=20)
        # if entry=True create and set focus
        if cbo:
            self.cbo = ttk.Combobox(frm_1, state="readonly", justify="center", values= cboList)
            self.cbo.pack()
            self.cbo.focus_set()
            self.cbo.current(0)
        # button frame
        frm_2 = tkinter.Frame(frm_1)
        frm_2.pack(padx=4, pady=4)
        # buttons
        btn_1 = tkinter.Button(frm_2, width=8, text=b1)
        btn_1['command'] = self.b1_action
        if cbo: btn_1.pack(side='left', padx=5)
        else: btn_1.pack(side='left', padx=10)
        if not cbo: btn_1.focus_set()
        btn_2 = tkinter.Button(frm_2, width=8, text=b2)
        btn_2['command'] = self.b2_action
        if cbo: btn_2.pack(side='left', padx=5)
        else: btn_2.pack(side='left', padx=10)
        # the enter button will trigger the focused button's action
        btn_1.bind('<KeyPress-Return>', func=self.b1_action)
        btn_2.bind('<KeyPress-Return>', func=self.b2_action)
        # roughly center the box on screen
        # for accuracy see: https://stackoverflow.com/a/10018670/1217270
        root.update_idletasks()
        root.geometry("210x110+%d+%d" % (parent.winfo_rootx()+7,
                                         parent.winfo_rooty()+70))

        root.protocol("WM_DELETE_WINDOW", self.close_mod)

        # a trick to activate the window (on windows 7)
        root.deiconify()

    def b1_action(self, event=None):
        try: x = self.cbo.get()
        except AttributeError:
            self.returning = self.b1_return
            self.root.quit()
        else:
            if x:
                self.returning = x
                self.root.quit()

    def b2_action(self, event=None):
        self.returning = self.b2_return
        self.root.quit()

    def close_mod(self):
        # top right corner cross click: return value ;`x`;
        # we need to send it a value, otherwise there will be an exception when closing parent window
        self.returning = ";`x`;"
        self.root.quit()

它应该是快速和容易使用。下面是一个例子:

from mbox import MessageBox
from tkinter import *

root = Tk()


def mbox(msg, b1, b2, parent, cbo=False, cboList=[]):
    msgbox = MessageBox(msg, b1, b2, parent, cbo, cboList)
    msgbox.root.mainloop()
    msgbox.root.destroy()
    return msgbox.returning


prompt = {}

# it will only show 2 buttons & 1 label if (cbo and cboList) aren't provided
# click on 'x' will return ;`x`;
prompt['answer'] = mbox('Do you want to go?', ('Go', 'go'), ('Cancel', 'cancel'), root)
ans = prompt['answer']
print(ans)
if ans == 'go':
    # do stuff
    pass
else:
    # do stuff
    pass


allowedItems = ['phone','laptop','battery']
prompt['answer'] = mbox('Select product to take', ('Take', 'take'), ('Cancel', 'cancel'), root, cbo=True, cboList=allowedItems)
ans = prompt['answer']
print(ans)
if (ans == 'phone'):
    # do stuff
    pass
elif (ans == 'laptop'):
    # do stuff
    pass
else:
    # do stuff
    pass

由于对象inputDialog没有被破坏,所以我能够访问对象属性。我添加了返回字符串作为属性:

import tkinter as tk

class MyDialog:

    def __init__(self, parent):
        top = self.top = tk.Toplevel(parent)
        self.myLabel = tk.Label(top, text='Enter your username below')
        self.myLabel.pack()
        self.myEntryBox = tk.Entry(top)
        self.myEntryBox.pack()
        self.mySubmitButton = tk.Button(top, text='Submit', command=self.send)
        self.mySubmitButton.pack()

    def send(self):
        self.username = self.myEntryBox.get()
        self.top.destroy()

def onClick():
    inputDialog = MyDialog(root)
    root.wait_window(inputDialog.top)
    print('Username: ', inputDialog.username)

root = tk.Tk()
mainLabel = tk.Label(root, text='Example for pop up input box')
mainLabel.pack()

mainButton = tk.Button(root, text='Click me', command=onClick)
mainButton.pack()

root.mainloop()

在想到的两个场景中,使用global statement是不必要的。

  1. 您希望编写一个对话框,该对话框可以导入,以便与主GUI一起使用
  2. 您希望编写一个对话框,该对话框可以导入为使用而不使用主GUI

编写一个对话框,该对话框可以导入以与主图形用户界面一起使用


在创建对话框实例时,可以通过传递dictionary&key来避免全局语句。然后可以使用lambda将字典键与按钮的命令关联起来。这将创建一个匿名函数,在按下按钮时执行函数调用(带参数)。

通过将父对象绑定到类属性(本例中为根),可以避免每次创建对话框实例时都需要传递父对象。

您可以将以下文件保存为mbox.py中的your_python_folder\Lib\site-packages或与主GUI文件位于同一文件夹中。

import tkinter

class Mbox(object):

    root = None

    def __init__(self, msg, dict_key=None):
        """
        msg = <str> the message to be displayed
        dict_key = <sequence> (dictionary, key) to associate with user input
        (providing a sequence for dict_key creates an entry for user input)
        """
        tki = tkinter
        self.top = tki.Toplevel(Mbox.root)

        frm = tki.Frame(self.top, borderwidth=4, relief='ridge')
        frm.pack(fill='both', expand=True)

        label = tki.Label(frm, text=msg)
        label.pack(padx=4, pady=4)

        caller_wants_an_entry = dict_key is not None

        if caller_wants_an_entry:
            self.entry = tki.Entry(frm)
            self.entry.pack(pady=4)

            b_submit = tki.Button(frm, text='Submit')
            b_submit['command'] = lambda: self.entry_to_dict(dict_key)
            b_submit.pack()

        b_cancel = tki.Button(frm, text='Cancel')
        b_cancel['command'] = self.top.destroy
        b_cancel.pack(padx=4, pady=4)

    def entry_to_dict(self, dict_key):
        data = self.entry.get()
        if data:
            d, key = dict_key
            d[key] = data
            self.top.destroy()

您可以在effbot上看到子类TopLevel和tkSimpleDialog(py3中的tkinter.simpledialog)的示例。

值得注意的是,ttk widgets可以与本例中的tkinter小部件互换。

要使对话框精确居中,请阅读→this

使用示例:

import tkinter
import mbox

root = tkinter.Tk()

Mbox = mbox.Mbox
Mbox.root = root

D = {'user':'Bob'}

b_login = tkinter.Button(root, text='Log in')
b_login['command'] = lambda: Mbox('Name?', (D, 'user'))
b_login.pack()

b_loggedin = tkinter.Button(root, text='Current User')
b_loggedin['command'] = lambda: Mbox(D['user'])
b_loggedin.pack()

root.mainloop()

编写一个对话框,可以导入该对话框以使用而不使用主图形用户界面


创建包含对话框类(此处为MessageBox)的模块。此外,还包括一个函数,该函数创建该类的实例,并最终返回按下的按钮的值(或来自Entry小部件的数据)。

下面是一个完整的模块,您可以在这些引用的帮助下进行自定义:NMTech&;Effbot
your_python_folder\Lib\site-packages中将以下代码另存为mbox.py

import tkinter

class MessageBox(object):

    def __init__(self, msg, b1, b2, frame, t, entry):

        root = self.root = tkinter.Tk()
        root.title('Message')
        self.msg = str(msg)
        # ctrl+c to copy self.msg
        root.bind('<Control-c>', func=self.to_clip)
        # remove the outer frame if frame=False
        if not frame: root.overrideredirect(True)
        # default values for the buttons to return
        self.b1_return = True
        self.b2_return = False
        # if b1 or b2 is a tuple unpack into the button text & return value
        if isinstance(b1, tuple): b1, self.b1_return = b1
        if isinstance(b2, tuple): b2, self.b2_return = b2
        # main frame
        frm_1 = tkinter.Frame(root)
        frm_1.pack(ipadx=2, ipady=2)
        # the message
        message = tkinter.Label(frm_1, text=self.msg)
        message.pack(padx=8, pady=8)
        # if entry=True create and set focus
        if entry:
            self.entry = tkinter.Entry(frm_1)
            self.entry.pack()
            self.entry.focus_set()
        # button frame
        frm_2 = tkinter.Frame(frm_1)
        frm_2.pack(padx=4, pady=4)
        # buttons
        btn_1 = tkinter.Button(frm_2, width=8, text=b1)
        btn_1['command'] = self.b1_action
        btn_1.pack(side='left')
        if not entry: btn_1.focus_set()
        btn_2 = tkinter.Button(frm_2, width=8, text=b2)
        btn_2['command'] = self.b2_action
        btn_2.pack(side='left')
        # the enter button will trigger the focused button's action
        btn_1.bind('<KeyPress-Return>', func=self.b1_action)
        btn_2.bind('<KeyPress-Return>', func=self.b2_action)
        # roughly center the box on screen
        # for accuracy see: https://stackoverflow.com/a/10018670/1217270
        root.update_idletasks()
        xp = (root.winfo_screenwidth() // 2) - (root.winfo_width() // 2)
        yp = (root.winfo_screenheight() // 2) - (root.winfo_height() // 2)
        geom = (root.winfo_width(), root.winfo_height(), xp, yp)
        root.geometry('{0}x{1}+{2}+{3}'.format(*geom))
        # call self.close_mod when the close button is pressed
        root.protocol("WM_DELETE_WINDOW", self.close_mod)
        # a trick to activate the window (on windows 7)
        root.deiconify()
        # if t is specified: call time_out after t seconds
        if t: root.after(int(t*1000), func=self.time_out)

    def b1_action(self, event=None):
        try: x = self.entry.get()
        except AttributeError:
            self.returning = self.b1_return
            self.root.quit()
        else:
            if x:
                self.returning = x
                self.root.quit()

    def b2_action(self, event=None):
        self.returning = self.b2_return
        self.root.quit()

    # remove this function and the call to protocol
    # then the close button will act normally
    def close_mod(self):
        pass

    def time_out(self):
        try: x = self.entry.get()
        except AttributeError: self.returning = None
        else: self.returning = x
        finally: self.root.quit()

    def to_clip(self, event=None):
        self.root.clipboard_clear()
        self.root.clipboard_append(self.msg)

以及:

def mbox(msg, b1='OK', b2='Cancel', frame=True, t=False, entry=False):
    """Create an instance of MessageBox, and get data back from the user.
    msg = string to be displayed
    b1 = text for left button, or a tuple (<text for button>, <to return on press>)
    b2 = text for right button, or a tuple (<text for button>, <to return on press>)
    frame = include a standard outerframe: True or False
    t = time in seconds (int or float) until the msgbox automatically closes
    entry = include an entry widget that will have its contents returned: True or False
    """
    msgbox = MessageBox(msg, b1, b2, frame, t, entry)
    msgbox.root.mainloop()
    # the function pauses here until the mainloop is quit
    msgbox.root.destroy()
    return msgbox.returning

mbox创建MessageBox的实例之后,它将启动主循环,
在通过root.quit()退出主循环之前有效地停止那里的函数。
然后,mbox函数可以访问msgbox.returning,并返回其值。

示例:

user = {}
mbox('starting in 1 second...', t=1)
user['name'] = mbox('name?', entry=True)
if user['name']:
    user['sex'] = mbox('male or female?', ('male', 'm'), ('female', 'f'))
    mbox(user, frame=False)

相关问题 更多 >