【道楽】株の売買システムを自動的に実行する[3/4]

2022年11月21日

~ 続き ~

マケスピ2の起動メソッドを作ったので、次はエクセルを動かす話

仕様はシンプルだが・・・・

  • エクセルワークブックを開く
  • マクロを実行する
  • 接続許可、発注許可操作をする
  • エクセル終了処理はしない。クローズはVBA側で行う

実際やると面倒の連続だったので愚痴を書いておく

エクセルワークブックを開く

検索すると、Microsoft.Office.Interop.Excel を使うやり方が良く出てくる
だがこれだとマケスピ2のアドインが読み込まれない
あとからロードできるらしいが、そもそもこれを使う理由がないことに気付いた
自動売買の処理は全部VBAなんだから、単にエクセルが起動すればそれでいいのだ

なので process.start() で起動する

マクロを実行する

マクロはショートカットキーで起動する。

なのでエクセルファイル側の設定で、VBAの起動ショートカットキーに ctrl+m を設定しておく必要がある。
ちなみに

クイックアクセスツールバーにマクロ実行ボタンを置いて押す方式も試したが、何かの拍子でボタンが消えたりして挙動不審だからやめた

接続、発注許可操作をする

エクセルを起動しただけでは発注はできない。
RSSに接続して発注を許可するには、マケスピ2アドインのリボンメニューにあるボタンを押す必要がある
このボタンが曲者で・・・・・

まずUI Automationから押せなくてハマった

ボタン名がステータスを兼ねていて、UI要素としてはトグルボタンの分類らしい。ボタンの押下はInvokePatternではなくTogglePatternが必要だった

それから、リボンメニューの操作が一筋縄ではいかず。
誰もがそうするようにリボンメニューは自動で隠す設定にしている
すると
「リボンメニューを出して」「所望のボタンを押す」という連続した操作が必要になる
この間にPCをちょっとでも弄るとリボンメニューがお隠れになってしまうので具合が悪い。
そんなリボンUIの都合は知らんしどーでもいいと思ったのと
自動実行中でもPCは弄りたいし
リボンメニューを常時表示するのは無粋だし
結局
クイックアクセスツールバーに置いたボタンを押すことにした
エクセルファイル側の設定で、マーケットスピード2の接続ボタンと発注ボタンを右クリックして、クイックアクセスツールバーに追加しておくのが必要

ちなみにマクロ実行ボタンが勝手に消えてる場合でも、コイツは消えないのが不思議なんだぜ・・・・

その他

マクロを実行してから接続/発注許可する順で書いてるけど、特に順序の制約はないです。

ただし、新たなエクセルを開く度に発注が不許可になるのがRSSの仕様なので、必要なエクセルを全部開いてから接続/発注許可するのが良いと思われる。俺のはVBA動かすと順次別のエクセルファイルをオープンしていくので、マクロ実行→ 暫くしてエクセルが全部開いてから接続/発注許可、としてますねー。


とまあ、これでよーやく

マケスピ2とエクセルを動かせるよーになったので
あとは日付とか時刻に合わせて呼び出すだけだぜ😃!!

~ 続く ~

// Copyright 2022 maxsan
// Licensed under the Apache License, Version 2.0
using System;
using System.Diagnostics;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Threading;
using System.Windows.Automation;

namespace TradeAutomation
{
    internal class ExcelLib
    {
        public enum MacroExecMethod
        {
            shortcutKey,
            quickAccessToolBar
        }

        public string bookname;
        public string macroName = "app";
        public MacroExecMethod macroExecMethod = MacroExecMethod.shortcutKey;

        [DllImport("user32.dll")]
        private static extern IntPtr FindWindow(string lpClassName, string lpWindowName);

        /// <summary>
        /// エクセルを起動する
        /// </summary>
        /// <returns></returns>
        public int Launch()
        {
            var methodName = MethodBase.GetCurrentMethod().Name;

            Disp(methodName, "開始");

            var hWnd = GetWindowHandleByName(bookname + " - Excel", 0);
            if (hWnd != IntPtr.Zero)
            {
                Disp(methodName, bookname + " のウィンドウは既にある");
                return -1;
            }

            // エクセル起動
            // interopだとaddinが読み込まれないのでprocess.startを使用
            // 引数でブック名を渡すとマクロが動かないのでブック名を直指定
            try
            {
                var proc = new Process();
                proc.StartInfo.FileName = bookname;
                proc.Start();
                //proc.WaitForInputIdle();
            }
            catch (Exception e)
            {
                Disp(methodName, "エクセル起動で例外が発生");
                Console.WriteLine(e.ToString());
                return -1;
            }

            // ウインドウ生成確認・待ち
            hWnd = GetWindowHandleByName(bookname + " - Excel", 300);
            if (hWnd == IntPtr.Zero)
            {
                Disp(methodName, "エクセル起動後、ウィンドウが生成されない");
                return -1;
            }

            Disp(methodName, "起動後ウェイト");
            Thread.Sleep(30000);

            Disp(methodName, "終了");

            return 0;
        }

