画像をクリックした場所の座標を取得する

本音はサンプルソースをビルド実行して動作を確認してもらいたいのですが、ZIPソースコードをダウンロードするのに抵抗感があるひとも多いと思うので、むりやり文章で説明を試みます。

画像をクリックした場所の座標を取得するには、Image を内包した Grid で マウスクリックやマウス移動イベントを拾い上げます。WindowsSDK 的にいえば WM_LBUTTONDOWN や WM_MOUSEMOVE のことです。

これを自分の好きなイベントハンドラ名で定義します。ここでは OnMouseDown() と OnMouseMove() にしました。

Grid で生じたマウスイベントによるマウス座標は Image の左上隅座標を基準とします。MainWindow.xaml.cs の OnMouseDown() の冒頭にある

Point pnt = e.GetPosition( image000 );

というコードがそれにあたります。e.GetPosition() において、引数を別のコントロールにすれば、そのコントロールの左上隅座標が基準になります。

注目点すべきは Grid 内ならばどこでもマウスイベントが発生するため、Image が占める領域以外の座標も取得できてしまうということです。

たとえば Image 左上隅よりも左上の領域では、X, Y座標値は、ともにマイナスを示します。

Grid 内で生じたマウスクリック座標ならばどこでも取得できるというのは利点でもありますが、画像処理の観点からすると Image の領域以外の座標が取得されてしまうと大変問題があります。

例えば、画像のサイズが 640 × 480 ピクセルである場合、

X座標は 0 ~ 639 以外の座標値
Y座標は 0 ~ 479 以外の座標値

が取得されてしまうと、画像のピクセルにアクセスするアドレス計算を行う場合に不具合がおこり、意図とは違う画素をアクセスしたり、画像メモリ以外の場所へのアクセスエラーを生じたりします。( 昔のOSだと突然ブルースクリーンになりました )

それを避けるために Image の領域外の座標値が取得されると強制的に領域内に収めるようにします。MainWindow.xaml.cs において128~131行目で施している領域境界チェックがそれにあたります。こういうのを英語そのままでバウンダリチェックといったりします。

<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" Height="760" Width="640">

    <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="menuFileOpen" Header="Open" Click="menuFileOpen_Click" />
                <MenuItem x:Name="menuFileSave" Header="Save" Click="menuFileSave_Click" />
            </MenuItem>

            <MenuItem x:Name="menuDebug" Header="Debug">
                <MenuItem x:Name="menuDebugExec000" Header="Exec000" Click="menuDebugExec000_Click" />
                <MenuItem x:Name="menuDebugExec001" Header="Exec001" Click="menuDebugExec001_Click" />
                <MenuItem x:Name="menuDebugExec002" Header="Exec002" Click="menuDebugExec002_Click" />
                <MenuItem x:Name="menuDebugExec003" Header="Exec003" Click="menuDebugExec003_Click" />
            </MenuItem>

        </Menu>

        <ToolBarTray DockPanel.Dock="Top">
            <ToolBar>

                <Border BorderBrush="DarkViolet" BorderThickness="1" >
                    <Button x:Name="tbnApplicationQuit" Content="Quit" Width ="48" Height="48" Click="menuApplicationQuit_Click"> </Button>
                </Border>

                <Separator />

                <Button x:Name="tbnFileOpen" Content="Open" Width ="48" Height="48" Click="menuFileOpen_Click"> </Button>
                <Button x:Name="tbnFileSave" Content="Save" Width ="48" Height="48" Click="menuFileSave_Click"> </Button>

                <Separator />

                <Button x:Name="tbnDebugExec000" Content="Exec000" Width ="48" Height="48" Click="menuDebugExec000_Click"></Button>
                <Button x:Name="tbnDebugExec001" Content="Exec001" Width ="48" Height="48" Click="menuDebugExec001_Click"></Button>
                <Button x:Name="tbnDebugExec002" Content="Exec002" Width ="48" Height="48" Click="menuDebugExec002_Click"></Button>
                <Button x:Name="tbnDebugExec003" Content="Exec003" Width ="48" Height="48" Click="menuDebugExec003_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"/>
        </StatusBar>

        <Border BorderBrush="DarkViolet" BorderThickness="1" >
            <ScrollViewer Name="scv000" Background="DarkViolet" ScrollViewer.HorizontalScrollBarVisibility="Visible" ScrollViewer.VerticalScrollBarVisibility="Visible">
                <Grid x:Name="grid000" MouseDown="OnMouseDown" MouseMove="OnMouseMove">
                    <Image x:Name="image000" Stretch="Uniform"/>
                </Grid>
            </ScrollViewer>
        </Border>

    </DockPanel>


</Window>

using System.Windows.Media.Imaging; をお忘れなく。

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 tot add this sentence.
using System.Windows.Media.Imaging;

namespace aaa
{

	public partial class MainWindow : Window
	{

