You are on page 1of 32

C#上位机实战开发指南

C#上位机实战开发指南

1
C#上位机实战开发指南

第一章 C#和 Visual Stduio

1.1 .NET 时代

在.NET 之前,尤其是 20 世纪 90 年代,Windows 程序员几乎使用 VB,C 或者


C++。部分 C 和 C++开发者使用纯 Win32 Api,但是大多数人还是选择使用 MFC。
这些语言开发难度较大,底层代码复杂。21 世纪初期越来越多的开发者迫切需
要一个安全,集成度高,面向对象的开发框架。
2002 年,微软如期发布了.NET 框架的第一个版本,它具有如下几个特点:
●多平台 可在任意计算机系统运行,包括服务器,台式机等。
●安全性 提供更加安全的运行环境,即使有来源可疑的代码存在。
●行业标准 使用标准通信协议,比如 HTTP,SOAP,JSON 等。
在 2016 年最新一期的编程语言排行榜中 C#.NET 位列第四,而且呈上升趋势。
排行榜如图 1-1 所示。

图 1-1:编程语言排行榜

2
C#上位机实战开发指南

1.2 C#的前世今生

C#是微软发布的一种面向对象,运行于.NET 之上的高级语言。也是微软近几
年主推的开发语言,可以说是微软.NET 框架的主角。只要具备一些 C 语言基础
就可以非常迅速的入门 C#开发,这也是我极力推荐使用 C#开发上位机的一个重
要原因。

1.3 难以置信的 Visual Studio 2015

Visual Studio 2015(以下简称 VS2015)是微软推出的开发环境,C#也是基于


此开发。相比较之前的版本,VS2015 具有更强大的调试功能,甚至集成了安卓,
IOS 等跨平台开发环境。作为一个强大的集成开发环境,VS2015 同时还能支持
STM32 单片机的编译。
具体教程请参看:http://www.openedv.com/thread-10273-1-1.html。
笔者认为 VS2015 是宇宙最强 IDE,完虐我们常用的单片机开发环境如 KEIL,
IAR 等。更多使用技巧就留给读者自己去发现吧。

1.4 VS2015 的安装与使用

3
C#上位机实战开发指南

第二章 C#语法基础

2.1 C#编程概述

本章将为上位机开发打基础,当然具有 C 语言或者单片机开发经验的同学也
可以跳过本章,直接进入第三章窗体程序的学习中。因为 C#和 C 语言在语法上
大致相同。本章只讲解一些与单片机 C 语言相差较大的部分,其余不再过多讲解。
代码分析也全部放在第三章以后。若想深入学习 C#,请参考专业入门书籍,推
荐《C#图解教程》(第四版)。

2.2 命名空间

在 C#中,命名空间提供了一种组织相关类和其它类型的方式。我理解的命名
空间就是一个集装箱,里面可以装下很多类和方法。其实我们也可以认为所谓的
命名空间相当于 C 语言中的头文件,只不过 include 变为了 using namespace。具
体的书写规范见代码清单 2-1。
代码清单 2-1:命名空间书写规范

1. using System; //命名空间类似于头文件


2. using System.Collections.Generic; //using ≈ include; Systerm ≈ xxxx.h
3. using System.ComponentModel;
4. using System.Data;
5. using System.Drawing;
6. using System.Text;
7. using System.Windows.Forms;
8.
9. //用户自定义命名空间,相当于新定义一个头文件
10. //一般情况下一个上位机工程对应一个新的命名空间
11. namespace Demo
12. {
13.
14. public partial class Form1 : Form
15. {
16. //构造函数,新建窗体工程时自动创建这段代码,可先忽略
17. public Form1()
18. {
19. InitializeComponent();
20. }
21. }
22. }

4
C#上位机实战开发指南

这段代码在新建工程之后由 VS2015 自动创建,第 1 到 7 行代码全都为系统


自带的命名空间。第 11 行为开发人员自定义的命名空间,之后的每一个上位机
项目都是一个自定义命名空间。
大概了解了 C#命名空间的书写格式规范后,我们再简单回忆一下 C 语言中头
文件的书写规范并比较二者的异同点,C 头文件书写格式见代码清单 2-2。
代码清单 2-2:C 头文件书写规范

1. #ifndef __USART_H
2. #define __USART_H
3.
4.
5. #include "stm32f10x.h"
6. #include "stdio.h"
7. #include "string.h"
8.
9.
10. #define TxBuffSize 256
11.
12.
13. #define Debug_ON 1
14.
15.
16. #define DebugPutInfo(fmt,arg...) do{if(Debug_ON)printf(fmt,##arg);}while(0)
17.
18.
19. void USART_Config(void);
20. void USART1_SendByte(uint8_t DataToSend);
21. void USART1_SendString(const char* StringToSend);
22. void USART1_SendBuff(uint8_t* DataToSend, uint8_t DataNum);
23.
24.
25. #endif

通过代码清单 2-2 我们很容易发现,C#的命名空间和 C 的头文件遵循一样的


规则,即要想使用某方法某函数则必须要包含方法所在的命名空间或者头文件。
这是相同点。不同点则表现在 C 语言在声明了头文件和函数接口后必须要在对应
的 C 文件中编写函数体后才可使用。C#则将省去了函数声明,直接即可编写对应
的函数体。

5
C#上位机实战开发指南

2.3 类

