Laden...

FolderBrowserDialog mit WPF wie umsetzen?

Letzter Beitrag vor einem Jahr 16 Posts 861 Views
FolderBrowserDialog mit WPF wie umsetzen?

Hallo!

Ich möchte ein Verzeichnis-Auswahl Dialog in meinem WPF, .Net 5 Projekt implementieren.

Bisher habe ich den Path-Dialog von Windows.Form benutzt.

Unter .Net 5 (wahrscheinlich alle .NETCore) kann ich den Forms-Namespace jedoch nicht mehr einbinden / finden! (siehe Bild)

Woran liegt das bzw. welche Verzeichchnis-Auswahl-Dialog(e) benutzt ihr?

Hallo Abt!

Danke! für die schnelle Antwort.

Es ist schon ein Stückchen her, wo ich mich damit beschäftigt hatte ... jetzt nervt es aber doch ...

Den Beitrag hatte ich auch gefunden. Wenn ich die Projekt-Einstellungen ändere:

  <PropertyGroup>
    <OutputType>WinExe</OutputType>
    <TargetFramework>net5.0-windows</TargetFramework>
    <UseWindowsForms>true</UseWindowsForms>
    <UseWPF>true</UseWPF>
    <ApplicationIcon>DVD-Verwaltung-2.ico</ApplicationIcon>
  </PropertyGroup>

, erhalte ich jedoch (beim nächsten öffnen des Projektes) die Fehlermeldung:

Warnung	MSB3290	Fehler beim Erstellen der Wrapperassembly für die Typbibliothek "{215d64d2-031c-33c7-96e3-61794cd1ee61}". Die Typbibliothek "System_Windows_Forms" kann nicht als eine CLR-Assembly importiert werden, da sie aus einer CLR-Assembly exportiert wurde.

deshalb hatte ich damals aufgegeben. 😦

PS:

Wieso steht der Assembly-Name überhaupt mit Unterstrichen, anstatt mit Punkt???

System.Windows.Forms

PS2:

Wir brauchen uns hier aber auch nicht "festbeißen" ... welchen Verzeichnis-Auswahl Dialog verwendet ihr denn unter .NET Core?

Wir brauchen uns hier aber auch nicht "festbeißen" ... welchen Verzeichnis-Auswahl Dialog verwendet ihr denn unter .NET Core?

Auch dafür gibts hunderte Beiträge, sowohl auf SO, hier oder auf GitHub - da diese Frage primär was mit WPF und nicht mit .NET Core zutun hat. Denn WPF hat keine Implementierung dafür, seit beginn.

Erster Treffer von Palladin007 dazu: Win32 Dialog

Hallo Abt!

Alle Lösungen die ich auf GitHub gefunden habe gehen auf .NET Framework Implementationen zurück und beim Einbinden in das Projekt bekommt man dann die entsprechenden Warnungen. Kann man ausblenden, aber ich suche etwas, dass ich auch in Zukunft ohne Einschränkungen verwenden kann.

Alle sonstigen Lösungen im Netz (die ich gefunden habe)  gehen auf den Forms-Namenspace zurück:

Danke für deinen Link zu Palladin007's Beitrag!

Es stellt sich zwar die Frage: Warum muss man das Rad jedes mal neu erfinden? Einen Verzeichnis-Dialog ist doch etwas "alltägliches", aber ich versuche es einmal. Bei deinem 2. Link auf den Win32 Dialog fehlen mir z.B. die Klassen-Namen und die Parameter

Anfangen würde es mit:

/// <summary>
/// Hilfs-Klasse zum Zugriff auf Windows API-Methoden
/// </summary>
public class WindowsAPI
{
    [System.Runtime.InteropServices.DllImport("win32")]
    public static extern int BrowseDialogBox(HDC hdc, string Title ...);

}

Da ist noch viel Luft nach oben ... 😉

Alle Lösungen die ich auf GitHub gefunden habe gehen auf .NET Framework Implementationen zurück und beim Einbinden in das Projekt bekommt man dann die entsprechenden Warnungen.

Google Suche nach "github .net 5 wpf folder dialog"
https://github.com/dotnet/wpf/issues/438

War gar nicht so schwer 😉

