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

在 GDI+ 中创建语音气泡/对话气泡

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.94/5 (40投票s)

2010年1月9日

CPOL

9分钟阅读

viewsIcon

101379

downloadIcon

3424

一个高度可配置的类,可以在 GDI+ 中生成几种风格的语音气泡。

Screenshot 1

Screenshot 2

Screenshot 3

Screenshot 3

引言

我当时正在做一个 Windows Forms 项目,要求用户能够在照片或图纸上添加语音气泡(也称为对话气泡)。由于我对 GDI+ 比较熟悉,所以我认为这不会太复杂;我会使用 GraphicsPathRegion。最终,我想绘制气泡,将其与尾巴合并,填充所得形状,然后绘制其轮廓。我知道 Region 有一个 Union 方法,所以我先尝试了那个。合并效果很好,尽管我能够用背景色填充组合形状的区域,但在 GDI+ 中没有用于区域的 Draw 方法,因此我无法绘制语音气泡的轮廓。如果我尝试使用两个 GraphicsPath 对象绘制单个形状的轮廓,轮廓会相互交叉,完全不起作用。

然后我尝试了 GraphicsPath,虽然 GDI+ 对它同时有 FillDraw 方法,但无法将两个 GraphicsPath 对象合并成一个形状,并且我在绘制边框方面遇到了与 Region 相同的问题。

我还研究了是否可以将 Region 转换为 GraphicsPath(以便从合并的 Region 获取 GraphicsPath 对象),但没有。这时,我停止了自己摸索,并求助于程序员最好的朋友——互联网。在网上搜索解决方案(实际上我发现有几个人问如何制作语音气泡),普遍的答案是由于 GDI+ 以及 RegionGraphicsPath 类(如上所述)的限制,无法做到这一点。本文介绍了我如何解决这个问题,并提供了一个示例应用程序,您可以使用它来生成自己的语音气泡图像或生成复制您设计的气泡所需的代码。

背景

语音气泡的解剖结构

为了使我的代码和示例程序更容易理解,我将提供一些基本术语。我大部分是根据自己的理解创造了这些术语,所以对于那些了解这些零件技术术语的漫画艺术家,如果我没有使用正确的名称,我表示歉意。

Anatomy

无论我们讨论的是哪种气泡(语音、想法、星形),中心区域都称为气泡体。气泡体周边突出的部分(想法气泡的圆圈,星形气泡的尖刺或射线)我简单地称为气泡。您可能会问,想法气泡的气泡和星形气泡的气泡有什么区别?区别在于星形气泡比想法气泡的气泡更尖锐。指向气泡说话者(或思考者)的单个突出部分,我称之为尾巴

要求

绘制语音气泡涉及两个部分:绘制实际的气泡体(及其气泡),以及绘制尾巴。因为我喜欢尽可能地使事物通用和可重用,所以我的语音气泡绘制系统必须支持以下要求:

  • 能够将任何文本放入气泡中
  • 能够指定文本颜色、气泡填充颜色和边框颜色
  • 能够设置绘制文本的字体
  • 能够显示或隐藏边框
  • 能够显示或隐藏尾巴
  • 能够更改尾巴相对于气泡的大小和方向
  • 能够更改气泡的形状以模拟语音(椭圆)、想法(圆润的椭圆)和动作(星形)

SpeechBalloon 类

在我的解决方案中,我创建了一个名为 SpeechBalloon 的新类,它保存了语音气泡的所有可设置属性,以及将气泡实际渲染到 Graphics 对象的方法。我将不详细介绍诸如 FontBoundsBorderWidth 等常用属性,因为它们应该很直观,而且代码注释也很充分。

存储气泡形状的 GraphicsPath 对象位于一个名为 Path 的只读属性中。我缓冲 GraphicsPath 而不是在每次调用 Draw 时重新创建它,因为这是一个复杂的过程(如下面所示)。其他属性的更改将导致路径被重新创建(如果该属性影响气泡的形状),并向 SpeechBalloon 实例引发一个 RedrawRequired 事件,以通知父程序需要重绘语音气泡的绘制表面。以下是一些示例:

此属性更改气泡的形状,并导致我们的代码重新生成其 GraphicsPath(通过 RecreatePath 方法)。

Public Property Width() As Integer
    Get
        Return MyBounds.Width
    End Get
    Set(ByVal value As Integer)
        Dim changed As Boolean = value <> MyBounds.Width

        MyBounds.Width = value

        If changed Then
            RecreatePath()
            RaiseEvent RedrawRequired(Me, EventArgs.Empty)
        End If
    End Set
End Property

此属性更改气泡的形状(因此仅引发 RedrawRequired 事件)。

