## 四、系统的设计
### 4.1 使用流程图
Hyperpoint 作为一个演示文稿编辑工具,主要使用流程如下:
```
```
### 4.2 功能模块设计
Hyperpoint 系统包含以下主要功能模块:
```
```
各模块功能说明:
| 模块 | 主要功能 |
| -------------------- | ------------------------------------------------------------------ |
| **演示文稿管理模块** | 管理演示文稿的创建、加载、保存;管理幻灯片的添加、删除、排序 |
| **页面编辑模块** | 管理幻灯片中的页面;负责页面的创建、删除和内容管理 |
| **对象绘制模块** | 渲染和管理页面上的所有可绘制对象(文本、图形、图片);处理鼠标交互 |
| **属性编辑模块** | 提供 UI 界面编辑选中对象的属性(颜色、大小、字体等) |
| **撤销重做模块** | 实现编辑操作的撤销和重做功能,基于命令模式 |
| **文件管理模块** | 处理文件的打开、保存、导出等操作 |
| **主题管理模块** | 管理应用的视觉主题,支持多种颜色方案 |
| **演示播放模块** | 全屏播放演示,支持动画转换和翻页操作 |
### 4.3 程序结构设计
#### 4.3.1 类层次结构
系统包含 **18 个主要类**,可分为以下几个层次:
**数据模型层:**
- `Presentation`:演示文稿
- `Slide`:幻灯片
- `SlidePage`:页面
- `PageContent`:页面内容容器
- `DrawableObject`:可绘制对象(抽象基类)
- `TextObject`:文本对象
- `ShapeObject`:图形对象
- `ImageObject`:图片对象
**UI 层:**
- `Controller`:主控制器
- `DrawingCanvas`:绘图画布
- `PropertyPanel`:属性编辑面板
- `PresentationWindow`:演示播放窗口
**命令层:**
- `Command`:命令接口
- `CommandHistory`:命令历史管理器
- `AddObjectCommand`:添加对象命令
- `DeleteObjectCommand`:删除对象命令
- `ModifyObjectCommand`:修改对象命令
**工具层:**
- `Theme`:主题类
- `ThemeManager`:主题管理器
#### 4.3.2 类关系图(完整版)
```
```
#### 4.3.3 主要类的职责说明
| 类名 | 层次 | 职责 |
| ----------------------- | -------- | -------------------------------------- |
| **Presentation** | 数据模型 | 管理整个演示文稿,包含多个幻灯片 |
| **Slide** | 数据模型 | 代表单个幻灯片,管理多个页面 |
| **SlidePage** | 数据模型 | 代表幻灯片中的单个页面,关联页面内容 |
| **PageContent** | 数据模型 | 容器类,管理页面上的所有可绘制对象 |
| **DrawableObject** | 数据模型 | 抽象基类,所有可绘制对象的父类 |
| **TextObject** | 数据模型 | 文本对象,包含字体、颜色等属性 |
| **ShapeObject** | 数据模型 | 图形对象,支持多种形状(矩形、圆形等) |
| **ImageObject** | 数据模型 | 图片对象,管理图片路径 |
| **Controller** | UI 层 | 主控制器,协调各个 UI 组件的交互 |
| **DrawingCanvas** | UI 层 | 绘图画布,负责对象的渲染和鼠标交互 |
| **PropertyPanel** | UI 层 | 属性编辑面板,提供对象属性编辑 UI |
| **PresentationWindow** | UI 层 | 全屏演示窗口,实现演示播放功能 |
| **Command** | 命令层 | 接口,定义命令的执行和撤销操作 |
| **CommandHistory** | 命令层 | 管理撤销/重做功能,维护命令栈 |
| **AddObjectCommand** | 命令层 | 实现对象添加命令 |
| **DeleteObjectCommand** | 命令层 | 实现对象删除命令 |
| **ModifyObjectCommand** | 命令层 | 实现对象修改命令 |
| **Theme** | 工具层 | 主题数据类,存储颜色方案 |
| **ThemeManager** | 工具层 | 单例类,管理应用主题 |
---
## 五、程序关键类的实现
### 5.1 Controller 类详解
#### 5.1.1 类的成员变量
```java
@FXML private ListView pageListView; // 页面列表视图
@FXML private Label pageNameLabel; // 页面名称标签
@FXML private AnchorPane drawingCanvasContainer; // 绘图画布容器
@FXML private VBox propertyPanelContainer; // 属性面板容器
@FXML private MenuItem undoMenuItem; // 撤销菜单项
@FXML private MenuItem redoMenuItem; // 重做菜单项
private Slide currentSlide; // 当前幻灯片
private DrawingCanvas drawingCanvas; // 绘图画布
private PropertyPanel propertyPanelComponent; // 属性面板组件
private CommandHistory commandHistory; // 命令历史
private ThemeManager themeManager; // 主题管理器
```
#### 5.1.2 关键方法的功能与流程
**initialize() 方法**
该方法是控制器的初始化方法,在应用启动时被调用。首先初始化命令历史对象用于管理撤销重做功能,然后获取单例的主题管理器实例并应用初始主题。接着禁用页面列表视图(因为此时还没有打开任何幻灯片),最后为页面列表视图添加选择监听器,当用户在列表中选择页面时会触发 displayPageContent() 方法来显示该页面的内容。
**onNewSlide() 方法**
该方法处理创建新幻灯片的请求。首先弹出文本输入对话框要求用户输入幻灯片名称,随后验证输入内容是否为空。如果名称有效,创建一个新的 Slide 对象并调用 loadSlidePages() 方法将其页面加载到页面列表视图中。最后显示成功提示信息告知用户幻灯片已创建,并更新应用窗口标题以显示当前打开的幻灯片名称。
**displayPageContent(SlidePage page) 方法**
该方法用于显示选中页面的内容。首先进行非空检查,然后更新页面名称标签显示当前页面的标题。接着清空绘图画布容器和属性面板容器中的旧组件,从页面对象中获取 PageContent 实例。创建一个新的 DrawingCanvas 对象用于绘制页面上的对象,并将命令历史管理器注入到 Canvas 中。将 Canvas 添加到容器并通过设置 AnchorPane 约束使其充满容器。最后创建属性编辑面板组件并添加到属性面板容器中。
**onAddText() 方法**
该方法处理添加文本对象的请求。首先检查 drawingCanvas 是否存在,如果不存在则显示警告并返回。弹出文本输入对话框让用户输入文本内容,验证输入非空后创建 TextObject 对象。获取当前选中的页面,创建 AddObjectCommand 命令对象来封装这个添加操作,然后通过 commandHistory.execute() 执行该命令。命令执行完成后调用 redraw() 方法重新绘制画布以显示新添加的文本对象。
**onAddRectangle() 等图形添加方法**
这些方法(包括 onAddCircle、onAddLine 等)处理添加各种图形对象的请求。首先检查 drawingCanvas 是否存在,然后根据方法类型创建对应的 ShapeObject 实例并指定相应的形状类型(如 RECTANGLE、CIRCLE、LINE 等)。获取当前页面的 PageContent 容器,创建 AddObjectCommand 命令对象来封装添加操作,通过 commandHistory.execute() 执行该命令。最后调用 redraw() 重新绘制画布显示新添加的图形。
### 5.2 DrawingCanvas 类详解
#### 5.2.1 类的成员变量
```java
private Canvas canvas; // JavaFX Canvas 对象
private PageContent pageContent; // 页面内容
private DrawableObject selectedObject; // 当前选中对象
private double lastX, lastY; // 上一次鼠标位置
private CommandHistory commandHistory; // 命令历史
private Runnable onSelectionChanged; // 选择变化回调
private ResizePoint resizePoint = ResizePoint.NONE; // 当前调整点
private static final double HANDLE_SIZE = 8; // 调整点大小
```
#### 5.2.2 关键方法的功能与流程
**handleMousePressed(MouseEvent event) 方法**
该方法处理鼠标按下事件。首先记录当前鼠标的位置为 lastX 和 lastY。如果已有选中对象,检查点击位置是否在对象的四个角上的调整点(corner handles)上,如果是则设置相应的 resizePoint 标记并返回。如果不是调整点,则重置 resizePoint 为 NONE。随后调用 pageContent.findObjectAt() 方法从后往前搜索被点击位置的对象(后添加的对象优先级更高),如果找到对象则调用 setSelectedObject() 选中它,否则取消当前选择。最后调用 redraw() 重新绘制画布显示选中状态。
**handleMouseDragged(MouseEvent event) 方法**
该方法处理鼠标拖动事件。首先计算鼠标移动的差值 dx 和 dy。如果有选中对象,根据 resizePoint 的值判断是否在进行对象缩放操作。如果正在缩放(resizePoint != NONE),调用 resizeObject() 方法根据拖动方向和调整点计算新的对象尺寸;否则直接更新对象的坐标位置以实现拖动。然后更新 lastX 和 lastY 为当前鼠标位置,调用 redraw() 重新绘制画布显示对象的新位置或尺寸。
**resizeObject() 方法**
该方法根据鼠标拖动方向对选中对象进行缩放。该方法定义最小尺寸常数 MIN_SIZE = 20 以防止对象过小。根据 resizePoint 的值执行不同的缩放策略:从左上角调整时 x 和 y 增加而 width 和 height 减少,从右上角调整时 y 增加、width 增加、height 减少,从左下角调整时 x 增加、width 减少、height 增加,从右下角调整时 width 和 height 都增加。每次调整前都检查新的尺寸是否满足最小尺寸要求,只有满足条件时才真正更新对象属性,否则忽略此次调整。
**redraw() 方法**
该方法负责重新绘制整个画布内容。首先清空 Canvas 的内容并获取图形上下文。遍历 pageContent 中的所有可绘制对象,根据每个对象的类型调用不同的绘制方法:对于文本对象使用 Font 和 GraphicsContext.fillText() 绘制文本,对于图形对象使用 drawRect()、drawOval()、drawLine() 等方法绘制,对于图片对象则从指定路径加载图片并绘制。对于被选中的对象,在其周围绘制虚线矩形框作为选中指示,并在四个角上绘制小方块作为调整点供用户拖动调整大小。最后刷新 Canvas 以显示所有的绘制内容。
### 5.3 PageContent 类详解
#### 5.3.1 类的成员变量
```java
private ObservableList drawableObjects; // 可绘制对象列表
```
#### 5.3.2 关键方法的功能与流程
**findObjectAt(double x, double y) 方法**
该方法用于查找包含指定点的可绘制对象。从可绘制对象列表的末尾开始反向遍历(即从最后添加的对象开始),对每个对象调用其 contains(x, y) 方法检查指定点是否在对象内部。如果某个对象的 contains() 方法返回 true,表示该点在该对象内,立即返回该对象。如果遍历完整个列表都没有找到包含该点的对象,返回 null。这种反向遍历的设计目的是为了实现对象的优先级概念,后添加的对象会覆盖先添加的对象,因此优先级更高,点击时应优先选中。
### 5.4 DrawableObject 及其子类详解
#### 5.4.1 DrawableObject 抽象基类
**成员变量:**
```java
protected double x, y; // 位置坐标
protected double width, height; // 尺寸
protected String id; // 唯一标识
protected boolean selected; // 选中状态
```
**抽象方法:**
```java
public abstract boolean contains(double px, double py); // 判断点是否在对象内
public abstract String getTypeName(); // 获取对象类型名称
```
#### 5.4.2 TextObject 类详解
**成员变量:**
```java
private String text; // 文本内容
private String fontFamily; // 字体名称
private double fontSize; // 字体大小
private String fontStyle; // 字体样式 (NORMAL/BOLD/ITALIC/BOLD_ITALIC)
private String textColor; // 文本颜色
private List layoutLines; // 排版后的文本行
private boolean layoutDirty; // 排版是否需要重新计算
```
**getLayoutLines() 方法**
该方法用于获取排版后的文本行列表。首先检查 layoutDirty 标志位,如果该标志为 true,表示文本或相关属性已经改变,需要重新进行排版计算。调用 recalculateLayout() 方法重新计算排版,该方法根据对象的宽度对文本进行自动换行处理,计算每行文本的宽度,当某行文本宽度超过对象的宽度时就将其拆分为多行。排版计算完成后生成排版后的文本行列表,并将 layoutDirty 标志设为 false 表示排版已是最新状态。最后返回排版后的文本行列表,绘图器会使用此列表在 Canvas 上逐行绘制文本。
#### 5.4.3 ShapeObject 类详解
**成员变量:**
```java
private ShapeType shapeType; // 形状类型 (LINE/RECTANGLE/CIRCLE/ELLIPSE)
private String fillColor; // 填充颜色(16进制)
private String strokeColor; // 边框颜色(16进制)
private double strokeWidth; // 边框宽度
```
**contains() 方法**
该方法判断给定的点 (px, py) 是否在该图形对象内。对于直线类型的对象,由于直线本身没有面积,采用特殊的处理方式。调用 distanceToLine() 方法计算点到直线的距离,如果距离小于等于 5 像素则认为点在直线上或非常接近,返回 true;这样设计是为了提供更好的用户体验,使得用户点击直线附近时也能选中它。对于其他形状(矩形、圆形、椭圆),进行简单的矩形碰撞检测,检查点是否满足条件 px >= x && px <= x+width && py >= y && py <= y+height,如果满足则返回 true,否则返回 false。虽然圆形和椭圆的精确碰撞检测应该基于圆心和半径,但这里使用矩形检测简化了计算,在实际应用中通常也足够用。
### 5.5 CommandHistory 和 Command 体系详解
#### 5.5.1 CommandHistory 类
**成员变量:**
```java
private Stack undoStack; // 撤销栈
private Stack redoStack; // 重做栈
private Runnable onHistoryChanged; // 历史记录变化监听器
```
**execute(Command command) 方法**
该方法执行一个命令并将其添加到历史记录中。首先调用 command.execute() 方法执行该命令,使其对数据模型产生影响。执行成功后,将该命令对象压入 undoStack 栈中,用于后续的撤销操作。随后清空 redoStack,因为执行了新命令意味着用户改变了意图,之前通过 undo 操作积累的 redo 命令序列已经不再有效。最后通知监听器(onHistoryChanged),使得 UI 组件(如撤销/重做菜单项)可以根据栈的状态更新自己的启用/禁用状态。
**undo() 方法**
该方法撤销最后一个执行的命令。首先检查 undoStack 是否为空,如果为空则表示没有可以撤销的命令,直接返回。如果栈不为空,弹出栈顶的命令对象,调用其 undo() 方法来反转该命令的效果,使数据模型恢复到执行该命令前的状态。然后将这个被撤销的命令压入 redoStack,使得用户可以在之后通过 redo 操作重新执行它。最后通知监听器更新 UI 状态。
**redo() 方法**
该方法重新执行一个被撤销的命令。首先检查 redoStack 是否为空,如果为空则表示没有可以重做的命令,直接返回。如果栈不为空,弹出栈顶的命令对象,调用其 execute() 方法重新执行该命令。执行完成后将这个命令压入 undoStack,使得用户可以再次撤销它。最后通知监听器更新 UI 状态,使撤销/重做按钮的启用状态保持最新。
#### 5.5.2 Command 实现类
**AddObjectCommand**
该命令实现了在页面上添加对象的功能。在 execute() 方法中,直接调用 pageContent.addObject(object) 将指定的对象添加到页面内容的可绘制对象列表中,使其出现在画布上。在 undo() 方法中,调用 pageContent.removeObject(object) 将该对象从页面内容中移除,恢复到执行此命令之前的状态。
**DeleteObjectCommand**
该命令实现了从页面上删除对象的功能。在 execute() 方法中,调用 pageContent.removeObject(object) 将指定的对象从页面内容中移除,使其从画布上消失。在 undo() 方法中,调用 pageContent.addObject(object) 将该对象重新添加回页面内容,恢复到执行此命令之前的状态。
**ModifyObjectCommand**
该命令实现了修改对象属性的功能。在创建命令实例时,先保存对象的原始状态(通过深拷贝或快照机制)。在 execute() 方法中,应用新的属性值到对象,使其显示新的状态。在 undo() 方法中,恢复对象为保存的原始状态,使得修改的效果被完全反转。
### 5.6 ThemeManager 类详解
#### 5.6.1 类的成员变量
```java
private static final ThemeManager instance; // 单例实例
private Map themes; // 主题映射表
private Theme currentTheme; // 当前主题
private Runnable onThemeChanged; // 主题变化回调
```
#### 5.6.2 关键方法的功能与流程
**getInstance() 方法**
该方法是单例模式的实现,返回 ThemeManager 的静态唯一实例。在应用的任何地方都可以通过调用这个方法获得相同的 ThemeManager 实例,确保整个应用中只有一个主题管理器,这样所有的主题设置都是全局一致的。通过单例模式避免了创建多个 ThemeManager 实例造成的内存浪费和数据不一致问题。
**initializeDefaultThemes() 方法**
该方法在 ThemeManager 初始化时被调用,创建并初始化所有预定义的应用主题。创建 Light 主题(浅色),主要使用浅色调的颜色,适合亮色环境。创建 Dark 主题(深色),主要使用深色调的颜色,减轻眼睛疲劳。创建 Blue 主题(蓝色),以蓝色作为主色调,展现专业感。创建 Green 主题(绿色),以绿色作为主色调,代表生机。每个主题都包含 8 种颜色定义,包括背景颜色、菜单栏颜色、菜单文字颜色、页面背景颜色、边框颜色、主色调和次色调,这些颜色共同定义了该主题的整体视觉风格。
**switchTheme(String themeName) 方法**
该方法用于切换应用的主题。首先从 themes 映射表中查找指定名称的主题对象,如果主题存在,将其设置为 currentTheme 成为当前使用的主题。然后调用 onThemeChanged 回调函数,通知所有监听主题变化的 UI 组件,使它们能够根据新主题重新计算样式并刷新自己的外观,从而实现整个应用界面的实时主题切换效果。
### 5.7 PresentationWindow 类详解
#### 5.7.1 类的成员变量
```java
private final Slide slide; // 要播放的幻灯片
private final Stage stage; // 演示窗口舞台
private final Canvas canvas; // 绘图画布
private final StackPane root; // 根节点
private int currentPageIndex; // 当前页面索引
private TransitionEffect currentTransition; // 当前转换效果
private boolean isTransitioning; // 是否正在转换中
```
#### 5.7.2 关键方法的功能与流程
**initializeStage() 方法**
该方法初始化演示播放窗口的舞台(Stage)和相关组件。首先定义页面的标准尺寸为 1024x768,保证与编辑界面的尺寸一致。设置舞台为全屏模式并去掉装饰,使用户获得完整的演示体验。绑定 Canvas 的宽高到根节点 root 的宽高,实现自动缩放以适应不同的屏幕分辨率。添加键盘事件处理程序,右箭头键或空格键用于翻到下一页,左箭头键翻到上一页,ESC 键用于退出演示。添加鼠标点击事件处理,使用户可以单击屏幕翻页。监听场景大小变化事件,当窗口大小改变时重新绘制当前页面以适应新尺寸。最后绘制演示的第一页。
**drawCurrentPage() 方法**
该方法负责在 Canvas 上绘制当前页面的内容。首先获取当前页面索引对应的 SlidePage 对象及其 PageContent。计算缩放因子用于将标准页面尺寸缩放到实际窗口尺寸,获取舞台的实际宽高并计算宽度和高度方向的缩放比例,取较小的比例以保持宽高比。清空 Canvas 并填充黑色背景。计算内容应该绘制的中心位置以实现居中显示。应用缩放变换将所有绘制操作应用到 Canvas 上。遍历当前页面的所有可绘制对象,根据对象类型调用相应的绘制方法(与编辑界面类似)。如果主题设置了转换效果(TransitionEffect),在页面切换时应用相应的动画效果,如淡入淡出、幻灯片等。
**nextPage() 和 previousPage() 方法**
nextPage() 方法处理翻到下一页的请求。首先检查 isTransitioning 标志,如果当前正在进行页面转换动画,则忽略此次输入以防止快速点击导致混乱。检查是否还有下一页,如果没有则直接退出演示。如果有下一页,设置 isTransitioning 为 true 表示开始转换。根据 currentTransition 设置的转换效果类型创建相应的动画(如淡入淡出或幻灯片),启动动画。在动画进行过程中更新 currentPageIndex。当动画完成时设置 isTransitioning 为 false,调用 drawCurrentPage() 绘制新页面。previousPage() 方法的流程类似,但 currentPageIndex 是减少而不是增加。
---
**总结:**
该演示文稿系统采用了 **MVC 架构** 结合 **命令模式** 的设计:
- **Model 层**:Presentation、Slide、SlidePage、PageContent 和各种 DrawableObject
- **View 层**:DrawingCanvas、PropertyPanel、PresentationWindow
- **Controller 层**:Controller 类协调用户交互
- **命令模式**:实现灵活的撤销/重做功能
系统具有良好的可扩展性,添加新的对象类型或命令只需继承相应的基类即可。
---
## 六、Java 课程学习心得与作业总结
### 6.1 主要技术应用
项目应用了面向对象设计(继承、多态、组合)、设计模式(命令模式、单例模式、MVC 架构)、事件驱动编程、集合框架(ObservableList、Stack、HashMap)等核心 Java 知识。DrawingCanvas 中的鼠标事件处理完整展示了 JavaFX 事件驱动模型,CommandHistory 的撤销/重做机制充分体现了命令模式的灵活性。
### 6.2 主要难点与解决
对象选择与碰撞检测通过反向遍历和距离公式解决了对象重叠和直线选中的问题。文本自动换行使用 layoutDirty 标志延迟计算提高性能。四角独立缩放通过为每个调整点设定不同策略精确控制尺寸变化。撤销/重做功能利用命令模式和双栈(undoStack、redoStack)实现了完整的操作历史管理。
### 6.3 改进方向
应该添加文件保存/加载和导出功能提高实用性。将 DrawingCanvas 中的绘制逻辑按对象类型拆分为独立方法提高可维护性。为常用操作添加快捷键、右键菜单和多选功能改善交互体验。实现脏矩形机制优化大量对象的渲染性能。考虑添加插件系统和国际化支持增强扩展性。
### 6.4 学习收获
通过该项目深刻理解了分层架构和设计模式在实际项目中的应用价值,体会到了良好的系统设计能够使代码更加灵活、可维护和可扩展。从需求分析到最终实现的全过程提升了对软件工程的认识,特别是在复杂交互设计和代码组织方面获得了宝贵经验。