Since Windows Vista, a new API called Windows Contacts (WC) was born. It replaces the Windows Address BookAPI.

You can get your personal properties by using the Windows Address Book Editor. It can be opened using “wab” in command line.

Bild

At first you see your personal “Contacts” folder. This folder contains your personal contact file. Its extension is “.contact”. A double click on it opens the Address editor as seen below:

Bild

“Web.exe” can also receive any personal contact file to import it to your personal contacts.

The contact file contains a formatted text in xml syntax. Data is arranged in a tree that is used to retrieve values. When I am writing of a path, I iterate through the tree. Some of the elements contains a collection of the same element type. The reason comes from the fact, that several other contacts can be added to the personal contact. The WC API uses the array brackets to access different contact values.

The following sample path shows you how to obtain the first user’s name.(Notice that the array starts with one not zero.)

NameCollection/Name[1]/GivenName

XML (manually formatted):

<c:NameCollection>
  <c:Name c:ElementID="cf809c84-6692-404e-9f57-6f81e130ee11">
    <c:GivenName c:Version="1" c:ModificationDate="2008-03-27T20:29:18Z">TestUser</c:GivenName>
    <c:FormattedName>TestUser</c:FormattedName>
  </c:Name>
</c:NameCollection>

This parts can be cut into three parts:

  1. NameCollection defines a container for the array
  2. /Name defines an array which contains all contacts
  3. GivenName defines a name property of exact one contact.

There are also Value tags:

ContactIDCollection/ContactID[1]/Value

XML (manually formatted):

<c:ContactIDCollection>
  <c:ContactID c:ElementID="aa8014fe-423c-4e8e-ad9f-4d750a6ba8bb">
    <c:Value>5634564e-72356-339-8220-12342f4a4106</c:Value>
  </c:ContactID>
</c:ContactIDCollection>

This path returns the GUID of contact number one.

Many parts of a path are already defined as constants. Have a look at them in MSDN.

So how can we read from the personal contact file? Since new API uses COM, we will use the following interfaces

  • IContact
  • IContactCollection
  • IContactManager
  • IContactProperties
  • IContactPropertyCollection

Actually there is no implementation of these interfaces for Delphi. To create your own type library (TLB) do the following steps (in Vista of course):

  1. Get Visual Studio 200x
  2. Download and install the latest version Windows SDK for Vista for MIDL and IContact.IDL.
  3. Open the Visual Studio Command Prompt (in VS open menu Tools). We need the predefined include path settings.
  4. Add path of midl.exe from the Windows SDK to your path environment. Use this command:
    set path=%path%;<your path here>\Microsoft SDKs\Windows\v6.0\Bin
  5. Change folder to ;<your path here>\Microsoft SDKs\Windows\v6.0\Include
  6. Backup and open IContact.idl in your favorite text editor.
    Find these lines…

    interface IContactPropertyCollection;
    interface IContactCollection;

    …and add :

    interface IContactProperties;
    interface IContactPropertyCollection;

    Additionally find this method…

    HRESULT CommitChanges([in] DWORD dwCommitFlags);

    …and also add :

    HRESULT _DO_NOT_USE1([in] IContactProperties* pC);
    HRESULT _DO_NOT_USE2([in] IContactPropertyCollection* pC);

    In this way midl.exe will also create interfaces in the typelibrary for IContactProperties and IContactPropertyCollection.
    I could not figure out how to do it in another way. Comments are apreciated.

  7. You should also find all appearances of…
    procedure Next; safecall;

    …and replace them by:

    function Next : HRESULT; stdcall;

    It is necessary because we need to check for a positive return value (S_FALSE). Delphi does not raise an EOleSysError exception for positive values.

  8. Save your work and start the conversion:
    midl icontact.idl /tlb icontact.tlb
  9. The new tlb file can now be imported using Delphi. To do so, start Delphi as Administrator, open menu “Components” and select “Install components” and hit the next button until all the components are listed. Click button “add” and choose the newly created IContact.tlb. Delphi imports the library and also generates the Delphi type library source code.

