如何解决 LostFocus/LostKeyboardFocus 问题?

发布于 2024-11-04 03:07:51 字数 767 浏览 0 评论 0 原文

好的,我有一个具有 IsEditing 属性的控件,为了论证,该控件有一个默认模板,该模板通常是文本块,但当 IsEditing 为 true 时,它​​会交换文本框以进行就地编辑。现在,当控件失去焦点时,如果它仍在编辑,则应该退出编辑模式并交换回 TextBlock 模板。很简单,对吧?

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

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

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

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

所以...任何人都知道我如何成功地编写此行为?

标记

Ok, I have a control that has an IsEditing property which for argument's sake has a default template that is normally a text block, but when IsEditing is true, it swaps in a textbox for in-place editing. Now when the control loses focus, if it's still editing, it's supposed to drop out of editing mode and swap back in the TextBlock template. Pretty straight forward, right?

Think of the behavior of renaming a file in Windows Explorer or on your desktop (which is the same thing I know...) That's the behavior we want.

The issue is you can't use the LostFocus event because when you switch to another window (or element that is a FocusManager) LostFocus doesn't fire since the control still has logical focus, so that won't work.

If you instead use LostKeyboardFocus, while that does solve the 'other FocusManager' issue, now you have a new one: when you're editing and you right-click on the textbox to show the context menu, because the context menu now has keyboard focus, your control loses keyboard focus, drops out of edit mode and closes the context menu, confusing the user!

