你如何解决这个LostFocus / LostKeyboardFocus问题?

时间:2011-05-01 19:31:26

标签: wpf lost-focus focusmanager lostfocus keyboardfocusmanager

好吧,我有一个具有IsEditing属性的控件,为了参数的缘故,它有一个默认模板,通常是一个文本块,但是当IsEditing为true时,它会在文本框中交换以进行就地编辑。现在当控件失去焦点时,如果它仍在编辑,它应该退出编辑模式并在TextBlock模板中交换回来。非常直接,对吗?

想想在Windows资源管理器或桌面上重命名文件的行为(这与我所知道的相同......)这就是我们想要的行为。

问题是你不能使用LostFocus事件,因为当你切换到另一个窗口(或者是一个FocusManager的元素)时,由于控件仍然具有逻辑焦点,LostFocus不会触发,因此无法工作。 / p>

如果您使用LostKeyboardFocus,虽然这确实解决了“其他FocusManager”问题,但现在您有了一个新问题:当您进行编辑并右键单击文本框以显示上下文菜单时,因为上下文菜单现在有键盘焦点,你的控制失去键盘焦点,退出编辑模式并关闭上下文菜单,让用户感到困惑!

现在我已经尝试设置一个标志,在菜单打开之前忽略LostKeyboardFocus,然后在LostKeyboardFocus事件中使用该fiag来确定是否退出编辑模式,但是如果菜单是打开的,我点击在应用程序的其他地方,因为控件本身不再具有键盘焦点(菜单具有它)控件永远不会获得另一个LostKeyboardFocus事件,因此它仍然处于编辑模式。 (我可能必须在菜单关闭时添加一个检查以查看焦点是什么,然后手动将其从EditMode中踢出来,如果它不是控件。这看起来很有希望。)

所以......任何人都知道如何成功编码这种行为?

标记

6 个答案:

答案 0 :(得分:9)

好的......这在程序员乐趣中很“有趣”。凯斯特想弄清楚真正的痛苦,但我脸上露出了一个漂亮的笑容。 (是时候为我的肩膀准备一些IcyHot,因为我自己也很努力地拍拍它!:P)

无论如何,这是一个多步骤的事情,但是一旦你弄明白了,它就会非常简单。简短版本是您需要使用两个 LostFocus LostKeyboardFocus,而不是其中一个。

LostFocus很简单。每当您收到该事件时,请将IsEditing设置为false。做完了。

上下文菜单和丢失的键盘焦点

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

要处理此行为,请覆盖ContextMenuOpening(或处理事件)并设置一个类级别标志,指示菜单正在打开。 (我使用bool _ContextMenuIsOpening。)然后在LostKeyboardFocus覆盖(或事件)中,检查该标志,如果已设置,则只需清除它,不执行任何其他操作。但是,如果没有设置,这意味着除了上下文菜单打开之外的某些内容导致控件失去键盘焦点,因此在这种情况下,您确实要将IsEditing设置为false。

已经开放的上下文菜单

现在有一种奇怪的行为,如果控件的上下文菜单打开,因此控件已经失去了键盘焦点,如上所述,如果你点击应用程序中的其他地方,在新控件获得焦点之前,你的控件就会得到键盘焦点首先,但只有一瞬间,然后它立即产生它到新的控件。

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

现在让焦点简单地转移到你点击的控件而不先将焦点设置回拥有上下文菜单的控件,然后我们必须做一些事情,比如挂钩ContextMenuClosing事件并检查一下控件将会成为下一个焦点,然后我们只会将IsEditing设置为false,如果即将关注的控件不是产生上下文菜单的那个,那么我们基本上就避开了一个子弹。 / p>

警告:默认上下文菜单

现在还有一点需要注意的是,如果您使用类似文本框的内容并且未在其上明确设置自己的上下文菜单,则获取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表示'使用默认值')

现在,当ContextMenu为false时,用户可以使用常规IsEditing属性...当IsEditing为true时,他们可以使用IsEditingContextMenu,如果他们没有指定IsEditingContextMenu,我们定义的内部默认值用于文本框。由于文本框的上下文菜单实际上永远不会为空,因此它的ContextMenuOpening始终会触发,因此支持此行为的逻辑可以正常工作。

就像我说的那样......真正的痛苦可以把这一切搞清楚,但该死的如果我在这里没有一种非常酷的成就感。

我希望这可以帮助其他人解决同样的问题。欢迎在这里回复或PM问我。

标记

答案 1 :(得分:3)

不幸的是,您正在寻找解决复杂问题的简单方法。简单的问题就是拥有智能的自动提交用户界面控件,这些控件需要最少的交互,并且当你“切换”它们时“做正确的事”。

它复杂的原因是因为正确的事情取决于应用程序的上下文。 WPF采用的方法是为您提供逻辑焦点和键盘焦点概念,并让您决定如何在您的情况下为您做正确的事情。

如果打开上下文菜单怎么办?如果打开应用程序菜单会发生什么?如果重点转移到另一个应用程序怎么办?如果打开属于本地控件的弹出窗口怎么办?如果用户按Enter键关闭对话框怎么办?所有这些情况都可以处理,但是如果你有一个提交按钮或用户必须按Enter键提交它们就会消失。

所以你有三个选择:

  • 当控件具有逻辑焦点时,让控件保持编辑状态
  • 添加显式提交或应用机制
  • 处理当您尝试支持自动提交时出现的所有混乱案例

答案 2 :(得分:0)

我不确定上下文菜单问题,但我尝试做类似的事情,并发现使用鼠标捕获为您提供(几乎)您所追求的行为:

请在此处查看答案:How can a control handle a Mouse click outside of that control?

答案 3 :(得分:0)

不确定,但这可能会有所帮助。我在Editable组合框中遇到了类似的问题。我的问题是我使用的OnLostFocus覆盖方法没有被调用。修复是我附加了一个回调LostFocus事件,它工作得很好。

答案 4 :(得分:0)

我在搜索类似问题的解决方案时通过了这里:我ListBoxContextMenu打开时失去了焦点,我不希望这种情况发生。

我的简单解决方案是将Focusable设置为FalseContextMenu及其MenuItem

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

希望这有助于未来的寻求者......

答案 5 :(得分:0)

这不会更容易:

    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...
    }