2

I have a created a small drawing application in QML, I created a small subclass of QQuickPaintedItem. Then in QML, I used a MouseArea to feed the input to my class. From there I simply store the mouse positions in a vector and then paint the points as I receive onto a QImage using QPainter (I used a simple algorithm to draw a quadratic bezier curve using the last three points in my vector). Then I call QQuickPainted::update() and in my implementation of QQuickPaintedItem::paint() I draw the image. Now the program works ok, but the problem is that the rendering of the painting is quite poor (I am already using QPainter::AntiAliasing). Below there is a picture. As you can see the curves are not very sharp and I can see the "pixels" on oblique lines (when I try the same thing with OneNote everything is smooth and nice).

Here is the a full example from my github repository if you want to test it out (the code is below as well). Is there something I can do about this? Annotation 2019-12-05 151210.png. The same image with onenote

#ifndef DRAWINGCANVAS_H
#define DRAWINGCANVAS_H

#include <QObject>
#include <QQuickPaintedItem>
#include <QPixmap>
#include <QPainter>

struct Outline{
    QPolygonF points;

    void addPoint(QPointF p){
        points.append(p);
    }
    void clear(){
        points.clear();
    }
};

// a  custom QQuickPainted used as a canvas in QML
class DrawingCanvas : public QQuickPaintedItem
{
    Q_OBJECT
    Q_PROPERTY(bool drawing READ drawing WRITE setDrawing NOTIFY drawingChanged)
    Q_PROPERTY(int penWidth READ penWidth WRITE setPenWidth NOTIFY penWidthChanged)
    Q_PROPERTY(QString penColor READ penColor WRITE setPenColor NOTIFY penColorChanged)

public:
    explicit DrawingCanvas(QQuickItem *parent = nullptr);
    bool drawing() const;
    Q_INVOKABLE void initiateBuffer();

    Q_INVOKABLE void penPressed(QPointF pos);
    Q_INVOKABLE void penMoved(QPointF pos);
    Q_INVOKABLE void penReleased();
    int penWidth() const;

    void paint(QPainter *painter) override;

    QString penColor() const;


public slots:
    void setDrawing(bool drawing);

    void setPenWidth(int penWidth);

    void setPenColor(QString penColor);

signals:
    void drawingChanged(bool drawing);
    void penWidthChanged(int penWidth);
    void penColorChanged(QString penColor);

private:
    void drawOnBuffer(QPointF pos);

    bool m_drawing;
    QPixmap m_buffer;
    int m_penWidth;
    QString m_penColor;

    QPointF m_lastPoint;
    Outline m_currentOutline;
    QRect m_updateRect;
    QVector<Outline> m_outlines;


    bool m_outlineEraser;
};

#endif // DRAWINGCANVAS_H
#include "drawingcanvas.h"

#include <QPainter>

DrawingCanvas::DrawingCanvas(QQuickItem *parent) : QQuickPaintedItem(parent)
{
    m_penWidth = 4;
}

bool DrawingCanvas::drawing() const
{
    return m_drawing;
}

void DrawingCanvas::penPressed(QPointF pos)
{
    setDrawing(true);
    m_currentOutline.addPoint(pos);
    m_lastPoint = pos;

}

void DrawingCanvas::penMoved(QPointF pos)
{
    if(drawing()){
        m_currentOutline.addPoint(pos);
        // draw the points on the buffer
        drawOnBuffer(pos);
    }
    m_lastPoint = pos;
}

void DrawingCanvas::penReleased()
{
    setDrawing(false);
    m_outlines.append(m_currentOutline);
    m_currentOutline.clear();
    m_lastPoint = QPointF();
}

// draws the actual item
void DrawingCanvas::paint(QPainter *painter)
{
    painter->setRenderHints(QPainter::Antialiasing | QPainter::SmoothPixmapTransform);
    QPen pen;
    pen.setWidth(penWidth());
    pen.setColor(penColor());

    painter->setPen(pen);
    painter->drawPixmap(m_updateRect, m_buffer, m_updateRect);

    m_updateRect = QRect();
}

// draws on the image
void DrawingCanvas::drawOnBuffer(QPointF pos)
{
    QPainter bufferPainter;
    if(bufferPainter.begin(&m_buffer)){
        QPen pen;
        pen.setWidth(penWidth());
        pen.setColor(penColor());

        bufferPainter.setPen(pen);
        bufferPainter.setRenderHints(QPainter::Antialiasing | QPainter::SmoothPixmapTransform);

        int pointsLength = m_currentOutline.points.length();
        QPainterPath path;

        // this will help smoothing the curves
        if(pointsLength > 2){
            auto previousPoint = m_currentOutline.points.at(pointsLength - 3);

            auto mid1 = (m_lastPoint + previousPoint)/2;
            auto mid2 = (pos + m_lastPoint)/2;

            path.moveTo(mid1);
            path.quadTo(m_lastPoint, mid2);
            bufferPainter.drawPath(path);
        }
        // update the canvas
        int rad = (penWidth() / 2) + 2;

        auto dirtyRect = path.boundingRect().toRect().normalized()
                .adjusted(-rad, -rad, +rad, +rad);

        // change the canvas dirty region
        if(m_updateRect.isNull()){
            m_updateRect = dirtyRect;
        }
        else{
            m_updateRect = m_updateRect.united(dirtyRect);
        }
        update(dirtyRect);

        m_lastPoint = pos;
    }
}

QString DrawingCanvas::penColor() const
{
    return m_penColor;
}

int DrawingCanvas::penWidth() const
{
    return m_penWidth;
}

