Click here to Skip to main content
15,867,568 members
Articles / Desktop Programming / Windows Forms

Animated Eye Candy for Programmers

Rate me:
Please Sign up or sign in to vote.
4.93/5 (82 votes)
16 Apr 2010LGPL317 min read 85.7K   7.4K   164   20
A class library that allows (almost) any Control to show animations
Animations on a Controls (e.g. a DataListView)

Image 1

Google Enviers' Anonymous

"I'd like to welcome you all to this weeks meeting of Google Enviers' Anonymous. Hi. My name is Phillip and I'm a Google envier. I've managed to control my condition for almost six days. But this morning, I fell off the wagon in a big way. I was working normally and then I saw it! You all know what happens next. My breathing was suddenly rapid and shallow. I reached for my paper bag to keep calm. But it was too late. Waves of jealousy swept over me. In desperation, I speed-dialed my Google Envier buddies, Steve and Bill, and they talked me through it. After a minute or so, the spasm passed and I returned to (more or less) normal, though the bitter taste of envy lingered for the rest of the day.

In my self defence, the source of my envy is nothing as crass as their billion dollar development budget, their wonderful working conditions, or their thousand plus dollar share price. All these things fall into insignificance in comparison with the true object of my obsession: their animations! The way they effortlessly add little spinning stars, glowing text or fading sparkles to their applications. My applications sit there fully functional and obedient -- but static and passive, lacking the moving eye candy that make Google apps cute and cool.

But no more! To you, fellow enviers, I present the Sparkle animation framework. With this framework, you too can put animations into your applications and free yourself from the shackles of Google envy."

Understanding Sparkle in 30 Seconds or Less

OK. A little more seriously this time. The Sparkle library's purpose is to allow (almost) any Control to show animations.

The design goals of the Sparkle library are:

  • Short-lived animations on any control. The Sparkle library is designed to draw short animations over the top of existing Controls.
  • Declarative. The Sparkle library is declarative. You say what you want the animation to do, and then you run the animation. The animation is completely defined before it begins.
  • Non-interactive. The Sparkle library does not do user interaction -- it does not listen to mouse moves, clicks or drags. It doesn't do collision detection or physics models. It just draws eye candy.

To use the library itself, you'll need to grasp its four major concepts:

  1. Animations. An animation is the canvas upon which sprite are placed. It is the white board upon which things are drawn.
  2. Sprites. Sprites are things that can be drawn. There are several flavours of sprites -- one for images, another for text, still another for shapes. It is normal to make your own types of sprites by subclassing Sprite (or implementing ISprite).
  3. Effects. Effects are things that make changes to sprites over time. They are the "movers and shakers" in the library, who actually do things. Sprites sit there, completely passive, looking pretty, but the effects push them around, change their visibility, spin or size. Again, you can use existing Effects, or implement your own through the IEffect interface.
  4. Locators. Locators are things that known how to calculate a point or a rectangle. Rather than saying "Put this sprite at (10, 20)," they allow you to say "Put this sprite at the bottom right corner of this other sprite." This idea can be tricky to get your mind around, but once you have grasped it, it is powerful.

How to Use It

Adding any sort of animation to an application is a multi-step process. With Sparkle, the workflow for creating an animation is:

  1. Decide where the animation will appear. That is your Animation.
  2. Think about what you want to show. They are your Sprites.
  3. Think about what you want each Sprite to do. They are your Effects.
  4. Whenever an Effect needs a "where", that's when you need Locators.

Simple Example

To get a feeling for how to use the library, there's no substitute for seeing code. So let's do a simple example of moving a word across a control.

According to our workflow (above), we first need to decide where we want our animation to appear. So let's make a new project, with a new form, and put on it a UserControl that's docked to fill the whole form. Put a button on the form called "Run". In the click handler for that button, we'll do the work of running an animation.

The first thing we need is an Animation on that UserControl.

C#
AnimationAdapter adapter = new AnimationAdapter(this.userControl1);  
Animation animation = adapter.Animation;  

AnimationAdapter is the class that links an Animation to an existing control. We've used a UserControl, but it could be any Control that supports the Paint event.

Once we have an Animation, the workflow says we need to decide what be seen: they are our Sprites. We want to show the word "Sparkle" so we make a TextSprite. If we had want to show an image, there is an ImageSprite, and to show shapes, there is a ShapeSprite. You can (of course) make your own sprites by implementing the ISprite interface or by subclassing Sprite:

