Visual Basic 初級講座 [改訂版]
VB2005 VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

第45回 プラットフォーム呼び出し

2021/9/6

この記事が対象とする製品・バージョン

VB2019 Visual Basic 2019 対象です。
VB2017 Visual Basic 2017 対象です。
VB2015 Visual Basic 2015 対象です。
VB2013 Visual Basic 2013 対象です。
VB2012 Visual Basic 2012 対象です。
VB2010 Visual Basic 2010 対象です。
VB2008 Visual Basic 2008 対象です。
VB2005 Visual Basic 2005 対象です。
VB.NET 2003 Visual Basic.NET 2003 対象です。
VB.NET 2002 Visual Basic.NET (2002) 対象です。
VB6対応 Visual Basic 6.0 × 対象外です。

 

目次

 

1.相互運用

VBには .NET 以外の手段で実装されている機能を呼び出す方法がいくつか提供されています。これらを使って C++ など別の言語で記述された関数を呼び出すことができます。このようにプラットフォーム(≒基盤)の境界を超えた呼び出しを「相互運用」と呼びます。

VBと相性が良い相互運用は COM と C/C++ です。

今回は C/C++形式の関数をVBから呼び出す方法を扱いますが、対象がVBでも.NETでもないため、VBや.NETの世界には登場しない概念や言葉が多く登場します。そのためVBを説明している通常の記事とは雰囲気が異なります。簡単に補足説明はしますが、C/C++を理解するという趣旨ではないので、どうやって考え方で、どういうVBの機能を使ってそれらを呼び出すかという点に着目していただければと思います。

 

2.プラットフォーム呼び出し

2-1.C/C++形式の関数の呼び出し

C言語やC++言語で作成された関数が一定の要件で呼び出せる設定になっている場合、VBからはDLLImport属性またはDeclareステートメントを使って呼び出すことができます。

発展 発展学習  -  一定の要件で呼び出せる設定

発展学習では意欲的な方のために現段階では特に理解する必要はない項目を解説します。

「一定の要件で呼び出せる設定」は、C/C++側で作業が必要で、この要件を満たしていない関数は呼び出すことはできません。

DLL からのエクスポート | Microsoft Docs

 

また、C/C++と同じエクスポート方式を採用できるのであれば、プログラミング言語はC/C++に限定されません。

.NETでこれを呼び出すことを「プラットフォーム呼び出し」や「P/Invoke」と呼びます。

C/C++側の機能は自作することもできますし、誰かが作成したものを呼び出すこともできます。

Windowsの核心部分は C++ で実装されており、多くの機能が公開されています。これらはWindows API(ウィンドウズ エーピーアイ)と呼びます。プラットフォーム呼び出しのケースの多くはこのWindows APIです。

Windows API の一覧はここで見られます。大量のカテゴリーが掲載されていますが、これがすべてではなく、ここから辿れないAPIも多数あるようです。

Windows API インデックス - Win32 apps | Microsoft Docs

大量の機能がありますが、.NETのフレームワークで同じことができるものもの多く、.NET FrameworkとWindows APIの対応表もあります。

Microsoft Win32 と Microsoft .NET Framework API との対応 | Microsoft Docs

この表を見ると、たとえば、半透明で画像を出力するWindows APIの AlphaBlend 関数は、.NET Framework の System.Drawaing.Graphics.DrawImage メソッドで同じことができるということがわかります。

 

2-2.呼び出し方

VBからプラットフォーム呼び出しを行うには次のような定義を作成します。

下記の例はウィンドウの座標を取得するWindows APIである GetWindowRect を呼び出すための定義例です。

VB.NET 2002 VB.NET 2003 VB2005 VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

<Runtime.InteropServices.DllImport("user32.dll")>
Private Shared Function GetWindowRect(hwnd As IntPtr, ByRef lpRect As RECT) As Boolean
    'ここには何も記述しません。
End Function

→ Module内に定義する場合は Shared を除去してください。Sharedがあるとエラーになります。

このメソッド GetWindowRect についている DllImport属性 (読み方:DllImport=ディーエルエルインポート)です。これがプラットフォーム呼び出しの目印になります。

