I'm writing an application that displays a lot of text. It's not words and sentences though, it's binary data displayed in CP437 charset. Current form:
I'm having a problem though with drawing those characters. I need to draw each character one by one, because later I would like to apply different coloring. Those characters should have a transparent background as well, because later I would like to draw sections and ranges with different colors in the background (to group those characters based on some criteria).
The application supports multiple opened files at the same time, but when there are multiple files opened, the drawing starts to be noticeable on fast i7, so it's probably badly written.
What would be the best approach to draw this kind of data in Qt5? Should I just prerender characters to a bitmap and start from there, or it actually is possible to draw lots of characters by using normal Qt functions to draw text?
Edit: I'm using a normal QFrame widget that does drawing in paintEvent, using QPainter. Is this a wrong approach? I've read some docs on QGraphicsScene, from which I've remembered that it's best used in situations where a widget needs to have some control on the objects it draws. I don't need any control on what I draw; I just need to draw it and that's all. I won't reference any particular character after I'll draw it.
The widget has 2000 lines, so I won't paste the whole code, but currently my drawing approach is like this:
First, create a table (cache) with 256 entries, put the iterator counter to i variable,
For each entry, create a QStaticText object that contains drawing information about a character identified by ASCII code taken from i variable,
Later, in the drawing function, for each byte in the input stream (i.e. from the file), draw the data using QStaticText from the cache table. So, to draw ASCII character 0x7A, I'll look up QStaticText from index 0x7a in cache table, and feed this QStaticText object into the QPainter object.
I was also experimenting with a different approach, rendering the whole line in one QPainter::drawText call, and indeed it was faster, but I've lost possibility of coloring each character with different color. I would like to have this possibility.
The use of a QGraphicsScene wouldn't improve things - it's an additional layer on top of a QWidget. You're after raw performance, so you shouldn't be using it.
You could implement a QTextDocument as a viewmodel for the visible section of your memory buffer/file, but painting the fresh QTextDocument each time you scroll wouldn't be any faster than drawing things directly on a QWidget.
Using QStaticText is a step in the right direction, but insufficient: rendering QStaticText still requires the rasterization of the glyph's shape. You can do better and cache the pixmap of each QChar, QColor combination that you wish to render: this will be much faster than rasterizing character outlines, whether using QStaticText or not.
Instead of drawing individual characters, you then draw pixmaps from the cache. This commit demonstrates this approach. The character drawing method is:
void drawChar(const QPointF & pos, QChar ch, QColor color, QPainter & p) {
auto & glyph = m_cache[{ch, color}];
if (glyph.isNull()) {
glyph = QPixmap{m_glyphRect.size().toSize()};
glyph.fill(Qt::white);
QPainter p{&glyph};
p.setPen(color);
p.setFont(m_font);
p.drawText(m_glyphPos, {ch});
}
p.drawPixmap(pos, glyph);
}
You could also cache each (character,foreground,background) tuple. Alas, this gets quickly out of hand when there are many foreground/background combinations.
If all of your backgrounds are of the same color (e.g. white), you'd wish to store a negative mask of the character: the glyph has a white background and a transparent shape. This commit demonstrates this approach. The glyph rectangle is filled with glyph color, then a white mask is applied on top:
void drawChar(const QPointF & pos, QChar ch, QColor color, QPainter & p) {
auto & glyph = m_glyphs[ch];
if (glyph.isNull()) {
glyph = QImage{m_glyphRect.size().toSize(), QImage::Format_ARGB32_Premultiplied};
glyph.fill(Qt::white);
QPainter p{&glyph};
p.setCompositionMode(QPainter::CompositionMode_DestinationOut);
p.setFont(m_font);
p.drawText(m_glyphPos, {ch});
}
auto rect = m_glyphRect;
rect.moveTo(pos);
p.fillRect(rect, color);
p.drawImage(pos, glyph);
}
Instead of storing a fully pre-rendered character of a given color, you could store just the alpha mask and composite them on-demand:
Start with a pre-rendered white glyph on a transparent background (CompositionMode_Source).
Fill the glyph rect with background in CompositionMode_SourceOut: the background will remain with a hole for the character itself.
Fill the glyph rect with foreground in CompositionMode_DestinationOver: the foreground will fill the hole.
(Optional) Draw the composite on the widget, if you're not painting on the widget already.
This turns out to be reasonably fast, and the rendering is fully parallelizable - see the example below.
Note: The pre-rendered glyph could use further premultiplication of the color with alpha to appear less thick.
Yet another approach, with excellent performance, would be to emulate a text-mode display using the GPU. Store the pre-rendered glyph outlines in a texture, store the glyph indices and colors to be rendered in an array, and use OpenGL and two shaders to do the rendering. This example might be a starting point to implement such an approach.
A complete example, using CPU rendering across multiple threads, follows.
We start with the backing store view, used to produce QImages that are views into the backing store for a given widget, and can be used to parallelize painting.
On a 2013 iMac, this code repaints the full-screen widget in about 8ms.
// https://github.com/KubaO/stackoverflown/tree/master/questions/hex-widget-40458515
#include <QtConcurrent>
#include <QtWidgets>
#include <algorithm>
#include <array>
#include <cmath>
struct BackingStoreView {
QImage *dst = {};
uchar *data = {};
const QWidget *widget = {};
explicit BackingStoreView(const QWidget *widget) {
if (!widget || !widget->window()) return;
dst = dynamic_cast<QImage*>(widget->window()->backingStore()->paintDevice());
if (!dst || dst->depth() % 8) return;
auto byteDepth = dst->depth()/8;
auto pos = widget->mapTo(widget->window(), {});
data = const_cast<uchar*>(dst->constScanLine(pos.y()) + byteDepth * pos.x());
this->widget = widget;
}
// A view onto the backing store of a given widget
QImage getView() const {
if (!data) return {};
QImage ret(data, widget->width(), widget->height(), dst->bytesPerLine(), dst->format());
ret.setDevicePixelRatio(widget->devicePixelRatio());
return ret;
}
// Is a given image exactly this view?
bool isAView(const QImage &img) const {
return data && img.bits() == data && img.depth() == dst->depth()
&& img.width() == widget->width() && img.height() == widget->height()
&& img.bytesPerLine() == dst->bytesPerLine() && img.format() == dst->format();
}
};
Then, the CP437 character set:
static auto const CP437 = QStringLiteral(
" ☺☻♥♦♣♠•◘○◙♂♀♪♫☼▶◀↕‼¶§▬↨↑↓→←∟↔▲▼"
"␣!\"#$%&'()*+,-./0123456789:;<=>?"
"#ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_"
"`abcdefghijklmnopqrstuvwxyz{|}~ "
"ÇüéâäàåçêëèïîìÄÅÉæÆôöòûùÿÖÜ¢£¥₧ƒ"
"áíóúñѪº¿⌐¬½¼¡«»░▒▓│┤╡╢╖╕╣║╗╝╜╛┐"
"└┴┬├─┼╞╟╚╔╩╦╠═╬╧╨╤╥╙╘╒╓╫╪┘┌█▄▌▐▀"
"αßΓπΣσµτΦΘΩδ∞φε∩≡±≥≤⌠⌡÷≈°∙·√ⁿ²■ ");
The HexView widget derives from QAbstractScrollArea and visualizes a memory-mapped chunk of data:
class HexView : public QAbstractScrollArea {
Q_OBJECT
QImage const m_nullImage;
const int m_addressChars = 8;
const int m_dataMargin = 4;
const char * m_data = {};
size_t m_dataSize = 0;
size_t m_dataStart = 0;
QSize m_glyphSize;
QPointF m_glyphPos;
int m_charsPerLine, m_lines;
QMap<QChar, QImage> m_glyphs;
QFont m_font{"Monaco"};
QFontMetricsF m_fm{m_font};
struct DrawUnit { QPoint pos; const QImage *glyph; QColor fg, bg; };
QFutureSynchronizer<void> m_sync;
QVector<DrawUnit> m_chunks;
QVector<QImage> m_stores;
using chunk_it = QVector<DrawUnit>::const_iterator;
using store_it = QVector<QImage>::const_iterator;
static inline QChar decode(char ch) { return CP437[uchar(ch)]; }
inline int xStep() const { return m_glyphSize.width(); }
inline int yStep() const { return m_glyphSize.height(); }
void initData() {
int const width = viewport()->width() - m_addressChars*xStep() - m_dataMargin;
m_charsPerLine = (width > 0) ? width/xStep() : 0;
m_lines = viewport()->height()/yStep();
if (m_charsPerLine && m_lines) {
verticalScrollBar()->setRange(0, m_dataSize/m_charsPerLine);
verticalScrollBar()->setValue(m_dataStart/m_charsPerLine);
} else {
verticalScrollBar()->setRange(0, 0);
}
}
const QImage &glyph(QChar ch) {
auto &glyph = m_glyphs[ch];
if (glyph.isNull()) {
QPointF extent = m_fm.boundingRect(ch).translated(m_glyphPos).bottomRight();
glyph = QImage(m_glyphSize, QImage::Format_ARGB32_Premultiplied);
glyph.fill(Qt::transparent);
QPainter p{&glyph};
p.setPen(Qt::white);
p.setFont(m_font);
p.translate(m_glyphPos);
p.scale(std::min(1.0, (m_glyphSize.width()-1)/extent.x()),
std::min(1.0, (m_glyphSize.height()-1)/extent.y()));
p.drawText(QPointF{}, {ch});
}
return glyph;
}
The parallelized rendering is done in class methods - they don't modify the state of the widget, other than accessing read-only data, and rendering into the backing store. The threads each act on isolated lines in the store.
static void drawChar(const DrawUnit & u, QPainter &p) {
const QRect rect(u.pos, u.glyph->size());
p.setCompositionMode(QPainter::CompositionMode_Source);
p.drawImage(u.pos, *u.glyph);
p.setCompositionMode(QPainter::CompositionMode_SourceOut);
p.fillRect(rect, u.bg);
p.setCompositionMode(QPainter::CompositionMode_DestinationOver);
p.fillRect(rect, u.fg);
}
static QFuture<void> submitChunks(chunk_it begin, chunk_it end, store_it store) {
return QtConcurrent::run([begin, end, store]{
QPainter p(const_cast<QImage*>(&*store));
for (auto it = begin; it != end; it++)
drawChar(*it, p);
});
}
This method distributes the chunks of work between threads:
int processChunks() {
m_stores.resize(QThread::idealThreadCount());
BackingStoreView view(viewport());
if (!view.isAView(m_stores.last()))
std::generate(m_stores.begin(), m_stores.end(), [&view]{ return view.getView(); });
std::ptrdiff_t jobSize = std::max(128, (m_chunks.size() / m_stores.size())+1);
auto const cend = m_chunks.cend();
int refY = 0;
auto store = m_stores.cbegin();
for (auto it = m_chunks.cbegin(); it != cend;) {
auto end = it + std::min(cend-it, jobSize);
while (end != cend && (end->pos.y() == refY || (refY = end->pos.y(), false)))
end++; // break chunks across line boundaries
m_sync.addFuture(submitChunks(it, end, store));
it = end;
store++;
}
m_sync.waitForFinished();
m_sync.clearFutures();
m_chunks.clear();
return store - m_stores.cbegin();
}
The remainder of the implementation is uncontroversial:
protected:
void paintEvent(QPaintEvent *ev) override {
QElapsedTimer time;
time.start();
QPainter p{viewport()};
QPoint pos;
QPoint const step{xStep(), 0};
auto dividerX = m_addressChars*xStep() + m_dataMargin/2.;
p.drawLine(dividerX, 0, dividerX, viewport()->height());
int offset = 0;
QRect rRect = ev->rect();
p.end();
while (offset < m_charsPerLine*m_lines && m_dataStart + offset < m_dataSize) {
const auto address = QString::number(m_dataStart + offset, 16);
pos += step * (m_addressChars - address.size());
for (auto c : address) {
if (QRect(pos, m_glyphSize).intersects(rRect))
m_chunks.push_back({pos, &glyph(c), Qt::black, Qt::white});
pos += step;
}
pos += {m_dataMargin, 0};
auto bytes = std::min(m_dataSize - offset, (size_t)m_charsPerLine);
for (int n = bytes; n; n--) {
if (QRect(pos, m_glyphSize).intersects(rRect))
m_chunks.push_back({pos, &glyph(decode(m_data[m_dataStart + offset])), Qt::red, Qt::white});
pos += step;
offset ++;
}
pos = {0, pos.y() + yStep()};
}
int jobs = processChunks();
newStatus(QStringLiteral("%1ms n=%2").arg(time.nsecsElapsed()/1e6).arg(jobs));
}
void resizeEvent(QResizeEvent *) override {
initData();
}
void scrollContentsBy(int, int dy) override {
m_dataStart = verticalScrollBar()->value() * (size_t)m_charsPerLine;
viewport()->scroll(0, dy * m_glyphSize.height(), viewport()->rect());
}
public:
HexView(QWidget * parent = nullptr) : HexView(nullptr, 0, parent) {}
HexView(const char * data, size_t size, QWidget * parent = nullptr) :
QAbstractScrollArea{parent}, m_data(data), m_dataSize(size)
{
QRectF glyphRectF{0., 0., 1., 1.};
for (int i = 0x20; i < 0xE0; ++i)
glyphRectF = glyphRectF.united(m_fm.boundingRect(CP437[i]));
m_glyphPos = -glyphRectF.topLeft();
m_glyphSize = QSize(std::ceil(glyphRectF.width()), std::ceil(glyphRectF.height()));
initData();
}
void setData(const char * data, size_t size) {
if (data == m_data && size == m_dataSize) return;
m_data = data;
m_dataSize = size;
m_dataStart = 0;
initData();
viewport()->update();
}
Q_SIGNAL void newStatus(const QString &);
};
We leverage modern 64-bit systems and memory-map the source file to be visualized by the widget. For test purposes, a view of the character set is also available:
int main(int argc, char ** argv) {
QApplication app{argc, argv};
QFile file{app.applicationFilePath()};
if (!file.open(QIODevice::ReadOnly)) return 1;
auto *const map = (const char*)file.map(0, file.size(), QFile::MapPrivateOption);
if (!map) return 2;
QWidget ui;
QGridLayout layout{&ui};
HexView view;
QRadioButton exe{"Executable"};
QRadioButton charset{"Character Set"};
QLabel status;
layout.addWidget(&view, 0, 0, 1, 4);
layout.addWidget(&exe, 1, 0);
layout.addWidget(&charset, 1, 1);
layout.addWidget(&status, 1, 2, 1, 2);
QObject::connect(&exe, &QPushButton::clicked, [&]{
view.setData(map, (size_t)file.size());
});
QObject::connect(&charset, &QPushButton::clicked, [&]{
static std::array<char, 256> data;
std::iota(data.begin(), data.end(), char(0));
view.setData(data.data(), data.size());
});
QObject::connect(&view, &HexView::newStatus, &status, &QLabel::setText);
charset.click();
ui.resize(1000, 800);
ui.show();
return app.exec();
}
#include "main.moc"
One solution I sometimes use is to keep a cache of pre-rendered lines. I normally use a doubly-linked LRU list of entries with about twice the lines that can be seen on the screen. Every time a line is used for rendering is moved to the front of the list; when I need to create a new line and the current cache count is past the limit I reuse the last entry in the list.
By storing the final result of individual lines you can repaint the display very quickly as probably in many cases most of the lines will not change from one frame to the next (including when scrolling).
The increased complexity is also reasonably confined in having to invalidate the line when you change the content.
Related
How to create a QTableWidget with some cells filled using uniform sized widgets and the unused cells filled with the size of widgets?The resulting table should contain a row/column as blank even if no widget is present.
Since the QTableWidget apparently has to contain something in each cell, and the default is an editable text box, it seem the solution would be to insert a non-editable dummy item into the blank cells. Or have a custom widget which can hide its contents, which is what I've done in the example below.
You specifically asked about fixed size... there's really a number of ways to go about this, depending on the exact needs. You can set fixed sizes on the whole table by using the horizontal and vertical headers and making them non-resizable. Another way is to call QTableView::setColumnWidth() / setRowHeight() for each row/column (while adding/item or whenever).
Here's an example using the former method (setting size on headers), and two ways to determine the actual size -- either a fixed size before building the table, or using a lookup on the custom widget item to determine a size.
UPDATE: Previous version used a dummy QTableWidgetItem to fill in the blank areas, but I think this is better if one needs to use a custom widget anyway. See edit history for previous version.
// main.cpp
#include <QtWidgets>
// A custom table widget item which contains a progress bar. The bar is
// hidden if the value is set to < 0.
class ProgressBarTwItem : public QWidget, public QTableWidgetItem
{
Q_OBJECT
public:
ProgressBarTwItem(QWidget *parent = nullptr, int value = -1) :
QWidget(parent),
QTableWidgetItem(QTableWidgetItem::UserType)
{
QHBoxLayout *l = new QHBoxLayout(this);
l->setContentsMargins(0,0,0,0);
m_pb = new QProgressBar(this);
// don't let PB size dictate our size
m_pb->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Ignored);
m_pb->setAlignment(Qt::AlignCenter);
m_pb->setMaximum(44);
l->addWidget(m_pb);
setValue(value);
}
QSize sizeHint() const override { return QSize(75, 25); }
public slots:
void setValue(int value = -1) const
{
m_pb->setVisible(value > -1);
if (value > -1)
m_pb->setValue(value);
}
private:
QProgressBar *m_pb;
};
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
a.setStyle("Fusion");
QDialog d;
d.setLayout(new QVBoxLayout);
QTableWidget *tw = new QTableWidget(5, 4, &d);
d.layout()->addWidget(tw);
// set to `true` to find the largest size from added custom widgets,
// otherwise use preset size
const bool useWidgetSize = true;
// This will be the fixed cell size, either preset or determined from custom widget.
// If looking up from widgets, this becomes the minimum size
// (eg. in case widgets didn't return valid sizes).
QSize cellSize = (useWidgetSize ? QSize(10, 10) : QSize(50, 15));
for (int r=0; r < tw->rowCount(); ++r) {
for (int c=0; c < tw->columnCount(); ++c) {
// make some widgets hidden and others display a progress value
const int val = (!((r*c) % (1+c)) ? -1 : (c+1 + r*10));
ProgressBarTwItem *item = new ProgressBarTwItem(tw, val);
tw->setCellWidget(r, c, item);
// tw->setItem(r, c, item); // needed? widget is shown anyway... docs not clear.
// Check the size.
if (useWidgetSize)
cellSize = cellSize.expandedTo(item->sizeHint());
}
}
// set fixed sizes for columns and rows on the horizontal and vertical headers
// respectively (this works even if they are hidden)
tw->horizontalHeader()->setDefaultSectionSize(cellSize.width());
tw->horizontalHeader()->setSectionResizeMode(QHeaderView::Fixed);
tw->verticalHeader()->setDefaultSectionSize(cellSize.height());
tw->verticalHeader()->setSectionResizeMode(QHeaderView::Fixed);
return d.exec();
}
#include "main.moc"
I have a problem if I want to display a video using QGLWidget. With one instance it works, but it doesn't and widgets are black with multi-occurrence (usage in QGridLayout for example).
I subclass QGLWidget that way to play a video (by refreshing the QPixmap I want to show using my setCurrentImage method):
class GLWidget : public QGLWidget
{
Q_OBJECT
public:
GLWidget(QGLWidget* shareWidget = 0, QWidget* parent = 0)
: QGLWidget(parent, shareWidget)
{
//...
// I do this because the QPixmap to refresh is produced in a different thread than UI thread.
connect(this, SIGNAL(updated()), this SLOT(update()));
}
//...
void setCurrentImage(const QPixmap& pixmap)
{
m_mutex.lock();
m_pixmap = pixmap;
m_mutex.unlock();
emit updated();
}
protected:
void initializeGL()
{
static const int coords[4][2] = {{ +1, -1 }, { -1, -1 }, { -1, +1 }, { +1, +1 }};
for(int j = 0; j < 4; ++j){
m_texCoords.append(QVector2D(j == 0 || j == 3, j == 0 || j == 1));
m_vertices.append(QVector2D(0.5 * coords[j][0], 0.5 * coords[j][1]));
}
glEnable(GL_DEPTH_TEST);
glEnable(GL_CULL_FACE);
glEnable(GL_TEXTURE_2D);
#define PROGRAM_VERTEX_ATTRIBUTE 0
#define PROGRAM_TEXCOORD_ATTRIBUTE 1
QGLShader* vShader = new QGLShader(QGLShader::Vertex, this);
const char* szVShaderCode = /*...*/;
vShader->compileSourceCode(szVShaderCode);
QGLShader *fShader = new QGLShader(QGLShader::Fragment, this);
const char* szFShaderCode = /*...*/;
fShader->compileSourceCode(szFShaderCode);
m_pProgram = new QGLShaderProgram(this);
m_pProgram->addShader(vShader);
m_pProgram->addShader(fShader);
m_pProgram->bindAttributeLocation("vertex", PROGRAM_VERTEX_ATTRIBUTE);
m_pProgram->bindAttributeLocation("texCoord", PROGRAM_TEXCOORD_ATTRIBUTE);
m_pProgram->link();
m_pProgram->bind();
m_pProgram->setUniformValue("texture", 0);
}
void paintGL()
{
qglClearColor(Qt::black);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
// I use some uniform values to "modify" image
m_pProgram->setUniformValue(/*...*/);
//...
QMatrix4x4 m;
m.ortho(-0.5f, +0.5f, +0.5f, -0.5f, 4.0f, 15.0f);
m.translate(0.0f, 0.0f, -10.0f);
m_pProgram->setUniformValue("matrix", m);
m_pProgram->enableAttributeArray(PROGRAM_VERTEX_ATTRIBUTE);
m_pProgram->enableAttributeArray(PROGRAM_TEXCOORD_ATTRIBUTE);
m_pProgram->setAttributeArray(PROGRAM_VERTEX_ATTRIBUTE, m_vertices.constData());
m_pProgram->setAttributeArray(PROGRAM_TEXCOORD_ATTRIBUTE, m_texCoords.constData());
glBindTexture(GL_TEXTURE_2D, m_texture);
glDrawArrays(GL_TRIANGLE_FAN, 0, 4);
}
void resizeGL(int iWidth, int iHeight)
{
glViewport(0, 0, iWidth, iHeight);
}
private slots:
void update()
{
QPixmap pixmap;
m_mutex.lock();
pixmap = m_pixmap;
m_mutex.unlock();
m_texture = bindTexture(pixmap, GL_TEXTURE_2D);
updateGL();
}
private:
QMutex m_mutex;
QPixmap m_pixmap;
QVector<QVector2D> m_vertices;
QVector<QVector2D> m_texCoords;
QGLShaderProgram* m_pProgram;
};
Then, to simulate a multi thread rendering (each thread produces its own video images), I wrote these little classes.
The image producer class:
class ImageProducer : public QThread
{
Q_OBJECT
public:
ImageProducer(QGLWidget* pGLWidget)
: QThread(pGLWidget), m_pGLWidget(pGLWidget)
{
m_pixmap = QPixmap("fileName.jpg");
m_bMustStop = false;
}
protected:
void run()
{
while(!m_bMustStop){
static_cast<GLWidget*>(m_pGLWidget)->setCurrentImage(m_pixmap);
// Simulate a frame rate
msleep(1000 / /*FRAME_RATE*/);
}
}
private:
QGLWidget* m_pGLWidget;
QPixmap m_pixmap;
bool m_bMustStop;
};
And the rendering class:
#define ROWS 1
#define COLS 1
class Window : public QWidget
{
public:
Window()
{
QGridLayout* pMainLayout = new QGridLayout(this);
setLayout(pMainLayout);
for(int i = 0; i < ROWS; ++i){
for(int j = 0; j < COLS; ++j){
QGLWidget* pGLWidget = new GLWidget();
pMainLayout->addWidget(pGLWidget, i, j);
ImageProducer* pImageProducer = new ImageProducer(pGLWidget);
pImageProducer->start();
}
}
}
};
Ok stop with code samples ^^ Problem is with ROWS = 1 and COLS = 1 (see Window class) it works, but I have black widgets with other values... I'm lost, what do I miss?
Thanks!
EDIT: (context is always multi-GLWidget instance, all works fine with only one)
Strange thing I just discover: I overrode QGLWidget's mouseMoveEvent (so, in my GLWidget class) which simply calls updateGL();. And fact is when I press and move the mouse with the current code nothing happens. But if I replace (in my ImageProducer's run() method):
static_cast<GLWidget*>(m_pGLWidget)->setCurrentImage(m_pixmap);
By
static_cast<GLWidget*>(m_pGLWidget)->setCurrentImage(QPixmap("fileName.jpg"));
The image is refreshed in the current component as long as I move the mouse. When I release it, or do it in an other components, the background become black again.
I am writing down the answer after a chat with the OP, for the benefit of others. Here are the main topics that lead to a resolution of the issue.
Note: At the time of writing, the Qt OpenGL module is deprecated and its usage is discouraged. According to the official documentation the suggested approach is to use the OpenGL* classes in the GUI module.
Textures
The main issue with the code above is essentially due to the way the textures are handled.
First of all, the bindTexture() method does not bind in OpenGL terms, but actually create the OpenGL texture and upload the data passed as argument to it (QImage or QPixmap). That is confusing and can lead to serious issues. In the code posted by the OP, he is essentially leaking memory allocating a new texture at each frame update.
To ensure you are not leaking memory, you should at the least release the previously allocated texture, calling QGLWidget::deleteTexture.
However, it is worth to note that there are better approaches to avoid unnecessary memory fragmentation and inefficiency. In this case, the best approach is to allocate the texture once and simply update its content when necessary. This may not be possible directly with the API offered by the old Qt OpenGL module, but you can always mix Qt and native OpenGL code.
Context handling
Another issue is the way OpenGL context is handled. As a rule of thumb, one should always ensure the correct context is current before issuing commands to the OpenGL implementation. Sometimes, Qt does this for you automatically (i.e. before calling the paintGL() method).
In this case, we need to explicitly make the QGLWidget's context the current context before calling the bindTexture method, otherwise it will effect the last context that was made current. That is why only the last widget was showing something untill somewhone was triggering a makeCurrent call by interacting with the other widgets.
Threading
There are a couple of issues here. First of all, it is not safe to use a QPixmap object in a thread other than the GUI thread. QPixmap is designed to optimize pixmap blitting on screen. In this case, the pixmap is really just the frame to be uploaded to the OpenGL implementation, which handles all the rendering. So it is safe to use a QImage instead.
The other issue is that the GLWidget::setCurrentImage() and thus, the bindTexture method, are called directly from the run() method of the ImageProducer thread. This can't be because we need to make the widget context current, but it is not possible to call makeCurrent() from a thread other than the GUI thread (more details here).
A possible approach to solve this issue is to add a signal to ImageProducer to notify the frame has been updated, and connect this signal to the setCurrentImage() slot. Qt's signal-slot mechanism will ensure that the setCurrentImage is executed in the GLWidget thread, that is the GUI thread.
Code
Here are the suggested (and tested) modifications to the code posted above.
ImageProduer class
add the signal:
void imageUpdated(const QImage &image);
and emit it when the frame is updated:
void ImageProducer::run()
{
while(!m_bMustStop){
QImage frame(m_pixmap.width(), m_pixmap.height(), m_pixmap.format());
QPainter painter(&frame);
painter.drawImage(QPoint(), m_pixmap);
painter.setFont(QFont("sans-serif", 22));
painter.setPen(Qt::white);
painter.drawText(20, 50, QString("Frame: %1").arg(QString::number(_frameCount)));
painter.end();
emit imageUpdated(frame);
msleep(1000 / FRAME_RATE);
_frameCount++;
}
}
GLWidget class
ensure the setCurrentImage method handle the OpenGL context and destroy the old texture:
void GLWidget::setCurrentImage(const QImage& pixmap)
{
m_mutex.lock();
m_pixmap = pixmap;
m_mutex.unlock();
makeCurrent();
deleteTexture(m_texture);
m_texture = bindTexture(pixmap, GL_TEXTURE_2D);
doneCurrent();
emit updated();
}
What is the proper way to draw thousands of rects in QT (around 100,000 or more)?
I tried:
Simple with paintEvent() of QWidget.
Drawing objects to QImage then this image to QWidget.
Using QGraphicsScene (maybe I didn't use it properly, I just added rects to scene)
Every time drawing was really slow and I don't have more ideas on how to do this (maybe with opengl/directx but this doesn't sound like a good idea). I know that there exist applications that do that so there should be some way.
EDIT:
I wonder how drawRects() work? Is there a chance that filling some uchar* array and passing it to QImage will be better?
The first trick is to do the drawing in a separate thread onto a QImage, then pass that into the main thread. This won't make it quicker, but it'll make it not block the GUI thread.
// https://github.com/KubaO/stackoverflown/tree/master/questions/threaded-paint-36748972
#include <QtWidgets>
#include <QtConcurrent>
class Widget : public QWidget {
Q_OBJECT
QImage m_image;
bool m_pendingRender { false };
Q_SIGNAL void hasNewRender(const QImage &);
// Must be thread-safe, we can't access the widget directly!
void paint() {
QImage image{2048, 2048, QImage::Format_ARGB32_Premultiplied};
image.fill(Qt::white);
QPainter p(&image);
for (int i = 0; i < 100000; ++i) {
QColor c{rand() % 256, rand() % 256, rand() % 256};
p.setBrush(c);
p.setPen(c);
p.drawRect(rand() % 2048, rand() % 2048, rand() % 100, rand() % 100);
}
emit hasNewRender(image);
}
void paintEvent(QPaintEvent *) {
QPainter p(this);
p.drawImage(0, 0, m_image);
}
public:
Widget(QWidget * parent = 0) : QWidget(parent) {
this->setAttribute(Qt::WA_OpaquePaintEvent);
setMinimumSize(200, 200);
connect(this, &Widget::hasNewRender, this, [this](const QImage & img) {
m_image = img;
m_pendingRender = false;
update();
});
refresh();
}
Q_SLOT void refresh() {
if (!m_pendingRender) {
m_pendingRender = true;
QtConcurrent::run([this] { paint(); });
}
}
};
int main(int argc, char ** argv) {
QApplication app{argc, argv};
Widget w;
QPushButton button{"Refresh", &w};
button.connect(&button, &QPushButton::clicked, &w, [&w]{ w.refresh(); });
w.show();
return app.exec();
}
#include "main.moc"
As a separate concern, you can then split the drawing across multiple parallel jobs, by clipping each job's painter to a sub-area of the shared image, and noting that fully clipped rectangle draws are no-ops, and partially clipped ones will only fill the pixels they affect.
Solution which I found:
Create array of uint32_t which can contain all pixels of widget, fill it using memcpy(). Create QImage with this array and use drawImage() to show it.
It can have some optimization like (for profiler) merging rects that are continues ( start time second is equal to end of first ). Don't draw rects that are out of time bounds. Maybe skip too small rects.
For drawing things like text, tool tips you can still use Qt functions.
For alpha blending in simplest case you can just take existing values, blend them in loop and write blended values or maybe use SIMD for this.
Of course for more complex shapes it will get harder to draw but still, I think, it will be faster than using Qt functions.
I use QGraphicsScene of the Qt framework. Inside the scene I have some QGraphicsItems which the user can select and move.
I would like to have an info label where the current x and y coordinate of the currently moved selection (can consist of many items) is displayed.
I have tried with the signal changed of QGraphicsScene. But it is fired before the x() and y() property of the items is set to the new values. So the labels always show the second-to-last coordinates. If one moves the mouse slowly, the display is not very wrong. But with fast moves and sudden stops, the labels are wrong. I need a signal that is fired after the scene hast changed.
I have also tried to override the itemChange method of QGraphicsItem. But it is the same. It is fired before the change. (The new coordinates are inside the parameters of this method, but I need the new coordinates of all selected items at once)
I have also tried to override the mouseMove events of QGraphicsScene and of QGraphicsView but they, too, are before the new coordinates are set.
I did a test: I used a oneshot timer so that the labels are updated 100 ms after the signals. Then everything works fine. But a timer is no solution for me.
What can I do?
Make all items un-moveable and handle everything by my own?
QGraphicsItem::itemChange() is the correct approach, you were probably just checking the wrong flag. Something like this should work fine:
QVariant::myGraphicsItem( GraphicsItemChange change, const QVariant &value )
{
if( change == QGraphicsItem::ItemPositionHasChanged )
{
// ...
}
}
Note the use of QGraphicsItem::ItemPositionHasChanged rather than QGraphicsItem::ItemPositionChange, the former is called after the position changes rather than before.
The solution is to combine various things that you're already doing. Instrument itemChange, looking for and count the items with updated geometry. Once you've counted as many items as there are in the current selection, fire off a signal that will have everything ready for updating your status. Make sure you've set the QGraphicsItem::ItemSendsGeometryChanges flag on all your items!
This code was edited to remove the lag inherent in using a zero-timer approach. Below is a sscce that demonstrates it.
You create circles of random radius by clicking in the window. The selection is toggled with Ctrl-click or ⌘-click. When you move the items, a centroid diamond follows the centroid of the selected group. This gives a visual confirmation that the code does indeed work. When the selection is empty, the centroid is not displayed.
I've gratuitously added code to show how to leverage Qt's property system so that the items can be generic and leverage the notifier property of a scene if it has one. In its absence, the items simply don't notify, and that's it.
// https://github.com/KubaO/stackoverflown/tree/master/questions/scenemod-11232425
#include <QtWidgets>
const char kNotifier[] = "notifier";
class Notifier : public QObject
{
Q_OBJECT
int m_count = {};
public:
int count() const { return m_count; }
void inc() { m_count ++; }
void notify() { m_count = {}; emit notification(); }
Q_SIGNAL void notification();
};
typedef QPointer<Notifier> NotifierPointer;
Q_DECLARE_METATYPE(NotifierPointer)
template <typename T> class NotifyingItem : public T
{
protected:
QVariant itemChange(QGraphicsItem::GraphicsItemChange change, const QVariant &value) override {
QVariant v;
if (change == T::ItemPositionHasChanged &&
this->scene() &&
(v=this->scene()->property(kNotifier)).isValid())
{
auto notifier = v.value<NotifierPointer>();
notifier->inc();
if (notifier->count() >= this->scene()->selectedItems().count()) {
notifier->notify();
}
}
return T::itemChange(change, value);
}
};
// Note that all you need to make Circle a notifying item is to derive from
// NotifyingItem<basetype>.
class Circle : public NotifyingItem<QGraphicsEllipseItem>
{
QBrush m_brush;
public:
Circle(const QPointF & c) : m_brush(Qt::lightGray) {
const qreal r = 10.0 + (50.0*qrand())/RAND_MAX;
setRect({-r, -r, 2.0*r, 2.0*r});
setPos(c);
setFlags(QGraphicsItem::ItemIsMovable | QGraphicsItem::ItemIsSelectable |
QGraphicsItem::ItemSendsGeometryChanges);
setPen({Qt::red});
setBrush(m_brush);
}
};
class View : public QGraphicsView
{
Q_OBJECT
QGraphicsScene scene;
QGraphicsSimpleTextItem text;
QGraphicsRectItem centroid{-5, -5, 10, 10};
Notifier notifier;
int deltaCounter = {};
public:
explicit View(QWidget *parent = {});
protected:
Q_SLOT void gotUpdates();
void mousePressEvent(QMouseEvent *event) override;
};
View::View(QWidget *parent) : QGraphicsView(parent)
{
centroid.hide();
centroid.setRotation(45.0);
centroid.setPen({Qt::blue});
centroid.setZValue(2);
scene.addItem(¢roid);
text.setPos(5, 470);
text.setZValue(1);
scene.addItem(&text);
setRenderHint(QPainter::Antialiasing);
setScene(&scene);
setSceneRect(0,0,500,500);
scene.setProperty(kNotifier, QVariant::fromValue(NotifierPointer(¬ifier)));
connect(¬ifier, &Notifier::notification, this, &View::gotUpdates);
connect(&scene, &QGraphicsScene::selectionChanged, ¬ifier, &Notifier::notification);
}
void View::gotUpdates()
{
if (scene.selectedItems().isEmpty()) {
centroid.hide();
return;
}
centroid.show();
QPointF centroid;
qreal area = {};
for (auto item : scene.selectedItems()) {
const QRectF r = item->boundingRect();
const qreal a = r.width() * r.height();
centroid += item->pos() * a;
area += a;
}
if (area > 0) centroid /= area;
auto st = QStringLiteral("delta #%1 with %2 items, centroid at %3, %4")
.arg(deltaCounter++).arg(scene.selectedItems().count())
.arg(centroid.x(), 0, 'f', 1).arg(centroid.y(), 0, 'f', 1);
this->centroid.setPos(centroid);
text.setText(st);
}
void View::mousePressEvent(QMouseEvent *event)
{
const auto center = mapToScene(event->pos());
if (! scene.itemAt(center, {})) scene.addItem(new Circle{center});
QGraphicsView::mousePressEvent(event);
}
int main(int argc, char *argv[])
{
QApplication app{argc, argv};
View v;
v.show();
return app.exec();
}
#include "main.moc"
I have a graph shown in a QImage and want to set a cross (+) in yellow colour for measurement, if right mouse button is pressed.
void foo::mousePressEvent(QMouseEvent *event)
{
if (event->button() == Qt::RightButton) {
QPoint pos = event->pos();
int x = pos.x();
int y = pos.y();
QLine line(x-5,y,x+5,y);
QLine line(x,y-5,x,y+5);
QPainter painter(&my_image);
painter.setPen( Qt::red );
painter.setBrush( Qt::yellow );
/*
QPainter::begin: Cannot paint on an image with the QImage::Format_Indexed8 format
QPainter::setPen: Painter not active
QPainter::setBrush: Painter not active
*/
painter.drawLine(line); //no effect
}
}
if I do it in Paintevent(...), I destroy the original pic. how can I do it.
additional Information:
the imag is indexed.
my_image.setColorCount(33);
for(int i = 0;i<33;i++)
{
my_image.setColor(i,qRgb((unsigned char)palette[i*3], (unsigned char)palette[i*3+1], (unsigned char)palette[i*3+2]));
}
my_imag has a black-Background and I want to draw a cross in white color --> (this is the index 32)
int color = 32;//_index_value_of_cross_color;
for (int ix=x-5;ix<x+5;ix++) {
my_image.setPixel(ix,y,color);
}
for (int iy=y-5;iy<y+5;iy++) {
my_imag.setPixel(x,iy,color);
}
but I see no effect !
From your comments, you cannot paint on a QImage with Format_Indexed8.
From the QImage docs:
Warning: Painting on a QImage with the format QImage::Format_Indexed8 is not supported.
Choose a different format like QImage::Format_ARGB32_Premultiplied and things should work.
Another quick and dirty alternative is to simply set the values in the image data.
You will have to do a little more work - because there is no line command, see setpixel
int x = pos.x();
int y = pos.y();
int color = _index_value_of_cross_color;
for (int ix=x-5;ix<x+5;ix++) {
my_image.setPixel(ix,y,color);
}
for (int iy=y-5;iy<y+5;iy++) {
my_image.setPixel(x,iy,color);
}