如何解决LostFocus/LostKeyboardFocus问题?

12

好的,我有一个控件,它有一个IsEditing属性,用于编辑模式下默认使用文本块作为模板,但当IsEditing为true时,会替换成文本框进行原地编辑。现在,当控件失去焦点时,如果它仍在编辑中,则应该退出编辑模式并切换回TextBlock模板。这很简单明了,对吧?

可以将其想象为在Windows资源管理器或桌面上重命名文件时的行为(我知道它们是一样的...),那就是我们想要的行为。

问题在于您不能使用LostFocus事件,因为当您切换到另一个窗口(或是一个FocusManager元素)时,LostFocus不会触发,因为控件仍然具有逻辑焦点,所以这不起作用。

如果改用LostKeyboardFocus,虽然它解决了“其他FocusManager”问题,但现在又产生了一个新问题:当您正在编辑并右键单击文本框以显示上下文菜单时,由于上下文菜单现在具有键盘焦点,您的控件失去了键盘焦点,退出了编辑模式并关闭了上下文菜单,这会使用户感到困惑!

现在,我已经尝试设置一个标志以忽略LostKeyboardFocus事件,然后在LostKeyboardFocus事件中使用该标志来确定是退出编辑模式还是不退出,但如果菜单已经打开,而我在应用程序的其他地方单击,由于控件本身不再具有键盘焦点(菜单具有焦点),所以控件永远不会再次触发LostKeyboardFocus事件,因此它仍处于编辑模式。(如果菜单关闭时添加一个检查,查看焦点在哪里,然后手动将其从EditMode中退出,则似乎很有前途。)

那么...有人有什么想法可以成功编写这个行为的代码吗?

马克


2
我认为阅读所有内容可能有点太多了。如果您能提供一些小结、要点和代码示例,那就太好了。只有这些东西看起来有趣,人们才会详细阅读并回答您的问题。 - Akash Kava
5
你需要细节才能理解我在说什么。我个人喜欢像这样更长的解释,因为我知道程序员已经经历了什么。我发现当我不这样做时,人们会继续发布我已经尝试过的确切解决方案,即使我已经解释了为什么它们不起作用,因为他们不理解为什么它们不起作用。简单地说,这不是一个可以用项目符号简化的“新手”网站的基础问题,因此我首先在SO上发布。 - Mark A. Donohoe
4
哦,天啊...我一直以为SO(Stack Overflow)不需要TL;DR规则——毕竟程序员应该能够读懂! - Vladislav Zorov
谢谢,@Vladislav Zorov!完全同意! - Mark A. Donohoe
1
是的,@PaulMcCarthy,但即使如此,当上下文菜单弹出时,你仍然无法使用它,因为你仍然有失去键盘焦点的问题。这就是为什么你需要两者的组合。 - Mark A. Donohoe
显示剩余2条评论
6个回答

11

好的......这很有趣,就像程序员的乐趣一样。要找出其中的奥秘其实挺费劲的,但是我做到了,并因此脸上洋溢着灿烂的微笑。(现在得花点时间给肩膀涂些IcyHot,因为我拍自己的肩膀拍得太用力了! :P )

总之,这是一个多步骤的过程,但一旦你搞清楚了一切,它就会令人惊讶地简单。简短来说,你需要同时使用 both LostFocusand LostKeyboardFocus,而不是其中之一。

LostFocus 很容易。每当你接收到该事件时,将 IsEditing 设置为 false。完成了。

上下文菜单和失去键盘焦点

LostKeyboardFocus 有点棘手,因为控件的上下文菜单可以在其本身上触发它 (即当控件的上下文菜单打开时,控件仍然具有焦点,但它失去了键盘焦点,因此 LostKeyboardFocus 被触发)。

为了处理这种行为,你需要覆盖 ContextMenuOpening (或处理该事件) 并设置一个类级标志来指示菜单正在打开。 (我使用 bool _ContextMenuIsOpening。) 然后在 LostKeyboardFocus 覆盖 (或事件) 中,你检查该标志,如果它被设置了,那么你只需清除它,而不做其他任何事情。如果没有设置,则意味着除了上下文菜单打开之外,还有其他原因导致控件失去键盘焦点,在这种情况下,你确实希望将 IsEditing 设置为 false。

已打开的上下文菜单

现在有一种奇怪的行为,即如果控件的上下文菜单已经打开,并且因此控件已经像上面描述的那样失去了键盘焦点,如果你在应用程序中的其他地方点击了一下,在新控件获得焦点之前,你的控件首先获得了键盘焦点,但只持续了短暂的一瞬间,然后立即将焦点移交给新控件。

这实际上对我们有利,因为这意味着我们也会得到另一个 LostKeyboardFocus 事件,但这次 _ContextMenuOpening 标志将被设置为 false,就像上面描述的那样,我们的 LostKeyboardFocus 处理程序然后将 IsEditing 设置为 false,这正是我们想要的。我喜欢这种偶然发生的事情!

