How to make a custom scrollbar in tkinter for frames - python

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()

Related

I need to make a fill function on Canvas Tkinter

I can't get the x or y of where the user has drawn already.
I'd like to be able to press a fill button then press in the area, and if a circle has been drawn for example, it would fill with a certain colour.
def fill(event):
global colour
for x in range(x1,x2):
x = canvas.create_oval(x1,y1,x2,y2,fill=colour)
To fill an existing element in the canvas there is .itemconfig:
def fill(canvas, element_id, event):
canvas.itemconfig(element_id, fill=colour)
To bind it to an event there is .tag_bind, to which we can pass the unique id of our circle and the above function with the corresponding arguments:
canvas.tag_bind(circle, "<Button-1>", lambda e: fill(canvas, circle, e))
Where "<Button-1>" is the event of a left-click
We can then make it a class to make it cleaner:
class FillableOval:
def __init__(self, canvas, x0, y0, x1, y1, **kwargs):
self.canvas = canvas
self.id = self.canvas.create_oval(x0, y0, x1, y1, **kwargs)
self.canvas.tag_bind(self.id, "<Button-1>", self.fill)
def fill(self, event):
self.canvas.itemconfig(self.id, fill=colour)
I am not good at English.
so
I'll show you a simple example code I made.
import tkinter as tk
def draw_start(event):
line_points.extend((event.x, event.y))
draw_obj = draw_variety.get()
if draw_obj == 'circle':
global obj_id
obj_id = canvas.create_oval(line_points[0], line_points[1], line_points[0],
line_points[1], width=1, outline='black')
if draw_obj == 'fill':
fill(event)
def state_draw_op(event):
draw_obj=draw_variety.get()
if draw_obj == 'circle':
global obj_id
canvas.coords(obj_id, line_points[0], line_points[1], event.x, event.y)
def end_draw(event=None):
draw_obj = draw_variety.get()
if draw_obj == 'circle':
global obj_id
canvas.coords(obj_id, line_points[0], line_points[1], event.x, event.y)
line_points.clear()
def fill(event):
color = color_variety.get()
item = canvas.find_closest(event.x, event.y)
canvas.itemconfigure(item, fill=color)
line_points=[]
root=tk.Tk()
canvas=tk.Canvas(root, relief="solid", bd=2)
canvas.pack()
draw_variety=tk.StringVar()
color_variety=tk.StringVar()
radio_red = tk.Radiobutton(root, value='red', indicatoron=False, relief='flat', bg='red',
height=2, width=5,
overrelief='flat', activebackground='red', highlightcolor='red',
highlightbackground='red', selectcolor='red',
variable=color_variety)
radio_blue = tk.Radiobutton(root, value='blue', indicatoron=False, relief='flat', bg='blue',
height=2, width=5,
overrelief='flat', activebackground='blue', highlightcolor='blue',
highlightbackground='blue', selectcolor='blue',
variable=color_variety)
radio_cir = tk.Radiobutton(root, text='ㅇ', value='circle', indicatoron=False, relief='flat',
font=('Consolas', 20,),
overrelief='flat', command=None,
variable=draw_variety,
selectcolor='red')
radio_fill = tk.Radiobutton(root, text='fill', value='fill', indicatoron=False, relief='flat',
font=('Consolas', 20,),
overrelief='flat', command=None,
variable=draw_variety,
selectcolor='red')
radio_red.pack()
radio_blue.pack()
radio_cir.pack()
radio_fill.pack()
canvas.bind('<Button-1>', draw_start)
canvas.bind('<B1-Motion>', state_draw_op)
canvas.bind('<ButtonRelease-1>', end_draw)
root.mainloop()
In this case, it would be better to use a class, but for the sake of simplicity I used a global variable.
I copied your code and modified it into my code workaround.
No need to do global and add a constant thickness.
Do not use the keyword fill. It is valid for Python.
Snippet:
_COLOR = "black"
T = 2
def paint(event):
x1, y1 = (event.x - T), (event.y - T)
x2, y2 = (event.x + T), (event.y + T)
x = canvas.create_oval(x1,y1,x2,y2,fill=_COLOR)
Screenshot:

