概览

图形视图控件负责活动和系统层的渲染、活动编辑、事件通知、命中检测、系统层管理以及上下文菜单支持。

图形

渲染

图形控件使用 Canvas 节点及其直接绘制 API(不同于通过 Scenegraph 执行的延迟渲染)。这是因为甘特图通常会显示大量数据。将活动直接渲染到位图中,比更新场景图、重新应用 CSS 样式并布局节点要快得多。FlexGanttFX 的图形控件采用可插拔的渲染器架构,可以将渲染器实例映射到活动类型,这与 Swing 为列表和表格单元格渲染器所采用的方式非常相似。下面的代码示例展示了如何为给定的“Flight”活动和布局类型注册自定义渲染器。请注意,图形视图能够以三种不同布局显示活动,因此还必须将布局类型传递给该方法。

GanttChart ganttChart = new GanttChart();
GraphicsBase<?> graphics = ganttChart.getGraphics();
graphics.setActivityRenderer(
    Flight.class, // the type of activities that will be rendered
    GanttLayout.class, // the type of layout where the renderer will be used
    new FlightRenderer(graphics)); // the actual renderer instance

系统层

需要显示的不只有活动。还包括当前时间(“now”)、网格线、内部线、日程 / 图表线等。这些内容都由所谓的系统层渲染。图形控件管理这类层的两个列表:一个用于背景层,另一个用于前景层。

背景层绘制在活动“后面”,前景层绘制在活动“上方”。这些列表都已预先填充,但应用程序可以修改它们。有关可用系统层的更多信息,请参阅各自的文档。

可以通过图形控件的 API 直接开启或关闭系统层。每个层都有一个 boolean 属性。可以调用符合 setShowXYZLayer 模式的方法来设置这些属性的值。以这种方式控制的系统层会通过淡入 / 淡出动画显示和隐藏,而直接调用 SystemLayer.setVisible(boolean) 则不会产生任何动画。

编辑

控制活动编辑行为需要使用两个不同的回调。第一个回调将鼠标事件 / 鼠标位置映射到 GraphicsBase.EditMode,可通过调用 setEditModeCallback(Class, Class, Callback) 注册。第二个回调用于判断给定的编辑模式 / 操作是否可以应用于某个活动。该回调通过调用 setActivityEditingCallback(Class, Callback) 注册。大多数应用程序只需要使用第二个回调,并保留编辑模式位置的默认值(例如,右边缘用于更改结束时间,左边缘用于更改开始时间)。

事件

每当用户在图形视图中执行更改时,都会发送 ActivityEvent 类型的事件。希望接收这些事件的应用程序可以调用任意一个 setOnActivityXYZEvent() 方法,或者通过 addEventHandler(ActionEvent.ACTIVITY_XYZ, ...) 直接添加事件处理器。事件会在更改执行过程中以及更改完成后触发。因此,ActivityEvent 类列出了以 CHANGINGCHANGED 两种不同结尾命名的事件类型。

命中点检测

图形视图支持查询给定位置的信息。可以调用 getActivityBoundsAt(double, double)getActivityRefAt(double, double) 查找活动。可以调用 getTimeAt(double) 查询某个 x 坐标对应的时间。反向查询也可用:可以调用 getLocation(Instant) 查找给定时间对应的位置。

上下文菜单

JavaFX 中,可以为任何控件设置上下文菜单,但鉴于图形视图的复杂性,提供额外的内置支持是有意义的。通过调用 setContextMenuCallback(Callback),可以向图形控件注册一个上下文菜单专用回调。当用户触发上下文菜单时,会调用该回调。一个回调参数对象(参见 GraphicsBase.ContextMenuParameter)会传递给回调,并且已经填充了构建上下文菜单时可能相关的最重要值。

系统层

系统层用于每一行的背景和前景。背景层在活动绘制之前绘制,前景层在活动绘制之后绘制。每个层都专门绘制一种信息:当前时间、选中的时间区间、网格线等。图形视图在两个列表中管理这些层,并提供便捷方法以便轻松查找它们。

  • getBackgroundSystemLayers() – 返回活动背景中使用的系统层完整列表。
  • getForegroundSystemLayers() – 返回活动前景中使用的系统层完整列表。
  • getBackgroundSystemLayer(Class) – 返回给定类型的系统背景层实例。
  • getForegroundSystemLayer(Class) – 返回给定类型的系统前景层实例。
  • getSystemLayer(Class) – 返回给定类型的系统层实例,无论它是前景层还是背景层。

可以通过将层加入前景或背景列表、或从这些列表中移除层,来向图形视图添加或移除层。查找到某个层后,可以设置其属性来自定义外观。最常用的属性用于设置线条颜色和宽度。

系统层示例

GraphicsBase<?> graphics = ganttChart.getGraphics();
NowLineLayer nowLayer = graphics.getBackgroundSystemLayer(NowLineLayer .class);
nowLayer.setStroke(Color.ORANGE);
nowLayer.setLineWidth(3); // thick line

系统层与模型层

请注意,系统层与模型层没有任何关系。系统层本质上是用于某些图形反馈的渲染器,而模型层用于对活动进行分组。

可用的背景层

下表列出了 FlexGanttFX 随附的所有背景层。

