(Python) Text widget line numbers - python

I have some code working that displays line numbers along the left side of a text widget. For the most part, it works well, but there are a couple of issues that I need some pointers on how to fix.
I have posted the code below.
Here is a link to a test file with 200 lines (each line is numbered).
test file here
Issues:
(1) You can mouse wheel (scroll) up and down the file. However, this causes the line numbers to get out of sync with the text widget line numbers. If someone can point me in the right direction to look at, I probably can figure it out and fix it.
(2) The PgDn/PgUp keys have a similar effect. I can hit PgDn a few times and notice the Text widget lines get out of sync with the line numbers, as well as losing their horizontal alignment. Again, if someone can point me in the right direction to look at, I'm sure I can figure out what's happening in the code and fix it.
from tkinter import *
class LineNumbers(Text):
def __init__(self, master, text_widget, **kwargs):
super().__init__(master, **kwargs)
self.text_widget = text_widget
self.text_widget.bind('<KeyRelease>', self.on_key_release)
self.text_widget.bind('<FocusIn>', self.on_key_release)
self.text_widget.bind('<MouseWheel>', self.on_key_release)
self.insert(1.0, '1')
self.configure(state='disabled')
def on_key_release(self, event=None):
p, q = self.text_widget.index("#0,0").split('.')
p = int(p)
final_index = str(self.text_widget.index(END))
num_of_lines = final_index.split('.')[0]
line_numbers_string = "\n".join(str(p + no) for no in range(int(num_of_lines)))
width = len(str(num_of_lines))
self.configure(state='normal', width=width)
self.delete(1.0, END)
self.insert(1.0, line_numbers_string)
self.configure(state='disabled')
if __name__ == '__main__':
win = Tk()
win.title("Line Numbers Test")
win.geometry("800x600+1000+300")
txt = Text(win)
ln = LineNumbers(win, txt, width=2)
f = open("line_test.txt", 'r')
lines = f.readlines()
for line in lines:
txt.insert(END, line)
f.close()
ln.pack(side=LEFT, fill=BOTH)
txt.pack(expand=True, fill=BOTH)
txt.focus()
win.mainloop()

class LineNumbers(Text):
def __init__(self, master, text_widget, **kwargs):
super().__init__(master, **kwargs)
self.busy = False
self.master = master
self.text_widget = text_widget
self.text_widget.bind('<KeyRelease>', self.on_key_release)
self.text_widget.bind('<FocusIn>', self.on_key_release)
self.text_widget.bind('<MouseWheel>', self.on_mousew)
self.text_widget.bind('<Button-4>', self.on_mousew)
self.text_widget.bind('<Button-5>', self.on_mousew)
self.insert(1.0, '1')
self.configure(state='disabled')
def on_mousew(self, event=None):
self.after( 50, self.bogus)
def on_key_release(self, event=None):
self.after( 50, self.bogus)
return "break"
def bogus( self):
self.text_widget.update()
p, q = self.text_widget.index("#0,0").split('.')
np = int(p)
self.text_widget.see( "#" + str( np+1) + ","+ q)
final_index = str(self.text_widget.index(END))
num_of_lines = final_index.split('.')[0]
line_numbers_string = "\n".join( str(np+no) for no in range(0, int(num_of_lines)))
width = len(str(num_of_lines))
self.configure(state='normal', width=width)
self.delete(1.0, END)
self.insert(1.0, line_numbers_string)
self.configure(state='disabled')

Related

Tkinter button highlight feature stops working after command is called

