WPF TextBox 移动光标和改变焦点

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/13995489/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-09-13 06:44:33  来源:igfitidea点击:

WPF TextBox Move Cursor and Change Focus

c#.netwpfmvvmtextbox

提问by Skinner927

This is a weird one and I don't really even know what to search for, but trust me I have.

这是一个奇怪的,我什至不知道要搜索什么,但相信我,我有。

I have a text box and bound to its OnTextChangedevent is the below method.

我有一个文本框,绑定到它的OnTextChanged事件是下面的方法。

The purpose here is to give the text box focus, move the cursor to the end of the TextBox and return focus back to whatever was actually focused (usually a button). The problem is that it seems the TextBox is not "redrawn" (for lack of a better word?) before I send the focus back to the originally focused element so the cursor position does not update on screen (though all properties think it has).

此处的目的是为文本框提供焦点,将光标移动到 TextBox 的末尾并将焦点返回到实际聚焦的任何内容(通常是按钮)。问题是,在我将焦点发送回最初聚焦的元素之前,似乎 TextBox 没有“重绘”(因为缺少更好的词?)所以光标位置不会在屏幕上更新(尽管所有属性都认为它有) .

Currently, I have brutally hacked this together that basically delays the refocus of the previous focused item by 10 ms and runs it in a different thread so the UI has time to update. Now, this is obviously an arbitrary amount of time and works fine on my machine but someone running this app on an older machine may have problems.

目前,我粗暴地将其混为一谈,基本上将先前聚焦的项目的重新聚焦延迟了 10 毫秒,并在不同的线程中运行它,以便 UI 有时间更新。现在,这显然是任意时间,并且在我的机器上运行良好,但在旧机器上运行此应用程序的人可能会遇到问题。

Is there a proper way to do this? I can't figure it out.

有没有合适的方法来做到这一点?我想不通。

private void TextBoxBase_OnTextChanged(object sender, TextChangedEventArgs e)
{
    if (sender == null) return;
    var box = sender as TextBox;

    if (!box.IsFocused)
    {

        var oldFocus = FocusManager.GetFocusedElement(FocusManager.GetFocusScope(this));
        box.Select(box.Text.Length, 0);
        Keyboard.Focus(box); // or box.Focus(); both have the same results

        var thread = new Thread(new ThreadStart(delegate
                                                    {
                                                        Thread.Sleep(10);
                                                        Dispatcher.Invoke(new Action(() => oldFocus.Focus()));
                                                    }));
        thread.Start();
    }
}

EDIT

编辑

