How to toggle svg icons on button click - Using QT C++ - qt

I am relatively new to svg + QT concepts.
I have created an svg icon which has 2 states.
I want to add this svg icon to a QPushButton. When the button is clicked,the icon should get toggled ( show the other state ).
Can anybody pls give me a heads up on how to do this in QT? QT with C++!
Edit:
I have tried using the svg icon and toggling it when button is clicked.
I have achieved the expected behavior but I wonder if this is the correct way of doing it.
I am pasting the code files and the svg icon content below
I have tried something using the QSvgRenderer and QIcon.
I have achieved what I was looking for.
But I am not sure if this is how it should be done.
Please have a look and let me know if this is the preferred way or not.
I have created a svg icon ( using 2 other svg icons ) from the https://nucleoapp.com/tool/icon-transition
The svg icon has 2 svg icons embedded into it. My objective now is to toggle the icons when button is clicked.
Checkout my files below
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
#include <QSvgRenderer>
namespace Ui {
class MainWindow;
}
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
explicit MainWindow(QWidget *parent = nullptr);
~MainWindow();
private:
Ui::MainWindow *ui;
bool isDefaultIconLoaded;
QString otherIconSvgContent;
QString defaultIconSvgContent;
QSvgRenderer m_svgRenderer;
void loadCurrentSvgIcon(QByteArray ba);
private slots:
void toolBtnClicked();
};
#endif // MAINWINDOW_H
The cpp file
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <QFile>
#include <QPainter>
#include <QtSvg/QSvgRenderer>
#include <QPixmap>
#include <QDomDocument>
#include <QDomNodeList>
#include <QTextStream>
#include <QDebug>
MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::MainWindow),
isDefaultIconLoaded(true)
{
ui->setupUi(this);;
connect(ui->toolButton, &QToolButton::clicked, this, &MainWindow::toolBtnClicked);
ui->toolButton->setStyleSheet("border:none;");
// open svg icon which has multiple states
QFile file(":/icon/theSvg.svg");
file.open(QIODevice::ReadOnly);
const QByteArray baData = file.readAll();
QDomDocument doc;
doc.setContent(baData, false);
// Read both the <g> elements from the .svg xml and store each 'icon content' in member var.
QDomNodeList gList = doc.elementsByTagName("g");
for(int i = 0; i < gList.size(); i++)
{
QString aContent;
QTextStream ts(&aContent);
gList.at(i).save(ts,0);
qDebug("%s", qPrintable(aContent));
if( isDefaultIconLoaded )
{
defaultIconSvgContent.insert(0, "<svg>");
defaultIconSvgContent += aContent;;
defaultIconSvgContent.append("</svg>");
isDefaultIconLoaded = false;
qDebug() <<defaultIconSvgContent;
}
else {
otherIconSvgContent.insert(0, "<svg>");
otherIconSvgContent += aContent;
otherIconSvgContent.append("</svg>");
qDebug() <<otherIconSvgContent;
}
}
// Load the default svg icon first
QByteArray ba = defaultIconSvgContent.toUtf8();
isDefaultIconLoaded = true;
loadCurrentSvgIcon(ba);
}
MainWindow::~MainWindow()
{
delete ui;
}
void MainWindow::loadCurrentSvgIcon(QByteArray ba)
{
m_svgRenderer.load(ba);
QPixmap pix(m_svgRenderer.defaultSize());
QPainter pixPainter(&pix);
m_svgRenderer.render(&pixPainter);
QIcon myicon(pix);
ui->toolButton->setIcon(myicon);
}
void MainWindow::toolBtnClicked()
{
if ( isDefaultIconLoaded == true )
{
// Toggle the icon to "other"
QByteArray ba = otherIconSvgContent.toUtf8();
loadCurrentSvgIcon(ba);
isDefaultIconLoaded = false;
}
else {
QByteArray ba = defaultIconSvgContent.toUtf8();
loadCurrentSvgIcon(ba);
isDefaultIconLoaded = true;
}
}
Content of the svg file
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" height="16" width="16">
<g
class="nc-icon-wrapper" stroke-width="1" fill="#111111" stroke="#111111">
<title>default</title>
<path
fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" data-color="color-2" d="M1.5 1.5h2">
</path>
<circle
fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" cx="9" cy="9" r="3.5" data-color="color-2">
</circle>
<path
fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" data-color="color-2" d="M2.5 5.5h1">
</path>
<path
fill="none" stroke="#111111" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" d="M14.5 3.5h-2l-1-2h-5l-1 2h-4a1 1 0 0 0-1 1v9a1 1 0 0 0 1 1h13a1 1 0 0 0 1-1v-9a1 1 0 0 0-1-1z">
</path>
</g>
<g
class="nc-icon-wrapper" fill="#111111">
<title>other</title>
<path
fill="#111111" d="M15,3h-2.5l-1.7-2.6C10.6,0.2,10.3,0,10,0H6C5.7,0,5.4,0.2,5.2,0.4L3.5,3H1C0.4,3,0,3.4,0,4v11 c0,0.6,0.4,1,1,1h14c0.6,0,1-0.4,1-1V4C16,3.4,15.6,3,15,3z M14,14H2V5h2c0.3,0,0.6-0.2,0.8-0.4L6.5,2h2.9l1.7,2.6 C11.4,4.8,11.7,5,12,5h2V14z">
</path>
<circle
data-color="color-2" cx="8" cy="9" r="3">
</circle>
</g>
</svg>
basically, each 'g' element represents an icon.
I am reading the each g element contents in C'tor and storing them into member variables ( in order to avoid reading again and again ).
Then I am "loading" the icon based on the click event using the "QSvgRenderer".
This works as expected, but is it the best practice or is there a better way I can acheive this ?

