0

Im pretty new to python and tkinter and wrote a simple program where a user can fill 4 variables with input and can print it out in one button click. Now im wondering: Is there a way to make the program constantly scan the user input windows and refresh the output as soon as the user changed the input?

This is my program:

from tkinter import *

def calc():
    val1 = e1.get()
    val2 = e2.get()
    val3 = e3.get()
    val4 = e4.get()
    res = val1 + " " + val2 + " " + val3 + " " + val4
    label2 = Label(master)
    label2["text"] = res
    label2.grid(row=4, column = 1)

master = Tk()

Label(master, text="Main Value").grid(row=0, sticky = E)
Label(master, text="Second Value").grid(row=1, sticky = E)
Label(master, text="Third Value").grid(row=2, sticky = E)
Label(master, text="Fourth Value").grid(row=3, sticky = E)

e1 = Entry(master)
e2 = Entry(master)
e3 = Entry(master)
e4 = Entry(master)

e1.grid(row=0, column=1)
e2.grid(row=1, column=1)
e3.grid(row=2, column=1)
e4.grid(row=3, column=1)

button1 = Button(master, text="Calculate", command=calc)
button1.grid(row=4, column=0, sticky=W, pady=4)

master.mainloop()

I want the output to change as soon as the user changes one of the inputs.

doostaay
  • 71
  • 2
  • 12
  • 1
    if you have separated `Labels` for every `Entry` then you can use the same `StringVar` in `Entry` and `Label` - when you will put new char in `Entry` then it will automatically change text in `Label` – furas Apr 16 '19 at 14:52
  • It looks like what you want is to call a function when the input is changed, this is called a "callback" function. See this answer here: https://stackoverflow.com/questions/6548837/how-do-i-get-an-event-callback-when-a-tkinter-entry-widget-is-modified – joedeandev Apr 16 '19 at 13:54

3 Answers3

3

I would make use of the trace_add function of the tkinter variable classes. This ensures that the function gets called every time the contents of the Entry are changed. The downside to this is that you need to create a StringVar object for every Entry, but you're sure that you capture all changes, you don't call the function when you don't need to and there is no delay.

Next to that, you're creating a new Label widget every time you call calc now. Instead of creating a new Label every time, you should make one Label and update its text in calc.

Putting these changes together:

from tkinter import *


# Let calc accept the arguments passes by trace_add
def calc(*args):
    # Get the values from the StringVar objects
    val1 = v1.get()
    val2 = v2.get()
    val3 = v3.get()
    val4 = v4.get()
    res = val1 + " " + val2 + " " + val3 + " " + val4
    # Only change the text of the existing Label
    label2["text"] = res

master = Tk()

# make this Label once
label2 = Label(master)
label2.grid(row=4, column=1)

Label(master, text="Main Value").grid(row=0, sticky=E)
Label(master, text="Second Value").grid(row=1, sticky=E)
Label(master, text="Third Value").grid(row=2, sticky=E)
Label(master, text="Fourth Value").grid(row=3, sticky=E)

# Create StringVars
v1 = StringVar()
v2 = StringVar()
v3 = StringVar()
v4 = StringVar()

e1 = Entry(master, textvariable=v1)
e2 = Entry(master, textvariable=v2)
e3 = Entry(master, textvariable=v3)
e4 = Entry(master, textvariable=v4)

# Trace when the StringVars are written
v1.trace_add("write", calc)
v2.trace_add("write", calc)
v3.trace_add("write", calc)
v4.trace_add("write", calc)

e1.grid(row=0, column=1)
e2.grid(row=1, column=1)
e3.grid(row=2, column=1)
e4.grid(row=3, column=1)

master.mainloop()
fhdrsdg
  • 10,297
  • 2
  • 41
  • 62
  • 1
    Setting up an observer as shown, is probably the best way to achieve what the op wants; much better than repeatedly call `calc` every 100 ms as suggested in some answers. – Reblochon Masque Apr 16 '19 at 15:49
