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
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
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
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.
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.
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
BorderWidth, etc., as they should be pretty self-explanatory and the code is well-commented.
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
Public Property Width() As Integer
Set(ByVal value As Integer)
Dim changed As Boolean = value <> MyBounds.Width
MyBounds.Width = value
If changed Then
RaiseEvent RedrawRequired(Me, EventArgs.Empty)
This property does not change the shape of the balloon (and thus only raises the
Private MyFillColor As Color = Color.White
Public Property FillColor() As Color
Set(ByVal value As Color)
Dim changed As Boolean = value <> MyFillColor
MyFillColor = value
If changed Then RaiseEvent RedrawRequired(Me, EventArgs.Empty)
Creating the Balloon
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
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()
BubbleSize property is set to
0, we have a simple speech balloon, and
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
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:
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)
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
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
tail.AddLine(-TailBaseWidth, 0, TailBaseWidth, 0)
tail.AddLine(TailBaseWidth, 0, 0, -(TailLength + (Height \ 2)))
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))
gstate = g.Save()
If BorderVisible Then
If TailVisible Then
gstate = g.Save()
g.TranslateTransform(Left() + (Width / 2), Top + (Height / 2))
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
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.
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).
- 2010/01/09 - Initial version
- 2010/11/14 - Added support for rectangular (and round-rectangular) balloons, with adjustable corner radius