软件课设:OCR文字标注

注:项目为学生小组完成,如有引用请注明原文链接 https://www.cnblogs.com/Architect15806/p/16108240.html
源码请访问 https://github.com/Architect15806/OCR-Marker

一、设计任务

(一)应用场景:

图片文字检测识别项目中,需要将识别结果(包括检测框出的文字结果以及识别的文字内容)可视化,并且对结果进行修正。
现要求设计一个如下功能的软件:

(二)基础要求:

  1. 读取选定文件夹下的所有jpg,png图片,具有可以显示文件列表的区域,可以通过点击相应文件名打开相应图片,具有显示图片的区域;
  2. 读取选定文件夹下的所有xml文件(xml文件名前缀与图片文件名前缀相同),xml文件中存储有标注框相对于图片的坐标,以及框内文本的识别内容;
  3. 将xml中的识别内容解析出来并显示在列表区域中,根据标注框的坐标将标注框绘制在图片上;
  4. 将标注框绘制在图片上,要求可以选中标注框,对标注框的位置、大小进行调整,可以对框内识别内容进行修改;
  5. 修改后可以保存当前修改,重写xml文件。

(三)扩展要求:

  1. 图片可以在视图区域中使用鼠标滚轮进行中心缩放、拖动平移;
  2. 设计一个固定位置的工作区,用于放大显示当前选中的标注框以及标注框中的文字内容;
  3. 注意选中标注框时,内容列表焦点也跳到相应部位,工作区内容也更改为相应框;
  4. 创新更多人性化设计。

二、基本思路

(一)基本框架确定

  1. 编程语言选择

    考虑到小组成员的知识基础,Java和Python语言为待定选择。综合评价这两种语言可以使用的编程工具:Java中可以使用Java AWT包或Java FX包编写界面程序,调节和美化容易,实现后台运算逻辑也较为方便;Python相比之下,界面编写不容易实现高自由度的自定义,此外在此体量的工程中容易造成代码管理混乱。

    故综合要素,选择Java为编程语言。

  2. 框架选择

    Java AWT包编写界面较为底层,速度快,理论上有最高的调整自由度,有开源的美化包,组员对此技术熟练度高,但缺点是此方法无法使用可视化编程制作界面,操作监听逻辑复杂,如使用则制作周期较长;

    Java FX包为在Java AWT包基础上开发出的界面技术,可以使用Scene Builder界面可视化编程工具快速生成客户端界面,操作监听简单,程序结构清晰,容易Debug和更新维护,但缺点此技术是在开发之前相对陌生,需要花费精力磨合学习。

    在程序设计开始之前,小组成员分为两组,在两周时间内分别尝试两种技术编写最基本的框选功能程序。结果表明,Java AWT包的编程封装困难,逻辑混乱,扩展性不强,界面编写审美不佳,代码冗长而效果寥寥;Java FX包编写逻辑清晰,界面美观,且可以预留日后开发的模块位置,扩展性强。

    综合以上考虑,小组选择Java FX技术。

(二)程序模块分解

​ 由于Java FX可视化界面编程的特性,界面相关排布和监听对应函数代码被部署在FXML文件中,故我们因此可以将界面和业务逻辑完全分离。分别以界面和业务逻辑为基点,可以分为如图模块:

image

图1:程序模块分析

(三)关键技术难点

​ 实现本工程所有功能需要有以下技术储备为前提:

  1. 图片文件(jpeg和png等)可以与Xml配置文件(如果存在)成对读取并存储其url;
  2. 界面组件可以实现图片在指定的位置以指定的大小精确显示;
  3. 界面组件可以实现自定义图形(直线和矩形)被显示在图片之上;
  4. 鼠标事件可以实现实时获取鼠标位置,可以实现在指定的区域通过拖拽绘制矩形图形;
  5. 列表组件可以选中,并获取选中的元素数据,列表可以多次加载;

三、方案设计

(一)技术基础

1. 以maven构建项目,引入Java FX等包
    <dependency>
      <groupId>com.jfoenix</groupId>
      <artifactId>jfoenix</artifactId>
      <version>8.0.8</version>
    </dependency>

    <dependency>
      <groupId>org.dom4j</groupId>
      <artifactId>dom4j</artifactId>
      <version>2.0.0</version>
    </dependency>
2. 实现工具类:文件对FilePair

此数据结构用于将读入的工作文件夹中所有的图片和Xml配置文件的url以配对的形式保存和操作。

public class FilePair {
   public File picture;
   public File property;
   public String picURL;
   public String proURL;
   public boolean hasInitialized;
   public String name;

   public FilePair(String picStr, String proStr, String name) {...}

   public FilePair(String picStr, String name){...}

   @Override
   public String toString() {...}
}

3. 实现工具类:矩形结构RectArc

此数据结构用于将绘制的矩形在程序运行时存储于内存中,并在合适的时机绘制在图片展示面板上

public class RectArc {
   public double X1;
   public double Y1;
   public double X2;
   public double Y2;
   public String word;
   public Color color;
   public Line lu, ld, ll, lr;
   public boolean isRemoved = false;

   public RectArc(double x1, double y1, double x2, double y2, Color c) {...}
   public RectArc(double x1, double y1, double x2, double y2, String OCR, Color c) {...}
   public void initializeLines(Pane father,int flag){...}
   public void resetLines(){...}
   public void removeLines(Pane father){...}
   public void initializeWord(ListView listView, int index){...}
   public String getWord(){...}
   public void setWord(String str){...}
   public void remove(){...}
}
  1. 实现工具类:Xml文件读写XmlAccessor

此类静态函数用于读写指定格式的Xml文件,每次读取全部文件,或完全复写旧的Xml文件。

在xml文件的安排中,我们选择存储矩形框的相对图片长宽的归一化相对位置(以防止在不同设备上读取框位置的偏移),以及框的颜色和识别内容。

public class XmlAccessor {
	public static void Xml2RectArcList(double width, double height, double offsetX, double offsetY, FilePair filePair, ArrayList<RectArc> rectArcList){...}

	public static void RectArcList2Xml(double width, double height, double offsetX, double offsetY, FilePair filePair, ArrayList<RectArc> rectArcList){...}
}

(二)从stage开始实现一个FX程序的开端

public class mainFrame extends Application {
	...
    public static final String primaryScenePath = "/FXController/PrimarySceneController.fxml";
    public static final String MarkScenePath = "/FXController/MarkSceneController.fxml";

    public static void main( String[] args ){Application.launch(args);}

    @Override
    public void start(Stage stage) throws Exception {
        ...
        Parent root = FXMLLoader.load(getClass().getResource(primaryScenePath));
        Scene scene = new Scene(root);
        root = FXMLLoader.load(getClass().getResource(MarkScenePath));
        scene = new Scene(root);
        mainFrame.mainStage.setScene(mainFrame.primaryScene);
        mainFrame.mainStage.show();
        ...
    }
}

此处为程序执行的开端,执行后界面由stage跳转到scene,此后在各个scene之间相互切换,实现换页功能;
每个scene的启动需要一个控制类controller和对应的同名配置文件controller.fxml,在创建时需要使用用类函数FXMLLoader创建controller对象,并以此加载一个scene以展示。以下我们依次介绍主页面的MarkSceneControllerMarkSceneController.fxml

image

图2:Java FX运行流程

(三)主页面MarkSceneController

1. 文件夹读取及文件配对
	//将文件装载入fileList并部署到界面
	public void fileDeploy(File dirFile){
        //获得文件总列表
        File allFiles[] = dirFile.listFiles();

        //获得图片列表和配置文件列表(过滤掉子文件夹和其他文件类型)
        ArrayList<File> picList = new ArrayList<>();
        ArrayList<File> xmlList = new ArrayList<>();
        
        //过滤并分类该文件目录下所有的图片文件和xml配置文件
        for (int i = 0; i < allFiles.length; i++) {...}

        //初始化存储文件信息的列表
        //fileList、allPicNameList、donePicNameList、undonePicNameList
        {...}

        //填充fileList、allPicNameList、donePicNameList、undonePicNameList
        //将配好的文件对装载入FilePair中并存储进fileList
        //创建缺省xml配置的FilePair,创建缺省的xml文件,并将这部分文件对存储
        Iterator picIt = picList.iterator();
        Iterator xmlIt;
        while(picIt.hasNext()) {...}
    }

    //将文件列表fileList装载到三个PicListView用于显示到界面列表中
    public void picListDeploy(){...}
2. 图片在指定区域的合理显示和矩形框列表的实时装载
	 //从xml装载或调整当前图片,以适应当前窗口状态,同时载入矩形框列表
    public void fitImage(){
        if(markImage != null){
            //图片适应策略:
        	//	当图片为较宽图片时:缩减高度,适应宽度,两边顶天,上下留白
        	//	当图片为较高图片时:缩减宽度,适应高度,头顶顶天,两边留白
            {...}

            //定义画笔范围
        	{...}

            //消除上次的矩形框显示并重新装载文字列表
            {...}
        }
    }
3. 鼠标拖拽矩形框的生成和存储
    //鼠标按压检测
	//在鼠标不越界的情况下,记录鼠标按压点为起始点,以此绘制矩形框的一个固定角
    @FXML
    void mPressed(MouseEvent event) {...}

    //鼠标拖动检测
	//在鼠标不越界的情况下,记录鼠标拖拽点为临时终点,以此在拖拽的过程中不断刷新界面,绘制拖拽矩形框
    @FXML
    void mDragged(MouseEvent event) {...}

    //鼠标释放检测
	//在拖拽结束后,如果拖拽框不至于过于窄,则将此框数据存储进RectArcList,重新归位所有部件
    @FXML
    void mRelease(MouseEvent event) {...}
4. 列表点击切换的监听实现
    /**
     * 内部监听类,实现列表点击的图片切换
     * 实现功能:
     * 1.保存上一次的标记
     * 2.清除上一次的绘图痕迹
     * 3.载入下一张图片及配置文件
     * */
    private class imageListItemChangeListener implements ChangeListener<Object> {
        @Override
        public void changed(Object oldValue, Object newValue) {
            //除旧:
            //	1. 模式重置
            //	2. 保存已经做出的矩形框改变
            {...}

            //迎新:
            //	1. 载入新的图片到图片显示区域
            //	2. 将图片对应的矩形框列表载入到右侧的列表中
            {...}
        }
    }

    /**
     * 内部监听类,实现列表点击的标注文字切换(即选择框的切换)
     * 实现功能:
     * 1.实时保存标注文字
     * 2.改变文字输入框中的内容
     * 3.存储矩形框列表选择的当前位置
     * */
    private class wordListItemChangeListener implements ChangeListener<Object> {

        @Override
        public void changed(Object oldValue, Object newValue) {
            //除旧
            //	保存已经做出的矩形框改变
            {...}
            //迎新
            //	1. 更新选择序号
            //	2. 将新的文字标注信息载入到可标注区域
            //	3. 激活新的选择框高亮
            {...}
        }
    }
5. 通过矩形点击选择矩形列表
    //鼠标点击检测
    @FXML
    void mClicked(MouseEvent event) {
		//模式为选择模式时,通过点击事件获得鼠标位置,按照存储RectArcList的顺序
        //遍历寻找包含此鼠标位置的矩形框,如果找到,则将其选中,高亮,更改当前列表选择序号
        ...
    }

以上功能4和5共同实现了图形和文字的相互选择,可以更适合标注的修改。

