Classic VB Corner

Finding an Associated Executable

After using a given API for a decade or more, you tend to just take it for granted that it works. Karl Peterson shows how he worked around a challenge when it didn't.

Not long after I submitted my last column, which showed among other things how to find what application would launch a given document file, I ran across an interesting discussion. An old acquaintance of mine had noticed that FindExecutable wasn't returning results for the .ACCDB file extension.

The discussion evolved and it was discovered that there were numerous 5-letter file extensions FindExecutable didn't like, but others it had no trouble with whatsoever. The pattern was totally random, as were many of the made-up file extensions. All in all, it was a fine demonstration that the ClassicVB community is still alive and thriving in the public newsgroups, as the problem was diagnosed and other methods were found. I'd like to share my own new strategy with you. My thanks to Tony Toews, Mike Williams and "Nobody" for finding the problem and contributing to its solution. It's a good thread that's worth reading.

If you haven't gotten around to moving beyond Office 2003, a little background may be in order. ACCDB is the new extension used by Access 2007 for its data files. Microsoft would like us to view MDBs as relics of the past. Unfortunately, they didn't really test out this concept against their tried and true set of file APIs. If you pass the full path and name of an ACCDB file to FindExecutable, it will return SE_ERR_NOASSOC indicating there is no associated application for that filetype. However, Windows certainly knows the filetype, as it shows "Microsoft Office Access 2007 Database" in Explorer. (Oddly enough, FindExecutable returns correct results on Windows 98. Hmmm.)

Turns out, there's another API function that'll not only tell you what the associated executable is, but much more. AssocQueryString provides numerous association strings, such as the command line to launch a document, the friendly document and application names, DDE topics and commands, and so on. And, best of all, AssocQueryString has no troubles whatsoever with 5-letter file extensions.

That said, to make this solution work on older platforms (NT4, 98, 95) you need to have Internet Explorer 5 (or higher) installed. That means we need to use both solutions together, to assure best possible results. If all you're looking for is the associated executable, the old time FindExecutable will work in nearly all cases.

So, building on our last project, my new FindApplication routine looks like this:

Public Function FindApplication(ByVal DocName As String) As String
   Dim sRet As String
   ' Try FindExecutable first...
   sRet = FindApplication1(DocName)
   If Len(sRet) = 0 Then
      ' and AssocQueryString otherwise.
      If Exported("shlwapi", "AssocQueryStringA") Then
         sRet = FindApplication2(DocName)
      End If
   End If
   FindApplication = sRet
End Function

The FindApplication1 function uses the previous method of calling FindExecutable, and FindApplication2 uses the new method of calling AssocQueryString. Note that I'm also testing to be sure AssocQueryString is available, to avoid an unhandled error in the unlikely event my code finds itself running on a system with IE4. FindApplication2 looks like this:

Private Function FindApplication2(ByVal DocName As String) As String
   ' Minumum OS: Windows 2000, Windows NT 4.0 with IE5,
   '             Windows 98, Windows 95 with IE5
   Dim n As Long
   Dim Buffer As String
   Dim BufLen As Long
   
   ' Reduce document name to just extension, taking care to
   ' allow cases where just extension itself is passed.
   n = InStrRev(DocName, ".")
   If n Then
      DocName = Mid$(DocName, n)
   Else
      DocName = "." & DocName
   End If
   
   ' Prepare buffer for results.
   Buffer = Space$(MAX_PATH)
   BufLen = Len(Buffer)
   
   ' Use the AssocQueryString API to find application
   ' associated with passed document extension.
   n = AssocQueryString(0&, ASSOCSTR_EXECUTABLE, _
         DocName, "open", Buffer, BufLen)
   If n = S_OK Then
      FindApplication2 = Left$(Buffer, BufLen - 1)
   End If
End Function

One big difference between these two API functions is that AssocQueryString is just returning results for a given file extension, whereas FindExecutable requires the complete filename of an actually existing file. So in this regard alone, the newer function is far more versatile, in that you no longer need to create a temporary file to determine random associations. I've updated the Which sample on my site with this code, in case you already downloaded the original and would like to update that.

