Tkinter - Floating Window - Resize - python

Inspired by this question, I would like to write my own resizing function for my root window.
But I just noticed that my code shows some performance issues. If you resize it quickly you can see that the window doesn't finds its height prompt as I wish, it "stutters". (It's more like a swinging)
Does someone know why this happens? My best guess is that tkinter event handler is too slow for it, or the math I did isn't the quickest way.
I did try update_idletasks() on different locations and also several times. Another way that I have tried was to use the after method but it made it worse.
Here is an example code:
import tkinter as tk
class FloatingWindow(tk.Tk):
def __init__(self):
super().__init__()
self.overrideredirect(True)
self.center()
self.label = tk.Label(self, text="Grab the upper-right corner to resize")
self.label.pack(side="top", fill="both", expand=True)
self.grip2 = tk.Label(self,bg='blue')
self.grip2.place(relx=1.0, rely=0, anchor="ne")
self.grip2.bind("<B1-Motion>",self.OnMotion)
def OnMotion(self, event):
abs_x = self.winfo_pointerx() - self.winfo_rootx()
abs_y = self.winfo_pointery() - self.winfo_rooty()
if abs_x >0:
x = self.winfo_rootx()
y = self.winfo_rooty()+abs_y
height = self.winfo_height()-abs_y
if height >0:
self.geometry("%dx%d+%d+%d" % (abs_x,height,
x,y))
def center(self):
width = 300
height = 300
screen_width = self.winfo_screenwidth()
screen_height = self.winfo_screenheight()
x_coordinate = (screen_width/2) - (width/2)
y_coordinate = (screen_height/2) - (height/2)
self.geometry("%dx%d+%d+%d" % (width, height,
x_coordinate, y_coordinate))
app=FloatingWindow()
app.mainloop()
full example
Update
It appears that the performance issue is Microsoft related and a well known issue which drives most MS-Developer crazy.
Update 2
Since this issue seems MS-Windows related, I tried to find a MS specific solution and did a lot of research. I've tried to intercept messages like wm_pain, wm_nccalcsize and many more.
Somewhere on the way I thought, there is already an sizebox so it makes sense to make use of it. But it appears another issue with this solution.
A thin white stripe on the top edge. I took my quite a while till I found the answer its just the sizebox itself. Unfortunately, I haven't found a way to configure the sizebox via the win32 api or the Dwmapi.
TL;DR
The answer to this question is preferably a smooth resizing event with the blue and green Labels. But if you find a way to erase the thin white line and still have resizing ability, (just shrinking the window rect to the client rect does not work or you have just 1 pixel to resize) would be a solution too.
The updated code looks like this:
import tkinter as tk
import win32gui
import win32api
import win32con
class FloatingWindow(tk.Tk):
def __init__(self):
super().__init__()
#self.overrideredirect(True)
self.hWnd = int(self.wm_frame(), 16)
self.label = tk.Label(self, text="Grab one of the blue")
self.label.pack(side="top", fill="both", expand=True)
blues = {'se' : (1,1),'ne' : (1,0),'nw' : (0,0),'sw' : (0,1)}
grens = {'e' : (1,0.5), 'n' : (0.5,0), 'w' : (0,0.5), 's' : (0.5,1)}
for k,v in blues.items():
ref = tk.Label(self, bg='blue')
ref.place(relx=v[0],rely=v[1],anchor=k)
ref.bind("<B1-Motion>", lambda e, mode=k:self.OnMotion(e,mode))
for k,v in grens.items():
ref = tk.Label(self, bg='green')
ref.place(relx=v[0],rely=v[1],anchor=k)
ref.bind("<B1-Motion>", lambda e, mode=k:self.OnMotion(e,mode))
self.bind('<ButtonPress-1>', self.start_drag)
self.bind('<ButtonRelease-1>', self.stop_drag)
return
def stop_drag(self,event):
self.start_abs_x = None
self.start_abs_y = None
self.start_width = None
self.start_height= None
self.start_x = None
self.start_y = None
def start_drag(self,event):
self.update_idletasks()
self.start_abs_x = self.winfo_pointerx() - self.winfo_rootx()
self.start_abs_y = self.winfo_pointery() - self.winfo_rooty()
self.start_width = self.winfo_width()
self.start_height= self.winfo_height()
self.start_x = self.winfo_x()
self.start_y = self.winfo_y()
def OnMotion(self, event, mode):
self.update_idletasks()
abs_x = self.winfo_pointerx() - self.winfo_rootx()
abs_y = self.winfo_pointery() - self.winfo_rooty()
width = self.winfo_width()
height= self.winfo_height()
x = self.winfo_x()
y = self.winfo_y()
x_motion = self.start_abs_x - abs_x
y_motion = self.start_abs_y - abs_y
self.calc_x = x;self.calc_y=y;self.calc_w=width;
self.calc_h=self.start_height
if 'e' in mode:
self.calc_w = self.start_width-x_motion
if 's' in mode:
self.calc_h -= y_motion
if 'n' in mode:
self.calc_y = y-y_motion
self.calc_h = height+y_motion
if 'w' in mode:
self.calc_w = width+x_motion
self.calc_x = x-x_motion
self.geometry("%dx%d+%d+%d" % (self.calc_w,self.calc_h,
self.calc_x,self.calc_y))
def center(self):
width = 300
height = 300
screen_width = self.winfo_screenwidth()
screen_height = self.winfo_screenheight()
x_coordinate = (screen_width/2) - (width/2)
y_coordinate = (screen_height/2) - (height/2)
self.geometry("%dx%d+%d+%d" % (width, height,
x_coordinate, y_coordinate))
app=FloatingWindow()
app.update_idletasks()
hwnd = win32gui.GetParent(app.hWnd)
style= win32api.GetWindowLong(hwnd, win32con.GWL_STYLE)
style&= ~win32con.WS_CAPTION
#style&= ~win32con.WS_SIZEBOX
valid= win32api.SetWindowLong(hwnd, win32con.GWL_STYLE, style)
app.mainloop()
System Information:
Windows 10 Home; x64-base,Intel(R) Core(TM) i3-2120 # 3.30GHz, 3300
MHz, 2Cores
with
Python 3.7.2 and tkinter 8.6

