WPF文本框移动光标和更改焦点

3

这是一个奇怪的问题,我甚至不知道该搜索什么,但请相信我已经尝试过了。

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

这里的目的是给文本框焦点,将光标移动到文本框的末尾,然后将焦点返回到实际上具有焦点的元素(通常是一个按钮)。问题在于,在我将焦点返回到最初具有焦点的元素之前,似乎文本框没有被“重新绘制”(缺乏更好的词?),因此屏幕上的光标位置不会更新(尽管所有属性都认为已经更新了)。

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

有没有正确的方法来解决这个问题?我无法想出来。

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();
    }
}

编辑

我有一个新想法,就是在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()));

我猜这是一个WPF应用程序,所以我已经更改了标签。通常您应该告诉我们您正在编写的应用程序类型,这样我们就不必猜错了。 - John Saunders
你尝试过使用 oldFocus.Invalidate() 强制重绘吗? - Jason Tyler
1
你是不是指“插入符”? - e_ne
@Ramin 我正在尝试将文本框中的文本右对齐,该文本框由用户或按下按钮填充。文本框的内容是文件位置。我希望用户看到文件位置的结尾。我发现唯一可行的方法是将插入符号移动到文本框的末尾。 - Skinner927
@Skinner927 如果你使用一个ScrollViewer并设置ScrollBar的位置,会怎么样呢?你可以使用HorizontalOffset方法。不需要将HorizontalScrollBarVisibility设置为Visible。 - Ramin
显示剩余3条评论
3个回答

2
你走在正确的道路上,问题在于为了让你的.Focus()调用生效,你需要将其延迟到稍后在Dispatcher中执行。
不要使用DispatcherPriority值为Send(最高优先级),尝试使用Dispatcher在稍后的DispatcherPriority(例如Input)设置焦点。
Dispatcher.BeginInvoke(DispatcherPriority.Input,
new Action(delegate() { 
    oldFocus.Focus();         // Set Logical Focus
    Keyboard.Focus(oldFocus); // Set Keyboard Focus
 }));

正如您所看到的,我也正在设置键盘焦点。
WPF 可以有多个焦点范围,而且可以有多个元素具有逻辑焦点(IsFocused = true)。但是,只有 一个 元素可以拥有键盘焦点并接收键盘输入。


感谢您的回复。虽然我相信我已经在我的编辑中这样做了。我调用最高级别委托(Send)上的选择和文本框的焦点,然后调用最低级别委托(Idle)来返回焦点。但结果并没有更好。 - Skinner927

0
经过多天的努力,我终于成功了。它需要 Dispatcher 检查文本框是否具有焦点和键盘焦点,以及大量的循环。
以下是参考代码。其中有一些注释,但如果有人在访问此页面时正在寻找答案,您将不得不自己阅读。提醒一下,这是在文本更改事件中发生的事情。
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();
        }
    }

0

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

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

实际上,你不需要将焦点放在文本框上 - 这个hack是必需的,因为如果TextBox没有焦点,TextBox.CaretIndex、TextBox.Select()等方法都无法执行。使用其中一种Scroll方法可以在没有焦点的情况下工作。我不知道double offset应该是什么(对我来说,使用1000.0的过度值有效)。该值的行为类似于像素,因此请确保它足够大以适合您的情况。

接下来,您不希望在用户使用键盘输入编辑值时触发此行为。作为奖励,我将垂直和水平滚动组合在一起,多行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 的使用:

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

其中xmlns:b是对应的clr-namespace。祝编码愉快!


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接