1

Yes, it is possible to do so.

Here is my way

You can use window.after(ms, func=None, args)to keep running in background which will update the user input without press of the button.

Updated Code

from tkinter import *

def calc():
    val1 = e1.get()
    val2 = e2.get()
    val3 = e3.get()
    val4 = e4.get()
    res = val1 + " " + val2 + " " + val3 + " " + val4
    label2["text"] = res

    # This will run the function in every 100ms (0.1 secs).
    master.after(100, calc)

master = Tk()

Label(master, text="Main Value").grid(row=0, sticky = E)
Label(master, text="Second Value").grid(row=1, sticky = E)
Label(master, text="Third Value").grid(row=2, sticky = E)
Label(master, text="Fourth Value").grid(row=3, sticky = E)

e1 = Entry(master)
e2 = Entry(master)
e3 = Entry(master)
e4 = Entry(master)

e1.grid(row=0, column=1)
e2.grid(row=1, column=1)
e3.grid(row=2, column=1)
e4.grid(row=3, column=1)

button1 = Button(master, text="Calculate", command=calc)
button1.grid(row=4, column=0, sticky=W, pady=4)

label2 = Label(master)
label2.grid(row=4, column = 1)

# Run the function and it will keep running in the background.
calc()

master.mainloop()
Saad
  • 3,340
  • 2
  • 10
  • 32
  • 1
    Do you realize this code creates 10 new Labels every second? – fhdrsdg Apr 16 '19 at 14:54
  • Yes I just noticed, I just assigned the label outside the function and inside the function `label2` will just update. – Saad Apr 16 '19 at 15:03
1

Trace is the right approach, tkinter takes care of the changes. I would use it with a data structure and create the widgets then in a loop immediately with trace, because it's much less code. The following is a full example from a GUI that uses many live monitored inputs with trace added on creation. Each change in an entry or click on a Checkbutton is traced and triggers a call to a method that deals with the change.

This is an all python >= 3 tkinter trace studying example :

# inputpanel.py derived and improved from my Coolprop GUI on github
#
from tkinter import *
import tkinter.ttk as ttk

