Image browser in Qt - c++

The code below displays thumbnails in a left pane. When a thumbnail is clicked, the full-size image appears in the right pane.
I have the impression that even though this code is rather brief, it is not the most natural way to do this task in Qt. Am I reinventing the wheel? Are there Model-View classes that are more suitable for this task?
// main.cpp
#include "PixmapPair.h"
#include <QLabel>
#include <QWidget>
#include <QApplication>
#include <QSplitter>
#include <QGridLayout>
int main(int argc, char* argv[])
{
QApplication app(argc, argv);
QSplitter* page = new QSplitter;
QGridLayout* gridLayout = new QGridLayout;
QWidget* leftPane = new QWidget(page);
leftPane->setLayout(gridLayout);
QLabel* rightPane = new QLabel(page);
PixmapPair pair1(":/images/ocean.jpg", gridLayout, rightPane);
PixmapPair pair2(":/images/forest.jpg", gridLayout, rightPane);
page->setWindowTitle("Images");
page->show();
return app.exec();
}
// PixmapPair.h
#include <QPixmap>
#include <QIcon>
#include <QLabel>
#include <QPushButton>
#include <QGridLayout>
class PixmapPair : public QObject
{
Q_OBJECT
public:
PixmapPair(QString file, QGridLayout * gridLayout, QLabel* rp)
: rightPane(rp), largePixmap(file)
{
smallPixmap = largePixmap.scaled(QSize(100,100), Qt::KeepAspectRatio, Qt::SmoothTransformation);
QPushButton* pushButton = new QPushButton;
pushButton->setIcon(QIcon(smallPixmap));
pushButton->setFlat(true);
pushButton->setIconSize(QSize(100,100));
QObject::connect(pushButton, SIGNAL(clicked()), SLOT(displayInRightPane()));
gridLayout->addWidget(pushButton);
}
public slots:
void displayInRightPane()
{
rightPane->setPixmap(largePixmap);
}
private:
QLabel* rightPane;
QPixmap largePixmap;
QPixmap smallPixmap;
};

