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

使用 C# 为 Paint.NET 2.1 编写特效插件

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.92/5 (16投票s)

2005年1月2日

12分钟阅读

viewsIcon

95681

downloadIcon

1825

本文介绍如何使用 C# 为 Paint.NET 2.1 创建自己的特效插件。

引言

Paint.NET 2.1 上周发布了。它被设计为 Windows 自带的画图程序的免费替代品,对广大普通用户来说非常有吸引力。但对于开发者来说,它更有吸引力,原因有两个。第一,它是开源的。如果你喜欢研究几兆字节的 C# 代码,或者想了解一些架构问题是如何解决的,那就去下载它吧。第二,该应用程序提供了一个简单而吸引人的接口,用于创建自己的特效插件。这正是本文的主题(如果你在寻找一些花哨的特效算法,请到别处去,因为本文中使用的特效相当简单)。

入门

您需要做的第一件事是获取 Paint.NET 源代码。除了源代码本身,它还兼作其文档和 Paint.NET SDK。该解决方案包含多个项目。但是,在开发 Paint.NET 特效插件时,唯一相关的项目是 PdnLib 库,它包含了我们将用于渲染特效的类,以及 Effects 库,它包含了用于派生您自己的特效实现的基类。

项目基础

要创建新的特效插件,我们首先创建一个新的 C# 类库,并添加对 PdnLib (PdnLib.dll) 和 PaintDotNet.Effects 库 (PaintDotNet.Effects.dll) 正式发布版本的引用。我们项目的根命名空间应该设置为 PaintDotNet.Effects,因为我们正在创建一个旨在无缝集成的插件。当然,这不仅仅限于命名空间,更是一个通用规则:在为 Paint.NET 编写软件时,要像 Paint.NET 的开发者那样做。实际的实现需要派生三个类。

  1. Effect 是所有 Paint.NET 特效实现的基本类,它也是 Paint.NET 用于非参数化特效的接口。它包含 Render(RenderArgs, RenderArgs, Rectangle) 方法,派生的非参数化特效会重写此方法。
  2. 大多数特效都是参数化的。EffectConfigToken 类是所有特定特效参数类的基类。
  3. 最后,由于参数化特效很可能需要一个用户界面,因此有一个用于特效对话框的基类:EffectConfigDialog

实现基础设施

现在,我们将以噪点特效(顾名思义,它只是给图像添加噪点)为例,看看实现细节。顺便说一句,在使用本文提供的源代码时,您很可能需要更新 Paint.NET 库的引用。

特效参数

正如我之前所说,我们需要派生一个类来继承 EffectConfigToken,以便能够传递我们的特效参数。鉴于我们的特效名为噪点特效,并且我们希望与现有源代码保持一致,我们的参数类必须命名为 NoiseEffectConfigToken

public class NoiseEffectConfigToken : EffectConfigToken

关于构造函数应该是什么样子,没有硬性规定。您可以使用简单的默认构造函数,或者带有参数的构造函数。从 Paint.NET 的角度来看,这无关紧要,因为(正如您稍后将看到的)负责创建 EffectConfigToken 实例的是(派生自)EffectConfigDialog 的类。所以,您不必做任何额外的事情,只需要有一个非私有构造函数。

public NoiseEffectConfigToken() : base()
{

}

但是,我们的基类实现了 ICloneable 接口,并定义了一个如何处理克隆的模式。因此,我们需要创建一个 protected 构造函数,它接受一个该类自身类型的对象,并使用它来复制所有值。然后,我们需要重写 Clone() 方法,并使用 protected 构造函数来进行实际的克隆。这也意味着构造函数应该调用基类构造函数,但 Clone() 不能调用其基类实现。

protected NoiseEffectConfigToken(NoiseEffectConfigToken copyMe) : base(copyMe)
{

  this.frequency      = copyMe.frequency;
  this.amplitude      = copyMe.amplitude;
  this.brightnessOnly = copyMe.brightnessOnly;

}

public override object Clone()
{
  return new NoiseEffectConfigToken(this);
}

其余的实现细节再次完全取决于您。最有可能的是,您将定义一些私有字段和相应的公共属性(如果适用,可能还包含一些合理性检查)。

设置特效参数的用户界面

现在我们有了一个参数容器,我们需要一个用户界面来设置它们。如前所述,我们将从 EffectConfigDialog 派生 UI 对话框。这一点很重要,因为它有助于确保整个 UI 的一致性。例如,在 Paint.NET 2.0 中,特效对话框默认以 0.9 的不透明度显示(终端服务会话除外)。如果我不使用 Paint.NET 的基类,而开发人员决定不透明度为 0.6 更酷,我的对话框就会突然看起来“不对劲”。由于我们仍然试图与原始代码保持一致,所以我们的 UI 类名为 NoiseEffectConfigDialog

