Правильный способ реализации настраиваемого всплывающего диалогового окна tkinter
Я только начал изучать, как создать собственное всплывающее диалоговое окно; и, как оказалось, tkinter messagebox
действительно прост в использовании, но он также не делает слишком много. Вот моя попытка создать диалоговое окно, которое будет принимать ввод, а затем сохранять его в имени пользователя.
У меня вопрос, какой стиль рекомендуется использовать для реализации этого? Как Брайан Окли предложил в этом комментарии.
Я бы посоветовал не использовать глобальную переменную. Вместо того, чтобы диалог уничтожал сам себя, пусть он уничтожит только фактический виджет, но оставит объект живым. Затем позвоните что-то вроде
inputDialog.get_string()
а потомdel inputDialog
из вашей основной логики.
Может быть, использование глобальной переменной для возврата моей строки - не лучшая идея, но почему? И каков предложенный способ? Я запутался, потому что я не знаю, как вызвать gettring после того, как окно разрушено, и... строка об уничтожении фактического виджета, я не уверен, имеет ли он в виду 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()
7 ответов
Использование глобального утверждения не является необходимым в двух сценариях, которые приходят на ум.
- Вы хотите закодировать диалоговое окно, которое можно импортировать для использования с основным графическим интерфейсом
- Вы хотите закодировать диалоговое окно, которое можно импортировать для использования без основного графического интерфейса
закодировать диалоговое окно, которое можно импортировать для использования с основным графическим интерфейсом
Избежать глобального оператора можно, передав словарь и ключ при создании экземпляра диалогового окна. Словарь и ключ могут быть связаны с командой кнопки с помощью лямбды. Это создает анонимную функцию, которая будет выполнять вызов вашей функции (с аргументами) при нажатии кнопки.
Вы можете избежать необходимости передавать родительский элемент каждый раз, когда создаете экземпляр диалогового окна, привязывая родительский элемент к атрибуту класса (в данном примере 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://stackru.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)
Поскольку объект 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()
Вместо использования окна сообщений вы можете использовать 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
Я использовал 2-ую часть кода Честного Эйба под названием:
закодировать диалоговое окно, которое можно импортировать для использования без основного графического интерфейса
в качестве шаблона и внесены некоторые изменения. Мне нужен комбо-бокс вместо входа, поэтому я также реализовал его. Если вам нужно что-то еще, это должно быть довольно легко изменить.
Ниже приведены изменения
- Действует как ребенок
- Модал для родителя
- В центре сверху родителя
- Не изменяется
- Combobox вместо входа
- Нажмите крестик (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://stackru.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
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://stackru.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()
Я думаю, что правильный способ создания всплывающего сообщения — использовать встроенный
SimpleDialog
класс, который имеет ожидаемую функциональность. Пример подкласса можно найти ниже, может помочь просмотр исходного кода .
import tkinter as tk
import tkinter.simpledialog as simpledialog
class MessageBox(simpledialog.SimpleDialog):
def __init__(self, master,**kwargs):
simpledialog.SimpleDialog.__init__(self,master,**kwargs)
def done(self,num):
print(num)
self.root.destroy()
root = tk.Tk()
MessageBox(root,title='Cancel',text='Im telling you!',class_=None,
buttons=['Got it!','Nah'], default=None, cancel=None)
root.mainloop()
Tkinter simpledialog может быть полезен для решения этой проблемы.
Пример использования
import tkinter as tk
name = tk.simpledialog.askstring("Title", "Message")
ИСТОЧНИК: