将网格的垂直滚动位置与RichEditBox或TextBox匹配

7
我有一个Windows Store应用程序,其中包含一个RichEditBox(编辑器)和一个Grid(MarginNotes)。
我需要两个元素的垂直滚动位置始终匹配。这样做的目的是允许用户在文档的边距中添加注释。
我已经根据光标位置确定了注释位置 - 当添加注释时,会对光标之前的所有内容进行文本选择。然后将此选择添加到第二个不可见的RichEditBox中,该控件位于StackPanel内。然后,我获取此控件的ActualHeight,以获得注释在网格中的位置。
我的问题是,当我上下滚动RichEditBox时,Grid不会相应地滚动。 第一种技术 我尝试将它们都放在ScrollViewer中,并在RichEditBox上禁用滚动。
<ScrollViewer x:Name="EditorScroller" 
    VerticalAlignment="Stretch" HorizontalAlignment="Stretch">
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="150" />
            <ColumnDefinition Width="{Binding *" />
            <ColumnDefinition Width="150" />
        </Grid.ColumnDefinitions>
        <Grid x:Name="MarginNotes" Grid.Column="0" HorizontalAlignment="Right"                  
            Height="{Binding ActualHeight, ElementName=editor}">
        </Grid>
        <StackPanel Grid.Column="1">
            <RichEditBox x:Name="margin_helper" Opacity="0" Height="Auto"></RichEditBox>
        </StackPanel>
        <RichEditBox x:Name="editor" Grid.Column="1" Height="Auto"
            ScrollViewer.VerticalScrollBarVisibility="Hidden" />
    </Grid>
</ScrollViewer>

当我滚动到RichEditBox控件的底部并按几次回车键时,光标消失了。 ScrollViewer不会自动随着光标滚动。
我尝试添加C#代码来检查光标位置,将其与编辑器的VerticalOffset和高度进行比较,然后相应地调整滚动。这个方法有效,但速度非常慢。最初我将它放在KeyUp事件上,当我输入句子时,应用程序就会停止响应。之后我将其放在5秒计时器上,但这仍然会减慢应用程序的性能,并且意味着光标消失并且RichEditBox滚动之间可能会有5秒的延迟。
第二种技术
我还尝试将MarginNotes放在自己的ScrollViewer中,并根据我的RichEditBoxViewChanged事件编程设置VerticalOffset
<Grid>
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="150" />
        <ColumnDefinition Width="{Binding *" />
        <ColumnDefinition Width="150" />
    </Grid.ColumnDefinitions>
    <ScrollViewer x:Name="MarginScroller" Grid.Column="0" 
         VerticalAlignment="Stretch" HorizontalAlignment="Stretch">
        <Grid x:Name="MarginNotes" HorizontalAlignment="Right"                  
            Height="{Binding ActualHeight, ElementName=editor}">
        </Grid>
    </ScrollViewer>
    <StackPanel Grid.Column="1">
        <RichEditBox x:Name="margin_helper" Opacity="0" Height="Auto"></RichEditBox>
    </StackPanel>
    <RichEditBox x:Name="editor" Grid.Column="1" Height="Auto" 
        Loaded="editor_loaded" SizeChanged="editor_SizeChanged" />
</Grid>

相关事件处理程序
void editor_Loaded(object sender, RoutedEventArgs e)
{
    // setting this in the OnNavigatedTo causes a crash, has to be set here. 
    // this uses WinRTXAMLToolkit as suggested by Nate Diamond to find the 
    // ScrollViewer and add the event handler
    editor.GetFirstDescendantOfType<ScrollViewer>().ViewChanged += editor_ViewChanged;
}

private void editor_ViewChanged(object sender, ScrollViewerViewChangedEventArgs e)
{
    // when the RichEditBox scrolls, scroll the MarginScroller the same amount
    double editor_vertical_offset = ((ScrollViewer)sender).VerticalOffset;
    MarginScroller.ChangeView(0, editor_vertical_offset, 1);       
}

private void editor_SizeChanged(object sender, SizeChangedEventArgs e)
{
    // when the RichEditBox size changes, change the size of MarginNotes to match
    string text = "";
    editor.Document.GetText(TextGetOptions.None, out text);
    margin_helper.Document.SetText(TextSetOptions.None, text);
    MarginNotes.Height = margin_helper.ActualHeight;
}

这个方案可以运行,但由于滚动的应用直到 ViewChanged 事件被触发后才能生效,所以它的执行速度相当缓慢。我尝试使用 ViewChanging 事件,但出于某种原因它根本不会触发。此外,在快速滚动后,Grid 有时会位置不正确。


我会尝试的第一件事是确保在 RichEditBox 上启用了 VerticalScrollChaining。您还可以尝试禁用 RichEditBox 上的整个滚动并查看是否可以解决问题。您可能需要在 SizeChanged 或类似事件上更新偏移量。 - Nate Diamond
如果你还没有使用 "ScrollViewer.VerticalScrollMode =“ Disabled”",我建议你完全禁用 RichEditBox 上的滚动。 - Nate Diamond
我有一个基于代码的解决方案,可以计算光标高度。 基本上,它选择文本一直到光标,将其放入第二个RichEditBox中并测量其高度。 如果光标高度大于VerticalOffset + ActualHeight,则以编程方式滚动到光标高度。不幸的是,这种运行速度非常慢,无法胜任工作。 - roryok
这可能听起来很奇怪,需要一些研究才能完成,但我的建议是通过解析RichEditBox的VisualTree找到Carat控件(闪烁的光标),然后通过在它和父级ScrollViewer之间进行转换,找到Carat的位置。此时,我会查看Carat的当前位置是否大于ScrollViewer的高度,如果是的话,我会将VerticalOffset增加从Carat底部(加上一些缓冲区)到ScrollViewer底部的差值。 - Nate Diamond
嗨,Nate。我不确定Caret是否是一个实际的UI元素,所以我认为它不能以这种方式被选择。 - roryok
显示剩余4条评论
1个回答

1
因为文本的大小或放置在不同类型的TextBox中,使得同步滚动条并不能保证同步文本,这就使得问题变得困难。话虽如此,以下是如何解决它的方法。
void MainPage_Loaded(object sender, RoutedEventArgs args)
{
    MyRichEditBox.Document.SetText(Windows.UI.Text.TextSetOptions.None, MyTextBox.Text);
    var textboxScroll = Children(MyTextBox).First(x => x is ScrollViewer) as ScrollViewer;
    textboxScroll.ViewChanged += (s, e) => Sync(MyTextBox, MyRichEditBox);
}

public void Sync(TextBox textbox, RichEditBox richbox)
{
    var textboxScroll = Children(textbox).First(x => x is ScrollViewer) as ScrollViewer;
    var richboxScroll = Children(richbox).First(x => x is ScrollViewer) as ScrollViewer;
    richboxScroll.ChangeView(null, textboxScroll.VerticalOffset, null);
}

public static IEnumerable<FrameworkElement> Children(FrameworkElement element)
{
    Func<DependencyObject, List<FrameworkElement>> recurseChildren = null;
    recurseChildren = (parent) =>
    {
        var list = new List<FrameworkElement>();
        for (int i = 0; i < VisualTreeHelper.GetChildrenCount(parent); i++)
        {
            var child = VisualTreeHelper.GetChild(parent, i);
            if (child is FrameworkElement)
                list.Add(child as FrameworkElement);
            list.AddRange(recurseChildren(child));
        }
        return list;
    };
    var children = recurseChildren(element);
    return children;
}

决定何时调用同步是棘手的。可能在PointerReleased、PointerExit、LostFocus、KeyUp等事件中进行滚动是真正的问题所在。你可能需要处理所有这些情况。但是,事实就是这样。至少你可以尝试。
祝你好运。

嗨,Gerry。感谢您的回复。正如我在原帖中所指出的那样,ViewChanged 方法非常卡顿,只有在滚动停止时才应用滚动。使用 ViewChanging 也没有帮助。 - roryok
你是错误的。ViewChanged不仅在滚动停止时才被触发。查找ScrollViewerViewChangedEventArgs.IsIntermediate属性以了解更多信息。简而言之,没有更好的方法可以附加到其他方法,因为它们重叠导致冗余调用,这些调用可能成为它们之间竞争条件的受害者。 - Jerry Nixon
无论如何,对我来说仍然太卡了。不过还是谢谢你。另外很抱歉拼错了你的名字! - roryok

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