上一篇文章:Qt桌面白板工具其一(解决曲线不平滑的问题——贝塞尔曲线)
一、前言:
我比较啰嗦,可能会说很多废话。在上一篇文章中,我主要分享了有关贝塞尔算法解决曲线不平滑的问题,简单来说我们可以通过算法,对所搜集的一系列点进行重新绘制,在画布中绘制我们需要的QPaintPath路径。这个过程中,我一直有在考虑荧光笔的实现问题。荧光笔,简单来说就是绘制一条半透明的线段,常见于需要对一些显示文本进行高亮提示。而采用上述贝塞尔整条路径线条绘制的方法,确实能实现。不过一旦放弃整段路径绘制,转而采用线段叠加的方式绘制线条,就难免会出现因为多条不透明线段叠加,所产生的重复点颜色较深的情况,如图:
经过尝试,一旦整段路径,也就是QPaintPath中添加的线段过多,单次绘制的时候会产生比较严重的耗时。这在鼠标拖动moveevent和刷新显示paintevent中,是不能忍受的。如果需要采用贝塞尔算法,我会选择在鼠标move过程中先采用线段叠加法,在鼠标release释放的时候再统一绘制所采集的点集和线段。又或者将线段拆开,但难免会有凸点和重合点。
上面都是题外话了,本篇文章中想要实现的,是在move过程中,即便采用线段叠加,也能实现荧光笔的效果。这个要求,需要我实现线段叠加的时候,不能产生重合区域颜色较深现象。这个问题在过往很长一段时间内困扰了我很久,但现在终于是解决了。
二、混合模式
首先,分享一个概念QPainter.CompositionMode(附带链接),这是QPainter的绘制混合模式。
被绘制的画布叫做“目标”,需要绘制上去的东西叫做“源”。
而一般来说,QPainter默认的是CompositionMode_SourceOver,即覆盖模式。如果底部有一条实色的红线,你绘制一条半透明绿线,将会产生叠加色。
图一
而如果选择CompositionMode_Source,则是重合部分都只显示“源”,而忽略“目标”的部分。
图二
可以看出,虽然绿线是半透明的,但实际上重合部分已经没有了红色。
所以,本文第一张图的绿色重合点,就能完美解决了,即半透明绿色线段的叠加,重合部分只会显示后一条线段的颜色,透明度通道不会产生叠加,进而形成一条完整的半透明统一透明度的线条。
好啦,以上其实只是为了绘制绿色线段而已,不要被这两张图的红线绿线混淆了,实际上我们需要实现的是图一的效果。
三、简单代码分享
以下是我的操作步骤
//画布资源
QImage *draw_image = nullptr; //qimage画布,最终结果
QImage last_image; //记录动作开始前的原图
QImage temp_image; //中间的临时图片
QImage layer_image; //中间图层,单个动作的图层
QVector<QPointF> ployline_points; //移动中搜集的点集
void xxx::mousePressEvent(QMouseEvent *event)
{
last_image = draw_image->copy(); //记录动作开始前的原图
temp_image = last_image; //move过程中临时显示的图片层
layer_image.fill(Qt::transparent); //图层层,填充透明
ployline_points.clear();
ployline_points.append(event->pos());
}
void xxx::mouseMoveEvent(QMouseEvent *event)
{
//过滤密集点(会影响效果)
if(qAbs(ployline_points.last().x()-event->pos().x())>15 || qAbs(ployline_points.last().y()-event->pos().y())>15)
{
ployline_points.append(event->pos());
if(ployline_points.count()>=2){
//画图层
QPainter painter(&layer_image); //绘制在透明图层上,不干扰原图
painter.setCompositionMode(QPainter::CompositionMode_Source); //这里直接用源来覆盖,不会收到目标的影响
painter.setRenderHint(QPainter::Antialiasing, true);
painter.setBackgroundMode(Qt::TransparentMode); //设置背景模式 将画师的背景模式设置为给定模式
painter.setPen(current_draw_mode->pen);
//只画最后两个点线(短线段叠加,不会产生重合深色点)
painter.drawLine(ployline_points[ployline_points.count()-2],ployline_points.last());
painter.end();
//复制原图
temp_image = last_image; //拷贝原图
painter.begin(&temp_image); //在临时图片层绘制
painter.setCompositionMode(QPainter::CompositionMode_SourceOver); //调整为正常的混合模式
QRect rect = layer_image.rect();
painter.drawImage(rect, layer_image); //将图层绘制在临时图片层中
}
}
}
void xxx::mouseReleaseEvent(QMouseEvent *event)
{
QImage image = last_image; //拷贝原图
QPainter painter(&image); //在拷贝层重新绘制搜集点
painter.setRenderHint(QPainter::Antialiasing, true);
if(isBezier){
//贝塞尔优化(也可以不优化,这里不提供代码了)
QVector<QPointF> points;
for(int i = 0; i < ployline_points.count(); i++){
QPointF pt = ployline_points[i];
if(points.count()<2){
points.append(pt);
}else{
points.append((points.last()+pt)/2);
points.append(pt);
}
}
drawBezierPath(&painter,points);
}else{
drawPolyline(&painter,ployline_points);
}
//将重新绘制的图片赋给最终图片
*draw_image = image;
last_image = draw_image->copy();
ployline_points.clear();
}
void xxx::paintEvent(QPaintEvent *event)
{
if(is_drawing){//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());//move期间显示临时图片
painter.end();
}else{
//画布源绘制
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结束显示最终图片,即实际的画布源
painter.end();
}
}
}
paintevent交由负责update的定时器触发,避免paint事件产生过多,绘制耗时导致move事件触发过少,采集点数过少,导致线条不流畅。这里不再赘述。
有什么问题可以在评论区提出,有关荧光笔怎么实现,应该是已经清楚啦。