Wpf ScrollViewer Scroll Amount - wpf

Is it possible to change the amount that the WPF ScrollViewer scrolls? I am simply wondering if it's possible to change the scrollviewer so that when using the mouse wheel or the scrollviewer arrows, the amount of incremental scrolling can be changed.

The short answer is: there is no way to do this without writing some custom scrolling code, but don't let that scare you it's not all that hard.
The ScrollViewer either works by scrolling using physical units (i.e. pixels) or by engaging with an IScrollInfo implementation to use logical units. This is controlled by the setting the CanContentScroll property where a value of false means "scroll the content using physical units" and a value of true means "scroll the content logically".
So how does the ScrollViewer scroll the content logically? By communicating with an IScrollInfo implementation. So that's how you could take over exactly how much the content of your panel scrolls when someone performs a logical action. Take a look at the documentation for IScrollInfo to get a listing of all the logical units of measurment that can be requested to scroll, but since you mentioned the mouse wheel you'll be mostly interested in the MouseWheelUp/Down/Left/Right methods.

Here's a simple, complete and working WPF ScrollViewer class that has a data-bindable SpeedFactor property for adjusting the mouse wheel sensitivity. Setting SpeedFactor to 1.0 means identical behavior to the WPF ScrollViewer. The default value for the dependency property is 2.5, which allows for very speedy wheel scrolling.
Of course, you can also create additional useful features by binding to the SpeedFactor property itself, i.e., to easily allow the user to control the multiplier.
public class WheelSpeedScrollViewer : ScrollViewer
{
public static readonly DependencyProperty SpeedFactorProperty =
DependencyProperty.Register(nameof(SpeedFactor),
typeof(Double),
typeof(WheelSpeedScrollViewer),
new PropertyMetadata(2.5));
public Double SpeedFactor
{
get { return (Double)GetValue(SpeedFactorProperty); }
set { SetValue(SpeedFactorProperty, value); }
}
protected override void OnPreviewMouseWheel(MouseWheelEventArgs e)
{
if (ScrollInfo is ScrollContentPresenter scp &&
ComputedVerticalScrollBarVisibility == Visibility.Visible)
{
scp.SetVerticalOffset(VerticalOffset - e.Delta * SpeedFactor);
e.Handled = true;
}
}
};
Complete XAML demo of 'fast mouse wheel scrolling' of around 3200 data items:
note: 'mscorlib' reference is only for accessing the demonstration data.
<UserControl x:Class="RemoveDuplicateTextLines.FastScrollDemo"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:MyApp"
xmlns:sys="clr-namespace:System;assembly=mscorlib">
<local:WheelSpeedScrollViewer VerticalScrollBarVisibility="Auto">
<ListBox ItemsSource="{Binding Source={x:Type sys:Object},Path=Assembly.DefinedTypes}" />
</local:WheelSpeedScrollViewer>
</UserControl>
Fast mouse wheel:

You could implement a behavior on the scrollviewer. In my case CanContentScroll did not work. The solution below works for scrolling with the mouse wheel as well as draging the scrollbar.
public class StepSizeBehavior : Behavior<ScrollViewer>
{
public int StepSize { get; set; }
#region Attach & Detach
protected override void OnAttached()
{
CheckHeightModulesStepSize();
AssociatedObject.ScrollChanged += AssociatedObject_ScrollChanged;
base.OnAttached();
}
protected override void OnDetaching()
{
AssociatedObject.ScrollChanged -= AssociatedObject_ScrollChanged;
base.OnDetaching();
}
#endregion
[Conditional("DEBUG")]
private void CheckHeightModulesStepSize()
{
var height = AssociatedObject.Height;
var remainder = height%StepSize;
if (remainder > 0)
{
throw new ArgumentException($"{nameof(StepSize)} should be set to a value by which the height van be divised without a remainder.");
}
}
private void AssociatedObject_ScrollChanged(object sender, ScrollChangedEventArgs e)
{
const double stepSize = 62;
var scrollViewer = (ScrollViewer)sender;
var steps = Math.Round(scrollViewer.VerticalOffset / stepSize, 0);
var scrollPosition = steps * stepSize;
if (scrollPosition >= scrollViewer.ScrollableHeight)
{
scrollViewer.ScrollToBottom();
return;
}
scrollViewer.ScrollToVerticalOffset(scrollPosition);
}
}
You would use it like this:
<ScrollViewer MaxHeight="248"
VerticalScrollBarVisibility="Auto">
<i:Interaction.Behaviors>
<behaviors:StepSizeBehavior StepSize="62" />
</i:Interaction.Behaviors>

