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

Adding Balloon Windows to a .NET Application

0.00/5 (No votes)
13 Jul 2003 1  
Introduction to the BalloonWindow class which allows .NET applications to implement balloon windows similar to what is available in Windows XP. Complete customization allows both the appearence and shape to be configured as well as projecting an alpha-blended shadow.

Sample screen demonstrating the BalloonWindow class.

Introduction

Operating system balloon window anchored to notify icon.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();

Anchor quadrant and offset.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.


Anchor appearance changes depending on quadrant and offset.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;
}

Anchor position after rotation.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.

Shows the overlapping content and shadow.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.

Object model for the BalloonWindow class structure.

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.

    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