引言

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

目标

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

准备工作

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

接下来创建这个整体的控件,不使用 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;
}
}
}
}

测试结果

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

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

结论

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

实现核心就是两个方案:

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

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

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

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

WPF --- 非Button自定义控件实现点击功能的更多相关文章

  1. WPF中button按钮同时点击多次触发click解决方法

    DateTime lastClick = DateTime.Now; object obj = new object(); ; private void Button_Click(object sen ...

  2. revit二次开发wpf里button按钮无法实现事务

    不能在revit提供的api外部使用事务,解决此方法, 1.把button里要实现的功能写到外部事件IExternalEventHandler中,注册外部事件,在button事件中.raise()使用 ...

  3. wpf 触摸屏 button 背景为null的 问题

    原文:wpf 触摸屏 button 背景为null的 问题 <!-- button样式--> <Style x:Key="myBtn" TargetType=&q ...

  4. WPF 4 DataGrid 控件(基本功能篇)

    原文:WPF 4 DataGrid 控件(基本功能篇)      提到DataGrid 不管是网页还是应用程序开发都会频繁使用.通过它我们可以灵活的在行与列间显示各种数据.本篇将详细介绍WPF 4 中 ...

  5. WPF 精修篇 自定义控件

    原文:WPF 精修篇 自定义控件 自定义控件 因为没有办法对界面可视化编辑 所以用来很少 现在实现的是 自定义控件的 自定义属性 和自定义方法 用VS 创建自定义控件后 会自动创建 Themes 文件 ...

  6. winform中button点击后再点击其他控件致使button失去焦点,此时button出现黑色边线,去掉黑色边线的方法

    winform中button点击后再点击其他控件致使button失去焦点,此时button出现黑色边线,去掉黑色边线的方法 button的FlatAppearence属性下,设置BorderSize= ...

  7. Android 三档自定义滑动开关,禁止点击功能的实现,用默认的seekbar组件实现

    android三档自定义滑动开关,禁止点击功能的实现,普通开关网上有很多例子,三档滑动开关的则找了整天都没有相关例子,开始用普通开关的源码修改了自己实现了一个类,但效果不如人意,各种边界情况的算法很难 ...

  8. android selector 背景选择器的使用, button (未点击,点击,选中保持状态)效果实现

              android selector 背景选择器的使用, button (未点击,点击,选中保持状态)效果实现 首先看到selector的属性: android:state_focus ...

  9. 在WPF的DATAGRID中快速点击出现在ADDNEW或EDITITEM事务过程不允许DEFERREFRESH

    原文 在WPF的DATAGRID中快速点击出现在ADDNEW或EDITITEM事务过程不允许DEFERREFRESH 在项目中关于DataGrid的遇到过一些问题,其中是关于迁入CheckBox的双向 ...

  10. unity, ugui button 禁止重复点击

    如上图,button名称为btn_sim,当点击button后,开始播放zoomToTarget动画.为了防止在动画播放过程中再次点击button导致动画被打断,希望当首次点击button后butto ...

随机推荐

  1. 2021-02-25:给定一个正数数组arr,请把arr中所有的数分成两个集合。如果arr长度为偶数,两个集合包含数的个数要一样多;如果arr长度为奇数,两个集合包含数的个数必须只差一个。请尽量让两个集合的累加和接近,返回最接近的情况下,较小集合的累加和。

    2021-02-25:给定一个正数数组arr,请把arr中所有的数分成两个集合.如果arr长度为偶数,两个集合包含数的个数要一样多:如果arr长度为奇数,两个集合包含数的个数必须只差一个.请尽量让两个 ...

  2. 2022-04-12:给定一个字符串形式的数,比如“3421“或者“-8731“, 如果这个数不在-32768~32767范围上,那么返回“NODATA“, 如果这个数在-32768~32767范围上

    2022-04-12:给定一个字符串形式的数,比如"3421"或者"-8731", 如果这个数不在-32768~32767范围上,那么返回"NODAT ...

  3. 2021-08-17:谷歌面试题扩展版,面值为1~N的牌组成一组,每次你从组里等概率的抽出1~N中的一张,下次抽会换一个新的组,有无限组,当累加和<a时,你将一直抽牌,当累加和>=a且<b时,你将获胜

    2021-08-17:谷歌面试题扩展版,面值为1N的牌组成一组,每次你从组里等概率的抽出1N中的一张,下次抽会换一个新的组,有无限组,当累加和<a时,你将一直抽牌,当累加和>=a且< ...

  4. 2021-10-25:计数质数。统计所有小于非负整数 n 的质数的数量。力扣204。

    2021-10-25:计数质数.统计所有小于非负整数 n 的质数的数量.力扣204. 福大大 答案2021-10-25: 自然智慧即可.从i从3开始遍历,每次加2,i*i<n. 代码用golan ...

  5. Django4全栈进阶之路5 Model模型

    在 Django 中,模型(Model)是用于定义数据结构的组件,其作用如下: 定义数据结构:模型用于定义数据库中的表格和表格中的字段(列),其中每个模型类对应一个表格,模型中的每个字段对应表格中的一 ...

  6. 防抖节流utils

    /** * 防抖原理:一定时间内,只有最后一次操作,再过wait毫秒后才执行函数 * * @param {Function} func 要执行的回调函数 * @param {Number} wait ...

  7. 小H分糖果

    7-5 小H分糖果 (20 分) 小H来到一个小学分糖果,小学生们很听话,站成一排等着分糖果,小H将根据每个人的上次考试分数给一定的糖果,规则如下. 每个人都有自己分数ai​,代表上次考试成绩. 每个 ...

  8. linux ssh远程登录

    目录 一.ssh概念 二.配置文件 三.ssh组成结构 四.远程控制过程 五.远程复制 六.配置密钥 七.wraooers防火墙 一.ssh概念 ssh:一种安全通道协议 功能:1.实现字符界面远程登 ...

  9. odoo开发教程十七:controller

    一:controller简述 odoo里面的controller相似于springMVC,也是根据url来控制请求,把请求处理映射到具体某个方法上的. 类比于springmvc中,根据请求,在请求处理 ...

  10. GLIBC 升级安装与 SCL 知识盲区

    前言 glibc 是 GNU 发布的 libc 库,即 c 运行库.glibc 是 linux 系统中最底层的 api,几乎其它任何运行库都会依赖于 glibc.glibc 除了封装 linux 操作 ...