Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

C# WPF .NET 4.0 ArrowRepeatButton, NumericUpDown, and TimeCtrl Controls

0.00/5 (No votes)
14 Oct 2011 1  
User controls for entering either a time or a number within a range.

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.

All up/down button themes

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.

Arrow button demo

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 ArrowRepeatButtons 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 TimeCtrls set with different TimePatterns.

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 ValidTimeItems. 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)));

Valid Times Example

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:

Aero Screenshot

Windows 7, Classic:

Classic Screenshot

Windows XP, Luna Normal:

Luna Normal Screenshot

Windows XP, Luna Homestead:

Luna Homestead Screenshot

Windows XP, Luna Metallic:

Luna Metallic Screenshot

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:
    1. 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.
    2. New NumericUpDown property OutOfRangeTextBrush that allows display of text in a different color when value is out of range.
    3. Renamed CornerCtrlEdge in ArrowRepeatButton to the more meaningful IsCornerCtrlCorner.
    4. 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.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here