So I wanted to put in a recent files menu selection for my program, and was going to tackle it with a bunch of functions and sub-routines, when I had another brilliant idea, make a Recent Files Class. The premise is simple, you create the single global class object, and pass it the two menu strings which point to the popup menu you will be using for the recent files entry on your menu. The class handles all the dirty details of maintaining the order of the recent files, mainly last one in is first on the list, and provides a bit of additional functionality as well like clearing "dead entries" from the list and ensuring that the menu will at least contain the disabled item "Empty" when there's none. So rather than either having to maintain several globals, or constantly having to locate the menu handle each time you want it, you have a single global or static object to work with. You do need to include ShlWAPI because the class calls on PathCompactPathEx to produce an elipse'd path if you so desire to limit the length of each entry on the menu, the class maintains the full path for all file names given to the class.
Simple initialization, requires one of my rants about PB classes not having a constructor, ...
Build menu is an internal method for reconstructing the menu when you add / remove file names from the class, it will also handle the case where you've purged all the entries by using the function VerifyFiles and none of the entries are valid, so it sticks the disabled "Empty" entry into the menu.
This internal method is for gaining the popup handle to the menu we want to use for recent files. The only "limitation" is that it must be a popup from a main menu item, like "File -> Recent". If you do things differently, you will need to modify the AssignMenu method to work for your application.
NOTE: This method REMOVES any &'s from the retrieved menu text, so if you do have && in either the main menu string or the popup's string, do NOT pass the &'s to the AssignMenu method otherwise this will FAIL. It will also fail on case and spaces.
PackPath is a wrapper for the PathCompactPathEx, it's only called from one place, however, it's here and if so desired, you can either move this to being an interface method or add a wrapper to the interface methods to call on this.
The meat of the class, first up is the method used to handle the addition of a new file name to the list, although I call it "AppendRecent", that's a lie. The function inserts it into the beginning of the list. It also tests for the presence of the "Empty" entry, and if found, deletes it before calling on BuildMenu. If the file name was already on the list, it's bumped to the top. This also ensures that you don't end up with duplicates on the list, which would just plain look silly to the end user.
Main "entry" point for the function, requires my rant about constructors.
You need to pass this method several things, the window handle, the two strings leading to the menu item which you are using for your recent files popup, the starting ID for your WM_COMMAND handling, and how many elements you want on the menu. In theory, it's probably bottomless, limited only by the amount of memory and the limits of LONG, or rather just how many IDM's you can use, ...
The command accelerator keyboard shortcut's will tag up to 36 elements on the array, this is probably way more than enough to handle most applications. Once the menu's popup handle is gained, the method deletes all existing entries on the menu and tags the "Empty" element into the menu. Do note that the method will bail if it cannot get the handle to the menu, and hence you should test for a %TRUE return from the call to ensure that it's locked and loaded. And also, when passing it the number of elements to manage, it's the one based value, if you want ten entries, pass it 10.
The class does not bother storing the number of entries you set up from the above call, the FileCount method just returns the ubound of the FileNames array plus one. This section also contains a method for finding out how many entries are used, and a GET / SET for using the MaxLength on passed file names/paths.
The GET RecentByID could probably use a counterpart to GET RecentByIndex, should be easy to code if you want it, otherwise if you need to get all the items from the recent files you can also just run:
FOR myID = %IDM_RECENTFILES to %(IDM_RECENTFILES - (%TOTALRECENT - 1))
And achieve the same thing.
To be notated: It is very important that when you set up your main program, that you leave enough spaces (unused numbers) from your starting ID for your recent files processing and the next available ID used by your program, otherwise seriously unexpected results will occur depending on the order in which your construct your %WM_COMMAND processor.
%IDM_RECENTFILES = 150
%TOTALRECENT = 10
%IDM_NEXTCOMMANDID = 160
If you use "helper text in the status bar when the mouse is over a menu item", you may want to also have the functionality to get the file name from this class to use in the status bar, especially if you use the MaxLength property of the class to shorten the names on the menu.
RemoveAll is a public method because you may want to clear all the items from the menu. It's used internally in the class when starting up, and puts the "Empty" menu item into the menu.
RemoveByID permits you to selectively delete a single entry in the menu. The method returns %TRUE to indicate that it did remove the selected entry.
VerifyFiles is a method to determine the validity of all the files in the array, once it's processed all the file names, (removing any non-accessible files), it calls on the BuildMenu class method to rebuild the menu.
And there you have it, a rather simple means to add a Recent Files menu to any program without much fuss.
Attached ZIP contains the class and a simple test program for checking it out.
Note: If you actually have the two fake entries in the test program, what the heck are you doing with a clone of my drive?
Code:
' recent file manager class %IDM_ISEMPTY = &hEFFF ' selected to be one less than minimum WS_SYSCOMMAND $KEYSTRING = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ" ' beyond this, the menu entries contain no command accelerator key letter. CLASS RecentFileClass INSTANCE FileNames() AS STRING INSTANCE MaxPathLength AS LONG ' if 0, do not shorten ' max path INSTANCE hWindow AS DWORD INSTANCE hMenu AS DWORD INSTANCE StartID AS DWORD CLASS METHOD CREATE() DIM FileNames(0) MaxPathLength = 0 hWindow = 0 hMenu = 0 StartID = 0 END METHOD
Build menu is an internal method for reconstructing the menu when you add / remove file names from the class, it will also handle the case where you've purged all the entries by using the function VerifyFiles and none of the entries are valid, so it sticks the disabled "Empty" entry into the menu.
Code:
CLASS METHOD BuildMenu LOCAL mCount, mIndex AS LONG LOCAL mString AS ASCIIZ * %MAX_PATH + 1 LOCAL aChar AS STRING LOCAL apiReturn AS LONG ' walk the menu and append / modify elements in the menu mCount = GetMenuItemCount(hMenu) FOR mIndex = 0 TO UBOUND(FileNames) IF LEN(FileNames(mIndex)) = 0 THEN ' zap any excess entries in the menu ' we will assume that we can zap it by command ' due to how we build the menu apiReturn = DeleteMenu(hMenu, StartID + mIndex, %MF_BYCOMMAND) ' we also need to check if we zapped last item in the menu mCount = GetMenuItemCount(hMenu) IF mCount = 0 THEN AppendMenu hMenu, %MF_STRING OR %MF_GRAYED, _ %IDM_ISEMPTY, "Empty" ' nothing more to do! EXIT FOR END IF ELSE ' check if we can assign a command accelerator keyboard shortcut IF (mIndex < LEN($KEYSTRING)) THEN aChar = "&"& MID$($KEYSTRING, mIndex + 1, 1) &" " ELSE aChar = "" END IF mString = aChar & IIF$(ISTRUE MaxPathLength, _ ME.PackPath(FileNames(mIndex)), FileNames(mIndex)) IF mIndex < mCount THEN ' modify existing menu elements apiReturn = ModifyMenu(hMenu, mIndex, _ %MF_BYPOSITION OR %MF_ENABLED OR %MF_STRING, _ StartID + mIndex, mString) ELSE ' oh hey, append the sucker apiReturn = AppendMenu(hMenu, %MF_ENABLED OR %MF_STRING, _ StartID + mIndex, mString) END IF END IF NEXT mIndex END METHOD
NOTE: This method REMOVES any &'s from the retrieved menu text, so if you do have && in either the main menu string or the popup's string, do NOT pass the &'s to the AssignMenu method otherwise this will FAIL. It will also fail on case and spaces.
Code:
CLASS METHOD MenuHandle(mName AS STRING, pMenu AS DWORD) AS DWORD LOCAL mIndex, mCount, sCount, iMenu AS DWORD LOCAL mString AS ASCIIZ * 80 IF (pMenu = 0) THEN iMenu = GetMenu(hWindow) ELSE iMenu = pMenu END IF mCount = GetMenuItemCount(iMenu) FOR mIndex = 0 TO mCount - 1 sCount = GetMenuString(iMenu, mIndex, mString, 79, %MF_BYPOSITION) IF sCount > 0 THEN REPLACE "&" WITH "" IN mString IF mName = mString THEN METHOD = GetSubMenu(iMenu, mIndex) EXIT FOR END IF END IF NEXT mIndex END METHOD
Code:
CLASS METHOD PackPath(oldPath AS STRING) AS STRING LOCAL pszOut, pszSrc AS ASCIIZ * %MAX_PATH LOCAL apiReturn AS LONG pszSrc = oldPath apiReturn = PathCompactPathEx(pszOut, pszSrc, MaxPathLength, 0) IF ISTRUE apiReturn THEN METHOD = pszOut ELSE METHOD = oldPath END IF END METHOD
Code:
INTERFACE RecentFileHandler INHERIT IUNKNOWN METHOD AppendRecent(sFileName AS STRING) AS LONG ' this is a LIE, it sticks it into the first slot LOCAL mReEntry, mIndex AS LONG LOCAL apiReturn AS LONG LOCAL mString AS ASCIIZ * %MAX_PATH + 1 ' test if we have a menu hendle, if not, bail IF hMenu = 0 THEN EXIT METHOD ' check if the entry is already in the list, if so, bump it to the top mReEntry = %TRUE FOR mIndex = 0 TO UBOUND(FileNames) IF FileNames(mIndex) = sFileName THEN ARRAY INSERT FileNames(0) FOR mIndex + 1, sFileName mReEntry = %FALSE EXIT FOR END IF NEXT mIndex ' if the file was already in the list, then above makes this fail IF ISTRUE mReEntry THEN _ ARRAY INSERT FileNames(0), sFileName ' test for %IDM_ISEMPTY and if found, delete it apiReturn = GetMenuString(hMenu, %IDM_ISEMPTY, mString, _ %MAX_PATH, %MF_BYCOMMAND) IF ISTRUE apiReturn THEN _ DeleteMenu hMenu, %IDM_ISEMPTY, %MF_BYCOMMAND ME.BuildMenu END METHOD
You need to pass this method several things, the window handle, the two strings leading to the menu item which you are using for your recent files popup, the starting ID for your WM_COMMAND handling, and how many elements you want on the menu. In theory, it's probably bottomless, limited only by the amount of memory and the limits of LONG, or rather just how many IDM's you can use, ...
The command accelerator keyboard shortcut's will tag up to 36 elements on the array, this is probably way more than enough to handle most applications. Once the menu's popup handle is gained, the method deletes all existing entries on the menu and tags the "Empty" element into the menu. Do note that the method will bail if it cannot get the handle to the menu, and hence you should test for a %TRUE return from the call to ensure that it's locked and loaded. And also, when passing it the number of elements to manage, it's the one based value, if you want ten entries, pass it 10.
Code:
METHOD AssignMenu(pWindow AS DWORD, sBase AS STRING, sRecent AS STRING, _ iStart AS DWORD, iCount AS LONG) AS LONG IF ISTRUE hMenu THEN EXIT METHOD hWindow = pWindow StartID = iStart REDIM FileNames(iCount - 1) hMenu = ME.MenuHandle(sBase, 0) IF hMenu = 0 THEN EXIT METHOD hMenu = ME.MenuHandle(sRecent, hMenu) IF hMenu = 0 THEN EXIT METHOD ' menu is now mine bah hahahaha ME.RemoveAll METHOD = %TRUE END METHOD
Code:
PROPERTY GET FileCount() AS LONG PROPERTY = UBOUND(FileNames) + 1 END PROPERTY PROPERTY GET FilesUsed() AS LONG LOCAL iCount, fIndex AS LONG iCount = 0 FOR fIndex = 0 TO UBOUND(FileNames) IF LEN(FileNames(fIndex)) > 0 THEN INCR iCount END IF NEXT fIndex PROPERTY = iCount END PROPERTY PROPERTY GET MaxLength() AS LONG PROPERTY = MaxPathLength END PROPERTY PROPERTY SET MaxLength(BYVAL nLength AS LONG) IF (nLength > -1) AND (nLength < %MAX_PATH) THEN MaxPathLength = nLength ' process menu entries END IF END PROPERTY
FOR myID = %IDM_RECENTFILES to %(IDM_RECENTFILES - (%TOTALRECENT - 1))
And achieve the same thing.
To be notated: It is very important that when you set up your main program, that you leave enough spaces (unused numbers) from your starting ID for your recent files processing and the next available ID used by your program, otherwise seriously unexpected results will occur depending on the order in which your construct your %WM_COMMAND processor.
%IDM_RECENTFILES = 150
%TOTALRECENT = 10
%IDM_NEXTCOMMANDID = 160
Code:
PROPERTY GET RecentByID(BYVAL RecentID AS DWORD) AS STRING LOCAL fIndex AS LONG LOCAL Results AS STRING fIndex = RecentID - StartID IF (fIndex >= 0) AND (fIndex <= UBOUND(FileNames)) THEN _ Results = FileNames(fIndex) PROPERTY = Results END PROPERTY
RemoveAll is a public method because you may want to clear all the items from the menu. It's used internally in the class when starting up, and puts the "Empty" menu item into the menu.
Code:
METHOD RemoveAll() ' removes all elements, resets the array ' appends "empty" to the menu LOCAL mCount, mIndex AS LONG ' test if we have a menu hendle, if not, bail IF hMenu = 0 THEN EXIT METHOD mCount = GetMenuItemCount(hMenu) IF mCount > 0 THEN FOR mIndex = 1 TO mCount DeleteMenu hMenu, 0, %MF_BYPOSITION NEXT mIndex END IF RESET FileNames() AppendMenu hMenu, %MF_STRING OR %MF_GRAYED, %IDM_ISEMPTY, "Empty" END METHOD
Code:
METHOD RemoveByID(RecentID AS DWORD) AS LONG LOCAL fIndex AS LONG fIndex = RecentID - StartID IF (fIndex >= 0) AND (fIndex <= UBOUND(FileNames)) THEN ARRAY DELETE FileNames(fIndex) METHOD = %TRUE END IF ME.BuildMenu END METHOD
Code:
METHOD VerifyFiles LOCAL mPoint, mIndex AS LONG LOCAL vFileName AS STRING ' test if we have a menu hendle, if not, bail IF hMenu = 0 THEN EXIT METHOD mPoint = 0 DO IF FileNames(mPoint) = "" THEN EXIT DO vFileName = DIR$(FileNames(mPoint)) DIR$ CLOSE IF LEN(vFileName) = 0 THEN ARRAY DELETE FileNames(mPoint) ELSE INCR mPoint IF mPoint > UBOUND(FileNames) THEN EXIT DO END IF LOOP me.BuildMenu END METHOD END INTERFACE END CLASS
Attached ZIP contains the class and a simple test program for checking it out.
Note: If you actually have the two fake entries in the test program, what the heck are you doing with a clone of my drive?

Comment