0
votes

I am trying to address issues with the sensitivity of a capacitive touch screen where WPF buttons are being triggered if the users fingers pass too close to the surface of the screen.

This issue is that many users end up with fingers or parts of their hands, other than their primary touch finger, close to the surface of the screen and this causes incorrect buttons to be triggered.

Adjusting the sensitivity of the screen seems to make little difference to I thought I could try modifying the button pressed events to only trigger a Click if the button is pressed for more than a certain amount of time.

Can anyone explain how I might create a custom button that would have an adjustable 'pressed' time before triggering a Clicked event.

If possible perhaps you would be kind enough to include a very simple C#/WPF application with such a custom button.

EDIT

OK, so I have created a subclassed Button using the code below, as per @kidshaw's answer but I think I must be missing a few things because nothing is getting called except the default Click event.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media.Animation;

namespace AppName
{
    public class TouchButton : Button
    {
        DoubleAnimationUsingKeyFrames _animation;

        public static readonly DependencyProperty DelayElapsedProperty =
         DependencyProperty.Register("DelayElapsed", typeof(double), typeof(TouchButton), new PropertyMetadata(0d));

        public static readonly DependencyProperty DelayMillisecondsProperty =
                DependencyProperty.Register("DelayMilliseconds", typeof(int), typeof(TouchButton), new PropertyMetadata(100));

        public double DelayElapsed
        {
            get { return (double)this.GetValue(DelayElapsedProperty); }
            set { this.SetValue(DelayElapsedProperty, value); }
        }

        public int DelayMilliseconds
        {
            get { return (int)this.GetValue(DelayMillisecondsProperty); }
            set { this.SetValue(DelayMillisecondsProperty, value); }
        }
        private void BeginDelay()
        {
            this._animation = new DoubleAnimationUsingKeyFrames() { FillBehavior = FillBehavior.Stop };
            this._animation.KeyFrames.Add(new EasingDoubleKeyFrame(0, KeyTime.FromTimeSpan(TimeSpan.FromMilliseconds(0)), new CubicEase() { EasingMode = EasingMode.EaseIn }));
            this._animation.KeyFrames.Add(new EasingDoubleKeyFrame(1, KeyTime.FromTimeSpan(TimeSpan.FromMilliseconds(this.DelayMilliseconds)), new CubicEase() { EasingMode = EasingMode.EaseIn }));
            this._animation.Completed += (o, e) =>
            {
                this.DelayElapsed = 0d;
                //this.Command.Execute(this.CommandParameter);    // Replace with whatever action you want to perform
                Console.Beep();
                this.RaiseEvent(new RoutedEventArgs(Button.ClickEvent));
            };

            this.BeginAnimation(DelayElapsedProperty, this._animation);
        }

        private void CancelDelay()
        {
            // Cancel animation
            this.BeginAnimation(DelayElapsedProperty, null);
        }
        private void TouchButton_TouchDown(object sender, System.Windows.Input.TouchEventArgs e)
        {
            this.BeginDelay();
        }

        private void TouchButton_TouchUp(object sender, System.Windows.Input.TouchEventArgs e)
        {
            this.CancelDelay();
        }

    }
}

How does the TouchButton_TouchDown method ever get called ? Don't I have to assign this to the TouchDown even handler somehow?

OK, I added a constructor and set the TouchDown/Up event handlers so that works but the CancelDelay() does not stop the event from being fired. It seems work OK and gets called when the user lift their finger but does not prevent the event from being triggered.

3
couldnt you create a usercontrol with a button, which starts a timer on press, and after a 100ms you fire the click event manually? - Sebastian L
Well the issue is that the Windows touch driver does not seem to differentiate between a very light touch or a long touch. So I need the button to only trigger the event if the button is long pressed (and some experimentation would be required to determine the optimal duration for usability). My guess is a press is around 200 - 300 milliseconds whereas a passing trigger would be less that 200. I guess I could start the timer on Press and stop it on Release and if > X trigger the event. - Duncan Groenewald
thats what i meant ;) - Sebastian L
Rather than use the ClickEvent - create a new one and raise that, lets say DeferredClickEvent. The ClickEvent will be triggered by the button when it is first clicked, by separating it to its own event, you only handle the deferred trigger AFTER the delay. - kidshaw
Thanks I have done this but I had to add a flag IsCancelled which gets set when the TouchUp event is received. So when the animation completes it checks if IsCancelled = true and if not raises the IsTouched event. And then I have to reset IsTouched property to false after raising the event because it must be a momentary property and I use it to animate a colour fade. Now to test it with users on the real touch screen! - Duncan Groenewald

3 Answers

0
votes

A time delay button would be the best option.

I have provided an example in this other stack overflow answer.

