How to reproduce class within tkinter - python-3.x

I built a little Synchronizer GUI program, using tkinter. I recently tried adding a +-Button, which reproduces the filebrowsing buttons and entry fields, so I can sync more than 2 directories. (I.e. personally, I have my Documents as well as Pictures backed up on a USB drive that I want to keep updated, but I dont want to copy my whole /home directory.)
I have run into two issues. The first issue is with the positioning of the plus Button: I have initialized the rowcount to zero, so everytime I click the +-Button, the new line of Buttons/fields is actually on a new line. sadly, this does not work for the plus button, that is defined outside the fuction, but shouldn't self.rowcount += 1 adjust rowcount for the whole class?
Secondly, I am not sure how to handle self.sourcefile and self.targetfile: When I select a new folder, it replaces the previous input, which is not the idea.
I would really appreciate any help!
from tkinter import *
from tkinter import filedialog
class Application():
def __init__(self):
self.root = Tk()
self.rowcount = 0
self.sourcefile = ""
self.targetfile = ""
self.sourceDirectory = Entry(self.root, width=10)
self.targetDirectory = Entry(self.root, width=10)
self.sourceDirectory.insert(0, "Source")
self.targetDirectory.insert(1, "Target")
selectSource = Button(self.root, text = "browse source", command=self.select_source, height=15, width=15)
selectTarget = Button(self.root, text = "browse target", command=self.select_target, height=15, width=15)
plusButton = Button(self.root, text = "+", command=self.create_new)
self.sourceDirectory.grid(row=0, column=0)
self.targetDirectory.grid(row=0, column=5)
selectSource.grid(row=0, column=1)
selectTarget.grid(row=0, column=10)
plusButton.grid(row=self.rowcount + 1, column=10)
self.root.mainloop()
def create_new(self):
self.rowcount += 1
print(self.rowcount)
self.sourceDirectory = Entry(self.root, width=10)
self.targetDirectory = Entry(self.root, width=10)
self.sourceDirectory.insert(0, "Source")
self.targetDirectory.insert(1, "Target")
selectSource = Button(self.root, image=self.browsericon, command=self.select_source, height=15, width=15)
selectTarget = Button(self.root, image=self.browsericon, command=self.select_target, height=15, width=15)
self.sourceDirectory.grid(row=self.rowcount, column=0)
self.targetDirectory.grid(row=self.rowcount, column=5)
selectSource.grid(row=self.rowcount, column=1)
selectTarget.grid(row=self.rowcount, column=10)
def select_source(self):
source = filedialog.askdirectory(title="Select Source")
self.sourceDirectory.delete(0, END)
self.sourceDirectory.insert(0, source)
self.sourcefile = source
def select_target(self):
target = filedialog.askdirectory(title="Select Target")
self.targetDirectory.delete(0, END)
self.targetDirectory.insert(1, target)
self.targetfile = target
Application()

