DateTimePicker内部验证



在使用VB提供的常规DateTimePicker时,我的程序中经常出现这种情况。. NET 2010工具箱。

参见:

Public Class Form1
    Private Sub Form1_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load
        Me.DateTimePicker1.Format = DateTimePickerFormat.Custom
        Me.DateTimePicker1.CustomFormat = "dd.MM.yyyy."
        Me.DateTimePicker1.Value = "01.09.2016."
    End Sub
End Class

现在,我想在这个控件317 2016中键入,但我不能,至少不容易或如预期的那样。
我认为这是因为31.09不存在作为有效日期,但在这一点上,我仍然没有完成输入我想要的日期。

是否有任何技巧可以关闭dtp的内部验证或任何其他方法在给定情况下获得所需的(描述的)功能?

由于您使用的是WinForms,而WinForm DateTimePicker控件只是本机通用DateTimePicker控件的包装,因此您可以创建派生控件来允许应用程序解析输入字符串。这是通过在控件上设置DTS_APPCANPARSE样式来完成的。您可以通过覆盖控件的CreateParams属性来设置此样式。

当DTS_APPCANPARSE样式被设置时,本机控件发送一个DTN_USERSTRING通知给控件。此消息在控件的WndProc方法中接收。

下面显示的大部分代码只是所使用的本机结构的定义。本例中使用的解析函数(TryParse_NMDATETIMESTRING)重用了DateTime。TryParse方法,以适应"dd.MM. xml"格式。通过将线程文化更改为"de-DE",因为该文化设置支持此格式。你可以定义任何你想要的解析逻辑。

Imports System.Runtime.InteropServices
Imports System.Globalization
Public Class DateTimePickerCustomParse : Inherits DateTimePicker
    Protected Overrides ReadOnly Property CreateParams As CreateParams
        Get
            Const DTS_APPCANPARSE As Int32 = &H10
            Dim cp As CreateParams = MyBase.CreateParams
            cp.Style = cp.Style Or DTS_APPCANPARSE
            Return cp
        End Get
    End Property
#Region "Native Structures"
    Structure NMDATETIMESTRING
        Public nmhdr As NMHDR
        Public pszUserString As IntPtr
        Public st As SYSTEMTIME
        Public dwFlags As GDT
    End Structure
    Public Enum GDT
        GDT_ERROR = -1
        GDT_VALID = 0
        GDT_NONE = 1
    End Enum
    <StructLayout(LayoutKind.Sequential)> _
    Public Structure SYSTEMTIME
         Public wYear As Short
         Public wMonth As Short
         Public wDayOfWeek As Short
         Public wDay As Short
         Public wHour As Short
         Public wMinute As Short
         Public wSecond As Short
         Public wMilliseconds As Short
    End Structure
    <StructLayout(LayoutKind.Sequential, CharSet:=CharSet.Auto, Pack:=1)> _
    Public Class NMHDR
         Public hwndFrom As IntPtr = IntPtr.Zero
         Public idFrom As Integer = 0
         Public code As Integer = 0
    End Class
#End Region
    Private Shared Function TryParse_NMDATETIMESTRING(ByRef nmDTS As NMDATETIMESTRING) As Boolean
        Dim ret As Boolean
        Dim enteredDate As String = Marshal.PtrToStringUni(nmDTS.pszUserString)
        Dim savedThreadCulture As CultureInfo = Threading.Thread.CurrentThread.CurrentCulture
        Try
            Threading.Thread.CurrentThread.CurrentCulture = New CultureInfo("de-DE")
            Dim dt As DateTime
            If DateTime.TryParse(enteredDate, dt) Then
                nmDTS.dwFlags = GDT.GDT_VALID
                nmDTS.st = DateTimeToSYSTEMTIME(dt)
                ret = True
            Else
                nmDTS.dwFlags = GDT.GDT_ERROR
            End If
        Finally
            Threading.Thread.CurrentThread.CurrentCulture = savedThreadCulture
        End Try
        Return ret
    End Function

    Private Shared Function DateTimeToSYSTEMTIME(dt As DateTime) As SYSTEMTIME
        Dim ret As New SYSTEMTIME
        ret.wYear = CShort(dt.Year)
        ret.wDay = CShort(dt.Day)
        ret.wMonth = CShort(dt.Month)
        ret.wDayOfWeek = CShort(dt.DayOfWeek)
        ret.wHour = CShort(dt.Hour)
        ret.wMinute = CShort(dt.Minute)
        ret.wSecond = CShort(dt.Second)
        ret.wMilliseconds = CShort(dt.Millisecond)
        Return ret
    End Function
    Protected Overrides Sub WndProc(ByRef m As Message)
        Const WM_NOTIFY As Int32 = &H4E
        Const WM_REFLECT_NOTIFY As Int32 = WM_NOTIFY + &H2000
        Const DTN_FIRST As Int32 = -740
        Const DTN_USERSTRINGW As Int32 = (DTN_FIRST - 5)
        If m.Msg = WM_REFLECT_NOTIFY OrElse m.Msg = WM_NOTIFY Then
            Dim hdr As New NMHDR
            Marshal.PtrToStructure(m.LParam, hdr)
            If hdr.code = DTN_USERSTRINGW Then
                Dim nmDTS As NMDATETIMESTRING = Marshal.PtrToStructure(Of NMDATETIMESTRING)(m.LParam)
                If TryParse_NMDATETIMESTRING(nmDTS) Then
                    Marshal.StructureToPtr(nmDTS, m.LParam, True)
                    Exit Sub
                End If
            End If
        End If
        MyBase.WndProc(m)
    End Sub
