feature: 实现文字换行

This commit is contained in:
2025-11-24 15:41:16 +08:00
parent 8a7d69df94
commit 4c3ba08c72
3 changed files with 223 additions and 6 deletions
@@ -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);
@@ -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) {
@@ -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<String> 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<String> 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 {
}
}