エクセル VBAで VSフォームアプリ操作

概要

 エクセルVBAから、Visual Studio フォームアプリケーションを操作する方法について確認しました。

 次の動画では、エクセルVBAから、C#フォームのボタンをクリックする処理 と テキストボックス・チェックボックスの内容を確認、変更する処理の確認を行っています。
 エクセルシートとC#フォーム上の同じ番号のボタンは、クリックすると同じメッセージを出力します。これは、エクセルVBA側から、C#フォームボタンのクリックに相当する操作を行っているためです。
 次にエクセルシートの “Formの値を取得” ボタンをクリックすると、VBA側からC#フォーム側テキストボックスの設定内容を確認し、交互に文字を入れ替えます。同様にチェックボックス2も“チェック”/“未チェック” 状態を交互に入れ替えています。読み込んだ C#フォームの値は、エクセルシートに表示しています。

  

ウィンドウハンドルによる操作

(1) ウィンドウハンドルについて

“ウィンドウハンドル” なる各ウィンドウに割り振られた管理番号を用いて各ウィンドウを操作します。
 今回のC#フォームアプリの場合、次の図の赤くマーキングした箇所の様にフォーム、ボタン、チェックボックス 等には、それぞれウィンドウ名(タイトル?)がついています。このウィンドウ名を手掛かりにウィンドウハンドルを見つけることが出来ます。

   

 “Spy++” というソフトを使用して、ウィンドウハンドルがどの様に割り振られているか確認します。私の場合、Microsoft Visual Studio Community 2019 をインストールしていますが、次の図の通り、MENUバーから起動することが出来ます。

 

 “Spy++” を起動すると、たくさんウィンドウがあるので、探す必要はありますが、C#フォームアプリが動作している場合、トップレベルの階層に “VBA_TEST” というウィンドウを見つけることが出来ます。下位の階層を展開すると次の図の様に、ボタン等のウィンドウ名も確認することができます。赤色でマークしたウィンドウ名の前に青色でマークした16進数の値が、ウィンドウハンドルになります。
 ウィンドウハンドルはプログラム起動時に割り振られるので、外部操作に利用する場合は最新の値を確認する必要があります。

   

(2) Win32 API 関数

 実際のプログラムでは、Win32 API を用いてウィンドウハンドルを取得・操作します。今回、使用している関数について記載します。

① FindWindow

 指定するクラス名・ウィンドウ名を持つトップレベルウィンドウハンドルを返します。C#フォームアプリ( VBA_TEST)を一番最初に検索する時に使用しています。

【宣言】
Declare Function FindWindow Lib “user32” Alias “FindWindowA”     (ByVal lpClassName As String, ByVal lpWindowName As String)     As Long

pClassNameウィンドウクラス名を指定
lpWindowNameウィンドウタイトルを指定
戻り値ウィンドウハンドルを返す

【使用例】クラス名が不明で、ウィンドウ名は判っている場合。
Dim hWnd As Long
hWnd = FindWindow(vbNullString, “ウィンドウ名”)

② FindWindowEx

 親ハンドル・クラス名・ウィンドウ名から子ハンドルを探します。button 等、ウィンドウ名が変わらない場合は検索しやすいですが、テキストボックス、チェックボックス等のウィンドウ名が変わる場合の検索は不向きと思います。(使える?)

【宣言】
Declare Function FindWindowEx Lib “user32.dll” Alias “FindWindowExA” _
(ByVal hwndParent As Long, ByVal hwndChildAfter As Long, _
ByVal lpszClass As String, ByVal lpszWindow As String) As Long

hwndParent親ハンドル指定
hwndChildAfter同名クラス・タイトルのハンドル検索時、“0” 指定で、Zオーダー最初のハンドルのみ検索。
Zオーダー指定で他同名ハンドルも検索。
lpszClassウィンドウクラス名を指定
lpszWindowウィンドウタイトルを指定
戻り値ウィンドウハンドルを返す

【使用例】クラス名が不明で、ウィンドウ名は判っている場合。
Dim hWnd As Long
hWnd = FindWindowEx ( hParent , 0, vbNullString , “button1” );

