如何正确过滤鼠标事件可能导致的不同操作?

How to properly filter the different actions that may result from a mouse event?

我正在设计 CAD 应用程序,但不确定如何以干净的方式设计鼠标事件的事件处理。

为简单起见,假设我在 UI 中有两个按钮:CreateSquare 和 CreateCircle。当用户单击其中一个时,我希望应用程序等待 canvas 中的单击,并在用户单击后在指定位置创建正方形或圆形。

据我了解,有必要在我的 canvas 中监听 MouseDown 事件,并在处理程序中编写如下内容:

bool CreatingSquaresMode;
bool CreatingCirclesMode;

private void ModelViewport_MouseDown(object sender, MouseButtonEventArgs e)
{
    if (CreatingSquaresMode) { 
        // create square at selected position. 
    }
    if (CreatingCirclesMode) { 
        // create circle at selected position. 
    }
}

然而这看起来很臭,特别是因为我将有许多不同的创建命令并且很快就会失控。它还破坏了 MVVM,因为我希望将 CreateSquare 和 CreateCircle 按钮绑定到命令,而不必担心 MouseDown 事件。

我考虑过的一个替代方案是状态机,我将在其中确定应用程序的所有可能模式,并以更优雅的方式复制上述 if-nest 或 switch-case。仍然感觉这不是正确的解决方案,但它会更好。

第二种选择——我认为是正确的——将以某种方式在我的 CreateSquare 命令中监听 MouseDown 事件:

   private void CreateSquare() {
       //do something here
       //wait for user mouse input
       //create square
   }

但我不知道这是否可能。

处理这种情况的正确方法是什么?我确定有一种设计模式应该可以帮到我。

有许多不同的方法可以解决您的问题,因此没有 "correct" 方法,但是肯定有更好的方法。我提供了一个供您考虑,但我永远不能说我(或其他任何人)的设计是 "best",这由您决定。

在您的解决方案中,您使用第一次按下按钮来设置名为 "CreatingShapeMode" 的属性。您是否只想在任何特定时间设置一种模式?如果是这样,请考虑使用 enum

在我看来,您的 MouseDown 事件处理程序不应该执行绘制形状的工作(因为您指出此列表只会变得更大)。相反,我会考虑使用一些自定义形状绘图 类,它们都是从基础 "ObjectCreator" 类型继承的。您的第一个按钮点击将设置正确的类型,例如圆形或方形;第二次单击 canvas(这会触发鼠标按下事件)只会调用 createShape 方法;它不需要任何 if 语句,因为对象创建者知道如何绘制形状。

这是一个控制台应用程序,它使用最少的代码来演示我想说的内容。显然它需要大量的适应才能适应你的问题。

using System;

namespace ConsoleApplication27
{
    class Program
    {
        private static ObjectCreator creator;
        static void Main(string[] args)
        {
            creator = new CircleCreator();//change this to change the "shape" you get

            Console.ReadLine();
        }

        private static void YourMouseDownMethod()
        {
            creator.CreateObject();//no if statements required, the createObject method does the work
        }
    }



    abstract class ObjectCreator
    {
        public abstract void CreateObject();
    }

    class SquareCreator:ObjectCreator
    {
        public override void CreateObject()
        {
            Console.WriteLine("Create Square");
        }
    }

    class CircleCreator:ObjectCreator
    {
        public override void CreateObject()
        {
            Console.WriteLine("Create Circle");
        }
    }
}

您提到您希望将按钮绑定到命令,您可以使用命令对象设置正确的 ObjectCreator 类型来实现。

    class SetObjectCreatorCommand:ICommand 
    {
        private readonly ObjectCreator _creator;

        public SetObjectCreatorCommand(ObjectCreator creator)
        {
            _creator = creator;//for each shape type you would create an instance of this class
            //e.g. pass in a circleCreator for circle shapes etc...
        }

        public bool CanExecute(object parameter)
        {
            return true;
        }

        public void Execute(object parameter)
        {
            //Set the creator class here, you can bind a button's command property to a Command object
        }

        public event EventHandler CanExecuteChanged;
    }

对于那些正在阅读的人,这就是我解决这个问题的方法。上面的回答帮助证实了我的想法。

首先,我创建了一个具有以下状态和触发器的状态机(我使用了 Stateless):

public enum States
{
    Idle,
    CreatingShape
}

public enum Triggers
{
    CreateShape,
    CancelCurrentAction
}

public class AppStateMachine : ObservableObject
{
    public StateMachine<States, Triggers> _stateMachine = new StateMachine<States, Triggers>(States.Idle);

    public AppStateMachine()
    {
        _stateMachine.Configure(States.Idle)
            .Permit(Triggers.CreateShape, States.CreatingJunction)
            .Ignore(Triggers.CancelCurrentAction);

        _stateMachine.Configure(States.CreatingShape)
            .Permit(Triggers.CancelCurrentAction, States.Idle)
            .Ignore(Triggers.CreateShape);
    }

然后,为了尽我所能保留 MVVM,我将 UI 按钮绑定到 "SetCreationMode" 命令,命令参数是要创建的形状类型。命令是这样的:

XAML:

<RibbonButton x:Name="CreateSquare" Command="{Binding SetShapeCreationCommand}" CommandParameter="{Binding SquareShape}" Label="Create Square"/>

视图模型:

    private void SetShapeCreationMode(ShapeTypeEnum ShapeType)
    {
        SelectedShapeType = ShapeType;
        _mainViewModel.AppState._stateMachine.Fire(Triggers.CreateShape);
    }

这样,当您按下按钮时,App 状态机会触发 "CreatingShape" 状态,并且 "SelectedShapeType" 变量会设置为您的形状(在示例中为正方形)。

之后,您仍然需要将 canvas 单击作为事件处理(我正在尝试使用 MVVMLight 中的 EventToCommand,但还没有实现飞跃。)事件处理方法是这样的:

private void ModelViewport_MouseDown(object sender, MouseButtonEventArgs e)
    {
        if (_mainViewModel.AppState.IsInState(States.CreatingShape))
        {
            // retrieve mouse position

            // Execute command with retrieved position
            _CreateShapeViewModel.CreateShapeGraphicallyCommand.Execute(Position);
        }
    }

这样您就可以在选定的鼠标位置使用命令,而无需在 EventHandler 中使用大量代码,而 EventHandler 位于 ViewModel 中。

这是最好的处理方式吗?不知道,希望对你有帮助。