6

This is most likely a duplicate question, but I have to ask it because other answers aren't helping in my case, since I am new to pyqt (switched from tkinter few days ago).

I am wondering if is it possible to connect to an event of a widget like this:

 self.lineEdit = QtGui.QLineEdit(self.frame)

 self.lineEdit.keyReleaseEvent(lambda: someFunction(QtCore.Qt.Key_A ))

 self.lineEdit.setObjectName(_fromUtf8("lineEdit"))

 self.horizontalLayout.addWidget(self.lineEdit)

and then...

def someFunction(event):
    print(event)
    ...

My question is how to bind to a specific event from another widget, and connect that event with a function - like btn.clicked.connect(function_goes_here).

In tkinter it's something be like this:

self.Entry.bind("<KeyRelease-a>", lambda event: someFunction(event))
ekhumoro
  • 115,249
  • 20
  • 229
  • 336

3 Answers3

6

There are a number of different ways to achieve this. A generic way to listen to all events for a given widget, is to install an event-filter on it. All protected functions have a corresponding event type that can be accessed in this way:

class MainmWindow(QMainWindow):
    def __init__(self):
        ...
        self.lineEdit = QLineEdit(self.frame)
        self.lineEdit.installEventFilter(self)

    def eventFilter(self, source, event):
        if source is self.lineEdit:
            if event.type() == QEvent.KeyRelease:
                print('key release:', event.key())
                # the following line will eat the key event
                # return True
        return super(MainmWindow, self).eventFilter(source, event)

Alternatively, you can sub-class the widget, re-implement the relevant event handler, and emit a custom signal:

class LineEdit(QLineEdit):
    keyReleased = pyqtSignal(int)

    def keyReleaseEvent(self, event):
        self.keyReleased.emit(event.key())
        super(LineEdit, self).keyReleaseEvent(event)

class MainmWindow(QMainWindow):
    def __init__(self):
        ...
        self.lineEdit = LineEdit(self.frame)
        self.lineEdit.keyReleased.connect(self.handleKeyRelease)

    def handleKeyRelease(self, key):
        print('key release:' key)

A more hackish variation on this is to overwrite the method directly:

class MainmWindow(QMainWindow):
    def __init__(self):
        ...
        self.lineEdit = QLineEdit(self.frame)
        self.lineEdit.keyReleaseEvent = self.handleKeyRelease

    def handleKeyRelease(self, event):
        print('key release:', event.key())
        QLineEdit.keyReleaseEvent(self.lineEdit, event)

Note that if you don't want to invoke the default event handling, you can omit the call to the base-class method.

