在单片机项目开发中,上位机也是一个很重要的部分,主要用于数据显示(波形、温度等)、用户控制(LED,继电器等),下位机(单片机)与 上位机之间要进行数据通信的两种方式都是基于串口的:
Windows上位机(EXE可执行程序),最早用VB语言开发,后来由于C++的发展,采用MFC开发,近几年,微软发布了基于.NET框架的面向对象语言C#,更加稳定安全,再配合微软强大的VS进行开发,效率奇高。
本文使用Visual Studio 2022作为开发环境,上位机开发主要有WPF框架与Winform框架,他们都是基于.NET框架
首先新建,这边我们限定选项C#、Windowws、桌面,然后选择Windows窗体应用
修改项目路径、名称,最后选择框架,由于是单项目所以勾选,否则在解决方案文件夹里会生成子项目文件夹,然后下一步就可以生成工程
打开解决方案,双击Program.cs可以打开主函数Main,Application.Run(new Form1())
就是循环执行应用。
WinForm项目结构
其中双击Form1.cs就可以打开UI设计界面,如果左侧工具栏和右侧属性栏不显示,可以去视图里打开工具箱和属性窗口。然后我们添加Button和TextBox控件,单击Button,在右侧属性Text可以修改文字
双击Button按钮就可以编辑代码生成对应功能,这里textBox1就是上图属性里的Name
//按下Send按钮 textBox1.Text = "^_^Hello,World^_^"; //文本框显示
点击启动就可以进入调试页面,生成我们的应用,此时点击Send按钮,下面就可以显示字符
在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(在父窗体中居中) |
在属性点击事件,常见的事件为:
事件名称 | 描述 |
---|---|
Load | 窗体加载时触发 |
Click | 在窗体上单击时触发 |
MouseDoubleClick | 鼠标双击事件 |
MouseMove | 鼠标在窗体上移动时触发 |
KeyPress | 键盘按键被按下时触发 |
FormClosing | 窗体即将关闭时触发 |
Resize | 窗体尺寸改变时触发 |
KeyDown | 键盘按下事件 |
KeyUp | 键盘释放事件 |
FormClosing | 窗体关闭事件,关闭窗体时发生 |
FormClosed | 窗体关闭事件,关闭窗体后发生 |
自定义的窗体都继承自 System.Windows.Form 类,能使用 Form 类中已有的成员,包括属性、方法、事件等。窗体中也有一些从 System.Windows.Form 类继承的方法,如下表所示:
方法 | 作用 |
---|---|
Show() | 显示窗体 |
Hide() | 隐藏窗体 |
Close() | 关闭窗体 |
Activate() | 激活窗体并给予它焦点 |
Invalidate() | 强制重新绘制窗体 |
ShowDialog() | 以对话框模式显示窗体 |
CenterToParent() | 使窗体在父窗体边界内居中 |
CenterToScreen() | 使窗体在当前屏幕上居中 |
TextBox:输入文本框
CheckBox:复选框
ComboBox:下拉列表(只能单选)
ListBox:列表框(展示数据、可单选/多选)
Button、RadioButton、CheckBox、CheckedListBox:按钮
Label、LinkLabel:标签控件
MenuStrip:菜单栏
Timer:定时器,Interval设置计时时间间隔,以毫秒为单位
PictureBox:图片框
自动布局
选中多个控件,就可以在工具栏进行对齐排列
使用布局器
注意:
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); } } }
首先新建一个项目,命名SerialPort
容器控件(Panel)
用容器给功能分组
文本标签控件(Label)
添加文本控件,选中多个可以在工具栏选择对齐方式,选择单个控件->属性->Text可以修改文本内容
文本框控件(TextBox)
TextBox控件与label控件不同的是,文本框控件的内容可以由用户修改,这也满足我们的发送文本框需求;在默认情况下,TextBox控价是单行显示的,如果想要多行显示,需要设置其Multiline属性为true;
TextBox的方法中最多的是APPendText方法,它的作用是将新的文本数据从末尾处追加至TextBox中,那么当TextBox一直追加文本后就会带来本身长度不够而无法显示全部文本的问题,此时我们需要使能TextBox的纵向滚动条来跟踪显示最新文本,所以我们将TextBox的属性ScrollBars的值设置为Vertical即可;
下拉组合框控件(ComboBox)
DropDownStyle可以选择下拉模式:
按钮控件(Button)
在Text里修改按钮名称
复选框控件(CheckBox)
串口组件(SerialPort)
添加串口组件后就可以调用串口的功能
添加图标
点击项目->属性->应用程序->浏览,然后添加.ico文件图标,这是.exe可执行文件图标
在属性列表里找到Form1属性,然后在Icon添加图标
最后界面设计大概这样
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()
添加进来
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(), "提示!"); } }
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) { } }
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); } } ))); }
private void button3_Click(object sender, EventArgs e) { textBox1.Clear(); }
注意:安装需要关闭Visual Studio,如果安装失败重启一下电脑
新建Setup Project项目
右击解决方案->添加->新建项目
搜索Setup Project
新建项目文件夹
添加项目文件
首先右击Application Folder->Add->文件
然后将工程目录Relese里的文件全部添加进来
创建桌面文件
对.exe文件右击创建桌面文件
然后将生成的文件拖到User’s Desktop里
点击桌面文件可以在属性里修改Name
找一个ico文件添加进来,然后点击Icon添加图标
生成工程
点击解决方案app,在属性里可以修改信息,其中Manufacturer不填会报错
右击找到属性
然后在Prerequisites添加组件
右击生成,就可以在文件夹里生成安装文件