Java在服务端领域能够独占鳌头,然而,在客户端领域一直不温不火。Java的客户端技术,从AWT,SWING,SWT,一直到JAVAFX,即使每一代都有非常大的改善,但仍改变不了它尴尬的地位。依我看来,使用JAVA来开发桌面应用,大概只有我有我们这批javaer了

不管怎样,javafx的设计理念还是非常优秀的。它借鉴了html开发的特点,将代码,界面,样式三者分离。使用java代码来控制逻辑,使用xml来设计界面,使用css来控制样式。

本文将使用一个简单的例子,来演示javafx的开发案例。

开发工具


e(fx)eclipse 是一个带有javafx开发插件的eclipse版本,它自带css,xml代码自动补全。


JavaFX Scene Builder 2.0是一个javafx界面开发的辅助工具,其实就是支持你使用拖曳的方式来设计界面。对于调整界面位置非常有用。


界面样式逻辑三权分立


有了efxeclipse,我们就可以来开发我们的javafx项目啦。


登录界面控件非常少,可以拿来作为入门例子。


在javafx里,Stage表示一个有用户界面的应用程度窗口,在Stage下面,则是由Scene来表示界面容器。Scene包含所有界面组件,例如各种ui控件和ui子容器。


类似于Flex开发,我们的javafx界面设计是写在fxml文件里。如下:


<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.geometry.*?>
<?import javafx.scene.paint.*?>
<?import javafx.scene.text.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.chart.*?>
<?import javafx.scene.image.*?>
<?import java.lang.*?>
<?import javafx.scene.layout.*?>

<AnchorPane maxHeight="-Infinity" maxWidth="-Infinity"
	minHeight="-Infinity" minWidth="-Infinity" prefHeight="330.0"
	prefWidth="430.0" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1"
	fx:controller="com.kingston.ui.controller.LoginViewController"
	stylesheets="@../css/login.css">
	<children>
		<ImageView fitHeight="330.0" fitWidth="430.0" layoutX="91.0"
			layoutY="90.0" pickOnBounds="true" preserveRatio="true"
			AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0"
			AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0">
			<image>
				<Image url="@../img/login.png" />
			</image>
		</ImageView>
		<Button fx:id="login" layoutX="135.0" layoutY="288.0"
			mnemonicParsing="false" onMouseClicked="#login" onMouseEntered="#login_en"
			onMouseExited="#login_ex" prefHeight="32.0" prefWidth="194.0" text="%login.safeLogin"
			textFill="WHITE">
			<font>
				<Font size="18.0" />
			</font>
		</Button>
		<ImageView fitHeight="80.0" fitWidth="80.0" layoutX="38.0"
			layoutY="195.0" pickOnBounds="true" preserveRatio="true">
			<image>
				<Image url="@../img/head.jpg" />
			</image>
		</ImageView>
		<TextField fx:id="userId" layoutX="135.0" layoutY="195.0"
			prefHeight="25.0" prefWidth="194.0" promptText="%login.account" text="1000" />
		<PasswordField fx:id="password" layoutX="135.0" layoutY="226.0"
			prefHeight="25.0" prefWidth="194.0" promptText="%login.password" />
		<CheckBox fx:id="rememberPsw" layoutX="135.0" layoutY="258.0"
			mnemonicParsing="false" text="%login.rememberPsw" />
		<CheckBox fx:id="autoLogin" layoutX="260.0" layoutY="258.0"
			mnemonicParsing="false" text="%login.autoLogin" />
		<Label />
		<Label />
		<ImageView fx:id="closeBtn" fitHeight="30.0" fitWidth="30.0"
			layoutX="399.0" onMouseClicked="#close" onMouseEntered="#closeEntered"
			onMouseExited="#closeExited" pickOnBounds="true" preserveRatio="true"
			AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0">
			<image>
				<Image url="@../img/close.png" />
			</image>
			<rotationAxis>
				<Point3D />
			</rotationAxis>
		</ImageView>
		<ImageView fx:id="minBtn" fitHeight="30.0" fitWidth="30.0"
			layoutX="346.0" onMouseClicked="#min" onMouseEntered="#minEntered"
			onMouseExited="#minExited" pickOnBounds="true" preserveRatio="true"
			AnchorPane.rightAnchor="30.0" AnchorPane.topAnchor="0.0">
			<image>
				<Image url="@../img/min.png" />
			</image>
		</ImageView>
		<ProgressBar layoutX="132.0" layoutY="289.0" prefHeight="32.0" fx:id="loginProgress"
			prefWidth="200.0" visible="false" />
		<Pane fx:id="errorPane" layoutY="36.0" prefHeight="307.0"
			prefWidth="430.0" style="-fx-background-color: #9AD3EE;" visible="false"
			AnchorPane.topAnchor="30.0">
			<children>
				<Label fx:id="errorTips" layoutX="118.0" layoutY="52.0" prefHeight="153.0"
					prefWidth="194.0" wrapText="true">
					<font>
						<Font size="27.0" />
					</font>
				</Label>
				<Button layoutX="118.0" layoutY="223.0" mnemonicParsing="false"
					onMouseClicked="#backToLogin" prefHeight="32.0" prefWidth="194.0"
					style="-fx-background-color: #09A3DC;" text="%login.backToLogin" textFill="WHITE">
					<font>
						<Font size="18.0" />
					</font>
				</Button>
			</children>
		</Pane>
		<Hyperlink layoutX="339.0" layoutY="195.0" text="%login.registerAccount"
			onMouseClicked="#gotoRegister">
			<font>
				<Font size="14.0" />
			</font>
		</Hyperlink>
		<Hyperlink layoutX="339.0" layoutY="226.0" text="%login.findPsw">
			<font>
				<Font size="14.0" />
			</font>
		</Hyperlink>
	</children>