I have been working on my first GUI in tkinter - I am using Windows. My goal right now is to have buttons that accomplish these goals:
The buttons are highlighted when moused over.
The button remains highlighted if clicked.
Only one button can be "selected" (click-highlighted) at a time.
I initially thought that I had accomplished this! But I realize now that my work is not complete.
Here is what I am seeing:
I mouse over button A. It becomes highlighted! (GOOD)
I click on button A. It stays highlighted! (GOOD)
I mouse over button B. It becomes highlighted! (GOOD)
I click on button B. It stays highlighted! The highlight from A is removed! (GOOD)
I mouse over button A. It does not highlight. (BAD)
I am calling the default_coloring class function on button A when I click on button B. However, this appears to turn off the highlighting functions of button A, and the button no longer functions correctly according to the three rules I listed at the top.
How do I ensure that the buttons continue to function normally, even after the command is called? Am I approaching this the wrong way?
import tkinter as tk
blue = '#0000BB'
white = '#FFFFFF'
class HoverButton(tk.Button):
def __init__(self, master, position = None, **kw):
tk.Button.__init__(self,master=master,**kw)
self.defaultBackground = self["background"]
self.defaultForeground = self["foreground"]
self.bind("<Enter>", self.on_enter)
self.bind("<Leave>", self.on_leave)
self.bind("<Button-1>", self.hover_click)
self.state = 0
self.position = position
def on_enter(self, e):
if self.state == 0:
self['background'] = self['activebackground']
self['foreground'] = self['activeforeground']
def on_leave(self, e):
if self.state == 2:
self.state = 0
if self.state == 0:
self['background'] = self.defaultBackground
self['foreground'] = self.defaultForeground
def hover_click(self, e):
self.state += 1
self.state = self.state % 3
if self.state == 2:
self['background'] = self.defaultBackground
self['foreground'] = self.defaultForeground
def default_coloring(self):
self['background'] = self.defaultBackground
self['foreground'] = self.defaultForeground
class AddOnFrame(tk.Frame):
def __init__(self, master):
self.selectedbutton = None
super().__init__(master)
games = ['A','B','C']
self.objs = list()
self['bg'] = blue
for i in range(3):
self.objs.append(HoverButton(self,position = i, text = games[i].upper(), activebackground = white,activeforeground = blue,fg = white, bg = blue, borderwidth=0, relief = 'flat', highlightbackground = white))
self.objs[i]['command'] = lambda c=i: self._hover_button_clicked(self.objs[c])
self.objs[i].grid(row = i, column = 0, sticky = tk.W + tk.E)
self.blanklabel = tk.Label(self, text = '', background = white)
self.blanklabel.grid(row = 0, column = 1,rowspan = 10, sticky = tk.N + tk.E + tk.W + tk.S)
self.grid_columnconfigure(1, weight=1, minsize=10)
self.grid_columnconfigure(2, weight=1, minsize=500)
self.grid_columnconfigure(3, weight=1, minsize=500)
self.grid_columnconfigure(4, weight=1, minsize=500)
self.pack(expand = True)
def _hover_button_clicked(self, HoverButton):
self.lastbutton = self.selectedbutton
if self.lastbutton != None:
self.objs[self.lastbutton].default_coloring()
self.selectedbutton = HoverButton.position
window = tk.Tk()
window.geometry('1750x950')
window['bg'] = blue
window.title('Testing')
lf = AddOnFrame(window)
lf['bg'] = blue
window.mainloop()
I think I found the main source of the problem. When another button is clicked, you restore color of the last clicked button, but you do not reset its state. Change your default_coloring function to:
def default_coloring(self):
self.state = 0
self['background'] = self.defaultBackground
self['foreground'] = self.defaultForeground
But you should also prevent default_coloring if same button is pressed again:
def _hover_button_clicked(self, HoverButton):
self.lastbutton = self.selectedbutton
if (self.lastbutton != None) and (self.lastbutton != HoverButton.position):
self.objs[self.lastbutton].default_coloring()
self.selectedbutton = HoverButton.position
After cursory inspection, this sequence seems to be the problem:
When a button is clicked, the AddOnFrame._hover_button_clicked
method is invoked.
AddOnFrame.selectedbutton is initially None, which means the
if-statement in AddOnFrame._hover_button_clicked will not be
executed the first time. This is why the buttons seem to work the
first time you click them, but not after that.
However, the next time it is invoked (the next time a button is
pressed), AddOnFrame.selectedbutton is not None, and will never
be None again, meaning that from now on, every click will result in
a call to that HoverButton's default_coloring method.
default_coloring is invoked as soon as a button is clicked, which
results in a quick flash from the active color to the default color,
and the button does not stay highlighted.
The quick fix:
Basically, don't do the default_coloring stuff. It seems to be hurting you more than it's helping. Not really sure why you're doing it in the first place (all that stuff with setting the command, the lambda, the whole _hover_button_clicked method) since the buttons seem to be setting their colors back to the default just fine when on_leave or hover_click are invoked. You can fix your problem by changing the body of your HoverButton.default_coloring function to this:
def default_coloring(self):
return
The real fix would be some restructuring of your code.
EDIT I'm offering this to help you simplify things:
import tkinter as tk
colors = {
"white": "#FFFFFF",
"blue": "#0000BB"
}
class HoverButton(tk.Button):
def __init__(self, *args, **kwargs):
tk.Button.__init__(self, *args, **kwargs)
self.is_selected = False
self.is_highlighted = False
self["borderwidth"] = 0
self["relief"] = tk.FLAT
self["font"] = ("United Sans Cd Bk", 30)
self["activeforeground"] = colors["blue"]
self["activebackground"] = colors["white"]
self["highlightbackground"] = colors["white"]
self.recolor()
self.bind("<Enter>", self.on_enter)
self.bind("<Leave>", self.on_leave)
self.bind("<Button-1>", self.on_click)
def recolor(self):
self["background"] = [colors["blue"], colors["white"]][self.is_highlighted]
self["foreground"] = [colors["white"], colors["blue"]][self.is_highlighted]
def on_enter(self, *args):
self.is_highlighted = True
self.recolor()
def on_leave(self, *args):
if self.is_selected:
return
self.is_highlighted = False
self.recolor()
def on_click(self, *args):
self.is_selected = not self.is_selected
class Application(tk.Tk):
def __init__(self, *args, **kwargs):
tk.Tk.__init__(self, *args, **kwargs)
self.title("Window")
self.geometry("256x256")
self.resizable(width=False, height=False)
self["background"] = colors["blue"]
button_labels = ["A", "B", "C"]
self.buttons = []
for row, button_label in enumerate(button_labels):
button = HoverButton(text=button_label)
button.grid(row=row, column=0, sticky=tk.W)
self.buttons.append(button)
def main():
application = Application()
application.mainloop()
return 0
if __name__ == "__main__":
import sys
sys.exit(main())

