ささいなことですが。

Windowsアプリテスト自動化ライブラリFriendly開発者の日記です。

Facebookに宣伝出してみた

Codeerも一応Facebookページ作ってます。たまに写真とかアップしてます。「いいね」押していただけるとありがたいっす!
https://www.facebook.com/codeer.soft

Facebookからちょこちょこ「宣伝だしませんか?」的な通知が来てて気になってたんですよね。どれくらい効果あるのかなーって。ちょうど勉強会開催するのでやってみました。

効果あった?

効果あったのかどうかはわからんw。勉強会でアンケートとればよかった。クリックしてくれた人が来てくれたかどうかわかんないんですよね。(でも多分来てくれた人はそれ以外のソースな気がする)

金額

1000円かららしい。それで日数を減らすか、金額を増やすとリーチ数が増えるらしい。あと次の対象も絞ると恐らく表示される回数とか増えるんかなーと。

今回実験した対象

  1. 大阪在住の18-65才
  2. 大阪在住の18-65才でページにいいねした人
  3. 東京在住の18-65才でページにいいねした人とその友達

大阪の方は①やってて、途中で「いや、それは広すぎてイマイチやろ」って思って②もやりました。つまり2000円払ってます。東京の方は③だけで1400円分やりました。なんだけど、実は東京のは設定間違えてたんですよね・・・。

結果

いやいや、30クリックに3000円とか高すぎやしw

若干掲載期間に差はあるものの「ページにいいねした人とその友達」がやっぱり一番効果高いかなーって感じですね。
f:id:ishikawa-tatsuya:20190319230322p:plain
f:id:ishikawa-tatsuya:20190319230505p:plain
しかも東京にしたと思ったけど、間違えて大阪在住の人にしてた・・・。
f:id:ishikawa-tatsuya:20190319230652p:plain

大阪

15人枠に対して13人申し込みキャンセル2名
friendly.connpass.com

東京

20人枠に対して12人申し込みキャンセル4名
friendly.connpass.com

皆様ご参加ありがとうございました!

反省

ページに「いいね」が少ない

「いいね」増やすともっと効果あったかも。マイナーページなんで。100人くらいなんですよねw。ていうか真面目にもっと更新しないとダメですね。後せめてHPからリンク張っておこう。

.NetCoreから.NetFrameworkのdllを普通に参照できた・・・

え?これって常識だったの?僕が知らんかっただけ?

きっかけはこのブログ

qiita.com
えーと、.NetCore3でWinFormsとかWPFつくるコマンドってどうだったけなー。って感じでググってたら、こんなブログがありました。サーとみてると、「.NET Frameworkライブラリの使用」ってのがありました。えええ?どういうこと?なんでそんなことできんの?だって、objectクラスの実装されているdll別々やで・・・

あ...ありのまま 今 起こった事を話すぜ

な… 何を言っているのか わからねーと思うが 
おれも 何をされたのか わからなかった…
↑書きたかっただけ。

とりあえず、WinFormsでCore3.0とFrameworkのexe作りました。

f:id:ishikawa-tatsuya:20190302164246p:plain
両方ともWinFormsのアプリですね。
こんなコードを入れて、objectとFormの入ってるアセンブリのパスを表示させてます。

var text = new TextBox { Multiline = true, Dock = DockStyle.Fill };
text.Text = string.Join(Environment.NewLine, new[]
{
    typeof(object).Assembly.Location,
    GetType().BaseType.Assembly.Location
});
Controls.Add(text);

f:id:ishikawa-tatsuya:20190302164548p:plain
C:\Windows\Microsoft.NET\Framework\v4.0.30319\mscorlib.dll
C:\Windows\Microsoft.Net\assembly\GAC_MSIL\System.Windows.Forms\v4.0_4.0.0.0__b77a5c561934e089\System.Windows.Forms.dll

f:id:ishikawa-tatsuya:20190302164527p:plain
C:\Program Files\dotnet\shared\Microsoft.NETCore.App\3.0.0-preview-27122-01\System.Private.CoreLib.dll
C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App\3.0.0-alpha-27128-4\System.Windows.Forms.dll

