FlexGanttFX 已由 Dirk Lemmerman 以 AGPL 许可证开源。这个基于 JavaFX 的资源排程框架面向项目计划、生产排程等场景,提供甘特图绘制、任务依赖建模和直接编辑能力。它最值得关注的地方,是使用 Canvas 渲染甘特图,从而更适合处理大量时间轴元素。
为什么 Canvas 适合甘特图
甘特图通常同时展示任务、资源、时间刻度、依赖关系和编辑状态。若每个任务都映射成一个独立的 JavaFX 节点,任务数量增长后,节点布局、样式计算和事件管理会带来明显开销。
Canvas 的思路是把可视区域作为一块绘图表面,根据当前滚动位置和时间范围集中绘制内容。对于排程界面,这种方式有几个实际收益:
- 可以只绘制当前视口内的任务。
- 时间轴缩放时,可以统一计算像素与日期之间的映射。
- 大量任务条不必全部创建为独立 UI 节点。
- 拖拽、调整任务长度和移动任务时,重绘逻辑更容易集中管理。
这并不意味着 Canvas 会自动解决性能问题。依赖线、文本、选中态和鼠标命中检测仍然需要合理设计。特别是任务很多时,应避免每次鼠标移动都重新计算完整项目模型。
从甘特图到排程模型
FlexGanttFX 不只是一个静态图表组件。它还覆盖了任务依赖建模和直接编辑,因此应用需要把“显示数据”和“排程规则”分开处理。
一个实用的任务模型通常至少包含以下字段:
id:任务唯一标识。name:任务名称。start与end:计划开始和结束时间。resource:负责资源或资源组。predecessors:前置任务及依赖类型。editable:任务是否允许直接调整。
拖动任务条时,界面可以先更新临时位置,再由领域层检查依赖约束、工作日历和资源冲突。不要把所有规则塞进鼠标事件处理器,否则后续接入服务端校验或撤销重做会变得困难。
AGPL 也需要在项目早期纳入评估。它不仅影响发布包中的源代码分发方式,也可能影响通过网络提供软件服务时的合规策略。企业接入前应让工程、法务和发布团队共同确认许可证义务,并记录所使用的版本与修改内容。
一个可改造的 Canvas 原型
下面是一个独立的 JavaFX Canvas 原型,用于演示任务条绘制、时间到像素的映射和简单的依赖线。它不是 FlexGanttFX 的 API 示例,而是一个可以运行和改造的最小排程视图。运行环境假设为 JDK 17 和 Gradle,JavaFX 依赖由 Gradle 管理。
先创建 settings.gradle、build.gradle 和 src/main/java/example/GanttDemo.java,内容如下。
settings.gradle:
rootProject.name = 'gantt-demo'
build.gradle:
plugins {
id 'application'
id 'org.openjfx.javafxplugin' version '0.1.0'
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.openjfx:javafx-controls:21.0.2'
}
javafx {
version = '21.0.2'
modules = ['javafx.controls']
}
application {
mainClass = 'example.GanttDemo'
}
src/main/java/example/GanttDemo.java:
package example;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
import java.time.LocalDate;
import java.time.temporal.ChronoUnit;
import java.util.List;
public final class GanttDemo extends Application {
record Task(String id, String name, LocalDate start, LocalDate end, int row,
String predecessor) {}
private static final LocalDate PROJECT_START = LocalDate.of(2025, 1, 1);
private static final double DAY_WIDTH = 28.0;
private static final double ROW_HEIGHT = 42.0;
private static final double LEFT_MARGIN = 150.0;
@Override
public void start(Stage stage) {
Canvas canvas = new Canvas(980, 260);
draw(canvas.getGraphicsContext2D(), List.of(
new Task("design", "设计", LocalDate.of(2025, 1, 2),
LocalDate.of(2025, 1, 7), 0, null),
new Task("api", "API 开发", LocalDate.of(2025, 1, 8),
LocalDate.of(2025, 1, 16), 1, "design"),
new Task("test", "测试", LocalDate.of(2025, 1, 17),
LocalDate.of(2025, 1, 22), 2, "api")
));
stage.setTitle("Canvas Gantt Demo");
stage.setScene(new Scene(new StackPane(canvas)));
stage.show();
}
private static void draw(GraphicsContext g, List<Task> tasks) {
g.setFill(Color.WHITE);
g.fillRect(0, 0, 980, 260);
g.setStroke(Color.LIGHTGRAY);
g.setFill(Color.DARKSLATEGRAY);
for (int day = 0; day <= 24; day++) {
double x = LEFT_MARGIN + day * DAY_WIDTH;
g.strokeLine(x, 0, x, 230);
g.fillText(PROJECT_START.plusDays(day).toString(), x + 2, 18);
}
for (Task task : tasks) {
double x = LEFT_MARGIN + daysFromStart(task.start()) * DAY_WIDTH;
double width = Math.max(DAY_WIDTH,
(daysFromStart(task.end()) - daysFromStart(task.start()) + 1) * DAY_WIDTH);
double y = 34 + task.row() * ROW_HEIGHT;
g.setFill(Color.STEELBLUE);
g.fillRoundRect(x, y, width, 24, 6, 6);
g.setFill(Color.DARKSLATEGRAY);
g.fillText(task.name(), 12, y + 17);
if (task.predecessor() != null) {
Task previous = tasks.stream()
.filter(item -> item.id().equals(task.predecessor()))
.findFirst()
.orElseThrow();
double fromX = LEFT_MARGIN
+ (daysFromStart(previous.end()) + 1) * DAY_WIDTH;
double fromY = 34 + previous.row() * ROW_HEIGHT + 12;
double toX = x;
double toY = y + 12;
g.setStroke(Color.DARKORANGE);
g.strokeLine(fromX, fromY, toX, toY);
}
}
}
private static long daysFromStart(LocalDate date) {
return ChronoUnit.DAYS.between(PROJECT_START, date);
}
public static void main(String[] args) {
launch(args);
}
}
执行:
gradle run
这个例子可以继续扩展为真实排程组件:把固定的 List<Task> 替换为可观察模型,增加 Canvas 的鼠标事件处理,再将拖拽后的日期变化交给依赖校验器处理。接入 FlexGanttFX 时,建议沿用同样的分层方式:框架负责时间轴和交互呈现,业务代码负责资源约束、权限、持久化和冲突处理。
采用前的检查清单
- 先用真实数据量测试 Canvas 重绘、滚动和缩放,而不是只用十几个任务验证体验。
- 明确任务依赖的语义,例如完成到开始、开始到开始,以及循环依赖如何处理。
- 将拖拽编辑拆成预览、校验、提交三个阶段,保留撤销和失败回滚能力。
- 评估 JavaFX 在目标桌面环境中的打包、升级和图形加速表现。
- 在引入 AGPL 代码前完成许可证审查,并保留修改记录与依赖清单。
FlexGanttFX 的价值不只在于“画出一张甘特图”,而在于它把时间轴视图、排程依赖和直接操作放进了同一个 JavaFX 技术栈。对于已经使用 JavaFX、又需要桌面端资源排程能力的团队,它值得通过小规模真实项目验证;对于只需要静态报表的应用,则应先比较 Canvas 交互能力与引入完整框架的维护成本。