C#
TextSprite sparkle = new TextSprite("Sparkle!", new Font("Gill Sans", 48), Color.Blue);  

OK. We have our sprite. What do we want it to do? We want to move it from the top left of animation to the bottom right. Moving (or any other sort of change over time) requires an Effect. In this case, we need a MoveEffect. You can create these directly – using new MoveEffect(...) – or you can use the Effects factory, which has lots methods to make Effect objects:

C#
sparkle.Add(100, 1000, Effects.Move(Corner.TopLeft, Corner.BottomRight));  

This says, “Beginning 100 milliseconds after the sprite starts, and lasting for 1000 milliseconds, move this sprite from the top right to the bottom left of the animation.”

That’s all we want the sprite to do at the moment, so now we add the sprite to the animation. Not all sprites are active at the beginning of the animation, so when we add the sprite the animation we also tell it when the sprite should begin. In this case, we do want the sprite to start when the animation starts, so we give 0 as the start time for the sprite:

C#
animation.Add(0, sparkle);  

And finally, we tell the animation to run:

C#
animation.Start();

All being well, you should see something like this:

Image 2

For those who noted that the above graphic lacks one of the fundamental requirements for an animation (namely animated-ness), CodeProject doesn't support animations within a page, so click here to see the actual animation.

A Little More Interesting

Under whelmed? Admittedly, it's not that impressive, but you did only write six lines of code! But if we add a few different effects, you can easily do something more impressive. For example, if we want the text to walk around the edge of the control, spinning and fading at the same time:

C#
sparkle.Add(0, 5000, Effects.Rotate(0, 360 * 4));
sparkle.Add(0, 5000, Effects.Blink(5));
sparkle.Add(0, 5000, Effects.Walk(Locators.AnimationBounds(-100, -50), 
	WalkDirection.Anticlockwise)); 

This gives the Sprite three effects that run simultaneously. The first two are fairly obvious, but the third is a little trickier. It uses the Effects factory to create an effect that will walk a sprite around a rectangle. The rectangle to be walked is a "where" so it uses a Locator. This particular locator returns the bounds of the Animation, inset by (100, 50).

Putting these together produces this:

Image 3

Again, you have to look here to see the actual animation.

Admittedly, this is garish, but it does give an idea of what you can do with just a few more lines of code.

Animations

An animation has two distinct functions:

  1. It implements a timer tick based animation system. At each tick of a clock, it advances the state of the animation: it decides which sprites should become active/inactive, gives effects the opportunity to do their magic. This portion does not perform any rendering -- it simply changes the state of parts of the animation. If anything needs to be rendered, the animation triggers a Redraw event.
  2. It draws the sprites according to their current state. Normally, something would listen for the Redraw event on an Animation, and in response to that event, it would redraw the animation. To do that, it calls the Animation.Draw(Graphics g) method. This will render the animation, in its current state, onto the given Graphics object. This operation does not change the state of the Animation.

In addition to animating sprites and rendering them, an animation supports the basic set of commands to control its execution:

  • Start()
  • Pause()/Unpause()
  • Stop()

Repeat Behaviour

Animations have a Repeat property, which controls the animation's behaviour when it reaches the end of the animation.

  • Repeat.None - The animation simply quits. All sprites disappear. This is the default.
  • Repeat.Pause - The animation pauses. All sprites that were visible at the end of the animation remain visible and motionless.
  • Repeat.Loop - The animation begins again.

Sprites

Sprite are the actual eye candy -- the pretty do-nothing things that the user can see. They keep whatever state information they require -- location, size, color, transparency -- and then use that state information to draw themselves when asked. They don't change their own state -- that's the responsibility of Effects.

There are several flavours of sprites that come with the Sparkle library:

  • ImageSprite. This takes an Image and draws it according to the sprites state. If the given Image is a frame animation itself, the Sparkle framework will animate that image automatically. I think it is only animated GIFs that Microsoft supports as frame animations.
  • TextSprite. TextSprites draw text (no prizes). But they can do a bit more formatting than just that. The text can be colored (ForeColor property), they can be drawn with a background (BackColor property). They can draw a border around the text (BorderWidth and BorderColor properties). The border can be either a rectangle (set CornerRounding to 0) or a round cornered rectangle (set CornerRounding to greater than 0 -- 16 is normally nice).
  • ShapeSprites. These draw regular shapes (square, rectangles, round cornered rectangle, triangles, ellipses/circles). Like TextSprites, ShapeSprites can have a ForeColor (color of frame of the shape), BackColor (used for the filled part of the shape), and PenWidth (width of the frame).

