C# does Shell, Part 4

This article is about the AutoComplete features enabled by windows and how to use them with C#. AutoComplete is the ability to expand strings written in an edit box. The article will develop a class for using this functionality in you applications.

Introduction

Hello again. In part 2 I've said I will start explaining shell extension the next article. I'm sorry but as I said in part 3 there is still one issue I have to talked about before moving to shell extensions, I do hope the next article I'll start explaining it. The issue involved is using the AutoComplete features that the operating system enable us. As in the previous article (Part 3) which talked about Application Desktop Toolbars, this article is also about an interesting subject.

Note: This article doesn't requires previous reading, but, as always I do recommend reading the previous 'C# does Shell' parts and the relevant MSDN articles:

So, what is this AutoComplete stuff? I'll start with an example. Click on the Start menu, then on Run, in the opened dialog enter a single char. You will probably see a list of available options for completing the string. It should look something like this:

The same is with the address bar in the internet explorer. Start writing and the rest of the string will be expanded.

In this article we will learn what the operating system let us do with it, and how to use this functionality in our own applications. As always we will create a class named ShellAutoComplete that nicely wraps it, this class will be added to the ShellLib library we are developing.

Well, lets get to work.

Section 1: using SHAutoComplete

The most common task we usually want to accomplish is use the AutoComplete to expand strings like file names and directories or URL's that we used (History) or even strings from the Most Recently Used list (MRU). So if we want to accomplish one of those things we can use a function called SHAutoComplete. This function is part of the Shell API functions. This functions receive a window handle of an edit box and a flags parameter to set some AutoComplete options. After using this function on the edit box, the control will have AutoComplete features installed. This do work like magic.

So how is the SHAutoComplete function look like, and how we translate it to c#? here is the original definition:

As you can see, the function has two parameters. The first one, hwndEdit, is the window handle of the edit box we want to enable AutoComplete on. In fact this handle can be also a handle of a window that has an edit box embedded in it, only if you want to do that you need to respond to the message CBEM_GETEDITCONTROL by returning the embedded edit box handle. One control that acts like that is the ComboBoxEx, when using the CBS_DROPDOWN style. Anyway, the normal use is directly on edit box.

The second parameter is dwFlags. This parameter set some AutoComplete options. It can be any combination of values of the AutoCompleteFlags enum. Here is its definition:

[Flags]
publicenum AutoCompleteFlags : uint
{
// The default setting, equivalent to FileSystem | UrlAll. Default cannot be
// combined with any other flags.
Default = 0x00000000,
// This includes the File System as well as the rest of the shell
// (Desktop\My Computer\Control Panel\)
FileSystem = 0x00000001,
// Include the URLs in the users History and Recently Used lists. Equivalent
// to UrlHistory | UrlMRU.
UrlAll = (UrlHistory | UrlMRU),
// Include the URLs in the user's History list.
UrlHistory = 0x00000002,
// Include the URLs in the user's Recently Used list.
UrlMRU = 0x00000004,
// Allow the user to select from the autosuggest list by pressing the TAB
// key. If this flag is not set, pressing the TAB key will shift focus to
// the next control and close the autosuggest list. If UseTab is set,
// pressing the TAB key will select the first item in the list. Pressing
// TAB again will select the next item in the list, and so on. When the user
// reaches the end of the list, the next TAB key press will cycle the focus
// back to the edit control. This flag must be used in combination with one
// or more of the FileSys* or Url* flags.
UseTab = 0x00000008,
// This includes the File System
FileSys_Only = 0x00000010,
// Same as FileSys_Only except it only includes directories, UNC servers,
// and UNC server shares.
FileSys_Dirs = 0x00000020,
// Ignore the registry value and force the autosuggest feature on. A
// selection of possible completed strings will be displayed as a drop-down
// list, below the edit box. This flag must be used in combination with one
// or more of the FileSys* or Url* flags.
AutoSuggest_Force_On = 0x10000000,
// Ignore the registry default and force the autosuggest feature off. This
// flag must be used in combination with one or more of the FileSys* or
// Url* flags.
AutoSuggest_Force_Off = 0x20000000,
// Ignore the registry value and force the autoappend feature on. The
// completed string will be displayed in the edit box with the added
// characters highlighted. This flag must be used in combination with one
// or more of the FileSys* or Url* flags.
AutoAppend_Force_On = 0x40000000,
// Ignore the registry default and force the autoappend feature off. This
// flag must be used in combination with one or more of the FileSys* or
// Url* flags.
AutoAppend_Force_Off = 0x80000000
}