ここまでは、あたり前な話。.NetFrameworkはMicrosoft.NET以下、Core3.0はdotnet以下のアセンブリを参照してますね。

.NetCoreから.NetFrameworkのアセンブリを参照

え?マジで?普通にできたよ。これって常識やったの?
f:id:ishikawa-tatsuya:20190302165620p:plain
これ実行したらどうなるの?
ってことで、Core3.0のexeからFrameworkのFormを呼び出してみました。

//これはCore3.0のWinForms
public partial class CoreForm : Form
{
    public CoreForm()
    {
        InitializeComponent();

        Text = "DotNetCore3";

        var text = new TextBox { Multiline = true, Dock = DockStyle.Fill };
        text.Text = string.Join(Environment.NewLine, new[]
        {
            typeof(object).Assembly.Location,
            GetType().BaseType.Assembly.Location
        });
        Controls.Add(text);

        var button = new Button { Text = "Show Framework", Dock = DockStyle.Bottom};
        button.Click += (_, __) => 
        {
            //FrameworkのFormを呼び出し
            using (var framework = new FrameworkForm())
            {
                framework.ShowDialog();
            }
        };
        Controls.Add(button);
    }
}

.NetFrameworkのFormのアセンブリの参照がDotNetCoreの方に変わった!

f:id:ishikawa-tatsuya:20190302170157p:plain

これって実質.NetStandardなのでは?

これって正式仕様なの?よくわかんないなー。

Test Assistant Pro リリース!

Test Assistant Pro っていうツールをこっそり作っていました。
https://www.codeer.co.jp/Tools/TestAssistantPro

Codeer初の有料ツールです。実装補助ツールという位置づけです。(キャプチャリプレイ的なこともできますが、そういうツールとちょっと違う感じです)実装補助ツールなんで結局C#とFriendlyの理解は必要なのですw。
結局UIレベルのテストで価値出るくらいのものを作ろうを思ったらプログラムで制御は必須になってくるんですよね・・・。
でもFriendlyユーザーの皆さんは今まで手書きでDriverとScenario書いていたと思うのですが、その大部分を自動でサクッと作れるようになります。Codeer内部でも使っているのですけど、超便利です。

比較表

「比較表書いて」ってよく言われますけど、客観的に書くのは難しいですよね。(だって自分たちで作ったツールが使いやすいに決まってるじゃん)Codeerメンバーで某社ツールと使い比べた感想はこんな感じでした。とは言え、某社ツールから手書きFriendlyに乗り換えたお客さんもいますよ。

初期学習難易度 カスタマイズ プロジェクトごとの調整 作成速度 ソース可読性 メンテ効率 動作確実性 実行速度 テスト実行時ライセンス
某社ツール 不要 必要
TestAssistantPro 高※1 必要 不要

※1 Friendlyの学習コストも入れてます。

ツール詳細

詳細はこちらに書いてるので、ご興味ある人は是非見てください。(日本語もあります)ダウンロードしてREADMEに沿って自分でトライすることもできます。(ライセンスは必要な方はお問い合わせいただくと、トライアルライセンスを発行いたします)
github.com

Friendlyと同じくカスタマイズできるってとこがポイント高いかなーと思ってます。

  • 特殊なコントロールへの対応
  • プロジェクトごとに欲しいコード生成機能の追加

なんかも簡単にできます。

ハンズオンやります。

それで、その紹介もかねてハンズオンを大阪と東京で開催することにしました!ご興味ある方はお気軽にご参加お願いいたします。ライセンスは当日貸出ます。

大阪

2/21(木) 19:30~ 場所:Codeer
friendly.connpass.com

東京

3/10(日) 13:00~ 場所:株式会社ビズリーチさま 本社
friendly.connpass.com
ハンズオンではないですが、.NetCore3のリリースがあるんでそれに関連したネタでも話します。
3/8(金) 15:00~ インフラジスティックス様
connpass.com

Friendly for .NetCore

.NetCoreでWinFormsとWPFが動かせるようになりましたね。Friendlyで動かせるの?って聞かれますけど、今のバージョンのでは無理ですね。なので何か新しい仕組みを作らねば。ってわけでプロトタイプを作ってみました。サンプルはWinFormsがWPFでもほとんど同じです。