③ GetWindow

 ウィンドウと関係(Zオーダー等)を指定し、検索ウィンドウのハンドルを返します。“Spy++” 等で予めウィンドウ構成を把握していれば、ウィンドウ名が変わるテキストボックス等でも、対象ウィンドウを検索できると思います。

【宣言】
Declare Function GetWindow Lib “user32” Alias “GetWindow” (ByVal hwnd As Long, ByVal wCmd As Long) As Long

hwnd 基準となるウィンドウのハンドルを指定
wCmd次の例の通り、基準ウィンドウに対する関係指定
GW_CHILD    : 子ウィンドウ
GW_HWNDNEXT : 次ウィンドウ
GW_HWNDPREV : 前ウィンドウ
戻り値ウィンドウハンドルを返す

【使用例】
Dim hWnd As Long
hWnd = GetWindow ( myHwnd , GW_HWNDNEXT )

④ SendMessage 、SendMessageStr

 ウィンドウへ、指定されたメッセージを送信します。button をクリックする時、テキストボックスの値を設定する時や、反対にウィンドウ名を取得する時などにも使用しています。

【宣言】
Declare Function SendMessage Lib “user32” Alias “SendMessageA” (ByVal hwnd As Long, ByVal wMsg As Long, ByVal wParam As Long, lParam As Any) As Long

Declare Function SendMessageStr Lib “user32.dll” Alias “SendMessageA” (ByVal hwnd As Long, ByVal wMsg As Long, ByVal wParam As Long, ByVal lParam As String) As Long

hwndメッセージ送り先のウィンドウハンドルを指定
wMsgメッセージ コード指定
wParamメッセージ付加情報指定
lParamメッセージ付加情報指定
戻り値処理結果を返す。
⑤ SetForegroundWindow

 指定ハンドルのウィンドウを最前面にアクティブ化します。

【宣言】
Declare Sub SetForegroundWindow Lib “user32.dll” (ByVal ms As Long)


プログラム

(1) C#フォームアプリ

 同一アプリ内のウィンドウ構成(順番)は、Zオーダーによって確定する様です。Zオーダーはフォームにコントロールを追加した順番の様です。確認しながらプログラムを作成する為、後からコントロールを追加するとウィンドウ構成(順番)が変化し、エクセルVBA側でのプログラム修正が必要となりました。行番24~32では、C#プログラムで操作対象となるコントロールを最前面に移動し、ウィンドウ構成(順番)を一定にしています。この様にすることで、エクセルVBA側でのプログラム変更を不要にしています。
 行番35~44,行番46~56 では、チェックボックスの値が変わる際に、C#側でウィンドウ名(テキスト)を変更しています。本来、エクセルVBA側から “SendMessage” でチェック状態を取得できる様なのですが、うまくいかず、ウィンドウ名(テキスト)を取得することで対応しています。ただ、理由は判りませんが、フリーのアプリでチェックボックス状態を取得出来るものがあることを確認しました。
 行番58~61,63~66,68~71 は、ボタンクリック時にメッセージボックスを表示するものです。エクセルVBA側から、ボタンクリック相当のメッセージを送って、実行可能です。
 

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace VBA_Ope_TEST
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();

            this.checkBox1.Checked = false;
            this.checkBox1.Text = "OFF";
            this.checkBox2.Checked = false;
            this.checkBox2.Text = "OFF";

            this.button3.BringToFront();
            this.button2.BringToFront();
            this.button1.BringToFront();

            this.checkBox2.BringToFront();
            this.checkBox1.BringToFront();
           
            this.textBox2.BringToFront();
            this.textBox1.BringToFront();
        }

        private void checkBox1_CheckedChanged(object sender, EventArgs e)
        {
            if (this.checkBox1.Checked)
            {
                this.checkBox1.Text = "ON";
            }
            else {
                this.checkBox1.Text = "OFF";
            }
        }

        private void checkBox2_CheckedChanged(object sender, EventArgs e)
        {
            if (this.checkBox2.Checked)
            {
                this.checkBox2.Text = "ON";
            }
            else
            {
                this.checkBox2.Text = "OFF";
            }
        }

        private void button1_Click(object sender, EventArgs e)
        {
            MessageBox.Show("ボタン 1 がクリックされました。","button1_click",MessageBoxButtons.OK,MessageBoxIcon.Information);
        }

        private void button2_Click(object sender, EventArgs e)
        {
            MessageBox.Show("ボタン 2 がクリックされました。", "button2_click", MessageBoxButtons.OK, MessageBoxIcon.Information);
        }

        private void button3_Click(object sender, EventArgs e)
        {
            MessageBox.Show("ボタン 3 がクリックされました。", "button3_click", MessageBoxButtons.OK, MessageBoxIcon.Information);
        }

    }
}

