分类目录归档:Winform

Command in WPF & WinForm

1 命令的用途

命令这个概念,最早是在AutoCAD中认识的,它可以由多种途径引发,进而执行一系列动作。在WPF中命令被附加了其他的特征,但其存在主要目的在于如下几点。

1. 重用逻辑:友好的界面为用户提供多种方式执行同一操作以迎合不同用户的习惯,如菜单条目、工具栏按钮、窗体快捷键、鼠标手势、显式命令行等。虽然也可以重用事件处理函数,但命令是更高级的抽象,也更加容易被理解。

2. 统一管理:某些动作只有在满足一定条件时才能被触发,而条件不满足时相应控件应为不可用的状态。命令可以(直接或间接)管理关联到自身的触发者,使其可用状态一同变化,这省去了编码时的很多麻烦。

3. 脚本控制:命令从事件处理函数中独立出来,使其单独调用更加方便,从而简化命令行或脚本方式的控制。

4. 历史记录:命令的抽象化在某种程度上是操作历史的前提,这使得实现Command模式的撤销、重做功能成为可能(本文不讨论Command模式的实现)。

 

2 WPF中的命令

在学习WPF的命令模型时,首先接触到的是ICommand接口(System.Windows.Input名空间)。

public interface ICommand

{

     void Execute(object parameter);

     bool CanExecute(object parameter);

 

     event EventHandler CanExecuteChanged;

}

观察其成员,发现实现该接口的类实例应当包含一个执行逻辑、一个可用性判断逻辑及其变更事件,但是实际上在WPF中使用的RoutedCommand(System.Windows.Input名空间)并没有简单的实现这个接口,而是拓展了该接口并与其他几个类构成了较为复杂的命令模型(主要是为了应用路由事件)。

 

WPF命令模型

 

 

从图中可见,参与一次完整的命令调用需要(至少)三个主体实例:作为指令调用发出方的Command Trigger、作为命令事件激发点的Command Target、以及接收命令事件并进行处理的Parent UIElement。

其中CommandTrigger可以是菜单项、按钮等实现ICommandSource接口的类的实例,它会包含一个名为Command的成员,这个Command是RoutedUICommand的实例(继承自RoutedCommand,后者实现ICommand接口),RoutedCommand类重写了Execute和CanExecute方法,在执行命令时Execute方法会从CommandTarget引发CommandBinding.Executed事件。CommandTarget是界面上的元素,命令可以定位到它可能是出于两种情况,一是通过指定ICommandSource的CommandTarget属性,二是(当没有指定ICommandSource.CommandTarget时)元素持有焦点。

注意:Target单词意味“目标”,是指Command对象激发路由事件的目标;CommandBinding.Executed是路由事件,因此可以在ICommand.Execute方法中调用RaiseEvent触发。

路由事件沿可视化树向上冒泡,沿途的UIElement在其CommandBindings集合中检测事件对应的CommandBinding对象,若检测到则执行CommandBinding.Execute方法,进行真正的逻辑处理。

注意:CommandTrigger并不一定处于CommandTarget和ParentUIElement相同的可视化树中。

 

3 实现WinForm的命令

在传统的WinForm中不存在路由事件,因此可以相对简单的实现Command类,下面直接给出源码。

using System;

using System.Collections.Generic;

using System.Linq;

using System.Text;

using System.Windows.Forms;

using System.ComponentModel;


namespace IEPI.App.Utility

{

    /// <summary>

    /// 一条命令可以包含多个触发器,并同步触发器可用状态

    /// </summary>

    public class Command

    {

        /// <summary>

        /// 实例化命令

        /// </summary>

        /// <param name=”Description”>对命令的描述</param>

        /// <param name=”Excution”>命令触发事件的执行体</param>

        public Command(string Description, EventHandler Excution)

        {

            this.TriggerControls = new List<Control>();

            this.TriggerItems = new List<ToolStripItem>();

            this.Description = Description;

            this.ExcutionBody = Excution;

            this._Enabled = true;

        }


        //当前命令的可用状态,不应直接使用

        bool _Enabled;

        /// <summary>

        /// 获取或设置当前命令的可用状态,这将影响所有触发器的Enabled属性

        /// </summary>

        public bool Enabled

        {

            get { return _Enabled; }

            set

            {

                _Enabled = value;

                foreach (Control TriggerControl in TriggerControls)

                { TriggerControl.Enabled = value; }

                foreach (ToolStripItem TriggerItem in TriggerItems)

                { TriggerItem.Enabled = value; }

            }

        }


        //Control类型触发器

        List<Control> TriggerControls;

        //ToolStripItem类型触发器

