複雑な曲線とイベントハンドラ

解決


三戸  2006-03-17 02:07:34  No: 130670

OS:Windows XP Home
開発環境:Visual Basic 6.0

CanvasやIllustrator等のソフトで、画面上に自分で描いた線や曲線を自分でクリックして選択し、修正する というような処理があります。
あの様な処理はどのようにして実現しているのでしょうか?

VB6でPictureboxにLineメソッドをで描いた線をいくらクリックしてもPictureboxのクリックイベントが発生して線に対してイベントを発生させることは出来ません。

予想としては、線や曲線や円や長方形というような「オブジェクト」を画面上に配置させることで実現していると思うのですが、VB6でそれと似たことは出来ますか?
シェイプを使えば円と長方形は出来ると思うのですが、斜めの線や複雑な一本の曲線(細かい直線の集合)はシェイプでは難しいと思います。
オブジェクト指向?といわれているVisual Basic .netはどうですか?

どなたかよろしくお願いします。


K.J.K.  2006-03-17 03:13:45  No: 130671

そういう図形のメタファイルを作って、それをImageコントロールの
Pictureプロパティに設定するとか。
# でも、それなりに面倒だし、形状を変化させるとなると更に面倒。


めだか  2006-03-17 07:36:21  No: 130672

>VB6でPictureboxにLineメソッドをで描いた線をいくらクリックしてもPictureboxのクリックイベントが発生して線に対してイベントを発生させることは出来ません

PictureボックスのMouseDownイベントやMouseUpイベント等で何の図形が
クリックされたかを判定する処理を自分で書いてやればよいだけのことです。
各図形の座標をクラスか配列かで覚えていれば済むことですね。


三戸  2006-03-17 11:28:46  No: 130673

>K.J.K.さん
御返答ありがとうございます。
メタファイルを使う方法を全く知りませんでした。
しかし、実際に使ったことは無いので、一度勉強して挑戦してみます。

>めだかさん
>各図形の座標をクラスか配列かで覚えていれば済むことですね。
御返答ありがとうございます。
確かにその通りなのですが、描こうとしている曲線が、百を超える座標から
成り立っていて、それが200本ほど有るのでそれぞれに総当たりで
線分(x1,y1)-(x2,y2)の間にクリックした座標があるかを調べるのは
ナカナカ大変そうだったので何か手を抜く方法は無いものかと思って
質問しました。


我龍院忠太  2006-03-17 18:54:12  No: 130674

線の描画はX1,X2,Y1,Y2,Colorを配列に仕込めば、再描画は出来ますね。
問題はどのように線上がクリックされたか識別する方法ですが、
APIのGetPixelを使えばクリックされた点の色がわかります、
そこで線の色に番号を仕込んで、クリックした途端に一瞬だけ色を変えます。
そしてその色をGetPixelで取得して・・・後はそっと元の色に戻しておく。
200本位の線だと殆どちらつきは判らないものです。

およそのサンプルですが、Form1にタイマーを一つ置きます。
Private Declare Function GetPixel Lib "gdi32" _
    (ByVal hdc As Long, ByVal X As Long, ByVal Y As Long) As Long
Private Sub Form_Load()
    Me.BackColor = vbWhite
    Timer1.Interval = 100
    Timer1.Enabled = True
End Sub
Sub DrawColor(flg As Boolean)
    Dim i As Integer
    '実際の場合は配列にX1,X2,Y1,Y2,Colorを仕込む
    ScaleMode = 3                        ' ScaleMode をピクセルに設定します。
    DrawWidth = 6                        ' DrawWidth を設定します。
    For i = 1 To 255
       If flg Then
         ForeColor = QBColor(i Mod 15)   'とりあえず線の色
       Else
         ForeColor = RGB(255, 255, i)    '線の色に番号を仕込む
       End If
       Line (10, i * 10)-(1000, i * 10)
   Next i
End Sub
Private Sub Form_MouseDown(Button As Integer, _
    Shift As Integer, X As Single, Y As Single)
    Dim lngPcolor As Long
    Dim strColor As String
    If GetPixel(Form1.hdc, X, Y) = Val(&HFFFFFF) Then Exit Sub '線以外をクリックした
    DrawColor (False)
    'DoEvents
    lngPcolor = GetPixel(Form1.hdc, X, Y)
    strColor = Right("000000" & Hex(lngPcolor), 6)
    DrawColor (True)
    MsgBox (Val("&h" & Left(strColor, 2)) & "番目のラインがクリックされました。")
