スクロールビューのスクロール量をSet/Getする

画像のサイズが画面より大きい場合、または画像のサイズ自体は小さくてもそれを拡大表示した場合は、スクロールビューによって表示するのが最適です。

そこで、スクロールビューに付随するスクロールバーのスクロール量(ツマミの位置)を設定したり、取得したりする方法を紹介します。

<Window x:Class="aaa.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:aaa"
        mc:Ignorable="d"
        Title="MainWindow" Width="720" Height="760">

    <DockPanel>

        <Menu DockPanel.Dock="Top">

            <MenuItem x:Name="menuApplication" Header="Application">
                <MenuItem x:Name="menuApplicationQuit" Header="Quit" Click="menuApplicationQuit_Click"/>
            </MenuItem>

            <MenuItem x:Name="menuFile" Header="File">
                <MenuItem x:Name="menuFileSave" Header="Save" Click="menuFileSave_Click"/>
            </MenuItem>
            
            <MenuItem x:Name="menuCursor" Header="Cursor">
                <MenuItem x:Name="menuCursorMoveToCenter" Header="MoveToCenter" Click="menuCursorMoveToCenter_Click" />
                <MenuItem x:Name="menuCursorMoveToLeftTop" Header="MoveToLT" Click="menuCursorMoveToLeftTop_Click" />
                <MenuItem x:Name="menuCursorMoveToRightBottom" Header="MoveToRB" Click="menuCursorMoveToRightBottom_Click" />
            </MenuItem>

        </Menu>

        <ToolBarTray DockPanel.Dock="Top">
            <ToolBar>
                <Button x:Name="tbnFileSave" Content="Save" Width ="48" Height="48" Click="menuFileSave_Click"></Button>
                <Separator/>
                <Button x:Name="tbnCursorMoveToCenter" Content="MoveToCenter" Width ="96" Height="48" Click="menuCursorMoveToCenter_Click"></Button>
                <Button x:Name="tbnCursorMoveToLeftTop" Content="MoveToLT" Width ="96" Height="48" Click="menuCursorMoveToLeftTop_Click"></Button>
                <Button x:Name="tbnCursorMoveToRightBottom" Content="MoveToRB" Width ="96" Height="48" Click="menuCursorMoveToRightBottom_Click"></Button>
            </ToolBar>
        </ToolBarTray>

        <StatusBar DockPanel.Dock="Bottom" Height="30">
            <Label Name="stbLabel000"/>
            <Separator/>
            <Label Name="stbLabel001"/>
            <Separator/>
            <Label Name="stbLabel002"/>
            <Separator/>
            <Label Name="stbLabel003"/>
            <Separator/>
            <Label Name="stbLabel004"/>
        </StatusBar>

        <Border BorderBrush="DarkViolet" BorderThickness="1" >
            <ScrollViewer Name="scv000" Background="DarkViolet" ScrollViewer.HorizontalScrollBarVisibility="Visible" ScrollViewer.VerticalScrollBarVisibility="Visible" ScrollChanged="OnScrollChanged">
                <Grid x:Name="grid000" MouseDown="OnMouseDown" MouseMove="OnMouseMove">

                    <Image x:Name="image000" Stretch="Uniform"/>

                    <Canvas x:Name="canvas000" Background="Transparent">
                        <Line x:Name="cross_hair_horz" Stroke="Aqua" StrokeThickness="2"/>
                        <Line x:Name="cross_hair_vert" Stroke="Aqua" StrokeThickness="2"/>
                    </Canvas>
                    
                </Grid>
            </ScrollViewer>
        </Border>

    </DockPanel>


</Window>

下記の MainWindow.xaml.cs において、
234行目、水平方向のスクロール量 HorizontalOffset を参照しています。
235行目、垂直方向のスクロール量 VerticalOffset を参照しています。