6. 放大镜
    //鼠标拖动检测
    @FXML
    void mDragged(MouseEvent event) {
        if(mode.equals("r")) {
				//如果模式为框选模式,则在鼠标拖动时调用setZoomer()来刷新放大镜数据
            	...
                setZoomer(endX, endY);
            }
        }
        else if(mode.equals("e")){...}
    }

    //鼠标释放检测
    @FXML
    void mRelease(MouseEvent event) {
		//当拖拽结束后,重设放大镜,清除显示数据,为下次放大镜开启做准备	
		...
		resetZoomer();
        ...
    }

    //鼠标移动检测
    @FXML
    void mMoved(MouseEvent event) {
        if(mode.equals("v")) {
			//在查看模式中,每当检测到鼠标在规定范围内移动,则按照当前鼠标位置刷新放大镜数据
            ...
            setZoomer(endX, endY);
        }
    }

    //将当前图片以指定的倍率,根据鼠标位置,显示在放大面板上(传入鼠标当前位置)
    public void setZoomer(double mouseX, double mouseY){
		//根据鼠标位置、图片显示位置来确定鼠标相对于图片的位置
        //根据放大面板的尺寸、放大倍率来确定图片在放大镜内的尺寸和位置
        //刷新图片在放大面板上的刷新
        ...
    }

    //重设放大面板
    public void resetZoomer(){
        //载入图片
        //设置放大面板不可见
        ...
    }
7. 矩形框的编辑

此功能实装了屏幕按钮和键盘按键两种操作方式,两种方式实现结果完全一致。

    //鼠标拖动检测
    @FXML
    void mDragged(MouseEvent event) {
        if(mode.equals("r")) {...}
        else if(mode.equals("e")){
			//当目前存在被选中的矩形时,在编辑模式下激活拖动以实现矩形框的位置变动
            ...
        }
    }

    //移动动作按钮组
	//以下一组按钮的响应事件对应一个选中矩形框的四条边向各自两个方向移动的事件
	//所有按钮效果程度由灵敏度条设置,即时生效
    @FXML
    void DDFunc(ActionEvent event) {...}
    void DUFunc(ActionEvent event) {...}
    void LLFunc(ActionEvent event) {...}
    void LRFunc(ActionEvent event) {...}
    void RLFunc(ActionEvent event) {...}
    void RRFunc(ActionEvent event) {...}
    void UDFunc(ActionEvent event) {...}
    void UUFunc(ActionEvent event) {...}

    //页面初始化函数
    @Override
    public void initialize(URL location, ResourceBundle resources) {
        //方向选择器初始化
        {
            //更改当前键盘编辑的矩形框的边(ctrl+wsad)
            ...
        }
        //键盘监听器和初始化
        {
            //监听键盘wsad,如果当前编辑边合适于按下的方向,则将该边向一个移动方向移动灵敏度个单位距离
            ...
        }
    }

(四)程序整体结构

1. 程序总体函数调用图

image

图3:程序总体函数调用图
2. 程序执行流程

image

图4:程序执行流程

四、程序源代码

(一)文件目录结构

image

图5:文件目录结构

(二)各文件全代码

1. mainFrame.java
package org.controllers;

import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;

public class mainFrame extends Application {

    public static Stage mainStage;
    public static Scene primaryScene;
    public static Scene markScene;

    public static final String primaryScenePath = "/FXController/PrimarySceneController.fxml";
    public static final String MarkScenePath = "/FXController/MarkSceneController.fxml";

    public static final String avilableAccount = "Architect";
    public static final String password = "123456";

    public static void main( String[] args ){
        Application.launch(args);// 启动软件
    }

    @Override
    public void start(Stage stage) throws Exception {
        mainFrame.mainStage = stage;
        mainFrame.mainStage.setTitle("Marker 01");

        Parent root = FXMLLoader.load(getClass().getResource(primaryScenePath));
        Scene scene = new Scene(root);
        mainFrame.primaryScene = scene;

        root = FXMLLoader.load(getClass().getResource(MarkScenePath));
        scene = new Scene(root);
        mainFrame.markScene = scene;
        mainFrame.mainStage.setScene(mainFrame.primaryScene);
        mainFrame.mainStage.setMaximized(false);
        mainFrame.mainStage.setResizable(false);
        mainFrame.mainStage.show();
    }
}
2. MarkSceneController.java
package org.controllers;

import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.geometry.Bounds;
import javafx.geometry.Point2D;
import javafx.scene.control.*;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.input.*;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.HBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.Line;
import javafx.stage.DirectoryChooser;
import org.XmlReader.XmlAccessor;
import org.structures.FilePair;
import org.structures.RectArc;

import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.ResourceBundle;

import static org.XmlReader.XmlAccessor.RectArcList2Xml;

public class MarkSceneController implements Initializable {

    //定义文件存储的哈希表,用于在选择文件后反向检索
    public static HashMap<String, FilePair> fileList;
    //定义三个文件名清单,用于左侧列表选择文件
    public static ArrayList<String> allPicNameList, donePicNameList, undonePicNameList;
    //定义矩形清单,用于绘制当前所有矩形
    public ArrayList<RectArc> rectArcList = null;
    public Image markImage = null;
    public FilePair currentFilePair = null;

    private double startX, startY, endX, endY;
    private double actualX1 = 0;
    private double actualY1 = 0;
    private double actualX2 = 0;
    private double actualY2 = 0;
    private Line lu, ld, lr, ll;
    private Line focusLine1, focusLine2;
    private boolean isDragging;
    private boolean hasInitialized = false;
    private boolean isEditing = false;
    private int rectIndex = 1;
    private int senseRate = 4;
    private int rectIndexCurrent = -1;
    private int flag = 0;
    private boolean isRebooting = false;
    public String  describe1="";
    public String  describe2="";
    public double usedWidth = 0;
    public double usedHeight = 0;
    public double offsetX = 0;
    public double offsetY = 0;
    public double zoomRate = 5;
    public String mode = "r";
    public String direction = "up";

    public double markAbsoluteX;
    public double markAbsoluteY;

    public Color defaultColor = Color.BLACK;

    @FXML
    private HBox aboveHBox;
    @FXML
    private ListView allPicListView;
    @FXML
    private ListView donePicListView;
    @FXML
    private ListView undonePicListView;
    @FXML
    private ListView wordListView;
    @FXML
    private ColorPicker colorPicker;
    @FXML
    private AnchorPane markAnchorPane;
    @FXML
    private AnchorPane zoomerAnchorPane;
    @FXML
    private ImageView markImageView;
    @FXML
    private ImageView zoomerImageView;
    @FXML
    private TextArea wordTextArea;
    @FXML
    private Label conditionLabel;
    @FXML
    private Slider zoomerSlider;
    @FXML
    private Button editButton;
    @FXML
    private Slider senseSlider;
    @FXML
    private ToggleGroup TG;
    @FXML
    private RadioButton editModeRadio;
    @FXML
    private RadioButton rectModeRadio;
    @FXML
    private RadioButton selectModeRadio;
    @FXML
    private RadioButton viewModeRadio;
    @FXML
    private ToggleGroup DTG;
    @FXML
    private RadioButton upModeRadio;
    @FXML
    private RadioButton downModeRadio;
    @FXML
    private RadioButton leftModeRadio;
    @FXML
    private RadioButton rightModeRadio;

    /**
     * 页面初始化函数
     * */
    @Override
    public void initialize(URL location, ResourceBundle resources) {
        /**
         * 变量初始化,防空值
         * */
        {
            isDragging = false;
            startX = 0;
            startY = 0;
            endX = 0;
            endY = 0;
        }
        /**
         * 线条初始化
         * */
        {
            lu = new Line(0, 0, 0, 0);
            ld = new Line(0, 0, 0, 0);
            lr = new Line(0, 0, 0, 0);
            ll = new Line(0, 0, 0, 0);
            markAnchorPane.getChildren().add(lu);
            markAnchorPane.getChildren().add(ld);
            markAnchorPane.getChildren().add(lr);
            markAnchorPane.getChildren().add(ll);

            focusLine1 = new Line(175, 90, 175, 110);
            focusLine2 = new Line(165, 100, 185, 100);
            zoomerAnchorPane.getChildren().add(focusLine1);
            zoomerAnchorPane.getChildren().add(focusLine2);
        }
        /**
         * 状态词条初始化
         * */
        {
            conditionLabel.setText("就绪");
        }
        /**
         * 列表选择器初始化
         * 包含监听初始化
         * 选择模式初始化
         * */
        {
            imageListItemChangeListener ilicl =  new imageListItemChangeListener();
            allPicListView.getSelectionModel().setSelectionMode(SelectionMode.SINGLE);
            allPicListView.getSelectionModel().selectedItemProperty().addListener(ilicl);
            donePicListView.getSelectionModel().setSelectionMode(SelectionMode.SINGLE);
            donePicListView.getSelectionModel().selectedItemProperty().addListener(ilicl);
            undonePicListView.getSelectionModel().setSelectionMode(SelectionMode.SINGLE);
            undonePicListView.getSelectionModel().selectedItemProperty().addListener(ilicl);
            wordListView.getSelectionModel().selectedItemProperty().addListener(new wordListItemChangeListener());
        }
        /**
         * 画板尺寸监听器初始化
         * */
        {
            resizeChangeListener rcl = new resizeChangeListener();
            markAnchorPane.widthProperty().addListener(rcl);
            markAnchorPane.widthProperty().addListener(rcl);
        }
        /**
         * 矩形列表初始化
         * */
        {
            rectArcList = new ArrayList<>();
        }
        /**
         * 拖动条监听器初始化
         * */
        {
            zoomerSlider.setValue(zoomRate);
            zoomerSlider.valueProperty().addListener(new ChangeListener<Number>() {
                @Override
                public void changed(ObservableValue<? extends Number> observable, Number oldValue, Number newValue) {
                    if(newValue.intValue() != zoomRate) {
                        zoomRate = newValue.intValue();

                    }
                }
            });
        }
        /**
         * 拖动条监听器初始化
         * */
        {
            senseSlider.setValue(senseRate);
            senseSlider.valueProperty().addListener(new ChangeListener<Number>() {
                @Override
                public void changed(ObservableValue<? extends Number> observable, Number oldValue, Number newValue) {
                    if(newValue.intValue() != senseRate) {
                        senseRate = newValue.intValue();
                        System.out.println(senseRate);

                    }
                }
            });
        }
        /**
         * 模式选择器初始化
         * */
        {
            selectModeRadio.setUserData("s");
            editModeRadio.setUserData("e");
            viewModeRadio.setUserData("v");
            rectModeRadio.setUserData("r");

            TG.selectedToggleProperty().addListener(new ChangeListener<Toggle>() {
                public void changed(ObservableValue<? extends Toggle> ov,
                                    Toggle old_toggle, Toggle new_toggle) {
                    if (TG.getSelectedToggle() != null) {
                        mode = TG.getSelectedToggle().getUserData().toString();
                        System.out.println("Current Mode : " + mode);
                    }
                    endEdit();
                    resetZoomer();
                }
            });
        }
        /**
         * 方向选择器初始化
         * */
        {
            editButton.setVisible(false);
            upModeRadio.setUserData("up");
            downModeRadio.setUserData("down");
            leftModeRadio.setUserData("left");
            rightModeRadio.setUserData("right");

            DTG.selectedToggleProperty().addListener(new ChangeListener<Toggle>() {
                public void changed(ObservableValue<? extends Toggle> ov,
                                    Toggle old_toggle, Toggle new_toggle) {
                    if (DTG.getSelectedToggle() != null) {
                        direction = DTG.getSelectedToggle().getUserData().toString();
                        System.out.println("Current Direction : " + direction);
                    }
                }
            });
        }
        /**
         * 键盘监听器和初始化
         * */
        {
            aboveHBox.setOnKeyTyped(new EventHandler<KeyEvent>() {

                @Override
                public void handle(KeyEvent event) {

                    if (event.isControlDown()) {
                        if (string2Ascii(event.getCharacter()).equals("23")) {
                            upModeRadio.setSelected(true);
                            upModeRadio.requestFocus();
                        } else if (string2Ascii(event.getCharacter()).equals("19")) {
                            downModeRadio.setSelected(true);
                            downModeRadio.requestFocus();
                        } else if (string2Ascii(event.getCharacter()).equals("1")) {
                            leftModeRadio.setSelected(true);
                            leftModeRadio.requestFocus();
                        } else if (string2Ascii(event.getCharacter()).equals("4")) {
                            rightModeRadio.setSelected(true);
                            rightModeRadio.requestFocus();
                        }
                    } else if (mode.equals("e") && rectIndexCurrent > 0) {
                        if (event.getCharacter().equalsIgnoreCase("w")) {
                            if (direction.equals("up")) {     //UU
                                int a = rectIndexCurrent;
                                rectArcList.get(rectIndexCurrent - 1).Y1 -= senseRate;
                                rectArcList.get(rectIndexCurrent - 1).resetLines();
                                save();
                                rectIndexCurrent = a;
                                startEdit();
                            } else if (direction.equals("down")) {  //DU
                                int a = rectIndexCurrent;
                                rectArcList.get(rectIndexCurrent - 1).Y2 -= senseRate;
                                rectArcList.get(rectIndexCurrent - 1).resetLines();
                                save();
                                rectIndexCurrent = a;
                                startEdit();
                            }
                        } else if (event.getCharacter().equalsIgnoreCase("s")) {
                            if (direction.equals("up")) {     //UD
                                int a = rectIndexCurrent;
                                rectArcList.get(rectIndexCurrent - 1).Y1 += senseRate;
                                rectArcList.get(rectIndexCurrent - 1).resetLines();
                                save();
                                rectIndexCurrent = a;
                                startEdit();
                            } else if (direction.equals("down")) {  //DD
                                int a = rectIndexCurrent;
                                rectArcList.get(rectIndexCurrent - 1).Y2 += senseRate;
                                rectArcList.get(rectIndexCurrent - 1).resetLines();
                                save();
                                rectIndexCurrent = a;
                                startEdit();
                            }
                        } else if (event.getCharacter().equalsIgnoreCase("a")) {
                            if (direction.equals("left")) {     //LL
                                int a = rectIndexCurrent;
                                rectArcList.get(rectIndexCurrent - 1).X1 -= senseRate;
                                rectArcList.get(rectIndexCurrent - 1).resetLines();
                                save();
                                rectIndexCurrent = a;
                                startEdit();
                            } else if (direction.equals("right")) {  //RL
                                int a = rectIndexCurrent;
                                rectArcList.get(rectIndexCurrent - 1).X2 -= senseRate;
                                rectArcList.get(rectIndexCurrent - 1).resetLines();
                                save();
                                rectIndexCurrent = a;
                                startEdit();
                            }
                        } else if (event.getCharacter().equalsIgnoreCase("d")) {
                            if (direction.equals("left")) {     //LR
                                int a = rectIndexCurrent;
                                rectArcList.get(rectIndexCurrent - 1).X1 += senseRate;
                                rectArcList.get(rectIndexCurrent - 1).resetLines();
                                save();
                                rectIndexCurrent = a;
                                startEdit();
                            } else if (direction.equals("right")) {  //RR
                                int a = rectIndexCurrent;
                                rectArcList.get(rectIndexCurrent - 1).X2 += senseRate;
                                rectArcList.get(rectIndexCurrent - 1).resetLines();
                                save();
                                rectIndexCurrent = a;
                                startEdit();
                            }
                        }
                    }

                }
            });
        }
    }