2.3.1 什么是类

在 C#开发中,类(class)至关重要。可以认为类是 C#一个很大的主题。关于它
的讨论将一直延续到本书结束。我们在单片机软件开发中设计数据结构时往往离
不开先设计结构体,其实类就相当于结构体,这也是面向对象的一个前提条件。
我们可以将类抽象成一个既能存储数据又能执行代码的数据结构。它包含数据成
员和函数成员,因此类对 C#代码的封装起着举足轻重的作用。

2.3.2 如何声明一个类

类的声明和结构体类似,即定义了一个新类的成员和特征。但是它并不创建
类的实例,相当于结构体声明后并不分配内存,只有在使用时声明后才会分配内
存一样,类的声明和实例化不可混淆。类的声明方式如代码清单 2-3 所示。
代码清单 2-3:类的声明方式

1. using System; //命名空间类似于头文件


2. using System.Collections.Generic; //using ≈ include; Systerm ≈ xxxx.h
3. using System.ComponentModel;
4. using System.Data;
5. using System.Drawing;
6. using System.Text;
7. using System.Windows.Forms;
8.
9. //用户自定义命名空间,相当于新定义一个头文件
10. //一般情况下一个上位机工程对应一个新的命名空间
11. namespace Demo
12. {
13.
14. public partial class Form1 : Form
15. {
16. //构造函数,新建窗体工程时自动创建,可先忽略
17. public Form1()
18. {
19. InitializeComponent();
20. }
21.
22. //类的声明方式
23. class MyClass
24. {
25. //成员声明... ...

6
C#上位机实战开发指南

26. //class 为类的关键字,MyClass 为类名


27. }
28. }
29. }

从代码清单 2-3 可以清晰看出,类的声明非常简单。其中类的成员可以是变


量,也可以是函数方法。

2.4 Main: 程序由你开始

每一个 C#程序都必须有一个类带有 Main 函数(方法),它是程序的开始,它


通常被声明在 Program 类中。这就好比我们在开发单片机时喜欢将 main 函数声
明在 main.c 中一样。通常 Program.cs 文件随工程一起创建,详细代码见图 2-1。

图 2-1:Main 方法

从图中我们清晰的看到了 Class 关键字,其实 Program.cs 本身就是一个类文


件。

2.5 变量与常量

2.5.1 值类型与引用类型

值类型和我们单片机开发中的数据类型类似,需要一段独立内存存放它的实
际数据。如果值类型变量定义在方法(函数)内部那么在调用结束后这片内存回收。
相反如果定义为全局,那这片内存则不会被回收。这和 C 基本一样。char,int
float,enum,struct 等都是值类型。

7
C#上位机实战开发指南

引用类型是一个特殊的类型,它的存储需要两片内存。实例数据存放在堆中,
引用存放在栈中,引用可以理解为指针。具体引用类型为什么需要两片内存不再
做任何讨论,我们只需要知道引用类型的使用和常规的值类型有什么区别就行。
C 语言中如果我们表示一段字符串可以定义一个指针,在 C#中直接使用 string 关
键字即可定义。string 便是一个非常典型的引用类型,它不遵循值类型的规则。
当我们定义一个 string 类型变量并且第一次赋值时假设它在地址 0x02000000 中,
那么在第二次赋值再次查看内存时,它已经不在上一次地址中,即引用类型每次
在使用后都会变更内存地址。引用类型在并行多线程的使用中尤为重要。
当然,在上位机开发中我们可以将引用类型当作一般类型来使用。

2.5.2 声明变量

C#声明变量和 C 语言相同,声明过程完成两件事。
●给变量命名,并且关联一种类型
●编译器为其分配一片内存

2.5.3 变量的作用域

类中的变量作用域就在类中,类被回收,变量即被回收。方法(函数)内部变
量作用域为整个方法体。其中如果变量是某循环某判断中定义的,作用域就在循
环或者判断体内。

2.5.4 访问修饰符

代码清单 2-3 中类的声明在 class 前未添加任何访问修饰符,C#规定无访问修


饰符的情况下类成员即为隐式私有,外部不可访问。
C#常用的访问修饰符有以下 5 个。

◇私有的:private
◇公开的:public
◇受保护的:protected
◇内部的:internal
◇受保护内部的:protected internal

顾名思义,private 私有即外部不可访问,只能在类的内部使用,而 public


修饰的变量则可以在类的外部访问。关于 private 和 public 以及变量在类中的
使用查看代码清单 2-4。
代码清单 2-4:访问修饰符及变量在类中的简单使用

1. using System; //命名空间类似于头文件


2. using System.Collections.Generic; //using ≈ include; Systerm ≈ xxxx.h

8
C#上位机实战开发指南

3. using System.ComponentModel;
4. using System.Data;
5. using System.Drawing;
6. using System.Text;
7. using System.Windows.Forms;
8.
9. //用户自定义命名空间,相当于新定义一个头文件
10. //一般情况下一个上位机工程对应一个新的命名空间
11. namespace Demo
12. {
13. public partial class Form1 : Form
14. {
15. //构造函数,新建窗体工程时自动创建,可先忽略
16. public Form1()
17. {
18. InitializeComponent();
19. }
20.
21. int data0 = 0; //全局变量声明,变量声明后即可在方法中使用
22. //与 C 语言相同
23.
24.
25. //类的声明方式
26. class MyClass
27. {
28. //成员声明... ...
29. //class 为类的关键字,MyClass 为类名
30.
31. int data1 = 0; //无修饰符默认隐式私有
32. //外部不可访问
33.
34. public int data2 = 0;//公有,外部可访问
35. }
36. }
37. }

