カスタム コントロールの作成。

アプリケーション独自のコントロールを作成する一番簡単な方法は、Curl® GUI Toolkit 内にある既存のコントロール クラスのサブクラスを作成することです。 親クラス内のメソッドをオーバーライドすると(特にスタティック イベント ハンドラ)、コントロールの挙動をカスタマイズすることが出来ます。全く新しい機能のコントロールを作成する必要がある時は、ControlFrameValueControlFrame-ofMultiUIControlFrameMultiUIValueControlFrame-of 等のような低レベルのコントロールのサブクラスを作成してください。
コントロールの外観を変更する最善の方法は、スキンコントロールとスタイルシートを使用することです。スタイルコントロールスタイルシート を御参照下さい。
コントロールのユーザー インターフェイス全体にわたるカスタマイズの詳細については、「カスタム コントロール UI の作成」を参照してください。

コントロールの機能の変更

サブクラス化したコントロールで継承したメソッドをオーバーライドすることにより、機能を変更して提供することもできます。例えば、ユーザーがクリックした時に、ログアウト処理を実行するログアウトボタンを作成するとします。以下のサンプルでは、その様なボタンを作成しています。Action イベントのスタティック イベント ハンドラでログアウト処理を実行します。サンプルでは、ボタンを含むダイアログを単に閉じることで、ログアウトをシミュレートしています。イベントとイベント ハンドラについての詳細は、 イベント の章を参照してください。

例: ボタンにログアウト機能を追加
{define-class LogoutButton {inherits CommandButton}

  {constructor {default ...}
    {construct-super ...}
    set self.label = "logout"
  }
  {method public {on-action e:Action}:void
    {e.consume}
    {if-non-null self.dialog then
        {self.dialog.close Dialog.cancel}
    }
  }
}
{let dialog:Dialog = 
    {Dialog
        width = 3cm, height = 3cm,
        {LogoutButton}
    }
}
{CommandButton
    label = "Open Dialog",
    {on Action at b:CommandButton do
        let v:#View = {b.get-view}
        {unless dialog.open? do
            {dialog.show modal? = false, owner = v}
        }
    }
}

リスト コントロールのカスタマイズ

クラス ListValueItem および ListSeparator をサブクラス化して、リスト コントロールをカスタマイズすることができます。これらのクラスは、ListBoxDropdownList、および ComboBox のデータ アイテムの書式と動作を定義します。データ モデルおよびリスト コントロールの詳細については、「リスト コントロール」のセクションを参照してください。プロテクトされたメソッド ListValueItem.adjust-visuals を使用すると、アイテムが選択および選択解除されたときの視覚的な応答方法を簡単に変更できます。
次の例では OutlinedListValueItem と呼ばれる ListValueItem のサブクラスを作成し、リスト アイテムが選択されたときにその周りに赤い縁取りをします。また OutlinedListValueItem を自動的に使用して渡されたデータの値のリストからリスト アイテムを作成する、OutlinedListBox と呼ばれる ListBox の特化型を作成します。OutlinedListBoxdefault-list-item-creation-proc と呼ばれるデータの値から ListItem を作成するクラス プロシージャを定義し、そのプロシージャをコンストラクタの construct-super 呼び出しで引数として渡します。

例: アウトライン リスト アイテムに対する ListBox の作成
{define-class public OutlinedListValueItem {inherits ListValueItem}

  {constructor public {default 
                          label:Visual,
                          value:any = null,
                          enabled?:bool = true
                      }
    {construct-super
        label,
        enabled? = enabled?,
        value = value
    }
    set self.border-width = 3px
    set self.margin = 1px
    set self.border-color = {self.background.to-FillPattern}
  }

  {method protected {adjust-visuals}:void
    {if self.selected? then
        set self.border-color = {FillPattern.get-red}
     else
        set self.border-color = {self.background.to-FillPattern}
    }
  }
}