End Sub
Private Sub Timer1_Timer()
    '1番最初だけ
    Timer1.Enabled = False
    DrawColor (True)
End Sub


めだか  2006-03-17 21:19:28  No: 130675

直線の始点と終点がわかっているんだから簡単な直線(y=ax+b)の式で
クリックされた点が線上にあるかどうかは分かると思いますが。
それを200回ほど繰り返すのが難しいでしょうか?


我龍院忠太  2006-03-17 21:54:52  No: 130676

>直線の始点と終点がわかっているんだから簡単な直線(y=ax+b)の式で
>クリックされた点が線上にあるかどうかは分かると思いますが。
>それを200回ほど繰り返すのが難しいでしょうか?

線の幅はどのように処理しましょうか?
始点と終点の形は?
斜めの線の丸め誤差は?
結構ムズだと思いますが。


めだか  2006-03-17 23:56:07  No: 130677

そうでしょうかね?
たいていのソフトは直線そのものじゃなくある程度の幅を持たせていると
思いますのでその線を囲む4つの式をだせばいいだけじゃないかと思いますが。


めだか  2006-03-18 00:04:11  No: 130678

>線を囲む4つの式をだせばいいだけじゃないかと
こんなことしなくてもクリックした点と線との直交距離をだせばいいですね。


我龍院忠太  2006-03-18 01:07:44  No: 130679

めだかさん、大雑把な話をされてますか?
大雑把で良いなら色々と方法は有るでしょう。
実際のところ、VBの場合Lineコントロールと Lineメソドで
描画される線は、4本の線で構成されているのではなく、
2つの半円とそれに接する2本の線によって構成されます。
しかもこれがある法則をもってピクセル単位に丸められてます。

>たいていのソフトは直線そのものじゃなくある程度の幅を持たせていると
>思いますのでその線を囲む4つの式をだせばいいだけじゃないかと思いますが。
クリックされた点が4つの式で構成される図形の内側にいるのか、外側に
いるのかどのように判断しましょうか?

