Entries tagged with 'winforms'

Extending the ImageBox component to display the contents of a PDF file using C#

In this article, I'll describe how to extend the ImageBox control discussed in earlier articles to be able to display PDF files with the help of the GhostScript library and the conversion library described in the previous article.

Getting Started

You can download the source code used in this article from the links below, these are:

  • Cyotek.GhostScript - core library providing GhostScript integration support
  • Cyotek.GhostScript.PdfConversion - support library for converting a PDF document into images
  • PdfImageBoxSample - sample project containing an updated ImageBox control, and the extended PdfImageBox.

Please note that the native GhostScript DLL is not included in these downloads, you will need to obtain that from the GhostScript project page.

Extending the ImageBox

To start extending the ImageBox, create a new class and inherit the ImageBox control. I also decided to override some of the default properties, so I added a constructor which sets the new values.

    public PdfImageBox()
    {
      // override some of the original ImageBox defaults
      this.GridDisplayMode = ImageBoxGridDisplayMode.None;
      this.BackColor = SystemColors.AppWorkspace;
      this.ImageBorderStyle = ImageBoxBorderStyle.FixedSingleDropShadow;

      // new pdf conversion settings
      this.Settings = new Pdf2ImageSettings();
    }

To ensure correct designer support, override versions of the properties with new DefaultValue attributes were added. With this done, it's time to add the new properties that will support viewing PDF files. The new properties are:

  • PdfFileName - the filename of the PDF to view
  • PdfPassword - specifies the password of the PDF file if one is required to open it (note, I haven't actually tested that this works!)
  • Settings - uses the Pdf2ImageSettings class discussed earlier to control quality settings for the converted document.
  • PageCache - an internal dictionary which stores a Bitmap against a page number to cache pages after these have loaded.

With the exception of PageCache, each of these properties also has backing event for change notifications, and as Pdf2ImageSettings implements INotifyPropertyChanged we'll also bind an event detect when the individual setting properties are modified.

    [Category("Appearance"), DefaultValue(typeof(Pdf2ImageSettings), "")]
    public virtual Pdf2ImageSettings Settings
    {
      get { return _settings; }
      set
      {
        if (this.Settings != value)
        {
          if (_settings != null)
            _settings.PropertyChanged -= SettingsPropertyChangedHandler;

          _settings = value;
          _settings.PropertyChanged += SettingsPropertyChangedHandler;

          this.OnSettingsChanged(EventArgs.Empty);
        }
      }
    }
    
    private void SettingsPropertyChangedHandler(object sender, PropertyChangedEventArgs e)
    {
      this.OnSettingsChanged(e);
    }

    protected virtual void OnSettingsChanged(EventArgs e)
    {
      this.OpenPDF();

      if (this.SettingsChanged != null)
        this.SettingsChanged(this, e);
    }

Navigation support

Although the PdfImageBox doesn't supply a user interface for navigating to different pages, we want to make it easy for the hosting application to provide one. To support this, a new CurrentPage property will be added for allowing the active page to retrieved or set, and also a number of readonly CanMove* properties. These properties allow the host to query which navigation options are applicable in order to present the correct UI.

    [Browsable(false)]
    public virtual int PageCount
    { get { return _converter != null ? _converter.PageCount : 0; } }

    [Category("Appearance"), DefaultValue(1)]
    public int CurrentPage
    {
      get { return _currentPage; }
      set
      {
        if (this.CurrentPage != value)
        {
          if (value < 1 || value > this.PageCount)
            throw new ArgumentException("Page number is out of bounds");

          _currentPage = value;

          this.OnCurrentPageChanged(EventArgs.Empty);
        }
      }
    }

    [Browsable(false)]
    public bool CanMoveFirst
    { get { return this.PageCount != 0 && this.CurrentPage != 1; } }

    [Browsable(false)]
    public bool CanMoveLast
    { get { return this.PageCount != 0 && this.CurrentPage != this.PageCount; } }

    [Browsable(false)]
    public bool CanMoveNext
    { get { return this.PageCount != 0 && this.CurrentPage < this.PageCount; } }

    [Browsable(false)]
    public bool CanMovePrevious
    { get { return this.PageCount != 0 && this.CurrentPage > 1; } }

Again, to make it easier for the host to connect to the control, we also add some helper navigation methods.

    public void FirstPage()
    {
      this.CurrentPage = 1;
    }

    public void LastPage()
    {
      this.CurrentPage = this.PageCount;
    }

    public void NextPage()
    {
      this.CurrentPage++;
    }

    public void PreviousPage()
    {
      this.CurrentPage--;
    }

Finally, it can sometimes take a few seconds to convert a page in a PDF file. To allow the host to provide a busy notification, such as setting the wait cursor or displaying a status bar message, we'll add a pair of events which will be called before and after a page is converted.

public event EventHandler LoadingPage;

public event EventHandler LoadedPage;

Opening the PDF file

Each of the property changed handlers in turn call the OpenPDF method. This method first clears any existing image cache and then initializes the conversion class based on the current PDF file name and quality settings. If the specified file is a valid PDF, the first page is converted, cached, and displayed.

    public void OpenPDF()
    {
      this.CleanUp();

      if (!this.DesignMode)
      {
        _converter = new Pdf2Image()
        {
          PdfFileName = this.PdfFileName,
          PdfPassword = this.PdfPassword,
          Settings = this.Settings
        };

        this.Image = null;
        this.PageCache= new Dictionary<int, Bitmap>();
        _currentPage = 1;

        if (this.PageCount != 0)
        {
          _currentPage = 0;
          this.CurrentPage = 1;
        }
      }
    }

    private void CleanUp()
    {
      // release  bitmaps
      if (this.PageCache != null)
      {
        foreach (KeyValuePair<int, Bitmap> pair in this.PageCache)
          pair.Value.Dispose();
        this.PageCache = null;
      }
    }

Displaying the image

Each time the CurrentPage property is changed, it calls the SetPageImage method. This method first checks to ensure the specified page is present in the cache. If it is not, it will load the page in. Once the page is in the cache, it is then displayed in the ImageBox, and the user can then pan and zoom as with any other image.

    protected virtual void SetPageImage()
    {
      if (!this.DesignMode && this.PageCache != null)
      {
        lock (_lock)
        {
          if (!this.PageCache.ContainsKey(this.CurrentPage))
          {
            this.OnLoadingPage(EventArgs.Empty);
            this.PageCache.Add(this.CurrentPage, _converter.GetImage(this.CurrentPage));
            this.OnLoadedPage(EventArgs.Empty);
          }

          this.Image = this.PageCache[this.CurrentPage];
        }
      }
    }

Note that we operate a lock during the execution of this method, to ensure that you can't try and load the same page twice.

With this method in place, the control is complete and ready to be used as a basic PDF viewer. In order to keep the article down to a reasonable size, I've excluded some of the definitions, overloads and helper methods; these can all be found in the sample download below.

The sample project demonstrates all the features described above and provides an example setting up a user interface for navigating a PDF document.

Future changes

At the moment, the PdfImageBox control processes on page at a time and caches the results. This means that navigation through already viewed pages is fast, but displaying new pages can be less than ideal. A possible enhancement would be to make the control multithreaded, and continue to load pages on a background thread.

Another issue is that as the control is caching the converted images in memory, it may use a lot of memory in order to display large PDF files. Not quite sure on the best approach to resolve this one, either to "expire" older pages, or to keep only a fixed number in memory. Or even save each page to a temporary disk file.

Finally, I haven't put in any handling at all for if the converter fails to convert a given page... I'll add this to a future update, and hopefully get the code hosted on an SVN server for interested parties.

Downloads:

  • PdfImageBoxSample.zip

    (513.29 KB | 04 September 2011 )

    Sample project showing how to extend the ImageBox control in order to display convert and display PDF files in a .NET WinForms application with the help of GhostScript.

  • Cyotek.GhostScript.zip

    (11.68 KB | 04 September 2011 )

    Work in progress class library for providing GhostScript integration in a .NET application.

  • Cyotek.GhostScript.PdfConversion.zip

    (5.43 KB | 04 September 2011 )

    Class library for converting PDF files into images using GhostScript. Also requires the Cyotek.GhostScript assembly.

2 comments | | Trackback specific URL for this entry

CSS Syntax Highlighting in the DigitalRune Text Editor Control

For projects where I need some form of syntax highlighting, I tend to use the open source DigitalRune Text Editor Control which is a modified version of the text editor used in SharpDevelop. While it has a number of syntax definitions built in, the one it didn't have was for CSS formatting.

After doing a quick search on the internet and finding pretty much nothing, I created my own. This article describes that process, along with how to embed the definition directly in a custom version of the control, or loading it into the vendor supplied control.

Creating the rule set

Each definition is an XML document which contains various sections describing how to syntax highlight a document. An XSD schema is available, named Mode.xsd and located in the /Resources directory in the control's source code.

Here's an example of an (almost) empty definition - I've filled in the definition name and the list of file extensions it will support:

<?xml version="1.0" encoding="utf-8"?>
<SyntaxDefinition name="CSS" extensions="*.css">
  <RuleSets>
  </RuleSets>
</SyntaxDefinition>

The RuleSets element contains one of more RuleSet elements which in turn describe formatting. I'm not sure how the control decides to process these, but in my example I started with an unnamed ruleset which references a named ruleset, and in turn that references another - seems to work fine.

There are two key constructs we'll be using for highlighting - first is span highlighting, where an block of text which starts and ends with given symbols is highlighted. The second is keywords, where distinct words are highlighted. From having a quick look through the source code to figure out problems, there appears to be one or two other constructs available, but I'll ignore these for now.

First, I need to add a rule for comments, which should be quite straight forward - look for a /* and end with /*:

    <RuleSet ignorecase="false">
      <Span name="Comment" bold="false" italic="false" color="Green" stopateol="false">
        <Begin>/*</Begin>
        <End>*/</End>
      </Span>
    </RuleSet>

The Span tag creates a span highlighting construct. The Begin and End tags describe the phrase that marks the beginning and end of the text to match. The stopateol attribute determines if the line breaks should stop at the end of a line. The formatting properties should be evident!

Next, I added another span rule to process the highlighting of the actual CSS rules - so anything between { and }.

      <Span name="CssClass" rule="CssClass" bold="false" italic="false" color="Black" stopateol="false">
        <Begin>{</Begin>
        <End>}</End>
      </Span>

Note this time the rule attribute - this is pointing to a new ruleset (more on that below). Without this attribute, I found that I was unable to style keywords and values inside the CSS rule, as the span above always took precedence. The new ruleset looks similar to this, although in this example I have stripped out most of the CSS property names. (The list of which came from w3schools)

    <RuleSet name="CssClass" ignorecase="true">
      <Span name="Value" rule="ValueRules" bold="false" italic="false" color="Blue" stopateol="false">
        <Begin color="Black">:</Begin>
        <End color="Black">;</End>
      </Span>
      <KeyWords name="CSSLevel1PropertyNames" bold="false" italic="false" color="Red">
        <Key word="background" />
        <Key word="background-attachment" />
        (snip)
      </KeyWords>
      <KeyWords name="CSSLevel2PropertyNames" bold="false" italic="false" color="Red">
        <Key word="border-collapse" />
        <Key word="border-spacing" />
        (snip)
      </KeyWords>
      <KeyWords name="CSSLevel3PropertyNames" bold="false" italic="false" color="Red">
        <Key word="@font-face" />
        <Key word="@keyframes" />
        (snip)
      </KeyWords>
    </RuleSet>
    

First is a new span to highlight attribute values (found between the : and ; characters in blue, and then 3 sets of a new construct - KeyWords. This basically matches a given word and formats it appropriately. In this example, I have split each of the 3 major CSS versions into separate sections, on the off chance you want to reconfigure the file to only support a subset, for example CSS1 and CSS2. Also note that I haven't included any vendor prefixes.

One thing to note, in the Value span above, the begin and end tags have color attributes. This overrides the overall span color (blue) and colors those individual colors with the override (black). Again, from checking the scheme it looks like this can be done for most elements, and supports the color, bold and italic attributes, plus a bgcolor attribute that I haven't used yet.

The span in the above ruleset references a final ruleset, as follows:

    <RuleSet name="ValueRules" ignorecase="false">
      <Span name="Comment" bold="false" italic="false" color="Green" stopateol="false">
        <Begin>/*</Begin>
        <End>*/</End>
      </Span>
      <<pan name="String" bold="false" italic="false" color="BlueViolet" stopateol="true">
        <Begin>"</Begin>
        <End>"</End>
      </Span>
      <Span name="Char" bold="false" italic="false" color="BlueViolet" stopateol="true">
        <Begin>'</Begin>
        <End>'</End>
      </Span>
      <KeyWords name="Flags" bold="true" italic="false" color="BlueViolet">
        <Key word="!important" />
      </KeyWords>
    </RuleSet>

This ruleset has 3 spans, and one keyword. I had to duplicate the comment span from the first ruleset, I couldn't comment highlighting to work inside { } blocks otherwise - probably some subtlety of the definition format that I'm missing. This is followed by two spans which highlight strings (depending on whether single or double quoted). Finally, we have a keyword rule for formatting !important. (Of course, ideally you wouldn't be using this keyword at all, but you never know!)

Put togehter, this definition nicely highlights CSS. Except for one thing - everything outside a comment or style block is black. And I want it to be something else! Initially I tried just setting the ForeColor property of the control itself, but this was blatantly ignored when it drew itself. Fortunately a scan of the schema gave the answer - you can add an Environment tag and set up a large bunch of colors. Or one, in this case.

  <Environment>
    <Default color="Maroon" bgcolor="White" />
  </Environment>

Now save the file somewhere with the .xshd extension - in keeping with the convention of the existing definitions, I named it CSS-Mode.xshd.

Loading the definition into the Text Editor control

This is where I was a little bit stumped - as I didn't have a clue how to get the definition in. Fortunately, DigitalRune's technical support were able to help.

If you are using a custom version of the source code, you can add the definition directly into the source and have it available with the compiled assembly. However, if you are using the vendor supplied assembly, you'll need to include the definition with your application in order to load it in.

Compiling the definition into the assembly

This is quite straight forward, and easily recommended if you have a custom version.

  1. Copy the definition file into the Resources folder of the control's project
  2. Set the Build Action to be Embedded Resource
  3. Open SyntaxModes.xml located in the same folder and add a mode tag which points to your definition, for example
    <Mode file="CSS-Mode.xshd" name="CSS" extensions=".css"/>
    While I haven't checked to see if it is enforced, common sense would suggest you ensure the name and extensions attributes match in both the syntax definition and the ruleset definition.
  4. Compile the solution.

With that done, your definition is now available for use!

Loading the definitions externally

You don't need to compile the definitions into the control assembly, but can load them externally. To do this, you need to have the definition file and the syntax mode file available for loading.

  1. Add a new folder to your project and copy into this folder your .xshd file and set the Copy to Output Directory property to Copy always.
  2. Create a file named SyntaxMode.xml in the folder, and paste in the definition below. You'll also need to set the copy to output directory attribute.
    <?xml version="1.0" encoding="utf-8"?>
    <SyntaxModes version="1.0">
      <Mode file="CSS-Mode.xshd" name="CSS" extensions=".css" />
    </SyntaxModes>
  3. The following line of code will load the definition file into the text editor control:
    HighlightingManager.Manager.AddSyntaxModeFileProvider(new FileSyntaxModeProvider(definitionsFolder));

Setting up the Text Editor Control

To instruct instances of the Text Editor control to use CSS syntax highlighting, add the following line of code to your application (replacing CSS with the name of your definition if you called it something different):

textEditorControl.Document.HighlightingStrategy = HighlightingManager.Manager.FindHighlighter("CSS");

Syntax highlighting isn't appearing, what went wrong?

Rather frustratingly, the control doesn't raise an error if a definition file is invalid, it just silently ignores it and uses a default highlighting scheme. Use the source code for the control so you can catch the exceptions being raised by the HighlightingDefinitionParser class in order to determine any problems. Remember the definition you create is implicitly linked to the schema and so must conform to it.

SharpDevelop?

As the DigitalRune control is derived from the original SharpDevelop editing component, I believe this article and sample code will work in exactly the same way for the SharpDevelop control. However, I don't have this installed and so this remains untested - let me know if it works for you!

Sample application

The download available below includes the CSS definition file and a sample application which will load in the definition files. Note that no binaries are included in the archive, you'll need to add a reference to a copy of the DigitalRune Text Editor control installed on your own system.

Downloads:

  • DigitalRuneTextEditorCssHighlighting.zip

    (9.66 KB | 08 July 2011 )

    Sample project which shows how to create a definition ruleset to allow CSS formatting in the DigitalRune/SharpDevelop Text Editor Controls, and to load custom definition rulesets into the control.

Post a Comment | | Trackback specific URL for this entry

Enabling shell styles for the ListView and TreeView controls in C#

For those who remember the Common Controls OCX's featured in Visual Basic 5 and 6, there was one peculiarity of these. In Visual Basic 5, the Common Controls were linked directly to their shell counterparts. As the shell was updated, so did the look of any VB app using these. However, for Visual Basic 6, this behaviour was changed and they didn't use the shell for drawing.

Curiously enough, history repeats itself in a limited way with Visual Studio .NET. If you use the ListView or TreeView controls on Windows Vista or higher, you'll find they are somewhat drawn according to the "classic" Windows style - no gradients on selection highlights, column separators (ListView) or alternate +/- glyphs (TreeView).

Fortunately however, it is quite simple to enable this with a single call to the SetWindowTheme API when creating the control.

    [DllImport("uxtheme.dll", CharSet = CharSet.Unicode)]
    public extern static int SetWindowTheme(IntPtr hWnd, string pszSubAppName, string pszSubIdList);

In the sample application (available for download from the link below), we create two new ListView and TreeView classes which inherit from their System.Windows.Forms counterparts.

In each class, override the OnHandleCreated method, and check to see what OS is being run - if you try to call SetWindowTheme on an unsupported OS, you'll get a crash. In this case, I'm checking for Windows Vista or higher.

If the version is fine, call SetWindowTheme with the handle of the control, and the name of the shell style - explorer in this case.

It's as simple as that - now when you run the application, the controls will be drawn using whatever shell styles are in use.

using System;

namespace ShellControlsExample
{
  class TreeView : System.Windows.Forms.TreeView
  {
    protected override void OnHandleCreated(EventArgs e)
    {
      base.OnHandleCreated(e);

      if (!this.DesignMode && Environment.OSVersion.Platform == PlatformID.Win32NT && Environment.OSVersion.Version.Major >= 6)
        NativeMethods.SetWindowTheme(this.Handle, "explorer", null);
    }
  }
}

For the TreeView control, I'd also recommend setting the ShowLines property to false as it will look odd otherwise.

Downloads:

  • shellcontrolsexample.zip

    (8.97 KB | 16 April 2011 )

    Sample which shows how to display ListView and TreeView controls using Visual Styles in Windows Vista or higher via the SetWindowTheme API.

2 comments | | Trackback specific URL for this entry

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