Now I've tried setting a flag to ignore the LostKeyboardFocus just before the menu opens, then using that fiag in the LostKeyboardFocus event to determine to kick it out of editing mode or not, but if the menu is open and I click elsewhere in the app, since the control itself didn't have keyboard focus anymore (the menu had it) the control never gets another LostKeyboardFocus event so it remains in edit mode. (I may have to add a check when the menu closes to see what has focus then manually kick it out of EditMode if it's not the control. That seems promising.)

So... anyone have any idea how I can successfully code this behavior?

Mark

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(6

一身仙ぐ女味 2024-11-11 03:07:51

好吧...这很“有趣”,就像程序员的乐趣一样。弄清楚这个问题真的很痛苦,但我脸上挂着灿烂的笑容。 (考虑到我自己拍得很用力,是时候给我的肩膀买一些 IcyHot 了!:P)

无论如何,这是一个多步骤的事情,但一旦你弄清楚了一切,就会出人意料地简单。简而言之,您需要使用两者 LostFocus LostKeyboardFocus,而不是其中之一。

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

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

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

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

已打开的上下文菜单

现在,如果某个控件的上下文菜单已打开,并且因此该控件已经失去键盘焦点(如上所述),并且之前单击应用程序中的其他位置,则会出现奇怪的行为新控件获得焦点,您的控件首先获得键盘焦点,但只持续一瞬间,然后它立即将其交给新控件。

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

现在,如果焦点只是转移到您单击的控件上,而不首先将焦点设置回拥有上下文菜单的控件,那么我们必须执行一些操作,例如挂钩 ContextMenuClosing 事件并检查什么控件接下来将获得焦点,那么如果即将获得焦点的控件不是生成上下文菜单的控件,我们只需将 IsEditing 设置为 false,这样我们基本上就躲过了一劫那里。

警告:默认上下文菜单

现在还有一个警告,如果您使用文本框之类的东西并且没有在其上明确设置您自己的上下文菜单,那么您 得到ContextMenuOpening事件,这让我很惊讶。不过,这很容易解决,只需使用与默认上下文菜单相同的标准命令(例如剪切、复制、粘贴等)创建一个新的上下文菜单并将其分配给文本框即可。它看起来完全一样,但现在您得到了设置标志所需的事件。

但是,即使您遇到问题,就像您正在创建第三方可重用控件并且该控件的用户想要拥有自己的上下文菜单一样,您也可能会意外地将自己的优先级设置为更高,并且会覆盖他们的优先级!

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

这是文本框的内部样式(名为“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< /code> 属性,当 IsEditing 为 false 时...他们可以在 IsEditing 为 true 时使用 IsEditingContextMenu,并且如果他们没有指定 IsEditingContextMenu >,我们定义的内部默认值用于文本框。由于文本框的上下文菜单实际上永远不会为空,因此它的 ContextMenuOpening 始终会触发,因此支持此行为的逻辑有效。

就像我说的……弄清楚这一切确实很痛苦,但如果我在这里没有一种非常酷的成就感,那该死的。

我希望这可以帮助其他遇到同样问题的人。欢迎在这里回复或者私信我提问。

标记

Ok... this was "fun" as in Programmer-fun. A real pain in the keester to figure out, but with a nice huge smile on my face that I did. (Time to get some IcyHot for my shoulder considering I'm patting it myself so hard! :P )

Anyway it's a multi-step thing but is surprisingly simple once you figure out everything. The short version is you need to use both LostFocus and LostKeyboardFocus, not one or the other.

LostFocus is easy. Whenever you receive that event, set IsEditing to false. Done and done.

Context Menus and Lost Keyboard Focus

LostKeyboardFocus is a little more tricky since the context menu for your control can fire that on the control itself (i.e. when the context menu for your control opens, the control still has focus but it loses keyboard focus and thus, LostKeyboardFocus fires.)

To handle this behavior, you override ContextMenuOpening (or handle the event) and set a class-level flag indicating the menu is opening. (I use bool _ContextMenuIsOpening.) Then in the LostKeyboardFocus override (or event), you check that flag and if it's set, you simply clear it and do nothing else. If it's not set however, that means something besides the context menu opening is causing the control to lose keyboard focus, so in that case you do want to set IsEditing to false.

Already-Open Context Menus

Now there's an odd behavior that if the context menu for a control is open, and thus the control has already lost keyboard focus as described above, if you click elsewhere in the application, before the new control gets focus, your control gets keyboard focus first, but only for a split second, then it instantly yields it to the new control.

This actually works to our advantage here as this means we'll also get another LostKeyboardFocus event but this time the _ContextMenuOpening flag will be set to false, and just like described above, our LostKeyboardFocus handler will then set IsEditing to false, which is exactly what we want. I love serendipity!

Now had the focus simply shifted away to the control you clicked on without first setting the focus back to the control owning the context menu, then we'd have to do something like hooking the ContextMenuClosing event and checking what control will be getting focus next, then we'd only set IsEditing to false if the soon-to-be-focused control wasn't the one that spawned the context menu, so we basically dodged a bullet there.

Caveat: Default Context Menus

Now there's also the caveat that if you are using something like a textbox and haven't explicitly set your own context menu on it, then you don't get the ContextMenuOpening event, which surprised me. That's easily fixed however, by simply creating a new context menu with the same standard commands as the default context menu (e.g. cut, copy, paste, etc.) and assigning it to the textbox. It looks exactly the same, but now you get the event you need to set the flag.

However, even there you have an issue as if you're creating a third-party-reusable control and the user of that control wants to have their own context menu, you may accidentally set yours to a higher precedence and you'll override theirs!

The way around that was since the textbox is actually an item in the IsEditing template for my control, I simply added a new DP on the outer control called IsEditingContextMenu which I then bind to the textbox via an internal TextBox style, then I added a DataTrigger in that style that checks the value of IsEditingContextMenu on the outer control and if it's null, I set the default menu I just created above, which is stored in a resource.

Here's the internal style for the textbox (The element named 'Root' represents the outer control that the user actually inserts in their 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>

Note that you have to set the initial context menu binding in the style, not directly on the textbox or else the style's DataTrigger gets superseded by the directly-set value rendering the trigger useless and you're right back to square one if the person uses 'null' for the context menu. (If you WANT to suppress the menu, you wouldn't use 'null' anyway. You'd set it to an empty menu as null means 'Use the default')

So now the user can use the regular ContextMenu property when IsEditing is false... they can use the IsEditingContextMenu when IsEditing is true, and if they didn't specify an IsEditingContextMenu, the internal default that we defined is used for the textbox. Since the textbox's context menu can never actually be null, its ContextMenuOpening always fires, and therefore the logic to support this behavior works.

Like I said... REAL pain in the can figuring this all out, but damn if I don't have a really cool feeling of accomplishment here.

I hope this helps others here with the same issue. Feel free to reply here or PM me with questions.

Mark

酒几许 2024-11-11 03:07:51

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

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

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

所以你有三个选择:

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

Unfortunately you are looking for a simple solution to a complex problem. The problem stated simply is to have smart auto-committing user interface controls that require a minimum of interaction and "do the right thing" when you "switch away" from them.

The reason it is complex is because what the right thing is depends on the context of the application. The approach WPF takes is to give you to logical focus and keyboard focus concepts and to let you decide how to do the right thing for you in your situation.

What if the context menu is opened? What should happen if the application menu is opened? What if the focus is switched to another application? What if a popup is opened belonging to the local control? What if the user presses enter to close a dialog? All these situations can be handled but they all go away if you have a commit button or the user has to press enter to commit.

So you have three choices:

  • Let the control stay in the editing state when it has the logical focus
  • Add an explicit commit or apply mechanism
  • Handle all the messy cases that arise when you try to support auto-commit
爱本泡沫多脆弱 2024-11-11 03:07:51

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

请参阅此处的答案:控件如何处理外部的鼠标单击那个控件?

Im not sure about the context menu issue but I was trying to do something similar and found that using mouse capture gives you (just about) the behaviour you are after:

see the answer here: How can a control handle a Mouse click outside of that control?

上课铃就是安魂曲 2024-11-11 03:07:51

难道不是更容易:

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

Wouldn't it be just easier to:

    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...
    }
梦幻的心爱 2024-11-11 03:07:51

不确定,但这可能会有所帮助。我对可编辑组合框也有类似的问题。我的问题是我正在使用 OnLostFocus 重写方法,但该方法没有被调用。修复的是我已将回调附加到 LostFocus 事件,并且一切正常。

Not sure, but this could be helpful. I had a similar issue with Editable combo box. My problem was I was using OnLostFocus override method which was not getting called. Fix was I had attached a callback to LostFocus event and it worked all fine.

秋意浓 2024-11-11 03:07:51

我在这里寻找类似问题的解决方案:我有一个 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>

希望这会有所帮助未来的探索者...

I passed through here on my search for a solution for a similar problem: I have a ListBox which loses focus when the ContextMenu opens, and I don't want that to happen.

My simple solution was to set Focusable to False, both for the ContextMenu and its MenuItems:

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

Hope this helps future seekers...

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文