FocusScope学习二: 很好的理解FocusScope的工作原理

http://www.codeproject.com/Articles/38507/Using-the-WPF-FocusScope

Introduction

Often, it is useful to maintain a separate focus for different parts of the user interface. For example, when you have a tab control with different data entry fields on each page, it makes sense to remember the focus for each individual page. The default WPF behavior is to reset the focus to the first child element whenever the active page changes - highly annoying if you switch tab pages using the Ctrl+Tab shortcut to look at something you entered on another page, and then when you Ctrl+Tab back to the tab page you were editing, you always have to fetch the mouse or press Tab N times to get back to the text box you were editing.
The solution to this problem is to remember the logical focus inside each tab page, and restore keyboard focus to the appropriate control when the page is activated again.

注意,文章的作者并没有给出以上问题的Solution,只不过把TabControl的例子作为一个研究的开头而已,并通过以下的例子来说明FocusScope的底层机制,并实现了自己的Enhanced Focus Scope Attached Dependency Property.


KeyboardNavigationMode.Once - A Solution?

You just set KeyboardNavigation.ControlTabNavigation="Once" on the GroupBox, and it suddenly starts working - as long as you only navigate using the keyboard. But in my usecase, it is a requirement to also restore the previous focus when the GroupBox is clicked with the mouse. Unfortunately, WPF doesn't provide any API for the magic behind KeyboardNavigationMode.Once; it seems to be impossible to programmatically restore focus without using Reflection to access WPF's internals (I hope someone proves me wrong on this).
If you feel adventurous, call KeyboardNavigation.GetActiveElement using Reflection and skip the rest of this article (the example code does this for the leftmost group box).

Focus Scope - A Solution?

Wait - separate logical focus from keyboard focus? WPF already does that!
It seems that we could simply set FocusManager.IsFocusScope="True", and WPF would do the hard work for us. Unfortunately, this has some horrible side effects.

The MSDN thread "A FocusScope Nightmare (Bug?)" captures my initial reaction quite well. (这篇讨论很经典,我会在后面的博文里面转载关键内容)

  • Why does this seemingly innocent change totally cripple WPF routed commands?
  • Did I run into a WPF bug?
  • How do I get out of this nightmare?

This article explains why the focus scopes in WPF work like they do; and it presents a simple solution that makes them work like we want.

What are the Problems with the WPF FocusScope?

  • Routed Commands do not work inside focus scopes.(是指挂接在容器外的事件处理函数,没法处理那些在FocusScope内的CommandSource的命令)
  • It causes other controls to think they still have focus. In the screenshot at the beginning of the article, two text boxes display a caret.
  • Several controls like buttons and checkboxes will move focus somewhere else when pressed.

For What was FocusScope Designed?

Microsoft uses FocusScope in WPF to create a temporary secondary focus. Every ToolBar and Menu in WPF has its own focus scope.

With this knowledge, we can clearly see why we have those problems:

A toolbar button should not execute commands on itself, but on whatever had focus before the toolbar was clicked. To accomplish this, routed commands ignore the focus from focus scopes and use the 'main' logical focus instead. (说的很对,会忽略在Scope内具有焦点Element,而把事件路由的起点设置在具有Main Focus的元素上;这个很好理解,比如Toolbar上的按钮是事件源,TextBox是Main Focus的元素,那么当你点击Toolbar上的按钮是,焦点会被按钮夺走,那么命令还怎么执行呢?所以WPF选择记住被夺走前的焦点元素,并将它作为事件的对象Command Target, 我觉得这也正是FocusScope被引入的原因,而WinFORM和Win32没这个问题,是因为他们根本没有路由这个概念,而且命令的实现永远是你来处理的)
This explains why routed commands don't work inside focus scopes.

Why does the large text box in the test application screenshot still display a caret? I don't know the answer to this - but why shouldn't it? Granted, the text box doesn't have the keyboard focus (the small text box in the WPF focus scope has that); but it still has the main logical focus in the active Window and is the receiver of all routed commands.

Why does the keyboard focus move to the large text box when you tab to the CheckBox in the WPF focus scope and press Space to toggle it?

Well, this is exactly what you expect when you click a menu item or a toolbar: the keyboard focus should return to the main focus. All ButtonBase-derived controls will do this. (我觉的,这个是WPF设计者的无奈,这样设计真的有点over,但是对于Toolbar和MeuItem又不得不这样做,在我看来,在这个方面WPF设计的并不好!)

How Does the WPF FocusScope Work Under the Covers?

If you don't know focus scopes yet: you can turn any control into a focus scope by setting the attached property FocusManager.IsFocusScope to true. The default styles of ToolBar and Menu do this; there isn't any magic involved with those controls.

Each focus scope stores the logical focus in the attached property FocusManager.FocusedElement. When a control receives keyboard focus, WPF will look up its parent focus scope (the nearest parent with IsFocusScope turned on) and assign it to the FocusedElement property, giving the control logical focus within that scope.
(注意,只会设置一个!每个Scope都会有自己的Logic Focus Element,而且互补干扰,一个Scope的焦点元素不会是其他Scope或者是子Scope的元素)

A WPF Window itself is a focus scope, so the main logical focus simply is the FocusedElement property on the Window instance. Routed Commands simply execute on the main focus. (Window 的Focus Elememt就是Main Focus Element,注意可能是null哦,因为Window下面所有的Container都是一个独立的Scope就有可能的!这个并不奇怪)

The Solution (确保Main Focus设置成和子Scope的焦点元素相同。。这个其实违背了上面的Rule,但是就是可以Work,哈哈)

Actually, now that we know what is happening, there is only a single problem to fix: we need to ensure that the main logical focus gets set.

WPF only gives a control the logical focus within the nearest parent focus scope. We will simply give it the logical focus within all parent focus scopes.

But we don't want to break ToolBars and Menus. Instead, we will implement the new focus logic as an Attached Behavior. This allows using our improved focus scope as easily as the existing: t:EnhancedFocusScope.IsEnhancedFocusScope="True".

If we encounter a focus scope that is not of our 'enhanced' kind, we will stop, just like WPF does.

static void OnGotKeyboardFocus(object sender, KeyboardFocusChangedEventArgs e)
{
    IInputElement focusedElement = e.NewFocus;
    for (DependencyObject d = focusedElement as DependencyObject; 
		d != null; d = VisualTreeHelper.GetParent(d)) {
        if (FocusManager.GetIsFocusScope(d)) {
            d.SetValue(FocusManager.FocusedElementProperty, focusedElement);
            if (!(bool)d.GetValue(IsEnhancedFocusScopeProperty)) {
                break;
            }
        }
    }
}

Basically, this converts the focus scope from being 'temporary' (like menus/toolbars) to a permanent focus scope.

Now, all that's left is restoring the focus, e.g. when an empty area on the GroupBox is clicked. This can be easily done using:

IInputElement storedFocus = FocusManager.GetFocusedElement(groupBox);
if (storedFocus != null)
    Keyboard.Focus(storedFocus);

Conclusion

I hope focus scopes are less of a mystery to you after reading this article.

We didn't have to do a lot to get them working, but we still ended up both manually saving and reading the focus.
This raises the question whether we should use the WPF focus scopes at all - we could have simply invented our own kind instead.


原文地址:https://www.cnblogs.com/puncha/p/3877004.html