I was able to solve the problem by adding the update() function in the beginning of your OnMotion method

I just noticed that Windows itself hadn't figured it out yet. If you take an ordinary directory / folder and resize it you will see the same flickering in the client area as in my example above. The only difference seems to be that they haven't an issue with erased background. So for Windows 10 and 11 the case seems closed, for now.

I have added quickly some widgets, and some events in order that everyone can notice the performance issues, if the commented line (in the beginning of the OnMotion method) is deactivated self.update(). I have played with the code without getting any error. Hope this solve your issue.
import tkinter as tk
class FloatingWindow(tk.Tk):
def __init__(self):
super().__init__()
self.overrideredirect(True)
self.geometry("800x400+300+100")
self.minsize(200, 200)
self.config(bg="green")
self.grid_columnconfigure(0, weight=3)
self.grid_rowconfigure(1, weight=3)
self.menu()
self.textbox()
self.grip_se = tk.Label(self,bg='blue')
self.grip_se.place(relx=1.0, rely=1.0, anchor="se")
self.grip_se.bind("<B1-Motion>",lambda e, mode='se':self.OnMotion(e,mode))
self.grip_e = tk.Label(self,bg='green')
self.grip_e.place(relx=1.0, rely=0.5, anchor="e")
self.grip_e.bind("<B1-Motion>",lambda e, mode='e':self.OnMotion(e,mode))
self.grip_ne = tk.Label(self,bg='blue')
self.grip_ne.place(relx=1.0, rely=0, anchor="ne")
self.grip_ne.bind("<B1-Motion>",lambda e, mode='ne':self.OnMotion(e,mode))
self.grip_n = tk.Label(self,bg='green')
self.grip_n.place(relx=0.5, rely=0, anchor="n")
self.grip_n.bind("<B1-Motion>",lambda e, mode='n':self.OnMotion(e,mode))
self.grip_nw = tk.Label(self,bg='blue')
self.grip_nw.place(relx=0, rely=0, anchor="nw")
self.grip_nw.bind("<B1-Motion>",lambda e, mode='nw':self.OnMotion(e,mode))
self.grip_w = tk.Label(self,bg='green')
self.grip_w.place(relx=0, rely=0.5, anchor="w")
self.grip_w.bind("<B1-Motion>",lambda e, mode='w':self.OnMotion(e,mode))
self.grip_sw = tk.Label(self,bg='blue')
self.grip_sw.place(relx=0, rely=1, anchor="sw")
self.grip_sw.bind("<B1-Motion>",lambda e, mode='sw':self.OnMotion(e,mode))
self.grip_s = tk.Label(self,bg='green')
self.grip_s.place(relx=0.5, rely=1, anchor="s")
self.grip_s.bind("<B1-Motion>",lambda e, mode='s':self.OnMotion(e,mode))
def menu(self):
self.frame = tk.Frame(self, height=25, bg='black')
self.frame.grid(row=0, column=0, sticky="new")
color = ['#FEF3B3','#FFF9DC', "#341C09"]
for i in range(3):
self.button = tk.Button(self.frame, text="Can you see", font=('calibri',12), bg=color[i-1], fg="red", relief="flat", bd=0)
self.button.pack(side="left", fill="both", padx=3)
self.lbl_space = tk.Label(self.frame ,text="",bd=0,bg="black")
self.lbl_space.pack(side="left", padx=5)
self.button = tk.Button(self.frame, text="Can you see", font=('calibri',12), bg=color[i-1], fg="red", relief="flat", bd=0)
self.button.pack(side="right", fill="both", padx=3)
def textbox(self):
self.frame2 = tk.Frame(self, bg='white')
self.frame2.grid(row=1, column=0, sticky="wens")
self.text_editor = tk.Text(self.frame2, wrap='word', font='calibri 12',undo = True, relief=tk.FLAT,bg="white")
self.yscrollbar = tk.Scrollbar(self.frame2, command=self.text_editor.yview)
self.yscrollbar.grid(row=0, column=1, sticky="ns")#ns
self.text_editor.config(yscrollcommand=self.yscrollbar.set)
self.text_editor.grid(row=0, column=0, sticky="wens", padx=3)
self.frame2.grid_columnconfigure(0, weight=3)
self.frame2.grid_rowconfigure(0, weight=3)
self.text_editor.insert("1.0", 'Bed sincerity yet therefore forfeited his certainty neglected questions. Pursuit chamber as elderly amongst on. Distant however warrant farther to of. My justice wishing prudent waiting in be. Comparison age not pianoforte increasing delightful now. Insipidity sufficient dispatched any reasonably led ask. Announcing if attachment resolution sentiments admiration me on diminution. ')
# insert a widget inside the text box
options = ["choice 1","choice 2"]
clicked = tk.StringVar()
clicked.set(options[0])
self.drop = tk.OptionMenu(self.text_editor, clicked, *options)
self.text_editor.window_create("1.0", window=self.drop)
self.drop.config(bg="#474747", relief='flat', font=('calibri',11, 'bold'))
def OnMotion(self, event, mode):
self.update() # <==== if you deactivate this line you can see the performance issues
abs_x = self.winfo_pointerx() - self.winfo_rootx()
abs_y = self.winfo_pointery() - self.winfo_rooty()
width = self.winfo_width()
height= self.winfo_height()
x = self.winfo_rootx()
y = self.winfo_rooty()
if mode == 'se' and abs_x >0 and abs_y >0:
self.geometry("%sx%s" % (abs_x,abs_y)
)
if mode == 'e':
self.geometry("%sx%s" % (abs_x,height)
)
if mode == 'ne' and abs_x >0:
y = y+abs_y
height = height-abs_y
if height >0:
self.geometry("%dx%d+%d+%d" % (abs_x,height,
x,y))
if mode == 'n':
height=height-abs_y
y = y+abs_y
if height >0 and width >0:
self.geometry("%dx%d+%d+%d" % (width,height,
x,y))
if mode == 'nw':
width = width-abs_x
height=height-abs_y
x = x+abs_x
y = y+abs_y
if height >0 and width >0:
self.geometry("%dx%d+%d+%d" % (width,height,
x,y))
if mode == 'w':
width = width-abs_x
x = x+abs_x
if height >0 and width >0:
self.geometry("%dx%d+%d+%d" % (width,height,
x,y))
if mode == 'sw':
width = width-abs_x
height=height-(height-abs_y)
x = x+abs_x
if height >0 and width >0:
self.geometry("%dx%d+%d+%d" % (width,height,
x,y))
if mode == 's':
height=height-(height-abs_y)
if height >0 and width >0:
self.geometry("%dx%d+%d+%d" % (width,height,
x,y))
app=FloatingWindow()
app.mainloop()