If you look on the enum definition you see they divides into several categories. The values that influence the AutoComplete source are FileSystem, UrlHistory, UrlMRU. The values that overrides registry defaults are: AutoSuggest_Force_On, AutoSuggest_Force_Off, AutoAppend_Force_On, AutoAppend_Force_Off. And here comes the place where I can explain what the AutoSuggest and AutoAppend options are.

AutoAppend: This means that as you write, the string is automatically completed with the current string entered, as you write more of the string it will get more precise, but the thing to remember is that the string is automatically appended.

AutoSuggest: As you write your string, a drop down list appears with the current suggestion for completion of the string. Off course you can select a string for the list.

One thing to remember is that you can choose several sources for the AutoComplete list. It can be any combination of the file system, History URL's and the Most Recently Used list. But what if you want to use your own list? what if you want to combine an the History and a list of your own? All this cannot be done with this function but will be reviewed in this article.

Section 2: AutoComplete Object Model

Well, maybe object model is a bit strong, but there is a model that should be explained here. You see, Microsoft has put the entire AutoComplete functionality in a COM object called AutoComplete. This object reveals the interfaces IAutoComplete and IAutoComplete2. The AutoComplete Object knows how to create a window with the string list and how to expand the string once the user start typing it. What the AutoComplete object does not have is the string list. The string list is held in another object that should have the IEnumString interface. This object knows only how to enumerate on its string list. The OS provides 4 source objects. One that have the File System string list named ACListISF. One that gave the History string list named ACLHistory. One that have the MRU string list named ACLMRU. And finally a special object named ACLMulti, which will be explained later. All those objects have the interface IEnumString and can be used as a source for the AutoComplete object. Some of those objects have also the interfaces: IACList and IACList2. And the ACLMulti Object has in addition the IObjMgr interface. All those interfaces will be explained, but the important thing to remember is that we have a main AutoComplete object and several possible sources for it. Here is a little diagram I've made for you to better understand what objects we have in the system and their interfaces:

Now, what do we need to do in order to have AutoComplete on an edit box? First we need to create the AutoComplete object, which has a specific GUID (like any other COM object in the world). Then we create a source object, ACLHistory for example. Then we attach the ACLHistory object to our AutoComplete object and activate the AutoComplete object on our edit box. This is basically what has to be done when using the objects model instead of the simple and not expandable SHAutoComplete function.

I've mention the GUID's subject. We have 5 objects that the Operating System supply us. In order to create them we need their GUID's. Here is their C# declaration. These declarations are found in the ShellLib library in the already existing class ShellGUIDS:

The next section will discuss creating and using of the AutoComplete object, this includes off course a review of the interfaces it exposes.

Section 3: AutoComplete Object

The first thing we should do is creating the object, so how do we create a COM object in C#? One way is to 'add reference' of the object, in this case I prefer the more dynamic way, using the Activator class. We will write a private function in the ShellAutoComplete class which will create the Autocomplete object and returns it to the caller. Here is the code:

This function use the GetTypeFromCLSID static function to return the Type of a registered COM object. Then using the CreateInstance of the Activator class it creates the object. In order to use the object we need to cast it to one of its interfaces.

As I've said before, this object has two Interfaces, first we will discuss IAutoComplete. Here is the original declaration and then comes the C# equivalent:

Note that the original declaration is here for you to better understand the changes that need to be done when translating a c++ interface into a c# interface.

