フォーカスとキー イベント

要約:
  • 各アプレット ウィンドウは View および FocusManager に関連付けられています。
  • FocusManager は、Viewキーボード フォーカスを管理します。
  • キーボード フォーカスは、ユーザーのキーストロークが送られる場所を決定します。
  • 複数の KeyEvent は、押されたあるいは離された 1 つのキーまたは複数のキーの組み合わせを表現します。
Curl® 言語で記述されたアプレットが動作している場合、アプレットはキーボード イベントを処理して、キーボードで入力するユーザーに応答します。キーボード イベントは、オペレーティング システムによりアプレット ウィンドウに送られます。各ウィンドウは View オブジェクトに関連づけられ、各 View オブジェクトには、すべてのキーボードおよびポインタ イベントに関係する、オブジェクト自身の FocusManager があります。これはキーボード フォーカスを管理するものです。キーボード フォーカスではユーザーのキーストロークの送信先が決定されます。
アプレットが複数の View を表示する場合、アクティブ ウィンドウの View のみがキーボード イベントを受け取ります。さらにキーボード イベントは、View により View 内のオブジェクトに送出されます。ただし、これはオブジェクトがキーボード フォーカスを持っている場合に限ります。
次のポップアップ ダイアログを見てください。3 つのテキスト フィールドが含まれています。(既定では、ポップアップ ダイアログは新しい View に表示されます。) このダイアログを表示した直後にキーボードから入力する場合、キーボードの入力は自動的に 2 番目のテキスト フィールドに送信されます。つまり、キーボード フォーカスを持つオブジェクトは View にあることを示しています。

例: キーボード フォーカスを持つ TextField
{value
    let tf1:TextField = {TextField width=3.0in}
    let tf2:TextField = {TextField width=3.5in}
    let tf3:TextField = {TextField width=4.0in}
    let vbox:VBox={VBox
                      tf1,
                      tf2,
                      tf3
                  }
    let dialog:Dialog={Dialog vbox,
                          {on DialogShow do
                              || the 2nd TextField requests the keyboard focus
                              {tf2.request-key-focus}
                          }
                      }

    {CommandButton label="Click and Type",
        {on Action do
            {dialog.show
                modal?=false
            }
        }
    }
}

ホスト フォーカスとキーボード フォーカス

ウィンドウのタイトル バーがハイライト表示されることで示されるように、ウィンドウがアクティブの場合は、オペレーティング システムはそのウィンドウがホスト フォーカスを持っていると認識します。これは、すべてのキーボード イベントがそのウィンドウに送られるということを意味します。
Curl® 実行環境 は、オペレーティング システムによりウィンドウに送られるキーボードの動作を、同じウィンドウに関連付けられた View で発生する KeyEvent に変換します。
ホスト フォーカスがオペレーティング システムによるキーボード イベントの送信先であるのとは対象的に、各 Viewキーボード フォーカスを追跡します。キーボード フォーカスは、各 View で最高 1 つのグラフィック オブジェクトに必ず属しています。オブジェクトがキーボード フォーカスを保持している間は、その View がオペレーティング システムから受け取るすべての KeyEvent のターゲットになります。

フォーカス イベント

FocusEvent は、オブジェクトで発生するイベントであり、オブジェクトがキーボード フォーカスを取得および喪失する原因となるものです。FocusEvent の 2 つのサブクラス、FocusIn および FocusOut を次に示します。
FocusEvent
FocusIn
FocusOut
グラフィック オブジェクトは、FocusIn イベントがオブジェクトで発生するときにキーボード フォーカスを得ます。別のオブジェクトが KeyEvent のターゲットになるときは、FocusOut イベントがその前のターゲットで発生してキーボード フォーカスを失います。

フォーカス マネージャ

