動的に描画アイテムを作成し画像の上に描画する

描画アイテムの個数がアプリケーション実行前から決定しており、実行中も数が変わらないのであれば、XAMLに描画アイテムを定義しておいて、その描画アイテムの位置をコードから指定してやるだけでかまいません。

しかし、そういう状況はレアケースです。古くは MacDraw 最近では PowerPoint や Visio のようなドローソフトを作成する場合は、描画アイテムを動的に作成し、動的に配置してやる必要があります。

このようなサンプルを作ってみました。描画アイテムとして Rectangle と Line 2本と Polyline を使いました。

まずは、XAML のコードを示します。
41行目が象徴的です。描画アイテムは動的に作成するため、あらかじめ定義しようがありません。

<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>
            
        </Menu>

        <ToolBarTray DockPanel.Dock="Top">
            <ToolBar>
                <Button x:Name="tbnApplicationQuit" Content="Quit" Width ="48" Height="48" Click="menuApplicationQuit_Click"></Button>
            </ToolBar>
        </ToolBarTray>

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

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

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

                    <Canvas x:Name="canvas000" Background="Transparent">
                        <!-- 通常ならここに描画アイテムを定義するが動的に生成するため何も書かない. -->
                    </Canvas>
 
                </Grid>
            </ScrollViewer>
        </Border>

    </DockPanel>


</Window>

つぎに下記の MainWindow.xaml.cs を解説します。

描画は OnRender の中で行います。つまり描画アイテムの生成やキャンバスへの追加は、すべて再描画ごとに行うのです。(ウインドウズメッセージでいうと WM_PAINT 以外のところに描画コードを書いてはいけません)

これは、SDK や MFC や Windows Forms どんなフレームワークであっても同じです。再描画ごとに同じことを何度も実施するので、ひどく効率が悪い気がしますが、そういうものなのです。

WPFの描画速度は、前述したフレームワークよりも遅いため、非力なPCでは描画がワンテンポ遅れる気もしますが、がまんしましょう。(がまんしてまで WPF を使う必要あるのか?という疑問は生じますけれど)

さて、具体的なコード行を示して解説にはいります。

117行目からの OnRender() の中でしか描画を実施していません。
231行目からの OnMouseDown() の中では描画のコードが一切ないことにも注目してください。

140行目で、描画アイテムをすべてクリアします。すでにこれで非効率な気がしますけれど、しようがありません。

158行目からのループで矩形と対角線2本の合計3つの描画アイテムをループ回数ぶん生成して、キャンバスに追加配置します。

Rectangle は SetLet() と SetTop() で位置を決定します。
Line は (X1, Y1)( X2, Y2 ) で位置を決定します。

201~203行目が、位置を設定した矩形と2本の対角線をキャンバスに追加しているところです。

ループを抜けたら矩形を結ぶポリラインを描画します。そのためにループ内でポリライン用の座標コレクションに座標追加しておきます。これが208行目です。

213行目がポリラインを動的に生成して、ループ内で作成した座標コレクションに関連付けています。ただしこのままでは描画されません。

219行目で、ポリラインをキャンバスに追加しています。