The left part of the SplitView is basically a list presenting all the available pictures. Qt provides a way to handle this using the model/view pattern.
The class for showing a list is a QListView, it will do the job automatically based on a model given with the function setModel().
This function requires a QAbstractItemModel, since this class is a pure abstract one we will need to create a custom class deriving from it.
Inheriting from it will require a lot of glue code but thankfully Qt already provides a class that takes care of most of it when we want to represent a list, it is called QAbstractListModel.
So I created an ImageListModel like this :
///////////////////////
// imagelistmodel.h ///
#ifndef IMAGELISTMODEL_H
#define IMAGELISTMODEL_H
#include <QAbstractListModel>
#include <QPixmap>
struct PixmapPair
{
QString _file;
QPixmap _small;
QPixmap _large;
};
class ImageListModel : public QAbstractListModel
{
Q_OBJECT
public:
// QAbstractItemModel retrieves various information (like text, color, ...)
// from the same index using roles. We can define custom ones, however to
// avoid a clash with predefined roles, ours must start at Qt::UserRole.
// All numbers below this one are reserved for Qt internals.
enum Roles
{
LargePixmapRole = Qt::UserRole + 1
};
explicit ImageListModel(std::initializer_list<QString> files, QObject *parent = 0);
virtual ~ImageListModel();
// QAbstractItemModel interface ===========================
public:
int rowCount(const QModelIndex &parent) const;
QVariant data(const QModelIndex &index, int role) const;
// ========================================================
private:
QList<PixmapPair*> _data;
};
#endif // IMAGELISTMODEL_H
///////////////////////
// imagelistmodel.cpp /
#include "imagelistmodel.h"
ImageListModel::ImageListModel(std::initializer_list<QString> files, QObject *parent)
: QAbstractListModel(parent)
{
auto iter = files.begin();
while (iter != files.end())
{
QPixmap large(*iter);
PixmapPair *pair = new PixmapPair();
pair->_file = *iter;
pair->_large = large;
pair->_small = large.scaled(100, 100, Qt::KeepAspectRatio, Qt::SmoothTransformation);
_data.append(pair);
++iter;
}
}
ImageListModel::~ImageListModel()
{
qDeleteAll(_data);
}
int ImageListModel::rowCount(const QModelIndex &parent) const
{
// This function should return the number of rows contained in the parent
// parameter, the parent parameter is used for trees in order to retrieve the
// number of rows contained in each node. Since we are doing a list each element
// doesn't have child nodes so we return 0
// By convention an invalid parent means the topmost level of a tree. In our case
// we return the number of elements contained in our data store.
if (parent.isValid())
return 0;
else
return _data.count();
}
QVariant ImageListModel::data(const QModelIndex &index, int role) const
{
if (index.isValid())
{
switch (role)
{
case Qt::DecorationRole:
{
// DecorationRole = Icon show for a list
return _data.value(index.row())->_small;
}
case Qt::DisplayRole:
{
// DisplayRole = Displayed text
return _data.value(index.row())->_file;
}
case LargePixmapRole:
{
// This is a custom role, it will help us getting the pixmap more
// easily later.
return _data.value(index.row())->_large;
}
}
}
// returning a default constructed QVariant, will let Views knows we have nothing
// to do and we let the default behavior of the view do work for us.
return QVariant();
}
///////////////////////
Our list is now ready and we are almost done.
// main.cpp ///////////
#include <QApplication>
#include <QSplitter>
#include <QLabel>
#include <QListView>
#include "imagelistmodel.h"
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
QSplitter page;
QListView *imageList = new QListView(&page);
imageList->setModel(new ImageListModel({ "ocean.jpg", "forest.jpg" }, imageList));
// We tell the list view to show our icon, this mode will call the data function
// of our model with the role : DecorationRole.
imageList->setViewMode(QListView::IconMode);
// We want our list to show data vertically
imageList->setFlow(QListView::TopToBottom);
// We allow only one selection at a time in the list
imageList->setSelectionMode(QListView::SingleSelection);
QLabel *imagePresenter = new QLabel(&page);
// We connect to the signal emitted when the selection is changed
// to update the image presenter.
QObject::connect(imageList->selectionModel(), &QItemSelectionModel::selectionChanged, [imageList, imagePresenter] {
QModelIndex selectedIndex = imageList->selectionModel()->selectedIndexes().first();
// We use our custom role here to retrieve the large image using the selected
// index.
imagePresenter->setPixmap(selectedIndex.data(ImageListModel::LargePixmapRole).value<QPixmap>());
});
page.setWindowTitle("Images");
page.show();
return a.exec();
}
Advantages for this solution are:
- We can easily add filtering by wrapping our custom ListModel into a QSortFilterProxyModel.
- No need to create and manage a lot of buttons.
- The model never needs to know who shows it on screen.
- The QListView will autoscroll if necessary.
- Using a custom role allows us to easily retrieve the large image. If we added the large image in another column, it would show when using this model with a QTableView and when we want retrieve it from the selected index we would have to create a new index pointing to the right column. (Not really hard but require a little more code, and prone to error if we wrap the model in a ProxyModel)
Lambda explanation
For the lambda in C++ the full syntax is:
[CAPTURES]\(PARAMETERS\)->RESULT {FUNCTION}.
Between brackets we capture variables to be able to use them inside the FUNCTION without having to pass them as parameters.
The PARAMETERS between parenthesis have the same signification as any other function, if omitted the lambda takes no parameters.
RESULT is the return type of the FUNCTION and can be omitted.
FUNCTION the body to execute
In this example I decided to ignore the parameters given by the signal so I omitted the parenthesis. I use the captured controls to retrieve the user selection and update the picture shown.

Related

Is it possible to constrain a Qt parent QWidget to always be the same size as its child QWidget?

