LiveChartsCore.SkiaSharpView.WinForms、拖一个 CartesianChart 控件、给 Series 赋一个 ColumnSeries<T> 就能跑起来,全程不用写一行 GDI+ 绘图代码。咱们做 C# 桌面开发的兄弟,多多少少都有过这么一段回忆:领导拍着桌子说"这个月报表得加个图表",然后你打开工具箱找 Chart 控件——嗯,.NET Framework 时代还有个 System.Windows.Forms.DataVisualization.Charting,到了 .NET 6/7/8 直接就没了。去网上一搜,要么是十几年前的老控件配色土到掉渣,要么是商业库动辄几千刀一个授权,要么就是官方文档写得像天书,照着抄都跑不起来。
我自己在去年做一个工业数据采集系统的时候也踩过这坑,老项目用的是 MSChart,迁移到 .NET 8 之后直接报错,最后选型选了 LiveCharts 2(也就是社区里常说的 LVC)。这东西基于 SkiaSharp 渲染,速度快、样式漂亮、API 也算干净,关键是免费开源、跨框架(WinForms / WPF / MAUI / Avalonia 都能用)。
这篇文章咱们不整那些花里胡哨的理论,就聚焦一件事:从零开始,在 WinForms 里画出你的第一张柱状图。读完之后你能拿到:一份可以直接复制运行的完整代码、三种由浅入深的实现方式、以及几个我踩过的坑的避雷指南。
在正式动手之前,我想花一点篇幅说清楚选型这件事。因为我见过太多同学上来就 Ctrl+C、Ctrl+V,跑起来一出问题就懵了,根源就是没搞懂自己用的是什么。
WinForms 图表库现在市面上主流的有这么几个:
| 图表库 | 渲染方式 | .NET 8 支持 | 授权 | 上手难度 |
|---|---|---|---|---|
| MSChart(老牌) | GDI+ | 需手动引用包 | 免费 | 低 |
| LiveCharts 2 | SkiaSharp | 原生支持 | MIT | 中 |
| ScottPlot | GDI+/Skia | 原生支持 | MIT | 低 |
| 商业控件(如 DevExpress) | GDI+/DirectX | 支持 | 付费 | 中高 |
LiveCharts 2 最大的优势是跨框架一致性——你在 WinForms 里写的配置代码,挪到 WPF 项目里几乎不用改。这对做多端桌面应用的团队来说太香了。另外它的动画效果是原生内置的,柱子从零开始"长"出来的那种丝滑感,用 MSChart 想做得手撸计时器,LVC 里就是一个属性的事儿。
当然它也不是完美的。我在实际使用中发现它的内存占用比 MSChart 稍高(大概高 20~30%),如果你的场景是嵌入式工控机内存只有 2G,可能还得权衡一下。
先把前置条件列清楚,省得大家半路卡壳。
测试环境说明:
注意:LiveCharts 2 目前仍处于 rc 阶段,NuGet 上搜索时必须勾选"包括预发行版本",否则你会搜不到包。这是 99% 新手第一次踩的坑。
新建一个 WinForms 项目,目标框架选 .NET 8.0,然后打开 NuGet 包管理器,安装:
LiveChartsCore.SkiaSharpView.WinForms
这一个包会自动把 LiveChartsCore、SkiaSharp、以及 WinForms 适配层全部带进来,不用你一个个装。
咱们先追求"能跑起来",再谈"跑得好看"。新建一个 Form,拖一个 CartesianChart 控件到窗体上(工具箱里找不到的话,先编译一次项目,控件就会自动出现)。
然后在 Form1.cs 里写下这段代码:
csharpusing LiveChartsCore;
using LiveChartsCore.SkiaSharpView;
namespace AppLiveChart04
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
InitChart();
}
private void InitChart()
{
// 1. 准备数据:各城市 Q1 销售额(单位:万元)
var values = new double[] { 128, 256, 189, 342, 215 };
// 2. 构造柱状图系列
cartesianChart1.Series = new ISeries[]
{
new ColumnSeries<double>
{
Values = values,
Name = "销售额"
}
};
// 3. 配置 X 轴标签
cartesianChart1.XAxes = new[]
{
new Axis
{
Labels = new[] { "北京", "上海", "广州", "深圳", "杭州" },
LabelsRotation = 0
}
};
// 4. 配置 Y 轴
cartesianChart1.YAxes = new[]
{
new Axis
{
Name = "金额(万元)",
MinLimit = 0
}
};
}
}
}

