目录
- 一、上位机简介
- 二、C#语法
- 三、新建VS工程
- 四、WinForm控件
- 4.1 属性
- 4.2 事件
- 4.3 窗体方法
- 4.4 常用控件
- 4.5 布局
- 五、Serial上位机
- 5.1 UI界面设计
- 5.2 功能设计
- 六、项目打包成安装包
- 6.1 前提准备
- 6.2 打包步骤
一、上位机简介
在单片机项目开发中,上位机也是一个很重要的部分,主要用于数据显示(波形、温度等)、用户控制(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#语法
- C#语法基础:C#语言入门
- Winform学习:Windows窗体入门
三、新建VS工程
首先新建,这边我们限定选项C#、Windowws、桌面,然后选择Windows窗体应用
修改项目路径、名称,最后选择框架,由于是单项目所以勾选,否则在解决方案文件夹里会生成子项目文件夹,然后下一步就可以生成工程
打开解决方案,双击Program.cs可以打开主函数Main,Application.Run(new Form1())
就是循环执行应用。
WinForm项目结构
- 引用:包括所有的系统库文件的引用依赖
- App.config:当前项目的配置文件
- Form1.cs:当前窗体的事件逻辑源码
- Form1.Designer.cs:当前窗体的控件布局源码
- Form1.resx:当前窗体的资源文件(图片、图标、资源等)
- Program.cs:当前项目程序的主入口Main,启动项目,运行初始窗口
其中双击Form1.cs就可以打开UI设计界面,如果左侧工具栏和右侧属性栏不显示,可以去视图里打开工具箱和属性窗口。然后我们添加Button和TextBox控件,单击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(选项改变)
- 常用属性:SelectionMode(单选/多选模式)
-
Button、RadioButton、CheckBox、CheckedListBox:按钮
-
Label、LinkLabel:标签控件
-
MenuStrip:菜单栏
- 右键菜单栏ContextMenuStrip
- 状态栏菜单StatusStrip
- 工具栏ToolStrip
-
Timer:定时器,Interval设置计时时间间隔,以毫秒为单位
-
PictureBox:图片框
- 常用属性:
- Image:“获取或设置图片控件中显示的图片
- ImageLocation:获取或设置图片控件中显示图片的路径
- SizeMode:获取或设置图片控件中图片显示的大小和位置,如果值为 Normal,则图片显不在控件的左上角;如果值为 Stretchimage,则图片在图片控件中被拉伸或收缩,适合图片的大小;如果值为AutoSize,则控件的大小适合图片的大小;如果值为 Centerimage,图片在图片控件中居中;如果值为 Zoom,则图片会自动缩放至符合图片控件的大小
- dock:停靠方式
- 常用属性:
4.5 布局
-
自动布局
选中多个控件,就可以在工具栏进行对齐排列
-
使用布局器
- 工具箱->容器->TableLayoutPanel —— 表格布局器
- 工具箱->容器->FlowLayoutPanel —— 排列布局器
- 手动布局
- 通过设计界面拖拽/手动注册组件的方式,初始化初始界面布局(此时属于自动布局)
- 在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界面设计
-
容器控件(Panel)
用容器给功能分组
-
文本标签控件(Label)
添加文本控件,选中多个可以在工具栏选择对齐方式,选择单个控件->属性->Text可以修改文本内容
-
文本框控件(TextBox)
TextBox控件与label控件不同的是,文本框控件的内容可以由用户修改,这也满足我们的发送文本框需求;在默认情况下,TextBox控价是单行显示的,如果想要多行显示,需要设置其Multiline属性为true;
TextBox的方法中最多的是APPendText方法,它的作用是将新的文本数据从末尾处追加至TextBox中,那么当TextBox一直追加文本后就会带来本身长度不够而无法显示全部文本的问题,此时我们需要使能TextBox的纵向滚动条来跟踪显示最新文本,所以我们将TextBox的属性ScrollBars的值设置为Vertical即可;
-
下拉组合框控件(ComboBox)
DropDownStyle可以选择下拉模式:
- DropDown模式,既可以选择下拉项,也可以选择直接编辑
- DropDownList模式,只能从下拉列表中选择
在属性面板中Items属性中加入下拉项,默认项可以在Text里设置
- 停止位:1,1.5,2
- 数据位:8,7,6,5
- 波特率:230400,115200,76800,57600,38400,19200,9600,4800
- 奇偶校验:None,奇校验,偶校验
-
按钮控件(Button)
在Text里修改按钮名称
-
复选框控件(CheckBox)
-
串口组件(SerialPort)
添加串口组件后就可以调用串口的功能
-
添加图标
点击项目->属性->应用程序->浏览,然后添加.ico文件图标,这是.exe可执行文件图标
在属性列表里找到Form1属性,然后在Icon添加图标
最后界面设计大概这样
5.2 功能设计
- 串口初始化
先创建一个初始化函数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()
添加进来
- 打开串口
双击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(), "提示!");
}
}
- 发送
首先需要将字符串转化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) { }
}
- 接收
在使用串口接收之前要先为串口注册一个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);
}
}
)));
}
- 清空接收
双击“清空接收”(Button3),修改如下。此时项目结束
private void button3_Click(object sender, EventArgs e)
{
textBox1.Clear();
}
六、项目打包成安装包
6.1 前提准备
- 生成项目文件
首先将Debug版本变成Release版本,进行编译
- Debug (调试):不进行优化,便于程序员调试应用程序。
- Release (发布):进行完全优化,减少代码大小,提高运行速度。
- 安装扩展.
点击扩展->管理扩展,在搜索框里搜索Visual studio Installer,点击安装
也可以去插件官网下载安装:Microsoft Visual Studio Installer Projects 2022 - Visual Studio Marketplace
注意:安装需要关闭Visual Studio,如果安装失败重启一下电脑
6.2 打包步骤
-
新建Setup Project项目
右击解决方案->添加->新建项目
搜索Setup Project
新建项目文件夹
-
添加项目文件
首先右击Application Folder->Add->文件
然后将工程目录Relese里的文件全部添加进来
-
创建桌面文件
对.exe文件右击创建桌面文件
然后将生成的文件拖到User’s Desktop里
点击桌面文件可以在属性里修改Name
找一个ico文件添加进来,然后点击Icon添加图标
-
生成工程
点击解决方案app,在属性里可以修改信息,其中Manufacturer不填会报错
右击找到属性
然后在Prerequisites添加组件
右击生成,就可以在文件夹里生成安装文件