QTreeWidgetItem issue: Items set using setWidgetItem are dispearring after moving - qt

I have a bunch of QTreeWidgetItems that have embedded widgets in them that i set using the setItemWidget() function in the QTreeWidgetItem class.
Problem is anytime I move QTreeWidgetItem using drag/drop or any other means the embedded widget I set before disappears. I've seen around various forums that others have had this same problem (see link below)
http://www.qtcentre.org/threads/40500-QTreeWidget-setItemWidget%28%29-item-disappears-after-moving-item
Any possible solutions?

The problem is caused by QTreeWidget's implementation. When items are moved within the model, it deletes items at old positions and recreates them at new positions. We need to ensure 3 thinngs:
Rescue embedded widget from being deleted when its item is deleted.
Attach some information to items so we can track them and choose which widget belongs to an item.
Re-insert widget after item is moved.
Here is proof-of-concept implementation. Tree_widget_keeper_wrapper ensures 1st objective, setItemWidget's reimplementation ensures the 2nd one, and rows_inserted slot ensures the 3rd one. I tested that it works but it should be improved before using in real projects. Qt::UserRole should be changed to a configurable role. We should use role that is not used by the model itself. I put all implementation to class declaration to make it more readable but you should separate them in real code.
class Tree_widget_keeper_wrapper : public QWidget {
Q_OBJECT
public:
Tree_widget_keeper_wrapper(QWidget* child) {
_child = child;
QVBoxLayout* layout1 = new QVBoxLayout(this);
layout1->setContentsMargins(0, 0, 0, 0);
layout1->addWidget(_child);
}
~Tree_widget_keeper_wrapper() {
if (_child->parent() == this) {
_child->hide();
_child->setParent(0);
}
}
private:
QWidget* _child;
};
class Fixed_tree_widget : public QTreeWidget {
Q_OBJECT
public:
Fixed_tree_widget(QWidget* parent) : QTreeWidget(parent) {
connect(model(), SIGNAL(rowsInserted(QModelIndex,int,int)),
this, SLOT(rows_inserted(QModelIndex,int,int)));
}
void setItemWidget(QTreeWidgetItem* item, int column, QWidget* widget) {
QTreeWidget::setItemWidget(item, column, new Tree_widget_keeper_wrapper(widget));
item->setData(column, Qt::UserRole, all_widgets.count());
all_widgets << widget;
}
private:
QWidgetList all_widgets;
private slots:
void rows_inserted(QModelIndex parent, int start, int end) {
for(int column = 0; column < model()->columnCount(parent); column++) {
for(int row = start; row <= end; row++) {
QModelIndex index = model()->index(row, column, parent);
QVariant data = model()->data(index, Qt::UserRole);
if (data.type() == QVariant::Int) {
int i = data.toInt();
QTreeWidgetItem* item = itemFromIndex(index);
if (item && i >= 0 && i < all_widgets.count()) {
setItemWidget(item, column, all_widgets[i]);
all_widgets[i]->show();
}
}
}
}
}
};
I tested it against InternalMove mode and dragging items with mouse. Maybe in some other cases you will need to listen to other model's signals.

Related

QT Signals and Slots, dynamic menubar

I am new to qt and I want to know how to make a dynamic menu.
I did get it to make new submenus but I don't know how I can implement the "triggered() function" of these dynamic made submenus, so that I have access to what happens if I want to click on such a new submenu.
Here what I have so far (with: vector<QString> = vec; and some .ui Window named "New_Window")
in mainwindow.cpp
in some function:
QMenu *menu = this->menuBar()->addMenu("Chat Members");
for (int i = 0; i < vec.size(); ++i){
QString name = vec.at(i);
QAction *act = menu->addAction(name);
New_Window* new_window = new New_Window;
QObject::connect(act,SIGNAL(triggered()),
new_window,SLOT(actionReaction()));
}
here is an example of how a signal slot with a dynamic interface works ,
class A is created after starting the program, then the user clicks on a button from class A, for example, a class A is created many times and we need to determine from what object we get a signal to press the button, so
class A : public QMainWindow
{
Q_OBJECT
public:
A(QWidget *parent = nullptr);
~A();
void setID(const int id);
void getId() const;
signals:
void onButtonPress(int ID);
private:
int mID;
};
here we create a new class A and store it in the vector in such a way,
QVector<A*> mCreatingClassA;
void createNewClassA
{
QVector<A*> mCreatingClassA;
....
A* a = new A();
int id = // create your unique ID
a->setId(id);
connect(a,SIGNAL(onButtonPress(int)),this,SLOT(onyourSlot(int)));
mCreatingClassA.push_back(a);
....
}
detect the object from which the signal was received)
void onyourSlot(int ID)
{
for (int i = 0; i < mCreatingClassA.size(); ++i) {
if(mCreatingClassA[i]->getId()==ID)
{
mCreatingClassA[i] // received a signal from this object
}
}
}