307行目からは、スクロール量を設定するための private なメソッドを作ってみました。
338行目、ScrollToHorizontalOffset()で、水平方向のスクロール量を設定します。
339行目、ScrollToVerticalOffset()で、垂直方向のスクロール量を設定します。

そのときに与える引数には、スクロールビューで見えている領域の縦横サイズ( ビューポートのサイズとでもいうのでしょうか )を考慮してやるとユーザーフレンドリーな動作になると思います。

329行目、ビューポートの横(水平方向)のサイズは ActualWidth で取得できます。
330行目、ビューポートの縦(垂直方向)のサイズは ActualHeight で取得できます。

335行目、ScrollToHorizontalOffset() に与える引数をビューポートの横サイズを考慮して計算します。
336行目、ScrollToVerticalOffset() に与える引数をビューポートの縦サイズを考慮して計算します。

下記の名前空間の追加をお忘れなく.
using System.Windows.Media.Imaging;
using System.IO;
using Microsoft.Win32;

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.Windows.Media;

// これの追加を忘れないように. Don't forget to add this sentence.
using System.Windows.Media.Imaging;
using System.IO;
using Microsoft.Win32;

namespace aaa
{

	public partial class MainWindow : Window
	{

		const double DPI_X = 96.0;
		const double DPI_Y = 96.0;

		byte [] Data000;
		WriteableBitmap Bmp000;

		int TheX;
		int TheY;

		// 画像のサイズ.
		const int REQ_W = 4096;
		const int REQ_H = 2048;

		// コサイン波形の周期.
		const double CYCLE_HORZ = 32.0;
		const double CYCLE_VERT =  8.0;

		// 水平垂直加算コサインデータを取得する.
		private bool get_data_cos_wave_horz_vert(
						byte [] data,
						int width,
						int height,
						double cycle_horz, // 水平方向の周期.
						double cycle_vert  // 垂直方向の周期.
						)
		{

			int w = width;
			int h = height;

			double two_pai = 2.0 * Math.PI;
			double rate;
			double rad;

			double tmp;

			double [] table_for_a = new double[ h ];
			double [] table_for_b = new double[ w ];

			const double OFFSET = 1.0;
			const double MUL = 0.5;

			// テーブルを作る.
			for ( int j = 0; j < h; j++ )
			{
				// ラジアンを算出する.
				rate = (double)(j)/(double)( h - 1 );
				rad = rate * two_pai;

				// ここでコサインの計算をする.
				tmp = -( Math.Cos( cycle_vert * rad ));

				// 0.0 から 1.0 が格納される.
				table_for_a[j] = (( tmp + OFFSET ) * MUL );
			}

			// テーブルを作る.
			for ( int i = 0; i < w; i++ )
			{
				// ラジアンを算出する.
				rate = (double)(i)/(double)( w - 1 );
				rad = rate * two_pai;

				// ここでコサインの計算をする.
				tmp = -( Math.Cos( cycle_horz * rad ));

				// 0.0 から 1.0 が格納される.
				table_for_b[i] = (( tmp + OFFSET ) * MUL );
			}

			double a;
			double b;
			byte level;

			int adrs;

			for ( int j = 0; j < h; j++ )
			{

				// 0.0 から 1.0 の範囲をとる.
				a = table_for_a[j];

				adrs = w * j;

				for ( int i = 0; i < w; i++ )
				{
					// 0.0 から 1.0 の範囲をとる.
					b = table_for_b[i];

					// 両方を加算すると 0.0 から 2.0 の範囲をとる.
					tmp = a + b; 

					// 0 から 255 の byte 区間にマッピングする.
					level = (byte)( Math.Round(( tmp * 0.5 ) * 255.0 ));

					// 8bits画像データとして格納する.
					data[ adrs ] = level;

					adrs++;

				}
			}

			return true;

		}

		///////////////////////////////////////////////////////////////////////
		///////////////////////////////////////////////////////////////////////
		///////////////////////////////////////////////////////////////////////