说明
AgendaLinesLayer 如果某行或其任何内部线使用日程布局,则为该行绘制水平网格线。
CalendarLayer 绘制附加到某一行或整个图形视图的日历所返回的条目。日历层使用可插拔渲染器,并将其映射到条目类型。应用程序可以调用 CalendarLayer.setCalendarActivityRenderer() 注册自己的渲染器。
ChartLinesLayer 如果某行或其任何内部线使用图表布局,则为该行绘制水平网格线。
DSTLineLayer 在夏令时发生变化的时间绘制一条垂直线。
GridLinesLayer 根据日期线中当前存在的刻度分辨率绘制垂直网格线。该层可配置为显示 0 到 3 个网格线级别。例如,如果日期线显示天和周,那么级别 2 会使该层绘制天和周的网格线,而网格线级别 1 只会渲染天的网格线。
HoverTimeIntervalLayer 绘制由日期线指定的悬停时间区间。如果鼠标光标悬停在日期线中的某一周上,该层会用高亮颜色填充该周定义的时间区间。
InnerLinesLayer 在内部线之间绘制分隔线。默认情况下,该层的线宽属性设置为 0,因此完全不会绘制这些线。要更改此行为,只需将线宽设置为大于 0。
NowLineLayer 在当前时间 / now 时间的位置绘制一条垂直线。当前时间在时间轴模型中定义。
RowLayer 绘制每一行的背景。该层可配置可插拔渲染器,并将其映射到行的类型。应用程序可以调用 RowLayer.setRowRenderer() 注册自己的渲染器。更多信息请阅读 3.4.7 行渲染。
SelectedTimeIntervalsLayer 绘制用户(或应用程序)在日期线中选中的时间区间。
ZoomIntervalLayer 绘制由时间轴定义的缩放区间。缩放区间由用户借助时间轴套索创建。

可用的前景层

下表列出了 FlexGanttFX 随附的所有前景层。

说明
LayoutLayer 绘制布局的内边距区域。每种布局都可能在顶部和底部添加一些内边距。该层用纯色填充内边距区域。
ScaleLayer 为整行或行内的每条线绘制刻度。刻度会根据行 / 线所使用的布局而变化。图表布局的刻度显示最小值和最大值,而日程布局的刻度显示时间刻度(8am、9am、10am,……)。刻度层中的标签和刻度线必须与日程线层和图表线层绘制的线精确对齐。

拖放

FlexGanttFX 中由平台提供的重量级拖放(DnD)机制仅用于将活动从一行移动到另一行。所有其他编辑操作都通过标准鼠标事件(按下、拖动)处理。 新的行实际上可能位于另一个 Gantt 图中。启动 DnD 的默认方式是在按住 SHIFT 键的同时,将鼠标光标移动到活动中心。如果目标活动支持这种编辑操作,光标会变为 DnD 光标(另请参见“活动编辑”)。 用户松开鼠标按钮后,DnD 将结束。

事件

与所有其他编辑操作一样,DnD 在执行过程中也会触发多个事件。下面的列表描述了这些事件:

  • DRAG_STARTEDDRAG_ONGOINGDRAG_FINISHED - 如果编辑操作为 EditMode.DRAGGING,则触发这些事件类型。
  • VERTICAL_DRAG_STARTEDVERTICAL_DRAG_ONGOINGVERTICAL_DRAG_FINISHED - 如果编辑操作为 EditMode.DRAGGING_VERTICAL,则触发这些事件类型。

编辑模式 DRAGGING_HORIZONTAL 不使用平台 DnD。因此,上面没有列出事件类型 HORIZONTAL_DRAG_STARTED / ONGOING / FINISHED

拖放信息属性

图形视图提供了一个名为 dragAndDropInfo 的特殊属性,用于监视 DnD 操作。这是对上述标准事件类型的补充。该属性中存储的信息为应用程序提供了关于被拖动活动所需的最重要信息。

字段 说明
row 鼠标光标 / 被拖动活动当前悬停其上的行。
activityBounds 被拖动活动的边界(包含活动引用和实际活动)。
dragEvent 最后一个拖动事件(正在拖动或已放下)。
dropInterval 活动将被放置或实际已放置的时间区间。
offset 鼠标抓取活动的位置偏移(拖动的视觉反馈需要此值)。

反馈类型

FlexGanttFX 提供了多种可视化 DnD 反馈的方式。枚举 DragAndDropFeedback 列出了以下值,可通过在 GraphicsBase 上调用 setDragAndDropFeedback() 方法进行设置。

说明
NATIVE 会截取活动的快照图像并放置在鼠标光标下方。该图像会在识别出拖动手势的时刻设置。也可以选择使用拖动图像提供器。图像大小可能与活动大小不同(与平台有关)。
RENDERED 被拖动的活动会持续渲染在图形区域上方的单独 canvas 上。保证活动保持其原始大小。
RENDERED_GRID_SNAPPED 被拖动的活动会持续渲染在图形区域上方的单独 canvas 上。保证活动保持其原始大小。当前活动的网格将用于使被拖动活动吸附到网格位置。

拖动图像提供器

如果 DnD 反馈类型已设置为 NATIVE,则可以为拖动操作传入自定义图像。可以通过调用 setDragImageProvider()GraphicsBase 上设置拖动图像提供器来实现这一点。该方法接受一个回调 lambda 表达式。回调的输入是 ActivityRef,输出是图像。

GraphicsBase<?> graphics = ganttChart.getGraphics();
graphics.setDragImageProvider(ref -> createImage(ref));

默认图像是拖动开始时活动的快照。

放置层提供器

