using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
namespace DirectoryTest
{
class Program
{
static void Main(string[] args)
{
try
{
Console.WriteLine("Creating test directory");
CreateTestDirectory();
}
catch (Exception ex)
{
DisplayError(ex);
WaitForKey();
return;
}
Console.WriteLine("Directory created");
WaitForKey();
DeleteTestDirectory();
//SafeDeleteTestDirectory();
Console.WriteLine("Directory deleted");
WaitForKey();
}
static void DeleteTestDirectory()
{
try
{
Console.WriteLine("Deleting test directory");
Directory.Delete("c:\\Temp\\Test1", true);
}
catch (Exception ex)
{
DisplayError(ex);
WaitForKey();
return;
}
}
static void SafeDeleteTestDirectory()
{
try
{
Console.WriteLine("Deleting test directory with retry");
Directory.Delete("c:\\Temp\\Test1", true);
}
catch (Exception ex)
{
DisplayError(ex);
Thread.Sleep(1000);
Console.WriteLine("Failed! Trying again...");
try
{
Directory.Delete("c:\\Temp\\Test1", true);
}
catch (Exception ex2)
{
DisplayError(ex2);
WaitForKey();
return;
}
}
}
static char WaitForKey()
{
Console.WriteLine("Press any key...");
ConsoleKeyInfo cki = Console.ReadKey();
return cki.KeyChar;
}
static void CreateTestDirectory()
{
Directory.CreateDirectory("c:\\Temp\\Test1");
Directory.CreateDirectory("c:\\Temp\\Test1\\Test2");
using (StreamWriter sw = new StreamWriter("c:\\Temp\\Test1\\TestFile1.txt", false))
{
sw.WriteLine("This is a line of text");
sw.Close();
}
using (StreamWriter sw = new StreamWriter("c:\\Temp\\Test1\\Test2\\TestFile2.txt", false))
{
sw.WriteLine("This is a line of text");
sw.Close();
}
}
static void DisplayError(Exception ex)
{
Exception current = ex;
while (null != current)
{
Console.WriteLine("Error: " + ex.Message);
Console.WriteLine("Stack: " + ex.StackTrace);
Console.WriteLine(Environment.NewLine);
current = current.InnerException;
}
}
}
}
Code
Source code listings
Playing With Blocks Listing 3 – DragHandleConnector.cs
/******************************************************************************
* DragHandleConnector.cs
*
* This module implements the code behind for the DragHandleConnector class.
*
* Date: 2/2009
*
* Copyright (c) 2009, Mark Betz
*
* All rights reserved.
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* * Neither the name of the Author nor the names of contributors may be
* used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY MARK BETZ ''AS IS'' AND ANY EXPRESS
* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PART-
* ICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL MARK BETZ BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
* OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
* OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
using System;
using System.Collections.Generic;
using System.Windows;
namespace DrawControls
{
/// <summary>
/// Delegate type for position coercion callbacks used to
/// adjust DragHandle position properties for consumption
/// by clients
/// </summary>
/// <param name="p">A <see cref="T:System.Windows.Point">Point</see></param>
/// <returns></returns>
public delegate Point CoercePointCallback(Point p);
/// <summary>
/// Enumerates the position axes that should serve as update data
/// for a given client
/// </summary>
public enum SourceAxes
{
/// <summary>
/// Specifies that the X axis will be reported. The
/// implied update type is double.
/// </summary>
X,
/// <summary>
/// Specifies that the Y axis will be reported. The
/// implied update type is double.
/// </summary>
Y,
/// <summary>
/// Specifies that both axes will be reported. The
/// implied update type is Point.
/// </summary>
Both
};
/// <summary>
/// DragHandleTarget encapsulates a single update point from a DragHandle
/// to a client object property. Multiple targets can be defined to send
/// values to multiple properties. This is primarily used to send Point
/// data to a double X/Y pair, as in the case of the Line shape.
/// </summary>
public class DragHandleTarget
{
/// <summary>
/// Constructs a new DragHandle target for the specified property,
/// owner object, and source
/// </summary>
/// <param name="p">A DependencyProperty to be updated when the
/// DragHandle position changes</param>
/// <param name="o">The DependencyObject that owns the property to be updated</param>
/// <param name="s">A value from the <see cref="DrawControls.SourceAxes">SourceAxes</see>
/// enum specifying which axis should be used to update the property.</param>
public DragHandleTarget( DependencyProperty p, DependencyObject o,
SourceAxes s )
{
Property = p;
Owner = o;
Axes = s;
}
/// <summary>
/// The property which this target should update when the DragHandle
/// position changes. It's type must be compatible with the type
/// implied by the Axes property.
/// </summary>
public DependencyProperty Property { get; set; }
/// <summary>
/// The DependencyObject which owns the property to be updated
/// </summary>
public DependencyObject Owner { get; set; }
/// <summary>
/// A value from the <see cref="DrawControls.SourceAxes">SourceAxes</see>
/// enum specifying which axis of handle movement should be used to
/// update the target property.
/// </summary>
public SourceAxes Axes { get; set; }
}
/// <summary>
/// A DragHandleConnection is a list of DragHandleTargets and a callback
/// used to examine and coerce position values before they are reported
/// to a client.
/// </summary>
public class DragHandleConnection
{
public void AddTarget(DependencyProperty p, DependencyObject o,
SourceAxes s)
{
_targets.Add( new DragHandleTarget(p, o, s) );
}
/// <summary>
/// The CoercePointCallback will be invoked when the DragHandle
/// position changes, before the position is sent to clients.
/// </summary>
public CoercePointCallback CoercePoint;
/// <summary>
/// Gets the list of DragHandleTargets associated with this connection
/// </summary>
public List<DragHandleTarget> Targets
{
get { return _targets; }
}
/// <summary>
/// Implements the Targets property
/// </summary>
List<DragHandleTarget> _targets = new List<DragHandleTarget>();
}
/// <summary>
/// DragHandleConnector is the overall container for a set of
/// connections attached to a specific DragHandler
/// </summary>
public class DragHandleConnector
{
/// <summary>
/// Create a DragHandleConnector attached to the specified
/// DragHandle
/// </summary>
/// <param name="dh">An instance of DragHandle</param>
public DragHandleConnector( DragHandle dh )
{
SetHandle( dh );
}
/// <summary>
/// Disconnects the connection from a DragHandle by unwiring its
/// event handler. This should when the connection is no
/// longer needed.
/// </summary>
public void Disconnect()
{
if (null != _dh)
_dh.Drag -= new EventHandler<DragHandleEventArgs>(DragHandle_Drag);
}
/// <summary>
/// Gets and sets the DragHandle associated with this connector
/// </summary>
public DragHandle Handle
{
get { return _dh; }
set { SetHandle(value); }
}
/// <summary>
/// Gets the list of DragHandleConnections for this connector
/// </summary>
public List<DragHandleConnection> Connections
{
get { return _connections; }
}
/// <summary>
/// Used internally to connect a drag handle, while making sure that
/// the previous handle, if any, is disconnected.
/// </summary>
/// <param name="dh"></param>
private void SetHandle( DragHandle dh )
{
Disconnect();
_dh = dh;
_dh.Drag += new EventHandler<DragHandleEventArgs>(DragHandle_Drag);
}
/// <summary>
/// Handles the Drag event for connected DragHandles
/// </summary>
/// <param name="sender">a DragHandle</param>
/// <param name="e">event arguments</param>
private void DragHandle_Drag( object sender, DragHandleEventArgs e )
{
for( int i = 0; i < _connections.Count; i++ )
{
Point p;
DragHandleConnection d = _connections[i];
if (null != d.CoercePoint)
p = d.CoercePoint(e.Position);
else
p = e.Position;
for ( int j = 0; j < d.Targets.Count; j++ )
{
DragHandleTarget t = d.Targets[j];
switch ( t.Axes )
{
case SourceAxes.Both:
t.Owner.SetValue(t.Property, p);
break;
case SourceAxes.X:
t.Owner.SetValue(t.Property, p.X);
break;
case SourceAxes.Y:
t.Owner.SetValue(t.Property, p.Y);
break;
}
}
}
}
private DragHandle _dh;
private List<DragHandleConnection> _connections =
new List<DragHandleConnection>();
}
}
Playing With Blocks Listing 2 – DragHandle.xaml.cs
/******************************************************************************
* DragHandle.xaml.cs
*
* This module implements the code behind for the DragHandle class.
*
* Date: 2/2009
*
* Copyright (c) 2009, Mark Betz
*
* All rights reserved.
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* * Neither the name of the Author nor the names of contributors may be
* used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY MARK BETZ ''AS IS'' AND ANY EXPRESS
* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PART-
* ICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL MARK BETZ BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
* OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
* OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Shapes;
namespace DrawControls
{
/// <summary>
/// DragConstraint enumerates possible constraints on the motion of a
/// DragHandle. Motion constraints are relative to the HandleOrigin
/// of the handle. For more information
/// see the <see cref="P:DrawControls.DragHandle.HandleOrigin">
/// HandleOrigin property documentation.</see>.
/// </summary>
public enum DragConstraint
{
/// <summary>
/// Specifies that no movement constraint will be placed on the Drag
/// Handle. This is the default.
/// </summary>
None,
/// <summary>
/// Specifies that the DragHandle will be able to move on the east-
/// west horizontal axis to both sides of its
/// <see cref="P:DrawControls.DragHandle.HandleOrigin">origin;</see>
/// (+/-x, y).
/// </summary>
EW,
/// <summary>
/// Specifies that the DragHandle will be able to move on the north-
/// south vertical axis above and below its
/// <see cref="P:DrawControls.DragHandle.HandleOrigin">origin;</see>
/// (x, +/-y).
/// </summary>
NS,
/// <summary>
/// Specifies that the DragHandle will be able to move on the north-
/// east-southwest diagonal axis, to both sides of its
/// <see cref="P:DrawControls.DragHandle.HandleOrigin">origin;</see>
/// (-x, +y) or (+x, -y).
/// </summary>
NESW,
/// <summary>
/// Specifies that the DragHandle will be able to move on the south-
/// east-northwest diagonal axis, to both sides of its
/// <see cref="P:DrawControls.DragHandle.HandleOrigin">origin;</see>
/// (-x, -y) or (+x, +y).
/// </summary>
SENW,
/// <summary>
/// Specifies that the DragHandle will be able to move from its
/// <see cref="P:DrawControls.DragHandle.HandleOrigin">origin</see>
/// to the northeast; (+x, -y).
/// </summary>
NE,
/// <summary>
/// Specifies that the DragHandle will be able to move from its
/// <see cref="P:DrawControls.DragHandle.HandleOrigin">origin</see>
/// to the northwest; (-x, -y).
/// </summary>
NW,
/// <summary>
/// Specifies that the DragHandle will be able to move from its
/// <see cref="P:DrawControls.DragHandle.HandleOrigin">origin</see>
/// to the southeast; (+x, +y).
/// </summary>
SE,
/// <summary>
/// Specifies that the DragHandle will be able to move from its
/// <see cref="P:DrawControls.DragHandle.HandleOrigin">origin</see>
/// to the southwest; (-x, +y).
/// </summary>
SW,
/// <summary>
/// Specifies that the DragHandle will be able to move from its
/// <see cref="P:DrawControls.DragHandle.HandleOrigin">origin</see>
/// to the east (+x, y).
/// </summary>
E,
/// <summary>
/// Specifies that the DragHandle will be able to move from its
/// <see cref="P:DrawControls.DragHandle.HandleOrigin">origin</see>
/// to the north (x, -y).
/// </summary>
N,
/// <summary>
/// Specifies that the DragHandle will be able to move from its
/// <see cref="P:DrawControls.DragHandle.HandleOrigin">origin</see>
/// to the south (x, +y).
/// </summary>
S,
/// <summary>
/// Specifies that the DragHandle will be able to move from its
/// <see cref="P:DrawControls.DragHandle.HandleOrigin">origin</see>
/// to the west (-x, y).
/// </summary>
W
};
/// <summary>
/// Event arguments for the DragHandle class events
/// </summary>
public class DragHandleEventArgs : EventArgs
{
/// <summary>
/// The position of the handle when the event was raised. The
/// passed coordinates are relative to the
/// <see cref="P:DrawControls.DragHandle.ReferenceElement"> coordinate
/// space, if one was set. Otherwise they are relative to the parent
/// UIElement of the handle.</see>
/// </summary>
public Point Position { get; set; }
/// <summary>
/// The position of the handle's
/// <see cref="P:DrawControls.DragHandle.HandleOrigin">HandleOrigin</see>
/// at the time the event was raised.
/// </summary>
public Point Origin { get; set; }
}
/// <summary>
/// DragHandle represents a control that can contain a shape, and that
/// can be moved around the screen with a mouse. Its movement can be
/// constrained in various directions, and it supports several events
/// to provide clients with notifications when it is moved or clicked.
/// </summary>
public partial class DragHandle : UserControl
{
/// <summary>
/// Creates a DragHandle initially positioned at 0,0 with its
/// <see cref="P:DrawControls.DragHandle.HandleOrigin">origin</see>
/// at 0,0.
/// </summary>
public DragHandle()
{
InitializeComponent();
InitHandle();
}
/// <summary>
/// Creates a DragHandle initially positioned at <c>position</c> with its
/// <see cref="P:DrawControls.DragHandle.HandleOrigin">origin</see>
/// at <c>position</c>.
/// </summary>
public DragHandle(Point position)
: base()
{
InitializeComponent();
HandlePosition = position;
InitHandle();
}
/// <summary>
/// Causes the DragHandle to relocate to its
/// <see cref="P:DrawControls.DragHandle.HandleOrigin">origin</see>.
/// </summary>
public void SnapToOrigin()
{
HandlePosition = HandleOrigin;
}
/// <summary>
/// The Click event is raised when the MouseLeftButtonDown event occurs
/// on the DragHandle's shape. The handle responds by capturing the
/// mouse and raising this event.
/// </summary>
public event EventHandler<DragHandleEventArgs> Click;
/// <summary>
/// The BeginDrag event is raised when the mouse is moved with the left
/// button down, within the confines of the handle's shape, and the
/// handle is not already dragging.
/// </summary>
public event EventHandler<DragHandleEventArgs> BeginDrag;
/// <summary>
/// The Drag event is raised when the mouse is moved with the left
/// button down, and the handle is in the dragging state (BeginDrag
/// has been raised).
/// </summary>
public event EventHandler<DragHandleEventArgs> Drag;
/// <summary>
/// The EndDrag event is raised when the MouseLeftButtonUp event
/// occurs with the handle in the dragging state. The handle releases
/// mouse capture and raises this event.
/// </summary>
public event EventHandler<DragHandleEventArgs> EndDrag;
/// <summary>
/// Backing DP for the HandleCursor property.
/// </summary>
public static readonly DependencyProperty HandleCursorProperty =
DependencyProperty.Register("HandleCursor", typeof(Cursor),
typeof(DragHandle), new PropertyMetadata(Cursors.Arrow,
new PropertyChangedCallback(OnHandleCursorChange)));
/// <summary>
/// Gets and sets the <see cref="T:System.Windows.Input.Cursor">
/// mosue cursor that will be displayed when the mouse is hovered
/// over the handle. Defaults to Cursors.Arrow.</see>
/// </summary>
public Cursor HandleCursor
{
get { return (Cursor)GetValue(HandleCursorProperty); }
set { SetValue(HandleCursorProperty, value); }
}
/// <summary>
/// Handles the property changed event for the HandleCursor DP.
/// </summary>
private static void OnHandleCursorChange(DependencyObject source,
DependencyPropertyChangedEventArgs e)
{
((DragHandle)source).UpdateHandleCursor((Cursor)e.NewValue);
}
/// <summary>
/// Called from the property changed event handler for the HandleCursor
/// DP. If the handle has a shape it sets the cursor.
/// </summary>
/// <param name="newCursor">A Cursor</param>
private void UpdateHandleCursor(Cursor newCursor)
{
if (null != _shape)
_shape.Cursor = newCursor;
}
/// <summary>
/// Backing DP for the HandleDragConstraint property
/// </summary>
public static readonly DependencyProperty HandleDragConstraintProperty =
DependencyProperty.Register("HandleDragConstraint", typeof(DragConstraint),
typeof(DragHandle), new PropertyMetadata(DragConstraint.None,
new PropertyChangedCallback(OnHandleDragConstraintChange) ));
/// <summary>
/// Gets and sets the <see cref="T:DrawControls.DragConstraint">DragConstraint</see>
/// that determines where this handle is allowed to move. Using the compass rose
/// as a visualization tool, with north at the top of the screen, handles can be
/// constrained to move EW, NS, NESW, SENW, N, E, S, W, NE, SE, SW, NW.
/// </summary>
public DragConstraint HandleDragConstraint
{
get { return (DragConstraint)GetValue(HandleDragConstraintProperty); }
set { SetValue(HandleDragConstraintProperty, value); }
}
/// <summary>
/// Handles the property changed event for the HandleDragConstraint DP
/// </summary>
/// <param name="source"></param>
/// <param name="e"></param>
private static void OnHandleDragConstraintChange( DependencyObject source,
DependencyPropertyChangedEventArgs e )
{
((DragHandle)source).UpdateHandleDragConstraint( (DragConstraint)e.NewValue );
}
/// <summary>
/// Called from the HandleDragConstraint property changed event handler to
/// update the HandleDragConstraint
/// </summary>
/// <param name="d">DragConstraint to be applied</param>
private void UpdateHandleDragConstraint( DragConstraint d )
{
SetDragConstraint( d );
}
/// <summary>
/// Backing DP for the HandleWidth property
/// </summary>
public static readonly DependencyProperty HandleWidthProperty =
DependencyProperty.Register("HandleWidth", typeof(double),
typeof(DragHandle), new PropertyMetadata(2.0,
new PropertyChangedCallback(OnHandleWidthChange)));
/// <summary>
/// Gets and sets the width of the handle in pixels. Setting this
/// property will set the width on the handle's shape, as well as
/// the backing canvas.
/// </summary>
public double HandleWidth
{
get { return (double)GetValue(HandleWidthProperty); }
set { SetValue(HandleWidthProperty, value); }
}
/// <summary>
/// Handles property change events for the HandleWidth DP.
/// </summary>
private static void OnHandleWidthChange(DependencyObject source,
DependencyPropertyChangedEventArgs e)
{
((DragHandle)source).UpdateHandleWidth((double)e.NewValue);
}
/// <summary>
/// Called from the property change handler for the HandleWidth
/// property.
/// </summary>
/// <param name="newWidth">A double representing the new width</param>
private void UpdateHandleWidth(double newWidth)
{
this.Width = newWidth;
if (null != _shape)
_shape.Width = newWidth;
}
/// <summary>
/// Backing DP for the HandleHeight property
/// </summary>
public static readonly DependencyProperty HandleHeightProperty =
DependencyProperty.Register("HandleHeight", typeof(double),
typeof(DragHandle), new PropertyMetadata(2.0,
new PropertyChangedCallback(OnHandleHeightChange)));
/// <summary>
/// Gets and sets the height of the handle in pixels. Setting this
/// property will set the height on the handle's shape, as well as
/// the backing canvas.
/// </summary>
public double HandleHeight
{
get { return (double)GetValue(HandleHeightProperty); }
set { SetValue(HandleHeightProperty, value); }
}
/// <summary>
/// Handles property change events for the HandleWidth DP.
/// </summary>
private static void OnHandleHeightChange(DependencyObject source,
DependencyPropertyChangedEventArgs e)
{
((DragHandle)source).UpdateHandleHeight((double)e.NewValue);
}
/// <summary>
/// Called from the property change handler for the HandleHeight
/// property.
/// </summary>
/// <param name="newWidth">A double representing the new height</param>
private void UpdateHandleHeight(double newHeight)
{
this.Height = newHeight;
if (null != _shape)
_shape.Height = newHeight;
}
/// <summary>
/// Backing DP for the HandleOffsetX property
/// </summary>
public static readonly DependencyProperty HandleOffsetXProperty =
DependencyProperty.Register("HandleOffsetX", typeof(double),
typeof(DragHandle), new PropertyMetadata(0.0,
new PropertyChangedCallback(OnHandleOffsetXChange)));
/// <summary>
/// Gets and sets the offset, in pixels, from the handle's reported position
/// to its top-left corner. When the handle is drawn its x coordinate
/// will be adjusted by adding HandleOffsetX.
/// </summary>
public double HandleOffsetX
{
get { return (double)GetValue(HandleOffsetXProperty); }
set { SetValue(HandleOffsetXProperty, value); }
}
/// <summary>
/// Handles property change events for the HandleOffsetX DP
/// </summary>
private static void OnHandleOffsetXChange( DependencyObject source,
DependencyPropertyChangedEventArgs e )
{
((DragHandle)source).UpdateHandleOffsetX( (double)e.NewValue );
}
/// <summary>
/// Called from the property change event handler for HandleOffsetX
/// DP. Updates the control's position on the parent canvas.
/// </summary>
/// <param name="newOffset">A double containing the offset</param>
private void UpdateHandleOffsetX( double newOffset )
{
Canvas.SetLeft( this, HandlePosition.X + HandleOffsetX );
}
/// <summary>
/// Backing DP for the HandleOffsetY property
/// </summary>
public static readonly DependencyProperty HandleOffsetYProperty =
DependencyProperty.Register("HandleOffsetY", typeof(double),
typeof(DragHandle), new PropertyMetadata(0.0,
new PropertyChangedCallback(OnHandleOffsetYChange)));
/// <summary>
/// Gets and sets the offset from the handle's reported position
/// to its top-left corner. When the handle is drawn its y coordinate
/// will be adjusted by adding HandleOffsetY.
/// </summary>
public double HandleOffsetY
{
get { return (double)GetValue(HandleOffsetYProperty); }
set { SetValue(HandleOffsetYProperty, value); }
}
/// <summary>
/// Handles property change events for the HandleOffsetY DP
/// </summary>
private static void OnHandleOffsetYChange(DependencyObject source,
DependencyPropertyChangedEventArgs e)
{
((DragHandle)source).UpdateHandleOffsetY((double)e.NewValue);
}
/// <summary>
/// Called from the property change event handler for HandleOffsetX
/// DP. Updates the control's position on the parent canvas.
/// </summary>
/// <param name="newOffset">A double containing the offset</param>
private void UpdateHandleOffsetY(double newOffset)
{
Canvas.SetTop(this, HandlePosition.Y + HandleOffsetY);
}
/// <summary>
/// Backing DP for the HandlePosition property
/// </summary>
public static readonly DependencyProperty HandlePositionProperty =
DependencyProperty.Register("HandlePosition", typeof(Point),
typeof(DragHandle), new PropertyMetadata(new Point(0, 0),
new PropertyChangedCallback(OnHandlePositionChange)));
/// <summary>
/// Gets and sets the position of the handle. Unless a
/// <see cref="T:DrawControls.DragHandle.ReferenceElement">reference element</see>
/// has been set, this position will be relative to the Silverlight plug-in's
/// overall screen area.
/// </summary>
public Point HandlePosition
{
get { return (Point)GetValue(HandlePositionProperty); }
set { SetValue(HandlePositionProperty, value); }
}
/// <summary>
/// Handles property change events for the HandlePosition DP
/// </summary>
private static void OnHandlePositionChange(DependencyObject source,
DependencyPropertyChangedEventArgs e)
{
((DragHandle)source).UpdateHandlePosition((Point)e.NewValue);
}
/// <summary>
/// Called from the property change event handler for the HandlePosition
/// DP/
/// </summary>
/// <param name="newPos">A Point containing the new position</param>
private void UpdateHandlePosition(Point newPos)
{
Canvas.SetLeft( this, newPos.X + HandleOffsetX );
Canvas.SetTop(this, newPos.Y + HandleOffsetY );
}
/// <summary>
/// Backing DP for the HandleOrigin property
/// </summary>
public static readonly DependencyProperty HandleOriginProperty =
DependencyProperty.Register("HandleOrigin", typeof(Point),
typeof(DragHandle), new PropertyMetadata(new Point(0, 0), null));
/// <summary>
/// Gets and sets the <see cref="T:System.Windows.Point">Point</see>
/// that serves as the handle's origin, or point of reference for
/// movement calculations.
/// </summary>
public Point HandleOrigin
{
get { return (Point)GetValue(HandleOriginProperty); }
set { SetValue(HandleOriginProperty, value); }
}
/// <summary>
/// Backing DP for the HandleShape property
/// </summary>
public static readonly DependencyProperty HandleShapeProperty =
DependencyProperty.Register("HandleShape", typeof(Shape),
typeof(DragHandle), new PropertyMetadata(null,
new PropertyChangedCallback(OnHandleShapeChange)));
/// <summary>
/// Gets and sets the shape used to display the handle. By default
/// the handle creates a rectangle and displays it on an unfilled
/// canvas. Setting this property to a different shape will replace
/// the rectangle with that shape.
/// </summary>
public Shape HandleShape
{
get { return (Shape)GetValue(HandleShapeProperty); }
set { SetValue(HandleShapeProperty, value); }
}
/// <summary>
/// Handles property change events for the HandleShape DP
/// </summary>
/// <param name="source"></param>
/// <param name="e"></param>
private static void OnHandleShapeChange(DependencyObject source,
DependencyPropertyChangedEventArgs e)
{
((DragHandle)source).UpdateHandleShape((Shape)e.NewValue);
}
/// <summary>
/// Called from the property change event handler for the HandleShape
/// DP. Calls UnwireShape() to disconnect event handles from the current
/// shape, if any, then sets up the new shape's properties, adds it to
/// the handle canvas, and calls WireShape() to connect its event
/// handlers.
/// </summary>
/// <param name="newShape">The new shape for the handle</param>
public void UpdateHandleShape(Shape newShape)
{
if (null != _shape)
{
UnwireShape();
HandleCanvas.Children.Remove(_shape);
}
_shape = newShape;
_shape.Width = HandleWidth;
_shape.Height = HandleHeight;
_shape.Fill = HandleFill;
_shape.Stroke = HandleStroke;
_shape.Cursor = HandleCursor;
HandleCanvas.Children.Add(_shape);
WireShape();
}
/// <summary>
/// The backing DP for the HandleFill property
/// </summary>
public static readonly DependencyProperty HandleFillProperty =
DependencyProperty.Register("HandleFill", typeof(Brush),
typeof(DragHandle), new PropertyMetadata(null,
new PropertyChangedCallback(OnHandleFillChange)));
/// <summary>
/// Gets and sets the <see cref="T:System.Windows.Brush">brush</see>
/// used to fill the handle shape. Defaults to null, which will result
/// in an unfilled shape.
/// </summary>
public Brush HandleFill
{
get { return (Brush)GetValue(HandleFillProperty); }
set { SetValue(HandleFillProperty, value); }
}
/// <summary>
/// Property change event handler for the HandleFill DP
/// </summary>
private static void OnHandleFillChange(DependencyObject source,
DependencyPropertyChangedEventArgs e)
{
((DragHandle)source).UpdateHandleFill((Brush)e.NewValue);
}
/// <summary>
/// Called from the HandleFill property change event handler to
/// update the current shape's fill.
/// </summary>
/// <param name="newFill">A Brush which will be used to fill the handle shape</param>
private void UpdateHandleFill(Brush newFill)
{
if (null != _shape)
_shape.Fill = newFill;
}
/// <summary>
/// Backing DP for the HandleStroke property
/// </summary>
public static readonly DependencyProperty HandleStrokeProperty =
DependencyProperty.Register("HandleStroke", typeof(Brush),
typeof(DragHandle), new PropertyMetadata(null,
new PropertyChangedCallback(OnHandleStrokeChange)));
/// <summary>
/// Gets and sets the <see cref="T:System.Windows.Brush">brush</see>
/// used to outline the handle shape. Defaults to null, which results
/// in an unoutlined shape. If <see cref="T:DrawControls.DragHandle.HandleFill">
/// HandleFill</see> is also null the handle will be invisible.
/// </summary>
public Brush HandleStroke
{
get { return (Brush)GetValue(HandleStrokeProperty); }
set { SetValue(HandleStrokeProperty, value); }
}
/// <summary>
/// Property change event handler for the HandleStroke DP
/// </summary>
private static void OnHandleStrokeChange(DependencyObject source,
DependencyPropertyChangedEventArgs e)
{
((DragHandle)source).UpdateHandleStroke((Brush)e.NewValue);
}
/// <summary>
/// Called from the HandleStroke property change event handler to
/// update the stroke brush for the current shape, if any.
/// </summary>
/// <param name="newStroke">A Brush which will be used to outline the handle shape</param>
private void UpdateHandleStroke(Brush newStroke)
{
if (null != _shape)
_shape.Stroke = newStroke;
}
/// <summary>
/// Backing DP for the HandleStrokeThickness property
/// </summary>
public static readonly DependencyProperty HandleStrokeThicknessProperty =
DependencyProperty.Register("HandleStrokeThickness", typeof(double),
typeof(DragHandle), new PropertyMetadata(1.0,
new PropertyChangedCallback(OnHandleStrokeThicknessChange)));
/// <summary>
/// Gets and sets the thickness of the stroke used to outline the handle
/// shape. Defaults to a thickness of 1.0.
/// </summary>
public double HandleStrokeThickness
{
get { return (double)GetValue(HandleStrokeThicknessProperty); }
set { SetValue(HandleStrokeThicknessProperty, value); }
}
/// <summary>
/// Property change event handler for the HandleStrokeThickness DP.
/// </summary>
private static void OnHandleStrokeThicknessChange(DependencyObject source,
DependencyPropertyChangedEventArgs e)
{
((DragHandle)source).UpdateHandleStrokeThickness((double)e.NewValue);
}
/// <summary>
/// Called from the HandleStrokeThickness property change event handler
/// to update the stroke thickness for the current shape, if any.
/// </summary>
/// <param name="newThickness">A double containing the thickness</param>
private void UpdateHandleStrokeThickness(double newThickness)
{
if (null != _shape)
_shape.StrokeThickness = newThickness;
}
/// <summary>
/// Backing DP for the ReferenceElement property
/// </summary>
public static readonly DependencyProperty ReferenceElementProperty =
DependencyProperty.Register("ReferenceElement", typeof(UIElement),
typeof(DragHandle), new PropertyMetadata(null, null));
/// <summary>
/// Gets and sets the UIElement whose coordinate space will be used as
/// the frame or reference for reported handle movements. If this
/// property is left at the default null value then reported movement
/// will be relative to the overall Silverlight plug-in onscreen
/// area.
/// </summary>
public UIElement ReferenceElement
{
get { return (UIElement)GetValue(ReferenceElementProperty); }
set { SetValue(ReferenceElementProperty, value); }
}
/// <summary>
/// Backing DP for the HandleMinX property
/// </summary>
public static readonly DependencyProperty HandleMinXProperty =
DependencyProperty.Register("HandleMinX", typeof(double),
typeof(DragHandle), new PropertyMetadata(Double.MinValue, null));
/// <summary>
/// Gets and sets the minimum x coordinate to which the DragHandle will
/// be allowed to move, regardless of other movement constraints.
/// </summary>
public double HandleMinX
{
get { return (double)GetValue(HandleMinXProperty); }
set { SetValue(HandleMinXProperty, value); }
}
/// <summary>
/// Backing DP for the HandleMaxX property
/// </summary>
public static readonly DependencyProperty HandleMaxXProperty =
DependencyProperty.Register("HandleMaxX", typeof(double),
typeof(DragHandle), new PropertyMetadata(Double.MaxValue, null));
/// <summary>
/// Gets and sets the maximum x coordinate to which the DragHandle will
/// be allowed to move, regardless of other movement constraints.
/// </summary>
public double HandleMaxX
{
get { return (double)GetValue(HandleMaxXProperty); }
set { SetValue(HandleMaxXProperty, value); }
}
/// <summary>
/// Backing DP for the HandleMinY property
/// </summary>
public static readonly DependencyProperty HandleMinYProperty =
DependencyProperty.Register("HandleMinY", typeof(double),
typeof(DragHandle), new PropertyMetadata(Double.MinValue, null));
/// <summary>
/// Gets and sets the minimum y coordinate to which the DragHandle will
/// be allowed to move, regardless of other movement constraints.
/// </summary>
public double HandleMinY
{
get { return (double)GetValue(HandleMinYProperty); }
set { SetValue(HandleMinYProperty, value); }
}
/// <summary>
/// Backing DP for the HandleMaxY property
/// </summary>
public static readonly DependencyProperty HandleMaxYProperty =
DependencyProperty.Register("HandleMaxY", typeof(double),
typeof(DragHandle), new PropertyMetadata(Double.MaxValue, null));
/// <summary>
/// Gets and sets the maximum y coordinate to which the DragHandle will
/// be allowed to move, regardless of other movement constraints.
/// </summary>
public double HandleMaxY
{
get { return (double)GetValue(HandleMaxYProperty); }
set { SetValue(HandleMaxYProperty, value); }
}
/// <summary>
/// Called to raise the Click event on the DragHandle
/// </summary>
/// <param name="e">DragHandleEventArgs</param>
protected void OnClick(DragHandleEventArgs e)
{
if ( null != Click )
Click( this, e );
}
/// <summary>
/// Called to raise the BeginDrag event on the DragHandle
/// </summary>
/// <param name="e">DragHandleEventArgs</param>
protected void OnBeginDrag(DragHandleEventArgs e)
{
if (null != BeginDrag)
BeginDrag(this, e);
}
/// <summary>
/// Called to raise the Drag event on the DragHandle
/// </summary>
/// <param name="e">DragHandleEventArgs</param>
protected void OnDrag(DragHandleEventArgs e)
{
if (null != Drag)
Drag(this, e);
}
/// <summary>
/// Called to raise the EndDrag event on the DragHandle
/// </summary>
/// <param name="e">DragHandleEventArgs</param>
protected void OnEndDrag(DragHandleEventArgs e)
{
if (null != EndDrag)
EndDrag(this, e);
}
/// <summary>
/// Sets up the default handle with a Rectangle for a shape
/// </summary>
private void InitHandle()
{
HandleShape = new Rectangle();
HandleOrigin = HandlePosition;
}
/// <summary>
/// Called at intialization, and when the handle shape changes. Wires up
/// the shape's mouse events to handlers
/// </summary>
private void WireShape()
{
_shape.MouseLeftButtonDown += new MouseButtonEventHandler(_shape_MouseLeftButtonDown);
_shape.MouseMove += new MouseEventHandler(_shape_MouseMove);
_shape.MouseLeftButtonUp += new MouseButtonEventHandler(_shape_MouseLeftButtonUp);
}
/// <summary>
/// Called when the handle's shape changes. Disconnects event handlers from the
/// current shape.
/// </summary>
private void UnwireShape()
{
_shape.MouseLeftButtonDown -= new MouseButtonEventHandler(_shape_MouseLeftButtonDown);
_shape.MouseMove -= new MouseEventHandler(_shape_MouseMove);
_shape.MouseLeftButtonUp -= new MouseButtonEventHandler(_shape_MouseLeftButtonUp);
}
// The mouse event handlers operate according to the following rules:
//
// - If the left button is pressed on the shape the mouse is captured and
// Click is raised.
// - If the mouse is moved over the shape with the left button down and
// the shape is not dragging it goes into drag state and BeginDrag is
// raised.
// - If the mouse is moved over the shape with the left button down and
// the shape is dragging Drag is raised.
// - If the mouse left button is released the mouse capture is released and
// EndDrag is raised.
/// <summary>
/// Handles the MouseLeftButtonDown event on the handle's shape
/// </summary>
/// <param name="sender">A Shape object</param>
/// <param name="e">MouseButtonEventArgs</param>
private void _shape_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
_shape.CaptureMouse();
DragHandleEventArgs args = new DragHandleEventArgs();
args.Position = e.GetPosition(ReferenceElement);
args.Origin = HandleOrigin;
OnClick(args);
_mouseDown = true;
e.Handled = true;
}
/// <summary>
/// Handles the MouseLeftButtonUp event on the handle's shape
/// </summary>
/// <param name="sender">A Shape object</param>
/// <param name="e">MouseButtonEventArgs</param>
private void _shape_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
_shape.ReleaseMouseCapture();
if ( _dragging )
{
DragHandleEventArgs args = new DragHandleEventArgs();
args.Position = e.GetPosition(ReferenceElement);
args.Origin = HandleOrigin;
_dragging = false;
OnEndDrag(args);
}
_mouseDown = false;
e.Handled = true;
}
/// <summary>
/// Handles the MouseMove event on the handle's shape
/// </summary>
/// <param name="sender">A Shape object</param>
/// <param name="e">MouseEventArgs</param>
private void _shape_MouseMove(object sender, MouseEventArgs e)
{
if (_mouseDown)
{
Point pos = e.GetPosition(ReferenceElement);
if (RequestDragTo(ref pos))
{
HandlePosition = pos;
DragHandleEventArgs args = new DragHandleEventArgs();
args.Position = pos;
args.Origin = HandleOrigin;
if ( !_dragging )
{
_dragging = true;
OnBeginDrag( args );
}
else OnDrag(args);
}
}
}
/// <summary>
/// Called when the HandleDragConstraint property changes. This method
/// sets up the appropriate eval delegate to be called when the handle
/// moves.
/// </summary>
/// <param name="d">DragConstraint to be applied</param>
private void SetDragConstraint( DragConstraint d )
{
switch ( d )
{
case DragConstraint.None:
_evalMove = null;
break;
case DragConstraint.E:
_evalMove = new MovementEvalCallback(Eval_MoveE);
break;
case DragConstraint.W:
_evalMove = new MovementEvalCallback(Eval_MoveW);
break;
case DragConstraint.N:
_evalMove = new MovementEvalCallback(Eval_MoveN);
break;
case DragConstraint.S:
_evalMove = new MovementEvalCallback(Eval_MoveS);
break;
case DragConstraint.EW:
_evalMove = new MovementEvalCallback(Eval_MoveEW);
break;
case DragConstraint.NS:
_evalMove = new MovementEvalCallback(Eval_MoveNS);
break;
case DragConstraint.NESW:
_evalMove = new MovementEvalCallback(Eval_MoveNESW);
break;
case DragConstraint.SENW:
_evalMove = new MovementEvalCallback(Eval_MoveSENW);
break;
case DragConstraint.NE:
_evalMove = new MovementEvalCallback(Eval_MoveNE);
break;
case DragConstraint.SW:
_evalMove = new MovementEvalCallback(Eval_MoveSW);
break;
case DragConstraint.SE:
_evalMove = new MovementEvalCallback(Eval_MoveSE);
break;
case DragConstraint.NW:
_evalMove = new MovementEvalCallback(Eval_MoveSW);
break;
}
}
// The following small methods each provide movement evaluation
// for a specific DragConstraint
/// <summary>
/// Evaluate movement with DragConstraint.None applied
/// </summary>
/// <param name="dest">The point the DragHandle wants to move to</param>
/// <returns>True if the movement is permitted, otherwise false. Note that
/// even when the return value is true the values of dest.X and dest.Y
/// may have changed to constrain movement.</returns>
private bool Eval_MoveNone( ref Point dest )
{
return true;
}
/// <summary>
/// Evaluate movement with DragConstraint.E applied
/// </summary>
/// <param name="dest">The point the DragHandle wants to move to</param>
/// <returns>True if the movement is permitted, otherwise false. Note that
/// even when the return value is true the values of dest.X and dest.Y
/// may have changed to constrain movement.</returns>
private bool Eval_MoveE(ref Point dest)
{
if (dest.X >= HandleOrigin.X)
{
dest.Y = HandleOrigin.Y;
return true;
}
else return false;
}
/// <summary>
/// Evaluate movement with DragConstraint.W applied
/// </summary>
/// <param name="dest">The point the DragHandle wants to move to</param>
/// <returns>True if the movement is permitted, otherwise false. Note that
/// even when the return value is true the values of dest.X and dest.Y
/// may have changed to constrain movement.</returns>
private bool Eval_MoveW(ref Point dest)
{
if (dest.X <= HandleOrigin.X)
{
dest.Y = HandleOrigin.Y;
return true;
}
else return false;
}
/// <summary>
/// Evaluate movement with DragConstraint.N applied
/// </summary>
/// <param name="dest">The point the DragHandle wants to move to</param>
/// <returns>True if the movement is permitted, otherwise false. Note that
/// even when the return value is true the values of dest.X and dest.Y
/// may have changed to constrain movement.</returns>
private bool Eval_MoveN(ref Point dest)
{
if (dest.Y <= HandleOrigin.Y)
{
dest.X = HandleOrigin.X;
return true;
}
else return false;
}
/// <summary>
/// Evaluate movement with DragConstraint.S applied
/// </summary>
/// <param name="dest">The point the DragHandle wants to move to</param>
/// <returns>True if the movement is permitted, otherwise false. Note that
/// even when the return value is true the values of dest.X and dest.Y
/// may have changed to constrain movement.</returns>
private bool Eval_MoveS(ref Point dest)
{
if (dest.Y >= HandleOrigin.Y)
{
dest.X = HandleOrigin.X;
return true;
}
else return false;
}
/// <summary>
/// Evaluate movement with DragConstraint.EW applied
/// </summary>
/// <param name="dest">The point the DragHandle wants to move to</param>
/// <returns>True if the movement is permitted, otherwise false. Note that
/// even when the return value is true the values of dest.X and dest.Y
/// may have changed to constrain movement.</returns>
private bool Eval_MoveEW(ref Point dest)
{
dest.Y = HandleOrigin.Y;
return true;
}
/// <summary>
/// Evaluate movement with DragConstraint.NS applied
/// </summary>
/// <param name="dest">The point the DragHandle wants to move to</param>
/// <returns>True if the movement is permitted, otherwise false. Note that
/// even when the return value is true the values of dest.X and dest.Y
/// may have changed to constrain movement.</returns>
private bool Eval_MoveNS(ref Point dest)
{
dest.X = HandleOrigin.X;
return true;
}
/// <summary>
/// Evaluate movement with DragConstraint.NESW applied
/// </summary>
/// <param name="dest">The point the DragHandle wants to move to</param>
/// <returns>True if the movement is permitted, otherwise false. Note that
/// even when the return value is true the values of dest.X and dest.Y
/// may have changed to constrain movement.</returns>
private bool Eval_MoveNESW(ref Point dest)
{
dest.Y = HandleOrigin.Y - (dest.X - HandleOrigin.X);
return true;
}
/// <summary>
/// Evaluate movement with DragConstraint.SENW applied
/// </summary>
/// <param name="dest">The point the DragHandle wants to move to</param>
/// <returns>True if the movement is permitted, otherwise false. Note that
/// even when the return value is true the values of dest.X and dest.Y
/// may have changed to constrain movement.</returns>
private bool Eval_MoveSENW(ref Point dest)
{
dest.Y = HandleOrigin.Y + (dest.X - HandleOrigin.X);
return true;
}
/// <summary>
/// Evaluate movement with DragConstraint.NE applied
/// </summary>
/// <param name="dest">The point the DragHandle wants to move to</param>
/// <returns>True if the movement is permitted, otherwise false. Note that
/// even when the return value is true the values of dest.X and dest.Y
/// may have changed to constrain movement.</returns>
private bool Eval_MoveNE(ref Point dest)
{
if (dest.X < HandleOrigin.X)
return false;
else
{
dest.Y = HandleOrigin.Y - (dest.X - HandleOrigin.X);
return true;
}
}
/// <summary>
/// Evaluate movement with DragConstraint.SW applied
/// </summary>
/// <param name="dest">The point the DragHandle wants to move to</param>
/// <returns>True if the movement is permitted, otherwise false. Note that
/// even when the return value is true the values of dest.X and dest.Y
/// may have changed to constrain movement.</returns>
private bool Eval_MoveSW(ref Point dest)
{
if (dest.X > HandleOrigin.X)
return false;
else
{
dest.Y = HandleOrigin.Y - (dest.X - HandleOrigin.X);
return true;
}
}
/// <summary>
/// Evaluate movement with DragConstraint.SE applied
/// </summary>
/// <param name="dest">The point the DragHandle wants to move to</param>
/// <returns>True if the movement is permitted, otherwise false. Note that
/// even when the return value is true the values of dest.X and dest.Y
/// may have changed to constrain movement.</returns>
private bool Eval_MoveSE(ref Point dest)
{
if (dest.X < HandleOrigin.X)
return false;
else
{
dest.Y = HandleOrigin.Y + (dest.X - HandleOrigin.X);
return true;
}
}
/// <summary>
/// Evaluate movement with DragConstraint.NW applied
/// </summary>
/// <param name="dest">The point the DragHandle wants to move to</param>
/// <returns>True if the movement is permitted, otherwise false. Note that
/// even when the return value is true the values of dest.X and dest.Y
/// may have changed to constrain movement.</returns>
private bool Eval_MoveNW(ref Point dest)
{
if (dest.X > HandleOrigin.X)
return false;
else
{
dest.Y = HandleOrigin.Y + (dest.X - HandleOrigin.X);
return true;
}
}
/// <summary>
/// Called when the handle is dragged to a new position.
/// </summary>
/// <param name="dest">The Point to which the handle was moved.
/// Parameter may be modified by the evaluation.</param>
/// <returns>True if movement is allowed, otherwise false</returns>
private bool RequestDragTo(ref Point dest)
{
if (dest == HandleOrigin)
return true;
if (dest.X < HandleMinX || dest.X > HandleMaxX ||
dest.Y < HandleMinY || dest.Y > HandleMaxY)
return false;
return (null != _evalMove ? _evalMove(ref dest) : true);
}
private delegate bool MovementEvalCallback(ref Point p);
private MovementEvalCallback _evalMove;
private Shape _shape;
private bool _dragging;
private bool _mouseDown;
}
}
Playing With Blocks Listing 1 – DragHandle.xaml
<!-- /****************************************************************************** * DragHandle.xaml * * This module the XAML markup for the DragHandle class. * * Date: 2/2009 * * Copyright (c) 2009, Mark Betz * * All rights reserved. * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of the Author nor the names of contributors may be * used to endorse or promote products derived from this software * without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY MARK BETZ ''AS IS'' AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. * IN NO EVENT SHALL MARK BETZ BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * */ --> <UserControl x:Class="DrawControls.DragHandle" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> <Grid> <Canvas x:Name="HandleCanvas"> </Canvas> </Grid> </UserControl>
AvalonLife Listing 4 – ALMainWin.xaml.cs
using System;
using System.ComponentModel;
using System.IO;
using System.Net;
using System.Text;
using System.Runtime.Serialization.Formatters.Binary;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Media;
using System.Windows.Documents;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Shapes;
namespace AvalonLife
{
/// <summary>
/// UIStateChanges
///
/// This enum provides indicators for the user interface states
/// that the program can be in. Used to call UIStateChange().
/// </summary>
enum UIStateChanges
{
ModelCreated,
ModelSaved,
ModelSavedAs,
ModelLoadedFromFile,
ModelLoadedFromNet,
ModelCellEdited,
ModelRun,
ModelPaused,
ModelHalted,
ModelReset,
ModelPropertiesEdited
}
/// <summary>
/// Used to indicate the current type of cell brush used to pain
/// the grid
/// </summary>
enum CellBrushType
{
Radial,
Linear,
Solid
}
enum ALFileType
{
None,
AVL,
Cells
}
public partial class ALMainWin : System.Windows.Window
{
#region ReticleAdorner class
/// <summary>
/// ReticleAdorner
///
/// This class is derived from Adorner, and renders the reticle (crosshairs)
/// over the main game grid.
/// </summary>
public class ReticleAdorner : Adorner
{
public ReticleAdorner( UIElement target )
: base(target)
{
_reticleColor = ALSettings.Default.ReticleColor;
_reticlePen = new Pen(new SolidColorBrush(_reticleColor), 1);
}
protected override void OnRender( DrawingContext dctxt )
{
double height = ((Grid)(this.AdornedElement)).ActualHeight;
double width = ((Grid)(this.AdornedElement)).ActualWidth;
Point start = new Point( 0, height / 2 );
Point end = new Point( width, height / 2 );
dctxt.DrawLine( _reticlePen, start, end );
start = new Point( width / 2, 0 );
end = new Point( width / 2, height );
dctxt.DrawLine( _reticlePen, start, end );
}
/// <summary>
/// Set this property to change the color of the reticle
/// </summary>
private Color _reticleColor;
public Color ReticleColor
{
get
{
return _reticleColor;
}
set
{
_reticleColor = value;
ALSettings.Default.ReticleColor = _reticleColor;
_reticlePen = new Pen(new SolidColorBrush(_reticleColor), 1);
}
}
private Pen _reticlePen;
}
#endregion
public ALMainWin()
{
InitializeComponent();
}
/// <summary>
/// PrepMessage( string, int )
///
/// A simple function to insert line breaks into a string destined for a messagebox.
/// MessageBox.Show() seems to work differently on XP and Vista. On Vista a long
/// message is sensibly wrapped at word boundaries. On XP it is not. If you insert
/// line breaks to look right in XP, they don't look right in Vista. So this is an
/// attempt to get some consistency.
/// </summary>
/// <param name="msg"></param>
/// <param name="lineLen"></param>
/// <returns></returns>
public string PrepMessage(string msg, int lineLen)
{
string strout = "";
int j = 0;
for (int i = 0; i < msg.Length; i++)
{
if ((j >= lineLen && msg[i] == ' ') || i == msg.Length - 1)
{
strout += msg.Substring(i - j, j);
strout += "\n";
j = 0;
}
else j++;
}
return strout;
}
#region menu bar event handlers
/// <summary>
/// Menu_OnGameNew(Object, RoutedEventArgs)
///
/// Handles the click event for the game menu, new command. Prompts the user and
/// on yes rebuilds the simulation and life models and resets the grid.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public void Menu_OnGameNew(Object sender, RoutedEventArgs e)
{
Cursor oldCursor = LifeGrid.Cursor;
LifeGrid.Cursor = Cursors.Wait;
bool paused = _ls.IsPaused;
_ls.IsPaused = true;
if ( ExecNew(false) )
oldCursor = LifeGrid.Cursor;
else
_ls.IsPaused = paused;
LifeGrid.Cursor = oldCursor;
}
/// <summary>
/// Menu_OnGameReset(Object, RoutedEventArgs)
///
/// Handles a click event on the Game menu, Reset item. Prompts the user, and on 'Yes'
/// rebuilds the model and simulation controller, and resets various parameters.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public void Menu_OnGameReset(Object sender, RoutedEventArgs e)
{
Cursor oldCursor = LifeGrid.Cursor;
LifeGrid.Cursor = Cursors.Wait;
bool paused = _ls.IsPaused;
_ls.IsPaused = true;
if (MessageBox.Show(this, Properties.Resources.UI_MB_PromptText2, Properties.Resources.UI_MB_CaptionText2,
MessageBoxButton.YesNo, MessageBoxImage.Question) == MessageBoxResult.Yes)
{
_ls.ResetSim();
UIStateChange(UIStateChanges.ModelReset);
oldCursor = LifeGrid.Cursor;
}
else
_ls.IsPaused = paused;
LifeGrid.Cursor = oldCursor;
}
/// <summary>
/// Menu_OnGameSave(Object, RoutedEventArgs)
///
/// Handles the click event for the game menu, save command. Pauses the sim if
/// it is not already, and then calls ExecSave()
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public void Menu_OnGameSave(Object sender, RoutedEventArgs e)
{
Cursor oldCursor = LifeGrid.Cursor;
LifeGrid.Cursor = Cursors.Wait;
bool paused = _ls.IsPaused;
_ls.IsPaused = true;
ExecSave(false);
_ls.IsPaused = paused;
LifeGrid.Cursor = oldCursor;
}
/// <summary>
/// Menu_OnGameSaveAs(Object, RoutedEventArgs)
///
/// Handles a click on the "save as" command in the Game menu. The only difference
/// from the function above is that this one calls ExecSave with true, causing
/// a new filename to be chosen.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public void Menu_OnGameSaveAs(Object sender, RoutedEventArgs e)
{
Cursor oldCursor = LifeGrid.Cursor;
LifeGrid.Cursor = Cursors.Wait;
bool paused = _ls.IsPaused;
_ls.IsPaused = true;
ExecSave(true);
_ls.IsPaused = paused;
LifeGrid.Cursor = oldCursor;
}
/// <summary>
/// Menu_OnGameLoad(Object, RoutedEventArgs)
///
/// Handles the click event for the game menu, load command. Pauses the sim and
/// then checks to see if the current game is dirty. If so it pops a save dialog
/// and calls ExecSave() if the user answers affirm. It then calls ExecLoad(). If
/// load fails (ExecLoad returns false) the function unpauses the game (if it was
/// paused on entry).
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public void Menu_OnGameLoad(Object sender, RoutedEventArgs e)
{
Cursor oldCursor = LifeGrid.Cursor;
LifeGrid.Cursor = Cursors.Wait;
bool paused = _ls.IsPaused;
_ls.IsPaused = true;
bool final = false;
System.Windows.Forms.OpenFileDialog opendlg =
new System.Windows.Forms.OpenFileDialog();
opendlg.DefaultExt = ".avl";
opendlg.Filter = "AvalonLife Saved Games (.avl)|*.avl|Life Lexicon Cells (.cells)|*.cells";
opendlg.Title = "Load Saved Model";
if (opendlg.ShowDialog() == System.Windows.Forms.DialogResult.OK)
{
if ( ExecLoadFile(opendlg.FileName) )
{
final = true;
}
}
if ( !final )
_ls.IsPaused = paused;
LifeGrid.Cursor = oldCursor;
}
/// <summary>
/// Menu_OnGameExit(Object, RoutedEventArgs)
///
/// Handles a click on the game menu, exit command, and shuts down the application
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public void Menu_OnGameExit(Object sender, RoutedEventArgs e)
{
bool paused = _ls.IsPaused;
_ls.IsPaused = true;
if ( CheckSave() == true )
Application.Current.Shutdown();
_ls.IsPaused = paused;
}
/// <summary>
/// Menu_OnSettingsGridLines(Object, RoutedEventArgs)
///
/// Handles a click on the settings menu, show grid command, and turns the
/// grid lines on/off accordingly.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public void Menu_OnSettingsGridLines(Object sender, RoutedEventArgs e)
{
if (MenuSettingsGridLines.IsChecked == false)
{
ALSettings.Default.GridOn = true;
LifeGrid.ShowGridLines = true;
MenuSettingsGridLines.IsChecked = true;
}
else
{
ALSettings.Default.GridOn = false;
LifeGrid.ShowGridLines = false;
MenuSettingsGridLines.IsChecked = false;
}
}
/// <summary>
/// Menu_OnSettingsReticle(Object, RoutedEventArgs)
///
/// Handles a click on the settings menu, show reticle command, and turns the
/// reticle (crosshairs) on/off accordingly.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public void Menu_OnSettingsReticle(Object sender, RoutedEventArgs e)
{
if ( MenuSettingsReticle.IsChecked == false )
{
ALSettings.Default.ReticleOn = true;
_gridAdornerLayer.GetAdorners(LifeGrid)[0].Visibility = Visibility.Visible;
MenuSettingsReticle.IsChecked = true;
}
else
{
ALSettings.Default.ReticleOn = false;
_gridAdornerLayer.GetAdorners(LifeGrid)[0].Visibility = Visibility.Hidden;
MenuSettingsReticle.IsChecked = false;
}
}
/// <summary>
/// Menu_OnSettingsHaltStable(Object, RoutedEventArgs)
///
/// Handles a click on the Settings | Halt Stable Model command. Flips the
/// state of this property in ALSettings. Controls whether the simulation will
/// halt when a model ceases evolving.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public void Menu_OnSettingsHaltStable(Object sender, RoutedEventArgs e)
{
if (MenuSettingsHaltStable.IsChecked == false)
{
MenuSettingsHaltStable.IsChecked = true;
ALSettings.Default.HaltOnStability = true;
}
else
{
MenuSettingsHaltStable.IsChecked = false;
ALSettings.Default.HaltOnStability = false;
}
}
/// <summary>
/// Menu_OnSettingsGridBackground(Object, RoutedEventArgs)
///
/// Handles a click on the settings menu, grid background command. Opens a color picker
/// dialog to let the user choose the grid background color. I'm sure there must be an easier
/// way to deal with the collision between System.Windows.Media.xxx, which is used by
/// the WPF shapes and brushes, and System.Drawing.xxx which is used by the standard color
/// dialog in System.Windows.Forms, but I haven't taken the time to research it yet. So
/// for the moment this function is full of ugly conversions. Works, though.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public void Menu_OnSettingsGridBackground(Object sender, RoutedEventArgs e)
{
bool paused = _ls.IsPaused;
_ls.IsPaused = true;
System.Windows.Forms.ColorDialog clrDlg = new System.Windows.Forms.ColorDialog();
clrDlg.AllowFullOpen = true;
clrDlg.SolidColorOnly = true;
System.Drawing.Color origColor = System.Drawing.Color.FromArgb(
((SolidColorBrush)LifeGrid.Background).Color.A,
((SolidColorBrush)LifeGrid.Background).Color.R,
((SolidColorBrush)LifeGrid.Background).Color.G,
((SolidColorBrush)LifeGrid.Background).Color.B );
clrDlg.Color = origColor;
if ( clrDlg.ShowDialog() == System.Windows.Forms.DialogResult.OK )
{
Color newColor = Color.FromArgb(clrDlg.Color.A, clrDlg.Color.R, clrDlg.Color.G, clrDlg.Color.B);
ALSettings.Default.GridBackground = newColor;
SolidColorBrush gridBkgBrush = new SolidColorBrush( newColor );
LifeGrid.Background = gridBkgBrush;
}
_ls.IsPaused = paused;
}
/// <summary>
/// Menu_OnSettingsReticleColor(Object, RoutedEventArgs)
///
/// Handles a click on the settings menu, reticle color command. Opens a color
/// picker and allows the user to choose a new color for drawing the reticle.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public void Menu_OnSettingsReticleColor(Object sender, RoutedEventArgs e)
{
bool paused = _ls.IsPaused;
_ls.IsPaused = true;
ReticleAdorner ad = _gridAdornerLayer.GetAdorners(LifeGrid)[0] as ReticleAdorner;
System.Windows.Forms.ColorDialog clrDlg = new System.Windows.Forms.ColorDialog();
clrDlg.AllowFullOpen = true;
clrDlg.SolidColorOnly = true;
if ( clrDlg.ShowDialog() == System.Windows.Forms.DialogResult.OK )
{
Color newColor = Color.FromArgb(clrDlg.Color.A, clrDlg.Color.R, clrDlg.Color.G, clrDlg.Color.B);
ad.ReticleColor = newColor;
ALSettings.Default.ReticleColor = newColor;
ad.InvalidateVisual();
}
_ls.IsPaused = paused;
}
/// <summary>
/// Menu_OnSettingsCellBrush(Object, RoutedEventArgs)
///
/// Handles a click on the Settings | Cell Brush command, and pops the brush definer
/// dialog. Calls ApplyCellBrush on return if the user accepted the dialog.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public void Menu_OnSettingsCellBrush(Object sender, RoutedEventArgs e)
{
Cursor oldCursor = LifeGrid.Cursor;
bool paused = _ls.IsPaused;
_ls.IsPaused = true;
ALBrushDlg dlg = new ALBrushDlg();
dlg.Owner = this;
if ( dlg.ShowDialog() == true )
{
LifeGrid.Cursor = Cursors.Wait;
ApplyRectStyle();
LifeGrid.Cursor = oldCursor;
}
_ls.IsPaused = paused;
}
/// <summary>
/// Menu_OnSettingsGridType(Object, RoutedEventArgs)
///
/// Handles a click on one of the grid type checkable menuitems in Settings | Grid Type.
/// The Tag property of these controls has been set to the appropriate value from
/// the GridType enumeration, so the tag can just be cast and passed through to
/// SetGridType.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public void Menu_OnSettingsGridType(Object sender, RoutedEventArgs e)
{
GridType gridType = (GridType)((MenuItem)sender).Tag;
if (_ls.Model.LifeGridType != gridType)
{
_ls.Model.LifeGridType = gridType;
SetGridType(gridType);
UIStateChange(UIStateChanges.ModelPropertiesEdited);
}
}
/// <summary>
/// Menu_OnSettingsGridSize(Object, RoutedEventArgs)
///
/// Handles a click on one of the pre-defined grid sizes in the Settings | Grid Size
/// | [predefined grid size] menu. Since the predefined model sizes are square we can
/// just store one dimensionin the object tag at startup, grab it here, and use it to
/// figure out what size was requested.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public void Menu_OnSettingsGridSize(Object sender, RoutedEventArgs e)
{
Cursor oldCursor = LifeGrid.Cursor;
LifeGrid.Cursor = Cursors.Wait;
bool paused = _ls.IsPaused;
_ls.IsPaused = true;
int size = (int)(((MenuItem)sender).Tag);
if ( _ls.Model.ResizeModel(size, size) )
{
InitUIState();
UIStateChange(UIStateChanges.ModelPropertiesEdited);
}
else
MessageBox.Show(this, Properties.Resources.UI_MB_ResizeFailedMsg, Properties.Resources.UI_MB_ResizeFailedCaption,
MessageBoxButton.OK, MessageBoxImage.Error);
_ls.IsPaused = paused;
LifeGrid.Cursor = oldCursor;
}
/// <summary>
/// Menu_OnSettingsGridSizeCustom(Object, RoutedEventArgs)
///
/// Handles a click on the Settings | Grid Size | Custom menu. The dialog enforces type
/// constraints on the edit, so we know we're getting ints, but we need to check how
/// large a model the user has asked for. You can ask the sim to make a 1000 x 1000 grid.
/// I did, and by the time I killed the process five minutes later it had a 1.7 gig
/// working set. A grid that size requires the system to create about 3 million objects,
/// plus or minus a couple hundred thousand. So we check to see if the aggregate grid
/// size is greater than 10k cells, and pop a warning dialog. If the user wants to go
/// ahead we let them. I've done a 200 x 200 grid and it loads relatively quickly and will
/// actually run, but not smoothly.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public void Menu_OnSettingsGridSizeCustom(Object sender, RoutedEventArgs e)
{
Cursor oldCursor = LifeGrid.Cursor;
LifeGrid.Cursor = Cursors.Wait;
bool paused = _ls.IsPaused;
_ls.IsPaused = true;
ALModelSizeDlg dlg = new ALModelSizeDlg();
dlg.Owner = this;
dlg.Tag = _ls.Model;
dlg.Resize = true;
if ( dlg.ShowDialog() == true )
{
int rows = dlg.Rows;
int cols = dlg.Columns;
if ( rows * cols > 10000 )
{
string msg = PrepMessage(Properties.Resources.UI_MB_LargeModelMsg, 80);
msg += "\n\nRequested model size will cause the program to create approximately "
+ Convert.ToString((rows * cols) * 3) + " objects.";
if ( MessageBox.Show(this, msg, Properties.Resources.UI_MB_LargeModelCaption,
MessageBoxButton.OKCancel, MessageBoxImage.Question) == MessageBoxResult.Cancel )
{
LifeGrid.Cursor = oldCursor;
return;
}
}
if (_ls.Model.ResizeModel(rows, cols))
{
InitUIState();
UIStateChange(UIStateChanges.ModelPropertiesEdited);
}
else
MessageBox.Show(this, Properties.Resources.UI_MB_ResizeFailedMsg, Properties.Resources.UI_MB_ResizeFailedCaption,
MessageBoxButton.OK, MessageBoxImage.Error);
}
_ls.IsPaused = paused;
LifeGrid.Cursor = oldCursor;
}
/// <summary>
/// Menu_OnSettingsGridSizeShrink(Object, RoutedEventArgs)
///
/// Handles a click on the Settings | Grid Size | Shrink to Model menu item. Calls
/// the model to resize the grid to the model size. See LifeModel.ResizeModel() for
/// failure conditions.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public void Menu_OnSettingsGridSizeShrink(Object sender, RoutedEventArgs e)
{
Cursor oldCursor = LifeGrid.Cursor;
LifeGrid.Cursor = Cursors.Wait;
bool paused = _ls.IsPaused;
_ls.IsPaused = true;
if (_ls.Model.ResizeModel(0, 0))
{
InitUIState();
UIStateChange(UIStateChanges.ModelPropertiesEdited);
}
else
MessageBox.Show(this, Properties.Resources.UI_MB_ResizeFailedMsg, Properties.Resources.UI_MB_ResizeFailedCaption,
MessageBoxButton.OK, MessageBoxImage.Error);
_ls.IsPaused = paused;
LifeGrid.Cursor = oldCursor;
}
/// <summary>
/// Menu_OnSettingsGridSettings(Object, RoutedEventArgs)
///
/// Handles a click on the grid settings menuitem in Settings | Grid Settings. Pops
/// the grid settings dialog window.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public void Menu_OnSettingsGridSettings(Object sender, RoutedEventArgs e)
{
ALGridDlg dlg = new ALGridDlg();
dlg.Owner = this;
dlg.ShowDialog();
}
/// <summary>
/// Menu_OnSettingsModelName(Object, RoutedEventArgs)
///
/// Handles a click on the model name menuitem in Settings. Pops the model
/// name dialog window. Tracks changes to the name of the model so it can
/// update the window title bar after the dialog closes.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public void Menu_OnSettingsModelName(Object sender, RoutedEventArgs e)
{
ALModelNameDlg dlg = new ALModelNameDlg();
dlg.Owner = this;
dlg.Tag = _ls;
if ( dlg.ShowDialog() == true )
UIStateChange(UIStateChanges.ModelPropertiesEdited);
}
/// <summary>
/// Menu_OnHelpHowTo(Object, RoutedEventArgs)
///
/// Handles a click on the help menu, How to Play command.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public void Menu_OnHelpHowTo(Object sender, RoutedEventArgs e)
{
System.Windows.Forms.HelpNavigator nav =
System.Windows.Forms.HelpNavigator.TopicId;
System.Windows.Forms.Help.ShowHelp(null, @"avalonlife.chm", nav, "1020");
}
/// <summary>
/// Menu_OnHelpAbout(Object, RoutedEventArgs)
///
/// Handles a click on the help menu, about AvalonLife command.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public void Menu_OnHelpAbout(Object sender, RoutedEventArgs e)
{
System.Windows.Forms.HelpNavigator nav =
System.Windows.Forms.HelpNavigator.TopicId;
System.Windows.Forms.Help.ShowHelp(null, @"avalonlife.chm", nav, "1000");
}
/// <summary>
/// Menu_OnHelpAboutLife(Object, RoutedEventArgs)
///
/// Handles a click on the help menu, about the Game of Life command.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public void Menu_OnHelpAboutLife(Object sender, RoutedEventArgs e)
{
System.Windows.Forms.HelpNavigator nav =
System.Windows.Forms.HelpNavigator.TopicId;
System.Windows.Forms.Help.ShowHelp(null, @"avalonlife.chm", nav, "1010");
}
#endregion
#region other ui event handlers
/// <summary>
/// ALMainWin_OnLoaded()
///
/// Handles the loaded event for the main window. Initializes the simulation
/// model, controller, and display grid.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public void ALMainWin_OnLoaded(Object sender, RoutedEventArgs e)
{
ExecNew(true);
RunSpeedSlider.ToolTip = Properties.Resources.UI_RunSpeedSlider_ToolTip;
StatusGenCount.ToolTip = Properties.Resources.UI_StatusGenCount_ToolTip;
CellBirthCount.ToolTip = Properties.Resources.UI_CellBirthCount_ToolTip;
CellDeathCount.ToolTip = Properties.Resources.UI_CellDeathCount_ToolTip;
PopulationCount.ToolTip = Properties.Resources.UI_PopulationCount_ToolTip;
PeakPopulationCount.ToolTip = Properties.Resources.UI_PeakPopulationCount_ToolTip;
MenuGridSizeText.ToolTip = Properties.Resources.UI_GridSize_ToolTip;
_gridAdornerLayer = AdornerLayer.GetAdornerLayer(LifeGrid);
ReticleAdorner ad = new ReticleAdorner(LifeGrid);
_gridAdornerLayer.Add(ad);
MenuSettingsReticle.IsChecked = ALSettings.Default.ReticleOn;
if ( MenuSettingsReticle.IsChecked )
ad.Visibility = Visibility.Visible;
else
ad.Visibility = Visibility.Hidden;
LifeGrid.Drop += new DragEventHandler( Grid_OnDrop );
LifeGrid.MouseUp += new MouseButtonEventHandler( Grid_OnMouseUp );
this.MouseLeave += new MouseEventHandler( Window_OnMouseLeave );
this.Closing += new CancelEventHandler( ALMainWin_OnClosing );
Application.Current.SessionEnding += new SessionEndingCancelEventHandler( ALMainWin_OnClosing );
LifeGrid.Background = new SolidColorBrush(ALSettings.Default.GridBackground);
LifeGrid.ShowGridLines = ALSettings.Default.GridOn;
MenuSettingsGridLines.IsChecked = LifeGrid.ShowGridLines;
MenuSettingsGridTorus.Tag = GridType.Torus;
MenuSettingsGridXCyl.Tag = GridType.XCylinder;
MenuSettingsGridYCyl.Tag = GridType.YCylinder;
MenuSettingsGridFinite.Tag = GridType.Finite;
MenuSettingsGrid40x40.Tag = 40;
MenuSettingsGrid50x50.Tag = 50;
MenuSettingsGrid60x60.Tag = 60;
MenuSettingsGrid70x70.Tag = 70;
if ( ALSettings.Default.HaltOnStability )
MenuSettingsHaltStable.IsChecked = true;
else
MenuSettingsHaltStable.IsChecked = false;
}
/// <summary>
/// ALMainWin_OnClosing(Object, CancelEventArgs)
///
/// This handles the closing event for the main window. If the game is not
/// dirty or the user saves it using 'Ok' in CheckSave, then this handler
/// will commit any settings changes and allow the close to continue. Other
/// wise if the user clicks Cancel in CheckSave it will cancel.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public void ALMainWin_OnClosing(Object sender, CancelEventArgs e )
{
bool paused = _ls.IsPaused;
_ls.IsPaused = true;
if ( CheckSave() == true )
{
if ( ALSettings.Default.Changed )
ALSettings.Default.Save();
}
else
{
e.Cancel = true;
}
_ls.IsPaused = paused;
}
/// <summary>
/// RunButton_OnClick()
///
/// Handles the click event for the run button on the main window. The button
/// changes state when it is clicked. The behavior of the sim drives off of
/// the IsPaused property of _ls (LifeSim). When the sim is paused the UI allows
/// editing of the grid cells.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public void RunButton_OnClick(Object sender, RoutedEventArgs e)
{
if ( _ls.IsPaused )
{
_ls.IsPaused = false;
UIStateChange(UIStateChanges.ModelRun);
}
else
{
_ls.IsPaused = true;
UIStateChange(UIStateChanges.ModelPaused);
}
}
/// <summary>
/// Rect_OnMouseDown(Object, MouseButtonEventArgs)
///
/// Handles the mouse down event on a Rectangle in the grid, and if the game is
/// in the paused state (editable) flips the cell state. Since we allow dragging
/// _lastMouseCell is used to avoid the effects of repeated MouseEnter events
/// getting fired.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public void Rect_OnMouseDown(Object sender, MouseButtonEventArgs e)
{
if ( _ls.IsPaused && (_ls.Generation == 0) )
{
LifeCell lc = ((Rectangle)sender).DataContext as LifeCell;
if ( lc != null )
{
_startEdit = true;
_lastMouseCell = lc;
lc.IsAlive = !lc.IsAlive;
UIStateChange(UIStateChanges.ModelCellEdited);
}
else throw (new System.InvalidOperationException("Rect_OnMouseDown"));
}
}
/// <summary>
/// Rect_OnMouseEnter(Object, MouseEventArgs)
///
/// Handles the mouseover event for an Ellipse. If the game is paused and the mouse
/// is entering a new cell, flip that cell's state.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public void Rect_OnMouseEnter(Object sender, MouseEventArgs e)
{
if ( _startEdit )
{
LifeCell lc = ((Rectangle)sender).DataContext as LifeCell;
if ( lc != null && lc != _lastMouseCell )
{
_lastMouseCell = lc;
lc.IsAlive = !lc.IsAlive;
}
else if ( lc == null )
throw (new System.InvalidOperationException("Rect_OnMouseEnter"));
}
}
/// <summary>
/// Grid_OnMouseUp(Object, MouseEventArgs)
///
/// Handles the mouse left button up event for the grid. We use it to check
/// the grid state and enable/disable menu items accordingly.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public void Grid_OnMouseUp(Object sender, MouseButtonEventArgs e)
{
if ( _startEdit )
{
_startEdit = false;
if ( _ls.Model.IsEmpty() )
{
UIStateChange(UIStateChanges.ModelCreated);
}
}
}
/// <summary>
/// Window_OnMouseLeave(Object, MouseEventArgs)
///
/// This function makes sure we handle things correctly if the user drags
/// the mouse out of the window while drawing cells. Ordinarily you would
/// capture the mouse and not release it until mouse up, but that's
/// cumbersome here, because we're getting events at the rectangle level
/// but can't capture the mouse there. If we capture it at the grid the
/// rectangles stop getting events. So this is the next best alternative.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public void Window_OnMouseLeave(Object sender, MouseEventArgs e)
{
if ( _startEdit )
{
_startEdit = false;
if (_ls.Model.IsEmpty())
{
UIStateChange(UIStateChanges.ModelCreated);
}
}
}
/// <summary>
/// Grid_OnDrop(Object, DragEventArgs)
///
/// This function gets called when an object is dropped on the grid. It handles drops
/// of .cells data from the Life Lexicon website, as well as .avl and .cells saved files.
/// The first half of the function detects a drop of a link from the Lexicon, and calls
/// ExecLoadWeb to get the data from the net. The second half detects file drops, and hands
/// off to ExecFileLoad.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public void Grid_OnDrop(Object sender, DragEventArgs e)
{
Cursor old = this.Cursor;
bool force = this.ForceCursor;
this.ForceCursor = true;
this.Cursor = Cursors.Wait;
if (e.Data.GetDataPresent(DataFormats.Text))
{
if ( (e.AllowedEffects & DragDropEffects.Link) == DragDropEffects.Link )
{
string url = e.Data.GetData(DataFormats.Text) as string;
if ( Uri.IsWellFormedUriString(url, UriKind.RelativeOrAbsolute) )
{
ExecLoadWeb( url );
}
}
}
else if (e.Data.GetDataPresent(DataFormats.FileDrop) )
{
string[] files = e.Data.GetData(DataFormats.FileDrop) as string[];
ExecLoadFile(files[files.Length - 1]);
}
this.Cursor = old;
this.ForceCursor = force;
}
/// <summary>
/// SimStatusCallback()
///
/// This function handles a callback from the simulation controller when it detects
/// that model evolution has halted. Will only be called if ALSettings.HaltOnStability
/// is true.
/// </summary>
/// <returns></returns>
public void SimStatusCallback()
{
string msg = null;
if ( _ls.Model.EvolutionHalted )
{
msg = Properties.Resources.UI_SimStatus_HaltMsg;
msg += " " + _ls.Generation.ToString();
msg += "\n\nCell births: " + _ls.Model.CellBirths.ToString();
msg += "\nCell deaths: " + _ls.Model.CellDeaths.ToString();
msg += "\nPopulation: " + _ls.Model.Population.ToString();
msg += "\nPeak population: " + _ls.Model.PeakPopulation.ToString();
msg += "\n\nThe simulation has been halted.";
MessageBox.Show(this, msg, "Simulation Status", MessageBoxButton.OK, MessageBoxImage.Information);
UIStateChange(UIStateChanges.ModelHalted);
}
}
#endregion
#region ALMainWin private methods
/// <summary>
/// UIStateChange(UIStateChanges)
///
/// This function wraps up all the UI state changes that the program goes through.
/// The possible states are defined in the AvalonLIfe.UIStateChanges enum. Each
/// case of the switch statement handles setting the UI controls and some variables
/// to appropriate settings for a given state.
/// </summary>
/// <param name="uis"></param>
private void UIStateChange(UIStateChanges uis)
{
switch (uis)
{
case UIStateChanges.ModelCreated:
RunButton.Content = Properties.Resources.UI_RunButton_Content1;
RunButton.ToolTip = Properties.Resources.UI_RunButton_ToolTip1;
LifeGrid.ToolTip = Properties.Resources.UI_LifeGrid_ToolTip;
LifeGrid.Cursor = Cursors.Hand;
RunButton.IsEnabled = false;
MenuGameSave.IsEnabled = false;
MenuGameSaveAs.IsEnabled = false;
MenuGameReset.IsEnabled = false;
SetGameDirty(false);
_currentModelFileBase = null;
break;
case UIStateChanges.ModelSaved:
case UIStateChanges.ModelSavedAs:
MenuGameSave.IsEnabled = false;
SetGameDirty(false);
break;
case UIStateChanges.ModelLoadedFromFile:
RunButton.Content = Properties.Resources.UI_RunButton_Content1;
RunButton.ToolTip = Properties.Resources.UI_RunButton_ToolTip1;
RunButton.IsEnabled = true;
LifeGrid.ToolTip = Properties.Resources.UI_LifeGrid_ToolTip;
LifeGrid.Cursor = Cursors.Hand;
MenuGameSave.IsEnabled = false;
MenuGameSaveAs.IsEnabled = true;
SetGameDirty(false);
break;
case UIStateChanges.ModelLoadedFromNet:
RunButton.Content = Properties.Resources.UI_RunButton_Content1;
RunButton.ToolTip = Properties.Resources.UI_RunButton_ToolTip1;
RunButton.IsEnabled = true;
LifeGrid.ToolTip = Properties.Resources.UI_LifeGrid_ToolTip;
LifeGrid.Cursor = Cursors.Hand;
MenuGameSave.IsEnabled = true;
MenuGameSaveAs.IsEnabled = true;
SetGameDirty(true);
break;
case UIStateChanges.ModelCellEdited:
RunButton.IsEnabled = true;
MenuGameSave.IsEnabled = true;
MenuGameSaveAs.IsEnabled = true;
SetGameDirty(true);
break;
case UIStateChanges.ModelRun:
RunButton.Content = Properties.Resources.UI_RunButton_Content2;
RunButton.ToolTip = Properties.Resources.UI_RunButton_ToolTip2;
LifeGrid.Cursor = Cursors.Arrow;
LifeGrid.ToolTip = null;
MenuGameReset.IsEnabled = true;
break;
case UIStateChanges.ModelPaused:
RunButton.Content = Properties.Resources.UI_RunButton_Content1;
RunButton.ToolTip = Properties.Resources.UI_RunButton_ToolTip1;
break;
case UIStateChanges.ModelHalted:
RunButton.Content = Properties.Resources.UI_RunButton_Content1;
RunButton.ToolTip = Properties.Resources.UI_RunButton_ToolTip1;
RunButton.IsEnabled = false;
break;
case UIStateChanges.ModelReset:
RunButton.Content = Properties.Resources.UI_RunButton_Content1;
RunButton.ToolTip = Properties.Resources.UI_RunButton_ToolTip1;
RunButton.IsEnabled = true;
LifeGrid.ToolTip = Properties.Resources.UI_LifeGrid_ToolTip;
LifeGrid.Cursor = Cursors.Hand;
MenuGameReset.IsEnabled = false;
break;
case UIStateChanges.ModelPropertiesEdited:
MenuGameSave.IsEnabled = true;
SetGameDirty(true);
break;
}
}
/// <summary>
/// UpdateWrapInidicators(bool, bool, bool, bool)
///
/// This method is called from SetGridType() to enable/disable the wrap indicator
/// bars that border the grid.
/// </summary>
/// <param name="left">True if the left bar is on, else false</param>
/// <param name="top">True if the top bar is on, else false</param>
/// <param name="right">True if the right bar is on, else false</param>
/// <param name="bottom">True if the bottom bar is on, else false</param>
private void UpdateWrapIndicators( bool left, bool top, bool right, bool bottom )
{
if ( left )
LeftWrapIndicator.Width = 4;
else
LeftWrapIndicator.Width = 0;
if (top)
TopWrapIndicator.Height = 5;
else
TopWrapIndicator.Height = 0;
if (right)
RightWrapIndicator.Width = 4;
else
RightWrapIndicator.Width = 0;
if (bottom)
BottomWrapIndicator.Height = 5;
else
BottomWrapIndicator.Height = 0;
}
/// <summary>
/// SetGridType( GridType )
///
/// This method is called from ExecLoad or Menu_OnGridXXX to update the menu and UI to
/// correspond with the grid type of the loaded model.
/// </summary>
/// <param name="gridType"></param>
private void SetGridType( GridType gridType )
{
MenuSettingsGridTorus.IsChecked = false;
MenuSettingsGridXCyl.IsChecked = false;
MenuSettingsGridYCyl.IsChecked = false;
MenuSettingsGridFinite.IsChecked = false;
switch ( gridType )
{
case GridType.Torus:
MenuSettingsGridTorus.IsChecked = true;
UpdateWrapIndicators(false, false, false, false);
break;
case GridType.XCylinder:
MenuSettingsGridXCyl.IsChecked = true;
UpdateWrapIndicators(false, true, false, true);
break;
case GridType.YCylinder:
MenuSettingsGridYCyl.IsChecked = true;
UpdateWrapIndicators(true, false, true, false);
break;
case GridType.Finite:
MenuSettingsGridFinite.IsChecked = true;
UpdateWrapIndicators(true, true, true, true);
break;
}
}
/// <summary>
/// SetWindowTitle()
///
/// Called from one or two spots to set the proper window title.
/// </summary>
private void SetWindowTitle()
{
this.Title = _winTitleBase;
if (_ls.Model.ModelName != null && _ls.Model.ModelName.Length > 0)
this.Title += " [" + _ls.Model.ModelName;
else
this.Title += " [Untitled";
if (_gameIsDirty)
this.Title += "*";
this.Title += "]";
}
/// <summary>
/// SetGameDirty(bool)
///
/// Called from various places where the current game becomes "dirty", or in need
/// of saving to disk. Called with true if the game has become dirty. Updates
/// the _gameIsDirty flag and window title on a state change.
/// </summary>
/// <param name="dirty"></param>
private void SetGameDirty( bool dirty )
{
_gameIsDirty = dirty;
SetWindowTitle();
}
/// <summary>
/// SetGridSizeMenu()
///
/// Called from InitUIState to update the state of the grid size menu
/// to reflect the size of a model.
/// </summary>
private void SetGridSizeMenu()
{
MenuSettingsGrid40x40.IsChecked = false;
MenuSettingsGrid50x50.IsChecked = false;
MenuSettingsGrid60x60.IsChecked = false;
MenuSettingsGrid70x70.IsChecked = false;
MenuSettingsGridCustom.IsChecked = false;
if ((_ls.Model.Columns != _ls.Model.Rows) ||
(_ls.Model.Columns != 40 && _ls.Model.Columns != 50 &&
_ls.Model.Columns != 60 && _ls.Model.Columns != 70))
{
MenuSettingsGridCustom.IsChecked = true;
}
else
{
switch (_ls.Model.Rows)
{
case 40:
MenuSettingsGrid40x40.IsChecked = true;
break;
case 50:
MenuSettingsGrid50x50.IsChecked = true;
break;
case 60:
MenuSettingsGrid60x60.IsChecked = true;
break;
case 70:
MenuSettingsGrid70x70.IsChecked = true;
break;
}
}
MenuGridSizeText.Text = "r:" + _ls.Model.Rows.ToString() + " c:" + _ls.Model.Columns.ToString();
}
/// <summary>
/// InitUIState()
///
/// This function peforms common setup work when a game is created or loaded.
/// Initializes the grid and populates it, sets up some data contexts, and sets
/// the window title.
/// </summary>
private void InitUIState()
{
InitGrid();
PopulateGrid();
ApplyRectStyle();
SetGridSizeMenu();
SetWindowTitle();
SetGridType(_ls.Model.LifeGridType);
StatusGenCount.DataContext = _ls;
RunSpeedSlider.DataContext = _ls;
CellBirthCount.DataContext = _ls.Model;
CellDeathCount.DataContext = _ls.Model;
PopulationCount.DataContext = _ls.Model;
PeakPopulationCount.DataContext = _ls.Model;
}
/// <summary>
/// ExecNew()
///
/// Does the grunt work of initializing a new game with an empty grid. Initializes
/// the model and controller, populates the grid, and wires up some UI fields by setting
/// data contexts for items with property bindings.
/// </summary>
private bool ExecNew(bool defaults)
{
bool result = false;
if ( CheckSave() == true )
{
if ( !defaults)
{
ALModelSizeDlg dlg = new ALModelSizeDlg();
dlg.Owner = this;
dlg.Tag = _ls.Model;
if ( dlg.ShowDialog() == true )
{
int rows = dlg.Rows;
int cols = dlg.Columns;
if ( rows * cols > 10000 )
{
string msg = PrepMessage(Properties.Resources.UI_MB_LargeModelMsg, 80);
msg += "\n\nRequested model size will cause the program to create approximately "
+ Convert.ToString((rows * cols) * 3) + " objects.";
if ( MessageBox.Show(this, msg, Properties.Resources.UI_MB_LargeModelCaption,
MessageBoxButton.OKCancel, MessageBoxImage.Question) == MessageBoxResult.Cancel )
{
return result;
}
}
if (_ls == null)
{
_ls = new LifeSim(rows, cols);
_ls.UICallback = SimStatusCallback;
}
else
_ls.NewModel(rows, cols);
}
else return result;
}
else
{
if (_ls == null)
{
_ls = new LifeSim();
_ls.UICallback = SimStatusCallback;
}
else
_ls.NewModel();
}
InitUIState();
UIStateChange(UIStateChanges.ModelCreated);
result = true;
}
return result;
}
/// <summary>
/// ExecLoadWeb(string)
///
/// This function handles loading a stream of cells from the Life Lexicon website.
/// Most of the work is actually done in LifeModel.LifeModel(Stream), which is called
/// from LifeSim.NewModel(Stream).
/// </summary>
/// <param name="url"></param>
/// <returns></returns>
private bool ExecLoadWeb( string url )
{
bool result = false;
if (CheckSave() == true)
{
Uri uri = new Uri(url);
char[] sep = { '.' };
string[] splitstr = uri.LocalPath.Split(sep);
if (string.Compare("cells", splitstr[splitstr.Length - 1], true) == 0)
{
HttpWebRequest cellReq = (HttpWebRequest)HttpWebRequest.Create(uri);
cellReq.Timeout = 10000;
cellReq.UserAgent = "AvalonLife_1_0";
try
{
HttpWebResponse cellRes = (HttpWebResponse)cellReq.GetResponse();
if ( _ls.NewModel(cellRes.GetResponseStream()) )
{
InitUIState();
UIStateChange(UIStateChanges.ModelLoadedFromNet);
_currentModelFileType = ALFileType.None;
result = true;
}
else
MessageBox.Show(this, "Failed to load model data.", "Load Error",
MessageBoxButton.OK, MessageBoxImage.Error);
cellRes.Close();
}
catch (WebException wex)
{
string msg = "Failed to retrieve cell data. Response: ";
msg += wex.Response;
MessageBox.Show(this, msg, "Network Error",
MessageBoxButton.OK, MessageBoxImage.Error);
}
}
else
{
string msg = "Invalid data type: " + url;
MessageBox.Show(this, msg, "Invalid Data",
MessageBoxButton.OK, MessageBoxImage.Error);
}
}
return result;
}
/// <summary>
/// ExecLoadFile(string)
///
/// Does the work of loading a game from a file. Handles saved .avl files as
/// well as .cells files saved from Life Lexicon data.
/// </summary>
/// <returns></returns>
private bool ExecLoadFile(string fileName)
{
bool result = false;
if (CheckSave() == true)
{
ALFileType ft = GetFileType(fileName);
if ( ft == ALFileType.AVL )
{
BinaryFormatter formatter = new BinaryFormatter();
Stream str = File.OpenRead(fileName);
try
{
_ls = formatter.Deserialize(str) as LifeSim;
_ls.UICallback = SimStatusCallback;
SetFileType(fileName);
InitUIState();
UIStateChange(UIStateChanges.ModelLoadedFromFile);
result = true;
}
catch (System.Runtime.Serialization.SerializationException ex)
{
MessageBox.Show(this, Properties.Resources.UI_MB_LoadFailedMsg + " " + ex.Message,
Properties.Resources.UI_MB_LoadFailedCaption,
MessageBoxButton.OK, MessageBoxImage.Error);
}
finally
{
str.Close();
}
SetGameDirty(false);
}
else if ( ft == ALFileType.Cells )
{
Stream str = File.OpenRead(fileName);
if ( _ls.NewModel(str) )
{
SetFileType(fileName);
InitUIState();
UIStateChange(UIStateChanges.ModelLoadedFromFile);
result = true;
}
else
MessageBox.Show(this, "Failed to load model data.", "Load Error",
MessageBoxButton.OK, MessageBoxImage.Error);
str.Close();
}
else
{
string msg = "Invalid file type: " + fileName;
MessageBox.Show(this, msg, "Invalid File",
MessageBoxButton.OK, MessageBoxImage.Error);
}
}
return result;
}
/// <summary>
/// ExecSave()
///
/// Called from the menu savegame handler, or from the load game handler
/// if necessary. Saves the current model to disk.
/// </summary>
private void ExecSave( bool saveAs )
{
System.Windows.Forms.SaveFileDialog savedlg =
new System.Windows.Forms.SaveFileDialog();
savedlg.AddExtension = true;
savedlg.Filter = "AvalonLife Saved Games (.avl)|*.avl|Life Lexicon Cells (.cells)|*.cells";
bool haveFile = false;
if (_currentModelFileBase != null && _currentModelFileType != ALFileType.None)
{
haveFile = true;
savedlg.FileName = _currentModelFileBase;
if ( _currentModelFileType == ALFileType.Cells )
{
savedlg.DefaultExt = ".cells";
savedlg.FilterIndex = 2;
}
else if ( _currentModelFileType == ALFileType.AVL )
{
savedlg.DefaultExt = ".avl";
savedlg.FilterIndex = 1;
}
}
else
{
savedlg.FileName = "AvalonLife Saved Game";
savedlg.DefaultExt = ".avl";
savedlg.FilterIndex = 1;
}
if ( saveAs == true )
savedlg.Title = "Save Model As";
else
savedlg.Title = "Save Model";
if (savedlg.ShowDialog() == System.Windows.Forms.DialogResult.OK)
{
ALFileType ft = GetFileType(savedlg.FileName);
if ( ft == ALFileType.AVL )
{
BinaryFormatter formatter = new BinaryFormatter();
Stream str = File.OpenWrite(savedlg.FileName);
try
{
formatter.Serialize(str, _ls);
if ( !saveAs || !haveFile )
SetFileType(savedlg.FileName);
UIStateChange(UIStateChanges.ModelSaved);
}
catch (System.Exception ex)
{
MessageBox.Show(this, Properties.Resources.UI_MB_SaveFailedMsg + " " + ex.InnerException.Message,
Properties.Resources.UI_MB_SaveFailedCaption,
MessageBoxButton.OK, MessageBoxImage.Error);
}
finally
{
str.Close();
}
}
else if ( ft == ALFileType.Cells )
{
Stream str = File.OpenWrite(savedlg.FileName);
try
{
_ls.Model.StreamCells(str);
if (!saveAs || !haveFile)
SetFileType(savedlg.FileName);
UIStateChange(UIStateChanges.ModelSaved);
}
catch (System.Exception)
{
}
finally
{
str.Close();
}
}
else
{
string msg = "Invalid file type: " + savedlg.FileName;
MessageBox.Show(this, msg, "Invalid File",
MessageBoxButton.OK, MessageBoxImage.Error);
}
}
}
/// <summary>
/// CheckSave()
///
/// This utility function checks to see if the current game is dirty. If it is
/// it asks the user whether they want to save the game before continuing with
/// whatever operation called this function. Returns true if the operation is
/// good to proceed, false if the user clicks cancel.
/// </summary>
/// <returns></returns>
private bool CheckSave()
{
bool result = true;
if (_gameIsDirty)
{
MessageBoxResult mbres = MessageBox.Show( this, Properties.Resources.UI_MB_PromptText3, Properties.Resources.UI_MB_CaptionText3,
MessageBoxButton.YesNoCancel, MessageBoxImage.Question );
if ( mbres == MessageBoxResult.Yes )
ExecSave(false);
else if ( mbres == MessageBoxResult.No )
SetGameDirty(false);
else if ( mbres == MessageBoxResult.Cancel )
result = false;
}
return result;
}
/// <summary>
/// InitGrid()
///
/// Called from the OnLoaded event handler for the main window to initialize the
/// UI display grid with the appropriate number of rows and columns based on the
/// _lm.Rows and _lm.Columns properties. It then adds an ellipse to each cell and
/// sets its style.
/// </summary>
private void InitGrid()
{
LifeGrid.Children.Clear();
LifeGrid.RowDefinitions.Clear();
LifeGrid.ColumnDefinitions.Clear();
for (int i = 0; i < _ls.Model.Rows; i++)
{
LifeGrid.RowDefinitions.Add(new RowDefinition());
}
for (int i = 0; i < _ls.Model.Columns; i++)
{
LifeGrid.ColumnDefinitions.Add(new ColumnDefinition());
}
}
/// <summary>
/// GetCellBrush()
///
/// Constructs a brush from the settings in the config file.
/// </summary>
/// <returns></returns>
private Brush GetCellBrush()
{
CellBrushType brushType = ALSettings.Default.LifeCellBrushType;
if ( brushType == CellBrushType.Radial )
{
RadialGradientBrush brush = new RadialGradientBrush();
brush.GradientOrigin = new Point(0.5, 0.5);
brush.RadiusX = 0.5;
brush.RadiusY = 0.5;
brush.GradientStops.Add( new GradientStop(ALSettings.Default.CellBrushC1, ALSettings.Default.CellBrushC1Off) );
brush.GradientStops.Add(new GradientStop(ALSettings.Default.CellBrushC2, ALSettings.Default.CellBrushC2Off));
brush.GradientStops.Add(new GradientStop(ALSettings.Default.CellBrushC3, ALSettings.Default.CellBrushC3Off));
brush.Freeze();
return brush;
}
else if ( brushType == CellBrushType.Linear )
{
LinearGradientBrush brush = new LinearGradientBrush();
brush.StartPoint = new Point(0, 0);
brush.EndPoint = new Point(1, 1);
brush.GradientStops.Add(new GradientStop(ALSettings.Default.CellBrushC1, ALSettings.Default.CellBrushC1Off));
brush.GradientStops.Add(new GradientStop(ALSettings.Default.CellBrushC2, ALSettings.Default.CellBrushC2Off));
brush.GradientStops.Add(new GradientStop(ALSettings.Default.CellBrushC3, ALSettings.Default.CellBrushC3Off));
brush.Freeze();
return brush;
}
else if ( brushType == CellBrushType.Solid )
{
SolidColorBrush brush = new SolidColorBrush(ALSettings.Default.CellBrushC1);
brush.Freeze();
return brush;
}
else
{
SolidColorBrush brush = new SolidColorBrush(Colors.Red);
brush.Freeze();
return brush;
}
}
/// <summary>
/// ApplyRectStyle()
///
/// If you look at the source you'll see that this function is only called when a new
/// grid is being initialized, and when the cell brush has been changed. It builds a new
/// style that sets the Fill property of a rectangle to the new brush, and then goes
/// through the children of the grid setting this style. The style is based on an
/// existing style that binds the opacity property to govern visibility. So why go to
/// all this trouble to change fill brushes? Why not just set the fill property on the
/// rects and be done with it? Here's the issue: again, opacity is controlled by a
/// binding in a style. Element properties override style settings, and they happen at
/// different times too. If in the process of creating a new grid I set the rectangle
/// DataContext to point to a LifeCell, which will drive the opacity binding, and then
/// set the Fill property directly, sometimes, depending on timing, I get a repaint
/// before the opacity property is correctly set, and the grid renders all the cells
/// visible. It looks messy, and I don't want the grid repainted until the state of all
/// the cells is correct. I'm sure there must be other ways to handle suppressing the
/// repaint, but the issue there is that the flash happens after I return control to
/// the message pump. If I somehow surpress the repaint when will I unsurpress it? The
/// best way around this that I have found so far is to do as I have below: change
/// brushes by building a new style and then applying that style.
/// </summary>
private void ApplyRectStyle()
{
Brush cellBrush = GetCellBrush();
Style style = new Style(typeof(Rectangle), (Style)LifeGrid.FindResource(typeof(Rectangle)));
Setter setter = new Setter();
setter.Property = Rectangle.FillProperty;
setter.Value = cellBrush;
style.Setters.Add(setter);
LifeGrid.Resources.Remove("RectStyle");
LifeGrid.Resources.Add("RectStyle", style);
UIElementCollection rects = LifeGrid.Children;
foreach (UIElement uie in rects)
((Rectangle)uie).Style = (Style)(LifeGrid.Resources["RectStyle"]);
}
/// <summary>
/// PopulateGrid()
///
/// Does the work of setting up the rectangles in the cells of the life grid. Creates
/// the rectangles and assigns them to the grid, adds them to the child collection,
/// sets up rectangle data contexts for the rect->cell link, sets the rectangle style,
/// and wires up the rectangle mouse events
/// </summary>
private void PopulateGrid()
{
for (int row = 0; row < _ls.Model.Rows; row++)
{
for (int col = 0; col < _ls.Model.Columns; col++)
{
Rectangle rect = new Rectangle();
Grid.SetRow(rect, row);
Grid.SetColumn(rect, col);
LifeGrid.Children.Add(rect);
rect.DataContext = _ls.Model.CellGrid[row, col];
rect.MouseDown += new MouseButtonEventHandler(Rect_OnMouseDown);
rect.MouseMove += new MouseEventHandler(Rect_OnMouseEnter);
}
}
}
/// <summary>
/// GetFileType(string)
///
/// Called from ExecSave to determine the type of file that the user selected.
/// </summary>
/// <param name="fileName"></param>
/// <returns></returns>
private ALFileType GetFileType( string fileName )
{
string ext = System.IO.Path.GetExtension(fileName);
ALFileType ft = ALFileType.None;
if (string.Compare(".avl", ext, true) == 0)
ft = ALFileType.AVL;
else if (string.Compare(".cells", ext, true) == 0)
ft = ALFileType.Cells;
return ft;
}
/// <summary>
/// SetFileType( string )
///
/// Called from the ExecSave and ExecLoad functions. Retrieves and stores
/// the base filename and sets the file format type.
/// </summary>
/// <param name="fileName"></param>
private ALFileType SetFileType( string fileName )
{
_currentModelFileBase = System.IO.Path.GetFileNameWithoutExtension(fileName);
string ext = System.IO.Path.GetExtension(fileName);
if ( string.Compare(".avl", ext, true) == 0 )
_currentModelFileType = ALFileType.AVL;
else if ( string.Compare(".cells", ext, true) == 0 )
_currentModelFileType = ALFileType.Cells;
else
_currentModelFileType = ALFileType.None;
return _currentModelFileType;
}
#endregion
/// <summary>
/// Holds the instance of the simulation controller
/// </summary>
private LifeSim _ls = null;
/// <summary>
/// True if the game has been modified since last save
/// </summary>
private bool _gameIsDirty = false;
/// <summary>
/// Tracks the last cell that the mouse was in
/// </summary>
private LifeCell _lastMouseCell = null;
/// <summary>
/// Container for the ReticleAdorner
/// </summary>
private AdornerLayer _gridAdornerLayer = null;
/// <summary>
/// True if we are dragging across cells in edit mode
/// </summary>
private bool _startEdit = false;
/// <summary>
/// If not null contains the last file name used to load or
/// save the current model.
/// </summary>
private string _currentModelFileBase = null;
private ALFileType _currentModelFileType = ALFileType.AVL;
/// <summary>
/// Self-explanatory
/// </summary>
private string _winTitleBase = "AvalonLife 1.0";
}
}
AvalonLife Listing 3 – ALMainWin.xaml
<Window x:Class="AvalonLife.ALMainWin"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="AvalonLife 1.0" Height="600" Width="600" Name="ALMainWindow" Background="#FFFFFFFF"
Loaded="ALMainWin_OnLoaded" >
<Window.Resources>
<Style x:Key="RunSpeedSliderStyle" TargetType="{x:Type Slider}">
<Setter Property="Width" Value="120" />
<Setter Property="Value" Value="{Binding Path=TimerInterval, Mode=TwoWay}" />
<Setter Property="Orientation" Value="Horizontal" />
<Setter Property="HorizontalAlignment" Value="Left" />
<Setter Property="IsSnapToTickEnabled" Value="True" />
<Setter Property="Minimum" Value="100" />
<Setter Property="Maximum" Value="1000" />
<Setter Property="TickPlacement" Value="BottomRight" />
<Setter Property="TickFrequency" Value="100" />
<Setter Property="IsDirectionReversed" Value="True" />
<Setter Property="IsMoveToPointEnabled" Value="True" />
</Style>
<Style x:Key="GenCountStyle" TargetType="{x:Type TextBlock}">
<Setter Property="Foreground" Value="Blue" />
<Setter Property="Text" Value="{Binding Path=Generation}" />
</Style>
<Style x:Key="BirthCountStyle" TargetType="{x:Type TextBlock}">
<Setter Property="Foreground" Value="Blue" />
<Setter Property="Text" Value="{Binding Path=CellBirths}" />
</Style>
<Style x:Key="DeathCountStyle" TargetType="{x:Type TextBlock}">
<Setter Property="Foreground" Value="Blue" />
<Setter Property="Text" Value="{Binding Path=CellDeaths}" />
</Style>
<Style x:Key="PopulationCountStyle" TargetType="{x:Type TextBlock}">
<Setter Property="Foreground" Value="Blue" />
<Setter Property="Text" Value="{Binding Path=Population}" />
</Style>
<Style x:Key="PeakPopulationCountStyle" TargetType="{x:Type TextBlock}">
<Setter Property="Foreground" Value="Blue" />
<Setter Property="Text" Value="{Binding Path=PeakPopulation}" />
</Style>
</Window.Resources>
<Grid>
<DockPanel>
<DockPanel DockPanel.Dock="Top" LastChildFill="False"
Background="{DynamicResource {x:Static SystemColors.MenuBarBrushKey}}">
<Menu Margin="0,5,5,2" DockPanel.Dock="Left" Width="Auto" HorizontalAlignment="Left"
Background="{DynamicResource {x:Static SystemColors.MenuBarBrushKey}}" >
<MenuItem Header="Game">
<MenuItem Name="MenuGameNew" Header="New" Click="Menu_OnGameNew" />
<MenuItem Name="MenuGameReset" Header="Reset" Click="Menu_OnGameReset" />
<MenuItem Name="MenuGameSave" Header="Save..." Click="Menu_OnGameSave" />
<MenuItem Name="MenuGameSaveAs" Header="Save as..." Click="Menu_OnGameSaveAs" />
<MenuItem Name="MenuGameLoad" Header="Load..." Click="Menu_OnGameLoad" />
<Separator />
<MenuItem Header="Exit" Click="Menu_OnGameExit" />
</MenuItem>
<MenuItem Header="Settings">
<MenuItem Name="MenuSettingsGridLines" Header="Show Grid" Click="Menu_OnSettingsGridLines" />
<MenuItem Name="MenuSettingsReticle" Header="Show Reticle" Click="Menu_OnSettingsReticle" />
<MenuItem Name="MenuSettingsHaltStable" Header="Halt Stable Model" Click="Menu_OnSettingsHaltStable" />
<Separator />
<MenuItem Name="MenuSettingsGridBkgColor" Header="Grid Background..." Click="Menu_OnSettingsGridBackground" />
<MenuItem Name="MenuSettingsReticleColor" Header="Reticle Color..." Click="Menu_OnSettingsReticleColor" />
<MenuItem Name="MenuSettingsCellBrush" Header="Cell Color..." Click="Menu_OnSettingsCellBrush" />
<Separator />
<MenuItem Header="Grid Type">
<MenuItem Name="MenuSettingsGridTorus" Header="Torus" Click="Menu_OnSettingsGridType" />
<MenuItem Name="MenuSettingsGridXCyl" Header="X Cylinder" Click="Menu_OnSettingsGridType" />
<MenuItem Name="MenuSettingsGridYCyl" Header="Y Cylinder" Click="Menu_OnSettingsGridType" />
<MenuItem Name="MenuSettingsGridFinite" Header="Finite" Click="Menu_OnSettingsGridType" />
</MenuItem>
<MenuItem Header="Grid Size">
<MenuItem Name="MenuSettingsGrid40x40" Header="40 x 40" Click="Menu_OnSettingsGridSize" />
<MenuItem Name="MenuSettingsGrid50x50" Header="50 x 50" Click="Menu_OnSettingsGridSize" />
<MenuItem Name="MenuSettingsGrid60x60" Header="60 x 60" Click="Menu_OnSettingsGridSize" />
<MenuItem Name="MenuSettingsGrid70x70" Header="70 x 70" Click="Menu_OnSettingsGridSize" />
<Separator />
<MenuItem Name="MenuSettingsGridCustom" Header="Custom..." Click="Menu_OnSettingsGridSizeCustom" />
<Separator />
<MenuItem Name="MenuSettingsGridShrink" Header="Shrink to Model" Click="Menu_OnSettingsGridSizeShrink" />
</MenuItem>
<MenuItem Name="MenuSettingsGridSettings" Header="Grid Settings..." Click="Menu_OnSettingsGridSettings" />
<Separator />
<MenuItem Name="MenuSettingsModelName" Header="Model Name..." Click="Menu_OnSettingsModelName" />
</MenuItem>
<MenuItem Header="Help">
<MenuItem Name="MenuHelpHowTo" Header="How to Play..." Click="Menu_OnHelpHowTo" />
<MenuItem Name="MenuHelpAbout" Header="About AvalonLife..." Click="Menu_OnHelpAbout" />
<MenuItem Name="MenuHelpAboutLife" Header="About the Game of Life..." Click="Menu_OnHelpAboutLife" />
</MenuItem>
</Menu>
<Button Margin="10,5,5,2" DockPanel.Dock="Right" Name="RunButton" VerticalAlignment="Center" HorizontalAlignment="Right"
Click="RunButton_OnClick" Height="20" Width="60" Content="Run" />
<TextBlock Margin="0,5,5,2" VerticalAlignment="Center" Width="60" Foreground="Blue" Name="MenuGridSizeText" DockPanel.Dock="Right" />
<TextBlock Margin="0,5,5,2" VerticalAlignment="Center" Width="50" Text="Grid Size:" DockPanel.Dock="Right" />
</DockPanel>
<Canvas Name="TopWrapIndicator" Height="5" Width="Auto" DockPanel.Dock="Top" HorizontalAlignment="Stretch" Background="Gray" />
<StatusBar Background="{DynamicResource {x:Static SystemColors.MenuBarBrushKey}}"
Height="30" DockPanel.Dock="Bottom" Padding="4,0,4,0">
<StatusBarItem>
<TextBlock Width="Auto" Text="Time:" />
</StatusBarItem>
<StatusBarItem>
<TextBlock Name="StatusGenCount" Width="30" Style="{StaticResource GenCountStyle}" />
</StatusBarItem>
<StatusBarItem>
<TextBlock Width="Auto" Text="Census:" />
</StatusBarItem>
<StatusBarItem>
<TextBlock Name="PopulationCount" Width="30" Style="{StaticResource PopulationCountStyle}" />
</StatusBarItem>
<StatusBarItem>
<TextBlock Width="Auto" Text="Peak:" />
</StatusBarItem>
<StatusBarItem>
<TextBlock Name="PeakPopulationCount" Width="30" Style="{StaticResource PeakPopulationCountStyle}" />
</StatusBarItem>
<StatusBarItem>
<TextBlock Width="Auto" Text="Born:" />
</StatusBarItem>
<StatusBarItem>
<TextBlock Name="CellBirthCount" Width="40" Style="{StaticResource BirthCountStyle}" />
</StatusBarItem>
<StatusBarItem>
<TextBlock Width="Auto" Text="Died:" />
</StatusBarItem>
<StatusBarItem>
<TextBlock Name="CellDeathCount" Width="40" Style="{StaticResource DeathCountStyle}" />
</StatusBarItem>
<StatusBarItem>
<TextBlock Width="Auto" Padding="10,0,0,0">Speed:</TextBlock>
</StatusBarItem>
<StatusBarItem>
<Slider Name="RunSpeedSlider" Style="{StaticResource RunSpeedSliderStyle}" />
</StatusBarItem>
</StatusBar>
<Canvas Name="BottomWrapIndicator" Height="5" Width="Auto" DockPanel.Dock="Bottom" HorizontalAlignment="Stretch" Background="Gray" />
<Canvas Name="LeftWrapIndicator" Height="Auto" Width="4" DockPanel.Dock="Left" VerticalAlignment="Stretch" Background="Gray" />
<Canvas Name="RightWrapIndicator" Height="Auto" Width="4" DockPanel.Dock="Right" VerticalAlignment="Stretch" Background="Gray" />
<Grid Name="LifeGrid" Background="White" ForceCursor="True" AllowDrop="True" >
<Grid.Resources>
<Style TargetType="{x:Type Rectangle}">
<Setter Property="Opacity" Value="{Binding Path=IsAlive}" />
</Style>
<Style BasedOn="{StaticResource {x:Type Rectangle}}" TargetType="{x:Type Rectangle}" x:Key="RectStyle" >
<Setter Property="Fill" Value="Red" />
</Style>
</Grid.Resources>
</Grid>
</DockPanel>
</Grid>
</Window>
AvalonLife Listing 2 – LifeSim.cs
using System;
using System.IO;
using System.ComponentModel;
using System.Windows;
using System.Collections.Generic;
using System.Text;
using System.Runtime.Serialization;
namespace AvalonLife
{
[Serializable]
class LifeSim : INotifyPropertyChanged, ISerializable
{
#region LifeSim public interface
/// <summary>
/// LifeSim()
///
/// Constructs an instance of the sim controller and creates an empty model
/// for it to run. LifeSim is in the paused state after construction, but the
/// timer is running.
/// </summary>
/// <param name="lm"></param>
public LifeSim()
{
_lm = new LifeModel();
_timerInterval = ALSettings.Default.TimerInterval;
_timer = new System.Windows.Forms.Timer();
_timer.Interval = _timerInterval;
_timer.Tick += new EventHandler(TimerEvent);
_timer.Tag = this;
_timer.Start();
}
/// <summary>
/// LifeSim(int, int)
///
/// Constructs an instance of the sim controller and creates an empty model
/// for it to run, using the specified dimensions. LifeSim is in the paused
/// state after construction, but the timer is running.
/// </summary>
/// <param name="rows">Height of the requested model grid</param>
/// <param name="columns">Width of the requested model grid</param>
public LifeSim(int rows, int columns)
{
_lm = new LifeModel(rows, columns);
_timerInterval = ALSettings.Default.TimerInterval;
_timer = new System.Windows.Forms.Timer();
_timer.Interval = _timerInterval;
_timer.Tick += new EventHandler(TimerEvent);
_timer.Tag = this;
_timer.Start();
}
#region ISerializable methods
/// <summary>
/// LifeSim(SerializationInfo, StreamingContext)
///
/// Called by the BinaryFormatter to construct an instance of LifeSim from a
/// stream. Deserializes the private members, then the LifeModel deserialization
/// is called, and finally the timer is created and started. The sim is constructed
/// in a paused state. The UI is responsible for wiring up events.
/// </summary>
/// <param name="info"></param>
/// <param name="ctxt"></param>
public LifeSim( SerializationInfo info, StreamingContext ctxt )
{
_generation = (int)info.GetValue( "_generation", typeof(int) );
_timerInterval = (int)info.GetValue( "_timerInterval", typeof(int) );
_lm = new LifeModel( info, ctxt );
_isPaused = true;
_timer = new System.Windows.Forms.Timer();
_timer.Interval = _timerInterval;
_timer.Tick += new EventHandler(TimerEvent);
_timer.Tag = this;
_timer.Start();
}
/// <summary>
/// GetObjectData(SerializationInfo, StreamingContext)
///
/// Called by the BinaryFormatter to serialize an instance of LifeSim. Serializes
/// the private members and then calls the LifeModel serialization method directly.
/// </summary>
/// <param name="info"></param>
/// <param name="ctxt"></param>
public void GetObjectData( SerializationInfo info, StreamingContext ctxt )
{
info.AddValue( "_generation", _generation );
info.AddValue( "_timerInterval", _timerInterval );
_lm.GetObjectData( info, ctxt );
}
#endregion
/// <summary>
/// ~LifeSim()
///
/// I haven't verified it, but this is almost certainly unnecessary. The timer
/// class dispose() should kill the timer and clean up. Anyway no harm done.
/// </summary>
~LifeSim()
{
if ( _timer != null )
_timer.Stop();
}
/// <summary>
/// TimerEvent(Object, EventArgs)
///
/// This function handles the timer tick. If the game is in the unpaused state it calls
/// the LifeModel.Evaluate() method to iterate the model. It then increments the
/// generation count. It's a static method so we pass in the 'this' pointer for the
/// LifeSim instance that owns the timer in its Tag property. Useful little things, Tags.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private static void TimerEvent( Object sender, EventArgs e )
{
LifeSim ls = ((System.Windows.Forms.Timer)sender).Tag as LifeSim;
if ( ls != null && !ls.IsPaused )
{
if ( !ls._lm.EvolutionHalted || ALSettings.Default.HaltOnStability == false )
{
ls._lm.Evaluate();
ls.Generation++;
}
else
{
ls.IsPaused = true;
if (ls._uiCallback != null)
{
ls._uiCallback();
}
}
}
else if ( ls == null )
throw( new System.InvalidOperationException("TimerEvent") );
}
/// <summary>
/// ResetSim()
///
/// Called from the UI to reset the simulation to its starting condition. On
/// exit the simulation is paused.
/// </summary>
public void ResetSim()
{
if ( !_isPaused )
IsPaused = true;
Generation = 0;
_lm.ResetModel();
}
/// <summary>
/// NewModel()
///
/// Called from the UI during handling of a click on Game | New.
/// </summary>
public void NewModel()
{
if ( !_isPaused )
_isPaused = true;
_lm = new LifeModel();
Generation = 0;
}
/// <summary>
/// NewModel(int, int)
///
/// Called from the UI during handling of a click on Game | New. Creates a
/// new model using the passed in dimensions.
/// </summary>
public void NewModel(int rows, int columns)
{
if (!_isPaused)
_isPaused = true;
_lm = new LifeModel(rows, columns);
Generation = 0;
}
/// <summary>
/// NewModel(Stream)
///
/// Called to decode a stream of .cells format data into a LifeModel. The bulk
/// of the work is done in the LifeModel.LifeModel(Stream) constructor. If
/// construction fails we will already have warned the user in the constructor,
/// so all we do here is restore the paused state we had on entry.
/// </summary>
/// <param name="str">Stream containing the .cells formated data</param>
/// <returns></returns>
public bool NewModel( Stream str )
{
bool result = false;
bool paused = _isPaused;
_isPaused = true;
try
{
LifeModel lm = new LifeModel( str );
_lm = lm;
Generation = 0;
result = true;
}
catch( System.Exception )
{
_isPaused = paused;
}
return result;
}
#endregion
#region INotifyPropertyChanged members
public event PropertyChangedEventHandler PropertyChanged;
#endregion
#region LifeSim public properties
/// <summary>
/// Counts the generations that the current model has run
/// </summary>
private int _generation = 0;
public int Generation
{
get
{
return _generation;
}
protected set
{
_generation = value;
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs("Generation"));
}
}
/// <summary>
/// Holds a reference to the simulation model
/// </summary>
private LifeModel _lm = null;
public LifeModel Model
{
get
{
return _lm;
}
}
/// <summary>
/// Holds the timer interval, set to default on start
/// </summary>
private int _timerInterval = 0;
public int TimerInterval
{
get
{
return _timerInterval;
}
set
{
_timerInterval = value;
_timer.Interval = _timerInterval;
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs("TimerInterval"));
}
}
/// <summary>
/// Holds the simulation run state: false if running, true if paused.
/// </summary>
private bool _isPaused = true;
public bool IsPaused
{
get
{
return _isPaused;
}
set
{
_isPaused = value;
}
}
/// <summary>
/// The timer handler may detect that the model has ceased evolving,
/// i.e. entered a stable state. In that case it will make a call to
/// the function in this delegate to inform the UI. The UI is responsible
/// for setting a function-typed value to this delegate member if it
/// wants to receive this callback.
/// </summary>
public delegate void UISimStatusCallback();
private UISimStatusCallback _uiCallback = null;
public UISimStatusCallback UICallback
{
get
{
return _uiCallback;
}
set
{
_uiCallback = value;
}
}
#endregion
#region LifeSim private data
private System.Windows.Forms.Timer _timer = null;
#endregion
}
}
AvalonLife Listing 1 – LifeModel.cs
using System;
using System.IO;
using System.Windows.Media;
using System.Collections.Generic;
using System.ComponentModel;
using System.Text;
using System.Runtime.Serialization;
namespace AvalonLife
{
/// <summary>
/// GridType
///
/// This enumeration is used to specify the type of grid bounding that
/// the model will use:
///
/// Torus - the cells on all edges of the grid wrap to the opposite edge.
/// XCylinder - the cells on the x axis edges wrap, the y-axis is finite.
/// YCylinder - the cells on the y axis edges wrap, the x-axis is finite.
/// Finite - all four edges of the grid are finite.
///
/// </summary>
enum GridType
{
Torus,
XCylinder,
YCylinder,
Finite
};
/// <summary>
/// I just wanted a simple rectangle class with four numbers in it, to use in
/// resizing the model grid (I use it as return type from a function that
/// calculates the extent of the current model).
/// </summary>
struct ALRectangle
{
public int Left;
public int Top;
public int Right;
public int Bottom;
}
/// <summary>
/// The LifeCell class represents a single cell and its live/dead state
/// </summary>
class LifeCell : INotifyPropertyChanged
{
#region LifeCell public properties
private bool _isAlive = false;
public bool IsAlive
{
get
{
return _isAlive;
}
set
{
_isAlive = value;
if ( PropertyChanged != null )
PropertyChanged( this, new PropertyChangedEventArgs("IsAlive") );
}
}
#endregion
#region INotifyPropertyChanged members
public event PropertyChangedEventHandler PropertyChanged;
#endregion
}
/// <summary>
/// The LifeModel class represents a grid of life cells
/// </summary>
[Serializable]
class LifeModel : ISerializable, INotifyPropertyChanged
{
#region LifeModel public interface
/// <summary>
/// LifeModel()
///
/// Initializes the LifeCell array with the default bounds
/// </summary>
public LifeModel()
{
InitArrays( ALSettings.Default.GridHeight, ALSettings.Default.GridWidth );
}
/// <summary>
/// LifeModel(int, int)
///
/// Initializes the LifeCell array with the passed in bounds
/// </summary>
/// <param name="columns">int, width of the life grid</param>
/// <param name="rows">int, height of the life grid</param>
public LifeModel( int rows, int columns )
{
InitArrays( rows, columns );
}
/// <summary>
/// LifeModel(Stream)
///
/// Constructs a LifeModel from a stream of .cells data compatible with the
/// Life Lexicon website.
/// </summary>
/// <param name="str"></param>
public LifeModel( Stream str )
{
int copyRow = 0;
int copyCol = 0;
StreamReader reader = new StreamReader(str);
string input = reader.ReadToEnd();
char[] sep = { '\n' };
string[] inputs = input.Split(sep);
if ( inputs.Length < 2 )
throw( new System.Exception("Bad cell data format") );
_modelName = inputs[0].Substring(6, inputs[0].Length - 6);
int newRows = inputs.Length - 3;
int newCols = inputs[2].Length;
int defRows = ALSettings.Default.GridHeight;
int defCols = ALSettings.Default.GridWidth;
if ( !ALSettings.Default.ShrinkGridToModel )
{
if (newRows < defRows)
copyRow = (defRows - newRows) / 2;
else
defRows = newRows;
if (newCols < defCols)
copyCol = (defCols - newCols) / 2;
else
defCols = newCols;
}
else
{
defRows = newRows;
defCols = newCols;
}
_evaluated = false;
PeakPopulation = 0;
Population = 0;
_gridType = ALSettings.Default.DefaultGridType;
InitArrays(defRows, defCols);
BuildWorkGrid();
for ( int row = 0; row < newRows; row++ )
{
for ( int col = 0; col < newCols; col++ )
{
bool set = false;
if ( inputs[row + 2][col] == '\x4f' )
{
set = true;
}
else if ( inputs[row + 2][col] != '\x2e' )
throw( new System.Exception("Invalid character in cell data: " + inputs[row + 2][col].ToString()) );
_cellGrid[row + copyRow, col + copyCol].IsAlive =
_lastGrid[row + copyRow, col + copyCol] =
_startingGrid[row + copyRow, col + copyCol] = set;
}
}
}
#region ISerializable methods
/// <summary>
/// LifeModel(SerializationInfo, StreamingContext)
///
/// Called by the BinaryFormatter to construct a LifeModel instance from a stream. Expects
/// _lastGrid to contain the most recent grid array, and copies it into a newly constructed
/// array of LifeCells.
/// </summary>
/// <param name="info"></param>
/// <param name="ctxt"></param>
public LifeModel( SerializationInfo info, StreamingContext ctxt )
{
_rows = (int)info.GetValue( "_rows", typeof(int) );
_columns = (int)info.GetValue( "_columns", typeof(int) );
_startingGrid = (bool[,])info.GetValue( "_startingGrid", typeof(bool[,]) );
_lastGrid = (bool[,])info.GetValue("_lastGrid", typeof(bool[,]) );
_evaluated = (bool)info.GetValue( "_evaluated", typeof(bool) );
_peakPopulation = (int)info.GetValue( "_peakPopulation", typeof(int) );
_gridType = (GridType)info.GetValue( "_gridType", typeof(GridType) );
_modelName = (string)info.GetValue( "_modelName", typeof(string) );
_cellGrid = new LifeCell[_rows, _columns];
for ( int row = 0; row < _rows; row++ )
{
for ( int col = 0; col < _columns; col++ )
{
_cellGrid[row, col] = new LifeCell();
_cellGrid[row, col].IsAlive = _lastGrid[row, col];
}
}
BuildWorkGrid();
}
/// <summary>
/// GetObjectData(SerializationInfo, StreamingContext)
///
/// Called by the BinaryFormatter to serialize the data for a LifeModel
/// into a stream. If the model has never been evaluated (i.e. edits have been
/// made to the grid but the sim hasn't been run, the contents of the grid array
/// are copied into _lastGrid first, and then serialization occurs.
/// </summary>
/// <param name="info"></param>
/// <param name="ctxt"></param>
public void GetObjectData( SerializationInfo info, StreamingContext ctxt )
{
if ( !_evaluated )
{
for ( int row = 0; row < _rows; row++ )
{
for ( int col = 0; col < _columns; col++ )
_lastGrid[row, col] = _cellGrid[row, col].IsAlive;
}
}
info.AddValue( "_rows", _rows );
info.AddValue( "_columns", _columns );
info.AddValue( "_startingGrid", _startingGrid );
info.AddValue( "_lastGrid", _lastGrid );
info.AddValue( "_evaluated", _evaluated );
info.AddValue( "_gridType", _gridType );
info.AddValue( "_modelName", _modelName );
info.AddValue( "_peakPopulation", _peakPopulation );
}
/// <summary>
/// StreamCells(Stream)
///
/// Writes out the current model grid to the passed in Stream in .cells
/// format compatible with the format on the Life Lexicon website. Note
/// that this format doesn't store any of the additional information, such
/// as grid type and starting state, that the .avl format saves.
/// </summary>
/// <param name="str"></param>
public void StreamCells( Stream str )
{
str.WriteByte( 0x21 );
byte[] bytes;
if ( _modelName != null && _modelName.Length > 0 )
bytes = Encoding.UTF8.GetBytes( "Name: " + _modelName );
else
bytes = Encoding.UTF8.GetBytes( "Name: untitled" );
str.Write( bytes, 0, bytes.Length );
str.WriteByte( 0x0a );
str.WriteByte( 0x21 );
str.WriteByte( 0x0a );
for ( int row = 0; row < _rows; row++ )
{
for ( int col = 0; col < _columns; col++ )
{
if ( _cellGrid[row, col].IsAlive )
str.WriteByte( 0x4f );
else
str.WriteByte( 0x2e );
}
str.WriteByte( 0x0a );
}
}
/// <summary>
/// ResizeModel(int, int)
///
/// This function performs a resizing of the grid according to the specified
/// dimensions. It does not resize the model, i.e. the current set of live cells
/// but will center the model in the new grid if it is larger. If this function
/// is passed 0 for the rows and columns it will shrink the grid to the size
/// of the current model. The function will return false if the specific requested
/// grid size is too small to contain the current set of live cells.
/// </summary>
/// <param name="newRows"></param>
/// <param name="newCols"></param>
/// <returns></returns>
public bool ResizeModel( int newRows, int newCols )
{
int copyRow = 0;
int copyCol = 0;
int currRows = 0;
int currCols = 0;
// if the dimensions haven't changed bail
if ( newRows == _rows && newCols == _columns )
return true;
// if the requested dimensions are larger we can just expand and center
// the model on the grid
if ( newRows > _rows && newCols > _columns )
{
copyRow = (newRows - _rows) / 2;
copyCol = (newCols - _columns) / 2;
bool[,] tempLifeGrid = new bool[_rows, _columns];
bool[,] tempStartGrid = _lastGrid;
for ( int row = 0; row < _rows; row++ )
{
for ( int col = 0; col < _columns; col++ )
{
tempLifeGrid[row, col] = _cellGrid[row, col].IsAlive;
tempStartGrid[row, col] = _startingGrid[row, col];
}
}
currRows = _rows;
currCols = _columns;
InitArrays(newRows, newCols);
BuildWorkGrid();
for ( int row = 0; row < currRows; row++ )
{
for ( int col = 0; col < currCols; col++ )
{
_cellGrid[row + copyRow, col + copyCol].IsAlive = tempLifeGrid[row, col];
_startingGrid[row + copyRow, col + copyCol] = tempStartGrid[row, col];
}
}
return true;
}
// if we get here either the requested grid is smaller than the current
// one, or the user has asked for the grid to be shrunk to the model. In
// either case we first need to know the extent of the current set of live
// cells.
ALRectangle rect = GetModelExtent();
currRows = (rect.Bottom - rect.Top) + 1;
currCols = (rect.Right - rect.Left) + 1;
if ( newRows == 0 ) newRows = currRows;
if ( newCols == 0 ) newCols = currCols;
if ( newRows >= currRows && newCols >= currCols )
{
copyRow = (newRows - currRows) / 2;
copyCol = (newCols - currCols) / 2;
bool[,] tempLifeGrid = new bool[_rows, _columns];
bool[,] tempStartGrid = _lastGrid;
for (int row = 0; row < _rows; row++)
{
for (int col = 0; col < _columns; col++)
{
tempLifeGrid[row, col] = _cellGrid[row, col].IsAlive;
tempStartGrid[row, col] = _startingGrid[row, col];
}
}
InitArrays(newRows, newCols);
BuildWorkGrid();
for (int row = 0; row < currRows; row++)
{
for (int col = 0; col < currCols; col++)
{
_cellGrid[row + copyRow, col + copyCol].IsAlive = tempLifeGrid[row + rect.Top, col + rect.Left];
_startingGrid[row + copyRow, col + copyCol] = tempStartGrid[row + rect.Top, col + rect.Left];
}
}
return true;
}
return false;
}
#endregion
/// <summary>
/// IsEmpty()
///
/// Returns false if at least one cell in the grid is alive
/// </summary>
/// <returns></returns>
public bool IsEmpty()
{
bool empty = true;
for ( int row = 0; row < _rows; row++ )
{
for ( int col = 0; col < _columns; col++ )
{
if ( _cellGrid[row, col].IsAlive )
{
empty = false;
break;
}
}
}
return empty;
}
/// <summary>
/// Called by the LifeSim class to iterate through the array and apply the game
/// rules once.
/// </summary>
public void Evaluate()
{
bool[,] temp = new bool[_rows, _columns];
int population = 0;
if ( !_evaluated )
{
_evaluated = true;
for ( int row = 0; row < _rows; row++ )
{
for ( int col = 0; col < _columns; col++ )
{
_startingGrid[row, col] = _lastGrid[row, col] = _cellGrid[row, col].IsAlive;
if ( _startingGrid[row, col] )
_peakPopulation++;
}
}
PeakPopulation = _peakPopulation;
}
for( int row = 0; row < _rows; row++ )
{
for ( int col = 0; col < _columns; col++ )
{
_lastGrid[row, col] = _cellGrid[row, col].IsAlive;
int adj = CountAdjacent( row, col );
if ( _cellGrid[row, col].IsAlive )
{
if ( adj == 2 || adj == 3 )
temp[row, col] = true;
else
CellDeaths++;
}
else
{
if ( adj == 3 )
{
temp[row, col] = true;
CellBirths++;
}
}
}
}
for ( int row = 0; row < _rows; row++ )
{
for ( int col = 0; col < _columns; col++ )
{
if ( temp[row, col] == true )
{
_cellGrid[row, col].IsAlive = true;
population++;
}
else
_cellGrid[row, col].IsAlive = false;
}
}
Population = population;
if ( _peakPopulation < population )
PeakPopulation = population;
if ( AreEqualGrids(_cellGrid, _lastGrid) )
_evoHalted = true;
}
/// <summary>
/// ResetModel()
///
/// Resets the life model to the starting state by restoring the values in
/// _startingGrid to the cell array.
/// </summary>
public void ResetModel()
{
_evaluated = false;
CellBirths = 0;
CellDeaths = 0;
_evoHalted = false;
Population = 0;
PeakPopulation = 0;
for ( int row = 0; row < _rows; row++ )
{
for ( int col = 0; col < _columns; col++ )
_cellGrid[row, col].IsAlive = _startingGrid[row, col];
}
}
#endregion
#region LifeModel private methods
/// <summary>
/// GetModelExtent()
///
/// Returns an ALRectangle structure with the extent of the current
/// set of life cells.
/// </summary>
/// <returns></returns>
private ALRectangle GetModelExtent()
{
ALRectangle rect;
rect.Left = _columns / 2;
rect.Top = _rows / 2;
rect.Right = rect.Left + 1;
rect.Bottom = rect.Top + 1;
for ( int row = 0; row < _rows; row++ )
{
for ( int col = 0; col < _columns; col++ )
{
if ( _cellGrid[row, col].IsAlive )
{
if (row < rect.Top ) rect.Top = row;
if (row > rect.Bottom ) rect.Bottom = row;
if (col < rect.Left) rect.Left = col;
if (col > rect.Right) rect.Right = col;
}
}
}
return rect;
}
/// <summary>
/// AreEqualGrids(LifeCell[,], bool[,])
///
/// Called from Evaluate() to determine if evolution has ceased or the sim
/// has fallen into an oscillating pattern. Compares an array of LifeCells
/// to a backup array of booleans and returns true if they are the same
/// </summary>
/// <param name="lc"></param>
/// <param name="b"></param>
/// <returns></returns>
private bool AreEqualGrids( LifeCell[,] lc, bool[,] b )
{
bool result = true;
for ( int row = 0; row < _rows; row++ )
{
for ( int col = 0; col < _columns; col++ )
{
if ( lc[row, col].IsAlive != b[row, col] )
{
result = false;
break;
}
}
}
return result;
}
/// <summary>
/// CountAdjacent(int, int)
///
/// This function counts neighbors using the work grid, which returns
/// the correct values for edge cells depending on the grid type. The work
/// grid is two cells larger in both dimensions, with the outer cells used
/// for correct wrapping of neighbor checks, as set up in BuildWorkGrid().
/// The incoming coordinates are in the _cellGrid space, so this function
/// shifts them down and right 1 cell to get to _workGrid space.
/// </summary>
/// <param name="row"></param>
/// <param name="column"></param>
/// <returns></returns>
private int CountAdjacent(int row, int column)
{
int count = 0;
row++;
column++;
// upper left
if ( _workGrid[row - 1, column - 1].IsAlive )
count++;
if ( _workGrid[row - 1, column].IsAlive )
count++;
// upper right
if ( _workGrid[row - 1, column + 1].IsAlive )
count++;
// left
if ( _workGrid[row, column - 1].IsAlive )
count++;
// right
if ( _workGrid[row, column + 1].IsAlive )
count++;
// lower left
if ( _workGrid[row + 1, column - 1].IsAlive )
count++;
// lower middle
if ( _workGrid[row + 1, column].IsAlive )
count++;
// lower right
if ( _workGrid[row + 1, column + 1].IsAlive )
count++;
return count;
}
/// <summary>
/// BuildWorkGrid()
///
/// The work grid is two cells larger in each dimension, with the edge cells being
/// used for controlling how the model wraps. This method builds the work grid off
/// of the _cellGrid taking the GridType into account.
/// </summary>
private void BuildWorkGrid()
{
_workGrid = new LifeCell[_rows + 2, _columns + 2];
for ( int row = 0; row < _rows + 2; row++ )
{
for ( int col = 0; col < _columns + 2; col++ )
{
// Handle the corner conditions. A corner cell can only be
// alive in the case of a torus. In all other grid types it
// will be dead by one of the other edges. In the case of a
// torus it wraps to the opposite corner.
if ( row == 0 && col == 0 )
{
switch (_gridType)
{
case GridType.Torus:
_workGrid[row, col] = _cellGrid[_rows - 1, _columns - 1];
break;
default:
_workGrid[row, col] = new LifeCell();
break;
}
}
else if ( row == 0 && col == _columns + 1 )
{
switch (_gridType)
{
case GridType.Torus:
_workGrid[row, col] = _cellGrid[_rows - 1, 0];
break;
default:
_workGrid[row, col] = new LifeCell();
break;
}
}
else if (row == _rows + 1 && col == _columns + 1)
{
switch (_gridType)
{
case GridType.Torus:
_workGrid[row, col] = _cellGrid[0, 0];
break;
default:
_workGrid[row, col] = new LifeCell();
break;
}
}
else if (row == _rows + 1 && col == 0)
{
switch (_gridType)
{
case GridType.Torus:
_workGrid[row, col] = _cellGrid[0, _columns - 1];
break;
default:
_workGrid[row, col] = new LifeCell();
break;
}
}
// Handle the non-corner edges. They are dead in the
// finite case, or the case where they lie along the top/bottom
// in an x cylinder grid, or the left/right in a y cylinder grid.
// Otherwise they wrap to the cell on the opposite side.
else if (row == 0)
{
switch( _gridType )
{
case GridType.Finite:
case GridType.XCylinder:
_workGrid[row, col] = new LifeCell();
break;
case GridType.Torus:
case GridType.YCylinder:
_workGrid[row, col] = _cellGrid[_rows - 1, col - 1];
break;
}
}
else if ( row == _rows + 1 )
{
switch( _gridType )
{
case GridType.Finite:
case GridType.XCylinder:
_workGrid[row, col] = new LifeCell();
break;
case GridType.Torus:
case GridType.YCylinder:
_workGrid[row, col] = _cellGrid[0, col - 1];
break;
}
}
else if ( col == 0 )
{
switch (_gridType)
{
case GridType.Finite:
case GridType.YCylinder:
_workGrid[row, col] = new LifeCell();
break;
case GridType.Torus:
case GridType.XCylinder:
_workGrid[row, col] = _cellGrid[row - 1, _columns - 1];
break;
}
}
else if ( col == _columns + 1 )
{
switch (_gridType)
{
case GridType.Finite:
case GridType.YCylinder:
_workGrid[row, col] = new LifeCell();
break;
case GridType.Torus:
case GridType.XCylinder:
_workGrid[row, col] = _cellGrid[row - 1, 0];
break;
}
}
else
_workGrid[row, col] = _cellGrid[row - 1, col - 1];
}
}
}
/// <summary>
/// InitArrays(int, int)
///
/// Called from the LifeModel constructors to create the LifeCell array and assign
/// values to related private members
/// </summary>
/// <param name="columns">int, the width (columns) of the grid to be created</param>
/// <param name="rows">int, the height (rows) of the grid to be created</param>
private void InitArrays(int rows, int columns )
{
if ( columns <= 0 || rows <= 0 )
throw( new System.ArgumentOutOfRangeException("InitArrays") );
_columns = columns;
_rows = rows;
_cellGrid = new LifeCell[_rows, _columns];
for ( int row = 0; row < _rows; row++ )
{
for ( int col = 0; col < _columns; col++ )
_cellGrid[row, col] = new LifeCell();
}
BuildWorkGrid();
_startingGrid = new bool[_rows, _columns];
_lastGrid = new bool[_rows, _columns];
}
#endregion
#region LifeModel Properties
/// <summary>
/// The number of columns across the life grid
/// </summary>
private int _columns = 0;
public int Columns
{
get
{
return _columns;
}
}
/// <summary>
/// The number of rows down the life grid
/// </summary>
private int _rows = 0;
public int Rows
{
get
{
return _rows;
}
}
/// <summary>
/// LifeCell
///
/// This property gives access to the array of LifeCell objects so that they can be used
/// as data context in binding to the UI. Not sure I like this design.
/// </summary>
private LifeCell[,] _cellGrid = null;
public LifeCell[,] CellGrid
{
get
{
if ( _cellGrid != null )
return _cellGrid;
else
throw( new System.InvalidOperationException("LifeCell_get") );
}
}
/// <summary>
/// Evaluated
///
/// True if the model has been evaluated at least once, meaning that the data
/// in _startingGrid is valid
/// </summary>
bool _evaluated = false;
public bool Evaluated
{
get
{
return _evaluated;
}
}
/// <summary>
/// CellBirths
///
/// Counts the number of cell births. Fires change notification.
/// </summary>
private int _cellBirths = 0;
public int CellBirths
{
get
{
return _cellBirths;
}
protected set
{
_cellBirths = value;
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs("CellBirths"));
}
}
/// <summary>
/// CellDeaths
///
/// Counts the number of cell deaths. Fires change notification.
/// </summary>
private int _cellDeaths = 0;
public int CellDeaths
{
get
{
return _cellDeaths;
}
protected set
{
_cellDeaths = value;
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs("CellDeaths"));
}
}
/// <summary>
/// EvolutionHalted
///
/// True if the model evolution has halted
/// </summary>
private bool _evoHalted = false;
public bool EvolutionHalted
{
get
{
return _evoHalted;
}
}
/// <summary>
/// PeakPopulation
///
/// Tracks the maximum population of cells on the grid. Fires
/// change notification.
/// </summary>
private int _peakPopulation = 0;
public int PeakPopulation
{
get
{
return _peakPopulation;
}
protected set
{
_peakPopulation = value;
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs("PeakPopulation"));
}
}
/// <summary>
/// Population
///
/// Tracks the current population of the grid at the close of each tick.
/// Fires change notification.
/// </summary>
private int _population = 0;
public int Population
{
get
{
return _population;
}
protected set
{
_population = value;
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs("Population"));
}
}
/// <summary>
/// GridType
///
/// Used to set the bounding behavior of the cell grid according to the
/// GridType enumeration.
/// </summary>
private GridType _gridType = ALSettings.Default.DefaultGridType;
public GridType LifeGridType
{
get
{
return _gridType;
}
set
{
_gridType = value;
BuildWorkGrid();
}
}
/// <summary>
/// Contains the name (title) of the current model
/// </summary>
private string _modelName = null;
public string ModelName
{
get
{
return _modelName;
}
set
{
_modelName = value;
}
}
#endregion
#region INotifyPropertyChanged members
public event PropertyChangedEventHandler PropertyChanged;
#endregion
#region LifeModel private data
// holds the starting grid state so we can revert on demand
private bool[,] _startingGrid = null;
// holds the state of the last calculate grid, used to
// detect a halt
private bool[,] _lastGrid = null;
// holds the working grid, which is two cells larger than
// the cell grid on each dimension, with the edge cells
// being set up to wrap correctly according to the
// grid type. See BuildWorkGrid()
private LifeCell[,] _workGrid = null;
#endregion
}
}