writing a tkinter scrollbar for canvas within a class - python

I've searched around and cannot seem to find an answer for my problem. I am trying to create a working scrollbar for the following code and cannot seem to get it to work. The problem appears to be with the OnFrameConfigure method. I have seen elsewhere that the method should be def OnFrameConfigure(event):however when I place the (event) part into my method it does not work unless I write the function outside of a class
class Main(tk.Tk):
def __init__(self, *args, **kwargs):
'''This initialisation runs the whole program'''
#tk.Tk.__init__(self, *args, **kwargs)
main = tk.Tk()
canvas = tk.Canvas(main)
scroll = tk.Scrollbar(main, orient='vertical', command=canvas.yview)
canvas.configure(yscrollcommand=scroll.set)
frame = tk.Frame(canvas)
scroll.pack(side='right', fill='y')
canvas.pack(side='left', fill='both', expand='yes')
canvas.create_window((0,0), window=frame)
frame.bind('<Configure>', self.OnFrameConfigure(parent=canvas))
for i in range(100):
tk.Label(frame, text='I am a Label').pack()
main.mainloop()
def OnFrameConfigure(self, parent):
'''Used to allowed scrolled region in a canvas'''
parent.configure(scrollregion=parent.bbox('all'))

Your problem starts here:
frame.bind('<Configure>', self.OnFrameConfigure(parent=canvas))
You are immediately calling the OnFrameConfigure function. That is not how you use bind. You must give a reference to a callable function. Since you're using a class, you don't need to pass parent in, unless you have this one function work for multiple widgets.
Change the binding to this:
frame.bind('<Configure>', self.OnFrameConfigure)
Change the method definition to this:
def OnFrameConfigure(self, event):
Finally, your __init__ needs to save a reference to the canvas, which you can then use in that function:
def __init__(self, *args, **kwargs):
...
self.canvas = tk.Canvas(...)
...
...
def OnFrameConfigure(self, event):
...
self.canvas.configure(scrollregion=self.canvas.bbox('all'))
...

Related

Indicating the target of button define in class

I am green in Python, after going through books and the questions here, I tried to code the following. The following is my simplified version of my code. this is a script for GUI. In my design, there would be a "back" button on the top, and 10 page buttons below it. And when I clicked the button, it will go to another page. But I replace it by showing the string of the picture instead.
However, I found that the "back" button is at the bottom now. I believe it goes wrong on Line 12.
I tried as "PageButton=ttk.Button(self, text=self.i, command=lambda: print(self.PicLink))" as well, but all page buttons disappeared. I am totally lost on what should be set to get back my designed pattern. Although it still work funtionally, I would like to know how should I edit to meet my orignial dseign, So I can improve my coding knowledge and problem solving skill. Thanks for all your effort in advance.
import tkinter as tk
from tkinter import ttk
class PageButton(ttk.Frame):
def __init__(self, i, PicLink):
self.i = i+1
self.row=i//3+1
self.column=i%3
self.PicLink=PicLink
super().__init__()
print(self.i, self.row, self.column)
PageButton=ttk.Button(text=self.i, command=lambda: print(self.PicLink))
PageButton.grid(row=self.row, column=self.column)
class ViewerFrame(ttk.Frame):
def __init__(self, parent, *args, **kwargs):
super().__init__(parent, *args, **kwargs)
ViewerFrame.backButton=ttk.Button(self, text="Back", command=lambda: BackPage(parent))
ViewerFrame.backButton.grid(row=0, column=0)
for i in range(10):
PicLink="abc%04i.jpg" % (i+1)
globals()['Button%s' % (i+1)]=PageButton(i, PicLink)
class ViewerPage(tk.Tk):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.title("Viewer")
self.resizable(width=False, height=False)
ViewerFrame(self).grid(sticky=(tk.E+tk.W+tk.N+tk.S))
def ViewerWindow():
app=ViewerPage()
app.mainloop()
if __name__=="__main__":
ViewerWindow()
Since you do not specify the parent when creating the button inside PageButton class, it will be a child of the root window instead and put inside the root window instead of instance of PageButton.
Those page buttons are created and put into the root window before the instance of ViewerPage class which includes the Back button, so the Back button is being put after those page buttons.
So it is better to specify the parent when creating those page buttons and also PageButton should inherit from ttk.Button directly instead of creating an instance of it inside.
Below is the modified code:
import tkinter as tk
from tkinter import ttk
# inherit from ttk.Button directly
class PageButton(ttk.Button):
# added parent argument
def __init__(self, parent, i, PicLink):
self.i = i+1
self.row=i//3+1
self.column=i%3
self.PicLink=PicLink
print(self.i, self.row, self.column)
# specify the parent
super().__init__(parent, text=self.i, command=lambda: print(self.PicLink))
self.grid(row=self.row, column=self.column)
class ViewerFrame(ttk.Frame):
def __init__(self, parent, *args, **kwargs):
super().__init__(parent, *args, **kwargs)
self.backButton=ttk.Button(self, text="Back", command=lambda: BackPage(parent))
self.backButton.grid(row=0, column=0)
# use a dictionary to store those page buttons
self.buttons = {}
for i in range(10):
PicLink="abc%04i.jpg" % (i+1)
self.buttons[f'Button{i+1}'] = PageButton(self, i, PicLink) # specify the parent
class ViewerPage(tk.Tk):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.title("Viewer")
self.resizable(width=False, height=False)
ViewerFrame(self).grid(sticky=(tk.E+tk.W+tk.N+tk.S))
def ViewerWindow():
app=ViewerPage()
app.mainloop()
if __name__=="__main__":
ViewerWindow()