拖放操作可以在两个不同的 Gantt 图之间执行,每个图都管理自己的层列表。默认情况下,被放置的活动会放在它被拖出时所在的同一层上。但如果目标 Gantt 图不包含该层,则需要告知应用程序应使用哪一层作为该活动的新归属。可以通过在目标 GraphicsBase 实例上设置“放置层提供器”回调来实现这一点。

GraphicsBase<?> graphics = targetGanttChart.getGraphics();
targetGraphics.setDropLayerProvider(info -> targetLayer); // info is of type DragAndDropInfo

该回调的默认实现如下所示:

info -> info.getActivityRef().getLayer(); // use same layer as before

如果放置层提供器没有返回层,或者返回的层并未添加到目标 Gantt 图 / 图形中,则会看到类似下面的消息。

  • “放置层提供器没有为被放置的活动返回任何层”
  • “放置层提供器返回了一个在 Gantt 图中不存在的层”

事件处理

图形视图会触发标准 JavaFX 事件,让应用程序能够对更改作出响应。FlexGanttFX 中用于事件处理器支持的概念,与标准 JavaFX 控件中的概念相同。

活动事件

每当用户删除或编辑活动时,都会触发活动事件。要接收活动事件,请通过其中一个便捷方法向图形视图注册事件处理器。

单个活动事件处理器

GraphicsBase<?> graphics = ganttChart.getGraphics();
graphics.setOnActivityChangeFinished(evt -> 
            System.out.println("An activity has changed"));

如果需要为某个特定事件类型注册多个处理器,请使用以下方式:

多个活动事件处理器

GraphicsBase<?> graphics = ganttChart.getGraphics();
graphics.addEventHandler(ActivityEvent.ACTIVITY_CHANGE_FINISHED, 
                            evt -> System.out.println("Listener 1"));
graphics.addEventHandler(ActivityEvent.ACTIVITY_CHANGE_FINISHED, 
                            evt -> System.out.println("Listener 2"));

下表列出了所有支持的活动事件类型以及图形视图的便捷 setter 方法。这些方法用于为各种事件类型快速注册事件处理器。

事件类型 说明
ACTIVITY_DELETED 每当用户通过 backspace 键删除活动时触发。
ACTIVITY_CHANGE 所有活动更改的父事件类型。可用于接收任何类型活动更改的通知。
ACTIVITY_CHANGE_STARTED, ACTIVITY_CHANGE_ONGOING, ACTIVITY_CHANGE_FINISHED 每当活动更改已开始、正在进行或已完成时触发。
CHART_HIGH_VALUE_CHANGE_STARTED, CHART_HIGH_VALUE_CHANGE_ONGOING, CHART_HIGH_VALUE_CHANGE_FINISHED 每当用户已开始编辑、正在编辑或已完成编辑高 / 低图表活动的“高”值时触发。
CHART_LOW_VALUE_CHANGE_STARTED, CHART_LOW_VALUE_CHANGE_ONGOING, CHART_LOW_VALUE_CHANGE_FINISHED 每当用户已开始编辑、正在编辑或已完成编辑高 / 低图表活动的“低”值时触发。
CHART_VALUE_CHANGE_STARTED, CHART_VALUE_CHANGE_ONGOING, CHART_VALUE_CHANGE_FINISHED 每当用户已开始编辑、正在编辑或已完成编辑图表活动的图表值时触发。
DRAG_STARTED, DRAG_ONGOING, DRAG_FINISHED 每当用户通过平台提供的拖放已开始拖动、正在拖动或已完成拖动活动时触发。当用户可以垂直和水平自由移动活动时使用此事件类型。
END_TIME_CHANGE_STARTED, END_TIME_CHANGE_ONGOING, END_TIME_CHANGE_FINISHED 每当用户已开始更改、正在更改或已完成更改活动结束时间时触发。
HORIZONTAL_DRAG_STARTED, HORIZONTAL_DRAG_ONGOING, HORIZONTAL_DRAG_FINISHED 每当用户已开始更改、正在更改或已完成更改活动的时间区间(开始和结束时间)时触发。更改此时间区间会使活动水平移动,向右(未来)或向左(过去)。
PERCENTAGE_CHANGE_STARTED, PERCENTAGE_CHANGE_ONGOING, PERCENTAGE_CHANGE_FINISHED 每当用户已开始更改、正在更改或已完成更改活动的“完成百分比”值时触发。
START_TIME_CHANGE_STARTED, START_TIME_CHANGE_ONGOING, START_TIME_CHANGE_FINISHED 每当用户已开始更改、正在更改或已完成更改活动开始时间时触发。
VERTICAL_DRAG_STARTED, VERTICAL_DRAG_ONGOING, VERTICAL_DRAG_FINISHED 每当用户通过平台提供的拖放已开始拖动、正在拖动或已完成拖动活动时触发。当用户只能垂直拖动活动(将活动重新分配到另一行)时使用此事件类型。
事件方法 说明
setOnActivityDeleted() 每当用户通过 backspace 键删除活动时触发。
setOnActivityChanged() 所有活动更改的父事件类型。可用于接收任何类型活动更改的通知。
setOnActivityChangeStarted() setOnActivityChangeOngoing() setOnActivityChangeFinished() 每当活动更改已开始、正在进行或已完成时触发。
setOnChartHighValueChangeStarted() setOnChartHighValueChangeOngoing() setOnChartHighValueChangeFinished(); 每当用户已开始编辑、正在编辑或已完成编辑高 / 低图表活动的“高”值时触发。
setOnChartLowValueChangeStarted() setOnChartLowValueChangeOngoing() setOnChartLowValueChangeFinished(); 每当用户已开始编辑、正在编辑或已完成编辑高 / 低图表活动的“低”值时触发。
setOnChartValueChangeStarted() setOnChartValueChangeOngoing() setOnChartValueChangeFinished(); 每当用户已开始编辑、正在编辑或已完成编辑图表活动的图表值时触发。
setOnActivityDragStarted() setOnActivityDragOngoing() setOnActivityDragFinished(); 每当用户通过平台提供的拖放已开始拖动、正在拖动或已完成拖动活动时触发。当用户可以垂直和水平自由移动活动时使用此事件类型。
setOnActivityEndTimeChangeStarted() setOnActivityEndTimeChangeOngoing() setOnActivityEndTimeChangeFinished(); 每当用户已开始更改、正在更改或已完成更改活动结束时间时触发。
setOnActivityHorizontalDragStarted() setOnActivityHorizontalDragOngoing() setOnActivityHorizontalDragFinished(); 每当用户已开始更改、正在更改或已完成更改活动的时间区间(开始和结束时间)时触发。更改此时间区间会使活动水平移动,向右(未来)或向左(过去)。
setOnActivityPercentageChangeStarted() setOnActivityPercentageChangeOngoing() setOnActivityPercentageChangeFinished(); 每当用户已开始更改、正在更改或已完成更改活动的“完成百分比”值时触发。
setOnActivityStartTimeChangeStarted() setOnActivityStartTimeChangeOngoing() setOnActivityStartTimeChangeFinished(); 每当用户已开始更改、正在更改或已完成更改活动开始时间时触发。
setOnActivityVerticalDragStarted() setOnActivityVerticalDragOngoing() setOnActivityVerticalDragFinished(); 每当用户通过平台提供的拖放已开始拖动、正在拖动或已完成拖动活动时触发。当用户只能垂直拖动活动(将活动重新分配到另一行)时使用此事件类型。

