编辑
2026-04-27
C#
00
编辑
2026-04-27
Python
00

🎬 你真的需要一个自己写的录屏工具吗?

先说结论:需要,而且非常值得。

市面上的录屏软件要么臃肿、要么收费、要么在某些企业内网环境下根本装不上。作为 Python 开发者,我们手里有 Tkinter、有 OpenCV、有 threading——完全可以在一个下午的时间里,从零撸出一个轻量、可控、可二次开发的屏幕录制工具。

我在给内部团队做技术分享录制时,就踩过这个坑:OBS 太重,ShareX 在某台老机器上崩溃,最后索性自己写。写完之后发现,不过 300 行代码,性能却出乎意料地稳。帧率稳在 25fps,CPU 占用不超过 15%。这篇文章,就把这套思路完整拆给你看。


🧱 技术选型:为什么是这套组合?

核心依赖只有三个:

  • Tkinter:Python 内置 GUI 库,零安装成本,跨平台
  • Pillow(PIL):截图能力,ImageGrab.grab() 在 Windows 下性能相当可观
  • OpenCV(cv2):视频编码写入,VideoWriter 支持多种编解码器

有人会问,为什么不用 pyautogui 截图?原因很简单——pyautogui.screenshot() 底层也是调 PIL,但多了一层封装,速度反而更慢。直接用 ImageGrab 是最短路径。

另外,帧率控制这块,咱们用 threading.Event 配合时间戳对齐,而不是简单粗暴地 time.sleep()。这个细节差别很大,后面会详细讲。


🔧 环境准备

bash
pip install pillow opencv-python numpy

Tkinter 是 Python 标准库的一部分,Windows 下安装 Python 时默认勾选,一般不需要额外安装。如果你用的是精简版 Python 环境,执行 import tkinter 报错的话,重装一遍 Python 并勾选 tcl/tk 组件即可。


🏗️ 整体架构设计

在动手写代码之前,先把架构想清楚。这个录制器分三层:

┌─────────────────────────────────┐ │ Tkinter GUI 层 │ ← 用户交互、状态展示 ├─────────────────────────────────┤ │ 录制控制层 │ ← 线程调度、帧率控制 ├─────────────────────────────────┤ │ 底层采集 & 编码层 │ ← 截图、帧写入 └─────────────────────────────────┘

GUI 层和录制逻辑必须跑在不同线程上。这不是可选项,是必须的——录制是 CPU 密集型操作,如果塞在主线程里,界面会直接卡死,按钮点不动,体验极差。


编辑
2026-04-27
C#
00

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

做过报表系统的开发者大概都有类似的经历:产品经理扔过来一张 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 的写法更安全。

编辑
2026-04-27
C#
00

🎯 开篇:当设备振动数据"狂飙",你的仪表盘还撑得住吗?

前不久在某风电场的状态监测系统项目中,遇到了个让人头疼的问题:3台风机,每台16个振动传感器,采样频率2kHz,也就是每秒钟有近10万个数据点涌入系统。原来用WPF Chart控件做的监控界面,跑了不到10分钟就开始卡顿,CPU直接飙到85%,客户现场工程师看着一帧一帧跳动的波形图,直接问:"这是实时监控还是慢动作回放?"

最终切换到ScottPlot 5.x后,同样的数据量下,界面刷新延迟从800ms降到35ms以内,CPU占用稳定在18%,48路振动信号同时流畅显示。这背后的性能提升不只是换个图表库这么简单,更多的是对多轴数据处理、内存管理和渲染优化的深入理解。

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

  • ScottPlot在多轴振动数据显示中的高效应用方案
  • 3种渐进式的数据管理架构(从快速入门到生产级)
  • 大数据量下的性能优化核心技巧(含真实测试数据对比)
  • 工业现场踩坑经验与完整的代码实现模板

💡 问题深度剖析:多轴振动监测的三大技术难点

🔥 难点一:数据吞吐量与渲染性能的冲突

振动分析不同于常规的温度、压力监控,它的数据密度要高出几个数量级。一台典型的旋转设备可能需要监测:

  • 轴承振动:X、Y、Z三轴加速度,采样率1-2kHz
  • 转子位移:径向、轴向位移,采样率500Hz
  • 转速信号:键相位标记,每转一个脉冲

这意味着单台设备每秒产生5000+数据点,多台设备并发时数据流量呈指数增长。传统Chart控件的"来一个画一个"模式在这种场景下完全崩盘。

我在某石化装置的压缩机监测系统中实测过,6轴振动数据+转速信号同时显示时:

方案刷新延迟CPU占用内存增长速率
WPF Chart1200ms72%120MB/小时
LiveCharts680ms58%85MB/小时
ScottPlot 5.x35ms18%稳定

⚡ 难点二:多轴数据的时间同步与坐标系管理