How to create a line count beside a Text widget Tkinter [duplicate]

Trying to learn tkinter and python. I want to display line number for the Text widget in an adjacent frame
from Tkinter import *
root = Tk()
txt = Text(root)
txt.pack(expand=YES, fill=BOTH)
frame= Frame(root, width=25)
#
frame.pack(expand=NO, fill=Y, side=LEFT)
root.mainloop()
I have seen an example on a site called unpythonic but its assumes that line height of txt is 6 pixels.
I am trying something like this:
1) Binding Any-KeyPress event to a function that returns the line on which the keypress occurs:
textPad.bind("<Any-KeyPress>", linenumber)
def linenumber(event=None):
line, column = textPad.index('end').split('.')
#creating line number toolbar
try:
linelabel.pack_forget()
linelabel.destroy()
lnbar.pack_forget()
lnbar.destroy()
except:
pass
lnbar = Frame(root, width=25)
for i in range(0, len(line)):
linelabel= Label(lnbar, text=i)
linelabel.pack(side=LEFT)
lnbar.pack(expand=NO, fill=X, side=LEFT)
Unfortunately this is giving some weird numbers on the frame.
Is there a simpler solution?
How to approach this?
I have a relatively foolproof solution, but it's complex and will likely be hard to understand because it requires some knowledge of how Tkinter and the underlying tcl/tk text widget works. I'll present it here as a complete solution that you can use as-is because I think it illustrates a unique approach that works quite well.
Note that this solution works no matter what font you use, and whether or not you use different fonts on different lines, have embedded widgets, and so on.
Importing Tkinter
Before we get started, the following code assumes tkinter is imported like this if you're using python 3.0 or greater:
import tkinter as tk
... or this, for python 2.x:
import Tkinter as tk
The line number widget
Let's tackle the display of the line numbers. What we want to do is use a canvas so that we can position the numbers precisely. We'll create a custom class, and give it a new method named redraw that will redraw the line numbers for an associated text widget. We also give it a method attach, for associating a text widget with this widget.
This method takes advantage of the fact that the text widget itself can tell us exactly where a line of text starts and ends via the dlineinfo method. This can tell us precisely where to draw the line numbers on our canvas. It also takes advantage of the fact that dlineinfo returns None if a line is not visible, which we can use to know when to stop displaying line numbers.
class TextLineNumbers(tk.Canvas):
def __init__(self, *args, **kwargs):
tk.Canvas.__init__(self, *args, **kwargs)
self.textwidget = None
def attach(self, text_widget):
self.textwidget = text_widget
def redraw(self, *args):
'''redraw line numbers'''
self.delete("all")
i = self.textwidget.index("#0,0")
while True :
dline= self.textwidget.dlineinfo(i)
if dline is None: break
y = dline[1]
linenum = str(i).split(".")[0]
self.create_text(2,y,anchor="nw", text=linenum)
i = self.textwidget.index("%s+1line" % i)
If you associate this with a text widget and then call the redraw method, it should display the line numbers just fine.
Automatically updating the line numbers
This works, but has a fatal flaw: you have to know when to call redraw. You could create a binding that fires on every key press, but you also have to fire on mouse buttons, and you have to handle the case where a user presses a key and uses the auto-repeat function, etc. The line numbers also need to be redrawn if the window is grown or shrunk or the user scrolls, so we fall into a rabbit hole of trying to figure out every possible event that could cause the numbers to change.
There is another solution, which is to have the text widget fire an event whenever something changes. Unfortunately, the text widget doesn't have direct support for notifying the program of changes. To get around that, we can use a proxy to intercept changes to the text widget and generate an event for us.
In an answer to the question "https://stackoverflow.com/q/13835207/7432" I offered a similar solution that shows how to have a text widget call a callback whenever something changes. This time, instead of a callback we'll generate an event since our needs are a little different.
A custom text class
Here is a class that creates a custom text widget that will generate a <<Change>> event whenever text is inserted or deleted, or when the view is scrolled.
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, *args):
# let the actual widget perform the requested action
cmd = (self._orig,) + args
result = self.tk.call(cmd)
# generate an event if something was added or deleted,
# or the cursor position changed
if (args[0] in ("insert", "replace", "delete") or
args[0:3] == ("mark", "set", "insert") or
args[0:2] == ("xview", "moveto") or
args[0:2] == ("xview", "scroll") or
args[0:2] == ("yview", "moveto") or
args[0:2] == ("yview", "scroll")
):
self.event_generate("<<Change>>", when="tail")
# return what the actual widget returned
return result
Putting it all together
Finally, here is an example program which uses these two classes:
class Example(tk.Frame):
def __init__(self, *args, **kwargs):
tk.Frame.__init__(self, *args, **kwargs)
self.text = CustomText(self)
self.vsb = tk.Scrollbar(self, orient="vertical", command=self.text.yview)
self.text.configure(yscrollcommand=self.vsb.set)
self.text.tag_configure("bigfont", font=("Helvetica", "24", "bold"))
self.linenumbers = TextLineNumbers(self, width=30)
self.linenumbers.attach(self.text)
self.vsb.pack(side="right", fill="y")
self.linenumbers.pack(side="left", fill="y")
self.text.pack(side="right", fill="both", expand=True)
self.text.bind("<<Change>>", self._on_change)
self.text.bind("<Configure>", self._on_change)
self.text.insert("end", "one\ntwo\nthree\n")
self.text.insert("end", "four\n",("bigfont",))
self.text.insert("end", "five\n")
def _on_change(self, event):
self.linenumbers.redraw()
... and, of course, add this at the end of the file to bootstrap it:
if __name__ == "__main__":
root = tk.Tk()
Example(root).pack(side="top", fill="both", expand=True)
root.mainloop()
Here's my attempt at doing the same thing. I tried Bryan Oakley's answer above, it looks and works great, but it comes at a price with performance. Everytime I'm loading lots of lines into the widget, it takes a long time to do that. In order to work around this, I used a normal Text widget to draw the line numbers, here's how I did it:
Create the Text widget and grid it to the left of the main text widget that you're adding the lines for, let's call it textarea. Make sure you also use the same font you use for textarea:
self.linenumbers = Text(self, width=3)
self.linenumbers.grid(row=__textrow, column=__linenumberscol, sticky=NS)
self.linenumbers.config(font=self.__myfont)
Add a tag to right-justify all lines added to the line numbers widget, let's call it line:
self.linenumbers.tag_configure('line', justify='right')
Disable the widget so that it cannot be edited by the user
self.linenumbers.config(state=DISABLED)
Now the tricky part is adding one scrollbar, let's call it uniscrollbar to control both the main text widget as well as the line numbers text widget. In order to do that, we first need two methods, one to be called by the scrollbar, which can then update the two text widgets to reflect the new position, and the other to be called whenever a text area is scrolled, which will update the scrollbar:
def __scrollBoth(self, action, position, type=None):
self.textarea.yview_moveto(position)
self.linenumbers.yview_moveto(position)
def __updateScroll(self, first, last, type=None):
self.textarea.yview_moveto(first)
self.linenumbers.yview_moveto(first)
self.uniscrollbar.set(first, last)
Now we're ready to create the uniscrollbar:
self.uniscrollbar= Scrollbar(self)
self.uniscrollbar.grid(row=self.__uniscrollbarRow, column=self.__uniscrollbarCol, sticky=NS)
self.uniscrollbar.config(command=self.__scrollBoth)
self.textarea.config(yscrollcommand=self.__updateScroll)
self.linenumbers.config(yscrollcommand=self.__updateScroll)
Voila! You now have a very lightweight text widget with line numbers:
I have seen an example on a site called unpythonic but its assumes that line height of txt is 6 pixels.
Compare:
# assume each line is at least 6 pixels high
step = 6
step - how often (in pixels) program check text widget for new lines. If height of line in text widget is 30 pixels, this program performs 5 checks and draw only one number.
You can set it to value that <6 if font is very small.
There is one condition: all symbols in text widget must use one font, and widget that draw numbers must use the same font.
# http://tkinter.unpythonic.net/wiki/A_Text_Widget_with_Line_Numbers
class EditorClass(object):
...
self.lnText = Text(self.frame,
...
state='disabled', font=('times',12))
self.lnText.pack(side=LEFT, fill='y')
# The Main Text Widget
self.text = Text(self.frame,
bd=0,
padx = 4, font=('times',12))
...
After thoroughly reading through each solution mentioned here, and trying some of them out myself, I decided to use Brian Oakley's solution with some modifications. This might not be a more efficient solution, but should be enough for someone who is looking for a quick and easy to implement method, which is also simple in principle.
It draws the line's in the same manner, but instead of generating <<Change>> events, it simply binds the key press, scroll, left click events to the text as well as left click event to the scrollbar. In order to not glitch when, e.g. a paste command is performed, it then waits 2ms before actually redrawing the line numbers.
EDIT: This is also similair to FoxDot's solution, but instead of constantly refreshing the line numbers, they are only refreshed on the bound events
Below is an example code with delays implemented, along with my implementation of the scroll
import tkinter as tk
# This is a scrollable text widget
class ScrollText(tk.Frame):
def __init__(self, master, *args, **kwargs):
tk.Frame.__init__(self, *args, **kwargs)
self.text = tk.Text(self, bg='#2b2b2b', foreground="#d1dce8",
insertbackground='white',
selectbackground="blue", width=120, height=30)
self.scrollbar = tk.Scrollbar(self, orient=tk.VERTICAL, command=self.text.yview)
self.text.configure(yscrollcommand=self.scrollbar.set)
self.numberLines = TextLineNumbers(self, width=40, bg='#313335')
self.numberLines.attach(self.text)
self.scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
self.numberLines.pack(side=tk.LEFT, fill=tk.Y, padx=(5, 0))
self.text.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True)
self.text.bind("<Key>", self.onPressDelay)
self.text.bind("<Button-1>", self.numberLines.redraw)
self.scrollbar.bind("<Button-1>", self.onScrollPress)
self.text.bind("<MouseWheel>", self.onPressDelay)
def onScrollPress(self, *args):
self.scrollbar.bind("<B1-Motion>", self.numberLines.redraw)
def onScrollRelease(self, *args):
self.scrollbar.unbind("<B1-Motion>", self.numberLines.redraw)
def onPressDelay(self, *args):
self.after(2, self.numberLines.redraw)
def get(self, *args, **kwargs):
return self.text.get(*args, **kwargs)
def insert(self, *args, **kwargs):
return self.text.insert(*args, **kwargs)
def delete(self, *args, **kwargs):
return self.text.delete(*args, **kwargs)
def index(self, *args, **kwargs):
return self.text.index(*args, **kwargs)
def redraw(self):
self.numberLines.redraw()
'''THIS CODE IS CREDIT OF Bryan Oakley (With minor visual modifications on my side):
https://stackoverflow.com/questions/16369470/tkinter-adding-line-number-to-text-widget'''
class TextLineNumbers(tk.Canvas):
def __init__(self, *args, **kwargs):
tk.Canvas.__init__(self, *args, **kwargs, highlightthickness=0)
self.textwidget = None
def attach(self, text_widget):
self.textwidget = text_widget
def redraw(self, *args):
'''redraw line numbers'''
self.delete("all")
i = self.textwidget.index("#0,0")
while True :
dline= self.textwidget.dlineinfo(i)
if dline is None: break
y = dline[1]
linenum = str(i).split(".")[0]
self.create_text(2, y, anchor="nw", text=linenum, fill="#606366")
i = self.textwidget.index("%s+1line" % i)
'''END OF Bryan Oakley's CODE'''
if __name__ == '__main__':
root = tk.Tk()
scroll = ScrollText(root)
scroll.insert(tk.END, "HEY" + 20*'\n')
scroll.pack()
scroll.text.focus()
root.after(200, scroll.redraw())
root.mainloop()
Also, I noticed that with Brian Oakley's code, if you use the mouse wheel to scroll (and the scrollable text is full), the top number lines sometimes glitch out and get out of sync with the actual text, which is why I decided to add the delay in the first place. Though I only tested it on my own implementation of Scrolled Text widget, so this bug might be unique to my solution, although it is still peculiar
There is a very simple method that I've used based on Bryan Oakley's answer above. Instead of listening for any changes made, simply "refresh" the widget using the self.after() method, which schedules a call after a number of milliseconds. Very simple way of doing it. In this instance I attach the text widget at instantation but you could do this later if you want.
class TextLineNumbers(tk.Canvas):
def __init__(self, textwidget, *args, **kwargs):
tk.Canvas.__init__(self, *args, **kwargs)
self.textwidget = textwidget
self.redraw()
def redraw(self, *args):
'''redraw line numbers'''
self.delete("all")
i = self.textwidget.index("#0,0")
while True :
dline= self.textwidget.dlineinfo(i)
if dline is None: break
y = dline[1]
linenum = str(i).split(".")[0]
self.create_text(2,y,anchor="nw", text=linenum)
i = self.textwidget.index("%s+1line" % i)
# Refreshes the canvas widget 30fps
self.after(30, self.redraw)