I've written a simple "proxy widget" class in Qt; the idea is that this widget will hold a single child QWidget and represent that widget in the QWidget hierarchy. (FWIW, the motivation for doing this is to make it easy to move the child widget around the hierarchy without having to directly disturb the state of various other container-QWidgets to do so).
This seems to work fairly well; the only problem I've run into is that I want my ProxyWidget to always be laid-out the same way as its child-QWidget would be (if the child had been added to the widget hierarchy directly); but instead I find that the ProxyWidget is often sized larger than its child would be, leading to wasted space in the GUI.
Therefore, is there some way I can craft my ProxyWidget class so that Qt's layout managers to move/size it exactly the same as if its child widget was added directly?
As a minimal test/example, you can compile the following code and run it with or without the "proxy" command line argument -- my goal is that the visual results would be the same either way, and in particular that you would never see any red pixels in the window (since red pixels indicate areas where the ProxyWidget has been sized larger than the blue child widget it contains)
#include <QApplication>
#include <QStackedLayout>
#include <QWidget>
class ProxyWidget : public QWidget
{
public:
ProxyWidget(QWidget * childWidget)
: _childWidget(childWidget)
, _layout(new QStackedLayout(this))
{
_layout->addWidget(childWidget);
setSizePolicy(childWidget->sizePolicy());
}
virtual QSize sizeHint() const {return _childWidget->sizeHint();}
virtual QSize minimumSizeHint() const {return _childWidget->minimumSizeHint();}
private:
QWidget * _childWidget;
QStackedLayout * _layout;
};
static void SetWidgetBackgroundColor(QWidget * w, const QColor bc)
{
QPalette p = w->palette();
p.setColor(QPalette::Window, bc);
w->setAutoFillBackground(true);
w->setPalette(p);
}
int main(int argc, char ** argv)
{
QApplication app(argc, argv);
QWidget * win = new QWidget;
win->setWindowTitle("Proxy Widget test");
QWidget * proxyMe = new QWidget;
proxyMe->setFixedSize(100, 50);
SetWidgetBackgroundColor(proxyMe, Qt::blue);
QBoxLayout * winLayout = new QBoxLayout(QBoxLayout::TopToBottom, win);
if ((argc >= 2)&&(strcmp(argv[1], "proxy") == 0))
{
ProxyWidget * proxyWidget = new ProxyWidget(proxyMe);
SetWidgetBackgroundColor(proxyWidget, Qt::red);
winLayout->addWidget(proxyWidget);
}
else winLayout->addWidget(proxyMe);
win->show();
return app.exec();
}
I guess minimumSize and maximumSize of ProxyWidget is different from its child widget and setting them fix things in your particular example :
ProxyWidget(QWidget * childWidget)
: _childWidget(childWidget)
, _layout(new QStackedLayout(this))
{
_layout->addWidget(childWidget);
setSizePolicy(childWidget->sizePolicy());
this->setMinimumSize(childWidget->minimumSize());
this->setMaximumSize(childWidget->maximumSize());
}
However i am not sure it's the best solution but it might gives you a hint to a better one.

Passing arguments to a slot in qt5 c++ [duplicate]