振动分析中,不同轴向的数据必须严格时间对齐才有分析价值。比如轴承故障诊断时,需要对比X、Y轴的相位关系来判断不平衡类型。如果各轴数据的时间戳有哪怕几毫秒的偏差,分析结果都会失真。

更复杂的是坐标系设置:

csharp
// ❌ 错误做法:各轴使用独立的坐标系 foreach(var axis in axes) { axis.Plot.Axes.AutoScale(); // 每个轴独立缩放,失去对比意义 } // ✅ 正确做法:统一坐标系管理 var globalTimeRange = GetGlobalTimeRange(); var globalAmplitudeRange = GetGlobalAmplitudeRange(); foreach(var axis in axes) { axis.Plot.Axes.SetLimits(globalTimeRange.Min, globalTimeRange.Max, globalAmplitudeRange.Min, globalAmplitudeRange.Max); }

🎯 难点三:实时频谱分析与时域数据的联动显示

振动分析往往需要时域波形和频域频谱同时显示。当时域数据更新时,频谱也要实时计算并刷新。这涉及到FFT计算、数据缓冲、多图表联动等复杂逻辑。

在某齿轮箱监测项目中,我们需要同时显示:

  • 时域振动波形(6个通道)
  • 实时频谱图(6个通道)
  • 总值趋势图(RMS、峰值、峰峰值)
  • 转速曲线

如果处理不当,界面很容易因为计算复杂度过高而卡死。


🛠️ 核心要点提炼:ScottPlot多轴显示的底层逻辑

⚙️ ScottPlot 5.x的多Plot架构

ScottPlot 5.x支持在单个WpfPlot控件中管理多个子图表,这为多轴显示提供了天然优势:

csharp
// 核心架构:一个容器控件管理多个子图表 WpfPlot mainPlot = new WpfPlot(); var subplot1 = mainPlot.Plot.Add.Subplot(0.0, 1.0, 0.7, 1.0); // 上半部分 var subplot2 = mainPlot.Plot.Add.Subplot(0.0, 1.0, 0.3, 0.7); // 中间部分 var subplot3 = mainPlot.Plot.Add.Subplot(0.0, 1.0, 0.0, 0.3); // 下半部分

这种设计的优势是所有子图共享时间轴,天然解决了时间同步问题。

🚀 高性能数据管理策略

针对振动数据的特点,ScottPlot提供了几种优化的数据容器:

  1. SignalXY: 适合不等间隔采样数据
  2. Signal: 适合等间隔采样数据(性能最优)
  3. DataStreamer: 适合实时流数据(内置循环缓冲)

对于振动监测,推荐使用DataStreamer:

csharp
// DataStreamer自动管理数据窗口,无内存泄漏风险 var streamer = myPlot.Plot.Add.DataStreamer(capacity: 2000); streamer.Color = Colors.Blue; streamer.LineWidth = 1.5f;

📊 内存优化的黄金法则

多轴显示的性能瓶颈往往在内存管理,关键原则:

  1. 预分配固定大小缓冲区:避免动态扩容导致的GC压力
  2. 循环覆写:新数据覆盖最旧数据,保持内存用量恒定
  3. 批量刷新:多个轴的数据更新后统一调用一次Refresh()
  4. 坐标轴固定:避免频繁的AutoScale计算

🚀 解决方案设计:三种渐进式实现方案

📦 方案一:快速入门版 - 3轴振动基础显示

适用场景:单台设备、低频采样(<100Hz)、快速验证需求

🛠️ 完整代码实现

