Winforms实战项目:运动控制界面原型

项目概述

创建一个完整的运动控制界面原型,模拟单轴运动控制系统的基本功能。这个项目将帮助你掌握 WinForms 的核心概念,为后续的运动控制卡开发打下基础。

项目目标

  • 掌握 WinForms 常用控件的使用

  • 理解事件驱动编程模型

  • 学习窗体布局和界面设计

  • 掌握数据绑定和状态显示

  • 实现多窗体应用程序

  • 练习 GDI+ 绘图(位置显示)

功能需求

1. 主界面功能

  • 轴状态显示区

    • 当前位置显示(Label 或 ProgressBar)

    • 当前速度显示

    • 轴状态指示(就绪/运动中/报警)- 使用不同颜色标识

  • 参数设置区

    • 目标位置输入(TextBox + NumericUpDown)

    • 运行速度设置(TrackBar + TextBox)

    • 加速度设置(TrackBar + TextBox)

  • 手动操作区

    • 正向点动按钮(JOG+)

    • 反向点动按钮(JOG-)

    • 回零按钮(Home)

    • 急停按钮(Stop)- 红色,醒目

  • 菜单栏

    • 文件菜单:打开参数、保存参数、退出

    • 设置菜单:参数配置(打开设置窗体)

    • 帮助菜单:关于

  • 状态栏

    • 显示当前系统状态

    • 显示连接状态(模拟)

2. 设置窗体功能

  • 轴参数配置

    • 脉冲当量

    • 最大速度

    • 最大加速度

    • 软限位(正向/反向)

  • 确定/取消按钮

  • 参数保存到配置文件(JSON 或 XML)

3. 位置显示功能

  • 使用 GDI+ 绘制位置指示器

  • 显示当前位置在行程范围内的位置

  • 实时更新显示

实现步骤

不管是做什么开发项目,最开始做的一定是做好框架,只有做好了条理清晰,分工明确的框架,才能在后续的开发中省去很多不必要的调试和麻烦。

正常开发中一般的框架一般分为数据模型层、业务服务层、静态资源层、通用工具类和主程序入口

分析本项目的需求,一共分为基础界面搭建以及功能实现和完善这两个主要步骤

基础界面搭建

创建项目和主界面框架

先在草稿中构建一个大致的框架,分析需求可得主界面一共有四个主要区域,分别是轴状态显示区、参数设置区、手动操作区、显示区。可用TableLayoutPanel划分主要区域,再在不同的区域中放置Panel容器进行单独的设计

功能实现和完善

主界面设置好后,下一步应该做的是创建数据模型,为什么这一步要先做呢,因为没有数据,后面具体的功能实现将寸步难行(比如条件的判断)。

创建数据模型

在Models文件夹下创建AxisState.cs类,封装轴状态的数据。

cs 复制代码
namespace MotionControl.Models
{
    public class AxisState
    {
        public double CurrentPosition {  get; set; }
        public double TargetPosition {  get; set; }
        public double CurrentSpeed { get; set; }
        public double RunSpeed { get; set; }
        public double Acceleration {  get; set; }
        public double MinPosition {  get; set; }
        public double MaxPosition { get; set; }
        public string AxisStatus { get; set; }
        public AxisState()
        {
            CurrentPosition = 0.0;
            TargetPosition = 0.0;
            CurrentSpeed = 0.0;
            RunSpeed = 100.0;
            Acceleration = 10.0;
            AxisStatus = "就绪";
            MinPosition = 0.0;
            MaxPosition = 1000.0;
        }
}
实现手动操作功能

按下JOG+正向移动,按下JOG-负向移动,松开停止。

设置这个功能不仅仅要考虑移动的方向,还要考虑移动的速度,移动距离的限制

想控制轴体的移动方向,可以定义一个枚举,分别为正向、负向、和停止

在构造器中初始化为停止

cs 复制代码
public JogDirection CurrentJogDirection { get; set; }
public enum JogDirection
{
    None,      // 不运动
    Positive,  // 正向运动
    Negative   // 反向运动
}

设置运动方法

cs 复制代码
 // JOG+:正向点动开始
 public void JogPositiveStart()
 {
     CurrentJogDirection = JogDirection.Positive;
     AxisStatus = "运动中";
 }