如果焦点仅仅从你单击的控件转移到了没有先将焦点返回到拥有上下文菜单的控件,则需要钩住 ContextMenuClosing 事件并检查将要获得焦点的控件,然后只有当即将获得焦点的控件不是生成上下文菜单的控件时,我们才会将 IsEditing 设置为 false,因此我们基本上

现在还有一点需要注意的是,如果你正在使用类似文本框之类的控件,并且没有显式地设置自己的上下文菜单,则你将无法获得ContextMenuOpening事件,这让我感到惊讶。不过,这很容易解决,只需创建一个与默认上下文菜单具有相同标准命令(如剪切、复制、粘贴等)的新上下文菜单,并将其分配给文本框。它看起来完全一样,但现在您可以获得所需的事件以设置标志。

然而,即使在这种情况下,你仍然会遇到一个问题,因为如果你正在创建一个第三方可重用控件,而该控件的用户想要拥有他们自己的上下文菜单,你可能会意外地将你的上下文菜单设置为更高的优先级,并覆盖他们的!

解决这个问题的方法是,由于文本框实际上是我控件的IsEditing模板中的一个项目,所以我只需在外部控件上添加一个名为IsEditingContextMenu的新DP,然后通过内部的TextBox样式将其绑定到文本框上,接着我在该样式中添加一个DataTrigger,检查外部控件上IsEditingContextMenu的值,如果为null,则设置默认菜单,该菜单存储在一个资源中。

以下是文本框的内部样式(名为“Root”的元素表示用户实际上插入到他们的XAML中的外部控件)...

<Style x:Key="InlineTextbox" TargetType="TextBox">

    <Setter Property="OverridesDefaultStyle" Value="True"/>
    <Setter Property="FocusVisualStyle"      Value="{x:Null}" />
    <Setter Property="ContextMenu"           Value="{Binding IsEditingContextMenu, ElementName=Root}" />

    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type TextBoxBase}">

                <Border Background="White" BorderBrush="LightGray" BorderThickness="1" CornerRadius="1">
                    <ScrollViewer x:Name="PART_ContentHost" />
                </Border>

            </ControlTemplate>
        </Setter.Value>
    </Setter>

    <Style.Triggers>
        <DataTrigger Binding="{Binding IsEditingContextMenu, RelativeSource={RelativeSource AncestorType=local:EditableTextBlock}}" Value="{x:Null}">
            <Setter Property="ContextMenu">
                <Setter.Value>
                    <ContextMenu>
                        <MenuItem Command="ApplicationCommands.Cut" />
                        <MenuItem Command="ApplicationCommands.Copy" />
                        <MenuItem Command="ApplicationCommands.Paste" />
                    </ContextMenu>
                </Setter.Value>
            </Setter>
        </DataTrigger>
    </Style.Triggers>

</Style>
请注意,您必须在样式中设置初始上下文菜单绑定,而不是直接在文本框上设置,否则样式的 DataTrigger 将被直接设置的值所取代,使触发器无效,如果使用 'null' 作为上下文菜单,则又回到了原点。(如果想要禁止菜单,您不会使用 'null'。您将将其设置为空菜单,因为 null 表示“使用默认值”)
因此,现在用户可以在 IsEditing 为 false 时使用常规的 ContextMenu 属性... 当 IsEditing 为 true 时,他们可以使用 IsEditingContextMenu,如果他们没有指定 IsEditingContextMenu,则文本框将使用我们定义的内部默认值。由于文本框的上下文菜单实际上永远不可能为 null,因此它的 ContextMenuOpening 总是触发,因此支持此行为的逻辑起作用。
就像我说的...真的很痛苦才算出这一切,但是我真的有一种非常酷的成就感。
我希望这能帮助其他遇到同样问题的人。随时在此处回复或私信我提问。
马克

3
很抱歉,您正在寻找一个简单的解决方案来解决一个复杂的问题。简单地说,问题是要拥有智能自动提交用户界面控件,需要最少的交互,并且在您“切换”它们时“做正确的事情”。
这个问题之所以复杂,是因为“做正确的事情”取决于应用程序的上下文。WPF采取的方法是给你逻辑焦点和键盘焦点的概念,并让你决定在你的情况下如何做正确的事情。
如果上下文菜单被打开怎么办?如果应用程序菜单被打开会发生什么?如果焦点切换到另一个应用程序会发生什么?如果打开属于本地控件的弹出窗口会发生什么?如果用户按Enter键关闭对话框会发生什么?所有这些情况都可以处理,但如果您有提交按钮或用户必须按Enter键才能提交,所有这些情况都将消失。
所以你有三个选择:
1. 当控件具有逻辑焦点时保持其处于编辑状态 2. 添加明确的提交或应用机制 3. 处理所有混乱的情况,当您尝试支持自动提交时会出现这些情况。