This question already has answers here:
Passing an argument to a slot
(6 answers)
Closed 6 years ago.
I'm creating a ToDo list app in c++ qt. When a plus button is pressed, it adds a QHBoxLayout containing a QLabel and a QToolButton to a vertical layout inside my gui, so I get new boxes with the 'ToDos' inside them, and buttons to remove them next to them. I have set up the various widgets inside my slot which is called when the add button is clicked. However, I need to pass them as arguments to the slot which is called when a remove button is pressed. I have researched already, and all I have found is QSignalMapper. However, I cannot find any cases close enough to mine to replicate, and I have read it only works with certain arguments, and not the three I need (QHBoxLayout, QLineEdit and QToolButton).
Some of the code for the slot which is called when the 'add' button is pressed is:
//Creates a read only LineEdit which the user will add
QLineEdit *toDoBox = new QLineEdit(this);
toDoBox->setText(ui->lineEdit->text());
toDoBox->setReadOnly(true);
//Creates a new X button for removal of ToDo's
QToolButton *removeButton = new QToolButton;
removeButton->setText("X");
//Adds a horizontal layout with the ToDo and the remove button in it, to keep them together
QHBoxLayout *toDoLayout = new QHBoxLayout;
toDoLayout->addWidget(toDoBox);
toDoLayout->addWidget(removeButton);
//Removes a ToDo when the remove button is clicked
connect(removeButton, SIGNAL(clicked()), this, SLOT(on_removeButton_clicked()));
My code is hosted on GitHub if you want to see the whole project:
https://github.com/DanWilkes02/ToDoList
Thanks for bearing with me- I struggle explaining things that are so clear in my head!
If I understand well your problem, you want to get the allocated objects which represent a todo in order to free them and to update your View.
You could achieve this by simply wrapping your QLineEdit, QToolButton and QHBoxLayout objects into a class, and use a container (a vector for instance) in your ToDoList class. That way, you push_back your "todo object" each time you press the on_toolButton_clicked method.
Then, you simply have to use a signal with an index triggering an on_delete_todo slot which deletes a "todo object" from your vector and update the view.
Also, take a look at this Qt Model-View Programming
Here is a sample (tested and working under QT5):
Your Todo Widget
#ifndef TODOVIEW_H
#define TODOVIEW_H
#include <QString>
class QLineEdit;
class QToolButton;
class QHBoxLayout;
#include <QWidget>
class TodoView : public QWidget
{
Q_OBJECT
private:
QLineEdit* frame;
QToolButton* removeButton;
QHBoxLayout* toDoLayout;
int index;
public:
TodoView(const QString& what, int index, QWidget* parent);
~TodoView();
inline void setIndex(int i) { index = i; }
inline int getIndex(){ return index; }
private slots:
void emitIndex();
signals:
void selectedIndex(int);
};
#endif // TODOVIEW_H
#include "todoview.h"
#include <QLineEdit>
#include <QToolButton>
#include <QHBoxLayout>
TodoView::TodoView(const QString& what, int index, QWidget* parent) : QWidget(parent), index(index)
{
frame = new QLineEdit(this);
frame->setText(what);
frame->setReadOnly(true);
removeButton = new QToolButton(this);
removeButton->setText("X");
toDoLayout = new QHBoxLayout(this);
toDoLayout->addWidget(frame);
toDoLayout->addWidget(removeButton);
connect(removeButton, SIGNAL(clicked()), this, SLOT(emitIndex()));
}
TodoView::~TodoView() {}
void TodoView::emitIndex()
{
emit selectedIndex(getIndex());
}
Your MainWindow
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
#include <vector>
class TodoView;
class QVBoxLayout;
namespace Ui {
class MainWindow;
}
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
explicit MainWindow(QWidget *parent = 0);
~MainWindow();
private slots:
void addTodo();
void delTodo(int);
private:
Ui::MainWindow* ui;
QVBoxLayout* vBoxLayout;
std::vector<TodoView*> todoView;
int max = -1;
};
#endif // MAINWINDOW_H
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include "todoview.h"
#include <QVBoxLayout>
#include <QAction>
MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::MainWindow)
{
ui->setupUi(this);
vBoxLayout = new QVBoxLayout(centralWidget());
QAction* add = new QAction(ui->mainToolBar);
ui->mainToolBar->addAction(add);
connect(add, SIGNAL(triggered()), this, SLOT(addTodo()));
}
MainWindow::~MainWindow()
{
delete ui;
}
void MainWindow::addTodo()
{
if(max > 9)
{
// Error msg.
}
else
{
TodoView* tdV = new TodoView("Yolo", max, centralWidget());
connect(tdV, SIGNAL(selectedIndex(int)), this, SLOT(delTodo(int)));
vBoxLayout->addWidget(tdV);
todoView.push_back(tdV);
++max;
}
}
void MainWindow::delTodo(int i)
{
// check if i < todoView.size().
delete todoView.at(i);
// update vector indexes !!!
--max;
}
I have edited this piece of code rapidly, I may have made several mistakes, but you have an idea of at least one solution.
It is also possible to use a fixed size for the vector (better solution). Setting the TodoView deleted objects to nullptr in the vector and search for nullptr when you want to add new Todo view components:
In the MainWindow constructor
todoView.reserve(10);
for(std::size_t i = 0; i < 10; ++i)
{
todoView[i] = nullptr;
}
In the addTodo Slot
// Do not use push back.
// retrieve the max index.
// if < 10
for(std::size_t i = 0; i < todoView.size(); ++i)
{
if(todoView[i] == nullptr)
{
// allocate TodoView and affect it to the i° element
}
}
In the delTodo slot
delete todoView[i];
todoView[i] = nullptr;
Using a vector of pair is also possible (a pair of int TodoView).
I can think of two approaches you can go with.
Using QObject::sender().
Using QSignalMapper.
QObject::sender()
This static method returns QObject * of the sender object who emitted the signal. In this case QToolButton, so you can use this reference to find its parent (i.e. QHBoxLayout) and its sibling (i.e. QLineEdit).
QSingalMapper
First define a list for saving references to all rows, then assign each row an unique identifier (e.g. row number), then according to the official documentation's example use this identifier as QSignalMapper key for each button. Now if user clicks a button, in your slot, you will be given the same identifier, use it and lookup the whole row in the list of rows.