Warum muss man das Rad jedes mal neu erfinden?

Willkommen in der Welt zwischen "ich will was fertiges, aber was besteht is mir nich flexibel genug" 😉

API32: Iterieren durch TreeView

Hallo Abt und API32 "Fans" (falls es so etwas gibt?)   😉!

Weil es unmittelbar mit diesen Thread zusammenhängt, habe ich noch ein paar Fragen die die API32 Programmierung betreffen.

Es hat mich natürlich nicht in Ruhe gelassen es zu versuchen ... auf den API32 Browser-Dialog von C# aus zuzugreifen.

Nach einer gewissen Einarbeitungszeit stellen sich dann doch ganz brauchbare Ergebnisse ein.

An einer Stelle komme ich jedoch nicht weiter.

Zum einstellen des selektierten Verzeichnisses bei der (Erst) Initialisierung des Dialog-Browsers hole ich (über einen Timer) das Browser-Dialog Fenster und greife auf das TreeView des Dialog-Browsers zu. Funktionier sicher und die Windows-Meldungen werden auch korrekt verarbeitet.

Die Iteration im TreeView erschließt sich mir jedoch nicht.

.

RootItem = SendMessage(pTV, TVM_GETNEXTITEM, new(TVGN_ROOT), IntPtr.Zero);          // Root-Eintrag holen

TVM_GETNEXTITEM mit TVGN_ROOT holt zuverlässig den Root-Eintrag des TreeView's. [Ebene 0]

.

AktItem = SendMessage(pTV, TVM_GETNEXTITEM, new(TVGN_CHILD), RootItem);          // 1. Eintrag = Untergeordneter Eintrag vom Root-Eintrag holen [Ebene 1]

TVM_GETNEXTITEM mit TVGN_CHILD holt den untergeordneten Item vom Root-Eintrag. ⇒ 1. Eintrag [Ebene 1]

.

AktItem = SendMessage(pTV, TVM_GETNEXTITEM, new(TVGN_NEXT), AktItem);            // 2. Eintrag holen [Ebene 1]

TVM_GETNEXTITEM mit TVGN_NEXT holt den nächsten Item (gleiche Ebene) vom 1. Eintrag. ⇒ 2. Eintrag [Ebene 1]

.

AktItem = SendMessage(pTV, TVM_GETNEXTITEM, new(TVGN_NEXT), AktItem);            // 3. Eintrag [Ebene 1] holen

TVM_GETNEXTITEM mit TVGN_NEXT holt den nächsten Item (gleiche Ebene) vom 2. Eintrag. ⇒ 3. Eintrag [Ebene 1]

.

Sobald ich aber "tiefer" iterieren will (Ebene 2), geht das nicht!

Anmerkung: Der 1. Eintrag wird ja eigentlich schon vom Root-Eintrag "abgeleitet" (benutzt RootItem vom Root-Eintrag!)

Bei weiteren Ableitungen gehen das jedoch nicht! Zum Beispiel wenn ich den 1. Unter-Eintrag vom 3. Eintrag = Ebene 2 "holen" möchte: (Müsste ich ja den gleichen Aufruf wie beim 1. Eintrag mit dem AktItem vom 3. Eintrag (lParam) benutzen!)

AktItem  = SendMessage(pTV, TVM_GETNEXTITEM, new(TVGN_CHILD), AktItem);          // 1. Unter-Eintrag vom 3. Eintrag [Ebene 2] holen

Damit wird aber IntPtr.Zero zurückgegeben.

Woran kann das liegen????

.

.

Der Probe-Code sieht folgendermaßen aus:

IntPtr RootItem = IntPtr.Zero;                                                      // Handle des Root-Eintrages initalisieren
IntPtr AktItem = IntPtr.Zero;                                                       // Handle des akt. TreeView-Items initalisieren
string AktVerzeichnis = RootVerzeichnis;                                            // Aktuelles Verzeichnis initalisieren

RootItem = SendMessage(pTV, TVM_GETNEXTITEM, new(TVGN_ROOT), IntPtr.Zero);          // Root-Eintrag holen
AktItem = RootItem;                                                                 // Zeiger auf den akt. Eintrag aktualisieren

