It is quite convenient to implement scrollbars in WPF. Just add a ScrollViewer to the periphery of the control, but the only drawback is that there is no animation effect when scrolling. Adding transition animations when scrolling can add a lot of color to our software. For example, it looks much more comfortable to support animation when scrolling in Office 2013. I have studied how to achieve this smooth scroll before, but most of the solutions online are as follows:
Find ScrollViewer via VisualTree
Add animation in ScrollChanged event
This solution is not effective. I thought that our scrolling often rolls several grids of scroll wheels in one breath. At this time, the previous animation has not ended yet, and the next animation comes, and there is a feeling of lag. Most of the algorithms on the Internet will cause offsets and misalignment.
Taking advantage of the time these two days, I studied ScorllViewer,MSDN DocumentationIt supports two scrolling methods:
Physical scrolling:
The system default scrolling scheme, the control itself does nothing, and scrolling is completely achieved by ScrollViewer. The advantage of this method is simplicity, but because of the simplicity, the control itself cannot completely sense the existence of ScorllViewer, so it cannot be controlled.
Logical scrolling:
To set the ScrollViewer's CanContentScroll to "True" to take effect, and the control needs to implement the IScrollInfo interface. At this time, ScrollViewer just passes the scroll event to the control through the IScrollInfo interface, and the control itself implements scrolling. At the same time, read the relevant attributes from the IScrollInfo interface to update the scrollbar interface.
In other words, logical scrolling is the solution we need. Since it requires the control to implement the IScrollInfo interface, it controls scrolling by itself. In other words, we need to implement our own Panel and implement the IScrollInfo interface. Regarding this interface, there are a series of articles on MSDN that describe how to implement it:
IScrollInfo in Avalon part I
IScrollInfo in Avalon part II
IScrollInfo in Avalon part III
IScrollInfo in Avalon part IV
Implementing this interface is not a hassle. I didn't read these articles carefully. I tried it after following the last example for a while and it was also done. In fact, the trouble is not to implement this interface, but to implement Panel. For simplicity, I directly inherited the WrapPanel class, and the code is as follows:
class MyWrapPanel : WrapPanel, IScrollInfo { TranslateTransform _transForm; public MyWrapPanel() { _transForm = new TranslateTransform(); = _transForm; } #region Layout Size _screenSize; Size _totalSize; protected override Size MeasureOverride(Size availableSize) { _screenSize = availableSize; if (Orientation == ) availableSize = new Size(, ); else availableSize = new Size(, ); _totalSize = (availableSize); return _totalSize; } protected override Size ArrangeOverride(Size finalSize) { var size = (finalSize); if (ScrollOwner != null) { _transForm.Y = -VerticalOffset; _transForm.X = -HorizontalOffset; (); } return _screenSize; } #endregion #region IScrollInfo public ScrollViewer ScrollOwner { get; set; } public bool CanHorizontallyScroll { get; set; } public bool CanVerticallyScroll { get; set; } public double ExtentHeight { get { return _totalSize.Height; } } public double ExtentWidth { get { return _totalSize.Width; } } public double HorizontalOffset { get; private set; } public double VerticalOffset { get; private set; } public double ViewportHeight { get { return _screenSize.Height; } } public double ViewportWidth { get { return _screenSize.Width; } } void appendOffset(double x, double y) { var offset = new Vector(HorizontalOffset + x, VerticalOffset + y); = range(, 0, _totalSize.Height - _screenSize.Height); = range(, 0, _totalSize.Width - _screenSize.Width); HorizontalOffset = ; VerticalOffset = ; InvalidateArrange(); } double range(double value, double value1, double value2) { var min = (value1, value2); var max = (value1, value2); value = (value, min); value = (value, max); return value; } const double _lineOffset = 30; const double _wheelOffset = 90; public void LineDown() { appendOffset(0, _lineOffset); } public void LineUp() { appendOffset(0, -_lineOffset); } public void LineLeft() { appendOffset(-_lineOffset, 0); } public void LineRight() { appendOffset(_lineOffset, 0); } public Rect MakeVisible(Visual visual, Rect rectangle) { throw new NotSupportedException(); } public void MouseWheelDown() { appendOffset(0, _wheelOffset); } public void MouseWheelUp() { appendOffset(0, -_wheelOffset); } public void MouseWheelLeft() { appendOffset(0, _wheelOffset); } public void MouseWheelRight() { appendOffset(_wheelOffset, 0); } public void PageDown() { appendOffset(0, _screenSize.Height); } public void PageUp() { appendOffset(0, -_screenSize.Height); } public void PageLeft() { appendOffset(-_screenSize.Width, 0); } public void PageRight() { appendOffset(_screenSize.Width, 0); } public void SetVerticalOffset(double offset) { (HorizontalOffset, offset - VerticalOffset); } public void SetHorizontalOffset(double offset) { (offset - HorizontalOffset, VerticalOffset); } #endregion }
Basically, the interaction process of the IScrollInfo interface can also be seen from the code, so I won't introduce it here.
The main interface code is as follows:
<ItemsControl ItemsSource="{Binding}" > <> <DataTemplate> <Border BorderThickness="1" BorderBrush="Black" Margin="8" Width="150" Height="50"> <Rectangle Fill="{Binding}" /> </Border> </DataTemplate> </> <> <ItemsPanelTemplate> <local:MyWrapPanel /> </ItemsPanelTemplate> </> <> <ControlTemplate> <ScrollViewer CanContentScroll="True"> <ItemsPresenter /> </ScrollViewer> </ControlTemplate> </> </ItemsControl>
It should be noted that <ScrollViewer CanContentScroll="True"> is required here, otherwise logical scrolling is not used.
The data source code is as follows:
var brushes = from property in typeof(Brushes).GetProperties() let value = (null) select value; = (100).ToArray();
Since IscrollInfo interface is used, all scrolling operations are implemented by myself. Here I implement scrolling operations by setting the X and Y offset of Panel's RenderTransFrom. After running, it looks like there is no difference between it and WrapPanel, but since it is controlled by yourself, and the animation effect is only a matter of minutes. Just change the X and Y of the RenderTransFrom in the above code to an animation switch:
protected override Size ArrangeOverride(Size finalSize) { var size = (finalSize); if (ScrollOwner != null) { var yOffsetAnimation = new DoubleAnimation() { To = -VerticalOffset, Duration = (0.3) }; _transForm.BeginAnimation(, yOffsetAnimation); var xOffsetAnimation = new DoubleAnimation() { To = -HorizontalOffset, Duration = (0.3) }; _transForm.BeginAnimation(, xOffsetAnimation); (); } return _screenSize; }
For other Panels, such as Grid, DockPanel, etc., it can basically be implemented in this way. The IScrollInfo interface can basically remain unchanged, and only the two functions of MeasureOverride and ArrangeOverride are required to be rewrite. A special control is StackPanel. Since it has implemented the IScrollInfo interface itself, that is, it has its own self-drawing scrolling solution and does not provide an interface to cover its own self-drawing scrolling, we need to write a StackPanel ourselves. Fortunately, it is not difficult to implement StackPanel. Due to limited space, I am too lazy to continue writing here. Readers can implement it by themselves. As for those non-Panel controls, the implementation is even simpler, and it is also kept for readers and friends to implement them themselves.
This is all about this article about WPF achieving smooth scrolling. I hope it will be helpful to everyone's learning and I hope everyone will support me more.