Creating QIcon from SVG contents in memory - c++

How can we create the QIcon object having SVG icon contents in the memory buffer?
P.S. Initially wanted to create QSvgIconEngine, but it is hidden on the plugins layer, and I can not create it explicitly. How can I do it with loading from plugin (taking in account, that plugin is loaded)?

After digging for a while here and there, and digging in how QIcon itself uses an svg file to load an icon, here's what I learned:
QIcon, when called with an svg file (or any other image type for that matter), it calles addFile() subsequently, which only uses the extension of the file (called QFileInfo::suffix in Qt) to determine the method that converts the image file to an icon.
The method (semantically speaking) is determined by a QIconEngine instance
QIconEngine classes for every image type are not apparently simply accessible by us (the Qt developers); apparently there's a plugin system to be used and it's not available at compile-time (not simply at least)
On the other hand; How does QIcon work? When an icon is requested from QIcon, it uses the information passed to it to determine what engine to use, and creates an instance of the engine. Then, every time the icon needs to draw something, it asks the engine to draw an icon with some size given to it. The size is used on the function QIconEngine::pixmap(), which creates a pixmap with the required size, then the method QIconEngine::paint() is used to paint on that pixmap.
So, given this information, the solution is simply to write an Icon Engine that QIcon will use in order to generate the icon depending on the size passed to it. Here's how to do that:
So here's the header file SvgIconEngine.h
#ifndef SVGICONENGINE_H
#define SVGICONENGINE_H
#include <QIconEngine>
#include <QSvgRenderer>
class SVGIconEngine : public QIconEngine {
QByteArray data;
public:
explicit SVGIconEngine(const std::string &iconBuffer);
void paint(QPainter *painter, const QRect &rect, QIcon::Mode mode,
QIcon::State state) override;
QIconEngine *clone() const override;
QPixmap pixmap(const QSize &size, QIcon::Mode mode,
QIcon::State state) override;
signals:
public slots:
};
#endif // SVGICONENGINE_H
And here's the implementation, SvgIconEngine.cpp
#include "SvgIconEngine.h"
#include <QPainter>
SVGIconEngine::SVGIconEngine(const std::string &iconBuffer) {
data = QByteArray::fromStdString(iconBuffer);
}
void SVGIconEngine::paint(QPainter *painter, const QRect &rect,
QIcon::Mode mode, QIcon::State state) {
QSvgRenderer renderer(data);
renderer.render(painter, rect);
}
QIconEngine *SVGIconEngine::clone() const { return new SVGIconEngine(*this); }
QPixmap SVGIconEngine::pixmap(const QSize &size, QIcon::Mode mode,
QIcon::State state) {
// This function is necessary to create an EMPTY pixmap. It's called always
// before paint()
QImage img(size, QImage::Format_ARGB32);
img.fill(qRgba(0, 0, 0, 0));
QPixmap pix = QPixmap::fromImage(img, Qt::NoFormatConversion);
{
QPainter painter(&pix);
QRect r(QPoint(0.0, 0.0), size);
this->paint(&painter, r, mode, state);
}
return pix;
}
Notice: You must override clone(), because it's an abstract method, and you must override pixmap(), because without that, you'll not have an empty pixmap to paint the svg on.
To use this, simply do this:
std::string iconSvgData = GetTheSvgPlainData();
QIcon theIcon(new SVGIconEngine(iconSvgData));
//Use the icon!
Notice that QIcon takes ownership of the engine object. It'll destroy it when it's destroyed.
Have fun!

I don't have c++ at hand, but it should be easy to convert:
QtGui.QIcon(
QtGui.QPixmap.fromImage(
QtGui.QImage.fromData(
b'<svg version="1.1" viewBox="0 0 32 32"'
b' xmlns="http://www.w3.org/2000/svg">'
b'<circle cx="16" cy="16" r="4.54237"/></svg>')))
This code will create transparent 32 by 32 pixels icon with a black circle.