Related

How to create a button for moving the window without the main button?

I'm french so my English is a little bit bad so don't pay attention.
I have made a tkinter calculator and for that I have delete the top of the window where the title and close button are situated and replace it by a canvas and it looks very nice, but I can no longer move the window on my screen, the window stays in the upper-left corner, and I can't move it in any way...I hope someone would have an idea.
For deleting the top of the window I have used win.overrideredirect(1).
I have tried something but this doesn't work:
import tkinter as tk
win = tk.Tk()
def coordsSouris(event):
# win.geometry()
print(event.x, event.y)
win.after(100, coordsSouris)
can = tk.Canvas(height = 400, width = 400)
can.pack()
can.bind("<Button-1>", coordsSouris)
win.mainloop()
The function misses the arg event and I can't give this argument with after()...
I have another idea: Is it possible to place the tittle bar in front of my canvas and hide it and cute i when it was in front of my exit button and menu ? so we doesnt see it but it work normaly
Set root .geometry( Width x Height + X + Y ).
keep Width & Height the same, just change X & Y parameters.
Edit: Yeah, that's a little more complicated. You need to store the initial drag position, then subtract it from subsequent events. Clear value once the button has been released. You also want to set min / max values to make sure it isn't going off the screen.
#! /usr/bin/env python3
import tkinter
Width, Height, = 100, 33
Xpos, Ypos = 20, 20
dragposX, dragposY = 0, 0
root = tkinter .Tk()
root .geometry( f'{Width}x{Height}+{Xpos}+{Ypos}')
root .overrideredirect( 1 ) ## no titlebar
Screenwidth = root .winfo_screenwidth()
Screenheight = root .winfo_screenheight()
Xmax = Screenwidth -Width
Ymax = Screenheight -Height
fontname = 'ariel'
fontsize = 8
fontstyle = 'normal'
font = fontname, fontsize, fontstyle
def left():
global Xpos ; Xpos -= 5
if Xpos < 0: Xpos = 0
root .geometry( f'{Width}x{Height}+{Xpos}+{Ypos}')
def right():
global Xpos ; Xpos += 5
if Xpos > Xmax: Xpos = Xmax
root .geometry( f'{Width}x{Height}+{Xpos}+{Ypos}')
def up():
global Ypos ; Ypos -= 5
if Ypos < 0: Ypos = 0
root .geometry( f'{Width}x{Height}+{Xpos}+{Ypos}')
def down():
global Ypos ; Ypos += 5
if Ypos > Ymax: Ypos = Ymax
root .geometry( f'{Width}x{Height}+{Xpos}+{Ypos}')
def drag( event ):
global Xpos, Ypos, dragposX, dragposY
if dragposX == 0 and dragposY == 0:
dragposX = event .x
dragposY = event .y
Xpos += event .x -dragposX
Ypos += event .y -dragposY
if Xpos < 0: Xpos = 0
if Ypos < 0: Ypos = 0
if Xpos > Xmax: Xpos = Xmax
if Ypos > Ymax: Ypos = Ymax
root .geometry( f'{Width}x{Height}+{Xpos}+{Ypos}')
def clear( event ):
global dragposX, dragposY
dragposX, dragposY = 0, 0
left_button = tkinter .Button( root, text='<', font=font, padx=2, pady=0, bg='blue', activebackground='lightblue', command=left )
left_button .grid( row=0, column=0, padx=1, pady=4 )
right_button = tkinter .Button( root, text='>', font=font, padx=2, pady=0, bg='blue', activebackground='lightblue', command=right )
right_button .grid( row=0, column=1, padx=1, pady=4 )
down_button = tkinter .Button( root, text='v', font=font, padx=2, pady=0, command=down )
down_button .grid( row=0, column=2, padx=1, pady=4 )
up_button = tkinter .Button( root, text='^', font=font, padx=2, pady=0, command=up )
up_button .grid( row=0, column=3, padx=1, pady=4 )
close_button = tkinter .Button( root, text='X', font=font, padx=2, pady=0, bg='red', activebackground='pink', command=root.destroy )
close_button .grid( row=0, column=4, padx=1, pady=4 )
root .bind( '<B1-Motion>', drag )
root .bind( '<ButtonRelease-1>', clear )
root .mainloop()
Here is a simplified method for moving canvas.
import tkinter as tk
def realcenter( o, w, h ) ->'o(w,h) centered on screen':
x = o.winfo_screenwidth( ) - w
y = o.winfo_screenheight( ) - h
o.geometry( '{0:d}x{1:d}+{2:d}+{3:d}'.format( w, h, int( x/2 ), int( y/2 ) ) )
def restore( ev ):
master.overrideredirect( 0 )
def unrestore( ev ):
master.overrideredirect( 1 )
master = tk.Tk()
sizew, sizeh = 400, 400
canvas_box = tk.Canvas(master, width=sizew, height=sizeh )
canvas_box.grid(row=0,column=0)
master.geometry( '400x400' )
realcenter( master, 400, 400 )
master.update_idletasks()
master.overrideredirect( 1 )
master.bind( '<F1>', restore )
master.bind( '<F2>', unrestore )
tk.mainloop()
I've included a window centering function
A slight modification to make it 2.x compatible.
I Have find the solution and its work perfectly !!!
I hope it will help people to do a personalized title bar!!!
my code :
from tkinter import *
import tkinter as tk
root = Tk()
x = 0
y = 0
def move_window(event):
# global x, y
print(x, y)
#event.x_the_name_of_the_widows is false you have to use x_root
#event if your page is called win or ...
root.geometry('+{0}+{1}'.format(event.x_root - x, event.y_root - y))
root.overrideredirect(True) # turns off title bar, geometry
root.geometry('400x200+200+200') # set new geometry
# make a frame for the title bar
title_bar = Frame(root, bg='white', relief='raised', bd=2)
# put a close button on the title bar
close_button = Button(title_bar, text='X', command=root.destroy)
# a canvas for the main area of the window
window = Canvas(root, bg='black')
# pack the widgets
title_bar.pack(expand=1, fill=X)
close_button.pack(side=RIGHT)
window.pack(expand=1, fill=BOTH)
# find the position of the cursor on the window and not on the screen
def set_xy(event):
global x,y
x=event.x_root - root.winfo_x()
y=event.y_root - root.winfo_y()
# print(x,y)
return x,y;
title_bar.bind('<1>',set_xy)
# bind title bar motion to the move window function
title_bar.bind('<B1-Motion>', move_window)
root.mainloop()

