ベスパリブ

プログラミングを主とした日記・備忘録です。ベスパ持ってないです。

WM_IME_CHAR、WM_IME_KEYDOWN、WM_IME_KEYUPの挙動を調べた

WinAPIの話です。

VK_DELETE メッセージを送信したら、"."(ドット)が出力される問題

仮想キーVK_DELETEをSendMessageしたら、なぜか"." (ドット) が出力されてしまう問題に悩んでいました。

原因は単純で、SendMessage(hWnd, WM_IME_CHAR, VK_DELETE, 0)としていたせいでした。

WM_IME_CHARとは何か。
http://www.geocities.jp/katayama_hirofumi_mz/imehackerz/ja/WM_IME_CHAR.html
によると、第三引数のwParamはUnicode文字の値と書いてあります。

VK_DELETEは0x2eです。
WikipediaUnicodeを調べます。
https://ja.wikipedia.org/wiki/Unicode%E4%B8%80%E8%A6%A7_0000-0FFF
なるほど。ドットです。

WM_IME_CHARとは?

WM_IME_CHARの仕様は、

IMEが変換結果の文字を取得するときに、アプリに送られます。ウィンドウは、そのウィンドウプロシージャーを通じてこのメッセージを受け取ります。

らしいですが、ちょっとよくわかりません。

原文のMSDNも読みましたが、やはりちょっとよくわかりません。

よくわかっていないなりの理解なのですが、Unicodeを送信したいときはWM_IME_CHARを使えということでしょう。実際、そういう動作をします。

Call SendMessage(hWnd, WM_IME_CHAR, &H21, 0)  ' !
Call SendMessage(hWnd, WM_IME_CHAR, &H2E, 0)  ' .
Call SendMessage(hWnd, WM_IME_CHAR, &H7E, 0)  ' ~ 

結局、DELETEを送信するには?

やりたいことは「DELETEキーを押したい」と同じです。
ここにきて、"仮想キー"の考えが腑に落ちた気がします。仮想キーは文字通り「仮想的なキーボード」なので、「キーを押して」、「キーを離す」というメッセージを送信するということになります。

Call SendMessage(hWnd, WM_IME_KEYDOWN, VK_DELETE , 0)
Call SendMessage(hWnd, WM_IME_KEYUP, VK_DELETE, 0)

何かがおかしい

Call SendMessage(hWnd_Edit, WM_IME_CHAR , Asc("a"), 0)
Call SendMessage(hWnd_Edit, WM_IME_CHAR , Asc("b"), 0)
Call SendMessage(hWnd_Edit, WM_IME_CHAR , Asc("c"), 0)
Call SendMessage(hWnd_Edit, WM_IME_KEYDOWN, VK_BACK, 0)
Call SendMessage(hWnd_Edit, WM_IME_KEYUP, VK_BACK, 0)

とすると、"b"と"c"が消えました( VK_BACK はBackSpace)。
WM_IME_KEYDOWN と WM_IME_KEYUPでワンセットかと思いきや、 WM_IME_KEYDOWN と WM_IME_KEYUPで二回VK_BACKを押したことになっているようです(私の環境だけ?)。

ちなみに、「押しっぱなし」がどうなるのかも確かめてみました。

Call SendMessage(hWnd_Edit, WM_IME_CHAR , Asc("a"), 0)
Call SendMessage(hWnd_Edit, WM_IME_CHAR , Asc("b"), 0)
Call SendMessage(hWnd_Edit, WM_IME_CHAR , Asc("c"), 0)
Call SendMessage(hWnd_Edit, WM_IME_KEYDOWN, VK_BACK, 0)

"c"が消えただけで、"a"と"b"は残りました。どうやら「押しっぱなし」のようなことにはならないようです。

さらに、VK_LEFT(左キー)の挙動を調べました。