関数や引数の定義は、呼び出し対象によって変わります。この例は Windows API の GetWindowRect 関数を呼び出せるようにする定義です。

奇妙に見えますが、このメソッドの中には何も記述しません。何かプログラムを記述するとエラーになります。

これはVBからプラットフォーム呼び出しを行う定義であって、呼び出されたときに何をするかは、VBではなく、C/C++等で作成されたプログラム内に記述されています。そのプログラムのファイル名はDLLImport属性で指定します。この例では user32.dll です。user32.dll は Windowsの核心的な機能が実装されている重要な dll として有名です。ほとんどの環境では C:\Windows\System32 フォルダー内にあります。System32だといくつかの重要なフォルダーはパスを指定しなくてもファイルを認識してくれますが、オリジナルの場所にdllを配置する場合は C:\temp\myprogram.dll のようにフルパスを指定する場合もあります。

 

GetWindowRect関数を呼び出すには、構造体RECTも定義します。C++の世界では、.NETと同様にさまざまな構造体が使用されており、関数を呼び出す場合に、その関数で使用している構造体も定義しなければならないことがよくあります。

RECT構造体の定義は次のようにします。

VB.NET 2002 VB.NET 2003 VB2005 VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

<Runtime.InteropServices.StructLayout(Runtime.InteropServices.LayoutKind.Sequential)>
Public Structure RECT
    Public Left, Top, Right, Bottom As Integer
End Structure

この構造体にも見慣れない属性 StructLayout がついています。これはプラットフォーム呼び出しに必須ではありませんが、ほとんどの場合で必要です。GetWindowRect関数でも必須です。これは構造体のメンバーをメモリ上にどのように配置するかを指定するものです。.NETの世界ではメモリを直接操作しないのでメモリ上にどのように配置されるのか気にすることはありませんが、C/C++の世界ではメモリを直接操作するので、メモリ上の配置まで指定されたとおりにしないと適切に動作しません。

 

GetWindowRectは第1引数に対象のウィンドウのハンドルを渡すと、そのウィンドウの位置を第2引数に設定してくれます。

「ウィンドウのハンドル」というのは、Windowsがウィンドウを識別するために各ウィンドウに割り当てている数値のことです。

 

試しにExcelのウィンドウの位置を取得してみましょう。

次はコンソールアプリケーションでの使用例です。

VB.NET 2002 VB.NET 2003 VB2005 VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

Module Program

    Sub Main(args As String())
        Do While True
            GetExcelLocation()
            Console.WriteLine("Enterを押すともう1度座標を取得します。終了するには Ctrl + C を押してください。")
            Console.ReadLine()
        Loop
    End Sub

    <Runtime.InteropServices.DllImport("user32.dll")>
    Private Function GetWindowRect(hwnd As IntPtr, ByRef lpRect As RECT) As Boolean
        'ここには何も記述しません。
    End Function


    <Runtime.InteropServices.StructLayout(Runtime.InteropServices.LayoutKind.Sequential)>
    Public Structure RECT
        Public Left, Top, Right, Bottom As Integer
    End Structure

    Private Sub GetExcelLocation()
        Dim excel As Process = Process.GetProcessesByName("EXCEL").FirstOrDefault
        Dim rect As RECT
        GetWindowRect(excel.MainWindowHandle, rect)

        Console.WriteLine($"Excelの位置 左上の座標({rect.Left},{rect.Top}) 右下の座標({rect.Right},{rect.Bottom})")

        '↓VB2013以前の場合
        'Console.WriteLine("Excelの位置 左上の座標(" & rect.Left & "," & rect.Top & ") 右下の座標(" & rect.Right & "," & rect.Bottom & ")")
    End Sub

End Module

この例では、ウィンドウのハンドルを取得するために Process.MainWindowHandleプロパティを使用しています。このプロパティはOSで実行中のプロセス(≒アプリケーション)のメインウィンドウのハンドルを取得します。1つのアプリケーションで複数のウィンドウを持っていることが普通ですが、この例では簡略化のためメインのウィンドウだけを相手にします。