View には FocusManager があり、View のキーボード フォーカスを管理します。View のフォーカス マネージャは focus-manager オプションにより指定されます。各フォーカス マネージャはキーボードのフォーカス ターゲットを保持します。フォーカス ターゲットとは、フォーカス マネージャがオペレーティング システムから View 経由で受け取って送信する、任意の KeyEvent のターゲットのことです。
PointerEventFocusEvent および KeyEvent の送信方法の主な相違点を認識することが大切です。FocusEvent および KeyEvent がフォーカス マネージャを通して送出されるのに対し、PointerEvent はグラフィック階層を通して転送されます。
たとえば、Curl アプレットで 2 つの View が開いていて、それぞれの ViewFocusManager を持っているとします。キーボード イベントがオペレーティング システムから送出される場合、現在の Curl アプレットの GuiManager を介して、キーボード フォーカスを持つ View に送出されます。次に ViewFocusManager を介して、その View のキーボード フォーカスを持つオブジェクトに送出されます。
オブジェクトは、メソッド Visual.request-key-focus を呼び出してキーボード フォーカスを要求します。同様に、目標オブジェクトはそのメソッド Visual.release-key-focus を呼び出してキーボード フォーカスを解放することができます。さらに、FocusManager は、別のオブジェクトがキーボード フォーカスを要求する場合、EventTarget からキーボード フォーカスを解放できます。
フォーカス マネージャは、グラフィック階層で現在のキーボード フォーカスを持つオブジェクトを決定するだけでなく、フォーカス マネージャ階層内で現在選択されているオブジェクトを追跡し、切り取り、コピー、貼り付けなどの操作を処理します。
また FocusManager は、アクティブなメニュー バーになる MenuBar の情報を記録します。これにより、ユーザーが Alt キーを押すとアクティブ メニューがポップ アップ表示されます。

キー イベント

オブジェクトがキーボード フォーカスを得ると、オブジェクトはその View のすべてのキーボード イベントのターゲットになります。キーボード イベントとは、KeyEvent の任意のサブクラスのインスタンスで以下が含まれます。
キー イベントは次の 2 つに大別されます。

Raw キー イベントとキー コード

Raw キー イベントはキーボードの物理キーが押されたまたは解放されたことを伝え、普通はビデオ ゲームなどの、キーの使用で生成される値よりも物理キーが押されたまたは離されたというアクションに重点が置かれるようなアプリケーションで使用されます。
物理キーが押されるたびに RawKeyPress イベントが生成され、物理キーが離されるたびに、RawKeyRelease イベントが生成されます。
各イベントでは、RawKeyEvent.keycode プロパティのキー コードにより、押されたまたは解放されたキーを識別します。
RawKeyEvent.keycode の値はクラス Keycode のオブジェクトである必要があります。このクラスは、共通キーのそれぞれに対する次のクラス定数(たとえば、Keycode.aKeycode.b など、Keycode.alt-leftKeycode.backspace および Keycode.space)を持ちます。クラス プロシージャ Keycode.keycode-for-name を使用して、任意の文字の Keycode を見つけることができます。別のクラス プロシージャ Keycode.name-for-keycode では、引数として提供する任意の Keycode に対してキーの名前を返します。
次の例では、各 RawKeyPress イベントに対して数値を表示することにより、キーが押されたことに応答する Frame を示しています。Frame をクリックしてキーボード フォーカスを与えます (テキストを直接クリックしないでください)。キーを押すたびに、最後に押したキーのキー コードを表すキー名が表示されます。Home キーを押して終了するまでこれが続きます。

例: RawKeyPressKeycode を表示
||  To display the key code as a Dynamic object
{let d:Dynamic = {Dynamic "(none)"}}
{Frame
    width=2in,
    height=1in,
    background={Color.from-string "#ab29df"},
    valign="center",
    halign="center",
    || Display the key code value
    {text Keycode: {value d}},

    || PointerPress event handler
    {on p:PointerPress at f:Frame do
        {f.request-key-focus}
        set f.background = "pink"
        || Consume the PointerPress event so that it is
        || not fired further up the graphic hierarchy
        {p.consume}
    },

    || Event handler for RawKeyPress
    {on rkp:RawKeyPress do
        || Sets the Dynamic object's value to the key code
        ||  that identifies the physical key pressed
        set d.value = {Keycode.name-for-keycode rkp.keycode}
        {if rkp.keycode == Keycode.home then
            {popup-message "You've hit the winning key!"}}
    }
}

Cooked キー イベントとキー プレス値

