首页 > 其他分享 >WPF --- 非Button自定义控件实现点击功能

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

时间:2023-08-17 19:46:29浏览次数:44  
标签:控件 自定义 FilePath Button 点击 Command OpenFolderBrowserControl public

引言

今天在做一个设置文件夹路径的功能,就是一个文本框,加个按钮,点击按钮,弹出 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="" />
                                                                <!--<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 onm ouseLeftButtonUp(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;
            }
        }
    }
}

测试结果

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

image.png

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

image.png

结论

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

实现核心就是两个方案:

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

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

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

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

标签:控件,自定义,FilePath,Button,点击,Command,OpenFolderBrowserControl,public
From: https://www.cnblogs.com/pandefu/p/17638683.html

相关文章

  • Python实现自定义请求头消息headers
    使用python爬虫爬取数据的时候,经常会遇到一些网站的反爬虫措施,一般就是针对于headers中的User-Agent,如果没有对headers进行设置,User-Agent会声明自己是python脚本,而如果网站有反爬虫的想法的话,必然会拒绝这样的连接。而修改headers可以将自己的爬虫脚本伪装成浏览器的正常访问,来......
  • 如何完美实现在DataGridView单元格中增加多个Button按钮?
    实现DataGridView多按钮操作列在很多WinForm过程中,经常会遇到使用DataGridView进行编辑的场景,用户希望在最后放一个操作列,里面放置两个按钮,一个增加行的按钮,一个删除行的按钮;并且第一行只有增加行的按钮,没有删除行的按钮,大概的界面如下:DataGridView本身提供了DataGridViewButtonCol......
  • 为远程群晖NAS的自定义域名免费申请SSL证书
    概述ERP系统对于企业来说重要性不言而喻,不管是财务、生产、销售还是采购,都需要用到ERP系统来协助。但ERP中这些重要数据属于企业机密文档,往往需要本地化管理,只能部署在企业内网之下。有时候我们会遇到在外需要远程登录ERP临时处理紧急事务,我们可以通过内网穿透来解决,将ERP服务端端......
  • mybatis自定义拦截器@Intercepts
    mybatis:自定义实现拦截器插件Interceptor-知乎(zhihu.com) 11.插件机制Interceptor|一灰灰Learning(hhui.top)......
  • Kettle中调用用户自定义的jar包
     ETL工具断断续续的也接触了Informatica,Kettle,SSIS,个人感觉Info很强大但是也很贵,而且有着一些神秘感。Kettle4.0版本以来已经有了User  definedjavaclass组件,使用户可以写Java代码让kettle来调用,这就说明了很多kettle不能处理的东西我们可以通过Java代码来实现,步骤如......
  • Oracle DB :用户自定义数据类型
    用户自定义类型可以使用Oracle内嵌的数据类型和其他用户自定义类型作为对象类型的构建块,对象模型构成了在应用中数据的结构和行为。下面将描述用户自定义类型的种类。提示:可以参考createType和createtypebody获取更多的创建用户定义数据类型的信息;==对象类型==对象类型是真......
  • Unity 自定义路径漫游-车辆 技术解析
    如第一张图所示,可以看到蓝线就是自定义的路径,车辆和飞机会沿自定义路径漫游,这对于动画技术来讲是一个基本的也是非常受欢迎博得眼球的技术,我需要记录一下这个技术的实现1.首先选择一个移动的物体,给它的inspector挂上脚本CarPath(Script)代码比较长,没有时间过度解读他,我说明怎......
  • 介绍一个工具,可快速自定义各种管理功能,包括数据管理和流程审批
    朋友找到一个叫蓝点通用管理系统的软件,真是太牛B了,普通人都可以三分钟入门,非常方便地自己定制需要的管理功能,像什么人事管理、订单管理、进销存管理、客户管理都是简简单单就可以搞定,并且随时可以调整或扩展管理功能。也可以导入官方提供的或别人发的模板,直接用! 还需要买什么......
  • Winform控件自适应窗体大小
    思路[参考他人]:1>保存窗体的初始宽度和高度;2>保存窗体内所有控件的初始宽度,初始高度和坐标;3>窗体的Resize事件触发时,计算新的Size和初始Size的比例prec;4>遍历窗体内所有控件,将其的Size和坐标乘以prec;代码:窗体注册,保存窗体及其所有控件的初始尺寸,编写Resize事件逻辑:public......
  • Python 自定义运算符
    Python自定义运算符正向运算符+__add__(self,other)-__sub__(self,other)*__mul__(self,other)/__truediv__(self,other)//__floordiv__(self,other)%__mod__(self,other)**__pow__(self,other)<__lt__(self,other)>__gt__(self,other)==__......