Making my own photo-mosaic app with Qt using C++ - c++

This is the output of what I have done till now:
Although this shows only black and white images, the code works for color images too. The code basically populates the larger image using smaller images from a small database.
So this is where I am stuck, rather clueless.
How can I get an effect like this one. or this one.
Qn1 : I need to take another input(large) image*(One for which the effect is to be seen)* and merge them, but how?
Qn2 : How can I evaluate the goodness of the photo-mosaic? I have a genetic algorithm written for this but am unable to fix the fitness function,(mutation and crossover work perfectly).
This is what I could think of(for Qn1):
1. Take alternate pixels of the image shown above and the image for which the mosaic has to be made.
2. Take average of the pixel values of the above and input image for which the mosaic has to be made.
But have no clue to evaluate the goodness.

Below is a self contained sketch. The mosaicing algorithm is mid-way through the algorithms implemented in an excellent reference. It works well enough for two hours of work, I think. I tried for the code to be reasonably correct, with two caveats, left, as they say, as an exercise to the reader.
I'm not tracking the worker threads - if you try to exit the application while workers are active, it is expected to crash on exit. This is not nice, but otherwise benign and doesn't affect the overall functionality. There may be a few corrupt images left on disk, but those should be ignored when reloading.
There is no scaling of the image displayed in the label. The window will resize to the image size.
The tile image database can be filled with random images from imgur, you can also fill it with your own images by storing them on disk yourself. It's located in a standard application data path suffixed by /so-photomosaic/image. The fetched images are added there. Upon startup, the image database is repopulated from disk in the background - that's how your own tile images would be loaded. In fact, all of image processing is done in non-GUI threads. On a rather unassuming 5 year old Core 2 OS X system, disk image loading proceeds at about 5000 images/s. The images requested from imgur are their small size, or 90x90.
The tile matching is done with a 4x4 subdivision grid (divs parameter to calcPropsFor). The images are downsampled to a 4x4 mosaic, and the RGB color values of consecutive pixels in that grid are stored in Props vectors. The squared sums of differences of elements of those vectors are the measure of fit. For each tile to be replaced, the images are sorted according to their fit, and one of the best ones are picked up at random. The randomness parameter is a power-of-to of the sample size from which the image is randomly selected.
It uses Qt 5 and C++11. Length: 300 lines, out of which 64 are the random image source, 25 are the disk image database, and 88 are actually to do with mosaics. The image processing code would probably look and perform better if OpenCV or Eigen was used instead of valarray/QImage, but oh well.
Also, all of this would be probably 50 lines in Mathematica :)
# main.pro
# Make sure to re-run quake once this is set.
TEMPLATE = app
QT += widgets network concurrent
CONFIG += c++11
SOURCES += main.cpp
TARGET = photomosaic
#include <QApplication>
#include <QLabel>
#include <QSlider>
#include <QPushButton>
#include <QCheckBox>
#include <QBoxLayout>
#include <QFileDialog>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QRegularExpression>
#include <QImage>
#include <QPainter>
#include <QColor>
#include <QAtomicInt>
#include <QMutex>
#include <QtConcurrent>
#include <QStandardPaths>
#include <algorithm>
#include <functional>
#include <valarray>
/// Provides random images. There may be more than one response per request.
class RandomImageSource : public QObject {
Q_OBJECT
int m_parallelism;
bool m_auto;
QNetworkAccessManager m_mgr;
QSet<QNetworkReply*> m_replies;
QList<QUrl> m_deferred;
QRegularExpression m_imgTagRE, m_imgUrlRE;
QUrl m_randomGallery;
void get(const QUrl & url) {
if (m_replies.count() < m_parallelism) {
QNetworkRequest req(url);
req.setAttribute(QNetworkRequest::HttpPipeliningAllowedAttribute, true);
m_replies.insert(m_mgr.get(req));
} else
m_deferred << url;
}
void finishReply(QNetworkReply * reply) {
m_replies.remove(reply);
if (reply) reply->deleteLater();
if (! m_deferred.isEmpty()) get(m_deferred.takeLast());
while (m_deferred.isEmpty() && m_auto) get(m_randomGallery);
}
Q_SLOT void rsp(QNetworkReply * reply) {
auto loc = reply->header(QNetworkRequest::LocationHeader);
if (loc.isValid()) {
get(loc.toUrl()); // redirect
} else {
auto ct = reply->header(QNetworkRequest::ContentTypeHeader).toString();
if (ct.startsWith("text/html"))
foreach (QUrl url, parseImageUrls(reply->readAll()))
get(url);
else if (ct.startsWith("image")) {
auto img = QImage::fromData(reply->readAll());
img.setText("filename", m_imgUrlRE.match(reply->url().toString()).captured(1));
if (!img.isNull()) emit rspImage(img);
}
}
finishReply(reply);
}
QList<QUrl> parseImageUrls(const QByteArray & html) {
QList<QUrl> urls;
auto it = m_imgTagRE.globalMatch(QString::fromUtf8(html));
while (it.hasNext()) { auto match = it.next(); // get small images
urls << QUrl("http:" + match.captured(1) + "s" + match.captured(2)); }
return urls;
}
public:
RandomImageSource(QObject * parent = 0) : QObject (parent),
m_parallelism(20), m_auto(false),
m_imgTagRE("<img src=\"(//i\\.imgur\\.com/[^.]+)(\\.[^\"]+)\""),
m_imgUrlRE("http://i\\.imgur\\.com/(.+)$"),
m_randomGallery("http://imgur.com/gallery/random")
{
connect(&m_mgr, SIGNAL(finished(QNetworkReply*)), SLOT(rsp(QNetworkReply*)));
}
Q_SLOT void reqImages(int count) {
while (count--) get(m_randomGallery);
}
Q_SIGNAL void rspImage(const QImage &);
bool automatic() const { return m_auto; }
Q_SLOT void setAutomatic(bool a) { if ((m_auto = a)) finishReply(0); }
int parallelism() const { return m_parallelism; }
Q_SLOT void setParallelism(int p) { m_parallelism = p; if (m_auto) finishReply(0); }
};
/// Stores images on disk, and loads them in the background.
class ImageStorage : public QObject {
Q_OBJECT
QString const m_path;
public:
ImageStorage() :
m_path(QStandardPaths::writableLocation(QStandardPaths::DataLocation)
+ "/images/")
{ QDir().mkpath(m_path); }
Q_SLOT void addImage(const QImage & img) {
QString path = img.text("filename");
if (path.isEmpty()) return;
path.prepend(m_path);
QtConcurrent::run([img, path]{ img.save(path); });
}
Q_SLOT void retrieveAll() {
QString const path = m_path;
QtConcurrent::run([this, path] {
QStringList const images = QDir(path).entryList(QDir::Files);
foreach (QString image, images) QtConcurrent::run([this, image, path] {
QImage img; if (img.load(path + image)) emit retrieved(img);
});
});
}
Q_SIGNAL void retrieved(const QImage &);
};
/// A memory database of images. Finds best match to a given image.
class ImageDatabase : public QObject {
Q_OBJECT
typedef std::valarray<qreal> Props;
typedef QPair<QImage, Props> ImageProps;
QMutex mutable m_mutex;
QList<ImageProps> m_images;
static void inline addProps(Props & p, int i, QRgb rgb) {
QColor const c = QColor::fromRgb(rgb);
p[i+0] += c.redF(); p[i+1] += c.greenF(); p[i+2] += c.blueF();
}
static Props calcPropsFor(const QImage & img, int divs = 4) {
Props props(0.0, 3 * divs * divs);
std::valarray<int> counts(0, divs * divs);
QSize div = img.size() / divs;
for (int y = 0; y < img.height(); ++y)
for (int x = 0; x < img.width(); ++x) {
int slice = x/div.width() + (y*divs/div.height());
if (slice >= divs*divs) continue;
addProps(props, slice*3, img.pixel(x, y));
counts[slice] ++;
}
for (size_t i = 0; i < props.size(); ++i) props[i] /= counts[i/3];
return props;
}
public:
Q_SIGNAL void newImageCount(int);
Q_SLOT void addImage(const QImage & img) {
QtConcurrent::run([this, img]{
Props props = calcPropsFor(img);
QMutexLocker lock(&m_mutex);
m_images << qMakePair(img, props);
int count = m_images.count();
lock.unlock();
emit newImageCount(count);
});
}
ImageProps bestMatchFor(const QImage & img, int randLog2) const {
QMutexLocker lock(&m_mutex);
QList<ImageProps> const images = m_images;
lock.unlock();
Props const props = calcPropsFor(img);
typedef QPair<qreal, const ImageProps *> Match;
QList<Match> matches; matches.reserve(images.size());
std::transform(images.begin(), images.end(), std::back_inserter(matches),
[props](const ImageProps & prop){
return qMakePair(pow(props - prop.second, 2).sum(), &prop);
});
std::sort(matches.begin(), matches.end(),
[](Match a, Match b) { return b.first < a.first; });
randLog2 = 1<<randLog2;
return *(matches.end()-randLog2+qrand()%randLog2)->second;
}
};
QImage getMosaic(QImage img, const ImageDatabase & db, int size, int randLog2)
{
QPainter p(&img);
for (int y = 0; y < img.height(); y += size)
for (int x = 0; x < img.width(); x += size) {
QImage r = db.bestMatchFor(img.copy(x, y, size, size), randLog2).first
.scaled(size, size, Qt::KeepAspectRatio, Qt::SmoothTransformation);
p.drawImage(x, y, r);
}
return img;
}
class MosaicGenerator : public QObject {
Q_OBJECT
QPointer<ImageDatabase> m_db;
int m_size, m_randLog2;
QAtomicInt m_busy;
QImage m_image;
void update() {
if (m_image.isNull() || m_busy.fetchAndAddOrdered(1)) return;
QImage image = m_image;
QtConcurrent::run([this, image]{ while (true) {
emit hasMosaic(getMosaic(image, *m_db, m_size, m_randLog2));
if (m_busy.testAndSetOrdered(1, 0)) return;
m_busy.fetchAndStoreOrdered(1);
}});
}
public:
MosaicGenerator(ImageDatabase * db) : m_db(db), m_size(16), m_randLog2(0) {}
Q_SLOT void setImage(const QImage & img) { m_image = img; update(); }
Q_SLOT void setSize(int s) { m_size = s; update(); }
Q_SLOT void setRandLog2(int r) { m_randLog2 = r; update(); }
Q_SIGNAL void hasMosaic(const QImage &);
};
class Window : public QWidget {
Q_OBJECT
bool m_showSource;
QImage m_source, m_mosaic;
QBoxLayout m_layout;
QSlider m_parallelism, m_cellSize, m_randomness;
QLabel m_imgCount, m_parCount, m_image;
QPushButton m_add, m_load, m_toggle;
MosaicGenerator m_gen;
Q_SIGNAL void newSource(const QImage &);
void updateImage() {
const QImage & img = m_showSource ? m_source : m_mosaic;
m_image.setPixmap(QPixmap::fromImage(img));
}
public:
Window(ImageDatabase * db, QWidget * parent = 0) : QWidget(parent),
m_showSource(true), m_layout(QBoxLayout::TopToBottom, this),
m_parallelism(Qt::Horizontal), m_cellSize(Qt::Horizontal),
m_randomness(Qt::Horizontal), m_add("Fetch Images"),
m_load("Open for Mosaic"), m_toggle("Toggle Mosaic"), m_gen(db)
{
QBoxLayout * row = new QBoxLayout(QBoxLayout::LeftToRight);
row->addWidget(new QLabel("Images in DB:"));
row->addWidget(&m_imgCount);
row->addWidget(new QLabel("Fetch parallelism:"));
row->addWidget(&m_parallelism);
row->addWidget(&m_parCount);
row->addWidget(&m_add);
m_parallelism.setRange(1, 100);
m_layout.addLayout(row);
m_layout.addWidget(&m_image);
row = new QBoxLayout(QBoxLayout::LeftToRight);
row->addWidget(new QLabel("Cell Size:"));
row->addWidget(&m_cellSize);
row->addWidget(new QLabel("Randomness:"));
row->addWidget(&m_randomness);
m_cellSize.setRange(4, 64); m_cellSize.setTracking(false);
m_randomness.setRange(0,6); m_randomness.setTracking(false);
m_layout.addLayout(row);
row = new QBoxLayout(QBoxLayout::LeftToRight);
row->addWidget(&m_load);
row->addWidget(&m_toggle);
m_layout.addLayout(row);
m_add.setCheckable(true);
m_parCount.connect(&m_parallelism, SIGNAL(valueChanged(int)), SLOT(setNum(int)));
connect(&m_add, SIGNAL(clicked(bool)), SIGNAL(reqAutoFetch(bool)));
connect(&m_parallelism, SIGNAL(valueChanged(int)), SIGNAL(reqParallelism(int)));
m_gen.connect(&m_cellSize, SIGNAL(valueChanged(int)), SLOT(setSize(int)));
m_gen.connect(&m_randomness, SIGNAL(valueChanged(int)), SLOT(setRandLog2(int)));
m_parallelism.setValue(20);
m_cellSize.setValue(16);
m_randomness.setValue(4);
connect(&m_load, &QPushButton::clicked, [this]{
QString file = QFileDialog::getOpenFileName(this);
QtConcurrent::run([this, file]{
QImage img; if (!img.load(file)) return;
emit newSource(img);
});
});
connect(this, &Window::newSource, [this](const QImage &img){
m_source = m_mosaic = img; updateImage(); m_gen.setImage(m_source);
});
connect(&m_gen, &MosaicGenerator::hasMosaic, [this](const QImage &img){
m_mosaic = img; updateImage();
});
connect(&m_toggle, &QPushButton::clicked, [this]{
m_showSource = !m_showSource; updateImage();
});
}
Q_SLOT void setImageCount(int n) { m_imgCount.setNum(n); }
Q_SIGNAL void reqAutoFetch(bool);
Q_SIGNAL void reqParallelism(int);
};
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
a.setOrganizationDomain("stackoverflow.com");
a.setApplicationName("so-photomosaic");
RandomImageSource src;
ImageDatabase db;
ImageStorage stg;
Window ui(&db);
db.connect(&src, SIGNAL(rspImage(QImage)), SLOT(addImage(QImage)));
stg.connect(&src, SIGNAL(rspImage(QImage)), SLOT(addImage(QImage)));
db.connect(&stg, SIGNAL(retrieved(QImage)), SLOT(addImage(QImage)));
ui.connect(&db, SIGNAL(newImageCount(int)), SLOT(setImageCount(int)));
src.connect(&ui, SIGNAL(reqAutoFetch(bool)), SLOT(setAutomatic(bool)));
src.connect(&ui, SIGNAL(reqParallelism(int)), SLOT(setParallelism(int)));
stg.retrieveAll();
ui.show();
return a.exec();
}
#include "main.moc"