How to create a Matrix like QWidget?

How to create a QTableWidget with some cells filled using uniform sized widgets and the unused cells filled with the size of widgets?The resulting table should contain a row/column as blank even if no widget is present.
Since the QTableWidget apparently has to contain something in each cell, and the default is an editable text box, it seem the solution would be to insert a non-editable dummy item into the blank cells. Or have a custom widget which can hide its contents, which is what I've done in the example below.
You specifically asked about fixed size... there's really a number of ways to go about this, depending on the exact needs. You can set fixed sizes on the whole table by using the horizontal and vertical headers and making them non-resizable. Another way is to call QTableView::setColumnWidth() / setRowHeight() for each row/column (while adding/item or whenever).
Here's an example using the former method (setting size on headers), and two ways to determine the actual size -- either a fixed size before building the table, or using a lookup on the custom widget item to determine a size.
UPDATE: Previous version used a dummy QTableWidgetItem to fill in the blank areas, but I think this is better if one needs to use a custom widget anyway. See edit history for previous version.
// main.cpp
#include <QtWidgets>
// A custom table widget item which contains a progress bar. The bar is
// hidden if the value is set to < 0.
class ProgressBarTwItem : public QWidget, public QTableWidgetItem
{
Q_OBJECT
public:
ProgressBarTwItem(QWidget *parent = nullptr, int value = -1) :
QWidget(parent),
QTableWidgetItem(QTableWidgetItem::UserType)
{
QHBoxLayout *l = new QHBoxLayout(this);
l->setContentsMargins(0,0,0,0);
m_pb = new QProgressBar(this);
// don't let PB size dictate our size
m_pb->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Ignored);
m_pb->setAlignment(Qt::AlignCenter);
m_pb->setMaximum(44);
l->addWidget(m_pb);
setValue(value);
}
QSize sizeHint() const override { return QSize(75, 25); }
public slots:
void setValue(int value = -1) const
{
m_pb->setVisible(value > -1);
if (value > -1)
m_pb->setValue(value);
}
private:
QProgressBar *m_pb;
};
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
a.setStyle("Fusion");
QDialog d;
d.setLayout(new QVBoxLayout);
QTableWidget *tw = new QTableWidget(5, 4, &d);
d.layout()->addWidget(tw);
// set to `true` to find the largest size from added custom widgets,
// otherwise use preset size
const bool useWidgetSize = true;
// This will be the fixed cell size, either preset or determined from custom widget.
// If looking up from widgets, this becomes the minimum size
// (eg. in case widgets didn't return valid sizes).
QSize cellSize = (useWidgetSize ? QSize(10, 10) : QSize(50, 15));
for (int r=0; r < tw->rowCount(); ++r) {
for (int c=0; c < tw->columnCount(); ++c) {
// make some widgets hidden and others display a progress value
const int val = (!((r*c) % (1+c)) ? -1 : (c+1 + r*10));
ProgressBarTwItem *item = new ProgressBarTwItem(tw, val);
tw->setCellWidget(r, c, item);
// tw->setItem(r, c, item); // needed? widget is shown anyway... docs not clear.
// Check the size.
if (useWidgetSize)
cellSize = cellSize.expandedTo(item->sizeHint());
}
}
// set fixed sizes for columns and rows on the horizontal and vertical headers
// respectively (this works even if they are hidden)
tw->horizontalHeader()->setDefaultSectionSize(cellSize.width());
tw->horizontalHeader()->setSectionResizeMode(QHeaderView::Fixed);
tw->verticalHeader()->setDefaultSectionSize(cellSize.height());
tw->verticalHeader()->setSectionResizeMode(QHeaderView::Fixed);
return d.exec();
}
#include "main.moc"

How do I implement the ability to edit a QTableWidget's vertical and horizontal header text in-line from the GUI instead of programmatically?

