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

  • WPF — 非Button自定义控件实现点击功能已关闭评论
  • 105 次浏览
  • A+
所属分类:.NET技术
摘要

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


引言

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

目标

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

准备工作

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

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

接下来创建这个整体的控件,不使用 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="" />                                                                 <!--<Condition Property="IsFocused" Value="False"/>-->                                                             </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;     /// <summary>     /// 文件路径     /// </summary>     public string FilePath     {         get { return filePath; }         set { filePath = value; OnPropertyChanged(nameof(FilePath)); }     }       private ICommand clickCommand = null;     /// <summary>     /// 点击事件     /// </summary>     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;             }         }     } } 

测试结果

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

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

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

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

结论

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

实现核心就是两个方案:

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

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

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

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