Calling SHGetFileInfo in thread to avoid UI freeze

2019-02-17 18:03发布

In a .NET 4.0 application (WPF) we're using SHGetFileInfo to obtain shell icons for a directory tree. Since this takes quite some time in some cases (i.e. for a network drive that is unreachable or for a floppy drive), we wanted to do this in a thread and then update the icon when it has been read in.

The call is basically the same, it is now just executed within a thread. Because someone said that the thread must be STA for this to work, we used Thread rather than ThreadPool for testing, with the same results. Using ThreadPool also did not work.

SHGetFileInfo succeeds (returns 1), but the hIcon member in the structure is zero.

IntPtr GetIcon(string name)
{
    Shell32.SHFILEINFO shfi = new Shell32.SHFILEINFO();
    uint flags = Shell32.SHGFI_ICON | Shell32.SHGFI_USEFILEATTRIBUTES | Shell32.SHGFI_SMALLICON;

    Shell32.SHGetFileInfo(
        name, System.IO.Directory.Exists(name) ? Shell32.FILE_ATTRIBUTE_DIRECTORY : Shell32.FILE_ATTRIBUTE_NORMAL,
        ref shfi, 
        (uint) System.Runtime.InteropServices.Marshal.SizeOf(shfi), 
        flags );
    return shfi.hIcon;
}

The very same code works fine from the GUI thread. What has to be done to make the function work from a separate thread, or, however, make it work without blocking the GUI thread?


Update: The code around this is basically this:

var thread = new System.Threading.Thread(() => {
    var result = GetIcon("C:\\");
    // ... do something with the result
});
thread.SetApartmentState(System.Threading.ApartmentState.STA);
thread.Start();

if only the lines within the thread delegate are left in, it works fine (but on the GUI thread, of course).


Update: For now, we just Invoke the call to SHGetFileInfo to make it work. This has the advantage that the original problem (the page with the file view was not displayed until all the icons have been loaded) has been solved, though it means that the page hangs for each icon. But at least the user now sees that something is going on. We're still looking for an actual solution to the problem.

2条回答
够拽才男人
2楼-- · 2019-02-17 18:39

Just successfully got something just like this working. It had previously been crashing badly when run outside of Visual Studio. Before that we'd often been getting back the default icon rather than the correct one for the file type (since we're after file icons, not the directory icon).

A summary of the factors to consider.

  • As discussed already, interacting with the Shell requires using a STA thread which pumps messages. A BackgroundWorker isn't going to be enough here.
  • When you initialize SHFILEINFO, set both string properties (display name and type name) to string.Empty. This isn't shown in most samples, but failing to do this was causing a crash for us. (In debug mode it meant that the first icon we got back was wrong, which could be the same as your problem.)
  • Check your interop declarations are right. For example, the SHFILEINFO class should presumably have a [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode, Pack = 4)] attribute.

A TaskScheduler For Background Tasks on an STA Thread

We're using the Task Parallel Library, so wanted a TaskScheduler that would schedule work on a suitable background thread. The following code sample is for a class that exposes a TaskScheduler property that can be used for this.

Note that in our we have a single instance of this class that lasts for the whole lifetime of the application, so we've not implemented IDisposable. If you want to create/destroy these you'll need to handle that.

namespace MyNamespace
{
    using System;
    using System.ComponentModel.Composition;
    using System.Threading;
    using System.Threading.Tasks;
    using System.Windows;
    using System.Windows.Threading;

    /// <summary>
    /// Exposes a <see cref="TaskScheduler"/> that schedules its work on a STA background thread.
    /// </summary>
    [Export]
    public class StaTaskSchedulerSource
    {
        /// <summary>
        /// A window that is used for message pumping.
        /// </summary>
        private Window window;

        /// <summary>
        /// Thread on which work is scheduled.
        /// </summary>
        private Thread thread;

        /// <summary>
        /// The <see cref="TaskScheduler"/> exposed by this class.
        /// </summary>
        private TaskScheduler taskScheduler;

        /// <summary>
        /// Initializes a new instance of the <see cref="StaTaskSchedulerSource"/> class.
        /// </summary>
        public StaTaskSchedulerSource()
        {
            using (ManualResetEvent re = new ManualResetEvent(false))
            {
                this.thread = new Thread(
                    () =>
                    {
                        this.window = new Window();

                        re.Set();

                        Dispatcher.Run();
                    });

                this.thread.IsBackground = true;
                this.thread.SetApartmentState(ApartmentState.STA);

                this.thread.Start();

                re.WaitOne();
            }

            this.window.Dispatcher.Invoke(
                new Action(
                    () =>
                    {
                        this.taskScheduler = TaskScheduler.FromCurrentSynchronizationContext();
                    }));
        }

