C#上位机开发

发布于:2024-06-11 ⋅ 阅读:(44) ⋅ 点赞:(0)


一、上位机简介

  在单片机项目开发中,上位机也是一个很重要的部分,主要用于数据显示(波形、温度等)、用户控制(LED,继电器等),下位机(单片机)与 上位机之间要进行数据通信的两种方式都是基于串口的:

  • USB转串口 —— 上位机和下位机通过USB转串口连接线直接相连进行数据交互
  • 串口转WIFI(ESP8266)—— 上位机和下位机基于TCP/IP协议通过以太网或者WIFI传输数据
  • 串口转蓝牙(HC-06)—— 不多用,暂不介绍

  Windows上位机(EXE可执行程序),最早用VB语言开发,后来由于C++的发展,采用MFC开发,近几年,微软发布了基于.NET框架的面向对象语言C#,更加稳定安全,再配合微软强大的VS进行开发,效率奇高。

  本文使用Visual Studio 2022作为开发环境,上位机开发主要有WPF框架与Winform框架,他们都是基于.NET框架

  • WPF需要C/S基础,使用XAML来构建应用UI,界面比较美观,但是内存开销大
  • Winform可以使用窗口控件来构建应用,比较简单易学,本文以Winform为例做一个简单的上位机

二、C#语法

三、新建VS工程

  首先新建,这边我们限定选项C#、Windowws、桌面,然后选择Windows窗体应用
在这里插入图片描述
  修改项目路径、名称,最后选择框架,由于是单项目所以勾选,否则在解决方案文件夹里会生成子项目文件夹,然后下一步就可以生成工程
在这里插入图片描述

  打开解决方案,双击Program.cs可以打开主函数MainApplication.Run(new Form1())就是循环执行应用。
在这里插入图片描述
  WinForm项目结构

  • 引用:包括所有的系统库文件的引用依赖
  • App.config:当前项目的配置文件
  • Form1.cs:当前窗体的事件逻辑源码
    • Form1.Designer.cs:当前窗体的控件布局源码
    • Form1.resx:当前窗体的资源文件(图片、图标、资源等)
  • Program.cs:当前项目程序的主入口Main,启动项目,运行初始窗口

  其中双击Form1.cs就可以打开UI设计界面,如果左侧工具栏和右侧属性栏不显示,可以去视图里打开工具箱和属性窗口。然后我们添加ButtonTextBox控件,单击Button,在右侧属性Text可以修改文字
在这里插入图片描述
  双击Button按钮就可以编辑代码生成对应功能,这里textBox1就是上图属性里的Name

//按下Send按钮
textBox1.Text = "^_^Hello,World^_^";    //文本框显示

在这里插入图片描述
  点击启动就可以进入调试页面,生成我们的应用,此时点击Send按钮,下面就可以显示字符
在这里插入图片描述

四、WinForm控件

4.1 属性

在这里插入图片描述
  在Windows窗体应用程序中右击窗体或控件,在弹出的右键菜单中 选择“属性”命令,窗体的常用属性如下表所示:

属性 作用
Name 窗体/空间的名称
WindowState 获取或设置窗体的窗口状态,取值有Normal(正常)、Minimized(最小化)、Maximized(最大化)
Text 窗口标题栏中的文字
Size 窗体的尺寸
MaximizeBox 获取或设置窗体标题栏右上角是否有最大化按钮,默认为 True
MinimizeBox 获取或设置窗体标题栏右上角是否有最小化按钮,默认为 True
BackColor 获取或设置窗体的背景色
BackgroundImage 获取或设置窗体的背景图像
FormBorderStyle 窗体边框的样式
Enabled 获取或设置窗体是否可用
Font 获取或设置窗体上文字的字体
ForeColor 获取或设置窗体上文字的颜色
Icon 获取或设置窗体上显示的图标
Location 窗体在屏幕上的位置
BackgroundImageLayout 获取或设置图像布局,取值有 5 种,即 None(图片居左显示)、Tile(图像重复,默认值)、Stretch(拉伸)、Center(居中)、Zoom(按比例放大到合适大小)
StartPosition 获取或设置窗体运行时的起始位置,取值有 5 种,即 Manual(窗体位置由 Location 属性决定)、CenterScreen(屏幕居中)、WindowsDefaultLocation( Windows 默认位置)、WindowsDefaultBounds(Windows 默认位置,边界由 Windows 决定)、CenterParent(在父窗体中居中)

