在这些课堂讲稿中,我将带你们完成一个交互式日程安排程序的构建,该程序可以安排酒店会议室的活动。这个例子将给我们一个学习JavaFX中的菜单和对话框的机会。我还将介绍一些新的组件。
下面是完成的Reservation Manager程序的界面:
赢博体育程序的接口由三个部分组成。在窗口的最上方有一个菜单栏,其中包含“文件”和“预订”菜单。在菜单栏下面,我们可以看到一个DatePicker控件,它允许用户选择日期,还有一个ChoiceBox控件,它允许用户在酒店可用的会议室之间切换。底部部分是一个ListView控件,用于显示给定房间和日期的赢博体育当前计划事件。事件以一种有点神秘的符号显示,显示每个事件的预订号、组大小、开始时间和结束时间。
下面是关于这个赢博体育程序要解决的问题的一些附加细节。
当地一家旅馆雇用你编写一个程序来管理他们的会议室预订。酒店拥有一套会议室,其名称和容量如下:
房间的名字 | 能力 |
---|---|
大宴会厅 | 50 |
奥奈达市的房间 | 30 |
密歇根的房间 | 20 |
曼德特的房间 | 20 |
朝阳的房间 | 12 |
关于这些会议室房间预订的信息存储在一个文本文件中。文本文件的每一行都以这种格式包含关于单个预订的信息:
<预订号> <客户> <群组大小> <日> <开始时间> <持续时间>
事件的开始时间为整小时,采用24小时制。例如,从下午3点开始的事件将被列为从第15小时开始。下面是对这些行项目的更详细的描述。
项 | 描述 |
---|---|
预订号码 | 分配给预订的唯一标识号-整数 |
客户 | 分配给客户的唯一识别码-整数 |
组的大小 | 组内人数-整数 |
一天 | 预订日期 |
开始时间 | 事件的开始时间,24小时格式-整型 |
持续时间 | 以小时为单位的事件持续时间-整数 |
当程序启动时,它将从文本文件中读取现有预订的列表。然后,赢博体育程序将允许用户按日期和房间查看这些预订,并进行新的预订。该赢博体育程序还允许用户将更新后的预订列表保存回文本文件。
由于我们将要构建的赢博体育程序必须跟踪大量数据,因此我们首先需要创建一组适当的类来记录这些数据。我们将需要三个数据类:一个表示会议室集合的Hotel类、一个表示会议室的Room类和一个表示预定在会议室中的活动的预订的Reservation类。
Reservation类表示要在酒店的一个房间中安排的事件。这个类有一个相当明显的结构:
包edu.lawrence.hotel;进口java.io.PrintWriter;进口java.time.LocalDate;进口java.util.Scanner;public class Reservation实现Comparable<Reservation> {private int eventNumber;private int customerNumber;private LocalDate日期;private int starh;private int endHour;private int groupSize;public Reservation() {} public Reservation(int eventNumber,int customerNumber, LocalDate,int startHour,int endHour,int groupSize) {eventNumber = eventNumber;这一点。customerNumber = customerNumber;这一点。Day =日;这一点。startHour = startHour;这一点。endHour = endHour;这一点。groupSize = groupSize;}公共int getGroupSize(){返回groupSize;}公共LocalDate getDate(){返回日期;}公共无效readFrom(扫描器输入){eventNumber = input. nextint ();customerNumber = input.nextInt();输入。useDelimiter(“|”);int year = input.next ();int month = input.nextInt();int dayOfMonth = input.nextInt();input.reset ();day = LocalDate。of(年,月,日ofmonth);startHour = input.nextInt();endHour = input.nextInt();groupSize = input.nextInt();}公共无效writeTo(printwwriter输出){output.print(eventNumber);输出。打印(" ");output.print (customerNumber);输出。打印(" ");output.print (day.getYear ());output.print(“/”);output.print (day.getMonthValue ());output.print(“/”);output.print (day.getDayOfMonth ());输出。打印(" ");output.print (startHour);输出。打印(" ");output.print (endHour);输出。打印(" ");output.println (groupSize);}公共String toString(){返回eventNumber + "(" + groupSize + "):" + startHour + "-" + endHour;} @覆盖公共int compareTo(预留0){if(day.isBefore(.day))返回-1;if(day.isAfter(.day))返回1;if(endHour <= 0 . starthour) return -1;如果(startHour >= . endhour)返回1;返回0;}}
这里有几件事值得注意。每个Reservation都有一个day成员变量,用于表示计划活动的日期。为此,我使用java.time.LocalDate数据类型,以便与JavaFX DatePicker组件配合得很好,后者使用相同的数据类型来表示用户选择的日期。要注意的第二件事是,由于我们将在文本文件中存储reservation,因此需要一对方法从该文本文件中读写reservation。第三,因为我们想要在GUI的ListView中显示Reservation,所以Reservation类需要一个toString方法,ListView可以用它来构造一个String来显示Reservation。最后,由于我们希望按照开始时间增加的顺序显示预订,因此需要一个compareTo方法来促进预订的最终排序。注意,compareTo有一个稍微不同寻常的结构:通常情况下,只有当比较的两个reservation完全相同时,compareTo才应该返回0。相反,如果两个reservation以任何方式重叠,这个版本的compareTo返回0。这是有意为之,我将最终解释为什么这是有帮助的。
预订对象分配给各个房间。下面是Room类的代码:
包edu.lawrence.hotel;进口java.io.PrintWriter;进口java.util.Scanner;进口javafx.collections.FXCollections;进口javafx.collections.ObservableList;公共类房间{私有字符串名称;私人int能力;private ObservableList<Reservation> Reservation;public Room() {reservations = FXCollections.observableArrayList();}公共字符串getName(){返回名称;}公共布尔allowsReservation(Reservation newReservation) {if(newReservation. getgroupsize() >容量)返回false;for(Reservation e: reservations) {if(e.p areto (newReservation) == 0)返回false;}返回true;}公共无效地址保留(预订newReservation){预订.添加(新预订);FXCollections.sort(预订);}公共无效removeReservation(保留删除){保留删除(删除);}公共无效readFrom(扫描器输入){//从文件名读取房间详细信息和预订= input.next() + input. nextline ();capacity = input.nextInt();int howMany = input.nextInt();for(int n = 0;n < howMany;n++) {Reservation nexreservation = new Reservation();nextReservation.readFrom(输入);reservations.add (nextReservation);} FXCollections.sort(预订);}公共无效writeTo(printwwriter输出){output.println(名称);output.println(能力);int howMany = reservations.size();output.println(多少);for(int n = 0;n < howMany;n++) reservation .get(n).writeTo(输出);}公共ObservableList<预订> getReservations(){返回预订;}}
每个房间都有名称和容量,以及该房间预定的预订列表。由于我们最终将在ListView中显示预订列表,因此我们希望首先将赢博体育房间的预订存储在一个可观察列表中。
因为我们总是希望按照时间排序来维护我们的Reservation,所以我们需要在读取完成后以及每当添加新的Reservation时对列表进行排序。这是通过调用
FXCollections.sort(预订);
这相当于集合。为属性列表排序。(回想一下,我们已经为Reservation类配备了一个compareTo方法,因此排序方法可以完成它的工作。)
最后,请注意allowsReservation方法,该方法确定是否可以将预期的新Reservation安排到此Room中。由于我们之前在Reservation类中设置了compareTo,以便在重叠的情况下返回0,因此我们在这里所要做的就是搜索现有的事件,以查看是否有任何事件与新的Reservation在时间上重叠。如果其中任何一个错误,我们可以停止并立即返回false。
我们需要的最后一个数据类是Hotel类,它作为各种房间的容器:
包edu.lawrence.hotel;进口java.io.PrintWriter;进口java.util.ArrayList;进口java.util.Scanner;public类Hotel {private ArrayList<Room> rooms;public Hotel() {rooms = new ArrayList<Room>();}公共void readFrom(扫描器输入){//从输入文件中读取五个房间(int n = 0;n < 5;n++) {Room newRoom = newRoom ();newRoom.readFrom(输入);rooms.add (newRoom);}}公共无效writeTo(printwwriter输出){for(房间r:房间){r.writeTo(输出);}}公共房间getRoom(String forName) {for(房间r:房间){if(r.t getname ().equalsIgnoreCase(forName))返回r;}返回null;}}
这里唯一值得评论的是最后一个方法,它允许我们按名称搜索房间。我们将需要这个,因为GUI包含一个列出房间名称的ChoiceBox。当用户从那个ChoiceBox中进行选择时,我们将读取用户选择的房间名称,并使用它在Hotel类中搜索相应的room对象。
主赢博体育程序窗口的控制器类是FXMLDocumentController类。这个类包含以下成员变量:
民营酒店模式;私人房间;private LocalDate;@FXML private ListView reservationsList;@FXML私有DatePicker;@FXML private ChoiceBox roomChoice;
最后三个成员变量提供了接口组件的访问,这些接口组件将显示预订、日期和房间列表。
模型成员变量使我们能够访问Hotel对象,而Hotel对象又使我们能够访问Rooms和Reservations。我将这个成员变量命名为model,因为在JavaFX赢博体育程序中提供对数据访问的主类通常被称为模型类。
当用户从ChoiceBox中选择一个房间名时,我们将在Hotel中查找相应的room对象,并将其存储在currentRoom成员变量中。同样,当用户使用DatePicker选择时间中的某一天时,我们将把选择的日期存储在selectedDate成员变量中。
正如我们在上一个示例中看到的,控制器类可以选择提供初始化方法来设置接口。下面是这个控制器的初始化方法:
@Override public void initialize(URL URL, ResourceBundle rb) {selectedDate = LocalDate.now();datePicker.setValue (selectedDate);roomChoice.getItems()。addAll(《Grand Ballroom》、《Oneida》、《Michigan》、《Mendota》、《Sunrise》);roomChoice。setValue(“大舞厅”);.selectedItemProperty roomChoice.getSelectionModel()()。addListener((observable, oldValue, newValue)->setRoom(newValue));model = new Hotel();扫描器输入= null;try {input = new Scanner(new File("reservations.txt"));model.readFrom(输入);input.close ();currentRoom = model。getRoom(“大舞厅”);} catch(异常ex) {System.out。println(“无法加载数据文件”);ex.printStackTrace ();If (input != null) input.close();} FilteredList<预订> filteredReservations =新的FilteredList<预订>(currentRoom.getReservations(),(事件)->事件. getdate ().equals(selectedDate));reservationsList.setItems (filteredReservations);}
该方法的顶部设置了DatePicker和ChoiceBox,用今天的日期初始化DatePicker,用固定的房间名称列表初始化ChoiceBox。为了处理当用户从ChoiceBox中选择房间时触发的选择事件,我们使用ChoiceBox的选择模型为该事件设置了一个侦听器。该模型提供了一个选定的项属性,我们可以使用lambda表达式将更改侦听器挂接到该属性。该lambda表达式获取新选择房间的名称(newValue),并将该房间名称传递给控制器中的setRoom()方法。
initialize()方法的中间部分创建Hotel对象并从文本文件中读取其内容。
在读取了reservation之后,我们需要从Hotel中获取其中一个Rooms,并在reservationsList中显示它的预订。在这一点上,我们必须做一个重要的调整。回想一下,Rooms将包含许多不同日期的预订:我们只想显示当前选定日期的预订。为了解决这个问题,JavaFX允许我们在包含该房间的reservation的属性周围包装一个FilteredList对象。FilteredList的构造函数接受第二个参数,该参数是一个lambda表达式,将赢博体育于属性列表中的每个Reservation。只有lambda表达式返回true的reservation才会显示在ListView中。将过滤后的属性列表与ListView挂钩
reservationsList.setItems (filteredReservations);
我们将使列表视图仅显示当前选定日期的事件。如果我们想要在不过滤的情况下显示房间中的赢博体育事件,我们就可以这样做
reservationsList.setItems (currentRoom.getReservations ());
主窗口的用户界面包含一个带有两个元素的VBox:一个菜单栏和一个包含界面其余部分的BorderPane。BorderPane是一种容器类型,它包含一个大的中心区域以及中心区域的赢博体育四个侧面的较小的边界区域。在这个赢博体育程序中,中心区域被预订列表占用,而顶部边界区域包含一个HBox,其中包含DatePicker和ChoiceBox。
菜单栏本质上是一个或多个菜单对象的容器。反过来,每个Menu对象是一个或多个MenuItem对象的容器。菜单项非常类似于按钮,因为它们显示文本并可以有关联的操作方法。将操作方法与MenuItem关联的方式与将操作方法与Button关联的方式完全相同。
“文件”菜单包含保存和退出命令,用于保存赢博体育程序的数据。reservation菜单包含添加新预订和删除选定预订的命令。
进行新的预订
添加预订…Reservation菜单中的MenuItem用于向当前选定的房间添加Reservation对象。当用户选择此菜单项时,他们将看到弹出一个对话框,提示他们输入有关新事件的详细信息。
实现此对话框需要以下步骤:
要创建一个新的FXML文件/控制器类组合,我们只需右键单击NetBeans中包含JavaFX资源文件的Other Sources中的文件夹,并选择创建一个新的Empty FXML文件。这时还必须在相应的源包中创建一个控制器类。
下面是我为对话框的控制器类编写的代码。
公共类FXMLNewReservationDialogController实现Initializable{私人房间房间;private LocalDate日期;@FXML私有TextField reservationNumber;@FXML私有文本字段customerNumber;@FXML私有TextField groupSize;@FXML private TextField startTime;@FXML私有TextField endTime;@FXML私有无效cancelDialog(ActionEvent事件){reservationNumber.getScene(). getwwindow ().hide();} @FXML私有标签errorText;@FXML私有无效acceptDialog(ActionEvent事件){int rsvNumber = Integer.parseInt(reservationNumber.getText());int custNumber = Integer.parseInt(customerNumber.getText());int start = Integer.parseInt(startTime.getText());int end = Integer.parseInt(endTime.getText());int size = Integer.parseInt(groupSize.getText());预订newReservation =新预订(rsvNumber,custNumber,日期,开始,结束,大小);if(room.allowsReservation(newReservation)) {room. addressreservation (newReservation);hide .getWindow reservationNumber.getScene () () ();} else errorText。setext(“无法安排预订”);} @覆盖公共无效初始化(URL URL, ResourceBundle rb) {// TODO}公共无效setRoom(房间房间){这。Room =房间;}公共无效setDate(LocalDate日期){这。Date =日期;}}
请注意,这个控制器类包含房间和日期成员变量,它们告诉我们要向哪个房间对象添加新的Reservation,以及要为哪个LocalDate进行预订,以及我们可以用来正确设置这些成员变量的适当setter方法。
正如您所看到的,这个控制器类中唯一有意义的代码位是在acceptDialog方法中,该方法链接到对话框界面中的Create按钮。该方法创建一个新的Reservation对象,然后尝试将其添加到Room。如果Room接受新的Reservation,则acceptDialog方法继续执行并关闭对话框。如果Room拒绝预订,该方法将在界面中设置一条错误消息,并保持对话框打开。
这里现在是链接到添加预订的代码…主窗口中的菜单项。
@FXML private void adreservation (ActionEvent事件){Stage parent = (Stage) reservationsList.getScene(). getwinwindow ();try {FXMLLoader loader = new FXMLLoader(getClass().getResource("FXMLNewEventDialog.fxml"));Parent root = (Parent)loader.load();Scene =新场景(根);Stage dialog = new Stage();dialog.setScene(现场);对话框。setTitle("Create New Reservation");dialog.initOwner(父);dialog.initModality (Modality.WINDOW_MODAL);dialog.initStyle (StageStyle.UTILITY);dialog.setX(parent.getX() + parent.getWidth()/4);dialog.setY(parent.getY() + parent.getHeight()/3);FXMLNewReservationDialogController controller = (FXMLNewReservationDialogController) loader.getController();controller.setRoom (currentRoom);controller.setDate (selectedDate);dialog.show ();} catch(异常ex) {System.out。println(“无法打开对话框”);ex.printStackTrace ();}}
为了创建对话框,我们必须创建一个新的Stage和Scene来容纳对话框界面。为了加载对话场景的接口,我们制作了一个可以从FXML文件加载接口的FXMLLoader。
上面的第二个代码块执行了一些额外的步骤,使我们刚刚创建的新Stage看起来和行为像一个对话框。第一个语句将主阶段设置为对话阶段的赢博体育者。这样,如果用户最小化主赢博体育程序窗口,对话框也会随之最小化。第二个语句使包含对话框的窗口成为模态窗口。这意味着用户将无法与主窗口交互,直到对话框消失。块中的最后两个语句设置了对话框窗口相对于主赢博体育程序窗口的位置—这纯粹是为了使对话框弹出在屏幕上的合理位置。
最后的代码块要求FXMLLoader为对话框获取控制器对象。我们需要访问对话框的控制器,以便将当前选择的房间和日期信息传递给它。
设置好一切之后,我们向对话框的舞台发出show()命令,使对话框可见。