下面的图片文字内容主要摘录翻译整理自 Christian Mosers 的两周学习WPF的入门文章(前5天):
http://www.wpftutorial.net/GettingStarted.html
如有错误,欢迎指正,并请见谅,我也在学习中。
一)开始
外观与行为的分离:程序开发的趋势。
丰富的内容:文本、图形、多媒体。
高度自定义: Style 和 Template
硬件方案独立:界面元素的定义使用的是 logical units 逻辑单位而不是 pixel 像素。
二)概念
1)XAML文件
将类的属性作为XAML的元素使用
内建隐含的类型转换
标记扩展
绑定Binding 将两个属性值连结在一起
静态资源StaticResource 查找一次资源项目
动态资源DynamicResource 查找资源项目并自动更新
模板绑定TemplateBinding 将控件的依赖属性绑定到一个控件模板
x:Static 解析静态属性的值
x:Null Null值
2)逻辑树与可视树
样例代码:
<Window>
<Grid>
<Label Content="Label" />
<Button Content="Button" />
</Grid>
</Window>
逻辑树
可以继承依赖属性的值
可以解析动态资源的相关引用
在绑定时查找元素的名称
支持路由事件
可视树
呈现所有可视化元素
处理元素透明度
处理界面布局和呈现转换
处理IsEnabled属性
执行点击测试Hit-Testing
支持相对资源RelativeSource(FindAncestor) 静态类VisualTreeHelperExtensions的静态泛型方法FindAncestor<T>,如 var grid = VisualTreeHelperExtensions.FindAncestor<Grid>(this);静态类VisualTreeHelper。
3)依赖属性
概念:
.NET中的属性 存储在类的私有成员中,直接读取访问。
WPF的依赖属性 存储在字典当中,通过对应的Key动态解析获取。
依赖属性的特性 降低内存空间;属性值可以继承;属性值变化时自动发送通知。
值解析策略Value resolution strategy
读取 按一定策略和规则从自己开始读取,然后逐级向上到根元素。
设置 直接设置自己的本地值。
依赖属性的创建 使用DependencyProperty.Register(),按照命名习惯,依赖属性的名字以Property结尾,在代码中使用GetValue()和SetValue()即可。
样例代码:
// 定义依赖属性:给MyClockControl类添加一个DateTime类型的CurrentTime属性,默认值是当前时间。
public static readonly DependencyProperty CurrentTimeProperty =
DependencyProperty.Register( "CurrentTime", typeof(DateTime),
typeof(MyClockControl), new FrameworkPropertyMetadata(DateTime.Now));
// .NET 属性封装
public DateTime CurrentTime
{
get { return (DateTime)GetValue(CurrentTimeProperty); }
set { SetValue(CurrentTimeProperty, value); }
}
//如果需要回调事件,则使用:
new FrameworkPropertyMetadata( DateTime.Now,
OnCurrentTimePropertyChanged,
OnCoerceCurrentTimeProperty ),
OnValidateCurrentTimeProperty );
//其中的值变化回调事件Value Changed Callback
private static void OnCurrentTimePropertyChanged(DependencyObject source,
DependencyPropertyChangedEventArgs e)
{
MyClockControl control = source as MyClockControl;
DateTime time = (DateTime)e.NewValue;
// Put some update logic here...
}
//其中的值控制回调事件Coerce Value Callback,控制属性值的有效性
private static object OnCoerceTimeProperty( DependencyObject sender, object data )
{
if ((DateTime)data > DateTime.Now )
{
data = DateTime.Now;
}
return data;
}
//其中的值检验回调事件Validation Callback,控制属性值的合法性
private static bool OnValidateTimeProperty(object data)
{
return data is DateTime;
}
只读的依赖属性 使用DependencyProperty.RegisterReadonly()及其返回的DependencyPropertyKey。
//注册PropertyKey
private static readonly DependencyPropertyKey IsMouseOverPropertyKey =
DependencyProperty.RegisterReadOnly("IsMouseOver",
typeof(bool), typeof(MyClass),
new FrameworkPropertyMetadata(false));
//注册依赖属性
public static readonly DependencyProperty IsMouseoverProperty =
IsMouseOverPropertyKey.DependencyProperty;
//.NET属性封装
public int IsMouseOver
{
get { return (bool)GetValue(IsMouseoverProperty); }
private set { SetValue(IsMouseOverPropertyKey, value); }
}
附加的依赖属性
<Canvas>
<Button Canvas.Top="20" Canvas.Left="20" Content="Click me!"/>
</Canvas>
//给Button注册一个附加属性,附加到Canvas,类型是double,默认值是0且指定此属性值由子元素继承。
public static readonly DependencyProperty TopProperty =
DependencyProperty.RegisterAttached("Top",
typeof(double), typeof(Canvas),
new FrameworkPropertyMetadata(0d,
FrameworkPropertyMetadataOptions.Inherits));
public static void SetTop(UIElement element, double value)
{
element.SetValue(TopProperty, value);
}
public static double GetTop(UIElement element)
{
return (double)element.GetValue(TopProperty);
}
监视属性值的变化 使用DependencyPropertyDescriptor及其提供的AddValueChanged()
方法。
DependencyPropertyDescriptor textDescr = DependencyPropertyDescriptor.
FromProperty(TextBox.TextProperty, typeof(TextBox));
if (textDescr != null)
{
textDescr.AddValueChanged(myTextBox, delegate
{
// Add your propery changed logic here...
});
}
清除本地值Local Value DependencyProperty.UnsetValue代表了一个没有赋值的属性值。
button1.ClearValue(Button.ContentProperty);
4)路由事件(主要翻译于MSDN)
概念
路由事件成对出现,PreviewXXX和XXX,如PreviewMouseDown和MouseDown。路由事件自己是不会停止下来的,可以设置 e.Handled = true 来停止当前的路由事件。
按功能定义 是一种能够唤起元素树当中所有监听事件的元素(包括实际触发事件的元素本身)的事件处理的事件。
按实现定义 是一个CLR事件,通过 RoutedEvent 的实例在WPF事件系统中来处理。
路由事件与WPF中控件的内容丰富性有关;在WinForm中对相同事件处理的多个对象的事件赋值需要执行多次相同的语句,而WPF中则只执行一次即可;在类中定义的静态事件处理,在事件触发时最先执行,类的实例中定义的事件处理其后执行;不需要反射即可引用路由事件。
HandledEventsToo 属性,可以在EventSetter 和 AddHandler(RoutedEvent, Delegate, Boolean)方法中使用,从而可以在e.Handled = true的情况下,仍然可以执行路由事件。
路由策略RoutingStrategy
策略:隧道Tunneling 由根元素最先执行,沿可视树逐级向下路由,到达实际事件触发元素为止,如果遇到 e.Handled = true 则停止路由。在Bubbling事件之前发生。通常用于控件的内容包含的子元素的事件处理,WPF中的输入事件都是以tunneling/bubbling的形式成对出现的,
策略:冒泡Bubbling 由实际事件触发元素最先执行,沿可视树逐级向上路由,到达根元素为止,如果遇到 e.Handled = true 则停止路由。在Tunneling事件之后发生。大部分路由事件都是Bubbling类型的。通常用于报告不同控件或其他UI元素的输入和状态变化。
策略:Direct 与.NET事件类同,只有事件触发元素本身自己执行事件处理,不进行上下路由(传递)。可以在类的静态事件中使用,可以在EventSetter 和 EventTrigger 使用。
具体的路由方向(向上或向下)是由事件定义本身来决定的。
任何 UIElement 和 ContentElement 都可以监听任何路由事件。利用路由事件,可以在元素树当中进行通信,路由事件中的事件数据是可以传递共享的。
<Border Height="50" Width="300" BorderBrush="Gray" BorderThickness="1">
<StackPanel Background="LightGray" Orientation="Horizontal" Button.Click="CommonClickHandler">
<Button Name="YesButton" Width="Auto" >Yes</Button>
<Button Name="NoButton" Width="Auto" >No</Button>
<Button Name="CancelButton" Width="Auto" >Cancel</Button>
</StackPanel>
</Border>
假设按钮Button的路由事件定义是向上路由,当按钮被点击时,路由事件的触发顺序为:
Button-->StackPanel-->Border-->...
创建路由事件 路由事件通常都是定义为public static readonly类型的成员。
参数sender 事件被唤起的源(RaiseEvent调用者)
参数RoutedEventArgs.Source 事件的发生源
//注册事件:为MyCustomControl类注册一个标准路由事件类型RoutedEventHandler的路由策略是冒泡向上的路由事件,名字是Selected。
public static readonly RoutedEvent SelectedEvent =
EventManager.RegisterRoutedEvent( "Selected", RoutingStrategy.Bubble,
typeof(RoutedEventHandler), typeof(MyCustomControl));
//封装事件
public event RoutedEventHandler Selected
{
add { AddHandler(SelectedEvent, value); }
remove { RemoveHandler(SelectedEvent, value); }
}
//触发唤起事件
RaiseEvent(new RoutedEventArgs(MyCustomControl.SelectedEvent));
路由事件举例:
下面的图例中,#2元素是 PreviewMouseDown 和 MouseDown 两个事件的事件源。
#2元素的MouseDown事件发生时,整个树中的路由事件的处理顺序为:
1)根元素的PreviewMouseDown (隧道tunnel) 。
2)中间#1元素的PreviewMouseDown (隧道tunnel) 。
3)叶子#2元素的PreviewMouseDown (隧道tunnel) 。
4)叶子#2元素的MouseDown (冒泡bubble) 。
5)中间#1元素的MouseDown (冒泡bubble) 。
6)根元素的MouseDown (bubble) 。
路由事件的重叠
<StackPanel xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="SDKSample.EventOvw2"
Name="dpanel2"
Initialized="PrimeHandledToo"
>
<StackPanel.Resources>
<Style TargetType="{x:Type Button}">
<EventSetter Event="Click" Handler="b1SetColor"/>
</Style>
</StackPanel.Resources>
<Button>Click me</Button>
<Button Name="ThisButton" Click="HandleThis">
Raise event, handle it, use handled=true handler to get it anyway.
</Button>
</StackPanel>
当按钮 ThisButton 的鼠标点击事件发生时:先执行HandleThis的事件,如果该事件没有设置 handled=true,则会继续执行由Style指定的b1SetColor事件。而另一个按钮则只执行Style指定的b1SetColor事件。
三)布局和控件
1)基本概念
最佳实践准则:
避免使用固定位置。在Panel中使用Alignment 和 Margin来设置元素的位置。
避免使用固定大小。尽可能将元素的Width 和 Height设置为Auto。
使用Canvas来布局矢量图形,而不是用来布局普通的元素。
在对话框中使用StackPanel来排列按钮。
使用Grid来布局静态数据项窗口,标签列的宽度设置为Auto,文本框列的宽度则设置为*。
在数据模板中将ItemControl 与 Grid一起配合使用布局动态数值列表,利用SharedSize特性来同步标签的宽度。
垂直和水平属性 VerticalAlignment 和 HorizontalAlignment,较简单。
边距Margin和Padding
Margin是控件的外边距,Padding是控件中的内容部分的外边距(内边距),Margin是外部控件的Padding,Padding是内部控件的Margin。
剪辑 ClipToBounds属性
滚动 ScrollViewer ScrollbarVisibility
2)Grid
设置好Grid的Row定义和Column定义即可,行高度、列宽度设置为 Auto 或 * 。
GridSplitter 注意GridSplitter上下行的高度或左右列的宽度在Grid中定义为*,其ResizeBehavior属性设置为PreviousAndNext,或者使用ResizeDirection属性也可(效果稍不同,GridSplitter前一元素的宽度/高度不能调整)。
多个Grid之间共享列宽度 Grid.IsSharedSizeScope SharedSizeGroup
3)StackPanel
一般用于在对话框中放置按钮。
四)数据绑定
1)数据绑定
基本概念 绑定的源可以是普通.NET属性也可以是依赖属性,绑定的目标属性必须是依赖属性。要实现绑定能够正确运转,普通.NET属性是通过触发INotifyPropertyChanged接口的PropertyChanged事件,依赖属性则是通过属性元数据的PropertyChanged回调。在XAML文件中,绑定通常是使用扩展标记{Binding}来使用的。
数据上下文DataContext 每个从FrameworkElement继承的WPF控件都有一个DataContext属性。如果没有对控件指定绑定源,那么其DataContext就是默认的绑定源,或者使用{Binding}也是说明使用DataContext。控件可以传递其DataContext属性值给其子元素使用。
数值转换器 实现IValueConverter 接口。
下面的代码是将StackPanel的可视性绑定到CheckBox控件的IsChecked属性。
<StackPanel>
<StackPanel.Resources>
<BooleanToVisibilityConverter x:Key="boolToVis" />
</StackPanel.Resources>
<CheckBox x:Name="chkShowDetails" Content="Show Details" />
<StackPanel x:Name="detailsPanel"
Visibility="{Binding IsChecked, ElementName=chkShowDetails,
Converter={StaticResource boolToVis}}">
</StackPanel>
</StackPanel>
//布尔到可视性的转换器定义
public class BooleanToVisibilityConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter,
CultureInfo culture)
{
if (value is Boolean)
{
return ((bool)value) ? Visibility.Visible : Visibility.Collapsed;
}
else
return value;
}
public object ConvertBack(object value, Type targetType, object parameter,
CultureInfo culture)
{
if (value is Visibility)
{
bool myValue = false;
switch ((Visibility)value)
{
case Visibility.Visible:
myValue = true;
break;
case Visibility.Collapsed:
myValue = false;
break;
}
return myValue;
}
else
return value;
}
}
五)模板和样式
1)样式
使用样式的好处:代码更易维护,减少代码冗余,仅从一个入口修改一组控件的外观,运行时动态设置控件的外观。
使用样式:{StaticResource [resourceKey]}
继承:
<Style x:Key="baseStyle">
<Setter Property="FontSize" Value="12" />
<Setter Property="Background" Value="Orange" />
</Style>
<Style x:Key="boldStyle" BasedOn="{StaticResource baseStyle}">
<Setter Property="FontWeight" Value="Bold" />
</Style>
2)模板
WPF控件由逻辑和模板两部分组成。逻辑部分定义了控件的状态、事件和属性;模板部分则是控件的可视外表。逻辑和模板的连接是通过数据绑定实现的。每个控件都有其默认的模板,这个默认的模板则提供了控件的基本外表。控件的模板是由其依赖属性Template来设置,设置之后,该控件的可视树则会完全被模板的可视树替代。
<Style x:Key="DialogButtonStyle" TargetType="Button">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Button}">
<Grid>
<Ellipse Fill="{TemplateBinding Background}"
Stroke="{TemplateBinding BorderBrush}"/>
<ContentPresenter HorizontalAlignment="Center"
VerticalAlignment="Center"/>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Button Style="{StaticResource DialogButtonStyle}" />