重複した説明になりますが、201, 202, 203, 219 行目の Children.Add() を実行しなければ、内部的に描画アイテムが生成されているだけで、描画として目に見える結果には現れません。

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 to 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;

		// 矩形の描画位置を記憶しておくリスト.
		List<Point> ListPoint = new List<Point>(0);

		int TheX;
		int TheY;

		// ななめノコギリ波の画像を取得する.
		public bool get_data_sawtooth_naname( byte [] data, int width, int height )
		{

			int w = width;
			int h = height;

			byte level;

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

				int adrs = w * j;

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

			}

			return true;

		}

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

		public MainWindow()
		{

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

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

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

			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 ) );
			}

			// 8bppのビットマップを生成する.
			PixelFormat pixfmt = PixelFormats.Indexed8;
			BitmapPalette palette = new BitmapPalette( list_color );
			Bmp000 = new WriteableBitmap( w, h, DPI_X, DPI_Y, pixfmt, palette );
			
			// 画像出力先のサイズを決定する.
			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 );

			// 親要素 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;

			// -------------------------------------
			// ここから描画の核心.
			// -------------------------------------

			// キャンバスからいったん描画アイテムをすべてクリアする.
			canvas000.Children.Clear();

			// 描画のペン幅.
			const float PEN_THICKNESS = 2.0f;

			// 矩形の幅と高さ.
			const double RECT_W = 32.0;
			const double RECT_H = 32.0;

			// 「矩形」と「線分」と「ポリライン」のペン色.
			Brush color_rect = Brushes.Lime;
			Brush color_line = Brushes.DeepPink;
			Brush color_poly = Brushes.White;

			// ポリラインを描画するための座標列.
			PointCollection points = new PointCollection();

			// 座標リストのデータぶんだけループする.
			for ( int k = 0; k < ListPoint.Count; k++ )
			{

				// マウスクリックした座標をリストから取り出す.
				double the_x = ListPoint[k].X;
				double the_y = ListPoint[k].Y;

				// 矩形のプロファイルを決定する.
				Rectangle rect = new Rectangle();
				rect.Width = RECT_W;
				rect.Height =RECT_H;
				rect.Stroke = color_rect;
				rect.StrokeThickness = PEN_THICKNESS;

				// クリックした位置のまんなかに矩形のまんなかを描画する.
				double plot_offset_rect_x = rect.Width * 0.5;
				double plot_offset_rect_y = rect.Height * 0.5;
				double plot_x = the_x + plot_offset_x - plot_offset_rect_x;
				double plot_y = the_y + plot_offset_y - plot_offset_rect_y;

				// 矩形の位置を設定する(まだキャンバスには反映されない).
				Canvas.SetLeft( rect, plot_x );
				Canvas.SetTop( rect, plot_y );

				// 線分( 対角線 LeftTop to RightBottom )のプロファイルを決定する.
				Line lineA = new Line();
				lineA.Stroke = color_line;
				lineA.StrokeThickness = PEN_THICKNESS;
				lineA.X1 = plot_x;           // LT.
				lineA.Y1 = plot_y;           // LT.
				lineA.X2 = plot_x + RECT_W;  // RB.
				lineA.Y2 = plot_y + RECT_H;  // RB.

				// 線分( 対角線 LeftBottom to RightTop )のプロファイルを決定する.
				Line lineB = new Line();
				lineB.Stroke = color_line;
				lineB.StrokeThickness = PEN_THICKNESS;
				lineB.X1 = plot_x;           // LB.
				lineB.Y1 = plot_y + RECT_H;  // LB. 
				lineB.X2 = plot_x + RECT_W;  // RT.
				lineB.Y2 = plot_y;           // RT.

				// 描画アイテム(矩形、線分LTtoRB、線分LBtoRT)を追加する.
				canvas000.Children.Add( rect );
				canvas000.Children.Add( lineA );
				canvas000.Children.Add( lineB );

				// ループを脱出してからポリラインを描画するための座標データ列.
				double plot_poly_x = the_x + plot_offset_x; 
				double plot_poly_y = the_y + plot_offset_y; 
				points.Add( new Point( plot_poly_x, plot_poly_y ));

			}

			// ポリラインのプロファイルを決定する.
			Polyline poly = new Polyline();
			poly.Stroke = color_poly;
			poly.StrokeThickness = PEN_THICKNESS;
			poly.Points = points;

			// 描画アイテム(ポリライン)を追加する.
			canvas000.Children.Add( poly );

			// ステータスバーのラベルに座標を表示する.
			String str000 = String.Format( "ListPoint.Count is {0}", ( ListPoint.Count ));
			String str001 = "左クリックで座標追加";
			String str002 = "右クリックで座標リセット";
			stbLabel000.Content = str000;
			stbLabel001.Content = str001;
			stbLabel002.Content = str002;

		}

		private void OnMouseDown(object sender, MouseEventArgs e)
		{

			MouseButtonState mbs = MouseButtonState.Pressed;

			// 右ボタンクリックかどうか判定する.
			if (( e.RightButton & mbs ) == mbs )
			{
				// 座標のデータをクリアする.
				ListPoint.Clear();

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

				// このハンドラからすぐに脱出する.
				return; // warning.
			}

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

			// 親要素 Grid000 の座標系から image000 のオフセット量を取得する.
			Point pnt = e.GetPosition( image000 );

			// Pointのままだとバウンダリチェックが面倒なので X, Y 座標に代入する.
			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;

			// クリック位置をリストに追加する.
			ListPoint.Add( new Point( TheX, TheY ));

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

		}

		private void menuApplicationQuit_Click( object sender, RoutedEventArgs e )
		{

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

		}

	}

}

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

下記の記事は、あらかじめ XAML で定義しておいた描画アイテムを、xaml.cs から位置を制御する方法で描画する方法を紹介しています。静的に描画アイテムを準備しておくという単純なやり方です。よろしければ御覧ください。

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

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