Home » Programming » .NET Programming » Silverlight 3 / Expression 3 behaviors for enabling Multi-touch gestures

    In the previous post, I’ve described a basic example of WPF custom control for reusing multi-touch functionalities across applications. The same task can be achieved in Silverlight 3 using the new touch apis (check out this great article by Tim Heuer to get started), in addition we can use the new behaviors in order to write and simply reuse the code (a quick note: at this stage Silverlight 3 only supports basic touch events, gesture messages are not available).

    To start I’ve added two simple behaviors named TouchDrag and TouchZoom to the Multi-Touch Codeplex project.

    These new objects can be used in xaml by inserting the following code (the System.Windows.Interactivity namespace can be imported from the Expression Blend 3 SDK, already available if Blend 3 is installed):

    <UserControl x:Class="SilverlightMultiTouch.MainPage"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/ presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d= "http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/ markup-compatibility/2006"
        mc:Ignorable="d" d:DesignWidth="640" d:DesignHeight="480" Width="1280" Height="800"
        xmlns:myControls="clr-namespace:MultiTouch.Controls.Silverlight; assembly=MultiTouch.Controls.Silverlight"
        xmlns:behaviors="clr-namespace: MultiTouch.Controls.Silverlight.Behaviors; assembly=MultiTouch.Controls.Silverlight"
        xmlns:interactivity="clr-namespace: System.Windows.Interactivity; assembly=System.Windows.Interactivity"
                 >
        <Canvas Background="Black">
            <!-- Example using Behaviors -->
            <Canvas>
                <interactivity:Interaction.Behaviors>
                    <behaviors:TouchDrag TouchDragEnabled="True"/>
                    <behaviors:TouchZoom TouchZoomEnabled="True"/>
                </interactivity:Interaction.Behaviors>
                <Image Source="Images/image.png" Height="597" Width="448" Canvas.Left="404" Canvas.Top="96"/>
            </Canvas>
        </Canvas>
    </UserControl>
    

    Have you seen the previous lines of code? The touch features are simply associated to the Image object using this xaml:

                <interactivity:Interaction.Behaviors>
                    <behaviors:TouchDrag TouchDragEnabled="True"/>
                    <behaviors:TouchZoom TouchZoomEnabled="True"/>
                </interactivity:Interaction.Behaviors>
    

    The TouchDrag and TouchZoom behaviors can be defined in code-behind in this way:

    TouchDrag.cs

        public class TouchDrag : Behavior<FrameworkElement>
        {
            //Dependency property for enabling the TouchDrag
            public static readonly DependencyProperty TouchDragEnabledProperty =
                DependencyProperty.Register("TouchDragEnabled", typeof(bool), typeof(TouchDrag), new PropertyMetadata(false));
    
            [Category("Target Properties")]
            public bool TouchDragEnabled { get; set; }
    
            //Occurs when the primary point is touched down
            private bool _touchDown = false;
    
            //Origin point
            private Point _ptOrigin;
    
            //Translate Transform
            TranslateTransform _translateTransform;
    
            /// Initialize the behavior
            protected override void OnAttached()
            {
                base.OnAttached();
                Touch.FrameReported += AssociatedObjectTouchFrameReported;
            }
    
            /// Occurs when detaching the behavior
            protected override void OnDetaching()
            {
                base.OnDetaching();
                Touch.FrameReported -= AssociatedObjectTouchFrameReported;
            }
    
            /// Touch Frame Handler
            private void AssociatedObjectTouchFrameReported(object sender, TouchFrameEventArgs e)
            {
                //Handler for the Touch events
                if (this.TouchDragEnabled)
                {
                    if (e.GetPrimaryTouchPoint(AssociatedObject) != null)
                    {
                        switch (e.GetPrimaryTouchPoint( AssociatedObject).Action)
                        {
                            case TouchAction.Up:
                                this.AssociatedObjectTouchUp(sender, e);
                                break;
                            case TouchAction.Move:
                                this.AssociatedObjectTouchMove(sender, e);
                                break;
                            case TouchAction.Down:
                                this.AssociatedObjectTouchDown(sender, e);
                                break;
                        }
                    }
                }
            }
    
            /// Touch Down Handler
            private void AssociatedObjectTouchDown(object sender, TouchFrameEventArgs e)
            {
                this._touchDown = true;
                this.AssociatedObject.Opacity = .75;
                this._ptOrigin = e.GetPrimaryTouchPoint(AssociatedObject).Position;
    
                //Initialize the Translate transform and the TransformGroup
                if (this._translateTransform == null)
                {
                    this._translateTransform = new TranslateTransform();
                    if ((this.AssociatedObject.RenderTransform == null) || !(this.AssociatedObject.RenderTransform is TransformGroup))
                        this.AssociatedObject.RenderTransform = new TransformGroup();
                    if (this.AssociatedObject.RenderTransform is TransformGroup)
                        (this.AssociatedObject.RenderTransform as TransformGroup).Children.Add( this._translateTransform);
                }
    
                //Suspend Mouse promotion
                e.SuspendMousePromotionUntilTouchUp();
            }
    
            /// Handle the Touch movement
            private void AssociatedObjectTouchMove(object sender, TouchFrameEventArgs e)
            {
                if (this._touchDown)
                {
                    //Find the new position
                    Point newPosition = e.GetPrimaryTouchPoint(AssociatedObject.Parent as FrameworkElement).Position;
    
                    //Drag the Control using the TranslateTransform
                    this._translateTransform.X = (newPosition.X - _ptOrigin.X); this._translateTransform.Y = (newPosition.Y - _ptOrigin.Y);
                }
            }
    
            /// Touch Up Handler
            private void AssociatedObjectTouchUp(object sender, TouchFrameEventArgs e)
            {
                // turn off drag
                this._touchDown = false;
                AssociatedObject.Opacity = 1;
            }
        }
    

    TouchZoom.cs:

        public class TouchZoom : Behavior<FrameworkElement>
        {
            //Dependency property for enabling the TouchZoom
            public static readonly DependencyProperty TouchDragEnabledProperty =
                DependencyProperty.Register("TouchZoomEnabled", typeof(bool), typeof(TouchZoom), new PropertyMetadata(false));
    
            [Category("Target Properties")]
            public bool TouchZoomEnabled { get; set; }
    
            //Occurs when the primary point is touched down
            private bool _touchDown = false;
    
            //Origin points
            private TouchPointCollection _tpInitialPoints;
    
            //ScaleTransform
            ScaleTransform _scaleTransform;
    
            /// Calculate the distance between two points
            private double Distance(Point point1, Point point2)
            {
                try
                {
                    return Math.Sqrt((point1.X - point2.X) * (point1.X - point2.X) + (point1.Y - point2.Y) * (point1.Y - point2.Y));
                }
                catch { return double.NaN; }
            }
    
            /// Calculate the MidPoint
            private Point MidPoint(Point point1, Point point2)
            {
                return new Point((point1.X + point2.X) / 2, (point1.Y + point2.Y) / 2);
            }
    
            /// Initialize the behavior
            protected override void OnAttached()
            {
                base.OnAttached();
                Touch.FrameReported += AssociatedObjectTouchFrameReported;
            }
    
            /// Occurs when detaching the behavior
            protected override void OnDetaching()
            {
                base.OnDetaching();
                Touch.FrameReported -= AssociatedObjectTouchFrameReported;
            }
    
            /// Touch Frame Handler
            private void AssociatedObjectTouchFrameReported(object sender, TouchFrameEventArgs e)
            {
                if (this.TouchZoomEnabled)
                {
                    TouchPointCollection tpCollection = e.GetTouchPoints(this.AssociatedObject);
                    tpCollection.ToList().ForEach(tp =>
                        {
                            switch (tp.Action)
                            {
                                case TouchAction.Up:
                                    this.AssociatedObjectTouchUp(sender, e);
                                    break;
                                case TouchAction.Move:
                                    this.AssociatedObjectTouchMove(sender, e);
                                    break;
                                case TouchAction.Down:
                                    this.AssociatedObjectTouchDown(sender, e);
                                    break;
                            }
                        });
                }
            }
    
            /// Touch Down Handler
            private void AssociatedObjectTouchDown(object sender, TouchFrameEventArgs e)
            {
                //Initialize the parameters
                this._touchDown = true;
                this.AssociatedObject.Opacity = .75;
                this._tpInitialPoints = e.GetTouchPoints(this.AssociatedObject);
    
                //Initialize the Scale transform
                if (this._scaleTransform == null)
                {
                    this._scaleTransform = new ScaleTransform();
                    if ((this.AssociatedObject.RenderTransform == null) || !(this.AssociatedObject.RenderTransform is TransformGroup))
                        this.AssociatedObject.RenderTransform = new TransformGroup();
                    if (this.AssociatedObject.RenderTransform is TransformGroup)
                        (this.AssociatedObject.RenderTransform as TransformGroup).Children.Add( this._scaleTransform);
                }
                //TODO: e.SuspendMousePromotionUntilTouchUp();
            }
    
            /// Handle the Touch movement
            private void AssociatedObjectTouchMove(object sender, TouchFrameEventArgs e)
            {
                if (this._touchDown)
                {
                    //Find the new points
                    TouchPointCollection newPoints = e.GetTouchPoints(AssociatedObject.Parent as FrameworkElement);
                    if (newPoints!=null && this._tpInitialPoints!=null)
                        if (this._tpInitialPoints.Count == 2 && newPoints.Count == 2)
                        {
                            //Zoom the Control using the ScaleTransform
                            double ratio = this.Distance( newPoints.First().Position, newPoints.Last().Position) /
                                this.Distance( this._tpInitialPoints.First().Position, this._tpInitialPoints.Last().Position);
                            if (ratio != double.NaN)
                            {
                                Point midPoint = this.MidPoint( newPoints.First().Position, newPoints.Last().Position);
                                this._scaleTransform.CenterX = midPoint.X; this._scaleTransform.CenterY = midPoint.Y;
                                this._scaleTransform.ScaleX = ratio; this._scaleTransform.ScaleY = ratio;
                            }
                        }
                }
            }
    
            /// Touch Up Handler
            private void AssociatedObjectTouchUp(object sender, TouchFrameEventArgs e)
            {
               // turn off Zoom mode
               this._touchDown = false;
               this.AssociatedObject.Opacity = 1;
            }
        }
    

    The behavior approach is very powerful, in this way you can share functionalities between Visual Studio and Expression Blend 3 in a simple way, making the code maintainable and simply reusable in different projects.

    The source code is available for download on http://multitouch.codeplex.com.

    If you have a multi-touch enabled devices, the demo application is available on-line here, featuring Drag and Zoom gestures.

    Hope this helps and… happy Silverlighting!

    comments

    1. [...] source code is available on Codeplex, including a basic Silverlight 3 implementation using the new touch APIs and [...]

    2. [...] Silverlight 3 / Expression 3 behaviors for enabling Multi-touch gestures Yay! This is fantastic work (tags: Multi-touch touch .net windows) [...]

    3. [...] similar this, where interactivity represents the System.Windows.Interactivity namespace (see my previous post for the source code of these [...]

    4. Imran Shaik says:

      These behaviours came a bit too late, I have already written my own been banging my head on if at all its possible to do HitTesting. I guess hit testing is more imporatant and relative than anything else right now.

      Would love to see an Multitouch Application where it have more than one item and allows to interact with independent elements.

    5. Davide says:

      Hi Imran,
      agree with you, I’m waiting for the possibility to interact with more elements and then indipendently capture the touch events for each UIEement!

    6. Richard says:

      Hay ho, does the WPF sample only run on Win7 because I get an exception telling the following:

      _comManipulationProcessor = new ManipulationInterop.ManipulationProcessor();
      <—
      Retrieving the COM class factory for component with CLSID {597D4FB0-47FD-4AFF-89B9-C6CFAE8CF08E} failed due to the following error: 80040154

    7. Davide says:

      Hi Richard,
      yes, the sample only runs on Win7.

    Leave a Reply

    Your email address will not be published. Required fields are marked *

    You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>