You may remember quite an old post of mine about the shortcomings of ExtractAssociatedIcon() which, surprisingly, still gets a fair number of hits. In light of some of the feedback i’ve received since the original post, i’ve decided to update the code to include the following fixes and improvements:

Both methods in IconTools now use SHGetFileInfo

As was pointed out by Michael (no surname provided), the native SHGetFileInfo() function (which we were already using to get the icon for existing files) can also be used to get the icon for a file’s type. The additional flag SHGFI_USEFILEATTRIBUTES instructs the API not to attempt to access the file, and simply locates the icon by examining its attributes. It turns out that you can get away with passing a file extension directly to this function.

The more long-winded implementation based on the ExtractIconEx() function is no longer used. This also removes the need to access the Windows Registry to find the DefaultIcon key.

Handles are now released using DestroyIcon

Another commenter on the original post, Cobein, pointed out (quite correctly) that the managed Icon class does not automatically release handles to icons created using the FromHandle() method.

This posed a slight design challenge, in that users could not be expected to do this after they finished using the icon returned by the method in the IconTools class. To get around this, I decided to use the Clone() method to copy the icon, before releasing the native handle and returning the managed copy instead.

The GetIconForFile() method now looks like this:

public static Icon GetIconForFile(string filename, ShellIconSize size) {
   SHFILEINFO shinfo = new SHFILEINFO();
   NativeMethods.SHGetFileInfo(filename, 0, ref shinfo, (uint)Marshal.SizeOf(shinfo), size);

   Icon icon = null;

   if (shinfo.hIcon.ToInt32() != 0) {
      // create the icon from the native handle and make a managed copy of it
      icon = (Icon)Icon.FromHandle(shinfo.hIcon).Clone();

      // release the native handle
      NativeMethods.DestroyIcon(shinfo.hIcon);
   }

   return icon;
}

Example Usage

Usage remains unchanged; if you used the previous version of this code, it will continue to work as before.

Icon smallIcon = IconTools.GetIconForFile(
   @"C:\Windows\System32\notepad.exe",
   ShellIconSize.SmallIcon
);

Icon largeIcon = IconTools.GetIconForExtension(".html", ShellIconSize.LargeIcon);

Download

IconTools.cs

Share/Bookmark

Thank you!

At the time of writing, my ComboTreeBox control is the most popular item on this blog. I never could have imagined that a simple Windows Forms control would attract so much attention on the web! All I can say to those who have read my article and downloaded my code is, “Thank you!” Your continued support gives me the motivation to keep writing new and interesting content.

New Project Page

With this in mind, i’ve decided to move both ComboTreeBox and GroupedComboBox to their own project page here: Drop-Down Controls

This gives them a permanent home and makes it easier for me to manage occasional updates and new features. As before, you can download binaries and source code for the controls, as well as an example application that shows off their potential.

New Enhancements

This reshuffling has given me an opportunity to release a few bugfixes, including a memory leak in the Buffered Paint code.

Normal ComboTreeBox ComboTreeBox with ShowCheckBoxes set

I also took the opportunity to enhance the ComboTreeBox control by adding a ShowCheckBoxes property. In this mode of operation, the normal selection rules are suspended and checkbox glyphs are drawn beside each node. You can then select multiple items, accessible via the CheckedNodes property. This is further aligns the functionality offered by the control with that of the built-in TreeView control.

Enjoy!

Text Object Model Demo

I’ve recently been doing a lot of work with the WinForms RichTextBox control, trying to implement the familiar red wavy underlines associated with spell-checking. After some research, I discovered that the underlying (native) RichEdit control used by the RichTextBox does in fact support this style of underlining. The catch? It is only accessible through the EM_SETCHARFORMAT window message, which requires PInvoke. While it is relatively simple to set this style, there are drawbacks:

  • High overheads associated with marshalling
  • Clunky operation, requires you to send the CHARFORMAT2 structure
  • You can only get/set the style for the selected text

The last point in particular is of most concern to me, and is a serious limitation of the RichTextBox control. Almost all of the rich text functionality is restricted to the text selected in the control. In practice, this means that any non-trivial text manipulation requires you to constantly store the current selection, select the text you wish to manipulate, apply the styles and then restore the old selection. This is slow and extremely inefficient.

I then looked into something called the Text Object Model (or TOM). This comprises a set of COM interfaces which are implemented by the underlying RichEdit control. The functionality exposed by these interfaces is quite similar to that of the Microsoft Word object model, where you have the concept of Document, Range, Selection and so on. Importantly, TOM allows you to operate on ranges of rich text without the need to alter the selection. It also provides access to a range of functionality not otherwise available for the RichTextBox control:

  • Granular selection by character, word, sentence and paragraph
  • Font weights, underline styles, all-caps
  • Tab stops, list styles, full justification of text
  • Find functionality, translation to/from screen coordinates