How to change size of tkinter canvas text item while zooming in or out? [duplicate]

I was trying to understand how the scaling of canvas works.
Take for example, the following code. Why is that canvas.scale("all", ...), which is bind to mouse wheel, is scaling all the rectangles and not the text as well.
How can I achieve scaling of text along with the rectangles?
import Tkinter as tk
import random
pressed = False
class Example(tk.Frame):
def __init__(self, root):
tk.Frame.__init__(self, root)
self.canvas = tk.Canvas(self, width=400, height=400, background="bisque")
self.xsb = tk.Scrollbar(self, orient="horizontal", command=self.canvas.xview)
self.ysb = tk.Scrollbar(self, orient="vertical", command=self.canvas.yview)
self.canvas.configure(yscrollcommand=self.ysb.set, xscrollcommand=self.xsb.set)
self.canvas.configure(scrollregion=(0,0,1000,1000))
self.xsb.grid(row=1, column=0, sticky="ew")
self.ysb.grid(row=0, column=1, sticky="ns")
self.canvas.grid(row=0, column=0, sticky="nsew")
self.grid_rowconfigure(0, weight=1)
self.grid_columnconfigure(0, weight=1)
#Plot some rectangles
for n in range(50):
x0 = random.randint(0, 900)
y0 = random.randint(50, 900)
x1 = x0 + random.randint(50, 100)
y1 = y0 + random.randint(50,100)
color = ("red", "orange", "yellow", "green", "blue")[random.randint(0,4)]
self.canvas.create_rectangle(x0,y0,x1,y1, outline="black", fill=color, activefill="black", tags=n)
self.canvas.create_text(50,10, anchor="nw", text="Click and drag to move the canvas\nScroll to zoom.")
# This is what enables using the mouse:
self.canvas.bind("<ButtonPress-1>", self.move_start)
self.canvas.bind("<B1-Motion>", self.move_move)
self.canvas.bind("<ButtonPress-2>", self.pressed2)
self.canvas.bind("<Motion>", self.move_move2)
#linux scroll
self.canvas.bind("<Button-4>", self.zoomerP)
self.canvas.bind("<Button-5>", self.zoomerM)
#windows scroll
self.canvas.bind("<MouseWheel>",self.zoomer)
# Hack to make zoom work on Windows
root.bind_all("<MouseWheel>",self.zoomer)
#move
def move_start(self, event):
self.canvas.scan_mark(event.x, event.y)
def move_move(self, event):
self.canvas.scan_dragto(event.x, event.y, gain=1)
#move
def pressed2(self, event):
global pressed
pressed = not pressed
self.canvas.scan_mark(event.x, event.y)
def move_move2(self, event):
if pressed:
self.canvas.scan_dragto(event.x, event.y, gain=1)
#windows zoom
def zoomer(self,event):
if (event.delta > 0):
self.canvas.scale("all", event.x, event.y, 1.1, 1.1)
elif (event.delta < 0):
self.canvas.scale("all", event.x, event.y, 0.9, 0.9)
self.canvas.configure(scrollregion = self.canvas.bbox("all"))
#linux zoom
def zoomerP(self,event):
self.canvas.scale("all", event.x, event.y, 1.1, 1.1)
self.canvas.configure(scrollregion = self.canvas.bbox("all"))
def zoomerM(self,event):
self.canvas.scale("all", event.x, event.y, 0.9, 0.9)
self.canvas.configure(scrollregion = self.canvas.bbox("all"))
if __name__ == "__main__":
root = tk.Tk()
Example(root).pack(fill="both", expand=True)
root.mainloop()
The Scaling of text is not "really" a scaling of text. It is (at least with the Canvas-widget) a change of font.
From the documentation :
This method will not change the size of a text item, but may move it.
Edit:
This behaviour also applies to images (created with Tkinter.Canvas.create_image(...)) and windows (created with Tkinter.Canvas.create_window(...)). As the documentation defines scaling as
.scale(tagOrId, xOffset, yOffset, xScale, yScale)
Scale all objects according to their distance from a point P=(xOffset, yOffset). The scale factors xScale and yScale are based on a value of 1.0, which means no scaling. Every point in the objects selected by tagOrId is moved so that its x distance from P is multiplied by xScale and its y distance is multiplied by yScale.
Please note the italic part for how scaling works.
End Edit
If you want to perform an increase of font-size you need to configure all the text elements by yourself using the itemconfigure method.
To Implement it in a very functional way you could use tags to identify all text elements. Increasing can happen not only absolute but also relatively if you first get the fontsize (itemcget) and then increase it depending on scale factor.
I know this post is old but I solved a similar question so I thought I would post a solution for the problem as well.
The idea is to add a tag to the text item and then reconfigure it each time the zoomer is called. Note, this solution does not change the linux zoom (zoomerP)
import tkinter as tk
import random
pressed = False
class Example(tk.Frame):
def __init__(self, root):
tk.Frame.__init__(self, root)
self.canvas = tk.Canvas(self, width=400, height=400, background="bisque")
self.xsb = tk.Scrollbar(self, orient="horizontal", command=self.canvas.xview)
self.ysb = tk.Scrollbar(self, orient="vertical", command=self.canvas.yview)
self.canvas.configure(yscrollcommand=self.ysb.set, xscrollcommand=self.xsb.set)
self.canvas.configure(scrollregion=(0,0,1000,1000))
self.xsb.grid(row=1, column=0, sticky="ew")
self.ysb.grid(row=0, column=1, sticky="ns")
self.canvas.grid(row=0, column=0, sticky="nsew")
self.grid_rowconfigure(0, weight=1)
self.grid_columnconfigure(0, weight=1)
self.fontSize = 10
#Plot some rectangles
for n in range(50):
x0 = random.randint(0, 900)
y0 = random.randint(50, 900)
x1 = x0 + random.randint(50, 100)
y1 = y0 + random.randint(50,100)
color = ("red", "orange", "yellow", "green", "blue")[random.randint(0,4)]
self.canvas.create_rectangle(x0,y0,x1,y1, outline="black", fill=color, activefill="black", tags=n)
self.canvas.create_text(50,10, anchor="nw", text="Click and drag to move the canvas\nScroll to zoom.", font = ("Helvetica", int(self.fontSize)), tags="text")
# This is what enables using the mouse:
self.canvas.bind("<ButtonPress-1>", self.move_start)
self.canvas.bind("<B1-Motion>", self.move_move)
self.canvas.bind("<ButtonPress-2>", self.pressed2)
self.canvas.bind("<Motion>", self.move_move2)
#linux scroll
self.canvas.bind("<Button-4>", self.zoomerP)
self.canvas.bind("<Button-5>", self.zoomerM)
#windows scroll
self.canvas.bind("<MouseWheel>",self.zoomer)
# Hack to make zoom work on Windows
root.bind_all("<MouseWheel>",self.zoomer)
#move
def move_start(self, event):
self.canvas.scan_mark(event.x, event.y)
def move_move(self, event):
self.canvas.scan_dragto(event.x, event.y, gain=1)
#move
def pressed2(self, event):
global pressed
pressed = not pressed
self.canvas.scan_mark(event.x, event.y)
def move_move2(self, event):
if pressed:
self.canvas.scan_dragto(event.x, event.y, gain=1)
#windows zoom
def zoomer(self,event):
if (event.delta > 0):
self.canvas.scale("all", event.x, event.y, 1.1, 1.1)
self.fontSize = self.fontSize * 1.1
elif (event.delta < 0):
self.canvas.scale("all", event.x, event.y, 0.9, 0.9)
self.fontSize = self.fontSize * 0.9
self.canvas.configure(scrollregion = self.canvas.bbox("all"))
for child_widget in self.canvas.find_withtag("text"):
self.canvas.itemconfigure(child_widget, font=("Helvetica", int(self.fontSize)))
print(self.fontSize)
#linux zoom
def zoomerP(self,event):
self.canvas.scale("all", event.x, event.y, 1.1, 1.1)
self.canvas.configure(scrollregion = self.canvas.bbox("all"))
def zoomerM(self,event):
self.canvas.scale("all", event.x, event.y, 0.9, 0.9)
self.canvas.configure(scrollregion = self.canvas.bbox("all"))
if __name__ == "__main__":
root = tk.Tk()
Example(root).pack(fill="both", expand=True)
root.mainloop()

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()

