図形の塗りつぶし

タグの編集
投稿者 yamada  (社会人) 投稿日時 2010/7/29 14:52:35

こんにちは。趣味でプログラムを楽しんでいます。
早速質問です。
Windowsのペイントの塗りつぶしのように、閉じた図形の内部を塗りつぶしたいと思います。
いろいろと調べた結果、VB6でならExtFloodFill を使って可能であることを見つけましたが、これはVB2008では使用できますか?
塗りつぶしの色の指定がわかりません。

実際にはコッホ島の内部を塗りつぶしたいのですが、問題を単純にするためにフォームに四角を描いて塗りつぶしを試しています。
Public Class Form1

    Declare Function ExtFloodFill Lib "gdi32" Alias "ExtFloodFill" (ByVal hdc As LongByVal x As Long, _
        ByVal y As LongByVal crColor As LongByVal wFillType As LongAs Long

    'ByVal hdc As Long,            '/* 塗りつぶしを行うデバイスハンドル */ 
    'ByVal x As Long, _            '/* 塗りつぶしを行う開始座標(x) */ 
    'ByVal y As Long,             '/* 塗りつぶしを行う開始座標(y) */ 
    'ByVal crColor As Long,       '/* 塗りつぶしを行う対象色 or 境界線色 */ 
    'ByVal wFillType As Long      '/* 塗りつぶしモードフラグ */ 

    '/* 塗りつぶしモードフラグ */ 
    Public Const FLOODFILLBORDER = 0    '/* crColorの色の境界線色まで塗りつぶしなさいモード */ 
    Public Const FLOODFILLSURFACE = 1   '/* crColorの色の部分を塗りつぶしなさいモード */ 


    Private Sub Button1_Click(ByVal sender As System.ObjectByVal e As System.EventArgs) Handles Button1.Click
        Dim g As Graphics = Me.CreateGraphics

        Dim hdc As Long = Me.Handle
        Dim x As Long = 150
        Dim y As Long = 150
        Dim crcolor As Long = ColorTranslator.ToWin32(Me.BackColor)   'これもかなり怪しい 
        Dim wFillType As Long = FLOODFILLSURFACE

        g.Clear(Me.BackColor)
        g.DrawRectangle(Pens.Red, 100, 100, 100, 100)

        Me.ForeColor = Color.Blue
        'VB6ではFillColorプロパティで色を設定していました。試しにこうしてみましたが勿論ダメでした。 

        ExtFloodFill(hdc, x, y, crcolor, wFillType)

    End Sub

End Class

投稿者 魔界の仮面弁士  (社会人) 投稿日時 2010/7/29 16:02:52
ExtFloodFill も使えますが、宣言が間違っています。
Private Declare Function ExtFloodFill Lib "gdi32" ( _
  ByVal hdc As IntPtr, _
  ByVal x As Integer, _
  ByVal y As Integer, _
  ByVal crColor As Integer, _
  ByVal wFillType As UIntegerAs <MarshalAs(UnmanagedType.Bool)> Boolean
Private Const FLOODFILLBORDER As UInteger = 0UI
Private Const FLOODFILLSURFACE As UInteger = 1UI


> Dim crcolor As Long = ColorTranslator.ToWin32(Me.BackColor)   'これもかなり怪しい
COLORREF 構造体への変換は、ColorTranslator.ToWin32 で合っています。
ただし、それを As Long にするのは誤りです。

> 'VB6ではFillColorプロパティで色を設定していました。
塗りつぶし情報は、SelectObject API で切り替えます。

なお、API を使わずに処理するならこんな感じ。
http://dobon.net/cgi-bin/vbbbs/cbbs.cgi?mode=al2&namber=26983&rev=&no=0
投稿者 yamada  (社会人) 投稿日時 2010/7/29 22:57:08
さっそくありがとうごさいます。
調べたサンプルをそのまま張り付けてしまったのがバレバレですが、VB6のLongは2008ではIntegerになるのをうっかりしていました。そのほかにも書き方がずいぶん違うみたいですね。
MarshalAs属性で型が定義されていないというエラーが出るのも何とか克服しました。

>塗りつぶし情報は、SelectObject API で切り替えます。
とのことで、これについて調べてみるとVB6中級講座に6.ペンとブラシという項目があったので、それを参考に書き換えてみました。
Imports System.Runtime.InteropServices
Public Class Form1

    Private Declare Function ExtFloodFill Lib "gdi32" ( _
    ByVal hdc As IntPtr, _
    ByVal x As Integer, _
    ByVal y As Integer, _
    ByVal crColor As Integer, _
    ByVal wFillType As UIntegerAs <MarshalAs(UnmanagedType.Bool)> Boolean


    'hdc             '/* 塗りつぶしを行うデバイスハンドル */ 
    'x             '/* 塗りつぶしを行う開始座標(x) */ 
    'y             '/* 塗りつぶしを行う開始座標(y) */ 
    'crColor       '/* 塗りつぶしを行う対象色 or 境界線色 */ 
    'wFillType      '/* 塗りつぶしモードフラグ */ 

    '/* 塗りつぶしモードフラグ */ 
    Public Const FLOODFILLBORDER As UInteger = 0UI
    '/* crColorの色の境界線色まで塗りつぶしなさいモード */ 
    Public Const FLOODFILLSURFACE As UInteger = 1UI
    '/* crColorの色の部分を塗りつぶしなさいモード */ 

    Private Declare Function CreateBrushIndirect Lib "gdi32" _
        (ByVal lpLogBrush As LOGBRUSH) As <MarshalAs(UnmanagedType.Bool)> Boolean
    Private Declare Function SelectObject Lib "gdi32" (ByVal hdc As IntPtr, _
        ByVal hObject As IntegerAs <MarshalAs(UnmanagedType.Bool)> Boolean
    Private Structure LOGBRUSH
        Public lbStyle As Integer
        Public lbColor As Integer
        Public lbHatch As Integer
    End Structure

    Private Sub Button1_Click(ByVal sender As System.ObjectByVal e As System.EventArgs) Handles Button1.Click
        Dim g As Graphics = Me.CreateGraphics

        Dim hdc As IntPtr = Me.Handle
        Dim x As Integer = 150
        Dim y As Integer = 150
        Dim crcolor As Integer = ColorTranslator.ToWin32(Me.BackColor)
        Dim wFillType As UInteger = FLOODFILLSURFACE

        Dim hNewBrush As Integer
        Dim hOldBrush As Integer
        Dim NewBrush As LOGBRUSH

        g.Clear(Me.BackColor)
        g.DrawRectangle(Pens.Red, 100, 100, 100, 100)

        'ブラシの作成 
        NewBrush.lbColor = ColorTranslator.ToWin32(Color.Blue)
        NewBrush.lbStyle = 0
        NewBrush.lbHatch = 5
        hNewBrush = CreateBrushIndirect(NewBrush) 'ここでエラー 

        'ブラシを持ち替える 
        hOldBrush = SelectObject(hdc, hNewBrush)

        ExtFloodFill(hdc, x, y, crcolor, wFillType)

    End Sub
End Class

これを実行すると
hNewBrush = CreateBrushIndirect(NewBrush)のところで、AccessViolationExceptionはハンドルされませんでした。保護されているメモリに読みとりまたは書き込み操作を行おうとしました。
というエラーになります。
これはなぜでしょうか。

APIを使わない方法ですが、シード・フィル アルゴリズムを調べてみたところ、2008にはPointと言うメソッドがないというのが大問題でここでまた進まなくなってしまいました。
bitmapならGetPixelが使えるのですが、FormやPictureBoxなどは、調べてみてもBackColorプロパティを使うとかここでは意味のないことしかわかりませんでした。
投稿者 よねKEN  (社会人) 投稿日時 2010/7/29 23:13:11
> Private Declare Function CreateBrushIndirect Lib "gdi32" _
>       (ByVal lpLogBrush As LOGBRUSH) As <MarshalAs(UnmanagedType.Bool)> Boolean

ただしくは、「ByRef lpLogBrush As LOGBRUSH」ですね。
VB6ではByRef/ByValの指定を省略するとByRefを指定したものとして扱われます。

> APIを使わない方法ですが、シード・フィル アルゴリズムを調べてみたところ、2008にはPointと言うメソッドがないというのが大問題でここでまた進まなくなってしまいました。
> bitmapならGetPixelが使えるのですが、FormやPictureBoxなどは、調べてみてもBackColorプロパティを使うとかここでは意味のないことしかわかりませんでした。 

FormやPictureBoxに対して描画する場合にも、
BitmapクラスのGetPixel/SetPixelを使う方法が使えます。

最初の質問投稿で、

> Dim g As Graphics = Me.CreateGraphics

というコードがあり、CreateGraphicsメソッドを使っていますが、
このメソッドを使わないようにします。

具体的にはどうやるのかというと、手順は以下のURLを参照してください。
http://dobon.net/vb/dotnet/graphics/pictureboximageanddrawimage.html

ポイントは、
(1)Bitmapクラスのインスタンスを用意する
(2)GraphicsクラスはこのBitmapオブジェクトからGraphics.FromImageメソッドで取得する
(3)PictureBoxを使う場合なら、このBitmapオブジェクトをImageプロパティに設定する
ということです。
(PictureBoxを使わずに、Formに直接描画するような場合であれば、
 (1)、(2)の手順までは同じで、FormのPaintイベントの中でe.Graphicsを使って、
 Graphics.DrawImageメソッドで(1)のBitmapを描画してやります)
投稿者 魔界の仮面弁士  (社会人) 投稿日時 2010/7/30 10:42:13
> VB6のLongは2008ではIntegerになるのをうっかりしていました。
とは限りません。Long → Integer への単純置換で済むと言うのは、
32bit専用アプリしか作れなかった VB2002/2003 当時の話です。

たとえば API 側が DWORD 型であった場合は、(VB6)Long→(VB2008)Integer/UInteger へ
置きかえるだけで OK です。しかし、今回の場合はそうではありません。

元の型がハンドル(型名が h や H で始まるもの)や、ポインタ(型名が LP で始まるもの)で
あった場合、VB6 の Long/OLE_HANDLE 型は、VB.NET では Integer 型に置き換えるのではなく、
IntPtr 型とするべきです。(64bit専用アプリならLong、32bit専用アプリならIntegerでも可)


> 調べたサンプルをそのまま張り付けてしまったのがバレバレですが
(Windows 的には 張り付け → 貼り付け ですね)

サンプルコードを利用する場合には、「ByRef As 構造体」と「ByVal As クラス」の
いずれで宣言されているかにも注意しておいてください。

たとえば、もとの資料が .NET 用に書かれた API コードであったとしても、

--------------
Declare Function CreateBrushIndirect Lib "…" (ByRef x As LOGBRUSH) As …
Structure LOGBRUSH
 :
End Structure
--------------
Declare Function CreateBrushIndirect Lib "…" (ByVal x As LOGBRUSH) As …
Class LOGBRUSH
 :
End Class
--------------

のように、同じ API が資料によって異なる宣言で紹介される事があります。

複数の資料を利用した場合などに、これらを混ぜこぜにして、「ByRef As クラス」で
宣言してしまい、正常に動作しなくなった事例を見たことがあります。蛇足までに。


>> Private Declare Function CreateBrushIndirect Lib "gdi32" _
>>       (ByVal lpLogBrush As LOGBRUSH) As <MarshalAs(UnmanagedType.Bool)> Boolean
> ただしくは、「ByRef lpLogBrush As LOGBRUSH」ですね。
戻り値の指定も間違っているようです。

この API の戻り値は、成否を表す BOOL 型ではなく、論理ブラシを表す HBRUSH 型です。
HBRUSH はハンドル型ですから、この場合には IntPtr で受ける必要があります。
http://msdn.microsoft.com/ja-jp/library/cc428326.aspx

そもそも、Boolean 型の戻り値を持つ関数であるならば、
> Dim NewBrush As LOGBRUSH
> Dim hNewBrush As Integer
> hNewBrush = CreateBrushIndirect(NewBrush) 'ここでエラー 
のように、それを Integer で受けているのは、API で無くとも不自然ですよね。
投稿者 yamada  (社会人) 投稿日時 2010/7/30 13:52:34
ありがとうございます。
まず、ByValをByRef指定に変えたところエラーが出なくなりました。QuickBASICをやったことがあるのでこれはわかりました。
ところが実行しても何も起こらないので、もう一度よくサンプルを見てみるとExtFloodFillやSelectObjectは、ハンドルを渡すのではなくデバイスコンテキストなるものを渡すのだとわかりました。
VB2008でAPIを使うのは初めてでVB6も未経験なので、デバイスコンテキストとか何のことかよくわかりませんがとりあえずこうすればできるというのはわかったのでやってみました。
すると塗りつぶしはされるようになりましたが、色が必ず白で塗られるので、この時点で魔界の弁護士さんのご指摘に気付きました。(CreatePenIndirectがブラシへのハンドルを返す)

一応目的とすることはできたので解決とします。ピクチャーボックスに描いたクロスステッチやコッホ島内部の塗りつぶしもできました。
シード・フィル アルゴリズムについては宿題ということで。
なんというか、もっと単純な問題だと思っていたのに意外と大変だったので助かりました。
調べてみてわかったのは、この塗りつぶしというのは結構いろいろなところで質問されていてよくある質問なのかもしれません。VB2008で解決する方法はみつからなかったので、同じような疑問を持った方の解決の助けになるかと思い、長く重複していますが、一応できたのを再掲しておきます。
'フォームに図形を描いて、Windowsのペイントのように塗りつぶすサンプル 
Imports System.Runtime.InteropServices
'↑MarshalAs属性を使用するには必要みたいです 

Public Class Form1
    '塗りつぶし関数 
    Private Declare Function ExtFloodFill Lib "gdi32" ( _
    ByVal hdc As IntPtr, _
    ByVal x As Integer, _
    ByVal y As Integer, _
    ByVal crColor As Integer, _
    ByVal wFillType As UIntegerAs <MarshalAs(UnmanagedType.Bool)> Boolean


    'hdc            塗りつぶしを行うデバイスハンドル / 正しくはデバイスコンテキスト 
    'x              塗りつぶしを行う開始座標(x) 
    'y              塗りつぶしを行う開始座標(y) 
    'crColor        塗りつぶしを行う対象色 or 境界線色 
    'wFillType      塗りつぶしモードフラグ 

    ' 塗りつぶしモードフラグ 
    Public Const FLOODFILLBORDER As UInteger = 0UI
    ' crColorの色の境界線色まで塗りつぶしなさいモード  
    Public Const FLOODFILLSURFACE As UInteger = 1UI
    ' crColorの色の部分を塗りつぶしなさいモード  

    '色を指定するためのブラシ作成関数 
    Private Declare Function CreateBrushIndirect Lib "gdi32" (ByRef lpLogBrush As LOGBRUSH) As IntPtr
    '不要になったブラシの解放 
    Private Declare Function DeleteObject Lib "gdi32" (ByVal hObject As IntPtr) As <MarshalAs(UnmanagedType.Bool)> Boolean
    'デバイスコンテキストの取得 
    Private Declare Function GetWindowDC Lib "user32" (ByVal hwnd As IntPtr) As IntPtr
    'デバイスコンテキストの開放 
    Private Declare Function ReleaseDC Lib "user32" (ByVal hwnd As IntPtr, ByVal hdc As IntPtr) As <MarshalAs(UnmanagedType.Bool)> Boolean
    '色を指定するために使う関数(ブラシのもちかえ) 
    Private Declare Function SelectObject Lib "gdi32" (ByVal hdc As IntPtr, _
        ByVal hObject As IntegerAs <MarshalAs(UnmanagedType.Bool)> Boolean

    'CreateBrushIndirectに渡す引数の構造体 
    Private Structure LOGBRUSH
        Public lbStyle As Integer
        Public lbColor As Integer
        Public lbHatch As Integer
    End Structure

    Private Sub Button1_Click(ByVal sender As System.ObjectByVal e As System.EventArgs) Handles Button1.Click
        Dim g As Graphics = Me.CreateGraphics

        Dim hwnd As IntPtr = Me.Handle              'フォームのハンドル 
        Dim hwdc As IntPtr = GetWindowDC(hwnd)      'デバイスコンテキスト 
        Dim x As Integer = 150
        Dim y As Integer = 150
        Dim crcolor As Integer = ColorTranslator.ToWin32(Color.Red) 'System.Drawing.Colorをint型に変換する関数 

        Dim wFillType As UInteger = FLOODFILLBORDER '赤まで塗りつぶせモード 

        Dim hNewBrush As Integer
        Dim hOldBrush As Integer
        Dim NewBrush As LOGBRUSH

        g.Clear(Me.BackColor)
        g.DrawRectangle(Pens.Red, 100, 100, 100, 100)   '赤で四角を描く 

        'ブラシの作成  
        NewBrush.lbColor = ColorTranslator.ToWin32(Color.Blue)
        NewBrush.lbStyle = 0
        NewBrush.lbHatch = 0
        hNewBrush = CreateBrushIndirect(NewBrush)

        'ブラシを持ち替える  
        hOldBrush = SelectObject(hwdc, hNewBrush)
        'ブラシの色で塗りつぶし 
        ExtFloodFill(hwdc, x, y, crcolor, wFillType)

        '解放 これは必要なのかわからないけど念のため 
        ReleaseDC(hwnd, hwdc)                       'デバイスコンテキストを開放する 
        hNewBrush = SelectObject(hwdc, hOldBrush)   '元のブラシに戻す 
        DeleteObject(hNewBrush)                     '不要になったブラシを開放する 

    End Sub
End Class
投稿者 よねKEN  (社会人) 投稿日時 2010/7/30 14:48:22
>    Private Declare Function SelectObject Lib "gdi32" (ByVal hdc As IntPtr, _
>        ByVal hObject As Integer) As <MarshalAs(UnmanagedType.Bool)> Boolean

上記の定義も間違っています。Windows APIの定義は以下のようになっています。

http://msdn.microsoft.com/ja-jp/library/cc410576.aspx

HGDIOBJ SelectObject(
  HDC hdc,          // デバイスコンテキストのハンドル
  HGDIOBJ hgdiobj   // オブジェクトのハンドル
);

SelectObjectの第二引数と戻り値はどちらも同じ型でGDIオブジェクトのハンドルですので、
以下のようにIntPtrで扱うのがよいでしょう。

Private Declare Function SelectObject Lib "gdi32" (ByVal hdc As IntPtr, _
       ByVal hObject As IntPtr) As IntPtr

> Dim hNewBrush As Integer
> Dim hOldBrush As Integer

それに合わせて上記もIntegerの代わりにIntPtrを使うとよいでしょう。
投稿者 魔界の仮面弁士  (社会人) 投稿日時 2010/7/30 15:33:03
> この時点で魔界の弁護士さんのご指摘に気付きました。
弁護士ジャナイデスヨー? (ಥ_ಥ)
http://yaplog.jp/orator/archive/20

> ハンドルを渡すのではなくデバイスコンテキストなるものを渡すのだとわかりました。
ここで渡すデバイスコンテキストもハンドルです。
「ウィンドウ ハンドル(HWND)」と「デバイスコンテキスト ハンドル(HDC)」の違いですね。

デバイスコンテキストとは、描画ツールを意味するオブジェクトです。
いわば画材や画用紙。VB2008 で言えば、Brush/Pen/Font などを表します。
(ウィンドウについては説明不要ですよね)

ちなみに、デバイスコンテキスト ハンドルと Grapchis オブジェクトは、
Grapchis.FromHdc メソッド/Graphics.GetHdc メソッドを通じて変換できます。


> Private Declare Function CreateBrushIndirect Lib "gdi32" (ByRef lpLogBrush As LOGBRUSH) As IntPtr
Declare 宣言は正しくなりましたが、使い方が間違ったままです。

現在のコードは
> Dim hNewBrush As Integer
> hNewBrush = CreateBrushIndirect(NewBrush)
のように Integer で受けていますが、これは誤りです。API 宣言の戻り値は
As IntPtr なのですから、それを受け取る変数も As IntPtr にする必要があります。


> Private Declare Function SelectObject Lib "gdi32" (ByVal hdc As IntPtr, _
>  ByVal hObject As Integer) As <MarshalAs(UnmanagedType.Bool)> Boolean
戻り値の型が間違っています。UnmanagedType.Bool にしてはいけません。
先ほどの CreateBrushIndirect と同様、この戻り値も IntPtr とすべきです。

そもそも、使用時に
> Dim hOldBrush As Integer
> hOldBrush = SelectObject(hwdc, hNewBrush)
--
> Dim hNewBrush As Integer
> hNewBrush = SelectObject(hwdc, hOldBrush)   '元のブラシに戻す 
のように、Integer 型に入れているのですから、宣言側が Boolean ではおかしいですよね。


> Dim g As Graphics = Me.CreateGraphics
よねKENさんも指摘されていますが、CreateGraphics() メソッドは
基本的には使わないようにしましょう。
投稿者 yamada  (社会人) 投稿日時 2010/7/30 16:10:47
いやはや名前を間違えるとは大変失礼いたしました。お詫びします。
ご指摘の点も修正しました。まだまだツメが甘いのがよくわかりました。
今後ともよろしくお願いします。