WPF Series -10- InkCanvas with initial StrokeCollection
One of the requirements our WPF project had to meet describes the ability to create a sketch of the sewage shaft for which data is collected. The functional acceptance criteria […]
Audit and Consulting of Information Systems and Business Processes
One of the requirements our WPF project had to meet describes the ability to create a sketch of the sewage shaft for which data is collected. The functional acceptance criteria […]
One of the requirements our WPF project had to meet describes the ability to create a sketch of the sewage shaft for which data is collected. The functional acceptance criteria are the following.
After some research we decided to use InkCanvas
control to realize the sketches area. InkCanvas
class defines a property called Strokes
, which is of type StrokeCollection
, that holds all the stroke objects contained within the InkCanvas
. To support storing and reloading (i.e. to/from a file) StrokeCollection
can be saved to a MemoryStream
and then the MemoryStrema
can be written to a byte array. However to meet all the acceptance criteria we somehow had to add the initial geometric figures. Unfortunately it does not seem possible to transform predefined geometric figures like circles and ellipses to strokes for adding them to a StrokeCollection
so we decided to create these strokes manually.
Let’s have a look at the code.
StrokeCollectionBuilder.cs
We first implemented a class called StrokeCollectionBuilder
that allows building the initial geometric figures. For performance reasons the unit circle and unit ellipse points get calculated in the static constructor using angular methods of System.Math
class (i.e. Math.Sin(...)
and Math.Cos()
). The StrokeCollectionBuilder
class implements the builder pattern and provides methods to build and add the necessary strokes to the private StrokeCollection
property. The resulting StrokeCollection
can be returned by calling GetStrokeCollection()
method.
/** * Copyright 2017 d-fens GmbH * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ using System; using System.Collections.Generic; using System.Diagnostics.Contracts; using System.Windows.Ink; using System.Windows.Input; using biz.dfch.CS.Commons.Linq; namespace biz.dfch.CS.ArbitraryWpfApplication.UI.Geometry { public class StrokeCollectionBuilder { private const int FULL_CIRCLE_ANGLE = 360; private static readonly ICollection<Tuple<double, double>> _unitCirclePoints; private static readonly ICollection<Tuple<double, double>> _unitEllipsePoints; private readonly StrokeCollection strokeCollection; static StrokeCollectionBuilder() { _unitCirclePoints = new List<Tuple<double, double>>(); for (var angle = 0; angle < FULL_CIRCLE_ANGLE; angle++) { var x = Math.Sin(angle); var y = Math.Cos(angle); _unitCirclePoints.Add(new Tuple<double, double>(x, y)); } _unitEllipsePoints = new List<Tuple<double, double>>(); for (var angle = 0; angle < FULL_CIRCLE_ANGLE; angle++) { var x = Math.Cos(angle); var y = Math.Sin(angle); _unitEllipsePoints.Add(new Tuple<double, double>(x, y)); } } public StrokeCollectionBuilder() { strokeCollection = new StrokeCollection(); } public StrokeCollectionBuilder BuildHorizontalStroke(double xStart, double xEnd, double y) { Contract.Requires(xStart >= 0); Contract.Requires(xEnd > xStart); Contract.Requires(y >= 0); var horizontalLinePoints = new StylusPointCollection() { new StylusPoint(xStart, y), new StylusPoint(xEnd, y) }; strokeCollection.Add(new Stroke(horizontalLinePoints)); return this; } public StrokeCollectionBuilder BuildVerticalStroke(double x, double yStart, double yEnd) { Contract.Requires(x >= 0); Contract.Requires(yStart >= 0); Contract.Requires(yEnd > yStart); var verticalLinePoints = new StylusPointCollection() { new StylusPoint(x, yStart), new StylusPoint(x, yEnd) }; strokeCollection.Add(new Stroke(verticalLinePoints)); return this; } public StrokeCollectionBuilder BuildCircleStroke(double centerX, double centerY, double radius) { Contract.Requires(centerX >= radius); Contract.Requires(centerY >= radius); Contract.Requires(radius > 0); var circlePoints = new StylusPointCollection(); _unitCirclePoints.ForEach( ucp => circlePoints.Add( new StylusPoint(centerX + ucp.Item1 * radius, centerY + ucp.Item2 * radius) ) ); var circleStroke = new Stroke(circlePoints); strokeCollection.Add(circleStroke); return this; } public StrokeCollectionBuilder BuildEllipseStroke(double centerX, double centerY, double radiusX, double radiusY) { Contract.Requires(centerX >= radiusX); Contract.Requires(centerY >= radiusY); Contract.Requires(radiusX > 0); Contract.Requires(radiusY > 0); var ellipsePoints = new StylusPointCollection(); _unitEllipsePoints.ForEach( uep => ellipsePoints.Add( new StylusPoint(centerX + uep.Item1 * radiusX, centerY + uep.Item2 * radiusY) ) ); var ellipseStroke = new Stroke(ellipsePoints); strokeCollection.Add(ellipseStroke); return this; } public StrokeCollection GetStrokeCollection() { Contract.Ensures(null != Contract.Result<StrokeCollection>()); return strokeCollection; } } }
EditShaftProtocolViewModel.cs
In the constructors of the corresponding view model class InitialiseSketchData
method gets called to initialise the StrokeCollection
of the InkCanvas
. When loading an existing protocol that contains a StrokeCollection
it gets loaded using MemoryStream
and the resulting StrokeCollection
then gets passed as an argument to the overloaded InitialiseSketchData
method (InitialiseSketchData(new StrokeCollection(memoryStream))
) otherwise the parameterless InitialiseSketchData
method gets called. The parameterless InitialiseSketchData
method calls the overloaded InitialiseSketchData
method by passing an initial StrokeCollection
as an argument that just contains the initial geometric figures (built by the StrokeCollectionBuilder
). In the overloaded InitialiseSketchData
method the StrokeCollection
passed as argument gets assigned to the _sketchData
property and a event handler gets registered to the CollectionChanged
event.
The registered event handler EditShaftProtocolViewModel_SketchDataChanged
converts the StrokeCollection
on every change to a byte array and assigns the byte array to the corresponding model property.
Furthermore there is a command called ClearSketchCommand
that clears the SketchData
property of the ViewModel and reinitialises the SketchData
property with an initial StrokeCollection
.
/** * Copyright 2017 d-fens GmbH * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ using System; using System.Collections; using System.Collections.Generic; using System.Collections.Specialized; using System.ComponentModel.DataAnnotations; using System.Diagnostics.Contracts; using System.IO; using System.Linq; using System.Threading.Tasks; using System.Windows.Ink; using System.Windows.Media.Imaging; using biz.dfch.CS.ArbitraryWpfApplication.UI.Attributes; using biz.dfch.CS.ArbitraryWpfApplication.UI.Commands; using biz.dfch.CS.ArbitraryWpfApplication.UI.Controls; using biz.dfch.CS.ArbitraryWpfApplication.UI.Domain; using biz.dfch.CS.ArbitraryWpfApplication.UI.Domain.Sewer; using biz.dfch.CS.ArbitraryWpfApplication.UI.Geometry; using biz.dfch.CS.ArbitraryWpfApplication.UI.Managers; using biz.dfch.CS.ArbitraryWpfApplication.UI.Pages; using MahApps.Metro.Controls; using MahApps.Metro.Controls.Dialogs; using Validation = biz.dfch.CS.ArbitraryWpfApplication.UI.Constants.Validation; namespace biz.dfch.CS.ArbitraryWpfApplication.UI.ViewModels { public class EditShaftProtocolViewModel : ViewModelBase { internal readonly ShaftProtocol ShaftProtocol; private readonly ProtocolManager protocolManager = new ProtocolManager(); private readonly PhotoManager photoManager = new PhotoManager(); public EditShaftProtocolViewModel() { // ReSharper disable once UseObjectOrCollectionInitializer ShaftProtocol = new ShaftProtocol { Id = string.Empty, Metadata = new Metadata { CreationDate = CurrentApp.CreationDate, MunicipalityName = CurrentApp.MunicipalityName, Operator = CurrentApp.Operator } }; ShaftProtocol.Data.Owner = CurrentApp.Organisation; ShaftProtocol.Data.Maintainer = CurrentApp.Organisation; InitialiseSketchData(); } public EditShaftProtocolViewModel(ShaftProtocol shaftProtocolToBeEdited) { Contract.Requires(null != shaftProtocolToBeEdited); ShaftProtocol = shaftProtocolToBeEdited; _coverSizeOption = CoverDiameter == 0; _shaftSizeOption = ShaftDiameter == 0; if (null != ShaftProtocol.Data.SketchByteData) { using (var memoryStream = new MemoryStream(ShaftProtocol.Data.SketchByteData)) { InitialiseSketchData(new StrokeCollection(memoryStream)); } } else { InitialiseSketchData(); } } private void InitialiseSketchData() { InitialiseSketchData(GetInitialStrokeCollection()); } private void InitialiseSketchData(StrokeCollection strokeCollection) { Contract.Requires(null != strokeCollection); _sketchData = strokeCollection; ((INotifyCollectionChanged)_sketchData).CollectionChanged += EditShaftProtocolViewModel_SketchDataChanged; } ... private StrokeCollection _sketchData; public StrokeCollection SketchData { get { return _sketchData; } set { _sketchData = value; RaisePropertyChangedEvent(nameof(SketchData)); } } ... #region EventHandler void EditShaftProtocolViewModel_SketchDataChanged(object sender, NotifyCollectionChangedEventArgs e) { SketchData = (StrokeCollection) sender; using (var memoryStream = new MemoryStream()) { SketchData.Save(memoryStream); ShaftProtocol.Data.SketchByteData = memoryStream.ToArray(); } } #endregion EventHandler #region Commands public SimpleCommand ClearSketchCommand => new ClearSketchCommand(ClearSketch); private void ClearSketch() { SketchData.Clear(); SketchData.Add(GetInitialStrokeCollection()); } ... private StrokeCollection GetInitialStrokeCollection() { var builder = new StrokeCollectionBuilder(); builder.BuildHorizontalStroke(Constants.Controls.Attribute.INK_CANVAS_START_POS_X, Constants.Controls.Attribute.INK_CANVAS_WIDTH, Constants.Controls.Attribute.INK_CANVAS_HEIGHT / 2); builder.BuildVerticalStroke(Constants.Controls.Attribute.INK_CANVAS_WIDTH / 4, Constants.Controls.Attribute.INK_CANVAS_START_POS_Y, Constants.Controls.Attribute.INK_CANVAS_HEIGHT); builder.BuildVerticalStroke(Constants.Controls.Attribute.INK_CANVAS_WIDTH / 4 * 3, Constants.Controls.Attribute.INK_CANVAS_START_POS_Y, Constants.Controls.Attribute.INK_CANVAS_HEIGHT); builder.BuildCircleStroke(Constants.Controls.Attribute.INK_CANVAS_WIDTH / 4, Constants.Controls.Attribute.INK_CANVAS_HEIGHT / 2, 100.0); builder.BuildEllipseStroke(Constants.Controls.Attribute.INK_CANVAS_WIDTH / 4 * 3, Constants.Controls.Attribute.INK_CANVAS_HEIGHT / 2, 150.00, 100.0); return builder.GetStrokeCollection(); }
EditShaftProtocol.xaml
In XAML we just defined a label, an InkCanvas
control and a button for clearing custom strokes. The InkCanvas
control is bound to the SketchData
property of the ViewModel and the button is bound to the ClearSketchCommand
.
<Label Grid.Row="4" Grid.Column="0" Content="{x:Static p:Resources.Page_EditShaftProtocol_InkCanvas_Sketch__Label}" /> <Border x:Name="CanvasBorder" Grid.Row="5" Grid.Column="0" Grid.ColumnSpan="4" BorderBrush="Black" BorderThickness="2"> <InkCanvas Name="Sketch" Height="250" Width="625" HorizontalAlignment="Stretch" ForceCursor="True" Cursor="Pen" Strokes="{Binding SketchData}" TouchEnter="InkCanvas_TouchEnter" TouchLeave="InkCanvas_TouchLeave" /> </Border> <Button Name="ButtonClearSketch" Grid.Row="6" Grid.Column="3" HorizontalAlignment="Right" VerticalAlignment="Center" Style="{DynamicResource MetroCircleButtonStyle}" Content="{StaticResource appbar_delete}" Command="{Binding ClearSketchCommand}" />
That’s it. Let’s have a look at the result.
Sketches Area with initial StrokeCollection
Sketches Area with custom Strokes
« WPF Series -9- CustomListBox | WPF Series -11- Custom MetroDialog » |
3 Comments »