编辑
2026-04-27
C#
00

目录

🔧 环境准备:先把轮子装上
🥧 方案一:基础饼图,5 分钟跑起来
🖼️ 第一步:拖入控件
💻 第二步:绑定数据
⚠️ 踩坑预警
🍩 方案二:环形图(Doughnut),一个属性的距离
🚀 方案三:动态数据更新,让图表"活"起来
⚠️ 踩坑预警
🎨 进阶:自定义样式,让图表更专业
📊 性能参考
💡 三句话技术洞察
🧭 学习路线延伸
🏁 写在最后

数据可视化这件事,说难不难,说简单也真不简单。

做过报表系统的开发者大概都有类似的经历:产品经理扔过来一张 Excel,说"能不能做成好看的饼图,让老板一眼看明白"。于是翻遍了 WinForms 自带的 Chart 控件,捣鼓半天,出来的东西……怎么说,就是那种 2003 年 PPT 风格,颜色发灰,没有动画,悬停没有提示,客户一脸嫌弃。

LiveCharts 2 的出现解决了这个痛点。它基于 SkiaSharp 渲染,支持 60FPS 动画,内置 Tooltip、Legend,而且 API 设计非常现代化,MVVM 友好。更关键的是,它在 WinForms 里同样能跑得很顺。

读完这篇文章,你将掌握:

  1. 如何在 WinForms 项目中快速集成 LiveCharts 2;
  2. 从零搭建一个标准饼图(PieChart);
  3. 进阶实现环形图(Doughnut),并加入动态数据更新与自定义样式。

这三步下来,一个能直接交付给客户的数据大屏组件基本就有了。


🔧 环境准备:先把轮子装上

开发环境:.NET 6 / .NET Framework 4.7.2+,Visual Studio 2022,Windows 10/11

通过 NuGet 包管理器安装核心依赖:

bash
Install-Package LiveChartsCore.SkiaSharpView.WinForms

如果搜索不到,记得在 NuGet 管理器里勾选"包含预发行版",LiveCharts 2 的部分版本仍处于 RC 阶段。

安装完成后,项目会自动引入 LiveChartsCoreSkiaSharp 相关依赖。在需要使用图表的窗体代码文件顶部,加入以下命名空间:

csharp
using LiveChartsCore; using LiveChartsCore.SkiaSharpView; using LiveChartsCore.SkiaSharpView.Painting; using LiveChartsCore.SkiaSharpView.WinForms; using SkiaSharp;

🥧 方案一:基础饼图,5 分钟跑起来

先从最简单的场景说起。假设需要展示一个季度内各产品线的销售占比,数据是静态的,先把图画出来再说。

🖼️ 第一步:拖入控件

在 Visual Studio 的工具箱里,找到 PieChart 控件(安装 NuGet 包后会自动出现),直接拖到 Form 上,调整好大小。如果工具箱没有显示,右键工具箱 → "选择项" → 手动浏览 DLL 添加即可。

💻 第二步:绑定数据

Form1_Load 事件中写入以下代码:

