Shujis1964
@Shujis1964 (Shuji Sunano)

Are you sure you want to delete the question?

Leaving a resolved question undeleted may help others!

[C#] SendMessageでCheckBoxのBM_GETCHECKの戻り値が旨く取得できません

解決したいこと

C#で他のWindowのCheckBoxを操作するプログラムを書いていますが、WindowsAPIの関数でSenMessageを使用しようとしています。
該当するCheckBoxのハンドラを取得した上で、BM_GETCHECK(0x00F0)を送信していますが、戻り値が旨く取得できないので困っています。

使用しているPCは64bitで、プラットフォームは、x86とAnyCPUの両方を試しましたが、どちらでも同じ状況です。
APIの宣言の仕方もWebによって複数存在し、それらを片っ端から試しているような状況です。

特に、戻り値の型指定でIntPtr, int, long等があり色々と試しているのですが、
long以外は、checkの有無に関係なく全て0を戻します。
longの場合は、checkの有無に関係なく、常に"0xC735306000000000"を返してきます。

宣言の仕方に問題があるのか、関数の呼び出し方に問題があるのか、戻り値の受け方に問題があるのか、ビルドに問題があるのかなど
試行錯誤しても解決できていません。

C#で同様の操作が出来ている方がおられましたらご教授頂きたいです。

該当するソースコード

        // 戻り値の型とパラメータの型をそれぞれ複数の組合せで試しています。
        [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
        public static extern long SendMessage(IntPtr hWnd, uint Msg, uint wParam, uint lParam);

        [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
        public static extern int SendMessage(IntPtr hWnd, uint Msg, uint wParam, uint lParam);

        [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
        public static extern IntPtr SendMessage(IntPtr hWnd, uint Msg, uint wParam, uint lParam);

        [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
        private static extern IntPtr SendMessage(IntPtr hWnd, int Msg, IntPtr wParam, IntPtr lParam);


        // 以下のconstもそれぞれint, uintを書き換えたりして得います。
        // msg
        public const int BM_GETCHECK = 0x00F0;
        public const int BM_SETCHECK = 0x00F1;

        // wParam
        public const uint BST_CHECKED = 0x0001;
        public const uint BST_UNCHECKED = 0x0000;


        // 以下、ボタンをクリックしたらSendMessageをコールする。
        // 呼び出す定義によってwParm, lParmの型は変更する。戻り側の型も宣言に合わせて変数rの型を変更する。
        private void button4_Click(object sender, EventArgs e)
        {
            long r = SendMessage(hWndc, BM_GETCHECK, (uint)0,(uint)0);
            textBox5.Text = String.Format("{0:X}", r);
        }

<操作される側のウィンドウ>
20210711_203532[TestFormZ].png

<操作する側のウィンドウ>
※旨くいかないのは、操作される側のウィンドウのcheckBox1のチェック有無に対してBM_GETCHECKボタンをクリックしたときボタンの隣のtextBoxの値が出力される。
また、BST_CHECKED, BST_UNCHECKD を送信した際は操作される側のcheckBox1のチェックは変化しない。
ちなみにWM_GETTEXTはStringBuilderを使ってwParmで戻していて、問題無くテキストを取得できている。
ウィンドウの右上のtextBoxは操作される側のcheckBox1のハンドラが取得できていることを表している。
20210711_203602[TestAPISendMessage].png

自分で試したこと

パラメータの型と戻り値の型をそれぞれ変更して実行した。

よろしくお願いします。

0

3Answer

続報で、UIオートメーションで良さげなコードがありました。
https://docs.microsoft.com/ja-jp/dotnet/framework/ui-automation/get-the-toggle-state-of-a-check-box-using-ui-automation

そのまま拝借して試してみましたが、快く動いてくれました。
C#で作ったFormとメモ帳の両方を試しました。

選択肢が増えて良かったです。


        /// <summary>
        /// Gets the toggle state of an element in the target application.
        /// </summary>
        /// <param name="element">The target element.</param>
        private bool IsElementToggledOn(AutomationElement element)
        {
            if (element == null)
            {
                // TODO: Invalid parameter error handling.
                return false;
            }

            Object objPattern;
            TogglePattern togPattern;
            if (true == element.TryGetCurrentPattern(TogglePattern.Pattern, out objPattern))
            {
                togPattern = objPattern as TogglePattern;
                return togPattern.Current.ToggleState == ToggleState.On;
            }
            // TODO: Object doesn't support TogglePattern error handling.
            return false;
        }



        private void button7_Click(object sender, EventArgs e)
        {
            hWndc = new IntPtr(0x000A059C);// 取りあえずダイレクトにハンドルを指定

            AutomationElement AutoEle = AutomationElement.FromHandle(hWndc);
            bool ss = IsElementToggledOn(AutoEle);
            checkBox1.Checked = ss;
        }

1Like

記載されているコードでは.NETで作られたチェックボックスの状態を取得できないようです。
下記に記載したコードだと、.NETで作られたチェックボックスの状態も取得できました。

[DllImport("oleacc.dll", PreserveSig = false)]
[return: MarshalAs(UnmanagedType.Interface)]
public static extern object AccessibleObjectFromWindow(IntPtr hwnd, uint dwId, ref Guid riid);

const UInt32 OBJID_CLIENT = 0xFFFFFFFC;
const int UNCHECKED = 1048576;
const int CHECKED = 1048592;
Guid uid = new Guid("618736e0-3c3d-11cf-810c-00aa00389b71");

private void button4_Click(object sender, EventArgs e)
{
    Accessibility.IAccessible accObj = (Accessibility.IAccessible)AccessibleObjectFromWindow(hWndc, OBJID_CLIENT, ref uid);
    int state = (int)accObj.get_accState(0);
    switch (state)
    {
        case UNCHECKED:
            textBox5.Text = "UNCHECKED";
            break;
        case CHECKED:
            textBox5.Text = "CHECKED";
            break;
        default:
            textBox5.Text = "UNKNOWN";
            break;
    }
}

何かの、参考になれば幸いです。

0Like

Comments

  1. @Shujis1964

    Questioner

    早速の回答、大変感謝です。
    このコードで動きました。

    コントロールへのアクセスはSendMessageしか知りませんでした。Webでこの手の検索をすると殆どSendMessageの説明がHitしますので、気づきませんでした。
    Active Accessibilityというのが使えるのですね。その他にUIオートメーションというのも有ったりして。
    戴いた見慣れない関数を検索していく内にこの辺りの概念にHitし始めて、知識が広まりました。
    それと、この問題を英語で検索すると、同じ問題を抱えて解決したQ&Aもあって、頂いたコードと殆ど同じでした。どうもC#では、SendMessageの戻り値は旨く受け取れないようで、こちらを使うようです。
    でも何故SendMessageでは受け取れないのか、判らないとちょっとフラストレーションです。
    Accessibilityを使うと結構手続きが大変そうで、使いこなせるのか心配です。
    でも勉強になります。他のコントロールとかも試していきたいので、もうすこしいろいろ試してみます。

まず、SendMessageをオーバーロードされていますが、以下の型で良いはずです。

.cs
private static extern IntPtr SendMessage(IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam);

うまく動作しない原因は、このソースコードではなく、操作される側のウィンドウの方にあります。
checkBox1が「BM_GETCHECKに対応しているチェックボックスではない」ためです。

BM_GETCHECKをご覧いただくとわかりますが、BM_GETCHECKが動作するには、以下の条件があります。

  • BUTTONクラス※として生成したウィンドウ(コントロール)である
  • ウィンドウスタイルに「BS_AUTOCHECKBOX」か「BS_CHECKBOX」等を指定している

※ややこしいですが、上記のクラスは、C#のclassキーワードで定義する型のことではなく、Win32APIの「ウィンドウクラス」のことです。

おそらく、操作される側は.NET FrameworkのWindowsフォームで作成したと思われますが、何故かSystem.Windows.Forms.CheckBoxは「標準コントロールのチェックボックス」(BUTTONクラス)ではありません。
そのため、BM_GETCHECKメッセージを送信しても、何も反応しない(ゼロが返る)ものと思われます。

各コントロールが何のウィンドウクラスかは、Visual Studioを使用しているのでしたら、Spy++というツールを使用すると簡単に調べられます(64ビットの時はspyxx_amd64.exeの方を使用します)。

例えば、Windowsのメモ帳(notepad.exe)の検索ダイアログのチェックボックスは、BUTTONクラスのコントロールのため、BM_GETCHECKで状態を取得できると思います。

ご参考になれば幸いです。

0Like

Comments

  1. @Shujis1964

    Questioner

    詳細な解説をして頂き大変ありがとうございます。
    MicroSoft提供のCheckBoxにも複数有るんですね。

    メモ帳の検索ダイアログのチェックボックスをSEndMessageで試してみました。仰るとおり、元々私が書いていたコードで全く問題無く動きました。びっくりです。
    ついでにAccessibilityを使ったコードでもメモ帳のが使えるかも確認すると、こちらも問題無いようです。

    コントロールのクラスも確認しました。
    <C#で作ったフォーム>
    チェックボックス:WindowsForms10.BUTTON.app.0.141b42a_r7_ad1
    ボタン:WindowsForms10.BUTTON.app.0.141b42a_r7_ad1
    テキストボックス:WindowsForms10.EDIT.app.0.141b42a_r7_ad1

    <メモ帳>
    チェックボックス:Button
    ボタン:Button
    テキストボックス:Edit

    となっていました。ただ、ボタンとテキストボックスは、C#で作ったフォームのものでもSendMessageでも使えてますし、コンボボックスも使えています。
    と言うことは、クラスが違うから対応していないと言うより、.NETで作られた一部のコントロールがSendMessageに対応していないだけ?とも考えられる訳で、ちょっと困りました。

    コントロールする相手が特定していて、既知である場合は、対応、非対応を調べて対応するかとかになるか、Accessibilityを使う方が良いのか。
    でも、ライブラリを作って使いまわしも考えると、いちいちクラスをチェックするのかとか。
    結局、Accessibilityを使う方が安全と言うことになるのでしょうか?

    この件、奥が深いです。
    他のウィンドウのコントロールを制御する関係の検索をすると殆どSendMessageを使った説明ばかりなので。。。

    BM_GETCHECKが動作する条件については、まだ調べられてないです(見つからない)。他のメッセージについても確認していく必要が有りそうなので、調べてみます。
    もし良さげなページをご存じであれば、よろしくお願いします。
  2. お役に立てたようで良かったです。

    失礼いたしました。BM_GETCHECKのリンク先が間違っておりましたので、修正いたしました。
    BM_GETCHECK message
    https://docs.microsoft.com/en-us/windows/win32/controls/bm-getcheck

    SendMessageで送信するメッセージの種類によって、動作したり動作しなかったりするのは、
    送信先のウィンドウ(コントロール)次第のため、送信してみないとわからないのが現実です。
    メッセージのマクロ名をご覧いただくとわかりますが、マクロ名の先頭が
    汎用的なメッセージは「WM_」、各コントロール別のメッセージは「BM_」(ボタン系)「EM_」(エディット系)等となっています。
    https://docs.microsoft.com/en-us/windows/win32/winmsg/window-messages
    https://docs.microsoft.com/en-us/windows/win32/controls/bumper-button-control-reference-messages
    https://docs.microsoft.com/en-us/windows/win32/controls/bumper-edit-control-reference-messages

    汎用的なメッセージ以外は、コントロールの種類別に処理を分けないとうまく動かないでしょう。

    どの技術を使うべきかは、何を実現したいか?に依ります。
    例えば、SendMessageもAccessibilityも、WPFアプリやWin10ストアアプリでは動作しないと思われます。
    これらのコントロールは、ウィンドウハンドル(HWND)を持たないためです。
    より多くのアプリケーションに対応したいのでしたら、私も詳しくありませんが「UIオートメーション」を調べてみると良いかも知れません。
    https://docs.microsoft.com/ja-jp/dotnet/framework/ui-automation/ui-automation-fundamentals
  3. @Shujis1964

    Questioner

    度々のご教授ありがとうございます。内容を確認していきたいと思います。

    アプリが対象とする物によってHWNDを持たないものが有るのですね。気をつけます。
    今の所、大丈夫だと思います。
    そもそもやろうとしていることは、計測機器を動かしているWinidowsソフトを外部からコントロールしようとしていまして、そのソフトは社内で作られた物と、メーカーが作った物があります。
    社内で作った物はC#で作られているのですが、簡単には改造出来ないものなので、外から操作する仕組みを考えています。メーカのものはそもそもいじれないのでこれも外からの操作になります。また、この場合はどの言語で作られているかも判らないので、どの技術を使うのかが悩みどころです。

    ご説明頂いたように、やはり一つ一つ動作を確認していくしかないのかなと、思い始めています。ただ、現状ではSendMessageとAccessibilityを使い分ける形かなと今は思っています。
    現在、Accessibilityの使い方について、検索しまくっていますが、こちらもあまり良い説明をされている物がなさそうです。チェックボックス以外はどうすれば良いかなど、旨くHIT出来ません。最終は、各オブジェクトに実際にメッセージなり、メソッドなどを送ってみてその反応がどう返っているか、どういう値が返ってくるのかなどを自力で調査しないといけないのかも知れないです。

    取りあえずは、Spy++を使って、クラスを確認し、クラス毎のコントロールの挙動とどの技術でコントロール出来るかを確認してから、クラス毎に使い分ける方向でしょうか。

  4. Spy++でコントロールの情報を確認できるようでしたら、SendMessageやAccessibilityで対応可能です。

    後はどの技術を使うかですが、個人的な見解としては、SendMessageは相当大変だと思われます。
    マイクロソフトも、そのためにAccessibilityを提供している気がいたします。

    AccessibilityかUIオートメーションのどちらかですが、以下の情報を参考にどうぞ。
    個人的には、UIオートメーションの方が最新で情報も多いですし、.NET Frameworkからも使いやすそうな印象です。

    Microsoft Active Accessibility
    https://docs.microsoft.com/ja-jp/windows/win32/winauto/microsoft-active-accessibility

    UI オートメーションの基礎
    https://docs.microsoft.com/ja-jp/dotnet/framework/ui-automation/ui-automation-fundamentals

    UI AutomationでWindowsプログラムの自動化などしてみる
    https://qiita.com/ken_hamada/items/501b164374667319d270

    余談ですが、UIオートメーションは、Microsoft製の以下のツールでも使用していると思われます。
    Microsoft、自動UIテストの作成支援ツール「WinAppDriver UI Recorder」を公開
    https://forest.watch.impress.co.jp/docs/news/1128952.html
    Microsoft、デスクトップ操作の自動化ツールをWindows 10ユーザーに追加費用なしで提供
    https://forest.watch.impress.co.jp/docs/news/1309591.html
  5. @Shujis1964

    Questioner

    UIオートメーションでqittaの別の方の記事のコードを見てみましたが、なんとなくすっきり感がありますね。覚えるまでが大変そうですが。
    勉強してみます。

    所で話がちょっと前に戻りますが、今社内でC#で作ったソフトのそれぞれのコントロールをSpy++して見たのですが、以下の感じです。

    CheckBox: WindowsForms10.BUTTON.app.0.141b42a_r7_ad1
    TextBox: WindowsForms10.EDIT.app.0.141b42a_r7_ad1
    Label: WindowsForms10.STATIC.app.0.141b42a_r7_ad1
    Radio: WindowsForms10.BUTTON.app.0.141b42a_r7_ad1
    Bottom: WindowsForms10.BUTTON.app.0.141b42a_r7_ad1
    ComboBox: WindowsForms10.COMBOBOX.app.0.141b42a_r7_ad1
    ComboBoxの中のTextBox: Edit
    GroupBox: WindowsForms10.Window.8.app.0.141b42a_r7_ad1

    ここで、面白いなと思ったのは、ComboBoxで、ComboBoxはプルダウンの部分がTextBoxを包括するような構造になっているのですが、プルダウンそのものは.NETのクラスであるのに、その中のTextBoxは"Edit"になっていました。
    つまり、ComBoxの中のTextBoxと、コントロールのパーツとして用意されたTextBoxは別物の様です。

    普通にVS2017で作った物なので、特にこのアプリケーションに限った物ではありません。
  6. コンボボックスは複合コントロールで、コンボボックス、エディットコントロール、リストボックスを組み合わせてできているそうです。
    古いコントロールですが、なかなか複雑なことをがんばっていると思います。

    おそらくWindowsフォームのComboBoxも、基本機能は標準コントロールのコンボボックスをサブクラス化して使用しているのでしょう。

    UIオートメーションも無事動作できたようで、おめでとうございます。お役に立てて幸いです。
  7. @Shujis1964

    Questioner

    UIオートメーションで、Button,TextBoxも試しました。こちらも問題無く動いてます。
    UIオートメーションだとDllImport宣言をする必要が無く、慣れればこちらの方が良く、恐らく直接API呼び出さないので、プログラム的にもこちらの方が安全なのかなと思い始めました。

    ただ、先に覚えたWindowsのハンドルから追っていき対象のコントロールのハンドルを取得する方法をとっていたのですが、UI特有のツリーを追って行くようで、この辺りを今模索中です。コントロールのハンドルが判っているので直接指定することも出来るのですが、こちらもあまりスマートではないやり方を今はしているので、どうせならこの辺りから改善したいです。
    でも、一応の方向性と解決策はまとまったので、このスレッドは終了にしたいと思います。

    長らくお付き合い頂いて大変ありがとう御座いました。
    非常に助かりました。

Your answer might help someone💌