なんで今のFriendlyでは動かせないの?

Friendly.Windows(普通の)は以下の3ステップで内部API呼べるようにしてます。

  1. CreateRemoteThreadを使って対象プロセスで動作するスレッドを作る
  2. ホストAPIを使って、.NetFrameworkのランタイムで動作するサーバーを起動
  3. クライアント(操作側)からサーバーにAPI情報を投げてサーバーでリフレクションを実行

ネイティブのDLL公開関数も呼べるけど、それは3.を応用したものです。で、一応これは動きます。でもサーバーは.NetFramework側で動くわけで、.NetCoreのAPIは呼べません。例えば以下のコードでは開いているフォームの数は0を返します。.NetFrameworkのSystem.Windows.Forms.Application.OpenFormsの数は0で、.NetCore側の同プロパティの数が1だからです。

public void Test()
{
    using(var app = new WindowsAppFriend(Process.GetProcessesByName("CoreForms")))
    {
        //formCountは1になる
        int formCount = app.Type<Application>().OpenForms.Count;
    }
}

Friendly.DotNetCore(プロトタイプ)

というわけでFrienldy.Windowsは使えません。新しい Friendly.DotNetCore を作る必要があるわけです。起動しているプロセスにアタッチするのは多分無理なんで、特殊な起動方法で起動させたプロセスを操作するという方法にしてみました。こんな感じのコードで操作できます。

using Friendly.DotNetCore;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Codeer.Friendly.Dynamic;
using System.Windows.Forms;

namespace ExecuteTest
{
    [TestClass]
    public class CoreTest
    {
        [TestMethod]
        public void Test()
        {
            //特殊な方法でexe起動
            var uri = "http://localhost:8081/";
            var target = @"c:\Test\CoreForms.exe";
            var entry = "CoreForms.Program.Main";
            WinFormsStarter.Start(uri, target, entry);

            //アタッチ
            using (var app = new DotNetCoreAppFriend(uri))
            {
                //.NetCoreのApplicationを操作
                var form = app.Type<Application>().OpenForms[0];
                //ラベル書き換え
                form.label1.Text = "Hello Friendly!";
            }
        }
    }
}

実行するとこんな感じ
f:id:ishikawa-tatsuya:20190105133913p:plain

WinFormsStarterは何をやっているのか

実はそのexeを起動しているわけではなく、Friendly.DotNetCore.WinFormsStarter.exeというexeを起動しています。Friendly.DotNetCore.WinFormsStarter.exeは起動時に以下二つの仕事をします。

・指定のdll以下にあって読み込めるdllを全部読み込む
・リフレクションを実行するサーバーを起動(HTTPで通信できる)

イメージとしてはこんな感じです。
f:id:ishikawa-tatsuya:20190105135713p:plain

まだお試しです。

まだネタでやってみたレベルです。(meetup app osaka@3で披露するために作った感じ)
meetupapp.connpass.com
コードはGitHubに置いています。正式版になったらガラッと変わると思います。そもそも操作する側も.NetCoreなのかとか。(今は操作する側は.NetFramework)
.NetCoreのWinForms自体もまだプレビューなんで、こっちが正式になったあたりを目指して設計していきます。
github.com

真夜中に気持ちよくギターを弾きたい!

f:id:ishikawa-tatsuya:20181007214750j:plain
脈絡もなく、突然ギターネタです。
一年に一回くらい「真夜中に気持ちよくギターを弾くためのシステム」を構築したくてたまらなくなります。そして一応構築するもののやっぱり使わなくなって、最終的にはエレキ生音でペンペン弾くようになってしまうのです。しかし!今回は結構満足度高いのができたのでブログ書くことにしました。ポイントは

  • ワイヤレス
  • 満足感のある音

音に関しては感じ方は人それぞれなので僕は満足しているというものですw

MV50(アンプ)

