April 20, 2014

Day 13 CSS (Part 1)

ဤတစ်ခေါက် JavaFX အကြောင်းအား မိတ်ဆက်ရာတွင် GUI ၏ Styling နှင့် ပတ်သက်ပြီး ဖော်ပြသွားပါမည်။

Swing ဖြင့် GUI အပလီကေးရှင်းများအား ရေးသားဘူးသူဆိုပါက သိပါလိမ့်မည်။ Swing တွင် Component တစ်ခု၏ Font Style ဒါမှမဟုတ် အရောင်များအား ပြောင်းလည်းလိုပါက Properties များအား Set လုပ်၍ ပြောင်းလည်းရပါသည်။ သို့ပါသော်လည်း Application တစ်ခုလုံး၏ Style အား ပြောင်းလည်းလိုသည့် အခါတွင် လွယ်ကူမည်မဟုတ်ပေ။

Look And Feel အားပြောင်း၍ Application တစ်ခုလုံး၏ Style အား အသင့်ပေထားသော Style ဖြင့် ပြောင်းလည်းလိုသည့် အခါတွင် အစဉ်ပြေသော်လည်း၊ တစ်ချို့တစ်နေရာအား ပြောင်းလည်းလိုပါက လွယ်သည့် ကိစ္စတော့မဟုတ်ပေ။ ဒီလောက်ကလေး ပြောင်းဖို့အတွက် ဘာလို့ ဒီလောက် ကုဒ်တွေရေးနေရတာလဲ ဟု အကြိမ်ကြိမ် ဒေါပွခဲ့မိဘူးပါသည်။

Layout တွေလည်းချ Style တွေလည်း ပြောင်းပြီးရော Swing Class တစ်ခုသည် ပြန်ဖတ်ချင်စရာ မကောင်းလောက်အောင် Source Code တွေ ဖောင်းပွနေတတ်ပါသည်။ Business Logic နဲ့ မဆိုင်တဲ့ Style အတွက်ရေးနေရင်းနဲ့ Business Logic အတွက် အားနည်းသွားတတ်ပါသည်။ ဒါမှမဟုတ်ရင် Business Logic ကို အရမ်းအားပေးပြီး Style ကို ကရုမစိုက်နိုင်တော့ပဲ အလွန်နုံခြာတဲ့ အပလီကေးရှင်းတွေ ဖြစ်သွားတတ်ပါသည်။


Using CSS in JavaFX


လက်ရှိ JavaFX တွင် Style အား သတ်မှတ်နိုင်သော နည်း ၃မျိုးရှိကြပါသည်။ (Java SE 8 အရောက်တွင် JavaFX 8 ဖြစ်လာပြီး၊ ၎င်းတွင် Look and Feel များကဲ့သို့ Theme အား ခွဲခြားသတ်မှတ်လာနိုင်မည် ဖြစ်သည်။ )
  1. Properties များတွင် တိုက်ရိုက် Set လုပ်၍ သတ်မှတ်ခြင်း
  2. Inline CSS အား အသုံးပြု၍ သတ်မှတ်ခြင်း
  3. ပြင်ပ CSS File အား အသုံးပြု၍ သတ်မှတ်ခြင်း

နံပါတ်တစ်နည်းသည် Component များတွင် တိုက်ရိုက် setter method များကိုသုံး၍ ၎င်း၏ တန်ဖိုးအား သတ်မှတ်ပေးနိုင်ပါသည်။ သို့ပါသော်လည်း ဤနည်းအား အသုံးပြုပါက Swing တုန်းက ကဲ့သို့ပင် အလွန်ရှည်လျားပြီး ဖတ်ရခက်သည့် Source Code တွေ ထွက်လာပါလိမ့်မည်။

ဒုတိယနည်းသည်လည်း Component များတွင် တိုက်ရိုက် setStyle အား အသုံးပြု၍သော်၎င်း၊ FXML ဖိုင် အတွင်းရှိ style tag တွင် တိုက်ရိုက်ရေးသား၍ သော်၎င်း Style အား သတ်မှတ်နိုင်ပါသေးသည်။ ဤနည်းသည်လည်း Source Code ဒါမှမဟုတ် FXML ဖိုင်တွင် တိုက်ရိုက် ရေးသားနေရ၍၊ နောက်ပိုင်းတွင် ထိမ်းသိမ်းရခက်ခဲသော Code များကို မွေးထုတ်ပေးနေသလိုပင် ဖြစ်၏။

