最近有个使用Qt动态创建柱状图的需求,使用Qt中的图表类Qchart封装一个自定义柱状图类,该类需要完成三点需求:
1、支持X轴和Y轴的同时动态输入,即表中X轴条柱个数和Y轴数值动态变化;
2、X轴条柱个数不变,动态刷新Y轴对应各个条柱数值。
3、在柱状图显示时,支持鼠标悬停时显示条柱数值。
【思路】
1、考虑自定义这个类如何在主窗口中的展示,在类似使用QHBoxLayout创建一个固定的水平布局对象,然后将创建好的QChartView图表对象放置在这个布局中。当出现图表重新绘制时,将原来的图表对象从布局中移除,之后将新图表对象添加到原来的布局对象中,这样布局对象固定的只有一个图表。在主窗口上的显示,同样的创建一个水平布局的空间(通过ui设计师界面添加),在主窗帘对象的初始化过程中,将创建好了的自定义图表类对象中的布局成员添加到ui界面中,这样自定义类中图表的改变,就可实时动态显示在上面了。
2、考虑到支持X轴和Y轴的动态刷新,封装好的类提供一个接口接受数据,根据输入数据,如果发现新数据需要X轴条柱个数,则需要销毁原来图表,根据数据重新创建;如果新数据内容所需X轴条柱个数与当前图表X轴条柱个数一致,则仅仅需要更新当前图表的Y轴数据即可。这样一来,提供给外面的接口尽量的简单,将第一点和第二点需求内容的判断由接口内部判断。
3、鼠标悬停时显示条柱Y轴数值,这个涉及到鼠标事件,这个功能由主窗口实现,而鼠标悬停位置参数需要图表中的QBarSerises对象提供,因此自定义封装的图表需要同时对外更新提供QBarSeries成员,用于主窗口信号的触发。
4、平台要求:Qt Creator 4.5.1,支持Qtchart(需安装有对应的插件)
【实现】
基于上面的思路,数据更新和柱状图对象创建封装在自定义柱状图类中,自定义封装好的类主要提供接口方法和内部成员对象如下:
public:
//清空当前柱状图Y轴数据
void ClearData();
//数据更新接口:内部判断是重置原柱状图还是更新Y轴数据
void UpdateDate(std::map<QString, int> mapData);
//数据更新接口:仅更新柱状图Y轴数据,多于X轴条柱个数不做新增处理
void UpdateDate(std::vector<int> vecDate);
private:
//根据输入数据创建对应柱状图
QChartView * CreaterNewChartView(std::map<QString, int> mapData);
public:
QChartView *m_Chartview;
QHBoxLayout *m_Layout;
QBarSeries *m_Series;
QBarSeries *m_SeriesNew;
unsigned int m_uiCnt; //当前柱状图X轴条柱个数
QBarSet *m_Barset;
数据更新接口主要有两个,一个是外部已知的不改变X轴条柱个数,仅更新Y轴数据,另一个是X轴与Y轴需同步更新。两个数据更新接口的实现如下:
//不改变当前柱状图,仅更新Y轴数据
void BarChartBuilder::UpdateDate(std::vector<int> vecDate)
{
if(m_uiCnt == vecDate.size())
{
//已有图标条柱个数与新数据一致,不需要重置
for(unsigned int idx = 0; idx < vecDate.size(); idx++)
m_Barset->replace(idx, vecDate[idx]);
}
}
//改变当前柱状图重新创建
void BarChartBuilder::UpdateDate(std::map<QString, int> mapData)
{
if(0 == mapData.size())
return;
if(m_uiCnt == mapData.size())
{
std::vector<int> vecDate;
for(std::map<QString, int>::iterator it = mapData.begin(); it != mapData.end(); it++)
{
vecDate.push_back(it->second);
}
UpdateDate(vecDate);
return;
}
QChartView *pchartView = CreaterNewChartView(mapData);
m_Layout->removeWidget(m_Chartview);
m_Layout->addWidget(pchartView);
m_Chartview = pchartView;
m_Series = m_SeriesNew;
m_uiCnt = mapData.size();
}
其中,将柱状图创建功能单独作为一个private的方法实现,柱状图的创建方法返回一个QChartView对象,提供给数据更新接口重置布局中的图表对象。需要说明的是,我这里X轴上的每个条柱仅需要支持一个数据,如果需要支持多个,可参考官方案例,仅需要多创建对应的QBarSet即可,以此类推。
QChartView * BarChartBuilder::CreaterNewChartView(std::map<QString, int> mapData)
{
m_Barset = new QBarSet("");
m_SeriesNew = new QBarSeries();
m_SeriesNew->append(m_Barset);
QChart *pChart = new QChart();
pChart->addSeries(m_SeriesNew);
pChart->setTitle("Simple barchart example"); // 设置图表的标题
pChart->setAnimationOptions(QChart::SeriesAnimations); // 动画效果
//pseries->setLabelsPosition(QAbstractBarSeries::LabelsInsideEnd);//单条数据数值
//pseries->setLabelsVisible(true); //数值可视化
QStringList pcategories;
int iMax = 0;
for(std::map<QString, int>::iterator it = mapData.begin(); it != mapData.end(); it++)
{
m_Barset->append(it->second);
pcategories.append(it->first);
if(iMax < it->second)
iMax = it->second;
}
QBarCategoryAxis *paxis = new QBarCategoryAxis(); //X轴
paxis->append(pcategories);
pChart->setAxisX(paxis, m_SeriesNew);
QValueAxis *mAyis= new QValueAxis; //Y轴
pChart->addAxis(mAyis, Qt::AlignLeft);
m_SeriesNew->attachAxis(mAyis);
mAyis->setRange(0, iMax + 100); //Y轴大小值
mAyis->setLabelFormat("%u"); //Y轴数据格式
pChart->legend()->setVisible(false); //数据说明不可见
//pChart->legend()->setAlignment(Qt::AlignBottom);//数值说明放底部
QChartView *pchartView = new QChartView(pChart);
pchartView->setRenderHint(QPainter::Antialiasing);
return pchartView;
}
另外,为了支持柱状图数据的清空,提供ClearData接口用于清空Y轴数据,但不重置已有柱状图
void BarChartBuilder::ClearData()
{
if(nullptr != m_Series)
{
m_Series->clear();
}
}
针对鼠标在柱状图上悬停时显示对应Y轴数据的实现,需要在数据更新给自定义柱状图类对象后,将新的柱状图对象中的QBarSeries成员通过信号槽连接的方式连接到主窗口的槽函数中,这样主窗口可获知当前鼠标所处位置属于哪个条柱。鼠标悬停时数值内容的显示以标签的形式展示,显示的标签通过new出来的QLabel对象存放。为了方便管理和避免内存泄漏,主窗口用一个vector容器存放新数据更新好了之后新创建的QLabel标签对象。在每次新数据更新后,该vector容器先delete清空原来的标签对象,之后放入对应X轴条柱个数的QLabel对象。标签对象清理和新柱状图信号槽连接的实现如下:
//重置鼠标悬停时的标签对象,连接新柱状图的信号槽函数
void MainWindow::ResetLabel()
{
connect(m_BarChart->m_Series, SIGNAL(hovered(bool,int,QBarSet*)), this, SLOT(sltTooltip(bool,int,QBarSet*)));
for(unsigned int index = 0; index < m_vecToolTips.size(); index++)
{
delete m_vecToolTips[index];
}
m_vecToolTips.clear();
for(unsigned int index = 0; index < m_BarChart->m_uiCnt; index++)
{
QLabel *label = nullptr;
m_vecToolTips.push_back(label);
}
}
//鼠标悬停时标签创建和显示
void MainWindow::sltTooltip(bool status, int index, QBarSet *barset)
{
if(nullptr != m_BarChart->m_Series && nullptr != m_BarChart->m_Chartview)
{
QChart* pchart = m_BarChart->m_Chartview->chart();
QLabel *m_tooltip = nullptr;
if(nullptr != m_vecToolTips[index])
{
m_tooltip = m_vecToolTips[index];
}
else
{
m_tooltip = new QLabel(m_BarChart->m_Chartview); //头文件中的定义 QLabel* m_tooltip = nullptr;
m_tooltip->setStyleSheet("background: rgba(240, 128, 128,185);color: rgb(248, 248, 255);"
"border:0px groove gray;border-radius:10px;padding:2px 4px;"
"border:2px groove gray;border-radius:10px;padding:2px 4px");
m_tooltip->setVisible(false);
m_vecToolTips[index] = m_tooltip;
}
if (status)
{
double val = barset->at(index);
QPointF point(index, barset->at(index));
QPointF pointLabel = pchart->mapToPosition(point);
m_tooltip->setText(QString::number(val));
m_tooltip->move(pointLabel.x(), pointLabel.y() - m_tooltip->height()*1.5);
m_tooltip->show();
}
else
{
m_tooltip->hide();
}
}
}
【效果】
通过上述实现,在demo中的实现效果如下图所示
完整demo代码资源:定义封装Qt柱状图类,实现数据动态更新和鼠标数值显示