【WPF实现Canvas画布(二)】

WPF 实现Canvas画布二之EventHandler

使用WPF实现Canvas画布包括往画布中,添加控件,删除控件,修改样式,撤销和重做;拉伸移动,旋转控件,批量选中等等功能,效果图如下:总效果图

一、EventHandler种类

  • Canvas画布右键移动 Canvas画布右键移动效果如下:在这里插入图片描述图中可以按住整个Canvas 区域去拖动中间的蓝色背景,其中控件都是根据蓝色背景做相对定位的,且整个蓝色背景都使用MatrixTransform进行移动和缩放,这里它通过一个 3x3 的仿射转换矩阵来实现对象的旋转、缩放、倾斜和移动。
    MatrixTransform 使用一个包含六个成员的向量来描述变换:
	M11: 水平方向的缩放因子
	M12: 水平倾斜因子
	M21: 垂直倾斜因子
	M22: 垂直方向的缩放因子
	OffsetX: 水平方向的位移
	OffsetY: 垂直方向的位移
	| M11 M12 0 |
	| M21 M22 0 |
	| OffsetX OffsetY 1 |

具体实现如下:

public class CanvasMoveEventHandler : EventHandler
{
    private readonly Operation _operation;
    private Point _judgeSymbole;
    private ICommonSetting _commonSetting;

    public CanvasMoveEventHandler(ICommonSetting commonSetting) : base(commonSetting)
    {
        _commonSetting = commonSetting;
        _operation = OperationManager.GetOperation(LayerOperationTypeEnum.Move);
    }


    public override void HandleMouseDown(object sender, MouseButtonEventArgs e)
    {
        if (e.ChangedButton == MouseButton.Right && e.ChangedButton == MouseButton.Right)
        {
            _judgeSymbole = e.GetPosition(_commonSetting.DrawingCanvas);
            base.HandleMouseDown(sender, e);
            _operation.BeforeExecute(new BeforeOperationParam(_commonSetting.WidgetsCanvas, new Point(0.5, 0.5), StretchDirectionTypeEnum.None));
            _commonSetting.DrawingCanvas.ContextMenu.IsOpen = false;
        }
    }

    public override void HandleMouseMove(object sender, MouseEventArgs e)
    {
        if (_operation.Actioning && e.RightButton == MouseButtonState.Pressed)
        {
            var currentPoint = e.GetPosition(_commonSetting.DrawingCanvas);
            var d = currentPoint - _startPoint;
            var matrixTransform = _commonSetting.WidgetsCanvas.Element.RenderTransform as MatrixTransform ?? throw new NullReferenceException("no matrixTransform");
            var matrix = matrixTransform.Matrix;
            matrix.OffsetX += d.X;
            matrix.OffsetY += d.Y;
            matrixTransform.Matrix = matrix;
            var transformedPoint = _commonSetting.WidgetsCanvas.Element.TransformToAncestor(_commonSetting.DrawingCanvas).Transform(new Point(0, 0));
            _commonSetting.WidgetsCanvas.Left = transformedPoint.X;
            _commonSetting.WidgetsCanvas.Top = transformedPoint.Y;
            _operation.Execute(new OperationParam(_commonSetting.Selector, _commonSetting.Selector, d, _startPoint, currentPoint));
            _startPoint = currentPoint;
        }
    }

    public override void HandleMouseUp(object sender, MouseButtonEventArgs e)
    {
        if (e.ChangedButton == MouseButton.Right && e.RightButton == MouseButtonState.Released)
        {
            base.HandleMouseUp(sender, e);
            if (_judgeSymbole == _startPoint)
            {
                _judgeSymbole = _startPoint;
                // 这里因为右键菜单弹出菜单的动作,叠加在一起了
                _commonSetting.DrawingCanvas.ContextMenu.IsOpen = true;
                e.Handled = true;
            }
            _operation.AfterExecute(new AfterOperationParam(_commonSetting.WidgetsCanvas));
        }
    }
}

这里是鼠标操作事件的基本模式—MouseUp, MouseMove, MouseDown三种事件,其中核心部分为:

			// 找到WidgetCanvas中的矩阵变换信息
			var matrixTransform = _commonSetting.WidgetsCanvas.Element.RenderTransform as MatrixTransform ?? throw new NullReferenceException("no matrixTransform");
            var matrix = matrixTransform.Matrix;
            matrix.OffsetX += d.X;
            matrix.OffsetY += d.Y;
            matrixTransform.Matrix = matrix;
            // 这里非常关键: 获取矩阵变换后WidgetsCanvas相对于DrawingCanvas的坐标,
            // 这里DrawingCanvas是WidgetsCanvas的父级
            var transformedPoint = _commonSetting.WidgetsCanvas.Element.TransformToAncestor(_commonSetting.DrawingCanvas).Transform(new Point(0, 0));
            _commonSetting.WidgetsCanvas.Left = transformedPoint.X;
            _commonSetting.WidgetsCanvas.Top = transformedPoint.Y;
            _operation.Execute(new OperationParam(_commonSetting.Selector, _commonSetting.Selector, d, _startPoint, currentPoint));
  • Canvas画布Ctrl + 滚轮放大和缩小
    效果图如下:
    在这里插入图片描述
    按住Ctrl +滚轮可以按鼠标点为中心缩放蓝色画布以及其所有子级元素,核心代码如下:
