WPF框架---MvvmLight介绍

目录

1. MvvmLight 框架准备

2. MvvmLight 中的相关基类

3. MvvmLight 中的数据绑定与通知

a. 核心功能

b. 关键方法与属性

c. 完整示例

d. 高级用法

4. MvvmLight 中的命令对象

a. 命令对象的作用

b. 核心接口:ICommand

c. MvvmLight 中的 RelayCommand

d. 动态更新命令的可执行状态

e. 高级用法

5. Messenger 对象使用

a. Messenger 的核心作用

b. MvvmLight 中的 Messenger 实现

c. 基本使用场景

d. 高级用法

6. DispatcherHelper 对象使用

7. SimpleIoc 对象使用

a. SimpleIoc 的核心作用

b. 核心方法与属性

c. 基本使用步骤

d. 高级用法

e. 与 ViewModelLocator 结合使用


1. MvvmLight 框架准备

  • 作用: 快速搭建 MVVM 架构的应用程序,简化数据绑定、命令和消息传递。
  • 步骤: 通过 NuGet 安装 MvvmLightLibs 包。

2. MvvmLight 中的相关基类

  • 核心基类:
    • ViewModelBase: ViewModel 基类,实现 INotifyPropertyChanged
public class MainViewModel : ViewModelBase
{private string _name;public string Name{get => _name;set => Set(ref _name, value); // 自动触发 PropertyChanged}
}
    • ObservableObject: 轻量级可观察对象。
    • RelayCommand: 命令对象基类。

3. MvvmLight 中的数据绑定与通知

a. 核心功能

ViewModelBase 继承自 ObservableObject,并实现了 INotifyPropertyChanged 接口,主要负责:

  • 属性变更通知:当 ViewModel 的某个属性值发生变化时,自动通知 UI 更新。
  • 简化代码:通过 Set 方法简化属性定义,避免手动触发 PropertyChanged 事件。
  • 设计模式支持:提供静态属性 IsInDesignMode,用于区分代码是在设计时(如 Visual Studio 设计器)还是运行时执行。
b. 关键方法与属性

(1) Set<T>(ref T field, T value, [CallerMemberName] string propertyName = null)

  • 作用:在属性的 set 方法中调用此方法,自动比较新旧值,若不同则更新字段并触发 PropertyChanged 事件。
  • 示例
private string _name;
public string Name
{get => _name;set => Set(ref _name, value); // 自动触发通知
}

(2)RaisePropertyChanged(string propertyName)