Remember, all colors can have alpha values set for them, which will allow varying levels of transparency when drawing the sprites.

Custom Sprites

It is expected that application would implement new sprites to do whatever specialized drawing it requires. To do this, you would implement the ISprite interface or subclass Sprite directly.

C#
public interface ISprite : IAnimateable
{
   /// <summary>
   /// Gets or sets where the sprite is located
   /// </summary>
   Point Location { get; set; }
   
   /// <summary>
   /// Gets or sets how transparent the sprite is. 
   /// 0.0 is completely transparent, 1.0 is completely opaque.
   /// </summary>
   float Opacity { get; set; }
   
   /// <summary>
   /// Gets or sets the scaling that is applied to the extent of the sprite.
   /// The location of the sprite is not scaled.
   /// </summary>
   float Scale { get; set; }
   
   /// <summary>
   /// Gets or sets the size of the sprite
   /// </summary>
   Size Size { get; set; }
   
   /// <summary>
   /// Gets or sets the angle in degrees of the sprite.
   /// 0 means no angle, 90 means right edge lifted vertical.
   /// </summary>
   float Spin { get; set; }
   
   /// <summary>
   /// Gets or sets the bounds of the sprite. This is boundary within which
   /// the sprite will be drawn.
   /// </summary>
   Rectangle Bounds { get; set; }
   
   /// <summary>
   /// Gets the outer bounds of this sprite, which is normally the
   /// bounds of the control that is hosting the story board.
   /// Nothing outside of this rectangle will be drawn.
   /// </summary>
   Rectangle OuterBounds { get; }
   
   /// <summary>
   /// Gets or sets the reference rectangle in relation to which
   /// the sprite will be drawn. This is normal the ClientArea of
   /// the control that is hosting the story board, though it
   /// could be a subarea of that control (e.g. a particular 
   /// cell within a ListView).
   /// </summary>
   /// <remarks>This value is controlled by ReferenceBoundsLocator property.</remarks>
   Rectangle ReferenceBounds { get; set; }
   
   /// <summary>
   /// Gets or sets the locator that will calculate the reference rectangle 
   /// for the sprite.
   /// </summary>
   IRectangleLocator ReferenceBoundsLocator { get; set; }
   
   /// <summary>
   /// Gets or sets the point at which this sprite will always be placed.
   /// </summary>
   /// <remarks>
   /// Most sprites play with their location as part of their animation.
   /// But other just want to stay in the same place. 
   /// Do not set this if you use Move or Goto effects on the sprite.
   /// </remarks>
   IPointLocator FixedLocation { get; set; }
   
   /// <summary>
   /// Gets or sets the bounds at which this sprite will always be placed.
   /// </summary>
   /// <remarks>See remarks on FixedLocation</remarks>
   IRectangleLocator FixedBounds { get; set; }
   
   /// <summary>
   /// Draw the sprite in its current state
   /// </summary>
   /// <param name="g"></param>
   void Draw(Graphics g);
   
   /// <summary>
   /// Add an Effect to this sprite. This effect will run at the beginning of
   /// the sprite and will have 0 duration.
   /// </summary>
   /// <param name="effect">The effect to be applied to the sprite</param>
   void Add(IEffect effect);
   
   /// <summary>
   /// Add an Effect to this sprite. This effect will commences startTicks
   /// after the sprite begins and will have 0 duration
   /// </summary>
   /// <param name="startTick">When will the effect begins?</param>
   /// <param name="effect">What effect will be applied?</param>
   void Add(long startTick, IEffect effect);
   
   /// <summary>
   /// The main entry point for adding effects to Sprites.
   /// </summary>
   /// <param name="startTick">When will the effect begin?</param>
   /// <param name="duration">For how long will it last?</param>
   /// <param name="effect">What effect will be applied?</param>
   void Add(long startTick, long duration, IEffect effect);
}

Look carefully at the existing sprites to see how they should be implemented. Pay special attention to the role of co-ordinate transformations to handle Location and Rotation properties.

Effects

Effects are the movers and shakers of the Sparkle library. They push Sprites around, moving them here or there, making them visible or invisible, spinning them around. Any time you want a Sprite to change, you need an Effect.

Effects are given to a Sprite, and told when they should start and how long they will run for:

