Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Matching the vertical scroll position of a Grid to a RichEditBox or TextBox

I've got a Windows Store app with a RichEditBox (editor) and a Grid (MarginNotes).

I need the vertical scroll position of the two elements to be matched at all times. The purpose of this is to allow the user to add notes in the margin of the document.

I've already figured out Note positioning based on the cursor position - when a note is added, a text selection is made of everything up to the cursor. that selection is then added to a second, invisible RichEditBox, inside a StackPanel. I then get the ActualHeight of this control which gives me the position of the note in the grid.

My issue is that when I scroll the RichEditBox up and down, the Grid does not scroll accordingly.

First Technique

I tried putting them both inside a ScrollViewer, and disabling scrolling on the 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>

When I scroll to the bottom of the RichEditBox control, and hit enter a few times, the cursor drops out of sight. The ScrollViewer doesn't scroll automatically with the cursor.

I tried adding C# code which would check the position of the cursor, compare it to the VerticalOffset and height of the editor, and then adjust the scroll accordingly. This worked, but was incredibly slow. Initially I had it on the KeyUp event which brought the app to a standstill when I typed a sentence. Afterwards I put it on a 5 second timer, but this still slowed down the app performance and also meant that there could be a 5 second delay between the cursor dropping out of sight and the RichEditBox scrolling.

Second Technique

I also tried putting just MarginNotes in its own ScrollViewer, and programmatically setting the VerticalOffset based off my RichEditBoxs ViewChanged event.

<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>

relevant event handlers

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

This worked, but was quite laggy as scrolling is not applied until the ViewChanged event fires, after scrolling has stopped. I tried using the ViewChanging event, but it does not fire at all for some reason. Additionally, the Grid was sometimes mis-positioned after a fast scroll.

like image 962
roryok Avatar asked Feb 10 '14 10:02

roryok


1 Answers

So, what makes this difficult is that the size of the text or the placement of the text in different types of TextBoxes means that syncing the scrollbar doesn't guarantee you are syncing the text. Having said that, here's how you do it.

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

Deciding when to invoke the sync is tricky. Maybe on PointerReleased, PointerExit, LostFocus, KeyUp - there are a lot of ways to scroll is the real issue there. You might need to handle all of those. But, it is what it is. At least you can.

Best of luck.

like image 121
Jerry Nixon Avatar answered Sep 27 '22 17:09

Jerry Nixon