9-2 【操作】WPF 基础入门
新建一项目
Create a new project - WPF Application (A project for creating a .NET Core WPF Application) - Next - .NET 5.0 (Current) - Create
项目创建完成,VS自动打开 GUI用户界面,格式是 .xaml文件,跟xml差不多,也属于xml文件的类别。
虽然xaml是WPF用户的底层标准,但是VS已帮我们全部封装好了,可以直接拖拽布局界面。
9-3 【理论】XAML页面剖析
MainWindow.xaml代码示例
<Window x:Class="WpfApp.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WpfApp"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Grid>
<Button Height="50" Width="100" Content="CLick me 4" />
<d:Button>
<Button.Height>50</Button.Height>
<Button.Width>100</Button.Width>
<Button.Content>
<TextBlock>Click me 4</TextBlock>
</Button.Content>
</d:Button>
</Grid>
</Window>
上面标红的<Button> 相当于下面的7行代码(当然不能有 d:),因为<Button ……> 的内部属性可以通过“元素嵌套”的方式声明。
<Window ……> 根元素介绍
xmlns 是 xml-namespace 的缩写,用于定义命名空间的。上面看到的是MS系统默认的命名空间,可以使用这个命名空间下的所有UI控件。
比如这里的 Button TextBlock,另外当来源不同的类重名时,也可以通过命名空间区分不同的UI控件。
xmlns 是默认的命名空间,这种不带任何映射参数的命名空间,整个页面只能有一个,一般使用元素最频繁使用的命名空间。
比如这里的 Window Grid Button TextBlock,都来自 xmlns 。
接着,还能看到带有映射前缀的:x 、:d 等的命名空间,这些命名空间都与解析 xaml语言相关,
比如这里的 x:Class 来自 xmlns:x 这个命名空间,它将会指定当前页面所对应的 cs 文件,以及当前页面所对应的类的名称。
比如这里的 xmlns:local代表当前应用的命名空间,即项目名称:WpfApp,而
Title="MainWindow" Height="450" Width="800" 分别定义了视窗的标题、高度和宽度。
mc:Ignorable="d"这个命名空间稍微有点费解,它的名称叫 Ignorable,即可以忽略的,表示在运行过程中可以被忽略的控件,而这些可忽略的控件使用 d: 来引导。即运行时忽略,上面布局中有两个 Button ,运行时,下面的Button不会显示的。
MainWindow.xaml.cs(UI页面逻辑代码)代码示例
……
namespace WpfApp
{
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
var grid = (Grid)this.Content;
Button button = new Button();
button.Height = 50;
button.Width = 100;
Margin = new Thickness(0, 0, 700, 385);
button.Content = "Button !!!";
grid.Children.Add(button);
}
}
}
x:Class="WpfApp.MainWindow" 对应上面的 namespace WpfApp 和 class MainWindow。
构造函数 public MainWindow() 在程序启动时调用。InitializeComponent();帮助完成页面初始化工作(比如初始化标题、字体大小等)。
<Grid>就是页面的内容,cs中 this.Content 访问的<Grid>元素,cs中 var grid = (Grid)this.Content; 下面就是用 代码的方式添加Button按钮。
纯粹使用 cs 来完成用户界面的做法已经非常过时了,主流的界面开发工作更加倾向于MVC(Model View Controller),即:数据模型-视图-控制逻辑 三者分离的模式。
建议使用 xaml 来进行页面设计,而通过 cs 来控制页面和数据的动态变化。
9-4 【拓展】MVC 架构
什么是MVC
·软件工程的架构方式
·模型(Model)、视图(View)和控制器(Controller)
·分离业务操作、数据显示、逻辑控制
从而使用同一个程序可以使用不同的表现形式!
请问:Model就是用来访问数据库的吗?
对于整个MVC架构,最难理解的就是Model模型了。
很多程序员在最开始使用MVC框架时,都会误认为Model就是用来访问数据库索取数据的,而最重要的业务逻辑却放在控制器(Controller)里,最终写出来的系统结构,就变成了:View用来显示界面、Controller做核心业务、Model仅仅用来访问数据库,做数据变化而已。很明显,这是混淆了另一种软件开发模式:三层架构。
三层架构 和 MVC 有些类似,也是分为三个部分。
三层架构
·UI层,表示用户界面
·BLL(Business Logic Layer)业务逻辑层,处理核心业务以及数据封装
·DAL(Data Access Layer)层,表示数据访问
这种三层架构曾经红极一时,不过现在逐渐被 MVC、DDD(领域驱动)、Microservices(微服务)等架构取代了。
简单来说,MVC 与 三层架构 除了都分为三个部分以外,也没什么共同点了!
MVC 与 三层架构
·三层架构面向接口编程,而三个层级之间的完全解耦、完全可替换
·MVC的每个部分都是紧密结合的,它的核心并不是解耦,而是重用
也就是说,MVC中同样的Model可以适配于不同的控制器,搭配不同的视图用来显示不同的内容,然而系统最核心的逻辑始终包含在模型(Model)中,可以被重复使用。
三层架构所描述的是自下而上,具有明显层级的架构。首先,得有数据库,根据数据库来创建DAL(Data Access Layer) 来获取和映射数据;得到原始数据后传递给BLL(Business Logic Layer)业务逻辑层,进行数据验证、数据转换、对象封装;最后封装好的数据传递给UI层,显示给用户。
MVC架构各个组成部分是水平架构的,只有调用关系,没有层级关系。所有的数据流动和显示,都是通过数据绑定事件驱动所处理的。
首先,应该确定核心业务模型(Model),通过Model来创建数据库;
第二,用户发起请求,将请求传递给控制器;
第三,控制器调用模型;
第四,模型获取数据,对数据做出验证,并将转换好的数据交还给Controller,业务逻辑也发生在这里;
第五,Controller把数据交给视图,视图向用户展示数据。
所以,MVC架构中一定要知道自己的核心业务是什么,所有业务都要围绕它展开,不要混淆三层架构设计理念。
为什么要使用MVC呢?因为有各种好处:
MVC的优点
第一、耦合性低
视图层和业务层可以分离,这样就允许修改视图层代码,而不需要重新编译模型和控制器的代码;比如,改写 jsp、html、css 或者 js 代码,并不需要重启服务器。同样,一个业务流程或业务规则发生改变,只需要改变MVC的模型,因为模型与控制器、视图相分离,所以很容易改变应用程序的数据层 和 业务规则。
第二、可复用性高
MVC的各种组件都具有高可复用性,随着技术的不断进步需要越来越多的方式来访问应用程序,MVC模式允许各种各样不同样式的视图来访问同一个服务端的代码,多个视图可以共享一个模型。
比如用户可以通过 Web应用,也可以通过手机App来订购某样产品,虽然订购的方式不一样,但处理订购产品的业务是一样的,由于模型访问数据并没有发生改变,所以同样的功能可以被不同的界面使用。
第三、高可维护性
MVC也具有高可维护性,分离视图和业务逻辑,可以使得Web应用更容易维护和修改,比如,如果想更改业务逻辑,只需要更改业务逻辑,……,这样的好处就是后期维护成本降低,新功能的增加、代码的扩展也非常方便。
MVC的缺点
第一、定义不明确,学习曲线陡
大家都是依照自己经验来解释和使用MVC,而MVC内部原理比较复杂,组合多种设计模式,所以完全理解MVC并不是很容易,需要花一些时间去思考,尤其对新手来说,有一定的学习曲线
第二、结构复杂
MVC并不适合小型,甚至中等规模的应用程序,对于简单的页面,严格遵循MVC,反而会增加结构的复杂性,可能产生过多的数据操作,导致运行效率低下
第三、数据流动效率低
依据模型操作接口的不同,视图可能需要多次调用才能获得足够的数据显示,对于未变化的数据频繁地访问模型,也可能会造成操作性能的下降,所以从数据操作角度来说,过多地频繁访问会导致数据流动的效率下降。
9-5 【理论】逻辑树与视觉树
StackPanel 是纵向排列的空间,可以在里面添加各种各样的控件。
<StackPanel>
<TextBlock HorizontalAlignment="Center" Margin="20">Hello World</TextBlock>
<ListBox Height="100" Width="100">
<ListBoxItem Content="item 1"></ListBoxItem>
<ListBoxItem Content="item 2"></ListBoxItem>
<ListBoxItem Content="item 3"></ListBoxItem>
<ListBoxItem Content="item 4"></ListBoxItem>
</ListBox>
<Button Margin="20" Width="100" Click="Button_Click">随便</Button>
</StackPanel>
cs中代码
private void Button_Click(object sender, RoutedEventArgs e)
{
MessageBox.Show("按钮被点击了!"); //这里加入断点
}
给上面点击事件,加入断点。启动调试,在“Autos”中,找到“Name”中 sender 对应的“Value”中,鼠标放到放大镜上,提示“WPF Tree Visualizer”,点击【放大镜】进入视觉树查看页面。
在这个窗口中,左边是视觉树,右边是当前视觉树(选中的)节点所有的属性。
从页面来说整个像是一棵树被倒过来一样。从树根出发,一层一层开枝散叶,树根 MainWindow ,而 MainWindow 连接的是视觉节点 Border(边框),继续连接后续的视觉节点等等。
不过,在视觉树中,也可以找到逻辑节点。比如 StackPanel、TextBlock、ListBox 等等。
从原理来说,逻辑树是视觉树的子集
逻辑树是视觉树的子集示例图
9-6 【操作】Grid 网格系统
Grid网格是WPF中最基本的行列布局工具。
示例:3行3列的网格布局
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="3*"></ColumnDefinition>
<ColumnDefinition Width="2*"></ColumnDefinition>
<ColumnDefinition Width="*"></ColumnDefinition>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="*"></RowDefinition>
<RowDefinition Height="*"></RowDefinition>
<RowDefinition Height="*"></RowDefinition>
</Grid.RowDefinitions>
<Button Grid.Row="0" Grid.Column="0">click me dddd </Button>
<Button Grid.Row="0" Grid.Column="1">click me 2</Button>
<Button Grid.Row="1" Grid.Column="0">click me 3</Button>
<Button Grid.Row="1" Grid.Column="1">click me 2</Button>
<TextBlock Grid.Row="2" Grid.Column="0">Hello World</TextBlock>
</Grid>
Width值中:* 是权重操作,值还可以是 auto,或 数字(表示像素) 等
9-7 【操作】依赖属性与数据处理
<Grid>
<Button Height="100" Width="200" Content="ClickMe">
<Button.Style>
<Style TargetType="Button">
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="FontSize" Value="25"></Setter>
<Setter Property="Foreground" Value="White"></Setter>
</Trigger>
</Style.Triggers>
</Style>
</Button.Style>
</Button>
</Grid>
在xaml中到处都是属性,像这边Button中的属性Height等,所有这些属性都依赖于 Button.Style 中所定义的 property,也就是说,这些UI上的属性 与 事件触发属性是绑定在一起的,从数据流动的角度来说,这就是数据绑定最基本的逻辑。属性依赖也可以让相关的UI控件知道,当前事件的处理逻辑
public class Button : ButtonBase {
public static readonly DependencyProperty IsCancelProperty;
public static readonly DependencyProperty IsDefaultedProperty;
public static readonly DependencyProperty IsDefaultProperty;
public Button();
public bool IsCancel{ get; set; }
public bool IsDefault { get; set; }
public bool IsDefaulted { get; }
……
}
这里可以看到3个readonly的DependencyProperty,也就是属性依赖,分别用来判断默认属性、属性取消、以及是否回到默认属性。同时对应这三个依赖属性,还有3个基础属性,在程序正常运行时,这3个基础属性会与依赖属性互相连接,而这3组属性则可以控制UI控件中的动态变化。比如,样式数据的绑定、动画效果,甚至样式的继承。
而在这个 ButtonBase 中,继承于ContentControl,在它内部除了有可以处理事件的 RoutedEvent 以外,所有其他的都是依赖属性。
public abstract class ButtonBase : ContentControl, ICommandSource {
public static readonly RoutedEvent ClickEvent;
public static readonly DependencyProperty ClickModeProperty;
public static readonly DependencyProperty CommandParameterProperty;
public static readonly DependencyProperty CommandProperty;
public static readonly DependencyProperty CommandTargetProperty;
public static readonly DependencyProperty IsPressedProperty;
……
}
public class ContentControl : Control, IAddChild {
……
}
让我们更深入一点,进入ContentControl,再进入 Control,同样可以看到类似的属性依赖,可以看到刚刚使用过的 FontSizeProperty 字体大小、ForegroundProperty 前景,等各种各样其他的属性,对于WPF的UI控件来说,属性依赖是最基础的数据和最基础的事件传递机制。
public class Control : FrameworkElement {
public static readonly DependencyProperty BackgroundProperty;
public static readonly DependencyProperty TemplateProperty;
public static readonly DependencyProperty TabIndexProperty;
public static readonly RoutedEvent PreviewMouseDoubleClickEvent;
public static readonly DependencyProperty PaddingProperty;
public static readonly RoutedEvent MouseDoubleClickEvent;
public static readonly DependencyProperty IsTabStopProperty;
…………
public static readonly DependencyProperty FontSizeProperty;
…………
public static readonly DependencyProperty ForegroundProperty;
}
9-8 【操作】Data Binding 数据绑定
1. 单向绑定 one way bining : Source -> Target
2. 双向绑定 two way bining : Source <-> Target
3. 指定方向单向绑定 oneWayToSource Target -> Source
4. 单次绑定 One Time -> 构造方法中单次执行
<StackPanel>
<TextBox Name="myTextBox" Width="100" Margin="50" Text="{Binding ElementName=mySlider, Path=Value, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"></TextBox>
<Slider Name="mySlider" Minimum="0" Maximum="100" IsSnapToTickEnabled="True"></Slider>
</StackPanel>
指定绑定的数据内容是什么?指定数据需要使用 Path 属性,这里的 Path 其实是 Slider 的数据,也就是 Value,最后指定绑定方式。
IsSnapToTickEnabled="True" 则显示的数字,拖动不会显示小数了。
有什么办法,不需要按下 Tab键,就进行数据绑定呢?
有,加上 UpdateSourceTrigger=PropertyChanged
One Time绑定,就是一次性的绑定。
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
mySlider.Value = 35;
myTextBox.Text = mySlider.Value.ToString();
}
}
9-9 【操作】INotifyPropertyChanged 事件处理
<StackPanel>
<Label Content="number 1"></Label>
<TextBox Width="200" Margin="30" Text="{Binding Path=Num1, Mode=TwoWay}"></TextBox>
<Label Content="number 2"></Label>
<TextBox Width="200" Margin="30" Text="{Binding Path=Num2, Mode=TwoWay}"></TextBox>
<Label Content="Resule"></Label>
<TextBox Width="200" Margin="30" Text="{Binding Path=Result, Mode=TwoWay}"></TextBox>
</StackPanel>
cs中代码:
public partial class MainWindow : Window
{
public Sum Sum { get; set; }
public MainWindow()
{
InitializeComponent();
Sum = new Sum() { Num1 = "1", Num2 = "2" };
this.DataContext = Sum;
}
}
新建一个Sum.cs类文件。Sum实现INotifyPropertyChanged接口(在 System.ComponentModel 命名空间下),让VS自动实现接口:public event PropertyChangedEventHandler PropertyChanged;
这样就多了一个方法 PropertyChangedEventHandler,它是一个典型的事件处理委托。
接着来添加事件的触发处理 OnPropertyChanged 方法,因为 TextBox的输入都是字符串,所以监听数据是 string property。
为了能够处理这三个TextBox的联动,还需要在 Num1和Num2 的Set中发送 OnPropertyChanged("Result"); 计算Result这个事件。
public class Sum : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged(string property)
{
if (PropertyChanged == null) //防御性编程
{
return;
}
PropertyChanged(this, new PropertyChangedEventArgs(property)); //事件处理机制
}
private string _num1;
private string _num2;
private string _result;
public string Num1
{
get
{
return _num1;
}
set
{
int number;
bool result = int.TryParse(value, out number);
if(result)
{
_num1 = value;
OnPropertyChanged("Num1");
OnPropertyChanged("Result");
}
}
}
public string Num2
{
get
{
return _num2;
}
set
{
int number;
bool result = int.TryParse(value, out number);
if (result)
{
_num2 = value;
OnPropertyChanged("Num2");
OnPropertyChanged("Result");
}
}
}
public string Result
{
get
{
int result = int.Parse(_num1) + int.Parse(_num2);
return result.ToString();
}
set
{
int result = int.Parse(_num1) + int.Parse(_num2);
_result = result.ToString();
OnPropertyChanged("Result");
}
}
}