C# と VB.NET の質問掲示板

ASP.NET、C++/CLI、Java 何でもどうぞ

C# と VB.NET の入門サイト

Re[2]: VBAからC#にSendMessageしてもキャッチ出来ない


(過去ログ 111 を表示中)

[トピック内 8 記事 (1 - 8 表示)]  << 0 >>

■65664 / inTopicNo.1)  VBAからC#にSendMessageしてもキャッチ出来ない
  
□投稿者/ doragora (1回)-(2013/03/13(Wed) 17:30:34)

分類:[Microsoft Office 全般] 

☆やりたいこと
ExcelVBAからSendMessageで、プロセス間通信を行いたい

☆問題
・C#アプリ間(同一アプリ・別アプリ)は行えています
・ExcelVBA→C#の場合、WndProcのメッセージID振り分け直後にブレークをしてもキャッチ出来ない

☆環境
・Win7 x64
・Excel 2007
・VS 2012

☆仕様
・WM_COPYDATAメッセージを利用して、構造体の受け渡しを行う。(dwDataの値により型を指定)
・上記の検証として、文字列(Unicode)の受け渡しを行う。(dwData=0)

☆注意点
・FindWindow及び、SendMessageはAではなくW
・APIの型はStringやAnyではなくLongにし、StrPtrやVarPtrを使用
・C#のコンストラクタで出力しているウインドウハンドルと、ExcelのA2セルの値は10進で同一の為FindWindowは正常

☆C#

        public WinProc(AppSetting app)
        {
            InitializeComponent();

            this.Text = app.APIWindowName;
            this.textBox1.Text = "";

#if DEBUG
            textBox1.Text += string.Format("{0:yyyy/MM/dd HH:mm:ss} {1}{2}", DateTime.Now, this.Handle.ToString(), System.Environment.NewLine);
#endif
        }

        public delegate void receiveMsgHandler(object sender, receiveMsgEventArgs e);
        public event receiveMsgHandler recMsg;

        public void OnReceiveMsg(receiveMsgEventArgs e)
        {
            if (recMsg != null)
            {
                recMsg(this, e);
            }
        }

        public struct COPYDATASTRUCT
        {
            public IntPtr dwData;
            public UInt32 cbData;
            public IntPtr lpData;
        }
        public const int WM_COPYDATA = 0x4A;
        public const int WM_USER = 0x400;
        public const int WM_SETTEXT = 0xC;
        protected override void WndProc(ref Message m)
        {
            switch (m.Msg)
            {
                case WM_COPYDATA:
                    {
                        // 受信データ
                        COPYDATASTRUCT data = (COPYDATASTRUCT)Marshal.PtrToStructure(m.LParam, typeof(COPYDATASTRUCT));

                        //String msg = Marshal.PtrToStringUni(data.lpData);
                        byte[] bytes = new byte[data.cbData];
                        Marshal.Copy(data.lpData, bytes, 0, (int)data.cbData);
                        string msg = Encoding.Unicode.GetString(bytes);

                        // イベント
                        OnReceiveMsg(new receiveMsgEventArgs(msg));

#if DEBUG
                        textBox1.Text += string.Format("{0:yyyy/MM/dd HH:mm:ss} {1}{2}", DateTime.Now, msg, System.Environment.NewLine);
#endif
                        m.Result = new IntPtr(1);
                        break;
                    }

            }
            base.WndProc(ref m);
        }

☆Excel VBA(標準モジュール)
Option Explicit
Public Declare Function FindWindow Lib "User32.dll" Alias "FindWindowW" _
        (ByVal lpClassName As Long, _
         ByVal lpWindowName As Long) As Long
         
Public Declare Function SendMessage Lib "User32.dll" _
        Alias "SendMessageW" _
        (ByVal hWnd As Long, _
        ByVal Msg As Long, _
        ByVal wParam As Long, _
        ByVal lParam As Long) As Long

Public Type COPYDATASTRUCT
    dwData As Long
    cbData As Long
    lpData As Long
End Type

Public Const WM_COPYDATA As Integer = &H4A
Public Const WM_USER As Integer = &H400

☆Excel VBA(テストシート)
Option Explicit
Private Sub CommandButton1_Click()
    SendMsg (Sheet1.Range("D5").Value)
End Sub