I wanted to add to Drew Marsh accepted answer - while the other suggested answers solve it, in some cases you cannot override the PreviewMouseWheel event and handle it without causing other side effects. Namely if you have child controls that should receive priority to be scrolled before the parent ScrollViewer - like nested ListBox or ComboBox popups.
In my scenario, my parent control was a ItemsControl with its ItemsPanel being a VirtualizingStackPanel. I wanted its logical scrolling to be 1 unit per item instead of the default 3. Instead of fiddling with attached behaviors and intercepting/handling the mouse wheel events, I simply implemented a custom VirtualizingStackPanel to do this.
public class VirtualizingScrollSingleItemAtATimeStackPanel : VirtualizingStackPanel
{
public override void MouseWheelDown()
{
PageDown();
}
public override void MouseWheelUp()
{
PageUp();
}
public override void PageDown()
{
LineDown();
}
public override void PageUp()
{
LineUp();
}
}
then we use that panel like we normally would in our xaml markup:
<ItemsControl>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<controls:VirtualizingScrollSingleItemAtATimeStackPanel />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
Obviously my scenario is contrived and the solution very simple, however this might provide a path for others to have better control over the scrolling behavior without the side effects I encountered.

I did this to ensure whole numbers on scrollbar1.ValueChanged:
scrollbar1.Value = Math.Round(scrollbar1.Value, 0, MidpointRounding.AwayFromZero)

Related

Deferred loading of XAML

A project I'm working on has some rather complex XAML that is noticeably affecting visual performance. Quite a few controls are collapsed for the initial state; however, since their XAML is parsed and visual /logical trees built, it's very slow to show what amounts to an almost blank object.
It looks like (and would like confirmation here) that using a ContentControl with an initial state of Collapsed and then embedding the desired control as a DataTemplate for that ContentControl, will defer loading of the desired control in the DataTemplate until the ContentControl is made visible.
I've built a generic DeferredContentControl that listens for the LayoutUpdated event of the main UI control (in general whatever element it is that I want to appear quickly), and when the first LayoutUpdated event of that UIElement fires, I used the Dispatcher to flip the visibility of the DeferredContentControl to true, which causes the control in the DeferredContentControl's DataTemplate to instantiate. By the time the user has reacted to the initial view of the screen (which is now fast), the "slow to load" (but still collapsed) control in the data template is ready.
Does this seem like a sound approach? any pitfalls? It seems to work well in testing both for Silverlight and WPF, and while it doesn't make things any faster it gives the perception of being as much as 50% snappier in my specific scenario.
I had the same problem (in a Silverlight project), and solved it in nearly the same way. It proved to be working as expected, have not encountered any pitfalls yet.
When you need to control the point in time when xaml is parsed and view elements are instantiated you can always use DataTemplates (not necessarily in cunjuction with ContentControl). You can call DataTemplate.LoadContent() to instatiate it, you don't have to switch the visibility of a ContentControl (although internally this will result in such a LoadContent call).
Have a look at my implementation if you want, it can even display a static text message while the heavier VisualTree is build:
<DeferredContent HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
<DeferredContent.DeferredContentTemplate>
<DataTemplate>
<MyHeavyView/>
</DataTemplate>
</Controls:DeferredContent.DeferredContentTemplate>
<TextBlock Text="Loading content..."/>
</Controls:DeferredContent>
and the code
public class DeferredContent : ContentPresenter
{
public DataTemplate DeferredContentTemplate
{
get { return (DataTemplate)GetValue(DeferredContentTemplateProperty); }
set { SetValue(DeferredContentTemplateProperty, value); }
}
public static readonly DependencyProperty DeferredContentTemplateProperty =
DependencyProperty.Register("DeferredContentTemplate",
typeof(DataTemplate), typeof(DeferredContent), null);
public DeferredContent()
{
Loaded += HandleLoaded;
}
private void HandleLoaded(object sender, RoutedEventArgs e)
{
Loaded -= HandleLoaded;
Deployment.Current.Dispatcher.BeginInvoke(ShowDeferredContent);
}
public void ShowDeferredContent()
{
if (DeferredContentTemplate != null)
{
Content = DeferredContentTemplate.LoadContent();
RaiseDeferredContentLoaded();
}
}
private void RaiseDeferredContentLoaded()
{
var handlers = DeferredContentLoaded;
if (handlers != null)
{
handlers( this, new RoutedEventArgs() );
}
}
public event EventHandler<RoutedEventArgs> DeferredContentLoaded;
}