if (!SelectVerzeichnis.Equals(AktVerzeichnis))                                      // Select-Verzeichnis ist nicht das Root-Verzeichnis?
{
    if (AktItem != IntPtr.Zero)                                                     // Ist ein Root-Eintrag vorhanden?
    {
        // AktItem = FindItem(pTV, AktItem, SelectVerzeichnis, AktVerzeichnis);        // TV rekursiv durchsuchen

        AktItem = SendMessage(pTV, TVM_GETNEXTITEM, new(TVGN_CHILD), RootItem);          // 1. Eintrag = Untergeordneter Eintrag vom Root-Eintrag holen [Ebene 1]
        System.Diagnostics.Debug.Print("1. Eintrag: " + GetTVItem(pTV, AktItem));        // Bezeichnung des 1. Eintrages ermitteln

        AktItem = SendMessage(pTV, TVM_GETNEXTITEM, new(TVGN_NEXT), AktItem);            // 2. Eintrag [Ebene 1] holen
        System.Diagnostics.Debug.Print("2. Eintrag: " + GetTVItem(pTV, AktItem));        // Bezeichnung des 2. Eintrages ermitteln

        AktItem = SendMessage(pTV, TVM_GETNEXTITEM, new(TVGN_NEXT), AktItem);            // 3. Eintrag [Ebene 1] holen
        System.Diagnostics.Debug.Print("3. Eintrag: " + GetTVItem(pTV, AktItem));        // Bezeichnung des 3. Eintrages ermitteln

        AktItem  = SendMessage(pTV, TVM_GETNEXTITEM, new(TVGN_CHILD), AktItem);          // 1. Unter-Eintrag vom 3. Eintrag [Ebene 2] holen
        System.Diagnostics.Debug.Print("3.1 Eintrag: " + GetTVItem(pTV, AktItem));       // Bezeichnung des 3.1. Eintrages ermitteln
    }
}
return AktItem;

Das Ergebnis ist im Anhang abgebildet.

Wie ist denn TVGN_CHILD bei dir deklariert? Es sollte den Wert 4haben, s. z.B. Win32/NativeMethods.cs: TreeView constants

Ansonsten: Funktioniert es denn beim ersten Unterordner?

Guten morgen Th69!

Die Konstanten habe ich der commctrl.h - Header-Datei entnommen und passen:

// Konstanten zur Navigation im TreeView        
private const int TVGN_ROOT = 0;                        // Ruft das erste untergeordnete Element des Stammelements ab
private const int TVGN_NEXT = 1;                        // Ruft das nächste gleichgeordnete Element ab.
//private const int TVGN_PREVIOUS = 2;                  // Ruft das vorherige gleichgeordnete Element ab.
//private const int TVGN_PARENT = 3;                    // Ruft das übergeordnete Element des angegebenen Elements ab.
private const int TVGN_CHILD = 4;                       // Ruft das erste untergeordnete Element ab.
// private const int TVGN_FIRSTVISIBLE = 5;             // Ruft das erste sichtbare Element ab.
// private const int TVGN_NEXTVISIBLE = 6;              // Ruft das nächste sichtbare Element ab.
//private const int TVGN_PREVIOUSVISIBLE = 7;           // Ruft das vorhergehende sichtbare Element ab. 
//private const int TVGN_DROPHILITE = 8;                // Ruft das Element ab, das das Ziel eines Drag-and-Drop-Vorgangs ist.
private const int TVGN_CARET = 9;                       // Ruft das aktuell ausgewählte Element ab.

Das Unter-Element vom Eintrag 1 wird auch nicht referenziert.

Damit ich keinen Dominoeffekt erhalte, habe ich jeder Referenz auf einen Eintrag einer separaten Variable zugeordnet.

Folgende Abruf-Reihenfolge:

IntPtr Item1 = SendMessage(pTV, TVM_GETNEXTITEM, new(TVGN_CHILD), RootItem);   // 1. Eintrag = Untergeordneter Eintrag vom Root-Eintrag holen [Ebene 1]
System.Diagnostics.Debug.Print("1. Eintrag: " + GetTVItem(pTV, Item1));        // Bezeichnung des 1. Eintrages ermitteln

