|
|||||||||||||||||||||
|
|||||||||||||||||||||
|
Announcements
Chapters
Services
Feature Zones
|
IntroductionThis article is about an animation application created over the course of a couple months. The application is called Fluid Geometry, and it is being given to the world via www.fluidgeometry.com. It was written in C# using Visual Studio .NET 2003. Fluid Geometry provides an intuitive way for users to create animated “scenes” that can be saved and then viewed at a later time. A scene consists of one or more “paths” that move around in the scene. A path contains one or more “movers” that navigate the path. There are several types of paths that you can use to construct a scene; such as a sine wave, ellipse, infinity symbol, and others. Each path has a variety of settings that you can configure to customize its behavior. You can combine paths together in ways that create some truly stunning animated patterns. For more information regarding what Fluid Geometry is all about, feel free to visit www.fluidgeometry.com. Fluid Geometry is broken into three physical pieces:
How this Article is StructuredConsidering the size and modest complexity of the application, it would have been counterproductive to write about every aspect of it (not to mention tedious). With that in mind, I decided that the best way to structure this article would be as a collection of programming tasks. I chose what I considered to be the most interesting or challenging aspects of the application, and focused on them. The article contains multiple topics, each of which contains one or more sections. Before getting into the details of how certain things work, we will start off with a brief overview of the animation library’s architecture. That will provide the context needed to understand why things were implemented the way that they were. I hope that this task-based format will prove beneficial to both the pragmatist seeking a quick answer, and the more thorough “cover-to-cover” reader alike. Below you will find a collection of links to the different programming tasks explored in this article:
Before getting into the remainder of the article, I suggest that you download and play with Fluid Geometry first. You can download the files from the links at the top of this article or from www.fluidgeometry.com. The rest of this article will be much more meaningful if you have seen the application in action. Plus, it’s fun! When using the Fluid Geometry configuration application for the first time, be sure to read the helpful descriptions in the StatusBar as you move the mouse cursor over different controls on the Form. ArchitectureThe classes that constitute the animation library are straightforward. Below you will find a rudimentary class diagram illustrating the key classes and their relationships:
As you can see above, there are very few core types involved with the animation logic. Here is a brief rundown of the different types referenced in the diagram above. SceneThe ISceneHost
MovementManager
PathCollectionThis class is a type-safe wrapper of an PathThe MoverCollectionThis class is a type-safe wrapper of an Mover
I am not going to discuss the design of the configuration application because it is basically just a monolithic Using Reflection to Create an Extensible User InterfaceAfter reading this topic you will know:
One of the most challenging aspects of the entire undertaking was creating a user interface which both makes it simple for users to create scenes, and does not need to be updated when a new type of path is created. A major goal was to have new
When the user selects one of the paths added to the scene, the “Path Attributes” GroupBox is populated with controls representing the various settings exposed by the selected path. Keep in mind that different types of paths expose different custom settings, so the number and types of controls to be loaded depends on the type of the selected path. The dynamically loaded controls display and update the value of their corresponding properties. Dynamically loaded The net result of this functionality is that paths do not need to be associated with a set of controls at design-time. In fact, paths know nothing about how their settings are displayed in the configuration application and the configuration application knows very little about the paths it displays the settings of. When a new The next several sections of the article discuss how this flexible user interface was implemented. Discovering Path Types at RuntimeThe Fluid Geometry configuration application does not know the different type of paths that exist until runtime. Determining the private void LoadPathTypesData()
{
// If the "Path Types" ListBox is empty,
// fill it with the names of any Path-derived
// types found in the assembly containing
// the Path type (a.k.a. FluidGeometryLib.dll).
//
if( this.lstPathTypes.Items.Count == 0 )
foreach( Type type in Assembly.GetAssembly( typeof(Path) ).GetTypes() )
if( typeof(Path).IsAssignableFrom( type ) && ! type.IsAbstract )
this.lstPathTypes.Items.Add( new PathTypeWrapper( type ) );
}
The Querying Path Types for Custom SettingsOnce the configuration Form has loaded and the “Path Types” ListBox has been populated with all of the available types of paths, the user might add a path to the scene being created. For example, the user might double click on the “Ellipse” item in the “Path Types” ListBox. In response to that double click, an Before the controls can be loaded, however, it is necessary to find out what custom settings the selected path has to offer. Not all path types have the same settings, so reflection is used again to discover the eligible properties. This is accomplished by applying the // This code is from the get method
// of the CustomSettings property in the PathTypeWrapper class.
ArrayList propsWithAttribute = new ArrayList();
foreach( Type type in this.TypeHierarchy )
{
PropertyInfo[] propInfos = type.GetProperties(
BindingFlags.DeclaredOnly |
BindingFlags.Public |
BindingFlags.Instance );
foreach( PropertyInfo pi in propInfos )
{
object[] objArr =
pi.GetCustomAttributes( typeof(PathSettingAttribute),
true );
if( objArr.Length > 0 )
{
PathSettingAttribute pathSetting =
objArr[0] as PathSettingAttribute;
propsWithAttribute.Add( new
PathSettingInfo( pi, pathSetting ) );
}
}
}
// Helper property in PathTypeWrapper class.
private Type[] TypeHierarchy
{
get
{
ArrayList typeHierarchy = new ArrayList();
Type t = this.Type;
while( t != null )
{
typeHierarchy.Add( t );
t = t.BaseType;
}
typeHierarchy.Reverse();
return typeHierarchy.ToArray( typeof(Type) ) as Type[];
}
}
// An example of a custom setting
// on the SineWavePath class using the PathSetting attribute.
// The arguments to the PathSetting constructor
// are the friendly name of the setting, the
// minimum value, and the maximum value.
[PathSetting("Number of Wave Peaks", 0, 40)]
public int NumPeaks
{
get
{
//...
}
set
{
//...
}
}
As you can see in the code above, the Creating Controls Dynamically Based on Path-Specific SettingsOnce the custom settings of the selected path have been determined, it is possible to create controls that can be used to display and modify the values of those settings. Some of the controls in the “Path Attributes” GroupBox exist at design-time; such as the The type of control created for a custom setting is based on the data type of the setting (i.e. the type of the property). Boolean properties are given
Below is the logic which creates and loads the path-specific controls: private void LoadControlsForPreviewPath()
{
PathTypeWrapper wrapper =
this.lstPathsInScene.SelectedItem as PathTypeWrapper;
Path path = this.PreviewPath;
if( wrapper == null || path == null )
return;
// First remove the existing controls that
// were loaded for the previous Preview Path.
this.RemoveDynamicallyLoadedControls();
const int GAP = 6;
Control ctrl = null;
int tabIdx = this.pnlPermanentPathSettingsControlHost.TabIndex;
Point pt = new Point(
this.pnlPermanentPathSettingsControlHost.Location.X,
this.pnlPermanentPathSettingsControlHost.Location.Y +
this.pnlPermanentPathSettingsControlHost.Height + GAP );
foreach( PathSettingInfo settingInfo in wrapper.CustomSettings )
{
if( settingInfo.PropertyInfo.PropertyType == typeof(bool) )
{
#region Create CheckBox
CheckBox chkBox = new CheckBox();
chkBox.FlatStyle = FlatStyle.System;
chkBox.Checked = (bool)settingInfo.GetValue( path );
chkBox.CheckedChanged +=
new EventHandler( OnCheckBoxCheckedChanged );
chkBox.Text = settingInfo.PathSetting.FriendlyName;
ctrl = chkBox;
#endregion // Create CheckBox
}
else if( settingInfo.PropertyInfo.PropertyType.IsEnum )
{
#region Create ComboBox
ComboBox combo = new ComboBox();
combo.DropDownStyle = ComboBoxStyle.DropDownList;
string[] names =
Enum.GetNames( settingInfo.PropertyInfo.PropertyType );
string currentValue =
settingInfo.GetValue( path ).ToString();
foreach( string name in names )
{
int idx = combo.Items.Add( name );
if( name == currentValue )
combo.SelectedIndex = idx;
}
combo.SelectedIndexChanged +=
new EventHandler( OnComboBoxSelectedIndexChanged );
ctrl = combo;
#endregion // Create ComboBox
}
else
{
#region Create TextBox
TextBox txt = new TextBox();
txt.Text = settingInfo.GetValue( path ).ToString();
txt.Enter += new EventHandler( this.OnTextBoxEnter );
txt.KeyPress +=
new KeyPressEventHandler( this.OnTextBoxKeyPress );
txt.TextChanged +=
new EventHandler( this.OnTextBoxTextChanged );
txt.Leave += new EventHandler( this.OnTextBoxLeave );
ctrl = txt;
#endregion // Create TextBox
}
if( ctrl is CheckBox == false )
{
#region Add Label To Panel
Label lbl = new Label();
this.pnlPathAttributesControlHost.Controls.Add( lbl );
lbl.Text = settingInfo.PathSetting.FriendlyName;
lbl.FlatStyle = FlatStyle.System;
lbl.AutoSize = true;
lbl.Location = pt;
pt.Offset( 0, lbl.Height + 1 );
#endregion // Add Label To Panel
}
#region Add Control To Panel
this.pnlPathAttributesControlHost.Controls.Add( ctrl );
ctrl.Tag = settingInfo;
ctrl.Width = this.txtNumMoversOnPath.Width;
ctrl.Location = pt;
ctrl.TabIndex = tabIdx++;
this.AttachMouseEnterAndLeaveHandlers( ctrl );
pt.Offset( 0, ctrl.Height + GAP );
#endregion // Add Control To Panel
}
}
The code above loops over the array of One last thing to take note of is that the Validating Input Values Against Arbitrary ConstraintsAs mentioned previously, path settings with numeric data types are displayed in These circumstances present some interesting problems:
The code listed in the previous section demonstrates how the dynamically loaded TextBox txt = new TextBox();
txt.Enter += new EventHandler( this.OnTextBoxEnter );
txt.KeyPress += new KeyPressEventHandler( this.OnTextBoxKeyPress );
txt.TextChanged += new EventHandler( this.OnTextBoxTextChanged );
txt.Leave += new EventHandler( this.OnTextBoxLeave );
When a private void OnTextBoxEnter(object sender, System.EventArgs e)
{
this.lastValidValueInTextBox = (sender as TextBox).Text;
}
Below is the handler for the private void OnTextBoxKeyPress( object sender, KeyPressEventArgs e )
{
// IsControl returns true for the Backspace key,
// which is an allowable key.
if( ! Char.IsDigit( e.KeyChar ) && e.KeyChar != '.'
&& ! Char.IsControl( e.KeyChar ))
e.Handled = true;
}
The code above prevents a Once the text in a private void UpdateSettingInPreviewPath( TextBox textBox )
{
// This is necessary because otherwise it would be impossible
// to clear out the textbox or start a decimal number with a
// decimal point (as opposed to starting it with "0.").
//
string text = textBox.Text.Trim();
if( text == "" || text == "." )
return;
PathSettingInfo settingInfo = textBox.Tag as PathSettingInfo;
ValueType val = this.ConvertPathSettingInputValue( text, settingInfo );
if( val != null )
{
this.isPathSettingValueValid = true;
settingInfo.SetValue( this.PreviewPath, val );
this.lastValidValueInTextBox = textBox.Text;
this.errorProvider.SetError( textBox, String.Empty );
}
else
{
this.isPathSettingValueValid = false;
if( settingInfo.PathSetting.HasMaxValue &&
settingInfo.PathSetting.HasMinValue )
{
string errorMsg = String.Format(
"This value must be between {0} and {1}.",
settingInfo.PathSetting.MinValue.ToString(),
settingInfo.PathSetting.MaxValue.ToString() );
this.errorProvider.SetError( textBox, errorMsg );
}
}
}
private ValueType ConvertPathSettingInputValue(string inputText,
PathSettingInfo settingInfo)
{
bool isValid = true;
ValueType val = null;
try
{
Type propType = settingInfo.PropertyInfo.PropertyType;
val = Convert.ChangeType( inputText, propType ) as ValueType;
IComparable comparableVal = val as IComparable;
if( settingInfo.PathSetting.HasMinValue )
{
ValueType min = Convert.ChangeType(
settingInfo.PathSetting.MinValue, propType ) as ValueType;
isValid = comparableVal.CompareTo( min as IComparable ) >= 0;
}
if( isValid && settingInfo.PathSetting.HasMaxValue )
{
ValueType max = Convert.ChangeType(
settingInfo.PathSetting.MaxValue, propType ) as ValueType;
isValid = comparableVal.CompareTo( max as IComparable ) <= 0;
}
}
catch
{
isValid = false;
}
return isValid ? val : null;
}
The code above tests if the input value is within the acceptable range. If it is, the setting on the selected path (a.k.a. the “preview path”) is updated and the new valid value is cached. If the new value is invalid, the The [PathSetting("Number of Wave Peaks", 0, 40)]
public int NumPeaks
{
get
{
//...
}
set
{
//...
}
}
These values are exposed as public properties on the Lastly, when the user moves the input focus to another control, the private void OnTextBoxLeave(object sender, System.EventArgs e)
{
TextBox textBox = sender as TextBox;
if( ! this.isPathSettingValueValid || textBox.Text.Length == 0 )
{
textBox.Text = this.lastValidValueInTextBox;
}
}
Once an input value is successfully validated, the path setting is updated with the new value. The code which performs the update is called from the Updating Path Settings with ReflectionOnce the user has specified a new value for a path setting, it is necessary to set the corresponding property to that new value. Since the configuration application is not aware of the path types and custom path settings at compile-time, it is necessary to use reflection to set the property. All of the relevant reflection code is in the foreach( PropertyInfo pi in propInfos )
{
object[] objArr =
pi.GetCustomAttributes( typeof(PathSettingAttribute), true );
if( objArr.Length > 0)
{
PathSettingAttribute pathSetting = objArr[0] as PathSettingAttribute;
propsWithAttribute.Add( new PathSettingInfo( pi, pathSetting ) );
}
}
public void SetValue( Path path, object value )
{
this.PropertyInfo.SetValue( path, value, new object[0] );
}
The Implementing this indirect approach to path configuration was worth the extra effort. The configuration application does not need to know about the different path types that exist at compile-time. With the help of reflection and custom attributes, it is possible to decouple the configuration logic from the animation library. You can create and modify Serialization/DeserializationAfter reading this topic you will know:
The configuration application would be rather frustrating if it did not allow the user to save a scene and view it at a later time. The Fluid Geometry Screensaver would not be able to display user-defined scenes if there was no way to save a scene to disk. Basically, scenes need to be serializable. For those of you new to the concepts of serialization and deserialization, hopefully the following explanations will make these ideas clear: Serialization is akin to taking a snapshot of an object graph at runtime. The snapshot is written in a compact format (typically binary or SOAP) which describes the values of every object in the graph. That snapshot is put into a stream, at which point you can do anything with it as you please. Often the contents of the stream are flushed into a file and then saved to disk. Keep in mind that the object graph which was serialized continues to be used after the serialization process occurs. Serialization simply records the state of an object graph at a given point in time. Deserialization is the process of taking the snapshot created during serialization and converting it back into “live objects” – i.e. real objects in memory that your program can use. The next few sections of this article delve into the code required for scenes to be serialized and deserialized. Path classes require custom serialization and deserialization, which will be covered in the last two sections. The scene serialization/deserialization services are available via static methods of the public static bool Save( Scene scene, Stream stream )
{
bool success = false;
try
{
bool active = scene.IsActive;
if( active )
scene.Pause();
new BinaryFormatter().Serialize( stream, scene );
if( active )
scene.Resume();
success = true;
}
catch( Exception ex )
{
Debug.Fail( "Failed to save Scene.Reason: " + ex.Message );
}
return success;
}
public static Scene Load( Stream stream, bool pathsAreVisible )
{
if( stream == null || ! stream.CanRead )
throw new ArgumentException( "The stream passed" +
" to Scene.Load is invalid." );
Scene scene = null;
try
{
scene = new BinaryFormatter().Deserialize( stream ) as Scene;
foreach( Path path in scene.Paths )
path.Visible = pathsAreVisible;
}
finally
{
if( stream != null )
stream.Close();
}
return scene;
}
The Using the NonSerialized AttributeSome types can be serialized, others cannot. Some objects should be serialized, others should not. Determining which types can be serialized is easy since only types with the
You can inform the .NET serializers that certain fields in a type should not be serialized by applying the [NonSerialized]
private ISceneHost host;
The Custom Path Serialization Using ReflectionOne major drawback of using the default serialization services provided by .NET serialization classes ( This powerful version affinity proves quite problematic for Fortunately, the wise architects of the .NET serialization infrastructure provided ample support for this type of scenario. A class can indicate to the .NET serializers that it will take care of serializing and deserializing itself, by having both the It is important to note that if you go this route you are responsible for both the serialization and deserialization of the class instances, you cannot just do one or the other. The serialization of an object is performed in the Below is the public void GetObjectData( SerializationInfo info,
StreamingContext context )
{
// Cache the number of Movers on the Path.
// This information is used during deserialization.
//
this.totalMoversOnPath = this.Movers.Count;
// The serialization process is broken
// into two phases because the reflection API
// does not allow you to access
// the private fields of a type's base class.
// Since we do not want all of the Path class'
// fields to be protected for
// this reason, we need to serialize
// out the Path partial first. This allows
// the Path class to have private fields
// that get serialized. If a new type of
// path is created which indirectly
// derives from Path, this logic will need to be
// modified so that it loops over every
// partial, not just Path and most derived type.
//
this.GetObjectDataHelper( info, typeof(Path) );
this.GetObjectDataHelper( info, this.GetType() );
}
private void GetObjectDataHelper( SerializationInfo info, Type typeToProcess )
{
BindingFlags flags = this.GetSerializationBindingFlags( typeToProcess );
FieldInfo[] fields = typeToProcess.GetFields( flags );
// Save the value of every non-constant field which does not
// have the NonSerialized attribute applied to it.
//
foreach( FieldInfo field in fields )
if( ! field.IsLiteral && ! field.IsNotSerialized )
info.AddValue( field.Name, field.GetValue( this ) );
}
private BindingFlags GetSerializationBindingFlags( Type typeToProcess )
{
BindingFlags flags = BindingFlags.NonPublic | BindingFlags.Instance;
if( typeToProcess != typeof(Path) )
flags |= BindingFlags.DeclaredOnly;
return flags;
}
The Each class in the type hierarchy of the path is inspected individually because the reflection API does not allow you to get or set the value of private fields in a class’ base type. If the serialization was not performed in this manner, the There is one limitation with the current implementation of this custom serialization logic. A One final point to note is that the serialization logic seen above is in the abstract [Serializable]
public class SpiralPath : Path, ISerializable
{
// Other members omitted...
void ISerializable.GetObjectData( SerializationInfo info,
StreamingContext context )
{
base.GetObjectData( info, context );
}
protected SpiralPath( SerializationInfo info,
StreamingContext context )
: base( info, context )
{
}
}
Now that we’ve seen how a scene and its paths are serialized, it’s time to turn our attention to the other side of the coin. The next section examines the code required to deserialize a scene, including the complicated task of performing custom deserialization for paths. Deserializing Complex Object GraphsDeserializing scenes would be very simple if In the previous section it was mentioned that implementing the The deserialization constructor of the protected Path( SerializationInfo info, StreamingContext context )
{
// Refer to GetObjectData for an explanation
// of why this is performed in multiple steps.
//
this.DeserializationCtorHelper( info, typeof(Path) );
this.DeserializationCtorHelper( info, this.GetType() );
}
private void DeserializationCtorHelper( SerializationInfo info,
Type typeToProcess )
{
BindingFlags flags = this.GetSerializationBindingFlags( typeToProcess );
foreach( SerializationEntry entry in info )
{
FieldInfo field = typeToProcess.GetField( entry.Name, flags );
if( field != null )
field.SetValue( this, entry.Value );
}
}
private BindingFlags GetSerializationBindingFlags( Type typeToProcess )
{
BindingFlags flags = BindingFlags.NonPublic | BindingFlags.Instance;
if( typeToProcess != typeof(Path) )
flags |= BindingFlags.DeclaredOnly;
return flags;
}
The deserialization logic is extremely similar to the serialization logic seen in the previous section. Each type in the Another complicating factor is that the movers on a path are not serialized, which was discussed in the first section of this topic. As a result of that optimization it is necessary to add the appropriate number of movers back into the void IDeserializationCallback.OnDeserialization( object sender )
{
// Since this.movers is not serialized, we need to add the Movers
// back into the Path once deserialization is complete.Not serializing
// the Movers reduces the memory footprint of a serialized Scene.
//
for( int i = 0; i < this.totalMoversOnPath; ++i )
this.Movers.Add( new Mover( this ) );
}
I chose to use this interface, as opposed to putting the above code in the deserialization constructor, because this task involves more than just restoring the state of simple member variables. The documentation for This topic explained when and why it is worthwhile to implement custom serialization for a class. It is not necessary to use reflection to perform these tasks, but doing so prevents you from needing to update the serialization logic when you modify the set of fields belonging to the class. Also, using this reflection-based approach could prove beneficial in situations where the class which supports custom serialization is being maintained by less experienced developers who might not be aware of the custom serialization logic. Custom Double Buffered Rendering Using GDI+After reading this topic you will know:
Many WinForms applications have no need to perform much or any custom drawing. Typically all of the rendering involved in an application’s user interface is taken care of by controls on Forms. This holds true especially for business applications, where the primary focus of the user interface is on the creation, modification, transmission, analysis and deletion of business data. Applications of that nature usually do not need to draw many dashed lines, blue rectangles, etc. In some situations, however, it becomes necessary for an application to perform custom rendering. If the custom rendering performed by an application needs to be updated often, double buffering can be used to reduce flickering. For those of you new to the concept of double buffering, I hope that the following explanation will clarify it for you: Double Buffering is a drawing technique used to help reduce the amount of flickering caused by frequent paint operations. Rather than an application drawing directly to the screen, it first draws to an in-memory buffer. Once the entire image has been rendered, the buffer is then copied to the screen in one operation. Since drawing directly to the screen is very slow, it is better to do as much drawing as possible in memory and then just copy that buffered image directly to the screen. This is particularly beneficial in situations where the image being rendered is a composite of many overlapping visual elements, because drawing one layer of an image at a time to the screen increases the total number of drawing operations performed by the screen. The more drawing operations performed by the screen, the more flickering can be observed. It is important to note that double buffering does not necessarily make the rendering process any faster, actually it can make it slower. The impetus, or raison d'être, of double buffered rendering is simply to reduce the flickering caused by frequent paint operations. The .NET Framework provides support for double buffering via the Fluid Geometry absolutely requires double buffering, but it cannot rely on the standard double buffering provided by the .NET Framework. It needs to be double buffered because scenes repaint themselves hundreds of times per second. It cannot use the standard .NET double buffering because a scene has no idea who will be hosting it, as the next topic covers in great detail. Since a scene does not know who will be hosting it, there is no way for it to be sure that its host supports double buffering. This requires the animation library to implement custom double buffering which, as a side benefit, frees a scene host from ever needing to explicitly support double buffering. Actually, if a scene host were to support double buffering it would need to jump through hoops to ensure that the scene renders into the correct The following diagram illustrates the basic idea of how double buffering works in the animation library:
A scene has an off-screen buffer into which the movers in the scene render. After all of the paths have finished rendering their movers, the buffer is copied to a Below is the logic in the private Bitmap RenderingSurface
{
get
{
if( this.renderingSurface == null )
this.renderingSurface = new Bitmap(
Math.Max( this.Size.Width, 1 ),
Math.Max( this.Size.Height, 1 ) );
return this.renderingSurface;
}
}
internal Graphics OffscreenGraphics
{
get
{
if( this.grfxRenderingSurface == null )
this.grfxRenderingSurface =
Graphics.FromImage( this.RenderingSurface );
return this.grfxRenderingSurface;
}
}
private void OnHostResize(object sender, EventArgs e)
{
bool wasActive = this.IsActive;
this.Stop( true );
if( this.renderingSurface != null )
{
this.renderingSurface.Dispose();
this.renderingSurface = null;
}
if( this.grfxRenderingSurface != null )
{
this.grfxRenderingSurface.Dispose();
this.grfxRenderingSurface = null;
}
foreach( Path path in this.Paths )
path.Reinitialize();
if( wasActive )
this.Start();
}
The code seen above creates a buffer (a When it is time to copy the buffer to the screen, the following method in the internal void UpdateView()
{
if( this.host == null || this.isDisposed )
return;
try
{
this.host.SceneGraphics.DrawImageUnscaled( this.RenderingSurface,
Point.Empty );
}
catch( Exception ex )
{
// In case something goes wrong, stop the Scene.
//
this.Stop( false );
}
}
The buffer is copied to the screen via the The protected virtual void Draw( bool visible )
{
if( ! this.Scene.IsActive || ! this.IsInView )
return;
if( visible )
this.Scene.OffscreenGraphics.DrawImage( this.Path.MoverImage,
this.Bounds );
else
this.Scene.OffscreenGraphics.FillRectangle( this.Scene.BackgroundBrush,
this.EraseBounds );
}
The private void OnTimerTick(object sender, EventArgs e)
{
++this.tickCount;
if( this.ShouldCreateMotion )
{
PathCollection visiblePaths = this.Scene.VisiblePaths;
foreach( Path path in visiblePaths )
if( this.PathNeedsToMove( path ) )
path.Erase();
foreach( Path path in visiblePaths )
if( this.PathNeedsToMove( path ) )
path.Move();
// Draw every visible path whether it was moved or not
// because the erasing performed by moved Paths will
// leave "holes" in paths lower in the Z order.
foreach( Path path in visiblePaths )
path.Draw();
this.Scene.UpdateView();
}
}
This method responds to the This topic discussed the concept of double buffering and when it should be used. For most applications that would benefit from the use of double buffered rendering, it is sufficient to use the .NET Framework’s built-in double buffering support. Considering the graphics-intensive nature of Fluid Geometry and the fact that the rendering logic is exposed as a hosted service, it is necessary for Fluid Geometry to use custom double buffering. The next topic examines why and how the Decoupling a Scene from its Host (ISceneHost)After reading this topic you will know:
The Fluid Geometry animation library is able to display scenes through any object, provided that the object implements the Below is the declaration of the /// <summary>
/// ISceneHost provides services needed by a Scene.
/// </summary>
public interface ISceneHost
{
/// <summary>
/// Returns the Graphics object to be used
/// when rendering into the scene. Do NOT dispose of this object.
/// </summary>
System.Drawing.Graphics SceneGraphics { get; }
/// <summary>
/// Gets the size of the visual area available for the scene.
/// </summary>
System.Drawing.Size Size { get; }
/// <summary>
/// The color of the scene's background.
/// </summary>
System.Drawing.Color BackColor { get; }
/// <summary>
/// Fires when the scene needs to be repainted.
/// </summary>
event System.Windows.Forms.PaintEventHandler Paint;
/// <summary>
/// Fires when the size of the scene changes.
/// </summary>
event EventHandler Resize;
}
The two most important members of the interface are The following is the Color ISceneHost.BackColor
{
get { return this.pnlSceneHost.BackColor; }
}
Graphics ISceneHost.SceneGraphics
{
get
{
if( this.grfxSceneHost == null )
this.grfxSceneHost = this.pnlSceneHost.CreateGraphics();
return this.grfxSceneHost;
}
}
Size ISceneHost.Size
{
get { return this.pnlSceneHost.Size; }
}
It is important to note that if a When a public void AttachHost( ISceneHost host )
{
if( host == null )
throw new ArgumentNullException( "host",
"The Scene's host cannot be null." );
if( this.host != null )
this.DetachHost();
this.host = host;
this.host.Paint +=
new System.Windows.Forms.PaintEventHandler( this.RepaintScene );
this.host.Resize += new EventHandler( this.OnHostResize );
}
If the scene’s host needs to be removed or changed, the public ISceneHost DetachHost()
{
if( this.host == null )
return null;
ISceneHost sceneHost = this.host;
this.host.Paint -=
new System.Windows.Forms.PaintEventHandler( this.RepaintScene );
this.host.Resize -= new EventHandler( this.OnHostResize );
this.host = null;
return sceneHost;
}
This topic discussed the advantage of decoupling a scene from its host. If the Fluid Geometry animation library was not intended to be reusable then it would not have been necessary to generalize the relationship between a scene and the surface upon which it renders. The ConclusionThe topics covered in this article constitute only a handful of the challenges overcome while implementing Fluid Geometry. I hope that the topics examined prove useful and/or interesting for you. If you decide to explore the source code and come up with a question, comment, suggestion, etc., please feel free to post your thoughts to the message board associated with this article. I’ll do my best to provide a timely response, if necessary. Thank you for taking the time to read my article, I hope it was worthwhile. J | ||||||||||||||||||||