また、Excelのプロセスを探すために、Process.GetProcessesByNameメソッドを使って、プロセス名に EXCEL とついているものの1つめのプロセスを取得するようにしています。これも例を簡略化することが目的です。

実行するにはあらかじめExcelを起動しておく必要があり、2つ以上Excelが起動している場合は、1つ目しか認識できません。

肝心のWindows APIの呼び出しは、.NETの通常の呼び出しと同じで GetWindowRect というメソッド名にかっこを付けて引き数を指定しているだけです。DllImport属性をつけた宣言の効果で、この呼び出しによりC++で記述されたuser32.dll内にある関数が実行されます。

Excelのウィンドウの位置を少しずつずらしながら実行すると、ちゃんと取得する座標に反映されることを確認できます。

 

3.DllImport

3-1.構文

とりあえず DllImport属性を使って空の定義を作っておくことで、C++の関数を呼び出せることを体験することができました。

構文を確認しておきましょう。代表的な構文は次の通りです。

VB.NET 2002 VB.NET 2003 VB2005 VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

<Runtime.InteropServices.DllImport("DLLファイル名")>
Private Shared Function 関数名(引数リスト) As 戻り値の型
    'ここには何も記述しません。
End Function

 

この構文自体は特に難しいことはないのですが、対象がC/C++の関数なので、対象のC/C++の関数のリファレンスを見て、VBの定義に翻訳するのはなかなかな難しいものです。次から実例を紹介します。

 

3-2.定義の作成方法

DllImportの定義はどのように作成するのがよいでしょうか?

これはなかなか難しく、C++で対象の機能がどのように定義されているか調べて、VBに翻訳する必要があります。プラットフォーム呼び出しの場合、対象は.NETですらないので、型をどのように翻訳するかは特に難しいポイントです。

そこで、有志が作成している PInvoke.net というサイトがあり、誰かがWindows APIをVB/C#での定義に変換した内容がメモレベルですが、記載されており参考になります。

pinvoke.net: the interop wiki!

このサイトには検索機能があり、GetWindowRectで検索すると、だいたい上記のコードと同じ翻訳例が表示されます。

pinvoke.net: GetWindowRect (user32)

RECT構造体の定義も別途リンクされています。(…が、ちょっとシンプルではない例が表示れるようです。)

 

プラットフォーム呼び出し自体があまり使用する機能ではなく、万一必要になった時もWindows APIについては、pinvoke.net のようなサイトがあるので、自分でVBに翻訳することはあまりないと思いますが、せっかくなので少しどういう風にやるのか紹介しておきましょう。

知識がないと難しいので雰囲気だけ味わっていただければと思います。

GetWindowRectの説明は下記のページにありますが、言語がC++であり、知識がない人はまず理解できないと思います。

GetWindowRect function (winuser.h) - Win32 apps | Microsoft Docs

関数の定義は次のようになっています。

BOOL GetWindowRect(
  HWND hWnd,
  LPRECT lpRect
);

ここから読み取れる情報は次の通りです。

言語が違うだけでなく、.NETですらないので型の共通点はほとんどありません。2つ目の引数の型 LPRECT は構造体であり、VBから呼び出す際にはこの構造体も定義する必要があります。

BOOL型は、幸いVBでもそのまま対応する Boolean 型が使用できます。

HWND型は Windows APIの世界ではよく登場する型で、ウィンドウのハンドルを示す型です。ウィンドウのハンドルとは、前述したようにOSが個々のウィンドウを識別するのに使用している数値です。このようなハンドルを表現するのに.NETのフレームワークにはIntPtrという型が用意されているので、これに翻訳されます。

LPRECT型は少し難しいです。上記のGetWindowRectの説明には RECT構造体へのポインターであると示されています。(C++の世界では頭に lp が付いている識別子はポインターであるという慣習があります。)。

RECT構造体については、さらに下記の説明があります。

RECT (windef.h) - Win32 apps | Microsoft Docs

要するに、left, top, right, bottom の4つの LONG型のフィールドを持つ構造体です。ここで LONG型は VBのIntegerに相当するので同じように4つの名前をもつVBの構造体を定義してしまうのが良いです。名前は RECT にしてしまいます。