        /// <summary>
        /// Gets a <see cref="TaskScheduler"/> that schedules work on a background STA
        /// thread.
        /// </summary>
        public TaskScheduler TaskScheduler
        {
            get
            {
                return this.taskScheduler;
            }
        }
    }
}

Of course, this whole thing just wraps using a dispatcher to invoke the calls on another thread. This can be a bit slow. So if processing many icons it would be better to batch them up. Also, we cache icons we've already retrieved so we don't need to use the Win Shell at all the second time around.

SafeIconHandle

You may also find the following wrapper for an icon handle useful. It derives from SafeHandle and ensures that you icon is properly destroyed under all circumstances.

namespace UnmanagedResourceLib
{
    using System;
    using System.Runtime.ConstrainedExecution;
    using System.Runtime.InteropServices;
    using System.Security;
    using System.Security.Permissions;
    using Microsoft.Win32.SafeHandles;

    /// <summary>
    /// A <see cref="SafeHandle"/> implementation for HICON icon handles.
    /// </summary>
    [SecurityPermission(SecurityAction.InheritanceDemand, UnmanagedCode = true)]
    [SecurityPermission(SecurityAction.Demand, UnmanagedCode = true)]
    internal class SafeIconHandle : SafeHandleZeroOrMinusOneIsInvalid
    {
        /// <summary>
        /// Prevents a default instance of the <see cref="SafeIconHandle"/> class from being created.
        /// </summary>
        private SafeIconHandle()
            : base(true)
        {
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="SafeIconHandle"/> class.
        /// </summary>
        /// <param name="nativeHandle">The HICON to wrap.</param>
        /// <param name="ownsHandle"><c>true</c> if finalization of this object should cause the icon to be destroyed.</param>
        public SafeIconHandle(IntPtr nativeHandle, bool ownsHandle)
            : base(ownsHandle)
        {
            this.handle = nativeHandle;
        }

        /// <inheritdoc />
        [ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)]
        override protected bool ReleaseHandle()
        {
            return NativeMethods.DestroyIcon(this.handle);
        }

        /// <summary>
        /// Exposes Windows API call to destroy an icon.
        /// </summary>
        [SuppressUnmanagedCodeSecurity]
        internal static class NativeMethods
        {
            /// <summary>
            /// Destroys an icon and frees any memory the icon occupied. 
            /// </summary>
            /// <param name="hIcon">A handle to the icon to be destroyed.</param>
            /// <returns><c>true</c> if the function succeeds.</returns>
            [System.Runtime.InteropServices.DllImport("user32.dll", SetLastError = true)]
            public static extern bool DestroyIcon(IntPtr hIcon);
        }
    }
}
查看更多
趁早两清
3楼-- · 2019-02-17 18:46

I don't think there's any problem. You don't need to use SetApartmentState. According to the documentation you do need to have called CoInitialize or OleInitialize, but I think this should have been called for you anyway by WPF.

I created a simple WPF application below. This works fine. SHGetFileInfo runs on a different thread to the UI thread and shfi.hIcon is not zero.

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
    }

    private void button1_Click(object sender, RoutedEventArgs e)
    {
        Debug.WriteLine(Thread.CurrentThread.ManagedThreadId);
        Task<IntPtr> task = Task.Factory.StartNew(() => GetIcon("C:\\"));
    }

    private IntPtr GetIcon(string name)
    {
        Debug.WriteLine(Thread.CurrentThread.ManagedThreadId);

        var shfi = new Shell32.SHFILEINFO();
        uint flags = Shell32.SHGFI_ICON | Shell32.SHGFI_USEFILEATTRIBUTES | Shell32.SHGFI_SMALLICON;

        Shell32.SHGetFileInfo(
            name,
            Directory.Exists(name) ? Shell32.FILE_ATTRIBUTE_DIRECTORY : Shell32.FILE_ATTRIBUTE_NORMAL,
            ref shfi,
            (uint) Marshal.SizeOf(shfi),
            flags);

        Debug.WriteLine(shfi.hIcon);

        return shfi.hIcon;
    }
}


public class Shell32
{
    public const int MAX_PATH = 256;

