【问题标题】:Correct way to implement a custom popup tkinter dialog box实现自定义弹出 tkinter 对话框的正确方法
【发布时间】:2012-04-20 21:48:55
【问题描述】:

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

我的问题是推荐的实现方式是什么?正如 Bryan Oakley 在this comment 中所建议的那样。

我建议不要使用全局变量。与其让对话框自行销毁,不如让它仅销毁实际的小部件,但让对象保持活动状态。然后,从您的主逻辑中调用 inputDialog.get_string()del inputDialog 之类的东西。

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

我问的原因是因为我希望按下提交按钮后弹出框被销毁;因为毕竟我希望它恢复到主程序,更新一些东西等等。在这种情况下,按钮方法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()

【问题讨论】:

    标签: python tkinter dialog tkmessagebox


    【解决方案1】:

    在想到的两种情况下,使用global statement 是不必要的。

    1. 您想要编写一个可导入的对话框,以便主 GUI 一起使用
    2. 您想要编写一个可以导入的对话框,以便在没有主 GUI 的情况下使用

    编写一个可导入的对话框以使用 主 GUI


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

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

    您可以将以下内容保存为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()
    

    编写一个可以导入的对话框,以便在没有主 GUI 的情况下使用


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

    这是一个完整的模块,您可以借助以下参考资料对其进行自定义:NMTechEffbot
    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)
    

    【讨论】:

    • 如何使用第二个代码,以便在调用 mbox 时主 UI 不可点击?
    • @DRTauli 就我个人而言,如果我不想让人们与之交互,我会隐藏窗口;因为让它无响应可能会导致用户认为程序冻结了。但是,您可以暂时禁用大多数小部件。我建议将此作为一个广义的新问题提出; cmets 用于澄清和建议。
    【解决方案2】:

    由于对象 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()
    

    【讨论】:

    • 您能解释一下这与接受的答案有何不同和改进吗?
    • 我喜欢这样一个事实,即接受的答案包含创建带有或不带有根主循环的对话框的示例。它还向您展示了如何将参数传递给按钮命令。但我更喜欢将返回参数保存为类属性的更简单方法(如已接受答案的第二部分所述)。这个答案更多是关于组合我喜欢的部分以使其简单且用户可读。
    【解决方案3】:

    您可以使用 simpledialog,而不是使用消息框。它也是 tkinter 的一部分。它就像一个模板,而不是完全定义你自己的类。 simpledialog 解决了必须自己添加“确定”和“取消”按钮的问题。我自己也遇到过这个问题,java2s 有一个很好的例子来说明如何使用简单的对话框来制作自定义对话框。这是他们的两个文本字段和两个标签对话框的示例。它是 Python 2,所以你需要改变它。希望这会有所帮助:)

    from Tkinter import *
    import tkSimpleDialog
    
    class MyDialog(tkSimpleDialog.Dialog):
    
        def body(self, master):
    
            Label(master, text="First:").grid(row=0)
            Label(master, text="Second:").grid(row=1)
    
            self.e1 = Entry(master)
            self.e2 = Entry(master)
    
            self.e1.grid(row=0, column=1)
            self.e2.grid(row=1, column=1)
            return self.e1 # initial focus
    
        def apply(self):
            first = self.e1.get()
            second = self.e2.get()
            print first, second 
    
    root = Tk()
    d = MyDialog(root)
    print d.result
    

    来源:http://www.java2s.com/Code/Python/GUI-Tk/Asimpledialogwithtwolabelsandtwotextfields.htm

    【讨论】:

      【解决方案4】:

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

      编写一个可以在没有主 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
      

      【讨论】:

        【解决方案5】:
        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()
        

        【讨论】:

          猜你喜欢
          • 2014-03-21
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2020-05-24
          • 2017-03-11
          • 1970-01-01
          相关资源
          最近更新 更多