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

コントロールの外観を変更する最善の方法は、スタイル コントロールスタイル シート を使用することです。この章は、コントロールのユーザーインターフェースをより広い範囲で変更する必要がある時に役に立ちます。

コントロール UI の使用

UI オブジェクトは次のタスクを実行します。
既定では、それぞれのコントロールは、システムのために定義された外観と一致する実装をした基本の UI オブジェクトが関連付けられています。この関連付けを変更するには、コントロールの ui-object プロパティを別の UI オブジェクトに設定します。たとえば、CommandButton は 通常、SkinnableCommandButtonUI オブジェクトに関連付けられています。CommandButton に別のユーザー インターフェイスがほしい場合は、CommandButtonUI または、別の適切な基本クラスのサブクラスを作成することができます。
コントロール UI クラスの階層」の図では UI クラスの階層図を示しています。この図中のどのクラスからでもサブクラス化して独自の UI を作成することができます。いくつかの UI クラスは BaseButtonUI からも継承していることに注意してください。 BaseButtonUI は全てのボタン コントロールに共通しているイベント ハンドリング機能を持っています。
特定の制限がない限り、ControlUI または BaseButtonUI ではなく、CommandButtonUI のような「コントロール」UIクラスをサブクラス化することを推奨します。「コントロール」UI クラスは「未加工」のイベントを処理し、対応するコントロールで、コントロールが適用できるイベントを発生させるコードを含んでいます。これらのクラスからサブクラス化する場合は、コントロールの外観を記述するだけです。UI がまだ実装されていないユーザーの動作に対応する必要が場合は、イベント処理の部分もまた実装する必要がでてきます。
UI クラスを実装するときは、API リファレンス マニュアルを参照し、以下についての詳しい情報を得てください。
UI が高機能のレンダリング機能を必要とするときは、レンダリング作業を実行する為に Renderer2d を使いたくなるかもしれません。また、draw-as-* メソッドに加えて、draw メソッドを実装をする必要があるかもしれません。draw メソッドは、UI クラスが Graphic から継承されている場合のみ利用可能です。Curl 言語が提供しているUIクラスの全ては、Graphic のサブクラスです。

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

各コントロールの外観を定義するクラスについては「コントロール UI クラスの階層」で説明します。適切なサブクラスを作成し、コントロールの ui-object プロパティでコントロールが UI を使用するように設定できます。「楕円形のコマンド ボタンの作成」のセクションで、この方法について解説します。

新しい外観の定義

Curl 言語に備わっている外観メカニズムを介して、特定のアプレット内ですべてのコントロールの外観を操作できます。次のクラス階層は外観に関する主なクラスを示しています。
Graphic および Observable
LookAndFeel
StandardLookAndFeel
DelegatingLookAndFeel
外観クラスは、UI コントロールのグラフィカルな外観を決定するオプションを Graphic から継承します。StandardLookAndFeel はコントロールの標準 UI 外観を実装します。StandardLookAndFeel のインスタンスを作成し、オプションの値を変更して UI を変更します。StandardLookAndFeel または LookAndFeel のサブクラスを作成してより広範囲に変更できます。
さらに、Dialog および MultiUIControlFrame クラスは非ローカル オプション look-and-feel を実装しています。既定では、このオプションは the-default-look-and-feel に設定されています。DelegatingLookAndFeel.target-look-and-feel を介して StandardLookAndFeel を最終的にポイントする DelegatingLookAndFeel です。カスタマイズした LookAndFeel オブジェクトに look-and-feel オプションを設定できます。
次の例では、red-laf という StandardLookAndFeel のインスタンスを作成し、いくつかのオプションをリセットします。次に、コマンド ボタンを押したときに表示されるダイアログ ボックスの look-and-feel オプションを red-laf に設定します。コマンド ボタンをクリックし、ダイアログ内のすべてのコントロールに変更が反映されているが、例の中のその他のコントロールは変更されていないことを確認します。

例: 外観との連携
{let red-laf:StandardLookAndFeel =
    {StandardLookAndFeel}
}
{set red-laf.color = {FillPattern.get-orange}}
{set red-laf.control-color = {FillPattern.get-black}}
{set red-laf.background = {FillPattern.get-maroon}}

