Adding keyboard accelerators and visual cues to a WinForms control
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;
private bool _isDefault;
private ButtonState _state;
public Button()
{
this.SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer | ControlStyles.ResizeRedraw, true);
this.SetStyle(ControlStyles.StandardDoubleClick, false);
_state = ButtonState.Normal;
}
[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; }
}
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();
}
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; }
}
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.
Update History
- 2016-06-03 - First published
- 2020-11-21 - Updated formatting
Downloads
Filename | Description | Version | Release Date | |
---|---|---|---|---|
KeyboardSupportDemo.zip
|
Sample project for the adding keyboard accelerators and visual cues to a WinForms control article. |
1.0.0.0 | 04/06/2016 | Download |
Leave a Comment
While we appreciate comments from our users, please follow our posting guidelines. Have you tried the Cyotek Forums for support from Cyotek and the community?