Home > database >  How should I differentiate between whether a click happened in a QListWidget or a QWidget that is wi
How should I differentiate between whether a click happened in a QListWidget or a QWidget that is wi

Time:08-21

Grееtings аll. I am new to this site, so go easy on me.

I am building a program in python using PyQt5 for the interface. I currently have, among other things, a QListWidget (list window) into which I insert a number of QWidgets (list items) through a function during the running of the program. I have implemented an eventFilter by subclassing QObject, and I use it to differentiate between left and right click, and from that I send to the controller class to handle one or the other click accordingly.

I have made it so that when right-clicked on a list item, a context menu appears. However, I also need a context menu to appear when the list window is clicked (not on a list item). The problem which occurs is that when right-clicked on a list item, both the list item context menu and list window context menu appear. This must be because the event filter recognises the click as occurring within the list window, because it is occurring on a list item, which is within the list window. What I need is that when right-clicked on a list item, only its context menu appears, and similarly for the list window, when right-clicked outside the list items.

I have tried checking if source equals the widget where the event appeared, but it seems to recognise both widgets' events independently and if I gate the call to the handler with an if condition, one of the handlers never receives a call. I have searched around the web and this site, but I have found nothing of use. Perhaps it is due to me not being a native speaker and so not being able to phrase things correctly (you can see how clunky my phrasing is).

Below follows some code extracted from my program. Note that anything irrelevant has been cut away to make for a minimal example. For your convenience, I have also merged the GUI files into one and did the same for the control files. I have tested this minimal example and it reproduces the problem. It could not get smaller, so if you deem the code listed below too long, notify me and I can reupload it to GitHub if it is allowed to show the minimal example that way instead of putting code into the question directly.

custom_classes.py:

from PyQt5.QtCore import Qt, QEvent, QObject

class MyFilter(QObject):
    def __init__(self, parent, ctrl):
        super().__init__(parent)
        self._parent = parent
        self.ctrl = ctrl
        self.parent.installEventFilter(self)

    @property
    def parent(self):
        return self._parent

    def eventFilter(self, source, event):
        if event.type() == QEvent.MouseButtonPress:
            if event.button() == Qt.LeftButton:
                self.ctrl.handle_left_click()
            elif event.button() == Qt.RightButton:
                self.ctrl.handle_right_click(event)
        return super().eventFilter(source, event)

gui.py:

from PyQt5.QtWidgets import QWidget
from PyQt5.QtWidgets import QLabel
from PyQt5.QtWidgets import QHBoxLayout
from PyQt5.QtWidgets import QVBoxLayout
from PyQt5.QtWidgets import QListWidget
from PyQt5.QtWidgets import QScrollArea

from PyQt5.QtCore import Qt

class MainFrame(QWidget):
    def __init__(self):
        super().__init__()
        
        self.main_layout = QHBoxLayout()
        
        self.setLayout(self.main_layout)

class ListItemFrame(QWidget):
    def __init__(self):
        super().__init__()
        
        self.main = QHBoxLayout()
        self.main.setContentsMargins(0,0,0,0)

        self.name_layout = QHBoxLayout()
        self.name_layout.setContentsMargins(0,0,0,0)
        self.main.addLayout(self.name_layout)

        self.name = QLabel("")
        self.name.setMaximumHeight(20)
        self.name_layout.addWidget(self.name)
        
        self.setLayout(self.main)

class ListFrame(QListWidget):
    def __init__(self):
        super().__init__()

        self.main = QVBoxLayout()

        self.scroll_widget = QScrollArea()
        self.scroll_widget.setWidgetResizable(True)

        self.scroll_layout = QVBoxLayout()
        self.scroll_layout.setAlignment(Qt.AlignTop)

        self.scroll_layout_widget = QWidget()
        self.scroll_layout_widget.setLayout(self.scroll_layout)

        self.scroll_widget.setWidget(self.scroll_layout_widget)

        self.main.addWidget(self.scroll_widget)

        self.setLayout(self.main)

ctrl.py:

from PyQt5.QtWidgets import QMenu

from gui import ListFrame, ListItemFrame
from custom_classes import MyFilter

class Controller:
    def __init__(self, ui, app):
        self.ui = ui
        self.app = app

        self.list_ = ListControl(self)

class ListControl:
    def __init__(self, ctrl):
        self.ctrl = ctrl
        self.ui = ListFrame()

        self.the_list = self.get_list() #list of stuff

        self.item_list = [] #list of list items

        self.ctrl.ui.main_page.main_layout.addWidget(self.ui)
        self.index = self.ctrl.ui.main_page.main_layout.count() - 1

        self.filter = MyFilter(self.ui, self)

        self.show_list()
        
    def handle_left_click(self):
        pass #other irrelevant function

    def handle_right_click(self, event):
        self.show_options(event)

    def show_options(self, event):
        menu = QMenu()

        one_action = menu.addAction("Something!")
        quit_action = menu.addAction("Quit")

        action = menu.exec_(self.ui.mapToGlobal(event.pos()))

        if action == quit_action:
            self.ctrl.ui.close()
        elif action == one_action:
            self.something()
            
    def something(self):
        print("Something!")

    def show_list(self):
        for info in self.the_list:
            item = ListItem(self, info)
            self.item_list.append(item)
            
    def get_list(self):
        return [x for x in "qwertzuiopasdfghjklyxcvbnm"]

