WPF弹出窗口中的控件有时无法接收输入

3
我有一个WPF的CheckBox,它位于Popup内部。如果它位于TreeView的Item模板内部,那么CheckBox就无法响应用户输入。如果它位于TreeView之外,那么就没有问题。
我在这里创建了一个相对较小的模拟:https://github.com/logiclrd/TestControlsInPopupsNotWorking 有人知道为什么从TreeView弹出的CheckBox控件不能被选中吗?

你有没有做过任何确认复选框没有输入的事情(例如创建一个点击事件)?如果它没有得到任何东西,你能找出是什么获取了点击事件吗? - techvice
好的,我可以看到当鼠标悬停在上面时它的样式会改变,所以它确实得到了一些输入。我将在其上放置一些事件处理程序并查看结果。不过有一件令人困惑的事情,我认为Popup实际上代表了可视树的一个新逻辑根 - 它是一个独立的顶级窗口,位于其所有者之上。这是否意味着事件无法从比Popup更高的树上钩取?虽然我还没有测试过。 - Jonathan Gilbert
当您在Visual Studio中进行调试时,可以通过查看Live Visual Tree(也可以查看Live Property Explorer)来验证您的假设。这对于查看窗口的完整视图非常有帮助。 - techvice
实时可视化树对于除了“Popup”元素以外的其他元素都非常有用。它似乎没有将它们包含在树中。我看到了相同的事件序列,对于工作和不工作的“CheckBox”控件:一个“PreviewMouseDown”后跟一个“PreviewMouseUp”。这些事件的处理程序显然将它们标记为已处理,因为主要的“MouseDown”和“MouseUp”事件没有被触发。 - Jonathan Gilbert
我还注意到CheckBoxToggleButton的子类,而TreeView在内部使用ToggleButton,所以我想知道它是否可能正在为ToggleButton设置样式,但我通过将CheckBox放入TreeView的项模板中但在Popup之外来排除了这种可能性 - 那个CheckBox可以正常工作。Popup似乎是导致问题的一个重要部分。 - Jonathan Gilbert
2个回答

3
我认为这是TreeView设计上的疏忽。请看这个例子: 注:一些代码摘录已经被整理过,以避免换行。
// This method is called when MouseButonDown on TreeViewItem and also listen
// for handled events too.  The purpose is to restore focus on TreeView when
// mouse is clicked and focus was outside the TreeView.  Focus goes either to
// selected item (if any) or treeview itself
internal void HandleMouseButtonDown()
{
    if (!this.IsKeyboardFocusWithin)
    {
        if (_selectedContainer != null)
        {
            if (!_selectedContainer.IsKeyboardFocused)
                _selectedContainer.Focus();
        }
        else
        {
            // If we don't have a selection - just focus the TreeView
            this.Focus();
        }
    }
}

该方法是从TreeViewItem.OnMouseButtonDown调用的,我们可以看到它是一个类级别的处理程序,被配置为接收已处理事件
EventManager.RegisterClassHandler(
    typeof(TreeViewItem),
    Mouse.MouseDownEvent,
    new MouseButtonEventHandler(OnMouseButtonDown),
    /* handledEventsToo: */ true);

我已经使用调试器核实过,当事件传递到TreeViewItem时,“Handled”已被设置为“true”。
当您按下鼠标左键时,复选框会开始进行推测性的“单击”操作,并将事件标记为已处理。通常,祖先元素不会看到已处理的事件冒泡,但在这种情况下,它明确要求这样做。
TreeView看到“this.IsKeyboardFocusWithin”解析为“false”,因为焦点元素位于另一个可视树中(弹出窗口),然后将焦点返回到TreeViewItem。
现在,如果您查看ButtonBase:
protected override void OnLostKeyboardFocus(KeyboardFocusChangedEventArgs e)
{
    base.OnLostKeyboardFocus(e);

    if (ClickMode == ClickMode.Hover)
    {
        // Ignore when in hover-click mode.
        return;
    }

    if (e.OriginalSource == this)
    {
        if (IsPressed)
        {
            SetIsPressed(false);
        }

        if (IsMouseCaptured)
            ReleaseMouseCapture();

        IsSpaceKeyDown = false;
    }
}

