Some weeks ago I was trying to make parts of WebCopy's UI a little bit simpler via the expedient of hiding some of the more advanced (and consequently less used) options. And to do this, I created a basic toggle panel control. This worked rather nicely, and while I was writing it I also thought I'd write a short article on adding keyboard support to WinForm controls - controls that are mouse only are a particular annoyance of mine.
A demonstration control
Below is an fairly simple (but functional) button control that works - as long as you're a mouse user. The rest of the article will discuss how to extend the control to more thoroughly support keyboard users, and you what I describe below in your own controls.
internal sealed class Button : Control, IButtonControl { #region Constants private const TextFormatFlags _defaultFlags = TextFormatFlags.NoPadding | TextFormatFlags.SingleLine | TextFormatFlags.HorizontalCenter | TextFormatFlags.VerticalCenter | TextFormatFlags.EndEllipsis; #endregion #region Fields private bool _isDefault; private ButtonState _state; #endregion #region Constructors public Button() { this.SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer | ControlStyles.ResizeRedraw, true); this.SetStyle(ControlStyles.StandardDoubleClick, false); _state = ButtonState.Normal; } #endregion #region Events [Browsable(false)] [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public new event EventHandler DoubleClick { add { base.DoubleClick += value; } remove { base.DoubleClick -= value; } } [Browsable(false)] [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public new event MouseEventHandler MouseDoubleClick { add { base.MouseDoubleClick += value; } remove { base.MouseDoubleClick -= value; } } #endregion #region Methods protected override void OnBackColorChanged(EventArgs e) { base.OnBackColorChanged(e); this.Invalidate(); } protected override void OnEnabledChanged(EventArgs e) { base.OnEnabledChanged(e); this.SetState(this.Enabled ? ButtonState.Normal : ButtonState.Inactive); } protected override void OnFontChanged(EventArgs e) { base.OnFontChanged(e); this.Invalidate(); } protected override void OnForeColorChanged(EventArgs e) { base.OnForeColorChanged(e); this.Invalidate(); } protected override void OnMouseDown(MouseEventArgs e) { base.OnMouseDown(e); this.SetState(ButtonState.Pushed); } protected override void OnMouseUp(MouseEventArgs e) { base.OnMouseUp(e); this.SetState(ButtonState.Normal); } protected override void OnPaint(PaintEventArgs e) { Graphics g; base.OnPaint(e); g = e.Graphics; this.PaintButton(g); this.PaintText(g); } protected override void OnTextChanged(EventArgs e) { base.OnTextChanged(e); this.Invalidate(); } private void PaintButton(Graphics g) { Rectangle bounds; bounds = this.ClientRectangle; if (_isDefault) { g.DrawRectangle(SystemPens.WindowFrame, bounds.X, bounds.Y, bounds.Width - 1, bounds.Height - 1); bounds.Inflate(-1, -1); } ControlPaint.DrawButton(g, bounds, _state); } private void PaintText(Graphics g) { Color textColor; Rectangle textBounds; Size size; size = this.ClientSize; textColor = this.Enabled ? this.ForeColor : SystemColors.GrayText; textBounds = new Rectangle(3, 3, size.Width - 6, size.Height - 6); if (_state == ButtonState.Pushed) { textBounds.X++; textBounds.Y++; } TextRenderer.DrawText(g, this.Text, this.Font, textBounds, textColor, _defaultFlags); } private void SetState(ButtonState state) { _state = state; this.Invalidate(); } #endregion #region IButtonControl Interface public void NotifyDefault(bool value) { _isDefault = value; this.Invalidate(); } public void PerformClick() { this.OnClick(EventArgs.Empty); } [Category("Behavior")] [DefaultValue(typeof(DialogResult), "None")] public DialogResult DialogResult { get; set; } #endregion }
About mnemonic characters
I'm fairly sure most developers would know about mnemonic characters / keyboard accelerators, but I'll quickly outline regardless. When attached to a UI element, the mnemonic character tells users what key (usually combined with Alt) to press in order to activate it. Windows shows the mnemonic character with an underline, and this is known as a keyboard cue.
For example, File would mean press Alt+F.
Specifying the keyboard accelerator
In Windows programming, you generally use the &
character to denote the mnemonic in a string. So for example, &Demo
means the d
character is the mnemonic. If you actually wanted to display the &
character, then you'd just double them up, e.g. Hello && Goodbye
.
While the underlying Win32 API uses the &
character, and most other platforms such as classic Visual Basic or Windows Forms do the same, WPF uses the _
character instead. Which pretty much sums up all of my knowledge of WPF in that one little fact.
Painting keyboard cues
If you useTextRenderer.DrawText
to render text in your controls (which produces better output than Graphics.DrawString
) then by default it will render keyboard cues.
Older versions of Windows used to always render these cues. However, at some point (with Window 2000 if I remember correctly) Microsoft changed the rules so that applications would only render cues after the user had first pressed the Alt character. In practice, this means you need to check to see if cues should be rendered and act accordingly. There used to be an option to specify if they should always be shown or not, but that seems to have disappeared with the march towards dumbing the OS down to mobile-esque levels.
The first order of business then is to update our PaintText
method to include or exclude keyboard cues as necessary.
private const TextFormatFlags _defaultFlags = TextFormatFlags.NoPadding | TextFormatFlags.SingleLine | TextFormatFlags.HorizontalCenter | TextFormatFlags.VerticalCenter | TextFormatFlags.EndEllipsis; private void PaintText(Graphics g) { // .. snip .. TextRenderer.DrawText(g, this.Text, this.Font, textBounds, textColor, _defaultFlags); }
TextRenderer.DrawText
is a managed wrapper around the DrawTextEx
Win32 API, and most of the members of TextFormatFlags
map to various DT_*
constants. (Except for NoPadding
... I really don't know why TextRenderer
adds left and right padding by default but it's really annoying - I always set NoPadding
(when I'm not directly calling GDI via p/invoke)
As I noted the default behaviour is to draw the cues, so we need to detect when cues should not be displayed and instruct our paint code to skip them. To determine whether or not to display keyboard cues, we can check the ShowKeyboardCues
property of the Control
class. To stop DrawText
from painting the underline, we use the TextFormatFlags.HidePrefix
flag (DT_HIDEPREFIX
).
So we can update our PaintText
method accordingly
private void PaintText(Graphics g) { TextFormatFlags flags; // .. snip .. flags = _defaultFlags; if (!this.ShowKeyboardCues) { flags |= TextFormatFlags.HidePrefix; } TextRenderer.DrawText(g, this.Text, this.Font, textBounds, textColor, flags); }
Now our button will now hide and show accelerators based on how the end user is working.
If for some reason you want to use Graphics.DrawString
, then you can use something similar to the below - just set the HotkeyPrefix
property of a StringFormat
object to be HotkeyPrefix.Show
or HotkeyPrefix.Hide
. Note that the default StringFormat
object doesn't show prefixes, in a nice contradiction to TextRenderer
.
using (StringFormat format = new StringFormat(StringFormat.GenericDefault) { HotkeyPrefix = HotkeyPrefix.Show, Alignment = StringAlignment.Center, LineAlignment =StringAlignment.Center, Trimming = StringTrimming.EllipsisCharacter }) { g.DrawString(this.Text, this.Font, SystemBrushes.ControlText, this.ClientRectangle, format); }
As the above animation is just a GIF file, there's no audio - but when I ran that demo, pressing Alt+D triggered a beep sound as there was nothing on the form that could handle the accelerator.
Painting focus cues
Focus cues are highlights that show which element has the keyboard focus. Traditionally Windows would draw a dotted outline around the text of an element that performs a single action (such as a button or checkbox), or draws an item using both a different background and foreground colours for an element that has multiple items (such as a listbox or a menu). Normally (for single action controls at least) focus cues only appear after the Tab key has been pressed, memory fails me as to whether this has always been the case or if Windows use to always show a focus cue.
You can use the Focused
property of a Control
to determine if it currently has keyboard focus and the ShowFocusCues
property to see if the focus state should be rendered.
After that, the simplest way of drawing a focus rectangle would be to use the ControlPaint.DrawFocusRectangle
. However, this draws using fixed colours. Old-school focus rectangles inverted the pixels by drawing with a dotted XOR pen, meaning you could erase the focus rectangle by simply drawing it again - this was great for rubber banding (or dancing ants if you prefer). If you want that type of effect then you can use the DrawFocusRect
Win32 API.
private void PaintButton(Graphics g) { // .. snip .. if (this.ShowFocusCues && this.Focused) { bounds.Inflate(-3, -3); ControlPaint.DrawFocusRectangle(g, bounds); } }
Notice in the demo above how focus cues and keyboard cues are independent from each other.
So, about those accelerators
Now that we've covered painting our control to show focus / keyboard cues as appropriate, it's time to actually handle accelerators. Once again, the Control
class has everything we need built right into it.
To start with, we override the ProcessMnemonic
method. This method is automatically called by .NET when a user presses an Alt key combination and it is up to your component to determine if it should process it or not. If the component can't handle the accelerator, then it should return false
. If it can, then it should perform the action and return true
. The method includes a char
argument that contains the accelerator key (e.g. just the character code, not the alt modifier).
So how do you know if your component can handle it? Luckily the Control
class offers a static IsMnemonic
method that takes a char
and a string
as arguments. It will return true
if the source string contains a mnemonic matching the passed character. Note that it expects the &
character is used to identify the mnemonic. I assume WPF has a matching version of this method, but I don't know where.
We can now implement the accelerator handling quite simply using the following snippet
protected override bool ProcessMnemonic(char charCode) { bool processed; processed = this.CanFocus && IsMnemonic(charCode, this.Text); if (processed) { this.Focus(); this.PerformClick(); } return processed; }
We check to make sure the control can be focused in addition to checking if our control has a match for the incoming mnemonic, and if both are true then we set focus to the control and raise the Click
event. If you don't need (or want) to set focus to the control, then you can skip the CanFocus
check and Focus
call.
Bonus Points: Other Keys
Some controls accept other keyboard conventions. For example, a button accepts the Enter or Space keys to click the button (the former acting as an accelerator, the latter acting as though the mouse were being pressed and released), combo boxes accept F4 to display drop downs and so on. If your control mimics any standard controls, it's always worthwhile adding support for these conventions too. And don't forget about focus!
For example, in the sample button, I modify OnMouseDown
to set focus to the control if it isn't already set
protected override void OnMouseDown(MouseEventArgs e) { base.OnMouseDown(e); if (this.CanFocus) { this.Focus(); } this.SetState(ButtonState.Pushed); }
I also add overrides for OnKeyDown
and OnKeyUp
to mimic the button being pushed and then released when the user presses and releases the space bar
protected override void OnKeyDown(KeyEventArgs e) { base.OnKeyDown(e); if(e.KeyCode == Keys.Space && e.Modifiers == Keys.None) { this.SetState(ButtonState.Pushed); } } protected override void OnKeyUp(KeyEventArgs e) { base.OnKeyUp(e); if((e.KeyCode & Keys.Space) == Keys.Space) { this.SetState(ButtonState.Normal); this.PerformClick(); } }
However, I'm not adding anything to handle the enter key. This is because I don't need to - in this example, the Button
control implements the IButtonControl
interface and so it's handled for me without any special actions. For non-button controls, I would need to explicitly handle enter key presses if appropriate.
Downloads
- KeyboardSupportDemo.zip (10.17 KB)
All content Copyright (c) by Cyotek Ltd or its respective writers. Permission to reproduce news and web log entries and other RSS feed content in unmodified form without notice is granted provided they are not used to endorse or promote any products or opinions (other than what was expressed by the author) and without taking them out of context. Written permission from the copyright owner must be obtained for everything else.
Original URL of this content is http://www.cyotek.com/blog/adding-keyboard-accelerators-and-visual-cues-to-a-winforms-control?source=rss.