class InputFrame(LabelFrame):
    #
    # This input frame creates Entries and selects for Variables
    # contained in a Dictionary structure. It traces the inputs 
    # and keeps the values updated according to the type of the value.
    # 
    # datadict needs at least the three dicts and the list below
    # for one key must be an entry in every dict
    # the list order is used for processing
    # You can pass a list order with only one field e.g. to init
    # and only this field will be processed
    #  
    # datadict={
    #             'verbose_names':{},
    #             'values':{},
    #             'callback_vars':{},
    #             'order':[],
    #             }
    # 
    # if a dict units is added to the datadict, the units will be displayed behind the entry widgets
    #

    def __init__(self, parent,cnf={}, title=None,datadict=None,order=None,frameborder=5, InputWidth=60,**kwargs):
        #
        LabelFrame.__init__(self, parent)
        #
        self.InputWidth=InputWidth
        if datadict :
            self.datadict=datadict
        else:
            self.datadict={
                'verbose_names':{},
                'values':{},
                'callback_vars':{},
                'order':[],
                }
        #
        if order :
            self.order=order
        else:
            self.order=self.datadict['order']
        #
        if title :
            self.IFrame = LabelFrame(parent, relief=GROOVE, text=title,bd=frameborder,font=("Arial", 10, "bold"))
        else:
            self.IFrame = LabelFrame(parent, relief=GROOVE,bd=frameborder,font=("Arial", 10, "bold"))
        #
        self.IFrame.grid(row=1,column=1,padx=8,pady=5,sticky=W)
        #
        self.InputPanel(self.IFrame)

    def InputPanel(self, PanelFrame, font=("Arial", 10, "bold")):
        '''
        '''
        #
        order_number=1
        for Dkey in self.order :
            if self.datadict['verbose_names'][Dkey] :
                #
                self.datadict['callback_vars'][Dkey].trace("w", lambda name, index, mode,
                                                         var=self.datadict['callback_vars'][Dkey],
                                                         value=self.datadict['values'][Dkey],
                                                         key=Dkey: self.InputPanelUpdate(var, key, value)
                                                         )
                Label(PanelFrame, text=self.datadict['verbose_names'][Dkey], font=font).grid(column=1, row=order_number, padx=8, pady=5, sticky=W)
                if type(self.datadict['values'][Dkey])==type(True):
                    Checkbutton(PanelFrame, width=self.InputWidth, variable=self.datadict['callback_vars'][Dkey], font=font).grid(column=2, row=order_number, padx=8, pady=5, sticky=W)
                else:
                    Entry(PanelFrame, width=self.InputWidth, textvariable=self.datadict['callback_vars'][Dkey], font=font).grid(column=2, row=order_number, padx=8, pady=5, sticky=W)
                try:
                    Label(PanelFrame, text=self.datadict['units'][Dkey],font=font).grid(column=3, row=order_number,padx=8,pady=5,sticky=W)
                except KeyError :
                    Label(PanelFrame, text='       ',font=font).grid(column=3, row=order_number,padx=8,pady=5,sticky=W)
            else :
                Label(PanelFrame, text=' ', font=font).grid(column=1, row=order_number, padx=8, pady=5, sticky=W)
            #
            order_number+=1

    def InputPanelUpdate(self, tkVar, key, value):
        #
        # Called on ever button press in an entry or click in a Checkbutton
        #
        if type(self.datadict['values'][key])==type(True):
            # For booleans we misuse a string because it is so easy
            self.datadict['values'][key] = True if tkVar.get()=='1' else False
        elif type(self.datadict['values'][key])==type(1): 
            # int
            self.datadict['values'][key] = int(tkVar.getint())
        elif type(self.datadict['values'][key])==type(1.1):
            # float
            self.datadict['values'][key] = float(tkVar.getdouble())
        else:
            # all the rest
            self.datadict['values'][key] = tkVar.get()

This is a dialog to create the parameters for sphinx-quickstart. It is incomplete, but if you don't need custom templates, it works. Add a print command containing the parameters of the InputPaneUpdate method of the SPInputFrame class, and you will quickly understand trace by monitoring console output ...

# sphinx_quickstartpanel.py
#
from tkinter import filedialog
from tkinter import messagebox
from tkinter import *
import tkinter.ttk as ttk

from tkinter.simpledialog import Dialog

from .inputpanel import InputFrame

import os
#
import subprocess
#
from django.template.defaultfilters import slugify


class SpInputFrame(InputFrame):
    #
    # Add local functions to InputPanelUpdate
    #
    def InputPanelUpdate(self, tkVar, key, value):
        #
        # overwrite InputPanelUpdate
        #
        if type(self.datadict['values'][key])==type(True):
            self.datadict['values'][key] = True if tkVar.get()=='1' else False
        else:
            self.datadict['values'][key] = tkVar.get()
            if key=='project':
                #
                # On project update, update slugged name too
                #
                self.datadict['values']['project_fn']=slugify(self.datadict['values'][key])
                self.datadict['callback_vars']['project_fn'].set(self.datadict['values']['project_fn'])