Private Sub SendMsg(Msg As String)
    Dim ret As Long

    Dim hWnd As Long: hWnd = 0
    Dim sClass As String: sClass = vbNullString
    Dim sWindow As String: sWindow = "ReceiverMsg"
        
    Dim data As COPYDATASTRUCT
    Dim MsgBytes() As Byte

    hWnd = FindWindow(StrPtr(sClass), StrPtr(sWindow))
    If (hWnd = 0) Then
        MsgBox "起動してください"
        Exit Sub
    End If
    
    ' debug
    Sheet1.Range("A2").Value = hWnd
    
    MsgBytes = StrConv(Msg, vbUnicode)
    
    data.dwData = 0
    data.lpData = VarPtr(MsgBytes(1))
    data.cbData = UBound(MsgBytes)
    
    ret = SendMessage(hWnd, WM_COPYDATA, Me.Application.hWnd, VarPtr(data))
    
    ' debug
    Sheet1.Range("A1").Value = ret

End Sub

☆
不足が無いようにしたつもりですが、VB6ライクな言語(及びAPIの使用)は久しぶりのため
根本的な勘違いがあるかもしれませんが、よろしくお願いします。

引用返信 編集キー/
■65670 / inTopicNo.2)  Re[1]: VBAからC#にSendMessageしてもキャッチ出来ない
□投稿者/ Azulean (113回)-(2013/03/13(Wed) 22:54:01)
原因かどうかわかりませんが、気になった点があったのでお聞きします。

No65664 (doragora さん) に返信
> ・APIの型はStringやAnyではなくLongにし、StrPtrやVarPtrを使用

世の中によくあるサンプルでは COPYDATASTRUCT 構造体は As Any にしているパターンが多くあります。
あえて、VarPtr 路線を選んだ理由はあるのでしょうか?

一例:http://support.microsoft.com/kb/176058


ところで、C# アプリは x86? x64?
引用返信 編集キー/
■65675 / inTopicNo.3)  Re[2]: VBAからC#にSendMessageしてもキャッチ出来ない
□投稿者/ 通りすがり (3回)-(2013/03/14(Thu) 02:30:05)
 どもです。
まずは、wParam, lParam を使ったシンプルなメッセージで試されたら如何でしょうか。
もし、可能なら、C/C++ ベースで受信デバッグを行う方法もあります。
もし、ダメなら、スパイを使う、ってな方法もあります。


引用返信 編集キー/
■65676 / inTopicNo.4)  Re[2]: VBAからC#にSendMessageしてもキャッチ出来ない
□投稿者/ doragora (3回)-(2013/03/14(Thu) 03:31:39)
No65670 (Azulean さん) に返信
> 世の中によくあるサンプルでは COPYDATASTRUCT 構造体は As Any にしているパターンが多くあります。
> あえて、VarPtr 路線を選んだ理由はあるのでしょうか?
>
> 一例:http://support.microsoft.com/kb/176058

質問する前には色々試していて、もちろんAs Anyも試しています。
ByRefにするのがちょっと気持ち悪いとかその程度の事です。
AではなくWを使用しているのも同様です。
※ただExcel内部ではUnicodeなのに、APIではAnsiに(自動的に)されると条件の切り分けがしにくいと感じたこともあります。

KB拝見しました。詳細の警告部分はちょっと考えさせられましたが、サンプルでもVarPtr使っている。
>cds.cbData = Len(a$) + 1
+1する理由はnull終端分でしょうけど、CopyMemoryしたのはLen(a$)分なのですよね。
1バイト分はどうなってるんだ?って疑問に思うサンプルだと思います。

ターゲットサンプルで
>a$ = Left$(a$, InStr(1, a$, Chr$(0)) - 1)
してるので、なんとなく正常に動作すると思いますけど、+1した分は初期化されてないので、Chr$(0)で保証出来るのか疑問

> ところで、C# アプリは x86? x64?

現状C#アプリはAny CPUです。が、本番では同時に読み込むライブラリがx86であることが解っているので、最終的にはx86になります。
これは気にしてなかったので、試してみます。

No65675 (通りすがり さん) に返信
最終目標である構造体よりシンプルである文字列にしましたが、確かに数値の方が型が決まっていればシンプルですね。
LongとUInt32、C/C++、スパイも試してみます。

