65.9K
CodeProject 正在变化。 阅读更多。
Home

在 C# 中控制绘制的形状

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.75/5 (11投票s)

2011年3月15日

CPOL

7分钟阅读

viewsIcon

77692

downloadIcon

6002

如何通过鼠标手势在屏幕上绘制和管理形状

引言

在长时间编写主要业务逻辑和使用现成库的简单用户界面(在 Web 和桌面)之后,前一段时间我决定弄清楚如何在屏幕上绘制对象,并拥有一个允许用户通过简单拖动(类似于拖动窗口)来移动它们的 UI。由于我没有找到相关的文章,我决定自己摸索,然后发布我的发现和想法。

我为自己写了这篇文章,但最终决定出于两个原因撰写这篇博文

  • 这是对像我一样的人来说一个学习工具
  • 我们可以合作探讨如何以不同方式完成这些事情

如果你觉得这很有趣,请阅读我的下一篇文章

背景

我希望能够绘制形状并管理它们在屏幕上的移动,几乎就像 Windows 资源管理器 UI 中那样。

功能目标

我的目标是拥有一个可以执行以下操作的应用程序

  • 绘制任何对象
  • 用户可以选中或多选它们
  • 访问选择框
  • 用户可以拖动项目
  • 缩放功能
  • 能够拥有相同对象的多个视口
  • 使用真实世界单位绘制这些项目

简单但不同;并且足以让我乐在其中。

设计目标

  • 易于扩展
  • 是否易于迁移到不同平台,例如 WPF 或 Java?
  • 可维护

代码概述(设计)

从高层来看,该应用程序由一个形状宇宙(模型)、一个手势管理器(控制器)和一个提供绘图区域并知道如何解释手势的 UI(视图)组成。我想这更像是一个 MVC 模式。我还依赖于诸如 IShape 等接口,至少在我认为需要扩展性的地方。

视图和控制器逻辑

视图主要位于 Form1.cs(好名字)和用于绘图的 Canvas.cs 中。Form1 告诉画布绘制自己,并解释手势,然后手势被传递给不同的管理器(控制器)来管理对象。将 Canvas 作为一个单独的类提取出来,使我能够轻松创建同一宇宙的不同视口。例如,在我的情况下,一个视口显示常规视图,另一个则进行缩放和旋转。

解释和管理手势

我发现最有趣且需要正确实现的部分是多选。我希望它非常直观和标准,因此我模仿了 Windows 资源管理器(至少在很大程度上)。

所有手势都由手势管理器解释,然后手势管理器告诉动作管理器如何管理形状。同样,宇宙和形状管理器是解耦的。

每个动作都有自己的管理器,因为我觉得这简化了整体逻辑流。我想在某些时候它可能会变得过于复杂,但至少你知道每个类都会很简单,并且只有一个目的。

形状模型

这里的基本对象是 IShape。当想要引入新形状时,只需实现 IShape 和可能的 ISelectable。宇宙由 0、1 或更多形状(一个列表)组成。有人可能会质疑为什么 ISelectableIShape 是分开的,也许我有点急于解耦事物,在更大的系统中可能更需要。在某个时候驱使我这样做的是,我希望有一个用于多选边界框的绘制形状,它本身是不可选择的(然而,我还通过从不将其作为宇宙的一部分来避免其可选择)。无论如何,即使在这一点上,它也可以作为 IShape 接口上的 IsSelectable 实现。因此,一个设计决策最终可能在一个大型系统中成为遗留代码。

Using the Code

希望上述概述能说明大部分情况。以下是我认为重要的一些细节。

模型的核心是 IShapeIDrawableISelectable

 public interface IShape : IDrawable
 {
    bool Contains(PointF p);
    void Move(PointF delta);
    void FillRegion(Region region);
 }

IDrawableISelectable 是非常简单的接口,只有一个方法,在这里它们主要用作标记接口。如上所述,是否需要这样做是值得商榷的,归根结底这是一个设计决策。例如,这可以使我们非常容易地拥有永远无法触摸或移动的背景图像。无论如何,每当你有一个新形状时,你只需实现这些方法和接口,然后它就可以成为形状宇宙的一部分。

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);
 }

ContainsFillRegion 方法是帮助框架查看鼠标或选择框是否在其上方(如果它们是可选的)的方法。同样,这里我只是重复使用上面计算的图形路径来提供帮助。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日:首次发布
© . All rights reserved.