前言
WPF
是Microsoft
在.NET
平台下支持使用mvvm方式设计应用及其界面的一个优秀框架,本次项目的目的是使用WPF构建一个简单的家电厂家商用演示程序。使用.NET4.7
和Panuon.UI
框架搭建。
基本用法
WPF
是使用.xaml
( eXtensible Application Markup Language )文件对界面进行设计的,xaml
是xml
在应用上的变体,他的通常由引用和标记组成。
<Window x:Class="project.wpf.f.icooling._2002.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:pu="clr-namespace:Panuon.UI.Silver;assembly=Panuon.UI.Silver"
WindowStartupLocation="CenterScreen"
mc:Ignorable="d"
Title="主界面" Height="600" Width="1000">
<Grid>
<Button Height="30"
Width="200"
Content="Button"
pu:ButtonHelper.ButtonStyle="Standard"
pu:ButtonHelper.CornerRadius="5"
pu:ButtonHelper.IsWaiting="{Binding loading}"></Button>
</Grid>
</Window>
xmlns
表示命名空间,xmlns:pu
表示将后续值指定为pu
这个自定义名称的命名空间。
Grid
是WPF
的默认初始化节点,表示布局的开始,也是定义行列的基本样式。
<Button/>
是布局中的一个控件,并且此控件的样式、属性、操作等受其内部的标记所控制。
在上述代码块中,定义了一个基本的布局并展示一个按钮,此按钮的IsWaiting
属性受loading
属性控制(绑定)。
架构
本项目使用的是经典的MVVM
架构方式,即Model
、View
、ViewModel
。
Model
用于管理所有的对象模型,在较为大型的项目中通常会使用Repository
和Services
层进行中间管理,使得数据库和应用之间的耦合降低。
ViewModel
是Model
和UI
之间交互的桥梁,用于将数据绑定到对应的UI
上,降低开发人员管控界面的成本 。
View
UI层,.xaml
布局文件。
布局
Column/Row Definitions
使用Column/Row Definitions
进行分栏布局,GridSplitter
用于栏与栏之间的间隙定义。
<RowDefinition Height="10*"></RowDefinition>//10*表示相对布局,布局加权为10
<Button Grid.Column="0" Grid.Row="0" Content="布局到0列,0行"></Button>
<Button Grid.RowSpan="2" Content="这是一个占用2栏的按钮"></Button>
TreeView
示例样式
TreeView
是一个树状列表,可以用于作为菜单栏,其中pu:TreeViewHelper
是框架自带的快捷设置属性的方式,BasedOn
表示继承一个控件的样式,使得默认的MenuItem
变成资源文件里面的MenuItem
。
<TreeView x:Name="TvMenu"
Grid.Row="0"
pu:TreeViewHelper.SelectedForeground="#49A9C0"
pu:TreeViewHelper.ExpandMode="SingleClick"
pu:TreeViewHelper.SelectMode="ChildOnly"
SelectedItemChanged="TvMenu_SelectedItemChanged"
ItemsSource="{Binding MenuItems}">
<TreeView.ItemContainerStyle>
<Style TargetType="{x:Type TreeViewItem}"
BasedOn="{StaticResource {x:Type TreeViewItem}}">
<Setter Property="Visibility"
Value="{Binding Visibility}" />
<Setter Property="pu:TreeViewHelper.ItemIcon"
Value="{Binding Icon}" />
<Setter Property="IsExpanded"
Value="{Binding IsExpanded, Mode=TwoWay}" />
</Style>
</TreeView.ItemContainerStyle>
<TreeView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding MenuItems}">
<TextBlock Text="{Binding Path=Header}" />
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
此项目中,我们因为需要将菜单栏设置为横向布局,故使用TabControl
作为组件。同时在构造函数中将子视图/控件载入到缓存,加载到目标DataContext
中。
public MainWindow()
{
InitializeComponent();
var list = new ObservableCollection<TabControllViewItemModel>() {
new TabControllViewItemModel("主页","Splash","\uf05a"),
new TabControllViewItemModel("机柜空调","Device","\uf05a"),
new TabControllViewItemModel("风扇过滤器","Calendar","\uf05a"),
new TabControllViewItemModel("加热器","tag3","\uf05a"),
new TabControllViewItemModel("水热交换器","tag3","\uf05a"),
new TabControllViewItemModel("冷凝水蒸发器","tag3","\uf05a"),
};
ViewModel = new MainWindowViewModel(list);
DataContext = ViewModel;
}
/// <summary>
/// 用户更改界面
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void NavMenu_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
var selectedItem = NavMenu.SelectedItem as TabControllViewItemModel;
var loadPageName = _partialViewDic.ContainsKey(selectedItem.Tag) ? selectedItem.Tag : "Splash";
if (selectedItem.Content == null)
selectedItem.Content = Activator.CreateInstance(_partialViewDic[loadPageName]);
}
style.xaml
用于编辑全局的样式资源,将其置于Resources\style.xaml
同时在app.xaml
中添加:
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="pack://application:,,,/Panuon.UI.Silver;component/Control.xaml" />
<ResourceDictionary Source="pack://application:,,,/UIBrowser;component/Resources/Styles.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
public partial class MainWindow : System.Windows.Window, System.Windows.Markup.IComponentConnector
系统默认的窗体是继承System.Windows.Window
的,此处应修改为我们框架使用的WindowX
,此处直接修改是无效的,需要在MainWindow.xaml
中同时修改布局:
<pu:WindowX x:Class="UIBrowser.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:pu="clr-namespace:Panuon.UI.Silver;assembly=Panuon.UI.Silver"
xmlns:localized="clr-namespace:UIBrowser.Properties"
xmlns:local="clr-namespace:UIBrowser"
mc:Ignorable="d"
Title="UIBrowser"
Height="700"
Width="1200"
WindowStartupLocation="CenterScreen"
Closing="WindowX_Closing">
资源/资源文件
在编辑时可能会出现不具有由 URI* XXX*识别的资源
、无法找到资源*
等的报错,可能是有些dll
未同步导致,此时只需要按F11重新生成或卸载当前项目并重新加载即可解决问题。
Resources.resx
在WPF
中的使用频率相当高,在新建时应注意其命名空间是否与项目一致。同样的,在使用的时候也可能出现各种玄学bug,但记住重启是万能的即可。
<Image Margin="20 0" Source="/Resources/logo.png"></Image>
引用图片资源需要在图片资源文件的属性
中将生成操作
设置为Resources
,才能保证引用成功。
其中,pack
伪协议的格式为 pack://
application:,/你的项目程序集
;component/调用路径
Style
在UserController
、Window
、Application
中的应用通常需要同根节点同级,并进行声明。使用x:Key
表示样式的名称方便后面或其他模块调用。
<UserControl.Resources>
<Style x:Key="InputText"
TargetType="TextBox"
BasedOn="{StaticResource {x:Type TextBox}}">
<Setter Property="Margin" Value="5,0,0,0" />
<Setter Property="pu:TextBoxHelper.Watermark" Value="单位/mm" />
<Setter Property="pu:TextBoxHelper.CornerRadius" Value="5" />
<Setter Property="Height" Value="30" />
<Setter Property="Width" Value="200" />
</Style>
</UserControl.Resources>
调用此样式时,使用{StaticResource InputText}
即可,调用其他模块的样式或资源同上uri
或路径方式引用。
ResourceDictionary
资源字典
数据字典通常是在app.xaml
中定义的一种资源,方便应用级调用。需要注意:资源字典越靠下的越优先生效,一些样式往往会因为放在上面而被下面的更基本样式覆盖
<Application x:Class="project.wpf.f.icooling._2002.App" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:project.wpf.f.icooling._2002" StartupUri="MainWindow.xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" d1p1:Ignorable="d" xmlns:d1p1="http://schemas.openxmlformats.org/markup-compatibility/2006">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="pack://application:,,,/Panuon.UI.Silver;component/Control.xaml">
</ResourceDictionary>
<ResourceDictionary Source="pack://application:,,,/project.wpf.f.icooling.2002;component/Resources/style.xaml">
</ResourceDictionary>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
</Application>
常规控件
RadioGroup
单选框
以RadioGroup
为例,在添加选项框时,我们构造一个基本的选项框控件SingleOption
用于展示用户的选择。
<ItemsControl Name="items" ItemsSource="{Binding}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<pu:AnimateWrapPanel Height="300" Width="600" HorizontalSpacing="50" VerticalSpacing="20" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid ShowGridLines="True">
<Grid.RowDefinitions>
<RowDefinition Height="50*"></RowDefinition>
<RowDefinition Height="50*"></RowDefinition>
</Grid.RowDefinitions>
<RadioButton Grid.Row="0" Height="30" Width="150"
GroupName="edf39hax5m"
pu:RadioButtonHelper.RadioButtonStyle="Switch">
<RadioButton.Content>
<Image Source="{Binding Img}" />
</RadioButton.Content>
</RadioButton>
<TextBlock Grid.Row="1"
Width="150"
FontSize="10" TextWrapping="WrapWithOverflow"
Text="{Binding Description}"></TextBlock>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
即在ItemsControl
下添加目标,并使用ItemsControl.ItemsPanel
指定容器,xxx.[Property]
在WPF
中对目标控件指定属性的方式。ItemsControl.ItemTemplate
便是对ItemsControl
的子对象进行模板设置,其子层级一定是DtaTemplate
,在其中定义具体的子层级。此处也可以是将DataTemplate
中的对象另外放置到一个UserControl
中进行封装,充分解耦。
ComboBox
下拉框
ComboBox
和MutiComboBox
都是挺常规的,这里需要提一下:SelectedValuePath
表示选中后的Key
字段是什么,DisplayMemberPath
表示展示出来的内容是什么,展示项也可以是控件。
ViewModel
->View
MVVM
的目标之一就是为了解耦UI层
与UI数据
。View
负责前端展示,ViewModel
负责业务逻辑处理,并通知View
层去展示这些处理过后的数据。
我们引用Messager
模式,即ViewModel
层发送处理消息到View
。我们在nuget
管理包中下载安装mvvm-light
包,发现项目中出现了ViewModel
文件夹,我们暂时用不到文件夹中的文件,将其删除即可。
创建Model
Model
是基本的数据对象,是数据-逻辑-视图
分离的中的数据,在Model
文件夹中新建一个对象类,继承ViewModelBase
并初始化一些属性,同时在每个属性中使用this.Set
使得数据的变化同步到ViewModel
中,但这样做很麻烦,每创建一个属性都需要编辑一次。所以我们使用代码块
功能来减少劳动量。在C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\VC#\Snippets\2052\Visual C#
中创建一个propvm.snippet
文件,并输入以下代码后保存。
<?xml version="1.0" encoding="utf-8"?>
<CodeSnippets xmlns="http://schemas.microsoft.com/VisualStudio/2005/CodeSnippet">
<CodeSnippet Format="1.0.0">
<Header>
<Title>propvm</Title>
<Shortcut>propvm</Shortcut>
<Description>属性和支持字段的代码片段</Description>
<Author>Microsoft Corporation</Author>
<SnippetTypes>
<SnippetType>Expansion</SnippetType>
</SnippetTypes>
</Header>
<Snippet>
<Declarations>
<Literal>
<ID>type</ID>
<ToolTip>属性类型</ToolTip>
<Default>int</Default>
</Literal>
<Literal>
<ID>property</ID>
<ToolTip>属性名</ToolTip>
<Default>MyProperty</Default>
</Literal>
<Literal>
<ID>field</ID>
<ToolTip>支持此属性的变量</ToolTip>
<Default>myVar</Default>
</Literal>
</Declarations>
<Code Language="csharp">
<![CDATA[private $type$ $field$;
public $type$ $property$
{
get { return $field$;}
set {this.Set(ref $field$, value);}
}
$end$]]>
</Code>
</Snippet>
</CodeSnippet>
</CodeSnippets>
此时在IDE
中输入propvm
并按下2次Tab
键,则可快速出现一个属性方便编辑。
private DeviceSize size;
public DeviceSize Size
{
get { return size; }
set { this.Set(ref size, value); }
}
创建ViewModel
ViewModel
同样也需继承ViewModelBase
。使用ObservableCollection<Device>
实现 表示一个动态数据集合,它可在添加、删除项目或刷新整个列表时提供通知。
此项目中共有5种设备需要创建界面,且界面各有不同,故我们创建一个UserControl
并命名为DeviceView
作为基本视图。他们共同使用SurfaceArea
、DevicePosition
、DeviceMaterial
、DeviceInstallPosition
、TemperatureDifference
、DevicePower
(包含多种功率)、Atmospheric
、DeviceWindCurrent
这些控件。按上文提到的基本布局方式进行编辑每个控件.xaml
文件。
在.xaml
的构造方法中如果需要将某个控件的DataContext
或其他属性进行指定,注意一定是需要再InitializeComponent
方法之后,因为不能修改未初始化的控件。
xmal
的binding
属性
<TextBox x:Name="textBox1" Text="{Binding Path=Value,ElementName=slider1}"/>
Binding
的目的是快速的为控件进行值绑定,Path
为绑定到的目标属性,默认Binding *
等效于Binding Path=*
、Binding
等效于Binding this
。上述xaml
等价C#代码:
this.textBox.SetBinding(TextBox.TextProperty,new Binding("Value"){ ElementName="slider1"});
且几乎所有的xaml
本质都是通过C#实现的,例如:
this.listBoxStudents.ItemsSource = stuList;//数据源
this.listBoxStudents.DisplayMemberPath = "Name";//路径
//为TextBox设置Binding
Binding binding = new Binding("SelectedItem.Id") { Source=this.listBoxStudents};
this.textBoxId.SetBinding(TextBox.TextProperty,binding);
Converter
属性转换器
有时候我们为了使得属性展现出不同的展示方式,如DateTime
转为可被更好认识的yyyy年mm月dd日
等,便需要使用Converter
。
<TextBox
Text="{Binding Power, Converter={StaticResource muti},ConverterParameter=0.05}"
pu:TextBoxHelper.Watermark="单位/W" TextWrapping="Wrap" />
Converter
表示需要使用的转换器,需要在资源属性中进行定义:
<Grid.Resources>
<local:MutiConverter x:Key="muti" />
</Grid.Resources>
ConverterParameter
表示传入的参数,Converter
继承IValueConverter
接口(MultiBinding
继承IMutiValueConverter
。注意在返回值时的数据类型,否则可能会因为报错而无法将Converter
的值生效到UI
界面
public class MutiConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
return System.Convert.ToDouble(value) * System.Convert.ToDouble(parameter);
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
return (System.Convert.ToDouble(value) / System.Convert.ToDouble(parameter)).ToString();
}
}
复制布局
布局的.xaml
文件复制时需要注意修改文件名的同时,需要在.xaml
文件顶部的x:Class="project.wpf.f.icooling._2002.MainWindow
以及内部(F7键
)的类名同步修改到目标名称,否则则会报错。后期可以根据实际需要对.xaml
文件进行继承,本项目中不涉及。
自定义控件属性和事件
当使用用户自定义控件时,进行传参和传事件需要在自定义控件中预先定义。
定义属性
[Category("Extend Properties")]
public Brush ClearIconFill
{
get { return (Brush)GetValue(ClearIconFillProperty); }
set { SetValue(ClearIconFillProperty, value); }
}
public static readonly DependencyProperty ClearIconFillProperty =
DependencyProperty.Register("ClearIconFill", typeof(Brush), typeof(ClearTextBox), new PropertyMetadata(Brushes.Gray));
通过DependencyProperty
定义了一个自定义属性ClearIconFill
用来表示自定义控件的属性。typeof
表示类的名称,PropertyMetadata
表示属性默认值。[Category("Extend Properties")]
标记表示
定义事件
冒泡传递是WPF
中的事件传递机制,通过这一机制,使得模块之间的耦合进一步降低。
[Category("Behavior")]
public static readonly RoutedEvent OnSearchEvent =
EventManager.RegisterRoutedEvent(
"OnSearch",
RoutingStrategy.Bubble,
typeof(RoutedEventHandler),
typeof(ClearTextBox));
public event RoutedEventHandler OnSearch
{
add { AddHandler(OnSearchEvent, value); }
remove { RemoveHandler(OnSearchEvent, value); }
}
通过RoutedEvent
定义一个自定义事件,EventManager.RegisterRoutedEvent
用于注册此事件。并声明一个事件OnSearch
,添加add
和remove
方法。
正是因为冒泡
事件机制,当定义的事件名称与基类中事件同名时将发生冲突,也是报错已使用 OwnerType“System.Windows.Controls”的 RoutedEvent "XXX"
的原因。