使用QPainter绘制实现类甘特图,实现绘制画面缩放,实现直线击中算法。
近期因为工作原因,需要实现无线频谱的跳频检测功能,本想使用QCustomplot来实现类似甘特图绘制的,经过试验发现QCustomplot组件未包含该部分样例,后查找资料,发现Qt 相关的甘特图组件KDCHART,经过使用发现也不能满足要求,考虑良久,干脆自己实现,因此就存在来这个样例。当然这个样例程序离实际应用还有一定距离,但是可以给一个思路参考。
本样例程序实现功能要点
1、QPainter实现甘特图绘制(使用随机直线代替甘特图Item,需要扩充则更改直线为矩形Item)。
2、实现对绘制图形的橡皮筋缩放(包含局部缩放)。
3、实现对象的击中算法(这里实现仅仅实现直线击中:注意该直线击中是任意直线)
4、实现在绘制对象超多时的快速检索算法。
5、实现击中对象的Tips功能。
本样例效果:
原始生成并显示随机直线

鼠标击中直线对象后显示对应的直线tips

缩放处理演示
原始缩放状态(注意:这里打开了单元格线标志)

缩放后展示(注意:仅仅实现来X轴缩放, y轴类似)

缩放状态下的击中直线tips展示

1) 对象生成
首先我们要绘制,那么先需要存在对象,因此我们使用随机算法,随机生成一些直线对象,然后保存起来。
QVector m_vecLines;
void randGerneratLines(); //随机生成直线数据
void Widget::randGerneratLines()
{
int linesNumb = qrand() % 1024; //随机确认绘制的线数目
QVector <QLineF> tLines;
tLines.resize(linesNumb);
int w = width();
int h = height();
for (int i=0; i<linesNumb; ++i)
{
int x = qrand() % w;
int y = qrand() % h;
int xoff =qrand() % 40;
QLineF tLine(QPointF(x, y),QPointF(x+xoff, y)); //建立直线对象
tLines[i] = tLine; //保存直线对象到一个向量中
}
setDataLines (tLines); //初始化来数据后,就将直线数据传输到成员,并且绘制原始的Image
}
2)画板绘制
我们要提高绘制性能,需要使用图像缓冲方式绘制,如果直接在QPainter上绘制,在绘制对象非常多的情况下则性能会非常低下,导致界面卡顿。因此我们使用QImage作为一个画板来先行绘制。
QImage* m_pImage; //绘制的原始图片
绘制函数
void drawOrgBimp();
void Widget::drawOrgBimp()
{
//绘制原始的图像
QPainter painter(m_pImage);
painter.setRenderHint(QPainter::Antialiasing);
painter.save();
//设置绘制线笔颜色
painter.setPen(Qt::white); //QColor(123,235,12));
//绘制背景颜色
painter.fillRect(m_pImage->rect(), QBrush(QColor(0,0,0)));
int size = m_vecLines.size();
for (int i = 0; i<size; ++i)
{
painter.drawLine(m_vecLines[i]);
}
//绘制单元格线
if(m_bDrawCellLine)
{
painter.setPen(QColor(23,225,12));
int w = width();
int h = height();
for (int i=0; i<10; ++i)
{
painter.drawLine(QPointF(0,i*m_yCalibrationBase), QPointF(w, i*m_yCalibrationBase));
painter.drawLine(QPointF(i*m_xCalibrationBase,0), QPointF(i*m_xCalibrationBase, h));
}
}
//绘制单元格索引文本
if (m_bDrawCellIndex)
{
for (int row=0;row<10; row++)
{
for (int col=0;col<10;col++)
{
painter.drawText(QPointF(col*m_xCalibrationBase+m_xCalibrationBase/2, row*m_yCalibrationBase+m_yCalibrationBase/2), QString("%1,%2").arg(row).arg(col));
}
}
}
}
3)实际绘制显示
在我们绘制好原始图后,我们需要实际的绘制到我们的屏幕上。
因此我们覆盖实现 void paintEvent(QPaintEvent *event) override; 函数
void Widget::paintEvent(QPaintEvent *event)
{
if (!m_firstDraw)
{
drawOrgBimp();
m_pLocalImage = m_pImage->copy(0,0, m_pImage->width()*m_xScaleRate, m_pImage->height()*m_yScaleRate);
}
QPainter painterThis(this);
QTransform transform = painterThis.transform();
transform.translate(-m_startX*m_xScaleRate, 0); //偏移缩放区的起点部分
painterThis.setTransform(transform);
painterThis.drawImage(0, 0, m_pLocalImage.scaled(m_pLocalImage.width()*m_xScaleRate,height()*m_yScaleRate,Qt::IgnoreAspectRatio));
qDebug() << "m_pLocalImage.width()=" << m_pLocalImage.width();
}
在该函数中真实绘制到屏幕上使用来另外一个Image对象,使用该对象进行实际的屏幕显示是因为我们实现缩放功能所需。
4)缩放准备
我们完成来直线对象生成,绘制,接下来我们来实现缩放功能。要实现缩放功能,那么首先必然存在X轴缩放因子,Y轴缩放因子,因此我们定义成员:
double m_xScaleRate =1.0; //X向缩放比例
double m_yScaleRate =1.0; //Y向缩放比例
在我们绘制到屏幕时,我们采用的是使用一个本地Image从原始底图中拷贝显示内容,然后绘制到屏幕上,此时使用缩放因子来完成真实绘制计算,具体参看 3)中内容。
5)橡皮筋实现
我们要进行缩放控制,本样例采用的是鼠标橡皮筋方式,正向拖拽橡皮筋是放大,反向橡皮筋选择则是还原。
我们要实现橡皮筋,那么我们需要使用QRubberBand类。
定义成员:
QRubberBand * mRubberBand; //实现橡皮筋的对象
在构造中初始化列表中
, mRubberBand(new QRubberBand(QRubberBand::Rectangle, this))
然后我们重写
void mousePressEvent(QMouseEvent *event) override;
void mouseMoveEvent(QMouseEvent *event) override;
void mouseReleaseEvent(QMouseEvent *event)override;
//记录鼠标按下点,与标志,设置橡皮筋对象显示
void Widget::mousePressEvent(QMouseEvent *event)
{
if (event->button() == Qt::LeftButton)
{
m_bPressFlag = true;
mOrigin = event->pos();
mRubberBand->setGeometry(QRect(mOrigin, QSize()));
mRubberBand->show();
}
QWidget::mousePressEvent(event);
}
//在鼠标按下状态改变橡皮筋大小
void Widget::mouseMoveEvent(QMouseEvent *event)
{
if (m_bPressFlag)
{
if (mRubberBand->isVisible())
{
mRubberBand->setGeometry(QRect(mOrigin, event->pos()).normalized());
}
}
else {
QPointF curPt = event->pos();
QLineF line = getObjLineF(curPt);
}
QWidget::mouseMoveEvent (event);
}
//在鼠标弹起的位置判断橡皮筋对象是正向还是反向,正向执行放大处理,计算反向则进行缩放还原
void Widget::mouseReleaseEvent(QMouseEvent *event)
{
m_bPressFlag = false;
if (mRubberBand->isVisible())
{
if(mOrigin.rx() < event->pos().rx()) {
const QRect zoomRect = mRubberBand->geometry();
int xp1, yp1, xp2, yp2;
zoomRect.getCoords(&xp1, &yp1, &xp2, &yp2);
qreal rw = width();
double tScale = rw / zoomRect.width();
//需要通过当前鼠标点计算真实起点,以及终点,需要通过上一次存在的缩放因子计算,因此在这里要在最新因子计算的前面进行计算
//当前点除以上次缩放因子加上本次缩放窗口的原起点。
//真实起点X = 上次起点X + 当前鼠标点X / 缩放因子
m_startX = m_startX + mOrigin.rx()/m_xScaleRate; //等于上一次的起点+本次缩放区内部点长
//缩放因子计算正确,缩放因子=上次缩放因子*本次计算缩放因子
m_xScaleRate= m_xScaleRate * tScale;
qreal rh = height();
qreal ry = rh / zoomRect.height();
m_yScaleRate = 1.0;
m_pLocalImage = m_pImage->copy(mOrigin.rx(),0, zoomRect.width(), height());
}
else {
//还原只需要把原来记录下来的原始设置重新设置一次就搞定
m_xScaleRate = 1.0;
m_yScaleRate = 1.0;
m_startX = 0.0;
m_startY = 0.0;
m_pLocalImage = m_pImage->copy(m_startX, m_startY, m_pImage->width(), m_pImage->height());
}
mRubberBand->hide();
update();
}
QWidget::mouseReleaseEvent (event);
}
6)击中实现
接下来我们实现直线击中基础算法。
void calculateRegionRef (qreal Vx, qreal Hx, qreal x1, qreal y1, qreal x2, qreal y2,
qreal &ax1, qreal &ay1, qreal &ax2, qreal &ay2, qreal &ax3, qreal &ay3, qreal &ax4, qreal &ay4);
//计算直线的指定(x,y)偏移外围区域
void Widget::calculateRegionRef (qreal Vx, qreal Hx, qreal x1, qreal y1, qreal x2, qreal y2,
qreal &ax1, qreal &ay1, qreal &ax2, qreal &ay2, qreal &ax3, qreal &ay3, qreal &ax4, qreal &ay4)
{
//竖直
double xval = abs(x2 - x1);
//if (abs(x2 - x1) == 0) //本句原意
if (!(xval>0 || xval<0))
{
if (y2 > y1)
{
ax1 = x1 - Vx;
ay1 = y1 - Hx;
ax2 = x1 + Vx;
ay2 = y1 - Hx;
ax3 = x1 - Vx;
ay3 = y2 + Hx;
ax4 = x1 + Vx;
ay4 = y2 + Hx;
}
else
{
ax1 = x1 - Vx;
ay1 = y2 - Hx;
ax2 = x1 + Vx;
ay2 = y2 - Hx;
ax3 = x1 - Vx;
ay3 = y1 + Hx;
ax4 = x1 + Vx;
ay4 = y1 + Hx;
}
return;
}
qreal k = qreal(y2 - y1)/qreal(x2 - x1);
qreal angle = atan(k);
//线段长度的一半
//float halfLenth = float(sqrt((x2 - x1)*(x2 - x1) + (y2 - y1)*(y2 - y1)))/2;
//中点
qreal mx = (x2 + x1)/2;
qreal my = (y2 + y1)/2;
qreal alf;
qreal cx1,cy1,cx2,cy2;
alf = (-1)*angle;
cx1 = qreal(cos(alf) * (x1 - mx) - sin(alf) * (y1 - my) + mx);
cy1 = qreal(sin(alf) * (x1 - mx) + cos(alf) * (y1 - my) + my);
cx2 = qreal(cos(alf) * (x2 - mx) - sin(alf) * (y2 - my) + mx);
cy2 = qreal(sin(alf) * (x2 - mx) + cos(alf) * (y2 - my) + my);
qreal bx1, by1,bx2, by2,bx3, by3,bx4, by4;
if (x1>x2)
{
bx1 = cx1 + Hx;
by1 = cy1 + Vx;
bx2 = cx1 + Hx;
by2 = cy1 - Vx;
bx3 = cx2 - Hx;
by3 = cy2 + Vx;
bx4 = cx2 - Hx;
by4 = cy2 - Vx;
}
else
{
bx1 = cx1 - Hx;
by1 = cy1 + Vx;
bx2 = cx1 - Hx;
by2 = cy1 - Vx;
bx3 = cx2 + Hx;
by3 = cy2 + Vx;
bx4 = cx2 + Hx;
by4 = cy2 - Vx;
}
qreal theta = angle;
ax1 = qreal(cos(theta) * (bx1 - mx) - sin(theta) * (by1 - my) + mx);
ay1 = qreal(sin(theta) * (bx1 - mx) + cos(theta) * (by1 - my) + my);
ax2 = qreal(cos(theta) * (bx2 - mx) - sin(theta) * (by2 - my) + mx);
ay2 = qreal(sin(theta) * (bx2 - mx) + cos(theta) * (by2 - my) + my);
ax3 = qreal(cos(theta) * (bx3 - mx) - sin(theta) * (by3 - my) + mx);
ay3 = qreal(sin(theta) * (bx3 - mx) + cos(theta) * (by3 - my) + my);
ax4 = qreal(cos(theta) * (bx4 - mx) - sin(theta) * (by4 - my) + mx);
ay4 = qreal(sin(theta) * (bx4 - mx) + cos(theta) * (by4 - my) + my);
}
//实际的检测是否击中直线,传入参数_checkline是要检测的直线, _pt则是鼠标点
bool Widget::checkLineHit(const QLineF &_checkline,QPointF _pt)
{
QPointF pt = QPointF(_pt.rx(), _pt.ry());
qreal x1 = _checkline.p1().rx() ;
qreal x2 = _checkline.p2().rx() ;
qreal y1 = _checkline.p1().ry() ;
qreal y2 = _checkline.p2().ry() ;
QPolygonF ploygon;
qreal vx = 2.0, hx = 6.0;
qreal rsx1, rsy1, rsx2, rsy2, rsx3, rsy3, rsx4, rsy4;
//根据直线两点计算直线的外围矩形--与直线包容平行的矩形
calculateRegionRef(vx, hx, x1, y1, x2, y2, rsx1, rsy1, rsx2, rsy2, rsx3, rsy3, rsx4, rsy4);
ploygon.append(QPointF(rsx1, rsy1));
ploygon.append(QPointF(rsx3, rsy3));
ploygon.append(QPointF(rsx4, rsy4));
ploygon.append(QPointF(rsx2, rsy2));
return ploygon.containsPoint(_pt, Qt::OddEvenFill); // Qt::WindingFill
}
7)快速检索思路
我们实现来直线的击中算法后,通常我们就应该直接的在mouseMove函数中进行击中测试,一般的做法是遍历我们的所有对象进行击中测试,但是这样做效率非常的低下,假如存在10000个对象的情况下,最坏需要循环10000次,因此我们需要进行一个快速检索的算法。
思路:
a、对象都是存在于屏幕上,通常情况下我们的对象不会大量重叠,因此我们可以将屏幕进行表格化分区,然后将表格化单元格矩形保存到到一个Hash表中,这个hash表的key就是我们的单元格索引标号(行,列)
b、我们对所有的对象进行分区关联保存,保存到一个Multihash中。也就是对应单元格索引作为key,与该单元格矩形区域相关的对象均保存到这个key值中 (注意直线对象需要检测起点与终点---该算法存在一个问题,当直线太长超过多个单元格时,中间位置不能检索到,但我的应用中直线长度不会超过单元格,最多就是横跨2格单元格)。
c、在应用检索时,首先判断鼠标点在单元格的位置,获取到单元格索引,然后通过单元格索引号查找到该单元格关联的直线对象,这样通过一个二级检索,可以大量的减少检索次数,从而提高性能。
8) 快速检索具体实现
成员如下:
//将宽与高均分成10份,也就是一共10 X项hash,10个Y项hash.
int m_xNumb; //均分等份数目,默认是10
int m_yNumb; //均分等份数目,默认是10
qreal m_xCalibrationBase; //等于宽/m_xNumb
qreal m_yCalibrationBase; //等于高/m_yNumb
QHash <QPair<int,int> , QRectF> m_keyTableHash; //用来存储表格单元矩形的hash容器,其中QPair<int,int>是相当于QModelIndex(row, col)
//用来记录线对象属于哪一个单元区域的多值hash
QMultiHash<QPair<int,int>, QLineF > m_multiHash; //每个单元格下会包含的线对象
void Widget::positionLines()
{
int linesNumb = m_vecLines.size(); //线对象数目
m_multiHash.clear();
for (int i=0; i<linesNumb; ++i)
{
//判断线对象起点的所属块索引号
int row = getBlockIndex_Row(m_vecLines[i].p1().ry());
int col = getBlockIndex_Col(m_vecLines[i].p1().rx());
//创建对应单元索引
QPair<int, int> pair;
pair.first = col;
pair.second = row;
//根据求出的单元索引区判断该单元是否存在,并且按找到的单元矩形作为key,记录对应的线对象到多值hash中
auto it = m_keyTableHash.find(pair);
if (it != m_keyTableHash.end())
{
m_multiHash.insert(pair, m_vecLines[i]) ;
}
//检查直线的尾部是否也是归属与上面的区域,如果尾部点位置不属于上面单元,则需要把这条直线同时放到下一个单元中
int col1 = getBlockIndex_Col(m_vecLines[i].p2().rx());
if (col != col1)
{
QPair<int, int> pair1;
pair.first = col1;
pair.second = row;
auto it = m_keyTableHash.find(pair1);
if (it != m_keyTableHash.end())
{
m_multiHash.insert(pair, m_vecLines[i]) ;
}
}
}
update ();
}
9)鼠标移动击中对象后tips实现
我们已经实现了缩放,对象击中,这之后我们需要在鼠标移动到对象(对象被击中时)后,给出tips,因此我们可以直接使用我们的绘图窗口的toolTips功能,在我们的击中位置添加对应的tips就可以完成这个功能。具体位置在mouseMove中
void Widget::mouseMoveEvent(QMouseEvent *event)
{
if (m_bPressFlag)
{
if (mRubberBand->isVisible())
{
mRubberBand->setGeometry(QRect(mOrigin, event->pos()).normalized());
}
}
else {
QPointF curPt = event->pos();
QLineF line = getObjLineF(curPt); //检测在这里完成
}
QWidget::mouseMoveEvent (event);
}
QLineF Widget::getObjLineF(QPointF _pt)
{
//当前传入的点坐标需要计算出原来缩放因子1.0时的坐标(X,y),需要加上记录的m_startX
double realX = m_startX + _pt.rx()/m_xScaleRate;
QPointF xp = QPointF(realX , _pt.ry());
int col = getBlockIndex_Col(xp.rx());
int row = getBlockIndex_Row(xp.ry());
qDebug() << "(row,col)->(" << row << "," << col << ")" << "xp.rx()=" << realX << "_pt.ry()=" << _pt.ry();
QPair<int,int> curKey (col, row);
QList<QLineF> tLines = m_multiHash.values(curKey);
int size = tLines.size();
QLineF line ;
for (int i=0; i<size; ++i)
{
bool hited = checkLineHit(tLines.at(i), xp);
if (hited)
{
line = tLines.at(i);
qDebug() << "Hit lines : " << "rx1=" << line.p1().rx() << "ry1=" << line.p1().ry() << " rx2=" << line.p2().rx() << "ry2=" << line.p1().ry() ;
QString str = "Hit lines : ";
str += QString::number(line.p1().rx(), 'f' , 2);
str += "ry1=";
str += QString::number(line.p1().ry(), 'f' , 2);
str += " rx2=" ;
str += QString::number(line.p2().rx(), 'f' , 2);
str += " ry2=" ;
str += QString::number(line.p1().ry(), 'f' , 2);
this->setToolTip(str); //这里设置tips字符串
this->setToolTipDuration(2000);*
}
else {
*this->setToolTip("");
this->setToolTipDuration(2000);*
}
}
return line;
}
10)完整头文件如下:
#ifndef WIDGET_H
#define WIDGET_H
#include <QWidget>
#include <QMap>
#include <QVector>
#include <QVariant>
#include <QTimer>
#include <QLineF>
#include <QMultiHash>
#include <QModelIndex>
#include <QPixmap>
#include <QImage>
#include <QRubberBand>
#include <QLabel>
namespace Ui {
class Widget;
}
class Widget : public QWidget
{
Q_OBJECT
public:
explicit Widget(QWidget *parent = nullptr);
~Widget();
public :
void calculateRegionRef (qreal Vx, qreal Hx, qreal x1, qreal y1, qreal x2, qreal y2,
qreal &ax1, qreal &ay1, qreal &ax2, qreal &ay2, qreal &ax3, qreal &ay3, qreal &ax4, qreal &ay4);
void setDataLines(QVector <QLineF> &_lines); //设置需要绘制的直线数据
public slots:
void on_drawLines(QVector <QLineF> _lines); //绘制跳频线
void timeout();
protected:
void paintEvent(QPaintEvent *event) override;
void mousePressEvent(QMouseEvent *event) override;
void mouseMoveEvent(QMouseEvent *event) override;
void mouseReleaseEvent(QMouseEvent *event)override;
void resizeEvent(QResizeEvent *event);
//void wheelEvent(QWheelEvent *event);
private:
int getBlockIndex_Col(qreal xVal); //判断X值属于的单元格块X向索引
int getBlockIndex_Row(qreal yVal); //判断Y值属于的单元格块Y向索引
void initKeyTable();
void positionLines(); //关联直线到单元格,可以通过鼠标点查找到归属单元格,然后在归属单元格中判断击中直线对象,可以加快检索速度
void randGerneratLines(); //随机生成直线数据
QLineF getObjLineF (QPointF _pt);
bool checkLineHit (const QLineF &_checkline, QPointF _pt);
void drawOrgBimp();
private:
Ui::Widget *ui;
QColor m_LineColor;
QVector <QLineF> m_vecLines;
QTimer* m_pTimer;
//用来记录线对象属于哪一个单元区域的多值hash
QMultiHash<QPair<int,int>, QLineF > m_multiHash; //每个单元下会包含的线对象
//将宽与高均分成10份,也就是一共10 X项hash,10个Y项hash.
int m_xNumb; //均分等份数目,默认是10
int m_yNumb; //均分等份数目,默认是10
qreal m_xCalibrationBase; //等于宽/m_xNumb
qreal m_yCalibrationBase; //等于高/m_yNumb
QHash <QPair<int,int> , QRectF> m_keyTableHash; //用来存储表格单元矩形的hash容器,其中QPair<int,int>是相当于QModelIndex(row, col)
double m_xScaleRate; //X向缩放比例
double m_yScaleRate; //Y向缩放比例
QImage* m_pImage; //绘制的原始图片
QRubberBand * mRubberBand; //实现橡皮筋的对象
QPoint mOrigin; //记录鼠标位置的点对象
bool m_bPressFlag; //鼠标按下标志
qreal m_startX; //用来记录缩放区的真实X起点坐标
qreal m_startY; //用来记录缩放区的真实Y起点坐标
QImage m_pLocalImage; //用来实际显示的图片
bool m_firstDraw = false;
bool m_bDrawCellLine = true; //绘制单元格直线
bool m_bDrawCellIndex = false; //绘制每个单元的索引编号
};
#endif // WIDGET_H
有需要样例源码的请到:https://download.youkuaiyun.com/download/wangxuejun1972/21090178
本文档介绍了一种使用QPainter绘制甘特图的方法,包括随机生成直线模拟甘特图、图像缓冲绘制、橡皮筋缩放以及直线击中算法。此外,还实现了在大量直线对象中快速检索和显示Tips的功能。通过单元格分区和多值哈希表优化了检索性能。提供了完整的Qt代码实现。
1254

被折叠的 条评论
为什么被折叠?



