Announcement

Collapse
No announcement yet.

Class Object: Recent Files Class

Collapse
X
 
  • Filter
  • Time
  • Show
Clear All
new posts

  • Class Object: Recent Files Class

    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.
    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
    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.
    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
    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.
    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
    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.
    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
    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.
    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
    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.
    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
    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.
    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
    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
    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
    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.
    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
    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.
    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
    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.
    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
    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?
    Attached Files
    Furcadia, an interesting online MMORPG in which you can create and program your own content.

  • #2
    Originally posted by colin glenn View Post
    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....
    Colin,

    That's a great use of objects! I'd like to see more of this kind of thing posted.

    I wrote a Windows INI object about ten years ago in Visual Basic when Microsoft first introduced COM objects in VB. That object has been extremely useful over the years.

    I would like to re-write it in PowerBasic but there are a few things missing in PB that are holding me back (nothing insurmountable but I need a Dictionary object or something similar.) Also, my current INI object is pretty much self documented but PB doesn't let you insert help text for individual Methods and Properties. I'd hate to lose the ability to browse the INI object and see what each Method and Property does in plain english.

    If you are interested in seeing what my object looks like, I documented it during the design process and the docs can be found here:

    http://www.echo-dev.com/wini/

    Keep up the OOP!

    -Wes
    Last edited by WesWesthaver; 7 Apr 2009, 02:34 PM.

    Comment


    • #3
      but I need a Dictionary object or something similar
      The Dictionary object is not something that VB has and PB lacks. It is available in the Microsoft Scripting Runtime (scrrun.dll). Use a browser to generate the interface declarations.
      Forum: http://www.jose.it-berater.org/smfforum/index.php

      Comment


      • #4
        :coffee:
        Originally posted by José Roca View Post
        The Dictionary object is not something that VB has and PB lacks.
        Or if one if feeling ambitious, a properly written class can manifest the same interface as the Dictionary object, with the ability to extend it fairly easily.
        Furcadia, an interesting online MMORPG in which you can create and program your own content.

        Comment


        • #5
          Originally posted by José Roca View Post
          The Dictionary object is not something that VB has and PB lacks. It is available in the Microsoft Scripting Runtime (scrrun.dll). Use a browser to generate the interface declarations.
          I guess I wasn't clear in my intentions for re-writing my INI object. If I'm going to ditch the VisualBasic runtime, I also want to ditch the dependency on the scrrun.dll. The PB version should be compact and without such dependencies.

          But that's going to mean that I not only have to re-write my INI object but write a replacement for the Dictionary object as well. Still, it's a project I'd like to take on as time permits.

          Does anyone feel like writing a Dictionary object?

          -Wes
          Last edited by WesWesthaver; 7 Apr 2009, 02:37 PM.

          Comment

          Working...
          X