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

在.NET 4.0应用程序(WPF)中,我们使用SHGetFileInfo来获取目录树的shell图标。 因为在某些情况下(比如无法访问的networking驱动器或软盘驱动器)需要相当长的一段时间,所以我们希望在一个线程中执行此操作,然后在读取图标时更新图标。

调用基本相同,现在只是在一个线程内执行。 因为有人说线程必须是STA才能工作,所以我们使用Thread而不是ThreadPool来进行testing,结果相同。 使用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; } 

非常相同的代码从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(); 

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


更新:现在,我们只需调用SHGetFileInfo来使其工作。 这样做的好处是,原来的问题(带有文件视图的页面在所有的图标被加载之前都没有显示出来)已经解决了,尽pipe这意味着每个图标都会挂起页面。 但是至less用户现在看到了一些事情正在发生。 我们仍然在寻找解决问题的实际办法。

我不认为有任何问题。 您不需要使用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); } 

只是成功地得到了这样的工作。 在Visual Studio之外运行时,它以前一直崩溃。 在此之前,我们通常会取回默认图标,而不是正确的文件类型(因为我们在文件图标之后,而不是目录图标)。

要考虑的因素的总结。

  • 正如已经讨论的那样,与Shell进行交互需要使用一个STA消息泵。 BackgroundWorker在这里还不够。
  • 初始化SHFILEINFO时, 将字符串属性(显示名称和类型名称)设置为string.Empty 。 这在大多数样本中都没有显示,但是没有做到这一点就导致了我们的崩溃。 (在调试模式下,这意味着我们返回的第一个图标是错误的,这可能与您的问题相同。)
  • 检查你的interop声明是否正确。 例如, 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; } } } } 

当然,这整个事情只是使用调度程序来调用另一个线程上的调用。 这可能有点慢。 所以如果处理很多图标,最好把它们分组。 此外,我们缓存已经检索到的图标,所以我们不需要再次使用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); } } }