Private MyFillColor As Color = Color.White    
Public Property FillColor() As Color
    Get
        Return MyFillColor
    End Get
    Set(ByVal value As Color)
        Dim changed As Boolean = value <> MyFillColor

        MyFillColor = value

        If changed Then RaiseEvent RedrawRequired(Me, EventArgs.Empty)
    End Set
End Property

创建气泡

SpeechBalloon 类中的私有 RecreatePath 方法创建我们气泡的 GraphicsPath 缓冲区。为了在气泡周围创建气泡,我们将研究 SpeechBalloon 类的三个属性:BubbleWidth,它指定单个气泡在气泡体周长上的宽度(以度为单位);BubbleSize,它决定气泡从气泡体突出的距离;以及 BubbleSmoothness,它决定我们的气泡是柔和弯曲的(如想法气泡)还是硬朗尖锐的(如星形)。

RecreatePath 首先清除缓冲区。

Private Sub RecreatePath()
'NOTE: To make creating the path easier, I assume the origin is (0, 0)
'when adding the points to the GraphicsPath. When it comes time to 
'actually draw the balloon, I'll call TranslateTransform on the
'Graphics object to shift the origin to the actual location as
'determined in the Bounds property.

'Empty the path:
Path.Reset()

如果我们的 BubbleSize 属性设置为 0,我们就有一个简单的语音气泡,这时 GraphicPathAddEllipse 方法就足够了。

'If the BubbleSize is 0, we'll just create an ellipse:
If BubbleSize = 0 Then
    Path.AddEllipse(0, 0, Width, Height)

否则,我们将不得不做一些工作来制作这些气泡。对于每个气泡,我们需要三个点:气泡在气泡体椭圆上的起始点,气泡在气泡体椭圆上的结束点,以及一个介于两者之间的点,该点位于距气泡体周长一定距离的位置(由 BubbleSize 决定)。

我不会用循环细节和三角函数来计算这三个点,但您可以在代码中查看(我已经尽力注释了),了解更多细节。总而言之,我从未擅长三角函数,但我学到的足以知道如何在线查找我需要的公式。

    Else
        Dim theta As Integer = 0

        'Do an angle sweep around the circle moving 
        'the BubbleWidth in each iteration:
        For theta = 0 To (360 - BubbleWidth) Step +BubbleWidth
            Dim points(2) As Point

            '...
            '
            ' Incredibly boring trig stuff here (see code for details)
            '
            '...

            'Build the triangle between the start angle, 
            'the point away from the balloon 
            '(as determined by the BubbleSize), and the sweep angle:

            points(0) = New Point(x, y)
            points(1) = New Point(x2, y2)
            points(2) = New Point(x3, y3)

            'The BubbleSmoothness value determines how curve-like the lines
            'between the three points will be:
            Path.AddCurve(points, BubbleSmoothness)
        Next

        'Finish off the path:
        Path.CloseAllFigures()
    End If
End Sub

正如您所见,GraphicsPath.AddCurve 方法接受一个参数,该参数决定所得曲线的弯曲程度。值为 0.0 创建一个三角形形状。值为 1.0 创建一个完美的曲线。任何大于 1.0 的值都会产生奇怪的效果(欢迎尝试我的示例应用程序)。

创建尾巴

为简单起见,我仅支持三角形尾巴,并且其绘制原点位于气泡的中心。SpeechBalloon 类中有三个与尾巴相关的属性:TailLength,这是从气泡顶部到垂直尾巴的距离;TailBaseWidth(在示例应用程序中称为尾巴宽度),这是三角形底部的宽度;以及 TailRotation,它决定了尾巴在语音气泡周围指向的角度。我总是绘制一个向上的尾巴,并使用 TailRotationGraphics.RotateTransform 将其旋转到位。

由于尾巴的创建非常简单,我在每次调用 SpeechBalloon 上的 Draw 方法时都会重新创建尾巴。

'Create the tail's path:
'Note: To make drawing easier, I assume the tail is centered
'around the center point of the balloon (from an origin of [0, 0])
'and that it sticks straight up as far as TailLength.
'When it comes time to actually draw the tail, I'll
'call TranslateTransform and RotateTransform to adjust the origin
'and rotation to where we really want it:

tail.AddLine(-TailBaseWidth, 0, TailBaseWidth, 0)
tail.AddLine(TailBaseWidth, 0, 0, -(TailLength + (Height \ 2)))

tail.CloseFigure()

整合