 // JOG+:正向点动停止
 public void JogPositiveStop()
 {
     CurrentJogDirection = JogDirection.None;
     CurrentSpeed = 0.0;
     AxisStatus = "就绪";
 }

 // JOG-:反向点动开始
 public void JogNegativeStart()
 {
     CurrentJogDirection = JogDirection.Negative;
     AxisStatus = "运动中";
 }

 // JOG-:反向点动停止
 public void JogNegativeStop()
 {
     CurrentJogDirection = JogDirection.None;
     CurrentSpeed = 0.0;
     AxisStatus = "就绪";
 }

设置对应的事件

cs 复制代码
      // JOG+ 按钮按下
      private void btnJogPositive_MouseDown(object sender, MouseEventArgs e)
      {
          axisState.JogPositiveStart();
      }
      // JOG+ 按钮松开
      private void btnJogPositive_MouseUp(object sender, MouseEventArgs e)
      {
          axisState.JogPositiveStop();
      }

      // JOG- 按钮按下
      private void btnJogNegative_MouseDown(object sender, MouseEventArgs e)
      {
          axisState.JogNegativeStart();
      }
      // JOG- 按钮松开
      private void btnJogNegative_MouseUp(object sender, MouseEventArgs e)
      {
          axisState.JogNegativeStop();
      }

现在运动的方法和对应的事件都完善了,但是数据并没有更新,所有还需要写一个更新方法

在更新方法中,移动速度很好更新赋值,但是移动了多远怎么判断呢?只需用速度×时间,再每一个事件触发的时间重新更新当前位置即可。

首先在AxisState中写下更新的方法

cs 复制代码
     public void UpdatePosition(double deltaTime)
     {
         // 根据当前 JOG 方向更新位置
         if (CurrentJogDirection == JogDirection.Positive)
         {
             // 正向运动
             CurrentSpeed = RunSpeed;
             double positionChange = CurrentSpeed * deltaTime;
             CurrentPosition += positionChange;

             // 到达限位停止
             if (CurrentPosition >= MaxPosition)
             {
                 CurrentPosition = MaxPosition;
                 CurrentSpeed = 0.0;
                 AxisStatus = "就绪";
                 CurrentJogDirection = JogDirection.None;
             }
         }
         else if (CurrentJogDirection == JogDirection.Negative)
         {
             // 反向运动
             CurrentSpeed = -RunSpeed;
             double positionChange = CurrentSpeed * deltaTime;
             CurrentPosition += positionChange;

             // 到达限位停止
             if (CurrentPosition <= MinPosition)
             {
                 CurrentPosition = MinPosition;
                 CurrentSpeed = 0.0;
                 AxisStatus = "就绪";
                 CurrentJogDirection = JogDirection.None;
             }
         }
        }

方法有了,在哪调用呢,当然是主程序中了,在form1中创建对应的触发事件,设置每一毫秒更新一次数据

cs 复制代码
private System.Windows.Forms.Timer timer;//事件时间触发器

 public Form1()
 {
     InitializeComponent();
     axisState = new AxisState();
     timer = new System.Windows.Forms.Timer();//初始化
     timer.Interval = 100;//设置时间间隔
     timer.Tick += UpdateTimer_Tick;订阅更新方法
     timer.Start();触发事件
 }

现在只需要在主程序中创建一个方法调用更新方法就行了

cs 复制代码
private void UpdateTimer_Tick(object sender, EventArgs e)
{
    double deltaTime = 0.1;
    axisState.UpdatePosition(deltaTime);

    // 1. 更新位置显示
    lblPositionValue.Text = axisState.CurrentPosition.ToString("F2") + "mm";

    // 2. 更新速度显示
    lblSpeedValue.Text = axisState.CurrentSpeed.ToString("F2") + "mm/s";

    // 3. 更新状态显示
    lblStatusValue.Text = axisState.AxisStatus;

    // 4. 根据状态改变颜色
    if (axisState.AxisStatus == "运动中")
    {
        lblStatusValue.ForeColor = Color.Red;
    }
    else if (axisState.AxisStatus == "就绪")
    {
        lblStatusValue.ForeColor = Color.Lime;
    }
    else if (axisState.AxisStatus == "报警")
    {
        lblStatusValue.ForeColor = Color.Orange;
    }

    // 5. 更新进度条
    double positionRange = axisState.MaxPosition - axisState.MinPosition;
    double progress = (axisState.CurrentPosition - axisState.MinPosition) / positionRange * 100;
    progressBarPosition.Value = (int)Math.Clamp(progress, 0, 100);

    // 6. 更新状态栏
    statusLabel.Text = "系统状态:" + axisState.AxisStatus;

    panelPositionDisplay.Invalidate();
}

现在只剩回零和急停按钮了,比较好实现,只需要把相关逻辑完善就行了,更新已经写好了

回零和急停

在轴数据类中写方法

cs 复制代码
 // 回零:回到原点
 public void Home()
 {
     CurrentPosition = 0.0;
     CurrentSpeed = 0.0;
     AxisStatus = "就绪";
 }