How we can access #define variable from structure

I have defined macros for members that I want to access from a structure. I don't want to type cast to any another data type.
Example:
#define LABLE ui->lable->setText("NumbVal")
#define LABLE1 ui->lEditCaliCLFltBst->setText("UNDER PROCESS")
if (EditMode[LOC_04]!=0) { LABLE; } else { LABLE1; }
I want to access this LABLE variable from a structure. But what if I have a larger number of EditMode array entried - I can't make my program lenthy I just want to access through them through a structure.
What you are showing should be, at the very least, functions.
For example:
class Foo : public QWidget {
QScopedPointer<Ui::Foo> ui; // Don't use a raw pointer!
enum { LOC_04, LOC_END };
int m_editMode[LOC_END];
void lable1() { ui->lable->setText("NumbVal"); }
void lable2() { ui->lEditCaliCLFltBst->setText("UNDER PROCESS"); }
...
void f() {
...
if (EditMode[LOC_04]!=0) lable1(); else lable2();
...
}
}
With the little code you've shown, I infer that you have an interface that can be in various states, and those states are indicated through multiple user interface elements. This is what QStateMachine is for.
The example below demonstrates the following:
The use of a state machine to control the appearance of the user interface in each state.
The user interface has two parallel states: the m_editState and the m_boldState. The states are parallel, meaning that the state machine is in both of those states at the same time. Imagine this was in a text editor of some sort.
The edit state can be in one of two substates: m_edit1 and m_edit2. Similarly, the bold state can be in two states: m_boldOn and m_boldOff.
Clicking the buttons switches the states, and modifies the indications on the labels.
Concise setup of a user interface without using the UI designer.
Direct use of QObject members in a QObject, without explicit heap storage. Note the absence of a single explicit new and delete in the entire code. This shouldn't be an end unto itself, but it certainly helps avoid some pitfalls of unmanaged pointers, and it halves the number of heap allocations per each object. This pattens also works great when you put all the members in a pimpl class.
A reasonably concise way of repeating some code for elements of a constant list, created in place. This is pre-C++11 code.
Referring back to your original code, perhaps the EditMode could be represented by a set of states. If there are multiple aspects of the EditMode, they'd be represented by parallel states - perhaps each entry in EditMode would be a parallel state. Without knowing anything else about what you intend to achieve, it's hard to tell.
#include <QApplication>
#include <QLabel>
#include <QPushButton>
#include <QStateMachine>
#include <QGridLayout>
class Widget : public QWidget {
QGridLayout m_layout;
QLabel m_label1, m_label2, m_label3;
QPushButton m_button1, m_button2, m_button3;
QStateMachine m_machine;
QState m_editState, m_boldState, m_edit1, m_edit2, m_boldOn, m_boldOff;
public:
Widget(QWidget * parent = 0) : QWidget(parent), m_layout(this),
m_label1("--"), m_label2("--"), m_label3("--"),
m_button1("Edit State 1"), m_button2("Edit State 2"), m_button3("Toggle Bold State"),
m_editState(&m_machine), m_boldState(&m_machine),
m_edit1(&m_editState), m_edit2(&m_editState),
m_boldOn(&m_boldState), m_boldOff(&m_boldState)
{
m_layout.addWidget(&m_label1, 0, 0);
m_layout.addWidget(&m_label2, 0, 1);
m_layout.addWidget(&m_label3, 0, 2);
m_layout.addWidget(&m_button1, 1, 0);
m_layout.addWidget(&m_button2, 1, 1);
m_layout.addWidget(&m_button3, 1, 2);
m_edit1.assignProperty(&m_label1, "text", "Edit State 1");
m_edit2.assignProperty(&m_label2, "text", "Edit State 2");
m_boldOn.assignProperty(&m_label3, "text", "Bold On");
m_boldOff.assignProperty(&m_label3, "text", "Bold Off");
m_editState.setInitialState(&m_edit1);
m_boldState.setInitialState(&m_boldOff);
foreach (QState * s, QList<QState*>() << &m_edit1 << &m_edit2) {
s->addTransition(&m_button1, SIGNAL(clicked()), &m_edit1);
s->addTransition(&m_button2, SIGNAL(clicked()), &m_edit2);
}
m_boldOn.addTransition(&m_button3, SIGNAL(clicked()), &m_boldOff);
m_boldOff.addTransition(&m_button3, SIGNAL(clicked()), &m_boldOn);
m_machine.setGlobalRestorePolicy(QState::RestoreProperties);
m_machine.setChildMode(QState::ParallelStates);
m_machine.start();
}
};
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
Widget w;
w.show();
return a.exec();
}