WPF FrameworkElement not receiving Mouse input

Trying to get OnMouse events appearing in a child FrameworkElement. The parent element is a Panel (and the Background property is not Null).
class MyFrameworkElement : FrameworkElement
{
protected override void OnMouseDown(MouseButtonEventArgs e)
{
// Trying to get here!
base.OnMouseDown(e);
}
}
public class MyPanel : Panel
{
protected override void OnMouseDown(MouseButtonEventArgs e)
{
// This is OK
base.OnMouseDown(e);
}
}
OnMouse never gets called, event is always unhandled and Snoop tells me that the routed event only ever seems to get as far as the Panel element.
<Window
x:Class="WpfApplication5.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:l="clr-namespace:WpfApplication5"
Title="Window1" Height="300" Width="300">
<Border x:Name="myBorder" Background="Red">
<l:MyPanel x:Name="myPanel" Background="Transparent">
<l:MyFrameworkElement x:Name="myFE"/>
</l:MyPanel>
</Border>
</Window>
Docs say that FrameworkElement handles Input, but why not in this scenario?
OnMouseDown will only be called if your element responds to Hit Testing. See Hit Testing in the Visual Layer. The default implementation will do hit testing against the graphics drawn in OnRender. Creating a Panel with a Transparent background works because Panel draws a rectangle over its entire area, and that rectangle will catch the hit test. You can get the same effect by overriding OnRender to draw a transparent rectangle:
protected override void OnRender(DrawingContext drawingContext)
{
drawingContext.DrawRectangle(Brushes.Transparent, null,
new Rect(0, 0, RenderSize.Width, RenderSize.Height));
}
You could also override HitTestCore so that all clicks are counted as hits:
protected override HitTestResult HitTestCore(PointHitTestParameters hitTestParameters)
{
return new PointHitTestResult(this, hitTestParameters.HitPoint);
}
I was able to reproduce the scenario you described. I did some playing around, and it wasn't until I changed the base class of MyFrameworkElement from FrameworkElement to something more concrete, like UserControl that events started firing like they should. I'm not 100% sure why this would be, but I would recommend using one of the classes derived from FrameworkElement that would suit your needs (like Panel, as you did in the example above, or Button).
I'd be curious to know the exact reason your example above produces these results...

WPF Window Aspect Ratio

