Search code examples
c++qtqpaintercoordinate-transformation

Qt - difference between drawPolyline and drawLine


In my example I transformed the coordinate system of the qt painter in the following way:

 QTransform xform;
 xform.rotate(90);
 xform.scale(1, 1000000000000000);
 xform.translate(0, -std::abs(max));
 if (limit != 0)
     xform.scale(1, std::abs(max)/std::abs(limit));
 painter->setTransform(xform)

There is such a big scale because I have very small data in my example. Then I draw the plot for my data - it's values are about 1e-14 and everything is drawn correctly. Then I want to draw couple of lines in addition to my plot specified by two points.

I apply drawPolyLine function:

QPointF* line = new QPointF[2];
line[0].setX(0);
line[0].setY(0);
line[1].setX(0);
line[1].setY(std::abs(seismMax));
painter.drawPolyline(line, 2);

where seismMax is of the orders of 1e-14 ( as all my points in the plot). And the line is drawn. Then I apply drawLine ( as I have only two points):

painter.drawLine(QPointF(0, 0), QPointF(0, std::abs(seismMax)));

And in this case line is not drawn. I suspect that maybe these two functions process coordinates differently. As when I draw the line with drawLine in original scale (for example like this) :

 painter.drawLine(QPointF(0, 0), QPointF(0, 1000));

the line is drawn correctly.


Solution

  • I made an MCVE to come in touch with the problem.

    Thereby, I used much smaller scalings and was not able to reproduce the issue. Then, I used the scaling 1.0E14 as stated by OP. Suddenly, the line drawings disappeared. Lowering the scaling, I fiddled out that (in my case) upto 1E12 it worked quite good but with higher scalings lines begun to disappear. While toying with this, a colleague came away and provided the hint that this might been simply floating point issues. We shortly discussed this and came to the conclusion:

    • multiplying very big numbers with very small is not a problem at all. It consists of (integer) multiplying the mantissas and adding the exponents.
    • addition/subtraction of big numbers with very small is a problem as both numbers have to made with equal exponents (by bit-shifting mantissas) to add/subtract mantissas.

    So, the latter might happen which erases the non-0 values to 0.

    1014 is in binary a number with 47 digits. This is close to the precision of double with 53 bits mantissa. But: the used rendering engine of QPainter may be based on OpenGL where float is the default for many things. float provides only 23 bits for mantissa!

    So, after thinking a bit about this, I found good reasons not to paint with a scaling factor of 1014.

    To demonstrate this, I made a small sample testQPainterDrawLine.cc:

    #include <QtWidgets>
    
    class Widget: public QWidget {
      public:
        const double scale; 
      public:
        Widget(double scale, QWidget *pQParent = nullptr):
          QWidget(pQParent),
          scale(scale)
        { }
        virtual ~Widget() = default;
        Widget(const Widget&) = delete;
        Widget& operator=(const Widget&) = delete;
      protected:
        virtual void paintEvent(QPaintEvent *pQEvent) override;
    };
    
    void Widget::paintEvent(QPaintEvent*)
    {
      const double value = height() / scale;
      QPainter qPainter(this);
      qPainter.fillRect(0, 0, width(), height(), QColor(Qt::white));
      qPainter.drawRect(0, 0, width() - 1, height() - 1);
      QTransform xform;
      xform.scale(1, scale);
      qPainter.setTransform(xform);
      qPainter.setPen(QPen(QColor(Qt::red), 3.0));
      const double xL = 0.333 * width();
      qPainter.drawLine(QPointF(xL, 0.0), QPointF(xL, value));
      qPainter.setPen(QPen(QColor(Qt::blue), 3.0));
      const double xPL = 0.667 * width();
      QPointF qPts[] = { QPointF(xPL, 0.0), QPointF(xPL, value) };
      const int nPts = sizeof qPts / sizeof *qPts; 
      qPainter.drawPolyline(qPts, nPts);
    }
    
    int main(int argc, char **argv)
    {
      qDebug() << "Qt Version:" << QT_VERSION_STR;
      QApplication app(argc, argv);
      QWidget qWin;
      QGridLayout qGrid;
      qGrid.setRowStretch(0, 0); qGrid.setRowStretch(1, 1);
      double scale = 1.0E11;
      for (int i = 0; i < 4; ++i, scale *= 10.0) {
        qGrid.addWidget(
          new QLabel(QString("Scale: %1").arg(scale)),
          0, i);
        qGrid.addWidget(new Widget(scale), 1, i);
      }
      qWin.setLayout(&qGrid);
      qWin.resize(1024, 256);
      qWin.show();
      return app.exec();
    }
    

    testQPainterDrawLine.pro:

    SOURCES = testQPainterDrawLine.cc
    
    QT = widgets
    

    Compiled and tested in cygwin64:

    $ qmake-qt5 testQPainterDrawLine.pro
    
    $ make
    
    $ ./testQPainterDrawLine
    Qt Version: 5.9.4
    

    snapshot of testQPainterDrawLine

    So, finally, I believe, it hasn't anything to do with QPainter::drawLine() vs. QPainter::drawPolyline(). It's simply the scaling which is too high and causes floating point issues. That's why lines may accidentally (dis-)appear.

    The solution is simple: The values have to be scaled before drawing them with QPainter, so that internal transformations in QPainter happen in magnitudes closer to 0 and 1.