Related

SVG with marker having an issue on mouse over

I have a button with svg inside like:
<button>
Checkout
<ArrowSvg />
</button>
And ArrowSvg looks like this (line has class: svg-line):
<svg fill="none" stroke="#000">
<defs>
<marker id="m" overflow="visible">
<path d="M-4,-4L0,0 -4,4" />
</marker>
</defs>
<line x1="0" y1="50%" x2="100%" y2="50%"marker-end="url(#m)" class="svg-line" />
</svg>
When button is hovered, I change the stroke color:
btn:hover > .svg-line {
stroke: blue;
}
It's working well - when I hover the button, the arrow (line and arrow head) turns blue.
But when I display the multiple buttons (more than 1), hovering 1 button affects arrows in other buttons. The arrow head part (which is marker/path in svg?) of all the buttons get affected when hovering the first button.
I am also working with the arrow width, so I need line. I can't make everything with path because I won't be able to expand the arrow width.
Am I missing anything? Why am I seeing this issue?
Wrap it in a native JS Web Component, supported in all modern browsers, to create the <svg>
because every SVG requires a unique marker ID
note: the SVG is created for every instance, no need for a marker at all, use the path by itself
customElements.define("svg-button",class extends HTMLElement{
connectedCallback(){
let id = "id" + (Math.floor(Math.random() * 1e10));
let stroke = this.getAttribute("stroke") || "#000";
this.innerHTML = `
<button>
Checkout
<svg fill="none" stroke="${stroke}" viewBox="0 0 10 10">
<defs>
<marker id="${id}" overflow="visible">
<path d="M-4,-4L0,0 -4,4"/>
</marker>
</defs>
<line x1="0" y1="5" x2="9" y2="5" marker-end="url(#${id})"/>
</svg>
</button>`
}
});
<style>
button:hover svg {
stroke:gold;
}
</style>
<svg-button></svg-button>
<svg-button stroke="red"></svg-button>
<svg-button stroke="green"></svg-button>
<svg-button stroke="blue"></svg-button>
What you are writing looks like .JSX syntax, but the question is lacking a react tag. I will assume it for this answer anyway, but other frameworks using the format will probably work comparably.
All you need is a unique id for the <marker> element. This is easily done if you spell out the <ArrowSvg> as a function. Then, wrap it in a factory function to form a closure over a running number:
const ArrowSvg = (() => {
let id = 0;
return function (props) {
return (
const ref = 'arrowMarker' + ++id;
<svg fill="none" stroke="#000">
<defs>
<marker id=(ref) overflow="visible">
<path d="M-4,-4L0,0 -4,4" />
</marker>
</defs>
<line x1="0" y1="50%" x2="100%" y2="50%"
marker-end=(`url(#${ref})`) class="svg-line" />
</svg>
);
}
})();

