C#とExcel VBA間のデータ交換

 弊局JG6JAVは、通常運用記録とコンテスト運用もExcelを使っている。特に無線機の周波数とモードを自動的に記録することは重要だ。今までいろいろな手段でそれを行っていた。けれども時々問題発生していたのでもっと確実且つリアルタイムにその情報を取得する手段を模索していた。そこで最近頼りにしているChat-GPTに問い合わせたらlocalhost webサーバを使うという手法を提案された。そのwebサーバにExcel VBAからアクセスされたらその時点の最新情報を取得することができる。ということでさっそく組み込んでみたらストレスなく動作してくれている。IC7851RC2の全ソースコードは長いので今回取り組んだ部分のソースを何かの参考になればよいと思い公開する。

C# IC7851RC.exe IC-7851 Remote controler 無線機情報取得
// Rig Info Data Class
public static class RigInfoData
{
    public static int RunNr { get; set; }
    public static bool Run { get; set; }
    public static decimal VFO_Freq { get; set; }
    public static string LogMode { get; set; }
    public static int RFPower { get; set; }
    public static string Callsign { get; set; }
    public static string GetString()
    {
        return $"{RunNr};{Run};{VFO_Freq};{LogMode};{RFPower};{Callsign}";
    }
}
//
// Start_RigServer のRigInfoを更新する。
private void Send_RigInfo(int nr, bool run, string callsign = "")
{
    RigInfoData.RunNr = nr;
    RigInfoData.Run = run;
    RigInfoData.Callsign = callsign;
}
//
// 変更後: 戻り値を Task にし、async void は避ける
bool _isRigServerRunning = false;
CancellationTokenSource _cts = new ();
// Excel用 無線機情報サーバー アプリケーション起動時に起動
private async Task Start_RigServer()
{
    // サーバーが既に実行中の場合は起動しない
    if (_isRigServerRunning) return;
    _cts = new CancellationTokenSource();
    var token = _cts.Token;
    var listener = new HttpListener();
    listener.Prefixes.Add("http://localhost:8080/");
    try
    {
        listener.Start();
        StatusLabel.Text = "◎Rig Server Started";
        _isRigServerRunning = true;
        while (!token.IsCancellationRequested) // 停止指示がない間ループを継続
        {
            var contextTask = listener.GetContextAsync();
            await Task.WhenAny(contextTask, Task.Delay(-1, token));
            if (token.IsCancellationRequested)
            {
                break;
            }
            var ctx = contextTask.Result;
            if (ctx.Request.RawUrl == "/riginfo")
            { // アクセスされた。
                RigInfoData.VFO_Freq = VFO_Freq.Value; // 周波数表示用 NumericUpDown
                RigInfoData.LogMode = LogMode; // Log用モード名
                RigInfoData.RFPower = RI.RFP; // 出力値
                var RigInfo = RigInfoData.GetString();
                byte[] buf = Encoding.UTF8.GetBytes(RigInfo);
                ctx.Response.StatusCode = 200;
                ctx.Response.ContentType = "text/plain; charset=utf-8";
                ctx.Response.ContentEncoding = Encoding.UTF8;
                // キャッシュを無効化
                ctx.Response.Headers["Cache-Control"] = "no-store, no-cache, must-revalidate, max-age=0";
                ctx.Response.Headers["Pragma"] = "no-cache";
                ctx.Response.Headers["Expires"] = "-1";
                // 本文書き込み
                ctx.Response.OutputStream.Write(buf, 0, buf.Length);
                ctx.Response.OutputStream.Flush();
                ctx.Response.OutputStream.Close();
                ctx.Response.Close();
                this.BeginInvoke(()=>StatusLabel.Text = $"◎Rig Server Sent {RigInfo}");
            }
        }
    }
    catch (OperationCanceledException)
    {
        // キャンセルによる例外は無視する
    }
    catch (Exception ex)
    {
        // その他のエラー処理
        StatusLabel.Text = $"Start_RigServer : Error {ex.Message}";
    }
    finally
    {
        if (listener.IsListening)
        {
            listener.Stop();
        }
        listener.Close();
        _isRigServerRunning = false;
        _cts.Dispose();
    }
}
//
// CancellationTokenSource をキャンセルし、await 中の GetContextAsync を中断させる
public void Stop_RigServer()
{
    if (_isRigServerRunning && _cts != null)
    {
        _cts.Cancel();
    }
}
//
// Run status 設定
private void Set_RunStatus(bool sw, bool post = true)
{
    Running = sw;
    RunFreq = Running ? VFO_Freq.Value : 0M;
    if (post) { RunExcelMacro(1, Running); }
}
//
// Excel マクロ実行
void RunExcelMacro(int RunNr, bool args1 = false, string args2 = "")
{
    Excel.Application xl = (Excel.Application)Marshal.GetActiveObject("Excel.Application");
    if (xl == null) return;
    var macroName = RunNr switch 
    {
        1 => "StatusCheck",
        2 => "RcvRigInfo",
        _ => ""
    };
    if (macroName.Length < 1) { return; }
    try
    {
        xl.Run(macroName, RunNr == 1 ? args1 : args2);
    }
    catch (Exception ex)
    {
        StatusLabel.Text =
            $"[RunExcelMacro] {macroName}({args1},{args2}) Error: {ex.Message}";
    }
}

