2026-05-22
C#
0

🎯 你是否也遇到过这些困境?

在WPF项目开发中,有一类问题几乎每个开发者都踩过坑——界面展示逻辑与数据结构深度耦合

想象这样一个场景:产品经理要求列表中的"已完成"任务显示绿色勾选图标,"进行中"的显示蓝色进度条,"已逾期"的显示红色警告标识。如果用传统的代码后置(Code-Behind)方式处理,你可能会写出一堆if-else判断,把UI逻辑塞进ItemsControl的事件回调里,最终代码变成一锅粥,维护成本直线上升。

统计表明,在中大型WPF项目中,约35%的Bug来源于UI与数据绑定逻辑的不当处理,而其中相当一部分完全可以通过合理使用DataTemplate来规避。

读完本文,你将掌握:

  • DataTemplate的底层工作机制与正确使用姿势
  • 三种渐进式的数据模板应用方案(从基础到高阶)
  • DataTemplateSelector的实战落地方式
  • 性能优化与常见陷阱规避策略

🔍 问题深度剖析:为什么UI与数据会"打架"?

传统做法的根本缺陷

很多开发者在刚接触WPF时,习惯性地把"数据长什么样"和"数据怎么显示"混在一起处理。比如在ViewModel里直接拼接HTML字符串,或者在ListBoxSelectionChanged事件里手动修改子控件的颜色。这种做法短期看似方便,长期却是一颗定时炸弹。

根本原因在于:数据的"是什么"和"怎么呈现"本应是两个独立的关注点。 WPF的设计哲学从一开始就把这两者分离了——数据是数据,模板是模板,通过绑定系统连接,互不侵入。

常见误解

有开发者认为,DataTemplate只是"给ListBox美化用的",实际上这个理解非常片面。DataTemplate的作用域远不止列表控件,它可以应用于任何ContentControl(如ButtonContentPresenter),以及所有ItemsControl的子项渲染。更进一步,结合DataTemplateSelector,它能根据数据类型或状态动态切换整套UI方案,这才是它真正的威力所在。


💡 核心要点提炼:DataTemplate的底层机制

它究竟做了什么?

DataTemplate本质上是一个可视化树的"蓝图"。当WPF的内容呈现引擎(ContentPresenterItemsPresenter)需要渲染一个数据对象时,它会查找匹配的DataTemplate,然后按照模板定义实例化一棵可视化子树,并将数据对象设置为该子树的DataContext

整个过程简化如下:

数据对象 → ContentPresenter → 查找DataTemplate → 实例化可视化树 → 绑定DataContext

这意味着模板中的所有绑定表达式({Binding PropertyName})都会自动以当前数据对象为上下文解析,无需任何额外的手动赋值。

三种定义方式的适用场景

内联定义(Inline DataTemplate) 适用于仅在单一控件内使用的简单模板,直接写在控件的ItemTemplateContentTemplate属性中,作用域最小,优先级最高。

资源字典定义(Resource Dictionary) 适用于跨控件复用的模板,定义在Window.ResourcesApp.Resources中,通过x:Key引用,是最常见的工程化做法。

隐式数据模板(Implicit DataTemplate) 这是最"魔法"的一种——只设置DataType不设置x:Key,WPF会自动将其应用于所有该类型的数据对象,无需显式引用,非常适合多态数据场景。


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

方案一:基础数据模板——让列表"活"起来

应用场景:产品列表、任务列表等需要自定义每一项展示样式的场景。

以一个任务管理列表为例,每条任务需要展示标题、截止日期和优先级标签。

