Compare commits

...

4 Commits

Author SHA1 Message Date
gaozheng c4d76f57b6 新增claude.md(karpathy) 2026-06-09 21:24:28 +08:00
gaozheng 6df2c4832c chore(ela): ElaWidgetTools 评估 spike + 全面迁移计划 + 构建 TEMP 兜底
- spike/ela: 隔离 demo 验证 ElaWindow + ADS 内嵌 + QVTK + 明暗切换(Qt6.11.1/MSVC 构建通过)
- CMakeLists: FetchContent 引入 ElaWidgetTools(fork,SOURCE_SUBDIR 仅编库) + 挂 spike
- build.bat: TEMP/TMP 重定向到 D: 构建目录,规避 C: 盘满导致的 LNK1108
- docs: 全面 Ela 化迁移计划(P0-P4 + 控件映射表 + 风险登记)
2026-06-09 21:23:14 +08:00
gaozheng 1a9fb72cf0 feat(ui): impeccable 设计令牌体系 + 空状态/语义色/动效 + dock 标题修复
- typeset: Theme.hpp 新增排版令牌(type::),统一各处散落字号/字重
- layout: 间距/圆角令牌(space::/radius::),圆角 6 档→2 档,手调奇数余白对称化
- delight: 中央空状态引导浮层、上下文化加载文案、登录错误淡入
- colorize: 语义色令牌(semantic::),项目状态着色、状态栏错误染色、异常徽标警示色(休眠)
- overdrive(休眠): 详情视图相机补间+actor淡入(animateReveal),待 dd 详情渲染接通后激活
- fix(dock): restoreState 后重新隐藏 ADS 子窗口标题栏,修复已保存布局下标题栏复现
2026-06-09 20:26:00 +08:00
gaozheng caf6f9ebd0 docs(build): 新增 build.bat 一键构建脚本 + README 补充构建与运行说明 2026-06-09 19:07:14 +08:00
15 changed files with 860 additions and 99 deletions

65
CLAUDE.md Normal file
View File

@ -0,0 +1,65 @@
# CLAUDE.md
Behavioral guidelines to reduce common LLM coding mistakes. Merge with project-specific instructions as needed.
**Tradeoff:** These guidelines bias toward caution over speed. For trivial tasks, use judgment.
## 1. Think Before Coding
**Don't assume. Don't hide confusion. Surface tradeoffs.**
Before implementing:
- State your assumptions explicitly. If uncertain, ask.
- If multiple interpretations exist, present them - don't pick silently.
- If a simpler approach exists, say so. Push back when warranted.
- If something is unclear, stop. Name what's confusing. Ask.
## 2. Simplicity First
**Minimum code that solves the problem. Nothing speculative.**
- No features beyond what was asked.
- No abstractions for single-use code.
- No "flexibility" or "configurability" that wasn't requested.
- No error handling for impossible scenarios.
- If you write 200 lines and it could be 50, rewrite it.
Ask yourself: "Would a senior engineer say this is overcomplicated?" If yes, simplify.
## 3. Surgical Changes
**Touch only what you must. Clean up only your own mess.**
When editing existing code:
- Don't "improve" adjacent code, comments, or formatting.
- Don't refactor things that aren't broken.
- Match existing style, even if you'd do it differently.
- If you notice unrelated dead code, mention it - don't delete it.
When your changes create orphans:
- Remove imports/variables/functions that YOUR changes made unused.
- Don't remove pre-existing dead code unless asked.
The test: Every changed line should trace directly to the user's request.
## 4. Goal-Driven Execution
**Define success criteria. Loop until verified.**
Transform tasks into verifiable goals:
- "Add validation" → "Write tests for invalid inputs, then make them pass"
- "Fix the bug" → "Write a test that reproduces it, then make it pass"
- "Refactor X" → "Ensure tests pass before and after"
For multi-step tasks, state a brief plan:
```
1. [Step] → verify: [check]
2. [Step] → verify: [check]
3. [Step] → verify: [check]
```
Strong success criteria let you loop independently. Weak criteria ("make it work") require constant clarification.
---
**These guidelines are working if:** fewer unnecessary changes in diffs, fewer rewrites due to overcomplication, and clarifying questions come before implementation rather than after mistakes.

View File

@ -63,7 +63,21 @@ FetchContent_Declare(qtkeychain
GIT_TAG v0.14.0)
FetchContent_MakeAvailable(qtkeychain)
# ElaWidgetTools spike feat/elawidgettools Fluent UI for QWidget
# RainbowCandyX fork Qt6.10+ 6.11 SOURCE_SUBDIR
# /PySide bindingsMIT static DLL
# find_package(Qt6 Widgets/WidgetsPrivate) .qrc(靠全局 AUTORCC)
set(ELAWIDGETTOOLS_BUILD_STATIC_LIB ON CACHE BOOL "" FORCE)
FetchContent_Declare(elawidgettools
GIT_REPOSITORY https://github.com/RainbowCandyX/ElaWidgetTools.git
GIT_TAG main
SOURCE_SUBDIR ElaWidgetTools)
FetchContent_MakeAvailable(elawidgettools)
add_subdirectory(src)
# ElaWidgetTools spike demo geopro_desktop
add_subdirectory(spike/ela)
enable_testing()
add_subdirectory(tests)

View File