class sphinx_startpanel(Dialog):
    #
    # use gui to run sphinx-quickstart
    #
    def __init__(self, parent, title=None, data=None):
        #
        # Constructor
        #
        self.parent=parent
        self.data=data
        #
        self.Row1Frame = LabelFrame(parent, relief=GROOVE, text=' 1.) Enter project name',bd=5,font=("Arial", 10, "bold"))
        self.Row1Frame.grid(row=1,column=1,padx=8,pady=5,sticky=W+E, columnspan=3)
        #
        self.Row2Frame = LabelFrame(parent, relief=GROOVE, text=' 2.) Choose base directory' ,bd=5,font=("Arial", 10, "bold"))
        self.Row2Frame.grid(row=2,column=1,padx=8,pady=5,sticky=W+E, columnspan=3 )
        #
        self.Row3Frame = LabelFrame(parent, relief=GROOVE, text=' 3.) Enter main parameters',bd=5,font=("Arial", 10, "bold"))
        self.Row3Frame.grid(row=3,column=1,padx=8,pady=5,sticky=W)
        #
        self.Row4Frame = LabelFrame(parent, relief=GROOVE, text=' 4.) Run quickstart',bd=5,font=("Arial", 10, "bold"))
        self.Row4Frame.grid(row=4,column=1,padx=8,pady=5,sticky=W)
        #
        self.Row1IFrame=SpInputFrame(self.Row1Frame, title='Project Name',datadict=self.data,order=['project'])
        #
        self.b2=Button(self.Row2Frame,text="Choose parent directory of your new project")
        self.b2.grid(row=1,column=1,padx=8,pady=5,stick=W+E, columnspan=3)     
        self.b2.bind("<ButtonRelease-1>", self.Button_2_Click)
        #
        self.Row3IFrame=SpInputFrame(self.Row3Frame, title='Main configuration',datadict=self.data)
        #
        self.b4=Button(self.Row4Frame,text="Run this configuration and build the empty project")
        self.b4.grid(row=1,column=1,padx=8,pady=5,stick=W+E, columnspan=3)     
        self.b4.bind("<ButtonRelease-1>", self.runQuickstart)
        #

    def Button_2_Click(self,event): 
        #
        START_DIR = os.path.dirname(os.path.abspath(__file__) )
        #
        BASE_DIR = filedialog.askdirectory(parent=self.parent, initialdir=START_DIR ,title="Basisverzeichnis auswählen")
        self.data['values']['BASE_DIR']=BASE_DIR
        self.data['callback_vars']['BASE_DIR'].set(self.data['values']['BASE_DIR'])
        #
        self.data['values']['path']=os.path.join(BASE_DIR,self.data['values']['project_fn'])
        self.data['callback_vars']['path'].set(self.data['values']['path'])
        #
        print(self.data['values'])

    def getCommandline(self):
        '''
        creates the command for subprocess.Popen
        '''
        print('Running getCommandline ')
        # 
        cmdline=['sphinx-quickstart']
        cmdline.append(self.data['values']['path'])
        cmdline.append('-q')
        #
        print('getCommandline cmdline :',str(cmdline))
        #
        for key in self.data['argument_keys']:
            #
            if key in ['path','project_fn' ,'BASE_DIR'] :
                pass
            else:
                if self.data['values'][key] not in ['',False,' ']:
                    cmdline.append(self.data['argument_keys'][key])
                    if type(self.data['values'][key])==type(True):
                        pass
                    else :
                        cmdline.append(self.data['values'][key])
        #
        print(cmdline)
        return cmdline


    def runQuickstart(self,event):
        '''
        run sphinx quickstart -q with gathered information
        '''
        cmd=self.getCommandline()
        #
        retval = subprocess.call(["/bin/mkdir", "-p",self.data['values']['path']])
        #
        fproc=subprocess.Popen(cmd, stdout=subprocess.PIPE)
        #
        formbuffer,errortext=fproc.communicate()
        #
        print(errortext)

