Qt desktop whiteboard tool part 2 (implementation method of highlighter and solving the problem of overlapping dark spots caused by transparency)

Previous article: One of the Qt desktop whiteboard tools (solve the problem of uneven curves – Bezier curve)

1. Foreword:
I’m quite talkative and may talk a lot of nonsense. In the previous article, I mainly shared about the problem of non-smooth curves solved by the Bessel algorithm. Simply put, we can use the algorithm to redraw a series of collected points and draw the QPaintPath we need in the canvas. path. During this process, I have been thinking about the implementation of highlighters. A highlighter pen, simply speaking, is to draw a translucent line segment, which is often used to highlight some displayed text. And using the above-mentioned Bezier entire path line drawing method can indeed be achieved. However, once you give up drawing the entire path and instead use line segment superposition to draw lines, it is inevitable that the repeated points produced by the superposition of multiple opaque line segments will be darker, as shown in the figure:

After trying, once the entire path, that is, too many line segments are added to QPaintPath, it will take a serious time to draw in a single time. This is intolerable in mouse drag moveevent and refresh display paintevent. If I need to use the Bessel algorithm, I will choose to use the line segment superposition method first during the mouse move process, and then draw the collected point sets and line segments uniformly when the mouse is released. Or you can take the line segments apart, but there will inevitably be bumps and overlapping points.
The above are all digressions. What I want to achieve in this article is to achieve the effect of a highlighter even with line segment superposition during the move process. This requirement requires me to avoid overlapping areas with darker colors when superimposing line segments. This problem has troubled me for a long time in the past, but now it is finally solved.

2. Mixed mode
First, share a concept QPainter.CompositionMode (link attached), which is the drawing blending mode of QPainter.
The canvas being drawn is called the “target”, and the thing that needs to be drawn is called the “source”.
Generally speaking, QPainter’s default is CompositionMode_SourceOver, which is the overlay mode. If there is a solid red line at the bottom and you draw a translucent green line, it will create an overlay color.

Figure 1
And if CompositionMode_Source is selected, only the “source” will be displayed in the overlapping parts, while the “target” part will be ignored.

Figure II
It can be seen that although the green line is translucent, there is actually no red in the overlapping part.
Therefore, the green overlapping point in the first picture of this article can be perfectly solved, that is, the superposition of translucent green line segments. The overlapping part will only display the color of the latter line segment, and the transparency channel will not be superimposed, thus forming a complete semi-transparent green line segment. Transparent lines with unified transparency.
Okay, the above is actually just for drawing green line segments. Don’t be confused by the red and green lines in these two pictures. In fact, what we need to achieve is the effect of Figure 1.

3. Simple code sharing
Here are my steps

//Canvas resources
QImage *draw_image = nullptr; //qimage canvas, final result
QImage last_image; //Record the original image before the action starts
QImage temp_image; //Temporary picture in the middle
QImage layer_image; //Middle layer, single action layer
QVector<QPointF> ployline_points; //Point set collected during movement

void xxx::mousePressEvent(QMouseEvent *event)
{<!-- -->
last_image = draw_image->copy(); //Record the original image before the action starts
    temp_image = last_image; //The image layer temporarily displayed during the move process
layer_image.fill(Qt::transparent); //Layer layer, fill transparent
plotline_points.clear();
plotline_points.append(event->pos());
}

void xxx::mouseMoveEvent(QMouseEvent *event)
{<!-- -->
//Filter dense points (will affect the effect)
    if(qAbs(ployline_points.last().x()-event->pos().x())>15 || qAbs(ployline_points.last().y()-event->pos().y( ))>15)
    {<!-- -->
        plotline_points.append(event->pos());
        if(ployline_points.count()>=2){<!-- -->
            //Draw layer
            QPainter painter( & amp;layer_image); //Draw on the transparent layer without disturbing the original image
            painter.setCompositionMode(QPainter::CompositionMode_Source); //Here it is directly overwritten with the source and will not be affected by the target.
            painter.setRenderHint(QPainter::Antialiasing, true);
            painter.setBackgroundMode(Qt::TransparentMode); //Set the background mode Set the painter's background mode to the given mode
            painter.setPen(current_draw_mode->pen);
            //Only draw the last two dotted lines (short line segments are superimposed and no overlapping dark points will be produced)
            painter.drawLine(ployline_points[ployline_points.count()-2],ployline_points.last());
            painter.end();
            //Copy the original image
            temp_image = last_image; //Copy the original image
            painter.begin( & temp_image); //Draw on the temporary image layer
            painter.setCompositionMode(QPainter::CompositionMode_SourceOver); //Adjust to normal blending mode
            QRect rect = layer_image.rect();
            painter.drawImage(rect, layer_image); //Draw the layer in the temporary picture layer
        }
    }
}

void xxx::mouseReleaseEvent(QMouseEvent *event)
{<!-- -->
QImage image = last_image; //Copy the original image
    QPainter painter( & amp;image); //Redraw the collected points on the copy layer
    painter.setRenderHint(QPainter::Antialiasing, true);
    if(isBezier){<!-- -->
        //Bessel optimization (you don’t need to optimize, the code is not provided here)
        QVector<QPointF> points;
        for(int i = 0; i < plot_points.count(); i + + ){<!-- -->
            QPointF pt = plotline_points[i];
            if(points.count()<2){<!-- -->
                points.append(pt);
            }else{<!-- -->
                points.append((points.last() + pt)/2);
                points.append(pt);
            }
        }
        drawBezierPath( & amp;painter,points);
    }else{<!-- -->
        drawPolyline( & amp;painter,ployline_points);
    }
    
    //Assign the redrawn picture to the final picture
    *draw_image = image;
    last_image = draw_image->copy();
    plotline_points.clear();
}

void xxx::paintEvent(QPaintEvent *event)
{<!-- -->
if(is_drawing){<!-- -->//Flags in move
        QPainter painter(this);
        painter.setRenderHint(QPainter::Antialiasing, true);
        painter.setCompositionMode(QPainter::CompositionMode_SourceOver);
        QRect rect = this->rect();
        painter.drawImage(rect, temp_image, temp_image.rect());//Display temporary images during move
        painter.end();
    }else{<!-- -->
        //Canvas source drawing
        if(draw_image)
        {<!-- -->
            QPainter painter(this);
            painter.setBackgroundMode(Qt::TransparentMode);
            painter.setCompositionMode(QPainter::CompositionMode_SourceOver);
            QRect rect = this->rect();
            painter.drawImage(rect, *draw_image);//move ends and displays the final image, which is the actual canvas source
            painter.end();
        }
    }
}

The paintevent is triggered by the timer responsible for update to avoid too many paint events, too few move events triggered due to time-consuming drawing, and too few collection points, resulting in unsmooth lines. I won’t go into details here.
If you have any questions, you can ask them in the comment area. It should be clear about how to implement the highlighter.