Dual cursors in twin Tkinter Canvases - python

I've got two Canvases displaying images (one is the source, and the other is slightly modified).
I'd like to sync both Canvases' cursors, i.e when hovering over one, a cursor will also show on the other in the same position.
I've already done this by drawing a custom 'plus' cursor (2 intersecting lines), but I'm not satisfied with the result.
Is there a way to 'fake' the mouse hovering over the canvas at a certain position?
Edit:
My relevant code, as per request:
self.canvas_image.bind("<Motion>", self.processMouseEvent)
def processMouseEvent(self):
self.cursorSync.Sync(event)

The Motion event will have an x,y attribute assigned to it. Why not do something like this, where your cursor could be any object that can be managed by the place geometry manager:
def move_cursor(event):
cursor.place(x=event.x, y=event.y) # set x,y to cursor
root = Tk()
left = Canvas(root, width=100, height=100, bg='white')
right = Canvas(root, width=100, height=100, bg='black')
left.pack(fill=BOTH, expand=1, side=LEFT)
right.pack(fill=BOTH, expand=1, side=RIGHT)
cursor = Label(right, width=2, bg='red') # create cursor. this could be an image or whatever
left.bind('<Motion>', move_cursor)
mainloop()

No, there is no way to have more than one cursor active at a time. Your only option is to simulate a second cursor by either drawing one on the canvas, or using a small widget that you place over the canvas.

Related

How to display widgets on a frame that is mounted on canvas in Tkinter?