နောက်ဆုံးနည်းသည် ပြင်ပ CSS ဖိုင်အား အသုံးပြု၍ Style အား သတ်မှတ်သောနည်း ဖြစ်ပါသည်။ CSS ဆိုပါက ကျွှန်တော်တို့ HTML အား လေ့လာဘူးသည်နှင့် ရင်းနှီးပြီး ဖြစ်နေပါလိမ့်မည်။ Component ၏ Style အား Component အားတိုက်ရိုက် ရေး၍သော်၎င်း၊ Style Class နှင့် id များအား အသုံးပြု၍သော်၎င်း သတ်မှတ်ရေးသားနိုင်ပါသည်။ တဖန် ပြင်ပတွင် CSS ဖိုင်အား ထားရှိခြင်းကြောင့် အပလီကေးရှင်းတစ်ခုလုံးတွင် အသုံးပြုနိုင်သည့် Common CSS ဖိုင်တစ်ခုအား အသုံးပြု၍လည်း အပလီကေးရှင်းတစ်ခုလုံး၏ Style ညီညာမှု့ကိုလည်း ထိမ်းသိမ်းနိုင်ပါသည်။ အနည်းဆုံးတော့ Source Code ကို တိုက်ရိုက် ပြင်စရာမလိုသောကြောင့်၊ Style ပြင်ပြီးနောက် အထွေအထူး Test လုပ်စရာလိုမည် မဟုတ်။

အတိုဆုံးအကြံပေးရမည်ဆိုလျှင်၊ ခပ်သေးသေး Tutorial Application မျိုးရေးမည်ဆိုပါက နှစ်သက်သလို ရေးသားနိုင်သော်လည်း၊ Enterprise Level Application များအား ရေးသားမည်ဆိုပါက External CSS File အား အသုံးပြု၍ ရေးသားသင့်ပါသည်။

JavaFX ၏ Scene တစ်ခုတွင် External CSS File အား အသုံးပြုနိုင်ရန် ရေးသားပုံ ရေးသားနည်း ၂ မျိုးရှိပါသည်။ ပထမတစ်နည်းမှာ Program ထဲတွင် ရေးသား၍ သတ်မှတ်နည်းဖြစ်ပြီး၊ ဒုတိယနည်းမှာ FXML File အတွင်းတွင် ရေးသားသောနည်းတို့ ဖြစ်ပါသည်။ ဤသို့ ရေးသားရာတွင်လည်း ပထမနည်းအား သိပ်ပြီးအားမပေးချင်ပေ။ အကယ်၍ ပရိုဂရမ်အတွင်း Style Sheet အား Set လုပ်မည်ဆိုပါက FXML အတွင်းတွင် Style နှင့် ပတ်သက်သည်များအား ရေးသားထားတော့မည် မဟုတ်ပေ။ ထို့ကြောင့် SceneBuilder အား  အသုံးပြု၍ Scene အား ရေးသားရာတွင် Style ပြောင်းလည်းမှု့အား တွေ့မြင်နိုင်မည် မဟုတ်။ ထို့ကြောင့် ဤဘလောဂ်တွင် SceneBuilder မှတဆင့် FXML File တွင် တိုက်ရိုက် CSS File အား ရေးသားသည့်နည်းအား မိတ်ဆက်သွားပါမည်။

ကျွှန်တော်တို့ လက်တွေ့ JavaFX Project တစ်ခုအားတည်ဆောက်၍ CSS File အား အသုံးပြုသွား ကြည့်ပါမည်။

  1. Eclipse အားဖွင့်၍ photo-album အမည်နှင့် JavaFX Project တစ်ခုအားတည်ဆောက်ပါမည်။
  2. FXML ဖိုင်အား SceneBuilder ဖြင့်ဖွင့်၍ Edit လုပ်ပါမည်။
  3. Controller Class အား ပြုပြင်ရေးသားပါမည်။
  4. css ဖိုင်အား ပြုပြင်ရေးသားပါမည်။


Creating Java FX Project

ဒီတစ်ခေါက် Java SE 8 နှင့်အတူ JavaFX 8 ကိုလည်း Release လုပ်ခဲ့ပြီး၊ JavaFX သည် JDK ၏ အစိတ်အပိုင်းတစ်ခု ဖြစ်ခဲ့သည်။ ထို့ကြောင့် ဤနမှုနာတွင် Java 8 အား အခြေခံ၍ ရေးသားကြည့်ပါမည်။ အသုံးပြုသော ပတ်ဝင်းကျင်မှာ Eclipse Kepler SR2 နှင့် e(fx)clipse 0.9 Plugin တို့ပဲ ဖြစ်ပါသည်။

