Attaching event to self (canvas) tkinter - python

i have created a class in python that extends the tkinter canvas. I am trying to attach an event to this canvas to handle click's within the class. It functions if i attach the event outside of the class itself but when binding within the class the click event only occur's once and then proceeds not to do anything at all only performing the first click:
class myCanvas(Canvas):
def callback(event):
print('clicked at', event.x, event.y)
def __init__(self, parent, **kwargs):
Canvas.__init__(self, parent, **kwargs)
self.bind("<Button-1>", self.callback())
self.height = self.winfo_reqheight()
self.width = self.winfo_reqwidth()
Binding the event functions correctly only if i attach the event outside of the class. Any help in finding a way to attach the event to the extended canvas would be appreciated.

The problem is in this line:
self.bind("<Button-1>", self.callback())
You need to connect something callable (in other words, a function) to the event. The function is referenced as self.callback. If you call the function (self.callback()) then you're connecting the return value of self.callback() to the click event instead of the function itself.

Related

Trigger a function on clicking QFrame

I am working on a project with PyQt5 which has QFrames. I am using mouse press event to trigger a function on clicking frame as below :
frame.mousePressEvent = lambda x: print_name(x, name)
Above line is not executed at the start, It is executed after user has done some work in UI.
I am getting the behaviour I want but here is the problem:
If the user clicks the frame after the above line of code is executed, it works fine but if the user clicks on the frame before the above line of the code is executed and later again clicks the frame (after code is executed), I am not getting the same behaviour. Basically nothing happens.
I want to know where is the problem and how do I solve it?
The problem is caused because PyQt5 caches the methods so if the method is assigned then it cannot be changed. Instead of following the bad practice of assigning methods to mousePressEvent there are other better alternatives such as:
Implement inheritance
class Frame(QFrame):
def mousePressEvent(self, event):
super().mousePressEvent(event)
print(event)
Use an event filter
class MouseObserver(QObject):
def __init__(self, widget):
super().__init__(widget)
self._widget = widget
self.widget.installEventFilter(self)
#property
def widget(self):
return self._widget
def eventFilter(self, obj, event):
if obj is self.widget and event.type() == QEvent.MouseButtonPress:
print(event)
return super().eventFilter(obj, event)
Then
observer = MouseObserver(frame)
The second seems the most appropriate for your case.

PyQt5 QWidget event blocks other events. How to avoid it?

