# XCGUI_Java_PackAge **Repository Path**: SnailcatMall/XCGUI_Java_PackAge ## Basic Information - **Project Name**: XCGUI_Java_PackAge - **Description**: java开发炫彩GUI窗体代码,打包器工具开源 - **Primary Language**: Java - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-09-19 - **Last Updated**: 2026-09-20 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # JSCM 打包工具 · 框架使用说明 基于 **Java 8 + 炫彩界面库(XCGUI / 链式封装 `XCChainedUI`)**、通过 JNI 调用 `Jscm.dll` 的桌面打包工具。 本文档沉淀自实际开发过程,重点记录框架用法、技巧、注意事项,以及踩坑后的解决方案。 --- ## 一、技术栈与运行前提 | 项 | 说明 | |---|---| | JDK | **必须 JDK 8**。`Jscm.dll` 与 JNI 绑定按此版本,其他版本会出现 `UnsatisfiedLinkError` 或直接崩溃 | | 界面框架 | 炫彩界面库 XCGUI,项目使用其链式封装 `XCChainedUI` + 组件库 `XC*ComponentUI` | | 原生库 | `Jscm.dll`(需 x86 / x64 两份),可由 `Jscm-Core.jar` 内的 `init_/` 解压提供 | | JSON | fastjson2(已打进 `Jscm-Core.jar`,直接 import) | | 字体 | 内嵌 `HarmonyOS_SansSC_Regular.ttf`,无需目标机器安装 | ### 模块结构 ``` org.example ├─ App 入口:SDK → 引擎 → 全局字体/文本质量 → 主窗 → 消息循环 ├─ core │ ├─ SdkLoader Jscm.dll 路径解析(本机 SDK → jar 内解压到临时目录) │ └─ UiMetrics 全部布局 / 字体 / 颜色尺寸令牌(集中管理,避免数值散落失配) └─ ui ├─ PackWindow 主窗外壳、自绘标题栏、选项卡、日志/进度、后台任务编排 ├─ WebPackForm / JarPackForm 两个功能表单 ├─ FormBuilder 表单行构建器(分组框 + 文本/目录/复选/下拉/气泡/虚表/按钮) └─ UiUtils 通用件:字体、主题重着色、自绘、编辑框读写、圆角/投影 ``` ### 编译与运行 ```bat :: 编译(JDK 8) javac -encoding UTF-8 -nowarn ^ -cp "..\Jscm-Core\out\patch;..\Jscm-Core\out\artifacts\Jscm_Core_jar\Jscm-Core.jar" ^ -d out\classes src\main\java\org\example\**\*.java :: 运行(classpath 必须包含 resources,字体与图标从这里读) java -cp "out\classes;..\Jscm-Core\out\patch;..\Jscm-Core\out\artifacts\Jscm_Core_jar\Jscm-Core.jar;src\main\resources" ^ org.example.App ``` 自定义 SDK 目录:`-Djscm.sdk=D:\path\to\sdk\` --- ## 二、启动顺序(不可颠倒) `App.java` 的编排顺序本身就是一组硬约束: ```java SdkLoader.setup(); // 1. 任何原生调用前,必须先备好 Jscm.dll 路径 XC_EnableResMonitor(true); XCChainedUI.App app = XCChainedUI.app(true); // 2. 初始化引擎,true = D2D 渲染 app.init(); applyGlobalTextStyle(); // 3. 字体 / DPI / 文本抗锯齿,必须在建控件之前 PackWindow window = new PackWindow(app); window.assemble(); window.show(); app.run(); // 4. 消息循环(阻塞) app.exit(); System.exit(0); ``` **为什么必须是这个顺序:** - `SdkLoader.setup()` 必须最先 —— 任何原生调用都可能触发 `Jscm.dll` 加载,路径没就绪就崩。 - 全局字体 / 文本质量必须在**创建窗口与控件之前**设置,之后设置不生效。 - `XC_EnableDPI` + `XC_EnableAutoDPI` 必须开启,否则高分屏下整窗被系统拉伸、文字发虚。 --- ## 三、框架核心模型 ### 3.1 句柄(handle)而非对象 XCGUI 的元素都是 `int` / `long` 句柄,Java 侧的对象只是薄包装。因此: - `handle() == 0` 通常表示创建失败,**每个原生调用前都要判空**。 - 句柄分两类,**不能混用**: - **元素句柄**(布局 / 面板 / 控件根布局)→ 走 `XEle_*` - **形状句柄**(`XCChainedUI.Text` 等)→ 走 `XCGUI_Shape.XShapeText_*` > 把形状句柄传给元素级 `XEle_SetTextColor` 会被当作元素解引用而**崩溃**。 ### 3.2 坐标系:全部相对父元素 `XEle_SetRectEx` 与各创建方法的坐标都是**相对父元素**,不是屏幕坐标。窗口本体坐标相对窗口左上角 `(0,0)`。 ```java /** 移动 / 缩放一个元素,坐标相对该元素的父元素。 */ public static void setRect(int hEle, int x, int y, int w, int h) { if (hEle == 0) return; XEle_SetRectEx(hEle, x, y, Math.max(1, w), Math.max(1, h), false, 0, 0); } ``` ### 3.3 事件绑定与「保活」 事件通过 `XCUIEventDispatcher.bind(元素, 事件, "回调方法名", 回调对象)` 绑定。**回调由 JNI 侧持有,Java 侧必须保留强引用,否则被 GC 回收后回调失效甚至崩溃。** ```java XEleEventCallBack draw = new XEleEventCallBack() { @Override public boolean OnDraw(int hEle, long hDraw) { /* ... */ return true; } }; UiUtils.keep(draw); // ← 关键 XCUIEventDispatcher.bind(hEle, XE_PAINT, "OnDraw", draw); ``` ### 3.4 渲染引擎:坚持 D2D,不要改回 GDI 引擎用 `XCChainedUI.app(true)`(D2D)。曾为拿到 ClearType 试过切 GDI,**代价是原生元素与带 Alpha 的绘制全部失效**:滚动条不显示、候选面板画不出来、下拉弹层背景变透明。这些都写在 XCGUI 内部实现里,应用层改不动。 结论:**引擎保持 D2D**,文字观感靠字体与各绘制面的渲染质量设置来补。 --- ## 四、框架代码使用技巧 ### 技巧 1|静态回调不能用匿名内部类捕获外部状态 窗口定时器回调必须是 `static`、无状态的(引擎会重新实例化回调对象),通过静态 `Map` 反查实例: ```java private static final Map INSTANCES = new ConcurrentHashMap<>(); private static final WindowEventCallBack TIMER_HANDLER = new WindowEventCallBack() { @Override public boolean OnWndTimer(int hWindow, long nIDEvent) { if (nIDEvent != TIMER_ID) return false; PackWindow w = INSTANCES.get(hWindow); if (w != null) w.drainQueue(); return false; } }; ``` ### 技巧 2|重绘一律用「延迟」重绘 `XEle_Redraw(h, false)` / `XWnd_Redraw(h, false)` 的 `false` = 延迟到本轮消息处理结束后统一重绘。用 `true`(立即重绘)会在事件回调栈内同步重绘、造成重入卡顿。 ```java public void redraw() { XWnd_Redraw(hWindow, false); } ``` ### 技巧 3|自绘前必须 `prepareDraw` 凡 `OnDraw` 回调,先调 `UiUtils.prepareDraw(hDraw)` 再绘制,否则该绘制面内文字发虚、有锯齿: ```java public static void prepareDraw(long hDraw) { XCGUI_Draw.XDraw_EnableSmoothingMode(hDraw, true); // 几何抗锯齿 XCGUI_Draw.XDraw_SetD2dTextRenderingMode(hDraw, TEXT_RENDERING_MODE); // ClearType 自然 XCGUI_Draw.XDraw_SetTextRenderingHint(hDraw, TEXT_RENDERING_HINT); // 灰度抗锯齿+网格拟合 } ``` ### 技巧 4|自绘背景让元素「自适应尺寸」 自绘回调每帧按元素当前尺寸重画,因此元素缩放后背景自动跟随,无需重算。颜色在**绘制时求值**,传主题令牌方法引用即可自动跟随主题切换: ```java UiUtils.fillBackground(panel.handle(), ElementTheme::background, 8); ``` > 注意传 `ElementTheme::background`(方法引用),而不是取好值的 `long`。 ### 技巧 5|主题切换:「一次性写入」要登记,「自绘」不用 颜色一旦写进原生元素(`XEle_SetTextColor` / `XEle_AddBkFill`)就固定了,切主题不会自己变。所以: - **自绘路径**:绘制时取色器重新求值 → **自动跟随**,无需登记。 - **一次性写入原生元素的颜色**:必须登记到 `UiUtils.onThemeChange(...)`,切主题后统一重刷。 ```java public static void themeText(final int hEle, final LongSupplier color) { Runnable apply = new Runnable() { @Override public void run() { XEle_SetTextColor(hEle, color.getAsLong()); } }; apply.run(); onThemeChange(apply); // 登记,切主题时重刷 } ``` ### 技巧 6|批量重着色:把「改色」和「上屏」分开 `ElementTheme.setMode()` 内部会逐个通知组件、对每个实例下发**立即重绘**,肉眼看到的就是「组件一个一个换主题」。本项目通过反射跳过那次通知,批量期间只改颜色、收尾整窗重绘一次,整批配色在同一帧翻过去: ```java public static void setThemeMode(boolean dark) { // 反射直接改 mode 字段 + 调 applyTokens(),绕开 notifyThemeChanged() THEME_MODE_FIELD.set(null, target); THEME_APPLY_TOKENS.invoke(null); } // 收尾:UiUtils.reapplyTheme() 重刷登记项 → 调用方整窗重绘一次 ``` ### 技巧 7|原生编辑框读写走封装 原生编辑框的文字是 UTF-16 字节流,直接用 API 容易踩编码坑,统一走 `UiUtils`: ```java String s = UiUtils.nativeText(hEdit); // 读全文(多行同样适用) UiUtils.setNativeText(hEdit, "…"); // 覆盖写入 UiUtils.initEdit(hEdit, true, true); // 多行 + 只读日志域(自动去边框/焦点框) ``` ### 技巧 8|后台任务 + UI 线程刷新 **所有 XCGUI 原生调用必须在 UI 线程**。后台打包线程只投递任务,由窗口定时器(`WM_TIMER`,100ms)在 UI 线程内排空队列: ```java public void post(Runnable task) { // 后台线程安全 if (task != null) uiQueue.offer(task); } private void drainQueue() { // 只在 UI 线程(定时器回调里)执行 Runnable r; while ((r = uiQueue.poll()) != null) { /* ... */ } } ``` ### 技巧 9|拖动窗口交给引擎 用原生 `XWnd_EnableDragWindow`,**不要自己算光标偏移**。自己实现那套依赖窗口级鼠标消息的坐标语义(屏幕坐标还是客户区坐标随引擎版本而变),一旦把屏幕坐标当客户区坐标判断「是否落在标题栏内」,拖拽就会整体失效。 同时,标题栏面板要**穿透鼠标**,否则空白区会吃掉按下事件: ```java XEle_EnableMouseThrough(bar.handle(), true); // 面板空白处可拖动窗口,子元素照常命中 XWnd_EnableDragWindow(hWindow, true); ``` ### 技巧 10|文件对话框的默认值 `SystemUtils.OpenFileDialog` / `SaveFileDialog` 是 JNI 薄封装,参数含义: | 参数 | 含义 | |---|---| | `filter` | `"说明(*.json)|*.json|所有文件(*.*)|*.*"` | | `initialFilterIndex` | 从 1 开始 | | `initialDirectory` | 初始目录,留空用系统默认 | | `defaultExtension` | 用户没写后缀时自动补 | | `promptOnOverwrite` | 保存时是否提示覆盖 | | `dontChangeDirectory` | 是否禁止改变当前目录 | --- ## 五、注意事项(重要,容易踩) 1. **必须用 JDK 8 启动**,否则原生库加载失败。 2. **不要把 `SdkLoader` 的方法写进静态字段初始化**,否则会在 SDK 路径就绪前触发 `Jscm.dll` 加载 → `UnsatisfiedLinkError`。 3. **classpath 必须包含 `resources` 目录**,字体与图标从这里读。 4. **资源路径的前导斜杠要区分**:`ClassLoader.getResourceAsStream` 不能带前导斜杠(如 `favicon.ico`);而 `UiUtils` 用的是 `Class#getResourceAsStream("/HarmonyOS_SansSC_Regular.ttf")`,语义不同,注意区分。 5. **句柄混用会崩溃**:形状句柄必须走 `XCGUI_Shape` 专用 API(见 3.1)。 6. **回调必须 `keep`**(见 3.3),否则随机失效。 7. **所有原生调用只能在 UI 线程**(见技巧 8)。 8. **不要用 `SetWindowRgn` 做圆角**:它是二值裁剪、不做抗锯齿,圆角会有锯齿。本项目改用「异型透明窗口 + 面板自绘抗锯齿圆弧」。 9. **不要在模态文件对话框关闭的返回栈里就地写大量控件**(见问题 8)。 10. **`XEle_SetRectEx` 的宽高至少为 1**,传 0 会导致元素异常。 --- ## 六、代码使用的技巧(表单构建) `FormBuilder` 用链式调用自上而下堆叠分组框,调用方**无需显式关闭分组**(下一次 `section` 或 `height()` 自动收尾): ```java form.section("基本信息") .text("jarPath", "JAR 文件", "", "要打包成 EXE 的可执行 JAR") .dir("outputDir", "输出目录", "", "留空则输出到 JAR 所在目录", "选择输出目录") .check("copyJre", "复制 JRE", true) .select("pathPrefixApi", "路径前缀 API", new String[]{"0", "1", "2"}, 2) .tags("runArgs", "默认运行参数", PRESET_ARGS, "输入自定义参数后回车加入气泡", ...) .table("rewrite", "路径改写列表", COLS, WIDTHS, rows, 120) .action("扫描 JAR 并自动填充", 140, this::scan) .section("SDK 与运行时") .note("……"); ``` 要点: - **布局占位高度**:滚动内容根面板初始高度给足(`EST_FORM_H = 4000`),否则装配期间创建的控件会被提前裁掉;装配完成后按表单实际高度回设。 - **分组框高度**:内容累计完后一次性 `XEle_SetHeight` 回设。 - **自适应高度变化**:气泡行增多后内容变高,必须回设根布局高度 + 滚动视图内容尺寸,否则超出的部分被裁掉、滚动条行程对不上(`onFormContentResize`)。 - **虚表列宽**:列宽合计要扣掉右侧预留的滚动条宽度(`TABLE_SCROLL_W = 24`),否则会多出横向滚动条。 - **表格回填**:`setTableRows` 用 `rows.clear() + addAll + tableRefresh` 三步。 --- ## 七、解决方案与问题 | # | 问题现象 | 根因 | 解决方案 | |---|---|---|---| | 1 | 小字号文字有锯齿、笔画发毛 | 组件库自绘回调只开几何抗锯齿,从不设置文字渲染质量 | 自绘面注入 `prepareDraw`;库内绘制面拿不到句柄,改用引擎级 `XC_SetD2dTextAntialiasMode(灰度抗锯齿)` | | 2 | 分组框「顶部边框线被选项卡压住」,看不到顶线 | 滚动视口把内容最上面 1px 压掉,正好压在卡片顶部描边上 | `FormBuilder.TOP_GAP = 2`,首张卡片整体下移 | | 3 | 主题切换「一个组件一个组件地换」 | 库内 40+ 组件静态注册了主题监听,回调里逐个立即重绘 | 反射绕过通知,批量只改色,收尾整窗重绘一次 | | 4 | 拖不动窗口 | 标题栏面板空白区吃掉了按下事件 | 面板 `XEle_EnableMouseThrough(true)`,拖动交给 `XWnd_EnableDragWindow` | | 5 | 自定义参数输入中文乱码 | 原生编辑框按字节流读写,编码未对齐 | 统一走 `UiUtils.nativeText` / `setNativeText`(内部用 UTF-8 宽字符串转换) | | 6 | 「展开」气泡面板收起后滚动条有残影 | 浮层覆盖过的区域不会自动重画 | 显隐浮层后调用 `redraw()`(`XWnd_Redraw(h, false)`) | | 7 | 气泡内容超出窗口高度 | 候选面板高度未受约束 | 面板加滚动区 + 按窗口可用空间钳制高度(下限保证标题与滚动条仍在) | | 8 | **走文件对话框读取配置时程序偶发崩溃** | 在「关闭模态对话框」返回途中的消息处理栈里**就地**回填大量控件并触发布局重算,与引擎恢复自身状态交错 | 回填**投递到 UI 队列**,等本轮消息结束后由窗口定时器统一执行 | | 9 | 原生列表在暗色主题下「看不出边界」 | 控件自身无外框,底色与卡片底色相同 | `tintBackground(hEle, 颜色, 描边色)` 补一条 1px 描边 | | 10 | 组件内部写死的白色背景去不掉 | 背景色被写进组件自身元素,透明化 API 无效 | 用 `tintBackground` 重新填充为页面色,并登记主题切换重刷 | ### 问题 8 的示例代码 ```java private void loadConfig() { String path = SystemUtils.OpenFileDialog(CONFIG_FILTER, hwnd(), "读取配置", 1, onJarTab() ? jarForm.configDir() : webForm.configDir(), "json", false, false, true); if (path == null || path.trim().isEmpty()) return; /* 回填要写大量控件并触发布局重算,必须跳出现在这层栈:此刻仍停在「关闭模态文件对话框」 返回途中的消息处理里,就地写控件会与引擎的状态恢复交错,偶发崩溃。投递到 UI 队列, 等本轮消息结束后由窗口定时器统一执行。 */ final String file = path.trim(); post(() -> applyConfigFile(file)); } ``` --- ## 八、功能速览 - **Web 打包**:整个 Web 项目目录 → 单个 `.jscm` 文件,可选加密。 - **JAR 打包**:可执行 JAR → EXE,覆盖 SDK/JRE 内嵌与裁剪、路径改写、环境变量、签名、图标、运行参数等。 - **保存 / 读取配置**:配置文件名 = 打包产物名 + `.json`,两个选项卡全部参数存同一个 JSON;读取后自动回填(缺失键保持原值)。 - **主题切换**:标题栏按钮切换明 / 暗色,整批配色同帧生效。 - **实时日志 / 进度**:后台打包线程 → UI 队列 → 定时器刷新。