为什么FolderBrowserDialog对话框不会滚动到所选文件夹?

81

如下截图所示,被选文件夹未在视图中显示,需要向下滚动才能查看被选文件夹。

enter image description here

同样的对话框在另一台电脑上可以看到选定的文件夹。

enter image description here

我在两台都运行了Windows 7的计算机上运行了它。它在其中一个上正常工作,但在第二个上没有工作。这似乎是Windows环境问题而不是代码问题?有人能提供任何修复建议吗?

代码没有更改。我从不同驱动器使用了更长的路径,但结果相同。

private void TestDialog_Click ( object sender, EventArgs e )
        {
            //Last path store the selected path, to show the same directory as selected on next application launch.
            //Properties.Settings.Default.LastPath

            FolderBrowserDialog dlgFolder = new FolderBrowserDialog ();

            dlgFolder.RootFolder = Environment.SpecialFolder.DesktopDirectory;

            dlgFolder.SelectedPath = Properties.Settings.Default.LastPath;

            if (dlgFolder.ShowDialog () == System.Windows.Forms.DialogResult.OK)
            {

                Properties.Settings.Default.LastPath = dlgFolder.SelectedPath;               

                Properties.Settings.Default.Save ();
            }

        }

是的,这是环境相关的。该对话框在Windows中实现,而不是Silverlight中。很可能是Windows的错误,我打赌通常不存在的“文件夹”文本框是根本原因。如果没有它,“问题”文件夹将可见。如果您想追究此事,请联系Microsoft支持。 - Hans Passant
16个回答

91
根本问题在于FolderBrowserDialog中的一个糟糕的设计决策。首先,我们需要认识到FolderBrowserDialog不是.NET控件,而是Windows的Common Dialog的一部分。此对话框的设计者选择在显示对话框并选择初始文件夹后不向TreeView控件发送TVM_ENSUREVISIBLE消息。这个消息会导致TreeView控件滚动,以便当前选定的项目在窗口中可见。
因此,我们只需要向FolderBrowserDialog中的TreeView发送TVM_ENSUREVISIBLE消息,一切都会很好。对吗?嗯,并非如此简单。这确实是答案,但有一些事情阻碍了我们。
  • 首先,由于FolderBrowserDialog实际上不是.NET控件,所以它没有内部Controls集合。这意味着我们无法从.NET中找到和访问TreeView子控件。

  • 其次,.NET FolderBrowserDialog类的设计者决定将此类封装。这个不幸的决定阻止了我们从中派生并覆盖窗口消息处理程序。如果我们能够做到这一点,我们可能会尝试在消息处理程序中收到WM_SHOWWINDOW消息时发布TVM_ENSUREVISIBLE消息。

  • 第三个问题是,我们不能发送TVM_ENSUREVISIBLE消息,直到TreeView控件实际上作为一个真正的窗口存在,而它不存在,直到我们调用ShowDialog方法。但是,这个方法会阻塞,所以一旦调用此方法,我们就没有机会发布我们的消息。

为了解决这些问题,我创建了一个静态帮助器类,其中包含一个单独的方法,可用于显示FolderBrowserDialog,并将其滚动到选定的文件夹。我通过在调用对话框的ShowDialog方法之前启动一个短的Timer,然后在Timer处理程序(即,在显示对话框后)中跟踪TreeView控件的句柄来管理此操作,并发送我们的TVM_ENSUREVISIBLE消息。