I have a QTableWidget that is displayed in the user interface that I can add and remove rows and columns using buttons. The problem is, when I add a row or column, I can change the data in the actual cells, but I cannot name the row or column. The name is simply a static number.
Is there a way to allow the user of my program to perhaps double-click on a row/column header and edit the name in-line or something similar?
Thanks.
As far as I know there is no built-in way to do this. However this can be implemented manually. The main idea of the following code is to detect double clicks on header items, place QLineEdit over them and save edited text once it loses focus. The example is based on Qt generated Designer Form Class with a table named ui->tableWidget which can be either QTableWidget or QTableView.
class MainWindow : public QMainWindow {
Q_OBJECT
public:
explicit MainWindow(QWidget *parent = 0);
~MainWindow();
private:
Ui::MainWindow *ui;
QLineEdit* header_editor;
int editor_index;
bool eventFilter(QObject*, QEvent*);
};
Source:
MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::MainWindow)
{
header_editor = 0;
ui->setupUi(this);
ui->tableWidget->horizontalHeader()->viewport()->installEventFilter(this);
ui->tableWidget->verticalHeader()->viewport()->installEventFilter(this);
}
MainWindow::~MainWindow() {
delete ui;
}
bool MainWindow::eventFilter(QObject* object, QEvent* event) {
if ((object == ui->tableWidget->horizontalHeader()->viewport() ||
object == ui->tableWidget->verticalHeader()->viewport()) &&
event->type() == QEvent::MouseButtonDblClick) {
if (header_editor) { //delete previous editor just in case
header_editor->deleteLater();
header_editor = 0;
}
QMouseEvent* e = static_cast<QMouseEvent*>(event);
QHeaderView* header = static_cast<QHeaderView*>(object->parent());
int mouse_pos = header->orientation() == Qt::Horizontal ? e->x() : e->y();
int logical_index = header->logicalIndex(header->visualIndexAt(mouse_pos));
if (logical_index >= 0) { // if mouse is over an item
QRect rect; // line edit rect in header's viewport's coordinates
if (header->orientation() == Qt::Horizontal) {
rect.setLeft(header->sectionPosition(logical_index));
rect.setWidth(header->sectionSize(logical_index));
rect.setTop(0);
rect.setHeight(header->height());
} else {
rect.setTop(header->sectionPosition(logical_index));
rect.setHeight(header->sectionSize(logical_index));
rect.setLeft(0);
rect.setWidth(header->width());
}
rect.adjust(1, 1, -1, -1);
header_editor = new QLineEdit(header->viewport());
header_editor->move(rect.topLeft());
header_editor->resize(rect.size());
header_editor->setFrame(false);
//get current item text
QString text = header->model()->
headerData(logical_index, header->orientation()).toString();
header_editor->setText(text);
header_editor->setFocus();
editor_index = logical_index; //save for future use
header_editor->installEventFilter(this); //catch focus out event
//if user presses Enter it should close editor
connect(header_editor, SIGNAL(returnPressed()),
ui->tableWidget, SLOT(setFocus()));
header_editor->show();
}
return true; // filter out event
} else if (object == header_editor && event->type() == QEvent::FocusOut) {
QHeaderView* header = static_cast<QHeaderView*>(
header_editor->parentWidget()->parentWidget());
//save item text
header->model()->setHeaderData(editor_index, header->orientation(),
header_editor->text());
header_editor->deleteLater(); //safely delete editor
header_editor = 0;
}
return false;
}
Downsides of this method are that it's hacky, things go bad when headers are resized or table is scrolled. It's just an example that can be improved.
I have a feeling there has to be a simpler way. But Qt headers ignore Qt::ItemIsEditable flag and can't use delegates.

QTreeView doesn't scale - is there a workaround to fetch fewer ModelIndexes?