csharp
using LiveChartsCore; using LiveChartsCore.SkiaSharpView; using LiveChartsCore.SkiaSharpView.Painting; using SkiaSharp; namespace AppLiveChart05 { public partial class Form1 : Form { public Form1() { InitializeComponent(); this.Load += Form1_Load; } private void Form1_Load(object sender, EventArgs e) { // 定义各产品线的销售占比数据 pieChart1.Series = new ISeries[] { new PieSeries<double> { Name = "产品A", Values = new double[] { 42 }, // 设置数据标签显示 DataLabelsPaint = new SolidColorPaint(SKColors.White), DataLabelsSize = 14, DataLabelsPosition = LiveChartsCore.Measure.PolarLabelsPosition.Middle, DataLabelsFormatter = point => $"{point.Model:N0} ({point.StackedValue!.Share:P1})" }, new PieSeries<double> { Name = "产品B", Values = new double[] { 28 }, DataLabelsPaint = new SolidColorPaint(SKColors.White), DataLabelsSize = 14, DataLabelsPosition = LiveChartsCore.Measure.PolarLabelsPosition.Middle, DataLabelsFormatter = point => $"{point.Model:N0} ({point.StackedValue!.Share:P1})" }, new PieSeries<double> { Name = "产品C", Values = new double[] { 18 }, DataLabelsPaint = new SolidColorPaint(SKColors.White), DataLabelsSize = 14, DataLabelsPosition = LiveChartsCore.Measure.PolarLabelsPosition.Middle, DataLabelsFormatter = point => $"{point.Model:N0} ({point.StackedValue!.Share:P1})" }, new PieSeries<double> { Name = "其他", Values = new double[] { 12 }, DataLabelsPaint = new SolidColorPaint(SKColors.White), DataLabelsSize = 14, DataLabelsPosition = LiveChartsCore.Measure.PolarLabelsPosition.Middle, DataLabelsFormatter = point => $"{point.Model:N0} ({point.StackedValue!.Share:P1})" } }; // 显示图例(放在右侧) pieChart1.LegendPosition = LiveChartsCore.Measure.LegendPosition.Right; // 设置动画速度(毫秒) pieChart1.AnimationsSpeed = TimeSpan.FromMilliseconds(800); } } }

image.png

就这些,运行之后你会看到一个带入场动画、有图例、有数据标签的现代风格饼图。和 WinForms 原生 Chart 控件比起来,视觉效果差距相当明显。

⚠️ 踩坑预警

DataLabelsFormatter 里用到了 point.StackedValue,这个属性在数据系列未完成布局计算前可能为 null,所以加了 ! 非空断言。如果项目开了严格的可空检查(<Nullable>enable</Nullable>),建议改成 point.StackedValue?.Share ?? 0 的写法更安全。


🍩 方案二:环形图(Doughnut),一个属性的距离

饼图和环形图在 LiveCharts 2 里共用同一个 PieChart 控件,区别只在于 PieSeriesInnerRadius 属性。把这个值设置为大于 0 的数,饼图中间就被"掏空"了,变成了环形图。

这种图在展示"完成率"、"占比对比"时特别好用,视觉上更简洁,中间还能放一个核心数字,信息密度更高。

csharp
using LiveChartsCore; using LiveChartsCore.SkiaSharpView; using LiveChartsCore.SkiaSharpView.Painting; using SkiaSharp; using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Text; using System.Windows.Forms; namespace AppLiveChart05 { public partial class Form2 : Form { public Form2() { InitializeComponent(); LoadDonutChart(); } private void LoadDonutChart() { var series = new ISeries[] { new PieSeries<double> { Name = "已完成", Values = new double[] { 73 }, InnerRadius = 80, // 关键:设置内圆半径,形成环形 DataLabelsPaint = new SolidColorPaint(SKColors.White), DataLabelsSize = 13, DataLabelsPosition = LiveChartsCore.Measure.PolarLabelsPosition.Middle, DataLabelsFormatter = point => $"{point.StackedValue!.Share:P0}", Fill = new SolidColorPaint(new SKColor(76, 175, 80)) // 绿色 }, new PieSeries<double> { Name = "未完成", Values = new double[] { 27 }, InnerRadius = 80, DataLabelsPaint = new SolidColorPaint(SKColors.White), DataLabelsSize = 13, DataLabelsPosition = LiveChartsCore.Measure.PolarLabelsPosition.Middle, DataLabelsFormatter = point => $"{point.StackedValue!.Share:P0}", Fill = new SolidColorPaint(new SKColor(244, 67, 54)) // 红色 } }; pieChart1.Series = series; pieChart1.LegendPosition = LiveChartsCore.Measure.LegendPosition.Bottom; // 从 -90 度开始,让图从顶部起始,视觉上更自然 pieChart1.InitialRotation = -90; } } }

image.png

InnerRadius = 80 这个值是相对于图表渲染尺寸的像素值,根据控件大小适当调整。通常控件宽度在 400px 左右时,60~100 是比较合适的范围。


🚀 方案三:动态数据更新,让图表"活"起来

静态图表只是第一步,实际项目里更常见的场景是:数据来自数据库或接口,需要定时刷新,或者用户操作后图表要跟着变。LiveCharts 2 对动态更新的支持非常友好,核心在于使用 ObservableCollectionObservableValue,数据变了,图表自动重绘,不需要手动刷新控件。

下面是一个完整的动态刷新示例,模拟每 2 秒更新一次各分类的数据占比:

csharp
using LiveChartsCore; using LiveChartsCore.Defaults; using LiveChartsCore.SkiaSharpView; using LiveChartsCore.SkiaSharpView.Painting; using SkiaSharp; using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Text; using System.Windows.Forms; namespace AppLiveChart05 { public partial class Form3 : Form { // 使用 ObservableValue 包装数据,修改后图表自动响应 private ObservableValue _valueA = new ObservableValue(42); private ObservableValue _valueB = new ObservableValue(28); private ObservableValue _valueC = new ObservableValue(18); private ObservableValue _valueD = new ObservableValue(12); private System.Windows.Forms.Timer _refreshTimer; private Random _rng = new Random(); public Form3() { InitializeComponent(); InitChart(); StartTimer(); } private void InitChart() { pieChart1.Series = new ISeries[] { new PieSeries<ObservableValue> { Name = "华东区", Values = new ObservableValue[] { _valueA }, InnerRadius = 70, DataLabelsPaint = new SolidColorPaint(SKColors.White), DataLabelsSize = 12, DataLabelsPosition = LiveChartsCore.Measure.PolarLabelsPosition.Middle, DataLabelsFormatter = p => $"华东 {p.StackedValue!.Share:P1}" }, new PieSeries<ObservableValue> { Name = "华南区", Values = new ObservableValue[] { _valueB }, InnerRadius = 70, DataLabelsPaint = new SolidColorPaint(SKColors.White), DataLabelsSize = 12, DataLabelsPosition = LiveChartsCore.Measure.PolarLabelsPosition.Middle, DataLabelsFormatter = p => $"华南 {p.StackedValue!.Share:P1}" }, new PieSeries<ObservableValue> { Name = "华北区", Values = new ObservableValue[] { _valueC }, InnerRadius = 70, DataLabelsPaint = new SolidColorPaint(SKColors.White), DataLabelsSize = 12, DataLabelsPosition = LiveChartsCore.Measure.PolarLabelsPosition.Middle, DataLabelsFormatter = p => $"华北 {p.StackedValue!.Share:P1}" }, new PieSeries<ObservableValue> { Name = "其他", Values = new ObservableValue[] { _valueD }, InnerRadius = 70, DataLabelsPaint = new SolidColorPaint(SKColors.White), DataLabelsSize = 12, DataLabelsPosition = LiveChartsCore.Measure.PolarLabelsPosition.Middle, DataLabelsFormatter = p => $"其他 {p.StackedValue!.Share:P1}" } }; pieChart1.LegendPosition = LiveChartsCore.Measure.LegendPosition.Right; pieChart1.InitialRotation = -90; pieChart1.AnimationsSpeed = TimeSpan.FromMilliseconds(600); } private void StartTimer() { _refreshTimer = new System.Windows.Forms.Timer(); _refreshTimer.Interval = 2000; // 每 2 秒刷新一次 _refreshTimer.Tick += (s, e) => RefreshData(); _refreshTimer.Start(); } private void RefreshData() { // 模拟从接口或数据库取回新数据 // 直接修改 ObservableValue.Value,图表自动带动画更新,无需其他操作 _valueA.Value = _rng.Next(20, 60); _valueB.Value = _rng.Next(10, 40); _valueC.Value = _rng.Next(10, 35); _valueD.Value = _rng.Next(5, 20); } protected override void OnFormClosed(FormClosedEventArgs e) { _refreshTimer?.Stop(); _refreshTimer?.Dispose(); base.OnFormClosed(e); } } }

image.png

核心机制解释ObservableValue 内部实现了变更通知,当 Value 属性被赋新值时,LiveCharts 2 会捕获到这个变更事件,自动触发图表的重绘与动画过渡。整个过程不需要调用任何 Refresh()Invalidate() 方法,这正是 LiveCharts 2 相比旧版本在架构上的一个明显进步。

⚠️ 踩坑预警

定时器的回调在 UI 线程执行(System.Windows.Forms.TimerTick 事件天然在 UI 线程),所以直接修改 ObservableValue.Value 是安全的。但如果你用的是 System.Timers.TimerTask.Run 中的后台线程来更新数据,就需要通过 this.Invoke() 切回 UI 线程,否则会遇到跨线程访问异常。


🎨 进阶:自定义样式,让图表更专业

做到这一步,图表已经能用了。但如果想让它更"高级",还有几个常用的样式调整点值得了解。

调整起始角度,让饼图从顶部开始,视觉上更符合大多数人的阅读习惯:

csharp
pieChart1.InitialRotation = -90; // -90 度 = 从 12 点方向开始

限制最大角度,可以做出"半圆仪表盘"效果:

csharp
pieChart1.MaxAngle = 180; // 只渲染半圆 pieChart1.InitialRotation = -180;

关闭动画(某些场景下大量数据刷新时可能需要):

csharp
pieChart1.EasingFunction = null; // 禁用动画

不过官方文档里有一句话值得记住:禁用动画并不能显著提升性能。LiveCharts 2 的性能瓶颈通常不在动画上,如果遇到卡顿,应该优先检查数据量和绑定逻辑,而不是第一反应就把动画关掉。

自定义每个扇区的颜色,通过 Fill 属性传入 SolidColorPaint

csharp
new PieSeries<double> { Name = "自定义色块", Values = new double[] { 30 }, Fill = new SolidColorPaint(new SKColor(33, 150, 243)), // Material Blue InnerRadius = 60 }

SKColor 接受 RGB 三个字节参数,可以直接把设计稿里的颜色值转换过来,颜色控制粒度非常细。


📊 性能参考

以下数据在测试环境(Windows 11, .NET 6, i7-12700H, 16GB RAM)下实测:

场景数据系列数刷新频率CPU 占用
静态饼图6<1%
动态环形图41秒/次~2~4%
动态环形图8500ms/次~6~8%

对于绝大多数业务报表场景,这个性能开销完全可以接受。


💡 三句话技术洞察

饼图是比例的语言,环形图是空间的艺术。 选对图表类型,数据自己会说话。

ObservableValue 是动态图表的灵魂。 数据驱动视图,比手动刷新优雅十倍。

InnerRadius 只是一个属性,却是饼图与环形图之间的全部距离。 LiveCharts 2 的设计哲学就是这样,简单到极致。


🧭 学习路线延伸

掌握了饼图与环形图之后,LiveCharts 2 的学习路径可以这样继续:

  • 折线图 / 面积图 → 适合时序数据展示,结合 DateTimePoint 处理时间轴
  • 柱状图 / 堆叠柱状图 → 多维度对比场景的首选
  • 仪表盘(Gauge) → 基于 PieChart 扩展,适合 KPI 指标展示
  • 地图与热力图 → LiveCharts 2 的 GeoMap 控件,适合地域分布数据

官方文档与示例库:livecharts.dev


🏁 写在最后

从一个"2003 年风格"的原生 Chart 控件,到动画流畅、样式现代、支持动态更新的 LiveCharts 2,这条路其实并不长。最核心的三个概念——PieSeriesInnerRadiusObservableValue——搞清楚了,饼图和环形图的 80% 场景基本都能覆盖。

剩下的 20%,是样式的打磨、交互的细化,以及和具体业务数据的对接。这些靠的不是文档,靠的是在项目里真正跑一遍。

欢迎在评论区分享你在实际项目中使用 LiveCharts 2 遇到的问题,或者你觉得哪种图表类型最难处理——说不定下一篇就写它。


#C#开发 #WinForms #LiveCharts2 #数据可视化 #性能优化

本文作者:技术老小子

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!