当前位置:网站首页>Qt桌面白板工具其一(解决曲线不平滑的问题——贝塞尔曲线)
Qt桌面白板工具其一(解决曲线不平滑的问题——贝塞尔曲线)
2022-07-23 20:59:00 【Larry_Yanan】
Qt桌面白板工具其一(解决曲线不平滑的问题——贝塞尔曲线)
前言:
有关Qt的绘画、白板、画板等应用,前前后后研究过好几次,每一次都有新的收获和体会。而这次终于研究明白,如何解决因为电脑配置应用卡顿所导致的线条折线明显、存在卡顿的问题。是的,网上说的都是贝塞尔曲线,但研究半天没有很明确地解决我的需求,所以这里我也结合了自己的思考,给出以下的解决方法和代码吧。
一、核心实现代码
我们首先要在mousePressEvent和mouseMoveEvent里面收集我们的鼠标点击移动点,或者触摸屏的移动点。
void BezierTestWidget::mousePressEvent(QMouseEvent *event)
{
if(flag_bezier)
{
bezier_points.clear();
bezier_points.append(event->pos());
}
}
void BezierTestWidget::mouseMoveEvent(QMouseEvent *event)
{
if((event->buttons() & Qt::LeftButton))//是否左击
{
QImage image = last_image;
if(flag_bezier)
{
//采集点的时候,适当过滤一下比较接近的一些点,不然会影响平滑处理的效果
if(qAbs(bezier_points.last().x()-event->pos().x())>15 || qAbs(bezier_points.last().y()-event->pos().y())>15)
{
bezier_points.append(event->pos());
}
QPainter painter(&image);
painter.setRenderHint(QPainter::Antialiasing, true);
QPen pen;
QColor brush_color(0,255,0,100);
pen.setBrush(brush_color);
pen.setWidth(5);
painter.setPen(pen);
drawBezier(&painter, &image);
painter.end();
}
*draw_image = image;
repaint();
}
}
这段是真正绘制到图片上的代码:
void BezierTestWidget::drawBezier(QPainter *painter, QImage *image)
{
if(bezier_points.size()<=0)
return;
//最终生成的点队列
QList<QPointF> points;
//遍历添加中点,将实际点当做控制点
if(bezier_points.count() > 2)
{
points.append(bezier_points[0]);
points.append(bezier_points[1]);//根据算法,第一个和第二个点间不添加中点
for (int i = 2; i <bezier_points.count(); i++) {
points.append((bezier_points[i]+bezier_points[i-1])/2);
points.append(bezier_points[i]);
}
}
QPainterPath draw_path;
if(bezier_points.count() > 2)
{
int i = 0;
while(i < points.count())
{
if(i+3 <= points.count())//按照顺序进行贝塞尔曲线处理,并添加到绘图路径中
{
QPainterPath path;
path.moveTo(points[i]);
path.quadTo(points[i+1],points[i+2]);
draw_path.addPath(path);
}else{
int a = i;
QPolygon polypon;
while(a < points.count())
{
polypon << points[a].toPoint();
a++;
}
draw_path.addPolygon(polypon);
}
i = i + 2;
}
}
//绘制path
painter->drawPath(draw_path);
}
有关如何实现的原理和逻辑,放到后面再仔细展开。接下来先简单介绍一下我画板实现的基本思路。
二、画板实现思路
在网上找过很多资料,有关画板、桌面白板等实现方案,无非就是两种。
#1.QGraphicsView和QGraphicsScene
通过添加图元的方法来实现。关于这个,其实我了解也不是很深刻,其实还挺复杂的。我的理解是,创建一个个单独独立的图元对象,然后添加进QGraphicsScene里面。
比如说最简单的,QGraphicsLineItem *addLine(qreal x1, qreal y1, qreal x2, qreal y2, const QPen &pen = QPen(),实际上就是往绘图场景中添加一段直线,这里的直线其实相当于是一个对象,添加进了整个场景画布中。
我们知道,鼠标移动绘制一段曲线,其实是由许许多多个点组成的,而每一个点都用直线相连的话,就是一段曲线了。而之所以使用贝塞尔曲线处理来追求平滑,无非就是电脑卡顿应用卡顿的时候,mouseMoveEvent鼠标事件触发得不够,导致我们的样本点太少了,不然其实一般都很顺滑的。
缺点:
(1)QGraphicsScene的绘制可以实现画矩形、文字等功能,但因为是一个个图元添加进去的,而不是实际我们直观上绘制再画布上的,所以不能很好地实现“橡皮擦”、“消除”的功能。
该方案如果想要用橡皮擦,只能不断去判断当前鼠标移动的点,有无和图元队列中的图元相交,有就去除。这种做法客户体验并不好,我所做的项目也是因为这个而取消了橡皮擦的开发。
(2)刚才说了,实际上曲线是通过点与点之间的无数曲线叠加组成的。问题来了,如果我们的画笔是设置成荧光笔,也就是带有透明度,会变成怎样呢?结果就是线段叠加的部分颜色也会叠加,移动速度快的话能清楚看到叠加点,而移动速度慢的话,点基本重合,又没有透明度的效果。
综上,该方法虽然封装比较好,扩展功能也做得不错,而且好像有硬件加速还是什么鬼的功能,但其实并不能很好地实现我们传统想象中的绘画画板,故我现在使用了第二种。
2.利用QImage间接绘制窗口
如果你有接触过QPainter和paintEvent(),你应该知道其实窗口的刷新都在这个里面去做,你可以重载paintEvent(),将你期望的内容绘制在窗口部件中。当然,这也可以绘制你的鼠标点击路径。
之所以在paintEvent()不断绘制QImage,而mouseMoveEvent触发时把路径绘制在QImage里面,其实是考虑到撤销的问题。撤销需要记录上一步的画面,与其记住繁杂的路径点位,不如直接记住上一次的画面QImage算了,并且限定最多撤销10次,那我也就最多储存10个QImage而已,内存上涨不大。
优点:
(1)画图可以直接绘制在上面,并且利用QPainter的方法drawPath或drawPolyline,绘制出来的曲线没有上述的重合点颜色叠加问题。另外橡皮擦也很好实现,直接擦除了QImage的像素,甚至可以设置橡皮擦的形状和大小。文字也可以画上去,再加一些什么三角形矩形之类的东西。
总的来说,这种实现方式的限制比较少,容易实现我们想要的功能。
关键是,QPainter相关的QPainterPath提供了以下两个方法,以实现贝塞尔曲线处理:
void cubicTo(const QPointF &ctrlPt1, const QPointF &ctrlPt2, const QPointF &endPt);
void quadTo(const QPointF &ctrlPt, const QPointF &endPt);
三、贝塞尔曲线实现的详细思路
首先,我们要说清楚什么是贝塞尔曲线,这里也参考了不少文章,感兴趣可以自己去看一下。
贝塞尔曲线的作用和特点
QPainterPath详解
Qt用算法画平滑曲线(cubicTo)
贝塞尔曲线的数学概念我们不用深究,但我们得知道接口方法的每一项参数。简单来说
两点之间的曲线效果,或是由一到两个控制点来决定的。图
图一,对应void quadTo(const QPointF &ctrlPt, const QPointF &endPt);
图二,对应void cubicTo(const QPointF &ctrlPt1, const QPointF &ctrlPt2, const QPointF &endPt);
我们期望的平滑曲线效果,使用图一这种就好了。
首先,我们用QVetor获取了一系列的QPointF点对不对?然后我们再来看,如何获取这个二阶贝塞尔曲线信息算法(参考第三个文章):
假设我们在鼠标移动的过程中有A、B、C、D、E、F、G、这6个点。如何画出平滑的曲线呢, 我们取B点和C点的中点B1
作为第一条贝塞尔曲线的终点,B点作为控制点。如图: 贝塞尔曲线
接下来呢 算出 cd 的中点 c1 以 B1 为起点, c点为控制点, c1为终点画出下面图形: 连续曲线图
这两个图很直观了,不明白的我再描述一次。当我们拥有一系列ABCD的点集时,从第二个点开始,点和下一个点之间计算出一个新的中点(直接相加除二),然后添加进点集队列当中。这样,我们除了第一个点之外的其他实际点,都将作为控制点成为参数,而第一个点和我们手动计算出的中点,反而成为了实际点,与曲线相连。
这实际上是一种很精妙且简洁的算法,利用已有点生成成倍的点,再巧妙地曲线平滑化,成功做到了因为设备应用卡顿导致的折线现象。
不明白的话,只能说再多看几次哈哈。具体的代码实现,其实也很简单。将我们收集到的点,除去第一个点之外,其余都插入一个中点。然后,我们再遍历以贝塞尔曲线的方式绘制即可。
这是插入中点的实现,bezier_points是我mouse事件收集到的点集。
//最终生成的点队列
QList<QPointF> points;
//遍历添加中点,将实际点当做控制点
if(bezier_points.count() > 2)
{
points.append(bezier_points[0]);
points.append(bezier_points[1]);//根据算法,第一个和第二个点间不添加中点
for (int i = 2; i <bezier_points.count(); i++) {
points.append((bezier_points[i]+bezier_points[i-1])/2);
points.append(bezier_points[i]);
}
}
我们继续遍历,每三个点为一组,用QPainterPath 来实现。moveTo参数是起始点,quadTo的参数分别是控制点和终点。最后将QPainterPath 绘制在总的QPainterPath 当中,结束遍历后再总的绘制QPainterPath(可以避免透明画笔的重合点问题)。如果不足 三个点,那就直接画直线(折线)算了。
QPainterPath draw_path;
if(bezier_points.count() > 2)
{
int i = 0;
while(i < points.count())
{
if(i+3 <= points.count())//按照顺序进行贝塞尔曲线处理,并添加到绘图路径中
{
QPainterPath path;
path.moveTo(points[i]);
path.quadTo(points[i+1],points[i+2]);
draw_path.addPath(path);
}else{
int a = i;
QPolygon polypon;
while(a < points.count())
{
polypon << points[a].toPoint();
a++;
}
draw_path.addPolygon(polypon);
}
i = i + 2;
}
}
//绘制path
painter->drawPath(draw_path);
另外,有很多时候画得太慢了,点与点之间靠的太近,所以在收集的时候,就适当地过滤掉一些点,以免影响处理的效果。
//采集点的时候,适当过滤一下比较接近的一些点,不然会影响平滑处理的效果
if(qAbs(bezier_points.last().x()-event->pos().x())>15 || qAbs(bezier_points.last().y()-event->pos().y())>15)
{
bezier_points.append(event->pos());
}
四、最终演示效果
第一个红色的是普通的折线效果;第二个是折线和贝塞尔处理后的对比;第三个绿色的就是贝塞尔曲线处理过的效果啦。
五、结尾
终于解决了这个问题,感觉收货还是蛮多的。但还是有些缺憾,比如在4K屏上的绘制还是太卡了,不知道有什么方法可以优化一下。另外,这只是简单的贝塞尔曲线示例,日后有机会的话再写一下橡皮擦啊,三角形,文字输入等实现,日后争取做一个比较完善且好看的白板工具。
边栏推荐
猜你喜欢

OpenLayers实例-Advanced Mapbox Vector Tiles-高级Mapbox矢量贴图

【着色器实现RoundWave圆形波纹效果_Shader效果第六篇】

STM32c8t6驱动激光雷达(一)

STM32C8t6 驱动激光雷达实战(二)

利用ENVI对TROPOMI(哨兵5P)数据预处理

第十一天:续第十天BGP的基本配置

LU_ASR01语音模块使用
![[100 cases of scratch drawing] Figure 46-scratch drawing flowers children's programming scratch programming drawing case tutorial grade examination competition drawing training case](/img/44/832686f3ee198772794cbd53e7d5a5.png)
[100 cases of scratch drawing] Figure 46-scratch drawing flowers children's programming scratch programming drawing case tutorial grade examination competition drawing training case

支付宝常用接口统一封装,可直接支付参数使用(适用于H5、PC、APP)

1309_STM32F103上增加GPIO的翻转并用FreeRTOS调度测试
随机推荐
LU_ Asr01 voice module usage
"Pulse" to the future! Huawei cloud Mrs helps smooth migration to the cloud
TCP半连接队列和全连接队列(史上最全)
微服务架构 VS 单体服务架构【华为云服务在微服务模式里可以做什么】
An interview question about common pitfalls in golang for range
OpenLayers实例-Advanced Mapbox Vector Tiles-高级Mapbox矢量贴图
Now I don't know how to synchronize at all
Network learning infrared module, 8-way emission independent control
[kernel] platform bus model for driving development and learning
Major optimization of openim - Message loading on demand, consistent cache, uniapp Publishing
从哪些维度评判代码质量的好坏?如何具备写出高质量代码的能力?
OpenIM重大升级-群聊读扩散模型发布 群管理功能升级
确定括号序列中的一些位置
第十二天:续第十一天(BGP相关知识)
Educational Codeforces Round 132 A-D题解
OpenIM重大优化-消息按需加载 一致性缓存 uniapp发布
Chapter 3 business function development (creating clues)
[100 cases of scratch drawing] Figure 46-scratch drawing flowers children's programming scratch programming drawing case tutorial grade examination competition drawing training case
[shader realizes roundwave circular ripple effect _shader effect Chapter 6]
Comment présenter votre expérience de projet lors d'une entrevue

