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;

  #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

Filename Description Version Release Date
KeyboardSupportDemo.zip
  • md5: 90e557815ee49f581745ed334ea2d9ed

Sample project for the adding keyboard accelerators and visual cues to a WinForms control article.

1.0.0.0 04/06/2016 Download

About The Author

Gravatar

The founder of Cyotek, Richard enjoys creating new blog content for the site. Much more though, he likes to develop programs, and can often found writing reams of code. A long term gamer, he has aspirations in one day creating an epic video game. Until that time, he is mostly content with adding new bugs to WebCopy and the other Cyotek products.

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?

Styling with Markdown is supported