shouldn't self.rowcount += 1 adjust rowcount for the whole class?
Yes, and it does in your code. However, changing the variable won't change the location of a widget that used that variable in a grid command.
My advice is to put the rows in one frame and the buttons in another. That way you don't have to keep adjusting the location of the buttons. For example:
self.row_frame = Frame(self.root)
self.button_frame = Frame(self.root)
self.button_frame.pack(side="bottom", fill="x")
self.row_frame.pack(side="top", fill="both", expand=True
Also, if the "+" button creates a new row, it shouldn't be duplicating code. You need to have a single function for adding a row. Since you already have a function to do that, you can call that function in __init__.
Putting it all together it looks something like this:
class Application():
def __init__(self):
self.root = Tk()
self.root.title("File Synchronizer")
self.rowcount = 0
self.sourcefile = ""
self.targetfile = ""
self.row_frame = Frame(self.root)
self.button_frame = Frame(self.root)
self.button_frame.pack(side="bottom", fill="x")
self.row_frame.pack(side="top", fill="both", expand=True)
startSync = Button(self.button_frame, text="Start", command=self.synchronize)
plusButton = Button(self.button_frame, text = "+", command=self.create_new)
startSync.grid(row=1, column=2)
plusButton.grid(row=0, column=10)
self.create_new()
self.root.mainloop()
def create_new(self):
self.rowcount += 1
self.sourceDirectory = Entry(self.row_frame, width=10)
self.targetDirectory = Entry(self.row_frame, width=10)
self.sourceDirectory.insert(0, "Source")
self.targetDirectory.insert(1, "Target")
selectSource = Button(self.row_frame, text = "browse source", command=self.select_source)
selectTarget = Button(self.row_frame, text = "browse source", command=self.select_target)
self.sourceDirectory.grid(row=self.rowcount, column=0)
self.targetDirectory.grid(row=self.rowcount, column=5)
selectSource.grid(row=self.rowcount, column=1)
selectTarget.grid(row=self.rowcount, column=10)
This doesn't put the "plus" and "Start" button in exactly the same place, but that's just because it's somewhat irrelevant to the answer. You can use whatever options you want to place it in the button frame. Since the two frames are independent, you can adjust rows, columns, and weights in one without affecting the other. Or, you can use pack in one frame and grid in another.
The other problem with your code is that self.sourceDirectory and self.targetDirectory can only hold one value so it will always refer to the last widgets that were created.
Since you are creating multiple source and target widgets, you need to save them in a list.
For example, start by adding an empty list to your application in the __init__ method:
self.sources = []
self.targets = []
Then, when you add a new row, append it to the list:
source_entry = Entry(self.row_frame, width=10)
target_entry = Entry(self.row_frame, width=10)
self.sources.append(source_entry)
self.targets.append(target_entry)
You can then iterate over these two lists to process all source and target values.
You will also have to modify the callback for the browse functions to accept an index so that the button knows which entry to update.

Related

Tkinter dialog's elements position

I am building custom Tkinter dialog window with Entry and Combobox. I am stuck with placing text and enter frames. Currently I am placing them manually. I am looking for the way to let tkinter do it automatically (maybe with pack() method). And also configure TopLevel size automatically.
My code:
def ask_unit_len():
values = ['millimeters', 'micrometers', 'nanometers']
top = Toplevel()
top.geometry('170x100')
top.resizable(False, False)
top.focus_set()
top.grab_set()
top.title('Enter length and units')
label_length = Label(top, text='Length:')
label_length.place(x=0, y=0)
units_type = StringVar()
length = StringVar()
answer_entry = Entry(top, textvariable=length, width=10)
answer_entry.place(x=55, y=0)
label_units = Label(top, text='Units:')
label_units.place(x=0, y=30)
combo = Combobox(top, width=10, textvariable=units_type,
values=values)
combo.place(x=50, y=30)
button = Button(top, text='Enter',
command=lambda:
mb.showwarning("Warning",
"Enter all parameters correctly")
if (units_type.get() == "" or not length.get().isdigit()
or int(length.get()) <= 0)
else top.destroy())
button.place(x=65, y=70)
top.wait_window(top)
return int(length.get()), units_type.get()
So, is there any way to perform this?

How to get the values of all the OptionMenu widgets within a frame inside a canvas in Tkinter

I'm writing a minimalist image tagging app that will list out all the image files in a specific location alongside a dropdown menu to select the options for tagging the image. Once the images are tagged, I need to save the changes to a JSON file and I've got a button for that. How can we read all the options selected so that it can be written into a file?
Following is the code so far:
from tkinter import N, RIGHT, Button, OptionMenu, Scrollbar, StringVar, Tk, Canvas, Frame, Label
class App:
def __init__(self):
self.root = Tk()
self.tags = ['Apples', 'Oranges', 'Berries']
self.GetRows()
self.SaveButton()
self.root.mainloop()
def GetRows(self):
self.canvas = Canvas(self.root)
self.scroll_y = Scrollbar(self.root, orient="vertical", command=self.canvas.yview)
self.frame = Frame(self.canvas)
lst = [f"A01{str(i)}.JPG" for i in range(100)]
for idx, r in enumerate(lst):
filename = Label(self.frame, text=r)
filename.grid(row=idx+2, column=0, sticky=N)
label = StringVar()
drop = OptionMenu(self.frame, label, *self.tags)
drop.grid(row=idx+2, column=1)
# put the frame in the canvas
self.canvas.create_window(0, 0, anchor='nw', window=self.frame)
# make sure everything is displayed before configuring the scrollregion
self.canvas.update_idletasks()
self.canvas.configure(scrollregion=self.canvas.bbox('all'),
yscrollcommand=self.scroll_y.set)
self.canvas.pack(fill='both', expand=True, side='left')
self.scroll_y.pack(fill='y', side='right')
def SaveState(self):
pass
def SaveButton(self):
self.save_button = Button(self.root, text="Save Changes", padx=50, pady=10, command=self.SaveState)
self.save_button.pack(side=RIGHT)
if __name__ == '__main__':
App()
The SaveState method is what will be used to write the selections so far into a file.
Thanks in advance!
In order to make OptionMenu results available try modifying your code so that
all StringVars are accessible outside of GetRows.
def GetRows(self):
...
# Define label as a list
self.label = []
for idx, r in enumerate(lst):
filename = Label(self.frame, text=r)
filename.grid(row=idx+2, column=0, sticky=N)
label = StringVar()
drop = OptionMenu(self.frame, label, *self.tags)
drop.grid(row=idx+2, column=1)
# Save StringVar reference
self.label.append(label)
...
def SaveState(self):
self.data = dict()
# Retrieve results into dictionary
for i, a in enumerate(self.label):
self.data[f"A_{i}"] = a.get()
print(self.data)
Then use json.dump(self.data, a_file) to save it

Passing OptionMenu into a callback (or retrieving a reference to the used widget)

I'm working on a (toplevel in a) GUI that consists of an array of 8 OptionMenus, each of them containing the same option list. Currently, Im building these widgets using a for-loop, and I save references in a dictionary. All OptionMenus link to the same (lambda) callback function.
To stay practical: the items in the option list represent a sequence of processing steps, and the user can alter the order of processes.
A change in one of the lists will result in one process being executed twice, and one process not at all. However, I want each item to occur only once. Hence, each user input should be accompanied by a second OptionMenu alteration.
For example: initial order 1-2-3 --> user changes the second process: 1-3-3, which autocorrects to: 1-3-2, where each process is again executed only once.
To my understanding, I can only get this to work if I have a reference to the OptionMenu that was just altered (from within the callback function). I was looking into passing the widget into the callback. The sample code is an attempt to implement the second suggested method, but the result is not what I would have expected.
The thing is that the OptionMenu widget seems to behave somewhat differently from other widgets. The OptionMenu does not allow for a re-defintion of the command function. No matter what input I pass along with the command function, the callback only seems to retrieve the OptionMenu selection, which is insufficient information for me to determine my process order.
Suggestions would be much apreciated!
import tkinter as tk
class Application(tk.Frame):
def __init__(self, master=None):
super().__init__(master)
self.grid(row=0, column=0, sticky=tk.N+tk.S+tk.E+tk.W)
self.create_widgets()
def create_widgets(self):
self.active_procs = ['proc 1','proc 2','proc 3','proc 4',
'proc 5','proc 6','proc 7','proc 8']
itemnr, widgets = dict(), dict()
for index in range(8):
name_construct = 'nr' + str(index)
itemnr[name_construct] = tk.StringVar(root)
itemnr[name_construct].set(self.active_procs[index])
widgets[name_construct] = tk.OptionMenu(self, itemnr[name_construct], *self.active_procs,
command=lambda widget=name_construct:
self.order_change(widget))
widgets[name_construct].grid(row=index+2, column=2, columnspan=2,
sticky="nwse", padx=10, pady=10)
def order_change(self,widget):
print(widget)
root = tk.Tk()
root.title("OptionMenu test")
app = Application(master=root)
root.mainloop()
The OptionMenu will pass the new value to the callback, so you don't have to do anything to get the new value. That's why your widget value isn't the value of name_construct -- the value that is passed in is overwriting the default value that you're supplying in the lambda.
To remedy this you simply need to add another argument so that you can pass the value of name_construct to the callback to go along with the value which is automatically sent.
It would look something like this:
widgets[name_construct] = tk.OptionMenu(..., command=lambda value, widget=name_construct: self.order_change(value, widget))
...
def order_change(self, value, widget):
print(value, widget)
Note: the OptionMenu isn't actually a tkinter widget. It's just a convenience function that creates a standard Menubutton with an associated Menu. It then creates one item on the menu for each option, and ties it all together with a StringVar.
You can get the exact same behavior yourself fairly easily. Doing so would make it possible to change what each item in the menu does when selected.
For those interested, below you can find an example code of how I got the widget behaviour I wanted. I took Bryan's advice to replace the OptionMenu for a Menubutton/Menu combination. I also made use of this post to find duplicate entries in my process order list.
Any thoughts or suggestions on how to implement this in a cleaner or shorter way, or how to get the same functionality with a different interface (e.g. drag and drop), are ofcourse welcome!
import tkinter as tk
class Application(tk.Frame):
def __init__(self, master=None):
super().__init__(master)
self.grid(row=0, column=0, sticky=tk.N+tk.S+tk.E+tk.W)
self.create_widgets()
def create_widgets(self):
# Assisting text
l1 = tk.Label(self, text = "Data in", font=(None, 15))
l1.grid(row=0, column=2)
l2 = tk.Label(self, text = u'\N{BLACK DOWN-POINTING TRIANGLE}', font=(None, 15))
l2.grid(row=1, column=2)
l3 = tk.Label(self, text = "Data out", font=(None, 15))
l3.grid(row=11, column=2)
l4 = tk.Label(self, text = u'\N{BLACK DOWN-POINTING TRIANGLE}', font=(None, 15))
l4.grid(row=10, column=2)
# Process list
self.active_procs = ['proc a','proc b','proc c','proc d',
'proc e','proc f','proc g','proc h']
self.the_value, self.widgets, self.topmenu = dict(), dict(), dict()
for index in range(8):
name_construct = 'nr' + str(index)
self.the_value[name_construct] = tk.StringVar(root)
self.the_value[name_construct].set(self.active_procs[index])
self.widgets[name_construct] = tk.Menubutton(self, textvariable=
self.the_value[name_construct],
indicatoron=True)
self.topmenu[name_construct] = tk.Menu(self.widgets[name_construct],
tearoff=False)
self.widgets[name_construct].configure(menu=self.topmenu[name_construct])
for proc in self.active_procs:
self.topmenu[name_construct].add_radiobutton(label=proc, variable=
self.the_value[name_construct],
command=lambda proc=proc,
widget=name_construct:
self.order_change(proc,widget))
self.widgets[name_construct].grid(row=index+2, column=2, columnspan=2,
sticky="nwse", padx=10, pady=10)
def order_change(self,proc,widget):
# Get the index of the last changed Menubutton
index_user_change = list(self.widgets.keys()).index(widget)
procs_order = [] # Current order from widgets
for index in range(8):
name_construct = 'nr' + str(index)
procs_order.append(self.widgets[name_construct].cget("text"))
# 1 change may lead to 1 double and 1 missing process
doubles = self.list_duplicates_of(procs_order,proc)
if len(doubles) == 2: # If double processes are present...
doubles.remove(index_user_change) # ...remove user input, change the other
missing_proc = str(set(self.active_procs)^set(procs_order)).strip('{"\'}')
index_change_along = int(doubles[0])
# Update references
self.active_procs[index_user_change] = proc
self.active_procs[index_change_along] = missing_proc
# Update widgets
name_c2 = 'nr'+str(index_change_along)
self.the_value[name_c2].set(self.active_procs[index_change_along])
self.widgets[name_c2].configure(text=missing_proc)
def list_duplicates_of(self,seq,item):
start_at = -1
locs = []
while True:
try:
loc = seq.index(item,start_at+1)
except ValueError:
break
else:
locs.append(loc)
start_at = loc
return locs
root = tk.Tk()
root.title("OptionMenu test")
app = Application(master=root)
root.mainloop()

tkinter user input widget to xlsx

How do I write user input from tkinter entry widget to xlsx?
Should I be using a different module?
from tkinter import *
import xlsxwriter
def output():
c = Toplevel(root)
c.title =("Main")
c.geometry =('')
e= Entry(c, width=20).grid(row=0, column=1, sticky=W, padx=10)
l=Label(c, text="Current Value").grid(row = 0, column=0, sticky=W, padx=10)
e2 = Entry(c, width=20).grid(row=1, column=1, sticky=W, padx=10)
l2= Label(c, text="New Value").grid(row = 1, column=0, sticky=W, padx=10)
b=Button(c, text="Submit",command= write_to_xlsx).grid(row= 1, column=2, sticky=E, padx=10)
def write_to_xlsx():
workbook =xlsxwriter.Workbook('tkintertest18.xlsx')
worksheet = workbook.add_worksheet()
worksheet.write_string('C1', 'e2.get()')
workbook.close()
root = Tk()
root.title("Main Menu")
root.geometry('400x400+230+130')
Button(root, text="1", command=output).grid(row =0, column= 2)
root.mainloop()
You should almost always use a class when tkinter is involved. So here is a working prototype of your code above. There were multiple problems. One is that when you slap grid next to your widgets instead of putting them on a new line, grid returns None, not the object. Implementing the app as a class enables the other functions to access the GUI components (in this case self.e2). Another error was you had single-quoted the 'e2.get()' call, which is technically a string. So removing the quotes fixes that. There may have been other things...
import xlsxwriter
class XLwriter:
def __init__(self):
c = Toplevel(root)
c.title =("Main")
c.geometry =('')
self.e= Entry(c, width=20).grid(row=0, column=1, sticky=W, padx=10)
self.l=Label(c, text="Current Value").grid(row = 0, column=0, sticky=W, padx=10)
self.e2 = Entry(c, width=20)
self.e2.grid(row=1, column=1, sticky=W, padx=10)
self.l2= Label(c, text="New Value").grid(row = 1, column=0, sticky=W, padx=10)
self.b=Button(c, text="Submit",command=self.write_to_xlsx).grid(row= 1, column=2, sticky=E, padx=10)
def write_to_xlsx(self):
workbook =xlsxwriter.Workbook('tkintertest18.xlsx')
worksheet = workbook.add_worksheet()
worksheet.write_string('C1', self.e2.get())
workbook.close()
root = Tk()
root.title("Main Menu")
root.geometry('400x400+230+130')
app = XLwriter()
#Button(root, text="1", command=output).grid(row =0, column= 2)
root.mainloop()
Seeing that Ron already provided you with a class to perform this task I will respond with the non-class method of doing this to more closely reflect the current code you have.
Keep in mind that with xlsxwriter there is not a way to read from the spreadsheet so for the entry field you have that is labeled as "Current Value" you will need to use a different excel library to fill that field with data from the spreadsheet.
Make sure if you are going to interact with a widget that you do not use the geometry manager directly with the creation of the widget. This will return None to anything trying to interact with it. Instead on the next line after you have created the widget you can use the widget variable name and then your geometry manager of choice(grid(), pack(), place()). This will allow us to interact with the widget without the issue of None being returned by the geometry manager.
In order for you to be able to interact with e2 outside of the output() function you will need to assign e2 to the global namespace. You can do this by simply adding global e2 inside your output() function.
Note: The use of global is best avoided whenever possible this is one compelling reason to use a more Object Oriented Programing method (class) as the use of global is not needed when using class attributes.
from tkinter import *
import xlsxwriter
root = Tk()
root.title("Main Menu")
#root.geometry('400x400+230+130')
def output():
global e2
c = Toplevel(root)
c.title = ("Main")
e = Entry(c, width = 20)
e.grid(row = 0, column = 1, sticky = W, padx = 10)
l = Label(c, text = "Current Value")
l.grid(row = 0, column = 0, sticky = W, padx = 10)
e2 = Entry(c, width=20)
e2.grid(row = 1, column = 1, sticky = W, padx = 10)
l2 = Label(c, text = "New Value")
l2.grid(row = 1, column = 0, sticky = W, padx = 10)
b = Button(c, text = "Submit",command = write_to_xlsx)
b.grid(row = 1, column = 2, sticky = E, padx = 10)
def write_to_xlsx():
workbook = xlsxwriter.Workbook('tkintertest18.xlsx')
worksheet = workbook.add_worksheet()
worksheet.write_string('C1', e2.get())
workbook.close()
btn = Button(root, text = "1", command = output)
btn.grid(row = 0, column = 2)
root.mainloop()

Imposible change text in a tkinter label

I'm making an editor with tkinter, I've made a status bar and i have 2 labels to show the actual line and column, and the total number of lines but the text of the labels don't change anything.
My code is very very long to show here.
I figure out the line, column and lines with these 2 functions:
def get_position(self, event=None):
"""get the line and column number of the text insertion point"""
self.line = tk.StringVar()
self.column = tk.StringVar()
self.line, self.column = self.textView.index('insert').split('.')
self.s = tk.StringVar()
self.s.set(('Line : {0} - Column : {1}'.format(self.line, self.column)))
print(self.s)
return self.s
def getwindowlines(self, event=None):
self.numberoflines = int(self.textView.index('end-1c').split('.')[0])
return self.numberoflines
And the function of my status bar is the next one:
def statusBar(self):
self.frameStatus = tk.Frame(self.master, border=2, bg='#272822',
relief='sunken')
self.frameStatus.pack(side='bottom', after=self.toolbar,
fill='x', padx=5, pady=1)
numberoflinestxt = str(self.getwindowlines())
self.labelNumberOfLines = tk.Label(self.frameStatus,
text='Lines: {0} '.format(numberoflinestxt))
self.labelNumberOfLines.configure(bg='#272822', fg='white')
self.labelNumberOfLines.pack(side='right', fill='x', padx=10, pady=2)
self.labelLinePosition = tk.Label(self.frameStatus,
textvariable=self.get_position())
self.labelLinePosition.configure(bg='#272822', fg='white')
self.labelLinePosition.pack(side='left', fill='x', padx=10, pady=2)
All the code is in Github Code Link in the file IdlePlus.py
With print console all works fine, but with a Label the numbers of lines and columns don't change.
Thanks
Your code doesn't seem to be trying to change the labels in the statusbar. I don't understand why you think they should change. In your get_position function you're creating new StringVars each time it is called.
I wouldn't use StringVars at all here, though you can. If you want to use them, you create them exactly once and associate them with Label widgets, and then whenever you want the labels to change, you change the variables. If you want to use .format(...), you have to call that when you change the values, not when you create the label.
For example:
def statusBar(self):
...
self.line = tk.StringVar()
self.column = tk.StringVar()
self.labelLinePosition = tk.Label(self.frameStatus,
textvariable=self.self.column)
self.labelLinePosition = tk.Label(self.frameStatus,
textvariable=self.column)
...
def get_position(self):
line, column = self.textView.index('insert').split('.')
self.line.set("Line: {0}".format(line))
self.column.set("Position: {0}".format(column)
That will cause the labels to update every time get_position is called.
However, there's really no need for the special StringVars. You can directly set the text of the label, eliminating a couple of objects and thus reducing the complexity of your code slightly:
def statusbar(self):
...
self.labelNumberOfLines = tk.Label(self.frameStatus)
self.labelLinePosition = tk.Label(self.frameStatus)
...
def get_position(self):
line, column = self.textView.index('insert').split('.')
self.labelNumberOfLines.configure(text="Lines: {0}".format(lines))
self.labelLinePosition.configure(text="Character: {0}".format(column))

Resources