WPF ScrollViewer scroll to element of FlowDocument
I am working on a chat application. There is a scrollviewer containing a richtextbox which contains a flowdocument which contains paragraphs in its blocks. Now when a new message is added then the scrollviewer scrolls down its content properly (automatic scroll when the scrollbar is at the bottom). The flowdocument has always maximum 100 paragraphs in it while it is scrolled down to the bottom. (Old paragraphs at the top will be removed when new messages are arrived.)
What I would like to add is when I scroll up the scrollviewer to its top then I would like to load older messages (kind of Facebook style). When I scroll to the top old messages are loaded properly, but I would like to achive that the topmost paragraph before loading old messages will still be displayed on the top. For this I think I would need to calculate the new position of that paragraph and set the scrollviewer's vertical offset to the Y coordinate of that paragraph. (After the new messages are loaded the scrollviewer stays still scrolled up to its top.)
But how could I detect where is that paragraph after inserting old messages?
Unfortunately BringIntoView() doesn't work for this. I think the reason may be that the Paragraph is in the FlowDocument and not in the ScrollViewer directly. And I don't know if you can tell (or it works like this..) BringIntoView to scroll the ScrollViewer to the position where that paragraph is at the top.
The other linked solution is not good, because a Paragraph object can not be converted to a FrameworkElement and only that has a TranslatePoint method.
What I managed to do is: since I know how many rows were inserted into the FlowDocument, I sum the size of the inserted paragraphs so I get the point of the paragraph where I need to scroll the ScrollViewer. Then I call sw.ScrollToVerticalOffset with that parameter and it scrolls there fine. :)
Here is the full code, I hope it may help for some people. This function is subscribed to the ScrollChanged event of my ScrollViewer.
However note that this solution will work correctly only paragraphs that are displayed in one row!
bool StopLoading = false; // This is required to ensure that only the specified amount of messages will be loaded at once.
// If we scroll upper the messages, then don't scroll. If we scroll to bottom, scroll the messages
private void MessageScrollChanged(object sender, ScrollChangedEventArgs e)
{
var obj = sender as ScrollViewer;
// The scrollbar is at the bottom
if (obj.VerticalOffset == obj.ScrollableHeight)
{
// The while loop removes loaded messages when we have scrolled to the bottom
Channel ch = (Channel)((ScrollViewer)sender).Tag;
while (ch.TheFlowDocument.Blocks.Count > GlobalManager.MaxMessagesDisplayed)
{
ch.TheFlowDocument.Blocks.Remove(ch.TheFlowDocument.Blocks.FirstBlock);
if (ch.MessagesLoadedFrom + 1 + GlobalManager.MaxMessagesDisplayed < GlobalManager.MaxMessagesInMemory)
ch.MessagesLoadedFrom++;
}
// The automatic scroll when the scrollbar is at the bottom
obj.ScrollToEnd();
}
// The scrollbar is at the top
else if (obj.VerticalOffset == 0) // Load older messages
{
if (!StopLoading)
{
Channel ch = (Channel)((ScrollViewer)sender).Tag;
if (ch.MessagesLoadedFrom != 0)
{
int loaded = LoadMessages(ch, GlobalManager.NumOfOldMessagesToBeLoaded);
double plus = first.Padding.Top + first.Padding.Bottom + first.Margin.Bottom + first.Margin.Top; // Since all of my paragraphs have the same Margin and Padding I can do this
double sum = 0;
Block temp = ch.TheFlowDocument.Blocks.FirstBlock;
for (int i = 0; i < loaded; i++)
{
sum += temp.FontSize + plus;
temp = temp.NextBlock;
if (temp == null)
break;
}
obj.ScrollToVerticalOffset(sum);
}
StopLoading = true;
}
}
// The scrollbar is not at the top and not at the bottom. We can enable loading older messages
else
{
StopLoading = false;
}
e.Handled = true;
}
链接地址: http://www.djcxy.com/p/61130.html