在 C# 中控制绘制的形状
如何通过鼠标手势在屏幕上绘制和管理形状
引言
在长时间编写主要业务逻辑和使用现成库的简单用户界面(在 Web 和桌面)之后,前一段时间我决定弄清楚如何在屏幕上绘制对象,并拥有一个允许用户通过简单拖动(类似于拖动窗口)来移动它们的 UI。由于我没有找到相关的文章,我决定自己摸索,然后发布我的发现和想法。
我为自己写了这篇文章,但最终决定出于两个原因撰写这篇博文
- 这是对像我一样的人来说一个学习工具
- 我们可以合作探讨如何以不同方式完成这些事情
如果你觉得这很有趣,请阅读我的下一篇文章
- 文章 #2:为形状添加物理引擎!
背景
我希望能够绘制形状并管理它们在屏幕上的移动,几乎就像 Windows 资源管理器 UI 中那样。
功能目标
我的目标是拥有一个可以执行以下操作的应用程序
- 绘制任何对象
- 用户可以选中或多选它们
- 访问选择框
- 用户可以拖动项目
- 缩放功能
- 能够拥有相同对象的多个视口
- 使用真实世界单位绘制这些项目
简单但不同;并且足以让我乐在其中。
设计目标
- 易于扩展
- 是否易于迁移到不同平台,例如 WPF 或 Java?
- 可维护
代码概述(设计)
从高层来看,该应用程序由一个形状宇宙(模型)、一个手势管理器(控制器)和一个提供绘图区域并知道如何解释手势的 UI(视图)组成。我想这更像是一个 MVC 模式。我还依赖于诸如 IShape
等接口,至少在我认为需要扩展性的地方。
视图和控制器逻辑
视图主要位于 Form1.cs(好名字)和用于绘图的 Canvas.cs 中。Form1
告诉画布绘制自己,并解释手势,然后手势被传递给不同的管理器(控制器)来管理对象。将 Canvas
作为一个单独的类提取出来,使我能够轻松创建同一宇宙的不同视口。例如,在我的情况下,一个视口显示常规视图,另一个则进行缩放和旋转。
解释和管理手势
我发现最有趣且需要正确实现的部分是多选。我希望它非常直观和标准,因此我模仿了 Windows 资源管理器(至少在很大程度上)。
所有手势都由手势管理器解释,然后手势管理器告诉动作管理器如何管理形状。同样,宇宙和形状管理器是解耦的。
每个动作都有自己的管理器,因为我觉得这简化了整体逻辑流。我想在某些时候它可能会变得过于复杂,但至少你知道每个类都会很简单,并且只有一个目的。
形状模型
这里的基本对象是 IShape
。当想要引入新形状时,只需实现 IShape
和可能的 ISelectable
。宇宙由 0、1 或更多形状(一个列表)组成。有人可能会质疑为什么 ISelectable
和 IShape
是分开的,也许我有点急于解耦事物,在更大的系统中可能更需要。在某个时候驱使我这样做的是,我希望有一个用于多选边界框的绘制形状,它本身是不可选择的(然而,我还通过从不将其作为宇宙的一部分来避免其可选择)。无论如何,即使在这一点上,它也可以作为 IShape
接口上的 IsSelectable
实现。因此,一个设计决策最终可能在一个大型系统中成为遗留代码。
Using the Code
希望上述概述能说明大部分情况。以下是我认为重要的一些细节。
模型的核心是 IShape
、IDrawable
和 ISelectable
。
public interface IShape : IDrawable
{
bool Contains(PointF p);
void Move(PointF delta);
void FillRegion(Region region);
}
IDrawable
和 ISelectable
是非常简单的接口,只有一个方法,在这里它们主要用作标记接口。如上所述,是否需要这样做是值得商榷的,归根结底这是一个设计决策。例如,这可以使我们非常容易地拥有永远无法触摸或移动的背景图像。无论如何,每当你有一个新形状时,你只需实现这些方法和接口,然后它就可以成为形状宇宙的一部分。
paint
方法只是将形状绘制到传入的 Graphics
参数中。你可以假设任何对象的 paint
都是从下往上完成的,即只绘制你的形状,如果有东西在后面,它将被这个形状实例隐藏。对于选定的项目,我只是勾勒出它们的轮廓。出于性能原因,图形路径仅在模型更改时计算。在这种情况下,唯一可以更改事物的方法是 Move
,但如果需要,也可以类似地进行 Resize
。
public void Paint(Graphics g)
{
g.FillPath(Brushes.Yellow, gp);
if (_isSelected)
g.DrawPath(Pens.Black, gp);
}
public void Move(PointF d)
{
_border.X += d.X;
_border.Y += d.Y;
gp.Reset();
gp.AddEllipse(_border);
}
Contains
和 FillRegion
方法是帮助框架查看鼠标或选择框是否在其上方(如果它们是可选的)的方法。同样,这里我只是重复使用上面计算的图形路径来提供帮助。Contains
方法使用我们的朋友 GraphicsPath
中的 IsVisible
方法相当直接。FillRegion
用于查看边界框是否正在选择我们的形状。它的工作方式是:框架将一个空区域传递给 FillRegion
,并期望此代码用所需的形状填充它。然后选择框在逻辑上与顶部 And
运算。如果存在任何重叠,则结果 Region
将不会为空,从而表明应选择该形状。
public void FillRegion(Region r)
{
r.Union(gp);
}
在 Canvas
中,Drawing2D
中的 Matrix
派上了用场。这使我们能够更改视口的方向并根据需要缩放它。唯一需要注意的是,要使用 MatrixOrder.Append
参数,否则它可能无法按预期工作。此变换可用于 paint
dc.Transform = GetWorldToViewTransform();
以及解释鼠标选择时
gestureControl.HandleMouseDown(
((Canvas)sender).TransformToWorldCoordinates(e.Location),
IsControlPressed());
关注点
处理用户选择和手势主要在鼠标抬起事件上,而不是我最初认为的鼠标按下事件。如你所见,现在我只是返回 false
。
每个 canvas
都有自己的坐标系,并且可以在自身和父坐标系(或在这种情况下,世界坐标)之间进行转换。这使我们能够做一些有趣的事情,以两种完全不同的视图来观察宇宙。在我的示例中,我展示了宇宙中更大的一部分(缩小了 50%)并且还进行了旋转(为了好玩)。
我有一个形状组(它碰巧也是一个形状)。顺便说一下,这就是宇宙的实现方式。然而,这里的想法是,实现组形状(如 PowerPoint 中)应该是微不足道的。我们所需要做的就是从宇宙中移除相关的形状,将它们添加到形状组中,然后将其添加回宇宙。
点击测试是我不太清楚如何处理的事情,特别是以一种易于重用的方式。我最终通过用形状填充一个区域进行点击测试,并与当前光标进行逻辑 AND
运算。如果有任何结果,则意味着有点击。这很好,因为它很容易处理多种形状,包括像甜甜圈这样的空心形状。
最后,我尝试在真实世界坐标和尺寸与屏幕之间进行某种形式的转换。我失败得很惨。你可以看到我尝试这样做的代码,但这没有奏效,而且最终如果你有多个分辨率/DPI 不同的显示器(例如,当形状从一个显示器移动到另一个显示器时,你如何使它的像素大小发生变化?),它也无法奏效。
尽情享用!
历史
- 2011年3月15日:首次发布