Related

Rotate QImage in QListWidget

Got a small app where in a QListWidget many QImage and text are displayed with this function:
void TileDisplay::DisplayTiles()
{
for(int i = 0; i < m_tiles.size(); i++) {
QListWidgetItem *item = new QListWidgetItem("Tile " + QString::number(i+1), ui->listWidget);
item->setData(Qt::DecorationRole, m_tiles[i].scaled(64, 64, Qt::IgnoreAspectRatio, Qt::FastTransformation));
}
}
I'm looking for a solution where the QImage is rotated around it's centre with this function:
void TileDisplay::RotateImage(int degree)
{
if(GetTiles().size() > 0) {
QImage *tileToRotate = GetCurrentTile();
if(tileToRotate != nullptr) {
QTransform rotate;
rotate.rotate(degree);
tileToRotate->transformed(rotate);
DisplayTiles();
}
}
}
It is running without any issue, but the image is not rotated.
Any help appreciated.
From the documentation the signature of the QImage::transformed member function is...
QImage QImage::transformed(const QTransform &matrix, Qt::TransformationMode mode) const;
So you need to make use of the returned QImage. Try...
*tileToRotate = tileToRotate->transformed(rotate);

Push QML ChartView updates from c++

I'm trying to adapt the Qt5.9 QML Oscilloscope example to have the graph data pushed from c++ rather than requested from QML. Below are the pertinent sections from the QML Oscilloscope example.
datasource.h:
#ifndef DATASOURCE_H
#define DATASOURCE_H
#include <QtCore/QObject>
#include <QtCharts/QAbstractSeries>
QT_BEGIN_NAMESPACE
class QQuickView;
QT_END_NAMESPACE
QT_CHARTS_USE_NAMESPACE
class DataSource : public QObject
{
Q_OBJECT
public:
explicit DataSource(QQuickView *appViewer, QObject *parent = 0);
Q_SIGNALS:
public slots:
void generateData(int type, int rowCount, int colCount);
void update(QAbstractSeries *series);
private:
QQuickView *m_appViewer;
QList<QVector<QPointF> > m_data;
int m_index;
};
#endif // DATASOURCE_H
datasource.cpp:
#include "datasource.h"
#include <QtCharts/QXYSeries>
#include <QtCharts/QAreaSeries>
#include <QtQuick/QQuickView>
#include <QtQuick/QQuickItem>
#include <QtCore/QDebug>
#include <QtCore/QtMath>
QT_CHARTS_USE_NAMESPACE
Q_DECLARE_METATYPE(QAbstractSeries *)
Q_DECLARE_METATYPE(QAbstractAxis *)
DataSource::DataSource(QQuickView *appViewer, QObject *parent) :
QObject(parent),
m_appViewer(appViewer),
m_index(-1)
{
qRegisterMetaType<QAbstractSeries*>();
qRegisterMetaType<QAbstractAxis*>();
generateData(0, 5, 1024);
}
void DataSource::update(QAbstractSeries *series)
{
if (series) {
QXYSeries *xySeries = static_cast<QXYSeries *>(series);
m_index++;
if (m_index > m_data.count() - 1)
m_index = 0;
QVector<QPointF> points = m_data.at(m_index);
// Use replace instead of clear + append, it's optimized for performance
xySeries->replace(points);
}
}
void DataSource::generateData(int type, int rowCount, int colCount)
{
// Remove previous data
m_data.clear();
// Append the new data depending on the type
for (int i(0); i < rowCount; i++) {
QVector<QPointF> points;
points.reserve(colCount);
for (int j(0); j < colCount; j++) {
qreal x(0);
qreal y(0);
switch (type) {
case 0:
// data with sin + random component
y = qSin(3.14159265358979 / 50 * j) + 0.5 + (qreal) rand() / (qreal) RAND_MAX;
x = j;
break;
case 1:
// linear data
x = j;
y = (qreal) i / 10;
break;
default:
// unknown, do nothing
break;
}
points.append(QPointF(x, y));
}
m_data.append(points);
}
}
main.cpp:
#include <QtWidgets/QApplication>
#include <QtQml/QQmlContext>
#include <QtQuick/QQuickView>
#include <QtQml/QQmlEngine>
#include <QtCore/QDir>
#include "datasource.h"
int main(int argc, char *argv[])
{
// Qt Charts uses Qt Graphics View Framework for drawing, therefore
QApplication must be used.
QApplication app(argc, argv);
QQuickView viewer;
// The following are needed to make examples run without having to install the module
// in desktop environments.
#ifdef Q_OS_WIN
QString extraImportPath(QStringLiteral("%1/../../../../%2"));
#else
QString extraImportPath(QStringLiteral("%1/../../../%2"));
#endif
viewer.engine()->addImportPath(extraImportPath.arg(QGuiApplication::applicationDirPath(),
QString::fromLatin1("qml")));
//QObject::connect(viewer.engine(), &QQmlEngine::quit, &viewer, &QWindow::close);
viewer.setTitle(QStringLiteral("QML Oscilloscope"));
DataSource dataSource(&viewer);
viewer.rootContext()->setContextProperty("dataSource", &dataSource);
viewer.setSource(QUrl("qrc:/qml/qmloscilloscope/main.qml"));
viewer.setResizeMode(QQuickView::SizeRootObjectToView);
viewer.setColor(QColor("#404040"));
viewer.show();
return app.exec();
}
ScopeView.qml:
import QtQuick 2.0
import QtCharts 2.1
ChartView {
id: chartView
animationOptions: ChartView.NoAnimation
theme: ChartView.ChartThemeDark
property bool openGL: true
property bool openGLSupported: true
onOpenGLChanged: {
if (openGLSupported) {
series("signal 1").useOpenGL = openGL;
}
}
Component.onCompleted: {
if (!series("signal 1").useOpenGL) {
openGLSupported = false
openGL = false
}
}
ValueAxis {
id: axisY1
min: -1
max: 4
}
ValueAxis {
id: axisX
min: 0
max: 1024
}
LineSeries {
id: lineSeries1
name: "signal 1"
axisX: axisX
axisY: axisY1
useOpenGL: chartView.openGL
}
Timer {
id: refreshTimer
interval: 1 / 60 * 1000 // 60 Hz
running: true
repeat: true
onTriggered: {
dataSource.update(chartView.series(0));
}
}
}
Rather than using the Timer in QML, I'd like to use an existing Timeout in a c++ class to push new data to the QML ChartView. I have two questions:
How would I achieve this for the QML Oscilloscope example posted above?
What format would be most suitable for the c++ data to facilitate this? I'm thinking a QVector of some sort; the data will be an integer or float with a vector index.
As you say in a comment you need to pass a series, then we create a method that receives the series and saves it in a member of the C ++ class, We also create a QTimer, and we do the same to update the interval:
*.h
public:
Q_INVOKABLE void setSeries(QAbstractSeries *series);
Q_INVOKABLE void setInterval(int interval);
[...]
private:
QXYSeries *mSeries;
QTimer *timer;
[...]
*.cpp
void DataSource::setSeries(QAbstractSeries *series)
{
if (series) {
mSeries = static_cast<QXYSeries *>(series);
}
}
Then we remove the update argument and use mSeries:
DataSource::DataSource(QQuickView *appViewer, QObject *parent) :
QObject(parent),
m_appViewer(appViewer),
m_index(-1)
{
[...]
timer = new QTimer(this);
connect(timer, &QTimer::timeout, this, &DataSource::update);
timer->start(1 / 60 * 1000 );
}
void DataSource::update()
{
if (mSeries) {
m_index++;
if (m_index > m_data.count() - 1)
m_index = 0;
QVector<QPointF> points = m_data.at(m_index);
// Use replace instead of clear + append, it's optimized for performance
mSeries->replace(points);
}
}
void DataSource::setInterval(int interval)
{
if(timer){
if(timer->isActive())
timer->stop();
timer->start(interval);
}
}
*.qml
Component.onCompleted: {
dataSource.setSeries(chartView.series(0));
if (!series("signal 1").useOpenGL) {
openGLSupported = false
openGL = false
}
}
[...]
function changeRefreshRate(rate) {
dataSource.setInterval(1 / Number(rate) * 1000);
//refreshTimer.interval = 1 / Number(rate) * 1000;
}
You can find the complete example in the following link.