How can we create the QIcon object having SVG icon contents in the
memory buffer?
For that all the required functionality is delivered via external interface of QSvgRenderer class. To construct that type of renderer we need to use either of:
QSvgRenderer(const QByteArray &contents, QObject *parent = Q_NULLPTR);
QSvgRenderer(QXmlStreamReader *contents, QObject *parent = Q_NULLPTR);
or we can just load the content with:
bool QSvgRenderer::load(const QByteArray &contents)
bool QSvgRenderer::load(QXmlStreamReader *contents)
And to create the actual icon:
QIcon svg2Icon(const QByteArray& svgContent, QPainter::CompositionMode mode =
QPainter::CompositionMode_SourceOver)
{
return QIcon(util::svg2Pixmap(svgContent, QSize(128, 128), mode));
}
QPixmap svg2Pixmap(const QByteArray& svgContent,
const QSize& size,
QPainter::CompositionMode mode)
{
QSvgRenderer rr(svgContent);
QImage image(size.width(), size.height(), QImage::Format_ARGB32);
QPainter painter(&image);
painter.setCompositionMode(mode);
image.fill(Qt::transparent);
rr.render(&painter);
return QPixmap::fromImage(image);
}
We can also use other composition modes as well, say,
QPainter::RasterOp_NotSourceOrDestination to invert the icon color.

Related

How to draw when holding down the mouse button?

I am trying to create a simple painting application - it is just supposed to draw when you click and drag a cursor (just like Paint). I know I have to use QPainter but how can I handle it? How to do it? Any help would be really appreciated. I tried lurking through internet but didn't find too much info (I daw drawing lines etc. by code that you launch an app and it is here but I can not find an example of drawing something by user).
Here's Jeremy's answer in code instead of prose:
// https://github.com/KubaO/stackoverflown/tree/master/questions/simplepaint-39358392
#include <QtWidgets>
// Make a subclass of the QWidget class, so that you can override some of its
// virtual methods
class PaintWidget : public QWidget {
// Create a QPixmap object that you will use to store the bitmap
// that the user will draw [on].
QPixmap m_pixmap;
QPoint m_lastPos;
// Override the paintEvent(QPaintEvent *) [...]
void paintEvent(QPaintEvent *) override {
QPainter painter{this};
painter.drawPixmap(0, 0, m_pixmap);
}
void resizeEvent(QResizeEvent *) override {
// [...] size the QPixmap to be at least as big as the maximum size of the window
// We'll also never let it shrink so as not to lose the already drawn image.
auto newRect = m_pixmap.rect().united(rect());
if (newRect == m_pixmap.rect()) return;
QPixmap newPixmap{newRect.size()};
QPainter painter{&newPixmap};
painter.fillRect(newPixmap.rect(), Qt::white);
painter.drawPixmap(0, 0, m_pixmap);
m_pixmap = newPixmap;
}
// Override the mousePressEvent(QMouseEvent *) [...]
void mousePressEvent(QMouseEvent * ev) override {
m_lastPos = ev->pos();
draw(ev->pos());
}
// Override the mouseMoveEvent(QMouseEvent *) [...]
void mouseMoveEvent(QMouseEvent * ev) override {
draw(ev->pos());
}
void draw(const QPoint & pos) {
QPainter painter{&m_pixmap};
painter.setRenderHint(QPainter::Antialiasing);
painter.setPen({Qt::blue, 2.0});
painter.drawLine(m_lastPos, pos);
m_lastPos = pos;
update();
}
public:
using QWidget::QWidget;
};
int main(int argc, char ** argv) {
QApplication app{argc, argv};
// Create an object of your subclass and call show()
PaintWidget ui;
ui.show();
return app.exec();
}
It is not necessary to override the mouseReleaseEvent. In a widget, the default behavior is to track the mouse movement only when a mouse button is depressed. The mouseMoveEvent won't be called unless a button is depressed.
It's a pretty broad question, but here are the basics:
Make a subclass of the QWidget class, so that you can override some of its virtual methods later on.
Create an object of your subclass and call show() on it (just before calling QApplication::exec()). This object will appear on screen as a very simple window, and it will serve as your user's painting-surface.
Create a QPixmap object that you will use to store the bitmap that the user will draw. Make sure to size the QPixmap to be at least as big as the maximum size of the window that you want to support. Call fill() on the QPixmap to fill it with your favorite background color.
Override the mousePressEvent(QMouseEvent *) method of your object to set a boolean is_mouse_down flag, and also to record the current position of the mouse pointer within the window (by calling pos() on the QMouseEvent object that gets passed in to the mousePressEvent() call and storing that into a member variable of your object).
Override the mouseMoveEvent(QMouseEvent *) method so that if is_mouse_down_is set to true, it creates a QPainter object on the stack -- pass a pointer to the QPixmap to the QPainter object's constructor so that the QPainter will draw into your QPixmap object. Then call drawLine() on the QPainter object to draw a line from the previous mouse position to your current one. Finally, call update() to tell Qt to call paintEvent() for you ASAP.
Override the mouseReleaseEvent(QMouseEvent *) method to set is_mouse_down to false again
Override the paintEvent(QPaintEvent *) method to create a QPainter object on the stack -- pass a pointer to (this) to the QPainter object's constructor, so that it will paint onto the QWidget directly. Then call drawPixmap() on the QPainter object so that it will draw your QPixmap object onto the widget's visible surface.
If you'd like to see a pre-written example, check out the Scribble application included with Qt, in $QTDIR/examples/widgets/widgets/scribble.

