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 } }