Search code examples
pythonpositioncontextmenupyqt5qtableview

How to get the row number from a rigth-click on a QTableView?


I'm developing an app with PyQt5, and can't find out how to translate the position of a right click on a QTableView into a row number. The way I implemented it, which is by using the rowAt method on the QPoint cursor position, I get row numbers that look like having an offset with the real row number that was clicked. I've been looking around but couldn't get any clear solution. Sounds like there's something to do with the scrolling area. An example of my code is below.

The UI code generated with Qt Designer:

from PyQt5 import QtCore, QtGui, QtWidgets

class Ui_MainWindow(object):
    def setupUi(self, MainWindow):
        MainWindow.setObjectName("MainWindow")
        MainWindow.setWindowModality(QtCore.Qt.NonModal)
        MainWindow.resize(941, 767)
        MainWindow.setMinimumSize(QtCore.QSize(922, 767))
        MainWindow.setMaximumSize(QtCore.QSize(1900, 1200))
        MainWindow.setAutoFillBackground(False)
        MainWindow.setAnimated(False)
        self.MyWindows = QtWidgets.QWidget(MainWindow)
        self.MyWindows.setEnabled(True)
        self.MyWindows.setMinimumSize(QtCore.QSize(921, 726))
        self.MyWindows.setMaximumSize(QtCore.QSize(1900, 1200))
        self.MyWindows.setObjectName("MyWindows")
        self.layoutWidget = QtWidgets.QWidget(self.MyWindows)
        self.layoutWidget.setGeometry(QtCore.QRect(10, 0, 911, 441))
        self.layoutWidget.setObjectName("layoutWidget")
        self.gridLayout = QtWidgets.QGridLayout(self.layoutWidget)
        self.gridLayout.setContentsMargins(0, 0, 0, 0)
        self.gridLayout.setObjectName("gridLayout")
        self.tableView = QtWidgets.QTableView(self.layoutWidget)
        self.tableView.setEnabled(True)
        sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed)
        sizePolicy.setHorizontalStretch(1)
        sizePolicy.setVerticalStretch(1)
        sizePolicy.setHeightForWidth(self.tableView.sizePolicy().hasHeightForWidth())
        self.tableView.setSizePolicy(sizePolicy)
        self.tableView.setMinimumSize(QtCore.QSize(900, 400))
        font = QtGui.QFont()
        font.setPointSize(10)
        self.tableView.setFont(font)
        self.tableView.setSizeAdjustPolicy(QtWidgets.QAbstractScrollArea.AdjustToContents)
        self.tableView.setAlternatingRowColors(True)
        self.tableView.setSelectionMode(QtWidgets.QAbstractItemView.MultiSelection)
        self.tableView.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
        self.tableView.setGridStyle(QtCore.Qt.NoPen)
        self.tableView.setSortingEnabled(False)
        self.tableView.setObjectName("tableView")
        self.tableView.horizontalHeader().setCascadingSectionResizes(True)
        self.tableView.horizontalHeader().setSortIndicatorShown(False)
        self.tableView.horizontalHeader().setStretchLastSection(True)
        self.tableView.verticalHeader().setVisible(True)
        self.tableView.verticalHeader().setCascadingSectionResizes(True)
        self.tableView.verticalHeader().setHighlightSections(True)
        self.tableView.verticalHeader().setSortIndicatorShown(False)
        self.tableView.verticalHeader().setStretchLastSection(True)
        self.gridLayout.addWidget(self.tableView, 1, 1, 1, 1)
        MainWindow.setCentralWidget(self.MyWindows)

        self.retranslateUi(MainWindow)
        QtCore.QMetaObject.connectSlotsByName(MainWindow)

    def retranslateUi(self, MainWindow):
        _translate = QtCore.QCoreApplication.translate
        MainWindow.setWindowTitle(_translate("MainWindow", "Thib Imave Smoother V0.2"))

The main code:

from PyQt5.QtCore import *
from PyQt5.QtWidgets import QMainWindow, QSizePolicy, QHeaderView, QFileSystemModel, QMenu, QAction
# QtDesigner generated code imports
from UI import *

class ShowTableView:

    def __init__(self, guiObject):
        self._gui = guiObject  # store QWidget from Gui class
        #initialize fake data for testing
        self.my_data = [["test" for x in range(7)] for x in range(5000)]
        self.tm = MyTableModel(self.my_data)
        self._gui.tableView.setModel(self.tm)
        header = self._gui.tableView.horizontalHeader()
        header.setSectionResizeMode(QHeaderView.ResizeToContents)  # have columns width adjusted to content

class MyTableModel(QAbstractTableModel):
    def __init__(self, my_data, parent=None, *args):
        QAbstractTableModel.__init__(self, parent)
        self.my_data = my_data
        self.title_list = ["on","two","three","four","five","six","seven"]

    def rowCount(self, parent):
        return len(self.my_data)

    def columnCount(self, parent):
        return len(self.my_data[0])

    def data(self, index, role):
        if not index.isValid():
            return QVariant()
        elif role != Qt.DisplayRole:
            return QVariant()
        else: #
            return QVariant(self.my_data[index.row()][index.column()])

    def headerData(self, col, orientation, role):
        if orientation == Qt.Horizontal and role == Qt.DisplayRole:
            return QVariant(self.title_list[col])
        elif orientation == Qt.Vertical and role == Qt.DisplayRole:
            return QVariant(col + 1)

class Gui(QMainWindow, Ui_MainWindow):
    def __init__(self, parent=None):
        super(Gui, self).__init__(parent)
        self.setupUi(self)
        self.myTable = ShowTableView(self)

    def contextMenuEvent(self, QContextMenuEvent):
        self.menu = QMenu(self)
        removeAction = QAction('Remove', self)
        self.menu.addAction(removeAction)
        removeAction.triggered.connect(lambda: self.remove_row_from_rigth_click(QtGui.QCursor.pos()))
        self.menu.popup(QtGui.QCursor.pos())

    def remove_row_from_rigth_click(self, event,q_point):
        row = self.tableView.rowAt(q_point.y())
        print("row =" + str(row))  # HERE I GET WRONG VALUE WITH AN "RANDOM" OFFSET - IF I CLICK ROW 10 I GET 16 !

if __name__ == "__main__":

    app = QtWidgets.QApplication(sys.argv)
    ui = Gui()
    ui.show()
    sys.exit(app.exec_())

Solution

  • The rowAt method uses local coordinates, whereas you are using global coordinates. So you must use mapFromGlobal to get the correct y value:

    class Gui(QMainWindow, Ui_MainWindow):
        ...
        def contextMenuEvent(self, event):
            ...
            pos = event.globalPos()
            removeAction.triggered.connect(
                lambda: self.remove_row_from_rigth_click(pos))
            self.menu.popup(pos)
    
        def remove_row_from_rigth_click(self, q_point):
            row = self.tableView.rowAt(
                self.tableView.viewport().mapFromGlobal(q_point).y())
            print("row =" + str(row))
    

    Note that the returned row is zero-based, unlike the vertical header labels.