    /**
     * 鼠标按压检测
     * */
    @FXML
    void mPressed(MouseEvent event) {
        if(isEditing)
            endEdit();
        if(mode.equals("r")) {
            if (event.getSceneX() - markAbsoluteX >= actualX1 &&
                    event.getSceneX() - markAbsoluteX <= actualX2 &&
                    event.getSceneY() - markAbsoluteY >= actualY1 &&
                    event.getSceneY() - markAbsoluteY <= actualY2)

            {//这里防止画笔起始越界
                System.out.println("Mouse Pressed : (" + event.getSceneX() + ", " + event.getSceneY() + ")");
                startX = event.getSceneX();
                startY = event.getSceneY();
                isDragging = true;
                locationUpdate();
                setZoomer(event.getSceneX(), event.getSceneY());
            }
        }
        else if(mode.equals("e")){
            if(rectIndexCurrent > 0) {
                startX = event.getSceneX();
                startY = event.getSceneY();
            }
        }

    }
    
    /**
     * 鼠标拖动检测
     * */
    @FXML
    void mDragged(MouseEvent event) {
        if(mode.equals("r")) {
            if (isDragging) {

                if (event.getSceneX() < actualX1 + markAbsoluteX) {
                    endX = markAbsoluteX + actualX1;
                } else if (event.getSceneX() > actualX2 + markAbsoluteX) {
                    endX = markAbsoluteX + actualX2;
                } else {
                    endX = event.getSceneX();
                }

                if (event.getSceneY() < actualY1 + markAbsoluteY) {
                    endY = markAbsoluteY + actualY1;
                } else if (event.getSceneY() > actualY2 + markAbsoluteY) {
                    endY = markAbsoluteY + actualY2;
                } else {
                    endY = event.getSceneY();
                }

                paintRect();
                setZoomer(endX, endY);
            }
        }
        else if(mode.equals("e")){
            if(rectIndexCurrent > 0) {
                double moveX = event.getSceneX() - startX;
                double moveY = event.getSceneY() - startY;
                startX = event.getSceneX();
                startY = event.getSceneY();
                rectArcList.get(rectIndexCurrent - 1).X1 += moveX;
                rectArcList.get(rectIndexCurrent - 1).X2 += moveX;
                rectArcList.get(rectIndexCurrent - 1).Y1 += moveY;
                rectArcList.get(rectIndexCurrent - 1).Y2 += moveY;
                if(currentFilePair != null)//手动保存
                    RectArcList2Xml(actualX2 - actualX1, actualY2 - actualY1, actualX1, actualY1, currentFilePair, rectArcList);
                fitImage();

            }
        }
    }
    
    /**
     * 鼠标释放检测
     * */
    @FXML
    void mRelease(MouseEvent event) {
        if(isDragging) {
            System.out.println("Mouse Released : (" + event.getSceneX() + ", " + event.getSceneY() + ")");

            if (!((event.getSceneX() - startX < 0.1 && event.getSceneX() - startX > -0.1) ||
                    (event.getSceneY() - startY < 0.1 && event.getSceneY() - startY > -0.1))) {
                //此处if防止细微误触的错误
                rectArcList.add(new RectArc(startX - markAbsoluteX,
                        startY - markAbsoluteY,
                        endX - markAbsoluteX,
                        endY - markAbsoluteY,
                        defaultColor));
            }

            if (currentFilePair != null)//自动保存
                RectArcList2Xml(actualX2 - actualX1, actualY2 - actualY1, actualX1, actualY1, currentFilePair, rectArcList);
            //消除上一次加载的影响
            resetRectArc();
            resetWord();
            //从xml装载rectArcList
            XmlAccessor.Xml2RectArcList(actualX2 - actualX1, actualY2 - actualY1, actualX1, actualY1, this.currentFilePair, this.rectArcList);
            //从rectArcList中载入图像和文字
            loadRectArc();
            loadWord();
            setPaintRectDefault();
            isDragging = false;
            resetZoomer();

            Iterator it = rectArcList.iterator();
            int num = -1;
            while(it.hasNext()){
                num++;
                it.next();
            }
            if(num >= 0){
                System.out.println("num = " + num);
                wordListView.getSelectionModel().select(num);
                wordListView.getFocusModel().focus(num);
            }

        }
    }
    
    /**
     * 鼠标点击检测
     * */
    @FXML
    void mClicked(MouseEvent event) {
        if(mode.equals("s")) {
            int i = getRectIndex(event.getSceneX() - markAbsoluteX, event.getSceneY() - markAbsoluteY);
            System.out.println("Rect Selected : " + i);
            if(i >= 0) {
                wordListView.getSelectionModel().select(i);
                wordListView.getFocusModel().focus(i);
            }
        }

    }
    
    /**
     * 鼠标移动检测
     * */
    @FXML
    void mMoved(MouseEvent event) {
        if(mode.equals("v")) {
            if (event.getSceneX() < actualX1 + markAbsoluteX) {
                endX = markAbsoluteX + actualX1;
            } else if (event.getSceneX() > actualX2 + markAbsoluteX) {
                endX = markAbsoluteX + actualX2;
            } else {
                endX = event.getSceneX();
            }
            if (event.getSceneY() < actualY1 + markAbsoluteY) {
                endY = markAbsoluteY + actualY1;
            } else if (event.getSceneY() > actualY2 + markAbsoluteY) {
                endY = markAbsoluteY + actualY2;
            } else {
                endY = event.getSceneY();
            }
            setZoomer(endX, endY);
        }
    }
    
    /**
     * 清除按钮
     * */
    @FXML
    void ClearFunc(ActionEvent event){
        if(isEditing)
            endEdit();
        if(rectIndexCurrent > 0){
            isRebooting = true;

            rectArcList.get(rectIndexCurrent - 1).setWord("");
            if(currentFilePair != null)//在清空时自动保存
                RectArcList2Xml(actualX2 - actualX1, actualY2 - actualY1, actualX1, actualY1, currentFilePair, rectArcList);
            wordTextArea.setText("");
            fitImage();
            rectIndexCurrent = -1;

            isRebooting = false;

        }
    }
    
    /**
     * 删除按钮
     * */
    @FXML
    void DeleteFunc(ActionEvent event){
        if(isEditing)
            endEdit();
        if(rectIndexCurrent > 0){
            isRebooting = true;

            rectArcList.get(rectIndexCurrent - 1).remove();
            if(currentFilePair != null)//在清空时自动保存
                RectArcList2Xml(actualX2 - actualX1, actualY2 - actualY1, actualX1, actualY1, currentFilePair, rectArcList);
            wordTextArea.setText("");
            fitImage();
            rectIndexCurrent = -1;
            isRebooting = false;
        }
    }
    
    /**
     * 打开文件夹按钮
     * */
    @FXML
    void OpenDirFunc(ActionEvent event) {
        if(isEditing)
            endEdit();
        DirectoryChooser directoryChooser = new DirectoryChooser();
        directoryChooser.setTitle("选择目标文件夹");
        File dirFile = directoryChooser.showDialog(mainFrame.mainStage);

        if(dirFile.exists()){
            fileDeploy(dirFile);//文件列表装载
            picListDeploy();//显示列表装载
            conditionLabel.setText("文件装载就绪");
        }
        else{
            conditionLabel.setText("请选择正确的文件夹");
        }
    }
        /**
     * 帮助按钮
     * */
    @FXML
    void HelpFunc(ActionEvent event) throws IOException {
        Runtime.getRuntime().exec("rundll32 url.dll,FileProtocolHandler https://blog.csdn.net/qq_46391766/article/details/123209968");//使用默认浏览器打开url
    }

    /**
     * 保存按钮
     * */
    @FXML
    void SaveFunc(ActionEvent event) {
        if(currentFilePair != null)//手动保存
            RectArcList2Xml(actualX2 - actualX1, actualY2 - actualY1, actualX1, actualY1, currentFilePair, rectArcList);
    }
    