www.youtube.com
これは去年買ってたんですが、チューブ感あって凄い気に入ってます。コスパ最高。アッテネータもついてて、小さいボリュームでアンプで歪ませた音も出せます。ライン出力にはキャビネットシミュレータもついてて結構いい感じの音でヘッドホンでも聞けます。が、もう少し改善したかったんですよねー。

BluBox(キャビネットシミュレータ)

WIRED MUSIC|ワイアードミュージック|Bluguitar-ブルギター
色々と検索したのですが、これがいいかなーと思いました。ライン出力でこの音出せるって最高!
www.youtube.com
www.youtube.com
アンプのスピーカーアウトからの出力を入れることができる。しかもチューブアンプの場合はTHRUってとこから出た出力をダミーロードに繋げば音の劣化なくキャビネットシミュレータにいれることができると!この辺はややこしい話なのですが、チューブアンプはキャビネットつながないで使ったら壊れるらしいのです。
guitar-hakase.com
今回使っているMV50はNutubeというなんちゃってチューブアンプでしかもプリアンプだけでアンプ部はソリッドなんでダミーロードもいらないようです。(コルグに問い合わせたので間違いない)

で、肝心の音ですが、いい感じになります(語彙力)

微妙な表現ですが、ギターとアンプに合うタイプのキャビネットを選んで、マイクの位置を調整するとMV50付属のキャビネットシミュレータより求めている音に近づきます。この辺は空間系的なエフェクトとイコライジングの合わせ技なのかな?その空間系のエフェクトも昔ながらのってのではなく、「Impulse Response」というのを使っているようです。宅録界隈では数年前からメジャーになってきた技術とのこと。それからMV50付属のものより、高音にキラキラ感があります(キンキンって感じではなく気持ちい感じの)低音弦もゴリって感じになって、歪ませた音で弾くときは特に気持ちいい感じになりますね。これはホントに想像ですが、スピーカーアウトから出した音を使っているのが効いてるのではないかなーと思ってます。(根拠なしです。すみません。)とは言えほんとに自己満足な話でこの差は聞く人によると思います。僕は良くなったと思いますが、おそらくうちの奥さんとかに聞かせたら「違いがわからん」とか言うと思います。
それから少しデメリットもあって、この筐体を通すことによって少しホワイトノイズが乗ります。クリーントーンで静かに弾くときに若干気になるかなーって程度です。これもどんな音か調べているときに気になっているのであって、弾くことに集中したら気にならないとは思います。

それと電源ついてなくて、自分で買わないとダメなんですが以下の仕様らしいっす。
[DC9~18V、200mA、センターマイナス]
最初BOSSの9V使ってたんですが、18V使ったらもっと良くなるんじゃね?って思ってモリダイラ楽器の18V-400mAを買ってきて使いました。あんまり電気詳しくないんで400m?イケんの?って思ったんですが楽器屋の店員さんが「200m以上なら大丈夫ですよ」っていうからそうなんだーって感じで。でも使ってみたら、大きい音出すと、LED点滅して音でなくなるw。これ内部でASSERT的な状態になってるやろw。LED点滅なんて説明書に書いてない。回路壊れたら困るのでBOSSの9Vに戻しました。ホントの原因は18Vなんか400mAなのかわかりません。問い合わせ中です。でも18Vの方が確かに少し太くていい音になりました。まあ少しだけですが。

Line6 RELAY G10(ギターとアンプ間のワイヤレスシステム)

www.youtube.com
ギターにシールド刺さってないとほんとに楽。抜き差しも楽だし、引っかからないしね。家の中って普通のスタジオより狭いし物多いしで特に思います。実はかなり前からここを電波で飛ばすシステムはあってプロとかステージで使ってたんですが、家庭で導入可能な時代になってたんですねー。
しかも普通のシールドより音いいんですよ。さらにノイズも少ない。これはなんでかっていうと、ギターから出た直後で電気入れてローインピーダンスに変えているからですね。電波で飛ばしてもノイズが乗らないし劣化も少ない。でも昔はこれやると「なんか違うなー」な音になっていたのですが、最近はなんとケーブルトーン・テクノロジーとかいうので、シールドの音の感じを再現するとのこと。ケーブルのキャラクターをシミュレーションしようとか普通思いつきます?考えた人凄いなー。
とにかくノイズが乗らないのはありがたいですね。ヘッドホンで聞くと普通に音出すよりノイズが気になるんですよねー。
ちなみにほんとはBOSSのやつが欲しかったけど、人気すぎて年内手に入らなそう。こっちの方が持ち運び便利そうです。
www.boss.info

