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

Doodle - GDI+ 中的基本绘画程序

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.72/5 (14投票s)

2001年6月4日

10分钟阅读

viewsIcon

427058

downloadIcon

3339

使用 GDI+ 创建具有柔和画笔和图像加载/保存功能的绘图程序

Sample Image - Doodle.jpg

引言

在本文中,我希望向您展示 GDI+ 能够做的一些真正令人惊叹的事情。我一直在开发一个绘图程序,到现在已经 18 个月了,我惊讶于我不得不费劲学习的许多东西现在都变得轻而易举,因为 GDI+ 为你完成了这项工作。本文的主题是一个简单的绘图程序,它允许你加载和保存图像,以及创建新图像,并在其上自由绘制,创建线条/填充形状和渐变填充形状,以及一个柔和画笔(一种中心实心,边缘逐渐透明的画笔)。

我将假设您熟悉我的前两篇文章中介绍的概念,我建议您参考它们作为作业,并在 Doodle 中实现我的 GDI 画笔和矩阵文章中涵盖的一些工具。你会发现创建像 Windows 自带的画图程序一样好的东西并不需要太多的努力。

我想如果你像我一样,你会先下载可执行文件并玩一下,看看它是否能做你想学习的事情。尽管去吧——Doodle 启动时会为你呈现一个 350 x 350 像素的默认画布。点击“视图/画笔选项”选择你要使用的工具,并选择颜色/Alpha 值。第二个颜色值选项用于渐变填充——填充从边缘到中心。你可以通过分别点击“加载”或“新建”来加载现有图像或创建不同大小的图像。

好了,你已经试过了吗?我向你保证,这只是我们能做的事情的冰山一角。让我们快速谈谈我将在本文中涵盖的内容。

  • 加载/保存图像
  • 图案画笔
  • 路径渐变画笔
  • 路径
  • 画笔
  • 绘制线条

首先,我们来谈谈图案画笔。在我们的 OnEraseBackground 函数中,我们选择一个 HatchBrush 并用它来绘制未被位图覆盖的 SDI 区域。这些画笔已经比 GDI 进步了很多,GDI 只提供六种图案值。GDI+ 提供 52 种,如下所示:

HatchStyleHorizontal = 0,
  HatchStyleVertical = 1,
  HatchStyleForwardDiagonal = 2,
  HatchStyleBackwardDiagonal = 3,
  HatchStyleCross = 4,
  HatchStyleDiagonalCross = 5
  HatchStyle05Percent = 6,,
  HatchStyle10Percent = 7,
  HatchStyle20Percent = 8,
  HatchStyle25Percent = 9,
  HatchStyle30Percent = 10,
  HatchStyle40Percent = 11,
  HatchStyle50Percent = 12,
  HatchStyle60Percent = 13,
  HatchStyle70Percent = 14,
  HatchStyle75Percent = 15,
  HatchStyle80Percent = 16,
  HatchStyle90Percent = 17,
  HatchStyleLightDownwardDiagonal = 18,
  HatchStyleLightUpwardDiagonal = 19,
  HatchStyleDarkDownwardDiagonal = 20,
  HatchStyleDarkUpwardDiagonal = 21,
  HatchStyleWideDownwardDiagonal = 22,
  HatchStyleWideUpwardDiagonal = 23,
  HatchStyleLightVertical = 24,
  HatchStyleLightHorizontal = 25,
  HatchStyleNarrowVertical = 26,
  HatchStyleNarrowHorizontal = 27,
  HatchStyleDarkVertical = 28,
  HatchStyleDarkHorizontal = 29,
  HatchStyleDashedDownwardDiagonal = 30,
  HatchStyleDashedUpwardDiagonal = 31,
  HatchStyleDashedHorizontal = 32,
  HatchStyleDashedVertical = 33,
  HatchStyleSmallConfetti = 34,
  HatchStyleLargeConfetti = 35,
  HatchStyleZigZag = 36,
  HatchStyleWave = 37,
  HatchStyleDiagonalBrick = 38,
  HatchStyleHorizontalBrick = 39,
  HatchStyleWeave = 40,
  HatchStylePlaid = 41,
  HatchStyleDivot = 42,
  HatchStyleDottedGrid = 43,
  HatchStyleDottedDiamond = 44,
  HatchStyleShingle = 45,
  HatchStyleTrellis = 46,
  HatchStyleSphere = 47,
  HatchStyleSmallGrid = 48,
  HatchStyleSmallCheckerBoard = 49,
  HatchStyleLargeCheckerBoard = 50,
  HatchStyleOutlinedDiamond = 51,
  HatchStyleSolidDiamond = 52,
  HatchStyleTotal,
  HatchStyleLargeGrid = HatchStyleCross,
  HatchStyleMin = HatchStyleHorizontal,
  HatchStyleMax = HatchStyleTotal - 1

