web-dev-qa-db-fra.com

Façon correcte d'implémenter une boîte de dialogue popup tkinter personnalisée

Je viens de commencer à apprendre à créer une boîte de dialogue contextuelle personnalisée; et il s'avère que le tkinter messagebox est vraiment facile à utiliser, mais il n'en fait pas trop non plus. Voici ma tentative de créer une boîte de dialogue qui prendra des entrées, puis les stockera dans le nom d'utilisateur.

Ma question est quel est le style recommandé pour implémenter cela? Comme Bryan Oakley l'a suggéré dans ce commentaire .

Je déconseille d'utiliser une variable globale. Au lieu de laisser la boîte de dialogue se détruire, faites-la détruire uniquement le widget réel, mais laissez l'objet en vie. Ensuite, appelez quelque chose comme inputDialog.get_string() puis del inputDialog À partir de votre logique principale.

Peut-être que l'utilisation de la variable globale pour renvoyer ma chaîne n'est pas la meilleure idée, mais pourquoi? Et quelle est la manière suggérée? Je suis confus parce que je ne sais pas comment déclencher le getstring une fois la fenêtre détruite, et ... la ligne sur la destruction du widget réel, je ne sais pas s'il fait référence à TopLevel.

La raison pour laquelle je demande, c'est parce que je veux que la boîte pop-up soit détruite après avoir appuyé sur le bouton Soumettre; car après tout, je veux qu'il reprenne le programme principal, mette à jour quelque chose, etc. Que doit faire la méthode du bouton send dans ce cas? Parce que l'idée dans cet exemple particulier est de permettre à l'utilisateur de le faire encore et encore, s'il le souhaite.

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

L'utilisation de instruction globale n'est pas nécessaire dans les deux scénarios qui viennent à l'esprit.

  1. vous voulez coder une boîte de dialogue qui peut être importée pour utiliser avec une interface graphique principale
  2. vous voulez coder une boîte de dialogue qui peut être importée pour utiliser sans une interface graphique principale

coder une boîte de dialogue qui peut être importée pour utiliser avec une interface graphique principale


Pour éviter l'instruction globale, vous pouvez passer un dictionnaire et une clé lorsque vous créez une instance d'une boîte de dialogue. Le dictionnaire et la clé peuvent ensuite être associés à la commande du bouton, en utilisant lambda . Cela crée une fonction anonyme qui exécutera votre appel de fonction (avec args) lorsque vous appuyez sur le bouton.

Vous pouvez éviter d'avoir à transmettre le parent à chaque fois que vous créez une instance de la boîte de dialogue en liant le parent à un attribut de classe (racine dans cet exemple).

Vous pouvez enregistrer les éléments suivants sous mbox.py Dans your_python_folder\Lib\site-packages Ou dans le même dossier que le fichier de votre interface graphique principale.

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

Vous pouvez voir des exemples qui sous-classe TopLevel et tkSimpleDialog (tkinter.simpledialog dans py3) à effbot .

Il est à noter que widgets ttk sont interchangeables avec les widgets tkinter dans cet exemple.

Pour centrer avec précision la boîte de dialogue, lisez → this .

Exemple d'utilisation:

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

coder une boîte de dialogue qui peut être importée pour utiliser sans une interface graphique principale


Créez un module contenant une classe de boîte de dialogue (MessageBox ici). Incluez également une fonction qui crée une instance de cette classe et renvoie finalement la valeur du bouton enfoncé (ou les données d'un widget Entrée).

Voici un module complet que vous pouvez personnaliser à l'aide de ces références: NMTech & Effbot .
Enregistrez le code suivant sous mbox.py Dans 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)

et:

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

Après mbox crée une instance de MessageBox il démarre la boucle principale,
qui arrête effectivement la fonction jusqu'à ce que la boucle principale soit quittée via root.quit().
La fonction mbox peut alors accéder à msgbox.returning Et renvoyer sa valeur.

Exemple:

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)
30
Honest Abe

Étant donné que l'objet inputDialog n'est pas détruit, j'ai pu accéder à l'attribut d'objet. J'ai ajouté la chaîne de retour comme attribut:

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

J'ai utilisé 2ème partie du code de Honest Abe intitulé:

coder une boîte de dialogue qui peut être importée pour être utilisée sans interface graphique principale

comme modèle et fait quelques modifications. J'avais besoin d'une combobox au lieu d'une entrée, donc je l'ai également implémentée. Si vous avez besoin d'autre chose, il devrait être assez facile à modifier.

Voici les changements

  • Agit comme un enfant
  • Modal au parent
  • Centré sur le parent
  • Non redimensionnable
  • Combobox au lieu de l'entrée
  • Cliquez sur la croix (X) pour fermer la boîte de dialogue

Supprimé

  • cadre, minuterie, presse-papiers

Enregistrez les éléments suivants sous mbox.py dans your_python_folder\Lib\site-packages ou dans le même dossier que le fichier de votre interface graphique principale.

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

Il doit être rapide et facile à utiliser. Voici un exemple:

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
2
SKS