Click here to Skip to main content
15,885,216 members
Articles / Programming Languages / C#
Article

Yet Another Balloon Goes Up

Rate me:
Please Sign up or sign in to vote.
4.58/5 (10 votes)
6 Sep 20026 min read 144.5K   1.2K   67   17
Demonstrates custom form shapes, regions, delegates, form ownership

Image 1

Introduction

The provided code was the proof-of-concept for my own breed of Balloon dialogue box. I’ve kept the code to a minimum, because I wanted to demonstrate the following:

  1. How to use a Form’s Region property to define a custom form shape
  2. Simple generation of a shadow for the balloon form
  3. Use of a Form’s Owner property
  4. Generation of Regions using a GraphicsPath object
  5. Linking an event in one form to a handler in another

There are no WndProc intercept or Hooks. Consult previous Balloon submissions where these topics are amply covered.

Compile the Program or run the Executable!

Run the program, and a static balloon appears with a ‘press me’ label. Clicking on the label throws up a plain, vanilla form containing a text box. The text box is used as the anchor point for the balloon. Press the ‘Open Balloon’ button and my shameless self-image should appear.

You can drag the form around and, with a little luck, the balloon will follow. The position and orientation of the tail should change to fit the balloon within the screen. If you minimize the form, the balloon should also minimize. Dismiss the balloon either by clicking on the ‘press me’ label, or by clicking on the form surface. Clicking on the shadow or a control surface within the balloon will not work (which is one reason why you might want to consider the use of hooks)

Okay, how was this achieved?

All the significant code resides in the CBalloonBase Class. The static balloon displayed at the start is an instance of this base class, whereas my example balloon (CExample) is simply subclass with no additional code worth remarking on. The visual appearance of CExample (picture boxes, labels etc) was generated through the setting of Properties and through placing controls using the IDE’s Designer view.

Building the Balloon’s custom form shape

On my first attempt, I created a GraphicsPath, populated with four arcs and the straight lines required to create the outline of the balloon. Drawn to a surface, such as a form or picture box, the outline looked perfect. But when I tried constructing a Region using the GraphicsPath object, the results were far less than satisfactory.

My second attempt was much simpler. A Region is essentially a collection of interacting rectangles. So I created a Region, using a series of overlapping rectangles to represent the body of the balloon. The balloon’s tail was created using a GraphicsPath object. The balloon Region is completed by merging (with the Region.Union(GraphicsPath) method) the tail.

C#
GraphicsPath m_path = new GraphicsPath();

// generate balloon tail outer path (pA, pB, pC 
// are pre-calculated points)
m_path.AddLines(new Point[] { pA, pB, pC } );

// generate the outer frame (m_rOuterFrame) 
// which becomes the shape of the balloon form
Size pSize = new Size(nW - (nInner + nInner), nH - (nOuter + nOuter));
Point pLoc  = new Point(nInner, nOuter);
Rectangle aRect = new Rectangle(pLoc, pSize);
m_rOuterFrame = new Region(aRect);
			
// generate 2nd pass region for the outer frame, 
// starting to form the rounded edges
pSize.Width += 4; pSize.Height -= 2; pLoc.X -= 2; pLoc.Y += 1;
m_rOuterFrame.Union(new Rectangle(pLoc, pSize));

// generate 3rd pass for the outer frame, developing rounded edges
pSize.Width += 2; pSize.Height -= 2; pLoc.X -= 1; pLoc.Y += 1;
m_rOuterFrame.Union(new Rectangle(pLoc, pSize));

// generate 4th pass for the outer frame, developing rounded edges
pSize.Width += 2; pSize.Height -= 2; pLoc.X -= 1; pLoc.Y += 1;
m_rOuterFrame.Union(new Rectangle(pLoc, pSize));

// generate 5th pass for the outer frame, completing the rounded edges
pSize.Width += 2; pSize.Height -= 4; pLoc.X -= 1; pLoc.Y += 2;
m_rOuterFrame.Union(new Rectangle(pLoc, pSize));