    /**
     * 编辑按钮
     * */
    @FXML
    void EditFunc(ActionEvent event) {
        if(isEditing){
            endEdit();
        }
        else
            if(rectIndexCurrent > 0){
                startEdit();
            }
    }
    
    /**
     * 完成按钮
     * */
    @FXML
    void FinishFunc(ActionEvent event) {
        if(isEditing)
            endEdit();
        if(describe1 != null) {
            //除旧
            isRebooting = true;
                    System.out.println("Word List Item Changed");
            System.out.println(describe2);
            if (rectIndexCurrent > 0) {
                rectArcList.get(rectIndexCurrent - 1).setWord(wordTextArea.getText());
                if(currentFilePair != null)//在换页时自动保存
                    RectArcList2Xml(actualX2 - actualX1, actualY2 - actualY1, actualX1, actualY1, currentFilePair, rectArcList);
                fitImage();
            }
            isRebooting = false;
        }
    }
    
    /**
     * 颜色更改按钮
     * */
    @FXML
    void ColorSetFunc(ActionEvent event) {
        if(isEditing){
            rectArcList.get(rectIndexCurrent - 1).color = colorPicker.getValue();
            save();
        }
        else
            defaultColor = colorPicker.getValue();
    }
    
    /**
     * 移动动作按钮组
     * */
    @FXML
    void DDFunc(ActionEvent event) {
        if(mode.equals("e") && rectIndexCurrent > 0){
            int a = rectIndexCurrent;
            rectArcList.get(rectIndexCurrent - 1).Y2 += senseRate;
            rectArcList.get(rectIndexCurrent - 1).resetLines();
            save();
            rectIndexCurrent = a;
            startEdit();
        }
    }
    @FXML
    void DUFunc(ActionEvent event) {
        if(mode.equals("e") && rectIndexCurrent > 0){
            int a = rectIndexCurrent;
            rectArcList.get(rectIndexCurrent - 1).Y2 -= senseRate;
            rectArcList.get(rectIndexCurrent - 1).resetLines();
            save();
            rectIndexCurrent = a;
            startEdit();
        }
    }
    @FXML
    void LLFunc(ActionEvent event) {
        if(mode.equals("e") && rectIndexCurrent > 0){
            int a = rectIndexCurrent;
            rectArcList.get(rectIndexCurrent - 1).X1 -= senseRate;
            rectArcList.get(rectIndexCurrent - 1).resetLines();
            save();
            rectIndexCurrent = a;
            startEdit();
        }
    }
    @FXML
    void LRFunc(ActionEvent event) {
        if(mode.equals("e") && rectIndexCurrent > 0){
            int a = rectIndexCurrent;
            rectArcList.get(rectIndexCurrent - 1).X1 += senseRate;
            rectArcList.get(rectIndexCurrent - 1).resetLines();
            save();
            rectIndexCurrent = a;
            startEdit();
        }
    }
    @FXML
    void RLFunc(ActionEvent event) {
        if(mode.equals("e") && rectIndexCurrent > 0){
            int a = rectIndexCurrent;
            rectArcList.get(rectIndexCurrent - 1).X2 -= senseRate;
            rectArcList.get(rectIndexCurrent - 1).resetLines();
            save();
            rectIndexCurrent = a;
            startEdit();
        }
    }
    @FXML
    void RRFunc(ActionEvent event) {
        if(mode.equals("e") && rectIndexCurrent > 0){
            int a = rectIndexCurrent;
            rectArcList.get(rectIndexCurrent - 1).X2 += senseRate;
            rectArcList.get(rectIndexCurrent - 1).resetLines();
            save();
            rectIndexCurrent = a;
            startEdit();
        }
    }
    @FXML
    void UDFunc(ActionEvent event) {
        if(mode.equals("e") && rectIndexCurrent > 0){
            int a = rectIndexCurrent;
            rectArcList.get(rectIndexCurrent - 1).Y1 += senseRate;
            rectArcList.get(rectIndexCurrent - 1).resetLines();
            save();
            rectIndexCurrent = a;
            startEdit();
        }
    }
    @FXML
    void UUFunc(ActionEvent event) {
        if(mode.equals("e") && rectIndexCurrent > 0){
            int a = rectIndexCurrent;
            rectArcList.get(rectIndexCurrent - 1).Y1 -= senseRate;
            rectArcList.get(rectIndexCurrent - 1).resetLines();
            save();
            rectIndexCurrent = a;
            startEdit();
        }
    }
    
    /**
     * 开始编辑
     * */
    public void startEdit(){
        colorPicker.setValue(rectArcList.get(rectIndexCurrent - 1).color);
        editButton.setText("完成");
        isEditing = true;
    }
    
    /**
     * 保存
     * */
    public void save(){
        if(currentFilePair != null)//保存
            RectArcList2Xml(actualX2 - actualX1, actualY2 - actualY1, actualX1, actualY1, currentFilePair, rectArcList);
        fitImage();
    }
    
    /**
     * 结束编辑
     * */
    public void endEdit(){
        colorPicker.setValue(defaultColor);
        isEditing = false;
    }
    
    /**
     * 绘制临时方框
     * */
    public void paintRect(){
        double x1 = startX - markAbsoluteX;
        double x2 = endX - markAbsoluteX;
        double y1 = startY - markAbsoluteY;
        double y2 = endY - markAbsoluteY;
        setLineData(lu, x1, x2, y1, y1, defaultColor);
        setLineData(ld, x1, x2, y2, y2, defaultColor);
        setLineData(ll, x1, x1, y1, y2, defaultColor);
        setLineData(lr, x2, x2, y1, y2, defaultColor);
    }
    
    /**
     * 设定线条参数
     * */
    public static void setLineData(Line l, double x1, double x2, double y1, double y2, Color color){
        if(l != null){
            l.setStartX(x1);
            l.setStartY(y1);
            l.setEndX(x2);
            l.setEndY(y2);
            l.setStroke(color);
        }
    }
    
    /**
     * 固定刷新窗口坐标数据,防止位置跑偏
     * 每次在按下鼠标时执行,防止期间用户拖动改变窗口大小
     * 窗口宽度数据aboveSpiltPane.getWidth()不会在initialize中更新,必须放至此
     * */
    public void locationUpdate(){
        Bounds layoutBounds = markAnchorPane.getLayoutBounds();
        Point2D localToScene = markAnchorPane.localToScene(layoutBounds.getMinX(), layoutBounds.getMinY());
        markAbsoluteX = localToScene.getX();
        markAbsoluteY = localToScene.getY();
    }
    
    /**
     * 将文件数据装载到fileList
     * */
    public void fileDeploy(File dirFile){
        //获得文件总列表
        File allFiles[] = dirFile.listFiles();

        //获得图片列表和配置文件列表(过滤掉子文件夹和其他文件类型)
        ArrayList<File> picList = new ArrayList<>();
        ArrayList<File> xmlList = new ArrayList<>();
        for (int i = 0; i < allFiles.length; i++) {
            File fs = allFiles[i];
            if (fs.isDirectory()) {
                System.out.println(fs.getName() + " [目录]");
            } else {
                //获得扩展名,如“.png”
                String suffix = fs.getAbsolutePath().
                        substring(fs.getAbsolutePath().lastIndexOf("."));
                //获得文件名(不包含扩展名)
                String name = fs.getName().
                        substring(0, fs.getName().lastIndexOf("."));
                System.out.println(suffix + "   " + name);

                if(suffix.equalsIgnoreCase(".png")
                        || suffix.equalsIgnoreCase(".jpg")
                        || suffix.equalsIgnoreCase(".jpeg")){
                    picList.add(fs);
                }
                else if(suffix.equalsIgnoreCase(".xml")){
                    xmlList.add(fs);
                }

            }
        }

        /**
         * 各列表初始化
         * */
        {
            if(fileList == null) {fileList = new HashMap<>();}
            else {fileList.clear();}

            if (donePicNameList == null) {donePicNameList = new ArrayList<>();}
            else {donePicNameList.clear();}

            if (undonePicNameList == null){undonePicNameList = new ArrayList<>();}
            else{undonePicNameList.clear();}

            if (allPicNameList == null){allPicNameList = new ArrayList<>();}
            else{allPicNameList.clear();}
        }

        //填充fileList、allPicNameList、donePicNameList、undonePicNameList
        Iterator picIt = picList.iterator();
        Iterator xmlIt;

        while(picIt.hasNext()) {
            File picFile = (File) picIt.next();
            boolean createFlag = true;  //当为true时,外循环需要创建一个没有xml的FilePair
            String picName = picFile.getName().substring(0, picFile.getName().lastIndexOf("."));
            xmlIt = xmlList.iterator();

            while (xmlIt.hasNext()) {
                File xmlFile = (File) xmlIt.next();
                String xmlName = xmlFile.getName().substring(0, xmlFile.getName().lastIndexOf("."));
                //创建齐全的FilePair
                if (picName.equals(xmlName)) {
                    allPicNameList.add(picName);
                    donePicNameList.add(picName);
                    FilePair filePair = new FilePair(picFile.getAbsolutePath(), xmlFile.getAbsolutePath(), picName);
                    fileList.put(picName, filePair);
                    xmlList.remove(xmlFile);
                    createFlag = false;
                    break;
                }
            }

            //创建缺省xml配置的FilePair
            if (createFlag) {
                allPicNameList.add(picName);
                undonePicNameList.add(picName);
                FilePair filePair = new FilePair(picFile.getAbsolutePath(), picName);
                fileList.put(picName, filePair);
            }
        }
    }
    
    /**
     * 将文件列表fileList装载到三个PicListView
     * */
    public void picListDeploy(){
        Iterator iterator = allPicNameList.iterator();
        while(iterator.hasNext()){
            allPicListView.getItems().add(iterator.next());
        }
        iterator = donePicNameList.iterator();
        while(iterator.hasNext()){
            donePicListView.getItems().add(iterator.next());
        }

        iterator = undonePicNameList.iterator();
        while(iterator.hasNext()){
            undonePicListView.getItems().add(iterator.next());
        }
    }
    
    /**
     * 从xml装载或调整当前图片,以适应当前窗口状态,同时载入文字列表
     * 调用时机:
     * 1.在切换图片时
     * 2.在窗口尺寸变化时
     * */
    public void fitImage(){
        if(markImage != null){
            double maxWidth = markAnchorPane.getWidth();
            double maxHeight = markAnchorPane.getHeight();
            double imgWidth = markImage.getWidth();
            double imgHeight = markImage.getHeight();
            double ratio;//缩放比例

            markImageView.setImage(markImage);
            if(maxWidth/maxHeight > imgWidth/imgHeight){//这是一个相对高的图片
                /**策略:
                * 缩减宽度,适应高度,头顶顶天,两边留白
                */
                ratio = maxHeight / imgHeight;
                offsetX = maxWidth / 2 - imgWidth * ratio / 2;
                offsetY = 0;
                usedWidth = imgWidth * ratio;
                usedHeight = maxHeight;
                markImageView.setFitHeight(usedHeight);
                markImageView.setX(offsetX);
                markImageView.setY(0);
            }
            else{//这是一个相对宽的图片
                /**策略:
                 * 缩减高度,适应宽度,两边顶天,上下留白
                 * */
                ratio = maxWidth / imgWidth;
                offsetX = 0;
                offsetY = maxHeight / 2 - imgHeight * ratio / 2;
                usedWidth = maxWidth;
                usedHeight = imgHeight * ratio;
                markImageView.setFitWidth(usedWidth);
                markImageView.setX(0);
                markImageView.setY(offsetY);
            }

            //定义画笔范围
            actualX1 = offsetX;
            actualY1 = offsetY;
            actualX2 = offsetX + usedWidth;
            actualY2 = offsetY + usedHeight;

            //消除上一次加载的影响
            resetRectArc();
            resetWord();
            //从xml装载rectArcList
            XmlAccessor.Xml2RectArcList(usedWidth, usedHeight, offsetX, offsetY, this.currentFilePair, this.rectArcList);
            //从rectArcList中载入图像和文字
            loadRectArc();
            loadWord();
        }
    }
    