{define-proc
    {order-coffee milk?:bool, coffee-type:String}:void
    {popup-message
        {if milk? then
            {text Here is some {value coffee-type} with milk!}
         else
            {text Sorry, no milk with your {value coffee-type}!}
        }
    }
}
{value
    || Uncomment the following line to change the default look and feel
    || and modify the appearance of all controls
    ||    set the-default-look-and-feel.target-look-and-feel = red-laf
    {let milk?:bool = true}
    {let coffee-type:String = "Kenyan"}
    {let b:CommandButton =
        {CommandButton
            label = "Show non-modal dialog"
        }
    }
    || construct the dialog
    {let rf:RadioFrame =
        {radio-buttons
            "Kenyan",
            "Arabica",
            "Sumatran",
            value = "Kenyan"
        }
    }
    {let cb:CheckButton =
        {CheckButton
            label = "with milk?"}
    }
    {let ok:CommandButton =
        {CommandButton
            label = "OK",
            {on Action do
                {ok.dialog.commit}
                {ok.dialog.close Dialog.ok}
            }
        }
    }
    {let cancel:CommandButton =
        {CommandButton
            label = "Cancel",
            {on Action do
                {cancel.dialog.close Dialog.cancel}
            }
        }
    }
    ||This dialog comes up red, nothing else does.
    {let dialog:Dialog =
        {Dialog
            look-and-feel = red-laf,
            {spaced-vbox
                rf,
                cb,
                {spaced-hbox ok, cancel}
            },
            {on com:Commit do
                set milk? = cb.value
                set coffee-type = rf.value
                {order-coffee milk?, coffee-type}
                {com.consume}
            },
            {on WindowClose do
                set b.enabled? = true
            }
        }
    }
    {b.add-event-handler
        {on Action at b:CommandButton do
            let v:#View = {b.get-view}
            {unless dialog.open? do
                set b.enabled? = false
                {dialog.show modal? = false, owner = v}
            }
        }
    }
    b
}

既定の外観の変更

既定の外観を変更して、カスタム外観のスコープをさらに拡張できます。the-default-look-and-feel 定数は DelegatingLookAndFeel クラスのインスタンスです。DelegatingLookAndFeel の目的は、使用中の外観をある LookAndFeel オブジェクトから他のオブジェクトに切り替えることです。target-look-and-feel プロパティにより、Web アプリケーション内のすべてのコントロールが定義された外観を使用するように設定することができます。
前の例をもう一度見て、コードの次の行に注目してください。
set the-default-look-and-feel.target-look-and-feel = red-laf
この行は、target-look-and-feel を設定して red-laf で定義した外観がすべてのコントロールで使用されるようにします。この行を実行すると、現在実行中のアプレットにおいてこのページの任意の位置のすべてのコマンド ボタンを含め、すべてのコントロールで red-laf が既定の外観になるため、上記の例ではこの行はコメント アウトされています。
既定の外観をリセットしたときの効果を確認するには、このコード行のコメントを解除し、例で Execute ボタンをクリックします。コントロールを元の標準の既定色に戻す場合は、ページを再ロードします。例をアプレットとして保存して、Curl ドキュメント・ビューワの外で作業することも可能です。

新しい外観のカスタム コントロール UI の使用

上記の両方のアプローチを組み合わせることができます。つまり、StandardLookAndFeel クラスには、任意のコントロールのコントロール UI を新たに定義した UI に設定するための StandardLookAndFeel.register-ui メソッドが提供されています。これにより、新しい外観を使用するコントロールが登録されているコントロール UI を使用することになります。次のセクションの例で、この方法について説明します。

楕円形のコマンド ボタンの作成

このセクションの例では、コマンド ボタンの新しい UI オブジェクトを作成し、このオブジェクトを使って新しいコマンド ボタンの外観を提供します。このコマンド ボタン UI は EllipticalCommandButtonUI クラスで定義され、CommandButtonUI をサブクラス化します。このクラスには、ボタンの外観を定義するコードが含まれています。特別なイベント ハンドリングの必要がないので、イベント処理コードは含まれていません。
EllipticalCommandButtonUI クラスを使用するにはいくつかの方法があります。この例ではそれらのすべてを説明します。