// generate 6th pass for the outer frame, adding the balloon tail
m_rOuterFrame.Union(m_path);

// set the region for the form
this.Region = m_rOuterFrame;

This second approach worked perfectly. The final step is to assign our balloon Region (see last code statement above) to the Form’s Region property. CBalloonBase itself does not change shape until run-time, however any subclass will appear as a snazzy looking balloon in the IDE’s Designer.

Building the Balloon’s frame

Building the outline was the real trick. The interior frame is composed in much the same way as the outline. First the outline region is cloned and then a client area and interior tail path are excluded from the clone. What remains is the frame, which we shall paint, using a gradient brush, in the reDrawMe() function.

Building the Shadow

The shadow is implemented as a form object, generated from within CBalloonBase in the bMakeShadow() function. This is the same form as the balloon itself, however it is drawn without a frame, using a fixed background color (Color.DarkGray), and with the Opacity Property on the shadow’s form set to 0.6 (or 60% if you prefer). I have also chosen to mask out the outline of the balloon’s form from the shadow’s form. I’m not sure if this makes a real-time improvement, but it certainly looks better at run-time:

C#
// for shadow windows we don't need to paint the
// area under the foreground balloon
if (m_bIsShadow == true) 
{
    Region rgnEx;
    rgnEx = m_rOuterFrame.Clone();
    // the shadow is offset five pixels down and to the right
    rgnEx.Translate(-5, -5);
    m_rOuterFrame.Exclude(rgnEx);
}

Setting Ownership

Because you are all so smart, you already know the Owner Property guarantees the Owned form will never appear behind its Owner. Likewise, if the Owner is minimized, the Owned form is also minimized. Consequently the balloon is owned by the shadow and the shadow is owned by the form which contains the anchor point.

The only wrinkle occurs because the shadow is optional in my balloon implementation (turned off by a Property in CBalloonBase). In this event the balloon is directly owned by the form with the anchor. The simple procedure of setting ownership saves a good deal of code! On the downside, you really should remember to RemoveOwnedForm() from the Owner when dissolving the relationship (see CBalloon_Closing() and DestroyShadow()).

The feeble magic surrounding the Move Event

Okay, what happens when the form containing the anchor moves? How does the balloon know to move with the anchor form? The secret lies in adding an Event Handler to the anchor form’s Move Event. The call to establish the anchor point is setBalloonPosition() and always contains a handle to the anchor’s form. (The anchor itself can either be a point or a control).

C#
// entry point creates an anchor control point
// (onControl) within an anchoring form (onForm)
public void setBalloonPosition(Form onForm, Control onControl) 
{
    // determine ownership so the balloon stays ontop of the owning form
    if (m_fmShadow == null) 
    {
        this.Owner = onForm;
    } 
    else
    {
        m_fmShadow.Owner = onForm;
    }
    // remember the control's position and dimensions (snip!)

    // (end of snip) attach a callback so we know when
    // the owner is moved. Oh the joys and advantages of delegates!
    m_evhMove = new System.EventHandler(this.Parent_Move);
    onForm.Move += m_evhMove;
}

CBalloonBase simply adds an Event Handler (delegate) to the anchor’s form on the Move Event, and thereafter receives automatic notification whenever the anchor’s form is moved. A more sophisticated version might also keep track of the anchor point within its parent form! Ah, the joys of delegates!

Again, when the balloon is dismissed the event handler should be removed from the anchor’s form. Otherwise a reference to the balloon object is maintained for the life of the anchor’s form. Garbage collection will have difficulty closing out the balloon even though it is no longer visible!

Properties

I added several Properties. These are tailOffset, tailAlign, tailSide, IsShadow, FrameTopLeft, FrameBottomRight and HasShadow. Of these, you should set only FrameTopLeft, FrameBottomRight and HasShadow.

