How to simulate a drag and drop action using QTest - qt

In order to create a test case for a drag and drop bug in the QTreeView widget I tried to simulate a drag and drop mouse movement behavior.
I basically select the first element in the QTreeView and want it to drag and drop on the third element. I did this by using a combination of QTest::mousePress and QTest::mouseMove. At the end there should be of course a QTest::mouseRelease, but I already failed in reproducing the mousePress and mouseMove.
These are steps necessary to reproduce:
User moves mouse on center of first item
User presses the left mouse button
User holds the left mouse button pressed and moves the mouse to the center of the third item
User releases the left mouse button
If one does these action as described I can see, that the QTreeView Widgets reacts appropriately, and indicating special highlights and vertical lines, in case the item will be move between to items.
Unfortunately, my automatized test fails to reproduce this behavior. It seems that calling QTest::mousePress in sequence does something different. Also using a pair of QTest::mousePress and QTest::mouseMove is something differently.
This is my code:
main.cpp
#include "TestObject.h"
#include <QTest>
QTEST_MAIN(TestObject)
TestObject.h
#include <QtTest/QtTest>
class TestObject : public QObject
{
Q_OBJECT
private slots:
void dragAndDrop();
};
TestObject.cpp
#include "TestObject.h"
#include "TestObject.moc"
#include <QTest>
#include <QTreeView>
#include <QStandardItemModel>
#include <QPropertyAnimation>
#include "MouseMover.h"
void TestObject::dragAndDrop() {
qDebug() << "Hello";
QStandardItemModel model;
QTreeView view;
view.setModel(&model);
view.show();
view.setHeaderHidden(true);
view.setDragDropMode(QAbstractItemView::DragDropMode::DragDrop);
view.setDefaultDropAction(Qt::DropAction::MoveAction);
view.setColumnHidden(1, true);
for (auto rowIter = 0; rowIter < 3; rowIter++) {
QList<QStandardItem*> items;
for (auto colIter = 0; colIter < 2; colIter++) {
items << new QStandardItem(QString("%1-%2").arg(rowIter).arg(colIter));
}
model.appendRow(items);
}
MouseMover mover;
mover.setWidget(view.viewport());
QPropertyAnimation anim(&mover, "mousePosition");
QTimer::singleShot(0, [&]() {
auto startValue = view.visualRect(model.index(0, 0)).center();
auto endValue = view.visualRect(model.index(2, 0)).center();
QTest::mousePress(view.viewport(), Qt::MouseButton::LeftButton, Qt::KeyboardModifier::NoModifier, startValue);
anim.setStartValue(startValue);
anim.setEndValue(endValue);
anim.setDuration(500);
anim.start();
});
qApp->exec();
}
MouseMover.h
#pragma once
#include <QObject>
#include <QTest>
class MouseMover : public QObject {
Q_OBJECT
public:
Q_PROPERTY(QPoint mousePosition READ mousePosition WRITE setMousePosition MEMBER mMousePosition)
void setWidget(QWidget* widget) {
mWidget = widget;
}
QPoint mousePosition() const {
return mMousePosition;
}
void setMousePosition(const QPoint& pos) {
mMousePosition = pos;
if (mWidget) {
QTest::mousePress(mWidget, Qt::MouseButton::LeftButton, Qt::KeyboardModifier::NoModifier, mMousePosition);
QTest::mouseMove(mWidget, mMousePosition);
}
}
private:
QPoint mMousePosition;
QWidget* mWidget{ nullptr };
};
MouseMover.cpp
#include "MouseMover.h"

mouseMoveEvent() handler starts and blocks at QDrag.exec(). In order to simulate the drop, you have to schedule it e.g. with QTimer:
def complete_qdrag_exec():
QTest.mouseMove(drop_target)
QTest.qWait(50)
QTest.mouseClick(drop_target, Qt.MouseButton.LeftButton)
QTimer.singleShot(1000, complete_qdrag_exec)
QTest.mousePress(drag_source, Qt.MouseButton.LeftButton, Qt.KeyboardModifier.NoModifier, QPoint(10, 10))
QTest.mouseMove(drag_source, QPoint(50, 50)) # Ensure distance sufficient for DND start threshold
Above is plain Python (PyQt6/PySide6). I assume it's similar for C++ ...

Related

Extend mousePressEvent() for dragging but retain previous behaviour for clicking

