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.
A 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} €}"
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} £}"
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} ¥}"
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" />
-->
<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:
public class DragIt
{
#region -- Members --
private const NumberStyles numberStyle =
NumberStyles.AllowThousands | NumberStyles.Float | NumberStyles.AllowCurrencySymbol;
private const string numberPattern = @"[-+]?[0-9]*[.,]?[0-9]+";
[DllImport("User32.dll")]
private static extern bool SetCursorPos(int x, int y);
#endregion
#region -- Properties --
#region InvertYAxis
public static readonly DependencyProperty InvertYAxisProperty = DependencyProperty.RegisterAttached(
"InvertYAxis", typeof (bool), typeof (DragIt), new PropertyMetadata(default(bool)));
public static void SetInvertYAxis(DependencyObject element, bool value)
{
element.SetValue(InvertYAxisProperty, value);
}
public static bool GetInvertYAxis(DependencyObject element)
{
return (bool) element.GetValue(InvertYAxisProperty);
}
#endregion
#region Pressed
protected static readonly DependencyProperty PressedProperty =
DependencyProperty.RegisterAttached("Pressed", typeof(Boolean), typeof(DragIt),
new PropertyMetadata(default(bool)));
protected static void SetPressed(DependencyObject element, bool value)
{
element.SetValue(PressedProperty, value);
}
protected static bool GetPressed(DependencyObject element)
{
return (bool)element.GetValue(PressedProperty);
}
#endregion
#region DragEnabled
public static readonly DependencyProperty DragEnabledProperty =
DependencyProperty.RegisterAttached("DragEnabled", typeof(Boolean), typeof(DragIt),
new FrameworkPropertyMetadata(OnDragEnabledChanged));
public static void SetDragEnabled(DependencyObject element, Boolean value)
{
element.SetValue(DragEnabledProperty, value);
}
public static Boolean GetDragEnabled(DependencyObject element)
{
return (Boolean)element.GetValue(DragEnabledProperty);
}
#endregion
#region Precision
public static readonly DependencyProperty PrecisionProperty =
DependencyProperty.RegisterAttached("Precision", typeof(double),
typeof(DragIt), new PropertyMetadata(default(double)));
public static void SetPrecision(DependencyObject element, double value)
{
element.SetValue(PrecisionProperty, value);
}
public static double GetPrecision(DependencyObject element)
{
return (double)element.GetValue(PrecisionProperty);
}
#endregion
#region StartPoint
protected static readonly DependencyProperty StartPointProperty =
DependencyProperty.RegisterAttached("StartPoint", typeof(Point), typeof(DragIt),
new PropertyMetadata(default(Point)));
protected static void SetStartPoint(UIElement element, Point value)
{
element.SetValue(StartPointProperty, value);
}
protected static Point GetStartPoint(UIElement element)
{
return (Point)element.GetValue(StartPointProperty);
}
#endregion
#region EndPoint
protected static readonly DependencyProperty EndPointProperty =
DependencyProperty.RegisterAttached("EndPoint", typeof(Point), typeof(DragIt),
new PropertyMetadata(default(Point)));
protected static void SetEndPoint(DependencyObject element, Point value)
{
element.SetValue(EndPointProperty, value);
}
protected static Point GetEndPoint(DependencyObject element)
{
return (Point)element.GetValue(EndPointProperty);
}
#endregion
#endregion
#region -- CallBacks --
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);
}
private static void ControlPreviewMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
var source = sender as TextBox;
if (source == null) return;
SetPressed(source, false);
}
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);
}
private static void ControlOnPreviewMouseMove(object sender, MouseEventArgs mouseEventArgs)
{
var source = sender as TextBox;
if (source == null || !GetPressed(source)) return;
var value = ParseValue(source.Text);
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);
if (vector.Length < SystemParameters.MinimumHorizontalDragDistance)
{
SetEndPoint(source, new Point(deltaX, deltaY));
return;
}
var invert = GetInvertYAxis(source) ? -1 : 1;
source.Text = (value + ((int)((deltaX - deltaY * invert) / SystemParameters.MinimumHorizontalDragDistance)) *
GetPrecision(source)).ToString(CultureInfo.InvariantCulture);
SetPosition(GetStartPoint(source));
SetEndPoint(source, new Point(0,0));
}
private static void SetPosition(Point point)
{
SetCursorPos((int)point.X, (int)point.Y);
}
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