モジュール 3 タスク 2: テキストコントロールの追加
- 概要
-
- 検索テキストボックスを作成する
- グラデーション効果と不透明度を使用する
はじめに
このタスクでは、壁の最上部にタイトルバーを追加し、検索テキストボックスをその中に配置します。検索機能の結果は壁に読み込まれます。
プロジェクトの実行
- モジュール 3 タスク 2 NetBeans プロジェクトをダウンロードします。
- プロジェクトを実行します。
- 検索テキストボックスをクリックします。検索文字列を入力し、Enter キーを押します。マウスをテキストボックスに合わせると、デフォルトの検索テキストの不透明度が変化することに注意してください。
図 1
アーキテクチャー
このタスクでは、図 2 のようにカスタムノード TitleBar.fx および SearchTextBox.fx を追加します。
- TitleBar はサービス名 (テキスト文字列) のコンテナです。このタスクでは、サービス名は Yahoo! です。
- SearchTextBox では、
javafx.scene.control.TextBoxと WebSearch API が使用され、ユーザーが検索語句を入力すると新しい検索を呼び出します。 Wall.fxには、タイトルバーと検索テキストボックスが表示されます。
図 2
タイトルバーの作成
TitleBar は、タイトル、ファサード、ボーダー を含む CustomNode です。コンテンツは最終的に Wall.fx で初期化されます。
override function create(): Node {
return
Group {
content: [
facade,
border,
title
]
};
}
タイトルテキストの作成
TitleBar では、javafx.scene.text.Text を使用して、Web サービスを識別する serviceName を表示します。この場合、serviceName は、Yahoo! です。serviceName には、ラベル以上の意味があります。アプリケーションでは、serviceName が保持されます。将来の機能では、複数の Web サービス API がサポートされて、複数のサービス名が存在する可能性があり、これをサポートするためです。
def title : Text = Text {
font : titleFont
strokeWidth: 2
fill: Color.rgb(176, 198, 255)
translateX: bind (stageWidth - title.boundsInLocal.width) / 2
translateY: bind (facade.boundsInLocal.height
- title.boundsInLocal.height)/2 + titleFont.size
content: serviceName
}
Text のコンテンツを serviceName にバインドする必要はないことに注意してください。「タイトルバーと検索テキストボックスの Wall.fx への追加」で説明するように、serviceName は Wall.fx で初期化され、変更されません。また、テキストを縦方向に中央揃えする際に、フォントのサイズを考慮する必要があることにも注意してください。Text のデフォルトの発生元は、TextOrigin.BASELINE であることを思い出してください。そのため、フォントサイズには追加のオフセットが必要になります。TOP または BOTTOM を原点にして Text を中央揃えするには、下方の高さを調整するために余分な計算が必要になります。
titleFont は TitleBar.fx で定義されていますが、TitleBar クラスの外部にあります。これはスクリプトレベルの変数、つまり titleFont のインスタンスが 1 つだけ作成されるということを意味します。titleFont の定義は変更されないため、インスタンス変数にする必要はありません。
塗りつぶしの色は、SearchTextBox の境界線と同じ色になります。
リニアグラデーションの作成と、ファサードでの使用
リニアグラデーションの定義は、slate 変数に割り当てられています。stops は、色の値を変更するポイントのことです (API ドキュメントで javafx.scene.paint.LinearGradient.html を参照してください)。モジュール 2 ではもともと SCROLLCTL_COLOR 定数が定義されていたので、この定数を別の stops でも再利用します。titleFont のように、slate も定義が変更されないため、スクリプト変数として定義されています。
def slate = LinearGradient{
startX: 0
startY: 0
endX: 0
endY: 1
proportional: true
stops: [
Stop { offset: 0.0 color: Constants.SCROLLCTL_COLOR },
Stop { offset: 0.5 color: Color.BLACK }
Stop { offset: 1.0 color: Constants.SCROLLCTL_COLOR },
]
}
ファサードは、slate (リニアグラデーション) で塗りつぶされた長方形です。
def facade : Rectangle = Rectangle {
translateX: 0
translateY: 0
x: 0
y: 0
width: bind stageWidth
height: Constants.TITLE_BAR_FACADE_HEIGHT
opacity: 0.3
fill: slate
}
slate の定義を変えずに外観を明るくするため、不透明度が使用されていることに注意してください。
水平線の描画
border は、ファサードの上下に位置する明るい色の線を構成します。下記のコードでは、javafx.scene.shape.Path を使用して線を描画しています。
def border : Path = Path {
opacity: 0.3
stroke: Constants.SCROLLCTL_COLOR
strokeWidth: 2
elements: [
MoveTo{ x: 0, y: 0 },
HLineTo{ x: bind stageWidth },
MoveTo{ x: 0, y: Constants.TITLE_BAR_FACADE_HEIGHT },
HLineTo{ x: bind stageWidth }
]
}
配置は、ステージからの相対的な位置になります。Path は、鉛筆での描画と同様の機能と考えてください。MoveTo で、鉛筆を手に取って所定の位置に鉛筆を下ろします。HLineTo で、所定の位置から終点へ、実際の線が描画されます (この場合は水平線)。線の終点は、stageWidth にバインドされ、ステージの幅が変更されると再描画されます。
Path の代わりに javafx.scene.shape.LineTo を使用することもできます。この方法でコードを記述した場合、上部の線と下部の線のそれぞれに LineTo が 1 つずつ必要になります。
検索テキストボックスの作成
SearchTextBox では、javafx.lang.control.TextBox を使用してユーザーからの入力を受け付け、WebSearch を使用して新しい検索を開始します。
def searchTB : TextBox = TextBox {
columns : 15
visible : true
width : Constants.SEARCH_BOX_WIDTH
height : Constants.SEARCH_BOX_HEIGHT
focusable : true
selectOnFocus : true
action : function() {
searchTB.focusable = false;
getMetaDataforSearchText();
}
opacity: .25
onMouseEntered: function(ev : MouseEvent) : Void {
if ( not searchTB.focused ) {
searchTB.opacity = .75;
}
}
onMouseExited: function(ev : MouseEvent) : Void {
if ( not searchTB.focused ) {
searchTB.opacity = .25;
}
}
}
action 関数に注意してください。この関数は、アクションが発生したときに、通常は KeyCode.VK_ENTER への応答時に、TextBox から呼び出されます。focusable を false に設定している理由は、あとで説明します。
このクラスには onMouseEntered と onMouseExited アクションが含まれ、searchTB にフォーカスがない場合に、ボックスの不透明度を変更しています。searchTB にフォーカスがある場合は、マウスを合わせても不透明度は変更されません。これにより、「ここをクリックしてください」というアフォーダンスを効果的に示すことができます。
ここでは TextBox の外観を指定していないことにも注意してください。しかし、border には特定の色が設定され、角は丸みを帯びています。TextBox の外観は、スタイルシートを適用して制御します。プロジェクトの src/mediabrowser ディレクトリには、TextBoxSkin.css というファイルがあります。このファイルには、TextBox にフォーカスがある場合とない場合の、CSS スタイルが記述されています。このようなスタイルを使用するには、次のようなスタイルシートを Main.fx のシーンに追加する必要があります。
stylesheets: [
MediaComponent.css_skins,
Constants.TEXTBOX_CSS_SKIN
]
ユーザーが検索クエリーを入力し、Enter キーを押すと、getMetaDataforSearchText 関数が呼び出されます。
function getMetaDataforSearchText() : Void {
if ( clearSearchResults != null ) {
clearSearchResults();
}
webSearch.searchQuery = searchTB.value.trim();
webSearch.clearSearchResults();
webSearch.search();
}
SearchTextBox は WebSearch API を直接使用します。TextBox.value はコミットされたテキストの値です。つまり通常は、ユーザーが Enter キーを押したときの値です。clearSearchResults が定義されている場合は、この関数が呼び出されます。このオプションの関数によって、Wall に新しい検索結果を表示する準備ができます。同様に、WebSearch.clearSearchResults によって、新しい検索の準備をするように WebSearch インスタンスに通知されます。WebSearch の結果シーケンスを空にすることで、Wall からサムネイルが削除されます。
SearchTextBox の action では、searchTB.focusable を false にする呼び出しが発生します。これにより、ユーザーが Enter キーを押すと SearchTextBox はフォーカスを失い、Wall はキーボードナビゲーションのフォーカスを再取得できます。ただし、SearchTextBox は focusable に設定して、キーボード入力を受け取るようにする必要があります。次のコードブロックでは、searchTB.focused が false になったときに (action 関数で focusable が false に設定されたときのように)、focusable を true に再設定しています。詳細については、コード内のコメントを参照してください。
override var focused = bind searchTB.focused on replace {
if ( not focused ) {
searchTB.opacity = .25;
searchTB.focusable = true;
searchTB.value = webSearch.searchQuery
} else {
searchTB.opacity = .75
}
}
タイトルバーと検索テキストボックスの Wall.fx への追加
Wall.fx では、Wall にタイトルバーと検索テキストボックスが追加されます。
def wall : Group = Group {
content: [
titleBar,
searchTB,
thumbs,
scrollCtl
]
titleBar はステージの幅に広がり、serviceName は WebSearch インスタンスの serviceName に初期化されます。serviceName をバインドする必要がないことに注意してください。これは、webSearch.serviceName は、初期化後も変更されないためです。
def titleBar : TitleBar = TitleBar {
stageWidth: bind maxVisibleWidth;
serviceName : webSearch.serviceName
};
SearchTextBox は TitleBar の中央に配置され、右端から Constants.BORDER_WIDTH の値でオフセットされます。ここでは、clearSearchResults() の実装に注意してください。この関数は、新しい検索がキューに入る直前に、SearchTextBox から呼び出されます。thumbnails シーケンスは、SearchTextBox の webSearch.clearSearchResults() の呼び出しにより空になろうとしているため、Wall ではフォーカスのあるサムネイルがリセットされます。
def searchTB : SearchTextBox = SearchTextBox {
translateX : bind maxVisibleWidth - searchTB.boundsInLocal.width
- Constants.BORDER_WIDTH
translateY : bind (titleBar.boundsInLocal.height
- searchTB.boundsInLocal.height)/2
webSearch: webSearch
clearSearchResults: function () : Void {
thumbnails[thumbWithFocus].hasFocus = false;
thumbWithFocus = 0;
}
};
ユーザーが Enter キーを押すと、SearchTextBox のフォーカスが失われることを思い出してください。これにより、Wall はキーボードナビゲーションのフォーカスを再取得できます。Wall は SearchTextBox のフォーカスの状態が変更されたかどうかを searchBoxLostFocus 変数で監視します。この変数は、searchTB.focused の逆の値にバインドされています。検索テキストボックスがフォーカスを失うと、次のコードによって、Wall にフォーカスを要求するトリガーが発生します。
def searchBoxLostFocus = bind not searchTB.focused on replace {
if ( searchBoxLostFocus ) {
wall.requestFocus();
}
};
試してみましょう
- 別の検索文字を入力してみます。
TitleBar.fxで、LineTo を使って何か線を引いてみます (線を斜めに引くこともできます)。TextBoxSkin.cssのスタイルを変更して、SearchTextBox の外観を変更してみます。- WebSearch インスタンスの SearchTextBox から clearSearchResults を呼び出さないと、どうなるでしょうか?
SearchTextBox.focusedまたはWall.searchBoxLostFocusのいずれかで「on replace」ブロックをコメントアウトするとどうなるでしょうか?