Rendering a section of a non-smooth QImage using QSGImageNode

I'm trying to render individual tiles from a tileset. For example, I want to display the grey tile in the tileset below:
In the real use case, these would be e.g. water, grass, etc. tiles in a game. There are some requirements for rendering these tiles:
They are 32x32 pixels and will be rendered fullscreen, so performance is important.
They should not be smoothed when scaled.
None of the built-in Qt Quick types meet these requirements (rendering a section of an image that's not smoothed), as far as I can tell. I've tried QQuickPaintedItem with various QPainter render hints (such as SmoothPixmapTransform set to false) without success; the image is "blurry" when upscaled. AnimatedSprite supports rendering sections of an image, but has no API to disable smoothing.
My idea was to implement a custom QQuickItem using the scene graph API.
main.cpp:
#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include <QQuickItem>
#include <QQuickWindow>
#include <QSGImageNode>
static QImage image;
static const int tileSize = 32;
static const int tilesetSize = 8;
class Tile : public QQuickItem
{
Q_OBJECT
Q_PROPERTY(int index READ index WRITE setIndex NOTIFY indexChanged)
public:
Tile() :
mIndex(-1) {
setWidth(tileSize);
setHeight(tileSize);
setFlag(QQuickItem::ItemHasContents);
}
QSGNode *updatePaintNode(QSGNode *oldNode, UpdatePaintNodeData *)
{
if (!oldNode) {
oldNode = window()->createImageNode();
}
if (mIndex == -1)
return oldNode;
if (image.isNull()) {
image = QImage("C:/tileset.png");
if (image.isNull())
return oldNode;
}
QSGTexture *texture = window()->createTextureFromImage(image);
qDebug() << "textureSize:" << texture->textureSize();
if (!texture)
return oldNode;
QSGImageNode *imageNode = static_cast<QSGImageNode*>(oldNode);
// imageNode->setOwnsTexture(true);
imageNode->setTexture(texture);
qDebug() << "source rect:" << (mIndex % tileSize) * tileSize << (mIndex / tileSize) * tileSize << tileSize << tileSize;
imageNode->setSourceRect((mIndex % tileSize) * tileSize, (mIndex / tileSize) * tileSize, tileSize, tileSize);
return oldNode;
}
int index() const {
return mIndex;
}
void setIndex(int index) {
if (index == mIndex)
return;
mIndex = index;
emit indexChanged();
}
signals:
void indexChanged();
private:
int mIndex;
};
int main(int argc, char *argv[])
{
QGuiApplication app(argc, argv);
qmlRegisterType<Tile>("App", 1, 0, "Tile");
QQmlApplicationEngine engine;
engine.load(QUrl(QStringLiteral("qrc:/main.qml")));
return app.exec();
}
#include "main.moc"
main.qml:
import QtQuick 2.9
import QtQuick.Controls 2.2
import App 1.0
ApplicationWindow {
id: window
width: 800
height: 800
visible: true
Slider {
id: slider
from: 1
to: 10
}
Tile {
scale: slider.value
index: 1
anchors.centerIn: parent
Rectangle {
anchors.fill: parent
color: "transparent"
border.color: "darkorange"
}
}
}
The output from this application looks fine, but nothing is rendered within the rectangle:
textureSize: QSize(256, 256)
source rect: 32 0 32 32
Judging from the minimal docs, my implementation (in terms of how I create nodes) seems OK. Where am I going wrong?
A year late to the party, but I was running into the same problem as you. For the sake of anyone else who is trying to subclass a QQuickItem and has come across this thread, there's a little nugget that's in the documentation in regards to updatePaintNode:
The function is called as a result of QQuickItem::update(),
if the user has set the QQuickItem::ItemHasContents flag on the item.
When I set that flag, everything rendered.
And I considered myself a detail-oriented person...
EDIT:
After the OP pointed out they had already set the ItemHasContents flag, I looked at the code again and saw that while the OP had set the sourceRect on the node, the OP hadn't set the rect of the node, and that indeed was the problem the OP was running into.
I ended up going with a friend's idea of using QQuickImageProvider:
tileimageprovider.h:
#ifndef TILEIMAGEPROVIDER_H
#define TILEIMAGEPROVIDER_H
#include <QQuickImageProvider>
#include <QHash>
#include <QString>
#include <QImage>
class TileImageProvider : public QQuickImageProvider
{
public:
TileImageProvider();
QImage requestImage(const QString &id, QSize *size, const QSize &requestedSize) override;
private:
QHash<QString, QImage> mTiles;
};
#endif // TILEIMAGEPROVIDER_H
tileimageprovider.cpp:
#include "tileimageprovider.h"
#include <QImage>
#include <QDebug>
TileImageProvider::TileImageProvider() :
QQuickImageProvider(QQmlImageProviderBase::Image)
{
QImage tilesetImage(":/sprites/tiles/tileset.png");
if (tilesetImage.isNull()) {
qWarning() << "Failed to load tileset image";
return;
}
int index = 0;
for (int row = 0; row < 8; ++row) {
for (int col = 0; col < 8; ++col) {
int sourceX = col * 32;
int sourceY = row * 32;
QImage subTile = tilesetImage.copy(sourceX, sourceY, 32, 32);
if (tilesetImage.isNull()) {
qWarning() << "Tile image at" << sourceX << sourceY << "is null";
return;
}
mTiles.insert(QString::number(index++), subTile);
}
}
}
QImage TileImageProvider::requestImage(const QString &id, QSize *size, const QSize &)
{
Q_ASSERT(mTiles.find(id) != mTiles.end());
*size = QSize(32, 32);
return mTiles.value(id);
}
I then create tile instances from the following Component:
Image {
width: 32
height: 32
smooth: false
asynchronous: true
source: "image://tile/" + index
property int index
}
There's also Tiled's tile rendering code, which uses scenegraph nodes and is likely more efficient.
It does seem like you can have it both easier and more efficient.
Instead of implementing a custom QQuickItem you can just use a trivial ShaderEffect. You can pass an offset and control the sampling.
Additionally, you would only need one single black and white plus, and you can have the color dynamic by passing it as a parameter to the shader.
Lastly, doing an atlas yourself is likely redundant, as the scenegraph will likely put small textures in atlases anyway. And of course, there is the advantage of not having to write a single line of C++, while still getting an efficient and flexible solution.

Qwt - replot does not clear current plot

This is my first question here and I believe that I havent seen anyone else asking for this specific problem but what I'm currently dealing with is a qwt plot that doesn't want to replot.
What I want to do: Call the replot() method to clear my current plot.
My problem: When calling replot() does not clear my current plot and my plots are drawn on top of eachother.
Here is a link to an image of my problem
As can be seen in the image, the new curves are drawn on top of the existing ones and this is what I want to solve.
Here is some of my code: (let me know if I missed some parts)
plot.cpp
#include "plot.h"
#include <qwt_plot.h>
#include <qwt_plot_grid.h>
#include <qwt_plot_layout.h>
#include <qwt_plot_canvas.h>
#include <qwt_plot_curve.h>
#include <qwt_plot_directpainter.h>
#include <qwt_curve_fitter.h>
#include <qwt_painter.h>
class CurveData: public QwtArraySeriesData<QPointF>
{
public:
CurveData()
{
}
virtual QRectF boundingRect() const
{
if ( d_boundingRect.width() < 0.0 )
d_boundingRect = qwtBoundingRect( *this );
return d_boundingRect;
}
inline void append( const QPointF &point )
{
d_samples += point;
}
void clear()
{
d_samples.clear();
d_samples.squeeze();
d_boundingRect = QRectF( 0.0, 0.0, -1.0, -1.0 );
}
};
Plot::Plot( QWidget *parent ):
QwtPlot( parent )
{
d_directPainter = new QwtPlotDirectPainter(this);
setAutoReplot(false);
setAxisScale(QwtPlot::yLeft, 0.014,0.016);
setAxisScale(QwtPlot::xBottom, 0, 1000);
d_curve = new QwtPlotCurve();
d_curve->setData(new CurveData());
d_curve->attach(this);
}
void Plot::AppendPoint(const QPointF &point)
{
CurveData *data = static_cast<CurveData *>(d_curve->data());
data->append(point);
}
void Plot::DrawCurveSegment()
{
CurveData *data = static_cast<CurveData *>(d_curve->data());
d_directPainter->drawSeries(d_curve, data->size()-11, data->size()-1);
}
void Plot::ClearPlot()
{
CurveData *data = static_cast<CurveData *>(d_curve->data());
data->clear();
QwtPlot::replot();
}
mainwindow.cpp
MainWindow::MainWindow(QWidget *parent):
QWidget( parent )
{
d_plot = new Plot();
counter = 0;
loopCounter = 0;
// ... //
}
void MainWindow::timerEvent(QTimerEvent *) {
if (counter>0 && counter%1000==0)
{
d_plot->ClearPlot();
d_plot->replot();
loopCounter++;
qDebug()<<"clear!";
}
for (int ii=0; ii<10;ii++)
{
double y = someArray[counter];
double x = (double)counter-((double)loopCounter*1000);
counter++;
d_plot->AppendPoint(QPointF(x,y));
}
d_plot->DrawCurveSegment();
}
Would very much appreciate if someone can see what I'm doing wrong.
Best regards!
I solved the problem by changing the clearPlot method to the following:
void Plot::ClearPlot()
{
CurveData *data = static_cast<CurveData *>(d_curve->data());
data->clear();
QwtPlot::replot();
QwtPlot::repaint();
}
You can also do this:
QwtPlot::detachItems(QwtPlotItem::Rtti_PlotItem, true);

QTableWidget : personal widget as header?

What is the easiest way (without subclassing QTableWidget if possible) to put a personnal Widget (which is a group of QPixmap + QLabel + QComboBox in my case) as headers in a QTableWidget ?
It is relatively easy to do it for the array content as we have the function QTableWidget::setCellWidget(int row, int column, QWidget * widget), but for the headers, we only have QTableWidget::setHorizontalHeaderItem (int column, QTableWidgetItem * item)
Thank you very much !
EDIT :
Maybe I've found a very unelegant way to do this that may work :
1) I initialize each column with an empty QString header
2) I extract the size and position of each section of the header with _table->horizontalHeader()->sectionPosition(index) and _table->horizontalHeader()->sectionSize(index)
3) Then I draw a QFrame onto each section with a little margin
4) And finally, I load my QWidgets in these QFrame
If any one else has a better solution, I'm interested...
I guess setCellWidget() will do the job. I don't see an easier way. Instead of a QFrame you might also use a QWidget, if you don't use the additional functionality of QFrame.
I researched a lot to create an excel like filter header.
Replacing header does not have a simple nor fast way.
Finaly I found this blog post: Qt Support Weekly #27 – Widgets on a header. Based on its guide, here is complete code to create a QHeaderView and QTableWidget subclass that can accept custom widget as header item:
customtableheader.h:
#ifndef CUSTOMTABLEHEADER_H
#define CUSTOMTABLEHEADER_H
#include <QWidget>
#include <QHeaderView>
class CustomTableHeader : public QHeaderView
{
public:
struct Margins
{
int left;
int right;
int top;
int buttom;
Margins(int left = 2, int right = 2, int top = 2, int buttom = 2);
};
CustomTableHeader(Qt::Orientation orientation,
QWidget *parent = nullptr);
void FixComboPositions();
void SetItemWidget(int index, QWidget * widget);
void SetItemMargins(int index, Margins margins);
private:
struct Item
{
QWidget * item;
Margins margins;
Item();
};
QMap<int, Item> mItems;
void showEvent(QShowEvent * e);
void HandleSectionResized(int i);
void HandleSectionMoved(int logical, int oldVisualIndex, int newVisualIndex);
};
#endif // CUSTOMTABLEHEADER_H
customtableheader.cpp:
#include "customtableheader.h"
CustomTableHeader::CustomTableHeader(Qt::Orientation orientation, QWidget * parent) :
QHeaderView(orientation, parent)
{
connect(this, &CustomTableHeader::sectionResized, this, &CustomTableHeader::HandleSectionResized);
connect(this, &CustomTableHeader::sectionMoved, this, &CustomTableHeader::HandleSectionMoved);
}
void CustomTableHeader::showEvent(QShowEvent *e)
{
for (int i = 0; i < count(); i++)
{
if (!mItems[i].item)
mItems[i].item = new QWidget(this);
else
mItems[i].item->setParent(this);
mItems[i].item->setGeometry(sectionViewportPosition(i) + mItems[i].margins.left,
mItems[i].margins.top,
sectionSize(i) - mItems[i].margins.left - mItems[i].margins.right - 1,
height() - mItems[i].margins.top - mItems[i].margins.buttom - 1);
mItems[i].item->show();
}
QHeaderView::showEvent(e);
}
void CustomTableHeader::HandleSectionResized(int i)
{
int logical;
for (int j = visualIndex(i); j < count(); j++)
{
logical = logicalIndex(j);
mItems[logical].item->setGeometry(sectionViewportPosition(logical) + mItems[i].margins.left,
mItems[i].margins.top,
sectionSize(logical) - mItems[i].margins.left - mItems[i].margins.right - 1,
height() - mItems[i].margins.top - mItems[i].margins.buttom - 1);
}
}
void CustomTableHeader::HandleSectionMoved(int logical, int oldVisualIndex, int newVisualIndex)
{
Q_UNUSED(logical);
for (int i = qMin(oldVisualIndex, newVisualIndex); i < count(); i++)
{
int logical = logicalIndex(i);
mItems[logical].item->setGeometry(sectionViewportPosition(logical) + mItems[i].margins.left,
mItems[i].margins.top,
sectionSize(logical) - mItems[i].margins.left - mItems[i].margins.right - 1,
height() - mItems[i].margins.top - mItems[i].margins.buttom - 1);
}
}
void CustomTableHeader::FixComboPositions()
{
for (int i = 0; i < count(); i++)
mItems[i].item->setGeometry(sectionViewportPosition(i) + mItems[i].margins.left,
mItems[i].margins.top,
sectionSize(i) - mItems[i].margins.left - mItems[i].margins.right - 1,
height() - mItems[i].margins.top - mItems[i].margins.buttom - 1);
}
void CustomTableHeader::SetItemWidget(int index, QWidget * widget)
{
widget->setParent(this);
mItems[index].item = widget;
}
void CustomTableHeader::SetItemMargins(int index, CustomTableHeader::Margins margins)
{
mItems[index].margins = margins;
}
CustomTableHeader::Margins::Margins(int left, int right, int top, int buttom) :
left(left),
right(right),
top(top),
buttom(buttom)
{
}
CustomTableHeader::Item::Item() :
item(nullptr)
{
}
customheaderedtable.h:
#ifndef CUSTOMHEADEREDTABLE_H
#define CUSTOMHEADEREDTABLE_H
#include <QtCore/qglobal.h>
#include <QTableWidget>
#include "customtableheader.h"
class CustomHeaderedTable : public QTableWidget
{
public:
CustomHeaderedTable(QWidget * parent = Q_NULLPTR);
void scrollContentsBy(int dx, int dy);
void SetHorizontalHeaderItemWidget(int column, QWidget * widget);
void SetHorizontalHeaderItemMargins(int column, CustomTableHeader::Margins margins);
private:
CustomTableHeader * mHorizontalHeader;
};
#endif // CUSTOMHEADEREDTABLE_H
customheaderedtable.cpp:
#include "customheaderedtable.h"
CustomHeaderedTable::CustomHeaderedTable(QWidget * parent) :
QTableWidget(parent)
{
mHorizontalHeader = new CustomTableHeader(Qt::Orientation::Horizontal, this);
setHorizontalHeader(mHorizontalHeader);
}
void CustomHeaderedTable::scrollContentsBy(int dx, int dy)
{
QTableWidget::scrollContentsBy(dx, dy);
if (dx != 0)
mHorizontalHeader->FixComboPositions();
}
void CustomHeaderedTable::SetHorizontalHeaderItemWidget(int column, QWidget * widget)
{
mHorizontalHeader->SetItemWidget(column, widget);
}
void CustomHeaderedTable::SetHorizontalHeaderItemMargins(int column, CustomTableHeader::Margins margins)
{
mHorizontalHeader->SetItemMargins(column, margins);
}
Sample useage:
QComboBox * combo = new QComboBox();
combo->addItem("1");
combo->addItem("2");
combo->addItem("3");
CustomHeaderedTable table = new CustomHeaderedTable(this);
table->setRowCount(70);
table->setColumnCount(70);
table->SetHorizontalHeaderItemWidget(1, combo);