前のセクションで説明したように、Raw キー イベントのイベント ハンドラでは、どの物理キーが押されたかに焦点がおかれています。Shift キーを押しながら a キーを押すと、2 つの異なる Raw キー プレス イベント (Keycode.a のキー コードを持つイベントと、Keycode.shift-left または Keycode.shift-right のキー コードを持つイベント) が発生します。
これに対して、単一 Cooked キー イベントは、ユーザーが 1 つ以上の修飾キーと非修飾キーを組み合わせて押すことにより発生します。ユーザーが、Shift キーを押しながら、または CapsLock キーが押された状態で、a キーを押すと、値 'A' を持つ単一 Cooked キー イベントのみが生成されます。
Shift キーなどの修飾キーまたは CapsLock キーなどのロック キーを押すだけでは、Cooked キー イベントが発生しないことに注意してください。
また、Cooked キー イベントではユーザーが実行した操作に関する重要な情報を保存してその値を生成します。たとえば、Cooked キー イベントはイベント時に Shift キーが押されたかどうかは記録しても、それが左シフト キーか右シフト キーかは記録しません。
Cooked キー イベントに関連する値は、char 型として KeyPress.value プロパティに保存されます。この値の表現方法は、標準または特殊の出力表現によって異なります。
キー定数値は、次の構文で表現されます。
KeyPressValue. key-name
ここで、key-nameKeyPressValue クラスの定数クラス フィールドの 1 つです。
『API リファレンス』で KeyPressValue クラスを検索し、そのフィールドのドキュメントを参照して、有効なキー定数値を確認します。
たとえば、Cooked キー イベントの値が Shift+e を押して生成された値であるかどうかを調べるには、次のイベント ハンドラ コードを書き込みます。
{on kp:KeyPress do
    {if kp.value == 'E' then
        ...
    }
}
また、Cooked キー イベントの値が Home を押して生成された値であるかどうかを調べるには、次のイベント ハンドラ コードを書き込みます。
{on kp:KeyPress do
    {if kp.value == KeyPressValue.home then
        ...
    }
}

キー プレス プロパティ

Cooked キー イベントの value 属性だけが、アプリケーションで重要なプロパティではありません。たとえば、9 キーまたは Ctrl+9 キー組み合わせのいずれの場合も、同じキーの値 '9' を生成しますが、通常 2 つのイベントはまったく異なるものとして扱われます。
このため、KeyPress クラスではクエリ可能な他のプロパティが保持されます。この戻り値は、次の質問の回答に相当します。
Cooked キー イベントが生成されたときに対応する修飾キーが押されていた場合、次のプロパティ値は true になります。
使用された修飾キーを示すプロパティのほかに、KeyPress イベントをトリガした動作に関する情報を示すキー プレス プロパティがあります。以下のプロパティはすべて bool 型の値を持ちます。
たとえば、a または Ctrl+a を押しても、操作の値は両方とも'a' になります。しかし、Ctrl+a を押す操作で生成された 'a'TextField に挿入することはないはずです。
Ctrl+a では、normal?true ですが、unmodified?false になります。したがって、insertable? も同様に false になります。

キー プレス値およびキー プレス プロパティの表示

このセクションでは、さまざまな Cooked キー イベントの KeyPress 値と属性を例を使用して説明します。
この例では、赤い四角形をクリックするとキーボード フォーカスが要求されます。キー フォーカスを得ると、FocusIn イベントが発生します。対応するイベント ハンドラがこれを緑に変えます。次に任意のキーを押すと、結果の KeyPress イベント値、修飾キー プロパティおよび追加プロパティが表示されます。
たとえば、ここで a キーを押すと、通常は値 'a' が生成されます。しかし、Shift または CapsLock のいずれか (両方ではない) がその時にアクティブである場合は、値 'A' が生成されます。値が標準であり、特殊なキー定数ではないので、KeyPress.normal? はいずれの場合でも真です。KeyPress.shifty? は、両方とも偽になります。これは、Shift キーが押されている場合では、値 'A' の生成に組み込まれているためです。
別の例として、Home キーをキーパッドで押すと、通常はキー定数値 KeyPressValue.home が生成されます。従って、KeyPress.normal? は偽です。KeyPress.shifty?true になります。これは、Shift+Home を押しても KeyPress 値は変わらないためです。キー プレス イベントでは Shift を使わずに異なる KeyPress 値が計算されます。ただし、NumLock が押されている場合、結果の KeyPress 値は '7' になります。この場合 KeyPress.normal? は真 (KeyPress 値が標準であるため) で、KeyPress.shifty? は偽です。その理由は、Shift を押すと、異なる KeyPress 値、すなわち KeyPressValue.homeを計算するために使用されるからです。
注意: KeyPress 値を表示するには、コードは KeyPressValue.name-for-char を使って char 値を String に変換します。

