18

Any suggestions on how one might create event bindings that would allow a user to mouse drag a window without borders, eg. a window created with overridedirect(1)?

Use case: We would like to create a floating toolbar/palette window (without borders) that our users can drag around on their desktop.

Here's where I'm at in my thinking (pseudo code):

  1. window.bind( '<Button-1>', onMouseDown ) to capture the initial position of the mouse.

  2. window.bind( '<Motion-1>', onMouseMove ) to track position of mouse once it starts to move.

  3. Calculate how much mouse has moved and calculate newX, newY positions.

  4. Use window.geometry( '+%d+%d' % ( newX, newY ) ) to move window.

Does Tkinter expose enough functionality to allow me to implement the task at hand? Or are there easier/higher-level ways to achieve what I want to do?

martineau
  • 119,623
  • 25
  • 170
  • 301
Malcolm
  • 5,125
  • 10
  • 52
  • 75
  • This sounds exactly opposite to what you were asking in your previous question, i.e. create a window that can't be moved or sized. If you just want a window that can't be sized, then fix the window min and max size and let the user move the window via the title bar. Or am I not understanding what you're trying to accomplish? – Brandon Oct 29 '10 at 19:44
  • The use case for the previous question was to create a docked window effect, eg. where a window was docked to a certain position on the user's desktop. Tkinter exposes .minsize() and .maxsize() window methods, but there are no equivalent methods for controlling window x, y positions on the desktop. You are correct - the question above is almost the opposite of my earlier question. The difference is the use case, eg. my new question is based on the need for users to drag small floating toolbar ("palette") windows on their desktop ... – Malcolm Oct 29 '10 at 20:21
  • ... continued from above: These floating toolbars do not need to be position constrained. For esthetic reasons we would like these floating toolbar windows/palettes to be windows without borders (we will add the border appearances ourselves). Thank you for your help! – Malcolm Oct 29 '10 at 20:24

5 Answers5

26

Yes, Tkinter exposes enough functionality to do this, and no, there are no easier/higher-level ways to achive what you want to do. You pretty much have the right idea.

Here's one example, though it's not the only way:

import tkinter as tk

class App(tk.Tk):
    def __init__(self):
        tk.Tk.__init__(self)
        self.floater = FloatingWindow(self)

class FloatingWindow(tk.Toplevel):
    def __init__(self, *args, **kwargs):
        tk.Toplevel.__init__(self, *args, **kwargs)
        self.overrideredirect(True)

        self.label = tk.Label(self, text="Click on the grip to move")
        self.grip = tk.Label(self, bitmap="gray25")
        self.grip.pack(side="left", fill="y")
        self.label.pack(side="right", fill="both", expand=True)

        self.grip.bind("<ButtonPress-1>", self.start_move)
        self.grip.bind("<ButtonRelease-1>", self.stop_move)
        self.grip.bind("<B1-Motion>", self.do_move)

    def start_move(self, event):
        self.x = event.x
        self.y = event.y

    def stop_move(self, event):
        self.x = None
        self.y = None

    def do_move(self, event):
        deltax = event.x - self.x
        deltay = event.y - self.y
        x = self.winfo_x() + deltax
        y = self.winfo_y() + deltay
        self.geometry(f"+{x}+{y}")

app=App()
app.mainloop()
Bryan Oakley
  • 370,779
  • 53
  • 539
  • 685
  • 3
    Bryan! Thank you very much for your help. What a cool feature - I'm going to have a lot of fun building tools that take advantage of this capability. I also enjoyed your technique for creating the grip - very clever. For readers following this thread, I added the line "self.attributes( '-topmost', 1 )" after the line "self.overrideredirect(True)" to make Bryan's floating window show on top of all windows. I think this shows off the potential for how one might use Bryan's solution to create a variety of desktop utilities. – Malcolm Oct 30 '10 at 11:23
  • Bryan, you don't mind making an edit illustrating how to reposition the mouse cursor over the specific coordinate the user clicked on? As your code is currently, the mouse cursor re-positions itself on the top left of the window after the user completes dragging the window by the label and releases the left mouse button. – the_prole Nov 19 '14 at 17:54
  • @the_prole: My code shouldn't be moving the mouse anywhere. All it does is _respond_ to mouse movement. Are you saying this exact code is moving your mouse for you? On what platform? – Bryan Oakley Nov 19 '14 at 19:25
  • @BryanOakley It's not even the cursor that moves. It's the canvas that moves underneath the cursor. The window looks like this after I begin dragging it. http://imgur.com/qmOIOqG – the_prole Nov 19 '14 at 20:28
  • @the_prole: that looks like your code, not mine. Does the code in my example exhibit this problem? – Bryan Oakley Nov 19 '14 at 20:33
  • 1
    @BryanOakley You're right... I got this post http://stackoverflow.com/questions/7455573/drag-window-handle-for-tkinter-python confused with yours. Which approach do you recommend? I tried to use your approach since it works apparently, but I do not see the label. Also the window frame does not disappear unless I insert `self.overrideredirect(True)` into `def __init__(self):`. If it makes any difference, I am using Python 3.4. – the_prole Nov 19 '14 at 21:01
  • This is the best Tkinter draggable overrideredirect(True)-code I found in 2 days! – Alex Dec 19 '17 at 13:38
  • This is awesome code! Credited you Bryan in my project for your code. Working great so far! – Mike from PSG Sep 08 '18 at 01:14
  • 2
    I wanted to hide the main window that in my case didn't contain anything. I added the next line: tk.Tk.withdraw(self) after tk.Tk.__init__(self) and the main window disappeared while still displaying the floating window. – Herii Aug 02 '20 at 04:42