File > New > Project Menu ဖြင့် New Project Wizard အား ဖွင့်ပါမည်။ ပြီးလျှင် JavaFX Project အား ရွေး၍ Next အား နှိပ်ပါမည်။


အောက်ပါအတိုင်း  Project name နေရာတွင် photo-album ဟု ဖြည့်၍ Next အား နှိပ်ပါမည်။


ပြီးပါက Application type တွင် Desktop၊ Package Name တွင် com.dtc.album, Language တွင် FXML, Root-Type ကတော့ Default အတိုင်း BorderPane၊ File Name တွင် Album နှင့် Controller နေရာတွင် AlbumController ဟု ဖြည့်ကာ Finish ကို နှိပ်ပါမည်။


အောက်ပါအတိုင်း လိုအပ်သည့် Main Class, AlbumController Class, Album.fxml နှင့် application.css တို့အား အလိုအလျှောက် ရေးသားပြီး ဖြစ်နေပါလိမ့်မည်။


Main.java တွင် အတော်များများသော ကုဒ်များအား အလိုအလျှောက်ရေးသားထားပါသည်။ အထွေအထူး ပြုပြင်စရာမလိုအပ်ပါက ဒီအတိုင်းသုံးနိုင်ပါသည်။ သို့ရာတွင် Scene scene အား new Scene(root, 400, 400) ဟု Scene ၏ Size အား သတ်မှတ်ပေးနေပါသည်။ Scene ၏ size အား FXML တွင် ရေးသားလိုသောကြောင့် ဤနေရာတွင် new Scene(root) ဟု ပြုပြင်လိုက်ပါသည်။

ကျွှန်တော်တို့အနေနှင့် Main Class အား ဒီထက်ပို၍ ပြုပြင်စရာမလိုပဲ၊ FXML, Controller နှင့် CSS တို့ကိုသာ ပြုပြင်ရေးသားသွားရုံသာပင် ဖြစ်၏။


FXML ဖိုင်အား ရေးသားခြင်း

SceneBuilder 2.0 ဖြင့် Album.fxml ဖိုင်အား ဖွင့်၍ အောက်ပါအတိုင်း ပြုပြင် ရေးဆွဲပါမည်။


Component များအား Drug And Drop လုပ်၍ နေရာချရုံသာ ဖြစ်၏။ အသေးစိတ်မှာမူ CSS ဖြင့် Style အား သတ်မှတ်ပါမည်။ ရရှိလာသော Code များမှာ အောက်ပါအတိုင်း ဖြစ်ပါသည်။
<?xml version="1.0" encoding="UTF-8"?>

<?import java.lang.*?>
<?import javafx.geometry.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>

<BorderPane xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.dtc.album.AlbumController">
<top><HBox alignment="CENTER" BorderPane.alignment="CENTER">
<children><Label text="My Photos">
<styleClass>
<String fx:value="label" />
<String fx:value="my_label" />
</styleClass></Label>
</children>
<padding>
<Insets bottom="10.0" top="10.0" />
</padding></HBox>
</top>
<bottom><HBox alignment="CENTER" spacing="10.0">
<children><Button fx:id="" mnemonicParsing="false" onAction="#previous" text="&lt; Previous">
<styleClass>
<String fx:value="button" />
<String fx:value="my_button" />
</styleClass></Button><Button fx:id="" mnemonicParsing="false" onAction="#next" text="Next &gt;">
<styleClass>
<String fx:value="button" />
<String fx:value="my_button" />
</styleClass></Button>
</children>
<padding>
<Insets bottom="10.0" top="10.0" />
</padding></HBox>
</bottom>
<center><StackPane id="" fx:id="container" prefHeight="420.0" prefWidth="580.0">
<BorderPane.margin>
<Insets left="20.0" right="20.0" />
</BorderPane.margin></StackPane>
</center></BorderPane>


Controller Class အား ရေးသားခြင်း

AlbumController.java အား အောက်ပါအတိုင်း ရေးသားပါမည်။
package com.dtc.album;

import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.util.Arrays;
import java.util.ListIterator;
import java.util.ResourceBundle;
import java.util.stream.Collectors;

import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.StackPane;
import javafx.stage.DirectoryChooser;

public class AlbumController implements Initializable {

 @FXML
 private StackPane container;

 private Path photoDirectory;
 private ListIterator<ImageView> photos;

