2011年3月8日火曜日

SlimDXでDirect2Dのキーボード入力

Direct2Dのキーボード入力!

なんて言っても、Direct2Dはあくまで描画するための道具ですので、キーボードとかマウスとかゲームのコントローラー(ジョイパッド)などから入力を受け取りたいときは全く別の道具を使わないといけません。

じゃあ何を使うのか?

そりゃあもう何でも使っていいんじゃないでしょうか。
あなたのお気に入りのものを使っちゃってください。何せDirect2Dは描画するための道具ですから、入力とはぜんっぜん関係ありません

ただしここでは3つの方法を紹介します。

1.フォームのイベントを用いる方法
2.SlimDXのRawInputを用いる方法
3.SlimDXのDirectInputを用いる方法

扱いが楽なのは1,2,3の順
応用が利くのは3,2,1の順
という感じですかね。



1.フォームのイベントを用いる方法

Direct2DやDirect3Dを使っていると、私には「DirectXがウィンドウを乗っ取った!」という感覚があります。だから入出力含めあらゆる操作にDirectX流のやり方があるのだと。
DirectXを使ってみる前やDirectXの使い始めはそういう考えを持っていました。だからなかなかDirectXの世界に入り込めませんでした。

しかし、使ってみて分かってきたのですが、DirectXは通常のフォームアプリと仲良く楽しく一緒に使えるように作られています。
Direct2Dの場合、何もしないと単なるねずみ色になってしまうフォームの描画を肩代わりするだけです。よって、フォームに元から付いている機能はほぼ全てそのまま活用できます。

フォームには元からKeyDown、KeyPress、KeyUpイベントがありますが、それもそのまま活用できます。
以下は、フォームのタイトルが、押されたキーの名前になるサンプルです。

using System;
using System.Drawing;
using System.Windows.Forms;
using SlimDX;
using SlimDX.Direct2D;
using SlimDX.Windows;

namespace DirectInputSample
{
    static class Program
    {
        [STAThread]
        static void Main()
        {
            var form = new RenderForm("SlimDX - Keyboard Sample");
            var factory = new Factory();

            var target = new WindowRenderTarget(factory,new WindowRenderTargetProperties(){
                Handle = form.Handle,
                PixelSize = form.ClientSize
            });

            form.KeyDown += (sender,e) =>{
              form.Text = e.KeyCode.ToString();
            };

            MessagePump.Run(form, () =>
            {
                target.BeginDraw();
                target.Clear();

                target.EndDraw();
            });

            foreach (var item in ObjectTable.Objects)
                item.Dispose();
        }
    }
}


Direct2Dの基本については[SlimDXでDirect2D]を参考にされてください。

            form.KeyDown += (sender,e) =>{
              form.Text = e.KeyCode.ToString();
            };
ここで、KeyDownイベントが発生したときの処理を書いています。キーコードをフォームのタイトルにしているだけですけどね。ちなみにここでは匿名メソッド偉い人の解説という手法を用いています。本当はフォームのイベントに対して匿名メソッドを使うのはあまりよろしくありません。これはサンプルですから、なるべくMain関数に全ての処理を書きたいのでわざと使用しました。





2.SlimDXのRawInputを用いる方法

次はSlimDXに付属しているRawInputを用いる方法です。

RawInputはDirectXの機能ではありません。それとはまた別のWindowsの機能です。RawInputについては検索してもあんまりヒットしませんがだいたい概要は掴めると思います。Microsoftの解説ページ(英語)を見てみるとわかりますが生入力(エロい)というだけあって実に低レベルで使いにくそうです。マウスの入力に直に触れちゃう感じなんだと思います。

しかし心配は要りません。そのRawInputをラップしたSlimDXバージョンは実に使いやすいですから。個人的には後に紹介するDirectInputよりもこちらのほうが好きです(設計美的に)。

それじゃあサンプルです。

using System;
using System.Drawing;
using System.Windows.Forms;
using SlimDX;
using SlimDX.Direct2D;
using SlimDX.Windows;
using SlimDX.Multimedia;
using SlimDX.RawInput;

