资讯动态

Qt QChart饼状图实战:配置、交互、刷新与避坑指南

发布时间:2026/10/20 7:50:49 来源:尧图企业网站定制
前一阵做项目改版产品丢过来一句话“这几块统计数据别老用表格列了整点直观的。”我第一反应就是上图表而Qt项目里最顺手的官方方案就是QChart。可真等我去写饼状图才发现“把饼图画出来”和“把饼图用得舒服”之间隔着不小的一段路。这篇文章也是从那时候起想写的免得以后再做同类需求时还得重新趟一遍。这篇文章不会只甩一段能跑的Demo而是完整走一遍饼状图的从零搭建过程工程配置、QPieSeries的基本用法、标签和配色、点击交互、动态刷新以及我在实际项目里踩过的几个坑。内容以Qt Widgets QChart为背景适合已经在用Qt、想把饼状图做得不那么像Demo的开发者。1. 先把QChart的饼状图套路摸清楚数据入模、图表装配、标签成型1.1 为什么不自己画扇形而是用QChart想在Qt里画出饼状图摆在桌面上的路线其实有好几条纯QPainter手绘、QCustomPlot、QWT、QChart。纯QPainter听起来很自由但饼状图真正麻烦的从来不是画那几条扇形边而是数据到图形元素的绑定、标签防重叠、图例、动画、后续缩放和换肤。手写一版能跑等产品提一句“点击这一块要弹详情”工程量立刻就上来了。QCustomPlot和QWT我也都在项目里接触过社区质量不错但饼状图在其中的生态相对薄弱而且引入第三方库还要考虑维护和授权。QChart是Qt官方的图表模块Widgets和QML两条线都能用饼图基于QPieSeries每个数据点被封装成一个QPieSlice交互信号、动画、主题都是现成的。对大多数项目来说这是投入产出比最合适的一条路。我并不是说QChart高级到没有缺点它的滚动缩放能力、大序列性能都比不上一些商业图表库。但就饼状图这个场景它能让我们把精力集中在“数据呈现得对不对”这件事上而不是跟图形学底层较劲。1.2 工程配置与最小代码骨架先说工程配置。qmake项目在pro文件里加一行QT charts如果走CMake在CMakeLists里写find_package(Qt6 COMPONENTS Charts REQUIRED) target_link_libraries(your_app PRIVATE Qt6::Charts)下面这段代码是最小可运行版本能直接画出一张包含三个分类的饼状图#include QApplication #include QtCharts/QChartView #include QtCharts/QPieSeries #include QtCharts/QPieSlice int main(int argc, char *argv[]) { QApplication app(argc, argv); QPieSeries *series new QPieSeries(); series-append(QStringLiteral(部门A), 320); series-append(QStringLiteral(部门B), 180); series-append(QStringLiteral(部门C), 90); series-setLabelsVisible(true); series-setLabelsPosition(QPieSlice::LabelOutside); QChart *chart new QChart(); chart-addSeries(series); chart-setTitle(QStringLiteral(某季度任务完成量占比)); chart-legend()-setAlignment(Qt::AlignRight); QChartView *view new QChartView(chart); view-setRenderHint(QPainter::Antialiasing, true); view-resize(600, 400); view-show(); return app.exec(); }这段代码里有几个地方值得解释。QPieSeries::append的第一个参数会成为默认的图例文本第二个参数是原始数值传整数或小数都可以不需要预先算好百分比——QChart在内部会把所有切片值加起来做归一化。这一点对新手很友好意味着从数据库里查出来多少就塞多少比例关系它自己处理。setLabelsVisible(true)之后切片外侧会默认显示一个百分比。如果想让标签显示成“部门A 54%”这种语义完整的文字不能只靠默认开关需要逐切片定制。后面我会专门说这块。1.3 先想清楚标签显示原始值还是百分比做统计分析时饼状图的标签通常有两种写法纯百分比或者“名称百分比”。纯百分比配合图例看也不是不行但用户要来回对照颜色和图例体验会打折。我实际项目里的习惯是标签统一显示“名称百分比”然后视情况把图例隐藏让画面更干净。要实现这种效果需要逐块设置切片的标签for (QPieSlice *slice : series-slices()) { QString name slice-label(); qreal percent slice-percentage() * 100.0; slice-setLabel(QStringLiteral(%1\n%2%).arg(name).arg(QString::number(percent, f, 1))); }注意slice-label()取到的是最初append时传进的名称不等于默认百分比标签。percentage()返回的是当前值在总数中的比例系数范围0到1所以乘100才是百分比。这条规则也决定了数据模型该存哪些字段。我建议用一个结构体保存业务key、显示名、value而不是只存一个裸数值struct PieDatum { QString key; QString displayName; qreal value; };后面要做的点击事件、图例匹配、颜色匹配都会依赖这个key。拿“部门A”这种显示名去做匹配一旦文案调整代码就全乱。这是个看起来很小、后劲却很痛的设计决定。2. 让它变得不廉价配色、标签和环形图的实际处理2.1 颜色要统一不要让每张饼各唱各的调QChart默认会给每个切片分配颜色颜色来自当前主题的调色板。问题在于同一张饼里切片的顺序不同颜色分配就不同如果页面里有五张饼同一个部门在不同饼里的颜色可能完全不一样。用户看完第一张图再看第二张直觉是“部门A就应该是蓝色”结果它变成了橙色之前建立的颜色认知就被打断了。我的做法是在数据层定一张全局色板用业务key做索引。无论哪个页面只要是同一个业务key颜色必须保持一致。实现不复杂创建切片时手动设置颜色QHashQString, QColor colorMap; colorMap.insert(QStringLiteral(A), QColor(#3B82F6)); colorMap.insert(QStringLiteral(B), QColor(#F59E0B)); colorMap.insert(QStringLiteral(C), QColor(#10B981)); QListPieDatum datas loadFromDatabase(); for (const PieDatum d : datas) { QPieSlice *slice series-append(d.displayName, d.value); if (colorMap.contains(d.key)) { slice-setColor(colorMap.value(d.key)); } }你可能会问少了自动配色不会更麻烦吗只有一张饼时确实会但当系统里有十几个页签、多张图表时统一的色板能省掉大量UI走查沟通用户也更容易把不同页面的同一业务关联起来。2.2 标签引线和小切片处理标签不丑是专业图表的第一道门。饼状图的标签量大最容易出现两个问题小切片之间的引线交叉标签文字重叠。默认的LabelOutside模式会把标签放在切片外侧再通过一根短引线连到切片。当两个相邻切片都很小时标签很容易挤在一起。我遇到这种情况的处理顺序是先看数据能不能合并小切片不能合并时把所有小于5%的切片改成LabelInside实在不行直接把该切片的标签隐藏只保留图例。这三个手段比在QChart里找“自动避让”开关现实得多。说实话QChart并没有特别智能的标签避让算法所以别指望一个神秘参数能让标签自己飞开。引线的长度可以用切片的labelArmLengthFactor控制默认值是0.2。觉得引线太短可以调大这个参数是相对比例不是像素值调的时候要对着实际渲染效果反复试。顺便说一句如果标签包含中文记得同时设置字体和字号。QChart默认字体在中文环境下偏小偏细我会在主函数里设置全局字体QFont defaultFont(QStringLiteral(Microsoft YaHei), 10); QApplication::setFont(defaultFont);这个点在文章后面“踩坑”部分还会再出现因为很多中文方框问题并不在QChart在全局字体配置。2.3 用环形图承载中心KPI仪表盘场景里实心饼状图的中间区域往往是浪费的。改成环形图成本极低QPieSeries一行代码就够series-setHoleSize(0.45);holeSize是空洞直径相对饼图直径的比例0.45是我项目里用得最多的值看起来像圆环又不至于太细。有了洞之后中间那块地方可以叠一个QLabel显示总数或完成率视觉重心一下就起来了。把Label叠在QChartView上的操作并不复杂QVBoxLayout *layout new QVBoxLayout(widget); layout-setContentsMargins(0, 0, 0, 0); layout-addWidget(view); QLabel *centerLabel new QLabel(QStringLiteral(590)); centerLabel-setAlignment(Qt::AlignCenter); centerLabel-setStyleSheet(QStringLiteral(color:#333; font-size:28px; font-weight:bold; background:transparent;)); layout-addWidget(centerLabel, 0, Qt::AlignCenter);用一个带透明背景的QLabel盖上去比直接在chart上画文字简单后续改成两行、加动画都方便。3. 交互和动态刷新饼状图才算真正能用起来3.1 切片点击反馈和业务信号饼状图不能只是静态展示点一下没反应用户会觉得自己在看一张截图。QPieSlice自带两个很实用的信号hovered和clicked。hovered适合做鼠标悬停高亮clicked适合做选中或跳转。我的常见写法是这样connect(slice, QPieSlice::hovered, this, [slice](bool state) { if (state) { slice-setBrush(slice-brush().color().lighter(115)); } else { slice-setBrush(slice-brush().color()); } }); connect(slice, QPieSlice::clicked, this, [this, key](bool state) { for (QPieSlice *s : m_series-slices()) { s-setExploded(false); } slice-setExploded(true); emit sliceClicked(key); });这里有几个细节值得注意。hovered信号会传入一个bool直接在槽函数里用即可。clicked的lambda里如果没有把key值捕获进去拿到的永远是循环结束后的最后一个key这是新手特别容易踩的坑一定要用[key]或[this, key]形式。setExploded(true)是QChart提供的切片分离效果让选中的一块外飘一点。想让分离更明显可以调slice-setExplodeDistanceFactor(0.1)。这个值表示分离距离相对饼图半径的比例经验上0.1到0.15比较舒服太大会让整张图显得松散。把点击信号暴露给外部后外面就可以接业务了。比如饼图旁边放一个详情面板点击“部门A”就刷新该部门的明细列表。3.2 正确的刷新方式整组替换而不是原地清空仪表盘里的数据几乎不会永远不变。最简单也最稳妥的刷新方式是把旧的QPieSeries从chart上摘下来delete掉再换一个全新的series。不要试图在同一个series上反复clear和append。我的刷新代码大致长这样void PieChartWidget::setData(const QListPieDatum datas) { QChart *chart m_chart; QListQAbstractSeries* oldSeries chart-series(); for (QAbstractSeries *s : oldSeries) { chart-removeSeries(s); delete s; } QPieSeries *series new QPieSeries(); for (const PieDatum d : datas) { QPieSlice *slice series-append(d.displayName, d.value); slice-setLabel(createLabel(d)); slice-setColor(colorOf(d.key)); connect(slice, QPieSlice::clicked, this, [this, d]() { ... }); } series-setHoleSize(0.45); series-setLabelsVisible(true); chart-addSeries(series); m_series series; }解释一下为什么不建议原地clear。clear之后图例里旧项有时不会立刻消失切片指针也会失去引用。如果之前保存了某个切片指针后续再点到它轻则没反应重则直接崩溃。整组替换虽然看起来粗暴但能保证图例、信号、动画缓存都是全新状态稳定性要好得多。如果数据刷新频率很高比如一秒钟好几回也别用整组替换应该改成只更新某个切片的值。但从业务习惯看仪表盘的刷新周期通常按分钟计整组替换在绝大多数场景都够用。3.3 分类太多时的归并策略有些业务方会把A、B、C一路排到Z问你能不能画一个饼。能画但画出来不是给人看的。切片的数量如果超过8个人眼就很难快速提取主要对比关系超过12个边缘小切片基本就是一条细线标签根本放不下。我更推荐在图前做一次归并把低于阈值的所有分类合并成“其他”。这个步骤与其说是在处理QChart不如说是在处理数据的可视化语义。用一段通用函数来描述QListPieDatum mergePieDatas(const QListPieDatum src, qreal minPercent) { qreal total 0; for (const PieDatum d : src) total d.value; QListPieDatum result; qreal mergedValue 0; for (const PieDatum d : src) { if (d.value / total * 100.0 minPercent) { mergedValue d.value; } else { result.append(d); } } if (mergedValue 0) { result.append(PieDatum{QStringLiteral(other), QStringLiteral(其他), mergedValue}); } return result; }这样做的另一个好处是合并后的分类数量稳定配色系统不需要动态扩展。用户也更容易记住图里那几大块代表什么。4. 现场复盘三个让我差点砸键盘的坑4.1 悬空切片指针保存slice地址的代价我刚开始把点击逻辑封装进组件时曾把一串QPieSlice*存在成员变量里。第一次刷新数据后程序直接崩溃。排查链路是这样的先用调试器看崩溃栈发现崩溃在点击槽函数访问slice的brush时再看内存发现这个slice地址已经被反复释放。原因就是我在旧series上保存了slice指针而旧series在刷新时已经被delete了。后来的解决方案是点击时用业务key到当前series-slices()里动态查找切片不再长期持有指针QPieSlice *findSliceByKey(QPieSeries *series, const QString key) { for (QPieSlice *slice : series-slices()) { if (slice-label() key) { return slice; } } return nullptr; }这里有个前提标签里存的必须是稳定的业务key而不是展示名。展示名会变key不会。如果项目一开始没建key只能退而求其次用displayName做查找同时约定展示名不许重复。这是个很痛的经验也是我后来坚持在设计阶段就引入key的重要原因。4.2 中文标签显示成方框编码和字体要分开查中文显示成方框在Qt里是经典问题。它通常有两个根源。第一个是源码字符集和编译器不一致尤其在Windows MSVC环境下源文件是UTF-8编译器却按本地编码解释字符串常量进去就变乱码。第二个是渲染字体本身不支持中文或者存在字体但没被指定使用。我推荐的排查步骤是先去掉所有自定义设置只保留一个纯中文的QLabel看显示是否正常。如果正常说明问题出在Qt或QChart的字体设置而不是编码如果不正常先去检查源文件和编译器的编码参数。然后在main里设置全局字体#include QFont int main(int argc, char *argv[]) { QApplication app(argc, argv); QFont font(QStringLiteral(Microsoft YaHei), 10); app.setFont(font); // ... }如果项目里确实没有微软雅黑就替换成系统自带的中文字体。另外当把字符串通过slice-setLabel()传给图表渲染时要确保它在构造时就已经是正确的QString。用QStringLiteral包裹字符串字面量是最省心的做法。4.3 图例挤压绘图区饼图尺寸和比例一起跑偏图例默认放在图表右侧。窗口本身不大时图例会把绘图区挤成一个窄条饼状图的视觉占比明显变小。这个问题不报错但非常影响观感。我当时排查的顺序是先手动调用legend()-hide()看饼图是否立刻变大确认后再决定图例是挪到底部还是完全隐藏。如果标签里已经写了名称和百分比大多数情况下图例是可以隐藏的因为信息重复了。如果业务方坚持要图例把对齐方式改成Qt::AlignBottom同时把窗口宽度拉大一点。另一个相关问题是饼状图在窗口拉伸后看起来会像椭圆。QChart并不会自动让饼图保持正圆饼图的直径取决于绘图区域的短边。所以当图例把右侧挤窄时饼的直径也会跟着变小。要避免太明显的变形可以在布局里给view设置一个接近正方形的最小尺寸或者把图放到一个更宽的容器里。如果整体布局做不到1:1稍微椭圆化其实在仪表盘里并不显眼不用死磕。5. 把饼状图沉淀成一个可复用组件5.1 我建议的组件接口设计在做了三四个需要饼状图的页面之后我决定把逻辑统一封装成一个PieChartWidget。原因很简单每个页面都写一遍append、setLabel、关联信号、处理hover代码重复率太高而且后续改主题要一处一处找。对外接口尽量不给业务方暴露QChart类型class PieChartWidget : public QWidget { Q_OBJECT public: struct PieDatum { QString key; QString name; qreal value; }; explicit PieChartWidget(QWidget *parent nullptr); void setData(const QListPieDatum datas); void setColors(const QHashQString, QColor colors); void setCenterText(const QString text); signals: void sliceClicked(const QString key); private: QChart *m_chart; QPieSeries *m_series; QLabel *m_centerLabel; };这个接口有几个刻意的选择。第一外部只需要关心数据和颜色不需要知道QPieSeries的细节以后想换成别的图表类型也不影响外围。第二点击信号只回传key不回传QPieSlice避免调用方拿到指针做危险操作。第三中心文本单独设置因为仪表盘场景里那个数值通常不是静态的。5.2 用自动化手段守住图表质量图表看起来偏视觉但同样能自动化。比如维护一个纯数据层的单元测试输入若干PieDatum验证setData之后切片数量是否正确、merge函数是否正确归并、颜色分配是否覆盖所有分类。这些内容不涉及渲染非常好测。视觉回归我则是用截图对比来做。写一个简单的测试函数构造固定的模拟数据生成图表后把view渲染到QImage再与基线图片做像素比对。一旦有人改动样式导致颜色偏移、标签位置变化测试会直接飘红。团队协作时它能拦住不少“我觉得这样更好看”式的小改动。5.3 什么时候该放弃饼状图最后说个看起来有点反高潮的建议饼状图真的不适合承载太多数据。如果你发现自己一直在纠结配色、标签、归并这些问题很多时候不是QChart不好用而是视觉形式本身有局限。占比信息少于6个分类、数值之间差异够明显、用户只需要一个粗略对比这时候饼状图最合适。要精确对比、要展示趋势、要处理几十个分类换成条形图或表格反而更高效。我现在接到需求的第一件事不是打开Qt写代码而是先问清楚给谁看看什么结论。只要结论是占比概览才把饼状图搬出来。这个习惯帮我砍掉了大量无意义的美化工作。做这类图表组件我自己的经验是先用默认配置快速跑通再在一个统一的地方收口主题样式最后用模拟数据把交互和刷新滚一遍再接手真实业务数据。这套流程避开了我在前面写到的绝大多数问题也让我在项目里的饼状图从“能跑”变成了“能上生产环境”。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