1
我认为这种行为并不复杂。它只是简单的“失去焦点时退出编辑模式”,但要注意,显示上下文菜单并不构成失去焦点。Windows资源管理器一直都是这样做的。重命名文件时,你得到的就是这种精确的行为...不需要“提交/取消”按钮。在WinForms中也很容易,因为上下文菜单不会窃取焦点。在我看来,我不同意(尽管我理解为什么)微软让上下文菜单窃取键盘焦点。他们应该考虑到这一点。现在你必须绕七个弯才能走直路。 - Mark A. Donohoe
@MarqueIV:实际上相当复杂,即使DataGrid也存在一些问题。如果您在TabControl的DataTemplate中有DataGrids并且在编辑时切换选项卡,则会出现异常,因为某些编辑逻辑出错了。 - H.B.
@MarquelIV:但这不是Windows Explorer,也不是WinForms。我并不是在为WPF的整个焦点设计和基础设施辩护;它就是它所是的。鉴于它是什么,你有三个选择。 - Rick Sladkey
1
@MarqueIV:我很抱歉暗示你在寻找简单的解决方案。我应该说“我们都希望有一个更简单的解决方案”。 - Rick Sladkey
1
@MarqueIV:Josh Smith 是 WPF 社区的偶像之一。他回答了你说没有真正回答的问题。如果你有机会回答你的问题,那就是它了。顺便说一句,我知道你因为这个问题受到了一些抨击,所以我很高兴你超越了争论并专注于解决问题。 - Rick Sladkey
显示剩余8条评论

1
"不是这样更容易吗:"

    void txtBox_LostKeyboardFocus(object sender, KeyboardFocusChangedEventArgs e)
    {
        TextBox txtBox = (sender as TextBox);

        if (e.NewFocus is ContextMenu && (e.NewFocus as ContextMenu).PlacementTarget == txtBox)
        {
            return;
        }

        // Rest of code for existing edit mode here...
    }

很遗憾,因为如果您打开上下文菜单,您会失去键盘焦点,然后您将返回。 如果您随后在应用程序中的其他位置或另一个应用程序中单击其他地方,则不会有任何其他通知退出编辑模式。 但是,您的代码可能简化了我上面介绍的上下文菜单黑客,其中我重新设计了控件,尽管放置目标则成为一个因素,因为它可能设置在其他地方。 仍然是一个很好的思路。 谢谢! - Mark A. Donohoe

0

我在寻找类似问题的解决方案时经过了这里:我有一个 ListBox,当 ContextMenu 打开时会失去焦点,而我不希望发生这种情况。

我的简单解决方案是将 Focusable 设置为 False,对于 ContextMenu 及其 MenuItem 均如此:

<ContextMenu x:Key="QueryResultsMenu" Focusable="False">
    <ContextMenu.Resources>
        <Style TargetType="MenuItem">
            <Setter Property="Focusable" Value="False"/>
        </Style>
    </ContextMenu.Resources>
    <MenuItem ... />
</ContextMenu>

希望这能帮助未来的寻求者...

1
这难道不会破坏上下文菜单的键盘导航吗?请记住,它们可以通过键盘启动。 - Mark A. Donohoe
你说得对,在我的情况下并不重要,因为没有键盘。 - TheDark
你能否将上下文菜单设置为空集合而不是null?此外,我认为有一个事件可以拦截和处理,从而阻止它首先打开。 - Mark A. Donohoe

0

这种方法的问题在于它没有考虑到仅键盘导航。不过,对于其他人来说是很有用的信息。谢谢! - Mark A. Donohoe
我即将尝试为我的控件添加选项卡导航,所以可能很快就会感受到你的痛苦!你有没有通过键盘失去焦点而无法获得按键事件的方法?总的来说,WPF中的上下文菜单有点让人头疼——它们似乎充满了错误,所以这里出现问题也不奇怪。 - JonnyRaa
当您执行系统键(即Alt-Tab)时,您将不会收到通知。好吧,您会看到“Alt”,但在切换时您不会看到“Tab”,只会得到一半的通知。然而,上述方法(虽然有些复杂)也适用于这些情况,因为它依赖于常规的LostFocus和键盘焦点,所以如果您遵循它,即使在您的情况下,您也应该是完美的。希望对您有所帮助! - Mark A. Donohoe

0

不确定,但这可能会有所帮助。我曾经遇到过可编辑组合框的类似问题。我的问题是我使用了OnLostFocus重写方法,但它没有被调用。解决方法是我将回调附加到了LostFocus事件上,然后一切都正常了。


你能否详细阐述一下,最好附上代码示例来更好地解释你的意思以及它解决了什么问题? - Mark A. Donohoe

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