        List<ToolStripItem> TriggerItems;


        /// <summary>

        /// 注册触发器

        /// </summary>

        /// <param name=”TriggerControl”>触发器</param>

        public void RegistTrigger(Control TriggerControl)

        {

            this.TriggerControls.Add(TriggerControl);

            TriggerControl.Click += new EventHandler(this.ExcutionShell);

        }

        /// <summary>

        /// 注册触发器

        /// </summary>

        /// <param name=”TriggerToolStripItem”>触发器</param>

       public void RegistTrigger(ToolStripItem TriggerToolStripItem)

        {

            this.TriggerItems.Add(TriggerToolStripItem);

            TriggerToolStripItem.Click += new EventHandler(this.ExcutionShell);

        }


       //命令执行外壳,用于检验可用状态

        void ExcutionShell(object sender, EventArgs e)

        {

            if (_Enabled) ExcutionBody(sender, e);

        }

        //命令执行主体

        EventHandler ExcutionBody;


        /// <summary>

        /// 显式强制执行命令处理过程,无论Enabled状态如何

        /// </summary>

        public void Excute()

        {

            ExcutionBody(thisnew EventArgs());

        }

        /// <summary>

        /// 显式强制执行命令处理过程,无论Enabled状态如何

        /// </summary>

        /// <param name=”sender”>指定事件的发出者</param>

        /// <param name=”e”>事件参数</param>

        public void Excute(object sender, EventArgs e)

        {

            ExcutionBody(sender, e);

        }


       /// <summary>

        /// 获取命令描述

        /// </summary>

       public string Description { getprivate set; }

    }

}


在上述代码中,RegistTrigger包含分别接受ControlToolStripItem实例的两个重载,这是考虑到此两类独立继承自Component、并分别实现了各自的Enabled等属性。

以下链接指向本文原址,其中最末位置包含示例,内容为.csproj项目(以Visual Studio 2010、.Net FrameWork 4.0为平台)。

 

Command in WPF & WinForm

 

Dock属性下自定义控件的尺寸

在Dock不为None时,控件的尺寸会被限制,例如Dock为Top时无法修改其宽度、为Left时则无法修改其高度。然而在某些情况下,我们希望自定义控件满足一定的高宽比,因此必须用编程方式设置其大小(即使控件的Dock被设置为Fill)。此时则必须将设置控件Size属性(或Height、Width属性)的代码放置在Resize事件处理函数中(或其所在函数能够在Resize事件触发时被调用),因为只有在Resize时Dock属性的限制才会放开。若用户没有调整控件或其容器的尺寸,则需要在代码中引发Resize事件(即使用OnResize方法)。

关于FileDialog的路径问题

InitialDirectory RestoreDirectory 使用Reset() 每次运行初始位置 运行时记忆 CurrentDirectory初始值 CurrentDirectory改变
有效 True False 设置的初始值 True %startup% False
有效 False False 设置的初始值 True %startup% True
无效或未设置 True False 上一次运行的值 True %startup% False
无效或未设置 False False 上一次运行的值 True %startup% True
有效 重置项 True 设置的初始值 False %startup% 重置项
无效或未设置 重置项 True 上一次运行的值 False %startup% 重置项

 

1. FileDialog[1] 在使用中,其RestoreDirectory属性很少被用到,这是因为它只与System.Environment.CurrentDirectory的值有关[2](也可以通过System.IO.Directory.GetCurrentDirectory() 方法获取),而与FileDialog实例所使用的路径无关。

2. 当程序中不使用FileDialog.Reset() 方法时,InitialDirectory的值只在程序每次运行第一次调用FileDialog.ShowDialog() 方法时有效,后续调用打开对话框时将使用上一次的路径,这是由系统记忆的[3]

3. 若要在程序中控制任意一次FileDialog.ShowDialog() 所使用的路径,则应将FileDialog.Reset() 和 InitialDirectory属性配合使用,调用Reset方法后FileDialog实例的所有属性均被重置,因此其他必要配置(如FileName、Filter、RestoreDirectory属性等)均需要重新指定,但是不需要对事件进行重新关联。


[1] FileDialog是文件对话框的基类,此处代表其所有子类,包括OpenFileDialog和SaveFileDialog。

[2] 当RestoreDirectory设置为True时,CurrentDirectory的值将保持在程序根目录下不会发生改变,否则将随对话框使用的路径而改变。

[3] 根据C++相关的帖子中透露,CFileDialog的路径保存在注册表HKEY_CURRENT_USERSoftwareMicrosoftWindowsCurrentVersionExplorerComDlg32 LastVisitedMRU中。