PictureBoxに描画した画像の移動

タグの編集
投稿者 永字  (社会人) 投稿日時 2008/9/29 06:04:12
初めまして、VB初心者の永字と申します。
現在VB上で、PictureBox上に画像を表示するプログラムを作っています。
仕様としましては。

1.Form上にPictureBoxを配置。
2.画像名と画像パスを設定し、10個までリストボックスで管理。
3.PictureBoxをクリックすると、クリックした位置にリストボックスで選択されているパスの画像を表示。
4.PictureBoxの他の座標をクリックした時、表示する画像が既にPictureBox上に存在する場合は、その画像が移動する。

というものです。

    Private Sub mapBox_MouseClick(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles mapBox.MouseClick
        '白を透明色として、画像の読み込み
        Dim bmp As Bitmap = New Bitmap(iPath.Text)
        bmp.MakeTransparent(Color.White)

        'mapBoxのGraphicsオブジェクトの作成
        Dim g As Graphics = mapBox.CreateGraphics()
        '表示
        g.DrawImage(bmp, mapBox.PointToClient(System.Windows.Forms.Cursor.Position).X, mapBox.PointToClient(System.Windows.Forms.Cursor.Position).Y)
        'オブジェクトを解放
        g.Dispose()
        bmp.Dispose()
    End Sub



mapBox:表示領域のPictureBox
iPath:リストボックスで選択されている画像のパス

このソースで仕様の3までは出来たのですが、これではクリックする度に新しい画像を描画してしまい、既存の画像の移動を行う事が出来ません。

描画されている画像の位置などを管理するにはどのようにすれば良いのでしょうか?

よろしくお願い致します。
投稿者 るきお  (社会人) 投稿日時 2008/9/29 07:15:52
こんにちは。
移動やアニメーションを伴う画像を描画する際は座標などの状態の管理はプログラマ自身が明示的に行い、描画のみPaintイベントなどで行います。
座標の変更が必要なタイミングで、プログラマは管理している座標の変数を更新して対象PictureBoxのInvalidateメソッドを呼び出します。Invalidateメソッドを呼び出すとPaintイベントが発生します。

ご提示の仕様とは少し違いますが簡単なサンプルを作りましたのでご確認ください。

Public Class Form1

    Private myImages As New List(Of Bitmap)
    Private points As New List(Of Point)

    Private Sub Form1_Load(ByVal sender As System.ObjectByVal e As System.EventArgs) Handles MyBase.Load

        '画像のロード(画像は先にメモリ上に読み込んでおく) 
        myImages.Add(New Bitmap("C:\Test\Sample1.bmp"))
        myImages.Add(New Bitmap("C:\Test\Sample2.bmp"))
        myImages.Add(New Bitmap("C:\Test\Sample3.bmp"))

        '初期設定 
        For i = 0 To myImages.Count - 1
            '白を透明化 
            myImages(i).MakeTransparent(Color.White)
            '初期位置は(0,0) 
            points.Add(New Point(0, 0))
        Next

        'この段階で3つの画像は座標(0,0)に重なって表示されます。 

    End Sub

    Private Sub PictureBox1_Click(ByVal sender As System.ObjectByVal e As System.EventArgs) Handles PictureBox1.Click

        '▼対象の画像を決定 
        '「リストボックスで選択されているパスの画像」ということですが、 
        'ここでは[Ctrl]キーか、[Alt]キーかが押されているかいないかで判定しています。 
        '仕様に応じて対象の画像の決定方法は適宜変更してください。 
        Dim targetIndex As Integer

        Select Case Control.ModifierKeys
            Case Keys.Control
                'Controlキーが押されている場合は0番目が対象 
                targetIndex = 0
            Case Keys.Alt
                'Altキーが押されている場合は1番目が対象 
                targetIndex = 1
            Case Else
                '何も押されていない場合は2番目が対象 
                targetIndex = 2
        End Select

        '対象の画像の座標を現在のマウスの座標にセット 
        points(targetIndex) = PictureBox1.PointToClient(System.Windows.Forms.Cursor.Position)

        '※この段階では座標がセットされただけで、画像は描画していない。 

        'PictureBox1に再描画を指示 
        PictureBox1.Invalidate()

    End Sub

    Private Sub PictureBox1_Paint(ByVal sender As ObjectByVal e As System.Windows.Forms.PaintEventArgs) Handles PictureBox1.Paint

        '現在セットされている座標にそれぞれの画像を描画 
        For i = 0 To myImages.Count - 1
            e.Graphics.DrawImage(myImages(i), points(i))
        Next

        'なお、再描画時にはそれまでの描画内容はすべてクリアされているので、 
        '以前に描画した画像を消すような処理は必要ない。 

    End Sub
End Class
投稿者 永字  (社会人) 投稿日時 2008/9/29 11:44:00
るきお様

丁寧な御回答とサンプルの提示、ありがとうございます。
成る程、あくまでも情報として保持しなくては行けないのは、各画像の座標で有って、描画自体は、画面全体に掛けるんですね。

自分では出来ない発想でした、参考にさせて頂きました。
リストボックスで、各画像の座標を管理し、クリックする事で内容を変更&画像の再描画を行う事で、上手く行きました。

規模が大きいので、完成までは時間が掛かりそうですが、一先ずは解決とさせて頂きます。

ありがとうございました。
投稿者 永字  (社会人) 投稿日時 2008/10/3 12:41:57
先日は、丁寧なご回答をありがとうございました。
ご提示されたサンプルを応用して、何とか望んだような動作をするようになりました。
しかし、更にもう一つ問題が発生してしまいまして…。

マップをクリックすることで座標を指定し、画像を描画する事には成功していると思います。
本体ウィンドウとは別フォームでマップフォームを作り、そちらに描画しています。

・本体側(マップをクリックすることで呼び出される)
    Sub mapRedraw()
        Dim i As Integer
        myImages = New List(Of Bitmap)
        points = New List(Of Point)
        For i = 0 To 19
            If Not Me.tokenListC.Items(i) Is "-" Then
                '画像名、画像パス、座標をタブで分解する
                Dim args() As String = Me.tokenListC.Items(i).Split(CChar(vbTab))
                'マップフォームに値を渡す
                  Dim XX As Integer = args(2) - 16
                Dim YY As Integer = args(3) - 16
                myImages.Add(New Bitmap(args(1)))
                points.Add(New Point(XX, YY))
            End If
        Next
        My.Forms.mapForm.Invalidate()
    End Sub

・マップフォーム側
    'マップを再描画する
    Private Sub mapBox_Paint(ByVal sender As Object, ByVal e As System.Windows.Forms.PaintEventArgs) Handles mapBox.Paint
        Dim i As Integer
        '現在セットされている座標にそれぞれの画像を描画
        For i = 0 To Form1.myImages.Count - 1
            Form1.myImages(i).MakeTransparent(Color.White)
            e.Graphics.DrawImage(Form1.myImages(i), Form1.points(i))
        Next
    End Sub

マップをクリックし、画像を描画した後、画面を一度他ウィンドウの後ろなどに隠せば、Paintイベントが走って描画を行ってくれるのですが、本体側の
My.Forms.mapForm.Invalidate()
が動作してくれず、Paintイベントが起こらず、自動での再描画を行ってくれません。
RefreshやUpdateでも同様でした。

本体側から、子ウィンドウの自動再描画を行う事は出来るのでしょうか?

度々で申し訳御座いませんが、御指導をお願い致します。
投稿者   (社会人) 投稿日時 2008/10/3 15:37:29
My.Forms.mapFormが実際に表示されてるものと違うとかってことはないですか?
mapFormを表示するときにNewしてたりして?
投稿者 永字  (社会人) 投稿日時 2008/10/3 17:47:28
早速のご回答ありがとうございます。

ご指摘の通り、マップフォーム表示の際は以下のようにNewしています。
指定方法が違うのでしょうか?

    'マップフォーム表示
    Private Sub ToolStripMenuItemMap_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles ToolStripMenuItemMap.Click
        '表示させるフォームのインスタンスを作成
        Dim musicForm As New mapForm()
        '表示させるフォームを所有する
        Me.AddOwnedForm(My.Forms.mapForm)
        My.Forms.mapForm.Show()
    End Sub

しかし、試しに

・本体側
My.Forms.mapForm.reMap();

・マップフォーム側
Public Sub reMap()
     MessageBox.Show("ここを通った?")
     Me.Invalidate()
End Sub

でマップフォームの関数を作って呼び出した所
メッセージボックスは表示されましたが、Paintイベントは発生しませんでした。
フォーム自体はちゃんと指定出来ていると思います。

ただ、画面の更新だけが無視されてしまうのは、何故なのでしょうか…。

よろしくお願い致します。
投稿者   (社会人) 投稿日時 2008/10/3 18:09:09
こんにちは。

mapBoxというPictureBoxコントロールがmapForm上に配置されていて、
実際に画像が描画されるのはmapBoxの上ということで合っていますでしょうか。

Me.Invalidate()
My.Forms.mapForm.Invalidate()


↑のようにしても再描画されるのはmapFormだけで、
ひょっとしてmapBoxは再描画されていないのではないでしょうか?
だとしたら以下のようにmapBoxを再描画することで対処できると思うのですが・・・
どうでしょう?

My.Forms.mapForm.mapBox.Invalidate()
投稿者 るしぇ  (社会人) 投稿日時 2008/10/3 19:42:46
本題とはずれるかもしれないけど、マップフォーム表示の
  Dim musicForm As New mapForm()
これは不味いんじゃないかなぁ。
  musicForm.Show()
ってコードを入れれば分かるけど、2画面表示されるでしょ?
  My.Forms.mapForm
とは別のインスタンスを生成することになるよ。
  My.Forms.mapForm
で既定のインスタンスを使う場合は New で
インスタンスを生成しないようにしてください。

  Dim musicForm As New mapForm()
丸ごと↑要りません。musicForm 使ってないですし。
投稿者 永字  (社会人) 投稿日時 2008/10/4 12:02:44
お返事が遅れて申し訳有りません。

>鍵さん
ご意見ありがとうございます。

>mapBoxというPictureBoxコントロールがmapForm上に配置されていて、
>実際に画像が描画されるのはmapBoxの上ということで合っていますでしょうか。
はい、それで間違っておりません。

My.Forms.mapForm.mapBox.Invalidate()
は、こちらもUpdate,Refresh共に試しましたが無理なようでした。

>るしぇさん
ご指摘ありがとうございます。
ポカミスでした…。すみません。
musicFormのコピペで作った際に、修正し忘れていたようです。

正しくは

   'マップフォーム表示
    Private Sub ToolStripMenuItemMap_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles ToolStripMenuItemMap.Click
        '表示させるフォームのインスタンスを作成
        Dim mapForm As New mapForm()
        '表示させるフォームを所有する
        Me.AddOwnedForm(My.Forms.mapForm)
        My.Forms.mapForm.Show()
    End Sub

でした。
これが原因かとも思ったのですが、修正してみても更新には影響が出ませんでした。


    'マップを再描画する
    Sub mapRedraw()
        Dim i As Integer
        myImages = New List(Of Bitmap)
        points = New List(Of Point)
        For i = 0 To 19
            If Not Me.tokenListC.Items(i) Is "-" Then
                Dim args() As String = Me.tokenListC.Items(i).Split(CChar(vbTab))
                'マップフォームに値を渡す
                Dim XX As Integer = args(2) - 16
                Dim YY As Integer = args(3) - 16
                myImages.Add(New Bitmap(args(1)))
                points.Add(New Point(XX, YY))
            End If
        Next
        mapForm.mapBox.Invalidate()
    End Sub

再描画命令をmapBoxに渡しましたが、相変わらず画面を一度窓の裏に隠さないと更新されないようです。

ちなみに上半のコードにあるDim mapForm As New mapForm()
は、元々表示されていないマップフォームをメインフォームの前に出す処理です。

Newをすると不都合、というご意見も有ったのですが、Newせずに定義する方法が有るのでしょうか?

よろしくお願いします。
投稿者   (社会人) 投稿日時 2008/10/4 18:15:37
こんにちは。

>My.Forms.mapForm.mapBox.Invalidate()
>は、こちらもUpdate,Refresh共に試しましたが無理なようでした。
そうでしたか、自分の環境だと問題なく再描画されるのですが・・・

'マップフォーム表示 
    Private Sub ToolStripMenuItemMap_Click(ByVal sender As System.ObjectByVal e As System.EventArgs) Handles ToolStripMenuItemMap.Click
        '表示させるフォームのインスタンスを作成 
        Dim mapForm As New mapForm()
        '表示させるフォームを所有する 
        Me.AddOwnedForm(My.Forms.mapForm)
        My.Forms.mapForm.Show()
    End Sub

↑はmusicFormがそのままmapFormに変わっただけですよね?
「Dim mapForm As New mapForm()」の行を丸ごと削除してみてください。
問題なくmapFormを表示できるはずです。

他にもform1側で「Dim mapForm As New mapForm()」を使って
mapFormのインスタンスを作成していたりしませんか?
もし、あるならその行を削除するか、

mapForm.mapBox.Invalidate()

と、いうコードを
My.Forms.mapForm.mapBox.Invalidate()

に置き換えてみてください。
これでどうでしょう・・・?

>Newをすると不都合、というご意見も有ったのですが、Newせずに定義する方法が有るのでしょうか?
自分はうまく説明する自信がありません^^;
と、いうかMy.Forms~でFormを表示する方法も実は初めて知ったので・・・
そこに関しては他の方のフォローに期待します。。。
投稿者 (削除されました)  () 投稿日時 2008/10/4 20:03:05
(削除されました)
投稿者 永字  (社会人) 投稿日時 2008/10/4 20:14:27
>鍵さん

状況の説明が不十分でお手数をお掛けしています。

Dim mapForm As New mapForm()


確かに削除しても問題なくフォームが動作しました。
インスタンスを作成した場合はMy.Formsで使用するものとは別インスタンスとなってしまうのですね。
ありがとうございます。

My.Forms.mapForm.mapBox.Invalidate()



申し訳ないです。

My.Forms.mapForm.mapBox.Invalidate()
も試したのですが、やはり反映はされないようです。
更に、別途mapForm側に関数を作り、

Public Sub reMap()
     MessageBox.Show("ここを通った?")
     Me.Invalidate()
End Sub



で更新を掛けてもやはり反映はされない状態です(関数自体が実行されている事は、MessageBoxで確認しています)

Me.Hide()を行っても反応しませんでした。
こうやって見ると、mapForm側の情報を参照を参照したり、格納されている変数を変える事は出来ているのですが、Me.BackColor = Color.AliceBlueのような、フォームの情報を変更するような命令は全て弾かれている気がします。

ソースに問題がないとすれば、フォーム側のプロパティに問題が有るのでしょうか?

よろしくお願いします


※前発言を削除し、訂正しました

My.Forms.mapForm.tokenList.Items(CInt(args(4))) = args(0)

のようなソースが動作していたと書きましたが、勘違いだったようです。
リストの更新も動作しませんでした。
フォームの値を参照する事は出来ても、変更することが出来ないようです。
投稿者 永字  (社会人) 投稿日時 2008/10/4 20:53:01
>鍵さん
>るきおさん
>るしぇさん
>桜さん

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

別フォームでの値を変更する知識に根本的な誤りが有ったようです。
以下の方法で解決を行いました。

・マップフォーム側
    'mapFormオブジェクトを保持するためのフィールド 
    Private Shared _mapFormInstance As mapForm

    'mapFormオブジェクトを取得、設定するためのプロパティ 
    Public Shared Property mapFormInstance() As mapForm
        Get
            Return _mapFormInstance
        End Get
        Set(ByVal Value As mapForm)
            _mapFormInstance = Value
        End Set
    End Property


・本体側
    'マップフォーム表示 
    Private Sub ToolStripMenuItemMap_Click(ByVal sender As System.ObjectByVal e As System.EventArgs) Handles ToolStripMenuItemMap.Click
        '表示させるフォームのインスタンスを作成 
        Dim mapForm As New mapForm()
        '表示させるフォームを所有する 
        Me.AddOwnedForm(mapForm)
        '表示させるフォームの情報を設定 
        mapForm.mapFormInstance = mapForm
        mapForm.Show()
    End Sub

※マップフォームは当初のソースのように、インスタンスを作成し、逆にMy.Formsは使用しないようにしました。

    'マップを再描画する 
    Sub mapRedraw()
        Dim i As Integer
        myImages = New List(Of Bitmap)
        points = New List(Of Point)
        For i = 0 To 19
            If Not Me.tokenListC.Items(i) Is "-" Then
                Dim args() As String = Me.tokenListC.Items(i).Split(CChar(vbTab))
                'マップフォームに値を渡す 
                Dim XX As Integer = args(2) - 16
                Dim YY As Integer = args(3) - 16
                myImages.Add(New Bitmap(args(1)))
                points.Add(New Point(XX, YY))
            End If
        Next

        '現在セットされている座標にそれぞれの画像を描画 
        Dim g As Graphics = mapForm.mapBox.CreateGraphics()

        For i = 0 To myImages.Count - 1
            myImages(i).MakeTransparent(Color.White)
            g.DrawImage(myImages(i), points(i))
        Next
        mapForm.mapFormInstance.mapBox.Invalidate()
    End Sub


別フォームの情報にアクセスするには、ただ、作成したフォーム名だけを指定すれば良いと勘違いしていたのが全ての原因だったようです。
知識不足にてお手間を取らせて申し訳ありませんでした。

ご提示頂いた情報のお陰で、理解が深まったように感じます。

長い時間お付き合い頂いて、ありがとうございました。
投稿者 るしぇ  (社会人) 投稿日時 2008/10/4 22:39:03
>別フォームの情報にアクセスするには、ただ、作成したフォーム名だけを指定すれば
>良いと勘違いしていたのが全ての原因だったようです。
フォームだけ特別なんです。(他のクラスは New が必要なのに。。。)

時代にフォーム名を直接指定すると VB が裏で勝手にインスタンスをつくる
仕様でした。では廃止されていたのですが、
復活してしまいました。賛否両論ありますが、今後も永字さんのように勘違いして
質問する人は続くでしょうねぇ。。。

別のインスタンスが生成されている疑いがある場合は、フォームに対する命令の
直前で、Show してやることです。画面が複数表示されればアウトです。

Shared 宣言で共有化した変数でインスタンスを管理。。。
ご自分で考えられたのだと思いますが、よく考えられていると思います。
ただ、やはり皆、考えることは同じでして、
[DOBON.NET > プログラミング道 > .NET Tips > フォームが一つしか表示されないようにする]
http://dobon.net/vb/dotnet/form/singleform.html
自分のインスタンスの生成までフォーム内部に実装してしまえばいいのです。

更に、既存の
  Public Sub New()

  Private Sub New()
に書き換えます。…すると、Instance プロパティを使うしかフォームの
インスタンスを得る手段がなくなります。
この手法は、1つの画面を1つしか表示しない場合、ボクもよく使う手法です。
投稿者 永字  (社会人) 投稿日時 2008/10/5 02:14:04
フォームだけは特別扱いなんですね…。
今回の事は勉強させて頂きました。
バージョンごとにそんな目まぐるしく仕様が変わっているとは。

手法も参考にさせて頂きますね。

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