I created a main root with two frames.
-One frame is for program toolbar.
-Other frame is for canvas where data will be displayed and a scrollbar widget.
-Inside of the canvas is a third smaller frame which will be used for scrolling trough data.
However, when I try to define new widgets and place them on that third smaller frame, nothing happens. I'm defining new widgets inside of a function call of a button command. I have also tried declaring everything as global variables but without luck.
Hint: I tried placing the code from the function to the top level of the code and it works. Also, if I try to mount these widgets on the toolbar frame it also works. It seems that the only thing I can't do is to mount these new widgets on the small frame that is inside the canvas.
I used a simple for loop to create labels just for testing.
Could anyone tell what I am doing wrong?
from tkinter import *
from tkinter import ttk
#Creating main window
root = Tk()
root.resizable(width=False, height=False)
#Defining Background
toolbar = Frame(root, width=613, height=114)
toolbar.grid(row=0, column=0)
background_frame = Frame(root, width=615, height=560)
background_frame.grid(row=1, column=0)
background = Canvas(background_frame, width=615, height=560)
background.pack(side=LEFT, fill=BOTH, expand=1)
scroll_bar = ttk.Scrollbar(background_frame, orient=VERTICAL, command=background.yview)
scroll_bar.pack(side=RIGHT, fill=Y)
background.configure(yscrollcommand=scroll_bar.set)
background.bind('<Configure>', lambda e:background.configure(scrollregion = background.bbox('all')))
second_frame = Frame(background)
background.create_window(150,100, window=second_frame, anchor='nw')
def confirm1():
for x in range(100):
Label(second_frame, text = x ).grid(row=x, column=1)
show_labels = Button(toolbar, text= "Show labels", fg="black", command=confirm1)
show_labels.grid(row=0, column=2)
root.mainloop()
Picture of the app so far
I surely can't reproduce the issue with your current code, but looking at the previous edit it is pretty clear what your problem is.
(taken from your previous edit)
def confirm1():
global background_image1
background.delete('all') # <--- this line of code
for x in range(100):
Label(second_frame, text = x ).grid(row=x, column=1)
Here you delete all your items from your canvas:
background.delete('all')
hence no item appears.
You should instead delete only those items that you want to remove by passing the id or tags to delete method. You can delete multiple items together at once by giving the same tags.
Another option would be to recreate the frame item again on canvas using create_window (Do note: your frame is not deleted/destroyed, it's only removed from canvas)

Tkinter fix size of Frame to not overlap Canvas border with scrollbar

I'm working on a GUI with tkinter and i have a problem.
When i add a scrollbar to my app, the frame on my canvas overlaps the outlines (see image)
Here is the code:
from tkinter import *
window = Tk()
window.geometry("400x225")
scrollbar1 = Scrollbar(window, orient=VERTICAL)
canvas1 = Canvas(window, bg="#003333", yscrollcommand=scrollbar1.set)
frame1 = Frame(canvas1, bg="#003333")
scrollbar1.config(command=canvas1.yview)
scrollbar1.pack(side=RIGHT, fill=Y)
canvas1.pack(fill=BOTH, expand=True)
canvas1.create_window((0, 0), window=frame1, anchor="nw")
for x in range(20):
string = "line " + str(x)
label1 = Label(frame1, fg="white", bg="#003333", text=string, font=("Calibri Bold", 14))
label1.pack(pady=5)
window.update()
canvas1.config(scrollregion=canvas1.bbox("all"))
window.mainloop()
I don't know if it's possible but i want the frame to fit within the canvas and keeping the outlines as well.
I hope you get my problem and can probably help me out! Thanks in advance.
The highlightthickness
Specifies a non-negative value indicating the width of the highlight rectangle to draw around the outside of the widget when it has the input focus.
So, this is not really the "border" that you want. It is a part of the drawing space within the canvas, when you use window_create to draw a window, the parent of that window is the canvas, which begins before the highlight and so the window slides over it.
A solution, as also suggested by #martineau would be to make this 0 by specifying highlightthickness=0 and as you suggested that you need the "border" around the whole thing, you can either create a container frame and specify the bd parameter, or just set the bd of the window window.config(bd=2).

Dragging collection of widgets around in Python using Tkinter

Alright, so to start I'm using Python 3.7 with tkinter.
I have a canvas and I can drag a label around with that using the mouse events. My next step is to be able to drag around something on which I can put more widgets. So imagine a box which has a text box and an image on it, perhaps a combobox too. That box can then be dragged around.
I figured perhaps what I needed was a frame widget on my canvas which I could then set up in the same way as I had done with the label. But this is where it seems to fall apart - clearly I'm doing something wrong.
Here's the code I've been playing around with, to no avail:
root = tk.Tk()
root.geometry("800x600")
def ClickedCallback(event):
print(f'Clicked: coords: {event.x}, {event.y}')
def ReleaseCallback(event):
print(f'Released: coords: {event.x}, {event.y}')
def MotionCallback(event):
print(f'Motion: coords: {event.x}, {event.y}')
canvas.coords(frame_id, event.x, event.y)
canvas = tk.Canvas(root, width=1000, height=600, bg='blue')
canvas.bind("<B1-Motion>", MotionCallback)
canvas.pack()
frame = tk.Frame(canvas, bg='green')
frame.pack()
l2 = tk.Label(frame, bg='red')
l2.bind("<Button-1>", ClickedCallback)
l2.pack()
l2['text'] = "Test"
frame_id = canvas.create_window((300,300), window=frame)
label_id = canvas.create_window((100, 100), window=l2)
root.mainloop()
My thinking here is that I attach the frame to the canvas and then the label to the frame so that, f I move the frame, the label within it will be moved too.
The above won't work though, and it tells me the following:
File "C:\Users\JohnSmith\AppData\Local\Programs\Python\Python37-32\lib\tkinter__init__.py", line 2480, in _create
*(args + self._options(cnf, kw))))
_tkinter.TclError: can't use .!canvas.!frame.!label in a window item of this canvas
I may be going about this in entirely the wrong way. I had looked around for something more, but if there's something I'm doing entirely wrong or an established way I should be doing this, I would appreciate it if you could point me in the right direction.

Button Layout with Tkinter?

