I have a QTableWidget, where Drag & Drop is enabled using the table widget's QHeaderView instance:
m_tableWidget->verticalHeader()->setSectionsMovable(true);
Also, I have implemented an undo mechanic. Now, I want to implement that a Drag & Drop action can be reversed using said undo mechanic.
For this undo mechanic, I need to save the data before and after the Drag & Drop. Let's say I have a method to store old data, which is not important right now.
To store the new data and push on the undo stack, I connect to the sectionMoved signal of the QHeaderView:
connect(m_tableWidget->verticalHeader(), &QHeaderView::sectionMoved, this, &MyTableWidget::dragAndDrop);
The function which is then called basically looks like this:
void
MyTableWidget::dragAndDrop(int logicalIndex, int oldVisualIndex, int newVisualIndex)
{
// Get the table data which is stored in a vector
auto& tableData = oldVector;
// Synchronize to reflect the drag & drop change
auto synchronizedTableData = synchronize(oldVisualIndex, newVisualIndex);
// Push on the undo stack
m_undoStack->push(new Undo(synchronizedTableData, oldTableData));
}
And, finally, the function in the undo stack basically looks like this:
void
Undo::redo()
{
tableWidget->setRowCount(synchronizedTableData.size());
// Apply the synchronized vector data to the table
for (int i = 0; i < synchronizedTableData.size(); ++i) {
for (int j = 0; j < synchronizedTableData.at(i).size(); ++j) {
m_tableWidget->setItem(i, j, new QTableWidgetItem(synchronizedTableData.at(i).at(j).toString()));
}
}
}
}
However, the whole approach has one major flaw: Afaik, the drag & drop using the vertical header only changes the view, while the model itself is not updated. So, basically, if I iterated over the table and would display its contents, I would still have the items arranged the way they were before the drag & drop.
Now, if the table is set as shown in the undo stack function, the view is broken as shown in the next few images:
If I want to drag the second row before the last one, it should look like this:
But instead, it looks like this:
This looks like the drag & drop has actually been performed twice: 123456 -> 134526 -> 145236. Interesting, however, is that if I iterate over the table once more, the data has been changed correctly. So now, the model is intact, yet the view is not updated properly.
Now, my guess is that I mixed something up between the view and model for the vertical header. So, my idea is that I could fix this problem by updating the model behind the header after I dragged & dropped a row. Or, alternatively, that I have to reupdate the view after the model has been successfully updated in the undo stack.
But I don't know how to achieve that. I still do not really know what exactly in the header is called twice so that the view is changed twice. Maybe the whole approach itself is flawed? Any idea would be appreciated.
Thanks in advance!
So, I found a first solution. However, it only works as some sort of a workaround.
The new idea is, that I can save the header state and restore it.
QByteArray m_headerState;
connect(m_tableWidget->verticalHeader(), &QHeaderView::sectionPressed, this, [](int logicalIndex) {
m_headerState = m_tableWidget->verticalHeader->saveState();
});
So basically, every time I want to rearrange header items via drag and drop, I have to press such an item. I can use this to store the old header state. Then, after the drag & drop, I can do the following:
void
MyTableWidget::dragAndDrop(int logicalIndex, int oldVisualIndex, int newVisualIndex)
{
// Restore the old header state
m_tableWidget->verticalHeader()->restoreState(m_headerState);
// Get the table data which is stored in a vector
auto& tableData = oldVector;
// Synchronize to reflect the drag & drop change
auto synchronizedTableData = synchronize(oldVisualIndex, newVisualIndex);
// Push on the undo stack
m_undoStack->push(new Undo(synchronizedTableData, oldTableData));
}
Now, I restore this state after the drag & drop and do it manually. This is of course error-prone, but at least prevents the drag & drop being performed twice.
However, it does not answer the question itself. Therefore, I won't mark this as an answer. But in case someone else might encounter this problem, this might help you.
Related
I'm using wxWidgets 3.1.0 and I'm developing a Windows app in C++.
I'm using the base wxGrid and I've enabled column re-ordering via dragging them with the mouse (EnableDragColMove(true)). My problem now is that I need to get the moved column's new position/index after the column has been dragged to its new position.
Unfortunately, I could not find a way to do that from the available APIs.
I've tried catching the wxGridEvent wxEVT_GRID_COL_MOVE then using GetCol() and GetColPos() to check the column's new index:
gridDataList->Bind(wxEVT_GRID_COL_MOVE, &FormData::OnList_ColumnMove, this);
...
void FormData::OnList_ColumnMove(wxGridEvent& event)
{
int movedCol = event.GetCol();
int movedColPos = gridDataList->GetColPos(movedCol );
...
}
But it seems the event is triggered BEFORE the column is actually moved, so GetColPos() will still return the current column index, NOT the new index.
There seems to be no event to catch AFTER the column is moved.
My current solutions/workarounds are to:
Manually handle the column movement after catching the wxEVT_GRID_COL_MOVE event (as suggested in the wxWidgets docs) so that I can properly track the before and after index of the moved column.
Manually trigger a callback or a timer event after the column moves to its new position, similar to a wxPython workaround suggested in another SO post.
Though, I would like to know if there is a cleaner, less complicated way without resorting to the workarounds above.
Any suggestions are appreciated.
Yes, this wxEVT_GRID_COL_MOVE is generated before moving the column because it can be vetoed, preventing the move from happening. And it's true that it would be convenient if it carried the new column position, but unfortunately currently it doesn't (it would be simple to fix this and any patches doing this would be welcome!).
The standard workaround of using CallAfter() to execute your code at later time should work just fine without changing wxWidgets however. I.e., assuming you use C++11, you should be able to just write
void FormData::OnList_ColumnMove(wxGridEvent& event)
{
const int movedCol = event.GetCol();
CallAfter([movedCol]() {
int movedColPos = gridDataList->GetColPos(movedCol);
...
});
}
I created the wxGrid in editablemode. I registered the following event handler.
class ReadWriteGrid : public wxGrid
{
public:
ReadWriteGrid(wxWindow *parent, wxWindowID ID,
const wxPoint &pos=wxDefaultPosition, const wxSize &size=wxDefaultSize,
long style=262144, const wxString &name=wxGridNameStr)
: wxGrid(parent, ID, pos, size, style, name)
{
SetEditable();
}
};
EVT_GRID_CELL_CHANGE(IndexTableFrame::OnEditField)
Now when user changes the cell value my function gets called but after it completes. The cell value changes back to its old value.
void IndexTableFrame::OnEditField(wxGridEvent& event)
{
int RowNumber;
int ColNumber;
wxString type;
wxGridCellEditor *m_Editor;
wxString NewValue;
RowNumber = event.GetRow();
ColNumber = event.GetCol();
m_Editor = m_grid->GetDefaultEditorForCell(RowNumber,ColNumber);
NewValue = m_Editor->GetValue();
m_Editor->Destroy();
m_grid->SetCellValue(NewValue,RowNumber,ColNumber);
event.skip();
}
This is the first time I am using WxWidget. I am not sure how to avoid the cell from changing back to its old value.
You are working too hard! Relax and let wxGrid take the strain.
The simplest thing to do is to do nothing. wxGrid will accept user edits in cells and display the changes without you having to do anything. When the user is finished, he can click a button SAVE, where you can implement code to read the values from the cells and use them for whatever you need.
In many applications, one has to do processing on the cell value on the fly as the user changes it, (for example to update other on-screen controls on the fly based on the new cell values; or to send data via a serial port to another device immediately as the cells are changed).
Requiring the user to click a separate "save button" as suggested above is not a solution that will be acceptable to users in many use cases.
I can confirm the observations by the original poster: If you create a wxGrid and attach either wxEVT_GRID_CELL_CHANGED, wxEVT_GRID_CELL_CHANGING, or wxEVT_GRID_EDITOR_HIDDEN events to it, a call to grid->GetCellValue(row,col); returns the old value. The event parameter to the handler function also does not contain the new text. It looks like a "feature" of wxGrid.
You should create your own class inherited from wxGridTableBase.
Then you should attach your grid table object to the wxGrid, using it's SetTable method.
wxGrid object will use your table's methods (GetValue, SetValue) to retrieve and store data.
There's a grid sample shipped with wxWidgets, that will help you understand how does wxGrid work.
I am a student programmer using Qt to build a GUI for work. I have ran into an issue; more or less and inconvience of sorts wiith multiple selections in the QTreeWidget. My GUI has a main interface with a QTreeWidget as the central item in this window. Below the QTreeWidget I have several buttons; copy, edit, and delete. As you might've already guessed each one of these buttons correlates to a function that executes the command. My tree widget has the ability to select multiple items; however, when multiple items are selected the only item that is passed through is the last item that was selected. I was hoping that somebody with some more insight in this IDE might be able to point me in the right direction for accomplishing this. Here is the process that is followed when one of these functions is executed.
void InjectionGUI::copyInjection_Clicked(QTreeWidgetItem *itemToCopy)
{
InjectionData copyInjectionData; //first use data from the tree widget row
QString converter = itemToCopy->text(0); //to find the vector item that will be copied
int id = converter.toInt();
int nameNumber;
copyInjectionData = qTreeInjectionData.at(id);
qTreeInjectionData.append(copyInjectionData);
buildTreeWidget();
}
void InjectionGUI::slotInjectionCopy()
{
if(ui->treeWidgetInjections->currentItem() == 0)
{
QMessageBox invalidSelection;
invalidSelection.setText("No row selected to copy");
invalidSelection.setWindowTitle("Error");
invalidSelection.exec();
}
else
{
copyInjection_Clicked(ui->treeWidgetInjections->currentItem());
}
}
I'm not too sure what code will be relevant towards making this change; so if there is additional structure that anyone would like to see please just requested. I'm pretty sure that my problem or my solution is going to lie in the way that I'm using current item. After reviewing the documentation from Qt's website I'm still unsure how I would change this to allow multiple selections to be passed through the function. Please only provide constructive feedback; I'm only interested in learning and accomplishing a solution. Thanks in advance.
UPDATE! SOLVED!!!
Just thought it might be nice to show what this looked like implemented:
QList<QTreeWidgetItem *> items = ui->treeWidgetInjections->selectedItems();
for(int i = 0; i < items.size(); i++)
{
QTreeWidgetItem *qTreeWidgetitem = new QTreeWidgetItem;
qTreeWidgetitem = items.at(i);
copyInjection_Clicked(qTreeWidgetitem);
}
If you need to know which items are selected, you can use
QList<QTreeWidgetItem *> QTreeWidget::selectedItems() const
to have a list of all the currently selected items in the tree.
Then you may call your function once for every item in the list, or you can overload your function to take as argument a QList<QTreeWidgetItem *> and then run through the list inside the called function.
I have a Qt application for which I derived my own model class from QAbstractTableModel. I have implemented the necessary methods as prescribed in the documentation. When I call removeRows method the changes are correct in my View (the rows I wanted to remove are removed).
But somehow, the operations on the model doesn't seem to be propagated to the QList I use in the model to store my data. When I save the values stored in the QList to the disk, it look like nothing was erased from it by removeRows.
Here is what my removeRows implementation looks like (it is based on the code from the book Advanced Qt Programming, Chapter 3, p.125):
bool MyModel::removeRows(int row, int count, const QModelIndex&)
{
beginRemoveRows( QModelIndex(), row, row + count - 1);
for (int i = 0; i < count; ++i) {
mMyQList.removeAt(row);
}
endRemoveRows();
return true;
}
How do I fix this? What did I miss?
Thanks!
Like Frank O. implies, it's hard to know what's going on without seeing some code. But from the sound of it, the values haven't been removed from QList simply because you haven't taken them out. When you move from Widget to Model/View classes, you have to do this yourself. I.e., in your removeRows() method you must remove the rows from the QList 'by hand'.
It turns out that nothing was wrong with my implementation of removeRows.
The save method was called by my unit tests just before showing my dialog. The dialog was not calling the save method at all.
No wonder the change were visible in the View and not in the output file...
I'm trying to keep some array data synchronized with the contents of a QTableWidget. I'd like to enable drag and drop reordering (moving items within the table, as opposed to copying), but it's not clear to me how, when the drop event is fired, I can find out what index the item was dragged FROM. Hence, I have no way of knowing what object to move within the list I'm synchronizing with. How can I get the original row index of the item being dragged?
Encode the from index in QMimeData and store it in the QDrag object with setMimeData(). When the drop event occurs, extract the data from the QDropEvent with mimeData().
Step 1. Override the QTableWidget::mimeData function. Call the base class implementation, then stuff your own custom MIME type into the QMimeData, and return it.
Step 2. Override the QTableWidget::dropEvent function. If your MIME data is in the QMimeData, accept the drop and extract your data. Use the QTableWidget::indexAt to find what row/column the drop went into.
QDropEvent has a source() function that will give you the widget that started the drag drop event. Then do a qobject_cast<QTableWidget> on the source. Once you verify the pointer, call QTableWidget::findItems to get the row of the item.
So something like this:
void dropEvent ( QDropEvent * event ) {
if (event) {
QTableWidget* table = qobject_cast<QTableWidget*>(event->source());
if (table) {
QString item = ""// Decode MIME data here.
Qt::MatchFlag someFlag = Qt::MatchExactly; // Check documentation for match type.
QList<QTableWidgetItem *> items = table->findItems(item, someFlag)
// If you don't have repeats, items[0] is what you want.
int initialRow = table->row(items[0]);
}
}
}
I tend to use model/view classes so this might be a little off, but it should work.