One of Plaxo's cooler integration features involves putting little icons in Outlook's contacts view that indicate various pieces of state (e.g. Plaxo member, updated, bounced, etc) and a number of people have wondered how we do it.

Originally, the feature started out as part of a screenshot in some early mockups. When we were first designing the UI, we had a designer mockup a bunch of screens. One of them had a cutout of Outlook with a little Plaxo icon in the upper right. When we saw the screen we said, "Hey, that's really cool! How can we actually implement that?"

We actually ended up implementing the feature twice. Our first implementation started out as an exercise to see if it was even possible. It worked, but it left a lot to be desired. Initially, we annotated the name of each Plaxo member in your address book by adding an underscore to the end. When Outlook displayed the contacts view, the code would grab the pixels using GetDIBits and then perform some super simple OCR.

The concept is pretty simple, but getting it to actually work took some time. First, you have to subclass the window so you know when it's being drawn and can grab the bits. Now, in the WM_PAINT call, you can have something like this:

HDC hdc = GetDC(hwnd);
HDC hdcBuffer = CreateCompatibleDC(hdc);
HGDIOBJ hOldObj = SelectObject(hdcBuffer, m_hBuffer);

BitBlt(hdcBuffer, 0, 0, w, h, hdc, 0, 0, SRCCOPY);

if (GetDIBits(hdcBuffer, m_hBuffer, 0, (DWORD)bi.biHeight,(LPBYTE)lpbi +
              (bi.biSize + nColors * sizeof(RGBQUAD)), (LPBITMAPINFO)lpbi, (DWORD)DIB_RGB_COLORS)
    DrawCards(hwnd, hdcBuffer);

BitBlt(hdc, rcUpdate.left, rcUpdate.top, rcUpdate.right - rcUpdate.left, rcUpdate.bottom - rcUpdate.top,
       hdcBuffer, rcUpdate.left, rcUpdate.top, SRCCOPY);

SelectObject(hdcBuffer, hOldObj);
DeleteDC(hdcBuffer);
ReleaseDC(hwnd, hdc);

I left out a few details (getting the bits per pixel, heights, widths, etc) but that's the general idea. Now that you have the bits, to the next step is actually figuring out who's a member and where to draw the cards. Once that’s done, we’ll also need to know how wide each darkened name region is. Fortunately, the left and top borders are a fixed number of pixels (exactly how many changes slightly based on Outlook version and theme) so finding the first name region is easy. Now all we need to do is scan right, looking for the first white pixel as that represents the end of the name area.

Once you know how wide the name region is, you can calculate the number of columns and start searching for name regions. Scanning from the top, name regions are identified by their fixed background color (#D4D0C8) or their highlight color (#0A246A). Once you find one, it’s pretty easy to check for an underscore at the end by scanning the name region from the right border. If you find one, you draw the little icon and move on to the next name region.

Obviously, there are all kinds of problems with this approach and as soon as we'd implemented it, we knew it wasn't shippable. First, we were modifying the data in the address book for purely display purposes. Second, scanning the bits is pretty slow. Third, there are lots of cases where the bits aren't drawn (e.g. partial updates, obscured windows, etc) or are drawn differently (fonts, colors, etc). The list goes on.

We decided it was a pretty neat idea, but we needed to find something that was a lot more robust. Dru came up with a much better approach. Using techniques described by Matt Pietrek and Jeffrey Richter in various books and Microsoft Journal articles, there’s a clever way to insert your own function in between various system calls. For those looking for something more in-depth, check out this overview

The basic technique goes something like this: when a module (exe or DLL) is linked against a .lib stub, calls to external DLL functions are placed in an import address table (IAT) in the module. When the module is then loaded, the Windows loader resolves those imports by filling in the IAT with the addresses of the functions in the DLL. One benefit of this level of indirection is that it allows for functions to be relocated within a DLL without having to recompile all of the modules that use it (i.e. if you rebuild a DLL, you don't have to rebuild all of the exes that link to it). While not designed for this purpose, it also provides a systematic way to insert code in between the caller and the callee (for calls dispatched by the IAT) by changing the address in the IAT.

Here's a screen shot of the dependency walker included with Microsoft Visual C++ showing the imports that will be resolved at runtime in the upper right. For example, plx_core.dll has an import entry for BitBlt in GDI32.dll:

To draw the icons, all we have to do is replace the import entries for some of the text drawing functions in Outlook (DrawText, TextOut, etc. depending on the version). To do that, we’ll first walk through the import descriptors, since there's one descriptor per DLL. In the screenshot above, each item under plx_core.dll corresponds to an import descriptor. Once we find the descriptor we're looking for, we then have to find its entry in the import table. The following code finds the correct import table and then looks for entry you're trying to replace:

while (pImportDesc->Name)
{
  // note that the szImportDLLName should match the szDllName (from 
  // c_szDllName that we passed in). we can optimize this to continue
  // if the names don't match
  char* szImportDLLName = MakePtr(char*, hLocalModule, pImportDesc->Name);

  // do a case insensitive compare and if these names match, break so that we can
  // process this import descriptor
  if (_stricmp(szDllName, szImportDLLName) == 0)
    break;

  // iterate to the next import descriptor
  pImportDesc++;
}

// if we have an invalid import descriptor, then we know that we didn't find the 
// DLL we were looking for.
if (!pImportDesc->Name)
  return FALSE;


PIMAGE_THUNK_DATA pThunkAddress;
pThunkAddress = MakePtr(PIMAGE_THUNK_DATA, hLocalModule, pImportDesc->FirstThunk);

// as long as these are non-null, we can keep iterating
while (pThunkAddress->u1.Function)
{
  // if we get a match, then return the current pThunkAddress
  if (DWORD(pThunkAddress->u1.Function) == dwAddressToIntercept)
    break;

  // otherwise, iterate to the next PIMAGE_THUNK_DATA item
  pThunkAddress++;
}

if (!pThunkAddress->u1.Function)
  return FALSE;

// pThunkAddress now contains the import entry we're looking for

Once you have the import entry, you can replace it with your own call:

// we can write to this address, so save off the old API so that we can still access it.
if (p_pApiOrg)
  *p_pApiOrg = PVOID(pThunkAddress->u1.Function);
 
// we can write to the address of this function, so simply write over the old address
// with the address of our new function.
pThunkAddress->u1.Function = (DWORD)pApiNew;

For the sake of some brevity (not that it's very short in the first place), some of the details are left out (e.g. the page pointed to by pThunkAddress is probably write protected so you have enable writing while you update the import entry using VirtualProtect and then disable write access once you're done), but it's the right idea.

By replacing the import entry for various text drawing functions (DrawTextEx, ExtTextOut, etc), we can pretty easily track when things are drawn in the address book. Unfortunately, it's always a little more complicated in practice, as different versions of Outlook uses different functions and you only want to replace those calls when the address book is being displayed. Another thing to watch out for is when other people change the import table. Once you take care of those issues though, it's pretty straight forward to have a quick lookup table for items that should have icons. In the end, hooking into the IAT is a lot faster, more robust, and more precise than doing OCR ;) and that's still how we draw the icons today.

If you're interested in more information, check out IAT articles on Code Project

TrackBack

TrackBack URL for this entry:

http://blogadmin.plaxo.com/mt-tb.cgi/58

Comments

Oooh baby, I love it when you're technical!

Very cool!!!

Posted by: Nicholas at April 12, 2006 07:30 AM

Post a comment










Remember personal info?