在程序中添加一个菜单选项来改变图案样式会非常容易,并且能让你探索一些很棒的新样式。但还有更好的。 HatchBrush 构造函数如下所示:

HatchBrush(
  HatchStyle hatchStyle,
  const Color& foreColor,
  const Color& backColor
)

简而言之,这意味着除了图案样式,你还可以指定绘制它的颜色。所有的 Graphics::FillXXX 方法都接受 HatchBrush,允许你使用这些图案绘制任何你喜欢的形状。

尽管 Doodle 没有理由不提供用线性渐变绘制的形状,但我之前已经介绍过 LinearGradientBrush,所以在 Doodle 中我使用了 PathGradientBrush

PathGradientBrush 构造函数如下所示:

PathGradientBrush(const GraphicsPath* path)
PathGradientBrush(const Point* points, INT count, WrapMode wrapMode)
PathGradientBrush(const PointF* pointsF, INT count, WrapMode wrapMode) 

尽管接受点的这些方法接受一个 wrapmode,这**看起来**更有用,但事实是我们可以使用 GraphicsPathIteratorGraphicsPath 中获取点,而且 GraphicsPath 对象本身有一些非常酷的选项,我们现在将讨论它们,尽管 Doodle 没有使用它们,但它很容易就能做到。

GraphicsPath(FillMode fillMode)

FillMode 枚举允许我们指定交替填充或缠绕填充。Alternate 是默认方法,它填充每隔一个区域,换句话说,如果一条线穿过奇数个路径段,则起点位于封闭区域内部,因此是填充或剪切区域的一部分。缠绕方法还考虑每个段的方向。一旦有了 GraphicsPath 对象,就可以使用以下方法向其中添加内容。

 GraphicsPath::AddArc
 GraphicsPath::AddBezier
 GraphicsPath::AddBeziers 
 GraphicsPath::AddClosedCurve 
 GraphicsPath::AddCurve 
 GraphicsPath::AddEllipse 
 GraphicsPath::AddLine 
 GraphicsPath::AddLines
 GraphicsPath::AddPath
 GraphicsPath::AddPie 
 GraphicsPath::AddPolygon 
 GraphicsPath::AddRectangle 
 GraphicsPath::AddRectangle 
 GraphicsPath::AddString

通常,这些函数很多都有四个重载,它们接受由 REALINT 构成的矩形,或相应 REALINT 形式的点。显然,其中一些只需要两个(AddLine),或具有更具体的数据要求(AddStringAddCurve 等)。我们将在 Doodle 中使用 AddLine,但我希望随着我们讨论 GraphicsPath 可以做的一些事情,您能看到其中的可能性。

GraphicsPath 的一些方法包括:

  • Status Flatten(const Matrix* matrix, REAL flatness) - 允许您提供一个可选的变换矩阵并将曲线转换为点集合。
  • Status GetBounds(Rect* bounds, const Matrix* matrix, const Pen* pen) - 用定义路径大小的 Rect 填充 bounds 变量
  • Status Reverse() - 反转路径中点的顺序
  • Status Transform(const Matrix* matrix) - 应用矩阵,实现缩放、旋转和平移
  • Status Warp(const PointF* destPoints, INT count, const RectF& srcRect, const Matrix* matrix, WarpMode warpMode, REAL flatness) - 使用透视或双线性扭曲,将路径的副本填充到 destPoints 中,并可选地应用变换矩阵,如果需要,平展曲线。我迫不及待地想在一些 strings 上尝试这个...
  • Status Widen(const Pen* pen, const Matrix* matrix, REAL flatness ) - 将路径加宽指定的画笔宽度

哇——看起来很有趣,不是吗?在我们的 OnMouseMove 函数中,你会注意到我们会在上一个点和当前点之间 AddLine,所以最终结果是一个跟随我们鼠标自按下左键以来移动路径的路径。当我们抬起鼠标时,我们会检查绘图模式,如果它是填充形状,我们就这样做:

SolidBrush brush(m_Colour);
graphics.FillPath(&brush, &m_Path);	

因此,最终结果是使用我们的主色填充的形状。如果我们选择了渐变画笔,那么事情就变得更有趣了。这引入了 PathGradientBrush。这个画笔使用我们的次要颜色作为中心,主要颜色作为外边缘来填充路径。我们已经讨论了它的构造函数,它的其他方法允许我们做一些事情,比如指定混合率、中心点、混合样式、伽马校正、环绕模式、矩阵变换等。我们还用它来创建柔和画笔,如下所示:

GraphicsPath path;
path.AddEllipse(point.x, point.y, m_Width, m_Width);
PathGradientBrush brush(&path);
brush.SetCenterColor(Color(255, m_Colour.GetRed(), m_Colour.GetGreen(), 
                     m_Colour.GetBlue()));
Color colors[] = { Color(0, m_Colour.GetRed(), m_Colour.GetGreen(), 
                            m_Colour.GetBlue()) };
INT count = 1;
brush.SetSurroundColors(colors, &count);
graphics.FillEllipse(&brush, point.x, point.y, m_Width, m_Width);
path.Reset();

柔和画笔的想法很简单——我们用一种颜色绘制,颜色融入图像,因为画笔中间是实心的,边缘逐渐褪色。为了在 GDI+ 中创建这种效果,我们只需将中心颜色设置为完全不透明(255),并将边缘设置为相同颜色但完全透明(0)。然后,只需将所需的形状(通常是圆形)创建成路径,并将路径应用于构造我们的画笔。特别是,这在 GDI 中需要大量工作——我的绘图程序中的柔和画笔类使用这些方法后,大小约为原来的 10%。

当我们在 Doodle 中移动鼠标时,我们使用最基本的可用对象——设置为实色画笔的 Pen 绘制线条。Pen 构造函数如下所示:

  • Pen(const Brush* brush, REAL width) - 允许您使用画笔绘制纹理线/渐变线/影线
  • Pen(const Color& color, REAL width) - 我们用于绘制实线的基本构造函数
  • Pen(const Pen& pen) - 创建一个副本
  • Pen(GpPen* nativePen, Status status) - 无论您在方法中看到“native”一词,它都用于内部使用,请勿使用。

画笔还可以做一些很酷的事情。在 Doodle 中,我们提供四种线条样式。实际上,有十一种,如下所示:

  LineCapFlat     = 0,
  LineCapSquare   = 1,
  LineCapRound    = 2,
  LineCapTriangle = 3,
  LineCapNoAnchor = 0x10,
  LineCapSquareAnchor  = 0x11,
  LineCapRoundAnchor   = 0x12,
  LineCapDiamondAnchor = 0x13,
  LineCapArrowAnchor   = 0x14,
  LineCapCustom        = 0xff,
  LineCapAnchorMask    = 0xf0

我看不出 LineCapTriangle 的作用,如果有人知道,请告诉我。我怀疑它有待添加。XXXAnchor 线帽会创建一个锚点,即一个比正在绘制的线条大的形状。线帽通过以下方法之一指定:

Status SetLineCap(LineCap startCap, LineCap endCap, DashCapCap dashCap)
Status SetEndCap(LineCap endCap)
Status SetStartCap(LineCap startCap)
Status SetDashCap(DashCap dashCap)

起始和结束端帽不言而喻,虚线端帽与这些方法配合使用:

Status SetDashOffset(REAL dashOffset)
Status SetDashPattern(const REAL* dashArray, INT count)
Status SetDashStyle(DashStyle dashStyle)

enum DashStyle{
  DashStyleSolid = 0,
  DashStyleDash = 1,
  DashStyleDot = 2,
  DashStyleDashDot = 3,
  DashStyleDashDotDot = 4,
  DashStyleCustom = 5
};

并指定虚线末端的样式。绘制线条序列时,您还可以指定线条连接的绘制方式,我将让您在 Doodle 中实现。您还可以实现其他线条端帽以及线条起始和结束处的不同端帽。您可能希望在线条模式下执行此操作,在该模式下您可以拉伸线条,因为在自由模式下这样做只会看起来很糟糕,除非您在线条完成后而不是之前使用路径应用端帽。