        /// <summary>
        /// エクセルのマクロを実行する
        /// </summary>
        /// method1:ctrl + m をマクロショートカットとして起動する
        /// method2:クイックアクセスツールバーにマクロボタンを追加して押す
        /// <returns></returns>
        public int StartMacro()
        {
            UIAutomationLib ui = new UIAutomationLib();
            IntPtr hWnd;
            InvokePattern ivpat;

            var methodName = MethodBase.GetCurrentMethod().Name;

            Disp(methodName, "開始");

            hWnd = GetWindowHandleByName(bookname + " - Excel", 300);
            if (hWnd == IntPtr.Zero)
            {
                Disp(methodName, bookname + " のウィンドウがない");
                return -1;
            }

            var el_excel = AutomationElement.FromHandle(hWnd);
            if (el_excel == null)
            {
                Disp(methodName, bookname + " のUIA要素がnull");
                return -1;
            }

            switch (macroExecMethod)
            {
                case MacroExecMethod.shortcutKey:
                    // method1
                    // マクロにショートカットキーctrl + mを登録しておくこと
                    // TODO:sendkeyは不確実なのでリトライが欲しい
                    Disp(methodName, "マクロショートカットキーを送信");
                    ui.Keyin(true, el_excel, "^m");
                    break;

                case MacroExecMethod.quickAccessToolBar:
                    // method2
                    // クイックアクセスツールバーにマクロボタンを置いておくこと
                    // 動作が不安定
                    var el_qatbar = ui.FindElementByNameAndLCType(el_excel,
                                        "クイック アクセス ツール バー", "ツール バー");
                    if (el_qatbar == null) return -1;

                    var el = ui.FindElementByNameAndLCType(el_qatbar,
                                        "app", "ボタン");
                    if (el == null) return -1;

                    ivpat = (InvokePattern)el.GetCurrentPattern(InvokePattern.Pattern);
                    ivpat.Invoke();
                    break;

                default: return -1;
            }

            Disp(methodName, "終了");

            return 0;
        }

        /// <summary>
        /// MarketSpeed II RSSに接続し、発注を許可する
        /// クイックアクセスツールバーにボタン登録が必要
        /// 戻り値がOKになるまで何度もコールする作り
        /// </summary>
        /// <returns></returns>
        public int RssConnectAndEnableOrder()
        {
            UIAutomationLib ui = new UIAutomationLib();
            IntPtr hWnd;
            TogglePattern togpat;
            int ret = 0;
            var methodName = MethodBase.GetCurrentMethod().Name;

            Disp(methodName, "開始");

            hWnd = GetWindowHandleByName(bookname + " - Excel", 10);
            if (hWnd == IntPtr.Zero)
            {
                Disp(methodName, bookname + " のウィンドウがない");
                return -1;
            }

            var el_excel = AutomationElement.FromHandle(hWnd);
            if (el_excel == null)
            {
                Disp(methodName, bookname + " のUIA要素がnull");
                return -1;
            }

            // クイックアクセスツールバー捜索
            var el_qatbar = ui.FindElementByNameAndLCType(el_excel,
                                "クイック アクセス ツール バー", "ツール バー");
            if (el_qatbar == null)
            {
                Disp(methodName, "クイックアクセスツールバーのUIA要素がnull");
                return -1;
            }

            // MarketSpeedII 未接続ボタン捜索
            var el = ui.FindElementByNameAndLCType(el_qatbar,
                                "接続中\n", "ボタン");

            if (el == null)
            {
                el = ui.FindElementByNameAndLCType(el_qatbar,
                                    "未接続\n", "ボタン");
                if (el == null)
                {
                    Disp(methodName, "接続中/未接続ボタンがない");
                    return -1;
                }

                // ボタントグル(toggle)
                Disp(methodName, "接続ボタン押下");
                togpat = (TogglePattern)el.GetCurrentPattern(TogglePattern.Pattern);
                togpat.Toggle();
                Disp(methodName, "ウェイト");
                Thread.Sleep(5000);

                ret = -1;
            }

            // MarketSpeedII 発注不可ボタン捜索
            el = ui.FindElementByNameAndLCType(el_qatbar,
                                "発注可\n", "ボタン");

            if (el == null)
            {
                el = ui.FindElementByNameAndLCType(el_qatbar,
                                    "発注不可\n", "ボタン");
                if (el == null)
                {
                    Disp(methodName, "発注可/発注不可ボタンがない");
                    return -1;
                }

                // ボタントグル(toggle)
                Disp(methodName, "発注ボタン押下");
                togpat = (TogglePattern)el.GetCurrentPattern(TogglePattern.Pattern);
                togpat.Toggle();
                Disp(methodName, "ウェイト");
                Thread.Sleep(5000);

                ret = -1;
            }

            Disp(methodName, "終了");

            return ret;
        }


