Multi-Line Combobox in Tkinter - python

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.

Related

how to pass button value from custom widget to main application in tkinter when clicked

I have created a custom widget for tkinter that lays out 5 buttons. The widget works beautifully for the most part. The problem is that I cannot figure out how to pass the button that the user presses in the widget to the main application. The custom widget stores the last button pressed in a variable, but I cannot figure out how to make the main application see that it has been changed without resorting to binding a button release event to root. I would like to try to build out this custom widget further, and I want it to work without having to do some messy hacks. Ideally, in the example below, when a button is pressed, the label should change to reflect the button pressed. For example, if the user clicks the "2" button, the label should change to "2 X 2 = 4". How can I pass the text on the button directly to the main application for use? Hopefully, I am making it clear enough. I want to be able to get the value from the widget just like any other tkinter widget using a .get() method. Here is the code that I am using:
import tkinter as tk
from tkinter import ttk
class ButtonBar(tk.Frame):
def __init__(self, parent, width=5, btnLabels=''):
tk.Frame.__init__(self, parent)
self.btnLabels = []
self.btnNames = []
self.setLabels(btnLabels)
self.selButton = None
self.display()
def getPressedBtn(self,t):
"""
This method will return the text on the button.
"""
self.selButton = t
print(t)
def createBtnNames(self):
"""
This method will create the button names for each button. The button
name will be returned when getPressedBtn() is called.
"""
for i in range(0,5):
self.btnNames.append(self.btnLabels[i])
def display(self):
"""
This method is called after all options have been set. It will display
the ButtonBar instance.
"""
self.clear()
for i in range(len(self.btnLabels)):
self.btn = ttk.Button(self, text=self.btnLabels[i], command=lambda t=self.btnNames[i]: self.getPressedBtn(t))
self.btn.grid(row=0, column=i)
def setLabels(self, labelList):
if labelList == '':
self.btnLabels = ['1', '2', '3', '4', '5']
self.createBtnNames()
else:
btnLabelStr = list(map(str, labelList))
labelsLen = len(btnLabelStr)
def clear(self):
"""
This method clears the ButtonBar of its data.
"""
for item in self.winfo_children():
item.destroy()
root = tk.Tk()
def getButtonClicked(event):
global selBtn
print(event)
if example.winfo_exists():
selBtn = example.selButton
answer = int(selBtn) * 2
myLabel.config(text='2 X ' + selBtn + ' = ' + str(answer))
tabLayout = ttk.Notebook(root)
tabLayout.pack(fill='both')
vmTab = tk.Frame(tabLayout)
myLabel = tk.Label(vmTab, text='2 X 0 = 0', width=50, height=10)
myLabel.pack()
vmTab.pack(fill='both')
tabLayout.add(vmTab, text='Volume Movers')
# Create the ButtonBar.
example = ButtonBar(vmTab)
selBtn = None
example.pack()
lbl = tk.Label(root, text='')
root.mainloop()
I have looked at some other posts on stackoverflow. This one creating a custom widget in tkinter was very helpful, but it didn't address the button issue. I though this Subclassing with Tkinter might help. I didn't understand the If I bind the event using root.bind("<ButtonRelease-1>", getButtonClicked), then the widget works fine. Is there any other way to do it though?
I'd say that you have made the code more complex than it should be, you really just need to create the buttons and give them some callback that is passed as an argument. And that callback should take at least one argument which would be the text that would be on the button which will be also passed to that callback.
import tkinter as tk
from tkinter import ttk
class ButtonBar(tk.Frame):
def __init__(self, master, values: list, command=None):
tk.Frame.__init__(self, master)
for col, text in enumerate(values):
btn = ttk.Button(self, text=text)
if command is not None:
btn.config(command=lambda t=text: command(t))
btn.grid(row=0, column=col, sticky='news')
def change_label(val):
res = 2 * int(val)
new_text = f'2 X {val} = {res}'
my_label.config(text=new_text)
root = tk.Tk()
my_label = tk.Label(root, text='2 X 0 = 0')
my_label.pack(pady=100)
texts = ['1', '2', '3', '4', '5']
example = ButtonBar(root, values=texts, command=change_label)
example.pack()
root.mainloop()
You can also base the buttons on a list of values so that you can specify any values and it will create buttons that have that text on them and pressing them will call the given function with an argument of their text. That way you can use it as really any other widget, it would require the master, some values (text) and a command. Then you would just create that callback, which will take that one argument and then change the label accordingly. (I also removed all the notebook stuff, but I am just showing how you can achieve what you asked for)
Also:
I strongly suggest following PEP 8 - Style Guide for Python Code. Function and variable names should be in snake_case, class names in CapitalCase. Have two blank lines around function and class declarations. Object method definitions have one blank line around them.

