Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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()
like image 636
c00kiemonster Avatar asked Oct 16 '11 02:10

c00kiemonster


2 Answers

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
like image 108
UpAndAdam Avatar answered Oct 12 '22 23:10

UpAndAdam


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)
like image 33
c00kiemonster Avatar answered Oct 13 '22 01:10

c00kiemonster