Text widgets as List

I'm trying to create tabs with Text using tkinter Notebook widget and Text widget.
I have created list of tabs and Text widgets everything going as far as good except add_tabs method. Whenever I press control-n for adding new tabs only for first time i got this exception:
I have no idea how can i fix this problem thanks for helping me.
Thank you.
C:\Users\Imtiyaz\Desktop>python maintabtest.py
Exception in Tkinter callback
Traceback (most recent call last):
File "C:\Python34\lib\tkinter\__init__.py", line 1482, in __call__
return self.func(*args)
File "maintabtest.py", line 32, in add_tabs
self.nb.add(self._tabs[self.i],text="untitled")
IndexError: list index out of range
code:
import tkinter.ttk as ttks
from tkinter import BOTH,LEFT
class mainbody:
def __init__(self,master):
self.master = master
self._tabs = []
self._text = []
self.i = 1
self.body = ttks.Frame(self.master)
self.nb = ttks.Notebook(self.master)
self.nb.pack(fill=BOTH,expand=1)
self.body.pack(fill=BOTH,expand=1)
self.initial_tab = ttks.Frame(self.nb)
self.Inittext = ttks.tkinter.Text(self.initial_tab)
self.Inittext.pack(fill=BOTH,expand=1)
self.initial_tab.pack(fill=BOTH,expand=1)
self._text.append()
self.nb.add(self.initial_tab,text="first_tab")
self.File_name = ttks.Entry(self.master)
self.File_name.pack(side=LEFT)
self.sbtn = ttks.Button(self.master,text="save_btn",command=lambda:self.save_())
self.sbtn.pack(side=LEFT)
self.master.bind('<Control-n>',self.add_tabs)
def add_tabs(self,event):
self._tabs.append(ttks.Frame())
self.nb.add(self._tabs[self.i],text="untitled")
self._text.append(ttks.tkinter.Text(self._tabs[self.i]))
self._text[self.i].pack(fill=BOTH,expand=1)
self.i = self.i + 1
def save_(self):
self.fname = self.File_name.get()
self._txt_id = self.nb.index('current')
self.get_input = self._text[self._txt_id].get("1.0","end-1c")
with open(self.fname,'w') as f:
f.write(self.get_input)
if __name__ == "__main__":
root = ttks.tkinter.Tk()
mainbody(root)
root.mainloop()
Your problem is that self.i did not synchronize with the size of the 2 lists: self._tabs and self._text.
Basically you don't need the self.i to track the index to the last item of the 2 lists. Just use -1 instead of self.i in add_tabs() to refer the last item in the list as below:
import tkinter.ttk as ttks
from tkinter import BOTH,LEFT
from tkinter.messagebox import showinfo
class mainbody:
def __init__(self,master):
self.master = master
self._tabs = []
self._text = []
self.body = ttks.Frame(self.master)
self.nb = ttks.Notebook(self.master)
self.nb.pack(fill=BOTH, expand=1)
self.body.pack(fill=BOTH, expand=1)
self.add_tabs("first_tab") # add the initial tab
self.File_name = ttks.Entry(self.master)
self.File_name.pack(side=LEFT)
self.sbtn = ttks.Button(self.master, text="save_btn", command=self.save_file)
self.sbtn.pack(side=LEFT)
self.master.bind('<Control-n>', lambda e:self.add_tabs())
def add_tabs(self, name="untitled"):
self._tabs.append(ttks.Frame())
self.nb.add(self._tabs[-1], text=name)
self._text.append(ttks.tkinter.Text(self._tabs[-1]))
self._text[-1].pack(fill=BOTH, expand=1)
def save_file(self):
self.fname = self.File_name.get().strip()
if not self.fname == '':
self._txt_id = self.nb.index('current')
self.get_input = self._text[self._txt_id].get("1.0","end-1c")
with open(self.fname, 'w') as f:
f.write(self.get_input)
else:
showinfo('Warning', 'Please input filename')
if __name__ == "__main__":
root = ttks.tkinter.Tk()
mainbody(root)
root.mainloop()