Python tkinter: Allow window to be resized, but prevent it from automatically shrinking based on content?

I've been following the ttkbootstrap example for creating a collapsible frame widget. This example almost does what I need, but I've noticed the following odd behaviour:
If you run the example and do not resize the window, collapsing or expanding a frame changes the size of the entire window.
If you run the example and resize the window manually, collapsing or expanding a frame no longer changes the window size.
I'd really like to be able to use the example to build my own application, but avoid the behaviour where expanding or collapsing a frame changes the window size. I still need the window itself to be manually resizeable, so disabling window resizing altogether is not an option. Is there any way I can accomplish this?
PS: Below is a slightly modified version of the example code, which can be run without requiring the image assets that the original example uses, if you want to run the example to test it out.
"""
Author: Israel Dryer
Modified: 2021-05-03
"""
import tkinter
from tkinter import ttk
from ttkbootstrap import Style
class Application(tkinter.Tk):
def __init__(self):
super().__init__()
self.title('Collapsing Frame')
self.style = Style()
cf = CollapsingFrame(self)
cf.pack(fill='both')
# option group 1
group1 = ttk.Frame(cf, padding=10)
for x in range(5):
ttk.Checkbutton(group1, text=f'Option {x+1}').pack(fill='x')
cf.add(group1, title='Option Group 1', style='primary.TButton')
# option group 2
group2 = ttk.Frame(cf, padding=10)
for x in range(5):
ttk.Checkbutton(group2, text=f'Option {x+1}').pack(fill='x')
cf.add(group2, title='Option Group 2', style='danger.TButton')
# option group 3
group3 = ttk.Frame(cf, padding=10)
for x in range(5):
ttk.Checkbutton(group3, text=f'Option {x+1}').pack(fill='x')
cf.add(group3, title='Option Group 3', style='success.TButton')
class CollapsingFrame(ttk.Frame):
"""
A collapsible frame widget that opens and closes with a button click.
"""
ARROW_RIGHT = "\u2b9a"
ARROW_DOWN = "\u2b9b"
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.columnconfigure(0, weight=1)
self.cumulative_rows = 0
def add(self, child, title="", style='primary.TButton', **kwargs):
"""Add a child to the collapsible frame
:param ttk.Frame child: the child frame to add to the widget
:param str title: the title appearing on the collapsible section header
:param str style: the ttk style to apply to the collapsible section header
"""
if child.winfo_class() != 'TFrame': # must be a frame
return
style_color = style.split('.')[0]
frm = ttk.Frame(self, style=f'{style_color}.TFrame')
frm.grid(row=self.cumulative_rows, column=0, sticky='ew')
# header title
lbl = ttk.Label(frm, text=title, style=f'{style_color}.Invert.TLabel')
if kwargs.get('textvariable'):
lbl.configure(textvariable=kwargs.get('textvariable'))
lbl.pack(side='left', fill='both', padx=10)
# header toggle button
btn = ttk.Button(frm, text=self.ARROW_DOWN, style=style, command=lambda c=child: self._toggle_open_close(child))
btn.pack(side='right')
# assign toggle button to child so that it's accesible when toggling (need to change image)
child.btn = btn
child.grid(row=self.cumulative_rows + 1, column=0, sticky='news')
# increment the row assignment
self.cumulative_rows += 2
def _toggle_open_close(self, child):
"""
Open or close the section and change the toggle button image accordingly
:param ttk.Frame child: the child element to add or remove from grid manager
"""
if child.winfo_viewable():
child.grid_remove()
child.btn.configure(text=self.ARROW_RIGHT)
else:
child.grid()
child.btn.configure(text=self.ARROW_DOWN)
if __name__ == '__main__':
Application().mainloop()
I'd really like to ... avoid the behaviour where expanding or collapsing a frame changes the window size.
You can do that by forcing the window to a specific size. Once the geometry of a window has been set -- whether by the user dragging the window or the program explicitly setting it -- it won't resize when the contents of the window resize.
For example, you can force the window to be drawn, ask tkinter for the window size, and then use the geometry method to be that same size.
For example, try adding this as the last lines in Application.__init__:
self.update()
self.geometry(self.geometry())