csharp
using ScottPlot; using ScottPlot.WPF; using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Channels; using System.Threading.Tasks; using System.Windows; using System.Windows.Threading; namespace AppScottPlot10 { /// <summary> /// Interaction logic for MainWindow.xaml /// </summary> // 振动数据包:包含时间戳,确保多设备同步 public record VibrationPacket(DateTime Timestamp, string DeviceId, string Channel, double Value); public partial class MainWindow : Window { private readonly Channel<VibrationPacket> _dataChannel; private readonly Dictionary<string, Queue<(double time, double value)>> _deviceBuffers; private readonly Dictionary<string, ScottPlot.Plottables.SignalXY> _signalPlots; private readonly PeriodicTimer _refreshTimer; private CancellationTokenSource _cts; private DateTime _startTime; private const double DISPLAY_TIME_WINDOW = 30.0; // 显示最近30秒 private const int MAX_POINTS_PER_DEVICE = 3000; // 多设备多通道配置 private readonly (string DeviceId, string Channel, string Color)[] _channels = { ("风机1号", "轴承X", "#E74C3C"), ("风机1号", "轴承Y", "#FF6B68"), ("风机1号", "轴承Z", "#C0392B"), ("风机2号", "轴承X", "#3498DB"), ("风机2号", "轴承Y", "#5DADE2"), ("风机2号", "轴承Z", "#2980B9"), }; public MainWindow() { InitializeComponent(); // 首先初始化CancellationTokenSource _cts = new CancellationTokenSource(); // 创建高性能数据通道 _dataChannel = Channel.CreateBounded<VibrationPacket>(new BoundedChannelOptions(10000) { FullMode = BoundedChannelFullMode.DropOldest, SingleWriter = false, SingleReader = true }); _deviceBuffers = new Dictionary<string, Queue<(double, double)>>(); _signalPlots = new Dictionary<string, ScottPlot.Plottables.SignalXY>(); InitializeAdvancedCharts(); // 高频刷新定时器 _refreshTimer = new PeriodicTimer(TimeSpan.FromMilliseconds(40)); // 25Hz刷新 // 启动异步处理 StartAsyncDataProcessing(); Task.Run(RefreshLoop); } private void InitializeAdvancedCharts() { var plt = VibrationPlot.Plot; // 专业工业主题 plt.Font.Set("Microsoft YaHei"); plt.FigureBackground.Color = Color.FromHex("#1E1E1E"); plt.DataBackground.Color = Color.FromHex("#2D2D30"); // 高对比度网格 plt.Grid.MajorLineColor = Colors.Gray.WithAlpha(100); plt.Grid.MajorLineWidth = 1; plt.Grid.MinorLineColor = Colors.Gray.WithAlpha(40); plt.Grid.MinorLineWidth = 0.5f; // 为每个通道创建SignalXY(支持自定义时间轴) foreach (var (deviceId, channel, color) in _channels) { string key = $"{deviceId}_{channel}"; // 初始化缓冲区 _deviceBuffers[key] = new Queue<(double, double)>(); // 创建SignalXY对象 var plot = plt.Add.SignalXY( new double[] { 0 }, new double[] { 0 } ); plot.Color = Color.FromHex(color); plot.LineWidth = 1.2f; plot.LegendText = $"{deviceId}-{channel}"; plot.MarkerSize = 0; _signalPlots[key] = plot; } // 坐标轴配置 plt.Title("多设备振动联合监控系统", size: 18); plt.XLabel("时间 (秒)"); plt.YLabel("振动幅值 (m/s²)"); plt.Legend.IsVisible = true; plt.Legend.Alignment = Alignment.UpperRight; // 添加报警线 var alarmLine = plt.Add.HorizontalLine(8.0); alarmLine.Color = Colors.Red; alarmLine.LinePattern = LinePattern.Dashed; alarmLine.LineWidth = 2; alarmLine.LegendText = "报警阈值"; VibrationPlot.Refresh(); } private void StartAsyncDataProcessing() { // 模拟多设备异步数据采集 Task.Run(async () => { var random = new Random(); _startTime = DateTime.Now; while (!_cts.Token.IsCancellationRequested) { foreach (var (deviceId, channel, _) in _channels) { // 模拟不同设备的采样时间差异 var timestamp = DateTime.Now; // 生成特征频率振动信号 double time = (timestamp - _startTime).TotalSeconds; double baseFreq = deviceId.Contains("1号") ? 25 : 30; // 不同设备不同转频 double harmonics = 2 * Math.Sin(2 * Math.PI * baseFreq * 2 * time); // 2倍频 double noise = (random.NextDouble() - 0.5) * 1.5; double value = 5 * Math.Sin(2 * Math.PI * baseFreq * time) + harmonics + noise; // 发送到数据通道 var packet = new VibrationPacket(timestamp, deviceId, channel, value); await _dataChannel.Writer.WriteAsync(packet, _cts.Token); // 模拟采样间隔 await Task.Delay(random.Next(5, 15), _cts.Token); } } }, _cts.Token); } private async Task RefreshLoop() { while (!_cts.Token.IsCancellationRequested) { try { // 批量处理数据包 var processedCount = 0; var hasData = false; while (_dataChannel.Reader.TryRead(out var packet) && processedCount < 100) { string key = $"{packet.DeviceId}_{packet.Channel}"; double relativeTime = (packet.Timestamp - _startTime).TotalSeconds; // 更新缓冲区 var buffer = _deviceBuffers[key]; buffer.Enqueue((relativeTime, packet.Value)); // 清理过期数据 while (buffer.Count > 0 && relativeTime - buffer.Peek().time > DISPLAY_TIME_WINDOW) { buffer.Dequeue(); } // 限制数据点数量 while (buffer.Count > MAX_POINTS_PER_DEVICE) { buffer.Dequeue(); } processedCount++; hasData = true; } // 如果处理了数据,更新图表 if (hasData) { await UpdateChartsAsync(); } // 等待下一个刷新周期 await _refreshTimer.WaitForNextTickAsync(_cts.Token); } catch (OperationCanceledException) { break; } catch (Exception ex) { // 记录异常但继续运行 System.Diagnostics.Debug.WriteLine($"RefreshLoop error: {ex.Message}"); } } } private async Task UpdateChartsAsync() { await Application.Current.Dispatcher.InvokeAsync(() => { foreach (var (key, buffer) in _deviceBuffers) { if (buffer.Count == 0) continue; var timeArray = buffer.Select(p => p.time).ToArray(); var valueArray = buffer.Select(p => p.value).ToArray(); var signal = _signalPlots[key]; VibrationPlot.Plot.Remove(signal); var newSignal = VibrationPlot.Plot.Add.SignalXY(timeArray, valueArray); newSignal.Color = signal.Color; newSignal.LineWidth = signal.LineWidth; newSignal.LegendText = signal.LegendText; newSignal.MarkerSize = signal.MarkerSize; _signalPlots[key] = newSignal; } // 自动调整时间轴范围 double currentTime = (DateTime.Now - _startTime).TotalSeconds; VibrationPlot.Plot.Axes.SetLimitsX(currentTime - DISPLAY_TIME_WINDOW, currentTime); VibrationPlot.Refresh(); }); } protected override void OnClosed(EventArgs e) { _cts?.Cancel(); _refreshTimer?.Dispose(); base.OnClosed(e); } } }

image.png

⚠️ 踩坑预警

  1. Signal数组引用问题:绝对不能重新创建数组,只能修改数组元素
  2. 刷新时机控制:多个轴的数据更新完后统一调用一次Refresh()
  3. 字体设置必须:不设置中文字体会显示为方框
编辑
2026-04-27
C#
00

用 LiveCharts 2 在 WinForms 里画出第一张柱状图,核心只需三步:NuGet 安装 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 里画出你的第一张柱状图。读完之后你能拿到:一份可以直接复制运行的完整代码、三种由浅入深的实现方式、以及几个我踩过的坑的避雷指南。


🔍 为什么是 LiveCharts 2,而不是别的?

在正式动手之前,我想花一点篇幅说清楚选型这件事。因为我见过太多同学上来就 Ctrl+C、Ctrl+V,跑起来一出问题就懵了,根源就是没搞懂自己用的是什么。

WinForms 图表库现在市面上主流的有这么几个:

图表库渲染方式.NET 8 支持授权上手难度
MSChart(老牌)GDI+需手动引用包免费
LiveCharts 2SkiaSharp原生支持MIT
ScottPlotGDI+/Skia原生支持MIT
商业控件(如 DevExpress)GDI+/DirectX支持付费中高

LiveCharts 2 最大的优势是跨框架一致性——你在 WinForms 里写的配置代码,挪到 WPF 项目里几乎不用改。这对做多端桌面应用的团队来说太香了。另外它的动画效果是原生内置的,柱子从零开始"长"出来的那种丝滑感,用 MSChart 想做得手撸计时器,LVC 里就是一个属性的事儿。

当然它也不是完美的。我在实际使用中发现它的内存占用比 MSChart 稍高(大概高 20~30%),如果你的场景是嵌入式工控机内存只有 2G,可能还得权衡一下。


🧱 环境准备:搭好地基再盖楼

先把前置条件列清楚,省得大家半路卡壳。

测试环境说明:

  • 操作系统:Windows 11 23H2
  • IDE:Visual Studio 2022(17.9 及以上)
  • .NET SDK:.NET 8.0
  • LiveCharts 2 版本:2.0.0-rc5.4(截至撰文时的稳定预览版)

注意:LiveCharts 2 目前仍处于 rc 阶段,NuGet 上搜索时必须勾选"包括预发行版本",否则你会搜不到包。这是 99% 新手第一次踩的坑。

新建一个 WinForms 项目,目标框架选 .NET 8.0,然后打开 NuGet 包管理器,安装:

LiveChartsCore.SkiaSharpView.WinForms

这一个包会自动把 LiveChartsCoreSkiaSharp、以及 WinForms 适配层全部带进来,不用你一个个装。


🎯 第一版:最小可运行示例(Hello BarChart)

咱们先追求"能跑起来",再谈"跑得好看"。新建一个 Form,拖一个 CartesianChart 控件到窗体上(工具箱里找不到的话,先编译一次项目,控件就会自动出现)。

然后在 Form1.cs 里写下这段代码:

csharp
using 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 } }; } } }

image.png

按 F5 运行,你会看到一张带柔和动画的柱状图,五根柱子依次"弹"出来。到这一步,你就已经完成了第一张 LiveCharts 2 柱状图

这段代码有几个关键点值得拎出来说

  • ColumnSeries<T> 里的 T 是数据类型,doubleint、甚至自定义对象都行
  • Series 属性接受的是 ISeries[] 数组,意味着你可以同时画多组柱子做对比
  • XAxesYAxes 也是数组,理论上你能做双 Y 轴图表