How to create a "loading wheel" in a Tkinter Text widget?

How to create a loading wheel in a Tkinter Text Widget?
e.g. run through the sequence of -, \, |, / to create the illusion of a spinning bar.
The only examples I could find were for commandline/console applications
Insert text into the console by using console.write('text here')
If you wish for a spinning bar/wheel on the end of the line simple add the additional parameter console.write('text here', 'loader')
The spinning wheel will stop and disappear once you write another line of text.
class Console(Frame):
def __init__(self, master, **kwargs):
Frame.__init__(self, master)
self.text = Text(self, wrap='word', **kwargs)
self.text.pack()
self.text.config(state='disabled')
self.sequence = ['-', '\\', '|', '/']
self.load = False
self.queue = Queue.Queue()
self.update_me()
def write(self, line, link=None):
self.queue.put((line,link))
def clear(self):
self.queue.put((None, None))
def update_me(self):
try:
while 1:
line, link = self.queue.get_nowait()
self.text.config(state='normal')
if line is None:
self.text.delete(1.0, END)
elif link and link == 'loader':
self.load = True
self.text.delete(self.text.index("end-2c"))
self.text.insert(self.text.index("end-1c"), str(line))
else:
if self.load:
self.text.delete(self.text.index("end-2c"))
self.text.insert(self.text.index("end-1c"), str(line))
else:
self.text.insert(END, str(line))
self.load = False
self.text.see(END)
self.update_idletasks()
self.text.config(state='disabled')
except Queue.Empty:
pass
self.after(100, self.update_me)
if self.load:
self.queue.put((self.sequence[0], 'loader'))
self.sequence.append(self.sequence.pop(0))
if __name__ == '__main__':
# testing application
import time
root = Tk()
console = Console(root)
console.pack()
def load_it():
console.write('Loading World...', 'loader')
time.sleep(3)
console.write('Done')
import threading
t = threading.Thread(target=load_it)
t.daemon = True
t.start()
root.mainloop()
exit()