ekhumoro
  • 115,249
  • 20
  • 229
  • 336
  • i like this hackish way :D , but i have one more question. Is it possible to bind two keys like " Ctrl + a ", or some other combination, because this key release event returns two key codes ,one for 'Ctrl' and other for 'a' ? Or there is another event intended to use instead of 'keyRelase', for this purpose – Dušan Atanacković Jul 29 '17 at 07:24
  • 1
    @DušanAtanacković. All you need is a simple if statement to check for any key combination you like: `if event.modifiers() == QtCore.Qt.ControlModifier and event.key() == QtCore.Qt.Key_A:`. – ekhumoro Jul 29 '17 at 13:44
  • I know that this question is not related with previous but i am wondering what OS you are using, since this questions that i am asking are related with program i am currently developing (for fun).The program i want to make is something like quick installer for Linux so i am wondering if you are interesting in that project, you can check it here if you want https://github.com/Dusan92Atanackovic/luqi.git regards – Dušan Atanacković Jul 30 '17 at 08:28
  • @DušanAtanacković. I use arch linux, where GUI installers aren't used very much. Good luck with your project. If you need any further coding help, just ask on SO. – ekhumoro Jul 30 '17 at 11:53
  • Sorry for asking to many questions, but i have found one strange behavior of QLineEdits (QLE). I have one QLE and a button next to it which calls some function, and when i press ENTER inside QLE that function is called even i have removed that KeyRelease handling, furthermore i have dynamically made inputs (QLE) bellow that one, and they also call that same function. – Dušan Atanacković Aug 08 '17 at 09:27
  • @DušanAtanacković. Please [ask a new question](https://stackoverflow.com/questions/ask), and make sure you show all the relevant code. – ekhumoro Aug 08 '17 at 13:54
  • I did, here's the link if you have time to take a look https://stackoverflow.com/questions/45584519/strange-behavior-of-qlineedit-widets-binding-button-function Thanks. – Dušan Atanacković Aug 09 '17 at 07:46
  • For me only the "hackish" way worked! But it's good. Thanks!! – ioaniatr Sep 03 '18 at 21:29
  • In the "hackish" way, if I have more than one QLineEdits that call's the same function, how can I know which one called the `handleKeyRelease()` function? (within the function) – ioaniatr Sep 03 '18 at 22:01
  • @ioaniatr Same as you would with any other method: `if self is whatever:`. But really, the other solutions are far superior to resorting to such crude hackery. – ekhumoro Sep 03 '18 at 22:43
  • With `self` you get the `QMainWindow`, not the lineEdit that triggered the event. – ioaniatr Sep 03 '18 at 22:52
  • @ioaniatr Of course you do - that's why it's a hack. You obviously need to create a [proper instance method](https://stackoverflow.com/q/972/984421) to make it work. But I don't really see the point when there are two better solutions available. – ekhumoro Sep 04 '18 at 17:28
1

I don't have enough reputation to reply to @ekhumoro, but wanted to add to their answer with an applied answer specific to MouseEvents for those who might benefit, based on a similar issue I was having.

Scenario

Display an image in the central widget, and connect a custom signal to capture mouse double click actions in the central widget.

Source for test image: https://en.wikipedia.org/wiki/Standard_test_image#/media/File:SIPI_Jelly_Beans_4.1.07.tiff

Consider the following:

A basic QMainWindow class which loads a supplied image_file and shows the image in a custom sub-class of the QLabel class, fit to the window:

import sys

from PyQt5.QtCore import Qt, pyqtSignal
from PyQt5.QtGui import QPixmap, QImage, QPalette, QMouseEvent
from PyQt5.QtWidgets import (QApplication, QMainWindow, QLabel, QScrollArea)


class ImageView(QMainWindow):
    def __init__(self):
        super().__init__()
        self.initializeUI()
        self.image = QImage()

    def initializeUI(self):
        self.setMinimumSize(300, 200)
        self.setWindowTitle("Image Viewer")
        # self.showMaximized()
        self.createMainQLabel()
        self.show()

    def createMainQLabel(self):
        """Create an instance of the imageQLabel class and set it
           as the main window's central widget."""
        self.image_qlabel = imageQLabel(self)
        self.image_qlabel.resize(self.image_qlabel.pixmap().size())

        self.scroll_area = QScrollArea()
        self.scroll_area.setBackgroundRole(QPalette.Dark)
        self.scroll_area.setAlignment(Qt.AlignCenter)
        self.scroll_area.setWidget(self.image_qlabel)
        self.setCentralWidget(self.scroll_area)


class imageQLabel(QLabel):
    """Subclass of QLabel for displaying image"""

    def __init__(self, parent, image_file="images/SIPI_Jelly_Beans_4.1.07.tiff"):
        super().__init__(parent)
        self.openImage(image_file)
        self.setScaledContents(True)
        self.setAlignment(Qt.AlignCenter)

    def openImage(self, image_file):
        # Get image, create the pixmap and resize to fit window
        self.image = QImage(image_file)
        self.setPixmap(QPixmap().fromImage(self.image))
        self.resize(self.pixmap().size())

    """
    Other methods used to customize the class, for example:
        - Resize 
        - Rotate
        - Crop
        - ...
    ...

    """


if __name__ == "__main__":
    app = QApplication(sys.argv)
    app.setAttribute(Qt.AA_DontShowIconsInMenus, True)
    window = ImageView()
    sys.exit(app.exec_())

When executed, yield a simple window with the image: SIPI Test Image shown in PyQT5 Window

What we would like to do is add a mouseDoubleClickEvent to the imageQLabel sub-class, but ensure that other widgets or classes in the program can access the event signals. To do this, you can modify @ekhumoro's answer #2 to accommodate a pyqtSignal.

First, add the signal to the attributes of the imageQLabel class. Here, we need the full QMouseEvent rather than just an int:

import sys

from PyQt5.QtCore import Qt, pyqtSignal
from PyQt5.QtGui import QPixmap, QImage, QPalette, QMouseEvent
from PyQt5.QtWidgets import (QApplication, QMainWindow, QLabel, QScrollArea)

class imageQLabel(QLabel):
    """Subclass of QLabel for displaying image"""

    # Class attributes
    mouseDoubleClickSignal = pyqtSignal(QMouseEvent)

We wish to pass the event from that pyqtSignal to some other method in our ImageView main window class. As an example, let's make a method to print the cursor position when the user double clicks in the imageQLabel sub-class.

In ImageView:

class ImageView(QMainWindow):
    """
    ...
    The rest of the class code
    ...
    """
    def print_mouse(self, event):
        print("print_mouse event from QMainWindow: {}".format((event.x(), event.y())))

Now we just need to connect print_mouse to the custom pyqtSignal we made. It makes the most sense to do this in the initializeUI method:

    def initializeUI(self):
        self.setMinimumSize(300, 200)
        self.setWindowTitle("Image Viewer")
        # self.showMaximized()
        self.createMainQLabel()

        # Connect the signal from custom imageQLabel class
        # to the QMainWindow
        self.image_qlabel.mouseDoubleClickSignal.connect(self.print_mouse)
        self.show()

These modifications now print the cursor position when double clicking inside of the imageQLabel sub-class in the central widget. The call to print_mouse is outside of the sub-class. Moreover, double-clicking outside of the central widget (like outside of the frame of the image) does not call print_mouse because the mouseDoubleClickEvent is only active for the central widget.

%user profile%\AppData\Local\Programs\Python\Python39\python.exe 
%script path%\stackoverflowsolution.py
print_mouse event from QMainWindow: (115, 140)
print_mouse event from QMainWindow: (54, 55)
0

override using python lambda magic

def on_key(line_edit : QLineEdit, key):
    print(key)

...

line_edit = self._lineedit = QLineEdit()
line_edit.keyReleaseEvent = lambda ev, self=line_edit, super=line_edit.keyReleaseEvent: \
                            (super(ev), on_key(self, ev.key()), None)[-1]
        

We save current keyReleaseEvent function in lambda kwarg super.

Also we need to save line_edit in lambda kwarg self, because line_edit will be removed from locals during lambda execution.

Finally we need to return None, thus get it from last element from our Tuple.

iperov
  • 455
  • 7
  • 8