I'm trying to add the Drag and Drop functionality to a QListWidget. The first milestone was reached with the following code.
//draglist.h
#include <QListWidget>
class SourceList : public QListWidget
{
public:
SourceList(QWidget * parent = 0);
protected:
void mousePressEvent(QMouseEvent *event);
};
//draglist.cpp
#include "draglists.h"
#include <QMouseEvent>
#include <QDebug>
#include <QMimeData>
#include <QDrag>
SourceList::SourceList(QWidget * parent)
:QListWidget(parent)
{
}
void SourceList::mousePressEvent(QMouseEvent *event)
{
//Data from the selected QListWidgetItem
QString itemData = currentItem()->data(Qt::DisplayRole).toString();
QMimeData *mimeData = new QMimeData;
mimeData->setText(itemData);
QDrag *drag = new QDrag(this);
drag->setMimeData(mimeData);
drag->exec();
}
It enabled the drag functionality for the list items, but they can't be clicked any more.
How to retain the functionality in which one left click selects the item in the list (this is the behaviour if mousePressEvent() isn't overridden) ?
Possible solution
Check the source code for the original QAbstractItemView::mousePressEvent(QMouseEvent *event) and recopy the needed code int the SourceList::mousePressEvent(QMouseEvent *event) implementation. I'm looking for an alternative to that, if existent.
To preserve the default functionality you can propagate the event up the the parent class:
void SourceList::mousePressEvent(QMouseEvent *event)
{
// Your handling of mouse event.
[..]
QListWidget::mousePressEvent(event);
}
If you need to handle the left mouse button press, you can use QMouseEvent::button() function and compare returning value with one or more Qt::MouseButton values. For example:
if (event->button() & Qt::LeftButton) {
// Handle the left mouse button click.
}

Qt : Child graphic items hover event

I have two items of type QGraphicsRectItem. One over the other.
The first one is a custom class called Wall. Inside the wall there are windows and doors.
In fact, i have a list of Doors and Windows inside this custom Wall class.
The Doors are Items too, and are drawn inside the wall.
When i move the mouse over the door, the hover function of the wall is emited, but the hover of the door is not. Both of them correctly copied one from each other as virtual void protected.
Why is that happening? How can i make the door and window items realize about the hover?.
I have tried with a custom QGraphicsItem instead of a custom QGraphicsRectItem. It seems the hover event handler is successfully called for both Wall and Door. This is happening when explicitly setting QGraphicsItem with setAcceptHoverEvents(true). This is not tested with custom QGraphicsRectItem.
#include <QApplication>
#include <QGraphicsScene>
#include <QGraphicsView>
#include <QGraphicsItem>
#include <QPainter>
class Item : public QGraphicsItem
{
QBrush m_brush;
public:
explicit Item(bool nested = true, QGraphicsItem* parent = 0) : QGraphicsItem(parent), m_brush(Qt::white)
{
if (nested) {
Item* item = new Item(false, this);
item->setPos(10,10);
m_brush = Qt::red;
}
setAcceptHoverEvents(true);
}
QRectF boundingRect() const
{
return QRectF(0,0,100,100);
}
void hoverEnterEvent(QGraphicsSceneHoverEvent *)
{
m_brush = Qt::red;
update();
}
void hoverLeaveEvent(QGraphicsSceneHoverEvent *)
{
m_brush = Qt::white;
update();
}
void paint(QPainter *p, const QStyleOptionGraphicsItem *, QWidget *)
{
p->setBrush(m_brush);
p->drawRoundRect(boundingRect());
}
};
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
QGraphicsScene scene;
scene.addItem(new Item);
QGraphicsView view;
view.setScene(&scene);
view.setMouseTracking(true);
view.show();
return app.exec();
}

connecting three comboboxes to control the options avaliable in qt