从代码清单 2-4 可看出类 MyClass 中 data2 添加了修饰符 public,因此它可以


在类的外部被调用。

9
C#上位机实战开发指南

2.6 多线程的使用

2.6.1 线程概述

相信大家在嵌入式 RTOS 中就已经接触过多线程(多任务)的处理机制。同样在


多线程的使用下 C#便可以并行执行代码。注意,这里的并行并不是真正意义上
的同时执行,只是任务上下文切换速度极快,给人的感觉好像是在并行。
一个 C#程序开始于一个单线程(Main 方法入口),这个线程是由操作系统自动
创建的,我们也称之为主线程或者 UI 线程。同时主线程下可以创建多个子线程。

2.6.2 何时使用多线程

多线程一般情况下用在后台处理耗时任务,主线程保持执行。对于 Winform
来讲,如果所有耗时任务都放在主线程执行,那就会带来鼠标键盘等响应迟钝现
象。为了避免这个现象,我们可以在主线程中再创建一个子线程,这样就避免了
阻塞主线程,导致 UI 响应迟钝的现象。一个优秀的交互软件必定会有多线程的
使用。

2.6.3 多线程的优缺点

在多线程的帮助下,我们可以快速的实现异步操作,这使得软件的 UI 可以
迅速响应,给客户一个极佳的 UI 体验。无论我们是否使用过 RTOS,但 STM32
中的 DMA 我相信大家使用的非常之多,DMA 在进行内存拷贝传输时完全不需要
CPU 干预,由此我们完全可以理解为 DMA 是一个全硬件实现的子线程。
当然多线程并非全无缺点,最大的问题便是加大了代码的复杂性。当然多线
程本身非常简单,但线程间的交互却非常复杂,使用不当甚至会带来间歇性或重
复性的 BUG。同时多线程无意间又增加了 CPU 资源的消耗。

2.6.4 多线程的简单使用

一般情况下上位机多线程都使用局部线程,它和局部变量类似,用时创建,
用完销毁。全局线程在上位机开发当中使用的相对比较少。当然全局也可以使用
但必须要自己实现挂起和恢复函数,系统自带的接口函数已经过时,容易造成阻
塞,实际开发中我们也几乎很少用到全局线程。因此我将只介绍局部线程的使用
方法。局部线程存在于方法中,像局部变量一样使用,具体介绍请看代码清单
2-5。
代码清单 2-5:局部线程的使用

1. using System; //命名空间类似于头文件


2. using System.Collections.Generic; //using ≈ include; Systerm ≈ xxxx.h
3. using System.ComponentModel;

10
C#上位机实战开发指南

4. using System.Data;
5. using System.Drawing;
6. using System.Text;
7. using System.Threading;
8. using System.Windows.Forms;
9.
10. //用户自定义命名空间,相当于新定义一个头文件
11. //一般情况下一个上位机工程对应一个新的命名空间
12. namespace Demo
13. {
14. public partial class Form1 : Form
15. {
16. //新建窗体工程时自动创建这段代码,可先忽略
17. public Form1()
18. {
19. InitializeComponent();
20. }
21.
22. int data0 = 0; //全局变量声明,变量声明后即可在方法中使用
23. //与 C 语言相同
24.
25.
26. //类的声明方式
27. class MyClass
28. {
29. //成员声明... ...
30. //class 为类的关键字,MyClass 为类名
31.
32. int data1 = 0; //无修饰符默认隐式私有
33. //外部不可访问
34.
35. public int data2 = 0;//公有,外部可访问
36. }
37.
38. /// <summary>
39. /// 多线程的使用
40. /// </summary>
41. public void ThreadTest()
42. {
43. new Thread(() =>
44. {
45. ///
46. ///添加自定义代码
47. ///例如线程内部运行函数方法:xxxxx

11
C#上位机实战开发指南

48. /// xxxxx();


49. })
50. { IsBackground = true, Name = "多线程的使用" }.Start();
51. }
52. }
53. }

从代码清单 2-5 中我们看到函数 ThreadTest,内部就是一个局部子线程的写


法。其中 IsBackgroud = true 是将该线程设置为了后台线程。后台线程的作用主
要在于当上位机程序关闭时线程也自动随窗体一起销毁了,如果不设置为后台线
程窗体关闭时线程依然在消耗着 CPU。因此使用时切记一定要将后台属性设置为
true。Name 即线程的名字,注意区分的作用,没有什么好讲,写与不写没有区
别。Start 即启动线程的接口函数,运行后就将执行线程内部的方法。内部函数
执行完线程销毁。
注意局部线程和全局线程只是个人的称呼,在 C#中并没有严格的定义来区分
这两种线程,我们会用即可。

2.7 异常处理

2.7.1 异常概述

见名知意,异常就是软件在运行中所发生的错误。比如上位机串口未打开就
调用了发送方法,此时系统就会捕获到这个错误,并抛出一个异常。如果软件设
计时没有提供一个异常处理的方法,则系统自动将软件挂起。通常我们在调用串
口发送方法前都会判断是否开启串口或者嵌套 try...catch 语句来捕获异常防止软
件被系统挂起。