csharp
using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.ComponentModel; using System.Linq; using System.Text; using System.Threading.Tasks; namespace AppTriger { // ViewModel层:任务数据模型 public class TaskItem { public string Title { get; set; } public DateTime DueDate { get; set; } public PriorityLevel Priority { get; set; } public bool IsCompleted { get; set; } } public enum PriorityLevel { Low, Medium, High } // ViewModel public class TaskListViewModel : INotifyPropertyChanged { public ObservableCollection<TaskItem> Tasks { get; set; } public TaskListViewModel() { Tasks = new ObservableCollection<TaskItem> { new TaskItem { Title = "完成需求评审", DueDate = DateTime.Now.AddDays(2), Priority = PriorityLevel.High }, new TaskItem { Title = "编写单元测试", DueDate = DateTime.Now.AddDays(5), Priority = PriorityLevel.Medium }, new TaskItem { Title = "更新文档", DueDate = DateTime.Now.AddDays(10), Priority = PriorityLevel.Low, IsCompleted = true } }; } public event PropertyChangedEventHandler PropertyChanged; } }
xml
<Window.Resources> <local:PriorityToColorConverter x:Key="PriorityToColorConverter"/> <DataTemplate x:Key="TaskItemTemplate"> <Border Margin="4,2" Padding="12,8" CornerRadius="6" Background="#F8F9FA" BorderThickness="1" BorderBrush="{Binding Priority, Converter={StaticResource PriorityToColorConverter}}"> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition Width="Auto"/> <ColumnDefinition Width="*"/> <ColumnDefinition Width="Auto"/> </Grid.ColumnDefinitions> <!-- 完成状态指示器 --> <Ellipse Grid.Column="0" Width="10" Height="10" Margin="0,0,10,0" Fill="{Binding Priority, Converter={StaticResource PriorityToColorConverter}}"/> <!-- 任务信息 --> <StackPanel Grid.Column="1"> <TextBlock Text="{Binding Title}" FontWeight="SemiBold" FontSize="14" TextDecorations="{Binding IsCompleted, Converter={StaticResource BoolToStrikethroughConverter}}"/> <TextBlock Text="{Binding DueDate, StringFormat='截止:{0:yyyy-MM-dd}'}" FontSize="11" Foreground="#888888" Margin="0,2,0,0"/> </StackPanel> <!-- 优先级标签 --> <Border Grid.Column="2" Padding="6,2" CornerRadius="4" Background="{Binding Priority, Converter={StaticResource PriorityToColorConverter}}"> <TextBlock Text="{Binding Priority}" Foreground="White" FontSize="11"/> </Border> </Grid> </Border> </DataTemplate> </Window.Resources> <!-- 应用模板 --> <ListBox ItemsSource="{Binding Tasks}" ItemTemplate="{StaticResource TaskItemTemplate}" Background="Transparent" BorderThickness="0"/>
xml
<Window x:Class="AppTriger.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local="clr-namespace:AppTriger" mc:Ignorable="d" Title="MainWindow" Height="450" Width="800"> <Window.Resources> <!-- 优先级颜色转换器(见下方代码) --> <local:PriorityToColorConverter x:Key="PriorityToColorConverter"/> <local:BoolToStrikethroughConverter x:Key="BoolToStrikethroughConverter"/> <!-- 核心:任务项数据模板 --> <DataTemplate x:Key="TaskItemTemplate"> <Border Margin="4,2" Padding="12,8" CornerRadius="6" Background="#F8F9FA" BorderThickness="1" BorderBrush="{Binding Priority, Converter={StaticResource PriorityToColorConverter}}"> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition Width="Auto"/> <ColumnDefinition Width="*"/> <ColumnDefinition Width="Auto"/> </Grid.ColumnDefinitions> <!-- 完成状态指示器 --> <Ellipse Grid.Column="0" Width="10" Height="10" Margin="0,0,10,0" Fill="{Binding Priority, Converter={StaticResource PriorityToColorConverter}}"/> <!-- 任务信息 --> <StackPanel Grid.Column="1"> <TextBlock Text="{Binding Title}" FontWeight="SemiBold" FontSize="14" TextDecorations="{Binding IsCompleted, Converter={StaticResource BoolToStrikethroughConverter}}"/> <TextBlock Text="{Binding DueDate, StringFormat='截止:{0:yyyy-MM-dd}'}" FontSize="11" Foreground="#888888" Margin="0,2,0,0"/> </StackPanel> <!-- 优先级标签 --> <Border Grid.Column="2" Padding="6,2" CornerRadius="4" Background="{Binding Priority, Converter={StaticResource PriorityToColorConverter}}"> <TextBlock Text="{Binding Priority}" Foreground="White" FontSize="11"/> </Border> </Grid> </Border> </DataTemplate> </Window.Resources> <StackPanel> <ListBox ItemsSource="{Binding Tasks}" ItemTemplate="{StaticResource TaskItemTemplate}" Background="Transparent" BorderThickness="0"/> </StackPanel> </Window>
csharp
using System; using System.Globalization; using System.Windows; using System.Windows.Data; namespace AppTriger { public class BoolToStrikethroughConverter : IValueConverter { public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { return value is bool isCompleted && isCompleted ? TextDecorations.Strikethrough : null; } public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) => throw new NotImplementedException(); } }

