Canvas leaves scrollregion if scrolled with mouswheel - python-3.x

Problem: you can scroll upwards even though the first item should be at the very top of the canvas
How to reproduce the problem: have at least one item on the canvas and scroll using the mouse ( press space to make an item )
Note: the problem ceases once the items are wider than the canvas itself
Sample code
import tkinter as tk
class App(tk.Tk):
def __init__(self):
tk.Tk.__init__(self)
self.bind("<MouseWheel>", self.scroll)
self.bind("<KeyPress>", self.keypressed)
self.TargetCanvas = None
self.makeui()
def makeui(self):
self.Canvas = tk.Canvas(self, bg = "white")
self.Canvas.grid(row = 0, column = 0, sticky = "W")
sby = tk.Scrollbar(self, orient = "vertical", command = self.Canvas.yview)
sby.grid(row = 0, column = 1, sticky = "ns")
self.Canvas.configure(yscrollcommand = sby.set)
self.CanvFrame = tk.Frame(self.Canvas, bg = "white")
self.Canvas.create_window((0, 0), window = self.CanvFrame, anchor = "nw")
self.CanvFrame.bind("<Configure>", lambda event: self.Canvas.configure(scrollregion = self.Canvas.bbox("all")))
self.Canvas.bind("<Enter>", lambda event: self.settarget(event, True))
self.Canvas.bind("<Leave>", lambda event: self.settarget(event, False))
tk.Label(self, text = "space = add item, c = clear all").grid(row = 1, column = 0)
def settarget(self, event, check):
if check:
self.TargetCanvas = event.widget
else:
self.TargetCanvas = None
def scroll(self, event):
if self.TargetCanvas is not None:
direction = 0
if event.num == 5 or event.delta == -120:
direction = 1
if event.num == 4 or event.delta == 120:
direction = -1
self.TargetCanvas.yview_scroll(direction, tk.UNITS)
def keypressed(self, event):
if event.keysym == "space":
self.additem()
elif event.keysym == "c":
self.clearall()
def clearall(self):
for i in self.CanvFrame.winfo_children():
i.destroy()
x = tk.Label(self.CanvFrame)
#kick in frame resizing
x.pack()
self.update()
self.Canvas.configure(scrollregion = self.Canvas.bbox("all"))
x.destroy()
def additem(self):
tk.Label(self.CanvFrame, text = "asd").pack(side = tk.TOP)
def main():
App().mainloop()
if __name__ == "__main__":
main()

Related

Two tkinter Objects act differently when bound on the same event