I have the next problem:
A created a custom widget (simple derived QWidget class). When I middle-mouse click on it - it creates another QWidget class (sort of context menu). When I release middle-mouse button - that context widget disappears. So that works. What does not work is - that context widget also has some content added, like other small widgets, icons, etc and they all have their own custom events (simple example - enterEvent and leaveEvent with prints indicating those events). But those inner widget events are not working, they are blocked while I keep middle-mouse pressed. When I release it - context widget disappears. Would like to know if there is any solution to let inner widgets'events work as expected.
Here is a minimal example where inner widget does not run mouse events as they are blocked by MainWidget event:
from PyQt5 import QtWidgets, QtGui, QtCore
class SomeSmallWidget(QtWidgets.QWidget):
def __init__(self, increment=1, globalValue=0):
super(SomeSmallWidget, self).__init__()
# UI
self.setMinimumWidth(40)
self.setMaximumWidth(40)
self.setMinimumHeight(40)
self.setMaximumHeight(40)
self.mainLayout = QtWidgets.QVBoxLayout()
self.setLayout(self.mainLayout)
def enterEvent(self, event):
print('Entered') # NOT WORKING
super(SomeSmallWidget, self).enterEvent(event)
def leaveEvent(self, event):
print('Leaved') # NOT WORKING
super(SomeSmallWidget, self).leaveEvent(event)
class ContextWidget(QtWidgets.QWidget):
def __init__(self, parent=None):
super(ContextWidget, self).__init__()
self.setMouseTracking(True)
# position
point = parent.rect().topLeft()
global_point = parent.mapToGlobal(point)
self.move(global_point - QtCore.QPoint(0, 0))
self.innerWidget = SomeSmallWidget() # Here is that small widget, which event does not work
self.mainLayout = QtWidgets.QVBoxLayout()
self.setLayout(self.mainLayout)
self.mainLayout.addWidget(self.innerWidget)
class MainWidget(QtWidgets.QLineEdit):
def __init__(self, value='0'):
super(MainWidget, self).__init__()
self.setMouseTracking(True)
self.popMenu = None
def mousePressEvent(self, event):
if event.button() == QtCore.Qt.MiddleButton: # while we keep MMB pressed - we see context widget
self.popMenu = ContextWidget(parent=self)
self.popMenu.show()
super(MainWidget, self).mousePressEvent(event)
def mouseReleaseEvent(self, event):
if event.button() == QtCore.Qt.MiddleButton:
if self.popMenu:
self.popMenu = None
Events are not blocked, all mouse events are sent to widget that was under cursor when mouse button was pressed. Usually (always) it makes more sense. Imagine two buttons next to each other. Suppose user pressed one, moved cursor and released over second button. What was his intention? He probably changed his mind. If button triggers action on mouse press - this options will not be available and it's probably too soon, if button triggers action on mouse release, which button should recieve mouserelease event? If we send mouseevent to second button - that was not pressed it will trigger action that used didn't want. If we dont send mouserelease event to first button - it will stay in sunken mode. Imagine user is selecting text in lineedit, and while selecting he leaves lineedit and goes to other widgets, should they react somehow, and should focus be switched? Probably not. So there is only one active window and only one focused widget at a time and it receives keyboard and mouse input and reacts to it. Most of menus are shown after mouserelease and closes on next mouseclick, providing better user experience.
However, If you still want your widget to receive mouse events, you can achive this by translating it from ContextWidget to SomeSmallWidget like this:
class SomeSmallWidget(QtWidgets.QWidget):
...
def paintEvent(self, event):
painter = QtGui.QPainter(self)
painter.fillRect(self.rect(), QtCore.Qt.blue)
def onMouseEnter(self):
print('onMouseEnter')
def onMouseLeave(self):
print('onMouseLeave')
class MainWidget(QtWidgets.QLineEdit):
...
def mouseMoveEvent(self, event):
if self.popMenu:
self.popMenu.mouseTest(event.globalPos())
class ContextWidget(QtWidgets.QWidget):
def __init__(self, parent=None):
...
self._inside = False
def mouseTest(self, p):
widget = self.innerWidget
rect = QtCore.QRect(widget.mapToGlobal(QtCore.QPoint(0,0)), widget.size())
inside = rect.contains(p)
if inside != self._inside:
if inside:
widget.onMouseEnter()
else:
widget.onMouseLeave()
self._inside = inside
Notice I added paintEvent to see the widget bounds.

Destroying login page window and opening a new one

I'm trying to close a window while creating a new one. The following function runs once a login has been entered. Do you have any suggestions on how to close this window while the MenuPage(window) is opened?
from tkinter import *
class LoginPage():
def __init__(self):
window = Tk()
#UI to retrieve login details
#While loop when submit is clicked.
if UserLogins:
#Maybe here a close window function is ran
window = Toplevel()
start= MenuPage(window)
else:
print('Incorrect Login')
class MenuPage(object):
#Menu UI
if __name__ == '__main__':
LoginPage()
Hello and welcome to StackOverflow.
Your main issue is your class LoginPage.
You do initialize Tk() inside the __init__ of your class. As soon as this object is destroyed, the Tkinter Instance gets destroyed as well.
Consider using the following approach:
Create a Main Widget (e.g. based on Toplevel or Tk)
Create a LoginPage based on e.g. Frame inside your Main Widget
After login attempt, either destroy the LoginPage and create another Frame holding new data. (MenuPage)
Using this approach your Tkinter Main Instance Runs all the time, and you delete or add objects / widgets to it.
If you do not use your MainWidget (that should persist) as Tk-Instance, an "unnamed" Instance will be used for that. Alternatively use Tk() as Parent for your LoginPage, but do not embed it inside it.
Pseudo Code Example:
import tkinter as tk
class MainWidget(tk.Tk):
def __init__(self, *args, **kwargs):
tk.Tk.__init__()
self.__load_widgets()
def __load_widgets(self):
self.page = LoginPage(self)
self.page.bind("&ltLoginEvent>", self.show_menu)
self.page.grid(sticky=tk.NW+tk.SE)
def show_menu(self, event=None):
self.page.destroy()
self.page=MenuPage(self)
self.page.grid(sticky=tk.NW+tk.SE)
class LoginPage(tk.Frame):
def __init__(self, *args, **kwargs):
tk.Frame.__init__(self, *args, **kwargs)
def raise_login_result(self):
""" some event may raise the event you can use from main widget to bind to """
# ...
raise("&ltLoginEvent>")
class MenuPage(tk.Frame):
def __init__(self, *args, **kwargs):
tk.Frame.__init__(self, *args, **kwargs)
if __name__ == "__main__":
app = MainWidget()
app.mainloop()
Edit
As your question expands into geometry managers, I hereby refer to one corresponding documentation on geometry managers(that is e.g. grid/pack/place). This documentation shows the corresponding interfaces.