It uses an animation to delay triggering a command.

Hope it helps.

Do wpf have touch and gold gesture

0
votes

You could almost certainly come up with a solution to do this. There are two approaches I would look at:

  1. Create a specialisation derived from Button. You would override various handlers to implement your own behaviour.
  2. Create an attached dependency property that subscribes to the preview mouse events. The preview events would allow you to intercept the up/down events to inject your own behaviour before the standard button handling can generate the click events.

Option #1 is probably the easiest to get your head around. The handling to generate click events lives in ButtonBase in both the OnMouseLeftButtonDown and OnMouseLeftButtonUp handlers. If you implement (override) your own version of both these handlers you should be able to fairly easily introduce a timer that only calls OnClick to generate the click event once a certain time has expired since the user pressed (and held down) the button.

PS: If you don't have it already, I highly recommend you get a copy of .NET Reflector. It will allow you to easily view the code for the WPF button implementation. I quickly used it to have a look at the WPF button implementation to get an idea of how it works in order to answer this question.

0
votes

For completeness here is the full solution I used based on @kidshaw's original answer. Might save someone else some time fiddling around pulling the pieces together.

Note that I am getting VS Designer errors with it complaining about not finding the custom classes in the apps namespace. Strangely this does not seem to happen on an earlier version of VS so perhaps its a bug in VS.

TouchButton.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media.Animation;

namespace TouchButtonApp
{
    public class TouchButton : Button
    {
        DoubleAnimationUsingKeyFrames _animation;
        bool _isCancelled = false;

        public static readonly DependencyProperty DelayElapsedProperty =
         DependencyProperty.Register("DelayElapsed", typeof(double), typeof(TouchButton), new PropertyMetadata(0d));

        public static readonly DependencyProperty DelayMillisecondsProperty =
                DependencyProperty.Register("DelayMilliseconds", typeof(int), typeof(TouchButton), new PropertyMetadata(Properties.Settings.Default.ButtonTouchDelay));

        public static readonly DependencyProperty IsTouchedProperty =
 DependencyProperty.Register("IsTouched", typeof(bool), typeof(TouchButton), new PropertyMetadata(false));


        // Create a custom routed event by first registering a RoutedEventID 
        // This event uses the bubbling routing strategy 
        public static readonly RoutedEvent TapEvent = EventManager.RegisterRoutedEvent(
            "Tap", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(TouchButton));


        public TouchButton()
        {
            this.TouchDown +=TouchButton_TouchDown;
            this.TouchUp +=TouchButton_TouchUp;
        }

        // Provide CLR accessors for the event 
        public event RoutedEventHandler Tap
        {
            add { AddHandler(TapEvent, value); }
            remove { RemoveHandler(TapEvent, value); }
        }

        // This method raises the Tap event 
        void RaiseTapEvent()
        {
            if (!_isCancelled)
            {
                //Console.Beep();
                this.IsTouched = true;
                Console.WriteLine("RaiseTapEvent");
                RoutedEventArgs newEventArgs = new RoutedEventArgs(TouchButton.TapEvent);
                RaiseEvent(newEventArgs);
            }
        }

        public bool IsTouched
        {
            get { return (bool)this.GetValue(IsTouchedProperty); }
            set { this.SetValue(IsTouchedProperty, value); }
        }

        public double DelayElapsed
        {
            get { return (double)this.GetValue(DelayElapsedProperty); }
            set { this.SetValue(DelayElapsedProperty, value); }
        }

        public int DelayMilliseconds
        {
            get { return (int)this.GetValue(DelayMillisecondsProperty); }
            set { this.SetValue(DelayMillisecondsProperty, value); }
        }

        //Start the animation and raise the event unless its cancelled
        private void BeginDelay()
        {
            _isCancelled = false;
            Console.WriteLine("BeginDelay ");
            this._animation = new DoubleAnimationUsingKeyFrames() { FillBehavior = FillBehavior.Stop };
            this._animation.KeyFrames.Add(new EasingDoubleKeyFrame(0, KeyTime.FromTimeSpan(TimeSpan.FromMilliseconds(0)), new CubicEase() { EasingMode = EasingMode.EaseIn }));
            this._animation.KeyFrames.Add(new EasingDoubleKeyFrame(1, KeyTime.FromTimeSpan(TimeSpan.FromMilliseconds(this.DelayMilliseconds)), new CubicEase() { EasingMode = EasingMode.EaseIn }));
            this._animation.Completed += (o, e) =>
            {
                this.DelayElapsed = 0d;
                //this.Command.Execute(this.CommandParameter);    // Replace with whatever action you want to perform     

                RaiseTapEvent();
                this.IsTouched = false;
            };

            this.BeginAnimation(DelayElapsedProperty, this._animation);
        }