		public MainWindow()
		{

			// これはデフォルトのコード.
			InitializeComponent();

			// データ領域を確保する.
			int w = REQ_W;
			int h = REQ_H;
			int numpix = w * h;
			Data000 = new byte[ numpix ];

			// 確保した領域にデータを仕込む.
			get_data_cos_wave_horz_vert( Data000, w, h, CYCLE_HORZ, CYCLE_VERT );

			// グレースケールパレットの要素数.
			const int NUM_ELEMENT_OF_PAL = 256;

			// 8bppのビットマップを作るので256要素カラーパレットを作る.
			List<Color> list_color = new List<Color>(0);
			for ( int k = 0; k < NUM_ELEMENT_OF_PAL; k++ )
			{
				const byte LEVEL_ALPHA = 0xff;
				byte value = ( byte )(k);
				list_color.Add( Color.FromArgb( LEVEL_ALPHA, value, value, value ) );
			}

			// 白黒グレースケールのカラーパレット.
			BitmapPalette Palette = new BitmapPalette( list_color );

			// 8bppのビットマップを生成する.
			PixelFormat pixfmt = PixelFormats.Indexed8;
			Bmp000 = new WriteableBitmap( w, h, DPI_X, DPI_Y, pixfmt, Palette );

			// -------------------------------------------------------------------
			// Bmp000 に WritePixels( Data000 ) するのは OnRender() で実施する.
			// -------------------------------------------------------------------

			// 画像出力先のサイズを決定する.
			image000.Width = Bmp000.PixelWidth;
			image000.Height = Bmp000.PixelHeight;

			// 画像出力先にビットマップを関係づける.
			image000.Source = Bmp000;

			// 十字カーソルは真ん中に出現.
			TheX = w >> 1;
			TheY = h >> 1;

			// ウインドウのタイトル.
			this.Title = "GazoYaro";

		}

		protected override void OnRender( DrawingContext dc )
		{

			// 画像のサイズを取得する.
			int w = Bmp000.PixelWidth;
			int h = Bmp000.PixelHeight;

			// ビットマップに画像データを書き込む.
			Int32Rect rct = new Int32Rect( 0, 0, w, h );
			int stride_bytes = w * sizeof( byte );
			int offset_bytes = 0;
			Bmp000.WritePixels( rct, Data000, stride_bytes, offset_bytes );

			// カーソル位置の濃度を取得する.
			int adrs = ( TheX + ( w * TheY ));
			byte the_level = Data000[ adrs ];

			// 親要素 Grid000 の座標系から image000 のオフセット量を取得する.
			Point img_point = image000.TranslatePoint( new Point( 0, 0 ), grid000 );
			double plot_offset_x = img_point.X;
			double plot_offset_y = img_point.Y;

			// Canvas の座標系は Grid の座標系と同じ.
			double plot_x = (double)( TheX + plot_offset_x );
			double plot_y = (double)( TheY + plot_offset_y );

			// 出力コントロール image000 のサイズを取得する.
			double img_w = image000.Width;
			double img_h = image000.Height;

			// 水平線を引く(十字線の横).
			cross_hair_vert.X1 = plot_x;
			cross_hair_vert.Y1 = plot_offset_y;
			cross_hair_vert.X2 = plot_x;
			cross_hair_vert.Y2 = plot_offset_y + img_h;

			// 垂直線を引く(十字線の縦).
			cross_hair_horz.X1 = plot_offset_x;
			cross_hair_horz.Y1 = plot_y;
			cross_hair_horz.X2 = plot_offset_x + img_w;
			cross_hair_horz.Y2 = plot_y;

			// スクロールバーのスクロール量を取得する.
			double scroll_x = scv000.HorizontalOffset;
			double scroll_y = scv000.VerticalOffset;

			// スクロールビューの領域サイズを取得する.
			double scv_aw = scv000.ActualWidth;
			double scv_ah = scv000.ActualHeight;

			// ステータスバーのラベルに座標を表示する.
			const String FMT000 = "PlotOffsetXY({0},{1})";;
			const String FMT001 = "BmpWH {0}*{1}";
			const String FMT002 = "TheXY({0},{1}) is {2}";
			const String FMT003 = "ScrollX,ScrollY {0:f1},{1:f1}";
			const String FMT004 = "ScrollView size {0:f1}*{1:f1}";
			String str000 = String.Format( FMT000, plot_offset_x, plot_offset_y );
			String str001 = String.Format( FMT001, Bmp000.PixelWidth, Bmp000.PixelHeight );
			String str002 = String.Format( FMT002, TheX, TheY, the_level );
			String str003 = String.Format( FMT003, scroll_x, scroll_y );
			String str004 = String.Format( FMT004, scv_aw, scv_ah );
			stbLabel000.Content = str000;
			stbLabel001.Content = str001;
			stbLabel002.Content = str002;
			stbLabel003.Content = str003;
			stbLabel004.Content = str004;

		}