现在我有了气泡路径和尾巴路径,我必须克服如何将它们像一个形状一样填充和勾勒的挑战。经过一些反复试验,我发现了一种相对简单的方法:

  1. 绘制尾巴的边框,其厚度是气泡边框的两倍。当气泡被填充时,它会覆盖掉绘制在气泡下方的尾巴边框部分。
  2. 使用背景色填充气泡路径。
  3. 绘制气泡的边框。
  4. 使用背景色填充尾巴路径。这确保了气泡与尾巴相遇的外围边框被背景色覆盖,从而产生尾巴和气泡都是一个整体的视觉效果,并且它覆盖了尾巴边框的一半(我们将其绘制为双倍大小),使其看起来与气泡边框的其余部分一样宽。

在代码中,这可以转化为以下内容:

'1. Draw the tail border first (if the border is visible):
If TailVisible AndAlso BorderVisible Then
    'We double the pen's size because we're going to fill the
    'tail's color overtop half of the border:
    Dim thickPen As New Pen(BorderColor, BorderWidth * 2.0)

    'Save the graphic state:
    gstate = g.Save()

    'Move to our tail's origin (center of the balloon):
    g.TranslateTransform(Left() + (Width / 2), Top + (Height / 2))

    'Rotate the tail around its origin:
    g.RotateTransform(TailRotation)

    'Draw the border:
    g.DrawPath(thickPen, tail)

    'Restore the previous graphic state:
    g.Restore(gstate)
End If

'Save the state again:
gstate = g.Save()

'Move to our balloon's origin:
g.TranslateTransform(Left, Top)

'2. Fill the balloon's path using the background brush:
g.FillPath(fillBrush, Path)

'3. Draw the balloon's border using the border pen 
'   (if the border is visible):
If BorderVisible Then
    g.DrawPath(borderPen, Path)
End If

'Restore the previous graphic state:
g.Restore(gstate)

'4. Fill the tail's path using the background brush:
If TailVisible Then
    'Save the state yet again:
    gstate = g.Save()

    'Move to our tail's origin (center of the balloon):
    g.TranslateTransform(Left() + (Width / 2), Top + (Height / 2))

    'Rotate the tail around its origin:
    g.RotateTransform(TailRotation)

    'Fill 'er up:
    '   This will cover up half of the tail border 
    '  (thus our need for doubling it above)
    '   and will cover up the balloon border where 
    '   the balloon and the tail intersect
    g.FillPath(fillBrush, tail)

    'Restore the graphics state:
    g.Restore(gstate)
End If

完成所有这些之后,只剩下最后一件事要做了:绘制文本。

'Set our text alignment within the bounds of the balloon, excluding the tail
sf.LineAlignment = StringAlignment.Center
sf.Alignment = StringAlignment.Center

'Draw out our text using the font and text color brush:
g.DrawString(Text, Font, textBrush, Bounds, sf)

Using the Code

我提供的示例应用程序使得使用代码变得非常容易,因为它会生成 VB.NET 代码,用于重现您通过它进行视觉设计的语音气泡。

要快速在您的项目中使用代码,请按照以下步骤操作:

  1. SpeechBalloon.vb 从示例应用程序复制到您的应用程序中。
  2. 运行示例应用程序。
  3. 调整设置,直到您得到想要的气泡。
  4. 在“文件”菜单下,选择“生成代码…”。复制该气泡所需的 VB.NET 代码将显示在其下方。
  5. 从示例应用程序复制生成的代码并粘贴到您的应用程序中。
  6. 在您的应用程序的某个地方,调用语音气泡实例上的 Draw 方法,并传入适当的 Graphics 对象。

超级快乐奖励

我构建了演示应用程序,使其还可以将您设计的语音气泡导出为可移植网络图形(PNG)文件。尽情享用!

我努力注释了演示应用程序代码(都在示例代码的 Form1.vb 中),所以如果您有兴趣了解我是如何编写演示应用程序的,请随意查看。

Possible Enhancements

虽然认为我的小语音气泡生成器很巧妙,但它也有其不足之处。以下是我认为可以进行重大改进的一些可能功能列表:

  • 生成器可以使用双缓冲来减少重绘气泡时出现的闪烁。我不想让示例代码变得复杂。
  • 支持气泡中的富文本。
  • 支持矩形气泡(通常在漫画中用于表示电子或非人类交流)。- 见下方历史记录
  • 能够拥有花哨的尾巴(弯曲的、闪电形状的、想法气泡的云朵等)。
  • 能够拥有随机大小的气泡,为星形和想法气泡提供不太 uniform 的外观。
  • 能够让气泡绘制自己的半透明(即 alpha 混合)阴影。
  • 能够有几个气泡通过小桥接线连接(在漫画中用于在一个面板中分隔对话片段)。

历史

  • 2010/01/09 - 初始版本
  • 2010/11/14 - 添加了对矩形(和圆角矩形)气泡的支持,具有可调节的圆角半径
© . All rights reserved.