{define-class public OutlinedListBox {inherits ListBox}

  || Constructor: pass on all arguments to parent,
  || substituting the proper default list-item-creation-proc
  {constructor {default ...}
    {construct-super
        list-item-creation-proc =
            OutlinedListBox.default-list-item-creation-proc,
        ...
    }
  }
  || specify the custom list-item-creation-proc
  {define-proc {default-list-item-creation-proc val:any}:ListItem
    {return
        {OutlinedListValueItem
            value = val,
            val
        }
    }
  }
}
{OutlinedListBox "Banana","Apple","Orange", "Peach"}

リンク ボタン コントロールの作成

既存のコントロールをサブクラス化するその他の例として、Web アプリケーションで広く使用されているボタン LinkButton のバージョンがあります。このボタンの外観はハイパーリンクのように見えますが、CommandButton のすべての機能を継承しています。コードをトリガして他の場所へ移動する場合に特に便利です。
LinkButton の方法は実に簡単です。CommandButton のスタイルを CommandButtonStyle.label-only に設定すると、ボタン自体が長方形の無い状態で描画されることを確認してみてください。このスタイルをすべての LinkButton に既定として設定し、適切なリアクティブ ラベルを追加すると、ハイパーリンクは次のようになります。

例: ハイパーリンクの CommandButton の作成
|| LinkButton --CommandButton with the appearance of a hyperlink
{define-class LinkButton {inherits CommandButton}

  {constructor {default
                   label:Visual = "LinkButton",
                   text-underline?:bool = true,
                   outline?:bool = false,
                   color:FillPattern = {FillPattern.get-blue},
                   color-rollover:FillPattern = {FillPattern.get-red},
                   color-pressed:FillPattern = {FillPattern.get-red},
                   ...
               }

    || 1. Set the style of the button appropriately
    ||    Also set button to inherit formatting from document
    {construct-super
        style = CommandButtonStyle.label-only,
        control-appearance-changeable? = true,
        ...
    }

    || 2. Add a reactive label based on clones of the label passed in
    let regLabel:#Visual = {label.clone-appearance}
    let roverLabel:#Visual = {label.clone-appearance}
    let pressLabel:#Visual = {label.clone-appearance}

    set regLabel.text-underline? = text-underline?
    set roverLabel.text-underline? = text-underline?
    set pressLabel.text-underline? = text-underline?

    set regLabel.color = color
    set roverLabel.color = color-rollover
    set pressLabel.color = color-pressed

    {if outline? then
        set regLabel =
            {Frame
                border-width = 1px,
                margin = 3px,
                border-color = color,
                regLabel
            }
        set roverLabel =
            {Frame
                border-width = 1px,
                margin = 3px,
                border-color = color-rollover,
                roverLabel
            }
        set pressLabel =
            {Frame
                border-width = 1px,
                margin = 3px,
                border-color = color-pressed,
                pressLabel
            }
    }

    {set self.reactive-label =
        {ReactiveLabel
            label = regLabel,
            label-rollover = roverLabel,
            label-pressed = pressLabel
        }
    }
  }
}

|| Examples
{LinkButton
    label = "Visit the White House Web Site (Curl language version)",
    {on Action at btn:CommandButton do
        {popup-message
            "Sorry this site is still under construction!"
        }
    }
}
{LinkButton
    label =
        {image source = {url "../../default/images/whlogo-big-stripe.gif"}},
    outline? = true,
    {on Action at btn:CommandButton do
        {popup-message
            "Sorry this site is still under construction!"
        }
    }
}

注意: LinkButtoncontrol-appearance-changeable? オプションを既定で true に設定します。通常このオプションは、他の GUI Toolkit コントロールでは既定で FALSE に設定されています。ここではオプションを true に設定し、LinkButton が既定でその親コンテナまたはドキュメントの書式属性を持つようにします。
前の例では、Visual.clone-appearance を使用して、ラベルの複数のコピー、つまりリアクティブ ラベルのそれぞれのビューをコンストラクタに渡す方法について見てきました。単一のグラフィカル オブジェクトをグラフィック階層内の複数の場所に置くことができないことを思い出してください。1 つの入力ラベルをリアクティブ ラベルの 3 つの部分すべてに使用しようとすると、予期しない結果を招くことがあります。

新しいコントロールの定義