我们可以看到,当失去焦点时,IsPressed被设置为false。如果我们转到OnMouseLeftButtonUp,我们会看到这样的代码:
bool shouldClick = !IsSpaceKeyDown && IsPressed && ClickMode == ClickMode.Release;

IsPressed现在为false,单击操作永远不会完成,这是因为当您尝试单击按钮时,TreeViewItem夺走了焦点。


我正在尝试找到一种解决方法,包括可能调用类的非公共成员,但迄今为止我还没有想出任何有效的方法。我看不到任何取消注册类处理程序的方法,这就是TreeViewItem挂接MouseDown事件的方式。事件顺序似乎意味着我无法使用任何技巧来说服TreeViewMouseDown事件发生时将IsKeyboardFocusWithin设置为true - 看起来它在我之前获取了该事件。你有什么想法吗? - Jonathan Gilbert
1
我已经在Microsoft Developer Community上为此记录了一个错误报告,如果您认为它值得的话,可以对其进行“投票” :-):https://developercommunity.visualstudio.com/content/problem/190202/button-controls-hosted-in-popup-windows-do-not-wor.html - Jonathan Gilbert
我也通过Microsoft Connect进行了记录,尽管我听说Microsoft现在基本上忽略了Connect。这是链接,供参考:https://connect.microsoft.com/VisualStudio/feedback/details/3146283 - Jonathan Gilbert
谢谢,Jonathan!我已经点赞了这两个帖子,但我并不抱太大的希望,因为可能没有人会去阅读它们,更别提实现修复了 ;)。 - Mike Strobel
作为一种解决方法,到目前为止,我已经成功地使用了NuGet库Ryder(它看起来像是Microsoft Detours的免费可用开源版本)来拦截TreeView中的HandleMouseButtonDown方法。 - Jonathan Gilbert

0
作为解决方案,我迄今已经成功地使用了NuGet库Ryder(它看起来像是Microsoft Detours的免费可用开源版本(MIT许可证)),以截取TreeView中HandleMouseButtonDown方法。
Ryder库可以在NuGet库中找到,其代码可以在此处找到:

https://github.com/6A/Ryder

挂钩 HandleMouseButtonDown 方法非常简单:

        var realMethod = typeof(System.Windows.Controls.TreeView).GetMethod("HandleMouseButtonDown", BindingFlags.Instance | BindingFlags.NonPublic);
        var replacementMethod = typeof(Program).GetMethod(nameof(TreeView_HandleMouseButtonDown_shim), BindingFlags.Static | BindingFlags.NonPublic);

        Redirection.Redirect(realMethod, replacementMethod);

替换方法的 shim 基本上可以做到真实方法所能做到的,但是它修复了检测跨可视树焦点情况的问题:

    static void TreeView_HandleMouseButtonDown_shim(TreeView @this)
    {
        // Fix as seen in: https://developercommunity.visualstudio.com/content/problem/190202/button-controls-hosted-in-popup-windows-do-not-wor.html
        if (!@this.IsKeyboardFocusWithin)
        {
            // BEGIN NEW LINES OF CODE
            var keyboardFocusedControl = Keyboard.FocusedElement;

            var focusPathTrace = keyboardFocusedControl as DependencyObject;

            while (focusPathTrace != null)
            {
                if (ReferenceEquals(@this, focusPathTrace))
                    return;

                focusPathTrace = VisualTreeHelper.GetParent(focusPathTrace) ?? LogicalTreeHelper.GetParent(focusPathTrace);
            }
            // END NEW LINES OF CODE

            var selectedContainer = (System.Windows.Controls.TreeViewItem)TreeView_selectedContainer_field.GetValue(@this);

            if (selectedContainer != null)
            {
                if (!selectedContainer.IsKeyboardFocused)
                    selectedContainer.Focus();
            }
            else
            {
                // If we don't have a selection - just focus the treeview
                @this.Focus();
            }
        }
    }

需要进行一些反思,因为这与一个未从TreeView类中公开的私有字段交互,但作为解决方法,这比我最初尝试的要少得多,那是从参考源代码库中导入整个TreeView类(和相关类型)到我的项目中以更改一个成员。 :-)

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