C# 示例代码/文章:扩展 C# (.NET) 中的 GDI+ 功能 - 第一部分
本文档介绍了如何在 C# 中扩展 GDI+ 的功能。当程序员想要开发严肃的图像处理应用程序时,会注意到 GDI+ 有一个令人惊讶的缺点,那就是缺少一个能够以用户交互方式选择图像区域的有用 API。

摘要
在 Adobe Photoshop、Google Picasa 或 Coral Paint Shop Pro 等图像处理应用程序中,总是需要选择图像的一部分。也许用户想放大图像的某个部分,或者想处理图像的某个选定区域(请参阅下面的 **图 1**)。
无论情况如何,图像处理应用程序通常都需要支持上述功能。当使用 MFC 编写应用程序时,程序员可以使用 `DrawDragRect (…)` API。`DrawDragRect (…)` 是 CDC 类的一个成员。
尽管几乎所有的 MFC API 都有等效的 Win32 API,但令人惊讶的是,Microsoft 没有提供 `DrawDragRect (…)` MFC API 的 Win32 等效项。在 MFC-Win32 世界中这不成问题,但在 .NET 世界中就麻烦了。.NET 中没有这样的 API。
使用 C# 等 .NET 语言编写/移植的图像处理应用程序在这方面损失很大,因为 C# 或其他 .NET 语言中没有 `DrawDragRect (…)` API。问题更加复杂,因为没有可以通过 `pInvoke`(一种常用的从 .NET 语言调用 Win32 API 的技术)使用的 Win32 API。本文提供了一个用 C# 编写的 `DrawDragRect (…)` API 的实现,该实现可以非常轻松地集成到您的 C# 窗体程序或任何其他基于 .NET 窗体的程序中。