End Class
编辑:

使用DTS_APPCANPARSE样式有一个特别恼人的事情,那就是它排除了使用Tab键来离开控件。这是在datetimepicker类中记录的-使用dts_appcanparse样式-不能tab out。输入的文本通过失去焦点或按下Enter键发送以进行验证。然而,Enter键仍然使DTP聚焦,没有键盘方式离开它。此外,如果您将Tab输入控件,它的行为就像标准DTP一样;按下F2键进入编辑模式并选择整个文本。要使用上面定义的控件解决这些问题,您可以添加以下内容:

Public Property MoveNextOnEnterKey As Boolean = True
Public Property SelectAllOnEnter As Boolean = True
Protected Overrides Sub OnEnter(e As EventArgs)
    MyBase.OnEnter(e)
    If SelectAllOnEnter Then SendKeys.Send("{F2}")
End Sub
Private Sub MoveNext()
    Me.Parent.SelectNextControl(Me, True, True, True, True)
End Sub

您需要将WndProc方法替换为:

Protected Overrides Sub WndProc(ByRef m As Message)
    Const WM_NOTIFY As Int32 = &H4E
    Const WM_REFLECT_NOTIFY As Int32 = WM_NOTIFY + &H2000
    Const DTN_FIRST As Int32 = -740
    Const DTN_USERSTRINGW As Int32 = (DTN_FIRST - 5)
    If m.Msg = WM_REFLECT_NOTIFY OrElse m.Msg = WM_NOTIFY Then
        Dim hdr As New NMHDR
        Marshal.PtrToStructure(m.LParam, hdr)
        If hdr.code = DTN_USERSTRINGW Then
            Dim nmDTS As NMDATETIMESTRING = Marshal.PtrToStructure(Of NMDATETIMESTRING)(m.LParam)
            If TryParse_NMDATETIMESTRING(nmDTS) Then
                Marshal.StructureToPtr(nmDTS, m.LParam, True)
                If MoveNextOnEnterKey Then
                    Me.BeginInvoke(New Action(AddressOf MoveNext))
                End If
                Exit Sub
            End If
        End If
    End If
    MyBase.WndProc(m)
End Sub

TnTinMn发布了一个很好的答案,但他的代码仍然有一个Tab键不能正常工作的问题(即使在合并了他2017年的编辑之后)。

下面是我修改的c#版本,它解决了这个问题。它通过捕获WM_CREATEWM_PARENTNOTIFY消息来检测何时创建了DateTimePicker的内部文本框,并钩子的控件的WndProc来捕获按键。

using System;
using System.Globalization;
using System.Runtime.InteropServices;
using System.Windows.Forms;
using System.Diagnostics;
// See also:
// https://stackoverflow.com/questions/39319609/datetimepicker-internal-validation
// https://stackoverflow.com/questions/43576035/is-there-a-way-to-get-the-handle-of-the-entry-field-in-a-date-time-picker-dtp
// https://wiki.winehq.org/List_Of_Windows_Messages
// https://referencesource.microsoft.com/#system.windows.forms/winforms/Managed/System/WinForms/DateTimePicker.cs
  