How to make a custom scrollbar in tkinter for frames

I have been using the tkinter scrollbar for my project. But, when I use the properties troughcolor, the color of the scrollbar doesn't change. So, I want to make a custom scrollbar for tkinter and python that can be used to scroll through a frame. I then will add color to this custom scrollbar. Is there any way to do so? Here is my code:
root=Tk()
container = ttk.Frame(root)
canvas = Canvas(container, highlightbackground="black", highlightthickness=1, bg="black", width=400, height=600)
scrollbar = Scrollbar(container, orient="vertical", command=canvas.yview, troughcolor="red")
scrollable_frame = ttk.Frame(canvas)
scrollable_frame.bind(
"<Configure>",
lambda e: canvas.configure(
scrollregion=canvas.bbox("all")
)
)
canvas.create_window((0, 0), window=scrollable_frame, anchor="nw")
canvas.configure(yscrollcommand=scrollbar.set)
container.pack()
canvas.pack(side="left", fill="both", expand=True)
scrollbar.pack(side="right", fill="y")
root.mainloop()
Based on Bryan Oakley's comment and a variety of his posts on SO, and several days of work on my part, here is a pretty complete answer to the question. This scrollbar is configurable and can be optionally hidden when not needed. I am a beginner and this can be improved, but it seems to work OK. I left my comments in, as well as Bryan Oakley's from an original post where he showed part of this code. This was not easy for me to do and the comments might help someone understand it better. I wrote this code a few weeks ago and it's working fine so far.
import tkinter as tk
'''
Much of this code was elucidated by Bryan Oakley on StackOverflow.com.
Without his explanations and examples, I would not have figured out how to
create a configurable Tkinter scrollbar. Any mistakes in this code are mine
of course.
I didn't add the little arrows at the ends of the trough.
'''
class Scrollbar(tk.Canvas):
'''
A scrollbar is gridded as a sibling of what it's scrolling.
'''
def __init__(self, parent, orient='vertical', hideable=False, **kwargs):
print('kwargs is', kwargs)
'''
kwargs is {
'width': 17,
'command': <bound method YView.yview of
<widgets.Text object .!canvas.!frame.!frame.!text>>}
https://stackoverflow.com/questions/15411107
You can use dict.pop:... delete an item in a dictionary only if the given key exists... not certain if key exists in the dictionary...
mydict.pop("key", None)
...if the second argument, None is not given, KeyError is raised if the key is not in the dictionary. Providing the second argument prevents the conditional exception... the second argument to .pop() is what it returns if the key is not found.
'''
self.command = kwargs.pop('command', None)
print('self.command is', self.command)
tk.Canvas.__init__(self, parent, **kwargs)
self.orient = orient
self.hideable = hideable
self.new_start_y = 0
self.new_start_x = 0
self.first_y = 0
self.first_x = 0
self.slidercolor = 'steelblue'
self.troughcolor = 'lightgray'
self.config(bg=self.troughcolor, bd=0, highlightthickness=0)
# coordinates are irrelevant; they will be recomputed
# in the 'set' method
self.create_rectangle(
0, 0, 1, 1,
fill=self.slidercolor,
width=2, # this is border width
outline='teal',
tags=('slider',))
self.bind('<ButtonPress-1>', self.move_on_click)
self.bind('<ButtonPress-1>', self.start_scroll, add='+')
self.bind('<B1-Motion>', self.move_on_scroll)
self.bind('<ButtonRelease-1>', self.end_scroll)
def set(self, lo, hi):
'''
For resizing & repositioning the slider. The hideable
scrollbar portion is by Fredrik Lundh, one of Tkinter's authors.
'''
lo = float(lo)
hi = float(hi)
if self.hideable is True:
if lo <= 0.0 and hi >= 1.0:
self.grid_remove()
return
else:
self.grid()
height = self.winfo_height()
width = self.winfo_width()
if self.orient == 'vertical':
x0 = 2
y0 = max(int(height * lo), 0)
x1 = width - 2
y1 = min(int(height * hi), height)
# This was the tricky part of making a horizontal scrollbar
# when I already knew how to make a vertical one.
# You can't just change all the "height" to "width"
# and "y" to "x". You also have to reverse what x0 etc
# are equal to, comparing code in if and elif. Till that was
# done, everything worked but the horizontal scrollbar's
# slider moved up & down.
elif self.orient == 'horizontal':
x0 = max(int(width * lo), 0)
y0 = 2
x1 = min(int(width * hi), width)
y1 = height
self.coords('slider', x0, y0, x1, y1)
self.x0 = x0
self.y0 = y0
self.x1 = x1
self.y1 = y1
def move_on_click(self, event):
if self.orient == 'vertical':
# don't scroll on click if mouse pointer is w/in slider
y = event.y / self.winfo_height()
if event.y < self.y0 or event.y > self.y1:
self.command('moveto', y)
# get starting position of a scrolling event
else:
self.first_y = event.y
elif self.orient == 'horizontal':
# do nothing if mouse pointer is w/in slider
x = event.x / self.winfo_width()
if event.x < self.x0 or event.x > self.x1:
self.command('moveto', x)
# get starting position of a scrolling event
else:
self.first_x = event.x
def start_scroll(self, event):
if self.orient == 'vertical':
self.last_y = event.y
self.y_move_on_click = int(event.y - self.coords('slider')[1])
elif self.orient == 'horizontal':
self.last_x = event.x
self.x_move_on_click = int(event.x - self.coords('slider')[0])
def end_scroll(self, event):
if self.orient == 'vertical':
self.new_start_y = event.y
elif self.orient == 'horizontal':
self.new_start_x = event.x
def move_on_scroll(self, event):
# Only scroll if the mouse moves a few pixels. This makes
# the click-in-trough work right even if the click smears
# a little. Otherwise, a perfectly motionless mouse click
# is the only way to get the trough click to work right.
# Setting jerkiness to 5 or more makes very sloppy trough
# clicking work, but then scrolling is not smooth. 3 is OK.
jerkiness = 3
if self.orient == 'vertical':
if abs(event.y - self.last_y) < jerkiness:
return
# scroll the scrolled widget in proportion to mouse motion
# compute whether scrolling up or down
delta = 1 if event.y > self.last_y else -1
# remember this location for the next time this is called
self.last_y = event.y
# do the scroll
self.command('scroll', delta, 'units')
# afix slider to mouse pointer
mouse_pos = event.y - self.first_y
if self.new_start_y != 0:
mouse_pos = event.y - self.y_move_on_click
self.command('moveto', mouse_pos/self.winfo_height())
elif self.orient == 'horizontal':
if abs(event.x - self.last_x) < jerkiness:
return
# scroll the scrolled widget in proportion to mouse motion
# compute whether scrolling left or right
delta = 1 if event.x > self.last_x else -1
# remember this location for the next time this is called
self.last_x = event.x
# do the scroll
self.command('scroll', delta, 'units')
# afix slider to mouse pointer
mouse_pos = event.x - self.first_x
if self.new_start_x != 0:
mouse_pos = event.x - self.x_move_on_click
self.command('moveto', mouse_pos/self.winfo_width())
def colorize(self):
print('colorize')
self.slidercolor = 'blue'
self.troughcolor = 'bisque'
self.config(bg=self.troughcolor)
if __name__ == '__main__':
def resize_scrollbar():
root.update_idletasks()
canvas.config(scrollregion=canvas.bbox('all'))
def resize_window():
root.update_idletasks()
page_x = content.winfo_reqwidth()
page_y = content.winfo_reqheight()
root.geometry('{}x{}'.format(page_x, page_y))
root = tk.Tk()
root.config(bg='yellow')
root.grid_columnconfigure(0, weight=1)
root.grid_rowconfigure(0, weight=1)
root.grid_rowconfigure(1, weight=0)
canvas = tk.Canvas(root, bg='tan')
canvas.grid(column=0, row=0, sticky='news')
content = tk.Frame(canvas)
content.grid_columnconfigure(0, weight=1)
content.grid_rowconfigure(0, weight=1)
ysb_canv = Scrollbar(root, width=24, hideable=True, command=canvas.yview)
xsb_canv = Scrollbar(root, height=24, hideable=True, command=canvas.xview, orient='horizontal')
canvas.config(yscrollcommand=ysb_canv.set, xscrollcommand=xsb_canv.set)
frame = tk.Frame(content)
frame.grid_columnconfigure(0, weight=0)
frame.grid_rowconfigure(0, weight=1)
text = tk.Text(frame, bd=0)
ysb_txt = Scrollbar(frame, width=17, command=text.yview)
text.config(yscrollcommand=ysb_txt.set)
space = tk.Frame(content, width=1200, height=500)
ysb_canv.grid(column=1, row=0, sticky='ns')
xsb_canv.grid(column=0, row=1, sticky='ew')
frame.grid(column=0, row=0, sticky='news')
text.grid(column=0, row=0)
ysb_txt.grid(column=1, row=0, sticky='ns')
space.grid(column=0, row=1)
with open(__file__, 'r') as f:
text.insert('end', f.read())
canvas.create_window(0, 0, anchor='nw', window=content)
resize_scrollbar()
resize_window()
root.mainloop()