Can't use QPainter in paintEvent of custom QWidget (Qt5)

The error in question is as follows:
QWidget::paintEngine: Should no longer be called
QPainter::begin: Paint device returned engine == 0, type: 1
QPainter::setPen: Painter not active
QPainter::setFont: Painter not active
After looking at a ton of online forum posts that all came down to people making the same mistake of trying to paint on their widgets outside of paintEvent(), I've been unable to have any luck. I tried drawing directly on this custom QWidget subclass, I've tried making a child QWidget and drawing on that. Can someone please show me what thing I'm (probably obviously to someone else) doing wrong?
Thanks in advance.
Header:
#ifndef TEXTDISPLAY_H
#define TEXTDISPLAY_H
#include <QWidget>
class TextDisplay : public QWidget
{
Q_OBJECT
public:
TextDisplay(QString text, QString fontFamily = "Helvetica", int fontSize = 20,
int fontColor = Qt::black, QWidget* parent = 0);
protected:
void paintEvent(QPaintEvent *e) Q_DECL_OVERRIDE;
void resizeEvent(QResizeEvent *e) Q_DECL_OVERRIDE;
private:
QString text;
QString fontFamily;
int fontSize;
int fontColor;
};
#endif // TEXTDISPLAY_H
Cpp:
#include "textdisplay.h"
#include <QPainter>
TextDisplay::TextDisplay(QString text, QString fontFamily, int fontSize,
int fontColor, QWidget* parent)
: QWidget(parent), text(text), fontFamily(fontFamily),
fontSize(fontSize), fontColor(fontColor)
{
this->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Minimum);
}
void TextDisplay::resizeEvent(QResizeEvent*) {
paintEvent(NULL);
}
void TextDisplay::paintEvent(QPaintEvent *) {
QPainter painter(this);
painter.setPen(fontColor);
painter.setFont(QFont(fontFamily, fontSize));
QRect rect(QPoint(0, 0), this->size());
QRect bound;
QTextOption options;
options.setWrapMode(QTextOption::WrapAtWordBoundaryOrAnywhere);
painter.drawText(rect, Qt::TextWordWrap | Qt::TextWrapAnywhere, text, &bound);
this->setMinimumHeight(bound.size().height());
}
Edit with solution:
Thanks to both responders - I needed to do two things to get it working:
a) Get rid of paintEvent(NULL). I got rid of the resizeEvent override as well, it was unnecessary as suggested.
b) Set a minimum size for the widget. Without this, the paintEvent was never called by Qt.
If you want to schedule a redraw for your widget, just call update().
If you need an immediate repaint (which you almost never need) you can call repaint() instead.
Calling paintEvent() directly will not work -- Qt needs to prepare the backing store to handle the painting, so you can't bypass the update mechanism. Instead, call the methods above, which will result in a call to paintEvent() (if the widget is visibile, not occluded, etc.etc.etc.).
Note also that you should not need to reimplement resizeEvent() just to update your widget. That should already be done for you by Qt...

