QListView max number of items in view - qt

I need to calculate max number of items in current view of QListView.
I wrote code like this:
void MyListView::resizeEvent(QResizeEvent *event)
{
QListView::resizeEvent(event);
QFontMetrics fm (this->font());
int fontHeight = fm.lineSpacing();
QRect cr = contentsRect();
int windowHeight = cr.bottom() - cr.top();
int maxItemsCount = windowHeight / fontHeight;
qDebug()<<"max items in view: "<< maxItemsCount;
}
but calculated max number of items is is incorrect.
E.g. in case of my window height and font height I get 32 max items in view when in fact current view has 28 items. Perhaps someone can suggest something, how to calculate it properly?

My idea is to use QListView::indexAt() (inherited from QAbstractView) to obtain the row index for
the top-left corner
the bottom-left corner
of the list view viewport and determining the number of visible items by difference of them.
To check this out, I made an MCVE testQListViewNumVisibleItems.cc:
// Qt header:
#include <QtWidgets>
class ListWidget: public QListWidget {
public:
ListWidget(QWidget *pQParent = nullptr): QListWidget(pQParent) { }
virtual ~ListWidget() = default;
ListWidget(const ListWidget&) = delete;
ListWidget& operator=(const ListWidget&) = delete;
int getNumVisibleItems() const
{
const QSize size = viewport()->size();
const QModelIndex qMIdx0 = indexAt(QPoint(0, 0));
const QModelIndex qMIdx1 = indexAt(QPoint(0, size.height() - 1));
//qDebug() << "qMIdx0:" << qMIdx0 << "qMIdx1:" << qMIdx1;
return qMIdx0.isValid()
? (qMIdx1.isValid() ? qMIdx1.row() : count()) - qMIdx0.row()
: 0;
}
};
const int MaxItems = 20;
// main application
int main(int argc, char **argv)
{
qDebug() << "Qt Version:" << QT_VERSION_STR;
QApplication app(argc, argv);
// setup GUI
ListWidget qLst;
qLst.resize(200, 200);
qLst.show();
// timer to populate list view
using namespace std::chrono_literals;
QTimer qTimer;
qTimer.setInterval(1000ms);
// install signal handlers
int n = 0;
QObject::connect(&qTimer, &QTimer::timeout,
[&]() {
qLst.addItem(QString("item %0").arg(++n));
qDebug() << "Visible items:" << qLst.getNumVisibleItems();
if (n >= MaxItems) qTimer.stop();
});
// runtime loop
qTimer.start();
return app.exec();
}
and a CMakeLists.txt:
project(QListViewNumVisibleItems)
cmake_minimum_required(VERSION 3.10.0)
set_property(GLOBAL PROPERTY USE_FOLDERS ON)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
find_package(Qt5Widgets CONFIG REQUIRED)
include_directories("${CMAKE_SOURCE_DIR}")
add_executable(testQListViewNumVisibleItems testQListViewNumVisibleItems.cc)
target_link_libraries(testQListViewNumVisibleItems Qt5::Widgets)
built and tested in VS2017 on Windows 10:
After having implemented what came in my mind, I googled a bit to possibly see other approaches. (I admit I should've done before.)
Thereby I found the following possible duplicate:
SO: Simple way to get all visible items in the QListView
The accepted answer doesn't contain much more than the hint for indexAt and a link to a Qt-FAQ article:
How can i get hold of all of the visible items in my QListView?
In order to get hold of the visible items in a QListView http://doc.qt.io/qt-5/qlistview.html, then you can iterate over them using indexAt() http://doc.qt.io/qt-5/qlistview.html#indexAt. You can get hold of the first visible item using indexAt(QPoint(0, 0)), then in order to get the index at the next position then use visualRect() http://doc.qt.io/qt-5/qlistview.html#visualRect to find out what your next call to itemAt() should be. This position would be:
visualRect.y() + visualRect.height() + 1 effectively.
See the following example for an illustration:
#include <QtGui>
QList <QModelIndex>myList;
class ListView : public QListView
{
Q_OBJECT
public:
ListView()
{
QStringListModel *myModel = new QStringListModel(this);
QStringList list;
list << "a" << "b" <<"c" <<"d" <<"e" <<"f" <<"g" <<"h" <<"i" <<"j" <<"k";
myModel->setStringList(list);
setModel(myModel);
QTimer::singleShot(3000, this, SLOT(test1()));
}
public slots:
void test1()
{
QModelIndex firstIndex = indexAt(QPoint(0, 0));
if (firstIndex.isValid()) {
myList << firstIndex;
} while (viewport()->rect().contains(QPoint(0, visualRect(firstIndex).y() + visualRect(firstIndex).height() + 1 ))) {
firstIndex = indexAt(QPoint(0, visualRect(firstIndex).y() + visualRect(firstIndex).height() + 1 ));
myList << firstIndex;
}
qDebug() << myList.count() << "are visible";
}
};
#include "main.moc"
int main(int argc, char** argv)
{
QApplication app(argc, argv);
ListView window;
window.resize(100, 50);
window.show();
return app.exec();
}

Related

How to add curve path to connect flow widgets from two columns in qt?

I know this question is somehow hard to describe, so a picture may help, as following:
I'm developing application in qt-5.15.x. Say I have a window or widget or something else which can layout two columns of widgets(customized, call it rectangle here), I want to:
drag and drop one item up or down within same column by mouse
connect two widgets from two different columns with a customized curve path by mouse
all elements(rectangle and curve item) have mouse events: move, hover, click and key event
the contents of window or widget can scroll vertically due to more and more items will be added
Before this, I have implemented bazier curve, graphicsview with customized widgets and connecting these widgets with bazier curve, but all these widgets don't lay on line(horizontally or vertically).
So my question is not about how to implement it in detail, but just a guide - what widget , layout or event something else in qt I can use, or which document I can refer to. I have searched a lot, but with no results.
OPs requirement can be achieved by simply deriving a widget from QWidget and overriding the paintEvent().
Beside of that, the derived widget still can contain children and layout managers, so that both can be combined.
Sample code testQWidgetPaintOverChildren.cc to demonstrate:
#include <QtWidgets>
class Canvas: public QWidget {
private:
using Link = std::pair<int, int>;
std::vector<Link> _links;
public:
void addLink(int from, int to)
{
_links.emplace_back(from, to);
update();
}
protected:
virtual void paintEvent(QPaintEvent *pQEvent) override;
};
void Canvas::paintEvent(QPaintEvent* pQEvent)
{
QWidget::paintEvent(pQEvent);
const int lenTan = 64;
QPainter qPainter(this);
auto drawCurve = [&](const QPoint& qPosFrom, const QPoint& qPosTo) {
QPainterPath qPath;
qPath.moveTo(qPosFrom);
qPath.cubicTo(qPosFrom + QPoint(lenTan, 0), qPosTo - QPoint(lenTan, 0), qPosTo);
qPainter.drawPath(qPath);
};
const QObjectList& pQChildren = children();
for (const Link& link : _links) {
const int from = link.first;
if (from >= pQChildren.size()) continue; // invalid from index
const QWidget* pQWidgetFrom
= dynamic_cast<const QWidget*>(pQChildren[from]);
if (!pQWidgetFrom) continue; // shouldn't happen
const int to = link.second;
if (to >= pQChildren.size()) continue; // invalid to index
const QWidget* pQWidgetTo
= dynamic_cast<const QWidget*>(pQChildren[to]);
if (!pQWidgetTo) continue; // shouldn't happen
const QPoint qPosFrom
= pQWidgetFrom->pos() + QPoint(3 * pQWidgetFrom->width() / 4, pQWidgetFrom->height() / 2);
const QPoint qPosTo
= pQWidgetTo->pos() + QPoint(1 * pQWidgetTo->width() / 4, pQWidgetTo->height() / 2);
//qPainter.drawLine(qPosFrom, qPosTo);
drawCurve(qPosFrom, qPosTo);
}
}
int main(int argc, char** argv)
{
qDebug() << "Qt Version:" << QT_VERSION_STR;
QApplication app(argc, argv);
// setup GUI
Canvas qCanvas;
qCanvas.setWindowTitle("Test Draw Over Children");
qCanvas.resize(320, 240);
QGridLayout qGrid;
qGrid.setSpacing(16);
#define ITEM(ROW, COL) \
QLabel qLbl##ROW##COL("txt "#ROW", "#COL); \
qLbl##ROW##COL.setAlignment(Qt::AlignCenter); \
qLbl##ROW##COL.setFrameStyle(QFrame::Box | QFrame::Plain); \
qGrid.addWidget(&qLbl##ROW##COL, ROW - 1, COL - 1)
ITEM(1, 1); ITEM(1, 2);
ITEM(2, 1); ITEM(2, 2);
ITEM(3, 1); ITEM(3, 2);
ITEM(4, 1); ITEM(4, 2);
qCanvas.setLayout(&qGrid);
qCanvas.addLink(1, 8);
qCanvas.addLink(5, 2);
qCanvas.show();
// runtime loop
return app.exec();
}
Output:
Qt Version: 5.15.1
The drawback of this first attempt is that the links are drawn under the children. In the case of the QLabel, this isn't visible as QLabels are mostly transparent. Replacing them by QPushButtons makes this obvious.
This can be solved by using two widgets, one as container for children, the other for drawing the links, and ensuring that the second widget has the same position and size than the first.
Improved sample code testQWidgetPaintOverChildren.cc:
#include <QtWidgets>
class Widget: public QWidget {
private:
using Link = std::pair<int, int>;
struct Canvas: public QWidget {
std::vector<Link> links;
using QWidget::QWidget;
virtual void paintEvent(QPaintEvent *pQEvent) override;
} _canvas;
public:
explicit Widget(QWidget* pQParent = nullptr):
QWidget(pQParent),
_canvas(this)
{ }
public:
void addLink(int from, int to)
{
_canvas.links.emplace_back(from, to);
_canvas.update();
_canvas.setParent(nullptr);
_canvas.setParent(this);
}
protected:
virtual void resizeEvent(QResizeEvent* pQEvent) override
{
QWidget::resizeEvent(pQEvent);
_canvas.resize(size());
}
};
void Widget::Canvas::paintEvent(QPaintEvent* pQEvent)
{
QWidget::paintEvent(pQEvent);
const int lenTan = 64;
QPainter qPainter(this);
auto drawCurve = [&](const QPoint& qPosFrom, const QPoint& qPosTo) {
QPainterPath qPath;
qPath.moveTo(qPosFrom);
qPath.cubicTo(qPosFrom + QPoint(lenTan, 0), qPosTo - QPoint(lenTan, 0), qPosTo);
qPainter.drawPath(qPath);
};
const QObjectList& pQChildren = parent()->children();
for (const Link& link : links) {
const int from = link.first;
if (from >= pQChildren.size()) continue; // invalid from index
const QWidget* pQWidgetFrom
= dynamic_cast<const QWidget*>(pQChildren[from]);
if (!pQWidgetFrom) continue; // shouldn't happen
const int to = link.second;
if (to >= pQChildren.size()) continue; // invalid to index
const QWidget* pQWidgetTo
= dynamic_cast<const QWidget*>(pQChildren[to]);
if (!pQWidgetTo) continue; // shouldn't happen
const QPoint qPosFrom
= pQWidgetFrom->pos() + QPoint(3 * pQWidgetFrom->width() / 4, pQWidgetFrom->height() / 2);
const QPoint qPosTo
= pQWidgetTo->pos() + QPoint(1 * pQWidgetTo->width() / 4, pQWidgetTo->height() / 2);
//qPainter.drawLine(qPosFrom, qPosTo);
drawCurve(qPosFrom, qPosTo);
}
}
int main(int argc, char** argv)
{
qDebug() << "Qt Version:" << QT_VERSION_STR;
QApplication app(argc, argv);
// setup GUI
Widget qWinMain;
qWinMain.setWindowTitle("Test Draw Over Children");
qWinMain.resize(320, 240);
QGridLayout qGrid;
qGrid.setSpacing(16);
#define ITEM(ROW, COL) \
QPushButton qBtn##ROW##COL("txt "#ROW", "#COL); \
qGrid.addWidget(&qBtn##ROW##COL, ROW - 1, COL - 1)
ITEM(1, 1); ITEM(1, 2);
ITEM(2, 1); ITEM(2, 2);
ITEM(3, 1); ITEM(3, 2);
ITEM(4, 1); ITEM(4, 2);
#undef ITEM
qWinMain.setLayout(&qGrid);
qWinMain.addLink(1, 8);
qWinMain.addLink(5, 2);
qWinMain.show();
// runtime loop
return app.exec();
}
Output:
Qt Version: 5.15.1

Qt update TableView every x seconds

I have a QAbstractTableModel that has a list of custom items it displays, this TableModel is the model of a TableView. How can i refresh the TableView every x seconds? I tried beginInsertRows and endInsertRows, but this caused laggs because im editing too many items per second, so i only want to refresh it every x seconds.
use QTimer
for example,
QTimer *timer = new QTimer(this);
connect(timer, SIGNAL(timeout()), this, SLOT(processOneThing()));
timer->start(1000);
in processOneThing , you can write the code of refreshing data and set the timer again using timer->start(1000);
Using beginInsertRows and endInsertRows could potentially cause a whole lot of reorgansation internally that is not necessary. If the structure of the model (i.e. ordering, number of items,etc.) doesn't change, only the display content, you're far better off emitting the dataChanged signal.
That signal tells the connected views to refresh, and they'll only redraw the items that are visible within the viewport, they don't process items that are hidden.
#include <QtWidgets/QApplication>
#include <QtCore/qtimer.h>
#include <QtWidgets/qtableview.h>
#include <QtCore/QAbstractTableModel>
class TableModel : public QAbstractTableModel {
public:
TableModel(QObject *parent = nullptr) : QAbstractTableModel(parent) {
connect(&timer, &QTimer::timeout, [=]() {
emit dataChanged(index(0, 0), index(rowCount() - 1, columnCount() - 1));
});
timer.start(1000);
}
virtual int rowCount(QModelIndex const &index = QModelIndex()) const { return index.parent().isValid() ? 0 : 5; }
virtual int columnCount(QModelIndex const &index = QModelIndex()) const { return index.parent().isValid() ? 0 : 10; }
virtual QVariant data(QModelIndex const &index, int role = Qt::DisplayRole) const {
QVariant value;
if (index.isValid() && role == Qt::DisplayRole) {
value = QString("X %1; Y: %2").arg(qrand()).arg(qrand());
}
return value;
}
private:
QTimer timer;
};
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
TableModel model;
QTableView view;
view.setModel(&model);
view.show();
return a.exec();
}

How to immediately detect checkbox state change in a QTreeView?

I'm using Qt 5.9 on linux. I have a QTreeView where I set a custom model which is derived from QAbstractItemModel and I fill the tree with several plain classes for the tree items. Each treeview item has a checkbox next to it.
I have the following attribute set on the QTreeView ...
treeView->viewport()->setAttribute(Qt::WA_Hover);
so that when the mouse hovers over a tree item, I can capture the event via my delegates paint method.
The problem is that I also have a checkbox in each tree item and I'd like to be able to capture when the state of the checkbox changes, but my delegate doesn't seem to capture that. I can tell what state the checkbox is in when I hover the mouse over the item, but what I want is to be able to immediately know when the state of the checkbox changes w/o having to move the mouse any further.
Any thoughts on how to immediately detect when the state of the checkbox changes in a tree item?
A possible solution is to track the state change of the checkbox using the editorEvent method:
#include <QtWidgets>
class CheckboxDelegate: public QStyledItemDelegate{
Q_OBJECT
public:
using QStyledItemDelegate::QStyledItemDelegate;
bool editorEvent(QEvent *event,
QAbstractItemModel *model,
const QStyleOptionViewItem &option,
const QModelIndex &index) override
{
Qt::CheckState last = static_cast<Qt::CheckState>(model->data(index, Qt::CheckStateRole).toInt());
bool res = QStyledItemDelegate::editorEvent(event, model, option, index);
Qt::CheckState current = static_cast<Qt::CheckState>(model->data(index, Qt::CheckStateRole).toInt());
if(last != current)
Q_EMIT stateChanged(index);
return res;
}
Q_SIGNALS:
void stateChanged(const QModelIndex & index);
};
#include "main.moc"
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
QStandardItemModel model;
model.setColumnCount(2);
for(int i=0; i<4; ++i){
QList<QStandardItem *> l;
for (int c=0; c< model.columnCount(); ++c) {
QStandardItem *parent = new QStandardItem(QString("%1-%2").arg(i).arg(c));
parent->setCheckable(true);
l << parent;
for (int j=0; j<4; ++j) {
QList<QStandardItem *> ll;
for (int c=0; c< model.columnCount(); ++c) {
QStandardItem *child = new QStandardItem(QString("%1-%2-%3").arg(i).arg(j).arg(c));
child->setCheckable(true);
ll << child;
}
parent->appendRow(ll);
}
}
model.appendRow(l);
}
QTreeView w;
w.viewport()->setAttribute(Qt::WA_Hover);
CheckboxDelegate *delegate = new CheckboxDelegate(&w);
w.setItemDelegate(delegate);
QObject::connect(delegate, &CheckboxDelegate::stateChanged, [](const QModelIndex & index){
QString text = index.data().toString();
Qt::CheckState state = static_cast<Qt::CheckState>(index.data(Qt::CheckStateRole).toInt());
qDebug() << text << state;
});
w.setModel(&model);
w.resize(640, 480);
w.expandAll();
w.show();
return a.exec();
}

How display multiple values in QLabel in Qcreator

I am developing calculator using Qt SDK I want to get values from the calculator buttons in to one Qlabel. I have designed GUI using design view I tried to get values using following code.
void MainWindow::on_three_button_clicked(){
ui->label->setText("3");
}
but then Only I can get one button value at a time
please help me on this...
You can have one QString variable to store current pressed value. Here I am considering your class variable defined as QString myEnteredNum. Then you can use it as below code:
void MainWindow::on_three_button_clicked(){
myEnteredNum.append("3"); // This method you can add in each number slot
ui->label->setText(myEnteredNum);
}
I hope this helps you.
There are two general approaches to this. You can append the digits directly to a number, and bind the label to the number. The m_first label below is updated that way. You can also append the digits directly to the label. The m_second label is updated so.
#include <QtWidgets>
class Window : public QWidget {
Q_OBJECT
Q_PROPERTY(double number MEMBER m_number WRITE setNumber NOTIFY numberChanged)
double m_number;
QGridLayout m_grid;
QLabel m_first, m_second;
QPushButton m_digits[10], m_clear;
public:
Window(QWidget * parent = 0) : QWidget(parent),
m_grid(this), m_clear("C") {
m_grid.addWidget(&m_first, 0, 0, 1, 3);
m_grid.addWidget(&m_second, 1, 0, 1, 3);
m_grid.addWidget(&m_clear, 5, 2);
for (int i = 0; i < 9; ++i) {
m_digits[i].setText(QString::number(i));
if (i > 0) m_grid.addWidget(m_digits + i, 2 + (i-1)/3, (i-1)%3);
else m_grid.addWidget(m_digits + i, 5, 1);
QObject::connect(m_digits + i, &QPushButton::clicked, [i, this]{
// Add the digit to the number
setNumber(m_number * 10.0 + i);
// Add the digit to the label
m_second.setText(m_second.text().append(QString::number(i)));
});
}
QObject::connect(&m_clear, &QPushButton::clicked, [this]{
setNumber(0);
m_second.setText("0");
});
QObject::connect(this, &Window::numberChanged,
&m_second, &QLabel::setNum);
emit m_clear.clicked(); // initialize everything
}
Q_SIGNAL void numberChanged(double);
void setNumber(double n) { m_number = n; emit numberChanged(n); }
};
int main(int argc, char ** argv) {
QApplication app(argc, argv);
Window window;
window.show();
return app.exec();
}
#include "main.moc"
Finally, a calculator is stateful and, to avoid common mistakes, you could use a QStateMachine to make this fact explicit, and to adjust the behavior of the calculator depending on which state it's in.

Trouble displaying sequence of images with setPixmap in Qlabel

I'm trying to display a sequence of images through a Qlabel using setPixmap. I have a QStringList containing the image file names and a for loop which iterates through the images with a 5 second wait after each image. However, only the last image file is ever being displayed. Currently the screen remains blank during the wait of the first iterations until the last image is finally shown. I've read that using a for loop wont work and that I should be using signals and slots instead. I'm new to this concept though and I would really appreciate an example to point me in the right direction.
Here is my current code:
MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent),ui(new Ui::MainWindow){
ui->setupUi(this);
QStringList images;
QString imageName;
images << "redScreen.png" << "blueScreen.png" << "greenScreen.png";
for(int x=0; x < images.size(); x++){
imageName = images.at(x);
this->displayScreen(imageName, 5);
}
}
void MainWindow::displayScreen(QString imageName, int wait){
QTimer t;
QEventLoop loop;
QPixmap myImage;
myImage.load(":/images/" + imageName);
ui->imageLabel->setPixmap(myImage);
ui->imageLabel->repaint();
// 5 second wait between next iteration
t.start(wait*1000);
connect(&t, SIGNAL(timeout()), &loop, SLOT(quit()));
loop.exec();
}
The reentrant wait-via-eventloop hack is a great source of hard-to-diagnose bugs. Don't use it. It's very, very rare that you'll need to instantiate your own event loop. Even rather complex projects can entirely avoid it.
You should simply run a timer and react to timer ticks. Here's one example:
#include <QApplication>
#include <QImage>
#include <QGridLayout>
#include <QLabel>
#include <QBasicTimer>
class Widget : public QWidget {
QGridLayout m_layout;
QLabel m_name, m_image;
QStringList m_images;
QStringList::const_iterator m_imageIt;
QBasicTimer m_timer;
void timerEvent(QTimerEvent * ev) {
if (ev->timerId() == m_timer.timerId()) tick();
}
void tick() {
display(*m_imageIt);
m_imageIt ++;
const bool loop = false;
if (m_imageIt == m_images.end()) {
if (loop)
m_imageIt = m_images.begin();
else
m_timer.stop();
}
}
void display(const QString & imageName) {
QImage img(":/images/" + imageName);
m_name.setText(imageName);
m_image.setPixmap(QPixmap::fromImage(img));
}
public:
Widget(QWidget * parent = 0) : QWidget(parent), m_layout(this) {
m_images << "redScreen.png" << "blueScreen.png" << "greenScreen.png";
m_imageIt = m_images.begin();
m_layout.addWidget(&m_name, 0, 0);
m_layout.addWidget(&m_image, 1, 0);
tick();
m_timer.start(5000, Qt::CoarseTimer, this);
}
};
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
Widget w;
w.show();
return a.exec();
}
A code similar to the below must work for the task you mentioned. ( needs cleaning/class organization though )
QTimer timer;
int x=0;
QStringList images;
QString imageName;
MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent),ui(new Ui::MainWindow){
ui->setupUi(this);
images << "redScreen.png" << "blueScreen.png" << "greenScreen.png";
connect( &timer, SIGNAL(timeout()), this, SLOT(ChangeImageSlot()) );
timer.start(5000);
}
void ChangeImageSlot()
{
imageName = images.at(x++);
this->displayScreen(imageName, 5);
if( x < images.size() )
timer.start(5000);
}
Best solution is DeepBlack with a couple of QTimer, but if you want at you risk you can try insert an processEvent() function inside the for loop of display image.

Resources