Repository Wiki
isHarryh/Ark-Pets

行为与显示设置页面

行为与显示设置页面是 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

Loading diagram...

架构说明:

  • 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:一次配置修改的端到端路径

Loading diagram...

这条链路解释了为什么控制器里每个回调都以 app.config.save() 结尾:启动器与桌宠进程之间没有 IPC 同步机制,配置文件是两者之间唯一的契约,因此"即时落盘"是保证用户下一次启动桌宠即可生效的最低成本方案。

模块加载与初始化

BehaviorModule 是一个 final 类,实现泛型接口 Controller<ArkHomeFX>。它的全部控件都是 @FXML 注入的私有字段,按功能分组命名(configBehavior*、configDeploy*、configTransition*、configPhysic*),命名前缀直接对应 ArkConfig 中的字段前缀,便于一一对照。

java
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 体验优化:

java
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 回填,事件回调里写配置并保存:

java
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,这样写入配置的永远是落在合法刻度上的整数:

java
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 为粒度的吸附拖动,避免用户拖出无意义的碎数:

java
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 始终忠实反映真实配置:

java
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。使用相对坐标的原因显而易见:不同分辨率/不同显示器布局下,绝对像素值无法迁移,而比例值可以:

java
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:

java
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 与渲染层共用同一套函数定义:

java
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

缓动函数下拉框的取值来源:

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() 提供中文说明文案。把帮助文案就近写在控件绑定代码旁边(而不是集中到资源文件),让每个选项的"是什么"与"为什么"在源码层面保持紧邻:

java
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:

java
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 作为操作反馈:

java
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_walkCheckBox布尔—是否允许行走行为
behavior_allow_sitCheckBox布尔—是否允许坐下行为
behavior_allow_sleepCheckBox布尔—是否允许睡眠行为
behavior_allow_specialCheckBox布尔—是否允许特殊(干员专属)动画
behavior_ai_activationSlider (整数)0–16,步进 1%d 级AI 自主动画的活跃级别
behavior_walk_speedSlider (整数, ×5 吸附)0–200%d px/s行走速度
behavior_allow_interactCheckBox布尔—是否允许点击交互
behavior_do_peer_repulsionCheckBox布尔—多桌宠之间的同伴斥力
behavior_direction_switchingComboBox禁用=0 / 松开拖拽时=1 / 拖拽时=2 / 光标掠过时=3整数编码换向(水平翻转)触发时机
display_multi_monitorsCheckBox布尔—允许部署到多个显示屏
display_margin_bottomSlider (整数)0–120%d px距屏幕底部的部署边距
initial_position_x / initial_position_yCanvas DotPicker0.0–1.0 相对坐标float初始部署位置(比例)
render_animation_mixtureComboBox禁用=0 / 快速=0.1 / 标准=0.3 / 慢速=0.6秒动画间交叉过渡时长
transition_durationComboBox禁用=0 / 快速=0.1 / 标准=0.3 / 慢速=0.6秒属性(位置/透明度/翻转/描边)过渡时长
transition_typeComboBoxLINEAR / EASE_OUT_SINE / EASE_OUT_CUBIC / EASE_OUT_QUINT枚举名(String)缓动函数
physic_gravity_accSlider (整数, ×10 吸附)0–2000%d px/s²重力加速度
physic_air_friction_accSlider (整数, ×10 吸附)0–2000%d px/s²空气摩擦加速度
physic_static_friction_accSlider (整数, ×10 吸附)0–2000%d px/s²静摩擦加速度
physic_speed_limit_xSlider (整数, ×10 吸附)0–2000%d px/sX 方向速度上限
physic_speed_limit_ySlider (整数, ×10 吸附)0–2000%d px/sY 方向速度上限

所有字段的持久化目标均为启动器所维护的 Ark Pets 配置 JSON(默认模板见仓库 ArkPetsConfigDefault.json)。

运行时消费:核心进程如何使用这些配置

配置的最终消费者是 core 模块的 ArkPets 桌宠进程。它在创建渲染与行为对象时直接读取同一套字段,例如用 display_scale 构建角色、用 behavior_* 字段构建 GeneralBehavior 状态机、用 display_fps 限制前台帧率:

java
Gdx.graphics.setForegroundFPS(config.display_fps);

Source: ArkPets.java

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 时翻倍:

java
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 匿名实现即可获得统一的 ? 按钮与浮层手册。

Sources

(1 files)