QGraphicsScene paint event inside QGraphicsProxyWidget

I am streaming from a webcam using OpenCV. Each captured frame (type is cv::Mat) is transported via custom signal to slots in a node-based QGraphicsScene. Since I have a single type of node that actually requires to display the cv::Mat while the rest is just for processing the data I have created a viewer widget that converts the cv::Mat data to QImage data and then using paintEvent() draws it on its surface:
Here is the code for the widget itself:
qcvn_node_viewer.hpp
#ifndef QCVN_NODE_VIEWER_HPP
#define QCVN_NODE_VIEWER_HPP
#include <QWidget>
#include <QImage>
#include <QSize>
#include <QPaintEvent>
#include <opencv2/core.hpp>
class QCVN_Node_Viewer : public QWidget
{
Q_OBJECT
QImage output;
public:
explicit QCVN_Node_Viewer(QWidget *parent = 0);
~QCVN_Node_Viewer();
QSize sizeHint() const;
QSize minimumSizeHint() const;
// Source: http://stackoverflow.com/a/17137998/1559401
static QImage Mat2QImage(cv::Mat const& frame);
static cv::Mat QImage2Mat(QImage const& src);
protected:
void paintEvent(QPaintEvent *event);
signals:
// Nodes connect to sendFrame(cv::Mat) signal
void sendFrame(cv::Mat);
public slots:
void receiveFrame(cv::Mat frame);
};
#endif // QCVN_NODE_VIEWER_HPP
qcvn_node_viewer.cpp
#include "qcvn_node_viewer.hpp"
#include <opencv2/imgproc.hpp>
#include <QPainter>
QCVN_Node_Viewer::QCVN_Node_Viewer(QWidget *parent) :
QWidget(parent)
{
qRegisterMetaType<cv::Mat>("cv::Mat");
}
QCVN_Node_Viewer::~QCVN_Node_Viewer()
{
}
QSize QCVN_Node_Viewer::sizeHint() const
{
return output.size();
}
QSize QCVN_Node_Viewer::minimumSizeHint() const
{
return output.size();
}
QImage QCVN_Node_Viewer::Mat2QImage(cv::Mat const& frame)
{
cv::Mat temp;
cvtColor(frame, temp,CV_BGR2RGB);
QImage dest((const uchar *) temp.data, temp.cols, temp.rows, temp.step, QImage::Format_RGB888);
dest.bits(); // enforce deep copy
return dest;
}
cv::Mat QCVN_Node_Viewer::QImage2Mat(QImage const& frame)
{
cv::Mat tmp(frame.height(),frame.width(),CV_8UC3,(uchar*)frame.bits(),frame.bytesPerLine());
cv::Mat result; // deep copy just in case (my lack of knowledge with open cv)
cvtColor(tmp, result,CV_BGR2RGB);
return result;
}
void QCVN_Node_Viewer::paintEvent(QPaintEvent *event)
{
QPainter painter(this);
painter.drawImage(QPoint(0,0), output);
painter.end();
}
void QCVN_Node_Viewer::receiveFrame(cv::Mat frame)
{
if(frame.empty()) return;
// Pass along the received frame
emit sendFrame(frame);
// Generate
// http://stackoverflow.com/questions/17127762/cvmat-to-qimage-and-back
// Applications crashes sometimes! Conversion seems to be incorrect
output = Mat2QImage(frame);
output.bits();
setFixedSize(output.width(), output.height());
repaint();
}
As you can see from the screenshot above the QWidget is embedded in a QGraphicsScene using QGraphicsProxyWidget along with a QGraphicsRectItem that I use for selecting/moving the node around in the scene.
Using QPainter is considered to be more efficient than using QLabel and settings its QPixmap to the captured and converted frame. Using an OpenGL viewport inside a QGraphicsScene as far as I know is not an option. It seems that processing the paint events in the scene also handles the local ones inside the QGraphicsItems and QGraphicsProxyWidgets. The frame that the viewer paints on its surface changes only when the scene's paint event handler is called (hovering some of my nodes, moving them around etc.).
Is there a way to fix this? I'm about to go for the QLabel+QPixmap option (even though I'm not sure if it won't suffer from the same or different problem) but first I'd like to make sure there is nothing I can do to keep my current way of doing things.
EDIT: Okay, so I have changed my viewer to use QLabel instead of the widget's paint event and it works like a charm. The question is still opened though since I'd like to know how to use local paint events.