    // Browsing for directory.
    public const uint BIF_RETURNONLYFSDIRS = 0x0001;
    public const uint BIF_DONTGOBELOWDOMAIN = 0x0002;
    public const uint BIF_STATUSTEXT = 0x0004;
    public const uint BIF_RETURNFSANCESTORS = 0x0008;
    public const uint BIF_EDITBOX = 0x0010;
    public const uint BIF_VALIDATE = 0x0020;
    public const uint BIF_NEWDIALOGSTYLE = 0x0040;
    public const uint BIF_USENEWUI = (BIF_NEWDIALOGSTYLE | BIF_EDITBOX);
    public const uint BIF_BROWSEINCLUDEURLS = 0x0080;
    public const uint BIF_BROWSEFORCOMPUTER = 0x1000;
    public const uint BIF_BROWSEFORPRINTER = 0x2000;
    public const uint BIF_BROWSEINCLUDEFILES = 0x4000;
    public const uint BIF_SHAREABLE = 0x8000;

    public const uint SHGFI_ICON = 0x000000100; // get icon
    public const uint SHGFI_DISPLAYNAME = 0x000000200; // get display name
    public const uint SHGFI_TYPENAME = 0x000000400; // get type name
    public const uint SHGFI_ATTRIBUTES = 0x000000800; // get attributes
    public const uint SHGFI_ICONLOCATION = 0x000001000; // get icon location
    public const uint SHGFI_EXETYPE = 0x000002000; // return exe type
    public const uint SHGFI_SYSICONINDEX = 0x000004000; // get system icon index
    public const uint SHGFI_LINKOVERLAY = 0x000008000; // put a link overlay on icon
    public const uint SHGFI_SELECTED = 0x000010000; // show icon in selected state
    public const uint SHGFI_ATTR_SPECIFIED = 0x000020000; // get only specified attributes
    public const uint SHGFI_LARGEICON = 0x000000000; // get large icon
    public const uint SHGFI_SMALLICON = 0x000000001; // get small icon
    public const uint SHGFI_OPENICON = 0x000000002; // get open icon
    public const uint SHGFI_SHELLICONSIZE = 0x000000004; // get shell size icon
    public const uint SHGFI_PIDL = 0x000000008; // pszPath is a pidl
    public const uint SHGFI_USEFILEATTRIBUTES = 0x000000010; // use passed dwFileAttribute
    public const uint SHGFI_ADDOVERLAYS = 0x000000020; // apply the appropriate overlays
    public const uint SHGFI_OVERLAYINDEX = 0x000000040; // Get the index of the overlay

    public const uint FILE_ATTRIBUTE_DIRECTORY = 0x00000010;
    public const uint FILE_ATTRIBUTE_NORMAL = 0x00000080;

    [DllImport("Shell32.dll")]
    public static extern IntPtr SHGetFileInfo(
        string pszPath,
        uint dwFileAttributes,
        ref SHFILEINFO psfi,
        uint cbFileInfo,
        uint uFlags
        );

    #region Nested type: BROWSEINFO

    [StructLayout(LayoutKind.Sequential)]
    public struct BROWSEINFO
    {
        public IntPtr hwndOwner;
        public IntPtr pidlRoot;
        public IntPtr pszDisplayName;
        [MarshalAs(UnmanagedType.LPTStr)] public string lpszTitle;
        public uint ulFlags;
        public IntPtr lpfn;
        public int lParam;
        public IntPtr iImage;
    }

    #endregion

    #region Nested type: ITEMIDLIST

    [StructLayout(LayoutKind.Sequential)]
    public struct ITEMIDLIST
    {
        public SHITEMID mkid;
    }

    #endregion

    #region Nested type: SHFILEINFO

    [StructLayout(LayoutKind.Sequential)]
    public struct SHFILEINFO
    {
        public const int NAMESIZE = 80;
        public IntPtr hIcon;
        public int iIcon;
        public uint dwAttributes;
        [MarshalAs(UnmanagedType.ByValTStr, SizeConst = MAX_PATH)] public string szDisplayName;
        [MarshalAs(UnmanagedType.ByValTStr, SizeConst = NAMESIZE)] public string szTypeName;
    };

    #endregion

    #region Nested type: SHITEMID

    [StructLayout(LayoutKind.Sequential)]
    public struct SHITEMID
    {
        public ushort cb;
        [MarshalAs(UnmanagedType.LPArray)] public byte[] abID;
    }

    #endregion
}

/// <summary>
/// Wraps necessary functions imported from User32.dll. Code courtesy of MSDN Cold Rooster Consulting example.
/// </summary>
public class User32
{
    /// <summary>
    /// Provides access to function required to delete handle. This method is used internally
    /// and is not required to be called separately.
    /// </summary>
    /// <param name="hIcon">Pointer to icon handle.</param>
    /// <returns>N/A</returns>
    [DllImport("User32.dll")]
    public static extern int DestroyIcon(IntPtr hIcon);
}
查看更多
登录 后发表回答