From 4c3ba08c72b76e91e3d1367f947cb63b70a1eb31 Mon Sep 17 00:00:00 2001 From: Gary Gan Date: Mon, 24 Nov 2025 15:41:16 +0800 Subject: [PATCH] =?UTF-8?q?feature:=20=E5=AE=9E=E7=8E=B0=E6=96=87=E5=AD=97?= =?UTF-8?q?=E6=8D=A2=E8=A1=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dev/bytevibe/hyperpoint/Controller.java | 4 +- .../bytevibe/hyperpoint/DrawingCanvas.java | 22 +- .../dev/bytevibe/hyperpoint/TextObject.java | 203 +++++++++++++++++- 3 files changed, 223 insertions(+), 6 deletions(-) diff --git a/src/main/java/dev/bytevibe/hyperpoint/Controller.java b/src/main/java/dev/bytevibe/hyperpoint/Controller.java index 2e4dffe..541bc10 100644 --- a/src/main/java/dev/bytevibe/hyperpoint/Controller.java +++ b/src/main/java/dev/bytevibe/hyperpoint/Controller.java @@ -97,6 +97,8 @@ public class Controller implements Initializable { stage.setTitle("Hyperpoint - " + slideName); } alert.showAndWait(); + } else { + showWarning("无效名称", "幻灯片名称不能为空。"); } } } @@ -348,7 +350,7 @@ public class Controller implements Initializable { * 显示警告对话框 */ private void showWarning(String title, String message) { - Alert alert = new Alert(Alert.AlertType.WARNING); + MyAlert alert = new MyAlert(Alert.AlertType.WARNING); alert.setTitle(title); alert.setHeaderText(title); alert.setContentText(message); diff --git a/src/main/java/dev/bytevibe/hyperpoint/DrawingCanvas.java b/src/main/java/dev/bytevibe/hyperpoint/DrawingCanvas.java index 14302bf..912fcf3 100644 --- a/src/main/java/dev/bytevibe/hyperpoint/DrawingCanvas.java +++ b/src/main/java/dev/bytevibe/hyperpoint/DrawingCanvas.java @@ -41,6 +41,7 @@ public class DrawingCanvas extends Pane { setOnMouseDragged(this::handleMouseDragged); setOnMouseReleased(this::handleMouseReleased); + setStyle("-fx-border-color: #e0e0e0; -fx-border-width: 1;"); // 绑定Canvas大小到Pane大小 @@ -281,14 +282,27 @@ public class DrawingCanvas extends Pane { * 绘制文本对象 */ private void drawTextObject(GraphicsContext gc, TextObject textObj, boolean selected) { - gc.setFill(textObj.getTextColorAsColor()); - // 设置字体和风格 Font font = createFont(textObj.getFontFamily(), textObj.getFontSize(), textObj.getFontStyle()); gc.setFont(font); - // 绘制文本 - gc.fillText(textObj.getText(), textObj.getX(), textObj.getY() + textObj.getFontSize()); + // 绘制背景 + gc.setFill(Color.WHITE); + gc.fillRect(textObj.getX(), textObj.getY(), textObj.getWidth(), textObj.getHeight()); + gc.setStroke(Color.LIGHTGRAY); + gc.strokeRect(textObj.getX(), textObj.getY(), textObj.getWidth(), textObj.getHeight()); + + // 设置文字颜色 + gc.setFill(textObj.getTextColorAsColor()); + + // 获取排版后的文本行并绘制 + double lineHeight = textObj.getFontSize() * 1.2; + double currentY = textObj.getY() + 5; + + for (String line : textObj.getLayoutLines()) { + currentY += lineHeight; + gc.fillText(line, textObj.getX() + 5, currentY); + } // 如果选中,绘制边框 if (selected) { diff --git a/src/main/java/dev/bytevibe/hyperpoint/TextObject.java b/src/main/java/dev/bytevibe/hyperpoint/TextObject.java index 0a33d98..4de1eeb 100644 --- a/src/main/java/dev/bytevibe/hyperpoint/TextObject.java +++ b/src/main/java/dev/bytevibe/hyperpoint/TextObject.java @@ -1,6 +1,13 @@ package dev.bytevibe.hyperpoint; import javafx.scene.paint.Color; +import javafx.scene.text.Font; +import javafx.scene.text.FontPosture; +import javafx.scene.text.FontWeight; +import javafx.scene.text.Text; + +import java.util.ArrayList; +import java.util.List; /** * 文本对象 @@ -9,9 +16,13 @@ public class TextObject extends DrawableObject { private String text; private String fontFamily; private double fontSize; - private String fontStyle; // NORMAL, ITALIC, BOLD + private String fontStyle; // NORMAL, ITALIC, BOLD, BOLD_ITALIC private String textColor; + // 缓存的排版结果 + private List layoutLines; + private boolean layoutDirty = true; + public TextObject(double x, double y, String text) { super(x, y, 200, 50); this.text = text; @@ -19,6 +30,7 @@ public class TextObject extends DrawableObject { this.fontSize = 16; this.fontStyle = "NORMAL"; this.textColor = "000000"; // 黑色 + this.layoutLines = new ArrayList<>(); } public String getText() { @@ -27,6 +39,7 @@ public class TextObject extends DrawableObject { public void setText(String text) { this.text = text; + markLayoutDirty(); } public String getFontFamily() { @@ -35,6 +48,7 @@ public class TextObject extends DrawableObject { public void setFontFamily(String fontFamily) { this.fontFamily = fontFamily; + markLayoutDirty(); } public double getFontSize() { @@ -43,6 +57,7 @@ public class TextObject extends DrawableObject { public void setFontSize(double fontSize) { this.fontSize = fontSize; + markLayoutDirty(); } public String getFontStyle() { @@ -51,6 +66,7 @@ public class TextObject extends DrawableObject { public void setFontStyle(String fontStyle) { this.fontStyle = fontStyle; + markLayoutDirty(); } public String getTextColor() { @@ -65,6 +81,188 @@ public class TextObject extends DrawableObject { return Color.web("#" + textColor); } + /** + * 标记排版为脏,需要重新计算 + */ + public void markLayoutDirty() { + layoutDirty = true; + } + + /** + * 获取排版后的文本行 + * 当宽度或高度改变时应该调用此方法重新计算 + */ + public List getLayoutLines() { + if (layoutDirty) { + recalculateLayout(); + layoutDirty = false; + } + return layoutLines; + } + + /** + * 重新计算文本排版 + */ + private void recalculateLayout() { + layoutLines.clear(); + + if (text == null || text.isEmpty()) { + return; + } + + // 创建字体用于测量 + Font font = createFont(fontFamily, fontSize, fontStyle); + double lineHeight = fontSize * 1.2; + double maxWidth = width - 10; // 减去左右边距 + double maxHeight = height - 10; // 减去上下边距 + int maxLines = Math.max(1, (int) (maxHeight / lineHeight)); + + // 检查文本是否包含中文 + boolean hasChinese = containsChinese(text); + + if (hasChinese) { + // 中文排版:逐字符处理,支持在任何字符处换行 + layoutChinese(text, font, maxWidth, maxHeight, maxLines); + } else { + // 英文排版:基于空格分词 + layoutEnglish(text, font, maxWidth, maxHeight, maxLines); + } + } + + /** + * 检查文本是否包含中文字符 + */ + private boolean containsChinese(String text) { + for (char c : text.toCharArray()) { + // Unicode中文范围: + // \u4E00-\u9FFF: CJK统一表意文字 + // \u3400-\u4DBF: CJK扩展A + // \uF900-\uFAFF: CJK兼容表意文字 + if ((c >= '\u4E00' && c <= '\u9FFF') || + (c >= '\u3400' && c <= '\u4DBF') || + (c >= '\uF900' && c <= '\uFAFF')) { + return true; + } + } + return false; + } + + /** + * 中文文本排版(逐字符处理) + */ + private void layoutChinese(String text, Font font, double maxWidth, double maxHeight, int maxLines) { + StringBuilder line = new StringBuilder(); + + for (int i = 0; i < text.length(); i++) { + char c = text.charAt(i); + + // 测试添加当前字符后的行宽 + String testLine = line.toString() + c; + Text textNode = new Text(testLine); + textNode.setFont(font); + double lineWidth = textNode.getLayoutBounds().getWidth(); + + // 如果超过最大宽度 + if (lineWidth > maxWidth && !line.toString().isEmpty()) { + // 当前行满,添加到结果 + layoutLines.add(line.toString()); + line = new StringBuilder(String.valueOf(c)); + + // 检查是否超过最大行数 + if (layoutLines.size() >= maxLines) { + // 在最后一行添加省略号 + if (layoutLines.size() > 0) { + String lastLine = layoutLines.get(layoutLines.size() - 1); + if (lastLine.length() > 1) { + lastLine = lastLine.substring(0, lastLine.length() - 1) + "…"; + layoutLines.set(layoutLines.size() - 1, lastLine); + } + } + return; + } + } else { + // 当前字符添加到行 + line.append(c); + } + } + + // 添加最后一行 + if (!line.toString().isEmpty()) { + layoutLines.add(line.toString()); + } + } + + /** + * 英文文本排版(基于空格分词) + */ + private void layoutEnglish(String text, Font font, double maxWidth, double maxHeight, int maxLines) { + String[] words = text.split(" "); + StringBuilder line = new StringBuilder(); + + for (String word : words) { + // 获取添加单词后的宽度 + String testLine = line.toString().isEmpty() ? word : line + " " + word; + Text textNode = new Text(testLine); + textNode.setFont(font); + double lineWidth = textNode.getLayoutBounds().getWidth(); + + // 如果超过最大宽度,换行 + if (lineWidth > maxWidth && !line.toString().isEmpty()) { + layoutLines.add(line.toString()); + line = new StringBuilder(word); + + // 检查是否超过最大行数 + if (layoutLines.size() >= maxLines) { + // 在最后一行添加省略号 + if (!layoutLines.isEmpty()) { + String lastLine = layoutLines.get(layoutLines.size() - 1); + if (lastLine.length() > 1) { + lastLine = lastLine.substring(0, lastLine.length() - 1) + "…"; + layoutLines.set(layoutLines.size() - 1, lastLine); + } + } + return; + } + } else { + if (line.toString().isEmpty()) { + line = new StringBuilder(word); + } else { + line.append(" ").append(word); + } + } + } + + // 添加最后一行 + if (!line.toString().isEmpty()) { + layoutLines.add(line.toString()); + } + } + + /** + * 创建字体对象 + */ + private Font createFont(String family, double size, String style) { + FontWeight weight = style.contains("BOLD") ? FontWeight.BOLD : FontWeight.NORMAL; + FontPosture posture = style.contains("ITALIC") ? FontPosture.ITALIC : FontPosture.REGULAR; + return Font.font(family, weight, posture, size); + } + + @Override + public void setWidth(double width) { + if (this.width != width) { + super.setWidth(width); + markLayoutDirty(); + } + } + + @Override + public void setHeight(double height) { + if (this.height != height) { + super.setHeight(height); + markLayoutDirty(); + } + } + @Override public boolean contains(double px, double py) { return px >= x && px <= x + width && py >= y && py <= y + height; @@ -76,3 +274,6 @@ public class TextObject extends DrawableObject { } } + + +