Qt Scrollbar doesn't appear on custom widget

I tried to modify this example code (http://doc.qt.digia.com/4.6/widgets-scribble.html) so the image can be scrolled when the size is too big.
Basically this is a code to create a mspaint-like program (my code is almost identical to the tutorial, except i changed the class name):
class ImageDisplay : public QWidget
{
Q_OBJECT
public:
ImageDisplay(QWidget *parent = 0);
~ImageDisplay();
void LoadImage(QString img);
protected:
void mousePressEvent(QMouseEvent *event);
void mouseMoveEvent(QMouseEvent *event);
void mouseReleaseEvent(QMouseEvent *event);
void paintEvent(QPaintEvent *event);
void resizeEvent(QResizeEvent *event);
private:
bool modified;
bool scribbling;
int myPenWidth;
QColor myPenColor;
QImage svgImage;
QPoint lastPoint;
private:
void ResizeImage(QImage *image, const QSize &newSize);
void DrawLineTo(const QPoint &endPoint);
};
Code attaching custom widget to scroll area
...
scrollArea = new QScrollArea(centralWidget);
scrollArea->setWidgetResizable(true);
myImageDisplay = new ImageDisplay();
myImageDisplay->setGeometry(QRect(0, 0, 780, 559));
QSizePolicy sizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred);
myImageDisplay->setSizePolicy(sizePolicy);
myImageDisplay->setMinimumSize(QSize(0, 0));
scrollArea->setWidget(myImageDisplay);
gridLayout->addWidget(scrollArea, 0, 0, 1, 1);
...
I put my custom widget inside scrollarea, but the scrollbar never appears. When i debug it, the size of the widget cannot be larger than the srollarea.
I've read somewhere the size of a widget can;t be expanded larger than the container size, or sort of, i don't quite understand.
i found a inelegant solution which seems to be "hack" where i set the minimum size of the widget to be the size of the image, i put the code inside load image
void ImageDisplay::LoadImage(QString img)
{
QImage loadedImage;
loadedImage.load(img);
QSize newSize = loadedImage.size().expandedTo(size());
ResizeImage(&loadedImage, newSize);
svgImage = loadedImage;
modified = false;
update();
// "hack" code
this->setMinimumHeight(newSize.height());
this->setMinimumWidth(newSize.width());
}
This isn't really a "hack." This is how it is supposed to be. If you want the widget to never be smaller than the image it displays, then that's what you do; you tell it that it should never make itself smaller than the image. But in your code, you've specified that it can be smaller than its contents:
QSizePolicy sizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred);
What this means is that the widget will prefer to have a size that fits inside the visible area of its container, even if it means that its contents are getting cut off.
The more elegant way to do this is though, is using this instead:
QSizePolicy sizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
This will tell the layout to never try and resize the widget at all. You set a size when you load the image, and it will keep that size unless you manually resize it yourself.
With that being said, maybe you should be looking at using a QLabel instead of a custom QWidget class. QLabel is already set up to display images, and I believe it knows on its own which size it should be.