例: 楕円形の CommandButton
{define-class public EllipticalCommandButtonUI {inherits CommandButtonUI}

  || Gradient FillPatterns for the unpressed and pressed button
  field private _inner-up-right-gfp:FillPattern =
      {uninitialized-value-for-type FillPattern}
  field private _inner-down-right-gfp:FillPattern =
      {uninitialized-value-for-type FillPattern}

  || Graphical structure for managing label
  field private _label-box:#Graphic
  field private _ellipse:EllipseGraphic = {EllipseGraphic}
  field private _label-shifted-forward?:bool = false

  {constructor public {default ...}
    {construct-super ...}
    {self.set-gradients self.control-color}
  }
  || notice when control color changes and update self appropriately
  {nonlocal-option public control-color:FillPattern
    {self.set-gradients control-color}
    {self.request-draw}
  }
  {method public {draw renderer2d:Renderer2d}:void
    let bounds:GRect = {self.layout.get-bounds}

    let top-edge:Distance    = -bounds.ascent
    let bottom-edge:Distance = bounds.descent
    let left-edge:Distance   = -bounds.lextent
    let right-edge:Distance  = bounds.rextent

    let w:Distance = right-edge  - left-edge
    let h:Distance = bottom-edge - top-edge

    || render the outer ellipse in gradient of control-color
    {renderer2d.render-ellipse
        left-edge,
        top-edge,
        w,h,
        fill-pattern = self._inner-up-right-gfp
    }
    || Note super.draw is necessary for the graphical children
    || (i.e.the label) to be drawn
    {super.draw renderer2d}

  }
  ||----------------------------------------
  || Methods inherited from CommandButtonUI
  ||----------------------------------------
  {method public {draw-as-normal}:void
    set self._ellipse.fill-color = self._inner-up-right-gfp
    {self.shift-label forward?=false}
    {self.request-draw}
  }
  {method public {draw-as-disabled}:void
    set self._ellipse.fill-color = self._inner-up-right-gfp
    {self.shift-label forward?=false}
    {self.request-draw}
  }
  {method public {draw-as-pressed}:void
    set self._ellipse.fill-color = self._inner-down-right-gfp
    {self.shift-label forward?=true}
    {self.request-draw}
  }
  {method public {draw-as-rollover}:void
    set self._ellipse.fill-color = self._inner-up-right-gfp
    {self.shift-label forward?=true}
    {self.request-draw}
  }
  {method public {modify-for-focus focus?:bool}:void
    {if focus? then
        set self._ellipse.color = {FillPattern.get-black}
     else
        set self._ellipse.color = self.control-color
    }
    {self.request-draw}
  }
  {method public {modify-for-default default?:bool}:void
  }
  {setter public {control control:Control}:void
    set super.control = control
    {self.setup-label}
  }
  {method public {react-to-visual-change}:void
    {self.setup-label}
  }
  ||-----------------------------------------------
  || Private utility methods
  ||----------------------------------------------
  {method private {shift-label forward?:bool=true}
    ||if label is already shifted appropriately, do nothing
    {if forward? == self._label-shifted-forward? then
        {return}
    }
    ||otherwise, shift label appropriately by pixel
    let lc:LayoutContext = {self.get-layout-context}
    let shift-distance:Distance = lc.layout-display-context.pixel-size
    {if not forward? then
        set shift-distance = -shift-distance
    }
    {self.child.shift-x-origin shift-distance, layout-context=lc}
    {self.child.shift-y-origin shift-distance, layout-context=lc}

    set self._label-shifted-forward? = not self._label-shifted-forward?
  }
  {method private {set-gradients bc:FillPattern}:void

    || set gradient patterns for the button up and down states
    set self._inner-up-right-gfp =
        {RadialGradientFillPattern
            {Spectrum.from-endpoints "white", bc},
            center = {Fraction2d 0.2, 0.2},
            radius = 1.0
        }
    set self._inner-down-right-gfp =
        {RadialGradientFillPattern
            {Spectrum.from-endpoints "white", bc},
            center = {Fraction2d 0.3, 0.3},
            radius = 1.0
        }
  }
  {method private {setup-label}:void
    let btn = self.control asa CommandButton
    {if-non-null label=btn.label then
        set self._ellipse =
            {EllipseGraphic
                horigin="center",
                vorigin="center",
                color = self._inner-up-right-gfp
            }
        set self._label-box =
            {OverlayBox
                self._ellipse,
                {HBox
                    {Fill width=15pt},
                    {VBox
                        {Fill height= 15pt},
                        {TextFlowBox
                            horigin="center",
                            vorigin="center",
                            label
                        },
                        {Fill height= 15pt}
                    },
                    {Fill width=15pt}
                }
            }
    }
    {self.add-internal self._label-box, replace?=true}
  }
}

{let ellipse-laf:StandardLookAndFeel =
    {StandardLookAndFeel}
}
||Registers EllipticalCommandButtonUI as the UI for command buttons
{let registered:bool =
    {ellipse-laf.register-ui
        CommandButton,
        EllipticalCommandButtonUI}
}
{set ellipse-laf.color="green"}
{set ellipse-laf.control-color="blue"}
{Dialog
    || The following line sets look and feel for this dialog and
    || its children to one that makes command buttons elliptical
    || rather than the default
    ||    look-and-feel = ellipse-laf,
    {spaced-vbox
        background="wheat",
        || CommandButton 1
        {CommandButton
            || The following line sets the UI object for this button
            || to the elliptical UI defined above
            ui-object = {EllipticalCommandButtonUI},
            control-color = {FillPattern.get-red},
            label={bold Uses ui-object}
        },
        || CommandButton 2
        {CommandButton
            || The following line sets look and feel for this button to one that
            || makes command buttons elliptical rather than the default
            look-and-feel = ellipse-laf,
            label={bold Uses look-and-feel}
        },
        || CommandButton 3
        {CommandButton
            enabled?=false,
            label={bold disabled}
        }
    }
}
|| The following line resets the default look and feel
|| {set the-default-look-and-feel.target-look-and-feel = ellipse-laf}