		const int INI_W = 512;
		const int INI_H = 512;

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

		byte [] Data000;
		WriteableBitmap Bmp000;

		const int NUM_ELEMENT_OF_PAL = 256;

		int TheX = 0;
		int TheY = 0;

		bool get_uroko_data( byte [] data, int width, int height )
		{

			int w = width;
			int h = height;
			byte level;

			for ( int j = 0; j < h; j++ )
			{
				for ( int i = 0; i < w; i++ )
				{
					level = (byte)( i + j );
					data[ i + w  * j ] = level;
				}
			}

			return true;

		}

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

		public MainWindow()
		{

			InitializeComponent();

			int w = INI_W;
			int h = INI_H;
			int numpix = w * h;
			Data000 = new byte[ numpix ];

			// ななめウロコデータを格納する.
			get_uroko_data( Data000, w, h );

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

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

			Int32Rect rct = new Int32Rect( 0, 0, w, h );
			int stride_bytes = w * sizeof( byte );
			int offset = 0;
			Bmp000.WritePixels( rct, Data000, stride_bytes, offset );
			
			// 画像出力先とビットマップを関係づける.
			image000.Width = Bmp000.PixelWidth;
			image000.Height = Bmp000.PixelHeight;
			image000.Source = Bmp000;

		}

		protected override void OnRender( DrawingContext dc )
		{

			String str = String.Format( "You click ({0},{1})", TheX, TheY );

			// タイトルバーに座標を表示する.
			this.Title = str;

			// ステータスバーのラベルに座標を表示する.
			stbLabel000.Content = str;
			stbLabel001.Content = "label001";
			stbLabel002.Content = "label002";
			stbLabel003.Content = "label003";

		}

		private void OnMouseDown(object sender, MouseEventArgs e)
		{

			Point pnt = e.GetPosition( image000 );

			int w = (int)( Math.Round( image000.Width ));
			int h = (int)( Math.Round( image000.Height ));

			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;

			// 再描画をうながす.
			this.InvalidateVisual();

		}

		private void OnMouseMove(object sender, MouseEventArgs e)
		{

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

		}

		private void menuApplicationQuit_Click( object sender, RoutedEventArgs e )
		{
			this.Close();
		}

		private void menuFileOpen_Click( object sender, RoutedEventArgs e )
		{

			if ( sender.Equals( menuFileOpen ))
			{
				MessageBox.Show( "FileOpen() sender is Menu." );
			}
			else
			{
				MessageBox.Show( "FileOpen() sender is ToolButton." );
			}

		}

		private void menuFileSave_Click( object sender, RoutedEventArgs e )
		{

			if ( sender.Equals( menuFileSave ))
			{
				MessageBox.Show( "FileSave() sender is Menu." );
			}
			else
			{
				MessageBox.Show( "FileSave() sender is ToolButton." );
			}

		}

		private void menuDebugExec000_Click( object sender, RoutedEventArgs e )
		{

			if ( sender.Equals( menuDebugExec000 ))
			{
				MessageBox.Show( "DebugExec000() sender is Menu." );
			}
			else
			{
				MessageBox.Show( "DebugExec000() sender is ToolButton." );
			}

		}

		private void menuDebugExec001_Click( object sender, RoutedEventArgs e )
		{

			if ( sender.Equals( menuDebugExec001 ))
			{
				MessageBox.Show( "DebugExec001() sender is Menu." );
			}
			else
			{
				MessageBox.Show( "DebugExec001() sender is ToolButton." );
			}

		}

		private void menuDebugExec002_Click( object sender, RoutedEventArgs e )
		{

			if ( sender.Equals( menuDebugExec002 ))
			{
				MessageBox.Show( "DebugExec002() sender is Menu." );
			}
			else
			{
				MessageBox.Show( "DebugExec002() sender is ToolButton." );
			}

		}

		private void menuDebugExec003_Click( object sender, RoutedEventArgs e )
		{

			if ( sender.Equals( menuDebugExec003 ))
			{
				MessageBox.Show( "DebugExec003() sender is Menu." );
			}
			else
			{
				MessageBox.Show( "DebugExec003() sender is ToolButton." );
			}

		}
	}

}

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

このような感じの実行イメージです。クリックしたポイントの座標値は、タイトルバーとステータスバーにリアルタイム表示するようにしました。

画像以外の部分をクリックしても反応がないのはバウンダリチェックのおかげです。ためしに 128~131 行目をコメントアウトしてみれば、バウンダリチェックの効き目がわかってもらえると思います。

ここまできたら、取得された座標を使って Image 上に十字線をオーバレイ描画したくなりますね。これについては別の記事で紹介します。

下記の記事でマウスの動きとは関係なく画像上にオーバレイ描画をする方法を紹介しています。

画像の中心に十字線と円を描画する | GazoYaro

画像に線分や円などのグラフィック要素をオーバレイ描画する方法を紹介します。