Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Creating a Speech Balloon/Speech Bubble in GDI+

0.00/5 (No votes)
16 Nov 2010 1  
A highly configurable class that can generate a few styles of speech balloons in GDI+.

Screenshot 1

Screenshot 2

Screenshot 3

Screenshot 3

Introduction

I was working on a Windows Forms project that required the user to be able to add speech balloons (also known as speech bubbles) on top of photographs or drawings. As I'm somewhat proficient with GDI+, I figured it wouldn't be too complicated; I'd either use a GraphicsPath or a Region. Ultimately, I wanted to draw the balloon, union it with a tail, fill the resulting shape, and draw its outline. I knew Region had a Union method, so I tried that one first. The union worked just fine, but while I was able to fill the combined shapes area with the background color, there is no Draw method for regions in GDI+, thus I was unable to draw the outline of the speech balloon. If I tried to draw the outlines of the individual shapes using the two GraphicsPath objects, the outlines would intersect with each other and not work at all.

I then tried GraphicsPath, and while GDI+ has both Fill and Draw methods for it, there's no way to union two GraphicsPath objects into one shape, and I was left with the same problem for drawing the border as I was with Region.

I also looked into whether there's a way to convert from a Region to a GraphicsPath (in order to get a GraphicsPath object from a unioned Region), but there wasn't. At that point, I stopped trying to figure it out by myself, and fell back on the programmer's best friend, the Internet. Searching for solutions online (where I actually found a few people asking how to make speech balloons), the resounding answer was that you cannot do this due to the limitations of GDI+ and the Region and GraphicsPath classes (which I mentioned above). This article describes how I worked around the problem, and provides a sample application that you can use to generate your own speech balloon images or generate the code required to replicate the balloon you design.

Background

Anatomy of a Speech Balloon

To help make my code and the sample program a little easier to follow, I'm going to provide some basic terminology. I've made most of this stuff up in a way that makes sense to me, so to actual comic artists out there who know all the technical terms for these bits and pieces, I apologize if I'm not using the right names.

Anatomy

Regardless of what type of balloon (speech, thought, starburst) we're talking about, the central area is called the balloon. The protrusions around the perimeter of the balloon (bubbles for thought balloons, spikes or rays for the starbursts) I simply call bubbles. What's the difference between thought balloon bubbles and starburst bubbles, you might ask? The difference is that starburst bubbles are sharper than thought balloon bubbles. The single protrusion that points to the speaker (or thinker) of the balloon, I call the tail.

Requirements

Drawing a speech balloon involves two parts: drawing the actual balloon itself (with its bubbles), and drawing the tail. Because I like to make things as generic and reusable as possible, my speech balloon drawing system had to support the following requirements:

  • The ability to have any text in the balloon
  • The ability to specify the colors of the text, the balloon fill color, and the border color
  • The ability to set the font used to draw the text
  • The ability to show or hide the border
  • The ability to show or hide the tail
  • The ability to change the size and orientation of the tail around the balloon
  • The ability to change the shape of the balloon to simulate speech (ellipse), thought (bubbly ellipse), and actions (starburst)

The SpeechBalloon Class

In my solution, I created a new class called SpeechBalloon which holds all the properties of the speech balloon we can set, as well as the method for actually rendering the balloon into a Graphics object. I'm not going to bother going into the details of pedestrian properties like Font, Bounds, BorderWidth, etc., as they should be pretty self-explanatory and the code is well-commented.

The GraphicsPath object that holds the shape of the balloon is stored in a read-only property named Path. I buffer the GraphicsPath instead of re-creating it on each call to Draw as it can be a complex process (as we'll see below). Each of the other properties, when changed, will cause the path to be recreated (if the said property affects the balloon's shape), and will raise an event, RedrawRequired, on the SpeechBalloon instance to inform the parent program that the speech balloon needs to be redrawn on its drawing surface. Here are some examples:

This property changes the shape of the balloon and causes our code to regenerate its GraphicsPath (via the RecreatePath method):

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

This property does not change the shape of the balloon (and thus only raises the RedrawRequired event):

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

Creating the Balloon

The private RecreatePath method in the SpeechBalloon class creates our balloon's GraphicsPath buffer. To create the bubbles around the balloon, we're going to look at three properties of the SpeechBalloon class: BubbleWidth, which specifies how many degrees wide a single bubble is on the perimeter of the balloon; BubbleSize, which determines how far from the balloon the bubbles protrude; and BubbleSmoothness, which determines whether our bubbles will be soft and curvy (as in thought balloons) or hard and sharp (as in starbursts).

