1. 什么是滾動輪劫持
這篇文章介紹一個很簡單的繼承自ScrollViewer的控件:
public class ExtendedScrollViewer : ScrollViewer
{
protected override void OnMouseWheel(MouseWheelEventArgs e)
{
if (ViewportHeight + VerticalOffset >= ExtentHeight && e.Delta <= 0)
return;
if (VerticalOffset == 0 && e.Delta >= 0)
return;
base.OnMouseWheel(e);
}
}
所有代碼就這么多,這個ExtendedScrollViewer 只是用來解決滾動輪劫持(scroll-wheel-hijack)的問題。所謂的滾動輪劫持,簡單來說即是在一個可以滾動的頁面使用鼠標滾輪滾動頁面的過程中鼠標進入某個可以滾動的子元素導致只在這個子元素中滾動而整個頁面想滾滾不動了。
具體看看這個例子:
這個情況相信很多人都遇到過,滾輪被“劫持”后索性去拖動滾動條。有次我遇到個內嵌了很多ScrollViewer的長頁面,使用起來真的很惱人,所以我使用ExtendedScrollViewer 解決了這個問題。當然還有另外很多種情況的滾動輪劫持,也有很多解決方案,這篇文章只介紹我遇到的情況和我的解決方案。
2. 實現
在WPF中要禁止ScrollViewer捕獲鼠標滾動時間,可以重寫OnMouseWheel
成一個空的方法:
protected override void OnMouseWheel(MouseWheelEventArgs e)
{
}
OnMouseWheel方法用於響應鼠標滾輪的事件,將它重載成空方法即不再處理鼠標滾利事件。注意在這種情況下不可以使用e.Handled = true
,因為我們的目標是讓外層的ScrollViewer可以接收到鼠標滾輪事件,所以不能更改MouseWheelEventArgs 的Handled。
當然我們不滿足於無腦禁用鼠標滾輪,我們應該更智能些,先讓ScrollViewer滾到底,再交由外層的ScrollViewer滾下去。這里面用到幾個屬性:
MouseWheelEventArgs中的Delta表示鼠標滾輪的變更量,當這個值為正數時表示滾輪向上。
ExtentHeight,獲取ScrollViewer內容的實際高度。
ViewportHeight,獲取當前可視區域的高度。
VerticalOffset,包含滾動內容對應於頁首的垂直偏移量的值,有效值介於 0 與 ExtentHeight 減去 ViewportHeight 所得的數值之間。
熟悉了上面幾個屬性的作用后我們可以更好地控制鼠標滾輪的行為,當鼠標向上滾動時,判斷現在是否已經滾到頂了,如果是就不處理鼠標滾輪事件:
if (VerticalOffset == 0 && e.Delta >= 0)
return;
而當鼠標向下滾動時,需要根據ViewportHeight
、VerticalOffset
和ExtentHeight
判斷當前是否已經滾動到底,如果是就不處理鼠標滾輪事件:
if (ViewportHeight + VerticalOffset >= ExtentHeight && e.Delta <= 0)
return;
3. 其他ScrollViewer方案
ScrollViewer還有很多中玩法,但我工作中不常用到所以就沒做。如果覺得不滿足還可以參考HandyControl的ScrollViewer,它直接提供了一個CanMouseWheel
屬性用於控制是否響應鼠標滾輪,另外還支持了滾動等功能。
4. 參考
ScrollViewer.OnMouseWheel(MouseWheelEventArgs) Method (System.Windows.Controls) Microsoft Docs
MouseWheelEventArgs.Delta Property (System.Windows.Input) Microsoft Docs
ScrollViewer.ExtentHeight Property (System.Windows.Controls) Microsoft Docs
ScrollViewer.ViewportHeight Property (System.Windows.Controls) Microsoft Docs
ScrollViewer.VerticalOffset Property (System.Windows.Controls) Microsoft Docs