我的第一个 .NET 组件,一个不接受日期的 DateTimePicker
我需要一个 datetimepicker,允许用户像掩码文本框一样轻松地清空和编辑它,所以我从实际的 DateTimePicker 类创建了一个。
引言
我正在更新公司现有的程序,需要一个 DateTimePicker,允许用户将其留空或使其变为空(可空)。我搜索了一个我喜欢的控件,但大多发现人们将一个面板或按钮放在 DateTimePicker
旁边。我想使用 DateTimePicker 本身,所以我自己做了一个。我对 .NET 还比较陌生,尽管我有 Delphi 的背景,在那里我制作过组件,所以我想这是一个不错的开始。
使用代码
这部分很简单。将文件拖到你的项目中,然后进行构建,新组件就会出现在你的组件列表中。现在将其拖到你的窗体上。该组件看起来就像 DateTimePicker
,因为它继承自它。
这里是一些它与窗体上仅有一个按钮的简单图像...
我上面正在用键盘输入日期。
我输入完日期后,按钮获得了焦点。请注意,myDateTimePicker
使用 DateTimePicker
中设置的格式进行显示。MM/DD/YYYY 仅在控件内输入日期时使用。如果你住在其他国家,你可以在代码中更改使用的掩码。
我回到组件并按 Delete 清空日期,然后回到按钮焦点。现在我的鼠标悬停在控件的右侧(我想鼠标在 MS 截图工具中是看不见的)。
在 datetimepicker 上,如果你将其设置得太小以至于无法显示日期,日历 BMP 就会消失。由于我的组件使用 DateTimePicker
,它的行为相同。这是 DTP,日历消失了,只剩下箭头,我的鼠标悬停在上面。
在我删除日期(置空)之后
现在我点击日历弹出窗口
我决定按 Escape,所以日历消失了,并且没有接受日期。
代码
我从一个继承 DateTimePicker
的类开始。
Imports System.ComponentModel
Partial Class MyDateTimePicker
Inherits System.Windows.Forms.DateTimePicker
这给了我 DTP (DateTimePicker
) 作为我的基础。现在我需要控制控件的显示,但在仔细阅读了该类并搜索了其他人的工作之后,我决定要么不可能,要么以我现有的知识水平不可能。所以我想到了将一个文本框放在组件上方。这在我第一次尝试时带来了一些新问题。
- 即使我使用了 DTP 的客户宽度,文本框也会覆盖
DateTimePicker
右侧位图的部分。 - 我找不到任何方法可以告诉我 DTP 中文本显示的宽度,或者位图从哪里开始。
- 如果 DTP 被调整大小或格式发生变化,它真的会导致
TextBox
出现问题。
我不得不想出一些方法来处理列出的问题。我决定使用一个 Panel
放在 DTP **内部**。我会调整它的大小,然后将 TextBox
放在 Panel
**内部**。所以接下来我添加了 Panel
。
Private pnl As Panel
Public Sub New() ' The DateTimePicker Consutructor
MyBase.New()
' This call is required by the Component Designer.
InitializeComponent()
'Add any initialization after the InitializeComponent() call
Value = Today
pnl = New Panel
With pnl
pnl.Top = 1
pnl.Left = 1
pnl.Height = Me.ClientRectangle.Height
pnl.Width = Me.ClientRectangle.Width - 34
pnl.Anchor = CType(((System.Windows.Forms.AnchorStyles.Top Or _
System.Windows.Forms.AnchorStyles.Bottom) Or _
System.Windows.Forms.AnchorStyles.Left), _
System.Windows.Forms.AnchorStyles)
pnl.BackColor = System.Drawing.SystemColors.Window
pnl.Name = "pnl"
pnl.Margin = New System.Windows.Forms.Padding(0)
pnl.Padding = New System.Windows.Forms.Padding(0)
End With
Me.Controls.Add(pnl)
我在 DTP 新构造函数中创建了 Panel
。我将初始位置设置为 DTP 的左上角 **加上** 1,以考虑到 DTP 的边框,并且我覆盖了 DTP 的显示区域,宽度减去 34 像素。这个设置只是基础,并且会经常变化,所以面板 **不会** 覆盖 DTP 右侧的位图(稍后会详细介绍)。然后我设置了锚点并将 Panel
添加到 DTP。
现在我需要一种方法来处理设置实际面板大小,使其永远不会覆盖 DTP 图像 **或** 右侧的悬停边框。这对我来说比我想象的要困难。首先,我使用了 Graphics
类来渲染 DTP 文本的副本,使用相同的字体,然后检查文本宽度,但如果格式很长,DTP 的间距似乎有些奇怪,文本宽度很接近但总是不对。所以我放弃了那个想法,想出了一个新主意。似乎 DTP 中使用的位图 **不会** 调整大小,无论字体有多大,所以我可以依赖固定大小(面板宽度中的 -34)。但是,如果你将 DTP 的宽度设置得太小,它就会删除日历位图,只留下箭头,所以大小确实会改变一些。如果我要计算新的面板宽度,我需要一种方法来判断箭头是单独的还是日历也在。然后我意识到,在 VB.NET 2003 到 2010 中,箭头有两种颜色,取决于日历位图是否存在,所以我决定使用它。我编写了一个例程来制作 DTP 的图像副本,定位箭头并测试其颜色,以决定面板需要多宽。这效果很好,并且保留了自然的 DTP 悬停高亮和边框,所以没有人会注意到显示被覆盖了。
Private Function getCoverSize() As Integer
Dim g As Graphics = Me.CreateGraphics()
Dim bmp As New Bitmap(Me.Width, Me.Height)
Me.DrawToBitmap(bmp, New Rectangle(0, 0, Me.Width, Me.Height))
Dim cc As Color
Dim y As Integer
' find the first blank column
For x = bmp.Width - 3 To 0 Step -1
y = bmp.Height - 2
While y > 1
cc = bmp.GetPixel(x, y)
If (cc.A <> Color.White.A Or cc.R <> Color.White.R Or _
cc.G <> Color.White.G Or cc.B <> Color.White.B) Then
If cc.R = 0 And cc.G = 0 And cc.B = 0 Then
Return Me.Width - 18
' seems to be down arrow only fixed
' size to include border box
Else
Return Me.Width - 35
' seems to be down arrow and calender
' fixed size to include border box
End If
Else
y = y - 1
End If
End While
Next
Return 1
End Function
这个函数位于 DTP 中,在 DTP 调整大小或日期更改时调用。
所以我有一个画布,我无法在上面放置一个 TextBox
并将其锁定到画布的大小。
所以我把文本框放到了面板上。但我对 TextBox
的自由文本不满意,想限制它为日期。我决定使用 MaskedTextBox
(MTB) 并从此基础开始。所以我的 DTP 构造函数,在插入面板后,包含了 MTB 的创建和放置。
mtb = New DateTimePickerMaskedTextBox
With mtb
mtb.AutoSize = True
.Anchor = CType(((System.Windows.Forms.AnchorStyles.Top Or _
System.Windows.Forms.AnchorStyles.Bottom) _
Or System.Windows.Forms.AnchorStyles.Left Or _
System.Windows.Forms.AnchorStyles.Right), _
System.Windows.Forms.AnchorStyles)
.BorderStyle = Windows.Forms.BorderStyle.None
.Size = New System.Drawing.Size(pnl.Width - 2, pnl.Height - 0)
.BackColor = Color.Transparent
.Font = Me.Font
.Location = New System.Drawing.Size(2, (pnl.Height - .Height) / 2)
.Mask = ""
If _StartEmpty Then .Text = "" Else .Text = ShowDate()
._DTP = Me ' I need to refer to the DTP and this was the best way
.TabIndex = Me.TabIndex
End With
pnl.Controls.Add(mtb)
MyBase.TabStop = False
End Sub
该组件有一个名为 Mask
的属性...
此属性用于 MTB 的数据输入。你可以自己填充掩码,如果未设置任何内容,组件将从 Windows 中设置的区域性日期短日期格式构建掩码。组件还将从使用的 Mask
中确定日期分隔符字符。
在用户输入日期时,组件将验证输入的每个部分(月、日、年)的信息,并且如果日期无效,将不允许用户继续。所以假设掩码是 MM/dd/yyyy,如果用户在输入控件时输入 62,一旦光标移出掩码的月份部分,62 就被发现无效,用户会听到提示音,并且不允许输入日期的更多内容。对于像非闰年的 2 月 29 日这样的日期也同样适用。
以下按键在输入区域中有效
- DELETE - 将清空值(如果需要,可以置空,这被视为有效日期)。这还将设置
DateTimePicker
的Checked
状态为true
。 - ESCAPE - 将字段恢复到用户输入时的值。(如果原始日期是 1998 年 6 月 3 日,用户输入了部分新日期或完整新日期,然后按 Escape,值将恢复到 1998 年 6 月 3 日)。
- 箭头键、END 和 HOME 键 - 用于在输入区域中移动。
- BACKSPACE - 按退格键工作。
- 日期分隔符(通常是 "/" 或 "-",但可以是掩码中的**任何**符号) - 用于分隔日期的各个部分。
- 空格键 - 用作日期分隔符。
如果掩码已填满并且日期已验证,组件将自动移至日期的下一个部分。因此,使用掩码 "MM/dd/yyyy",用户可以输入 1998 年 6 月 13 日,输入为 "06/13/1998" 或 "06131998" 或 "6/13/1998" 或 "6/131998"(请记住,用户也可以在其中任何一个中用 <空格> 替换 "/")。
最后的 काम 是弹出式日历... 我无法知道用户是**取消**了还是**选择**了日期,所以我跟踪输入时的原始日期,并将其与输出时的值进行比较。如果用户输入了一个新日期,信息就会发送到 MTB,如果他们按 Escape,MTB 则不会发生任何变化。
由于我对 .NET 开发还很陌生,我希望在其他有经验的开发者那里能学到更好的方法来制作这个新组件。我很想了解更多。我意识到很多代码本可以写得更好,所以如果你做了改进,我希望能知道你做了什么,它如何改进了组件,以及我是否可以将更新发布在这里供其他人参考。
谢谢,并享受这个“可空”的 DateTimePicker。