2.7.2 try...catch 语句

try 语句用来指明为避免异常而被保护的代码段,并在发生异常时提供处理
代码。一般情况下我们使用 try...catch 组合语句来保护关键性代码。具体使用实
例在上位机实战章节具体介绍。

2.8 属性和方法

2.8.1 什么是属性

在本章第二小节中我们简要的接触了类的概念,类相当于一个结构体但不能
等价于一个结构体,因为类是具有属性的,而结构体没有。在结构体内部定义一
12
C#上位机实战开发指南

个缓冲区,这个缓冲区的大小必须在程序编译前确定下来,运行中不可改变。但
类通过属性却可以修改这个缓冲区的大小。那么什么是属性呢?属性就好比一个
人的发色,生来黑色,但不会永远是黑色,我们可以随意染成红蓝紫色。也就是
说属性是一个类的动态特性,比如上位机在运行过程中我们可以随时修改波特
率。

2.8.2 属性的优点

上一节中我们提到上位机的波特率可以在运行过程中任意修改,这就是属性
的一个优点。
当然属性也对类内部的私有变量提供了一种保护机制。要想修改类内部私有
变量值就必须通过属性来操作。这就好比去银行存钱我们无法进入金库,只能通
过 ATM 机是一样的。

2.8.3 属性的使用介绍

属性一般情况下可直接在控件属性栏中设置,也可通过代码设置。上位机开
发中一般都是使用系统自带的类库,很少会自己编写类库以及属性。因此本节就
不再介绍如何写声明属性,在具体讲到控件使用时再介绍属性的妙用。

2.8.4 什么是方法

C#中的方法类似于 C 语言函数,只是 C#赋予了方法属性,方法既可以是私有


的,又可以是共用的。谈及私有方法,其实我们完全可以将它理解为 C 语言函数
套了一个 static 关键字,代表这是单文件中使用的。

2.9 委托和事件

2.9.1 什么是委托

委托可以说是 C#第一个要跨过去的坎儿,理解难度比较大。但我会在接下来
的上位机实战章节中具体介绍学习的每一步,在本章就做一个简要的介绍。
我非常喜欢将委托比喻成 C 语言中的函数指针数组,我们知道函数指针的存
在极大的方便了我们设计单片机软件架构,事件回调机制等封装技术都基于函数
指针实现。无独有偶,在 C#中事件回调机制也是通过委托实现,所以我一直认
为软件思想都是相通的,只是表现形式上换了一个说法而已。对我们单片机出身
的软件开发人员来讲,理解起委托易如反掌,因为我们已经在底层深耕多年。
那么 C#如何定义委托呢?可以认为委托是持有一个或者多个方法的对象。委
托和类一样,是一种用户自定义类型,不同在类是数据和方法的集合。执行委托
即执行了委托中所有的方法。

13
C#上位机实战开发指南

2.9.2 什么是事件

学习 STM32 之时,我们已经接触过事件的概念。事件是由硬件实现,可触发
中断以及关联性操作,如 ADC,DMA 等。它和中断最大的区别在于事件无需返
回,而中断需要返回。事件不仅在 MCU 硬件中大量使用,同时又与单片机软件
架构设计息息相关。
所有的 PC 端程序都需要在某个特定的时刻响应某个操作,处理某件事情,
比如响应鼠标单击事件,键盘事件等,因此 C#也引入了事件触发机制。在上一
节的内容中我们简要介绍了委托,事件本质上源于委托,是一种特殊的委托,它
为委托提供了封装性,一方面允许从类的外部增删绑定方法,另一方面又严禁从
类的外部触发委托所绑定的方法。
我们的目的是快速开发上位机,因此在使用过程中完全可以将事件理解为中
断,事件回调函数就是我们常说的中断服务函数。同时一般情况下我们也不需要
自己封装事件,调用控件已经封装好的事件函数即可。因此本章就不再做过多的
代码实例讲解,事件的使用以及注意点将在上位机实战章节中做具体的介绍。

14
C#上位机实战开发指南

第三章 Windows 窗体程序

3.1 第一个窗体程序

3.1.1 新建本地工程文件夹

为了使工程易于管理,我们首先在电脑本地新建一个文件夹用于存放整个共
工程,比如命名为”DEMO”。注意文件夹名虽然根据喜好命名,但最好不要使用
中文命名,因为有时候如果上位机需要加载本地文件遇到中文名必须要转码,比
如加载本地 URL 时路径存在中文就相对麻烦,因此我建议使用英文命名。

3.1.2 新建工程

启动 VS2015,新建一个工程,如图 3-1 所示。

图 3-1:在 VS2015 中新建工程

1. 选择.NET 版本
通常我们可以选择.NET2.0 或者.NET4.0,.NET3.x 版本兼容性差,BUG 较多,
几乎没有人使用,.NET4.5 以上版本太高,不再支持 Windows XP。
一般情况下,Windows XP 已经很少使用.NET4.0,而.NET2.0 可以流畅的在
Windows XP 及其以上版本系统上运行,即使在企业级开发中 4.0 也已经算非常高
的版本了,因此出于兼容性的考虑,建议选择.NET2.0 进行开发。当然在讲到波
形绘制项目时,我们会优先选择.NET4.0。

15
C#上位机实战开发指南