I'm working on an app that uses Qt's QTreeView to display hierarchical data. In some cases a single leaf in the hierarchy will contain 100,000's of child nodes.
I've discovered that QTreeView can't handle too many child nodes.
This is because when the user opens a parent node, Qt fetches ALL of the child nodes' ModelIndexes, not just the ones that are needed to fill the QTreeView display.
Why does it do this, and is there any workaround to make it fetch fewer ModelIndexes?
My previous answer was unfortunately useless due to QTreeWidget(I am using QTreeWidget to simplyfy) sliders cannot be set by user.
Your goal could be achieved by using extra QScrollBar.
MainWidget.h
class MainWidget : public QWidget
{
Q_OBJECT
public:
MainWidget(QWidget *parent = 0);
private:
QTreeWidget* Tree;
QScrollBar* Bar;
private slots:
void tree(QTreeWidgetItem*);
void slider();
};
MainWidget.cpp
MainWidget::MainWidget(QWidget *parent): QWidget(parent) {
Tree = new QTreeWidget();
Tree->header()->hide();
connect(Tree,SIGNAL(itemExpanded(QTreeWidgetItem*)),this,SLOT(tree(QTreeWidgetItem*)));
Bar = new QScrollBar(Qt::Vertical);
Bar->setRange(0,100000);
connect(Bar,SIGNAL(sliderReleased()),this,SLOT(slider()));
for(uint i=0;i<10;i++) {
QTreeWidgetItem* Item=new QTreeWidgetItem(QStringList()<<QString::number(i)); //Add 10 topLevelItems
Tree->addTopLevelItem(Item);
for(uint j=0;j<30;j++) {
Item->addChild(new QTreeWidgetItem(QStringList()<<QString::number(j))); //Add first 30 items
}
}
QHBoxLayout* Lay = new QHBoxLayout();
Lay->addWidget(Tree);
Lay->addWidget(Bar);
setLayout(Lay);
}
void MainWidget::tree(QTreeWidgetItem* I) { //SLOT Only one item expanded
Tree->blockSignals(true); //Block SIGNAL(itemExpanded())
Tree->collapseAll();
Tree->expandItem(I);
Tree->blockSignals(false);//Allow SIGNAL(itemExpanded())
}
void MainWidget::slider() { //SLOT manage tree
for (int i=0;i<Tree->topLevelItemCount();i++) {
if (Tree->topLevelItem(i)->isExpanded()) {
for(uint j=Tree->topLevelItem(i)->childCount(); j>0;j--) { //Clear children
delete Tree->topLevelItem(i)->child(j-1);
}
uint Value = Bar->value();
for(uint j=0; j<30; j++) {
Tree->topLevelItem(i)->addChild(new QTreeWidgetItem(QStringList()<<QString::number(Value+j))); //Add 30 new children
}
break;
}
}
}
This way you have one slider to scroll the QTreeWidget and the second one to change content of expanded topLevelItem.

Need QGraphicsScene signal or event for _after_ change

