This post describes the design of the PanView custom control, discussing both the C# and XAML that achieves the desired functionality.
Posts in this series:
PanView is a custom control with the following dependency properties:
TransformGroup TransformGroup
– The transform to be applied to the canvas double MinTranslateX
– A translation constraint double MaxTranslateX
– A translation constraint double MinTranslateY
– A translation constraint double MaxTranslateY
– A translation constraint
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:PanViewLibrary">
<Style
TargetType="local:PanView">
<Setter
Property="Template">
<Setter.Value>
<ControlTemplate
TargetType="local:PanView">
<Grid
Background="Transparent">
<ContentPresenter
RenderTransform="{TemplateBinding TransformGroup}"/>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
The XAML above shows how the RenderTransform
of the content is bound to the custom control’s TransformGroup
dependency property.
One approach for developing PanView is to keep appending transformations into the TransformGroup
each time the user performs a manipulation. If PanView kept appending each transformation into a transformation group, then the number of transformations would grow each time the user touched the control. This growth would make PanView unreliable in commercial applications. The PanView code demonstrates how to keep the TransformGroup
from growing beyond two transformations.
The public
method Reset
is a quick programmatic way of resetting the transformation back to the default.
The method ConstrainDelta
is employed to enforce the user-defined constraints on x/y panning distances.
using Windows.UI.Input;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Input;
using Windows.UI.Xaml.Markup;
using Windows.UI.Xaml.Media;
namespace PanViewLibrary
{
[ContentProperty(Name = "Content")]
public class PanView : ContentControl
{
public PanView()
{
DefaultStyleKey = typeof(PanView);
ManipulationMode = ManipulationModes.All;
_currentTransformation = new CompositeTransform();
_previousTransformations = new MatrixTransform() { Matrix = Matrix.Identity };
TransformGroup = new TransformGroup();
TransformGroup.Children.Add(_previousTransformations);
TransformGroup.Children.Add(_currentTransformation);
ManipulationStarting += (sender, args) => { args.Handled = true; };
ManipulationStarted += OnManipulationStarted;
ManipulationDelta += OnManipulationDelta;
ManipulationCompleted += (sender, args) => { args.Handled = true; };
ManipulationInertiaStarting += (sender, args) => { args.Handled = true; };
MinTranslateX = double.MinValue;
MaxTranslateX = double.MaxValue;
MinTranslateY = double.MinValue;
MaxTranslateY = double.MaxValue;
}
public void Reset()
{
_currentTransformation.Reset();
_previousTransformations.Matrix = Matrix.Identity;
}
CompositeTransform _currentTransformation;
MatrixTransform _previousTransformations;
void OnManipulationStarted(object sender, ManipulationStartedRoutedEventArgs args)
{
_previousTransformations.Matrix = TransformGroup.Value;
_currentTransformation.Reset();
_currentTransformation.CenterX = args.Position.X;
_currentTransformation.CenterY = args.Position.Y;
args.Handled = true;
}
private void OnManipulationDelta(object sender, ManipulationDeltaRoutedEventArgs args)
{
var delta = ConstrainDelta(args.Delta);
_currentTransformation.TranslateX += delta.Translation.X;
_currentTransformation.TranslateY += delta.Translation.Y;
_currentTransformation.Rotation += delta.Rotation;
_currentTransformation.ScaleX *= delta.Scale;
_currentTransformation.ScaleY *= delta.Scale;
args.Handled = true;
}
private ManipulationDelta ConstrainDelta(ManipulationDelta delta)
{
var newTranslateX = _previousTransformations.Matrix.OffsetX +
_currentTransformation.TranslateX + delta.Translation.X;
var tooLittleX = MinTranslateX - newTranslateX;
if (tooLittleX > 0)
{
delta.Translation.X += tooLittleX;
}
var tooMuchX = newTranslateX - MaxTranslateX;
if (tooMuchX > 0)
{
delta.Translation.X -= tooMuchX;
}
var newTranslateY = _previousTransformations.Matrix.OffsetY +
_currentTransformation.TranslateY + delta.Translation.Y;
var tooLittleY = MinTranslateY - newTranslateY;
if (tooLittleY > 0)
{
delta.Translation.Y += tooLittleY;
}
var tooMuchY = newTranslateY - MaxTranslateY;
if (tooMuchY > 0)
{
delta.Translation.Y -= tooMuchY;
}
return delta;
}
public TransformGroup TransformGroup
{
get { return (TransformGroup)GetValue(TransformGroupProperty); }
private set { SetValue(TransformGroupProperty, value); }
}
public static readonly DependencyProperty TransformGroupProperty =
DependencyProperty.Register("TransformGroup",
typeof(TransformGroup), typeof(PanView), new PropertyMetadata(null));
public double MinTranslateX
{
get { return (double)GetValue(MinTranslateXProperty); }
set { SetValue(MinTranslateXProperty, value); }
}
public static readonly DependencyProperty MinTranslateXProperty =
DependencyProperty.Register("MinTranslateX",
typeof(double), typeof(PanView), new PropertyMetadata(null));
public double MaxTranslateX
{
get { return (double)GetValue(MaxTranslateXProperty); }
set { SetValue(MaxTranslateXProperty, value); }
}
public static readonly DependencyProperty MaxTranslateXProperty =
DependencyProperty.Register("MaxTranslateX",
typeof(double), typeof(PanView), new PropertyMetadata(null));
public double MinTranslateY
{
get { return (double)GetValue(MinTranslateYProperty); }
set { SetValue(MinTranslateYProperty, value); }
}
public static readonly DependencyProperty MinTranslateYProperty =
DependencyProperty.Register("MinTranslateY",
typeof(double), typeof(PanView), new PropertyMetadata(null));
public double MaxTranslateY
{
get { return (double)GetValue(MaxTranslateYProperty); }
set { SetValue(MaxTranslateYProperty, value); }
}
public static readonly DependencyProperty MaxTranslateYProperty =
DependencyProperty.Register("MaxTranslateY",
typeof(double), typeof(PanView), new PropertyMetadata(null));
}
}
The code above is the entirety of the PanView custom control.
We construct the TransformGroup
to consist of the two transforms, previousTransformations
and currentTransformation
.
When starting a manipulation, we make sure to copy (flatten) the TransformGroup
’s end result (Value
) into _previousTransformations
. We also reset _currentTransformation
. At this point, the TransformGroups
’ end result should be the same.
We record the center point of the manipulation at the start of the manipulation. I’ve seen some code that places this assignment in the ManipulationDelta
handler. As far as I can tell on my Samsung Slate, the end result of that variation is very shaky and unreliable performance.
For each ManipulationDelta
, we simply update currentTransformation
.
That’s it!
John Hauck has been developing software professionally since 1981, and focused on Windows-based development since 1988. For the past 17 years John has been working at LECO, a scientific laboratory instrument company, where he manages software development. John also served as the manager of software development at Zenith Data Systems, as the Vice President of software development at TechSmith, as the lead medical records developer at Instrument Makar, as the MSU student who developed the time and attendance system for Dart container, and as the high school kid who wrote the manufacturing control system at Wohlert. John loves the Lord, his wife, their three kids, and sailing on Lake Michigan.