When I hide a button and unhide the button, another duplicate button is added in tkinter. How to fix this?

Functionality: When I click "Music theory" button, I want it to show another button ("Sight reading" button) and when I click "Music theory" button again, I want the "Sight reading button to hide.
Inside musictheorysubbtns() I'm able to hide the dropdown optionmenu when I click the "Sight reading" button. I tried doing the same for the "music theory button thats outside the function and it added two buttons on the window. I'm able to hide it but when I click "music theory button again, it adds 2 more duplicate buttons, now in total 4 buttons.
How to fix this?
...
tab6 = ttk.Frame(tabControl)
tabControl.add(tab6, text="Music")
tabControl.pack(expand=5, fill='both')
musicbtnsframe = tk.Frame(tab6)
musicbtnsframe.pack(side='top')
mtheorysubbtsframe = tk.Frame(tab6)
mtheorysubbtsframe.pack(anchor='center')
mtdropdownframe = tk.Frame(tab6)
mtdropdownframe.pack(anchor='center')
mrefreshingframe = tk.Frame(tab6)
mrefreshingframe.pack(anchor='center')
def musictheorysubbtns():
def mtheorydrops():
mtheory_opt = mtdropdown
mtheory_opt.config(width=50, font=('Helvetica', 12))
mtheory_opt.pack()
def mtheory_images(mt):
print(mt) # selected option
mt_label.config(image=mtimgz[mt])
mt_label = tk.Label(mtdropdownframe)
mt_label.pack(side = 'bottom', pady=padylength)
mtimgz = {}
for mtimgz_name in tradinglists.retracements:
mtimgz[mtimgz_name] = ImageTk.PhotoImage(Image.open("./Images/{}.png".format(mtimgz_name)))
mtvariable = tk.StringVar(tab2)
mtvariable.set(tradinglists.retracements[0])
mtdropdown = tk.OptionMenu(mtdropdownframe, mtvariable, *tradinglists.retracements, command=mtheory_images)
def refreshmtsub():
mtdropdownframe.pack_forget() if mtdropdownframe.winfo_manager() else mtdropdownframe.pack(anchor='center')
mtheory_k = tk.Button(mtheorysubbtsframe, text="Sight reading", width = artbtn_width, height = btnsize_height, command=lambda:[mtheorydrops(), refreshmtsub()])
mtheory_k.pack(side = 'left', padx=padxwidth, pady=padylength)
def refreshmt():
mtheorysubbtsframe.pack_forget() if mtheorysubbtsframe.winfo_manager() else mtheorysubbtsframe.pack(anchor='center')
theory_k = tk.Button(musicbtnsframe, text="Music theory", width = btnsize_width, height = btnsize_height, command=lambda:[musictheorysubbtns(), refreshmt()])
theory_k.pack(side='left', padx=padxwidth, pady=padylength)
v2:
tab6 = ttk.Frame(tabControl)
tabControl.add(tab6, text="Music")
tabControl.pack(expand=5, fill='both')
class ShowHideButton(tk.Button):
def __init__(self, parent, target_widget, *args, **kwargs):
self.target = target_widget
super().__init__(parent, *args, **kwargs)
self.config(command=self.toggle)
def toggle(self, force_off = False):
if force_off or self.target.winfo_manager():
self.target.pack_forget()
else:
self.target.pack()
if isinstance(self.target, ShowHideButton):
self.target.toggle(force_off=True)
musicbtnsframe = tk.Frame(tab6)
musicbtnsframe.pack(side='top')
mt_sub_frame = tk.Frame(tab6)
mt_sub_frame.pack(side='top')
mt_SRframe = tk.Frame(tab6)
mt_SRframe.pack(anchor='center')
mt_compframe = tk.Frame(tab6)
mt_compframe.pack(anchor='center')
def mt_images(m1t):
print(m1t) # selected option
mt_label.config(image=mtimgz[m1t])
mt_label = tk.Label(mt_SRframe)
mt_label.pack(side = 'bottom', pady=padylength)
mtimgz = {}
for mt_name in musiclists.sightReaing:
mtimgz[mt_name] = ImageTk.PhotoImage(importImageWithResize("./Music_images/Sightreading/{}.png".format(mt_name)))
mtvar = tk.StringVar(tab2)
mtvar.set(musiclists.sightReaing[0])
mt = tk.OptionMenu(mt_SRframe, mtvar, *musiclists.sightReaing, command=mt_images)
mt_opt = mt
mt_opt.config(width=50, font=('Helvetica', 12))
mt_opt.pack()
def mtcomp_images(mtcomp1t):
print(mtcomp1t) # selected option
mtcomp_label.config(image=mtcompimgz[mtcomp1t])
mtcomp_label = tk.Label(mt_compframe)
mtcomp_label.pack(side = 'bottom', pady=padylength)
mtcompimgz = {}
for mtcomp_name in musiclists.composition:
mtcompimgz[mtcomp_name] = ImageTk.PhotoImage(importImageWithResize("./Music_images/Composition/{}.png".format(mtcomp_name)))
mtcompvar = tk.StringVar(tab2)
mtcompvar.set(musiclists.composition[0])
mtcomp = tk.OptionMenu(mt_compframe, mtcompvar, *musiclists.composition, command=mtcomp_images)
mtcomp_opt = mtcomp
mtcomp_opt.config(width=50, font=('Helvetica', 12))
mtcomp_opt.pack()
def mt_sub_btns():
theory_k_sightreading_k = ShowHideButton(mt_sub_frame, mt_opt, text='Sight Reading')
theory_k_composition_k = ShowHideButton(mt_sub_frame, mtcomp_opt, text='Composition')
theory_k = ShowHideButton(musicbtnsframe, mt_sub_btns, text='Music theory')
theory_k.pack(side='left', padx=padxwidth, pady=padylength)
When you want functionality added to a feature in tkinter, it is usually best to customize the widgets to do what you need instead of stringing together functionality. Let me show you what I mean with an example. Since you want one button that show/hides another button, and another button that show/hides an optionmenu, it sounds like it would be best for you to have buttons that show/hide other widgets.
To do this, make your own version of a button:
import tkinter as tk
class ShowHideButton(tk.Button):
def __init__(self, parent, target_widget, *args, **kwargs):
self.target = target_widget
super().__init__(parent, *args, **kwargs)
self.config(command=self.toggle)
The ShowHideButton is our custom name of our new widget. The (tk.Button) says which widget we are basing it off of.
The __init__ method (that's init with two underscores on each side) is what happens right when you make this widget. The parameters we put in there basically all need to be there. self is just required for python reasons, parent and *args, **kwargs are things that tk.Button needs, but target_widget is just something I added to be the target of our show/hide tools.
self.target = target_widget saves the widget we pass through the parameters to the instance of our button.
The line that starts with 'super' makes python run tk.Button's init method so it will build everything that is needed to be a button. (Which is good because I am trying to take tkinter's button widget and adjust it to fit our needs.)
The last line sets the command of the button to a function called self.toggle. Since this happens, you will never need to set the command of our buttons. In fact, we want our buttons to hide/show some other widget, and the whole purpose is to have that functionality built in so it wouldn't make sense to manually set the command.
Put this under the init method to define what self.toggle does:
def toggle(self):
if or self.target.winfo_manager():
self.target.pack_forget()
else:
self.target.pack()
That should be the same indentation level as the init method. You can probably see what this does. It checks if self.target is packed or not and then changes it.
If you use this, you may notice something kind of strange: if you hide a widget, and that widget had opened something, it leaves that something packed. If you would like it to pass down hides, change it to be something like this:
def toggle(self, force_off = False):
if force_off or self.target.winfo_manager():
self.target.pack_forget()
else:
self.target.pack()
if isinstance(self.target, ShowHideButton):
self.target.toggle(force_off=True)
This will check if the target is itself a ShowHideButton and then turn it off as well if it is.
Here is my whole test script to demo the new button:
import tkinter as tk
root = tk.Tk()
class ShowHideButton(tk.Button):
def __init__(self, parent, target_widget, *args, **kwargs):
self.target = target_widget
super().__init__(parent, *args, **kwargs)
self.config(command=self.toggle)
def toggle(self, force_off = False):
if force_off or self.target.winfo_manager():
self.target.pack_forget()
else:
self.target.pack()
if isinstance(self.target, ShowHideButton):
self.target.toggle(force_off=True)
my_option_value = tk.StringVar()
my_option_value.set('opt1')
my_option_menu = tk.OptionMenu(root, my_option_value, 'opt1', 'opt2', 'etc')
s_r_button = ShowHideButton(root, my_option_menu, text='Sight Reading')
m_t_button = ShowHideButton(root, s_r_button, text='Music theory')
m_t_button.pack()
root.mainloop()
You can just copy and paste that class-block into your code and start using ShowHideButtons like they are any other widget. Note the order that I made the buttons in the demo above though. They need to have their targets when you make them so you have to make the ones that are targets before the ones that target them.
This could also be adapted to use grid or place instead of pack. Really, you can modify tkinter widgets in any way you like. Let us know if you have any other questions etc.
EDIT: if you would like a version of this where you can have more than one item being toggled by the button, here you go:
class ShowHideButton(tk.Button):
def __init__(self, parent, target_widgets, *args, **kwargs):
self.targets = target_widgets
super().__init__(parent, *args, **kwargs)
self.config(command=self.toggle)
def toggle(self, force_off = False):
for i in self.targets:
if force_off or i.winfo_manager():
i.pack_forget()
else:
i.pack()
if isinstance(i, ShowHideButton):
self.target.toggle(force_off=True)
Note that you must make target_widgets an iterable item (like a list or tuple) in this case. If you use this version but only want one widget to toggle, you can make a single-length list by surrounding the name of it by [ ].

