• WPF --- 非Button自定义控件实现点击功能


    引言

    今天在做一个设置文件夹路径的功能,就是一个文本框,加个按钮,点击按钮,弹出 FolderBrowserDialog 再选择文件夹路径,简单做法,可以直接 StackPanel 横向放置一个 TextBox 和一个 Image Button,然后点击按钮在 后台代码中给 ViewModelFilePath赋值。但是这样属实不够优雅,UI 不够优雅,代码实现也可谓是强耦合,那接下来我分享一下我的实现方案。

    目标

    做这个设置文件夹路径的功能,我的目标是点击任何地方都可以打开 FolderBrowserDialog,那就需要把文本框,按钮作为一个整体控件,且选择完文件夹路径后就给绑定的 ViewModelFilePath 赋值。

    准备工作

    首先,既然要设计一个整体控件,那么 UI 如下:

    image.png

    接下来创建这个整体的控件,不使用 Button ,直接使用 Control,来创建自定义控件 OpenFolderBrowserControl :

    Code Behind 代码如下:

    public class OpenFolderBrowserControl : Control,
    {
        static OpenFolderBrowserControl()
        {
            DefaultStyleKeyProperty.OverrideMetadata(typeof(OpenFolderBrowserControl), new FrameworkPropertyMetadata(typeof(OpenFolderBrowserControl)));
        }
    
        public static readonly DependencyProperty FilePathProperty = DependencyProperty.Register("FilePath", typeof(string), typeof(OpenFolderBrowserControl));
    
        [Description("文件路径")]
        public string FilePath
        {
            get => (string)GetValue(FilePathProperty);
            set => SetValue(FilePathProperty, value);
        }
    }
    

    Themes/Generic.xaml 中的设计代码如下:

    <Style TargetType="{x:Type local:OpenFolderBrowserControl}">
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="{x:Type local:OpenFolderBrowserControl}">
                        <Border
                            Background="{TemplateBinding Background}"
                            BorderBrush="{TemplateBinding BorderBrush}"
                            BorderThickness="{TemplateBinding BorderThickness}">
                            <StackPanel Orientation="Horizontal">
    
                                <TextBox
                                    Width="{TemplateBinding Width}"
                                    Height="56"
                                    Padding="0,0,60,0"
                                    IsEnabled="False"
                                    IsReadOnly="True"
                                    Text="{Binding FilePath, RelativeSource={RelativeSource Mode=TemplatedParent}}">
                                    <TextBox.Style>
                                        <Style TargetType="{x:Type TextBox}">
                                            <Setter Property="Background" Value="White" />
                                            <Setter Property="BorderBrush" Value="#CAD2DD" />
                                            <Setter Property="Foreground" Value="#313F56" />
                                            <Setter Property="BorderThickness" Value="2" />
                                            <Setter Property="KeyboardNavigation.TabNavigation" Value="None" />
                                            <Setter Property="HorizontalContentAlignment" Value="Left" />
                                            <Setter Property="FocusVisualStyle" Value="{x:Null}" />
                                            <Setter Property="AllowDrop" Value="False" />
                                            <Setter Property="FontSize" Value="22" />
                                            <Setter Property="ScrollViewer.PanningMode" Value="VerticalFirst" />
                                            <Setter Property="Stylus.IsFlicksEnabled" Value="False" />
                                            <Setter Property="HorizontalAlignment" Value="Left" />
                                            <Setter Property="VerticalAlignment" Value="Center" />
                                            <Setter Property="Margin" Value="20,0,0,0" />
                                            <Setter Property="Template">
                                                <Setter.Value>
                                                    <ControlTemplate TargetType="{x:Type TextBox}">
                                                        <Border
                                                            x:Name="border"
                                                            Background="{TemplateBinding Background}"
                                                            BorderBrush="{TemplateBinding BorderBrush}"
                                                            BorderThickness="{TemplateBinding BorderThickness}"
                                                            CornerRadius="8"
                                                            SnapsToDevicePixels="True">
                                                            <Grid>
                                                                <ScrollViewer
                                                                    x:Name="PART_ContentHost"
                                                                    Margin="20,0,0,0"
                                                                    VerticalAlignment="{TemplateBinding VerticalAlignment}"
                                                                    VerticalContentAlignment="Center"
                                                                    Focusable="False"
                                                                    FontFamily="{TemplateBinding FontFamily}"
                                                                    FontSize="{TemplateBinding FontSize}"
                                                                    HorizontalScrollBarVisibility="Hidden"
                                                                    VerticalScrollBarVisibility="Hidden" />
                                                                <TextBlock
                                                                    x:Name="WARKTEXT"
                                                                    Margin="20,0,0,0"
                                                                    HorizontalAlignment="Left"
                                                                    VerticalAlignment="Center"
                                                                    FontFamily="{TemplateBinding FontFamily}"
                                                                    FontSize="{TemplateBinding FontSize}"
                                                                    Foreground="#A0ADBE"
                                                                    Text="{TemplateBinding Tag}"
                                                                    Visibility="Collapsed" />
                                                            Grid>
                                                        Border>
                                                        <ControlTemplate.Triggers>
                                                            <Trigger Property="IsEnabled" Value="False">
                                                                <Setter TargetName="border" Property="Opacity" Value="0.56" />
                                                            Trigger>
                                                            <Trigger Property="IsMouseOver" Value="True">
                                                                <Setter TargetName="border" Property="BorderBrush" Value="#CAD2DD" />
                                                            Trigger>
                                                            <Trigger Property="IsKeyboardFocused" Value="True">
                                                                <Setter TargetName="border" Property="BorderBrush" Value="#CAD2DD" />
                                                            Trigger>
                                                            <MultiTrigger>
                                                                <MultiTrigger.Conditions>
                                                                    <Condition Property="Text" Value="" />
                                                                    
                                                                MultiTrigger.Conditions>
                                                                <Setter TargetName="WARKTEXT" Property="Visibility" Value="Visible" />
                                                            MultiTrigger>
                                                        ControlTemplate.Triggers>
                                                    ControlTemplate>
                                                Setter.Value>
                                            Setter>
                                        Style>
                                    TextBox.Style>
                                TextBox>
                                <Border
                                    Height="56"
                                    Margin="-60,0,0,0"
                                    Background="White"
                                    BorderBrush="#CAD2DD"
                                    BorderThickness="2"
                                    CornerRadius="0,8,8,0">
                                    <StackPanel
                                        HorizontalAlignment="Center"
                                        VerticalAlignment="Center"
                                        Orientation="Horizontal">
                                        <Ellipse
                                            Width="5"
                                            Height="5"
                                            Margin="3"
                                            Fill="#949494" />
                                        <Ellipse
                                            Width="5"
                                            Height="5"
                                            Margin="3"
                                            Fill="#949494" />
                                        <Ellipse
                                            Width="5"
                                            Height="5"
                                            Margin="3"
                                            Fill="#949494" />
                                    StackPanel>
                                Border>
    
                            StackPanel>
                        Border>
                    ControlTemplate>
                Setter.Value>
            Setter>
        Style>
    

    这样创建的控件实际上是没有点击功能的。

    那么接下来看一下点击功能方案实现。

    点击功能方案实现

    因为有 MVVM 的存在,所以在 WPF 中 Button 点击功能有两种方案,

    • 第一种是直接注册点击事件,比如 Click="OpenFolderBrowserControl_Click"
    • 第二种是绑定Command、CommandParameter、CommandTarget,比如 Command="{Binding ClickCommand}" CommandParameter="" CommandTarget=""

    但是上文中我们定义的是一个 Control ,它既没有 Click 也没有 Command,所以,我们需要给 OpenFolderBrowserControl 定义ClickCommand

    定义点击事件

    定义点击事件比较简单,直接声明一个 RoutedEventHandler ,命名为 Click 就可以了。

    public event RoutedEventHandler? Click;
    

    定义Command

    定义 Command 就需要 ICommandSource 接口,重点介绍一下 ICommandSource 接口。

    ICommandSource 接口用于指示控件可以生成和执行命令。该接口定义了三个成员

    • 定义了一个 ICommand 类型的属性 Command
    • 定义了一个表示与控件关联的, IInputElement 类型的 CommandTarget
    • 定义了一个表示命令参数,object 类型的属性 CommandParameter

    上述两段的定义如下:

    public class OpenFolderBrowserControl : Control, ICommandSource
    {
        //上文中已有代码此处省略...
    
        #region 定义点击事件
    
        public event RoutedEventHandler? Click;
    
        #endregion
    
    
        #region 定义command
    
        public static readonly DependencyProperty CommandProperty =
            DependencyProperty.Register("Command", typeof(ICommand), typeof(OpenFolderBrowserControl), new UIPropertyMetadata(null))
        public ICommand Command
        {
            get { return (ICommand)GetValue(CommandProperty); }
            set { SetValue(CommandProperty, value); }
        }
        public object CommandParameter
        {
            get { return (object)GetValue(CommandParameterProperty); }
            set { SetValue(CommandParameterProperty, value); }
        }
    
        public static readonly DependencyProperty CommandParameterProperty =
            DependencyProperty.Register("CommandParameter", typeof(object), typeof(OpenFolderBrowserControl));
    
        public IInputElement CommandTarget
        {
            get { return (IInputElement)GetValue(CommandTargetProperty); }
            set { SetValue(CommandTargetProperty, value); }
        }
    
        public static readonly DependencyProperty CommandTargetProperty =
            DependencyProperty.Register("CommandTarget", typeof(IInputElement), typeof(OpenFolderBrowserControl));
    

    实现点击功能

    好了,到此为止我仅定义好了点击事件和 Command,但是并没有能够触发这两个功能的地方。

    既然是要实现点击功能,那最直观的方法就是 OnMouseLeftButtonUp,该方法是 WPF 核心基类 UIElement的虚方法,我们可以直接重写。如下代码:

    public class OpenFolderBrowserControl : Control, ICommandSource
    {
        //上文中已有代码此处省略...
        
        protected override void OnMouseLeftButtonUp(MouseButtonEventArgs e)
        {
    
            base.OnMouseLeftButtonUp(e);
            //调用点击事件
            Click?.Invoke(e.Source, e);
            //调用Command
            ICommand command = Command;
            object parameter = CommandParameter;
            IInputElement target = CommandTarget;
    
            RoutedCommand routedCmd = command as RoutedCommand;
            if (routedCmd != null && routedCmd.CanExecute(parameter, target))
            {
                routedCmd.Execute(parameter, target);
            }
            else if (command != null && command.CanExecute(parameter))
            {
                command.Execute(parameter);
            }
        }
    }
    
    

    到此位置,我们的非Button自定义控件实现点击的需求就完成了,接下来测试一下。

    测试

    准备测试窗体和 ViewModel,这里为了不引入依赖包,也算是复习一下 MVVM 的实现,就手动实现 ICommandINotifyPropertyChanged

    ICommand 实现:

    public class RelayCommand : ICommand
    {
        private readonly Action? _execute;
    
        public RelayCommand(Action? execute)
        {
            _execute = execute;
        }
    
        public bool CanExecute(object? parameter)
        {
            return true;
        }
    
        public void Execute(object? parameter)
        {
            _execute?.Invoke();
        }
    
        public event EventHandler? CanExecuteChanged;
    }
    
    

    TestViewModel 实现:
    这里的 ClickCommand 触发之后,我输出了当前 FilePath的值。

    public class TestViewModel : INotifyPropertyChanged
    {
    
        public TestViewModel()
        {
            FilePath = Environment.GetFolderPath(Environment.SpecialFolder.Desktop);
        }
    
        public event PropertyChangedEventHandler? PropertyChanged;
        
        protected virtual void OnPropertyChanged(string propertyName)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
    
        private string filePath = string.Empty;
        /// 
        /// 文件路径
        /// 
        public string FilePath
        {
            get { return filePath; }
            set { filePath = value; OnPropertyChanged(nameof(FilePath)); }
        }
    
    
        private ICommand clickCommand = null;
        /// 
        /// 点击事件
        /// 
        public ICommand ClickCommand
        {
            get { return clickCommand ??= new RelayCommand(Click); }
            set { clickCommand = value; }
        }
    
        private void Click()
        {
            MessageBox.Show($"ViewModel Clicked!The value of FilePath is {FilePath}");
        }
    }
    

    窗体UI代码

    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="1*" />
            <ColumnDefinition Width="2*" />
        Grid.ColumnDefinitions>
        
        <TextBlock
            HorizontalAlignment="Center"
            VerticalAlignment="Center"
            FontSize="22"
            Text="设置文件路径:" />
    
        <local:OpenFolderBrowserControl
            Grid.Column="1"
            HorizontalAlignment="Left"
            Click="OpenFolderBrowserControl_Click"
            Command="{Binding ClickCommand}"
            FilePath="{Binding FilePath, Mode=TwoWay}" />
    Grid>
    

    窗体 Code Behind 代码

    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
            DataContext = new TestViewModel();
        }
    
        private void OpenFolderBrowserControl_Click(object sender, RoutedEventArgs e)
        {
            FolderBrowserDialog folderBrowserDialog = new FolderBrowserDialog();
    
            DialogResult result = folderBrowserDialog.ShowDialog();
    
            if (result == System.Windows.Forms.DialogResult.OK)
            {
                string selectedFolderPath = folderBrowserDialog.SelectedPath;
    
                var Target = sender as OpenFolderBrowserControl;
    
                if (Target != null)
                {
                    Target.FilePath = selectedFolderPath;
                }
            }
        }
    }
    

    测试结果

    我点击整个控件的任意地方,都能打开文件夹浏览器。

    image.png

    选择音乐文件夹后,弹窗提示 ViewModel Clicked!The value of FilePath is C:\Users\Administrator\Music

    image.png

    结论

    从测试结果中可以看出,在 UI 注册的 ClickCommand 均触发。这个方案仅仅是抛砖引玉,只要任意控件(非button)需要实现点击功能,都可以这样去实现。

    实现核心就是两个方案:

    • 直接定义点击事件。
    • 实现ICommandSource。

    然后再重写各种鼠标事件,鼠标按下,鼠标抬起,双击等都可以实现。

    上述方案既保证了 UI 的优雅也保证了 MVVM 架构的前后分离特性。

    如果大家有更好更优雅的方案,欢迎留言讨论。

  • 相关阅读:
    Fmoc-PEG4-NHS酯,1314378-14-7 含有Fmoc保护胺和NHS酯
    Codeforces Round #831 (Div. 1 + Div. 2)
    h3c交换机配置教程命令(新手配置交换机详细教程)
    JavaScript的字符串介绍
    冲刺备战金九银十,奉上万字面经[Java一线大厂高岗面试题解合集]
    以太坊智能合约的历史里程碑: 从DAO到数据隐私的技术演进
    C++day3
    【机器学习习题】估计一个模型在未见过的数据上的性能
    点击事件 事件委托的情况下实现阻止冒泡
    AWK编程语言笔记第一章:基础语法
  • 原文地址:https://www.cnblogs.com/pandefu/p/17638683.html