I am creating instances of two different objects (features_object and line_object) on a canvas. The idea is to connect several of the features by lines (). Later on when features are moved around the lines should follow.
At the moment my issue is that, when I apply the delete method on the lines I can delete them (Lines created between features by left mouse click. Line deleted by right mouse double click). However with the similar method of the feature class nothing happens! I cannot delete the features! The features seem invisible on the canvas! Is there any solution to that?
Thanks a lot!
from tkinter import Tk, Frame, Canvas, Text, END, TRUE, BOTH, CURRENT
click = 0
class line_object:
all_connections = []
def __init__(self):
self.connections_data = []
def create_line(self, master, start_elem, end_elem, tag_start, tag_end):
self.master = master
self.start_elem = start_elem
self.end_elem = end_elem
self.tag_start = tag_start
self.tag_end = tag_end
self.start_x = self.master.coords(self.start_elem)[0]
self.start_y = self.master.coords(self.start_elem)[1]
self.end_x = self.master.coords(self.end_elem)[0]
self.end_y = self.master.coords(self.end_elem)[1]
self.line_tag = (f'Connector_{self.start_elem}_{self.end_elem}', 'connection', 'drag')
self.master.delete(self.line_tag)
self.line_id = self.master.create_line(self.start_x, self.start_y, self.end_x, self.end_y, fill="red", width=6, tags=(self.line_tag, self.tag_start, self.tag_end))
self.master.tag_lower(self.line_id)
self.bind_events_lines()
def bind_events_lines(self):
self.master.bind('<Double Button-3>', self.deleteLine)
def deleteLine(self, event=None):
actual = self.master.find_withtag(CURRENT)
self.master.delete(actual)
class features_object():
def __init__(self):
self.features_data = []
def create_feature(self, master, feature_text, feature_xpos, feature_ypos, feature_color):
self.master = master
self.feature_text = feature_text
self.feature_xpos = feature_xpos
self.feature_ypos = feature_ypos
self.feature_color = feature_color
self.feature_frame = Frame(self.master, bg=self.feature_color, border=20)
self.feature_text = Text(self.feature_frame, font=("Helvetica", 12), relief='flat', width=20, height=3, selectbackground=self.feature_color, selectforeground="black", exportselection=TRUE)
self.feature_text.grid(row=0, column=0, padx=5, pady=5, sticky='w')
self.feature_text.config(wrap='word')
self.feature_text.insert(END, feature_text)
self.feature_tags = 'drag'
self.feature_id = self.master.create_window(self.feature_xpos, self.feature_ypos, window=self.feature_frame, tags=self.feature_tags)
self.master.itemconfigure(self.feature_id, tags=[self.feature_id])
self.bind_events_features()
def bind_events_features(self):
self.feature_frame.bind('<Button-1>', self.draw_line_with_bind)
self.feature_frame.bind('<Double Button-3>', self.deleteFeature)
def deleteFeature(self, event):
actual = self.master.find_withtag(CURRENT)
self.master.delete(actual)
def draw_line_with_bind(self, event):
global click, start_x, start_y, end_x, end_y, elem_start, tag_start
if click == 0:
elem_start = self.feature_id
tag_start = f'{elem_start}'
click = 1
elif click == 1:
elem_end = self.feature_id
tag_end = f'{elem_end}'
lin = line_object()
self.connection = lin.create_line(draw_canvas, elem_start, elem_end, tag_start, tag_end)
click = 0
window = Tk()
window.geometry("1000x800")
frame_start = Frame(window)
frame_start.pack(expand=TRUE, fill=BOTH)
draw_canvas = Canvas(frame_start)
draw_canvas.pack(expand=TRUE, fill=BOTH)
features_object().create_feature(draw_canvas, 'feature A', 100, 100, 'red')
features_object().create_feature(draw_canvas, 'feature B', 300, 300, 'green')
features_object().create_feature(draw_canvas, 'feature C', 500, 500, 'magenta')
def read_id(event):
currently_clicked = draw_canvas.find_withtag(CURRENT)
print(f'Currently clicked instance {currently_clicked}')
draw_canvas.bind('<Button-2>', read_id)
if __name__ == '__main__':
window.mainloop()

Tkinter Scrollbar inactive untill the window is manually resized

The scrollbar activates only when the window is rezised, despite reconfiguring the scrollregion everytime a widget(Button in this case) is added.
class Editor:
def __init__(self,master):
self.master = master
self.master.resizable(True,False)
self.edit_frame = tk.Frame(self.master)
self.edit_frame.pack(fill="none")
self.elem_frame = tk.Frame(self.master,width=700)
self.elem_frame.pack(fill="y", expand=1)
self.my_canvas = tk.Canvas(self.elem_frame, width = 700)
self.my_canvas.pack(side = "left", fill="both", expand=1)
my_scrollbar = ttk.Scrollbar(self.elem_frame, orient = "vertical", command =self.my_canvas.yview)
my_scrollbar.pack(side = "right",fill="y")
self.my_canvas.configure(yscrollcommand=my_scrollbar.set)
self.my_canvas.bind('<Configure>', self.movescroll )
self.second_frame = tk.Frame(self.my_canvas, bg = "black", width=700)
self.my_canvas.create_window((0,0), window=self.second_frame, anchor="nw")
self.naz = Nazioni()
paesi = [*self.naz.iso3]
print(paesi)
self.comb_paese = ttk.Combobox(self.edit_frame, value = paesi)
self.comb_paese.current(1)
self.kwordvar= tk.StringVar()
entry_keyword = tk.Entry(self.edit_frame, textvariable=self.kwordvar)
entry_keyword.bind("<FocusOut>", self.callback)
writer = tk.Text(self.edit_frame)
self.edit_frame.pack(side="left")
writer.pack()
self.comb_paese.pack()
entry_keyword.pack()
def callback(self, *args):
kword = self.kwordvar.get()
self.colonnarisultati(kword)
def colonnarisultati(self, keyword):
#TROVA IL VALORE CODICE ISO A 3CHAR IN BASE ALLA NAZIONE NEL COMBOBOX
p = self.comb_paese.get()
paese = self.naz.iso3[p]
ricerca = Ricerca(paese, keyword)
"""
for label in self.elem_frame.winfo_children():
label.destroy()
"""
self.count = 0
print("ciao")
for x in ricerca.listaricerca:
tit = str(x['titolo']).lstrip("<h4>").rstrip("</h4>")
print(tit)
self.elementotrovato(tit,self.count)
self.count +=1
self.my_canvas.configure(scrollregion=self.my_canvas.bbox("all"))
def movescroll(self, *args):
self.my_canvas.configure(scrollregion=self.my_canvas.bbox("all"))
def elementotrovato(self, testobottone, conta):
Bott_ecoifound = tk.Button(self.second_frame, text = testobottone,width = 100 ,height = 30, font = ("Helvetica", 12, "bold"), wraplength=200, justify="left")
print("CIAONE")
Bott_ecoifound.pack(fill="both", expand=1)
self.my_canvas.configure(scrollregion=self.my_canvas.bbox("all"))
The last method is the one adding button to the scrollable frame.
After the window is manually resized, no matter if back to its original size, the scrollbar activates and works fine.