なお、引数の名前や構造体の名前はプラットフォーム呼び出し時には使用されないので、VB側で自由に変えてよいです。呼び出し時に重要なのは引数の順番と型、メモリ上の配置です。(関数の名前はデフォルトでは、呼び出しに使用されるので自由に変えてはダメです。メモリ上の配置はVBでは普段意識しないので、理解しにくい謎のこだわりに感じられるかもしれません。たとえば、4バイトおきに配置するような指示(アライメント)がある場合があります。)

以上により、型は次のように翻訳されます。

BOOL → Boolean、 HWND → IntPtr、LPRECT → RECT。

もっとこった置き換えが必要な場合、MarshalAs属性を使用することもあります。

MarshalAsAttribute クラス (System.Runtime.InteropServices) | Microsoft Docs

 

後は関数の構文をC++からVBのメソッドに変えれば定義のでき上がりですが、最後に1つだけ注意があります。

このメソッドの定義は共有メンバーとして記述する必要があります。つまり、Sharedをつけて宣言する必要があるということです。

ただし、Module ~ End Module 内に定義するメソッドは自動的に共有メンバーになるので、Sharedを付けるとエラーになります。これはVisual Studioに赤い波線が表示されるのですぐに気が付けます。その場合はSharedを除去してください。

VB.NET 2002 VB.NET 2003 VB2005 VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

Private Shared Function GetWindowRect(hwnd As IntPtr, ByRef lpRect As RECT) As Boolean
    'ここには何も記述しません。
End Function

第2引数 lpRect に ByRef を付けているのは、GetWindowRectの説明に、この引数で座標を受け取ると書いてあるからです。VBではByRefを使って引数の値を呼び出された関数側で書き換えるのは良い方法ではなく、推奨されませんが、C++ではよくあります。(引数の名前に lp が付いているのは、この引数を呼び出された関数側で書き換えるという意思表示であることが多く、つまり、lp をあれば ByRef を疑えということです。)

あとは、DllImport 属性でこの関数の実体が user32.dll にあることを示せば完了です。

関数の実体がどこにあるかも、GetWindowRectの説明ページに DLL User32.dll と明記されているのでわかります。

VB.NET 2002 VB.NET 2003 VB2005 VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

<Runtime.InteropServices.DllImport("user32.dll")>
Private Shared Function GetWindowRect(hwnd As IntPtr, ByRef lpRect As RECT) As Boolean
    'ここには何も記述しません。
End Function

 

 

3-3.EntryPointによる関数名の明示

DLLImport属性のフィールドを使うと、プラットフォーム呼び出しでの関数の呼び出し方法を少し変更できます。

もっともよく使用するのは EntryPoint です。

EntryPointを使うと、VB上での関数の定義名と、実際に呼び出す関数の名前を変えることができます。

プログラム中で使用している重要なキーワードと関数名が同じになる場合に、関数名の定義名を変更する場合や、同じ関数の呼び出しを引数の定義が異なる複数の定義に分けたりなどできます。(Windows API側で引数の型がAnyの場合に役に立ちます。)

次の例はWindows APIの MessageBox関数を呼び出します。これは画面にメッセージボックスをポップアップする機能で、VBのMsgBoxと同じような機能です。

VB2005 VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

<Runtime.InteropServices.DllImport("user32.dll")>
Private Shared Function MessageBox(hWnd As IntPtr, lpText As String, lpCaption As String, uType As UInteger) As Integer
    'ここには何も記述しません。
End Function

→ Module内に定義する場合は Shared を除去してください。Sharedがあるとエラーになります。

呼び出すときは次のようにします。

VB.NET 2002 VB.NET 2003 VB2005 VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019


MessageBox(0, "あいう", "タイトルです。", 0)

 

メモ メモ  - コンソールアプリでも使えます

