Set renderer of QGraphicsSvgItem on preprocessed s

2019-05-31 09:32发布

I am using QGraphicsSvgItem subclass, that reads some content from a file, places content into a QDomDocument, does some initial processing, then sets the processed DOM onto a renderer.

During program processing, additional changes are required on a copy of pre-processed DOM, so the DOM is stored in class. After changes, the DOM is placed on renderer.

class MyGraphicsSvgItem : public QGraphicsSvgItem
{
public:
    MyGraphicsSvgItem (QGraphicsItem *parent = 0):
        QGraphicsSvgItem(parent),
        _svgXML() {}
    ~MyGraphicsSvgItem () { delete renderer(); }
    void CheckAndChangeSomeThings() {}
    void LoadStuff (QString fileName)
    {
        QFile file(fileName);
        file.open(QFile::ReadOnly | QFile::Text);
        QTextStream in(&file);
        QString svgContent = in.readAll();
        file.close();
        _svgXML.setContent(svgContent);
        CheckAndChangeSomeThings();   // this modifies _svgXML
        QByteArray _data = _svgXML.toByteArray();
        setSharedRenderer(new QSvgRenderer(_data));  // very slow
    }
    void ChangeThingslater();
    void ChangeSomeThingslater() 
    {
        ChangeThingslater(); // this modifies _svgXML
        renderer()->load(_svgXML.toByteArray());  // very slow - no file involved
    }
protected:
    QDomDocument _svgXML;
};

There seems to be a significant slow processing during the lines that assign the DOM to the renderer.

QByteArray _data = _svgXML.toByteArray();
setSharedRenderer(new QSvgRenderer(_data));

If I skip the DOM processing - if I set the renderer to the file - the code speeds up considerably:

Leaving all the code in, but replacing

setSharedRenderer(new QSvgRenderer(_data));   // VERY SLOW 

with

setSharedRenderer(new QSvgRenderer(fileName));   // FAST

So it seems the bottleneck is loading the svg renderer from QByteArray.

I looked for alternatives... there is no mention of performance in documentation

QSvgRenderer::QSvgRenderer(const QString & filename, QObject * parent = 0)
Constructs a new renderer with the given parent and loads the contents of the SVG file with the specified filename.

QSvgRenderer::QSvgRenderer(const QByteArray & contents, QObject * parent = 0)
Constructs a new renderer with the given parent and loads the SVG data from the byte array specified by contents.

QSvgRenderer::QSvgRenderer(QXmlStreamReader * contents, QObject * parent = 0)
Constructs a new renderer with the given parent and loads the SVG data using the stream reader specified by contents.

Looking in QXmlStreamReader Class, I find that its constructors are similar ! In addition, it says

In some cases it might also be a faster and more convenient alternative for use in applications that would otherwise use a DOM tree

I seem to be going in circles, and even though having already a well formed xml in the DOM, it seems I cannot take advantage of it !

What are my alternatives, to either loading the renderer from the pre-processed DOM, or to a different way of pre-processing the xml - using something other than DOM that the renderer can read fast ?

qt 4.8. c++

1条回答
ゆ 、 Hurt°
2楼-- · 2019-05-31 09:49

You can do all the DOM processing in a worker method that's executed on the thread queue using QtConcurrent::run.

You can use the QSvgRenderer directly in your item. Initialize it in the worker method, and load from QByteArray and not a file. You can then pass the renderer to the GUI thread and use it to render the graphics item by setting it on the QGraphicsSvgItem.

Caveats:

  1. Since you create the renderer in a worker thread, you must move it to a null thread after you've done using it in the worker thread. Conversely, you must move it to the GUI thread once it has been received by the GUI thread.

    Recall that moveToThread can only be called from the object's current thread, or any thread if thread() == 0.

  2. In Qt 4.8, there's a bug in QGraphicsSvgItem::setSharedRenderer: it doesn't properly connect the renderer's repaintNeeded signal to its update method. The work around this to connect the signal manually to your own update slot.

This will prevent the GUI from getting blocked by the long processing.

Reentering the event loop, as you do, from within the item is a source of bugs and just a very bad idea from the design standpoint. Use the non-blocking API of the file dialog instead.

Below is an example that demonstrates this technique. It also displays a small spinner when the item is being loaded/processed. There's a simulated delay for this purpose.

#include <QGraphicsView>
#include <QGraphicsSvgItem>
#include <QGraphicsSceneMouseEvent>
#include <QFileDialog>
#include <QSvgRenderer>
#include <QDomDocument>
#include <QtConcurrentRun>
#include <QFutureWatcher>
#include <QThread>
#include <QApplication>

struct Thread : public QThread { using QThread::sleep; }; // Needed for Qt 4 only

