2007-06-14

Developing Office Add-ins the Easy Way - Part 2 of 2 (AKA: The tutorial stuff...)

If you haven't read Part 1, I'd advise you to do so. In it you'll find some more information on these components and, more importantly, your 20% discount coupon redeemable until the end of June 2007...

Step 1: Running the new Add-In Wizard

  1. Fire up Delphi and choose File->New->Other, Delphi Projects->Add-In Express VCL->ADX Outlook Add-in.
  2. Click Next and, for the project name, I'm using Insider and coInsider for the coClass Name. The first is used, guess what?, for the Project's name and the 2nd is used in Outlook's COM Addins dialog (Tools->Options->Other->Advanced Options)
  3. Clicking next will show you the "Option Pages" step which is optional.
    • For this addin we'll just hit next again. For a more complex add-in, this would be where you'd create your own option pages. You can have option pages displayed as another tab on the Options dialog or by right-clicking all or some folders in outlook, say one for Inbox, other for Outbox: they are different types of dialogs serving different purposes and you can add as many as you like.
  4. Hit next again and we'll be in the "Outlook 2007 Task Panes"; again, not of interest to us here so move along.
  5. Click Next, Finish and we're done here.

Step 1.5: More blah, blah, blah...

We now have a basic project that we'll add some meat to in order for it to do what we set out to. We could easily add an explorer or inspector command bar, but we don't need them for this project. An explorer command bar is one that is displayed on Outlook; an inspector command bar is displayed when you double-click some outlook item and "inspect" it in it's own window. Say, when you double click an e-mail message and another window pops up. You can have the inspector command bars only display for certain types of folders by simply setting a property, say, create an e-mail inspector bar that is displayed only when inspecting a mail message but not when viewing an appointment. And, of course, you can add as many as you want and add buttons, menus and other controls to them at will.

Among other cool objects you can use (not needed for this add-in), are the TADX<AppNameHere>AppEvents, such as TADXOutlookAppEvents from where you can add your own handlers. Here's the list of OutlookAppEvents (these are global events: other components have their own events as well, but these are events that don't relate to a particular button being pressed or hotkey being used):

  • OnItemSend
  • OnNewMail
  • OnNewMailEx
  • OnQuit
  • OnReminderFire
  • OnStartup

BTW: Thanks to blogger messing up my formatting big-time, I had to shorten the list to just a few sample items so as not to have a whole lot of space wasted. Maybe I'll take a prt-scr later and update this...