按 F5 运行,你会看到一张带柔和动画的柱状图,五根柱子依次"弹"出来。到这一步,你就已经完成了第一张 LiveCharts 2 柱状图。
这段代码有几个关键点值得拎出来说:
ColumnSeries<T> 里的 T 是数据类型,double、int、甚至自定义对象都行Series 属性接受的是 ISeries[] 数组,意味着你可以同时画多组柱子做对比XAxes 和 YAxes 也是数组,理论上你能做双 Y 轴图表光画一组柱子远远不够用。实际项目里,我们经常要做同比、环比、不同维度对比。这一版咱们把"Q1 和 Q2 销售对比"做出来,并且把硬编码的数组换成可绑定的 ViewModel,方便后续和数据库对接。
csharpusing LiveChartsCore;
using LiveChartsCore.SkiaSharpView;
using LiveChartsCore.SkiaSharpView.Painting;
using SkiaSharp;
namespace FirstBarChartDemo
{
// 定义一个简单的数据模型
public class SalesRecord
{
public string City { get; set; } = string.Empty;
public double Q1 { get; set; }
public double Q2 { get; set; }
}
public partial class Form2 : Form
{
private readonly List<SalesRecord> _records = new()
{
new SalesRecord { City = "北京", Q1 = 128, Q2 = 156 },
new SalesRecord { City = "上海", Q1 = 256, Q2 = 289 },
new SalesRecord { City = "广州", Q1 = 189, Q2 = 210 },
new SalesRecord { City = "深圳", Q1 = 342, Q2 = 378 },
new SalesRecord { City = "杭州", Q1 = 215, Q2 = 248 }
};
public Form2()
{
InitializeComponent();
BuildChart();
}
private void BuildChart()
{
// 通过 Mapping 让 LiveCharts 知道怎么从对象里取值
cartesianChart1.Series = new ISeries[]
{
new ColumnSeries<SalesRecord>
{
Name = "Q1",
Values = _records,
// 关键:告诉图表用对象的哪个属性作为 Y 值
Mapping = (record, index) => new(index, record.Q1),
Fill = new SolidColorPaint(SKColors.SteelBlue),
DataLabelsPaint = new SolidColorPaint(SKColors.Black),
DataLabelsPosition = LiveChartsCore.Measure.DataLabelsPosition.Top
},
new ColumnSeries<SalesRecord>
{
Name = "Q2",
Values = _records,
Mapping = (record, index) => new(index, record.Q2),
Fill = new SolidColorPaint(SKColors.DarkOrange),
DataLabelsPaint = new SolidColorPaint(SKColors.Black),
DataLabelsPosition = LiveChartsCore.Measure.DataLabelsPosition.Top
}
};
cartesianChart1.XAxes = new[]
{
new Axis
{
Labels = _records.Select(r => r.City).ToArray(),
LabelsRotation = 0,
TextSize = 14
}
};
cartesianChart1.YAxes = new[]
{
new Axis
{
Name = "销售额(万元)",
NameTextSize = 14,
MinLimit = 0,
Labeler = value => value.ToString("N0")
}
};
// 图例位置
cartesianChart1.LegendPosition = LiveChartsCore.Measure.LegendPosition.Top;
}
}
}

这一版相比第一版有几个实质性升级:
SalesRecord 类承载业务数据,接近真实项目结构ColumnSeries 并排显示,对比关系一目了然Fill 指定颜色,不再依赖默认主题DataLabelsPaint 让每根柱子顶部显示数值Labeler 用于把 Y 轴刻度格式化成千分位我在实际项目中发现,Mapping 这个属性是很多人卡住的地方。官方文档一笔带过,但它的本质是"告诉图表库怎么从你的对象里把 X、Y 坐标抠出来"。记住这个心智模型就不会错。
前面两版都是静态数据。但真实项目里,柱状图经常需要实时刷新——比如监控大屏、实时订单统计、设备产量看板。这一版咱们做一个"每秒刷新一次"的动态柱状图。
csharpusing LiveChartsCore;
using LiveChartsCore.SkiaSharpView;
using System.Collections.ObjectModel;
namespace FirstBarChartDemo
{
public partial class Form3 : Form
{
// 用 ObservableCollection 让图表自动感知数据变化
private readonly ObservableCollection<double> _liveValues = new();
private readonly System.Windows.Forms.Timer _timer = new();
private readonly Random _random = new();
private const int MaxPoints = 20;
public Form3()
{
InitializeComponent();
InitData();
BuildChart();
StartTimer();
}
private void InitData()
{
for (int i = 0; i < MaxPoints; i++)
{
_liveValues.Add(_random.Next(50, 300));
}
}
private void BuildChart()
{
cartesianChart1.Series = new ISeries[]
{
new ColumnSeries<double>
{
Values = _liveValues,
Name = "实时产量"
}
};
cartesianChart1.XAxes = new[]
{
new Axis
{
Name = "时间片",
MinLimit = 0,
MaxLimit = MaxPoints
}
};
// 关键优化:关闭动画可以显著降低高频刷新的 CPU 占用
cartesianChart1.AnimationsSpeed = TimeSpan.FromMilliseconds(300);
}
private void StartTimer()
{
_timer.Interval = 1000;
_timer.Tick += (s, e) =>
{
// 移除最老的一个,追加最新的一个,形成滚动效果
_liveValues.RemoveAt(0);
_liveValues.Add(_random.Next(50, 300));
};
_timer.Start();
}
protected override void OnFormClosed(FormClosedEventArgs e)
{
_timer.Stop();
_timer.Dispose();
base.OnFormClosed(e);
}
}
}