    /**
     * 将所有的矩形框显示,并载入文字列表
     * */
    public void loadRectArc(){
        Iterator it = this.rectArcList.iterator();
        int count = 0;
        while(it.hasNext()){
            count++;
            if(count == rectIndexCurrent){
                flag = 1;
            }
            else{
                flag = 0;
            }
            RectArc rectArc = (RectArc)it.next();
            rectArc.initializeLines(markAnchorPane,flag);
        }
    }
    
    /**
     * 重设输入框
     * */
    public void resetWord(){
        rectIndex = 1;
        wordListView.getItems().clear();
    }
    
    /**
     * 装载所有框和文字信息到右侧列表
     * */
    public void loadWord(){
        Iterator it = this.rectArcList.iterator();
        while(it.hasNext()){
            RectArc rectArc = (RectArc)it.next();
            rectArc.initializeWord(wordListView, rectIndex);
            rectIndex++;
        }
    }
    
    /**
     * 将画面上所有的痕迹删除,清空文字列表
     * */
    public void resetRectArc(){
        if(rectArcList != null){
            Iterator it = rectArcList.iterator();
            while (it.hasNext()){
                RectArc rac = (RectArc)it.next();
                rac.removeLines(markAnchorPane);
                System.out.println("removed");
            }
            wordListView.getItems().clear();
            rectArcList.clear();
        }
        else{
            rectArcList = new ArrayList<>();
        }
    }
    public void resetRectArc2(){
        if(rectArcList != null){
            Iterator it = rectArcList.iterator();
            while (it.hasNext()){
                RectArc rac = (RectArc)it.next();
                rac.removeLines(markAnchorPane);
                System.out.println("removed");
            }
            wordListView.getItems().clear();
        }
        else{
            rectArcList = new ArrayList<>();
        }
    }
    
    /**
     * 清除绘图模块的矩形至初始状态
     * */
    public void setPaintRectDefault(){
        setLineDefault(lu);
        setLineDefault(ld);
        setLineDefault(ll);
        setLineDefault(lr);
    }
    
    /**
     * 清除单一线条至初始状态
     * */
    public static void setLineDefault(Line l){
        if(l != null){
            l.setStartX(0);
            l.setStartY(0);
            l.setEndX(0);
            l.setEndY(0);
        }
    }
    
    /**
     * 将当前图片以固定的倍率 根据鼠标位置 显示在放大面板上
     * 传入鼠标当前位置
     * */
    public void setZoomer(double mouseX, double mouseY){
        double zoomHeight = usedHeight * zoomRate;
        double zoomWidth = usedWidth * zoomRate;
        double zoomOffsetX = (mouseX - markAbsoluteX - offsetX) * zoomRate - 175;
        double zoomOffsetY = (mouseY - markAbsoluteY - offsetY) * zoomRate - 100;
        zoomerImageView.setVisible(true);
        zoomerImageView.setImage(markImage);
        zoomerImageView.setFitWidth(zoomWidth);
        zoomerImageView.setFitHeight(zoomHeight);
        zoomerImageView.setX(-zoomOffsetX);
        zoomerImageView.setY(-zoomOffsetY);


    }
    
    /**
     * 重设放大面板
     * */
    public void resetZoomer(){
        zoomerImageView.setVisible(false);
    }
    
    /**
     * 重设状态变量
     * */
    public void resetMode(){
        rectModeRadio.setSelected(true);
        rectModeRadio.requestFocus();
    }
    
    /**
     * 获取对应鼠标位置的框序号,没有则返回-1
     * */
    public int getRectIndex(double mx, double my){
        Iterator it = rectArcList.iterator();
        int RID = -1;
        boolean hasSelected = false;
        while(it.hasNext()){
            RID++;
            if(isInRect((RectArc)(it.next()), mx, my)) {
                hasSelected = true;
                break;
            }
        }
        if(hasSelected)
            return RID;
        else
            return -1;
    }
    
    /**
     * 判定指定坐标位置是否属于指定框中
     * */
    public boolean isInRect(RectArc ra, double mx, double my){
        boolean xin = false;
        boolean yin = false;
        System.out.println("Rect : (" + ra.X1 + ", " + ra.Y1 + ") ~ (" + ra.X2 + ", " + ra.Y2 + ")");
        System.out.println("Mouse : (" + mx + ", " + my + ")");
        if((mx - ra.X1) * (mx - ra.X2) <= 0)
            xin = true;
        if((my - ra.Y1) * (my - ra.Y2) <= 0)
            yin = true;
        return (xin && yin);
    }
    
    /**
     * 内部监听类,实现列表点击的图片切换
     * 实现功能:
     * 1.保存上一次的标记
     * 2.清除上一次的绘图痕迹
     * 3.载入下一张图片及配置文件
     * */
    private class imageListItemChangeListener implements ChangeListener<Object> {
        @Override
        public void changed(ObservableValue<? extends Object> observable, Object oldValue, Object newValue) {
            //除旧
            resetMode();
            System.out.println(oldValue);

            if(rectIndexCurrent > 0){//文字存盘
                rectArcList.get(rectIndexCurrent - 1).setWord(wordTextArea.getText());
                wordListView.getItems().set(rectIndexCurrent - 1, rectIndexCurrent + "." + wordTextArea.getText());
            }
            wordTextArea.setText("");
            rectIndex = 1;
            rectIndexCurrent = 0;

            if(currentFilePair != null)//在换页时自动保存
                RectArcList2Xml(actualX2 - actualX1, actualY2 - actualY1, actualX1, actualY1, currentFilePair, rectArcList);
            //迎新
            System.out.println(newValue);
            FilePair fp = fileList.get(newValue);
            currentFilePair = fp;
            if(fp != null){
                /*
                 *经过实测,以下这种方法初始化Image会导致内存驻留,在高速切换图片时内存爆炸
                 * markImage = new Image("file:" + fp.picURL);
                 * 故改用IO流的方式输入
                 * */
                try {
                    markImage = new Image(Files.newInputStream(Paths.get(fp.picURL)));
                } catch (IOException e) {e.printStackTrace();}
                fitImage();
            }
            setPaintRectDefault();
        }
    }
    
    /**
     * 内部监听类,实现列表点击的标注文字切换(即选择框的切换)
     * 实现功能:
     * 1.实时保存标注文字
     * 2.改变文字输入框中的内容
     * */
    private class wordListItemChangeListener implements ChangeListener<Object> {
        @Override
        public void changed(ObservableValue<? extends Object> observable, Object oldValue, Object newValue) {
            endEdit();
            if(!isRebooting && newValue != null) {
                //除旧
                System.out.println("Word List Item Changed");
                System.out.println(oldValue);
                if (rectIndexCurrent > 0) {
                    rectArcList.get(rectIndexCurrent - 1).setWord(wordTextArea.getText());
                    wordListView.getItems().set(rectIndexCurrent - 1, rectIndexCurrent + "." + wordTextArea.getText());
                }
                //迎新
                System.out.println(newValue);
                String indexStr = ((String) newValue).substring(0, ((String) newValue).lastIndexOf("."));
                rectIndexCurrent = Integer.parseInt(indexStr);
                wordTextArea.setText(rectArcList.get(rectIndexCurrent - 1).getWord());
                fitImage();
                System.out.println(rectIndexCurrent);
            }
        }
    }
    
    private class resizeChangeListener implements ChangeListener<Object> {
        @Override
        public void changed(ObservableValue<?> observable, Object oldValue, Object newValue) {
            if(isEditing)
                endEdit();
            fitImage();
        }
    }
    
    /**
     * 工具函数,将字符串转换为ASCII码值
     * */
    public static String string2Ascii(String value) {
        StringBuffer sbu = new StringBuffer();
        char[] chars = value.toCharArray();
        for (int i = 0; i < chars.length; i++) {
            if(i != chars.length - 1)
            {
                sbu.append((int)chars[i]).append(",");
            }
            else {
                sbu.append((int)chars[i]);
            }
        }
        return sbu.toString();
    }
}
3. PrimarySceneController.java
package org.controllers;

import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.PasswordField;
import javafx.scene.control.TextField;
import java.net.URL;
import java.util.ResourceBundle;

public class PrimarySceneController implements Initializable {
    @Override
    public void initialize(URL location, ResourceBundle resources) {
        System.out.println("PrimarySceneController initialized");
        if(accountTextField != null)
            accountTextField.setText("Architect");
        if(passwordTextField != null)
            passwordTextField.setText("123456");

    }
    @FXML
    private Button ForgetButton;
    @FXML
    private Button createAccountButton;
    @FXML
    private Button loginButton;
    @FXML
    private TextField accountTextField;
    @FXML
    private PasswordField passwordTextField;
    @FXML
    private Label conditionLabel;
    /**
     * 登录按钮
     * */
    @FXML
    void loginButtonFunc(ActionEvent event) {
        String acStr = accountTextField.getCharacters().toString();
        String psStr = passwordTextField.getCharacters().toString();
        if(acStr.equals(mainFrame.avilableAccount) && psStr.equals(mainFrame.password)){
            System.out.println("Login successfully");
            pageSwitch();
        }
        else{
            conditionLabel.setText("用户名或密码错误,请重试");
        }
    }
    /**
     * 页面转换
     * */
    public void pageSwitch(){
        mainFrame.mainStage.setX(200);
        mainFrame.mainStage.setY(100);
        mainFrame.mainStage.setScene(mainFrame.markScene);
        mainFrame.mainStage.setMaximized(false);
        mainFrame.mainStage.setResizable(true);
        mainFrame.mainStage.setMinWidth(1500);
        mainFrame.mainStage.setMinHeight(800);
    }
}

4. FilePair.java
package org.structures;

import java.io.File;
import java.io.IOException;

public class FilePair {
   public File picture;
   public File property;
   public String picURL;
   public  String proURL;
   public boolean hasInitialized;
   public String name;

   public FilePair(String picStr, String proStr, String name) {
      this.name = name;
      picURL = picStr;
      proURL = proStr;
      picture = new File(picStr);
      property = new File(proStr);
      hasInitialized = true;
   }

   public FilePair(String picStr, String name){
      this.name = name;
      picture = new File(picStr);
      property = new File(picStr.substring(0, picStr.lastIndexOf(".")) + ".xml");
      try {property.createNewFile();}
      catch (IOException e) {e.printStackTrace();}
      hasInitialized = false;
   }

   @Override
   public String toString() {
      return "FilePair{" +
            picture.getName() +
            ", " + property.getName() +
            '}';
   }
}
5. RectArc.java
package org.structures;

import javafx.scene.control.ListView;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Line;


public class RectArc {
	public double X1;
	public double Y1;
	public double X2;
	public double Y2;
	public String word;
	public Color color;
	public Line lu, ld, ll, lr;
	public boolean isRemoved = false;

	public RectArc(double x1, double y1, double x2, double y2, Color c) {
		X1 = x1;
		Y1 = y1;
		X2 = x2;
		Y2 = y2;
		word = null;
		color = c;
	}