Call SendMessage(hWnd_Edit, WM_IME_CHAR, Asc("a"), 0)
Call SendMessage(hWnd_Edit, WM_IME_CHAR, Asc("b"), 0)
Call SendMessage(hWnd_Edit, WM_IME_CHAR, Asc("c"), 0)
Call SendMessage(hWnd_Edit, WM_IME_CHAR, Asc("d"), 0)
Call SendMessage(hWnd_Edit, WM_IME_CHAR, Asc("e"), 0)
Call SendMessage(hWnd_Edit, WM_IME_KEYDOWN, VK_LEFT, 0)
Call SendMessage(hWnd_Edit, WM_IME_KEYUP, VK_LEFT, 0)
Call SendMessage(hWnd_Edit, WM_IME_KEYDOWN, VK_LEFT, 0)
Call SendMessage(hWnd_Edit, WM_IME_KEYUP, VK_LEFT, 0)
Call SendMessage(hWnd_Edit, WM_IME_CHAR, Asc("X"), 0)
' "abcXde"
Call SendMessage(hWnd_Edit, WM_IME_CHAR, Asc("a"), 0)
Call SendMessage(hWnd_Edit, WM_IME_CHAR, Asc("b"), 0)
Call SendMessage(hWnd_Edit, WM_IME_CHAR, Asc("c"), 0)
Call SendMessage(hWnd_Edit, WM_IME_CHAR, Asc("d"), 0)
Call SendMessage(hWnd_Edit, WM_IME_CHAR, Asc("e"), 0)
Call SendMessage(hWnd_Edit, WM_IME_KEYDOWN, VK_LEFT, 0)
Call SendMessage(hWnd_Edit, WM_IME_KEYDOWN, VK_LEFT, 0)
Call SendMessage(hWnd_Edit, WM_IME_CHAR, Asc("X"), 0)
' "abcXde"
Call SendMessage(hWnd_Edit, WM_IME_CHAR, Asc("a"), 0)
Call SendMessage(hWnd_Edit, WM_IME_CHAR, Asc("b"), 0)
Call SendMessage(hWnd_Edit, WM_IME_CHAR, Asc("c"), 0)
Call SendMessage(hWnd_Edit, WM_IME_CHAR, Asc("d"), 0)
Call SendMessage(hWnd_Edit, WM_IME_CHAR, Asc("e"), 0)
Call SendMessage(hWnd_Edit, WM_IME_KEYUP, VK_LEFT, 0)
Call SendMessage(hWnd_Edit, WM_IME_KEYUP, VK_LEFT, 0)
Call SendMessage(hWnd_Edit, WM_IME_CHAR, Asc("X"), 0)
' "abcdeX"

この辺、挙動を確認しながらプログラム組まないと、よくわからないバグになりそうです。環境による気もしてきました……。 どこかに良い書籍や情報はないでしょうか。

使い分けとか

キーボードでできることは仮想キーにやらせよう(オライリー・ジャパン風)
逆に、 「ʨ」とかの入力はUnicodeでしかできないと思います(入力できるのか知らないけど。私の環境だと?に変換された)

補足

MSDNを辿っていると、知識として入れておいたほうが良さそうなページを見つけました。
キーボード入力

Editコントロールが入力可能になるまで待つ

WinAPIの話です。
Editコントロールのハンドルを取得できても、Editコントロールに入力可能というわけではないようです。
このせいで、ハンドル取得した直後に文字列をSendMessageしても、テキストボックスに文字が入っていない場合があります。

しょうがないので、Editコントロールが入力可能になるまで待つ関数を作ります。
方法としては、
1. Editコントロールに適当な文字をSendMessageし、文字数を取得する。
2. 文字数が1文字以上なら、入力可能なので、文字を消して終了
3. 文字数が0ならば、入力できていないので、1.に戻る
です。単純ですね。

Public Declare Function SendMessage4GetText Lib "user32" Alias "SendMessageW" (ByVal hWnd As IntPtr, ByVal wMsg As Integer, ByVal wParam As Integer, ByVal iParam As System.Text.StringBuilder) As Integer
Public Const VK_BACK As Integer = &H8  ' BackSpace