How can I test a QAbstractListModel via Qt Quick Test?

I have written a QAbstractListModel class to expose data from my library to QML. It seems to work correctly together with the QML ListView component, but I'd like to write some tests for it to ensure it continues to behave.
I've been testing other parts of my QML module via Qt Quick Test, but couldn't work out how to access the model directly from QML. I'm after simple things like checking the row count, and accessing role data values for arbitrary rows in the model.
Is this actually possible, or would I need to write the tests in C++?
This is a subject that I always have to look up myself, and I'm still a bit unsure, so there may be better ways than what I suggest. Anyway, it seems that QAbstractItemModel doesn't provide the functions that you're trying to test. I can think of two ways to get around that.
Add them yourself
#include <QtGui/QGuiApplication>
#include <QQmlContext>
#include <QQuickView>
#include <QDebug>
#include "qtquick2applicationviewer.h"
#include "QAbstractListModel"
class Model : public QAbstractListModel
{
Q_OBJECT
public:
Model() {}
int rowCount(const QModelIndex &parent) const
{
return mList.size();
}
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const{
if (index.row() < 0 || index.row() >= mList.size()) {
return QVariant();
}
return mList.at(index.row());
}
Q_INVOKABLE QVariant get(int index) {
return data(createIndex(index, 0));
}
Q_INVOKABLE void append(QVariant element) {
beginInsertRows(QModelIndex(), mList.size() + 1, mList.size() + 1);
mList.append(element.toMap().value("name").toString());
endInsertRows();
}
private:
QList<QString> mList;
};
int main(int argc, char *argv[])
{
QGuiApplication app(argc, argv);
QtQuick2ApplicationViewer viewer;
Model model;
viewer.rootContext()->setContextProperty("model", &model);
viewer.setMainQmlFile(QStringLiteral("qml/quick/main.qml"));
viewer.showExpanded();
return app.exec();
}
#include "main.moc"
Here's a small hack of a QML test case:
import QtQuick 2.2
Item {
width: 360
height: 360
Component.onCompleted: {
model.append({name: "blah"});
console.assert(model.get(0) === "blah");
}
}
Use QQmlListProperty
This is only useful if your model is or can be wrapped by a QObject subclass, as it is, as its name suggests, used as a property of an object. It's useful for it's convenience, but is less flexible than the first option, as it can only store QObject-derived object pointers. I won't go into details on this, as I'm assuming that this is not relevant for your use case.
You can find more information below:
Using C++ Models with Qt Quick Views
I know this topic is old, but anyway here is your answer.
Once your QAbstractListModel is defined on cpp side, you access delegate's roles via delegate's property:
import QtQuick 2.5
import QtTest 1.0
Item {
width: 1 // the minimum size of visible compoment
height: 1 // the minimum size of visible compoment
ListView {
id: testView
model: yourModelId
delegate: Item {
property variant dataModel: model
}
}
TestCase {
when: windowShown
function test_dataModelReadDelegate() {
testView.currentIndex = 0
testView.currentItem.dataModel["yourRole"]);
}
}
}
Be careful, ListView must be visible (also TestCase waits until windowShown is true), otherwise the delegates will not be created (TestCase itself is invisible component).
And for some reason, when the root Item has no size, I'll get warning about invalid component size.

How to create different popup (context) menus for each *type* of QTreeWidgetItem