C#
this.imageSprite.Add(100, 250, new FadeEffect(0.0f, 0.8f));

This says, "100 milliseconds after imageSprite starts in the animation, this FadeEffect should, during 250 milliseconds, fade the sprite from hidden (0.0 opacity) to 80% visible (0.8 opacity)."

Many Effects work by "tweening" - they are given an initial value and a target value, and as the effect progresses, the effect gradually change a property on their Sprite from the initial value to the end value. In the above example, the FadeEffect's initial value is 0.0 and its end value is 0.8. As the animation progresses, the FadeEffect would gradually change the Opacity property of its Sprite from 0.0 to 0.8. So, 100 milliseconds after the sprite starts, the imageSprite will be hidden; after 225 milliseconds, it will be 40% visible; after 350 milliseconds, it will be 80% visible, and then the effect will stop.

Effects Factory

Effects factory contains static methods to create many commonly used effects.

  • C#
    Move(Corner to)

    Move the sprite from it's current location to a corner of the animation.

  • C#
    Move(Corner from, Corner to)

    Move the sprite from one corner of the animation to another. This has a zillion variations which allow different ways of saying where to start and where to end.

  • C#
    Goto(Corner to)

    Go to (as in Monopoly) the given corner without any transition.

  • C#
    Fade(float from, float to)

    Change the Opacity of the sprite from the start to the end value, effectively fading it in or out.

  • C#
    Rotate(float from, float to)

    Change the Spin of the sprite from the start to the end value (both in degrees).

  • C#
    Scale(float from, float to)

    Change the Scale of the sprite, effectively making it bigger or smaller.

  • C#
    Bounds(IRectangleLocator locator)

    Change the Bounds of the sprite.

  • C#
    Walk(IRectangleLocator locator)

    This is the first interesting effect. This changes the location of the sprite so that it "walks" around the perimeter of the given rectangle. This has several flavours saying which exact point of the sprite will be walked, which direction the walk should take, and where the walking should start.

  • C#
    Blink(int repetitions)

    Another interesting effect. This changes the Opacity of the sprite so that it blinks a number of times. There are a couple of variations that allow the characteristics of the "blink" to be changed: how long it takes to fade in, stay visible, fade out, stay invisible.

  • C#
    Repeater(int repetitions, IEffect effect)

    This applies the given Effect several times to the Sprite.

Custom Effects

Of course, it's again expected that applications would want to make their ways to change Sprites. You might want a move effect that bounces a sprite along an arc, or that does a checker board transition between two images. To do your own stuff, you need to implement the IEffect interface.

C#
public interface IEffect
{
    /// <summary>
    /// Gets or set the sprite to which the effect will be applied
    /// </summary>
    ISprite Sprite { get; set; }

    /// <summary>
    /// Signal that this effect is about to applied to its sprite for the first time
    /// </summary>
    void Start();

    /// <summary>
    /// Apply this effect to the underlying sprite
    /// </summary>
    /// <param name="fractionDone">How far through the total effect are we?
    /// This will always in the range 0.0 .. 1.0.</param>
    void Apply(float fractionDone);

    /// <summary>
    /// The effect has completed
    /// </summary>
    void Stop();

    /// <summary>
    /// Reset the effect AND the sprite to its condition before the effect was applied.
    /// </summary>
    void Reset();
}

Effects need to know what Sprite they are changing, and to know when they start and stop. A sequence diagram would look something like this:

  1. Start()
  2. Apply() [called 0 or more times, with any value 0.0..1.0]
  3. Stop()
  4. Reset() [called 0 or once]

The only interesting bit is the Apply() method. This is where Effect do their actual work. Notice that the Effect is given a "fraction done" value, not a click count (or something similar). Effects cannot rely on Apply() being called in any particular order: first Apply() could have fractionDone=0.1, the next time it could be 0.9 and then 0.5.

Also note that Reset() must revert the state of the Effect and the Sprite to their original conditions before the Effect was Start()'ed. This means that in the Start() method, effects normally store any state they are going to change, and then in Reset() that state is put back.

Locators

In some ways, locators are the most difficult concept to grasp. If you can get this concept, everything else normally falls into place.

A Locator is a point or a rectangle that can calculate itself whenever needed. A plain Point is fixed, but a PointLocator can be different every time it is called. By using a Locator, "how" a point is calculated can be replaced at runtime to use any strategy it likes.