アプリケーションが標準のどの GUI Toolkit とも大きく異なる外観と動作のインタラクティブなコントロールを必要とする場合は、既存のコントロールのサブクラス化および変更では不十分です。このセクションでは新しいコントロールを作成する方法について説明します。Curl IDE はCurl のコントロール クラスの API のソース コードを提供しています。Curl のコントロールがどのように実装されているかを分析することは、独自のコントロールの実装方法の理解に役立つでしょう。また Curl のコントロールのオープン ソース コードは開発の便利な始点ともなるでしょう。このリソースの検索と使用についての詳細は、「オープン コントロール」を参照してください。
外観と同様に、コントロールのユーザーに対するレスポンスについての重要な側面は、コントロール UI に実装されています。

コントロール UIの変更方法についての詳細は、「カスタム コントロール UI の作成」を参照してください。
コントロールクラスの階層」のセクションではCurl のコントロールが多くの動作を継承するクラスについて説明しています。必要なカスタム 機能のタイプによって、カスタム コントロールは1つ以上の上位クラスを土台にできます。
このセクションでは、コントロール機能を作成するための開始ポイントとして設計された 2 つのクラス、ControlFrame および ValueControlFrame-of の使用方法について説明します。MultiUIControlFrameMultiUIValueControlFrame-of の2つのクラスは1つ以上の外観を持つコントロールを作成する場合には便利ですが、それらのサブクラスの作成についてはこのセクションの範囲を超えていることに注意してください。どの基本クラスの使用を選択する場合も、API ドキュメンテーション を注意深く読み、オープン コントロール の ソースとドキュメンテーションを参考にしてください。
次のリストはカスタム コントロール作成の手順を要約しています。

値を持たない新しいコントロール

ControlFrame は情報を表示するコントロールの適切な開始ポイントです。ユーザーの対話が可能ですが、特定の値をユーザーから収集するためのものではありません。こうしたコントロールの例としては、CommandButton、Web ページのヒットカウンタ、株式市場やニュースを流すスクロールなどがあります。
最初に、ControlFrameBox の子孫にあたる BaseFrame の子孫であることに注意してください。この系統から、ControlFrame は任意のグラフィック レイアウトに配置し、グラフィカル オプションを継承し、フレームで起動されるさまざまなイベントについてスタティックおよびダイナミック イベント ハンドラを定義する機能を継承しています。BaseFrameFrame の親クラスであること、および単一のグラフィカルな子を保持するように設計されているコンテナであることに注意してください。
さらに、ControlFrame はダイアログに配置され、タブ移動が可能で、フォーカスを取得し、ニーモニック キーボード ショートカットに応答する機能を継承します。この機能の概要は、StandardControl とその親クラスで定義されます。
次の例では、現在の時刻を表示するだけの簡単なコントロールを作成します。コントロールはタブを使ってダイアログ内を移動したり、無効にすることができます。
ClockControl のコードは、現在の時刻にリンクされた Dynamic 文字列およびこのデータを定期的に更新する Timer です。この文字列は、適切なグラフィカル オブジェクトに変換され、コントロールのグラフィック階層に追加されます。

例: ControlFrame の使用
|| ClockControl

|| Basic control for viewing the current time.
|| This is a non-editable control akin to a TextDisplay.

{define-class public ClockControl {inherits ControlFrame}

  || Formatted time string, which will change dynamically
  field private time-display-string:Dynamic={Dynamic ""}

  || Constructor
  {constructor {default}
    || BaseFrame functionality
    || add a display view of the current time
    set self.time-display-string.value = {DateTime}.info.locale-time
    {self.add-internal
        {Frame
            border-width = 1px,
            margin = 2px,
            border-color = {self.background.to-FillPattern},
            {TextFlowBox text-selectable? = false,
                self.time-display-string
            }
        }
    }

    || add a timer to update the display.
    || an interval of 1s should show every second tick
    {self.animate
        interval = 1s,
        {on TimerEvent do
            {if self.enabled? then
                set self.time-display-string.value =
                    {DateTime}.info.locale-time
            }
        }
    }
  }
  || provide GuiEvent functionality:
  || on-focus-in:  give border that shows selected state
  {method public {on-focus-in e:FocusIn}:void
    {if not self.enabled? then
        {return}
    }
    let ch:Graphic = {self.graphical-children.read-one}
    set ch.border-color = {FillPattern.get-gray}
  }

  || on-focus-out: hide selected border
  {method public {on-focus-out e:FocusOut}:void
    {if not self.enabled? then
        {return}
    }
    let ch:Graphic = {self.graphical-children.read-one}
    set ch.border-color = {self.background.to-FillPattern}
  }

  || on-pointer-press: make control selectable with mouse
  {method public {on-pointer-press e:PointerPress}:void
    {if not self.enabled? then
        {return}
    }
    {e.consume}
    {self.request-key-focus}
  }
}