.NETの機能ではないので、Windows環境でさえあれば.NET側の都合は無視してこのMessageBoxを呼び出せます。ただし、サービスのようなユーザーとの対話を想定していないアプリケーションからは呼び出せないと思います。また、.NETが気軽にMsgBoxを呼び出せないように制限をしているシーンでは、それなりの理由があることがあるので、MsgBoxのに代わりにWindows APIのMessageBoxを使うのは避けるのが賢明です。

 

発展 発展学習  -  hWnd と uType

発展学習では意欲的な方のために現段階では特に理解する必要はない項目を解説します。

読者の中にはMessageBox関数の第1引数と第4引数の機能が気になる方がいるかもしれないので補足しておきます。第1引数にはウィンドウのハンドルを指定することができます。ウィンドウのハンドルを指定した場合、メッセージボックスが表示されている間そのウィンドウを操作できなくなります。この状態のポップアップウィンドウをモーダルウィンドウとも呼びます。0 を指定した場合、非モーダル(=モードレス)でポップアップします。

第4引数はメッセージボックスに表示するアイコンやボタンの指定です。

 

 .NETのフレームワークにも同じ役割の System.Windows.Forms.MessageBoxクラス と System.Windows.MessageBoxクラスがあります。

クラス名と関数名が同じになってしまいますが、VBは賢いのでちゃんと区別してくれます。しかし、使う方の人間からすると紛らわしいので少し名前を変えたいと思うことがあるかもしれません。

その時に、関数名を変更して、代わりに次のように := を使って属性のEntryPointフィールドに"MessageBox"と指定することができます。

名前を Alert にしてみましょう。

VB2005 VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

<Runtime.InteropServices.DllImport("user32.dll", EntryPoint:="MessageBox")>
Private Shared Function Alert(hWnd As IntPtr, lpText As String, lpCaption As String, uType As UInteger) As Integer
    'ここには何も記述しません。
End Function

これで、呼び出し側はこのMessageBox関数をVB上ではAlertという名前で呼び出せます。

VB.NET 2002 VB.NET 2003 VB2005 VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019


Alert(0, "あいう", "タイトルです。", 0)

 

4.ウィンドウのスクリーンショットを取得

4-1.コンソールアプリケーションの準備

先ほど作成した、Excelのウィンドウの位置を取得するプログラムにもう少し機能を追加して、Excelのウィンドウのスクリーンショットを画像ファイルに保存するプログラムにしてみましょう。

上から順番に読んでいない方も安心してください。ここでさきほど作ったプログラムを再掲します。

私は .NET 6 のコンソールアプリケーションでこのプログラムを作成していますが、.NET Framework 1.0 以降なら動作すると思います。

VB.NET 2002 VB.NET 2003 VB2005 VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

Module Program

    Sub Main(args As String())
        Do While True
            GetExcelLocation()
            Console.WriteLine("Enterを押すともう1度座標を取得します。終了するには Ctrl + C を押してください。")
            Console.ReadLine()
        Loop
    End Sub

    <Runtime.InteropServices.DllImport("user32.dll")>
    Private Function GetWindowRect(hwnd As IntPtr, ByRef lpRect As RECT) As Boolean
        'ここには何も記述しません。
    End Function


    <Runtime.InteropServices.StructLayout(Runtime.InteropServices.LayoutKind.Sequential)>
    Public Structure RECT
        Public Left, Top, Right, Bottom As Integer
    End Structure

    Private Sub GetExcelLocation()
        Dim excel As Process = Process.GetProcessesByName("EXCEL").FirstOrDefault
        Dim rect As RECT
        GetWindowRect(excel.MainWindowHandle, rect)

        Console.WriteLine($"Excelの位置 左上の座標({rect.Left},{rect.Top}) 右下の座標({rect.Right},{rect.Bottom})")

        '↓VB2013以前の場合
        'Console.WriteLine("Excelの位置 左上の座標(" & rect.Left & "," & rect.Top & ") 右下の座標(" & rect.Right & "," & rect.Bottom & ")")
    End Sub

End Module

このプログラムはExcelのウィンドウの位置の座標を取得するだけでした。

この状態から改造していきましょう。

 

4-2.System.Drawing