2. 选择项目类别
上位机开发选择 Windows 窗体应用程序。

3. 命名项目名
建议使用英文名命名,避免路径中出现中文,这里我取名”Demo”。

图 3-2:创建项目

我们将新建的工程放置于 DEMO 文件夹中即可。此时 VS2015 切换至窗体设


计器界面。

4. 控件及属性栏介绍
Windows 窗体程序离不开控件的使用,在新建好工程后我们就需要找到
Windows 原生的控件库。
在界面左边的工具箱中就存放着所有我们可能会用到的 Windows 原生控件。
一般情况工具箱会自动隐藏,考虑到设计界面时我们需要比较大的空间去揣摩界
面设计思路,因此建议使用时都将工具箱隐藏,用到时再选择控件即可。
当然出于个人喜好又或者开发电脑有一个非常大的屏幕,我们也可以将工具
栏显示在设计器界面中。
属性栏一般在界面右下角,使用频率非常高,控件样式,事件的注册等都需
要在属性一栏中进行设置。

16
C#上位机实战开发指南

右上角解决方案管理器是整个工程文件的结构脉络。
设计器界面如图 3-3 所示。

图 3-3:设计器界面

控件库界面如图 3-4 所示。

图 3-4:控件栏

3.1.3 项目代码区

在解决方案管理器中选中 Form1.cs 文件右击出现查看代码选项,单击即可进


入窗体代码区。
同时资源管理器中还有 Program.cs 文件,在第二章我们已经大概了解了

17
C#上位机实战开发指南

Program.cs 的代码结构。通常我们几乎不会去修改 Program.cs 文件中的代码,所


以我们暂时忽略。
一般上位机代码的主体全部在 Form1.cs 文件中。我们在第二章分析命名空间
时已经查看了相关代码。查看代码步骤以及窗体代码如图 3-5,3-6 所示。

图 3-5:查看步骤

图:3-6:窗体代码

3.1.4 调试窗体程序

VS2015 的调试功能非常强大,我们经常用到的并不会太多,和单片机在线仿
真类似,通常使用打断点单步调试,查看变量值,调用堆栈等功能。
当然我们也可以实时查看 CPU 的使用率,每一句代码所运行的时间,这些可

18
C#上位机实战开发指南

以方便我们优化代码结构以及算法。

图 3-7:启动调试

当按下启动调试后窗体程序也就弹出,此时即可打断点调试。最终的软件则
在 Debug 文件夹中生成。

图 3-8:exe 生成目录

3.2 Windows 控件简述

3.2.1 控件概述

在学习 emWin 时我们已经接触过控件的概念,控件是用户可以操作的窗体


内部对象。
我非常喜欢将控件比喻成电子元器件,窗体比喻成 PCB 板框,那么此时程序
代码自然也就等价于布线。我们完全可以将上位机的开发过程当作一次 PCB 板的
设计过程。

3.2.2 添加控件

我们可以通过三种方式添加控件至窗体,这三种方式分别是“在窗体绘制”,
“拖动至窗体”,“程序添加”。这三种方式是等效的,我们最常使用第二种方

19
C#上位机实战开发指南

式,傻瓜式操作,直接在工具箱选中控件然后拖动到窗体内部即可。

3.3 常用控件的使用方法

3.3.1 文本标签控件(Label)

Label 控件主要用于显示一些不能编辑的文本,文本的显示本质上是修改了
Label 的 Text 属性,例如我们需要上位机实时显示下位机传来的温度数据,那么
只要将温度值转换为字符串格式赋值给 Label 的 Text 属性即可。
下面详细介绍 Label 控件的基本使用方法。

1. 设置标签文本
我们可以通过 2 种方式设置 Label 控件的显示文本:第一种是直接在属性面
板中设置 Text 属性,第二种是通过代码设置 Text 属性。

◇ 属性面板设置
我们从工具箱中将 Label 控件拖至窗体中,选中 Label 控件后在属性面板中
找到 Text 属性输入你要显示的文本,比如“C#上位机实战开发指南”。操作流
程如图 3-9 所示。

图 3-9:属性面板设置 Label 属性

◇ 代码设置
通常固定不变的文本我们在属性面板中设置一次即可,但如果是实时动态刷
新的数据则需要通过代码设置修改。代码清单如 3-1 所示。
代码 3-1:代码设置 Label 的 Text 属性

1. label1.Text = "C#上位机实战开发指南"; //代码设置 Text 属性

20
C#上位机实战开发指南

在第二章介绍类时我们已经了解到类就相当于结构体,通过代码清单 3-1 可
以看出访问类的内部成员和结构体一样都是通过”.”来访问。注意 C#对类内部
成员的访问不可以使用”->”。

2. 修改 Label 文本字体大小以及颜色
通常为了字体美观,我们会放弃默认字体显示格式,重新设置新的显示样式。
同样颜色和字体也有 2 种方式修改:第一种在属性面板直接修改,第二种通过代
码修改。一般颜色和字体样式决定后就不再变,因此我们大多数都是在属性面板
中一次设置成功。设置方法同设置 Text 文本属性一致。
选中 Label 后在属性面板中找到属性 Font 即可修改字体大小以及风格等。
找到 ForeColor 即可修改 Label 文本的显示颜色。如图 3-10 所示。

图 3-10:文本样式设置

3.3.2 按钮控件(Button)