Tkinter: Identifying button by row and column

I want to be able to select a button based on what row and column it is in on a grid and the button and control its Text and Relief. I haven't been able to find anything on widgets or cells used in this manner.
Edit:
I changed where root is placed and now it says that I can't use a tuple that I recieved for 'relief' which makes sense, I need to access the widget itself. Any reccomendations
import tkinter
import functools
import random
from time import sleep
width = input('Enter the grid width. ')
height = input('Enter the grid height. ')
numb = input('Enter the number of bombs. ')
Matrix = [[0 for lp in range(int(width))] for fg in range(int(height))]
def ranintx():
return random.randint(0,int(width))
def raninty():
return random.randint(0,int(height))
def placemines():
y = ranintx()
x = raninty()
for ranintformine in range(int(numb)):
x = ranintx()
y = raninty()
Matrix[y-1][x-1] = 1
placemines()
def sunken(event, self, x, y):
button = event.widget
button['relief'] = 'sunken'
if x - 1 < 0 :
return
if x > int(width) + 1 :
return
if y - 1 < 0 :
return
if y > int(height) + 1 :
return
if Matrix[x][y] == 1 :
top = tkinter.Toplevel()
top.title("About this application...")
msg = tkinter.Message(top, text="You Lose")
msg.pack()
button = tkinter.Button(top, text="Dismiss", command=top.destroy)
button.pack()
print('Column = {}\nRow = {}'.format(x, y))
else:
n1 = x - 1
n2 = y - 1
for lp in range(3):
for lp2 in range(3):
abutton = root.grid_location(n1, n2)
abutton['relief'] = ['sunken']
# I want to be able to change and select the button here. This was one of my poor attempt
n2 =+ 1
n1 =+ 1
def push(event, self, x, y):
button = event.widget
if Matrix[x][y] == 1 :
print('Column = {}\nRow = {}'.format(x, y))
class MineSweep(tkinter.Frame):
#classmethod
def main(cls, width, height):
window = cls(root, width, height)
'''placemine()'''
root.mainloop()
def __init__(self, master, width, height):
super().__init__(master)
self.__width = width
self.__height = height
self.__build_buttons()
self.grid()
#def sunken(event):
# button = event.widget
# button['relief'] = 'sunken'
def __build_buttons(self):
self.__buttons = []
for y in range(self.__height):
row = []
for x in range(self.__width):
button = tkinter.Button(self, state='disabled')
button.grid(column=x, row=y)
button['text'] = ' '
print(grid.slaves)
self.checked = True
#button['command'] = functools.partial(self.__push, x, y)
button.bind("<Button-3>",
lambda event, arg=x, brg=y: push(event, self, arg, brg))
button['relief'] = 'raised'
button.bind("<Button-1>",
lambda event, arg=x, brg=y: sunken(event, self, arg, brg))
#button['command'] = sunken
row.append(button)
self.__buttons.append(row)
root = tkinter.Tk()
if __name__ == '__main__':
MineSweep.main(int(width), int(height))
You have a few things wrong with your program. First, sunken should be a method on the class. It's very weird to have it outside the class, and then you pass in self as some other argument. It works, but it makes the code very confusing.
That being said, you're actually very close to making this work. You're already saving a reference to each button in a list of lists, so you should be able to get the widget with self.__buttons[y][x]. However, because sunken is not part of the class, and because you named the variable with two underscores, the variable is not accessible to the sunken function.
If you change the variable to have a single underscore instead of a double, your code should work more-or-less exactly as it is (once you fix the syntax and indentation errors). The other solution is to make sunken a method on the class and fix how you call it (remove the self argument, call it as self.sunken), it will work with two underscores.
Frankly, using two underscores has zero practical benefit. Avoid the temptation to use it. At the very least, don't use it until you have your basic logic working, then you can go back and hide attributes you don't want to be exposed.

