根本问题是糟糕的设计决策FolderBrowserDialog
。首先,我们需要认识到,FolderBrowserDialog
不是 .NET 控件,而是Common Dialog
并且是 Windows 的一部分。该对话框的设计者选择不向 TreeView 控件发送TVM_ENSUREVISIBLE
显示对话框并选择初始文件夹后的消息。此消息导致 TreeView 控件滚动,以便当前选定的项目在窗口中可见。
因此,解决这个问题我们需要做的就是发送 TreeView,它是FolderBrowserDialog
the TVM_ENSUREVISIBLE
消息,一切都会很棒。正确的?嗯,没那么快。这确实是答案,但有些事情阻碍了我们。
首先,因为FolderBrowserDialog
并不是真正的 .NET 控件,它没有内部Controls
收藏。这意味着我们不能仅仅从.NET 中查找和访问TreeView 子控件。
二、.NET的设计者FolderBrowserDialog
班级决定seal这个班。这个不幸的决定阻止我们从中派生并覆盖窗口消息处理程序。如果我们能够做到这一点,我们可能会尝试发布TVM_ENSUREVISIBLE
当我们收到消息时WM_SHOWWINDOW
消息处理程序中的消息。
第三个问题是我们无法发送TVM_ENSUREVISIBLE
消息直到树视图控件实际上作为真实窗口存在,并且直到我们调用ShowDialog
方法。但是,此方法会阻塞,因此一旦调用此方法,我们将没有机会发布消息。
为了解决这些问题,我创建了一个静态帮助器类,其中包含一个方法,可用于显示FolderBrowserDialog
,并将使其滚动到所选文件夹。我通过开始一个短片来解决这个问题Timer
就在调用对话之前ShowDialog
方法,然后追踪该句柄TreeView
控制在Timer
处理程序(即,显示对话后)并发送我们的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;
}
}