void DrawingCanvas::setDrawing(bool drawing)
{
    if (m_drawing == drawing)
        return;

    m_drawing = drawing;
    emit drawingChanged(m_drawing);
}

void DrawingCanvas::setPenWidth(int penWidth)
{
    if (m_penWidth == penWidth)
        return;

    m_penWidth = penWidth;
    emit penWidthChanged(m_penWidth);
}

void DrawingCanvas::setPenColor(QString penColor)
{
    if (m_penColor == penColor)
        return;

    m_penColor = penColor;
    emit penColorChanged(m_penColor);
}

// initiates the QImage buffer
void DrawingCanvas::initiateBuffer()
{
    qDebug() << this << "Initiating buffer" << width() << height();
    m_buffer = QPixmap(width(), height());
}

In QML:

import QtQuick 2.12
import QtQuick.Window 2.12
import QtQuick.Controls 2.13
import QtQuick.Layouts 1.12
import QtQuick.Dialogs 1.3
import Drawing 1.0

ApplicationWindow {
    visible: true
    width: 640
    height: 480
    title: qsTr("Hello World")

    Flickable {
        id: scrollView
        anchors.fill: parent
        contentHeight: drawingCanvas.height
        DrawingCanvas {
            id: drawingCanvas
            width: parent.width
            height: 2000
            penColor: "red"
            onWidthChanged: drawingCanvas.initiateBuffer()
        }
    }

    MouseArea {
        anchors.fill: parent
        anchors.rightMargin: 20
        onPressed: drawingCanvas.penPressed(
                       Qt.point(mouseX, mouseY + scrollView.contentY))
        onPositionChanged: drawingCanvas.penMoved(
                               Qt.point(mouseX, mouseY + scrollView.contentY))
        onReleased: drawingCanvas.penReleased()
    }
}
reckless
  • 741
  • 12
  • 53
  • 1
    provide a [mre] – eyllanesc Dec 06 '19 at 01:10
  • 1
    @eyllanesc I linked my github repo with my full project – reckless Dec 06 '19 at 01:11
  • 2
    No, that is not what I asked for. The purpose of the MRE in SO is for future readers to analyze whether they have a similar problem or not, and then just try if the solutions can help, but a link cannot be an MRE since nobody guarantees that 1 year, 10 years or the time when there is still SO the url does not break. So if you want help provide a MRE otherwise your question could be closed. – eyllanesc Dec 06 '19 at 01:15
  • 1
    @eyllanesc Ok I will copy the code here – reckless Dec 06 '19 at 01:15
  • 1
    The same argument applies to why in SO we do not accept questions that are only links and therefore we eliminate them. Q&A must be self-sufficient. – eyllanesc Dec 06 '19 at 01:17
  • 1
    You could place an image of what you want to get with the same test text: "Hello, ..." to better understand the problem, that is, the one generated by OneNote. – eyllanesc Dec 06 '19 at 05:59
  • 1
    @eyllanesc I add a screenshot of the same "thing" done with Onenote. – reckless Dec 06 '19 at 14:57
  • 1
    No need for "quadratic bezier curve using the last three points" for such Paint editor example. Also, it could have been an issue of High DPI Screen and then adding `QCoreApplication::setAttribute(Qt::AA_UseHighDpiPixmaps);` should help as well. I tried the example of normal lower end monitor and seeing no problem except it does produce wrong artifacts while drawing fast and changing direction of the pen because of "bezier". Make it point to point and that is it. – Alexander V Dec 28 '19 at 05:56
  • 1
    @AlexanderV yes, I disabled the quadratic approximation, but the problem persists. I think it might because other painting applications use some sort of advance "antialiasing" (maybe with alpha blending). This is very noticeable on lines drawn using a QPen with pixel width = 1, with a higher pixel width the lines are much smoother. – reckless Dec 28 '19 at 11:25
  • 1
    @daljit97, it's clear that antialiasing is applied. The difference between your implementation and the Onenote implementation is that Onenote is drawing thicker lines but narrows them on start and endpoints which mimics a physical pen behavior. let's talk about how to achieve that goal in QQuickPaintedItem. – Soheil Armin Dec 30 '19 at 06:11
  • 1
    @SoheilArmin no that's not the case `QQuickPaintedItem` does not have edge antialiasing enabled. Indeed this is only possible to implement with a custom `QQuickItem`, see [here](https://doc.qt.io/qt-5/qquickitem.html#antialiasing-prop). The antialiasing you are talking about applies to `QPainter` is different. You can check this [bug report](https://bugreports.qt.io/browse/QTBUG-32992) – reckless Dec 30 '19 at 14:02
  • 1
    @daljit97, move the cursor faster, you will see that the antialiasing is working. actually you are drawing on a QImage as per docs. You are seeing pixels on those paths with very near start-mid-end points. – Soheil Armin Dec 30 '19 at 14:23
  • I also noticed that if I set a pen width = 1 the rendering is even worse – reckless Jan 01 '20 at 21:24

1 Answers1

1

Your rendering issue doesn't seem to be due to the antialising qt option but more to the smoothing of your strokes. I recommend you to modify your custom bezier smoothing techniques or to use a dedicated lib for that [0] .

Secondly, you should create a dedicated QPen in your draw methods and "play" with the QPen and QBrush options [1] if you want the "OneNote drawing feeling". The major difference I saw between the two screenshots was the brush scale dynamics (at the beginning and the end of the strokes).

0: For example, https://github.com/oysteinmyrmo/bezier

1: https://doc.qt.io/qt-5/qpen.html

Alaenix
  • 83
  • 6