WPF基础 | WPF 基础概念全解析:布局、控件与事件
- 一、前言
- 二、WPF 布局系统
- 2.1 布局的重要性与基本原理
- 2.2 常见布局面板
- 2.3 布局的测量与排列过程
- 三、WPF 控件
- 3.1 控件概述与分类
- 3.2 常见控件的属性、方法与事件
- 3.3 自定义控件
- 四、WPF 事件
- 4.1 路由事件概述
- 4.2 事件处理方式
- 4.3 事件处理的最佳实践
- 结束语
- 优质源码分享
WPF基础 | WPF 基础概念全解析:布局、控件与事件
, 本文全面深入地解析 Windows Presentation Foundation(WPF)中的基础概念,重点聚焦于布局、控件与事件。首先介绍 WPF 的背景及其在 Windows 应用程序开发领域的重要性,随后详细阐述布局系统,包括各种布局面板的特点与用法、布局的测量与排列过程等。接着对 WPF 中的常见控件进行分类介绍,分析其属性、方法和事件,以及如何自定义控件。最后深入探讨 WPF 的事件机制,包括路由事件的类型、事件处理的方式以及事件在实际应用中的最佳实践。通过对这些基础概念的透彻理解,为开发人员熟练运用 WPF 构建功能强大、交互性强且界面美观的 Windows 应用程序奠定坚实基础。
一、前言
在数字浪潮汹涌澎湃的时代,程序开发宛如一座神秘而宏伟的魔法城堡,矗立在科技的浩瀚星空中。代码的字符,似那闪烁的星辰,按照特定的轨迹与节奏,组合、交织、碰撞,即将开启一场奇妙且充满无限可能的创造之旅。当空白的文档界面如同深邃的宇宙等待探索,程序员们则化身无畏的星辰开拓者,指尖在键盘上轻舞,准备用智慧与逻辑编织出足以改变世界运行规则的程序画卷,在 0 和 1 的二进制世界里,镌刻下属于人类创新与突破的不朽印记。
在 Windows 应用程序开发的演进历程中,Windows Presentation Foundation(WPF)的出现具有标志性意义。传统的 Windows 开发技术,如 Windows Forms,在应对日益复杂的用户界面需求时逐渐显现出局限性。随着用户对应用程序界面的美观性、交互性以及数据展示与处理的高效性要求不断提高,WPF 应运而生。
WPF 是微软.NET 框架下专门用于创建 Windows 客户端应用程序的技术框架。它整合了丰富的图形、媒体、文档处理能力以及强大的用户界面设计功能,采用了全新的编程模型和架构设计。通过 XAML(eXtensible Application Markup Language)与 C# 或其他.NET 语言的结合,开发人员能够实现界面设计与业务逻辑的分离,大大提高了开发效率和代码的可维护性。在当今的软件开发环境中,无论是企业级的大型业务应用系统,还是面向消费者的多媒体娱乐应用,WPF 都成为了构建高质量 Windows 应用程序的有力工具。
WPF从入门到精通专栏,旨在为读者呈现一条从对 WPF(Windows Presentation Foundation)技术懵懂无知到精通掌握的学习路径。首先从基础入手,介绍 WPF 的核心概念,涵盖其独特的架构特点、开发环境搭建流程,详细解读布局系统、常用控件以及事件机制等基础知识,帮助初学者搭建起对 WPF 整体的初步认知框架。随着学习的深入,进阶部分聚焦于数据绑定、样式模板、动画特效等关键知识点,进一步拓展 WPF 开发的能力边界,使开发者能够打造出更为个性化、交互性强的桌面应用界面。高级阶段则涉及自定义控件开发、MVVM 设计模式应用、多线程编程等深层次内容,助力开发者应对复杂的业务需求,构建大型且可维护的应用架构。同时,通过实战项目案例解析,展示如何将所学知识综合运用到实际开发中,从需求分析到功能实现再到优化测试,全方位积累实践经验。此外,还探讨了性能优化、与其他技术集成以及安全机制等拓展性话题,让读者对 WPF 技术在不同维度有更深入理解,最终实现对 WPF 技术的精通掌握,具备独立开发高质量桌面应用的能力。
🛕 点击进入WPF从入门到精通专栏
二、WPF 布局系统
2.1 布局的重要性与基本原理
- 重要性
良好的布局是构建用户友好型应用程序的关键。在 WPF 中,布局决定了各个 UI 元素在窗口或页面中的位置和大小关系。合理的布局能够使应用程序的界面更加清晰、美观,方便用户操作和信息获取。例如,在一个数据管理应用程序中,布局需要将数据表格、输入框、按钮等元素有序地组织起来,避免界面的混乱和拥挤,提高用户的工作效率。
对于不同分辨率和屏幕尺寸的设备,灵活的布局能够确保应用程序的适应性。WPF 的布局系统允许开发人员创建响应式的界面,使得应用程序在桌面电脑、平板电脑甚至智能手机等多种设备上都能呈现出良好的视觉效果和可用性。
- 基本原理
WPF 布局基于容器和子元素的概念。容器负责管理子元素的布局,子元素向容器提供自身的布局需求信息。布局过程是一个递归的过程,从根元素开始,依次处理每个容器及其子元素。在布局时,容器首先会对其子元素进行测量,子元素返回其期望的大小,这个大小通常基于其内容、样式设置以及自身的布局约束等。然后容器根据自身的布局策略对这些测量结果进行整理和计算,确定每个子元素最终的位置和大小。例如,一个文本框的大小可能取决于其文本内容的长度、字体大小以及是否设置了固定宽度等因素,而容器(如网格面板)则会根据自身的行和列设置来安排文本框在界面中的位置。
2.2 常见布局面板
Grid
布局面板
特点与用法:Grid
是 WPF 中最为常用且功能强大的布局面板之一。它通过定义行和列来组织子元素,可以实现复杂而灵活的布局结构。开发人员可以为行和列指定不同的高度和宽度,可以是固定值、比例值(使用星号 “*” 表示)或者自动适应内容的值(使用 “Auto” 表示)。例如,在一个登录界面中,可以使用 Grid
将用户名输入框、密码输入框、登录按钮等元素分别放置在不同的行和列中,并且可以设置列的宽度比例,使界面在不同分辨率下都能保持较好的布局效果。
示例代码:
<Grid><Grid.RowDefinitions><RowDefinition Height="Auto"/><RowDefinition Height="Auto"/><RowDefinition Height="*"/></Grid.RowDefinitions><Grid.ColumnDefinitions><ColumnDefinition Width="Auto"/><ColumnDefinition Width="*"/></Grid.ColumnDefinitions><TextBlock Text="Username:" Grid.Row="0" Grid.Column="0"/><TextBox Grid.Row="0" Grid.Column="1"/><TextBlock Text="Password:" Grid.Row="1" Grid.Column="0"/><PasswordBox Grid.Row="1" Grid.Column="1"/><Button Content="Login" Grid.Row="2" Grid.Column="1" HorizontalAlignment="Right"/>
</Grid>
在上述示例中,Grid
定义了三行两列,第一行和第二行的高度自动适应内容,第三行占据剩余空间。第一列宽度自动适应内容,第二列占据剩余宽度。通过 Grid.Row
和 Grid.Column
属性将各个子元素放置在相应的网格单元中。
StackPanel
布局面板
特点与用法:StackPanel
按照水平或垂直方向依次堆叠其子元素。当子元素添加到 StackPanel
中时,它们会按照设定的方向紧密排列。水平方向的 StackPanel
适用于创建横向排列的菜单、工具条等;垂直方向的 StackPanel
常用于构建列表式的布局,如新闻列表、联系人列表等。例如,在一个简单的音乐播放器界面中,可以使用垂直方向的 StackPanel
来排列播放列表中的歌曲条目,每个条目包含歌曲名称、歌手等信息,并且可以方便地添加滚动条以适应较多的歌曲数量。
示例代码:
xml
此示例创建了一个垂直方向的 StackPanel
,其中包含三个按钮,它们依次垂直排列。
Canvas
布局面板
特点与用法:Canvas
提供了绝对定位的布局方式。开发人员可以精确地指定子元素在 Canvas
中的坐标位置(使用 Canvas.Left
和 Canvas.Top
属性)。这种布局方式适用于需要精确控制元素位置的场景,如绘制图形、创建自定义的可视化界面等。例如,在一个简单的绘图应用程序中,可以使用 Canvas
作为容器,然后在其中放置各种形状元素(如矩形、圆形等),并通过指定坐标来确定它们的位置和相互关系,绘制出复杂的图形图案。
示例代码:
xml
2.3 布局的测量与排列过程
- 测量阶段
在布局的测量阶段,容器会遍历其所有的子元素,并调用子元素的 Measure 方法。子元素在 Measure 方法中根据自身的内容、样式和布局约束计算出其期望的大小,并将这个大小信息返回给容器。例如,一个包含较长文本内容的文本框,会根据文本的长度、字体大小以及是否设置了最大宽度等因素计算出其期望的宽度和高度。容器收集所有子元素的期望大小信息,但此时并不确定子元素的最终位置。
- 排列阶段
测量阶段完成后,进入排列阶段。容器根据自身的布局策略以及子元素的测量结果,调用子元素的 Arrange 方法来确定子元素的最终位置和大小。例如,对于 Grid 布局面板,会根据行和列的定义以及子元素在网格中的设置,将子元素放置在相应的网格单元中,并调整其大小以适应网格单元的尺寸。在排列过程中,子元素的实际大小可能会与测量阶段返回的期望大小不同,这取决于容器的布局规则和可用空间。例如,如果一个容器的宽度有限,而某个子元素的期望宽度较大,容器可能会根据其布局策略(如按比例分配空间)来调整子元素的实际宽度。
三、WPF 控件
3.1 控件概述与分类
- 概述
WPF 控件是构建用户界面的基本单元,它们封装了特定的功能和外观表现。控件可以响应用户的操作,如鼠标点击、键盘输入等,并通过事件机制将这些操作信息传递给应用程序的逻辑层进行处理。例如,按钮控件可以在被点击时触发 Click 事件,文本框控件可以接收用户的键盘输入并显示相应的文本内容。
- 分类
🚩 按钮类控件:包括 Button
(普通按钮)、RadioButton
(单选按钮)、CheckBox
(复选框)等。Button
主要用于触发特定的操作,如提交表单、打开新窗口等;RadioButton
用于在一组互斥的选项中选择一个;CheckBox
则可以用于多选操作,如选择多个兴趣爱好或功能选项等。
🚩 文本类控件:如 TextBox
(文本框)、PasswordBox
(密码框)、RichTextBox(富文本框)等。TextBox
用于接收用户输入的普通文本信息;PasswordBox
专门用于输入密码,其文本内容以掩码形式显示以保护隐私;RichTextBox
则支持更丰富的文本格式,如字体设置、段落排版、插入图片等,适用于创建文档编辑类应用程序。
🚩 列表类控件:例如 ListBox
(列表框)、ComboBox
(组合框)、ListView
(列表视图)等。ListBox
显示一个可滚动的列表,用户可以从中选择一个或多个项目;ComboBox
是一种下拉式列表,平时只显示一个选中项,点击下拉箭头可以展开列表进行选择;ListView 则提供了更强大的列表展示功能,如多列显示、分组显示等,常用于数据展示和管理应用程序。
🚩 容器类控件:除了前面提到的布局面板(如 Grid
、StackPanel
、Canvas
等),还包括 TabControl
(选项卡控件)、GroupBox
(分组框控件)等。TabControl 可以将不同的内容页面组织在不同的选项卡下,方便用户在有限的空间内切换查看不同的信息;GroupBox 用于将相关的控件组合在一起,形成一个逻辑分组,提高界面的可读性和可维护性。
3.2 常见控件的属性、方法与事件
- Button 控件
🌜 属性:Content 属性用于设置按钮上显示的文本或内容(可以是文本、图像或其他 UI 元素);IsEnabled 属性控制按钮是否可用,当设置为 false 时,按钮呈现灰色不可点击状态;Width 和 Height 属性可以指定按钮的大小;Background
和 Foreground
属性分别用于设置按钮的背景颜色和前景颜色(即文本颜色)等。
🌜 方法:Click
方法可以在代码中手动触发按钮的点击事件,这在某些情况下(如模拟用户操作进行自动化测试等)非常有用。
🌜 事件:Click 事件是 Button
控件最常用的事件,当用户点击按钮时触发。例如,可以在 Click 事件处理方法中编写代码来执行特定的业务逻辑,如保存数据、打开新窗口等。
- TextBox 控件
🌜 属性:Text 属性获取或设置文本框中的文本内容;MaxLength 属性可以限制用户输入的最大字符数;IsReadOnly
属性设置文本框是否只读,当设置为 true 时,用户不能编辑文本框中的内容;FontFamily
、FontSize
等属性用于设置文本的字体和大小等显示样式。
🌜 方法:SelectAll
方法可以选中文本框中的所有文本内容,这在一些需要用户快速替换或操作全部文本的场景中很方便;AppendText
方法可以在文本框现有文本的末尾添加新的文本内容。
🌜 事件:TextChanged
事件在文本框中的文本内容发生变化时触发,开发人员可以在该事件处理方法中实时监控用户输入,并进行相应的处理,如数据验证、自动保存等;LostFocus
事件在文本框失去焦点时触发,可用于在用户完成输入后进行一些后续处理,如数据格式检查等。
- ListBox 控件
🌜 属性:ItemsSource
属性用于绑定数据源,将数据集合显示在列表框中;SelectedItem
属性获取当前选中的项目;SelectedIndex
属性获取当前选中项目的索引;DisplayMemberPath
属性指定在列表框中显示数据源中对象的哪个属性作为列表项的文本内容等。
🌜 方法:ClearSelected
方法可以清除列表框中的所有选中项;SelectAll
方法可以选中列表框中的所有项目(如果支持多选)。
🌜 事件:SelectionChanged
事件在列表框的选中项发生变化时触发,通过该事件可以获取新选中的项目信息,并进行相应的业务逻辑处理,如根据选中项显示详细信息、更新相关数据等。
3.3 自定义控件
- 自定义控件的需求与场景
在实际应用中,WPF 提供的内置控件可能无法满足特定的界面设计和功能需求。例如,在一个专业的图形设计应用程序中,可能需要自定义一个具有特殊绘图功能和外观的工具按钮;在一个企业级的业务流程管理应用程序中,可能需要自定义一个符合企业特定数据展示和操作要求的列表控件。自定义控件可以使应用程序具有独特的用户界面风格和更贴合业务需求的功能。
- 自定义控件的创建步骤
继承现有控件或基类:可以选择继承 WPF 中的现有控件类,如 Button
、TextBox
等,然后在继承的基础上扩展其功能和修改外观。例如,如果要创建一个具有特殊样式的按钮,可以继承 Button
类,然后重写其模板(Template
)属性来定义新的外观样式。也可以继承更基础的 Control
类或 FrameworkElement
类,从头开始构建自定义控件,这种方式提供了更大的灵活性,但也需要更多的开发工作。
定义属性、方法和事件:根据自定义控件的功能需求,定义相应的属性、方法和事件。例如,如果自定义一个数据输入控件,可能需要定义一个属性来指定数据的类型要求,一个方法来验证输入的数据是否符合要求,以及一个事件在数据验证失败时触发。
设计控件的外观:使用 XAML
和样式(Style
)、模板(Template
)等技术来设计自定义控件的外观。可以在控件的模板中定义各种视觉元素,如形状、颜色、布局等,以实现独特的界面效果。例如,在自定义按钮的模板中,可以使用矩形、椭圆等形状元素组合成一个独特的按钮外观,并设置相应的动画效果,使按钮在点击时有动态的视觉反馈。
测试与优化:创建完自定义控件后,需要进行充分的测试,确保其功能正常,外观在不同的场景和分辨率下都能正确显示。根据测试结果对自定义控件进行优化,调整其性能、兼容性等方面的问题,使其能够稳定地集成到应用程序中。
四、WPF 事件
4.1 路由事件概述
- 路由事件的概念与特点
WPF 采用路由事件机制来处理事件,这与传统的.NET 事件有所不同。路由事件可以在元素树中进行传播,它具有冒泡和隧道两种传播方式。冒泡事件是从事件源开始,沿着元素树向上传播,即从子元素向父元素传播。例如,当点击一个按钮时,按钮的 Click
事件会先在按钮自身触发,然后依次向上传播到按钮所在的容器、窗口等父元素。这种传播方式使得父元素可以统一处理多个子元素的相同类型事件,提高了代码的复用性和可维护性。
隧道事件则是从元素树的根元素开始,沿着元素树向下传播到事件源。隧道事件通常用于预览事件,例如在一个窗口中,可以在窗口级别处理鼠标按下的隧道事件,以便在事件到达具体的子元素之前进行一些全局的处理,如判断是否满足某些条件才允许事件继续向下传播到子元素进行具体的处理。隧道事件的名称通常以 “Preview”
开头,如 PreviewMouseDown
事件。
- 路由事件在元素树中的传播过程
以一个简单的 WPF 窗口为例,窗口中包含一个网格面板,网格面板中有一个按钮。当按钮被点击时,首先触发按钮自身的 Click
事件(这是一个冒泡事件),然后该事件会沿着元素树向上冒泡,依次到达网格面板和窗口。如果在窗口级别处理了按钮的 Click
事件,那么事件的传播就会停止,除非在事件处理方法中明确指示事件继续传播。对于隧道事件,如 PreviewMouseDown
事件,首先会在窗口级别触发,然后向下传播到网格面板,最后到达按钮。在传播过程中,每个元素都有机会处理该事件,可以根据需要在特定的元素上注册事件处理方法来拦截和处理事件。
4.2 事件处理方式
- 在 XAML 中声明事件处理程序
在 XAML
中声明事件处理程序是一种简单直观的方式。例如,对于一个按钮的 Click
事件,可以在 XAML
中这样声明:<Button Click="Button_Click">Click Me</Button>
,其中 “Button_Click”
是在代码后置文件中定义的事件处理方法。这种方式将事件与对应的处理方法进行了关联,使得界面设计与事件处理逻辑有了清晰的连接。在 XAML
中声明事件处理程序时,还可以传递事件参数,如<Button Click="Button_Click">Click Me</Button>
,在事件处理方法中可以通过参数获取事件的相关信息,如事件源对象、鼠标点击的位置等。
- 在代码后置文件中编写事件处理方法
在代码后置文件中编写事件处理方法提供了更强大的编程逻辑控制能力。以按钮的 Click 事件为例,在代码后置文件(如 MainWindow.xaml.cs
)中,事件处理方法需要遵循特定的签名格式,如private void Button_Click(object sender, RoutedEventArgs e)
,其中 “sender”
表示事件源对象,“e” 表示事件参数,包含了关于事件的详细信息,如事件发生的位置、鼠标按键状态等。在事件处理方法中,可以编写任意的 C# 代码来实现业务逻辑,如更新数据、调用其他方法、打开新窗口等。例如:
private void Button_Click(object sender, RoutedEventArgs e)
{// 在这里处理按钮点击后的业务逻辑MessageBox.Show("按钮被点击了!");
}
除了处理单个控件的事件,在代码后置文件中还可以方便地进行事件的注册与注销。例如,可以在窗口的加载事件(Loaded
)中注册多个控件的事件处理方法,在窗口关闭事件(Closing
)中注销这些事件处理方法,以确保资源的正确管理和避免内存泄漏。
4.3 事件处理的最佳实践
- 合理利用事件冒泡与隧道机制
在处理复杂的 UI 布局和多个控件的事件时,要善于利用事件的冒泡和隧道机制。例如,在一个包含多个按钮的容器中,如果需要对所有按钮的点击事件进行统一的日志记录,可以在容器级别处理按钮的 Click 事件(利用事件冒泡),而不是为每个按钮单独编写事件处理方法。这样可以减少代码的冗余,提高代码的可维护性。对于一些需要全局控制或预处理的事件,如鼠标按下事件,可以在根元素(如窗口)级别处理隧道事件(PreviewMouseDown),进行权限验证、全局状态更新等操作,然后再根据需要决定是否允许事件继续传播到具体的子元素进行处理。
- 避免过度嵌套事件处理
虽然 WPF 的事件机制提供了强大的功能,但过度嵌套事件处理可能会导致代码的复杂性增加和性能下降。例如,在一个深度嵌套的元素树中,如果每个元素都对同一事件进行处理并修改事件参数或执行复杂的逻辑,可能会导致事件传播的延迟和难以调试的问题。因此,应尽量将事件处理逻辑集中在合适的层次上,避免在不必要的元素上进行事件处理。如果确实需要在多个层次上处理事件,要确保每个层次的事件处理逻辑清晰、简洁,并且不会相互干扰。
- 正确处理事件参数与数据传递
事件参数在事件处理中起着重要的作用。在事件处理方法中,要正确地使用事件参数来获取与事件相关的信息,如鼠标位置、键盘按键等。同时,在某些情况下,可能需要在事件处理过程中传递额外的数据。可以通过自定义事件参数类来实现,继承自 RoutedEventArgs
或其相关子类,并添加自定义的属性来存储需要传递的数据。例如,在一个数据列表的 SelectionChanged
事件处理中,如果需要传递与所选数据项相关的详细信息到其他方法中进行处理,可以创建一个自定义的事件参数类,包含所选数据项的引用或相关属性,然后在事件触发时传递该自定义事件参数。这样可以避免使用全局变量或复杂的数据查找机制来传递事件相关的数据,提高代码的可读性和可维护性。
结束语
通过对 WPF 中布局、控件与事件这些基础概念的全面深入解析,我们可以看到 WPF 为 Windows 应用程序开发提供了一套强大而灵活的工具集。布局系统的多样性和灵活性使得开发人员能够创建出适应各种场景和设备的用户界面;丰富的控件库涵盖了从基本的输入输出到复杂的数据展示和交互功能,并且可以通过自定义控件进一步满足特殊需求;而独特的路由事件机制则为处理用户交互和系统事件提供了高效、可扩展的方式。在实际的 WPF 应用程序开发过程中,深入理解和熟练运用这些基础概念是构建高质量、功能丰富、用户体验良好的应用程序的关键。无论是开发小型的工具软件还是大型的企业级应用系统,WPF 的布局、控件与事件相关知识都将为开发人员提供坚实的技术支撑,帮助他们在 Windows 应用程序开发领域中创造出更加出色的作品,满足不断增长的用户需求和业务挑战,推动 Windows 应用程序开发技术的不断发展和创新。
在开发过程中,遵循 MVVM 设计模式和合理使用资源字典等最佳实践,可以使代码结构更加清晰、易于维护和扩展,提高开发团队的协作效率。同时,了解并掌握数据绑定不生效、布局异常等常见问题的解决方法,能够帮助开发人员快速排除故障,减少开发时间和成本。
无论是开发简单的工具类应用程序,还是复杂的企业级桌面软件,WPF 都能够满足各种需求,并能够与其他微软技术如 WCF、Entity Framework 等无缝集成,进一步拓展了其应用范围和功能深度。随着技术的不断发展和应用场景的不断拓展,WPF 将继续在桌面应用开发领域发挥重要作用,为用户带来更加优质的桌面应用体验。对于希望深入学习桌面应用开发的读者来说,WPF 无疑是一个值得深入探索和掌握的技术框架,它将为个人的技术成长和职业发展提供坚实的基础和广阔的空间。
亲爱的朋友,无论前路如何漫长与崎岖,都请怀揣梦想的火种,因为在生活的广袤星空中,总有一颗属于你的璀璨星辰在熠熠生辉,静候你抵达。
愿你在这纷繁世间,能时常收获微小而确定的幸福,如春日微风轻拂面庞,所有的疲惫与烦恼都能被温柔以待,内心永远充盈着安宁与慰藉。
至此,文章已至尾声,而您的故事仍在续写,不知您对文中所叙有何独特见解?期待您在心中与我对话,开启思想的新交流。
优质源码分享
-
【百篇源码模板】html5各行各业官网模板源码下载
-
【模板源码】html实现酷炫美观的可视化大屏(十种风格示例,附源码)
-
【VUE系列】VUE3实现个人网站模板源码
-
【HTML源码】HTML5小游戏源码
💞 关注博主 带你实现畅游前后端
🏰 大屏可视化 带你体验酷炫大屏
💯 神秘个人简介 带你体验不一样得介绍
🎀 酷炫邀请函 带你体验高大上得邀请
① 🉑提供云服务部署(有自己的阿里云);
② 🉑提供前端、后端、应用程序、H5、小程序、公众号等相关业务;
如🈶合作请联系我,期待您的联系。
注:本文撰写于CSDN平台,作者:xcLeigh(所有权归作者所有) ,https://blog.csdn.net/weixin_43151418,如果相关下载没有跳转,请查看这个地址,相关链接没有跳转,皆是抄袭本文,转载请备注本文原地址。
亲,码字不易,动动小手,欢迎 点赞 ➕ 收藏,如 🈶 问题请留言(评论),博主看见后一定及时给您答复,💌💌💌
原文地址:https://blog.csdn.net/weixin_43151418/article/details/144661030(防止抄袭,原文地址不可删除)