最近嘗試開發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>