I have three comboboxes with three similar options, 'one', 'two', 'three', and I want to prevent the same option in different combobox.
Example:
combobox1 : 'one',
so when I go choose in combobox2 and combobox3 there's only 'two' and 'three' to choose from.
I know I could do this in a combination of for loop and if, but can anyone help me with some trick I could use here?
Write your own class MyComboBox which is derived from QComboBox.
Implement a slot in MyComboBox that would do something like this:
void excludeIndex(const QString & text)
{
// Get the list of all the options for the ComboBox
QStringList list = getIndices();
// Remove one (the only) item 'text' from the list
list.removeOne( text );
// Clear the ComboBox and fill with the new values
this->clear();
this->addItems( list );
}
In your parent widget/application, connect the signal void currentIndexChanged(const QString & text) from each 'sending' MyComboBox or QComboBox to this slot of the 'receiving' MyComboBox.
If you need to exclude the values from multiple other ComboBoxes, then it might be better to implement the slot in the parent Widget. That way, you can loop over all the 'receiving' ComboBoxes. Per ComboBox you will read all the current values of the other ComboBoxes and remove those values from list. Your slot in the parent Widget will not need an input QString anymore.
How about using only one combobox? There are only six possible options:
one two three
one three two
two one three
two three one
three one two
three two one
It would be much easier for the user to use only one combobox instead of using three comboboxes whose available options are continuously changing.
Here is one solution.
Basically it initializes all of the boxes with all the items, and as a box's current text is changed, it removes that text from the other boxes. If the previous text that the box contained wasn't blank, it adds it back into all the other boxes.
If you care to test it with more than 8, change the variable m_numOfBoxes in mainwindow.cpp to some other value.
main.cpp
#include <QApplication>
#include "mainwindow.h"
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
MainWindow w;
w.show();
return a.exec();
}
mainwindow.h
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
#include <QComboBox>
#include <QList>
#include <QStringList>
#include <QMap>
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
MainWindow(QWidget *parent = 0);
~MainWindow();
public slots:
void on_comboBox_changed();
private:
QList <QComboBox *> m_boxes;
QMap <QComboBox *, QString> m_previousText;
int m_numOfBoxes;
QStringList m_allItems;
};
#endif // MAINWINDOW_H
mainwindow.cpp
#include "mainwindow.h"
#include <QVBoxLayout>
#include <QDebug>
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
{
QVBoxLayout * vbox = new QVBoxLayout();
m_numOfBoxes = 8;
m_allItems << "";
for(int i = 0; i< m_numOfBoxes; i++)
{
m_allItems << QString::number(i+1);
}
for(int i = 0; i< m_numOfBoxes; i++)
{
QComboBox * temp = new QComboBox;
QObject::connect(temp, SIGNAL(currentIndexChanged(int)), this, SLOT(on_comboBox_changed()));
vbox->addWidget(temp);
temp->addItems(m_allItems);
m_boxes.append(temp);
m_previousText[temp] = "";
}
this->setCentralWidget(new QWidget());
this->centralWidget()->setLayout(vbox);
}
MainWindow::~MainWindow()
{
}
void MainWindow::on_comboBox_changed()
{
QComboBox * editedBox = qobject_cast <QComboBox *> (QObject::sender());
QString currentText = editedBox->currentText();
if(currentText == m_previousText[editedBox])
{
return;
}
foreach(QComboBox * box, m_boxes)
{
if(box == editedBox)
{
continue;
}
if(currentText != "")
{
// remove the current text from the other boxes
int index = box->findText(currentText);
if(index != -1)
{
box->removeItem(index);
}
}
if(m_previousText[editedBox] != "")
{
// add the previous text back into the lists for the other boxes
box->addItem(m_previousText[editedBox]);
qDebug() << "Adding back" << m_previousText[editedBox];
}
}
m_previousText[editedBox] = currentText;
}
Note that when an item is added back into a box's list, it just gets tacked onto the end of the list, so over time the order of your items may get scrambled.
Hope that helps.

How to remove an Item from a List View?

