WPF 常见 Hacker Solution 汇总 前言及基础篇

阅读: 评论:0

WPF 常见 Hacker Solution 汇总 前言及基础篇

WPF 常见 Hacker Solution 汇总 前言及基础篇

  • 前言
  • MVVM、BaseAttachedProperty、BaseValueConverter、以及动画功能的实现
    • BaseViewModel
    • BaseAttachedProperty
    • BaseValueConverter
    • 动画实现
  • AttachedProperty实现逆时针顺时针旋转功能
    • AP实现动画原理
    • 几种动画的坑
      • 顺时针逆时针旋转
      • 变形扩大缩小
      • ScrollView 展开收缩
  • ScrollView嵌套导致子ScrollView鼠标滚动事件吞没父ScrollView鼠标滚动事件

前言

最近尝试开发WPF项目中,遇到了很多困难,每次都是StackOverflow流,很多方案都是前所未见的,我觉得有记录的价值,也供以后自己参考,由于时间跨度比较大,有些方案我已经找不到当时查找的资料了。

WPF中给我感触最深的地方是条条道路通罗马,实现一种视觉效果有N种方法,但是有的方法看上去又优雅,The MVVM方式,有的方法看上去就像hack,比较多的是 Attached Property 方式

WPF在今天看来可能是辉煌不再了,有更多的桌面跨平台实现方案,但是我觉得有些编程思想还是很有学习价值的,再加上我自己的项目主要还是在Windows平台上运行为主,今后再考虑用Mono或者.Net Core迁移跨平台,至少目前看下来,WPF仍然是Windows桌面开发的最好选择。

如果你是WPF初学者,又像我一样看书在前几章就被各种 XAMLDependency Property 搞得云里雾里,推荐你去油管上看AngelSix的WPF UI教程,虽然时间长,但是看一遍并参照模仿,能让你迅速从 WinformCodeBehind 模式转为MVVM模式。学习WPF对初学者来说绝对不算简单,所以不要觉得经常去网上找‘XXXX怎么实现’很丢人。

由于我也是初学,如果有不正确的地方欢迎指正,谢谢。

MVVM、BaseAttachedProperty、BaseValueConverter、以及动画功能的实现

MVVMModel-View-ViewModelUI层面主要关注的是 View-ViewModel ,WPF可能有一半内容就是在ViewModel变化通知ViewView变化通知ViewModel过程中,通常实现某个功能的套路就是:

  1. 创建用户自定义控件A、以及对应的ViewModel
  2. 将自定义控件的DataContext绑定到ViewModel上(创建一个DesignModel用来给设计器提供数据)
  3. ViewModel中的属性Bind到自定义控件的子属性上,如果需要转换,创建对应的ValueConverter
  4. 对于View上的用户操作例如点击鼠标、按下回车键等,绑定上ViewModel上的Command对象

具体原理我就不多说了,这里主要简单贴上代码实现套路,这里基本照搬AngelSix的方法

BaseViewModel

public class BaseViewModel : INotifyPropertyChanged
{public event PropertyChangedEventHandler PropertyChanged = (sender, e) => { };public void OnPropertyChanged(string name){PropertyChanged(this, new PropertyChangedEventArgs(name));}
}

所有ViewModel全部继承自BaseViewModel,然后安装PropertyChanged.FodyNuget包,项目目录增加文件并写入

<?xml version="1.0" encoding="utf-8" ?>
<Weavers><PropertyChanged/>
</Weavers>

这个包的作用主要是用来在编译的时候将PropertyChanged方法植入到public属性的Set方法中,这样你就不用自己每个Set都写PropertyChanged了,有兴趣研究Fody的同学可以到Github上看看,有很多预编译的强大东东。这是ViewModel项目唯一需要安装的包,其它的例如DI容器,可以随自己喜好安装

BaseAttachedProperty

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就很简单了

public class IsHighlightProperty : BaseAttachedProperty<IsHighlightProperty, bool>
{
}

BaseValueConverter

public abstract class BaseValueConverter<T> : MarkupExtension, IValueConverterwhere 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;elsereturn (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中创建动画的,在下一节介绍

AttachedProperty实现逆时针顺时针旋转功能

AP实现动画原理

这套方法是AngelSix的代码,几经他自己修改,我觉得已经挺完美了,我们先看下调用的时候。

<TextBoxText="{Binding EditedText, UpdateSourceTrigger=PropertyChanged}"local:AnimateFadeInProperty.Value="{Binding Editing}"
/>

根据ViewModel的值,转为true的时候就会fadeInfalse就会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);elseawait 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基本可以在任何控件上用,但是要当心VisibleCollapse的问题。

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));}
}

这是StoryBorderHelper类,通过这个可以组合多个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);}
}

几种动画的坑

顺时针逆时针旋转
<TextBlockRenderTransformOrigin="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);
}
ScrollView 展开收缩

这个比较麻烦了,用到了 MutiBinding ,主要思想是根据所有子元素的高度,去乘以一个 doubleTag ,如果 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);
}
<ScrollViewerx: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();}
}

参考文献:

ScrollView嵌套导致子ScrollView鼠标滚动事件吞没父ScrollView鼠标滚动事件

如题,在子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 eventscrollViewer.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);}}
}
<ScrollViewerlocal:MouseWheelEventBubbleUpAttachedProperty.Value="True"VerticalScrollBarVisibility="Auto"><ItemsControl ItemsSource="{Binding Items}"><ItemsControl.ItemTemplate><DataTemplate><!-- 可能包含子ScrollView --></DataTemplate></ItemsControl.ItemTemplate></ItemsControl>
</ScrollViewer>

转载于:.html

本文发布于:2024-01-31 20:04:03,感谢您对本站的认可!

本文链接:https://www.4u4v.net/it/170670264531026.html

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。

标签:常见   基础   WPF   Solution   Hacker
留言与评论(共有 0 条评论)
   
验证码:

Copyright ©2019-2022 Comsenz Inc.Powered by ©

网站地图1 网站地图2 网站地图3 网站地图4 网站地图5 网站地图6 网站地图7 网站地图8 网站地图9 网站地图10 网站地图11 网站地图12 网站地图13 网站地图14 网站地图15 网站地图16 网站地图17 网站地图18 网站地图19 网站地图20 网站地图21 网站地图22/a> 网站地图23