例: キー プレス値およびプロパティの表示
{value
    let label:TextDisplay =
        {TextDisplay
            font-size = 9pt,
            font-family = "monospace",
            value="Click in the rectangle below to get the focus...."
        }

    let rect:RectangleGraphic =
        {RectangleGraphic
            fill-color = "red",
            width = 6in,
            height = 1in,
            {on e:PointerPress at r:RectangleGraphic do
                || Checks that the left mouse button was clicked
                {if e.button == left-button then
                    || The RectangleGraphic requests the keyboard focus
                    || for itself
                    {r.request-key-focus}
                    || Consumes the event to prevent further firing of
                    || the PointerPress event up the graphic hierarchy
                    {e.consume}
                }
            },
            || When the RectangleGraphic gets the keyboard focus,
            || FocusIn is fired at it
            {on e:FocusIn at r:RectangleGraphic do
                || Changes the color of the rectangle
                set r.fill-color = "green"
                || Changes its text
                set label.value =
                    "Press keys and see how this display changes...."
                {e.consume}
            },
            || When the RectangleGraphic loses focus,
            || FocusOut is fired at it
            {on e:FocusOut at r:RectangleGraphic do
                || Changes the color back to red
                set r.fill-color = "red"
                set label.value =
                    "Click in the rectangle below to get the focus...."
                {e.consume}
            },
            || This event handler handles cooked key events
            {on e:KeyPress do
                || Sets the text to show the key value and properties
                set label.value =

                || name-for-char converts key values to a String
                || This is necessary for undisplayable key
                || constant values (KeyPressValue.*)
                || format displays the String
                "Key Value="&
                {format "%-6s", {KeyPressValue.name-for-char e.value}}&

                || Displays the properties if they are true
                "  Flags="&
                {if e.normal? then "(Normal)" else ""}&
                {if e.shifty? then "(Shifty)" else ""}&
                {if e.keypad? then "(Keypad)" else ""}&
                {if e.repeat? then "(Repeat)" else ""}&
                {if e.unmodified? then "(Unmodified)" else ""}&
                {if e.insertable? then "(Insertable)" else ""}&

                || Displays the modifiers if they are true
                "  Modifiers="&
                {if e.alt? then "(Alt)" else ""}&
                {if e.ctrl? then "(Ctrl)" else ""}&
                {if e.shift? then "(Shift)" else ""}
                {e.consume}
            }
        }
    {VBox
        label,
        {Fill height=0.2in},
        rect
    }
}

キー イベントの処理

次のステップでは、ユーザーからのキー イベントのキャプチャと応答を実行するアプレットを作成する場合の全般的な手順を示します。
  1. オブジェクトを作成します。
  2. キーボード フォーカスを取得する手段を含むオブジェクトを作成します。
    最も簡単な方法は、ポインタ クリック (PointerPress) などの明確な動作に応答する Visual.request-key-focus を呼び出すことです。
  3. 予想されるさまざまな KeyPress イベント (RawKeyPress が適切な場合はこのイベント) のイベント ハンドラを書きます。
イベント ハンドラが、不適切な KeyPress イベントに応答しないことが大切です。
たとえば、上向き矢印が押されると応答するというイベント ハンドラでは、修飾キーが使用されているときには上向き矢印が押されても応答しないはずです。この場合、unmodified? プロパティが役に立ちます。
あるいは、文字や数値、その他の印刷可能な文字に応答するイベント ハンドラでは、矢印キーやファンクション キーなど特殊なキーが押されても、または、任意の修飾キー (Shift は可能であれば除く) が使用されている状態で標準キーが押されても、これに応答しないようにする必要があります。この場合、insertable? プロパティが役に立ちます。
以下の 2 つのセクションでは、ユーザーのキーボード入力を分析するアプレットの例を示します。

簡単なテキスト エディタ

次の例では、ユーザーがテキストを入力して編集すると四角形の下に表示されます。
KeyPress イベントに応答するイベント ハンドラは、以下を実行します。