最后,GDI 最受要求的功能:加载和保存图像。回想起来,GDI 不提供此功能似乎很奇怪,但雷德蒙德的开发人员现在确实让从磁盘加载位图并再次保存变得容易。加载很简单,您只需使用此方法:

Bitmap(const WCHAR* filename, BOOL useIcm)

CString 转换为 WCHAR 在我的 GDI+ 画笔和矩阵文章中有所介绍,但我后来发现 CString 会为你完成所有转换。CString 构造函数将接受 BSTR,而要转换回来,AllocSysString 函数会返回 BSTR。因此最终结果如下所示:

CString filename(lpszPathName);

m_Bitmap = Bitmap::FromFile(filename.AllocSysString());

useIcm 的默认值为 false,ICM 是一种颜色校正方法。Bitmap 继承自 Image,除了从磁盘加载外,还增加了 Get/SetPixelLockBits 方法,这使我们可以在需要时轻松访问原始数据(例如,应用滤镜)。

保存稍微复杂一些,但不多。我们需要一个函数来访问我们需要的编码器的 Clsid。GDI+ 没有,但幸运的是,文档中提供了一个:

int GetCodecClsid(const WCHAR* format, CLSID* pClsid)
{
   UINT  num = 0;          // number of image encoders
   UINT  size = 0;         // size of the image encoder array in bytes

   ImageCodecInfo* pImageCodecInfo = NULL;

   GetImageEncodersSize(&num, &size);
   if(size == 0)
      return -1;           // Failure

   pImageCodecInfo = (ImageCodecInfo*)(malloc(size));
   if(pImageCodecInfo == NULL)
      return -1;           // Failure

   GetImageEncoders(num, size, pImageCodecInfo);

   for(UINT j = 0; j < num; ++j)
   {
      if( wcscmp(pImageCodecInfo[j].MimeType, format) == 0 )
      {
         *pClsid = pImageCodecInfo[j].Clsid;
         return j;         // Success
      }    
   }                       // for

   return -1;              // Failure

}                          // GetCodecClsid

现在要保存文件,我们使用 Image::Save 函数,其原型如下:

Status Save(
  const WCHAR* filename,
  const CLSID* clsidEncoder,
  const EncoderParameters* encoderParams
)

EncoderParameters 参数默认为 NULL,因此我们只需获取一个 Clsid。辅助函数需要一个 Clsid* 来填充,以及一个 string。此 string 的值如下:

image/jpeg
image/gif
image/tiff
image/png
image/bmp

在 Doodle 中,我们检查给定文件名的最后三个字符,然后相应地加载一个 Clsid。如果找不到任何可识别的格式,我们默认使用 bmp。程序的这个区域需要美化一下,因为我们还没有花时间将可用的文件类型放入文件对话框中。我在此阶段要提到的是,在 Doodle 的整个过程中,我们的理念是以一种清晰简洁的方式展示事物的完成方式,因此很少进行错误检查。GDI+ 非常类似于 COM,因为大多数函数都返回一个 Status 对象,并使用传入的引用或指针来提供非错误相关的返回值。在“真实世界”中,您会检查返回值以确保您的代码行为符合预期。

最后一个小提示——我花了一天时间试图让文件保存功能正常工作,才意识到我把代码放在了 CDocument 类的 OnSaveDocument 函数中。如果你这样做,请确保不要调用基类——它会尝试序列化文件,由于 Serialise 函数是空的,它会覆盖我的文件。有趣的是,本周早些时候我为其他人修复了完全相同的问题,当时我立刻就发现了。这说明:当使用新东西时,不要太快地认为它坏了——如果我仔细检查了*我的*代码,而不是假设 Save 方法有问题,我应该像本周早些时候一样轻易地发现它。

好了,这次我们讲了很多内容——加载和保存文件,创建填充形状,柔和画笔等等。我建议任何认真学习 GDI+ 的人参考我的 GDI+ 画笔和矩阵文章,并将那里的技术应用到 Doodle 中。最好的学习方式是实践,而且你手头有这两篇文章中的所有代码,所以应该是一个简单的开始。祝你好运!!

许可证

本文未附加明确的许可证,但可能在文章文本或下载文件本身中包含使用条款。如有疑问,请通过下面的讨论区联系作者。

作者可能使用的许可证列表可以在此处找到。

© . All rights reserved.