The next logical question is, what are you going to do with that association? Well, odds are, you're planning to launch the document file, right? For nearly all purposes, of course, ShellExecute will work just fine for that. But there are situations where it won't, for whatever reason, and you'd simply like to be able to fire it off yourself using VB's own Shell or the CreateProcess API. Here's another case where FindExecutable doesn't provide quite enough information. For example, if you have the Windows Picture and Fax Viewer associated with GIF or JPG files, FindExecutable will point you straight at C:\WINDOWS\system32\shimgvw.dll. That's not something you would ordinarily be able to execute!

Well, one of the other strings provided by AssocQueryString is the launch command for documents. In the case just mentioned, you'd get back:

rundll32.exe C:\WINDOWS\system32\shimgvw.dll,ImageView_Fullscreen %1

Just about there! As you're probably aware, that %1 parameter represents the filename to be launched. The lack of quotes around it is something that ought to set off a quiet alarm. Most applications that work with long filenames will use "%1" instead to help delineate the actual filename. Reading up on rundll32, we can confirm that it doesn't understand how to parse quoted strings. So, you'll need to chop the filename down to its short version.

Microsoft provides a method to derive a short filename from a long filename in KB175512. Of course, like most KB code, it needs some good scrubbing to really be presentable. Here's my cleaned-up version of that routine:

Private Declare Function GetShortPathName Lib "kernel32" _
      Alias "GetShortPathNameA" (ByVal lpszLongPath As String, _
      ByVal lpszShortPath As String, ByVal cchBuffer As Long) As Long

Public Function GetShortName(ByVal LFN As String) As String
    Dim nRet As Long
    Dim Buffer As String
    
    ' Determine size needed for buffer.
    nRet = GetShortPathName(LFN, vbNullString, 0&)
    Buffer = Space$(nRet)

    ' Call the function, and clean results.
    nRet = GetShortPathName(LFN, Buffer, Len(Buffer))
    GetShortName = Left$(Buffer, nRet)
End Function

Given all that, if LFN represents the long filename of a document you'd like to launch, you would build a Shell string by taking the ASSOCSTR_COMMAND string returned by AssocQueryString and replacing "%1" with the document filename. Then, in case there wasn't a "%1" but there is a %1, you'd replace %1 with GetShortName(LFN). In cases where there wasn't a %1 at all, you're kind of left on your own, but can probably assume the program accepts the filename as the first/only parameter on its command line. Safest would be to, again, pass the short filename in these cases.

I've added a new Assoc sample to my site, which demonstrates about a dozen different strings returned by AssocQueryString (see figure below). It lets you enter any extension and verb (open, print, etc.) pair to see what Windows knows about those. The sample provides a drop-in ready class that'll work in any VB6 or VBA application. VB5 users would need to uncomment a replacement function for InstrRev.


[Click on image for larger view.]

 

About the Author

Karl E. Peterson wrote Q&A, Programming Techniques, and various other columns for VBPJ and VSM from 1995 onward, until Classic VB columns were dropped entirely in favor of other languages. Similarly, Karl was a Microsoft BASIC MVP from 1994 through 2005, until such community contributions were no longer deemed valuable. He is the author of VisualStudioMagazine.com's new Classic VB Corner column. You can contact him through his Web site if you'd like to suggest future topics for this column.

comments powered by Disqus

Featured

  • Compare New GitHub Copilot Free Plan for Visual Studio/VS Code to Paid Plans

    The free plan restricts the number of completions, chat requests and access to AI models, being suitable for occasional users and small projects.

  • Diving Deep into .NET MAUI

    Ever since someone figured out that fiddling bits results in source code, developers have sought one codebase for all types of apps on all platforms, with Microsoft's latest attempt to further that effort being .NET MAUI.

  • Copilot AI Boosts Abound in New VS Code v1.96

    Microsoft improved on its new "Copilot Edit" functionality in the latest release of Visual Studio Code, v1.96, its open-source based code editor that has become the most popular in the world according to many surveys.

  • AdaBoost Regression Using C#

    Dr. James McCaffrey from Microsoft Research presents a complete end-to-end demonstration of the AdaBoost.R2 algorithm for regression problems (where the goal is to predict a single numeric value). The implementation follows the original source research paper closely, so you can use it as a guide for customization for specific scenarios.

  • Versioning and Documenting ASP.NET Core Services

    Building an API with ASP.NET Core is only half the job. If your API is going to live more than one release cycle, you're going to need to version it. If you have other people building clients for it, you're going to need to document it.

Subscribe on YouTube