class RendererGenerator {
   QString m_fileName;
   void process(QDomDocument &) {
      Thread::sleep(3); /* let's pretend we process the DOM for a long time here */
   }
   QByteArray generate(const QByteArray & data) {
      QDomDocument dom;
      dom.setContent(data);
      process(dom);
      return dom.toByteArray();
   }
public:
   typedef QSvgRenderer * result_type;
   RendererGenerator(const QString & fileName) : m_fileName(fileName) {}
   QSvgRenderer * operator()() {
      QFile file(m_fileName);
      if (file.open(QIODevice::ReadOnly)) {
         QByteArray data = file.readAll();
         QScopedPointer<QSvgRenderer> renderer(new QSvgRenderer);
         renderer->load(generate(data));
         renderer->moveToThread(0);
         return renderer.take();
      }
      return 0;
   }
};

class UserSvgItem : public QGraphicsSvgItem {
   Q_OBJECT
   QSvgRenderer m_spinRenderer, * m_lastRenderer;
   QScopedPointer<QSvgRenderer> m_renderer;
   QFuture<QSvgRenderer*> m_future;
   QFutureWatcher<QSvgRenderer*> m_watcher;
   QGraphicsView * aView() const {
      QList<QGraphicsView*> views = scene()->views();
      return views.isEmpty() ? 0 : views.first();
   }
   Q_SLOT void update() { QGraphicsSvgItem::update(); }
   void mousePressEvent(QGraphicsSceneMouseEvent * event) {
      if (event->button() == Qt::LeftButton) askForFile();
   }
   void setRenderer(QSvgRenderer * renderer) {
      if (m_lastRenderer) disconnect(m_lastRenderer, SIGNAL(repaintNeeded()), this, SLOT(update()));
      setSharedRenderer(renderer);
      m_lastRenderer = renderer;
      connect(renderer, SIGNAL(repaintNeeded()), SLOT(update()));
      if (aView()) aView()->centerOn(this);
   }
   void askForFile() {
      QFileDialog * dialog = new QFileDialog(aView());
      connect(dialog, SIGNAL(fileSelected(QString)), SLOT(loadFile(QString)));
      dialog->setAcceptMode(QFileDialog::AcceptOpen);
      dialog->setAttribute(Qt::WA_DeleteOnClose);
      dialog->show();
   }
   Q_SLOT void loadFile(const QString & file) {
      if (m_future.isRunning()) return;
      setRenderer(&m_spinRenderer);
      m_future = QtConcurrent::run(RendererGenerator(file));
      m_watcher.setFuture(m_future);
   }
   Q_SLOT void rendererReady() {
      m_renderer.reset(m_future.result());
      m_renderer->moveToThread(thread());
      setRenderer(m_renderer.data());
   }
public:
   UserSvgItem(const QString & fileName = QString(), QGraphicsItem *parent = 0) :
      QGraphicsSvgItem(fileName, parent), m_lastRenderer(0) {
      connect(&m_watcher, SIGNAL(finished()), SLOT(rendererReady()));
      setFlags(QGraphicsItem::ItemClipsToShape);
      setCacheMode(QGraphicsItem::NoCache);
   }
   void setWaitAnimation(const QByteArray & data) { m_spinRenderer.load(data); }
};

namespace {
   const char svgCircle[] =
      "<svg height=\"100\" width=\"100\"><circle cx=\"50\" cy=\"50\" r=\"40\" stroke=\"black\" stroke-width=\"3\" fill=\"red\" /></svg>";
   const char svgRectangle[] =
      "<svg width=\"400\" height=\"110\"><rect width=\"300\" height=\"100\" style=\"fill:rgb(0,0,255);stroke-width:3;stroke:rgb(0,0,0)\"></svg>";
   const char svgThrobber[] =
      "<svg width=\"16\" height=\"16\" viewBox=\"0 0 300 300\" xmlns=\"http://www.w3.org/2000/svg\" version=\"1.1\"><path d=\"M 150,0 a 150,150 0 0,1 106.066,256.066 l -35.355,-35.355 a -100,-100 0 0,0 -70.711,-170.711 z\" fill=\"#3d7fe6\"><animateTransform attributeName=\"transform\" attributeType=\"XML\" type=\"rotate\" from=\"0 150 150\" to=\"360 150 150\" begin=\"0s\" dur=\"1s\" fill=\"freeze\" repeatCount=\"indefinite\" /></path></svg>";

   void write(const char * str, const QString & fileName) {
      QFile out(fileName);
      if (out.open(QIODevice::WriteOnly | QIODevice::Truncate)) out.write(str);
   }
}

int main(int argc, char *argv[])
{
   QApplication app(argc, argv);
   write(svgRectangle, "rectangle.svg"); // Put svg resources into the working directory
   write(svgCircle, "circle.svg");

   QGraphicsScene scene;
   UserSvgItem item("circle.svg");
   QGraphicsView view(&scene);
   scene.addItem(&item);
   item.setWaitAnimation(QByteArray::fromRawData(svgThrobber, sizeof(svgThrobber)-1));
   view.show();

   return app.exec();
}

#include "main.moc"
查看更多
登录 后发表回答