概要
エクセル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
まとめ
かなり前に同じ様なことをしましたがすっかり忘れています。ということで、今回は考え方を残しておくことにしました。