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:
C# does Shell, Part 1
C# does Shell, Part 2
C# does Shell, Part 3
MSDN: Using Autocomplete
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:
HRESULT SHAutoComplete(
HWND hwndEdit,
DWORD dwFlags);
and here is the C# equivalent:
[DllImport("shlwapi.dll")]
public static extern Int32 SHAutoComplete(
IntPtr hwndEdit,
UInt32 dwFlags);
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]
public enum AutoCompleteFlags : uint
{
Default = 0x00000000,
FileSystem = 0x00000001,
UrlAll = (UrlHistory | UrlMRU),
UrlHistory = 0x00000002,
UrlMRU = 0x00000004,
UseTab = 0x00000008,
FileSys_Only = 0x00000010,
FileSys_Dirs = 0x00000020,
AutoSuggest_Force_On = 0x10000000,
AutoSuggest_Force_Off = 0x20000000,
AutoAppend_Force_On = 0x40000000,
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
:
public static Guid CLSID_AutoComplete =
new Guid("{00BB2763-6A77-11D0-A535-00C04FD7D062}");
public static Guid CLSID_ACLHistory =
new Guid("{00BB2764-6A77-11D0-A535-00C04FD7D062}");
public static Guid CLSID_ACListISF =
new Guid("{03C036F1-A186-11D0-824A-00AA005B4383}");
public static Guid CLSID_ACLMRU =
new Guid("{6756A641-dE71-11D0-831B-00AA005B4383}");
public static Guid CLSID_ACLMulti =
new Guid("{00BB2765-6A77-11D0-A535-00C04FD7D062}");
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:
private Object GetAutoComplete()
{
Type typeAutoComplete = Type.GetTypeFromCLSID(ShellGUIDs.CLSID_AutoComplete);
Object obj;
obj = Activator.CreateInstance(typeAutoComplete);
return obj;
}
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.
MIDL_INTERFACE("00bb2762-6a77-11d0-a535-00c04fd7d062")
IAutoComplete : public IUnknown
{
public:
virtual HRESULT STDMETHODCALLTYPE Init(
HWND hwndEdit,
IUnknown *punkACL,
LPCOLESTR pwszRegKeyPath,
LPCOLESTR pwszQuickComplete) = 0;
virtual HRESULT STDMETHODCALLTYPE Enable(
BOOL fEnable) = 0;
};
C# equivalent:
[ComImport]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
[Guid("00BB2762-6A77-11D0-A535-00C04FD7D062")]
public interface IAutoComplete
{
[PreserveSig]
Int32 Init(
IntPtr hwndEdit,
[MarshalAs(UnmanagedType.IUnknown)]
Object punkACL,
[MarshalAs(UnmanagedType.LPWStr)]
String pwszRegKeyPath,
[MarshalAs(UnmanagedType.LPWStr)]
String pwszQuickComplete);
[PreserveSig]
Int32 Enable(
Int32 fEnable);
}
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:
MIDL_INTERFACE("EAC04BC0-3791-11d2-BB95-0060977B464C")
IAutoComplete2 : public IAutoComplete
{
public:
virtual HRESULT STDMETHODCALLTYPE SetOptions(
DWORD dwFlag) = 0;
virtual HRESULT STDMETHODCALLTYPE GetOptions(
DWORD *pdwFlag) = 0;
};
and the c# equivalent:
[ComImport]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
[Guid("EAC04BC0-3791-11D2-BB95-0060977B464C")]
public interface IAutoComplete2
{
[PreserveSig]
Int32 Init(
IntPtr hwndEdit,
[MarshalAs(UnmanagedType.IUnknown)]
Object punkACL,
[MarshalAs(UnmanagedType.LPWStr)]
String pwszRegKeyPath,
[MarshalAs(UnmanagedType.LPWStr)]
String pwszQuickComplete);
[PreserveSig]
Int32 Enable(
Int32 fEnable);
[PreserveSig]
Int32 SetOptions(
UInt32 dwFlag);
[PreserveSig]
Int32 GetOptions(
out UInt32 pdwFlag);
}
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:
[Flags]
public enum AutoCompleteOptions
{
None = 0,
AutoSuggest = 0x1,
AutoAppend = 0x2,
Search = 0x4,
FilterPreFixes = 0x8,
UseTab = 0x10,
UpDownKeyDropsList = 0x20,
RtlReading = 0x40
}
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:
public void SetAutoComplete(Boolean enable)
{
Int32 ret;
IAutoComplete2 iac2 = (IAutoComplete2)GetAutoComplete();
if (EditHandle == IntPtr.Zero)
throw new Exception("EditHandle must not be zero!");
if (ListSource == null)
throw new Exception("ListSource must not be null!");
ret = iac2.Init(EditHandle,ListSource,"","");
ret = iac2.SetOptions((UInt32)ACOptions);
ret= iac2.Enable(enable ? 1 : 0);
Marshal.ReleaseComObject(iac2);
}
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:
public IntPtr EditHandle = IntPtr.Zero;
public Object ListSource = null;
public AutoCompleteOptions ACOptions =
AutoCompleteOptions.AutoSuggest | AutoCompleteOptions.AutoAppend;
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:
public static Object GetACLHistory()
{
Type typeACLHistory = Type.GetTypeFromCLSID(ShellGUIDs.CLSID_ACLHistory);
Object obj;
obj = Activator.CreateInstance(typeACLHistory);
return obj;
}
public static Object GetACLMRU()
{
Type typeACLMRU = Type.GetTypeFromCLSID(ShellGUIDs.CLSID_ACLMRU);
Object obj;
obj = Activator.CreateInstance(typeACLMRU);
return obj;
}
public static Object GetACListISF()
{
Type typeACListISF = Type.GetTypeFromCLSID(ShellGUIDs.CLSID_ACListISF);
Object obj;
obj = Activator.CreateInstance(typeACListISF);
return obj;
}
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:
public string[] StringList;
private Int32 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:
public Int32 Next(
Int32 celt,
String[] rgelt,
out Int32 pceltFetched)
{
pceltFetched = 0;
while ((currentPosition <= StringList.Length-1) && (pceltFetched<celt))
{
rgelt[pceltFetched] = StringList[currentPosition];
pceltFetched++;
currentPosition++;
}
if (pceltFetched == celt)
return 0;
else
return 1;
}
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:
public Int32 Skip(
Int32 celt)
{
currentPosition += (int)celt;
if (currentPosition <= StringList.Length-1)
return 0;
else
return 1;
}
Next is the Reset
function. I will not explain this function, rather I'll let you guess what it does:
public Int32 Reset()
{
currentPosition = 0;
return 0;
}
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:
public void Clone(
out UCOMIEnumString ppenum)
{
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:
public static Object GetACLMulti()
{
Type typeACLMulti = Type.GetTypeFromCLSID(ShellGUIDs.CLSID_ACLMulti);
Object obj;
obj = Activator.CreateInstance(typeACLMulti);
return obj;
}
And here is the IObjMgr
definition:
[ComImport]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
[Guid("00BB2761-6A77-11D0-A535-00C04FD7D062")]
public interface IObjMgr
{
[PreserveSig]
Int32 Append(
[MarshalAs(UnmanagedType.IUnknown)]
Object punk);
[PreserveSig]
Int32 Remove(
[MarshalAs(UnmanagedType.IUnknown)]
Object punk);
}
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
:
private void btnConnectSimple_Click(object sender, System.EventArgs e)
{
ShellLib.ShellAutoComplete.AutoCompleteFlags flags = 0;
flags |= (chkFileSystem.Checked) ?
ShellLib.ShellAutoComplete.AutoCompleteFlags.FileSystem : 0;
flags |= (chkUrlHistory.Checked) ?
ShellLib.ShellAutoComplete.AutoCompleteFlags.UrlHistory : 0;
flags |= (chkUrlMRU.Checked) ?
ShellLib.ShellAutoComplete.AutoCompleteFlags.UrlMRU : 0;
flags |= (chkUseTab.Checked) ?
ShellLib.ShellAutoComplete.AutoCompleteFlags.UseTab : 0;
flags |= (chkFileSysOnly.Checked) ?
ShellLib.ShellAutoComplete.AutoCompleteFlags.FileSys_Only : 0;
flags |= (chkFileSysDirs.Checked) ?
ShellLib.ShellAutoComplete.AutoCompleteFlags.FileSys_Dirs : 0;
flags |= (rdoAutoAppendForceOff.Checked) ?
ShellLib.ShellAutoComplete.AutoCompleteFlags.AutoAppend_Force_Off : 0;
flags |= (rdoAutoAppendForceOn.Checked) ?
ShellLib.ShellAutoComplete.AutoCompleteFlags.AutoAppend_Force_On : 0;
flags |= (rdoAutoSuggestForceOff.Checked) ?
ShellLib.ShellAutoComplete.AutoCompleteFlags.AutoSuggest_Force_Off : 0;
flags |= (rdoAutoSuggestForceOn.Checked) ?
ShellLib.ShellAutoComplete.AutoCompleteFlags.AutoSuggest_Force_On : 0;
ShellLib.ShellAutoComplete.DoAutoComplete(edtFile.Handle,flags);
}
The second sample uses the AutoComplete Object Model and creates source objects, allowing also to create the multi source object and to append several sources to it:
private void btnConnectObject_Click(object sender, System.EventArgs e)
{
ShellLib.ShellAutoComplete ac = new ShellLib.ShellAutoComplete();
ac.EditHandle = edtFile.Handle;
ac.ACOptions = ShellLib.ShellAutoComplete.AutoCompleteOptions.None;
ac.ACOptions |= (chkACOAutoSuggest.Checked) ?
ShellLib.ShellAutoComplete.AutoCompleteOptions.AutoSuggest : 0;
ac.ACOptions |= (chkACOAutoAppend.Checked) ?
ShellLib.ShellAutoComplete.AutoCompleteOptions.AutoAppend : 0;
ac.ACOptions |= (chkACOSearch.Checked) ?
ShellLib.ShellAutoComplete.AutoCompleteOptions.Search : 0;
ac.ACOptions |= (chkACOUpDownKeyDropsList.Checked) ?
ShellLib.ShellAutoComplete.AutoCompleteOptions.UpDownKeyDropsList : 0;
ac.ACOptions |= (chkACOFilterPrefixs.Checked) ?
ShellLib.ShellAutoComplete.AutoCompleteOptions.FilterPreFixes : 0;
ac.ACOptions |= (chkACOUseTab.Checked) ?
ShellLib.ShellAutoComplete.AutoCompleteOptions.UseTab : 0;
ac.ACOptions |= (chkACORtlReading.Checked) ?
ShellLib.ShellAutoComplete.AutoCompleteOptions.RtlReading : 0;
if (rdoMultiSource.Checked)
{
if ((!chkHistory.Checked)
&& (!chkMRU.Checked)
&& (!chkShellNamespace.Checked)
&& (!chkCustomList.Checked))
{
MessageBox.Show("At least one source should be checked!");
return;
}
ShellLib.IObjMgr multi =
(ShellLib.IObjMgr)ShellLib.ShellAutoComplete.GetACLMulti();
if (chkHistory.Checked)
multi.Append(ShellLib.ShellAutoComplete.GetACLHistory());
if (chkMRU.Checked)
multi.Append(ShellLib.ShellAutoComplete.GetACLMRU());
if (chkShellNamespace.Checked)
multi.Append(ShellLib.ShellAutoComplete.GetACListISF());
if (chkCustomList.Checked)
{
ShellLib.SourceCustomList custom = new ShellLib.SourceCustomList();
custom.StringList = GetCustomList();
multi.Append(custom);
}
ac.ListSource = multi;
}
else if (rdoHistory.Checked)
ac.ListSource = ShellLib.ShellAutoComplete.GetACLHistory();
else if (rdoMRU.Checked)
ac.ListSource = ShellLib.ShellAutoComplete.GetACLMRU();
else if (rdoShellNamespace.Checked)
ac.ListSource = ShellLib.ShellAutoComplete.GetACListISF();
else if (rdoCustomList.Checked)
{
ShellLib.SourceCustomList custom = new ShellLib.SourceCustomList();
custom.StringList = GetCustomList();
ac.ListSource = custom;
}
ac.SetAutoComplete(true);
}
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.
Hope you like it, Don't forget to vote.