-1

To classify the question:

Basically, it's all about a user dialog!!! I work with WPF and don't want to use the FolderBrowserDialog because the Forms NameSpace can only be integrated poorly in a .NETCore WPF project. So I chose the SHBrowseForFolder dialog natively under the API32 in the ShellNameSpace. The FolderBrowserDialog is just a wrapper around this Windows dialog. (I'm building an MVVM-enabled WPF wrapper, for a directory selection dialog.)

No problem, the dialog is displayed, a root directory can be defined ... the only problem is the initial select directory. (Selecting the start directory in the dialog window/TreeView))

For this I use a launcher that is started before the directory dialog is called and (via a timer) "gets" the dialog window and accesses the directory tree view.

Now I want to iterate through the treeview (of the dialog window) and select the treeview item that matches the select directory.

It is precisely at this point that the problem arises that I cannot "access" subordinate items.

The details ...

I'm working with C# and I want to iterate through the TreeView of a Windows FolderDialogBrowser (WINAPI).

I get the pointer to the TreeView via the Windows dialog window (user32.dll -> FindWindow).

IntPtr hwndDlg = FindWindow(null, _topLevelSearchString);  // "get" the handle of the dlg box window

if (hwndDlg != IntPtr.Zero)                                // Has the Dlg box window found?
{
    IntPtr hwndFolderCtrl = GetDlgItem(hwndDlg, _dlgControl); // "get" the dlg box control in the dlg box window

    if (hwndFolderCtrl != IntPtr.Zero) // Has the dlg box control found in the dlg box window?
    {
        IntPtr hwndTV = GetDlgItem(hwndFolderCtrl, _dlgTreeView);  // "Get" the handle of the TreeView of the Dlg box
   ...

Now I want to iterate through this one... as an example with fixed references:

IntPtr RootItem = IntPtr.Zero;                  // Initialize the handle of the root entry
IntPtr AktItem = IntPtr.Zero;                   // handle of the current Initialize TreeView items
string AktVerzeichnis = RootVerzeichnis;        // Initialize current directory

RootItem = SendMessage(pTV, TVM_GETNEXTITEM, new(TVGN_ROOT), IntPtr.Zero);   // Get root entry
AktItem = RootItem;                             // Pointer to the current update entry

if (!SelectVerzeichnis.Equals(AktVerzeichnis))  // Select Directory Is Not Root Directory?
{
    if (AktItem != IntPtr.Zero)                 // Is there a root item?
    {
        IntPtr Item1 = SendMessage(pTV, TVM_GETNEXTITEM, new(TVGN_CHILD), RootItem);   // Item1 entry = get child entry from root entry [level 1]
        System.Diagnostics.Debug.Print("1. Eintrag: " + GetTVItem(pTV, Item1));        // Determine the designation of the 1st entry

        IntPtr Item1_1 = SendMessage(pTV, TVM_GETNEXTITEM, new(TVGN_CHILD), Item1);    // Get 1st sub-item from Item1 [level 2].
        System.Diagnostics.Debug.Print("1.1 Eintrag: " + GetTVItem(pTV, Item1_1));     // Designation of 1.1. Determine entry

        IntPtr Item2 = SendMessage(pTV, TVM_GETNEXTITEM, new(TVGN_NEXT), Item1);       // Get 2nd entry [level 1].
        System.Diagnostics.Debug.Print("2. Eintrag: " + GetTVItem(pTV, Item2));        // Determine the designation of the 2nd entry

        IntPtr Item3 = SendMessage(pTV, TVM_GETNEXTITEM, new(TVGN_NEXT), Item2);       // Get 3rd entry [level 1].
        System.Diagnostics.Debug.Print("3. Eintrag: " + GetTVItem(pTV, Item3));        // Determine the designation of the 3rd entry

        IntPtr Item3_1 = SendMessage(pTV, TVM_GETNEXTITEM, new(TVGN_CHILD), Item3);    // Get 1st sub-item from Item3 [level 2].
        System.Diagnostics.Debug.Print("3.1 Eintrag: " + GetTVItem(pTV, Item3_1));     // Designation of 3.1. Determine entry

The above sequence gives: Item1: Anruf_Info Item1_1: Item2: Anruf_Info REPORT22 Item3: Druck Item3_1:

My problem is that I always don't get the sub-items (both Item1_1 and Item3_1) (=> IntPtr.Zerro).

TVGN_CHILD is (except sub-entry from root entry => Item1 !!!) not work!!!

What can that be???

image

Different calls from:

SendMessage(pTV, TVM_GETNEXTIM, new(TVGN_CHILD), Item1)

I select the entry before retrieving the sub-entry so that (internally through the TreeView) the sub-entries are loaded dynamically.

IntPtr Item1 = SendMessage(pTV, TVM_GETNEXTITEM, new(TVGN_CHILD), RootItem);   // 1. Eintrag = Untergeordneter Eintrag vom Root-Eintrag holen [Ebene 1]
SendMessage(pTV, TVM_SELECTITEM, new IntPtr(TVGN_CARET), Item1);               // 1. Eintrag selektieren
SendMessage(pTV, TVGN_FIRSTVISIBLE, 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

perlfred
  • 1
  • 3
  • 1
    Do you mean a `FolderBrowserDialog` https://learn.microsoft.com/en-us/dotnet/api/system.windows.forms.folderbrowserdialog?view=windowsdesktop-7.0 what are you ultimately trying to achieve by iterating through it? – Charlieface Jul 02 '23 at 15:10
  • @Charlieface! No, it's not the Forms.FolderBrowserDialog but the native API32 SHBrowseForFolder [link](https://www.pinvoke.net/default.aspx/shell32/SHBrowseForFolder.html) call to the Windows window I'm accessing. I would like to search for an entry (by name or PIDCList entry) from this TreeView and set it as a selected entry. – perlfred Jul 02 '23 at 16:53
  • A similar question as [here](https://stackoverflow.com/questions/3660556/how-to-select-an-item-in-a-treeview-using-win32-api). However, Martin Prikryl's "solution" doesn't work for me either. – perlfred Jul 02 '23 at 17:54
  • I still don't get what you are trying to *achieve*, what do you ultimately want to *do*, why is looping through this treeview the right solution? If you want to enumerate files why not just use `Directory.EnumerateFiles`? – Charlieface Jul 02 '23 at 19:45
  • @Charlieface Hello Charlie Face! I put the purpose of my question in front of the question. See: To classify the question: I've also ruled out a possible reason why I can't access the sub-entries: See: What have I tried... at the bottom. **Can you give me any reason for why I can't access the sub items???** – perlfred Jul 03 '23 at 08:41
  • So why don't you just pass a `pidlRoot` parameter in the `BROWSEINFO` struct https://learn.microsoft.com/en-us/windows/win32/api/shlobj_core/ns-shlobj_core-browseinfow – Charlieface Jul 03 '23 at 08:58
  • @Charlieface I **don't!** want to change the root directory of the TreeView but **select** a (defined) entry below the root directory!!! And for that I need exactly **this** entry. And I have to navigate to this entry. See: **question** – perlfred Jul 03 '23 at 09:30

1 Answers1

0

I think you are beating down the wrong path. There is a documented method to change the path to a specific selected path, and this is used in the official FolderBrowserDialog also.

Use a callback function which receives messages from the dialog. In this case you want to catch the INITIALIZED message, and send a SETSELECTION message back.

[DllImport("user32.dll", CharSet = CharSet.Unicode, ExactSpelling = true, SetLastError = true)]
public static extern IntPtr SendMessageW(IntPtr hWnd, uint Msg, IntPtr wParam, [MarshalAs(UnmanagedType.LPWStr)] string lParam);

delegate int BrowseCallback(IntPtr hwnd, int msg, IntPtr lParam, IntPtr lpData);

string _selectedPath;
BrowseCallback _callbackKeepAlive = BrowseCallbackProc;

private int BrowseCallbackProc(IntPtr hwnd, int msg, IntPtr lParam, IntPtr lpData)
{
    // Indicates the browse dialog box has finished initializing. The lpData value is zero. 
    const int BFFM_INITIALIZED = 1;
    const int BFFM_SETSELECTIONW = 0x400 + 103,

    switch (msg)
    {
        case BFFM_INITIALIZED: 
            if (_selectedPath?.Length > 0) 
            {
                SendMessage(hwnd, BFFM_SETSELECTIONW, (IntPtr)1, _selectedPath);
            }
        break;
    }
    return 0;
}

Pass the _callbackKeepAlive value in the lpfn field of your BROWSEINFOW struct. Note the use of a keep alive: you need to make sure the runtime does not dispose your callback before you're finished PInvoke.

Charlieface
  • 52,284
  • 6
  • 19
  • 43
  • Hello Charlie Face! That looks good! :-) However, I am now entering an area that is new to me. So 2 questions. 1. It's unclear to me (and VS), where does the BrowseCallbackProc get the string _selectedPath ? 2. What value should the BrowseCallbackProc return? `Win32API.Shell32.BFFCALLBACK _callbackKeepAlive = BrowseCallbackProc; string _selectedPath = SelectVerzeichnis; Win32API.Shell32.BROWSEINFO bi = new(); ... bi.pidlRoot = pidlRoot; bi.hwndOwner = hWndFenster; bi.lpfn = _callbackKeepAlive; pidlRet = Win32API.Shell32.SHBrowseForFolder(ref bi);` – perlfred Jul 03 '23 at 14:19
  • 1. From wherever it is you have already that you were going to search for (unclear in the question but seems to be `SelectVerzeichnis`). 2. `return 0;` fixed now. 3. Yes that's correct – Charlieface Jul 03 '23 at 14:27
  • Hello Charlieface! **_selectedPath** is not declared anywhere within the BrowseCallbackProc function! Where should the function take the value from??? This cannot be compiled! – perlfred Jul 03 '23 at 16:44
  • Hello Charlieface! 1. I declared _selectedPath public in the class and assign the value in the method where I call the browser dialog. 2. The lParam and wParam parameters must be passed as pointers in the CallbackProc. ´´ IntPtr **pSelPath** = Marshal.StringToHGlobalUni(_selectedPath); Win32API.Shell32.SendMessage(hwnd, Win32API.Shell32.BFFM_SETSELECTIONW, **new(1)** , **pSelPath**); Marshal.FreeHGlobal(pSelPath); ´´ But then it works!!! Thanks very much!!!! :-) :-) – perlfred Jul 03 '23 at 17:37
  • You can update this in your solution to make it suitable for others. – perlfred Jul 03 '23 at 17:44
  • As you can see `_selectedPath` is declared as an instance field outside the function. Do not marshal the string yourself it's unnecessary. Just declare `SendMessage` like I have done, with a `string` parameter, and marshalling will happen automatically. `int` can be implicitly cast to `IntPtr` anyway – Charlieface Jul 03 '23 at 19:46
  • Well, you are a fox! **Thank you** again! `// Overload of SendMessage for BFFCALLBACK [DllImport("user32.dll", CharSet = CharSet.Unicode)] public static extern int SendMessage(IntPtr hWnd, int wMsg, int wParam, string lParam);` – perlfred Jul 04 '23 at 08:51
  • `IntPtr wParam` not `int wParam` – Charlieface Jul 04 '23 at 09:20
  • Ok, BFFM_SETSELECTIONW also works with wParam as an int. If I define a pointer in the wParam declaration, then I also have to pass a pointer in the method call: new(1). Otherwise I get the compiler error: CS1503 Argument "3": Cannot convert "int" to "System.IntPtr". – perlfred Jul 04 '23 at 10:18
  • It works only because of a fluke in the calling convention (placing it in a register) I wouldn't do it as it could fail in some circumstances. Ah you must be on an old .NET version which doesn't have the implicit conversion, yes you need either `new(1)` or `(IntPtr)1` – Charlieface Jul 04 '23 at 10:20