活动事件层次结构

ActivityEvent 类中定义的事件类型构成了一个事件层次结构。所有事件都是输入事件(InputEvent.ANY),并且它们会更改活动。其中一些在用户开始更改时触发,一些在更改进行中触发,还有一些在更改完成时触发。

  • InputEvent.ANY
  • ACTIVITY_CHANGE
    • ACTIVITY_DELETED
    • ACTIVITY_CHANGE_STARTED // 所有表示“开始”的事件类型
    • CHART_VALUE_CHANGE_STARTED
      • CHART_HIGH_VALUE_CHANGE_STARTED
      • CHART_LOW_VALUE_CHANGE_STARTED
    • DRAG_STARTED
    • END_TIME_CHANGE_STARTED
    • HORIZONTAL_DRAG_STARTED
    • PERCENTAGE_CHANGE_STARTED
    • START_TIME_CHANGE_STARTED
    • VERTICAL_DRAG_STARTED
    • ACTIVITY_CHANGE_ONGOING // 所有表示“进行中”的事件类型
    • CHART_VALUE_CHANGE_ONGOING
      • CHART_HIGH_VALUE_CHANGE_ONGOING
      • CHART_LOW_VALUE_CHANGE_ONGOING
    • DRAG_ONGOING
    • END_TIME_CHANGE_ONGOING
    • HORIZONTAL_DRAG_ONGOING
    • PERCENTAGE_CHANGE_ONGOING
    • START_TIME_CHANGE_ONGOING
    • VERTICAL_DRAG_ONGOING
    • ACTIVITY_CHANGE_FINISHED // 所有表示“完成”的事件类型
    • CHART_VALUE_CHANGE_FINISHED
      • CHART_HIGH_VALUE_CHANGE_FINISHED
      • CHART_LOW_VALUE_CHANGE_FINISHED
    • DRAG_FINISHED
    • END_TIME_CHANGE_FINISHED
    • HORIZONTAL_DRAG_FINISHED
    • PERCENTAGE_CHANGE_FINISHED
    • START_TIME_CHANGE_FINISHED
    • VERTICAL_DRAG_FINISHED

活动事件属性

应用程序显然会关心活动的属性。不仅关心这些属性的新值(例如新的开始时间),也关心旧值(更改前的开始时间)。当用户执行更改时,新值已经设置在活动上,因此可直接从活动获取。旧值存储在事件对象上。下表列出了 ActivityEvent 上用于检索这些值的方法。

方法 说明 事件类型
getOldTime 返回活动的旧开始时间或结束时间。 END_TIME_CHANGE
START_TIME_CHANGE
getOldTimeInterval 返回活动的旧开始时间和结束时间。 DRAG
HORIZONTAL_DRAG
VERTICAL_DRAG
getOldRow 返回活动之前所在的旧行。 DRAG
VERTICAL_DRAG
getOldValue 返回“完成百分比”或“图表值”的旧值。 CHART_VALUE_CHANGE
CHART_HIGH_VALUE
CHART_LOW_VALUE
PERCENTAGE_CHANGE

套索事件

用户可以使用套索选择活动。发生这种情况时会触发事件。要接收套索事件,只需通过其中一个便捷方法向图形视图注册事件处理器。

单个套索事件处理器

GraphicsBase<?> graphics = ganttChart.getGraphics();
graphics.setOnLassoFinished(evt -> System.out.println("The lasso was used"));

如果需要为某个特定事件类型注册多个处理器,请使用以下方式:

多个套索事件处理器