	public RectArc(double x1, double y1, double x2, double y2, String OCR, Color c) {
		X1 = x1;
		Y1 = y1;
		X2 = x2;
		Y2 = y2;
		word = OCR;
		color = c;
	}

	public void initializeLines(Pane father,int flag){
		if(flag == 0){
			lu = new Line(X1, Y1, X2, Y1);
			lu.setStroke(color);
			ld = new Line(X1, Y2, X2, Y2);
			ld.setStroke(color);
			lr = new Line(X2, Y1, X2, Y2);
			lr.setStroke(color);
			ll = new Line(X1, Y1, X1, Y2);
			ll.setStroke(color);
		}
		else{
			lu = new Line(X1, Y1, X2, Y1);
			lu.setStroke(Color.RED);
			lu.setStrokeWidth(2);
			ld = new Line(X1, Y2, X2, Y2);
			ld.setStroke(Color.RED);
			ld.setStrokeWidth(2);
			lr = new Line(X2, Y1, X2, Y2);
			lr.setStroke(Color.RED);
			lr.setStrokeWidth(2);
			ll = new Line(X1, Y1, X1, Y2);
			ll.setStroke(Color.RED);
			ll.setStrokeWidth(2);
		}
		father.getChildren().add(lu);
		father.getChildren().add(ld);
		father.getChildren().add(lr);
		father.getChildren().add(ll);
	}

	public void resetLines(){
		lu.setStartX(X1);
		lu.setStartY(Y1);
		lu.setEndX(X2);
		lu.setEndY(Y1);

		ld.setStartX(X1);
		ld.setStartY(Y2);
		ld.setEndX(X2);
		ld.setEndY(Y2);

		lr.setStartX(X2);
		lr.setStartY(Y1);
		lr.setEndX(X2);
		lr.setEndY(Y2);

		ll.setStartX(X1);
		ll.setStartY(Y1);
		ll.setEndX(X1);
		ll.setEndY(Y2);

	}

	public void removeLines(Pane father){
		if(lu != null)
			lu.setVisible(false);
		if(lu != null)
			lr.setVisible(false);
		if(lu != null)
			ll.setVisible(false);
		if(lu != null)
			ld.setVisible(false);
		father.getChildren().removeAll(lu, ld, lr, ll);
		lu = null;
		ld = null;
		lr = null;
		ll = null;
	}

	public void initializeWord(ListView listView, int index){
		if(word != null)
			//listView.getItems().add(word);
			listView.getItems().add(index + "." + word);
	}

	public String getWord(){
		if(word != null){
			return word;
		}
		else{
			return "";
		}
	}

	public void setWord(String str){
		word = str;
	}

	public void remove(){
		if(lu != null)
			lu.setVisible(false);
		if(lu != null)
			lr.setVisible(false);
		if(lu != null)
			ll.setVisible(false);
		if(lu != null)
			ld.setVisible(false);
		isRemoved = true;
	}

}
6. XmlAccessor.java
package org.XmlReader;

import javafx.scene.paint.Color;
import org.dom4j.Document;
import org.dom4j.DocumentHelper;
import org.dom4j.Element;
import org.dom4j.io.OutputFormat;
import org.dom4j.io.SAXReader;
import org.dom4j.io.XMLWriter;
import org.structures.FilePair;
import org.structures.RectArc;

import java.io.File;
import java.io.FileOutputStream;
import java.util.ArrayList;
import java.util.Iterator;

public class XmlAccessor {
	//以下width、height和offset都是实际显示尺寸
	//利用FilePair中的Xml文件重建
	public static void Xml2RectArcList(double width, double height, double offsetX, double offsetY, FilePair filePair, ArrayList<RectArc> rectArcList){
		if(filePair.hasInitialized) {
			double rax1, ray1, rax2, ray2, rgb_r, rgb_g, rgb_b;
			String word;
			Color color;
			try {
				File f = filePair.property;

				SAXReader reader = new SAXReader();
				Document doc = reader.read(f);
				Element root = doc.getRootElement();
				Element foo;
				for (Iterator i = root.elementIterator("rectArc"); i.hasNext(); ) {
					foo = (Element) i.next();
					rax1 = width * (Double.parseDouble(foo.elementText("startX"))) + offsetX;
					rax2 = width * (Double.parseDouble(foo.elementText("endX"))) + offsetX;
					ray1 = height * (Double.parseDouble(foo.elementText("startY"))) + offsetY;
					ray2 = height * (Double.parseDouble(foo.elementText("endY"))) + offsetY;
					word = foo.elementText("OCR");
					rgb_r = Double.parseDouble(foo.elementText("RGB_R"));
					rgb_g = Double.parseDouble(foo.elementText("RGB_G"));
					rgb_b = Double.parseDouble(foo.elementText("RGB_B"));
					color = new Color(rgb_r, rgb_g, rgb_b, 1);
					RectArc ra = new RectArc(rax1, ray1, rax2, ray2, word, color);
					rectArcList.add(ra);
				}
			} catch (Exception e) {
				//e.printStackTrace();
			}
		}
	}

	public static void RectArcList2Xml(double width, double height, double offsetX, double offsetY, FilePair filePair, ArrayList<RectArc> rectArcList){
		try {
			// 1、创建document对象
			Document document = DocumentHelper.createDocument();
			// 2、创建根节点rectArcs
			Element rectArcs = document.addElement("rectArcs");
			// 3、向rectArcs节点添加version属性
			//rss.addAttribute("version", "2.0");
			// 4、生成子节点及子节点内容
			Iterator it = rectArcList.iterator();
			RectArc ra = null;
			while(it.hasNext()) {
				ra = (RectArc)it.next();
				if(ra.isRemoved)
					continue;
				Element rectArc = rectArcs.addElement("rectArc");
				Element startX = rectArc.addElement("startX");
				startX.setText("" + ((ra.X1 - offsetX) / width));
				Element startY = rectArc.addElement("startY");
				startY.setText("" + ((ra.Y1 - offsetY) / height));
				Element endX = rectArc.addElement("endX");
				endX.setText("" + ((ra.X2 - offsetX) / width));
				Element endY = rectArc.addElement("endY");
				endY.setText("" + ((ra.Y2 - offsetY) / height));
				Element OCR = rectArc.addElement("OCR");
				OCR.setText(ra.getWord());
				Element RGB_R = rectArc.addElement("RGB_R");
				RGB_R.setText("" + ra.color.getRed());
				Element RGB_G = rectArc.addElement("RGB_G");
				RGB_G.setText("" + ra.color.getGreen());
				Element RGB_B = rectArc.addElement("RGB_B");
				RGB_B.setText("" + ra.color.getBlue());
				System.out.println("保存成功");

			}

			// 5、设置生成xml的格式
			OutputFormat format = OutputFormat.createPrettyPrint();
			// 设置编码格式
			format.setEncoding("GB2312");


			// 6、生成xml文件
			File file = filePair.property;
			XMLWriter writer = new XMLWriter(new FileOutputStream(file), format);
			// 设置是否转义,默认使用转义字符
			writer.setEscapeText(false);
			writer.write(document);
			writer.close();
			filePair.hasInitialized = true;
			System.out.println("生成rss.xml成功");
		} catch (Exception e) {
			e.printStackTrace();
			System.out.println("生成rss.xml失败");
		}
	}
}
7. MarkSceneController.fxml
<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.scene.control.Accordion?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.ButtonBar?>
<?import javafx.scene.control.ColorPicker?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.ListView?>
<?import javafx.scene.control.Menu?>
<?import javafx.scene.control.MenuBar?>
<?import javafx.scene.control.MenuItem?>
<?import javafx.scene.control.RadioButton?>
<?import javafx.scene.control.Slider?>
<?import javafx.scene.control.SplitPane?>
<?import javafx.scene.control.TextArea?>
<?import javafx.scene.control.TitledPane?>
<?import javafx.scene.control.ToggleGroup?>
<?import javafx.scene.image.ImageView?>
<?import javafx.scene.layout.AnchorPane?>
<?import javafx.scene.layout.BorderPane?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.paint.Color?>
<?import javafx.scene.shape.Rectangle?>
<?import javafx.scene.text.Font?>

