午後わてんのブログ

ベランダ菜園とWindows用アプリ作成(WPFとC#)

WPFとVB.NET、FillContainsWithDetailとGeometryを使って面と面の重なりを判定

 
エクセルのグループ化を真似したくていろいろ試している続き
 
マウスドラッグで四角形の範囲を作ってその範囲に重なったものを取得するっていうだいぶ前の方法
WPFVB.NET、エクセルのグループ化とグループ化解除を真似したい4 ( ソフトウェア ) - 午後わてんのブログ - Yahoo!ブログ
http://blogs.yahoo.co.jp/gogowaten/14203583.html
この時の方法だとコントロールを回転とか変形をさせた時にうまくいかないことがわかった(’・ω・‘)
 

f:id:gogowaten:20210129105908g:plain

以前の方法
RotateTransformで回転させた後だと四隅に重ねても取得できていない
これはRotateTransformで変形させても変わるのは見た目だけで中身は変わっていないのに、その中身と判定しているから
解決するには見た目通りの形を取得する必要があるかなと思って
 
なんとかできたのが今回の
イメージ 2
正方形の赤が元の形で
斜めになっている長方形は元の形から横拡大率2.0、縦拡大率0.7、右に30度回転
これをいろいろな選択範囲で判定している
下のステータスバーは判定結果を表示している
 
ググっていたら
VisualTreeHelper.HitTestっていうのを使うのもあるみたいだったけど難しくてわからなかった(小並感)CallBackってのがわからん
だから別の方法
 
前回まで判定に使ったのはRectクラス、つまり長方形で判定していた
回転させると菱型になるからRectは使えないので今回はGeometryクラス
 
GeometryクラスのFillContainsWithDetailメソッドを使ってコントロール同士の重なりの判定をしている
この便利なメソッドFillContainsWithDetail、これにたどり着くまでが長かった
ヒントになったのがこちら
片鱗懐古のブログ: wpf : UIElement.InputHitTestを試したら予想と違った動作
http://pieceofnostalgy.blogspot.jp/2011/11/wpf-uielementinputhittest.html
ありがとうございます!
 
 
デザイン画面とXAML
 
 
 

f:id:gogowaten:20191030125526p:plain

赤に塗ったBorderを2つ表示している
正方形のは目印用
拡大回転させた長方形のほうが目的のものになる
このふたつは見た目が違うだけで中身は同じ大きさ同じ位置
 
MainWindowのVBコード

f:id:gogowaten:20191030125537p:plain

Class MainWindow

    Private selectPath As New Path      '選択範囲用
    Private intersectPath As New Path   '重なった場所用
    Private syokiP As Point             '最初にクリックした場所
    Private IsDrag As Boolean           'マウスドラッグ移動中判定用
    Private bRedGeometry As PathGeometry '赤BorderのGeometry


    '表示している赤Borderの見た目上のGeometryを作成、ついでにPathで黒枠表示
    Private Sub AddGeometry()
        '見た目上の四隅の位置を取得
        Dim gt As GeneralTransform = bRed.TransformToVisual(canvas1)
        Dim p0 As Point = gt.Transform(New Point(0, 0)) '左上
        Dim p1 As Point = gt.Transform(New Point(bRed.Width, 0)) '右上
        Dim p2 As Point = gt.Transform(New Point(bRed.Width, bRed.Height)) '右下
        Dim p3 As Point = gt.Transform(New Point(0, bRed.Height)) '左下

        'PathFigure作成
        Dim pf As New PathFigure
        pf.IsClosed = True '線を閉じる
        pf.IsFilled = True '塗りつぶしするにはこれをTrue、さらにPathのFillに色指定
        'FillContainsWithDetailを使って重なり判定するときはTrueにする、PathのFillは無くてもOK

        '線の追加、順番は左上、右上、右下、左下、一筆書きならどうでもいい
        pf.StartPoint = p0
        pf.Segments.Add(New LineSegment(p1, True))
        pf.Segments.Add(New LineSegment(p2, True))
        pf.Segments.Add(New LineSegment(p3, True))

        Dim g As New PathGeometry()
        g.Figures.Add(pf)
        bRedGeometry = g '赤BorderのPathGeometry完成

        'Pathを使って目印用の黒枠表示
        'PathFigureからPathGeometryを作ってPathのDataに指定
        Dim kuroWaku As New Path With {.Stroke = Brushes.Black}
        kuroWaku.Data = g
        'kuroWaku.Fill = Brushes.Blue '塗りつぶし
        canvas1.Children.Add(kuroWaku)
    End Sub

    Private Sub MainWindow_Initialized(sender As Object, e As EventArgs) Handles Me.Initialized
        canvas1.Background = Brushes.Transparent 'マウスドラッグ移動で必要
        selectPath.Stroke = Brushes.Cyan '選択枠
        'intersectPath.Stroke = Brushes.Gold
        intersectPath.Fill = Brushes.Cyan '重なり判定

    End Sub


    Private Sub MainWindow_ContentRendered(sender As Object, e As EventArgs) Handles Me.ContentRendered
        Call AddGeometry() '赤BorderのGeometry作成
    End Sub


    'マウスドラッグ移動開始
    Private Sub canvas1_MouseLeftButtonDown(sender As Object, e As MouseButtonEventArgs) Handles canvas1.MouseLeftButtonDown
        '範囲選択枠表示開始
        syokiP = e.GetPosition(canvas1)
        canvas1.CaptureMouse()
        canvas1.Children.Add(selectPath)
        tbk1.Text = ""
        tbk2.Text = ""
        intersectPath.Data = Nothing
        canvas1.Children.Remove(intersectPath)

        IsDrag = True
    End Sub

    'マウスドラッグ移動中
    Private Sub canvas1_MouseMove(sender As Object, e As MouseEventArgs) Handles canvas1.MouseMove
        If IsDrag = False Then Return
        '最初にクリックした場所と今の場所の2点を使ってRectangleGeometry作成
        Dim rg As New RectangleGeometry(New Rect(syokiP, e.GetPosition(canvas1)))
        selectPath.Data = rg '選択枠のDataにする
    End Sub

    'マウスドラッグ移動終了時
    '選択範囲枠と赤Borderの見た目上のGeometryを比較
    '2つが重なった場所を水色で塗りつぶし
    Private Sub canvas1_MouseLeftButtonUp(sender As Object, e As MouseButtonEventArgs) Handles canvas1.MouseLeftButtonUp
        canvas1.ReleaseMouseCapture()
        canvas1.Children.Remove(selectPath)
        IsDrag = False

        If selectPath.Data Is Nothing Then Return

        '重なり判定する2つのGeometry
        Dim g1 As Geometry = selectPath.Data
        Dim g2 As Geometry = bRedGeometry
        '判定
        Dim iDetail As IntersectionDetail = g1.FillContainsWithDetail(g2)
        tbk1.Text = iDetail.ToString '判定結果表示
        'IntersectionDetailが
        'Empty            重なりなし
        'FullyContains    g2のすべてはg1の中に入っている
        'FullyInside      g1のすべてはg2の中に入っている
        'Intersects       一部が重なっている
        'つまりEmpty以外なら重なっている




        'ここから下は蛇足
        Dim ex As PathGeometry = Geometry.Combine(g1, g2, GeometryCombineMode.Exclude, Nothing)
        Dim int As PathGeometry = Geometry.Combine(g1, g2, GeometryCombineMode.Intersect, Nothing)
        Dim uni As PathGeometry = Geometry.Combine(g1, g2, GeometryCombineMode.Union, Nothing)
        Dim xo As PathGeometry = Geometry.Combine(g1, g2, GeometryCombineMode.Xor, Nothing)

        '重なった部分のPathGeometryの各Pointを取得して表示
        Dim pCount As Integer = 0
        Dim tPoint As String = ""
        For Each pff As PathFigure In int.Figures
            For Each pss As PolyLineSegment In pff.Segments
                For Each pos As Point In pss.Points
                    tPoint &= $"p{pCount}({pos:0})  "
                    pCount += 1
                Next
            Next
        Next
        tbk2.Text = tPoint 'ステータスバーに表示

        '重なった部分を水色で塗りつぶし
        intersectPath.Data = int
        canvas1.Children.Add(intersectPath)

        selectPath.Data = Nothing
    End Sub


End Class
 
 
FillContainsWithDetailメソッドは2つのGeometryを渡すと重なり具合を比較して結果を返してくれる
 
変形させた赤Borderの見た目通りの形をしたGeometryの作成のための4頂点を取得
イメージ 6
Geometryってのは順番が付いた点の集合みたいなものかなあ、順番に従って点を直線や曲線で繋いでいくと図形ができあがる感じ
なので見た目通りのGeometryを作るには各頂点座標が必要
各頂点は元の位置から移動している、それぞれどこに移動したか取得するには
TransformToVisualで得られるGeneralTransformを使う
 
赤Borderは
canvas1に表示している
Transformで変形させているので
39行目で赤Borderのcanvas1に対するGeneralTransformを取得して
40行目で元の左上の頂点(0,0)をGeneralTransformのTransformメソッドを使ってどこに移動しているか取得している
赤Borderがcanvas1上のどこに表示されていても左上の頂点がどこにあるのか取得するときは(0,0)を変形させればいいみたい
同じように他の3つの頂点も取得
 
 
イメージ 5
4隅の頂点座標がわかったらこれを使ってGeometryを作る、正確にはPathGeometryを作った、この辺はよくわかっていなくて
ジオメトリの概要
https://msdn.microsoft.com/ja-jp/library/ms751808(v=vs.110).aspx
ここ見ながら書いた
大事なのはPathFigureのIsClosedとIsFilledにはTrueを指定する
IsClosedは最初の点と最後の点を直線で繋いで図形を閉じるかどうか
IsFilledは図形の閉じた内側を塗りつぶすかどうかを指定しているみたい
とくにIsFilledはTrueにしないとFillContainsWithDetailで期待通りの判定が返ってこなかった
ここまでで赤Borderの見た目通りの形をしたGeometryは完成
 
目印用の黒枠表示
イメージ 7
完成したGeometryは本当に期待通りの形になっているのかの確認用
Pathを作ってそのDataにGeometryを指定して表示している
赤Borderの外側の黒枠がそれ
 
ここからcanvas1のマウスイベントを使った
マウスドラッグ移動で範囲選択用の枠表示
Handles canvas1.MouseLeftButtonDown
イメージ 8

83行目、マウスドラッグ移動の最初の点を記録

それ以降はステータスバー表示とかの初期化
 
Handles canvas1.MouseMove、マウス移動中
 
イメージ 9
移動開始地点と今の場所の2点を使ってRectを作成
Rectを使ってRectangleGeometryを作成
それを選択範囲枠用のselectPathのDataに指定
ってそのままだな、こんなふうにGeometryにはいくつかの種類があって
RectangleGeometryはRectから簡単に作ることができる
この時点で比較する2つのGeometryは完成しているけど、今回はマウス移動中に判定しない
 
Handles canvas1.MouseLeftButtonUp、左クリックを離した時
マウスドラッグ終了に判定
 
イメージ 10
108行目までは初期化とかしているだけで判定は111行目から
2つのGeometry
selectPathはマウスドラッグで作成した選択範囲用の四角枠、そのDataにはGeometryなのでこれをg1
bRedGeometryは赤BorderのGeometry、これをg2
114行目、ここでやっとFillContainsWithDetailを使って判定
Dim iDetail As IntersectionDetail = g1.FillContainsWithDetail(g2)
これで返ってくる判定結果の種類はだいたい以下の4つ
  • Empty             重なっている部分はない
  • FullyContains     g2のすべてはg1の中に入っている
  • FullyInside       g1のすべてはg2の中に入っている
  • Intersects       一部が重なっている
今回の目的は重なりの有無だから、Emptyかそれ以外がわかればいいことになる
つまりEmpty以外なら重なっている
 
これはg1、g2を入れ替えて
Dim iDetail As IntersectionDetail = g2.FillContainsWithDetail(g1)
ってしても今回の目的なら同じかも
 
 
一時停止して中を見てみる
赤BorderのGeometry作成時
 
イメージ 11
 
 
マウスドラッグで範囲指定した時
 
イメージ 12
この時
 
イメージ 13
こうしてみると同じGeometryでもRectangleGeometryとPathGeometryでは
ずいぶん感じが違う、にもかかわらずしっかり判定してくれるFillContainsWithDetailメソッドはすごいなあ
で、この結果は
 
 
イメージ 14
こうなる
Intersectsは一部分が重なっていた判定
 
 
 
 
 
イメージ 15
選択範囲のすべてが赤Borderの中に入っているときは
FullyInside
 
イメージ 16
こんな感じで選択範囲の中に赤Borderが全て入った時は
 
 
イメージ 17
FullyContains
 
 
 
イメージ 18
全く重ならなかった時は
 
 
 
イメージ 19
Empty
 
イメージ 20
なんかこれだけで楽しい
 
ステータスバーに表示しているp0(n,n)とかは、重なり部分を水色で塗りつぶしに使っているGeometryの各頂点座標
これの処理は
 
 
 
 

f:id:gogowaten:20191030125602p:plain

この辺
GeometryクラスのCombineメソッドを使うと2つのGeometryを判定(合成?)した結果のPathGeometryを返してくれる
今回使っているのが2つが重なった部分だけの形になるPathGeometryを返してくれるGeometryCombineMode.Intersect、128行目
これで得たPathGeometryをIntersectPathのDataにして水色で塗りつぶし表示
PathGeometryの中に入っている各頂点座標をp0からの連番で表示
これは
方法 : 結合したジオメトリを作成する
https://msdn.microsoft.com/ja-jp/library/ms746682(v=vs.110).aspx
ここを参照
 
 
 
今回のコード一式
 
 
関連記事
前回
WPFVB.NET、エクセルのグループ化とグループ化解除を真似したい6 ( ソフトウェア ) - 午後わてんのブログ - Yahoo!ブログ
http://blogs.yahoo.co.jp/gogowaten/14215386.html
 
 
5年後はC#