IntPtr Item1_1 = SendMessage(pTV, TVM_GETNEXTITEM, new(TVGN_CHILD), Item1);    // 1. Unter-Eintrag vom 1. Eintrag [Ebene 2] holen
System.Diagnostics.Debug.Print("1.1 Eintrag: " + GetTVItem(pTV, Item1_1));     // Bezeichnung des 1.1. Eintrages ermitteln

IntPtr Item2 = SendMessage(pTV, TVM_GETNEXTITEM, new(TVGN_NEXT), Item1);       // 2. Eintrag [Ebene 1] holen
System.Diagnostics.Debug.Print("2. Eintrag: " + GetTVItem(pTV, Item2));        // Bezeichnung des 2. Eintrages ermitteln

IntPtr Item3 = SendMessage(pTV, TVM_GETNEXTITEM, new(TVGN_NEXT), Item2);        // 3. Eintrag [Ebene 1] holen
System.Diagnostics.Debug.Print("3. Eintrag: " + GetTVItem(pTV, Item3));         // Bezeichnung des 3. Eintrages ermitteln

IntPtr Item3_1 = SendMessage(pTV, TVM_GETNEXTITEM, new(TVGN_CHILD), Item3);      // 1. Unter-Eintrag vom 3. Eintrag [Ebene 2] holen
System.Diagnostics.Debug.Print("3.1 Eintrag: " + GetTVItem(pTV, Item3_1));       // Bezeichnung des 3.1. Eintrages ermitteln

ergibt dann folgendes Ergebnis:

1. Eintrag: Anruf_Info
1.1 Eintrag: <leer>
2. Eintrag: Anruf_Info REPORT22
3. Eintrag: Druck
3.1 Eintrag: <leer>

Das Interessante ist doch, dass es bei dem 1. Eintrag, der ein Untereintrag von Root ist, (mit dem gleichen Aufruf!) funktioniert (siehe Probe Code)! Hier ist lParam intPtr.Zero. Ich würde interpretieren, der Bezug ist damit der Zeiger auf das TreeView selbst. Was ist am TreeView anders als der Bezug auf ein TreeView-Item? Auf der gleichen Ebene (TVGN_NEXT) wirkt ja der Bezug auf das vorhergehende TreeView-Item.

Danke für deine Überlegung(en)!

Auf Anhieb habe ich dafür auch keine Erklärung.

Zeige mal deine Deklaration von SendMessage.

Und verwendest du anyCPU oder explizit 32 oder 64 bit?

Evtl. laß auch mal die Zeigerwerte von den ItemX- Werten ausgeben.

Edit:

Kurz nachdem ich den Rechner ausgemacht hatte, um einen Film im Fernsehen zu schauen, fiel mir ein, warum die Unterordner null zurückgeben: die Verzeichnisse der Unterordner werden erst dynamisch beim Anklicken durch den Anwender eingelesen und daher kannst du sie nicht schon vorher auslesen.

Beim FolderBrowserDialog kannst du doch SelectedPath benutzen, um ein Verzeichnis vorauszuwählen.

Hallo Th69!

Die SendMessage Deklaration ist die ganz klassische:

[DllImport("user32.dll", CharSet = CharSet.Auto)]
private static extern IntPtr SendMessage(IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam);

Meinst du jetzt die Build-Projekt-Einstellung?  →  Zielplattform: Any CPU .

Das wäre jetzt ja ganz schlecht (dynamisch und nur "manuell")!!! Ich habe zwischendurch auch schon den Eintrag selektieren lassen, bevor ich die Unter-Einträge ermitteln lasse:

IntPtr Item1 = SendMessage(pTV, TVM_GETNEXTITEM, new(TVGN_CHILD), RootItem);   // 1. Eintrag = Untergeordneter Eintrag vom Root-Eintrag holen [Ebene 1]
System.Diagnostics.Debug.Print("1. Eintrag: " + GetTVItem(pTV, Item1));        // Bezeichnung des 1. Eintrages ermitteln

SendMessage(pTV, TVM_SELECTITEM, new IntPtr(TVGN_CARET), Item1);               // 1. Eintrag selektieren
SendMessage(pTV, TVM_ENSUREVISIBLE, IntPtr.Zero, Item1);                       // 1. Eintrag in den sichtbaren Bereich bringen