 @Override
 public void initialize(URL location, ResourceBundle resources) {
  try {
   this.loadPhoto();
  } catch (Exception e) {
   this.handleError();
  }
 }

 public void next() {
  try {
   if (this.photos.hasNext()) {
     this.setImageView(this.photos.next());
   } else {
    this.showMessage("There is no images to next");
    this.photos.previous();
   }
  } catch (Exception e) {
   this.handleError();
  }
 }

 public void previous() {
  try {
   if (this.photos.hasPrevious()) {
     this.setImageView(this.photos.previous());
   } else {
    this.showMessage("There is no images to next");
    this.photos.next();
   }
  } catch (Exception e) {
   this.handleError();
  }
 }

 private void loadPhoto() throws IOException {

  // configuration file
  Path conf = Paths.get(getClass().getSimpleName() + ".txt");
  if (Files.exists(conf, LinkOption.NOFOLLOW_LINKS)) {
   Files.readAllLines(conf).forEach(
     (x) -> this.photoDirectory = Paths.get(x));
  }
  // no configuration file (first time only)
  else {
   DirectoryChooser fc = new DirectoryChooser();
   fc.setInitialDirectory(new File(System.getProperty("user.home")
     + "\\Pictures"));
   fc.setTitle("Photos Directory");
   File f = fc.showDialog(null);

   if (null != f) {
    this.photoDirectory = f.toPath();
    Files.write(conf,
      Arrays.asList(this.photoDirectory.toString()),
      StandardOpenOption.CREATE_NEW);
   }
  }

  if (null != this.photoDirectory
    && Files.isDirectory(photoDirectory, LinkOption.NOFOLLOW_LINKS)) {

   // load photos as imageview
   this.photos = Files
     .list(photoDirectory)
     .filter(x -> x.toString().toLowerCase().endsWith("jpg")
       || x.toString().toLowerCase().endsWith("png")
       || x.toString().toLowerCase().endsWith("gif"))
     .map(AlbumController.this::getImageView)
     .collect(Collectors.toList()).listIterator();

   // set first image
   if (this.photos.hasNext()) {
    this.setImageView(this.photos.next());
   }
  } else {
   this.showMessage("There is no photos to show. Please restart again.");
   this.showCloseBtn();
  }
 }

 private ImageView getImageView(Path path) {
  try {
   Image image = new Image(Files.newInputStream(path,
     StandardOpenOption.READ));
   ImageView view = new ImageView(image);
   view.setPreserveRatio(true);
   view.setFitWidth(this.container.getPrefWidth());
   view.setFitHeight(this.container.getPrefHeight());

   return view;
  } catch (Exception e) {
   this.handleError();
  }
  return null;
 }

 private void handleError() {
  this.container.getChildren().clear();
  this.showMessage("There is an error. Please restart again.");
  this.showCloseBtn();
 }

 private void setImageView(ImageView view) throws IOException {
  this.container.getChildren().clear();
  this.container.getChildren().add(view);
 }

 private void showMessage(String message) {
  Label l = new Label(message);
  l.getStyleClass().add("message");
  this.container.getChildren().add(l);
 }

 private void showCloseBtn() {
  Button btn = new Button("OK");
  btn.getStyleClass().add("ok_btn");
  btn.setOnAction(e -> System.exit(0));
  this.container.getChildren().add(btn);
 }

}
AlbumController Class အဓိက အလုပ်လုပ်သော Class ဖြစ်ပါသည်။ ဤအခန်းတွင် CSS အကြောင်းကို ဖော်ပြလိုသောကြောင့် Private Method များကိုတော့ အသေးစိတ် ရှင်းပြတော့မည် မဟုတ်ပါ။

AlbumController တွင် အဓိကပါဝင်သည်မှာ Member အနေနှင့် ဓာတ်ပုံတွေကို ဖော်ပြမည့် StackPane Object တစ်ခု၊ Photo များကို သိမ်းပေးထားနိုင်မည့် ListIterator Object တစ်ခုနှင့် Photo တွေကိုထားထားသည့် Directory အားကိုယ်စားပြုသည့် Path Object တစ်ခုတို့ ဖြစ်ကြပါသည်။ Public Method အနေနှင့် Application စစခြင်းလုပ်မည့် initialize, next နှင့် previous တို့သာရှိကြပါသည်။

