Inside the PowerPoint Presentation Indexer

Home
Back To Tips Page
  Back to the PowerPoint Indexer
 

Like most of my projects, this has a lot of useful MFC techniques, control management techniques, and so on that are worth examining as a programmer.  Some of the tricks I use include, in no particular order,

To help expedite searching for specific features, there is a simple index at the end of this page.

Part of why such a complex program can go together quickly (less than a week total effort) is the richness of my libraries.  Most of the important ones are already on my Web site and therefore are available to everyone.

Ultimately, there are about 27,000 source lines in this project.  Of these, about 5200 are cribbed piecewise from other files of mine, and about 3900 were automatically generated by including the appropriate PowerPoint objects.  So I wrote, from scratch, a mere 18,000 lines of code to implement this whole thing.  This is the power of having a rich set of reusable libraries.

Avoiding the CWinApp trap

Many programmers who are used to programming in C tend to create a lot of global variables, although I cannot figure out why.  Global variables are very rare objects in C++ programming.  However, those who have been admonished to "not use global variables" then fall into another trap: putting all their global state in their CWinApp-derived class.  This is almost always a mistake.  The excuse is given "but they aren't global variables" belies the fact that dumping them all into the CWinApp-derived class is nothing more than syntactic saccharine (not even as good as syntactic sugar) for declaring a global variable.  The first thing to recognize is that if you ever write

(CMyWinAppClass *)AfxGetApp()->

or

theApp.

then you have a poor program structure.  If a variable is truly global, make it global!  Don't be embarrassed if you have two or three global variables in your application!  Generally, these represent the true "global state" of the application and must be widely accessible.  However, it is good practice to not actually access member variables of these objects, and instead use methods.  A more C++-centric viewpoint is to create what are called "singleton classes", but that's not a critical distinction.  The important thing is to not see the CWinApp class as a "dumping ground" for all kinds of random state that is accessed in all kinds of random places in your code.

The simplest way to avoid this mistake is to simply remove the #include of your CWinApp-derived class from every file that does not require it.  Generally, this means that you will remove it from every file except the CWinApp-derived implementation file.  There is exactly one occurrence of "#include "Indexer.h"" in my code, and that is in indexer.cpp.  In many cases you can simply replace it with #include "resource.h" and in the remaining cases, you simply delete it entirely.  By removing this, you remove a piece of completely unnecessary clutter from your project, and it is cleaner and more robust.

There is no need to access your CWinApp-derived class outside the CWinApp-derived implementation file. If you think there is, you are probably wrong.  If you are writing MDI or SDI and think you need it in a view or document, the probability that you are wrong is so close to 100% that it hardly makes a difference.  If you access it from a dialog, the probability your design is wrong is 100%.  What is a dialog doing accessing anything outside its own class or information that is passed directly to it?  See my essay on common mistakes in application design, The n Habits of Highly Defective Windows Programs.

Background threading

I started off with the typical example of using a single GUI thread.  But it would take nearly two minutes to read in my large presentations, and during testing I would occasionally decide that I wanted to stop the reading process, but I couldn't.  So I moved long computations to background threads.

Many of the techniques are discussed in my essay on worker threads.

Key here is that I had to use PostMessage to send messages back to the main GUI thread.  Since I did not want to pop up a MessageBox in the secondary thread, this meant that error messages also had to be handled by the main GUI thread.

In addition, the secondary thread could easily flood the message queue of the main thread, meaning that while I indeed was running a secondary thread that in principle could be aborted, because it was a separate thread, in practice it couldn't be aborted, because it had filled up the message queue with messages to process.  To avoid this, I used my I/O Completion Port technique of posting messages to the main thread.  So instead of calling PostMessage, I will call my enqueue method MessageQueue::PostQueuedMessage.

Reporting File Open Errors

/****************************************************************************
*                           UWM_THREAD_OPEN_FAILURE
* Inputs:
*       WPARAM: (WPARAM)::GetLastError(): error code
*       LPARAM: (LPARAM)(CString *) failing file name
* Result: LRESULT
*       Logically void, 0, always
* Effect: 
*       Pops up a bad-file-name warning
****************************************************************************/

DECLARE_MESSAGE(UWM_THREAD_OPEN_FAILURE)

The handler is invoked by the following line in the Message Map

 ON_REGISTERED_MESSAGE(UWM_THREAD_OPEN_FAILURE, OnThreadOpenFailure)

and the handler is

LRESULT CIndexerDlg::OnThreadOpenFailure(WPARAM wParam, LPARAM lParam)
    {
     CString * filename = (CString *)lParam;
     ReportOpenError((DWORD)wParam, *filename);
     delete filename;
     return 0;
    } // CIndexerDlg::OnThreadOpenFailure
void CIndexerDlg::ReportOpenError(DWORD err, const CString & filename)
    {
     CString msg;
     CString fmt;
     fmt.LoadString(IDS_FILE_OPEN_FAILED);
     msg.Format(fmt, filename, ErrorString(err));
     AfxMessageBox(msg, MB_ICONERROR | MB_OK);
    } // CIndexerDlg::ReportOpenError

This uses my ErrorString function.

When I open a file in my thread, the code looks like this:

     CStdioFile f;
     if(!f.Open(pathname, CFile::modeRead))
        { /* failed */
         DWORD err = ::GetLastError();
         MessageQueue::PostQueuedMessage(this, UWM_THREAD_OPEN_FAILURE, (WPARAM)err, (LPARAM)new CString(pathname));
         MessageQueue::PostQueuedMessage(this, UWM_READER_THREAD_FINISHED, err);
         return err;
        } /* failed */ 

This code is executed in my top-level thread function within the class (there is the typical C-to-C++ transfer function, but it is as described in my  essay on worker threads), so returning from the function will terminate the thread, and I have to notify my caller that the thread is finished.  This is handled in the top-level thread function:

/****************************************************************************
*                           CIndexerDlg::ReadThread
* Inputs:
*       LPVOID p: (LPVOID)(CIndexerDlg *)
* Result: UINT
*       0, always
* Effect: 
*       Thread top-level function
****************************************************************************/

/*static */ UINT CIndexerDlg::ReadThread(LPVOID p)
    {
     ReaderThreadParams * parms = (ReaderThreadParams *)p;
     UINT err = parms->object->ReadIndexFile(parms->filename);
     MessageQueue::PostQueuedMessage(parms->object, UWM_READER_THREAD_FINISHED, err);
     delete parms;
     return 0;
    } // CIndexerDlg::ReadThread

No matter how I leave my thread function I will return to this function, which will notify the initiator of the thread, whose CWnd * is in parms->object, that the thread has terminated.  This implies that I exercise Best Practice and never, ever, under any circumstances imaginable, would do ExitThread or any of its numerous aliases from within the thread.

Updating controls

In the reading of a file, or the creation of an index, I have to update windows in the main thread.  To do this, I queue up messages to the main thread.  This follows the techniques I outline in my essay on worker threads.  The progress bar is updated quite simply, and I point this out only because I see so many programmers thinking that a progress bar can only have a range of 0..100 and go through all kinds of contortions to convert values to that range.

Once I've opened the file as above, I will do

     MessageQueue::PostQueuedMessage(this, UWM_SET_PROGRESS_RANGE, 0, (LPARAM)length);
     MessageQueue::PostQueuedMessage(this, UWM_SET_PROGRESS_POS, 0); 