How to make QSvgGenerator produce vectorized SVG instead of bitmapped SVG?

I'm trying to export my QGraphicsScene to an SVG like this:
void MyScene::toSvg(QString filename)
{
QSvgGenerator svgGen {};
svgGen.setFileName(filename);
svgGen.setSize({ 200, 200 });
svgGen.setViewBox(QRect(0, 0, 200, 200));
QPainter painter {};
painter.begin(&svgGen);
render(&painter);
painter.end();
}
MyScene is inherited from QGraphicsScene.
In my scene I have objects that are inherited from QGraphicsItem.
My item renders like this:
void Node::paint(QPainter * painter, const QStyleOptionGraphicsItem * option, QWidget * widget)
{
Q_UNUSED(widget)
Q_UNUSED(option)
painter->save();
QPainterPath path;
const QRectF rect(-m_size.width() / 2, -m_size.height() / 2, m_size.width(), m_size.height());
path.addRoundedRect(rect, m_cornerRadius, m_cornerRadius);
painter->setRenderHint(QPainter::Antialiasing);
painter->fillPath(path, QBrush(m_color));
painter->restore();
}
Now the problem is that I'm only getting a bitmapped SVG so it's basically useless. Something like this:
<image x="38" y="59" width="41" height="15" preserveAspectRatio="none" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACkAAAAPCAYAAAB5lebdAAAACXBIWXMAAA7EAAAOxAGVKw4bAAABGUlEQVRIie2TwWqEMBCG/2TXgCwe40lY8OhL6Ev4PEuex5fQl/AoCHsyeFIQdZ301FLFtVRLaWG/45BkvsnMAC9e/C3YN+N7MUcuL2VYFEWnqqr49XplAFCWpXFdl44kAYAwDEkpZbBDeCYZx/FJa22N42gRkQUAnPPxcrkckmyahjzPm7TW9El2jdX4TDKKonPf9xYRWZzzMwAQ0cO2bRJC7G5ZXdfGcRyybdtsdebZb2+2u+s6JqWkoigOzRQA+L7PtNa873vmOA5fFiGEMJ7nTUEQPJRSsyLOi7dMmqYTgCnP86NeH9xuN5ZlGZdS4n6/n9bODMPA2rZdXdif3uIt2LvsswNSSpMkCeGLdv8Wz/IeHqsX/543cmeARGeRY78AAAAASUVORK5CYII=" />
Should I do something special in my items in order to actually get SVG instead of bitmaps?
EDIT: This seems to be related, but disabling cache didn't help in my case with plain QGraphicsItems: QSvgGenerator converts QSvgGraphicsItem to image when generating Svg
It seems that setting QGraphicsDropShadowEffect for the items forces them as bitmaps (together with the effect graphics). The SVG will be vectorized if I don't set this effect.

Dynamically find the path of SVG

I have this SVG file
I want to know the equation of the line. is it possible? I need the equation to dynamically add some SVG points on this line in different positions. So, in that case, I know the x value but I need to calculate the y value.
Here is the source of the SVG file:
<?xml version="1.0" encoding="UTF-8"?>
<svg width="1366px" height="332px" viewBox="0 0 1366 332" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 55.1 (78136) - https://sketchapp.com -->
<title>#0.5xTimeline - Deactivated</title>
<desc>Created with Sketch.</desc>
<g id="🖥-Design---Desktop" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="02-About-Us" transform="translate(0.000000, -989.000000)" fill-rule="nonzero" stroke="#EFEFEF" stroke-width="11">
<g id="HISTORY" transform="translate(-28.000000, 813.000000)">
<g id="Graphic---Timeline" transform="translate(0.000000, 182.000000)">
<path d="M0,38.8855723 C99.4313478,-10.5096141 202.976477,-12.845873 310.635387,31.8767959 C472.123752,98.9607991 512.237609,231.556286 773.130376,301.214063 C1034.02314,370.871839 1355.95795,229.12379 1417,164.042295" id="Timeline---Deactivated"></path>
</g>
</g>
</g>
</g>
</svg>
If you know that you have a function, that is a path that only ever goes left-to-right and never doubles back on itself, then you can scan along its length until you find the x-value that you want.
const x = 100; // known x-value
const path = document.getElementById('Timeline---Deactivated');
const pathLength = path.getTotalLength();
const range = [0, pathLength];
let guess, result;
for( let iterations = 0; iterations < 11; iterations++) {
guess = (range[0]+range[1])/2;
result = path.getPointAtLength(guess);
if( result.x < x) {
// target is to the right
range[0] = guess;
}
else range[1] = guess;
}
return result; // SVGPoint with result.x and result.y
The ideal number of iterations is based on the possible range of X values. In your case the path ends at (1417,164), so iterations should go to ceil(log_2(1417)) which is 11. If you have bigger graphs, you might need to make it 12, or you can just set it to 20 or something. It's not like it takes that many more steps to keep a high accuracy result.