[ComImport]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
[Guid("00BB2762-6A77-11D0-A535-00C04FD7D062")]
publicinterface IAutoComplete
{
// Initializes the autocomplete object.
[PreserveSig]
Int32 Init(
IntPtr hwndEdit, // Handle to the window for the
// system edit control that is to
// have autocompletion enabled.
[MarshalAs(UnmanagedType.IUnknown)]
Object punkACL, // Pointer to the IUnknown interface
// of the string list object that
// is responsible for generating
// candidates for the completed
// string. The object must expose
// an IEnumString interface.
[MarshalAs(UnmanagedType.LPWStr)]
String pwszRegKeyPath, // Pointer to an optional null-
// terminated Unicode string that
// gives the registry path,
// including the value name, where
// the format string is stored as
// a REG_SZ value. The
// autocomplete object first
// looks for the path under
// HKEY_CURRENT_USER . If it fails,
// it then tries HKEY_LOCAL_MACHINE.
// For a discussion of the
// format string, see the
// definition of pwszQuickComplete.
[MarshalAs(UnmanagedType.LPWStr)]
String pwszQuickComplete); // Pointer to an optional string
// that specifies the format to be
// used if the user enters some text
// and presses CTRL+ENTER. Set
// this parameter to NULL to disable
// quick completion. Otherwise,
// the autocomplete object treats
// pwszQuickComplete as a sprintf
// format string, and the text in
// the edit box as its associated
// argument, to produce a new
// string. For example, set
// pwszQuickComplete to
// "http://www. %s.com/". When a
// user enters "MyURL" into the
// edit box and presses CTRL+ENTER,
// the text in the edit box is
// updated to
// "http://www.MyURL.com/".
// Enables or disables autocompletion.
[PreserveSig]
Int32 Enable(
Int32 fEnable); // Value that is set to TRUE to
// enable autocompletion, or to
// FALSE to disable it.
}

The IAutoComplete interface has two functions, Init and Enable. The Init function Initialize the AutoComplete object and the Enable function enables or disables the autocompletion. Note that the Init function is where you give the edit box window handle (in the hwndEdit parameter) and the Source object (in the punkACL parameter). Later in this section I'll show an example of using these functions.

The second interface the AutoComplete object expose is IAutoComplete2. This interface expands the first one and declares two more methods. Here is its declaration:

[ComImport]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
[Guid("EAC04BC0-3791-11D2-BB95-0060977B464C")]
publicinterface IAutoComplete2 /*: IAutoComplete */
{
// Initializes the autocomplete object.
[PreserveSig]
Int32 Init(
IntPtr hwndEdit, // Handle to the window for the system
// edit control that is to
// have autocompletion enabled.
[MarshalAs(UnmanagedType.IUnknown)]
Object punkACL, // Pointer to the IUnknown interface
// of the string list object that
// is responsible for generating
// candidates for the completed
// string. The object must expose
// an IEnumString interface.
[MarshalAs(UnmanagedType.LPWStr)]
String pwszRegKeyPath, // Pointer to an optional null-
// terminated Unicode string that
// gives the registry path,
// including the value name, where
// the format string is stored as
// a REG_SZ value. The
// autocomplete object first
// looks for the path under
// HKEY_CURRENT_USER . If it fails,
// it then tries
// HKEY_LOCAL_MACHINE.
// For a discussion of the format
// string, see the definition of
// pwszQuickComplete.
[MarshalAs(UnmanagedType.LPWStr)]
String pwszQuickComplete); // Pointer to an optional string
// that specifies the format to be
// used if the user enters some text
// and presses CTRL+ENTER. Set
// this parameter to NULL to
// disable quick completion.
// Otherwise, the autocomplete
// object treats pwszQuickComplete
// as a sprintf format string,
// and the text in the
// edit box as its associated
// argument, to produce a new
// string. For example, set
// pwszQuickComplete to
// "http://www. %s.com/". When a
// user enters "MyURL" into the edit
// box and presses CTRL+ENTER, the
// text in the edit box is updated
// to "http://www.MyURL.com/".
// Enables or disables autocompletion.
[PreserveSig]
Int32 Enable(
Int32 fEnable); // Value that is set to
// TRUE to enable autocompletion,
// or to FALSE to disable it.
// Sets the current autocomplete options.
[PreserveSig]
Int32 SetOptions(
UInt32 dwFlag); // Flags that allow an application to specify
// autocomplete options.
// Retrieves the current autocomplete options.
[PreserveSig]
Int32 GetOptions(
outUInt32 pdwFlag); // that indicate the options that are
// currently set.
}

