
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:
- How to use a
Form
�s Region property to define a custom form shape
- Simple generation of a shadow for the balloon form
- Use of a
Form
�s Owner
property
- Generation of Regions using a
GraphicsPath
object
- 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.
GraphicsPath m_path = new GraphicsPath();
m_path.AddLines(new Point[] { pA, pB, pC } );
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);
pSize.Width += 4; pSize.Height -= 2; pLoc.X -= 2; pLoc.Y += 1;
m_rOuterFrame.Union(new Rectangle(pLoc, pSize));
pSize.Width += 2; pSize.Height -= 2; pLoc.X -= 1; pLoc.Y += 1;
m_rOuterFrame.Union(new Rectangle(pLoc, pSize));
pSize.Width += 2; pSize.Height -= 2; pLoc.X -= 1; pLoc.Y += 1;
m_rOuterFrame.Union(new Rectangle(pLoc, pSize));
pSize.Width += 2; pSize.Height -= 4; pLoc.X -= 1; pLoc.Y += 2;
m_rOuterFrame.Union(new Rectangle(pLoc, pSize));
m_rOuterFrame.Union(m_path);
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:
if (m_bIsShadow == true)
{
Region rgnEx;
rgnEx = m_rOuterFrame.Clone();
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).
public void setBalloonPosition(Form onForm, Control onControl)
{
if (m_fmShadow == null)
{
this.Owner = onForm;
}
else
{
m_fmShadow.Owner = onForm;
}
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!