Правильный способ реализации настраиваемого всплывающего диалогового окна tkinter

Я только начал учиться создавать настраиваемое всплывающее диалоговое окно; и, как оказалось, tkinter messagebox действительно прост в использовании, но он также не делает слишком много. Вот моя попытка создать диалоговое окно, которое будет вводить данные, а затем сохранять их в имени пользователя.

Мой вопрос в том, какой стиль рекомендуется использовать для этого? Как предложил Брайан Окли в этом комментарии.

Я бы не советовал использовать глобальную переменную. Вместо того, чтобы диалог уничтожать сам себя, пусть он уничтожит только фактический виджет, но оставит объект живым. Затем вызовите что-то вроде 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()

person George    schedule 07.04.2012    source источник


Ответы (4)


Использование глобального оператора не требуется в двух сценариях, которые приходить в голову.

  1. вы хотите создать код диалогового окна, которое можно импортировать для использования с основным графическим интерфейсом
  2. вы хотите закодировать диалоговое окно, которое можно импортировать для использования без основного графического интерфейса.

запрограммируйте диалоговое окно, которое можно импортировать для использования с основным графическим интерфейсом.


Избежать глобального оператора можно, передав словарь и ключ при создании экземпляра диалогового окна. Затем словарь и ключ можно связать с командой кнопки с помощью лямбда. Это создает анонимную функцию, которая будет выполнять вызов вашей функции (с аргументами) при нажатии кнопки.

Вы можете избежать необходимости передавать родительский элемент каждый раз, когда вы создаете экземпляр диалогового окна, привязав родительский элемент к атрибуту класса (root в этом примере).

Вы можете сохранить следующее как mbox.py в your_python_folder\Lib\site-packages или в той же папке, что и ваш основной файл графического интерфейса.

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

Вы можете увидеть примеры подкласса TopLevel и tkSimpleDialog (tkinter.simpledialog в py3) в effbot.

Стоит отметить, что виджеты ttk в этом примере взаимозаменяемы с виджетами tkinter.

Чтобы точно центрировать диалоговое окно, прочтите → это.

Пример использования:

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.
Сохраните следующий код как mbox.py в your_python_folder\Lib\site-packages

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)
person Honest Abe    schedule 08.04.2012
comment
Как я могу использовать второй код, чтобы основной пользовательский интерфейс не был доступен для кликов во время вызова mbox? - person DRTauli; 21.08.2014
comment
@DRTauli Лично я бы спрятал окно, если бы не хотел, чтобы люди с ним взаимодействовали; потому что если он не отвечает, пользователь может подумать, что программа зависла. Однако вы можете временно отключить большинство виджетов. Я рекомендую задать это как новый обобщенный вопрос; комментарии предназначены для разъяснений и предложений. - person Honest Abe; 22.08.2014

Поскольку объект 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()
person ashwinjv    schedule 30.07.2014
comment
Можете ли вы объяснить, чем это отличается от принятого ответа и улучшает его? - person skrrgwasme; 31.07.2014
comment
Мне нравится, что в принятом ответе есть примеры создания диалогового окна с корневым основным циклом или без него. Он также показывает, как передать аргумент кнопочной команде. Но я предпочитаю более простой способ сохранить возвращаемые аргументы в качестве атрибута класса (как указано во второй части принятого ответа). Этот ответ больше касался объединения частей, которые мне нравились, чтобы сделать его простым и понятным для пользователя. - person ashwinjv; 01.08.2014

Я использовал вторую часть кода Честного Эйба под названием:

закодируйте диалоговое окно, которое можно импортировать для использования без основного графического интерфейса

в качестве шаблона и внесены некоторые изменения. Мне нужен был комбинированный список вместо записи, поэтому я также реализовал его. Если вам нужно что-то еще, это должно быть довольно легко изменить.

Ниже приведены изменения

  • Действует как ребенок
  • Модально по отношению к родителю
  • По центру над родителем
  • Не изменяемый размер
  • Поле со списком вместо записи
  • Щелкните крестик (X), чтобы закрыть диалоговое окно.

Удалено

  • кадр, таймер, буфер обмена

Сохраните следующее как mbox.py в your_python_folder\Lib\site-packages или в той же папке, что и ваш основной файл графического интерфейса.

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
person SKS    schedule 11.09.2018

Вместо использования окна сообщений вы можете использовать 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

person theRookieCoder    schedule 15.05.2020