Here you see two extra functions: SetOptions and GetOptions. Those functions are simply to enable you tune a bit the AutoComplete object behavior. The options allowed are those declared in the AutoCompleteOptions enum:

There is one very important thing to note about the IAutoComplete2 interface. According to the original declaration it inherits IAutoComplete, but when I wanted to declare this interface in C# it just wouldn't work if it was inheriting IAutoComplete. I guess I've did something wrong, but it might just be a bug in C# or even in the .Net Framework. The only way it worked was when I add the functions of the IAutoComplete in the IAutoComplete2 interface and not to inherit. If someone has a solution to this problem I will gladly here it.

So, after all these declarations all we have is 2 interfaces with 4 functions total. Very simple indeed. As I've stated before all we need to do is create the AutoComplete object, call the Init function with the edit box window handle and the source object and then call the Enable function and finally call the SetOptions function to set some settings of the object. Lets see some code.

Here is the implementation of the SetAutoComplete function in the ShellAutoComplete class. This is the main function, we call this function after setting the EditHandle member and the ListSource member. Here it is:

What we do here is call the GetAutoComplete private function which creates an AutoComplete object. After casting the object to the IAutoComplete2 interface we call the Init function, then SetOptions and finally Enable function. When we finish we call ReleaseComObject. Note that the ACOptions is another member that we set before calling this function.

Here is the declaration of the members of this class, the SetAutoComplete function uses them:

In the following section we will discuss creating predefined source objects and their interfaces.

Section 4: Predefined Source Objects

As noted before the operating system supply us with 4 source objects. I this section we will discuss 3 of them: ACListISF, ACLHistory and ACLMRU. Also we will discuss their interfaces IACList and IACList2.

So we want to create a source object. The creation itself is similar to the creation of the AutoComplete object. Here is 3 static functions of the ShellAutoComplete class that let you create those objects:

No need to review those functions. I will not bring the declarations of the interfaces cause I don't use them in my code but we will discuss them. The IACList interface has only one method named Expand which receive only one string parameter. This method is used by the operating system. It tells the object to change the source list according to the string parameter. For example lets suppose the source object has all the files in the system. When the object gives its list, it contains only the strings in the root folder, once the user enters a delimiter the expand function is called and the object get an opportunity to set his string list to the files in the current folder. The IACList2 interface has two methods. GetOptions and SetOptions. These functions allow you to tune specific options of your source object. The options are depended on the source, so if you create a custom object you can inherit the IACList2 interface also and have your object deal with its own defined options.

Note that an example of using the source objects can be found in the final section.

Section 5: User Defined Source Object

One of the main reasons for using the AutoComplete Object Model instead of the simple SHAutoComplete function is the ability to create a custom list and use it with the AutoComplete object. The source object need only to expose the IEnumString interface. IACList and IACList2 interfaces are not mandatory. Now you probably expects me to declare the IEnumString interface and use it. Well, here comes the good news, In the System.Runtime.InteropServices there is a bunch of predefined managed interfaces. Among them is the interface UCOMIEnumString which is the managed version of the IEnumString interface, so in this case .Net has done the work alone and left me only to implement this interface in my own object.

Now we will develop our own custom source object named SourceCustomList. This object will have a public field field named StringList of type string[]. The first thing to do is to declare his fields. A source object should also remember where in the string list he is, so we will declare a private integer to remember the current position in the list:

publicstring[] StringList;
privateInt32 currentPosition = 0;

The IEnumString has four functions that should be implemented. The first function is called Next. This function receive a number of elements requested, a string array for returning the requested strings and another integer to write how many string are really returned. Here is our implementation:

// This method retrieves the next celt items in the enumeration sequence. If
// there are fewer than the requested number of elements left in the sequence,
// it retrieves the remaining elements. The number of elements actually retrieved
// is returned through pceltFetched, unless the caller passed in NULL for that
// parameter.
publicInt32 Next(
Int32 celt, // Number of elements being requested.
String[] rgelt, // Array of size celt (or larger) of the
// elements of interest. The type of this
// parameter depends on the item being
// enumerated.
outInt32 pceltFetched) // Pointer to the number of elements actually
// supplied in rgelt. The Caller can pass
// in NULL if celt is 1.
{
pceltFetched = 0;
while ((currentPosition <= StringList.Length-1) && (pceltFetched<celt))
{
rgelt[pceltFetched] = StringList[currentPosition];
pceltFetched++;
currentPosition++;
}
if (pceltFetched == celt)
return0; // S_OK;
elsereturn1; // S_FALSE;
}

