Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Drag it

0.00/5 (No votes)
16 Jun 2015 1  
Drag values up/down like in Expression Design & Blend

Introduction

Several design programs like Microsoft Expression Blend&Design features TextBox controls where you are able to drag values and thereby increase or decrease numbers. 



The controls are especially intuitive when re-sizing design elements and when the size follow the mouse movements.
This article illustrates how to achieve this functionality with any TextBox using attached properties.  

Also availabe as NuGet package.

Sample application 

 

The included test project demonstrates the dragging functionality.  

The sample application is basically a currency calculator.  
When entering or dragging a currency of a single input field the corresponding currencies are calculated from it.
Each TextBox has a different Precision to demonstrate the ability to set the precision.
CheckBox has been added to enable or disable the overall dragging ability of the Sample application.    

Up / Down
Notice that the TextBox (Top & Left) has it's Y-axis inverted.
Microsoft Expression programs will increase values when the mouse is dragged down which correlate well with the general UI layout where the Y-axis points down.
This implementation will decrease values if you drag down ward (default) but it is possible to invert the Y-axis.

Markup   

 

The sample MainWindow.xaml and how to set the attached properties:  

<Window x:Class="DragItTest.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:controls="clr-namespace:NMT.Wpf.Controls;assembly=DragIt"
        Title="DragIt Test" Height="299" Width="277" ResizeMode="NoResize" Icon="DragIt.ico">
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto" />
            <ColumnDefinition Width="*" />
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="*" />
        </Grid.RowDefinitions>
        <TextBox Grid.Row="1" Grid.Column="0" Height="23" Margin="3,0,3,3" Width="124"
                 Text="{Binding Dkk, StringFormat=DKK {0:f0} Kr.}"  
                 controls:DragIt.DragEnabled="{Binding ElementName=EnableDragging, Path=IsChecked}"
                 controls:DragIt.Precision="5"
                 controls:DragIt.InvertYAxis="True"/>
        <TextBox  Grid.Row="1" Grid.Column="1" Height="23" Margin="3,0,3,3" Width="124"
                 Text="{Binding Eur, StringFormat=EUR {0:f1} &euro;}" 
                 controls:DragIt.DragEnabled="{Binding ElementName=EnableDragging, Path=IsChecked}" 
                 controls:DragIt.Precision=".5"/>
        <TextBox Grid.Row="3" Grid.Column="0" Height="23" Margin="3,0,3,3" Width="124"
                 Text="{Binding Usd, StringFormat=USD {0:f0} $}" 
                 controls:DragIt.DragEnabled="{Binding ElementName=EnableDragging, Path=IsChecked}" 
                 controls:DragIt.Precision="2" />
        <TextBox   Grid.Row="3" Grid.Column="1" Height="23" Margin="3,0,3,3" Width="124"
                 Text="{Binding Gbp, StringFormat=GBP {0:f1} &pound;}"
                 controls:DragIt.DragEnabled="{Binding ElementName=EnableDragging, Path=IsChecked}" 
                 controls:DragIt.Precision=".2" />
        <TextBox Grid.Row="5" Grid.Column="0" Height="23" Margin="3,0,3,3" Width="124"
                 Text="{Binding Yen, StringFormat=YEN {0:f0} &yen;}" 
                 controls:DragIt.DragEnabled="{Binding ElementName=EnableDragging, Path=IsChecked}" 
                 controls:DragIt.Precision="1" />
        <TextBox Grid.Row="5" Grid.Column="1" Height="23" Margin="3,0,3,3" Width="124"
                 Text="{Binding Cad, StringFormat=CAD {0:f2} $}" 
                 controls:DragIt.DragEnabled="{Binding ElementName=EnableDragging, Path=IsChecked}" 
                 controls:DragIt.Precision=".01" />
        <!-- Labels -->
        <Label Grid.Row="0" Grid.Column="0" Content="Precision 5" Margin="3,3,0,0" />
        <Label Grid.Row="0" Grid.Column="1"  Content="Precision 0.5" Margin="3,3,0,0" />
        <Label Grid.Row="2" Grid.Column="0"  Content="Precision 2" Margin="3,3,0,0" />
        <Label Grid.Row="2" Grid.Column="1"  Content="Precision 0.2" Margin="3,3,0,0" />
        <Label Grid.Row="4" Grid.Column="0" Content="Precision 1" Margin="3,3,0,0" />
        <Label Grid.Row="4" Grid.Column="1" Content="Precision 0.1" Margin="3,3,0,0" />
        <CheckBox Grid.Row="6" Grid.Column="0"  x:Name="EnableDragging" IsChecked="True" Margin="3" 
                  Content="Enable dragging" />
        <Rectangle Grid.Row="6" Grid.Column="1" Grid.RowSpan="2"  Fill="{StaticResource MoveBrush}" Margin="3"
                   RenderTransformOrigin="0.5,0.5">
            <Rectangle.RenderTransform>
                <TransformGroup>
                    <ScaleTransform/>
                    <SkewTransform/>
                    <RotateTransform Angle="-25.843"/>
                    <TranslateTransform/>
                </TransformGroup>
            </Rectangle.RenderTransform>
        </Rectangle>
    </Grid>