这一版有几个生产环境级别的考量:
1. ObservableCollection 的威力
LiveCharts 2 会自动订阅 ObservableCollection 的 CollectionChanged 事件。你只要增删元素,图表就会自己刷新,不用手动调用任何重绘方法。这个机制比 WPF 的 DataBinding 还要省心。
2. 高频刷新的性能陷阱
我之前把 Timer 间隔设成 100ms,结果 CPU 飙到 15%。后来排查发现,默认动画时长是 800ms,当刷新频率高于动画时长时,新动画会打断旧动画,造成大量无效计算。
性能对比数据(测试环境:i5-12400、16G 内存、20 个数据点):
| 刷新间隔 | 动画时长 | CPU 占用 | 视觉体验 |
|---|---|---|---|
| 1000ms | 800ms | ~2% | 流畅 |
| 500ms | 800ms | ~7% | 有卡顿 |
| 100ms | 800ms | ~15% | 明显卡顿 |
| 100ms | 0ms(关闭动画) | ~3% | 硬切,但不卡 |
结论:刷新频率 > 动画时长时,果断关闭动画(AnimationsSpeed = TimeSpan.Zero)。
3. 资源释放别忘了
Timer 必须在 OnFormClosed 里显式 Dispose,否则窗体关了,定时器还在后台空跑,早晚内存泄漏给你看。
坑1:安装包时没勾预发行版本
前面提过一次,这里再强调:LiveCharts 2 还是 rc 状态,默认搜不到。
坑2:控件找不到
安装完包之后,工具箱里不会立刻出现 CartesianChart,必须先编译一次项目。
坑3:中文字体显示成方块
SkiaSharp 默认字体不支持中文。解决办法是给 Axis 配置 LabelsPaint:
csharpnew Axis
{
Labels = new[] { "北京", "上海" },
LabelsPaint = new SolidColorPaint(SKColors.Black)
{
SKTypeface = SKTypeface.FromFamilyName("Microsoft YaHei")
}
}
坑4:多个 Form 都用图表时首次加载慢
LiveCharts 2 初始化 SkiaSharp 渲染上下文有一次性开销,大概 200~400ms。解决思路是应用启动时预热——在 Splash 界面加载阶段就 new 一个 CartesianChart 实例并丢弃,后续窗体打开就秒开了。
Mapping 是 LiveCharts 2 里最容易被忽略、但最能体现其设计哲学的机制如果你想把 LiveCharts 2 玩得更溜,建议按这个顺序深入:
官方仓库地址:LiveCharts2 on GitHub,里面有非常全的 samples 工程,几乎每种图表都有 WinForms 版本的示例,强烈建议 clone 下来对照学习。
本文讨论话题:
欢迎在评论区聊聊你的实践经验,也欢迎把这篇文章分享给正在做桌面端可视化的同事。
回到开头说的三步走:装包、拖控件、配 Series。LiveCharts 2 把柱状图这件事做到了"五分钟入门、一小时进阶、一天能上生产"的程度。
本文从最小示例讲到多系列对比,再到实时动态刷新,覆盖了柱状图最常见的三类场景。代码都经过 .NET 8 + LiveCharts 2.0.0-rc5.4 实际验证,复制到你的项目里改改数据源就能用。
如果你刚好在做 .NET 桌面端的数据可视化需求,希望这篇能帮你少走一些弯路。接下来如果有机会,咱们可以继续聊聊折线图的实时高频刷新(万级数据点),那才是真正考验图表库性能的硬仗。
相关标签:C#开发 WinForms LiveCharts2 数据可视化 性能优化
本文作者:技术老小子
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!