</AnchorPane>


public class LoginViewController implements ControlledStage, Initializable {

	@FXML
	private Button login;
	@FXML
	private TextField userId;
	@FXML
	private PasswordField password;
	@FXML
	private CheckBox rememberPsw;
	@FXML
	private CheckBox autoLogin;
	@FXML
	private ImageView closeBtn;

	@FXML
	private void login() throws IOException {
		final long useId = Long.parseLong(userId.getText());
		final String psw = password.getText();

		if (!IoBaseService.INSTANCE.isConnectedSever()) {
			errorPane.setVisible(true);
			errorTips.setText(I18n.get("login.failToConnect"));
			return;
		}

		loginProgress.setVisible(true);
		login.setVisible(false);

		LoginManager.getInstance().beginToLogin(useId, psw);
	}
}


类似于html的css文件,我们的ui控件样式最好也是写在独立的css文件,然后通过fxml进行指向,例如: stylesheets="@../css/login.css"。


在这里有个需要特别注意的是,如果代码需要更新ui显示,我们必须要在javafx的ui线程上进行


/**
	 * 将任务转移给fxapplication线程延迟执行
	 * @param task
	 */
	public void runTaskInFxThread(Runnable task){
		Platform.runLater(task);
	}


文字资源的国际化支持


客户端开发程序,我们就不可避免地要进行国际化支持,对不同的国际地区显示不同的文字。javafx在这方面也做得不错。


在加载fxml的时候,我们可以指定国际化资源文件,如下 


URL url = Thread.currentThread().getContextClassLoader().getResource(resource);
			FXMLLoader loader = new FXMLLoader(url);
			loader.setResources(ResourceBundle.getBundle("i18n/message"));


在非fxml文件里使用国际化文字的方式。定义一个加载使用国际化文字的工具类(I18n.java)


/**
 * 国际化资源池
 * @author kingston
 */
public class I18n {

    private static ResourceBundle resourcePool;

    static {
        try {
            resourcePool = ResourceBundle.getBundle("i18n/message");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static String get(String key, Object... args) {
        if (!resourcePool.containsKey(key)) {
           return "国际化资源不存在";
        }
        String message = resourcePool.getString(key);
        if (args != null) {
            return MessageFormat.format(message, args);
        } else {
            return message;
        }
    }
}

在代码里,使用 I18n.get("register.operateSucc") 即可。


 


统一管理Stage的加载与切换


在JAVAFX里,经常需要在不同的stage进行切换。为了能高效统一管理stage,我们可以将全部stage缓存起来,根据需要动态显示隐藏窗口。

public class StageController {

	private Map<String, Stage> stages = new HashMap<>();

	private Map<String, ControlledStage> controllers = new HashMap<>();

	public void addStage(String name, Stage stage) {
		this.stages.put(name, stage);
	}

	public Stage getStageBy(String name) {
		return this.stages.get(name);
	}

	public void setPrimaryStage(String name, Stage stage) {
		this.addStage(name, stage);
	}

	public Stage loadStage(String name, String resource, StageStyle... styles) {
		Stage result = null;
		try{
			URL url = Thread.currentThread().getContextClassLoader().getResource(resource);
			FXMLLoader loader = new FXMLLoader(url);
			loader.setResources(ResourceBundle.getBundle("i18n/message"));
			Pane tmpPane = (Pane)loader.load();
			ControlledStage controlledStage = (ControlledStage)loader.getController();
			this.controllers.put(name, controlledStage);
			Scene tmpScene = new Scene(tmpPane);
			result = new Stage();
			result.setScene(tmpScene);

			for (StageStyle style:styles) {
				result.initStyle(style);
			}
			this.addStage(name, result);
		}catch(Exception e) {
			e.printStackTrace();
		}
		return result;
	}

	@SuppressWarnings("unchecked")
	public <T> T load(String resource, Class<T> clazz) {
		try{
			URL url = Thread.currentThread().getContextClassLoader().getResource(resource);
			FXMLLoader loader = new FXMLLoader(url);
			return (T)loader.load();
		}catch(Exception e){
			e.printStackTrace();
		}
		return null;
	}

	@SuppressWarnings("unchecked")
	public <T> T load(String resource, Class<T> clazz, ResourceBundle resources) {
		try{
			URL url = Thread.currentThread().getContextClassLoader().getResource(resource);
			return (T)FXMLLoader.load(url, resources);
		}catch(Exception e){
			e.printStackTrace();
		}
		return null;
	}

	public Stage setStage(String name) {
		Stage stage = this.getStageBy(name);
		if (stage == null) {
			return null;
		}
		stage.show();
		return stage;
	}

	public boolean switchStage(String toShow, String toClose) {
		getStageBy(toClose).close();
		setStage(toShow);

		return true;
	}

	public void closeStge(String name) {
		Stage target = getStageBy(name);
		target.close();
	}

	public boolean unloadStage(String name) {
		return this.stages.remove(name) != null;
	}

	public ControlledStage getController(String name) {
		return this.controllers.get(name);
	}

}

 

全部代码已在github上托管

服务端代码请移步 --> netty聊天室服务器

客户端代码请移步 --> netty聊天室客户端