引用返信 編集キー/
■65677 / inTopicNo.5)  Re[3]: VBAからC#にSendMessageしてもキャッチ出来ない
□投稿者/ Azulean (116回)-(2013/03/14(Thu) 07:28:36)
No65676 (doragora さん) に返信
> >cds.cbData = Len(a$) + 1
> +1する理由はnull終端分でしょうけど、CopyMemoryしたのはLen(a$)分なのですよね。
> 1バイト分はどうなってるんだ?って疑問に思うサンプルだと思います。

0 クリアされているのかもしれませんが、私の方での動作は未確認です。


あと、もう一点確認ポイントとして出しておきます。関係なければすみません。
VS2012 を管理者として実行している場合、デバッグ実行される exe も管理者として実行された状態になるので、他の一般権限のプロセスからメッセージが届かない可能性はあります。(UAC による制限。UIPI)
VS2012 を一般権限で実行しているか?デバッグ実行以外(エクスプローラーから直接起動など)でも現象は同じかというあたりは確認していただいた方がよいと思います。
引用返信 編集キー/
■65680 / inTopicNo.6)  Re[1]: VBAからC#にSendMessageしてもキャッチ出来ない
□投稿者/ 魔界の仮面弁士 (169回)-(2013/03/14(Thu) 09:54:32)
2013/03/14(Thu) 09:56:36 編集(投稿者)

No65664 (doragora さん) に返信
> ・Excel 2007
ということは、VBA 側は 32bit 版ですね。
となると、C# 側も x86 ビルドにしておくべきでしょうね。


まずは、VBA 側について。

> Private Sub CommandButton1_Click()
Excel にはボタンが二種類あります。
フォームコントロールとActiveXコントロールのものです。
今回は、ActiveX版(MSForms.CommandButton)をお使いなのですね?


> SendMsg (Sheet1.Range("D5").Value)
VBA の文法では、戻り値を利用しないメソッドの呼び出しには
括弧をつけてはならないことになっていますが、上記で
括弧をつけているのは、わざとでしょうか?

今回は引数が一個なので、上記は構文的に
 Call SendMsg((Sheet1.Range("D5").Value))
と同じ意味になります。


> Private Sub SendMsg(Msg As String)
ここは、ByVal Msg As String としておくべきかと。


> MsgBytes = StrConv(Msg, vbUnicode)
これが最初の間違い。

VBA の String 型は、UTF16 相当の Unicode 文字列です。
それを vbUnicode 変換してしまっては、データが破損してしまいます。

この場合は、
 MsgBytes = Msg
もしくは
 MsgBytes = StrConv(Msg, 0)
などとしておくべきかと。


> data.lpData = VarPtr(MsgBytes(1))
これもマズイです。

事前に、Msg 変数の内容をチェックしておかないと、
インデックスが範囲外となってしまうでしょう。

というのも、D5セルが空だった場合、Sheet1.Range("D5").Value は
「Variant 型の Empty 値」を返します。それが String に変換されるため、
SendMsg の引数には、長さゼロの文字列が渡されることになるためです。
空データなら、何も通信せずに Exit Sub するか、あるいは
空データ用の COPYDATASTRUCT を送るための If 文を付与しましょう。


また、MsgBytes(1) という表現にも問題があります。
配列の先頭は 0 ベースのため、通常は
 data.lpData = VarPtr(MsgBytes(0))
とするべきです。

Option Base 1 にしている場合はこの限りではありませんが、
提示されたコードはそうなってはいません。
もしも 0 ベースでも 1 ベースでも動作できるようにしたいなら、
 data.lpData = VarPtr(MsgBytes(LBound(MsgBytes)))
のようなコードとなるでしょう。


> data.cbData = UBound(MsgBytes)
これも NG です。
COPYDATASTRUCT.cbData に、データ長(バイト数)を指定しているのでしょうが、
UBound 関数は、「配列の長さ」ではなく「インデックスの上限値」を指すからです。
そのため、通常の Option Base 0 相当で処理される場合には、
 data.cbData = 1 + UBound(MsgBytes)
のようなコードにしておく必要があります。
この 1 が、終端文字列 vbNullChar を指すものでは無いことに注意してください。

Option Base 1 の場合は、元のコードのままでも長さを表せますが、それならば、
 data.cbData = 1 + UBound(MsgBytes) - LBound(MsgBytes)