How to make child Tkinter object trigger a change in parent's widget

Edit: thanks to #jasonharper's comments below I can ask a more informed question:
I have a main app, and a separate module snipping_tool.py that handles creating a new window with the option to screen-snip or select an image file. I want snipping_tool.py to provide an image to the main app, but currently I'm trying to retrieve the image too soon (before the snipping_tool window even opens).
How can I wait until the user selects or grabs an image before I try to assign MyNewObject.selected_image? Should I use a binding or some event handler? (I have limited experience with both). Or is there a simpler way?
Simplified Main App:
import tkinter as tk
import snipping_tool
class MinCodeEx:
def __init__(self, master):
self.master = master
self.ButtonA = tk.Button(width=60,height=40,command = lambda: self.UpdateImg(master))
self.ButtonA.pack()
def UpdateImg(self, master):
newDialog = snipping_tool.AcquireImage(self.master)
# self.ButtonA['image'] = newDialog.image_selected
#if newDialog.image_selected:
self.ButtonA.config(image=newDialog.image_selected)
print(newDialog.image_selected)
def main():
root = tk.Tk()
MinCodeEx(root)
root.mainloop()
if __name__ == '__main__':
main()
snipping_tool.py
returns None instead of an image file since I'm trying to retrieve the selected_image too soon.
import tkinter as tk
from PIL import ImageGrab, ImageTk, Image
import cv2
import numpy as np
from tkinter import filedialog
class ScreenSnip(tk.Toplevel):
def __init__(self, master):
super().__init__(master)
self.image = None
def get_snip(self):
self.configure(cursor='cross')
self.attributes('-fullscreen', True)
self.attributes('-alpha', 0.4)
self.canvas = tk.Canvas(self, bg='dark gray')
self.canvas.pack(fill='both', expand=True)
self.begin_x = 0
self.begin_y = 0
self.end_x = 0
self.end_y = 0
self.click_drag = False
self.canvas.create_rectangle(0, 0, 0, 0, outline='#0052d6', width=2, fill='white', tags='snip_rect')
self.canvas.bind('<ButtonPress-1>', self.mousePressEvent)
self.canvas.bind('<B1-Motion>', self.mouseMoveEvent)
self.canvas.bind('<ButtonRelease-1>', self.mouseReleaseEvent)
print('Capture the screen...')
def mousePressEvent(self, event):
self.begin_x = event.x
self.begin_y = event.y
print(self.begin_x,self.begin_y)
def mouseMoveEvent(self, event):
self.click_drag = True
self.end_x = event.x
self.cur_y = event.y
width = self.end_x - self.begin_x
height = abs(width * 2/3)
if self.cur_y < self.begin_y:
height *= -1
self.end_y = self.begin_y + height
self.canvas.coords('snip_rect', self.begin_x, self.begin_y, self.end_x, self.end_y)
def mouseReleaseEvent(self, event):
self.destroy()
self.master.update_idletasks()
self.master.after(100) # give time for screen to be refreshed so as not to see the blue box on the screenshot
if not self.click_drag: # if the user just clicks, instead of clicking and dragging
self.begin_x -= 300
self.begin_y += 200
self.end_x = self.begin_x + 600
self.end_y = self.begin_y - 400
x1 = min(self.begin_x, self.end_x)
y1 = min(self.begin_y, self.end_y)
x2 = max(self.begin_x, self.end_x)
y2 = max(self.begin_y, self.end_y)
self.img = ImageGrab.grab(bbox=(x1, y1, x2, y2))
self.image = ImageTk.PhotoImage(self.img)
#self.img = cv2.cvtColor(np.array(img), cv2.COLOR_BGR2RGB)
#cv2.imshow('Captured Image', self.img)
#cv2.waitKey(0)
font1 = ("arial", 18, "bold")
class AcquireImage:
def __init__(self, master):
self.master = master
self.nWin = tk.Toplevel(master)
self.fontA = ("arial", 20, "bold")
self.frame = tk.Frame(self.nWin, bg="#1B2631")
self.frame.pack(fill="both", expand=True)
self.button1 = tk.Button(self.frame, text="Select Image File", padx=10, pady=10, bg="#d9a193",
font = self.fontA, command =lambda: self.show_dialogs(1))
self.button1.grid(row=0, column=0, sticky="nsew")#, padx=10, pady=10)
self.button2 = tk.Button(self.frame, text="Get Screen Snip", padx=10, pady=10, bg="#d9a193",
font = self.fontA, command=lambda: self.show_dialogs(2))
self.button2.grid(row=0, column=1, sticky="nsew")#, padx=10, pady=10)
self.image_selected = None
def show_dialogs(self, method): ################### THIS IS WHERE THE IMAGE IS SELECTED ###########
if method == 1:
ret = filedialog.askopenfilename()
if ret:
self.image_selected = ImageTk.PhotoImage(file = ret)
self.nWin.destroy()
elif method == 2:
newWin = ScreenSnip(self.nWin)
newWin.get_snip()
ret = newWin.image
if ret:
self.image_selected = ret
def main():
root = tk.Tk()
AcquireImage(root)
root.mainloop()
if __name__ == '__main__':
main()
#jasonharper was right. This solution is derived from his comments.
When I create an instance of AcquireImage I also pass ButtonA as a parameter so that I can modify its image. Within AcquireImage I first get a new image (either by snipping it, or with the file explorer) and then to avoid garbage collection immediately deleting it, I save it by assigning it to ButtonA.img. (I first create the .img part of ButtonA in the main program). Once I have the image, I can then assign it.
I probably could have also solved this issue by creating a function
within the main program that changes the image of whatever widget is
passed to it as a parameter. I could then pass this function to the
instance of AcquireImage with ButtonA as a parameter. It might
have looked something like this in the main program: def callback(i_file): ButtonA['image'] = i_file newDialog = snipping_tool.AcquireImage(self.master, self.callback) Later I
would have AcquireImage initialized/defined with
self.some_function as the second argument (after master), and I
could pass the image file to it. At least that's how I probably could
have done it.
This is how I actually solved it:
MAIN APP
import tkinter as tk
import snipping_tool
waitingforImage = True
class MinCodeEx:
global waitingforImage
def __init__(self, master):
self.master = master
self.ButtonA = tk.Button(width=60,height=40,command = lambda: self.UpdateImg())
self.ButtonA.pack()
self.ButtonA.img = None
def UpdateImg(self):
newDialog = snipping_tool.AcquireImage(self.master, self.ButtonA)
def main():
root = tk.Tk()
MinCodeEx(root)
root.mainloop()
if __name__ == '__main__':
main()
snipping_tool.py
import tkinter as tk
from PIL import ImageGrab, ImageTk, Image
from tkinter import filedialog
class ScreenSnip(tk.Toplevel):
def __init__(self, master, changeThis):
super().__init__(master)
self.image = None
self.master = master
self.changeThis = changeThis
def get_snip(self):
self.configure(cursor='cross')
self.attributes('-fullscreen', True)
self.attributes('-alpha', 0.4)
print("attempting to create tk.Canvas for get_snip")
self.canvas = tk.Canvas(self, bg='dark gray')
self.canvas.pack(fill='both', expand=True)
self.begin_x = 0
self.begin_y = 0
self.end_x = 0
self.end_y = 0
self.click_drag = False
self.canvas.create_rectangle(0, 0, 0, 0, outline='#0052d6', width=2, fill='white', tags='snip_rect')
self.canvas.bind('<ButtonPress-1>', self.mousePressEvent)
self.canvas.bind('<B1-Motion>', self.mouseMoveEvent)
self.canvas.bind('<ButtonRelease-1>', self.mouseReleaseEvent)
print('Capture the screen...')
def mousePressEvent(self, event):
self.begin_x = event.x
self.begin_y = event.y
print(self.begin_x,self.begin_y)
def mouseMoveEvent(self, event):
self.click_drag = True
self.end_x = event.x
self.cur_y = event.y
width = self.end_x - self.begin_x
height = abs(width * 2/3)
if self.cur_y < self.begin_y:
height *= -1
self.end_y = self.begin_y + height
self.canvas.coords('snip_rect', self.begin_x, self.begin_y, self.end_x, self.end_y)
def mouseReleaseEvent(self, event):
self.destroy()
self.master.update_idletasks()
self.master.after(100) # give time for screen to be refreshed so as not to see the blue box on the screenshot
if not self.click_drag: # if the user just clicks, instead of clicking and dragging
self.begin_x -= 300
self.begin_y += 200
self.end_x = self.begin_x + 600
self.end_y = self.begin_y - 400
x1 = min(self.begin_x, self.end_x)
y1 = min(self.begin_y, self.end_y)
x2 = max(self.begin_x, self.end_x)
y2 = max(self.begin_y, self.end_y)
self.img = ImageGrab.grab(bbox=(x1, y1, x2, y2))
print("getting image grab")
self.changeThis.img = ImageTk.PhotoImage(self.img)
self.changeThis['image'] = self.changeThis.img
font1 = ("arial", 18, "bold")
class AcquireImage:
def __init__(self, master, changeThis):
self.master = master
self.changeThis = changeThis
self.nWin = tk.Toplevel(master)
self.fontA = ("arial", 20, "bold")
self.frame = tk.Frame(self.nWin, bg="#1B2631")
self.frame.pack(fill="both", expand=True)
self.button1 = tk.Button(self.frame, text="Select Image File", padx=10, pady=10, bg="#d9a193",
font = self.fontA, command =lambda: self.show_dialogs(1))
self.button1.grid(row=0, column=0, sticky="nsew")#, padx=10, pady=10)
self.button2 = tk.Button(self.frame, text="Get Screen Snip", padx=10, pady=10, bg="#d9a193",
font = self.fontA, command=lambda: self.show_dialogs(2))
self.button2.grid(row=0, column=1, sticky="nsew")#, padx=10, pady=10)
self.image_selected = None
def show_dialogs(self, method):
if method == 1:
ret = filedialog.askopenfilename() #filedialog.askopenfilename(initialdir='/home/user/images/')
self.changeThis.img = ImageTk.PhotoImage(file = ret)
self.changeThis['image'] = self.changeThis.img
elif method == 2:
print("attempting ScreenSnip")
newWin = ScreenSnip(self.master, self.changeThis)
newWin.get_snip()
self.nWin.destroy()
def main():
root = tk.Tk()
bt = tk.Button(root, width = 20, height = 20)
bt.pack()
ScreenSnip(root, bt)
#AcquireImage(root,bt)
root.mainloop()
if __name__ == '__main__':
main()
I still need to fix one unrelated thing -- the button doesn't fit the size of the image that I select or grab. It always ends up being a small square.