Saving a QGraphicsScene to Svg changes scaling

I need to save the items from my QGraphicsScene to an svg, and be able to load that svg back on the scene.
I can do it...
But each time the canvas is saved to svg, upon load the items are somewhat bigger (and repeatedly saving and loading the same svg causes it to grow).
I can't find the cause.
I am attaching a sample code - and the result.
test1.pro
QT += gui svg
greaterThan(QT_MAJOR_VERSION, 4): QT += widgets core
TARGET = test1
TEMPLATE = app
SOURCES += \
svggenerator.cpp
svggenerator.cpp
#include <QApplication>
#include <QGraphicsView>
#include <QGraphicsEllipseItem>
#include <QGraphicsSvgItem>
#include <QSvgGenerator>
#include <QSvgRenderer>
#include <QFile>
#include <QByteArray>
#include <QMessageBox>
void saveSceneToSvg(QGraphicsScene* s, const QString &filename) {
QRectF newSceneRect;
QGraphicsScene *tempScene = new QGraphicsScene(s->sceneRect());
tempScene->setBackgroundBrush(QBrush(Qt::transparent));
tempScene->setItemIndexMethod(QGraphicsScene::BspTreeIndex);
foreach(QGraphicsItem* item, s->items()) {
newSceneRect |= item->mapToScene(item->boundingRect()).boundingRect();
tempScene->addItem(item);
}
tempScene->setSceneRect(newSceneRect);
tempScene->clearSelection();
QSize sceneSize = newSceneRect.size().toSize();
QSvgGenerator generator;
generator.setFileName(filename);
generator.setSize(sceneSize);
generator.setViewBox(QRect(0, 0, sceneSize.width(), sceneSize.height()));
generator.setDescription(QObject::tr("My canvas exported to Svg"));
generator.setTitle(filename);
QPainter painter;
painter.begin(&generator);
tempScene->render(&painter);
painter.end();
tempScene->clear();
delete tempScene;
}
void loadSvg(QGraphicsScene* s, const QString &filename, const QPointF& p) {
QGraphicsSvgItem* item = new QGraphicsSvgItem();
QFile file(filename);
file.open(QFile::ReadOnly);
QByteArray contents = file.readAll();
item->setSharedRenderer(new QSvgRenderer(contents));
file.close();
item->setPos(p);
s->addItem(item);
}
void processScene(QGraphicsScene* s) {
QGraphicsEllipseItem* eli = new QGraphicsEllipseItem();
eli->setRect(QRectF(0, 0, 100, 100));
eli->setPen(Qt::NoPen);
eli->setBrush(Qt::red);
eli->setPos(100, 300);
s->addItem(eli);
QGraphicsEllipseItem* eli1 = new QGraphicsEllipseItem();
eli1->setRect(QRectF(0, 0, 100, 100));
eli1->setPen(Qt::NoPen);
eli1->setBrush(Qt::yellow);
eli1->setPos(150, 300);
s->addItem(eli1);
QMessageBox::information(NULL, "hi", "click");
saveSceneToSvg(s, "abcd.svg");
loadSvg(s, "abcd.svg", QPointF(100,300));
QMessageBox::information(NULL, "hi", "click");
saveSceneToSvg(s, "abcd1.svg"); // saved with a dif name so I can see
loadSvg(s, "abcd1.svg", QPointF(100,300));
QMessageBox::information(NULL, "hi", "click");
saveSceneToSvg(s, "abcd2.svg");
loadSvg(s, "abcd2.svg", QPointF(100,300));
// .... each time i call them they grow larger
}
int main(int argc, char *argv[]) {
QApplication app(argc, argv);
QGraphicsScene s;
s.setSceneRect(50, 0, 1000, 800);
QGraphicsView view(&s);
view.show();
processScene(&s);
return app.exec();
}
Result:
Looking at the svgs themselves, I can see that the svgs increase in size by approx 1.25... I can't explain, and can't be sure this will be true for other examples. (it seems to)
What is causing this growth ? How can I stop it ?
(Also I notice the ordering is different ... I just noticed and that is a different problem... But since in my "real" code I also have z order I don't care.)
Saving I think is fine - the resulting svg has size and view box of expected size.
Loading other svgs is fine - saving an svg from outside source creates an svg similar in size.
It seems the problem is when I am loading an svg created by the svg generator that it increases in size.
(If I could be sure it is always the case I could try scaling it down on load, but the ratio is not exactly 1.25 each time, close though... and I don't know how to tell the difference between an outside svg and a generated one).
It looks like a bug of SvgGenerator. The size which you provide to SVG generator is used only to form the header of svg. The actual size of .svg file differs from one written in the header. The only workaround I found is simple 25 percent decrease of size on save similar to:
int width = qCeil(qreal(sceneSize.width()/1.25));
int height = qCeil(qreal(sceneSize.height()/1.25));
generator.setSize(QSize(width, height));
generator.setViewBox(QRect(0, 0, width, height));

How To Generate SVG using MVC Razor

We am trying to make an SVG generated radio button control for my MVC3 application. Ideally, the control would be a partial view and we would pass in a model which would contain data on how to generate the SVG using Razor.
We have tried to write the SVG into Razor normally, but we get nothing but compile errors.
How can we generate SVG using MVC3 Razor?
The error is: Expression Must Return a Value To Render
EDIT: The error is not coming from within the partial view, but when we call #Html.RenderPartial("SvgRadioButtonControl", ((IResponseLabeledItem)Model).ResponseOptions)
it gives the error.
#using SmartQWeb.Models.Entities
#model IEnumerable<ResponseOption>
<svg
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
version="1.1"
width="300"
height="400"
id="radio">
<script type="application/ecmascript"><![CDATA[
var innerCircleExpandedSize = 11;
function ResponseOptionClicked(evt) {
console.log('Response option clicked');
// Remove circle in enabled element
var enabledElem = document.getElementsByClassName('enabled')[0];
if (enabledElem != undefined) {
console.log('Removing a inner circle');
enabledElem.getElementsByClassName('response-innercircle')[0].setAttribute('r', 0);
enabledElem.className.baseVal = enabledElem.className.baseVal.replace('enabled', 'disabled')
}
// Add circle in radio button
console.log('Adding a inner circle');
evt.currentTarget.getElementsByClassName('response-innercircle')[0].setAttribute('r', innerCircleExpandedSize);
evt.currentTarget.className.baseVal = evt.currentTarget.className.baseVal.replace('disabled', 'enabled');
}
]]></script>
<g id="base">
#{
int iteration = 1;
}
#foreach (ResponseOption option in Model)
{
<g id="response-never" class="response-option disabled" transform="translate(50,#{ (iteration++ * 1).ToString(); })" onclick="ResponseOptionClicked(evt)" fill="#ffffff">
<circle class="response-outercircle" r="18" stroke="#000000" stroke-width="3" stroke-miterlimit="4" stroke-opacity="1" stroke-dasharray="none" />
<circle class="response-innercircle" r="0" fill="#000000" stroke="#000000" />
<text x="40" y="6.5" font-size="1.5em" fill="blue" class="response-text">#option.Label</text>
</g>
}
</g>
</svg>
Replace:
#Html.RenderPartial("SvgRadioButtonControl", ((IResponseLabeledItem)Model).ResponseOptions)
with:
#{Html.RenderPartial("SvgRadioButtonControl", ((IResponseLabeledItem)Model).ResponseOptions);}
or if you prefer:
#Html.Partial("SvgRadioButtonControl", ((IResponseLabeledItem)Model).ResponseOptions)
Also the following expression in your partial just stinks:
#{ (iteration++ * 1).ToString(); })
Didn't you mean:
#(iteration++)

Resources