ATH-DWL550(ヘッドホン)

www.audio-technica.co.jp
そしたら、やっぱりヘッドホンもワイヤレスにしたいでね。でもここ普通のBluetoothではダメなんですよ。めっちゃ遅延して楽器では使い物にならない。音ゲーでもダメって聞きますね。で現状では選択肢は「非圧縮2.4GHz帯デジタルワイヤレス方式」か「赤外線伝送」のどちらか。赤外線は遅延0らしいのです。
www.sony.jp
実は去年これ買っててそれなりに満足してたんですが、耳当てが固くて結局使わなくなりました。まあ音もヘッドホンの前にイコライザ入れてかなり作って妥協できるものにしてましたしね。色々挟んだからノイズも多かったし。赤外線って検索したらこれしかないんですよね。遅延なしってメリットがあるんだからもう少し高級バージョン出してもいいと思うんだけどなー。
で今年は「非圧縮2.4GHz帯デジタルワイヤレス方式」にしました。まあおそらく若干の遅延はあるとは思います。公表してないんですよね。でも気にはならないレベル。島村楽器も遅延なしって言ってるしw。だいたい、3mくらいアンプから離れて弾いても遅延とか気になりませんよね?
www.shimamura.co.jp
音は去年の赤外線のより全然いいです。最近有線のヘッドホン持ってないから有線のとは比べられませんが。(まあ同価格帯なら有線の方が良いのでしょう)でもヘッドホンにケーブルがないのは気持ちよくギター弾くうえで圧倒的にメリットです。

買ったけど使わなかったもの

www.e-earphone.jp
ヘッドホンの前にいれて、真空管の味付けを上乗せしてやろうという思い付きで買いました。一応狙い通り真空管ぽさは増したのですが、音のレンジが狭くなったかなーと。高音の美味しい部分が消えた感じ。あと、BluBoxのホワイトノイズがこれで増幅されたのもあります。今回の用途には合わなかったけど、これ自体はノイズも少ないし、何かいい使い道あるかもなーと思ってます。
www.e-earphone.jp
ホワイトノイズを消したかったんですが、これいれるとボリュームが半分くらいになってかつかなり音も悪くなりました。まあ少し予想はしてたんですが、値段的に一旦トライしてあかんかったらあきらめつくかなーと。可能性を消さないと延々考え続けることになる・・・。まあWebの評判は悪くないみたいだし、身近に必要な人がいたら1500円くらいで売ろうかなw

今年はこれで楽しみます。

そう。目的は「練習」ではなく、楽しむことなんですよね。だからケーブルとかストレスの原因になるのを取り除くことが重要だったのです。音だけ考えたら有線でってことになるのですが、音と楽さとコストのバランスでこんな感じにしました。来年はさらに新しい機材がリーズナブルに出てるといいですねー。

Friendly.Windows.NativeStandardControls2.5.0をリリースしました

Win32(MFCも含む)用のNativeStandardControlsに3年ぶりくらいに機能追加です。
メニューのユーティリティを追加しました。

NativeMenuItem

こんな感じで使います。
f:id:ishikawa-tatsuya:20180503154410p:plain

[TestMethod]
public void SampleWindowMenu()
{
    var app = new WindowsAppFriend(Process.Start(TargetPath.NativeControls));
    var main = WindowControl.FromZTop(app);

    //ウィンドウの持っているメニューから検索
    var b0 = NativeMenuItem.GetMenuItem(main, "B0");
    //クリックエミュレート
    //これは Friendly.Windows.KeyMouse の拡張メソッド
    b0.Click();

    //押すとポップアップが表示されるのでそこから取得
    var b01 = NativeMenuItem.GetPopupMenuItem(app, "B0-1");
    b01.Click();

    //さらにその先
    var b011 = NativeMenuItem.GetPopupMenuItem(app, "B0-1-2");
    b011.Click();
}

