日程功能模块

最后成果

JAVAFX里面没有封装日历控件,找了些项目源码做参照肝了一个,不过为了简化分析的过程,不会详细写其中业务逻辑。
总的来说,从建模带代码实现的功能完成了90%,日程列表和日历的通信没写,预期效果是如果有新增的日程,日历上相应的日历块会有 marked 的标记。这一块按照我的想法写代码会变得很乱,也是建模过程中没有认真考虑的点。
日程功能模块【从建模到代码实现】UML + JavaFX-LMLPHP
日程功能模块【从建模到代码实现】UML + JavaFX-LMLPHP

建模过程

需求

日程主要是帮助用户查看和管理日常事务,用户可以记录待办事件并且设置提醒时间,有助于管理时间和提高工作效率。
而作为成绩管理系统学生界面的一个子功能模块,除了帮助学生管理课程和学习任务,它还需要能自动导入课程考试信息,便于学生规划学习进度。

用例图及主要用例描述

  • 用例图
    从需求分离出对象为学生。把所有有意义的动宾短语先列出来【查看日常事务】,【管理日常事务】,【记录待办事件】,【设置提醒时间】,【自动导入课程考试信息】。这里是因为需求比较直观简单,一般还需要根据语义找隐藏的功能需求。
    进一步分析用例之间的关系,【管理日程事务】即对日程做删改,应该是在选定具体的事务后。【记录待办事件】即新建日程,包括【设置提醒时间】等信息设置。【自动导入课程信息】应该不是由学生完成的,学生只能查看,所以还有参与者 —— 考试管理系统。
    画出用例图如下
    心得:在画用例图时并没有花太多时间打磨,构建的快也修改的快。从需求快速提取用例,在写用例描述的时候还会再倒回来修改的
    日程功能模块【从建模到代码实现】UML + JavaFX-LMLPHP

  • 用例描述

活动图

日程功能模块【从建模到代码实现】UML + JavaFX-LMLPHP
日程功能模块【从建模到代码实现】UML + JavaFX-LMLPHP


较为倾向于从用例描述中抽象出类,老师发的资料中也写到,用例描述占据着皇后的位置,而三王一后中没有出现活动图。我在写完用例描述后对程序也已经有了轮廓。
日程功能模块【从建模到代码实现】UML + JavaFX-LMLPHP

类图 <重中之重>

学习博客【深入浅出UML类图】
从用例描述中,抽象出所有的类。我们先提取实体类,有日程类,以及填充日历的日期格类。边界类这里就是界面类,有日程主界面类,添加日程的界面类,两个界面类分别都有控制类实现相关的业务逻辑。界面类里面的部件主要是日历类,日程列表类,考试列表类。
日程功能模块【从建模到代码实现】UML + JavaFX-LMLPHP