class Sphinxdialog:

    def __init__(self, master):
        dummyvar = sphinx_startpanel(master,data=self.getData())

    def getData(self):
        #
        # Define, create and deliver the initial data structure
        #
        # datadict needs at least the three dicts and the list below
        #  
        # datadict={
        #             'verbose_names':{},
        #            'values':{},
        #            'callback_vars':{},
        #            'order':[],
        #            }
        #
        # for each key must be an entry in every dict
        # the list order is used for processing
        # You can pass a list order with only one field e.g. to init
        # and only this field will be processed
        # 
        # see self.Row1IFrame above, passig the full dict but order contains only ['project'] 
        #
        # if a dict units is added to the datadict, the units will be displayed behind the entry widgets
        # the units dict can be incomplete
        # 
        # the argument_keys dict was added to call quickstart by commandline 
        #
        datadict = {
            'verbose_names':{
                'path'          :  'The directory name for the new project',
                'sep'           :  'if True, separate source and build dirs',
                'dot'           :  'replacement for dot in _templates etc.',
                'project'       :  'project name',
                'project_fn'    :  'Slugged project name for filenames',
                'author'        :  'author names',
                'version'       :  'version of project',
                'release'       :  'release of project',
                'language'      :  'document language',
                'suffix'        :  'source file suffix',
                'master'        :  'master document name',
                'epub'          :  'use epub',
                'autodoc'       :  'enable autodoc extension',
                'doctest'       :  'enable doctest extension',
                'intersphinx'   :  'enable intersphinx extension',
                'todo'          :  'enable todo extension',
                'coverage'      :  'enable coverage extension',
                'imgmath'       :  'enable imgmath for pdf, disable mathjax)',
                'mathjax'       :  'enable mathjax extension',
                'ifconfig'      :  'enable ifconfig extension',
                'viewcode'      :  'enable viewcode extension',
                'githubpages'   :  'enable githubpages extension',

                'BASE_DIR'      :  'Directory to create your project folder',

                'makefile'      :  'Create Makefile',
                'batchfile'     :  'Create batch command file',

                'TEMPLATE_DIR'  :  'Where to find the script templates (OPTIONAL)',

                },
            'values':{
                'path'          :  '.',
                'sep'           :  True,
                'dot'           :  '_',
                'project'       :  'project name',
                'project_fn'    :  'Slugged project name for filenames',
                'author'        :  'author names',
                'version'       :  'version of project',
                'release'       :  'release of project',
                'language'      :  'de',
                'suffix'        :  '.rst',
                'master'        :  'index',
                'epub'          :  False,
                'autodoc'       :  True,
                'doctest'       :  False,
                'intersphinx'   :  True,
                'todo'          :  False,
                'coverage'      :  False,
                'imgmath'       :  False,
                'mathjax'       :  True,
                'ifconfig'      :  True,
                'viewcode'      :  False,
                'githubpages'   :  False,

                'BASE_DIR'      :  '.',

                'makefile'      :  True,
                'batchfile'     :  False,

                'TEMPLATE_DIR'  :  '',
                },
            'argument_keys':{
                'path'          :  ' ',
                'sep'           :  '--sep',
                'dot'           :  '--dot',
                'project'       :  '--project',
                'project_fn'    :  None,
                'author'        :  '--author',
                'version'       :  '-v',
                'release'       :  '--release',
                'language'      :  '--language',
                'suffix'        :  '--suffix',
                'master'        :  '--master',
                'epub'          :  '--epub',
                'autodoc'       :  '--ext-autodoc',
                'doctest'       :  '--ext-doctest',
                'intersphinx'   :  '--ext-intersphinx',
                'todo'          :  '--ext-todo',
                'coverage'      :  '--ext-coverage',
                'imgmath'       :  '--ext-imgmath',
                'mathjax'       :  '--ext-mathjax',
                'ifconfig'      :  '--ext-ifconfig',
                'viewcode'      :  '--ext-viewcode',
                'githubpages'   :  '--ext-githubpages',

                'BASE_DIR'      :  None,

                'makefile'      :  '--makefile',
                'batchfile'     :  '--batchfile',

                'TEMPLATE_DIR'  :  '',
                },
            'order':[],
            'callback_vars':{},
            }
        #
        for key in datadict['verbose_names'] :
            datadict['callback_vars'][key]=StringVar()
            datadict['order'].append(key)
            datadict['callback_vars'][key].set(datadict['values'][key])

        return datadict

def main():
    root = Tk() 
    app = Sphinxdialog(root)
    root.mainloop()

if __name__ == '__main__':
    main()

You need to install django as I use it's slugify to make the example work.

JRM
  • 185
  • 2
  • 9