Title: Make a frame that lets you drag its contents in tkinter and Python
The post Make a scrolled frame that responds to the mouse wheel in tkinter and Python builds a ScrolledFrame class that lets you use scroll bars to move a scrolling area. This example uses a different approach. It lets you click and drag to move the scrolling area's contents. (Similar to the way Facebook lets you move pictures.)
The new DragFrame class is actually a bit simpler than the ScrolledFrame class used by the previous example. The following code shows the class's constructor.
class DragFrame(tk.Frame):
def __init__(self, frame, *args, **kwargs):
tk.Frame.__init__(self, frame, *args, **kwargs)
# Canvas.
self.canvas = tk.Canvas(self)
self.canvas.pack(expand=True, fill=tk.BOTH)
# Frame.
self.frame = tk.Frame(self.canvas)
self.frame_window = self.canvas.create_window(0, 0,
window=self.frame, anchor=tk.NW)
# Initially we are not moving.
self.start_x = None
This code creates the class's Canvas and puts a Frame in it. It creates a window for the frame and sets start_x to None.
To use the DragFrame, you create it, get its frame from its frame property, and then add your widgets to that frame. After you're done creating widgets, call the class's configure_frame method. That method, which is shown in the following code, prepares the class for mouse events.
def configure_frame(self):
self.frame.update_idletasks()
self.frame.place(x=0, y=0,
width=self.frame.winfo_reqwidth(),
height=self.frame.winfo_reqheight())
# Track Button-1 down, Button-1 motion, and Button-1 up.
for child in widget_and_descendants(self.canvas):
child.bind('<Button-1>', self.mouse_down)
child.bind('<B1-Motion>', self.mouse_move)
child.bind('<ButtonRelease-1>', self.mouse_up)
This code positions the frame in the Canvas widget's upper left corner. (I don't think that's strictly necessary because it lands there by default.)
The code then uses the widget_and_descendants method used by the previous example to loop through the Canvas widget and its contained controls. For each of those controls, it binds the mouse button 1 down, move, and release events.
The following code executes when you press the mouse down over the frame or any of the widgets that it contains.
def mouse_down(self, event):
'''Record the starting info.'''
# Save the mouse's position with respect to the root window.
self.start_x = event.x_root
self.start_y = event.y_root
# Save the frame's current location.
self.frame_x = self.frame.winfo_x()
self.frame_y = self.frame.winfo_y()
# Calculate maximum X and Y coordinates.
self.min_x = self.canvas.winfo_width() - self.frame.winfo_width()
self.min_y = self.canvas.winfo_height() - self.frame.winfo_height()
The code first gets the mouse's position with respect to the root window.
(If you get it with respect to the widget that raised the event, then when that widget moves, it generates another move event and the frame shudders all over the place. It's an interesting bug and I've seen it in other circumstances so it's useful to know about. Modify the code if you want to see it firsthand.)
Next, the code gets the frame's current location. It then uses that location and the frame's dimensions to calculate the minimum values that we should use for the frame's X and Y coordinates. For example, if you move the frame farther to the left than min_x, parts of the canvas will be blank on the right.
When you move the mouse over the widgets, the following code executes.
def mouse_move(self, event):
# Ignore extraneous events.
if self.start_x is None: return
# Move the frame.
x = self.frame_x + (event.x_root - self.start_x)
y = self.frame_y + (event.y_root - self.start_y)
if x < self.min_x: x = self.min_x
if y < self.min_y: y = self.min_y
if x > 0: x = 0
if y > 0: y = 0
self.frame.place(x=x, y=y)
If start_x is None, then you have not pressed the mouse down so the event handler simply returns.
If a drag is in progress, the code subtracts the starting mouse position from the mouse's current position to get the amount by which the frame should be moved. It uses that value to calculate the frame's new position. It then adjusts that value if necessary so the X and Y coordinates are no smaller than the min_x and min_y values and updates the frame's position.
When you release the mouse button, the following code executes.
def mouse_up(self, event):
self.start_x = None
This code simply sets start_x to None so the widgets ignore mouse move events until you press the mouse button again.
One thing this example does not do is reset the frame's position when the canvas is resized. That means, for example, that you could drag the frame to the left and then enlarge the application to make extra blank space appear on the right. I'll probably make a new example soon to fix that.
Meanwhile, download the example to see addition details and to experiment with it.
|