 // 急停:紧急停止
 public void EmergencyStop()
 {
     CurrentSpeed = 0.0;
     AxisStatus = "就绪";
 }

在form1中创建事件

cs 复制代码
 private void btnHome_Click(object sender, EventArgs e)
 {
     axisState.Home();
 }

 private void btnEmergencyStop_Click(object sender, EventArgs e)
 {
     axisState.EmergencyStop();
 }
实现位置显示
cs 复制代码
  private void panelPositionDisplay_Paint(object sender, PaintEventArgs e)
  {
      Graphics g = e.Graphics;

      // 1. 获取 Panel 的尺寸
      int width = panelPositionDisplay.Width;
      int height = panelPositionDisplay.Height;

      // 2. 计算边距
      int margin = 20;
      int drawWidth = width - 2 * margin;

      // 3. 绘制行程线(水平线)
      int lineY = height / 2;
      g.DrawLine(Pens.Black, margin, lineY, width - margin, lineY);

      // 4. 绘制起点和终点标记
      g.FillEllipse(Brushes.Black, margin - 5, lineY - 5, 10, 10);
      g.FillEllipse(Brushes.Black, width - margin - 5, lineY - 5, 10, 10);

      // 5. 绘制刻度标签
      g.DrawString("0mm", SystemFonts.DefaultFont, Brushes.Black, margin, lineY - 25);
      g.DrawString("1000mm", SystemFonts.DefaultFont, Brushes.Black, width - margin - 40, lineY - 25);

      // 6. 计算当前位置的 X 坐标
      double positionRange = axisState.MaxPosition - axisState.MinPosition;
      double positionRatio = (axisState.CurrentPosition - axisState.MinPosition) / positionRange;
      int currentX = margin + (int)(positionRatio * drawWidth);

      // 7. 绘制位置指示器(三角形)
      Point[] triangle = new Point[]
      {
     new Point(currentX, lineY - 15),      // 顶点
     new Point(currentX - 8, lineY - 5),   // 左下
     new Point(currentX + 8, lineY - 5)    // 右下
      };
      g.FillPolygon(Brushes.Red, triangle);

      // 8. 绘制位置标签
      string positionText = axisState.CurrentPosition.ToString("F2") + "mm";
      g.DrawString(positionText, SystemFonts.DefaultFont, Brushes.Red, currentX - 20, lineY + 10);
  }
实现菜单功能

打开设置中的参数配置会打开一个小窗口,这个小窗口我们应该用模态窗口来实现,其中的数据我们应该在创建一个保存数据的类,实现多窗口之间的数据能够双向传递。

首先设置窗体框架

在models中创建数据类

cs 复制代码
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace MotionControl.Models
{
    public class AxisConfig
    {
        public double PulseEquivalent { get; set; }
        public double MaxSpeed { get; set; }
        public double MaxAcceleration { get; set; }
        public double SoftLimitPositive { get; set; }
        public double SoftLimitNegative { get; set; }
        public AxisConfig()
        {
            PulseEquivalent = 0.001;
            MaxSpeed = 1000.0;
            MaxAcceleration = 1000.0;
            SoftLimitPositive = 1000.0;
            SoftLimitNegative = 0.0;
        }
    }
}

设置菜单栏的点击事件

cs 复制代码
        private void menuItemConfig_Click(object sender, EventArgs e)
        {
            FormSettings formSettings = new FormSettings(axisConfig);
            DialogResult result = formSettings.ShowDialog();
            if (result == DialogResult.OK)
            {
                // 用户点击了确定,使用修改后的配置
                axisConfig = formSettings.Config;

                // 应用配置到 axisState
                axisState.MinPosition = axisConfig.SoftLimitNegative;  // 反向限位
                axisState.MaxPosition = axisConfig.SoftLimitPositive;  // 正向限位
                axisState.RunSpeed = axisConfig.MaxSpeed;              // 最大速度
                axisState.Acceleration = axisConfig.MaxAcceleration;    // 最大加速度

                // 更新 TrackBar 的 Maximum 属性
                trackBarSpeed.Maximum = (int)axisConfig.MaxSpeed;
                trackBarAccel.Maximum = (int)axisConfig.MaxAcceleration;

                // 更新界面显示
                trackBarSpeed.Value = (int)axisConfig.MaxSpeed;
                txtSpeed.Text = axisConfig.MaxSpeed.ToString();

                trackBarAccel.Value = (int)axisConfig.MaxAcceleration;
                txtAccel.Text = axisConfig.MaxAcceleration.ToString();

                // 更新行程范围显示
                lblTravelRangeValue.Text = $"{axisConfig.SoftLimitNegative}-{axisConfig.SoftLimitPositive}mm";

                MessageBox.Show("配置已保存", "提示",
                               MessageBoxButtons.OK, MessageBoxIcon.Information);
            }
            else
            {
                // 用户点击了取消,不使用修改
                MessageBox.Show("配置未保存", "提示",
                               MessageBoxButtons.OK, MessageBoxIcon.Information);
            }
        }

到此为止修改参数这一部分就完成了,只剩最后一个任务,参数保存到配置文件

参数保存到配置文件

我们可以讲数据转换为json数据类型,保存在文件夹中,方便读取和保存文件,保存时只需要把axis config中的数据转换为json形式保存就行了,读取时也只需要把数据读取出来后,让axis state提取axisconfig中的数据就行。

既然想要做到这些事,肯定就需要方法,这就是工具,我们应该将其放到utils中专门写一个转换json的代码

cs 复制代码
using MotionControl.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;

namespace MotionControl.Utils
{
    public static class ConfigHelper
    {
        // 配置文件名常量
        private static readonly string ConfigFileName = "axis_config.json";


