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.
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()
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 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
For theta = 0 To (360 - BubbleWidth) Step +BubbleWidth
Dim points(2) As Point
points(0) = New Point(x, y)
points(1) = New Point(x2, y2)
points(2) = New Point(x3, y3)
Path.AddCurve(points, BubbleSmoothness)
Next
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
:
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:
- 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.
- Fill the balloon's path using the background color.
- Draw the balloon's border.
- 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:
If TailVisible AndAlso BorderVisible Then
Dim thickPen As New Pen(BorderColor, BorderWidth * 2.0)
gstate = g.Save()
g.TranslateTransform(Left() + (Width / 2), Top + (Height / 2))
g.RotateTransform(TailRotation)
g.DrawPath(thickPen, tail)
g.Restore(gstate)
End If
gstate = g.Save()
g.TranslateTransform(Left, Top)
g.FillPath(fillBrush, Path)
If BorderVisible Then
g.DrawPath(borderPen, Path)
End If
g.Restore(gstate)
If TailVisible Then
gstate = g.Save()
g.TranslateTransform(Left() + (Width / 2), Top + (Height / 2))
g.RotateTransform(TailRotation)
g.FillPath(fillBrush, tail)
g.Restore(gstate)
End If
Once that's all done, there's only one thing left to do; draw the text:
sf.LineAlignment = StringAlignment.Center
sf.Alignment = StringAlignment.Center
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:
- Copy SpeechBalloon.vb from the sample application into your application.
- Run the sample application.
- Fiddle with the settings until you have the balloon that you want.
- Under the "File" menu, select "Generate Code...". The VB.NET code required to reproduce that balloon will be displayed below it.
- Copy the generated code from the sample application and paste it into your application.
- 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