How to set Margin or Offset tag?

I am attempting to add a small automatic margin inside my text widget however I am having a hard time with writing the tags.
I have a text box and I am trying to insert text into that box while keeping a margin.
I can get the text that is inserted to have a margin but when I type past the last line the margin is gone. So far all I can dig up is how to write the tag and use it with insert() but I want to keep the margin always.
Question: Is there a way to keep the margin on all lines and not just the ones that were inserted from a file or string?
Note the same question extends to Offset tag because I experience the same problem with typing after the inserted text.
Here is what I have tried in a Minimal, Complete, and Verifiable example example.
import tkinter as tk
root = tk.Tk()
text = tk.Text(root, width = 10, height = 10)
text.pack()
text.tag_configure("marg", lmargin1 = 10, lmargin2 = 10)
text.insert("end", "Some random text!", ("marg"))
root.mainloop()
Unfortunately, the edge cases of adding and deleting text at the very start and end of the widget makes working with tags difficult.
If your goal is to maintain a margin, one solution is to create a proxy for the text widget so that you can intercept all inserts and deletes, and always add the margin each time the contents of the widget changes.
For example, start with a custom widget that generates a <<TextModified>> event whenever the widget is modified:
class CustomText(tk.Text):
def __init__(self, *args, **kwargs):
tk.Text.__init__(self, *args, **kwargs)
# create a proxy for the underlying widget
self._orig = self._w + "_orig"
self.tk.call("rename", self._w, self._orig)
self.tk.createcommand(self._w, self._proxy)
def _proxy(self, command, *args):
cmd = (self._orig, command) + args
result = self.tk.call(cmd)
if command in ("insert", "delete", "replace"):
self.event_generate("<<TextModified>>")
return result
(see https://stackoverflow.com/a/40618152/7432)
Next, modify your program to use this proxy to force the margin tag to always apply to the entire contents:
def add_margin(event):
event.widget.tag_add("marg", "1.0", "end")
text = CustomText(root, width = 10, height = 6)
text.bind("<<TextModified>>", add_margin)
If you add the tag to the entire range of text (including the final trailing newline), then new characters you type will inherit that tag.
Add the following, and perhaps it will work like you expect:
text.tag_add("marg", "1.0", "end")
Unfortunately, you'll lose this if you delete all of the text in the widget, but that can be worked around.

Event when entry's xview changes

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.

Categories

Resources