Entries tagged with 'combobox'

Creating a WYSIWYG font ComboBox using C#

This article shows how to use the built in ownerdraw functionality of a standard Windows Forms ComboBox control to display a WYSIWYG font list.

Setting up the control

To start, we'll create a new class, and inherit this from the ComboBox control.

We are going to use variable ownerdraw for this sample, as it gives us a little more flexibility without having to mess around with the ItemHeight property. We'll add a constructor, and set the ownerdraw mode here. Also, we'll add a new version of the DrawMode property, which we'll both hide and disable the value persistence. We always want the font list to be sorted, so for now we'll do the same with the Sorted property.

    public FontComboBox()
    {
      this.DrawMode = DrawMode.OwnerDrawVariable;
      this.Sorted = true;
    }

    [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden), EditorBrowsable(EditorBrowsableState.Never)]
    public new DrawMode DrawMode
    {
      get { return base.DrawMode; }
      set { base.DrawMode = value; }
    }

    [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden), EditorBrowsable(EditorBrowsableState.Never)]
    public new bool Sorted
    {
      get { return base.Sorted; }
      set { base.Sorted = value; }
    }

Caching Font objects

In order to avoid continuously creating and destroying font objects, we'll create a internal cache of fonts. When it's time to draw the control, the cache will be queried - if the requested font exists, it will be returned, otherwise the font will be created and added to the cache. This will be done via the GetFont method below.

    protected virtual Font GetFont(string fontFamilyName)
    {
      lock (_fontCache)
      {
        if (!_fontCache.ContainsKey(fontFamilyName))
        {
          Font font;

          font = this.GetFont(fontFamilyName, FontStyle.Regular);
          if (font == null)
            font = this.GetFont(fontFamilyName, FontStyle.Bold);
          if (font == null)
            font = this.GetFont(fontFamilyName, FontStyle.Italic);
          if (font == null)
            font = this.GetFont(fontFamilyName, FontStyle.Bold | FontStyle.Italic);

          _fontCache.Add(fontFamilyName, font);
        }
      }

      return _fontCache[fontFamilyName];
    }

    protected virtual Font GetFont(string fontFamilyName, FontStyle fontStyle)
    {
      Font font;

      try
      {
        font = new Font(fontFamilyName, this.PreviewFontSize, fontStyle);
      }
      catch
      {
        font = null;
      }

      return font;
    }

Note: Whilst testing the control, I discovered that some of the fonts installed on the development system only had bold or italic styles. The original version of this method, which always attempts to get the normal style would cause a crash.

Due to this, I changed the method to try and access the normal style, and if that failed, to try the other styles. Perhaps there is a better way of doing this, but I leave that as an exercise for the future.

As we don't want the font size of the dropdown list to necessarily match that of the display/edit portion, we'll add a new property named PreviewFontSize.

    public event EventHandler PreviewFontSizeChanged;

    [Category("Appearance"), DefaultValue(12)]
    public int PreviewFontSize
    {
      get { return _previewFontSize; }
      set
      {
        _previewFontSize = value;

        this.OnPreviewFontSizeChanged(EventArgs.Empty);
      }
    }

    protected virtual void OnPreviewFontSizeChanged(EventArgs e)
    {
      if (PreviewFontSizeChanged != null)
        PreviewFontSizeChanged(this, e);

      this.CalculateLayout();
    }

When certain actions occur, such as this property changing, we want to calculate the height of items in the dropdown list.

    private void CalculateLayout()
    {
      this.ClearFontCache();

      using (Font font = new Font(this.Font.FontFamily, (float)this.PreviewFontSize))
      {
        Size textSize;

        textSize = TextRenderer.MeasureText("yY", font);
        _itemHeight = textSize.Height + 2;
      }
    }

Loading the list of font families

In order to avoid slowing the control down without reason, we'll delay loading the list of font families until there is a reason - either when the control's text has changed, or when the control gets focus.

This will be done by creating a LoadFontFamilies method which will be called by overriding OnGotFocus and OnTextChanged.

    public virtual void LoadFontFamilies()
    {
      if (this.Items.Count == 0)
      {
        Cursor.Current = Cursors.WaitCursor;

        foreach (FontFamily fontFamily in FontFamily.Families)
          this.Items.Add(fontFamily.Name);

        Cursor.Current = Cursors.Default;
      }
    }

    protected override void OnGotFocus(EventArgs e)
    {
      this.LoadFontFamilies();

      base.OnGotFocus(e);
    }

    protected override void OnTextChanged(EventArgs e)
    {
      base.OnTextChanged(e);

      if (this.Items.Count == 0)
      {
        int selectedIndex;

        this.LoadFontFamilies();

        selectedIndex = this.FindStringExact(this.Text);
        if (selectedIndex != -1)
          this.SelectedIndex = selectedIndex;
      }
    }