Public Sub Wait4Edit(ByVal hWnd As IntPtr)
    Dim buf As New System.Text.StringBuilder()
    While 1
        buf.Length = 2048
        System.Windows.Forms.Application.DoEvents()
        Call SendMessage(hWnd, WM_IME_CHAR, Asc("a"), 0)  ' 適当な文字を送信する
        If SendMessage4GetText (hWnd, WM_GETTEXT, buf.Length, buf) >= 1 Then  ' 入力できていたら、文字数が1文字以上になる
            Exit While
        End If
        System.Threading.Thread.Sleep(100)
    End While
    ' 適当な文字を送っているので、コントロールに入力されている文字を全部消す
    While 1
        buf.Length = 2048
        System.Windows.Forms.Application.DoEvents()
        Call SendMessage(hWnd_Edit, WM_IME_KEYDOWN, VK_BACK, 0)
        Call SendMessage(hWnd_Edit, WM_IME_KEYUP, VK_BACK, 0)
        If SendMessage4GetText (hWnd, WM_GETTEXT, buf.Length, buf) = 0 Then
            Return
        End If
        System.Threading.Thread.Sleep(100)
    End While
End Sub

引数hWndにはEditコントロールのハンドルを与えます。
与えるハンドル間違えたりすると無限ループから返ってこないことに注意。

なぜWhile文を2つに分けているかというと、最初一つにまとめていたのですが、なぜか入力ボックスに文字が残っていたりした("a"が2回送られる? or VK_BACKが反応しない?)ので、「文字を消す」処理を確実に行わせたかったため二つに分けています。この辺の仕様をまだよくわかっていません。でもまあ、動くしいいか(適当)

追記[20180525]

IsWindowVisible()とIsWindowEnabled()という便利な関数を見つけました。これを使えば良さそうです。

Public Declare Function IsWindowEnabled Lib "user32" Alias "IsWindowEnabled" (ByVal hWnd As IntPtr) As Boolean
Public Declare Function IsWindowVisible Lib "user32" Alias "IsWindowVisible" (ByVal hWnd As IntPtr) As Boolean

Public Sub Wait4Edit(ByVal hWnd As IntPtr)
    ' エディットボックスが入力可能になるまで待つ
    While True
        If (IsWindowEnabled(hWnd) = True) And (IsWindowVisible(hWnd) = True) Then
            Exit While
        End If
        System.Threading.Thread.Sleep(10)
    End While
End Sub

FindWindowに「タイムアウトするまで探し続ける機能」欲しい!!!!

うるさい!!!
WinAPIの話です。

FindWindowの問題点

FindWIndowでウィンドウハンドルを取得する際、PCが重かったりするとウィンドウの表示が遅れたりして取得に失敗したりするので、FindWindow()前にSleepするという方法がありますが、これもどれだけSleepすればよいかという問題を抱えており、PCが激重だった場合、Sleepして待っても結局ウィンドウハンドルが取得できないという問題も抱えています。

このような問題はFindWIndowに「タイムアウトするまで探し続ける機能」があればそれで解決する話なので、そのようなラッパー関数を作成しました。できれば標準で欲しかった。

Declare Unicode Function FindWindow Lib "user32.dll" Alias "FindWindowW" (ByVal lpClassName As String, ByVal lpWindowName As String) As IntPtr

Public Function FindWindow2(ByVal cnm As String, ByVal cap As String, Optional ByVal timeout As Integer = 10000) As IntPtr
    ' ウィンドウが見つからない場合、timeoutミリ秒経過するまで無限ループで取得し続ける
    ' タイムアウトしたら0を返す
    ' 関数名はもっと考えよう
    Dim hWnd As IntPtr = 0
    Dim sw As New System.Diagnostics.Stopwatch()
    sw.Start()
    While 1
        System.Windows.Forms.Application.DoEvents()
        hWnd = FindWindow(cnm, cap)
        If hWnd <> 0 Then
            Return hWnd
        End If
        If sw.ElapsedMilliseconds >= timeout Then
            ' タイムアウトした
            Return 0
        End If
        'System.Threading.Thread.Sleep(500)
    End While
    Return hWnd