f:id:ishikawa-tatsuya:20180503161112p:plain

[TestMethod]
public void SampleContextMenu()
{
    var app = new WindowsAppFriend(Process.Start(TargetPath.NativeControls));
    var main = WindowControl.FromZTop(app);

    //右クリック
    main.Click(MouseButtonType.Right, 100, 100);

    //コンテキストメニューから
    var a00 = NativeMenuItem.GetPopupMenuItem(app, "A0-0");
    a00.Click();
}

有効無効とIdも取得できます。WM_COMMANDを使ってメッセージを送ることも可能です。

[TestMethod]
public void SampleSendMessage()
{
    var app = new WindowsAppFriend(Process.Start(TargetPath.NativeControls));
    var main = WindowControl.FromZTop(app);
    main.Click(MouseButtonType.Right, 5, 5);
    var p0 = NativeMenuItem.GetPopupMenuItem(app, "A0-0");

    //p0.Click();
    //ほとんどの場合はClickで問題ない
    //内部的に対象プロセスがタイマメッセージを拾えるようになるまで待つので
    //発生するイベントの完了を待ち合わすことができる

    //そのイベント内部で自分でメッセージループ回したりするような場合は、イベントの終了を待てない
    //そのような場合はこちらが同期をとりやすい
    if (p0.Enabled)
    {
        const int WM_COMMAND = 0x111;
        main.SendMessage(WM_COMMAND, new IntPtr(p0.Id), IntPtr.Zero);
    }

    //それからモーダルダイアログが表示される場合は抜けてくるが
    //その場合は表示されるダイアログの完了をまてば問題なく同期がとれる場合が多いが
    //変わった処理がされている場合は上記をasyncを渡して対応するとよい
}

ちなみにFriendlyの提供するSendMessageは特殊で、対象のスレッドでSendMessageを実行させるという方式になっています。別スレッド(プロセス)からSendMessageすると想定外のタイミング(対象スレッドが特定のAPI使っている最中に割り込むとか)で割り込んでトラブルが発生する場合があるからです。ダイアログが出てくる場合などはSendMessageなのに非同期で実行できます。これは対象のプロセスにSendMessageを実行させる箇所を非同期にしています。その処理が完了することはasyncオブジェクトで監視することができます。

var async = new Async();
main.SendMessage(WM_COMMAND, new IntPtr(p0.Id), IntPtr.Zero, async);

var dlg = main.WaitForNextModal();
dlg.Close();

//発生するイベントの完了を確実に待ち合わすことができる
async.WaitForCompletion();

必要?

そうなんですよ。テスト自動化用なんでIDも送信先のウィンドウもわかってるんです。実行するにはなくても良いのです。

なんで作った?

SendMessageでWM_COMMANDを送るのは確実に動作して良いのですが、逆に言えばどんな時でも実行できてしまうというのもあります。メニューが存在してなかったり、無効だったりした場合でも実行できてしまうのです。実はネイティブアプリの場合は、ここは手動でやってもらったり、別口をDLL公開関数で作ってもらったりで対応してました。あと、この方針はキーマウスエミュレートと組み合わせて使うので去年まではあんまり推奨してなかったのですよね。でも、去年キーマウスエミュレートでも同期のとれる方法を思いついて、ちょくちょく使ってるのでこちらも頑張ってみるかという流れです。

おまけ

さっきのSendMessageはこんな感じで書きたいですよね。

//これ相当のことを一発でやりたい
//if (p0.Enabled)
//{
//	main.SendMessage(WM_COMMAND, new IntPtr(p0.Id), IntPtr.Zero);
//}
p0.Execute();

それでこれをやるためにはメニューハンドルから送信先のウィンドウハンドルを引っ張ってくる必要があるのですが、(もちろん p0.Execute(main) とか引数付けたらいいんですけど、それはイマイチなのでやりたくない)でもちょっと調べた感じではメニューハンドルから送信先のウィンドウハンドルを逆引きするAPIはないようです。
それでも、@さんに聞いたところフックしてWM_INITMENUPOPUPを見張ればいいんじゃない?とのご意見をいただいたのでやってみました。(その他ご意見いただいた@さん、@さん、@さんもありがとうございます!)
Friendlyを使うとフックもこんなに簡単に書けちゃうんですよー。