Drawing the items

Drawing an overdraw ComboBox is done by overriding the OnDrawItem method. However, as we have told the control we are doing variable sized ownerdraw, we also need to override OnMeasureItem. This method allows us to define the size for each item, or in the case of this control to set the height of each item to match the pixel height calculated for the value of the PreviewFontSize property.

    protected override void OnMeasureItem(MeasureItemEventArgs e)
    {
      base.OnMeasureItem(e);

      if (e.Index > -1 && e.Index < this.Items.Count)
      {
        e.ItemHeight = _itemHeight;
      }
    }

 protected override void OnDrawItem(DrawItemEventArgs e)
    {
      base.OnDrawItem(e);

      if (e.Index > -1 && e.Index < this.Items.Count)
      {
        e.DrawBackground();

        if ((e.State & DrawItemState.Focus) == DrawItemState.Focus)
          e.DrawFocusRectangle();

        using (SolidBrush textBrush = new SolidBrush(e.ForeColor))
        {
          string fontFamilyName;

          fontFamilyName = this.Items[e.Index].ToString();
          e.Graphics.DrawString(fontFamilyName, this.GetFont(fontFamilyName), textBrush, e.Bounds, _stringFormat);
        }
      }
    }

The actual drawing is very simple - we use the built in drawing for the background and focus rectangle, and then use the Graphics object to draw the text using the GetFont method explained above.

You might notice that the above code is referencing a previously defined StringFormat object. This is created using the below method.

    protected virtual void CreateStringFormat()
    {
      if (_stringFormat != null)
        _stringFormat.Dispose();

      _stringFormat = new StringFormat(StringFormatFlags.NoWrap);
      _stringFormat.Trimming = StringTrimming.EllipsisCharacter;
      _stringFormat.HotkeyPrefix = HotkeyPrefix.None;
      _stringFormat.Alignment = StringAlignment.Near;
      _stringFormat.LineAlignment = StringAlignment.Center;

      if (this.IsUsingRTL(this))
        _stringFormat.FormatFlags |= StringFormatFlags.DirectionRightToLeft;
    }

    private bool IsUsingRTL(Control control)
    {
      bool result;

      if (control.RightToLeft == RightToLeft.Yes)
        result = true;
      else if (control.RightToLeft == RightToLeft.Inherit && control.Parent != null)
        result = IsUsingRTL(control.Parent);
      else
        result = false;

      return result;
    }

Cleaning up

As we are creating a large number of objects, we need to clean these up in the controls Dispose method.

    protected override void Dispose(bool disposing)
    {
      this.ClearFontCache();

      if (_stringFormat != null)
        _stringFormat.Dispose();

      base.Dispose(disposing);
    }

    protected virtual void ClearFontCache()
    {
      if (_fontCache != null)
      {
        foreach (string key in _fontCache.Keys)
          _fontCache[key].Dispose();
        _fontCache.Clear();
      }
    }

Suggestions for improvement

The control as it stands is a basic example, and depending on your application's needs, it could be further expanded. For example:

  • Currently each instance of the control will use its own font cache. By making the cache and access methods static, a single cache could be used by all instances
  • When you select a font in Word, this is added to a kind of "recently used" list at the top of Word's own font picker. The same sort of functionality could be quite easily added to this control.
  • Currently the font text is displayed on a single line. If the control isn't wide enough, the text is trimmed and therefore it may not be always possible to tell the full name of a font. Either tooltip support or drawing across multiple lines could help with this, or by resizing the dropdown component to be the minimum width required to display all font names without trimming.

Full source

The full source of the class is below.

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Drawing.Text;
using System.Windows.Forms;

namespace Cyotek.Windows.Forms
{
  public class FontComboBox : ComboBox
  {
  #region  Private Member Declarations  

    private Dictionary<string, Font> _fontCache;
    private int _itemHeight;
    private int _previewFontSize;
    private StringFormat _stringFormat;

  #endregion  Private Member Declarations  

  #region  Public Constructors  

    public FontComboBox()
    {
      _fontCache = new Dictionary<string, Font>();

      this.DrawMode = DrawMode.OwnerDrawVariable;
      this.Sorted = true;
      this.PreviewFontSize = 12;

      this.CalculateLayout();
      this.CreateStringFormat();
    }

  #endregion  Public Constructors  

  #region  Events  

    public event EventHandler PreviewFontSizeChanged;

  #endregion  Events  

  #region  Protected Overridden Methods  