I was able to create a context menu for my QTreeWidget as below
QMenu* pContextMenu = new QMenu(this)
QTreeWidget* pTreeWidget = new QTreeWidget();
QAction* pOpenFile = new QAction(tr("Open A File"), pContextMenu);
pTreeWidget->setContextMenuPolicy(Qt::ActionsContextMenu);
pTreeWidget->addAction(pOpenFile);
But I want a different popup for a branch than a leaf. How do I assign a different popup depending on the type of widgetitem clicked?
My tree:
Branch1 <-- Popup1
Leaf1
Leaf2 <-- Popup2
Branch2
Branch3
Leaf1
QWidget::actions() is not listed as virtual. Else I would have derived my own class from QTreeWidget & reimplemented actions().
Method 1: Override QTreeWidget
A context menu assigned to the QTreeWidget itself will not let you have different context menus for different items, as you have discovered.
As the Qt item views don't have special API for context menus, you have to implement this yourself. Fortunately, it's not very difficult; you just need to:
Create a subclass of QTreeWidget.
Connect the customContextMenuRequested(const QPoint&) signal to a custom slot.
Display the desired context menu.
I've posted a complete working example. Some details to note include:
QTreeWidgetItem provides a handy type property to let you identify items easily without casting, string parsing, or other awkward/fragile methods.
Custom QTreeWidgetItem type values should be greater than or equal to QTreeWidgetItem::UserType.
When displaying a context menu, you must pass a global position to exec(). To correctly map from a position in the widget's space in the slot, you must use the item's viewport widget.
Method 2: Override QItemDelegate (and QTreeWidget ...)
An alternate method is to implement your own QAbstractItemDelegate subclass, and assign it to your tree widget. In your item delegate, you can override editorEvent() to handle mouse presses in the same way.
Although this approach frres is actually more in line with Qt's item view API design, there are a few key disadvantages to this approach:
Item delegates use QModelIndex objects to represent items. To convert to a QTreeWidgetItem, you must use the QTreeWidget::itemFromIndex() method. Unfortunately, this is protected, so it will actually require you to subclass QTreeWidget anyway to provide this API for your delegate. This adds some more boilerplate complexity to your code.
The editorEvent() hook is invoked before the item view handles the event. This means that you can't easily display a context menu and allow the default behavior at the same time (such as selecting the item that was right-clicked).
Since the editorEvent() handler sees all kinds of different events, you must be even more careful to handle them correctly. You must also be careful not to let this monolithic handler grow out of control if your behaviors are complicated.
The core code is very similar, but again, there's a bit more boilerplate. I've posted an example of this approach, as well.
I've modified jmk's code slightly to show how this can be done with
setContextMenuPolicy(Qt::CustomContextMenu) and customContextMenuRequested(const QPoint&) signal.
mytreewidget.h
#include <QTreeWidget>
static const int ItemType1 = QTreeWidgetItem::UserType + 1;
static const int ItemType2 = QTreeWidgetItem::UserType + 2;
class MyTreeWidget : public QTreeWidget
{
Q_OBJECT
public:
MyTreeWidget(QWidget *parent = 0);
private slots:
void showContextMenu(const QPoint &pos);
};
mytreewidget.cpp:
#include "mytreewidget.h"
#include <QMenu>
#include <QTreeWidgetItem>
MyTreeWidget::MyTreeWidget(QWidget *parent)
: QTreeWidget(parent)
{
setContextMenuPolicy(Qt::CustomContextMenu);
connect(this, SIGNAL(customContextMenuRequested(const QPoint&)),
SLOT(showContextMenu(const QPoint&)));
}
void MyTreeWidget::showContextMenu(const QPoint &pos)
{
QMenu menu;
QTreeWidgetItem* item = itemAt(pos);
switch (item->type()) {
case ItemType1:
menu.addAction("This is a type 1");
break;
case ItemType2:
menu.addAction("This is a type 2");
break;
}
menu.exec(mapToGlobal(pos));
}
main.cpp:
#include <QApplication>
#include "mytreewidget.h"
int main(int argc, char** argv)
{
QApplication app(argc, argv);
MyTreeWidget w;
// Add test items.
w.addTopLevelItem(new QTreeWidgetItem(QStringList("A (type 1)"),
ItemType1));
w.addTopLevelItem(new QTreeWidgetItem(QStringList("B (type 1)"),
ItemType1));
w.addTopLevelItem(new QTreeWidgetItem(QStringList("C (type 2)"),
ItemType2));
w.addTopLevelItem(new QTreeWidgetItem(QStringList("D (type 2)"),
ItemType2));
w.show();
return app.exec();
}