一、はじめに
WPFの開発において、ScrollViewerはよく使われるコントロールです。自分のプロジェクトで、ScrollViewer内のコンテンツが長すぎるとスクロールバーのつまみが小さくなり、クリックしにくくなるというフィードバックを受けました。最初はスタイルでつまみの最小値を設定しようとしましたが、効果がありませんでした。そこで別の方法として、元のつまみを非表示にし、代わりにコントロールを追加して間接的にScrollViewerのスクロールを制御することにしました。
二、本文
- ここでは以前作成した曲線グラフコントロールを例に示します。曲線グラフのデータが多いと、つまみが非常に小さくなります。これはデフォルトスタイルの場合で、カスタムスタイルの場合はさらに小さくなります。

- 曲線グラフの上にCanvasを配置し、Borderをつまみとして追加します。Canvas全体を曲線グラフに重ねているのは、クリックしてドラッグ移動する機能も追加したいからです。そしてScrollViewerのつまみを非表示にし、つまみの最小幅と高さを設定し、デフォルトで非表示にします。
<Grid>
<local:CruveDrawingVisual x:Name="curve" Margin="0,10,0,15" />
<ScrollViewer
Name="scroll"
HorizontalScrollBarVisibility="Hidden"
ScrollChanged="ScrollViewer_ScrollChanged"
VerticalScrollBarVisibility="Disabled">
<Canvas x:Name="canvas" />
</ScrollViewer>
<Canvas x:Name="CurvePanel" Background="Transparent">
<Border
x:Name="border"
Canvas.Left="0"
Canvas.Bottom="0"
Height="15"
MinWidth="80"
Background="Green"
PreviewMouseLeftButtonDown="Border_PreviewMouseLeftButtonDown"
Visibility="Collapsed" />
</Canvas>
</Grid>
- 次にバックエンドで対応するロジックコードを追加します。詳細はコード内のコメントに記載しているので、ここでは省略します。
public partial class MainWindow : Window
{
private bool isAdd = true;
private List<int> lists = new List<int>();
private Point point_border;
private double offset = -1;
public MainWindow()
{
InitializeComponent();
CurvePanel.MouseMove += delegate (object sender, MouseEventArgs e)
{
if (e.LeftButton == MouseButtonState.Pressed)
{
if (Mouse.Captured == null) Mouse.Capture(CurvePanel);
// つまみをドラッグ
if (isBorder)
{
Point point = e.GetPosition(this);
// マウスがコントロールの左端を超えた場合
if (point.X - point_border.X <= 0)
{
scroll.ScrollToHorizontalOffset(0);
}
// マウスがコントロールの右端を超えた場合
else if (point.X - point_border.X >= CurvePanel.ActualWidth - border.ActualWidth)
{
scroll.ScrollToHorizontalOffset(lists.Count - CurvePanel.ActualWidth);
}
// マウスがコントロール範囲内の場合
else if (point.X - point_border.X > 0 && point.X - point_border.X < CurvePanel.ActualWidth - border.ActualWidth)
{
double left = point.X - point_border.X;
scroll.ScrollToHorizontalOffset((lists.Count - CurvePanel.ActualWidth) / (CurvePanel.ActualWidth - border.ActualWidth) * left);
}
}
// キャンバスをドラッグ
else
{
if (offset >= 0 && offset <= CurvePanel.ActualWidth)
{
scroll.ScrollToHorizontalOffset(scroll.HorizontalOffset - (e.GetPosition(this).X - offset));
}
offset = e.GetPosition(this).X;
}
}
else
{
offset = -1;
isBorder = false;
Mouse.Capture(null); // マウスキャプチャを解放
}
};
}
private void Window_Loaded(object sender, RoutedEventArgs e)
{
int temp = 20;
for (int i = 0; i < 24 * 60 * 60 * 2; i++)
{
if (isAdd)
{
lists.Add(temp);
temp += 2;
}
else
{
lists.Add(temp);
temp -= 2;
}
if (temp == 280) isAdd = false;
if (temp == 20) isAdd = true;
}
canvas.Width = lists.Count;
// つまみを表示するか判定
if (canvas.Width > CurvePanel.ActualWidth)
{
border.Visibility = Visibility.Visible;
// ScrollViewerのコンテンツの比率に基づいてつまみの幅を計算
border.Width = CurvePanel.ActualWidth * CurvePanel.ActualWidth / canvas.Width;
}
else
{
border.Visibility = Visibility.Collapsed;
}
curve.SetupData(lists);
}
private void ScrollViewer_ScrollChanged(object sender, ScrollChangedEventArgs e)
{
curve.OffsetX(scroll.HorizontalOffset);
Canvas.SetLeft(border, scroll.HorizontalOffset / ((lists.Count - CurvePanel.ActualWidth) / (CurvePanel.ActualWidth - border.ActualWidth)));
}
private bool isBorder = false;
private void Border_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
isBorder = true;
// マウスがつまみをクリックした位置を取得
point_border = e.GetPosition(border);
}
}
- 実行結果は以下の通りです。