同样,在设计对话框方面,您有很多自由度,所以我会再次专注于强制性的实现细节。特效对话框完全负责创建和维护特效参数对象。因此,有三个虚拟基方法您必须重写。而且,可能出乎意料的是,不要调用它们的基类实现(似乎基类实现的一些早期版本在被调用时甚至会抛出异常)。第一个是 InitialInitToken(),它负责创建一个新的具体 EffectConfigToken,并将引用存储在 protected 字段 theEffectToken 中(它将隐式地将引用转换为 EffectConfigToken 引用)。

protected override void InitialInitToken()
{

  theEffectToken = new NoiseEffectConfigToken();

}

其次,我们需要一个方法来根据对话框的状态更新特效令牌。因此,我们需要重写 InitTokenFromDialog() 方法。

protected override void InitTokenFromDialog()
{

  NoiseEffectConfigToken token = (NoiseEffectConfigToken)theEffectToken;
  token.Frequency      = (double)FrequencyTrackBar.Value / 100.0;
  token.Amplitude      = (double)AmplitudeTrackBar.Value / 100.0;
  token.BrightnessOnly = BrightnessOnlyCheckBox.Checked;

}

最后,我们需要能够做反向操作。也就是说,根据令牌的值更新 UI。这就是 InitDialogFromToken() 的作用。与其他两个方法不同,这个方法需要一个令牌的引用来处理。

protected override void InitDialogFromToken(EffectConfigToken effectToken)
{

  NoiseEffectConfigToken token = (NoiseEffectConfigToken)effectToken;

  if ((int)(token.Frequency * 100.0) > FrequencyTrackBar.Maximum)
    FrequencyTrackBar.Value = FrequencyTrackBar.Maximum;
  else if ((int)(token.Frequency * 100.0) < FrequencyTrackBar.Minimum)
    FrequencyTrackBar.Value = FrequencyTrackBar.Minimum;
  else
    FrequencyTrackBar.Value = (int)(token.Frequency * 100.0);

  if ((int)(token.Amplitude * 100.0) > AmplitudeTrackBar.Maximum)
    AmplitudeTrackBar.Value = AmplitudeTrackBar.Maximum;
  else if ((int)(token.Amplitude * 100.0) < AmplitudeTrackBar.Minimum)
    AmplitudeTrackBar.Value = AmplitudeTrackBar.Minimum;
  else
    AmplitudeTrackBar.Value = (int)(token.Amplitude * 100.0);

  FrequencyValueLabel.Text = FrequencyTrackBar.Value.ToString("D") + "%";
  AmplitudeValueLabel.Text = AmplitudeTrackBar.Value.ToString("D") + "%";

  BrightnessOnlyCheckBox.Checked = token.BrightnessOnly;

}

我们几乎完成了。还有什么没完成的呢?我们需要通知应用程序值何时被更改,以及用户最终决定是应用更改到图像还是取消操作。因此,每当用户更改某个值时,就调用 UpdateToken() 来让应用程序知道需要更新预览。另外,离开对话框时调用 Close() 并设置适当的 DialogResult。例如:

private void AmplitudeTrackBar_Scroll(object sender, System.EventArgs e)
{

  AmplitudeValueLabel.Text = AmplitudeTrackBar.Value.ToString("D") + "%";
  UpdateToken();

}

private void OkButton_Click(object sender, System.EventArgs e)
{

  DialogResult = DialogResult.OK;
  Close();

}

private void EscButton_Click(object sender, System.EventArgs e)
{

  DialogResult = DialogResult.Cancel;
  Close();

}

实现特效

现在一切就绪,可以开始实现特效了。如前所述,有一个非参数化特效的基类。噪点特效是参数化的,但这并不能阻止我们从 Effect 派生。但是,为了让 Paint.NET 知道这是一个参数化特效,我们还需要实现 IConfigurableEffect 接口,它会添加另一个重载的 Render() 方法。它还引入了 CreateConfigDialog() 方法,允许应用程序创建特效对话框。

public class NoiseEffect : Effect, IConfigurableEffect

那么,我们如何构造一个 Effect 对象,或者在这种情况下,一个 NoiseEffect 对象呢?这次,我们必须遵循应用程序的模式,这意味着我们使用一个 public 默认构造函数,它调用两个基类构造函数之一。第一个构造函数接受特效的名称、描述和要在“特效”菜单中显示的图标。第二个构造函数额外需要特效的快捷键。然而,快捷键仅适用于被归类为“调整”的特效。对于普通特效,它将被忽略(有关特效和调整的详细信息,请参见“特效属性”一章)。结合一些资源管理,这可能看起来像这样:

public NoiseEffect() : base(NoiseEffect.resources.GetString("Text.EffectName"),
  NoiseEffect.resources.GetString("Text.EffectDescription"),
  (Image)NoiseEffect.resources.GetObject("Icons.NoiseEffect.bmp"))
{

}

我们唯一必须实现的,是实现 IConfigurableEffect 接口所带来的部分。实现 CreateConfigDialog() 非常简单,它只是创建一个对话框对象并返回它的引用。

public EffectConfigDialog CreateConfigDialog()
{

  return new NoiseEffectConfigDialog();

}

应用特效更有趣,但我们将遇到一些我们可能从未听说过的奇怪类。所以,让我们先看看 Render() 方法的签名:

public void Render(EffectConfigToken properties,
                   PaintDotNet.RenderArgs dstArgs,
                   PaintDotNet.RenderArgs srcArgs,
                   PaintDotNet.PdnRegion roi)

RenderArgs 类包含了我们操作图像所需的一切;最重要的是,它为我们提供了 Surface 对象,这些对象实际允许读取和写入像素。但是,请注意不要混淆 dstArgssrcArgssrcArgs 对象(当然包括其 Surface)处理的是原始图像。因此,您绝对不应该对这些对象执行任何写操作。但您将不断地从源 Surface 读取,因为一旦您更改了目标 Surface,没有人会重置它们。目标(或目标)Surface 可通过 dstArgs 对象访问。使用需要 x 和 y 坐标的索引器可以轻松地定位到某个点的像素。例如,下面的代码片段从原始图像获取一个像素,执行一个操作,然后将更改后的像素分配到目标 Surface 中的相同位置。

point = srcArgs.Surface[x, y];
VaryBrightness(ref point, token.Amplitude);
dstArgs.Surface[x, y] = point;

但这还不是全部。应用程序要求我们处理的区域,由第四个对象 roi 表示,可以有任何形状。因此,我们需要调用一个方法,如 GetRegionScansReadOnlyInt(),来获取近似绘制区域的矩形集合。此外,我们应该从顶部开始逐行处理图像。这些规则导致了如下模式:

public void Render(EffectConfigToken properties, RenderArgs dstArgs,
                   RenderArgs srcArgs, PdnRegion roi)
{

  /* Loop through all the rectangles that approximate the region */
  foreach (Rectangle rect in roi.GetRegionScansReadOnlyInt())
  {
    for (int y = rect.Top; y < rect.Bottom; y++)
    {
      /* Do something to process every line in the current rectangle */
      for (int x = rect.Left; x < rect.Right; x++)
      {
        /* Do something to process every point in the current line */
      }
    }
  }

}

最后一个值得一提的有趣事实是,Surface 类通常使用 32 位格式,带有四个通道(红、绿、蓝和 alpha),每个通道 8 位,每个像素由一个 ColorBgra 对象表示。请记住,ColorBgra 实际上是一个 struct,所以为了按引用传递该类型的对象,您必须使用 ref 关键字。此外,该 struct 允许通过公共字段访问每个通道。

private void VaryBrightness(ref ColorBgra c, double amplitude)
{

  short newOffset = (short)(random.NextDouble() * 127.0 * amplitude);
  if (random.NextDouble() > 0.5)
    newOffset *= -1;

  if (c.R + newOffset < byte.MinValue)
    c.R = byte.MinValue;
  else if (c.R + newOffset > byte.MaxValue)
    c.R = byte.MaxValue;
  else
    c.R = (byte)(c.R + newOffset);

  if (c.G + newOffset < byte.MinValue)
    c.G = byte.MinValue;
  else if (c.G + newOffset > byte.MaxValue)
    c.G = byte.MaxValue;
  else
    c.G = (byte)(c.G + newOffset);

  if (c.B + newOffset < byte.MinValue)
    c.B = byte.MinValue;
  else if (c.B + newOffset > byte.MaxValue)
    c.B = byte.MaxValue;
  else
    c.B = (byte)(c.B + newOffset);

}

特效属性

