.NET MAUI Blazorでマウスイベント・タップイベントのどちらでも利用できるお絵描き機能を紹介していきます。
当記事ではプロジェクト作成方法から説明していきますので、手順通りにプロジェクトを作成し動作を確認してみてください。
紹介環境
当記事は以下の環境で作成しています。
- Visual Studio 2022
- .Net 8
サンプルソースのプロジェクトの準備
サンプルソースのプロジェクトを紹介していきます。サンプルソースをコピーして動作確認したい場合、利用してください。
環境はWindows版Visual Studio 2022になります。
手順① プロジェクトの準備
プロジェクトの準備をしていきます。
Visual Studio 2022を起動すると以下の画面が表示されますので、「新規プロジェクトの作成」を選択します。
次に作成するプロジェクトの種類を選択していきます。以下のように「新しいプロジェクトの作成」画面から「.NET MAUI Blazor アプリ」を選択して「次へ」を選択します。
次にプロジェクト名を決めていきます。以下のようにプロジェクト名を入力し「次へ」を選択していきます。サンプルソースではプロジェクト名を「SampleDrawing」にしています。
次にフレームワークを決めていきます。以下のようにフレームワークを選択し「作成」を選択していきます。サンプルソースは「.NET 8.0」で作成していきます。
手順② 不要なフォルダ・ファイルを削除
当記事に不要なフォルダ・ファイルを削除していきます。以下のように赤枠のフォルダ・ファイルが不要なので削除していきます。
手順➂ 必要なフォルダ・ファイルを追加
必要なフォルダ・ファイルを追加していきます。以下のように赤枠のフォルダ・ファイルを追加していきます。
JavaScriptの追加には工夫が必要になります。手順は以下で紹介しています。
手順➃ その他ファイルの修正
今回の説明で利用しないファイルを紹介していきます。そのまま置き換えてください。
「wwwroot\css\app.css」を以下のようにします。
* {
box-sizing: border-box;
}
body {
margin: 0;
background: #ccc;
}
「wwwroot\index.html」を以下のようにします。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" />
<title>SampleDrawing</title>
<base href="/" />
<link href="css/app.css" rel="stylesheet" />
<link href="SampleDrawing.styles.css" rel="stylesheet" />
</head>
<body>
<div id="app">Loading...</div>
<script src="_framework/blazor.webview.js" autostart="false"></script>
</body>
</html>
「Components\Pages\Index.razor.css」を以下のようにします。
#main {
margin: 15px;
height: calc(100vh - 30px);
}
#canvas_contents {
height: 100%;
}
「Components\Layout\MainLayout.razor」を以下のようにします。
@inherits LayoutComponentBase
<div class="page">
<main>
@Body
</main>
</div>
プロジェクトの準備は以上になります。お絵描き機能の説明に入ります。
画面読み込み時にイベント付与させる方法
お絵描き機能を実現するためにcanvasタグにイベント付与していきます。
イベント付与は画面読み込み時に行う必要があるため、次のように実装していきます。
次の例では画面読み込み時にJavaScriptのInitializedメソッドが実行されます。つまり、JavaScriptのInitializedメソッドにイベント付与を実装していきます。
@implements IAsyncDisposable
@inject IJSRuntime JS
@page "/"
<div id="main">
<div id="canvas_contents">
<canvas id="canvas"></canvas>
</div>
</div>
@code {
private IJSObjectReference module;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
module = await JS.InvokeAsync<IJSObjectReference>("import", "./js/index.js");
// ここでイベント付与を行う。
await module.InvokeVoidAsync("Initialized");
}
}
async ValueTask IAsyncDisposable.DisposeAsync()
{
if (module is not null)
{
await module.DisposeAsync();
}
}
}
イベント付与の詳細は、以下で紹介しています。
お絵描き機能について
お絵描き機能は次のようになります。部分的に説明していきます。
export function Initialized() {
const canvasContentsElem = document.getElementById("canvas_contents");
const drawPosition = { x: null, y: null };
let isDrag = false;
let canvasElem = document.querySelector('#canvas');
const context = canvasElem.getContext('2d');
// canvasをアプリのサイズ変更
resizeCanvas();
// マウスイベント(PC用)
canvasElem.addEventListener("mousedown", drawStart);
canvasElem.addEventListener("mousemove", (event) => {
draw(event.offsetX, event.offsetY);
});
canvasElem.addEventListener("mouseup", drawEnd);
// タッチイベント(スマートフォン用)
canvasElem.addEventListener("touchstart", drawStart);
canvasElem.addEventListener("touchmove", (event) => {
const rect = event.target.getBoundingClientRect();
const offsetX = (event.touches[0].clientX - window.pageXOffset - rect.left);
const offsetY = (event.touches[0].clientY - window.pageYOffset - rect.top);
event.preventDefault();
draw(offsetX, offsetY);
});
canvasElem.addEventListener("touchend", drawEnd);
/** お絵描き開始 */
function drawStart() {
context.beginPath();
isDrag = true;
};
/**
* お絵描き機能
* @param {any} x
* @param {any} y
* @returns
*/
function draw(x, y) {
if (!isDrag) {
return;
}
context.lineWidth = 5;
context.strokeStyle = '#000';
if (drawPosition.x === null || drawPosition.y === null) {
context.moveTo(x, y);
} else {
context.moveTo(drawPosition.x, drawPosition.y);
}
context.lineTo(x, y);
context.stroke();
drawPosition.x = x;
drawPosition.y = y;
}
/** お絵描き終了 */
function drawEnd() {
context.closePath();
isDrag = false;
drawPosition.x = null;
drawPosition.y = null;
};
/** canvasをアプリのサイズ変更する */
function resizeCanvas() {
// アプリのサイズに合わせる
canvasElem.width = canvasContentsElem.clientWidth;
canvasElem.height = canvasContentsElem.clientHeight;
// 背景色設定
context.beginPath();
context.fillStyle = '#ffffff';
context.fillRect(0, 0, canvas.width, canvas.height);
}
/*
* 画面をリサイズしたときにcanvasのサイズを調整します。
* リサイズ時にcanvasの内容がリセットされるので不要な場合は削除してください。
*/
window.addEventListener("resize", () => {
resizeCanvas();
});
}
canvasタグのサイズについて
canvasタグはCSSで大きさを変更すると拡大・縮小が行われます。そのため、指定された位置に線を引くとずれてしまいます。そのずれを防ぐためにcanvasタグのwidth・heightにサイズを指定する必要あります。
今回はcanvasタグを囲っているID:mainの要素に大きさを合わせています。
// アプリのサイズに合わせる
canvasElem.width = canvasContentsElem.clientWidth;
canvasElem.height = canvasContentsElem.clientHeight;
マウスイベント・タップイベントについて
マウスイベント・タップイベントは、次のように付与します。
基本的に動作は同じなるため、メソッドを利用してまとめています。
しかし、マウスとタップで座標の取得方法が変わるため、「mousemove」「touchmove」で内容が変わっています。
// マウスイベント(PC用)
canvasElem.addEventListener("mousedown", drawStart);
canvasElem.addEventListener("mousemove", (event) => {
draw(event.offsetX, event.offsetY);
});
canvasElem.addEventListener("mouseup", drawEnd);
// タッチイベント(スマートフォン用)
canvasElem.addEventListener("touchstart", drawStart);
canvasElem.addEventListener("touchmove", (event) => {
const rect = event.target.getBoundingClientRect();
const offsetX = (event.touches[0].clientX - window.pageXOffset - rect.left);
const offsetY = (event.touches[0].clientY - window.pageYOffset - rect.top);
event.preventDefault();
draw(offsetX, offsetY);
});
canvasElem.addEventListener("touchend", drawEnd);
動作イメージ
以下がサンプルソースをWindowsで動作させたイメージになります。
おまけ:画像の保存機能を追加してみる
お絵描き機能が作成できたので、保存できるようにしていきます。
手順① 画像保存機能を追加します
今回追加する画像保存は、以下の記事で紹介している内容を利用します。
クラスの配置方法は、以下のようになります。
手順② 画像保存機能のDIを登録します
手順①で追加した画像保存機能のDIを以下のように登録していきます。
using Microsoft.Extensions.Logging;
using SampleDrawing.DI;
#if WINDOWS
using SampleDrawing.Platforms.Windows.DI;
#elif ANDROID
using SampleDrawing.Platforms.Android.DI;
#elif IOS
using SampleDrawing.Platforms.iOS.DI;
#elif MACCATALYST
using SampleDrawing.Platforms.MacCatalyst.DI;
#endif
namespace SampleDrawing
{
public static class MauiProgram
{
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder
.UseMauiApp<App>()
.ConfigureFonts(fonts =>
{
fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
});
builder.Services.AddMauiBlazorWebView();
#if DEBUG
builder.Services.AddBlazorWebViewDeveloperTools();
builder.Logging.AddDebug();
#endif
// サービスの登録
builder.Services.AddSingleton<IImageWriter, ImageWriter>();
return builder.Build();
}
}
}
手順➂「Index.razor」に画像保存機能を追加します
「Index.razor」に画像保存ボタンと画像保存ボタンが選択されたときに画像保存される機能を追加していきます。
追加した内容が以下のようになります。詳細はコメントに記載されています。
@implements IAsyncDisposable
@inject IJSRuntime JS
@* 追加:IImageWriterを利用するため *@
@inject IImageWriter imageWriterService
@page "/"
@using SampleDrawing.DI;
<div id="main">
<div>
<!-- 追加:ファイル名を入力するためのテキストボックス -->
<input @bind-value="Name" />
<!-- 追加:画像保存するために保存ボタン -->
<button @onclick="Save">保存</button>
</div>
<div id="canvas_contents">
<canvas id="canvas"></canvas>
</div>
</div>
@code {
// 追加:ファイル名
/// <summary>
/// ファイル名を設定します。
/// </summary>
public string Name { get; set; }
private IJSObjectReference module;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
module = await JS.InvokeAsync<IJSObjectReference>("import", "./js/index.js");
// ここでイベント付与を行う。
await module.InvokeVoidAsync("Initialized");
}
}
// 追加:画像保存機能
/// <summary>
/// 画像保存します。
/// </summary>
/// <returns></returns>
public async Task Save()
{
if (string.IsNullOrEmpty(Name))
{
await Application.Current.MainPage.DisplayAlert("警告", "ファイル名を指定してください。", "OK");
return;
}
if (!await Application.Current.MainPage.DisplayAlert("確認", "保存しますか?", "はい", "いいえ"))
{
return;
}
// canvasの内容をBase64に変換します。
string imageBase64 = await module.InvokeAsync<string>("GetBase64");
byte[] imageBytes = Convert.FromBase64String(imageBase64);
try
{
// IImageWriterを利用して、画像保存します。
await imageWriterService.WriteAsync(Name, imageBytes);
await Application.Current.MainPage.DisplayAlert("完了", "保存しました。", "OK");
}
catch (Exception)
{
await Application.Current.MainPage.DisplayAlert("エラー", "保存失敗しました。", "OK");
}
}
async ValueTask IAsyncDisposable.DisposeAsync()
{
if (module is not null)
{
await module.DisposeAsync();
}
}
}
手順➃ canvasをBase64に変換する機能を追加します
canvasをBase64に変換する機能を「index.js」に追加していきます。この機能は「Index.razor」から呼び出されます。
/**
* canvasをBase64に変換します。
* @returns
*/
export function GetBase64() {
const canvasElem = document.querySelector('#canvas');
const base64 = canvasElem.toDataURL('image/png');
return base64.replace(/^.*,/, '');
}
手順⑤ 「Index.razor.css」を修正します
手順➂で画像保存ボタンが追加され見た目が変わったので、「Index.razor.css」を以下のように修正していきます。
#main {
margin: 15px;
height: calc(100vh - 30px);
display: grid;
grid-template-rows: max-content 1fr;
row-gap: 15px;
}
#canvas_contents {
overflow: hidden;
}
input {
padding: 7.5px 15px;
border-radius: 7.5px;
border: 1px solid #555;
}
button {
padding: 7.5px 15px;
background: #5555ff;
border-radius: 7.5px;
border: none;
color: #fff;
}
実装はこれで完了になります。
動作イメージ(Windows)
Windowsで動作したイメージは以下のようになります。
動作イメージ(Android)
Androidで動作したイメージは以下のようになります。
おわりに
.NET MAUI Blazorでお絵描き機能を作成しましたが、基本的にJavaScriptで完結してます。そのため、Web開発をしてる方は、Webアプリと同じように実装できたと思います。
今回紹介したお絵描き機能に様々な機能を追加してアプリに組み込んでみてください。