dahall / Vanara

A set of .NET libraries for Windows implementing PInvoke calls to many native Windows APIs with supporting wrappers.
MIT License
1.81k stars 197 forks source link

Question: How to find the path of all associated applications through extension? #181

Closed zhuxb711 closed 3 years ago

zhuxb711 commented 4 years ago

I have an extension, but I don’t know how to find all applications that support this extension.

“ShellAssociation" only returns the default application, but I need all of them...

Here is what I want:

.pdf ->
"Microsoft Edge" (full path) "Drawboard" (full path) "Adobe Acrobat Reader" (full path)

dahall commented 4 years ago

ShellAssociation is simply a wrapper around IQueryAssociations. It does not include a capability to walk the association's list. There are numerous articles on StackOverflow on how to do what you are asking. If you find a simple way, send me the code and I will add it to that class.

vessd commented 4 years ago

@zhuxb711 I think you are looking for SHAssocEnumHandlers. https://github.com/dahall/Vanara/blob/c3a63c8c1b8ce1c49aadfa0ad1126078b8aac745/PInvoke/Shell32/ShObjIdl.cs#L1819

zhuxb711 commented 4 years ago

@vessd That's what I need! Thanks a lot~ @dahall Consider the following code:

if (Shell32.SHAssocEnumHandlers(".png", Shell32.ASSOC_FILTER.ASSOC_FILTER_NONE, out Shell32.IEnumAssocHandlers AssocHandlers) == HRESULT.S_OK)
            {
                Shell32.IAssocHandler[] Handlers = new Shell32.IAssocHandler[100];

                if (AssocHandlers.Next(100, Handlers, out uint FetchedNum) == HRESULT.S_OK)
                {
                    Array.Resize(ref Handlers, Convert.ToInt32(FetchedNum));

                    foreach (Shell32.IAssocHandler Handler in Handlers)
                    {
                        try
                        {
                            if (Handler.GetName(out string FullPath) == HRESULT.S_OK && Handler.GetUIName(out string DisplayName) == HRESULT.S_OK)
                            {
                                Debug.WriteLine($"Associate Name: {DisplayName}, Executable Path: {FullPath}");
                            }
                        }
                        finally
                        {
                            Marshal.ReleaseComObject(Handler);
                        }
                    }
                }
            }
dahall commented 4 years ago

Yep. Instead I added the following:

/// <summary>Gets a list of file name extension handlers.</summary>
/// <value>The handlers for this association.</value>
public IReadOnlyList<ShellAssociationHandler> Handlers
{
   get
   {
      if (SHAssocEnumHandlers(Extension, ASSOC_FILTER.ASSOC_FILTER_NONE, out var ieah).Failed)
         return (IReadOnlyList<ShellAssociationHandler>)new List<ShellAssociationHandler>();
      using var pieah = ComReleaserFactory.Create(ieah);
      var e = new Vanara.Collections.IEnumFromCom<IAssocHandler>(ieah.Next, () => { });
      return (IReadOnlyList<ShellAssociationHandler>)e.Select(i => new ShellAssociationHandler(i)).ToList();
   }
}

/// <summary>Represents a handler (executable) for a <see cref="ShellAssociation"/>.</summary>
public class ShellAssociationHandler : ComObjWrapper<ShellAssociationHandler, IAssocHandler>
{
   internal ShellAssociationHandler(IAssocHandler h) : base(h)
   {
   }

   /// <summary>Retrieves the location of the icon associated with the application.</summary>
   /// <value>
   /// An <see cref="IconLocation"/> instance that contains the path and the index of the icon within the resource file for the
   /// application's icon.
   /// </value>
   public IconLocation IconLocation => ComInterface.GetIconLocation(out var p, out var i).Succeeded ? new IconLocation(p, i) : null;

   /// <summary>Indicates whether the application is registered as a recommended handler for the queried file type.</summary>
   /// <value><see langword="true"/> if this instance is recommended; otherwise, <see langword="false"/>.</value>
   /// <remarks>
   /// <para>
   /// Applications that register themselves as handlers for particular file types can specify whether they are recommended
   /// handlers. This has no effect on the actual behavior of the applications when launched. It is simply provided as a hint to
   /// the user and a value that the UI can utilize programmatically, if desired. For example, the Shell's <c>Open With</c> dialog
   /// separates entries into <c>Recommended Programs</c> and <c>Other Programs</c>.
   /// </para>
   /// <para>
   /// Note that program recommendations may change over time. One example is provided when the user chooses an application from
   /// the <c>Other Programs</c> of the <c>Open With</c> dialog to open a particular file type. That may cause the Shell to
   /// "promote" that application to recommended status for that file type. Because the recommended status may change over time,
   /// applications should not cache this value, but query it each time it is needed.
   /// </para>
   /// </remarks>
   public bool IsRecommended => ComInterface.IsRecommended() == HRESULT.S_OK;