How to handle callback and event binding in tkinter from different classes? [closed]

Closed. This question needs to be more focused. It is not currently accepting answers.
Want to improve this question? Update the question so it focuses on one problem only by editing this post.
Closed 2 years ago.
Improve this question
Following Bryan Oakley's answer to this post Best way to structure a tkinter application?, I would like to move the contents of tab1 to its own class, but leave say_hello and get_item in the main class as they are. How do I do that?
import tkinter as tk
from tkinter import ttk, N, S, END, EXTENDED
class MainApplication(tk.Frame):
def __init__(self, root, *args, **kwargs):
tk.Frame.__init__(self, root, *args, **kwargs)
self.root = root
self.nb = ttk.Notebook(root)
self.tab1 = ttk.Frame(self.nb)
self.tab2 = ttk.Frame(self.nb)
self.nb.add(self.tab1, text='TAB1')
self.nb.add(self.tab2, text='TAB2')
self.nb.grid(row=0, column=0)
#contents of tab1 - which I would like to place in their own class
self.frame = tk.Frame(self.tab1)
self.frame.grid(row=0, column=0)
self.button = tk.Button(self.frame, text='Hello', command=self.say_hello)
self.button.grid(row=0, column=0)
self.items = ['A','B','C']
self.listbox = tk.Listbox(self.frame, selectmode=EXTENDED, exportselection=0)
self.listbox.grid(row=1, column=0)
self.scrollbar = tk.Scrollbar(self.frame, orient='vertical')
self.scrollbar.grid(row=1, column=1, sticky=N+S)
self.listbox.config(yscrollcommand=self.scrollbar.set)
self.scrollbar.config(command=self.listbox.yview)
self.listbox.bind('<<ListboxSelect>>', self.get_item)
for item in self.items:
self.listbox.insert(END, item)
#contents of tab2 - which should be in their own class
#...
#...
def say_hello(self):
print('hello')
def get_item(self, evt):
w = evt.widget
index = int(w.curselection()[0])
print('item selected = {}'.format(self.items[index]))
if __name__ == "__main__":
root = tk.Tk()
MainApplication(root)
root.mainloop()
EDIT:
Thank you Saad for your detailed response. I ran your code, studied it, and learned quite a bit. However, I modified my question to make it more focused.
According to me, I think everyone has their own style of writing and organizing their files and classes. My way of organizing code might not be the best. But I'll try my best to make it organized and simpler for you. Though there is no hard and fast rule on how to organize any Tkinter application. But yes OOP is the best way to organize big projects as they make the code small and easier to understand.
You can create a class for each tab (Tab1, Tab2, ..) in a separate file (tab1.py, tab2.py, ..) but I would rather have all my tabs in one file named as tabs.py. So I can import them like so..
import tabs
tabs.Tab1()
tabs.Tab2()
...
Kindly go through each comment in the code and my points to get most of your answers.
I've divided your code into 4 parts.
First I made a base frame class for all tabs. In that base frame class, we can configure things that all tabs require in common. For example, let's say you want a frame in each tab that has background color 'lightyellow'. So you can create a base class and put it in a separate file as baseframe.py. like so...
# baseframe.py
import tkinter as tk
class BaseFrame(tk.Frame):
"This is a base frame for Tabs."
def __init__(self, master=None, **kw):
super().__init__(master=master, **kw)
# This will be your baseframe, if you need it.
# Do something here that is common for all tabs.
# for example if you want to have the same color for every tab.
# These are just some example, change it as per your need.
self['bg'] = 'lightyellow'
self['relief'] = 'sunken'
self['borderwidth'] = 5
...
def show_info(self):
"This function is common to every tab"
# You don't necessarily need this,
# This is just to show you the possibilities.
# Do something in this function, that you want to have in every tab.
...
...
print('show_info', self)
return
Would it make sense to have a separate file with a function in it for creating a generic Listbox?
I think a CustomListbox class would be nice, you can be very creative in creating this class but I kept it simple. I added a scrollbar inside of CustomListbox class and configured it.
You can either create file customlistbox.py for this. or change the name of baseframe.py to basewidgets.py and keep both BaseFrame and CustomListbox in one file.
# customlistbox.py
import tkinter as tk
class CustomListbox(tk.Listbox):
"Tkinter listbox which has vertical scrollbar configured."
def __init__(self, master=None, cnf={}, **kw):
super().__init__(master=master, cnf=cnf, **kw)
# Config scrollbar
self.scrollbar = tk.Scrollbar(self.master,orient='vertical',command=self.yview)
self.config(yscrollcommand=self.scrollbar.set)
See in tab classes how to use it.
Here's tabs.py file,
# tabs.py
import tkinter as tk
from baseframe import BaseFrame
from customlistbox import CustomListbox
from tkinter import ttk, N, S, END, EXTENDED
class Tab1(BaseFrame):
"This is Tab 1."
def __init__(self, master=None, **kw):
super().__init__(master=master, **kw)
self.button2 = tk.Button(self,text='Welcome to Tab1', command=self.show_info)
self.button2.grid(row=0, column=0)
self.button = tk.Button(self, text='Hello', command=self.say_hello)
self.button.grid(row=1, column=0)
self.items = ['A','B','C']
self.listbox = CustomListbox(self, selectmode=EXTENDED, exportselection=0)
self.listbox.grid(row=2, column=0)
self.listbox.scrollbar.grid(row=2, column=1, sticky=N+S)
self.listbox.bind("<<ListboxSelect>>", self.get_item)
for item in self.items:
self.listbox.insert(END, item)
def say_hello(self):
"Callback function for button."
print('hello')
def get_item(self, evt):
"Internal function."
w = evt.widget
index = int(w.curselection()[0])
print('item selected = {}'.format(self.items[index]))
class Tab2(BaseFrame):
"This is Tab2."
def __init__(self, master=None, **kw):
super().__init__(master=master, **kw)
self.button = tk.Button(self,text='Welcome to Tab2', command=self.show_info)
self.button.grid(row=0, column=0)
Finally comes the main.py file. Here you can import your tabs (import tabs). I organized the MainApplication class according to my comfort. For example, rather than creating each instance (self.tab1, self.tab2,..) for tabs, I created a list self.tabs to contain all the tabs, which is more convenient to access through the index of self.tabs.
# main.py
import tkinter as tk
import tabs as tb
from tkinter import ttk, N, S, END, EXTENDED
class MainApplication(ttk.Notebook):
def __init__(self, master=None, **kw):
super().__init__(master=master, **kw)
self.master = master
self.tabs = [] # All tabs list, easier to access.
# Add tabs here.
self.add_tab(tb.Tab1, text='TAB1')
self.add_tab(tb.Tab2, text='TAB2')
...
...
# Excess methods of Tabs:-
self.tabs[0].say_hello()
self.tabs[1].show_info()
# for example: Bind say_hello() with "Button-2" to this class.
self.bind('<Button-2>', lambda evt: self.tabs[0].say_hello())
def add_tab(self, tab, **kw):
"Adds tab to the notebook."
self.tabs.append(tab(self))
self.add(self.tabs[-1], **kw)
if __name__ == "__main__":
root = tk.Tk()
# Creating an container.
container = tk.Frame(root)
container.grid()
# Initializing the app.
app = MainApplication(container)
app.grid()
root.mainloop()
Hopefully, this has made a lot of things clear to you.