Did you know?

You can also (un)register a TLB file using regtlib.exe from C:\Windows\System32\URTTEMP
Type “regtlib Icontact.tlb” to register the library and “regtlib /u IContact.tlb” to unregister it.

You can find an already generated type library and and Delphi source code at the end of this article.

Using the Windows Contact API is really easy as soon as the Delphi type library unit exists. It implements class factories for IContact and IContactManager. Use the IContact class factory if you want to add your own contacts to the user’s personal database. If you just want to read data you have to use IContactManager.
Retrieving the current user’s profile is simple:

var
  ContactManager : IContactManager;
  Contact : IContact;
begin
  CoInitialize(nil);

  ContactManager := CoContactManager.Create;
  ContactManager.Initialize(‘Your AppName’,‘AppVersion’);

These lines creates an empty contact manager. Notice that the method Initialize is necessary for further calls.
Now we have to actually open the current user’s contact database.

ContactManager.GetMeContact(Contact);

The new interface Contact contains all the user’s personal data. We can obtain it through a path value as shown in the beginning of this article.
Let us obtain the user’s name. This task is done by retrieving the property interface IContactProperties using the Contact instance.

var
  ContactProp : IContactProperties;
  Path,
  Name : WideString;
  wName : PWideChar;
  Size : Cardinal;
begin
  OleCheck(Contact.QueryInterface(IID_IContactProperties, ContactProp));

The new instance ContactProp allows us to access a property through a path.

 …
  Path := ‘NameCollection/Name[1]/GivenName’;
  Size := 0;

  try
    ContactProp.GetString(PWideChar(Path), 0, nil, 0, Size);
  except
    GetMem(Name, Size*sizeof(WideChar));
    ContactProp.GetString(PWideChar(Path), 0, wName, Size, Size);
    Name := WideString(wName);
    FreeMem(wName);
  end;

The variable Name will contain the user’s name. In this way we make sure that the right amount of memory is allocated. Notice that the GetString actually returns the count of chars and not the count of bytes, so we have to double the size. We always get an exception in GetString because Delphi wraps (safecall convetion) the method and automatically raises an exception if the function returns a negative value (E_XXX code). We don’t distinguish between a memory error which we need to check for the correct size and a real error. So let us implement it in the right way:

 …
  try
    ContactProp.GetString(PWideChar(Path), 0, nil, 0, Size);
  except
    on E : EOleSysError do
    begin
      if (ResultCode(E.ErrorCode) <> ERROR_INSUFFICIENT_BUFFER) then
        raise;
      ….
    end;
  end;

In this way all “real” errors are catched and reraised.

All the other properties can be obtained in similar ways. However what properties are available at all? The interface IContactPropertyCollection allows us to obtain all available property names and types. IContactProperties implements a method GetPropertyCollection which returns a pointer to a IContactPropertyCollection that contains the requested properties. It is also possible to filter some of them.

function GetCollection(const ContactProp: IContactProperties)
var
  Dummy : PWideChar;
  Props : IContactPropertyCollection;
  H : HRESULT;

begin
  Dummy := nil;

  ContactProp.GetPropertyCollection(Props, 0, nil, 0, Dummy, Integer(true));

The new property collection contains an enumeration method to iterating all available properties. Be aware that you must call Next before you start getting data.

Dummy
ContactProp.GetPropertyCollection(Props, 0, nil, 0, Dummy, Integer(true));
H := Props.Next;

while H <> S_FALSE do
begin
  …
  H := Props.Next;
end;

In this way we can obtain name, type , version, modification time and ID of a property. Just use the available methods of IContactPropertyCollection.

  • GetPropertyName
  • GetPropertyType
  • GetPropertyVersion
  • GetPropertyModificationDate
  • GetPropertyArrayElementID

We already coded how to get a string using COM methods. So this code isn’t new to you. It retrieves the name of the property.