initialize method အတွင်းတွင် loadPhoto private method အား ခေါ်ယူနေပြီး၊ ၎င်းအတွင်းတွင် configuration file အား ရှာဖတ်၍ ၎င်းအတွင်းမှ path name ဖြင့် Member ဖြစ်သည့် Path Object တွင် အစားထိုးပါသည်။ Configuration File မတွေ့ရှိခဲ့ပါက DirectoryChooser ဖြင့် အသုံးပြုမည့် Directory အား ရွေးချယ်ခိုင်းကာ ၎င်းအား Path Object တွင် အစားထိုး၍ အဆိုပါ Path အား Configuration File တွင် သိမ်းဆည်းစေပါသည်။

ထို့နောက် Path Object အတွင်းမှ Photo File များအား ImageView အဖြစ်ပြောင်းကာ ListIterator တွင် သိမ်းဆည်းထားစေပါသည်။ ဤနေရာတွင် Java 8 ၏ Stream API အား အသုံးပြုထားသောကြောင့် တစ်ကြောင်းတည်းနှင့် ရေးသားနိုင်ပါသည်။

next method သည် Next Button အား နှိပ်သည့်အခါတွင် အလုပ်လုပ်မည့် Method ဖြစ်ပြီး၊ ListIterator အတွင်း နောက်တစ်ပုံရှိပါက နောက်တစ်ပုံအား StackPane တွင် ဖော်ပြပေးပါမည်။ မရှိပါက Message အား StackPane တွင် ဖော်ပြပါလိမ့်မည်။

previous method သည် Previous Button အား နှိပ်သည့်အခါတွင် အလုပ်လုပ်မည့် Method ဖြစ်ပြီး၊ ListIterator အတွင်း အရှေ့ပုံရှိပါက အရှေ့ပုံအား ဖော်ပြသွားမည် ဖြစ်ပါသည်။ မရှိပါက Message အား StackPane တွင် ဖော်ပြပါလိမ့်မည်။


CSS ဖိုင်ဖြင့် နေရာပြောင်းရမည့်နေရာများ



Application စစခြင်း၊ Directory Chooser Dialog အား Cancel လုပ်လိုက်သည့်အခါတွင် ထွက်ပေါ်လာမည့် Window ဖြစ်ပါသည်။ StackPane တွင် Label တစ်ခုနှင့် Button တစ်ခုအား ဒီအတိုင်း ဖြည့်စွက်ထားသောကြောင့် ဖြစ်သည်။ ၎င်းတို့အား CSS အား အသုံးပြု၍ နေရာပြောင်းပါမည်။

တဖန် ခေါင်းစဉ် စာလုံးသည်လည်း Default အတိုင်း ဖြစ်နေပါသေးသည်။ ထို့အပြင် Previous နှင့် Next Button တို့သည်လည်း မညီမညာဖြစ်နေသည်။ ၎င်းတို့အားလည်း CSS ကို အသုံးပြု၍ ပြုပြင်ရပါမည်။

အောက်ပါအတိုင်း application.css အား ပြုပြင်၍ ရေးသားကြည့်ပါမည်။
.my_button {
 -fx-pref-width: 120px;
}

.my_label {
 -fx-font-family: Century;
 -fx-font-size: 26px;
 -fx-effect: dropshadow(one-pass-box , black , 8 , 0.0 , 4 , 0) 
}

.message {
 -fx-translate-y: 100px;
 -fx-font-family: Century;
 -fx-font-size: 18px;
 -fx-text-fill: red;
}

.ok_btn {
 -fx-translate-y: 140px;
 -fx-pref-width: 120px;
}
ထို့နောက် Application အား စမ်းကြည့်သောအခါ အောက်ပါအတိုင်း ရရှိပါသည်။


Directory အား မှန်ကန်စွာရွေးပေးပြီးသောအခါ အောက်ပါအတိုင်းတွေ့ရပါသည်။


ဒီတစ်ခေါက် Blog ဖြင့် CSS အား အသုံးပြုပါက အလွယ်တကူ Style, Effect နှင့် Layout တို့အား ပြုပြင်နိုင်ကြောင်း နမှုနာအနေနှင့် ဖော်ပြခဲ့ပါသည်။ နောက်အကြိမ်များနှင့် Java FX ၏ Style များအကြောင်းကို ဖော်ပြသွားပါဦးမည်။

လေးစားစွာဖြင့်။
မင်းလွင်

2 comments:

  1. May you continue with this section!
    I favourate this section too much cause
    i want to learn javaFX
    May you explain how to connect with database section?

    ReplyDelete
  2. Thank you for reading my blog. I will write database connection with javafx in both of jdbc and jpa ways in near future.

    ReplyDelete