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.
Related
I have been trying to add a QToolBar inside a QTabWidget in order to achieve something like the image below, so that everytime I add a new QTabWidget I have also a related QToolBar inside it.
Everything seems to work fine, I create a QAction to link it to the QTabWidget and according to this post it seems to be possible to do that but the problem is that when I compile nothing shows up as shows below:
Below is what I have done so far:
mainwindow.h
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
MainWindow(QWidget *parent = nullptr);
~MainWindow();
void onChangeTab(int index);
void newTab();
void closeTab(const int &index);
private slots:
void on_addTabBtn_clicked();
void on_tabWidget_tabCloseRequested(int index);
private:
Ui::MainWindow *ui;
QAction *addTab1;
QToolBar *mToolBar1;
QAction *addIconToolBar1;
};
mainwindow.cpp
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
ui->setupUi(this);
ui->tabWidget->clear();
ui->tabWidget->setTabsClosable(true);
ui->tabWidget->addTab(new QLabel("Add"), QString("Add"));
ui->toolBar->setContextMenuPolicy(Qt::ActionsContextMenu);
mToolBar1 = new QToolBar;
addIconToolBar1 = new QAction;
addIconToolBar1->setIcon(QIcon("qrc:/cardio.png"));
ui->toolBar->addWidget(mToolBar1);
ui->toolBar->addAction(addIconToolBar1);
connect(ui->addTabBtn, &QPushButton::clicked, this, [&] { ui->tabWidget->addTab(new QLabel("Add"), QString("Add")); });
connect(ui->tabWidget, SIGNAL(tabCloseRequested(int)), this, SLOT(closeTab(int)));
}
MainWindow::~MainWindow()
{
delete ui;
}
void MainWindow::on_addTabBtn_clicked()
{
int index = 0;
if(index == this->ui->tabWidget->count() - 1) {
newTab();
}
}
void MainWindow::on_tabWidget_tabCloseRequested(int index)
{
ui->tabWidget->removeTab(index);
}
I tried to solve the problem in many ways and researched what the cause might be. I came across several references such as this one, which is the most important I found as the user seems to be doing it but there is no reference to documentation or no example to code to understand/study through.
Thanks for pointing to the right direction to solve this issue.
You can simply do something like this, and it really works.
QToolBar *toolbar=new QToolBar("toolbar",ui->tab);
toolbar->addAction("action1");
toolbar->addAction("action2");
enter image description here
I don't see where you are trying to add Toolbar to your TabWidget...
You must define Layout, add your toolbar to that layout and finally set layout to your tabWidget.
Try to do something like this, in your mainwindow constructor.
QHBoxLayout* tabWidgetLayout = new QHBoxLayout;
tabWidgetLayout->addWidget( your toolbar);
tabwidget->setLayout(tabWidgetLayout);
Also don't forget to include QHBoxLayout's header.
Even if other answers may seem to work, this is actually the right way to do what you asked for.
I've got a small problem with my web browser project. Whenever I enter the URL address (via QLineEdit), the browser doesn't show the page, and whenever I change the page (via click on-site with starting page included) the address doesn't show up on the URL bar.
Here's my mainwindow.cpp code. The program executes and exits with code 0. I tried using qDebug inside the functions (changeUrlBar(QUrl) and setUrl()) and it turns out that the program enters these functions but they don't do anything. Every advice would be very appreciated.
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QDebug>
MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::MainWindow),
browserView(new QWebEngineView),
urlBar(new QLineEdit)
{
ui->setupUi(this);
//
// initialization of widgets and layouts
// widgets
QWidget *browserWindow = new QWidget(this);
QLineEdit *urlBar = new QLineEdit;
QProgressBar *progressBar = new QProgressBar;
// WebEngineView - actual web browser
QWebEngineView *browserView = new QWebEngineView(parent);
// layouts
QVBoxLayout *mainLayout = new QVBoxLayout;
QHBoxLayout *topBarLayout = new QHBoxLayout;
// push buttons
QPushButton *buttonBack = new QPushButton("Back");
QPushButton *buttonForward = new QPushButton("Forward");
QPushButton *buttonReload = new QPushButton("Reload");
//
// creating the widgets and layouts
// top bar
topBarLayout->addWidget(buttonBack);
topBarLayout->addWidget(buttonForward);
topBarLayout->addWidget(buttonReload);
topBarLayout->addWidget(urlBar);
// main layout of the browser
mainLayout->addLayout(topBarLayout);
mainLayout->addWidget(progressBar);
mainLayout->addWidget(browserView);
browserWindow->setLayout(mainLayout);
setCentralWidget(browserWindow);
//
// connecting slots and signals
// internal connections
connect(buttonBack, SIGNAL(clicked()), browserView, SLOT(back()));
connect(buttonForward, SIGNAL(clicked()), browserView, SLOT(forward()));
connect(buttonReload, SIGNAL(clicked()), browserView, SLOT(reload()));
connect(browserView, SIGNAL(loadProgress(int)), progressBar, SLOT(setValue(int)));
// browser connections
connect(browserView, SIGNAL(urlChanged(QUrl)), this, SLOT(changeUrlBar(QUrl)));
connect(urlBar, SIGNAL(editingFinished()), this, SLOT(setUrl()));
// set starting page
browserView->load(QUrl("https://www.wikipedia.org"));
}
void MainWindow::setUrl()
{
browserView->load(QUrl::fromUserInput(urlBar->text()));
}
void MainWindow::changeUrlBar(QUrl)
{
urlBar->setText(browserView->url().toString());
}
MainWindow::~MainWindow()
{
delete ui;
delete browserView;
delete urlBar;
}
Your actual problem is that you've defined two local variables (urlBar and browserView) that are hiding the declaration of MainWindow::urlBar and MainWindow::browserView.
Those local objects are the ones added to the user interface, but in the slots you are using the member objects (those that were not included in the UI). Even when they are initialized in the constructor, they are not neither receiving user input nor being displayed on the user interface.
MainWindow::MainWindow(QWidget *parent) :
// ...
QLineEdit *urlBar = new QLineEdit; // <-- local variable hiding member declaration
QProgressBar *progressBar = new QProgressBar;
// WebEngineView - actual web browser
QWebEngineView *browserView = new QWebEngineView(parent); // <-- local variable hiding member declaration
// ...
void MainWindow::changeUrlBar(QUrl)
{
urlBar->setText(browserView->url().toString()); // <-- urlBar and browserView are members
}
Moral: avoid hiding or be conscious about it ;). Some tricks used to reduce this are to always access member through this (this->urlBar), or using a different notation for members (like m_urlBar or urlBar_). Also, many compilers should warn about this.
I feel like an idiot now because I managed to solve this issue and the only thing to do was to delete following lines:
QLineEdit *urlBar = new QLineEdit;
QWebEngineView *browserView = new QWebEngineView(parent);
As these objects were already initialised.
I'm trying to make a simple test to use an UI object made with "Qt Design" but I'm pretty new to Qt and C++.
I've got a quite simple Ui : 3 LineEdits and 1 PushButton :
IMAGE : the UI window
I've a Client Class which is supposed to control Ui. It connects the QPushButton and it should get the content from QLineEdit.
But the result in QDebug is always the same when I push the Button, even when I change QlineEdit field: "Client connected : "" : 0 "
Moreover, if I use on_pushButton_clicked made with QtDesign, it will display the real values of QlineEdits.
Why the QStrings are always the same ? Am I passing a copy of the initial object ? How to solve that ?
Is it the good way to make a ViewController ? Else, what is the good way?
Client.cpp
#include "client.h"
#include "mainwindow.h"
#include "logwindow.h"
Client::Client()
{
LogWindow* w1 = new LogWindow();
MainWindow* w2 = new MainWindow();
_stack = new QStackedWidget();
_stack->addWidget(w1);
connect(w1->getButton(),SIGNAL(clicked()),this,SLOT(connexion()));
_stack->addWidget(w2);
_stack->show();
}
//When the button is Pushed, gets the content from QlineEdits and prints them
void Client::connexion()
{
QString ip=(LogWindow (_stack->currentWidget())).getIP();
int port=((LogWindow (_stack->currentWidget())).getPort()).toInt();
socket = new QTcpSocket(this);
socket->connectToHost(ip, port);
_stack->setCurrentIndex((_stack->currentIndex()+1)%_stack->count());
qDebug() <<"Client connected : " << ip << ":"<<port;
}
And a class made automatically by Qt :
LogWindow.cpp
#include "logwindow.h"
#include "ui_logwindow.h"
LogWindow::LogWindow(QWidget *parent) :
QWidget(parent),
ui(new Ui::LogWindow)
{
ui->setupUi(this);
}
LogWindow::~LogWindow()
{
delete ui;
}
QPushButton* LogWindow::getButton()
{
return ui->pushButton;
}
QString LogWindow::getIP()
{
//LineEdit named "IP_text"
return ui->IP_text->text();
}
QString LogWindow::getPort()
{
//LineEdit named "Port_text"
return ui->Port_text->text();
}
LogWindow.h
namespace Ui {
class LogWindow;
}
class LogWindow : public QWidget
{
Q_OBJECT
public:
explicit LogWindow(QWidget *parent = 0);
~LogWindow();
QPushButton* getButton();
QString getIP();
QString getPort();
private slots:
void on_pushButton_clicked();
private:
Ui::LogWindow *ui;
};
Thuga solved it :
In Client::connexion you are creating a new instance of LogWindow.
Make LogWindow* w1 a member variable of your Client class, if you want
to access it in other Client's member functions as well.
There is not much to complain about, except that _stack is a widget
without a parent, so you must make sure you destroy it when you don't
need it anymore (for example call delete _stack; in the destructor).
Most beginners would have tried to make the ui variable public to get
the data from IP_text, but you did correctly, by making the
LogWindow::getIP function.
If you don't want to expose ui->pushButton outside of your class, you
can make a signal for your LogWindow class, and connect the clicked
signal of ui->pushButton to that signal. You can connect signals to
signals, it doesn't have to be a slot.
My application uses a QTabWidget for multiple 'pages', where the top-level menu changes depending on what page the user is on.
My issue is that attempting to re-create the contents of the menu bar results in major display issues. It works as expected with the first and third style (haven't tested second, but I'd rather not use that style) on all platforms except for Mac OS X.
The first menu is created in the way I create most in the application, and they receive the correct title, but disappear as soon as the menu is re-created.
The second menu appears both on the initial population and the re-population of the menu bar, but in both cases has the label "Untitled". The style for the second menu was only created when trying to solve this, so it's the only way I've been able to have a menu stick around.
The third dynamic menu never appears, period. I use this style for dynamically populating menus that are about to show.
I have tried deleting the QMenuBar and re-creating one with
m_menuBar = new QMenuBar(0);
and using that as opposed to m_menuBar->clear() but it has the same behavior.
I don't have enough reputation to post images inline, so I'll include the imgur links:
Launch behavior: http://i.imgur.com/ZEvvGKl.png
Post button-click behavior: http://i.imgur.com/NzRmcYg.png
I have created a minimal example to reproduce this behavior on Mac OS X 10.9.4 with Qt 5.3.
mainwindow.cpp
#include "mainwindow.h"
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
{
m_menuBar = new QMenuBar(0);
m_dynamicMenu = new QMenu("Dynamic");
connect(m_dynamicMenu, SIGNAL(aboutToShow()), this, SLOT(updateDynamicMenu()));
changeMenuBar();
QPushButton *menuBtn = new QPushButton("Test");
connect(menuBtn, SIGNAL(clicked()), this, SLOT(changeMenuBar()));
setCentralWidget(menuBtn);
}
void MainWindow::changeMenuBar() {
m_menuBar->clear();
// Disappears as soon as this is called a second time
QMenu *oneMenu = m_menuBar->addMenu("One");
oneMenu->addAction("foo1");
oneMenu->addAction("bar1");
oneMenu->addAction("baz1");
// Stays around but has 'Untitled' for title in menu bar
QMenu *twoMenu = new QMenu("Two");
twoMenu->addAction("foo2");
twoMenu->addAction("bar2");
twoMenu->addAction("baz2");
QAction *twoMenuAction = m_menuBar->addAction("Two");
twoMenuAction->setMenu(twoMenu);
// Never shows up
m_menuBar->addMenu(m_dynamicMenu);
}
void MainWindow::updateDynamicMenu() {
m_dynamicMenu->clear();
m_dynamicMenu->addAction("foo3");
m_dynamicMenu->addAction("bar3");
m_dynamicMenu->addAction("baz3");
}
mainwindow.h
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QtWidgets>
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
MainWindow(QWidget *parent = 0);
private slots:
void changeMenuBar();
void updateDynamicMenu();
private:
QMenuBar *m_menuBar;
QMenu *m_dynamicMenu;
};
#endif // MAINWINDOW_H
All this looks like Qt bug on OS X. And it's very old bug, actually.
You can do workaround and don't work with QMenu via QMenuBar::addMenu function calls, as you do here:
m_menuBar->addMenu("One");
Instead of this work with QAction retrieved from QMenu by creation of the QMenu instance dynamically and then calling of QMenuBar::addAction for the QAction instance retrieved by QMenu::menuAction, as following:
m_menuBar->addAction(oneMenu->menuAction());
Beside QMenuBar::addAction you can use QMenuBar::removeAction and QMenuBar::insertAction if you want to make creation only of some specific menu items dynamically.
Based on your source code here it's modified version of it which deals with all menus dynamic creation on every button click (you do this in your source code) and the menu 'Dynamic' is populated with different count of items every time you click the button.
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QtWidgets>
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
MainWindow(QWidget *parent = 0);
private slots:
void changeMenuBar();
private:
QMenuBar *m_menuBar;
QMenu *m_dynamicMenu;
int m_clickCounter;
};
#endif // MAINWINDOW_H
#include "mainwindow.h"
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent),
m_clickCounter(1)
{
m_menuBar = new QMenuBar(this);
connect(m_dynamicMenu, SIGNAL(aboutToShow()), this, SLOT(updateDynamicMenu()));
changeMenuBar();
QPushButton *menuBtn = new QPushButton("Test");
connect(menuBtn, SIGNAL(clicked()), this, SLOT(changeMenuBar()));
setCentralWidget(menuBtn);
}
void MainWindow::changeMenuBar() {
++m_clickCounter;
m_menuBar->clear();
QMenu *oneMenu = new QMenu("One");
oneMenu->addAction("bar1");
oneMenu->addAction("baz1");
m_menuBar->addAction(oneMenu->menuAction());
QMenu *twoMenu = new QMenu("Two");
twoMenu->addAction("foo2");
twoMenu->addAction("bar2");
twoMenu->addAction("baz2");
m_menuBar->addAction(twoMenu->menuAction());
m_dynamicMenu = new QMenu("Dynamic");
for (int i = 0; i < m_clickCounter; ++i) {
m_dynamicMenu->addAction(QString("foo%1").arg(i));
}
m_menuBar->addAction(m_dynamicMenu->menuAction());
}
Additionally while developing menus logic for OS X it's good to remember:
It's possible to disable QMenuBar native behavior by using QMenuBar::setNativeMenuBar
Because of turned on by default QMenuBar native behavior, QActions with standard OS X titles("About","Quit") will be placed automatically by Qt in the predefined way on the screen; Empty QMenu instances will be not showed at all.
I think your problem is this line:
QMenu *oneMenu = m_menuBar->addMenu("One");
To add menus to a menubar you'd want code as follows:
QMenuBar *m = new QMenuBar;
m->addMenu( new QMenu("Hmmm") );
m->show();
To create menus, and then add actions, and then add the menu to the menu bar:
QMenu *item = new QMenu( "Test1" );
item->addAction( "action1" );
QMenuBar *t = new QMenuBar;
t->addMenu( item );
t->show();
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.