var mousePosition = e.GetPosition(_commonSetting.DrawingCanvas);
double scaleFactor = e.Delta > 0 ? 1.1 : 0.9;
var widgetCanvas = _commonSetting.WidgetsCanvas.Element.RenderTransform as MatrixTransform ?? throw new InvalidCastException("no MatrixTransform");
// 原有的WidgetsCanvas相对于DrawingCanvas的坐标
var oldTransformedPoint = _commonSetting.WidgetsCanvas.Element.TransformToAncestor(_commonSetting.DrawingCanvas).Transform(new Point(0, 0)); 
// 计算变换后的相对坐标
var oldDistanceX = _commonSetting.Selector.Left - oldTransformedPoint.X;
var oldDistanceY = _commonSetting.Selector.Top - oldTransformedPoint.Y;
var t = widgetCanvas.Matrix;
// 以鼠标为中心点,按照比例缩放
t.ScaleAt(scaleFactor, scaleFactor, mousePosition.X, mousePosition.Y);
widgetCanvas.Matrix = t;
var transformedPoint = _commonSetting.WidgetsCanvas.Element.TransformToAncestor(_commonSetting.DrawingCanvas).Transform(new Point(0, 0));
// 选中框为了维持相对于蓝色背景图的位置
_commonSetting.Selector.Left = transformedPoint.X + oldDistanceX * scaleFactor;
_commonSetting.Selector.Top = transformedPoint.Y + oldDistanceY * scaleFactor;
_commonSetting.Selector.Width *= scaleFactor;
_commonSetting.Selector.Height *= scaleFactor;
// 这里仅保存WidgetsCanvas的信息,不对WidgetsCanvas更新
_commonSetting.WidgetsCanvas.Left = transformedPoint.X;
_commonSetting.WidgetsCanvas.Top = transformedPoint.Y;
_commonSetting.WidgetsCanvas.Width *= scaleFactor;
_commonSetting.WidgetsCanvas.Height *= scaleFactor;
_commonSetting.Selector.Render();
_commonSetting.WidgetsCanvas.Element.RenderTransform = widgetCanvas;
e.Handled = true;

这句话非常关键:

_commonSetting.WidgetsCanvas.Element.TransformToAncestor(_commonSetting.DrawingCanvas).Transform(new Point(0, 0));

因为Transform并不会改变在Canvas中的实际位置和大小,所以要使用上述方式获取真正的变换后的点坐标。
注意 : 这里选用MatrixTransform变换的原因,主要是Canvas画布蓝色背景Canvas要与所有的控件一起变换,如果仅仅改变Canvas大小和宽高,并不能将其自己元素也跟随一起变换,所以必须选用矩阵变换。

  • Canvas画布框选
    效果图如下:
    在这里插入图片描述
    鼠标左键按住任意空白处,即可框选,框选的所有控件即可选中。
    (1) HandleMouseDown部分
public static string? IsExistElementUnderMouse(List<Layer> widgets, Point mousePoint)
{
    var res = new List<Layer>();
    foreach (var widget in widgets)
    {
        var newLeftTop = GraphicsUitl.PointRatateByCenter(widget.LeftTop, widget.GetCenter(), widget.Angle);
        var newRightTop = GraphicsUitl.PointRatateByCenter(widget.RightTop, widget.GetCenter(), widget.Angle);
        var newLeftBottom = GraphicsUitl.PointRatateByCenter(widget.LeftBottom, widget.GetCenter(), widget.Angle);
        var xAxios = newRightTop - newLeftTop;
        var yAxios = newLeftBottom - newLeftTop;
        var v = mousePoint - newLeftTop;
        var xAngle = GraphicsUitl.CalculateAngle(xAxios, v);
        var yAngle = GraphicsUitl.CalculateAngle(yAxios, v);
        if (xAngle > 90.0 || yAngle > 90.0) continue;
        var xMagnitude = GraphicsUitl.CalculateProjection(xAxios, v);
        var yMagnitude = GraphicsUitl.CalculateProjection(yAxios, v);
        if (xMagnitude.Length <= widget.Width && yMagnitude.Length <= widget.Height)
        {
            res.Add(widget);
        }
    }
    // 当发生重叠,返回最顶层的元素
    var resFirst = res.Where(c => c.Type == LayerOperationTypeEnum.ImageBox)
        .OrderByDescending(c => c.ZIndex)
        .FirstOrDefault()?.Id;
    return resFirst;
}
// 检查当前鼠标下的控件是否有覆盖情况[1]
var isClickRect = Uitl.IsExistElementUnderMouse(_commonSetting.Layers, e.GetPosition(_commonSetting.WidgetsCanvas.Element));
if (isClickRect != null) return;
_startPoint = Mouse.GetPosition(_commonSetting.DrawingCanvas);
_selectionManager.CancelSelection();
_operation.BeforeExecute(new BeforeOperationParam(_commonSetting.BoxSelector, _startPoint, StretchDirectionTypeEnum.None));
Mouse.Capture(sender as FrameworkElement);
e.Handled = true;