按钮 Button 控件的用法非常简单,和 Label 一样文本样式都可以被修改,


这里便不再对文本样式做过多介绍。
我们使用过的上位机中按钮 Button 通常用作打开或者关闭串口,又或者清
空缓存,清空计数等,这都由于 Button 可以触发 Click 事件。那么我们如何为
Button 绑定 Click 事件呢?通常我们将 Button 拖至窗体中布好局后双击 Button
就可以自动注册 Click 单击事件,同时 VS2015 自动跳至 Click 事件的函数体内。
在上一节中我们已经学会了 Label 的使用方法,现在利用按钮 Button 来修
改 label1 的显示文本。label1 的默认文本为:“C#上位机实战开发指南”,由
Button 触发 Click 事件修改其文本为:“Button1_Click 事件成功触发”。
我们先将 Button 拖至窗体并为其注册 Click 事件(注册方式请看上文)。

21
C#上位机实战开发指南

最后我们在 Click 事件回调函数内书写代码修改 label1 的 Text 属性。完整代码


请看代码清单 3-2:Button 单击 Click 事件。
代码清单 3-2:Button 单击 Click 事件

1. private void button1_Click(object sender, EventArgs e)


2. {
3. label1.Text = "Button1_Click 事件成功触发";
4. }

注意此函数为注册后自动生成的,而不是手动输入,只有函数体内部修改文
本语句为手动添加的。

3.3.3 文本框控件(TextBox)

上位机通常都会有 2 个文本框,一个用作接收区,一个用作发送区。顾名思
义他们的作用也就是用于显示文本的。不同于 Label 控件,TextBox 允许运行中由
用户修改即我们可以随时通过键盘增删文本内容。
默认情况下,TextBox 只单行显示,如果将属性 Multiline 设置为 ture,那么
此时 TextBox 便支持多行显示。
TextBox 的方法中最常用的是 AppendText 方法,它的作用是将新的文本数据
从末尾处追加至 TextBox 中。当 TextBox 一直追加文本后就会带来本身长度不够
无法显示全部文本的问题,此时我们需要使能 TextBox 的纵向滚动条来跟踪显示
最新文本,所以我们将属性 ScrollBars 的值设置为:Vertical 即可。
紧接着上一个案例,我们再为窗体添加一个 TextBox 控件,设置 Multiline 属
性为 true,并将滚动条设置为纵向滚动。同时在 button1 的单击事件中往 TextBox
追加文本:“C#上位机实战开发指南\r\n”。详细代码见代码清单 3-3。
代码清单 3-3:TextBox 追加文本案例

1. private void button1_Click(object sender, EventArgs e)


2. {
3. label1.Text = "Button1_Click 事件成功触发";
4. textBox1.AppendText("C#上位机实战开发指南\r\n");
5. }

运行效果如图 3-11 所示。

22
C#上位机实战开发指南

图 3-11:TextBox 追加文本

3.3.4 下拉组合框控件(ComboBox)

下拉组合框控件通常用于选择串口号,波特率等功能,它由 2 部分组成,第
一部分是允许用户输入修改的文本框,第二部分是列表框,它提供给用户有限的
选择项。通常 ComboBox 也有两种使用模式,一种是 DropDown,这种模式下用
户既可以在下拉列表中选择所需要的选择项,又可以在列表没有所需选择项的情
况下手动输入,例如某上位机的波特率选择列表框只有“9600”一项,此时只要
ComboBox 工作在 DropDown 模式下就可以输入任意你想要的波特率。那么相反
另一种 DropDownList 模式则受到限制,无法手动输入新的选择项。设置 ComboBox
的属性 DropDownStyle 便可以切换 ComboBox 的下拉模式。那么如何为 ComboBox
添加下拉选项呢?C#为我们提供了两个方法,第一种是单个添加的方法,第二种
是批量添加的方法,下面详细介绍这两种方法。
◇ 单个添加
单个添加方法操作简单,直接传入字符串即可,同样我们紧接着之前的案例
使用按钮 button1 的 Click 事件进行添加单个下拉选项,代码清单如下。
代码清单 3-4:ComboBox 单个添加下拉选项

1. private void button1_Click(object sender, EventArgs e)


2. {
3. comboBox1.Items.Add("下拉选项 1");
4. comboBox1.Items.Add("下拉选项 2");
5. }

23
C#上位机实战开发指南

◇ 批量添加
批量添加时首先要定义好一个字符串数组,然后将数组名传入即可完成批量
添加。通常上位机在启动时会获取当前电脑中所有的串口,然后将串口号缓存至
定义的字符串数组中,最后批量传入 ComboBox。使用方法见代码清单 3-5。
代码清单 3-5:ComboBox 批量添加

1. private void button1_Click(object sender, EventArgs e)


2. {
3. //单个添加
4. comboBox1.Items.Add("下拉选项 1");
5. comboBox1.Items.Add("下拉选项 2");
6.
7. //批量添加
8. string[] Com = new string[3]; //C#数组定义方式
9. Com[0] = "下拉选项 3";
10. Com[1] = "下拉选项 4";
11. Com[2] = "下拉选项 5";
12. comboBox1.Items.AddRange(Com); //批量导入
13. }

在代码清单中我们看到一个全新的字符串数组定义方式,其中 new 就是实例


