目录
基础效果图
前言
使用Qt自定义折线图,可以自己控制折线图的重绘规则,究竟是每添加一个数据就刷新整个折线图,还是只刷新部分折线图。
我把折线图分为以下两类:
- 坐标系是静态的,折线图更新快。
- 坐标系是动态的,数据的变化更为明显。
设计要点
界面
1.线型的美化
2.给每个数据点设置一个圆点,突出一下
功能
1.坐标轴的绘制
2. 缩放
3. 拖拽
4.数据点的查询
理论学习
这里只总结这一次代码需要知道的理论知识,带着问题去学习。
问题1:QT窗口的刷新机制,何时会进入paintEvent?
void QWidget:paintEvent(QPaintEvent *event) [virtual protected]
这个event函数可以被它的子类重构,以接收处理重绘事件。
这个函数被调用的场景包括以下几种:
repaint()或者update()被调用时
这个控件出现且没有被其他窗口挡住。
其他原因比如界面的缩放、移动等
许多控件被要求重绘时,都会重绘整个界面,但是有些控件可能需要优化重绘,于是,我们可以通过设置QPaintEvent::region(),这样就可以只更新指定的区域。
Qt也支持将多个区域的重绘合并为一个区域的重绘,当update()函数被调用多次或者窗口系统发送了多次paint事件,Qt会将涉及到的区域合并为一个更大的区域。但是repaint()函数不会有以上的优化,每次它被调用就会立刻以最快速度重绘需要重绘的区域,所以Qt建议不管什么情况下,都尽量使用update()函数。
当重绘事件发生时,被更新的区域首先会被擦除,但你可以自定义控件的背景。
注意:自Qt 4.0 版本开始,QWidget就已经自动实现了双缓存,所以编程人员不需要为了避免界面闪烁,而在paintEvent()函数中编写有关双缓存的代码。
问题2:QPainter的使用
绘制折线图,主要用到QPainter的四个函数,分别起:构造、绘制直线、绘制字符串的作用、绘制圆。
问题3:怎么做到“部分刷新”以节约时间。
setAttribute(Qt::WA_OpaquePaintEvent); //实现部分重绘
代码实战
界面的美化
这里只提一个,就是线型的美化,用QPainter去绘制直线时,如果直接使用,绘制的线性会受画线的规则影响,加一个设置之后,可以美化直线,当然,缺点就是:多了运算量,目的是通过消除锯齿现象来美化直线。
美化时需要对QPainter进行设置:
QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing, true); //添加反走样可以使线型更加好看
美化前后对比图:
图中,因为数据源是随机产生的,所以前后数据不一致,但是光是从这两张图里,我们也可以可以看出第一张图里边,锯齿现象非常明显,而图二通过颜色的重新计算,用明亮度消除了锯齿带来的不良影响。
可能有人会想问,为什么不默认打开反走样呢?
这是因为,反走样是一种比较复杂的算法,在一些对图像质量要求不高的应用中,是不需要进行反走样的。
为了提高效率,一般的图形绘制系统,如Java2D、OpenGL之类都是默认不进行反走样的。
还有一个疑问,既然反走样比不反走样的图像质量高很多,不进行反走样的绘制还有什么作用呢?
前面说的是一个方面,也就是,在一些对图像质量要求不高的环境下,或者说性能受限的环境下,
比如嵌入式和手机环境,是不必须要进行反走样的。另外还有一点,在一些必须精确操作像素的应用中,
也是不能进行反走样的。
[整理自:https://blog.csdn.net/huayutiancheng/article/details/52857442]
动态折线图
坐标轴的绘制
X轴和Y轴都是动态的,根据实际数据进行调整。
首先将Y轴固定为4大段,每大段为5小段。 而 X轴则初始化为4大段,每一大段分为5个小段,如果范围超过了最大值,则往后面加小段,每加5个小段,便加一个刻度,直到大段数量为8时,重新将大段数置为4,再重复之前的过程。
代码如下:
//在接收到一个新的数据时,判断是否需要增加一条小刻度线,如果需要则将b_addSmallCount置为true,否则置为false
if(b_addSmallCount)
{
xSmallCount +=1;
if(xSmallCount==41) xSmallCount=20;
b_addSmallCount = false;
}
//绘制x轴刻度, 每个大刻度里边有5个小刻度
x_small =(m_width - margin_left*2)/xSmallCount;
for(int i=0 ; i<xSmallCount+1 ;++i)
{
if(i%5 == 0 )
{
mpainter->drawLine(QPointF(margin_left+i*x_small, m_height-margin_bottom), QPointF(margin_left + i*x_small, m_height-margin_bottom-rilling_long));
float val = x_min_mouseScaled+(1.0*i*(x_max_mouseScaled-x_min_mouseScaled)/(xSmallCount));
mpainter->drawText(QPointF(margin_left+i*x_small, m_height-14), QString::number(val,'f',2));
}
else
{
mpainter->drawLine(QPointF(margin_left+i*x_small, m_height-margin_bottom), QPointF(margin_left + i*x_small, m_height-margin_bottom-rilling_short));
}
}
QPainter *mpainter_white = new QPainter(this); //虚线
QPen mpen;
mpen.setColor(QColor(255,255,255));
mpen.setStyle(Qt::DotLine);
mpainter_white->setPen(mpen);
mpainter_white->begin(this);
for(int i=0 ; i<xSmallCount+1 ;++i)
{
if(i%5 == 0) //每一个大刻度,添加一个刻度值
{
if(i>0)
{
mpainter_white->drawLine(QPointF(margin_left+i*x_small, m_height-margin_bottom), QPointF(margin_left+i*x_small, margin_bottom));
}
}
}
缩放
这里只讲述通过鼠标+滚轮的方式进行缩放
原则,滚轮在绘图区滚动时,将当前鼠标坐标设置为中心点P, 根据滚轮的滚动方向判断是放大还是缩小。放大和缩小的实现一般有两种思路,首先是设置当前缩放比例为1.00.然后一种思路是将该缩放值进行加减[控制数值始终要大于0,否则会改变坐标系的正方向],另外一种是将该缩放值进行乘除,它们二者各有优缺点,但不管是加减,还是乘除,加减的幅度以及乘除的系数,需要设置一个比较合适的值,既能使缩放具有层次感不显得突兀,又能使缩放稳定,不会突然间啥数据也看不到。
还有一个比较重要的功能指标是,在同一个点进行“缩小后再放大”或者“放大后再缩小”的效果应当是一样的。
我最后的缩放原则,是结合了“加减”和“乘除”,第一次将缩放因子设置为1,用户第一次滚动滑轮,只有两种情况,一个是加[缩小],另一个是减[放大],后面的情况根据前面那一次操作来变动,具体决策操作为:
放大:
(1)上一次也是放大 同上一次,并将当前操作存入
(2)上一次是缩小 与上一次取反,并删除上一次操作
缩小:
(1)上一次也是放大 与上一次取反,并删除上一次操作
(2)上一次是缩小 同上一次,并将当前操作存入
我在代码里用了一个链表来存储用户的历史缩放操作,当折线图数据内容更新时,该链表会被清空。当用户通过滚轮缩放时,该链表会记录下用户的操作。当操作为0时,代表将缩放因子减小0.01,当操作为1时,代表将缩放因子增大0.01,当操作为2时,代表将缩放因子缩小1.02倍,当操作为3时,代表将缩放因子增大1.02倍。这样的结果就是,当缩放因子大于1.01时,使用乘除法,否则使用加减法进行缩放。
视觉上,折线图放大时,折线图,应该是以当前鼠标坐标为参考点P,其他的数据点往远离P的方向移动;缩小时,则是朝着靠近P的方向移动。
代码如下:
void FormDynamicCoordinate::wheelEvent(QWheelEvent *e)
{
if(!b_Enable) return;
mouse_pos = e->pos();
//放大缩小
float temp_x = (mouse_pos.x() - originalP.x())*(x_max_mouseScaled - x_min_mouseScaled)*1.0/(xSmallCount * x_small) + x_min_mouseScaled;
float temp_y = (originalP.y() - mouse_pos.y() )*(y_max_mouseScaled - y_min_mouseScaled)*1.0/(m_height-2*margin_bottom) + y_min_mouseScaled;
float cur_x_max_min = x_max_mouseScaled - x_min_mouseScaled;
float cur_y_max_min = y_max_mouseScaled - y_min_mouseScaled;
float percentx = (temp_x - x_min_mouseScaled)*1.0/cur_x_max_min;
float percenty = (temp_y - y_min_mouseScaled)*1.0/cur_y_max_min;
int moperator = -1; //0:-0.01 1:+0.01 2:/1.02 3:*1.02 4:删除最后一个操作
x_y_mouseScaled = 1;
if(e->delta()>0)
{
if(x_y_mouseScaled == 1 && operator_list.size()==0)
{
x_y_mouseScaled = 0.01;
operator_list.append(1);
}
else if(operator_list.size()>0)
{
int val = operator_list.last();
switch (val)
{
case 0:
x_y_mouseScaled += 0.01;
moperator = 4;
break;
case 1:
if(x_y_mouseScaled>1)
{
x_y_mouseScaled *= 1.02;
moperator = 3;
}
else
{
x_y_mouseScaled += 0.01;
moperator = 1;
}
break;
case 2:
x_y_mouseScaled *= 1.02;
moperator = 4;
break;
case 3:
x_y_mouseScaled *= 1.02;
moperator = 3;
break;
default:
break;
}
}
else
{
x_y_mouseScaled += 0.01;
moperator = 1;
}
}
else
{
//0:-0.01 1:+0.01 2:/1.02 3:*1.02
// 放大时:
// 上一次也是放大 同上一次,并将当前操作存入
// 上一次是缩小 与上一次取反,并删除上一次操作
// 缩小:
// 上一次也是放大 与上一次取反,并删除上一次操作
// 上一次是缩小 同上一次,并将当前操作存入
//放大
if(x_y_mouseScaled == 1 && operator_list.size()==0)
{
x_y_mouseScaled -= 0.01;
moperator = 0;
}
else if(operator_list.size()>0)
{
int val = operator_list.last();
switch (val)
{
case 0:
if(x_y_mouseScaled>0.01)
{
x_y_mouseScaled -= 0.01;
moperator = 0;
}
break;
case 1:
x_y_mouseScaled -= 0.01;
moperator = 4;
break;
case 2:
x_y_mouseScaled /= 1.02;
moperator = 2;
break;
case 3:
x_y_mouseScaled /= 1.02;
moperator = 4;
break;
default:
break;
}
}
else
{
x_y_mouseScaled -= 0.01;
operator_list.append(0);
}
}
cur_x_max_min *= x_y_mouseScaled;
cur_y_max_min *= x_y_mouseScaled;
if(cur_x_max_min>2 && cur_y_max_min>2) //加一个限制,当本界面可以显示3个以上数据时才缩放有效
{
if(moperator == 4)
{
operator_list.removeLast();
}
else if(moperator != -1)
{
operator_list.append(moperator);
}
QString str="";
for(int i=0; i<operator_list.size(); ++i)
{
str.append(QString::number(operator_list.at(i)));
}
x_min_mouseScaled = temp_x - cur_x_max_min * percentx;
x_max_mouseScaled = x_min_mouseScaled + cur_x_max_min;
y_min_mouseScaled = temp_y - cur_y_max_min * (percenty);
y_max_mouseScaled = y_min_mouseScaled + cur_y_max_min;
drawRillingData(); //重新绘制坐标系,以及数据
}
}
拖拽
这里要实现用户通过按住鼠标[左键或者右键都行],移动鼠标,以移动折线图。
所以需要重构两个虚函数,在按键刚被按下时,记录下此时的坐标1,当鼠标按键被松开时,记下此时的坐标2,根据这两个坐标,来决定坐标系的四个边界该怎么变化。在这里注意,人们比较愿意接受的折线图,坐标原点是在左下方,而Qt中返回的鼠标坐标所在的坐标原点是在左上方。
具体函数代码如下:
void FormDynamicCoordinate::mousePressEvent(QMouseEvent *e)
{
start_p = e->posF();
}
void FormDynamicCoordinate::mouseReleaseEvent(QMouseEvent *e)
{
if(b_Enable)
{
b_addSmallCount = false;
end_p = e->posF();
int xx = (end_p.x() - start_p.x())*(x_max_mouseScaled - x_min_mouseScaled)/(xSmallCount * x_small);//此处不能用float数据类型,否则会有副作用:期望之外的放大或者缩小
int yy = (end_p.y() - start_p.y())*(y_max_mouseScaled - y_min_mouseScaled)/(25 * y_small);
x_min_mouseScaled -= xx;
x_max_mouseScaled -= xx;
y_min_mouseScaled += yy;
y_max_mouseScaled += yy;
drawRillingData();
}
}
数据点的查询
当鼠标光标在坐标系内移动时,记录下坐标p1,将p1的屏幕坐标映射为坐标系内的坐标,然后借助Qt的QToolTip显示出来。
void FormDynamicCoordinate::mouseMoveEvent(QMouseEvent *e)
{
int temp_x = (e->pos().x() - originalP.x())*(x_max_mouseScaled - x_min_mouseScaled)*1.0/(m_width - margin_left)+x_min_mouseScaled;
if(temp_x>-1 && temp_x<weight_list.size())
{
QString str = "(";
str.append(QString::number(temp_x+1));
str.append(",");
str.append(QString::number(weight_list.at(temp_x)));
str.append(")");
QToolTip::showText(e->globalPos(),str);
}
}
静态折线图
静态折线图的静态,指的是坐标系的四个边界:x轴最小值、x轴最大值、y轴最小值、y轴最大值在初始化时[或者在坐标系出现前]设置一次,后面不再改变。这种坐标系看起来不够灵活,但是当要求数据的采集与显示高度趋于实时同步时,就可以派上用场,因为它可以辅助绘图的优化——只做局部更新,即每次新采集一个数据点,只绘制该数据点,而不重绘前面的数据点。
所以它与动态折线图的区别在于,坐标系只绘制一次,数据点也只绘制一次。
代码如下:
void FormCoordinateMaster::paintEvent(QPaintEvent *e)
{
if(b_drawRillingData)
{
drawRilling_xy(); //绘制坐标轴
b_drawRillingData = false;
}
else if(b_addData)
{
QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing, true); //添加反走样可以使线型更加好看
// painter.begin(this);
QBrush mbrush;
mbrush.setStyle(Qt::Dense4Pattern);
mbrush.setColor(QColor(0,255,0));
painter.setBrush(mbrush);
painter.setOpacity(10);
QPen pen;
pen.setWidth(1);
pen.setStyle(Qt::SolidLine);
pen.setJoinStyle(Qt::RoundJoin);
pen.setColor(QColor(0,255,0));
painter.setPen(pen);
QPointF p1 = getPosition(weight_list.size()-2);
if(p1.x() == -1 && p1.y() == -1)
{
p1 = getPosition(weight_list.size()-1);
}
QPointF p2 = getPosition(weight_list.size()-1);
if(p2.x() == -1 && p2.y() == -1)
{
b_addData = false;
return;
}
painter.drawLine(p1,p2);
b_addData = false;
}
}
扩展应用
动态折线图和静态折线图都各有所长,各有所短,可以根据实际应用场景进行选择以及更为个性化的定制。
这里的扩展应用就是,将动态折线图与静态折线图结合起来,静态折线图,作为驱动图,每接收到一个数据便绘制一条新折线,动态折线图作为从动图[观察区],每接收到一个数据点,只是默默地保存起来[数据源与驱动图的数据源保持一致],通过QSppinBox设置观察区的宽度[即展示的数据量],通过滑动蓝色滑块,改变观察区的左边界。
具体效果如下:
其中蓝色透明滑动块的实现,可以看博文:https://blog.csdn.net/qq_37385181/article/details/82894077
总结
学如逆水行舟,不进则退。
分享来自《你早该这么玩Excel》的3个职场感悟:
职场感悟1——“假设……更多”让人进步
对待Excel中的数据处理,我常常会做假设。即使手头的表格只有8列40行,我也会假设数据量更多,我问
自己:“如果同样的数据多达30列8000行,你还能应付吗?”如果不能,则代表表格需要调整,或者方法需要改
进,这促使我更严谨地思考问题,以及主动研究更合理的解决方法。凭借这样的思维方式,我才能在很短的时间
内,总结出正确的表格设计理念和掌握更多的技能。
以合并单元格为例,当一张表格只有10个合并单元格的时候,也许你会毫不犹豫地选择合并它们,心想:
只要还原10次,就能变回标准的源数据表,但假设把合并的数量放大100倍,你可能就会慎重考虑。如果研究不
出批量还原的方法,就只能选择别的数据记录方式。毕竟,拆分1000个单元格并补齐数据,不是一件好玩儿的
事情。
我常听人说:“我的表格很简单,数据量少,已经习惯了用笨办法,不想也没必要学习新方法了”。那是你
没有“假设……更多”。于是,十年过去,会做的工作还是那么一点点,相应的,拿的工资也还是那一点点。
在职场上,假设工作量更多的人,才能不断发掘更高效的工作方式;假设困难会更多的人,才能未雨绸
缪,提前做好各项准备;即便是假设薪水更多的人,也会因为梦想而有动力。具备实力,机会才有可能降临。词
人方文山说:“成功主要靠机会,但是有实力的人才懂得它是机会”。
职场感悟2——实践的重要性
哪怕是再微不足道的技巧,也要亲身实践,并设置多条件印证,过关后才能确定为一种可行的方法,决不
能贸然下结论,尤其是自己都没有操作过的。
职场感悟3——做个“懒蚂蚁”
“懒蚂蚁”现象,是指一些人平时看上去好像无所事事,但他们花很多时间去思考和提炼,所以往往平时不
显山不露水,而一旦到了关键时刻,就能挑起大梁。所以,“懒”不是态度,而是时间,是效率。
demo下载
https://download.csdn.net/download/qq_37385181/10721080