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

QuickFill .NET:一种高效的洪水填充算法,适配 GDI+

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.69/5 (10投票s)

2004年2月26日

6分钟阅读

viewsIcon

72330

downloadIcon

630

本文的目的是展示一种改编 John R. Shaw 出色的 QuickFill 算法的方法,使其能够与 GDI+ 和 .NET Framework 配合使用。

Sample Image - QuickFillNET.png

引言

本文的目的是展示一种改编 John R. Shaw 出色的 QuickFill 算法 的方法,使其能够与 GDI+ 和 .NET Framework 配合使用。

让我先承认,我没有花很多时间去完全理解 John 的算法。当我读到递归洪水填充算法的优点是“即使是初学者程序员也易于实现”时,我决定如果我发现自己无法理解高级程序员版本,那将对我自己是个打击。所以,我给这篇文章打了 '5' 分,然后继续前进。

我曾几次回来阅读评论。其中一条评论提出了我本人也考虑过的问题:如何在 GDI+ 应用程序中使用该算法。GDI+ 没有洪水填充功能,这对于需要这种功能的初学者程序员来说几乎是无用的。

于是,我开始着手让 John 的代码与 GDI+ 和 .NET Framework 协同工作。这个示例*并非*将 John 的算法改编为与原生 Win32 GDI+ API 配合使用,尽管这当然是可行的。

工作原理

首先,我不想修改 John 的代码。我害怕如果我以错误的方式看一眼那精妙的算法,它就会崩溃,而我将无法修复它。但我希望它能在 .NET 中工作。

Visual C++ .NET 允许我们将 C++ 代码编译成 MSIL。使用 `/clr` 开关,编译器就会生成一个 .NET 程序集,然后任何 .NET 语言都可以使用它。但这并没有多大用处,除非你也创建一些公开了功能的 CLR 数据类型。`/clr` 并不能神奇地将 C++ 数据类型变成 CLR 数据类型。

将现有 C++ 功能暴露给 .NET 的最佳方法是将其封装在一个新的 CLR 类中并公开一个新的接口。这就是我所做的,而且我做得非常基础。我没有暴露 John 的 `CQuickFill` 类的所有公共功能——而是暴露了主要功能——`QuickFill()` 方法(我将其重命名为 `Fill`)。我还开始暴露用位图图案进行洪水填充的能力,但尚未完全实现。

我的 CLR 类名为 QuickFill,它有几个可以调用的重载静态方法。与 John 的类不同,我的类不需要实例化即可使用。以下是代码外观的片段:

public __gc class QuickFill
{
private:
 
    // static members only; a user never constructs this class
    QuickFill()
    {
    }
 
public:
 
    static System::Drawing::Image* Fill(System::Drawing::Bitmap* bitmap, 
                                        int x, int y, 
                                        System::Drawing::Color fc)
    {
        CQuickFill qf;
        qf.QuickFill(bitmap, x, y, RGB(fc.R, fc.G, fc.B));
 
        return static_cast<Image*>(bitmap);
    }
};

我的 `QuickFill` 类是一个 CLR 数据类型(它被 `__gc` 装饰)。因为它不打算被实例化,所以它隐藏了它的构造函数。`Fill` 方法(不是 `QuickFill`,因为该名称会与类名冲突,并且编译器会抱怨构造函数返回一个值…)简单地实例化 John 的 `CQuickFill` 类并调用它的 `QuickFill` 方法。Voilà,魔法,太棒了。

但这并不是全部。GDI+ 的支持在哪里?我如何将 .NET Framework Bitmap 对象直接传递给 John 的代码,而他的代码期望的是 MFC `CBitmap` 对象?

调包换李

John 的 `QuickFill` 类依赖于另一个名为 `CDibBitmap` 的实用类。`CDibBitmap` 是对 Win32 DIB(设备无关位图)的抽象,它提供了对原始位图数据简化的访问和控制。`QuickFill` 包含两个 `CDibBitmap` 对象——一个用于填充的位图,另一个用于存储第二个图案位图,用于图案填充。`CDibBitmap` 继承自 MFC 类 `CObject`,并使用多个 MFC 支持类,包括 `CBitmap`。

我不想在此实验中涉及 MFC。MFC 很棒,但也许对于这个应用程序来说它有点过于庞大。我的意思是,如果我们创建一个轻量级的洪水填充组件供 .NET Framework 使用,我们为什么要引入庞大的 MFC 库呢?也许如果我们迁移大量更复杂的基于 MFC 的代码,但今天不行。

