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

動的に描画アイテムを作成して描画を実施する場合は、OnRender の中で常に canvas.Children.Clear() を実施し、必要な描画アイテムを canvas.Children.Add() で追加するのが定石です。これについては下記の記事をご参照ください。

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

PowerPoint や Visio のようなドローソフトを作成する場合は、キャンバスに対して描画アイテムを動的に作成し、動的に配置してやる必要があります。その実践的な方法を紹…

しかし、XAMLであらかじめ準備しておいた静的な描画アイテムと動的に作成した描画アイテムを同時に使う場合はどうなるでしょうか。

canvas.Children.Clear() をしてしまうと、残念ながら静的な描画アイテムと動的な描画アイテム、どちらもクリアしてしまいます。静的な描画アイテムはクリアせずに保持しておいてほしいですね。

たとえば下記に示すウインドウでは、描画の目盛り線は静的な描画アイテムです。マウスでクリックした場所に配置した矩形マーカは動的作成した描画アイテムです。

これらを同時に扱うにはどうすればよいでしょうか。どちらも必ず OnRender() の中で扱わなければなりません。

静的な描画アイテムと動的な描画アイテムを同時に扱うアプリケーション

こういった場合は、キャンバスを2個用意して、それらを重ねるというのが王道です。

ひとつは、動的に作成した描画アイテム用のキャンバスです、もうひとつは、あらかじめXAMLで準備しておいた静的な描画アイテム用のキャンバスです。これを重ねます。

下記の XAML では、40行目が前者にあたり、44行目が後者にあたります。下記のように命名しておきます。

・動的描画アイテム用のキャンバス canvas_dynamic
・静的描画アイテム用のキャンバス canvas_static

<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" MouseMove="OnMouseMove">

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

                    <Canvas x:Name="canvas_dynamic" Background="Transparent">
                        <!-- 通常ならここに描画アイテムを定義するが動的に生成するため何も書かない. -->
                    </Canvas>
 
                    <Canvas x:Name="canvas_static" Background="Transparent">
                        <Line x:Name="line_horz_001" Stroke="White" StrokeThickness="2"/>
                        <Line x:Name="line_horz_002" Stroke="White" StrokeThickness="2"/>
                        <Line x:Name="line_horz_003" Stroke="White" StrokeThickness="2"/>

                        <Line x:Name="line_vert_001" Stroke="Aqua" StrokeThickness="2"/>
                        <Line x:Name="line_vert_002" Stroke="Aqua" StrokeThickness="2"/>
                        <Line x:Name="line_vert_003" Stroke="Aqua" StrokeThickness="2"/>
                    </Canvas>
 
                </Grid>
            </ScrollViewer>
        </Border>

    </DockPanel>


</Window>

つぎに下記の MainWindow.xaml.cs のコードについて解説します。

110~231行目が OnRender() 描画のコードです。しつこいようですが、ここ以外では描画に関するコードを書いてはいけません。

128~165行目が、XAMLであらかじめ準備しておいた描画アイテムを canvas_static に配置するコードです。

168~221行目が、描画アイテムを動的に作成し、canvas_dynamic に配置するコードです。

173行目で、いったん描画アイテムをクリアします。canvas_dynamic.Children.Clear(); でわかるように、canvas_dynamic に配置されている描画アイテムのみクリアします。canvas_static のほうに配置されている描画アイテムには影響が及びません。

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

		// ななめノコギリ波の画像を取得する.
		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;

			// ウインドウのタイトル.
			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;

			// -------------------------------------------------------
			// こちらはあらかじめ XAML で描画アイテムを準備している.
			// -------------------------------------------------------

			int div_w = w >> 2;
			int div_h = h >> 2;

			// -------------------------------------------
			line_horz_001.X1 = plot_offset_x + 0;
			line_horz_001.Y1 = plot_offset_y + ( div_h * 1 );
			line_horz_001.X2 = plot_offset_x + w;
			line_horz_001.Y2 = line_horz_001.Y1;

			line_horz_002.X1 = plot_offset_x + 0;
			line_horz_002.Y1 = plot_offset_y + ( div_h * 2 );
			line_horz_002.X2 = plot_offset_x + w;
			line_horz_002.Y2 = line_horz_002.Y1;

			line_horz_003.X1 = plot_offset_x + 0;
			line_horz_003.Y1 = plot_offset_y + ( div_h * 3 );
			line_horz_003.X2 = plot_offset_x + w;
			line_horz_003.Y2 = line_horz_003.Y1;
			// -------------------------------------------
			line_vert_001.X1 = plot_offset_x + ( div_w * 1 );
			line_vert_001.Y1 = plot_offset_y + 0;
			line_vert_001.X2 = line_vert_001.X1;
			line_vert_001.Y2 = plot_offset_y + h;

			line_vert_002.X1 = plot_offset_x + ( div_h * 2 );
			line_vert_002.Y1 = plot_offset_y + 0;
			line_vert_002.X2 = line_vert_002.X1;
			line_vert_002.Y2 = plot_offset_y + h;

			line_vert_003.X1 = plot_offset_x + ( div_h * 3 );
			line_vert_003.Y1 = plot_offset_y + 0;
			line_vert_003.X2 = line_vert_003.X1;
			line_vert_003.Y2 = plot_offset_y + h;
			// -------------------------------------------


			// -------------------------------------------------------
			// ここからは動的に描画アイテムを生成追加する.
			// -------------------------------------------------------

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

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

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

			// 座標リストのデータぶんだけループする.
			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.StrokeThickness = PEN_THICKNESS;

				Brush c;

				// 3回ごとに矩形の色を変える.
				int mod = k % 3;	
				if      ( mod == 0 ) { c = Brushes.Blue; }
				else if ( mod == 1 ) { c = Brushes.Lime; }
				else                 { c = Brushes.Red; }

				// 矩形の枠線と中身の色を指定する.
				rect.Stroke = c;
				rect.Fill = c;

				// クリックした位置のまんなかに矩形のまんなかを描画する.
				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 );

				// 描画アイテムを追加する.
				canvas_dynamic.Children.Add( rect );

			}

			// ステータスバーのラベルに座標を表示する.
			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; }

			// ここでリストに追加される座標は画像領域の内側であることが保証される.
			ListPoint.Add( new Point( tmp_x, tmp_y ));

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

		}

		private void OnMouseMove(object sender, MouseEventArgs e)
		{

			MouseButtonState mbs = MouseButtonState.Pressed;

			if (( e.LeftButton & mbs ) == mbs )
			{
				this.OnMouseDown( sender, e );
			}

		}

		private void menuApplicationQuit_Click( object sender, RoutedEventArgs e )
		{

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

		}

	}

}

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