最近尝试开发WPF项目中,遇到了不少困难,每次都是StackOverflow流,不少方案都是前所未见的,我以为有记录的价值,也供之后本身参考,因为时间跨度比较大,有些方案我已经找不到当时查找的资料了。编程
WPF
中给我感触最深的地方是条条道路通罗马,实现一种视觉效果有N种方法,可是有的方法看上去又优雅,The MVVM
方式,有的方法看上去就像hack,比较多的是 Attached Property
方式c#
WPF
在今天看来多是辉煌再也不了,有更多的桌面跨平台实现方案,可是我以为有些编程思想仍是颇有学习价值的,再加上我本身的项目主要仍是在Windows平台上运行为主,从此再考虑用Mono或者.Net Core迁移跨平台,至少目前看下来,WPF
仍然是Windows桌面开发的最好选择。async
若是你是WPF初学者,又像我同样看书在前几章就被各类 XAML
、 Dependency Property
搞得云里雾里,推荐你去油管上看AngelSix的WPF UI教程,虽然时间长,可是看一遍并参照模仿,能让你迅速从 Winform
的 CodeBehind
模式转为MVVM
模式。学习WPF对初学者来讲绝对不算简单,因此不要以为常常去网上找‘XXXX怎么实现’很丢人。mvvm
因为我也是初学,若是有不正确的地方欢迎指正,谢谢。ide
MVVM
:Model-View-ViewModel
,UI
层面主要关注的是 View-ViewModel
,WPF可能有一半内容就是在ViewModel
变化通知View
,View
变化通知ViewModel
过程当中,一般实现某个功能的套路就是:学习
ViewModel
DataContext
绑定到ViewModel
上(建立一个DesignModel
用来给设计器提供数据)ViewModel
中的属性Bind
到自定义控件的子属性上,若是须要转换,建立对应的ValueConverter
View
上的用户操做例如点击鼠标、按下回车键等,绑定上ViewModel
上的Command
对象具体原理我就很少说了,这里主要简单贴上代码实现套路,这里基本照搬AngelSix
的方法动画
public class BaseViewModel : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged = (sender, e) => { }; public void OnPropertyChanged(string name) { PropertyChanged(this, new PropertyChangedEventArgs(name)); } }
全部ViewModel
所有继承自BaseViewModel
,而后安装PropertyChanged.Fody
的Nuget
包,项目目录增长FodyWeavers.xml
文件并写入this
<?xml version="1.0" encoding="utf-8" ?> <Weavers> <PropertyChanged/> </Weavers>
这个包的做用主要是用来在编译的时候将PropertyChanged
方法植入到public
属性的Set
方法中,这样你就不用本身每一个Set
都写PropertyChanged
了,有兴趣研究Fody
的同窗能够到Github
上看看,有不少预编译的强大东东。这是ViewModel
项目惟一须要安装的包,其它的例如DI
容器,能够随本身喜爱安装设计
public abstract class BaseAttachedProperty<Parent, Property> where Parent : new() { public event Action<DependencyObject, DependencyPropertyChangedEventArgs> ValueChanged = (sender, e) => { }; public event Action<DependencyObject, object> ValueUpdated = (sender, value) => { }; public static Parent Instance { get; private set; } = new Parent(); public static readonly DependencyProperty ValueProperty = DependencyProperty.RegisterAttached( "Value", typeof(Property), typeof(BaseAttachedProperty<Parent, Property>), new UIPropertyMetadata( default(Property), new PropertyChangedCallback(OnValuePropertyChanged), new CoerceValueCallback(OnValuePropertyUpdated) )); private static void OnValuePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { (Instance as BaseAttachedProperty<Parent, Property>)?.OnValueChanged(d, e); (Instance as BaseAttachedProperty<Parent, Property>)?.ValueChanged(d, e); } private static object OnValuePropertyUpdated(DependencyObject d, object value) { (Instance as BaseAttachedProperty<Parent, Property>)?.OnValueUpdated(d, value); (Instance as BaseAttachedProperty<Parent, Property>)?.ValueUpdated(d, value); return value; } public static Property GetValue(DependencyObject d) => (Property)d.GetValue(ValueProperty); public static void SetValue(DependencyObject d, Property value) => d.SetValue(ValueProperty, value); public virtual void OnValueChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e) { } public virtual void OnValueUpdated(DependencyObject sender, object value) { } }
全部Attached Property
(例如Grid.Column
就是Attached Property
,之后都简写做AP
)都继承自BaseAttachedProperty
,这样若是你想建立一个新的AP
就很简单了code
public class IsHighlightProperty : BaseAttachedProperty<IsHighlightProperty, bool> { }
public abstract class BaseValueConverter<T> : MarkupExtension, IValueConverter where T : class, new() { private static readonly T Converter = new T(); public override object ProvideValue(IServiceProvider serviceProvider) { return Converter ; } public abstract object Convert(object value, Type targetType, object parameter, ureInfo culture); public abstract object ConvertBack(object value, Type targetType, object parameter, ureInfo culture); }
这里增长MarkupExtension
的实现,就能够在XAML
中直接使用
public class BooleanToVisiblityConverter : BaseValueConverter<BooleanToVisiblityConverter> { public override object Convert(object value, Type targetType, object parameter, ureInfo culture) { if (parameter == null) return (bool)value ? Visibility.Hidden : Visibility.Visible; else return (bool)value ? Visibility.Visible : Visibility.Hidden; } public override object ConvertBack(object value, Type targetType, object parameter, ureInfo culture) { throw new NotImplementedException(); } }
<Border Height="4" Background="{StaticResource IconHoverBlueBrush}" VerticalAlignment="Bottom" Visibility="{TemplateBinding local:IsHighlightProperty.Value,Converter={local:BooleanToVisiblityConverter},ConverterParameter=True}" />
<Border x:Name="border"> <!-- Add a render scale transform --> <Border.RenderTransform> <ScaleTransform /> </Border.RenderTransform> <Border.RenderTransformOrigin> <Point X="0.5" Y="0.5" /> </Border.RenderTransformOrigin> </Border> <!-- ... --> <ControlTemplate.Triggers> <EventTrigger RoutedEvent="MouseEnter"> <BeginStoryboard> <Storyboard> <DoubleAnimation To="1.4" Duration="0:0:0.15" Storyboard.TargetName="border" Storyboard.TargetProperty="(RenderTransform).(ScaleTransform.ScaleX)" /> <DoubleAnimation To="1.4" Duration="0:0:0.15" Storyboard.TargetName="border" Storyboard.TargetProperty="(RenderTransform).(ScaleTransform.ScaleY)" /> </Storyboard> </BeginStoryboard> </EventTrigger> </ControlTemplate.Triggers>
主要就是建立Storyboard
,而后往里面加各类Animation
,还有一种在AP
中建立动画的,在下一节介绍
这套方法是AngelSix
的代码,几经他本身修改,我以为已经挺完美了,咱们先看下调用的时候。
<TextBox Text="{Binding EditedText, UpdateSourceTrigger=PropertyChanged}" local:AnimateFadeInProperty.Value="{Binding Editing}" />
根据ViewModel
的值,转为true
的时候就会fadeIn
,false
就会fadeOut
,能够和丑陋的XAML
说拜拜了,开心。
这是AP的实现
public class AnimateFadeInProperty : AnimateBaseProperty<AnimateFadeInProperty> { protected override async void DoAnimation(FrameworkElement element, bool value, bool firstLoad) { if (value) await element.FadeInAsync(firstLoad, firstLoad ? 0 : 0.3f); else await element.FadeOutAsync(firstLoad ? 0 : 0.3f); } }
其中继承自AnimateBaseProperty
,这个基类封装处理了是否第一次载入、是否已经载入等一系列问题,使用弱引用能够防止内存对象不被回收
public abstract class AnimateBaseProperty<Parent> : BaseAttachedProperty<Parent, bool> where Parent : BaseAttachedProperty<Parent, bool>, new(){ private readonly Dictionary<WeakReference, bool> mAlreadyLoaded = new Dictionary<WeakReference, bool>(); private readonly Dictionary<WeakReference, bool> mFirstLoadValue = new Dictionary<WeakReference, bool>(); public override void OnValueUpdated(DependencyObject sender, object value) { if (!(sender is FrameworkElement element)) return; var alreadyLoadedReference = mAlreadyLoaded.FirstOrDefault(f => Equals(f.Key.Target, sender)); if ((bool) sender.GetValue(ValueProperty) == (bool) value && alreadyLoadedReference.Key != null) return; if (alreadyLoadedReference.Key == null) { var weakReference = new WeakReference(sender); mAlreadyLoaded[weakReference] = false; element.Visibility = Visibility.Hidden; async void onLoaded(object ss, RoutedEventArgs ee) { element.Loaded -= onLoaded; await Task.Delay(5); var firstLoadReference = mFirstLoadValue.FirstOrDefault(f => Equals(f.Key.Target, sender)); DoAnimation(element, firstLoadReference.Key != null ? firstLoadReference.Value : (bool) value, true); mAlreadyLoaded[weakReference] = true; } element.Loaded += onLoaded; } else if (!alreadyLoadedReference.Value) { mFirstLoadValue[new WeakReference(sender)] = (bool) value; } else { DoAnimation(element, (bool) value, false); } } protected virtual void DoAnimation(FrameworkElement element, bool value, bool firstLoad) { } }
全部UI Element
基本都继承自FrameworkElement
,因此这个AP
基本能够在任何控件上用,可是要小心Visible
和Collapse
的问题。
public static class FrameworkElementAnimations { public static async Task FadeInAsync(this FrameworkElement element, bool firstLoad, float seconds = 0.3f) { var sb = new Storyboard(); sb.AddFadeIn(seconds); sb.Begin(element); if (Math.Abs(seconds) > 1e-5 || firstLoad) element.Visibility = Visibility.Visible; await Task.Delay((int)(seconds * 1000)); } }
这是StoryBorder
的Helper
类,经过这个能够组合多个Animation
对象同时执行
public static class StoryboardHelpers { public static void AddFadeIn(this Storyboard storyboard, float seconds, bool from = false) { var animation = new DoubleAnimation { Duration = new Duration(TimeSpan.FromSeconds(seconds)), To = 1, }; if (from) animation.From = 0; Storyboard.SetTargetProperty(animation, new PropertyPath("Opacity")); storyboard.Children.Add(animation); } }
<TextBlock RenderTransformOrigin="0.5,0.5" local:AnimateCWProperty.Value="{Binding IsExpanded}"> <TextBlock.RenderTransform> <TransformGroup> <ScaleTransform /> <SkewTransform /> <RotateTransform x:Name="rtAngle" Angle="0" /> <TranslateTransform /> </TransformGroup> </TextBlock.RenderTransform> </TextBlock>
这里若是不使用 x:Name
命名 RotateTransform
是没法让动画生效的。
public static void AddRotateCW(this Storyboard storyboard, float seconds) { var animation = new DoubleAnimation { Duration = new Duration(TimeSpan.FromSeconds(seconds)), From = 360, To = 180, }; Storyboard.SetTargetName(animation, "rtAngle"); PropertyPath PropP = new PropertyPath(RotateTransform.AngleProperty); Storyboard.SetTargetProperty(animation, PropP); storyboard.Children.Add(animation); } public static void AddRotateCCW(this Storyboard storyboard, float seconds) { var animation = new DoubleAnimation { Duration = new Duration(TimeSpan.FromSeconds(seconds)), From=180, To = 360, }; Storyboard.SetTargetName(animation, "rtAngle"); PropertyPath PropP = new PropertyPath(RotateTransform.AngleProperty); Storyboard.SetTargetProperty(animation, PropP); storyboard.Children.Add(animation); }
同旋转
public static void AddScaleYExpand(this Storyboard storyboard, float seconds) { var animation = new DoubleAnimation { Duration = new Duration(TimeSpan.FromSeconds(seconds)), From = 0, To = 1, } Storyboard.SetTargetName(animation, "stScaleY"); PropertyPath PropP = new PropertyPath(ScaleTransform.ScaleYProperty); Storyboard.SetTargetProperty(animation, PropP) storyboard.Children.Add(animation); }
这个比较麻烦了,用到了 MutiBinding
,主要思想是根据全部子元素的高度,去乘以一个 double
值 Tag
,若是 double
值为0
,那么就收起 ScrollView
,若是为1
,则所有展开
public static void AddScrollViewExpand(this Storyboard storyboard, float seconds, FrameworkElement element) { if (DesignerProperties.GetIsInDesignMode(element)) { element.SetValue(FrameworkElement.TagProperty, 1); return; } var animation = new DoubleAnimation { Duration = new Duration(TimeSpan.FromSeconds(seconds)), From = 0, To = 1, }; PropertyPath PropP = new PropertyPath("Tag"); Storyboard.SetTargetProperty(animation, PropP); storyboard.Children.Add(animation); }
<ScrollViewer x:Name="ExpandScrollView" HorizontalScrollBarVisibility="Hidden" VerticalScrollBarVisibility="Hidden" HorizontalContentAlignment="Stretch" local:AnimateScrollViewExpandProperty.Value="{Binding IsExpand}" VerticalContentAlignment="Bottom"> <ScrollViewer.Tag> <system:Double>1.0</system:Double> </ScrollViewer.Tag> <ScrollViewer.Height> <MultiBinding Converter="{local:MultiplyConverter}"> <Binding Path="ActualHeight" ElementName="ExpanderContent" /> <Binding Path="Tag" RelativeSource="{RelativeSource Self}" /> </MultiBinding> </ScrollViewer.Height> <ContentControl x:Name="ExpanderContent"></ContentControl> </ScrollViewer>
这里用到了 MultiplyConverter
,能够同时绑定多个数据,照例仍是封装一个基类使用
public abstract class BaseMutiValueConverter<T> : MarkupExtension, IMultiValueConverter where T:class,new() { private static readonly T Converter = new T(); public override object ProvideValue(IServiceProvider serviceProvider) { return Converter ; } public abstract object Convert(object[] values, Type targetType, object parameter, CultureInfo culture); public abstract object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture); } public class MultiplyConverter : BaseMutiValueConverter<MultiplyConverter> { public override object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) { double result = 1.0; foreach (var t in values) { if (t is double d) result *= d; } return result; } public override object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) { throw new NotImplementedException(); } }
参考文献:https://www.codeproject.com/Articles/248112/Templating-WPF-Expander-Control#animation
如题,在子ScrollView
控件中鼠标滚轮的滚动事件会被handle
掉,这样,即便你滚动到子控件的底部,父ScrollView
仍然不能滚动,这个在作复杂的ScrollView
控件时可能会碰到,网上的解决方案使用Code Behind
方式,我稍加修改成AP
方式,在使用上注意加载顺序
public class MouseWheelEventBubbleUpAttachedProperty:BaseAttachedProperty<MouseWheelEventBubbleUpAttachedProperty,bool> { public override void OnValueChanged(DependencyObject sender, ndencyPropertyChangedEventArgs e) { if (!(sender is ScrollViewer scrollViewer)) return; if ((bool) e.NewValue) { void OnLoaded(object s, RoutedEventArgs ee) { scrollViewer.Loaded -= OnLoaded; //Hook the event scrollViewer.FindAndActToAllChild<ScrollViewer>((scrollchildview) => { scrollchildview.PreviewMouseWheel += (sss, eee) => PreviewMouseWheel(sss, eee, scrollViewer); }); } scrollViewer.Loaded += OnLoaded; } } private void PreviewMouseWheel(object sender, MouseWheelEventArgs e, ScrollViewer scrollViewer) { if (!e.Handled) { e.Handled = true; var eventArg = new MouseWheelEventArgs(e.MouseDevice, e.Timestamp, e.Delta) { RoutedEvent = UIElement.MouseWheelEvent, Source = sender }; scrollViewer.RaiseEvent(eventArg); } } }
<ScrollViewer local:MouseWheelEventBubbleUpAttachedProperty.Value="True" VerticalScrollBarVisibility="Auto"> <ItemsControl ItemsSource="{Binding Items}"> <ItemsControl.ItemTemplate> <DataTemplate> <!-- 可能包含子ScrollView --> </DataTemplate> </ItemsControl.ItemTemplate> </ItemsControl> </ScrollViewer>