この方法だとExcel VBAからアクセスするだけでよいのが利点だ。当然だが動作確認は、WEBブラウザでもできる。

Excel VBA jg6jav_log.xlsm 情報取得部分
' C# 起動中の RigServerにアクセス
Function HttpGetUTF8(url As String) As String
    Dim xml As Object
    Set xml = CreateObject("MSXML2.XMLHTTP")
    xml.Open "GET", url, False
    xml.send

    Dim stream As Object
    Set stream = CreateObject("ADODB.Stream")
    stream.Type = 1 'binary
    stream.Open
    stream.Write xml.responseBody
    stream.Position = 0

    stream.Type = 2 'text
    stream.Charset = "UTF-8"

    HttpGetUTF8 = stream.ReadText
    stream.Close
End Function
'
' RigServer で取得したデータを必要データに転記
Sub GetRigInfo(Optional Ret As Boolean = False)
    Dim txt As String
    txt = HttpGetUTF8("http://localhost:8080/riginfo")
    Dim Rcv As Variant
    Rcv = Split(txt, ";")
    Dim i As Integer
    Dim RigInfo As Range
    Set RigInfo = Sheets("Settings").Range("RigInfo")
    With RigInfo.Cells(1, 1)
        For i = 0 To 5
            .Offset(i, 0).Value = Rcv(i)
        Next
    End With
    Dim Nr As Integer
    Dim sta As Boolean
    Dim Callsign As String
    With RigInfo
        Nr = .Cells(1, 1).Value '// マクロ選択番号
        sta = .Cells(2, 1).Value '// Bloolean
        rigFreq = .Cells(3, 1).Value '// 周波数情報
        rigMode = .Cells(4, 1).Value '// モード
        rigPower = .Cells(5, 1).Value '// 出力
        Callsign = .Cells(6, 1).Value '// メモリーのコールサイン
    End With
    rigBand = Band(rigFreq)
    If Ret Then Exit Sub
    Select Case Nr
    Case 1
        Call RcvRigInfo(Callsign)
    Case 2
        Call StatusCheck(sta)
    End Select
End Sub
'
'---// IC7851RCへSendkey //---
Sub IC7851(SKey As String)
    Dim AC As Range
    Set AC = ActiveCell
    On Error GoTo ExitIC7851
    AppActivate "IC-7851"
    SendKeys (SKey)
    SendKeys ("{NUMLOCK}")
ExitIC7851:
    AppActivate Application.Caption
    Err.Clear
    On Error GoTo 0
    AC.Activate
End Sub

 コンテスト運用中は特にCWの自動送出を行っている。その送出文字列は、Excelからクリップボード経由で受け取り送出している。

C# Excel VBA からの SendKeyで反応する
// KeyDown イベント 部分
private void Form1_KeyDown(object sender, KeyEventArgs e)
{
    //
    //
    var Ctrl = e.Control;
    var Alt = e.Alt;
    if (Ctrl && e.KeyCode == Keys.V) // Ctrl + V 貼付に同じ
    {
        // ExcelでClipboardに記録された文字列データをCW送出させる
        IDataObject data = Clipboard.GetDataObject();
        if (data == null) { return; }
        if (data.GetDataPresent(DataFormats.Text))
        {
            var SendText = (string)data.GetData(DataFormats.Text);
            if (SendText.StartsWith("#F5:"))
            {
                var lng = SendText.Length - 4;
                F5msg.Text = SendText.Substring(4, lng); // TextBox F5msg に転記
            }
            else
            {
                SendCW(SendText); // CW電文送出
            }
        }
    }
    if (Alt) // Alt + Keys
    {
        switch (KeyTop)
        {
            case Keys.Y:
                Set_RunStatus(true, false);
                return;
            case Keys.N:
                Set_RunStatus(false, false);
                return;
            default:
                return;
        }
    }
    //
    //
}
Excel VBA CW 送出用文字列渡し部分
Dim Clip As New DataObject
With Clip
    .SetText MSG 'CW送出文字列
    .PutInClipboard
End With
AppActivate "IC-7851" 'IC7851RC2.exeをアクティブにする
SendKeys ("^V") 'Ctrl+V(貼付)

Excel 側から IC7851RC2.exe への文字列情報渡しは、CW送出文字列の他はないので他の設定変更はSendKeyで済ませいる。出力変更、フィルター設定、Band ScopeのEdge設定などができる。


 ログもMySQLなどに移行してすべてC#で構成するとよいのだろうけれどもExcelは何かと使い勝手が良いのでなかなか思い切れないでいる。だからといってアマチュア無線局の皆さんの間で多く使われているアプリケーションを使う気にはなれない。

コメント