今回のプログラムでは Windows の描画機能を利用する必要があります。.NETのフレームワークでは System.Drawing 名前空間の大部分が、この機能を担っています。

コンソールアプリケーションでこれらの名前空間の機能を活用できるようにするためには、NuGet で System.Drawing.Common をインストールしてください。

操作方法 NuGet で System.Drawing.Common をインストールするには?

ソリューションエクスプローラーでプロジェクトを右クリック → NuGetパッケージの管理 → 左上の「参照」をクリック → 検索欄に System.Drawing.Common を入力 → 対象がヒットしたらクリック → 右側のインストールをクリック。

下記の記事では画像付きで丁寧に操作方法を説明しています。

NuGetでパッケージをインストールする方法

 

4-3.PrintWindow関数

指定したウィンドウの内容を描画するには Windows API の PrintWindow関数 を使います。

PrintWindow関数のリファレンスはここにあります。

PrintWindow function (winuser.h) - Win32 apps | Microsoft Docs

関数の定義は次のようになっています。

BOOL PrintWindow(
  HWND hwnd,
  HDC hdcBlt,
  UINT nFlags
);

HWND型は先ほども登場したウィンドウのハンドルを表しており、VBではIntPtr型です。この引数で対象のウィンドウを指定します。

HDC型はデバイスコンテキストのハンドルというもので、ハンドルなのでやはりIntPtr型です。この引数は描画する対象を表しており、VBでこれに相当するのはSystem.Drawing.Graphicsオブジェクトです。第1引数のhwndで指定したウィンドウの内容を、この引数で指定したGraphicsオブジェクトで描画するわけです。ただ、Windows APIに、.NETのGraphicsオブジェクトを直接渡すことはできず、その代わりにウィンドウの時と同じようにGraphicsオブジェクトを識別するための「ハンドル」と呼ばれる番号をAPIの引数に指定することになります。Graphicsオブジェクトにはまさにこのために GetHdc というメソッドがあり、これを使ってハンドルを取得できます。Windowsの観点ではこれをデバイスコンテキストのハンドルと呼びます。

UINT型は意味と関連付けられていない純粋な値で、符号なし整数型です。VBでは UInteger に相当します。ウィンドウ全体を描画対象とするのか、ウィンドウの枠などは対象外とするのか指定できます。0 を指定しておくとウィンドウの全体が描画対象になるので、今回は 0 固定で構いません。

以上のことから、これをVBのDLLImportを使って書くと次のようになります。

VB2005 VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

<Runtime.InteropServices.DllImport("user32.dll")>
Private Function PrintWindow(hWnd As IntPtr, hdcBlt As IntPtr, nFlags As UInteger) As Boolean
    'ここには何も記述しません。
End Function

自力でがんばらないで pinvoke.net から定義をコピーしてくることもできます。(おそらく多くの人はコピーしてくる方を選ぶでしょう)

pinvoke.net: PrintWindow (user32)

 

4-4.プログラム

このプログラムには.NETの描画機能であるSystem.Drawing名前空間を使用します。これらの機能群は GDI+ (ジーディーアイプラス)とも呼ばれます。

GDI+は簡単に高度なグラフィックスを扱うことができるすばらしい機能でですが、使うには基本的なことを少し習得する必要があります。

今回はプラットフォーム呼び出しがテーマなので、GDI+の説明は割愛して完成版のプログラムで簡単に紹介します。

Excelを起動した状態で下記のプログラムを実行すると、そのExcelの画面が、C:\temp\winapi.bmp として保存されます。

VB2005 VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

Option Strict On