    protected override void Dispose(bool disposing)
    {
      this.ClearFontCache();

      if (_stringFormat != null)
        _stringFormat.Dispose();

      base.Dispose(disposing);
    }

    protected override void OnDrawItem(DrawItemEventArgs e)
    {
      base.OnDrawItem(e);

      if (e.Index > -1 && e.Index < this.Items.Count)
      {
        e.DrawBackground();

        if ((e.State & DrawItemState.Focus) == DrawItemState.Focus)
          e.DrawFocusRectangle();

        using (SolidBrush textBrush = new SolidBrush(e.ForeColor))
        {
          string fontFamilyName;

          fontFamilyName = this.Items[e.Index].ToString();
          e.Graphics.DrawString(fontFamilyName, this.GetFont(fontFamilyName), textBrush, e.Bounds, _stringFormat);
        }
      }
    }

    protected override void OnFontChanged(EventArgs e)
    {
      base.OnFontChanged(e);

      this.CalculateLayout();
    }

    protected override void OnGotFocus(EventArgs e)
    {
      this.LoadFontFamilies();

      base.OnGotFocus(e);
    }

    protected override void OnMeasureItem(MeasureItemEventArgs e)
    {
      base.OnMeasureItem(e);

      if (e.Index > -1 && e.Index < this.Items.Count)
      {
        e.ItemHeight = _itemHeight;
      }
    }

    protected override void OnRightToLeftChanged(EventArgs e)
    {
      base.OnRightToLeftChanged(e);

      this.CreateStringFormat();
    }

    protected override void OnTextChanged(EventArgs e)
    {
      base.OnTextChanged(e);

      if (this.Items.Count == 0)
      {
        int selectedIndex;

        this.LoadFontFamilies();

        selectedIndex = this.FindStringExact(this.Text);
        if (selectedIndex != -1)
          this.SelectedIndex = selectedIndex;
      }
    }

  #endregion  Protected Overridden Methods  

  #region  Public Methods  

    public virtual void LoadFontFamilies()
    {
      if (this.Items.Count == 0)
      {
        Cursor.Current = Cursors.WaitCursor;

        foreach (FontFamily fontFamily in FontFamily.Families)
          this.Items.Add(fontFamily.Name);

        Cursor.Current = Cursors.Default;
      }
    }

  #endregion  Public Methods  

  #region  Public Properties  

    [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden), EditorBrowsable(EditorBrowsableState.Never)]
    public new DrawMode DrawMode
    {
      get { return base.DrawMode; }
      set { base.DrawMode = value; }
    }

    [Category("Appearance"), DefaultValue(12)]
    public int PreviewFontSize
    {
      get { return _previewFontSize; }
      set
      {
        _previewFontSize = value;

        this.OnPreviewFontSizeChanged(EventArgs.Empty);
      }
    }

    [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden), EditorBrowsable(EditorBrowsableState.Never)]
    public new bool Sorted
    {
      get { return base.Sorted; }
      set { base.Sorted = value; }
    }

  #endregion  Public Properties  

  #region  Private Methods  

    private void CalculateLayout()
    {
      this.ClearFontCache();

      using (Font font = new Font(this.Font.FontFamily, (float)this.PreviewFontSize))
      {
        Size textSize;

        textSize = TextRenderer.MeasureText("yY", font);
        _itemHeight = textSize.Height + 2;
      }
    }

    private bool IsUsingRTL(Control control)
    {
      bool result;

      if (control.RightToLeft == RightToLeft.Yes)
        result = true;
      else if (control.RightToLeft == RightToLeft.Inherit && control.Parent != null)
        result = IsUsingRTL(control.Parent);
      else
        result = false;

      return result;
    }

  #endregion  Private Methods  

  #region  Protected Methods  

    protected virtual void ClearFontCache()
    {
      if (_fontCache != null)
      {
        foreach (string key in _fontCache.Keys)
          _fontCache[key].Dispose();
        _fontCache.Clear();
      }
    }

    protected virtual void CreateStringFormat()
    {
      if (_stringFormat != null)
        _stringFormat.Dispose();

      _stringFormat = new StringFormat(StringFormatFlags.NoWrap);
      _stringFormat.Trimming = StringTrimming.EllipsisCharacter;
      _stringFormat.HotkeyPrefix = HotkeyPrefix.None;
      _stringFormat.Alignment = StringAlignment.Near;
      _stringFormat.LineAlignment = StringAlignment.Center;

      if (this.IsUsingRTL(this))
        _stringFormat.FormatFlags |= StringFormatFlags.DirectionRightToLeft;
    }

    protected virtual Font GetFont(string fontFamilyName)
    {
      lock (_fontCache)
      {
        if (!_fontCache.ContainsKey(fontFamilyName))
        {
          Font font;

          font = this.GetFont(fontFamilyName, FontStyle.Regular);
          if (font == null)
            font = this.GetFont(fontFamilyName, FontStyle.Bold);
          if (font == null)
            font = this.GetFont(fontFamilyName, FontStyle.Italic);
          if (font == null)
            font = this.GetFont(fontFamilyName, FontStyle.Bold | FontStyle.Italic);
          if (font == null)
            font = (Font)this.Font.Clone();

          _fontCache.Add(fontFamilyName, font);
        }
      }

      return _fontCache[fontFamilyName];
    }

    protected virtual Font GetFont(string fontFamilyName, FontStyle fontStyle)
    {
      Font font;

      try
      {
        font = new Font(fontFamilyName, this.PreviewFontSize, fontStyle);
      }
      catch
      {
        font = null;
      }

      return font;
    }

    protected virtual void OnPreviewFontSizeChanged(EventArgs e)
    {
      if (PreviewFontSizeChanged != null)
        PreviewFontSizeChanged(this, e);

      this.CalculateLayout();
    }

  #endregion  Protected Methods  
  }
}
2 comments | | Trackback specific URL for this entry