Trying to add keybinding to buttons whose callbacks are created with a factory

So I have the following code creating a grid of buttons using tkinter:
class Application(Frame):UP = 'Up'
DOWN = 'Down'
LEFT = 'Left'
RIGHT = 'Right'
END = "E"
SEND = "S"
STAR = "*"
POUND = "#"
def __init__(self, master=None):
Frame.__init__(self, master)
self.grid()
self.createWidgets()
def createWidgets(self):
self.arrowButtons = []
self.arrowButtons += [Button(self, text=self.LEFT[0], command=self.buttonPressFactory(self.LEFT) )]
self.arrowButtons += [Button(self, text=self.UP[0], command=self.buttonPressFactory(self.UP) )]
self.arrowButtons += [Button(self, text=self.RIGHT[0], command=self.buttonPressFactory(self.RIGHT) )]
self.arrowButtons += [Button(self, text=self.DOWN[0], command=self.buttonPressFactory(self.DOWN) )]
self.send = Button(self, text=self.SEND, command=self.buttonPressFactory(self.SEND) )
self.end = Button(self, text=self.END, command=self.buttonPressFactory(self.END) )
self.send.grid(row=2,column=1)
self.end.grid(row=2,column=3)
self.numButtons = []
for i in range(0,10):#make the number buttons
self.numButtons.append(Button(self, text=str(i), command=self.buttonPressFactory(str(i))))
self.starButton = Button(self, text=self.STAR, command=self.buttonPressFactory(self.STAR) )
self.hashButton = Button(self, text=self.POUND, command=self.buttonPressFactory(self.POUND) )
self.arrowButtons[0].grid(row=1,column=1)
self.arrowButtons[1].grid(row=1,column=2)
self.arrowButtons[2].grid(row=1,column=3)
self.arrowButtons[3].grid(row=2,column=2)
self.send.grid(row=2,column=1)
self.end.grid(row=2,column=3)
self.numButtons[1].grid(row=3,column=1)
self.numButtons[2].grid(row=3,column=2)
self.numButtons[3].grid(row=3,column=3)
self.numButtons[4].grid(row=4,column=1)
self.numButtons[5].grid(row=4,column=2)
self.numButtons[6].grid(row=4,column=3)
self.numButtons[7].grid(row=5,column=1)
self.numButtons[8].grid(row=5,column=2)
self.numButtons[9].grid(row=5,column=3)
self.starButton.grid(row=6,column=1)
self.numButtons[0].grid(row=6,column=2)
self.hashButton.grid(row=6,column=3)
def press(self, x):
print(x)
def buttonPressFactory(self, button):
def buttonPress(*args):
self.press(button)
root.bind("<"+button+">", buttonPress)
return buttonPress
if __name__ == '__main__':
root = Tk()
app = Application(root)
app.master.title("stackoverflow is great")
app.mainloop()
The problem started when I added the line in buttonPressFactory that starts "root.bind("
When I added that line, suddenly clicking any button seems to call press(1) then call press(whatever). The keybinds work correctly except for 1,2,3,4, and 5 (on the numpad or numrow). If you remove that line, everything works ok, but of course then you can't use the keybinds. I am new to tkinter, so I might have made an obvious mistake, though my intuition tells me I might have made a syntax mistake.
How can I add keybindings to the code without breaking it?
'<1>' is mouse button 1, '1' is the literal character (relevant docs). So don't use brackets on literal keys.
Here is the fixed buttonPressFactory:
def buttonPressFactory(self, button):
def buttonPress(*args):
self.press(button)
eventname = '<' + button + '>' if len(button) > 1 else button
root.bind(eventname, buttonPress)
return buttonPress
Regarding your statement
I am new to tkinter, so I might have made an obvious mistake, though
my intuition tells me this is actually a python mistake.
A good rule of thumb is that it is almost always your mistake. That's true for everyone.

Categories