jgsedi
jgsedi

Reputation: 247

PyQt5 QTableWidget: select column with right click, and show a delete entry in context menu

I want to select a column on a right click and afterwards a context menu has to provide a delete entry for deleting the selected column.

With the transportet QPoint arg the mouse position is sent to the slot. But i need the clicked column.

How to fix it?

Upvotes: 1

Views: 4880

Answers (2)

jgsedi
jgsedi

Reputation: 247

Ok - based on your solution (thanks a lot) - i tried a view. Mostly it does what i want. If i click on the right mouse button on a cell in the table body the cell should be selected and a context menu should be shown - ok - works. If a row or column head is under the mouse cursor the whole row (column) should be selected - works.

BUT the selection remains and that shouldn't be so!

Here's my code for the view:

import logging
import sys

from PyQt5.QtWidgets import \
    QApplication, QWidget, QVBoxLayout, QMenu, \
    QTableWidget, QTableWidgetItem, QTableWidgetSelectionRange
from PyQt5.QtCore import \
    QPoint, Qt


class View(QWidget):
    def __init__(self, app, parent=None):
        super().__init__(parent)

        self.app = app

        # gui elements

        self.layout = None
        self.tbl = None
        self.tbl_selection = None
        self.tbl_item = None
        self.tbl_item_pos = [-1, -1]

    def _setup_layout(self):
        logging.debug("<View._setup_layout>")

        self.setMinimumHeight(600)
        self.setMinimumWidth(800)

        self.layout = QVBoxLayout()
        self.setLayout(self.layout)

    def _setup_table(self):
        logging.debug("<View._setup_table()>")

        if self.app.data:
            data = self.app.data
            rows, cols = len(data), len(data[0])
            self.tbl = tbl = QTableWidget(rows, cols)
            tbl.setHorizontalHeaderLabels(data.pop(0))
            for r, ds in enumerate(data):
                for c, f in enumerate(ds):
                    item = QTableWidgetItem(f)
                    tbl.setItem(r, c, item)

    def _setup_widgets(self):
        logging.debug("<View._setup_widgets()>")
        if self.app is not None:
            self._setup_table()
        else:
            self.tbl = QTableWidget()

        tbl = self.tbl
        tbl.verticalHeader().setContextMenuPolicy(
            Qt.CustomContextMenu)
        tbl.horizontalHeader().setContextMenuPolicy(
            Qt.CustomContextMenu)

        self.layout.addWidget(self.tbl)

    def _connect_evt_with_handler(self):
        logging.debug("<View._connect_evt_with_handler()>")

        tbl = self.tbl
        tbl.setContextMenuPolicy(Qt.CustomContextMenu)
        tbl.customContextMenuRequested.connect(self.on_tbl_rightclick)

        # event for click on row heads
        tbl.verticalHeader().customContextMenuRequested.connect(
            self.on_tbl_rhead_rightclick)
        # event for click on col heads
        tbl.horizontalHeader().customContextMenuRequested.connect(
            self.on_tbl_chead_rightclick)

    # protected

    def _get_clicked_item(self, qpoint: QPoint) -> bool:
        logging.debug("<View._get_clicked_item(qpoint={})>".format(qpoint))

        self.tbl_item = item = self.tbl.itemAt(qpoint)
        self.tbl_item_pos = [item.row(), item.column()]

        return self.tbl_item is not None

    def _set_selection(self):
        logging.debug("<View._set_selection()>")

        r, c = self.tbl_item_pos
        logging.debug("... row={}, col={}".format(r, c))

        if r >= 0 and c >= 0:
            self.tbl_selection = QTableWidgetSelectionRange(
                r, c, r, c)
        elif c < 0:
            # row head
            self.tbl_selection = QTableWidgetSelectionRange(
                r, 0, r, self.tbl.columnCount() - 1)
        elif r < 0:
            # col head
            self.tbl_selection = QTableWidgetSelectionRange(
                0, c, self.tbl.rowCount() - 1, c)
        self.tbl.setRangeSelected(self.tbl_selection, True)

    def _build_ctx_mnu(self, qpoint: QPoint):
        logging.debug("<View._build_ctx_mnu(qpoint={})>".format(qpoint))

        r, c = self.tbl_item_pos
        logging.debug("... row={}, col={}".format(r, c))

        # build menu
        action_del_col = None
        action_del_row = None

        menu = QMenu()
        if c < 0:
            # row head
            action_del_row = menu.addAction("Delete row")
        elif r < 0:
            # col head
            action_del_col = menu.addAction("Delete column")
        else:
            # table body
            action_del_col = menu.addAction("Delete column")
            action_del_row = menu.addAction("Delete row")

        action = menu.exec_(self.tbl.viewport().mapToGlobal(qpoint))
        if action is not None and action == action_del_col:
            logging.debug("... action: {}".format(action_del_col.text()))
            self.tbl.removeColumn(c)
        elif action is not None and action == action_del_row:
            logging.debug("... action: {}".format(action_del_row.text()))
            self.tbl.removeRow(r)
    # UI

    def setup(self):
        logging.debug("<View.setup()>")
        self._setup_layout()
        self._setup_widgets()
        self._connect_evt_with_handler()

    # events

    def on_tbl_rightclick(self, qpoint: QPoint):
        logging.debug("<View.on_tbl_rightclick(qpoint={})>".format(qpoint))

        if self._get_clicked_item(qpoint):
            self._set_selection()
            self._build_ctx_mnu(qpoint)

    def on_tbl_rhead_rightclick(self, qpoint: QPoint):
        logging.debug(
            "<View.on_tbl_rhead_rightclick(qpoint={})>".format(qpoint))

        if self._get_clicked_item(qpoint):
            self.tbl_item_pos[1] = -1
            self._set_selection()
            self._build_ctx_mnu(qpoint)

    def on_tbl_chead_rightclick(self, qpoint: QPoint):
        logging.debug(
            "<View.on_tbl_chead_rightclick(qpoint={})>".format(qpoint))

        if self._get_clicked_item(qpoint):
            self.tbl_item_pos[0] = -1
            self._set_selection()
            self._build_ctx_mnu(qpoint)