这个解决方案并不完美,因为它依赖于一些关于FolderBrowserDialog的先前知识。具体来说,我使用窗口标题找到对话框。这将在非英语安装中出现问题。我使用对话框项ID而不是标题文本或类名来跟踪对话框中的子控件,因为我认为这会随着时间的推移更可靠。
此代码已在Windows 7(64位)和Windows XP上进行了测试。
以下是代码: (您可能需要:using System.Runtime.InteropServices;
public static class FolderBrowserLauncher
{
    /// <summary>
    /// Using title text to look for the top level dialog window is fragile.
    /// In particular, this will fail in non-English applications.
    /// </summary>
    const string _topLevelSearchString = "Browse For Folder";

    /// <summary>
    /// These should be more robust.  We find the correct child controls in the dialog
    /// by using the GetDlgItem method, rather than the FindWindow(Ex) method,
    /// because the dialog item IDs should be constant.
    /// </summary>
    const int _dlgItemBrowseControl = 0;
    const int _dlgItemTreeView = 100;

    [DllImport("user32.dll", SetLastError = true)]
    static extern IntPtr FindWindow(string lpClassName, string lpWindowName);

    [DllImport("user32.dll")]
    static extern IntPtr GetDlgItem(IntPtr hDlg, int nIDDlgItem);

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

    /// <summary>
    /// Some of the messages that the Tree View control will respond to
    /// </summary>
    private const int TV_FIRST = 0x1100;
    private const int TVM_SELECTITEM = (TV_FIRST + 11);
    private const int TVM_GETNEXTITEM = (TV_FIRST + 10);
    private const int TVM_GETITEM = (TV_FIRST + 12);
    private const int TVM_ENSUREVISIBLE = (TV_FIRST + 20);

    /// <summary>
    /// Constants used to identity specific items in the Tree View control
    /// </summary>
    private const int TVGN_ROOT = 0x0;
    private const int TVGN_NEXT = 0x1;
    private const int TVGN_CHILD = 0x4;
    private const int TVGN_FIRSTVISIBLE = 0x5;
    private const int TVGN_NEXTVISIBLE = 0x6;
    private const int TVGN_CARET = 0x9;


    /// <summary>
    /// Calling this method is identical to calling the ShowDialog method of the provided
    /// FolderBrowserDialog, except that an attempt will be made to scroll the Tree View
    /// to make the currently selected folder visible in the dialog window.
    /// </summary>
    /// <param name="dlg"></param>
    /// <param name="parent"></param>
    /// <returns></returns>
    public static DialogResult ShowFolderBrowser( FolderBrowserDialog dlg, IWin32Window parent = null )
    {
        DialogResult result = DialogResult.Cancel;
        int retries = 10;

        using (Timer t = new Timer())
        {
            t.Tick += (s, a) =>
            {
                if (retries > 0)
                {
                    --retries;
                    IntPtr hwndDlg = FindWindow((string)null, _topLevelSearchString);
                    if (hwndDlg != IntPtr.Zero)
                    {
                        IntPtr hwndFolderCtrl = GetDlgItem(hwndDlg, _dlgItemBrowseControl);
                        if (hwndFolderCtrl != IntPtr.Zero)
                        {
                            IntPtr hwndTV = GetDlgItem(hwndFolderCtrl, _dlgItemTreeView);

                            if (hwndTV != IntPtr.Zero)
                            {
                                IntPtr item = SendMessage(hwndTV, (uint)TVM_GETNEXTITEM, new IntPtr(TVGN_CARET), IntPtr.Zero);
                                if (item != IntPtr.Zero)
                                {
                                    SendMessage(hwndTV, TVM_ENSUREVISIBLE, IntPtr.Zero, item);
                                    retries = 0;
                                    t.Stop();
                                }
                            }
                        }
                    }
                }

                else
                {
                    //
                    //  We failed to find the Tree View control.
                    //
                    //  As a fall back (and this is an UberUgly hack), we will send
                    //  some fake keystrokes to the application in an attempt to force
                    //  the Tree View to scroll to the selected item.
                    //
                    t.Stop();
                    SendKeys.Send("{TAB}{TAB}{DOWN}{DOWN}{UP}{UP}");
                }
            };

            t.Interval = 10;
            t.Start();

            result = dlg.ShowDialog( parent );
        }

        return result;
    }
}

8
应该将此标记为答案。我刚遇到了同样的问题,这段代码完美地解决了它。这也是一篇非常详细和写得很好的解释。 - Dan
@Syspect - IWin32Window参数只是启动文件夹选择器的父窗体。如果您直接从表单代码调用此函数,可以将“this”关键字用作参数。(从技术上讲,IWin32Window实际上是围绕窗体后面的hWnd的包装器,但C#会为您隐藏所有与此相关的丑陋内容!) - Brad Oestreicher
2
在Win7上,我观察到当系统文件夹(如图书馆等)在对话框初始显示后添加到树中时,会发生滚动然后重置的情况。设置一个初始间隔为1000毫秒足以克服这个问题,尽管这只是又增加了一张牌! - Jonathan Mitchell
1
在Win10上,就像@Jonathan Mitchell所指出的那样,存在一个时间问题。将t.Interval = 100;设置为足以解决我的机器上的这个问题(对于速度较慢的机器来说需要更长时间吗?)。 - avenmore
@Brad Oestreicher - 我在Windows 10上测试了你的示例,但它没有起作用。它根本没有滚动到所选目录。 - Munawar
显示剩余7条评论

11

我知道这个帖子早就过时了,但是通过扩展方法,可以将此方法添加到FolderBrowserDialog.ShowDialog方法中,并在需要的地方重复使用。

下面的示例仅使用简单的SendKeys方法(我很讨厌这样做,但在这种情况下,它可以很好地工作)。当使用SendKeys方法跳转到对话框中选定的文件夹时,如果您在Visual Studio中调试此代码,则SendKeys调用会应用于当前窗口,即活动的VS窗口。为了更加安全可靠并避免将SendKeys消息发送到错误的窗口,扩展方法将包含类似于Marc F发布的特定窗口发送消息的外部方法调用,但翻译成C#。

internal static class FolderBrowserDialogExtension
{
    public static DialogResult ShowDialog(this FolderBrowserDialog dialog, bool scrollIntoView)
    {
        return ShowDialog(dialog, null, scrollIntoView);
    }

    public static DialogResult ShowDialog(this FolderBrowserDialog dialog, IWin32Window owner, bool scrollIntoView)
    {
        if (scrollIntoView)
        {
            SendKeys.Send("{TAB}{TAB}{RIGHT}");
        }

        return dialog.ShowDialog(owner);
    }
}

1
这对我在x64 Windows 8操作系统上非常有帮助。然而,我通过在500毫秒后在Timer_Tick事件中执行sendkeys来扩展它,因为它移动到所选文件夹,然后恢复到该文件夹的根驱动器。因此需要延迟。 - hynsey

10
在VB.Net代码中,在显示对话框之前添加以下代码行。
SendKeys.Send ("{TAB}{TAB}{RIGHT}")

这个答案太棒了!暂时解决了我的问题..真的很讨厌不得不向下滚动选择.. :+1: 如果由我来决定,这将是我接受的答案 :P - Dan Bradbury
很遗憾,这在我的系统(Windows 10)上不起作用; - Phil Rogers
@Phil,如果你的“新建文件夹”按钮已经显示,SendKeys.Send ("{RIGHT}") 对我有效。 - Ste

9

我使用了从https://www.daniweb.com/software-development/csharp/threads/300578/folderbrowserdialog-expanding-the-selected-directory-得到的解决方法。

该链接提供了一种处理 C# 文件夹浏览对话框的方法。
FolderBrowserDialog^ oFBD = gcnew FolderBrowserDialog;
oFBD->RootFolder = Environment::SpecialFolder::MyComputer;
oFBD->SelectedPath = i_sPathImport;
oFBD->ShowNewFolderButton = false;     // use if appropriate in your application
SendKeys::Send ("{TAB}{TAB}{RIGHT}");  // <<-- Workaround
::DialogResult oResult = oFBD->ShowDialog ();

这不是最好的方式,但它对我有效。
没有RootFolder在第一次调用时不起作用,但在第二次及以后可以。有了它,它总是有效的。

正如其他人所观察到的,这个失败取决于操作系统:
我正在使用Win 7 Pro x64 SP1。


1
对我有效。有趣的是学到了在键盘上使用tab-tab-right箭头序列可以滚动到选定的目录。在C#中:SendKeys.Send("{TAB}{TAB}{RIGHT}"); - Roland
1
这个失败:我想“这个”指的是SendKeys技巧,而“failure”应该是“特性”。 - Roland

5

这对我有效

folderBrowserDialog1.Reset();  
folderBrowserDialog1.RootFolder = Environment.SpecialFolder.MyComputer;
folderBrowserDialog1.SelectedPath = WorkingFolder;

但仅在对话框第二次使用后才能这样做。

3

我发现:

  1. 如果.SelectedPath以"\ "结尾,对话框将向下滚动以使路径可见。
  2. 如果.SelectedPath不以"\ "结尾,则仍选择该路径,但不能确保可见。

抱歉:此解决方案仅在一半的情况下有效。似乎存在某种竞态条件。注意:目录应存在。 - Aleksandr
我还没有看到这个起作用。选择总是停留在根节点。 - Phil Rogers
这个技巧并不可靠,因为它只在某些情况下有效(在Windows 10上测试过)。请注意,对话框返回的选定路径没有尾部斜杠。 - AntonK

3

我在不同的论坛上读到,这可能是由于RootFolder引起的,因为SelectedPath和RootFolder是相互排斥的,这意味着两者不能共存,但使用默认的RootFolder(.Desktop),它允许至少爬树(浏览驱动器/文件夹)。

然而,如果RootFolder更改为除桌面外的其他内容,则无法导航到UNC路径。

回答Hans Passant: 我尝试了这个带有TextBox的对话框扩展,但没有成功。

自定义浏览文件夹对话框以显示路径


2

我在c++ /mfc中遇到了同样的问题。在BFFM_INITIALIZED回调函数中,使用::PostMessage而不是::SendMessage对我很有帮助,以便放置TVM_ENSUREVISIBLE消息。

    case BFFM_INITIALIZED: 
{
// select something
::SendMessage(m_hDialogBox, BFFM_SETSELECTION, TRUE, (LPARAM) pszSelection);


// find tree control
m_hTreeCtrl = 0;
HWND hchild = GetWindow(hWnd, GW_CHILD) ;
while (hchild != NULL)
{
  VS_TChar classname[200] ;
  GetClassName(hchild, classname, 200) ;

  if (VS_strcmp(classname, _T("SHBrowseForFolder ShellNameSpace Control")) == 0)
  {
    HWND hlistctrl = GetWindow(hchild, GW_CHILD) ;
    do
    { 
      GetClassName(hlistctrl, classname, 200) ;
      if (lstrcmp(classname, _T("SysTreeView32")) == 0)
      {
        m_hTreeCtrl = hlistctrl;
        break ;   
      }

      hlistctrl = GetWindow(hlistctrl, GW_HWNDNEXT) ;
    } while (hlistctrl != NULL);
  }      
  if (m_hTreeCtrl)
    break;
  hchild = GetWindow(hchild, GW_HWNDNEXT);      
}

if (m_hTreeCtrl)
{
  int item = ::SendMessage(m_hTreeCtrl, TVM_GETNEXTITEM, TVGN_CARET, 0);
  if (item != 0)             
    ::PostMessage(m_hTreeCtrl, TVM_ENSUREVISIBLE,0,item);
}
break;
}

2

我在VB.NET中计算了一些东西,所以将其转换为C#会很容易。 我是法国人,也是VB的初学者。 无论如何,你可以尝试我的解决方案。

我的想法是在显示folderBrowserDialog之前启动异步任务。

我自己发现了这个方法,但是受到Brad帖子的启发。 以下是我的代码:

Imports System.Threading.Tasks
Imports Microsoft.VisualBasic.FileIO.FileSystem

Public Enum GW
    HWNDFIRST = 0
    HWNDLAST = 1
    HWNDNEXT = 2
    HWNDPREV = 3
    OWNER = 4
    CHILD = 5
    ENABLEDPOPUP = 6
End Enum

Public Declare Function SendMessageW Lib "user32.dll" (ByVal hWnd As IntPtr, ByVal msg As UInteger, ByVal wParam As Integer, <MarshalAs(UnmanagedType.LPWStr)> ByVal lParam As String) As IntPtr
Public Declare Function FindWindowExW Lib "user32.dll" (ByVal hWndParent As IntPtr, ByVal hWndChildAfter As IntPtr, <MarshalAs(UnmanagedType.LPWStr)> ByVal lpszClass As String, <MarshalAs(UnmanagedType.LPWStr)> ByVal lpszWindow As String) As IntPtr
Public Declare Function GetWindow Lib "user32" (ByVal hwnd As IntPtr, ByVal wCmd As Long) As Long
Public Declare Function GetDesktopWindow Lib "user32" () As IntPtr
Public Declare Function GetClassName Lib "user32" Alias "GetClassNameA" (ByVal hwnd As IntPtr, ByVal lpClassName As System.Text.StringBuilder, ByVal nMaxCount As Integer) As Integer

Private Sub FolderBrowserDialog_EnsureVisible(FB As FolderBrowserDialog, _Owner As IntPtr)
    Dim hwnd As IntPtr
    Dim sClassname As New System.Text.StringBuilder(256)
    Thread.Sleep(50)                                     'necessary to let FolderBrowserDialog construct its window
    hwnd = GetDesktopWindow()                            'Desktop window handle.
    hwnd = GetWindow(hwnd, GW.CHILD)                     'We will find all children.
    Do Until hwnd = 0
        If GetWindow(hwnd, GW.OWNER) = _Owner Then       'If one window is owned by our main window...
            GetClassName(hwnd, sClassname, 255)
            If sClassname.ToString = "#32770" Then       'Check if the class is FolderBrowserDialog.
                Exit Do                                  'Then we found it.
            End If
        End If
        hwnd = GetWindow(hwnd, GW.HWNDNEXT)              'Next window.
    Loop                                                 'If no found then exit.
    If hwnd = 0 Then Exit Sub
    Dim hChild As IntPtr = 0
    Dim hTreeView As IntPtr = 0
    Dim i As Integer = 0
    Do
        i += 1
        If i > 1000 Then Exit Sub                                       'Security to avoid infinite loop.
        hChild = FindWindowExW(hwnd, hChild, Nothing, Nothing)          'Look for children windows of FolderBrowserDialog.
        hTreeView = FindWindowExW(hChild, 0, "SysTreeView32", Nothing)  'Look for treeview of FolderBrowserDialog.
        Thread.Sleep(5)                                                 'delay necessary because FolderBrowserDialog is in construction, then treeview maybe not yet exist.
    Loop While hTreeView = 0
    If SendMessageW(hwnd, &H46A, 1, FB.SelectedPath) = 0 Then           'Send message BFFM_SETEXPANDED to FolderBrowserDialog.
        SendMessageW(hTreeView, &H7, 0, Nothing)                        'Send message WM_SETFOCUS to the treeeview.
    End If
End Sub


Dim My_save_dir = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments) & "\My-Saves"

Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
    Dim FolderBrowserDialog1 As New FolderBrowserDialog
    FolderBrowserDialog1.Description = "Choose your save files path."
    If Directory.Exists(My_save_dir) Then
        FolderBrowserDialog1.SelectedPath = My_save_dir
    Else
        FolderBrowserDialog1.SelectedPath = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData)
    End If

    Dim Me_handle = Me.Handle         'Store the main handle to compare after with each windows owner.
    Task.Run(Sub() FolderBrowserDialog_EnsureVisible(FolderBrowserDialog1, Me_handle))      'Here's the trick, run an asynchronous task to modify the folderdialog.
    If FolderBrowserDialog1.ShowDialog(Me) = System.Windows.Forms.DialogResult.OK Then
        My_save_dir = FolderBrowserDialog1.SelectedPath
    End If
