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.M11 和 temp.M22,是因为控件受Transform变换后,大小已经发生变化,所以必须换算到变化后的体系下。temp.M11 和 temp.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