How to reference correctly to frame names in tkinter

I am creating a card game in tkinter and need help with referencing to the frame names. My problem is that when I want to "refresh" the frame, I need to destroy and recreate it and this changes the progressive numbering of the frames.
Please take a look at the code below. The example shows that the third frame every time gets a new name as it gets recreated.
import tkinter as tk
class RootFrame(tk.Tk):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.main_window = tk.Frame(self)
self.main_window.pack(side="top", fill="both", expand=True)
self.main_label = tk.Label(self.main_window, text="Main Window")
self.main_label.pack()
self.second_frame = SecondFrame(self.main_window, self)
self.second_frame.pack()
class SecondFrame(tk.Frame):
def __init__(self, parent, controller, *args, **kwargs):
super().__init__(*args, **kwargs)
self.controller = controller
label = tk.Label(self, text="Second Frame")
label.pack()
self.create_third_frame()
def create_third_frame(self):
self.third_frame = ThirdFrame(self, self.controller)
self.third_frame.pack()
def update_frame(self):
self.third_frame.destroy()
self.create_third_frame()
class ThirdFrame(tk.Frame):
def __init__(self, parent, controller, *args, **kwargs):
super().__init__(*args, **kwargs)
self.controller = controller
self.parent = parent
label = tk.Label(self, text="Third Frame")
label.pack()
refresh_button = tk.Button(
self, text="Resfresh", command=self.parent.update_frame)
refresh_button.pack()
print(self.winfo_name())
if __name__ == "__main__":
app = RootFrame()
app.mainloop()
The code above is used to illustrate the problem. Please run the code and you'll see the changing widget name in the terminal.
I use winfo_parent and winfo_name in my actual code to create different conditions for button bindings. For example, if the user clicks a widget1 in frame6 happens X and when I click a widget8 in frame2 happens Y. This works until I destroy() and recreate something and everything breaks.
I suppose that using winfo_name and winfo_parent for this kind of referencing is not the correct way to get around, but I really can't think of anything else.
I'm not sure exactly what you are asking, but you can assign a specific name to the widget:
def create_third_frame(self):
self.third_frame = ThirdFrame(self, self.controller, name='testframe')
self.third_frame.pack()
Then each time the name of the frame created will be consistent.
You can also reference the widget by name with Tk().nametowidget(), see this relevant answer here: Is it possible to search widget by name in Tkinter?
>>> from Tkinter import *
>>> win = Tk()
>>> button = Button( Frame( win, name = "myframe" ), name = "mybutton" )
>>> win.nametowidget("myframe.mybutton")
<Tkinter.Button instance at 0x2550c68>
I would recommend sticking with a OOP approach however, and just reference it with from your code like self.thirdframes where you might have a list or dict of ThirdFrame objects. This way your python code can easily reference the objects without going back to the tcl interpreter and parse the widget name. If you ever will only have one ThirdFrame, then just reference back to self.thirdframe whenever you need it.