End Sub

我正在等待您的建议。 由于我不懂C#,希望有人能够将其翻译成C#。


这个问题很久了,所以我不抱太大希望会有人很快回复。感谢您的意见! - ZygD
1
这个问题应该在自己的问题中提出。在这里不太可能被看到。 - Stuart Siegler

1
我已经阅读了上面的讨论和解决方案。特别是Brat Oestreicher给了我正确的方向。本质上,我们必须先在SHBrowseForFolder对话框中找到TreeView控件,并向该窗口发送TVM_ENSUREVISIBLE消息。以下是使用C语言实现此操作的代码。
#include <windows.h>
#include <objbase.h>
#include <objidl.h>
#include <Shlobj.h>
#include <Dsclient.h>
#include <wchar.h>
// 
//  EnumCallback - Callback function for EnumWindows 
// 
static BOOL CALLBACK EnumCallback(HWND hWndChild, LPARAM lParam)
{
   char szClass[MAX_PATH];
   HTREEITEM hNode;
   if (GetClassName(hWndChild, szClass, sizeof(szClass))
   &&  strcmp(szClass,"SysTreeView32")==0) {
      hNode = TreeView_GetSelection(hWndChild);    // found the tree view window
      TreeView_EnsureVisible (hWndChild, hNode);   // ensure its selection is visible
      return(FALSE);   // done; stop enumerating
   }
   return(TRUE);       // continue enumerating
}
// 
//  BrowseCallbackProc - Callback function for SHBrowseForFolder 
// 
static INT CALLBACK BrowseCallbackProc (HWND hWnd, UINT uMsg, LPARAM lParam, LPARAM lpData) 
{
    switch (uMsg) 
    { 
        case BFFM_INITIALIZED:
            SendMessage (hWnd, BFFM_SETEXPANDED, TRUE, lpData);    // expand the tree view
            SendMessage (hWnd, BFFM_SETSELECTION, TRUE, lpData);   // select the item
            break;
        case BFFM_SELCHANGED:
            EnumChildWindows(hWnd, EnumCallback,0);
            break;
    } 
    return 0; 
} 
// 
//  SelectDirectory - User callable entry point 
// 
int SelectDirectory (HWND hWndParent, char *path, int pathSize) 
{ 
    BROWSEINFO bi = {0};
    LPITEMIDLIST pidl = NULL;
    wchar_t ws[MAX_PATH];

    CoInitialize(0);
    if (pathSize < MAX_PATH) return(FALSE);

    swprintf(ws, MAX_PATH, L"%hs", path);

    bi.hwndOwner = hWndParent; 
    bi.lpszTitle = "Select Directory"; 
    bi.ulFlags = BIF_RETURNONLYFSDIRS | BIF_NEWDIALOGSTYLE;
    bi.lpfn = BrowseCallbackProc;
    bi.lParam = (LPARAM) ws;

    pidl = SHBrowseForFolder (&bi); 
    if (pidl != NULL) 
    { 
        LPMALLOC pMalloc = NULL; 
        SHGetPathFromIDList (pidl, path);
        path[pathSize-1]= '\0';

        SHGetMalloc(&pMalloc);
        pMalloc->lpVtbl->Free(pMalloc,pidl);    // deallocate item 
        pMalloc->lpVtbl->Release(pMalloc);

        return (TRUE);
    } 
    return (FALSE);
} 

非常感谢Gary Beene

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接