先把实体类的属性写好,日程类我们很容易可以知道,有日程名称,开始和截止时间,提醒时间。用户在填写事件时,可能还有一些额外的信息需要提醒自己,那么就添加一个事件备注属性。
日期格类应该包含的是当前格子表示的日期,因为我们还可以直接看到这个格子是否有日程,应该是抽象为一个状态。这里我预备用布尔型 isMarked 来表示。(当时写的时候还加了Mark属性,作为如果存在日程的标记,多余了。
然后进一步思考类之间的关系,先看聚合关系,ScheduleList 和 ExamList 都是由 Schedule 聚合而来,但 ExamList 属于特殊的日程,它在这里只能查看不能修改。日期格和日历Calendar也是聚合关系,我的想法是按月显示,那关系就是一个日历中由35个日期格。
考试列表,日程列表和日历都属于主界面的部件,包括主界面在内的业务逻辑都由 ScheduleController 来处理。最后得到类图如下
日程功能模块【从建模到代码实现】UML + JavaFX-LMLPHP

代码实现

用starUML的正向工程工具根据上面画好的类图导出所有的代码。然后用 sceneBuilder 开始页面布局。

主界面我直接用的做平时成绩管理系统的界面稍加改动,已经有基本布局和css渲染。添加新日程界面根据用例描述,就是有对日程基本信息的编辑,然后确认取消按键。
下面是静态初始界面,还没有实现任何功能只是个UI。
日程功能模块【从建模到代码实现】UML + JavaFX-LMLPHP
日程功能模块【从建模到代码实现】UML + JavaFX-LMLPHP

编写界面类,主界面继承 Application 类。添加日程是由主界面按钮触发弹出的,暂时不写。
public class ScheduleStage extends Application {
    public static void main(String[] args) {
        launch(args);
    }
    @Override
    public void start(Stage stage) throws Exception {
        Parent root = FXMLLoader.load(getClass().getResource("resource/ScheduleStage.fxml"));
        stage.setTitle("Student Schedule");
        stage.setScene(new Scene(root, 1080, 720));
        stage.setResizable(false);
        stage.centerOnScreen();
        stage.show();
    }
}
开始编写代码,正向工程导入后已经有了属性,实体类只需要编写setter和getter方法以及构造器

日程功能模块【从建模到代码实现】UML + JavaFX-LMLPHP

日历是用GirdPane写的,每一个日期格都继承AnchorPane,便于在内部进行布局
日程功能模块【从建模到代码实现】UML + JavaFX-LMLPHP

两个 List 部件类

ScheduleList 有 Schedule 的聚合,初始化我们从数据库导入数据(因为用到数据库的地方较少,就不分离出来的(绝不是懒:/,删除和修改的方法这里暂时不写。
ExamList 直接从数据库初始化数据之后不再会变化,所以它包含的应该是 final static 的 Schedule 数组,与上面类似就已经完成了。
日程功能模块【从建模到代码实现】UML + JavaFX-LMLPHP

日历类 Calendar 和两个控件按钮的行为

日历类其中涉及细节较多,这里把它当作已经封装好的日历控件FXCalendar。根据类图,我们要完成的时在按上月和下月的按钮时,日历要做出变化。
两个按钮触发的行为实现代码

    @FXML
    void onButtonLastMonthClicked(ActionEvent event){
        LocalDateTime now = LocalDateTime.of(Integer.parseInt(labelYear.getText()), Month.valueOf(labelMonth.getText()), 1, 0, 0, 0);
        now = now.minusMonths(1);
        labelYear.setText(String.valueOf(now.getYear()));
        labelMonth.setText(String.valueOf(now.getMonth()));
        changeCalendar(now.getYear(), String.valueOf(labelMonth.getText()));
    }

    @FXML
    void onButtonNextMonthClicked(ActionEvent event){
        LocalDateTime now = LocalDateTime.of(Integer.parseInt(labelYear.getText()), Month.valueOf(labelMonth.getText()), 1, 0, 0, 0);
        now = now.plusMonths(1);
        labelYear.setText(String.valueOf(now.getYear()));
        labelMonth.setText(String.valueOf(now.getMonth()));
        changeCalendar(now.getYear(), String.valueOf(labelMonth.getText()));
    }

可以看到上面调用了 changeCalendar() 方法来实现日历的变化,下面是 changeCalendar() 代码实现

    private void changeCalendar(int year, String month){
        DateTimeFormatter formatter = new DateTimeFormatterBuilder()
                .parseCaseInsensitive()
                .appendPattern("yyyy MMMM")
                .toFormatter(Locale.ENGLISH);
         populateDate(YearMonth.parse(year + " " + month, formatter));
        selectedMonth = month;
    }
两个界面的交互和添加日程界面完善

在主界面按下添加日程按键是触发新增日程界面信息,每次都会产生一个新的界面,一个主界面可以有多个添加日程界面。

   /*
    * 添加新的日程
    * */
    @FXML
    void onButtonAddNewClicked(ActionEvent event) {
        Stage addNewStage = new Stage();
        Parent root = null;
        try {
            root = FXMLLoader.load(getClass().getResource("resource/AddNewSchedule.fxml"));
        } catch (IOException e) {
            e.printStackTrace();
        }
        addNewStage.initStyle(StageStyle.UNDECORATED);
        addNewStage.setTitle("Student");
        addNewStage.setScene(new Scene(root, 600, 450));
        addNewStage.centerOnScreen();
        addNewStage.show();
    }

我们从类图中得知,主要有填入事件信息,是否需要提醒,确认添加和取消操作。(这里在编写的时候就发现,类图的不足之处,打开提醒应该是由控制类来完成。
填入事件信息是在界面中完成的
确认添加事件 getNewScheduleButton()

    /*
    * 将新增的事件放入Schedule
    * */
    @FXML
    void getNewScheduleButton(ActionEvent event) {
        String name = itemName.getText();
        String comment = itemRemark.getText();
        DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");
        String btime = beginDate.getValue()  + " " + beginTime.getValue();
        String etime = endDate.getValue() + " " + endTime.getValue();
        newSchedule = new Schedule(name, comment, LocalDateTime.parse(btime, dtf), LocalDateTime.parse(etime, dtf));
        if(isRemindTogButton.isSelected()) {
            newSchedule.setRemind(true);
            newSchedule.setReminderTime(LocalDateTime.parse(remindDate.getValue() + " " + remindTime.getValue(), dtf));
        }

        addNew(newSchedule);
        closeAddNewStage(event);
    }

确认和取消都会触发窗口的关闭,而窗口是在主界面控制类生成的,在这里需要获取当前按钮所在窗口来关闭

    @FXML
    void closeAddNewStage(ActionEvent event) {
        Stage stage = (Stage) closeButton.getScene().getWindow();
        stage.close();
    }

是否提醒

    /*
    * 是否打开提醒
    * */
    private JFXTimePicker remindTime = new JFXTimePicker();
    private JFXDatePicker remindDate = new JFXDatePicker();
    @FXML
    void isReminding(ActionEvent event) {
        boolean isSelected = isRemindTogButton.isSelected();
        if(isSelected) {
            remindDate.setDefaultColor(Paint.valueOf("#0442bf"));
            remindTime.setDefaultColor(Paint.valueOf("#0442bf"));
            remindDate.setPrefSize(153, 23);
            remindTime.setPrefSize(153, 23);
            remindTime.setTranslateY(15);
            remindDate.setTranslateY(15);
            remindHBox.getChildren().addAll(remindDate, remindTime);
        }
        else {
            remindHBox.getChildren().remove(1, 3);
        }
    }

到这里,添加日程界面和控制类都完成了

最后要做的是删除选中日程和查看选中日程deleteSchedule(in schedule:Schedule),showScheduleDetail(schedule:Schedule)

这里有个十分迷惑的小bug,虽然处理了。但还是不知道为什么,希望有大佬解惑
ClickedID是在监听日程列表中被鼠标选中的事件编号。

    /*
     * 删除选中日程
     * */
    @FXML
    void deleteButtonOnAction(ActionEvent event) {
        System.out.println(clickedId);
        if(clickedId == -1) return;
        observableList.remove(clickedId);
        // 十分神奇的bug!!!在observableList移除最后一个元素后,clickedId自动从0变成-1,故加下面这句
        if(clickedId == -1) clickedId++;
        System.out.println(clickedId);
        delete(clickedId);
    }

查看选中日程时,生成对话框来提示选中日程的所有细节。

/*
     * 显示选择日程细节
     * */
    @FXML
    void showDetailButtonOnAction(ActionEvent event) {
        if(clickedId == -1) return;
        Schedule schedule = list.get(clickedId);
        JFXAlert alert = new JFXAlert(showDetailButton.getScene().getWindow());
        alert.initModality(Modality.APPLICATION_MODAL);
        alert.setOverlayClose(false);
        JFXDialogLayout layout = new JFXDialogLayout();
        Label label = new Label(schedule.getItemName());
        label.setFont(new Font("Cambria", 32));
        layout.setHeading(label);
        Label newContent = new Label("备注: " + schedule.getItemRemark()
                + "\n开始时间: " + schedule.getStartDate()
                + "\n结束时间: " + schedule.getEndDate()
                + "\n提醒时间: " + schedule.getReminderTime());
        newContent.setFont(new Font("Cambria", 16));
        layout.setBody(newContent);
        JFXButton closeButton = new JFXButton("确 认");
        closeButton.setPrefSize(150,55);
        closeButton.setFont(new Font("Cambria", 16));
        closeButton.getStyleClass().add("dialog-accept");
        closeButton.setOnAction(e -> alert.hideWithAnimation());
        layout.setActions(closeButton);
        alert.setContent(layout);
        alert.show();
    }

list 做出的相应操作
日程功能模块【从建模到代码实现】UML + JavaFX-LMLPHP

总结心得

从类图到代码仍旧花了不少时间在不断思考如何组织和实现,一度想不管结构全部累在一起,这里类图起了一个很大的规范作用。它在设计阶段,规范好整个框架,让我先对业务流程有了大致的轮廓。如果感觉有错误,可以在类图阶段就修改,而不是等到实现时修改代码,减小犯错成本。

其实代码实现后,我还返回去修改了类图

一个是命名规范,当时设计类图命名比较草率,导致在代码累积下来后不能见名知意,所以重构了代码并且修改类图中类和方法的命名;
第二个是方法的参数和返回值,类图设计时我对每个方法都写了形参和返回值,但具体实现时大概率会发生变化,比如删除日程那只需要传递日程ID,而不是把Schedule传过去;

类图上的时间安排

其出现两个问题,其一是在根据类图实现代码时发现有些细节没有考虑到,需要在实现时再花时间来设计。其二是根据类图的设计无从下手,有我Java功底尚浅的原因,但也可能是因为设计的不合理。
类图设计的快,可能会有细节被忽略;类图设计的慢,不断打磨精细,如果后续要修改,可能因为投入了较多的时间成本不想修改。
这次从建模到实现,收获很大,真的感受到一个好的建模可以让整个功能实现更加高效。我的建模可能还很不规范,之后还是多实践和总结!

11-28 15:17