软件课设:OCR文字标注


注:项目为学生小组完成,如有引用请注明原文链接
源码请访问 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文件中,故我们因此可以将界面和业务逻辑完全分离。分别以界面和业务逻辑为基点,可以分为如图模块:

图1:程序模块分析

(三)关键技术难点

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

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

三、方案设计

(一)技术基础

1. 以maven构建项目,引入Java FX等包
    
      com.jfoenix
      jfoenix
      8.0.8
    

    
      org.dom4j
      dom4j
      2.0.0
    
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 rectArcList){...}

	public static void RectArcList2Xml(double width, double height, double offsetX, double offsetY, FilePair filePair, ArrayList 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

图2:Java FX运行流程

(三)主页面MarkSceneController

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

        //获得图片列表和配置文件列表(过滤掉子文件夹和其他文件类型)
        ArrayList picList = new ArrayList<>();
        ArrayList 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 {
        @Override
        public void changed(Object oldValue, Object newValue) {
            //除旧:
            //	1. 模式重置
            //	2. 保存已经做出的矩形框改变
            {...}

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

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

        @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. 程序总体函数调用图

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

图4:程序执行流程

四、程序源代码

(一)文件目录结构

图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 fileList;
    //定义三个文件名清单,用于左侧列表选择文件
    public static ArrayList allPicNameList, donePicNameList, undonePicNameList;
    //定义矩形清单,用于绘制当前所有矩形
    public ArrayList 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() {
                @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() {
                @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() {
                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() {
                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() {

                @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 picList = new ArrayList<>();
        ArrayList 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 {
        @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 {
        @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 {
        @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 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 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?>