我注意到 `CQuickFill` 有几个地方。唯一使用 MFC 的地方是它接受为 `QuickFill` 方法参数的 `CBitmap`,而该方法只是将其传递给了 `CDibBitmap`。John 显然提供了对 `CBitmap` 的支持,作为对消费应用程序(很可能是一个 MFC 应用程序)的便利功能。

有了这些知识,我开始制定一个计划。首先,我将编写自己的 `CDibBitmap`,它将实现与原始 `CDibBitmap` 相同的接口,以便 `CQuickFill` 可以继续使用它。其次,我将剥离 MFC 代码。这比说起来容易:当原始 `CDibBitmap` 被排除在外后,唯一剩下的 MFC 代码就是 `QuickFill` 方法的 `CBitmap` 参数。一个简单的 `typedef` 使 `CBitmap` 成为 .NET Framework `System::Drawing::Bitmap` 类型的同义词,而我的替换 `CDibBitmap` 类正是以此为输入的。

// We're not using MFC; Substitute the .NET Framework Bitmap class (GDI+) 
// for CBitmap
typedef System::Drawing::Bitmap CBitmap;

当然,如果我没有如此坚决地不触碰 John 的代码,我本可以修改他的源代码来纠正这个问题。另一个好处是:如果 John 对他的类进行任何小的更改,我都不需要重新应用任何更改到我的副本上。

新的 `CDibBitmap` 类需要实现与旧 `CDibBitmap` 相同的接口,并且需要操作 GDI+ Bitmap。原始 `CDibBitmap` 类指定并实现了一系列成员——比我愿意实现的要多得多。幸运的是,消费 `CQuickFill` 类只调用了大约 10 个——而且它们相对容易实现。这个实现中最有趣的部分是,我们如何实现对像素数据的快速读写访问。GDI+ Bitmap 类公开了一个 `LockBits()` 方法,该方法返回一个指向原始位图数据的数组的指针,其布局由请求的位深度格式决定。这在功能上类似于 GDI API `GetDIBits()`,而原始 `CDibBitmap` 类使用了这个 API。在我的实现中,我在 `CDibData::CreateDIB()` 方法中调用 `LockBits()` 并存储返回的指针,该指针通过在 `CDibData::SetDIBits()` 中调用 `UnlockBits()` 来“解锁”。我还必须实现一个小技巧,以确保即使客户端从未显式调用 `CDibBitmap::SetDIBits()`,也会进行相应的 `UnlockBits()` 调用。在这种情况下,客户端是 `CQuickFill`,它的逻辑是围绕 `GetDIBits()` 构建的,而 `GetDIBits()` 不需要相应的“解锁”方法。

通过直接内存访问原始位图数据,可以轻松实现 `CDibData()` 中的其他核心方法,即 `GetPixel()` 和 `SetPixel()`。

COLORREF CDibData::GetPixel(int x, int y) const
{
      BYTE* pel = (BYTE*)m_pbmpSrcData->Scan0.ToPointer() +
                             (y*m_pbmpSrcData->Stride)+(x*4);
 
      return RGB(pel[2], pel[1], pel[0]) ;
}
 
BOOL CDibData::SetPixel(int x, int y, COLORREF clPixel)
{
      BYTE* pel = (BYTE*)m_pbmpSrcData->Scan0.ToPointer() +
                             (y*m_pbmpSrcData->Stride)+(x*4);
 
      pel[2] = GetRValue(clPixel);
      pel[1] = GetGValue(clPixel);
      pel[0] = GetBValue(clPixel);
 
      return TRUE;
}

请注意,这些方法假设数据是 32bpp 格式,因为这是在之前的 `LockBits()` 调用中指定的。还要注意这里公然缺乏错误检查。请自行承担使用此代码的风险。

结论

我的目的是举例说明如何将现有的 C++ 代码用于 .NET 应用程序(在本例中是一个 C# Windows 窗体应用程序)。在实际应用中,您可能希望在解决方案中更加健壮。

另请注意,此示例实现了加载器-锁定解决方法,如 http://support.microsoft.com/?id=814472 中所述。此解决方法要求组件由客户端初始化和反初始化。

© . All rights reserved.