For example, the MoveEffect changes the Location of a Sprite. It could be coded to move a Sprite to the TopLeft of an Animation:

C#
this.Sprite.Location = this.Animation.Bounds.Location;

This is nice and obvious solution, but not very flexible. If we then wanted to move the sprite to the centre of the Animation, we'd have to write a separate line of code, and then give some way to choose which line to execute. And another line of code for ever other possible location we could want.

But with Locators, the MoveEffect simply says:

C#
this.Sprite.Location = this.Locator.GetPoint();

By using this extra layer of abstraction, the intelligence of calculating the "where" is placed into a separate object, and becomes reusable from there.

Standard Locators

Locators is a factory that has static methods to produce many common locators. You can of course create the locators directly -- these are just a convenience.

  • C#
    IPointLocator At(int, int)

    Create a PointLocator for a fixed point.

  • C#
    IPointLocator SpriteAligned(Corner corner)

    Create a PointLocator which is where a Sprite must be moved to so that the given Corner is located at the corresponding corner of the Animation. So, Locators.SpriteAligned(Corner.BottomRight) calculates where a sprite must be moved to so that its BottomRight corner is at the BottomRight corner of the Animation.

  • C#
    IPointLocator SpriteAligned(Corner corner, Point offset)

    Same as above, but the point is offset by the given fixed amount.

  • C#
    IPointLocator SpriteAligned(Corner corner, float proportionX, float proportionY)

    Create a PointLocator which is where a Sprite must be moved to so that the given Corner is located at a point proportional across and down the bounds the Animation. So, Locators.SpriteAligned(Corner.BottomRight, 0.6f, 07.7) calculates where a sprite must be moved to so that its BottomRight corner is 60% across the Animation and 70% down.

  • C#
    IPointLocator SpriteBoundsPoint(Corner corner)

    Create a PointLocator which calculates the given corner of the sprite's bounds.

  • C#
    IPointLocator SpriteBoundsPoint(float proportionX, float proportionY)

    Create a PointLocator which calculates a given proportion across and down the sprite's bounds.

  • C#
    IRectangleLocator At(int, int, int, int)

    Create a RectangleLocator for a fixed rectangle.

  • C#
    IRectangleLocator AnimationBounds()

    Create a RectangleLocator for the bounds of the animation.

  • C#
    IRectangleLocator AnimationBounds(int x, int y)

    Create a RectangleLocator for the bounds of the animation inset by the given amount.

  • C#
    IRectangleLocator SpriteBounds()

    Create a RectangleLocator for the bounds of the sprite.

  • C#
    IRectangleLocator SpriteBounds(int x, int y)

    Create a RectangleLocator for the bounds of the sprite inset by the given amount.

AnimationAdaptor

The AnimationAdaptor mentioned above gives an example of linking the Animation.Redraw event to the Animation.Draw() method. When the Animation triggers a Redraw event, the AnimationAdaptor invalidates its Control. This causes the Control to repaint itself, and when the control triggers the Paint event, the AnimationAdaptor renders the animation through the Draw() method. Voilá! Any Control (with a Paint event) can show an animation.

An AnimationAdaptor can be used on Panels, Buttons, Labels, PictureBoxes, UserControls, numeric spin controls, and (oddly enough) DataGridView. It cannot be used on another control because they don't usefully support the Paint event. There is nothing that can be done about RichTextBox and the others, but if you want to put animations on a ListView or a TreeView, have a look at the ObjectListView project, which does support these animations.

One of the beautiful things about the design of Sparkle is that it can be trivially used within another framework. To use it within WPF, all that is needed is an equivalent of the AnimationAdaptor, which listens for redraw events on the Animation, and then do something to make the animation redraw itself.

Performance

The Sparkle library performs fairly well when used in accordance with its design goals. Animating dozens of sprites with dozens of effects has a minimal impact on performance. On my laptop, 20 or so sprites with a variety of effects uses only about 2-3% of the CPU. The limiting factor is not the animation but the redrawing of the underlying control. Currently, the whole control is redrawn every frame. For simple controls, like Buttons or UserControls, this is not a problem, but for complicated control, like DataGridView, this redrawing quickly becomes taxing.

In a later version, I'll optimize to invalidate only the smallest possible area of the control.

I haven't tried using Sparkle with thousands of sprites. That really wasn't its purpose.

Status and Stability

Sparkle is a new library. It has worked well for me, but I'm sure there are bugs in it. Please report them and I will fix them.