image.png 踩坑预警DataTemplate中的绑定路径是相对于DataContext的,如果在模板内部需要访问外部ViewModel的属性(如命令),需要使用RelativeSourceElementName绑定,直接写{Binding SomeCommand}是找不到的。

2026-05-22
C#
0

🔧 开篇

注塑车间的工程师小李最近遇到了一个让他抓狂的问题。

他写了一段采集模具温度的循环程序,逻辑很清楚:温度超过阈值就停止采集,触发报警。

代码跑起来,报警灯亮了,但采集循环还在继续转——数据一条条往数据库里写,停不下来。

他盯着屏幕看了二十分钟,才发现:循环里压根没有"出口"

这就是今天要解决的问题。学完本节,你的循环代码想停就停、想跳就跳,完全掌控。


📌 上节回顾

「上一节我们学了循环语句,掌握了用 forwhiledo-whileforeach 让代码反复执行的方法。

今天在这个基础上,我们进一步学习如何在循环执行过程中主动控制流程走向——该停的时候停,该跳的时候跳。」


💡 核心知识讲解

跳转语句是什么?

循环语句解决的是"重复执行"的问题。但工厂程序里,不可能每次都等循环自然跑完。

设备报警要立刻停采集,某个产品检测不合格要跳过,当前任务完成要立刻返回结果——这些都需要主动打断或跳出程序的正常流程。

跳转语句(Jump Statement)就是干这个的。C# 提供了四个:breakcontinuereturngoto


break:紧急停机按钮

break 的作用是立刻终止当前循环或 switch 分支,跳到循环体外面继续执行。

用工厂类比:就像车间里的紧急停机按钮。不管生产线转到哪一步,按下去,立刻停。

csharp
// 示例:温度超限,立刻停止采集 for (int i = 0; i < 100; i++) { if (deviceTemp > alarmThreshold) break; // 直接退出整个 for 循环 CollectData(); }

break 只退出最近一层循环。如果你有两层嵌套循环,内层 break 只退出内层,外层还在继续转。这是初学者最常踩的坑,后面避坑部分会专门讲。

2026-05-21
Python
0

CustomTkinter 入门小项目:构建一个简单的设备控制面板(完整示例)


🤔 先聊聊,为什么是 CustomTkinter?

做 Python 桌面开发,很多人第一反应是 Tkinter——毕竟内置、免安装、上手快。但打开一看,那个界面……说实话,像是从 Windows 98 穿越过来的。

用 PyQt?功能强,可学习曲线陡得像悬崖。用 wxPython?文档读起来比说明书还费劲。

CustomTkinter 刚好卡在中间。它基于原生 Tkinter 构建,但把所有控件重新包了一层,支持深色/浅色主题切换,圆角按钮、现代配色开箱即用。对于工控、自动化、内部工具这类场景,完全够用——而且学一下午就能上手。

今天咱们就用它做一个设备控制面板:模拟几台设备的开关控制、状态显示和日志记录。代码完整可运行,Windows 下直接跑。


🛠️ 环境准备

先把依赖装好,一行命令搞定:

bash
pip install customtkinter

Python 版本建议 3.9 及以上。CustomTkinter 不依赖任何第三方 GUI 库,底层还是 Tkinter,所以 Windows 上不需要额外折腾。

验证一下安装:

python
import customtkinter print(customtkinter.__version__)

能打印出版本号就 OK 了。


📐 面板设计思路

在动手写代码之前,先把结构想清楚。这个控制面板要实现:

  • 顶部:标题栏 + 主题切换按钮
  • 中部左侧:设备列表,每台设备有独立的开/关按钮和状态指示
  • 中部右侧:实时日志区域,记录操作历史
  • 底部:全局操作按钮(一键全开、一键全关)

整体布局用 grid 管理,比 pack 更好控制对齐。设备数据用字典维护状态,不搞复杂的类继承——够用就好。


2026-05-20
C#
0