End Function

CPU使用率が気になる場合は、ループ内で適当な時間Sleepさせれば良いと思います。

2018年の目標

2月の終わりですが、2018年の目標を決めました。

1. 英語
2. 競プロ
3. 機械学習系のプロダクトを1つ作成

英語を一番頑張りたいと思います。
他2つはジャストアイデアですが、興味がある分野なのでやっていきたいと思います(やらなそう)。

英語

・英検1級
・TOEIC900台
を目標とします。
たしか3年前くらいのTOEICが600台だったような気がします。
現状はReadingはまあまあできます。
Listening、Writing、Speakingがボロボロです。
なので結構強気の目標です。
なんで英検なのかっていうのは、WritingとSpeakingがあるからです(Writingは申し訳程度ですが)。
TOEFLにしろよと突っ込まれそうですが、TOEFLには怖いイメージしかないので英検にしました。

競プロ

最近競技プログラミングをはじめました。
全然問題解けないけど面白いので、趣味としてやろうと思います。
目標スコアは……よくわかりません。とりあえず一年やってみた結果どの程度になるのか見ようと思います。

機械学習系のプロダクトを1つ作成

なんかします(やらなそう)。

以上です。

Python3でラズパイのIPアドレスを取得する

python上でifconfigコマンドを実行し、返ってきた結果にIPアドレスがあればそれを表示することを考えます。
"LANG=C ifconfig"とするとコマンド結果が全て英語で返ってきて、処理しやすくなります。

import subprocess
subprocess.call(("LANG=C", "ifconfig"))  # エラー起きる

残念ながら、上記の方法だとエラーが発生します。"LANG=C"を使うときはcallではなくてPopenを使うと良いそうです。
Popenは公式によると、

17.5.1.2. Popen コンストラクター(原文) このモジュールの中で、根底のプロセス生成と管理は Popen クラスによって扱われます。簡易関数によってカバーされないあまり一般的でないケースを開発者が扱えるように、Popen クラスは多くの柔軟性を提供しています。 class subprocess.Popen(args, bufsize=-1, executable=None, stdin=None, stdout=None, stderr=None, preexec_fn=None, close_fds=True, shell=False, cwd=None, env=None, universal_newlines=False, startupinfo=None, creationflags=0, restore_signals=True, start_new_session=False, pass_fds=(), *, encoding=None, errors=None)
(中略)
注釈 子プロセスのために環境を変更する必要がある場合は、preexec_fn の中でそれをするのではなく env 引数を使用します。start_new_session 引数は、子プロセスの中で os.setsid() を呼ぶ過去の一般的な preexec_fn の使用方法の代わりになります。

(オプション引数多すぎ!)
結論から言うと、env引数で指定すれば良いです。LANG=Cは環境変更のコマンド(?)になるので、subprocess.callでifconfigと併用できなかったようです。

p = subprocess.Popen(
  "ifconfig",
  #stdin=subprocess.PIPE
  stdout=subprocess.PIPE,
  stderr=subprocess.PIPE, 
  env={'LANG':'C'},
  shell=True
)
out, err = p.communicate()
print(out)

shell引数をTrueにしておかないと、"ifconfig"コマンドを認識してくれません。
stdout, stderr引数がないと、outとerrを受け取ることが出来ません。
結果としてoutは以下のようなものを得られます。