I use QGraphicsScene of the Qt framework. Inside the scene I have some QGraphicsItems which the user can select and move.
I would like to have an info label where the current x and y coordinate of the currently moved selection (can consist of many items) is displayed.
I have tried with the signal changed of QGraphicsScene. But it is fired before the x() and y() property of the items is set to the new values. So the labels always show the second-to-last coordinates. If one moves the mouse slowly, the display is not very wrong. But with fast moves and sudden stops, the labels are wrong. I need a signal that is fired after the scene hast changed.
I have also tried to override the itemChange method of QGraphicsItem. But it is the same. It is fired before the change. (The new coordinates are inside the parameters of this method, but I need the new coordinates of all selected items at once)
I have also tried to override the mouseMove events of QGraphicsScene and of QGraphicsView but they, too, are before the new coordinates are set.
I did a test: I used a oneshot timer so that the labels are updated 100 ms after the signals. Then everything works fine. But a timer is no solution for me.
What can I do?
Make all items un-moveable and handle everything by my own?
QGraphicsItem::itemChange() is the correct approach, you were probably just checking the wrong flag. Something like this should work fine:
QVariant::myGraphicsItem( GraphicsItemChange change, const QVariant &value )
{
if( change == QGraphicsItem::ItemPositionHasChanged )
{
// ...
}
}
Note the use of QGraphicsItem::ItemPositionHasChanged rather than QGraphicsItem::ItemPositionChange, the former is called after the position changes rather than before.
The solution is to combine various things that you're already doing. Instrument itemChange, looking for and count the items with updated geometry. Once you've counted as many items as there are in the current selection, fire off a signal that will have everything ready for updating your status. Make sure you've set the QGraphicsItem::ItemSendsGeometryChanges flag on all your items!
This code was edited to remove the lag inherent in using a zero-timer approach. Below is a sscce that demonstrates it.
You create circles of random radius by clicking in the window. The selection is toggled with Ctrl-click or ⌘-click. When you move the items, a centroid diamond follows the centroid of the selected group. This gives a visual confirmation that the code does indeed work. When the selection is empty, the centroid is not displayed.
I've gratuitously added code to show how to leverage Qt's property system so that the items can be generic and leverage the notifier property of a scene if it has one. In its absence, the items simply don't notify, and that's it.
// https://github.com/KubaO/stackoverflown/tree/master/questions/scenemod-11232425
#include <QtWidgets>
const char kNotifier[] = "notifier";
class Notifier : public QObject
{
Q_OBJECT
int m_count = {};
public:
int count() const { return m_count; }
void inc() { m_count ++; }
void notify() { m_count = {}; emit notification(); }
Q_SIGNAL void notification();
};
typedef QPointer<Notifier> NotifierPointer;
Q_DECLARE_METATYPE(NotifierPointer)
template <typename T> class NotifyingItem : public T
{
protected:
QVariant itemChange(QGraphicsItem::GraphicsItemChange change, const QVariant &value) override {
QVariant v;
if (change == T::ItemPositionHasChanged &&
this->scene() &&
(v=this->scene()->property(kNotifier)).isValid())
{
auto notifier = v.value<NotifierPointer>();
notifier->inc();
if (notifier->count() >= this->scene()->selectedItems().count()) {
notifier->notify();
}
}
return T::itemChange(change, value);
}
};
// Note that all you need to make Circle a notifying item is to derive from
// NotifyingItem<basetype>.
class Circle : public NotifyingItem<QGraphicsEllipseItem>
{
QBrush m_brush;
public:
Circle(const QPointF & c) : m_brush(Qt::lightGray) {
const qreal r = 10.0 + (50.0*qrand())/RAND_MAX;
setRect({-r, -r, 2.0*r, 2.0*r});
setPos(c);
setFlags(QGraphicsItem::ItemIsMovable | QGraphicsItem::ItemIsSelectable |
QGraphicsItem::ItemSendsGeometryChanges);
setPen({Qt::red});
setBrush(m_brush);
}
};
class View : public QGraphicsView
{
Q_OBJECT
QGraphicsScene scene;
QGraphicsSimpleTextItem text;
QGraphicsRectItem centroid{-5, -5, 10, 10};
Notifier notifier;
int deltaCounter = {};
public:
explicit View(QWidget *parent = {});
protected:
Q_SLOT void gotUpdates();
void mousePressEvent(QMouseEvent *event) override;
};
View::View(QWidget *parent) : QGraphicsView(parent)
{
centroid.hide();
centroid.setRotation(45.0);
centroid.setPen({Qt::blue});
centroid.setZValue(2);
scene.addItem(&centroid);
text.setPos(5, 470);
text.setZValue(1);
scene.addItem(&text);
setRenderHint(QPainter::Antialiasing);
setScene(&scene);
setSceneRect(0,0,500,500);
scene.setProperty(kNotifier, QVariant::fromValue(NotifierPointer(&notifier)));
connect(&notifier, &Notifier::notification, this, &View::gotUpdates);
connect(&scene, &QGraphicsScene::selectionChanged, &notifier, &Notifier::notification);
}
void View::gotUpdates()
{
if (scene.selectedItems().isEmpty()) {
centroid.hide();
return;
}
centroid.show();
QPointF centroid;
qreal area = {};
for (auto item : scene.selectedItems()) {
const QRectF r = item->boundingRect();
const qreal a = r.width() * r.height();
centroid += item->pos() * a;
area += a;
}
if (area > 0) centroid /= area;
auto st = QStringLiteral("delta #%1 with %2 items, centroid at %3, %4")
.arg(deltaCounter++).arg(scene.selectedItems().count())
.arg(centroid.x(), 0, 'f', 1).arg(centroid.y(), 0, 'f', 1);
this->centroid.setPos(centroid);
text.setText(st);
}
void View::mousePressEvent(QMouseEvent *event)
{
const auto center = mapToScene(event->pos());
if (! scene.itemAt(center, {})) scene.addItem(new Circle{center});
QGraphicsView::mousePressEvent(event);
}
int main(int argc, char *argv[])
{
QApplication app{argc, argv};
View v;
v.show();
return app.exec();
}
#include "main.moc"

Resources