namespace DirectInputSample
{
    static class Program
    {
        [STAThread]
        static void Main()
        {
            var form = new RenderForm("SlimDX - Keyboard Sample");
            var factory = new Factory();

            var target = new WindowRenderTarget(factory,new WindowRenderTargetProperties(){
                Handle = form.Handle,
                PixelSize = form.ClientSize
            });

            Device.RegisterDevice(UsagePage.Generic,UsageId.Keyboard,DeviceFlags.None);
            Device.KeyboardInput += (sender,e) =>{
                form.Text = e.Key.ToString();
            };

            MessagePump.Run(form, () =>
            {
                target.BeginDraw();
                target.Clear();

                target.EndDraw();
            });

            foreach (var item in ObjectTable.Objects)
                item.Dispose();
        }
    }
}

キーボードから入力があると、押された文字がフォームのタイトルに表示されます。
では説明します。

using SlimDX.Multimedia;
using SlimDX.RawInput;
RawInputを扱うにはこの2つの参照を加えないといけません。SlimDX.Multimedia名前空間にはUsage(後で説明)に関するものが入っています。


            Device.RegisterDevice(UsagePage.Generic,UsageId.Keyboard,DeviceFlags.None);
ここでキーボードを登録しています。さて、SlimDXのRawInputではこのように、Deviceという名の静的クラスを用います。入力装置は1つしかないのでインスタンスを生成する必要は無いのです。
で、このコード、なんだか意味が分からないですよね。そもそもキーボードを登録って意味不明だし! Usage? うさげって何だよ! うさぎの毛? かわいいな!
……RegisterDevice関数はデバイスを登録するということです。デバイスというのは日本語では装置。つまりキーボードやらマウスやらのことです。本来そういうのは繋がってさえいれば使えるはずです。面倒ごとはWindowsさんがやってくれます。しかし、RawInputの仕組みがデバイスの登録制なのです。デバイスから生入力(エロい)を受け取るにはそのデバイスを登録しないといけませんよ、とそういうことです。
で、うさげ(Usage)というのは調べるとややこしいのですが、RawInputでは単純にデバイスの識別に使われているようです。UsagePageをGenericに、UsageIdをKeyboardにするとキーボードになります。
DeviceFlagsというのは、UsagePageとUsageIdで提供された情報をどう解釈するか決めるものです(直訳)。まあ、オプションと考えてください。現在特別なオプションを付けないのでNoneです。もしこれを
            Device.RegisterDevice(UsagePage.Generic,UsageId.Keyboard,DeviceFlags.InputSink,form.Handle);
としたならば、別のウィンドウをアクティブにした状態でもキーボード入力に反応するようになります。


            Device.KeyboardInput += (sender,e) =>{
                form.Text = e.Key.ToString();
            };
ここでキーボードから何らかの入力があったときの動作を書いています。今の状態だとマウスが押されたとき、離されたときのどちらも反応します。もし離されたときのみにしたいならばStateプロパティを用いて、
              if(e.State==KeyState.Released){
                form.Text = e.Key.ToString();
              }
とします。




3.SlimDXのDirectInputを用いる方法

DirectInputはDirectXです。これが一番高機能です。リファレンスを見ると沢山のクラスがずら~っと並んでいます。DirectXですので文献も豊富です。検索数で比較してRawInput(エロい)の10倍以上あります。

ただし、SlimDXバージョンは部分的に上手く設計されていません。個人的にキライ。
でも他に代わる物が無ければ使うしかありません。

んではサンプルです。

using System;
using System.Drawing;
using System.Windows.Forms;
using SlimDX;
using SlimDX.Direct2D;
using SlimDX.Windows;
using SlimDX.DirectInput;