[TestMethod]
public void GetLastPopupOwnerSample()
{
    //dllインジェクション
    app.LoadAssembly(GetType().Assembly);
    //フックするクラスを対象プロセス内部に生成
    var hooker = app.Type<Hooker>()();

    //ポップアップ表示
    main.Click(MouseButtonType.Right, 5, 5);

    //送信先ウィンドウハンドル取得
    IntPtr sendWnd = hooker.LastPopupOwner;
}

public class Hooker
{
    const int WM_INITMENUPOPUP = 0x0117;
    const int WH_CALLWNDPROC = 4;

    delegate IntPtr HookProc(int code, IntPtr wParam, IntPtr lParam);

    [DllImport("user32.dll")]
    static extern IntPtr SetWindowsHookEx(int hookType, HookProc lpfn, IntPtr hMod, int dwThreadId);

    [DllImport("user32.dll")]
    static extern IntPtr CallNextHookEx(IntPtr hookHandle, int nCode, IntPtr wParam, IntPtr lParam);

    [DllImport("kernel32.dll")]
    static extern int GetCurrentThreadId();

    [StructLayout(LayoutKind.Sequential)]
    struct CWPSTRUCT
    {
        public IntPtr wparam;
        public IntPtr lparam;
        public int message;
        public IntPtr hwnd;
    }

    HookProc _traceProc;
    IntPtr _idHook;

    public IntPtr LastPopupOwner { get; set; }

    public Hooker()
    {
        var threadId = GetCurrentThreadId();
        _traceProc = WindowProcHook;
        _idHook = SetWindowsHookEx(WH_CALLWNDPROC, _traceProc, IntPtr.Zero, threadId);
    }

    ~Hooker() => GC.KeepAlive(_traceProc);

    IntPtr WindowProcHook(int hookCode, IntPtr wParam, IntPtr lParam)
    {
        if (hookCode < 0)
        {
            return CallNextHookEx(_idHook, hookCode, wParam, lParam);
        }

        var msg = (CWPSTRUCT)Marshal.PtrToStructure(lParam, typeof(CWPSTRUCT));
        if (msg.message == WM_INITMENUPOPUP)
        {
            LastPopupOwner = msg.hwnd;
        }
        return CallNextHookEx(_idHook, hookCode, wParam, lParam);
    }
}

で取ることには成功したのですが、フック開始をユーザーに明示的にやらせるのかとか、あとやりたいことの割に仕掛けが大げさなので一旦ライブラリにはいれないことにしました。(やりたい人はこのコードを使ってね)もっとサクッとメニューハンドルから送信先のウィンドウハンドル取れたらいいんですけどねー。内部的には知ってるはずだよなー。なんかないかなー。

Friendly.Windows.KeyMouseを公開しました

Friendly.Windows.KeyMouseを作成しました!
github.com
あれだけ、キーマウスエミュレートをディスっておきながら、やるんかいってことなんですが・・・

繰り返しますが、最後の手段です。

もっと確実に簡単に操作できる手段があるならそちらを使ってください。どうしてもキー、マウスをエミュレートするテストがしたい場合や、他に手段がない場合にご利用ください。テスト中に全く触らなければ確実に(おそらく)動作しますが、処理中に机が揺れたりしてマウスが少し動いただけで失敗したりしますし。