Tkinter adding line number to text widget

Trying to learn tkinter and python. I want to display line number for the Text widget in an adjacent frame
from Tkinter import *
root = Tk()
txt = Text(root)
txt.pack(expand=YES, fill=BOTH)
frame= Frame(root, width=25)
#
frame.pack(expand=NO, fill=Y, side=LEFT)
root.mainloop()
I have seen an example on a site called unpythonic but its assumes that line height of txt is 6 pixels.
I am trying something like this:
1) Binding Any-KeyPress event to a function that returns the line on which the keypress occurs:
textPad.bind("<Any-KeyPress>", linenumber)
def linenumber(event=None):
line, column = textPad.index('end').split('.')
#creating line number toolbar
try:
linelabel.pack_forget()
linelabel.destroy()
lnbar.pack_forget()
lnbar.destroy()
except:
pass
lnbar = Frame(root, width=25)
for i in range(0, len(line)):
linelabel= Label(lnbar, text=i)
linelabel.pack(side=LEFT)
lnbar.pack(expand=NO, fill=X, side=LEFT)
Unfortunately this is giving some weird numbers on the frame.
Is there a simpler solution?
How to approach this?
I have a relatively foolproof solution, but it's complex and will likely be hard to understand because it requires some knowledge of how Tkinter and the underlying tcl/tk text widget works. I'll present it here as a complete solution that you can use as-is because I think it illustrates a unique approach that works quite well.
Note that this solution works no matter what font you use, and whether or not you use different fonts on different lines, have embedded widgets, and so on.
Importing Tkinter
Before we get started, the following code assumes tkinter is imported like this if you're using python 3.0 or greater:
import tkinter as tk
... or this, for python 2.x:
import Tkinter as tk
The line number widget
Let's tackle the display of the line numbers. What we want to do is use a canvas so that we can position the numbers precisely. We'll create a custom class, and give it a new method named redraw that will redraw the line numbers for an associated text widget. We also give it a method attach, for associating a text widget with this widget.
This method takes advantage of the fact that the text widget itself can tell us exactly where a line of text starts and ends via the dlineinfo method. This can tell us precisely where to draw the line numbers on our canvas. It also takes advantage of the fact that dlineinfo returns None if a line is not visible, which we can use to know when to stop displaying line numbers.
class TextLineNumbers(tk.Canvas):
def __init__(self, *args, **kwargs):
tk.Canvas.__init__(self, *args, **kwargs)
self.textwidget = None
def attach(self, text_widget):
self.textwidget = text_widget
def redraw(self, *args):
'''redraw line numbers'''
self.delete("all")
i = self.textwidget.index("#0,0")
while True :
dline= self.textwidget.dlineinfo(i)
if dline is None: break
y = dline[1]
linenum = str(i).split(".")[0]
self.create_text(2,y,anchor="nw", text=linenum)
i = self.textwidget.index("%s+1line" % i)
If you associate this with a text widget and then call the redraw method, it should display the line numbers just fine.
Automatically updating the line numbers
This works, but has a fatal flaw: you have to know when to call redraw. You could create a binding that fires on every key press, but you also have to fire on mouse buttons, and you have to handle the case where a user presses a key and uses the auto-repeat function, etc. The line numbers also need to be redrawn if the window is grown or shrunk or the user scrolls, so we fall into a rabbit hole of trying to figure out every possible event that could cause the numbers to change.
There is another solution, which is to have the text widget fire an event whenever something changes. Unfortunately, the text widget doesn't have direct support for notifying the program of changes. To get around that, we can use a proxy to intercept changes to the text widget and generate an event for us.
In an answer to the question "https://stackoverflow.com/q/13835207/7432" I offered a similar solution that shows how to have a text widget call a callback whenever something changes. This time, instead of a callback we'll generate an event since our needs are a little different.
A custom text class
Here is a class that creates a custom text widget that will generate a <<Change>> event whenever text is inserted or deleted, or when the view is scrolled.
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, *args):
# let the actual widget perform the requested action
cmd = (self._orig,) + args
result = self.tk.call(cmd)
# generate an event if something was added or deleted,
# or the cursor position changed
if (args[0] in ("insert", "replace", "delete") or
args[0:3] == ("mark", "set", "insert") or
args[0:2] == ("xview", "moveto") or
args[0:2] == ("xview", "scroll") or
args[0:2] == ("yview", "moveto") or
args[0:2] == ("yview", "scroll")
):
self.event_generate("<<Change>>", when="tail")
# return what the actual widget returned
return result
Putting it all together
Finally, here is an example program which uses these two classes:
class Example(tk.Frame):
def __init__(self, *args, **kwargs):
tk.Frame.__init__(self, *args, **kwargs)
self.text = CustomText(self)
self.vsb = tk.Scrollbar(self, orient="vertical", command=self.text.yview)
self.text.configure(yscrollcommand=self.vsb.set)
self.text.tag_configure("bigfont", font=("Helvetica", "24", "bold"))
self.linenumbers = TextLineNumbers(self, width=30)
self.linenumbers.attach(self.text)
self.vsb.pack(side="right", fill="y")
self.linenumbers.pack(side="left", fill="y")
self.text.pack(side="right", fill="both", expand=True)
self.text.bind("<<Change>>", self._on_change)
self.text.bind("<Configure>", self._on_change)
self.text.insert("end", "one\ntwo\nthree\n")
self.text.insert("end", "four\n",("bigfont",))
self.text.insert("end", "five\n")
def _on_change(self, event):
self.linenumbers.redraw()
... and, of course, add this at the end of the file to bootstrap it:
if __name__ == "__main__":
root = tk.Tk()
Example(root).pack(side="top", fill="both", expand=True)
root.mainloop()
Here's my attempt at doing the same thing. I tried Bryan Oakley's answer above, it looks and works great, but it comes at a price with performance. Everytime I'm loading lots of lines into the widget, it takes a long time to do that. In order to work around this, I used a normal Text widget to draw the line numbers, here's how I did it:
Create the Text widget and grid it to the left of the main text widget that you're adding the lines for, let's call it textarea. Make sure you also use the same font you use for textarea:
self.linenumbers = Text(self, width=3)
self.linenumbers.grid(row=__textrow, column=__linenumberscol, sticky=NS)
self.linenumbers.config(font=self.__myfont)
Add a tag to right-justify all lines added to the line numbers widget, let's call it line:
self.linenumbers.tag_configure('line', justify='right')
Disable the widget so that it cannot be edited by the user
self.linenumbers.config(state=DISABLED)
Now the tricky part is adding one scrollbar, let's call it uniscrollbar to control both the main text widget as well as the line numbers text widget. In order to do that, we first need two methods, one to be called by the scrollbar, which can then update the two text widgets to reflect the new position, and the other to be called whenever a text area is scrolled, which will update the scrollbar:
def __scrollBoth(self, action, position, type=None):
self.textarea.yview_moveto(position)
self.linenumbers.yview_moveto(position)
def __updateScroll(self, first, last, type=None):
self.textarea.yview_moveto(first)
self.linenumbers.yview_moveto(first)
self.uniscrollbar.set(first, last)
Now we're ready to create the uniscrollbar:
self.uniscrollbar= Scrollbar(self)
self.uniscrollbar.grid(row=self.__uniscrollbarRow, column=self.__uniscrollbarCol, sticky=NS)
self.uniscrollbar.config(command=self.__scrollBoth)
self.textarea.config(yscrollcommand=self.__updateScroll)
self.linenumbers.config(yscrollcommand=self.__updateScroll)
Voila! You now have a very lightweight text widget with line numbers:
I have seen an example on a site called unpythonic but its assumes that line height of txt is 6 pixels.
Compare:
# assume each line is at least 6 pixels high
step = 6
step - how often (in pixels) program check text widget for new lines. If height of line in text widget is 30 pixels, this program performs 5 checks and draw only one number.
You can set it to value that <6 if font is very small.
There is one condition: all symbols in text widget must use one font, and widget that draw numbers must use the same font.
# http://tkinter.unpythonic.net/wiki/A_Text_Widget_with_Line_Numbers
class EditorClass(object):
...
self.lnText = Text(self.frame,
...
state='disabled', font=('times',12))
self.lnText.pack(side=LEFT, fill='y')
# The Main Text Widget
self.text = Text(self.frame,
bd=0,
padx = 4, font=('times',12))
...
After thoroughly reading through each solution mentioned here, and trying some of them out myself, I decided to use Brian Oakley's solution with some modifications. This might not be a more efficient solution, but should be enough for someone who is looking for a quick and easy to implement method, which is also simple in principle.
It draws the line's in the same manner, but instead of generating <<Change>> events, it simply binds the key press, scroll, left click events to the text as well as left click event to the scrollbar. In order to not glitch when, e.g. a paste command is performed, it then waits 2ms before actually redrawing the line numbers.
EDIT: This is also similair to FoxDot's solution, but instead of constantly refreshing the line numbers, they are only refreshed on the bound events
Below is an example code with delays implemented, along with my implementation of the scroll
import tkinter as tk
# This is a scrollable text widget
class ScrollText(tk.Frame):
def __init__(self, master, *args, **kwargs):
tk.Frame.__init__(self, *args, **kwargs)
self.text = tk.Text(self, bg='#2b2b2b', foreground="#d1dce8",
insertbackground='white',
selectbackground="blue", width=120, height=30)
self.scrollbar = tk.Scrollbar(self, orient=tk.VERTICAL, command=self.text.yview)
self.text.configure(yscrollcommand=self.scrollbar.set)
self.numberLines = TextLineNumbers(self, width=40, bg='#313335')
self.numberLines.attach(self.text)
self.scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
self.numberLines.pack(side=tk.LEFT, fill=tk.Y, padx=(5, 0))
self.text.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True)
self.text.bind("<Key>", self.onPressDelay)
self.text.bind("<Button-1>", self.numberLines.redraw)
self.scrollbar.bind("<Button-1>", self.onScrollPress)
self.text.bind("<MouseWheel>", self.onPressDelay)
def onScrollPress(self, *args):
self.scrollbar.bind("<B1-Motion>", self.numberLines.redraw)
def onScrollRelease(self, *args):
self.scrollbar.unbind("<B1-Motion>", self.numberLines.redraw)
def onPressDelay(self, *args):
self.after(2, self.numberLines.redraw)
def get(self, *args, **kwargs):
return self.text.get(*args, **kwargs)
def insert(self, *args, **kwargs):
return self.text.insert(*args, **kwargs)
def delete(self, *args, **kwargs):
return self.text.delete(*args, **kwargs)
def index(self, *args, **kwargs):
return self.text.index(*args, **kwargs)
def redraw(self):
self.numberLines.redraw()
'''THIS CODE IS CREDIT OF Bryan Oakley (With minor visual modifications on my side):
https://stackoverflow.com/questions/16369470/tkinter-adding-line-number-to-text-widget'''
class TextLineNumbers(tk.Canvas):
def __init__(self, *args, **kwargs):
tk.Canvas.__init__(self, *args, **kwargs, highlightthickness=0)
self.textwidget = None
def attach(self, text_widget):
self.textwidget = text_widget
def redraw(self, *args):
'''redraw line numbers'''
self.delete("all")
i = self.textwidget.index("#0,0")
while True :
dline= self.textwidget.dlineinfo(i)
if dline is None: break
y = dline[1]
linenum = str(i).split(".")[0]
self.create_text(2, y, anchor="nw", text=linenum, fill="#606366")
i = self.textwidget.index("%s+1line" % i)
'''END OF Bryan Oakley's CODE'''
if __name__ == '__main__':
root = tk.Tk()
scroll = ScrollText(root)
scroll.insert(tk.END, "HEY" + 20*'\n')
scroll.pack()
scroll.text.focus()
root.after(200, scroll.redraw())
root.mainloop()
Also, I noticed that with Brian Oakley's code, if you use the mouse wheel to scroll (and the scrollable text is full), the top number lines sometimes glitch out and get out of sync with the actual text, which is why I decided to add the delay in the first place. Though I only tested it on my own implementation of Scrolled Text widget, so this bug might be unique to my solution, although it is still peculiar
There is a very simple method that I've used based on Bryan Oakley's answer above. Instead of listening for any changes made, simply "refresh" the widget using the self.after() method, which schedules a call after a number of milliseconds. Very simple way of doing it. In this instance I attach the text widget at instantation but you could do this later if you want.
class TextLineNumbers(tk.Canvas):
def __init__(self, textwidget, *args, **kwargs):
tk.Canvas.__init__(self, *args, **kwargs)
self.textwidget = textwidget
self.redraw()
def redraw(self, *args):
'''redraw line numbers'''
self.delete("all")
i = self.textwidget.index("#0,0")
while True :
dline= self.textwidget.dlineinfo(i)
if dline is None: break
y = dline[1]
linenum = str(i).split(".")[0]
self.create_text(2,y,anchor="nw", text=linenum)
i = self.textwidget.index("%s+1line" % i)
# Refreshes the canvas widget 30fps
self.after(30, self.redraw)

Categories