And here some date - once more:

data = [f.strip().split(",") for f in """\
    id,first_name,last_name,email,gender,ip_address
    1,Hillard,Tasseler,[email protected],Male,104.153.237.243
    2,Tyrus,Oley,[email protected],Male,163.73.24.45
    3,Kora,Covil,[email protected],Female,158.44.166.87
    4,Phineas,McEntee,[email protected],Male,71.82.246.45
    5,Dottie,Spraging,[email protected],Female,226.138.241.22
    6,Andria,Ivatts,[email protected],Female,57.5.76.78
    7,Missy,Featherstone,[email protected],Female,9.56.215.203
    8,Anstice,Sargant,[email protected],Female,36.115.185.109
    9,Teresita,Trounce,[email protected],Female,240.228.133.166
    10,Sib,Thomke,[email protected],Female,129.191.2.7
    11,Amery,Dallander,[email protected],Male,4.115.194.100
    12,Rourke,Rowswell,[email protected],Male,48.111.190.66
    13,Cloe,Benns,[email protected],Female,142.48.24.44
    14,Enos,Fery,[email protected],Male,59.19.200.235
    15,Russell,Capelen,[email protected],Male,38.205.20.141""".split()]

And for testing: the app object and the main function:

class App:
    def __init__(self, data=()):

        self.view = View(app=self)
        self.data = []

        if isinstance(data, (list, tuple)) and len(data) > 0:
            self.set(data)

    def set(self, data):
        logging.debug("<App.set(data)>")
        self.data = data

    def setup(self):
        logging.debug("<App.setup()>")
        self.view.setup()

    def show(self):
        logging.debug("<App.show()>")
        self.view.show()


def main():
    logging.basicConfig(level=logging.DEBUG)

    qapp = QApplication(sys.argv)

    app = App(data=data)
    app.setup()
    app.show()

    sys.exit(qapp.exec_())


main()

Upvotes: 1

eyllanesc
eyllanesc

Reputation: 243897

To obtain the column you must use the pressed item that is obtained using the itemAt() method.

It is selected using the setRangeSelected method by passing it QTableWidgetSelectionRange, which has the column as data.

Then you create the QMenu and the exec_() method you pass the position of the signal but you must convert it to global using mapToGlobal from the viewport.

Then you delete the column using the removeColumn() method.

from PyQt5 import QtCore, QtGui, QtWidgets

class Widget(QtWidgets.QWidget):
    def __init__(self, parent=None):
        super(Widget, self).__init__(parent)

        self.table_widget = QtWidgets.QTableWidget(20, 10)

        for i in range(self.table_widget.rowCount()):
            for j in range(self.table_widget.columnCount()):
                it = QtWidgets.QTableWidgetItem("{}-{}".format(i, j))
                self.table_widget.setItem(i, j, it)

        vlay = QtWidgets.QVBoxLayout(self)
        vlay.addWidget(self.table_widget)

        self.table_widget.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
        self.table_widget.customContextMenuRequested.connect(self.on_customContextMenuRequested)

    @QtCore.pyqtSlot(QtCore.QPoint)
    def on_customContextMenuRequested(self, pos):
        it = self.table_widget.itemAt(pos)
        if it is None: return
        c = it.column()
        item_range = QtWidgets.QTableWidgetSelectionRange(0, c, self.table_widget.rowCount()-1 , c)
        self.table_widget.setRangeSelected(item_range, True)

        menu = QtWidgets.QMenu()
        delete_column_action = menu.addAction("Delete column")
        action = menu.exec_(self.table_widget.viewport().mapToGlobal(pos))
        if action == delete_column_action:
            self.table_widget.removeColumn(c)

if __name__ == '__main__':
    import sys
    app = QtWidgets.QApplication(sys.argv)
    w = Widget()
    w.resize(640, 480)
    w.show()
    sys.exit(app.exec_())

Upvotes: 2

Related Questions