@ -8,7 +8,7 @@
## 技术栈
Qt 6.8 LTSQtWidgets+ VTK 9.3+ · CMake + vcpkg全量含 Qt· MSVC 2022 / C++17 · ADS 停靠 · GDAL/PROJ · OpenSSL · QtKeychain。
Qt 6.11QtWidgets+ VTK 9.6 · CMake + Ninja · 官方 MSVC 预编译 Qt + vcpkg仅非 Qt 依赖)· MSVCVS 2022/2026/ C++17 · ADS 停靠 · GDAL/PROJ · OpenSSL · QtKeychain。
## 目录(设计 §3
@ -25,19 +25,51 @@ tools/ 离线验证脚本validate_samples.py
docs/ 规约、API、样本数据、设计文档
```
## 快速开始
## 构建与运行
前置:VS2022(C++ 桌面开发)、Git、vcpkg`VCPKG_ROOT`)。详见 ENV_SETUP_Windows.md
前置:**Visual Studio 2022/2026**(勾选「使用 C++ 的桌面开发」工作负载,自带 CMake + Ninja、Git、**vcpkg** 并设环境变量 `VCPKG_ROOT`。构建方案②:单一官方 MSVC 预编译 Qt`CMAKE_PREFIX_PATH` → `D:/Qt/6.11.1/msvc2022_64`、VTK 预编译于 `external/vtk-install`、ADS/QtKeychain 经 FetchContent 对接同一份 Qt、仅非 Qt 依赖GDAL/PROJ/OpenSSL/…)走 vcpkg。详见 [docs/ENV_SETUP_Windows.md](docs/ENV_SETUP_Windows.md)
```powershell
# x64 Native Tools 命令行,项目根
vcpkg x-update-baseline --add-initial-baseline # 锁依赖版本
cmake --preset msvc-debug # 首次编译 Qt+VTK较久
cmake --build build/debug
.\build\debug\src\app\geopro_desktop.exe # spike 冒烟:应显示一个锥体
ctest --test-dir build/debug # 运行单测
> ⚠️ 本机 `cmake` / `ninja` / `cl` **默认不在 PATH**,必须在已激活 MSVC 环境的终端里构建。下面三种方式都已处理好这一点。
### 方式一:一键脚本(推荐)
项目根的 `build.bat` 自动用 vswhere 定位 VS、激活 MSVC 环境、按需配置并编译。在 **cmd** 里于项目根执行 `build <命令>`
| 命令 | 作用 |
|---|---|
| `build`(或 `build app` | 编译主程序 `geopro_desktop`(默认) |
| `build run` | 编译并运行主程序 |
| `build test` | 编译并跑单元测试ctest |
| `build all` | 编译全部目标 |
| `build configure` | 改了 CMakeLists / 新增源文件后,强制重新配置 |
### 方式二Visual Studio 打开文件夹
VS →「打开本地文件夹」→ 选仓库根 → 自动识别 `CMakePresets.json` → 选配置 **MSVC Release** → 菜单「生成 → 全部生成」;运行/调试目标选 `geopro_desktop`
### 方式三:手动命令行
开始菜单打开「**x64 Native Tools Command Prompt for VS**」(已带 MSVC 环境在仓库根CMake 用 VS 自带的全路径,因其不在 PATH
```bat
set CMAKE="%VSINSTALLDIR%Common7\IDE\CommonExtensions\Microsoft\CMake\CMake\bin\cmake.exe"
%CMAKE% --preset msvc-release :: 配置(首次 / 改 CMakeLists 后)
%CMAKE% --build build/release --target geopro_desktop :: 编译主程序
build\release\src\app\geopro_desktop.exe :: 运行
%CMAKE% --build build/release --target geopro_tests :: 编译测试
ctest --test-dir build/release --output-on-failure :: 跑测试
```
Debug 用 `--preset msvc-debug`,对应 `build/debug`。`%VSINSTALLDIR%` 在 Native Tools 提示符里已设好。)
### 构建目标与产物
- `geopro_desktop` — 主程序,产物 `build/release/src/app/geopro_desktop.exe`
- `geopro_tests` — 单元测试,配 `ctest`
- 不带 `--target` 编译全部
> 改了已有 `.cpp/.hpp` 直接 `--build`Ninja 增量);改了 `CMakeLists.txt` 或新增源文件需先 `--preset` / `build configure`。链接报 **LNK1104**(文件被占用)时,先关掉运行中的 `geopro_desktop.exe`
## 当前状态
M1 设计完成v2经双专家评审。进入 **spike 预研**(设计 §15① 全 vcpkg 构建/部署 ② ADS + QVTKOpenGLStereoWidget 停靠稳定 ③ 真实样本跑通 banded contour。spike 通过后展开完整实现计划。

85
build.bat Normal file
View File

@ -0,0 +1,85 @@
@echo off
REM ============================================================
REM geopro build helper (Windows / MSVC + Ninja, CMake presets)
REM
REM Usage: build [app | all | test | run | configure]
REM app (default) build target geopro_desktop
REM all build all targets
REM test build + run unit tests via ctest
REM run build + launch geopro_desktop
REM configure force re-run CMake configure (after CMakeLists changes)
REM
REM Requires: Visual Studio 2022/2026 (Desktop C++ workload, ships
REM CMake + Ninja) and the VCPKG_ROOT environment variable.
REM Note: cmake/ninja/cl are NOT on PATH on this machine; this script
REM locates VS via vswhere and activates the MSVC env itself.
REM ============================================================
setlocal
set "ROOT=%~dp0"
set "BUILDDIR=%ROOT%build\release"
set "PRESET=msvc-release"
REM 把临时目录指向 D: 的构建目录,规避 C: 盘满导致链接器写 %TEMP% 失败(LNK1108)。
REM 仅作用于本次构建(setlocal 作用域),不污染用户 shell。注意仍建议尽快清理 C: 盘。
set "TEMP=%BUILDDIR%\tmp"
set "TMP=%BUILDDIR%\tmp"
if not exist "%TEMP%" mkdir "%TEMP%"
REM --- locate Visual Studio (vswhere lives at a fixed path) ---
set "VSWHERE=%ProgramFiles(x86)%\Microsoft Visual Studio\Installer\vswhere.exe"
if not exist "%VSWHERE%" (
echo [build] vswhere not found. Open "x64 Native Tools Command Prompt for VS" and build manually.
exit /b 1
)
for /f "usebackq tokens=*" %%i in (`"%VSWHERE%" -latest -property installationPath`) do set "VSPATH=%%i"
if not defined VSPATH ( echo [build] Visual Studio not found. & exit /b 1 )
set "VCVARS=%VSPATH%\VC\Auxiliary\Build\vcvars64.bat"
set "CMAKE=%VSPATH%\Common7\IDE\CommonExtensions\Microsoft\CMake\CMake\bin\cmake.exe"
set "CTEST=%VSPATH%\Common7\IDE\CommonExtensions\Microsoft\CMake\CMake\bin\ctest.exe"
if not exist "%VCVARS%" ( echo [build] vcvars64.bat not found: %VCVARS% & exit /b 1 )
if not exist "%CMAKE%" ( echo [build] cmake not found: %CMAKE% & exit /b 1 )
REM --- activate MSVC environment (cl / link / include / lib) ---
call "%VCVARS%" >nul
set "CMD=%~1"
if "%CMD%"=="" set "CMD=app"
if /i "%CMD%"=="configure" goto :configure
if /i "%CMD%"=="app" goto :app
if /i "%CMD%"=="all" goto :all
if /i "%CMD%"=="test" goto :test
if /i "%CMD%"=="run" goto :run
echo [build] unknown command "%CMD%". Use: app ^| all ^| test ^| run ^| configure
exit /b 1
:ensure
if not exist "%BUILDDIR%\CMakeCache.txt" "%CMAKE%" --preset %PRESET%
exit /b 0
:configure
"%CMAKE%" --preset %PRESET%
exit /b %errorlevel%
:app
call :ensure
"%CMAKE%" --build "%BUILDDIR%" --target geopro_desktop
exit /b %errorlevel%
:all
call :ensure
"%CMAKE%" --build "%BUILDDIR%"
exit /b %errorlevel%
:test
call :ensure
"%CMAKE%" --build "%BUILDDIR%" --target geopro_tests || exit /b 1
"%CTEST%" --test-dir "%BUILDDIR%" --output-on-failure
exit /b %errorlevel%
:run
call :ensure
"%CMAKE%" --build "%BUILDDIR%" --target geopro_desktop || exit /b 1
"%BUILDDIR%\src\app\geopro_desktop.exe"
exit /b %errorlevel%

View File

@ -0,0 +1,83 @@
# geopro_desktop → ElaWidgetTools (Fluent) 迁移计划
**分支**`feat/elawidgettools` **日期**2026-06-09 **决策**:全面 Ela 化(最彻底),支持明/暗主题。
**前置评估已完成**spike(`spike/ela/`) 证明 ElaWidgetTools(RainbowCandyX fork) 可用官方 Qt 6.11.1 + MSVC 经 FetchContent 构建ElaWindow + ADS 内嵌 + QVTK 渲染均可行ElaTheme 明暗切换可用,但**只自动覆盖 Ela\* 控件与 ElaWindow 外壳**,标准 QWidget/ADS/VTK 需手工主题联动。
---
## 0. 硬前提(动手前必须满足)
- **P0-a 清理 C: 盘**:当前 C: 0 GB 可用,链接器写 `%TEMP%` 失败(`LNK1108`)。迁移需大量构建验证。需用户清理 C:;我同时把 `TEMP/TMP→D:` 兜底固化进 `build.bat`,避免反复手动重定向。
- **P0-b 验证靠用户**:每阶段我构建通过后,用户运行 + 截图,我据反馈迭代(登录门槛 + GUI我无法目视
- **P0-c 回退保障**:全程保留 `GEOPRO_UI_SHELL=classic|ela` 环境变量开关,可在「现 QMainWindow 壳」与「ElaWindow 壳」间切换,便于 A/B 与回退;迁移稳定后再移除。
## 1. 依赖固化P0 工程)
- ElaWidgetTools 从 spike 提升为**正式依赖**`FetchContent` 的 `GIT_TAG``main` **钉到具体 commit**(可复现)。
- **静/动态**先静态MIT省 DLL若遇静态资源字体/SVG 图标 .qrc被剥离导致图标缺失改动态`ELAWIDGETTOOLS_BUILD_STATIC_LIB OFF` + DLL 随 `TARGET_RUNTIME_DLLS` 拷贝)。
- **插件部署**:把 `platforms / styles / imageformats / iconengines`(含 SVG 图标用的 `qsvg`/`qsvgicon`)部署接进 `geopro_desktop` 的 post-build今天 spike 是手动拷的要正式化windeployqt 会被 ADS 的 DLL 依赖卡住,改为显式 copy Qt plugins
## 2. P1 — 换壳(带开关)
- 新建 `ElaShellWindow`(继承 `ElaWindow`)或在 `main()` 分支构造。把现有 `buildWorkbench()` 产出的中心内容ADS `CDockManager` + 工具条)作为 ElaWindow 的一个 page/central content 挂入(`addPageNode` / `setCentralCustomWidget`)。
- `eApp->init()``QApplication` 后调用;保留高 DPI 与 QVTK surface format 设置顺序。
- **dock 持久化注意**ElaWindow 接管后,`restoreState` 后重隐藏 ADS 标题栏的时序修复(已在主分支)需在新壳下复核。
- **验收**`GEOPRO_UI_SHELL=ela` 启动 → 登录 → 工作台ADS 停靠可拖动;中央/详情 VTK 正常;导航/标题栏 Fluent 外观。截图确认。
## 3. P2 — 主题桥(明/暗覆盖所有非 Ela 面)
- 新建 `ThemeBridge`:监听 `ElaTheme::themeModeChanged`,把明/暗同步到:
1. **全局 QSS**:把 `Theme.cpp``kStyleSheet` 拆成「明」与「暗」两版(用已有 `type/space/radius/semantic` 令牌派生暗色盘),按主题切换。
2. **ADS 停靠区**`CDockManager::setStyleSheet` 明/暗两套。
3. **VTK 背景**:中央 + 详情 renderer 背景随主题切深/浅底并 `Render()`
4. **内联样式面板**PanelHeader / TopBar / LoginWindow / main 浮层 的内联 QSS 改为「跟随主题」(去硬编码色,引用桥提供的明/暗令牌)。
- **暗色盘设计**:在 `Theme.hpp` 增加暗色语义surface/ink/border/accent 的暗版保持品牌蓝在暗底的可读性与对比度≥4.5:1
- **验收**:明/暗一键切换,全界面(外壳+停靠+面板+VTK协调一致、无残留亮/暗块;对比度达标。截图明、暗各一。
## 4. P3 — 全面控件 Ela 化(工作量主体)
逐面替换标准控件为 `Ela*` 等价物,"白嫖"明暗与 Fluent 观感。映射(精确 Ela 类名在实施时按头文件确认):
| 现状 | → Ela 等价 | 所在 |
|---|---|---|
| QPushButton | ElaPushButton | LoginWindow / 各处 |
| QLineEdit | ElaLineEdit | LoginWindow / ProjectListDialog 过滤 |
| QCheckBox | ElaCheckBox | LoginWindow / 图层浮层 / 异常列表 |
| QComboBox | ElaComboBox | ProjectListDialog / 全局 |
| QLabel(文本) | ElaText | 各处文本/标题 |
| QToolButton(Tab/操作) | ElaToolButton / ElaIconButton | PanelHeader / TopBar |
| QMenuBar / QMenu | ElaMenuBar / ElaMenu | TopBar |
| QTreeWidget | ElaTreeView(+model) 或保留+主题联动 | ObjectTreePanel |
| QListWidget | ElaListView(+model) 或保留+联动 | Dataset/Anomaly 面板 |
| QTableWidget | ElaTableView 或保留+联动 | ProjectListDialog |
| QProgressBar | ElaProgressBar | 全局 |
| QStatusBar | ElaStatusBar 或保留+联动 | main |
| QTabWidget/分段 | ElaTabWidget / ElaToggleSwitch | PanelHeader 数据/文件 |
| QDialog(登录) | ElaWidget/ElaWindow 风格弹窗 | LoginWindow |
| **保留(无替代)** | QVTKOpenGLStereoWidget、ADS CDockManager | 中央/详情/停靠 |
- 树/列表/表若用 Ela 的 View 需改 model成本高可分两步先保留 widget 版做主题联动,后续再评估换 View。
- 登录窗重做为 Fluent 风格(沿用现有令牌与文案)。
## 5. P4 — 收尾
- 插件部署正式化、ElaWidgetTools 版本锁定、静态资源核验。
- 去掉 `GEOPRO_UI_SHELL` 过渡开关(确认稳定后)。
- 开源声明ElaWidgetTools(MIT)、ADS(LGPLv2.1)、Qt(LGPL) NOTICE 归集。
- 回归:登录、项目切换、对象树、数据集/文件分页、异常、VTK 各视图、dock 持久化。
## 风险登记
| 风险 | 缓解 |
|---|---|
| Qt 6.11 Windows Popup 渲染(作者红旗) | spike 已初验P1 重点复核菜单/下拉/提示;必要时打 fork 的条件补丁 |
| ADS 在 ElaWindow 内主题/交互异常 | spike 已验内嵌P2 专门做 ADS 明暗 QSS |
| Ela View 需 model 重写(树/列表/表) | 分步:先 widget 版主题联动,再评估换 View |
| 静态库资源剥离(图标/字体缺失) | 改动态库 |
| 我无法目视 | 每阶段用户运行+截图验收 |
| C: 满导致构建反复失败 | 清理 C: + TEMP→D: 固化进 build.bat |
| 大重构回归 | 全程 env 开关可回退;主分支零影响 |
## 执行顺序
P0 → P1验收→ P2验收→ P3按面板分批每批验收→ P4。每步构建通过后由用户运行+截图确认再进下一步。

18
spike/ela/CMakeLists.txt Normal file
View File

@ -0,0 +1,18 @@
# ElaWidgetTools spike demo exe feat/elawidgettools
# geopro_desktop ElaWidgetTools(Fluent ) + ADS + VTK
add_executable(geopro_ela_spike WIN32 main.cpp)
target_link_libraries(geopro_ela_spike PRIVATE
Qt6::Core Qt6::Gui Qt6::Widgets
ElaWidgetTools
ads::qt6advanceddocking
${VTK_LIBRARIES})
vtk_module_autoinit(TARGETS geopro_ela_spike MODULES ${VTK_LIBRARIES})
if(WIN32)
add_custom_command(TARGET geopro_ela_spike POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
$<TARGET_RUNTIME_DLLS:geopro_ela_spike> $<TARGET_FILE_DIR:geopro_ela_spike>
COMMAND_EXPAND_LISTS)
endif()

121
spike/ela/main.cpp Normal file
View File

@ -0,0 +1,121 @@
// ElaWidgetTools 评估 spike隔离 demo不属于产品 app仅 feat/elawidgettools 分支评估用)。
// 一锤定音验证四件事:
// ① 用你们官方 Qt 6.11.1 + MSVC 经 FetchContent 能否构建 ElaWidgetTools(RainbowCandyX fork)
// ② ElaWindow 的 Fluent 观感在你们机器上渲染是否正常(重点看 Qt6.11 的 Popup/弹窗);
// ③ Qt Advanced Docking System(ADS) 能否内嵌进 ElaWindow
// ④ QVTKOpenGLStereoWidget 视口在 ElaWindow + ADS 内能否正常渲染。
// 结论决定是否值得对真实 app 做外壳重构。
#include <QApplication>
#include <QLabel>
#include <QSurfaceFormat>
#include <QVBoxLayout>
#include <QWidget>
#include "ElaApplication.h"
#include "ElaDef.h"
#include "ElaPushButton.h"
#include "ElaText.h"
#include "ElaTheme.h"
#include "ElaWindow.h"
#include <DockManager.h>
#include <DockWidget.h>
#include <QVTKOpenGLStereoWidget.h>
#include <vtkActor.h>
#include <vtkConeSource.h>
#include <vtkGenericOpenGLRenderWindow.h>
#include <vtkNew.h>
#include <vtkPolyDataMapper.h>
#include <vtkProperty.h>
#include <vtkRenderer.h>
int main(int argc, char* argv[])
{
QApplication::setHighDpiScaleFactorRoundingPolicy(
Qt::HighDpiScaleFactorRoundingPolicy::PassThrough);
QSurfaceFormat::setDefaultFormat(QVTKOpenGLStereoWidget::defaultFormat());
QApplication app(argc, argv);
eApp->init(); // ElaApplication 初始化Fluent 主题/字体/动画基建)
ElaWindow window;
window.setWindowTitle(QStringLiteral("ElaWidgetTools Spike — Fluent + ADS + VTK"));
window.resize(1200, 760);
// 「工作台」页内嵌 ADS 停靠管理器:验证 ADS 能否在 ElaWindow 内正常工作。
auto* dockHost = new QWidget;
auto* hostLay = new QVBoxLayout(dockHost);
hostLay->setContentsMargins(0, 0, 0, 0);
auto* dockManager = new ads::CDockManager(dockHost);
hostLay->addWidget(dockManager);
// dock1Fluent 控件样例看观感ElaText / ElaPushButton 与标准控件对照)。
auto* sample = new QWidget;
sample->setObjectName(QStringLiteral("sampleHost"));
auto* sLay = new QVBoxLayout(sample);
sLay->setContentsMargins(16, 16, 16, 16);
sLay->setSpacing(12);
sLay->addWidget(new ElaText(QStringLiteral("ElaText —— Fluent 文本"), sample));
sLay->addWidget(new ElaPushButton(QStringLiteral("ElaPushButton 主操作"), sample));
sLay->addWidget(new QLabel(QStringLiteral("(对照)标准 QLabel"), sample));
// 浅/深主题切换ElaWidgetTools 内置 ElaTheme运行期一键切换整套 Fluent 主题。
auto* themeBtn = new ElaPushButton(QStringLiteral("切换 浅色 / 深色"), sample);
QObject::connect(themeBtn, &QPushButton::clicked, themeBtn, [] {
eTheme->setThemeMode(eTheme->getThemeMode() == ElaThemeType::Light ? ElaThemeType::Dark
: ElaThemeType::Light);
});
sLay->addWidget(themeBtn);
sLay->addStretch();
auto* d1 = new ads::CDockWidget(QStringLiteral("Fluent 控件"));
d1->setWidget(sample);
dockManager->addDockWidget(ads::LeftDockWidgetArea, d1);
// dock2QVTK 视口(蓝色锥体),验证 VTK 在 ElaWindow + ADS 内渲染。
auto* vtkWidget = new QVTKOpenGLStereoWidget;
vtkNew<vtkGenericOpenGLRenderWindow> renderWindow;
vtkNew<vtkRenderer> renderer;
renderer->SetBackground(1.0, 1.0, 1.0);
vtkWidget->setRenderWindow(renderWindow);
renderWindow->AddRenderer(renderer);
vtkNew<vtkConeSource> cone;
cone->SetResolution(48);
vtkNew<vtkPolyDataMapper> mapper;
mapper->SetInputConnection(cone->GetOutputPort());
vtkNew<vtkActor> actor;
actor->SetMapper(mapper);
actor->GetProperty()->SetColor(0.18, 0.42, 0.71); // 品牌蓝 #2D6CB5 近似
renderer->AddActor(actor);
renderer->ResetCamera();
auto* d2 = new ads::CDockWidget(QStringLiteral("VTK 视口(锥体)"));
d2->setWidget(vtkWidget);
dockManager->addDockWidget(ads::RightDockWidgetArea, d2);
// 关键发现演示ADS 停靠区与普通 QWidget 不会自动跟随 ElaTheme只有 Ela* 控件与
// ElaWindow 外壳跟随)。这里手动把「停靠区背景 + 普通容器 + VTK 背景」同步到当前主题,
// 并监听 ElaTheme::themeModeChanged。这段「同步」正是真集成时要为每个非 Ela 面板付出的成本。
auto* rendererPtr = renderer.Get();
auto* rwPtr = renderWindow.Get();
auto applyContentTheme = [dockManager, sample, rendererPtr, rwPtr](ElaThemeType::ThemeMode mode) {
const bool dark = (mode == ElaThemeType::Dark);
const QString bg = dark ? QStringLiteral("#1E1F22") : QStringLiteral("#FFFFFF");
const QString fg = dark ? QStringLiteral("#E3E3E3") : QStringLiteral("#1F2A3D");
dockManager->setStyleSheet(
QStringLiteral("ads--CDockAreaWidget, ads--CDockContainerWidget { background:%1; }")
.arg(bg));
sample->setStyleSheet(
QStringLiteral("#sampleHost { background:%1; } #sampleHost QLabel { color:%2; }")
.arg(bg, fg));
rendererPtr->SetBackground(dark ? 0.11 : 1.0, dark ? 0.12 : 1.0, dark ? 0.14 : 1.0);
rwPtr->Render();
};
QObject::connect(eTheme, &ElaTheme::themeModeChanged, dockManager,
[applyContentTheme](ElaThemeType::ThemeMode m) { applyContentTheme(m); });
applyContentTheme(eTheme->getThemeMode()); // 初始同步当前主题
window.addPageNode(QStringLiteral("工作台"), dockHost);
window.show();
return app.exec();
}

View File

@ -1,5 +1,7 @@
#include "PanelHeader.hpp"
#include "Theme.hpp"
#include <QButtonGroup>
#include <QColor>
#include <QHBoxLayout>
@ -20,19 +22,33 @@ constexpr int kTitleIcon = 20; // 表头标题图标
constexpr int kActionIcon = 19; // 表头操作按钮图标
constexpr int kTabIcon = 19; // Tab 图标
// 表头统一样式(标准表头 + Tab 表头共用)。
const char* kHeaderQss =
"#panelHeader { background:#FFFFFF; border-bottom:1px solid #E6EAF1; }"
"#panelTitle { color:#1F2A3D; font-size:14px; font-weight:600; }"
"#panelBadge { background:#EAEEF5; color:#5A6B85; border-radius:9px;"
" padding:1px 7px; font-size:12px; font-weight:600; }"
"QToolButton#panelAction { border:none; border-radius:7px; padding:5px; }"
"QToolButton#panelAction:hover { background:#EEF3FB; }"
"QToolButton#tabBtn { border:none; border-bottom:2px solid transparent; color:#5A6B85;"
" padding:8px 4px; font-size:14px; }"
"QToolButton#tabBtn:hover { color:#1F2A3D; }"
"QToolButton#tabBtn:checked { color:#1F2A3D; font-weight:600;"
" border-bottom:2px solid #2D6CB5; }";
// 表头统一样式(标准表头 + Tab 表头共用)。字号/字重引用 Theme 排版令牌:
// 面板标题=title(15)、徽标=caption(12)、Tab 文本=body(13),加粗统一 semibold。
// #panelBadge 为中性计数徽标;#panelBadgeWarn 为“需注意”变体(语义 warning 色),
// 供异常计数等承载“待复查”含义的徽标使用(调用方改 objectName 即切换)。
QString headerQss()
{
return QStringLiteral(
"#panelHeader { background:#FFFFFF; border-bottom:1px solid #E6EAF1; }"
"#panelTitle { color:#1F2A3D; font-size:%1px; font-weight:%4; }"
"#panelBadge { background:#EAEEF5; color:#5A6B85; border-radius:9px;"
" padding:1px 7px; font-size:%2px; font-weight:%4; }"
"#panelBadgeWarn { background:%5; color:%6; border-radius:9px;"
" padding:1px 7px; font-size:%2px; font-weight:%4; }"
"QToolButton#panelAction { border:none; border-radius:7px; padding:5px; }"
"QToolButton#panelAction:hover { background:#EEF3FB; }"
"QToolButton#tabBtn { border:none; border-bottom:2px solid transparent; color:#5A6B85;"
" padding:8px 4px; font-size:%3px; }"
"QToolButton#tabBtn:hover { color:#1F2A3D; }"
"QToolButton#tabBtn:checked { color:#1F2A3D; font-weight:%4;"
" border-bottom:2px solid #2D6CB5; }")
.arg(type::kTitle)
.arg(type::kCaption)
.arg(type::kBody)
.arg(type::kWeightSemibold)
.arg(QString::fromUtf8(semantic::kWarningFill))
.arg(QString::fromUtf8(semantic::kWarning));
}
// 数量徽标(默认隐藏,调用方 setText+setVisible 显示)。
QLabel* makeBadge(QWidget* parent)
@ -65,7 +81,7 @@ QWidget* buildPanelHeader(Glyph icon, const QString& title, const QVector<Header
auto* header = new QWidget();
header->setObjectName(QStringLiteral("panelHeader"));
header->setFixedHeight(kHeaderHeight);
header->setStyleSheet(QString::fromUtf8(kHeaderQss));
header->setStyleSheet(headerQss());
auto* lay = new QHBoxLayout(header);
lay->setContentsMargins(12, 0, 8, 0);
@ -98,7 +114,7 @@ TabbedPanel buildTabbedPanel(const QVector<PanelTab>& tabs, const QVector<Header
auto* header = new QWidget(box);
header->setObjectName(QStringLiteral("panelHeader"));
header->setFixedHeight(kHeaderHeight);
header->setStyleSheet(QString::fromUtf8(kHeaderQss));
header->setStyleSheet(headerQss());
auto* hlay = new QHBoxLayout(header);
hlay->setContentsMargins(10, 0, 8, 0);
hlay->setSpacing(2);

View File

@ -3,6 +3,7 @@
#include <QAbstractItemView>
#include <QColor>
#include <QComboBox>
#include <QFont>
#include <QHBoxLayout>
#include <QHeaderView>
#include <QLabel>
@ -13,6 +14,8 @@
#include <QTableWidgetItem>
#include <QVBoxLayout>
#include "Theme.hpp"
namespace geopro::app {
namespace {
QString statusText(int s) {
@ -22,6 +25,15 @@ QString statusText(int s) {
default: return QString::number(s);
}
}
// 状态语义色(寻路):未开始=弱化中性、进行中=信息蓝(活动中);未知状态用中性灰。
const char* statusColorHex(int s) {
switch (s) {
case 1: return "#8A93A3"; // 未开始:弱化
case 2: return semantic::kInfo; // 进行中:活动中
default: return "#5A6B85"; // 未知:中性
}
}
} // namespace
ProjectListDialog::ProjectListDialog(data::IProjectRepository& repo, QWidget* parent)
@ -145,7 +157,15 @@ void ProjectListDialog::query() {
nameItem->setForeground(QColor("#2D6CB5"));
table_->setItem(i, 1, nameItem);
set(2, QString::fromStdString(p.code));
set(3, statusText(p.status));
// 状态列语义着色:颜色承载“未开始/进行中”分类,进行中加粗强调(不只靠颜色)。
auto* statusItem = new QTableWidgetItem(statusText(p.status));
statusItem->setForeground(QColor(statusColorHex(p.status)));
if (p.status == 2) {
QFont f = statusItem->font();
f.setBold(true);
statusItem->setFont(f);
}
table_->setItem(i, 3, statusItem);
set(4, QString::fromStdString(p.typeName));
set(5, QString::fromStdString(p.ownerCompany));
set(6, QString::fromStdString(p.responsiblePerson));

View File

@ -26,7 +26,7 @@ QToolTip {
background: #1F2A3D;
color: #F4F6FA;
border: 1px solid #2D6CB5;
border-radius: 4px;
border-radius: 6px;
padding: 4px 8px;
}
@ -42,7 +42,7 @@ QToolBar QToolButton {
background: transparent;
color: #5A6B85;
border: none;
border-radius: 7px;
border-radius: 8px;
padding: 6px 14px;
font-weight: 500;
}
@ -75,7 +75,7 @@ QTreeWidget, QListWidget, QTreeView, QListView {
outline: none;
}
QTreeWidget::item, QListWidget::item, QTreeView::item, QListView::item {
padding: 7px 8px;
padding: 8px 8px;
border-radius: 6px;
margin: 1px 4px;
}
@ -170,7 +170,7 @@ QLineEdit {
color: #1F2A3D;
border: 1px solid #C7D2E0;
border-radius: 6px;
padding: 5px 8px;
padding: 6px 8px;
selection-background-color: #2D6CB5;
selection-color: #FFFFFF;
}
@ -190,7 +190,7 @@ QScrollBar:vertical {
}
QScrollBar::handle:vertical {
background: #C2CCDA;
border-radius: 5px;
border-radius: 6px;
min-height: 28px;
}
QScrollBar::handle:vertical:hover {
@ -203,7 +203,7 @@ QScrollBar:horizontal {
}
QScrollBar::handle:horizontal {
background: #C2CCDA;
border-radius: 5px;
border-radius: 6px;
min-width: 28px;
}
QScrollBar::handle:horizontal:hover {
@ -253,7 +253,7 @@ QMenuBar {
}
QMenuBar::item {
background: transparent;
padding: 5px 12px;
padding: 6px 12px;
border-radius: 6px;
}
QMenuBar::item:selected {
@ -264,11 +264,11 @@ QMenu {
color: #1F2A3D;
border: 1px solid #D5DBE5;
border-radius: 8px;
padding: 5px;
padding: 6px;
}
QMenu::item {
padding: 6px 24px 6px 14px;
border-radius: 5px;
border-radius: 6px;
}
QMenu::item:selected {
background: #DCE9F8;
@ -277,7 +277,7 @@ QMenu::item:selected {
QMenu::separator {
height: 1px;
background: #E1E6EE;
margin: 5px 8px;
margin: 6px 8px;
}
/* ── 下拉框(按需出现时也与主题一致)──────────────────────── */
@ -286,7 +286,7 @@ QComboBox {
color: #1F2A3D;
border: 1px solid #C2CCDA;
border-radius: 6px;
padding: 5px 10px;
padding: 6px 10px;
min-height: 18px;
}
QComboBox:hover {
@ -327,14 +327,14 @@ QGroupBox::title {
QProgressBar {
background: #E6EBF3;
border: none;
border-radius: 5px;
border-radius: 6px;
height: 8px;
text-align: center;
color: #5A6B85;
}
QProgressBar::chunk {
background: #2D6CB5;
border-radius: 5px;
border-radius: 6px;
}
/* ── ADS 停靠:标题栏做成「固定面板表头」(对齐原型)──────────────
@ -416,12 +416,19 @@ void applyTheme(QApplication& app)
app.setStyle(QStyleFactory::create(QStringLiteral("Fusion")));
// 基础字体:中文界面用 微软雅黑 UI 渲染最清爽;缺失时 Qt 自动回退。
// 10pt≈13px对齐主流商用客户端基准9pt 偏小显拥挤。抗锯齿优先,观感更精致。
QFont base(QStringLiteral("Microsoft YaHei UI"), 10);
// 基准字号取排版令牌 type::kBody(13px)——统一为 px与 QSS 同单位
// (旧值 10pt≈13.3px观感几乎不变9pt 偏小显拥挤。抗锯齿优先,观感更精致。
QFont base(QStringLiteral("Microsoft YaHei UI"));
base.setPixelSize(type::kBody);
base.setStyleStrategy(QFont::PreferAntialias);
app.setFont(base);
app.setPalette(buildPalette());
// 注意:不要给 ADS 停靠标题ads--CDockWidgetTab QLabel追加任何样式——
// 这些子窗口标题栏在 main.cpp 里被 setVisible(false) 刻意隐藏(表头由各面板
// 自绘的 PanelHeader 承担)。改写其字号/内边距会变更度量并触发 ADS 重新
// 评估标题栏可见性,把隐藏的标题又显示出来。字号统一只作用于可见控件。
app.setStyleSheet(QString::fromUtf8(kStyleSheet));
}

View File

@ -13,6 +13,73 @@ class QApplication;
namespace geopro::app {
// ── 排版令牌(全项目唯一字号阶 + 字重角色)──────────────────────────
// 各处 QSS 的 font-size / font-weight 一律引用这些值,不再散落硬编码 px。
// 阶比 ~1.18body→title→heading刻意拉开层级——避免 11/12/13/14
// 这类只差 1px 的"糊层级",让标题/正文/说明三档一眼可分。
// 单位px与全局 px 化的 QSS、固定像素行高对齐后续若做无障碍字号
// 缩放再统一切 pt。字重400 正文 / 500 可交互激活 /
// 600 标签·标题 / 700 仅展示性大标题。
namespace type {
inline constexpr int kCaption = 12; // 徽标·提示·角色名·错误·副标题·字段标签
inline constexpr int kBody = 13; // 树/列表/菜单/正文(= 全局基准字号)
inline constexpr int kLabel = 13; // 表头·用户名等需加粗的同级标签(配 600
inline constexpr int kTitle = 15; // 面板/停靠区/区段标题·主操作按钮
inline constexpr int kHeading = 18; // 视图/对话框级标题(预留)
inline constexpr int kDisplay = 24; // 登录品牌名(唯一展示性大字)
inline constexpr int kWeightRegular = 400;
inline constexpr int kWeightMedium = 500;
inline constexpr int kWeightSemibold = 600;
inline constexpr int kWeightBold = 700;
} // namespace type
// ── 间距令牌(全项目唯一间距阶)──────────────────────────────────
// 取代散落的 5/7/9/11/13/15/26 等任意值。这是密集专业工具的实际节奏
// (非外加的 8pt 网格):相邻档 2px 粒度,足够紧凑又不糊成一片。
// 用法:布局 setContentsMargins/setSpacing/addSpacing 与 QSS padding/margin
// 一律引用这些档明显的奇数值就近归档13/15→lg, 11→ml, 26→xxl
namespace space {
inline constexpr int kXxs = 2; // 发丝级:下划线偏移、滚动条边距
inline constexpr int kXs = 4; // 紧凑内边距、最小间隙
inline constexpr int kSm = 6; // 行内紧凑(控件竖向 padding
inline constexpr int kMd = 8; // 标准间隔(最常用)
inline constexpr int kMl = 10; // 偏大行距(密集行/验证码行)
inline constexpr int kLg = 12; // 分组间隔、面板内左右边距
inline constexpr int kXl = 16; // 区块内边距
inline constexpr int kXxl = 24; // 区块间距、表单纵向边距
inline constexpr int kXxxl = 32; // 页面级留白(登录窗左右边距)
} // namespace space
// ── 圆角令牌(统一原先 4/5/6/7/8/9 共 6 档为 3 档)────────────────
// 圆形元素(头像等)用 直径/2 单独写字面量,不入档。
namespace radius {
inline constexpr int kSm = 6; // 按钮·输入·菜单项·滚动条·进度条
inline constexpr int kMd = 8; // 卡片·面板·对话框·菜单·分组框
inline constexpr int kPill = 9; // 数量徽标胶囊
} // namespace radius
// ── 语义色令牌(状态/反馈,产品语境:只在承载含义处用,不作装饰)──────────
// 文字值均针对白底面板(#FFFFFF)选深色,对比度 ≥4.5:1正文级与冷调中性
// 调色板调和。danger 沿用既有红,避免引入第二种红。
namespace semantic {
inline constexpr const char* kInfo = "#2D6CB5"; // 信息·进行中(= 品牌蓝)
inline constexpr const char* kSuccess = "#15803D"; // 成功·已完成(深绿)
inline constexpr const char* kWarning = "#B45309"; // 警告·需注意(深琥珀)
inline constexpr const char* kDanger = "#C0392B"; // 危险·错误(沿用既有红)
// 浅色填充(徽标/标签底色,配同族深色文字使用)。
inline constexpr const char* kWarningFill = "#FBEAD2"; // 警告底纹(配 kWarning 文字)
} // namespace semantic
// 应用浅色专业主题Fusion + 调色板 + 全局样式表)。幂等,启动调用一次即可。
void applyTheme(QApplication& app);

View File

@ -14,6 +14,7 @@
#include <QWidget>
#include "Glyphs.hpp"
#include "Theme.hpp"
namespace geopro::app {
@ -116,10 +117,11 @@ QWidget* buildMenuBar(QWidget* parent)
mb->setObjectName(QStringLiteral("appMenuBar"));
// 自带样式(覆盖全局),加大字号/内边距,专业观感。
mb->setStyleSheet(QStringLiteral(
"#appMenuBar { background:#FFFFFF; border-bottom:1px solid #EEF1F5; padding:2px 8px; }"
"#appMenuBar::item { padding:7px 14px; border-radius:6px; font-size:14px; color:#1F2A3D; }"
"#appMenuBar::item:selected { background:#EAF1FB; color:#2D6CB5; }"
"#appMenuBar::item:pressed { background:#DCE6F4; }"));
"#appMenuBar { background:#FFFFFF; border-bottom:1px solid #EEF1F5; padding:2px 8px; }"
"#appMenuBar::item { padding:7px 14px; border-radius:6px; font-size:%1px; color:#1F2A3D; }"
"#appMenuBar::item:selected { background:#EAF1FB; color:#2D6CB5; }"
"#appMenuBar::item:pressed { background:#DCE6F4; }")
.arg(type::kBody));
mb->addMenu(buildViewMenu(mb));
mb->addMenu(buildProjectMenu(mb));
mb->addMenu(buildToolsMenu(mb));
@ -130,19 +132,27 @@ QWidget* buildMenuBar(QWidget* parent)
TopBar::TopBar(QWidget* parent) : QWidget(parent) {
setObjectName(QStringLiteral("appToolBar"));
setFixedHeight(56);
// 字号引用 Theme 排版令牌:工作空间切换器=title(15)、头像/用户名=body·label(13)、
// 角色名=caption(12)。原 11px 角色名上调到 12去掉只差 1px 的糊层级。
setStyleSheet(QStringLiteral(
"#appToolBar { background:#FFFFFF; border-bottom:1px solid #E1E6EE; }"
"#topDivider { color:#E1E6EE; }"
"#wsSwitcher { color:#1F2A3D; border:none; border-radius:8px; padding:8px 12px;"
" font-size:14px; font-weight:600; }"
"#wsSwitcher:hover { background:#EEF3FB; }"
"QToolButton#iconBtn { border:none; border-radius:8px; padding:8px; }"
"QToolButton#iconBtn:hover { background:#EEF3FB; }"
"QToolButton::menu-indicator { image:none; }"
"#avatar { background:#2D6CB5; color:#FFFFFF; border-radius:17px; font-weight:700;"
" font-size:13px; }"
"#userName { color:#1F2A3D; font-size:13px; font-weight:600; }"
"#userRole { color:#8A93A3; font-size:11px; }"));
"#appToolBar { background:#FFFFFF; border-bottom:1px solid #E1E6EE; }"
"#topDivider { color:#E1E6EE; }"
"#wsSwitcher { color:#1F2A3D; border:none; border-radius:8px; padding:8px 12px;"
" font-size:%1px; font-weight:%5; }"
"#wsSwitcher:hover { background:#EEF3FB; }"
"QToolButton#iconBtn { border:none; border-radius:8px; padding:8px; }"
"QToolButton#iconBtn:hover { background:#EEF3FB; }"
"QToolButton::menu-indicator { image:none; }"
"#avatar { background:#2D6CB5; color:#FFFFFF; border-radius:17px; font-weight:%6;"
" font-size:%2px; }"
"#userName { color:#1F2A3D; font-size:%3px; font-weight:%5; }"
"#userRole { color:#8A93A3; font-size:%4px; }")
.arg(type::kTitle)
.arg(type::kBody)
.arg(type::kLabel)
.arg(type::kCaption)
.arg(type::kWeightSemibold)
.arg(type::kWeightBold));
auto* lay = new QHBoxLayout(this);
lay->setContentsMargins(14, 0, 14, 0);
@ -156,7 +166,7 @@ TopBar::TopBar(QWidget* parent) : QWidget(parent) {
wsBtn_->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
wsBtn_->setPopupMode(QToolButton::InstantPopup);
wsBtn_->setCursor(Qt::PointingHandCursor);
wsBtn_->setText(QStringLiteral("(加载中…)"));
wsBtn_->setText(QStringLiteral("正在加载工作空间…"));
wsBtn_->setMenu(new QMenu(wsBtn_));
lay->addWidget(wsBtn_);
@ -172,7 +182,7 @@ TopBar::TopBar(QWidget* parent) : QWidget(parent) {
projBtn_->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
projBtn_->setPopupMode(QToolButton::InstantPopup);
projBtn_->setCursor(Qt::PointingHandCursor);
projBtn_->setText(QStringLiteral("(加载中…)"));
projBtn_->setText(QStringLiteral("正在加载项目…"));
projBtn_->setMenu(new QMenu(projBtn_));
lay->addWidget(projBtn_);

View File

@ -2,19 +2,23 @@
#include <QCheckBox>
#include <QColor>
#include <QEasingCurve>
#include <QFont>
#include <QGraphicsOpacityEffect>
#include <QHBoxLayout>
#include <QLabel>
#include <QLineEdit>
#include <QPainter>
#include <QPen>
#include <QPixmap>
#include <QPropertyAnimation>
#include <QPushButton>
#include <QRandomGenerator>
#include <QVBoxLayout>
#include <QWidget>
#include "AuthService.hpp"
#include "Theme.hpp"
namespace geopro::app {
@ -82,21 +86,27 @@ LoginWindow::LoginWindow(geopro::net::AuthService& auth, QWidget* parent)
// 仅外观:登录窗自带样式(沿用全局主题令牌,保证一脉相承)。
// QLineEdit 在所有状态都显式白底深字 + 边框,避免失焦时取调色板默认色与背景相近不可读。
// 字号引用 Theme 排版令牌:品牌名=display(24)、副标题/字段标签=caption(12)。
setStyleSheet(QStringLiteral(
"QDialog { background: #F4F6FA; }"
"#headerBand {"
" background: qlineargradient(x1:0, y1:0, x2:1, y2:1,"
" stop:0 #2D6CB5, stop:1 #234F87); }"
"#brandTitle { color: #FFFFFF; font-size: 23px; font-weight: 700; }"
"#brandSubtitle { color: rgba(255,255,255,0.82); font-size: 12px; }"
"#fieldLabel { color: #5A6B85; font-size: 12px; font-weight: 600; }"
"QLineEdit {"
" background: #FFFFFF; color: #1F2A3D;"
" border: 1px solid #C7D2E0; border-radius: 8px; padding: 0 12px;"
" selection-background-color: #2D6CB5; selection-color: #FFFFFF; }"
"QLineEdit:focus { border: 1px solid #2D6CB5; }"
"QLineEdit:disabled { background: #F0F2F6; color: #8A93A3; }"
"#captchaImg { border: 1px solid #C7D2E0; border-radius: 8px; background: #EEF2FB; }"));
"QDialog { background: #F4F6FA; }"
"#headerBand {"
" background: qlineargradient(x1:0, y1:0, x2:1, y2:1,"
" stop:0 #2D6CB5, stop:1 #234F87); }"
"#brandTitle { color: #FFFFFF; font-size: %1px; font-weight: %2; }"
"#brandSubtitle { color: rgba(255,255,255,0.82); font-size: %3px; }"
"#fieldLabel { color: #5A6B85; font-size: %4px; font-weight: %5; }"
"QLineEdit {"
" background: #FFFFFF; color: #1F2A3D;"
" border: 1px solid #C7D2E0; border-radius: 8px; padding: 0 12px;"
" selection-background-color: #2D6CB5; selection-color: #FFFFFF; }"
"QLineEdit:focus { border: 1px solid #2D6CB5; }"
"QLineEdit:disabled { background: #F0F2F6; color: #8A93A3; }"
"#captchaImg { border: 1px solid #C7D2E0; border-radius: 8px; background: #EEF2FB; }")
.arg(type::kDisplay)
.arg(type::kWeightBold)
.arg(type::kCaption)
.arg(type::kCaption)
.arg(type::kWeightSemibold));
auto* root = new QVBoxLayout(this);
root->setContentsMargins(0, 0, 0, 0);
@ -122,8 +132,9 @@ LoginWindow::LoginWindow(geopro::net::AuthService& auth, QWidget* parent)
// ── 表单主体:标签在上、输入在下的纵向字段(现代、留白充分)──
auto* body = new QWidget(this);
auto* form = new QVBoxLayout(body);
form->setContentsMargins(32, 24, 32, 26);
form->setSpacing(6);
// 表单边距取间距令牌:左右 xxxl(32)、上下 xxl(24),对称(原底部 26 是手调奇数)。
form->setContentsMargins(space::kXxxl, space::kXxl, space::kXxxl, space::kXxl);
form->setSpacing(space::kSm);
// 统一字段构造小号muted标签 + 40px 高输入框 + 字段间距。
auto addField = [&](const QString& labelText, QLineEdit* edit) {
@ -178,12 +189,14 @@ LoginWindow::LoginWindow(geopro::net::AuthService& auth, QWidget* parent)
// 记住登录:勾选后成功登录将安全存储 token30 天内免登录。默认不勾(更安全)。
rememberChk_ = new QCheckBox(QStringLiteral("记住登录30 天内免登录)"), body);
rememberChk_->setCursor(Qt::PointingHandCursor);
rememberChk_->setStyleSheet(QStringLiteral("color:#5A6B85; font-size:13px;"));
rememberChk_->setStyleSheet(
QStringLiteral("color:#5A6B85; font-size:%1px;").arg(type::kBody));
form->addWidget(rememberChk_);
// 错误提示:固定占位高度,避免出现时整体布局跳动。
errorLabel_ = new QLabel(body);
errorLabel_->setStyleSheet(QStringLiteral("color: #C0392B; font-size: 12px;"));
errorLabel_->setStyleSheet(
QStringLiteral("color: #C0392B; font-size: %1px;").arg(type::kCaption));
errorLabel_->setWordWrap(true);
errorLabel_->setMinimumHeight(18);
form->addWidget(errorLabel_);
@ -195,11 +208,13 @@ LoginWindow::LoginWindow(geopro::net::AuthService& auth, QWidget* parent)
loginBtn_->setMinimumHeight(44);
loginBtn_->setCursor(Qt::PointingHandCursor);
loginBtn_->setStyleSheet(QStringLiteral(
"QPushButton { background: #2D6CB5; color: #FFFFFF; border: none; border-radius: 8px; "
"font-size: 15px; font-weight: 600; }"
"QPushButton:hover { background: #2862A6; }"
"QPushButton:pressed { background: #234F87; }"
"QPushButton:disabled { background: #9FB4CC; }"));
"QPushButton { background: #2D6CB5; color: #FFFFFF; border: none; border-radius: 8px; "
"font-size: %1px; font-weight: %2; }"
"QPushButton:hover { background: #2862A6; }"
"QPushButton:pressed { background: #234F87; }"
"QPushButton:disabled { background: #9FB4CC; }")
.arg(type::kTitle)
.arg(type::kWeightSemibold));
loginBtn_->setDefault(true);
form->addWidget(loginBtn_);
@ -279,6 +294,20 @@ bool LoginWindow::remember() const
void LoginWindow::showError(const QString& msg)
{
errorLabel_->setText(msg);
// 错误淡入:柔化失败时刻(仅透明度 200mserrorLabel_ 已预留固定高度,
// 不引发布局跳动)。复用同一 opacity effect重复报错每次重新淡入。
auto* fx = qobject_cast<QGraphicsOpacityEffect*>(errorLabel_->graphicsEffect());
if (!fx) {
fx = new QGraphicsOpacityEffect(errorLabel_);
errorLabel_->setGraphicsEffect(fx);
}
auto* anim = new QPropertyAnimation(fx, "opacity", errorLabel_);
anim->setDuration(200);
anim->setStartValue(0.0);
anim->setEndValue(1.0);
anim->setEasingCurve(QEasingCurve::OutQuad);
anim->start(QAbstractAnimation::DeleteWhenStopped);
}
} // namespace geopro::app

View File

@ -28,18 +28,25 @@
#include <QCheckBox>
#include <QColor>
#include <QDialog>
#include <QEasingCurve>
#include <QEvent>
#include <QFile>
#include <QFrame>
#include <QGraphicsOpacityEffect>
#include <QLabel>
#include <QListWidget>
#include <QListWidgetItem>
#include <QSettings>
#include <QSignalBlocker>
#include <QPropertyAnimation>
#include <QVariantAnimation>
#include <QStringList>
#include <QTabWidget>
#include <QMainWindow>
#include <QStatusBar>
#include <QStyle>
#include <QSurfaceFormat>
#include <QTimer>
#include <QToolBar>
#include <QTreeWidget>
#include <QTreeWidgetItem>
@ -93,15 +100,90 @@
#include <vector>
#include <QVTKOpenGLStereoWidget.h>
#include <vtkActor.h>
#include <vtkCamera.h>
#include <vtkCameraInterpolator.h>
#include <vtkGenericOpenGLRenderWindow.h>
#include <vtkImagePlaneWidget.h>
#include <vtkLookupTable.h>
#include <vtkProperty.h>
#include <vtkRenderWindowInteractor.h>
#include <vtkRenderer.h>
#include <vtkSmartPointer.h>
namespace {
// 居中浮层定位器:监视 host中央 QVTK尺寸/显示变化,把 overlay 浮层
// (与 host 同父的兄弟控件)始终摆到 host 区域正中。用于中央“空状态”引导层。
// 仅外观,无业务逻辑;无信号槽故不需 Q_OBJECT/moc。
class CenterOverlay : public QObject {
public:
CenterOverlay(QWidget* overlay, QWidget* host)
: QObject(host), overlay_(overlay), host_(host)
{
host_->installEventFilter(this);
}
void reposition()
{
overlay_->adjustSize();
const QSize h = host_->size();
const QSize o = overlay_->size();
overlay_->move(host_->x() + (h.width() - o.width()) / 2,
host_->y() + (h.height() - o.height()) / 2);
overlay_->raise();
}
protected:
bool eventFilter(QObject* obj, QEvent* e) override
{
if (obj == host_ && (e->type() == QEvent::Resize || e->type() == QEvent::Show))
reposition();
return QObject::eventFilter(obj, e);
}
private:
QWidget* overlay_;
QWidget* host_;
};
// 相机补间 + actor 淡入:从 from 位姿平滑过渡到 to 位姿,同时 actors 透明度 0→1。
// vtkCameraInterpolator 两关键帧线性插值(缓动交给 QEasingCurve单条 QVariantAnimation
// 逐帧驱动并 Render结束回调锁定到目标态防插值末值误差/残留半透明)。
// 渐进增强:动效只是过渡,最终一帧永远是正确的目标态,故即使观感不佳也不破坏功能。
void animateReveal(vtkRenderer* renderer, vtkGenericOpenGLRenderWindow* rw,
vtkSmartPointer<vtkCamera> fromCam, vtkSmartPointer<vtkCamera> toCam,
std::vector<vtkSmartPointer<vtkActor>> actors, int durationMs, QObject* owner)
{
auto interp = vtkSmartPointer<vtkCameraInterpolator>::New();
interp->SetInterpolationTypeToLinear();
interp->AddCamera(0.0, fromCam);
interp->AddCamera(1.0, toCam);
auto* anim = new QVariantAnimation(owner);
anim->setDuration(durationMs);
anim->setStartValue(0.0);
anim->setEndValue(1.0);
anim->setEasingCurve(QEasingCurve::OutCubic);
QObject::connect(anim, &QVariantAnimation::valueChanged, owner,
[interp, renderer, rw, actors](const QVariant& v) {
const double t = v.toDouble();
interp->InterpolateCamera(t, renderer->GetActiveCamera());
for (const auto& a : actors)
if (a) a->GetProperty()->SetOpacity(t);
renderer->ResetCameraClippingRange();
rw->Render();
});
QObject::connect(anim, &QVariantAnimation::finished, owner,
[renderer, rw, actors, toCam]() {
renderer->GetActiveCamera()->DeepCopy(toCam);
for (const auto& a : actors)
if (a) a->GetProperty()->SetOpacity(1.0);
renderer->ResetCameraClippingRange();
rw->Render();
});
anim->start(QAbstractAnimation::DeleteWhenStopped);
}
// 读取 RSA 公钥 PEM 全文(登录时密码加密用)。读不到返回空串,登录将报错。
std::string readPem(const std::string& path)
{
@ -247,11 +329,16 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
"border-radius:8px;} QCheckBox{padding:2px 1px;color:#1F2A3D;}"
"QCheckBox:disabled{color:#9AA6B6;}"));
auto* layerLayout = new QVBoxLayout(layerPanel);
layerLayout->setContentsMargins(13, 10, 15, 11);
layerLayout->setSpacing(6);
// 浮层内边距取间距令牌:左右 lg(12)、上下 ml(10),对称(原 13/10/15/11 是手调奇数)。
layerLayout->setContentsMargins(geopro::app::space::kLg, geopro::app::space::kMl,
geopro::app::space::kLg, geopro::app::space::kMl);
layerLayout->setSpacing(geopro::app::space::kSm);
auto* layerTitle = new QLabel(QStringLiteral("视图详情"));
layerTitle->setStyleSheet(QStringLiteral(
"font-weight:600;color:#2D6CB5;border:none;background:transparent;padding-bottom:3px;"));
"font-weight:%1;color:#2D6CB5;border:none;background:transparent;"
"padding-bottom:3px;font-size:%2px;")
.arg(geopro::app::type::kWeightSemibold)
.arg(geopro::app::type::kTitle));
auto* chkCurtain = new QCheckBox(QStringLiteral("帘面(断面墙)"));
chkCurtain->setChecked(true);
auto* chkVoxel = new QCheckBox(QStringLiteral("体素dd_voxel"));
@ -278,6 +365,56 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
layerLayout->addWidget(chkTerrain);
layerPanel->setVisible(false); // 默认二维,不显示图层浮层
// ── 中央“空状态”引导浮层:未接入真实 sections 时,引导首次使用者从左侧入手。──
// 透明背景 + 鼠标穿透(不挡 QVTK 交互CenterOverlay 随视口尺寸保持居中;
// 接入真实中央数据后改成依 sections 是否为空调 setVisible 即可。
auto* emptyState = new QFrame(centerWidget);
emptyState->setObjectName(QStringLiteral("centralEmpty"));
emptyState->setAttribute(Qt::WA_TransparentForMouseEvents);
emptyState->setStyleSheet(QStringLiteral(
"#centralEmpty { background: transparent; }"
"#centralEmpty QLabel { background: transparent; }"));
auto* esLay = new QVBoxLayout(emptyState);
esLay->setContentsMargins(geopro::app::space::kXl, geopro::app::space::kXl,
geopro::app::space::kXl, geopro::app::space::kXl);
esLay->setSpacing(geopro::app::space::kMd);
esLay->setAlignment(Qt::AlignCenter);
auto* esIcon = new QLabel(emptyState);
esIcon->setPixmap(
geopro::app::makeGlyph(geopro::app::Glyph::Dataset, QColor("#C2CCDA"), 56).pixmap(56, 56));
esIcon->setAlignment(Qt::AlignCenter);
auto* esTitle = new QLabel(QStringLiteral("选择左侧数据集开始分析"), emptyState);
esTitle->setAlignment(Qt::AlignCenter);
esTitle->setStyleSheet(QStringLiteral("color:#5A6B85; font-size:%1px; font-weight:%2;")
.arg(geopro::app::type::kHeading)
.arg(geopro::app::type::kWeightSemibold));
auto* esHint = new QLabel(QStringLiteral("单击左侧采集批次,查看反演剖面与异常点\n"
"切到「三维视图」可叠加帘面、体素与地形图层"),
emptyState);
esHint->setAlignment(Qt::AlignCenter);
esHint->setStyleSheet(
QStringLiteral("color:#8A93A3; font-size:%1px;").arg(geopro::app::type::kBody));
esLay->addWidget(esIcon);
esLay->addWidget(esTitle);
esLay->addWidget(esHint);
auto* emptyCentering = new CenterOverlay(emptyState, vtkWidget);
emptyCentering->reposition();
// 引导层淡入350ms仅透明度OutCubic首屏空态出现的克制过渡不阻塞任务。
auto* esFx = new QGraphicsOpacityEffect(emptyState);
emptyState->setGraphicsEffect(esFx);
auto* esAnim = new QPropertyAnimation(esFx, "opacity", emptyState);
esAnim->setDuration(350);
esAnim->setStartValue(0.0);
esAnim->setEndValue(1.0);
esAnim->setEasingCurve(QEasingCurve::OutCubic);
esAnim->start(QAbstractAnimation::DeleteWhenStopped);
auto* vtkDock = new ads::CDockWidget(QStringLiteral("二维地图/三维视图"));
vtkDock->setWidget(centerWidget);
auto* centerDockArea = dockManager->addDockWidget(ads::CenterDockWidgetArea, vtkDock);
@ -376,6 +513,15 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
{{geopro::app::Glyph::Filter, QStringLiteral("筛选")},
{geopro::app::Glyph::Plus, QStringLiteral("添加异常")}});
auto* anomalyBadge = anomalyPanel.badges.value(0); // 异常列表 Tab 的数量徽标
// colorize(C):异常计数用语义 warning“需注意”变体区别于普通中性计数徽标
// 提示“这些异常点待复查”。改 objectName 后重新 polish 以应用 #panelBadgeWarn 样式。
// 注:徽标的填充/显隐在 loadDataset 内(当前被 park故此色与徽标本身同属休眠态
// 接 dd 详情渲染那轮一并可见。
if (anomalyBadge) {
anomalyBadge->setObjectName(QStringLiteral("panelBadgeWarn"));
anomalyBadge->style()->unpolish(anomalyBadge);
anomalyBadge->style()->polish(anomalyBadge);
}
auto* rightDock = new ads::CDockWidget(QStringLiteral("异常列表/对象属性"));
rightDock->setWidget(anomalyPanel.container);
@ -393,12 +539,16 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
// 固定全部面板(对齐原型):移除 关闭/浮动/拖动/钉住 等子窗口操作,仅保留分隔条调整边界。
// 同时隐藏 ADS 自带标题栏——表头已由各面板内的自绘 PanelHeader 承担,避免“双标题”。
// 注AlwaysShowTabs=true 时 ADS 不再自动改写标题栏可见性,手动隐藏可稳定保持。
for (ads::CDockWidget* d : {vtkDock, detailDock, leftDock, datasetDock, rightDock, propDock}) {
d->setFeatures(ads::CDockWidget::NoDockWidgetFeatures);
if (auto* area = d->dockAreaWidget())
if (auto* bar = area->titleBar()) bar->setVisible(false);
}
// 抽成 lambdaADS restoreState() 恢复布局时会重建停靠区并重新显示标题栏,
// 故须在恢复布局之后再调用一次,确保任何已保存布局下标题栏都稳定隐藏。
const auto hideDockTitleBars = [&]() {
for (ads::CDockWidget* d : {vtkDock, detailDock, leftDock, datasetDock, rightDock, propDock}) {
d->setFeatures(ads::CDockWidget::NoDockWidgetFeatures);
if (auto* area = d->dockAreaWidget())
if (auto* bar = area->titleBar()) bar->setVisible(false);
}
};
hideDockTitleBars();
// 中央编排已解耦到 CentralScene::rebuildCentralScene数据驱动。本轮空 sections → 空背景占位。
// 下一轮:用真实 DS 数据构建 sections 调同一 helper 即复活。
@ -416,16 +566,29 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
auto showElectrodes = std::make_shared<bool>(true); // 默认显示电极 ▼
auto showContour = std::make_shared<bool>(true); // 默认显示等值线
auto hiddenAnoms = std::make_shared<std::set<int>>(); // 异常列表中被取消勾选(隐藏)的异常下标
auto prevDsId = std::make_shared<QString>(); // 上次渲染的 DS id判定“切换数据集”以触发揭示过渡
// 按当前选中 DS + 详情模式重建下方数据详情(平躺俯视正交,纵向夸张填面板)。
// 勾选「显示异常/电极/等值线」控制对应叠加(同纵向夸张对齐)。
auto rebuildDetail = [&repo, detailRendererPtr, detailRenderWindowPtr, currentDsId, detailMode,
showAnomalies, showElectrodes, showContour, hiddenAnoms]() {
// overdrive(A):仅“切换数据集”这一加载时刻播放相机补间 + actor 淡入揭示;模式/叠加层开关
// 属同一数据集内微调,直接落定不放动画(特殊时刻才特殊,避免每次交互都动的疲劳)。
auto rebuildDetail = [&repo, detailRendererPtr, detailRenderWindowPtr, detailWidget, currentDsId,
prevDsId, detailMode, showAnomalies, showElectrodes, showContour,
hiddenAnoms]() {
const bool dsChanged = (*currentDsId != *prevDsId);
const bool animate = dsChanged && !prevDsId->isEmpty() && !currentDsId->isEmpty();
// 过渡起点:清场景前先快照当前相机位姿。
auto fromCam = vtkSmartPointer<vtkCamera>::New();
fromCam->DeepCopy(detailRendererPtr->GetActiveCamera());
detailRendererPtr->RemoveAllViewProps();
if (currentDsId->isEmpty()) { // 未选数据集:清空即可
*prevDsId = *currentDsId;
detailRenderWindowPtr->Render();
return;
}
std::vector<vtkSmartPointer<vtkActor>> added; // 本次加入的 actor供淡入
const std::string id = currentDsId->toStdString();
if (*detailMode == DetailMode::Section18) {
// 网格数据:#18 banded 等值面(+「显示等值线」时叠黑色等值线),纵向夸张 1.5x(沿 y
@ -435,10 +598,12 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
if (actors.bands) {
actors.bands->SetScale(1.0, kVerticalExaggeration, 1.0);
detailRendererPtr->AddViewProp(actors.bands);
added.push_back(actors.bands);
}
if (actors.edges && *showContour) {
actors.edges->SetScale(1.0, kVerticalExaggeration, 1.0);
detailRendererPtr->AddViewProp(actors.edges);
added.push_back(actors.edges);
}
// 顶部电极标记 ▼(仅网格数据;同纵向夸张对齐)。
if (*showElectrodes) {
@ -446,6 +611,7 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
if (elec) {
elec->SetScale(1.0, kVerticalExaggeration, 1.0);
detailRendererPtr->AddViewProp(elec);
added.push_back(elec);
}
}
} else {
@ -456,6 +622,7 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
if (a) {
a->SetScale(1.0, kVerticalExaggeration, 1.0);
detailRendererPtr->AddViewProp(a);
added.push_back(a);
}
}
// 异常叠加(与剖面同坐标系/同纵向夸张)。逐异常构建以按列表显隐(下标=原 vector 序)过滤。
@ -466,12 +633,26 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
for (auto& act : geopro::render::buildAnomalies({anomalies[i]})) {
act->SetScale(1.0, kVerticalExaggeration, 1.0);
detailRendererPtr->AddViewProp(act);
added.push_back(act);
}
}
}
geopro::render::applyTop2D(detailRendererPtr);
detailRendererPtr->ResetCamera();
detailRenderWindowPtr->Render();
*prevDsId = *currentDsId;
if (animate) {
// 目标位姿快照 → 相机回退到旧位姿 + actors 透明 → 补间到目标并淡入。
auto toCam = vtkSmartPointer<vtkCamera>::New();
toCam->DeepCopy(detailRendererPtr->GetActiveCamera());
for (const auto& a : added) a->GetProperty()->SetOpacity(0.0);
detailRendererPtr->GetActiveCamera()->DeepCopy(fromCam);
detailRendererPtr->ResetCameraClippingRange();
animateReveal(detailRendererPtr, detailRenderWindowPtr, fromCam, toCam, added, 450,
detailWidget);
} else {
detailRenderWindowPtr->Render();
}
};
// 加载某数据集到「数据详情 + 异常列表 + 属性」(数据列表单击与启动默认共用)。
@ -503,9 +684,14 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
.arg(name).arg(g.nx()).arg(g.ny()).arg(g.vmin).arg(g.vmax)
.arg(anomalies.size()));
};
(void)loadDataset; // 暂未触发:保留待下一轮真实 DS 详情渲染复用
// 暂未触发:保留待下一轮真实 DS 详情渲染复用。
// TODO(overdrive-A 依赖):把下面数据集单击处理改调 loadDataset(dsId, name) 接通真实详情
// 渲染后rebuildDetail 里已就绪的“相机补间 + actor 淡入”揭示动画会在切换数据集时自动激活
// (见 rebuildDetail 的 animate 分支与 animateReveal。在此之前该动画为休眠态、不可见。
(void)loadDataset;
// ── 单击左下数据列表的采集批次(DS) → 占位(真实剖面/反演渲染下一阶段接 dd 接口)──
// 接 dd 那轮:把本处占位改为 loadDataset(id, name) 即接通详情渲染,并自动激活 overdrive-A 揭示动画。
QObject::connect(datasetList, &QListWidget::itemClicked, datasetList,
[propLabel, detailRendererPtr, detailRenderWindowPtr, &nav](QListWidgetItem* item) {
if (item->data(geopro::app::kDsLoadMoreRole).toBool()) {
@ -713,8 +899,12 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
if (stage == QStringLiteral("structure") ||
stage == QStringLiteral("projects"))
objectTree->showMessage(QStringLiteral("加载失败:%1").arg(msg));
window.statusBar()->showMessage(
QStringLiteral("加载失败(%1%2").arg(stage, msg), 8000);
// 状态栏错误反馈:临时染 danger 红8s 后随消息超时还原全局中性色。
auto* sb = window.statusBar();
sb->setStyleSheet(QStringLiteral("QStatusBar{color:%1;}")
.arg(QString::fromUtf8(geopro::app::semantic::kDanger)));
sb->showMessage(QStringLiteral("加载失败(%1%2").arg(stage, msg), 8000);
QTimer::singleShot(8000, sb, [sb]() { sb->setStyleSheet(QString()); });
});
QObject::connect(&nav, &geopro::controller::WorkbenchNavController::busyChanged, &window,
[](bool busy) {
@ -738,7 +928,11 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
const QByteArray geo = settings.value(QStringLiteral("ui/geometry")).toByteArray();
if (!geo.isEmpty()) window.restoreGeometry(geo);
const QByteArray dockState = settings.value(QStringLiteral("ui/dockState")).toByteArray();
if (!dockState.isEmpty()) dockManager->restoreState(dockState);
if (!dockState.isEmpty()) {
dockManager->restoreState(dockState);
// restoreState 重建停靠区会重新显示 ADS 标题栏,再隐藏一次保持“无双标题”。
hideDockTitleBars();
}
}
// 退出时保存当前布局与几何aboutToQuit 早于 window 析构dockManager/window 仍存活)。
QObject::connect(qApp, &QCoreApplication::aboutToQuit, dockManager, [dockManager, &window]() {

View File

@ -50,7 +50,7 @@ ObjectTreePanel::ObjectTreePanel(QWidget* parent) : QWidget(parent) {
}
lay->addWidget(tree_, 1);
hint_ = new QLabel(QStringLiteral("(加载中…)"), this);
hint_ = new QLabel(QStringLiteral("正在加载对象…"), this);
hint_->setAlignment(Qt::AlignCenter);
hint_->setStyleSheet(QStringLiteral("color:#9AA6B6; padding:16px;"));
hint_->setVisible(false);