        private void CancelDelay()
        {
            // Cancel animation
            _isCancelled = true;
            Console.WriteLine("CancelDelay ");
            this.BeginAnimation(DelayElapsedProperty, null);
        }
        private void TouchButton_TouchDown(object sender, System.Windows.Input.TouchEventArgs e)
        {
            this.BeginDelay();
        }

        private void TouchButton_TouchUp(object sender, System.Windows.Input.TouchEventArgs e)
        {
            this.CancelDelay();
        }

    }
}

Custom animation when IsTouched event is triggered in App.xaml

<Style x:Key="characterKeyT" TargetType="{x:Type local:TouchButton}">
    <Setter Property="Focusable" Value="False" />
    <Setter Property="HorizontalContentAlignment" Value="Center"/>
    <Setter Property="VerticalContentAlignment" Value="Center"/>
    <Setter Property="Padding" Value="1"/>
    <Setter Property="Margin" Value="6,4,8,4"/>
    <Setter Property="FontSize" Value="24"/>
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type local:TouchButton}">
                <Grid x:Name="grid">
                    <Border x:Name="border" CornerRadius="0">                                
                        <Border.Background>
                            <SolidColorBrush x:Name="BackgroundBrush" Color="{Binding Source={StaticResource settingsProvider}, Path=Default.ThemeColorPaleGray2}"/>
                        </Border.Background>
                        <ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center" TextElement.Foreground="Black" 
                                          TextElement.FontSize="24"></ContentPresenter>
                    </Border>
                </Grid>
                <ControlTemplate.Resources>
                    <Storyboard x:Key="FadeTimeLine" BeginTime="00:00:00.000" Duration="00:00:02.10">
                        <ColorAnimation Storyboard.TargetName="BackgroundBrush" Storyboard.TargetProperty="Color"                                                 
                                         To="#FF22B0E6" 
                                        Duration="00:00:00.10"/>
                        <ColorAnimation Storyboard.TargetName="BackgroundBrush" Storyboard.TargetProperty="Color"                                                 
                                         To="#FFECE8E8" 
                                        Duration="00:00:02.00"/>
                    </Storyboard>
                </ControlTemplate.Resources>
                <ControlTemplate.Triggers>                            
                    <Trigger Property="IsTouched" Value="True">
                        <Trigger.EnterActions>
                            <BeginStoryboard Storyboard="{StaticResource FadeTimeLine}"/>
                        </Trigger.EnterActions>
                    </Trigger>
                    <Trigger Property="IsMouseOver" Value="True">
                        <Setter Property="BorderBrush" TargetName="border" Value="{StaticResource ThemeSolidColorBrushPaleGray}"/>
                    </Trigger>
                    <Trigger Property="IsMouseOver" Value="False">
                        <Setter Property="Background" TargetName="border"  Value="{StaticResource ThemeSolidColorBrushPaleGray2}"/>
                    </Trigger>
                    <Trigger Property="IsEnabled" Value="False">
                        <Setter Property="Opacity" TargetName="grid" Value="0.25"/>
                    </Trigger>

                </ControlTemplate.Triggers>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

XAML Usage

<UserControl x:Class="TouchButtonApp.Keyboard1"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:local="clr-namespace:TouchButtonApp"
             mc:Ignorable="d" 
             d:DesignHeight="352" d:DesignWidth="1024">
    <Grid>
        <Grid Margin="0,0,0,0">
            <Grid.RowDefinitions>
                <RowDefinition Height="90*"/>
                <RowDefinition Height="90*"/>
                <RowDefinition Height="90*"/>
                <RowDefinition Height="90*"/>
            </Grid.RowDefinitions>
            <Grid>
                <Grid.RowDefinitions>

                    <RowDefinition Height="8*"/>
                </Grid.RowDefinitions>
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="93*"/>
                    <ColumnDefinition Width="93*"/>
                    <ColumnDefinition Width="93*"/>
                    <ColumnDefinition Width="93*"/>
                    <ColumnDefinition Width="93*"/>
                    <ColumnDefinition Width="93*"/>
                    <ColumnDefinition Width="93*"/>
                    <ColumnDefinition Width="93*"/>
                    <ColumnDefinition Width="93*"/>
                    <ColumnDefinition Width="93*"/>
                    <ColumnDefinition Width="93*"/>
                </Grid.ColumnDefinitions>
                <local:TouchButton x:Name="qButton" Tap="Button_Click" Content="Q"  Grid.Row="1" Style="{DynamicResource characterKeyT}" />
                <local:TouchButton x:Name="wButton" Tap="Button_Click" Content="W"  Grid.Column="1" Grid.Row="1" Style="{DynamicResource characterKeyT}" />
...