>こんなことしなくてもクリックした点と線との直交距離をだせばいいですね。
これはだめですよね、線の端は円ですから。(^^;


めだか  2006-03-18 01:28:10  No: 130680

たとえば点1と点2があってp1,p2としたとき
点の形が四角なら以下のように判断すればいいし
点の形が円ならその四角形に納まる円の式で判断すればいいだけじゃないでしょうか?
そんなに難しい話をしていないと思いますよ。
あとは応用を利かせるだけでしょう。

If (p1.left < x) and (x < p1.right) and (p1.top < y) and (y < p1.bottom) Then
  '点1がクリックされた
ElseIf (p2.left < x) and (x < p2.right) and (p2.top < y) and (y < p2.bottom) Then
  '点2がクリックされた
ElseIf (p1.left < x) and (x < p1.right) and (p2.top < y) and (y < p2.bottom)
  dblDst = 線分との直交距離を求める
  If dblDst < 誤差 Then
    '線がクリックされた
  End If
End If


特攻隊長まるるう  2006-03-18 01:41:58  No: 130681

…なつかしい。昔は中3くらいで点が図形の内部か?外部か?って
やった気がする…高3かなぁ…微積の教科書…だったかも?…でも
一番単純なのは不等号の方程式が成り立つかどうかだから中3でも
分かる。VBの知識じゃなくって数学の知識が要る。そこは板違いと
言ってもいい感じ。

  幅を持たせた2つの不等号方程式が成り立つかどうかでいいんじゃない?
…って同じ内容を過去に書いた気もするが。…無いから他の掲示板だなぁ。
  しかし、この質問…ろくに自分で調べた情報を開示せず、回答は
気に入らないときた。…すげぇ。オイラはこれ以上邪魔しないからw


めだか  2006-03-18 01:54:02  No: 130682

>ElseIf (p1.left < x) and (x < p1.right) and (p2.top < y) and (y < p2.bottom)

ここおかしいですね。
ようはクリックされた点x,yがそれぞれ2点の間にあるかを調べたかったんですが。

これ以上詳細にするとプログラムを作ってしまいそうなので、この辺で引いておきます。

質問主さんもあれこれ考えるより、まず正当方で試してみてはいかがでしょう?


我龍院忠太  2006-03-18 03:07:13  No: 130683

ScaleMode = 3                       
DrawWidth = 40                    
Line (100, 100)-(500, 200)
この様な線を引いて、クリックした点がこの線の内側にあるか、外側に
有るか判断するコードを書いてみてください。
机上の理論の虚しさを味わえます。
線は先ず始点と終点に直径DrawWidthの円を書き、その2つの円に接する
2本の線を引き、その閉じた空間を線としますが、線が斜めの場合結構
この式が大変です。
もちろん出来ないことは有りません、ガーバーデーターを作る時などこれ
をやるのですが、スピードが必要なのでたいていはCで書きます。
ただし私が示した簡易的な方法も結構便利ですよ。

>オブジェクト指向?といわれているVisual Basic .netはどうですか?
VB.NetのGDI+は結構良く出来ていて、閉じた多角形の内側をいっぺんに
同じ色で塗ったり、たしか内側か外側かの判断も出来た気がします。
ただし惜しむらくは遅い・・・
2005は最近インストールしたばかりなのでまだ試してはいないのですが。


三戸  2006-03-18 03:33:03  No: 130684

何かオブジェクト?の様な物を配置することで手抜きが出来ないものかと思って
質問させていただきましたが、少し違う方向に・・・

CanvasやIllustratorといった商用ソフトがどうやっているのかという疑問はまだ
残ったままですが、我龍院忠太さんとK.J.K.さんの方法を試してみます。

ありがとうございました。


めだか  2006-03-18 03:52:28  No: 130685

線幅がいくらになろうとも数学的に同じことですよね?

点1と点2を通る直線の端点が丸い場合

If クリックされた点が点1を中心とする半径=線幅の2分の1+αの円内 Then
  '点1がクリックされた
ElseIf クリックされた点が点2を中心とする半径=線幅の2分の1+αの円内 Then
  '点2がクリックされた
ElseIf クリックされた点x,yがそれぞれ点1と点2の間にある Then
  dblDst = 線分とクリックされた点の直交距離を求める
  If dblDst < 線幅の2分の1+α Then
    '線がクリックされた
  End If
End If

なにも数行で上記アルゴリズムがコーディングできるとは考えていませんが、そんなに高度な
計算でもないので、それなら最初から正攻法で作っておいて処理速度的に問題が生じたなら
考えてみるという方向で作ってみてはということです。
おそらく200回程度上記計算を繰り返してもほとんど瞬時に計算されるとは思いますが。

P.S 我龍院忠太さんの方法を否定しているわけじゃありませんよ。
まず、基礎的な事を知った上で処理速度的にどうしても問題があるというなら
我龍院忠太さんのような別のアルゴリズムを考えればよいかなと思っただけです。


特攻隊長まるるう  2006-03-18 04:13:05  No: 130686

…やっぱり過去、同じ事を我龍院忠太さんに言われた気がするw。うーん。。。

めちゃめちゃ厳密な判定するんですね。つまりはピクセル単位に丸められたx,yでないと反応しないと。
逆に操作性が悪くなりそうなのでボクは使わないかなぁ。描画に同じメソッド使えば丸めたx,yで描画
してくれるだろうから、クリック判定はメモリ上の方程式からyを求めてyの理想値との差が一定範囲内なら
クリックできたとします。

…とか言って
>大雑把で良いなら色々と方法は有るでしょう。
と書きました…って言われた気がする。。。デジャブ?


我龍院忠太  2006-03-18 06:39:56  No: 130687

>めちゃめちゃ厳密な判定するんですね。
こういった画像処理の場合、画像の拡大、縮小が対になっている場合が多いんです、
たとえばクリックした時、線上に有るか無いか判断して、線上に有ったと判断した場合、
そのまま拡大モードで拡大する、そしたら実は線上に無いじゃんという事になりかねないのです。
又有る線の端を掴んで別の線の上に載せるとします、別の線の上に来たらちょっと色を変える
ようなソフトがありますよね、これで閉じた多角形を作成して、中を塗り潰そうとしたら、
実は線の端が1ピクセル空いていた為に、塗りつぶせなかった、等という事が起こります。
実際GDI+の場合は1ピクセルでも空いていると塗りつぶせません。
このようなソフト作って納品すると即クレームになります。
めだかさんすみません、こだわった理由は、実は、「相当痛い目に有ってます。」
特に端面の処理で。(^^;

>逆に操作性が悪くなりそうなのでボクは使わないかなぁ
VBのエディターは実に良くできています、実際線を掴む場合、マウスが線上に無くても、
線に近ければクリックしますし、太い線の場合、両端の半円の中心が表示されます。
私もこの辺を相当参考にさせてもらいました。
ただしこれは、適当にやってる訳じゃなく、かなり綿密な計算の上でやっているのでしょう。大雑把に見えて、実は緻密、これが使いやすい。
上のサンプルでも実は、座標を取る為に書く線は元の線より少し太めに書きます、
こうすると結構それらしく見えます。
あまり手の内を明かしてしまうと、飯の種が無くなりますのでこの辺にします。

>デジャブ?
ですね。
線上をクリックする件については以前もありましたが、
>大雑把で良いなら色々と方法は有るでしょう。
と書いた覚えはありません。
というのは大雑把と言う方法は、クリックした点から、調べる線の太さの半分
の半径を持つ円を書き、これと線の中心の直線の式が解を持つか計算する方法です。
これは計算の部分をCでDLLを作って置けば高速ですし、端面の問題もクリアーしてます。
最近思いついた方法なので以前は書けないと思うのですが・・・


三戸  2006-03-19 02:58:57  No: 130688

>めだかさん
>おそらく200回程度上記計算を繰り返してもほとんど瞬時に計算されるとは思いますが。
200回くらいなら私もそうしているのですが、
  判定する回数=曲線の数(約200)×曲線構成座標(約200)≒40000回
という感じで曲線の数や構成座標の数が多くなってくるとどうなのだろうかと
思ったから手抜きの方法が無いものかと思いました。

また、表示させた曲線の拡大や縮小も考慮したとき、特攻隊長まるるうさん、
めだかさん、我龍院忠太さんが議論となっているように、複雑な処理を必要と
するだろうと思いました。その辺りは、いい感じで用意されている機能があっ
たならば、その機能に全て丸投げの形で任せてしまおうと思っていました。


めだか  2006-03-19 09:42:03  No: 130689

クリックされたことを知って、その後なにがしたいのでしょうか?
単に移動、全体の拡大縮小、変形をするだけなら線のクリックを
知る必要が無いような気もしますが。
つまり、その曲線を囲む四角形内をクリックすれば移動でき、四角形の
隅がクリックされれば全体的に変形できるような感じでしょうか。

構成する直線群の一つの線の端をつかんで変形させたいということで
あれば、その線分の端点がクリックされたかを知る必要があるので
我龍院忠太さんのサンプルそのままでは求めることができないでしょう。

>複雑な処理
とありますが、そうなるかならないかはあなたの仕様しだいです。
複雑といっても既に言ってるようにクリックされた点を2つの端点、
直線との距離と3つ比較するだけです。
案ずるより生むが易しで、一つルーチンをつくっておけば40000回でも
何万回でも繰り返すだけです。
数千行もコーディングしなければ出来ないというわけじゃないんだし
とりあえずやってみましょう。

>その機能に全て丸投げの形で任せてしまおうと
作ってしまえば丸投げできますよ。


めだか  2006-03-19 11:19:31  No: 130690

とかいって、暇にまかせて検証してみました。

フォームにPicture1という名のピクチャーボックスを貼り付けてください。

4万の線分をランダムに引いています。(殆ど画面は真っ黒になると思いますが)

線の重なりを気にせず40000の線分について全部判定した場合の処理時間は
WinXP Pro
VB6.0 SP6 
Pen4 1.7GHz
の環境で約0.3秒です。

重なりを考慮する場合は最初にヒットしたところでExit Forを使ってループを抜けると良いでしょう。
(ただし引数の小さい方が上にあると考えています。反対にする場合はForの開始と終了を逆にしたらいいでしょう)

線幅や線数を変えて色々ためしてみて下さい。
DLLに蹴りだせば速度も若干上がるんじゃないでしょうか。

以下にソースを貼っておきます。

Option Explicit

Private Type LineStruct
  X1  As Double   '点1 X
  Y1  As Double   '点1 Y
  X2  As Double   '点2 X
  Y2  As Double   '点2 Y
  W   As Double   '線幅
  C   As Long     '線色
End Type

Private stLine(39999)   As LineStruct '直線の数=40000
Private Const Mergine   As Double = 2 '線の近傍2pixelならクリックされたと判断する
Private Const LW        As Double = 20 '線幅
Private Const LC        As Long = &HFFFF&  '線色

'
' 線の描画
'
Private Sub DrawLines()

  Dim i As Long
  
  For i = 0 To UBound(stLine) - 1&
    Picture1.DrawWidth = stLine(i).W
    Picture1.Line (stLine(i).X1, stLine(i).Y1)-(stLine(i).X2, stLine(i).Y2), stLine(i).C
  Next

  Picture1.Refresh
  
End Sub

Private Sub Form_Load()

  Dim i   As Long
  
  Picture1.AutoRedraw = True
  Picture1.ScaleMode = 3      'Pixel
  
  'ランダムな直線を生成
  
  
  For i = 0 To UBound(stLine)
    If i = 0 Then
      stLine(i).X1 = Rnd * Picture1.ScaleWidth
      stLine(i).Y1 = Rnd * Picture1.ScaleHeight
    Else
      stLine(i).X1 = stLine(i - 1).X2
      stLine(i).Y1 = stLine(i - 1).Y2
    End If
    stLine(i).X2 = Rnd * Picture1.ScaleWidth
    stLine(i).Y2 = Rnd * Picture1.ScaleHeight
    stLine(i).W = LW
    stLine(i).C = LC
  Next

  DrawLines

End Sub

'
'  点が円に含まれるかを調べる
'
Private Function IncludeCircle(X1 As Double, Y1 As Double, R As Double, X As Double, Y As Double) As Boolean

  If Sqr((X1 - X) ^ 2 + (Y1 - Y) ^ 2) > R + Mergine Then
    IncludeCircle = False
  Else
    IncludeCircle = True
  End If

End Function

'
' 点と線との直行距離を求める
'
Private Function CalDistance(X1 As Double, Y1 As Double, X2 As Double, Y2 As Double, X As Double, Y As Double) As Double

  Dim dblDist As Double
  Dim A       As Double
  Dim B       As Double
  Dim dx      As Double
  Dim dy      As Double
  
  If X1 = X2 Then
    '縦線
    dblDist = Abs(X1 - X)
  ElseIf Y1 = Y2 Then
    '横線
    dblDist = Abs(Y1 - Y)
  Else
    A = (Y2 - Y1) / (X2 - X1)
    B = Y1 - A * X1
    dx = (Y - B) / A
    dx = Abs(X - dx)
    dy = A * X + B
    dy = Abs(dy - Y)
    dblDist = (dx * dy) / Sqr(dx * dx + dy * dy)
  End If
  
  CalDistance = dblDist

End Function

Private Sub Picture1_MouseDown(Button As Integer, Shift As Integer, X As Single, Y As Single)

  Dim i   As Long
  Dim X1  As Double
  Dim Y1  As Double
  Dim X2  As Double
  Dim Y2  As Double
  
  Dim t   As Double
  
  t = Timer  

  For i = 0 To UBound(stLine)
    If IncludeCircle(stLine(i).X1, stLine(i).Y1, stLine(i).W / 2#, CDbl(X), CDbl(Y)) Then
      'Debug.Print Format(i) & "番目の線の点1がクリックされた"
      'Exit For
    ElseIf IncludeCircle(stLine(i).X2, stLine(i).Y2, stLine(i).W / 2#, CDbl(X), CDbl(Y)) Then
      'Debug.Print Format(i) & "番目の線の点2がクリックされた"
      'Exit For
    Else
      'X1<X2となるように調整
      If stLine(i).X1 < stLine(i).X2 Then
        X1 = stLine(i).X1
        X2 = stLine(i).X2
      Else
        X1 = stLine(i).X2
        X2 = stLine(i).X1
      End If
      'Y1<Y2となるように調整
      If stLine(i).Y1 < stLine(i).Y2 Then
        Y1 = stLine(i).Y1
        Y2 = stLine(i).Y2
      Else
        Y1 = stLine(i).Y2
        Y2 = stLine(i).Y1
      End If
      If X1 < X And X < X2 And Y1 < Y And Y < Y2 Then
        If CalDistance(stLine(i).X1, stLine(i).Y1, stLine(i).X2, stLine(i).Y2, CDbl(X), CDbl(Y)) < stLine(i).W / 2 + Mergine Then
          'Debug.Print Format(i) & "番目の線がクリックされた"
          'Exit For
        End If
      End If
    End If
  Next
  
  Debug.Print Timer - t
End Sub


のびた  2006-03-19 11:24:36  No: 130691

最後に感想

クリックの判定後、図形を移動とか変形とかして40000の線を再描画する方が時間がかかりますよね。。。


三戸  2006-03-23 00:30:05  No: 130692

>めだか さん
詳細なサンプルをありがとうございます。
4万の線分をたった約0.3秒で判定出来るとは思ってませんでした。
あまりVBの計算速度を全く信用していなかったので、これからは反省したいと思います。

このコードを参考にしながら、作っていきたいと思います。
ありがとうございました。


※返信する前に利用規約をご確認ください。

※Google reCAPTCHA認証からCloudflare Turnstile認証へ変更しました。






  このエントリーをはてなブックマークに追加