开局一张图
实现的功能有
- 拖动查看
- 旋转
- 点击缩放及鼠标滚轮/触控板缩放
- 选择多图、文件夹时,上/下页切换
0、代码结构
├── java
│ └── com
│ └── wnewey
│ ├── ImgViewer.java # 入口类
│ ├── commponet
│ │ └── draglistener # 拖动相关类
│ │ ├── DragListener.java
│ │ ├── DragListenerCall.java
│ │ └── caller
│ │ └── ImgViewerImgViewPaneDragListenerCall.java
│ ├── controller # 展示及控制相关类
│ │ └── imgviewer
│ │ ├── ImgViewerController.java
│ │ ├── ImgViewerView.java
│ │ └── ImgViewerWindow.java
│ ├── kit # 工具类
│ │ ├── PathKit.java
│ │ └── StrKit.java
│ ├── util # 工具类
│ │ ├── FileChooserUtil.java
│ │ └── ImageUtil.java
│ └── vo # VO对象
│ └── ImageSize.java
└── resources
└── view # FXML布局文件
└── ImgViewerView.fxml
1、界面设计
1.1 使用FXML完成大致布局
<!-- imgviewer.fxml -->
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.*?>
<?import javafx.scene.image.ImageView?>
<?import javafx.scene.layout.*?>
<AnchorPane xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1"
fx:id="panel" fx:controller="com.wnewey.controller.imgviewer.ImgViewerController">
<!-- 菜单 -->
<MenuBar fx:id="menuBar" useSystemMenuBar="true">
<Menu text="文件">
<MenuItem text="打开文件" onAction="#openImgFile"/>
<MenuItem text="打开文件夹" onAction="#openImgFold"/>
</Menu>
</MenuBar>
<!-- 图片显示部分 -->
<GridPane alignment="CENTER">
<AnchorPane.topAnchor>0</AnchorPane.topAnchor>
<AnchorPane.leftAnchor>0</AnchorPane.leftAnchor>
<AnchorPane.rightAnchor>0</AnchorPane.rightAnchor>
<AnchorPane.bottomAnchor>0</AnchorPane.bottomAnchor>
<HBox alignment="CENTER" fx:id="imgViewPane" visible="false">
<ImageView fx:id="imageView"/>
<Label fx:id="tipsLabel" visible="false"/>
</HBox>
</GridPane>
<!-- 初始打开时显示打开文件按钮 -->
<GridPane fx:id="fileBtns" alignment="CENTER">
<AnchorPane.topAnchor>0</AnchorPane.topAnchor>
<AnchorPane.leftAnchor>0</AnchorPane.leftAnchor>
<AnchorPane.rightAnchor>0</AnchorPane.rightAnchor>
<AnchorPane.bottomAnchor>0</AnchorPane.bottomAnchor>
<HBox alignment="CENTER" spacing="100">
<Button text="打开文件" prefHeight="50" prefWidth="150" onAction="#openImgFile"/>
<Button text="打开文件夹" prefHeight="50" prefWidth="150" onAction="#openImgFold"/>
</HBox>
</GridPane>
<!-- 操作按钮 -->
<HBox alignment="CENTER" spacing="20" fx:id="optBtns" visible="false">
<AnchorPane.leftAnchor>0</AnchorPane.leftAnchor>
<AnchorPane.rightAnchor>0</AnchorPane.rightAnchor>
<AnchorPane.bottomAnchor>20</AnchorPane.bottomAnchor>
<Button text="上一张" fx:id="prevBtn" onAction="#goPrev"/>
<Button text="放大" onAction="#zoomIn"/>
<Button text="还原" onAction="#rest"/>
<Button text="缩小" onAction="#zoomOut"/>
<Button text="旋转" onAction="#rotate"/>
<Button text="下一张" fx:id="nextBtn" onAction="#goNext"/>
</HBox>
</AnchorPane>
-
外层使用AnchorPane,主要是便于内部节点随窗口变化时能自适应位置。
-
操作按钮固定在距离底部20像素位置。
-
初始打开时,由于没有图片要展示,所以添加打开文件的按钮占位。点击后可打开图片进行展示,同时隐藏该按钮。后续可通过【打开文件】菜单来重新选择图片。
-
打开文件 时可选择多个文件,打开文件夹 会加载文件夹下所有的图片,通过 上/下一页 按钮进行切换。
-
ImageView 外新增一层节点,一是便于 Label 的居中展示,二是后续的图片拖动主要通过控制这个节点来完成。
1.2 显示主窗口
// ImgViewer.java
public class ImgViewer extends Application {
public static void main(String[] args) {
launch(args);
}
@Override
public void start(Stage primaryStage) throws Exception {
initWindow();
}
private static void initWindow() {
FXMLLoader loader = new FXMLLoader();
// 加载布局文件
URL url = Object.class.getResource("/view/ImgViewerView.fxml");
loader.setLocation(url);
try {
Parent layout = loader.load();
Scene scene = new Scene(layout);
// 加载样式表
scene.getStylesheets().add(Objects.requireNonNull(ImgViewer.class.getResource("/com/only/common/css/Only.css")).toString());
Stage popupStage = new Stage();
// 设置窗体信息
popupStage.initModality(Modality.WINDOW_MODAL);
popupStage.setTitle("图片浏览器");
popupStage.setScene(scene);
popupStage.setMinWidth(500);
popupStage.setMinHeight(400);
popupStage.setWidth(1000);
popupStage.setHeight(800);
ImgViewerController controller = loader.getController();
// 显示窗体
popupStage.show();
// 显示图片组件
controller.showViewer(null, 0, popupStage);
popupStage.setOnCloseRequest(event -> System.exit(0));
} catch (IOException e) {
e.printStackTrace();
}
}
}
主入口代码不需要过多介绍,主要是初始化窗体信息。
需要注意的是 showViewer 必须在 popupStage.show() 之后,否则窗体未显示出来,后面的图片初始大小没有进行设置。
2、打开文件/文件夹
2.1 打开文件
// FileChooserUtil.java
public static List<File> getMultiImgFile(String initPath, Stage stage) {
Scene scene = stage.getScene();
Window w = (null == scene) ? null : scene.getWindow();
return getImgFileChooser(initPath).showOpenMultipleDialog(w);
}
private static FileChooser getImgFileChooser(String initPath) {
FileChooser fileChooser = new FileChooser();
if (StrKit.notBlank(initPath)) {
File file = new File(initPath);
if (file.exists() && !file.isDirectory()) {
fileChooser.setInitialDirectory(file);
}
}
fileChooser.setTitle("请选择文件");
fileChooser.getExtensionFilters().addAll(
new FileChooser.ExtensionFilter("图片文件", "*.png", "*.jpg", "*.jpeg", "*.gif"),
new FileChooser.ExtensionFilter("PNG", "*.png"),
new FileChooser.ExtensionFilter("JPG", "*.jpg", "*.jpeg"),
new FileChooser.ExtensionFilter("GIF", "*.gif")
);
return fileChooser;
}
通过 JavaFx 的 FileChooser.showOpenMultipleDialog 方法打开文件选择器,并可进行文件多选操作。同时,通过增加 ExtensionFilter 来进行文件类型过滤。
2.2 打开文件夹
// FileChooserUtil.java
public static List<String> getImgListFromFolder(String initPath, Stage stage) {
DirectoryChooser directoryChooser = new DirectoryChooser();
if (StrKit.notBlank(initPath)) {
File file = new File(initPath);
if (file.exists() && !file.isDirectory()) {
directoryChooser.setInitialDirectory(file);
}
}
Scene scene = stage.getScene();
Window w = (null == scene) ? null : scene.getWindow();
File file = directoryChooser.showDialog(w);
List<String> list = null;
if (file != null) {
list = Arrays.asList(Objects.requireNonNull(file.list((dir, name) -> {
String affix = name.substring(name.lastIndexOf(".") + 1);
return "'png','jpg','jpeg','gif'".contains("'" + affix + "'");
})));
list.sort(String::compareTo);
for (int i = 0; i < list.size(); i++) {
list.set(i, file.getPath() + File.separator + list.get(i));
}
}
return list;
}
通过 JavaFx 的 DirectoryChooser.showDialog 方法打开文件夹选择器,并将得到的文件列表进行过滤,只保留图片文件。
3、图片展示
// ImgViewerController.java
private void showImg() {
try {
if (fileList == null || fileList.isEmpty()) {
return;
}
if (currentIndex < 0) {
currentIndex = 0;
return;
}
if (currentIndex >= fileList.size()) {
currentIndex = fileList.size() - 1;
return;
}
prevBtn.setDisable(false);
nextBtn.setDisable(false);
if (currentIndex == 0) {
prevBtn.setDisable(true);
}
if (currentIndex == fileList.size() - 1) {
nextBtn.setDisable(true);
}
imageView.setRotate(0);
imgViewPane.setTranslateX(0);
imgViewPane.setTranslateY(0);
this.popupStage.setTitle(PathKit.getFileName(fileList.get(currentIndex)));
File imgFile = new File(fileList.get(currentIndex));
if (!imgFile.exists()) {
imageView.setVisible(false);
tipsLabel.setText("文件不存在或已损坏");
tipsLabel.setVisible(true);
return;
} else {
tipsLabel.setVisible(false);
imageView.setVisible(true);
}
Image image = new Image(new FileInputStream(imgFile));
double imgWidth = image.getWidth();
double imgHeight = image.getHeight();
double windWidth = popupStage.getScene().getWidth();
double windHeight = popupStage.getScene().getHeight();
double finalWidth = Math.min(imgWidth, windWidth);
double finalHeight = Math.min(imgHeight, windHeight);
ImageSize size = ImageUtil.calFinalImgSize(imgWidth, imgHeight, finalWidth, finalHeight);
imageView.setFitHeight(size.getHeight());
imageView.setFitWidth(size.getWidth());
imgViewPane.setPrefWidth(size.getWidth());
imgViewPane.setPrefHeight(size.getHeight());
imageView.setImage(image);
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
// ImageUtil.java
public static ImageSize calFinalImgSize(double imgWidth, double imgHeight, double containerWidth, double containerHeight) {
double finalWidth = containerWidth;
double finalHeight = containerHeight;
// 宽度过大
if (imgWidth / imgHeight > containerWidth / containerHeight) {
finalHeight = containerWidth / imgWidth * imgHeight;
} else {
// 高度过大
finalWidth = containerHeight / imgHeight * imgWidth;
}
return new ImageSize(finalWidth, finalHeight);
}
-
一些控制
- 图片列表为空时不展示
- 图片文件损毁时提示
- 上/下一页的按钮展示控制
-
根据窗口和图片的尺寸大小计算得到最终的图片展示尺寸。
- 图片高度过长,则最大高度为视窗高度,图片宽度按照高度的缩放比进行缩放。
- 图片宽度过长,则最大宽度为视窗宽度,图片高度按照宽度的缩放比进行缩放。
-
根据最后得到的最终尺寸设置图片和图片外层的节点高度。
-
向 ImageView 中设置图片。
至此,图片展示就完成了。
4、操作响应
4.1 拖动
// ImgViewerController.java
new DragListener(imgViewPane, new ImgViewerImgViewPaneDragListenerCall());
// DragListener.java
public class DragListener implements EventHandler<MouseEvent> {
private Node container = null;
private double xOffset = 0;
private double yOffset = 0;
private Node node;
private DragListenerCall call;
public DragListener(Node node, Node container, DragListenerCall call) {
this.node = node;
this.node.setOnMousePressed(this);
this.node.setOnMouseDragged(this);
this.node.setOnMouseReleased(this);
this.call = call;
this.container = container;
}
public DragListener(Node node, DragListenerCall call) {
this.node = node;
this.node.setOnMousePressed(this);
this.node.setOnMouseDragged(this);
this.node.setOnMouseReleased(this);
this.call = call;
}
@Override
public void handle(MouseEvent event) {
if (event.getEventType() == MouseEvent.MOUSE_PRESSED) {
// 记录鼠标点击的初始位置
xOffset = event.getX();
yOffset = event.getY();
if (this.call != null) {
this.call.onMousePressed(this.node, event);
}
} else if (event.getEventType() == MouseEvent.MOUSE_DRAGGED) {
// 最终偏移量=当前偏移量+鼠标当前位置-鼠标移动开始的位置
double transX = node.getTranslateX() + event.getX() - xOffset;
double transY = node.getTranslateY() + event.getY() - yOffset;
if (this.container != null) {
Bounds bounds = node.getLayoutBounds();
Bounds cBonuds = container.getLayoutBounds();
// 有外部容器,则要保证不能移到外部容器的外面
if (bounds.getMinX() + transX < cBonuds.getMinX() || bounds.getMaxX() + transX > cBonuds.getMaxX()
|| bounds.getMaxY() + transY > cBonuds.getMaxY() || bounds.getMinY() + transY < cBonuds.getMinY()) {
return;
}
}
node.setTranslateX(transX);
node.setTranslateY(transY);
if (this.call != null) {
this.call.onMouseDragged(this.node, event);
}
} else if (event.getEventType() == MouseEvent.MOUSE_RELEASED) {
if (this.call != null) {
this.call.onMouseReleased(this.node, event);
}
}
}
}
一个完整的拖动动作主要包含
- 点击事件(MouseEvent.MOUSE_PRESSED)
- 拖动事件(MouseEvent.MOUSE_DRAGGED)
- 释放事件(MouseEvent.MOUSE_RELEASED)
点击时,记录当前的点击位置,拖动完,设置控件的偏移位置。
4.2 缩放
public void zoom(double factor) {
imageView.setFitWidth(imageView.getFitWidth() * factor);
imageView.setFitHeight(imageView.getFitHeight() * factor);
imgViewPane.setPrefWidth(imgViewPane.getPrefWidth() * factor);
imgViewPane.setPrefHeight(imgViewPane.getPrefHeight() * factor);
}
// 放大
public void zoomIn() {
zoom(1.2);
}
// 缩小
public void zoomOut() {
zoom(1 / 1.2);
}
// 鼠标滚轮缩放
imgViewPane.setOnScroll(event -> {
double deltaY = event.getDeltaY();
if (deltaY < 0) {
zoomOut();
} else {
zoomIn();
}
event.consume();
});
// 触摸板缩放
imgViewPane.setOnZoom(event -> {
zoom(event.getZoomFactor());
event.consume();
});
设置宽高按比例缩放即可。同时给按钮、鼠标滚轮、触控板绑定相关事件。
4.3 旋转
public void rotate() {
rotate += 90;
imageView.setRotate(rotate % 360);
}
使用控件的setRotate方法即可。360为一圈。
4.4 还原
public void rest() {
// 还原为和窗口一样
showImg();
rotate = 0;
imageView.setRotate(rotate);
imgViewPane.setTranslateX(0);
imgViewPane.setTranslateY(0);
}
重新展示图片(重新计算宽高,缩放还原),旋转角度设置为0,拖动还原。
4.5 上/下一页
@FXML
public void goPrev() {
--currentIndex;
showImg();
}
/**
* 下一张
*/
@FXML
public void goNext() {
++currentIndex;
showImg();
}
只需设置当前文件的索引即可。
5、总结
这是个练手项目,理清思路后,整体功能比较实现比较简单。可以通过此项目了解javafx的布局和基础控件的使用,以及事件交互。