【问题标题】:DateTimePicker internal validationDateTimePicker 内部验证
【发布时间】:2016-09-04 17:27:08
【问题描述】:

当我使用 VB.NET 2010 工具箱提供的常规 DateTimePicker 时,我的程序中经常会遇到这种情况。

看到这个:

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

现在,我想输入此控件 31 7 2016,但我不能,至少不容易或按预期输入。
我认为这是因为 31.09 不作为有效日期存在,但此时我仍未完成输入我希望的日期。

这里有什么技巧可以关闭 dtp 的内部验证或在特定情况下获得所需(描述)功能的任何其他方式吗?

【问题讨论】:

  • 如果有人询问使用MMDDYYYY 而不是DDMMYYYY 有什么好处,应该指出这个问题。
  • 这个“好处”在这里非常不可用,因为我们有本地方式使用显示格式的日期。
  • 我知道,这里我们使用相同的日期格式并且有相同的“问题”。
  • 如果是这样,那么这确实是一个“问题”。我仍然相信 MS 提供了一种处理它的方法。
  • 这正是 DTP 的工作方式,您必须键入“31072016”才能使其自动在日/月/年字段之间跳转。或使用 Tab 键结束月份。周围有很多类似 DTP 的自定义控件,它是 Codeproject.com 最喜欢的项目。或者只是假设您的用户知道如何使用 DTP,他们之前总是想出来的。

标签: .net vb.net


【解决方案1】:

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

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

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

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 class - with dts_appcanparse style - can't 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

【讨论】:

  • 这听起来很有趣。我使用新的子类控件尝试此代码,但我不知道如何使用它。像这样的细微变化 Dim hdr As NMHDR = DirectCast(Marshal.PtrToStructure(m.LParam, GetType(NMHDR)), NMHDR)。如果我禁用解析,则 dtp 的行为类似于普通文本框。您能否在示例中添加 dtp 将如何接受 31.09.2016。作为有效日期?毕竟我可以对 _Validate 或 _Leave 事件处理程序进行验证。
  • @user1697111 - 它永远不会接受31.09.2016 一个有效日期,因为它不是一个有效日期。您的问题是如何允许用户输入它,因为基本 DTP 在您键入时验证每个组件(日、月、年)。这允许用户输入一个完整的无效日期,然后必须验证整个字符串。 TryParse_NMDATETIMESTRING 方法中提供了验证 如果要更改验证逻辑,请更改该方法中的逻辑。但是,它当前的配置完全允许您所要求的。
  • 是的,我相信可以用它做点什么。谢谢你的好回答。
  • @user1697111,不客气。另外,看看我的最后一次编辑。我解决了使用此自定义控件的一些奇怪之处。
  • 经过您的所有努力,我无法获得我想要的可行版本。但我在这里学到了一些新东西。毕竟,从一开始就让我自己控制不会太难,我会增加临时输入 31.09 的能力,也可以输入有时需要的毫秒数,所以现在我正在考虑这种方式。再次感谢。
【解决方案2】:

TnTinMn 发布了a great answer,但他的代码仍然存在 Tab 键无法正常工作的问题(即使在合并了他 2017 年的编辑之后)。

以下是我修改后的 C# 版本,它解决了这个问题。它通过捕获WM_CREATEWM_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 的其他内部行为,可以选择将窗口消息记录到即时窗口。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2014-10-18
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多