Trouble with Tkinter event handler inside of a class

My problem is that I have a class that creates a Tkinter topclass object and then puts a field into it and I want to add an event handler that runs a method (that is also in the class) every time a button is pressed but when the event is called it says
AttributeError: Toplevel instance has no attribute 'updateSearch'
class EditStudentWindow():
def __init__(self):
searchResultList = ['student1', 'student2', 'student3'] # test list
##### window attributes
# create window
self = Tkinter.Toplevel()
#window title
self.title('Edit Students')
##### puts stuff into the window
# text
editStudentInfoLabel = Tkinter.Label(self,text='Select the student from the list below or search for one in the search box provided')
editStudentInfoLabel.grid(row=0, column=0)
# entry box
searchRepositoryEntry = Tkinter.Entry(self)
searchRepositoryEntry.grid(row=1, column=0)
# list box
searchResults = Tkinter.Listbox(self)
searchResults.grid(row=2, column=0)
##### event handler
right here
searchRepositoryEntry.bind('<Key>',command = self.updateSearch)
# search results
for result in searchResultList:
searchResults.insert(Tkinter.END, result)
def updateSearch(self, event):
print('foo')
Judging solely on the indentation of your example, it appears that updateSearch is indeed not part of the class definition.
Assuming the indentation is a markup mistake, and based on the error message you report, the other problem is that you redefine self, so 'self.updateSearch' points to the toplevel rather than the EditStudentWindow class. Note that the message says Toplevel instance has no attribute 'updateSearch' rather than EditStudentWindow instance...
Typically, such widgets are created with inheritance rather than composition. You might want to consider refactoring your code to look something like:
class EditStudentWindowClass(Tkinter.Toplevel):
def __init__(self, *args, **kwargs):
Tkinter.Toplevel.__init__(self, *args, **kwargs)
self.title('Edit Students')
...

How can I handle a mouseMiddleDrag event in PythonCard?

I would like to use the middle mouse button to drag an image in an application written in Python and using PythonCard/wxPython for the GUI.
The latest version of PythonCard only implements a "left mouse button drag" event and I am trying to modify PythonCard to handle a "middle mouse button drag" as well.
Here is the relevant code from Lib\site-packages\PythonCard\event.py :
class MouseMoveEvent(MouseEvent, InsteadOfTypeEvent):
name = 'mouseMove'
binding = wx.EVT_MOTION
id = wx.wxEVT_MOTION
def translateEventType(self, aWxEvent):
if aWxEvent.Dragging():
return MouseDragEvent.id
else:
return self.id
class MouseDragEvent(MouseMoveEvent):
name = 'mouseDrag'
id = wx.NewEventType()
class MouseMiddleDragEvent(MouseMoveEvent): #My addition
name = 'mouseMiddleDrag'
id = wx.NewEventType()
My addition does not work. What can I do instead? Is there a specific wxPython method that I could use to bypass PythonCard?
It turns out the the mouseDrag event is active regardless of which button on the mouse is pressed. To filter the middle mouse button, you need to call the MiddleIsDown() method from the MouseEvent.
def on_mouseDrag( self, event ):
do_stuff()
if event.MiddleIsDown():
do_other_stuff()

Categories