GraphicsBase<?> graphics = ganttChart.getGraphics();
graphics.addEventHandler(LassoEvent.SELECTION_FINISHED, 
                            evt -> System.out.println("Listener 1"));
graphics.addEventHandler(LassoEvent.SELECTION_FINISHED, 
                            evt -> System.out.println("Listener 2"));

下表列出了图形视图的事件类型和便捷 setter 方法。

事件类型 方法 说明
ALL setOnLassoSelection 任何套索操作(开始、进行中、完成)。
SELECTION_STARTED setOnLassoSelectionStarted 用户已按下鼠标按钮并开始拖动。套索已变为可见。
SELECTION_ONGOING setOnLassoSelectionOngoing 用户正在更改套索的大小。
SELECTION_FINISHED setOnLassoSelectionFinished 用户已完成套索选择。套索不再可见。

套索事件层次结构

LassoEvent 类中定义的事件类型构成了一个事件层次结构。所有事件都是输入事件(InputEvent.ANY)。

* InputEvent.ANY
    * LassoEvent.ALL
        * LassoEvent.SELECTION_STARTED
        * LassoEvent.SELECTION_ONGOING
        * LassoEvent.SELECTION_FINISHED

套索信息

套索会自动执行活动选择,但有时我们可能希望进一步了解该选择的确切性质,或者希望将套索用于其他用例(例如创建新活动)。因此,LassoEvent 实例还提供一个 LassoInfo 类型的对象,其中包含许多属性,应用程序可以据此作出相应响应。可以通过调用 LassoEvent.getInfo() 检索套索信息。下表列出了 LassoInfo 的属性。

方法 说明
List<ActivityRef<?>> getActivities 返回套索选中的所有活动。
Instant getStartTime;, Instant getEndTime 根据套索左右边缘的位置,返回套索的开始时间和结束时间。
LocalTime getLocalStartTime, LocalTime getLocalEndTime 返回本地开始时间和结束时间。仅当套索的上边缘或下边缘位于使用 AgendaLayout 的区域中时,才会提供这些值。
List<Row<?,?,?>> getRows 返回套索触及的行。

链接 / 延伸阅读

Oracle JavaFX 文档 事件处理示例

活动编辑

图形视图上的两个不同回调用于控制活动的编辑行为。第一个回调将鼠标事件 / 鼠标位置映射到编辑模式。第二个回调用于判断给定的编辑模式 / 操作是否可以应用于某个活动。大多数应用程序只需要使用第二个回调,并保留编辑模式位置的默认值(例如:右边缘用于更改结束时间,左边缘用于更改开始时间)。枚举 GraphicsBase.EditMode 列出了可对活动执行的所有可用编辑操作。

模式 说明
AGENDA_ASSIGNING 将 AgendaLayout 中的活动分配到另一行。
AGENDA_DRAGGING 在同一行内上下或横向拖动 AgendaLayout 中的活动。
AGENDA_END_TIME_CHANGE 更改 AgendaLayout 中活动的结束时间。
AGENDA_START_TIME_CHANGE 更改 AgendaLayout 中活动的开始时间。
CHART_VALUE_CHANGE 更改 ChartActivity 的值。
CHART_VALUE_HIGH_CHANGE 更改 HighLowActivity 的“高”值。
CHART_VALUE_LOW_CHANGE 更改 HighLowActivity 的“低”值。
DRAGGING 对活动执行全方向拖放。
DRAGGING_HORIZONTAL 在活动所在行内水平移动活动(更改开始和结束时间)。
DRAGGING_VERTICAL 仅在垂直方向对活动执行拖放。
END_TIME_CHANGE 更改活动的结束时间。
NONE 不执行任何操作。
PERCENTAGE_COMPLETE_CHANGE 更改 CompletableActivity 的“完成百分比”值。
START_TIME_CHANGE 更改活动的开始时间。

编辑模式回调

编辑模式回调用于确定给定鼠标位置处的编辑模式。可以通过 GraphicsBase.setEditModeCallback() 方法注册此回调的实例,该方法会将回调映射到活动类型与布局类型的组合。

public final void setEditModeCallback(
    Class<? extends MutableActivity> activityType,
    Class<? extends Layout> layoutType,
    Callback<EditModeCallbackParameter, EditMode> callback);

编辑模式回调参数

传递给编辑模式回调的参数对象类型为 EditModeCallbackParameter,并包含以下信息:

字段 说明
activityBounds 鼠标光标当前悬停其上的活动边界。x 和 y 坐标相对于显示该活动的行的坐标空间。
mouseEvent 触发编辑模式查找的鼠标事件(通常是 MOUSE_OVER)。

编辑模式回调示例

下面是编辑模式回调的一个简单示例。

public class MyEditModeCallback implements Callback<EditModeCallbackParameter, EditMode> {

