Статьи

JavaFX — создание компонента

В этой статье описывается, как создавать новые компоненты для платформы JavaFX с акцентом на использование графических средств при создании GUI.


Постановка задачи

Обычно используемый выбор даты не является одним из виджетов, включенных в
дистрибутив JavaFX. Здесь вы узнаете, как создать этот виджет с нуля.

Как и в большинстве реализаций, наш компонент должен отображать даты с использованием имен дней недели и месяцев на родном языке пользователя.


Среда разработки

Для создания компонента вам потребуется бесплатная среда IDE NetBeans с установленным плагином JavaFX.

Для подготовки прототипа и обработки графики вы можете использовать любой редактор растровой графики. В этой статье мы используем редактор Gimp.

Подготовка прототипа

Внешний вид компонента можно подготовить в графическом редакторе.

Предположим, что наш прототип выглядит следующим образом:

На чертеже вы можете увидеть внешний вид и расположение элементов графического интерфейса. Вы должны сохранить изображение вашего прототипа в формате, который позволяет хранить элементы в отдельных слоях. Наиболее часто используемый формат этого типа — .psd, предоставляемый Adobe Photoshop (редактор Gimp имеет свой собственный аналогичный формат).

Далее вы будете воспроизводить рисунок с использованием средств JavaFX и добавите функциональность.

Создание компонентов

Я п для того , чтобы выбрать даты, Вы можете использовать отдельное окно диалога или облегченную-форму. В этом примере показан легкий компонент, который использует JPopupMenu.

Create the FxDateChooser.fx JavaFX class:

class FxDateChooser {
operation choose(parent:Widget);
attribute choosen:String;
attribute color:Color;
private attribute popupChooser:JPopupMenu;
private attribute days:Button*;
private attribute currentMonth:Number;
private attribute currentMonthName:String;
private attribute currentYear:Number;
operation fillDays();
operation setMonth();
}

The popupChooser initializes in a trigger, is shown in the choose operation, and a result is placed into the chosen attribute.

Choice Between Graphics and Code

This prototype’s elements can be created by using different JavaFX widgets or by pasting together pieces of a drawing.

For example, the band with buttons used in the prototype looks like a piece of glass. Similar elements became more commonly used with the advent of the Aqua interface for Apple computers:

«Glass» buttons have a spectacular appearance and are easy to render. As a rule, glass buttons have a rectangular shape with rounded corners that are filled with a colored gradient. The colored gradient changes from a darker tone to a lighter one. They also include a shadow, a title and a semi-transparent white band as a flare.

Here is an example of a glass button that only uses JavaFX facilities:

import javafx.ui.*;
import javafx.ui.canvas.*;
import javafx.ui.filter.*;
import javafx.ui.Canvas;
import java.awt.Dimension;

Frame {
height: 150
width: 400
visible: true
content: BorderPanel {
border: EmptyBorder {left:32 top:32 right:32 bottom:32}
center: Canvas {
background: new Color(0,0.7,0.9,1)
content: View {
sizeToFitCanvas: true
content: BorderPanel {
var: main
center: Canvas {
content:
[
View{
filter: ShadowFilter{
distance: 0
opacity: 1
radius: 8
}
content: BorderPanel {
border: EmptyBorder {
left: 4
top: 4
right: 4
bottom: 4
}
center: Canvas {
content: Rect {
x:0
y:0
width: bind main.width-16
height: bind main.height-16
arcWidth: bind main.height-16
arcHeight: bind main.height-16
fill: LinearGradient {
x1:0 y1:0.3 x2:0 y2:1
stops:
[
Stop {offset: 0.0
color: new Color(0,0.7,0.2,1)},
Stop {offset: 1.0
color: new Color(1,1,1,1)}
]
}
visible: true
}
}
}
}
,View {
filter: ShadowFilter{
distance: 1
opacity: 1
radius: 5
}
sizeToFitCanvas: true
content: BorderPanel {
border: EmptyBorder {
left: 8
top: 0
right: 8
bottom: 12
}
center: Button {
text: "Press me!"
cursor: HAND
focusPainted: false
contentAreaFilled: false
font: new Font("SanSerif", "BOLD", 12)
background: new Color(1,1,1,0)
foreground: new Color(1,1,1,1)
action: operation() {
MessageDialog {
message: "Click!"
visible: true
};
}
}
}
visible: true
}
,Rect {
x:bind main.height/3
y:10
width: bind main.width-main.height/3-main.height/3
height: bind main.height/3
arcWidth: bind main.height/3
arcHeight: bind main.height/3
fill: new Color(1,1,1,0.3)
visible: true
}
]
}
}
}
}
}
}

This script results in the following:

Although the button’s code is not difficult, it’s size is quite large. It is more convenient to implement a glass panel by pasting images.