<VBox fx:id="rootVBox" maxHeight="1080.0" maxWidth="1920.0" minHeight="850.0" minWidth="1500.0" prefHeight="850.0" prefWidth="1500.0" xmlns="http://javafx.com/javafx/17" xmlns:fx="http://javafx.com/fxml/1" fx:controller="org.controllers.MarkSceneController">
   <children>
      <MenuBar fx:id="myMenuBar">
        <menus>
          <Menu mnemonicParsing="false" text="文件">
            <items>
              <MenuItem mnemonicParsing="false" onAction="#OpenDirFunc" text="打开目录" />
                  <MenuItem mnemonicParsing="false" onAction="#SaveFunc" text="保存" />
            </items>
          </Menu>
          <Menu mnemonicParsing="false" text="编辑">
            <items>
              <MenuItem mnemonicParsing="false" text="Delete" />
            </items>
          </Menu>
          <Menu mnemonicParsing="false" text="帮助">
            <items>
              <MenuItem mnemonicParsing="false" text="About" />
            </items>
          </Menu>
        </menus>
      </MenuBar>
      <HBox fx:id="aboveHBox" prefHeight="100.0" prefWidth="200.0" VBox.vgrow="ALWAYS">
         <children>
            <VBox fx:id="leftVbox" prefWidth="350.0">
               <children>
                  <SplitPane minHeight="200.0" minWidth="350.4" prefHeight="200.0" prefWidth="350.0" VBox.vgrow="NEVER">
                     <items>
                      <AnchorPane fx:id="zoomerAnchorPane" minHeight="200.0" minWidth="350.4" prefHeight="200.0" prefWidth="350.0">
                           <children>
                              <ImageView fx:id="zoomerImageView" fitHeight="200.0" fitWidth="350.0" pickOnBounds="true" preserveRatio="true" />
                           </children>
                        </AnchorPane>
                     </items>
                  </SplitPane>
                  <Slider fx:id="zoomerSlider" blockIncrement="1.0" majorTickUnit="4.0" max="20.0" min="1.0" nodeOrientation="LEFT_TO_RIGHT" showTickLabels="true" showTickMarks="true" snapToTicks="true" style="-fx-background-color: orchid;" value="5.0" VBox.vgrow="NEVER" />
                  <Accordion VBox.vgrow="ALWAYS">
                    <panes>
                        <TitledPane animated="false" text="全部图片">
                           <content>
                              <BorderPane>
                                 <center>
                                    <ListView fx:id="allPicListView" BorderPane.alignment="CENTER" />
                                 </center>
                              </BorderPane>
                           </content>
                        </TitledPane>
                      <TitledPane animated="false" text="已标注图片">
                        <content>
                              <BorderPane>
                                 <center>
                                    <ListView fx:id="donePicListView" prefHeight="445.6" prefWidth="348.8" BorderPane.alignment="CENTER" />
                                 </center>
                              </BorderPane>
                        </content>
                      </TitledPane>
                      <TitledPane animated="false" text="未标注图片">
                        <content>
                              <BorderPane>
                                 <center>
                                    <ListView fx:id="undonePicListView" prefHeight="445.6" prefWidth="348.8" BorderPane.alignment="CENTER" />
                                 </center>
                              </BorderPane>
                        </content>
                      </TitledPane>
                    </panes>
                  </Accordion>
               </children>
            </VBox>
            <SplitPane fx:id="midSpiltPane" orientation="VERTICAL" prefHeight="200.0" prefWidth="160.0" HBox.hgrow="ALWAYS">
               <items>
                  <VBox prefHeight="200.0" prefWidth="100.0">
                     <children>
                        <SplitPane orientation="VERTICAL" prefHeight="200.0" prefWidth="160.0" VBox.vgrow="ALWAYS">
                          <items>
                            <AnchorPane fx:id="markAnchorPane" minHeight="0.0" minWidth="0.0" onMouseClicked="#mClicked" onMouseDragged="#mDragged" onMouseMoved="#mMoved" onMousePressed="#mPressed" onMouseReleased="#mRelease" prefHeight="100.0" prefWidth="160.0">
                                 <children>
                                    <ImageView fx:id="markImageView" fitHeight="1080.0" fitWidth="1920.0" pickOnBounds="true" preserveRatio="true" />
                                 </children>
                              </AnchorPane>
                          </items>
                        </SplitPane>
                        <SplitPane orientation="VERTICAL" prefHeight="200.0" prefWidth="160.0">
                          <items>
                              <BorderPane prefHeight="200.0" style="-fx-background-color: antiquewhite;">
                                 <center>
                                    <TextArea fx:id="wordTextArea" prefHeight="189.0" prefWidth="743.0" BorderPane.alignment="CENTER" />
                                 </center>
                                 <top>
                                    <Label fx:id="editArea" text="文本标注区" BorderPane.alignment="CENTER">
                                       <font>
                                          <Font size="18.0" />
                                       </font>
                                    </Label>
                                 </top>
                              </BorderPane>
                          </items>
                        </SplitPane>
                     </children>
                  </VBox>
               </items>
            </SplitPane>
            <BorderPane minWidth="400.0" prefHeight="200.0" prefWidth="400.0" style="-fx-background-color: antiquewhite;" HBox.hgrow="NEVER">
               <center>
                  <ListView fx:id="wordListView" prefHeight="714.0" prefWidth="400.0" BorderPane.alignment="CENTER" />
               </center>
               <bottom>
                  <SplitPane maxHeight="250.0" maxWidth="1.7976931348623157E308" minHeight="250.0" prefHeight="250.0" BorderPane.alignment="CENTER">
                     <items>
                        <BorderPane prefHeight="20.0" prefWidth="200.0" style="-fx-background-color: lightskyblue;">
                           <top>
                              <AnchorPane prefHeight="35.0" prefWidth="398.0" BorderPane.alignment="CENTER">
                                 <children>
                                    <HBox layoutY="5.0">
                                       <children>
                                          <Label alignment="CENTER" contentDisplay="TOP" prefHeight="26.0" prefWidth="118.0" text="选择框颜色" textOverrun="CLIP">
                                             <font>
                                                <Font size="14.0" />
                                             </font>
                                          </Label>
                                          <ColorPicker fx:id="colorPicker" onAction="#ColorSetFunc" prefHeight="26.0" prefWidth="230.0">
                                             <value>
                                                <Color />
                                             </value>
                                          </ColorPicker>
                                       </children>
                                    </HBox>
                                 </children>
                              </AnchorPane>
                           </top>
                           <center>
                              <AnchorPane prefHeight="214.0" prefWidth="417.0" BorderPane.alignment="CENTER">
                                 <children>
                                    <Rectangle arcHeight="5.0" arcWidth="5.0" fill="#0a71f70c" height="89.0" layoutX="141.0" layoutY="53.0" stroke="BLACK" strokeType="INSIDE" width="176.0" />
                                    <Button layoutX="181.0" layoutY="58.0" mnemonicParsing="false" onAction="#UDFunc" prefHeight="23.0" prefWidth="100.0" text="▾" />
                                    <Button layoutX="181.0" layoutY="26.0" mnemonicParsing="false" onAction="#UUFunc" prefHeight="23.0" prefWidth="100.0" text="▴" />
                                    <Button layoutX="148.0" layoutY="64.0" mnemonicParsing="false" onAction="#LRFunc" prefHeight="70.0" prefWidth="23.0" text="▸" />
                                    <Button layoutX="322.0" layoutY="64.0" mnemonicParsing="false" onAction="#RRFunc" prefHeight="70.0" prefWidth="23.0" text="▸" />
                                    <Button layoutX="181.0" layoutY="114.0" mnemonicParsing="false" onAction="#DUFunc" prefHeight="23.0" prefWidth="100.0" text="▴" />
                                    <Button layoutX="181.0" layoutY="146.0" mnemonicParsing="false" onAction="#DDFunc" prefHeight="23.0" prefWidth="100.0" text="▾" />
                                    <Button layoutX="113.0" layoutY="64.0" mnemonicParsing="false" onAction="#LLFunc" prefHeight="70.0" prefWidth="23.0" text="◂" />
                                    <Button layoutX="288.0" layoutY="64.0" mnemonicParsing="false" onAction="#RLFunc" prefHeight="70.0" prefWidth="23.0" text="◂" />
                                    <Label layoutX="24.0" layoutY="15.0" text="选择框微调">
                                       <font>
                                          <Font size="14.0" />
                                       </font>
                                    </Label>
                                    <Slider fx:id="senseSlider" layoutX="24.0" layoutY="169.0" majorTickUnit="2.0" max="8.0" minorTickCount="1" showTickLabels="true" showTickMarks="true" snapToTicks="true" value="4.0" />
                                    <Label fx:id="labelSence" layoutX="28.0" layoutY="150.0" text="灵敏度">
                                       <font>
                                          <Font size="14.0" />
                                       </font>
                                    </Label>
                                    <RadioButton fx:id="leftModeRadio" layoutX="89.0" layoutY="90.0" mnemonicParsing="false">
                                       <toggleGroup>
                                          <ToggleGroup fx:id="DTG" />
                                       </toggleGroup>
                                    </RadioButton>
                                    <RadioButton fx:id="upModeRadio" layoutX="223.0" layoutY="7.0" mnemonicParsing="false" selected="true" toggleGroup="$DTG" />
                                    <RadioButton fx:id="downModeRadio" layoutX="224.0" layoutY="175.0" mnemonicParsing="false" toggleGroup="$DTG" />
                                    <RadioButton fx:id="rightModeRadio" layoutX="354.0" layoutY="92.0" mnemonicParsing="false" toggleGroup="$DTG" />
                                 </children>
                              </AnchorPane>
                           </center>
                        </BorderPane>
                     </items>
                  </SplitPane>
               </bottom>
               <top>
                  <Label fx:id="drawInIndex" text="标注目录" BorderPane.alignment="CENTER">
                     <font>
                        <Font size="18.0" />
                     </font>
                  </Label>
               </top>
            </BorderPane>
         </children>
      </HBox>
      <SplitPane prefHeight="40.0" prefWidth="200.0" style="-fx-background-color: lemonchiffon;" VBox.vgrow="NEVER">
         <items>
            <ButtonBar prefHeight="40.0" prefWidth="200.0" scaleX="0.9">
              <buttons>
                  <RadioButton fx:id="rectModeRadio" mnemonicParsing="false" selected="true" text="框选模式">
                     <toggleGroup>
                        <ToggleGroup fx:id="TG" />
                     </toggleGroup>
                  </RadioButton>
                  <RadioButton fx:id="editModeRadio" mnemonicParsing="false" text="编辑模式" toggleGroup="$TG" />
                  <RadioButton fx:id="viewModeRadio" mnemonicParsing="false" text="查看模式" toggleGroup="$TG" />
                  <RadioButton fx:id="selectModeRadio" mnemonicParsing="false" text="选择模式" toggleGroup="$TG" />
                  <Button fx:id="editButton" mnemonicParsing="false" onAction="#EditFunc" style="-fx-border-color: gold; -fx-border-width: 4; -fx-border-radius: 12; -fx-background-radius: 16; -fx-background-color: wheat; -fx-font-weight: bold;" text="编辑" />
                  <Button fx:id="finishButton" mnemonicParsing="false" onAction="#FinishFunc" style="-fx-border-color: gold; -fx-border-width: 4; -fx-border-radius: 12; -fx-background-radius: 16; -fx-background-color: wheat; -fx-font-weight: bold;" text="完成" />
                <Button mnemonicParsing="false" onAction="#ClearFunc" style="-fx-border-color: gold; -fx-border-width: 4; -fx-border-radius: 12; -fx-background-radius: 16; -fx-background-color: wheat; -fx-font-weight: bold;" text="清空" />
                  <Button mnemonicParsing="false" onAction="#DeleteFunc" style="-fx-border-color: gold; -fx-border-width: 4; -fx-border-radius: 12; -fx-background-radius: 16; -fx-background-color: wheat; -fx-font-weight: bold;" text="删除" />
              </buttons>
            </ButtonBar>
         </items>
      </SplitPane>
      <BorderPane minHeight="20.0" prefHeight="20.0" prefWidth="200.0" VBox.vgrow="NEVER">
         <right>
            <Label fx:id="conditionLabel" alignment="CENTER_RIGHT" prefHeight="20.0" prefWidth="229.0" scaleX="0.9" text="正在准备" BorderPane.alignment="CENTER">
               <font>
                  <Font size="13.0" />
               </font></Label>
         </right>
      </BorderPane>
   </children>
</VBox>

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

<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.PasswordField?>
<?import javafx.scene.control.TextField?>
<?import javafx.scene.layout.AnchorPane?>
<?import javafx.scene.text.Font?>

<AnchorPane prefHeight="400.0" prefWidth="600.0" xmlns="http://javafx.com/javafx/17" xmlns:fx="http://javafx.com/fxml/1" fx:controller="org.controllers.PrimarySceneController">
   <children>
      <PasswordField fx:id="passwordTextField" layoutX="242.0" layoutY="206.0" prefHeight="23.0" prefWidth="170.0" />
      <TextField fx:id="accountTextField" layoutX="242.0" layoutY="172.0" prefHeight="23.0" prefWidth="170.0" />
      <Label layoutX="188.0" layoutY="174.0" prefHeight="19.0" prefWidth="53.0" text="账户:">
         <font>
            <Font size="14.0" />
         </font>
      </Label>
      <Label layoutX="188.0" layoutY="208.0" prefHeight="19.0" prefWidth="53.0" text="密码:">
         <font>
            <Font size="14.0" />
         </font>
      </Label>
      <Button fx:id="loginButton" layoutX="252.0" layoutY="251.0" mnemonicParsing="false" onAction="#loginButtonFunc" prefHeight="23.0" prefWidth="95.0" text="登  录">
         <font>
            <Font size="15.0" />
         </font>
      </Button>
      <Button fx:id="createAccountButton" layoutX="490.0" layoutY="326.0" mnemonicParsing="false" text="创建账户" />
      <Button fx:id="ForgetButton" layoutX="490.0" layoutY="356.0" mnemonicParsing="false" text="忘记密码" />
      <Label fx:id="conditionLabel" layoutX="192.0" layoutY="298.0" prefHeight="15.0" prefWidth="216.0" />
   </children>