AttributeError: '_tkinter.tkapp' object has no attribute 'mousePressEvent'

I'm having trouble with two tkinter classes that I want to interact. I think it comes from my lack of understanding of the Tk.Toplevel. Instead of creating an entirely new full-screen window for screen-grabbing, my code gives an error:
AttributeError: '_tkinter.tkapp' object has no attribute 'mousePressEvent'
Can someone help me understand the hierarchy of parent-child relationships in my code? I'm not asking anyone to rewrite my code, I just want to understand what I already have, so I can figure out what's wrong. I haven't found many examples in SO of multiple classes that interact with multiple windows - so it's hard to grasp the inter-workings of inheritance.
This is the structure I think I have:
root
object: AquireImage
method: show_dialogs()
object: ScreenSnip
method: get_snip()
method: mousePressEvent()
method: mouseMoveEvent()
method: mouseReleaseEvent()
Is this accurate?
CODE
import tkinter as tk
from PIL import ImageGrab, ImageTk, Image
import cv2
import numpy as np
from tkinter import filedialog
class ScreenSnip(tk.Toplevel):
def __init__(self, master):
super().__init__(master)
def get_snip(self):
self.configure(cursor='cross')
self.attributes('-fullscreen', True)
self.attributes('-alpha', 0.4)
self.canvas = tk.Canvas(self, bg='dark gray')
self.canvas.pack(fill='both', expand=True)
self.begin_x = 0
self.begin_y = 0
self.end_x = 0
self.end_y = 0
self.click_drag = False
self.canvas.create_rectangle(0, 0, 0, 0, outline='#0052d6', width=2, fill='white', tags='snip_rect')
self.canvas.bind('<ButtonPress-1>', self.mousePressEvent)
self.canvas.bind('<B1-Motion>', self.mouseMoveEvent)
self.canvas.bind('<ButtonRelease-1>', self.mouseReleaseEvent)
print('Capture the screen...')
def mousePressEvent(self, event):
self.begin_x = event.x
self.begin_y = event.y
print(self.begin_x,self.begin_y)
def mouseMoveEvent(self, event):
self.click_drag = True
self.end_x = event.x
self.cur_y = event.y
width = self.end_x - self.begin_x
height = abs(width * 2/3)
if self.cur_y < self.begin_y:
height *= -1
self.end_y = self.begin_y + height
self.canvas.coords('snip_rect', self.begin_x, self.begin_y, self.end_x, self.end_y)
def mouseReleaseEvent(self, event):
self.destroy()
self.master.update_idletasks()
self.master.after(100) # give time for screen to be refreshed so as not to see the blue box on the screenshot
if not self.click_drag: # if the user just clicks, instead of clicking and dragging
self.begin_x -= 300
self.begin_y += 200
self.end_x = self.begin_x + 600
self.end_y = self.begin_y - 400
x1 = min(self.begin_x, self.end_x)
y1 = min(self.begin_y, self.end_y)
x2 = max(self.begin_x, self.end_x)
y2 = max(self.begin_y, self.end_y)
img = ImageGrab.grab(bbox=(x1, y1, x2, y2))
self.img = cv2.cvtColor(np.array(img), cv2.COLOR_BGR2RGB)
cv2.imshow('Captured Image', self.img)
cv2.waitKey(0)
font1 = ("arial", 18, "bold")
class AcquireImage:
def __init__(self, master):
self.master = master
self.fontA = ("arial", 20, "bold")
self.frame = tk.Frame(master, bg="#1B2631")
self.frame.pack(fill="both", expand=True)
self.button1 = tk.Button(self.frame, text="Select Image File", padx=10, pady=10, bg="#d9a193",
font = self.fontA, command =lambda: self.show_dialogs(1))
self.button1.grid(row=0, column=0, sticky="nsew")#, padx=10, pady=10)
self.button2 = tk.Button(self.frame, text="Get Screen Snip", padx=10, pady=10, bg="#d9a193",
font = self.fontA, command=lambda: self.show_dialogs(2))
self.button2.grid(row=0, column=1, sticky="nsew")#, padx=10, pady=10)
self.image_selected = None
def show_dialogs(self, method):
if method == 1:
ret = filedialog.askopenfilename() #filedialog.askopenfilename(initialdir='/home/user/images/')
if ret:
self.image_selected = ImageTk.PhotoImage(file = ret)
self.master.destroy()
elif method == 2:
newWin = ScreenSnip.get_snip(self.master)
ret = newWin.img
if ret:
self.image_selected = ImageTk.PhotoImage(file = ret)
def main():
root = tk.Tk()
AcquireImage(root)
root.mainloop()
if __name__ == '__main__':
main()
This answer is derived from #jasonharper's comment:
Instances of ScreenSnip would indeed have a mousePressEvent
attribute. But you never create any such instance; instead, you
attempt to call get_snip() on the class itself, which ends up
providing a completely inappropriate value for its self parameter.
The mistake I made was here:
newWin = ScreenSnip.get_snip(self.master)
I tried to create a new object newWin without putting the parent in the correct place, and while calling the method get_snip at the same time. This is what solved the issue:
newWin = ScreenSnip(self.master)
newWin.get_snip()
First create the object ScreenSnip from the class, with the parent
then call the method `get_snip'

create a python dictionary after collecting data from Tkinter image

i am trying to create a list of dictionaries, based on the information I take out from the image(coordinates and image type) by selecting it with a rectangle. On button release, i want to append the dictionary extracted to an empty list. The code works fine for the first dictionary, but when i select the second triangle the dictionary i obtain overrides the first one.
Could you please come up with a solution so that in the end i get the list of dictionaries like this:
[{'bottom_right_coords': [447, 349], 'type': 'middle name', 'top_left_coords': [290, 311]}, {'bottom_right_coords': [447, 349], 'type': 'first name', 'top_left_coords': [290, 311]}, etc etc etc. ]
import Tkinter as tk
from Tkinter import *
import PIL
from PIL import Image, ImageTk
import pygame
import Pmw
from collections import OrderedDict
pygame.init()
global d, D, dict_list
global DEFAULTVALUE_OPTION
global options
DEFAULTVALUE_OPTION = "Select an option"
options = ['address',
'name',
'occupation']
d = {}
dict_list = [None] * 2000
list = range(2000)
D = OrderedDict.fromkeys(list)
class ExampleApp(tk.Tk):
def __init__(self, parent = None):
tk.Tk.__init__(self, parent)
self.x = self.y = 0
self.canvas = tk.Canvas(self, width=700, height=700, cursor="cross", relief=SUNKEN)
self.canvas.bind("<ButtonPress-1>", self.on_button_press)
self.canvas.bind("<B1-Motion>", self.on_move_press)
self.canvas.bind("<ButtonRelease-1>", self.on_button_release)
self.rect = None
self.start_x = None
self.start_y = None
self._draw_image()
def _draw_image(self):
self.sbarV = Scrollbar(self, orient=VERTICAL)
self.sbarH = Scrollbar(self, orient=HORIZONTAL)
self.sbarV.config(command=self.canvas.yview)
self.sbarH.config(command=self.canvas.xview)
self.canvas.config(yscrollcommand=self.sbarV.set)
self.canvas.config(xscrollcommand=self.sbarH.set)
self.sbarV.pack(side=RIGHT, fill=Y)
self.sbarH.pack(side=BOTTOM, fill=X)
self.canvas.pack(side="top", fill="both", expand=True)
self.im = Image.open("/home/madalina/madalina/image/page-001.jpg")
width, height = self.im.size
self.canvas.config(scrollregion=(0, 0, width, height))
self.tk_im = ImageTk.PhotoImage(self.im)
self.canvas.create_image(0,0,anchor="nw",image=self.tk_im)
def on_button_press(self, event):
# save mouse drag start position
self.start_x = event.x
self.start_y = event.y
d["top_left_coords"] = [self.start_x, self.start_y]
# create rectangle if not yet exist
#if not self.rect:
self.rect = self.canvas.create_rectangle(self.x, self.y, 100, 100, width = 2, outline = "gold")
def on_move_press(self, event):
curX, curY = (event.x, event.y)
# expand rectangle as you drag the mouse
self.canvas.coords(self.rect, self.start_x, self.start_y, curX, curY)
def on_button_release(self, event):
top = Tk()
Label(top, text="New Option").grid(row=0)
Label(top, text = "Coordinates").grid(row=1)
E1 = Entry(top, bd = 5)
E2 = Entry(top, bd = 5)
# e1 = E1.get()
# e2 = E2.get()
E1.grid(row=0, column=1)
E2.grid(row=1, column=1)
def addOption():
d["type"] = E1.get()
print d
def createDict():
dict_list.append(d)
print "lista de dictionare este " + str(dict_list)
B1 = Button(top, text = "ADD", command = addOption)
B1.grid(row=3, column=0, sticky=W, pady=4)
B1 = Button(top, text = "Create Dict", command = createDict)
B1.grid(row=3, column=1, sticky=W, pady=4)
d["bottom_right_coords"] = [event.x, event.y]
if __name__ == "__main__":
app = ExampleApp()
app.mainloop()
Finding your Problem
Let's look at this function here
def on_button_press(self, event):
# save mouse drag start position
self.start_x = event.x
self.start_y = event.y
d["top_left_coords"] = [self.start_x, self.start_y]
I like those first two lines. So I suspect that last one is the culpret.
d["top_left_coords"] = [self.start_x, self.start_y]
Not Your Problem But Worth Mentioning
My first impression was: "d is undeclared, so this will crash". Not so, since you've declared it as a global variable. So my first recomendation is: make sure you definately need a global variable there. As this 2000+ voted answer says
I imagine the reason for it is that, since global variables are so dangerous, Python wants to make sure that you really know that's what you're playing with by explicitly requiring the global keyword.
Here's somewhere to start if you want to remove globals.
If you decide global variable is the way to go, please please name it something more helpful than d
But Onto Your problem
Make d a list of dictionaries and append to it. And by the way, tuples might make more sense than lists for coordinates, since they're immutable
d = []
...
d.append({self.start_x,self.start_y})
...
#retrieving last click
print d[-1]
# adding other things to the 'last click dictionary'
d[-1]['type'] = 'foobar'

Categories