namespace DirectInputSample
{
    static class Program
    {
        [STAThread]
        static void Main()
        {
            var form = new RenderForm("SlimDX - DirectInput Sample");
            var factory = new Factory();

            var target = new WindowRenderTarget(factory,new WindowRenderTargetProperties(){
                Handle = form.Handle,
                PixelSize = form.ClientSize
            });

            var di = new DirectInput();

            var keyboard = new Keyboard(di);
            keyboard.SetCooperativeLevel(form.Handle,CooperativeLevel.Foreground|CooperativeLevel.Nonexclusive);
            keyboard.Properties.BufferSize = 4;

            MessagePump.Run(form, () =>
            {
              if(keyboard.Acquire().IsSuccess){
                foreach(var state in keyboard.GetBufferedData()){
                  foreach(var key in state.PressedKeys){
                    form.Text = key.ToString();
                  }
                }
              }

              target.BeginDraw();
              target.Clear();

              target.EndDraw();
            });

            foreach (var item in ObjectTable.Objects)
                item.Dispose();
        }
    }
}


DirectInputでは入力があった場合、それをバッファに一度貯めておきます。そして、任意のタイミングでそれを取得できます。これは本来OSがやってくれることですが、DirectInputではそこまで自在に操れるということです。このサンプルでは描画の直前に取得しています。
さらに、複数のキーが同時に押された場合というのも識別できるようです。私のキーボードではそのような状況を発生させることができませんでしたが……。


using SlimDX.DirectInput;
DirectInputを用いる場合はSlimDX.DirectInput名前空間への参照を加えます。


            var di = new DirectInput();
ここでDirectInputで色々なデバイスを使うためのDirectInputクラスのインスタンスを作ります。
とてつもなく意味の分からないクラスですが、とりあえずこれを作らないといけません。
小言になりますが私が気になるのはここでして、ここで作ったインスタンスを使えばなぜかキーボードやらマウスやらを使うことができます。まるでシステムに繋がっているかのようです。このことは直感に反します。インスタンスの生成はあくまで生成するだけで、そこで作られたインスタンスは何とも繋がっていないはずです。もしインスタンスの生成時点で他のものとの関連を持たせるならば、コンストラクタに何らかの引数を与えるか、静的なCreateDirectInputメソッドでも用意してそれで生成させるべきです。


            var keyboard = new Keyboard(di);
ここでキーボードを作っています。残念ながら痛キーボードにするような設定はできないみたいです。


            keyboard.SetCooperativeLevel(form.Handle,CooperativeLevel.Foreground|CooperativeLevel.Exclusive);
作ったキーボードの協調レベルを設定します。協調レベルというのはまたの名を共同組合レベルと言い、他のアプリケーションとの間でどのようにキーボードを奪い合うかということです。
現在のアプリケーションはフォームですのでフォームのハンドルを渡しています。
Foregroundの場合はアプリケーションがアクティブになっているときしかキーボードにアクセスできません。Backgroundになると別のアプリケーションがアクティブになっているときでもキーボードにアクセスできます。
Exclusiveの場合はこのアプリケーションがキーボードにアクセスしている間、別のアプリケーションからキーボードにアクセスできなくします。Nonexclusiveになると別のアプリケーションもキーボードにアクセスできるようになります。ちなみに実際にはアクセス権の取引によりこういうことを実現しています。。
この他にNoWinKeyというのがあります。これを設定すると、キーボードにWIndowsマークのキーがあると思いますが、そのキーを押したときにWindowsのメニューを出さなくします。


            keyboard.Properties.BufferSize = 4;