		private void OnMouseDown(object sender, MouseEventArgs e)
		{

			// 画像のサイズを取得する.
			int w = (int)( Math.Round( image000.Width ) );
			int h = (int)( Math.Round( image000.Height ) );

			// image000 基準のマウス座標を取得する.
			Point pnt = e.GetPosition( image000 );

			// ここに入る値はゼロ未満や幅高以上もありうる.
			int tmp_x = (int)( Math.Round( pnt.X ) );
			int tmp_y = (int)( Math.Round( pnt.Y ) );

			// クリックした座標が画像からはみ出ないようにする.
			if ( tmp_x < 0 ) { tmp_x = 0; }
			if ( tmp_y < 0 ) { tmp_y = 0; }
			if ( tmp_x >= w ) { tmp_x = w - 1; }
			if ( tmp_y >= h ) { tmp_y = h - 1; }

			// ここで代入される値は画像領域の内側であることが保証される.
			TheX = tmp_x;
			TheY = tmp_y;

			// 再描画をうながす、OnRender() が呼ばれる.
			this.InvalidateVisual();

		}

		private void OnMouseMove(object sender, MouseEventArgs e)
		{

			// 左ボタンが押し下げられているかどうか.
			if (( e.LeftButton & MouseButtonState.Pressed ) == MouseButtonState.Pressed )
			{
				// 押し下げられていたら OnMouseDown を実行する.
				this.OnMouseDown( sender, e );
			}

		}

		private void OnScrollChanged( object sender, ScrollChangedEventArgs e )
		{
			// スクロールバーが動かされたら OnRender() 再描画する.
			this.InvalidateVisual();
		}

		// 所望の位置に水平垂直スクロールバーを移動する.
		private bool set_scroll_offset(
						ScrollViewer scroll_viewer,
						int image_width,
						int image_height,
						int the_x_on_image,
						int the_y_on_image
						)
		{
		
			if ( scroll_viewer == null ) return false;
			if ( image_width <= 0 ) return false;
			if ( image_height <= 0 ) return false;

			int w = image_width;
			int h = image_height;

			if ( the_x_on_image < 0 || w <= the_x_on_image ) return false;
			if ( the_y_on_image < 0 || h <= the_y_on_image ) return false;

			ScrollViewer scv = scroll_viewer;

			double scv_aw = scroll_viewer.ActualWidth;
			double scv_ah = scroll_viewer.ActualHeight;

			int req_x = the_x_on_image;
			int req_y = the_y_on_image;

			double req_scroll_x = ( req_x - ( scv_aw * 0.5 ));
			double req_scroll_y = ( req_y - ( scv_ah * 0.5 ));

			scv.ScrollToHorizontalOffset( req_scroll_x );
			scv.ScrollToVerticalOffset( req_scroll_y );

			return true;

		}