Adding a horizontal scrollbar to a ComboBox using C#

In our WebCopy application we decided to update the User Agent configuration to allow selection from a predefined list of common agents, but still allow the user to enter their own custom agent if required.

Rather than use two separate fields, we choose to use a ComboBox in simple mode, which is both a textbox and a listbox in a single control. This mode seems somewhat out of fashion, I think the only place I see it used is in the Font common dialog, virtually unchanged since Windows 3.1.

The problem was immediately apparent however on firing up WebCopy and going to select a user agent - the agent strings can be very long, far longer than the width of the control.

Unfortunately however, the .NET ComboBox doesn't allow you to directly enable horizontal scrolling. So we'll do it the old fashioned way using the Windows API.

In order for a window to support horizontal scrolling, it needs to have the WS_HSCROLL style applied to it. And to setup the horizontal scrollbar, we need to call the SendMessage API with the CB_SETHORIZONTALEXTENT message.

As usual, we'll be starting off by creating a new Component, which we'll inherit from ComboBox.

Traditionally, you would call GetWindowLong and SetWindowLong API's with the GWL_STYLE or GWL_EXSTYLE flags. However, we can more simply override the CreateParams property of our component and set the new style when the control is created.

protected override CreateParams CreateParams
{
  get
  {
    CreateParams createParams;

    createParams = base.CreateParams;
    createParams.Style |= WS_HSCROLL;

    return createParams;
  }
}

With that done, we can now inform Windows of the size of the horizontal scroll area, and it will automatically add the scrollbar if required. To do this, I'll add two new methods to the component. The first will set the horizontal extent to a given value. The second will calculate the length of the longest piece of text in the control and then set the extent to match.

public void SetHorizontalExtent()
{
  int maxWith;

  maxWith = 0;
  foreach (object item in this.Items)
  {
    Size textSize;

    textSize = TextRenderer.MeasureText(item.ToString(), this.Font);
    if (textSize.Width > maxWith)
      maxWith = textSize.Width;
  }

  this.SetHorizontalExtent(maxWith);
}

public void SetHorizontalExtent(int width)
{
  SendMessage(this.Handle, CB_SETHORIZONTALEXTENT, new IntPtr(width), IntPtr.Zero);
}

The first overload of SetHorizontalExtent iterates through all the items in the control and uses the TextRenderer object to measure the size of the text. Once it has found the largest piece of text, it calls the second overload with the size.

The second overload does the actual work of notifying Windows using the SendMessage call, CB_SETHORIZONTALEXTENT message and the given width. SendMessage takes two configuration parameters per message, but CB_SETHORIZONTALEXTENT only requires one, and so we send 0 for the second.

The above function works with all display modes of the ComboBox.

For completeness, here are the API declarations we are using:

private const int WS_HSCROLL = 0x100000;
private const int CB_SETHORIZONTALEXTENT = 0x015E;

[DllImport("user32.dll")]
private static extern IntPtr SendMessage(IntPtr hWnd, UInt32 msg, IntPtr wParam, IntPtr lParam);

As usual, a demonstration project is available from the link below.

Downloads:

  • horizontallyscrollingcombobox.zip

    (11.88 KB | 13 July 2010 )

    Sample C# project showing how to add a horizontal scrollbar to a ComboBox in C# using the WS_HSCROLL style and CB_SETHORIZONTALEXTENT message.

3 comments | | Trackback specific URL for this entry
  • Page 1 of 1
  • First
  • Previous
  • 1
  • Next
  • Last