Module Program

    Sub Main(args As String())
        SaveExcelAsImage()
    End Sub

    <Runtime.InteropServices.DllImport("user32.dll")>
    Private Function GetWindowRect(hwnd As IntPtr, ByRef lpRect As RECT) As Boolean
        'ここには何も記述しません。
    End Function

    <Runtime.InteropServices.StructLayout(Runtime.InteropServices.LayoutKind.Sequential)>
    Public Structure RECT
        Public Left, Top, Right, Bottom As Integer
    End Structure

    <Runtime.InteropServices.DllImport("user32.dll")>
    Private Function PrintWindow(hWnd As IntPtr, hdcBlt As IntPtr, nFlags As UInteger) As Boolean
        'ここには何も記述しません。
    End Function

    Private Sub SaveExcelAsImage()

        Dim excel As Process = Process.GetProcessesByName("EXCEL").FirstOrDefault

        If excel Is Nothing Then
            Console.WriteLine("Excelは起動していません。")
            Return
        End If

        '●1.一時的な画像保存領域を確保 
        'ウィンドウの座標を取得
        Dim rect As RECT
        GetWindowRect(excel.MainWindowHandle, rect)

        'ウィンドウと同じ大きさの画像を作成。(この時点では内容は描画されていない)
        Using image As New Drawing.Bitmap(rect.Right - rect.Left, rect.Bottom - rect.Top)

            '●2.ウィンドウの内容を用意した画像に描画 
            Using targetGraphics As Drawing.Graphics = Drawing.Graphics.FromImage(image)
                Dim hDC As IntPtr = targetGraphics.GetHdc()
                PrintWindow(excel.MainWindowHandle, hDC, 0)
                targetGraphics.ReleaseHdc(hDC) '不要になったデバイスコンテキストは開放するお作法です。
            End Using

            '●3.画像の内容をファイルに保存 
            image.Save("C:\temp\winapi.bmp")
        End Using

    End Sub
End Module

このプログラムでは、機能はすべて SaveExcelAsImage メソッドにプログラムしています。

SaveExcelAsImageメソッドは、まず、Process.GetProcessesByNameメソッドを使って、実行中のExcelのプロセスを取得します。Excelは複数実行されている可能性がありますが、ここでは簡略化のため FirstOrDefault を使って最初に発見した1つだけを取得します。Excelが1つも起動されていない場合は、Processクラスの既定値であるNothingが変数excelに設定されます。

次にExcelのプロセスが取得できたか確認します。取得できていない場合は「Excelは起動していません。」というメッセージを表示して処理を抜けます。

Excelのプロセスを取得できた場合、既に説明しているWindowsAPIのGetWindowRectを使って、そのExcelのメインウィンドウの四隅の座標を取得します。

四隅の座標がわかればウィンドウの幅と高さを計算できますので、同じ幅と高さを持つ画像をメモリ上に作成します。これが Using image As New Drawing.Bitmap…  の行です。この時点では、このメモリ上の画像には何も描画されていないので、もし目視することができたとしたら真っ黒な画像に見えます。

その後、このメモリ上の画像に描画を実行する Graphicsオブジェクト(読み方:Graphics=グラフィックス)を生成します。これにはGraphics.FromImageメソッドを使用しています。このGraphicsクラスはGDI+の中心的なクラスでさまざまな描画を実行できます。

が、今回はプログラムを使ってGraphicsクラスに直接命令はしません。描画はWindowsAPIである、PrintWindow関数が実行してくれるので、PrintWindow関数に生成したGraphicsオブジェクトを渡します。ここで、.NETとWindowsのプラットフォームの境界があって、.NETのオブジェクトであるGraphicsをWindowsAPIに直接渡すことはできません。Windowsはデバイスコンテキストという概念で描画を監視していて、PringWindow関数もデバイスコンテキストが引数として渡されることを期待しています。

Graphicsオブジェクトの設計者は、このことを予見しており、GraphicsオブジェクトのGetHdcメソッド(ゲットエイッチディースィー)メソッドを使って、Graphicsオブジェクトからデバイスコンテキストのハンドルを取得できるようにしてくれているので、これを利用してPrintWindow関数に渡します。

PrintWindow関数の1つ目の引数には対象のウィンドウのハンドルを指定します。

これで、後はPrintWindow関数が、対象のウィンドウについてこのデバイスコンテキストに描画処理を行います。.NETの世界ではGraphicsオブジェクトの描画機能が呼び出されていることになり、結果として、先ほど作ったメモリ上の画像に対象のウィンドウの内容が描画されます。

最後にImageのSaveメソッド(読み方:Save=セーブ)を使って、このメモリ上の画像をファイルとして保存します。