|| Example
{Dialog
    {spaced-vbox
        {ClockControl},
        {CheckButton
            label = "Clock Enabled",
            enabled? = true,
            value = true,
            {on ValueFinished at c:CheckButton do
                set c.parent.enabled? = c.value
            }
        }
    }
}

       

値を持つ新しいコントロール

特定の値をアプリケーションに入力するためのコントロールの場合、適切な開始ポイントはパラメータ化されたクラス ValueControlFrame-of です。このクラスは ControlFrame を継承しますが、関連付けられた値をクラス ValueControl-of からアクセスする標準的な方法も継承します。
次の例では、時間の選択に使用できる基本コントロールを作成します。この TimeSelectorControl の基本的な内容は、前に説明した ClockControl と非常によく似ています。時間を表示する動的文字列がコントロールのグラフィック階層に追加され、必要に応じて更新されます。TimeSelectorControl のグラフィカルな動作は ClockControl ともよく似ています。主な違いは、値が編集できることを示すために、テキストを枠で囲むのではなくハイライト表示することです。

例: ValueControlFrame-of クラスの使用
|| TimeSelectorControl = a control for selecting a time
||
|| This is an editable control,|with a value of DateTime.
|| For clarity, the Date portion of the time will always
|| be the date of the epoch, and times truncated to minutes
||
{define-class public TimeSelectorControl
  {inherits {ValueControlFrame-of DateTime}}

  || dynamic display strings for time:
  field private hrs-display:Dynamic = {Dynamic  "00"}
  field private mins-display:Dynamic = {Dynamic "00"}
  field private _value:DateTime = {DateTime}
  field private has-focus?:bool = false

  || pending-value? indicate whether user currently changing value
  field private _pending-value?:bool = false
  {getter {pending-value?}:bool
    {return self._pending-value?}
  }

  || Constructor
  {constructor {default val = {DateTime hour = 12}, ...}
    {construct-super  ...}
    set self._value = val

    || BaseFrame functionality
    || add a display view of the current time setting,
    || with appropriate event handlers
    {self.add-internal
        {HBox
            border-width = 1px,
            margin = 2px,
            border-color = {self.background.to-FillPattern},
            {TextFlowBox text-selectable? = false,
                self.hrs-display,
                {on p:PointerPress do
                    {if self.has-focus? then
                        {self.set-value-with-events self.value+1hr}
                        {p.consume}
                    }
                }
            },
            ":",
            {TextFlowBox text-selectable? = false,
                self.mins-display,
                {on p:PointerPress do
                    {if self.has-focus? then
                        {self.set-value-with-events self.value+1min}
                        {p.consume}
                    }
                }
            }
        }
    }
  }

  || GuiEvent functionality--
  || on-focus-in:
  {method public {on-focus-in e:FocusIn}:void
    {if not self.enabled? then {return}}
    let ch:Graphic = {self.graphical-children.read-one}
    set ch.background = "#000090"
    set ch.color = {FillPattern.get-white}
    set self.has-focus? = true
  }

  || on-focus-out:
  {method public {on-focus-out e:FocusOut}:void
    {if not self.enabled? then {return}}
    let ch:Graphic = {self.graphical-children.read-one}
    {unset ch.background}
    {unset ch.color}
    set self.has-focus? = false

    {if self._pending-value? then
        {self.enqueue-event {ValueFinished}}
        set self._pending-value? = false
    }
  }

  || on-pointer-press:
  {method public {on-pointer-press e:PointerPress}:void
    {if not self.enabled? then {return}}
    {e.consume}
    {self.request-key-focus}
  }

  || ValueControl-of Functionality
  {getter public open {has-value?}:bool {return true} }

  {method public open {unset-value}:void
    set self.value = {DateTime}
  }

  {getter public open {value}:DateTime
    {return self._value}
  }

  {setter public open {value d:DateTime}:void
    || Normalize time and trigger the display
    set self._value = {DateTime
                          hour = d.info.hour,
                          minute = d.info.minute
                      }
    set self.hrs-display.value = {TimeSelectorControl.padded-int d.info.hour}
    set self.mins-display.value = {TimeSelectorControl.padded-int d.info.minute}
  }

  || set-value-with-events: handle (or simulate) the user actually
  || setting/resetting the value with the ui
  {method public {set-value-with-events t:DateTime}:void
    set self.value = t || actually set the value (see above)
    {self.enqueue-event {ValueChanged}}
    set self._pending-value? = true
  }

  || padded-int: local utility to convert integer into two character string
  {define-proc {padded-int v:int}:String
    {return {if v <= 9 then {String "0"&v} else {String v}}}
  }

}