  • 作用:手动触发某个属性的 PropertyChanged 事件。
  • 场景:当某个属性的值依赖于其他属性时,手动通知 UI 更新。
public string FullName => $"{FirstName} {LastName}";private string _firstName;
public string FirstName
{get => _firstName;set{Set(ref _firstName, value);RaisePropertyChanged(nameof(FullName)); // 通知 FullName 属性变化}
}

(3) IsInDesignMode

  • 作用:静态属性,判断当前代码是否在设计器(如 Visual Studio 或 Blend)中运行。
  • 用途:在设计时提供假数据,避免调用真实服务或数据库。
public class MainViewModel : ViewModelBase
{public MainViewModel(){if (IsInDesignMode)Name = "Design Mode Sample"; // 设计器显示假数据elseLoadRealData(); // 运行时加载真实数据}
}
c. 完整示例
using GalaSoft.MvvmLight;public class UserViewModel : ViewModelBase
{private string _userName;private int _age;public string UserName{get => _userName;set => Set(ref _userName, value);}public int Age{get => _age;set => Set(ref _age, value);}// 计算属性(依赖其他属性)public string UserInfo => $"{UserName} (Age: {Age})";// 当 UserName 或 Age 变化时,手动通知 UserInfo 更新protected override void OnPropertyChanged(string propertyName = null){base.OnPropertyChanged(propertyName);if (propertyName == nameof(UserName) || propertyName == nameof(Age))RaisePropertyChanged(nameof(UserInfo));}
}
d. 高级用法

(1)批量通知多个属性

通过 RaisePropertyChanged(null) 或指定空字符串,通知所有属性更新(慎用,可能影响性能):

public void ResetAllProperties()
{_userName = "Default";_age = 0;RaisePropertyChanged(""); // 通知所有属性更新
}

(2) 继承与扩展

可继承 ViewModelBase 并添加通用逻辑(如日志记录、验证):

public abstract class CustomViewModelBase : ViewModelBase
{protected void LogPropertyChange(string propertyName){Debug.WriteLine($"属性 {propertyName} 已更新");}public override void RaisePropertyChanged(string propertyName = null){base.RaisePropertyChanged(propertyName);LogPropertyChange(propertyName);}
}


4. MvvmLight 中的命令对象

a. 命令对象的作用
  • 解耦 UI 与业务逻辑:将用户操作(如点击按钮)映射到 ViewModel 的方法。
  • 控制可执行状态:根据条件动态启用或禁用 UI 元素(例如按钮的 IsEnabled)。
  • 支持参数传递:允许从 UI 传递参数到 ViewModel(如选中项的 ID)。
b. 核心接口:ICommand

所有命令对象均实现 System.Windows.Input.ICommand 接口,其定义如下:

public interface ICommand
{event EventHandler CanExecuteChanged; // 通知命令可执行状态变化bool CanExecute(object parameter);    // 判断命令是否可执行void Execute(object parameter);       // 执行命令逻辑
}
c. MvvmLight 中的 RelayCommand

MvvmLight 提供 RelayCommandRelayCommand<T> 类,简化了 ICommand 的实现。

(1) 基本用法(无参数)

public class MainViewModel : ViewModelBase
{public RelayCommand SaveCommand { get; }public MainViewModel(){// 初始化命令:绑定方法 + 可执行条件SaveCommand = new RelayCommand(SaveData, CanSave);}private void SaveData(){// 保存逻辑}private bool CanSave(){return !string.IsNullOrEmpty(Name); // 仅当 Name 非空时按钮可用}
}

(2) 支持参数传递(RelayCommand<T>)

public RelayCommand<string> FilterCommand { get; }public MainViewModel()
{FilterCommand = new RelayCommand<string>(param => ApplyFilter(param), param => !string.IsNullOrEmpty(param));
}private void ApplyFilter(string keyword)
{// 根据关键字过滤数据
}
d. 动态更新命令的可执行状态

(1) 手动触发更新

CanExecute 依赖的属性变化时,调用 RelayCommandRaiseCanExecuteChanged 方法:

private string _name;
public string Name
{get => _name;set{Set(ref _name, value);SaveCommand.RaiseCanExecuteChanged(); // 触发重新检查 CanSave}
}

(1) 自动触发更新

利用 ViewModelBaseSet 方法自动触发属性变更通知,无需手动调用 RaiseCanExecuteChanged

private string _name;
public string Name
{get => _name;set => Set(ref _name, value); // Set 方法已自动触发 PropertyChanged
}// CanSave 方法中依赖 Name 属性
private bool CanSave() => !string.IsNullOrEmpty(Name);
e. 高级用法

(1) 异步命令

直接在命令中执行异步操作时,需注意线程安全(通过 DispatcherHelper):

public RelayCommand LoadDataCommand { get; }public MainViewModel()
{LoadDataCommand = new RelayCommand(async () => await LoadDataAsync());
}private async Task LoadDataAsync()
{try{IsLoading = true;var data = await _dataService.GetData();// 更新 UI(确保在 UI 线程)DispatcherHelper.CheckBeginInvokeOnUI(() => DataList = data);}finally{IsLoading = false;}
}

(2) 复合命令

将多个命令组合成一个逻辑操作:

public RelayCommand SubmitAllCommand { get; }public MainViewModel()
{SubmitAllCommand = new RelayCommand(() =>{SaveCommand.Execute(null);LogCommand.Execute(null);}, () => SaveCommand.CanExecute(null) && LogCommand.CanExecute(null));
}

(3) 命令的泛型约束

public RelayCommand<int> DeleteItemCommand { get; }public MainViewModel()
{DeleteItemCommand = new RelayCommand<int>(id => DeleteItem(id), id => id > 0);
}private void DeleteItem(int itemId)
{// 删除指定 ID 的项
}

5. Messenger 对象使用

MVVM 模式 中,Messenger 是一个用于实现 松耦合通信 的核心组件,尤其在 MvvmLight 框架 中被广泛使用。它允许不同组件(如 ViewModel、View、服务)之间通过消息进行通信,而无需直接引用彼此,从而降低依赖、提升代码可维护性。

a. Messenger 的核心作用
  • 解耦组件通信:组件无需持有对方引用,通过消息订阅/发布机制交互。
  • 跨层级传递数据:例如从子 ViewModel 通知父 ViewModel,或跨页面传递状态。
  • 支持复杂场景:如广播通知、请求-响应模式、事件聚合等。
b. MvvmLight 中的 Messenger 实现

MvvmLight 的 Messenger 类是一个静态单例(Messenger.Default),提供消息的注册、发送和注销功能。其核心方法如下:

方法

作用

Register<TMessage>(object recipient, Action<TMessage> action)

订阅类型为 TMessage

的消息。recipient

为接收者标识(通常为 this

)。

Send<TMessage>(TMessage message)

发送一条 TMessage

类型的消息,所有订阅者会收到通知。

Unregister(object recipient)

注销某个接收者的所有消息订阅,避免内存泄漏。

c. 基本使用场景

(1) 发送简单通知消息

  • 场景:ViewModel A 完成数据加载后,通知 ViewModel B 刷新界面。

发送消息方

// 发送一个无参数通知
Messenger.Default.Send(new NotificationMessage("DataLoaded"));

接收消息方(ViewModel B)

public ViewModelB()
{// 注册接收 NotificationMessage 类型的消息Messenger.Default.Register<NotificationMessage>(this, message =>{if (message.Notification == "DataLoaded"){RefreshData();}});
}

(2) 传递数据对象

  • 场景:用户选择某条数据后,跨页面传递选中项。

定义消息类型

public class ItemSelectedMessage
{public int ItemId { get; set; }public ItemSelectedMessage(int id) => ItemId = id;
}

发送方:

// 用户选择某项后发送消息
Messenger.Default.Send(new ItemSelectedMessage(selectedItem.Id));

接收方:

Messenger.Default.Register<ItemSelectedMessage>(this, message =>
{LoadItemDetails(message.ItemId); // 根据 ItemId 加载详情
});
d. 高级用法

(1) 消息令牌(Token)

  • 作用:区分相同消息类型的不同用途,避免消息冲突。

发送带令牌的消息

// 发送消息时指定令牌
Messenger.Default.Send(new NotificationMessage("UpdateChart"), "ChartToken" // 令牌标识
);

接收指定令牌的消息

Messenger.Default.Register<NotificationMessage>(this, message => UpdateChartData(), "ChartToken" // 仅接收带有此令牌的消息
);

(2) 泛型消息

  • 场景:传递强类型数据,避免类型转换。

定义泛型消息

public class GenericMessage<T>
{public T Content { get; }public GenericMessage(T content) => Content = content;
}

发送泛型消息

var data = new List<User>();
Messenger.Default.Send(new GenericMessage<List<User>>(data));

接收泛型消息

Messenger.Default.Register<GenericMessage<List<User>>>(this, message =>
{UserList = message.Content; // 直接获取 List<User>
});

(3) 双向通信(请求-响应模式)

  • 场景:ViewModel A 请求数据,ViewModel B 响应返回结果。

发送请求消息

public class DataRequestMessage
{public Action<string> Callback { get; set; } // 定义回调委托
}// 发送请求,并注册回调
Messenger.Default.Send(new DataRequestMessage 
{ Callback = response => HandleResponse(response) 
});

接收请求并响应

Messenger.Default.Register<DataRequestMessage>(this, message =>
{var data = FetchDataFromService(); // 获取数据message.Callback?.Invoke(data);    // 执行回调
});

6. DispatcherHelper 对象使用

  • 作用: 在非 UI 线程更新 UI。
  • 初始化:
DispatcherHelper.Initialize(); // 在 App.xaml.cs 中调用
  • 使用:
Task.Run(() =>{// 后台线程操作DispatcherHelper.CheckBeginInvokeOnUI(() =>{// 更新 UI 元素StatusText = "Processing...";});});

7. SimpleIoc 对象使用

SimpleIocMvvmLight 框架中提供的一个轻量级依赖注入(Dependency Injection, DI)容器,用于管理应用程序中各个组件(如 ViewModel、服务、数据源)的依赖关系。它通过 控制反转(IoC)依赖注入 机制,帮助开发者实现代码解耦、提高可测试性和可维护性。

a. SimpleIoc 的核心作用
  • 解耦组件依赖:将类的依赖关系从代码中抽离,通过容器统一管理。
  • 单例生命周期管理:默认以单例模式提供实例,避免重复创建对象。
  • 简化实例获取:通过接口或类型直接获取已注册的实例。
  • 支持构造函数注入:自动解析构造函数参数,完成依赖注入。
b. 核心方法与属性

以下是 SimpleIoc 的常用方法及其功能:

方法/属性

作用

Register<TInterface, TClass>()

注册接口 TInterface

和其实现类 TClass

Register<TClass>()

直接注册类型 TClass

(无接口)。

GetInstance<T>()

获取类型 T

的实例(已注册的接口或类)。

ContainsCreated<T>()

检查类型 T

的实例是否已被创建。

Reset()

重置容器,清空所有注册和实例(用于测试或重新初始化)。

IsRegistered<T>()

检查类型 T

是否已注册。

c. 基本使用步骤

(1) 注册服务与 ViewModel

在应用程序启动时(如 ViewModelLocatorApp.xaml.cs 中),注册所有依赖项:

public class ViewModelLocator
{public ViewModelLocator(){// 注册服务(接口 + 实现类)SimpleIoc.Default.Register<IDataService, DataService>();// 注册 ViewModel(无接口)SimpleIoc.Default.Register<MainViewModel>();}// 提供 ViewModel 实例的公共属性public MainViewModel Main => SimpleIoc.Default.GetInstance<MainViewModel>();
}

(2) 获取实例

通过 GetInstance<T> 方法获取已注册的实例:

// 获取 ViewModel 实例
var mainVM = SimpleIoc.Default.GetInstance<MainViewModel>();// 获取服务实例
var dataService = SimpleIoc.Default.GetInstance<IDataService>();

(3) 构造函数注入

当注册的类(如 ViewModel)依赖其他服务时,SimpleIoc 会自动解析构造函数参数:

public class MainViewModel : ViewModelBase
{private readonly IDataService _dataService;// 构造函数依赖注入:自动传入已注册的 IDataService 实例public MainViewModel(IDataService dataService){_dataService = dataService;}
}
d. 高级用法

(1) 单例模式 vs. 瞬时模式

单例模式(默认):整个应用程序生命周期内只创建一个实例。

// 默认单例模式
SimpleIoc.Default.Register<IDataService, DataService>();

瞬时模式:每次调用 GetInstance 时创建新实例。

SimpleIoc.Default.Register<IDataService, DataService>(createInstanceImmediately: false);
var service = SimpleIoc.Default.GetInstance<IDataService>(); // 每次返回新实例

(2) 手动指定实例

允许直接注册一个已存在的对象实例:

var logger = new FileLogger();
SimpleIoc.Default.Register<ILogger>(() => logger); // 注册现有实例

(3) 依赖覆盖

在测试中,可以替换实现类以注入 Mock 对象:

// 生产环境注册真实服务
SimpleIoc.Default.Register<IDataService, DataService>();// 测试环境覆盖为 Mock 服务
SimpleIoc.Default.Unregister<IDataService>();
SimpleIoc.Default.Register<IDataService, MockDataService>();
e. 与 ViewModelLocator 结合使用

ViewModelLocator 是 MvvmLight 中用于集中管理 ViewModel 的类,通常与 SimpleIoc 配合使用,通过 XAML 绑定 ViewModel。

(1) 定义 ViewModelLocator

public class ViewModelLocator
{public ViewModelLocator(){// 注册服务和 ViewModelSimpleIoc.Default.Register<IDataService, DataService>();SimpleIoc.Default.Register<MainViewModel>();}// 暴露 ViewModel 属性供 XAML 绑定public MainViewModel Main => SimpleIoc.Default.GetInstance<MainViewModel>();
}

(2) 在 XAML 中声明资源

App.xaml 中合并 ViewModelLocator

<Application.Resources><ResourceDictionary><vm:ViewModelLocator x:Key="Locator" /></ResourceDictionary>
</Application.Resources>

(3) 在 View 中绑定 ViewModel

<Window DataContext="{Binding Main, Source={StaticResource Locator}}"><!-- UI 元素绑定到 MainViewModel 的属性 -->
</Window>


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/897201.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【Linux】线程同步与互斥

线程同步与互斥 一.线程互斥1.互斥相关概念2.互斥锁 Mutex3.互斥锁接口4.互斥锁实现原理5.互斥锁封装 二.线程同步1.同步相关概念2.条件变量 Condition Variable3.条件变量接口4.条件变量封装5.信号量 Semaphore6.信号量接口7.信号量封装8.生产者 - 消费者模型1.基于 Blocking …

URIError: URI malformed

&#x1f90d; 前端开发工程师、技术日更博主、已过CET6 &#x1f368; 阿珊和她的猫_CSDN博客专家、23年度博客之星前端领域TOP1 &#x1f560; 牛客高级专题作者、打造专栏《前端面试必备》 、《2024面试高频手撕题》、《前端求职突破计划》 &#x1f35a; 蓝桥云课签约作者、…

linux c++11 gcc4 环境编译安装googletest/gtest v1.10

c11对应googletest/gtest 经过测试&#xff0c;c11对应版本是googletest v1.10.x 编译安装 编译环境 sudo apt-get update sudo apt-get install -y build-essential cmake下载或git clone代码 git clone https://github.com/google/googletest.git cd googletest git che…

鸿蒙与DeepSeek深度整合:构建下一代智能操作系统生态

前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。点击跳转到网站。 https://www.captainbed.cn/north 目录 技术融合背景与价值鸿蒙分布式架构解析DeepSeek技术体系剖析核心整合架构设计智能调度系统实现…

极狐GitLab 17.9 正式发布,40+ DevSecOps 重点功能解读【二】

GitLab 是一个全球知名的一体化 DevOps 平台&#xff0c;很多人都通过私有化部署 GitLab 来进行源代码托管。极狐GitLab 是 GitLab 在中国的发行版&#xff0c;专门为中国程序员服务。可以一键式部署极狐GitLab。 学习极狐GitLab 的相关资料&#xff1a; 极狐GitLab 官网极狐…

LeetCode - 28 找出字符串中第一个匹配项的下标

题目来源 28. 找出字符串中第一个匹配项的下标 - 力扣&#xff08;LeetCode&#xff09; 题目解析 暴力解法 本题如果采用暴力解法的话&#xff0c;可以定义两个指针 i&#xff0c;j&#xff0c;其中 i 指针用于扫描 S&#xff08;haystack&#xff09;串&#xff0c;j 指针…

Spring Boot 异步编程

文章目录 一、异步方法的使用1. 开启异步支持2. 定义异步方法3. 调用异步方法踩坑记录心得体会 二、线程池配置1. 自定义线程池2. 使用自定义线程池踩坑记录心得体会 三、异步任务的监控与管理1. 日志记录2. 异常处理3. 线程池监控踩坑记录心得体会 在现代应用程序开发中&#…

0.大模型开发知识点需求综述

文章目录 一、机器学习与深度学习基础二、自然语言处理&#xff08;NLP&#xff09;基础三、大模型架构四、训练优化技术五、数据处理与预处理六、分布式训练与并行化策略七、微调方法与参数高效微调八、训练框架、工具与自动化流程九、评估与部署十、前沿技术与未来趋势 已更新…

docker目录挂载与卷映射的区别

在 Docker 中&#xff0c;目录挂载&#xff08;Bind Mount&#xff09;和卷映射&#xff08;Volume Mount&#xff09;的命令语法差异主要体现在路径格式上&#xff0c;具体表现为是否以斜杠&#xff08;/&#xff09;开头。以下是两者的核心区别及使用场景的总结&#xff1a; …

[Java基础-线程篇]7_线程设计模式与总结

摘要&#xff1a;懒汉单例模式怎么变得线程安全&#xff1f;Master-Worker归并模式&#xff0c;工作窃取算法。Java线程相关源码使用了什么设计模式&#xff1f; 资料引用&#xff1a;《Java高并发核心编程卷2》 目录 线程安全的单例模式 Master-Worker模式 工作窃取算法 …

在【k8s】中部署Jenkins的实践指南

&#x1f407;明明跟你说过&#xff1a;个人主页 &#x1f3c5;个人专栏&#xff1a;《Kubernetes航线图&#xff1a;从船长到K8s掌舵者》 &#x1f3c5; &#x1f516;行路有良友&#xff0c;便是天堂&#x1f516; 目录 一、引言 1、Jenkins简介 2、k8s简介 3、什么在…

代码随想录算法营Day59 | 寻找存在的路径, 冗余连接,冗余连接II

寻找存在的路径 这题使用并查集即可。并查集加路径压缩。 #include <iostream> using namespace std; int find(int* father,int u){return father[u] u ? u : father[u] find(father,father[u]); }bool isSame(int* father,int u,int v){return find(father,u) fi…

正则表达式(2)匹配规则

正则表达式的匹配规则定义了如何识别字符串中的特定模式。这些规则包括字符类匹配、元字符匹配、数量词、字符转义和分组。 字符类匹配 字符类匹配允许你指定一个字符集合&#xff0c;并匹配该集合中的任意单个字符。这是通过方括号 [] 来实现的。 简单字符类&#xff1a;[abc…

【QWEN】机器人控制器的控制周期越短精度越高吗

好的&#xff0c;用户问的是“机器人控制器的控制周期越短精度越高吗”。我需要根据提供的知识库内容来回答这个问题。首先&#xff0c;我得回忆一下知识库里的相关信息。 知识库里有几篇相关的文章。比如&#xff0c;[3]提到控制周期越短&#xff0c;控制性能的上限越高&…

[总概]Vue2/3React Diff算法

根据搜索结果,大部分文档都详细描述了Vue的Diff算法原理、优化策略以及实现细节。例如,网页1详细介绍了Vue Diff算法的核心设计,包括双端比较和key的作用;Vue3中的快速Diff算法; 通常,解释一个算法可以从其基本原理、核心策略、优化手段、源码实现以及应用场景等方面展开…

【MySQL_03】数据库基本--核心概念

文章目录 一、数据库基础1.1 数据库基础定义1.2 数据库分类与典型产品1.3 数据库模型1.4 数据库层次结构1.5 数据库核心机制1.6 数据表和视图1.61 数据表&#xff08;Table&#xff09;1.62 视图&#xff08;View&#xff09; 1.7 键类型1.8 MySQL数据类型1.9 数据库范式化 二、…

FreeRTOS第16篇:FreeRTOS链表实现细节04_为什么FreeRTOS选择“侵入式链表”

文/指尖动听知识库-星愿 文章为付费内容,商业行为,禁止私自转载及抄袭,违者必究!!! 文章专栏:深入FreeRTOS内核:从原理到实战的嵌入式开发指南 1 传统链表 vs. 侵入式链表 在嵌入式系统中,内存和性能的优化至关重要。FreeRTOS选择侵入式链表而非传统链表,其背后是内…

STM32读写片内FLASH 笔记

文章目录 前言STM32F105的内部ROM分布STM32F10x的闪存擦写解锁FPECMain FLASH 的编写 main Flash的擦除注意点 前言 在通过OTA的方式对设备进行升级&#xff0c;若在使用内部FLASH装载固件程序的方式下&#xff0c;需要擦写 内部FLASH 从而实现把新的固件程序写入到 内部FLASH…

Python爬虫实战:爬取财金网实时财经信息

注意:以下内容仅供技术研究,请遵守目标网站的robots.txt规定,控制请求频率避免对目标服务器造成过大压力! 一、引言 在当今数字化时代,互联网数据呈爆炸式增长,其中蕴含着巨大的商业价值、研究价值和社会价值。从金融市场动态分析到行业趋势研究,从舆情监测到学术信息收…

3.3.2 用仿真图实现点灯效果

文章目录 文章介绍Keil生成.hex代码Proteus仿真图中导入.hex代码文件开始仿真 文章介绍 点灯之前需要准备好仿真图keil代码 仿真图参考前文&#xff1a;3.3.2 Proteus第一个仿真图 keil安装参考前文&#xff1a;3.1.2 Keil4安装教程 keil新建第一个项目参考前文&#xff1a;3.1…