4.2 事件

在这里插入图片描述
  在属性点击事件,常见的事件为:

事件名称 描述
Load 窗体加载时触发
Click 在窗体上单击时触发
MouseDoubleClick 鼠标双击事件
MouseMove 鼠标在窗体上移动时触发
KeyPress 键盘按键被按下时触发
FormClosing 窗体即将关闭时触发
Resize 窗体尺寸改变时触发
KeyDown 键盘按下事件
KeyUp 键盘释放事件
FormClosing 窗体关闭事件,关闭窗体时发生
FormClosed 窗体关闭事件,关闭窗体后发生

4.3 窗体方法

  自定义的窗体都继承自 System.Windows.Form 类,能使用 Form 类中已有的成员,包括属性、方法、事件等。窗体中也有一些从 System.Windows.Form 类继承的方法,如下表所示:

方法 作用
Show() 显示窗体
Hide() 隐藏窗体
Close() 关闭窗体
Activate() 激活窗体并给予它焦点
Invalidate() 强制重新绘制窗体
ShowDialog() 以对话框模式显示窗体
CenterToParent() 使窗体在父窗体边界内居中
CenterToScreen() 使窗体在当前屏幕上居中

4.4 常用控件

  • TextBox:输入文本框

    • 常用属性:尺寸Size、单行/多行Multiline、密码输入PasswordChar、只读ReadOnly、显示/获取文本Text等
    • 常用事件:KeyPress按下按键(常用于输出完成点击回车,box.Text取输入值,事件e.keyChar取按下哪个键)
  • CheckBox:复选框

    • 常用属性:尺寸Size、显示文本Text、Checked是否勾选
    • 常用事件:Click点击事件、CheckedChanged勾选状态改变事件
  • ComboBox:下拉列表(只能单选)

    • 常用属性:
      • a.添加数据项:设计器编辑添加(属性->数据->Items,一行表示一个数据项)、程序手工添加(comboBox.Items.Add(“some”);)
      • b.其他设置:Size尺寸、Text提示文本等
    • 常用事件:
      • a.获取选中的项:SelectedItem(选中项的值)、SelectedIndex(选中项的索引,-1表示未选中)
      • b.选项改变事件:SelectedIndexChanged
  • ListBox:列表框(展示数据、可单选/多选)

    • 常用属性:SelectionMode(单选/多选模式)
      • a.单选模式获取选项:SelectedItem/SelectedIndex(选中项的值/索引)
      • b.多选模式获取选项:SelectedIndices/SelectedItems(选中项的索引集合/值集合)
      • c.添加数据项:设计器编辑添加(属性->数据->Items)、程序手工添加(listBox.Items.Add(“some”);)
    • 常用事件:SelectedIndexChanged(选项改变)
  • Button、RadioButton、CheckBox、CheckedListBox:按钮

  • Label、LinkLabel:标签控件

  • MenuStrip:菜单栏

    • 右键菜单栏ContextMenuStrip
    • 状态栏菜单StatusStrip
    • 工具栏ToolStrip
  • Timer:定时器,Interval设置计时时间间隔,以毫秒为单位

  • PictureBox:图片框

    • 常用属性:
      • Image:“获取或设置图片控件中显示的图片
      • ImageLocation:获取或设置图片控件中显示图片的路径
      • SizeMode:获取或设置图片控件中图片显示的大小和位置,如果值为 Normal,则图片显不在控件的左上角;如果值为 Stretchimage,则图片在图片控件中被拉伸或收缩,适合图片的大小;如果值为AutoSize,则控件的大小适合图片的大小;如果值为 Centerimage,图片在图片控件中居中;如果值为 Zoom,则图片会自动缩放至符合图片控件的大小
      • dock:停靠方式