    public EditMode call(EditModeCallbackParameter param) {
        MouseEvent event = param.getMouseEvent();
        ActivityBounds bounds = param.getActivityBounds();

        /*
         * If the mouse cursor is touching the left edge of the activity
         * then begin a change of the start time of the activity.
         */
        if (event.getX() - bounds.getMinX() < 5) {
            return EditMode.CHANGE_START_TIME;
        }

        return EditMode.NONE;
    }

现在可以这样注册该回调:

GraphicsBase<?> graphics = ganttChart.getGraphics();
    graphics.setEditModeCallback(
    ActivityBase.class,
    GanttLayout.class,
    new MyEditModeCallback());

我们本可以为整个回调实例使用 lambda 表达式,但为了更清晰而没有这样做。

编辑回调

编辑回调用于判断某个特定编辑模式当前是否可用于给定活动。可以通过 GraphicsBase.setActivityEditingCallback() 方法注册此回调的实例,该方法会将回调映射到活动类型。

public final void setActivityEditingCallback(
    Class<? extends MutableActivity> activityType,
    Callback<EditingCallbackParameter, Boolean> callback);

编辑回调参数

传递给编辑回调的参数对象类型为 EditingCallbackParameter,并包含以下信息:

字段 说明
activityRef 要为其执行检查的活动引用。
editMode 需要检查的编辑模式。

编辑回调示例

下面是编辑回调的一个简单示例。

public class MyEditingCallback implements Callback<EditingCallbackParameter, Boolean> {

    public Boolean call(EditingCallbackParameter param) {
        ActivityRef ref = param.getActivityRef();
        Activity activity = ref.getActivity();

        /*
         * Only allow editing for activities that that have not
         * started, yet.
         */
        if (activity.getStartTime().isAfter(Instant.now())) {

            /*
             * Only allow changes to the start and end time
             * of the activity.
             */
            switch (param.getEditMode()) {
                case CHANGE_START_TIME:
                case CHANGE_END_TIME:
                    return true;
                default:
                    return false;
            }
          } 

        return false;
    }
}

现在可以这样注册该回调:

GraphicsBase<?> graphics = ganttChart.getGraphics();
    graphics.setActivityEditingCallback(
    ActivityBase.class,
    new MyEditingCallback());

行编辑

图形视图不仅支持编辑活动,也支持编辑行。如果某一行被编辑,整行会翻转,其他控件会显示在该行的“背面”。如果行背面需要的空间(高度)大于行正面,则高度会自动调整。下表列出了与行编辑相关的方法:

方法 说明
void startRowEditing(R row); 在给定行上启动行编辑序列。行的背面会变为可见,并显示用于更改行设置的控件。
void stopRowEditing();
void stopRowEditing(R row);
停止所有行或仅给定行的行编辑。行的正面会再次变为可见。
ObjectProperty<RowEditingMode> rowEditingModeProperty();
void setRowEditingMode(RowEditingMode);
RowEditingMode getRowEditingMode();
存储、设置并检索行编辑模式。枚举 GraphicsBase.RowEditingMode 用于确定用户是否可以编辑行,以及一次编辑一行还是同时编辑多行。
ObservableList<R> getRowsEditing(); 当前正在编辑的所有行(其背面已显示)的可观察列表。
BooleanProperty animateRowEditor();
void setAnimateRowEditor(boolean);<br>boolean isAnimateRowEditor();
存储、设置并检索一个标志,用于表示行背面的显示是立即完成还是带动画。

行编辑器工厂

行编辑器工厂用于在用户请求编辑某一行时,为该行创建控件。该工厂是一个回调方法,调用时会传入 GraphicsBase.RowEditorParameter 对象。此参数对象存储了一些有助于创建编辑器控件的字段,并提供了一个用于停止行编辑的方法。

方法 说明
GraphicsBase getGraphics(); 返回将发生编辑的图形视图引用。
R getRow(); 返回要为其创建行编辑器的行。
void stopEditing(); 供行编辑器控件使用的便捷方法,可用于表示用户已完成对行的编辑。该方法通常由编辑器 UI 中的某种关闭按钮调用。

行编辑器工厂可能如下所示:

public class MyRowEditorFactory implements Callback<RowEditorParameter<R>, Node> {

