一次使用WPF进行项目开发的经历

前言

WPFMicrosoft.NET平台下支持使用mvvm方式设计应用及其界面的一个优秀框架,本次项目的目的是使用WPF构建一个简单的家电厂家商用演示程序。使用.NET4.7Panuon.UI框架搭建。

基本用法

WPF是使用.xaml( eXtensible Application Markup Language )文件对界面进行设计的,xamlxml在应用上的变体,他的通常由引用和标记组成。

<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这个自定义名称的命名空间。

GridWPF的默认初始化节点,表示布局的开始,也是定义行列的基本样式。

<Button/>是布局中的一个控件,并且此控件的样式、属性、操作等受其内部的标记所控制。

在上述代码块中,定义了一个基本的布局并展示一个按钮,此按钮的IsWaiting属性受loading属性控制(绑定)。

架构

本项目使用的是经典的MVVM架构方式,即ModelViewViewModel

Model用于管理所有的对象模型,在较为大型的项目中通常会使用RepositoryServices层进行中间管理,使得数据库和应用之间的耦合降低。

ViewModelModelUI之间交互的桥梁,用于将数据绑定到对应的UI上,降低开发人员管控界面的成本 。

ViewUI层,.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.resxWPF中的使用频率相当高,在新建时应注意其命名空间是否与项目一致。同样的,在使用的时候也可能出现各种玄学bug,但记住重启是万能的即可。

<Image Margin="20 0" Source="/Resources/logo.png"></Image>

引用图片资源需要在图片资源文件的属性中将生成操作设置为Resources,才能保证引用成功。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LUqUPc6O-1583400101726)(%E4%B8%80%E6%AC%A1%E4%BD%BF%E7%94%A8WPF.NETCore%E8%BF%9B%E8%A1%8C%E9%A1%B9%E7%9B%AE%E5%BC%80%E5%8F%91%E7%9A%84%E7%BB%8F%E5%8E%86.assets/image-20200229171129510.png)]

其中,pack伪协议的格式为 pack://application:,/你的项目程序集;component/调用路径

StyleUserControllerWindowApplication中的应用通常需要同根节点同级,并进行声明。使用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用于展示用户的选择。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IgaxdIvF-1583400101728)(%E4%B8%80%E6%AC%A1%E4%BD%BF%E7%94%A8WPF.NETCore%E8%BF%9B%E8%A1%8C%E9%A1%B9%E7%9B%AE%E5%BC%80%E5%8F%91%E7%9A%84%E7%BB%8F%E5%8E%86.assets/image-20200301164041528.png)]

<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下拉框

ComboBoxMutiComboBox都是挺常规的,这里需要提一下: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作为基本视图。他们共同使用SurfaceAreaDevicePositionDeviceMaterialDeviceInstallPositionTemperatureDifferenceDevicePower(包含多种功率)、AtmosphericDeviceWindCurrent这些控件。按上文提到的基本布局方式进行编辑每个控件.xaml文件。

.xaml的构造方法中如果需要将某个控件的DataContext或其他属性进行指定,注意一定是需要再InitializeComponent方法之后,因为不能修改未初始化的控件。

xmalbinding属性

<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,添加addremove方法。

正是因为冒泡事件机制,当定义的事件名称与基类中事件同名时将发生冲突,也是报错已使用 OwnerType“System.Windows.Controls”的 RoutedEvent "XXX"的原因。

发布了3 篇原创文章 · 获赞 0 · 访问量 116

猜你喜欢

转载自blog.csdn.net/m0_46059204/article/details/104679182