Introduction
WPF allows strict separation between layout/look and the underlying functionality. Compared to MFC, WinForms, or other GUI frameworks, it is extremely easy to change the look of your combo box controls, for instance.
I feel that with this feature, all published WPF controls should support all main themes with possibility of supporting others. I think it is strange that one should use a control that looks differently from the ones surrounding it. I have seen several up/down controls, including the one in CodePlex, but none of these have full theme support for the up/down buttons on right hand side.
This article/code sample is about arrow buttons, and as these are useless on their own, I have written a couple of controls that use them.
ArrowRepeatButton
I shall start by discussing the arrow button, as this forms the basis of the whole project.
ArrowRepeatButton
is a WPF custom control that derives from RepeatButton
. Essentially, this control is meant to be inserted into another control and has four types of buttons, each with a different arrow drawn on top: up, down, left, and right. The arrow type is set with the dependency property ButtonArrowType
. You need to pass an enum ButtonArrowType
of type Down
, Up
, Left
, or Right
.
The other dependency property IsCornerCtrlCorner
is used to indicate which corners of the button are also in corners of the container control. This information is required, because some themes have rounding in such cases, as for instance does the Aero theme.
The information is passed using the IsCornerCtrlCorner
struct. This struct takes four booleans in the order top left, top right, bottom right, bottom left.
In XAML, you can write for instance:
<local:ArrowRepeatButton x:Name="UpButton"
ButtonArrowType="Up" IsCornerCtrlCorner="False,True,False,False"/>
This would indicate a button with an up arrow whose top right hand corner is on one of the corners of the control.
The XAML drawing of the buttons is done in the files located in the Theme folder.
For Luna and Royale themes, I have cheated by using the standard scroll buttons for drawing the arrow buttons. This is reasonable, since these look exactly the same and there is no special rounding in corners.
For Classic theme, I have partly used in-built features. The arrow drawings themselves had to be coded.
The Aero theme is more interesting, as it has extra rounding on corners adjacent to the edge of the control. You need to look carefully at the button to see this. It is not possible to use the theme in-built drawing for buttons, because this offers no control for rounding of corners: Microsoft_Windows_Themes:ButtonChrome
has a single property RoundCorners
, which can be set to either True or False. When set to False, the left hand side of the button is right angled, while the right hand side is slightly rounded. We want to be able to choose which corners are rounded and which are not. Therefore, it has been necessary to write XAML code that draws the buttons from scratch.
The extra rounding on the corners of the buttons adjacent to the side of the control is calculated using the IsCornerCtrlCornerToRadiusConverter
converter. The extra rounding amount is specified in ConverterParameter
.
Actually, things are more elaborate as it is possible to have rounding for a corner not adjacent to a corner of the container control. To do this, you still pass an int
in the ConverterParameter
. The non-adjacent corner rounding amount is given by right shifting this int
8 bits. For example, we want rounding of 2 for non-adjacent, and 4 for adjacent. We pass 2 << 8 | 4 = 516, or alternatively: 0x204. Both decimal and hexadecimal notations are handled.
For Aero, the theme is set in Aero.NormalColor.XAML, and the rounding of the corners is given by the following XAML code:
CornerRadius="{Binding IsCornerCtrlCorner, RelativeSource={RelativeSource FindAncestor,
AncestorType={x:Type local:ArrowRepeatButton}},
Converter={StaticResource IsCornerCtrlCornerToRadiusConverter},
ConverterParameter=0x2}">
The full description of the two ArrowRepeatButton
dependency properties are as follows:
ButtonArrowType
public ButtonArrowType ButtonArrowType { get; set; }
where ButtonArrowType
is an enum defined as:
public enum ButtonArrowType : byte
{
Down,
Up,
Left,
Right
}
The default is ButtonArrowType.Down
. This just displays the relevant arrow on the button.
IsCornerCtrlCorner
public IsCornerCtrlCorner IsCornerCtrlCorner { get; set; }
where IsCornerCtrlCorner
is a struct defined as (with only constructors and member variables shown):
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);
}
The default is IsCornerCtrlCorner(false, true, true, false)
, which is a button on the right hand side of a container control.
Utilities: UpDownButtons, TextBox frame, multi-language support...
In order to facilitate use of ArrowRepeatButton
and avoid code duplicates, some utilities have been created.
UpDownButtons
UpDownButtons
is just a WPF User Control with two ArrowRepeatButton
s and message handling for when user presses these.
TextBox frame
The control frame used is a TextBox
for both a NumericUpDown
and a TimeCtrl
control.
One point of interest is XAML styles. Apart from avoiding a lot of code duplicates, style
can also be used to support themes.
For instance, in Aero theme, the up/down buttons reach the actual border of the control. For other themes used, these up/down buttons have to fit inside a TextBox
frame. To see this, you just need to look at UpDownButtonsStyle
within the Theme *.xaml files. You can actually bind to an element only defined in the caller of the style, and not defined within where the style itself is defined, which is great.
To get buttons to fit properly inside the frame, a theme specific ThicknessToMarginConverter
had to be used as well. This converter converts a border thickness into a margin. The ConvertParameter
is a Boolean used to indicate how the conversion should take place.
Static coerce functions were required for both NumericUpDown
and TimeCtrl
. These are needed for adjusting BorderThickness
, Background
, and BorderBrush
correctly. The reasons these are required are because:
BorderThickness
: you never want to enable change of BorderThickness
of the UserControl
. Doing otherwise will display another frame. Instead, we want to manipulate directly the thickness of the bounding TextBox
. Background
and BorderBrush
: These have default values for TextBox
. Trying out a Binding in XAML will remove these defaults. This is a problem if we do not set the field.
To avoid code duplicates, a static class Coercer
was created in the Helper.cs file which has all the coerce functions. All that is required is that both NumericUpDown
and TimeCtrl
support IframeTxtBoxCtrl
, defined as:
internal interface IFrameTxtBoxCtrl
{
TextBox TextBox { get; }
}
TextBox
returns the relevant TextBox
of the control.
Multi-language support
As it was desirable for the controls not to be stuck within one Culture and a few strings were required for the TimeCtrl
popup menu, it made sense for this project to support language dependent strings.
The strings are stored in Resources.resx. French and German versions have been created. This was done by copying Resources.resx and renaming the copy with the relevant language tag inserted. To find out the language tag used in your machine: simply put a breakpoint in the function GetLangStr
of the static class LanguageStrings
, defined at the bottom of Helpers.cs. You will see the value straight away. Once you have created your Resources.xx.XX.resx, you need to add it to the project and move it to the folder Properties. You should get a message telling you the file already exists – just click ‘Yes’ and overwrite the file with its identical copy.
There is auto-generated code in Resources.Designer.cs, with code for retrieving these strings, but this does not seem to work for other Resources.xx.XX.resx files. This is why the static class LanguageStrings
has been used for obtaining culture-dependent strings instead.
NumericUpDown control
Other programmers have already written WPF numeric up/down controls. There is one in the WPF extended toolkit: WPF Extended Toolkit DecimalUpDown.
It should be possible to re-use the ArrowRepeatButton
on most other controls. Nevertheless, the author has written another version from scratch.
This version behaves more like the NumberBox
control, already published on CodeProject by the same author, see NumberBox UserControl.
The WPF Extended Toolkit version validates an entry on exiting the field. There are no restrictions on the characters you can type. You can even type letters of the alphabet. Once you have exited the field, the last valid entry is re-displayed. The font color is not updated to gray when the field is disabled. Maybe this is because it can also run in Silverlight?
This version will restrict entries on input. It is not possible to enter an illegal character. Only numbers can be entered. If the value entered is less than Minimum
or greater than Maximum
, then it will be erased on exiting the field and the last valid entry will be redisplayed instead. It is also possible to erase everything using delete or backspace keyboard keys. In this situation, similarly the last valid input will be re-displayed on exiting the field.
When the user is entering a value that is out of range, it is possible to set the text brush to a different color using the “OutOfRangeTextBrush
” dependency property.
If Minimum
is less than zero, it is possible to enter a negative number. To do this, you need to type the negative symbol, which is usually the minus (‘-’) sign. This works the same way as with calculators: on pressing the first time, the number is negative, on pressing again, it becomes positive again. Caret position is irrelevant.
Drag and drop has been restricted. Cut/Copy/Paste is allowed when it is possible to keep the number valid. This is not always so: for instance, you have a maximum of 1000, the user types “1000.00” and then selects the ‘0.’ part and attempts ‘cut’. This is disallowed, because otherwise you would have a number “10000”, which is greater than 1000.
Otherwise, NumericUpDown
can be configured so that numbers are entered in different formats: the programmer can select whether the negative symbol is prefixed or suffixed, whether the decimal separator is a point, a comma, or system set, etc... See the full list of dependency properties below.
To increment/decrement a number, you can:
- Press the up/down arrow buttons on the right had side of the control.
- Press the up and down buttons on the keyboard.
- Put the mouse cursor over the number you want to increase/decrease and use the mouse wheel.
The list of dependency properties for the NumericUpDown
control are as follows:
Value
public decimal Value { get; set; }
Default is 0. This property sets/gets the number displayed in the control. If Value
< Minimum
, Value
gets set to Minimum
, if Value
> Maximum
, Value
gets set to Maximum
.
Maximum
public decimal Maximum { get; set; }
Default is 100. This property sets/gets the maximum allowed number. It is not possible to increment or to enter a number greater than Maximum
.
Minimum
public decimal Minimum { get; set; }
Default is 0. This property sets/gets the minimum allowed number. It is not possible to decrement or to enter a number less than Minimum
.
Step
public decimal Step { get; set; }
Default is 1. Indicates the increment/decrement amount on pressing the up/down arrow button.
DecimalPlaces
public short DecimalPlaces { get; set; }
Default is 0. This sets/gets the number of decimal places allowed after the decimal separator. If set to 0 or less, the user can no longer enter the decimal separator.
DecimalSeparatorType
public DecimalSeparatorType DecimalSeparatorType { get; set; }
Where:
public enum DecimalSeparatorType : byte
{
System_Defined,
Point,
Comma
}
Default is System_Defined
. In the UK or US, a point is used as decimal separator in a number such as ‘pi = 3.14’. In France, a comma is used instead, as in ‘pi = 3,14’. This property enables a NumericUpDown
to supersede the system setting.
NegativeSignType
public NegativeSignType NegativeSignType { get; set; }
Where:
public enum NegativeSignType : byte
{
System_Defined,
Minus
}
Default is System_Defined
. A negative number is usually represented by the minus sign (‘-’). There is a default setting that allows you to represent negative numbers with a sign other than ‘-’. This property allows you to overload the default setting with a minus sign.
NegativeSignSide
public NegativeSignSide NegativeSignSide { get; set; }
Where:
public enum NegativeSignSide : byte
{
System_Defined,
Prefix,
Suffix
}
Default is System_Defined
. In some cultures, negative numbers are suffixed with the negative sign symbol, rather than prefixed. This property allows you to either use the system setting (default), or specify whether the negative symbol should be the prefix or suffix of a number. If you choose System_Defined
, assume it is prefix if the system format is set to ‘(1.1)’, ‘-1.1’, or ‘- 1.1’. Otherwise, it is suffix.
NegativeTextBrush
public Brush NegativeTextBrush { get; set; }
Default is null (not set). This property allows you to set negative numbers in a color different from positive numbers. A common practice is to show negatives in red.
OutOfRangeTextBrush
public Brush OutOfRangeTextBrush { get; set; }
Default is null (not set). This property allows you to set the text brush to a different color if the user is entering an entry that is out of range. Supersedes the NegativeTextBrush
property. Purpose is to give user a warning that value being entered will be erased on exiting the field unless modified so it is within a valid range.
TextAlignment
public TextAlignment TextAlignment { get; set; }
Default is TextAlignment.Right
. Indicates text alignment, similar to the TextAlignment
of TextBox
. Only Left
, Right
, and Center
TextAlignment
handled.
TimeCtrl
WPF already has a DatePicker
. Sometimes a user will want to select a time. This could either be incorporated in the DatePicker
or included in a separate control.
One common practice is to attempt the equivalent for time controls as for dates and to display a clock face that appears when pressing a drop down button. It is not very practical to select a time this way: whereas there are maximum 31 days in a month, there are 86400 seconds in a day!
Therefore, the author has coded this TimeCtrl
, which uses the up/down buttons.
The time format displayed should be the same as the system setting on your computer, but it is possible to configure that to any format you wish. See the TimePattern
dependency property below.
Each hour/minute/second field is in fact an independent frameless TextBox
field, and as such, it is only possible to select one at once.
Similar to the NumericUpDown
, it is not possible to enter an invalid value for a time field. This leads to some interesting results: for a 12-hour field which has a range 1 to 12, ‘0’ is an invalid entry, and selecting all and typing ‘0’ will result in erasing the field. My Windows 7 clock behaves the same way.
Drag and drop has been completely disabled. Copy/cut/paste has been restricted to insure that entries always remain valid.
Incrementing/decrementing values is done exactly the same way as with the NumericUpDown
control: up/down arrow buttons, keyboard up/down buttons, and mouse wheel over control.
In addition, fields rotate: incrementing ‘59’ for minutes and seconds will display ‘0’ or ‘00’.
Because it is only possible to select one field at a time, a couple of extra right-click menu items have been added: ‘Copy Time’ and ‘Paste Time’. These menu items allow you to copy/paste all the fields appearing in the TimeCtrl
. It is only possible to paste a clipboard string that complies with the TimePattern
set for the control. Therefore it is not possible to copy/paste between TimeCtrl
s set with different TimePattern
s.
One thing that is generally missing from time controls is a practical way of showing valid hours, should there be restrictions on these.
In the DatePicker
, you have a popup. This shows a calendar month. In this, you can see additional information such as the day of week. Furthermore, there is a feature enabling you to disable certain dates.
TimeCtrl
has a feature UseValidTimes
, set to false by default. This feature allows restrictions on hours. When UseValidTimes
is set to true, a new menu item will appear: ‘Valid Times’. On selecting this, either “(None)” or a list of valid hours will appear. These need to be inserted as ValidTimeItem
s. The ValidTimeItem
has two dependency properties: BeginTime
and EndTime
, each of type TimeEntry
.
It is possible to add a ValidTimeItem
either in XAML:
<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>
or (C#):
MyTimeCtrl.Children.Add(
new ValidTimeItem(new TimeEntry(9,00,00), new TimeEntry(17,30,00)));
When a time is invalid, it will be shown in another color. By default, this color is red, but it can be set through the InvalidTimeTextBrush
dependency property. If the time is invalid, the IsValidTime
dependency property will return false.
The full list of dependency properties for the TimeCtrl
control is as follows:
Value
public DateTime Value { get; set; }
Default is the current date/time as set on your machine. Enables to get/set the time part of a DateTime
.
TimePattern
public string TimePattern { get; set; }
Default is the long time pattern as set on your machine. TimePattern
allows you to set the way you wish to enter times. Practically any string is allowed, and you are perfectly permitted to duplicate fields if you wish to do so. The valid tags are described at the bottom of this link: MSDN DateTime format info. As not all these tags are used here, a list of relevant ones is included below:
- HH, H: hour from 0 to 23. HH: number is prefixed with '0' if less than 10. H: not prefixed with '0' if less than 10.
- hh, h: hour from 1 to 12. hh: number is prefixed with '0' if less than 10. h: not prefixed with '0' if less than 10.
- mm, m: minutes from 0 to 59. mm: number is prefixed with '0' if less than 10. m: not prefixed with '0' if less than 10.
- ss, s: seconds from 0 to 59. ss: number is prefixed with '0' if less than 10. s: not prefixed with '0' if less than 10.
- tt, t: AM/PM tags. Either "AM" or "PM" in Britain. tt: full two character tag. t: one character tag. In Britain, the first character, but can be second character in languages where the first characters of tags are the same.
UseValidTimes
public bool UseValidTimes { get; set; }
Default is false. Setting UseValidTimes
to true allows you to use the valid times feature of this control – please see below. Setting it to false disables the feature.
ValidTimesName
public string ValidTimesName { get; set; }
Default is the language string that matches the VALID_TIMES
tag in the relevant *.resx file. This string is generic and it is possible to supersede that with something more meaningful in the context in which the control is being used - for instance, 'Opening Hours'. On right clicking, you will see this string appear. If you select the bottom item of the popup menu, this string will re-appear as a header of a list of valid times.
NoValidTimesString
public string NoValidTimesString { get; set; }
Default is the language string that matches the NONE
tag in the relevant *.resx file. This string is generic and it is possible to supersede that with something more meaningful. With English machines, "(None)" will appear in valid times popup if the TimeCtrl
has no ValidTimeItem
. This might happen on a Sunday for instance, and a more meaningful string would be 'Closed' if you have set ValidTimesName
to 'Opening Hours' for instance.
InvalidTimeTextBrush
public Brush InvalidTimeTextBrush { get; set; }
Default is Red. This is the color in which the time is displayed when it is invalid.
IsValidTime
public bool IsValidTime { get; }
Default is true. Read-only. Indicates whether the control is currently displaying a valid time.
TextAlignment
public TextAlignment TextAlignment { get; set; }
Default is TextAlignment.Left
. Indicates text alignment, similar to the TextAlignment
of TextBox
. Only Left
, Right
, and Center
TextAlignment
handled.
Screenshots
Windows 7, Aero:
Windows 7, Classic:
Windows XP, Luna Normal:
Windows XP, Luna Homestead:
Windows XP, Luna Metallic:
Known Issues
- With Aero style, the frame of
TimeCtrl
does not highlight when the mouse cursor is over an inserted time item.
History
- 11th October, 2011: Initial version.
- 13th October, 2011: New version with the following changes:
- Allows user to enter numbers that are out of range. If the number is out of range when exiting the field, then the last valid entry is displayed.
- New
NumericUpDown
property OutOfRangeTextBrush
that allows display of text in a different color when value is out of range. - Renamed
CornerCtrlEdge
in ArrowRepeatButton
to the more meaningful IsCornerCtrlCorner
. - New
TimeCtrl
property NoValidTimesString
that allows superseding of the "(Closed)" string.
- 26th April, 2018: Fixed crash issue when am or pm is set to empty string.