WindowsフォームアプリケーションなどのGUIアプリケーションであればファイルとして保存する代わりに画面に表示することもできます。

発展 発展学習  -  高DPI対応はしていません

発展学習では意欲的な方のために現段階では特に理解する必要はない項目を解説します。

このプログラムはSurfaceなどの高DPIのディスプレイには対応していません。高DPI環境で実行すると画像にはウィンドウの一部のみが描画されます。

DPIの違うマルチモニター環境などを考えると高DPI対応は結構面倒に思います。

 

メモ メモ  - すべてのウィンドウで通用するわけではありません

2021年現在、Windows 10 + Excel 2019 でこのプログラムが動作することを確認しています。アプリケーションがウィンドウに内容を描画する方法には種類があり、PrintWindowを使って描画内容を取得できないアプリケーションもあります。将来Excelの描画方法が変更になったりするとExcelでも通用しなくなる日が来るかもしれません。

 

 

4-5.代替API

たいていのWindowsAPIには、対応している.NETの機能があり、プラットフォーム呼び出しを使う機会はそれほど多くありません。

Windows API の PrintWindow関数 も自分でプラットフォーム呼び出しを行う代わりに System.Drawing.Graphics.CopyFromScreenメソッドを使うことで代替できます。(PrintWindow関数の第3引数の機能だけはありませんが、あまり使わないので…)

先ほどの例で、PrintWindowを呼び出している部分は次のように書き換えても動作します。(長いので途中で改行しています。VB2008以前で実行する場合は、改行しないか、行継続文字を使用してください。)

VB2005 VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

'●2.ウィンドウの内容を用意した画像に描画 
Using targetGraphics As Drawing.Graphics = Drawing.Graphics.FromImage(image)
    targetGraphics.CopyFromScreen(
        rect.Left, rect.Top,
        0, 0,
        New Drawing.Size(rect.Right - rect.Left, rect.Bottom - rect.Top))
End Using     

やはり、.NETの世界だけでプログラムしたほうが見慣れた、理解しやすい形になります。

 

5.Declare

VBでは、DllImport属性の他に、Declareキーワード(読み方:Declare=ディクレア)を使ってプラットフォーム呼び出しを定義することもできます。

Declareは古いキーワードで.NET以前のVBから存在していました。.NETのVBでは Declare の使用は古いVisual Basic との互換性のためだけに存在しており、新しいプログラムでは使う必要はないと考えていただいて良いと思います。

相違点を表にまとめてみます。

  DllImport Declare
時期 新しい
2002年に.NETとともに導入された。
古い
.NET以前からあった。
VB以外では VB以外のC#などの.NET言語でも使用できる。 VB専用
使い分けは? 優先的に使用する 古いプログラムを移植する場合などは残してもよい。
それ以外の場合、積極的に使う理由はない。

表に記載したようにDllImportの方はC#でも共通で使用できる機能なので、インターネットで検索した時の情報量が違います。

DllImport と Declare はプラットフォーム呼び出しを定義するという同じ目的なので、似たようなことを記述することになりますが、細かいお作法は異なります。

WindowsAPI の MessageBox で見比べてみましょう。

DllImport版はさきほど紹介しました。次のようになります。

VB2005 VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

<Runtime.InteropServices.DllImport("user32.dll")>
Private Shared Function MessageBox(hWnd As IntPtr, lpText As String, lpCaption As String, uType As UInteger) As Integer
    'ここには何も記述しません。
End Function

Declare版は次のようになります。(長いので途中で改行しています。VB2008以前で実行する場合は、改行しないか、行継続文字を使用してください。)

VB2005 VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019


Private Declare Auto Function MessageBox Lib "user32.dll" (
    hWnd As IntPtr, lpText As String, lpCaption As String, uType As UInteger) As Integer

見比べると構文は違いますが、だいたい同じことが書いてあるのがわかると思います。

Declareで定義するとSharedがなくても共有メンバーになるのでSharedは不要です。この例には登場しませんが、DllImportのEntryPointに相当する Alias というキーワードもあります。また、End Function / End Sub は無用になります。