    public Node call(RowEditorParameter<R> param) {
        VBox box = new VBox();

        /*
         * Bind the text property of the textfield to the name
         * property of the row. This allows us to change the name
         * of the row.
         */
        TextField nameField = new TextField();
        Bindings.bindBidirectional(param.getRow().nameProperty(), 
                nameField.textProperty());

        /*
         * A close button to invoke the stopEditing() method
         * on the parameter object.
         */
        Button closeButton = new Button("Close");
        closeButton.setOnAction(evt -> param.stopEditing());
        box.getChildren().addAll(nameField, closeButton);

        /*
         * Return the vbox node.
         */
        return box;
    }
}

可以这样注册行编辑器:

GraphicsBase<?> graphics = ganttChart.getGraphics();
graphics.setRowEditorFactory(new MyRowEditorFactory());

行控件工厂

要触发行编辑,用户界面需要提供某种控件。这可以通过多种方式实现,例如借助行上的上下文菜单。另一种方式是使用对所谓“行控件”的内置支持。每当鼠标光标进入 / 离开某一行时,这些控件会显示 / 消失。它们由回调实现创建。该回调接收 GraphicsBase.RowControlsParameter 类型的参数对象。下表列出了此类型的字段。

字段 说明
graphics 触发该回调的图形视图。
row 要为其创建控件的行。

示例 1

该回调的一种可能实现如下所示:

public class MyRowControlsFactory extends StackPane 
        implements Callback<RowControlsParameter, Node> {

    private Button button;

    public MyRowControlsFactory() {

        /*
         * Important: let mouse events pass through.
         */
        setMouseTransparent(true);
        button = new Button("Press Me");
        getChildren().add(button);
    }

    /*
     * Reuse the button. Simply exchange the action that will
     * happen when the user presses on it.
     */
    public Node call(RowControlsParameter param) {
       button.setOnAction(evt -> 
            System.out.println("Pressed on row " + 
                    param.getRow().getName());
        return this;
    }

请注意,该工厂本身是一个 Node 对象,并且每次调用 call() 方法时都会返回自身。每次调用时只会替换按钮的动作。这样做非常合理,因为行控件始终一次只为一行显示(不同于行编辑器,后者可以同时使用多个实例)。

可以这样注册该回调:

GraphicsBase<?> graphics = ganttChart.getGraphics();
graphics.setRowControlsFactory(new MyRowControlsFactory());

示例 2

下面是 FlexGanttFX “Extras”项目中 RowControls 类的代码。它向行添加一个简单的“编辑”按钮。单击该按钮时,会在行的背面显示行编辑器控件。

package com.flexganttfx.extras;


import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.Button;
import javafx.scene.layout.HBox;
import javafx.util.Callback;
import com.flexganttfx.model.Row;
import com.flexganttfx.view.graphics.GraphicsBase.RowControlsParameter;

public class RowControls<R extends Row<?, ?, ?>> extends HBox implements
        Callback<RowControlsParameter<R>, Node> {


    private Button editButton;

    public RowControls() {
        setPickOnBounds(false);
        setMinSize(0, 0);
        setAlignment(Pos.TOP_RIGHT);
        setFillHeight(true);
        editButton = new Button("EDIT");
        editButton.getStyleClass().add("row-controls-button");
        getChildren().add(editButton);
    }

    @Override
    public Node call(RowControlsParameter<R> param) {
        editButton.setOnAction(evt -> param.getGraphics().startRowEditing(
                param.getRow()));
        return this;
    }
}

按钮对应的 CSS 定义如下:

/*
 * Row controls button are shown when the mouse hovers over a row that can be
 * edited (flipped around).
 */
.row-controls-button {
  -fx-padding: 5 9 7 7;
  -fx-background-insets: 0 4 2 2;
  -fx-background-color: rgba(0,0,0,.5);
  -fx-background-radius: 0;
  -fx-text-fill: white;
  -fx-font-size: 8;
  -fx-font-weight: bold;
}

.row-controls-button:hover,
.row-controls-button:focused {
  -fx-padding: 5 9 7 7;
  -fx-background-insets: 0 4 2 2;
  -fx-background-color: rgba(0,0,0,.6);
  -fx-background-radius: 0;
  -fx-text-fill: white;
  -fx-font-size: 8;
  -fx-font-weight: bold;
}

.row-controls-button:pressed,
.row-controls-button:selected {
  -fx-background-color: rgba(0,0,0,.7);
  -fx-background-radius: 0;
}

活动渲染

图形视图使用 JavaFX 的 canvas API。这是因为 Gantt 图本身较复杂,并且其中通常会出现大量数据。将大量活动直接渲染到位图中,比不断更新场景图并重新应用 CSS 样式要快得多。FlexGanttFX 实现了一种可插拔的渲染器架构,可以将渲染器实例映射到活动类型,这与 Swing 过去的做法非常相似。

下面的代码示例展示了如何为给定的“Flight”活动类型注册自定义渲染器。请注意,图形视图能够以不同布局显示活动,因此还必须将布局类型传递给该方法。

GraphicsBase<?> graphics = ganttChart.getGraphics();
graphics.setActivityRenderer(
                Flight.class, 
                GanttLayout.class, 
                new FlightRenderer(graphics));

我们通常也会在构造时将图形视图传递给渲染器。这是必要的,因为当渲染器的任何属性发生变化时,渲染器会在图形上触发重绘。这与 Swing 的做法非常不同。这也意味着渲染器实例应仅用于单个图形视图,即传递给其构造函数的那个视图。

GraphicsBase 上的以下方法用于处理渲染器:

方法 说明
void setActivityRenderer(...); 为给定活动和布局类型注册新的渲染器。
ActivityRenderer getActivityRenderer(...); 返回给定活动和布局类型的渲染器。

绘制

活动渲染器有一个用于绘制的单一入口点,即名为 draw() 的方法。该方法是 final 的,不能被重写。调用后,它会调用多个 protected 方法来执行实际绘制。调用层次结构如下:

  • public final draw() .... 调用 ...
    • protected ActivityBounds drawActivity()
    • protected void drawBackground()
    • protected void drawBorder()

子类可以自由重写这三个 protected 方法中的任意一个,以自定义活动外观。所有 drawXXX() 方法都具有相同的参数:

ActivityRef<A> activityRef,  // the activity to draw
Position position,           // agenda layout only (first, middle, last, only)
GraphicsContext gc,          // the graphics context into which to draw
double x,                    // the location of the start time of the activity
double y,                    // the y coordinate (0 when drawn on row or line location)
double w,                    // end time location minus start time location
double h,                    // row or line height
boolean selected,            // is activity currently selected?
boolean hover,               // is mouse cursor currently hovering over it?
boolean highlighted,         // is activity currently blinking?
boolean pressed)             // is user currently pressing on it?

默认渲染器

下表列出了默认提供的各种活动渲染器。

渲染器类 说明
ActivityRenderer 最基础的活动渲染器。在活动位置绘制一个填充矩形。所有默认渲染器都是此类型的子类。
ActivityBarRenderer 绘制条形,而不是填充整个区域。可以指定条形的高度。还支持在条形内外的多个位置显示文本。
ChartActivityRenderer 根据图表值垂直绘制 ChartActivity。
CompletableActivityRenderer 条形渲染器的子类。将 CompletableActivity 绘制为条形,并用另一种颜色填充其背景的一部分。该部分的大小取决于活动的完成百分比值。

活动边界

每个活动渲染器都负责在绘制活动后返回一个 ActivityBounds 实例。这些边界是框架的关键组成部分,许多操作只有在这些边界有效时才能正常工作。它们用于活动编辑、命中点检测、链接布局、上下文菜单等。下表列出了 ActivityBounds 类的属性。

属性 说明
activity 这些边界所属的活动。
activityRef 指向该活动的活动引用。
layer 绘制该活动所在的层。
layout 绘制该活动时使用的布局。
lineIndex 活动所在的线索引(如果活动位于行上而非线上,则为 -1)。
position 在日程布局中绘制活动时边界的位置(first、middle、layout)。这是必需的,因为同一活动可能会跨多天渲染为多个片段。
row 绘制该活动所在的行。

请忽略属性 overlapColumn、overlapCount 以及列表 overlapBounds。它们都在内部用于与日程布局相关的操作。

属性

所有渲染器都定义了若干属性,可用于自定义其外观。其中许多属性取决于活动的“伪状态”:hover、pressed、selected、highlighted。为了更容易在正确的时机查找正确的颜色,提供了多个便捷方法:

  • Renderer: protected Paint getFill(boolean selected, boolean hover, boolean highlighted, boolean pressed);
  • 根据传入的伪状态返回活动背景应使用的颜色。
  • ActivityRenderer: protected Paint getStroke(boolean selected, boolean hover, boolean highlighted, boolean pressed);
  • 根据传入的伪状态返回活动边框应使用的颜色。
  • ActivityBarRenderer: protected Paint getTextFill(boolean selected, boolean hover, boolean highlighted, boolean pressed);
  • 根据传入的伪状态返回文本应使用的颜色。

行渲染

系统层 RowLayer 支持可插拔渲染器,可根据行类型自定义每一行的背景。在教程中我们已经看到,可以有 Aircraft 行和 Crew 行。为了清晰起见,这两种行可以使用不同的背景颜色。这可以通过行渲染器实现。

行渲染器

所有行渲染器都必须继承 RowRenderer。该类定义了一个名为 draw() 的 final public 方法,由框架调用。随后它会调用 protected 方法 drawRow(),子类可以重写该方法。一种可能的实现如下所示:

public class AircraftRowRenderer extends RowRenderer<Aircraft> {

    public AircraftRowRenderer(GraphicsBase<?> graphics) {
        super(graphics, "Aircraft Row Renderer");
    }

    protected void drawRow(Aircraft row,
                           GraphicsContext gc,
                           double w,
                           double h,
                           boolean selected,
                           boolean hover,
                           boolean highlighted,
                           boolean pressed) {
        gc.setFill(Color.ORANGE);
        gc.fillRect(0, 0, w, h);       
    }
}

现在可以这样将该渲染器注册到 RowLayer:

GraphicsBase<?> graphics = ganttChart.getGraphics();
graphics.getSystemLayer(RowLayer.class).setRowRenderer(
                    Aircraft.class, new AircraftRowRenderer());

上下文菜单

有两种方式可以向图形视图注册上下文菜单。标准方式是调用 GraphicsBase.setContextMenu(ContextMenu),或者通过调用 GraphicsBase.setContextMenuCallback() 注册上下文菜单回调。第二种方式的优点是会将 ContextMenuParameter 类型的参数对象传递给回调方法。该参数对象包含大多数上下文菜单所需的最相关参数,以便让用户在图形视图上执行某种操作。

请注意,上下文菜单回调的优先级高于标准上下文菜单。

代码示例

以下片段展示了上下文菜单回调实现的示例。这里我们只是为用户请求上下文菜单的鼠标位置处找到的每个活动添加一个菜单项。

GraphicsBase<?> graphics = ganttChart.getGraphics();
graphics.setContextMenuCallback(param -> {
    ContextMenu menu = new ContextMenu();
    for (ActivityRef<?> ref : param.getActivities()) {
        Activity activiy = ref.getActivity();
        MenuItem item = new MenuItem("Move " + activity.getName());
        item.setOnAction(evt -> moveActivity(activity);
        menu.getItems().add(item);
    }
    return menu;
});

行表头

图形区域中的每一行都可以显示自己的表头,表头会显示在该行左侧。当可见时间范围发生变化时(例如用户向左或向右滚动),表头始终保持在此位置,不会滚动。

行表头

FlexGanttFX 随附的默认行表头知道如何在应用程序使用图表或日程布局时显示刻度。默认表头的实现可在 ScaleRowHeader 类中找到。

可以通过继承 GraphicsBase.RowHeader 创建自定义行表头。每当使用该表头的行发生变化时,item 属性都会更新。与列表或表格单元格一样,行表头类是 Label 的扩展,这意味着可以通过调用 setGraphics(Node node) 和 setContentDisplay(ContentDisplay.GRAPHICS_ONLY) 完全替换其内容。

另请参见:

public void setRowHeaderFactory(Callback<GraphicsBase<R>, RowHeader<R>> factory);
public void setShowRowHeaders(boolean showRowHeaders);

注意:在 FlexGanttFX 的旧版本中,显示行表头 / 刻度的功能位于 ScaleLayer 类中。随着画布缓冲区概念以及通过 translateX 属性滚动的引入,必须对此进行更改,因为刻度会根据 translateX 属性值被放置在各种不同位置。