</Window> 

The Text properties are bound the the view model and is formatted using the StringFormat option.
Using the string format complicates matters since the value has to be parsed as a string and not as values (double).
Parsing the string is done using regular expressions and handled within the attached properties.  

The code 

The class DragIt containing the public attached properties DragEnabled, Precision, InvertYAxix:  

 

Most of the protected properties are used to remember states and values.
 

The  DragIt class:

 

 

  /// <summary>
  /// DragIt class
  /// Used to extend the functionality of TextBoxes so that a value can be altered by
  /// dragging up/down/left/right.
  /// </summary>
  public class DragIt
  {
    #region -- Members --
    /// <summary>
    /// The number style
    /// </summary>
    private const NumberStyles numberStyle =
      NumberStyles.AllowThousands | NumberStyles.Float | NumberStyles.AllowCurrencySymbol;
    /// <summary>
    /// The number pattern
    /// </summary>
    private const string numberPattern = @"[-+]?[0-9]*[.,]?[0-9]+";
    /// <summary>
    /// Sets the cursor position.
    /// </summary>
    /// <param name="x">The x.</param>
    /// <param name="y">The y.</param>
    /// <returns><c>true</c> if success, <c>false</c> otherwise.</returns>
    [DllImport("User32.dll")]
    private static extern bool SetCursorPos(int x, int y);
 
    #endregion
 
    #region -- Properties --
 
    #region InvertYAxis
    /// <summary>
    /// The invert y axis property
    /// </summary>
    public static readonly DependencyProperty InvertYAxisProperty = DependencyProperty.RegisterAttached(
      "InvertYAxis", typeof (bool), typeof (DragIt), new PropertyMetadata(default(bool)));
 
    /// <summary>
    /// Sets the invert y axis.
    /// </summary>
    /// <param name="element">The element.</param>
    /// <param name="value">if set to <c>true</c> [value].</param>
    public static void SetInvertYAxis(DependencyObject element, bool value)
    {
      element.SetValue(InvertYAxisProperty, value);
    }
 
    /// <summary>
    /// Gets the invert y axis.
    /// </summary>
    /// <param name="element">The element.</param>
    /// <returns><c>true</c> if XXXX, <c>false</c> otherwise.</returns>
    public static bool GetInvertYAxis(DependencyObject element)
    {
      return (bool) element.GetValue(InvertYAxisProperty);
    }
    #endregion
 
    #region Pressed
    /// <summary>
    /// The pressed property
    /// </summary>
    protected static readonly DependencyProperty PressedProperty =
      DependencyProperty.RegisterAttached("Pressed", typeof(Boolean), typeof(DragIt), 
      new PropertyMetadata(default(bool)));
 
    /// <summary>
    /// Sets the pressed.
    /// </summary>
    /// <param name="element">The element.</param>
    /// <param name="value">if set to <c>true</c> [value].</param>
    protected static void SetPressed(DependencyObject element, bool value)
    {
      element.SetValue(PressedProperty, value);
    }
 
    /// <summary>
    /// Gets if pressed.
    /// </summary>
    /// <param name="element">The element.</param>
    /// <returns><c>true</c> if pressed, <c>false</c> otherwise.</returns>
    protected static bool GetPressed(DependencyObject element)
    {
      return (bool)element.GetValue(PressedProperty);
    }
    #endregion
 
    #region DragEnabled
 
    /// <summary>
    /// The drag enabled property
    /// </summary>
    public static readonly DependencyProperty DragEnabledProperty =
      DependencyProperty.RegisterAttached("DragEnabled", typeof(Boolean), typeof(DragIt), 
      new FrameworkPropertyMetadata(OnDragEnabledChanged));
 
    /// <summary>
    /// Sets the drag enabled.
    /// </summary>
    /// <param name="element">The element.</param>
    /// <param name="value">The value.</param>
    public static void SetDragEnabled(DependencyObject element, Boolean value)
    {
      element.SetValue(DragEnabledProperty, value);
    }
 
    /// <summary>
    /// Gets the drag enabled.
    /// </summary>
    /// <param name="element">The element.</param>
    /// <returns>Boolean.</returns>
    public static Boolean GetDragEnabled(DependencyObject element)
    {
      return (Boolean)element.GetValue(DragEnabledProperty);
    }
 

    #endregion
 
    #region Precision
    /// <summary>
    /// The precision property
    /// </summary>
    public static readonly DependencyProperty PrecisionProperty =
      DependencyProperty.RegisterAttached("Precision", typeof(double), 
      typeof(DragIt), new PropertyMetadata(default(double)));
 
    /// <summary>
    /// Sets the precision.
    /// </summary>
    /// <param name="element">The element.</param>
    /// <param name="value">The value.</param>
    public static void SetPrecision(DependencyObject element, double value)
    {
      element.SetValue(PrecisionProperty, value);
    }
 
    /// <summary>
    /// Gets the precision.
    /// </summary>
    /// <param name="element">The element.</param>
    /// <returns>System.Double.</returns>
    public static double GetPrecision(DependencyObject element)
    {
      return (double)element.GetValue(PrecisionProperty);
    }
 
    #endregion
 
    #region StartPoint
    /// <summary>
    /// The start point property
    /// </summary>
    protected static readonly DependencyProperty StartPointProperty =
      DependencyProperty.RegisterAttached("StartPoint", typeof(Point), typeof(DragIt), 
      new PropertyMetadata(default(Point)));
 
    /// <summary>
    /// Sets the start point.
    /// </summary>
    /// <param name="element">The element.</param>
    /// <param name="value">The value.</param>
    protected static void SetStartPoint(UIElement element, Point value)
    {
      element.SetValue(StartPointProperty, value);
    }
 
    /// <summary>
    /// Gets the start point.
    /// </summary>
    /// <param name="element">The element.</param>
    /// <returns>Point.</returns>
    protected static Point GetStartPoint(UIElement element)
    {
      return (Point)element.GetValue(StartPointProperty);
    }
    #endregion
 
    #region EndPoint
    /// <summary>
    /// The end point property
    /// </summary>
    protected static readonly DependencyProperty EndPointProperty = 
      DependencyProperty.RegisterAttached("EndPoint", typeof(Point), typeof(DragIt), 
      new PropertyMetadata(default(Point)));
 
    /// <summary>
    /// Sets the end point.
    /// </summary>
    /// <param name="element">The element.</param>
    /// <param name="value">The value.</param>
    protected static void SetEndPoint(DependencyObject element, Point value)
    {
      element.SetValue(EndPointProperty, value);
    }
 
    /// <summary>
    /// Gets the end point.
    /// </summary>
    /// <param name="element">The element.</param>
    /// <returns>Point.</returns>
    protected static Point GetEndPoint(DependencyObject element)
    {
      return (Point)element.GetValue(EndPointProperty);
    }
    #endregion
 
    #endregion
 
    #region -- CallBacks --
 
    /// <summary>
    /// Callback for the OnDragEnabled state change event.
    /// </summary>
    /// <param name="obj">The object.</param>
    /// <param name="e">The instance containing the event data.</param>
    private static void OnDragEnabledChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
    {
      var control = (TextBox)obj;
      if (!((bool)e.NewValue))
      {
        control.PreviewMouseLeftButtonDown -= ControlOnPreviewMouseLeftButtonDown;
        control.PreviewMouseMove -= ControlOnPreviewMouseMove;
        control.PreviewMouseLeftButtonUp -= ControlPreviewMouseLeftButtonUp;
        control.Cursor = null;
        return;
      }
      control.PreviewMouseLeftButtonDown += ControlOnPreviewMouseLeftButtonDown;
      control.PreviewMouseMove += ControlOnPreviewMouseMove;
      control.PreviewMouseLeftButtonUp += ControlPreviewMouseLeftButtonUp;
      using (var stream = new MemoryStream(Resources.Expression_move))
        control.Cursor = new Cursor(stream);
    }
 
    /// <summary>
    /// Callback for the preview mouse left button up.
    /// </summary>
    /// <param name="sender">The sender.</param>
    /// <param name="e">The instance containing the event data.</param>
    private static void ControlPreviewMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
    {
      var source = sender as TextBox;
      if (source == null) return;
      SetPressed(source, false);
    }
 
    /// <summary>
    /// Callback for the preview mouse left button down.
    /// </summary>
    /// <param name="sender">The sender.</param>
    /// <param name="mouseButtonEventArgs">The instance containing the event data.</param>
    private static void ControlOnPreviewMouseLeftButtonDown(object sender,
      MouseButtonEventArgs mouseButtonEventArgs)
    {
      var source = sender as TextBox;
      if (source == null) return;
      var position = source.PointToScreen(Mouse.GetPosition(source));
      SetStartPoint(source, position);
      SetEndPoint(source, new Point(0,0));
      SetPressed(source, true);
    }
 
    /// <summary>
    /// Callback for the preview mouse move.
    /// </summary>
    /// <param name="sender">The sender.</param>
    /// <param name="mouseEventArgs">The instance containing the event data.</param>
    private static void ControlOnPreviewMouseMove(object sender, MouseEventArgs mouseEventArgs)
    {
      var source = sender as TextBox;
      if (source == null || !GetPressed(source)) return;
      // Get value
      var value = ParseValue(source.Text);
      // Get mouse position
      var point = source.PointToScreen(Mouse.GetPosition(source));
      var deltaX = GetEndPoint(source).X + point.X - GetStartPoint(source).X;
      var deltaY = GetEndPoint(source).Y + point.Y - GetStartPoint(source).Y;
      var vector = new Vector(deltaX, deltaY);
      // Test against minimum dragging distance.
      if (vector.Length < SystemParameters.MinimumHorizontalDragDistance)
      {
        SetEndPoint(source, new Point(deltaX, deltaY));
        return;
      }
      // Set value
      var invert = GetInvertYAxis(source) ? -1 : 1;
      source.Text = (value + ((int)((deltaX - deltaY * invert) / SystemParameters.MinimumHorizontalDragDistance)) * 
        GetPrecision(source)).ToString(CultureInfo.InvariantCulture);
      // Reset mouse position
      SetPosition(GetStartPoint(source));
      SetEndPoint(source, new Point(0,0));
    }
 
    /// <summary>
    /// Sets the position.
    /// </summary>
    /// <param name="point">The point.</param>
    private static void SetPosition(Point point)
    {
      SetCursorPos((int)point.X, (int)point.Y);
    }
 
    /// <summary>
    /// Parses the value.
    /// </summary>
    /// <param name="number">The number.</param>
    /// <returns>System.Double.</returns>
    private static double ParseValue(String number)
    {
      string match = Regex.Match(number, numberPattern).Value;
      double retVal;
      var success = Double.TryParse(match, numberStyle, CultureInfo.InvariantCulture, out retVal);
      if (!success)
        retVal = 0;
      return retVal;
    }
 
    #endregion
 
  } 

 

 

Points of Interest

Regular expressions
Parsing the results from StringFormat using regular expressions required some attention.
I ended up testing input and results using http://regexpal.com/

System parameters
I found the SystemParameters to be a very useful.
It holds a ton of system settings that you will often find yourself struggling to find.

 

DragIt
The DragIt attatched properties are currently working with TextBoxes but could be extended to cover other controls as well. 

History    

Version 1.0.1 - Hides cursor while dragging.
Initial version 1.0.0

 

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here