IntPtr Item1_1 = SendMessage(pTV, TVM_GETNEXTITEM, new(TVGN_CHILD), Item1);    // 1. Unter-Eintrag vom 1. Eintrag [Ebene 2] holen
System.Diagnostics.Debug.Print("1.1 Eintrag: " + GetTVItem(pTV, Item1_1));     // Bezeichnung des 1.1. Eintrages ermitteln

Aber hilft alles nichts:

1. Eintrag: Anruf_Info
1.1 Eintrag: <leer>
2. Eintrag: Anruf_Info REPORT22

Die SendMessage-Aufrufe funktionieren. Mit ihnen (unverändert) kann ich einen Eintrag selektieren.

Wie im Bild ersichtlich, wird der 1. Eintrag auch aufgeklappt dargestellt.

Genau den FolderBrowserDialog möchte ich ja ersetzen, da er im Forms-NamenSpace integriert ist (siehe Thread-Ausgangspunkt).

Mal sehen, ob ich das (mit meinen Vorstellungen) schaffe!

Danke! und ich hoffe, du hast beim Film abschalten können. 😉

Welche WinAPI-Funktion benutzt du denn jetzt? Für SHBrowseForFolder kannst du direkt den Beispiel-Code aus P/Invoke.net: Shell32.SHBrowseForFolder (mit der Callback-Funktion OnBrowseEvent, welche die NachrichtBFFM_SETSELECTION absendet) benutzen - du brauchst also selber nicht den TreeView durchsuchen. Intern benutzt der FolderBrowserDialog (s. Zeile 285 und 341) diese auch!

Weitere Nachrichten dafür sind in BFFCALLBACK-Rückruffunktion: Parameters beschrieben.

Hallo Th69!

Sorry, in den letzten Tagen habe ich das Projekt schon 2x komplett überarbeitet, dass ich gar keine Zeit hatte hier noch einmal vorbei zu sehen!

Ja du hast recht, das iterieren durch das TreeView kann man sich ersparen (ich glaube sogar das ist praktisch nicht umsetzbar) und über die CallBack Funktion das Select-Verzeichnis setzen. Zusätzlich kann man sich auch das setzen des Root-Verzeichnisses über Folder-ID's sparen und Zuweisungen über Environment.SpecialFolder und PIDCList Ermittlung über die API-ILCreateFromPath Bestimmung ausführen. Die Angaben ordnet man dann einer BROWSERINFO-Struktur zu und kann diese an die SHBrowseForFolder Methode übergeben um den Verzeichnis-Dialog anzuzeigen. Ich habe auch noch eine Oberfläche zum Testen gebastelt ... ich denke ich werde das Projekt jetzt noch ein drittes mal überarbeiten (sehr straffen) und hoffe, dass ich am Wochenende dann hier etwas Veröffentlichen kann.

Vielen Dank für deine Ermittlungen!

PS:

Der Forms-NameSpace lässt sich in Core-Projekte einbinden, wenn man nur die Projekt-Datei ändert (<UseWindowsForms>true</UseWindowsForms>) und die Forms-Assembly nicht dem Projekt als COM-Assembly hinzufügt. Forms wird dann beim direkten Referenzieren (System.Windows.Forms...) automatisch geladen.  Alles Anstrengungen (fast) umsonst 😦

Teil 1: GUI zum Verzeichnis Auswahl-Dialog

Hallo!

Viel später als gedacht, aber jetzt ist es doch erledigt. Mein Verzeichnis Auswahl-Dialog (Framework: .NETCore Assembly).

Der Verzeichnis Auswahl-Dialog hat 4 Überladungen für den Aufruf.

Sie unterscheiden sich in der Art wie die Root und Select-Verzeichnis - Eigenschaft übergeben wird (Auswahl-Art). Diese Eigenschaften können als String (spezifiziert einen Verzeichnispfad) oder als Aufzählungswert der Aufzählung: System.Environment.SpecialFolder übergeben werden (Spezial-Verzeichnis). Über die Spezial-Verzeichnisse können Einträge im Verzeichnis-Dialog selektiert werden, die keine direkte Entsprechung im Dateisystem haben. (Desktop als oberste Ebene des Verzeichnis-Dialoges oder Arbeitsplatz als Sammelbehälter für weitere Spezial-Verzeichnisse und Datenträger).