4.5 布局

  1. 自动布局
      选中多个控件,就可以在工具栏进行对齐排列
    在这里插入图片描述

  2. 使用布局器

  • 工具箱->容器->TableLayoutPanel —— 表格布局器
  • 工具箱->容器->FlowLayoutPanel —— 排列布局器
    在这里插入图片描述
  1. 手动布局
  • 通过设计界面拖拽/手动注册组件的方式,初始化初始界面布局(此时属于自动布局)
  • 在Form.cs逻辑代码中重写Form父类的OnLayout方法,在方法内实现手动布局。OnLayout方法会在窗口大小变化时自动被回调调用,来重新设置组件的位置大小等属性实现自适应。
    • a.调用父类的OnLayout(),不是必须的。
    • b.获取当前窗口大小 CilentSize(仅客户区,不含标题栏)
    • c.计算和设置每一个控件新的的大小和位置,实现动态布局

注意

  • Size属性指窗口大小(包括工具栏),ClientSize指客户区大小(不包含工具栏)
  • 本质:OnLayout方法会在窗口大小变化时自动被调用,来设置组件的位置实现自适应
namespace WindowsFormsApp_learning
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }
 
        //重写父类的OnLayout方法,实现手动布局自适应
        protected override void OnLayout(LayoutEventArgs levent)
        {
            //1.调用父类的OnLayout(),不是必须的
            base.OnLayout(levent);
            //2.获取当前客户窗口大小 ClientSize
            int w = this.ClientSize.Width;
            int h = this.ClientSize.Height;
            //3.计算并设置每一个控件的大小和位置
            int yoff = 0;
 
            yoff = 4;
            this.text_box.Location = new Point(0, yoff);//坐标(0,4)
            this.text_box.Size = new Size(w - 80, 30);//尺寸(w-80,30)
            this.btn_click.Location = new Point(w - 80, yoff);//坐标(w-80,4)
            this.btn_click.Size = new Size(80, 30);//尺寸(80,30)
 
            yoff += 30;//第一行的高度
            yoff += 4;//间隔
            this.panel1.Location = new Point(0, yoff);
            this.panel1.Size = new Size(w, h - yoff - 4);
        }
    }
}

五、Serial上位机

  首先新建一个SerialPort项目