Getting spinner working without threading

I am trying to get a spinner working, but without using threads (I had a working version, but it gave me erratic behaviour. After reading online people told me that it's best to avoid threading with tkinter). So I wanted to give the after function a try.
The goal is to have a spinner pop up before starting something is done and have it disappear once the 'something' is done. Here is a minimalistic example of what I have:
import tk
from time import sleep
class Spinner(tk.Toplevel):
def __init__(self, master, text = None, barsize = 10, speed = 100, spinnerSize = 50):
super().__init__(master = master)
self.barsize = barsize
self.speed = speed
self.size = spinnerSize
self.done = False
self.progress = 0
self.forward = True
if text is None:
self.text = 'Something is happening'
else:
self.text = text
self.title(self.text)
self.out = tk.Label(master = self,
text = self.spinnerCalc(),
height = 1,
borderwidth = 0,
width = 80,
bg = self['background'])
self.out.pack()
self.update_self()
def update_self(self):
print(self.progress)
if self.forward:
if self.progress == self.size - self.barsize:
self.forward = False
self.progress -= 1
else:
self.progress += 1
else:
if self.progress == 0:
self.forward = True
self.progress += 1
else:
self.progress -= 1
self.out.config(text = self.spinnerCalc())
if self.done:
self.grab_release()
self.destroy()
else:
# print('entered continue update')
self.update()
# print('step 2')
self.grab_set()
# print('step 3')
self.after(self.speed, self.update_self)
# print('step 4')
def spinnerCalc(self):
#returns something like |------█████--------------|
bar = '|'
barsize = self.barsize
size = self.size
for i in range (self.progress):
bar += '-'
for i in range (barsize):
bar += '\u2588'
for i in range (size-barsize-self.progress):
bar += '-'
bar += '|'
return bar
def stop(self):
self.done = True
root = tk.Tk()
def spin():
spinner = Spinner(root)
a_bunch_of_stuff_happening(10)
spinner.stop()
def a_bunch_of_stuff_happening(amount):
count = 0
for i in range(100000000):
count += i
print(count)
tk.Button(master = root,
text = 'Press me',
command = spin).pack()
tk.Button(master = root,
text = 'Close me',
command = root.destroy).pack()
root.mainloop()
The problem is that the update_self is not running. I do not want to mainloop the Spinner as that will block my a_bunch_of_stuff_happening() function. The a_bunch_of_stuff_happening is also a complicated function which changes attributes of the main root class.
I am stuck with what else to try without going into threading again (which was a big pain last time). Anyone have advice what else I could try to get this to work?

After text widget opens, tkinter GUI crashes/does not respond whenever its closed

Right now, after I press the 'Time Range' button and call the calculateTime function, the text widget would appear with the results that I've inserted into it. However, after that, whenever I close the GUI window, the program would freeze and I'll have to forced quit it. This is my code:
import tkinter
from tkinter import *
import math
from tkinter import messagebox
class MyClass(tkinter.Frame):
def __init__(self, *args, **kwargs):
tkinter.Frame.__init__(self, *args, **kwargs)
#Setting up frame and widgets
vcmd1 = (self.register(self.__vcmd1), '%P', '%S')
vcmd2 = (self.register(self.__vcmd2), '%P')
vcmd3 = (self.register(self.__vcmd3), '%P', '%S')
label_iso = Label(self,text="Isotope A, Element")
label_vol = Label(self, text="Voltage")
label_range = Label(self, text="Charge Range")
label_iso.grid(row=0, column=0, sticky=E)
label_vol.grid(row=1, column=0, sticky=E)
label_range.grid(row=2, column=0, sticky=E)
self.entry1 = tkinter.Entry(self, validate="key", validatecommand=vcmd1)
self.entry2 = tkinter.Entry(self, validate="key", validatecommand=vcmd2)
self.entry3 = tkinter.Entry(self, validate="key", validatecommand=vcmd3)
self.entry1.grid(row=0, column=1)
self.entry2.grid(row=1, column=1)
self.entry3.grid(row=2, column=1)
def __vcmd1(self, P, S):
validString = 'qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM,1234567890'
if not S in validString:
return False
if "," in P:
if (len(P) - 1) > len(P.replace(",","")):
return False
messagebox.showinfo("Error", "Expected Form: ex. 133,Cs")
else:
return True
def __vcmd2(self, P):
if P == '':
return True
try:
float(P)
return True
except ValueError:
messagebox.showinfo("Error", "Entry is not a float or integer")
return False
def __vcmd3(self, P, S):
if "," in P:
if len(P.split(",")) > 2:
return False
a = P.split(",")[0]
b = P.split(",")[1]
if a != '' and b != '':
try:
int(a)
int(b)
except ValueError:
messagebox.showinfo("Error", "Expected form: ex. 1,12")
return False
else:
return True
class TimeGenerator:
def __init__(self,master):
frame = MyClass(master)
frame.grid(columnspan=2)
button = Button(root, text='Time Range', command=self.calculateTime)
button.grid(row=3, columnspan=2)
self.text = Text(root)
self.iso = frame.entry1
self.vol = frame.entry2
self.r = frame.entry3
def calculateTime(self):
x = 5
if self.r.get() == "" or self.iso.get() == "" or self.vol.get() == "":
messagebox.showinfo("Error", "No field can be empty")
return None
self.iso = self.iso.get().replace(" ", "")
list = []
for e in self.iso.split(","):
list.append(e)
f = open("/Users/LazyLinh/PycharmProjects/mass.mas12.txt", "r")
i = 0
while (i < 40):
header = f.readline()
i += 1
self.mass = 0
#iterate through text file
for line in f:
line = line.strip()
columns = line.split()
if (list[0] == columns[3]):
if (list[1].lower() == columns[4].lower()):
if (len(columns) == 16):
self.mass = float(columns[13].replace("#","")) + float(columns[14].replace("#",""))
else:
self.mass = float(columns[12].replace("#","")) + float(columns[13].replace("#",""))
#Calculation
self.r = self.r.get().replace(" ", "")
tup = tuple(int(x) for x in self.r.split(","))
list = []
for q in range(tup[0], tup[1] + 1):
y = (x * math.sqrt(self.mass * 1.6605402e-27 / (2 * q * float(self.vol.get())))) * 10e6
list.append(y)
i = tup[0]
#inserting to text widget
for time in list:
self.text.insert("end", "%d: %s\n" % (i, time))
i = i + 1
self.text.pack()
root = Tk()
b = TimeGenerator(root)
root.mainloop()
I've tried to searched up on this topic, but I'm not really using any weird update() function, and text shows up after the function is finished, so how likely that it is an event loop problem? Am I also doing something wrong that could cause this problem?
Thank you!
You have widgets in the root window that use both pack and grid. You cannot do this. Within a given container (root window, frame, etc) you can use one or the other, but not both.
The reason your program freezes is due to pack and grid fighting to gain control of the layout. When you pack the text widget it causes a change in the size and/or position of other widgets in the root window. That triggers grid to try to re-layout the widgets it is responsible for. This triggers pack to try to re-layout the widgets it is responsible for, and on and on until the end of time.
My guess is that you need to use grid with self.text since you use grid everywhere else.

Display of a large amount of data in a Canvas with a Scrollbar

I'm having a problem trying to display a large table in tkinter.
First, I tried to display all label at once in the canvas, but over few hundred rows, the program shut down. So I tried to create a scrollable canvas that updates everytime I scroll: I collect the position of the scrollbar and depending on the position of it, I display the 10 values corresponding.
But I can't get this code working. For now it only displays a black background with the scrollbar on the right.
Here is the code:
from tkinter import *
class Application(object):
def __init__(self, parent):
self.x = []
for i in range(1, 1000):
self.x.append(i)
self.parent = parent
self.mainFrame = Frame(self.parent)
self.mainFrame.pack()
self.canvas = Canvas(self.mainFrame, width = 200, height = 500, bg = "black")
self.canvas.grid(row = 0, column = 0)
self.scroll = Scrollbar(self.mainFrame, orient = VERTICAL, command = self.update)
self.scroll.grid(row = 0, column = 1)
self.canvas.configure(yscrollcommand = self.scroll.set)
self.tabCursor = 0
self.scrollPosition = self.scroll.get()
def update(self):
self.tabCursor = round(self.scrollPosition[0]*len(self.x))
if ((len(self.x) - self.tabCursor) < 10):
self.tabCursor = len(self.x) - 10
for i in range(0, 10): #display 10 values
label = Label(self.canvas, text = str(self.x[tabCursor + i]), width = 50)
label.grid(column = 0, row = i)
if __name__ == '__main__':
root = Tk()
app = Application(root)
root.mainloop()
EDIT :
I finally had time to implement your answer. It looks fine but I can't get the scrollbar working and i don't know why.
class TableauDeDonnees(object):
"Tableau de données -- Onglet Tableau de données"
def __init__(self, data, parent):
self.parent = parent
self.data = data
print(self.data[0], self.data[1])
print(len(self.data[0]), len(self.data[1]))
self.labels = []
self.navigationFrame = Frame(self.parent)
self.canvas = Canvas(self.parent, bg = "black", width = 200, height = 500)
self.mainFrame = Frame(self.canvas)
self.navigationFrame.pack()
print(len(data))
for row in range(50):
for column in range(len(data)):
self.labels.append(Label(self.canvas, text = str(data[column][row])))
for i in range(len(self.labels)):
self.labels[i].grid(row = i // 2, column = i % 2, sticky = NSEW)
self.boutonRetour = Button(self.navigationFrame, text = "Retour", command = lambda: self.move(-2))
self.quickNav = Entry(self.navigationFrame, width = 3)
self.quickNav.bind('<Return>', lambda x: self.move(self.quickNav.get()))
self.boutonSuivant = Button(self.navigationFrame, text = "Suivant", command = lambda: self.move(0))
temp = divmod(len(data[0]), len(self.labels) // 2)
self.pages = temp[0] + (1 if temp[1] else 0)
self.position = Label(self.navigationFrame, text='Page 1 sur ' + str(self.pages))
self.pageCourante = 1
self.boutonRetour.grid(row = 0, column = 0)
self.quickNav.grid(row = 0, column = 1)
self.boutonSuivant.grid(row = 0, column = 2)
self.position.grid(row = 0, column = 3)
self.scroll = Scrollbar(self.parent, orient = VERTICAL, command = self.canvas.yview)
self.canvas.configure(yscrollcommand = self.scroll.set)
self.scroll.pack(side = RIGHT, fill='y')
self.canvas.pack(side = LEFT, fill = 'both')
self.canvas.create_window((4,4), window = self.mainFrame, anchor = "nw", tags = "frame")
self.canvas.configure(yscrollcommand = self.scroll.set)
self.mainFrame.bind("<Configure>", self.update)
self.canvas.configure(scrollregion = self.canvas.bbox("all"))
def update(self, event):
self.canvas.configure(scrollregion = self.canvas.bbox("all"))
def move(self, direction):
if (self.pageCourante == 1 and direction == -2) or (self.pageCourante == self.pages and direction == 0):
return
if direction in (-2, 0):
self.pageCourante += direction + 1
else:
try:
temp = int(direction)
if temp not in range(1, self.pages + 1):
return
except ValueError:
return
else:
self.pageCourante = temp
for i in range(len(self.labels)):
try:
location = str(self.data[i % 2][len(self.labels)*(self.pageCourante - 1) + i])
except IndexError:
location = ''
self.labels[i].config(text = location)
self.position.config(text = 'Page ' + str(self.pageCourante) + ' sur ' + str(self.pages))
I don't understand why the scrollbar isn't working properly. Note, that my parent is a notebook.
Also, there is a problem with the number of items displayed. The number of pages is right but it seems it displays more than it should cause last pages are empty and the last values displayed seems right.
Thank you for your attention
The scrollbar doesn't work by continuously creating new widgets ad infinitum. You were also missing some key parts - unfortunately, Scrollbar isn't as straightforward as most tkinter widgets.
from tkinter import *
class Application(object):
def __init__(self, parent):
self.parent = parent
self.canvas = Canvas(self.parent, bg='black', width = 200, height = 500)
self.mainFrame = Frame(self.canvas)
self.scroll = Scrollbar(self.parent, orient = VERTICAL, command=self.canvas.yview)
self.canvas.configure(yscrollcommand=self.scroll.set)
self.scroll.pack(side='right', fill='y')
self.canvas.pack(side='left', fill='both')
self.canvas.create_window((4,4), window=self.mainFrame, anchor="nw", tags="frame")
self.canvas.configure(yscrollcommand = self.scroll.set)
self.mainFrame.bind("<Configure>", self.update)
self.x = []
for i in range(1000):
self.x.append(Label(self.mainFrame, text=str(i)))
self.x[i].grid()
def update(self, event):
self.canvas.configure(scrollregion=self.canvas.bbox("all"))
if __name__ == '__main__':
root = Tk()
app = Application(root)
root.mainloop()
If you'd like to show only a few at a time and provide a forum-like interface, you can use Buttons to navigate between pages. This example allows the user to navigate with Back and Forward buttons, as well as by entering a page number in the box and pressing Enter.
from tkinter import *
class Application(object):
def __init__(self, parent):
self.x = list(range(1000))
self.labels = []
self.parent = parent
self.navigation_frame = Frame(self.parent)
self.canvas = Canvas(self.parent, bg='black', width = 200, height = 500)
self.mainFrame = Frame(self.canvas)
self.navigation_frame.pack()
for i in range(100):
self.labels.append(Label(self.mainFrame, text=str(i)))
self.labels[i].grid()
self.back_button = Button(self.navigation_frame, text='Back', command=lambda: self.move(-2))
self.quick_nav = Entry(self.navigation_frame, width=3)
self.quick_nav.bind('<Return>', lambda x: self.move(self.quick_nav.get()))
self.forward_button = Button(self.navigation_frame, text='Forward', command=lambda: self.move(0))
temp = divmod(len(self.x), len(self.labels))
self.pages = temp[0] + (1 if temp[1] else 0)
self.you_are_here = Label(self.navigation_frame, text='Page 1 of ' + str(self.pages))
self.current_page = 1
self.back_button.grid(row=0, column=0)
self.quick_nav.grid(row=0, column=1)
self.forward_button.grid(row=0, column=2)
self.you_are_here.grid(row=0, column=3)
self.scroll = Scrollbar(self.parent, orient = VERTICAL, command=self.canvas.yview)
self.canvas.configure(yscrollcommand=self.scroll.set)
self.scroll.pack(side='right', fill='y')
self.canvas.pack(side='left', fill='both')
self.canvas.create_window((4,4), window=self.mainFrame, anchor="nw", tags="frame")
self.canvas.configure(yscrollcommand = self.scroll.set)
self.mainFrame.bind("<Configure>", self.update)
def update(self, event):
self.canvas.configure(scrollregion=self.canvas.bbox("all"))
def move(self, direction):
if (self.current_page == 1 and direction == -2) or (self.current_page == self.pages and direction == 0):
return
if direction in (-2, 0):
self.current_page += direction + 1
else:
try:
temp = int(direction)
if temp not in range(1, self.pages+1):
return
except ValueError:
return
else:
self.current_page = temp
for i in range(len(self.labels)):
try:
location = str(self.x[len(self.labels)*(self.current_page - 1) + i])
except IndexError:
location = ''
self.labels[i].config(text=location)
self.you_are_here.config(text='Page ' + str(self.current_page) + ' of ' + str(self.pages))
if __name__ == '__main__':
root = Tk()
app = Application(root)
root.mainloop()

Resources