I'm creating a game launcher specially in Python 3.7 tkInter, and I want to make my own styled Scrollbar (in Windows 10 (version 1903)).
I've tried adding a hidden Scrollbar, and hiding works but i can't simulate it:
def scroll(self, i, reqHeight, vbarValue):
print("vbarValue", vbarValue)
value = -i / 1.4
a1 = int(self.canvass.coords(self.scroll2)[1]) == 5
a2 = value > 0
a = not(a1 ^ a2)
b1 = ((self.canvass.coords(self.scroll2)[3] > self.cHeight))
b2 = value < 0
b = not(b1 ^ b2)
print(value, value < 0)
print(a1, 5)
print("====")
print(a1, a2)
print(a)
print("----")
print(b1, b2)
print(b)
print("====\n\n")
print("OK")
x1, y1, x2, y2 = self.canvass.coords(self.scroll2)
_y1, _y2 = vbarValue
print("1:",y1, y2)
print("2:",_y1, _y2)
print("3:",(_y2 - _y1) / 2 - y2)
print("4:",(_y1 + (_y2 - _y1) / 120) * self.cHeight)
print("5:",(_y1 + (_y2 - _y1) / 120) * self.cHeight - (y2 / y1))
print("6:",((_y2 - _y1) / 120) * self.cHeight - y2* -i)
print("7:",(_y1 + (_y2 - _y1) / 120))
value = (_y1 + (_y2 - _y1) / 120) * self.cHeight / (y1 / y2)
print("8:",(y2 / y1))
# value = value - (y1 / y2)
print("Dynamic Canvas Region Height:")
print("DCRH:", self.cHeight)
print("Value: %s", value)
self.canvass.move(self.scroll2, 0, -y2)
self.canvass.move(self.scroll2, 0, value)
print("coords: %s" % self.canvass.coords(self.scroll2))
print("reqHeight: %s" % reqHeight)
Event:
def _bound_to_mousewheel(self, event): # <Enter> Event
self.canv.bind_all("<MouseWheel>", self._on_mousewheel)
def _unbound_to_mousewheel(self, event): # <Leave> Event
self.canv.unbind_all("<MouseWheel>")
def _on_mousewheel(self, event): # <Configure> Event
self.canv.yview_scroll(int(-1 * (event.delta / 120)), "units")
self.scrollCommand(int(-1 * (event.delta / 120)), self.scrollwindow.winfo_reqheight(), self.vbar.get())
def _configure_window(self, event):
# update the scrollbars to match the size of the inner frame
size = (self.scrollwindow.winfo_reqwidth(), self.scrollwindow.winfo_reqheight()+1)
self.canv.config(scrollregion='0 0 %s %s' % size)
# if self.scrollwindow.winfo_reqwidth() != self.canv.winfo_width():
# # update the canvas's width to fit the inner frame
# # self.canv.config(width=self.scrollwindow.winfo_reqwidth())
# if self.scrollwindow.winfo_reqheight() != self.canv.winfo_height():
# # update the canvas's width to fit the inner frame
# # self.canv.config(height=self.scrollwindow.winfo_reqheight())
By the way, self.scrollCommand(...) is the same as scroll on the first code.
I expect to get some x and y output for the canvas.move method.
How do i simulate a Scrollbar in Tkinter Canvas
The scrollbar has a well defined interface. To simulate a scrollbar all you need to do is implement this interface. This is most easily done by creating a class that has the following attributes:
you need to define the set method, which is called whenever the widget being scrolled wants to update the scrollbar
you need to add mouse bindings to call the yview method of the widget being controlled by the scrollbar (or xview if creating a horizontal widget).
If you do those two things, your scrollbar can be used exactly like a built-in scrollbar.
For the rest of this answer, I'm going to assume you want to simulate a vertical scrollbar. Simulating a horizontal scrollbar works identically, but instead of 'top' and 'bottom', you are dealing with 'left' and right'.
Defining the set method
The set method will be called with two fractions. The canonical documentation describes it like this:
This command is invoked by the scrollbar's associated widget to tell the scrollbar about the current view in the widget. The command takes two arguments, each of which is a real fraction between 0 and 1. The fractions describe the range of the document that is visible in the associated widget. For example, if first is 0.2 and last is 0.4, it means that the first part of the document visible in the window is 20% of the way through the document, and the last visible part is 40% of the way through.
Defining the bindings
The other half of the equation is when the user interacts with the scrollbar to scroll another widget. The way this happens is that the scrollbar should call the yview command of the widget to be controlled (eg: canvas, text, listbox, etc).
The first argument you must pass to the command is either the string "moveto" or "scroll".
If "moveto", the second argument is a fraction which represents the amount that has been scrolled off of the top. This is typically called when clicking on the scrollbar, to immediately move the scrollbar to a new position
if "scroll", the second argument is an integer representing an amount, and the third argument is either the string "units" or "pages". The definition of "units" refers to the value of the yscrollincrement option. "pages" represents 9/10ths of the window height. This is typically called when dragging the mouse over the scrollbar.
The options are spelled out in the man pages for each scrollable widget.
Example
The following is an example of a uses a text widget, so that you can see that when you type, the scrollbar properly grows and shrinks. If you click anywhere in the scrollbar, it will scroll to that point in the documentation.
To keep the example short, this code doesn't handle dragging the scrollbar, and it hard-codes a lot of values that probably ought to be configurable. The point is to show that all you need to do to simulate a scrollbar is create a class that has a set method and which calls the yview or xview method of the connected widget.
First, the scrollbar class
import tkinter as tk
class CustomScrollbar(tk.Canvas):
def __init__(self, parent, **kwargs):
self.command = kwargs.pop("command", None)
tk.Canvas.__init__(self, parent, **kwargs)
# coordinates are irrelevant; they will be recomputed
# in the 'set' method
self.create_rectangle(0,0,1,1, fill="red", tags=("thumb",))
self.bind("<ButtonPress-1>", self.on_click)
def set(self, first, last):
first = float(first)
last = float(last)
height = self.winfo_height()
x0 = 2
x1 = self.winfo_width()-2
y0 = max(int(height * first), 0)
y1 = min(int(height * last), height)
self.coords("thumb", x0, y0, x1, y1)
def on_click(self, event):
y = event.y / self.winfo_height()
self.command("moveto", y)
Using the class in a program
You would use this class exactly like you would a native scrollbar: instantiate it, and set the command to be the yview command of a scrollable widget.
This example uses a text widget so you can see the scrollbar updating as you type, but the exact same code would work with a Canvas, or any other scrollable window.
root = tk.Tk()
text = tk.Text(root)
sb = CustomScrollbar(root, width=20, command=text.yview)
text.configure(yscrollcommand=sb.set)
sb.pack(side="right", fill="y")
text.pack(side="left", fill="both", expand=True)
with open(__file__, "r") as f:
text.insert("end", f.read())
root.mainloop()
Related
Trying to figure out how the scaling method in the Tkinter Canvas package works.
import tkinter
root = tkinter.Tk()
root.geometry("1200x800")
root.resizable(False,False)
root.title("Examples")
myCanvas = tkinter.Canvas(root, bg="white", height=700, width=1000)
myCanvas.create_rectangle(0,200,300,300, tags="myTag")
myCanvas.scale("myTag", 8200,1200,0.99,0.99)
myCanvas.create_rectangle(0,400,300,300, tags="myTag2")
myCanvas.move("myTag2",200,0)
input_elem = tkinter.Entry(root,width=50)
input_elem.grid(row=1, column=0)
btn = tkinter.Button(root,width=50, text="here", height=5)
btn.grid(row=1, column=2)
myCanvas.grid(row=0, column=0)
root.mainloop()
in the documentation I found this:
.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.
This method will not change the size of a text item, but may move it.
Based on this I would have expected the .scale() to work in the same units as the canvas (by default pixels) but it seems like it doesn't. My xOffset value is fairly large and the rectangle moved only very little. So I created a second rectangle to compare and realized it's scaling based off of the width of the canvas, so this :
myCanvas.scale("myTag", (20*0.99)*1000,1200,0.99,0.99)
myCanvas.move("myTag2",(0.99*200),0)
equals to the same xOffset. Why is is the scaling a factor of 10 though? Shouldn't (200*0.99)*1000 in the scaling method equal to 0.99*200 in the move method? Or can someone point me to a more detailed documentation?
I think the best way to understand Canvas move and scale is to see them in action.
Here is a small interactive demo that may help.
It draws a small rectangle (square) at origin.
Control for moving (pick and place) the square is single Button-1 click
Control for scale is via keyboard (Up, Right) = grow (Down and Left) = shrink
Run the code, pick up the square and place it near canvas center.
Make it grow by pressing and holding Up or Right key.
Now make it shrink by pressing and holding Down or Left key.
Try moving mouse pointer inside and outside of square while changing scale.
Note how the square moves away from your mouse pointer when growing
and moves toward your mouse pointer when shrinking.
Exactly what one would expect from objects approaching or receding.
You can change scale and move square at the same time.
This works with objects line, polygon, rectangle and oval.
import tkinter as tk
class moveScale(tk.Tk):
def __init__(self):
super().__init__()
# Remove various paddings so the entire canvas is visible
self.canvas = tk.Canvas(
self, highlightthickness = 0, borderwidth = 0,
cursor = "crosshair")
self.canvas.grid(sticky = tk.NSEW)
self.xx = self.yy = 0 # Store for dynamic mouse pointer position
self.con = False # Flag controls move on|off
self.tag = "myTag"
self.sU = 1.005 # Scale up (growth)
self.sD = 1 / self.sU # Scale down (shrink)
self.canvas.create_rectangle(
0, 0, 40, 40, fill = "", tags = self.tag)
# Mouse and Keyboard bindings
self.canvas.bind("<Motion>", self.mover)
self.canvas.bind("<Button-1>", self.swap)
self.canvas.event_add("<<BIG>>", "<Up>", "<Right>")
self.canvas.event_add("<<GIB>>", "<Left>", "<Down>")
self.canvas.bind("<<BIG>>", self.big)
self.canvas.bind("<<GIB>>", self.small)
# Keyboard input only works when canvas has the focus
self.canvas.focus_force()
# Movement control
def swap(self, ev):
self.con = self.con == False
def mover(self, ev):
x, y = self.canvas.canvasx(ev.x), self.canvas.canvasy(ev.y)
if self.con:
self.canvas.move(self.tag, x - self.xx, y - self.yy)
self.xx, self.yy = x, y
def big(self, ev):
self.canvas.scale(self.tag, self.xx, self.yy, self.sU, self.sU)
def small(self, ev):
self.canvas.scale(self.tag, self.xx, self.yy, self.sD, self.sD)
main = moveScale()
main.mainloop()
I want a widget which is been dragged by the user to be fixed in a specific grid cell when the widget comes near it, is there any way to do it in tkinter? I managed to make the widget draggable by taking the mouse position of user as he drags the widget, but i dont know how to make it fix into a grid cell.
this is what i use currently to move widgets
def make_draggable(self):
def drag_start(event):
widget = self
widget = event.widget
self._drag_start_x = event.x
self._drag_start_y = event.y
def drag_motion(event):
widget = self
widget = event.widget
x = self.winfo_x() - self._drag_start_x + event.x
y = self.winfo_y() - self._drag_start_y + event.y
self.place(x=x, y=y)
self.bind("<Button-1>", drag_start)
self.bind("<B1-Motion>", drag_motion)
for example, if the grid cell left top corner position is (x=10,y=10) I need the button to get fixed in the cell when its position is near that value (like 9,9 8,8 9,5 etc)
Based on what I have understood from your question, I have tried the following
from tkinter import *
class Drag:
def __init__(self,widget):
self.widget=widget
self.widget.bind("<Button-1>", self.drag_start)
self.widget.bind("<B1-Motion>", self.drag_motion)
self.grid=(100,100)
self.margin=(20,20)
def drag_start(self,event):
self._drag_start_x = event.x
self._drag_start_y = event.y
def drag_motion(self,event):
x = self.widget.winfo_x() - self._drag_start_x + event.x
y = self.widget.winfo_y() - self._drag_start_y + event.y
if (
x%self.grid[0] in range(self.margin[0]) and
y%self.grid[1] in range(self.margin[1])
):
self.widget.place(x=x-x%self.grid[0],y=y-y%self.grid[1])
else:
self.widget.place(x=x, y=y)
root=Tk()
root.minsize(300,200)
label=Label(root,text='Drag Me',bg='yellow')
label.pack()
Drag(label)
root.mainloop()
This basically creates grid of "cells" of height and width 100 and it clips it to the cell if it is in 20 pixel range of the top left corner of the cell. This is basically done by taking the remainders of the current coordinates when divided by the cell's height/width using the modulo % operator and comparing if this lies in the desired range.
I can't find answer for my question anywhere so i decided to ask here.
I star a Python project and i want to add some Buttons on my app's background.
I try Tkinter but this adds a "sub-window" in my app's window, not a button.
I want transparent buttons because in my background there are already graphical buttons. Each button will do a command.
Since you say your background already has the look of a button, you don't need to add an additional button. Instead, you should determine the coordinates of the bounding rectangle for each 'pseudo-button' and then create event bindings that will perform actions if the user clicks within those bounds.
class Rectangle:
def __init__(self, x1, y1, x2, y2):
# (x1, y1) is the upper left point of the rectangle
# (x2, y2) is the lower right point of the rectangle
self.x1 = x1
self.y1 = y1
self.x2 = x2
self.y2 = y2
return
def contains(self, newx, newy):
# Will return true if the point (newx, newy) lies within the rectangle.
# False otherwise.
return (self.x1 <= newx <= self.x2) and (self.y1 <= newy <= self.y2)
try:
import tkinter as tk
except:
import Tkinter as tk
from PIL import ImageTK, Image
def button_click(event):
# The location of the click is stored in event sent by tkinter
# and can be accessed like:
print("You clicked ({}, {})".format(event.x, event.y))
## Now that you're in here, you can determine if the user clicked
## inside one of your buttons. If they did, perform the action that
## you want. For example, I've created two rectangle objects which
## simulate the location of some.
button1_rect = Rectangle( 100, 100, 200, 200 )
button2_rect = Rectangle( 300, 300, 400, 400 )
# Change the rectangles to reflect your own button locations.
if button1_rect.contains(event.x, event.y):
print("You clicked button number 1!")
if button2_rect.contains(event.x, event.y):
print("You clicked button number 2!")
return
root = tk.Tk()
canvas = tk.Canvas(master = root, height = 500, width = 500)
canvas.bind("<Button-1>", button_click)
# The above command means whenever <Button-1> (left mouse button) is clicked
# within your canvas, the button_click function will be called.
canvas.pack()
your_background = ImageTK.PhotoImage(Image.open("info_screen.png")) # <-- Or whatever image you want...
canvas.create_image(20, 20, image = your_background)
# 20, 20 represents the top left corner for where your image will be placed
root.mainloop()
Run that and click around while watching your console
Read more about binding to tkinter events here: https://effbot.org/tkinterbook/tkinter-events-and-bindings.html
I want to to fill my window with, say, labels and I want them to wrap once the column would be bigger than the current window (or rather parent frame) size.
I've tried using the grid layout, but then I have to calculate the size of the content of each row myself, to know when to put the next element in the next row.
The reason I ask, is because I want to create some sort of tiled file icons.
Or asked differently, is there something like Swing's FlowLayout for TkInter?
What I do when I want something like this is use the text widget for a container. The text widget can have embedded widgets, and they wrap just like text. As long as your widgets are all the same height the effect is pretty nice.
For example (cut and pasted from the question at the author's request):
textwidget = tk.Text(master)
textwidget.pack(side=tk.LEFT, fill=tk.BOTH)
for f in os.listdir('/tmp'):
textwidget.window_create(tk.INSERT, window=tk.Label(textwidget, text=f))
Here is a way to make flow behavior inside a frame.
I wrote a function that will do this. Basically you pass a frame to the function (not root or top level) and the function will look at all the children of the frame, go through them measure their sizes and place them in the frame.
Here is the placement procedure
Place the first widget, and move x over an amount equal to its width.
Measure the next widget.
If placing the next widget would cause it to goes past the frame width, bump its x value to 0 and bump it down a y value equal to the largest widget in the current row (start a new row).
Reset the value of the largest widget since you are starting a new row.
Keep repeating until all widgets are placed.
Bind that procedure to the resizing of the frame event.
I used 3 functions to make this work:
The function that runs the procedure.
The function that binds the resizing of the frame to the function.
The function that unbinds the resizing of the frame.
Here are the functions:
from tkinter import *
def _reorganizeWidgetsWithPlace(frame):
widgetsFrame = frame
widgetDictionary = widgetsFrame.children
widgetKeys = [] # keys in key value pairs of the childwidgets
for key in widgetDictionary:
widgetKeys.append(key)
# initialization/priming loop
width = 0
i = 0
x = 0
y = 0
height = 0
maxheight = 0
# loop/algorithm for sorting
while i < len(widgetDictionary):
height = widgetDictionary[widgetKeys[i]].winfo_height()
if height > maxheight:
maxheight = height
width = width + widgetDictionary[widgetKeys[i]].winfo_width()
# always place first widget at 0,0
if i == 0:
x = 0
y = 0
width = widgetDictionary[widgetKeys[i]].winfo_width()
# if after adding width, this exceeds the frame width, bump
# widget down. Use maximimum height so far to bump down
# set x at 0 and start over with new row, reset maxheight
elif width > widgetsFrame.winfo_width():
y = y + maxheight
x = 0
width = widgetDictionary[widgetKeys[i]].winfo_width()
maxheight = height
# if after adding width, the widget row length does not exceed
# frame with, add the widget at the start of last widget's
# x value
else:
x = width-widgetDictionary[widgetKeys[i]].winfo_width()
# place the widget at the determined x value
widgetDictionary[widgetKeys[i]].place(x=x, y=y)
i += 1
widgetsFrame.update()
def organizeWidgetsWithPlace(frame):
_reorganizeWidgetsWithPlace(frame)
frame.bind("<Configure>", lambda event: _reorganizeWidgetsWithPlace(frame))
_reorganizeWidgetsWithPlace(frame)
def stopOrganizingWidgetsWithPlace(frame):
frame.unbind("<Configure>")
And here is an example of them in use:
def main():
root = Tk()
root.geometry("250x250")
myframe = Frame(root)
# make sure frame expands to fill parent window
myframe.pack(fill="both", expand=1)
buttonOrganize = Button(myframe, text='start organizing',
command=lambda: organizeWidgetsWithPlace(myframe))
buttonOrganize.pack()
buttonStopOrganize = Button(myframe, text='stop organizing',
command=lambda: stopOrganizingWidgetsWithPlace(myframe))
buttonStopOrganize.pack()
##### a bunch of widgets #####
button = Button(myframe, text="---a random Button---")
canvas = Canvas(myframe, width=80, height=20, bg="orange")
checkbutton = Checkbutton(myframe, text="---checkbutton----")
entry = Entry(myframe, text="entry")
label = Label(myframe, text="Label", height=4, width=20)
listbox = Listbox(myframe, height=3, width=20)
message = Message(myframe, text="hello from Message")
radioButton = Radiobutton(myframe, text="radio button")
scale_widget = Scale(myframe, from_=0, to=100, orient=HORIZONTAL)
scrollbar = Scrollbar(myframe)
textbox = Text(myframe, width=3, height=2)
textbox.insert(END, "Text Widget")
spinbox = Spinbox(myframe, from_=0, to=10)
root.mainloop()
main()
Notice:
That you do not need to grid, pack or place them. As long as you specify the frame, that will all be done at once when the function is called. So that is very convenient. And it can be annoying if you grid a widget, then try to pack another, then try to place another and you get that error that you can only use one geometry manager. I believe this will simply overwrite the previous choices and place them. I believe you can just drop this function in and it will take over management. So far that has always worked for me, but I think you should really not try to mix and match geometry managers.
Notice that initially the buttons are packed, but after pressing the button, they are placed.
I have added the "WithPlace" naming to the functions because I have a similar set of functions that do something very similar with the grid manager.
I have a window whose content changes. Sometimes the content is larger than the window, so the window expands to fit it's children. However, when I center a window using a call to "geometry", the window no longer resizes. Below, you will find code that illustrates this.
If you comment out the delayed center() function call, you'll notice that the window expands to fit its content. If you leave it as is, the window centers, but no longer expands to fit its content.
Is it possible to center a window AND have it continue to resize to fit its content?
from Tkinter import *
import ttk
def center(root):
w = root.winfo_screenwidth()
h = root.winfo_screenheight()
rootsize = tuple(int(_) for _ in root.geometry().split('+')[0].split('x'))
x = w/2 - rootsize[0]/2
y = h/2 - rootsize[1]/2
root.geometry("%dx%d+%d+%d" % (rootsize + (x, y)))
root = Tk()
var = StringVar()
var.set('Small Text')
label = ttk.Label(root, textvariable=var)
label.grid(column=0, row=0)
# Change the text label in a couple of seconds.
def changeit():
var.set('BIG TXT - ' * 5)
root.after(2000, changeit)
# Comment out this center call and the label expands.
root.after(100, lambda: center(root))
root.mainloop()
When you call the geometry command, don't provide a width and height -- just supply the x/y values. When you give it an explicit width and height, you're telling Tk "I want the window to be exactly this size" so it turns it's auto-resize behavior off.
root.geometry("+%d+%d" % (rootsize + (x, y)))
Also, you can use winfo_width() and winfo_height() to get the actual size of the window, instead of parsing the output of the geometry method.