The interfaces and major classes are stable, but not yet fixed (unchangeable). It's possible that I will add a few more properties to the ISprite interface (I think it needs Skew).

Conclusion

Go ahead. Dazzle your users with the glittery, snazzy animations.

Now when I look at Google's apps, I no longer suffer from envy. I still don't have their work conditions or share price, but when a little star spins and fades, I think, "Hey, I can do that too!"

To Do

  • Handle updates more efficiently (calculate damaged region and only redraw that portion).
  • Add fancier effects, like glow and mirror.
  • Add composites (ways of putting effects onto multiple sprites at same time).
  • Allow different interpolation calculations. All interpolations are currently linear. We should allow for example, acceleration, so that falling shapes in the demo accelerate as they fall.
  • Allow animations to reverse.

Library License

This library is released under the LGPL v2.0.

History

v0.8 - 30 March 2010

  • Initial public release

License

This article, along with any associated source code and files, is licensed under The GNU Lesser General Public License (LGPLv3)


Written By
Team Leader
Australia Australia
Phillip has been playing with computers since the Apple II was the hottest home computer available. He learned the fine art of C programming and Guru meditation on the Amiga.

C# and Python are his languages of choice. Smalltalk is his mentor for simplicity and beauty. C++ is to programming what drills are to visits to the dentist.

He worked for longer than he cares to remember as Lead Programmer and System Architect of the Objective document management system. (www.objective.com)

He has lived for 10 years in northern Mozambique, teaching in villages.

He has developed high volume trading software, low volume FX trading software, and is currently working for Atlassian on HipChat.

Comments and Discussions

 
QuestionHow to fix BrightIdeasSoftware.AnimatedDecoration Namespace issue ? Pin
martinvarghese17-Sep-15 7:55
martinvarghese17-Sep-15 7:55 
BugFYI (Tiny bug) Pin
drifter10111-Dec-14 9:57
drifter10111-Dec-14 9:57 
QuestionSparkLibrary in WPF? Pin
Adel Khalil2-Aug-14 21:33
Adel Khalil2-Aug-14 21:33 
AnswerRe: SparkLibrary in WPF? Pin
Phillip Piper4-Aug-14 14:04
Phillip Piper4-Aug-14 14:04 
GeneralMy vote of 5 Pin
Manoj Kumar Choubey20-Feb-12 0:00
professionalManoj Kumar Choubey20-Feb-12 0:00 
QuestionHow can this be used to paint on child controls? Pin
Geo2424-Aug-10 7:47
Geo2424-Aug-10 7:47 
GeneralVB.Net Pin
Alan Bates29-Jun-10 1:03
Alan Bates29-Jun-10 1:03 
GeneralRe: VB.Net Pin
PCoffey7-Mar-13 5:26
PCoffey7-Mar-13 5:26 
GeneralAwesome! Pin
Diamonddrake13-Apr-10 15:03
Diamonddrake13-Apr-10 15:03 
Generalamazing!! Pin
Druuler7-Apr-10 2:04
Druuler7-Apr-10 2:04 
GeneralExcellent Pin
Nicholas Butler6-Apr-10 9:08
sitebuilderNicholas Butler6-Apr-10 9:08 
General100 from me Pin
Som Shekhar6-Apr-10 6:50
Som Shekhar6-Apr-10 6:50 
GeneralCool !!! Pin
Armando Airo'6-Apr-10 5:12
Armando Airo'6-Apr-10 5:12 
QuestionScreenshot? PinPopular
Johnny J.6-Apr-10 4:01
professionalJohnny J.6-Apr-10 4:01 
AnswerRe: Screenshot? Pin
Dav26-Apr-10 4:47
Dav26-Apr-10 4:47 
GeneralRe: Screenshot? PinPopular
PCoffey6-Apr-10 5:36
PCoffey6-Apr-10 5:36 
GeneralRe: Screenshot? Pin
jeffb426-Apr-10 8:09
jeffb426-Apr-10 8:09 
GeneralRe: Screenshot? Pin
Anthony Daly6-Apr-10 9:15
Anthony Daly6-Apr-10 9:15 
AnswerRe: Screenshot? Pin
Phillip Piper6-Apr-10 15:05
Phillip Piper6-Apr-10 15:05 
AnswerRe: Screenshot? Pin
Phillip Piper16-Apr-10 19:13
Phillip Piper16-Apr-10 19:13 

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.