实现细节
整个代码都嵌入在 *CDrawDragRect.cs* 文件中。`CDrawDragRect` 类实现了该代码。使用此功能的用户只需从 `CDrawDragRect` 继承其窗体,而不是标准地从 `Form` 类继承。
public partial class MainForm : CDrawDragRect
//Form
{
...
//other implementation details skipped for brevity
}
这是使用者所需做的全部工作。
在 `CDrawDragRect` 类中,`DrawDragRect(MouseEventArgs e)` 函数负责此功能。该函数在窗体的 `OnMouseMove(MouseEventArgs e)` 事件响应中被调用。如您所见,Win32 API(通过 `pInvoke`)在该功能的实现中得到了广泛使用。
`DrawDragRect(MouseEventArgs e)` API 首先创建 4 个 Windows 区域
rgnOld
rgnNew
rgnDiff
rgnDiffOld
`rgnDiff` 区域实际上是 `rgnOld` 和 `rgnNew` 区域的二补数差集,其中 `rgnOld` 是 `rgnNew` 的收缩,而 `rgnNew` 是由 `rcNew` 矩形构建的。因此,`rgnDiff` 等效于一个带有 2 像素边框的矩形。`rgnDiff` 现在存储在 `rgnDiffOld` 中以供将来使用,如下所示。
这个过程在 `rgnDiff` 中重复进行,但这次 `rgnNew` 被赋值为 `rcOld` 矩形。最后,对 `rgnDiffOld` 和 `rgnDiff` 进行异或运算,并将结果区域存储在 `rgnDiff` 中。
该区域被选择到从窗体客户区域的图形对象创建的 HDC 中。在该区域中使用 `PATINVERT` 光栅操作码(它使用布尔 OR 运算符将指定图案的颜色与目标矩形的颜色结合起来)进行 `PatBlt (…)` 操作,从而产生期望的效果。
请注意,`rcOld` 和 `rcNew` 是矩形,它们分别表示用户在鼠标按下拖动操作中将生成的老矩形和新矩形的大小。`rcOld` 在擦除旧矩形方面起着重要作用。这同样通过第二次调用 `PatBlt (…)` 并使用 `PATINVERT` 光栅操作码来实现。
最后几行只是清理内存设备上下文分配。这有助于将系统范围内的 GDI 对象计数保持在最低水平,并防止任何 GDI 特定的内存泄漏。
关于示例应用程序
该应用程序是一个用 C# 编写的基于窗体的应用程序。它在窗体加载时显示一个文件对话框,用于选择图像。用户选择图像文件后,可以选中图像的一个小区域,该区域将被渲染到一个子窗体中。
我已按照一些读者的建议添加了调整大小、拖放和保存功能。
我参考了两个项目
- Image traveller.Zip
- ImageTraveller_Panel_NoResize.Zip
在 ImageTraveller 项目中,`CDrawDragRect` 类继承自 `Form` 类;而在 ImageTraveller_Panel_NoResize 项目中,同一个 `CDrawDragRect` 类继承自 `Panel` 类。从 Panel 控件派生 `CDrawDragRect` 类,可以方便地将 `CDrawDragRect` 类嵌入到各种容器中。另一个不同之处在于,在 ImageTraveller_Panel_NoResize 项目中,用户在调整大小或拖放选择矩形后需要双击才能看到选定的部分。
调整大小功能
通过抓取任意一个角矩形(如下所示),用户可以调整选择矩形的大小。调整大小功能有两种类型,如选项菜单所示,即“弹跳”选项和“滑动选择”选项。“滑动”选项是调整矩形大小的常规选项,“弹跳”选项是用于变化的。
调整大小功能的内部工作原理
与之前一样,操作在鼠标移动消息中进行。但首先需要检测用户是在哪个角上执行了鼠标按下操作。这可以是左上角、左下角、右上角或右下角中的任何一个。变量
int nResizeRT
int nResizeBL
int nResizeLT and
int nResizeRB
被引入用于此目的(我们称之为调整大小变量组)。在任何给定时间,上述变量中的一个可以为 1,其余为 0。这在鼠标按下消息处理程序中设置。完成此操作后,在鼠标移动消息处理程序中,`rcNew` 矩形计算如下:
rcNew.X = rcBone.X;
rcNew.Y = rcBone.Y;
rcNew.Width = pt.X - rcNew.Left;
rcNew.Height = pt.Y - rcNew.Top;
其中 `pt` 是当前鼠标位置。
不失一般性,我们假设用户在右下角按下了鼠标并正在调整选择矩形的大小。当用户拖动时,鼠标可以向四个方向中的任何一个移动:右、左、上或下。让我们以用户向右或向左移动的情况为例。如果鼠标向右移动,一切正常。如果它向左移动并且鼠标穿过了选择矩形的左侧(原始矩形),则上述 `rcNew` 宽度的计算将变为负数。检测到这种情况后,将重新调整宽度。然后,如以下所示,调整大小变量也将被重置。
if (rcNew.X > pt.X)
{
rcNew.X = pt.X;
rcNew.Width = rcBegin.X - pt.X;
nResizeRB = 0;
nResizeBL = 1;
rcBegin = rcNew;
}
结果是,这就像用户抓住了选择矩形的左下角,并通过向左拖动鼠标来进行调整大小操作。最后,在鼠标移动函数结束时,调用
void DrawDragRect(MouseEventArgs e)
来像以前一样绘制矩形。
其余情况也以类似方式处理。
拖放功能
在此功能中,用户可以拖放选择矩形。为此,用户只需在选择矩形内按住鼠标左键并拖动。
拖放功能的内部工作原理
当用户在选择矩形内按下鼠标左键时,消息会到达 `OnMousedown` 消息处理程序,在该处理程序中,我检查鼠标指针是否在选择矩形内,以下代码对此进行了演示:
if (rcBone.Contains(pt))
{
nBone = 1; //=1 if pt is inside the selection
rectangle
ptNew = ptOld = pt;
nResizeBL = nResizeLT = nResizeRB =
nResizeRT = 0;
}
在 `OnMousemove` 处理程序中,进一步检测到这一点,并计算 `rcNew`,然后调用 `DrawDragRect (…)`,如下所示:
if (nBone == 1) //Moving the rectangle
{
ptNew = pt;
int dx = ptNew.X - ptOld.X;
int dy = ptNew.Y - ptOld.Y;
rcBone.Offset(dx, dy);
rcNew = rcBone;
DrawDragRect(e);
ptOld = ptNew;
}
保存功能
保存功能在显示选定部分的子对话框中提供。