none
TextBoxでキー長押し時にKeyDownが発生しないことがある(Windows 7) RRS feed

  • 質問

  • TextBoxのTextChangedイベント内で重い処理を行っている場合に、TextBox上でキーの長押しをすると

    KeyDownイベントが発生しない(KeyDownイベントハンドラが実行されない)という現象が起こっています。

    (PreviewKeyDown、KeyPressも同様にイベントハンドラが実行されません)

    Windows7で現象を確認し、WindowsXPで試したところ再現しませんでした。

    下記のコードを使って現象を確認しています。

    フォーム上にTextBoxを貼り付けてTextChenged、KeyDownイベントハンドラを実装。

        private void textBox1_TextChanged(object sender, EventArgs e)
        {
          int j = 0;
    
          Console.WriteLine("TextChanged");
    
          // TextChangedイベント内での重い処理
          for (int i = 0; i < 10000000; i++)
          {
            j++;
          }      
        }
    
        private void textBox1_KeyDown(object sender, KeyEventArgs e)
        {
          Console.WriteLine("KeyDown");
        }
    

    Windows7(CPU:Core2Duo E8500)でこのコードを実行し、キーを押し続けた場合

    KeyDown
    TextChanged
    KeyDown
    TextChanged
    KeyDown
    TextChanged
    KeyDown
    TextChanged
    KeyDown
    TextChanged
    KeyDown
    TextChanged
    KeyDown
    TextChanged
    KeyDown
    TextChanged
    KeyDown
    TextChanged
    KeyDown
    TextChanged
    TextChanged
    KeyDown
    TextChanged
    TextChanged

    こんな感じになります。もっと性能のいいCPUの場合はループ回数を増やせば現象が確認できると思います。

    WindowsXPでは同じCPUではありませんが、KeyDown、TextChangedが常に交互に発生していました。

    なお、Windows7のキーフィルター機能はOFFにしてあります。

    TextChangedイベント内で重い処理をするべきではないという議論は置いておいて、この現象が起こるのは「Windowsの仕様」なのかどうかが知りたいです。

    Windows7で起こってWindowsXPで起きないのは、XPでも同じ現象が起きるけどたまたまこのコードでは起こらない(起こりにくい)だけなのかどうか、

    Windows7(もしくはVista以降)の仕様で起こる現象なのか、それともバグなのか。

    間違いの指摘もしくは、なにかご存じの方は教えていただけると気持ちがスッキリするのでよろしくお願いします。

    2010年9月18日 7:39

回答

  • WM_KEYDOWN は必ず押された(リピートされた)回数分、受け取れる保障はありません。
    MSDN のドキュメントにもありますが、メッセージキューが埋まることを避けるため、いくつかの WM_KEYDOWN をつなげて、Repeat Count をインクリメントする仕様です。

    http://msdn.microsoft.com/en-us/library/ms646267(VS.85).aspx#_win32_Keystroke_Message_Flags
    (Repeat Count 参照)

    Windows のバージョンによって挙動の差はあるかもしれません。
    しかし、必ず WM_KEYDOWN が入力と同じ回数分呼ばれる保障は元々ないのです。


    質問スレッドで解決した場合は、解決の参考になった投稿に対して「回答としてマーク」のボタンを押すことで、同じ問題に遭遇した別のユーザが役立つ投稿を見つけやすくなります。
    • 回答としてマーク げきから 2010年9月20日 0:47
    2010年9月20日 0:20
    モデレータ