public class DateTimePickerCustomParse : DateTimePicker {
  #region Win32 API's and helpers
  const int DTN_FIRST = -740;
  const int DTN_USERSTRINGW = DTN_FIRST - 5;
  const int DTN_WMKEYDOWN = DTN_FIRST - 4; // unfortunately doesn't seem to capture keystrokes in Edit control
  const int EN_CHANGE = 0x0300;
  const int EN_KILLFOCUS = 0x0200;
  const int EN_UPDATE = 0x0400;
  const int WM_PARENTNOTIFY = 0x0210;
  const int WM_CREATE = 0x0001;
  const int WM_DESTROY = 0x0002;
  const int WM_KILLFOCUS = 0x0008;
  const int WM_PAINT = 0x000F;
  const int WM_KEYDOWN = 0x0100;
  const int WM_KEYUP = 0x0101;
  const int WM_CHAR = 0x0102;
  const int WM_COMMAND = 0x0111;
  const int WM_NOTIFY = 0x004E;
  const int WM_REFLECT_NOTIFY = WM_NOTIFY + 0x2000;
  [DllImport("user32.dll", EntryPoint = "SendMessage")]
  private static extern IntPtr SendGetDateTimePickerInfoMessage(IntPtr hWnd, int Msg, IntPtr wParam, ref DATETIMEPICKERINFO info);
  [DllImport("USER32.dll")]
  private static extern short GetKeyState(int nVirtKey); // see https://www.pinvoke.net/default.aspx/user32.getkeystate
  [DllImport("user32.dll", EntryPoint = "SetWindowLongW")]
  private static extern IntPtr SetWindowLongPtr32(IntPtr hWnd, int nIndex, IntPtr dwNewLong);
  [DllImport("user32.dll", EntryPoint = "SetWindowLongPtrW")]
  private static extern IntPtr SetWindowLongPtr64(IntPtr hWnd, int nIndex, IntPtr dwNewLong);
  private delegate IntPtr WndProcDelegate(IntPtr hWnd, int message, IntPtr wParam, IntPtr lParam);
  private static WndProcDelegate SetWindowProc(IntPtr hWnd, WndProcDelegate newWndProc) {
    IntPtr newWndProcPtr = Marshal.GetFunctionPointerForDelegate(newWndProc);
    IntPtr oldWndProcPtr;
    if (IntPtr.Size == 4) {
      oldWndProcPtr = SetWindowLongPtr32(hWnd, -4, newWndProcPtr);
    } else {
      oldWndProcPtr = SetWindowLongPtr64(hWnd, -4, newWndProcPtr);
    }
    return (WndProcDelegate)Marshal.GetDelegateForFunctionPointer(oldWndProcPtr, typeof(WndProcDelegate));
  }
  struct NMDATETIMESTRING {
    #pragma warning disable CS0649 // suppress "never assigned to" warning
    public NMHDR nmhdr;
    public IntPtr pszUserString;
    #pragma warning restore CS0649
    public SYSTEMTIME st;
    public GDT dwFlags;
  }
  public enum GDT {
    GDT_ERROR = -1,
    GDT_VALID = 0,
    GDT_NONE = 1
  }
  [StructLayout(LayoutKind.Sequential)]
  public struct SYSTEMTIME {
    public short wYear;
    public short wMonth;
    public short wDayOfWeek;
    public short wDay;
    public short wHour;
    public short wMinute;
    public short wSecond;
    public short wMilliseconds;
  }
  [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto, Pack = 1)]
  public class NMHDR {
    public IntPtr hwndFrom = IntPtr.Zero;
    public int idFrom = 0;
    public int code = 0;
  }
  [StructLayout(LayoutKind.Sequential)]
  struct DATETIMEPICKERINFO {
    public int cbSize; // technically uint
    public RECT rcCheck;
    public int stateCheck; // technically uint
    public RECT rcButton;
    public int stateButton; // technically uint
    public IntPtr hwndEdit;
    public IntPtr hwndUD;
    public IntPtr hwndDropDown;
  }
  [StructLayout(LayoutKind.Sequential)]
  public struct RECT {
    public int left, top, right, bottom;
  }
  private static bool TryParse_NMDATETIMESTRING(ref NMDATETIMESTRING nmDTS) {
    bool ret = false;
    string enteredDate = Marshal.PtrToStringUni(nmDTS.pszUserString);
    CultureInfo savedThreadCulture = System.Threading.Thread.CurrentThread.CurrentCulture;
    try {
      //System.Threading.Thread.CurrentThread.CurrentCulture = new CultureInfo("de-DE");
      DateTime dt;
      if (DateTime.TryParse(enteredDate, out dt)) {
        nmDTS.dwFlags = GDT.GDT_VALID;
        nmDTS.st = DateTimeToSYSTEMTIME(dt);
        ret = true;
      } else
        nmDTS.dwFlags = GDT.GDT_ERROR;
    } finally {
      //System.Threading.Thread.CurrentThread.CurrentCulture = savedThreadCulture;
    }
    return ret;
  }
  private static SYSTEMTIME DateTimeToSYSTEMTIME(DateTime dt) {
    SYSTEMTIME ret = new SYSTEMTIME();
    ret.wYear = System.Convert.ToInt16(dt.Year);
    ret.wDay = System.Convert.ToInt16(dt.Day);
    ret.wMonth = System.Convert.ToInt16(dt.Month);
    ret.wDayOfWeek = System.Convert.ToInt16(dt.DayOfWeek);
    ret.wHour = System.Convert.ToInt16(dt.Hour);
    ret.wMinute = System.Convert.ToInt16(dt.Minute);
    ret.wSecond = System.Convert.ToInt16(dt.Second);
    ret.wMilliseconds = System.Convert.ToInt16(dt.Millisecond);
    return ret;
  }
  private static int LoWord(int value) {
    return BitConverter.ToUInt16(BitConverter.GetBytes((uint)value), 0);
  }
  private static int HiWord(int value) {
    return BitConverter.ToUInt16(BitConverter.GetBytes((uint)value), 2);
  }
  private static bool HiBit(short value) {
    const ushort HiBit = 0x8000;
    return ((value & HiBit) == HiBit);
  }
  private static int LoByte(short value) {
    return (int)BitConverter.GetBytes(value)[0];
  }
  private static int HiByte(short value) {
    return (int)BitConverter.GetBytes(value)[1];
  }
  #endregion
  public bool SelectAllOnEnter { get; set; } = true;
  public bool TabOnEnterKey { get; set; } = true;
  private bool tabPrev = false;
  private IntPtr editHandle { get; set; }
  private WndProcDelegate editHandler = null;
  private WndProcDelegate oldEditHandler = null;
  static DateTime lastStamp = DateTime.MinValue;
  private static void DebugPrint(string format, params object[] args) {
    return; // comment out to display extended debugging messages
    #pragma warning disable CS0162 // suppress "Unreachable code detected" warning
    var dt = DateTime.Now;
    if (dt > lastStamp.AddMilliseconds(200)) {
      Debug.Print(""); // group into batches for friendlier debugging
    }
    Debug.Print(format, args);
    lastStamp = dt;
    #pragma warning restore CS0162
  }
  protected override CreateParams CreateParams {
    get {
      const Int32 DTS_APPCANPARSE = 0x10;
      CreateParams cp = base.CreateParams;
      cp.Style = cp.Style | DTS_APPCANPARSE;
      return cp;
    }
  }
  protected override void OnEnter(EventArgs e) {
    base.OnEnter(e);
    if (SelectAllOnEnter) SendKeys.Send("{F2}");
  }
  private void MoveNext() {
    this.Parent.SelectNextControl(this, true, true, true, true);
  }
  private void MovePrev() {
    this.Parent.SelectNextControl(this, false, true, true, true);
  }
  /// <summary>
  /// Attempts to gain a handle to the DateTimePicker's internal Edit control.
  /// </summary>
  /// <returns>Returns the handle, or <see cref="IntPtr.Zero"/> if it doesn't exist.</returns>
  private IntPtr GetEditHandle() {
    const int DTM_FIRST = 0x1000;
    const int DTM_GETDATETIMEPICKERINFO = DTM_FIRST + 14;
    var info = new DATETIMEPICKERINFO();
    info.cbSize = Marshal.SizeOf<DATETIMEPICKERINFO>();
    // Alternative method, also works:
    //SendGetDateTimePickerInfoMessage(Handle, DTM_GETDATETIMEPICKERINFO, IntPtr.Zero, ref info);
    //return info.hwndEdit;
    IntPtr p = Marshal.AllocCoTaskMem(info.cbSize);
    try {
      Marshal.StructureToPtr(info, p, false);
      Message m = new Message() { HWnd = Handle, Msg = DTM_GETDATETIMEPICKERINFO, WParam = IntPtr.Zero, LParam = p };
      base.WndProc(ref m);
      info = Marshal.PtrToStructure<DATETIMEPICKERINFO>(p);
    } finally {
      Marshal.FreeCoTaskMem(p);
    }
    return info.hwndEdit;
  }
  protected override void WndProc(ref Message m) {
    // Note breakpoints triggered in this function may freeze the form until you switch to another window (e.g. Ctrl+Esc)
    switch (m.Msg) {
      case WM_REFLECT_NOTIFY:
      case WM_NOTIFY:
        NMHDR hdr = new NMHDR();
        Marshal.PtrToStructure(m.LParam, hdr);
        if (hdr.code == DTN_USERSTRINGW) {
          NMDATETIMESTRING nmDTS = Marshal.PtrToStructure<NMDATETIMESTRING>(m.LParam);
          if (TryParse_NMDATETIMESTRING(ref nmDTS)) {
            Marshal.StructureToPtr(nmDTS, m.LParam, true);
            if (TabOnEnterKey) {
              if (tabPrev) {
                BeginInvoke(new Action(MovePrev));
                tabPrev = false; // in case next time is just plain Enter key
              } else {
                BeginInvoke(new Action(MoveNext));
              }
            }
            return;
          }
        }
        break;
      case WM_PARENTNOTIFY:
        var h = GetEditHandle();
        var lo = LoWord(m.WParam.ToInt32());
        var hi = HiWord(m.WParam.ToInt32());
        DebugPrint("WM_PARENTNOTIFY  wParam: {0}  lo: {1}  hi: {2}  lParam: {3}  h: {4}", m.WParam, lo, hi, m.LParam, h);
        switch (lo) {
          case WM_CREATE:
            DebugPrint("  WM_CREATE  wParam: {0}  lo: {1}  hi: {2}  lParam:  {3}  h: {4}", m.WParam, lo, hi, m.LParam, h);
            DebugPrint("  Detected creation: {0}", m.LParam);
            editHandle = m.LParam;
            editHandler = new WndProcDelegate(EditWndProc);
            oldEditHandler = SetWindowProc(editHandle, editHandler);
            break;
          case WM_DESTROY:
            DebugPrint("  WM_DESTROY  wParam: {0}  lo: {1}  hi: {2}  lParam:  {3}  h: {4}", m.WParam, lo, hi, m.LParam, h);
            if (m.LParam == editHandle) {
              DebugPrint("  Detected destruction: {0}", m.LParam);
            } else {
              DebugPrint("  *** DETECTED DESTRUCTION OF SOMETHING ELSE: {0}", m.LParam);
            }
            SetWindowProc(editHandle, oldEditHandler);
            oldEditHandler = null;
            editHandler = null;
            editHandle = IntPtr.Zero;
            break;
        }
        break;
      // Can be used as alternative to WM_CREATE.  Has advantage of being able to send a DTM_GETDATETIMEPICKERINFO query
      // to be absolutely sure you get the right control, although I haven't seen a case where CM_CREATE had it wrong.
      case WM_KILLFOCUS:
        var h2 = GetEditHandle();
        DebugPrint("WM_KILLFOCUS  wParam: {0}  h: {1}", m.WParam, h2);
        if (h2 != IntPtr.Zero && h2 == m.WParam) {
          DebugPrint("  KillFocus confirmed creation: {0}", h2);
        }
        break;
    } // ...switch(m.Msg)
    base.WndProc(ref m);
  }
  private IntPtr EditWndProc(IntPtr hWnd, int message, IntPtr wParam, IntPtr lParam) {
    DebugPrint("WndProc  {0}  {1}  {2}", message, wParam, lParam);
    switch (message) {
      case WM_KEYDOWN:
        DebugPrint("WM_KEYDOWN  wParam: {0}  lParam: {1}", wParam, lParam);
        // See https://learn.microsoft.com/en-us/windows/win32/inputdev/virtual-key-codes
        if (wParam.ToInt32() == (int)Keys.Tab) {
          bool shift = HiBit(GetKeyState((int)Keys.ShiftKey));
          // Bit of a hack but BeginInvoke(new Action(MoveNext)) doesn't do anything, neither does this.Parent?.GetNextControl(this, false)?.Focus() or .Select().
          // Also tried several PostMessage() commands.
          string keys = TabOnEnterKey ? "{ENTER}" : (shift ? "{ENTER}+{TAB}" : "{ENTER}{TAB}");
          if (shift) {
            DebugPrint("   SHIFT+TAB detected; moving Prev");
            tabPrev = true;
          } else {
            DebugPrint("   TAB detected; moving Next");
            tabPrev = false;
          }            
          SendKeys.Send(keys);
          return IntPtr.Zero; // indicate keydown handled
        }
        break;
      case WM_KEYUP:
      case WM_CHAR: // note WM_CHAR is the one that causes a beep
        if (wParam.ToInt32() == (int)Keys.Tab) {
          return IntPtr.Zero; // indicate keyup handled
        }
        break;
    } // ...switch(message)
    // Propogate to original WndProc for the edit control
    if (oldEditHandler != null) return oldEditHandler(hWnd, message, wParam, lParam);
    return IntPtr.Zero;
  }
} // ...class

如果你想重写DateTimePicker的其他内部行为,有一个选项可以记录窗口消息到即时窗口。