Compare commits
4 Commits
045bb3cc1f
...
c4d76f57b6
| Author | SHA1 | Date |
|---|---|---|
|
|
c4d76f57b6 | |
|
|
6df2c4832c | |
|
|
1a9fb72cf0 | |
|
|
caf6f9ebd0 |
|
|
@ -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.
|
||||
|
|
@ -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 bindings。静态链接(MIT 许可,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)
|
||||
|
|
|
|||
52
README.md
52
README.md
|
|
@ -8,7 +8,7 @@
|
|||
|
||||
## 技术栈
|
||||
|
||||
Qt 6.8 LTS(QtWidgets)+ VTK 9.3+ · CMake + vcpkg(全量,含 Qt)· MSVC 2022 / C++17 · ADS 停靠 · GDAL/PROJ · OpenSSL · QtKeychain。
|
||||
Qt 6.11(QtWidgets)+ VTK 9.6 · CMake + Ninja · 官方 MSVC 预编译 Qt + vcpkg(仅非 Qt 依赖)· MSVC(VS 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 通过后展开完整实现计划。
|
||||
|
|
|
|||
|
|
@ -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%
|
||||
|
|
@ -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。每步构建通过后由用户运行+截图确认再进下一步。
|
||||
|
|
@ -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()
|
||||
|
|
@ -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);
|
||||
|
||||
// dock1:Fluent 控件样例(看观感: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);
|
||||
|
||||
// dock2:QVTK 视口(蓝色锥体),验证 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();
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -13,6 +13,73 @@ class QApplication;
|
|||
|
||||
namespace geopro::app {
|
||||
|
||||
// ── 排版令牌(全项目唯一字号阶 + 字重角色)──────────────────────────
|
||||
// 各处 QSS 的 font-size / font-weight 一律引用这些值,不再散落硬编码 px。
|
||||
// 阶比 ~1.18(body→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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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_);
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
|||
// 记住登录:勾选后成功登录将安全存储 token,30 天内免登录。默认不勾(更安全)。
|
||||
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);
|
||||
|
||||
// 错误淡入:柔化失败时刻(仅透明度 200ms;errorLabel_ 已预留固定高度,
|
||||
// 不引发布局跳动)。复用同一 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
|
||||
|
|
|
|||
226
src/app/main.cpp
226
src/app/main.cpp
|
|
@ -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);
|
||||
}
|
||||
// 抽成 lambda:ADS 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]() {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Reference in New Issue