Related
I have a window showing a tree view of multiple records fetched from MySql server. I was hoping to show another pop-up window with entries and comboboxes ,and have their default values set by the information of the record that was selected on.
The problem is that it does not display anything instead the boxes are left with blanks
I have tried to set the textvariable=var.get(), but this makes the combobox unable to change values and collect the users' input.
I have also tried to use a class:
class Application:
def __init__(self, parent, vars, pos_x, pos_y, to_test, width):
self.parent = parent
self.vars = vars
self.pos_x = pos_x
self.pos_y = pos_y
self.to_test = to_test
self.width = width
self.combo()
def combo(self):
self.box_value = tk.StringVar()
self.box = ttk.Combobox(self.parent, width=self.width,
textvariable=self.box_value)
self.box['values'] = self.vars
for i in range(len(self.vars)):
if not self.to_test == 'None':
if self.vars[i] == self.to_test:
self.box.current(i)
self.box.place(x=self.pos_x, y=self.pos_y)
#I have the function edit_tree called in a button of a previous window win
#The tree is the tree view displayed in the previous window
def edit_tree(win, tree):
selected = tree.selection()
edit_vendor_window = tk.Toplevel(win)
edit_vendor_window.geometry('600x570')
tk.Label(edit_vendor_window, text='Report Date:').place(x=10, y=10)
vars = ('2018', '2019')
Application(edit_vendor_window, vars, 150, 10, tree.item(selected,
'values)[0], 5)
I saw in some other answers that the problem is StringVar is getting garbage collected, and the solution is to use a class. But it does not work for me. This stuff just drove me crazy. Please help me out.
Your self.vars[i] and self.to_test might not be of the same type, so your for loop never reach the comparison part. Try cast both into Str and also remove the textvariable parameter in your Combobox.
def combo(self):
self.box = ttk.Combobox(self.parent, width=self.width)
self.box['values'] = self.vars
for i in range(len(self.vars)):
if not self.to_test == 'None':
if str(self.vars[i]) == str(self.to_test):
self.box.current(i)
self.box.place(x=self.pos_x, y=self.pos_y)
Is it possible to have a multi-line text entry field with drop down options?
I currently have a GUI with a multi-line Text widget where the user writes some comments, but I would like to have some pre-set options for these comments that the user can hit a drop-down button to select from.
As far as I can tell, the Combobox widget does not allow changing the height of the text-entry field, so it is effectively limited to one line (expanding the width arbitrarily is not an option). Therefore, what I think I need to do is sub-class the Text widget and somehow add functionality for a drop down to show these (potentially truncated) pre-set options.
I foresee a number of challenges with this route, and wanted to make sure I'm not missing anything obvious with the existing built-in widgets that could do what I need.
Terry's feedback made it clear that there was no simple way to solve this, so I created a custom class which wraps a Text and a Button into a frame, with a Toplevel containing a Listbox spawned by the button's callback function. I added a couple of "nice-to-have" features, like option highlighting within the Listbox, and I mapped bindings of the main widget onto the internal Text widget to make it easier to work with. Please leave a comment if there's any glaring bad practices here; I'm definitely still pretty inexperienced! But I hope this helps anybody else who's looking for a multi-line combobox!
class ComboText(tk.Frame):
def __init__(self, parent=None, **kwargs):
super().__init__(parent)
self.parent = parent
self._job = None
self.data = []
self['background'] = 'white'
self.text = tk.Text(self, **kwargs)
self.text.pack(side=tk.LEFT, expand=tk.YES, fill='x')
symbol = u"\u25BC"
self.button = tk.Button(self,width = 2,text=symbol, background='white',relief = 'flat', command = self.showOptions)
self.button.pack(side=tk.RIGHT)
#pass bindings from parent frame widget to the inner Text widget
#This is so you can bind to the main ComboText and have those bindings
#apply to things done within the Text widget.
#This could also be applied to the inner button widget, but since
#ComboText is intended to behave "like" a Text widget, I didn't do that
bindtags = list(self.text.bindtags())
bindtags.insert(0,self)
self.text.bindtags(tuple(bindtags))
def showOptions(self):
#Get the coordinates of the parent Frame, and the dimensions of the Text widget
x,y,width,height = [self.winfo_rootx(), self.winfo_rooty(), self.text.winfo_width(), self.text.winfo_height()]
self.toplevel = tk.Toplevel()
self.toplevel.overrideredirect(True) #Use this to get rid of the menubar
self.listbox = tk.Listbox(self.toplevel,width=width, height =len(self.data))
self.listbox.pack()
#Populate the options in the listbox based on self.data
for s in self.data:
self.listbox.insert(tk.END,s)
#Position the Toplevel so that it aligns well with the Text widget
list_height = self.listbox.winfo_reqheight()
self.toplevel.geometry("%dx%d+%d+%d" % (width, list_height, x, y+height))
self.listbox.focus_force()
self.listbox.bind("<Enter>", self.ListboxHighlight)
self.listbox.bind("<Leave>",self.stopListboxHighlight)
self.listbox.bind("<Button-1>",self.selectOption)
self.toplevel.bind("<Escape>", self.onCancel)
self.toplevel.bind("<FocusOut>", self.onCancel)
def ListboxHighlight(self,*ignore):
#While the mouse is moving within the listbox,
#Highlight the option the mouse is over
x,y = self.toplevel.winfo_pointerxy()
widget = self.toplevel.winfo_containing(x,y)
idx = self.listbox.index("#%s,%s" % (x-self.listbox.winfo_rootx(),y-self.listbox.winfo_rooty()))
self.listbox.selection_clear(0,100) #very sloppy "Clear all"
self.listbox.selection_set(idx)
self.listbox.activate(idx)
self._job = self.after(25,self.ListboxHighlight)
def stopListboxHighlight(self,*ignore):
#Stop the recurring highlight function.
if self._job:
self.after_cancel(self._job)
self._job = None
def onCancel(self,*ignore):
#Stop callback function to avoid error once listbox destroyed.
self.stopListboxHighlight()
#Destroy the popup Toplevel
self.toplevel.destroy()
def selectOption(self,event):
x,y = [event.x,event.y]
idx = self.listbox.index("#%s,%s" % (x,y))
if self.data:
self.text.delete('1.0','end')
self.text.insert('end',self.data[idx])
self.stopListboxHighlight()
self.toplevel.destroy()
self.text.focus_force()
def setOptions(self,optionList):
self.data = optionList
#Map the Text methods onto the ComboText class so that
#the ComboText can be treated like a regular Text widget
#with some other options added in.
#This was necessary because ComboText is a subclass of Frame, not Text
def __getattr__(self,name):
def textMethod(*args, **kwargs):
return getattr(self.text,name)(*args, **kwargs)
return textMethod
if __name__ == '__main__':
root = tk.Tk()
ct = ComboText(root, width = 50, height = 3)
ct.pack()
ct.setOptions(['Option %d' % i for i in range (0,5)])
root.mainloop()
I don't think you are missing anything. Note that ttk.Combobox is a composite widget. It subclasses ttk.Entry and has ttk.Listbox attached.
To make multiline equivalent, subclass Text. as you suggested. Perhaps call it ComboText. Attach either a frame with multiple read-only Texts, or a Text with multiple entries, each with a separate tag. Pick a method to open the combotext and methods to close it, with or without copying a selection into the main text. Write up an initial doc describing how to operate the thing.
I am creating labels in a for loop that display integers every time I fire an event (a mouse click) on my application. The problem is that old labels don't get erased and the new ones come on top of them causing a big mess.
Here is the working code that you can try out:
import numpy as np
import Tkinter as tk
class Plot(object):
def __init__(self, win):
self.win = win
self.bu1 = tk.Button(win,text='Load',command=self.populate,fg='red').grid(row=0,column=0)
self.listbox = tk.Listbox(win, height=5, width=5)
self.listbox.grid(row=1,column=0)#, rowspan=10, columnspan=2)
self.listbox.bind("<Button-1>", self.print_area)
def populate(self):
"""Populate listbox and labels"""
self.time = [1,2,3]
self.samples = ['a','b','c']
for item in self.time:
self.listbox.insert(tk.END,item)
for i,v in enumerate(self.samples):
tk.Label(self.win, text=v).grid(row=2+i,column=0,sticky=tk.W)
self.lbl_areas = []
for i in range(0, len(self.samples)):
self.lbl=tk.IntVar()
self.lbl.set(0)
self.lbl_areas.append(tk.Label(self.win,textvariable=self.lbl).grid(row=2+i,column=1,sticky=tk.W))
def print_area(self, event):
"""Prints the values"""
widget = event.widget
selection=widget.curselection()
value = widget.get(selection[0])
#Here is the dictionary that maps time with values
self.d = {1:[('a',33464.1),('b',43.5),('c',64.3)],
2:[('a',5.1),('b',3457575.5),('c',25.3)],
3:[('a',12.1),('b',13.5),('c',15373.3)]}
lbl_val = []
for i in range(0, len(self.samples)):
lbl_val.append(self.d[value][i][1])
for i in range(0, len(self.samples)):
self.lbl=tk.IntVar()
self.lbl.set(lbl_val[i])
tk.Label(self.win,textvariable=self.lbl).grid(row=2+i,column=1,sticky=tk.W)
def main():
root = tk.Tk()
app = Plot(root)
tk.mainloop()
if __name__ == '__main__':
main()
If You try to run this code and click on LOAD you will see the numbers appearing in the listbox and labels a,b,c with values set to zero at the beginning. If you click on the number in the listbox the values (mapped into the dictionary d) will appear but you will see the overwrite problem. How can I fix that?
How can I overcome this problem? Thank you
Don't create new labels. Create the labels once and then update them on mouse clicks using the configure method of the labels.
OR, before creating new labels delete the old labels.If you design your app so that all of these temporary labels are in a single frame you can delete and recreate the frame, and all of the labels in the frame will automatically get deleted. In either case (destroying the frame or destroying the individual labels) you would call the destroy method on the widget you want to destroy.
I'm currently trying to make simple image viewer, where you can adjust brightness, contrast, color and sharpness with scale. I'm doing it with tkinter and PIL. So far I have two classes, one for brightness and another for sharpness and I just can't find a way(I'm a beginner) to draw two scales in one window and one picture, which can be modified by this scales. All i could do was to draw scales, two pictures, where each scale modified their own picture.
Sending a code:
from tkinter import *
from PIL import Image, ImageTk, ImageEnhance
class Enhance(Frame):
def __init__(self, master, image, name, enhancer, lo, hi):
Frame.__init__(self, master)
self.tkim = ImageTk.PhotoImage(image.mode, image.size)
self.enhancer = enhancer(image)
self.update("1.0")
w = Label(self, image=self.tkim)
w.grid(row = 0, column = 0, padx = 0, pady = 0)
s = Scale(self, label=name, orient=VERTICAL,from_=hi, to=lo, resolution=0.01,command=self.update)
s.set(self.value)
s.grid(row = 1, column = 1, padx = 0, pady = 0)
def update(self, value):
self.value = eval(value)
self.tkim.paste(self.enhancer.enhance(self.value))
class Enhance1(Frame):
def __init__(self, master, image, name, enhancer, lo, hi):
Frame.__init__(self, master)
self.tkim = ImageTk.PhotoImage(image.mode, image.size)
self.enhancer = enhancer(image)
self.update("1.0")
w = Label(self, image=self.tkim).grid(row = 0, column = 0, padx = 0, pady = 0)
s = Scale(self, label=name, orient=VERTICAL,from_=hi, to=lo, resolution=0.01,command=self.update)
s.set(self.value)
s.grid(row = 0, column = 1, padx = 0, pady = 0)
def update(self, value):
self.value = eval(value)
self.tkim.paste(self.enhancer.enhance(self.value))
root = Tk()
im = Image.open("plant.jpg") #choose your image
Enhance(root, im, "Brightness", ImageEnhance.Brightness, 0.0, 3.0).grid(row = 0, column = 0, padx = 0, pady = 0)
Enhance1(root, im, "Sharpness", ImageEnhance.Sharpness, -1.0, 5.0).grid(row = 0, column = 0, padx = 0, pady = 0)
root.mainloop()
It's a design problem, not a Python one. You have to decouple the scales (possibly with a "ScaleDrawer" class) and enhancers from your frame and image, and the Frame class should take a list of enhancers and a list of "ScaleDrawer" objects.
You are putting both of the frames that hold the scales in the same spot -- row 0, column 0. If you want them to appear side-by-side you need to put them in different columns, or use pack.
Second, when using grid you should use the sticky argument, so that widgets expand to fill their containers. Also, you need to use the grid_rowconfigure and grid_columnconfigure methods to give at least one row and one column a weight of 1 (one) or more, so that your GUI has proper resize behavior. For simple layouts where everything is left-to-right or top-to-bottom, pack requires a few less lines of code.
Third, since you want both sliders to affect the same image, you don't want to create a label with the image in each of your classes Enhance and Enhance1. Instead, create a single image and put it in a single label. So, at the highest level you might create three widgets: a Label, an instance of Enhance, and an instance of Enhance1.
Finally, you seem to have an indentation error. The method update isn't indented, so it's not part of the class definition. Also, since this method is part of a class that subclasses Frame, you shouldn't call this method update, because that's a built-in method of all tkinter widgets.
My recommendation is to start over, rather than try to fix what you have. Begin with a blank slate, and first try to get the basic layout working before worrying about the sliders. This lets you focus on one thing instead of trying to get everything working at once.
For example, start with something like this to get the basic structure of your program (notice that I am not doing a global import, and I made the main program a class too, to help cut down on the need for global variables.
import Tkinter as tk
class ImageViewer(tk.Frame):
def __init__(self, master):
tk.Frame.__init__(self, master, background="green")
# for now, don't use images. This, so that we can get
# the basic structure working
self.im = None
self.tkim = None
# these three widgets make up our main layout
label = tk.Label(self, image=self.tkim, text="label")
e = Enhance(self, self.im)
e1 = Enhance1(self, self.im)
# grid works as well as pack in this case, but requires a
# little more code. For that reason I prefer pack for very
# simple layouts such as this.
label.pack(side="bottom", fill="both", expand=True)
e.pack(side="left", fill="both", expand=True)
e1.pack(side="right", fill="both", expand=True)
class Enhance(tk.Frame):
def __init__(self, master, image):
# we will be operating on this image, so save a
# reference to it
self.image = image
# width, height, and color are only temporary, they
# make it easy to see the frames before they have
# any content
self.image = image
tk.Frame.__init__(self, master, background="bisque", width=100, height=100)
class Enhance1(tk.Frame):
def __init__(self, master, image):
# we will be operating on this image, so save a
# reference to it
self.image = image
# width, height, and color are only temporary, they
# make it easy to see the frames before they have
# any content
tk.Frame.__init__(self, master, background="blue", width=100, height=100)
if __name__ == "__main__":
root = tk.Tk()
ImageViewer(root).pack(fill="both", expand=True)
root.mainloop()
Notice how the areas resize properly as the window is resized. Once we get that working, we can add widgets to the inner frames without having to worry about how they affect the main layout.
With this, you now have a good foundation for building up your GUI. Start by fleshing out just one of the Enhance classes, and make sure it's working exactly as you want. Then, do the same for the second one.
I'm attempting to have the user delete lines when the right mouse button is clicked. I have binded a button 3 press event to the canvas, and passed that to the following function
def eraseItem(self,event):
objectToBeDeleted = self.workspace.find_closest(event.x, event.y, halo = 5)
if objectToBeDeleted in self.dictID:
del self.dictID[objectToBeDeleted]
self.workspace.delete(objectToBeDeleted)
However nothing happens when I right click the lines. I have tested the dictionary separately and the line objects are being stored correctly.
Here is my binding:
self.workspace.bind("<Button-3>", self.eraseItem)
Per request some other snippets from the dictionary initialization
def __init__(self, parent):
self.dictID = {}
... Some irrelevant code omitted
For the line creation I have two handlers, an on click and an on release which draws the lines between both the coordinates
def onLineClick(self, event):
self.coords = (event.x, event.y)
def onLineRelease(self, event):
currentLine = self.workspace.create_line(self.coords[0], self.coords[1], event.x, event.y, width = 2, capstyle = ROUND)
self.dictID[currentLine] = self.workspace.coords(currentLine)
print(self.dictID.keys()) #For testing dictionary population
print(self.dictID.values()) #For testing dictionary population
The dictionary prints fine here.
Note that these are all functions within one class.
I've tried making a working example based on your code, and now I know where is the problem: find_closest returns a tuple with one element if an item is found, so when you check if it is in the dictionary, first you have to retrieve the first element of the tuple.
def eraseItem(self,event):
tuple_objects = self.workspace.find_closest(event.x, event.y, halo = 5)
if len(tuple_objects) > 0 and tuple_objects[0] in self.dictID:
objectToBeDeleted = tuple_objects[0]
del self.dictID[objectToBeDeleted]
self.workspace.delete(objectToBeDeleted)