I have been trying to write simple CAD application in Python. I was tinkering with pyglet, got some results: , but I decided to switch back to Gtk and I hit the wall:
I cannot get the mouse pointer position over the EventBox (with pyglet it was the label in the bottom left corner of the window app in the picture). What signal is designed for it? Or should I use another approach?
I will appreciate any piece of information or resources. Thanks in advance.
Question: How to detect the mouse position over EventBox?
how-to-capture-event-on-event-box-to-detect-mouse-movement-in-gtk
Gtk.EventBox
The Gtk.EventBox widget is a subclass of Gtk.Bin which also has its own window. It is useful since it allows you to catch events for widgets which do not have their own window.
Gtk.Widget.add_events(events)
Adds the events in the bitfield events to the event mask for self.
Gdk.EventMask
A set of bit-flags to indicate which events a window is to receive.
GDK_MOTION_NOTIFY
the pointer (usually a mouse) has moved
The Gtk.EventBox should have got the flag, not the window!
box = Gtk.EventBox()
box.connect("motion-notify-event", self.on_mouse_move)
box.add_events(Gdk.EventMask.POINTER_MOTION_MASK)
With the help of stovfl the solution was achieved.
The application
consisted of a drawing area, a frame, an event box and a label, for displaying of the mouse pointer coordinates. The drawing area was added to the box, the box was added to the frame, which was added to the grid.
from gi.repository import Gtk
from gi.repository import Gdk
# (...)
box = Gtk.EventBox()
box.add_events(Gdk.EventMask.POINTER_MOTION_MASK) # 1
box.connect("button-press-event", self.on_click)
box.connect("motion-notify-event", self.on_mouse_move) # 2
self.canvas = Gtk.DrawingArea()
self.canvas.connect("draw", self.on_draw)
self.canvas.set_size_request(800, 600)
box.add(self.canvas)
grid = Gtk.Grid()
frame = Gtk.Frame()
frame.set_label("KHAD")
frame.add(box)
grid.attach(frame, 0, 1, 1, 1)
self.add(grid)
# (...)
self.locationLabel = Gtk.Label("X,Y")
self.locationLabel.set_alignment(0, 0)
grid.attach(self.locationLabel, 0, 2, 1, 1)
The solution was:
Add POINTER_MOTION_MASK to the event box: box.add_events(Gdk.EventMask.POINTER_MOTION_MASK).
Connect motion-notify-event of the box with a method, which read and updated a label (down-left corner; it was aligned with self.locationLabel.set_alignment(0, 0)).
Related
I've got an image (drawn in Pillow) with a transparent background. I can add this onto a Tkinter canvas via create_image() and bind mouse events to it, e.g. button clicks.
However, Tkinter canvas triggers the mouse events for the entire image including the transparent background.
Is there any way in Tkinter to have mouse events ignore an image's transparent pixels?
Everything else works fine and as expected, however I've not been able to find any option or if it's even possible to have Tkinter ignore mouse events for transparency in images?
Thanks in advance to anyone who knows!
EDIT2: On request, more details. The situation is that I've got a map which serves as the background of the canvas object, on this map I draw routes, and I want those routes to react to mouse events such as clicks.
for route in travel_routes:
route_map = route.draw_route_map(canvas_width, canvas_height)
self.CanvasTravelMapRoutes.append(ImageTk.PhotoImage(route_map))
self.CanvasTravelMap.create_image(
0, 0, image = self.CanvasTravelMapRoutes[-1], anchor = tk.NW, tag = (route.RouteID, 'travel_route') )
self.CanvasTravelMap.tag_bind('travel_route', "<Button-1>", self.travel_canvas_route_click)
In the route class, the draw_route_map method works as follows:
route_map = Image.new("RGBA", (canvas_width, canvas_height), (0, 0, 0, 0))
draw = ImageDraw.Draw(route_map)
draw.line()
draw.ellipse()
etc. etc.
The result is a route_map image laid on top of the canvas, but which is mostly transparent except for the lines and circles drawn to mark the route's course across the map.
All elements of this code function as designed, except that the canvas mouse events also respond to the transparent parts of the route_map images. Obviously, this means any click anywhere on the canvas always selects whichever route_map is on top and not the specific line/mark being clicked on.
So if you wanted to have this kind of collision you could try this:
from tkinter import Tk, Canvas
from PIL import Image, ImageTk
import requests
url = 'http://media-s3-us-east-1.ceros.com/g3-communications/images/2019/01/15/05eea4b9b9ce010d2dd6b0c063d2f5ca/p1-blob.png?imageOpt=1&fit=bounds&width=893'
data = requests.get(url, stream=True).raw
image = Image.open(data)
def on_opaque(event, offset, img):
print('clicked on bounding box')
x, y = event.x - offset[0], event.y - offset[1]
if img.getpixel((x, y)) != 0:
print('clicked on opaque pixel')
root = Tk()
photo = ImageTk.PhotoImage(image)
canvas = Canvas(root, height=image.height, width=image.width, highlightthickness=0)
canvas.pack(fill='both', expand=True)
offset_x, offset_y = 0, 0
c_img = canvas.create_image(offset_x, offset_y, image=photo, anchor='nw')
canvas.tag_bind(c_img, '<Button-1>', lambda e: on_opaque(e, (offset_x, offset_y), image))
root.mainloop()
I use image from internet, you can use your own, it is just that you can test it immediately. Basically it binds the whole image to button click, but then in the callback it additionally checks if the click was in a position where there is not a transparent background, also be careful with fake transparent background, I then suggest that you set the background of canvas to black and see if the bg behind image is black too.
I'm trying to create a simple drag-and-drop interface in Tkinter using the canvas. However, I've hit a snag:
The event binds 'enter' and 'leave' seem to only work for the TOP-most widget under the cursor. E.g., I wrote this bit of code to try and recognize when the cursor is over the 'board' space or not:
self.canvas.create_rectangle(50, 50, 1250, 750, fill = 'grey', tag = 'Board')
self.canvas.tag_bind('Board', "<Enter>", self.on_board)
self.canvas.tag_bind('Board', "<Leave>", self.off_board)
self.mouse_on_board = False
def on_board(self, event):
self.mouse_on_board = True
def off_board(self, event):
self.mouse_on_board = False
But when the cursor enters a draggable item on the board (e.g. a canvas.create_image() image drawn on top of the 'board' rectangle) this is treated as the cursor leaving the board widget. Whereas for my purpose, I want the code to recognize it is still over the board (just over another widget which is itself drawn over the board).
Is there a simple way in Tkinter to know whether the cursor is on top of a widget REGARDLESS of whether there are any other widgets stacked above or beneath it?
Thanks in advance for any answers!
I am using PyQt5 to design GUI for image analysis.The image window is designed using plotWidget, and hence user can zoom in/out the displayed image using mouse.
Issue description: when user click a position, the software has mouse click event with callback of the (x, y) coordinate of the mouse on the image. However after image zooming in/out, the coordinates callback at same clicked point is not changing. (I expected the coordinates should change with IMAGE zoom in/out, NOT the absolute positions respect to the GUI window).
Is there way to achieve this?
Qt reports the mouse position in pixels relative to the widget receiving the mouse event. You need to map the mouse position to the coordinate system of the image. Pyqtgraph's image analysis example shows one way to do this--if you get the event directly from the ImageItem, it will already have been mapped for you:
https://github.com/pyqtgraph/pyqtgraph/blob/develop/examples/imageAnalysis.py#L97
An alternative is to do the mapping yourself:
import pyqtgraph as pg
import numpy as np
class PW(pg.PlotWidget):
def __init__(self):
pg.PlotWidget.__init__(self)
self.img = pg.ImageItem(np.random.normal(size=(100, 100)))
self.addItem(self.img)
def mouseMoveEvent(self, ev):
scene_pos = self.mapToScene(ev.pos())
img_pos = self.img.mapFromScene(scene_pos)
print(img_pos)
return pg.PlotWidget.mouseMoveEvent(self, ev)
pg.mkQApp()
pw = PW()
pw.show()
I'm pretty new to Python and I'm currently playing a bit with Tkinter which looks amazingly simple. I've tried to implement a simple drag and drop effect using the following code (right mouse button creates a circle, left mouse button allows dragging) :
from tkinter import *
class Point:
def __init__(self, ref, x, y):
self.ref = ref
self.x = x
self.y = y
points = []
selected = None
def OnSelect(event):
global selected
for p in points:
if event.x>=(p.x-10) and event.y>=(p.y-10) and event.x<(p.x+10) and event.y<(p.y+10):
selected = p
break
def OnMMove(event):
if selected is not None:
selected.x = event.x
selected.y = event.y
canvas.coords(selected.ref, event.x-10, event.y-10, event.x+10, event.y+10)
def OnStopDrag(event):
global selected
selected = None
def OnCreate(event):
point = canvas.create_oval(event.x-10, event.y-10, event.x+10, event.y+10, fill="black")
points.append(Point(point, event.x, event.y))
window = Tk()
window.wm_title("Python")
canvas = Canvas(window, width=800, height=600, background='white')
canvas.bind("<Button-1>", OnSelect)
canvas.bind("<B1-Motion>", OnMMove)
canvas.bind("<ButtonRelease-1>", OnStopDrag)
canvas.bind("<Button-3>", OnCreate)
canvas.pack(fill=BOTH, expand=YES)
window.mainloop()
As can be seen in this code, I'm using canvas.cords to move the dragged object around. Everything works fine when the mouse cursor is slowly moved while dragging, however when the mouse cursor is moved rapidly, the dragged circle seems to be partialy clipped in a rectangle while moved as seen on this picture (the whole circle is correctly drawn entirely again when dragging stops or slows down) :
I've already encountered a similar issue whend using GDI in a Win32 C application, when calling screen invalidation to repaint the window client area on the sole area covered by the initial position of the circle being currently dragged.
And indeed, when the window created in my example code is placed above a window which is constantly and entirely being redrawn, like a video game window, the cropping effect while dragging elements is not seen and the whole circle is correctly being redrawn as it is dragged.
Is there a way to solve this issue, like a canvas setting making window invalidation being called on a wider or the whole client area ? I would like to stick with Tkinter so I'm not really interested in switching to another GUI API/framework. This code has been tested on Windows 10.
This answer may confuse you (it does me). But a solution is to configure the cursor within OnMMove. Here is the excerpt that worked on Windows for me.
def OnMMove(event):
if selected is not None:
canvas.configure(cursor='arrow')
selected.x = event.x
selected.y = event.y
canvas.coords(selected.ref, event.x-10, event.y-10, event.x+10, event.y+10)
How can I give a widget a fixed position? Like so I can "attach"/put it at the bottom of the window and it will always be there; even when the window is expanded. I couldn't find anything useful on how to do it and, I suppose obviously, none of the obvious things work (resize(), setGeometry(), etc.). Any help?
I assume by "fixed position" you mean a position relative to one of the window edges. That's what your second sentence implies. So that's the question I will answer.
Use a layout manager with stretches and spacings. Here's a simple example to attach a widget "w" to the bottom of a window "win". This code typically gets called by (or goes inside) your window's constructor.
lay = QVBoxLayout(win)
lay.addStretch(1)
lay.addWidget(w)
The BoxLayout makes "w" stick to the bottom of the window and stay in that position as the window is resized.
you must reimplement the parent windows resizeEvent function, here is the code to make a widget "attached" to the bottom of the window:
def resizeEvent(self, event):
#widget.move(x, y)
self.bottom_widget.move(0, self.height() - self.bottom_widget.height())
#if you want the widgets width equal to window width:
self.bottom_widget.setWidth(self.width())
whenever the window is resized, this function will be called and it will move the widget to the bottom of the window. this is an absolute positioning approach, but you can always use QSpacerItem to push your widget to the bottom.