Tkinter - Floating Window - Resize

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()

zooming on a tkinter canvas with lines created by live data

I am new to Tkinter and trying to figure out how to be able to zoom in and out on a canvas without messing up the drawn lines created by live data. Every second a new line is drawn and is supposed to be connected to the previous one, but if I zoom in on the canvas the next line that will be drawn is not connected to the previous one. It seems like all the lines are drawn based on some window coordinates (which are consistent no matter how much I zoom in or out), instead of being based on the canvas itself. I want to be able to both zoom in and zoom out as much as I want and still see a line being drawn from the upper left corner of the canvas to the bottom right corner of the canvas. I have tried to provide some code to make it easier to understand.
import tkinter as tk
import threading
import time
root = tk.Tk()
pressed = False
flag_run = False
GRID_W = 500
GRID_H = 500
def thread_entry(name):
print("<starting thread>")
i = 0
j = 0
while flag_run:
time.sleep(1)
canvas.create_line(i, j, i+1, j+1, fill="red")
i += 1
j += 1
print("<ending thread>")
def start():
global flag_run
flag_run = True
global thread
thread = threading.Thread(target=thread_entry, args=(1,))
thread.start()
def stop():
global flag_run
if flag_run is True:
flag_run = False
global thread
thread.join(timeout=0.1)
# move
def move_start(event):
canvas.scan_mark(event.x, event.y)
def move_move(event):
canvas.scan_dragto(event.x, event.y, gain=1)
# move
def pressed2(event):
global pressed
pressed = not pressed
canvas.scan_mark(event.x, event.y)
def move_move2(event):
if pressed:
canvas.scan_dragto(event.x, event.y, gain=1)
# windows zoom
def zoomer(event):
if (event.delta > 0):
canvas.scale("all", event.x, event.y, 1.1, 1.1)
elif (event.delta < 0):
canvas.scale("all", event.x, event.y, 0.9, 0.9)
canvas.configure(scrollregion = canvas.bbox("all"))
def zooming_in(event):
canvas.scale("all", event.x, event.y, 1.1, 1.1)
def zooming_out(event):
canvas.scale("all", event.x, event.y, 0.9, 0.9)
canvas = tk.Canvas(root, width=GRID_W, height=GRID_H, background="white")
canvas.grid(row=0, column=0, sticky="nsew")
root.grid_rowconfigure(0, weight=1)
root.grid_columnconfigure(0, weight=1)
for i in range(GRID_W//10 + 1):
canvas.create_line(i*10, 0, i*10, GRID_H, fill="#c9c9c9")
for j in range(GRID_H//10 + 1):
canvas.create_line(0, j*10, GRID_W, j*10, fill="#c9c9c9")
canvas.create_line(GRID_W // 2, 0, GRID_W // 2, GRID_H, fill="black", width=2)
canvas.create_line(0, GRID_H // 2, GRID_W, GRID_H // 2, fill="black", width=2)
canvas.bind("<ButtonPress-1>", move_start)
canvas.bind("<B1-Motion>", move_move)
canvas.bind("<ButtonPress-2>", pressed2)
canvas.bind("<Motion>", move_move2)
zoom_in = tk.Button(root, text="+")
zoom_in.grid(row=1, column=0)
zoom_in.bind("<Button-1>", zooming_in)
zoom_out = tk.Button(root, text="-")
zoom_out.grid(row=1, column=1)
zoom_out.bind("<Button-1>", zooming_out)
button = tk.Button(root, text="Start", command = start)
button.grid(row=2, column=0)
button_s = tk.Button(root, text="Stopp", command = stop)
button_s.grid(row=2, column=1)
root.mainloop()
I appreciate all the help and guidance I can get!
try stopping the flag_run while you hit the start button (zooming_in(event)) and continue when done zooming.
try remembering where the line was, and keep logging the data while you zoom in and out so you are not missing any values as you currently do.

Categories