现在我们的特效已经上线并正常运行了。我们还需要做其他事情吗?嗯,在这种情况下,一切都很好。但是,由于每个特效都不同,您可能希望应用 PaintDotNet.Effects 命名空间中提供的三个属性之一。首先是 EffectCategoryAttribute 属性,它用于告知 Paint.NET 该特效是特效还是调整。这两者之间的区别在于,特效旨在对图像进行实质性更改,并在“特效”菜单中列出;而调整只对图像进行小的更正,并在“图层”菜单下的“调整”子菜单中列出。只需查看 Paint.NET 中集成的特效和调整,就能对如何对某个插件进行分类有所了解。EffectCategoryAttribute 通过使用传递给属性构造函数的 EffectCategory 值来明确设置特效的类别。默认情况下,任何没有 EffectCategoryAttribute 的特效插件都被视为特效(因此出现在“特效”菜单中),这等同于应用如下属性:

[EffectCategoryAttribute(EffectCategory.Effect)]

当然,EffectCategory 枚举有两个值,第二个值 EffectCategory.Adjustment 用于将特效分类为调整,以便它出现在 Paint.NET 的“调整”子菜单中。

[EffectCategoryAttribute(EffectCategory.Adjustment)]

除了能够对特效进行分类之外,您还可以通过应用 EffectSubMenu 属性来定义自己的子菜单。想象一下,您创建了十个超级酷的效果,现在想在 Paint.NET 的“特效”菜单中将它们分组,以显示它们构成了一个工具箱。现在,为了将所有这些插件放入“特效”菜单下的“我的超级酷工具箱”子菜单中,您所要做的就是在您的工具箱的每个插件上应用 EffectSubMenu 属性。当然,这也可以用于调整插件,以在“调整”子菜单中创建子菜单。但是,有一个重要的限制:由于 Paint.NET 管理特效的方式,特效名称必须是唯一的。这意味着您不能有一个名为 Foo 的特效直接出现在“特效”菜单中,第二个名为 Foo 的特效出现在“我的超级酷工具箱”子菜单中。如果您尝试这样做,Paint.NET 将只调用这两个特效中的一个,无论您使用的是“特效”菜单中的命令还是子菜单中的命令。

[EffectSubMenu("My Ultra-Cool Toolbox")]

最后但同样重要的是 SingleThreadedEffect 属性。现在,让我们先谈谈多线程。通常,Paint.NET 是一个多线程应用程序。这意味着,例如,当它需要渲染一个特效时,它会利用工作线程来执行实际的渲染。这确保了 UI 保持响应,并且如果渲染是由至少两个线程完成的,并且 Paint.NET 运行在多核 CPU 或多处理器系统上,它还可以显著减少渲染时间。默认情况下,Paint.NET 将使用与系统中逻辑处理器数量相同数量的线程来渲染特效,最少线程数为两个。

处理器(s)

物理 CPU

逻辑 CPU

线程

Intel Pentium III

1

1

2

带超线程的 Intel Pentium 4

1

2

2

不带超线程的双 Intel Xeon

2

2

2

带超线程的双 Intel Xeon

2

4

4

但是,如果应用了 SingleThreadedEffect 属性,Paint.NET 将只使用一个线程,而不管逻辑处理器的数量。如果渲染是由多个线程完成的,您必须确保 Render() 方法中任何对象的用法都是线程安全的。特效配置令牌通常不是问题(只要您不更改其值,这通常也不推荐),因为渲染线程会获取 UI 使用的令牌实例的副本。Paint.NET 自带的 PdnRegion 类也是线程安全的,因此您不必担心这些对象。然而,GDI+ 对象,如 RenderArgs.GraphicsRenderArgs.Bitmap,不是线程安全的,因此每当您想使用这些对象来渲染您的特效时,都必须应用 SingleThreadedEffect 属性。如果您不确定您的实现是否真正线程安全,或者您只是不想纠结于多线程,您也可以应用该属性。尽管这样做会在多处理器系统和多核 CPU 上降低性能,但至少您会处于字面意义上的安全一边。

[SingleThreadedEffect]

结论

创建 Paint.NET 的特效插件毕竟并不难。您需要使用的对象模型部分并不复杂(尝试一下是一个绝佳的周末活动),而且它看起来相当健壮。当然,本文并未涵盖关于 Paint.NET 特效插件的所有知识,但它应该足以让您创建第一个自己的插件。

致谢

我衷心感谢 Rick Brewster 和 Craig Taylor 提供的反馈和校对本文。

更改历史

  • 2005-05-08:添加了关于快捷键仅适用于调整的说明以及关于属性的章节。
  • 2005-01-06:修正了 NoiseEffectConfigDialog.InitDialogFromToken(EffectConfigToken) 中的一个重大错误。旧实现使用了 EffectConfigDialog.EffectToken 属性而不是 effectToken 参数。
  • 2005-01-03:初始发布。
© . All rights reserved.