I'm building a desktop application that lets you insert some data into a form and then the data is displayed in a series (3) of Treeview widgets.
This is the form that I'm using to enter new data:
It's in a Toplevel widget. When the Add button is pressed the new data is stored in a file and it also should insert the new data in the corresponding Treeview Widget.
This is the root window:
It's comprised of 3 Treeview widgets. The purpose of the application is to give the user the opportunity to sort candidates into the right Treeview widget.
The issue that I'm facing is that when the Add button is pressed the new data is not shown in the Treeview widget and no errors are given. I think it may be an issue of class instantiation. This is an excerpt from my app, please see below a Minimal, Complete and Verifiable example
class MainApp(tk.Tk):
def __init__(self, *args, **kwargs):
...
# frame and menu classes are instantiated here
self.FrameList = {ViableCandidates: ViableCandidates(self),
NotViableCandidates: NotViableCandidates(self),
InProgressCandidates: InProgressCandidates(self)}
...
def InstanceLinker(self, frame):
link = self.FrameList[frame]
return link
class GUIMenu(tk.Menu):
def __init__(self, parent):
...
# menu code is here
addcandidates.add_command(label='Quick Add', command=lambda: QuickAdd(parent))
class QuickAdd(tk.Toplevel):
def __init__(self, parent):
...
# code for the small Toplevel window
...
# this is the code that I use to add the new item to Treeview when the Add button is pressed
if CandidateInfo['status'] == 'Viable':
app.InstanceLinker(ViableCandidates).AddtoList()
elif CandidateInfo['status'] == 'Not Viable':
app.InstanceLinker(NotViableCandidates).AddtoList()
else:
app.InstanceLinker(InProgressCandidates).AddtoList()
# ViableCandidates, NotViableCandidates, InProgressCandidates are created with the same pattern
class InProgressCandidates(tk.Frame):
def __init__(self, parent):
global Counter
tk.Frame.__init__(self, parent)
self.columnconfigure(0, weight=1)
self.rowconfigure(1, weight=1)
title = tk.Label(self, text="Candidates In Progress", font="Verdana 10 bold")
title.grid(row=0, column=0, sticky='nesw')
self.tree = ttk.Treeview(self)
self.tree.grid(row=1, column=0, sticky='nesw')
scrollbar = ttk.Scrollbar(self, orient='vertical', command=self.tree.yview)
scrollbar.grid(row=1, column=1, sticky='nws')
self.tree.config(columns=('Name', 'Date'), selectmode='browse', height=20, yscrollcommand=scrollbar.set)
self.tree.column('#0', width=20, minwidth=10, stretch=tk.YES)
self.tree.column('Name', width=150, minwidth=10, stretch=tk.YES)
self.tree.column('Date', width=80, minwidth=10, stretch=tk.YES)
self.tree.heading('#0', text='#', anchor=tk.W)
self.tree.heading('Name', text='Name', anchor=tk.W)
self.tree.heading('Date', text='Date', anchor=tk.W)
if Counter < 4:
Counter += 1
self.PopulateList()
def PopulateList(self):
selection = Database().SelectFromDB('name, date', "status = 'In progress'")
for i in range(len(selection)):
name = list(selection[i])[0]
date = adjusttotimezone(list(selection[i])[1])
self.tree.insert("", i, name, text=i + 1)
self.tree.set(name, 'Name', name)
self.tree.set(name, 'Date', date)
CandidateCounter['InProgressCandidates'] = i
def AddtoList(self):
CandidateCounter['InProgressCandidates'] += 1
print('I was here')
self.tree.insert("", CandidateCounter['InProgressCandidates'], CandidateInfo['name'],
text=CandidateCounter['InProgressCandidates'])
self.tree.set(CandidateInfo['name'], 'Name', CandidateInfo['name'])
selection = Database().SelectFromDB('date', "name = '" + CandidateInfo['name'] + "'")
date = adjusttotimezone(list(selection[0])[0])
self.tree.set(CandidateInfo['name'], 'Date', date)
app = MainApp()
app.mainloop()
When the "Add" button is pressed there are no errors and "I was here" is printed so the AddtoList method is instantiated, but there are no new items added to Treeview. I did check if the variables that I'm using to create the new Treeview item hold the correct data and they do.
EDIT: This is a Minimal, Complete and Verifiable example:
import tkinter as tk
from tkinter import ttk
Bigbadtext = ''
Counter = 0
class MainApp(tk.Tk):
def __init__(self, *args, **kwargs):
self.MainWindow = tk.Tk.__init__(self, *args, **kwargs)
menu = GUIMenu(self)
self.config(menu=menu)
frame = InProgressCandidates(self)
frame.grid(row=0, column=1, sticky='nesw')
self.FrameList = {InProgressCandidates:InProgressCandidates(self)}
def InstanceLinker(self, frame):
link = self.FrameList[frame]
return link
class GUIMenu(tk.Menu):
def __init__(self, parent):
tk.Menu.__init__(self, parent)
addcandidates = tk.Menu(self, tearoff=0)
self.add_cascade(label='Add Candidates', menu=addcandidates)
addcandidates.add_command(label='Quick Add', command=lambda: QuickAdd(parent))
class QuickAdd(tk.Toplevel):
def __init__(self, parent):
tk.Toplevel.__init__(self, parent)
saysomething = tk.Entry(self)
saysomething.grid(row=1, column=0)
def addbutton():
global Bigbadtext
Bigbadtext = saysomething.get()
app.InstanceLinker(InProgressCandidates).AddtoList()
okbutton = ttk.Button(self, text='Add', command=addbutton)
okbutton.grid(row=2, column=0)
class InProgressCandidates(tk.Frame):
def __init__(self, parent):
tk.Frame.__init__(self, parent)
self.tree = ttk.Treeview(self)
self.tree.grid(row=1, column=0, sticky='nesw')
scrollbar = ttk.Scrollbar(self, orient='vertical', command=self.tree.yview)
scrollbar.grid(row=1, column=1, sticky='nws')
self.tree.config(columns='something', selectmode='browse', height=20, yscrollcommand=scrollbar.set)
self.tree.column('#0', width=20, minwidth=10, stretch=tk.YES)
self.tree.column('something', width=150, minwidth=10, stretch=tk.YES)
self.tree.heading('#0', text='#', anchor=tk.W)
self.tree.heading('something', text='Say something', anchor=tk.W)
def AddtoList(self):
global Counter
Counter += 1
print('I was here')
self.tree.insert("", Counter, Bigbadtext, text=Counter)
self.tree.set(Bigbadtext, 'something', Bigbadtext)
app = MainApp()
app.mainloop()
The problem is that you are creating two treeview widgets, and then adding items to the one that is invisible.
You create one here:
frame = InProgressCandidates(self)
Then you create another one here:
self.FrameList = {InProgressCandidates:InProgressCandidates(self)}
Since you've already created one, the one you created should be what goes in self.FrameList:
self.FrameList = {InProgressCandidates:frame}
It is not really an answer but I up voted the question because it solved me a problem. I wanted to add items to the widget but did not want to show it to the user until I finished to populate the tree. But each insert showed right away. Now I create 2 identical widgets, one visible and the other is not, and once it is populated I change between them. Thus even a mistake can have a benefit.
Related
I'm having an update problem in tkinter GUI, for past two days, I have searched a lot, Cant find something specific to my problem.This post Tkinter updating labels in stacked frame windows come close to my problem but not exactly. I am using classes to structure my application... The structure is given here Application structure image ( SOF not letting me embed images but link is provided )
From above structure you can see, I'm trying to make changes in DetailFrame from ListProduct Frame, now the code is reaching there and changing the values successfully but not updating the label, I'm using config method to change label... and frame background,but no luck..
I have tried StringVar as well for updating label, but nothing... Sample Code is provided below...
This application is a part of main app and for Original Code Structure Thanks to .. Bryan Oakley
class ProductWindow(tk.Tk):
def __init__(self):
tk.Tk.__init__(self)
self.geometry("600x500")
container = tk.Frame(self)
container.pack(side='top', fill='both', expand=True)
container.grid_rowconfigure(0, weight=1)
container.grid_columnconfigure(0, weight=1)
self.notebook = ttk.Notebook(container)
self.notebook.grid(row=0, column=0, sticky='nsew')
self.new_product_tab = tk.Frame(self.notebook, bg='#233223' )
self.list_product_tab = tk.Frame(self.notebook, bg='#323232')
self.edit_product_tab = tk.Frame(self.notebook, bg='#433434')
# Adding Tabs to Notebook
self.notebook.add(self.new_product_tab, text=" Add New Product ")
self.notebook.add(self.list_product_tab, text=" List All Product ")
self.notebook.add(self.edit_product_tab, text=" Edit Product ")
self.productframe = EditProductFrame(self.edit_product_tab)
self.detailframe = DetailFrame(self.productframe)
button = tk.Button(self.list_product_tab, text="Change background in Edit Form", command=self.change_method)
button.pack()
def change_method(self):
print("Trying to change the frame")
self.productframe.raise_edit_frame(DetailFrame)
self.detailframe.change_bg('green')
self.notebook.select(self.edit_product_tab)
if __name__ == "__main__":
testObj = ProductWindow()
testObj.mainloop()
In another file, I have DetailFrame below.
class EditProductFrame(tk.Frame):
def __init__(self, parent):
print("Edit product frame constructor is called...")
tk.Frame.__init__(self, parent)
self.pack(side='top', fill='both', expand=True)
self.grid_rowconfigure(0, weight=1)
self.grid_columnconfigure(0, weight=1)
# define frames and pack them in
self.frames = {}
for F in {DetailFrame, EditFrame}:
frame = F(self)
self.frames[F] = frame
frame.grid(row=0, column=0, sticky='nsew')
self.raise_edit_frame(DetailFrame)
def raise_edit_frame(self, container):
frame = self.frames[container]
frame.tkraise()
class EditFrame(tk.Frame):
def __init__(self, parent):
tk.Frame.__init__(self, parent)
self.config(bg='green')
label = tk.Label(self, text="Edit Page",)
label.pack(pady=0,padx=0)
tk.Button(self, text="Go to Detail", command=lambda:parent.raise_edit_frame(DetailFrame)).pack()
class DetailFrame(tk.Frame):
def __init__(self, parent):
print("something detail view")
tk.Frame.__init__(self, parent)
self.config(bg='blue')
self.label = tk.Label(self, text='Original Label')
self.label.pack(pady=0,padx=0)
tk.Button(self, text="Go to Edit Frame", command=lambda:parent.raise_edit_frame(EditFrame)).pack()
def change_bg(self, color):
# doesn't update the background
self.config(bg=color)
# doesn't update the Label text
self.label.config(text='Changed Label')
# print the correct changed value = 'Changed Label'
print(self.label.cget('text'))
Thanks ...
Note that you have created another instance of DetailFrame (self.detailframe) inside ProductWindow but it is not visible since no layout function is called on it. Actually there is already an instance of DetailFrame created when creating instance of EditProductFrame, so you need to call change_bg() on this instance instead:
class ProductWindow(tk.Tk):
def __init__(self):
tk.Tk.__init__(self)
self.geometry("600x500")
container = tk.Frame(self)
container.pack(side='top', fill='both', expand=True)
container.grid_rowconfigure(0, weight=1)
container.grid_columnconfigure(0, weight=1)
self.notebook = ttk.Notebook(container)
self.notebook.grid(row=0, column=0, sticky='nsew')
self.new_product_tab = tk.Frame(self.notebook, bg='#233223' )
self.list_product_tab = tk.Frame(self.notebook, bg='#323232')
self.edit_product_tab = tk.Frame(self.notebook, bg='#433434')
# Adding Tabs to Notebook
self.notebook.add(self.new_product_tab, text=" Add New Product ")
self.notebook.add(self.list_product_tab, text=" List All Product ")
self.notebook.add(self.edit_product_tab, text=" Edit Product ")
# -- there is an instance of DetailFrame created inside EditProductFrame
self.productframe = EditProductFrame(self.edit_product_tab)
# -- so don't create another instance of DetailFrame
#self.detailframe = DetailFrame(self.productframe)
button = tk.Button(self.list_product_tab, text="Change background in Edit Form", command=self.change_method)
button.pack()
def change_method(self):
print("Trying to change the frame")
self.productframe.raise_edit_frame(DetailFrame)
#self.detailframe.change_bg('green')
# -- call change_bg() on the instance of DetailFrame inside EditProductFrame
self.productframe.frames[DetailFrame].change_bg('green')
self.notebook.select(self.edit_product_tab)
Another option is to make self.detailframe the reference to the instance of EditFrame inside EditProductFrame:
self.detailframe = self.productframe.frames[DetailFrame]
I'm trying to create new tabs in my navigation bar notebook by clicking on the last tab. To further complicate the task my application is written with classes. My current and less elegant solution requires an Entry with a Button to create the new tab with the title entered in the Entry widget. Does anyone know a more elegant solution to my problem?
Heres my code:
import tkinter as tk
from tkinter import ttk
class MainApp(tk.Tk):
"""Main window class"""
def __init__(self):
super(MainApp, self).__init__()
self.geometry("1000x1000")
self.main_window = tk.Frame(self)
self.main_window.pack(side="top", fill="both", expand=True)
# saving all tabs & Frames to a dictionary to be able to access them later
self.frames = {}
self.tabs = {}
# create a tab bar
self.navbar = Navbar(self.main_window)
# set the layout
self.set_layout()
def set_layout(self):
"""creates the default app layout"""
self.add_tab("Settings") # first page
self.add_tab("Case 1")
def add_tab(self, title):
"""adds a new tab with name title to the navbar"""
tab = ttk.Frame(self)
self.navbar.add(tab, text=title)
self.tabs[title] = tab
# check if Settings or Case type Tab, to create MainInput frame with correct buttons
if title.lower().find("setting") != -1:
self.frames[title] = MainInput(self, tab, self.navbar, settings=True)
else:
self.frames[title] = MainInput(self, tab, self.navbar, case=True)
self.navbar.pack(fill=tk.BOTH, expand=tk.YES)
for tab in self.tabs:
self.frames[tab].grid(sticky="nesw")
def add_frame(self, title, frame):
"""adds the frame to frames dict with key title"""
self.frames[title] = frame
class Navbar(ttk.Notebook):
"""returns a Notebook"""
def __init__(self, parent):
ttk.Notebook.__init__(self, parent)
#staticmethod
def delete(tab):
"""delete current tab"""
tab.forget(tab.select())
class MainInput(tk.Frame):
"""The base frame of every tab"""
def __init__(self, root, parent, notebook, settings=False, case=False):
tk.Frame.__init__(self, parent)
# Either build a settings or testcase tab
if settings is True:
SettingsField(root, parent)
if case is True:
CaseGeneral(parent, notebook)
class SettingsField(tk.Frame):
"""Creates a settings tab"""
def __init__(self, root, parent):
tk.Frame.__init__(self, parent)
# add the "new tab" name entry and button
tk.Label(parent, text="Add new Testcase:").grid(row=0, column=0, columnspan=2, sticky="w")
tk.Label(parent, text="Name:").grid(row=1, column=0, sticky="w", padx=20)
new_tab_title = tk.Entry(parent, textvariable=tk.StringVar(), width=30)
new_tab_title.grid(row=1, column=1, columnspan=2, sticky="w")
add_button = tk.Button(parent, text="add", command=lambda: [root.add_tab(new_tab_title.get())])
add_button.grid(row=1, column=3, sticky="w", padx=5)
class CaseGeneral(tk.Frame):
def __init__(self, parent, notebook):
tk.Frame.__init__(self, parent)
# create "delete current tab" buton
tk.Label(parent, text="delete Testcase").grid(row=0, column=0)
delete_button = tk.Button(parent, text="delete", command=lambda: [Navbar.delete(notebook)])
delete_button.grid(row=0, column=1, sticky="w")
Images of the running code:
Settings Tab
Cases
Note: Please don't roast me since this is my first post here :)
A really simple solution would be to create a binding on <<NotebookTabChanged>> that creates a new tab whenever the last tab on the row is selected. You can then create a tab with a "+" as the label and make sure it's the last tab.
Here's a simple example of the technique. It gives the new tab a label of <untitled>. Every time you click on the last tab it creates a new tab immediately before the last tab and then selects it.
import tkinter as tk
from tkinter import ttk
def handleTabChange(event):
if notebook.select() == notebook.tabs()[-1]:
index = len(notebook.tabs())-1
frame = tk.Frame(notebook)
notebook.insert(index, frame, text="<untitled>")
notebook.select(index)
root = tk.Tk()
notebook = ttk.Notebook(root)
notebook.bind("<<NotebookTabChanged>>", handleTabChange)
notebook.pack(fill="both", expand=True)
# add a tab that creates new tabs when selected
frame = tk.Frame()
notebook.add(frame, text="+")
root.mainloop()
I want to create a window that allows entering one-to-many fields for the file transfer.
I created a Scrollable Frame and I am adding Entry-Text pairs in runtime. If I click the button for the first time, everything goes well. After the second time, nothing happens on the UI side. It works perfectly after the second click. But I saw that all pairs added successfully, just the UI did not display it. Does anybody know how to fix it?
import tkinter as tk
class VerticalScroolFrame(tk.Frame):
"""A frame with a vertical scroolbar"""
def __init__(self, master, *args, **kwargs):
main_frame = tk.Frame(master)
main_frame.grid()
main_frame.rowconfigure(0, weight=1)
main_frame.columnconfigure(0, weight=1)
self._canvas = tk.Canvas(main_frame)
self._canvas.grid(row=0, column=0, sticky=tk.NSEW)
scrollbar = tk.Scrollbar(main_frame, command=self._canvas.yview)
scrollbar.grid(row=0, column=1, sticky=tk.N+tk.S+tk.W)
self._canvas.configure(yscrollcommand=scrollbar.set)
self._canvas.bind('<Configure>', lambda *_: self.on_configure())
super().__init__(self._canvas, *args, **kwargs)
self._canvas.create_window((0, 0), window=self, anchor=tk.NW)
def on_configure(self):
"""update scrollregion after starting 'mainloop'
when all widgets are in self._canvas. And also need to be triggered
whenever a widget added as a child.
"""
self._canvas.configure(scrollregion=self._canvas.bbox('all'))
class AdvancedTransfer:
"""Opens a window that allows you to enter source file list
and targets for them. One-to-many relation.
"""
def __init__(self, root):
self._scroolable_frame = VerticalScroolFrame(root)
self._entry_text_dict = {}
self._button = tk.Button(root, text="Add", command=self.add_item)
self._button.grid()
def add_item(self):
"""Add entry-text widget group"""
row = len(self._entry_text_dict)
entry = tk.Entry(self._scroolable_frame)
entry.insert(0, "row number: {0}".format(row))
entry.grid(row=row, column=0)
text = tk.Text(self._scroolable_frame)
text.grid(row=row, column=1)
self._entry_text_dict[entry] = text
self._scroolable_frame.on_configure()
def main():
root = tk.Tk()
main_frame = tk.Frame(root)
main_frame.grid()
AdvancedTransfer(main_frame)
root.mainloop()
if __name__ == "__main__":
main()
Repro steps:
Run the code below.
Click the button two times.
You should see 2 pairs but only 1 pair shown instead.
It is because you bind <Configure> event on wrong widget. You should bind on the internal frame (i.e. instance of VerticalScroolFrame) instead of canvas (self._canvas):
class VerticalScroolFrame(tk.Frame):
"""A frame with a vertical scroolbar"""
def __init__(self, master, *args, **kwargs):
main_frame = tk.Frame(master)
main_frame.grid()
main_frame.rowconfigure(0, weight=1)
main_frame.columnconfigure(0, weight=1)
self._canvas = tk.Canvas(main_frame)
self._canvas.grid(row=0, column=0, sticky=tk.NSEW)
scrollbar = tk.Scrollbar(main_frame, command=self._canvas.yview)
scrollbar.grid(row=0, column=1, sticky=tk.N+tk.S+tk.W)
self._canvas.configure(yscrollcommand=scrollbar.set)
#self._canvas.bind('<Configure>', lambda *_: self.on_configure())
super().__init__(self._canvas, *args, **kwargs)
self._canvas.create_window((0, 0), window=self, anchor=tk.NW)
# bind <Configure> event on itself
self.bind('<Configure>', lambda _: self.on_configure())
I am trying to do a dynamic search function, where the user also can select multiple items in a list. If we consider this example (I added this part, selectmode=MULTIPLE):
from Tkinter import *
# First create application class
class Application(Frame):
def __init__(self, master=None):
Frame.__init__(self, master)
self.pack()
self.create_widgets()
# Create main GUI window
def create_widgets(self):
self.search_var = StringVar()
self.search_var.trace("w", self.update_list)
self.entry = Entry(self, textvariable=self.search_var, width=13)
self.lbox = Listbox(self,selectmode=MULTIPLE, width=45, height=15)
self.entry.grid(row=0, column=0, padx=10, pady=3)
self.lbox.grid(row=1, column=0, padx=10, pady=3)
self.btn = ttk.Button(self, text="Select", command=self.Select)
self.btn.grid(column=1, row=1)
# Function for updating the list/doing the search.
# It needs to be called here to populate the listbox.
self.update_list()
def update_list(self, *args):
search_term = self.search_var.get()
# Just a generic list to populate the listbox
lbox_list = ['Adam', 'Lucy', 'Barry', 'Bob',
'James', 'Frank', 'Susan', 'Amanda', 'Christie']
self.lbox.delete(0, END)
for item in lbox_list:
if search_term.lower() in item.lower():
self.lbox.insert(END, item)
def Select(self):
reslist = list()
selecion = self.lbox.curselection()
for i in selecion:
entered = self.lbox.get(i)
reslist.append(entered)
print reslist
root = Tk()
root.title('Filter Listbox Test')
app = Application(master=root)
print 'Starting mainloop()'
app.mainloop()
The search function works perfectly fine, however, once a search has been done and an item has been selected, the selections is not saved since the lbox.delete function is used in update_list. Is there a way to keep each item selected while using the search function?
This is what I came up with. I don't know if you need it anymore but I'll post it anyway.
I've basically added code to set values as selected if they are in user selection list after the listbox is refreshed on a change in Entry widget, and to remove it from user selection if user deselects irrespective of what is currently in the listbox.
from tkinter import *
sel=list()
# First create application class
class Application(Frame):
def __init__(self, master=None):
Frame.__init__(self, master)
self.pack()
self.create_widgets()
def CurSelet(self,evt):
global sel
temp=list()
for i in self.lbox.curselection():
temp.append(self.lbox.get(i))
allitems=list()
for i in range(self.lbox.size()):
allitems.append(self.lbox.get(i))
for i in sel:
if i in allitems:
if i not in temp:
sel.remove(i)
for x in self.lbox.curselection():
if self.lbox.get(x) not in sel:
sel.append(self.lbox.get(x))
def select(self):
global sel
s=', '.join(map(str,sel))
self.cursel.set('Current Selection: '+s)
# Create main GUI window
def create_widgets(self):
self.search_var = StringVar()
self.search_var.trace("w", lambda name, index, mode: self.update_list())
self.entry = Entry(self, textvariable=self.search_var, width=13)
self.lbox = Listbox(self, selectmode=MULTIPLE,width=45, height=15)
self.lbox.bind('<<ListboxSelect>>',self.CurSelet)
self.entry.grid(row=0, column=0, padx=10, pady=3)
self.lbox.grid(row=1, column=0, padx=10, pady=3)
self.btn=Button(self,text='Okay', command=self.select, width=20)
self.btn.grid(row=2,column=0, padx=10, pady=3)
self.cursel=StringVar()
self.lb1=Label(self,textvariable=self.cursel)
self.lb1.grid(row=3,column=0,padx=10,pady=3)
# Function for updating the list/doing the search.
# It needs to be called here to populate the listbox.
self.update_list()
def update_list(self):
global sel
global l
search_term = self.search_var.get()
# Just a generic list to populate the listbox
lbox_list = ['Adam', 'Lucy', 'Barry', 'Bob',
'James', 'Frank', 'Susan', 'Amanda', 'Christie']
self.lbox.delete(0, END)
for item in lbox_list:
if search_term.lower() in item.lower():
self.lbox.insert(END, item)
allitems=list()
for i in range(self.lbox.size()):
allitems.append(self.lbox.get(i))
for i in sel:
if i in allitems:
self.lbox.select_set(self.lbox.get(0, "end").index(i))
root = Tk()
root.title('Filter Listbox Test')
Label(root, text='Search enabled').pack()
app = Application(master=root)
print('Starting mainloop()')
app.mainloop()
In python 2.7, I am trying to get a callback every time something is changed in the Tkinter Text widget.
The program uses multiple frames based on code found here: Switch between two frames in tkinter
The callback part is taken from the following example: http://code.activestate.com/recipes/464635-call-a-callback-when-a-tkintertext-is-modified/
Both codes work fine separately, but combining those two is difficult for me.
Here is my attempt with as bare bones code as possible.
import Tkinter as tk
class Texter(tk.Tk):
def __init__(self, *args, **kwargs):
tk.Tk.__init__(self, *args, **kwargs)
container = tk.Frame(self)
container.pack()
self.frames = {}
for F in (ConnectPage, EditorPage):
frame = F(container, self)
self.frames[F] = frame
frame.grid(row=0, column=0, sticky="nsew")
page_name = EditorPage.__name__
self.frames[page_name] = frame
self.show_frame(ConnectPage)
def show_frame(self, cont):
frame = self.frames[cont]
frame.tkraise()
def get_page(self, page_name):
return self.frames[page_name]
class ConnectPage(tk.Frame):
def __init__(self, parent, controller):
tk.Frame.__init__(self, parent)
button1 = tk.Button(self, text="SecondPage",
command=lambda: controller.show_frame(EditorPage))
button1.grid(row=2, column=3, padx=15)
class EditorPage(tk.Frame):
def __init__(self, parent, controller):
tk.Frame.__init__(self, parent)
self.text = tk.Text(self, height=25, width=80)
self.text.grid(column=0, row=0, sticky="nw")
button2 = tk.Button(self, text="FirstPage",
command=lambda: controller.show_frame(ConnectPage))
button2.grid(row=2, column=3, padx=15)
self.clearModifiedFlag()
self.bind_all('<<Modified>>', self._beenModified)
def _beenModified(self, event=None):
if self._resetting_modified_flag: return
self.clearModifiedFlag()
print("Hello!")
#self.beenModified(event)
def clearModifiedFlag(self):
self._resetting_modified_flag = True
try:
self.tk.call(self._w, 'edit', 'modified', 0)
finally:
self._resetting_modified_flag = False
if __name__ == '__main__':
gui = Texter()
gui.mainloop()
I tried taking only the necessary parts from the callback example.
The code does do a callback (if self.tk.call(self._w, 'edit', 'modified', 0) line is commented out) when the text is modified, but resetting the modified flag does not work, so only the first modification is registered.
At the moment I get the following error:
line 67, in clearModifiedFlag
self.tk.call(self._w, 'edit', 'modified', 0)
_tkinter.TclError: bad option "edit": must be cget or configure
In the callback example code "edit" works fine.
Edit: This is the working code
import Tkinter as tk
class Texter(tk.Tk):
def __init__(self, *args, **kwargs):
tk.Tk.__init__(self, *args, **kwargs)
container = tk.Frame(self)
container.pack()
self.frames = {}
for F in (ConnectPage, EditorPage):
frame = F(container, self)
self.frames[F] = frame
frame.grid(row=0, column=0, sticky="nsew")
page_name = EditorPage.__name__
self.frames[page_name] = frame
self.show_frame(ConnectPage)
def show_frame(self, cont):
frame = self.frames[cont]
frame.tkraise()
def get_page(self, page_name):
return self.frames[page_name]
class ConnectPage(tk.Frame):
def __init__(self, parent, controller):
tk.Frame.__init__(self, parent)
button1 = tk.Button(self, text="SecondPage",
command=lambda: controller.show_frame(EditorPage))
button1.grid(row=2, column=3, padx=15)
class EditorPage(tk.Frame):
def __init__(self, parent, controller):
tk.Frame.__init__(self, parent)
self.text = CustomText(self, height=25, width=80)
self.text.grid(column=0, row=0, sticky="nw")
self.text.bind("<<TextModified>>", self.onModification)
button2 = tk.Button(self, text="FirstPage",
command=lambda: controller.show_frame(ConnectPage))
button2.grid(row=2, column=3, padx=15)
def onModification(self, event):
print("Yellow!")
class CustomText(tk.Text):
def __init__(self, *args, **kwargs):
"""A text widget that report on internal widget commands"""
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
if __name__ == '__main__':
gui = Texter()
gui.mainloop()
I suggest a simpler approach. You can set up a proxy for the widget, and within that proxy you can detect whenever anything was inserted or deleted. You can use that information to generate a virtual event, which can be bound to like any other event.
Let's start by creating a custom text widget class, which you will use like any other text widget:
import Tkinter as tk
class CustomText(tk.Text):
def __init__(self, *args, **kwargs):
"""A text widget that report on internal widget commands"""
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
The proxy in this example does three things:
First it calls the actual widget command, passing in all of the arguments it received.
Next it generates an event for every insert and every delete
Then it then generates a virtual event
And finally it returns the results of the actual widget command
You can use this widget exactly like any other Text widget, with the added benefit that you can bind to <<TextModified>>.
For example, if you wanted to display the number of characters in the text widget you could do something like this:
root = tk.Tk()
label = tk.Label(root, anchor="w")
text = CustomText(root, width=40, height=4)
label.pack(side="bottom", fill="x")
text.pack(side="top", fill="both", expand=True)
def onModification(event):
chars = len(event.widget.get("1.0", "end-1c"))
label.configure(text="%s chars" % chars)
text.bind("<<TextModified>>", onModification)
root.mainloop()
I integrated the above <<TextModified>> example in my code and it
worked quite well, except that it was interfering with some
edit_modified() commands.
Fortunately, the tkinter Text window has a poorly documented feature
which is as good and is fully compatible with the edit_modified() get
or set commands: the predefined <<Modified>> tag. You don't even have
to create it, it works out-of-the-box.
Here are the relevant parts of my code:
The "self" prefixes were removed, some adjustments may be needed
Put that in your Text gadget code:
title = set_title(fname, numbr)
text.bind("<<Modified>>", lambda dummy: save_indicator(title))
Make sure these functions are visible:
def set_title(fname, numbr):
"Creates a window title showing the save indicator,"
"the file name and a window number"
fname = strip_path(fname)
if not fname:
fname = "(New Document)"
return "+ {} - Window no.{}".format(fname, numbr)
def strip_path(fname):
return os.path.split(fname)[-1]
def save_indicator(title, event=None):
"Update the window title"
titre = toggle_star(title)
text.winfo_toplevel().title(title)
def toggle_star(title):
"Change the first character of the title"
chr='+'; chr0='x'
if text.edit_modified():
title = chr0 + title[1:]
else:
title = chr + title[1:]
return title
Here is a complete working example with the predefined <<Modified>> tag:
def toggle_star(title):
"Change the color of the star in the title bar"
chr='+'; chr0='x'
if text.edit_modified():
title = chr0 + title[1:]
else:
title = chr + title[1:]
return title
def set_title(fname, winno):
"Put save indicator, file name and window number in the title"
if not fname:
fname = "(New Document)"
return "+ {} - Window no.{}".format(fname, winno)
def mockSave(title, event=None):
title = toggle_star(title)
root.winfo_toplevel().title(title)
text.edit_modified(0)
def read_ed_mod():
print("text.edit_modified()=", text.edit_modified())
def onModification(title, event=None):
title = toggle_star(title)
root.winfo_toplevel().title(title)
from tkinter import *
fname = 'blabla.txt'
winno = 1 ;
root = Tk()
label = Label(root, anchor="w")
text = Text(root, width=40, height=4)
label.pack(side="bottom", fill="x")
text.pack(side="top", fill="both", expand=True)
Button(root, text='Mock Save', command= lambda: mockSave(title)).pack(side=LEFT)
Button(root, text='Read ed_mod', command= lambda: read_ed_mod()).pack(side=RIGHT)
text.bind('<<Modified>>', lambda event: onModification(title))
title = set_title(fname, winno)
root.winfo_toplevel().title(title)
text.edit_modified(0)
root.mainloop()