TnTinMn 发布了a great answer,但他的代码仍然存在 Tab 键无法正常工作的问题(即使在合并了他 2017 年的编辑之后)。
以下是我修改后的 C# 版本,它解决了这个问题。它通过捕获WM_CREATE 的WM_PARENTNOTIFY 消息来检测DateTimePicker 的内部文本框何时被创建,并通过钩子的that 控件的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://docs.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 的其他内部行为,可以选择将窗口消息记录到即时窗口。