Using the Text Object Model

The TOM interfaces are:

  • ITextDocument – Represents a top-level document, from which text ranges can be obtained.
  • ITextRange – Represents a range of rich text. Ranges can be moved, resized and styled with ease.
  • ITextSelection – Special text range that represents the selected text in the control.
  • ITextFont – Character formatting options.
  • ITextPara – Paragraph formatting options.

It is relatively easy to obtain an instance of ITextDocument from a RichTextBox control. One simply sends the EM_GETOLEINTERFACE message to the control. There are a number of ways that you can interact with the object that gets returned:

  1. Use the dynamic keyword to access the object’s members – this requires the least programming effort, but introduces the possibility for errors and has a number of overheads.
  2. Define the TOM interfaces in managed code and use COM interop – this allows you to work in a strongly-typed manner, but still has the overheads associated with marshalling. Also, the interfaces themselves are not terribly .NET-friendly, and they lack some of the conveniences that would be familiar to most .NET developers.
  3. Write wrapper classes for each of the TOM interfaces – while this approach requires the most coding, the end result is a fast, efficient way of accessing the TOM functionality. The calling code does not have to use COM interop and the wrapper classes can translate unfriendly constructs into more .NET-friendly ones.

My solution

Needless to say, I settled for the 3rd option. I decided to implement the wrapper classes in a C++/CLI assembly, because this allowed me to access the TOM interfaces using native code, while ultimately exposing a set of managed classes. Well-written native code (that explicitly marshals data to managed code) will always be faster than COM interop, where the runtime has to apply more checks and is unable to make as many assumptions about the types it is operating on.

Some of the benefits offered by my wrappers include:

  • Managed enumerations to replace integer constants and flags
  • Properties to replace Get and Set methods
  • Translation of HRESULT codes into managed exceptions
  • Returning objects instead of pointers
  • Omission of TOM functionality not supported by the RichEdit control (e.g. text shadows, animation)
  • Use of more .NET-friendly types and nomenclature; e.g. IEquatable, ToString, IDataObject, Color and Point

The top-level class in my implementation is TextDocument, which wraps ITextDocument. It includes a static method which creates an instance from a RichTextBox control. Once created, you have full access to the Text Object Model functionality to manipulate the text inside the control.

Usage

Once obtaining a TextDocument instance from a RichTextBox control, working with formatted text is easy:

// create a RichTextBox control in the usual way
RichTextBox rtb = new RichTextBox();

// create a TOM document object (and enable advanced typography on the control)
TextDocument doc = TextDocument.FromRichTextBox(rtb, true);
TextRange range = doc.EntireRange;

// set some text
range.Text = "This is a piece of rich text.";
range.Font.Name = "Calibri";
range.Font.Size = 16f;

// find a word and apply formatting
range.FindText("piece");
range.Font.Bold = true;
range.Font.ForeColor = Color.Red;

// insert a tab
range.Collapse(RangePosition.End);
range.Text = "\t";

// resize range and apply more formatting
range.FindText("rich");
range.MoveEnd(TextUnit.Word, 2);
range.Font.UnderlineStyle = TextUnderlineStyle.Wave;
range.Font.UnderlineColor = TextUnderlineColor.Blue;

// append raw RTF using IDataObject
range.MoveEnd(TextUnit.Story, 1);
range.Collapse(RangePosition.End);
range.SetDataObject(new DataObject(
    DataFormats.Rtf, 
    @"{\rtf1\ansi\deff0\pard \par Here is some \ul more\ul0  rich text.\par}"
));

// set paragraph formatting
doc.EntireRange.Para.Alignment = TextAlignment.Center;

Download

Go to the project page here: TOM Classes for .NET

Final words

This set of managed wrapper classes for the Text Object Model provides a fast and efficient way to manipulate the text in a RichTextBox control. Importantly, it solves the limitation of only being able to apply styles to the selected text in the control. As an added bonus, it also provides access to a wider range of character and paragraph formats, as well as a number of convenience methods for working with ranges of formatted text. I hope you find it useful in your own rich text applications.

Additional resources

Text Object Model on MSDN
Rich Text Format Specification

Hi all,

Just a quick announcement to say that comments are working again.

It seems that the captcha plug-in I was using has been discontinued, including the server that validates the captchas themselves. I’ve replaced the old captcha plug-in with one that uses Google reCAPTCHA instead. This should be familiar to most users from other sites on the web. While I don’t like the clunky nature of most captchas, they are necessary on this blog given the amount of spam I receive.

Anyway, apologies for any inconvenience if you were unable to comment on an article. Everything should be fine now.