10

Here is my solution:

from tkinter import *
from webbrowser import *


lastClickX = 0
lastClickY = 0


def SaveLastClickPos(event):
    global lastClickX, lastClickY
    lastClickX = event.x
    lastClickY = event.y


def Dragging(event):
    x, y = event.x - lastClickX + window.winfo_x(), event.y - lastClickY + window.winfo_y()
    window.geometry("+%s+%s" % (x , y))


window = Tk()
window.overrideredirect(True)
window.attributes('-topmost', True)
window.geometry("400x400+500+300")
window.bind('<Button-1>', SaveLastClickPos)
window.bind('<B1-Motion>', Dragging)
window.mainloop()
David58
  • 101
  • 1
  • 4
2

The idea of Loïc Faure-Lacroix is useful, the following is my own simple code snippets on Python3.7.3, hope it will help:

from tkinter import *


def move_window(event):
    root.geometry(f'+{event.x_root}+{event.y_root}')


root = Tk()
root.bind("<B1-Motion>", move_window)
root.mainloop()

But the position of the mouse is always in the upper left corner of the window. How can I keep it unchanged? Looking forward to a better answer!


Thanks to Bryan Oakley, because at the beginning I couldn't run your code on my computer, I didn't pay attention to it. Just now after the modification, it was very good to run, and the above situation would not happen (the mouse is always in the upper left corner), The updated code recently as follows:

def widget_drag_free_bind(widget):
    """Bind any widget or Tk master object with free drag"""
    if isinstance(widget, Tk):
        master = widget  # root window
    else:
        master = widget.master

    x, y = 0, 0
    def mouse_motion(event):
        global x, y
        # Positive offset represent the mouse is moving to the lower right corner, negative moving to the upper left corner
        offset_x, offset_y = event.x - x, event.y - y  
        new_x = master.winfo_x() + offset_x
        new_y = master.winfo_y() + offset_y
        new_geometry = f"+{new_x}+{new_y}"
        master.geometry(new_geometry)

    def mouse_press(event):
        global x, y
        count = time.time()
        x, y = event.x, event.y

    widget.bind("<B1-Motion>", mouse_motion)  # Hold the left mouse button and drag events
    widget.bind("<Button-1>", mouse_press)  # The left mouse button press event, long calculate by only once
bruce
  • 462
  • 6
  • 9
1

Try this, and it surely works;

  1. Create an event function to move window:

    def movewindow(event): root.geometry('+{0}+{1}'.format(event.x_root, event.y_root))

  2. Bind window:

    root.bind('', movewindow)

Now you can touch the the window and drag

Loïc Faure-Lacroix
  • 13,220
  • 6
  • 67
  • 99
sjana
  • 64
  • 1
  • 10
  • in python 3 i'm getting an error File "C:\python\python3.5\lib\tkinter\__init__.py", line 1686, in wm_geometry return self.tk.call('wm', 'geometry', self._w, newGeometry) _tkinter.TclError: bad geometry specifier "+562 + 580" – omgimdrunk Jan 22 '17 at 10:49
0

This code is the same as Bryan's solution but it does not use overridedirect.

It was tested with: python 3.7, Debian GNU/Linux 10 (buster), Gnome 3.30

import tkinter as tk


class App(tk.Tk):
    def __init__(self):
        tk.Tk.__init__(self)
        self.floater = FloatingWindow(self)


class FloatingWindow(tk.Toplevel):
    def __init__(self, *args, **kwargs):
        tk.Toplevel.__init__(self, *args, **kwargs)

        #self.overrideredirect(True)
        self.resizable(0, 0)  # Window not resizable
        self.wm_attributes('-type', 'splash')  # Hide title bar (Linux)

        self.label = tk.Label(self, text="Click on the grip to move")
        self.grip = tk.Label(self, bitmap="gray25")
        self.grip.pack(side="left", fill="y")
        self.label.pack(side="right", fill="both", expand=True)

        self.grip.bind("<ButtonPress-1>", self.StartMove)
        self.grip.bind("<ButtonRelease-1>", self.StopMove)
        self.grip.bind("<B1-Motion>", self.OnMotion)

    def StartMove(self, event):
        self.x = event.x
        self.y = event.y

    def StopMove(self, event):
        self.x = None
        self.y = None

    def OnMotion(self, event):
        deltax = event.x - self.x
        deltay = event.y - self.y
        x = self.winfo_x() + deltax
        y = self.winfo_y() + deltay
        self.geometry("+%s+%s" % (x, y))


app = App()
app.mainloop()