数据可视化这件事,说难不难,说简单也真不简单。
做过报表系统的开发者大概都有类似的经历:产品经理扔过来一张 Excel,说"能不能做成好看的饼图,让老板一眼看明白"。于是翻遍了 WinForms 自带的 Chart 控件,捣鼓半天,出来的东西……怎么说,就是那种 2003 年 PPT 风格,颜色发灰,没有动画,悬停没有提示,客户一脸嫌弃。
LiveCharts 2 的出现解决了这个痛点。它基于 SkiaSharp 渲染,支持 60FPS 动画,内置 Tooltip、Legend,而且 API 设计非常现代化,MVVM 友好。更关键的是,它在 WinForms 里同样能跑得很顺。
读完这篇文章,你将掌握:
这三步下来,一个能直接交付给客户的数据大屏组件基本就有了。
开发环境:.NET 6 / .NET Framework 4.7.2+,Visual Studio 2022,Windows 10/11。
通过 NuGet 包管理器安装核心依赖:
bashInstall-Package LiveChartsCore.SkiaSharpView.WinForms
如果搜索不到,记得在 NuGet 管理器里勾选"包含预发行版",LiveCharts 2 的部分版本仍处于 RC 阶段。
安装完成后,项目会自动引入 LiveChartsCore 和 SkiaSharp 相关依赖。在需要使用图表的窗体代码文件顶部,加入以下命名空间:
csharpusing LiveChartsCore;
using LiveChartsCore.SkiaSharpView;
using LiveChartsCore.SkiaSharpView.Painting;
using LiveChartsCore.SkiaSharpView.WinForms;
using SkiaSharp;
先从最简单的场景说起。假设需要展示一个季度内各产品线的销售占比,数据是静态的,先把图画出来再说。
在 Visual Studio 的工具箱里,找到 PieChart 控件(安装 NuGet 包后会自动出现),直接拖到 Form 上,调整好大小。如果工具箱没有显示,右键工具箱 → "选择项" → 手动浏览 DLL 添加即可。
在 Form1_Load 事件中写入以下代码:
csharpusing 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);
}
}
}

就这些,运行之后你会看到一个带入场动画、有图例、有数据标签的现代风格饼图。和 WinForms 原生 Chart 控件比起来,视觉效果差距相当明显。
DataLabelsFormatter 里用到了 point.StackedValue,这个属性在数据系列未完成布局计算前可能为 null,所以加了 ! 非空断言。如果项目开了严格的可空检查(<Nullable>enable</Nullable>),建议改成 point.StackedValue?.Share ?? 0 的写法更安全。
饼图和环形图在 LiveCharts 2 里共用同一个 PieChart 控件,区别只在于 PieSeries 的 InnerRadius 属性。把这个值设置为大于 0 的数,饼图中间就被"掏空"了,变成了环形图。
这种图在展示"完成率"、"占比对比"时特别好用,视觉上更简洁,中间还能放一个核心数字,信息密度更高。
csharpusing 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;
}
}
}

InnerRadius = 80 这个值是相对于图表渲染尺寸的像素值,根据控件大小适当调整。通常控件宽度在 400px 左右时,60~100 是比较合适的范围。
静态图表只是第一步,实际项目里更常见的场景是:数据来自数据库或接口,需要定时刷新,或者用户操作后图表要跟着变。LiveCharts 2 对动态更新的支持非常友好,核心在于使用 ObservableCollection 或 ObservableValue,数据变了,图表自动重绘,不需要手动刷新控件。
下面是一个完整的动态刷新示例,模拟每 2 秒更新一次各分类的数据占比:
csharpusing 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);
}
}
}

核心机制解释:ObservableValue 内部实现了变更通知,当 Value 属性被赋新值时,LiveCharts 2 会捕获到这个变更事件,自动触发图表的重绘与动画过渡。整个过程不需要调用任何 Refresh() 或 Invalidate() 方法,这正是 LiveCharts 2 相比旧版本在架构上的一个明显进步。
定时器的回调在 UI 线程执行(System.Windows.Forms.Timer 的 Tick 事件天然在 UI 线程),所以直接修改 ObservableValue.Value 是安全的。但如果你用的是 System.Timers.Timer 或 Task.Run 中的后台线程来更新数据,就需要通过 this.Invoke() 切回 UI 线程,否则会遇到跨线程访问异常。
做到这一步,图表已经能用了。但如果想让它更"高级",还有几个常用的样式调整点值得了解。
调整起始角度,让饼图从顶部开始,视觉上更符合大多数人的阅读习惯:
csharppieChart1.InitialRotation = -90; // -90 度 = 从 12 点方向开始
限制最大角度,可以做出"半圆仪表盘"效果:
csharppieChart1.MaxAngle = 180; // 只渲染半圆
pieChart1.InitialRotation = -180;
关闭动画(某些场景下大量数据刷新时可能需要):
csharppieChart1.EasingFunction = null; // 禁用动画
不过官方文档里有一句话值得记住:禁用动画并不能显著提升性能。LiveCharts 2 的性能瓶颈通常不在动画上,如果遇到卡顿,应该优先检查数据量和绑定逻辑,而不是第一反应就把动画关掉。
自定义每个扇区的颜色,通过 Fill 属性传入 SolidColorPaint:
csharpnew 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% |
| 动态环形图 | 4 | 1秒/次 | ~2~4% |
| 动态环形图 | 8 | 500ms/次 | ~6~8% |
对于绝大多数业务报表场景,这个性能开销完全可以接受。
饼图是比例的语言,环形图是空间的艺术。 选对图表类型,数据自己会说话。
ObservableValue是动态图表的灵魂。 数据驱动视图,比手动刷新优雅十倍。
InnerRadius只是一个属性,却是饼图与环形图之间的全部距离。 LiveCharts 2 的设计哲学就是这样,简单到极致。
掌握了饼图与环形图之后,LiveCharts 2 的学习路径可以这样继续:
DateTimePoint 处理时间轴PieChart 扩展,适合 KPI 指标展示GeoMap 控件,适合地域分布数据官方文档与示例库:livecharts.dev
从一个"2003 年风格"的原生 Chart 控件,到动画流畅、样式现代、支持动态更新的 LiveCharts 2,这条路其实并不长。最核心的三个概念——PieSeries、InnerRadius、ObservableValue——搞清楚了,饼图和环形图的 80% 场景基本都能覆盖。
剩下的 20%,是样式的打磨、交互的细化,以及和具体业务数据的对接。这些靠的不是文档,靠的是在项目里真正跑一遍。
欢迎在评论区分享你在实际项目中使用 LiveCharts 2 遇到的问题,或者你觉得哪种图表类型最难处理——说不定下一篇就写它。
#C#开发 #WinForms #LiveCharts2 #数据可视化 #性能优化
本文作者:技术老小子
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!