6 Stimmen

Aufruf von SHGetFileInfo in einem Thread, um ein Einfrieren der Benutzeroberfläche zu vermeiden

In einer .NET 4.0-Anwendung (WPF) verwenden wir SHGetFileInfo um Shell-Symbole für einen Verzeichnisbaum zu erhalten. Da dies in manchen Fällen recht lange dauert (z. B. bei einem nicht erreichbaren Netzlaufwerk oder einem Diskettenlaufwerk), wollten wir dies in einem Thread erledigen und das Symbol dann aktualisieren, wenn es eingelesen wurde.

Der Aufruf ist im Grunde derselbe, er wird jetzt nur innerhalb eines Threads ausgeführt. Weil jemand sagte, dass der Thread sein muss STA damit dies funktioniert, haben wir für die Tests Thread statt ThreadPool verwendet, mit den gleichen Ergebnissen. Verwendung von ThreadPool hat ebenfalls nicht funktioniert.

SHGetFileInfo ist erfolgreich (gibt 1 zurück), aber das hIcon-Mitglied in der Struktur ist Null.

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;
}

Derselbe Code funktioniert auch über den GUI-Thread. Was muss getan werden, damit die Funktion von einem separaten Thread aus funktioniert, oder, wie auch immer, damit sie funktioniert, ohne den GUI-Thread zu blockieren?


Update: Der Code dazu ist im Grunde folgender:

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

wenn nur die Zeilen innerhalb des Thread-Delegaten gelassen werden, funktioniert es gut (aber auf dem GUI-Thread, natürlich).


Aktualisierung: Im Moment rufen wir nur den Aufruf von SHGetFileInfo damit es funktioniert. Dies hat den Vorteil, dass das ursprüngliche Problem (die Seite mit der Dateiansicht wurde erst angezeigt, nachdem alle Symbole geladen waren) gelöst wurde, obwohl es bedeutet, dass die Seite für jedes Symbol hängen bleibt. Aber zumindest sieht der Benutzer jetzt, dass etwas vor sich geht. Wir sind immer noch auf der Suche nach einer tatsächlichen Lösung für das Problem.

7voto

Olly Punkte 5876

Ich habe gerade erfolgreich etwas Ähnliches zum Laufen gebracht. Es war zuvor zum Absturz schlecht, wenn außerhalb von Visual Studio ausgeführt. Davor hatten wir oft zurück das Standard-Symbol statt der richtigen für den Dateityp (da wir nach Datei-Symbole, nicht das Verzeichnis-Symbol sind) erhalten.

Eine Zusammenfassung der zu berücksichtigenden Faktoren.

  • Wie bereits erwähnt, erfordert die Interaktion mit der Shell die Verwendung eines STA-Threads, der Nachrichten pumpt. Ein BackgroundWorker wird hier nicht ausreichen.
  • Wenn Sie SHFILEINFO initialisieren, beide String-Eigenschaften (Anzeigename und Typname) auf string.Empty setzen . Dies wird in den meisten Beispielen nicht angezeigt, aber bei uns führte es zu einem Absturz, wenn wir dies nicht taten. (Im Debug-Modus bedeutete es, dass das erste Symbol, das wir zurückbekamen, falsch war, was dasselbe wie Ihr Problem sein könnte).
  • Prüfen Sie, ob Ihre Interop-Erklärungen richtig sind. Zum Beispiel muss die SHFILEINFO Klasse sollte vermutlich eine [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode, Pack = 4)] Attribut.

Ein TaskScheduler für Hintergrundtasks auf einem STA-Thread

Wir verwenden die Task Parallel Library und wollten daher einen TaskScheduler, der die Arbeit auf einem geeigneten Hintergrund-Thread planen würde. Das folgende Codebeispiel ist für eine Klasse, die eine TaskScheduler-Eigenschaft bereitstellt, die dafür verwendet werden kann.

Beachten Sie, dass wir in unserem Fall eine einzige Instanz dieser Klasse haben, die für die gesamte Lebensdauer der Anwendung gilt, weshalb wir IDisposable nicht implementiert haben. Wenn Sie diese erstellen/zerstören möchten, müssen Sie dies selbst tun.

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;
            }
        }
    }
}

Natürlich wird das Ganze mit einem Dispatcher verpackt, der die Aufrufe in einem anderen Thread aufruft. Das kann ein bisschen langsam sein. Wenn also viele Icons verarbeitet werden, wäre es besser, sie zu stapeln. Außerdem werden die bereits abgerufenen Symbole zwischengespeichert, so dass wir die Win Shell beim zweiten Mal gar nicht mehr verwenden müssen.

SafeIconHandle

Vielleicht finden Sie auch den folgenden Wrapper für ein Icon-Handle nützlich. Er leitet sich ab von SafeHandle und stellt sicher, dass Ihr Symbol unter allen Umständen ordnungsgemäß zerstört wird.

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);
        }
    }
}

3voto

Phil Punkte 40767

Ich glaube nicht, dass es ein Problem gibt. Sie brauchen SetApartmentState nicht zu verwenden. Laut der Dokumentation müssen Sie CoInitialize oder OleInitialize aufgerufen haben, aber ich denke, dies sollte für Sie sowieso von WPF aufgerufen worden sein.

Ich habe unten eine einfache WPF-Anwendung erstellt. Dies funktioniert gut. SHGetFileInfo läuft auf einem anderen Thread als der UI-Thread und shfi.hIcon ist nicht Null.

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);
}

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X