|| Display Example
{paragraph Click on hours or minutes to edit time, tab to shift focus.}
{Dialog
    {spaced-vbox
        {TimeSelectorControl},
        {CheckButton label = "Time Selector Enabled",
            enabled? = true,
            value = true,
            {on ValueFinished at c:CheckButton do
                set c.parent.enabled? = c.value
            }
        }
    }
}

このコントロールには、value を設定する代わりの方法が 2 つあることに注目してください。1 番目の方法は、継承した value のセッターをオーバーライドすることです。セッターの定義は絶対に必要ではありませんが、あれば便利です。このアクセッサの新しいコードではいくつかの時間フィールドに入力し、コントロールの表示を更新し、継承したセッター (super.value) を呼び出して新しい値を格納します。
値を変更する 2 番目のメソッド set-value-with-events も標準メソッドで、GUI Toolkit のすべてのValue コントロールで定義されています。呼び出して、ユーザーによるコントロールの実際の使用を実行 (またはシミュレーション) します。この役割は、コントロールの値を実際に変更するだけでなく、値を変更するための適切なイベントを生成し、コントロール自体で発生させることです。この場合、コントロールは値の変更すべてについてイベント ValueChanged を発生させる必要があります。コントロールがフォーカスを失うと、対応する ValueFinished が発生します。その他のコントロールは、特定のコントロールの意味に適したカスタム イベントのようなその他のイベント (通常は Action イベント) を発生させる必要があります。
ユーザーがコントロールを使用しているかどうか、値がさらに変更されるかどうかを示すための標準ゲッター pending-value? が実装されています。このメソッドは、GUI Toolkit のすべてのValue コントロールに実装されています。(詳細については「Value(値) コントロール」を参照してください。)

カスタム コントロールと自動テスト

このセクションでは、Mercury QuickTest Professional®(QTP) でテストをサポートするカスタム コントロールを記述する方法を説明します。 QTP テスト ツールに関する情報は Mercury QuickTest Professional をご覧ください。 QTP での Curl アプレットのテスト方法は Curl IDE ドキュメントを御覧下さい。
以下の例では MyObject と呼ばれる Frame のサブクラスを作成します。 MyObject は、いくつかのポインタ アクションのイベント ハンドラを提供しますが、 QTP テスト用の特別なサポートは実装してません。 この例を保存して実行し、テスト スクリプトを記録してみてください。 このオブジェクトの RawClickRawDoubleClick および MouseDrag などの操作は記録できます。

