How can I get right-click context menus for clicks in QTableView header?

17,989

Solution 1

Turned out to be simpler than I thought. In the same manner as I add the popup menu for the QTableView widget itself, I can just get the header from table object and then attach a context menu in the same way as I did with the regular context menu.

headers = self.tv.horizontalHeader()
headers.setContextMenuPolicy(Qt.CustomContextMenu)
headers.customContextMenuRequested.connect(self.header_popup)

Solution 2

There's another potentially more powerful way to do this if you take the step and inherit the view instead of simply composing it. Does custom context menu work here? Yes, but why does anything other than the view need to know about it? It also will help better shape your code to deal with other issues properly. Currently the implementation doesn't provide any encapsulation, cohesion or support separation of responsibility. In the end you will have one big blob which is the antithesis of good design. I mention this because you seem to be placing all of the GUI Logic in this ever growing main function, and its the reason you ended up putting the sort implementation inside your model, which makes no sense to me. (What if you have two views of the model, you are forcing them to be sorted in the same way)

Is it more code? Yes, but it gives you more power which I think is worth mentioning. Below I'm demonstrating how to handle the headers and also any given cell you want. Also note that in my implementation if some OTHER widget exists which also defines a context menu event handler it will potentially get a chance to have crack at handling the event after mine; so that if someone else adds a handler for only certain cases they can do so without complicating my code. Part of doing this is marking if you handled the event or not.

Enough of my rambling and thoughts here's the code:

    #Alteration : instead of self.tv = QTableView...
        self.tv = MyTableView()
        ....