ここでキーボードの入力を貯めるバッファのサイズを決めています。初期値が0らしく、わざわざ設定しています。4としたことに特に意味はありません。


              if(keyboard.Acquire().IsSuccess){
Acquireメソッドでアクセス権を取得しています。協調レベルがForegroundのときなどに失敗することがあるので、成功したときということでif文を使っています。



                foreach(var state in keyboard.GetBufferedData()){
GetBufferedDataメソッドでバッファに溜まっている全てのデータを取得します。そしてforeachでそれら1つずつに対して処理を行っています。


                  foreach(var key in state.PressedKeys){
押されている全てのキーを取得します。そしてforeachでそれらh(ry


DirectInputは高機能な故に簡単なことをするのにも手間がかかりますね。ちゃんと説明できている気がしませんし。なるべくリファレンスを読むようにお願いします。



ところで、3つ紹介しましたがどれをどう使い分けるか気になりますよね。1の方法はちょっと低機能すぎるかなという気がします。対して3はとにかく使いにくいです。やっぱり個人的にはRawInput(エロい)がステキかな。

なんか今回はまじめに書いた気がします。前の記事との間に時間が空いたので真面目になったのですね。せっかくなので今聴いている作業用BGMを載せておきます。




10 件のコメント:

  1.  記事アップ、ご苦労様です。

    >DirectXは通常のフォームアプリと仲良く楽しく一緒に使えるように作られています。
     そうですか。
     それでは安心して作り進めたいと思います。
     こう言う基本的な事が全然分かってないんで、とても助かります。

    >本当はフォームのイベントに対して匿名メソッドを使うのはあまりよろしくありません
     「自分で調べる」などとほざいたクセに、早速質問ですいません。
     どのような問題が出るんでしょうか?

     やっと二つのFormそれぞれのPanelに描画出来るようになりました。

    返信削除
  2. >Mooさん

    私も基本的なことが分かっていませんでした^^
    あんまり初歩的なことを書くと相手に失礼な気もしますが、たぶんDirectXに触れたばかりの人はDirectXの常識を知らないのではないかと。というか私が知らなかったので、そういう暗黙の常識はなるべく書くようにしています。

    匿名メソッドを使うことで出てくる問題はソースコードの分かりにくさです。バグが出るとか速度が遅いとかではありません。
    今回の場合、「マウスをクリックしたとき」に発生するイベントをMain関数の中に書いています。しかし実際にはMain関数で実行されるわけではなく、マウスをクリックしたときに実行されます。フォームには他にも沢山のイベントが発生するわけですが、それを全てMain関数に書いてしまうと、Main関数で何をやっているのか分からなくなってしまいます。
    また、イベントは匿名メソッドを使わずに1つの関数に独立させるのが一般的です。例えば、マウスをクリックしたときにどんな処理をしているのかな~とソースコードを覗くときに、まずすることはMouseやClickedという名の付いた関数を探すことです。Main関数にその処理を書いてしまうと、どこにあるのか見つけにくくなってしまいます。
    プログラミングするほうは匿名メソッドのほうが圧倒的に簡単ですが、読む方は少し見にくいわけです。

    匿名メソッドを用いる場面はフォームのようにどっしりと構えたやつではなく、もっと小さい場面です。例えば[いつも思うけどC#のジェネリックスが不完全]で書いた最後のコードのような場面です。そのコードは色々省略しているので分かりにくいのですが、リストの中身全てをCircle型からShape型へ変換するというものです。1つ1つの変換は「c as IShape」のところで行われています。「c as IShape」というだけのコードをわざわざ関数にするのは面倒ですし、読む方もこの関数は一体何だろうと不要な混乱を招いてしまいます。
    ……説明が長くなってしまいましたが、こんな感じです。ただしこれはあくまで私の解釈です。使っているうちにMooさん独自の解釈が生まれてくると思います。

    >やっと二つのFormそれぞれのPanelに描画出来るようになりました。
    おお~、着々と進んでいますね。ところでVisual Studioを使われているのでしょうか?

    返信削除
  3. >匿名メソッドを使うことで出てくる問題はソースコードの分かりにくさです。
     「美しさ」の問題ですね。
     それは一番大事。
     自分は若者にいつも「ソースは芸術だ」と言ってます(笑)
     文学小説の言い回しとなんも変わらない。
     「通じれば良い、動けば良い」ってもんじゃない。
     やっぱり「美しさ」「統一性」「人に親切に」は非常に大事です。
     結局、頭の中がまとまってるかどうかがソースに全部現れてる。

     匿名メソッドの長短所、了解しました。

     ところでこれが気に入ってます。
    foreach (var item in ObjectTable.Objects)
    item.Dispose();
     「オブジェクトをまとめて破棄」って事だろうと想像してますが、美しいですねぇ・・一発!

    >暗黙の常識はなるべく書くようにしています。
     この「知ってるやつには当たり前」部分を全然知らないので、とても有り難いです。
     「別々の二つのFormで、それぞれに別々のfactoryとtargetで描画可能」ってことも、昨日はじめて知りました。
     と言う事は「一つのFormに二つのPanelを置いて、別々に描画(管理)も可能かも」と言う風に結論が出てきた。
     教えてもらっちゃえば簡単な事でも、自分で見つけるのはやはり手間のかかるものです。
     「暗黙の常識」・・これからも気が付いたらよろしくお願いします。

    >Visual Studioを使われているのでしょうか?
     そうです。
     例の無料版。
     若い連中がみんな使ってるんで、これで揃えておけばプロジェクトのやりとりに便利かなってことでした。
     基本的に「MS嫌い」なので、ずっとC++Builderだったんだけど、この頃は劣勢に立たされてます(^^;

    返信削除
  4. >ソースは芸術だ

    な、なんて大胆な!
    でもまさしくその通りだと思います。ソースの中でくらい美しくなっていいじゃない。

    Disposeの所はSlimDXで用いたリソースの解放をまとめてやっています。Microsoftはリソースの解放を明示的にすることを"お勧め"していますが、どのタイミングで解放するのかは述べていません。SlimDXは最後にまとめて行ったわけですね。
    ただし、この方法だと長時間起動し続けるソフトでも終了するまで解放されないことになってしまうので、その場合は注意が必要ですね。
    ……ん? これはすごく重要な問題じゃないか? 例えばループの度に新しくオブジェクトを作ったらどうなるんだろう……後で調べときます。

    >別々の二つのFormで、それぞれに別々のfactoryとtargetで描画可能
    ふむふむ。Direct2Dを2つ同時に使うのは試していないので参考になります。Factoryは1つでもいいかもしれません。


    おお、ということは2008でしょうか。
    うちはコンソールでやっているのでVisual Studioでは簡単にできるのかちょっと気になっていましたが大丈夫そうですね。

    >MS嫌い
    私は「MS好き」です^^ こっち側にいればたいてい勝てますよw(IEを除く)

    返信削除
  5. >Visual Studioでは簡単にできるのかちょっと気になっていましたが大丈夫そうですね。
     なかなか快適です・・・MS製品にしてはw
     特にコード補完が便利で、これって「書いて良いコードしか出てこない」んですね。
     補完されない(出てこない)コードを無理に書くとコンパイルエラーになるんで、これだけでかなりC#の勉強が捗ります。
     やっぱり「そっち側」がお得みたいです。

    返信削除
  6. >Mooさん
    そう言われればそんな機能がありましたね。
    使おうと思っても名前が思い出せずに毎度検索している私からしたらなんとも便利な機能です。
    でもvimに慣れちゃうとなかなか抜けられないんですよね・・・

    返信削除
  7. vimですか!
    渋いですね~
    自分は俗に言う「統合環境」から抜けられない体でして。

    ところがルネサス、TI、Borland、MS・・・どれもキー配置がバラバラで、その度に指使いが違う(設定で変えられるけどいちいちいじるのも面倒で)。
    今じゃ体の方を合わせるようになりましたw

    やっぱりコマンドラインからエディタとMakefileで作るのが正統派と思います。

    返信削除
  8. >Mooさん
    私も最初はSharpDevelopという統合開発環境を使っていました。
    統合開発環境は色々と自動で書いてくれるんですけど、最初は何を自動で書いてくれているのか分からないんですよねぇ。
    そういうのを理解しようと思ってメモ帳で始めてそのままずるずると使っていますw

    あるときVisual Studioで「ぷよぷよ」を作ってみたらホント数時間でできちゃって、作業効率の違いに驚きました。私がリファレンスとにらめっこしながら書いたコードをみんなはマウスのひと操作で終えているのだと思うと何とも言えない気持ちになります。

    実はMakefileは使ってないんですよね……代わりにMSBuildというのを使っています。同じようなものですけど。ここでもMicrosoftのお世話になってます。

    返信削除
  9. 衣緒さん。
    地震対策で少しゴタゴタしています。
    少し休んでまた勉強を再開しようと思っています。
    またお邪魔します。

    返信削除
  10. >Mooさん

    おお、ご無事でしたか。
    またいらっしゃるとのことで楽しみにしています。

    返信削除