のようなコードにするべきです。これなら Option Base に左右されることもありません。


それから C# 側。
> public delegate void receiveMsgHandler(object sender, receiveMsgEventArgs e);
> public event receiveMsgHandler recMsg;

個人的には、
 public event EventHandler<ReceiveMsgEventArgs> recMsg;
とした方が良いと思います。

元のコードでも、文法的には間違いではありませんが、.NET においては
型名は先頭大文字の Pacal 形式とするのが一般的だからです。

(たとえば、描画イベントのハンドラーは、
paintEventHandler ではなく、PaintEventHandler ですね)
引用返信 編集キー/
■65697 / inTopicNo.7)  Re[2]: VBAからC#にSendMessageしてもキャッチ出来ない
□投稿者/ doragora (4回)-(2013/03/14(Thu) 13:19:24)
No65677 (Azulean さん) に返信
> VS2012 を管理者として実行している場合、デバッグ実行される exe も管理者として実行された状態になるので、他の一般権限のプロセスからメッセージが届かない可能性はあります。(UAC による制限。UIPI)

これでした。一般権限で起動後、プロセスのアタッチを行うことにより、キャッチ出来ました。
質問内容的にはこれで解決なのですが、魔界の仮面弁士さんのご指摘通りバグがあります。


No65680 (魔界の仮面弁士 さん) に返信

> 今回は、ActiveX版(MSForms.CommandButton)をお使いなのですね?
元々mswinsck.ocxを利用してループバック(用語が正しくない気がする)で処理を行おうとした名残です。※mswinsck.ocxがKill bitされてるのか、レジストリ変えてまでは行いかねる為断念


> > Private Sub SendMsg(Msg As String)
> ここは、ByVal Msg As String としておくべきかと。
> > MsgBytes = StrConv(Msg, vbUnicode)
> これが最初の間違い。
> > data.lpData = VarPtr(MsgBytes(1))
> これもマズイです。
> > data.cbData = UBound(MsgBytes)
> これも NG です。
ご指摘。ごもっとも。
VBは1オリジンってイメージがあったためです。
「Option Baseあったな。そういえば。」な状態です。

ご指摘通りの修正で無事メッセージの送信が出来ました。

皆様ありがとうございました。

引用返信 編集キー/
■65698 / inTopicNo.8)  Re[2]: VBAからC#にSendMessageしてもキャッチ出来ない
□投稿者/ doragora (5回)-(2013/03/14(Thu) 13:21:53)
解決コードです。

・No65680の魔界の仮面弁士さんの指摘により、C#側も修正(Pascal形式の型名・イベントハンドラーのジェネリク化)しています。
・標準モジュールは修正ありません。

☆Excel VBA(テストシート)
Option Explicit

Private Sub CommandButton1_Click()
    Call SendMsg(Sheet1.Range("D5").Value)
End Sub

Private Sub SendMsg(ByVal Msg As String)
    Dim ret As Long

    Dim hWnd As Long: hWnd = 0
    Dim sClass As String: sClass = vbNullString
    Dim sWindow As String: sWindow = "ReceiverMsg"
        
    Dim data As COPYDATASTRUCT
    Dim MsgBytes() As Byte

    If (Len(Msg) = 0) Then
        MsgBox "送信文が未入力"
        Exit Sub
    End If

    hWnd = FindWindow(StrPtr(sClass), StrPtr(sWindow))
    If (hWnd = 0) Then
        MsgBox "起動してください"
        Exit Sub
    End If
    
    ' debug
    Sheet1.Range("A2").Value = hWnd
    
    'MsgBytes = StrConv(Msg, vbUnicode)
    MsgBytes = Msg  ' StringのExcel内部文字コードはUnicode
    
    data.dwData = 0
    data.lpData = VarPtr(MsgBytes(LBound(MsgBytes)))
    data.cbData = UBound(MsgBytes) - LBound(MsgBytes) + 1   ' +1は終端文字列 vbNullChar を指すものでは無い
    
    ret = SendMessage(hWnd, WM_COPYDATA, Me.Application.hWnd, VarPtr(data))
    
    ' debug
    Sheet1.Range("A1").Value = ret

End Sub




解決済み
引用返信 編集キー/


トピック内ページ移動 / << 0 >>

このトピックに書きこむ

過去ログには書き込み不可

管理者用

- Child Tree -