すべての返信

  • 外池と申します。Windows VISTA以降については特に、XP以前についてもWindowsの仕様はどうなのか? と問われると無知なので・・・、問題の切り分けのためのアイデアの提案に留めさせてください。

    まず、順序は別にして、すべてのイベントが記録されていることは確実でしょうか? WindowsアプリからConsoleへ出力すること自体は順序がおかしくなることは無いと思うのですが、Consoleへ送った文字列が尻切れトンボになる可能性がないのかちょっと気になるところです。念のためですが、Consoleは使わずに、Windowsアプリに閉じた形でStringBuilderやStringWriterなんかを使ってログを採って、ファイルに吐き出すなり、別のTextBoxに表示するなりしてみてはいかがでしょう?

    あと、その上で、開発環境の上でDebugする場合と、ビルドしたExeファイルを単独で走らせる場合と違いが出るかどうかも気になります。イベントの順序は入れ替わってもらうと困る(キー入力関係については特に)とは思いますが、無いとも言い切れません。

    最後に、Windowsがアプリケーションにキーボードが押されたことを報せているメッセージを点検されてみてはいかがでしょうか? とりあえず、TextBoxが貼り付けられているFormで、WndProcをOverrideして、WM_KEYDOWNが送られてくる様子を同様にして捉えるわけです。もし、WM_KEYDOWNが欠落することがあるなら、ある意味スゴイんですが・・・、たぶん、あり得ないと思います。そのような場合には、後続する処理が行われないはずで、したがって、TextBoxに文字が現れることもないはず、ということになりますので。


    (ホームページを再開しました)
    2010年9月18日 16:45
  • 外池さんアイデアありがとうございます。ご提案いただいたアイデアで再度検証してみました。

    結果を先に書いてしまいますが、Windows7で実行した場合WM_KEYDOWNが発生していませんでした。WindowsXPで実行した場合は必ず発生していました。

    WndProcをオーバーライドするためにTextBoxを継承したCustomTextBoxを作って検証しています。

    今回使ったコード

     public partial class Form1 : Form
     {
      CustomTextBox CustomTextBox1 = new CustomTextBox();
    
      public Form1()
      {
       InitializeComponent();
    
       this.AutoSize = true;
       this.Controls.Add(CustomTextBox1);
       CustomTextBox1.KeyDown += new KeyEventHandler(CustomTextBox_KeyDown);
       CustomTextBox1.TextChanged += new EventHandler(CustomTextBox_TextChanged);
       CustomTextBox1.Size = textBox2.Size;
      }
    
      private void CustomTextBox_TextChanged(object sender, EventArgs e)
      {
       int j = 0;
    
       Console.WriteLine("TextChangedイベントハンドラ");
    
       // TextChangedイベント内での重い処理
       for (int i = 0; i < 600000000; i++)
       {
        j++;
       }
      }
    
      private void CustomTextBox_KeyDown(object sender, KeyEventArgs e)
      {
       Console.WriteLine("KeyDownイベントハンドラ");
       textBox2.Text = CustomTextBox1.KeyDownCount.ToString();
       textBox1.Text = CustomTextBox1.Text.Length.ToString();
      }
     }
    
     public class CustomTextBox : TextBox
     {
      private const int WM_KEYDOWN = 0x0100;
      private const int WM_KEYUP = 0x0101;
      private const int WM_GETDLGCODE = 0x0087;
      private const int WM_GETTEXTLENGTH = 0x000E;
      private const int WM_GETTEXT = 0x000D;
      private const int WM_CHAR = 0x0102;
      private const int EM_GETMODIFY = 0x00B8;
      private const int WM_IME_NOTIFY = 0x0282;
      private const int WM_USER = 0x0400;
      private const int WM_REFLECT = WM_USER + 0x1C00;
      private const int WM_COMMAND = 0x0111;
      private const int WM_REFLECT_COMMAND = WM_REFLECT + WM_COMMAND;
    
      private int keydownCount = 0;
    
      public CustomTextBox()
      {
       this.Size = new Size(100, 100);
       this.AutoSize = false;
       this.Multiline = true;
      }
    
      public int KeyDownCount
      {
       get
       {
        return keydownCount;
       }
      }
    
      protected override void WndProc(ref Message m)
      {
       Encoding sjisEnc = Encoding.GetEncoding("Shift_JIS");
       StreamWriter writer = null;
    
       try
       {
        writer = new StreamWriter(@"log.txt", true, sjisEnc);
    
        switch (m.Msg)
        {
         case WM_KEYDOWN:
          Console.WriteLine("WM_KEYDOWN");
          writer.WriteLine("WM_KEYDOWN");
          keydownCount++;
          break;
    
         case WM_KEYUP:
          Console.WriteLine("WM_KEYUP");
          writer.WriteLine("WM_KEYUP");
          break;
    
         case WM_GETDLGCODE:
          Console.WriteLine("WM_GETDLGCODE");
          writer.WriteLine("WM_GETDLGCODE");
          break;
    
         case WM_GETTEXTLENGTH:
          Console.WriteLine("WM_GETTEXTLENGTH");
          writer.WriteLine("WM_GETTEXTLENGTH");
          break;
    
         case WM_GETTEXT:
          Console.WriteLine("WM_GETTEXT");
          writer.WriteLine("WM_GETTEXT");
          break;
    
         case WM_CHAR:
          Console.WriteLine("WM_CHAR");
          writer.WriteLine("WM_CHAR");
          break;
    
         case EM_GETMODIFY:
          Console.WriteLine("EM_GETMODIFY");
          writer.WriteLine("EM_GETMODIFY");
          break;
    
         case WM_IME_NOTIFY:
          Console.WriteLine("WM_IME_NOTIFY");
          writer.WriteLine("WM_IME_NOTIFY");
          break;
    
         case WM_REFLECT_COMMAND:
          Console.WriteLine("WM_REFLECT + WM_COMMAND");
          writer.WriteLine("WM_REFLECT + WM_COMMAND");
          break;
    
         default:
          Console.WriteLine(m.Msg.ToString());
          writer.WriteLine(m.Msg.ToString());
          break;
        }
       }
       finally
       {
        writer.Close();
       }
    
       base.WndProc(ref m);
      }
     }
    

    今回はDebugではなくReleaseでビルドし、exeで実行しました。ループ回数が少ないと再現しなかったためかなりループ回数を増やしています。
    (おそらく再現するだろうという見込みで検証を始めたため、現象が発生するまでループ回数を増やしました)
    Windows7のCPUは前回同様Core2DuoE8500、WindowsXPはAthron64 3200+です。

    抜粋ですがファイルへの出力結果は以下の通りです。

    【 Windows7 】
    WM_GETDLGCODE
    WM_GETDLGCODE
    WM_KEYDOWN
    WM_GETTEXTLENGTH
    WM_GETTEXT
    WM_GETDLGCODE
    WM_CHAR
    WM_REFLECT + WM_COMMAND
    EM_GETMODIFY
    WM_IME_NOTIFY
    WM_REFLECT + WM_COMMAND
    675
    133
    WM_GETDLGCODE
    WM_GETDLGCODE
    WM_KEYDOWN
    WM_GETTEXTLENGTH
    WM_GETTEXT
    WM_GETDLGCODE
    WM_CHAR
    WM_REFLECT + WM_COMMAND
    EM_GETMODIFY
    WM_IME_NOTIFY
    WM_REFLECT + WM_COMMAND
    WM_GETDLGCODE
    WM_GETDLGCODE
    WM_KEYDOWN
    WM_GETTEXTLENGTH
    WM_GETTEXT
    WM_GETDLGCODE
    WM_CHAR
    WM_REFLECT + WM_COMMAND
    EM_GETMODIFY
    WM_IME_NOTIFY
    WM_REFLECT + WM_COMMAND
    WM_GETDLGCODE
    WM_GETDLGCODE
    WM_KEYDOWN
    WM_GETTEXTLENGTH
    WM_GETTEXT
    WM_GETDLGCODE
    WM_CHAR
    WM_REFLECT + WM_COMMAND
    EM_GETMODIFY
    WM_IME_NOTIFY
    WM_REFLECT + WM_COMMAND
    WM_REFLECT + WM_COMMAND
    EM_GETMODIFY
    WM_IME_NOTIFY
    WM_REFLECT + WM_COMMAND
    WM_GETDLGCODE
    WM_GETDLGCODE
    WM_KEYDOWN
    WM_GETTEXTLENGTH
    WM_GETTEXT
    WM_GETDLGCODE
    WM_CHAR
    WM_REFLECT + WM_COMMAND
    EM_GETMODIFY
    WM_IME_NOTIFY
    WM_REFLECT + WM_COMMAND
    WM_REFLECT + WM_COMMAND
    EM_GETMODIFY
    WM_IME_NOTIFY
    WM_REFLECT + WM_COMMAND
    WM_REFLECT + WM_COMMAND
    EM_GETMODIFY
    WM_IME_NOTIFY
    WM_REFLECT + WM_COMMAND

    【 WindowsXP 】
    WM_GETDLGCODE
    WM_GETDLGCODE
    WM_KEYDOWN
    WM_GETTEXTLENGTH
    WM_GETTEXT
    WM_GETDLGCODE
    WM_CHAR
    WM_REFLECT + WM_COMMAND
    EM_GETMODIFY
    WM_IME_NOTIFY
    WM_REFLECT + WM_COMMAND
    WM_GETDLGCODE
    WM_GETDLGCODE
    WM_KEYDOWN
    WM_GETTEXTLENGTH
    WM_GETTEXT
    WM_GETDLGCODE
    WM_CHAR
    WM_REFLECT + WM_COMMAND
    EM_GETMODIFY
    WM_IME_NOTIFY
    WM_REFLECT + WM_COMMAND
    WM_GETDLGCODE
    WM_GETDLGCODE
    WM_KEYDOWN
    WM_GETTEXTLENGTH
    WM_GETTEXT
    WM_GETDLGCODE
    WM_CHAR
    WM_REFLECT + WM_COMMAND
    EM_GETMODIFY
    WM_IME_NOTIFY
    WM_REFLECT + WM_COMMAND
    WM_GETDLGCODE
    WM_GETDLGCODE
    WM_KEYDOWN
    WM_GETTEXTLENGTH
    WM_GETTEXT
    WM_GETDLGCODE
    WM_CHAR
    WM_REFLECT + WM_COMMAND
    EM_GETMODIFY
    WM_IME_NOTIFY
    WM_REFLECT + WM_COMMAND
    WM_GETDLGCODE
    WM_GETDLGCODE
    WM_KEYDOWN
    WM_GETTEXTLENGTH
    WM_GETTEXT
    WM_GETDLGCODE
    WM_CHAR
    WM_REFLECT + WM_COMMAND
    EM_GETMODIFY
    WM_IME_NOTIFY
    WM_REFLECT + WM_COMMAND

    Debug実行してコンソール出力で確認したところでは

    KeyDownイベントハンドラは
    WM_KEYDOWN
    KeyDownイベントハンドラ
    WM_GETTEXTLENGTH

    TextChangedイベントハンドラは
    EM_GETMODIFY
    WM_IME_NOTIFY
    WM_REFLECT + WM_COMMAND
    TextChangedイベントハンドラ

    のタイミングで実行されているようです。


    ログの出力結果から、キーが1回押されるごとに
    WM_GETDLGCODE x 2 ~ EM_GETMODIFY→WM_IME_NOTIFY→WM_REFLECT + WM_COMMAND
    のようにメッセージが発生しているようなのですが、Windows7でKeyDownが発生していない箇所では
    WM_GETDLGCODE x 2 ~ WM_CHAR
    までの部分が欠落しています。

    実際に動かしてみて分かったのですが、テキストボックスにテキストが出力されていく様子も7とXPで異なっていて、
    7は入力に引っかかりがありつつも連続で文字が出力されていったのに対してXPは1文字ずつぽつ・ぽつと出力
    されていく感じでした。(CPUの性能差がありすぎるせいだったかもしれませんが)

    出力されていく様子が違うというのは見た目の感覚的な話なので置いておくとして、KEYDOWNのメッセージが
    発生していないところから考えるとやはりメッセージ処理まわりがXPと7で変わったのでしょうか。
    ここまで調べ始めたらやはりなんらかの結論は欲しい感じです。
    引き続き情報やアイデア、間違いの指摘等よろしくお願いします。

    • 編集済み げきから 2010年9月19日 16:35 誤字のため
    2010年9月19日 16:33
  • WM_KEYDOWN は必ず押された(リピートされた)回数分、受け取れる保障はありません。
    MSDN のドキュメントにもありますが、メッセージキューが埋まることを避けるため、いくつかの WM_KEYDOWN をつなげて、Repeat Count をインクリメントする仕様です。

    http://msdn.microsoft.com/en-us/library/ms646267(VS.85).aspx#_win32_Keystroke_Message_Flags
    (Repeat Count 参照)

    Windows のバージョンによって挙動の差はあるかもしれません。
    しかし、必ず WM_KEYDOWN が入力と同じ回数分呼ばれる保障は元々ないのです。


    質問スレッドで解決した場合は、解決の参考になった投稿に対して「回答としてマーク」のボタンを押すことで、同じ問題に遭遇した別のユーザが役立つ投稿を見つけやすくなります。
    • 回答としてマーク げきから 2010年9月20日 0:47
    2010年9月20日 0:20
    モデレータ
  • なるほど。仕様だったんですね。
    lparamからRepeatCountが取れるみたいなので、WM_KEYDOWNが来なかった場合の対応もできそうです。

    (`・ω・´)スッキリしました

    外池さん、Azuleanさんありがとうございました。

    2010年9月20日 0:47
  • 外池です。私も勉強になりました。

    lParamのうちPrevious key-state flagだけは使ったことがあって、そのときは、長押ししても機能しないように殺す操作しかしていませんでした。Key repeat countは必ず1だった(私の環境はWindows 2000やXP)ので、これが2以上になる可能性は顧慮してませんでした。

    http://msdn.microsoft.com/en-us/library/ms646280(VS.85).aspx
    の説明だけだと、Key repeat countについて何を言ってるのかほとんど不明なままなので、理解しないまま放置していました。

    Azuleanさんにご紹介して頂いたページは詳細な説明で判りやすいですね。ありがとうございます。


    (ホームページを再開しました)
    2010年9月20日 2:15