I'm wondering how to maintain the aspect ratio (i.e.: 16x9) of a window in WPF upon resize--if possible in a way that leverages MVVM. As I'm new to both MVVM and WPF, I'm not sure where to begin. Thanks.
This may be difficult to do with a "pure" MVVM implementation, because you need to know which direction the resize happened (horizontally or vertically). Note that if both change at once (i.e. the user resizes by dragging the corner), you will need to decide which of these to use.
In your ViewModel, you will probably have a property named AspectRatio.
In your View, you will most likely override the OnRenderSizeChanged event. Its then a matter of taste whether you do the work in the view using the property from the ViewModel, or whether you pass the value to a property in the ViewModel to do the work, and bind to the new values.
Example 1: Do the work here
protected override void OnRenderSizeChanged(SizeChangedInfo sizeInfo)
{
if (sizeInfo.WidthChanged)
{
this.Width = sizeInfo.NewSize.Height * mViewModel.AspectRatio;
}
else
{
this.Height = sizeInfo.NewSize.Width * mViewModel.AspectRatio;
}
}
Example 2: Do the work in the ViewModel
View.xaml.cs
protected override void OnRenderSizeChanged(SizeChangedInfo sizeInfo)
{
if (sizeInfo.WidthChanged)
{
viewModel.AspectWidth = sizeInfo.NewSize.Width;
}
else
{
viewModel.AspectHeight = sizeInfo.NewSize.Height;
}
}
ViewModel.cs
public Double AspectWidth
{
get { return mAspectWidth; }
set
{
// Some method that sets your property and implements INotifyPropertyChanged
SetValue("AspectWidth", ref mAspectWidth, value);
SetValue("AspectHeight", ref mAspectHeight, mAspectWidth * mAspectRatio);
}
}
public Double AspectHeight
{
get { return mAspectHeight; }
set
{
// Some method that sets your property and implements INotifyPropertyChanged
SetValue("AspectHeight", ref mAspectHeight, value);
SetValue("AspectWidth", ref mAspectWidth, mAspectHeight* mAspectRatio);
}
}
And your view (for example 2) would bind the window's width and height to the AspectWidth and AspectHeight properties in the viewmodel.
View.xaml
<Window Width="{Binding AspectWidth}"
Height="{Binding AspectHeight}">
</Window>
So, in either case, you override OnRenderSizeChanged. The details on how you implement that method are up to your tastes. I guess that Example #2 is closer to pure "MVVM" but it may also be overkill in this case.

Is this WPF ProgressBar Odd render behaviour a Bug?

