编辑
2025-12-13
C#
00

🤔 你是否遇到过这样的问题?

开发WinForms应用时,用户总是抱怨文件选择界面不够友好?保存文件时缺少必要的提醒?多文件选择功能实现起来很复杂?

今天,我们就来彻底解决这些让C#开发者头疼的文件操作问题!通过掌握OpenFileDialogSaveFileDialog这两个强大的组件,让你的应用用户体验瞬间提升一个档次。

🎯 为什么文件操作对话框如此重要?

在Windows应用开发中,文件操作是最常见的需求之一。无论是打开配置文件、导入数据还是导出报告,标准化的文件选择界面不仅能提升用户体验,还能避免路径输入错误等常见问题。

OpenFileDialog和SaveFileDialog的三大优势:

  • ✅ 封装了复杂的Windows API,使用简单
  • ✅ 提供标准化的用户界面,用户上手快
  • ✅ 内置文件过滤、多选等高级功能

🔥 实战技巧一:智能文件过滤,提升用户体验

很多开发者只会设置基础的文件过滤,但合理的过滤设置能大大提升用户体验:

C#
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace AppWinformFileDialog { public class SmartFileFilter { public void OpenWithSmartFilter() { using (OpenFileDialog openDialog = new OpenFileDialog()) { // 关键技巧:设置多层次的文件过滤 openDialog.Filter = "图片文件 (*.jpg;*.png;*.gif)|*.jpg;*.png;*.gif|" + "文档文件 (*.txt;*.doc;*.pdf)|*.txt;*.doc;*.pdf|" + "所有文件 (*.*)|*.*"; // 默认选择第一个过滤器 openDialog.FilterIndex = 1; // 设置友好的标题 openDialog.Title = "选择要处理的图片文件"; // 记住用户的选择目录 openDialog.RestoreDirectory = true; if (openDialog.ShowDialog() == DialogResult.OK) { string selectedFile = openDialog.FileName; MessageBox.Show($"已选择文件:{selectedFile}"); } } } } }

image.png

编辑
2025-12-12
C#
00

在现代移动应用开发中,SkiaSharp 作为跨平台2D图形库,为C#开发者提供了强大的图形绘制能力。其中,复合变换(Composite Transform) 是实现复杂图形效果的核心技术,掌握它能让你的应用界面更加生动和专业。

本文将深入讲解SkiaSharp中的复合变换技术,通过丰富的实例帮助你快速掌握这一重要技能。

什么是复合变换?

复合变换 是指将多个基础变换(平移、旋转、缩放、倾斜等)组合使用,创造出更复杂的视觉效果。在SkiaSharp中,这些变换通过矩阵运算实现,可以让图形元素产生丰富的动态效果。

基础变换类型

  1. 平移变换(Translation) - 移动图形位置
  2. 旋转变换(Rotation) - 围绕某点旋转图形
  3. 缩放变换(Scale) - 改变图形大小
  4. 倾斜变换(Skew) - 使图形产生倾斜效果

核心概念:变换矩阵

SkiaSharp使用 SKMatrix 来表示变换矩阵。理解矩阵的组合规则是掌握复合变换的关键:

C#
// 变换矩阵的基本结构 // [ScaleX SkewX TransX] // [SkewY ScaleY TransY] // [Persp0 Persp1 Persp2]

实战案例详解

案例1:旋转缩放组合效果

这个例子展示如何创建一个既旋转又缩放的矩形:

C#
using SkiaSharp; using SkiaSharp.Views.Desktop; namespace AppTransforms { public partial class Form1 : Form { private SKGLControl skControl; public Form1() { InitializeComponent(); skControl = new SKGLControl(); skControl.Dock = DockStyle.Fill; skControl.PaintSurface += SkControl_PaintSurface; this.Controls.Add(skControl); this.BackColor = System.Drawing.Color.Black; } private void SkControl_PaintSurface(object? sender, SKPaintGLSurfaceEventArgs e) { DrawRotateScaleComposite(e.Surface.Canvas, e.Info); } public void DrawRotateScaleComposite(SKCanvas canvas, SKImageInfo info) { // 清空画布背景 canvas.Clear(SKColors.White); // 创建画笔 using (var paint = new SKPaint()) { paint.Color = SKColors.Blue; paint.Style = SKPaintStyle.Fill; paint.IsAntialias = true; // 开启抗锯齿 // 保存当前画布状态 canvas.Save(); // 移动到画布中心 canvas.Translate(info.Width / 2, info.Height / 2); // 先缩放再旋转(注意顺序很重要) canvas.Scale(1.5f, 0.8f); // X轴放大1.5倍,Y轴缩小到0.8倍 canvas.RotateDegrees(45); // 顺时针旋转45度 // 绘制矩形(以原点为中心) var rect = new SKRect(-100, -30, 100, 30); canvas.DrawRect(rect, paint); // 恢复画布状态 canvas.Restore(); } } } }

image.png

编辑
2025-12-12
C#
00

你是否还在为应用程序的数据存储性能而苦恼?SQLite太重,内存存储又不够持久化,Redis需要额外的服务器资源...传统的数据存储方案似乎总是在性能、资源占用和易用性之间艰难权衡。

今天要为你介绍的Lightning.NET,正是解决这一痛点的完美方案! 它是OpenLDAP LMDB的.NET包装器,提供了内存级别的读取速度零配置的嵌入式部署,以及事务级别的数据安全保障。如果你正在寻找一个轻量级、高性能的本地数据存储解决方案,这篇文章将彻底改变你的技术选型思路。

💡 问题分析:传统键值存储的三大痛点

🔥 性能瓶颈

传统的SQLite在大量读写操作下性能不佳,而内存数据库又面临数据持久化问题。开发者经常需要在性能和数据安全之间做出妥协。

⚡ 部署复杂度

Redis、MongoDB等解决方案虽然性能优秀,但需要独立的服务器进程,增加了部署和运维的复杂度,对于桌面应用或边缘计算场景并不友好。

🎛️ 资源消耗

许多数据库解决方案消耗大量内存和CPU资源,对于资源受限的环境(如IoT设备、移动应用)来说负担过重。

🛠️ Lightning.NET:终极解决方案

🌟 什么是Lightning.NET

Lightning.NET是OpenLDAP LMDB的.NET包装库,LMDB(Lightning Memory-Mapped Database)是一个超快、超小的键值存储引擎。它使用内存映射文件技术,实现了读取性能接近内存数据库,同时保证数据持久化的完美平衡。

核心优势:

  • 📈 极致性能:读取速度接近内存访问
  • 🎯 零配置:嵌入式设计,无需额外服务
  • 🛡️ ACID事务:完整的事务支持
  • 💾 内存高效:使用系统虚拟内存,占用极小

🔆 Nuget 安装库

image.png

编辑
2025-12-11
C#
00

在工业自动化和精密设备控制领域,运动控制系统是核心技术之一。无论是3D打印机、数控机床,还是自动化生产线,都离不开精确的运动控制。作为C#开发者,你是否想过如何用熟悉的技术栈来构建一个专业级的运动控制系统?

今天就带大家从零开始,用C#和WinForms打造一个功能完整的单轴运动控制器。不仅有完整的运动算法实现,还包含直观的可视化界面和实时动画效果。这不仅是一次技术实战,更是将复杂工业控制概念转化为可理解代码的绝佳案例。

🎯 核心痛点分析

工业控制软件的三大挑战

1. 实时性要求高

运动控制需要毫秒级响应,任何延迟都可能影响精度甚至造成设备损坏。

2. 复杂的运动规划

需要实现平滑的加速度曲线,避免机械冲击,同时保证运动精度。

3. 界面与控制逻辑分离

工业软件往往逻辑复杂,界面更新频繁,如何保持代码清晰和系统稳定是关键。

🚩 流程图

image.png

🔧 架构设计:分层解耦的智慧

核心类结构设计

我们采用事件驱动 + 异步编程的架构模式:

C#
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace AppSingleAxisMotionControl { public class MotionAxis { #region 事件定义 public event EventHandler<PositionChangedEventArgs> PositionChanged; public event EventHandler<StatusChangedEventArgs> StatusChanged; public event EventHandler<AlarmEventArgs> AlarmOccurred; #endregion #region 私有字段 private double _currentPosition = 0; private double _currentVelocity = 0; private bool _isConnected = false; private bool _isHomed = false; private bool _isMoving = false; private bool _hasAlarm = false; private double? _targetPosition = null; private double _startPosition = 0; private CancellationTokenSource _moveCancellation; private System.Threading.Timer _simulationTimer; private Random _random = new Random(); #endregion #region 属性 public double CurrentPosition { get => _currentPosition; private set { if (Math.Abs(_currentPosition - value) > 0.001) { _currentPosition = value; PositionChanged?.Invoke(this, new PositionChangedEventArgs(value)); } } } public double CurrentVelocity { get => _currentVelocity; private set => _currentVelocity = value; } public bool IsConnected => _isConnected; public bool IsHomed => _isHomed; public bool IsMoving => _isMoving; public bool HasAlarm => _hasAlarm; public double? TargetPosition => _targetPosition; public double StartPosition => _startPosition; #endregion #region 公共方法 public void Connect(string port) { if (_isConnected) throw new InvalidOperationException("设备已连接"); // 模拟连接过程 Thread.Sleep(500); _isConnected = true; _simulationTimer = new System.Threading.Timer(SimulationUpdate, null, 0, 50); StatusChanged?.Invoke(this, new StatusChangedEventArgs("设备已连接")); } public void Disconnect() { if (!_isConnected) return; _simulationTimer?.Dispose(); _simulationTimer = null; _moveCancellation?.Cancel(); _isConnected = false; _isMoving = false; _currentVelocity = 0; StatusChanged?.Invoke(this, new StatusChangedEventArgs("设备已断开")); } public void Home() { if (!_isConnected) throw new InvalidOperationException("设备未连接"); if (_isMoving) throw new InvalidOperationException("设备正在运动中"); _isMoving = true; _startPosition = _currentPosition; _targetPosition = 0; StatusChanged?.Invoke(this, new StatusChangedEventArgs("开始回零")); // 模拟回零过程 Task.Run(() => { try { SimulateMotion(0, 20, 100, CancellationToken.None); _isHomed = true; StatusChanged?.Invoke(this, new StatusChangedEventArgs("回零完成")); } catch (Exception ex) { AlarmOccurred?.Invoke(this, new AlarmEventArgs($"回零失败: {ex.Message}")); } finally { _isMoving = false; _currentVelocity = 0; _targetPosition = null; } }); } public void MoveAbsolute(double position, double velocity, double acceleration, CancellationToken cancellationToken) { if (!_isConnected) throw new InvalidOperationException("设备未连接"); if (_isMoving) throw new InvalidOperationException("设备正在运动中"); // 参数验证和日志 if (velocity <= 0) velocity = 10; // 默认值 if (acceleration <= 0) acceleration = 100; // 默认值 // 添加调试信息 Console.WriteLine($"MoveAbsolute: 位置={position:F3}, 速度={velocity:F2}, 加速度={acceleration:F1}"); _isMoving = true; _startPosition = _currentPosition; _targetPosition = position; StatusChanged?.Invoke(this, new StatusChangedEventArgs($"开始绝对运动至 {position:F3}mm,速度{velocity:F1}mm/s")); try { SimulateMotion(position, velocity, acceleration, cancellationToken); StatusChanged?.Invoke(this, new StatusChangedEventArgs("绝对运动完成")); } finally { _isMoving = false; _currentVelocity = 0; _targetPosition = null; } } public void MoveRelative(double distance, double velocity, double acceleration, CancellationToken cancellationToken) { if (!_isConnected) throw new InvalidOperationException("设备未连接"); if (_isMoving) throw new InvalidOperationException("设备正在运动中"); double targetPos = _currentPosition + distance; MoveAbsolute(targetPos, velocity, acceleration, cancellationToken); } public void StartJog(double velocity) { if (!_isConnected) throw new InvalidOperationException("设备未连接"); _currentVelocity = velocity; StatusChanged?.Invoke(this, new StatusChangedEventArgs($"开始点动,速度: {velocity:F2}mm/s")); } public void StopJog() { _currentVelocity = 0; StatusChanged?.Invoke(this, new StatusChangedEventArgs("停止点动")); } public void Stop() { _moveCancellation?.Cancel(); _currentVelocity = 0; _isMoving = false; _targetPosition = null; StatusChanged?.Invoke(this, new StatusChangedEventArgs("急停执行")); } public void Reset() { _hasAlarm = false; StatusChanged?.Invoke(this, new StatusChangedEventArgs("报警复位")); } #endregion #region 私有方法 private void SimulateMotion(double targetPosition, double velocity, double acceleration, CancellationToken cancellationToken) { double startPos = _currentPosition; double totalDistance = Math.Abs(targetPosition - startPos); double direction = Math.Sign(targetPosition - startPos); if (totalDistance < 0.001) return; // 添加调试日志 Console.WriteLine($"SimulateMotion: 起始={startPos:F3}, 目标={targetPosition:F3}, 速度={velocity:F2}, 加速度={acceleration:F1}"); DateTime startTime = DateTime.Now; // 运动规划计算 double timeToMaxVelocity = velocity / acceleration; double distanceToMaxVelocity = 0.5 * acceleration * timeToMaxVelocity * timeToMaxVelocity; bool hasConstantVelocityPhase = totalDistance > 2 * distanceToMaxVelocity; double actualMaxVelocity; double totalTime; double accelTime, constTime, decelTime; double accelDist, constDist, decelDist; if (hasConstantVelocityPhase) { // 梯形速度曲线 actualMaxVelocity = velocity; accelTime = decelTime = actualMaxVelocity / acceleration; accelDist = decelDist = 0.5 * acceleration * accelTime * accelTime; constDist = totalDistance - accelDist - decelDist; constTime = constDist / actualMaxVelocity; totalTime = accelTime + constTime + decelTime; Console.WriteLine($"梯形曲线: 最大速度={actualMaxVelocity:F2}, 总时间={totalTime:F2}s"); Console.WriteLine($"加速时间={accelTime:F2}s, 匀速时间={constTime:F2}s, 减速时间={decelTime:F2}s"); } else { // 三角形速度曲线 actualMaxVelocity = Math.Sqrt(totalDistance * acceleration); accelTime = decelTime = actualMaxVelocity / acceleration; constTime = 0; accelDist = decelDist = totalDistance / 2; constDist = 0; totalTime = accelTime + decelTime; Console.WriteLine($"三角形曲线: 最大速度={actualMaxVelocity:F2}, 总时间={totalTime:F2}s"); } // 执行运动仿真 while (Math.Abs(_currentPosition - targetPosition) > 0.001 && !cancellationToken.IsCancellationRequested) { double elapsedTime = (DateTime.Now - startTime).TotalSeconds; double newPosition; double newVelocity; string phase = ""; if (elapsedTime >= totalTime) { newPosition = targetPosition; newVelocity = 0; phase = "完成"; } else if (elapsedTime <= accelTime) { // 加速阶段 newVelocity = acceleration * elapsedTime; newPosition = startPos + direction * (0.5 * acceleration * elapsedTime * elapsedTime); phase = "加速"; } else if (elapsedTime <= accelTime + constTime) { // 匀速阶段 double constElapsed = elapsedTime - accelTime; newVelocity = actualMaxVelocity; newPosition = startPos + direction * (accelDist + actualMaxVelocity * constElapsed); phase = "匀速"; } else { // 减速阶段 double decelElapsed = elapsedTime - accelTime - constTime; newVelocity = actualMaxVelocity - acceleration * decelElapsed; newPosition = startPos + direction * (accelDist + constDist + actualMaxVelocity * decelElapsed - 0.5 * acceleration * decelElapsed * decelElapsed); phase = "减速"; } // 限制位置范围 if (direction > 0) newPosition = Math.Min(newPosition, targetPosition); else newPosition = Math.Max(newPosition, targetPosition); CurrentPosition = newPosition; CurrentVelocity = direction * Math.Abs(newVelocity); // 输出调试信息 if ((int)(elapsedTime * 10) % 1 == 0) { Console.WriteLine($"时间={elapsedTime:F2}s, 阶段={phase}, 位置={newPosition:F3}, 速度={CurrentVelocity:F2}"); } Thread.Sleep(20); } CurrentPosition = targetPosition; CurrentVelocity = 0; Console.WriteLine("运动仿真结束"); } private void SimulationUpdate(object state) { if (!_isConnected) return; // 模拟点动运动 if (!_isMoving && Math.Abs(_currentVelocity) > 0.001) { CurrentPosition += _currentVelocity * 0.05; // 添加微小的位置抖动以模拟真实系统 CurrentPosition += (_random.NextDouble() - 0.5) * 0.001; } // 模拟随机报警 if (_random.NextDouble() < 0.0001) { _hasAlarm = true; AlarmOccurred?.Invoke(this, new AlarmEventArgs("模拟系统报警")); } } #endregion } #region 事件参数类 public class PositionChangedEventArgs : EventArgs { public double Position { get; } public PositionChangedEventArgs(double position) { Position = position; } } public class StatusChangedEventArgs : EventArgs { public string Status { get; } public StatusChangedEventArgs(string status) { Status = status; } } public class AlarmEventArgs : EventArgs { public string AlarmMessage { get; } public AlarmEventArgs(string alarmMessage) { AlarmMessage = alarmMessage; } } #endregion }

🎯 设计亮点:

  • 事件驱动:界面与业务逻辑完全解耦
  • 属性保护:关键状态只能内部修改,外部只读
  • 异步支持:所有运动操作支持取消和超时控制
编辑
2025-12-10
C#
00

在Windows窗体应用程序开发中,自定义控件的设计是展示UI设计能力和提升用户体验的重要方式。本文将详细介绍如何使用SkiaSharp图形库创建一个功能完善、视觉效果精美的时钟控件。我们将从基础实现开始,循序渐进地增加美化元素,最终打造出一个既实用又美观的自定义控件,SkiaSharp比系统自带的drawing要好一少。

SkiaSharp简介

SkiaSharp是Google Skia图形引擎的.NET绑定,提供了强大的2D绘图功能。它具有以下优势:

  • 跨平台支持,可在Windows、macOS和Linux上运行
  • 高性能渲染,适合实时绘制和动画
  • 丰富的绘图API,包括路径、变换、渐变等
  • 与.NET无缝集成

在Windows Forms应用程序中,可以通过SKControl控件轻松集成SkiaSharp的绘图能力。

基础时钟控件实现

控件结构设计

我们的时钟控件将继承自SKControl,主要包含以下核心组件:

  • 表盘和刻度
  • 时针、分针和秒针
  • 中心点装饰
  • 计时器用于更新时间显示

初始化与计时器设置

C#
public ClockControl() { // 设置控件基本属性 BackColor = Color.White; Size = new Size(300, 300); // 初始化计时器,每秒更新一次 _timer = new System.Timers.Timer(1000); _timer.Elapsed += OnTimerElapsed; _timer.AutoReset = true; _timer.Enabled = true; } private void OnTimerElapsed(object sender, ElapsedEventArgs e) { // 在UI线程上刷新控件 if (InvokeRequired) { BeginInvoke(new Action(() => Invalidate())); } else { Invalidate(); } }

这段代码创建了一个300x300像素的时钟控件,并设置一个每秒触发一次的计时器,用于更新控件显示。通过Invalidate()方法触发重绘,确保时钟指针根据当前时间实时更新。

绘制表盘和刻度

表盘是时钟的基础部分,包括外圈、刻度线和数字标记:

C#
private void DrawClockFace(SKCanvas canvas, float centerX, float centerY, float radius) { // 绘制外圆 using var paint = new SKPaint { Style = SKPaintStyle.Stroke, Color = SKColors.Black, StrokeWidth = 2, IsAntialias = true }; canvas.DrawCircle(centerX, centerY, radius, paint); // 绘制刻度线和数字 for (int i = 0; i < 60; i++) { float angle = i * 6; // 每分钟6度 bool isHourMark = i % 5 == 0; // 根据是否小时刻度设置不同长度和粗细 float innerRadius = isHourMark ? radius - 15 : radius - 5; float strokeWidth = isHourMark ? 3 : 1; // 绘制刻度线... // 绘制小时数字... } }

这段代码使用SkiaSharp的绘图API绘制表盘和刻度。我们使用三角函数计算每个刻度的位置,并区分小时刻度(更粗、更长)和分钟刻度。