[1] 情况如下所示:
在这里插入图片描述
(2) HandleUp 部分
当点击这些空白处则取消选中框,选中框四个点坐标及旋转角度信息:

var rect = new RectPoints()
{
    LeftTop = _boxSelector.LeftTop,
    TopRight = _boxSelector.RightTop,
    BottomLeft = _boxSelector.LeftBottom,
    BottomRight = _boxSelector.RightBottom,
    Angle = 0.0
};

检查每一个旋转后的图形的四个点坐标是否在BoxSelector的框选范围内,这里乘以 temp.M11temp.M22,是因为控件受Transform变换后,大小已经发生变化,所以必须换算到变化后的体系下。temp.M11temp.M22可具体看上面的解释。

var matrixTransform = _commonSetting.WidgetsCanvas.Element.RenderTransform as MatrixTransform ?? throw new NullReferenceException("no matrixTransform");
var temp = matrixTransform.Matrix;
var selectionRes = new List<Layer>();
var widgetCanvas = _commonSetting.WidgetsCanvas;
foreach (var item in _commonSetting.Layers)
{
    var isRange = GraphicsUitl.CheckIsRange(rect, new RectPoints
    {
        LeftTop = new Point(item.Left * temp.M11 + widgetCanvas.Left, item.Top * temp.M22 + widgetCanvas.Top),
        TopRight = new Point(item.Left * temp.M11 + item.Width * temp.M11 + widgetCanvas.Left, item.Top * temp.M22 + widgetCanvas.Top),
        BottomLeft = new Point(item.Left * temp.M11 + widgetCanvas.Left, item.Top * temp.M22 + item.Height * temp.M22 + widgetCanvas.Top),
        BottomRight = new Point(item.Left * temp.M11 + item.Width * temp.M11 + widgetCanvas.Left, item.Top * temp.M22 + item.Height * temp.M22 + widgetCanvas.Top),
        Angle = item.Angle
    });
    if (isRange) selectionRes.Add(item);
}

重点是检查旋转后的点坐标信息,找出所有点集,上下左右四个边界点,然后显示选中框。

  • 单个控件的左键单击动作
    效果图如下:
    在这里插入图片描述
    由于一开始每一个控件的上方没有选中框,所以所有的事件都绑定到每一个具体控件上,这种选择是单选
public void SelectionSingle(string widgetId)
{
    var selectedELement = _commonSetting.Layers.First(c => c.Id == widgetId);
    selectedELement.IsSelected = true;
    var matrixTransform = _commonSetting.WidgetsCanvas.Element.RenderTransform as MatrixTransform ?? throw new NullReferenceException("no matrixTransform");
    var transformedPoint = _commonSetting.WidgetsCanvas.Element.TransformToAncestor(_commonSetting.DrawingCanvas).Transform(new Point(0, 0));
    var matrix = matrixTransform.Matrix;
    _commonSetting.Selector.Width = selectedELement.Width * matrix.M11;
    _commonSetting.Selector.Height = selectedELement.Height * matrix.M22;
    _commonSetting.Selector.Left = selectedELement.Left * matrix.M11 + transformedPoint.X;
    _commonSetting.Selector.Top = selectedELement.Top * matrix.M22 + transformedPoint.Y;
    _commonSetting.Selector.IsVisiable = true;
    _commonSetting.Selector.Angle = selectedELement.Angle;
    _commonSetting.Selector.ZIndex = 999;
    _commonSetting.Selector.Render();
    // 执行外部的执行逻辑: 往外部传入对应的Layer
    _styleManager.OnStyleChangeEvent(new StyleChangeEventArgs(_commonSetting.Layers.FirstOrDefault(c => c.IsSelected)));
}

后面接着写选中框的EventHandler部分
gitHub地址: https://github.com/renjianyanhuo123/CanvasDemo

  • 15
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值