   /// <summary>Retrieves the full path and file name of the executable file associated with the file type.</summary>
   /// <value>A string that contains the full path of the file, including the file name.</value>
   public string Name => ComInterface.GetName(out var n).Succeeded ? n : null;

   /// <summary>Retrieves the display name of an application.</summary>
   /// <value>A string that contains the display name of the application.</value>
   public string UIName => ComInterface.GetUIName(out var n).Succeeded ? n : null;

   /// <summary>Indicates whether the current object is equal to another object of the same type.</summary>
   /// <param name="other">An object to compare with this object.</param>
   /// <returns>true if the current object is equal to the <paramref name="other"/> parameter; otherwise, false.</returns>
   public override bool Equals(IAssocHandler other) => Name.Equals(other.GetName(out var n).Succeeded ? n : null);

   /// <summary>Returns a hash code for this instance.</summary>
   /// <returns>A hash code for this instance, suitable for use in hashing algorithms and data structures like a hash table.</returns>
   public override int GetHashCode() => Name.GetHashCode();

   /// <summary>Directly invokes the associated handler.</summary>
   /// <param name="items">A sequence of selected items on which to invoke the handler.</param>
   /// <remarks>
   /// <para>
   /// IAssocHandler objects are typically used to populate an <c>Open With</c> menu. When one of those menu items is selected,
   /// this method is called to launch the chosen application.
   /// </para>
   /// <para>Invoke and CreateInvoker</para>
   /// <para>
   /// The IDataObject used by these methods can represent either a single file or a selection of multiple files. Not all
   /// applications support the multiple file option. The applications that do support that scenario might impose other
   /// restrictions, such as the number of files that can be opened simultaneously, or the acceptable combination of file types.
   /// </para>
   /// <para>
   /// Therefore, an application often must determine whether the handler supports the selection before trying to invoke the
   /// handler. For example, an application might enable a menu item only if it has verified that the selection in question was
   /// supported by that handler.
   /// </para>
   /// </remarks>
   public void Invoke(params ShellItem[] items)
   {
      if (items.Length == 0) throw new ArgumentException("", nameof(items));
      // TODO: Call for multiple invokeable items
      if (items.Length > 1) throw new NotImplementedException();
      ComInterface.Invoke(new ShellDataObject(items)).ThrowIfFailed();
   }

   /// <summary>Sets an application as the default application for this file type.</summary>
   /// <param name="description">
   /// <para>A string that contains the display name of the application.</para>
   /// </param>
   public void MakeDefault(string description) => ComInterface.MakeDefault(description).ThrowIfFailed();
}
dahall commented 4 years ago

Thanks @vessd for the pointer. This will get pushed once I'm done testing.

zhuxb711 commented 4 years ago
new IconLocation(p, i)

This will failed if it is UWP

dahall commented 4 years ago

@zhuxb711 Why will the IconLocation constructor fail?

zhuxb711 commented 4 years ago

Because it throw an exception and the message is "resourceIndex could not be 0"

And other problem is you should use CreateInvoker when items.Length >1 image

zhuxb711 commented 4 years ago

It is recommended to increase a new property "IsDefault" to determine whether it is the default association. You could get it from the code below:

uint Length = 0;

if (ShlwApi.AssocQueryString(ShlwApi.ASSOCF.ASSOCF_VERIFY, ShlwApi.ASSOCSTR.ASSOCSTR_EXECUTABLE, System.IO.Path.GetExtension(Path).ToLower(), null, null, ref Length) == HRESULT.S_FALSE)
{
          StringBuilder Builder = new StringBuilder(Convert.ToInt32(Length));

          if (ShlwApi.AssocQueryString(ShlwApi.ASSOCF.ASSOCF_VERIFY, ShlwApi.ASSOCSTR.ASSOCSTR_EXECUTABLE, System.IO.Path.GetExtension(Path).ToLower(), null, Builder, ref Length) == HRESULT.S_OK)
          {
               return Builder.ToString();
          }
          else
          {
               return string.Empty;
          }
}
else
{
     return string.Empty;
}