Inside the PowerPoint Presentation 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.
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.
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.
/**************************************************************************** * 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.
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.
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.
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] }
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.
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
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
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.
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.
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.
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.
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.
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.
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
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.
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.
/**************************************************************************** * 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] }
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] }
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
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
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.
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
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; };
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; };
/**************************************************************************** * 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] }
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 */ }
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.
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
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.
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.
This one is easy. I just reused my Arrow-button class, CArrowButton.
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.
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.
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 |
After a bit of experimentation, I came up with the following class to help me work with PowerPoint:
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
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
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
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
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
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.
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
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
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.
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.
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 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); }
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
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.
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.
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 FID. If 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. |
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 | 2 | 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
The views expressed in these essays are those of the author, and in no way represent, nor are they endorsed by, Microsoft.