I make frames, stick a few widgets in them, then place them on a canvas. My goal is to be able to drag-and-drop these window objects around in an up or down fashion.
What I'm trying to do is, when the user clicks a grip and moves a window object either up or down, I want the window object immediately above or below it to move out of its way once a boundary threshold is reached.
The exactly functionality I'm trying to replicate with Tkinter can be seen here:
jQuery Drag and Drop Sortable
For example, as I drag a window object upward, the blocks above it move downward once a boundary threshold is reached to fill the same coordinate slot below. The same type of concept applies to a downward movement. If I move downward, I want the blocks below to move up to take the place of the slots above as the currently selected window object moves.
My Primary Question: How do I get the above and below window object IDs relative to a current window object while it's in-motion?
Your first thought might be self.canvas.find_above(item) or self.canvas.find_below(item) but I haven't been able to figure out how to update the above and below window objects relative to the window object in-motion as it moves.
Your second thought might be tags. For example, add and remove tags as to the upper and lower objects with addtag_above(tag, item) or addtag_below(tag, item). I tried this extensively and it's still a moot point because I can't update above and below tags while the moving window object is in-motion.
The most relevant part of the code (note: this is minimal on purpose, I have tried so many things and this question is already quite long):
def StartMove(self, event):
self.y = event.y
self.cy = event.widget.nametowidget(event.widget.winfo_parent()).winfo_y()
self.current = self.canvas.find_closest(10, self.cy)[0]
def OnMotion(self, event):
self.canvas.move(self.current, 0, event.y - 10)
def StopMove(self, event):
self.y = None
Full Code:
import tkinter as tk
class DragAndDrop(tk.Frame):
def __init__(self, parent):
tk.Frame.__init__(self, parent)
self.parent = parent
self.parent.columnconfigure(0, weight=1)
self.grid_columnconfigure(0, weight=1)
self.block_count = 0
self.button = tk.Button(self, text='Add', command=self.addblock)
self.button.grid(row=0, column=0, columnspan=2, sticky='new')
self.container = tk.Frame(self)
self.container.grid(row=1, column=0, sticky='nsew')
self.canvas = tk.Canvas(self.container, width=200, height=450)
self.scrollbar = tk.Scrollbar(self.container,
orient='vertical',command=self.canvas.yview)
self.canvas.config(yscrollcommand=self.scrollbar.set)
self.canvas.grid(row=0, column=0, sticky='nsew')
self.scrollbar.grid(row=0, column=1, sticky='nse')
self.container.bind('<Configure>', self.handle_scroll)
def addblock(self):
self.block = tk.Frame(self.canvas, bd=1, relief='solid')
self.block.columnconfigure(0, weight=1)
self.grip = tk.Label(self.block, bitmap="gray25")
self.grip.grid(row=0, column=0, sticky='w')
self.grip.bind("<ButtonPress-1>", self.StartMove)
self.grip.bind("<ButtonRelease-1>", self.StopMove)
self.grip.bind("<B1-Motion>", self.OnMotion)
self.canvas.create_window((0, (self.block_count*25)),
window=self.block, anchor="nw",
width=200, height=24)
self.block_count += 1
self.canvas.configure(scrollregion=self.canvas.bbox("all"))
def handle_scroll(self, event):
self.canvas.configure(scrollregion=self.canvas.bbox("all"))
def StartMove(self, event):
self.y = event.y
self.cy = event.widget.nametowidget(event.widget.winfo_parent()).winfo_y()
self.current = self.canvas.find_closest(10, self.cy)[0]
def OnMotion(self, event):
self.canvas.move(self.current, 0, event.y - 10)
def StopMove(self, event):
self.y = None
root = tk.Tk()
app = DragAndDrop(root)
app.grid(row=0, column=0, sticky='ew')
root.mainloop()
My Primary Question: How do I get the above and below window object
IDs relative to a current window object while it's in-motion?
It seems to me the simplest solution is to start by generating a list of all blocks and their coordinates. Then, sort the list by Y coordinates. In your binding, find the last block in the list with a Y coordinate above the mouse, and the first object with a Y coordinate below the mouse.
Related
I'm trying to control multiple canvases widths with the mouse wheel. What I have so far is this:
import tkinter as tk
class App(tk.Frame):
row_amount = 3
def __init__(self, root):
super(App, self).__init__(root)
self.root = root
self.main_frame = tk.Frame(root)
self.main_frame.pack(expand=True, fill=tk.BOTH)
self.row_collection = RowCollection(root, self.main_frame)
for i in range(App.row_amount): self.row_collection.row()
window_height = App.row_amount * 100
window_width = root.winfo_screenwidth() - 30
root.geometry(f'{window_width}x{window_height}+0+0')
self.row_collection.right_frame.grid_columnconfigure(0, weight=1)
self.row_collection.left_frame.grid_columnconfigure(0, weight=1)
self.pack()
class RowCollection:
"""Collection of rows"""
def __init__(self, root, frame):
self.row_list = []
self.root = root
self.frame = frame
self.right_frame = tk.Frame(self.frame, bg='red')
self.right_frame.pack(side=tk.RIGHT, expand=tk.YES, fill=tk.BOTH)
self.left_frame = tk.Frame(self.frame)
self.left_frame.pack(side=tk.LEFT, fill=tk.Y)
self.scrollbar = tk.Scrollbar(self.right_frame, orient=tk.HORIZONTAL)
self.scrollbar.config(command=self.scroll_x)
def row(self):
row = Row(self)
self.row_list.append(row)
return row
def scroll_x(self, *args):
for row in self.row_list:
row.canvas.xview(*args)
def zoomer(self, event=None):
print('zooming')
for row in self.row_list:
scale_factor = 0.1
curr_width = row.canvas.winfo_reqwidth()
print(f'event delta={event.delta}')
if event.delta > 0:
row.canvas.config(width=curr_width * (1 + scale_factor))
elif event.delta < 0:
row.canvas.config(width=curr_width * (1 - scale_factor))
row.canvas.configure(scrollregion=row.canvas.bbox('all'))
class Row:
"""Every row consists of a label on the left side and a canvas with a line on the right side"""
row_count = 0
label_width = 15
line_weight = 3
line_yoffset = 3
padx = 20
def __init__(self, collection):
self.frame = collection.frame
self.root = collection.root
self.collection = collection
self.canvas = None
self.label = None
self.text = f'Canvas {Row.row_count}'
self.height = 100
self.root.update()
self.label = tk.Label(self.collection.left_frame,
text=self.text,
height=1,
width=Row.label_width,
relief='raised')
self.label.grid(row=Row.row_count, column=0, sticky='ns')
# configure row size to match future canvas height
self.collection.left_frame.grid_rowconfigure(Row.row_count, minsize=self.height)
self.root.update()
self.canvas = tk.Canvas(self.collection.right_frame,
width=10000,
height=self.height,
bg='white',
highlightthickness=0)
self.canvas.grid(row=Row.row_count, column=0, sticky=tk.W)
self.root.update()
# draw line
self.line = self.canvas.create_rectangle(self.padx,
self.canvas.winfo_height() - Row.line_yoffset,
self.canvas.winfo_width() - self.padx,
self.canvas.winfo_height() - Row.line_yoffset + Row.line_weight,
fill='#000000', width=0, tags='line')
# config canvas
self.canvas.config(scrollregion=self.canvas.bbox('all'))
self.canvas.config(xscrollcommand=self.collection.scrollbar.set)
self.canvas.bind('<Configure>', lambda event: self.canvas.configure(scrollregion=self.canvas.bbox('all')))
self.canvas.bind('<MouseWheel>', self.collection.zoomer)
# Create point at canvas edge to prevent scrolling from removing padding
self.bounding_point = self.canvas.create_rectangle(0, 0, 0, 0, width=0)
self.bounding_point = self.canvas.create_rectangle(self.canvas.winfo_width(), self.canvas.winfo_width(),
self.canvas.winfo_width(), self.canvas.winfo_width(),
width=0)
Row.row_count += 1
self.collection.scrollbar.grid(row=Row.row_count, column=0, sticky='ew')
if __name__ == '__main__':
root = tk.Tk()
app = App(root)
root.mainloop()
The canvases themselves are inside right_frame, and the number of canvases is given by row_amount. The left_frame contains labels for each of the canvases. The canvases should be allowed to be pretty wide, so I initially set a width value of 10000. Because of that, they start partially visible, with the rest being accessible via a scrollbar.
What I would like is for the mouse wheel to control the size of the canvas as a whole (that is, both what is currently visible and what could be viewed using the scrollbar), similar to what would happen in an audio or video editing software timeline.
Right now, when I use the mouse wheel, what seems to get resized is not the whole canvas, but only the 'visible' portion. Resize it to be small enough and you can start to see it's frame background on the right portion of the window.
What am I missing here?
What am I missing here?
I think what you're missing is that the drawable area of the canvas is not at all related to the physical size of the canvas widget. You do not need to resize the canvas once it has been created. You can draw well past the borders of the widget.
If you want to be able to scroll elements into view that are not part of the visible canvas, you must configure the scrollregion to define the area of the virtual canvas that should be visible.
You said in a comment you're trying to create a timeline. Here's an example of a canvas widget that "grows" by adding a tickmark every second. Notice that the canvas is only 500,100, but the drawable area gets extended every second.
import tkinter as tk
root = tk.Tk()
canvas = tk.Canvas(root, width=500, height=100, bg="black")
vsb = tk.Scrollbar(root, orient="vertical", command=canvas.yview)
hsb = tk.Scrollbar(root, orient="horizontal", command=canvas.xview)
canvas.configure(yscrollcommand=vsb.set, xscrollcommand=hsb.set)
canvas.grid(row=0, column=0, sticky="nsew")
vsb.grid(row=0, column=1, sticky="ns")
hsb.grid(row=1, column=0, sticky="ew")
root.grid_rowconfigure(0, weight=1)
root.grid_columnconfigure(0, weight=1)
counter = 0
def add_tick():
global counter
# get the current state of the scrollbar. We'll use this later
# to determine if we should auto-scroll
xview = canvas.xview()
# draw a new tickmark
counter += 1
x = counter * 50
canvas.create_text(x, 52, anchor="n", text=counter, fill="white")
canvas.create_line(x, 40, x, 50, width=3, fill="red")
# update the scrollable region to include the new tickmark
canvas.configure(scrollregion=canvas.bbox("all"))
# autoscroll, only if the widget was already scrolled
# as far to the right as possible
if int(xview[1]) == 1:
canvas.xview_moveto(1.0)
canvas.after(1000, add_tick)
add_tick()
root.mainloop()
In my orignal code I'm doing something like below, just with much more math.
It works but I dont like that I can see how the thumb of my scrollbar (hscrbar) is moveing from position 0 to 1 while I calculate the width in get_width(self): of my rectangel.
Cause in my original code I need to see it everytime I add something.
At the moment I havent an idea to solve this and you may are aware of a solution for it.
import tkinter as tk
root = tk.Tk()
class my_figure(tk.Frame):
def __init__(self, master):
tk.Frame.__init__(self, master)
self.master = master
self.root = self.winfo_toplevel()
# DownFrame
self.button = tk.Button(self, text='add', command=self.add)
self.button.grid(column=0, row=0)
self.body = tk.Frame(self, relief='sunken')
self.hscrbar = tk.Scrollbar(self.body, orient=tk.HORIZONTAL)
self.vscrbar = tk.Scrollbar(self.body)
self.Display = tk.Canvas(self.body,
xscrollcommand=self.hscrbar.set,
yscrollcommand=self.vscrbar.set)
self.hscrbar.config(command=self.Display.xview)
self.body.grid(column=0, row=1, sticky='nswe')
self.vscrbar.grid(column=1,sticky='ns')
self.hscrbar.grid(row=1, column=0, sticky='we')
self.Display.grid(column=0, row=0,
sticky='nswe')
self.vscrbar.config(command=self.Display.yview)
self.hscrbar.config(command=self.Display.xview)
self.x = tk.IntVar()
self.y = tk.IntVar()
self.x.set(10)
self.y.set(10)
self.height = 10
def add(self):
self.Display.create_rectangle(self.x.get(),self.y.get(),self.get_width(),self.height)
self.old_x = self.x.get()
self.old_y = self.y.get()
self.x.set(self.old_x+40)
self.y.set(self.old_y+20)
self.Display.config(scrollregion=self.Display.bbox("all"))
def get_width(self):
if self.hscrbar.get()[0] == 0 and self.hscrbar.get()[1] == 1: #if scrollbar shows everything
return self.Display.winfo_width()#return width of the canvas
else:
self.Display.xview_moveto(0) #scrollbar at postition 0
self.root.update_idletasks() #update idletasks to get correct value
value = self.Display.winfo_width()-round(self.Display.winfo_width()*self.hscrbar.get()[1])
width = value+self.Display.winfo_width() #calculate the width
self.Display.xview_moveto(1) #move to position 1 to show my the end of rectangel
return width
figure = my_figure(root)
figure.grid()
root.mainloop()
My guess is that the problem is largely due to the fact that inside of get_width you're moving the scrollbar, calling update_idletasks, and then moving the scrollbar again. That call to update_idletasks causes the window to redraw. That redraw means you'll see the scrollbar move to the left, and then it will move back to the right when the function is finished.
It's not entirely clear what get_width is supposed to do, but I'm guessing you can remove all of that code and replace it with self.Display.bbox("all"), and then grabbing the x coordinates from the result to compute the width of the drawing.
I have the current code below for some basic parameter entry into an AI assignment. It is just there to st the starting parameters and display the outpit of the different algorithms implemented, however the box that contains the output will not resize? I think I am doing something wrong with maybe the parent-child structure but I can't figure out what.
def __init__(self, master=None):
super().__init__(master)
self.master = master
self.pack()
self.create_widgets()
def create_widgets(self):
self.mainframe= tk.Frame(master=self, width=768, height=576)
self.mainframe.pack(fill=tk.BOTH, expand=1)
self.xsizelabel = tk.Label(self.mainframe, text="Size (X)")
self.xsizelabel.pack(side="top")
self.xsize = tk.Entry(self.mainframe)
self.xsize.insert(0, 2)
self.xsize.pack(side="top")
self.ysizelabel = tk.Label(self.mainframe, text="Size (Y)")
self.ysizelabel.pack(side="top")
self.ysize = tk.Entry(self.mainframe)
self.ysize.insert(0, 1)
self.ysize.pack(side="top")
self.xstartlabel = tk.Label(self.mainframe, text="Starting Position (X)")
self.xstartlabel.pack(side="top")
self.xStart = tk.Entry(self.mainframe)
self.xStart.insert(0, 0)
self.xStart.pack(side="top")
self.ystartlabel = tk.Label(self.mainframe, text="Starting Position (Y)")
self.ystartlabel.pack(side="top")
self.yStart = tk.Entry(self.mainframe)
self.yStart.insert(0, 0)
self.yStart.pack(side="top")
self.outputstartlabel = tk.Label(self.mainframe, text="Output")
self.outputstartlabel.pack(side="top")
self.separator = tk.Frame(master=self.mainframe, width=768, height=576, bd=1)
self.separator.pack(fill=tk.BOTH, padx=5, pady=5)
self.output = tk.Scrollbar(self.separator)
self.output.pack(side=tk.RIGHT, fill=tk.Y)
self.listbox = tk.Listbox(self.separator, yscrollcommand=self.output.set)
self.listbox.pack(side=tk.LEFT, fill=tk.BOTH, expand=1)
self.run_button = tk.Button(self.mainframe)
self.run_button["text"] = "Run with these settings"
self.run_button["command"] = self.runAlgorithm
self.run_button.pack(side="top")
self.quit = tk.Button(self.mainframe, text="QUIT", fg="red",
command=self.master.destroy)
self.quit.pack(side="bottom")
but the resulting window looks like this:
default
expanded
nothing expands when I expand the window, dispite setting the autofill and expand options. what am I doing wrong?
I can't run your program because you didn't present the whole thing. I see that you have set the fill and expand options on self.mainframe, but you didn't set those options in the constructor. Therefore the base window, which contains self.mainframe, will not expand to fill its available space. You need to make all the parent windows expandable, because when you drag the edges of the main window you are acting on the top level frame.
I have this code.
class App(object):
def __init__(self):
self.root = Tk()
self.root.attributes('-zoomed', True)
self.root.grid_rowconfigure(0, weight=1)
self.root.grid_columnconfigure(0, weight=1)
self.root.grid_columnconfigure(1, weight=1)
f1 = Frame(self.root, bd=1, bg="green")
f3 = Frame(self.root, bd=1, bg="blue")
self.image = Image.open("default.png")
self.photo = ImageTk.PhotoImage(self.image)
self.label = Label(image=self.photo)
self.label.grid(row=0, column=1, sticky="nsew")
f1.grid(row=0, column=0, sticky="nsew")
f3.grid(row=1, column=0, columnspan=2, sticky="ew")
app = App()
app.root.mainloop()
and the output is
How can I make image occupy equally to the left frame?
Before providing a solution, there are few remarks I would like to highlight:
You did not use the frame f3for anything. It is not even displayed when running your program. So I will simply ignore it in my solution below.
You set the attribute -zoomed for the main frame' attributes. I do not know which goal you are going to fulfill by using it. Anyway, there are many threads that state it is working only Windows and some Debian distributions like Ubuntu. I did not experience this problem though, however, as you can read on the first link I provide below, those arguments are platform specific. So for the sake of portability of your code, you may use -fullscreen instead. But here again, especially for your case, it will affect even the taskbar of your GUI which will not be displayed and thus you can not close your program by clicking on the close button. So in my solution I will rather use winfo_screenheight() and winfo_screenwidth() to set self.root to the size of the screen of your machine and call them inside the geometry() method.
Let us divide your problem into smaller ones:
First problem you need to resolve is how to set the first frame f1 and self.label to equal widths?. This is done by simply setting the width option of each one of them to the half of the width of your screen: width = self.root.winfo_screenwidth()/2
Once this is done, you will need to resolve the second problem you clarified in your comment. For that, you will need to resize self.image to fit self.label size by applying resize() method on the image, and pass to it the dimensions of the image which must be the ones of your label along with the constant PIL.Image.ANTIALIAS.
Full program
So here is the full program:
'''
Created on May 5, 2016
#author: billal begueradj
'''
import Tkinter as Tk
import PIL.Image
import PIL.ImageTk
class App(object):
def __init__(self):
self.root = Tk.Tk()
self.root.grid_rowconfigure(0, weight=1)
self.root.grid_columnconfigure(0, weight=1)
self.root.grid_columnconfigure(1, weight=1)
# Get width and height of the screen
self.h= self.root.winfo_screenheight()
self.w= self.root.winfo_screenwidth()
# Set the GUI's dimensions to the screen size
self.root.geometry(str(self.w) + "x" + str(self.h))
# Set the width of the frame 'f1' to the half of the screen width
self.f1 = Tk.Frame(self.root, bd=1, bg="green", width= self.w/2)
self.f1.grid(row=0, column=0, sticky="nsew")
# Load the image
self.image = PIL.Image.open("/home/Begueradj/mahomet_pedophile.jpg")
# Resize the image to fit self.label width
self.photo = PIL.ImageTk.PhotoImage(self.image)
self.label = Tk.Label(self.root, image=self.photo, width= self.w/2)
self.label.grid(row=0, column=1, sticky="nsew")
# Launch the main program
if __name__ == '__main__':
app = App()
app.root.mainloop()
Demo
Nota Bene
There is a little imprecision through your comment. It does not say weither you want the whole picture to appear of the same size as the frame f1 or just its label in which it is appended (self.label). If this is what you want to do, then just ignore this line in the previous program:
self.image = self.image.resize((self.w/2, self.h), PIL.Image.ANTIALIAS)
Demo
The output will then look like this (the image is simply positioned in the center of the label):
I created two overlapping buttons on a canvas, using tkinter and python 3.4:
Now I would like to bring button1 to the front (the button you cannot see right now, because it is under button2)
self.canvas.lift(self.button1)
But for some reason this does not work. Just nothing happens. Also lowering button2 has no effect. Can you tell me why?
import tkinter as tk
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.canvas.create_text(50,10, anchor="nw", text="Click to lift button1")
self.canvas.grid(row=0, column=0, sticky="nsew")
self.canvas.bind("<ButtonPress-1>", self.click_on_canvas)
self.button1 = tk.Button(self.canvas, text="button1")
self.button2 = tk.Button(self.canvas, text="button2")
x = 40
self.canvas.create_window(x, x, window=self.button1)
self.canvas.create_window(x+5, x+5, window=self.button2)
def click_on_canvas(self, event):
print("lifting", self.button1)
self.canvas.lift(self.button1)
self.canvas.lower(self.button2)
if __name__ == "__main__":
root = tk.Tk()
Example(root).pack(fill="both", expand=True)
root.mainloop()
Instead of calling lift() on the canvas, you need to call it on the widget instance directly:
def click_on_canvas(self, event):
print("lifting", self.button1)
self.button1.lift()
self.button2.lower() # Not necessary to both lift and lower
This is only true for widgets displayed via a window on your canvas.
If you were to draw objects such as lines or rectangles, you would use lift() or tag_raise() on the canvas instance as you were doing before.