Compare commits

..

10 Commits

Author SHA1 Message Date
gary 99d96013c2 update project 2026-05-10 14:09:45 +08:00
liujing133 7eb581d496 Merge branch 'main' of https://github.com/gan-fang-yi/hyperpoint 2025-12-08 01:18:43 +08:00
liujing133 5b159a719a 优化:导出幻灯片为PDF 2025-12-08 00:32:15 +08:00
gary a1c9f794a0 feature:初步实现主题 2025-12-07 23:23:10 +08:00
liujing133 12437bb6a6 优化撤销重做:组件的修改和删除 2025-12-06 21:55:13 +08:00
liujing133 fb7e360152 增加组件的旋转 2025-12-06 00:27:43 +08:00
liujing133 6b31c8051d 增加鼠标滚轮控制页面切换 2025-12-05 23:04:21 +08:00
gary f88dac9d2c feature:初步实现操作的撤回重做 2025-11-27 09:08:06 +08:00
gary 5fc98d3d9b fix: 全屏展示问题 2025-11-25 14:46:14 +08:00
gary cdc02d6bbd feature: 初步实现全屏播放 2025-11-25 14:36:19 +08:00
23 changed files with 3052 additions and 178 deletions
+4 -4
View File
@@ -17,9 +17,9 @@
## 三. 选做功能:
- [x] 对幻灯片页面上已有的文字进行修改(如颜色、大小、字体等)
- [ ] 对幻灯片页面上已有的基本图形进行修改(如填充色、边框色、边框线型、大小等)
- [ ] 对幻灯片页面的主题色进行修改(背景颜色、菜单颜色、页面排版样式)
- [ ] 幻灯片的全屏播放、翻页。支持3种以上翻页动画效果
- [x] 对幻灯片页面上已有的基本图形进行修改(如填充色、边框色、边框线型、大小等)
- [x] 对幻灯片页面的主题色进行修改(背景颜色、菜单颜色、页面排版样式)
- [x] 幻灯片的全屏播放、翻页。支持3种以上翻页动画效果
- [x] 通过鼠标拖拽改变幻灯片页面上的图片大小
- [ ] 操作的撤销与重做
- [x] 操作的撤销与重做
- [ ] 其他(可参考PowerPoint
+7 -2
View File
@@ -26,6 +26,11 @@
<artifactId>javafx-fxml</artifactId>
<version>${javafx.version}</version>
</dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-graphics</artifactId>
<version>${javafx.version}</version>
</dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-swing</artifactId>
@@ -69,8 +74,8 @@
<artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
<configuration>
<source>25</source>
<target>25</target>
<source>24</source>
<target>24</target>
</configuration>
</plugin>
<plugin>
+621
View File
@@ -0,0 +1,621 @@
## 四、系统的设计
### 4.1 使用流程图
Hyperpoint 作为一个演示文稿编辑工具,主要使用流程如下:
```
<mxfile host="app.diagrams.net" modified="2025-12-07T10:00:00.000Z" agent="5.0" version="24.1.0">
<diagram id="flowchart" name="使用流程图">
<mxGraphModel dx="1000" dy="700" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="827" pageHeight="1169" math="0" shadow="0">
<root>
<mxCell id="0"/>
<mxCell id="1" parent="0"/>
<mxCell id="2" value="启动应用" style="ellipse;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1">
<mxGeometry x="320" y="20" width="120" height="60" as="geometry"/>
</mxCell>
<mxCell id="3" value="创建新幻灯片" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;" vertex="1" parent="1">
<mxGeometry x="320" y="110" width="120" height="60" as="geometry"/>
</mxCell>
<mxCell id="4" value="创建/添加页面" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;" vertex="1" parent="1">
<mxGeometry x="320" y="200" width="120" height="60" as="geometry"/>
</mxCell>
<mxCell id="5" value="添加对象\n(文本/图形/图片)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;" vertex="1" parent="1">
<mxGeometry x="320" y="290" width="120" height="60" as="geometry"/>
</mxCell>
<mxCell id="6" value="编辑对象属性" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;" vertex="1" parent="1">
<mxGeometry x="320" y="380" width="120" height="60" as="geometry"/>
</mxCell>
<mxCell id="7" value="保存/导出" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;" vertex="1" parent="1">
<mxGeometry x="320" y="470" width="120" height="60" as="geometry"/>
</mxCell>
<mxCell id="8" value="播放演示" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;" vertex="1" parent="1">
<mxGeometry x="320" y="560" width="120" height="60" as="geometry"/>
</mxCell>
<mxCell id="9" value="退出" style="ellipse;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;" vertex="1" parent="1">
<mxGeometry x="320" y="650" width="120" height="60" as="geometry"/>
</mxCell>
<mxCell id="10" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="1" source="2" target="3">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="11" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="1" source="3" target="4">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="12" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="1" source="4" target="5">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="13" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="1" source="5" target="6">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="14" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="1" source="6" target="7">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="15" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="1" source="7" target="8">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="16" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="1" source="8" target="9">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="17" value="可循环返回\n继续编辑" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;dashed=1;" edge="1" parent="1" source="6" target="4">
<mxGeometry x="-0.5" y="20" relative="1" as="geometry">
<mxPoint x="220" y="300" as="sourcePoint"/>
<mxPoint x="220" y="410" as="targetPoint"/>
</mxGeometry>
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>
```
### 4.2 功能模块设计
Hyperpoint 系统包含以下主要功能模块:
```
<mxfile host="app.diagrams.net" modified="2025-12-07T10:00:00.000Z" agent="5.0" version="24.1.0">
<diagram id="moduleDiagram" name="功能模块图">
<mxGraphModel dx="1200" dy="800" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="827" pageHeight="1169" math="0" shadow="0">
<root>
<mxCell id="0"/>
<mxCell id="1" parent="0"/>
<mxCell id="2" value="Hyperpoint 演示文稿系统" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;fontSize=16;fontStyle=1;" vertex="1" parent="1">
<mxGeometry x="300" y="20" width="300" height="60" as="geometry"/>
</mxCell>
<mxCell id="3" value="演示文稿管理模块" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1">
<mxGeometry x="50" y="150" width="150" height="80" as="geometry"/>
</mxCell>
<mxCell id="4" value="页面编辑模块" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1">
<mxGeometry x="250" y="150" width="150" height="80" as="geometry"/>
</mxCell>
<mxCell id="5" value="对象绘制模块" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1">
<mxGeometry x="450" y="150" width="150" height="80" as="geometry"/>
</mxCell>
<mxCell id="6" value="属性编辑模块" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1">
<mxGeometry x="650" y="150" width="150" height="80" as="geometry"/>
</mxCell>
<mxCell id="7" value="撤销重做模块" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;" vertex="1" parent="1">
<mxGeometry x="50" y="300" width="150" height="80" as="geometry"/>
</mxCell>
<mxCell id="8" value="文件管理模块" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;" vertex="1" parent="1">
<mxGeometry x="250" y="300" width="150" height="80" as="geometry"/>
</mxCell>
<mxCell id="9" value="主题管理模块" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;" vertex="1" parent="1">
<mxGeometry x="450" y="300" width="150" height="80" as="geometry"/>
</mxCell>
<mxCell id="10" value="演示播放模块" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;" vertex="1" parent="1">
<mxGeometry x="650" y="300" width="150" height="80" as="geometry"/>
</mxCell>
<mxCell id="11" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="1" source="2" target="3">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="12" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="1" source="2" target="4">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="13" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="1" source="2" target="5">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="14" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="1" source="2" target="6">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="15" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="1" source="2" target="7">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="16" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="1" source="2" target="8">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="17" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="1" source="2" target="9">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="18" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="1" source="2" target="10">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>
```
各模块功能说明:
| 模块 | 主要功能 |
| -------------------- | ------------------------------------------------------------------ |
| **演示文稿管理模块** | 管理演示文稿的创建、加载、保存;管理幻灯片的添加、删除、排序 |
| **页面编辑模块** | 管理幻灯片中的页面;负责页面的创建、删除和内容管理 |
| **对象绘制模块** | 渲染和管理页面上的所有可绘制对象(文本、图形、图片);处理鼠标交互 |
| **属性编辑模块** | 提供 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 类关系图(完整版)
```
<mxfile host="app.diagrams.net" modified="2025-12-07T10:00:00.000Z" agent="5.0" version="24.1.0">
<diagram id="classdiagram" name="类关系图">
<mxGraphModel dx="1600" dy="1200" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1400" pageHeight="1000" math="0" shadow="0">
<root>
<mxCell id="0"/>
<mxCell id="1" parent="0"/>
<!-- 数据模型层 -->
<mxCell id="presentation" value="&lt;&lt;class&gt;&gt;&lt;br&gt;&lt;b&gt;Presentation&lt;/b&gt;&lt;br&gt;- name: String&lt;br&gt;- slides: List&amp;lt;Slide&amp;gt;&lt;br&gt;- currentSlideIndex: int&lt;br&gt;+ getName/setName()&lt;br&gt;+ getSlides()&lt;br&gt;+ addSlide()&lt;br&gt;+ removeSlide()" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;align=left;fontSize=11;" vertex="1" parent="1">
<mxGeometry x="50" y="50" width="200" height="140" as="geometry"/>
</mxCell>
<mxCell id="slide" value="&lt;&lt;class&gt;&gt;&lt;br&gt;&lt;b&gt;Slide&lt;/b&gt;&lt;br&gt;- id: String&lt;br&gt;- name: String&lt;br&gt;- pages: List&amp;lt;SlidePage&amp;gt;&lt;br&gt;- currentPageIndex: int&lt;br&gt;+ getName/setName()&lt;br&gt;+ getPages()&lt;br&gt;+ addPage()&lt;br&gt;+ removePage()" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;align=left;fontSize=11;" vertex="1" parent="1">
<mxGeometry x="350" y="50" width="200" height="140" as="geometry"/>
</mxCell>
<mxCell id="slidepage" value="&lt;&lt;class&gt;&gt;&lt;br&gt;&lt;b&gt;SlidePage&lt;/b&gt;&lt;br&gt;- id: String&lt;br&gt;- title: String&lt;br&gt;- pageContent: PageContent&lt;br&gt;+ getTitle/setTitle()&lt;br&gt;+ getPageContent()" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;align=left;fontSize=11;" vertex="1" parent="1">
<mxGeometry x="650" y="50" width="200" height="130" as="geometry"/>
</mxCell>
<mxCell id="pagecontent" value="&lt;&lt;class&gt;&gt;&lt;br&gt;&lt;b&gt;PageContent&lt;/b&gt;&lt;br&gt;- drawableObjects: List&lt;br&gt;+ addObject()&lt;br&gt;+ removeObject()&lt;br&gt;+ findObjectAt()&lt;br&gt;+ getDrawableObjects()" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;align=left;fontSize=11;" vertex="1" parent="1">
<mxGeometry x="900" y="50" width="200" height="130" as="geometry"/>
</mxCell>
<mxCell id="drawableobject" value="&lt;&lt;abstract class&gt;&gt;&lt;br&gt;&lt;b&gt;DrawableObject&lt;/b&gt;&lt;br&gt;- x, y: double&lt;br&gt;- width, height: double&lt;br&gt;- id: String&lt;br&gt;- selected: boolean&lt;br&gt;+ getX/setX()&lt;br&gt;+ contains()&lt;br&gt;+ getTypeName()" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#d4e6f1;strokeColor=#3498db;align=left;fontSize=11;" vertex="1" parent="1">
<mxGeometry x="400" y="300" width="220" height="150" as="geometry"/>
</mxCell>
<mxCell id="textobject" value="&lt;&lt;class&gt;&gt;&lt;br&gt;&lt;b&gt;TextObject&lt;/b&gt;&lt;br&gt;- text: String&lt;br&gt;- fontFamily: String&lt;br&gt;- fontSize: double&lt;br&gt;- fontStyle: String&lt;br&gt;- textColor: String&lt;br&gt;+ getText/setText()&lt;br&gt;+ getLayoutLines()" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#c8e6c9;strokeColor=#66bb6a;align=left;fontSize=11;" vertex="1" parent="1">
<mxGeometry x="100" y="500" width="200" height="130" as="geometry"/>
</mxCell>
<mxCell id="shapeobject" value="&lt;&lt;class&gt;&gt;&lt;br&gt;&lt;b&gt;ShapeObject&lt;/b&gt;&lt;br&gt;- shapeType: enum&lt;br&gt;- fillColor: String&lt;br&gt;- strokeColor: String&lt;br&gt;- strokeWidth: double&lt;br&gt;+ getShapeType()&lt;br&gt;+ setFillColor()" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#c8e6c9;strokeColor=#66bb6a;align=left;fontSize=11;" vertex="1" parent="1">
<mxGeometry x="400" y="500" width="200" height="130" as="geometry"/>
</mxCell>
<mxCell id="imageobject" value="&lt;&lt;class&gt;&gt;&lt;br&gt;&lt;b&gt;ImageObject&lt;/b&gt;&lt;br&gt;- imagePath: String&lt;br&gt;+ getImagePath()&lt;br&gt;+ setImagePath()" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#c8e6c9;strokeColor=#66bb6a;align=left;fontSize=11;" vertex="1" parent="1">
<mxGeometry x="700" y="500" width="180" height="100" as="geometry"/>
</mxCell>
<!-- UI层 -->
<mxCell id="controller" value="&lt;&lt;class&gt;&gt;&lt;br&gt;&lt;b&gt;Controller&lt;/b&gt;&lt;br&gt;&lt;i&gt;implements Initializable&lt;/i&gt;&lt;br&gt;- currentSlide: Slide&lt;br&gt;- drawingCanvas: DrawingCanvas&lt;br&gt;- commandHistory: CommandHistory&lt;br&gt;- themeManager: ThemeManager&lt;br&gt;+ initialize()&lt;br&gt;+ onNewSlide()&lt;br&gt;+ displayPageContent()" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#bbdefb;strokeColor=#1976d2;align=left;fontSize=10;" vertex="1" parent="1">
<mxGeometry x="50" y="250" width="220" height="150" as="geometry"/>
</mxCell>
<mxCell id="drawingcanvas" value="&lt;&lt;class&gt;&gt;&lt;br&gt;&lt;b&gt;DrawingCanvas&lt;/b&gt;&lt;br&gt;&lt;i&gt;extends Pane&lt;/i&gt;&lt;br&gt;- canvas: Canvas&lt;br&gt;- pageContent: PageContent&lt;br&gt;- selectedObject: DrawableObject&lt;br&gt;- commandHistory: CommandHistory&lt;br&gt;+ redraw()&lt;br&gt;+ handleMousePressed()&lt;br&gt;+ setSelectedObject()" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#bbdefb;strokeColor=#1976d2;align=left;fontSize=10;" vertex="1" parent="1">
<mxGeometry x="350" y="250" width="220" height="150" as="geometry"/>
</mxCell>
<mxCell id="propertypanel" value="&lt;&lt;class&gt;&gt;&lt;br&gt;&lt;b&gt;PropertyPanel&lt;/b&gt;&lt;br&gt;&lt;i&gt;extends VBox&lt;/i&gt;&lt;br&gt;- objectTypeLabel: Label&lt;br&gt;- fontFamilyCombo: ComboBox&lt;br&gt;- textColorPicker: ColorPicker&lt;br&gt;- canvas: DrawingCanvas&lt;br&gt;+ updateProperty()&lt;br&gt;+ deleteSelectedObject()" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#bbdefb;strokeColor=#1976d2;align=left;fontSize=10;" vertex="1" parent="1">
<mxGeometry x="650" y="250" width="220" height="150" as="geometry"/>
</mxCell>
<mxCell id="presentationwindow" value="&lt;&lt;class&gt;&gt;&lt;br&gt;&lt;b&gt;PresentationWindow&lt;/b&gt;&lt;br&gt;- slide: Slide&lt;br&gt;- stage: Stage&lt;br&gt;- canvas: Canvas&lt;br&gt;- currentPageIndex: int&lt;br&gt;- currentTransition: TransitionEffect&lt;br&gt;+ show()&lt;br&gt;+ nextPage()&lt;br&gt;+ drawCurrentPage()" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#bbdefb;strokeColor=#1976d2;align=left;fontSize=10;" vertex="1" parent="1">
<mxGeometry x="950" y="250" width="220" height="140" as="geometry"/>
</mxCell>
<!-- 命令层 -->
<mxCell id="command" value="&lt;&lt;interface&gt;&gt;&lt;br&gt;&lt;b&gt;Command&lt;/b&gt;&lt;br&gt;+ execute()&lt;br&gt;+ undo()" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;align=left;fontSize=11;" vertex="1" parent="1">
<mxGeometry x="50" y="700" width="160" height="100" as="geometry"/>
</mxCell>
<mxCell id="addcmd" value="&lt;&lt;class&gt;&gt;&lt;br&gt;&lt;b&gt;AddObjectCommand&lt;/b&gt;&lt;br&gt;&lt;i&gt;implements Command&lt;/i&gt;&lt;br&gt;- pageContent: PageContent&lt;br&gt;- object: DrawableObject" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#fce4d6;strokeColor=#ea6b66;align=left;fontSize=10;" vertex="1" parent="1">
<mxGeometry x="280" y="700" width="200" height="100" as="geometry"/>
</mxCell>
<mxCell id="deletecmd" value="&lt;&lt;class&gt;&gt;&lt;br&gt;&lt;b&gt;DeleteObjectCommand&lt;/b&gt;&lt;br&gt;&lt;i&gt;implements Command&lt;/i&gt;&lt;br&gt;- pageContent: PageContent&lt;br&gt;- object: DrawableObject" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#fce4d6;strokeColor=#ea6b66;align=left;fontSize=10;" vertex="1" parent="1">
<mxGeometry x="530" y="700" width="210" height="100" as="geometry"/>
</mxCell>
<mxCell id="modifycmd" value="&lt;&lt;class&gt;&gt;&lt;br&gt;&lt;b&gt;ModifyObjectCommand&lt;/b&gt;&lt;br&gt;&lt;i&gt;implements Command&lt;/i&gt;&lt;br&gt;- object: DrawableObject&lt;br&gt;- oldState/newState" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#fce4d6;strokeColor=#ea6b66;align=left;fontSize=10;" vertex="1" parent="1">
<mxGeometry x="780" y="700" width="210" height="100" as="geometry"/>
</mxCell>
<mxCell id="commandhistory" value="&lt;&lt;class&gt;&gt;&lt;br&gt;&lt;b&gt;CommandHistory&lt;/b&gt;&lt;br&gt;- undoStack: Stack&lt;br&gt;- redoStack: Stack&lt;br&gt;+ execute()&lt;br&gt;+ undo()&lt;br&gt;+ redo()&lt;br&gt;+ canUndo/canRedo()" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;align=left;fontSize=10;" vertex="1" parent="1">
<mxGeometry x="1050" y="700" width="180" height="130" as="geometry"/>
</mxCell>
<!-- 工具层 -->
<mxCell id="theme" value="&lt;&lt;class&gt;&gt;&lt;br&gt;&lt;b&gt;Theme&lt;/b&gt;&lt;br&gt;- name: String&lt;br&gt;- backgroundColor: Color&lt;br&gt;- menuBarColor: Color&lt;br&gt;- primaryColor: Color&lt;br&gt;- secondaryColor: Color&lt;br&gt;+ getters/setters" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#f0f4c3;strokeColor=#c0ca33;align=left;fontSize=10;" vertex="1" parent="1">
<mxGeometry x="1300" y="50" width="200" height="130" as="geometry"/>
</mxCell>
<mxCell id="thememanager" value="&lt;&lt;class&gt;&gt;&lt;br&gt;&lt;b&gt;ThemeManager&lt;/b&gt;&lt;br&gt;&lt;i&gt;(Singleton)&lt;/i&gt;&lt;br&gt;- instance: ThemeManager&lt;br&gt;- themes: Map&amp;lt;String, Theme&amp;gt;&lt;br&gt;- currentTheme: Theme&lt;br&gt;+ getInstance()&lt;br&gt;+ switchTheme()" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#f0f4c3;strokeColor=#c0ca33;align=left;fontSize=10;" vertex="1" parent="1">
<mxGeometry x="1300" y="250" width="200" height="140" as="geometry"/>
</mxCell>
<!-- 关系边 - 数据模型层 -->
<mxCell id="edge1" value="包含" style="endArrow=open;html=1;endSize=12;startArrow=diamondThin;startSize=14;startFill=1;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="presentation" target="slide">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="edge2" value="包含" style="endArrow=open;html=1;endSize=12;startArrow=diamondThin;startSize=14;startFill=1;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="slide" target="slidepage">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="edge3" value="包含" style="endArrow=open;html=1;endSize=12;startArrow=diamondThin;startSize=14;startFill=1;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="slidepage" target="pagecontent">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="edge4" value="包含" style="endArrow=open;html=1;endSize=12;startArrow=diamondThin;startSize=14;startFill=1;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="pagecontent" target="drawableobject">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="edge5" value="继承" style="endArrow=block;html=1;endSize=12;edgeStyle=orthogonalEdgeStyle;dashed=0;" edge="1" parent="1" source="textobject" target="drawableobject">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="edge6" value="继承" style="endArrow=block;html=1;endSize=12;edgeStyle=orthogonalEdgeStyle;dashed=0;" edge="1" parent="1" source="shapeobject" target="drawableobject">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="edge7" value="继承" style="endArrow=block;html=1;endSize=12;edgeStyle=orthogonalEdgeStyle;dashed=0;" edge="1" parent="1" source="imageobject" target="drawableobject">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<!-- UI层关系 -->
<mxCell id="edge11" value="使用" style="endArrow=open;html=1;endSize=12;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="controller" target="drawingcanvas">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="edge12" value="使用" style="endArrow=open;html=1;endSize=12;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="controller" target="propertypanel">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="edge13" value="使用" style="endArrow=open;html=1;endSize=12;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="drawingcanvas" target="pagecontent">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="edge14" value="使用" style="endArrow=open;html=1;endSize=12;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="propertypanel" target="drawingcanvas">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="edge15" value="使用" style="endArrow=open;html=1;endSize=12;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="presentationwindow" target="slide">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<!-- 命令层关系 -->
<mxCell id="edge16" value="实现" style="endArrow=block;html=1;endSize=12;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="addcmd" target="command">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="edge17" value="实现" style="endArrow=block;html=1;endSize=12;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="deletecmd" target="command">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="edge18" value="实现" style="endArrow=block;html=1;endSize=12;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="modifycmd" target="command">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="edge19" value="管理" style="endArrow=open;html=1;endSize=12;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="commandhistory" target="command">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="edge20" value="使用" style="endArrow=open;html=1;endSize=12;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="controller" target="commandhistory">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="edge21" value="使用" style="endArrow=open;html=1;endSize=12;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="drawingcanvas" target="commandhistory">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<!-- 工具层关系 -->
<mxCell id="edge22" value="管理" style="endArrow=open;html=1;endSize=12;startArrow=diamondThin;startSize=14;startFill=1;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="thememanager" target="theme">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="edge23" value="使用" style="endArrow=open;html=1;endSize=12;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="controller" target="thememanager">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>
```
#### 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<SlidePage> 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<DrawableObject> 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<String> 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<Command> undoStack; // 撤销栈
private Stack<Command> 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<String, Theme> 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 学习收获
通过该项目深刻理解了分层架构和设计模式在实际项目中的应用价值,体会到了良好的系统设计能够使代码更加灵活、可维护和可扩展。从需求分析到最终实现的全过程提升了对软件工程的认识,特别是在复杂交互设计和代码组织方面获得了宝贵经验。
@@ -0,0 +1,25 @@
package dev.bytevibe.hyperpoint;
/**
* 添加对象命令
*/
public class AddObjectCommand implements Command {
private PageContent pageContent;
private DrawableObject object;
public AddObjectCommand(PageContent pageContent, DrawableObject object) {
this.pageContent = pageContent;
this.object = object;
}
@Override
public void execute() {
pageContent.addObject(object);
}
@Override
public void undo() {
pageContent.removeObject(object);
}
}
@@ -0,0 +1,17 @@
package dev.bytevibe.hyperpoint;
/**
* 命令接口,用于撤销和重做功能
*/
public interface Command {
/**
* 执行命令
*/
void execute();
/**
* 撤销命令
*/
void undo();
}
@@ -0,0 +1,91 @@
package dev.bytevibe.hyperpoint;
import java.util.Stack;
/**
* 命令历史管理器,支持撤销和重做
*/
public class CommandHistory {
private Stack<Command> undoStack;
private Stack<Command> redoStack;
private Runnable onHistoryChanged;
public CommandHistory() {
this.undoStack = new Stack<>();
this.redoStack = new Stack<>();
}
/**
* 执行命令并将其添加到历史记录
*/
public void execute(Command command) {
command.execute();
undoStack.push(command);
redoStack.clear(); // 执行新命令后,清空重做栈
notifyHistoryChanged();
}
/**
* 撤销上一个命令
*/
public void undo() {
if (!undoStack.isEmpty()) {
Command command = undoStack.pop();
command.undo();
redoStack.push(command);
notifyHistoryChanged();
}
}
/**
* 重做上一个被撤销的命令
*/
public void redo() {
if (!redoStack.isEmpty()) {
Command command = redoStack.pop();
command.execute();
undoStack.push(command);
notifyHistoryChanged();
}
}
/**
* 检查是否可以撤销
*/
public boolean canUndo() {
return !undoStack.isEmpty();
}
/**
* 检查是否可以重做
*/
public boolean canRedo() {
return !redoStack.isEmpty();
}
/**
* 清空历史记录
*/
public void clear() {
undoStack.clear();
redoStack.clear();
notifyHistoryChanged();
}
/**
* 设置历史记录变化监听器
*/
public void setOnHistoryChanged(Runnable callback) {
this.onHistoryChanged = callback;
}
/**
* 通知历史记录已改变
*/
private void notifyHistoryChanged() {
if (onHistoryChanged != null) {
onHistoryChanged.run();
}
}
}
@@ -1,20 +1,25 @@
package dev.bytevibe.hyperpoint;
import java.io.File;
import java.net.URL;
import java.util.ResourceBundle;
import dev.bytevibe.hyperpoint.Utils.MyAlert;
import dev.bytevibe.hyperpoint.Utils.MyTextInputDialog;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.*;
import javafx.scene.control.Alert;
import javafx.scene.control.ButtonType;
import javafx.scene.control.Label;
import javafx.scene.control.ListView;
import javafx.scene.control.MenuItem;
import javafx.scene.control.TextInputDialog;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.VBox;
import javafx.stage.FileChooser;
import javafx.stage.Stage;
import java.io.File;
import java.net.URL;
import java.util.ResourceBundle;
public class Controller implements Initializable {
@FXML
private ListView<SlidePage> pageListView;
@@ -26,14 +31,31 @@ public class Controller implements Initializable {
private VBox propertyPanelContainer;
@FXML
private AnchorPane scenePane;
@FXML
private MenuItem undoMenuItem;
@FXML
private MenuItem redoMenuItem;
private Slide currentSlide;
private DrawingCanvas drawingCanvas;
private PropertyPanel propertyPanelComponent;
private CommandHistory commandHistory;
private ThemeManager themeManager;
Stage stage;
@Override
public void initialize(URL url, ResourceBundle resourceBundle) {
// 初始化命令历史
commandHistory = new CommandHistory();
commandHistory.setOnHistoryChanged(this::updateUndoRedoButtons);
// 初始化主题管理器
themeManager = ThemeManager.getInstance();
themeManager.setOnThemeChanged(this::applyTheme);
// 应用初始主题
applyTheme();
pageListView.setDisable(true);
// 添加页面列表选择监听
@@ -42,6 +64,25 @@ public class Controller implements Initializable {
displayPageContent(newVal);
}
});
// 为页面列表添加鼠标滚轮事件
pageListView.setOnScroll(event -> {
if (currentSlide == null || currentSlide.getPages().isEmpty()) {
return;
}
int currentIndex = pageListView.getSelectionModel().getSelectedIndex();
if (event.getDeltaY() < 0) {
if (currentIndex < currentSlide.getPages().size() - 1) {
pageListView.getSelectionModel().select(currentIndex + 1);
}
} else {
if (currentIndex > 0) {
pageListView.getSelectionModel().select(currentIndex - 1);
}
}
event.consume();
});
}
/**
@@ -103,6 +144,8 @@ public class Controller implements Initializable {
// 创建新的Canvas
PageContent pageContent = page.getPageContent();
drawingCanvas = new DrawingCanvas(pageContent);
drawingCanvas.setCurrentPage(page); // 设置当前页面
drawingCanvas.setCommandHistory(commandHistory); // 设置命令历史
drawingCanvasContainer.getChildren().add(drawingCanvas);
// 使用AnchorPane的约束来填充容器
@@ -211,7 +254,15 @@ public class Controller implements Initializable {
String text = dialog.getResult();
if (!text.trim().isEmpty()) {
TextObject textObj = new TextObject(50, 50, text);
drawingCanvas.addObject(textObj);
// 使用命令历史记录此操作
SlidePage currentPage = pageListView.getSelectionModel().getSelectedItem();
if (currentPage != null) {
AddObjectCommand command = new AddObjectCommand(currentPage.getPageContent(), textObj);
commandHistory.execute(command);
drawingCanvas.redraw();
} else {
drawingCanvas.addObject(textObj);
}
}
}
}
@@ -226,7 +277,14 @@ public class Controller implements Initializable {
return;
}
ShapeObject line = new ShapeObject(50, 50, 100, 100, ShapeObject.ShapeType.LINE);
drawingCanvas.addObject(line);
SlidePage currentPage = pageListView.getSelectionModel().getSelectedItem();
if (currentPage != null) {
AddObjectCommand command = new AddObjectCommand(currentPage.getPageContent(), line);
commandHistory.execute(command);
drawingCanvas.redraw();
} else {
drawingCanvas.addObject(line);
}
}
/**
@@ -239,7 +297,14 @@ public class Controller implements Initializable {
return;
}
ShapeObject rect = new ShapeObject(50, 50, 100, 60, ShapeObject.ShapeType.RECTANGLE);
drawingCanvas.addObject(rect);
SlidePage currentPage = pageListView.getSelectionModel().getSelectedItem();
if (currentPage != null) {
AddObjectCommand command = new AddObjectCommand(currentPage.getPageContent(), rect);
commandHistory.execute(command);
drawingCanvas.redraw();
} else {
drawingCanvas.addObject(rect);
}
}
/**
@@ -252,7 +317,14 @@ public class Controller implements Initializable {
return;
}
ShapeObject circle = new ShapeObject(50, 50, 100, 100, ShapeObject.ShapeType.CIRCLE);
drawingCanvas.addObject(circle);
SlidePage currentPage = pageListView.getSelectionModel().getSelectedItem();
if (currentPage != null) {
AddObjectCommand command = new AddObjectCommand(currentPage.getPageContent(), circle);
commandHistory.execute(command);
drawingCanvas.redraw();
} else {
drawingCanvas.addObject(circle);
}
}
/**
@@ -265,7 +337,14 @@ public class Controller implements Initializable {
return;
}
ShapeObject ellipse = new ShapeObject(50, 50, 150, 80, ShapeObject.ShapeType.ELLIPSE);
drawingCanvas.addObject(ellipse);
SlidePage currentPage = pageListView.getSelectionModel().getSelectedItem();
if (currentPage != null) {
AddObjectCommand command = new AddObjectCommand(currentPage.getPageContent(), ellipse);
commandHistory.execute(command);
drawingCanvas.redraw();
} else {
drawingCanvas.addObject(ellipse);
}
}
/**
@@ -290,7 +369,14 @@ public class Controller implements Initializable {
if (file != null) {
ImageObject imgObj = new ImageObject(50, 50, 200, 150, file.getAbsolutePath());
drawingCanvas.addObject(imgObj);
SlidePage currentPage = pageListView.getSelectionModel().getSelectedItem();
if (currentPage != null) {
AddObjectCommand command = new AddObjectCommand(currentPage.getPageContent(), imgObj);
commandHistory.execute(command);
drawingCanvas.redraw();
} else {
drawingCanvas.addObject(imgObj);
}
}
}
@@ -470,6 +556,128 @@ public class Controller implements Initializable {
}
}
/**
* 开始演示(默认淡出淡入效果)
*/
@FXML
public void onStartPresentation(ActionEvent actionEvent) {
if (currentSlide == null || currentSlide.getPages().isEmpty()) {
showWarning("无幻灯片", "请先创建或打开一个幻灯片。");
return;
}
startPresentation(TransitionEffect.FADE);
}
/**
* 使用淡出淡入效果播放演示
*/
@FXML
public void onPresentationWithFade(ActionEvent actionEvent) {
if (currentSlide == null || currentSlide.getPages().isEmpty()) {
showWarning("无幻灯片", "请先创建或打开一个幻灯片。");
return;
}
startPresentation(TransitionEffect.FADE);
}
/**
* 使用从右推入效果播放演示
*/
@FXML
public void onPresentationWithPushLeft(ActionEvent actionEvent) {
if (currentSlide == null || currentSlide.getPages().isEmpty()) {
showWarning("无幻灯片", "请先创建或打开一个幻灯片。");
return;
}
startPresentation(TransitionEffect.PUSH_LEFT);
}
/**
* 使用从左推入效果播放演示
*/
@FXML
public void onPresentationWithPushRight(ActionEvent actionEvent) {
if (currentSlide == null || currentSlide.getPages().isEmpty()) {
showWarning("无幻灯片", "请先创建或打开一个幻灯片。");
return;
}
startPresentation(TransitionEffect.PUSH_RIGHT);
}
/**
* 使用缩放放大效果播放演示
*/
@FXML
public void onPresentationWithZoomIn(ActionEvent actionEvent) {
if (currentSlide == null || currentSlide.getPages().isEmpty()) {
showWarning("无幻灯片", "请先创建或打开一个幻灯片。");
return;
}
startPresentation(TransitionEffect.ZOOM_IN);
}
/**
* 使用旋转翻页效果播放演示
*/
@FXML
public void onPresentationWithRotate(ActionEvent actionEvent) {
if (currentSlide == null || currentSlide.getPages().isEmpty()) {
showWarning("无幻灯片", "请先创建或打开一个幻灯片。");
return;
}
startPresentation(TransitionEffect.ROTATE);
}
/**
* 启动演示
*/
private void startPresentation(TransitionEffect transition) {
PresentationWindow presentationWindow = new PresentationWindow(currentSlide, transition);
presentationWindow.show();
}
/**
* 撤销操作
*/
@FXML
public void onUndo(ActionEvent actionEvent) {
commandHistory.undo();
if (drawingCanvas != null) {
drawingCanvas.redraw();
}
}
/**
* 重做操作
*/
@FXML
public void onRedo(ActionEvent actionEvent) {
commandHistory.redo();
if (drawingCanvas != null) {
drawingCanvas.redraw();
}
}
/**
* 更新撤销和重做按钮的状态
*/
private void updateUndoRedoButtons() {
// 确保菜单项不为null
if (undoMenuItem != null) {
undoMenuItem.setDisable(!commandHistory.canUndo());
}
if (redoMenuItem != null) {
redoMenuItem.setDisable(!commandHistory.canRedo());
}
}
/**
* 获取命令历史
*/
public CommandHistory getCommandHistory() {
return commandHistory;
}
/**
* 显示警告对话框
*/
@@ -480,4 +688,70 @@ public class Controller implements Initializable {
alert.setContentText(message);
alert.showAndWait();
}
/**
* 应用当前主题到界面
*/
private void applyTheme() {
if (themeManager == null) {
return;
}
// 应用菜单栏样式
VBox menuBarBox = (VBox) scenePane.lookup("VBox");
if (menuBarBox != null) {
menuBarBox.setStyle(themeManager.getMenuBarStyle());
}
// 应用主场景背景
scenePane.setStyle(themeManager.getMainSceneStyle());
}
/**
* 浅色主题
*/
@FXML
public void onThemeLight(ActionEvent actionEvent) {
themeManager.switchTheme("Light");
}
/**
* 深色主题
*/
@FXML
public void onThemeDark(ActionEvent actionEvent) {
themeManager.switchTheme("Dark");
}
/**
* 蓝色主题
*/
@FXML
public void onThemeBlue(ActionEvent actionEvent) {
themeManager.switchTheme("Blue");
}
/**
* 绿色主题
*/
@FXML
public void onThemeGreen(ActionEvent actionEvent) {
themeManager.switchTheme("Green");
}
/**
* 紫色主题
*/
@FXML
public void onThemePurple(ActionEvent actionEvent) {
themeManager.switchTheme("Purple");
}
/**
* 橙色主题
*/
@FXML
public void onThemeOrange(ActionEvent actionEvent) {
themeManager.switchTheme("Orange");
}
}
@@ -0,0 +1,27 @@
package dev.bytevibe.hyperpoint;
/**
* 删除对象命令
*/
public class DeleteObjectCommand implements Command {
private PageContent pageContent;
private DrawableObject object;
private int originalIndex;
public DeleteObjectCommand(PageContent pageContent, DrawableObject object) {
this.pageContent = pageContent;
this.object = object;
this.originalIndex = pageContent.getObjectIndex(object);
}
@Override
public void execute() {
pageContent.removeObject(object);
}
@Override
public void undo() {
pageContent.addObjectAt(originalIndex, object);
}
}
@@ -12,6 +12,7 @@ public abstract class DrawableObject implements Serializable {
protected double height;
protected String id;
protected boolean selected;
protected double rotation = 0;
public DrawableObject(double x, double y, double width, double height) {
this.x = x;
@@ -70,6 +71,14 @@ public abstract class DrawableObject implements Serializable {
this.selected = selected;
}
public double getRotation() {
return rotation;
}
public void setRotation(double rotation) {
this.rotation = (rotation % 360 + 360) % 360;
}
/**
* 检查点是否在对象内
*/
@@ -14,6 +14,10 @@ import javafx.scene.text.FontWeight;
* 绘图Canvas组件,用于渲染和交互所有可绘制对象
*/
public class DrawingCanvas extends Pane {
// 页面标准尺寸(与全屏播放保持一致)
public static final double PAGE_WIDTH = 1024;
public static final double PAGE_HEIGHT = 768;
// 缩放点的枚举
enum ResizePoint {
TOP_LEFT, TOP_RIGHT, BOTTOM_LEFT, BOTTOM_RIGHT, NONE
@@ -24,6 +28,9 @@ public class DrawingCanvas extends Pane {
private DrawableObject selectedObject;
private double lastX, lastY;
private Runnable onSelectionChanged;
private CommandHistory commandHistory;
private double startX, startY, startWidth, startHeight;
private SlidePage currentPage;
// 缩放点有关的属性
private ResizePoint resizePoint = ResizePoint.NONE;
@@ -32,7 +39,7 @@ public class DrawingCanvas extends Pane {
public DrawingCanvas(PageContent pageContent) {
this.pageContent = pageContent;
this.canvas = new Canvas();
this.canvas = new Canvas(PAGE_WIDTH, PAGE_HEIGHT);
getChildren().add(canvas);
@@ -41,18 +48,11 @@ public class DrawingCanvas extends Pane {
setOnMouseDragged(this::handleMouseDragged);
setOnMouseReleased(this::handleMouseReleased);
setStyle("-fx-border-color: #e0e0e0; -fx-border-width: 1;");
// 绑定Canvas大小到Pane大小
widthProperty().addListener((obs, oldVal, newVal) -> {
canvas.setWidth(newVal.doubleValue());
redraw();
});
heightProperty().addListener((obs, oldVal, newVal) -> {
canvas.setHeight(newVal.doubleValue());
redraw();
});
// 设置Pane的首选大小为页面尺寸,这样编辑界面显示时会按页面比例显示
setPrefWidth(PAGE_WIDTH);
setPrefHeight(PAGE_HEIGHT);
// 初始绘制
redraw();
@@ -67,12 +67,22 @@ public class DrawingCanvas extends Pane {
// 如果已经有选中对象,先检查是否点击在调整点上
if (selectedObject != null) {
resizePoint = getResizePointAt(lastX, lastY, selectedObject);
if (resizePoint != ResizePoint.NONE) {
// 点击在调整点上,进入缩放模式
return;
}
resizePoint = getResizePointAt(lastX, lastY, selectedObject);
if (resizePoint != ResizePoint.NONE) {
// 记录初始状态用于创建命令
startX = selectedObject.getX();
startY = selectedObject.getY();
startWidth = selectedObject.getWidth();
startHeight = selectedObject.getHeight();
return;
} else if (isPointInObject(lastX, lastY, selectedObject)) {
// 记录移动前的位置
startX = selectedObject.getX();
startY = selectedObject.getY();
startWidth = selectedObject.getWidth();
startHeight = selectedObject.getHeight();
}
}
// 重置缩放模式
resizePoint = ResizePoint.NONE;
@@ -89,6 +99,14 @@ public class DrawingCanvas extends Pane {
redraw();
}
/**
* 检查判断点是否在对象内
*/
private boolean isPointInObject(double x, double y, DrawableObject obj) {
return x >= obj.getX() && x <= obj.getX() + obj.getWidth() &&
y >= obj.getY() && y <= obj.getY() + obj.getHeight();
}
/**
* 获取点击位置对应的调整点
*/
@@ -197,9 +215,23 @@ public class DrawingCanvas extends Pane {
* 鼠标释放事件处理
*/
private void handleMouseReleased(MouseEvent event) {
// 释放鼠标时,重置缩放模式
resizePoint = ResizePoint.NONE;
// 如果有选中对象且位置或尺寸发生了变化,创建命令
if (selectedObject != null && commandHistory != null) {
if (selectedObject.getX() != startX || selectedObject.getY() != startY ||
selectedObject.getWidth() != startWidth || selectedObject.getHeight() != startHeight) {
Command command = new ModifyObjectCommand(
selectedObject,
startX, startY, startWidth, startHeight,
selectedObject.getX(), selectedObject.getY(),
selectedObject.getWidth(), selectedObject.getHeight()
);
commandHistory.execute(command);
}
}
// 释放鼠标时,重置缩放模式
resizePoint = ResizePoint.NONE;
}
/**
* 添加可绘制对象
@@ -244,6 +276,34 @@ public class DrawingCanvas extends Pane {
this.onSelectionChanged = callback;
}
/**
* 设置命令历史
*/
public void setCommandHistory(CommandHistory commandHistory) {
this.commandHistory = commandHistory;
}
/**
* 获取命令历史
*/
public CommandHistory getCommandHistory() {
return commandHistory;
}
/**
* 设置当前页面
*/
public void setCurrentPage(SlidePage page) {
this.currentPage = page;
}
/**
* 获取当前页面
*/
public SlidePage getCurrentPage() {
return currentPage;
}
/**
* 重新绘制画布
*/
@@ -269,14 +329,30 @@ public class DrawingCanvas extends Pane {
* 绘制单个对象
*/
private void drawObject(GraphicsContext gc, DrawableObject obj, boolean selected) {
if (obj instanceof TextObject) {
drawTextObject(gc, (TextObject) obj, selected);
} else if (obj instanceof ShapeObject) {
drawShapeObject(gc, (ShapeObject) obj, selected);
} else if (obj instanceof ImageObject) {
drawImageObject(gc, (ImageObject) obj, selected);
}
// 保存当前状态
gc.save();
// 计算旋转中心(对象中心)
double centerX = obj.getX() + obj.getWidth() / 2;
double centerY = obj.getY() + obj.getHeight() / 2;
// 应用旋转
gc.translate(centerX, centerY);
gc.rotate(obj.getRotation());
gc.translate(-centerX, -centerY);
// 绘制具体对象
if (obj instanceof TextObject) {
drawTextObject(gc, (TextObject) obj, selected);
} else if (obj instanceof ShapeObject) {
drawShapeObject(gc, (ShapeObject) obj, selected);
} else if (obj instanceof ImageObject) {
drawImageObject(gc, (ImageObject) obj, selected);
}
// 恢复状态
gc.restore();
}
/**
* 绘制文本对象
@@ -412,4 +488,3 @@ public class DrawingCanvas extends Pane {
redraw();
}
}
@@ -1,17 +1,28 @@
package dev.bytevibe.hyperpoint;
import javafx.scene.image.WritableImage;
import javafx.scene.image.PixelReader;
import javafx.scene.layout.Pane;
import com.itextpdf.text.*;
import com.itextpdf.text.pdf.PdfWriter;
import javax.imageio.ImageIO;
import java.awt.Color;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import javax.imageio.ImageIO;
import com.itextpdf.text.BaseColor;
import com.itextpdf.text.Document;
import com.itextpdf.text.DocumentException;
import com.itextpdf.text.Font;
import com.itextpdf.text.Image;
import com.itextpdf.text.Paragraph;
import com.itextpdf.text.RectangleReadOnly;
import com.itextpdf.text.pdf.BaseFont;
import com.itextpdf.text.pdf.PdfContentByte;
import com.itextpdf.text.pdf.PdfWriter;
import javafx.scene.image.PixelReader;
import javafx.scene.image.WritableImage;
import javafx.scene.layout.Pane;
/**
* 导出工具类,用于导出页面为图片和导出幻灯片为PDF
*/
@@ -85,124 +96,299 @@ public class ExportUtil {
* @throws Exception 如果导出失败
*/
public static void exportSlideToPDF(Slide slide, File file) throws Exception {
// 检查文件是否已存在且被占用
if (file.exists()) {
try (FileOutputStream fos = new FileOutputStream(file, false)) {
} catch (IOException e) {
throw new IOException("文件已被占用,请关闭该文件后重试:\n" + file.getAbsolutePath(), e);
}
}
if (slide == null || slide.getPages().isEmpty()) {
throw new IOException("幻灯片没有页面");
}
// 创建PDF文档
Document document = new Document(PageSize.A4);
PdfWriter.getInstance(document, new FileOutputStream(file));
document.open();
try (FileOutputStream fos = new FileOutputStream(file)) {
// 创建PDF文档,使用与编辑界面相同的宽高比
Document document = new Document(new RectangleReadOnly(
(float)DrawingCanvas.PAGE_WIDTH,
(float)DrawingCanvas.PAGE_HEIGHT
));
PdfWriter writer = PdfWriter.getInstance(document, new FileOutputStream(file));
document.open();
PdfContentByte canvas = writer.getDirectContent();
// 为每一页添加内容
for (SlidePage page : slide.getPages()) {
// 添加页面标题
Paragraph title = new Paragraph(page.getTitle(), new com.itextpdf.text.Font(
com.itextpdf.text.Font.FontFamily.HELVETICA, 16, com.itextpdf.text.Font.BOLD));
title.setSpacingAfter(10);
document.add(title);
// 为每一页添加内容
for (SlidePage page : slide.getPages()) {
// 添加页面标题
Paragraph title = new Paragraph(page.getTitle(), new Font(
Font.FontFamily.HELVETICA, 16, Font.BOLD));
title.setSpacingAfter(10);
document.add(title);
// 添加页面内容(如果有的话)
if (page.getContent() != null && !page.getContent().isEmpty()) {
Paragraph content = new Paragraph(page.getContent(), new com.itextpdf.text.Font(
com.itextpdf.text.Font.FontFamily.HELVETICA, 12));
content.setSpacingAfter(10);
document.add(content);
// 添加页面内容对象
addObjectsToDocumentWithPosition(document, canvas, page.getPageContent());
document.newPage();
}
// 添加对象列表
addObjectsToDocument(document, page.getPageContent());
// 每页之间添加分页符
document.newPage();
document.close();
writer.close();
}
document.close();
}
/**
* 将页面内容的对象添加到PDF文档
* 按位置添加对象到PDF文档
*/
private static void addObjectsToDocument(Document document, PageContent pageContent) throws DocumentException {
private static void addObjectsToDocumentWithPosition(Document document, PdfContentByte canvas, PageContent pageContent) throws DocumentException {
float margin = 50;
float pdfWidth = (float)(document.getPageSize().getWidth() - 2 * margin);
float pdfHeight = (float)(document.getPageSize().getHeight() - 2 * margin);
float scaleX = pdfWidth / (float)DrawingCanvas.PAGE_WIDTH;
float scaleY = pdfHeight / (float)DrawingCanvas.PAGE_HEIGHT;
float scale = Math.min(scaleX, scaleY);
for (DrawableObject obj : pageContent.getDrawableObjects()) {
float x = margin + (float)obj.getX() * scale;
float y = margin + (float)(DrawingCanvas.PAGE_HEIGHT - obj.getY() - obj.getHeight()) * scale;
float width = (float)obj.getWidth() * scale;
float height = (float)obj.getHeight() * scale;
if (obj instanceof TextObject) {
addTextObjectToDocument(document, (TextObject) obj);
addTextObjectWithPosition(canvas, (TextObject)obj, x, y, width, height);
} else if (obj instanceof ShapeObject) {
addShapeObjectToDocument(document, (ShapeObject) obj);
addShapeObjectWithPosition(canvas, (ShapeObject)obj, x, y, width, height);
} else if (obj instanceof ImageObject) {
addImageObjectToDocument(document, (ImageObject) obj);
addImageObjectWithPosition(document, canvas, (ImageObject)obj, x, y, width, height);
}
}
}
/**
* 添加文本对象到PDF
* 按位置添加文本对象
*/
private static void addTextObjectToDocument(Document document, TextObject textObj) throws DocumentException {
float fontSize = (float) textObj.getFontSize();
int fontStyle = com.itextpdf.text.Font.NORMAL;
if (textObj.getFontStyle().contains("BOLD")) {
fontStyle |= com.itextpdf.text.Font.BOLD;
private static void addTextObjectWithPosition(PdfContentByte canvas, TextObject textObj, float x, float y, float width, float height) {
try {
// 处理字体
BaseFont baseFont;
String fontFamily = textObj.getFontFamily();
if (fontFamily.contains("Times")) {
baseFont = BaseFont.createFont(BaseFont.TIMES_ROMAN, BaseFont.WINANSI, BaseFont.NOT_EMBEDDED);
} else if (fontFamily.contains("Courier")) {
baseFont = BaseFont.createFont(BaseFont.COURIER, BaseFont.WINANSI, BaseFont.NOT_EMBEDDED);
} else if (fontFamily.contains("Georgia")) {
baseFont = BaseFont.createFont(BaseFont.TIMES_ROMAN, BaseFont.WINANSI, BaseFont.NOT_EMBEDDED);
} else if (fontFamily.contains("Verdana")) {
baseFont = BaseFont.createFont(BaseFont.HELVETICA, BaseFont.WINANSI, BaseFont.NOT_EMBEDDED);
} else {
baseFont = BaseFont.createFont(BaseFont.HELVETICA, BaseFont.WINANSI, BaseFont.NOT_EMBEDDED);
}
// 处理字体样式(粗体/斜体)
int style = Font.NORMAL;
String fontStyle = textObj.getFontStyle();
if (fontStyle.contains("BOLD")) style |= Font.BOLD;
if (fontStyle.contains("ITALIC")) style |= Font.ITALIC;
// 处理文本颜色
BaseColor textColor = BaseColor.BLACK;
if (textObj.getTextColor() != null && !textObj.getTextColor().isEmpty()) {
Color fxColor = Color.decode("#" + textObj.getTextColor());
textColor = new BaseColor(fxColor.getRed(), fxColor.getGreen(), fxColor.getBlue());
}
// 处理旋转
double rotation = textObj.getRotation();
if (rotation != 0) {
canvas.saveState();
canvas.concatCTM(
(float) Math.cos(Math.toRadians(rotation)),
(float) Math.sin(Math.toRadians(rotation)),
(float) -Math.sin(Math.toRadians(rotation)),
(float) Math.cos(Math.toRadians(rotation)),
x, y
);
x = 0;
y = 0;
canvas.beginText();
canvas.setFontAndSize(baseFont, (float) textObj.getFontSize());
canvas.setColorFill(textColor);
canvas.setTextMatrix(x, y + (float) textObj.getFontSize());
canvas.showText(textObj.getText());
canvas.endText();
canvas.restoreState();
} else {
canvas.beginText();
canvas.setFontAndSize(baseFont, (float) textObj.getFontSize());
canvas.setColorFill(textColor);
canvas.setTextMatrix(x, y + (float) textObj.getFontSize());
canvas.showText(textObj.getText());
canvas.endText();
}
} catch (Exception e) {
e.printStackTrace();
}
if (textObj.getFontStyle().contains("ITALIC")) {
fontStyle |= com.itextpdf.text.Font.ITALIC;
}
com.itextpdf.text.Font font = new com.itextpdf.text.Font(
com.itextpdf.text.Font.FontFamily.HELVETICA, fontSize, fontStyle);
Paragraph paragraph = new Paragraph(textObj.getText(), font);
paragraph.setSpacingAfter(5);
document.add(paragraph);
}
/**
* 添加形状对象到PDF
* 按位置添加形状对象
*/
private static void addShapeObjectToDocument(Document document, ShapeObject shapeObj) throws DocumentException {
String shapeInfo = "图形: " + shapeObj.getShapeType().name() +
" (位置: " + (int)shapeObj.getX() + ", " + (int)shapeObj.getY() +
" 大小: " + (int)shapeObj.getWidth() + "x" + (int)shapeObj.getHeight() + ")";
Paragraph paragraph = new Paragraph(shapeInfo, new com.itextpdf.text.Font(
com.itextpdf.text.Font.FontFamily.HELVETICA, 10));
paragraph.setSpacingAfter(5);
document.add(paragraph);
}
/**
* 添加图片对象到PDF
*/
private static void addImageObjectToDocument(Document document, ImageObject imageObj) throws DocumentException {
String imagePath = imageObj.getImagePath();
if (imagePath != null && !imagePath.isEmpty()) {
try {
// 检查文件是否存在
File imageFile = new File(imagePath);
if (imageFile.exists()) {
Image img = Image.getInstance(imagePath);
// 限制图片大小
float maxWidth = document.getPageSize().getWidth() - 40;
float maxHeight = document.getPageSize().getHeight() / 3;
if (img.getWidth() > maxWidth) {
img.scaleToFit(maxWidth, maxHeight);
private static void addShapeObjectWithPosition(PdfContentByte canvas, ShapeObject shapeObj, float x, float y, float width, float height) {
try {
canvas.saveState();
// 设置线条和填充颜色
if (shapeObj.getStrokeColor() != null) {
Color strokeColor = Color.decode("#" + shapeObj.getStrokeColor());
canvas.setColorStroke(new BaseColor(strokeColor.getRed(), strokeColor.getGreen(), strokeColor.getBlue()));
}
if (shapeObj.getFillColor() != null && !shapeObj.getFillColor().isEmpty()) {
Color fillColor = Color.decode("#" + shapeObj.getFillColor());
canvas.setColorFill(new BaseColor(fillColor.getRed(), fillColor.getGreen(), fillColor.getBlue()));
}
canvas.setLineWidth((float)shapeObj.getStrokeWidth());
// 处理旋转
double rotation = shapeObj.getRotation();
if (rotation != 0) {
canvas.saveState();
float centerX = x + width / 2f;
float centerY = y + height / 2f;
canvas.concatCTM(
(float) Math.cos(Math.toRadians(rotation)),
(float) Math.sin(Math.toRadians(rotation)),
(float) -Math.sin(Math.toRadians(rotation)),
(float) Math.cos(Math.toRadians(rotation)),
centerX - centerX * (float) Math.cos(Math.toRadians(rotation)) + centerY * (float) Math.sin(Math.toRadians(rotation)),
centerY - centerX * (float) Math.sin(Math.toRadians(rotation)) - centerY * (float) Math.cos(Math.toRadians(rotation))
);
switch (shapeObj.getShapeType()) {
case LINE:
canvas.moveTo(x, y + height);
canvas.lineTo(x + width, y);
canvas.stroke();
break;
case RECTANGLE:
canvas.rectangle(x, y, width, height);
if (shapeObj.getFillColor() != null && !shapeObj.getFillColor().isEmpty()) {
canvas.fillStroke();
} else {
canvas.stroke();
}
img.setSpacingAfter(10);
document.add(img);
} else {
Paragraph para = new Paragraph("[图片不存在: " + imagePath + "]",
new com.itextpdf.text.Font(com.itextpdf.text.Font.FontFamily.HELVETICA, 10));
para.setSpacingAfter(5);
document.add(para);
break;
case CIRCLE:
float radius = Math.min(width, height) / 2f;
centerX = x + radius;
centerY = y + radius;
canvas.ellipse(centerX + radius, centerY + radius, centerX - radius, centerY - radius); // 宽高相等才是正圆
if (shapeObj.getFillColor() != null && !shapeObj.getFillColor().isEmpty()) {
canvas.fillStroke();
} else {
canvas.stroke();
}
break;
case ELLIPSE:
float ellipseRadiusX = width / 2f;
float ellipseRadiusY = height / 2f;
float ellipseCenterX = x + ellipseRadiusX;
float ellipseCenterY = y + ellipseRadiusY;
canvas.ellipse(ellipseCenterX + ellipseRadiusX, ellipseCenterY + ellipseRadiusY, ellipseCenterX - ellipseRadiusX, ellipseCenterY - ellipseRadiusY);
if (shapeObj.getFillColor() != null && !shapeObj.getFillColor().isEmpty()) {
canvas.fillStroke();
} else {
canvas.stroke();
}
break;
}
canvas.restoreState();
} else {
switch (shapeObj.getShapeType()) {
case LINE:
canvas.moveTo(x, y + height);
canvas.lineTo(x + width, y);
canvas.stroke();
break;
case RECTANGLE:
canvas.rectangle(x, y, width, height);
if (shapeObj.getFillColor() != null && !shapeObj.getFillColor().isEmpty()) {
canvas.fillStroke();
} else {
canvas.stroke();
}
break;
case CIRCLE:
float radius = Math.min(width, height) / 2f;
float centerX = x + radius;
float centerY = y + radius;
canvas.ellipse(centerX + radius, centerY - radius, centerX - radius, centerY + radius); // 宽高相等才是正圆
if (shapeObj.getFillColor() != null && !shapeObj.getFillColor().isEmpty()) {
canvas.fillStroke();
} else {
canvas.stroke();
}
break;
case ELLIPSE:
float ellipseRadiusX = width / 2f;
float ellipseRadiusY = height / 2f;
float ellipseCenterX = x + ellipseRadiusX;
float ellipseCenterY = y + ellipseRadiusY;
canvas.ellipse(ellipseCenterX + ellipseRadiusX, ellipseCenterY - ellipseRadiusY, ellipseCenterX - ellipseRadiusX, ellipseCenterY + ellipseRadiusY);
if (shapeObj.getFillColor() != null && !shapeObj.getFillColor().isEmpty()) {
canvas.fillStroke();
} else {
canvas.stroke();
}
break;
}
} catch (Exception e) {
Paragraph para = new Paragraph("[无法加载图片: " + e.getMessage() + "]",
new com.itextpdf.text.Font(com.itextpdf.text.Font.FontFamily.HELVETICA, 10));
para.setSpacingAfter(5);
document.add(para);
}
canvas.restoreState();
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 按位置添加图片对象
*/
private static void addImageObjectWithPosition(Document document, PdfContentByte canvas, ImageObject imageObj, float x, float y, float width, float height) {
try {
String imagePath = imageObj.getImagePath();
if (imagePath != null && new File(imagePath).exists()) {
Image img = Image.getInstance(imagePath);
// 设置图片大小
img.scaleAbsolute(width, height);
// 处理旋转
double rotation = imageObj.getRotation();
if (rotation != 0) {
canvas.saveState();
float centerX = x + width / 2f;
float centerY = y + height / 2f;
canvas.concatCTM(
(float) Math.cos(Math.toRadians(rotation)),
(float) Math.sin(Math.toRadians(rotation)),
(float) -Math.sin(Math.toRadians(rotation)),
(float) Math.cos(Math.toRadians(rotation)),
centerX - centerX * (float) Math.cos(Math.toRadians(rotation)) + centerY * (float) Math.sin(Math.toRadians(rotation)),
centerY - centerX * (float) Math.sin(Math.toRadians(rotation)) - centerY * (float) Math.cos(Math.toRadians(rotation))
);
img.setAbsolutePosition(x, y);
canvas.addImage(img);
canvas.restoreState();
} else {
img.setAbsolutePosition(x, y);
canvas.addImage(img);
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
@@ -1,10 +1,16 @@
package dev.bytevibe.hyperpoint;
import com.google.gson.*;
import javafx.scene.paint.Color;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.*;
import java.util.*;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
/**
* JSON序列化工具,用于将演示文稿保存和加载为JSON格式
@@ -180,6 +186,7 @@ public class JsonSerializationUtil {
ImageObject imgObj = (ImageObject) obj;
objJson.addProperty("imagePath", imgObj.getImagePath());
}
objJson.addProperty("rotation", obj.getRotation()); // 添加旋转角度
return objJson;
}
@@ -238,6 +245,11 @@ public class JsonSerializationUtil {
obj = new ImageObject(x, y, width, height, imagePath);
}
// 读取旋转角度并设置
if (objJson.has("rotation")) {
obj.setRotation(objJson.get("rotation").getAsDouble());
}
if (obj != null) {
obj.setId(id);
}
@@ -0,0 +1,41 @@
package dev.bytevibe.hyperpoint;
/**
* 修改对象命令
*/
public class ModifyObjectCommand implements Command {
private DrawableObject object;
private double oldX, oldY, oldWidth, oldHeight;
private double newX, newY, newWidth, newHeight;
public ModifyObjectCommand(DrawableObject object,
double oldX, double oldY, double oldWidth, double oldHeight,
double newX, double newY, double newWidth, double newHeight) {
this.object = object;
this.oldX = oldX;
this.oldY = oldY;
this.oldWidth = oldWidth;
this.oldHeight = oldHeight;
this.newX = newX;
this.newY = newY;
this.newWidth = newWidth;
this.newHeight = newHeight;
}
@Override
public void execute() {
object.setX(newX);
object.setY(newY);
object.setWidth(newWidth);
object.setHeight(newHeight);
}
@Override
public void undo() {
object.setX(oldX);
object.setY(oldY);
object.setWidth(oldWidth);
object.setHeight(oldHeight);
}
}
@@ -0,0 +1,124 @@
package dev.bytevibe.hyperpoint;
/**
* 修改对象属性命令(除位置和尺寸外的其他属性)
*/
public class ModifyObjectPropertyCommand implements Command {
private DrawableObject object;
private String oldText;
private String newText;
private String oldFontFamily;
private String newFontFamily;
private double oldFontSize;
private double newFontSize;
private String oldFontStyle;
private String newFontStyle;
private String oldTextColor;
private String newTextColor;
private String oldFillColor;
private String newFillColor;
private String oldStrokeColor;
private String newStrokeColor;
private double oldStrokeWidth;
private double newStrokeWidth;
private double oldRotation;
private double newRotation;
private PropertyType propertyType;
// 定义属性类型枚举
public enum PropertyType {
TEXT, FONT, COLOR, ROTATION, SHAPE_STYLE
}
// 文本内容修改构造函数
public ModifyObjectPropertyCommand(TextObject object, String oldText, String newText, int nothing) {
this.object = object;
this.oldText = oldText;
this.newText = newText;
this.propertyType = PropertyType.TEXT;
}
// 字体样式修改构造函数
public ModifyObjectPropertyCommand(TextObject object,
String oldFontFamily, String newFontFamily,
double oldFontSize, double newFontSize,
String oldFontStyle, String newFontStyle) {
this.object = object;
this.oldFontFamily = oldFontFamily;
this.newFontFamily = newFontFamily;
this.oldFontSize = oldFontSize;
this.newFontSize = newFontSize;
this.oldFontStyle = oldFontStyle;
this.newFontStyle = newFontStyle;
this.propertyType = PropertyType.FONT;
}
// 文本颜色修改构造函数
public ModifyObjectPropertyCommand(TextObject object, String oldTextColor, String newTextColor) {
this.object = object;
this.oldTextColor = oldTextColor;
this.newTextColor = newTextColor;
this.propertyType = PropertyType.COLOR;
}
// 形状样式修改构造函数
public ModifyObjectPropertyCommand(ShapeObject object,
String oldFillColor, String newFillColor,
String oldStrokeColor, String newStrokeColor,
double oldStrokeWidth, double newStrokeWidth) {
this.object = object;
this.oldFillColor = oldFillColor;
this.newFillColor = newFillColor;
this.oldStrokeColor = oldStrokeColor;
this.newStrokeColor = newStrokeColor;
this.oldStrokeWidth = oldStrokeWidth;
this.newStrokeWidth = newStrokeWidth;
this.propertyType = PropertyType.SHAPE_STYLE;
}
// 旋转属性修改构造函数
public ModifyObjectPropertyCommand(DrawableObject object, double oldRotation, double newRotation) {
this.object = object;
this.oldRotation = oldRotation;
this.newRotation = newRotation;
this.propertyType = PropertyType.ROTATION;
}
@Override
public void execute() {
applyChanges(newText, newFontFamily, newFontSize, newFontStyle,
newTextColor, newFillColor, newStrokeColor, newStrokeWidth, newRotation);
}
@Override
public void undo() {
applyChanges(oldText, oldFontFamily, oldFontSize, oldFontStyle,
oldTextColor, oldFillColor, oldStrokeColor, oldStrokeWidth, oldRotation);
}
private void applyChanges(String text, String fontFamily, double fontSize, String fontStyle,
String textColor, String fillColor, String strokeColor,
double strokeWidth, double rotation) {
// 应用旋转(所有对象都有旋转属性)
object.setRotation(rotation);
// 根据对象类型应用其他属性
if (object instanceof TextObject) {
TextObject textObj = (TextObject) object;
if (propertyType == PropertyType.TEXT) {
textObj.setText(text);
} else if (propertyType == PropertyType.FONT) {
textObj.setFontFamily(fontFamily);
textObj.setFontSize(fontSize);
textObj.setFontStyle(fontStyle);
} else if (propertyType == PropertyType.COLOR) {
textObj.setTextColor(textColor);
}
} else if (object instanceof ShapeObject && propertyType == PropertyType.SHAPE_STYLE) {
ShapeObject shapeObj = (ShapeObject) object;
shapeObj.setFillColor(fillColor);
shapeObj.setStrokeColor(strokeColor);
shapeObj.setStrokeWidth(strokeWidth);
}
}
}
@@ -61,5 +61,20 @@ public class PageContent implements Serializable {
public void clear() {
drawableObjects.clear();
}
}
/**
* 获取对象在列表中的索引
*/
public int getObjectIndex(DrawableObject object) {
return drawableObjects.indexOf(object);
}
/**
* 在指定索引处添加对象
*/
public void addObjectAt(int index, DrawableObject object) {
if (object != null && index >= 0 && index <= drawableObjects.size()) {
drawableObjects.add(index, object);
}
}
}
@@ -0,0 +1,699 @@
package dev.bytevibe.hyperpoint;
import javafx.animation.Animation;
import javafx.animation.AnimationTimer;
import javafx.scene.Scene;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.input.MouseEvent;
import javafx.scene.input.ScrollEvent;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
import javafx.stage.StageStyle;
/**
* 全屏演示窗口,支持翻页和动画效果
*/
public class PresentationWindow {
private final Slide slide;
private final Stage stage;
private final Canvas canvas;
private final StackPane root;
private int currentPageIndex = 0;
private TransitionEffect currentTransition = TransitionEffect.FADE;
private boolean isTransitioning = false;
public PresentationWindow(Slide slide, TransitionEffect transition) {
this.slide = slide;
this.currentTransition = transition;
this.stage = new Stage();
this.canvas = new Canvas();
this.root = new StackPane();
initializeStage();
}
private void initializeStage() {
// 页面标准宽高比 (4:3) - 使用与编辑界面相同的尺寸
final double PAGE_WIDTH = DrawingCanvas.PAGE_WIDTH; // 1024
final double PAGE_HEIGHT = DrawingCanvas.PAGE_HEIGHT; // 768
// 设置全屏,获取屏幕分辨率
stage.setFullScreen(true);
stage.setFullScreenExitHint("");
stage.initStyle(StageStyle.UNDECORATED);
// 绑定Canvas大小到rootCanvas会根据root大小自动调整
canvas.widthProperty().bind(root.widthProperty());
canvas.heightProperty().bind(root.heightProperty());
// 添加键盘和鼠标事件处理
root.setOnKeyPressed(this::handleKeyEvent);
root.setOnMouseClicked(this::handleMouseClick);
root.setOnScroll(this::handleMouseScroll);
root.getChildren().add(canvas);
root.setStyle("-fx-background-color: #000000;");
root.setFocusTraversable(true);
Scene scene = new Scene(root);
stage.setScene(scene);
// 监听Scene大小变化,计算缩放因子
scene.widthProperty().addListener((obs, oldVal, newVal) -> drawCurrentPage(null));
scene.heightProperty().addListener((obs, oldVal, newVal) -> drawCurrentPage(null));
// 显示第一页
drawCurrentPage(null);
}
/**
* 显示演示窗口
*/
public void show() {
stage.show();
root.requestFocus();
}
/**
* 处理键盘事件
*/
private void handleKeyEvent(KeyEvent event) {
if (event.getCode() == KeyCode.RIGHT || event.getCode() == KeyCode.SPACE) {
nextPage();
} else if (event.getCode() == KeyCode.LEFT) {
previousPage();
} else if (event.getCode() == KeyCode.ESCAPE) {
exitPresentation();
}
}
/**
* 处理鼠标点击事件
*/
private void handleMouseClick(MouseEvent event) {
if (event.getClickCount() == 1) {
nextPage();
}
}
/**
* 处理鼠标滚轮事件
*/
private void handleMouseScroll(ScrollEvent event) {
if (event.getDeltaY() < 0) {
nextPage();
} else {
previousPage();
}
event.consume();
}
/**
* 下一页
*/
private void nextPage() {
if (isTransitioning || currentPageIndex >= slide.getPages().size() - 1) {
return;
}
currentPageIndex++;
playTransition(currentTransition);
}
/**
* 上一页
*/
private void previousPage() {
if (isTransitioning || currentPageIndex <= 0) {
return;
}
currentPageIndex--;
playTransition(currentTransition);
}
/**
* 播放过渡动画
*/
private void playTransition(TransitionEffect effect) {
isTransitioning = true;
switch (effect) {
case FADE:
playFadeTransition();
break;
case PUSH_LEFT:
playPushTransition(-canvas.getWidth(), 0);
break;
case PUSH_RIGHT:
playPushTransition(canvas.getWidth(), 0);
break;
case PUSH_DOWN:
playPushTransition(0, canvas.getHeight());
break;
case PUSH_UP:
playPushTransition(0, -canvas.getHeight());
break;
case ZOOM_IN:
playZoomTransition(0.5, 1.0);
break;
case ZOOM_OUT:
playZoomTransition(1.0, 0.5);
break;
case ROTATE:
playRotateTransition();
break;
default:
drawCurrentPage(null);
isTransitioning = false;
}
}
/**
* 淡出淡入动画
*/
private void playFadeTransition() {
int duration = currentTransition.getDuration();
int halfDuration = duration / 2;
// 保存当前页面索引
int previousPageIndex = currentPageIndex - 1;
if (previousPageIndex < 0) previousPageIndex = slide.getPages().size() - 1;
// 淡出旧页面,显示黑屏,淡入新页面
AnimationTimer animationTimer = new AnimationTimer() {
private long startTime = System.currentTimeMillis();
private final long fadeOutDuration = halfDuration;
private final long fadeInDuration = halfDuration;
@Override
public void handle(long now) {
long elapsed = System.currentTimeMillis() - startTime;
if (elapsed < fadeOutDuration) {
// 淡出阶段
double progress = (double) elapsed / fadeOutDuration;
drawFadeTransition(progress, true);
} else if (elapsed < fadeOutDuration + fadeInDuration) {
// 淡入阶段
double progress = (double) (elapsed - fadeOutDuration) / fadeInDuration;
drawFadeTransition(progress, false);
} else {
// 动画完成
drawCurrentPage(null);
isTransitioning = false;
stop();
}
}
};
animationTimer.start();
}
/**
* 绘制淡出淡入过渡
*/
private void drawFadeTransition(double progress, boolean fadeOut) {
GraphicsContext gc = canvas.getGraphicsContext2D();
double canvasWidth = canvas.getWidth();
double canvasHeight = canvas.getHeight();
final double PAGE_WIDTH = DrawingCanvas.PAGE_WIDTH;
final double PAGE_HEIGHT = DrawingCanvas.PAGE_HEIGHT;
double scaleX = canvasWidth / PAGE_WIDTH;
double scaleY = canvasHeight / PAGE_HEIGHT;
double scale = Math.min(scaleX, scaleY);
double scaledWidth = PAGE_WIDTH * scale;
double scaledHeight = PAGE_HEIGHT * scale;
double offsetX = (canvasWidth - scaledWidth) / 2;
double offsetY = (canvasHeight - scaledHeight) / 2;
if (fadeOut) {
// 绘制当前页面并淡出
drawCurrentPage(null);
gc.setFill(Color.BLACK.interpolate(Color.TRANSPARENT, 1 - progress));
gc.fillRect(0, 0, canvasWidth, canvasHeight);
} else {
// 绘制黑屏并淡入下一页
gc.setFill(Color.BLACK);
gc.fillRect(0, 0, canvasWidth, canvasHeight);
gc.setFill(Color.WHITE);
gc.fillRect(offsetX, offsetY, scaledWidth, scaledHeight);
gc.save();
gc.translate(offsetX, offsetY);
gc.scale(scale, scale);
SlidePage page = slide.getPages().get(currentPageIndex);
for (DrawableObject obj : page.getPageContent().getDrawableObjects()) {
drawObject(gc, obj);
}
gc.restore();
// 淡入效果
gc.setFill(Color.color(1, 1, 1, 1 - progress));
gc.fillRect(0, 0, canvasWidth, canvasHeight);
}
}
/**
* 推送动画
*/
private void playPushTransition(double offsetX, double offsetY) {
int duration = currentTransition.getDuration();
AnimationTimer animationTimer = new AnimationTimer() {
private long startTime = System.currentTimeMillis();
@Override
public void handle(long now) {
long elapsed = System.currentTimeMillis() - startTime;
if (elapsed < duration) {
double progress = (double) elapsed / duration;
drawPushTransition(progress, offsetX, offsetY);
} else {
drawCurrentPage(null);
isTransitioning = false;
stop();
}
}
};
animationTimer.start();
}
/**
* 绘制推送过渡
*/
private void drawPushTransition(double progress, double offsetX, double offsetY) {
GraphicsContext gc = canvas.getGraphicsContext2D();
double canvasWidth = canvas.getWidth();
double canvasHeight = canvas.getHeight();
final double PAGE_WIDTH = DrawingCanvas.PAGE_WIDTH;
final double PAGE_HEIGHT = DrawingCanvas.PAGE_HEIGHT;
double scaleX = canvasWidth / PAGE_WIDTH;
double scaleY = canvasHeight / PAGE_HEIGHT;
double scale = Math.min(scaleX, scaleY);
double scaledWidth = PAGE_WIDTH * scale;
double scaledHeight = PAGE_HEIGHT * scale;
double centerOffsetX = (canvasWidth - scaledWidth) / 2;
double centerOffsetY = (canvasHeight - scaledHeight) / 2;
// 清空背景
gc.setFill(Color.BLACK);
gc.fillRect(0, 0, canvasWidth, canvasHeight);
// 获取前一页索引(确保有效)
int prevPageIndex = currentPageIndex - 1;
if (prevPageIndex < 0) {
prevPageIndex = slide.getPages().size() - 1; // 循环到最后一页
}
// 绘制旧页面
SlidePage prevPage = slide.getPages().get(prevPageIndex);
double slideOffsetX = offsetX * progress;
double slideOffsetY = offsetY * progress;
gc.save();
gc.translate(centerOffsetX + slideOffsetX, centerOffsetY + slideOffsetY);
gc.scale(scale, scale);
gc.setFill(Color.WHITE);
gc.fillRect(0, 0, PAGE_WIDTH, PAGE_HEIGHT);
for (DrawableObject obj : prevPage.getPageContent().getDrawableObjects()) {
drawObject(gc, obj);
}
gc.restore();
// 绘制新页面推入
SlidePage newPage = slide.getPages().get(currentPageIndex);
double newPageOffsetX = centerOffsetX - offsetX * (1 - progress);
double newPageOffsetY = centerOffsetY - offsetY * (1 - progress);
gc.save();
gc.translate(newPageOffsetX, newPageOffsetY);
gc.scale(scale, scale);
gc.setFill(Color.WHITE);
gc.fillRect(0, 0, PAGE_WIDTH, PAGE_HEIGHT);
for (DrawableObject obj : newPage.getPageContent().getDrawableObjects()) {
drawObject(gc, obj);
}
gc.restore();
}
/**
* 缩放动画
*/
private void playZoomTransition(double startScale, double endScale) {
int duration = currentTransition.getDuration();
AnimationTimer animationTimer = new AnimationTimer() {
private long startTime = System.currentTimeMillis();
@Override
public void handle(long now) {
long elapsed = System.currentTimeMillis() - startTime;
if (elapsed < duration) {
double progress = (double) elapsed / duration;
double currentScale = startScale + (endScale - startScale) * progress;
drawZoomTransition(currentScale, progress);
} else {
drawCurrentPage(null);
isTransitioning = false;
stop();
}
}
};
animationTimer.start();
}
/**
* 绘制缩放过渡
*/
private void drawZoomTransition(double zoomScale, double progress) {
GraphicsContext gc = canvas.getGraphicsContext2D();
double canvasWidth = canvas.getWidth();
double canvasHeight = canvas.getHeight();
final double PAGE_WIDTH = DrawingCanvas.PAGE_WIDTH;
final double PAGE_HEIGHT = DrawingCanvas.PAGE_HEIGHT;
double scaleX = canvasWidth / PAGE_WIDTH;
double scaleY = canvasHeight / PAGE_HEIGHT;
double scale = Math.min(scaleX, scaleY);
double scaledWidth = PAGE_WIDTH * scale;
double scaledHeight = PAGE_HEIGHT * scale;
double offsetX = (canvasWidth - scaledWidth) / 2;
double offsetY = (canvasHeight - scaledHeight) / 2;
// 清空背景
gc.setFill(Color.BLACK);
gc.fillRect(0, 0, canvasWidth, canvasHeight);
// 获取前一页索引(确保有效)
int prevPageIndex = currentPageIndex - 1;
if (prevPageIndex < 0) {
prevPageIndex = slide.getPages().size() - 1; // 循环到最后一页
}
// 绘制旧页面,逐渐缩小
SlidePage prevPage = slide.getPages().get(prevPageIndex);
double oldPageScale = scale * (1 - progress * 0.3); // 缩小30%
double oldPageWidth = PAGE_WIDTH * oldPageScale;
double oldPageHeight = PAGE_HEIGHT * oldPageScale;
double oldPageOffsetX = (canvasWidth - oldPageWidth) / 2;
double oldPageOffsetY = (canvasHeight - oldPageHeight) / 2;
gc.save();
gc.translate(oldPageOffsetX, oldPageOffsetY);
gc.scale(oldPageScale / scale, oldPageScale / scale);
gc.setFill(Color.WHITE);
gc.fillRect(0, 0, PAGE_WIDTH, PAGE_HEIGHT);
for (DrawableObject obj : prevPage.getPageContent().getDrawableObjects()) {
drawObject(gc, obj);
}
gc.restore();
// 绘制新页面,逐渐放大
SlidePage newPage = slide.getPages().get(currentPageIndex);
double newPageScale = scale * (0.7 + progress * 0.3); // 从70%放大到100%
double newPageWidth = PAGE_WIDTH * newPageScale;
double newPageHeight = PAGE_HEIGHT * newPageScale;
double newPageOffsetX = (canvasWidth - newPageWidth) / 2;
double newPageOffsetY = (canvasHeight - newPageHeight) / 2;
gc.save();
gc.translate(newPageOffsetX, newPageOffsetY);
gc.scale(newPageScale / scale, newPageScale / scale);
gc.setFill(Color.WHITE);
gc.fillRect(0, 0, PAGE_WIDTH, PAGE_HEIGHT);
for (DrawableObject obj : newPage.getPageContent().getDrawableObjects()) {
drawObject(gc, obj);
}
gc.restore();
}
/**
* 旋转动画
*/
private void playRotateTransition() {
int duration = currentTransition.getDuration();
AnimationTimer animationTimer = new AnimationTimer() {
private long startTime = System.currentTimeMillis();
@Override
public void handle(long now) {
long elapsed = System.currentTimeMillis() - startTime;
if (elapsed < duration) {
double progress = (double) elapsed / duration;
double rotation = progress * 360; // 360度旋转
drawRotateTransition(rotation, progress);
} else {
drawCurrentPage(null);
isTransitioning = false;
stop();
}
}
};
animationTimer.start();
}
/**
* 绘制旋转过渡
*/
private void drawRotateTransition(double rotation, double progress) {
GraphicsContext gc = canvas.getGraphicsContext2D();
double canvasWidth = canvas.getWidth();
double canvasHeight = canvas.getHeight();
final double PAGE_WIDTH = DrawingCanvas.PAGE_WIDTH;
final double PAGE_HEIGHT = DrawingCanvas.PAGE_HEIGHT;
double scaleX = canvasWidth / PAGE_WIDTH;
double scaleY = canvasHeight / PAGE_HEIGHT;
double scale = Math.min(scaleX, scaleY);
double scaledWidth = PAGE_WIDTH * scale;
double scaledHeight = PAGE_HEIGHT * scale;
double offsetX = (canvasWidth - scaledWidth) / 2;
double offsetY = (canvasHeight - scaledHeight) / 2;
// 清空背景
gc.setFill(Color.BLACK);
gc.fillRect(0, 0, canvasWidth, canvasHeight);
// 绘制旋转的页面
SlidePage newPage = slide.getPages().get(currentPageIndex);
gc.save();
// 移到中心,旋转,再移回
double centerX = offsetX + scaledWidth / 2;
double centerY = offsetY + scaledHeight / 2;
gc.translate(centerX, centerY);
gc.rotate(rotation);
gc.translate(-scaledWidth / 2, -scaledHeight / 2);
gc.scale(scale, scale);
gc.setFill(Color.WHITE);
gc.fillRect(0, 0, PAGE_WIDTH, PAGE_HEIGHT);
for (DrawableObject obj : newPage.getPageContent().getDrawableObjects()) {
drawObject(gc, obj);
}
gc.restore();
}
/**
* 绘制当前页面
*/
private void drawCurrentPage(Animation animation) {
if (currentPageIndex < 0 || currentPageIndex >= slide.getPages().size()) {
return;
}
GraphicsContext gc = canvas.getGraphicsContext2D();
double canvasWidth = canvas.getWidth();
double canvasHeight = canvas.getHeight();
// 页面标准宽高比
final double PAGE_WIDTH = DrawingCanvas.PAGE_WIDTH;
final double PAGE_HEIGHT = DrawingCanvas.PAGE_HEIGHT;
// 计算缩放因子,保持宽高比
double scaleX = canvasWidth / PAGE_WIDTH;
double scaleY = canvasHeight / PAGE_HEIGHT;
double scale = Math.min(scaleX, scaleY);
// 计算内容区域的实际宽高
double scaledWidth = PAGE_WIDTH * scale;
double scaledHeight = PAGE_HEIGHT * scale;
// 计算居中偏移
double offsetX = (canvasWidth - scaledWidth) / 2;
double offsetY = (canvasHeight - scaledHeight) / 2;
// 清空背景(黑色)
gc.setFill(Color.BLACK);
gc.fillRect(0, 0, canvasWidth, canvasHeight);
// 白色背景区域
gc.setFill(Color.WHITE);
gc.fillRect(offsetX, offsetY, scaledWidth, scaledHeight);
SlidePage page = slide.getPages().get(currentPageIndex);
// 保存绘图上下文
gc.save();
// 应用缩放和平移变换
gc.translate(offsetX, offsetY);
gc.scale(scale, scale);
// 绘制所有对象
for (DrawableObject obj : page.getPageContent().getDrawableObjects()) {
drawObject(gc, obj);
}
gc.restore();
// 绘制页码(不受缩放影响)
drawPageNumber(gc, canvasWidth, canvasHeight);
}
/**
* 绘制单个对象
*/
private void drawObject(GraphicsContext gc, DrawableObject obj) {
gc.save();
double centerX = obj.getX() + obj.getWidth() / 2;
double centerY = obj.getY() + obj.getHeight() / 2;
gc.translate(centerX, centerY);
gc.rotate(obj.getRotation());
gc.translate(-centerX, -centerY);
if (obj instanceof TextObject) {
drawTextObject(gc, (TextObject) obj);
} else if (obj instanceof ShapeObject) {
drawShapeObject(gc, (ShapeObject) obj);
} else if (obj instanceof ImageObject) {
drawImageObject(gc, (ImageObject) obj);
}
gc.restore();
}
/**
* 绘制文本对象
*/
private void drawTextObject(GraphicsContext gc, TextObject textObj) {
gc.setFill(textObj.getTextColorAsColor());
gc.setFont(new javafx.scene.text.Font(textObj.getFontFamily(), textObj.getFontSize()));
double lineHeight = textObj.getFontSize() * 1.2;
double currentY = textObj.getY();
for (String line : textObj.getLayoutLines()) {
currentY += lineHeight;
gc.fillText(line, textObj.getX() + 5, currentY);
}
}
/**
* 绘制形状对象
*/
private void drawShapeObject(GraphicsContext gc, ShapeObject shapeObj) {
gc.setFill(Color.web("#" + shapeObj.getFillColor()));
gc.setStroke(Color.web("#" + shapeObj.getStrokeColor()));
gc.setLineWidth(shapeObj.getStrokeWidth());
switch (shapeObj.getShapeType()) {
case RECTANGLE:
gc.fillRect(shapeObj.getX(), shapeObj.getY(), shapeObj.getWidth(), shapeObj.getHeight());
gc.strokeRect(shapeObj.getX(), shapeObj.getY(), shapeObj.getWidth(), shapeObj.getHeight());
break;
case CIRCLE:
double radius = Math.min(shapeObj.getWidth(), shapeObj.getHeight()) / 2;
gc.fillOval(shapeObj.getX(), shapeObj.getY(), shapeObj.getWidth(), shapeObj.getHeight());
gc.strokeOval(shapeObj.getX(), shapeObj.getY(), shapeObj.getWidth(), shapeObj.getHeight());
break;
case ELLIPSE:
gc.fillOval(shapeObj.getX(), shapeObj.getY(), shapeObj.getWidth(), shapeObj.getHeight());
gc.strokeOval(shapeObj.getX(), shapeObj.getY(), shapeObj.getWidth(), shapeObj.getHeight());
break;
case LINE:
gc.strokeLine(shapeObj.getX(), shapeObj.getY(),
shapeObj.getX() + shapeObj.getWidth(), shapeObj.getY() + shapeObj.getHeight());
break;
}
}
/**
* 绘制图片对象
*/
private void drawImageObject(GraphicsContext gc, ImageObject imgObj) {
try {
javafx.scene.image.Image image = new javafx.scene.image.Image("file:" + imgObj.getImagePath());
gc.drawImage(image, imgObj.getX(), imgObj.getY(), imgObj.getWidth(), imgObj.getHeight());
} catch (Exception e) {
// 图片加载失败,显示占位符
gc.setFill(Color.LIGHTGRAY);
gc.fillRect(imgObj.getX(), imgObj.getY(), imgObj.getWidth(), imgObj.getHeight());
gc.setFill(Color.BLACK);
gc.fillText("图片加载失败", imgObj.getX() + 10, imgObj.getY() + 20);
}
}
/**
* 绘制页码(不受缩放影响)
*/
private void drawPageNumber(GraphicsContext gc, double canvasWidth, double canvasHeight) {
gc.setFill(Color.BLACK);
gc.setFont(new javafx.scene.text.Font(16));
String pageNum = String.format("%d / %d", currentPageIndex + 1, slide.getPages().size());
gc.fillText(pageNum, canvasWidth - 100, canvasHeight - 20);
}
/**
* 设置翻页动画
*/
public void setTransitionEffect(TransitionEffect effect) {
this.currentTransition = effect;
}
/**
* 退出演示
*/
private void exitPresentation() {
stage.close();
}
/**
* 获取舞台
*/
public Stage getStage() {
return stage;
}
}
@@ -1,16 +1,22 @@
package dev.bytevibe.hyperpoint;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.fxml.Initializable;
import javafx.scene.control.*;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import java.io.IOException;
import java.net.URL;
import java.util.ResourceBundle;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.fxml.Initializable;
import javafx.scene.control.Button;
import javafx.scene.control.ColorPicker;
import javafx.scene.control.ComboBox;
import javafx.scene.control.Label;
import javafx.scene.control.Slider;
import javafx.scene.control.Spinner;
import javafx.scene.control.TextField;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
/**
* 属性编辑面板,用于编辑选中对象的属性
* 使用FXML加载UI布局
@@ -38,6 +44,10 @@ public class PropertyPanel extends VBox implements Initializable {
private Spinner<Double> strokeWidthSpinner;
@FXML
private Button deleteButton;
@FXML
private Slider rotationSlider;
@FXML
private Label rotationValueLabel;
private DrawingCanvas canvas;
@@ -80,6 +90,20 @@ public class PropertyPanel extends VBox implements Initializable {
"BOLD_ITALIC"
);
// 初始化旋转控制
rotationSlider.setMin(0);
rotationSlider.setMax(360);
rotationSlider.setValue(0);
rotationSlider.setMajorTickUnit(15);
rotationSlider.setMajorTickUnit(90);
rotationSlider.setShowTickMarks(true);
rotationSlider.setShowTickLabels(true);
rotationSlider.valueProperty().addListener((obs, oldVal, newVal) -> {
updateRotation();
rotationValueLabel.setText(String.format("%.0f°", newVal));
});
// 设置默认值
fontFamilyCombo.setValue("Arial");
fontStyleCombo.setValue("NORMAL");
@@ -112,6 +136,7 @@ public class PropertyPanel extends VBox implements Initializable {
DrawableObject selected = canvas.getSelectedObject();
if (selected == null) {
setEditingVisible(false);
rotationSlider.setDisable(true);
objectTypeLabel.setText("未选中对象");
return;
}
@@ -142,6 +167,10 @@ public class PropertyPanel extends VBox implements Initializable {
showImageControls();
}
rotationSlider.setValue(selected.getRotation());
rotationSlider.setDisable(false);
rotationValueLabel.setText(String.format("%.0f°", selected.getRotation()));
// 重新添加监听
textContentField.setOnKeyReleased(e -> updateTextContent());
}
@@ -151,6 +180,7 @@ public class PropertyPanel extends VBox implements Initializable {
*/
private void showTextControls() {
textContentField.setDisable(false);
rotationSlider.setDisable(false);
fontFamilyCombo.setDisable(false);
fontSizeSpinner.setDisable(false);
fontStyleCombo.setDisable(false);
@@ -167,6 +197,7 @@ public class PropertyPanel extends VBox implements Initializable {
*/
private void showShapeControls() {
textContentField.setDisable(true);
rotationSlider.setDisable(false);
fontFamilyCombo.setDisable(true);
fontSizeSpinner.setDisable(true);
fontStyleCombo.setDisable(true);
@@ -183,6 +214,7 @@ public class PropertyPanel extends VBox implements Initializable {
*/
private void showImageControls() {
textContentField.setDisable(true);
rotationSlider.setDisable(false);
fontFamilyCombo.setDisable(true);
fontSizeSpinner.setDisable(true);
fontStyleCombo.setDisable(true);
@@ -213,40 +245,68 @@ public class PropertyPanel extends VBox implements Initializable {
* 更新文本内容
*/
private void updateTextContent() {
DrawableObject selected = canvas.getSelectedObject();
if (selected instanceof TextObject) {
((TextObject) selected).setText(textContentField.getText());
canvas.redraw();
DrawableObject selected = canvas.getSelectedObject();
if (selected instanceof TextObject) {
TextObject textObj = (TextObject) selected;
String oldText = textObj.getText();
String newText = textContentField.getText();
if (!oldText.equals(newText) && canvas.getCommandHistory() != null) {
Command command = new ModifyObjectPropertyCommand(textObj, oldText, newText, 0);
canvas.getCommandHistory().execute(command);
}
}
}
/**
* 更新文本风格
*/
private void updateTextStyle() {
DrawableObject selected = canvas.getSelectedObject();
if (selected instanceof TextObject) {
TextObject textObj = (TextObject) selected;
textObj.setFontFamily(fontFamilyCombo.getValue());
textObj.setFontSize(fontSizeSpinner.getValue());
textObj.setFontStyle(fontStyleCombo.getValue());
canvas.redraw();
DrawableObject selected = canvas.getSelectedObject();
if (selected instanceof TextObject) {
TextObject textObj = (TextObject) selected;
String oldFontFamily = textObj.getFontFamily();
double oldFontSize = textObj.getFontSize();
String oldFontStyle = textObj.getFontStyle();
String newFontFamily = fontFamilyCombo.getValue();
double newFontSize = fontSizeSpinner.getValue();
String newFontStyle = fontStyleCombo.getValue();
if (!oldFontFamily.equals(newFontFamily) || oldFontSize != newFontSize ||
!oldFontStyle.equals(newFontStyle) && canvas.getCommandHistory() != null) {
Command command = new ModifyObjectPropertyCommand(
textObj, oldFontFamily, newFontFamily,
oldFontSize, newFontSize,
oldFontStyle, newFontStyle
);
canvas.getCommandHistory().execute(command);
}
}
}
/**
* 更新文本颜色
*/
private void updateTextColor() {
DrawableObject selected = canvas.getSelectedObject();
if (selected instanceof TextObject) {
if (selected instanceof TextObject && canvas.getCommandHistory() != null) {
TextObject textObj = (TextObject) selected;
String oldColor = textObj.getTextColor();
Color color = textColorPicker.getValue();
String hexColor = String.format("%02X%02X%02X",
String newColor = String.format("%02X%02X%02X",
(int) (color.getRed() * 255),
(int) (color.getGreen() * 255),
(int) (color.getBlue() * 255));
((TextObject) selected).setTextColor(hexColor);
canvas.redraw();
if (!oldColor.equals(newColor)) {
Command command = new ModifyObjectPropertyCommand(textObj, oldColor, newColor);
canvas.getCommandHistory().execute(command);
canvas.redraw();
}
}
}
@@ -255,14 +315,26 @@ public class PropertyPanel extends VBox implements Initializable {
*/
private void updateFillColor() {
DrawableObject selected = canvas.getSelectedObject();
if (selected instanceof ShapeObject) {
if (selected instanceof ShapeObject && canvas.getCommandHistory() != null) {
ShapeObject shapeObj = (ShapeObject) selected;
String oldFillColor = shapeObj.getFillColor();
Color color = fillColorPicker.getValue();
String hexColor = String.format("%02X%02X%02X",
String newFillColor = String.format("%02X%02X%02X",
(int) (color.getRed() * 255),
(int) (color.getGreen() * 255),
(int) (color.getBlue() * 255));
((ShapeObject) selected).setFillColor(hexColor);
canvas.redraw();
if (!oldFillColor.equals(newFillColor)) {
Command command = new ModifyObjectPropertyCommand(
shapeObj,
oldFillColor, newFillColor,
shapeObj.getStrokeColor(), shapeObj.getStrokeColor(),
shapeObj.getStrokeWidth(), shapeObj.getStrokeWidth()
);
canvas.getCommandHistory().execute(command);
canvas.redraw();
}
}
}
@@ -271,14 +343,26 @@ public class PropertyPanel extends VBox implements Initializable {
*/
private void updateStrokeColor() {
DrawableObject selected = canvas.getSelectedObject();
if (selected instanceof ShapeObject) {
if (selected instanceof ShapeObject && canvas.getCommandHistory() != null) {
ShapeObject shapeObj = (ShapeObject) selected;
String oldStrokeColor = shapeObj.getStrokeColor();
Color color = strokeColorPicker.getValue();
String hexColor = String.format("%02X%02X%02X",
String newStrokeColor = String.format("%02X%02X%02X",
(int) (color.getRed() * 255),
(int) (color.getGreen() * 255),
(int) (color.getBlue() * 255));
((ShapeObject) selected).setStrokeColor(hexColor);
canvas.redraw();
if (!oldStrokeColor.equals(newStrokeColor)) {
Command command = new ModifyObjectPropertyCommand(
shapeObj,
shapeObj.getFillColor(), shapeObj.getFillColor(),
oldStrokeColor, newStrokeColor,
shapeObj.getStrokeWidth(), shapeObj.getStrokeWidth()
);
canvas.getCommandHistory().execute(command);
canvas.redraw();
}
}
}
@@ -287,9 +371,38 @@ public class PropertyPanel extends VBox implements Initializable {
*/
private void updateStrokeWidth() {
DrawableObject selected = canvas.getSelectedObject();
if (selected instanceof ShapeObject) {
((ShapeObject) selected).setStrokeWidth(strokeWidthSpinner.getValue());
canvas.redraw();
if (selected instanceof ShapeObject && canvas.getCommandHistory() != null) {
ShapeObject shapeObj = (ShapeObject) selected;
double oldWidth = shapeObj.getStrokeWidth();
double newWidth = strokeWidthSpinner.getValue();
if (Math.abs(oldWidth - newWidth) > 0.01) {
Command command = new ModifyObjectPropertyCommand(
shapeObj,
shapeObj.getFillColor(), shapeObj.getFillColor(),
shapeObj.getStrokeColor(), shapeObj.getStrokeColor(),
oldWidth, newWidth
);
canvas.getCommandHistory().execute(command);
canvas.redraw();
}
}
}
/**
* 更新旋转
*/
private void updateRotation() {
DrawableObject selected = canvas.getSelectedObject();
if (selected != null && canvas.getCommandHistory() != null) {
double oldRotation = selected.getRotation();
double newRotation = rotationSlider.getValue();
if (Math.abs(oldRotation - newRotation) > 0.01) {
Command command = new ModifyObjectPropertyCommand(selected, oldRotation, newRotation);
canvas.getCommandHistory().execute(command);
canvas.redraw();
}
}
}
@@ -298,9 +411,15 @@ public class PropertyPanel extends VBox implements Initializable {
*/
private void deleteSelectedObject() {
DrawableObject selected = canvas.getSelectedObject();
if (selected != null) {
canvas.removeObject(selected);
updatePropertyPanel();
if (selected != null && canvas.getCommandHistory() != null) {
SlidePage currentPage = canvas.getCurrentPage();
if (currentPage != null) {
Command command = new DeleteObjectCommand(currentPage.getPageContent(), selected);
canvas.getCommandHistory().execute(command);
canvas.setSelectedObject(null);
canvas.redraw();
}
}
}
}
@@ -0,0 +1,80 @@
package dev.bytevibe.hyperpoint;
import javafx.scene.paint.Color;
/**
* 主题管理器,管理应用的颜色和样式主题
*/
public class Theme {
// 主题名称
private String name;
// 背景颜色
private Color backgroundColor;
// 菜单栏颜色
private Color menuBarColor;
// 菜单栏文字颜色
private Color menuTextColor;
// 页面背景颜色
private Color pageBackgroundColor;
// 边框颜色
private Color borderColor;
// 主色调(用于按钮、选中状态等)
private Color primaryColor;
// 次色调
private Color secondaryColor;
public Theme(String name, Color backgroundColor, Color menuBarColor, Color menuTextColor,
Color pageBackgroundColor, Color borderColor, Color primaryColor, Color secondaryColor) {
this.name = name;
this.backgroundColor = backgroundColor;
this.menuBarColor = menuBarColor;
this.menuTextColor = menuTextColor;
this.pageBackgroundColor = pageBackgroundColor;
this.borderColor = borderColor;
this.primaryColor = primaryColor;
this.secondaryColor = secondaryColor;
}
// Getters and Setters
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public Color getBackgroundColor() { return backgroundColor; }
public void setBackgroundColor(Color backgroundColor) { this.backgroundColor = backgroundColor; }
public Color getMenuBarColor() { return menuBarColor; }
public void setMenuBarColor(Color menuBarColor) { this.menuBarColor = menuBarColor; }
public Color getMenuTextColor() { return menuTextColor; }
public void setMenuTextColor(Color menuTextColor) { this.menuTextColor = menuTextColor; }
public Color getPageBackgroundColor() { return pageBackgroundColor; }
public void setPageBackgroundColor(Color pageBackgroundColor) { this.pageBackgroundColor = pageBackgroundColor; }
public Color getBorderColor() { return borderColor; }
public void setBorderColor(Color borderColor) { this.borderColor = borderColor; }
public Color getPrimaryColor() { return primaryColor; }
public void setPrimaryColor(Color primaryColor) { this.primaryColor = primaryColor; }
public Color getSecondaryColor() { return secondaryColor; }
public void setSecondaryColor(Color secondaryColor) { this.secondaryColor = secondaryColor; }
/**
* 转换颜色为十六进制字符串
*/
public static String colorToHex(Color color) {
return String.format("#%02X%02X%02X",
(int) (color.getRed() * 255),
(int) (color.getGreen() * 255),
(int) (color.getBlue() * 255));
}
}
@@ -0,0 +1,193 @@
package dev.bytevibe.hyperpoint;
import javafx.scene.paint.Color;
import java.util.HashMap;
import java.util.Map;
/**
* 主题管理器,管理和切换应用主题
*/
public class ThemeManager {
private static final ThemeManager instance = new ThemeManager();
private Map<String, Theme> themes;
private Theme currentTheme;
private Runnable onThemeChanged;
private ThemeManager() {
themes = new HashMap<>();
initializeDefaultThemes();
currentTheme = themes.get("Light");
}
/**
* 获取单例实例
*/
public static ThemeManager getInstance() {
return instance;
}
/**
* 初始化默认主题
*/
private void initializeDefaultThemes() {
// 浅色主题
themes.put("Light", new Theme(
"Light",
Color.web("#FFFFFF"), // 背景颜色 - 白色
Color.web("#F5F5F5"), // 菜单栏颜色 - 浅灰
Color.web("#333333"), // 菜单文字 - 深灰
Color.web("#FFFFFF"), // 页面背景 - 白色
Color.web("#CCCCCC"), // 边框色 - 浅灰
Color.web("#007AFF"), // 主色调 - 蓝色
Color.web("#E8E8E8") // 次色调 - 浅灰
));
// 深色主题
themes.put("Dark", new Theme(
"Dark",
Color.web("#2B2B2B"), // 背景颜色 - 深灰
Color.web("#1E1E1E"), // 菜单栏颜色 - 更深灰
Color.web("#E0E0E0"), // 菜单文字 - 浅灰
Color.web("#333333"), // 页面背景 - 深灰
Color.web("#444444"), // 边框色 - 中灰
Color.web("#0A84FF"), // 主色调 - 亮蓝
Color.web("#555555") // 次色调 - 中灰
));
// 蓝色主题
themes.put("Blue", new Theme(
"Blue",
Color.web("#E3F2FD"), // 背景颜色 - 浅蓝
Color.web("#1976D2"), // 菜单栏颜色 - 蓝色
Color.web("#FFFFFF"), // 菜单文字 - 白色
Color.web("#FFFFFF"), // 页面背景 - 白色
Color.web("#1976D2"), // 边框色 - 蓝色
Color.web("#1976D2"), // 主色调 - 蓝色
Color.web("#E3F2FD") // 次色调 - 浅蓝
));
// 绿色主题
themes.put("Green", new Theme(
"Green",
Color.web("#E8F5E9"), // 背景颜色 - 浅绿
Color.web("#388E3C"), // 菜单栏颜色 - 绿色
Color.web("#FFFFFF"), // 菜单文字 - 白色
Color.web("#FFFFFF"), // 页面背景 - 白色
Color.web("#388E3C"), // 边框色 - 绿色
Color.web("#388E3C"), // 主色调 - 绿色
Color.web("#E8F5E9") // 次色调 - 浅绿
));
// 紫色主题
themes.put("Purple", new Theme(
"Purple",
Color.web("#F3E5F5"), // 背景颜色 - 浅紫
Color.web("#7B1FA2"), // 菜单栏颜色 - 紫色
Color.web("#FFFFFF"), // 菜单文字 - 白色
Color.web("#FFFFFF"), // 页面背景 - 白色
Color.web("#7B1FA2"), // 边框色 - 紫色
Color.web("#7B1FA2"), // 主色调 - 紫色
Color.web("#F3E5F5") // 次色调 - 浅紫
));
// 橙色主题
themes.put("Orange", new Theme(
"Orange",
Color.web("#FFE0B2"), // 背景颜色 - 浅橙
Color.web("#F57C00"), // 菜单栏颜色 - 橙色
Color.web("#FFFFFF"), // 菜单文字 - 白色
Color.web("#FFFFFF"), // 页面背景 - 白色
Color.web("#F57C00"), // 边框色 - 橙色
Color.web("#F57C00"), // 主色调 - 橙色
Color.web("#FFE0B2") // 次色调 - 浅橙
));
}
/**
* 获取主题列表
*/
public String[] getThemeNames() {
return themes.keySet().toArray(new String[0]);
}
/**
* 获取指定主题
*/
public Theme getTheme(String name) {
return themes.get(name);
}
/**
* 切换主题
*/
public void switchTheme(String themeName) {
Theme theme = themes.get(themeName);
if (theme != null) {
currentTheme = theme;
notifyThemeChanged();
}
}
/**
* 获取当前主题
*/
public Theme getCurrentTheme() {
return currentTheme;
}
/**
* 设置主题变化回调
*/
public void setOnThemeChanged(Runnable callback) {
this.onThemeChanged = callback;
}
/**
* 通知主题已改变
*/
private void notifyThemeChanged() {
if (onThemeChanged != null) {
onThemeChanged.run();
}
}
/**
* 生成菜单栏样式字符串
*/
public String getMenuBarStyle() {
Theme theme = getCurrentTheme();
String bgColor = Theme.colorToHex(theme.getMenuBarColor());
return String.format("-fx-background-color: %s; -fx-text-fill: %s;",
bgColor, Theme.colorToHex(theme.getMenuTextColor()));
}
/**
* 生成主场景样式字符串
*/
public String getMainSceneStyle() {
Theme theme = getCurrentTheme();
String bgColor = Theme.colorToHex(theme.getBackgroundColor());
return String.format("-fx-background-color: %s;", bgColor);
}
/**
* 生成页面列表样式字符串
*/
public String getPageListStyle() {
Theme theme = getCurrentTheme();
String bgColor = Theme.colorToHex(theme.getPageBackgroundColor());
String borderColor = Theme.colorToHex(theme.getBorderColor());
return String.format("-fx-background-color: %s; -fx-border-color: %s; -fx-border-width: 0 1 0 0;",
bgColor, borderColor);
}
/**
* 生成页面编辑区样式字符串
*/
public String getPageEditStyle() {
Theme theme = getCurrentTheme();
String bgColor = Theme.colorToHex(theme.getBackgroundColor());
return String.format("-fx-background-color: %s; -fx-padding: 10;", bgColor);
}
}
@@ -0,0 +1,68 @@
package dev.bytevibe.hyperpoint;
/**
* 演示/播放模式下支持的翻页动画效果
*/
public enum TransitionEffect {
/**
* 无动画,直接切换
*/
NONE("无动画", 0),
/**
* 淡出/淡入效果
*/
FADE("淡出淡入", 500),
/**
* 从右向左推送效果
*/
PUSH_LEFT("从右推入", 400),
/**
* 从左向右推送效果
*/
PUSH_RIGHT("从左推入", 400),
/**
* 从上向下推送效果
*/
PUSH_DOWN("从上推入", 400),
/**
* 从下向上推送效果
*/
PUSH_UP("从下推入", 400),
/**
* 缩放效果 - 从小到大
*/
ZOOM_IN("缩放放大", 500),
/**
* 缩放效果 - 从大到小
*/
ZOOM_OUT("缩放缩小", 500),
/**
* 旋转效果
*/
ROTATE("旋转翻页", 600);
private final String displayName;
private final int duration; // 毫秒
TransitionEffect(String displayName, int duration) {
this.displayName = displayName;
this.duration = duration;
}
public String getDisplayName() {
return displayName;
}
public int getDuration() {
return duration;
}
}
@@ -26,6 +26,14 @@
</items>
</MenuButton>
<!-- 编辑菜单 -->
<MenuButton mnemonicParsing="false" text="编辑">
<items>
<MenuItem fx:id="undoMenuItem" mnemonicParsing="false" onAction="#onUndo" text="撤销(Ctrl+Z)" />
<MenuItem fx:id="redoMenuItem" mnemonicParsing="false" onAction="#onRedo" text="重做(Ctrl+Y)" />
</items>
</MenuButton>
<!-- 页面菜单 -->
<MenuButton mnemonicParsing="false" text="页面">
<items>
@@ -47,6 +55,30 @@
</items>
</MenuButton>
<!-- 演示菜单 -->
<MenuButton mnemonicParsing="false" text="演示">
<items>
<MenuItem mnemonicParsing="false" onAction="#onStartPresentation" text="开始演示(F5)" />
<MenuItem mnemonicParsing="false" onAction="#onPresentationWithFade" text="淡出淡入效果演示" />
<MenuItem mnemonicParsing="false" onAction="#onPresentationWithPushLeft" text="从右推入效果演示" />
<MenuItem mnemonicParsing="false" onAction="#onPresentationWithPushRight" text="从左推入效果演示" />
<MenuItem mnemonicParsing="false" onAction="#onPresentationWithZoomIn" text="缩放放大效果演示" />
<MenuItem mnemonicParsing="false" onAction="#onPresentationWithRotate" text="旋转翻页效果演示" />
</items>
</MenuButton>
<!-- 主题菜单 -->
<MenuButton mnemonicParsing="false" text="主题">
<items>
<MenuItem mnemonicParsing="false" onAction="#onThemeLight" text="浅色主题" />
<MenuItem mnemonicParsing="false" onAction="#onThemeDark" text="深色主题" />
<MenuItem mnemonicParsing="false" onAction="#onThemeBlue" text="蓝色主题" />
<MenuItem mnemonicParsing="false" onAction="#onThemeGreen" text="绿色主题" />
<MenuItem mnemonicParsing="false" onAction="#onThemePurple" text="紫色主题" />
<MenuItem mnemonicParsing="false" onAction="#onThemeOrange" text="橙色主题" />
</items>
</MenuButton>
<Label text=" | " />
<!-- 退出菜单 -->
@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.Slider?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.ColorPicker?>
<?import javafx.scene.control.ComboBox?>
@@ -23,6 +24,11 @@
<Separator/>
<!-- 旋转角度 -->
<Slider fx:id="rotationSlider" max="360.0" min="0.0" />
<Label text="旋转角度:" />
<Label fx:id="rotationValueLabel" text="0°" />
<!-- 文本内容 -->
<Label text="文本内容:"/>
<TextField fx:id="textContentField"/>
+155
View File
@@ -0,0 +1,155 @@
<mxfile host="app.diagrams.net" agent="Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36" version="29.2.4">
<diagram id="classdiagram" name="类关系图">
<mxGraphModel dx="1437" dy="882" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1400" pageHeight="1000" math="0" shadow="0">
<root>
<mxCell id="0" />
<mxCell id="1" parent="0" />
<mxCell id="presentation" parent="1" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;align=left;fontSize=11;" value="&lt;b&gt;Presentation&lt;/b&gt;&lt;br&gt;- name: String&lt;br&gt;- slides: List&amp;lt;Slide&amp;gt;&lt;br&gt;- currentSlideIndex: int&lt;br&gt;+ getName/setName()&lt;br&gt;+ getSlides()&lt;br&gt;+ addSlide()&lt;br&gt;+ removeSlide()" vertex="1">
<mxGeometry height="140" width="200" x="50" y="50" as="geometry" />
</mxCell>
<mxCell id="slide" parent="1" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;align=left;fontSize=11;" value="&lt;br&gt;&lt;b&gt;Slide&lt;/b&gt;&lt;br&gt;- id: String&lt;br&gt;- name: String&lt;br&gt;- pages: List&amp;lt;SlidePage&amp;gt;&lt;br&gt;- currentPageIndex: int&lt;br&gt;+ getName/setName()&lt;br&gt;+ getPages()&lt;br&gt;+ addPage()&lt;br&gt;+ removePage()" vertex="1">
<mxGeometry height="140" width="200" x="350" y="50" as="geometry" />
</mxCell>
<mxCell id="slidepage" parent="1" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;align=left;fontSize=11;" value="&lt;br&gt;&lt;b&gt;SlidePage&lt;/b&gt;&lt;br&gt;- id: String&lt;br&gt;- title: String&lt;br&gt;- pageContent: PageContent&lt;br&gt;+ getTitle/setTitle()&lt;br&gt;+ getPageContent()" vertex="1">
<mxGeometry height="130" width="200" x="650" y="50" as="geometry" />
</mxCell>
<mxCell id="pagecontent" parent="1" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;align=left;fontSize=11;" value="&lt;b&gt;PageContent&lt;/b&gt;&lt;br&gt;- drawableObjects: List&lt;br&gt;+ addObject()&lt;br&gt;+ removeObject()&lt;br&gt;+ findObjectAt()&lt;br&gt;+ getDrawableObjects()" vertex="1">
<mxGeometry height="130" width="200" x="1010" y="70" as="geometry" />
</mxCell>
<mxCell id="drawableobject" parent="1" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#d4e6f1;strokeColor=#3498db;align=left;fontSize=11;" value="&lt;b&gt;DrawableObject&lt;/b&gt;&lt;br&gt;- x, y: double&lt;br&gt;- width, height: double&lt;br&gt;- id: String&lt;br&gt;- selected: boolean&lt;br&gt;+ getX/setX()&lt;br&gt;+ contains()&lt;br&gt;+ getTypeName()" vertex="1">
<mxGeometry height="150" width="220" x="920" y="470" as="geometry" />
</mxCell>
<mxCell id="textobject" parent="1" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#c8e6c9;strokeColor=#66bb6a;align=left;fontSize=11;" value="&lt;b&gt;TextObject&lt;/b&gt;&lt;br&gt;- text: String&lt;br&gt;- fontFamily: String&lt;br&gt;- fontSize: double&lt;br&gt;- fontStyle: String&lt;br&gt;- textColor: String&lt;br&gt;+ getText/setText()&lt;br&gt;+ getLayoutLines()" vertex="1">
<mxGeometry height="130" width="200" x="390" y="1120" as="geometry" />
</mxCell>
<mxCell id="shapeobject" parent="1" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#c8e6c9;strokeColor=#66bb6a;align=left;fontSize=11;" value="&lt;b&gt;ShapeObject&lt;/b&gt;&lt;br&gt;- shapeType: enum&lt;br&gt;- fillColor: String&lt;br&gt;- strokeColor: String&lt;br&gt;- strokeWidth: double&lt;br&gt;+ getShapeType()&lt;br&gt;+ setFillColor()" vertex="1">
<mxGeometry height="130" width="200" x="1340" y="910" as="geometry" />
</mxCell>
<mxCell id="imageobject" parent="1" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#c8e6c9;strokeColor=#66bb6a;align=left;fontSize=11;" value="&lt;br&gt;&lt;b&gt;ImageObject&lt;/b&gt;&lt;br&gt;- imagePath: String&lt;br&gt;+ getImagePath()&lt;br&gt;+ setImagePath()" vertex="1">
<mxGeometry height="100" width="180" x="1520" y="490" as="geometry" />
</mxCell>
<mxCell id="controller" parent="1" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#bbdefb;strokeColor=#1976d2;align=left;fontSize=10;" value="&lt;b&gt;Controller&lt;/b&gt;&lt;br&gt;&lt;i&gt;implements Initializable&lt;/i&gt;&lt;br&gt;- currentSlide: Slide&lt;br&gt;- drawingCanvas: DrawingCanvas&lt;br&gt;- commandHistory: CommandHistory&lt;br&gt;- themeManager: ThemeManager&lt;br&gt;+ initialize()&lt;br&gt;+ onNewSlide()&lt;br&gt;+ displayPageContent()" vertex="1">
<mxGeometry height="150" width="220" x="160" y="360" as="geometry" />
</mxCell>
<mxCell id="drawingcanvas" parent="1" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#bbdefb;strokeColor=#1976d2;align=left;fontSize=10;" value="&lt;br&gt;&lt;b&gt;DrawingCanvas&lt;/b&gt;&lt;br&gt;&lt;i&gt;extends Pane&lt;/i&gt;&lt;br&gt;- canvas: Canvas&lt;br&gt;- pageContent: PageContent&lt;br&gt;- selectedObject: DrawableObject&lt;br&gt;- commandHistory: CommandHistory&lt;br&gt;+ redraw()&lt;br&gt;+ handleMousePressed()&lt;br&gt;+ setSelectedObject()" vertex="1">
<mxGeometry height="150" width="220" x="580" y="770" as="geometry" />
</mxCell>
<mxCell id="propertypanel" parent="1" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#bbdefb;strokeColor=#1976d2;align=left;fontSize=10;" value="&lt;b&gt;PropertyPanel&lt;/b&gt;&lt;br&gt;&lt;i&gt;extends VBox&lt;/i&gt;&lt;br&gt;- objectTypeLabel: Label&lt;br&gt;- fontFamilyCombo: ComboBox&lt;br&gt;- textColorPicker: ColorPicker&lt;br&gt;- canvas: DrawingCanvas&lt;br&gt;+ updateProperty()&lt;br&gt;+ deleteSelectedObject()" vertex="1">
<mxGeometry height="150" width="220" x="650" y="250" as="geometry" />
</mxCell>
<mxCell id="presentationwindow" parent="1" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#bbdefb;strokeColor=#1976d2;align=left;fontSize=10;" value="&lt;b&gt;PresentationWindow&lt;/b&gt;&lt;br&gt;- slide: Slide&lt;br&gt;- stage: Stage&lt;br&gt;- canvas: Canvas&lt;br&gt;- currentPageIndex: int&lt;br&gt;- currentTransition: TransitionEffect&lt;br&gt;+ show()&lt;br&gt;+ nextPage()&lt;br&gt;+ drawCurrentPage()" vertex="1">
<mxGeometry height="140" width="220" x="1520" y="710" as="geometry" />
</mxCell>
<mxCell id="command" parent="1" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;align=left;fontSize=11;" value="&lt;b&gt;Command&lt;/b&gt;&lt;br&gt;+ execute()&lt;br&gt;+ undo()" vertex="1">
<mxGeometry height="100" width="160" x="20" y="1180" as="geometry" />
</mxCell>
<mxCell id="addcmd" parent="1" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#fce4d6;strokeColor=#ea6b66;align=left;fontSize=10;" value="&lt;br&gt;&lt;b&gt;AddObjectCommand&lt;/b&gt;&lt;br&gt;&lt;i&gt;implements Command&lt;/i&gt;&lt;br&gt;- pageContent: PageContent&lt;br&gt;- object: DrawableObject" vertex="1">
<mxGeometry height="100" width="200" x="130" y="950" as="geometry" />
</mxCell>
<mxCell id="deletecmd" parent="1" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#fce4d6;strokeColor=#ea6b66;align=left;fontSize=10;" value="&lt;b&gt;DeleteObjectCommand&lt;/b&gt;&lt;br&gt;&lt;i&gt;implements Command&lt;/i&gt;&lt;br&gt;- pageContent: PageContent&lt;br&gt;- object: DrawableObject" vertex="1">
<mxGeometry height="100" width="210" x="210" y="1300" as="geometry" />
</mxCell>
<mxCell id="modifycmd" parent="1" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#fce4d6;strokeColor=#ea6b66;align=left;fontSize=10;" value="&lt;br&gt;&lt;b&gt;ModifyObjectCommand&lt;/b&gt;&lt;br&gt;&lt;i&gt;implements Command&lt;/i&gt;&lt;br&gt;- object: DrawableObject&lt;br&gt;- oldState/newState" vertex="1">
<mxGeometry height="100" width="210" x="330" y="830" as="geometry" />
</mxCell>
<mxCell id="commandhistory" parent="1" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;align=left;fontSize=10;" value="&lt;br&gt;&lt;b&gt;CommandHistory&lt;/b&gt;&lt;br&gt;- undoStack: Stack&lt;br&gt;- redoStack: Stack&lt;br&gt;+ execute()&lt;br&gt;+ undo()&lt;br&gt;+ redo()&lt;br&gt;+ canUndo/canRedo()" vertex="1">
<mxGeometry height="130" width="180" x="1160" y="680" as="geometry" />
</mxCell>
<mxCell id="theme" parent="1" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#f0f4c3;strokeColor=#c0ca33;align=left;fontSize=10;" value="&lt;b&gt;Theme&lt;/b&gt;&lt;br&gt;- name: String&lt;br&gt;- backgroundColor: Color&lt;br&gt;- menuBarColor: Color&lt;br&gt;- primaryColor: Color&lt;br&gt;- secondaryColor: Color&lt;br&gt;+ getters/setters" vertex="1">
<mxGeometry height="130" width="200" x="1300" y="50" as="geometry" />
</mxCell>
<mxCell id="thememanager" parent="1" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#f0f4c3;strokeColor=#c0ca33;align=left;fontSize=10;" value="&lt;b&gt;ThemeManager&lt;/b&gt;&lt;br&gt;&lt;i&gt;(Singleton)&lt;/i&gt;&lt;br&gt;- instance: ThemeManager&lt;br&gt;- themes: Map&amp;lt;String, Theme&amp;gt;&lt;br&gt;- currentTheme: Theme&lt;br&gt;+ getInstance()&lt;br&gt;+ switchTheme()" vertex="1">
<mxGeometry height="140" width="200" x="1600" y="300" as="geometry" />
</mxCell>
<mxCell id="edge1" edge="1" parent="1" source="presentation" style="endArrow=open;html=1;endSize=12;startArrow=diamondThin;startSize=14;startFill=1;edgeStyle=orthogonalEdgeStyle;" target="slide" value="包含">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="edge2" edge="1" parent="1" source="slide" style="endArrow=open;html=1;endSize=12;startArrow=diamondThin;startSize=14;startFill=1;edgeStyle=orthogonalEdgeStyle;" target="slidepage" value="包含">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="edge3" edge="1" parent="1" source="slidepage" style="endArrow=open;html=1;endSize=12;startArrow=diamondThin;startSize=14;startFill=1;edgeStyle=orthogonalEdgeStyle;" target="pagecontent" value="包含">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="edge4" edge="1" parent="1" source="pagecontent" style="endArrow=open;html=1;endSize=12;startArrow=diamondThin;startSize=14;startFill=1;edgeStyle=orthogonalEdgeStyle;" target="drawableobject" value="包含">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="edge5" edge="1" parent="1" source="textobject" style="endArrow=block;html=1;endSize=12;edgeStyle=orthogonalEdgeStyle;dashed=0;" target="drawableobject" value="继承">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="edge6" edge="1" parent="1" source="shapeobject" style="endArrow=block;html=1;endSize=12;edgeStyle=orthogonalEdgeStyle;dashed=0;" target="drawableobject" value="继承">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="1070" y="975" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="edge7" edge="1" parent="1" source="imageobject" style="endArrow=block;html=1;endSize=12;edgeStyle=orthogonalEdgeStyle;dashed=0;" target="drawableobject" value="继承">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="edge11" edge="1" parent="1" source="controller" style="endArrow=open;html=1;endSize=12;edgeStyle=orthogonalEdgeStyle;" target="drawingcanvas" value="使用">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="690" y="480" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="edge12" edge="1" parent="1" source="controller" style="endArrow=open;html=1;endSize=12;edgeStyle=orthogonalEdgeStyle;" target="propertypanel" value="使用">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="edge13" edge="1" parent="1" source="drawingcanvas" style="endArrow=open;html=1;endSize=12;edgeStyle=orthogonalEdgeStyle;" target="pagecontent" value="使用">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="1120" y="845" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="edge14" edge="1" parent="1" source="propertypanel" style="endArrow=open;html=1;endSize=12;edgeStyle=orthogonalEdgeStyle;" target="drawingcanvas" value="使用">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="760" y="585" />
<mxPoint x="720" y="585" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="edge15" edge="1" parent="1" source="presentationwindow" style="endArrow=open;html=1;endSize=12;edgeStyle=orthogonalEdgeStyle;" target="slide" value="使用">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="450" y="630" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="edge16" edge="1" parent="1" source="addcmd" style="endArrow=block;html=1;endSize=12;edgeStyle=orthogonalEdgeStyle;dashed=1;" target="command" value="实现">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="edge17" edge="1" parent="1" source="deletecmd" style="endArrow=block;html=1;endSize=12;edgeStyle=orthogonalEdgeStyle;dashed=1;" target="command" value="实现">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="edge18" edge="1" parent="1" source="modifycmd" style="endArrow=block;html=1;endSize=12;edgeStyle=orthogonalEdgeStyle;dashed=1;" target="command" value="实现">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="edge19" edge="1" parent="1" source="commandhistory" style="endArrow=open;html=1;endSize=12;edgeStyle=orthogonalEdgeStyle;" target="command" value="管理">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="edge20" edge="1" parent="1" source="controller" style="endArrow=open;html=1;endSize=12;edgeStyle=orthogonalEdgeStyle;exitX=1.001;exitY=0.624;exitDx=0;exitDy=0;exitPerimeter=0;" target="commandhistory" value="使用">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="edge21" edge="1" parent="1" source="drawingcanvas" style="endArrow=open;html=1;endSize=12;edgeStyle=orthogonalEdgeStyle;" target="commandhistory" value="使用">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="980" y="820" />
<mxPoint x="980" y="760" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="edge22" edge="1" parent="1" source="thememanager" style="endArrow=open;html=1;endSize=12;startArrow=diamondThin;startSize=14;startFill=1;edgeStyle=orthogonalEdgeStyle;" target="theme" value="管理">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="edge23" edge="1" parent="1" source="controller" style="endArrow=open;html=1;endSize=12;edgeStyle=orthogonalEdgeStyle;exitX=1.011;exitY=0.326;exitDx=0;exitDy=0;exitPerimeter=0;" target="thememanager" value="使用">
<mxGeometry relative="1" as="geometry">
<mxPoint x="390" y="410" as="sourcePoint" />
</mxGeometry>
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>