Qt自定义折线图控件

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/qq_37385181/article/details/83055915

目录

 

基础效果图

前言

设计要点

界面

功能

理论学习

代码实战

界面的美化

动态折线图

坐标轴的绘制

缩放

拖拽

数据点的查询

静态折线图

扩展应用

总结

demo下载


基础效果图

 

前言

使用Qt自定义折线图,可以自己控制折线图的重绘规则,究竟是每添加一个数据就刷新整个折线图,还是只刷新部分折线图。

我把折线图分为以下两类:

  1. 坐标系是静态的,折线图更新快。
  2. 坐标系是动态的,数据的变化更为明显。

 

设计要点

界面

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

 

 

 

猜你喜欢

转载自blog.csdn.net/qq_37385181/article/details/83055915