# somewhere in your TableView object's __init__ method
# yeah IMHO you should be inheriting and thus extending TableView 
class MyTableView(QTableView):
    def __init__(self, parent = None):
        super(MyTableView, self).__init__()
        self.setContextMenuPolicy(Qt.DefaultContextMenu)

        ## uniform one for the horizontal headers.
        self.horizontalHeader().setContextMenuPolicy(Qt.ActionsContextMenu)

        ''' Build a header action list once instead of every time they click it'''
        doSomething = QAction("&DoSomething", self.verticalHeader(),
                              statusTip = "Do something uniformly for headerss",
                              triggered = SOME_FUNCTION
        self.verticalHeader().addAction(doSomething)
        ...
        return

    def contextMenuEvent(self, event)
    ''' The super function that can handle each cell as you want it'''
        handled = False
        index = self.indexAt(event.pos())
        menu = QMenu()
        #an action for everyone
        every = QAction("I'm for everyone", menu, triggered = FOO)
        if index.column() == N:  #treat the Nth column special row...
            action_1 = QAction("Something Awesome", menu,
                               triggered = SOME_FUNCTION_TO_CALL )
            action_2 = QAction("Something Else Awesome", menu,
                               triggered = SOME_OTHER_FUNCTION )
            menu.addActions([action_1, action_2])
            handled = True
            pass
        elif index.column() == SOME_OTHER_SPECIAL_COLUMN:
            action_1 = QAction("Uh Oh", menu, triggered = YET_ANOTHER_FUNCTION)
            menu.addActions([action_1])
            handled = True
            pass

        if handled:
            menu.addAction(every)
            menu.exec_(event.globalPos())
            event.accept() #TELL QT IVE HANDLED THIS THING
            pass
        else:
            event.ignore() #GIVE SOMEONE ELSE A CHANCE TO HANDLE IT
            pass
        return


    pass #end of class
Share:
17,989
c00kiemonster
Author by

c00kiemonster

Updated on July 04, 2022

Comments

  • c00kiemonster
    c00kiemonster almost 2 years

    The sample code below (heavily influenced from here) has a right-click context menu that will appear as the user clicks the cells in the table. Is it possible to have a different right-click context menu for right-clicks in the header of the table? If so, how can I change the code to incorporate this?

    import re
    import operator
    import os
    import sys
    from PyQt4.QtCore import *
    from PyQt4.QtGui import *
    
    def main():
        app = QApplication(sys.argv)
        w = MyWindow()
        w.show()
        sys.exit(app.exec_())
    
    class MyWindow(QWidget):
        def __init__(self, *args):
            QWidget.__init__(self, *args)
    
            self.tabledata = [('apple', 'red', 'small'),
                              ('apple', 'red', 'medium'),
                              ('apple', 'green', 'small'),
                              ('banana', 'yellow', 'large')]
            self.header = ['fruit', 'color', 'size']
    
            # create table
            self.createTable()
    
            # layout
            layout = QVBoxLayout()
            layout.addWidget(self.tv)
            self.setLayout(layout)
    
        def popup(self, pos):
            for i in self.tv.selectionModel().selection().indexes():
                print i.row(), i.column()
            menu = QMenu()
            quitAction = menu.addAction("Quit")
            action = menu.exec_(self.mapToGlobal(pos))
            if action == quitAction:
                qApp.quit()
    
        def createTable(self):
            # create the view
            self.tv = QTableView()
            self.tv.setStyleSheet("gridline-color: rgb(191, 191, 191)")
    
            self.tv.setContextMenuPolicy(Qt.CustomContextMenu)
            self.tv.customContextMenuRequested.connect(self.popup)
    
            # set the table model
            tm = MyTableModel(self.tabledata, self.header, self)
            self.tv.setModel(tm)
    
            # set the minimum size
            self.tv.setMinimumSize(400, 300)
    
            # hide grid
            self.tv.setShowGrid(True)
    
            # set the font
            font = QFont("Calibri (Body)", 12)
            self.tv.setFont(font)
    
            # hide vertical header
            vh = self.tv.verticalHeader()
            vh.setVisible(False)
    
            # set horizontal header properties
            hh = self.tv.horizontalHeader()
            hh.setStretchLastSection(True)
    
            # set column width to fit contents
            self.tv.resizeColumnsToContents()
    
            # set row height
            nrows = len(self.tabledata)
            for row in xrange(nrows):
                self.tv.setRowHeight(row, 18)
    
            # enable sorting
            self.tv.setSortingEnabled(True)
    
            return self.tv
    
    class MyTableModel(QAbstractTableModel):
        def __init__(self, datain, headerdata, parent=None, *args):
            """ datain: a list of lists
                headerdata: a list of strings
            """
            QAbstractTableModel.__init__(self, parent, *args)
            self.arraydata = datain
            self.headerdata = headerdata
    
        def rowCount(self, parent):
            return len(self.arraydata)
    
        def columnCount(self, parent):
            return len(self.arraydata[0])
    
        def data(self, index, role):
            if not index.isValid():
                return QVariant()
            elif role != Qt.DisplayRole:
                return QVariant()
            return QVariant(self.arraydata[index.row()][index.column()])
    
        def headerData(self, col, orientation, role):
            if orientation == Qt.Horizontal and role == Qt.DisplayRole:
                return QVariant(self.headerdata[col])
            return QVariant()
    
        def sort(self, Ncol, order):
            """Sort table by given column number.
            """
            self.emit(SIGNAL("layoutAboutToBeChanged()"))
            self.arraydata = sorted(self.arraydata, key=operator.itemgetter(Ncol))
            if order == Qt.DescendingOrder:
                self.arraydata.reverse()
            self.emit(SIGNAL("layoutChanged()"))
    
    if __name__ == "__main__":
        main()
    
  • UpAndAdam
    UpAndAdam over 10 years
    I'm well aware that they are not required and are a no-op. For me (and the folks I work with) we find it greatly increases readability particularly since we hop back and forth from C/C++, and that is something PEP-8 advocates for. Personally I also like them so that style-formatting tools can correct indentation problems in a context free manner, code-generation tools can insert stubs for me with a finite end, and it makes merges easier when people rewrite something changing the levels of nesting.
  • NuclearPeon
    NuclearPeon over 10 years
    Okay, thanks for explanation. I figured it was out of habit rather than ignorance, and I was curious to know why. Much appreciated.
  • UpAndAdam
    UpAndAdam over 10 years
    always happy to explain, though if you are curious you could always ask your question directly it is a Q&A site :-P
  • NuclearPeon
    NuclearPeon over 10 years
    Although I could have messaged you directly, I figured someone else might be interested in knowing the answer as well lol. Thanks
  • UpAndAdam
    UpAndAdam over 10 years
    I think you misunderstand and I see why from how I wrote it. By directly I'm only referring to the way you 'asked' in your comment. You didn't ask a question; you made a statement indirectly questioning what I was doing. Compare this to the direct question "I'm presuming you know that pass isn't required; are you using it out of habit or for another reason?" Much more likely to be: well received, for me to mark your comment as useful and not to make the recipient feel defensive. ( I agree good for others, how do you message directly? )