Правильный способ реализации настраиваемого диалогового окна popup 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()

Ответы

Ответ 1

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

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

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


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

Вы можете избежать необходимости передавать родительский элемент каждый раз, когда вы создаете экземпляр диалогового окна, привязывая родителя к атрибуту класса (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 in py3) в effbot.

Следует отметить, что 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.
Сохраните следующий код как 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 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: /questions/145705/how-to-center-a-window-on-the-screen-in-tkinter/856529#856529
        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, он запускает mainloop,
который эффективно останавливает функцию там до тех пор, пока mainloop не выйдет через 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)

Ответ 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()