作为一名C#开发者,你是否遭遇过这样的情况:系统运行一段时间后,响应变得奇慢无比?打开性能监视器,发现CPU被某些莫名其妙的操作占用?等会儿...为啥日志记录占了这么大的开销?

最近在优化一个电商平台的订单处理模块时,我发现了一个让人震撼的数据:传统的 LogInformation 调用竟然比高性能日志方法慢了整整9倍!更要命的是,即使日志级别设置为Error,不输出任何日志内容,传统方法依然要消耗几乎相同的时间。

今天咱们就来深入剖析.NET日志系统的性能瓶颈,并掌握一种让日志性能飞跃的神器——LoggerMessage特性。读完这篇文章,你将获得:

  • 彻底理解传统日志方法的性能陷阱
  • 掌握高性能日志的两种实现模式
  • 获得可直接应用的性能优化代码模板
  • 避开实际项目中的常见踩坑点

🎯 传统日志方法的隐形杀手

让我先给你看个典型场景。在处理用户请求时,咱们经常这样写日志:

csharp
int orderCount = 100; DateTime processTime = DateTime.Now; _logger.LogInformation($"处理了 {orderCount} 个订单,时间:{processTime}");

这玩意儿看起来人畜无害,实际上却是性能杀手!问题出在哪儿?

💀 性能陷阱深度剖析

让咱们把这个看似无害的代码拆解一下:

  1. 字符串必须构建 - 不管日志级别如何,$"处理了 {orderCount} 个订单,时间:{processTime}" 这个字符串插值都会被执行
  2. 装箱开销 - orderCount 这个int值要被装箱成object类型
  3. DateTime格式化 - processTime 要转换成字符串表示
  4. 延迟检查 - 日志级别的检查发生在处理的最后阶段

在我测试的项目中,这些看似微不足道的操作累积起来,让单次日志调用耗时达到了0.45毫秒。你可能觉得,这点时间算什么?但在高并发场景下,一个接口调用包含10-20个日志点,累积效应就相当可怕了。

更糟糕的是——即使你把日志级别设为Error,不输出任何信息,这些开销依然存在!

🚀 LoggerMessage特性:性能救世主

Microsoft意识到了这个问题,在.NET 6中引入了 LoggerMessage 特性。这个特性通过编译时代码生成,彻底解决了传统日志的性能瓶颈。

⚡ 核心原理揭秘

LoggerMessage 特性的工作原理其实很巧妙:

  • 编译时生成:编译器自动生成优化的日志方法
  • 提前检查:首先检查日志级别是否启用
  • 延迟构建:只在需要输出时才构建消息字符串
  • 避免装箱:通过泛型避免值类型装箱

💪 实战解决方案:两种高性能模式

🔧 方案一:静态方法模式

这种方式适合工具类静态日志帮助方法