Hope someone can help.
I have a simple scenario where clicking checkboxes is driving a progress bar in WPF. The checkboxes are contained in a UserControl and the Progress bar is in a simple WPF client window.
On the user control I am using two dependency properties:
1) the existing Tag property has the value I wish to bind to the progress bar value and
2) a DP called CbCount which represents the total number of checkboxes.
The problem:
When the application runs the progress bar's progress shows as being 100% complete even though via Snoop I can see the value is in fact 0. Clicking on the checkboxes everything works fine as expected.
Code:
UserControl - within namespace ProgBarChkBxs:
public partial class ucChkBoxes : UserControl
{
#region CbCount
public static readonly DependencyProperty CbCountProperty =
DependencyProperty.Register("CbCount", typeof(double), typeof(ucChkBoxes),
new FrameworkPropertyMetadata((double)0));
/// <summary>
/// Gets or sets the CbCount property. This dependency property
/// indicates the number of checkBoxes
/// </summary>
public double CbCount
{
get { return (double)GetValue(CbCountProperty); }
private set { SetValue(CbCountProperty, value); }
}
#endregion
double _totalCount = 0;
double _numberChecked = 0;
double DEFAULT = 0;
public ucChkBoxes()
{
InitializeComponent();
this.Tag = DEFAULT;
this.Loaded += new RoutedEventHandler(ucChkBoxes_Loaded);
}
void ucChkBoxes_Loaded(object sender, RoutedEventArgs e)
{
if (this.ourContainer.Children.Count != 0)
{
_totalCount = this.ourContainer.Children.Count;
}
this.CbCount = (double)_totalCount;
}
private void CheckBox_Checked(object sender, RoutedEventArgs e)
{
if (e.OriginalSource.GetType() == typeof(CheckBox))
{
CheckBox cb = (CheckBox)e.OriginalSource;
if (cb.IsChecked == true) { _numberChecked++; }
if (cb.IsChecked != true) { _numberChecked--; }
//simple POC progress metric
this.Tag = (double)(_numberChecked / _totalCount * _totalCount);
}
}
}
XAML:
<UserControl x:Class="ProgBarChkBxs.ucChkBoxes"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Height="Auto" Width="Auto">
<StackPanel>
<TextBlock Text="Please select options" ></TextBlock>
<StackPanel Name="ourContainer"
CheckBox.Checked="CheckBox_Checked"
CheckBox.Unchecked="CheckBox_Checked">
<CheckBox>Fruit Juice</CheckBox>
<CheckBox>Coffee</CheckBox>
<CheckBox>Toast</CheckBox>
<CheckBox>Cereal</CheckBox>
<CheckBox>Grapefruit</CheckBox>
</StackPanel>
</StackPanel>
</UserControl>
The Client which just has the databindings is a simple window - the local namespace below refers to the project namespace xmlns:local="clr-namespace:ProgBarChkBxs", the meat of the code is:
<StackPanel>
<local:ucChkBoxes x:Name="chkBoxes"/>
<ProgressBar Name="pb" Background="Azure" Minimum="0" Height="30"
Value="{Binding ElementName=chkBoxes,Path=Tag }"
Maximum="{Binding ElementName=chkBoxes,Path=CbCount }"
/>
</StackPanel>
The really weird thing is if within the DP definition of the CbCount if I change the FrameworkPropertyMetadata to a really small value to say (double)0.001 the problem goes away.
I am running this on XP.
All help gratefully received - thanks.
Update:
I have been digging into this again as it gnaws at my sole (who said get a life!)
Things I did:
1) Adding a slider which also like progressBar inherits from RangeBase gives me the expected behaviour.
2) Spinning up reflector I can see the static ctor for ProgressBar sets the default value first to 100,
RangeBase.MaximumProperty.OverrideMetadata(typeof(ProgressBar), new FrameworkPropertyMetadata(100.0)); Should AffectMeasure?
whereas in the slider:
RangeBase.MaximumProperty.OverrideMetadata(typeof(Slider), new FrameworkPropertyMetadata(10.0, FrameworkPropertyMetadataOptions.AffectsMeasure));
3) So we need another layout pass after a I set the ProgressBar.Value
Going back to my simple POC application if within a the progressBar loaded handler in the client window I jig the layout on the first run through:
this.Width += 1; //trigger another layout pass
Then, hey, presto it works.
So is this a bug?
I still do not fully understand though how the progressBar value which is calculated from Minimum and Maximum values is affected in this way and not the Slider - the default value of Maximum appears to be having an effect and it looks as if the ProgressBar default should affect the measure pass. (missing FrameworkPropertyMetadataOptions.AffectsMeasure.)
Can anyone help, either confirm my thinking or explain what is really happening here?
ucChkBoxes_Loaded method gets called after the progressbar gets rendered. When the progressbar gets rendered, Tag and CbCount are zero meaning that the progressbar will have min=0, max=0 and value=0, which is correctly drawn as as 100%. If you invalidate the progressbar, for example resize window it will show as 0%, since now Tag and CbCount have been updated.
To fix, don't wait until ucChkBoxes.Loaded() is called to initialize your control, do it in constructor or when initializing the DP for CbCount, for example.
public ucChkBoxes()
{
InitializeComponent();
this.Tag = DEFAULT;
if (this.ourContainer.Children.Count != 0)
{
_totalCount = this.ourContainer.Children.Count;
}
this.CbCount = (double)_totalCount;
}

Synchronizing scroll positions for 2 WPF DataGrids

