好的......这很有趣,就像程序员的乐趣一样。要找出其中的奥秘其实挺费劲的,但是我做到了,并因此脸上洋溢着灿烂的微笑。(现在得花点时间给肩膀涂些IcyHot,因为我拍自己的肩膀拍得太用力了! :P )
总之,这是一个多步骤的过程,但一旦你搞清楚了一切,它就会令人惊讶地简单。简短来说,你需要同时使用 both LostFocus
和 and 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="" />
<Setter Property="ContextMenu" Value="" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="">
<Border Background="White" BorderBrush="LightGray" BorderThickness="1" CornerRadius="1">
<ScrollViewer x:Name="PART_ContentHost" />
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
<Style.Triggers>
<DataTrigger Binding="}" Value="">
<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 总是触发,因此支持此行为的逻辑起作用。
就像我说的...真的很痛苦才算出这一切,但是我真的有一种非常酷的成就感。
我希望这能帮助其他遇到同样问题的人。随时在此处回复或私信我提问。
马克