csharp
using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; using System.Text; namespace AppLoggerUp { internal static partial class HighPerformanceLogger { /// <summary> /// 记录订单处理信息 /// </summary> [LoggerMessage( EventId = 1001, Level = LogLevel.Information, Message = "订单处理完成:订单数量 {orderCount},处理时间 {processTime}")] public static partial void LogOrderProcessed( ILogger logger, int orderCount, DateTime processTime); /// <summary> /// 记录用户操作 /// </summary> [LoggerMessage( EventId = 1002, Level = LogLevel.Information, Message = "用户 {userId} 执行了 {action} 操作,结果:{result}")] public static partial void LogUserAction( ILogger logger, string userId, string action, string result); /// <summary> /// 记录异常信息 /// </summary> [LoggerMessage( EventId = 2001, Level = LogLevel.Error, Message = "订单处理发生异常:订单ID {orderId},用户ID {userId}")] public static partial void LogOrderError( ILogger logger, int orderId, string userId, Exception exception); } }

使用方法:

csharp
using Microsoft.Extensions.Logging; namespace AppLoggerUp { public class OrderService { private readonly ILogger<OrderService> _logger; // 通过 DI 注入 ILogger public OrderService(ILogger<OrderService> logger) { _logger = logger; } public async Task ProcessOrdersAsync() { var startTime = DateTime.Now; // 模拟异步批量处理 var processedCount = await ProcessOrderBatchAsync(); // 高性能日志调用 HighPerformanceLogger.LogOrderProcessed(_logger, processedCount, startTime); } private async Task<int> ProcessOrderBatchAsync() { // 模拟异步处理耗时 await Task.Delay(150); // 模拟处理了 18 笔订单 return 18; } } }
c#
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using AppLoggerUp; // 1. 构建 DI 容器 & Logger var services = new ServiceCollection(); services.AddLogging(builder => { builder.AddConsole(); builder.SetMinimumLevel(LogLevel.Debug); // 允许所有级别通过 }); services.AddTransient<OrderService>(); var provider = services.BuildServiceProvider(); // 2. 直接测试 HighPerformanceLogger var loggerFactory = provider.GetRequiredService<ILoggerFactory>(); var rawLogger = loggerFactory.CreateLogger("TestLogger"); Console.WriteLine("=== 直接调用 HighPerformanceLogger ===\n"); // 测试 LogOrderProcessed HighPerformanceLogger.LogOrderProcessed(rawLogger, 42, DateTime.Now); // 测试 LogUserAction HighPerformanceLogger.LogUserAction(rawLogger, "user_001", "CreateOrder", "成功"); // 测试 LogOrderError(带 Exception) try { throw new InvalidOperationException("库存不足,无法完成订单"); } catch (Exception ex) { HighPerformanceLogger.LogOrderError(rawLogger, orderId: 9988, userId: "user_001", exception: ex); } // 3. 通过 OrderService 调用(走完整业务流程) Console.WriteLine("\n=== 通过 OrderService 调用 ===\n"); var orderService = provider.GetRequiredService<OrderService>(); await orderService.ProcessOrdersAsync(); Console.WriteLine("\n=== 测试完成 ===");

image.png

2026-05-20
C#
0

🎯 折线图看趋势,蜡烛图看"过程"

做工业数据分析的时候,折线图是最常见的选择。但折线图有一个先天缺陷:它只能展示某一时刻的单一数值,丢失了这段时间内数据的波动过程

以设备压力监控为例,某台液压泵在某个小时内的压力均值是 4.2MPa,折线图上就是一个点。但这个小时里,压力可能从 3.8MPa 冲到 5.1MPa 又回落到 4.0MPa——这个波动过程完全被均值抹平了。如果设备的安全阈值是 5.0MPa,这次超限在折线图上根本看不出来。

蜡烛图(Candlestick)最初用于金融领域,但它的数据结构——开盘值、收盘值、最高值、最低值——天然契合工业数据的周期性统计需求。把这四个维度换成"周期起始值、周期结束值、周期最大值、周期最小值",一张蜡烛图就能同时展示数值的趋势方向、波动幅度和极值范围。

本文基于 LiveCharts 2(LiveChartsCore.SkiaSharpView.WinForms),从零到一实现:

  • ✅ 基础蜡烛图搭建(CandlesticksSeries
  • ✅ 工业数据映射方案(压力 / 温度 / 电流周期统计)
  • ✅ 实时数据动态追加与告警标注

🔍 问题深度剖析:工业数据可视化的三个常见误区

误区一:用均值代替过程

很多监控系统每分钟采集一次数据,然后把这一分钟内的均值存入数据库,再用折线图展示。均值掩盖了极值,而设备故障恰恰发生在极值处。蜡烛图强制你在数据聚合阶段同时记录 Open/Close/High/Low 四个值,这本身就是一种更完整的数据采集规范。

误区二:时间粒度选择不当

折线图在数据点密集时会变成一团乱麻,稀疏时又看不出规律。蜡烛图通过调整周期(5分钟、1小时、1班次)来控制信息密度,同一份原始数据,不同粒度的蜡烛图能揭示不同层次的规律:5分钟图看操作响应,1小时图看负载变化,班次图看产能趋势。

误区三:忽视"实体"与"影线"的信息量

蜡烛图的矩形实体(Open 到 Close)反映的是主趋势方向,上下影线(High 和 Low 超出实体的部分)反映的是瞬时波动强度。影线特别长说明这个周期内数据极不稳定,即使均值正常也值得警惕。这个信息在折线图里完全丢失了。


💡 核心要点提炼

LiveCharts 2 的蜡烛图使用 CandlesticksSeries<FinancialPoint>,数据类型是 FinancialPoint,构造函数为:

csharp
new FinancialPoint(DateTime date, double high, double open, double close, double low)

注意参数顺序:High 在 Open 之前,这和直觉不太一样,初次使用很容易搞错导致图形显示异常。

颜色规则与金融蜡烛图一致:Close > Open 时显示上涨色(默认绿色),Close < Open 时显示下跌色(默认红色)。在工业场景里,这对应"周期末值高于起始值"和"低于起始值",可以直观反映数据的变化方向。

X 轴类型需要配合 DateTimeAxis,否则时间标签无法正确显示。


🛠️ 方案一:基础蜡烛图——液压泵压力周期分析

场景描述

模拟液压泵每小时的压力数据,每小时采集 3600 个采样点,聚合为一根蜡烛。展示过去 24 小时的压力变化趋势与波动情况。

csharp
using LiveChartsCore; using LiveChartsCore.Defaults; using LiveChartsCore.SkiaSharpView; using LiveChartsCore.SkiaSharpView.Painting; using LiveChartsCore.SkiaSharpView.WinForms; using SkiaSharp; namespace AppLiveChart17 { public partial class Form1 : Form { public Form1() { InitializeComponent(); InitCandlestickChart(); } private void InitCandlestickChart() { Text = "液压泵压力分析 - 蜡烛图(每小时聚合)"; Size = new System.Drawing.Size(1000, 500); var pressureData = GeneratePressureData(hours: 24); var candleSeries = new CandlesticksSeries<FinancialPoint> { Values = pressureData, Name = "液压压力(MPa)", UpFill = new SolidColorPaint(new SKColor(33, 150, 243, 120)), UpStroke = new SolidColorPaint(new SKColor(33, 150, 243)) { StrokeThickness = 1.5f }, DownFill = new SolidColorPaint(new SKColor(255, 152, 0, 120)), DownStroke = new SolidColorPaint(new SKColor(255, 152, 0)) { StrokeThickness = 1.5f } }; var xAxis = new Axis { Name = "时间", LabelsRotation = 30, // ✅ 关键修复:对非法值做保护,避免 ArgumentOutOfRangeException Labeler = value => { // LiveCharts 在计算轴边界时可能传入超范围的浮点值 // 必须在转换前做合法性检查 if (double.IsNaN(value) || double.IsInfinity(value)) return string.Empty; long ticks = (long)value; if (ticks < DateTime.MinValue.Ticks || ticks > DateTime.MaxValue.Ticks) return string.Empty; return new DateTime(ticks).ToString("MM/dd HH:mm"); }, UnitWidth = TimeSpan.FromHours(1).Ticks, MinStep = TimeSpan.FromHours(1).Ticks }; var yAxis = new Axis { Name = "压力(MPa)", MinLimit = 2.0, MaxLimit = 6.5, Labeler = v => $"{v:F1} MPa" }; var chart = new CartesianChart { Dock = DockStyle.Fill, Series = new ISeries[] { candleSeries }, XAxes = new[] { xAxis }, YAxes = new[] { yAxis } }; Controls.Add(chart); } private static List<FinancialPoint> GeneratePressureData(int hours) { var rng = new Random(42); var result = new List<FinancialPoint>(); // 用固定基准时间 var baseTime = new DateTime(2026, 4, 30, 0, 0, 0); for (int h = 0; h < hours; h++) { var timestamp = baseTime.AddHours(h); double baseP = 3.8 + Math.Sin(h * 0.4) * 0.5; double open = baseP + rng.NextDouble() * 0.3 - 0.15; double close = baseP + rng.NextDouble() * 0.3 - 0.15; double high = Math.Max(open, close) + rng.NextDouble() * 0.6; double low = Math.Min(open, close) - rng.NextDouble() * 0.4; if (h >= 14 && h <= 18) { open += 0.8; close += 0.8; high += 1.0; low += 0.5; } // FinancialPoint 参数顺序:date, high, open, close, low result.Add(new FinancialPoint( timestamp, Math.Round(high, 2), Math.Round(open, 2), Math.Round(close, 2), Math.Round(low, 2) )); } return result; } } }

image.png

踩坑预警

FinancialPoint 的参数顺序是 (date, high, open, close, low)High 排在第二位,Open 第三位。如果写成 (date, open, high, close, low) 不会报错,但图形会显示异常——蜡烛实体可能跑到影线外面,看起来像一堆乱线。建议在代码里加注释强调这一点。