        /// <summary>
        /// 保存轴配置到 JSON 文件
        /// </summary>
        /// <param name="config">要保存的配置对象</param>
        public static void SaveConfig(AxisConfig config)  // 定义静态方法,参数是 AxisConfig 类型
        {
            try  
            {
                var options = new JsonSerializerOptions
                {
                    WriteIndented = true,  // 设置为 true,输出的 JSON 会格式化(有缩进和换行)
                    PropertyNamingPolicy = JsonNamingPolicy.CamelCase
                };

                string jsonString = JsonSerializer.Serialize(config, options);

                File.WriteAllText(ConfigFileName, jsonString);
            }
            catch (Exception ex)  // 捕获所有类型的异常,ex 是异常对象
            {
                // 抛出新的异常,并附加友好的错误信息
                // ex.Message 获取原始异常的错误信息
                throw new Exception("保存配置文件失败:" + ex.Message);
            }
        }

        // 加载配置方法
        public static AxisConfig? LoadConfig()// 返回类型是 AxisConfig?,表示可能返回 null
        {
            try
            {
                if (!File.Exists(ConfigFileName))
                {
                    return null;  // 返回 null
                }
                string jsonString = File.ReadAllText(ConfigFileName);

                // 创建 JSON 反序列化选项对象
                var options = new JsonSerializerOptions
                {
                    PropertyNameCaseInsensitive = true  // 设置为 true,属性名不区分大小写
                };

                // 使用 JsonSerializer.Deserialize() 将 JSON 字符串反序列化为 AxisConfig 对象
                // 第一个参数是 JSON 字符串,第二个参数是反序列化选项
                // <AxisConfig> 指定要反序列化成的类型
                AxisConfig? config = JsonSerializer.Deserialize<AxisConfig>(jsonString, options);

                // 返回反序列化得到的配置对象
                return config;
            }
            catch (Exception ex)  // 捕获所有类型的异常
            {
                // 抛出新的异常,并附加友好的错误信息
                throw new Exception("加载配置文件失败:" + ex.Message);
            }
        }
    }
}

方法写好后,form1中添加事件去调用他就行了

cs 复制代码
   private void menuItemOpen_Click(object sender, EventArgs e)
   {
       try  // 开始 try 块,捕获可能的异常
       {
           // 调用 ConfigHelper.LoadConfig() 加载配置
           AxisConfig? loadedConfig = ConfigHelper.LoadConfig();

           // 检查是否加载成功(loadedConfig 不为 null)
           if (loadedConfig != null)
           {
               // 将加载的配置赋值给 axisConfig
               axisConfig = loadedConfig;

               // 应用配置到 axisState
               axisState.MinPosition = axisConfig.SoftLimitNegative;  // 设置最小位置
               axisState.MaxPosition = axisConfig.SoftLimitPositive;  // 设置最大位置
               axisState.RunSpeed = axisConfig.MaxSpeed;              // 设置运行速度
               axisState.Acceleration = axisConfig.MaxAcceleration;    // 设置加速度

               // 更新 TrackBar 的 Maximum 属性(最大值)
               trackBarSpeed.Maximum = (int)axisConfig.MaxSpeed;       // 速度滑块的最大值
               trackBarAccel.Maximum = (int)axisConfig.MaxAcceleration; // 加速度滑块的最大值

               // 更新界面显示
               trackBarSpeed.Value = (int)axisConfig.MaxSpeed;         // 设置速度滑块的值
               txtSpeed.Text = axisConfig.MaxSpeed.ToString();         // 设置速度文本框的值

               trackBarAccel.Value = (int)axisConfig.MaxAcceleration;  // 设置加速度滑块的值
               txtAccel.Text = axisConfig.MaxAcceleration.ToString();  // 设置加速度文本框的值

               // 更新行程范围显示
               lblTravelRangeValue.Text = $"{axisConfig.SoftLimitNegative}-{axisConfig.SoftLimitPositive}mm"
   ;

               // 弹出提示框,告诉用户加载成功
               MessageBox.Show("配置加载成功!", "提示",
                              MessageBoxButtons.OK, MessageBoxIcon.Information);
           }
           else
           {
               // 如果 loadedConfig 是 null,说明文件不存在
               MessageBox.Show("未找到配置文件", "提示",
                              MessageBoxButtons.OK, MessageBoxIcon.Information);
           }
       }
       catch (Exception ex)  // 捕获所有异常
       {
           // 弹出错误提示框,显示错误信息
           MessageBox.Show(ex.Message, "错误",
                          MessageBoxButtons.OK, MessageBoxIcon.Error);
       }
   }