I have a QLineEdit, 2 QPushButtons (Add & Remove Buttons) and a QListView.
I want to add the QLineEdit text to the QListView when the add button is clicked. Same way, I have to delete an item from the QListView if the remove button is clicked.
I'm using a QStringListModel to add the QLineEdit text to the QListView. But I don't know how to delete the QListView item. How can I do this? Plz Help.. Thanks in Advance.
#ifndef EXAMPLE_H
#define EXAMPLE_H
#include <QWidget>
#include <QStringList>
#include <QStringListModel>
class EXAMPLE : public QWidget
{
Q_OBJECT
public:
explicit EXAMPLE(QWidget *parent = 0);
~EXAMPLE();
private slots:
void on_addButton_released();
void on_removeButon_released();
private:
Ui::EXAMPLE *ui;
QStringList stringList;
};
#endif // EXAMPLE_H
EXAMPLE.CPP
#include "EXAMPLE.h"
#include <QStringListModel>
EXAMPLE::EXAMPLE(QWidget *parent) :
QWidget(parent),
ui(new Ui::EXAMPLE)
{
ui->setupUi(this);
ui->listView->setModel(new QStringListModel(stringList));
}
EXAMPLE::~EXAMPLE()
{
delete ui;
}
void EXAMPLE::on_addButton_released()
{
stringList.append(ui->lineEdit->text());
((QStringListModel*) ui->listView->model())->setStringList(stringList);
ui->lineEdit->clear();
}
void EXAMPLE::on_removeButon_released()
{
}
If your list view is only using single selection, the following will work:
void EXAMPLE::on_removeButton_released()
{
QModelIndexList selected = ui->listView->selectionModel()->selectedIndexes();
if (!selected.isEmpty())
{
stringList.removeAt(selected.first().row()-i);
((QStringListModel*) ui->listView->model())->setStringList(stringList);
}
}
If your list view allows for multiple items to be selected, then you'll need something slightly more complex:
void EXAMPLE::on_removeButton_released()
{
QModelIndexList selected = ui->listView->selectionModel()->selectedIndexes();
if (!selected.isEmpty())
{
qSort(selected);
for (int i=0; i<selected.count(); ++i)
{
stringList.removeAt(selected.at(i).row()-i);
}
((QStringListModel*) ui->listView->model())->setStringList(stringList);
}
}
There is also a means of removing the items directly from the item model (as opposed to removing it from the string list and then setting the string list on the model). Refer to the documentation for QAbstractItemModel::removeRow. If you go down this road, your method of adding items will also need to change.

Qt - several QTextBlock inline

Is it possible to arrange several QTextBlocks in QTextDocument in one horizontal line?
I need to know which block of text was clicked and QTextBlock would be nice to use due to its method setUserState(int), which can be used to hold id of particular block. Are there better approaches?
Not sure if I got your question right, but I am taking a shot at it (some three years after the question was asked.....)
In principle you can put QTextBlocks in a horizontal line using a QTextTable. If you then create a class which inherits from QTextEdit you can catch the mouse events and find out which text block was clicked.
I post some code below where I have a very simply dialog that only has a textedit in it (of the derived class mentioned above). I create a table laying out three text blocks in a horizontal line and set their user state to the column number. Then I have the text edit class only with the overloaded mouseEvent method which only prints the userState of whatever text block it is in to the command line, just to show the principle.
Let me know if this is of any help or if misunderstood your question.
dialog.h
#ifndef MYDIALOG_H
#define MYDIALOG_H
#include "ui_dialog.h"
class MyDialog : public QDialog, public Ui::Dialog
{
public:
MyDialog(QWidget * parent = 0, Qt::WindowFlags f = 0);
void createTable();
};
#endif
dialog.cpp
#include "dialog.h"
#include <QTextTable>
#include <QTextTableFormat>
MyDialog::MyDialog(QWidget * parent, Qt::WindowFlags f) :
QDialog(parent,f)
{
setupUi(this);
}
void MyDialog::createTable()
{
QTextCursor cursor = textEdit->textCursor();
QTextTableFormat tableFormat;
tableFormat.setCellPadding(40);
tableFormat.setBorderStyle(QTextFrameFormat::BorderStyle_None);
QTextTable* table=cursor.insertTable(1,3,tableFormat);
for( int col = 0; col < table->columns(); ++col ) {
cursor = table->cellAt(0, col).firstCursorPosition();
cursor.insertBlock();
cursor.block().setUserState(col);
cursor.insertText(QString("Block in Column ")+QString::number(col));
}
}
mytextedit.h
#ifndef MYTEXTEDIT_H
#define MYTEXTEDIT_H
#include <QTextEdit>
class MyTextEdit : public QTextEdit
{
public:
MyTextEdit(QWidget * parent = 0);
void mousePressEvent(QMouseEvent *event);
};
#endif
mytextedit.cpp
#include "mytextedit.h"
#include <QMouseEvent>
#include <QTextBlock>
#include <QtCore>
MyTextEdit::MyTextEdit(QWidget * parent) :
QTextEdit(parent)
{
}
void MyTextEdit::mousePressEvent(QMouseEvent *event)
{
if(event->button() == Qt::LeftButton) {
qDebug() << this->cursorForPosition(event->pos()).block().userState();
}
}
main.cpp (just for completeness)
#include <QApplication>
#include "dialog.h"
int main(int argc, char** argv)
{
QApplication app(argc,argv);
MyDialog dialog;
dialog.show();
dialog.createTable();
return app.exec();
}

Resources