(mis)adventures in software development...

21 June 2013

Subclassing QTableWidget into submission

Category Python

Python example of subclassing a Qt QTableWidget control and overriding copy behavior so that the entire selected row is copied to the clipboard, and stored in a Excel-friendly format for proper pasting.

Hello there QTableWidget, you look like just the sort of Qt control I’m looking for. I want to stick some data into you. But once the data is inside you, I might want to do other things. Like perhaps copy and paste rows of data into Excel or some other spreadsheet. I wouldn’t have thought that’s an overly unusual request. But looks like you won’t do that without some overt persuasion. Seems like you’re not into data swapping with other applications. Well then, you’ve left me no choice but to subclass your ass:

#!/usr/bin/env python

import sys
from PySide.QtCore import *
from PySide.QtGui import *

class TableWidgetCustom(QTableWidget):
    def __init__(self, parent=None):
        super(TableWidgetCustom, self).__init__(parent)

    def keyPressEvent(self, event):
        if event.matches(QKeySequence.Copy):
            QTableWidget.keyPressEvent(self, event)

    def copy(self):
        selection = self.selectionModel()
        indexes = selection.selectedRows()
        if len(indexes) < 1:
            # No row selected
        text = ''
        for idx in indexes:
            row = idx.row()
            for col in range(0, self.columnCount()):
                item = self.item(row, col)
                if item:
                    text += item.text()
                text += '\t'
            text += '\n'

See QTableWidget, that wasn’t so bad after all now, was it?

I expected the Qt table widgets would implement proper copy and paste of rows by default, especially if options like QTableWidget.SelectRows are set. But seems I was being overly optimistic. No matter what options I tried, it would only copy the text in the single cell that was clicked on, regardless of how many cells were actually highlighted. So had to resort to subclassing. The above is a fairly minimal implementation that seems to work well enough for my purposes.

For data being pasted from the clipboard, apparently Excel expects columns to be tab delimited, with rows ending with a newline. The copy() method above attempts to satisfy Excel’s desires in this regard, and places the copied text in the required format onto the clipboard.

To see this in action, the customised QTableWidget can be used as normal:

class Form(QDialog):

    def __init__(self, parent=None):
        super(Form, self).__init__(parent)
        tableLabel = QLabel("Behold, some data, in a table:")
        self.customTable = TableWidgetCustom()
        layout = QVBoxLayout()
        self.setWindowTitle("QTableWidget Copy and Paste Example")
        for row in range(3):
            for col in range(5):
                item = QTableWidgetItem("Row %d Column %d" % (row+1,
                self.customTable.setItem(row, col, item)

app = QApplication(sys.argv)
form = Form()

The above code creates a dialog box with an instance of our customised QTableWidget and populates it with some dummy data. Selecting a row and pressing Control-C will copy that row to the clipboard, and it can then be pasted into Excel (also works in LibreOffice). But if you were to replace the TableWidgetCustom control with a standard QTableWidget, you’d see that Control-C will only copy the data in one cell, not the entire row.