I am creating a simple program using Tkinter. I want a function to be called every time xview property of entry changes. But there doesn't seem to be an event like this, at least not one that I can find.
The <Configure> event fires only on resize, which I already handled, but it doesn't fire when actual value I'm tracking changes in a different way, such as the user dragging his mouse to see the end of the entry.
Here is the code:
import Tkinter as Tk
import tkFileDialog
root = Tk.Tk()
class RepositoryFolderFrame(Tk.Frame):
def __init__(self, root):
Tk.Frame.__init__(self, root)
self.build_gui()
self.set_entry_text("Searching...")
#root.after(0, self.find_repo)
self.prev_entry_index = len(self.entry.get())
root.bind("<Configure>", self.on_entry_resize)
#self.entry.bind(???, self.on_entry_change)
#self.entry.bind("<Configure>", self.on_entry_change)
def on_entry_resize(self, event):
cur_entry_index = self.entry.xview()[1]
if cur_entry_index != self.prev_entry_index:
self.entry.xview(self.prev_entry_index)
def on_entry_change(self, event):
# This should be called when xview changes...
cur_entry_index = self.entry.xview()[1]
self.prev_entry_index = cur_entry_index
def set_entry_text(self, text):
self.entry_text.set(text)
self.entry.xview("end")
def build_gui(self):
label = Tk.Label(self, text = "Repository folder:")
label.pack(side = Tk.LEFT)
self.label = label
entry_text = Tk.StringVar()
self.entry_text = entry_text
entry = Tk.Entry(self, width = 50, textvariable = entry_text)
entry.configure(state = 'readonly')
entry.pack(side = Tk.LEFT, fill = Tk.X, expand = 1)
self.entry = entry
button = Tk.Button(self, text = "Browse...")
button.pack(side = Tk.LEFT)
self.button = button
repo_frame = RepositoryFolderFrame(root)
repo_frame.pack(fill = Tk.X, expand = 1)
root.mainloop()
There is no mechanism for getting notified when the xview changes. There are ways to do it by modifying the underlying tcl code, but it's much more difficult than it's worth.
A simple solution is to write a function that polls the xview every few hundred milliseconds. It can keep track of the most recent xview, compare it to the current, and if it has changed it can fire off a custom event (eg: <<XviewChanged>>) which you can bind to.
It would look something like this:
class RepositoryFolderFrame(Tk.Frame):
def __init__(self, root):
...
self.entry.bind("<<XviewChanged>>", self.on_entry_change)
# keep a cache of previous xviews. A dictionary is
# used in case you want to do this for more than
self._xview = {}
self.watch_xview(self.entry)
def watch_xview(self, widget):
xview = widget.xview()
prev_xview = self._xview.get(widget, "")
self._xview[widget] = xview
if xview != prev_xview:
widget.event_generate("<<XviewChanged>>")
widget.after(100, self.watch_xview, widget)
You'll need to modify that for the edge case that the entry widget is destroyed, though you can handle that with a simple try around the code. This should be suitably performant, though you might need to verify that if you have literally hundreds of entry widgets.
Related
I'm trying to create a page that outputs a large amount of data, and wraps the text dynamically depending on window size. I started by setting wraplength = self.master.winfo_width(), which sets the text wrapping to the current window size, but it does not change when the window does. I found this answer, which seemed like it would solve the problem, but when trying to recreate it myself, something went wrong. I suspect that I'm misunderstanding something with .bind or <Configure>, but I can't be sure. My base code is as follows:
from tkinter import *
class Wrap_example(Frame):
def __init__(self):
Frame.__init__(self)
self.place(relx=0.5, anchor='n')
#Initalize list and variable that populates it
self.data_list = []
self.data = 0
#Button and function for creating a bunch of numbers to fill the space
self.button = Button(self, text = "Go", command = self.go)
self.button.grid()
def go(self):
for self.data in range(1, 20000, 100):
self.data_list.append(self.data)
#Label that holds the data, text = list, wraplength = current window width
self.data = Label(self, text = self.data_list, wraplength = self.master.winfo_width(), font = 'arial 30')
self.data.grid()
#Ostensibly sets the label to dynamically change wraplength to match new window size when window size changes
self.data.bind('<Configure>', self.rewrap())
def rewrap(self):
self.data.config(wraplength = self.master.winfo_width())
frame01 = Wrap_example()
frame01.mainloop()
A few things of note: I tried using the lambda directly as shown in the linked answer, but it didn't work. If I remove the rewrap function and use self.data.bind('<Configure>', lambda e: self.data.config(wraplength=self.winfo_width()), it throws a generic Syntax error, always targeting the first character after that line, (the d in def if the function is left in, the f in frame01 if it's commented out). Leaving rewrap as-is doesn't throw an error, but it doesn't perform any other apparent function, either. Clicking 'Go' will always spawn data that wraps at the current window size, and never changes.
There are few issues:
frame Wrap_example does not fill all the horizontal space when window is resized
label self.data does not fill all the horizontal space inside frame Wrap_example when the frame is resized
self.rewrap() will be executed immediately when executing the line self.data.bind('<Configure>', self.rewrap())
To fix the above issues:
set relwidth=1 in self.place(...)
call self.columnconfigure(0, weight=1)
use self.data.bind('<Configure>', self.rewrap) (without () after rewrap) and add event argument in rewrap()
from tkinter import *
class Wrap_example(Frame):
def __init__(self):
Frame.__init__(self)
self.place(relx=0.5, anchor='n', relwidth=1) ### add relwidth=1
self.columnconfigure(0, weight=1) ### make column 0 use all available horizontal space
#Initalize list and variable that populates it
self.data_list = []
self.data = 0
#Button and function for creating a bunch of numbers to fill the space
self.button = Button(self, text = "Go", command = self.go)
self.button.grid()
def go(self):
for self.data in range(1, 20000, 100):
self.data_list.append(self.data)
#Label that holds the data, text = list, wraplength = current window width
self.data = Label(self, text = self.data_list, wraplength = self.master.winfo_width(), font = 'arial 30')
self.data.grid()
#Ostensibly sets the label to dynamically change wraplength to match new window size when window size changes
self.data.bind('<Configure>', self.rewrap) ### remove () after rewrap
def rewrap(self, event): ### add event argument
self.data.config(wraplength = self.master.winfo_width())
frame01 = Wrap_example()
frame01.mainloop()
I am trying to delete a widget that has the name of a string but I cant find how to do it. This is what I have done so far but I cant get my head around that. I want to be able to select the name of the widget that I want to get deleted. any help would be useful
and this is the code that i have made so far
lasthover = "button1"
def dlt():
for widget in frm.winfo_children():
if widget == lasthover:
widget.destroy()
You can set the widget name using option name when creating the widget and then destroy it using the given name.
Below is an example:
import tkinter as tk
root = tk.Tk()
frm = tk.Frame(root)
frm.pack()
# create buttons with name button1, button2, etc
for i in range(1, 10):
name = f"button{i}"
tk.Button(frm, text=name, name=name).pack(side="left")
lasthover = "button1"
def dlt():
for widget in frm.winfo_children():
if widget._name == lasthover:
widget.destroy()
tk.Button(root, text="Destroy Button", command=dlt).pack()
root.mainloop()
ok, so here is my solution (using classes), this is how I did it in one of my projects (similarly), this also may raise a lot of questions so feel free to ask:
from tkinter import Tk, Button
root = Tk()
removable_widget_dict = {}
class RemovableWidget(Button):
def __init__(self, title, parent, key):
Button.__init__(self, parent)
self.parent = parent
self.title = title
self.key = key
self.button = Button(self.parent, text=self.title)
self.button.pack()
def delete(self):
global removable_widget_dict
self.button.destroy()
del removable_widget_dict[self.key]
print(removable_widget_dict)
for i in range(10):
key = f'item{i}'
removable_widget_dict[key] = RemovableWidget(f'Button {i}', root, key)
print(removable_widget_dict)
for key in removable_widget_dict.keys():
if key == 'item5':
removable_widget_dict[key].delete()
break
root.mainloop()
in simple terms: there is the class that will be the removable widgets, each time an instance of that class is created in the for loop, it is saved in dictionary. when deleting You match Your criteria with dictionary key and execute a function to delete the widget and remove it from the dictionary
I want to create buttons labelled with names from a list. When you click on a button its relief shall change from groove to sunken. There is one condition, only one button is allowed to be sunken. Thus, when you click on a button while another one is already sunken, the sunken one has to go back to groove.
How it looks like
I was able to put my idea into action and coded the whole thing. However, I'm wondering if there might be a better way to implement it. What is your opinion? Here is my code:
import tkinter as tk
from functools import partial
class ButtonSunken:
def __init__(self):
self.tags = ('A','B','C','D','E','F')
self.buttons = []
self.win = tk.Tk()
self.create_buttons()
self.win.mainloop()
def create_buttons(self):
for j,i in enumerate(self.tags):
self.buttons.append(tk.Button(self.win, text = i))
self.buttons[-1].grid(column=0, row=j)
ho_general = partial(self.button_pressed, self.buttons[-1])
self.buttons[-1].configure(command = ho_general)
def button_pressed(self, button):
try: # first time active_button does not exist yet
self.active_button.configure(relief = 'groove')
except:
pass
button.configure(relief = 'sunken')
self.active_button = button
t_object = ButtonSunken()
Thank you very much for your help!
Your method is pretty much good, just that it can be done without using any special functions. In my code, I just store the index of the current active button and set its relief to groove whenever the next button is pressed whose relief is in turn changed to sunken. Have a look at the code.
import tkinter as tk
class ButtonSunken:
def __init__(self):
self.tags = ('A','B','C','D','E','F')
self.buttons = []
self.active = None
self.win = tk.Tk()
self.create_buttons()
self.win.mainloop()
def create_buttons(self):
for j,i in enumerate(self.tags):
self.buttons.append(tk.Button(self.win, text=i, command=lambda x=j: self.button_pressed(x)))
self.buttons[-1].grid(column=0, row=j)
def button_pressed(self, idx):
if self.active is not None:
self.buttons[self.active].configure(relief='groove')
self.buttons[idx].configure(relief='sunken')
self.active = idx
t_object = ButtonSunken()
I have made a small application with tkinter and Python 3 which has four buttons on the top of the window to form a menu. It works fine but I want to know how to make the buttons appear along the window over a period of time starting from a single button in the center when first started rather than being statically placed in the center.
Here is my script so far:
import tkinter as tk
class utilities(tk.Frame):
def __init__(self, master=None):
super().__init__(master)
self.pack()
self.window()
def window(self):
self.pluginrun = tk.Button(self)
self.pluginrun["text"] = "Run Existing Plugin"
self.pluginrun["command"] = self.run_plugin
self.pluginrun.pack(side="left")
self.owning = tk.Button(self)
self.owning["text"] = "Add A New Plugin"
self.owning["command"] = self.plugin
self.owning.pack(side="left")
self.webpage = tk.Button(self)
self.webpage["text"] = "Webpage"
self.webpage["command"] = self.web
self.webpage.pack(side="left")
self.more_info = tk.Button(self)
self.more_info["text"] = "More"
self.more_info["command"] = self.more
self.more_info.pack(side="left")
def run_plugin(self):
print('Running Plugin')
def plugin(self):
print('Available Extensions')
def web(self):
print("Opening Webpage To Python.org")
def more(self):
print('Made Entirely In Python')
root = tk.Tk()
root.geometry('500x500')
show = utilities(master=root)
show.mainloop()
Which gives this result:
When first opened I would like it to look like this:
and over a period of time for more buttons to appear alongside one at a time until it looks like the first image.
How can this be done?
You can add all your buttons to a list and then use a repeating timed method to pack each button in the list one at a time at a set interval.
I created a counter that we can use to keep track of what button is going to be packed next from the list.
I also created a new list to store all the buttons in.
Then I modified your window() method to add each button to the list instead.
The last thing was to create a timed method that would use the self.counter attribute I created to keep track of what button is to be packed next.
In tkinter the best method to use to keep a timed loop or set a timer for anything is to use after(). Using sleep() or wait() in tkinter will only cause the entire tkinter app to freeze.
Take a look at the below code.
import tkinter as tk
class utilities(tk.Frame):
def __init__(self, master=None):
super().__init__(master)
self.pack()
self.list_of_buttons = []
self.counter = 0
self.window()
def window(self):
for count in range(4):
self.list_of_buttons.append(tk.Button(self))
pluginrun = self.list_of_buttons[0]
pluginrun["text"] = "Run Existing Plugin"
pluginrun["command"] = self.run_plugin
owning = self.list_of_buttons[1]
owning["text"] = "Add A New Plugin"
owning["command"] = self.plugin
webpage = self.list_of_buttons[2]
webpage["text"] = "Webpage"
webpage["command"] = self.web
more_info = self.list_of_buttons[3]
more_info["text"] = "More"
more_info["command"] = self.more
self.timed_buttons()
def timed_buttons(self):
if self.counter != len(self.list_of_buttons):
self.list_of_buttons[self.counter].pack(side ="left")
self.counter +=1
root.after(1500, self.timed_buttons)
def run_plugin(self):
print('Running Plugin')
def plugin(self):
print('Available Extensions')
def web(self):
print("Opening Webpage To Python.org")
def more(self):
print('Made Entirely In Python')
root = tk.Tk()
root.geometry('500x500')
show = utilities(master=root)
show.mainloop()
Add the Buttons inside a Frame, which you centre, and then as you add more Buttons, the Frame should centre them. If not, you may need to call root.update(), to re-centre the Frame.
i am going to create an tkinter gui app, and i know how i want it to look. but after playing around with tkinter, i found no way to toggle between screens when you press buttons down at the bottom. i know it does nothing but below is the simple layout i want to have, and switch between "myframe1" and "myframe2" kind of like the Apple App Store layout. is this possible?
from tkinter import *
tk = Tk()
tk.geometry("300x300")
myframe1 = Frame(tk,background="green",width=300,height=275)
myframe1.pack()
myframe2 = Frame(tk,background="cyan",width=300,height=275)
myframe2.pack()
btnframe = Frame(tk)
btn1 = Button(btnframe,text="screen1",width=9)
btn1.pack(side=LEFT)
btn2 = Button(btnframe,text="screen2",width=9)
btn2.pack(side=LEFT)
btn3 = Button(btnframe,text="screen3",width=9)
btn3.pack(side=LEFT)
btn4 = Button(btnframe,text="screen4",width=9)
btn4.pack(side=LEFT)
myframe1.pack()
btnframe.pack()
tk.mainloop()
something for you to get started with:
def toggle(fshow,fhide):
fhide.pack_forget()
fshow.pack()
btn1 = Button(btnframe,text="screen1", command=lambda:toggle(myframe1,myframe2),width=9)
btn1.pack(side=LEFT)
btn2 = Button(btnframe,text="screen2",command=lambda:toggle(myframe2,myframe1),width=9)
btn2.pack(side=LEFT)
Are you looking for something like a tabbed widget? You could use forget and pack as suggested here
Here is a class that I use in my code that works:
class MultiPanel():
"""We want to setup a pseudo tabbed widget with three treeviews. One showing the disk, one the pile and
the third the search results. All three treeviews should be hooked up to exactly the same event handlers
but only one of them should be visible at any time.
Based off http://code.activestate.com/recipes/188537/
"""
def __init__(self, parent):
#This is the frame that we display
self.fr = tki.Frame(parent, bg='black')
self.fr.pack(side='top', expand=True, fill='both')
self.widget_list = []
self.active_widget = None #Is an integer
def __call__(self):
"""This returns a reference to the frame, which can be used as a parent for the widgets you push in."""
return self.fr
def add_widget(self, wd):
if wd not in self.widget_list:
self.widget_list.append(wd)
if self.active_widget is None:
self.set_active_widget(0)
return len(self.widget_list) - 1 #Return the index of this widget
def set_active_widget(self, wdn):
if wdn >= len(self.widget_list) or wdn < 0:
logger.error('Widget index out of range')
return
if self.widget_list[wdn] == self.active_widget: return
if self.active_widget is not None: self.active_widget.forget()
self.widget_list[wdn].pack(fill='both', expand=True)
self.active_widget = self.widget_list[wdn]