5.1 UI界面设计

  1. 容器控件(Panel
      用容器给功能分组
    在这里插入图片描述

  2. 文本标签控件(Label
      添加文本控件,选中多个可以在工具栏选择对齐方式,选择单个控件->属性->Text可以修改文本内容
    在这里插入图片描述

  3. 文本框控件(TextBox)
      TextBox控件与label控件不同的是,文本框控件的内容可以由用户修改,这也满足我们的发送文本框需求;在默认情况下,TextBox控价是单行显示的,如果想要多行显示,需要设置其Multiline属性为true;
      TextBox的方法中最多的是APPendText方法,它的作用是将新的文本数据从末尾处追加至TextBox中,那么当TextBox一直追加文本后就会带来本身长度不够而无法显示全部文本的问题,此时我们需要使能TextBox的纵向滚动条来跟踪显示最新文本,所以我们将TextBox的属性ScrollBars的值设置为Vertical即可;
    在这里插入图片描述

  4. 下拉组合框控件(ComboBox

  DropDownStyle可以选择下拉模式:

  • DropDown模式,既可以选择下拉项,也可以选择直接编辑
  • DropDownList模式,只能从下拉列表中选择
    在这里插入图片描述
      在属性面板中Items属性中加入下拉项,默认项可以在Text里设置
    在这里插入图片描述
  • 停止位:1,1.5,2
  • 数据位:8,7,6,5
  • 波特率:230400,115200,76800,57600,38400,19200,9600,4800
  • 奇偶校验:None,奇校验,偶校验
  1. 按钮控件(Button
      在Text里修改按钮名称
    在这里插入图片描述

  2. 复选框控件(CheckBox
    在这里插入图片描述

  3. 串口组件(SerialPort
      添加串口组件后就可以调用串口的功能
    在这里插入图片描述

  4. 添加图标
      点击项目->属性->应用程序->浏览,然后添加.ico文件图标,这是.exe可执行文件图标
    在这里插入图片描述
      在属性列表里找到Form1属性,然后在Icon添加图标
    在这里插入图片描述
      最后界面设计大概这样
    在这里插入图片描述

5.2 功能设计

  1. 串口初始化
      先创建一个初始化函数Serial_init()
private void Serial_init()
{
//    int i;
    
//    for (i = 300; i <= 38400; i = i*2)//单个添加
    {
//        comboBox2.Items.Add(i.ToString());  //添加波特率列表
    }

    //批量添加波特率列表
    string[] baud = { "43000", "56000", "57600", "115200", "128000", "230400", "256000", "460800" };
    comboBox2.Items.AddRange(baud);

    //设置默认值
    comboBox1.Text = "COM1";
    comboBox2.Text = "115200";
    comboBox3.Text = "8";
    comboBox4.Text = "None";
    comboBox5.Text = "1";
}

  添加函数get_Serial_port()来获取电脑的串口

private void get_Serial_port()
{
	//获取电脑当前可用串口并添加到选项列表中
    //comboBox1.Items.AddRange(System.IO.Ports.SerialPort.GetPortNames());
    string[] ports = System.IO.Ports.SerialPort.GetPortNames(); //获得可用的串口
    for (int i = 0; i < ports.Length; i++)
    {
        comboBox1.Items.Add(ports[i]);
    }
    comboBox1.SelectedIndex = comboBox1.Items.Count > 0 ? 0 : -1;//如果里面有数据,显示第0个
}

  双击UI空白处,在Form1.cs里会生成Form1_Load()函数,然后将get_Serial_port()Serial_init()添加进来
在这里插入图片描述

  1. 打开串口
      双击UI界面“打开串口”(Button1),进入按键事件
private void button1_Click(object sender, EventArgs e)
{
    try
    {
        if (button1.Text == "打开串口")
        {
            serialPort1.PortName = comboBox1.Text;//获取要打开的串口
            serialPort1.BaudRate = int.Parse(comboBox2.Text);//获得波特率
            serialPort1.DataBits = int.Parse(comboBox3.Text);//获得数据位
                                                             //设置停止位
            if (comboBox5.Text == "1")
            {
                serialPort1.StopBits = StopBits.One;
            }
            else if (comboBox5.Text == "1.5")
            {
                serialPort1.StopBits = StopBits.OnePointFive;
            }
            else if (comboBox5.Text == "2")
            {
                serialPort1.StopBits = StopBits.Two;
            }
            //设置奇偶校验
            if (comboBox4.Text == "None")
            {
                serialPort1.Parity = Parity.None;
            }
            else if (comboBox4.Text == "奇校验")
            {
                serialPort1.Parity = Parity.Odd;
            }
            else if (comboBox4.Text == "偶校验")
            {
                serialPort1.Parity = Parity.Even;
            }
            serialPort1.Open();//打开串口
            button1.Text = "关闭串口";
            button1.BackColor = Color.Firebrick;
        }
        else
        {
            //关闭串口
            serialPort1.Close();//关闭串口
            button1.Text = "打开串口"; //按钮显示打开
            button1.BackColor = Color.ForestGreen;
        }
    }
    catch (Exception err)
    {
        MessageBox.Show("打开失败" + err.ToString(), "提示!");
    }

}
  1. 发送
      首先需要将字符串转化16进制,可以用正则表达式
private byte[] strToHexbytes(string str)
{
    str = str.Replace(" ", "");//清除空格
    byte[] buff;
    if ((str.Length % 2) != 0)
    {
        buff = new byte[(str.Length + 1) / 2];
        try
        {
            for (int i = 0; i < buff.Length; i++)
            {
                buff[i] = Convert.ToByte(str.Substring(i * 2, 2), 16);
            }
            buff[buff.Length - 1] = Convert.ToByte(str.Substring(str.Length - 1, 1).PadLeft(2, '0'), 16);
            return buff;
        }
        catch (Exception err)
        {
            MessageBox.Show("含有f非16进制的字符", "提示");
            return null;
        }
    }
    else
    {
        buff = new byte[str.Length / 2];
        try
        {
            for (int i = 0; i < buff.Length; i++)
            {
                buff[i] = Convert.ToByte(str.Substring(i * 2, 2), 16);
            }
        }
        catch (Exception err)
        {
            {
                MessageBox.Show("含有非16进制的字符", "提示");
                return null;
            }
        }
    }
    return buff;
}

  串口发送有两种方法,一种是字符串发送WriteLine,一种是Write(),可以发送一个字符串或者16进制发送,其中字符串发送WriteLine默认已经在末尾添加换行符;先双击“发送”(Button2)

private void button2_Click(object sender, EventArgs e)
{
    Task.Run(() => {
        send_();
    });
}

string data_;
//发送数据  
private void send_()
{
    data_ = textBox2.Text.ToString();
    try
    {
        if (data_.Length != 0)
        {
            data_ += " ";
            if (checkBox2.Checked) //16进制发送
            {
                serialPort1.Write(Encoding.Default.GetString(strToHexbytes(data_)));
            }
            else
            {
                serialPort1.Write(data_);     
            }
        }

    }
    catch (Exception) { }
}
  1. 接收
      在使用串口接收之前要先为串口注册一个Receive事件,相当于单片机中的串口接收中断,然后在中断内部对缓冲区的数据进行读取,如图,双击DataReceived,就会跳转到响应代码部分:
    在这里插入图片描述
      在接收之前需要注意,这里的数据全部是byte型数据,接收文本框显示的内容都是以字符串形式呈现的,所以先添加16进制的方法
private string byteToHexstr(byte[] buff)
{
    string str = "";
    try
    {
        if (buff != null)
        {
            for (int i = 0; i < buff.Length; i++)
            {
                str += buff[i].ToString("x2");
                str += " ";//两个之间用空格
            }
            return str;
        }
    }
    catch (Exception)
    {
        return str;
    }
    return str;
}

  最后在serialPort1_DataReceived()进行接收数据处理

private void serialPort1_DataReceived(object sender, SerialDataReceivedEventArgs e)
{
    int len = serialPort1.BytesToRead; //获取可以读取的字节数

    byte[] buff = new byte[len];
    serialPort1.Read(buff, 0, len);//把数据读取到数组中
    string reslut = Encoding.Default.GetString(buff);
    //将byte值根据为ASCII值转为string
    Invoke((new Action(() =>
    {
        if (checkBox1.Checked)//16进制转化
        {
            textBox1.AppendText(" " + byteToHexstr(buff));
        }
        else
        {
            textBox1.AppendText(" " + reslut);
        }
    }
    )));
}
  1. 清空接收
      双击“清空接收”(Button3),修改如下。此时项目结束
private void button3_Click(object sender, EventArgs e)
{
    textBox1.Clear(); 
}   

六、项目打包成安装包

6.1 前提准备

  1. 生成项目文件
      首先将Debug版本变成Release版本,进行编译
  • Debug (调试):不进行优化,便于程序员调试应用程序。
  • Release (发布):进行完全优化,减少代码大小,提高运行速度。
    在这里插入图片描述
  1. 安装扩展.
      点击扩展->管理扩展,在搜索框里搜索Visual studio Installer,点击安装
    在这里插入图片描述
      也可以去插件官网下载安装:Microsoft Visual Studio Installer Projects 2022 - Visual Studio Marketplace

注意:安装需要关闭Visual Studio,如果安装失败重启一下电脑

6.2 打包步骤

  1. 新建Setup Project项目
      右击解决方案->添加->新建项目
    在这里插入图片描述
      搜索Setup Project
    在这里插入图片描述
      新建项目文件夹
    在这里插入图片描述

  2. 添加项目文件
      首先右击Application Folder->Add->文件
    在这里插入图片描述
      然后将工程目录Relese里的文件全部添加进来
    在这里插入图片描述

  3. 创建桌面文件
      对.exe文件右击创建桌面文件
    在这里插入图片描述
      然后将生成的文件拖到User’s Desktop里
    在这里插入图片描述
      点击桌面文件可以在属性里修改Name
    在这里插入图片描述
      找一个ico文件添加进来,然后点击Icon添加图标
    在这里插入图片描述

  4. 生成工程
      点击解决方案app,在属性里可以修改信息,其中Manufacturer不填会报错
    在这里插入图片描述
      右击找到属性
    在这里插入图片描述
      然后在Prerequisites添加组件
    在这里插入图片描述
      右击生成,就可以在文件夹里生成安装文件
    在这里插入图片描述
    在这里插入图片描述


网站公告

今日签到

点亮在社区的每一天
去签到