化,也就是实际要分配内存的,当然 new 的使用方法还有很多,但上位机使用不
多,因此这里便不再做过多讲解,请读者自行百度。效果图如下图所示。

图 3-12:ComboBox 下拉选择项添加

24
C#上位机实战开发指南

3.3.5 复选框控件(CheckBox)

复选框控件一般用于使能或失能某项功能,比如上位机是否开启时间戳显示
功能。通常我们会为 CheckBox 注册一个 Click 或者 Mouse_UP 事件,注册方式我
们采用在属性面板注册的方式,这里为 CheckBox 注册一个 Click 事件。注册步骤
如下图所示。

图 3-13:CheckBox 事件注册

选中文本框区域后鼠标双击即可注册好 Click 事件,同时 VS2015 自动跳转至


事件回调函数内部。事件注册好后如下图所示。

图 3-14:Click 事件成功注册

单击事件注册好之后我们就可以在 CheckBox1_Click 事件回调函数体内对


CheckBox1 的选中状态进行读取判断即判断 CheckBox 的 Checked 属性,如果复选
框被选中则 Checked 的值为 true,那么相反复选框未被选中则 Checked 的值为
false。
注意默认情况下 Checked 的值为 false,因此当我们将复选框拖进窗体时默认
未被选中。
C 复选框 CheckBox 的 Checked 属性设置方式如图 3-15 所示。

25
C#上位机实战开发指南

图 3-15:Checked 属性

紧接着上一个案例,我们将 CheckBox 拖入窗体中并为其注册单击 Click 事件,


然后对选中状态进行判断并弹窗提示。详细代码见代码清单 3-6。
代码清单 3-6:CheckBox 使用方式

1. private void checkBox1_Click(object sender, EventArgs e)


2. {
3. //如果 CheckBox 被选中
4. if (checkBox1.Checked)
5. {
6. MessageBox.Show("选中");//消息提示框,用于调试
7. //也可标志位置位
8. }
9. if (!checkBox1.Checked)
10. {
11. MessageBox.Show("未选中");//消息提示框,用于调试
12. }
13. }

在代码清单中,我们可以看到 MessageBox 的使用,MessageBox 主要用于打


印一些调试信息或者一些异常信息,我们可以将 MessageBox 理解为串口打印调
试。调用 Show 方法传入要显示的字符串即可。当然 MessageBox 有很多的重载
参数,我们只要传入一个调试信息就足够了。代码运行效果如图 3-16 所示。

26
C#上位机实战开发指南

图 3-16:CheckBox 被选中

3.3.6 单选按钮控件(RadioButton)

RadioButton 单选按钮控件使用方式和 CheckBox 类似,但 CheckBox 允许多个


进行复选,每个控件之间不存在互斥关系。而 RadioButton 则不允许多选,当用
户选中其中某一个后,其它几个的选中状态便全部为 false。
比如上位机串口接收方式,16 进制和字符串便不能同时存在,同一时间只能
存在一种接收方式,这种情况下就不适合使用 CheckBox,只有 RadioButton 具有
互斥关系,因此我们通常使用 RadioButton。RadioButton 控件如图 3-16 所示。

图 3-16:RadioButton

注意 RadioButton 并不是所有的都会互斥,只有在同一容器下才会互斥。因
此 RadioButton 在使用时都要进行分组分容器。一般情况下我们会使用 Panel 容
器控件进行分组。

27
C#上位机实战开发指南

3.3.7 容器控件(Panel)

容器控件主要用于为其它控件提供一个可识别的分组,比如上一节中我们讲
到的 RadioButton 单选按钮控件。Panel 就好比商场中的楼层,1 楼卖化妆品,2
楼卖电子产品一样。
在将 Panel 拖进窗体中时选中区域外会有一个虚线框包围,但在窗体运行后
虚线框消失。Panel 控件如图 3-17 所示。

图 3-17:Panel 控件

如上图所示虚线框即为 Panel,每个 Panel 都包围了 4 个 RadioButton 控件,


因此在这种情况下只有 Panel 内部的单选按钮互斥。

3.3.8 定时器控件(Timer)

严格来说 Timer 是一个组件并不能算作是控件,因为控件是可视的,能在窗


体中看到这个控件的样式,而组件不行,组件不需要绘制,不能在窗体中显示出
来。因此我们可以将组件理解为一个抽象的控件。
添加 Timer 控件的方式和普通控件一致,只是添加后控件出现在设计器下方,
如图 3-18 所示。

图 3-18:Timer 定时器控件

既然 Timer 是定时器,那就有一个定时的时长,通过设置 Timer 的 Interval


属性就可以设置定时的时长。注意 Interval 的单位是毫秒(ms),默认为 100 毫

28
C#上位机实战开发指南

秒触发一次 Tick 事件。启动定时器使用 Start 方法,停止定时器使用 Stop 方法。


定时器使用方式基本和单片机的定时器类似。
我们使用一个 button 作为定时器 timer1 的开关,并通过属性面板为该 butoon
的 Text 属性设置为“开始”,当启动定时器后将 Text 属性改为“停止”,并在
Tick 事件中定时改变 label2 的文本来验证 Timer 的使用方式。我们在属性面板中
设置定时周期为 1 秒,并为其注册 Tick 事件,并且同时为 button2 注册 Click 事
件。代码见清单 3-7。
代码清单 3-7:定时器的使用

