Skip to content

Instantly share code, notes, and snippets.

@qwertie
Last active June 5, 2023 18:31
Show Gist options
  • Select an option

  • Save qwertie/1150228 to your computer and use it in GitHub Desktop.

Select an option

Save qwertie/1150228 to your computer and use it in GitHub Desktop.

Revisions

  1. qwertie revised this gist Dec 18, 2014. 2 changed files with 14 additions and 3 deletions.
    5 changes: 3 additions & 2 deletions DateTimePicker.xaml
    Original file line number Diff line number Diff line change
    @@ -3,9 +3,10 @@
    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:IntelliMap.WPF"
    xmlns:local="clr-namespace:DTPicker"
    xmlns:wpftc="clr-namespace:Microsoft.Windows.Controls;assembly=WPFToolkit"
    mc:Ignorable="d">
    mc:Ignorable="d"><!--Uses Calendar in WPFToolkit.dll,
    see http://wpf.codeplex.com/releases/view/40535-->
    <UserControl.Resources>
    <ControlTemplate x:Key="IconButton" TargetType="{x:Type ToggleButton}">
    <Border>
    12 changes: 11 additions & 1 deletion DateTimePicker.xaml.cs
    Original file line number Diff line number Diff line change
    @@ -612,4 +612,14 @@ protected override void OnRender(DrawingContext drawingContext)
    }
    }
    }
    }

    public class BoolInverter : MarkupExtension, IValueConverter
    {
    public override object ProvideValue(IServiceProvider serviceProvider)
    { return this; }
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    { return !(bool)value; }
    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    { return !(bool)value; }
    }
    }
  2. qwertie revised this gist Nov 16, 2012. 1 changed file with 13 additions and 4 deletions.
    17 changes: 13 additions & 4 deletions DateTimePicker.xaml.cs
    Original file line number Diff line number Diff line change
    @@ -285,7 +285,11 @@ private void DateDisplay_LostFocus(object sender, System.Windows.RoutedEventArgs
    // When the user selects a field again, then the box loses focus, then
    // the user clicks the same field again, the selection is cleared,
    // causing the arrows not to appear. To fix, clear selection in advance.
    DateDisplay.SelectionLength = 0;
    try {
    DateDisplay.SelectionLength = 0;
    } catch(NullReferenceException) {
    // Occurs during shutdown. Bug in WPF? Ain't documented, that's for sure.
    }
    }
    void DateDisplay_TextChanged(object sender, TextChangedEventArgs e)
    {
    @@ -424,8 +428,7 @@ private bool SelectPosition(int selstart, Direction direction)
    selstart = CalcPosition(selstart, direction);
    if (selstart > -1)
    {
    FocusOnDatePart(selstart);
    return true;
    return FocusOnDatePart(selstart);
    }
    else
    return false;
    @@ -468,10 +471,11 @@ private int CalcPosition(int selStart, Direction direction)
    return i;
    }

    private void FocusOnDatePart(int selStart)
    private bool FocusOnDatePart(int selStart)
    {
    ReformatDateText();

    // Find beginning of field to select
    string df = DateFormat;
    if (selStart > df.Length - 1)
    selStart = df.Length - 1;
    @@ -488,8 +492,13 @@ private void FocusOnDatePart(int selStart)
    while (selStart + selLength < df.Length && df[selStart + selLength] == firstchar)
    selLength++;

    // don't select AM/PM: we have no interface to change it.
    if (firstchar == 't')
    return false;

    DateDisplay.Focus();
    DateDisplay.Select(selStart, selLength);
    return true;
    }

    private DateTime Increase(int selstart, int value)
  3. qwertie revised this gist Nov 16, 2012. 2 changed files with 159 additions and 38 deletions.
    20 changes: 10 additions & 10 deletions DateTimePicker.xaml
    Original file line number Diff line number Diff line change
    @@ -3,7 +3,7 @@
    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:DTPicker"
    xmlns:local="clr-namespace:IntelliMap.WPF"
    xmlns:wpftc="clr-namespace:Microsoft.Windows.Controls;assembly=WPFToolkit"
    mc:Ignorable="d">
    <UserControl.Resources>
    @@ -25,15 +25,15 @@
    VerticalContentAlignment="Center"
    Margin="0,0,0,0"
    MinHeight="{Binding ElementName=PopUpCalendarButton, Path=ActualHeight}" Text="yyyy-MM-dd HH:mm">
    <TextBox.Style>
    <Style TargetType="TextBox">
    <Style.Triggers>
    <DataTrigger Binding="{Binding DateTextIsWrong, RelativeSource={RelativeSource AncestorType={x:Type UserControl}}}" Value="True">
    <Setter Property="Background" Value="DarkGray" />
    </DataTrigger>
    </Style.Triggers>
    </Style>
    </TextBox.Style>
    <TextBox.Style>
    <Style TargetType="TextBox">
    <Style.Triggers>
    <DataTrigger Binding="{Binding DateTextIsWrong, RelativeSource={RelativeSource AncestorType={x:Type UserControl}}}" Value="True">
    <Setter Property="Background" Value="LightGray" />
    </DataTrigger>
    </Style.Triggers>
    </Style>
    </TextBox.Style>
    </TextBox>
    <ToggleButton Grid.Column="1" Template="{StaticResource IconButton}"
    MaxHeight="21"
    177 changes: 149 additions & 28 deletions DateTimePicker.xaml.cs
    Original file line number Diff line number Diff line change
    @@ -7,6 +7,8 @@
    using System.Windows.Markup;
    using System.Diagnostics;
    using System.Windows.Media;
    using System.Windows.Documents;
    using System.Windows.Shapes;

    namespace DTPicker
    {
    @@ -54,7 +56,14 @@ namespace DTPicker
    /// "August 12, 2001 23:00". If the typed date cannot not be parsed, the
    /// date will revert to the original date (stored internally in
    /// SelectedDate) when the control loses focus.</li>
    /// </ul>
    /// <li>Added up and down arrows (WPF Adorners) that appear when you select a
    /// field of the date or time, allowing you to change the date or time
    /// incrementally with the mouse. This feature will not work automatically
    /// in all windows; you may need to wrap the DateTimePicker, or the entire
    /// contents of your window, in an &lt;AdornerDecorator> object to allow
    /// the arrows to appear. See
    /// http://stackoverflow.com/questions/13389772/how-to-draw-wpf-adorners-on-top-of-everything-else
    /// </li></ul>
    /// Note: I removed support for "null" as a date value because I didn't need
    /// it for my application, so I didn't want to take responsibility for ensuring
    /// that it works correctly.
    @@ -88,6 +97,7 @@ namespace DTPicker
    /// update DateDisplay.Text when the SelectedDate changes and the text box has
    /// the focus. Instead, a trigger changes TextBox.Background to gray to indicate
    /// the discrepancy.
    /// [2012-11] Added up-down arrows
    /// </remarks>
    public partial class DateTimePicker : UserControl
    {
    @@ -97,15 +107,36 @@ private enum Direction : int
    Previous = -1,
    Next = 1
    }
    TextBoxUpDownAdorner _upDownButtons;

    public DateTimePicker()
    {
    InitializeComponent();
    // CalDisplay.SelectedDatesChanged += CalDisplay_SelectedDatesChanged;
    CalDisplay.SelectedDatesChanged += CalDisplay_SelectedDatesChanged;
    DateDisplay.PreviewMouseUp += DateDisplay_PreviewMouseUp;
    DateDisplay.LostFocus += DateDisplay_LostFocus;
    DateDisplay.PreviewKeyDown += DateTimePicker_PreviewKeyDown;
    DateDisplay.TextChanged += new TextChangedEventHandler(DateDisplay_TextChanged);

    this.Loaded += (s, e) =>
    {
    AdornerLayer adLayer = GetAdornerLayer(DateDisplay);
    if (adLayer != null)
    {
    adLayer.Add(_upDownButtons = new TextBoxUpDownAdorner(DateDisplay));
    _upDownButtons.Click += (textBox, direction) => { OnUpDown(direction); };
    }
    };
    }

    static AdornerLayer GetAdornerLayer(FrameworkElement subject)
    {
    AdornerLayer layer = null;
    do {
    if ((layer = AdornerLayer.GetAdornerLayer(subject)) != null)
    break;
    } while ((subject = subject.Parent as FrameworkElement) != null);
    return layer;
    }

    #region "Properties"
    @@ -186,10 +217,10 @@ public event RoutedEventHandler DateFormatChanged

    public static DependencyProperty MinimumDateProperty = DependencyProperty.Register("MinimumDate", typeof(DateTime), typeof(DateTimePicker), new FrameworkPropertyMetadata(Convert.ToDateTime("1900-01-01 00:00"), null, new CoerceValueCallback(CoerceMinDate)));

    public static readonly DependencyProperty SelectedDateProperty = DependencyProperty.Register("SelectedDate",
    typeof(DateTime), typeof(DateTimePicker),
    new FrameworkPropertyMetadata(DateTime.Now,
    new PropertyChangedCallback(OnSelectedDateChanged),
    public static readonly DependencyProperty SelectedDateProperty = DependencyProperty.Register("SelectedDate",
    typeof(DateTime), typeof(DateTimePicker),
    new FrameworkPropertyMetadata(DateTime.Now,
    new PropertyChangedCallback(OnSelectedDateChanged),
    new CoerceValueCallback(CoerceDate)));

    /// <summary>true when user is busy editing DateDisplay and the SelectedDate
    @@ -212,7 +243,7 @@ private void CalDisplay_SelectedDatesChanged(object sender, System.Windows.Contr
    {
    PopUpCalendarButton.IsChecked = false;
    TimeSpan timeOfDay = TimeSpan.Zero;
    timeOfDay = SelectedDate.TimeOfDay;
    timeOfDay = SelectedDate.TimeOfDay;
    SelectedDate = CalDisplay.SelectedDate.Value.Date + timeOfDay;
    }

    @@ -240,7 +271,8 @@ void ReformatDateText()
    {
    // Changes DateDisplay.Text to match the current DateFormat
    DateTime? date = ParseDateText(true);
    if (date != null) {
    if (date != null)
    {
    string newText = date.Value.ToString(DateFormat);
    if (DateDisplay.Text != newText)
    DateDisplay.Text = newText;
    @@ -250,32 +282,32 @@ void ReformatDateText()
    private void DateDisplay_LostFocus(object sender, System.Windows.RoutedEventArgs e)
    {
    DateDisplay.Text = SelectedDate.ToString(DateFormat);
    // When the user selects a field again, then the box loses focus, then
    // the user clicks the same field again, the selection is cleared,
    // causing the arrows not to appear. To fix, clear selection in advance.
    DateDisplay.SelectionLength = 0;
    }
    void DateDisplay_TextChanged(object sender, TextChangedEventArgs e)
    {
    DateTime? date = ParseDateText(true);
    if (date != null)
    SelectedDate = date.Value;
    }

    private void DateTimePicker_PreviewKeyDown(object sender, System.Windows.Input.KeyEventArgs e)
    {
    var selstart = DateDisplay.SelectionStart;
    int selstart = DateDisplay.SelectionStart;

    if (!IsDateInExpectedFormat())
    return;

    switch (e.Key)
    {
    case Key.Up:
    _forceTextUpdateNow = true;
    SelectedDate = Increase(selstart, 1);
    FocusOnDatePart(selstart);
    OnUpDown(+1);
    break;
    case Key.Down:
    _forceTextUpdateNow = true;
    SelectedDate = Increase(selstart, -1);
    FocusOnDatePart(selstart);
    OnUpDown(-1);
    break;
    case Key.Left:
    if (Keyboard.Modifiers != ModifierKeys.None)
    @@ -310,6 +342,14 @@ private void DateTimePicker_PreviewKeyDown(object sender, System.Windows.Input.K
    e.Handled = true;
    }

    private void OnUpDown(int increment)
    {
    int selstart = DateDisplay.SelectionStart;
    _forceTextUpdateNow = true;
    SelectedDate = Increase(selstart, increment);
    FocusOnDatePart(selstart);
    }

    private static object CoerceDate(DependencyObject d, object value)
    {
    DateTimePicker me = (DateTimePicker)d;
    @@ -327,7 +367,7 @@ private static object CoerceMinDate(DependencyObject d, object value)
    DateTime current = Convert.ToDateTime(value);
    if (current >= me.MaximumDate)
    throw new ArgumentException("MinimumDate can not be equal to, or more than maximum date");

    if (current > me.SelectedDate)
    me.SelectedDate = current;

    @@ -343,7 +383,7 @@ private static object CoerceMaxDate(DependencyObject d, object value)

    if (current < me.SelectedDate)
    me.SelectedDate = current;

    return current;
    }

    @@ -361,28 +401,33 @@ public static void OnSelectedDateChanged(DependencyObject obj, DependencyPropert
    var date = (DateTime)args.NewValue;
    me.CalDisplay.SelectedDate = date;
    me.CalDisplay.DisplayDate = date;
    if (me.DateDisplay.IsFocused && !me._forceTextUpdateNow) {
    if (me.DateDisplay.IsFocused && !me._forceTextUpdateNow)
    {
    DateTime? oldDate = me.ParseDateText(true);
    if (oldDate != null)
    me.DateTextIsWrong = date != oldDate.Value;
    } else {
    }
    else
    {
    me.DateTextIsWrong = false;
    me._forceTextUpdateNow = false;
    me.DateDisplay.Text = date.ToString(me.DateFormat);
    }
    }

    #endregion

    // Selects next or previous date value, depending on the incrementor value
    // Alternatively moves focus to previous control or the calender button
    private bool SelectPosition(int selstart, Direction direction)
    {
    selstart = CalcPosition(selstart, direction);
    if (selstart > -1) {
    if (selstart > -1)
    {
    FocusOnDatePart(selstart);
    return true;
    } else
    }
    else
    return false;
    }

    @@ -399,11 +444,12 @@ private int CalcPosition(int selStart, Direction direction)
    {
    string df = DateFormat;
    if (selStart >= df.Length)
    selStart = df.Length-1;
    selStart = df.Length - 1;
    char startChar = df[selStart];
    int i = selStart;

    for (;;) {
    for (; ; )
    {
    i += (int)direction;
    if ((uint)i >= (uint)df.Length)
    return -1;
    @@ -413,7 +459,7 @@ private int CalcPosition(int selStart, Direction direction)
    break;
    startChar = '\0'; // to handle cases like "yyyy-MM-dd (ddd)" correctly
    }

    if (direction < 0)
    // move to the beginning of the field
    while (i > 0 && df[i - 1] == df[i])
    @@ -450,7 +496,8 @@ private DateTime Increase(int selstart, int value)
    {
    DateTime retval = (ParseDateText(false) ?? SelectedDate);

    try {
    try
    {
    switch (DateFormat.Substring(selstart, 1))
    {
    case "h":
    @@ -482,4 +529,78 @@ private DateTime Increase(int selstart, int value)
    return retval;
    }
    }
    }


    // Adorners must subclass the abstract base class Adorner.
    public class TextBoxUpDownAdorner : Adorner
    {
    StreamGeometry _triangle = new StreamGeometry();
    bool _shown;
    double _x, _top, _bottom;
    public Pen Outline = new Pen(new SolidColorBrush(Color.FromArgb(64,255,255,255)), 5);
    public Brush Fill = Brushes.Black;

    public TextBoxUpDownAdorner(TextBox adornedBox) : base(adornedBox)
    {
    _triangle = new StreamGeometry();
    _triangle.FillRule = FillRule.Nonzero;
    using (StreamGeometryContext c = _triangle.Open())
    {
    c.BeginFigure(new Point(-10, 0), true /* filled */, true /* closed */);
    c.LineTo(new Point(10, 0), true, false);
    c.LineTo(new Point(0, 15), true, false);
    }
    _triangle.Freeze();

    MouseDown += (s, e) => {
    if (Click != null)
    {
    bool up = e.GetPosition(AdornedElement).Y < (_top + _bottom) / 2;
    Click((TextBox)AdornedElement, up ? 1 : -1);
    }
    };

    adornedBox.LostFocus += RelevantEventOccurred;
    adornedBox.SelectionChanged += RelevantEventOccurred;
    }

    void RelevantEventOccurred(object sender, RoutedEventArgs e)
    {
    // In OnRender, GetRectFromCharacterIndex may return Infinity values,
    // so measure the location of the selection here instead.
    var box = AdornedElement as TextBox;
    if (box.IsFocused) {
    int start = box.SelectionStart, len = box.SelectionLength;
    if (_shown = len > 0) {
    var rect1 = box.GetRectFromCharacterIndex(start);
    var rect2 = box.GetRectFromCharacterIndex(start + len);
    _top = rect1.Top - 2;
    _bottom = rect1.Bottom + 2;
    _x = (rect1.Left + rect2.Left) / 2;
    }
    } else
    _shown = false;

    InvalidateVisual();
    }

    public event Action<TextBox, int> Click;

    // A common way to implement an adorner's rendering behavior is to override the OnRender
    // method, which is called by the layout system as part of a rendering pass.
    protected override void OnRender(DrawingContext drawingContext)
    {
    if (_shown)
    {
    drawingContext.PushTransform(new TranslateTransform(_x, _top));
    drawingContext.PushTransform(new ScaleTransform(1, -1));
    drawingContext.DrawGeometry(Fill, Outline, _triangle);
    drawingContext.Pop();
    drawingContext.Pop();
    drawingContext.PushTransform(new TranslateTransform(_x, _bottom));
    drawingContext.DrawGeometry(Fill, Outline, _triangle);
    drawingContext.Pop();
    }
    }
    }
    }
  4. qwertie revised this gist Nov 14, 2012. 2 changed files with 64 additions and 33 deletions.
    17 changes: 12 additions & 5 deletions DateTimePicker.xaml
    Original file line number Diff line number Diff line change
    @@ -1,6 +1,3 @@
    <!-- NOTE: you must add a copy of Calendar.Icon.bmp to your project.
    You can get it from the VB version of DateTimePicker at
    http://www.codeproject.com/KB/WPF/wpfDateTimePicker.aspx -->
    <UserControl x:Class="DTPicker.DateTimePicker"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    @@ -27,14 +24,24 @@
    HorizontalAlignment="Stretch"
    VerticalContentAlignment="Center"
    Margin="0,0,0,0"
    MinHeight="{Binding ElementName=PopUpCalendarButton, Path=ActualHeight}">yyyy-MM-dd HH:mm</TextBox>
    MinHeight="{Binding ElementName=PopUpCalendarButton, Path=ActualHeight}" Text="yyyy-MM-dd HH:mm">
    <TextBox.Style>
    <Style TargetType="TextBox">
    <Style.Triggers>
    <DataTrigger Binding="{Binding DateTextIsWrong, RelativeSource={RelativeSource AncestorType={x:Type UserControl}}}" Value="True">
    <Setter Property="Background" Value="DarkGray" />
    </DataTrigger>
    </Style.Triggers>
    </Style>
    </TextBox.Style>
    </TextBox>
    <ToggleButton Grid.Column="1" Template="{StaticResource IconButton}"
    MaxHeight="21"
    Margin="-1,0,0,0"
    Name="PopUpCalendarButton"
    IsChecked="False"
    IsHitTestVisible="{Binding ElementName=CalendarPopup, Path=IsOpen, Mode=OneWay, Converter={local:BoolInverter}}" >
    <Image Source="Calendar.Icon.bmp" Stretch="None" HorizontalAlignment="Left" />
    <Image Source="../Icons/Calendar.Icon.bmp" Stretch="None" HorizontalAlignment="Left" />
    </ToggleButton>
    <Popup IsOpen="{Binding Path=IsChecked, ElementName=PopUpCalendarButton}"
    x:Name="CalendarPopup" Margin="0,-7,0,0"
    80 changes: 52 additions & 28 deletions DateTimePicker.xaml.cs
    Original file line number Diff line number Diff line change
    @@ -5,6 +5,8 @@
    using System.Windows.Data;
    using System.Windows.Input;
    using System.Windows.Markup;
    using System.Diagnostics;
    using System.Windows.Media;

    namespace DTPicker
    {
    @@ -74,12 +76,18 @@ namespace DTPicker
    /// to update the day-of-month and day-of-week at the same time, otherwise
    /// parsing will tend to fail. For example, suppose the date is currently
    /// 2011-08-16 (Tue). If the user selects the day and types "19", DateTimePicker
    /// refuses to accept the new day because August 18, 2011 is not a Tuesday.
    /// refuses to accept the new day because August 19, 2011 is not a Tuesday.
    /// Consequently, parsing fails unless the user manually changes "Tue" to "Fri"
    /// or deletes "(Tue)" from the end.
    /// <para/>
    /// License: The Code Project Open License (CPOL)
    /// http://www.codeproject.com/info/cpol10.aspx
    /// <para/>
    /// [2012-11] Changed (1) to change the SelectedDate immediately when text is
    /// typed instead of waiting for the text box to lose focus, and (2) not to
    /// update DateDisplay.Text when the SelectedDate changes and the text box has
    /// the focus. Instead, a trigger changes TextBox.Background to gray to indicate
    /// the discrepancy.
    /// </remarks>
    public partial class DateTimePicker : UserControl
    {
    @@ -93,10 +101,11 @@ private enum Direction : int
    public DateTimePicker()
    {
    InitializeComponent();
    CalDisplay.SelectedDatesChanged += CalDisplay_SelectedDatesChanged;
    // CalDisplay.SelectedDatesChanged += CalDisplay_SelectedDatesChanged;
    DateDisplay.PreviewMouseUp += DateDisplay_PreviewMouseUp;
    DateDisplay.LostFocus += DateDisplay_LostFocus;
    DateDisplay.PreviewKeyDown += DateTimePicker_PreviewKeyDown;
    DateDisplay.TextChanged += new TextChangedEventHandler(DateDisplay_TextChanged);
    }

    #region "Properties"
    @@ -113,6 +122,12 @@ public string DateFormat
    set { SetValue(DateFormatProperty, value); }
    }

    public bool ShowCalendarButton
    {
    get { return PopUpCalendarButton.Visibility == Visibility.Visible; }
    set { PopUpCalendarButton.Visibility = (value ? Visibility.Visible : Visibility.Collapsed); }
    }

    public string _inputDateFormat;
    public string InputDateFormat()
    {
    @@ -177,10 +192,22 @@ public event RoutedEventHandler DateFormatChanged
    new PropertyChangedCallback(OnSelectedDateChanged),
    new CoerceValueCallback(CoerceDate)));

    /// <summary>true when user is busy editing DateDisplay and the SelectedDate
    /// becomes different from the date shown on the text box.</summary>
    public static readonly DependencyProperty DateTextIsWrongProperty = DependencyProperty.Register("DateTextIsWrong", typeof(bool), typeof(DateTimePicker), new FrameworkPropertyMetadata(false));

    protected bool DateTextIsWrong
    {
    get { return (bool)GetValue(DateTextIsWrongProperty); }
    set { SetValue(DateTextIsWrongProperty, value); }
    }

    #endregion

    #region "EventHandlers"

    bool _forceTextUpdateNow = true;

    private void CalDisplay_SelectedDatesChanged(object sender, System.Windows.Controls.SelectionChangedEventArgs e)
    {
    PopUpCalendarButton.IsChecked = false;
    @@ -211,6 +238,7 @@ bool IsDateInExpectedFormat()

    void ReformatDateText()
    {
    // Changes DateDisplay.Text to match the current DateFormat
    DateTime? date = ParseDateText(true);
    if (date != null) {
    string newText = date.Value.ToString(DateFormat);
    @@ -220,11 +248,13 @@ void ReformatDateText()
    }

    private void DateDisplay_LostFocus(object sender, System.Windows.RoutedEventArgs e)
    {
    DateDisplay.Text = SelectedDate.ToString(DateFormat);
    }
    void DateDisplay_TextChanged(object sender, TextChangedEventArgs e)
    {
    DateTime? date = ParseDateText(true);
    if (date == null)
    DateDisplay.Text = SelectedDate.ToString(DateFormat);
    else if (SelectedDate != date.Value)
    if (date != null)
    SelectedDate = date.Value;
    }

    @@ -235,22 +265,26 @@ private void DateTimePicker_PreviewKeyDown(object sender, System.Windows.Input.K
    if (!IsDateInExpectedFormat())
    return;

    e.Handled = true;

    switch (e.Key)
    {
    case Key.Up:
    _forceTextUpdateNow = true;
    SelectedDate = Increase(selstart, 1);
    FocusOnDatePart(selstart);
    break;
    case Key.Down:
    _forceTextUpdateNow = true;
    SelectedDate = Increase(selstart, -1);
    FocusOnDatePart(selstart);
    break;
    case Key.Left:
    if (Keyboard.Modifiers != ModifierKeys.None)
    return;
    SelectPosition(selstart, Direction.Previous);
    break;
    case Key.Right:
    if (Keyboard.Modifiers != ModifierKeys.None)
    return;
    SelectPosition(selstart, Direction.Next);
    break;
    case Key.Tab:
    @@ -270,9 +304,10 @@ private void DateTimePicker_PreviewKeyDown(object sender, System.Windows.Input.K
    e.Key == Key.OemSemicolon && nextChar == ':')
    SelectPosition(selstart, Direction.Next);
    else
    e.Handled = false;
    return;
    break;
    }
    e.Handled = true;
    }

    private static object CoerceDate(DependencyObject d, object value)
    @@ -326,7 +361,15 @@ public static void OnSelectedDateChanged(DependencyObject obj, DependencyPropert
    var date = (DateTime)args.NewValue;
    me.CalDisplay.SelectedDate = date;
    me.CalDisplay.DisplayDate = date;
    me.DateDisplay.Text = date.ToString(me.DateFormat);
    if (me.DateDisplay.IsFocused && !me._forceTextUpdateNow) {
    DateTime? oldDate = me.ParseDateText(true);
    if (oldDate != null)
    me.DateTextIsWrong = date != oldDate.Value;
    } else {
    me.DateTextIsWrong = false;
    me._forceTextUpdateNow = false;
    me.DateDisplay.Text = date.ToString(me.DateFormat);
    }
    }

    #endregion
    @@ -439,23 +482,4 @@ private DateTime Increase(int selstart, int value)
    return retval;
    }
    }

    public class BoolInverter : MarkupExtension, IValueConverter
    {
    public override object ProvideValue(IServiceProvider serviceProvider)
    {
    return this;
    }

    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
    if (value is bool)
    return !((bool)value);
    return value;
    }
    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
    return Convert(value, targetType, parameter, culture);
    }
    }
    }
  5. qwertie revised this gist Aug 16, 2011. 1 changed file with 1 addition and 0 deletions.
    1 change: 1 addition & 0 deletions DateTimePicker.xaml.cs
    Original file line number Diff line number Diff line change
    @@ -35,6 +35,7 @@ namespace DTPicker
    /// original VB version too), the user is allowed to input single digit
    /// values.</li>
    /// <li>Shift+Tab now selects the previous field instead of the next field</li>
    /// <li>Left/right arrow keys no longer let the TextBox lose keyboard focus</li>
    /// <li>XAML changed so that the control expands to fill the space it is given,
    /// because IMO the control looks strange if it changes width as the user is
    /// typing.</li>
  6. @invalid-email-address Anonymous created this gist Aug 16, 2011.
    46 changes: 46 additions & 0 deletions DateTimePicker.xaml
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,46 @@
    <!-- NOTE: you must add a copy of Calendar.Icon.bmp to your project.
    You can get it from the VB version of DateTimePicker at
    http://www.codeproject.com/KB/WPF/wpfDateTimePicker.aspx -->
    <UserControl x:Class="DTPicker.DateTimePicker"
    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:DTPicker"
    xmlns:wpftc="clr-namespace:Microsoft.Windows.Controls;assembly=WPFToolkit"
    mc:Ignorable="d">
    <UserControl.Resources>
    <ControlTemplate x:Key="IconButton" TargetType="{x:Type ToggleButton}">
    <Border>
    <ContentPresenter />
    </Border>
    </ControlTemplate>
    </UserControl.Resources>

    <Grid>
    <Grid.ColumnDefinitions>
    <ColumnDefinition Width="*"/>
    <ColumnDefinition Width="Auto"/>
    </Grid.ColumnDefinitions>

    <TextBox x:Name="DateDisplay"
    HorizontalAlignment="Stretch"
    VerticalContentAlignment="Center"
    Margin="0,0,0,0"
    MinHeight="{Binding ElementName=PopUpCalendarButton, Path=ActualHeight}">yyyy-MM-dd HH:mm</TextBox>
    <ToggleButton Grid.Column="1" Template="{StaticResource IconButton}"
    MaxHeight="21"
    Margin="-1,0,0,0"
    Name="PopUpCalendarButton"
    IsChecked="False"
    IsHitTestVisible="{Binding ElementName=CalendarPopup, Path=IsOpen, Mode=OneWay, Converter={local:BoolInverter}}" >
    <Image Source="Calendar.Icon.bmp" Stretch="None" HorizontalAlignment="Left" />
    </ToggleButton>
    <Popup IsOpen="{Binding Path=IsChecked, ElementName=PopUpCalendarButton}"
    x:Name="CalendarPopup" Margin="0,-7,0,0"
    PopupAnimation="Fade"
    StaysOpen="False">
    <wpftc:Calendar Margin="0,-1,0,0" x:Name="CalDisplay" ></wpftc:Calendar>
    </Popup>
    </Grid>
    </UserControl>
    460 changes: 460 additions & 0 deletions DateTimePicker.xaml.cs
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,460 @@
    using System;
    using System.Globalization;
    using System.Windows;
    using System.Windows.Controls;
    using System.Windows.Data;
    using System.Windows.Input;
    using System.Windows.Markup;

    namespace DTPicker
    {
    /// <summary>
    /// A WPF DateTimePicker control that allows the user to edit the date with a
    /// drop-down calendar, edit the date and time with the arrow keys, type a date
    /// by hand, or type a date componentwise. Based on a Visual Basic control by
    /// Magnus Gudmundsson: http://www.codeproject.com/KB/WPF/wpfDateTimePicker.aspx
    /// </summary>
    /// <remarks>
    /// Improvements over the VB version:
    /// <ul>
    /// <li>Bug fix: doesn't crash if the cursor is at the end of the textbox with
    /// no selection, and user presses the left arrow key.</li>
    /// <li>Bug fix: when the user clicks one of the punctuation marks in the
    /// textbox, the field to the right is selected (instead of something weird)</li>
    /// <li>Bug fix: user can enter a 4-digit year (in VB, it was cleared after the
    /// second digit)</li>
    /// <li>Bug fix: after typing a single-digit month, day or hour, the left/right
    /// arrow keys will select the adjacent field correctly.</li>
    /// <li>Bug fix: date formats that contain both "dd" and "ddd" are handled
    /// correctly, e.g. "ddd MM dd, yyyy HH:mm".</li>
    /// <li>Bug fix: any custom DateFormat is now permitted, not just formats that
    /// are understood by Visual Basic CDate(). However, you should not use
    /// one-digit fields in the DateFormat string because the code is designed
    /// for fixed-length dates. For example, you must use "MM" instead of "M"
    /// and "dd" instead of "d". Despite this limitation (which existed in the
    /// original VB version too), the user is allowed to input single digit
    /// values.</li>
    /// <li>Shift+Tab now selects the previous field instead of the next field</li>
    /// <li>XAML changed so that the control expands to fill the space it is given,
    /// because IMO the control looks strange if it changes width as the user is
    /// typing.</li>
    /// <li>After typing a new value for a field, the user can type the appropriate
    /// punctuation character to move to the next field. For example, given the
    /// default format "yyyy-MM-dd HH:mm", the user could click the month and
    /// type "2-28" to change the date to Febuary 28. When the user presses the
    /// "-" key, the day field will become selected automatically. "2/28" is
    /// also accepted.</li>
    /// <li>Free-form editing of the date is now permitted. The user can delete the
    /// entire date and re-enter it, even in an unexpected format. For example,
    /// although the date format may be "yyyy-MM-dd hh:mm tt", the control can
    /// accept a typed (or pasted) date in a different (standard) format such as
    /// "August 12, 2001 23:00". If the typed date cannot not be parsed, the
    /// date will revert to the original date (stored internally in
    /// SelectedDate) when the control loses focus.</li>
    /// </ul>
    /// Note: I removed support for "null" as a date value because I didn't need
    /// it for my application, so I didn't want to take responsibility for ensuring
    /// that it works correctly.
    /// <para/>
    /// The control switches to free-form edit mode when the user types something
    /// that makes the date invalid, such as an unexpected character or an invalid
    /// month number. The control reverts to normal "assisted" editing when the
    /// date becomes valid again, or when the control loses focus.
    /// <para/>
    /// A pleasant side-effect of the free-form input logic is that the user can now
    /// type non-numeric fields. For example, if the date format contains a MMM
    /// field, the user can select it and type "jul" to change the month to July.
    /// Editing with up/down arrow keys is also supported for the MMM and ddd fields,
    /// but not the t or tt fields (AM/PM).
    /// <para/>
    /// Although the date format can contain both "dd" and "ddd", please note that
    /// this currently thwarts user editing if they attempt to type any component
    /// (instead of using the arrow keys or calendar). The reason is that you have
    /// to update the day-of-month and day-of-week at the same time, otherwise
    /// parsing will tend to fail. For example, suppose the date is currently
    /// 2011-08-16 (Tue). If the user selects the day and types "19", DateTimePicker
    /// refuses to accept the new day because August 18, 2011 is not a Tuesday.
    /// Consequently, parsing fails unless the user manually changes "Tue" to "Fri"
    /// or deletes "(Tue)" from the end.
    /// <para/>
    /// License: The Code Project Open License (CPOL)
    /// http://www.codeproject.com/info/cpol10.aspx
    /// </remarks>
    public partial class DateTimePicker : UserControl
    {
    private const int FormatLengthOfLast = 2;
    private enum Direction : int
    {
    Previous = -1,
    Next = 1
    }

    public DateTimePicker()
    {
    InitializeComponent();
    CalDisplay.SelectedDatesChanged += CalDisplay_SelectedDatesChanged;
    DateDisplay.PreviewMouseUp += DateDisplay_PreviewMouseUp;
    DateDisplay.LostFocus += DateDisplay_LostFocus;
    DateDisplay.PreviewKeyDown += DateTimePicker_PreviewKeyDown;
    }

    #region "Properties"

    public DateTime SelectedDate
    {
    get { return (DateTime)GetValue(SelectedDateProperty); }
    set { SetValue(SelectedDateProperty, value); }
    }

    public string DateFormat
    {
    get { return Convert.ToString(GetValue(DateFormatProperty)); }
    set { SetValue(DateFormatProperty, value); }
    }

    public string _inputDateFormat;
    public string InputDateFormat()
    {
    if (_inputDateFormat == null)
    {
    string df = DateFormat;
    if (!df.Contains("MMM"))
    df = df.Replace("MM", "M");
    if (!df.Contains("ddd"))
    df = df.Replace("dd", "d");
    // Note: do not replace Replace("tt", "t") because a single "t" will not accept "AM" or "PM".
    _inputDateFormat = df.Replace("hh", "h").Replace("HH", "H").Replace("mm", "m").Replace("ss", "s");
    }
    return _inputDateFormat;
    }

    public DateTime MinimumDate
    {
    get { return Convert.ToDateTime(GetValue(MinimumDateProperty)); }
    set { SetValue(MinimumDateProperty, value); }
    }

    public DateTime MaximumDate
    {
    get { return Convert.ToDateTime(GetValue(MaximumDateProperty)); }
    set { SetValue(MaximumDateProperty, value); }
    }

    #endregion

    #region "Events"

    public event RoutedEventHandler DateChanged
    {
    add { AddHandler(DateChangedEvent, value); }
    remove { RemoveHandler(DateChangedEvent, value); }
    }

    public static readonly RoutedEvent DateChangedEvent = EventManager.RegisterRoutedEvent("DateChanged", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(DateTimePicker));

    public event RoutedEventHandler DateFormatChanged
    {
    add { this.AddHandler(DateFormatChangedEvent, value); }
    remove { this.RemoveHandler(DateFormatChangedEvent, value); }
    }

    public static readonly RoutedEvent DateFormatChangedEvent = EventManager.RegisterRoutedEvent("DateFormatChanged", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(DateTimePicker));

    #endregion

    #region "DependencyProperties"

    public static readonly DependencyProperty DateFormatProperty = DependencyProperty.Register("DateFormat", typeof(string), typeof(DateTimePicker), new FrameworkPropertyMetadata("yyyy-MM-dd HH:mm", OnDateFormatChanged));

    public static DependencyProperty MaximumDateProperty = DependencyProperty.Register("MaximumDate", typeof(DateTime), typeof(DateTimePicker), new FrameworkPropertyMetadata(Convert.ToDateTime("3000-01-01 00:00"), null, new CoerceValueCallback(CoerceMaxDate)));

    public static DependencyProperty MinimumDateProperty = DependencyProperty.Register("MinimumDate", typeof(DateTime), typeof(DateTimePicker), new FrameworkPropertyMetadata(Convert.ToDateTime("1900-01-01 00:00"), null, new CoerceValueCallback(CoerceMinDate)));

    public static readonly DependencyProperty SelectedDateProperty = DependencyProperty.Register("SelectedDate",
    typeof(DateTime), typeof(DateTimePicker),
    new FrameworkPropertyMetadata(DateTime.Now,
    new PropertyChangedCallback(OnSelectedDateChanged),
    new CoerceValueCallback(CoerceDate)));

    #endregion

    #region "EventHandlers"

    private void CalDisplay_SelectedDatesChanged(object sender, System.Windows.Controls.SelectionChangedEventArgs e)
    {
    PopUpCalendarButton.IsChecked = false;
    TimeSpan timeOfDay = TimeSpan.Zero;
    timeOfDay = SelectedDate.TimeOfDay;
    SelectedDate = CalDisplay.SelectedDate.Value.Date + timeOfDay;
    }

    private void DateDisplay_PreviewMouseUp(object sender, System.Windows.Input.MouseButtonEventArgs e)
    {
    if (DateDisplay.SelectionLength == 0)
    FocusOnDatePart(DateDisplay.SelectionStart);
    }

    bool IsDateInExpectedFormat()
    {
    return ParseDateText(false) != null;
    }
    DateTime? ParseDateText(bool flexible)
    {
    DateTime selectedDate;

    if (!DateTime.TryParseExact(DateDisplay.Text, InputDateFormat(), null, DateTimeStyles.AllowWhiteSpaces, out selectedDate))
    if (!flexible || !DateTime.TryParse(DateDisplay.Text, out selectedDate))
    return null;
    return selectedDate;
    }

    void ReformatDateText()
    {
    DateTime? date = ParseDateText(true);
    if (date != null) {
    string newText = date.Value.ToString(DateFormat);
    if (DateDisplay.Text != newText)
    DateDisplay.Text = newText;
    }
    }

    private void DateDisplay_LostFocus(object sender, System.Windows.RoutedEventArgs e)
    {
    DateTime? date = ParseDateText(true);
    if (date == null)
    DateDisplay.Text = SelectedDate.ToString(DateFormat);
    else if (SelectedDate != date.Value)
    SelectedDate = date.Value;
    }

    private void DateTimePicker_PreviewKeyDown(object sender, System.Windows.Input.KeyEventArgs e)
    {
    var selstart = DateDisplay.SelectionStart;

    if (!IsDateInExpectedFormat())
    return;

    e.Handled = true;

    switch (e.Key)
    {
    case Key.Up:
    SelectedDate = Increase(selstart, 1);
    FocusOnDatePart(selstart);
    break;
    case Key.Down:
    SelectedDate = Increase(selstart, -1);
    FocusOnDatePart(selstart);
    break;
    case Key.Left:
    SelectPosition(selstart, Direction.Previous);
    break;
    case Key.Right:
    SelectPosition(selstart, Direction.Next);
    break;
    case Key.Tab:
    var dir = Direction.Next;
    if ((Keyboard.Modifiers & ModifierKeys.Shift) != 0)
    dir = Direction.Previous;
    e.Handled = SelectPosition(selstart, dir);
    break;
    default:
    char nextChar = '\0';
    if (selstart < DateDisplay.Text.Length)
    nextChar = DateDisplay.Text[selstart];

    if ((e.Key == Key.OemMinus || e.Key == Key.Subtract || e.Key == Key.OemQuestion || e.Key == Key.Divide) &&
    (nextChar == '/' || nextChar == '-') ||
    e.Key == Key.Space && nextChar == ' ' ||
    e.Key == Key.OemSemicolon && nextChar == ':')
    SelectPosition(selstart, Direction.Next);
    else
    e.Handled = false;
    break;
    }
    }

    private static object CoerceDate(DependencyObject d, object value)
    {
    DateTimePicker me = (DateTimePicker)d;
    DateTime current = Convert.ToDateTime(value);
    if (current < me.MinimumDate)
    current = me.MinimumDate;
    if (current > me.MaximumDate)
    current = me.MaximumDate;
    return current;
    }

    private static object CoerceMinDate(DependencyObject d, object value)
    {
    DateTimePicker me = (DateTimePicker)d;
    DateTime current = Convert.ToDateTime(value);
    if (current >= me.MaximumDate)
    throw new ArgumentException("MinimumDate can not be equal to, or more than maximum date");

    if (current > me.SelectedDate)
    me.SelectedDate = current;

    return current;
    }

    private static object CoerceMaxDate(DependencyObject d, object value)
    {
    DateTimePicker me = (DateTimePicker)d;
    DateTime current = Convert.ToDateTime(value);
    if (current <= me.MinimumDate)
    throw new ArgumentException("MaximimumDate can not be equal to, or less than MinimumDate");

    if (current < me.SelectedDate)
    me.SelectedDate = current;

    return current;
    }

    public static void OnDateFormatChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
    {
    var me = (DateTimePicker)obj;
    me._inputDateFormat = null; // will be recomputed on-demand
    me.DateDisplay.Text = me.SelectedDate.ToString(me.DateFormat);
    }

    public static void OnSelectedDateChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
    {
    var me = (DateTimePicker)obj;

    var date = (DateTime)args.NewValue;
    me.CalDisplay.SelectedDate = date;
    me.CalDisplay.DisplayDate = date;
    me.DateDisplay.Text = date.ToString(me.DateFormat);
    }

    #endregion

    // Selects next or previous date value, depending on the incrementor value
    // Alternatively moves focus to previous control or the calender button
    private bool SelectPosition(int selstart, Direction direction)
    {
    selstart = CalcPosition(selstart, direction);
    if (selstart > -1) {
    FocusOnDatePart(selstart);
    return true;
    } else
    return false;
    }

    static char At(string s, int index)
    {
    if ((uint)index < (uint)s.Length)
    return s[index];
    return '\0';
    }

    // Gets location of next/previous date field, depending on the incrementor value.
    // Returns -1 if there is no next/previous field.
    private int CalcPosition(int selStart, Direction direction)
    {
    string df = DateFormat;
    if (selStart >= df.Length)
    selStart = df.Length-1;
    char startChar = df[selStart];
    int i = selStart;

    for (;;) {
    i += (int)direction;
    if ((uint)i >= (uint)df.Length)
    return -1;
    if (df[i] == startChar)
    continue;
    if (char.IsLetter(df[i]))
    break;
    startChar = '\0'; // to handle cases like "yyyy-MM-dd (ddd)" correctly
    }

    if (direction < 0)
    // move to the beginning of the field
    while (i > 0 && df[i - 1] == df[i])
    i--;

    return i;
    }

    private void FocusOnDatePart(int selStart)
    {
    ReformatDateText();

    string df = DateFormat;
    if (selStart > df.Length - 1)
    selStart = df.Length - 1;
    char firstchar = df[selStart];
    while (!char.IsLetter(firstchar) && selStart + 1 < df.Length)
    {
    selStart++;
    firstchar = df[selStart];
    }
    while (selStart > 0 && df[selStart - 1] == firstchar)
    selStart--;

    int selLength = 1;
    while (selStart + selLength < df.Length && df[selStart + selLength] == firstchar)
    selLength++;

    DateDisplay.Focus();
    DateDisplay.Select(selStart, selLength);
    }

    private DateTime Increase(int selstart, int value)
    {
    DateTime retval = (ParseDateText(false) ?? SelectedDate);

    try {
    switch (DateFormat.Substring(selstart, 1))
    {
    case "h":
    case "H":
    retval = retval.AddHours(value);
    break;
    case "y":
    retval = retval.AddYears(value);
    break;
    case "M":
    retval = retval.AddMonths(value);
    break;
    case "m":
    retval = retval.AddMinutes(value);
    break;
    case "d":
    retval = retval.AddDays(value);
    break;
    case "s":
    retval = retval.AddSeconds(value);
    break;
    }
    }
    catch (ArgumentException ex)
    {
    //Catch dates with year over 9999 etc, dont throw
    }

    return retval;
    }
    }

    public class BoolInverter : MarkupExtension, IValueConverter
    {
    public override object ProvideValue(IServiceProvider serviceProvider)
    {
    return this;
    }

    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
    if (value is bool)
    return !((bool)value);
    return value;
    }
    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
    return Convert(value, targetType, parameter, culture);
    }
    }
    }