The first thing RecreatePath does is clear the buffer:

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()

If our BubbleSize property is set to 0, we have a simple speech balloon, and GraphicPath's AddEllipse method is all we need:

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

Otherwise, we're going to have to do some work to make these bubbles. For each bubble, we need three points: the starting point of the bubble along the balloon's ellipse, the ending point of the bubble along the balloon's ellipse, and a point between the two that is positioned away from the perimeter of the balloon (as determined by BubbleSize).

I won't bore you with the loop details and trig required to calculate those three points, but you can look at the code (which I've tried to comment as best as I can) for more details. Suffice it to say that I was never that good at trigonometry, but I learned enough to know how to find the formulae I needed online:

    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

As you can see, the GraphicsPath.AddCurve method takes a parameter that determines how curve-like the resulting curve is. A value of 0.0 creates a triangular shape. A value of 1.0 creates a perfect curve. Anything greater than 1.0 does strange things (feel free to experiment with my sample app).

Creating the Tail

To keep things simple, the only tail I support is a triangular one, and its drawing origin is located at the center of the balloon. There are three properties on the SpeechBalloon class associated with the tail: TailLength, which is the distance from a perfectly vertical tail from the top of the balloon; TailBaseWidth (referred to as Tail Width in the sample app), which is the width of the base of the triangle; and TailRotation, which determines at what angle around the speech balloon the tail is going to point towards. I always draw an upward tail and use TailRotation with Graphics.RotateTransform to spin it into position.

Since tail creation is very simple, I recreate the tail as part of each call to the Draw method on SpeechBalloon:

'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()

Putting it All Together

Now that I have my balloon path and my tail path, I had to overcome the challenge of how I was going to fill and outline them both as if they were one shape. After some trial and error, I discovered a relatively simple way to it:

  1. Draw the tail's border, twice as thick as the balloon's border. When the balloon is filled, it will cover up the part of the tail border that is drawn under the balloon itself.
  2. Fill the balloon's path using the background color.
  3. Draw the balloon's border.
  4. Fill the tail's path using the background color. This ensures that the outer border of the balloon where it meets the tail is colored over with the background color, giving the illusion that the tail and the balloon are all one big happy object, plus it covers up half of the tail's border (which we drew double-sized) to make it look as wide as the rest of the balloon's border.

In code, that translates to the following:

'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

Once that's all done, there's only one thing left to do; draw the text:

'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

The sample application I've provided makes using the code very easy as it will generate the VB.NET code needed to reproduce the speech balloon you visually design with it.

To quickly use the code in your project, follow these steps:

  1. Copy SpeechBalloon.vb from the sample application into your application.
  2. Run the sample application.
  3. Fiddle with the settings until you have the balloon that you want.
  4. Under the "File" menu, select "Generate Code...". The VB.NET code required to reproduce that balloon will be displayed below it.
  5. Copy the generated code from the sample application and paste it into your application.
  6. At some point in your application, call the Draw method on the instance of the speech balloon, passing it the appropriate Graphics object.

Super Happy Bonus

I've built the demo application so that it can also export the speech balloon you design to a Portable Network Graphics (PNG) file. Enjoy!

I made an effort to comment the demo application code (all in Form1.vb in the sample code), so if you're interested to see how I wrote the demo app, feel free to look through it.

Possible Enhancements

While I think my little speech balloon generator is nifty, it does have its shortcomings. Here are a list of possible features that I think would be great enhancements:

  • The generator could use a double-buffer to reduce the flickering when redrawing the balloon. I didn't want to complicate the sample code with that.
  • Support for rich text in the balloon.
  • Support for rectangular balloons (normally used in comics to show electronic or non-human communication). - See History Below
  • Ability to have fancy tails (curved, lightning-bolt shaped, little puffs of clouds for thought balloons, etc.).
  • Ability to have random-sized bubbles to give a less uniform appearance for starbursts and thought balloons.
  • Ability to have the balloon draw its own semi-transparent (i.e., alpha-blended) drop shadow.
  • Ability to have several balloons connected by small bridging lines (used in comics to separate pieces of a conversation in one panel).

History

  • 2010/01/09 - Initial version
  • 2010/11/14 - Added support for rectangular (and round-rectangular) balloons, with adjustable corner radius

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here