1. int Cnt = 0; //全局变量,用于计时显示


2. private void timer1_Tick(object sender, EventArgs e)
3. {
4. Cnt++;
5. label2.Text = Cnt.ToString();
6. //ToString 是将实际数值变为字符串形式的方法
7. //等价于单片机中加上:0x30
8. }
9.
10. private void button2_Click(object sender, EventArgs e)
11. {
12. if (button2.Text == "开始")
13. {
14. timer1.Start(); //开启定时器
15. button2.Text = "停止";
16. }
17. else
18. {
19. if (button2.Text == "停止")
20. {
21. timer1.Stop(); //停止定时器
22. button2.Text = "开始";
23. }
24. }
25. }

代码中 ToString 方法用于将数字变为字符串,比如上位机获取到传感器数据


如果要显示在 Label 标签上则必须要使用 ToString 方法。除非协议中传感器的数
据已经是字符串格式,当然一般数传协议很少会使用这种方式。ToString 方法还
可以格式化输出字符串只需要在括号中填入参数。整段代码逻辑非常清晰,易于
理解。Timer 在上位机中可以用于定时发送功能的实现,也可以用作超时处理机
制的实现。
button2_Click 事件内部用于开关定时器,通过按钮的文本来选择何时打开定
时器,何时关闭定时器。代码运行效果如图 3-19 所示。

29
C#上位机实战开发指南

图 3-19:Timer 运行效果

图中 Label2 在 Tick 事件下每秒自增 1。

3.3.9 串口控件(SerialPort)

串口控件和定时器一样,不需要绘制,因此无法将其添加至窗体中,只能和
定时器一样显示在设计器下方,如图 3-20 所示。

图 3-20:添加串口控件

串 口常 用 的 属性 有 2 个 , 一 个是 端 口 号( PortName ) ,一 个 是 波特 率
(BaudRate),其它停止位,校验位等默认即可。串口打开与关闭都有接口可以
直接调用。串口同时还有一个 IsOpen 属性,IsOpen 为 true 则表示串口已经打开,
IsOpen 为 false 则表示串口已经关闭。
串口控件的使用是整个上位机开发的基础,因此在接下来的几个小结我将详
细介绍串口控件的使用方法。
下面我从两个方面开始介绍。

1. 串口发送
串口发送方法也有 2 种,一种是字符串发送 WriteLine,一种是 16 进制发送,

30
C#上位机实战开发指南

Write。其中字符串发送 WriteLine 默认已经在末尾添加换行符。串口开关发送示


例见代码清单 3-8。
代码清单 3-8:串口开关发送

1. byte[] SendByte = new byte[5];//数组定义


2. SendByte[0] = 1;
3. SendByte[1] = 2;
4. SendByte[2] = 3;
5. SendByte[3] = 4;
6. SendByte[4] = 5;
7. serialPort1.Open(); //打开串口
8. serialPort1.Close(); //关闭串口
9. serialPort1.Write(SendByte, 0, 5);//发送 16 进制
10. serialPort1.WriteLine("字符串发送");//字符串发送

其中字符串发送直接传入要发送的字符串即可,最终发出去的自带换行符。
16 进制发送一共有 3 个参数,第一个参数是要发送的缓存也就是数组名,第二
个是偏移量即从缓存中第几个字节开始发送,第三个是发送的字节数。

2. 串口接收
使用串口接收之前先要为串口控件注册一个 Receive 事件,作用等价于串口
接收中断。然后在中断内部对缓冲区的数据进行读取。
串口接收与发送相同都有 2 种方法,一个是 16 进制方式读,一个是字符串
方式读。首先我们先注册串口接收事件,如图 3-21 所示。

图 3-21:串口接收事件注册

在注册好后接收事件之后,我们即可在事件中读取缓冲区中的数据,见代码
清单 3-9。
代码清单 3-9:串口数据读取

1. //串口接收事件,相当于单片机串口接收中断
2. private void serialPort1_DataReceived(object sender, System.IO.Ports.SerialD
ataReceivedEventArgs e)
3. {
4. //字符串方式读
5. string str = serialPort1.ReadExisting();//字符串方式读
6. //16 进制读

31
C#上位机实战开发指南

7. int n = serialPort1.BytesToRead; //获取缓冲区中的字节数


8. byte[] Buf = new byte[n]; //定义一个以有效字节数为大小的缓存
9. serialPort1.Read(Buf, 0, n); //读取至缓存
10. }

其中定义有效字节数为大小的缓存相当于动态分配,非常方便。Read 方法有
三个参数,第一个是缓存数组,第二个是偏移量,第三个是写入字节数。通常从
缓存头开始一直写满为止。

3.3.10 Windows 常用控件使用总结

Windows 原生控件的使用非常简单,上位机开发所涉及到的控件并不多,所
谓熟能生巧,在多次使用之后便会非常熟悉。关于其它未曾讲解的控件,读者可
以自行百度学习,或者去 MSDN 官网学习。在对某个控件或者对某个方法的使用
不熟悉时可在代码区选中它,按下“F1”,浏览器将自动跳转至微软官方的学习
介绍网页。
注意在使用 Button 等可以添加 Click 事件的控件时,一般我们都使用 MouseUp
鼠标弹起事件来代替 Click 事件,因为 Click 事件的触发条件比较特殊,而鼠标弹
起则每次都会触发。

32

You might also like