Step 2: Adding some components and code

  1. What we are now going to add is an "ADX Outlook Form" which is a small pane where we will be displaying the headers of the selected message, so go to File->New->Other, Delphi Projects->Add-In Express VCL and double click "ADX Outlook Form".
  2. Save the form (I used uHeader for the unit name).
  3. Add a TListBox, call it lbxHeader and set it to fill the form.
  4. We need another component to control creation of these panes. Go on to the main project form (should be empty) and add a "TadxOlFormsManager".
  5. In this component's events, add the following code to the "OnADXBeforeFormInstanceCreateEx" event:
    1 if args.FolderObj.EntryID = OutboxEntryID then begin
    2 args.Cancel := true;
    3 end;
  6. Let's add that OutboxEntryID as a protected property to the form (TAddInModule) of type string:
    1 property OutboxEntryID: string read FOutboxEntryID;
  7. And the corresponding internal field to the private section of TAddInModule:
    1 FOutboxEntryID: string;
  8. And a method to set the value in the protected section (not a setter method though!):
       1 procedure SetOutboxEntryID;
      2
      3 procedure TAddInModule.SetOutboxEntryID;
      4 var
      5 nameSpace : Outlook2000.NameSpace;
      6 outboxFolder: Outlook2000.MAPIFolder;
      7 begin
      8 try
      9 nameSpace := OutlookApp .GetNamespace ( 'MAPI' );
      10 outboxFolder := nameSpace .GetDefaultFolder( olFolderOutbox );
      11 FOutboxEntryID := outboxFolder.EntryID;
      12 finally
      13 nameSpace := nil;
      14 outboxFolder := nil;
      15 end;
      16 end;
    • Now click the form and enter the following code to it's "OnAddInInitialize" event:
      1 SetOutboxEntryID();
    • Finally, let's give the TadxOlFormsManager something to manage: double click it and add a new item. Set the following properties for that item:
      1. ExplorerItemTypes->expMailItem, set to True as we want this form displayed on mail folders only; leave all others to false.
      2. ExplorerLayout set to elTopSubpane; (you can play around with it later if you want!)
      3. And finally, let's link it to the form we created above: set FormClassName to our form created above. It will be available on the drop down as soon as you go into File->Use Unit and select our uHeader unit.

    If you build the project, register it and launch outlook, you'll see a blank new pane displayed: Sorry for the large, dimmed and blurred image: it's from my main outlook... :)

    Also, if you notice, so far most of the code was related to adding an exception, that is, we wanted our form to display on mail folders, unless that folder is Outbox.

    Now it's time to add our real meat into this.

    Step 3: Adding our application logic

    1. Lets go back to our uHeader unit and add the following code to the onADXSelectionChange event:
       1 procedure TfrmHeader.adxOlFormADXSelectionChange( Sender: TObject );
      2 var
      3 Selection: Outlook2000.Selection;
      4 IMail : _MailItem;
      5 IMessage : MAPIDefs.IMessage;
      6 PropValue: PSPropValue;
      7 begin
      8 lbxHeader.Items.Clear;
      9 Selection := Self.ExplorerObj.Selection;
      10 IMail := nil;
      11 if Selection.Count > 0 then begin
      12 try // Try for a "normal" mail item
      13 Selection.Item(1).QueryInterface( IID__MailItem, IMail );
      14 if not Assigned( IMail ) then begin
      15 // If it failed, try for a Read/Delivery Receipt item
      16 Selection.Item(1).QueryInterface( IID__ReportItem, IMail );
      17 end;
      18 if not Assigned( IMail ) then begin
      19 // I don't use Remote Mail, but for completeness this should be tested also...
      20 Selection.Item(1).QueryInterface( IID__RemoteItem, IMail );
      21 end;
      22 except// Ignore exceptions
      23 // QueryInterface may throw up an exception, but I don't recall under what condition it was
      24 // It was for some non->relevant item, i.e., one that we're not processing here
      25 // That's the bad thing of writing something and not documenting it properly when it happens!
      26 end;
      27 if Assigned(IMail) then begin
      28 try
      29 if IMail.Sent then begin
      30 IMail.MAPIOBJECT.QueryInterface(MapiDefs.IMessage, IMessage);
      31 if Assigned(IMessage) then begin
      32 try
      33 PropValue := nil;
      34 if HrGetOneProp(IMessage, PR_TRANSPORT_MESSAGE_HEADERS, PropValue) = S_OK then begin
      35 StringToList( PropValue^.Value.lpszA, lbxHeader.Items );
      36 end;
      37 finally
      38 MAPIFreeBuffer(PropValue);
      39 IMessage := nil;
      40 end;
      41 end;
      42 end;
      43 finally
      44 IMail := nil;
      45 end;
      46 end else begin // If no headers found...
      47 lbxHeader.Items.Add( 'This type of message does' );
      48 lbxHeader.Items.Add( 'not have mail headers' );
      49 end;
      50 end;
      51 end;
    2. Now let's add the MAPI units to our project and to the uses clause in this form:
      1 uses MapiDefs, MapiTags, MAPIUtil;
    3. Now all that is left is to add that StringToList function I used to process the semi-colon formatted mail headers into a more readable list:
       1 procedure StringToList( Str: string; lstResult: TStrings );
      2 // Convert a header string containing CR/LFs and TABs into
      3 // a stringlist where each new property starts on a new line
      4 var
      5 iPos : integer;
      6 const
      7 CRLF = #13#10;
      8 begin
      9 lstResult.Clear;
      10 iPos := Pos( CRLF, Str );
      11 while (iPos > 0) and (iPos < (Length( Str )-1) ) do begin
      12 // If the new line starts with a Space or Tab, it's a continuation line
      13 if (Str[iPos+2] = #9) or (Str[iPos+2] = #32) then begin
      14 Delete( Str, iPos, 2 );
      15 end else begin
      16 lstResult.Add( Copy( Str, 1, Pred(iPos) ) );
      17 Delete( Str, 1, Succ( iPos ) );
      18 end;
      19 iPos := Pos( CRLF, Str );
      20 end;
      21 end;

    And we're done!

    Just build again, and re-launch outlook and you'll have something like this:

    Final thoughts and notes:

    1. Don't forget that you can regsvr32 dlllname.dll or regsvr32 -u dllname.dll and any installer will do that for you if you mark the dll as self-registering.
    2. Don't forget that you'll need to close the application you're testing/compiling your add-in for, before you can compile it as the dll will be in use! :)
    3. If you look back, you'll see that you pratically never knew you were handling COM objects and that most of the code was for the application logic with part of the complexity in that arising from using Extended Mapi because Outlook's object model is a bit limited in some places... Creating Excel add-ins is even easier as you don't have to deal with Extended Mapi! ;) You will require some knowledge of the application's Object Model, but that comes out of the box in the form of some VBA help files that you can install with your Office.
    4. I didn't show in this simple demo two other easy to implement features, mostly out of lazyness: adding toolbars/buttons/menus and option pages. Trust me (or don't trust and instead try for yourself!): those are very easy to implement!
    5. Don't forget to take advantage of the 20% discount coupon (see Part 1), and have fun Developing Office Add-ins the Easy Way!
    6. Syntax highlighting done using Steve Trefethen's Syntax Highlighter and then manually tweaked the generated HTML to fix some coloring bugs.

    No comments: