行为与显示设置页面
行为与显示设置页面是 Ark Pets 启动器(ArkHomeFX)中负责编辑桌宠"行为"与"显示/部署"两大类运行参数的 GUI 模块,由 BehaviorModule 控制器与 BehaviorModule.fxml 布局共同实现,所有改动即时写回 ArkConfig 并持久化到配置文件。
Purpose and Scope
本页面覆盖启动器 GUI 中"行为与显示设置"模块的完整实现,包括:
- 控制器
BehaviorModule的初始化流程、控件绑定方式与事件处理逻辑; - 行为设置(走/坐/睡/交互等行为开关、AI 活跃度、移动速度、换向策略、同伴斥力);
- 显示与部署设置(多显示器、底部边距、初始部署坐标拾取器 DotPicker);
- 过渡动画设置(动画混合速度、属性过渡时长、缓动函数)与物理参数(重力、摩擦、速度上限);
- 配置项的持久化路径,以及核心进程(
ArkPets)对这些配置的下游消费方式。
以下相关内容不在本页范围内,请参考对应兄弟页面:
- 启动器整体框架、模块装配与窗口生命周期:见"启动器 GUI 概览"页;
- 通用/其它设置模块(
SettingsModule):见"设置页面"页; - 模型管理与启动流程(
ModelsModule):见"模型管理页面"页; - 行为状态机与动画调度的核心实现(
GeneralBehavior):属于桌面端核心进程主题,本页仅在"运行时消费"一节做交叉引用。
Overview
Ark Pets 将启动器(desktop 模块的 ArkHomeFX)与桌宠核心进程(core 模块的 ArkPets)解耦:启动器只负责编辑配置并保存,桌宠进程在启动/运行时读取同一份配置来决定行为表现。"行为与显示设置"页面正是启动器中承载这些参数编辑的三个功能模块之一(另外两个是 ModelsModule 与 SettingsModule)。
该页面的核心设计思路是"控件即配置":
- 每个控件(
CheckBox、Slider、ComboBox、DotPicker画布)在初始化时从app.config(ArkConfig实例)读取当前值并回填; - 任何一次用户交互(勾选、拖动、选择)都会在事件回调里把合法化后的值写回
app.config的对应字段,并立即调用app.config.save()持久化; - 没有单独的"保存"按钮或"应用"按钮——写回与持久化是同步、即时的,这与桌宠进程可以随时被启动/重启的使用方式匹配。
页面共覆盖约 20 个配置字段,按视觉分组分为:行为(Behavior)、显示/部署(Deploy)、过渡动画(Transition)、物理(Physic)四组,另有一个每 5 秒刷新一次的多显示器数量探测器。
Architecture
架构说明:
ArkHomeFX是启动器主类,它持有三个功能模块实例,其中public BehaviorModule behaviorModule;即本页面对应的控制器(见 ArkHomeFX.java)。BehaviorModule实现Controller<ArkHomeFX>接口,通过initializeWith(ArkHomeFX app)在模块被装配后拿到宿主引用,从而访问全局唯一的app.config。- 布局与逻辑分离:控件声明在
assets/UI/BehaviorModule.fxml中,控制器只通过@FXML注入的引用操作控件。 - 控件交互的"合法化 + 显示格式化"统一下沉到
utils.GuiComponents的各类 Setup 工具类(SliderSetup、ComboBoxSetup、DotPickerSetup等),控制器因此只保留"读值 → 写配置 → 保存"这一层薄逻辑,这是该页面最主要的代码组织意图。 - 配置层是唯一的共享状态:GUI 写、核心进程读。
ArkConfig.save()持久化到 JSON,核心进程ArkPets在启动时加载并把这些值交给GeneralBehavior(行为)与ArkChar(渲染,如display_scale、display_fps)使用。
Core Flow:一次配置修改的端到端路径
这条链路解释了为什么控制器里每个回调都以 app.config.save() 结尾:启动器与桌宠进程之间没有 IPC 同步机制,配置文件是两者之间唯一的契约,因此"即时落盘"是保证用户下一次启动桌宠即可生效的最低成本方案。
模块加载与初始化
BehaviorModule 是一个 final 类,实现泛型接口 Controller<ArkHomeFX>。它的全部控件都是 @FXML 注入的私有字段,按功能分组命名(configBehavior*、configDeploy*、configTransition*、configPhysic*),命名前缀直接对应 ArkConfig 中的字段前缀,便于一一对照。
1public final class BehaviorModule implements Controller<ArkHomeFX> {
2 @FXML
3 private ScrollPane moduleScroll;
4
5 @FXML
6 private CheckBox configBehaviorAllowWalk;
7 @FXML
8 private CheckBox configBehaviorAllowSit;
9 @FXML
10 private CheckBox configBehaviorAllowSleep;
11 @FXML
12 private CheckBox configBehaviorAllowSpecial;
13 @FXML
14 private Slider configBehaviorAiActivation;
15 @FXML
16 private Label configBehaviorAiActivationValue;
17 @FXML
18 private Slider configBehaviorWalkSpeed;
19 @FXML
20 private Label configBehaviorSpeedWalkValue;
21 @FXML
22 private CheckBox configBehaviorAllowInteract;
23 @FXML
24 private CheckBox configBehaviorDoPeerRepulsion;
25 @FXML
26 private ComboBox<NamedItem<Integer>> configDirectionSwitching;
27 @FXML
28 private CheckBox configDeployMultiMonitors;
29 @FXML
30 private Label configDeployMultiMonitorsStatus;
31 @FXML
32 private Slider configDeployMarginBottom;
33 @FXML
34 private Label configDeployMarginBottomValue;
35 @FXML
36 private Button toggleConfigDeployPosition;
37 @FXML
38 private HBox wrapperConfigDeployPosition;
39 @FXML
40 private Canvas configDeployPosition;Source: BehaviorModule.java
设计要点:
- 每个
Slider都配对一个Label(如configBehaviorAiActivationValue),由 Setup 工具负责把数值格式化后显示,控制器不需要手写任何格式化代码; configDirectionSwitching、configTransition*三个下拉框使用ComboBox<NamedItem<T>>,即"显示名 + 实际值"二元组,把中文 UI 文案与机器可读配置值解耦;configDeployPosition是一个javafx.scene.canvas.Canvas,配合折叠开关按钮toggleConfigDeployPosition与包装容器wrapperConfigDeployPosition组成可展开的坐标拾取器。
模块装配完成后由宿主调用 initializeWith,完成三件事:保存宿主引用、初始化全部配置控件、启动多显示器探测定时器;最后两行是纯 UI 体验优化:
1 @Override
2 public void initializeWith(ArkHomeFX app) {
3 this.app = app;
4 initConfigBehavior();
5 initScheduledListener();
6 ScrollUtils.addSmoothScrolling(moduleScroll);
7 Platform.runLater(() -> GuiPrefabs.disableScrollPaneCache(moduleScroll));
8 }Source: BehaviorModule.java
ScrollUtils.addSmoothScrolling为滚动面板附加平滑滚动;GuiPrefabs.disableScrollPaneCache关闭 ScrollPane 的缓存(通过Platform.runLater推迟到 JavaFX 应用线程执行),避免该页大量滑杆/画布控件在滚动时出现残影——这是 JavaFX 混合渲染下的典型性能规避手段。
Usage Examples:三类控件的绑定范式
1. 复选框(CheckBox):最小单位的"读回填 + 写保存"
行为开关组(走/坐/睡/特殊)是整个页面最简单的绑定模式——初始化时 setSelected 回填,事件回调里写配置并保存:
1 configBehaviorAllowWalk.setSelected(app.config.behavior_allow_walk);
2 configBehaviorAllowWalk.setOnAction(e -> {
3 app.config.behavior_allow_walk = configBehaviorAllowWalk.isSelected();
4 app.config.save();
5 });Source: BehaviorModule.java
configBehaviorAllowSit、configBehaviorAllowSleep、configBehaviorAllowSpecial、configBehaviorAllowInteract、configBehaviorDoPeerRepulsion、configDeployMultiMonitors 全部采用完全相同的两行式写法,仅字段名不同(见 L129-L192)。
2. 滑杆(Slider):链式 Setup + 合法化取值
滑杆走的是 SliderSetup 链式 API:setDisplay 指定数值 Label、单位格式与悬浮提示;setRange 限定范围;setTicks 设定刻度(主刻度间隔/次刻度数);setSliderValue 回填当前配置;setOnChanged 注册变更回调。回调里必须通过 getValidatedValue() 取值而不是直接用事件里的 newValue,这样写入配置的永远是落在合法刻度上的整数:
1 SliderSetup<Integer> setupBehaviorAiActivation = new SimpleIntegerSliderSetup(configBehaviorAiActivation);
2 setupBehaviorAiActivation
3 .setDisplay(configBehaviorAiActivationValue, "%d 级", "活跃级别 (activation level)")
4 .setRange(0, 16)
5 .setTicks(1, 0)
6 .setSliderValue(app.config.behavior_ai_activation)
7 .setOnChanged((observable, oldValue, newValue) -> {
8 app.config.behavior_ai_activation = setupBehaviorAiActivation.getValidatedValue();
9 app.config.save();
10 });Source: BehaviorModule.java
SimpleIntegerSliderSetup 与 SimpleMultipleIntegerSliderSetup 的区别在于后者支持"倍数吸附":例如移动速度滑杆用 new SimpleMultipleIntegerSliderSetup(configBehaviorWalkSpeed, 5) 构造,配合 setTicks(100, 10) 实现以 5 px/s 为粒度的吸附拖动,避免用户拖出无意义的碎数:
1 SliderSetup<Integer> setupBehaviorWalkSpeed = new SimpleMultipleIntegerSliderSetup(configBehaviorWalkSpeed, 5);
2 setupBehaviorWalkSpeed
3 .setDisplay(configBehaviorSpeedWalkValue, "%d px/s", "像素每秒 (pixel/s)")
4 .setRange(0, 200)
5 .setTicks(100, 10)
6 .setSliderValue(app.config.behavior_walk_speed)
7 .setOnChanged((observable, oldValue, newValue) -> {
8 app.config.behavior_walk_speed = setupBehaviorWalkSpeed.getValidatedValue();
9 app.config.save();
10 });Source: BehaviorModule.java
3. 下拉框(ComboBox):NamedItem 枚举映射 + 自定义值兜底
下拉框通过 NamedItem<>() 把中文显示名映射到实际配置值。关键在 selectValue(currentValue, customText) 的第二个参数:当用户手动在配置文件里写了不在预设项中的值时,下拉框会追加一个"xx(自定义)"项而不是显示空白,保证 UI 始终忠实反映真实配置:
1 new ComboBoxSetup<>(configDirectionSwitching).setItems(new NamedItem<>("禁用", 0),
2 new NamedItem<>("松开拖拽时", 1),
3 new NamedItem<>("拖拽时", 2),
4 new NamedItem<>("光标掠过时", 3))
5 .selectValue(app.config.behavior_direction_switching, app.config.behavior_direction_switching + "(自定义)")
6 .setOnNonNullValueUpdated((observable, oldValue, newValue) -> {
7 app.config.behavior_direction_switching = newValue.value();
8 app.config.save();
9 });Source: BehaviorModule.java
setOnNonNullValueUpdated(而非普通 setOnAction)确保只有选中了带值条目才触发写回,避免空选择导致配置被意外清空。
部署坐标拾取器(DotPicker)与多显示器探测
相对坐标拾取
初始部署位置不是输入框,而是一块可点选的 Canvas。GuiPrefabs.bindToggleAndWrapper 把开关按钮与包装容器绑定成可折叠区域;DotPickerSetup 在画布上绘制可拖动圆点,setRelXY 用相对坐标(0~1 的比例值)回填当前配置,setOnDotPicked 在用户松开圆点时把相对坐标写回 initial_position_x/y。使用相对坐标的原因显而易见:不同分辨率/不同显示器布局下,绝对像素值无法迁移,而比例值可以:
1 GuiPrefabs.bindToggleAndWrapper(toggleConfigDeployPosition, wrapperConfigDeployPosition, durationFast);
2 DotPickerSetup setupDeployPosition = new DotPickerSetup(configDeployPosition);
3 setupDeployPosition.setRelXY(app.config.initial_position_x, app.config.initial_position_y);
4 setupDeployPosition.setOnDotPicked(e -> {
5 float x = (float) setupDeployPosition.getRelX();
6 float y = (float) setupDeployPosition.getRelY();
7 Logger.debug("Config", "Specified deploy position to " + x + ", " + y);
8 app.config.initial_position_x = x;
9 app.config.initial_position_y = y;
10 app.config.save();
11 });Source: BehaviorModule.java
多显示器数量探测(后台定时器)
"允许部署到多个显示屏"复选框旁有一个状态 Label,显示当前系统检测到的显示器数量。实现上用 JavaFX 的 ScheduledService 起了一个周期任务:首次延迟 2.5 秒、此后每 5 秒执行一次、失败自动重启。真正的检测工作 Monitor.getMonitors().size() 被放进 Task 里异步执行,onSucceeded 回到 UI 线程更新 Label——避免显示器枚举(可能涉及原生调用)阻塞 JavaFX Application Thread:
1 private void initScheduledListener() {
2 ScheduledService<Boolean> ss = new ScheduledService<>() {
3 @Override
4 protected Task<Boolean> createTask() {
5 Task<Boolean> task = new Task<>() {
6 @Override
7 protected Boolean call() {
8 return true;
9 }
10 };
11 task.setOnSucceeded(e ->
12 configDeployMultiMonitorsStatus.setText("检测到 " + Monitor.getMonitors().size() + " 个显示屏"));
13 return task;
14 }
15 };
16 ss.setDelay(new Duration(2500));
17 ss.setPeriod(new Duration(5000));
18 ss.setRestartOnFailure(true);
19 ss.start();
20 }Source: BehaviorModule.java
注意一个实现细节:Monitor.getMonitors() 实际是在 onSucceeded 回调(即 FX 线程)中被调用的,Task 本体只返回 true 作为触发信号。由于该 Service 没有被 cancel/持有引用,其生命周期与模块同生共死。
过渡动画与缓动函数设置
这一组三个下拉框分别控制 render_animation_mixture(动画间交叉过渡时长)、transition_duration(位置/透明度/翻转/描边等属性过渡时长)、transition_type(缓动函数名)。前两个提供"禁用/快速/标准/慢速"四档预设;第三个直接引用核心模块的 EasingFunction 枚举,把枚举名作为配置值存储,保证了 GUI 与渲染层共用同一套函数定义:
1 new ComboBoxSetup<>(configTransitionAnimation).setItems(new NamedItem<>("禁用", 0f),
2 new NamedItem<>("快速", 0.1f),
3 new NamedItem<>("标准", 0.3f),
4 new NamedItem<>("慢速", 0.6f))
5 .selectValue(app.config.render_animation_mixture, app.config.render_animation_mixture + "s(自定义)")
6 .setOnNonNullValueUpdated((observable, oldValue, newValue) -> {
7 app.config.render_animation_mixture = newValue.value();
8 app.config.save();
9 });Source: BehaviorModule.java
缓动函数下拉框的取值来源:
1 new ComboBoxSetup<>(configTransitionFunction).setItems(new NamedItem<>("线性(Linear)", EasingFunction.LINEAR.name()),
2 new NamedItem<>("正弦缓出(EaseOutSine)", EasingFunction.EASE_OUT_SINE.name()),
3 new NamedItem<>("三次方缓出(EaseOutCubic)", EasingFunction.EASE_OUT_CUBIC.name()),
4 new NamedItem<>("五次方缓出(EaseOutQuint)", EasingFunction.EASE_OUT_QUINT.name()))
5 .selectValue(app.config.transition_type, app.config.transition_type)
6 .setOnNonNullValueUpdated((observableValue, oldValue, newValue) -> {
7 app.config.transition_type = newValue.value();
8 app.config.save();
9 });Source: BehaviorModule.java
内嵌帮助手册
三个下拉框各有一个 ? 帮助按钮,通过匿名内部类继承 HelpHandbookEntrance 并实现 getHandbook(),返回一个针对目标 Label 的 ControlHelpHandbook,其 getContent() 提供中文说明文案。把帮助文案就近写在控件绑定代码旁边(而不是集中到资源文件),让每个选项的"是什么"与"为什么"在源码层面保持紧邻:
1 new HelpHandbookEntrance(app.body, configTransitionAnimationHelp) {
2 @Override
3 public Handbook getHandbook() {
4 return new ControlHelpHandbook(configTransitionAnimationLabel) {
5 @Override
6 public String getContent() {
7 return "此选项控制的是动画间切换的过渡速度,越慢的过渡会使得动画间切换越平滑。" +
8 "如果禁用过渡,那么动画间切换将会立即完成,而不会进行交叉过渡。";
9 }
10 };
11 }
12 };Source: BehaviorModule.java
transition_duration 与 transition_type 的帮助入口采用相同模式,分别说明属性过渡与缓动函数的语义(见 L247-L258、L268-L279)。
物理参数组与"恢复默认"
五个物理滑杆(重力、空气阻力、静摩擦、X/Y 速度上限)与前面的滑杆范式一致,但数值范围大得多(0–2000),因此统一使用 10 倍数吸附的 SimpleMultipleIntegerSliderSetup,单位分别为 px/s² 与 px/s:
1 SliderSetup<Integer> setupPhysicGravity = new SimpleMultipleIntegerSliderSetup(configPhysicGravity, 10);
2 setupPhysicGravity
3 .setDisplay(configPhysicGravityValue, "%d px/s²", "像素每平方秒 (pixel/s²)")
4 .setRange(0, 2000)
5 .setTicks(200, 10)
6 .setSliderValue(app.config.physic_gravity_acc)
7 .setOnChanged((observable, oldValue, newValue) -> {
8 app.config.physic_gravity_acc = setupPhysicGravity.getValidatedValue();
9 app.config.save();
10 });Source: BehaviorModule.java
与其它组不同,物理组有一个"恢复默认"按钮。它不是简单地把五个字段写回默认值,而是先通过 ArkConfig.getDefaultConfig() 取默认配置对象,再调用每个滑杆 Setup 的 setSliderValue() 把 UI 滑回默认位置——由于 setSliderValue 会触发既有的变更回调,配置的写回与持久化由既有链路自动完成,按钮回调无需重复任何写配置代码。之后它主动切换到设置标签页并弹出成功 Toast 作为操作反馈:
1 EventHandler<MouseEvent> configPhysicRestoreEvent = e -> {
2 ArkConfig defaults = ArkConfig.getDefaultConfig();
3 if (defaults != null) {
4 setupPhysicGravity.setSliderValue(defaults.physic_gravity_acc);
5 setupPhysicAirFriction.setSliderValue(defaults.physic_air_friction_acc);
6 setupPhysicStaticFriction.setSliderValue(defaults.physic_static_friction_acc);
7 setupPhysicSpeedLimitX.setSliderValue(defaults.physic_speed_limit_x);
8 setupPhysicSpeedLimitY.setSliderValue(defaults.physic_speed_limit_y);
9 Logger.info("Config", "Physic params restored");
10 }
11 };
12 configPhysicRestore.setOnMouseClicked(e -> {
13 configPhysicRestoreEvent.handle(e);
14 app.rootModule.moduleWrapperComposer.activate(1);
15 app.toast.showText("已恢复默认物理设置",
16 GuiPrefabs.Icons.getIcon(GuiPrefabs.Icons.SVG_CHECK, GuiPrefabs.COLOR_SUCCESS), Const.durationLong);
17 });Source: BehaviorModule.java
其中 app.rootModule.moduleWrapperComposer.activate(1) 借助 RootModule 的包装器把界面切到第 2 个标签页(即设置页),让用户能立即看到持久化后的配置值——这是"恢复默认"跨越两个模块的一次交互设计。
Configuration Options:本页面管理的全部配置字段
| 配置字段(ArkConfig) | 控件类型 | UI 范围/选项 | 单位/显示格式 | 说明 |
|---|---|---|---|---|
behavior_allow_walk | CheckBox | 布尔 | — | 是否允许行走行为 |
behavior_allow_sit | CheckBox | 布尔 | — | 是否允许坐下行为 |
behavior_allow_sleep | CheckBox | 布尔 | — | 是否允许睡眠行为 |
behavior_allow_special | CheckBox | 布尔 | — | 是否允许特殊(干员专属)动画 |
behavior_ai_activation | Slider (整数) | 0–16,步进 1 | %d 级 | AI 自主动画的活跃级别 |
behavior_walk_speed | Slider (整数, ×5 吸附) | 0–200 | %d px/s | 行走速度 |
behavior_allow_interact | CheckBox | 布尔 | — | 是否允许点击交互 |
behavior_do_peer_repulsion | CheckBox | 布尔 | — | 多桌宠之间的同伴斥力 |
behavior_direction_switching | ComboBox | 禁用=0 / 松开拖拽时=1 / 拖拽时=2 / 光标掠过时=3 | 整数编码 | 换向(水平翻转)触发时机 |
display_multi_monitors | CheckBox | 布尔 | — | 允许部署到多个显示屏 |
display_margin_bottom | Slider (整数) | 0–120 | %d px | 距屏幕底部的部署边距 |
initial_position_x / initial_position_y | Canvas DotPicker | 0.0–1.0 相对坐标 | float | 初始部署位置(比例) |
render_animation_mixture | ComboBox | 禁用=0 / 快速=0.1 / 标准=0.3 / 慢速=0.6 | 秒 | 动画间交叉过渡时长 |
transition_duration | ComboBox | 禁用=0 / 快速=0.1 / 标准=0.3 / 慢速=0.6 | 秒 | 属性(位置/透明度/翻转/描边)过渡时长 |
transition_type | ComboBox | LINEAR / EASE_OUT_SINE / EASE_OUT_CUBIC / EASE_OUT_QUINT | 枚举名(String) | 缓动函数 |
physic_gravity_acc | Slider (整数, ×10 吸附) | 0–2000 | %d px/s² | 重力加速度 |
physic_air_friction_acc | Slider (整数, ×10 吸附) | 0–2000 | %d px/s² | 空气摩擦加速度 |
physic_static_friction_acc | Slider (整数, ×10 吸附) | 0–2000 | %d px/s² | 静摩擦加速度 |
physic_speed_limit_x | Slider (整数, ×10 吸附) | 0–2000 | %d px/s | X 方向速度上限 |
physic_speed_limit_y | Slider (整数, ×10 吸附) | 0–2000 | %d px/s | Y 方向速度上限 |
所有字段的持久化目标均为启动器所维护的 Ark Pets 配置 JSON(默认模板见仓库 ArkPetsConfigDefault.json)。
运行时消费:核心进程如何使用这些配置
配置的最终消费者是 core 模块的 ArkPets 桌宠进程。它在创建渲染与行为对象时直接读取同一套字段,例如用 display_scale 构建角色、用 behavior_* 字段构建 GeneralBehavior 状态机、用 display_fps 限制前台帧率:
Gdx.graphics.setForegroundFPS(config.display_fps);Source: ArkPets.java
cha = new ArkChar(config, config.display_scale);
behavior = new GeneralBehavior(config, cha.animList);Source: ArkPets.java
主循环中,行为状态机决定下一个动画(behavior.autoAnim()、behavior.dragging()),移动速度则由 behavior_walk_speed 驱动,且按住 Ctrl 时翻倍:
walkWindow(config.behavior_walk_speed * (isCtrlPressed() ? 2 : 1) * mobility);Source: ArkPets.java
由此可见本页面各字段的意义边界:behavior_* 进入 GeneralBehavior 的决策逻辑,display_*/render_*/transition_*/physic_* 影响窗口与渲染层。行为状态机的内部实现不属于本页范围。
API Reference
BehaviorModule.initializeWith(ArkHomeFX app): void
Controller<ArkHomeFX> 接口的实现,模块装配后由宿主调用。
参数: app(ArkHomeFX)——启动器宿主,提供 config(全局 ArkConfig)、rootModule(标签页容器)、toast(提示组件)与 body(帮助手册挂载容器)。
行为: 依次执行 initConfigBehavior()(全部控件绑定)、initScheduledListener()(多显示器探测定时器),随后为本模块 ScrollPane 附加平滑滚动并禁用缓存。
BehaviorModule.initConfigBehavior(): void(私有)
行为: 对页面全部约 20 个控件执行"回填初始值 → 注册变更回调 → 回调内写 app.config 并 save()"的标准流程;额外完成 DotPicker 坐标拾取、三个帮助手册入口注册、物理组恢复默认按钮绑定。
BehaviorModule.initScheduledListener(): void(私有)
行为: 创建并启动一个 ScheduledService<Boolean>:首延 2500ms、周期 5000ms、失败重启,每次成功后把 Monitor.getMonitors().size() 写入 configDeployMultiMonitorsStatus。
Failure Modes, Edge Cases & Concurrency
- 自定义配置值兜底:若用户在配置文件中手写了超出预设档位的值(例如
behavior_direction_switching = 5或transition_duration = 0.45),下拉框通过selectValue(value, value + "(自定义)")追加自定义项展示,UI 不会显示空白或错误回退(见 L178-L186、L217-L225、L238-L246)。 - 非法滑杆值的合法化:滑杆回调只信任
getValidatedValue()而不是原始newValue,保证落盘值永远处于setRange范围并吸附到刻度粒度,杜绝越界或碎数污染配置。 - 空下拉选择保护:下拉框使用
setOnNonNullValueUpdated,只有选中非空条目才写配置,防止瞬时空选择清掉字段。 ArkConfig.getDefaultConfig()可能为 null:物理恢复默认的处理器对defaults做了判空,若默认配置加载失败则静默跳过滑杆重置,但仍会执行后续的页面切换与 Toast 提示(见 L332-L348)。- UI 线程安全:多显示器探测的
Task本体不触碰控件,文本更新发生在onSucceeded(FX 线程);disableScrollPaneCache通过Platform.runLater延迟到 FX 线程执行,均符合 JavaFX 线程模型。 - 无并发写冲突:页面没有"应用/取消"两级状态,所有回调即改即存;由于启动器与桌宠进程间不存在双向同步,运行中的桌宠不会实时感知配置变化,需要重启桌宠进程才生效——这是该架构明确的取舍:用配置文件的简单性换取实时性的缺失。
- 滚动性能规避:页面控件密集(5 个滑杆 + 4 个滑杆 Label + 3 个下拉 + 画布),因此显式禁用 ScrollPane 缓存防止滚动残影(L120)。
Performance & Operational Notes
- 周期任务每 5 秒做一次显示器枚举(
Monitor.getMonitors()),属于可接受的低频系统查询;setRestartOnFailure(true)保证枚举在异常环境(如显示器热插拔瞬间)下自愈。 - 每次控件交互都触发一次
app.config.save(),即整份配置 JSON 的同步落盘。对交互频率最高的滑杆而言,吸附粒度(×5 / ×10)实际上限制了回调触发次数,间接控制了 IO 频率。 - 部署坐标使用相对坐标存储,跨分辨率迁移时无需重新校准;
display_margin_bottom(0–120 px)则弥补任务栏等底部遮挡场景。
Extension Points
- 新增一个开关型行为选项:在 FXML 加
CheckBox→ 控制器加@FXML字段 → 在initConfigBehavior()中复制两行式"回填 + setAction 写存"范式 → 在ArkConfig增加对应字段与默认值。整条链路无其它耦合点。 - 新增滑杆型选项:按
SimpleIntegerSliderSetup/SimpleMultipleIntegerSliderSetup范式链式配置即可,显示格式、范围、吸附粒度全部由 Setup 工具承担。 - 新增下拉型选项:若核心模块已有枚举(如
EasingFunction),直接以enum.name()作为NamedItem的值接入,可复用"自定义值兜底"逻辑。 - 帮助文案:为任意新控件挂
HelpHandbookEntrance匿名实现即可获得统一的?按钮与浮层手册。
Related Links
- BehaviorModule.java —— 本页面控制器完整实现
- BehaviorModule.fxml —— 页面布局与控件声明
- ArkHomeFX.java —— 模块宿主与装配
- ArkPets.java —— 核心进程对配置的消费
- ArkPetsConfigDefault.json —— 默认配置模板