In one of my projects I have to manage a list of items, that can be rearranged in their orders by using drag and drop.
Now, all items came with a priority, that cannot be changed by the user. There is an restriction on the order of the elements in the list, namely that elements with a lower priority must come first, but elements with the same priority can be in interchanged.
For example, the following list is sane:
(A,1),(B,1),(C,1),(D,2),(E,3)
whereas the following is broken:
(A,1),(B,1),(E,3),(D,2)
The following code shows a starting point of my problem:
#include <QApplication>
#include <QFrame>
#include <QHBoxLayout>
#include <QListView>
#include <QStandardItemModel>
QStandardItem* create(const QString& text, int priority) {
auto ret = new QStandardItem(text);
ret->setData(priority);
return ret;
}
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
auto frame = new QFrame;
frame->setLayout(new QVBoxLayout);
auto view = new QListView;
frame->layout()->addWidget(view);
auto model = new QStandardItemModel;
view->setModel(model);
model->appendRow(create("1. A", 1));
model->appendRow(create("1. B", 1));
model->appendRow(create("2. X", 2));
model->appendRow(create("2. Y", 2));
model->appendRow(create("2. Z", 2));
view->setDragEnabled(true);
view->viewport()->setAcceptDrops(true);
view->setDropIndicatorShown(true);
view->setDragDropMode(QAbstractItemView::DragDropMode::InternalMove);
view->setDefaultDropAction(Qt::DropAction::MoveAction);
view->setDragDropOverwriteMode(false);
frame->show();
return a.exec();
}
Now, the DefaultDropAction must change context depended on the item going to be moved and also the item where it is going to be dropped.
If the priorities of the two elements are equal, then I have a MoveAction. In case the priorities of the two elements differ, I have a IgnoreAction.
Can this behavior be achieved without implementing my on QListView and what
can be achieved by adapting a custom QAbstractItemModel.
A possible workaround might be even to abandon the drag and drop interface and using the arrow up and down keys to move items around. Or even more general an action with cut and paste operation. But, I really prefer to stick with drag and drop interface.
You could reimplement QStandardItemModel and override the canDropMimeData() method. There are other ways, though they would probably be more involved if you're happy with QStandardItemModel already. Implementing your own model could have performance advantages, especially if your data structure is fairly simple (like a single-column list). This would also let you customize the drag/drop behavior much more in general.
Note that this ignores the action type entirely (QStandardItemModel only allows move and copy by default). Moving an item onto another item will remove the destination item entirely -- which may not be what you want but is a separate issue (see comments in code below).
You could also implement the same logic in dropMimeData() method (before calling the base class method), but I'm not sure I see any advantage. And by using canDropMimeData() the user also gets visual feedback about what is and isn't going to work.
#include <QStandardItemModel>
class ItemModel : public QStandardItemModel
{
public:
using QStandardItemModel::QStandardItemModel;
bool canDropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent) const override
{
if (!QStandardItemModel::canDropMimeData(data, action, row, column, parent))
return false;
const int role = Qt::UserRole + 1; // what QStandardItem uses for setData() by default
int originPriority;
int destPriority;
// Find destination item priority.
if (parent.isValid()) {
// dropping onto an item
// Note: if you don't want MoveAction to overwrite items you could:
// if (action == Qt::MoveAction) return false;
destPriority = parent.data(role).toInt();
}
else if (row > -1) {
// dropping between items
destPriority = this->data(index(row, 0), role).toInt();
}
else {
// dropping somewhere else onto the view, treat it as drop after last item in model
destPriority = this->data(index(rowCount() - 1, 0), role).toInt();
}
// Need to find priority of item(s) being dragged (encoded in mime data). Could be several.
// This part decodes the mime data in a way compatible with how QAbstractItemModel encoded it.
// (QStandardItemModel includes it in the mime data alongside its own version)
QByteArray ba = data->data(QAbstractItemModel::mimeTypes().first());
QDataStream ds(&ba, QIODevice::ReadOnly);
while (!ds.atEnd()) {
int r, c;
QMap<int, QVariant> v;
ds >> r >> c >> v;
// If there were multiple columns of data we could also do a
// check on the column number, for example.
originPriority = v.value(role).toInt();
if (originPriority != destPriority)
break; //return false; Could exit here but keep going to print our debug info.
}
qDebug() << "Drop parent:" << parent << "row:" << row <<
"destPriority:" << destPriority << "originPriority:" << originPriority;
if (originPriority != destPriority)
return false;
return true;
}
};
For reference, here's how QAbstractItemModel encodes data (and decodes it in the next method down).
ADDED:
OK, it was bugging me a bit, so here is a more efficient version... :-) It saves a lot of decoding time by embedding the dragged item's priority right into the mime data when the drag starts.
#include <QStandardItemModel>
#define PRIORITY_MIME_TYPE QStringLiteral("application/x-priority-data")
class ItemModel : public QStandardItemModel
{
public:
using QStandardItemModel::QStandardItemModel;
QMimeData *mimeData(const QModelIndexList &indexes) const override
{
QMimeData *mdata = QStandardItemModel::mimeData(indexes);
if (!mdata)
return nullptr;
// Add our own priority data for more efficient evaluation in canDropMimeData()
const int role = Qt::UserRole + 1; // data role for priority value
int priority = -1;
bool ok;
for (const QModelIndex &idx : indexes) {
// Priority of selected item
const int thisPriority = idx.data(role).toInt(&ok);
// When dragging multiple items, check that the priorities of all selected items are the same.
if (!ok || (priority > -1 && thisPriority != priority))
return nullptr; // Cannot drag items with different priorities;
priority = thisPriority;
}
if (priority < 0)
return nullptr; // couldn't find a priority, cancel the drag.
// Encode the priority data
QByteArray ba;
ba.setNum(priority);
mdata->setData(PRIORITY_MIME_TYPE, ba);
return mdata;
}
bool canDropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent) const override
{
if (!QStandardItemModel::canDropMimeData(data, action, row, column, parent))
return false;
if (!data->hasFormat(PRIORITY_MIME_TYPE))
return false;
const int role = Qt::UserRole + 1; // what QStandardItem uses for setData() by default
int destPriority = -1;
bool ok = false;
// Find destination item priority.
if (parent.isValid()) {
// dropping onto an item
destPriority = parent.data(role).toInt(&ok);
}
else if (row > -1) {
// dropping between items
destPriority = this->data(index(row, 0), role).toInt(&ok);
}
else {
// dropping somewhere else onto the view, treat it as drop after last item in model
destPriority = this->data(index(rowCount() - 1, 0), role).toInt(&ok);
}
if (!ok || destPriority < 0)
return false;
// Get priority of item(s) being dragged which we encoded in mimeData() method.
const int originPriority = data->data(PRIORITY_MIME_TYPE).toInt(&ok);
qDebug() << "Drop parent:" << parent << "row:" << row
<< "destPriority:" << destPriority << "originPriority:" << originPriority;
if (!ok || originPriority != destPriority)
return false;
return true;
}
};
Related
I'm trying to implement drag'n'drop reordering of rows in a QTableView.
The operation needs to actually reorder the underlying data, therefore reordering the vertical header rows doesn't seem like a good solution.
I instead opted for the underlying model to handle this by providing the row indices in its mimeData function and accepting this list in dropMimeData. In the dropMimeData function, I use the beginMoveRows and endMoveRows to move individual rows visually, in addition to moving the underlying data. This approach seems to work, with only one shortcoming: as soon as I call endMoveRows, the QTableView's horizontal header resets all its section sizes to defaults.
Am I missing something I need to do, or is this a bug in Qt (or even intended behavior)? Is there any other way to handle drag'n'drop reordering?
Here is a simplified piece of code that shows the same behavior:
class Model: public QAbstractTableModel
{
public:
Model():
m_Items({"A", "B", "C", "D", "E"})
{
}
virtual QVariant data(const QModelIndex & a_Parent, int a_Role) const override
{
return QString("%1 - %2").arg(m_Items[a_Parent.row()]).arg(a_Parent.column());
}
virtual int rowCount(const QModelIndex & a_Parent) const override
{
return m_Items.count();
}
virtual int columnCount(const QModelIndex & a_Parent) const override
{
return 4;
}
void moveNow()
{
// Moves the last row as the 3rd row:
auto count = m_Items.count();
beginMoveRows(QModelIndex(), count, count, QModelIndex(), 2);
m_Items.insert(2, m_Items.last());
m_Items.pop_back();
endMoveRows();
}
protected:
QStringList m_Items;
};
static Model g_Model;
MainWindow::MainWindow(QWidget * parent):
QMainWindow(parent),
ui(new Ui::MainWindow)
{
// "ui" created by QtCreator, has a "tableView" and a "pushButton"
ui->setupUi(this);
ui->tableView->setModel(&g_Model);
ui->tableView->horizontalHeader()->resizeSection(0, 200);
ui->tableView->horizontalHeader()->resizeSection(1, 100);
ui->tableView->horizontalHeader()->resizeSection(2, 50);
ui->tableView->horizontalHeader()->resizeSection(3, 80);
connect(ui->pushButton, &QPushButton::clicked,
[this]()
{
// Outputs 200 on first click
qDebug() << ui->tableView->horizontalHeader()->sectionSize(0);
g_Model.moveNow();
// Outputs 100
qDebug() << ui->tableView->horizontalHeader()->sectionSize(0);
}
);
}
I found out that calling emit layoutAboutToBeChanged() just before the move operation fixes the issue:
void moveNow()
{
emit layoutAboutToBeChanged();
auto count = m_Items.count();
beginMoveRows(QModelIndex(), count, count, QModelIndex(), 2);
m_Items.insert(2, m_Items.last());
m_Items.pop_back();
endMoveRows();
}
This seems to contradict the documentation for beginMoveRows, which indicates that it is a replacement for calling this function. Also there must not be a matching emit layoutChanged() after endMoveRows, otherwise the columns are, again, reset.
This all together leads me to believe it's an actual bug in Qt.
I have a simple QTreeWidget pointing to the root directory:
#include <QTreeWidget>
#include <QStringList>
#include <QApplication>
int main(int argc, char **argv)
{
QApplication application(argc, argv);
QStringList fileNames{"TEST/branch", "trunk"};
QTreeWidget treeWidget;
treeWidget.setColumnCount(1);
for (const auto& filename : fileNames)
{
QTreeWidgetItem *parentTreeItem = new QTreeWidgetItem(&treeWidget);
parentTreeItem->setText(0, filename.split('/').first());
QStringList filenameParts = filename.split('/').mid(1);
for(const auto& filenamePart : filenameParts)
{
QTreeWidgetItem *treeItem = new QTreeWidgetItem();
treeItem->setText(0, filenamePart);
parentTreeItem->addChild(treeItem);
parentTreeItem = treeItem;
}
}
treeWidget.show();
return application.exec();
}
Output:
The item I have selected above is /TEST/branches. How can I get the absolute path of the currently selected item?
Well, I don't think there is a built in function does that but you can write a function yourself like
QString treeItemToFullPath(QTreeWidgetItem* treeItem)
{
QString fullPath= treeItem->text(0);
while (treeItem->parent() != NULL)
{
fullPath= treeItem->parent()->text(0) + "/" + fullPath;
treeItem = treeItem->parent();
}
return fullPath;
}
edit:
Input treeItem is the selected tree item that you want to show its path. if you are sure at least one item is selected, you can get it by
treeWidget.selectedItems().first();
Another mehtod is using tooltips. You can add tip for each item, while you are adding them to tree, but you can do this after you add them in their final place.
change this
for(const auto& filenamePart : filenameParts)
{
QTreeWidgetItem *treeItem = new QTreeWidgetItem();
treeItem->setText(0, filenamePart);
parentTreeItem->addChild(treeItem);
parentTreeItem = treeItem;
}
as this
for(const auto& filenamePart : filenameParts)
{
QTreeWidgetItem *treeItem = new QTreeWidgetItem();
treeItem->setText(0, filenamePart);
parentTreeItem->addChild(treeItem);
parentTreeItem = treeItem;
treeItem->setToolTip(0, treeItemToFullPath(treeItem));
}
this way you will see the full path whenever you hover the mouse on the item.
To get notified of the current item change, one can use QTreeWidget::currentItemChanged or QItemSelectionModel::currentChanged.
There are two main approaches to obtaining the full path:
Iterate up the tree from the selected item and reconstruct the path. This keeps the data model normalized - without redundant information.
Store full path to each item.
If the tree is large, storing the model normalized will use less memory. Given that selection of the items is presumably rare because it's done on explicit user input, the cost of iterating the tree to extract the full path is minuscule. Humans aren't all that fast when it comes to mashing the keys or the mouse button.
The example demonstrates both approaches:
// https://github.com/KubaO/stackoverflown/tree/master/questions/tree-path-41037995
#include <QtWidgets>
QTreeWidgetItem *get(QTreeWidgetItem *parent, const QString &text) {
for (int i = 0; i < parent->childCount(); ++i) {
auto child = parent->child(i);
if (child->text(0) == text)
return child;
}
return new QTreeWidgetItem(parent, {text});
}
int main(int argc, char **argv)
{
QApplication app(argc, argv);
QStringList filenames{"TEST/branch", "TEST/foo", "trunk"};
QWidget window;
QVBoxLayout layout(&window);
QTreeWidget treeWidget;
QLabel label1, label2;
for (const auto &filename : filenames) {
QString path;
auto item = treeWidget.invisibleRootItem();
for (auto const &chunk : filename.split('/')) {
item = get(item, chunk);
path.append(QStringLiteral("/%1").arg(chunk));
item->setData(0, Qt::UserRole, path);
}
}
QObject::connect(&treeWidget, &QTreeWidget::currentItemChanged, [&](const QTreeWidgetItem *item){
QString path;
for (; item; item = item->parent())
path.prepend(QStringLiteral("/%1").arg(item->text(0)));
label1.setText(path);
});
QObject::connect(&treeWidget, &QTreeWidget::currentItemChanged, [&](const QTreeWidgetItem *item){
label2.setText(item->data(0, Qt::UserRole).toString());
});
layout.addWidget(&treeWidget);
layout.addWidget(&label1);
layout.addWidget(&label2);
window.show();
return app.exec();
}
I am using a QSqlTableModel and QTableView to view an SQLite database table.
I would like to have the table auto refresh every second or so (it's not going to be a very large table - a couple of hundred rows). And i can do this - like so:
QTimer *updateInterval = new QTimer(this);
updateInterval->setInterval(1000);
updateInterval->start();
connect(updateInterval, SIGNAL(timeout()),this, SLOT(update_table()));
...
void MainWindow::update_table()
{
model->select(); //QSqlTableModel*
sqlTable->reset(); //QTableView*
}
But this removes any selection I have, so the selections only last for up to a second. This is annoying, as another pane in the GUI depends on what is selected. If nothing is selected, then it resets to an explanation splash page.
I then tried a somewhat hacky approach, which gets the selected row number, resets the table, and then selects that row. But this doesn't work either, as the selected row can move up or down based on additions to the table.
I know other classes have a dataChanged() signal, which would be ideal.
Do any of you know how I could have the table refresh to reflect changes to the database (from either command line usage, or other instances of the program) AND keep the current selection?
I know I could get data from the current selection, and then after the reset search for the same row and then reselect it, but this seems like a counter productive and bad solution to the problem.
EDIT: Current attempt at solution:
void MainWindow::update_table()
{
QList<QModelIndex> selection = sqlTable->selectionModel()->selection().indexes();
QList<int> selectedIDs;
bool somethingSelected = true;
for(QList<QModelIndex>::iterator i = selection.begin(); i != selection.end(); ++i){
int col = i->column();
QVariant data = i->data(Qt::DisplayRole);
if(col == 0) {
selectedIDs.append(data.toInt());
}
}
if(selectedIDs.empty()) somethingSelected = false;
model->select();
sqlTable->reset();
if(somethingSelected){
QList<int> selectedRows;
int rows = model->rowCount(QModelIndex());
for(int i = 0; i < rows; ++i){
sqlTable->selectRow(i);
if(selectedIDs.contains(sqlTable->selectionModel()->selection().indexes().first().data(Qt::DisplayRole).toInt())) selectedRows.append(i);
}
for(QList<int>::iterator i = selectedRows.begin(); i != selectedRows.end(); ++i){
sqlTable->selectRow(*i);
}
}
}
Okay so this more or less works now...
The real deal is the primary key on the result of your query. Qt's APIs offer a rather circuitous route from QSqlTableModel::primaryKey() to a list of columns. The result of primaryKey() is a QSqlRecord, and you can iterate over its field()s to see what they are. You can also look up all of the fields that comprise the query proper from QSqlTableModel::record(). You find the former in the latter to get a list of model columns that comprise the query.
If your query doesn't contain a primary key, you'll have to design one yourself and offer it using some protocol. For example, you can choose that if primaryKey().isEmpty() is true, the last column returned by the model is to be used as the primary key. It's up to you to figure out how to key the result of an arbitrary query.
The selected rows can then be indexed simply by their primary keys (a list of values of the cells that comprise the key -- a QVariantList). For this, you could use a custom selection model (QItemSelectionModel) if its design wasn't broken. The key methods, such as isRowSelected() aren't virtual, and you can't reimplement them :(.
Instead, you can use a proxy model that mimicks selection by providing a custom Qt::BackgroundRole for the data. Your model sits on top of the table model, and keeps a sorted list of selected keys. Each time the proxy model's data() is called, you get the row's key from underlying query model, then search for it in your sorted list. Finally, you return a custom background role if the item is selected. You'll have to write relevant comparison operator for QVariantList. If QItemSelectionModel was usable for this purpose, you could put this functionality into a reimplementation of isRowSelected().
The model is generic since you subscribe to a certain protocol for extracting the key from a query model: namely, using primaryKey().
Instead of using primary keys explicitly, you can also use persistent indices if the model supports them. Alas, until at lest Qt 5.3.2, QSqlTableModel does not preserve the persistent indices when the query is rerun. Thus, as soon as the view changes the sort order, the persistent indices become invalid.
Below is a fully worked out example of how one might implement such a beast:
#include <QApplication>
#include <QTableView>
#include <QSqlRecord>
#include <QSqlField>
#include <QSqlQuery>
#include <QSqlTableModel>
#include <QIdentityProxyModel>
#include <QSqlDatabase>
#include <QMap>
#include <QVBoxLayout>
#include <QPushButton>
// Lexicographic comparison for a variant list
bool operator<(const QVariantList &a, const QVariantList &b) {
int count = std::max(a.count(), b.count());
// For lexicographic comparison, null comes before all else
Q_ASSERT(QVariant() < QVariant::fromValue(-1));
for (int i = 0; i < count; ++i) {
auto aValue = i < a.count() ? a.value(i) : QVariant();
auto bValue = i < b.count() ? b.value(i) : QVariant();
if (aValue < bValue) return true;
}
return false;
}
class RowSelectionEmulatorProxy : public QIdentityProxyModel {
Q_OBJECT
Q_PROPERTY(QBrush selectedBrush READ selectedBrush WRITE setSelectedBrush)
QMap<QVariantList, QModelIndex> mutable m_selection;
QVector<int> m_roles;
QBrush m_selectedBrush;
bool m_ignoreReset;
class SqlTableModel : public QSqlTableModel {
public:
using QSqlTableModel::primaryValues;
};
SqlTableModel * source() const {
return static_cast<SqlTableModel*>(dynamic_cast<QSqlTableModel*>(sourceModel()));
}
QVariantList primaryValues(int row) const {
auto record = source()->primaryValues(row);
QVariantList values;
for (int i = 0; i < record.count(); ++i) values << record.field(i).value();
return values;
}
void notifyOfChanges(int row) {
emit dataChanged(index(row, 0), index(row, columnCount()-1), m_roles);
}
void notifyOfAllChanges(bool remove = false) {
auto it = m_selection.begin();
while (it != m_selection.end()) {
if (it->isValid()) notifyOfChanges(it->row());
if (remove) it = m_selection.erase(it); else ++it;
}
}
public:
RowSelectionEmulatorProxy(QObject* parent = 0) :
QIdentityProxyModel(parent), m_roles(QVector<int>() << Qt::BackgroundRole),
m_ignoreReset(false) {
connect(this, &QAbstractItemModel::modelReset, [this]{
if (! m_ignoreReset) {
m_selection.clear();
} else {
for (auto it = m_selection.begin(); it != m_selection.end(); ++it) {
*it = QModelIndex(); // invalidate the cached mapping
}
}
});
}
QBrush selectedBrush() const { return m_selectedBrush; }
void setSelectedBrush(const QBrush & brush) {
if (brush == m_selectedBrush) return;
m_selectedBrush = brush;
notifyOfAllChanges();
}
QList<int> selectedRows() const {
QList<int> result;
for (auto it = m_selection.begin(); it != m_selection.end(); ++it) {
if (it->isValid()) result << it->row();
}
return result;
}
bool isRowSelected(const QModelIndex &proxyIndex) const {
if (! source() || proxyIndex.row() >= rowCount()) return false;
auto primaryKey = primaryValues(proxyIndex.row());
return m_selection.contains(primaryKey);
}
Q_SLOT void selectRow(const QModelIndex &proxyIndex, bool selected = true) {
if (! source() || proxyIndex.row() >= rowCount()) return;
auto primaryKey = primaryValues(proxyIndex.row());
if (selected) {
m_selection.insert(primaryKey, proxyIndex);
} else {
m_selection.remove(primaryKey);
}
notifyOfChanges(proxyIndex.row());
}
Q_SLOT void toggleRowSelection(const QModelIndex &proxyIndex) {
selectRow(proxyIndex, !isRowSelected(proxyIndex));
}
Q_SLOT virtual void clearSelection() {
notifyOfAllChanges(true);
}
QVariant data(const QModelIndex &proxyIndex, int role) const Q_DECL_OVERRIDE {
QVariant value = QIdentityProxyModel::data(proxyIndex, role);
if (proxyIndex.row() < rowCount() && source()) {
auto primaryKey = primaryValues(proxyIndex.row());
auto it = m_selection.find(primaryKey);
if (it != m_selection.end()) {
// update the cache
if (! it->isValid()) *it = proxyIndex;
// return the background
if (role == Qt::BackgroundRole) return m_selectedBrush;
}
}
return value;
}
bool setData(const QModelIndex &, const QVariant &, int) Q_DECL_OVERRIDE {
return false;
}
void sort(int column, Qt::SortOrder order) Q_DECL_OVERRIDE {
m_ignoreReset = true;
QIdentityProxyModel::sort(column, order);
m_ignoreReset = false;
}
void setSourceModel(QAbstractItemModel * model) Q_DECL_OVERRIDE {
m_selection.clear();
QIdentityProxyModel::setSourceModel(model);
}
};
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
QWidget w;
QVBoxLayout layout(&w);
QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE");
db.setDatabaseName(":memory:");
if (! db.open()) return 255;
QSqlQuery query(db);
query.exec("create table chaps (name, age, constraint pk primary key (name, age));");
query.exec("insert into chaps (name, age) values "
"('Bob', 20), ('Rob', 30), ('Sue', 25), ('Hob', 40);");
QSqlTableModel model(nullptr, db);
model.setTable("chaps");
RowSelectionEmulatorProxy proxy;
proxy.setSourceModel(&model);
proxy.setSelectedBrush(QBrush(Qt::yellow));
QTableView view;
view.setModel(&proxy);
view.setEditTriggers(QAbstractItemView::NoEditTriggers);
view.setSelectionMode(QAbstractItemView::NoSelection);
view.setSortingEnabled(true);
QObject::connect(&view, &QAbstractItemView::clicked, [&proxy](const QModelIndex & index){
proxy.toggleRowSelection(index);
});
QPushButton clearSelection("Clear Selection");
QObject::connect(&clearSelection, &QPushButton::clicked, [&proxy]{ proxy.clearSelection(); });
layout.addWidget(&view);
layout.addWidget(&clearSelection);
w.show();
app.exec();
}
#include "main.moc"
I need my QT application to create a table and copy this table into the clipboard, so that it can be pasted as table into libreoffice Writer or MS Word later.
My first approach was to create html code for the table and insert it into the clipboard with
QClipboard *clipboard = QApplication::clipboard();
QMimeData *mimeData = new QMimeData();
mimeData->setData("text/html", html.toUtf8());
clipboard->setMimeData(mimeData, QClipboard::Clipboard);
This approach didn't work. When pasting, the table cells where just appended to each other and inserted without formatting.
My second approach using RTF:
QTextDocument rtfDocument;
rtfDocument.setHtml(html);
But I found no way to copy this QTextDocument into the clipboard. Is there any?
If I could get the RTF code out of the QTextDocument, I could use a way like
QClipboard *clipboard = QApplication::clipboard();
QMimeData *mimeData = new QMimeData();
mimeData->setData("text/rtf", rtfDocument.getCode());
clipboard->setMimeData(mimeData, QClipboard::Clipboard);
But I also didn't find a function returning the rtf code.
edit:
With the last code box above I have a working way to copy rtf code into the clipboard. So any solution that can create RTF code representing a table would solve my problem.
I'm not sure what the source of your data is, but here is code we used to subclass the normal QTableView to make it copy-able. Some of the code has been cut out, but you can get the basic idea. RTF/HTML is overkill--all the spreadsheets accept good ol' CSV.
Of course, this answer won't help at all if you require formatting. I wasn't clear from your question if that was a requirement or not.
// Escapes a string according to RFC-4180 specification.
static QString csvEscape(const QString &value) {
if (value.contains(QRegExp(QLatin1String("[\"\\n\\r,]")))) {
QString escaped(value);
escaped.replace(QLatin1String("\""), QLatin1String("\"\""));
return QString::fromLatin1("\"%1\"").arg(escaped);
} else {
return value;
}
}
void ClipboardAwareTableView::Copy() const {
QModelIndexList indexes = selectedIndexes();
Q_ASSERT(!indexes.isEmpty());
if(indexes.isEmpty()) {
return;
}
// The default sort is by rows then columns. This is what we want.
qSort(indexes);
// Remember the mapping between model columns and visible columns. This is
// local instead of an instance member because it would need to be invalidated
// any time a column is added, removed, or moved. The minor performance hit
// is worth the simplicity.
QHash<int, int> map_cache;
// Before we start exporting text, we have to know the index of the left-
// most column in our selection range so we can add the appropriate number
// of column separators.
int minimum_column = GetViewColumnIndex(indexes.first().column(), &map_cache);
for (int i = 1; i < indexes.size(); ++i) {
minimum_column =
qMin(minimum_column,
GetViewColumnIndex(indexes.at(i).column(), &map_cache));
}
// Keep track of the previous index so that we know if we need a new line and
// how many column separators to insert. We start with an invalid index.
QModelIndex previous;
QString text;
for (int i = 0; i < indexes.size(); ++i) {
QModelIndex current = indexes.at(i);
// Do we need to add a new line character?
if (previous.isValid() && current.row() != previous.row()) {
text.append(QLatin1String("\n"));
}
// Are we on a new line?
if (!previous.isValid() || current.row() != previous.row()) {
// Add enough separators to get from the minimum to the current column.
text.append(QString::fromLatin1(",")
.repeated(GetViewColumnIndex(current.column(), &map_cache) -
minimum_column));
} else {
// Add enough separators to get from the previous to the current column.
text.append(QString::fromLatin1(",")
.repeated(GetViewColumnIndex(current.column(), &map_cache) -
GetViewColumnIndex(previous.column(), &map_cache)));
}
// Append the text. If the column delegate is a QStyledItemDelegate, we use
// the display text.
QStyledItemDelegate *delegate =
qobject_cast<QStyledItemDelegate*>(
itemDelegateForColumn(current.column()));
if (delegate) {
text.append(csvEscape(delegate->displayText(current.data(), QLocale())));
} else {
text.append(csvEscape(current.data().toString()));
}
previous = current;
}
qApp->clipboard()->setText(text);
}
int ClipboardAwareTableView::GetViewColumnIndex(
int model_column_index,
QHash<int, int> *cached_mappings) const {
if (cached_mappings->contains(model_column_index)) {
return cached_mappings->value(model_column_index);
}
int view_index = 0;
for (int i = 0; i < model()->columnCount(); ++i) {
if (model_column_index == i) {
cached_mappings->insert(model_column_index, view_index);
return view_index;
} else if (!isColumnHidden(i)) {
++view_index;
}
}
throw std::invalid_argument("model_column_index was out of range.");
}
void ClipboardAwareTableView::keyPressEvent(QKeyEvent *event) {
if (event->matches(QKeySequence::Copy) && !selectedIndexes().isEmpty()) {
Copy();
event->accept();
return; // The base class implementation will overwrite the clipboard.
}
event->ignore();
QTableView::keyPressEvent(event);
}
You could try using QTextDocument::toHtml() and set the mime type to text/html
I wrote in gedit 1[tab space]2[tab space]3\n4[tab space]5[tab space]6 and copied it to spreadsheet and it worked. So, I think if you use "\t" for separating cells in rows and "\n" for separating rows, it will work.
I have a SQLite-Database and I did it into a QSqlTableModel.
To show the Database, I put that Model into a QTableView.
Now I want to create a Method where the selected Rows (or the whole Line) will be copied into the QClipboard. After that I want to insert it into my OpenOffice.Calc-Document.
But I have no Idea what to do with the Selected SIGNAL and the QModelIndex and how to put this into the Clipboard.
To actually capture the selection you use the item view's selection model to get a list of indices. Given that you have a QTableView * called view you get the selection this way:
QAbstractItemModel * model = view->model();
QItemSelectionModel * selection = view->selectionModel();
QModelIndexList indexes = selection->selectedIndexes();
Then loop through the index list calling model->data(index) on each index. Convert the data to a string if it isn't already and concatenate each string together. Then you can use QClipboard.setText to paste the result to the clipboard. Note that, for Excel and Calc, each column is separated from the next by a newline ("\n") and each row is separated by a tab ("\t"). You have to check the indices to determine when you move to the next row.
QString selected_text;
// You need a pair of indexes to find the row changes
QModelIndex previous = indexes.first();
indexes.removeFirst();
foreach(const QModelIndex ¤t, indexes)
{
QVariant data = model->data(current);
QString text = data.toString();
// At this point `text` contains the text in one cell
selected_text.append(text);
// If you are at the start of the row the row number of the previous index
// isn't the same. Text is followed by a row separator, which is a newline.
if (current.row() != previous.row())
{
selected_text.append('\n');
}
// Otherwise it's the same row, so append a column separator, which is a tab.
else
{
selected_text.append('\t');
}
previous = current;
}
QApplication.clipboard().setText(selected_text);
Warning: I have not had a chance to try this code, but a PyQt equivalent works.
I had a similar problem and ended up adapting QTableWidget (which is an extension of QTableView) to add copy/paste functionality. Here is the code which builds on what was provided by quark above:
qtablewidgetwithcopypaste.h
// QTableWidget with support for copy and paste added
// Here copy and paste can copy/paste the entire grid of cells
#ifndef QTABLEWIDGETWITHCOPYPASTE_H
#define QTABLEWIDGETWITHCOPYPASTE_H
#include <QTableWidget>
#include <QKeyEvent>
#include <QWidget>
class QTableWidgetWithCopyPaste : public QTableWidget
{
Q_OBJECT
public:
QTableWidgetWithCopyPaste(int rows, int columns, QWidget *parent = 0) :
QTableWidget(rows, columns, parent)
{}
QTableWidgetWithCopyPaste(QWidget *parent = 0) :
QTableWidget(parent)
{}
private:
void copy();
void paste();
public slots:
void keyPressEvent(QKeyEvent * event);
};
#endif // QTABLEWIDGETWITHCOPYPASTE_H
qtablewidgetwithcopypaste.cpp
#include "qtablewidgetwithcopypaste.h"
#include <QApplication>
#include <QMessageBox>
#include <QClipboard>
#include <QMimeData>
void QTableWidgetWithCopyPaste::copy()
{
QItemSelectionModel * selection = selectionModel();
QModelIndexList indexes = selection->selectedIndexes();
if(indexes.size() < 1)
return;
// QModelIndex::operator < sorts first by row, then by column.
// this is what we need
// std::sort(indexes.begin(), indexes.end());
qSort(indexes);
// You need a pair of indexes to find the row changes
QModelIndex previous = indexes.first();
indexes.removeFirst();
QString selected_text_as_html;
QString selected_text;
selected_text_as_html.prepend("<html><style>br{mso-data-placement:same-cell;}</style><table><tr><td>");
QModelIndex current;
Q_FOREACH(current, indexes)
{
QVariant data = model()->data(previous);
QString text = data.toString();
selected_text.append(text);
text.replace("\n","<br>");
// At this point `text` contains the text in one cell
selected_text_as_html.append(text);
// If you are at the start of the row the row number of the previous index
// isn't the same. Text is followed by a row separator, which is a newline.
if (current.row() != previous.row())
{
selected_text_as_html.append("</td></tr><tr><td>");
selected_text.append(QLatin1Char('\n'));
}
// Otherwise it's the same row, so append a column separator, which is a tab.
else
{
selected_text_as_html.append("</td><td>");
selected_text.append(QLatin1Char('\t'));
}
previous = current;
}
// add last element
selected_text_as_html.append(model()->data(current).toString());
selected_text.append(model()->data(current).toString());
selected_text_as_html.append("</td></tr>");
QMimeData * md = new QMimeData;
md->setHtml(selected_text_as_html);
// qApp->clipboard()->setText(selected_text);
md->setText(selected_text);
qApp->clipboard()->setMimeData(md);
// selected_text.append(QLatin1Char('\n'));
// qApp->clipboard()->setText(selected_text);
}
void QTableWidgetWithCopyPaste::paste()
{
if(qApp->clipboard()->mimeData()->hasHtml())
{
// TODO, parse the html data
}
else
{
QString selected_text = qApp->clipboard()->text();
QStringList cells = selected_text.split(QRegExp(QLatin1String("\\n|\\t")));
while(!cells.empty() && cells.back().size() == 0)
{
cells.pop_back(); // strip empty trailing tokens
}
int rows = selected_text.count(QLatin1Char('\n'));
int cols = cells.size() / rows;
if(cells.size() % rows != 0)
{
// error, uneven number of columns, probably bad data
QMessageBox::critical(this, tr("Error"),
tr("Invalid clipboard data, unable to perform paste operation."));
return;
}
if(cols != columnCount())
{
// error, clipboard does not match current number of columns
QMessageBox::critical(this, tr("Error"),
tr("Invalid clipboard data, incorrect number of columns."));
return;
}
// don't clear the grid, we want to keep any existing headers
setRowCount(rows);
// setColumnCount(cols);
int cell = 0;
for(int row=0; row < rows; ++row)
{
for(int col=0; col < cols; ++col, ++cell)
{
QTableWidgetItem *newItem = new QTableWidgetItem(cells[cell]);
setItem(row, col, newItem);
}
}
}
}
void QTableWidgetWithCopyPaste::keyPressEvent(QKeyEvent * event)
{
if(event->matches(QKeySequence::Copy) )
{
copy();
}
else if(event->matches(QKeySequence::Paste) )
{
paste();
}
else
{
QTableWidget::keyPressEvent(event);
}
}
Quark's answer (the selected one) is good for pointing people in the right direction, but his algorithm is entirely incorrect. In addition to an off by one error and incorrect assignment, its not even syntactically correct. Below is a working version that I just wrote and tested.
Let's assume our example table looks like so:
A | B | C
D | E | F
The problem with Quark's algorithm is the following:
If we replace his \t separator with a ' | ', it will produce this output:
B | C | D
E | F |
The off by one error is that D appears in the first row. The incorrect assignment is evidenced by the omission of A
The following algorithm corrects these two problems with correct syntax.
QString clipboardString;
QModelIndexList selectedIndexes = view->selectionModel()->selectedIndexes();
for (int i = 0; i < selectedIndexes.count(); ++i)
{
QModelIndex current = selectedIndexes[i];
QString displayText = current.data(Qt::DisplayRole).toString();
// If there exists another column beyond this one.
if (i + 1 < selectedIndexes.count())
{
QModelIndex next = selectedIndexes[i+1];
// If the column is on different row, the clipboard should take note.
if (next.row() != current.row())
{
displayText.append("\n");
}
else
{
// Otherwise append a column separator.
displayText.append(" | ");
}
}
clipboardString.append(displayText);
}
QApplication::clipboard()->setText(clipboardString);
The reason I chose to use a counter instead of an iterator is just because it is easier to test if there exists another index by checking against the count. With an iterator, I suppose maybe you could just increment it and store it in a weak pointer to test if it is valid but just use a counter like I did above.
We need to check if the next line will be on on a new row. If we are on a new row and we check the previous row as Quark's algorithm does, its already too late to append. We could prepend, but then we have to keep track of the last string size. The above code will produce the following output from the example table:
A | B | C
D | E | F
For whatever reason I didn't have access to the std::sort function, however I did find that as a neat alternative to Corwin Joy's solution, the sort function can be implemented by replacing
std::sort(indexes.begin(), indexes.end());
with
qSort(indexes);
This is the same as writing:
qSort(indexes.begin(), indexes.end());
Thanks for your helpful code guys!
I wrote some code based on some of the others' answers. I subclassed QTableWidget and overrode keyPressEvent() to allow the user to copy the selected rows to the clipboard by typing Control-C.
void MyTableWidget::keyPressEvent(QKeyEvent* event) {
// If Ctrl-C typed
if (event->key() == Qt::Key_C && (event->modifiers() & Qt::ControlModifier))
{
QModelIndexList cells = selectedIndexes();
qSort(cells); // Necessary, otherwise they are in column order
QString text;
int currentRow = 0; // To determine when to insert newlines
foreach (const QModelIndex& cell, cells) {
if (text.length() == 0) {
// First item
} else if (cell.row() != currentRow) {
// New row
text += '\n';
} else {
// Next cell
text += '\t';
}
currentRow = cell.row();
text += cell.data().toString();
}
QApplication::clipboard()->setText(text);
}
}
Output example (tab-separated):
foo bar baz qux
bar baz qux foo
baz qux foo bar
qux foo bar baz
What you'll need to do is access the text data in the model, then pass that text to the QClipboard.
To access the text data in the model, use QModelIndex::data(). The default argument is Qt::DisplayRole, i.e. the displayed text.
Once you've retrieved the text, pass that text to the clipboard using QClipboard::setText().
a pyqt py2.x example:
selection = self.table.selectionModel() #self.table = QAbstractItemView
indexes = selection.selectedIndexes()
columns = indexes[-1].column() - indexes[0].column() + 1
rows = len(indexes) / columns
textTable = [[""] * columns for i in xrange(rows)]
for i, index in enumerate(indexes):
textTable[i % rows][i / rows] = unicode(self.model.data(index).toString()) #self.model = QAbstractItemModel
return "\n".join(("\t".join(i) for i in textTable))
I finally got it, thanks.
void Widget::copy() {
QItemSelectionModel *selectionM = tableView->selectionModel();
QModelIndexList selectionL = selectionM->selectedIndexes();
selectionL.takeFirst(); // ID, not necessary
QString *selectionS = new QString(model->data(selectionL.takeFirst()).toString());
selectionS->append(", ");
selectionS->append(model->data(selectionL.takeFirst()).toString());
selectionS->append(", ");
selectionS->append(model->data(selectionL.takeFirst()).toString());
selectionS->append(", ");
selectionS->append(model->data(selectionL.takeFirst()).toString());
clipboard->setText(*selectionS);
}
and
connect (tableView, SIGNAL(clicked(QModelIndex)), this, SLOT(copy()));
I can't help but notice that you can simplify your code using a foreach() construct and the QStringList class, which has a convenient join() function.
void Widget::copy()
{
QStringList list ;
foreach ( const QModelIndex& index, tableView->selectedIndexes() )
{
list << index.data() ;
}
clipboard->setText( list.join( ", " ) ) ;
}
Careful with the last element. Note below, indexes may become empty after 'removeFirst()'. Thus, 'current' is never valid and should not be used in model()->data(current).
indexes.removeFirst();
QString selected_text;
QModelIndex current;
Q_FOREACH(current, indexes)
{
.
.
.
}
// add last element
selected_text.append(model()->data(current).toString());
Consider
QModelIndex last = indexes.last();
indexes.removeFirst();
QString selected_text;
Q_FOREACH(QModelIndex current, indexes)
{
.
.
.
}
// add last element
selected_text.append(model()->data(last).toString());
Here is a variation on what Corwin Joy posted that works with QTableView and handles sparse selections differently. With this code if you have different columns selected in different rows (e.g. selected cells are (1,1), (1, 2), (2, 1), (3,2)) then when you paste it you will get empty cells corresponding to the "holes" in your selection (e.g. cells (2,2) and (3,1)). It also pulls in the column header text for columns that intersect the selection.
void CopyableTableView::copy()
{
QItemSelectionModel *selection = selectionModel();
QModelIndexList indices = selection->selectedIndexes();
if(indices.isEmpty())
return;
QMap<int, bool> selectedColumnsMap;
foreach (QModelIndex current, indices) {
selectedColumnsMap[current.column()] = true;
}
QList<int> selectedColumns = selectedColumnsMap.uniqueKeys();
int minCol = selectedColumns.first();
// prepend headers for selected columns
QString selectedText;
foreach (int column, selectedColumns) {
selectedText += model()->headerData(column, Qt::Horizontal, Qt::DisplayRole).toString();
if (column != selectedColumns.last())
selectedText += QLatin1Char('\t');
}
selectedText += QLatin1Char('\n');
// QModelIndex::operator < sorts first by row, then by column.
// this is what we need
qSort(indices);
int lastRow = indices.first().row();
int lastColumn = minCol;
foreach (QModelIndex current, indices) {
if (current.row() != lastRow) {
selectedText += QLatin1Char('\n');
lastColumn = minCol;
lastRow = current.row();
}
if (current.column() != lastColumn) {
for (int i = 0; i < current.column() - lastColumn; ++i)
selectedText += QLatin1Char('\t');
lastColumn = current.column();
}
selectedText += model()->data(current).toString();
}
selectedText += QLatin1Char('\n');
QApplication::clipboard()->setText(selectedText);
}
If anybody is interested, this web page provide a working code project on this topic, it's working pretty well.
Copy / paste functionality implementation for QAbstractTableModel / QTableView