Copy / paste functionality implementation for QAbstractTableModel / QTableView
This article demonstrates how to implement copy/paste functionality for QAbstractTableModel and QTableView. The implementation enables to copy/paste a rectangular area within the application itself, as well as between this aplication and an external application such as MS Excel, Google Sheets etc. The implementation simultaneously allows to insert and delete selected rows.
To implement the copy/paste functionality we need to do the following:
- override QAbstractTableModel::flags() and add Qt::ItemIsEditable
- override QAbstractTableModel::insertRows() and QAbstractTableModel::removeRows()
- override QTableView::keyPressEvent() for CTRL+C, CTRL+V, Insert and Delete key
Implementation of the custom dialog
Below you can see the declaration of the CustomDialog with the above-mentioned requirements.
class CustomDialog : public QDialog { Q_OBJECT public: explicit CustomDialog(QWidget *parent = Q_NULLPTR); private: QGroupBox* viewBox; CustomTableView* view; CustomTableModel* countryTableModel; void createModelAndView(); void createLayout(); };
In createModelAndView() we instantiate the model and the view and populate the model with several items. We also insert some empty rows.
CustomDialog::CustomDialog(QWidget *parent) : QDialog(parent) { createModelAndView(); createLayout(); setWindowTitle("Editable table model"); resize(500, 270); } void CustomDialog::createModelAndView(){ countryTableModel = new CustomTableModel; countryTableModel->addData(Country("Sweden", "Stockholm", "Swedish Krona (SEK)")); countryTableModel->addData(Country("Finland", "Helsinki", "Euro (EUR)")); countryTableModel->addData(Country("Norway", "Oslo", "Norwegian Krone (NOK)")); countryTableModel->insertRows(3,15); view = new CustomTableView; view->setModel(countryTableModel); view->verticalHeader()->setDefaultAlignment(Qt::AlignCenter); view->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch); }
CustomTableModel implementation
The code below presents important sections of the CustomTableModel implementation, namely flags(), insertRows() and removeRows().
class CustomTableModel : public QAbstractTableModel { public: CustomTableModel(QObject* parent = Q_NULLPTR); int columnCount(const QModelIndex &parent) const; int rowCount(const QModelIndex &parent) const; Qt::ItemFlags flags(const QModelIndex &index) const; QVariant data(const QModelIndex &index, int role) const; bool setData(const QModelIndex &index, const QVariant &value, int role); QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const; bool insertRows(int row, int count, const QModelIndex &parent = QModelIndex()); bool removeRows(int row, int count, const QModelIndex &parent = QModelIndex()); void addData(const Country& country); private: QList<Country> m_countryList; };
In the following, we perform the usual implementations related to custom data models. we store the countries inserted into the model in a QList<Country> m_countryList. We return 3 as columnCount() and the size of the m_countryList as the rowCount(). We add Qt::ItemIsEditable into the list of table model's flags(). The details of the methods data(), setData() and headerData() follow the usual rules of editable models, the details of their implementation can be found in the source file customtablemodel.cpp.
int CustomTableModel::columnCount(const QModelIndex &parent) const{ Q_UNUSED(parent); return 3; } int CustomTableModel::rowCount(const QModelIndex &parent) const{ Q_UNUSED(parent); return m_countryList.size(); } Qt::ItemFlags CustomTableModel::flags(const QModelIndex &index) const { return QAbstractTableModel::flags(index) | Qt::ItemIsEditable; }
We also override the methods insertRows() and removeRows() that handle the insertion and removal of items from m_countryList. Before inserting data into the model's underlying data store we have to call the method beginInsertRows(), similarly, endInsertRows() has to be called after the insertion. The same holds for the removeRows() method.
bool CustomTableModel::insertRows(int row, int count, const QModelIndex &){ beginInsertRows(QModelIndex(), row, row + count - 1); for (auto i = 0; i < count; ++i) m_countryList.insert(row, Country()); endInsertRows(); return true; } bool CustomTableModel::removeRows(int row, int count, const QModelIndex &){ beginRemoveRows(QModelIndex(), row, row + count - 1); for (auto i = 0; i < count; ++i) m_countryList.removeAt(row); endRemoveRows(); return true; } void CustomTableModel::addData(const Country &country){ this->m_countryList.append(country); }
CustomTableView implementation
Below you can see the header of the class CustomTableView. In order to achieve the copy/paste functionality we need to reimplement the method keyPressEvent().
class CustomTableView : public QTableView { public: CustomTableView(QWidget* parent = Q_NULLPTR); protected: void keyPressEvent(QKeyEvent *event); };
In the method keyPressEvent() we firstly address the insertion and deletion of rows. A row is inserted above the selected row if the Insert key is pressed. The selected row is deleted if the Delete key is pressed. The number of inserted/deleted rows is equal to the number of selected rows. The second part of the code handles any copy/paste events related to a rectangular selection area. The implementation below handles only single continuous areas (multiple discontinuous areas constructed with CTRL are not handled). We retrieve the selected area with selectionModel()->selection().first(). For copy functionality, we iterate over the contents in both directions and join them. Contents of a single row are joined with a tab, individual rows are joined via a new line character. To achieve paste functionality, we revert the above-mentioned process. We split the text retrieved by QApplication::clipboard()->text() by a newline character, after that we split the remaining text by a tab.
void CustomTableView::keyPressEvent(QKeyEvent *event){ QModelIndexList selectedRows = selectionModel()->selectedRows(); // at least one entire row selected if(!selectedRows.isEmpty()){ if(event->key() == Qt::Key_Insert) model()->insertRows(selectedRows.at(0).row(), selectedRows.size()); else if(event->key() == Qt::Key_Delete) model()->removeRows(selectedRows.at(0).row(), selectedRows.size()); } // at least one cell selected if(!selectedIndexes().isEmpty()){ if(event->key() == Qt::Key_Delete){ foreach (QModelIndex index, selectedIndexes()) model()->setData(index, QString()); } else if(event->matches(QKeySequence::Copy)){ QString text; QItemSelectionRange range = selectionModel()->selection().first(); for (auto i = range.top(); i <= range.bottom(); ++i) { QStringList rowContents; for (auto j = range.left(); j <= range.right(); ++j) rowContents << model()->index(i,j).data().toString(); text += rowContents.join("\t"); text += "\n"; } QApplication::clipboard()->setText(text); } else if(event->matches(QKeySequence::Paste)) { QString text = QApplication::clipboard()->text(); QStringList rowContents = text.split("\n", QString::SkipEmptyParts); QModelIndex initIndex = selectedIndexes().at(0); auto initRow = initIndex.row(); auto initCol = initIndex.column(); for (auto i = 0; i < rowContents.size(); ++i) { QStringList columnContents = rowContents.at(i).split("\t"); for (auto j = 0; j < columnContents.size(); ++j) { model()->setData(model()->index( initRow + i, initCol + j), columnContents.at(j)); } } } else QTableView::keyPressEvent(event); } }
That's it!
Tagged: Qt