I am trying to synchronize the horizontal scroll position of 2 WPF DataGrid controls.
I am subscribing to the ScrollChanged event of the first DataGrid:
<toolkit:DataGrid x:Name="SourceGrid" ScrollViewer.ScrollChanged="SourceGrid_ScrollChanged">
I have a second DataGrid:
<toolkit:DataGrid x:Name="TargetGrid">
In the event handler I was attempting to use the IScrollInfo.SetHorizontalOffset, but alas, DataGrid doesn't expose IScrollInfo:
private void SourceGrid_ScrollChanged(object sender, ScrollChangedEventArgs e)
{
((IScrollInfo)TargetGrid).SetHorizontalOffset(e.HorizontalOffset);
// cast to IScrollInfo fails
}
Is there another way to accomplish this? Or is there another element on TargetGrid that exposes the necessary IScrollInfo to achieve the synchronization of the scroll positions?
BTW, I am using frozen columns, so I cannot wrap both DataGrid controls with ScrollViewers.
There is great piece of code to do this:
http://www.codeproject.com/KB/WPF/ScrollSynchronization.aspx
According to the Microsoft product group, traversing the visual tree to find the ScrollViewer is the recommended method, as explained in their answer on Codeplex.
We had this same problem when using the Infragistics grid because it didn't (still doesn't) support frozen columns. So we had two grids side-by-side that were made to look as one. The grid on the left didn't scroll horizontally but the grid on the right did. Poor man's frozen columns.
Anyway, we ended up just reaching into the visual tree and pulling out the ScrollViewer ourselves. Afterall, we knew it was there - it just wasn't exposed by the object model. You could use a similar approach if the WPF grid does not expose the ScrollViewer. Or you could subclass the grid and add the functionality you require to make this work.
Interested in hearing why you need to do this.
This is a great solution. Worked fine for me in WPF.
http://www.codeproject.com/Articles/39244/Scroll-Synchronization
I just made a reference to ScrollSynchronizer dll, added a xml import:
xmlns:scroll="clr-namespace:ScrollSynchronizer"
then just added this to both my datagrids and bobs your uncle:
<DataGrid.Resources>
<Style TargetType="ScrollViewer">
<Setter Property="scroll:ScrollSynchronizer.ScrollGroup" Value="Group1" />
</Style>
</DataGrid.Resources>
You can trick the datagrid to expose its ScrollViewer as public property for each grid, when for example innerGridControl_ScrollChanged() handler called during initialisation of the usercontrol.
To expose it you can make your grid in an xaml View file, and then compose two of them in another xaml View.
Below code is on the innerGrid.xaml.cs for example:
public ScrollViewer Scroller { get; set; } // exposed ScrollViewer from the grid
private bool _isFirstTimeLoaded = true;
private void innerGridControl_ScrollChanged(object sender, ScrollChangedEventArgs e)
{
if (_isFirstTimeLoaded) // just to save the code from casting and assignment after 1st time loaded
{
var scroller = (e.OriginalSource) as ScrollViewer;
Scroller = scroller;
_isFirstTimeLoaded = false;
}
}
on OuterGridView.xaml put an attached event handler definition:
<Views:innerGridView Grid.Row="1" Margin="2,0,2,2" DataContext="{Binding someCollection}"
x:Name="grid1Control"
ScrollViewer.ScrollChanged="Grid1Attached_ScrollChanged"
></Views:innerGridView>
<Views:innerGridView Grid.Row="3" Margin="2,0,2,2" DataContext="{Binding someCollection}"
x:Name="grid2Control"
ScrollViewer.ScrollChanged="Grid2Attached_ScrollChanged"
></Views:innerGridView>
then access that public ScrollViewer.SetHorizontalOffset(e.HorizontalOffset) method when another scrolling event occur.
Below code is in the OuterGridView.xaml.cs on one of the handler definition (
private void Grid1Attached_ScrollChanged(object sender, ScrollChangedEventArgs e)
{
if (e != null && !e.Handled)
{
if (e.HorizontalChange != 0.0)
{
grid2Control.Scroller.ScrollToHorizontalOffset(e.HorizontalOffset);
}
e.Handled = true;
}
}
private void Grid2Attached_ScrollChanged(object sender, ScrollChangedEventArgs e)
{
if (e != null && !e.Handled)
{
if (e.HorizontalChange != 0.0)
{
grid1Control.Scroller.ScrollToHorizontalOffset(e.HorizontalOffset);
}
e.Handled = true;
}
}
Also make sure any other scroll_changed event inside the inner grid (if any, for example if you define a TextBox with default scroller in one of the column data template) has its e.Handled set to true to prevent outer grid's handler processing it (this happened due to default bubbling behaviour of routedevents). Alternatively you can put additional if check on e.OriginalSource or e.Source to filter the scroll event you're intended to process.

Resources