例: 簡単なテキスト エディタ
{value
    || The instructions
    let instruct:TextDisplay =
        {TextDisplay
            font-size = 9pt,
            font-family="monospace",
            value="Click in the rectangle below to get the focus...."}
    || The editable text
    let txt:TextDisplay =
        {TextDisplay
            font-size = 9pt,
            font-family = "monospace",
            value = ""
        }
    || The rectangle that requests and gets the keyboard focus
    || after which, it gets all keyboard events
    let rect:RectangleGraphic =
        {RectangleGraphic
            fill-color="red",
            width=6in,
            height=1in,

            || Respond to pointer clicks
            {on e:PointerPress at r:RectangleGraphic do
                {if e.button == left-button then
                    {r.request-key-focus}
                    {e.consume}
                }
            },

            || Respond to getting the focus
            {on e:FocusIn at r:RectangleGraphic do
                set r.fill-color="green"
                set instruct.value =
                    "Press keys to edit the text below the rectangle...."
                {e.consume}
            },

            || Respond to losing the focus
            {on e:FocusOut at r:RectangleGraphic do
                set r.fill-color="red"
                set instruct.value =
                    "Click in the rectangle below to get the focus...."
                {e.consume}
            },

            || Respond to KeyPress events
            {on e:KeyPress do

                {if e.insertable? then

                    || The user has pressed a normal key without
                    || pressing any significant modifier keys, so the
                    || KeyPress is "insertable".

                    || Append the value to the txt
                    set txt.value = txt.value & e.value

                 elseif (e.value == KeyPressValue.backspace and
                         e.unmodified?) then

                    || The user has pressed the backspace key
                    || without significant modifier keys.

                    let str:String = txt.value

                    || Clear the text, replacing it with String
                    || less one character
                    {if str.size > 0 then
                        set txt.value = {str.substr 0, str.size - 1}
                    }

                 elseif (e.value == KeyPressValue.esc and
                         e.unmodified?) then

                    || The user has pressed the escape key
                    || without any significant modifier keys.

                    || Clear the text entirely
                    set txt.value = ""
                }
                {e.consume}
            }
        }

    {VBox
        instruct,
        {Fill height=0.2in},
        rect,
        {Fill height=0.2in},
        txt
    }
}

shifty? の使用

このセクションでは、KeyPress イベント ハンドラを使用して、キー プレス値 KeyPressValue.home のさまざまな生成方法がどのように検出されるかを示します。
KeyPress イベントに応答するイベント ハンドラは、以下を実行します。

例: shifty?の使用
{let d-txt:Dynamic=
    {Dynamic "Click the Frame to get the keyboard focus."}}
{spaced-hbox
    valign="center",
    {Frame
        width=1in,
        height=1in,
        background="cyan",
        {on pp:PointerPress at fr:Frame do
            {fr.request-key-focus}
            {pp.consume}},
        {on FocusIn at fr:Frame do
            set fr.background = "blue"
            set d-txt.value="Press Shift+Home using different Home keys."},
        {on FocusOut at fr:Frame do
            set fr.background = "cyan"
            set d-txt.value=
            "Click the Frame to regain the keyboard focus."},
        || The event handler that responds to cooked key events
        {on kp:KeyPress do
            || Checks if the Home key on the keypad is used
            {if kp.value == KeyPressValue.home and kp.keypad? then
                || Checks if the Shift key is used
                {if kp.shift? then
                    || Checks if the Shift key was relevant to the app
                    {if kp.shifty? then
                        set d-txt.value =
                            "You used Shift+Home on the keypad, but " &
                        "did not use NumLock."

                     else
                        || This runs when Shift key was needed to compute
                        || the key value
                        set d-txt.value =
                            "You used Shift+Home on the keypad, and " &
                        "used NumLock too."
                    }
                 else
                    || This runs when Shift was not pressed
                    set d-txt.value = "You used Home on the keypad. " &
                    "Now try Shift+Home."
                }

             elseif kp.value == '7' and kp.keypad? then
                set d-txt.value = "The key value is not KeyPressValue.home; " &
                "it's '7'. NumLock is on."

             elseif kp.value == KeyPressValue.home then
                {if kp.shift? then
                    set d-txt.value = "You used Shift+Home on the special pad. " &
                    "Now use Home on the keypad."

                 else
                    set d-txt.value = "You used Home on the special pad. " &
                    "Now use Home on the keypad."
                }

             else
                set d-txt.value="Press Shift+Home using different " &
                "Home keys."
            }
        }
    },
    {TextFlowBox {value d-txt}}
}