   private void menuItemSave_Click(object sender, EventArgs e)
   {
       try  // 开始 try 块,捕获可能的异常
       {
           // 调用 ConfigHelper.SaveConfig() 保存配置
           ConfigHelper.SaveConfig(axisConfig);

           // 弹出提示框,告诉用户保存成功
           MessageBox.Show("配置保存成功!", "提示",
                          MessageBoxButtons.OK, MessageBoxIcon.Information);
       }
       catch (Exception ex)  // 捕获所有异常
       {
           // 弹出错误提示框,显示错误信息
           MessageBox.Show(ex.Message, "错误",
                          MessageBoxButtons.OK, MessageBoxIcon.Error);
       }
   }

到此位置,本项目圆满完成

相关推荐
MicroTech20252 小时前
微算法科技(NASDAQ :MLGO)量子安全哈希(QSHA),增强量子时代的区块链安全保障
科技·算法·安全
高洁012 小时前
数字孪生与数字样机的技术基础:建模与仿真
python·算法·机器学习·transformer·知识图谱
不忘不弃2 小时前
模拟内存分配器2
算法
被星1砸昏头2 小时前
C++中的享元模式
开发语言·c++·算法
淡忘旧梦2 小时前
词错误率/WER算法讲解
人工智能·笔记·python·深度学习·算法
狐573 小时前
2026-01-21-牛客每日一题-静态区间和(前缀和)
笔记·算法
2401_841495643 小时前
【Python高级编程】单词统计与查找分析工具
数据结构·python·算法·gui·排序·单词统计·查找
源代码•宸3 小时前
Leetcode—3. 无重复字符的最长子串【中等】
经验分享·后端·算法·leetcode·面试·golang·string
范纹杉想快点毕业3 小时前
嵌入式工程师一年制深度进阶学习计划(纯技术深耕版)
linux·运维·服务器·c语言·数据库·算法