        /// <summary>
        /// ウィンドウ名からハンドルを取得する
        /// </summary>
        /// <param name="name"></param>
        /// <param name="waitspecInSecond"></param>
        /// <returns></returns>
        private IntPtr GetWindowHandleByName(string name, int waitspecInSecond)
        {
            IntPtr hWnd = IntPtr.Zero;
            int i = 0;

            while (hWnd == IntPtr.Zero)
            {
                hWnd = FindWindow(null, name);
                if (++i > waitspecInSecond) return hWnd;
                Thread.Sleep(1000);
            }
            return hWnd;
        }

        /// <summary>
        /// コンソール出力(日時つき)
        /// </summary>
        /// <param name="msg"></param>
        private void Disp(string methodName, string msg)
        {
#if true
            Console.WriteLine(DateTime.Now + " [" + this.GetType().Name
                                                + "] " + methodName + " : " + msg);
#endif
        }


#if false
        /// <summary>
        /// リボンUIを使う版
        /// 味わい深いのでとっておく
        /// </summary>
        /// <returns></returns>
        public int bind_rss_ribbon()
        {
            UIAutomationLib ui = new UIAutomationLib();
            IntPtr hWnd;
            TogglePattern togpat;

            int i = 0;
            hWnd = (IntPtr)0;
            while (hWnd == (IntPtr)0)
            {
                hWnd = FindWindow(null, bookname + " - Excel");
                if (hWnd!=(IntPtr)0) break;
                Thread.Sleep(1000);
                if (i++ > 300) return -1;
            }

            var el_excel = AutomationElement.FromHandle(hWnd);

            // 接続警告ダイアログBox対応
            // TODO タイミングによっては(エクセルが先に起動してるとか)ログインして云々のダイアログBOXが出るので
            // OKボタン押さないといけない

            // MarketSpeedII リボンタブ捜索
            var el_ribbontab = ui.FindElementByNameAndLCType(el_excel,
                                "マーケットスピード II", "タブ アイテム");

            // MarketSpeedII リボンタブを選択(リボンを選択・表示させる)してriboon階層を捜索
            var selpat = (SelectionItemPattern)el_ribbontab.GetCurrentPattern(SelectionItemPattern.Pattern);
            selpat.Select();
            Thread.Sleep(3000); // wait for ribbon appear
            var el_ms2cc = ui.FindElementByNameAndLCType(el_excel,
                                "マーケットスピード II", "カスタム コントロール");



            // MarketSpeedII 未接続ボタン捜索
            var el = ui.FindElementByNameAndLCType(el_ms2cc,
                                "接続中\n", "ボタン");

            if (el == null)
            {
                el = ui.FindElementByNameAndLCType(el_ms2cc,
                                    "未接続\n", "ボタン");
                // ボタントグル(toggle)
                // 自動的に隠すになっていると、トグル後リボンは消える
                togpat = (TogglePattern)el.GetCurrentPattern(TogglePattern.Pattern);
                togpat.Toggle();
                Thread.Sleep(5000);

                return -1;
            }

            // MarketSpeedII リボンタブを選択(リボンを選択・表示させる)してriboon階層を捜索
            // 一旦画面から消えた要素は再捜査が必要
            selpat.Select();    // トグルするとリボンが引っ込んじゃう
            Thread.Sleep(3000); // wait for ribbon appear
            el_ms2cc = ui.FindElementByNameAndLCType(el_excel,
                                "マーケットスピード II", "カスタム コントロール");

            // MarketSpeedII 発注不可ボタン捜索
            el = ui.FindElementByNameAndLCType(el_ms2cc,
                                "発注可\n", "ボタン");

            if (el == null)
            {
                el = ui.FindElementByNameAndLCType(el_ms2cc,
                                    "発注不可\n", "ボタン");
                // ボタントグル(toggle)
                // 自動的に隠すになっていると、トグル後リボンは消える
                togpat = (TogglePattern)el.GetCurrentPattern(TogglePattern.Pattern);
                togpat.Toggle();
                Thread.Sleep(5000);

                return -1;
            }

            return 0;
        }
#endif
    }
}