確実に動作するキーマウスエミュレートなのです。(おそらく

半年ほど前にキーエミュレートに関する記事を書きました。
実はその時から、恐らく確実に動作するだろうキーマウスエミュレートは作れるだろうとは思っていたのですが、全面に押し出すものではないしなーと二の足を踏んでいました。
とは言え、要望はかなりあったので、一丁試しにやってみるかとの運びです。その辺の迷いが、Friendly.Windowsとかに実装するのではなく、新しいライブラリを追加しているとこにも表れています。

やっていることは単純

実装内容はいたって簡単で、旧来のキーマウスエミュレートAPIとFriendlyを組み合わせただけです。Friendlyでやっていることは

  • 座標の取得
  • 画面のアクティブ化(一部KeyのAPI
  • タイミングの調整

何故確実に動作させられるのか?

ポイントはタイミングの取り方なのです。
その辺は、前にこちらに長々と書いたのでご興味あれば参照お願いします。
Windowsアプリテスト自動化でのキーエミュレートはありなのか? - ささいなことですが。

マウスも実はこれでタイミングが取れます。
ビジーなタイミングを避ければ、キーマウスは失敗しない(はず)もちろんアプリ内で別スレッドでの非同期処理を実装している場合は、別途待ち処理が必要です。

意外とネックになるクリック、ダブルクリック問題にも対応

なんでかというと、クリック→クリックってやったときに、ダブルクリックになるという問題があります。「え、そんなのダブルクリック時間見ればいいんじゃない?」って思うでしょ?そうなんですけど、これではダメなんですよ。

//クリック送信
SendInput(inputArray.Length, inputArray, Marshal.SizeOf(inputArray[0]));

//ダブルクリック時間待ち
Thread.Sleep(SystemInformation.DoubleClickTime);

//クリック送信
SendInput(inputArray.Length, inputArray, Marshal.SizeOf(inputArray[0]));

//あれ?タイミングによってダブルクリックになる。

なぜかというと、ダブルクリックかどうかを判定するときに使う時間はあくまで対象のアプリが決めるからです。送信した時間から計測を始めても不正確なのです。相手が受信したタイミングから数えないといけない。それって相手の処理状態が分からないと無理なんですね。今回はタイマーメッセージを使うことにより、相手が受信したという状態を取得できるようにしています。実際のコードとは異なりますが、イメージ的には以下のようになるようにしています。

//クリック送信
SendInput(inputArray.Length, inputArray, Marshal.SizeOf(inputArray[0]));

//タイマーメッセージが通るのを待つ
//(相手に前に送ったマウスエミュレートが届いている)
WaitForTimerMessage(_app);

//ダブルクリック時間待ち
Thread.Sleep(SystemInformation.DoubleClickTime);

//クリック送信
SendInput(inputArray.Length, inputArray, Marshal.SizeOf(inputArray[0]));

サンプルコード

GitHubの方にも書きましたが、こっちにもサンプル書いておきます。
拡張メソッドなので、最初にusingおねがいします。

using Codeer.Friendly.Windows.KeyMouse;

キーエミュレート

var window = WindowControl.FromZTop(app);
var target = new FormsTextBox(window.Dynamic()._keyTest);

//引数はSystem.Windows.Forms.SendKeysと同じ仕様です。
target.SendKeys("aBc");

//CONTROL + Q
target.SendControlAndKey(Keys.Q);

//SHIFT + Q
target.SendShiftAndKey(Keys.A);

//ALT + Q
target.SendAltAndKey(Keys.Q);

//CONTROL + SHIFT + ALT + Q
target.SendModifyAndKey(true, true, true, Keys.Q);

マウスエミュレート

var window = WindowControl.FromZTop(app);
var target = new WindowControl(window.Dynamic()._mouseTest);

//左クリック 座標はコントロールの中央です。
target.Click();

//ボタンと座標指定
target.Click(MouseButtonType.Middle, new Point(4, 5));

//ダブルクリックも同様
target.DoubleClick();
target.DoubleClick(MouseButtonType.Middle, new Point(4, 5));

//Drag & Drop.
var dropTarget = new WindowControl(window.Dynamic()._dropTest);
target.MouseDown(MouseButtonType.Left, new Point(0, 0));
dropTarget.MouseUp(MouseButtonType.Left, new Point(2, 3));

キーとマウスを同時に操作

var window = WindowControl.FromZTop(app);
var target = new WindowControl(window.Dynamic()._keyMouseTest);

//ALT + MouseClick;
app.KeyDown(Keys.Menu);
target.Click():
app.KeyUp(Keys.Menu);

ご意見お待ちしております!

まだβ版という位置づけにしているのでご意見あれば言ってください。反映していきます。