</AnchorPane>
9. pom.xml
<?xml version="1.0" encoding="UTF-8"?>

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>org.example</groupId>
  <artifactId>OCR_Marker(with JavaFX)</artifactId>
  <version>2.0.1-SNAPSHOT</version>

  <name>OCR_Marker(with JavaFX)</name>
  <!-- FIXME change it to the project's website -->
  <url>http://www.example.com</url>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven.compiler.source>1.7</maven.compiler.source>
    <maven.compiler.target>1.7</maven.compiler.target>
  </properties>

  <dependencies>
    <dependency>
      <groupId>com.jfoenix</groupId>
      <artifactId>jfoenix</artifactId>
      <version>8.0.8</version>
    </dependency>
    <!-- 字体图标 -->
    <dependency>
      <groupId>de.jensd</groupId>
      <artifactId>fontawesomefx</artifactId>
      <version>8.9</version>
    </dependency>

    <dependency>
      <groupId>com.alibaba</groupId>
      <artifactId>fastjson</artifactId>
      <version>1.2.58</version>
    </dependency>

    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.11</version>
      <scope>test</scope>
    </dependency>

    <!-- https://mvnrepository.com/artifact/org.dom4j/dom4j -->
    <dependency>
      <groupId>org.dom4j</groupId>
      <artifactId>dom4j</artifactId>
      <version>2.0.0</version>
    </dependency>
  </dependencies>

  <build>
    <pluginManagement><!-- lock down plugins versions to avoid using Maven defaults (may be moved to parent pom) -->
      <plugins>
        <!-- clean lifecycle, see https://maven.apache.org/ref/current/maven-core/lifecycles.html#clean_Lifecycle -->
        <plugin>
          <artifactId>maven-clean-plugin</artifactId>
          <version>3.1.0</version>
        </plugin>
        <!-- default lifecycle, jar packaging: see https://maven.apache.org/ref/current/maven-core/default-bindings.html#Plugin_bindings_for_jar_packaging -->
        <plugin>
          <artifactId>maven-resources-plugin</artifactId>
          <version>3.0.2</version>
        </plugin>
        <plugin>
          <artifactId>maven-compiler-plugin</artifactId>
          <version>3.8.0</version>
        </plugin>
        <plugin>
          <artifactId>maven-surefire-plugin</artifactId>
          <version>2.22.1</version>
        </plugin>
        <plugin>
          <artifactId>maven-jar-plugin</artifactId>
          <version>3.0.2</version>
        </plugin>
        <plugin>
          <artifactId>maven-install-plugin</artifactId>
          <version>2.5.2</version>
        </plugin>
        <plugin>
          <artifactId>maven-deploy-plugin</artifactId>
          <version>2.8.2</version>
        </plugin>
        <!-- site lifecycle, see https://maven.apache.org/ref/current/maven-core/lifecycles.html#site_Lifecycle -->
        <plugin>
          <artifactId>maven-site-plugin</artifactId>
          <version>3.7.1</version>
        </plugin>
        <plugin>
          <artifactId>maven-project-info-reports-plugin</artifactId>
          <version>3.0.0</version>
        </plugin>
      </plugins>
    </pluginManagement>

    <resources>
    <!--注意!如果任何工程路径下的文件在编译过程中找不到,请检查此处resources的问题-->
    <!--请确保resources正确包含该文件,正常时target中会有该文件的复制品-->
      <resource>
        <directory>src/main/resources</directory>
        <includes>
          <include>**/*.fxml</include>
        </includes>
        <filtering>true</filtering>
      </resource>

      <resource>
        <directory>src/main/java</directory>
        <includes>
              <include>**/*.fxml</include>
        </includes>
        <filtering>true</filtering>
      </resource>
    </resources>

  </build>
</project>

五、调试过程中出现的问题及相应解决办法

(一)关于窗口变化对于界面影响的问题

1. 问题描述

我们在软件设计时希望,此软件界面可以根据客户的需求做以必要的灵活形变,故这就要求关于窗口resize的问题需要被纳入考虑。
这包含以下问题:

(1)窗口变化时,界面组件的拉伸问题

(2)窗口变化时,图片的尺寸及位置的自适应问题

2. 解决方法

对于问题(1),我们的设计工具Java FX有比较完善的解决对策。
由于界面设计中最经常被使用的界面布局组件为横向和纵向排列组件(HBoxVBox),且本软件的设计主要也应用了这些工具,故了解此类组件对于窗口拉伸的态度成为解决问题的关键。

研究中发现,VBox中的组件会有VBox所赋予的一个继承属性String VBox.vgrow,默认为"always",此时若在纵向拉伸此界面,VBox会更倾向于让此子组件拉伸;而如果此变量被赋值为"never",则拉伸时除尺寸上非迫不得已,否则不会拉伸此子组件。

<HBox fx:id="aboveHBox" prefHeight="100.0" prefWidth="200.0" VBox.vgrow="ALWAYS">
	<!--此为父组件VBox的一个子组件HBox,以此为例-->

由以上的结论,对于问题(2),根据软件设计的需求,我们将所有的变形任务交付给中心的图片显示区域,并且通过额外的编程手段使图片显示区域在拖拽窗口变形时显得合适合理。实现此效果的代码被封装在函数fitImage()中:

	 //从xml装载或调整当前图片,以适应当前窗口状态,同时载入矩形框列表
    public void fitImage(){
        if(markImage != null){
            //图片适应策略:
        	//	当图片为较宽图片时:缩减高度,适应宽度,两边顶天,上下留白
        	//	当图片为较高图片时:缩减宽度,适应高度,头顶顶天,两边留白
            ...
        }
    }

且此函数会在窗口尺寸监听器resizeChangeListener监听到拖拽窗口变形时被调用:

    private class resizeChangeListener implements ChangeListener<Object> {
        @Override
        public void changed(ObservableValue<?> observable, Object oldValue, Object newValue) {
            ...
            fitImage();
        }
    }

由此,本问题得到良好的解决。

(二)关于使用逻辑混乱的问题

1. 问题描述

在软件设计取得基础功能完成的阶段性进展时,经小组成员讨论发现,程序在使用上存在逻辑的混乱,这样的混乱导致后续开发扩展功能相对困难,且对于用户操作不甚友好。

2. 解决方案

通过功能拆分,将设计时已实现和即将实现的功能统一分为四个模式:
框选模式(r):框选的绘制和文字录入等;
编辑模式(e):矩形框的键鼠微调、鼠标拖动移动位置、矩形框颜色改变等;
查看模式(v):放大镜单独使用;
选择模式(s):从图形点击选择框。
这四个功能相互分离,并通过模式切换的单选按钮来切换,在切换的过程中仅需做好必要的数据隔离,则可以达到业务逻辑清晰明了,用户操作简单明确的效果。

(三)关于监听器过于敏感的问题

1. 问题描述

我们的软件中有一个列表切换的监听器wordListItemChangeListener,其判定是列表的条目发生切换,在写其他功能函数的过程中,有时候会发现不时的就调用了这个监听器,因为选中列表条目后再点编辑、完成按钮,实际上也是条目从有到无的过程,而由于我们的监听器中有个整体刷新的函数,从而导致了写其他函数的过程可能会产生逻辑的混乱,比如写标注进去的那个“完成”按钮的函数finish时候,开始发现怎么写都写不进去,但是把监听器里的刷新函数注释掉后发现就能标注进去了,才发现是这个函数里有清空LIST的过程,从而找到了问题在这上面。

2. 解决方案

在写其他功能,如很多按键功能,诸如finishedit还有clear的功能函数的时候时候也尽量使用xml的读功能来进行页面显示状态的刷新,这样能提高与监听器函数的兼容性,这样就不会因为多线程的原因而导致读写列表时的逻辑混乱,最后我们通过这个思路也使软件出现的很多未知BUG消除了。

六、软件效果

(一)实现的功能及介绍

我们实现了要求中的基本功能,并自主设计了许多附加人性化功能,详见如下效果。

(二)操作指南和效果

1. 文件夹的选择
img
图6:功能-文件夹选择
2. 图片的选择
img
图7:功能-图片选择切换
3. 标注框的构造
img
图8:功能-创建标注框和录入文字
4. 标注框的调整
img
图9:功能-标注框的编辑调整
5. 放大镜功能
img
图10:功能-放大镜查看模式
6. 在图中选择框
img
图11:功能-图形化选择
7. 文本的标注与修改
img
图12:功能-文字标注的修改录入
8. 生成xml文件
img
图13:数据保存结果
9. xml文件保存标注信息
img
图14:xml数据存储格式示例
10. 在帮助页面中,可以跳转到在线的软件操作手册。

image

图15:帮助页面

这里是受到市面上软件的启发,在真实软件中对应的就是其官网。我们可以实时发布一些相关的动态,软件说明书,软件更新情况等,并支持用户反馈等模块。由于时间限制,我们用博客实现此功能。

(三)后期工作

在验收完成后,为了精益求精,我们对软件的界面配色进行了后期调整,使其更护眼、美观。并打包封装为可执行程序。

image

图16:界面风格调整

七、个人体会及建议

这次软件课设让我们小组完整的体验了软件开发的一个较为完整的流程。我们在过去的一个月里协同工作,用设计完整的标注软件为我们的课设划上一个满意的句号。

在此次软件课设进行之前,我只有独立开发简单软件的经历,曾经用一个月左右完成了Java的坦克大战项目,用两个月完成了Java面向网络的一对一局域网聊天室项目,这些项目都属于模块划分单一,功能集中,适合单人独自开发,且功能主题集中在后台运算逻辑。比如坦克大战的重点在于多线程的同步,聊天室的重点在于socket TCP连接的建立、通信、保持与停止。相比之下,本次软件课设的独特之处体现在:其一,软件的主题在于前端界面编程,业务逻辑集中在用户的图形化操作;其二,软件的功能复杂,但是功能之间的耦合相对松散,在一个扩展性良好的基础界面实现上,各种功能的添加与修改相对简单和便捷,由此,本软件可以被明确的划分出各个模块的实现任务,从而可以进行多人分工。

在软件设计中,当确定技术栈为Java语言和FX界面包后,我先独立实现了软件的界面基本实现和接口(约占整体逻辑的半数),此时已经实现了图片的加载、切换、选择,配置xml文件的读写,画框的录入与加载,文字框的单项选择等基本框架内容。经过三人短时间内的评估与改进,我们成功得到了snapshot 1.0.0版本,此版本在后续的开发中成为了附加高级功能的基础。

后续的附加功能,比如文本与画框的双向选择、框的编辑及键控、帮助菜单、模式隔离与切换等,由我们先独立分工实现,再进行集中调试完成,由此我们进行了十余次的版本回溯与迭代,在设计的中后阶段成功得到了snapshot 2.0.0,此版本在经历联合debug后产生了我们第一个成品发行版本release 1.0.0。随后的发行版本是在此版本上做了界面的美化,在功能上已经趋近完善。

所以相比于我此前开发软件的经历,此次的软件课设的开发过程给予我了几个重大的收获:

  • 完善的基本框架、明确的模块划分与清晰的责任分工可以明显的加快开发进程和减少每个成员开发软件的工作量,这在日后参与工作中会起到至关重要的作用,这也同样是团队开发软件的基础和前提;
  • 清晰的版本迭代可以明显的为后续更改提供备份和参考,并且我们可以通过更新日志来总览功能的实现情况,在必要时可以使用版本回溯来规避bug;
  • Java语言可以运用于很多工程上,相对python语言来说较为底层,程序逻辑管理清晰明了,但是代价是实现语句较长。本次课设使用了Java语言至少2000行代码,如果换成python实现,则效果上会打折扣,代码长度上会至少缩减一半。日后的软件学习与开发可以在此经历上进行语言选择。

总的来说,通过这次软件课程设计,我们对工程开发有了更加定量的认识,更从中学到了不少设计、开发和调试的方法,大大增强了我们的工程能力。最后我们还要感谢老师对我们的教导和帮助,感谢我们组员间的互相配合与支持,如果设计过程中还有不足和谬误之处,期待得到老师的批评指正。

posted @   钻石非永恒  阅读(229)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· AI 智能体引爆开源社区「GitHub 热点速览」
· C#/.NET/.NET Core技术前沿周刊 | 第 29 期(2025年3.1-3.9)
· 从HTTP原因短语缺失研究HTTP/2和HTTP/3的设计差异
点击右上角即可分享
微信分享提示