TixComboBox subwidget not selecting/being set to specified value on first use - python

I have a TixExFileSelectDialog that is associated with several Entry objects in my program; the dialog is dynamically configured so that the selected file matches the text in the Entry the dialog is being used for. However, the first time the dialog is opened, regardless of which Entry is used, it only displays the pattern string, even when the Entry already has a default value. However, if I cancel out of the dialog and then reopen it, it displays the proper string. This happens when I set any combination of the combo box's selection and value options (one, the other, and both), as well as when I set the combo box's variable option to a StringVar. Is there something I'm missing in how TixComboBoxs function?
The code I'm currently using (with some reformatting/etc. for posting):
from tkinter.tix import *
opts = {'path': 'C:\\'}
class ImportGUI(Frame):
def _setfsd(self, directory='', pattern='*.xls', variable='', selection=None):
"Reconfigures the ExFileSelectDialog to enable reusability."
self.fsd.fsbox.config(directory=directory or opts['path'], # can't use opts['path'] as a default argument, because it could change.
pattern=pattern)
self.fsd.fsbox.file['variable'] = variable
if not variable:
self.fsd.fsbox.file['value'] = selection or pattern # Defaults to the pattern, which is the behavior of a fresh ExFileSelectionBox.
elif selection is not None: # variable exists, but setting selection manually as well
self.fsd.fsbox.file['value'] = selection
def _selectdatafile(self):
self._setfsd(selection='apple1.xls')
self.fsd.popup()
def _selectcodefile(self):
self._setfsd(selection='apple2.xls')
self.fsd.popup()
def createWidgets(self):
self.fsd = ExFileSelectDialog(self.master) # a top-level widget, so giving it the default root as master
self.dfrow = Frame(self)
self.cfrow = Frame(self)
self.dfentry = Entry(self.dfrow)
self.dfentry.pack(side='left')
self.cfentry = Entry(self.cfrow)
self.cfentry.pack(side='left')
self.dfbtn = Button(self.dfrow, text='...', command=self._selectdatafile)
self.dfbtn.pack(side='left')
self.cfbtn = Button(self.cfrow, text='...', command=self._selectcodefile)
self.cfbtn.pack(side='left')
self.dfrow.pack()
self.cfrow.pack()
self.pack()
def __init__(self, master=None):
Frame.__init__(self, master)
self.master.tk.eval('package require Tix')
self.createWidgets()
if __name__ == '__main__': # for testing
gui = ImportGUI()
gui.mainloop()

