Introduction
This article introduces the BalloonWindow
class. The design goal for BalloonWindow
was to allow any .NET application to display balloons that function like those that are part of the operating system such as the one shown in Figure 2.
Written in C#, BalloonWindow
exposes the necessary functionality to fully customize the balloon�s appearance. Examples of customizations include, setting the background style or color; defining the balloon layout by adjusting the anchor placement, corner curvature, or shadow effects; and placing controls within the balloon like all other Form classes.
Calculating the Shape
The shape of a balloon is maintained by a GraphicsPath
object and calculated by the RecalcLayout
method.
private GraphicsPath RecalcLayout(Rectangle rect, Point target)
{
GraphicsPath gp = new GraphicsPath();
Two independent components control the anchor�s shape and placement: The anchor quadrant indicating one of the four sides, and the anchor offset indicating the increasing distance along the specified anchor quadrant. Figure 3 shows the relation between these two components and the balloon as a whole.
An important design consideration was for these to be independent because, as evident in Figure 4, there are twelve anchor permutations. Along any quadrant, the anchor�s position can be pre-center, center, or post-center. Center is the exact location between the minimum and maximum offsets. When the anchor is centered, its sweep angle is 90-degrees while the angle for all other positions is 45-degrees with the perpendicular edge always facing away from center.
The code snippet below calculates necessary adjustments for the anchor quadrant. Calculating the quadrant is simple and only requires the use of the matrix
and anchorFlipped
variables, which are explained shortly. This coding is based on a rotation from the �top� of the balloon in order to determine the placement of any other anchors, i.e. if he anchor is required at one of the three �top� anchor locations, no rotation would be utilized, whereas, if the anchor is required on the right or left sides of the balloon, a 90- or -90-degree rotation, respectively, would be used.
switch(anchorQuadrant)
{
case AnchorQuadrant.Top:
break;
case AnchorQuadrant.Bottom:
matrix.Translate(balloonBounds.Width, balloonBounds.Height);
matrix.Rotate(180);
anchorFlipped = true;
break;
case AnchorQuadrant.Left:
balloonBounds.Size =
new Size(balloonBounds.Height, balloonBounds.Width);
matrix.Translate(0, balloonBounds.Width);
matrix.Rotate(-90);
anchorFlipped = true;
break;
case AnchorQuadrant.Right:
balloonBounds.Size =
new Size(balloonBounds.Height, balloonBounds.Width);
matrix.Translate(balloonBounds.Height, 0);
matrix.Rotate(90);
break;
}
As previously mentioned, when building the path, all calculations are assumed to be with the anchor on the top quadrant. This was done to make the calculations simple and predictable. Once the path is complete, it is adjusted using the matrix
variable which rotates the balloon so the anchor is on the correct quadrant. However, using a base anchor quadrant of the top posed an interesting problem. When the balloon is rotated either 180- or 270-degrees, the anchor offset ends up on the opposite side of center then required. Figure 5 demonstrates this situation. The anchorFlipped
variable flags this condition allowing the anchor offset to be recalculated as shown below. As can be seen from Figure 5, when the anchor is rotated 180-degrees, the anchor is brought down to the �bottom-right.� A subsequent shift needs to happen before the anchor is placed at the correct location, in this case the �bottom-left.�
if(anchorFlipped)
anchorOffset =
(int)balloonBounds.Width-(offsetFromEdge*2)-anchorOffset;
Regardless if the offset is pre-center, center, or post-center an anchor always has three points. The following code calculates these three points.
if(anchorOffset < balloonEdgeCenter)
{
anchorPoints[0] =
new Point(anchorOffset+offsetFromEdge,
(int)balloonBounds.Y+anchorMargin);
anchorPoints[1] =
new Point(anchorOffset+offsetFromEdge,
(int)balloonBounds.Y);
anchorPoints[2] =
new Point(anchorOffset+anchorMargin+offsetFromEdge,
(int)balloonBounds.Y+anchorMargin);
}
else if(anchorOffset > balloonEdgeCenter)
{
anchorPoints[0] =
new Point(anchorOffset-anchorMargin+offsetFromEdge,
(int)balloonBounds.Y+anchorMargin);
anchorPoints[1] =
new Point(anchorOffset+offsetFromEdge,
(int)balloonBounds.Y);
anchorPoints[2] =
new Point(anchorOffset+offsetFromEdge,
(int)balloonBounds.Y+anchorMargin);
}
else
{
anchorPoints[0] =
new Point(anchorOffset-anchorMargin+offsetFromEdge,
(int)balloonBounds.Y+anchorMargin);
anchorPoints[1] =
new Point(anchorOffset+offsetFromEdge,
(int)balloonBounds.Y);
anchorPoints[2] =
new Point(anchorOffset+anchorMargin+offsetFromEdge,
(int)balloonBounds.Y+anchorMargin);
}
With the calculations complete, the path can finally be constructed as shown below.
gp.AddArc(balloonBounds.Left, balloonBounds.Top+anchorMargin,
cornerDiameter, cornerDiameter,180, 90);
gp.AddLine(anchorPoints[0], anchorPoints[1]);
gp.AddLine(anchorPoints[1], anchorPoints[2]);
gp.AddArc(balloonBounds.Width-cornerDiameter,
balloonBounds.Top+anchorMargin,
cornerDiameter, cornerDiameter, -90, 90);
gp.AddArc(balloonBounds.Width-cornerDiameter,
balloonBounds.Bottom-cornerDiameter,
cornerDiameter, cornerDiameter, 0, 90);
gp.AddArc(balloonBounds.Left, balloonBounds.Bottom-cornerDiameter,
cornerDiameter, cornerDiameter, 90, 90);
One final issue; adjust the path so the anchor is located on the correct quadrant.
gp.Transform(matrix);
Constraining the Region
Support for the non-standard window shape is provided by the GraphicsPathWindow
class. This class, itself, does not know anything about the shape of the window; rather, a call to the virtual PreparePath
method is invoked when needed as shown below. Here the GetPath
method checks to see if a path is cached, if not it asks the derived class to provide the path.
public GraphicsPath GetPath()
{
GraphicsPath gp = __graphicsPath;
if(gp == null) gp = PreparePath();
SetPath(gp);
return gp;
}
This provides all derived classes the ability to define the path as needed. Below is the code that BalloonWindow
uses to define its path.
protected override GraphicsPath PreparePath()
{
return __layout.Path;
}
A window defines a region which instructs the operating system to draw only within the region. The following code constrains the region and ensures the window looks like a balloon. When a Region
is defined from a GraphicsPath
object, the region is defined as the inner area of the path. This code below includes adjustment to include the path border.
private Region RegionFromPath(GraphicsPath gp)
{
if(gp == null) throw(new ArgumentNullException("gp"));
Region region = new Region(gp);
float inflateBy = 1F+2F/(float)Width;
Matrix matrix = new Matrix();
matrix.Scale(inflateBy, inflateBy);
matrix.Translate(-1, -1);
region.Transform(matrix);
return region;
}
Projecting a Shadow
Windows 2000 introduced layer-windows. Layer-windows allow the operating system to alpha-blend the contents of a window with the background. An example of this would be the shadows projected from the balloon windows available in Windows 2000 and XP.
A design goal for BalloonWindow
was to support this same shadowing effect. Several design considerations were researched and ultimately the best approach was to implement shadows in a separate window positioned behind the content window as shown in Figure 6.
The class ShadowedWindow
maintains the shadow effect which BalloonWindow
inherits. When a shadow is first displayed, CreateShadowProjection
creates a Projection
object. Keep in mind that ShadowedWindow
is not the shadow itself. The shadow window itself is actually encapsulated by the Projection
object maintained by ShadowedWindow
.
private Projection CreateShadowProjection()
{
Projection shadow = new Projection(this);
shadow.BackColor = Color.White;
BindShadowToOwner(shadow, this);
return shadow;
}
The projected shadow always lies behind the content window and has the same dimensions. Because of this, the shadow needs to be offset slightly so it is visible.
public void ShowShadow()
{
GraphicsPathWindow shadow = __shadow;
if(shadow == null) shadow = __shadow = CreateShadowProjection();
int shadowMargin = ShadowMargin;
Point shadowLocation =
new Point(Location.X+shadowMargin, Location.Y+shadowMargin);
Size shadowSize = Size;
shadow.Location = shadowLocation;
shadow.Size = shadowSize;
shadow.Show();
}
A shadow is created on an off-screen bitmap rendered with a gradient pattern.
Bitmap img = new Bitmap(Width, Height);
GraphicsPath path = GetPath();
Graphics grx = Graphics.FromImage(img);
float scaleFactor = 1F-((float)__owner.ShadowMargin*2/(float)Width);
PathGradientBrush backStyle = new PathGradientBrush(path);
backStyle.CenterPoint = new Point(0, 0);
backStyle.CenterColor = __owner.ShadowColor;
backStyle.FocusScales =
new PointF(scaleFactor, scaleFactor);
backStyle.SurroundColors =
new Color[]{Color.Transparent};
Region region = new Region(path);
region.Translate(-__owner.ShadowMargin, -__owner.ShadowMargin);
grx.SetClip(region, CombineMode.Xor);
grx.FillPath(backStyle, path);
The SetBitmap method initializes and updates the layer-window using code provided by Rui Godinho Lopes which is explained in his document �Per Pixel Alpha Blend in C#.�
SetBitmap(img);
Conclusion
BalloonWindow is a powerful library for generating a simple UI element.
Class Structure
BalloonWindow is designed to be a robust and essential component for any application. Figure 7 shows the public object model available for BalloonWindow.
Build History
For information about each build, please refer to the
BalloonWindow Library for .NET portal.
Copyright
Copyright � 2002-2003 by Peter Rilling
The source file(s) and binaries may be redistributed unmodified by any means PROVIDING they are not sold for profit without the authors expressed written consent, and providing that this notice and the authors name and all copyright notices remain intact.
Any use of the software in source or binary forms, with or without modification, must include, in the user documentation ("About" box and printed documentation) and internal comments to the code, notices to the end user as follows:
"Portions Copyright � 2002-2003 Peter Rilling"
An email letting me know that you are using it would be nice as well. That's not much to ask considering the amount of work that went into this.
THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED. USE IT AT YOUT OWN RISK. THE AUTHOR ACCEPTS NO LIABILITY FOR ANY DATA DAMAGE/LOSS THAT THIS PRODUCT MAY CAUSE.