The tailOffset, tailAlign and tailSide are interesting during design, but when anchored to a control the code itself will determine all three Properties regardless of the design-time settings.

The remaining Property, IsShadow, should never be used. This, along with several other Properties such as FormBorderStyle (which must be set to ‘None’) should ideally be removed from the Designer’s Properties Window.

HasShadow determines if the balloon has a shadow (obviously)

What Else?

I intended this code as an illustration of what is possible, and I dispensed with a hosting application because it wasn’t needed. After minor modifications you can include the CBalloonBase form in your project and subclass to your heart’s content. You can even make a Custom Control out of it if you have the time and energy. Before embarking on that course, I recommend you read “GDI+ Programming: Creating Custom Controls Using C#” by Eric White and co.! It is succinct (unlike me) and very helpful.

Known faults include an excessive number of calls to the function reSizeMe(), which could be removed by proper design technique (I was cutting my GDI+ teeth on this project so I employed no technique whatsoever).

In the Custom Control version, which unfortunately I cannot publish, the balloon has a ‘fade-in, fade-out’ capability which makes its appearance and disappearance much more pleasing. I also suggest improving the visual appearance of the frame, and in particular the tail itself. Have fun!

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


Written By
Web Developer
United States United States
I am Borg. I have six computers, a Lego mindstorm with video camera and I'm currently building a Dalek.

Generally I spend so much time in front of computers I probably glow in the dark.

However I like skiing, flying aircraft and the company of old women and young wines. Hang on? Isnt' that the wrong way round? I forget.

I will answer any question about anything. I am particularly good at things I know nothing about.

Where is my gin and tonic?

Comments and Discussions

 
GeneralMy vote of 5 Pin
Vercas19-Oct-10 23:51
Vercas19-Oct-10 23:51 
GeneralSubclassing this in .Net 2005 Pin
Kountree18-Oct-07 4:06
Kountree18-Oct-07 4:06 
GeneralRe: Subclassing this in .Net 2005 Pin
Kountree18-Oct-07 5:36
Kountree18-Oct-07 5:36 
GeneralAll I want... Pin
bobishkindaguy23-Aug-05 9:13
bobishkindaguy23-Aug-05 9:13 
Generaltray icon Pin
bob dole++26-Jun-05 10:17
bob dole++26-Jun-05 10:17 
GeneralRe: tray icon Pin
radialronnie31-Mar-08 10:40
radialronnie31-Mar-08 10:40 
QuestionTransparency? Pin
kay.one9-Apr-04 12:15
kay.one9-Apr-04 12:15 
AnswerRe: Transparency? Pin
bryanjamesross23-Sep-04 14:57
bryanjamesross23-Sep-04 14:57 
GeneralRe: Transparency? [modified] Pin
Noah Moerbeek2-Nov-06 12:59
Noah Moerbeek2-Nov-06 12:59 
GeneralFocus Pin
Mark Mihevc21-Jan-04 6:22
Mark Mihevc21-Jan-04 6:22 
GeneralRe: Focus Pin
kay.one9-Apr-04 11:49
kay.one9-Apr-04 11:49 
Generalborder Pin
klenne26-Aug-03 4:04
klenne26-Aug-03 4:04 
GeneralDrawing Problem Pin
Dion Heskett22-Dec-02 8:34
Dion Heskett22-Dec-02 8:34 
GeneralRe: Drawing Problem Pin
Alastair Stell30-Mar-03 21:03
Alastair Stell30-Mar-03 21:03 
GeneralCool! Pin
Shog97-Sep-02 15:14
sitebuilderShog97-Sep-02 15:14 
GeneralBloom County Pin
Chris Maunder7-Sep-02 9:19
cofounderChris Maunder7-Sep-02 9:19 
GeneralRe: Bloom County Pin
Alastair Stell7-Sep-02 9:29
Alastair Stell7-Sep-02 9:29 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.