As I read lines, I will then do

     while(f.ReadString(s))
        { /* read index line */
         if(Stop)
            break;

         DWORD pos = (DWORD)f.GetPosition();

         MessageQueue::PostQueuedMessage(this, UWM_SET_PROGRESS_POS, pos); 

I have reasonable confidence no one will be indexing a PowerPoint presentation whose input file exceeds 4.2GB, so the truncation to a DWORD is safe.  Note the Stop variable, which is a BOOL variable which is set to FALSE before the thread is created and is set to TRUE to terminate the thread.

The handlers are

LRESULT CIndexerDlg::OnSetProgressRange(WPARAM wParam, LPARAM lParam)
    {
     c_Progress.SetRange32((int)wParam, (int)lParam);
     return 0;
    } // CIndexerDlg::OnSetProgressRange
LRESULT CIndexerDlg::OnSetProgressPos(WPARAM wParam, LPARAM)
    {
     c_Progress.SetPos((int)wParam);
     return 0;
    } // CIndexerDlg::OnSetProgressPos

To handle the adding of the text lines from the slides, I do

/****************************************************************************
*                          CIndexerDlg::OnSlideLine
* Inputs:
*       WPARAM: slide number
*       LPARAM: (LPARAM)(CString *): slide text
* Result: LRESULT
*       Logically void, 0, always
* Effect: 
*       Enters the line into the control
****************************************************************************/

LRESULT CIndexerDlg::OnSlideLine(WPARAM wParam, LPARAM lParam)
    {
     CString * s = (CString *)lParam;
     int Slide = (int)wParam;

     if(Stop)
        { /* stop */
         delete s;
         return 0;
        } /* stop */

     LVITEM item = {0};
     item.mask = LVIF_PARAM;
     item.iItem = c_IndexData.GetItemCount();
     item.iSubItem = 0;
     item.lParam = (LPARAM)new SlideData(Slide, s);

     int n = c_IndexData.InsertItem(&item);
     
     return 0;
    } // CIndexerDlg::OnSlideLine 

Some lines are worth noting in this sequence.  Note that I check the Stop flag.  This was a discovery that even if I stop the thread, it has queued up a lot of messages, and the processing of those messages takes time (in particular, the cost of updating the CListCtrl), so by bypassing the insertion of queued lines, control appears to return to the user more quickly after the thread stop command is given.

The only value set is the LPARAM.  This is because I use an owner-draw list control and everything I need to do the display is contained in a SlideData object.

When a thread terminates, it posts a notification.  The one for the reader thread (the thread that is reading the slide data) is rather interesting.

/****************************************************************************
*                      CIndexerDlg::OnReaderThreadFinished
* Inputs:
*       WPARAM: (WPARAM)(::GetLastError()): Error code
*       LPARAM: unused
* Result: LRESULT
*       Logically void, 0, always
* Effect: 
*       Finishes the processing.  If ERROR_SUCCESS, reads in additional files
****************************************************************************/

LRESULT CIndexerDlg::OnReaderThreadFinished(WPARAM wParam, LPARAM)
    {
     if(wParam == ERROR_SUCCESS)
        { /* finish up */
         CString TempNullFileName = MakeNullNameFromIndex();
         ReadNullWordFile(TempNullFileName, TRUE);

         CString TempRuleFileName = MakeRuleNameFromIndex();
         ReadRuleFile(TempRuleFileName);

         ClearHyphenates();
        } /* finish up */

     c_IndexCount.SetWindowText(ToString(_T("%d"), c_IndexData.GetItemCount()));
     reading = FALSE;

     CString newcaption = Caption;
     newcaption += _T(" - ");
     newcaption += IndexFile.name;
     SetWindowText(newcaption);
     
     AddMRU(IndexFile.name);
     SaveMRU();

     SlideTable.SetSize(0, c_IndexData.GetItemCount());

     for(int i = 0; i < c_IndexData.GetItemCount(); i++)
        { /* build non-control table */   
         SlideTable.Add((SlideData *)c_IndexData.GetItemData(i));
        } /* build non-control table */

     HandleClosePending();
     
     updateControls();
     return 0;
    } // CIndexerDlg::OnReaderThreadFinished 

I only continue processing if the error code in the WPARAM is ERROR_SUCCESS.  In that case, I read the .nul and .rule files of the same name, if they exist.  I also clear the list of hyphenated words.

The changing of the caption conforms to standard conventions of indicating the active document. However, I had to save the Caption value (a CString) in the OnInitDialog handler so I had a copy of it (this avoids having to put a separate caption string in the STRINGTABLE).

I add the file to the MRU list and then save the MRU list to the Registry.

To improve performance, I build an array of the slide data (this is much faster than doing search by doing a GetItemData on the slide data each time I need to search, and since the slide data is read-only once it has been loaded, I don't get into trouble dealing with the issues of element insertion and deletion.  Generally I try to avoid having duplicate sets of values (one in the control and one in an array) but the frequency of lookups meant that it was too slow getting the elements directly from the control.

The HandleClosePending method will be discussed when I discuss delayed app shutdown.

Disabling menu items

One of the most common errors of thinking I see in programmers new to threading is a failure to handle menus or controls properly.  There are two common errors

The second error is usually the result of having detected the first error and writing an incorrect solution to the problem.

I have a Boolean function GetBusy which returns TRUE if there is a secondary thread running.  If I were writing an SDI or MDI app, I would incorporate this into an ON_UPDATE_COMMAND_UI handler.  But this is a dialog-based app, so I don't have that option.

Here is my OnInitPopupMenu handler.  Note the use of macros to handle tedious tasks of typing.

/****************************************************************************
*                        CIndexerDlg::OnInitMenuPopup
* Inputs:
*       CMenu * pPopupMenu: popup menu
*       UINT nIndex: ignored
*       BOOL bSysMenu: ignored
* Result: void
*       
* Effect: 
*       Enables menu items
****************************************************************************/

void CIndexerDlg::OnInitMenuPopup(CMenu* pPopupMenu, UINT nIndex, BOOL bSysMenu)
    {
     CDialog::OnInitMenuPopup(pPopupMenu, nIndex, bSysMenu);

#define Enable(x) (MF_BYCOMMAND | ( (x) ? MF_ENABLED : MF_GRAYED))
#define EnableMenu(id, op) pPopupMenu->EnableMenuItem(id, Enable(op))
/* New                    */ EnableMenu(ID_FILE_NEW,              !GetBusy());
/* Open Index File...     */ EnableMenu(ID_FILE_OPENINDEXFILE,    !GetBusy() && CheckInputRange());
/* Open Null Word File... */ EnableMenu(ID_FILE_OPENNULLWORDFILE, !GetBusy());
/* Open Rules File...     */ EnableMenu(ID_FILE_OPENRULESFILE,    !GetBusy());
/*------------------------*/
/* Write HTML Output File */ EnableMenu(ID_FILE_WRITEINDEXFILE,   !GetBusy() && IndexingOK());
/*------------------------*/
/* Save Null Words...     */ EnableMenu(ID_FILE_SAVENULLWORDS,    !GetBusy() && NullFile.modified && c_NullWords.GetCount() > 0);
/* Save Null Words As...  */ EnableMenu(ID_FILE_SAVENULLWORDSAS,  !GetBusy() && c_NullWords.GetCount() > 0);
/* Save Rules File        */ EnableMenu(ID_FILE_SAVERULESFILE,    !GetBusy() && RuleFile.modified && c_CanonicalizationRules.GetCount() > 0);
/* Save Rules File As...  */ EnableMenu(ID_FILE_SAVERULESFILEAS,  !GetBusy() && c_CanonicalizationRules.GetCount() > 0);
/*------------------------*/
/* Write VBA Script file  */
/*------------------------*/
/* Recent Files          >*/ EnableMenu(ID_FILE_RECENTFILES,      !GetBusy() && RecentFileCheck(pPopupMenu));
/*------------------------*/
/* Exit                   */
/*------------------------*/
#undef Enable
#undef EnableMenu
    } 

Note that I leave some menu items, most notably the Exit menu item, enabled.  In addition, this is the only dropdown menu from the menu bar that requires any changes.  The View and Help menus have no items to be disabled. 

When one of these menu items is dropped down, this code is also executed, but because none of the menu IDs are present in them, the code has no effect.

The GetBusy function also features prominently in my updateControls handler that deals with control enabling.  This is described in my essay on Dialog Control Management.

void CIndexerDlg::updateControls()
    {
     if(!initialized)
        return;
     
     BOOL busy = GetBusy();

     c_Stop.EnableWindow(busy && !Stop);

     //****************************************************************
     // Progress bar
     //****************************************************************

     c_Progress.ShowWindow(busy ? SW_SHOW : SW_HIDE);

     //****************************************************************
     // Open button
     //****************************************************************

     BOOL rangeok = CheckInputRange();
     
     c_OpenPresentation.EnableWindow(!busy && rangeok);

     //****************************************************************
     // The overuse threshold
     //****************************************************************

     c_OverUse.EnableWindow(!busy);
     c_SpinOveruse.EnableWindow(!busy);

This is just a piece of the updateControls handler.  The progress bar is intended to show thread progress, so it is only shown if there is an active thread.  The Stop window is only enabled if a thread is running and we are not already handling a Stop request.  The Open button requires that there be no thread currently running, and the specified input range of slides is either unchecked or the page range is a valid range that includes at least one page.  Most of the other controls are then as simple as the last two lines shown, simply being disabled if there is a background thread.  But there are some exceptions which I will discuss later when I discuss controls. 

Handling deferred shutdown

Another threading issue that arises is how to deal with close requests.  Many people will do things like call ::WaitForSingleObject on the thread handle.  The problem with this is that it blocks the main GUI thread, which may have to be doing other things as part of the shutdown, such as properly closing files.  Therefore it is usually a bad idea to block this thread.  The way I do this is to request a thread shutdown, and when the thread has finished, complete the shutdown.

In my dialog-based apps, the OnOK and OnCancel handlers have their bodies removed, and the OnClose handler normally calls CDialog::OnOK.  This is discussed in my essay on Dialog Box Applications.  But because of the background threads, the handler in this application is a bit more complex.

/****************************************************************************
*                            CIndexerDlg::OnClose
* Result: void
*       
* Effect: 
*       Checks all modified structures; if no modifications, exits
****************************************************************************/

void CIndexerDlg::OnClose()
   {
    if(GetBusy())                       // [1]
       { /* terminate threads */
        Stop = TRUE;                    // [2]
        ClosePending = TRUE;            // [3]
        return;                         // [4]
       } /* terminate threads */

    if(CheckAllSaved())                 // [5]
       return;                          // [6]

    ClearAll();                         // [7]
    CDialog::OnOK();                    // [8]
   }
  1. See if there is any background thread running
  2. If there is a background thread, set the Stop flag to cause the thread to shutdown
  3. Set the ClosePending flag
  4. Returns without completing the close operation
  5. If no thread running, prompts the user about unsaved changes, with the usual Yes/No/Cancel options. 
  6. If cancelled, this function returns FALSE and we return without completing the close operation.
  7. We are about to exit.  Avoid any annoying messages about storage leaks by freeing up all storage in use
  8. Call CDialog::OnOK to complete the shutdown event

Thread termination functions, which are invoked by posting messages to the main GUI thread when a secondary thread terminates, will call the HandleClosePending handler when they have cleaned up everything that needs to be cleaned up.  This handler will determine if a close operation is pending, and if so, will continue the close operation.  This is called after the thread completion handler has reset the flag that causes GetBusy to return TRUE

/****************************************************************************
*                       CIndexerDlg::HandleClosePending
* Result: void
*       
* Effect: 
*       If there is a close pending, force it closed now, otherwise no effect
****************************************************************************/

void CIndexerDlg::HandleClosePending()
    {
     if(ClosePending)
        { /* close now */
         ClosePending = FALSE;
         PostMessage(WM_CLOSE);
        } /* close now */
    } // CIndexerDlg::HandleClosePending 

If there is no close pending, this function does nothing.  If there is a close pending, it Posts a message to continue the close operation.  When this message is processed and the OnClose handler is called, there will be no concurrent thread, and the close will finish.

Managing MRU lists

I thought I might use the CRecentFileList class, but typical of the general degradation of the quality of the documentation that has become a problem, there are no examples in the MSDN.  Instead, we are referred to a Knowledge Base article, Q243751.  However, a search for Q243751, on both the local MSDN installation and on the Microsoft online site, give no hits, with or without the Q prefix (without the Q prefix, MSDN online gives three irrelevant hits, with it, I locate the articles that cite it).  So I went back to my own code from several years ago (before CRecentFileList was documented).  I used the code I had written.  This uses methods of my Registry class

I decided that the simplest representation was a CStringArray, particularly because my Registry library has functions to load and store arrays of strings.

 CStringArray MRU;

There are several functions I wrote to deal with the MRU list. 

 

/****************************************************************************
*                             CIndexerDlg::AddMRU
* Inputs:
*       const CString & filename: File name to add to MRU list
* Result: void
*       
* Effect: 
*       Adds the file to the MRU list (internally) but does not save the MRU list
*       The specified file is always moved to the head of the list if it is already present
*       If the list exceeds MRU_LIMIT, the oldest item is now removed
****************************************************************************/

void CIndexerDlg::AddMRU(const CString & filename)
    {
     for(int i = 0; i < MRU.GetSize(); i++)
        { /* find if already there */
         if(_tcsicmp(filename, MRU[i]) == 0)
            { /* hit */
             MRU.RemoveAt(i);
             break;
            } /* hit */
        } /* find if already there */

     MRU.InsertAt(0, filename);

     if(MRU.GetSize() > MRU_LIMIT)
        MRU.SetSize(MRU_LIMIT);
    } // CIndexerDlg::AddMRU


/****************************************************************************
*                            CIndexerDlg::SaveMRU
* Result: void
*       
* Effect: 
*       Saves the MRU list
****************************************************************************/

void CIndexerDlg::SaveMRU()
    {
     SetRegistryValues(HKEY_CURRENT_USER, IDS_REGISTRY_MRU, MRU);
    } // CIndexerDlg::SaveMRU
    
/****************************************************************************
*                            CIndexerDlg::LoadMRU
* Result: void
*       
* Effect: 
*       Loads the MRU list
****************************************************************************/

void CIndexerDlg::LoadMRU()
    {
     MRU.RemoveAll();
     GetRegistryValues(HKEY_CURRENT_USER, IDS_REGISTRY_MRU, MRU);
    } // CIndexerDlg::LoadMRU

Initially, I create a simple text string menu item, Recent Files, and if the MRU list is empty, it remains a disabled text menu item.  But if there are files in the MRU list, I replace it with a popup menu that has the list of menu items.  The popup menu handling starts in OnInitMenuPopup, where I enable the menu item based on the return value from RecentFileCheck

EnableMenu(ID_FILE_RECENTFILES, !GetBusy() && RecentFileCheck(pPopupMenu));

The purpose of the RecentFileCheck is to replace the placeholder with an actual popup menu. 

/****************************************************************************
*                        CIndexerDlg::RecentFileCheck
* Inputs:
*       CMenu * menu: The popup menu
* Result: BOOL
*       TRUE to enable the menu
*       FALSE to disable it
* Effect: 
*       If the MRU list is empty, disables the menu item
*       If the MRU list has files, display them
****************************************************************************/

BOOL CIndexerDlg::RecentFileCheck(CMenu * menu)
    {
     //*******************************************************************
     // First, retrieve the current information about the Recent Menu item
     //*******************************************************************
     CString text;                                                         // [1]
     MENUITEMINFO info = { sizeof(MENUITEMINFO) };
     info.fMask = MIIM_FTYPE | MIIM_ID | MIIM_STRING | MIIM_SUBMENU;
     info.dwTypeData = text.GetBuffer(MAX_PATH);
     info.cch = MAX_PATH;
     BOOL b = menu->GetMenuItemInfo(ID_FILE_RECENTFILES, &info, FALSE);
     
     if(!b)
        { /* failed */
         text.ReleaseBuffer();
         return FALSE;
        } /* failed */
     text.ReleaseBuffer();
     
     //****************************************************************
     // If it is already a submenu, just repolulate it
     // If it is not a submenu, create the submenu and populate it
     //****************************************************************
     if(info.hSubMenu == NULL)
        { /* not yet a popup */
         // Load the recent file list.  If the MRU is empty, do nothing
         CMenu * popup = new CMenu;                                      // [2]
         popup->CreatePopupMenu();                                       // [3]
         if(PopulateMRU(popup))
            { /* change to popup */
             info.hSubMenu = popup->m_hMenu;                             // [4]
             if(menu->SetMenuItemInfo(ID_FILE_RECENTFILES, &info, FALSE))// [5]
                { /* success */
                 popup->Detach(); // remove handle so delete doesn't kill menu  // [6]
                 delete popup;                                           // [7]
                 return TRUE;
                } /* success */
             popup->DestroyMenu();                                       // [8]
             delete popup;                                               
             return FALSE;
            } /* change to popup */
         else
            { /* no MRU files */
             popup->DestroyMenu();                                       // [9]
             delete popup;                                               
             return FALSE;
            } /* no MRU files */
        } /* not yet a popup */
     else
        { /* has popup */
         if(!PopulateMRU(CMenu::FromHandle(info.hSubMenu)))              // [10]
            return FALSE;
         return TRUE;
        } /* has popup */
    } // CIndexerDlg::RecentFileCheck
 
  1. Retrieve the current menu information
  2. If there is not a popup menu item already, use new CMenu to create a CMenu to hold the new popup
  3. Create an actual popup menu
  4. Store its handle in the MENUITEMINFO structure
  5. Make the menu item contain the newly-filled popup menu
  6. We are about to destroy the CMenu object, which is no longer needed, but we need to Detach it from the MFC object so when the MFC object is deleted, we won't lose the menu itself.  See my essay on Attach/Detach for more details.
  7. The CMenu object, which we only needed temporarily, is now deleted
  8. If we get here, the SetMenuItemInfo failed.  So we destroy the menu and delete the CMenu object
  9. We get here if the PopulateMRU method returned FALSE, and delete the newly-created popup menu and delete the CMenu object
  10. There is already a menu for this item.  We simply repopulate it.  The CMenu::FromHandle creates a temporary CMenu object to represent the menu. This temporary CMenu will be deleted when the next OnIdle event happens.

When I created the ID for the menu item, I assigned it to a range that is unlikely to be used in this project.  I assigned it the id 5000.  Therefore, no other menu item I am likely to add is going to conflict with this value.  I have reserved the values ID_FILE_RECENTFILES to ID_FILE_RECENTFILES + MRU_LIMIT to represent menu items in the MRU list.  This allows me to write an ON_COMMAND_RANGE handler for the MRU events.

To see how this is set up, here's the PopulateMRU function.

/****************************************************************************
*                          CIndexerDlg::PopulateMRU
* Inputs:
*       CMenu * popup: The MRU popup menu
* Result: BOOL
*       TRUE if there is anything in the menu
*       FALSE if nothing in the menu
* Effect: 
*       Populates the MRU menu
****************************************************************************/

BOOL CIndexerDlg::PopulateMRU(CMenu * popup)
    {
     if(MRU.IsEmpty())
        return FALSE;

     // Delete all items from the menu
     while(popup->GetMenuItemCount() > 0)
        { /* delete each */
         popup->DeleteMenu(0, MF_BYPOSITION);
        } /* delete each */

     for(int i = 0; i < MRU.GetSize(); i++)
        { /* populate menu */
         CString s;
         // &0. filename
         s.Format(_T("&%d. %s"), i, MRU[i]);
         popup->AppendMenu(MF_STRING, ID_FILE_RECENTFILES + 1 + i, s);
        } /* populate menu */
     return TRUE;
    } // CIndexerDlg::PopulateMRU 

It first deletes any existing items from the MRU list.  Then it populates the MRU list with menu items and provides a shortcut key for them by using a single-digit key.  The MRU list will look like this when it comes up:

When a menu item is clicked the Message Map contains the entry

 ON_COMMAND_RANGE(ID_FILE_RECENTFILES + 1, ID_FILE_RECENTFILES + MRU_LIMIT, OnMRU)

The handler retrieves the string, removes the "&0. " string (this code assumes a single digit for the shortcut), and invokes the method that will run the reader thread.

/****************************************************************************
*                             CIndexerDlg::OnMRU
* Inputs:
*       UINT id: menu ID, ID_FILE_RECENTFILES+1..ID_FILE_RECENTFILES+MRU_LIMIT
* Result: void
*       
* Effect: 
*       Handles a file open request
****************************************************************************/

void CIndexerDlg::OnMRU(UINT id)
    {
     CString item;
     GetMenu()->GetMenuString(id, item, MF_BYCOMMAND);
     // Form of string is
     // &1. filename
     // ^^^^^
     // 01234
     // This is the syntax written in the PopulateMRU function
     if(item[0] != _T('&'))
        { /* error */
         ASSERT(FALSE);
         return;
        } /* error */
     
     if(!_istdigit(item[1]))
        { /* not digit */
         ASSERT(FALSE); // not a digit
         return;
        } /* not digit */

     if(item[2] != _T('.'))
        { /* not dot */
         ASSERT(FALSE);
         return;
        } /* not dot */
     
     if(item[3] != _T(' '))
        { /* not space */
         ASSERT(FALSE);
         return;
        } /* not space */
     
     item = item.Mid(4); // if everyone did their job right, this is the filename
     RunIndexFileThread(item);
    } // CIndexerDlg::OnMRU

Restricted types

The FID class

I have a type which is used to represent a "file".  This is the token, the "file ID", which is passed around, stored, and used to access various PowerPoint presentations.  A "file" representation is a very rich structure, which has the file name (obviously), the "short name" or "alias" used to display which file a word came from in various contexts, including the generated index; the current PowerPoint state such as the various automation collections that comprise the file; its original timestamp; and a fairly large amount of other information.

However, I did not want to represent these as a UINT.  Not only is this confusing about what a UINT might mean, but it means that the user could inadvertently modify a UINT by assigning another UINT to it.  So the idea was to create a type that had very restricted capability.  It can be compared for equality, compared for ordering (for std::map), and assigned, but pretty much nothing else.  Except in limited contexts.

/*****************************************************************************
*                                  class FID
*****************************************************************************/
class FID {                                                                    [1]
    public:                                                                    [2]
       FID() { fid = 0; }                                                      [3]
       bool Invalid() { return fid == 0; }                                     [4]
       void Clear() { fid = 0; }                                               [5]
       FID & operator =(const FID & rhs) { fid = rhs.fid; return *this;}       [6]
       bool operator==(const FID & op) const { return fid == op.fid; }         [7]
       bool operator!=(const FID & op) const { return fid != op.fid; }         [8]
       bool operator< (const FID & op) const { return fid <  op.fid; }         [9]
       bool operator> (const FID & op) const { return fid >  op.fid; }        [10]
    public: // constant
       static FID invalid;                                                    [11]
    protected:                                                      
       friend class FileSet;                                                  [12]
       void Next(){ fid++; }                                                  [13]
    protected:                                                                [14]
       UINT fid;                                                              [15]
};                                                                  

[1] This is a standalone class.  It is not derived from any other class, such as CObject.

[2] Only a limited number of operators are defined as public

[3] The default constructor sets the value of the instance to 0, an invalid value.  All valid values are > 0.  This predicate tests to see if the FID is valid, and returns TRUE if the FID is invalid.

[4] The predicate Invalid() tests to see if the FID is defined.  The programmer does not need to check if it is equal to 0

[5] The Clear method resets the FID to be invalid.

[6] A FID can be assigned to another FID

[7] Two FID values can be compared for equality, and their fid members are compared

[8] Two FID values can be compared for inequality.  Note that under C++ rules, != is not implicitly defined if == is defined.

[9] The < operator is required because FIDs are kept in std::map structures and these require a < operator be defined

[10] The > operator is required because the implementation of the Compare method of the map wanted to write a > b rather than the somewhat confusing b < a.

[11] This is a public, static constant that can be used when the programmer needs to return an "invalid" value.  The programmer can simply write return FID::invalid;

[12] Only a FileSet class is allowed to manipulate the "extended" features.  The FileSet class assignes new FIDs to FileSet values.

[13] The only extended feature is the ability to manipulate a FID value, so the master FID counter can be incremented.

[14] The representation of the FID is protected.

[15] The critical value, the integer which is the file ID, is protected and cannot be seen by the programmer.

Once a FID is established, all other operations are written in terms of FIDs.

Using std::map to hold patterns

I use several instances of maps in this application, but the key map type is a type called RuleMap

 typedef std::map<CString, CString, lessstr> RuleMap;

The lessstr function is defined as

class lessstr : public std::binary_function
   { /* less */
    public:
       bool operator()(LPCTSTR _Left, const LPCTSTR _Right) const{
          return _tcscmp(_Left, _Right) < 0;
       }
   }; /* less */

Note the above function is case-sensitive, so even though the various controls that display the strings are case-insensitive, we need to preserve case in the maps themselves.  If I wanted to make this more capable of localization, I would probably replace it with ::CompareString, which handles all kinds of locale-sensitive rules.

There are several instances of RuleMap.  In alphabetical order, they are

This is an example of how some of these are used:

void CIndexerDlg::CanonicalizeCase(CString & s, BOOL Secondary /* = FALSE */)
    {
     s.Trim();
     if(s.IsEmpty())
        return;

     //****************************************************************
     // Is it an untouchable key?
     //****************************************************************

     if(Unchanged.find(s) != Unchanged.end())
        return; // unchanged word

     //****************************************************************
     // See if it has a replacement rule
     //****************************************************************

     CString rule;
     RuleMap::iterator iter = Rules.find(s);

     if(iter != Rules.end())
        { /* found primary */
         rule = iter->second;
        } /* found primary */
     else
        { /* try secondary */
         iter = GeneratedRules.find(s);
         if(iter != GeneratedRules.end())
            { /* found secondary */
             rule = iter->second;
            } /* found secondary */
        } /* try secondary */

     if(!rule.IsEmpty())
        { /* apply rule */
         switch(RuleNameToID(rule))
            { /* rulename */
             //****************
             // *NORMAL
             //****************
             case RULES::NORMAL:
                   s.MakeLower();
                   Canonicalize(s);  // recursive
                   return;

There were two reasons to consider std::map instead of CMap.  The most important one was that the elements in std::map are an ordered set, and iterating through the map will deliver the objects according to the predicate used to compare them during insertion.   The other was less important, a desire to use a single representation for all such entities, rather than use std::map in one place and CMap in another, which would lead to confusion in development and maintenance.  I could have used std::hash in some cases, but decided that the nominal performance improvement would have to be demonstrated before I introduced a second representation.  To date, I have not measured the performance details.  Even for a large document, with some 18,000 lines in the database, it takes only about a minute-and-a-half to process on my machine, and that is comfortable enough to not justify the additional effort.

Using macros to simplify tedious programming

I strongly believe that when I find myself typing the same sequence of characters which differ only in some minor detail, it is time to either create a function or create a macro.  I prefer functions, but for some contexts, what I really want is a program-generation mechanism, and that's a macro.  Here are some common examples of where a macro makes more sense.

Declaring user-defined messages

I have a macro which I use to declare messages.  Over the years it has changed slightly in philosophy, and the latest version is used in this project

#pragma once
#define DECLARE_MESSAGE(x) static const UINT x = ::RegisterWindowMessage(_T(#x) _T("-{15F5D8DD-F4A1-4056-B279-0C701C640DCB}"));

I have a different copy of this header file in every project, and every project gets its own unique GUID assigned.  Note that because I declare this as a static variable, I can even put declarations of messages in header files, and each module that includes that header file will get a separate copy of the message.  I do not lump all my messages into a single header file in such a case, but have separate header files for related groups of messages. 

Enabling menu items

I could have written, for each menu item update

pPopupMenu->EnableMenuItem(ID_FILE_NEW, !GetBusy() ? MF_ENABLED : MF_GRAYED);

but that gets a bit tedious after a while.  So when there is a common pattern, a macro or two will improve things.

#define Enable(x) (MF_BYCOMMAND | ( (x) ? MF_ENABLED : MF_GRAYED))
#define EnableMenu(id, op) pPopupMenu->EnableMenuItem(id, Enable(op))
 /* New                    */ EnableMenu(ID_FILE_NEW,              !GetBusy()); 

Rather than duplicate the entire sequence twice in the same article. see the complete OnInitPopupMenu handler.

Initializing a table

One of the many disasters foisted on us is the notion of pre-populating combo boxes from the resources.  This is rarely interesting, because there is no way to attach semantic content to the entries.  You can't compare them to fixed strings to see what the user selected, because the fixed strings probably have to be hard-coded.  If they are not hard-coded, they have to be associated with STRINGTABLE entries which can also be localized.  Therefore, two places have an uncontrolled interdependence.  When there are multiple dependencies, it is best to create them so that inconsistencies result in compilation errors.  These are easier to deal with.

Many years ago I came up with my CIDCombo class, which is described in my essay on Initializing Combo Boxes.  I use this class all over the place, and nearly every project that has a combo box uses it, or a subclass of it. 

I also tend to use enum lists rather than long sequences of #define values, and I usually put the enum values inside a class so that I don't get name conflicts.  Thus, for the rules I describe, I have the following enumeration in my main dialog class:

class RULES {
    public:
        typedef enum {UNKNOWN, DIRECT, UNCHANGED, DROP_S, DROP_ES, DROP_D, DROP_ED, DROP_ING, 
                      DROP_LY, IES_TO_Y, ING_TO_E, TOLOWER, TOUPPER, NORMAL, HYPHEN, HEXWORD,
                      PERMUTE, PHRASE, 
       } RuleName;
};

Now, I want to associate these enum values with my ComboBox entries, so I would have to write

const IDData CIndexerDlg::RuleNames[] = {
    { IDS_RULE_DROP_S, CIndexerDlg::RULES::DROP_S },
    ...
    { 0, 0 } // EOT
}

which results in a lot of unnecessary typing.  So I defined a new macro

const IDData CIndexerDlg::RuleNames[] = {
#define RULE(x) { IDS_RULE_##x, CIndexerDlg::RULES::x }
   RULE(DROP_S),
   RULE(DROP_ES),
   RULE(DROP_D),
   RULE(DROP_ED),
   RULE(DROP_ING),
   ...
   {0, 0} // EOT
   };
#undef RULE

which is a lot easier to type.  Now if there is any inconsistency, I will most likely get a compilation error, either because I forgot to define the string name in the STRINGTABLE, or because I forgot to define the enumeration value.

Converting string rule names to enumeration types

This uses the same STRINGTABLE entries that were used to initialize the RuleNames table, above.

CIndexerDlg::RULES::RuleName CIndexerDlg::RuleNameToID(const CString & name)
    {
     if(name.IsEmpty())
        return RULES::UNKNOWN;
     CString rule;
#define CHECKRULE(x) { rule.LoadString(IDS_RULE_##x); if(name == rule) return RULES::x; }
     CHECKRULE(UNCHANGED);
     CHECKRULE(HYPHEN);
     CHECKRULE(DROP_S);
     CHECKRULE(DROP_ES);
     CHECKRULE(DROP_D);
     CHECKRULE(DROP_ED);
     CHECKRULE(DROP_ING);
     CHECKRULE(DROP_LY);
     CHECKRULE(IES_TO_Y);
     CHECKRULE(ING_TO_E);
     CHECKRULE(TOLOWER);
     CHECKRULE(TOUPPER);
     CHECKRULE(NORMAL);
     CHECKRULE(HEXWORD);
     CHECKRULE(PERMUTE);
     CHECKRULE(PHRASE);
#undef CHECKRULE
     if(name[0] == _T('*'))
        return RULES::UNKNOWN;
     return RULES::DIRECT;
    } // CIndexerDlg::RuleNameToID

Owner-draw CListBox and sorting control

Basic goals

The standard CListBox was inadequate for my purposes.  The key thing I had to do was have control over the collating sequence.  I needed case-sensitive insertion but case-insensitive comparison. In alphabetical sorting, we find digits sorting as

1
11
111
2
21
210
22
 

which is not what a user expects to see.  I wanted "words" like 3GB to sort as if they were the numerical value of 3 followed by the letters, so I would see

1K
3GB
4K
16K
32K
100MB

(No, I was not tempted to make 3GB sort in the order of 3-billion-and-something, although I could have...no, let's not go there...)

To do this, I had to use an owner-draw ListBox.  Once I had that, I could do even fancier things.

Validating the control styles

The key was to create a ListBox with owner-draw-fixed and without LBS_HASSTRINGS.  This means that the WM_COMPAREITEM message will be sent to do comparisons. To enforce this idea, I require that the styles be correct. I add this kind of code to a lot of my controls these days, because it reminds me to make sure I've set the correct styles.

/****************************************************************************
*                        CWordList::PreSubclassWindow
* Result: void
*       
* Effect: 
*       Checks for owner-draw attributes
****************************************************************************/

void CWordList::PreSubclassWindow()
    {
     DWORD style = GetStyle();
     ASSERT( (style & (LBS_OWNERDRAWFIXED | LBS_OWNERDRAWVARIABLE)) != 0 );
     ASSERT( (style & LBS_HASSTRINGS) == 0);

     CListBox::PreSubclassWindow();
    }

I also like the idea that the selection color is still shown when the control does not have focus, but in a different color.

Control has focus
Normal text (black font on white background)
Warning text (bold red font on white background)
Selected text (white font on blue background)
Control does not have focus
Normal text (black font on white background)
Warning text (bold red font on white background)
Selected text (white font on gray background)
Control has focus
Normal text (black font on white background)
Warning text (bold red font on white background)
Warning text selected (bold white font on blue background)
Control is disabled
Normal text (gray text on white background
Warning text (bold gray text on white background)
Selected text (white font on gray background)

Note also that I show the number of occurrences of the string inside the document.  Any amount over the "too many" threshold (set to 10 in these screen shots) will show up highlighted, and I chose bold red text for this purpose.  This helps me find words that might not be productive in an index.

Drawing the control

/****************************************************************************
*                             CWordList::DrawItem
* Inputs:
*       LPDRAWITEMSTRUCT dis
* Result: void
*       
* Effect: 
*       Draws the text
****************************************************************************/

void CWordList::DrawItem(LPDRAWITEMSTRUCT dis)
    {
     CDC * dc = CDC::FromHandle(dis->hDC);                   // [1]
     CRect r = dis->rcItem;                                  // [2]

     COLORREF txcolor;                                       // [3]
     COLORREF bkcolor;                                       // [1]

     if(dis->itemID == -1)                                   // [5]
        { /* nothing in list */
         dc->DrawFocusRect(&dis->rcItem);                    // [6]
         return;
        } /* nothing in list */

     int save = dc->SaveDC();                                // [7]

     IndexInfo * info = (IndexInfo *)dis->itemData;          // [8]
     BOOL overused = FALSE;                                  // [9]

     if(info != NULL)                                        // [10]
        overused = info->GetItemCount() >= OverUseThreshold; // [11]

     if(dis->itemState & ODS_SELECTED)                       // [12]
        { /* selected */
         if(::GetFocus() != m_hWnd)                          // [13]
            { /* doesn't have focus */
             bkcolor = ::GetSysColor(COLOR_GRAYTEXT);        // [14]
             txcolor = ::GetSysColor(COLOR_HIGHLIGHTTEXT);
            } /* doesn't have focus */
         else
            { /* has focus */
             bkcolor = ::GetSysColor(COLOR_HIGHLIGHT);       // [15]
             txcolor = ::GetSysColor(COLOR_HIGHLIGHTTEXT);
            } /* has focus */
        } /* selected */
     else
        { /* unselected */
         if(dis->itemState & (ODS_DISABLED | ODS_GRAYED))    // [16]
            { /* disabled */
             txcolor = ::GetSysColor(COLOR_GRAYTEXT);        // [17]
             bkcolor = ::GetSysColor(COLOR_WINDOW);
            } /* disabled */
         else
            { /* enabled */
             txcolor = overused ? RGB(255, 0, 0) : ::GetSysColor(COLOR_WINDOWTEXT);  // [18]
             bkcolor = ::GetSysColor(COLOR_WINDOW);
            } /* enabled */
        } /* unselected */

     if(overused)                                            // [19]
        { /* font change */
         if(BoldFont.GetSafeHandle() == NULL)                // [20]
            { /* create bold font */
             CFont * f = GetFont();                          // [21]
             LOGFONT lf;                                   
             f->GetLogFont(&lf);                             // [22]
             lf.lfWeight = FW_BOLD;                          // [23]
             BoldFont.CreateFontIndirect(&lf);               // [24]
            } /* create bold font */
         dc->SelectObject(&BoldFont);                        // [25]
        } /* font change */

     dc->SetBkColor(bkcolor);                                // [26]
     dc->SetTextColor(txcolor);

     dc->FillSolidRect(&r, bkcolor);                         // [27]

     int x = dis->rcItem.left + ::GetSystemMetrics(SM_CXBORDER) * 2;  // [28]

     CString s;
     if(info != NULL)
        s = info->text;

     int n = (int)info->GetItemCount();                      // [29]
     if(n > 0)
        s += ToString(_T(" (%d)"), n);                       // [30]
         
     dc->TextOut(x, dis->rcItem.top, s);                     // [31]

     if(dis->itemState & ODS_FOCUS && dis->itemAction != ODA_SELECT)
        dc->DrawFocusRect(&r);                               // [32]

     dc->RestoreDC(save);                                    // [33]
    }
  1. ñ Create a CDC * to represent the DC.  This means I don't have to use raw GDI calls in terms of an HDC
  2. ñ I make a copy of the rectangle in case I need to modify it
  3. ñ The txcolor is the text color.  It will be filled in by later code
  4. ñ The bkcolor is the background color.  It will be filled in by later code
  5. ñ If the itemID is -1, it means there is no valid index to the control, because there are no elements in the control. 
  6. ñ So just draw the focus rectangle and return
  7. ñ There will be changes made in the DC.  Rather than keep tons of silly variables to hold old-this and old-that, just save the entire DC state using SaveDC and restore it before leaving.  Note that this DC might come back again for another item, so it must be left unaltered by this call.
  8. ñ The IndexInfo structure is going to hold everything necessary to draw this item.  Retrieve the IndexInfo associated with this item.
  9. ñ The overused variable will tell us if we have "too many" occurrences of this word
  10. ñ Just to make sure, we don't try to access a NULL pointer
  11. ñ The info->GetItemCount() (a method of the IndexInfo structure) tells how many occurrences of the term appear in the presentation.  This is compared to the OverUseThreshold member variable, which has been set externally by the parent.
  12. ñ There are two cases to consider: the item is selected, and the item is not selected.  We will choose different colors depending on the selected/not selected state
  13. ñ Check to see if we have the focus.  I could also have written "if(GetFocus() == this)"
  14. ñ If another window has the focus, choose the "highlighted-but-not-focus" color scheme
  15. ñ If this window has the focus, choose the "highlighted-with-focus" color scheme
  16. ñ If the item is not selected, but this window is disabled...
  17. ñ ...choose the "disabled" color scheme (gray text on a white background)
  18. ñ Choose the "not-selected" color scheme.  The text color will be red if we have an "overused" item and the normal text color if it is not overused
  19. ñ If we have the "overused" state, set the font to be a bold font
  20. ñ If the font has not been created already, we will create it at this point (we can't create it in the constructor, because the window does not exist at that point.  Furthermore, if we created it in PreSubclassWindow, the client programmer might change it to some other font, and we want to respond properly to the actual font in use.  (This control does not recognize fonts that are changed after data has been loaded, but we could respond to WM_SETFONT messages and delete the font object)
  21. ñ To create the font, first get the current font
  22. ñ Use GetLogFont to get the parameters of the font
  23. ñ Set the font weight to be "bold"
  24. ñ Create a font which is otherwise identical to the original font, but is bold
  25. ñ Either use the existing font, or the newly-created font, and select it into the DC.  Note that the old font that was in the DC is not saved--I don't need to, because RestoreDC will reset it
  26. ñ Set the background and text colors.  Again note that the old values are not saved
  27. ñ Paint the background color for the entire item
  28. ñ If you don't do a bit of offset, the words are crowded to the left of the control.  I prefer to do small spacings in terms of multiples of SM_CXBORDER or SM_CXEDGE, which makes the distances display-independent and will work well on high-resolution displays.
  29. ñ Retrieve the actual item count
  30. ñ Format it in parentheses and concatenate them to the string to be displayed.  (The ToString function avoids the need to create a temporary variable whose sole purpose is to be a formatting target; see my essay on ToString)
  31. ñ Write out the text.  Note that the background has been painted, the text and background colors have been selected, and the correct font has been selected
  32. ñ If the item is selected, draw the focus rectangle
  33. ñ Restore the DC to its original state, undoing all the changes that have been made.  The DC is now in the original state it was in when the call first occurred.

Handling sorting

A ListBox normally does a case-independent comparison.  But we need a more sophisticated sort.  The sorting order that is used for the words in this list is the sorting order for the index, so it must be case-independent but words in different cases are not considered identical.  This makes lookup complicated, but that will be discussed later.

The basic notion is that if we are comparing two elements that start with digits, we actually compare the integer values first and do an alphabetic sub-sort of the rest of the string.

/****************************************************************************
*                           CWordList::CompareItem
* Inputs:
*       LPCOMPAREITEMSTRUCT cis:
* Result: int
*       -1 if item1 < item2
*       0  if item1 == item2
*       1  if item1 > item2
* Notes: 
*       Because we do not allow FindString/FindStringExact to do searches,
*       we are keeping case-sensitive data in the table and using the
*       auxiliary map for lookups.  But we want it sorted in merged
*       alphabetical order, so _tcsicmp is used for case-independent
*       comparison
*       Handle elements with leading numeric fields
****************************************************************************/

int CWordList::CompareItem(LPCOMPAREITEMSTRUCT cis)         // [1]
    {
     if(cis->itemData1 == 0 || cis->itemData2 == 0)         // [2]
        return 0;                                           // [3]
     LPCTSTR s1;
     if(cis->itemID1 == (DWORD)-1)                          // [4]
        s1 = (LPCTSTR)cis->itemData1;                       // [5]
     else
        { /* get from info */
         IndexInfo * info1 = (IndexInfo *)cis->itemData1;   // [6]
         s1 = (LPCTSTR)info1->text;                         // [7]
        } /* get from info */

     LPCTSTR s2;
     IndexInfo * info2 = (IndexInfo *)cis->itemData2;       // [8]
     s2 = (LPCTSTR)info2->text;                             // [9]

     int result;
     if(_istdigit(s1[0]) && _istdigit(s2[0]))               // [10]
        { /* numeric sort */
         LPTSTR b1;                                         // [11]
         LPTSTR b2;
         DWORD n1 = _tcstoul(s1, &b1, 10);                  // [12]
         DWORD n2 = _tcstoul(s2, &b2, 10);                  // [13]
         if(n1 < n2)                                        // [14]
            return -1;                                      // [15]
         if(n1 > n2)                                        // [16]
            return 1;                                       // [17]
         result = _tcsicmp(b1, b2);                         // [18]
        } /* numeric sort */
     else
     if(!_istalpha(s1[0]) || !_istalpha(s2[0]))             // [19]
        { /* non-alpha characters */
         CString t1;                                        // [20]
         if(!_istalpha(s1[0]))
            t1 = _T(" ");                                   // [21]
         t1 += s1;                                          // [22]
         CString t2;                                        // [23]
         if(!_istalpha(s2[0]))
            t2 = _T(" ");
         t2 += s2;
         result = _tcsicmp(t1, t2);                         // [24]
        } /* non-alpha characters */
     else
        { /* both alpha */
         result = _tcsicmp(s1, s2);                         // [25]
        } /* both alpha */
     if(result < 0)
        return -1;                                          // [26]
     if(result > 0)
        return 1;                                           // [27]
     return 0;                                              // [28]
    }
  1. ñ First, I replace that absurd parameter name with something that a sensible programmer might use, something that can be typed easily
  2. ñ If either itemData is 0 (itemData is a UINT_PTR, and is therefore an integer value) we cannot do comparison...
  3. ñ ...so pretend they are equal
  4. ñ When a string is being inserted, the itemID1 will be -1 for the input string (the itemID2 will be a pointer to the element in the control)
  5. ñ ...so if the itemID is -1, the itemData1 will be an LPCSTR pointer
  6. ñ If the itemID is not -1, the itemData1 will be an IndexInfo * pointer, so retrieve that element...
  7. ñ ...and extract its string
  8. ñ Whether this is an insertion operation or a straight comparison operation, itemData2 is an IndexInfo * pointer...
  9. ñ ...so extract its string
  10. ñ If both elements start with a digit, we will use the special integer-hybrid comparison
  11. ñ When _tcstoul executes, it will store the address of the non-digit that stopped the scan; I use these variables to hold that position for each of the strings
  12. ñ Convert the leading digit of s1 to a number n1 and save a pointer to the rest in b1
  13. ñ Convert the leading digit of s2 to a number n2 and save a pointer to the rest in b2
  14. ñ If n1 < n2 then it is obvious that element 1 is less than element 2...
  15. ñ  ...so return -1.  Note that some compare functions will accept any negative number, while others insist on -1, and rather than try to keep straight which is which, I just always return -1 in such a case
  16. ñ If n1 > n2 then it is obvious that element 1 is greater than element 2...
  17. ñ ...so return 1.  See the note for line 15.
  18. ñ If the two digits are equal, compute the result of doing a case-independent comparison of the non-digit parts of the string (either or both may be empty)
  19. ñ If we get this far, we do not have a numeric-to-numeric comparison situation.  However, I want all the special characters to sort ahead of all the letters.  Because all strings have Trim applied to them, no entry can legitimately start with a space.  So by introducing a gratuitous space ahead of special-character elements, but only for the string comparison, all special characters will sort ahead of all letters
  20. ñ The string t1 will be used for the comparison string
  21. ñ If the first character is not alphabetic, t1 gets an initial space
  22. ñ t1 has the input string appended to it.  If the first character is alphabetic, t1 is empty, otherwise it is the leading space
  23. ñ Repeat this algorithm for s2
  24. ñ Do a case-independent comparison of the constructed strings and return the result
  25. ñ If it is not numeric-to-numeric or special-character, a straight case-independent comparison works fine
  26. ñ If the value was negative, return -1 (see the note on 15)
  27. ñ If the value was positive, return 1 (see the note on 15)
  28. ñ Otherwise, the two strings are equal, return 0

Membership testing

A bit of experimentation with earlier versions showed that the cost of the lookup for an input key by doing an item-by-item scan of the ListBox contents was unacceptable.  For a large presentation, I might start with 6000 elements in the control, and linear search (which in some cases I was doing during keystroke-by-keystroke update) was so slow as to be completely unacceptable.  So I decided to maintain an internal cache for fast lookup.  The difference between doing this internally and requiring the user of the control to do it is substantial.  By making the cache part of the control, all consistency issues are handled within the control.  There's even a fallback: I have the option of forcing the cache to be automatically re-created by the control. 

To get performance, I chose to use std::map (my other choice was std::hash, but I went with std::map because I'm using it elsewhere and was therefore comfortable with it).

An attempt to add an element that already exists in the control will be ignored. 

For example, here are two functions, one to add an element and one to delete an element

/****************************************************************************
*                            CWordList::AddElement
* Inputs:
*       const CString & key: Key word to add
*       const CString & org: Original line, may be empty string
*       int slide = 0: Slide value
*       int line  = 0: Line in entry
* Result: void
*       
* Effect: 
*       Adds the element
****************************************************************************/

void CWordList::AddElement(const CString & key, const CString & org, int slide /* = 0 */, int line /* = 0 */)
    {
     BuildCache();                                                  // [1]

     CacheMap::iterator iter;                                       // [2]
     iter = cache.find(key);                                        // [3]
     if(iter == cache.end())                                        // [4]
        { /* not already in list */
         IndexInfo * info = CreateInfo(key, org, slide, line);      // [5]
         cache.insert(CacheElement(key, info));                     // [6]
         CListBox::AddString((LPCTSTR)info);                        // [7]
        } /* not already in list */
     else
        { /* already in list */
         if(slide != 0)                                             // [8]
            { /* add slide info */
             iter->second->AddSlide(slide, line);                   // [9]
            } /* add slide info */
        } /* already in list */
    } // CWordList::AddElement
  1. This will build the cache if it is not already valid; if it is valid, this call does nothing
  2. Create an iterator variable
  3. Look up the key in the cache (very efficient compared to linear search of the control's GetItemData retrieval)
  4. If the key is not already in the list...
  5. ...create a new IndexInfo element to represent it
  6. ...add the IndexInfo to the cache
  7. ...add the IndexInfo to the control
  8. If the element already exists, and it is associated with a slide...
  9. ...add the slide number to the existing element

The AddSlide method creates only one entry per slide, so duplicate-detection is handled inside this function

/****************************************************************************
*                          CWordList::DeleteElement
* Inputs:
*       int n: Element index to delete from list
* Result: void
*       
* Effect: 
*       Deletes the element from the cache and from the listbox, but does
*       not destroy the IndexInfo associated with it
****************************************************************************/

void CWordList::DeleteElement(int n)
    {
     IndexInfo * info = (IndexInfo *)GetItemDataPtr(n);   // [1]
     if(info != NULL)
        cache.erase(info->text);                          // [2]  
     CListBox::SetItemDataPtr(n, NULL);                   // [3]
     CListBox::DeleteString(n);                           // [4]
    } // CWordList::DeleteElement
  1. Retrieve the IndexInfo object associated with the element
  2. If this is non-NULL, remove it from the cache
  3. It is important to set this to NULL so the IndexInfo object is not destroyed when the element is removed (this is a non-destructive deletion)
  4. Remove the element from the control

There is a separate DestroyElement call which will destroy the IndexInfo object.  But when an element is transferred from one control to another, its IndexInfo is transferred to the target control, and if it were destroyed when the previous element is deleted this would leave a pointer to hyperspace in the target control.

Fast Lookup

Another performance problem I had was going from the A-Z buttons to the first item that started with the letter.  I wrote a slight variation of bsearch to accomplish this, particularly because there may be no actual instance of the letter in the list.  I chose in the case where there is no letter to simply do nothing (no change is made to the position).  This was done because I decided it was too complex and time-consuming to update the button enablement for the A-Z buttons each time there was a change in the contents of the control

Owner-draw CListCtrl

The CListCtrlEx class is one I defined to make a lot of CListCtrl manipulations easier; however, from the viewpoint of this discussion, it is essentially equivalent to CListCtrl.

All of the information is kept in the LPARAM field of the control.  The data structure I use is the one below.  For performance reasons, I chose to not make a string copy (it had a noticeable effect on a 10,000 line presentation!), but take the CString * which was sent from the reader thread and store the pointer directly.  This requires that the destructor delete the string.  The BOOL active tracks whether or not the selection is active.

class SlideData {
    public:
       SlideData(int sn, CString * L) { slide = sn; line = L; active = FALSE; }
       ~SlideData() { delete line; }
    public:
       CString * line;
       int slide;
       BOOL active;
}; 
  • A line which contains the keyword is highlighted
  • A line which contains the keyword and is selected is highlighted in a different color
  • The keyword appears in boldface in the line for each occurrence
  • A line which is selected but does not contain the keyword gets a full-width highlight (not just the first cell).  LVS_EX_FULLROWSELECT does not seem to work well as a window style
  • A line which has no keywords and is not selected gets the ordinary text/background color

Most of these are illustrated on the left.  The selected word is operation.  The lighter form of highlighting applies to all lines that contain the word (active is TRUE).  As the user steps through the sequence of words using the Slide Locator, the currently-selected line is shown in a darker color (active is TRUE and the ODS_SELECTED flag is set).

 

class CIndexData : public CListCtrlEx
{
        DECLARE_DYNAMIC(CIndexData)

public:
        CIndexData();
        virtual ~CIndexData();
        void SetKeyword(const CString & s) { keyword = s; keyword.MakeUpper(); }  // [1]
        void MarkActive(int n, BOOL active = TRUE);                               // [2]
        BOOL GetActive(int i);                                                    // [3]
        CString GetSlideText(int i);
        int GetSlideNumber(int i);
public:
        CString GetItemText(int n, int subitem) { ASSERT(FALSE); } // detect erroneous usage // [4]
protected:
        DECLARE_MESSAGE_MAP()
        afx_msg void OnLvnDeleteitem(NMHDR *pNMHDR, LRESULT *pResult);
        virtual void DrawItem(LPDRAWITEMSTRUCT /*lpDrawItemStruct*/);
protected:
   CString keyword;
   CFont boldfont;
}; 
  1. This sets the keyword that will be emphasized in a line.  To simplify algorithms, it is stored in all-uppercase
  2. This sets the active flag for element n
  3. This retrieves the active flag for element n
  4. To detect if the programmer has erroneously used the CListCtrl::GetItemText (instead of the appropriate GetSlideText) this method is used to hide the superclass method.  Since most of the important methods are not virtual, this is the best we can do.
/****************************************************************************
*                            CIndexData::DrawItem
* Inputs:
*       LPDRAWITEMSTRUCT dis: 
* Result: void
*       
* Effect: 
*       Draws the elements
****************************************************************************/

void CIndexData::DrawItem(LPDRAWITEMSTRUCT dis)
    {
     CRect r;
     CDC * dc = CDC::FromHandle(dis->hDC);                                   // [1]
     int save = dc->SaveDC();                                                // [2]

     LVCOLUMN colinfo = {0};                                                 // [3]
     colinfo.mask = LVCF_FMT | LVCF_WIDTH;                                   // [4]
     COLORREF fill;                                                          // [5]
     COLORREF text;                                                          // [6]
 
     int gap = 2 * ::GetSystemMetrics(SM_CXBORDER);                          // [7]
     int offset = -GetScrollPos(SB_HORZ);                                    // [8]

     SlideData * data = (SlideData *)GetItemData(dis->itemID);               // [9]

     if(boldfont.GetSafeHandle() == NULL)                                    // [10]
        { /* create bold font */
         CFont * f = GetFont();                                              // [11]
         LOGFONT lf;
         f->GetLogFont(&lf);                                                 // [12]
         lf.lfWeight = FW_BOLD;                                              // [13]
         boldfont.CreateFontIndirect(&lf);                                   // [14]
        } /* create bold font */
     //----------------------------------------------------------------      // [15]
     // Compute the colors for fill and text
     //----------------------------------------------------------------
     // Criteria         
     //         Selection       Active  Focus        fill            text
     //----------------------------------------------------------------------------------
     //         Selected        N       Yes          COLOR_HIGHLIGHT COLOR_HIGHLIGHTTEXT
     //         Selected        Y       Yes          SELECTED_BKGND  SELECTED_TEXT
     //         Selected        Y       No           SELECTED_BKGND  SELECTED_TEXT
     //         Selected        N       No           COLOR_GRAYTEXT  COLOR_WINDOWTEXT
     //         Unselected      N       n/a          COLOR_WINDOW    COLOR_WINDOWTEXT
     //         Unselected      Y       n/a          UNSELECTED_BKGND UNSELECTED_TEXT

#define SELECTED_BKGND   RGB(255, 125, 255)                                   // [16]
#define UNSELECTED_BKGND RGB(255, 204, 255)
#define SELECTED_TEXT    ::GetSysColor(COLOR_WINDOWTEXT)
#define UNSELECTED_TEXT  ::GetSysColor(COLOR_WINDOWTEXT)

     if(dis->itemState & ODS_SELECTED)                                        // [17]
        { /* selected */
         if(data->active)
            { /* of interest */
             fill = SELECTED_BKGND;
             text = SELECTED_TEXT;
            } /* of interest */
         else
            { /* not of interest */
             if(::GetFocus() == m_hWnd)
                { /* has focus */
                 fill = ::GetSysColor(COLOR_HIGHLIGHT);
                 text = ::GetSysColor(COLOR_HIGHLIGHTTEXT);
                } /* has focus */
             else
                { /* no focus */
                 fill = ::GetSysColor(COLOR_GRAYTEXT);
                 text = ::GetSysColor(COLOR_WINDOWTEXT);
                } /* no focus, no interest */
            } /* not of interest */
        } /* selected */
     else
        { /* not selected */
         if(data->active)
            { /* of interest */
             // fill = ::GetSysColor(COLOR_INFOBK);
             fill = UNSELECTED_BKGND;
             text = UNSELECTED_TEXT;
            } /* of interest */
         else
             { /* not of interest */
              fill = ::GetSysColor(COLOR_WINDOW);
              text = ::GetSysColor(COLOR_WINDOWTEXT);
             } /* not of interest */
        } /* not selected */

     for(int col = 0; GetColumn(col, &colinfo); col++)                        // [18]
        { /* scan columns */
         //----------------------------------------------------------------
         // Compute the rectangle
         //----------------------------------------------------------------
         GetSubItemRectEx(dis->itemID, col, LVIR_BOUNDS, r);                  // [19]
         int pos = r.left;                                                    // [20]

         dc->SetTextColor(text);                                              // [21]

         //----------------------------------------------------------------
         // Fill the background. Note the adjustment down and the expansion
         // This looks better
         //----------------------------------------------------------------
         CRect filler = r;                                                    // [22]                        
         filler.bottom += 2;                                                  // [23]
         dc->FillSolidRect(&filler, fill);                                    // [24]

         //----------------------------------------------------------------
         // Compute the correct width of the column
         //----------------------------------------------------------------
         int width = r.Width();                                               // [25]

         //----------------------------------------------------------------
         // Now compute the alignment. Note that if there is 
         // horizontal scrolling, we have to adjust for the scrolling
         // position
         //----------------------------------------------------------------

         int x;                                                               // [26]

         //----------------------------------------------------------------
         // Set up to honor the formatting alignment
         //----------------------------------------------------------------
         switch(colinfo.fmt & LVCFMT_JUSTIFYMASK)                             // [27]
            { /* fmt */
             case LVCFMT_CENTER:
                x = dis->rcItem.left + pos + width / 2 - offset;              // [28]
                dc->SetTextAlign(TA_CENTER);                                  // [29]
                break;
             case LVCFMT_RIGHT:
                x = dis->rcItem.left + pos + width - offset - gap;            // [30] 
                dc->SetTextAlign(TA_RIGHT);                                   // [31]
                break;
             case LVCFMT_LEFT:
             default:
                x = dis->rcItem.left + pos - offset;                          // [32]
                dc->SetTextAlign(TA_LEFT);                                    // [33]
                break;
            } /* fmt */

         //----------------------------------------------------------------
         // Set a clipping region so we don't paint outside the lines...
         // Note that we have to add +1 to deal with the fact that all
         // drawing is up-to-but-not-including the endpoint, and we
         // want to include the endpoint
         //----------------------------------------------------------------

         CRect clip(r.left, r.top, r.right - gap, r.bottom + 1);              // [34]

         CRgn cliprgn;                                                        // [35]
         cliprgn.CreateRectRgn(clip.left, clip.top, clip.right, clip.bottom); // [36]
         { /* draw clipped */
          int save2 = dc->SaveDC();                                           // [37]
          dc->SelectClipRgn(&cliprgn);                                        // [38]

          switch(col)
             { /* columns */
              case INDEX_SLIDE:
                 dc->TextOut(x, dis->rcItem.top - 1, ToString(_T("%d"), data->slide)); // [39]
                 break;
              case INDEX_TEXT:
                 if(data->active && !keyword.IsEmpty() && col == INDEX_TEXT)  // [40]
                    { /* active object */
                     //  xxxxxx keyword xxxxx keyword xxxxx
                     CString original = *data->line;                          // [41]
                     CString copy = original;                                 // [42]
                     copy.MakeUpper(); // canonicalize the copy               // [43]
                     while(!original.IsEmpty())                               // [44]
                        { /* draw part */
                         int n = copy.Find(keyword);                          // [45]
                         if(n < 0)                                            // [46]
                            { /* output rest */
                             dc->TextOut(x, dis->rcItem.top - 1, original);   // [47]
                             break;                                           // [48]
                            } /* output rest */
                         // output partial string, then keyword
                         CString prefix = original.Left(n);                   // [49]
                         dc->TextOut(x, dis->rcItem.top - 1, prefix);         // [50]
                         x += dc->GetTextExtent(prefix).cx;                   // [51]
                         original = original.Mid(n);                          // [52]
                         copy = copy.Mid(n);                                  // [53]
                         int save3 = dc->SaveDC();                            // [54]
                         dc->SelectObject(&boldfont);                         // [55]
                         CString key = original.Left(keyword.GetLength());    // [56]
                         dc->TextOut(x, dis->rcItem.top - 1, key);            // [57]
                         x += dc->GetTextExtent(key).cx;                      // [58]
                         dc->RestoreDC(save3);                                // [59]

                         original = original.Mid(key.GetLength());            // [60]
                         copy = copy.Mid(key.GetLength());                    // [61]
                        } /* draw part */
                    } /* active object */
                 else
                    { /* inactive object */
                     dc->TextOut(x, dis->rcItem.top - 1, *data->line);        // [62]
                    } /* inactive object */
                 break;
             } /* columns */

          dc->RestoreDC(save2);                                               // [63]
         } /* draw clipped */                                                 // [64]
        } /* scan columns */

     dc->RestoreDC(save);                                                     // [65]
    }
  1. ñ Create a CDC * from the HDC supplied
  2. ñ Save its state so we can restore it to a clean point, without requiring an assortment of random old-this and old-that variables to hold the previous state
  3. ñ Declare an LVCOLUMN structure and initialize it to zeroes
  4. ñ Mark it to retrieve the format flags and the column width (in pixels)
  5. ñ Declare a variable to hold the background color.  This will be filled in based on the color selection algorithm
  6. ñ Declare a variable to hold the text color.  This will be filled in based on the color selection algorithm
  7. ñ To keep the text from being jammed up to the left of the column, choose a gap size.  To make this portable for various resolutions, I base it on the SM_CXBORDER size
  8. ñ This offset is required to make sure that the display is correctly maintained in the presence of horizontal scrolling (this is a little-known factoid)
  9. ñ Retrieve the SlideData structure for the item we are drawing
  10. ñ Create the bold font.  This cannot be done in the constructor because the window does not exist.  It should not be done in PreSubclassWindow because the programmer may wish to change the font in the OnInitDialog handler.
  11. ñ Retrieve the current font
  12. ñ Retrieve the font parameters of the current font
  13. ñ Set the font weight to "bold"
  14. ñ Create the new font, which is identical to the current font except for the weight
  15. ñ This is a style I use frequently.  When there is a complex decision process, I write the conditions and outcome as a table.  Then it is easy to see if the code matches the specifications
  16. ñ Declare names for the colors.  Note that some of these are hardwired and some are based on the the user's selected color scheme.  I could have chosen to make these variables in the class, initialized them in the constructor, and provided interfaces to allow the programmer to change them, but I decided that this was sufficient for the current problem
  17. ñ Given the table [15], this code should now be obvious, and will not be explained in detail
  18. ñ Iterate across all the columns and display the contents appropriate
  19. ñ Retrieve the nominal rectangle for this element.  Note that this rectangle is a "virtual rectangle" and is not offset by any horizontal scrolling.  In addition, there is an undocumented feature: if you ask for the rectangle of column 0, it returns the entire rectangle, the same as GetItemRect.  My CListCtrlEx class has this GetSubItemRectEx method that handles this anomaly.
  20. ñ Extract the nominal starting position for the text output
  21. ñ Select the desired text color
  22. ñ Set the highlight rectangle size to the nominal rectangle
  23. ñ Add +1 because the Windows operations go from the left to the right-1 and from the top to the bottom-1.  Add another +1 because it has been empirically determined that the rectangle is one pixel too small (this is not documented anywhere)
  24. ñ Fill the background rectangle for this subitem
  25. ñ Get the nominal width of the rectangle
  26. ñ This will hold the client-relative x-coordinate at which the column starts
  27. ñ Choose the type of text alignment that will be used relative to the nominal x-position
  28. ñ Set the coordinate to be the nominal center of the column, adjusted for horizontal scrolling
  29. ñ Set the text alignment to draw the text centered on the nominal x-coordinate
  30. ñ Set the coordinate to the nominal right side of the column, up to but not including the right edge of the column (the gap makes sure it doesn't crowd the next column over), adjusted for the horizontal scrolling position
  31. ñ Set the text alignment to draw the text right justified
  32. ñ Set the coordinate to the nominal left side of the column, adjusted for the horizontal scrolling position
  33. ñ Set the text alignment to draw the text left justified
  34. ñ We will need to create a clipping region.  If the text that will be drawn exceeds the column size, it will "spill over" into the adjacent column.  This ensures that even when the text is too wide for the column, it will be truncated rather than spill over.  This rectangle defines the limit of the clipping region
  35. ñ Declare a CRgn variable to hold the region
  36. ñ Create the clipping region
  37. ñ Save the current DC state.  This is so that we can restore it to remove the clipping region, so when the CRgn::~CRgn destructor is called the region is not actively selected into a DC and will be properly deleted
  38. ñ Select the clipping region into the DC
  39. ñ If we are drawing the slide number column, format the slide number. Rather than create a temporary variable to hold the string, I use my ToString function.
  40. ñ If this is an active element (determined by the SetActive call having been done with a TRUE parameter), and there is a keyword set, we will apply the more complicated display algorithm; otherwise we will simply go to step 62
  41. ñ Make a copy of the line.  We will use two copies of the text line: one which is the original text, and one of which is the capitalized text.  This allows us to do a case-independent find in the string.  We will search the all-caps string for a hit, but extract and print the text from the original string
  42. ñ This creates the copy
  43. ñ Convert the copy to all-uppercase
  44. ñ Loop as long as there is any content remaining in the string
  45. ñ Find a copy of the key (which is in all uppercase) in the copy string (which is in all uppercase)
  46. ñ See if there is any instance of the key found in the copy
  47. ñ There is no instance.  Output the remainder of the string (the original string)
  48. ñ We are done. Exit the loop
  49. ñ The string has an instance of the key.  We will parse this as <prefix><keyword><suffix>.  We extract the prefix part.  Note that the prefix may actually be empty, but that doesn't matter.  Although we searched the copy string, we extract the prefix from the original string, so it is displayed in the original case
  50. ñ Output the prefix text at the current x-coordinate
  51. ñ Increase the x coordinate by the width of the text that has been just output
  52. ñ Remove the prefix from the original string
  53. ñ Remove the prefix from the uppercase copy
  54. ñ Save the DC context because we want to make a temporary change in the font
  55. ñ Select the bold font into the DC so the keyword will display in bold
  56. ñ Extract the keyword from the original string
  57. ñ Display the keyword (now using boldface) at the nominal x-coordinate
  58. ñ Increment the x-coordinate by the width of the keyword displayed
  59. ñ Restore the DC to the original font
  60. ñ Remove the keyword from the original string
  61. ñ Remove the keyword from the uppercase copy
  62. ñ Simple output.  There is nothing to do but write the text out
  63. ñ Restore the DC from the drawing state.  This removes the clipping region that had been set.
  64. ñ End the scope in which the CRgn was declared.  This will ultimately call ::DestroyObject to destroy the clipping region.  Because of the RestoreDC, the region is not actively selected into a DC, and the ::DestroyObject will successfully release it.
  65. ñ Restore the DC to its original input state

Checking file timestamp consistency

Well, I managed to do it.  I made some manual edits to the file, then proceeded to overwrite them by saving the contents of memory back to the file.  So I decided to add a timestamp checker.

To do this, I created a class that held file state.  It holds the file name, modified flag, and time stamps, plus methods to work with them.

The methods that are defined for the FileState class are

The idea is to check the time stamps whenever focus switches back to the application.  To do this, I use OnActivateApp to detect when activate has returned, and check the timestamps at that point.

However, when I first tried this, I got an interesting effect.  When I got an error message, I switched away, and when I switched back, I got another Message Box.  The OnActivateApp handler had to have another feature added.

/****************************************************************************
*                         CIndexerDlg::OnActivateApp
* Inputs:
*       BOOL bActive: TRUE if being activated, FALSE if being deactivated
*       DWORD dwThreadID: ignored
* Result: void
*       
* Effect: 
*       If being activated, checks for file timestamp consistency
****************************************************************************/

void CIndexerDlg::OnActivateApp(BOOL bActive, DWORD dwThreadID)
    {
     CDialog::OnActivateApp(bActive, dwThreadID);

     if(bActive)                                                 // [1]
        { /* check timestamps */
         // Note: do not check timestamps if there is any active popup
         if(GetLastActivePopup() == this)                        // [2]
            CheckFileTimeStamps();                               // [3]
        } /* check timestamps */
    }
  1. Only check the time stamps on activation
  2. CWnd::GetLastActivePopup when called from a class will return either a pointer to itself if there is no active popup, or a CWnd * of the current active popup.
  3. CIndexerDlg::CheckFileTimeStamps checks the file timestamps and pops up a MessageBox to indicate the nature of the error
This is the result of changing the file outside the PowerPoint Indexer.

Key ideas here: the filename is displayed.  The two timestamps are displayed.  The question is unambiguous.

Note that the type of this MessageBox is MB_ICONQUESTION and the default button is Yes.

This is the result of changing the file outside the PowerPoint Indexer.  However, in this case, reading in the file will replace the contents of the control, which would cause information to be lost.

Besides the key ideas above, this states, in large friendly letters, that reading this in will destroy information.  The question is unambiguous.

Note that the type of this MessageBox is MB_ICONERROR and the default button is No.

The result of changing the file outside the PowerPoint Indexer, and after having responded "No" to the request to read it, now attempting to save the changes onto the file that has been edited.

Besides the key ideas, this states, in large friendly letters, that writing this data out will destroy information.  The question is unambiguous.

Like the other information-losing error, above, the type is MB_ICONERROR and the default button is No.

The FileState::Compare method is a bit funky, and some of this is due to a very early design error at Microsoft: because they could not get a native 64-bit integer into the C language (the compiler group had its own agenda unrelated to the company making progress), a huge number of 64-bit values are represented as a pair of DWORD values.  Nearly 20 years after the Windows NT project began, we are still being forced to live with this mistake.  The FILETIME value is non exception to this blunder, being defined as

typedef struct _FILETIME
    {
     DWORD dwLowDateTime;
     DWORD dwHighDateTime;
    } 	FILETIME;

Therefore, to do the comparison, it is necessary to convert the FILETIME to a 64-bit integer.  This is the Microsoft-recommended Best Practice.

               /****************************************************************************
               *                             FileState::Compare
               * Result: int
               *       1 if disk file is newer than memory copy
               *       0 if disk file is same as memory copy
               *       -1 if disk file is older than memory copy
               ****************************************************************************/
               int FileState::Compare() {
                  FILETIME c;
                  FILETIME a;
                  FILETIME w;
                  if(!GetDiskTimestamp(c, a, w))
                     return 0;

                  // Comparison is based on write time
                  LARGE_INTEGER now;  // state now
                  now.LowPart = w.dwLowDateTime;
                  now.HighPart = w.dwHighDateTime;
                  LARGE_INTEGER then;
                  then.LowPart = write.dwLowDateTime;
                  then.HighPart = write.dwHighDateTime;
                  if(now.QuadPart > then.QuadPart)
                     return 1;  // disk file is newer than memory copy
                  if(now.QuadPart < then.QuadPart)
                     return -1; // disk file is older than memory copy!
                  return 0;     // disk file is same as memory copy
               } // FileState::Compare

To read the timestamp, I use FileState::GetDiskTimeStamp, which just opens the file and reads the timestamp.  If it fails, it returns FALSE

               /****************************************************************************
               *                         FileState::GetDiskTimestamp
               * Inputs:
               *       FILETIME & c: Creation timestamp, or NULL
               *       FILETIME & a: Access timestamp, or NULL
               *       FILETIME & w: Write time, or NULL
               * Result: BOOL
               *       TRUE if OK, FALSE if error
               * Effect: 
               *       Reads the current file time
               ****************************************************************************/

               BOOL FileState::GetDiskTimestamp(FILETIME & c, FILETIME & a, FILETIME & w)
                  {
                   DWORD err = ERROR_SUCCESS;
                   HANDLE h = ::CreateFile(name, 0, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
                   if(h == INVALID_HANDLE_VALUE)
                      return FALSE;

                   BOOL b = ::GetFileTime(h, &c, &a, &w);
                   if(!b)
                      { /* failed */
                       err = ::GetLastError();
                      } /* failed */
                   
                   ::CloseHandle(h);
                   ::SetLastError(err);
                   return err == ERROR_SUCCESS;
                  } // FileState::GetDiskTimestamp

What is interesting to note about this is that if it returns FALSE, then the user can call ::GetLastError to determine the reason for failure.  But if ::GetFileTime fails, then we still have to call ::CloseHandle, which can potentially change the value returned by ::GetLastError.  So before returning, we call ::SetLastError to set the error code to the real reason for the failure, not some secondary reason.

A better About... box

One of the travesties in MFC, which has been true since it was first delivered, is that the About... box code is hardwired into the main module.  This does not make sense, could not make sense, and is a prime example of poor programming methodology.  It makes it difficult to replug a standard About... box module in.  It is depressing, but not surprising, that with VS2005, this is still true.  I suspect it is also true in VS2008.  Again, like most real improvements, it has no "flash", it doesn't look "sexy", it is merely sensible, obvious, and necessary.

But I did modify the About... box to pick up the version number of the program, rather than having it hardwired into the program.  (It is also sad that after all these years, Microsoft does not provide us a way to have the build number incremented after each successful build, but again, that would merely be sensible, obvious, and useful, and therefore is probably considered boring).  I used the existing static control in the dialog, but instead of it saying "programname version 1.0" I changed it to say "programname version *" and I replace the * with the current version string:

/****************************************************************************
*                              CAboutDlg::OnInitDialog
* Result: BOOL
*       TRUE, always
* Effect: 
*       Initializes the dialog
****************************************************************************/

BOOL CAboutDlg::OnInitDialog()
   {
    CDialog::OnInitDialog();

    GetVer();
    
    return TRUE;  // return TRUE unless you set the focus to a control
                  // EXCEPTION: OCX Property Pages should return FALSE
   }

/****************************************************************************
*                              CAboutDlg::GetVer
* Result: void
*       
* Effect: 
*       Sets the version number string
****************************************************************************/

void CAboutDlg::GetVer()
    {
     CString name;
     LPTSTR p = name.GetBuffer(MAX_PATH);                    // [1]
     DWORD n = ::GetModuleFileName(NULL, p, MAX_PATH);       // [2]
     name.ReleaseBuffer();                                   // [3]
     if(n == 0)
        return;
     
     DWORD ignored;
     DWORD size = ::GetFileVersionInfoSize(name, &ignored);  // [4]

     if(size == 0)
        return;

     CByteArray buffer;                                      // [5]
     buffer.SetSize(size);                                   // [6]
     
     if(!::GetFileVersionInfo(name, NULL, size, buffer.GetData())) // [7]
        return;

     VS_FIXEDFILEINFO * info;                                // [8]
     UINT len;
     if(!::VerQueryValue(buffer.GetData(), _T("\\"), (LPVOID *)&info, &len))  // [9]
        return;

     CString version;
     c_Version.GetWindowText(version);                       // [10]
     
     CString verstr;
     verstr.Format(_T("%d.%d.%d.%d"), HIWORD(info->dwFileVersionMS), LOWORD(info->dwFileVersionMS),  // [11]
                                      HIWORD(info->dwFileVersionLS), LOWORD(info->dwFileVersionLS));
     version.Replace(_T("*"), verstr);                       // [12]
     c_Version.SetWindowText(version);                       // [13]
    } // CAboutDlg::GetVer
  1. By using GetBuffer, I can write directly into the CString contents, and don't need to introduce another level of buffering or a TCHAR array solely to hold the string value.
  2. Get the name of the file (full path) that is the executable file image
  3. ReleaseBuffer will now restore the integrity of the CString so it can be used.  See my essay on CString Techniques.
  4. Call GetFileVersionInfoSize to determine how many bytes of version information must be supplied.  (Note that using the Version APIs requires adding version.lib to the link.  I have no idea why this obvious file is not an implicit part of the standard link.
  5. By using a CByteArray, I do not have to worry about explicit new or delete.  When I leave scope, the space will be freed.
  6. By using SetSize, I force the array to have as many elements as specified by the parameter; for a CByteArray, this is (of course) the number of bytes.
  7. To access that buffer, I use the GetData method, which returns a pointer to an array of the type of elements of the MFC collection.
  8. To get the file version information, I only need to look at the VS_FIXEDFILEINFO header, which is all binary data.
  9. VerQueryValue will retrieve the value.  By specifying the path as _T("\\"), this means that I will get the binary header data.  Note that the second parameter to VerQueryValue is incorrectly specified in the headers as an LPTSTR, not an LPCTSTR.  This is a bug in the header files.  The documentation I have erroneously documents the third parameter as a PUINT, although the header file clearly states it is an LPVOID *
  10. Retrieve the current string which says "programname version *"
  11. Format the binary version string into a readable string.
  12. Replace the * with the new version string
  13. Set the text in the control
As specified in the resource editor As displayed in the About... box

Note also that I replace the silly (c) with an actual © symbol.  Why Visual Studio cannot put this symbol in escapes me.  There seems to be a reluctance to change from the way the 16-bit version worked in the early 1990s.

The use of progress bars

Progress bars are important.  They provide the user with visual feedback on how quickly the program is working, and allow the user to estimate time to completion. I had already implemented the "main" progress bar, which extends across the entire window.  But I discovered that when I was using the "slide locator", that I wanted a sense of how much of the presentation I had looked at for a given word.  So I added a little progress bar below it.  I find it very comforting.  And that's something to remember: the purpose of a progress bar is to make your users comfortable.  User comfort matters.

Progress bars have been around for a long time.  But when Microsoft decided to create their own, the designers were apparently living on a slightly different astral plane that everyone else.  The progress bars were ugly, segmented, did not display percentages, and overall looked like some amateur's idea of what a progress bar might be, after having one described over a noisy telephone connection.  They finally let us have smooth progress bars, but we still don't have all the support we need to make them as easy to use as they should be.

For simple things, progress bars are easy to use.  There is a constant failure I see, which is the myth that progress bars can only show values in the range of 0..100.  I have no idea why this is so soundly believed by so many, but it simply isn't true.  There is an issue about setting the range, however.  In the original version of the progress bar, done on Win16, the range was limited to -32768..32767.  As is typical of certain flawed decisions, Microsoft decided to retain this mistake in 32-bit Windows.  After some years, we were given a new message, PBM_SETRANGE32, which allowed us to set 32-bit ranges. After many more years, we were given an MFC method, CProgressCtrl::SetRange32, to let us set it easily.  It would have been so much simpler to just have the CProgressCtrl::SetRange method take 32-bit integers. As I illustrated in my discussion of the way the file-reader thread updates the progress bar, you don't need to normalize 32-bit ranges.  Only if you are dealing with 64-bit ranges is there a need to deal with normalization of any sort.

But I wanted to change the bar color.  The feature that let the progress bar color be changed was the PBM_SETBARCOLOR message, which came in with IE 4 (why the common controls were attached to the browser and not just an update of standard libraries is one of the many mysteries for which we will never have a satisfactory answer).  But after many years, and at least three, if not four, releases of Visual Studio, we still do not have a method to set the bar color.  Many beginners miss the fact that it is always possible to send a message to a window; to set the red bar color for the small presentation-browser progress bar, I just did

 c_LocatorPos.SendMessage(PBM_SETBARCOLOR, 0, RGB(255, 0, 0));

In my list of problems which I submitted with the 2007 MVP Summit, one was that every message shall have a method by which it can be sent.  Every notification shall have a handler, and every reflected and reflected-ex message shall have a way to create its handler from VS.  However, since the current philosophy is to add new and relatively useless features while not correcting the already-known defects, I do not have confidence that we will see this more-than-decade-old problem resolved.  It doesn't have any "flash", it is merely useful and makes us more productive.

Arrow buttons

This one is easy.  I just reused my Arrow-button class, CArrowButton.

Resizable dialog

For some reason, when I demonstrate this in my classes, the students all go "ooh, ahh!" because it is so cool.  I've done resizable dialogs for years, in fact, my very first Windows app used resizable dialogs in 1990!  The techniques for doing window resizing, control resizing within a dialog, and limiting the minimum size of the dialog are all covered in my essay on Limiting Dialog Box Resizing.

Using the PowerPoint Automation Interface

I decided it would be rather cool to be able to double-click an item in the text display and have it bring up PowerPoint so I could see the slide.  This turned out to be a bit of an adventure, but by the time I was done, I had the basic techniques that allowed me to read the contents of a PowerPoint file, so I went ahead and did that.

Adding a class from a TypeLib

I started with an article from CodeProbject, http://www.codeproject.com/com/ole_automation.asp, but it is very much out of date with the current version of Office Automation.  Probably in a few years, this article will be, also.  But I used it as a starting point.

In addition, another useful reference was http://msdn.microsoft.com/archive/en-us/office97/html/web/006.asp?frame=true, which is Chapter 6 of an older Microsoft Press book.

The VBA documentation, as mentioned earlier, was designed and written by people with no concept of online documentation, and is almost but not quite totally unusable.  You will have to use it, but be prepared to suffer a lot.  If you're used to quality documentation like MSDN documentation, you will be amazed that anyone could accomplish anything at all when they are forced to use documentation this poor.

To add a class in VS.NET, go to the ClassView (an unnecessary step if there had been any effort to actually design a user-friendly interface; it was not previously required by any version of Visual Studio), right click on the name of your project, and click Add...  When the secondary menu pops up (you can always tell a menu interface designed by a programmer.  It is well-structured, hierarchical, and completely unusable), select Add Class...

You will get a window that looks like the one shown below. 

Select the option to add and MFC class (the left panel) and then select the new class to be from an TypeLib (the right panel).  Click the ill-named Open button.  The next window that comes up is a window to add a type library.

As shown, click on the option to read it from a file, and then use the button to browse to the Office Type Library area, which is in c:\Program Files\Microsoft Office\OFFICE11 for Office 2003, which is my production version of Office (Office 2007 made gratuitous changes in the interface, but didn't fix any of the fundamental user interface problems, and I have no strong motivation to change to it).  Select the MSPPT.OLB file:

You can then add interfaces for each class in the left to the box on the right using the button in the Add Class wizard.

I used the classes shown in the table below.  Note a certain symmetry.  There is a Somethings class which is always a collection of Something objects, and a Something class which is the class of all the elements of the Somethings class.  There appears to be no particular explanation of why some classes have a 0 suffixed to the name, while others do not, except for CView0, which obviously cannot conflict with the MFC class of the same name.

Class name in list C++ Class name File name Explanation
_Application CApplication CApplication.h A representative for the PowerPoint program
_Presentation CPresentation CPresentation.h An individual presentation
_Slide CSlide CApplication.h A representative for a single slide
Cell CCell CCell.h A single cell in a table
CellRange CCellRange CCellRange.h A collection of cells in a table (typically a complete row or column).  Needed only as an intermediary value
Column CColumn CCollumn.h A designator of a column in a table
Columns CColumn0 CColumna0.h A designator for a range of columns
DocumentWindow CDocumentWindow CDocumentWindow.h A window in a PowerPoint presentation
DocumentWindows CDocumentWindows CDocumentWindows.h A collection of windows in a PowerPoint presentation
Placeholders CPlaceholders CPlaceholders.h A placeholder collection (used to get text from the Speaker's Notes section)
Presentations CPresentations CPresentations.h A collection of presentations
Row CRow CRow.h A class used to work with rows of a table
Rows CRows CRows.h A class which represents a collection of rows
Shape CShape CShape.h An individual shape on a PowerPoint slide.  A shape is any object on the slide.
Shapes CShapes CShapes.h A collection of shapes
SlideRange CSlideRange CSlideRange.h A range of slides (required to access the Notes section)
Slides CSlides CSlides.h A collection of slides
Table CTable0 CTable0.h A table object
TextFrame CTextFrame CTextFrame.h A text frame
TextRange CTextRange CTextRange.h A collection of text
View CView0 CView0.h A view of a presentation

Creating a helper class

After a bit of experimentation, I came up with the following class to help me work with PowerPoint:

PowerPointManager::Launch

BOOL PowerPointManager::Launch()                              
    {                                                               
     if(!Launched)                                                  [1]
        { /* launch PowerPoint */                                   
         if(!app.CreateDispatch(_T("Powerpoint.Application")))      [2]
            { /* failed */                                          
             AfxMessageBox(IDS_PPT_FAILED, MB_ICONERROR | MB_OK);   [3]
             return FALSE;                                          
            } /* failed */                                          
         Launched = TRUE;                                           [4]
         app.put_Visible(TRUE);                                     [5]
        } /* launch PowerPoint */                                   
     AfxOleGetMessageFilter()->EnableBusyDialog(FALSE);             [6]
     AfxOleGetMessageFilter()->EnableNotRespondingDialog(FALSE)     [7]
     return TRUE;                                                   [8]
    } // CIndexerDlg::PowerPoint::Launch                            
 

PowerPointManager::OpenPresentation

This introduces a level of indirection and catches exceptions thrown by the internal function InternalOpenPresentation.

BOOL PowerPointManager::OpenPresentation(FID fid)                   // REQ #088
    {                                                               // REQ #088
     try                                                            // REQ #088 [1]
        { /* try open */                                            // REQ #088
         return InternalOpenPresentation(fid);                      // REQ #088 [2]
        } /* try open */                                            // REQ #088
     catch(COpenException * e)                                      // REQ #088 [3]
        { /* open failed */                                         // REQ #088
         ::SetLastError(e->error);                                  // REQ #088 [4]
         e->Delete();                                               // REQ #088 [5]
         return FALSE;                                              // REQ #088 [6]
        } /* open failed */                                         // REQ #088
    } // PowerPointManager::OpenPresentation                        // REQ #088
  1. Surround the call to InternalOpenPresentation with an exception frame
  2. Call InternalOpenPresentation.  Actually, the only value it can return is TRUE.  Any error will throw an exception.
  3. If it threw an exception, it would be a COpenException, catch it here
  4. Uset ::SetLastError to set the error code for the caller.  This preserves the error code, because there are no COleDispatchDriver objects in this scope, and the erroneous implementation of the destructor does not affect us.
  5. Delete the COpenException object
  6. Return FALSE to the caller; the caller will be able to call ::GetLastError to retrieve the error code

 

COpenException

class COpenException : public CException {                          // REQ #088 [1]
    public:                                                         // REQ #088
       COpenException(DWORD err) : CException() { error = err; }    // REQ #088 [2] 
       DWORD error;                                                 // REQ #088 [3]
};                                                                  // REQ #088
  1. Like most exceptions, we derive this from CException
  2. The constructor takes a single parameter, the error code
  3. The error member will be available in the catch handler

PowerPointManager::InternalOpenPresentation

This method opens a presentation.  I discovered that if I executed this repeatedly, I got multiple copies of the presentation of the file, so I modified the method to look up in the existing app to see if the presentation is already open.

The InternalOpenPresentation is required because there are a huge set of problems with the implementation of the OLE interface.  For example, the destructor COleDispatchDriver::~COleDispatchDriver code will reset the ::GetLastError code to 0 (S_OK) so the obvious ability to detect an error by simply handling the exception and returning FALSE will fail.  The error code seen by the caller is 0!  This code will not work:

if(some_error_condition)
   { /* failed */
    DWORD err = ::GetLastError();
    ...deal with error
   ::SetLastError(err);
   return FALSE;
  } /* failed */

actually will not work correctly.  For example, the CPresentations class, the CPresentation class, and similar classes all end up calling COleDispatchDriver::~COleDispatchDriver.  So when the destructors are called when we exit scope of the try, the destructor resets the ::GetLastError value.  The solution was to create a new class, COpenException, and throw exceptions. 

BOOL PowerPointManager::InternalOpenPresentation(FID fid)           // REQ #041// REQ #088
    {                                                               // REQ #041
     if(!Launched)                                                  // REQ #041
        { /* not launched */                                        // REQ #088
         throw new COpenException(ERROR_OBJECT_NOT_FOUND);          // REQ #088
        } /* not launched */                                        // REQ #088

     if(FileSet::GetOpened(fid))                                    // REQ #041
        return TRUE;                                                // REQ #041
     CString filename = FileSet::GetFile(fid);                      // REQ #041
     WHERE;                                                         // REQ #047
     try { /* try */                                                // REQ #041
        CPresentations presentations;                               // REQ #041
        CPresentation presentation;                                 // REQ #041
                                                                    // REQ #041
        CString uname = filename;                                   // REQ #002
        uname.MakeUpper();                                          // REQ #002
                                                                    // REQ #002
        HERE();                                                     // REQ #047
        presentations = app.get_Presentations();                    // REQ #002
                                                                    // REQ #002
        HERE();                                                     // REQ #047
        int PresentationsCount = presentations.get_Count();         // REQ #002
                                                                    // REQ #002
        for(int i = 1; i <= PresentationsCount; i++)                // REQ #002
           { /* scan presentations */                               // REQ #002
            HERE();                                                 // REQ #047
            presentation = presentations.Item(COleVariant((long)i));// REQ #002// REQ #041
            HERE();                                                 // REQ #047
            CString pname = presentation.get_FullName();            // REQ #002
            pname.MakeUpper();                                      // REQ #002
            if(uname == pname)                                      // REQ #002
               { /* found presentation */                           // REQ #002
                TRACE(_T("PowerPointManager::OpenPresentation Found [%d] \"%s\"\n"), i, filename);      // REQ #002// REQ #041
                FileSet::SetPresentation(fid, presentation);        // REQ #041// REQ #048
                FileSet::SetAlreadyOpen(fid, TRUE);                 // REQ #041
                return TRUE;                                        // REQ #002
               } /* found presentation */                           // REQ #002
           } /* scan presentations */                               // REQ #002
                                                                    // REQ #002
        // This code is required because the MFC library was never  // REQ #088
        // debugged, and reports an error caused by trying to open  // REQ #088
        // a non-existent file as "Type Mismatch"                   // REQ #088
        // And in the Release version, this causes a fatal runtime  // REQ #088
        // error that terminates the program, because the MFC       // REQ #088
        // library in the Release version is erroneous              // REQ #088
        DWORD attr = ::GetFileAttributes(filename);                 // REQ #088
        if(attr == INVALID_FILE_ATTRIBUTES)                         // REQ #088
           { /* failed */                                           // REQ #088
            DWORD err = ::GetLastError();                           // REQ #088
            TRACE(_T("PowerPointManager::OpenPresentation file query failed \"%s\" (%s)\n"),// REQ #088
                  filename, ErrorString(err));                      // REQ #088
            throw new COpenException(err);                          // REQ #088
           } /* failed */                                           // REQ #088
        TRACE(_T("PowerPointManager::OpenPresentation Opening \"%s\"\n"), filename); // REQ #087
        HERE();                                                     // REQ #047
        presentation = presentations.Open(filename,                 // REQ #002// REQ #041
                                          0,  // ReadOnly           // REQ #002
                                          0,  // Untitled           // REQ #002
                                          1); // WithWindow         // REQ #002
                                                                    // REQ #002// REQ #041
        if(presentation == NULL)                                    // REQ #002// REQ #041
           { /* failed to open */                                   // REQ #088
           throw new COpenException(::GetLastError());              // REQ #088
           } /* failed to open */                                   // REQ #088
                                                                    // REQ #088
        BOOL result = FileSet::SetPresentation(fid, presentation);  // REQ #002// REQ #041// REQ #048// REQ #088
        if(!result)                                                 // REQ #088
           { /* throw exception */                                  // REQ #088
            throw new COpenException(::GetLastError());             // REQ #088
           } /* throw exception */                                  // REQ #088
                                                                    // REQ #088
        return TRUE;                                                // REQ #088
     } /* try  find */                                              // REQ #002
     catch(COleException * e)                                       // REQ #041
        { /* find failed */                                         // REQ #041
         HandleOleException(e, fid, 0, where, NULL);                // REQ #047// REQ #077
         return FALSE;                                              // REQ #041
        } /* find failed */                                         // REQ #041
                                                                    // REQ #041
    } // PowerPointManager::InternalOpenPresentation                // REQ #002// REQ #041// REQ #088

PowerPointManager::Open

This opens the presentation (using GetPresentation) and then retrieves a number of objects that will be useful in reading and manipulating the presentation.  In particular, I need the collections of Slides so I can read and search for slides; the collection of Windows so I can locate a specific Window, which is the first window for the presentation.  I need the View because that is necessary to select an individual slide.  If I manage to obtain all of these, then I declare the presentation open and ready to use.

BOOL PowerPointManager::Open(const CString & filename)              // REQ #002
    {                                                               // REQ #002
     if(!Opened)                                                    // REQ #002
        { /* open presentation */                                   // REQ #002
         File.name = filename;                                      // REQ #002
         if(!GetPresentation(filename))                             // REQ #002
            return FALSE;                                           // REQ #002
                                                                    // REQ #002
         if(Presentation == NULL)                                   // REQ #002
            return FALSE;                                           // REQ #002
                                                                    // REQ #002
         slides = Presentation.get_Slides();                        // REQ #002
         if(slides == NULL)                                         // REQ #002
            { /* failed slides */                                   // REQ #002
             Presentation.Close();                                  // REQ #002
             return FALSE;                                          // REQ #002
            } /* failed slides */                                   // REQ #002
                                                                    // REQ #002
         windows = Presentation.get_Windows();                      // REQ #002
         if(windows == NULL)                                        // REQ #002
            { /* failed windows */                                  // REQ #002
             Presentation.Close();                                  // REQ #002
             return FALSE;                                          // REQ #002
            } /* failed windows */                                  // REQ #002
                                                                    // REQ #002
         window = windows.Item(1);                                  // REQ #002
         if(window == NULL)                                         // REQ #002
            { /* failed window */                                   // REQ #002
             Presentation.Close();                                  // REQ #002
             return FALSE;                                          // REQ #002
            } /* failed window */                                   // REQ #002
                                                                    // REQ #002
         view = window.get_View();                                  // REQ #002
         if(view == NULL)                                           // REQ #002
            { /* failed view */                                     // REQ #002
             Presentation.Close();                                  // REQ #002
             return FALSE;                                          // REQ #002
            } /* failed view */                                     // REQ #002
                                                                    // REQ #002
         Opened = TRUE;                                             // REQ #002
        } /* open presentation */                                   // REQ #002
     return TRUE;                                                   // REQ #002
    } // CIndexerDlg::PowerPoint::Open                              // REQ #002 

PowerPointManager::Close

This method just closes the presentation.  It does not exit PowerPoint (it does not call the Application.Quit method) because that will force PowerPoint to close, and it may be opened for other reasons.

void PowerPointManager::Close()                                     // REQ #002
    {                                                               // REQ #002
     if(!Launched)                                                  // REQ #002
        return;                                                     // REQ #002
     if(!Opened)                                                    // REQ #002
        return;                                                     // REQ #002
     try {                                                          // REQ #002
         Presentation.Close();                                      // REQ #002
         Opened = FALSE;                                            // REQ #002
     }                                                              // REQ #002
     catch(COleException * e)                                       // REQ #002
        { /* failed */                                              // REQ #002
         ASSERT(FALSE);                                             // REQ #002
         e->Delete();                                               // REQ #002
        } /* failed */                                              // REQ #002
    } // CIndexerDlg::PowerPoint::Close                             // REQ #002 

The above examples are largely from the pre-*INCLUDE version of the code.

Disabling annoying messages

The default behavior of the automation interface is to time out if the operation "takes too long", where "too long" seems to be disconnected from most forms of reality.  To disable the automation interface timeouts, which generate confusing and misleading prompts to the end user, my first-cut solution is to disable them completely.  (Special thanks and a Wave Of The Flounder Fin to David Ching for pointing out how to do this)

I added this code in the PowerPointManager::Launch method

     AfxOleGetMessageFilter()->EnableBusyDialog(FALSE);             // REQ #082
     AfxOleGetMessageFilter()->EnableNotRespondingDialog(FALSE);    // REQ #082

Dealing with Exceptions

If an interface call to the automation interface fails, it indicates the failure by doing a throw new COleException(...) statement.  Thus there has to be protection at each call site to prevent these from producing meaningless notifications in the MFC runtime.  Also, it is essential that integrity of the operations which are in progress must be preserved.  Essentially, the mode is

try 
   { /* some operations */
    automation_call_1(...);
    automation_call_2(...);
    automation_call_3(...);
   } /* some operations */
catch(COleException * e)
   { /* operations failed */
    ... deal with failure mode
    e->Delete();
    ...make world safe
   } /* operations failed */

The only problem with this, particularly during development, was that I really needed to know which of those several operations failed.  It would also be useful, in the production version, to issue meaningful error messages that would help with tech support issues that might arise.  So I developed a set of protocols for handling this.

class here {                                                        
    public:                                                         
       here() { line = 0; }                                                    [1]
       here(int L, LPCTSTR F) { line = L + 1; filename = F; }                  [2]  
       here(here & h) { line = h.line; filename = h.filename; }                [3]  
       CString ToString() { return ::ToString(_T("%s(%d)"), filename, line);}  [4] 
    protected:                                                      
       int line;                                                               [5]  
       CString filename;                                                       [6]  
};                                                                  
                                                                    
#define WHERE here where;                                                      [7]
#define HERE() where = here(__LINE__, _T(__FILE__))                            [8]

[1] The default constructor simply sets the line to 0

[2] The normal constructor that would be used would take __LINE__ and __FILE__ as parameters.  By convention, this will appear on the line immediately before an automation call, so the line number is incremented by 1

[3] This allows passing a here object as a parameter

[4] The ToString method returns a printable string for this object

[5] The line number is protected

[6] The source file name is protected

[7] To declare a variable, the WHERE macro declares an object with a standardized name, where

[8] To record the call which is about to be done, the HERE() macro precides the automation line

So I would now code the example as

WHERE;
try 
   { /* some operations */
    HERE();
    automation_call_1(...);
    ...
    HERE();
    automation_call_2(...);
    ...
    HERE();
    automation_call_3(...);
   } /* some operations */
catch(COleException * e)
   { /* operations failed */
    ... deal with failure mode
    e->Delete();
    ...make world safe
   } /* operations failed */

The handling of an interface failure is nearly identical in every case, so a simple function was coded to handle this:

__inline void HandleOleException(COleException * e, FID fid, int slideno, here & where, CWnd * wnd)
      {                                                             
       DWORD err = e->m_sc;                                                                      [1]
#ifdef _DEBUG                                                                                    [2]  
       TRACE(_T("HandleOleException(e=%08x %s, fid=%d (%s), slideno=%d, where=%s, ...)\n"),      [3]
             err, ErrorString(err), fid, FileSet::GetAlias(fid), slideno, where.ToString()); 
       ASSERT(FALSE);                                                                            [4]  
#endif                                                              
       e->Delete();                                                                              [5]
       ReportException(wnd, slideno, where, err);                                                [6]  
       ::SetLastError(err);                                                                      [7]  
      } /* failed */                                                
                         

[1] Retrieve the actual error code from the COleException::m_sc member

[2] Only in debug mode print out a trace (this is conditionalized because I only include the required header files in debug mode)

[3] The trace includes the FID and its alias, the slide number (if relevant),  and doesn't bother displaying other parameters

[4] Since exceptions should not be thrown, in debug mode this will allow me to break into the debugger

[5] Delete the CException object (this is a standard idiom in handling MFC CException-derived objects)

[6] Generate a report.  The exception report request is queued for the main thread so a MessageBox can be displayed

[7] Set the error code so the caller can query it if looking for the actual reason

 

 

Hand-rolled ToolTips

I find a number of problems using the standard ToolTips.  From my viewpoint, the most serious limitation is that there is only one ToolTip notification sent, when the mouse first enters the control.  Since I want to change the ToolTip display depending on the exact cell it is hovering over, I need to track the mouse position continuously.  This led to building my own ToolTip control.

I derived my class directly from CWnd, because it must be a popup window, not a child window.  Classes like CStatic are based on child windows and would be unsuitable.  ToolTps also do not display immediately, and have a timeout; while you are reading them, they frequently just disappear.  It wanted them to appear immediately, and not disappear until the context for their display was no longer applicable.

The code is surprisingly complex because I wanted to handle all kinds of obscure cases, such as when the window crosses the boundary between two monitors, and the ToolTip would be split between monitors, and especially if the monitors had different resolutions; even when split, the ToolTip might not be readable on the "other" monitor.  So it took about 300 lines of moderately-carefully-thought-out code to make all these cases work.  For example, the screen shot to the left shows what happens when the cursor gets near the right edge of a multi-monitor system; the black rectangle represents the fact that the right-hand monitor has a lower resolution (1280×1024) than the left-hand monitor (1920×1200), so the left-hand monitor is 176 pixels shorter.  If the ToolTip shown had been split across the boundary, it would not have been seen in its entirety.

The trick is to anchor the ToolTip to always display within a single monitor.  Note that moving the mouse slightly to the right will result in the ToolTip being displayed above, as shown here.  The ToolTip could not be shown below the cursor because it would then be off-screen, and the algorithm to keep it on a single monitor has forced it to be somewhat to the right of where it would have been displayed if the default had been taken.

It's the little fiddly bits like this that take time, but have a high payoff in usability.

 

The GetParent() problem

It seemed simple except for one thing: there is an undocumented feature of ::CreateWindow and ::CreateWindowEx relating to popup windows.  I have not been able to locate any documentation anywhere that describes this issue.  I have added to my essay on MSDN errors and omissions.

All I had to do was to record the parent CWnd *.  Because the parent is part of the permanent handle map, it was safe to store the CWnd *GetParent() of the current window would give me not the parent I gave it for creation, but instead the top-level dialog window, the main window.  This is not the expected behavior of GetParent, and it is caused by the fact that the parent of the window actually is the top-level window of the application, in spite of my specifying another window for this purpose.

Handling WM_SETFONT and WM_GETFONT

The basic CWnd class does not support a window that handles either WM_SETFONT or WM_GETFONT.  Therefore, handles have to be created for these messages.

I added an HFONT member to the class to hold the font handle.  The functions were

LRESULT CTextToolTip::OnSetFont(WPARAM wParam, LPARAM lParam) 
    {                                                             
     font = (HFONT)wParam;                                        
     if(lParam)                                                   
        { /* force redraw */                                      
         Invalidate();                                           
         RedrawWindow();                                         
        } /* force redraw */                                     
     return 0;                                                   
    } // CTextToolTip::OnSetFont 
    
LRESULT CTextToolTip::OnGetFont(WPARAM, LPARAM)                     
    {                                                               
     return (LRESULT)font;                                          
    } // CTextToolTip::OnGetFont                                                                    

When doing drawing, the way this is handled is to use CFont::FromHandle to get a CFont * for the font

void CTextToolTip::OnPaint()                                   
    {                                                               
     CPaintDC dc(this); // device context for painting              
                                                                    
     CFont * f;                                                     
     if(font == NULL)                                               
        dc.SelectStockObject(DEFAULT_GUI_FONT);                     
     else                                                           
        { /* use user-specified font */                          
         f = CFont::FromHandle(font);                       
         dc.SelectObject(f);                             
        } /* use user-specified font */                  

Tracking the mouse

Tracking the mouse is a bit tricky because of how WM_MOUSELEAVE messages are generated.  Then window, when it is made visible, must not be under the cursor, for example, or suddenly the mouse finds itself above that window, which means it has "left" the current window.  This results in a lot of annoying flicker but nothing useful happens.

To track the mouse, I call ::TrackMouseEvent, specifying the control as the window being tracked. A WM_MOUSELEAVE will be sent to the control when the mouse leaves it.  Due tom an unfortunate attitude on the part of the VS developers, there is no way to add the WM_MOUSELEAVE message from the Properties, so this must be added manually.  So the following lines are added to the files via a text editor

In IndexData.h

afx_msg LRESULT OnMouseLeave(WPARAM, LPARAM);

In IndexData.cpp

ON_MESSAGE(WM_MOUSELEAVE, OnMouseLeave)
LRESULT CIndexData::OnMouseLeave(WPARAM, LPARAM)
   {
     popup.Hide();
     return 0; 
   } // CIndexData::OnMouseLeave

The mouse tracking is set by the popup control.  The mouse movement is intercepted by handling WM_MOUSEMOVE in the list control.  The trick is to use the CListCtrl::SubItemHitTest to tell us what subitem the mouse is hovering over at this point, and based on that, decide what, if any, text should be shown.  Currently, the File column will show the full file name on the flyover help (the column only shows the alias), and if there is an error message, it will display the error message text, which is represented by a STRINGTABLE index.

To show the popup text, I call the CTextToolTip::SetText method.  If the tooltip window does not exist, it is created; if it is not visible, it is shown; if it is visible, and the text changed, the text is updated, and if it is visible it is moved to track the mouse.

void CIndexData::OnMouseMove(UINT nFlags, CPoint point)       
    {                                                              
     LVHITTESTINFO hit;                                            
     hit.pt = point;                                               
     int n = SubItemHitTest(&hit);                                 
     if(n < 0)                                                     
        return;                                                    
     switch(hit.iSubItem)                                          
        { /* which subitem */                                      
         case INDEX_FILE:                                          
            { /* file name */                                      
             SlideData * data = (SlideData *)GetItemData(n);       
             CString s = FileSet::GetFile(data->fileid);           
             popup.SetText(point, s, this);                        
            } /* file name */                                      
            break;                                                 
         case INDEX_SLIDE:                                         
            popup.Hide();                                          
            break;                                                 
         case INDEX_TEXT:                                          
            { /* text */                                           
             SlideData * data = (SlideData *)GetItemData(n);       
             if(data->tooltip != 0)                                
                { /* has tip */                                    
                 CString s;                                        
                 s.LoadString(data->tooltip);                      
                 popup.SetText(point, s, this);                    
                } /* has tip */                                    
             else
                 popup.Hide();
            } /* text */                                           
            break;                                                 
        } /* which subitem */                                      
                                                                   
     CListCtrlEx::OnMouseMove(nFlags, point);                      
    }                                                               

Flyover help on a CListBox

I can use the same functions on a CListBox so the user does not have to horizontally scroll to see long strings.  Horizontal scrolling is generally a Really Bad Interface Idea and should be avoided whenever possible.

The goal here is to pop up a ToolTip only when the string is wide to be displayed in the ListBox without scrolling.  Note that the horizontal extent is set to perform scrolling if the user desires to do so, but I wanted to avoid the need for it.

void CWordList::OnMouseMove(UINT nFlags, CPoint point)              // REQ #078
    {                                                               // REQ #078
     BOOL outside;                                                  // REQ #078
     int n = ItemFromPoint(point, outside);                         // REQ #078 [1]
     if(!outside)                                                   // REQ #078 [2]
        { /* valid point */                                         // REQ #078
         CRect r;                                                   // REQ #078
         GetClientRect(&r);                                         // REQ #078 [3]
         CString s;                                                 // REQ #078 
         IndexInfo * info = (IndexInfo *)GetItemDataPtr(n);         // REQ #078 [4]
         s = info->GetText();                                       // REQ #078 [5]
         if(info->GetItemCount() > 0)                               // REQ #078 [6]
            { /* has count */                                       // REQ #078
             s += ToString(_T(" (%d)"), info->GetItemCount());      // REQ #078 [7]
            } /* has count */                                       // REQ #078
         CClientDC dc(this);                                        // REQ #078 [8]
         CFont * f = GetFont();                                     // REQ #078 [9]
         if(f != NULL)                                              // REQ #078
            dc.SelectObject(f);                                     // REQ #078 [10]
         int width = dc.GetTextExtent(s).cx;                        // REQ #078 [11]
         if(width > r.Width())                                      // REQ #078 [12]
            { /* need popup */                                      // REQ #078
             s = _T(" ") + s + _T(" ");                             // REQ #078 
             popup.SetText(point, s, this);                         // REQ #078 [13]
            } /* need popup */                                      // REQ #078
         else                                                       // REQ #078
            popup.Hide();                                           // REQ #078 [14]
        } /* valid point */                                         // REQ #078
     else                                                           // REQ #078
        { /* no longer inside */                                    // REQ #078
         popup.Hide();                                              // REQ #078 [15]
        } /* no longer inside */                                    // REQ #078
     CListBox::OnMouseMove(nFlags, point);                          // REQ #078
    }                                                               // REQ #078

 
  1. By using ItemFromPoint, I obtain the item under the cursor.  The outside parameter tells me if the item is outside the client area
  2. If the mouse is not outside the area, I want to consider the text length.   If it is outside the area, I will drop down to item 15.
  3. I get the current client rectangle
  4. I obtain the object that encodes the information I wish to display.
  5. The IndexInfo object has a GetText method that returns the printable text, but not the count, which is computed
  6. I want to include the count in the display, so I use IndexInfo::GetItemCount which gives me a count of the number of instances of this word or phrase in the PowerPoint document
  7. I append (n) to the text which gives me the complete string I want to display
  8. I will need to compute the display extent of the text.  For this I need a DC.  Note the absence of any GetDC call here.
  9. I want to use the font used for the control.  Note that a CPaintDC comes with the font pre-selected into it, but a CClientDC does not, so I must first obtain the font
  10. If there is a font (the value should always be non-NULL), I selected it into the DC
  11. I can now compute the text extent of the string
  12. If the display width is wider than the client area I will want to pop up the help
  13. The SetText method of my CTextToolTip class will cause the window to be created if it does not exist, and then will display it
  14. If the display width of the text fits within the client area, I do not display a Tooltip, and hide any Tooltip that might have been currently displayed.
  15. If the item found based on mouse position is outside the client area, I just hide the Tooltip.

 

A design philosophy: Sometimes, its easier to fix a mistake than avoid making it in the first place

The Table Of Contents (TOC) now contains a separator for each file indexed.  But if there's nothing to place there (the presentation in question has no *HEADINGn= directives), then I decided that the file should not clutter up the TOC.  This means that I should not add a "file element" to the TOC until I discover if there is a *HEADINGn= entry, and then add it, but only if it has not already been added.  This requires a lot of bookkeeping.  Why bother?  Whenever I open a file, I just toss the file-level-entry into the TOC.  Now, when I'm ready to print the TOC, all I have to do is scan and drop the printing of any file elements that don't have content following them!

The obvious way to do this is to defer printing out a file-level TOC element until a *HEADINGn element is found.  But this is also too much bookkeeping.  Why bother?  It is simple to scan the list of elements, which would be of the form

file1 H H ... H file2 H H ... H file3 file4  H...H file5

from front to back, and not print out filei if it contained no H elements, but this is requires extra effort.  But looking at the problem from a different perspective, if I scan the list in reverse order, assuming no headings seen, then setting a headings-seen flag when I encounter an H element, the algorithm is now trivial.  When I see an H element I set the flag, when I see a filei element, I clear the flag.  If the flag is not set when I find a filei element, I can delete that element.

Well, I can't delete it, actually; I keep the TOC in a std::map structure, and the erase method does not accept a reverse_iterator type; you can't remove an element during a reverse iteration.  So what I did was add an Erase member to the elements, set FALSE by the constructor.  As I scan backwards, I set the Erase member to TRUE if the element should be erased.  Then I do a forward pass, using an iterator type, because std::map does allow an erase on forward scanning, and simply delete all the elements that don't belong.

The nice thing about this is that a complex, distributed decision has been reduced to a simple, localized decision in a small subroutine that is easily understood.  "Efficiency" isn't even a discussion that arises in this context.  It is a common mistake to think that by lumping various unrelated pieces of logic together so that only "one pass" is required across the data, that the code is "more efficient".  It isn't.  It's harder to design, harder to code, harder to maintain, and definitely harder to reason about.  You only do this if "efficiency" matters at all, and to scan a std::map of a few hundred elements just isn't any effort at all.  Saving microseconds in a program that takes a couple minutes to run is not a reasonable design decision.

void CIndexerDlg::TrimContents()                                    // REQ #041
    {                                                               // REQ #041
     BOOL SawHeading = FALSE;                                       // REQ #041                               [1]
     // This algorithm is simpler to implement if we scan reverse   // REQ #041
     // but a reverse_iterator cannot be used for an erase, so      // REQ #041
     // instead, we mark the element as being eraseable, then       // REQ #041
     // iterator forward, deleting eraseable entries                // REQ #041
     for(ContentsMap::reverse_iterator iter = Contents.rbegin(); iter != Contents.rend(); iter++)// REQ #041  [2]
        { /* trim contents */                                       // REQ #041
         ContentsInfo & ci = iter->second;                          // REQ #041                               [3]
         if(ci.IsFileEntry())                                       // REQ #041                               [4]
            { /* file heading object */                             // REQ #041
             if(!SawHeading)                                        // REQ #041                               [5]
                { /* delete it */                                   // REQ #041
                 ci.Erase = TRUE;                                   // REQ #041                               [6]
                } /* delete it */                                   // REQ #041
             SawHeading = FALSE;                                    // REQ #041                               [7]
            } /* file heading object */                             // REQ #041
         else                                                       // REQ #041
            { /* *HEADING object */                                 // REQ #041
             SawHeading = TRUE;                                     // REQ #041                               [8]
            } /* *HEADING object */                                 // REQ #041
        } /* trim contents */                                       // REQ #041
                                                                    // REQ #041
     for(ContentsMap::iterator iter = Contents.begin(); iter != Contents.end(); )// REQ #041                  [9]
        { /* erase pass */                                          // REQ #041
         ContentsInfo & ci = iter->second;                          // REQ #041                              [10]
         if(ci.Erase)                                               // REQ #041                              [11]
            iter = Contents.erase(iter);                            // REQ #041                              [12]
         else                                                       // REQ #041
             iter++;                                                // REQ #041                              [13]
        } /* erase pass */                                          // REQ #041
                                                                    // REQ #041
     if(FileSet::IsSingleton())                                     // REQ #041                              [14]
        { /* delete first */                                        // REQ #041
         ContentsMap::iterator iter = Contents.begin();             // REQ #041                              [15]
         ContentsInfo & ci = iter->second;                          // REQ #041                              [16]
         if(ci.IsFileEntry())                                       // REQ #041                              [17]
            Contents.erase(iter);                                   // REQ #041                              [18]
        } /* delete first */                                        // REQ #041
    } // CIndexerDlg::TrimContents                                  // REQ #041

[1] This boolean tells whether or not we have seen a *HEADING= in the sequence.

[2] We do a reverse-iteration.  Note that the ++ means the reverse iterator is progressing from beginning to end.

[3] We obtain a ContentsInfo object which is what is indexed in this map

[4] There are two kinds of entries: *INCLUDE (file entries) and *HEADING entries.  If this is a *INCLUDE entry, we handle it

[5] If the *INCLUDE does not contain any *HEADING components, we mark it for deletion, but we cannot delete it because std::map::erase does not accept a reverse_iterator as an argument.

[6] Because we cannot delete it, we mark it as a "candidate for deletion".

[7] Since we have now seen a file entry, we set the flag that indicates no *HEADING elements have been seent

[8] If it is not a file entry object, it must be a *HEADING object, so we set the flag to indicate that a *HEADING has been seen

[9] This is an interesting iterator.  Note that it does not have an "increment" part of the loop; it only initializes and tests the loop. 

[10] We pick up the ContentsInfo object

[11] If it is marked to be erased we will erase it, otherwise not

[12] When we erase an object in a std::map, the return value is the next element in the iteration, so this is how we "increment" to the next element when a deletion occurs

[13] If we do not erase it, then ++ will select the next element in the std::map

[14] If this project is indexing only a single PowerPoint presentation, we delete the one-and-only TOC entry if it is present

[15] We pick up the first element of the std::map

[16] We pick up the ContentsInfo object

[17] If this is a file entry element, we will erase it

[18] Remove the one-and-only file entry.

Handling *INCLUDE

As usual, a generalization like this had a lot of pervasive implications.  Where before I had a single variable representing the "active file", and a variable for the "active file name", none of this would work any longer, when multiple files were being indexed.  Ultimately, there were about 2500 lines of code added just to handle the *INCLUDE directive, and most of those dealt with localizing the information to each element, not having a single global variable.

The FileSet class

To handle this, I created the notion of a "File set".  Initially, the file set is empty.  During an indexing run (starting with File > Open or the Open button), files are added to the file set.  A File Set is a singleton class.  A File Set maps "File IDs" (type FID) to file state information.

The FileSet class uses static methods.  The operations fall into several classes:

FileSet operations Operations which apply to the entire FileSet
  void CloseAll() Closes all open files
  void ReleaseAll() Disconnects all open files from the OLE connection to PowerPoint
  void Clear() Deletes all elements of the FileSet
  UINT QueryAndSave() Queries about saving changed files; if files are selected for saving, saves them.  Returns IDYES, IDNO, IDCANCEL
  BOOL AnyModified() TRUE if any file in the FileSet has its modified flag set; FALSE otherwise
  BOOL IsSingleton() TRUE if the indexing task is indexing exactly one presentation (no *INCLUDE= directives)
FID-discovery functions Operations which discover or create FIDs
 

FID FindByFileName(const CString & filename)

Looks up a file by its name.  The lookup is case-independent.  Full path is required.  If this fails, it returns an invalid FID (FID::Invalid() returns TRUE)
  FID Lookup(const CString & filename) Looks up a file by its name.  The lookup is case-independent.  Full path is required.  If this fails, it creates a new FID entry for the specified file
 

FID Lookup(const CString & filename, const CString & alias)

Looks up a file by its name.  The lookup is case-independent.  Full path is required.  If this fails, it creates a new FID entry for the specified file, and assigns the alias name as its alias.
  FID Add(const CString & filename) Unconditionally adds a filename to the FID table. 
 

FID Add(const Cstring & filename, const CString & alias)

Unconditionally adds a filename to the FID table, and assigns it the specified alias name.
FID-based functions Operations which operate on individual FIDs
  BOOL CheckFileId(FID fid) TRUE if the FID has a valid entry in the table
  void SetAlias(FID fid, const CString & alias) Sets the specified alias string for the FID
  CString GetAlias(FID fid) Returns the alias for the FIDIf no alias has been set, this will return the file name part of the file associated with the FID.
  CString GetFile(FID fid) Returns the complete path name for the FID.
 

void SetAlreadyOpen(FID fid, BOOL mode)

Sets the "presentation-was-already-open-outside-the-indexer" flag to the specified mode.
  BOOL GetAlreadyOpen(FID fid) Returns the "presentation-was-already-open-outside-the-indexer" flag
  void SetTOC(FID fid) Indicates that a *HEADINGn directive has been seen for this FID.
 

void SetReading(FID fid, BOOL mode = TRUE)

Indicates if we are actively reading the file represented by FID (used to detect circular *INCLUDE directives)
  BOOL IsReading(FID fid) Returns the state set by SetReading
PowerPoint operations Operations which act on the PowerPoint presentation via the automation interface
  BOOL GetReadOnly(FID fid) Indicates if the presentation designated by the FID is read-only
  BOOL Save(FID fid) Saves the presentation designated by FID.
 

int GoToSlide(FID fid, int SlideID, BOOL activate = TRUE)

Goes to the slide designated by SlideID, which is an actual "slide ID" (unique serial number of the slide within a presentation).  Returns the slide number.
  int GetCount(FID fid) Returns the count of slides (used to set scrollbar limits)
  BOOL Open(FID fid) Opens the presentation.  Establishes an automation interface with it.
  void Close(FID fid) Closes the presentation and disconnects the automation interface to it.
  CSlide GetSlide(FID fid, int n) Returns a CSlide object for the slide whose position is n
 

BOOL SetPresentation(CPresentation presentation)

Sets the CPresentation object for this FID.
  void Release(FID fid) Releases the connection between the indexer and PowerPoint for this FID.

Change Log

Change details

Version Nature

SLOC

Change
1.2.0.0 Release   First release
1.2.0.1 Feature 4 Change alphabetic sort so that if the two strings compare equal case-independent, do a case-dependent compare at that point. Thus 'aaa' will sort AFTER 'AAA' but before 'BBB'
2.0.0.2 Feature 236 Support PowerPoint automation
2.0.0.3 Feature 331 Read PowerPoint presentation directly
2.0.0.4 Feature 385 Use notes area to hold indexing keywords
2.0.0.5 Bug Fix 5 Update controls after changing upper bound
2.0.0.6 Internal 36 Regularize name of slide number variable
2.0.0.7 Bug Fix 1 Do not check for file consistency during busy phases
2.0.0.8 Bug Fix 5 Do not return file-inconsistent information if file data is not set
2.0.0.9 Bug Fix 2 Update window counts after clearing
2.0.0.10 Performance 2 Preallocate indexing array before filling it to send to thread
2.0.0.11 Bug Fix 1 Double-click of word in word list (move to NULL list) followed by attempt to restore would fail to restore
2.0.0.12 Cosmetic 16 Display disabled text of list control in gray
2.0.0.13 Bug Fix 4 If the presentation was already opened, do not close it after read phase.
2.0.0.14 Feature 45 Added *SKIPCONTENTS=1, *USECONTENTS=1 rules
2.0.0.15 Feature 300 Capability of adding directly to the Notes section of a slide
2.0.0.16 Bug Fix 3 catch(COleException*) clauses where error returns are involved now ::SetLastError
2.0.0.17 Feature 52 Add option to save modified PowerPoint presentation
2.0.0.18 Cosmetic 25 Do not allow Notes buttons if presentation is read-only
2.0.0.19 Bug Fix 1 Fixed an access fault
2.0.0.20 Bug Fix 4 Update times when 'no' is selected for file updates so it doesn't keep prompting!
2.0.0.21 Bug Fix 10 HTML-escape & < > characters
2.0.0.22 Performance 1 Changed MAX_MESSAGE_QUANTUM to 20
2.0.0.23 Bug Fix 2 When initiating a PowerPoint read, treat as an implicit OnFileNew
2.0.0.24 Bug Fix Move setting of file names to after OnFileNew
2.0.0.25 Bug Fix 1 Clear selection display when starting new file
2.0.0.26 Feature 82 Added *HEADING1, *HEADING2
2.0.0.27 Cosmetic 2 Darkened highlighting color for notes objects in word list
2.0.0.28 Feature 312 Added *TODO, report errors
2.0.0.29 Cosmetic 13 Make bottom right rectangle in fast index look nice by defining dummy rectangle when there are an odd number of items
2.0.0.30 Cosmetic 22 If there is a range of contiguous pages > 2, print out range with hyphen in between
2.0.0.31 Cosmetic 12 Allow word list to scroll horizontally
2.0.0.32 Bug Fix 28 Rewrote Notes line processing to avoid automation bug that truncates lines.  Side effect: it now runs much faster!
2.0.0.33 Feature 343 Added multilevel indexing
2.0.0.34 Bug Fix 5 Handle exception on get_Save check if presentation is closed before program is exited
2.0.0.35 Feature 27 Writes Table of Contents as a separate file
2.0.0.36 Feature 218 Split Notes and Errors so it is not necessary to skip over *TODO values to find errors
2.0.0.37 Feature 10 Added *HEADING3
2.0.0.38 Feature 601 Added floating button pad to make it easier to use on single-monitor systems
2.0.0.39 Feature 31 Added test to report an error for any rule of the form "*rulename=", that is, nothing follows the =.
2.0.0.40 Bug fix 1 Fixed bug that lost the default open file name for the Open button/menu item; consequence of a cut-and-pasto from 2.0.0.24
2.0.0.41 Feature 2470 Support *INCLUDE
2.0.0.42 Internal 49 Internal renaming of some structures to make sense in the expanded context of 2.0.0.41
2.0.0.43 Internal 3 Moved a class definition from standalone to being a component of CIndexerDlg.
2.0.0.44 Bug fix 5 Fixed syntax error in TOC file; do not write TOC file if no table-of-contents
2.0.0.45 Internal 5 Cleanup of empty-array tests
2.0.0.46 Internal 7 Put #pragma once in PowerPoint interface files generated by ClassWizard
2.0.0.47 Internal 168 Improvements in exception reporting of PowerPoint interface exceptions
2.0.0.48 Internal 174 Normalized names of certain identifiers to simply legibility
2.0.0.49 Feature 301 Support flyover tooltips to explain error lines
2.0.0.50 Feature 121 Added header to frame set
2.0.0.51 Internal 14 Reorganization of HTML functions to separate module
2.0.0.52 Feature 40 Check limits of numeric values, make sure parameter is numeric
2.0.0.53 Internal 38 Moved HTML color scheme information into CSS
2.0.0.54 Bug Fix 31 If the window is stretched or shrunk horizontally, adjust the rightmost column of the slide information display to follow it.
2.0.0.55 Feature 0 Enable the Maximize box.
2.0.0.56 Cosmetic 8 Do not display slide # in error messages if slide # is 0 (meaning general OLE error not correlated with a specific slide)
2.0.0.57 Internal 11 Cleaned up some formatting calls
2.0.0.58 Internal 36 Allow flyover help to use dynamically computed text
2.0.0.59 Internal 15 Put program version into HTML header "generated-by"
2.0.0.60 Internal 1 Set registry key name (although it is never used)
2.0.0.61 Cosmetic 9 Put extra space around floating button bar text display
2.0.0.62 Internal 202 Track memory allocations and frees in debug mode to detect memory leak situations
2.0.0.63 Internal 7 Ignore "object disconnected" error during close operations
2.0.0.64 Feature 86 Show progress dialog while loading sorted slide data after reader phase completes
2.0.0.65 Cosmetic 2 Stop button has disabled state bitmap, looks nicer
2.0.0.66 Feature 127 Expand the word list and hyphenates list if window is expanded, by moving the skip-words and arrow keys to the right as window expands
2.0.0.67 Cosmetic 1 If the input line had ,, to indicate a non-field-separating comma, display the ,, in the word list and other contexts where the composite string is shown
2.0.0.68 Feature 126 Added first, prev, next, last contents-indexed buttons
2.0.0.69 Bug Fix 1 Clear errors array when new document is loaded
2.0.0.70 Feature 83 Added Word List scanning to floating button box
2.0.0.71 Internal 41 Renaming to make naming conventions consistent
2.0.0.72 Bug Fix 164 Disable floating button box controls during computational periods
2.0.0.73 Bug Fix 1 Range test caused by switching between errors, notes, and contents where last selection is beyond last selection of the previous group has been fixed.
2.0.0.74 Feature 20 Added launch-floating-buttons button
2.0.0.75 Feature 56 Added file-finder dropdown
2.0.0.76 Bug Fix 4 Handle situation where a prompt to save causes runaway scrolling in another control (a Windows bug workaround)
2.0.0.77 Internal 25 Include FileID in trace messages when OLE exception occurs
2.0.0.78 Feature 44 Flyover help over Word List, Hyphenate List, and Null Word List
2.0.0.79 Feature 17 Display count of content-indexed lines
2.0.0.80 Cosmetic 5 Increase size around flyover help
2.0.0.81 Feature 130 Added incremental search to the word list
2.0.0.82 Bug Fix 3 Disable OLE timeout to avoid annoying messages
2.0.0.83 Internal 14 Renaming for consistency
2.0.0.84 Internal 52 Reorganized thread state management to a single class
2.0.0.85 Bug Fix 6 If program not connected to object, read-only test always returns TRUE (control updates may ask for state before connection established)
2.0.0.86 Bug Fix 2 Properly clear all fields when opening new file from MRU list
2.0.0.87 Internal 12 Added tracing to file close operations
2.0.0.88 Bug Fix 41 Deal with serious problems of COleDispatchDriver and friends
Total REQs             88
Changed lines        7243
Changes              7957
Total source lines  29280
Total files           119

Index

Index

A

AddElement, CWordList
AddMRU
AfxGetApp
AfxOleGetMessageFilter
AppendMenu, CMenu
Arrow buttons

C


CAboutDlg::GetVer
CAboutDlg::OnInitDialog
CArrowButton, class
CanonicalizeCase, CIndexerDlg
CIndexData::DrawItem
CIndexerData::OnMouseLeave
CIndexData::OnMouseMove
CIndexerDlg::CanonicalizeCase
CIndexerDlg::HandleClosePending
CIndexerDlg::LoadMRU

CIndexerDlg::AddMRU
CIndexerDlg::OnActivateApp
CIndexerDlg::OnClose
CIndexerDlg::OnInitMenuPopup
CIndexerDlg::OnMRU
CIndexerDlg::OnReaderThreadFinished
CIndexerDlg::OnSetProgressRange
CIndexerDlg::OnSlideLine
CIndexerDlg::OnThreadOpenFailure
CIndexerDlg::PopulateMRU
CIndexerDlg::ReadThread
CIndexerDlg::RecentFileCheck
CIndexerDlg::ReportOpenError
CIndexerDlg::RuleNameToID
CIndexerDlg::SaveMRU
CIndexerDlg::updateControls
class CArrowButton
class CIndexData
class FID
class FileSet
class lessstr
class RULES
class SlideData
CListBox,
owner-draw
CListCtrl, owner-draw
CListCtrl::SubItemHitTest

CMenu::AppendMenu
CMenu::EnableMenuItem
CMenu::GetMenuItemInfo
CMenu::SetMenuItemInfo
CProgressCtrl::SetRange32
::CreateWindow
::CreateWindowEx
CWinApp
trap
CWnd::GetLastActivePopup
CWordList::AddElement
CWordList::DeleteElement
CWordList::DrawItem
CWordList::PreSubclassWindow

D


DECLARE_MESSAGE
DeleteElement, CWordList
DrawItem, CIndexData
DrawItem, CWordList

E


EnableBusyDialog, AfxOleGetMessageFilter
EnableMenuItem, CMenu
EnableNotRespondingDialog, AfxOleGetMessageFilter
ErrorString

F

FID, class
FILETIME
FileSet, class
FileState::Compare
FileState::GetDiskTimeStamp

G


::GetFileTime
GetLastActivePopup, CWnd

GetMenuItemInfo, CMenu
::GetModuleFileName
::GetFileVersionInfo
::GetFileVersionInfoSize
GetVer, CAboutDlg

H


HandleClosePending, CIndexerDlg

L

lessstr, class
LoadMRU, CIndexerDlg
LVHITTESTINFO

M


MessageQueue::PostQueuedMessage
MRU

O

OnActivateApp, CIndexerDlg
OnClose, CIndexerDlg
OnInitDialog, CAboutDlg

OnInitMenuPopup, CIndexerDlg
OnMRU, CIndexerDlg
OnReaderThreadFinished, CIndexerDlg
ON_REGISTERED_MESSAGE
OnSetProgressPos, CIndexerDlg

OnSetProgressRange, CIndexerDlg
OnSlideLine, CIndexerDlg
OnThreadOpenFailure, CIndexerDlg
Owner-draw CListBox
Owner-draw CListCtrl

P


PBM_SETBARCOLOR
PopulateMRU, CIndexerDlg
PostQueuedMessage, MessageQueue
PreSubclassWindow, CWordList

R


ReadThread, CIndexerDlg
RecentFileCheck, CIndexerDlg
ReportOpenError, CIndexerDlg
RuleNameToID, CIndexerDlg
RULES, class

S


SaveMRU, CIndexerDlg
::SetLastError
SetMenuItemInfo, CMenu
SetRange32, CProgressCtrl
SlideData, class
std::map

T


_tcstoul

U


updateControls, CIndexerDlg
UWM_SET_PROGRESS_POS
UWM_SET_PROGRESS_RANGE
UWM_THREAD_OPEN_FAILURE

V
::VerQueryValue
VS_FIXEDFILEINFO

W

WM_GETFONT
WM_MOUSELEAVE
WM_SETFONT

[Dividing Line Image]

The views expressed in these essays are those of the author, and in no way represent, nor are they endorsed by, Microsoft.

Send mail to newcomer@flounder.com with questions or comments about this web site.
Copyright © 2007 FlounderCraft Ltd./The Joseph M. Newcomer Co. All Rights Reserved
Last modified: May 14, 2011