Taking screenshot of a QOpenGLWidget containing text
Working on a Qt application that uses OpenGL to draw lines and polygons, and QPainter
to draw text, I was surprised to find that it is not particularly straightforward to make the application take screenshots of itself.
The context is an application that has a bunch of widgets that inherit from QOpenGLWidget
, and that display some data in a graphical way. Usually, it comes to drawing a lot of lines, and add text here and there. OpenGL was used in order to make the lines display faster—that was especially important as the application was hosted on a Raspberry Pi. However, as displaying text directly in OpenGL is not particularly straightforward, I decided to rely on an ordinary QPainter
with a drawText()
for this part, make it paint to an image, transform the image into a texture, and make OpenGL display a rectangle with the aforementioned texture. Relatively simple, until you need to grab a screenshot.
In fact, in order to do regression testing of something which is mostly graphical, I am relying on snapshot testing. A snapshot test works like this: it sets up a given context, renders a widget to an image, and compares the image to a reference, that is an image that is considered “correct.” This meant that the application had to take screenshots of itself, or at least of a given widget. And this is where things started to go wrong.
But first, let me introduce the approach I was using to render elements in the first place. Let's start with standard initialization:
void Widget::initializeGL() {
initializeOpenGLFunctions();
glClearColor(1.0, 0.8, 0.0, 1.0); // Roughly matches #fc0.
glEnable(GL_TEXTURE_2D);
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
glGenTextures(1, &textureID);
glBindTexture(GL_TEXTURE_2D, textureID);
this->image = QImage(200, 200, QImage::Format_RGBA8888);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glBindTexture(GL_TEXTURE_2D, 0);
}
void Widget::resizeGL(int w, int h) {
glViewport(0, 0, w, h);
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
gluOrtho2D(0, w, 0, h);
glMatrixMode(GL_MODELVIEW);
}
The painting is made in two steps:
void Widget::paintGL() {
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glLoadIdentity();
this->paintDecor();
this->paintTexture();
}
The paintDecor()
draws the background using native OpenGL:
void Widget::paintDecor() {
glColor3f(1.0, 0.7, 0.0);
for (auto x = 0; x < 200; x += 20) {
for (auto y = 0; y < 200; y += 20) {
glBegin(GL_TRIANGLES);
glVertex2f(x, y);
glVertex2f(x, y + 20);
glVertex2f(x + 20, y + 20);
glEnd();
}
}
}
Then, paintTexture()
creates the texture and feeds it to OpenGL:
void Widget::paintTexture() {
glBindTexture(GL_TEXTURE_2D, textureID);
glColor4f(1.0f, 1.0f, 1.0f, 1.0f);
this->createImage();
glTexImage2D(
GL_TEXTURE_2D,
0,
GL_RGBA,
image.width(),
image.height(),
0,
GL_RGBA,
GL_UNSIGNED_BYTE,
image.bits());
glBegin(GL_QUADS);
glTexCoord2f(0, 1); glVertex2f(0, 0);
glTexCoord2f(1, 1); glVertex2f(200, 0);
glTexCoord2f(1, 0); glVertex2f(200, 200);
glTexCoord2f(0, 0); glVertex2f(0, 200);
glEnd();
glBindTexture(GL_TEXTURE_2D, 0);
}
In turn, it calls the function that draws some stuff to the image with the help of QPainter
:
void Widget::createImage() {
this->image.fill(Qt::transparent);
QPainter p(&this->image);
this->createImage(p);
p.end();
this->image.save("image.png");
}
void Widget::createImage(QPainter &p) {
p.setPen(QPen(QColor("#0cf"), 5));
p.drawLine(QPointF(10, 10), QPointF(60, 60));
p.drawLine(QPointF(60, 10), QPointF(10, 60));
p.setCompositionMode(QPainter::CompositionMode_Clear);
p.eraseRect(QRect(25, 25, 20, 20));
p.setCompositionMode(QPainter::CompositionMode_SourceOver);
auto font = QFont("Spectral", 13.0 * 3, QFont::Medium);
font.setStyleStrategy(QFont::PreferAntialias);
font.setHintingPreference(QFont::PreferFullHinting);
p.setFont(font);
p.setPen(QPen(QColor("#000"), 5));
p.drawText(QRectF(50, 50, 0, 0), Qt::TextDontClip, "Black");
p.setPen(QPen(QColor("#0cf"), 5));
p.drawText(QRectF(50, 95, 0, 0), Qt::TextDontClip, "Blue");
p.setPen(QPen(QColor("#fff"), 5));
p.drawText(QRectF(50, 140, 0, 0), Qt::TextDontClip, "White");
}
The image gets also saved to a file and looks like this (shown here with a gray background, in order to make white text visible):
When combined with the background by the Qt application, this is what is displayed on screen:
A naive attempt to render the widget to an image presents, however, visual artifacts around text. If the rendering seems fine for black text, it is all but fine as soon as color is used, and white text is the worst.
void Widget::takeScreenshot(QString fileName) {
QPixmap pixmap(this->size());
this->render(&pixmap, QPoint(), QRegion(this->rect()));
pixmap.save(fileName);
}
An alternative that was suggested to me on StackOverflow, provides a much better rendering, but there is a white glow around non-black text (including the white text—see how thick it is?):
void Widget::takeScreenshot(QString fileName) {
this->grabFramebuffer().save(fileName);
}
Finally, the only solution which I've found and which is quite ugly, IMHO, is to render only the background (i.e. the native OpenGL stuff), take the screenshot, and ask QPainter
to paint directly over the screenshot.
void Widget::takeScreenshot(QString fileName) {
QPixmap pixmap(this->size());
this->takingScreenshot = true;
this->render(&pixmap, QPoint(), QRegion(this->rect()));
this->createImage();
this->takingScreenshot = false;
QPainter p(&pixmap);
p.drawImage(QPointF(), this->image);
p.end();
pixmap.save(fileName);
}
with paintGL()
modified like this:
void Widget::paintGL() {
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glLoadIdentity();
this->paintDecor();
if (!this->takingScreenshot) {
this->paintTexture();
}
}