(2) エクセルVBA

 シートに貼り付けられたボタンのクリックイベントで各処理関数を呼び出しています。

Option Explicit

’テキストボックス・チェックボックスの内容確認・変更
Private Sub CommandButton1_Click()    
    Call TEST_GetWindowCaption    
End Sub

’ボタン1クリック
Private Sub CommandButton2_Click()
    Call TEST_ClikFormButton(0)
End Sub

’ボタン2クリック
Private Sub CommandButton3_Click()
    Call TEST_ClikFormButton(1)
End Sub

’ボタン3クリック
Private Sub CommandButton4_Click()
    Call TEST_ClikFormButton(2)
End Sub

 シートのボタンクリックイベント発生時に呼び出す関数です。  

【 Sub TEST_ClikFormButton(btn) 】行番24~41
 行番31ではトップウィンドウのハンドルを取得し、行番34~36で下位層の button1 ~ 3 をウィンドウ名を指定し、それぞれのウィンドウハンドルを取得しています。行番38で 対象button のウィンドウハンドルを指定し、クリック処理に相当するメッセージを送信します。

【 Sub TEST_GetWindowCaption() 】行番45~126
 行番58でトップウィンドウのハンドルを取得しています。行番63~122のループ処理の中の行番65 もしくは 行番67 によってウィンドウを移動、ハンドルを取得しています。行番73~76 では取得したウィンドウハンドルを指定しウィンドウ名(テキスト内容等)を取得しています。
 同一アプリ内のウィンドウ構成は決まっているので、Spy++ 等で予め確認した通り、テキストボックス1(cnt=0)、テキストボックス2(cnt=1)、チェックボックス1(cnt=2)、チェックボックス2(cnt=3)の順に処理ます。行番78~112 では、各ウィンドウ(cnt 回数)に対応する処理をそれぞれ実施しています。

Option Explicit

Private Declare Function FindWindow Lib "user32.dll" Alias "FindWindowA" (ByVal lpClassName As String, ByVal lpWindowName As String) As Long
Private Declare Function FindWindowEx Lib "user32.dll" Alias "FindWindowExA" (ByVal hwndParent As Long, ByVal hwndChildAfter As Long, ByVal lpszClass As String, ByVal lpszWindow As String) As Long
Private Declare Function GetWindow Lib "user32.dll" (ByVal hwnd As Long, ByVal wCmd As Long) As Long
Private Declare Function SendMessage Lib "user32.dll" Alias "SendMessageA" (ByVal hwnd As Long, ByVal msg As Long, ByVal wParam As Long, ByVal lParam As Long) As Long
Private Declare Function SendMessageStr Lib "user32.dll" Alias "SendMessageA" (ByVal hwnd As Long, ByVal wMsg As Long, ByVal wParam As Long, ByVal lParam As String) As Long
Private Declare Sub SetForegroundWindow Lib "user32.dll" (ByVal ms As Long)

Const BM_CLICK = &HF5                   'ボタンクリック
Const WM_SETTEXT = &HC                  'ウィンドウ(コントロール)のテキストを変更
Const WM_GETTEXT = &HD                  'コントロールのキャプション・テキストをバッファにコピー
Const WM_GETTEXTLENGTH = &HE            'WM_GETTEXTの前に文字数を調べる

