6

.NET 4.0 アプリケーション (WPF) ではSHGetFileInfo、ディレクトリ ツリーのシェル アイコンを取得するために使用しています。これにはかなりの時間がかかる場合があるため (つまり、ネットワーク ドライブにアクセスできない場合やフロッピー ドライブの場合)、これをスレッドで実行し、読み込まれたときにアイコンを更新する必要がありました。

呼び出しは基本的に同じで、スレッド内で実行されるだけです。これが機能するにはスレッドが必要であると誰かが言ったのでSTA、テストには ThreadPool ではなく Thread を使用しましたが、同じ結果が得られました。使用して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機能させるだけです。これには、アイコンごとにページがハングすることを意味しますが、元の問題 (すべてのアイコンが読み込まれるまでファイル ビューのページが表示されない) が解決されたという利点があります。しかし、少なくともユーザーは何かが起こっていることに気づきます。私たちはまだ問題の実際の解決策を探しています。

4

2 に答える 2

7

ちょうどこのようなものがうまく機能するようになりました。以前は、VisualStudioの外部で実行するとひどくクラッシュしていました。それ以前は、ファイルタイプの正しいアイコンではなく、デフォルトのアイコンを取り戻すことがよくありました(ディレクトリアイコンではなくファイルアイコンを探しているため)。

考慮すべき要素の要約。

  • すでに説明したように、シェルと対話するには、メッセージをポンピングする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;
            }
        }
    }
}

もちろん、このすべては、ディスパッチャを使用して別のスレッドで呼び出しを呼び出すためにラップするだけです。これは少し遅くなる可能性があります。したがって、多くのアイコンを処理する場合は、それらをまとめた方がよいでしょう。また、取得済みのアイコンをキャッシュするため、2回目にWinShellを使用する必要はありません。

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);
        }
    }
}
于 2012-04-19T13:02:49.227 に答える
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);
}
于 2012-04-15T20:23:35.963 に答える