class ListItem:
    def __init__(self, main, info): 
        self.main = main
        self.info = info*10
        self.ui = ListItemFrame()

        self.filter = MyFilter(self.ui, self)
        
        self.set_ui()
        self.add_to_ui()
        
        self.main.ui.scroll_layout.addWidget(self.ui)

    def handle_left_click(self):
        pass #other irrelevant function

    def handle_right_click(self, event):
        self.show_options(event)

    def show_options(self, event):
        menu = QMenu()

        item_action = menu.addAction("Hello!")
        quit_action = menu.addAction("Quit")

        action = menu.exec_(self.ui.mapToGlobal(event.pos()))

        if action == quit_action:
            self.main.ctrl.ui.close()
        elif action == item_action:
            self.hello()

    def hello(self):
        print(f"Hello! I am {self.info}")

    def set_ui(self):
        self.ui.name.setText(self.info)

    def add_to_ui(self):
        self.main.ui.scroll_layout.insertWidget(
            self.main.ui.scroll_layout.count() - 1, self.ui
        )

main.py:

import sys

from PyQt5.QtWidgets import QApplication
from PyQt5.QtWidgets import QStackedLayout
from PyQt5.QtWidgets import QWidget

from gui import MainFrame
from ctrl import Controller

class Window(QWidget):
    def __init__(self):
        super().__init__()
        
        self.setWindowTitle("minimal example")

        self.stacked = QStackedLayout()

        self.main_page = MainFrame()
        self.stacked.addWidget(self.main_page)

        self.setLayout(self.stacked)

if __name__ == "__main__":
    app = QApplication(sys.argv)
    app.setStyle("Fusion")
        
    window = Window()
    window.show()

    c = Controller(window, app)

    sys.exit(app.exec())

To reiterate, the context menu appears for both the list item and the list window when a list item is right-clicked. What I need is for it to appear only for the list item if a list item is right-clicked.

Edit: seems the site bit off a part of my introduction. Readded it!

CodePudding user response:

this is probably not the best way to do it but it works. You just create a global variable, for example list_element_clicked and when you click "hello" (of course not quit because you are going to exit the window and there is no point) you set that variable to True. If that variable is False, you show the ListControl context menu, and if not, you set that variable to True, so next time if you click on your ListControl it will appear, and if you click on ListItem it will not.

Finally there is an extra case, if you don't click anywhere after clicking on ListItem, nothing will happen (the ListControl is not shown and the variable is not changed) so everything will work perfectly next time.

So here is the code:

ctrl.py:

from PyQt5.QtWidgets import QMenu
from gui import ListFrame, ListItemFrame
from custom_classes import MyFilter
list_element_clicked = False

class Controller:
    def __init__(self, ui, app):
        self.ui = ui
        self.app = app

        self.list_ = ListControl(self)

class ListControl:
    def __init__(self, ctrl):
        self.ctrl = ctrl
        self.ui = ListFrame()

        self.the_list = self.get_list() #list of stuff

        self.item_list = [] #list of list items

        self.ctrl.ui.main_page.main_layout.addWidget(self.ui)
        self.index = self.ctrl.ui.main_page.main_layout.count() - 1

        self.filter = MyFilter(self.ui, self)

        self.show_list()
        
    def handle_left_click(self):
        pass #other irrelevant function

    def handle_right_click(self, event):
        global list_element_clicked
        if(list_element_clicked == False):
            self.show_options(event)
        else:
            list_element_clicked = False

    def show_options(self, event):
        menu = QMenu()

        one_action = menu.addAction("Something!")
        quit_action = menu.addAction("Quit")

        action = menu.exec_(self.ui.mapToGlobal(event.pos()))

        if action == quit_action:
            self.ctrl.ui.close()
        elif action == one_action:
            self.something()
            
    def something(self):
        print("Something!")

    def show_list(self):
        for info in self.the_list:
            item = ListItem(self, info)
            self.item_list.append(item)
            
    def get_list(self):
        return [x for x in "qwertzuiopasdfghjklyxcvbnm"]

class ListItem:
    def __init__(self, main, info): 
        self.main = main
        self.info = info*10
        self.ui = ListItemFrame()

        self.filter = MyFilter(self.ui, self)
        
        self.set_ui()
        self.add_to_ui()
        
        self.main.ui.scroll_layout.addWidget(self.ui)

    def handle_left_click(self):
        pass #other irrelevant function

    def handle_right_click(self, event):
        self.show_options(event)

    def show_options(self, event):
        menu = QMenu()

        item_action = menu.addAction("Hello!")
        quit_action = menu.addAction("Quit")

        action = menu.exec_(self.ui.mapToGlobal(event.pos()))

        if action == quit_action:
            self.main.ctrl.ui.close()
        elif action == item_action:
            global list_element_clicked
            list_element_clicked = True
            self.hello()


    def hello(self):
        print(f"Hello! I am {self.info}")

    def set_ui(self):
        self.ui.name.setText(self.info)

    def add_to_ui(self):
        self.main.ui.scroll_layout.insertWidget(
            self.main.ui.scroll_layout.count() - 1, self.ui
        )
  • Related