MessageBoxResult ShowDialog(string, string)	// Root + Select-Verzeichnis als Verzeichnis

MessageBoxResult ShowDialog(Environment.SpecialFolder, string)	// Root als Spezial-Verzeichnis / Select als Verzeichnis

MessageBoxResult ShowDialog(Environment.SpecialFolder, Environment.SpecialFolder) // Root + Special – Verzeichnis als Spezial-Verzeichnis

MessageBoxResult ShowDialog(string, Environment.SpecialFolder)	// Root als Verzeichnis und Select-Verzeichnis als Spezial-Verzeichnis

Zur leichteren Handhabung der Spezialverzeichnisse habe ich in der VerzeichnisDialog-Klasse eine Liste der Spezialverzeichnisse implementiert, durch die eine einfache Auswahl (auch mehrerer Spezialverzeichnisse = Gruppe) über Flags und die direkte Zuordnung einer Beschreibung des Spezial-Verzeichnisses zu der Spezialverzeichnis-Aufzählung ermöglicht wird.

Die Liste kann über die Methode:

GetSpezialVerzeichnisse (VerzListenFlag Flags) 

Type: ObservableCollection<SpezialVerzeichnisEintrag>

abgerufen werden. Weitere Details zur Handhabung der Spezial-Verzeichnis Liste könnt ihr dem hier veröffentlichten Projekt: Projekt Verzeichnis Dialog GUI.zip, entnehmen.

An dieser Stelle ein paar Hinweise:

- Der Rückgabe-Type der ShowDialog-Methode (der VerzeichnisDialog-Klasse) ist eine, der  System.Windows.MessageBoxResult – Aufzählung adäquate Aufzählung, in der VerzeichnisDialog-Klasse. (Dies ist notwendig um konsequent nur im Microsoft.NETCore.App Framework  zu bleiben.)

- Das selektierte Verzeichnis wird nur dann (zuverlässig) in den sichtbaren Bereich verschoben, wenn  die Auswahl-Art des Root- und Select – Verzeichnisses gleich sind und sich das Select-Verzeichnis unterhalb des Root-Verzeichnisses befindet.

- Wenn die Option OnlyFilesystem (Standard: Ja) auf Nein gestellt wird und ein Eintrag, der keine Entsprechung im Datei-System hat ausgewählt wird, wird bei der Bestätigung [OK-Schaltfläche] automatisch der Status: Abbruch (MessageBoxResult.Cancel) zurückgegeben.

- Wenn das Root-Verzeichnis durch die Auswahl-Art: Dateisystem spezifiziert wird, dann können die übergeordneten Spezial-Verzeichnisse: Desktop und Arbeitsplatz nicht als Select-Verzeichnis  angegeben werden. Wenn sie doch ausgewählt werden, wird für diese automatisch das Benutzer-Dokumenten-Verzeichnis  selektiert.

Die Bilder zu den einzelnen Varianten der Root- und Select- Verzeichnis-Auswahl stehen ebenfalls noch einmal hier im Verzeichnis Bilder zur Verfügung.

Teil 2: Assembly zum Verzeichnis Auswahl-Dialog

Verzeichnis Auswahl-Dialog (Assembly)

Der Kern der Verzeichnis-Anzeige basiert auf dem API32 SHBrowseForFolder – Methoden-Aufruf.

Ihm wird als Parameter eine (parametrisierte) BROWSEINFO-Struktur übergeben. Diese steuert alle Optionen der Anzeige des Windows Verzeichnis-Dialoges:

API32.BFFCALLBACK _callbackKeepAlive = BrowseCallbackProc;      // CallBack-Methode (für die BrowseInfo-Struktur) initalisieren

IntPtr buffer = Marshal.AllocHGlobal(MAX_PATH);                 // Speicherplatz für das Rückgabe-Verzeichnis reservieren