I want to make my buttons a 2 x 2 grid on the bottom of the window with a canvas above them but the buttons always seem to stack weird when I use .pack(side = whatever).
A major thing I also want the buttons and canvas to have relative size i.e. % so that whenever the window is resized the buttons still make up the right area.
I am not sure how to do this being new to tkinter and any help is appreciated.
import tkinter
from tkinter import *
code = Tk()
def LAttack():
print(something);
def HAttack():
print(something);
def FField():
print(something);
def Heal():
print(something);
def Restart():
print(something);
Attack1 = tkinter.Button(code,text = ("Light Attack"), command = LAttack)
Attack1.pack(side = LEFT)
Attack2 = tkinter.Button(code,text = ("Heavy Attack"), command = HAttack)
Attack2.pack(side = RIGHT)
Defense1 = tkinter.Button(code,text = ("Forcefield"), command = FField)
Defense1.pack(side = LEFT)
Defense2 = tkinter.Button(code,text = ("Heal"), command = Heal)
Defense2.pack(side = RIGHT)
Restart1 = tkinter.Button(code,text = ("Restart"), command = Restart)
Restart1.pack(side = TOP)
code.mainloop()
But I want it to look like this:
Mock up for GUI
I want to make my buttons a 2 x 2 grid on the bottom of the window with a canvas above them but the buttons always seem to stack weird when I use .pack(side = whatever).
To me this means that you clearly have two separate areas to be concerned with: a top area with a canvas, and a bottom area with buttons. The first step is to create those two areas. For the top just use the canvas, and for the bottom use a frame.
I'm going to assume you want the canvas to take up as much space as possible, with the buttons always on the bottom. For that sort of arrangement, pack makes the most sense.
The following gives us a program that has a canvas at the top and a frame at the bottom to hold the buttons. When you resize the window, the button frame remains at the bottom and the canvas fills the rest of the space:
import tkinter as tk
root = tk.Tk()
canvas = tk.Canvas(root, background="white")
button_frame = tk.Frame(root)
button_frame.pack(side="bottom", fill="x", expand=False)
canvas.pack(side="top", fill="both", expand=True)
# <button code will be added here...>
root.mainloop()
Now we can focus on the buttons. You want the buttons in a 2x2 grid (though you have 5 buttons...?), so the natural choice is to use grid rather than pack. We want these buttons to be in the bottom frame, so we give that frame as the parent or master of the buttons.
You also somewhat curiously wrote "whenever the window is resized the buttons still make up the right area" even though earlier you said you wanted them on the bottom. I'm going to assume you mean that you want them in the bottom-right corner.
To accomplish this, we are going to create a grid with two rows and three columns. The column on the left will be empty, and it will take up any extra space to force the buttons to be on the right (of course, you can put things in this column if you wish)
This creates four buttons:
attack1 = tk.Button(button_frame, text="Light Attack")
attack2 = tk.Button(button_frame, text="Heavy Attack")
defense1 = tk.Button(button_frame, text="Forcefield")
defense2 = tk.Button(button_frame, text="Heal")
... this causes the first column to expand to fill any extra space:
button_frame.grid_columnconfigure(0, weight=1)
... and this lays them out in a grid (it's always good to separate widget creation from widget layout, because it makes it easier to visualize the layout in the code)
attack1.grid(row=0, column=1, sticky="ew")
attack2.grid(row=0, column=2, sticky="ew")
defense1.grid(row=1, column=1, sticky="ew")
defense2.grid(row=1, column=2, sticky="ew")
The end result is this:
When resized, the buttons retain their relative position:
Summary
The point of this is to show that you need to spend a few minutes organizing the items on the screen into logical groups. There are different layout tools for solving different problems (grid, pack, place), and most windows will benefit from using the right tool for each type of layout problem you're trying to solve (pack is good for horizontal and vertical layouts, grid is good for grids, and place covers a few edge cases where pixel-perfect control is needed).

How to display canvas coordinates when hovering cursor over canvas?

When I hover over the canvas I want some labels on top of the canvas to display x,y coordinates which stay the same if I keep my cursor still but change when I move it. How would I do this?
You can use a callback method and bind it to a Motion event.
import tkinter
root = tkinter.Tk()
canvas = tkinter.Canvas(root)
canvas.pack()
def moved(event):
canvas.itemconfigure(tag, text="(%r, %r)" % (event.x, event.y))
canvas.bind("<Motion>", moved)
tag = canvas.create_text(10, 10, text="", anchor="nw")
root.mainloop()
Also use <Enter> event. So when you switch between windows (<Alt>+<Tab> hotkey), your cursor will show the correct coordinates.
For example, you put your cursor on the canvas and <Motion> event will track it, but when you press <Alt>+<Tab> and switch to another window, then move your cursor a bit and <Alt>+<Tab> on your canvas again -- coordinates of the your cursor will be wrong, because <Motion> event doesn't track switches between windows. To fix it use <Enter> event.
import tkinter
root = tkinter.Tk()
canvas = tkinter.Canvas(root)
canvas.pack()
def get_coordinates(event):
canvas.itemconfigure(tag, text='({x}, {y})'.format(x=event.x, y=event.y))
canvas.bind('<Motion>', get_coordinates)
canvas.bind('<Enter>', get_coordinates) # handle <Alt>+<Tab> switches between windows
tag = canvas.create_text(10, 10, text='', anchor='nw')
root.mainloop()

Categories

Resources