在线程中调用SHGetFileInfo以避免UI冻结

Ore*_*ost 6 windows multithreading shell32 .net-4.0

在.NET 4.0应用程序(WPF)中,我们使用SHGetFileInfo获取目录树的shell图标.由于这在某些情况下需要相当长的时间(即对于无法访问的网络驱动器或软盘驱动器),我们希望在一个线程中执行此操作,然后在读入时更新图标.

调用基本相同,现在只是在一个线程中执行.因为有人说线程必须STA为此工作,我们使用Thread而不是ThreadPool进行测试,结果相同.使用ThreadPool也没有用.

SHGetFileInfo 成功(返回1),但结构中的hIcon成员为零.

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;
}
Run Code Online (Sandbox Code Playgroud)

完全相同的代码可以从GUI线程中正常工作.要使函数从单独的线程工作,或者使其在不阻塞GUI线程的情况下工作,必须做些什么?


更新:围绕这个的代码基本上是这样的:

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

如果只留下线程委托中的行,它可以正常工作(当然,在GUI线程上).


更新:目前,我们只是调用SHGetFileInfo它来使其工作.这样做的好处是,原始问题(在加载所有图标之前未显示文件视图的页面)已经解决,但这意味着页面会挂起每个图标.但至少用户现在看到了一些事情正在发生.我们仍在寻找问题的实际解决方案.

Oll*_*lly 6

刚刚成功得到了像这样的工作.以前在Visual Studio外部运行时崩溃严重.在此之前,我们经常回到默认图标而不是文件类型的正确图标(因为我们是在文件图标之后,而不是目录图标).

要考虑的因素的摘要.

  • 如前所述,与Shell交互需要使用泵送消息的STA线程.这里的BackgroundWorker还不够.
  • 初始化SHFILEINFO时,将两个字符串属性(显示名称和类型名称)设置为string.Empty.这在大多数样本中都没有显示,但未能做到这一点导致我们崩溃.(在调试模式下,它意味着我们返回的第一个图标是错误的,这可能与您的问题相同.)
  • 检查您的互操作声明是否正确.例如,SHFILEINFO该类应该具有[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode, Pack = 4)]属性.

用于STA线程上的后台任务的TaskScheduler

我们正在使用任务并行库,因此需要一个可以在合适的后台线程上安排工作的TaskScheduler.以下代码示例适用于公开可用于此目的的TaskScheduler属性的类.

请注意,在我们中,我们有一个这个类的单个实例,它持续应用程序的整个生命周期,因此我们没有实现IDisposable.如果你想创建/销毁这些,你需要处理它.

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;
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

当然,这整个事情只是使用调度程序来调用另一个线程上的调用.这可能有点慢.因此,如果处理许多图标,最好将它们批处理.此外,我们缓存了我们已经检索过的图标,因此我们不需要在第二次使用Win Shell.

SafeIconHandle

您还可以找到以下用于图标处理的包装器.它源自SafeHandle,确保您在任何情况下都能正确销毁图标.

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);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)


Phi*_*hil 3

我不认为有什么问题。您不需要使用 SetApartmentState。根据文档,您确实需要调用 CoInitialize 或 OleInitialize,但我认为 WPF 无论如何都应该为您调用它。

我在下面创建了一个简单的 WPF 应用程序。这很好用。SHGetFileInfo 在与 UI 线程不同的线程上运行,并且 shfi.hIcon 不为零。

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);
}
Run Code Online (Sandbox Code Playgroud)

  • 感谢这个小而完整的例子,我们找到了实际的问题。看来你不能同时在两个线程上调用带有图标的SHGetFileInfo。 (2认同)