API32.BROWSEINFO bi = new();                                    // Browse-Info Struktur initalisieren
bi.pidlRoot = rootVerzeichnisPIDL;                              // Der PIDCList-Eintrag auf das Root-Verzeichnis übergeben
bi.hwndOwner = hwndDialogFenster;                               // Zeiger auf das Dialog-Fenster übergeben
bi.pszDisplayName = buffer;                                     // Dem Zeiger auf die DisplayName-Eigenschaft den Zeiger auf den reservierten Speicher übergeben
bi.lpszTitle = Title;                                           // Den Titel des Verzeichnis-Dialoges übergeben
bi.ulFlags = publicOptions | (int)API32.BFFStyles.NewDialogStyle; // Den Flags alle eingestellten Dialog-Optionen und das neue Dialog-Style-Flag übergeben
bi.lpfn = _callbackKeepAlive;                                   // CallBack Methode zum selektieren des Select-Verzeichnis-Eintrages im Dialog-Fenster (TreeView)

pidlRet = API32.SHBrowseForFolder(ref bi);                      // Verzeichnis-Dialog anzeigen. Das Ergebnis der Auswahl ist ein (Zeiger auf einen) PIDCList-Eintrag auf das ausgewählte Verzeichnis.
 

An dieser Stelle gibt es eigentlich nur einen Knackpunkt. Das ist das selektieren des Eintrages das dem Select-Verzeichnis zugeordnet werden soll. Ein iterieren durch das TreeView des Auswahl Verzeichnis-Dialog-Fensters scheitert an der Möglichkeit sich im TreeView immer weiter (rekursiv) fortzubewegen. Die Lösung stellt die Definition einer CallBack-Methode (hier BrowseCallbackProc), die man ebenfalls in der BROWSEINFO-Struktur zuordnen kann (bi.lpfn=), dar.

In ihr kann man abfragen, ob die Initalisierung des Dialog-Fensters abgeschlossen wurde und dann eine BFFM_SETSELECTIONW Nachricht an das Dialog-Fenster senden in dessen Parameter das Select-Verzeichnis (sogar als String!) angegeben ist, um den passenden TreeView-Eintrag zu selektieren. Gleichzeitig verschiebt diese Nachricht den selektierten Eintrag noch in den sichtbaren Bereich, aber das funktioniert nicht ganz so zuverlässig.

private int BrowseCallbackProc(IntPtr hwnd, uint msg, IntPtr lParam, IntPtr lpData)
{
    if (msg == API32.BFFM_INITIALIZED)  // Wenn die Initalisierung des Dialog-Fensters abgeschlossen wurde ...
    {
        _ = API32.SendMessage(hwnd, API32.BFFM_SETSELECTIONW, 1, _selectedPath) // Das TreeView-Item mit der Bezeichnung des Select-Verzeichnisses selektieren
    }
    return 0;                                       // Immer das Flag: Erfolgreich zurückgeben
}

Am Ende muss man nur noch den von der SHBrowseForFolder Methode zurückgegebenen PDCList-Eintrag  auswerten, dessen Wert auf das ausgewählte Verzeichnis, verweist.

if (pidlRet == IntPtr.Zero) return MessageBoxResult.Cancel;     // Wenn der Benutzer den Dialog abgebrochen hat, dann ein Abbruch-Ergebnis zurückgeben

directoryPath = GetVerzeichnisString(pidlRet);                    // Ausgewähltes Verzeichnis aus dem Rückgabe PIDCList-Eintrag ermitteln

Wie die Parametrisierung der BROWSEINFO-Struktur im Detail erfolgt oder sonstige Details könnt ihr ebenfalls dem veröffentlichten Projekt: Projekt VerzeichnisDialog Assembly.zip, entnehmen.

Beide Projekte sind ausführlichst dokumentiert.

Für die Praktiker: Hier die GUI zum Testen als Programm (GUI.zip) und den Verzeichnis Auswahl-Dialog als Assembly-DLL (Assembly.zip).

Damit hier nicht der falsche Eindruck entsteht! Das habe ich nicht alles selbst "entwickelt" sondern nur zusammengetragen und für mich so lang aufbereitet, bis es (für mich) passt!!!

Besonderen Dank an charlieface von stackoverflow

Einen zweiten Nachtrag muss ich auch noch machen. Th69 hat in seiner Antwort die Lösung (komprimiert) genau so angegeben.  Leider kann man hier eine Antwort nicht als Lösung deklarieren.

Also auch vielen Dank an dich!!!