The widget’s background has a view of a flare with a stretched out JavaFX title. The title can be inserted as an image. In order to change the component’s appearance, background can be filled in with the RadialGradient color.

Panels with day numbers can be easily implemented by means of buttons (the Button component). You will need to remove buttons’ standard filling and painting of a focus. See the example below:

foreach(i in [1..49]) Button {
var: me
border: EmptyBorder { left:2 top: 2 right: 2 bottom: 2}
contentAreaFilled: false
focusPainted: false
foreground: bind if i<8 then new Color(1,1,1,1) else color
font: bind if i<8 then new Font("SanSerif", "BOLD", 9)
else if i%7==0
then new Font("SanSerif", "ITALIC", 10)
else if (i+1)%7==0
then new Font("SanSerif", "ITALIC", 10)
else new Font("SanSerif", "BOLD", 10)
text: "{i}"
action: operation() {
if(i>7) {
if(me.text.length()>0) {
choosed="{currentYear}-{currentMonth+1}-{me.text}";
popupChooser.visible=false;
}
}
}
onMouseEntered: operation (e:MouseEvent) {
if(i>7) {
if(me.text.length()>0) {
me.background=new Color(1,1,1,0.7);
}
}
}
onMouseExited: operation (e:MouseEvent) {
if(i>7) {
if(me.text.length()>0) {
me.background=new Color(1,1,1,0.3);
}
}
}
};

 

This code creates an array of 49 buttons for the names of week days and the numbers of the days of the month. You can set rules to highlight elements in the onMouseEntered and onMouseExited events.

Prototype Slicing

To create a glass panel with buttons, cut the prototype into parts and save each piece in a separate file. You have the following six pieces:


  • a button for moving to the previous month

  • a panel with a month name
  • a button for moving to the next month
  • a button for moving to the previous year
  • a panel with a year
  • a button for moving to the next year

Open the prototype in the editor and on the layers panel make all elements which do not relate to the panel with buttons invisible.

For cutting in the Gimp editor use the following menu item

Image/Transform/Gullotine

Then select the required piece of the image and choose the following menu item:

Image/Crop Image 

to cut off unnecessary pieces:

Repeat the process above for all six elements and save them in separate files. Before saving, make the image’s background invisible (in this example it is green-blue).

The best format for storing separate image pieces is .png, since this format uses compression without losses and allows you to save the transparency’s alpha channel.

Four images of buttons should be redoubled and the arrows’ brightness should be increased. Those copies will be used for highlighting.

Here is an example of the code for a button that uses the cut image:

Button {
cursor: HAND
focusPainted: false
contentAreaFilled: false
background: new Color(1,1,1,0.0)
icon: Image { url: "auxiliary/datechooser/images/left.png" }
border: EmptyBorder { top: 0 left: 0 bottom: 0 right: 0 }
rolloverIcon: Image { url: "auxiliary/datechooser/images/leftHover.png" }
action: operation() {
currentMonth=currentMonth-1;
if(currentMonth<0) {
currentMonth=11;
currentYear=currentYear-1;
}
setMonth();
}
}


Composing Elements

Your calendar will have two layers: one layer for the background with the JavaFX title and another for the toolbar buttons and month numbers.

To arrange the calendar by layers, you can use either the StackPanel widget or the Canvas with an array of View elements. Place the BorderPanel, which contains a toolbar in the top area and buttons with numbers in the center area in the upper layer. For convenience, you can place the buttons that show day numbers in the GridPanel.

Gaps between elements will be defined by specifying the panels’ EmptyBorder parameters.


Adding Functionality

In a trigger on component’s creation add text to the names of week days:

var now = Calendar.getInstance();
var dayNames = new DateFormatSymbols().getShortWeekdays();
while(now.get(Calendar.DAY_OF_WEEK) <> now.getFirstDayOfWeek()) {
now.add(Calendar.DATE, 1);
}
for (i in [0..6]) {
days[i].text=dayNames[now.get(Calendar.DAY_OF_WEEK)];
days[i].background=new Color(1,1,1,0);
now.add(Calendar.DATE, 1);
}

In the same manner you can hide, display, or fill buttons for the days when the current month or year changes. The code for filling in the calendar is common on sites with Java examples.


Localization

Our component will display all titles based on the locale selected on a given computer:

Therefore it is necessary to provide for sufficient text size since titles in some languages may be long and difficult to fit in the allocated cells.


Use Case

In order to test the widget, you can create a simple form with a calendar calling button:

Button
{
var:me
text: bind chooser.choosen
action: operation()
{
chooser.choose(me);
}
}

Then add the link to your component to the text script:

import auxiliary.datechooser.FxDateChooser; 

and the initialization code specifying the color and initial value:

var chooser=FxDateChooser 
{
choosen: "Test!"
color: new Color(0,0,0.2,1)
};


Source Code

dch.zip — 300kb

— the source code and prototype in the .psd-format.