C# WPF .NET 4.0 ArrowRepeatButton、NumericUpDown 和 TimeCtrl 控件






4.39/5 (10投票s)
用于输入时间或一定范围内的数字的用户控件。
引言
WPF 允许在布局/外观和底层功能之间进行严格分离。与 MFC、WinForms 或其他 GUI 框架相比,更改组合框控件的外观等非常容易。
我认为,通过此功能,所有已发布的 WPF 控件都应支持所有主要主题,并有可能支持其他主题。我认为,使用一个外观与其他控件不同的控件是不奇怪的。我见过几个上下按钮控件,包括 CodePlex 上的那个,但这些控件都没有对右侧的上下按钮提供完整的主题支持。
本文/代码示例是关于箭头按钮的,由于它们本身没有用,我编写了几个使用它们的控件。
ArrowRepeatButton
我将从讨论箭头按钮开始,因为它是整个项目的基礎。
ArrowRepeatButton
是一个派生自 RepeatButton
的 WPF 自定义控件。本质上,此控件旨在插入到其他控件中,并具有四种类型的按钮,每种按钮上都绘制有不同的箭头:上、下、左和右。箭头类型通过依赖属性 ButtonArrowType
设置。您需要传入一个 ButtonArrowType
枚举类型,其值为 Down
、Up
、Left
或 Right
。
另一个依赖属性 IsCornerCtrlCorner
用于指示按钮的哪些角也位于容器控件的角上。此信息是必需的,因为某些主题在这些情况下具有圆角,例如 Aero 主题。
信息通过 IsCornerCtrlCorner
结构传递。此结构按左上、右上、右下、左下的顺序接受四个布尔值。
在 XAML 中,您可以这样写:
<local:ArrowRepeatButton x:Name="UpButton"
ButtonArrowType="Up" IsCornerCtrlCorner="False,True,False,False"/>
这将表示一个带有向上箭头的按钮,其右上角位于控件的角上。
按钮的 XAML 绘制位于 Theme 文件夹中的文件中。
对于 Luna 和 Royale 主题,我通过使用标准的滚动条按钮来绘制箭头按钮。这是合理的,因为它们看起来完全相同,并且没有特殊的圆角处理。
对于 Classic 主题,我部分使用了内置功能。箭头本身必须进行编码。
Aero 主题更有趣,因为它在与控件边缘相邻的角上具有额外的圆角。您需要仔细查看按钮才能看到这一点。无法使用主题内置的按钮绘制,因为这不提供圆角控制:Microsoft_Windows_Themes:ButtonChrome
有一个名为 RoundCorners
的属性,可以设置为 True 或 False。设置为 False 时,按钮的左侧是直角,而右侧略微圆润。我们希望能够选择哪些角是圆角,哪些不是。因此,有必要编写 XAML 代码从头开始绘制按钮。
与控件边缘相邻的按钮角的额外圆角使用 IsCornerCtrlCornerToRadiusConverter
转换器计算。额外圆角量在 ConverterParameter
中指定。
实际上,事情更复杂,因为可以对不与容器控件角相邻的角进行圆角处理。为此,您仍然在 ConverterParameter
中传入一个 int
。非相邻角圆角量通过将此 int
右移 8 位获得。例如,我们希望非相邻角圆角为 2,相邻角圆角为 4。我们传入 2 << 8 | 4 = 516,或者:0x204。十进制和十六进制表示法都受支持。
对于 Aero,主题在 Aero.NormalColor.XAML 中设置,角的圆角由以下 XAML 代码给出:
CornerRadius="{Binding IsCornerCtrlCorner, RelativeSource={RelativeSource FindAncestor,
AncestorType={x:Type local:ArrowRepeatButton}},
Converter={StaticResource IsCornerCtrlCornerToRadiusConverter},
ConverterParameter=0x2}">
两个 ArrowRepeatButton
依赖属性的完整描述如下:
ButtonArrowType
public ButtonArrowType ButtonArrowType { get; set; }
其中 ButtonArrowType
是一个定义如下的枚举:
public enum ButtonArrowType : byte
{
Down,
Up,
Left,
Right
}
默认值为 ButtonArrowType.Down
。这仅在按钮上显示相应的箭头。
IsCornerCtrlCorner
public IsCornerCtrlCorner IsCornerCtrlCorner { get; set; }
其中 IsCornerCtrlCorner
是一个定义如下的结构(仅显示构造函数和成员变量):
public struct IsCornerCtrlCorner : IEquatable<IsCornerCtrlCorner>
{
private bool topLeft, topRight, bottomRight, bottomLeft;
public IsCornerCtrlCorner(bool uniformCtrlCorner);
public IsCornerCtrlCorner(bool topLeft, bool topRight,
bool bottomRight, bool bottomLeft);
}
默认值为 IsCornerCtrlCorner(false, true, true, false)
,即容器控件的右侧按钮。
实用工具:UpDownButtons、TextBox 框架、多语言支持……
为了方便使用 ArrowRepeatButton
并避免代码重复,创建了一些实用工具。
UpDownButtons
UpDownButtons
只是一个 WPF 用户控件,包含两个 ArrowRepeatButton
以及处理用户按下这些按钮时的消息。
TextBox 框架
控件框架用于 NumericUpDown
和 TimeCtrl
控件的 TextBox
。
一个有趣的点是 XAML 样式。除了避免大量代码重复外,style
还可以用于支持主题。
例如,在 Aero 主题中,上下箭头按钮会触及控件的实际边框。对于其他使用的主题,这些上下箭头按钮必须放置在 TextBox
框架内。要看到这一点,只需查看主题 *.xaml 文件中的 UpDownButtonsStyle
。您实际上可以绑定到一个仅在样式调用者中定义而不在样式本身定义中的元素,这很棒。
为了让按钮正确地适应框架,还必须使用一个特定于主题的 ThicknessToMarginConverter
。此转换器将边框厚度转换为外边距。ConvertParameter
是一个布尔值,用于指示如何进行转换。
NumericUpDown
和 TimeCtrl
都需要静态强制函数。这些函数用于正确调整 BorderThickness
、Background
和 BorderBrush
。需要这些函数的原因是:
BorderThickness
:您永远不想启用对UserControl
的BorderThickness
的更改。否则会显示另一个框架。相反,我们希望直接操作边框TextBox
的厚度。Background
和BorderBrush
:这些对于TextBox
有默认值。在 XAML 中尝试绑定会删除这些默认值。如果我们不设置字段,这会是一个问题。
为了避免代码重复,在 Helper.cs 文件中创建了一个名为 Coercer
的静态类,其中包含所有强制函数。所有需要的是 NumericUpDown
和 TimeCtrl
都支持 IframeTxtBoxCtrl
,定义如下:
internal interface IFrameTxtBoxCtrl
{
TextBox TextBox { get; }
}
TextBox
返回控件相关的 TextBox
。
多语言支持
由于希望控件不局限于一种文化,并且 TimeCtrl
弹出菜单需要一些字符串,因此为该项目添加多语言支持是有意义的。
字符串存储在 Resources.resx 中。已创建法语和德语版本。这是通过复制 Resources.resx 并用相关的语言标签重命名副本来实现的。要查找您机器上使用的语言标签:只需在 Helpers.cs 文件底部定义的 LanguageStrings
静态类中的 GetLangStr
函数中设置断点。您将立即看到值。创建 Resources.xx.XX.resx 后,需要将其添加到项目中并移动到 Properties 文件夹。您应该会收到一条消息,告知文件已存在 - 只需单击“是”并用其相同的副本覆盖该文件。
Resources.Designer.cs 中有自动生成的代码,用于检索这些字符串,但这似乎不适用于其他 Resources.xx.XX.resx 文件。这就是为什么使用 LanguageStrings
静态类来获取区域性相关的字符串。
NumericUpDown 控件
其他程序员已经编写了 WPF 数字上下控件。WPF Extended Toolkit 中有一个:WPF Extended Toolkit DecimalUpDown。
理论上可以将 ArrowRepeatButton
重用于大多数其他控件。尽管如此,作者还是从头开始编写了另一个版本。
此版本更像作者已在 CodeProject 上发布的 NumberBox
控件,请参阅 NumberBox UserControl。
WPF Extended Toolkit 版本会在退出字段时验证输入。对您可以键入的字符没有限制。您甚至可以键入字母。退出字段后,会重新显示最后一个有效输入。禁用字段时,字体颜色不会更新为灰色。也许是因为它也可以在 Silverlight 中运行?
此版本将在输入时限制条目。无法输入非法字符。只能输入数字。如果输入值小于 Minimum
或大于 Maximum
,则在退出字段时将其清除,并重新显示最后一个有效输入。也可以使用 delete 或 backspace 键盘键清除所有内容。在这种情况下,同样会在退出字段时重新显示最后一个有效输入。
当用户输入的值超出范围时,可以使用“OutOfRangeTextBrush
”依赖属性将文本画笔设置为不同的颜色。
如果 Minimum
小于零,则可以输入负数。为此,您需要键入负号,通常是减号('-')号。这与计算器的工作方式相同:第一次按下时,数字为负,再次按下时,它再次变为正。插入符号位置无关紧要。
拖放已被限制。当可能保持数字有效时,允许剪切/复制/粘贴。这并非总是如此:例如,您的最大值为 1000,用户输入“1000.00”,然后选择“0.”部分并尝试“剪切”。这是不允许的,因为否则您会得到一个数字“10000”,大于 1000。
否则,NumericUpDown
可以配置为以不同格式输入数字:程序员可以选择负号是前缀还是后缀,小数点是点、逗号还是系统设置,依此类推。请参阅下面的依赖属性完整列表。
要增加/减少数字,您可以:
- 按下控件右侧的向上/向下箭头按钮。
- 按下键盘上的向上和向下按钮。
- 将鼠标光标放在要增加/减少的数字上,然后使用鼠标滚轮。
NumericUpDown
控件的依赖属性列表如下:
值
public decimal Value { get; set; }
默认值为 0。此属性设置/获取控件中显示的数字。如果 Value
< Minimum
,则 Value
设置为 Minimum
;如果 Value
> Maximum
,则 Value
设置为 Maximum
。
最大
public decimal Maximum { get; set; }
默认值为 100。此属性设置/获取允许的最大数字。无法增加或输入大于 Maximum
的数字。
最低
public decimal Minimum { get; set; }
默认值为 0。此属性设置/获取允许的最小数字。无法减少或输入小于 Minimum
的数字。
步骤
public decimal Step { get; set; }
默认值为 1。指示按下向上/向下箭头按钮时的增加/减少量。
DecimalPlaces
public short DecimalPlaces { get; set; }
默认值为 0。此属性设置/获取小数点后允许的小数位数。如果设置为 0 或更少,用户将无法再输入小数点。
DecimalSeparatorType
public DecimalSeparatorType DecimalSeparatorType { get; set; }
其中
public enum DecimalSeparatorType : byte
{
System_Defined,
Point,
Comma
}
默认值为 System_Defined
。在英国或美国,数字中的小数点用点表示,例如“pi = 3.14”。在法国,则使用逗号,如“pi = 3,14”。此属性使 NumericUpDown
能够覆盖系统设置。
NegativeSignType
public NegativeSignType NegativeSignType { get; set; }
其中
public enum NegativeSignType : byte
{
System_Defined,
Minus
}
默认值为 System_Defined
。负数通常用减号('-')表示。有一个默认设置允许您用非 '-' 的符号表示负数。此属性允许您用减号覆盖默认设置。
NegativeSignSide
public NegativeSignSide NegativeSignSide { get; set; }
其中
public enum NegativeSignSide : byte
{
System_Defined,
Prefix,
Suffix
}
默认值为 System_Defined
。在某些文化中,负数用负号符号作为后缀而不是前缀。此属性允许您使用系统设置(默认)或指定负号是数字的前缀还是后缀。如果您选择 System_Defined
,假设当系统格式设置为‘(1.1)’、‘-1.1’或‘- 1.1’时为前缀。否则为后缀。
NegativeTextBrush
public Brush NegativeTextBrush { get; set; }
默认值为 null(未设置)。此属性允许您用与正数不同的颜色设置负数。通常的做法是显示红色的负数。
OutOfRangeTextBrush
public Brush OutOfRangeTextBrush { get; set; }
默认值为 null(未设置)。此属性允许您在用户输入的值超出范围时将文本画笔设置为不同的颜色。覆盖 NegativeTextBrush
属性。目的是向用户发出警告,表明正在输入的值将在退出字段时被清除,除非进行修改使其在有效范围内。
TextAlignment
public TextAlignment TextAlignment { get; set; }
默认值为 TextAlignment.Right
。指示文本对齐方式,类似于 TextBox
的 TextAlignment
。只处理 Left
、Right
和 Center
TextAlignment
。
TimeCtrl
WPF 已有 DatePicker
。有时用户会想要选择时间。这可以合并到 DatePicker
中,也可以包含在一个单独的控件中。
一种常见的做法是为时间控件尝试等效于日期控件,并在按下下拉按钮时显示一个时钟面。用这种方式选择时间很不实用:一个月最多有 31 天,而一天有 86400 秒!
因此,作者编写了这个 TimeCtrl
,它使用上下箭头按钮。
显示的时间格式应与您计算机上的系统设置相同,但可以将其配置为您想要的任何格式。请参阅下面的 TimePattern
依赖属性。
每个小时/分钟/秒字段实际上都是一个独立的无边框 TextBox
字段,因此一次只能选择一个。
与 NumericUpDown
类似,无法为时间字段输入无效值。这会导致一些有趣的结果:对于范围为 1 到 12 的 12 小时字段,“0”是无效输入,选择所有内容并键入“0”将导致字段被清除。我的 Windows 7 时钟也表现相同。
拖放已完全禁用。剪切/复制/粘贴已受限制,以确保条目始终有效。
递增/递减值的方式与 NumericUpDown
控件完全相同:向上/向下箭头按钮、键盘向上/向下按钮,以及控件上的鼠标滚轮。
此外,字段会循环:递增分钟和秒的“59”会显示“0”或“00”。
由于一次只能选择一个字段,因此添加了几个额外的右键单击菜单项:“复制时间”和“粘贴时间”。这些菜单项允许您复制/粘贴 TimeCtrl
中出现的所有字段。只能粘贴符合控件设置的 TimePattern
的剪贴板字符串。因此,无法在设置了不同 TimePattern
的 TimeCtrl
之间复制/粘贴。
通常在时间控件中缺失的一点是,在有小时限制的情况下,实际显示有效小时的方法。
在 DatePicker
中,您有一个弹出窗口。它显示一个日历月。在其中,您可以查看额外信息,例如星期几。此外,还有一个功能可以禁用特定日期。
TimeCtrl
有一个 UseValidTimes
功能,默认设置为 false。此功能允许限制小时。当 UseValidTimes
设置为 true 时,将出现一个新菜单项:“有效时间”。选择此项后,将出现“(无)”或有效小时列表。这些需要作为 ValidTimeItem
插入。ValidTimeItem
有两个依赖属性:BeginTime
和 EndTime
,类型均为 TimeEntry
。
可以在 XAML 中添加 ValidTimeItem
<local:TimeCtrl TimePattern="hh:mm:ss tt" UseValidTimes="True">
<local:ValidTimeItem BeginTime="9,0,0" EndTime="12,0,0"/>
<local:ValidTimeItem BeginTime="13,30,0" EndTime="18,0,0"/>
</local:TimeCtrl>
或 (C#)
MyTimeCtrl.Children.Add(
new ValidTimeItem(new TimeEntry(9,00,00), new TimeEntry(17,30,00)));
当时间无效时,它将以另一种颜色显示。默认情况下,此颜色为红色,但可以通过 InvalidTimeTextBrush
依赖属性进行设置。如果时间无效,IsValidTime
依赖属性将返回 false。
TimeCtrl
控件的依赖属性完整列表如下:
值
public DateTime Value { get; set; }
默认值为您机器上设置的当前日期/时间。允许获取/设置 DateTime
的时间部分。
TimePattern
public string TimePattern { get; set; }
默认值为您机器上设置的长时格式。TimePattern
允许您设置输入时间的方式。几乎任何字符串都是允许的,并且您可以完全重复使用字段。有效标签在此链接的底部有描述:MSDN DateTime format info。由于这里没有使用所有这些标签,因此下方包含相关标签的列表:
- HH, H:0 到 23 小时。HH:如果小于 10,则数字前面加“0”。H:如果小于 10,则不加“0”。
- hh, h:1 到 12 小时。hh:如果小于 10,则数字前面加“0”。h:如果小于 10,则不加“0”。
- mm, m:0 到 59 分钟。mm:如果小于 10,则数字前面加“0”。m:如果小于 10,则不加“0”。
- ss, s:0 到 59 秒。ss:如果小于 10,则数字前面加“0”。s:如果小于 10,则不加“0”。
- tt, t:AM/PM 标签。在英国,“AM”或“PM”。tt:完整的两位字符标签。t:一位字符标签。在英国,是第一个字符,但在标签首字母相同的语言中可以是第二个字符。
UseValidTimes
public bool UseValidTimes { get; set; }
默认值为 false。将 UseValidTimes
设置为 true 可使用此控件的有效时间功能 – 请参见下方。将其设置为 false 可禁用该功能。
ValidTimesName
public string ValidTimesName { get; set; }
默认值为与相关 *.resx 文件中 VALID_TIMES
标签匹配的语言字符串。此字符串是通用的,您可以将其替换为在控件使用上下文中更有意义的内容,例如“营业时间”。右键单击时,您将看到此字符串。如果选择弹出菜单的最后一项,此字符串将作为有效时间列表的标题重新出现。
NoValidTimesString
public string NoValidTimesString { get; set; }
默认值为与相关 *.resx 文件中 NONE
标签匹配的语言字符串。此字符串是通用的,您可以将其替换为更有意义的内容。在英文机器上,如果 TimeCtrl
没有 ValidTimeItem
,则在有效时间弹出窗口中会显示“(无)”。例如,这可能发生在星期日,如果您已将 ValidTimesName
设置为“营业时间”,则更具意义的字符串将是“打烊”。
InvalidTimeTextBrush
public Brush InvalidTimeTextBrush { get; set; }
默认值为 Red。这是无效时间显示的颜色。
IsValidTime
public bool IsValidTime { get; }
默认值为 true。**只读**。指示控件当前是否显示有效时间。
TextAlignment
public TextAlignment TextAlignment { get; set; }
默认值为 TextAlignment.Left
。指示文本对齐方式,类似于 TextBox
的 TextAlignment
。只处理 Left
、Right
和 Center
TextAlignment
。
屏幕截图
已知问题
- 在 Aero 风格下,当鼠标光标悬停在插入的时间项上时,
TimeCtrl
的框架不会高亮显示。
历史
- 2011 年 10 月 11 日:初始版本。
- 2011 年 10 月 13 日:新版本,包含以下更改:
- 允许用户输入超出范围的数字。如果退出字段时数字超出范围,则会显示最后一个有效输入。
- 新的
NumericUpDown
属性OutOfRangeTextBrush
,允许在值超出范围时以不同的颜色显示文本。 - 将
ArrowRepeatButton
中的CornerCtrlEdge
重命名为更具描述性的IsCornerCtrlCorner
。 - 新的
TimeCtrl
属性NoValidTimesString
,允许覆盖“(打烊)”字符串。
- 2018 年 4 月 26 日:修复了当 am 或 pm 设置为空字符串时发生的崩溃问题。