		private void menuCursorMoveToCenter_Click( object sender, RoutedEventArgs e )
		{

			int w = Bmp000.PixelWidth;
			int h = Bmp000.PixelHeight;

			// 画像のまんなか.
			TheX = w >> 1;
			TheY = h >> 1;

			// 所望の位置に水平垂直スクロールバーを移動する.
			bool ret = set_scroll_offset( scv000, w, h, TheX, TheY );
			if ( !ret )
			{
				MessageBox.Show( "Error: set_scroll_offset();" );
				return;
			}

			this.InvalidateVisual();

		}

		private void menuCursorMoveToLeftTop_Click( object sender, RoutedEventArgs e )
		{

			int w = Bmp000.PixelWidth;
			int h = Bmp000.PixelHeight;

			// 画像の左上隅 LeftTop.
			TheX = 0;
			TheY = 0;

			// 所望の位置に水平垂直スクロールバーを移動する.
			bool ret = set_scroll_offset( scv000, w, h, TheX, TheY );
			if ( !ret )
			{
				MessageBox.Show( "Error: set_scroll_offset();" );
				return;
			}

			this.InvalidateVisual();

		}

		private void menuCursorMoveToRightBottom_Click( object sender, RoutedEventArgs e )
		{

			int w = Bmp000.PixelWidth;
			int h = Bmp000.PixelHeight;

			// 画像の右下隅 RightBottom.
			TheX = w - 1;
			TheY = h - 1;

			// 所望の位置に水平垂直スクロールバーを移動する.
			bool ret = set_scroll_offset( scv000, w, h, TheX, TheY );
			if ( !ret )
			{
				MessageBox.Show( "Error: set_scroll_offset();" );
				return;
			}

			this.InvalidateVisual();

		}

		private void menuFileSave_Click( object sender, RoutedEventArgs e )
		{

			// デフォルトで表示するファイル名.
			String fnm_ini = "default_image";

			// ファイルダイアログを生成する.
			SaveFileDialog sfd = new SaveFileDialog();
			sfd.FileName = fnm_ini;
			sfd.Filter = "BMP|*.bmp|ALL|*.*";

			// ファイルダイアログを表示する.
			bool? ret = sfd.ShowDialog();
			if ( ret == false )
			{
				return; // warning.
			}

			// ---------------------------
			// ここからファイル保存 ------
			// ---------------------------

			String filepath = sfd.FileName;
			FileMode file_mode = FileMode.Create;
			FileAccess file_access = FileAccess.Write;

			using ( FileStream fs = new FileStream( filepath, file_mode, file_access ))
			{

				BmpBitmapEncoder enc_bmp = new BmpBitmapEncoder();

				BitmapFrame bmf = BitmapFrame.Create( Bmp000 );

				enc_bmp.Frames.Add( bmf );
				enc_bmp.Save( fs );

				fs.Close();

			}

		}

		private void menuApplicationQuit_Click( object sender, RoutedEventArgs e )
		{

			// ウインドウを閉じてアプリケーションを終了する.
			this.Close();

		}

	}
}

サンプルソースを用意しました。上記のコードのコピーペーストでうまく動かない場合にご利用ください。

ビルドして実行したら、ぜひウインドウ下部のステータスバーにご注目ください。

ウインドウをリサイズしたり、スクロールバーを動かしたりしてみてください。そのときのスクロール量と、ビューポートの縦横サイズがステータスバーにリアルタイム表示されています。

またツールボタンの
MoveToCenter はカーソルが画像の中央に移動します。
MoveToLT はカーソルが画像の左上隅(LeftTop)に移動します。
MoveToRB はカーソルが画像の右下隅(RightBottom)に移動します。

MoveToLT、MoveToRB の実行結果がわかりにくければ、MainWindow.xaml で StrokeThickness の値を 4 とか 8 とか、太く変更してみてください。

<Canvas x:Name="canvas000" Background="Transparent">
	<Line x:Name="cross_hair_horz" Stroke="Aqua" StrokeThickness="2"/>
	<Line x:Name="cross_hair_vert" Stroke="Aqua" StrokeThickness="2"/>
</Canvas>