b'eth0      Link encap:Ethernet  HWaddr b8:27:eb:xx:xx:xx  \n          UP BROADC
AST MULTICAST  MTU:1500  Metric:1\n          RX packets:4431131 errors:0 dropped
:9222 overruns:0 frame:0\n          TX packets:18104015 errors:0 dropped:0 overr
uns:0 carrier:0\n          collisions:0 txqueuelen:1000 \n          RX bytes:122
0313021 (1.1 GiB)  TX bytes:3488088120 (3.2 GiB)\n\nlo        Link encap:Local L
oopback  \n          inet addr:127.0.0.1  Mask:255.0.0.0\n          inet6 addr:
::1/128 Scope:Host\n          UP LOOPBACK RUNNING  MTU:65536  Metric:1\n
  RX packets:31487788 errors:0 dropped:0 overruns:0 frame:0\n          TX packet
s:31487788 errors:0 dropped:0 overruns:0 carrier:0\n          collisions:0 txque
uelen:1 \n          RX bytes:29619310733 (27.5 GiB)  TX bytes:29619310733 (27.5
GiB)\n\nwlan0     Link encap:Ethernet  HWaddr b8:27:eb:xx:xx:xx  \n          ine
t addr:192.168.5.129  Bcast:192.168.5.255  Mask:255.255.255.0\n          inet6 a
ddr: fe80::9e6e:73a1:2f3a:e0b1/64 Scope:Link\n          UP BROADCAST RUNNING MUL
TICAST  MTU:1500  Metric:1\n          RX packets:19745725 errors:0 dropped:30867
91 overruns:0 frame:0\n          TX packets:1280736 errors:0 dropped:0 overruns:
0 carrier:0\n          collisions:0 txqueuelen:1000 \n          RX bytes:1023118
1 (9.7 MiB)  TX bytes:1462394071 (1.3 GiB)\n\n'

結果はバイナリで返ってきていますので、適当にdecodeして文字列に直してから処理をします。

  str_out = out.decode("ascii", "ignore")
  str_out_lines = str_out.splitlines()
  ip_addr = {"eth0":"", "wlan0":"", "lo":"", "":""}
  now_if  = ""
  for line in str_out_lines:
    line = line.lower()
    if line.startswith("eth0"):
      now_if = "eth0"
      continue
    elif line.startswith("lo"):
      now_if = "lo"
      continue
    elif line.startswith("wlan0"):
      now_if = "wlan0"
      continue
    if "inet addr:" in line:
      blocks = line.split()
      # blocks[1] = "addr:xxx.xxx.xxx.xxx"
      ip_addr[now_if] = blocks[1][5:]

    # 確認のため標準出力する
    for key in ip_addr:
      print("ip_addr[{}] = {}".format(key, ip_addr[key]))

標準出力の結果は以下のようになります。

ip_addr[lo] = 127.0.0.1
ip_addr[] =
ip_addr[eth0] = 192.168.5.128
ip_addr[wlan0] = 192.168.5.129

eth0が有線LAN、wlan0が無線LANIPアドレスです。

Pythonのopen関数のencoding引数は必須でもいいんじゃない

たまにファイルの読み込み時(後?)にDecodeErrorする謎現象に遭いました。
どうやらファイルがShift-JISで書き込まれていたのをUTF-8で読み込もうとしていたのが原因。そういえばファイルをAtomで編集したときSJISで保存したかもしれない。うちのAtomは気を抜いたらなぜかSJISで書き込もうとする。

調べたらopen関数でencoding引数があったので、それを指定すれば確実にUTF-8で読み書き可能です。

f = open("hoge.txt", "w", encoding="utf-8")
f.write("あいうえお")
f.close()

encoding引数を指定しない場合のデフォルト値はNoneで、どのエンコーディングになるかはプラットフォーム依存らしいです。

encoding はファイルのエンコードやデコードに使われる text encoding の名前です。このオプションはテキストモードでのみ使用してください。デフォルトエンコーディングはプラットフォーム依存 (locale.getpreferredencoding() が返すもの) ですが、Pythonでサポートされているエンコーディングはどれでも使えます。詳しくは codecs モジュール内のサポートしているエンコーディングのリストを参照してください。

参考URL:
https://docs.python.jp/3/library/functions.html#open