I'm trying to create a layer system like most photo editor programs (Photoshop) and I'm basically drawing a single QGraphicsPixmapItem using QGraphicsPixmapItem::setPixmap(QPixmap *image); on QGraphicsScene. How could I do this but instead I can add many QPixmaps and remove them at will. I tried creating a list of QPixmaps and one of QGraphicsPixmapItems but it gets messy if I remove or rearrange the order of my QPixmaps is there a better way to do this?
QTimer *timer = new QTimer(this);
connect(timer, SIGNAL(timeout()), this, SLOT(draw())); //calls the function below to redraw sc
timer->start(30);
This updates the GraphicsScene every 30ms so any drawing I do on the pixmap *image gets drawn but now I want to get a list a QPixmap and add them to the scene everytime draw() is called but the problem is I need a list of QGraphicsPixmapItems and if I delete a layer or move the order of them I want the associated QGraphicsPixmapItem to also be removed/moved. I guess I can do this but it seems very complicated so any advice?
void PaintArea::draw()
{
m_item->setPixmap(*image); //Do this but call layerManager.getListOfLayers() and add each to the scene
}
The following small example app shows how you might proceed. Basically, there are two models and two views. The models are the QGraphicsScene and a standard QStandardItemModel, whereas the views are a QListView and a QGraphicsView.
The main task is to keep both models in sync, by using signal and slots.
The model can be modified with the Add button and the context menu. For this small app one can only add, remove and change the picture of your pixmap. It is really simple to add other actions like moving the items with drag and drop and also to hide/visible them using a checkable action and other custom user role.
#include <QApplication>
#include <QFileDialog>
#include <QGraphicsPixmapItem>
#include <QGraphicsView>
#include <QHBoxLayout>
#include <QListView>
#include <QMenu>
#include <QPushButton>
#include <QStandardItemModel>
int main(int argc, char* argv[]) {
QApplication app(argc, argv);
auto frame = new QFrame;
frame->setLayout(new QHBoxLayout);
auto listView = new QListView;
frame->layout()->addWidget(listView);
auto graphicsView = new QGraphicsView;
frame->layout()->addWidget(graphicsView);
auto graphicsScene = new QGraphicsScene;
graphicsView->setScene(graphicsScene);
auto myModel = new QStandardItemModel;
auto btnAdd = new QPushButton("Add");
frame->layout()->addWidget(btnAdd);
QObject::connect(btnAdd, &QPushButton::clicked, [&]() {
auto item = new QStandardItem("Pixmap");
item->setData(QString("./data/test.png"), Qt::ItemDataRole::UserRole + 1);
myModel->appendRow(item);
});
listView->setContextMenuPolicy(Qt::ContextMenuPolicy::CustomContextMenu);
QObject::connect(listView, &QListView::customContextMenuRequested, [&](const QPoint& pos) {
auto index = listView->indexAt(pos);
QMenu menu;
auto remove = menu.addAction("Remove", [&]() {
myModel->removeRow(index.row(), index.parent());
});
if (!index.isValid()) remove->setEnabled(false);
auto changeImage = menu.addAction("Change...", [&]() {
auto file=QFileDialog::getOpenFileName(frame, "Select PNG file", "./data/", "(*.png)");
if (file.isEmpty()) return;
myModel->setData(index, file, Qt::ItemDataRole::UserRole + 1);
});
if (!index.isValid()) changeImage->setEnabled(false);
menu.exec(listView->mapToGlobal(pos));
});
QObject::connect(myModel, &QStandardItemModel::dataChanged, [&](const QModelIndex& topLeft, const QModelIndex& bottomRight, const QVector<int>& roles = QVector<int>()) {
if (auto item = myModel->itemFromIndex(topLeft)) {
if (auto pixItem = dynamic_cast<QGraphicsPixmapItem*>(graphicsScene->items()[topLeft.row()])) {
pixItem->setPixmap(QPixmap(item->data(Qt::ItemDataRole::UserRole + 1).toString()));
}
}
});
QObject::connect(myModel, &QStandardItemModel::rowsInserted, [&](const QModelIndex& parent, int first, int last) {
for (auto iter = first; iter <= last; iter++) {
auto index=myModel->index(iter, 0, parent);
auto pixmap=myModel->data(index, Qt::ItemDataRole::UserRole + 1).toString();;
auto item=graphicsScene->addPixmap(QPixmap(pixmap));
}
});
QObject::connect(myModel, &QStandardItemModel::rowsRemoved, [&](const QModelIndex& parent, int first, int last) {
auto items = graphicsScene->items();
for (auto iter = first; iter <= last; iter++) {
graphicsScene->removeItem(items[iter]);
}
});
listView->setModel(myModel);
frame->show();
return app.exec();
}
Header file:
...
QGraphicsScene *scene; QGraphicsItemGroup *itemGroup;
...
.cpp file:
void PaintArea::draw()
{
m_item->setPixmap(*image); m_item->setGroup(itemGroup); // Layers-related code
}
void PaintArea::deleteGroup(QGraphicsItemGroup *group)
{
scene->destroyItemGroup(group); // Layers-related code
}
Related
I have run into a very interesting problem. I have several widgets (they are QPushButton but in general they can be any QWidget), they are placed into an arbitrary hierarchy of layouts in a window (i.e. they are not necessarily in the same layout). But I want them all to keep the same width which should correspond to the maximum width of any of them. The width is of course determined by their content, which in case of QPushButton is the text of the button).
I created this code:
#include <QApplication>
#include <QGridLayout>
#include <QPointer>
#include <QPushButton>
#include <QVector>
#include <QWidget>
class WidthEqualizer : public QObject
{
public:
explicit WidthEqualizer(QObject *parent = nullptr) :
QObject(parent)
{
}
void addWidget(QWidget *widget)
{
m_widgets.append(widget);
}
int widthHint() const
{
// A hack: Recursion protection should prevent endless recursion
// when calling size hint from WidthEqualizer for its widgets.
m_recursionProtection = true;
// We get the maximum width of size hint of all widgets.
int width = 0;
for (QWidget *widget : qAsConst(m_widgets))
{
if (widget != nullptr)
{
width = qMax(width, widget->sizeHint().width());
}
}
m_recursionProtection = false;
return width;
}
bool recursionProtection() const
{
return m_recursionProtection;
}
private:
QVector<QPointer<QWidget>> m_widgets;
mutable bool m_recursionProtection = false;
};
class PushButton : public QPushButton
{
public:
explicit PushButton(const QString &text = {}, QWidget *parent = nullptr) :
QPushButton(text, parent)
{
setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
}
void setWidthEqualizer(WidthEqualizer* equalizer)
{
m_equalizer = equalizer;
m_equalizer->addWidget(this);
}
QSize sizeHint() const
{
if (m_equalizer != nullptr && !m_equalizer->recursionProtection())
{
int width = m_equalizer->widthHint();
int height = QPushButton::sizeHint().height();
return QSize(width, height);
}
return QPushButton::sizeHint();
}
private:
QPointer<WidthEqualizer> m_equalizer;
};
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
QWidget window;
auto equalizer1 = new WidthEqualizer(&window);
auto equalizer2 = new WidthEqualizer(&window);
auto layout = new QGridLayout();
window.setLayout(layout);
auto button1 = new PushButton("EQ1 very very very long");
button1->setWidthEqualizer(equalizer1);
layout->addWidget(button1, 0, 0);
auto button2 = new PushButton("EQ2");
button2->setWidthEqualizer(equalizer2);
layout->addWidget(button2, 0, 1);
auto button3 = new PushButton("EQ1");
button3->setWidthEqualizer(equalizer1);
layout->addWidget(button3, 1, 1);
auto button4 = new PushButton("EQ2 long");
button4->setWidthEqualizer(equalizer2);
layout->addWidget(button4, 1, 0);
window.show();
return a.exec();
}
It shows this situation:
This is exactly what I wanted to achieve. There are two groups of buttons "EQ1" and "EQ2". Note that the buttons in each group have equal width, which is given by the width of the button with the longer text. For demonstration purposes I put the buttons into a grid but this can be any possible hierarchy of layouts. And even if I change the text on any of them the correct widths will be maintained automatically.
My question is this: The code works but I find this code too complex for such a simple task. Does not Qt provide any simple trick to achieve the same without so much coding? Ideally make it even more general without the need to override the push button or any other type of widget which I want to equalize widths for? Maybe some trick with layouts? Or maybe some clever metaprogramming?
UPDATE:
Why I need this? I want my UI to be clean and elegant, for example to have a window/dialog which has equal width of buttons at the top and also at the bottom. See e.g.
It looks much better than if each button has a different width. Of course I cannot set the width to be fixed, because this would get broken when I applied translations.
On a subclassed QListWidget I have several items. Every QListWidget item (e.g. "ROS Init", "Images" etc) that is shown below is associated with a specific icon.
The problem I have is that I am trying to drag and drop the specific icon corresponding to that QListWidget item, but nothing happens.
Below the function responsible for the dragging:
void ListView::startDrag(Qt::DropActions supportedActions)
{
QMap<int, QString> icons;
icons.insert(IT_RosInit, "ROS Init");
icons.insert(IT_Images, "Images");
icons.insert(IT_Path, "Path");
icons.insert(IT_RosShutDown, "ROS Shutdown");
if (supportedActions & Qt::CopyAction)
{
const QList<QListWidgetItem *> &m_items(selectedItems());
if (m_items.isEmpty())
return;
QPixmap pixmapLaser("/home/images/laserscan.png");
QPixmap pixmapPCloud2("/home/images/pcloud2.png");
// etc ...
QStringList iconImages;
for(int i = 0; i < icons.count(); ++i)
{
for (const QString &tableType : iconImages) {
if (tableType == "ROS Init")
{
auto *data = mimeData(m_items);
auto *drag = new QDrag(this);
drag->setPixmap(pixmapLaser);
drag->setMimeData(data);
drag->setHotSpot(pixmapLaser.rect().center());
drag->exec(Qt::CopyAction);
}
else if(tableType == "Images")
{
auto *data2 = mimeData(m_items);
auto *drag2 = new QDrag(this);
drag2->setPixmap(pixmapPCloud2);
drag2->setMimeData(data2);
drag2->setHotSpot(pixmapPCloud2.rect().center());
drag2->exec(Qt::CopyAction);
}
}
}
}
else
{
QListWidget::startDrag(supportedActions);
}
}
After subclassing the QListWidget I just reimplemented the usual drag and drop function. All other function are working properly but the startDrag and in fact as I try to drag the proper QPixmap, I actually see nothing being dragged.
I consulted this source, useful, and also this other source which was useful but it didn't reimplement the startDrag but instead dropEvent which for me is not a problem because that is working well.
I also consulted this source and this other source but that also didn't help fixing the problem.
Thanks for shedding light on this matter for solving the problem
Solution
I would approach this problem in the following way:
Set the ItemIsDragEnabled flag of QListWidgetItem to enable the item for dragging:
item->setFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsDragEnabled);
Set the desired data for each item:
item->setData(Qt::UserRole, type);
where type is one of the enumerated values IT_RosInit, IT_Images, etc.
Enable the drag functionality of the ListWidget:
setDragEnabled(true);
setDragDropMode(QAbstractItemView::DragOnly);
Use the item settings to setup the QDrag object, created in startDrag.
Example
Here is an example I have prepared for you to demonstrate how the proposed solution could be implemented:
MainWindow.cpp
MainWindow::MainWindow(QWidget *parent) :
QWidget(parent)
{
auto *l = new QVBoxLayout(this);
auto *list = new ListView(this);
list->addItem(createItem(":/pix/images/laserscan.png", tr("RosInit"), IT_RosInit));
list->addItem(createItem(":/pix/images/icons/pcloud2.png", tr("Images"), IT_Images));
list->addItem(createItem(":/pix/images/icons/some_icon.png", tr("Path"), IT_Path));
list->addItem(createItem(":/pix/images/icons/another_icon.png", tr("RosShutDown"), IT_RosShutDown));
l->addWidget(list);
resize(300, 400);
setWindowTitle("IconDrag");
}
QListWidgetItem *MainWindow::createItem(const QString &pm, const QString &text, int type)
{
auto *item = new QListWidgetItem(QIcon(QPixmap(pm)), text);
item->setFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsDragEnabled);
item->setData(Qt::UserRole, type);
return item;
}
ListView.cpp
ListView::ListView(QWidget *parent) :
QListWidget(parent)
{
setDragEnabled(true);
setDragDropMode(QAbstractItemView::DragOnly);
setSelectionBehavior(QAbstractItemView::SelectRows);
setSelectionMode(QAbstractItemView::SingleSelection);
}
void ListView::startDrag(Qt::DropActions supportedActions)
{
if (supportedActions & Qt::CopyAction) {
const QList<QListWidgetItem *> &items(selectedItems());
if (items.isEmpty())
return;
const QPixmap &pm(items.first()->icon().pixmap(64));
auto *item = items.first();
auto *mimeData = new QMimeData();
auto *drag = new QDrag(this);
mimeData->setData("text/plain", item->data(Qt::UserRole).toByteArray());
drag->setPixmap(pm);
drag->setMimeData(mimeData);
drag->setHotSpot(pm.rect().center());
drag->exec(Qt::CopyAction);
}
}
I have a custom simple widget inheriting from QWidget and I add it to a QListWidget like this :
void MainWindow::AddToWidgetList(const QString &tag, const QString &html)
{
HtmlItem *html_item = new HtmlItem();
html_item->set_tag(tag);
html_item->set_html(html);
connect(html_item, SIGNAL(RemoveIt(uintptr_t)), this, SLOT(on_RmBtn_clicked(uintptr_t)));
QListWidgetItem *list_item = new QListWidgetItem();
html_item->set_list_item(list_item);
list_item->setSizeHint(html_item->sizeHint());
ui->CodeBlocks->addItem(list_item);
ui->CodeBlocks->setItemWidget(list_item, html_item);
}
I then want to move the selected element up when a button is pressed
void MainWindow::on_UpArrowBtn_clicked()
{
if (ui->CodeBlocks->count() < 2)
return;
int current_row = ui->CodeBlocks->currentRow();
if (current_row == 0)
return;
HtmlItem *item_widget = (HtmlItem*)ui->CodeBlocks->itemWidget(ui->CodeBlocks->item(current_row));
QListWidgetItem *item = ui->CodeBlocks->takeItem(current_row);
ui->CodeBlocks->insertItem(current_row - 1, item);
ui->CodeBlocks->setItemWidget(item, item_widget);
}
but I get crash in this line :
ui->CodeBlocks->setItemWidget(item, item_widget);
The following example shows what is happening. Basically, the rules are like this:
Calling setItemWidget transfers the ownership of the Item-Widget to the QListWidget instance. Hence, it is QListWidget's responsibility to destroy the set Item-Widget.
Now, QListWidget has no member, which allows to withdraw the ownership of a set Item-Widget. The only option one has is to create a new Item-Widget with the same properties like Item-Widget about to removed.
Note, that the Item-Widget ist deleted later after returning to the event loop, which happens by calling deleteLater() inside of takeItem. Hence, it is valid to access label till the end of the slot.
If you are not happy with this behavior, you are still able to switch to a QListView class with your own delegate. Albeit this seems to be more work, it is the more extensible approach.
#include <QApplication>
#include <QHBoxLayout>
#include <QPushButton>
#include <QListWidget>
#include <QLabel>
#include <QDebug>
int main(int argc, char** args) {
QApplication app(argc, args);
auto frame = new QFrame;
auto listWidget = new QListWidget;
for (auto iter=0; iter<10; iter++)
{
auto label = new QLabel(QString("Item-%1").arg(iter));
auto item = new QListWidgetItem();
listWidget->addItem(item);
listWidget->setItemWidget(item, label); // listWidget becomes the owner of label
}
auto moveUp = new QPushButton("Move Up");
frame->setLayout(new QHBoxLayout);
frame->layout()->addWidget(listWidget);
frame->layout()->addWidget(moveUp);
frame->show();
QObject::connect(moveUp, &QPushButton::clicked, [&]()
{
auto row = listWidget->currentRow();
auto item=listWidget->currentItem();
if (!item) return;
if (row == 0) return;
auto label = qobject_cast<QLabel*>(listWidget->itemWidget(item));
if (!label) return;
QObject::connect(label, &QLabel::destroyed, []()
{
qDebug() << "Destroyed"; // takeItem calls deleteLater on itemWidget
});
auto myItem=listWidget->takeItem(row);
listWidget->insertItem(row-1,myItem);
listWidget->setItemWidget(item, new QLabel(label->text())); // copy content of itemWidget and create new widget
listWidget->setCurrentRow(row-1);
});
app.exec();
}
I'm trying to generate a right-click menu on a QTabWidget (lists) containing only QListWidgets. I get a menu below where I click the distance of the tab bar's height, which is expected because the context menu is applied to the QTabWidget.
void onCustomContextMenuRequested(const QPoint& pos) {
QListWidgetItem * item = ((QListWidget*)(lists->currentWidget()))->itemAt(pos);
if (item) showContextMenu(item, QListWidget(lists->currentWidget()).viewport()->mapToGlobal(pos));
}
void showContextMenu(QListWidgetItem* item, const QPoint& globalPos) {
QMenu menu;
menu.addAction(item->text());
menu.exec(globalPos);
}
I can get the menu to appear at the mouse, while still referring to an item about 100px beneath it, by changing
QListWidget(lists->currentWidget()).viewport()->mapToGlobal(pos));
to
QListWidget(lists->currentWidget()).viewport()->mapToParent(mapToGlobal(pos)));
But I can't get the menu to refer to the item I am clicking on. I have tried transforming to and from parent coordinates to no effect.
QPoint pos_temp = ((QListWidget*)(lists->currentWidget()))->viewport()->mapFromParent(pos);
if (item) showContextMenu(item, QListWidget(lists->currentWidget()).viewport()->mapToGlobal(pos_temp));
I have also tried to and from global coordinate, and combinations of global and parent, to undesirable effect.
So how can I get the right click menu to refer to the item I am clicking on?
The position sent by the customContextMenuRequested signal is with respect to the widget where the connection is established, and in this case I am assuming that it is the main widget so when using itemAt() of QListWidget it throws inadequate values since this method waits for the position with respect to the viewport(). The approach in these cases is to convert that local position to a global one and then map that global to a local position of the final widget.
In the next part I show an example.
#include <QApplication>
#include <QTabWidget>
#include <QListWidget>
#include <QVBoxLayout>
#include <QMenu>
class Widget: public QWidget{
Q_OBJECT
QTabWidget *lists;
public:
Widget(QWidget *parent=Q_NULLPTR):QWidget(parent){
lists = new QTabWidget;
setContextMenuPolicy(Qt::CustomContextMenu);
connect(this, &Widget::customContextMenuRequested, this, &Widget::onCustomContextMenuRequested);
auto layout = new QVBoxLayout(this);
layout->addWidget(lists);
for(int i=0; i<4; i++){
auto list = new QListWidget;
lists->addTab(list, QString("tab-%1").arg(i));
for(int j=0; j<10; j++){
list->addItem(QString("item %1-%2").arg(i).arg(j));
}
}
}
private slots:
void onCustomContextMenuRequested(const QPoint& pos){
QPoint globalPos = mapToGlobal(pos);
QListWidget *list = static_cast<QListWidget *>(lists->currentWidget());
if(list){
QPoint p = list->viewport()->mapFromGlobal(globalPos);
QListWidgetItem *item = list->itemAt(p);
if(item)
showContextMenu(item, globalPos);
}
}
void showContextMenu(QListWidgetItem* item, const QPoint& globalPos) {
QMenu menu;
menu.addAction(item->text());
menu.exec(globalPos);
}
};
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
Widget w;
w.show();
return a.exec();
}
#include "main.moc"
In the following link is the complete example.
I have a tree like this:
|-Parent
| |-Child-Child
|-Parent
| |-Child-Child
...
Only the Parents are selectable. How can I get the data from the selected Parent?
I tried
ui->treeView->selectedIndexes()[0];
but it says that selectedIndexes() is protected.
You need to call QItemSelectionModel::selectedIndexes() instead, i.e.:
QModelIndexList indexes = ui->treeView->selectionModel()->selectedIndexes();
if (indexes.size() > 0) {
QModelIndex selectedIndex = indexes.at(0);
[..]
}
How to get the selected item in a QTreeView?
The question is simple and the answers are terrible, and the tutorial worse.
Below is a fully functional example which shows how to get the selected item. Specifically selected_item()
#include <QApplication>
#include <QTreeView>
#include <QStandardItemModel>
#include <QItemSelectionModel>
#include <QGridLayout>
#include <iostream>
struct Node:public QStandardItem {
Node(std::string name):QStandardItem(name.c_str()){}
virtual void operator()(){
std::cout<<"selected node named: "<<text().toStdString()<<std::endl;
}
};
class TreeView :public QWidget{
Q_OBJECT
public:
QTreeView tree;
using Model=QStandardItemModel;
Model* item_model(){ return (Model*)tree.model(); }
Node* selected_item() {
QModelIndex index = tree.currentIndex();
if(!index.isValid()) return nullptr; // if the user has selected nothing
return (Node*)(item_model()->itemFromIndex(index));
}
TreeView() {
// automatically sets to parent
auto layout=new QGridLayout(this);
layout->addWidget(&tree,0,0);
// set the item model, there is no sane choice but StandardItemModel
tree.setModel(new Model());
connect(tree.selectionModel(),
&QItemSelectionModel::selectionChanged,
this,
&TreeView::selected);
// create a small tree
auto top=new Node("top");
auto a=new Node("a");
a->appendRow(new Node("a0"));
a->appendRow(new Node("a1"));
auto b=new Node("b");
top->appendRow(a);
top->appendRow(b);
// add it to the treeview root
item_model()->invisibleRootItem()->appendRow(top);
}
private slots:
void selected(
const QItemSelection &news, // not used
const QItemSelection &olds)
{
auto* node=selected_item();
if(node) (*node)();
}
};
int main(int argc, char** argv){
QApplication a(argc, argv);
TreeView w;
w.show();
return a.exec();
}