The function is quite simple. First I set the pceltFetched variable to 0 and then I iterate over the list and add strings into the requested array, as long as I have what to add. Finally the return values is depends whether I've filled the requested array or not.

The second method to implement is called Skip. This function receive a number, and tell the object to skip some strings. So in our object we only need to advance the currentPosition field. Here is the code:

// This method skips the next specified number of elements in the
// enumeration sequence.
publicInt32 Skip(
Int32 celt) // Number of elements to be skipped.
{
currentPosition += (int)celt;
if (currentPosition <= StringList.Length-1)
return0;
elsereturn1;
}

Next is the Reset function. I will not explain this function, rather I'll let you guess what it does:

Finally, the Clone function. This function creates another object that contains the same enumeration state as the current one. In other words it creates an exact copy of this object:

// This method creates another enumerator that contains the same enumeration
// state as the current one. Using this function, a client can record a
// particular point in the enumeration sequence and return to that point at a
// later time. The new enumerator supports the same interface as the original one.
publicvoid Clone(
out UCOMIEnumString ppenum) // Address of the IEnumString pointer
// variable that receives the interface
// pointer to the enumeration object. If
// the method is unsuccessful, the value
// of this output variable is undefined.
{
SourceCustomList clone = new SourceCustomList();
clone.currentPosition = currentPosition;
clone.StringList = (String[])StringList.Clone();
ppenum = clone;
}

So there it is. We have created our own custom source object. Now all we should do is create an instance and set it as the source object of our AutoComplete object.

The only thing I haven't explained yet is how to use multi sources with the AutoComplete object. This is covered in the next section.

Section 6: Multi Source object

So what if we want to use the custom object we have just created along with the History list? In this case the operating system supplied us a source object named ACMulti. Remember him? I told you I'll explain him later. Well this object exposes the IEnumString interface (like any other source object), and another interface named IObjMgr. This interface allow us to attach several source objects to it. So when we want to use a combination of sources we need to create this object, attach some sources with functions of the IObjMgr interface (soon to be reviewed) and set the ACMulti object as the source object of the AutoComplete object. Here is the code to create this system object:

[ComImport]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
[Guid("00BB2761-6A77-11D0-A535-00C04FD7D062")]
publicinterface IObjMgr
{
// Appends an object to the collection of managed objects.
[PreserveSig]
Int32 Append(
[MarshalAs(UnmanagedType.IUnknown)]
Object punk); // Address of the IUnknown interface of the object
// to be added to the list.
// Removes an object from the collection of managed objects.
[PreserveSig]
Int32 Remove(
[MarshalAs(UnmanagedType.IUnknown)]
Object punk); // Address of the IUnknown interface of the object
// to be removed from the list.
}

This interface has functions for append and remove of other sources. I thing the definition is quite clear.

The next section will have an example of using the ShellAutoComplete class, including using multi sources.

Section 7: Using the ShellAutoComplete

Brought to you here is a full example of using the class we have created in this article. The first sample uses the simple AutoComplete functionality. It includes setting some options and invoke the DoAutoComplete function which in turn calls SHAutoComplete:

I really recommend debugging the demo program to better understand the flow of these samples.

Update:

There was a problem using the ShellAutoComplete class on ComboBox, because the .net ComboBox does not respond to the CBEM_GETEDITCONTROL message, and the shell try to get the EditBox handle with this message. So the code sample has changed to give a solution to this problem. The solution is to use the API function GetComboBoxInfo to get the handle manually, and then set the EditHandle property to the internal EditBox handle of the ComboBox instead of the Combobox handle. Big thanks goes to Aleeza for finding this out.

Comments and Discussions

This application is working good.
I need one more thing here, Can We show only few records in Autocomplete of TextBox.
If I write C:\ in TextBox,
then it will show all files in C:\ drive if I select FileSystem.
but I want to see only only first two records.
How can we do that.