例: テスト サポート なし
{define-class public MyObject {inherits Frame}
  field private on?:bool
  field private grab?:bool
  field private within?:bool

  {constructor public {default ...}
    {construct-super background = "red", ...}
  }

  {method public open {on-pointer-press e:PointerPress}:void
    {if not e.consumed? then
        {e.continue-implicit-pointer-grab self}
        set self.grab? = true
        set self.within? = true
        set self.background = {if self.on? then "pink" else "lime"}
        {e.consume}
    }
    {super.on-pointer-press e}
  }

  {method public open {on-pointer-motion e:PointerMotion}:void
    {if not e.consumed? then
        {if self.grab? then
            let bounds:GRect = {self.layout.get-cell-bounds}
            set self.within? = {bounds.within? e.x, e.y}
            set self.background =
                {if self.within? == self.on? then "pink" else "lime"}
        }
        {e.consume}
    }
    {super.on-pointer-motion e}
  }

  {method public open {on-pointer-release e:PointerRelease}:void
    {if not e.consumed? then
        set self.grab? = false
        {if self.within? then
            set self.on? = not self.on?
        }
        set self.background = {if self.on? then "green" else "red"}
        {e.consume}
    }
    {super.on-pointer-release e}
  }
}
{MyObject width=1in, height=1in, test-name = "Clicker"}
次の例は前の例と似ていますが、QTP テスト サポートが追加されています。 保存、実行、そして記録すると、低位ポインタ アクションの代わりに、 Activate アクションが一つ記録されます。
例では以下の点に注意してください。 on-pointer-release イベント ハンドラは test-record を使って Activate を記録します。
{self.test-record "Activate"}
それぞれのイベント ハンドラは GuiInputEvent.test-recorded? を使って、 イベントがテスト記録済みであることをマークし、一般のイベントが記録されるのを防ぎます。
set e.test-recorded? = true
例では test-run メソッドをオーバーライドして Activate アクションをプレイ バックできるようにします。
{method public open {test-run method:String, args:FastArray}:any
    {if method == "Activate" then
        set self.on? = not self.on?
        {return null}
    }
    
    {return {super.test-run method, args}}
}    
ゲッター test-type-name をオーバーライドして新しいテスト タイプを定義します。
{getter public open {test-type-name}:#String
    {return "MyTestObject"}
}
get-test-property メソッドは、is on プロパティをサポートします。
{method public open {get-test-property name:String}:any
    {if name == "is on" then
        {return self.on?}
    }

    {return {super.get-test-property name}}
}
次の点にも注意してください。
Curl コントロール API はオープン ソースとして利用可能であり、 コントロールの QTP テスト サポートにおいて有用な情報源となります。 「オープン コントロール」を参照してください。

例: テスト サポート あり
{define-class public MyTestObject {inherits Frame}
  field private on?:bool
  field private grab?:bool
  field private within?:bool

  {constructor public {default ...}
    {construct-super background = "red", ...}
  }

  {method public open {on-pointer-press e:PointerPress}:void
    {if not e.consumed? then
        set e.test-recorded? = true
        {e.continue-implicit-pointer-grab self}
        set self.grab? = true
        set self.within? = true
        set self.background = {if self.on? then "pink" else "lime"}
        {e.consume}
    }
    {super.on-pointer-press e}
  }

  {method public open {on-pointer-motion e:PointerMotion}:void
    {if not e.consumed? then
        {if self.grab? then
            set e.test-recorded? = true
            let bounds:GRect = {self.layout.get-cell-bounds}
            set self.within? = {bounds.within? e.x, e.y}
            set self.background =
                {if self.within? == self.on? then "pink" else "lime"}
        }
        {e.consume}
    }
    {super.on-pointer-motion e}
  }

  {method public open {on-pointer-release e:PointerRelease}:void
    {if not e.consumed? then
        set e.test-recorded? = true
        set self.grab? = false
        {if self.within? then
            {self.test-record "Activate"}
            set self.on? = not self.on?
        }
        set self.background = {if self.on? then "green" else "red"}
        {e.consume}
    }
    {super.on-pointer-release e}
  }

  {method public open {test-run method:String, args:FastArray}:any
    {if method == "Activate" then
        set self.on? = not self.on?
        {return null}
    }

    {return {super.test-run method, args}}
  }

  {getter public open {test-type-name}:#String
    {return "MyTestObject"}
  }

  {method public open {get-test-property name:String}:any
    {if name == "is on" then
        {return self.on?}
    }

    {return {super.get-test-property name}}
  }

}

{MyTestObject width=1in, height=1in}