※本記事は下記のエントリから始まる連載記事となります。
.NET5.0/C#9.0でオートシェイプ風図形描画ライブラリを作ろう!(Chapter0)
Chapter3 ドラッグ操作で図形のサイズを変更してみよう(その2)
前回 ドラッグ操作で図形のサイズを変更してみよう(その1) からの続きです。
ソースコード
Capter3の内容は下記ブランチにて実装されています。実装の詳細はこちらをご確認ください。
https://github.com/pierre3/CoreShape/tree/blog/capter3
前回のおさらい
前回は図形のサイズ変更用のつまみ「リサイズハンドル」を作成して、図形のサイズ変更をドラッグ操作で行えるようにしました。
しかし、まだ下記のような問題が残っています。
- 図形が選択状態の時のみリサイズハンドルを表示したいが、常に表示されている
- ドラッグしているハンドルの反対側の境界を越えてドラッグするとBoundsの幅高さがマイナスになる。
その状態になると当たり判定が正常に動作しない
今回はこれらの問題を解決していきたいと思います。
選択した図形のみにリサイズハンドルを表示しよう
- 図形が選択状態の時のみリサイズハンドルを表示したいが、常に表示されている
の問題ですが、こちらはIShapeに選択状態か否かを示すプロパティを追加することで対応します。
public interface IShape
{
bool IsSelected { get; set; }
void Draw(IGraphics g);
HitResult HitTest(Point p);
void Drag(Point oldPointer, Point currentPointer);
}
としたいところなのですが、その前にIShape
の実装を見直したいと思います。
IShape
の機能を分解する
前回、リサイズハンドルもIShape
を実装するようにしましたが、こちらは選択状態か否かのパラメータを持つ必要がありません。
リサイズハンドルに必要なのは今のところDraw()
メソッドとHitTest()
メソッドのみで、IsSelected
プロパティやDrag()
メソッドは不要です。(前回、Drag()
メソッドはNotImplementException
をThrowする実装としていました。)
そこで、IShape
インターフェースを分解して利用するもののみを実装できるようにします。
//描画可能なオブジェクトのインターフェース
public interface IDrawable
{
void Draw(IGraphics g);
}
//ドラッグ可能なオブジェクトのインターフェース
public interface IDraggable
{
void Drag(Point oldPointer, Point currentPointer);
}
//当たり判定チェック機能を提供するインターフェース
public interface IHitTest
{
HitResult HitTest(Point p);
}
そして、IShape
は上記全て+IsSelected
プロパティを実装します。
public interface IShape : IDrawable, IDraggable, IHitTest
{
bool IsSelected { get; set; }
}
一方、リサイズハンドルはIShape
ではなく、IDrawable
とIHitTest
のみを実装するように変更します。
public abstract class ResizeHandleBase : IDrawable, IHitTest
{
//...
}
選択状態の時のみリサイズハンドルを表示する
それでは、RectangleShape
にIsSelected
プロパティを追加してみましょう。
追加したら、IsSelected
がTrueの場合の時のみリサイズハンドルの HitTest()
とDraw()
を実施するように変更します。
public class RectangleShape : IShape
{
public bool IsSelected { get; set; }
//(省略)...
public virtual HitResult HitTest(Point p)
{
if (IsSelected) //選択状態の場合のみ
{
//リサイズハンドルの当たり判定を実施
var hitResult = ResizeHandles.HitTest(p);
if (hitResult is not HitResult.None)
{
return hitResult;
}
}
return HitTestStrategy.HitTest(p, this) ? HitResult.Body : HitResult.None;
}
public virtual void Draw(IGraphics g)
{
if (Fill is not null)
{
g.FillRectangle(Bounds, Fill);
}
if (Stroke is not null)
{
g.DrawRectangle(Bounds, Stroke);
}
if (IsSelected) //選択状態の場合のみ
{
//選択枠とリサイズハンドルを描画
if (Stroke is null)
{
g.DrawRectangle(Bounds, new Stroke(Color.Black, 1));
}
ResizeHandles.Draw(g);
}
}
}
※リサイズハンドルに加えて図形に外接する四角形(=Bounds
)を選択枠として表示します。
ただし、RectangleShape
の場合は図形の輪郭と一致するため、輪郭を描画しない(Strokeがnull)の場合のみ描画するようにしています。
後はサンプルアプリ(SampleWPF)側でIsSelectedプロパティを設定する処理を追加するだけですね。
サンプルアプリ側の実装
SkElementにMouseDownイベントを追加して、マウスの左ボタンをクリックした際にマウスカーソルにヒットしている図形(ActiveShape)のみを選択状態(IsSelected=true)とします。
[Mainwindow.xml]
...
<Grid>
<skiaSharp:SKElement x:Name="skElement"
PaintSurface="sKElement_PaintSurface"
MouseMove="sKElement_MouseMove"
MouseDown="skElement_MouseDown" />
</Grid>
public partial class MainWindow : Window
{
//....
private void skElement_MouseDown(object sender, MouseButtonEventArgs e)
{
if (e.LeftButton != MouseButtonState.Pressed)
{
return;
}
//ActiveShapeだけTrue、それ以外はFalse
foreach (var shape in shapes)
{
shape.IsSelected = shape == activeShape;
}
skElement.InvalidateVisual();
}
動作確認
これで図形の選択処理の実装は完了しました。アプリを実行して確認してみましょう!
うん、いい感じですね。
ドラック終了時の処理を追加して後処理を実施しよう
次に、下記問題について考えましょう。
- ドラッグしているハンドルの反対側の境界を越えてドラッグするとBoundsの幅高さがマイナスになる。
その状態になると当たり判定が正常に動作しない
実際に動きを見てみると、下記の問題があることがわかります。
- 角リサイズハンドルでマウスカーソルの矢印の向きがおかしい。
- 図形内部にマウスカーソルが入っていてもカーソルの形状が変化しない。
- 前者の問題は、幅、高さが反転したことでリサイズハンドルの位置関係が入れ替わってしまうのが原因です。
- 後者の問題は、当たり判定のロジックが、幅、高さがマイナスとなった場合を考慮していないことが原因です。
どちらも、幅・高さがマイナスにならなければ問題は発生しません。また、マイナスになった場合でも図形自体の描画には影響がありません。
ですので、ドラッグ処理を終了した時点で、マイナスになった幅または高さがプラスになるように座標を入れ替えてあげればよさそうです。
Drop()
処理で座標を補正する
下記の処理を追加して幅・高さが常にプラスの値となるように調整するようにします。
-
IDraggable
インターフェースにドラッグ終了時に実行する処理Drop()
メソッドを追加 -
Drop()
実行時に幅・高さがマイナスだった場合、(見た目はそのままで)幅高さがプラスの値になるようにBoundsの座標を補正する
public interface IDraggable
{
void Drag(Point oldPointer, Point currentPointer);
void Drop();
}
public class RectangleShape : IShape
{
//...
public void Drop()
{
var (left, top, width, height) = (Bounds.Left, Bounds.Top, Bounds.Width, Bounds.Height);
//幅がマイナスの場合
if (Bounds.Width < 0)
{
//左右の座標を入れ替えて、幅の符号(-)を取る
left = Bounds.Right;
width = Math.Abs(Bounds.Width);
}
//高さがマイナスの場合
if (Bounds.Height < 0)
{
//上下の座標を入れ替えて、高さの符号(-)を取る
top = Bounds.Bottom;
height = Math.Abs(Bounds.Height);
}
SetBounds(new Rectangle(left, top, width, height));
}
}
ちなみにSetBounds()
メソッドではBoundsに値を設定後、リサイズハンドルの座標をBoundsに合わせて再設定しています。ここでリサイズハンドルの位置関係も正常に戻るはずです。
protected void SetBounds(Rectangle bounds)
{
Bounds = bounds;
ResizeHandles.SetLocation(bounds);
}
これでCoreShape側の実装は完了です。サンプルアプリ側でDrop()
メソッドを実行するように変更して確認してみましょう!
サンプルアプリ側の実装
SkElementにMouseUpイベントを追加して、ドラッグしていた図形(ActiveShape)のDrop()
メソッドを呼ぶだけです。
[Mainwindow.xml]
...
<Grid>
<skiaSharp:SKElement x:Name="skElement"
PaintSurface="sKElement_PaintSurface"
MouseMove="sKElement_MouseMove"
MouseDown="skElement_MouseDown"
MouseUp="skElement_MouseUp" />
</Grid>
public partial class MainWindow : Window
{
//....
private void skElement_MouseUp(object sender, MouseButtonEventArgs e)
{
if (e.LeftButton != MouseButtonState.Released)
{
return;
}
if (activeShape is not null)
{
activeShape.Drop();
}
}
動作確認
では、実行してみましょう。
いい感じです。
マウスカーソルの矢印の向きが入れ替わることがなくなりました。当たり判定もできていますね。
次回
次回は、サンプルアプリ(SampleWPF)側の実装について考えてみたいと思います。