Rendering QImage on QGLWidget of QML plugin

I'm trying to write a QML plugin that reads frames from a video (using a custom widget to do that task, NOT QtMultimedia/Phonon), and each frame is converted to a QImage RGB888, and then displayed on a QGLWidget (for performance reasons). Right now nothing is draw to the screen and the screen stays white all the time.
It's important to state that I already have all of this working without QGLWidget, so I know the issue is setting up and drawing on QGLWidget.
The plugin is being registered with:
qmlRegisterType<Video>(uri,1,0,"Video");
so Video is the main class of the plugin. On it's constructor we have:
Video::Video(QDeclarativeItem* parent)
: QDeclarativeItem(parent), d_ptr(new VideoPrivate(this))
{
setFlag(QGraphicsItem::ItemHasNoContents, false);
Q_D(Video);
QDeclarativeView* view = new QDeclarativeView;
view->setViewport(&d->canvas()); // canvas() returns a reference to my custom OpenGL Widget
}
Before I jump to the canvas object, let me say that I overloaded Video::paint() so it calls canvas.paint() while passing QImage as parameter, I don't know if this is the right way to do it so I would like some advice on this:
void Video::paint(QPainter* painter, const QStyleOptionGraphicsItem* option, QWidget* widget)
{
Q_UNUSED(painter);
Q_UNUSED(widget);
Q_UNUSED(option);
Q_D(Video);
// I know for sure at this point "d->image()" is valid, but I'm hiding the code for clarity
d->canvas().paint(painter, option, d->image());
}
The canvas object is declared as GLWidget canvas; and the header of this class is defined as:
class GLWidget : public QGLWidget
{
Q_OBJECT
public:
explicit GLWidget(QWidget* parent = NULL);
~GLWidget();
void paint(QPainter* painter, const QStyleOptionGraphicsItem* option, QImage* image);
};
Seems pretty simple. Now, the implementation of QGLWidget is the following:
GLWidget::GLWidget(QWidget* parent)
: QGLWidget(QGLFormat(QGL::SampleBuffers), parent)
{
// Should I do something here?
// Maybe setAutoFillBackground(false); ???
}
GLWidget::~GLWidget()
{
}
And finally:
void GLWidget::paint(QPainter* painter, const QStyleOptionGraphicsItem* option, QImage* image)
{
// I ignore painter because it comes from Video, so I create a new one:
QPainter gl_painter(this);
// Perform drawing as Qt::KeepAspectRatio
gl_painter.fillRect(QRectF(QPoint(0, 0), QSize(this->width(), this->height())), Qt::black);
QImage scaled_img = image->scaled(QSize(this->width(), this->height()), _ar, Qt::FastTransformation);
gl_painter.drawImage(qRound(this->width()/2) - qRound(scaled_img.size().width()/2),
qRound(this->height()/2) - qRound(scaled_img.size().height()/2),
scaled_img);
}
What am I missing?
I originally asked this question on Qt Forum but got no replies.
Solved. The problem was that I was trying to create a new GL context within my plugin when I should be retrieving the GL context from the application that loaded it.
This code was very helpful to understand how to accomplish that.
By the way, I discovered that the stuff was being draw inside view. It's just that I needed to execute view->show(), but that created another window which was not what I was looking for. The link I shared above has the answer.
I think that you have to draw on your glwidget using the opengl functions.
One possible way is to override your paintGL() method in GLWidget
and then draw your image with glDrawPixels().
glClear(GL_COLOR_BUFFER_BIT);
glDrawPixels(buffer.width(), buffer.height(), GL_RGBA, GL_UNSIGNED_BYTE, buffer.bits());
Where buffer is a QImage object that needs to be converted using QGLWidget::converrtToGLFormat() static method.
Look at this source for reference:
https://github.com/nayyden/ZVector/blob/master/src/GLDebugBufferWidget.cpp