Const GW_HWNDFIRST = 0                  '最前面ウィンドウ検索
Const GW_HWNDLAST = 1                   '最背面ウィンドウ検索
Const GW_HWNDNEXT = 2                   '基準ウィンドウの次ウィンドウ検索
Const GW_HWNDPREV = 3                   '基準ウィンドウの前ウィンドウ検索
Const GW_OWNER = 4                      '基準ウィンドウのオーナーウィンドウ検索
Const GW_CHILD = 5                      '基準ウィンドウの子ウィンドウのトップレベルウィンドウ検索

'シート上のボタンをクリックした時に呼び出す関数
'ボタン番号を指定し、対象ウィンドウ検索・ボタンクリックメッセージ送信
Sub TEST_ClikFormButton(btn)    
    Dim hParent As Long
    Dim h_wnd(2) As Long
    Dim rtn
    Dim sht
    
    sht = ActiveSheet.Name
    hParent = FindWindow(vbNullString, "VBA_TEST")    
    SetForegroundWindow (hParent)
    
    h_wnd(0) = FindWindowEx(hParent, 0, vbNullString, "button1")
    h_wnd(1) = FindWindowEx(hParent, 0, vbNullString, "button2")
    h_wnd(2) = FindWindowEx(hParent, 0, vbNullString, "button3")

    rtn = SendMessage(h_wnd(btn), BM_CLICK, 0, 0)    
    Worksheets(sht).Cells(1, 1).Select
   
End Sub

'C#フォームアプリのトップウィンドウを検索後、
’対象ウィンドウを検索、データ内容確認・変更実施
Sub TEST_GetWindowCaption()
    
    Dim sht
    Dim hParent As Long
    Dim hwnd(7) As Long
    Dim hwnd_hex(7)
    Dim cnt
    Dim rtn
    Dim strText As String
    Dim msg

    sht = ActiveSheet.Name
    
    hParent = FindWindow(vbNullString, "VBA_TEST")
    SetForegroundWindow (hParent)
    
    'テキストボックス内容を確認し、異なる文字を設定
    cnt = 0
    Do
        If cnt = 0 Then
            hwnd(cnt) = GetWindow(hParent, GW_CHILD)
        Else
            hwnd(cnt) = GetWindow(hwnd(cnt - 1), GW_HWNDNEXT)
        End If
        
        hwnd_hex(cnt) = Hex(hwnd(cnt))
    
        msg = ""
        strText = Space(500)
        rtn = SendMessageStr(hwnd(cnt), WM_GETTEXT, Len(strText), strText)
        strText = Trim(strText)
        If strText <> "" Then strText = Left(strText, Len(strText) - 1)
        
        If cnt = 0 Then
            If strText = "Open" Then
                msg = "Closed"
            Else
                msg = "Open"
            End If
            
            rtn = SendMessageStr(hwnd(cnt), WM_SETTEXT, 0, msg)
            
        ElseIf cnt = 1 Then
            If strText = "準備中" Then
                msg = "営業中"
            Else
                msg = "準備中"
            End If
            
            rtn = SendMessageStr(hwnd(cnt), WM_SETTEXT, 0, msg)
            
        ElseIf cnt = 2 Then
            msg = strText
            
        ElseIf cnt = 3 Then
            rtn = SendMessage(hwnd(cnt), BM_CLICK, 0, 0)
        
            strText = Space(500)
            rtn = SendMessageStr(hwnd(cnt), WM_GETTEXT, Len(strText), strText)
            strText = Trim(strText)
            If strText <> "" Then strText = Left(strText, Len(strText) - 1)
        
            msg = strText
            
        Else
            Exit Do
            
        End If
        
        If msg <> "" Then
            Worksheets(sht).Cells(5 + cnt, 4).Value = msg
        End If
        
        If hwnd(cnt) = 0 Then Exit Do
        
        cnt = cnt + 1
        DoEvents
    Loop

    Worksheets(sht).Cells(1, 1).Select
    
End Sub

  

まとめ

 かなり前に同じ様なことをしましたがすっかり忘れています。ということで、今回は考え方を残しておくことにしました。