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


    前言

    最近尝试开发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包,项目目录增加FodyWeavers.xml文件并写入

    <?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, 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中创建动画的,在下一节介绍

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

    AP实现动画原理

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

    <TextBox
        Text="{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);
            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基本可以在任何控件上用,但是要当心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);
        }
    }
    

    几种动画的坑

    顺时针逆时针旋转

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

    参考文献:https://social.msdn.microsoft.com/Forums/vstudio/en-US/86039bcd-c550-43b9-b588-36859cc96479/why-doesnt-this-rotate

    变形扩大缩小

    同旋转

    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);
    }
    
    <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嵌套导致子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 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>
    
  • 相关阅读:
    032 代码复用与函数递归
    031 实例7-七段数码管绘制
    030 函数的定义与使用
    029 函数和代码复用
    2.4 Buffer
    2.3 字符串链接
    2.2 去除字符串特别字符
    2.1 字符串查询
    存储数据_文件读写
    template模板
  • 原文地址:https://www.cnblogs.com/enigmaxp/p/9362988.html
Copyright © 2020-2023  润新知