var
  PW : PWideChar;
  Name : WideString;
  Size : Cardinal;
begin
  Size := 0;
  try
    Props.GetPropertyName(nil,0, Size);
  except
    on E : EOleSysError do
    begin
      if (ResultCode(E.ErrorCode) <> ERROR_INSUFFICIENT_BUFFER) then
        raise;

      GetMem(PW, (Size+1)*sizeof(WideChar));
      Props.GetPropertyName(PW, Size, Size);
      Name := WideString(PW);
    end;
  end;
end;

To make life easier I created a unit that implements the shown code. You can download ContactMisc.pas in the download section of this article. The unit implements the following functions:

const
  CGD_DEFAULT = $00000000;

  CGD_UNKNOWN_PROPERTY = $00000000;
  CGD_STRING_PROPERTY = $00000001;
  CGD_DATE_PROPERTY = $00000002;
  CGD_BINARY_PROPERTY = $00000004;
  CGD_ARRAY_NODE = $00000008;

type TWideStringArray = array of WideString;

{@Name contains contact information}
TContactProperty = record
  //ID contains the contact’s guid as a string
  ID,
  //Name contains the contanct’s name
  Name : WideString;
  //Type contains contact type
  Typ,
  Version : Cardinal;
  Time : TFileTime;
end;

TContactPropertyArray = array of TContactProperty;

{@Name returns a contact value in binary form.
@param(Contact defines an interface to a contact which is used to retrieve data)
@param(Path defines a path to the value to be retrieved.
Most calls to @Name needs the string "/Value" at the end for success.
For more about possible path values visit:
http://msdn2.microsoft.com/en-us/library/ms735869(VS.85).aspx
)
@param(Mime returns the mime type of binary data)
@return(Returns a memory stream containing the data)
@raises(EPathNotFound will be raised if parameter Path could not be found.)
}

function GetContactBinaryValue(const Contact : IContact;const Path : WideString;
out Mime : WideString) : TMemoryStream;

{@Name returns an array of available contact properties.
@param(Contact defines an interface to a contact which is used to retrieve data)
}

function GetUserContactProperties(const Contact : IContact) : TContactPropertyArray;

{@Name returns all available contacts from a contact manager.
@param(Manager defines an interface to a contact manager interface)
@return(Returns a list of IContact interfaces)
}

function GetContactList(const Manager : IContactManager) : TInterfaceList;

{@Name returns the value of a contact’s property
@param(Path defines a path to the value to be retrieved.
Most calls to @Name needs the string "/Value" at the end for success.
For more about possible path values visit:
http://msdn2.microsoft.com/en-us/library/ms735869(VS.85).aspx
)
@param(Contact defines an interface to a contact which is used to retrieve data.)
@raises(EPathNotFound will be raised if parameter Path could not be found.)
}

function GetContactValue(const Contact : IContact;const Path : WideString) : WideString;

Some hints in the end

  • The Windows Contacts API does not allow to open foreign contact profiles. Although IContactManager implements the method Load, it is useless. Every contact file may contain several contacts. However only one contact belongs to the user. The method Loads expects a contact file name and the user’s ID (GUID) . /GUID:”xxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxx” /PATH:”……..\xy.contact”
    You cannot retrieve the GUID of a foreign user with the WC API. To do so, you had to parse the contact file manually.
  • So how does
    ContactManager.GetMeContact(Contact);

    retrieve the user’s ID?
    The ID is stored in the registry. Look at …
    HKEY_CURRENT_USER\Software\Microsoft\WAB\Me
    …and you’ll find the a string that is necessary for the Load method of IContactManager.

  • The WC API is sensible to the current token. A service may use an impersonated thread to get a foreign user contact.

Download:

Download the package here. It contains:

  • IContact.tlb – type library
  • Contact_tlb.pas – delphi type library header
  • ContactMisc.pas – IContact tools
  • Contacts.dpr – small demonstration