As it turns out, I had no need to do any of this in the first place because I can just use askopenfilename() from tkinter.filedialog to get exactly the functionality I desire, using the look-and-feel of the current OS. So much for Tix.
(Well, it wasn't exactly what I wanted, as the look was still a bit dated on Windows, but it was close enough. [IDLE seems to use it too, for that matter.])

Related

How do I bind callbacks to a listbox using tkinter?

I'm playing around with tkinter and am struggling somewhat with the use of listboxes. I want to bind an arbitrary function to a selection event. As I understand it - it should go something like this.
import Tkinter as tk
class SampleApp(tk.Tk):
def __init__(self, *args, **kwargs):
tk.Tk.__init__(self, *args, **kwargs)
lb = tk.Listbox(self)
lb.insert("end", "one")
lb.insert("end", "two")
lb.insert("end", "three")
lb.bind("<Double-Button-1>", self.OnDouble)
lb.pack(side="top", fill="both", expand=True)
def OnDouble(self, event):
widget = event.widget
selection=widget.curselection()
value = widget.get(selection[0])
print "selection:", selection, ": '%s'" % value
if __name__ == "__main__":
app = SampleApp()
app.mainloop()
My understanding is that when an element in the list box is double-clicked, a virtual event is created which can be bound to a callback function - in this case the OnDouble function defined.
I'm a little confused about the intricacies of this, would someone be able to explain exactly what is happening in this OnDouble function?
I'm trying to trigger an event when an element is selected from a listbox - i'm using pygubu to design the GUI but I understand that the syntax still follows tkinter conventions:
class TestApp:
def __init__(self):
self.builder = builder = pygubu.Builder()
builder.add_resource_path(PROJECT_PATH)
builder.add_from_file(PROJECT_UI)
self.mainwindow = builder.get_object('toplevel_1')
builder.connect_callbacks(self)
self.box = builder.get_object('listbox_1')
self.list = [1,2,3,4]
self.box.insert(tk.END, self.list[0])
self.box.insert(tk.END, self.list[1])
self.box.insert(tk.END, self.list[2])
self.box.insert(tk.END, self.list[3])
self.console = builder.get_object('text_1')
def print_console(self, msg):
self.console.configure(state="normal")
self.console.insert(tk.END, msg+"\n")
self.console.configure(state="disabled")
self.console.update_idletasks()
def run(self):
self.mainwindow.mainloop()
if __name__ == '__main__':
app = TestApp()
app.run()
Essentially I want the console to print (using the defined command) something like f'You have selected number {number}' when said number is double clicked on the list box. How can I do this? I would appreciate the workings behind it, so I can apply it to more complex exercises, and understand the solution rather than just copy/paste it.
Items in a listbox can be selected with a single click, but they can also be selected via the keyboard. For this reason, binding to a specific physical event such as <Double-1> is not the best way to be notified when the selection changes.
In the specific case of the listbox, binding to <<ListboxSelect>> is the best event to bind to, as that event will be generated after the selection has been set regardless of how it was set.
For example:
lb.bind("<<ListboxSelect>>", self.onSelect)
When a function is called via a binding it will automatically be passed an argument that contains details about the event that triggered the function call. One of the parameters is widget, which is a reference to the widget that received the call.
In your specific case you could ignore that and use self.listbox within the bound function, but using event.widget is a good habit to get into. That is why the code you copied starts with widget = event.widget
The next step is to get the selected item or items. In your case only a single selection is allowed at a time. However, the curselection method always returns a list. selection[0] will refer to the item selected, if there is a selection.
Note: the <<ListboxSelect>> event is triggered both when the selection is set or it is unset, so the code in your question could fail if the items are deselected by some means.
Finally, once you have the index of the selected item (eg: selection[0]), you can use that numerical index to get the text of the selected item. That is what the line value = widget.get(selection[0]) is doing.

How to run tkinter code after event? [duplicate]

Every time a character is entered into a Text widget, I want to get the contents of that widget and subtract its length from a certain number (basically a "you have x characters left" deal).
But the StringVar() is always one event behind. This is, from what I gather, because the event is processed before the character is entered into the Text widget. This means that if I have 3 characters in the field and I enter a 4th, the StringVar is updated but is still 3 characters long, then it updates to 4 when I enter a 5th character.
Is there a way to keep the two in line?
Here's some code. I removed irrelevant parts.
def __init__(self, master):
self.char_count = StringVar()
self.char_count.set("140 chars left")
self.post_tweet = Text(self.master)
self.post_tweet.bind("<Key>", self.count)
self.post_tweet.grid(...)
self.char_count = Label(self.master, textvariable=self.foo)
self.char_count.grid(...)
def count(self):
self.x = len(self.post_tweet.get(1.0, END))
self.char_count.set(str(140 - self.x))
A simple solution is to add a new bindtag after the class binding. That way the class binding will fire before your binding. See this answer to the question How to bind self events in Tkinter Text widget after it will binded by Text widget? for an example. That answer uses an entry widget rather than a text widget, but the concept of bindtags is identical between those two widgets. Just be sure to use Text rather than Entry where appropriate.
Another solution is to bind on KeyRelease, since the default bindings happen on KeyPress.
Here's an example showing how to do it with bindtags:
import Tkinter as tk
class Example(tk.Frame):
def __init__(self, master):
tk.Frame.__init__(self, master)
self.post_tweet = tk.Text(self)
bindtags = list(self.post_tweet.bindtags())
bindtags.insert(2, "custom") # index 1 is where most default bindings live
self.post_tweet.bindtags(tuple(bindtags))
self.post_tweet.bind_class("custom", "<Key>", self.count)
self.post_tweet.grid()
self.char_count = tk.Label(self)
self.char_count.grid()
def count(self, event):
current = len(self.post_tweet.get("1.0", "end-1c"))
remaining = 140-current
self.char_count.configure(text="%s characters remaining" % remaining)
if __name__ == "__main__":
root = tk.Tk()
Example(root).pack(side="top", fill="both", expand=True)
root.mainloop()
Like most events in Tk, your <Key> handler is fired before the event is processed by the built-in bindings, rather than after. This allows you to, for example, prevent the normal processing from happening, or change what it does.
But this means that you can't access the new value (whether via a StringVar, or just by calling entry.get()), because it hasn't been updated yet.
If you're using Text, there's a virtual event <<Modified>> that gets fired after the "modified" flag changes. Assuming you weren't using that flag for another purpose (e.g., in a text editor, you might want to use it to mean "enable the Save button"), you can use it to do exactly what you want:
def count(self, event=None):
if not self.post_tweet.edit_modified():
return
self.post_tweet.edit_modified(False)
self.x = len(self.post_tweet.get(1.0, END))
self.char_count.set(str(140 - self.x))
# ...
self.post_tweet.bind("<<Modified>>", self.count)
Usually, when you want something like this, you want an Entry rather than a Text. Which provides a much nicer way to do this: validation. As with everything beyond the basics in Tkinter, there's no way you're going to figure this out without reading the Tcl/Tk docs (which is why the Tkinter docs link to them). And really, even the Tk docs don't describe validation very well. But here's how it works:
def count(self, new_text):
self.x = len(new_text)
self.char_count.set(str(140 - self.x))
return True
# ...
self.vcmd = self.master.register(self.count)
self.post_tweet = Edit(self.master, validate='key',
validatecommand=(self.vcmd, '%P'))
The validatecommand can take a list of 0 or more arguments to pass to the function. The %P argument gets the new value the entry will have if you allow it. See VALIDATION in the Entry manpage for more details.
If you want the entry to be rejected (e.g., if you want to actually block someone from entering more than 140 characters), just return False instead of True.
By the way, it's worth looking over the Tk wiki and searching for Tkinter recipes on ActiveState. It's a good bet someone's got wrappers around Text and Entry that hide all the extra stuff you have to do to make these solutions (or others) work so you just have to write the appropriate count method. There might even be a Text wrapper that adds Entry-style validation.
There are a few other ways you could do this, but they all have downsides.
Add a trace to hook all writes to a StringVar attached to your widget. This will get fired by any writes to the variable. I guarantee that you will get the infinite-recursive-loop problem the first time you try to use it for validation, and then you'll run into other more subtle problems in the future. The usual solution is to create a sentinel flag, which you check every time you come into the handler to make sure you're not doing it recursively, and then set while you're doing anything that can trigger a recursive event. (That wasn't necessary for the edit_modified example above because we could just ignore anyone setting the flag to False, and we only set it to False, so there's no danger of infinite recursion.)
You can get the new char (or multi-char string) out of the <Key> virtual event. But then, what do you do with it? You need to know where it's going to be added, which character(s) it's going to be overwriting, etc. If you don't do all the work to simulate Entry—or, worse, Text—editing yourself, this is no better than just doing len(entry.get()) + 1.

Set entry tab value in Tkinter [duplicate]

I have been searching for a way to set the tab order in a tkinter application, that I have been working on. Currently the default order seems to be working from top-down, but it requires using CTRL + Tab to cycle through the controls.
Is there any way to customize the order and, more so, change the CTRL + Tab to just Tab?
Tab order is based on the stacking order, which in turn defaults to the order that widgets are created. You can adjust the stacking order (and thus the tab order) using the methods tkraise (or lift) and lower.
This should be working out of the box for you without the need to press CTRL + Tab. Be aware, however, that tab inserts a literal tab in text widgets rather than moving focus to another control. That default behavior can be changed of course.
Here's an example showing how to reverse the tab order. When running the example, pressing tab in the first entry should take you to the last. Pressing tab again takes you to the second, then the first, lather, rinse, repeat
Note that the native Tk commands are raise and lower, but since raise is a reserved word in Python it had to be renamed in tkinter.
import Tkinter as tk
class SampleApp(tk.Tk):
def __init__(self, *args, **kwargs):
tk.Tk.__init__(self, *args, **kwargs)
e1 = tk.Entry(self)
e2 = tk.Entry(self)
e3 = tk.Entry(self)
e1.insert(0,"1")
e2.insert(0,"2")
e3.insert(0,"3")
e1.pack()
e2.pack()
e3.pack()
# reverse the stacking order to show how
# it affects tab order
new_order = (e3, e2, e1)
for widget in new_order:
widget.lift()
if __name__ == "__main__":
app = SampleApp()
app.mainloop()
Since you mention you have to do CTRL + Tab, I'm guessing you're trying to have the tab key change focus from a text widget. Normally a tab key inserts a literal tab. If you want it to change the focus, just add a binding to the <Tab> event.
Tkinter has a function that will return the name of the next widget that should get focus. Unfortunately, for older versions of Tkinter that function is buggy. However, it's easy to work around that. Here's a couple of methods you can add to the above code:
def _focusNext(self, widget):
'''Return the next widget in tab order'''
widget = self.tk.call('tk_focusNext', widget._w)
if not widget: return None
return self.nametowidget(widget.string)
def OnTextTab(self, event):
'''Move focus to next widget'''
widget = event.widget
next = self._focusNext(widget)
next.focus()
return "break"
I've been searching for ways to skip some widgets while tabbing and found in tkinter's tk_focusNext function description the following: "A widget is omitted if it has the takefocus resource set to 0."
you can set takefocus on widget initialization as an argument.

Readonly tkinter text widget

I want to use tkinter text widget as a readonly widget. It should act as a transcript area. My idea is to keep this transcript in a file and whenever the user writes anything, just remove all the contents of the widget, and rewrite it again.
The code will look like:
transcript_entry = SimpleEditor() # SimpleEditor is inherited from ScrolledText
transcript_entry.text.delete("1.0", END)
# this is just a test string, it should be the contents of the transcript file
transcript_entry.text.insert("1.0", "This is test transcript")
transcript_entry.text.bind("<KeyPress>", transcript_entry.readonly)
And readonly function will look like:
def readonly(self, event):
self.text.delete("1.0", END)
# this is just a test string, it should be the contents of the transcript file
self.text.insert("1.0", "This is test transcript")
The bug here is that the last character entered by the user is added to the transcript. I suspect the reason is that the readonly function is called, then the user input is wrote to the widget. How to reverse this order & let the readonly function be called after the user input is wrote to the widget?
Any hints?
The reason that the last character is inserted is because the default bindings (which causes the insert) happens after custom bindings you put on the widget. So your bindings fire first and then the default binding inserts the characters. There are other questions and answers here that discuss this in more depth. For example, see https://stackoverflow.com/a/11542200/
However, there is a better way to accomplish what you are trying to do. If you want to create a readonly text widget, you can set the state attribute to "disabled". This will prevent all inserts and deletes (and means you need to revert the state whenever you want to programmatically enter data).
On some platforms it will seem like you can't highlight and copy text, but that is only because the widget won't by default get focus on a mouse click. By adding a binding to set the focus, the user can highlight and copy text but they won't be able to cut or insert.
Here's an example using python 2.x; for 3.x you just have to change the imports:
import Tkinter as tk
from ScrolledText import ScrolledText
class Example(tk.Frame):
def __init__(self, parent):
tk.Frame.__init__(self, parent)
t = ScrolledText(self, wrap="word")
t.insert("end", "Hello\nworld")
t.configure(state="disabled")
t.pack(side="top", fill="both", expand=True)
# make sure the widget gets focus when clicked
# on, to enable highlighting and copying to the
# clipboard.
t.bind("<1>", lambda event: t.focus_set())
if __name__ == "__main__":
root = tk.Tk()
Example(root).pack(fill="both", expand=True)
root.mainloop()
Please do not delete and reinsert your text :
It is huge performance issue.
It will remove any tags and marks set on the text
This will be visible to the user, and users don't like flickering interfaces
This is not necessary, Tkinter is customizable enough to just not allow the user change the content.
The best way I found to create a read only Text is to disable all the bindings leading to a text change.
My solution is to create a new Widget binding map containing only "read only commands". Then, just reconfigure your widget to use the new RO binding map instead of the default one :
from Tkinter import *
# This is the list of all default command in the "Text" tag that modify the text
commandsToRemove = (
"<Control-Key-h>",
"<Meta-Key-Delete>",
"<Meta-Key-BackSpace>",
"<Meta-Key-d>",
"<Meta-Key-b>",
"<<Redo>>",
"<<Undo>>",
"<Control-Key-t>",
"<Control-Key-o>",
"<Control-Key-k>",
"<Control-Key-d>",
"<Key>",
"<Key-Insert>",
"<<PasteSelection>>",
"<<Clear>>",
"<<Paste>>",
"<<Cut>>",
"<Key-BackSpace>",
"<Key-Delete>",
"<Key-Return>",
"<Control-Key-i>",
"<Key-Tab>",
"<Shift-Key-Tab>"
)
class ROText(Text):
tagInit = False
def init_tag(self):
"""
Just go through all binding for the Text widget.
If the command is allowed, recopy it in the ROText binding table.
"""
for key in self.bind_class("Text"):
if key not in commandsToRemove:
command = self.bind_class("Text", key)
self.bind_class("ROText", key, command)
ROText.tagInit = True
def __init__(self, *args, **kwords):
Text.__init__(self, *args, **kwords)
if not ROText.tagInit:
self.init_tag()
# Create a new binding table list, replace the default Text binding table by the ROText one
bindTags = tuple(tag if tag!="Text" else "ROText" for tag in self.bindtags())
self.bindtags(bindTags)
text = ROText()
text.insert("1.0", """A long text with several
lines
in it""")
text.pack()
text.mainloop()
Note that just the bindings are changed. All the Text command (as insert, delete, ...) are still usable.
I recently worked a different, slightly simpler solution. Rather than changing all the bindings, one can add a function to delete all input characters as soon as they are written:
def read_only(self, event):
if event.char is not '': # delete only if the key pressed
# corresponds to an actual character
self.text.delete('insert-1c')
and just bind it to any event:
root.bind('<Key>', self.read_only)

Tkinter: set StringVar after <Key> event, including the key pressed

Every time a character is entered into a Text widget, I want to get the contents of that widget and subtract its length from a certain number (basically a "you have x characters left" deal).
But the StringVar() is always one event behind. This is, from what I gather, because the event is processed before the character is entered into the Text widget. This means that if I have 3 characters in the field and I enter a 4th, the StringVar is updated but is still 3 characters long, then it updates to 4 when I enter a 5th character.
Is there a way to keep the two in line?
Here's some code. I removed irrelevant parts.
def __init__(self, master):
self.char_count = StringVar()
self.char_count.set("140 chars left")
self.post_tweet = Text(self.master)
self.post_tweet.bind("<Key>", self.count)
self.post_tweet.grid(...)
self.char_count = Label(self.master, textvariable=self.foo)
self.char_count.grid(...)
def count(self):
self.x = len(self.post_tweet.get(1.0, END))
self.char_count.set(str(140 - self.x))
A simple solution is to add a new bindtag after the class binding. That way the class binding will fire before your binding. See this answer to the question How to bind self events in Tkinter Text widget after it will binded by Text widget? for an example. That answer uses an entry widget rather than a text widget, but the concept of bindtags is identical between those two widgets. Just be sure to use Text rather than Entry where appropriate.
Another solution is to bind on KeyRelease, since the default bindings happen on KeyPress.
Here's an example showing how to do it with bindtags:
import Tkinter as tk
class Example(tk.Frame):
def __init__(self, master):
tk.Frame.__init__(self, master)
self.post_tweet = tk.Text(self)
bindtags = list(self.post_tweet.bindtags())
bindtags.insert(2, "custom") # index 1 is where most default bindings live
self.post_tweet.bindtags(tuple(bindtags))
self.post_tweet.bind_class("custom", "<Key>", self.count)
self.post_tweet.grid()
self.char_count = tk.Label(self)
self.char_count.grid()
def count(self, event):
current = len(self.post_tweet.get("1.0", "end-1c"))
remaining = 140-current
self.char_count.configure(text="%s characters remaining" % remaining)
if __name__ == "__main__":
root = tk.Tk()
Example(root).pack(side="top", fill="both", expand=True)
root.mainloop()
Like most events in Tk, your <Key> handler is fired before the event is processed by the built-in bindings, rather than after. This allows you to, for example, prevent the normal processing from happening, or change what it does.
But this means that you can't access the new value (whether via a StringVar, or just by calling entry.get()), because it hasn't been updated yet.
If you're using Text, there's a virtual event <<Modified>> that gets fired after the "modified" flag changes. Assuming you weren't using that flag for another purpose (e.g., in a text editor, you might want to use it to mean "enable the Save button"), you can use it to do exactly what you want:
def count(self, event=None):
if not self.post_tweet.edit_modified():
return
self.post_tweet.edit_modified(False)
self.x = len(self.post_tweet.get(1.0, END))
self.char_count.set(str(140 - self.x))
# ...
self.post_tweet.bind("<<Modified>>", self.count)
Usually, when you want something like this, you want an Entry rather than a Text. Which provides a much nicer way to do this: validation. As with everything beyond the basics in Tkinter, there's no way you're going to figure this out without reading the Tcl/Tk docs (which is why the Tkinter docs link to them). And really, even the Tk docs don't describe validation very well. But here's how it works:
def count(self, new_text):
self.x = len(new_text)
self.char_count.set(str(140 - self.x))
return True
# ...
self.vcmd = self.master.register(self.count)
self.post_tweet = Edit(self.master, validate='key',
validatecommand=(self.vcmd, '%P'))
The validatecommand can take a list of 0 or more arguments to pass to the function. The %P argument gets the new value the entry will have if you allow it. See VALIDATION in the Entry manpage for more details.
If you want the entry to be rejected (e.g., if you want to actually block someone from entering more than 140 characters), just return False instead of True.
By the way, it's worth looking over the Tk wiki and searching for Tkinter recipes on ActiveState. It's a good bet someone's got wrappers around Text and Entry that hide all the extra stuff you have to do to make these solutions (or others) work so you just have to write the appropriate count method. There might even be a Text wrapper that adds Entry-style validation.
There are a few other ways you could do this, but they all have downsides.
Add a trace to hook all writes to a StringVar attached to your widget. This will get fired by any writes to the variable. I guarantee that you will get the infinite-recursive-loop problem the first time you try to use it for validation, and then you'll run into other more subtle problems in the future. The usual solution is to create a sentinel flag, which you check every time you come into the handler to make sure you're not doing it recursively, and then set while you're doing anything that can trigger a recursive event. (That wasn't necessary for the edit_modified example above because we could just ignore anyone setting the flag to False, and we only set it to False, so there's no danger of infinite recursion.)
You can get the new char (or multi-char string) out of the <Key> virtual event. But then, what do you do with it? You need to know where it's going to be added, which character(s) it's going to be overwriting, etc. If you don't do all the work to simulate Entry—or, worse, Text—editing yourself, this is no better than just doing len(entry.get()) + 1.

Categories