A new idea I had was to run the oldFocus.Focus() method once the UI is done updating so I tried the following but I get the same result :(

我的一个新想法是在 UI 完成更新后运行 oldFocus.Focus() 方法,因此我尝试了以下操作,但得到了相同的结果:(

var oldFocus = FocusManager.GetFocusedElement(FocusManager.GetFocusScope(this));

Dispatcher.Invoke(DispatcherPriority.Send, new Action(delegate
 {
   box.Select(box.Text.Length, 0);
   box.Focus();
 }));

Dispatcher.Invoke(DispatcherPriority.SystemIdle, new Action(() => oldFocus.Focus()));

采纳答案by Skinner927

After many days, I was finally able to get it to work. It required the Dispatcher to check if the textbox has both focus AND keyboardfocus and lots of loops.

很多天后,我终于能够让它工作了。它要求调度程序检查文本框是否同时具有焦点和键盘焦点以及大量循环。

Here's the code for reference. There's some comments in it but if anyone hits this page looking for an answer, you'll have to read through it yourself. A reminder, this is on text change.

这是代码供参考。其中有一些评论,但如果有人点击此页面寻找答案,您必须自己通读一遍。提醒一下,这是关于文本更改。

protected void TextBox_ShowEndOfLine(object sender, TextChangedEventArgs e)
    {
        if (sender == null) return;
        var box = sender as TextBox;

        if (!box.IsFocused && box.IsVisible)
        {
            IInputElement oldFocus = FocusManager.GetFocusedElement(FocusManager.GetFocusScope(this));
            box.Focus();
            box.Select(box.Text.Length, 0);
            box.Focus();

            // We wait for keyboard focus and regular focus before returning focus to the button
            var thread = new Thread((ThreadStart)delegate
                                        {
                                            // wait till focused
                                            while (true)
                                            {
                                                var focused = (bool)Dispatcher.Invoke(new Func<bool>(() => box.IsKeyboardFocusWithin && box.IsFocused && box.IsInputMethodEnabled), DispatcherPriority.Send);
                                                if (!focused)
                                                    Thread.Sleep(1);
                                                else
                                                    break;
                                            }

                                            // Focus the old element
                                            Dispatcher.Invoke(new Action(() => oldFocus.Focus()), DispatcherPriority.SystemIdle);
                                        });
            thread.Start();
        }
        else if (!box.IsVisible)
        {
            // If the textbox is not visible, the cursor will not be moved to the end. Wait till it's visible.
            var thread = new Thread((ThreadStart)delegate
                                        {
                                            while (true)
                                            {
                                                Thread.Sleep(10);
                                                if (box.IsVisible)
                                                {
                                                    Dispatcher.Invoke(new Action(delegate
                                                                                     {
                                                                                         box.Focus();
                                                                                         box.Select(box.Text.Length, 0);
                                                                                         box.Focus();

                                                                                     }), DispatcherPriority.ApplicationIdle);
                                                    return;
                                                }
                                            }
                                        });
            thread.Start();
        }
    }

回答by Blachshma

You're on the right track, the problem is that for your .Focus()call to stick, you need to delay the call to a later time inthe Dispatcher.
Instead of using the DispatcherPriorityvalue of Send(which is the highest), try using the Dispatcher to set the focus at a later DispatcherPriority, such as Input.

您走在正确的轨道上,问题是为了让您的.Focus()呼叫保持不变,您需要将呼叫延迟到调度程序中的稍后时间。
不要使用SendDispatcherPriority值(最高),而是尝试使用 Dispatcher 将焦点设置在稍后的 DispatcherPriority 上,例如Input

Dispatcher.BeginInvoke(DispatcherPriority.Input,
new Action(delegate() { 
    oldFocus.Focus();         // Set Logical Focus
    Keyboard.Focus(oldFocus); // Set Keyboard Focus
 }));

As you can see, I'm also setting the Keyboard Focus.
WPF can have multiple Focus Scopes, and more then one element can have Logical Focus (IsFocused = true). But, only oneelement can have Keyboard Focus and will receive keyboard input.

如您所见,我还设置了键盘焦点。
WPF 可以有多个焦点范围,并且多个元素可以有逻辑焦点 ( IsFocused = true)。但是,只有一个元素可以具有键盘焦点并接收键盘输入。

回答by Patrick Stalph

Finally, I found the "right" solution for this issue (full solution at the bottom):

最后,我找到了这个问题的“正确”解决方案(底部的完整解决方案):

if (!tb.IsFocused)
{
    tb.Dispatcher.BeginInvoke(new Action(() => 
        tb.ScrollToHorizontalOffset(1000.0)), DispatcherPriority.Input);
}

Actually, you don't want to focus the textbox - this hack was required because TextBox.CaretIndex, TextBox.Select() etc. won't do anything if the TextBox does NOT have the focus. Using one of the Scroll methods instead works without focusing. I don't know what exactly the double offsetshould be (using excessive value of 1000.0worked for me). The value behaves like pixels, so make sure it's large enough for your scenario.

实际上,您不想聚焦文本框 - 需要此技巧,因为如果 TextBox 没有焦点,TextBox.CaretIndex、TextBox.Select() 等将不会执行任何操作。使用 Scroll 方法之一可以在不聚焦的情况下工作。我不知道到底double offset应该是什么(使用1000.0对我来说有效的过高价值)。该值的行为类似于像素,因此请确保它对于您的场景来说足够大。

Next, you don't want to trigger this behavior when the user edits the value using keyboard input. As a bonus I combined vertical and horizontal scrolling, where a multi-line TextBox scrolls vertically, while a single line TextBox scrolls horizontally. Finally, you may want to reuse this thing as a attached property / behavior. Hope you enjoy this solution:

接下来,您不希望在用户使用键盘输入编辑值时触发此行为。作为奖励,我结合了垂直和水平滚动,其中多行 TextBox 垂直滚动,而单行 TextBox 水平滚动。最后,您可能希望将此东西作为附加属性/行为重用。希望你喜欢这个解决方案:

    /// <summary>The attached dependency property.</summary>
    public static readonly DependencyProperty AutoScrollToEndProperty =
        DependencyProperty.RegisterAttached("AutoScrollToEnd", typeof(bool), typeof(TextBoxBehavior),
            new UIPropertyMetadata(false, AutoScrollToEndPropertyChanged));

    /// <summary>Gets the value.</summary>
    /// <param name="obj">The object.</param>
    /// <returns>The value.</returns>
    public static bool GetAutoScrollToEnd(DependencyObject obj)
    {
        return (bool)obj.GetValue(AutoScrollToEndProperty);
    }

    /// <summary>Enables automatic scrolling behavior, unless the <c>TextBox</c> has focus.</summary>
    /// <param name="obj">The object.</param>
    /// <param name="value">The value.</param>
    public static void SetAutoScrollToEnd(DependencyObject obj, bool value)
    {
        obj.SetValue(AutoScrollToEndProperty, value);
    }

    private static void AutoScrollToEndPropertyChanged(DependencyObject dependencyObject,
        DependencyPropertyChangedEventArgs e)
    {
        var textBox = dependencyObject as TextBox;
        var newValue = (bool)e.NewValue;
        if (textBox == null || (bool)e.OldValue == newValue)
        {
            return;
        }
        if (newValue)
        {
            textBox.TextChanged += AutoScrollToEnd_TextChanged;
        }
        else
        {
            textBox.TextChanged -= AutoScrollToEnd_TextChanged;
        }
    }

    private static void AutoScrollToEnd_TextChanged(object sender, TextChangedEventArgs args)
    {
        var tb = (TextBox)sender;
        if (tb.IsFocused)
        {
            return;
        }
        if (tb.LineCount > 1) // scroll to bottom
        {
            tb.ScrollToEnd();
        }
        else // scroll horizontally (what about FlowDirection ??)
        {
            tb.Dispatcher.BeginInvoke(new Action(() => tb.ScrollToHorizontalOffset(1000.0)), DispatcherPriority.Input);
        }
    }

XAML usage:

XAML 用法:

        <TextBox b:TextBoxBehavior.AutoScrollToEnd="True"
                 Text="{Binding Filename}"/>

where xmlns:bis the corresponding clr-namespace. Happy coding!

xmlns:b对应的 clr-namespace在哪里。快乐编码!