feat: initial public release TZshot

This commit is contained in:
easyz committed 2026-03-22 18:04:20 +08:00
commit 6418a9fa78
131 files changed
+15868

No files matched your search

+25
View File
@@ -0,0 +1,25 @@
root = true
[*.{cpp,h,c,qml,ts,md,txt,cmake}]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
indent_style = space
indent_size = 4
[CMakeLists.txt]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
indent_style = space
indent_size = 4
[*.yml]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
indent_style = space
indent_size = 2
+11
View File
@@ -0,0 +1,11 @@
* text=auto eol=lf
*.bat text eol=crlf
*.ps1 text eol=crlf
*.png binary
*.jpg binary
*.jpeg binary
*.gif binary
*.ico binary
*.svg text eol=lf
+106
View File
@@ -0,0 +1,106 @@
# This file is used to ignore files which are generated
# ----------------------------------------------------------------------------
*~
*.autosave
*.a
*.core
*.moc
*.o
*.obj
*.orig
*.rej
*.so
*.so.*
*_pch.h.cpp
*_resource.rc
*.qm
.#*
*.*#
core
!core/
tags
.DS_Store
.directory
*.debug
Makefile*
*.prl
*.app
moc_*.cpp
ui_*.h
qrc_*.cpp
Thumbs.db
*.res
*.rc
/.qmake.cache
/.qmake.stash
# qtcreator generated files
*.pro.user*
CMakeLists.txt.user
CMakeLists.txt.user.*
cmake-build-*/
# xemacs temporary files
*.flc
# Vim temporary files
.*.swp
# Visual Studio generated files
*.ib_pdb_index
*.idb
*.ilk
*.pdb
*.sln
*.suo
*.vcproj
*vcproj.*.*.user
*.ncb
*.sdf
*.opensdf
*.vcxproj
*vcxproj.*
# MinGW generated files
*.Debug
*.Release
# Python byte code
*.pyc
# Binaries
# --------
*.dll
*.exe
# Visual Studio / CMake build artifacts
# ------------------------------------
.vs/
ipch/
Debug/
Release/
x64/
x86/
[Bb]uild/
[Bb]uild*/
CMakeFiles/
CMakeCache.txt
cmake_install.cmake
compile_commands.json
*.dir/
*.tlog
*.lastbuildstate
*.vcxproj.user
*.cache
*.ilk
*.pdb
*.exp
*.lib
*.obj
*.idb
# Third-party packages (do not commit vendor/downloaded dependencies)
# ------------------------------------------------------------------
thirdpart/**
!thirdpart/.gitkeep
+425
View File
@@ -0,0 +1,425 @@
cmake_minimum_required(VERSION 3.16)
project(TZshot VERSION 1.0 LANGUAGES CXX C)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTORCC ON)
set(CMAKE_AUTOUIC ON)
include(ExternalProject)
find_package(Qt6 REQUIRED COMPONENTS
Core
Gui
Widgets
Quick
Qml
Network
Concurrent
LinguistTools
QuickDialogs2
)
qt_standard_project_setup()
# ── Third-party build options ─────────────────────────────────
option(BUILD_THIRDPART_OCR "Auto build OCR libs" ON)
option(OCR_FETCH_FROM_GITHUB "Fetch Leptonica/Tesseract from GitHub before building" ON)
# ── 翻译文件 ──────────────────────────────────────────────
set(TS_FILES
i18n/app_zh_CN.ts
i18n/app_en.ts
)
# ── 平台无关源文件 ────────────────────────────────────────
set(SOURCES
src/main.cpp
src/tray_icon_helper.cpp
src/tray_icon_helper.h
src/language_manager.cpp
src/language_manager.h
src/magnifier_provider.cpp
src/magnifier_provider.h
src/sticky_image_store.cpp
src/sticky_image_store.h
src/sticky_image_provider_proxy.cpp
src/sticky_image_provider_proxy.h
src/singleton.h
# Model
src/model/desktop_snapshot.cpp
src/model/desktop_snapshot.h
src/model/app_settings.cpp
src/model/app_settings.h
# ViewModel
src/viewmodel/screenshot_view_model.cpp
src/viewmodel/screenshot_view_model.h
src/viewmodel/sticky_view_model.cpp
src/viewmodel/sticky_view_model.h
src/viewmodel/ai_view_model.cpp
src/viewmodel/ai_view_model.h
src/viewmodel/storage_view_model.cpp
src/viewmodel/storage_view_model.h
src/viewmodel/gif_record_view_model.cpp
src/viewmodel/gif_record_view_model.h
src/viewmodel/ocr_view_model.cpp
src/viewmodel/ocr_view_model.h
# GIF Encoder
src/gif_encoder/gif.h
src/gif_encoder/gif_encoder.cpp
src/gif_encoder/gif_encoder.h
src/gif_encoder/gif_frame_grabber.cpp
src/gif_encoder/gif_frame_grabber.h
# OCR
src/ocr/ocr_engine.cpp
src/ocr/ocr_engine.h
# PaintBoard
src/paint_board/paint_board.cpp
src/paint_board/paint_board.h
src/paint_board/shape/shape.cpp
src/paint_board/shape/shape.h
src/paint_board/shape/shape_type.h
src/paint_board/shape/shape_factory.cpp
src/paint_board/shape/shape_factory.h
src/paint_board/shape/two_point_shape.cpp
src/paint_board/shape/two_point_shape.h
src/paint_board/shape/arrow_shape.cpp
src/paint_board/shape/arrow_shape.h
src/paint_board/shape/ellipse_shape.cpp
src/paint_board/shape/ellipse_shape.h
src/paint_board/shape/mosaic_shape.cpp
src/paint_board/shape/mosaic_shape.h
src/paint_board/shape/pen_shape.cpp
src/paint_board/shape/pen_shape.h
src/paint_board/shape/rect_shape.cpp
src/paint_board/shape/rect_shape.h
src/paint_board/shape/text_shape.cpp
src/paint_board/shape/text_shape.h
src/paint_board/shape/number_shape.cpp
src/paint_board/shape/number_shape.h
# AI
src/ai_call/ai_call_base.cpp
src/ai_call/ai_call_base.h
src/ai_call/ai_type.h
src/ai_call/qwen_ai_call.cpp
src/ai_call/qwen_ai_call.h
src/ai_call/seedream_ai_call.cpp
src/ai_call/seedream_ai_call.h
# Shortcut — 跨平台公共部分
src/shortcut_key/globalshortcut.cpp
src/shortcut_key/globalshortcut.h
# Resources
qml.qrc
i18n/i18n.qrc
)
# ── 平台专属源文件 ────────────────────────────────────────
if(UNIX)
list(APPEND SOURCES
src/shortcut_key/linux_grab.c
src/shortcut_key/linux_grab.h
)
endif()
if(WIN32)
list(APPEND SOURCES
src/shortcut_key/win_grab.c
src/shortcut_key/win_grab.h
)
endif()
qt_add_executable(TZshot
${SOURCES}
)
# ── include 路径 ──────────────────────────────────────────
target_include_directories(TZshot PRIVATE
src
src/ai_call
src/gif_encoder
src/ocr
src/paint_board
src/paint_board/shape
src/model
src/viewmodel
src/shortcut_key
)
# ── 编译定义 ──────────────────────────────────────────────
target_compile_definitions(TZshot PRIVATE
QT_DEPRECATED_WARNINGS
)
target_compile_definitions(TZshot PRIVATE
$<$<CONFIG:Debug>:STICKY_MEM_DEBUG>
)
# ── Qt 模块链接 ───────────────────────────────────────────
target_link_libraries(TZshot PRIVATE
Qt6::Core
Qt6::Gui
Qt6::Widgets
Qt6::Quick
Qt6::Qml
Qt6::Network
Qt6::Concurrent
Qt6::QuickDialogs2
)
# ── 平台系统库 ────────────────────────────────────────────
if(UNIX)
target_link_libraries(TZshot PRIVATE
X11
xcb
pthread
)
set(OCR_ENABLED FALSE)
set(OCR_PREFIX "$ENV{OCR_PREFIX}")
if(OCR_PREFIX STREQUAL "")
set(OCR_PREFIX "${CMAKE_CURRENT_SOURCE_DIR}/thirdpart/ocr")
endif()
set(OCR_LIB_DIR "")
if(EXISTS "${OCR_PREFIX}/lib/libtesseract.so" OR EXISTS "${OCR_PREFIX}/lib/libtesseract.a")
set(OCR_LIB_DIR "${OCR_PREFIX}/lib")
elseif(EXISTS "${OCR_PREFIX}/lib64/libtesseract.so" OR EXISTS "${OCR_PREFIX}/lib64/libtesseract.a")
set(OCR_LIB_DIR "${OCR_PREFIX}/lib64")
endif()
if(NOT OCR_LIB_DIR STREQUAL "")
target_include_directories(TZshot PRIVATE
"${OCR_PREFIX}/include"
"${OCR_PREFIX}/include/leptonica"
)
target_link_directories(TZshot PRIVATE "${OCR_LIB_DIR}")
target_link_libraries(TZshot PRIVATE tesseract leptonica)
target_compile_definitions(TZshot PRIVATE
ENABLE_OCR
OCR_TESSDATA_DIR=\"${OCR_PREFIX}/share/tessdata\"
)
set(OCR_ENABLED TRUE)
message(STATUS "OCR enabled via thirdpart: ${OCR_PREFIX}")
endif()
if(NOT OCR_ENABLED)
find_package(PkgConfig QUIET)
if(PkgConfig_FOUND)
pkg_check_modules(TESSERACT QUIET IMPORTED_TARGET tesseract)
pkg_check_modules(LEPT QUIET IMPORTED_TARGET lept)
if(TESSERACT_FOUND AND LEPT_FOUND)
target_link_libraries(TZshot PRIVATE PkgConfig::TESSERACT PkgConfig::LEPT)
target_compile_definitions(TZshot PRIVATE ENABLE_OCR)
message(STATUS "OCR enabled via pkg-config: tesseract + lept")
else()
message(STATUS "OCR disabled: thirdpart OCR not found and pkg-config tesseract/lept unavailable")
endif()
else()
message(STATUS "OCR disabled: PkgConfig not found")
endif()
endif()
endif()
if(WIN32)
target_link_libraries(TZshot PRIVATE
user32 # RegisterHotKey / UnregisterHotKey / EnumWindows
gdi32
)
set(OCR_ENABLED FALSE)
if(BUILD_THIRDPART_OCR AND OCR_FETCH_FROM_GITHUB)
set(OCR_THIRDPART_DIR "${CMAKE_CURRENT_SOURCE_DIR}/thirdpart")
set(OCR_INSTALL_DIR "${OCR_THIRDPART_DIR}/ocr-install")
set(OCR_TESSDATA_DIR "${OCR_INSTALL_DIR}/share/tessdata")
set(OCR_EP_REV "v2")
set(OCR_ZLIB_VERSION "v1.3.1")
set(OCR_LIBPNG_VERSION "v1.6.43")
set(OCR_LIBJPEG_TURBO_VERSION "3.0.3")
set(OCR_LIBTIFF_VERSION "v4.7.0")
set(OCR_LEPTONICA_VERSION "1.87.0")
set(OCR_TESSERACT_VERSION "5.5.2")
# Imported target include dirs are validated at generate time.
# Create expected install dirs up front; actual contents are produced by ExternalProject.
file(MAKE_DIRECTORY
"${OCR_INSTALL_DIR}/include"
"${OCR_INSTALL_DIR}/lib"
"${OCR_TESSDATA_DIR}"
)
ExternalProject_Add(zlib_ep
PREFIX "${OCR_THIRDPART_DIR}/zlib-${OCR_EP_REV}"
GIT_REPOSITORY "https://github.com/madler/zlib.git"
GIT_TAG "${OCR_ZLIB_VERSION}"
UPDATE_DISCONNECTED 1
CMAKE_ARGS
-DCMAKE_INSTALL_PREFIX=${OCR_INSTALL_DIR}
-DBUILD_SHARED_LIBS=ON
-DCMAKE_POSITION_INDEPENDENT_CODE=ON
)
ExternalProject_Add(libpng_ep
PREFIX "${OCR_THIRDPART_DIR}/libpng-${OCR_EP_REV}"
GIT_REPOSITORY "https://github.com/pnggroup/libpng.git"
GIT_TAG "${OCR_LIBPNG_VERSION}"
UPDATE_DISCONNECTED 1
DEPENDS zlib_ep
CMAKE_ARGS
-DCMAKE_INSTALL_PREFIX=${OCR_INSTALL_DIR}
-DCMAKE_PREFIX_PATH=${OCR_INSTALL_DIR}
-DZLIB_ROOT=${OCR_INSTALL_DIR}
-DPNG_SHARED=ON
-DPNG_TESTS=OFF
-DPNG_EXECUTABLES=OFF
-DPNG_FRAMEWORK=OFF
)
ExternalProject_Add(libjpeg_ep
PREFIX "${OCR_THIRDPART_DIR}/libjpeg-${OCR_EP_REV}"
GIT_REPOSITORY "https://github.com/libjpeg-turbo/libjpeg-turbo.git"
GIT_TAG "${OCR_LIBJPEG_TURBO_VERSION}"
UPDATE_DISCONNECTED 1
CMAKE_ARGS
-DCMAKE_INSTALL_PREFIX=${OCR_INSTALL_DIR}
-DBUILD_SHARED_LIBS=ON
-DENABLE_SHARED=ON
-DENABLE_STATIC=OFF
-DWITH_TURBOJPEG=ON
-DWITH_JPEG8=ON
)
ExternalProject_Add(libtiff_ep
PREFIX "${OCR_THIRDPART_DIR}/libtiff-${OCR_EP_REV}"
GIT_REPOSITORY "https://github.com/libsdl-org/libtiff.git"
GIT_TAG "${OCR_LIBTIFF_VERSION}"
UPDATE_DISCONNECTED 1
DEPENDS zlib_ep
CMAKE_ARGS
-DCMAKE_INSTALL_PREFIX=${OCR_INSTALL_DIR}
-DCMAKE_PREFIX_PATH=${OCR_INSTALL_DIR}
-DBUILD_SHARED_LIBS=ON
-Dzlib=ON
-Djpeg=OFF
-Dwebp=OFF
-Dzstd=OFF
-Dlzma=OFF
-Djbig=OFF
-Dlerc=OFF
-Dtiff-tools=OFF
-Dtiff-tests=OFF
-Dtiff-contrib=OFF
-Dtiff-docs=OFF
)
ExternalProject_Add(leptonica_ep
PREFIX "${OCR_THIRDPART_DIR}/leptonica-${OCR_EP_REV}"
GIT_REPOSITORY "https://github.com/DanBloomberg/leptonica.git"
GIT_TAG "${OCR_LEPTONICA_VERSION}"
UPDATE_DISCONNECTED 1
DEPENDS libpng_ep libjpeg_ep libtiff_ep
CMAKE_ARGS
-DCMAKE_INSTALL_PREFIX=${OCR_INSTALL_DIR}
-DCMAKE_PREFIX_PATH=${OCR_INSTALL_DIR}
-DBUILD_SHARED_LIBS=ON
-DCMAKE_POSITION_INDEPENDENT_CODE=ON
-DSW_BUILD=OFF
-DSTRICT_CONF=ON
-DTIFF_DIR=${OCR_INSTALL_DIR}/lib/cmake/tiff
-DTIFF_INCLUDE_DIR=${OCR_INSTALL_DIR}/include
-DTIFF_LIBRARY=${OCR_INSTALL_DIR}/lib/tiffd.lib
-DENABLE_ZLIB=ON
-DENABLE_PNG=ON
-DENABLE_GIF=OFF
-DENABLE_JPEG=ON
-DENABLE_TIFF=ON
-DENABLE_WEBP=OFF
-DENABLE_OPENJPEG=OFF
)
ExternalProject_Add(tesseract_ep
PREFIX "${OCR_THIRDPART_DIR}/tesseract-${OCR_EP_REV}"
GIT_REPOSITORY "https://github.com/tesseract-ocr/tesseract.git"
GIT_TAG "${OCR_TESSERACT_VERSION}"
UPDATE_DISCONNECTED 1
DEPENDS leptonica_ep
CMAKE_ARGS
-DCMAKE_INSTALL_PREFIX=${OCR_INSTALL_DIR}
-DCMAKE_PREFIX_PATH=${OCR_INSTALL_DIR}
-DBUILD_SHARED_LIBS=ON
-DSW_BUILD=OFF
-DBUILD_TRAINING_TOOLS=OFF
-DBUILD_TESTS=OFF
-DDISABLE_ARCHIVE=ON
-DDISABLE_CURL=ON
-DOPENMP_BUILD=OFF
)
add_dependencies(TZshot tesseract_ep)
target_include_directories(TZshot PRIVATE "${OCR_INSTALL_DIR}/include")
target_link_directories(TZshot PRIVATE "${OCR_INSTALL_DIR}/lib")
target_link_libraries(TZshot PRIVATE
"tesseract55$<$<CONFIG:Debug>:d>"
"leptonica-${OCR_LEPTONICA_VERSION}$<$<CONFIG:Debug>:d>"
Ws2_32
)
target_compile_definitions(TZshot PRIVATE
ENABLE_OCR
OCR_TESSDATA_DIR=\"${OCR_TESSDATA_DIR}\"
)
# Ensure runtime OCR DLLs are available beside TZshot.exe.
add_custom_command(TARGET TZshot POST_BUILD
COMMAND "${CMAKE_COMMAND}" -E copy_directory
"${OCR_INSTALL_DIR}/bin"
"$<TARGET_FILE_DIR:TZshot>"
VERBATIM
)
set(OCR_ENABLED TRUE)
message(STATUS "OCR enabled via GitHub fetch/build (Leptonica ${OCR_LEPTONICA_VERSION}, Tesseract ${OCR_TESSERACT_VERSION})")
endif()
if(NOT OCR_ENABLED)
message(STATUS "OCR disabled on Windows: BUILD_THIRDPART_OCR/OCR_FETCH_FROM_GITHUB is OFF")
endif()
# Windows 子系统:Release 用 /SUBSYSTEM:WINDOWS 隐藏控制台窗口
set_target_properties(TZshot PROPERTIES
WIN32_EXECUTABLE $<IF:$<CONFIG:Debug>,FALSE,TRUE>
)
endif()
# ── QML 导入路径 ──────────────────────────────────────────
set_target_properties(TZshot PROPERTIES
QT_QML_IMPORT_PATH "${CMAKE_CURRENT_SOURCE_DIR}/qml"
)
# ── 翻译 ──────────────────────────────────────────────────
qt_add_translations(TZshot
TS_FILES ${TS_FILES}
)
# ── 安装 ──────────────────────────────────────────────────
if(UNIX AND NOT ANDROID)
install(TARGETS TZshot
DESTINATION /opt/TZshot/bin
)
endif()
if(WIN32)
install(TARGETS TZshot
DESTINATION bin
)
endif()
+18
View File
@@ -0,0 +1,18 @@
SPDX-License-Identifier: GPL-3.0-only
Copyright (C) 2025 TZshot contributors
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, version 3 of the License.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Full license text:
https://www.gnu.org/licenses/gpl-3.0.txt
+243
View File
@@ -0,0 +1,243 @@
# TZshot
[中文](#中文) | [English](#english)
---
## 中文
### 开源许可
本项目采用 **GNU General Public License v3.0(GPL-3.0-only)** 开源。
详见仓库根目录 [LICENSE](./LICENSE)。
### 项目简介
`TZshot` 是一个基于 **Qt 6 + QML + C++** 的截图与贴图工具,支持多屏截图、标注、贴图编辑、OCR、GIF 录制、全局快捷键、系统托盘和 AI 图像编辑。
### 主要功能
- 多屏截图(虚拟桌面坐标、跨屏场景)
- 标注工具:
- 矩形、圆形、箭头、画笔
- 文字标注(文本内容、背景开关)
- 序号标注(支持自动递增)
- 高亮笔
- 马赛克(可调强度)
- 撤销
- 截图结果:
- 复制到剪贴板
- 保存到文件
- 贴图到桌面
- 贴图窗口:
- 拖拽、透明度调整
- 缩放、旋转、镜像、1:1 恢复
- 标注、OCR、右键菜单
- 完成后复制、另存为/覆盖闭环
- GIF 录制(可调录制区域)
- OCR 识别结果浮窗
- 全局快捷键(可配置并持久化)
- 系统托盘菜单
- 中英文切换(切换后自动重启应用)
- AI 图像编辑(可配置模型与 API Key)
### 技术栈
- C++17
- Qt 6(Core / Gui / Widgets / Quick / Qml / Network / QuickDialogs2 / LinguistTools)
- CMake
- 全局快捷键:
- Windows: `RegisterHotKey`
- Linux(X11): `xcb_grab_key`
### 目录结构
```text
TZshot/
├─ src/
│ ├─ main.cpp
│ ├─ model/
│ ├─ viewmodel/
│ ├─ paint_board/
│ ├─ shortcut_key/
│ ├─ ocr/
│ └─ ai_call/
├─ qml/
├─ i18n/
├─ resource/
├─ thirdpart/
└─ CMakeLists.txt
```
### 环境要求
- CMake >= 3.16
- Qt 6.x
- C++17 编译器
Windows:
- MSVC 2019/2022(推荐使用 Qt Creator 对应 Kit)
Linux:
- X11/xcb 相关开发库
### 构建与运行
#### 方式一:Qt Creator(推荐)
1. 打开项目根目录 `CMakeLists.txt`
2. 选择 Qt 6 Kit
3. Configure -> Build -> Run
#### 方式二:命令行
```bash
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build --config Release
```
运行(Windows):
```bash
./build/Release/TZshot.exe
```
运行(Linux,路径以实际生成为准):
```bash
./build/TZshot
```
### 默认快捷键
- `Alt + A`: 截图
- `Alt + S`: 截图并保存
- `Alt + P`: 贴图到桌面
- `Alt + Q`: 显示/隐藏窗口
### 配置项(QSettings)
- AI
- `AI/apiKey`
- `AI/selectedModel`
- 快捷键
- `Shortcuts/screenshot`
- `Shortcuts/screenshotSave`
- `Shortcuts/sticky`
- `Shortcuts/toggle`
- 通用
- `App/language`(`zh_CN` / `en`)
- `ImageSaver/savePath`
### 已知说明
- Linux 全局快捷键目前基于 X11(暂不覆盖 Wayland 原生实现)
- GIF 与部分平台能力仍可继续做跨平台增强
### 贡献与安全提示
- 请勿提交任何真实的 `API Key`、令牌或个人隐私数据。
- 提交前建议自查编码与换行:`UTF-8(无 BOM)` + `LF`。
- 若修改第三方依赖,请同步补充其许可证说明。
---
## English
### License
This project is licensed under **GNU General Public License v3.0 (GPL-3.0-only)**.
See [LICENSE](./LICENSE) in the repository root.
### Overview
`TZshot` is a screenshot and pin-image utility built with **Qt 6 + QML + C++**.
It supports multi-screen capture, rich annotations, sticky image editing, OCR, GIF recording, global shortcuts, tray integration, and AI image editing.
### Key Features
- Multi-screen capture (virtual desktop coordinates)
- Annotation tools:
- Rectangle, circle, arrow, freehand pen
- Text annotation (text content + background toggle)
- Number annotation (auto-increment supported)
- Highlighter
- Mosaic (adjustable intensity)
- Undo
- Output actions:
- Copy to clipboard
- Save to file
- Pin to desktop (sticky window)
- Sticky window:
- Drag, opacity control
- Zoom, rotate, mirror, reset-to-1:1
- Annotate, OCR, context menu actions
- Apply-and-copy / save-as / overwrite workflow
- GIF recording with adjustable capture area
- OCR result window
- Configurable global shortcuts (persisted)
- System tray menu
- Chinese/English UI switching (app restarts after switching)
- AI image editing with configurable model/API key
### Tech Stack
- C++17
- Qt 6 (Core / Gui / Widgets / Quick / Qml / Network / QuickDialogs2 / LinguistTools)
- CMake
- Global shortcuts:
- Windows: `RegisterHotKey`
- Linux(X11): `xcb_grab_key`
### Project Structure
```text
TZshot/
├─ src/
├─ qml/
├─ i18n/
├─ resource/
├─ thirdpart/
└─ CMakeLists.txt
```
### Requirements
- CMake >= 3.16
- Qt 6.x
- C++17 compiler
Windows:
- MSVC 2019/2022 (Qt Creator kit recommended)
Linux:
- X11/xcb development libraries
### Build & Run
#### Option 1: Qt Creator (Recommended)
1. Open `CMakeLists.txt` in project root
2. Select a Qt 6 kit
3. Configure -> Build -> Run
#### Option 2: Command Line
```bash
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build --config Release
```
Run on Windows:
```bash
./build/Release/TZshot.exe
```
Run on Linux (path may vary by generator):
```bash
./build/TZshot
```
### Default Shortcuts
- `Alt + A`: Screenshot
- `Alt + S`: Screenshot and Save
- `Alt + P`: Pin to Desktop
- `Alt + Q`: Show/Hide Window
### Configuration (QSettings)
- AI
- `AI/apiKey`
- `AI/selectedModel`
- Shortcuts
- `Shortcuts/screenshot`
- `Shortcuts/screenshotSave`
- `Shortcuts/sticky`
- `Shortcuts/toggle`
- General
- `App/language` (`zh_CN` / `en`)
- `ImageSaver/savePath`
### Notes
- Linux global shortcuts currently rely on X11 (no native Wayland implementation yet)
- GIF and some platform-dependent capabilities can be further improved cross-platform
### Contributing & Security
- Do not commit real API keys, tokens, or private data.
- Keep source files in `UTF-8 (no BOM)` with `LF` line endings.
- When adding/updating third-party dependencies, include their license notices.
+18
View File
@@ -0,0 +1,18 @@
# Third-Party Notices
This project includes or depends on third-party components.
Please ensure license compatibility before distribution.
## Included in source tree
- `src/viewmodel/stb_image_write.h`
License: Public domain / MIT (as stated in upstream file header)
- `src/gif_encoder/gif.h`
License: See upstream file header and original project license
## Runtime / system dependencies
- Qt 6
- Tesseract OCR
- Leptonica
- X11 / xcb (Linux)
Please refer to each dependency's official repository for full license text.
+61
View File
@@ -0,0 +1,61 @@
#!/bin/bash
# 功能:递归查找并删除指定目录下所有的 .o 文件
# 作者:编程助手
# 版本:1.0
# ======================== 配置区 ========================
# 设置要扫描的根目录(默认为当前目录,你可以修改为绝对路径)
TARGET_DIR="."
# 是否开启交互模式(true=删除前确认,false=直接删除)
INTERACTIVE=true
# =======================================================
# 检查目标目录是否存在
if [ ! -d "$TARGET_DIR" ]; then
echo "错误:目标目录 $TARGET_DIR 不存在!"
exit 1
fi
# 切换到目标目录
cd "$TARGET_DIR" || exit 1
# 查找所有 .o 文件
echo "正在查找 $TARGET_DIR 目录下的所有 .o 文件..."
O_FILES=$(find . -type f -name "*.o" -print)
# 如果没有找到 .o 文件
if [ -z "$O_FILES" ]; then
echo "未找到任何 .o 文件,无需删除。"
exit 0
fi
# 显示找到的 .o 文件列表
echo -e "\n找到以下 .o 文件:"
echo "$O_FILES"
# 交互模式确认
if [ "$INTERACTIVE" = true ]; then
echo -e "\n是否确定删除以上所有 .o 文件?[y/N]"
read -r CONFIRM
if [[ ! "$CONFIRM" =~ ^[Yy]$ ]]; then
echo "用户取消删除操作。"
exit 0
fi
fi
# 执行删除操作
echo -e "\n开始删除 .o 文件..."
find . -type f -name "*.o" -delete
# 验证删除结果
REMAINING_FILES=$(find . -type f -name "*.o" -print)
if [ -z "$REMAINING_FILES" ]; then
echo "✅ 所有 .o 文件已成功删除!"
else
echo "⚠️ 部分 .o 文件删除失败,剩余文件:"
echo "$REMAINING_FILES"
exit 1
fi
exit 0
+795
View File
@@ -0,0 +1,795 @@
<?xml version='1.0' encoding='utf-8'?>
<TS version="2.1" language="en_US">
<context>
<name>GifRecordViewModel</name>
<message>
<location filename="../src/viewmodel/gif_record_view_model.cpp" line="48" />
<source>录制区域无效</source>
<translation type="unfinished" />
</message>
<message>
<location filename="../src/viewmodel/gif_record_view_model.cpp" line="96" />
<source>未采集到任何帧</source>
<translation type="unfinished" />
</message>
<message>
<location filename="../src/viewmodel/gif_record_view_model.cpp" line="150" />
<source>GIF 编码失败,请检查保存路径或磁盘空间</source>
<translation type="unfinished" />
</message>
</context>
<context>
<name>QObject</name>
<message>
<source>开始截图</source>
<translation type="vanished">Take Screenshot</translation>
</message>
<message>
<source>显示主窗口</source>
<translation type="vanished">Show Main Window</translation>
</message>
<message>
<location filename="../src/tray_icon_helper.cpp" line="8" />
<source>截图</source>
<translation>Screenshot</translation>
</message>
<message>
<location filename="../src/tray_icon_helper.cpp" line="11" />
<source>设置</source>
<translation>Settings</translation>
</message>
<message>
<location filename="../src/tray_icon_helper.cpp" line="14" />
<source>关于</source>
<translation>About</translation>
</message>
<message>
<location filename="../src/tray_icon_helper.cpp" line="17" />
<source>退出</source>
<translation>Quit</translation>
</message>
<message>
<source>序号</source>
<translation>Number</translation>
</message>
</context>
<context>
<name>S:</name>
<message>
<location filename="../thirdpart/vcpkg/buildtrees/liblzma/src/v5.8.2-30365c07c3.clean/src/liblzma/common/string_conversion.c" line="731" />
<source />
<comment>Don't translate the</comment>
<translation />
</message>
</context>
<context>
<name>TZAboutWin</name>
<message>
<location filename="../qml/Setting/TZAboutWin.qml" line="9" />
<source>关于 TZshot</source>
<translation>About TZshot</translation>
</message>
<message>
<location filename="../qml/Setting/TZAboutWin.qml" line="63" />
<source>版本 v1.0.0</source>
<translation>Version v1.0.0</translation>
</message>
<message>
<location filename="../qml/Setting/TZAboutWin.qml" line="83" />
<source>简洁高效的截图贴图工具,支持标注、AI 编辑、马赛克等功能。</source>
<translation>A lightweight screenshot and annotation tool with AI editing, mosaic, and more.</translation>
</message>
<message>
<location filename="../qml/Setting/TZAboutWin.qml" line="96" />
<source>© 2025 TZshot. All rights reserved.</source>
<translation>© 2025 TZshot. All rights reserved.</translation>
</message>
<message>
<location filename="../qml/Setting/TZAboutWin.qml" line="112" />
<source>关闭</source>
<translation>Close</translation>
</message>
</context>
<context>
<name>TZChat</name>
<message>
<location filename="../qml/TZChat.qml" line="83" />
<source>AI 图像编辑</source>
<translation>AI Image Edit</translation>
</message>
<message>
<location filename="../qml/TZChat.qml" line="162" />
<source>当前贴图</source>
<translation>Current Image</translation>
</message>
<message>
<location filename="../qml/TZChat.qml" line="171" />
<source>未关联贴图</source>
<translation>No Image Linked</translation>
</message>
<message>
<location filename="../qml/TZChat.qml" line="178" />
<source>发送后将对此图执行 AI 编辑</source>
<translation>AI edit will be applied to this image after sending</translation>
</message>
<message>
<location filename="../qml/TZChat.qml" line="203" />
<source>描述您想要的修改...</source>
<translation>Describe the changes you want...</translation>
</message>
<message>
<location filename="../qml/TZChat.qml" line="228" />
<source>取消</source>
<translation>Cancel</translation>
</message>
<message>
<location filename="../qml/TZChat.qml" line="245" />
<source>发送</source>
<translation>Send</translation>
</message>
</context>
<context>
<name>TZGeneralSettings</name>
<message>
<location filename="../qml/Setting/TZGeneralSettings.qml" line="35" />
<source>大模型设置</source>
<translation>AI Model Settings</translation>
</message>
<message>
<location filename="../qml/Setting/TZGeneralSettings.qml" line="63" />
<source>大模型选择</source>
<translation>Select Model</translation>
</message>
<message>
<location filename="../qml/Setting/TZGeneralSettings.qml" line="71" />
<source>阿里云百炼</source>
<translation>Alibaba Cloud Bailian</translation>
</message>
<message>
<location filename="../qml/Setting/TZGeneralSettings.qml" line="71" />
<source>火山引擎</source>
<translation>Volcano Engine</translation>
</message>
<message>
<location filename="../qml/Setting/TZGeneralSettings.qml" line="124" />
<source>请输入 API Key</source>
<translation>Enter API Key</translation>
</message>
<message>
<location filename="../qml/Setting/TZGeneralSettings.qml" line="148" />
<source>保存</source>
<translation type="unfinished" />
</message>
<message>
<location filename="../qml/Setting/TZGeneralSettings.qml" line="183" />
<source>语言设置</source>
<translation>Language</translation>
</message>
<message>
<location filename="../qml/Setting/TZGeneralSettings.qml" line="206" />
<source>界面语言</source>
<translation>UI Language</translation>
</message>
<message>
<location filename="../qml/Setting/TZGeneralSettings.qml" line="241" />
<source>截图保存路径</source>
<translation>Screenshot Save Path</translation>
</message>
<message>
<location filename="../qml/Setting/TZGeneralSettings.qml" line="264" />
<source>保存路径</source>
<translation>Save Path</translation>
</message>
<message>
<location filename="../qml/Setting/TZGeneralSettings.qml" line="316" />
<source>浏览</source>
<translation>Browse</translation>
</message>
</context>
<context>
<name>TZGifRecordBar</name>
<message>
<location filename="../qml/TZGifRecordBar.qml" line="186" />
<source>正在生成 GIF...</source>
<translation type="unfinished" />
</message>
</context>
<context>
<name>TZHotkeySettings</name>
<message>
<location filename="../qml/Setting/TZHotkeySettings.qml" line="49" />
<source>编辑快捷键 · %1</source>
<translation>Edit Shortcut · %1</translation>
</message>
<message>
<location filename="../qml/Setting/TZHotkeySettings.qml" line="70" />
<source>请按下新的快捷键组合...</source>
<translation>Press the new shortcut key combination...</translation>
</message>
<message>
<location filename="../qml/Setting/TZHotkeySettings.qml" line="103" />
<source>取消</source>
<translation>Cancel</translation>
</message>
<message>
<location filename="../qml/Setting/TZHotkeySettings.qml" line="133" />
<source>确认</source>
<translation>Confirm</translation>
</message>
<message>
<location filename="../qml/Setting/TZHotkeySettings.qml" line="150" />
<location filename="../qml/Setting/TZHotkeySettings.qml" line="228" />
<source>截图</source>
<translation>Screenshot</translation>
</message>
<message>
<location filename="../qml/Setting/TZHotkeySettings.qml" line="150" />
<location filename="../qml/Setting/TZHotkeySettings.qml" line="229" />
<source>截图并保存</source>
<translation>Screenshot &amp; Save</translation>
</message>
<message>
<location filename="../qml/Setting/TZHotkeySettings.qml" line="150" />
<location filename="../qml/Setting/TZHotkeySettings.qml" line="230" />
<source>贴图到桌面</source>
<translation>Pin to Desktop</translation>
</message>
<message>
<location filename="../qml/Setting/TZHotkeySettings.qml" line="150" />
<location filename="../qml/Setting/TZHotkeySettings.qml" line="231" />
<source>显示/隐藏窗口</source>
<translation>Show/Hide Window</translation>
</message>
<message>
<location filename="../qml/Setting/TZHotkeySettings.qml" line="201" />
<source>全局快捷键</source>
<translation>Global Shortcuts</translation>
</message>
<message>
<location filename="../qml/Setting/TZHotkeySettings.qml" line="293" />
<source>编辑</source>
<translation>Edit</translation>
</message>
<message>
<location filename="../qml/Setting/TZHotkeySettings.qml" line="341" />
<source>说明</source>
<translation>Notes</translation>
</message>
<message>
<location filename="../qml/Setting/TZHotkeySettings.qml" line="362" />
<source>快捷键在全局范围内生效。点击「编辑」后直接按下新的组合键,再点「确认」即可保存。
若与系统或其他应用快捷键冲突,请修改为其他组合。</source>
<translation>Shortcuts work globally. Click "Edit", press the new key combination, then click "Confirm" to save.
If it conflicts with system or other app shortcuts, change to a different combination.</translation>
</message>
</context>
<context>
<name>TZMarkToolInfo</name>
<message>
<location filename="../qml/TZMarkToolInfo.qml" line="13" />
<source>文本</source>
<translation type="unfinished" />
</message>
<message>
<location filename="../qml/TZMarkToolInfo.qml" line="62" />
<source>细</source>
<translation>Thin</translation>
</message>
<message>
<location filename="../qml/TZMarkToolInfo.qml" line="63" />
<source>中</source>
<translation>Medium</translation>
</message>
<message>
<location filename="../qml/TZMarkToolInfo.qml" line="64" />
<source>粗</source>
<translation>Thick</translation>
</message>
<message>
<location filename="../qml/TZMarkToolInfo.qml" line="65" />
<source>特粗</source>
<translation>Extra Thick</translation>
</message>
<message>
<location filename="../qml/TZMarkToolInfo.qml" line="133" />
<source>输入文字</source>
<translation type="unfinished" />
</message>
<message>
<location filename="../qml/TZMarkToolInfo.qml" line="140" />
<source>背景</source>
<translation type="unfinished" />
</message>
<message>
<location filename="../qml/TZMarkToolInfo.qml" line="152" />
<source>模糊</source>
<translation type="unfinished" />
</message>
<message>
<location filename="../qml/TZMarkToolInfo.qml" line="177" />
<source>自动递增</source>
<translation type="unfinished" />
</message>
<message>
<location filename="../qml/TZMarkToolInfo.qml" line="187" />
<source>数字</source>
<translation type="unfinished" />
</message>
</context>
<context>
<name>TZOcrResult</name>
<message>
<location filename="../qml/TZOcrResult.qml" line="67" />
<source>OCR 识别结果</source>
<translation>OCR Results</translation>
</message>
<message>
<location filename="../qml/TZOcrResult.qml" line="167" />
<source>正在识别中…</source>
<translation>Recognizing…</translation>
</message>
<message>
<location filename="../qml/TZOcrResult.qml" line="190" />
<source>识别结果将显示在这里…</source>
<translation>Recognition results will appear here…</translation>
</message>
<message>
<location filename="../qml/TZOcrResult.qml" line="239" />
<source>复制全部</source>
<translation>Copy All</translation>
</message>
<message>
<location filename="../qml/TZOcrResult.qml" line="265" />
<source>关闭</source>
<translation>Close</translation>
</message>
<message>
<location filename="../qml/TZOcrResult.qml" line="293" />
<source>已复制!</source>
<translation>Copied!</translation>
</message>
</context>
<context>
<name>TZRightPanel</name>
<message>
<location filename="../qml/Setting/TZRightPanel.qml" line="90" />
<source>通用设置</source>
<translation>General</translation>
</message>
<message>
<location filename="../qml/Setting/TZRightPanel.qml" line="137" />
<source>快捷键设置</source>
<translation>Shortcuts</translation>
</message>
</context>
<context>
<name>TZSettingWin</name>
<message>
<location filename="../qml/Setting/TZSettingWin.qml" line="11" />
<source>设置</source>
<translation>Settings</translation>
</message>
<message>
<location filename="../qml/Setting/TZSettingWin.qml" line="29" />
<source>选择保存位置</source>
<translation>Choose Save Location</translation>
</message>
</context>
<context>
<name>TZStickyWindow</name>
<message>
<location filename="../qml/TZStickyWindow.qml" line="515" />
<source>当前透明度 %1%</source>
<translation>Opacity: %1%</translation>
</message>
<message>
<location filename="../qml/TZStickyWindow.qml" line="518" />
<location filename="../qml/TZStickyWindow.qml" line="572" />
<source>覆盖贴图(合并标注)</source>
<translation>Overwrite Image (Merge Annotations)</translation>
</message>
<message>
<location filename="../qml/TZStickyWindow.qml" line="523" />
<location filename="../qml/TZStickyWindow.qml" line="558" />
<source>显示工具栏</source>
<translation>Show Toolbar</translation>
</message>
<message>
<location filename="../qml/TZStickyWindow.qml" line="534" />
<source>透明度 +5%</source>
<translation>Opacity +5%</translation>
</message>
<message>
<location filename="../qml/TZStickyWindow.qml" line="535" />
<source>透明度 -5%</source>
<translation>Opacity -5%</translation>
</message>
<message>
<location filename="../qml/TZStickyWindow.qml" line="541" />
<source>旋转 90°</source>
<translation>Rotate 90°</translation>
</message>
<message>
<location filename="../qml/TZStickyWindow.qml" line="548" />
<source>重置位置</source>
<translation>Reset Position</translation>
</message>
<message>
<location filename="../qml/TZStickyWindow.qml" line="520" />
<location filename="../qml/TZStickyWindow.qml" line="566" />
<location filename="../qml/TZStickyWindow.qml" line="581" />
<source>AI 编辑</source>
<translation>AI Edit</translation>
</message>
<message>
<source>保存图片</source>
<translation type="vanished">Save Image</translation>
</message>
<message>
<location filename="../qml/TZStickyWindow.qml" line="519" />
<location filename="../qml/TZStickyWindow.qml" line="573" />
<location filename="../qml/TZStickyWindow.qml" line="580" />
<source>复制贴图</source>
<translation>Copy Image</translation>
</message>
<message>
<location filename="../qml/TZStickyWindow.qml" line="530" />
<location filename="../qml/TZStickyWindow.qml" line="583" />
<location filename="../qml/TZStickyWindow.qml" line="588" />
<source>关闭窗口</source>
<translation>Close Window</translation>
</message>
<message>
<location filename="../qml/TZStickyWindow.qml" line="570" />
<source>保存原图</source>
<translation>Save Original Image</translation>
</message>
<message>
<location filename="../qml/TZStickyWindow.qml" line="571" />
<location filename="../qml/TZStickyWindow.qml" line="602" />
<source>另存为(合并标注)</source>
<translation>Save As (Merge Annotations)</translation>
</message>
<message>
<location filename="../qml/TZStickyWindow.qml" line="578" />
<source>常用项置顶</source>
<translation>Pin Common Items</translation>
</message>
<message>
<location filename="../qml/TZStickyWindow.qml" line="579" />
<source>覆盖贴图</source>
<translation>Overwrite Image</translation>
</message>
<message>
<location filename="../qml/TZStickyWindow.qml" line="582" />
<source>显示/隐藏工具栏</source>
<translation>Show/Hide Toolbar</translation>
</message>
<message>
<location filename="../qml/TZStickyWindow.qml" line="593" />
<source>选择保存位置</source>
<translation>Choose Save Location</translation>
</message>
<message>
<location filename="../qml/TZStickyWindow.qml" line="341" />
<source>Pencil</source>
<translation>Pencil</translation>
</message>
<message>
<location filename="../qml/TZStickyWindow.qml" line="350" />
<source>Rectangle</source>
<translation>Rectangle</translation>
</message>
<message>
<location filename="../qml/TZStickyWindow.qml" line="359" />
<source>Circle</source>
<translation>Circle</translation>
</message>
<message>
<location filename="../qml/TZStickyWindow.qml" line="368" />
<source>Arrow</source>
<translation>Arrow</translation>
</message>
<message>
<location filename="../qml/TZStickyWindow.qml" line="382" />
<location filename="../qml/TZStickyWindow.qml" line="383" />
<source>Mosaic</source>
<translation>Mosaic</translation>
</message>
<message>
<location filename="../qml/TZStickyWindow.qml" line="404" />
<location filename="../qml/TZStickyWindow.qml" line="539" />
<source>缩小</source>
<translation>Zoom Out</translation>
</message>
<message>
<location filename="../qml/TZStickyWindow.qml" line="406" />
<location filename="../qml/TZStickyWindow.qml" line="540" />
<source>放大</source>
<translation>Zoom In</translation>
</message>
<message>
<location filename="../qml/TZStickyWindow.qml" line="407" />
<source>旋转90°</source>
<translation>Rotate 90°</translation>
</message>
<message>
<location filename="../qml/TZStickyWindow.qml" line="408" />
<location filename="../qml/TZStickyWindow.qml" line="542" />
<source>镜像</source>
<translation>Mirror</translation>
</message>
<message>
<location filename="../qml/TZStickyWindow.qml" line="409" />
<location filename="../qml/TZStickyWindow.qml" line="543" />
<source>恢复原始大小</source>
<translation>Reset Size</translation>
</message>
<message>
<location filename="../qml/TZStickyWindow.qml" line="415" />
<source>完成并复制</source>
<translation>Apply and Copy</translation>
</message>
<message>
<location filename="../qml/TZStickyWindow.qml" line="422" />
<source>撤销</source>
<translation>Undo</translation>
</message>
<message>
<location filename="../qml/TZStickyWindow.qml" line="434" />
<location filename="../qml/TZStickyWindow.qml" line="435" />
<location filename="../qml/TZStickyWindow.qml" line="523" />
<location filename="../qml/TZStickyWindow.qml" line="558" />
<source>隐藏工具栏</source>
<translation>Hide Toolbar</translation>
</message>
<message>
<source>序号</source>
<translation>Number</translation>
</message>
<message>
<source>旋转90°</source>
<translation>Rotate 90°</translation>
</message>
<message>
<source>恢复原始大小</source>
<translation>Reset Size</translation>
</message>
<message>
<source>隐藏工具栏</source>
<translation>Hide Toolbar</translation>
</message>
<message>
<location filename="../qml/TZStickyWindow.qml" line="324" />
<location filename="../qml/TZStickyWindow.qml" line="325" />
<source>OCR</source>
<translation>OCR</translation>
</message>
</context>
<context>
<name>TZTool</name>
<message>
<source>确认截图</source>
<translation type="vanished">Confirm</translation>
</message>
<message>
<location filename="../qml/TZTool.qml" line="83" />
<source>取消</source>
<translation>Cancel</translation>
</message>
<message>
<source>保存到文件</source>
<translation type="vanished">Save to File</translation>
</message>
<message>
<source>贴图到桌面</source>
<translation type="vanished">Pin to Desktop</translation>
</message>
<message>
<source>矩形标注</source>
<translation type="vanished">Rectangle</translation>
</message>
<message>
<source>椭圆标注</source>
<translation type="vanished">Ellipse</translation>
</message>
<message>
<source>箭头标注</source>
<translation type="vanished">Arrow</translation>
</message>
<message>
<location filename="../qml/TZTool.qml" line="76" />
<source>完成</source>
<translation>Done</translation>
</message>
<message>
<location filename="../qml/TZTool.qml" line="100" />
<source>录制GIF</source>
<translation>Record GIF</translation>
</message>
<message>
<location filename="../qml/TZTool.qml" line="109" />
<source>保存</source>
<translation>Save</translation>
</message>
<message>
<location filename="../qml/TZTool.qml" line="116" />
<source>贴图</source>
<translation>Pin</translation>
</message>
<message>
<location filename="../qml/TZTool.qml" line="127" />
<source>矩形</source>
<translation>Rectangle</translation>
</message>
<message>
<location filename="../qml/TZTool.qml" line="136" />
<source>圆形</source>
<translation>Circle</translation>
</message>
<message>
<location filename="../qml/TZTool.qml" line="145" />
<source>箭头</source>
<translation>Arrow</translation>
</message>
<message>
<location filename="../qml/TZTool.qml" line="154" />
<source>画笔</source>
<translation>Pen</translation>
</message>
<message>
<location filename="../qml/TZTool.qml" line="163" />
<source>高亮</source>
<translation>Highlight</translation>
</message>
<message>
<location filename="../qml/TZTool.qml" line="172" />
<source>马赛克</source>
<translation>Mosaic</translation>
</message>
<message>
<location filename="../qml/TZTool.qml" line="181" />
<source>文字</source>
<translation>Text</translation>
</message>
<message>
<location filename="../qml/TZTool.qml" line="190" />
<source>序号</source>
<translation>Number</translation>
</message>
<message>
<location filename="../qml/TZTool.qml" line="197" />
<source>撤销</source>
<translation>Undo</translation>
</message>
<message>
<source>序号</source>
<translation>Number</translation>
</message>
<message>
<location filename="../qml/TZTool.qml" line="92" />
<source>OCR</source>
<translation>OCR</translation>
</message>
<message>
<source>录制GIF</source>
<translation>Record GIF</translation>
</message>
<message>
<source>马赛克</source>
<translation type="vanished">Mosaic</translation>
</message>
</context>
<context>
<name>TZWindow</name>
<message>
<source>选择保存位置</source>
<translation type="vanished">Choose Save Location</translation>
</message>
<message>
<source>保存成功</source>
<translation type="vanished">Saved</translation>
</message>
<message>
<source>截图已保存到文件</source>
<translation type="vanished">Screenshot saved to file</translation>
</message>
<message>
<source>保存失败</source>
<translation type="vanished">Save Failed</translation>
</message>
<message>
<source>截图文件保存失败</source>
<translation type="vanished">Failed to save screenshot file</translation>
</message>
<message>
<location filename="../qml/TZWindow.qml" line="563" />
<source>%1 * %2</source>
<translation>%1 * %2</translation>
</message>
<message>
<source>截图完成</source>
<translation type="vanished">Done</translation>
</message>
<message>
<source>已复制到剪贴板</source>
<translation type="vanished">Copied to clipboard</translation>
</message>
<message>
<source>截图失败</source>
<translation type="vanished">Screenshot Failed</translation>
</message>
<message>
<source>截图复制到剪贴板失败</source>
<translation type="vanished">Failed to copy screenshot to clipboard</translation>
</message>
<message>
<location filename="../qml/TZWindow.qml" line="134" />
<source>Capture done</source>
<translation>Capture done</translation>
</message>
<message>
<location filename="../qml/TZWindow.qml" line="134" />
<source>Copied to clipboard</source>
<translation>Copied to clipboard</translation>
</message>
<message>
<location filename="../qml/TZWindow.qml" line="136" />
<source>Capture failed</source>
<translation>Capture failed</translation>
</message>
<message>
<location filename="../qml/TZWindow.qml" line="136" />
<source>Failed to copy image</source>
<translation>Failed to copy image</translation>
</message>
<message>
<location filename="../qml/TZWindow.qml" line="434" />
<source>Select save location</source>
<translation>Select save location</translation>
</message>
<message>
<location filename="../qml/TZWindow.qml" line="442" />
<source>Save success</source>
<translation>Save success</translation>
</message>
<message>
<location filename="../qml/TZWindow.qml" line="442" />
<source>Screenshot saved to file</source>
<translation>Screenshot saved to file</translation>
</message>
<message>
<location filename="../qml/TZWindow.qml" line="445" />
<source>Save failed</source>
<translation>Save failed</translation>
</message>
<message>
<location filename="../qml/TZWindow.qml" line="445" />
<source>Failed to save screenshot file</source>
<translation>Failed to save screenshot file</translation>
</message>
<message>
<location filename="../qml/TZWindow.qml" line="705" />
<source>Cancel</source>
<translation>Cancel</translation>
</message>
<message>
<location filename="../qml/TZWindow.qml" line="727" />
<source>OK</source>
<translation>OK</translation>
</message>
</context>
<context>
<name>main</name>
<message>
<location filename="../qml/main.qml" line="10" />
<source>TZshot</source>
<translation>TZshot</translation>
</message>
<message>
<location filename="../qml/main.qml" line="199" />
<source>OCR 识别失败</source>
<translation>OCR Recognition Failed</translation>
</message>
</context>
</TS>
+691
View File
@@ -0,0 +1,691 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE TS>
<TS version="2.1" language="zh_CN">
<context>
<name>GifRecordViewModel</name>
<message>
<location filename="../src/viewmodel/gif_record_view_model.cpp" line="48"/>
<source>录制区域无效</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../src/viewmodel/gif_record_view_model.cpp" line="96"/>
<source>未采集到任何帧</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../src/viewmodel/gif_record_view_model.cpp" line="150"/>
<source>GIF 编码失败,请检查保存路径或磁盘空间</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>QObject</name>
<message>
<location filename="../src/tray_icon_helper.cpp" line="8"/>
<source>截图</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../src/tray_icon_helper.cpp" line="11"/>
<source>设置</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../src/tray_icon_helper.cpp" line="14"/>
<source>关于</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../src/tray_icon_helper.cpp" line="17"/>
<source>退出</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>S:</name>
<message>
<location filename="../thirdpart/vcpkg/buildtrees/liblzma/src/v5.8.2-30365c07c3.clean/src/liblzma/common/string_conversion.c" line="731"/>
<source></source>
<comment>Don&apos;t translate the</comment>
<translation></translation>
</message>
</context>
<context>
<name>TZAboutWin</name>
<message>
<location filename="../qml/Setting/TZAboutWin.qml" line="9"/>
<source>关于 TZshot</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/Setting/TZAboutWin.qml" line="63"/>
<source>版本 v1.0.0</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/Setting/TZAboutWin.qml" line="83"/>
<source>简洁高效的截图贴图工具,支持标注、AI 编辑、马赛克等功能。</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/Setting/TZAboutWin.qml" line="96"/>
<source>© 2025 TZshot. All rights reserved.</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/Setting/TZAboutWin.qml" line="112"/>
<source>关闭</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>TZChat</name>
<message>
<location filename="../qml/TZChat.qml" line="83"/>
<source>AI 图像编辑</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/TZChat.qml" line="162"/>
<source>当前贴图</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/TZChat.qml" line="171"/>
<source>未关联贴图</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/TZChat.qml" line="178"/>
<source>发送后将对此图执行 AI 编辑</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/TZChat.qml" line="203"/>
<source>描述您想要的修改...</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/TZChat.qml" line="228"/>
<source>取消</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/TZChat.qml" line="245"/>
<source>发送</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>TZGeneralSettings</name>
<message>
<location filename="../qml/Setting/TZGeneralSettings.qml" line="35"/>
<source>大模型设置</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/Setting/TZGeneralSettings.qml" line="63"/>
<source>大模型选择</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/Setting/TZGeneralSettings.qml" line="71"/>
<source>阿里云百炼</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/Setting/TZGeneralSettings.qml" line="71"/>
<source>火山引擎</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/Setting/TZGeneralSettings.qml" line="124"/>
<source>请输入 API Key</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/Setting/TZGeneralSettings.qml" line="148"/>
<source>保存</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/Setting/TZGeneralSettings.qml" line="183"/>
<source>语言设置</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/Setting/TZGeneralSettings.qml" line="206"/>
<source>界面语言</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/Setting/TZGeneralSettings.qml" line="241"/>
<source>截图保存路径</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/Setting/TZGeneralSettings.qml" line="264"/>
<source>保存路径</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/Setting/TZGeneralSettings.qml" line="316"/>
<source>浏览</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>TZGifRecordBar</name>
<message>
<location filename="../qml/TZGifRecordBar.qml" line="186"/>
<source>正在生成 GIF...</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>TZHotkeySettings</name>
<message>
<location filename="../qml/Setting/TZHotkeySettings.qml" line="49"/>
<source>编辑快捷键 · %1</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/Setting/TZHotkeySettings.qml" line="70"/>
<source>请按下新的快捷键组合...</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/Setting/TZHotkeySettings.qml" line="103"/>
<source>取消</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/Setting/TZHotkeySettings.qml" line="133"/>
<source>确认</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/Setting/TZHotkeySettings.qml" line="150"/>
<location filename="../qml/Setting/TZHotkeySettings.qml" line="228"/>
<source>截图</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/Setting/TZHotkeySettings.qml" line="150"/>
<location filename="../qml/Setting/TZHotkeySettings.qml" line="229"/>
<source>截图并保存</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/Setting/TZHotkeySettings.qml" line="150"/>
<location filename="../qml/Setting/TZHotkeySettings.qml" line="230"/>
<source>贴图到桌面</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/Setting/TZHotkeySettings.qml" line="150"/>
<location filename="../qml/Setting/TZHotkeySettings.qml" line="231"/>
<source>显示/隐藏窗口</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/Setting/TZHotkeySettings.qml" line="201"/>
<source>全局快捷键</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/Setting/TZHotkeySettings.qml" line="293"/>
<source>编辑</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/Setting/TZHotkeySettings.qml" line="341"/>
<source>说明</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/Setting/TZHotkeySettings.qml" line="362"/>
<source>快捷键在全局范围内生效。点击「编辑」后直接按下新的组合键,再点「确认」即可保存。
若与系统或其他应用快捷键冲突,请修改为其他组合。</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>TZMarkToolInfo</name>
<message>
<location filename="../qml/TZMarkToolInfo.qml" line="13"/>
<source>文本</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/TZMarkToolInfo.qml" line="62"/>
<source>细</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/TZMarkToolInfo.qml" line="63"/>
<source>中</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/TZMarkToolInfo.qml" line="64"/>
<source>粗</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/TZMarkToolInfo.qml" line="65"/>
<source>特粗</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/TZMarkToolInfo.qml" line="133"/>
<source>输入文字</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/TZMarkToolInfo.qml" line="140"/>
<source>背景</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/TZMarkToolInfo.qml" line="152"/>
<source>模糊</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/TZMarkToolInfo.qml" line="177"/>
<source>自动递增</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/TZMarkToolInfo.qml" line="187"/>
<source>数字</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>TZOcrResult</name>
<message>
<location filename="../qml/TZOcrResult.qml" line="67"/>
<source>OCR 识别结果</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/TZOcrResult.qml" line="167"/>
<source>正在识别中…</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/TZOcrResult.qml" line="190"/>
<source>识别结果将显示在这里…</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/TZOcrResult.qml" line="239"/>
<source>复制全部</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/TZOcrResult.qml" line="265"/>
<source>关闭</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/TZOcrResult.qml" line="293"/>
<source>已复制!</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>TZRightPanel</name>
<message>
<location filename="../qml/Setting/TZRightPanel.qml" line="90"/>
<source>通用设置</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/Setting/TZRightPanel.qml" line="137"/>
<source>快捷键设置</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>TZSettingWin</name>
<message>
<location filename="../qml/Setting/TZSettingWin.qml" line="11"/>
<source>设置</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/Setting/TZSettingWin.qml" line="29"/>
<source>选择保存位置</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>TZStickyWindow</name>
<message>
<location filename="../qml/TZStickyWindow.qml" line="324"/>
<location filename="../qml/TZStickyWindow.qml" line="325"/>
<source>OCR</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/TZStickyWindow.qml" line="341"/>
<source>Pencil</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/TZStickyWindow.qml" line="350"/>
<source>Rectangle</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/TZStickyWindow.qml" line="359"/>
<source>Circle</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/TZStickyWindow.qml" line="368"/>
<source>Arrow</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/TZStickyWindow.qml" line="382"/>
<location filename="../qml/TZStickyWindow.qml" line="383"/>
<source>Mosaic</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/TZStickyWindow.qml" line="404"/>
<location filename="../qml/TZStickyWindow.qml" line="539"/>
<source>缩小</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/TZStickyWindow.qml" line="406"/>
<location filename="../qml/TZStickyWindow.qml" line="540"/>
<source>放大</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/TZStickyWindow.qml" line="407"/>
<source>旋转90°</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/TZStickyWindow.qml" line="408"/>
<location filename="../qml/TZStickyWindow.qml" line="542"/>
<source>镜像</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/TZStickyWindow.qml" line="409"/>
<location filename="../qml/TZStickyWindow.qml" line="543"/>
<source>恢复原始大小</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/TZStickyWindow.qml" line="415"/>
<source>完成并复制</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/TZStickyWindow.qml" line="422"/>
<source>撤销</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/TZStickyWindow.qml" line="434"/>
<location filename="../qml/TZStickyWindow.qml" line="435"/>
<location filename="../qml/TZStickyWindow.qml" line="523"/>
<location filename="../qml/TZStickyWindow.qml" line="558"/>
<source>隐藏工具栏</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/TZStickyWindow.qml" line="515"/>
<source>当前透明度 %1%</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/TZStickyWindow.qml" line="518"/>
<location filename="../qml/TZStickyWindow.qml" line="572"/>
<source>覆盖贴图(合并标注)</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/TZStickyWindow.qml" line="519"/>
<location filename="../qml/TZStickyWindow.qml" line="573"/>
<location filename="../qml/TZStickyWindow.qml" line="580"/>
<source>复制贴图</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/TZStickyWindow.qml" line="520"/>
<location filename="../qml/TZStickyWindow.qml" line="566"/>
<location filename="../qml/TZStickyWindow.qml" line="581"/>
<source>AI 编辑</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/TZStickyWindow.qml" line="523"/>
<location filename="../qml/TZStickyWindow.qml" line="558"/>
<source>显示工具栏</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/TZStickyWindow.qml" line="530"/>
<location filename="../qml/TZStickyWindow.qml" line="583"/>
<location filename="../qml/TZStickyWindow.qml" line="588"/>
<source>关闭窗口</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/TZStickyWindow.qml" line="534"/>
<source>透明度 +5%</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/TZStickyWindow.qml" line="535"/>
<source>透明度 -5%</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/TZStickyWindow.qml" line="541"/>
<source>旋转 90°</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/TZStickyWindow.qml" line="548"/>
<source>重置位置</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/TZStickyWindow.qml" line="570"/>
<source>保存原图</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/TZStickyWindow.qml" line="571"/>
<location filename="../qml/TZStickyWindow.qml" line="602"/>
<source>另存为(合并标注)</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/TZStickyWindow.qml" line="578"/>
<source>常用项置顶</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/TZStickyWindow.qml" line="579"/>
<source>覆盖贴图</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/TZStickyWindow.qml" line="582"/>
<source>显示/隐藏工具栏</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/TZStickyWindow.qml" line="593"/>
<source>选择保存位置</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>TZTool</name>
<message>
<location filename="../qml/TZTool.qml" line="83"/>
<source>取消</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/TZTool.qml" line="76"/>
<source>完成</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/TZTool.qml" line="92"/>
<source>OCR</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/TZTool.qml" line="100"/>
<source>录制GIF</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/TZTool.qml" line="109"/>
<source>保存</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/TZTool.qml" line="116"/>
<source>贴图</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/TZTool.qml" line="127"/>
<source>矩形</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/TZTool.qml" line="136"/>
<source>圆形</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/TZTool.qml" line="145"/>
<source>箭头</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/TZTool.qml" line="154"/>
<source>画笔</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/TZTool.qml" line="163"/>
<source>高亮</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/TZTool.qml" line="172"/>
<source>马赛克</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/TZTool.qml" line="181"/>
<source>文字</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/TZTool.qml" line="190"/>
<source>序号</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/TZTool.qml" line="197"/>
<source>撤销</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>TZWindow</name>
<message>
<location filename="../qml/TZWindow.qml" line="134"/>
<source>Capture done</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/TZWindow.qml" line="134"/>
<source>Copied to clipboard</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/TZWindow.qml" line="136"/>
<source>Capture failed</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/TZWindow.qml" line="136"/>
<source>Failed to copy image</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/TZWindow.qml" line="434"/>
<source>Select save location</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/TZWindow.qml" line="442"/>
<source>Save success</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/TZWindow.qml" line="442"/>
<source>Screenshot saved to file</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/TZWindow.qml" line="445"/>
<source>Save failed</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/TZWindow.qml" line="445"/>
<source>Failed to save screenshot file</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/TZWindow.qml" line="563"/>
<source>%1 * %2</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/TZWindow.qml" line="705"/>
<source>Cancel</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/TZWindow.qml" line="727"/>
<source>OK</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>main</name>
<message>
<location filename="../qml/main.qml" line="10"/>
<source>TZshot</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/main.qml" line="199"/>
<source>OCR 识别失败</source>
<translation type="unfinished"></translation>
</message>
</context>
</TS>
+6
View File
@@ -0,0 +1,6 @@
<RCC>
<qresource prefix="/i18n">
<file>app_zh_CN.qm</file>
<file>app_en.qm</file>
</qresource>
</RCC>
+47
View File
@@ -0,0 +1,47 @@
<RCC>
<qresource prefix="/">
<file>qml/main.qml</file>
<file>qml/TZStickyWindow.qml</file>
<file>qml/TZWindow.qml</file>
<file>qml/TZScreenOverlay.qml</file>
<file>qml/TZLable.qml</file>
<file>qml/TZTool.qml</file>
<file>qml/TZResizeHandle.qml</file>
<file>qml/TZResizeHandleGroup.qml</file>
<file>qml/TZChat.qml</file>
<file>qml/Setting/TZSettingWin.qml</file>
<file>qml/Setting/TZGeneralSettings.qml</file>
<file>qml/Setting/TZRightPanel.qml</file>
<file>qml/TZControls/qmldir</file>
<file>qml/TZControls/TZComboBox.qml</file>
<file>qml/Setting/TZHotkeySettings.qml</file>
<file>qml/Setting/TZAboutWin.qml</file>
<file>resource/img/lc_check.svg</file>
<file>resource/img/lc_x.svg</file>
<file>resource/img/lc_save.svg</file>
<file>resource/img/lc_pin.svg</file>
<file>resource/img/lc_square.svg</file>
<file>resource/img/lc_pencil.svg</file>
<file>resource/img/lc_circle.svg</file>
<file>resource/img/lc_arrow.svg</file>
<file>resource/img/lc_mosaic.svg</file>
<file>resource/img/lc_text.svg</file>
<file>resource/img/lc_list_ordered.svg</file>
<file>resource/img/lc_highlighter.svg</file>
<file>resource/img/lc_undo.svg</file>
<file>resource/img/tray_icon.svg</file>
<file>qml/TZMarkToolInfo.qml</file>
<file>qml/TZMagnifyingGlass.qml</file>
<file>resource/img/lc_close.svg</file>
<file>resource/img/lc_chat.svg</file>
<file>resource/img/lc_emoji.svg</file>
<file>resource/img/lc_file.svg</file>
<file>resource/img/lc_send.svg</file>
<file>qml/TZLoadingSpinner.qml</file>
<file>qml/TZGifRecordBar.qml</file>
<file>qml/TZOcrResult.qml</file>
<file>resource/img/lc_gif_record.svg</file>
<file>resource/img/lc_gif_stop.svg</file>
<file>resource/img/lc_ocr.svg</file>
</qresource>
</RCC>
+128
View File
@@ -0,0 +1,128 @@
import QtQuick
import QtQuick.Window
import QtQuick.Layouts
Window {
id: aboutWin
width: 400
height: 350
title: qsTr("关于 TZshot")
color: "#F8FAFC"
flags: Qt.Dialog | Qt.WindowCloseButtonHint
minimumWidth: width
maximumWidth: width
minimumHeight: height
maximumHeight: height
Component.onCompleted: {
x = (Screen.width - width) / 2
y = (Screen.height - height) / 2
}
ColumnLayout {
anchors.fill: parent
anchors.margins: 32
spacing: 0
// ── Logo 区 ──────────────────────────────────
Item {
Layout.alignment: Qt.AlignHCenter
width: 72; height: 72
Rectangle {
anchors.fill: parent
radius: 18
color: "#3B82F6"
Text {
anchors.centerIn: parent
text: "T"
font.pixelSize: 40
font.bold: true
color: "#FFFFFF"
}
}
}
Item { Layout.preferredHeight: 16 }
// ── 应用名称 ──────────────────────────────────
Text {
Layout.alignment: Qt.AlignHCenter
text: "TZshot"
font.pixelSize: 22
font.bold: true
color: "#1E293B"
}
Item { Layout.preferredHeight: 6 }
// ── 版本号 ────────────────────────────────────
Text {
Layout.alignment: Qt.AlignHCenter
text: qsTr("版本 v1.0.0")
font.pixelSize: 13
color: "#64748B"
}
Item { Layout.preferredHeight: 16 }
// ── 分割线 ────────────────────────────────────
Rectangle {
Layout.fillWidth: true
height: 1
color: "#E2E8F0"
}
Item { Layout.preferredHeight: 16 }
// ── 描述 ──────────────────────────────────────
Text {
Layout.fillWidth: true
Layout.alignment: Qt.AlignHCenter
text: qsTr("简洁高效的截图贴图工具,支持标注、AI 编辑、马赛克等功能。")
font.pixelSize: 13
color: "#475569"
wrapMode: Text.WordWrap
horizontalAlignment: Text.AlignHCenter
}
Item { Layout.fillHeight: true }
// ── 版权 ──────────────────────────────────────
Text {
Layout.alignment: Qt.AlignHCenter
Layout.bottomMargin: 4
text: qsTr("© 2025 TZshot. All rights reserved.")
font.pixelSize: 11
color: "#94A3B8"
}
// ── 关闭按钮 ──────────────────────────────────
Rectangle {
Layout.alignment: Qt.AlignHCenter
width: 88; height: 34
radius: 8
color: closeMouse.containsMouse ? "#2563EB" : "#3B82F6"
Behavior on color { ColorAnimation { duration: 100 } }
Text {
anchors.centerIn: parent
text: qsTr("关闭")
font.pixelSize: 13
color: "#FFFFFF"
}
MouseArea {
id: closeMouse
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: aboutWin.close()
}
}
Item { Layout.preferredHeight: 4 }
}
}
+336
View File
@@ -0,0 +1,336 @@
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls.Basic
import TZControls 1.0
Rectangle {
color: "#F8FAFC"
ScrollView {
id: scrollView
anchors.fill: parent
contentWidth: availableWidth
clip: true
ColumnLayout {
// 必须绑定到 ScrollView.availableWidth,不能用 parent.width
// (ScrollView 内部 contentItem 宽度不受限)
x: 24
width: scrollView.availableWidth - 48
spacing: 8
Item { Layout.preferredHeight: 4 }
// ── 大模型设置 标题 ──────────────────────────
RowLayout {
Layout.fillWidth: true
Layout.topMargin: 8
spacing: 8
Rectangle {
width: 3; height: 15; radius: 2
color: "#3B82F6"
Layout.alignment: Qt.AlignVCenter
}
Text {
text: qsTr("大模型设置")
font.pixelSize: 13
font.bold: true
color: "#1E293B"
}
}
// ── 大模型设置 卡片 ──────────────────────────
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 120
color: "#FFFFFF"
radius: 10
border.color: "#E2E8F0"
border.width: 1
ColumnLayout {
anchors.fill: parent
anchors.margins: 16
spacing: 12
// 大模型选择
RowLayout {
Layout.fillWidth: true
spacing: 12
Text {
Layout.preferredWidth: 90
text: qsTr("大模型选择")
font.pixelSize: 13
color: "#374151"
Layout.alignment: Qt.AlignVCenter
}
TZComboBox {
id: combobox
model: [qsTr("阿里云百炼"), qsTr("火山引擎")]
currentIndex: O_AIViewModel.selectedModel
Layout.preferredWidth: 220
Layout.preferredHeight: 36
onCurrentIndexChanged: {
O_AIViewModel.setSelectedModel(currentIndex)
}
}
}
// API Key
RowLayout {
Layout.fillWidth: true
spacing: 12
Text {
Layout.preferredWidth: 90
text: "API Key"
font.pixelSize: 13
color: "#374151"
Layout.alignment: Qt.AlignVCenter
}
Rectangle {
Layout.preferredWidth: 320
Layout.preferredHeight: 36
radius: 6
color: apiKeyInput.activeFocus ? "#EFF6FF" : "#F8FAFC"
border.color: apiKeyInput.activeFocus ? "#3B82F6" : "#E2E8F0"
border.width: apiKeyInput.activeFocus ? 2 : 1
Behavior on border.color { ColorAnimation { duration: 150 } }
Behavior on color { ColorAnimation { duration: 150 } }
TextInput {
id: apiKeyInput
anchors.fill: parent
anchors.margins: 10
font.pixelSize: 13
color: "#1E293B"
selectByMouse: true
verticalAlignment: Text.AlignVCenter
clip: true
// 初始化时从后端读取已持久化的 apiKey
Component.onCompleted: {
text = O_AIViewModel.apiKey
}
Text {
anchors.fill: parent
verticalAlignment: Text.AlignVCenter
text: qsTr("请输入 API Key")
font.pixelSize: 13
color: "#CBD5E1"
visible: !apiKeyInput.text && !apiKeyInput.activeFocus
}
}
}
// 保存按钮
Rectangle {
id: saveApiKeyBtn
Layout.preferredWidth: 60
Layout.preferredHeight: 36
radius: 6
property bool modified: apiKeyInput.text !== O_AIViewModel.apiKey
color: {
if (!modified) return "#E2E8F0"
if (saveArea.containsMouse) return "#2563EB"
return "#3B82F6"
}
Behavior on color { ColorAnimation { duration: 120 } }
Text {
anchors.centerIn: parent
text: qsTr("保存")
font.pixelSize: 13
color: saveApiKeyBtn.modified ? "#FFFFFF" : "#94A3B8"
}
MouseArea {
id: saveArea
anchors.fill: parent
hoverEnabled: true
cursorShape: saveApiKeyBtn.modified ? Qt.PointingHandCursor : Qt.ArrowCursor
enabled: saveApiKeyBtn.modified
onClicked: {
O_AIViewModel.setApiKey(apiKeyInput.text)
}
}
}
}
Item { Layout.fillHeight: true }
}
}
Item { Layout.preferredHeight: 8 }
// ── 语言设置 标题 ────────────────────────────
RowLayout {
Layout.fillWidth: true
Layout.topMargin: 8
spacing: 8
Rectangle {
width: 3; height: 15; radius: 2
color: "#3B82F6"
Layout.alignment: Qt.AlignVCenter
}
Text {
text: qsTr("语言设置")
font.pixelSize: 13
font.bold: true
color: "#1E293B"
}
}
// ── 语言设置 卡片 ────────────────────────────
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 72
color: "#FFFFFF"
radius: 10
border.color: "#E2E8F0"
border.width: 1
RowLayout {
anchors.fill: parent
anchors.margins: 16
spacing: 12
Text {
Layout.preferredWidth: 90
text: qsTr("界面语言")
font.pixelSize: 13
color: "#374151"
Layout.alignment: Qt.AlignVCenter
}
TZComboBox {
id: langComboBox
// Index 0 = zh_CN, Index 1 = en
model: ["中文", "English"]
currentIndex: O_LanguageManager.language === "en" ? 1 : 0
Layout.preferredWidth: 160
Layout.preferredHeight: 36
onCurrentIndexChanged: {
var lang = currentIndex === 1 ? "en" : "zh_CN"
O_LanguageManager.setLanguage(lang)
}
}
}
}
Item { Layout.preferredHeight: 8 }
// ── 截图保存路径 标题 ────────────────────────
RowLayout {
Layout.fillWidth: true
Layout.topMargin: 8
spacing: 8
Rectangle {
width: 3; height: 15; radius: 2
color: "#3B82F6"
Layout.alignment: Qt.AlignVCenter
}
Text {
text: qsTr("截图保存路径")
font.pixelSize: 13
font.bold: true
color: "#1E293B"
}
}
// ── 截图保存路径 卡片 ────────────────────────
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 72
color: "#FFFFFF"
radius: 10
border.color: "#E2E8F0"
border.width: 1
RowLayout {
anchors.fill: parent
anchors.margins: 16
spacing: 12
Text {
Layout.preferredWidth: 90
text: qsTr("保存路径")
font.pixelSize: 13
color: "#374151"
Layout.alignment: Qt.AlignVCenter
}
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 36
radius: 6
color: "#F8FAFC"
border.color: "#E2E8F0"
border.width: 1
Text {
id: saveFolder
anchors.fill: parent
anchors.margins: 10
font.pixelSize: 13
color: "#374151"
text: O_ImageSaver.savePath
elide: Text.ElideMiddle
verticalAlignment: Text.AlignVCenter
clip: true
wrapMode: Text.NoWrap
ToolTip {
id: folderTip
visible: false
text: saveFolder.text
delay: 500
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
onEntered: folderTip.visible = true
onExited: folderTip.visible = false
}
}
}
// 浏览按钮
Rectangle {
Layout.preferredWidth: 72
Layout.preferredHeight: 36
radius: 6
color: browseArea.containsMouse ? "#2563EB" : "#3B82F6"
Behavior on color { ColorAnimation { duration: 120 } }
Text {
anchors.centerIn: parent
text: qsTr("浏览")
font.pixelSize: 13
color: "#FFFFFF"
}
MouseArea {
id: browseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: saveDialog.open()
}
}
}
}
Item { Layout.fillHeight: true }
}
}
}
+373
View File
@@ -0,0 +1,373 @@
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls.Basic
Rectangle {
color: "#F8FAFC"
// ── 按键录制弹窗 ──────────────────────────────────────
Rectangle {
id: recordOverlay
anchors.fill: parent
color: "#80000000"
visible: false
z: 100
property int recordingActionId: 0
property string recordingLabel: ""
property string pendingKeySeq: ""
// 捕获键盘焦点以接收按键事件
focus: visible
Keys.onPressed: function(event) {
event.accepted = true
var seq = O_GlobalShortcut.buildKeySequence(event.key, event.modifiers)
if (seq !== "") {
recordOverlay.pendingKeySeq = seq
}
}
// 弹窗卡片
Rectangle {
anchors.centerIn: parent
width: 360
height: 220
radius: 12
color: "#FFFFFF"
border.color: "#E2E8F0"
border.width: 1
ColumnLayout {
anchors.fill: parent
anchors.margins: 24
spacing: 16
// 标题
Text {
Layout.fillWidth: true
text: qsTr("编辑快捷键 · %1").arg(recordOverlay.recordingLabel)
font.pixelSize: 14
font.bold: true
color: "#1E293B"
}
// 录制区域
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 56
radius: 8
color: "#F1F5F9"
border.color: recordOverlay.pendingKeySeq !== "" ? "#3B82F6" : "#CBD5E1"
border.width: recordOverlay.pendingKeySeq !== "" ? 2 : 1
Behavior on border.color { ColorAnimation { duration: 120 } }
Text {
anchors.centerIn: parent
text: recordOverlay.pendingKeySeq !== ""
? recordOverlay.pendingKeySeq
: qsTr("请按下新的快捷键组合...")
font.pixelSize: 15
font.family: "Monospace"
color: recordOverlay.pendingKeySeq !== "" ? "#1E293B" : "#94A3B8"
}
}
// 冲突提示
Text {
id: conflictHint
Layout.fillWidth: true
font.pixelSize: 12
color: "#EF4444"
visible: false
wrapMode: Text.WordWrap
}
// 按钮行
RowLayout {
Layout.fillWidth: true
spacing: 8
// 取消
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 36
radius: 6
color: cancelArea.containsMouse ? "#F1F5F9" : "#F8FAFC"
border.color: "#E2E8F0"
Behavior on color { ColorAnimation { duration: 100 } }
Text {
anchors.centerIn: parent
text: qsTr("取消")
font.pixelSize: 13
color: "#64748B"
}
MouseArea {
id: cancelArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
recordOverlay.visible = false
recordOverlay.pendingKeySeq = ""
conflictHint.visible = false
}
}
}
// 确认
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 36
radius: 6
color: {
if (recordOverlay.pendingKeySeq === "") return "#93C5FD"
return confirmArea.containsMouse ? "#2563EB" : "#3B82F6"
}
Behavior on color { ColorAnimation { duration: 100 } }
Text {
anchors.centerIn: parent
text: qsTr("确认")
font.pixelSize: 13
color: "#FFFFFF"
}
MouseArea {
id: confirmArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
enabled: recordOverlay.pendingKeySeq !== ""
onClicked: {
var seq = recordOverlay.pendingKeySeq
var aid = recordOverlay.recordingActionId
// 检查冲突
var conflictId = O_GlobalShortcut.checkConflict(seq)
if (conflictId !== 0 && conflictId !== aid) {
var labels = ["", qsTr("截图"), qsTr("截图并保存"), qsTr("贴图到桌面"), qsTr("显示/隐藏窗口")]
conflictHint.text = "「" + seq + "」已被「" + labels[conflictId] + "」使用,请换一个"
conflictHint.visible = true
return
}
// 应用
if (O_GlobalShortcut.updateShortcut(aid, seq)) {
recordOverlay.visible = false
recordOverlay.pendingKeySeq = ""
conflictHint.visible = false
} else {
conflictHint.text = "快捷键「" + seq + "」注册失败,请换一个组合"
conflictHint.visible = true
}
}
}
}
}
}
}
}
// ── 主内容区 ──────────────────────────────────────────
ScrollView {
anchors.fill: parent
contentWidth: availableWidth
clip: true
ColumnLayout {
width: parent.width
spacing: 8
anchors.left: parent.left
anchors.right: parent.right
anchors.margins: 24
anchors.topMargin: 24
Item { Layout.preferredHeight: 4 }
// ── 标题 ────────────────────────────────────
RowLayout {
Layout.fillWidth: true
Layout.topMargin: 8
spacing: 8
Rectangle {
width: 3; height: 15; radius: 2
color: "#3B82F6"
Layout.alignment: Qt.AlignVCenter
}
Text {
text: qsTr("全局快捷键")
font.pixelSize: 13
font.bold: true
color: "#1E293B"
}
}
// ── 快捷键列表卡片 ───────────────────────────
Rectangle {
Layout.fillWidth: true
implicitHeight: hotkeyCol.implicitHeight + 32
color: "#FFFFFF"
radius: 10
border.color: "#E2E8F0"
border.width: 1
ColumnLayout {
id: hotkeyCol
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.margins: 16
spacing: 0
Repeater {
// 动态从后端读取当前快捷键
model: [
{ label: qsTr("截图"), actionId: 1, keySeq: O_GlobalShortcut.shortcutScreenshot },
{ label: qsTr("截图并保存"), actionId: 2, keySeq: O_GlobalShortcut.shortcutScreenshotSave },
{ label: qsTr("贴图到桌面"), actionId: 3, keySeq: O_GlobalShortcut.shortcutSticky },
{ label: qsTr("显示/隐藏窗口"), actionId: 4, keySeq: O_GlobalShortcut.shortcutToggle }
]
delegate: ColumnLayout {
Layout.fillWidth: true
spacing: 0
RowLayout {
Layout.fillWidth: true
Layout.topMargin: 12
Layout.bottomMargin: 12
// 功能名称
Text {
Layout.preferredWidth: 120
text: modelData.label
font.pixelSize: 13
color: "#374151"
Layout.alignment: Qt.AlignVCenter
}
// 快捷键徽章
RowLayout {
spacing: 4
Layout.alignment: Qt.AlignVCenter
Repeater {
model: modelData.keySeq.split("+")
delegate: Rectangle {
implicitWidth: kbLabel.implicitWidth + 16
implicitHeight: 28
radius: 5
color: "#F1F5F9"
border.color: "#CBD5E1"
border.width: 1
Text {
id: kbLabel
anchors.centerIn: parent
text: modelData
font.pixelSize: 12
font.family: "Monospace"
color: "#374151"
}
}
}
}
Item { Layout.fillWidth: true }
// 编辑按钮
Rectangle {
implicitWidth: 60
implicitHeight: 28
radius: 5
color: editArea.containsMouse ? "#EFF6FF" : "#F8FAFC"
border.color: editArea.containsMouse ? "#3B82F6" : "#E2E8F0"
border.width: 1
Behavior on color { ColorAnimation { duration: 100 } }
Behavior on border.color{ ColorAnimation { duration: 100 } }
Text {
anchors.centerIn: parent
text: qsTr("编辑")
font.pixelSize: 12
color: editArea.containsMouse ? "#3B82F6" : "#64748B"
Behavior on color { ColorAnimation { duration: 100 } }
}
MouseArea {
id: editArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
recordOverlay.recordingActionId = modelData.actionId
recordOverlay.recordingLabel = modelData.label
recordOverlay.pendingKeySeq = ""
conflictHint.visible = false
recordOverlay.visible = true
recordOverlay.forceActiveFocus()
}
}
}
}
// 分割线(最后一行不显示)
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 1
color: "#F1F5F9"
visible: index < 3
}
}
}
}
}
Item { Layout.preferredHeight: 8 }
// ── 说明 ────────────────────────────────────
RowLayout {
Layout.fillWidth: true
Layout.topMargin: 8
spacing: 8
Rectangle {
width: 3; height: 15; radius: 2
color: "#3B82F6"
Layout.alignment: Qt.AlignVCenter
}
Text {
text: qsTr("说明")
font.pixelSize: 13
font.bold: true
color: "#1E293B"
}
}
Rectangle {
Layout.fillWidth: true
implicitHeight: noteText.implicitHeight + 24
color: "#FFFFFF"
radius: 10
border.color: "#E2E8F0"
border.width: 1
Text {
id: noteText
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.margins: 16
text: qsTr("快捷键在全局范围内生效。点击「编辑」后直接按下新的组合键,再点「确认」即可保存。\n若与系统或其他应用快捷键冲突,请修改为其他组合。")
font.pixelSize: 13
color: "#64748B"
wrapMode: Text.WordWrap
lineHeight: 1.6
}
}
Item { Layout.fillHeight: true }
}
}
}
+166
View File
@@ -0,0 +1,166 @@
import QtQuick
import QtQuick.Layouts
Rectangle {
signal signalSetting()
signal signalHotkeySettings()
property string selectName: "setting"
color: "#F1F5F9"
ColumnLayout {
anchors.fill: parent
spacing: 0
// 顶部 Logo / 标题区
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 72
color: "transparent"
ColumnLayout {
anchors.centerIn: parent
spacing: 4
Rectangle {
width: 36
height: 36
radius: 10
color: "#3B82F6"
Layout.alignment: Qt.AlignHCenter
Text {
anchors.centerIn: parent
text: "T"
font.pixelSize: 20
font.bold: true
color: "#FFFFFF"
}
}
Text {
Layout.alignment: Qt.AlignHCenter
text: "TZshot"
font.pixelSize: 12
color: "#64748B"
}
}
}
// 分割线
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 1
Layout.leftMargin: 16
Layout.rightMargin: 16
color: "#E2E8F0"
}
Item { Layout.preferredHeight: 12 }
// ── 通用设置 菜单项 ──────────────────────
Item {
Layout.fillWidth: true
Layout.preferredHeight: 44
Layout.leftMargin: 10
Layout.rightMargin: 10
Rectangle {
anchors.fill: parent
radius: 8
color: selectName === "setting" ? "#FFFFFF" : "transparent"
border.color: selectName === "setting" ? "#E2E8F0" : "transparent"
border.width: 1
}
Rectangle {
width: 3
height: 20
radius: 2
anchors.left: parent.left
anchors.leftMargin: 6
anchors.verticalCenter: parent.verticalCenter
color: selectName === "setting" ? "#3B82F6" : "transparent"
}
Text {
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
anchors.leftMargin: 18
text: qsTr("通用设置")
font.pixelSize: 13
font.bold: selectName === "setting"
color: selectName === "setting" ? "#3B82F6" : "#64748B"
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
selectName = "setting"
signalSetting()
}
}
}
Item { Layout.preferredHeight: 4 }
// ── 快捷键设置 菜单项 ────────────────────
Item {
Layout.fillWidth: true
Layout.preferredHeight: 44
Layout.leftMargin: 10
Layout.rightMargin: 10
Rectangle {
anchors.fill: parent
radius: 8
color: selectName === "shortcutKey" ? "#FFFFFF" : "transparent"
border.color: selectName === "shortcutKey" ? "#E2E8F0" : "transparent"
border.width: 1
}
Rectangle {
width: 3
height: 20
radius: 2
anchors.left: parent.left
anchors.leftMargin: 6
anchors.verticalCenter: parent.verticalCenter
color: selectName === "shortcutKey" ? "#3B82F6" : "transparent"
}
Text {
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
anchors.leftMargin: 18
text: qsTr("快捷键设置")
font.pixelSize: 13
font.bold: selectName === "shortcutKey"
color: selectName === "shortcutKey" ? "#3B82F6" : "#64748B"
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
selectName = "shortcutKey"
signalHotkeySettings()
}
}
}
// 底部弹性空间
Item { Layout.fillHeight: true }
// 版本号
Text {
Layout.alignment: Qt.AlignHCenter
Layout.bottomMargin: 12
text: "v1.0.0"
font.pixelSize: 11
color: "#CBD5E1"
}
}
}
+77
View File
@@ -0,0 +1,77 @@
import QtQuick
import QtQuick.Window
import QtQuick.Layouts
import QtQuick.Controls.Basic
import QtQuick.Dialogs
import TZControls 1.0
Window {
width: 750
height: 520
title: qsTr("设置")
visible: true
color: "#F8FAFC"
property string currentPage: "general"
Component.onCompleted: centerWindow()
onScreenChanged: centerWindow()
function centerWindow() {
if (Screen) {
const screen = this.screen || Screen.primaryScreen
x = (screen.width - width) / 2
y = (screen.height - height) / 2
}
}
FolderDialog {
id: saveDialog
title: qsTr("选择保存位置")
currentFolder: O_ImageSaver.savePath
onAccepted: {
O_ImageSaver.setSavePath(saveDialog.selectedFolder)
}
}
RowLayout {
anchors.fill: parent
spacing: 0
// 左侧导航栏
TZRightPanel {
Layout.preferredWidth: 160
Layout.fillHeight: true
onSignalSetting: {
currentPage = "general"
}
onSignalHotkeySettings: {
currentPage = "hotkey"
}
}
// 分割线
Rectangle {
Layout.preferredWidth: 1
Layout.fillHeight: true
color: "#E2E8F0"
}
// 右侧内容区
StackLayout {
Layout.fillHeight: true
Layout.fillWidth: true
currentIndex: currentPage === "general" ? 0 : 1
TZGeneralSettings {
Layout.fillHeight: true
Layout.fillWidth: true
}
TZHotkeySettings {
Layout.fillHeight: true
Layout.fillWidth: true
}
}
}
}
+264
View File
@@ -0,0 +1,264 @@
import QtQuick
import QtQuick.Window
import QtQuick.Layouts
import QtQuick.Controls.Basic
QtObject {
id: chatPanel
// 对应贴图的图片 URL,由外部绑定
property string imageUrl: ""
signal signalSendPrompt(var text)
// 公开方法
function open(px, py) {
win.x = px
win.y = py
win.visible = true
win.raise()
win.requestActivate()
}
function close() {
win.visible = false
}
// ── 内部 Window ─────────────────────────────────────
property var win: Window {
id: win
flags: Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint | Qt.Tool
color: "transparent"
width: 420
height: 380
visible: false
Rectangle {
anchors.fill: parent
color: "#ffffff"
radius: 10
border.color: "#E2E8F0"
border.width: 1
ColumnLayout {
anchors.fill: parent
anchors.margins: 12
spacing: 8
// ── 顶部标题栏 ────────────────────────────────────
Rectangle {
Layout.fillWidth: true
height: 36
color: "transparent"
// 拖动区域(z:-1 让关闭按钮的点击不被拦截)
MouseArea {
id: dragHandle
anchors.fill: parent
z: -1
property point startPos
onPressed: startPos = Qt.point(mouseX, mouseY)
onPositionChanged: {
if (pressed) {
win.x += mouseX - startPos.x
win.y += mouseY - startPos.y
}
}
cursorShape: pressed ? Qt.ClosedHandCursor : Qt.OpenHandCursor
}
RowLayout {
anchors.fill: parent
spacing: 8
Image {
source: "qrc:/resource/img/lc_chat.svg"
width: 20
height: 20
fillMode: Image.PreserveAspectFit
}
Text {
text: qsTr("AI 图像编辑")
font.pixelSize: 14
font.weight: Font.Medium
color: "#1E293B"
}
Item { Layout.fillWidth: true }
// 关闭按钮
Rectangle {
width: 28
height: 28
radius: 6
color: clearMouse.containsMouse ? "#FEE2E2" : "transparent"
Image {
anchors.centerIn: parent
source: "qrc:/resource/img/lc_close.svg"
width: 14
height: 14
fillMode: Image.PreserveAspectFit
}
MouseArea {
id: clearMouse
anchors.fill: parent
hoverEnabled: true
onClicked: chatPanel.close()
}
}
}
// 标题栏底部分割线
Rectangle {
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
height: 1
color: "#E2E8F0"
}
}
// ── 贴图缩略图预览 ────────────────────────────────
Rectangle {
Layout.fillWidth: true
height: 64
radius: 6
color: "#F1F5F9"
border.color: "#E2E8F0"
border.width: 1
clip: true
RowLayout {
anchors.fill: parent
anchors.margins: 8
spacing: 10
// 缩略图
Rectangle {
width: 48
height: 48
radius: 4
color: "#E2E8F0"
clip: true
Image {
anchors.fill: parent
source: chatPanel.imageUrl
fillMode: Image.PreserveAspectCrop
cache: false
}
}
// 文字说明
Column {
Layout.fillWidth: true
spacing: 3
Text {
text: qsTr("当前贴图")
font.pixelSize: 11
color: "#94A3B8"
}
Text {
width: parent.width
text: chatPanel.imageUrl !== ""
? chatPanel.imageUrl.replace(/^image:\/\/[^/]+\//, "ID: ")
: qsTr("未关联贴图")
font.pixelSize: 12
color: "#475569"
elide: Text.ElideRight
}
Text {
text: qsTr("发送后将对此图执行 AI 编辑")
font.pixelSize: 10
color: "#94A3B8"
}
}
}
}
// ── 输入区域 ──────────────────────────────────────
Rectangle {
Layout.fillWidth: true
Layout.fillHeight: true
color: "#F8FAFC"
radius: 8
border.color: "#E2E8F0"
border.width: 1
TextArea {
id: input
anchors.fill: parent
anchors.margins: 12
font.family: "Segoe UI"
font.pixelSize: 13
color: "#1E293B"
wrapMode: Text.Wrap
placeholderText: qsTr("描述您想要的修改...")
background: Rectangle { color: "transparent" }
selectByMouse: true
focus: true
}
}
// ── 底部工具栏 ────────────────────────────────────
Rectangle {
Layout.fillWidth: true
height: 40
color: "transparent"
RowLayout {
anchors.fill: parent
spacing: 8
Item { Layout.fillWidth: true }
// 取消按钮
Rectangle {
width: 70; height: 32; radius: 6
color: cancelMouse.containsMouse ? "#F1F5F9" : "#F8FAFC"
border.color: "#E2E8F0"; border.width: 1
Text { anchors.centerIn: parent; text: qsTr("取消"); font.pixelSize: 13; color: "#64748B" }
MouseArea {
id: cancelMouse
anchors.fill: parent; hoverEnabled: true
onClicked: chatPanel.close()
}
}
// 发送按钮
Rectangle {
width: 70; height: 32; radius: 6
color: sendMouse.pressed ? "#0066cc" : (sendMouse.containsMouse ? "#3399ff" : "#0088ff")
RowLayout {
anchors.centerIn: parent; spacing: 4
Image { source: "qrc:/resource/img/lc_send.svg"; width: 14; height: 14 }
Text { text: qsTr("发送"); font.pixelSize: 13; color: "white" }
}
MouseArea {
id: sendMouse
anchors.fill: parent; hoverEnabled: true
onClicked: {
if (input.text.trim() !== "") {
chatPanel.signalSendPrompt(input.text)
chatPanel.close()
}
}
}
}
}
}
}
}
}
}
+128
View File
@@ -0,0 +1,128 @@
import QtQuick
import QtQuick.Controls.Basic
ComboBox {
id: control
property color fillColor: "#F8FAFC"
property color borderColor: "#E2E8F0"
property color activeBorderColor: "#3B82F6"
property color hoverColor: "#F1F5F9"
property color textColor: "#1E293B"
property color dimTextColor: "#64748B"
width: 120
height: 36
font.pixelSize: 13
font.family: "Microsoft YaHei"
// 当前选中文字
contentItem: Text {
leftPadding: 12
rightPadding: 28
text: control.displayText
font: control.font
color: control.enabled ? textColor : dimTextColor
horizontalAlignment: Text.AlignLeft
verticalAlignment: Text.AlignVCenter
elide: Text.ElideRight
}
// 背景
background: Rectangle {
implicitWidth: control.width
implicitHeight: control.height
color: control.hovered ? hoverColor : fillColor
border.color: control.activeFocus ? activeBorderColor : borderColor
border.width: control.activeFocus ? 2 : 1
radius: 6
Behavior on border.color { ColorAnimation { duration: 150 } }
Behavior on color { ColorAnimation { duration: 120 } }
}
// 下拉箭头
indicator: Canvas {
id: canvas
x: control.width - width - 10
y: (control.height - height) / 2
width: 10
height: 6
contextType: "2d"
onPaint: {
context.reset()
context.lineWidth = 1.5
context.strokeStyle = dimTextColor
context.lineCap = "round"
context.lineJoin = "round"
context.moveTo(0, 0)
context.lineTo(width / 2, height)
context.lineTo(width, 0)
context.stroke()
}
}
// 下拉列表每项
delegate: ItemDelegate {
width: control.width
height: 36
contentItem: Text {
leftPadding: 12
text: modelData
font: control.font
color: highlighted ? "#3B82F6" : textColor
verticalAlignment: Text.AlignVCenter
elide: Text.ElideRight
}
background: Rectangle {
color: highlighted ? "#EFF6FF" : fillColor
}
}
// 弹出框
popup: Popup {
y: control.height + 4
width: control.width
implicitHeight: listview.contentHeight
padding: 4
contentItem: ListView {
id: listview
clip: true
model: control.popup.visible ? control.delegateModel : null
currentIndex: control.highlightedIndex
ScrollIndicator.vertical: ScrollIndicator {}
}
background: Rectangle {
color: "#FFFFFF"
border.color: "#E2E8F0"
border.width: 1
radius: 8
// 顶部细线装饰
Rectangle {
width: parent.width - 2
height: 1
anchors.top: parent.top
anchors.topMargin: 1
anchors.horizontalCenter: parent.horizontalCenter
color: "#3B82F6"
opacity: 0.4
}
}
enter: Transition {
NumberAnimation { property: "opacity"; from: 0.0; to: 1.0; duration: 120 }
NumberAnimation { property: "scale"; from: 0.95; to: 1.0; duration: 120 }
}
exit: Transition {
NumberAnimation { property: "opacity"; from: 1.0; to: 0.0; duration: 80 }
}
}
}
+2
View File
@@ -0,0 +1,2 @@
module TZControls
TZComboBox 1.0 TZComboBox.qml
+194
View File
@@ -0,0 +1,194 @@
import QtQuick 2.12
import QtQuick.Layouts 1.12
import QtQuick.Controls 2.5
// GIF 录制控制栏:在截图选区确定后显示
Rectangle {
id: gifBar
radius: 10
color: "#FFFFFF"
border.color: "#E2E8F0"
border.width: 1
layer.enabled: true
// 外部传入的录制区域
property rect captureRect: Qt.rect(0, 0, 0, 0)
signal signalCancel
signal signalRecordFinished
readonly property bool isRecording: O_GifRecordVM.isRecording
readonly property bool isEncoding: O_GifRecordVM.isEncoding
readonly property int elapsedSecs: O_GifRecordVM.elapsedSecs
readonly property int frameCount: O_GifRecordVM.frameCount
Connections {
target: O_GifRecordVM
function onEncodingFinished(savedPath) {
gifBar.signalRecordFinished()
}
function onEncodingFailed(error) {
console.warn("[GifRecordBar] 录制失败:", error)
gifBar.signalRecordFinished()
}
}
SequentialAnimation {
id: blinkAnim
running: gifBar.isRecording
loops: Animation.Infinite
PropertyAnimation { target: recDot; property: "opacity"; to: 0.2; duration: 500 }
PropertyAnimation { target: recDot; property: "opacity"; to: 1.0; duration: 500 }
}
RowLayout {
anchors.fill: parent
anchors.margins: 6
layoutDirection: Qt.RightToLeft
spacing: 4
// 取消按钮
Rectangle {
width: 32; height: 32; radius: 7
visible: !gifBar.isEncoding
color: cancelArea.containsMouse ? "#FEE2E2" : "transparent"
Behavior on color { ColorAnimation { duration: 100 } }
Image {
anchors.centerIn: parent
source: "qrc:/resource/img/lc_x.svg"
width: 18; height: 18
fillMode: Image.PreserveAspectFit
smooth: true
}
MouseArea {
id: cancelArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onPressed: {
O_GifRecordVM.cancelRecording()
gifBar.signalCancel()
}
}
}
// 停止录制按钮
Rectangle {
width: 32; height: 32; radius: 7
visible: gifBar.isRecording && !gifBar.isEncoding
color: stopArea.containsMouse ? "#FEF3C7" : "transparent"
Behavior on color { ColorAnimation { duration: 100 } }
Image {
anchors.centerIn: parent
source: "qrc:/resource/img/lc_gif_stop.svg"
width: 18; height: 18
fillMode: Image.PreserveAspectFit
smooth: true
layer.enabled: true
layer.effect: null
}
MouseArea {
id: stopArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onPressed: O_GifRecordVM.stopRecording()
}
}
Rectangle {
width: 1; height: 20
visible: gifBar.isRecording && !gifBar.isEncoding
color: "#E2E8F0"
Layout.alignment: Qt.AlignVCenter
}
// 录制中:时间 + 帧数
Item {
visible: gifBar.isRecording && !gifBar.isEncoding
Layout.fillWidth: true
height: 32
implicitWidth: recRow.implicitWidth + 8
Row {
id: recRow
anchors.centerIn: parent
spacing: 6
Rectangle {
id: recDot
width: 8; height: 8; radius: 4
color: "#EF4444"
anchors.verticalCenter: parent.verticalCenter
}
Text {
text: {
var m = Math.floor(gifBar.elapsedSecs / 60)
var s = gifBar.elapsedSecs % 60
return (m < 10 ? "0" + m : m) + ":" + (s < 10 ? "0" + s : s)
}
font.pixelSize: 13
font.family: "monospace"
color: "#1E293B"
anchors.verticalCenter: parent.verticalCenter
}
Text {
text: gifBar.frameCount + "f"
font.pixelSize: 11
color: "#94A3B8"
anchors.verticalCenter: parent.verticalCenter
}
}
}
// 编码中:进度提示
Item {
visible: gifBar.isEncoding
Layout.fillWidth: true
height: 32
implicitWidth: encodingRow.implicitWidth + 16
Row {
id: encodingRow
anchors.centerIn: parent
spacing: 8
Rectangle {
width: 16; height: 16
color: "transparent"
anchors.verticalCenter: parent.verticalCenter
Rectangle {
width: 16; height: 16
radius: 8
color: "transparent"
border.color: "#3B82F6"
border.width: 2
Rectangle {
width: 6; height: 6
color: "#3B82F6"
radius: 3
x: 5; y: -1
}
RotationAnimation on rotation {
running: gifBar.isEncoding
from: 0; to: 360
duration: 800
loops: Animation.Infinite
}
}
}
Text {
text: qsTr("正在生成 GIF...")
font.pixelSize: 13
color: "#1E293B"
anchors.verticalCenter: parent.verticalCenter
}
}
}
}
}
+26
View File
@@ -0,0 +1,26 @@
import QtQuick
// 显示当前截图大小
Rectangle {
property string lableText: ""
radius: 6
color: "#1E293B"
opacity: 0.85
border.color: "#334155"
border.width: 1
implicitWidth: labelContent.implicitWidth + 16
implicitHeight: labelContent.implicitHeight + 8
Text {
id: labelContent
anchors.centerIn: parent
font.pixelSize: 12
font.family: "Microsoft YaHei"
color: "#F1F5F9"
text: lableText
renderType: Text.NativeRendering
}
}
+52
View File
@@ -0,0 +1,52 @@
import QtQuick
// ── AI 加载动画层 ─────────────────────────────────────
Rectangle {
width: 100
height: 100
radius: 2
color: "#99000000" // 半透明黑色遮罩
// 旋转圆弧
Item {
id: spinnerRoot
anchors.centerIn: parent
width: 48
height: 48
RotationAnimator {
target: spinnerRoot
from: 0; to: 360
duration: 900
loops: Animation.Infinite
running: O_AIViewModel.isLoading
}
// 四个小圆点模拟旋转圆弧
Repeater {
model: 8
Rectangle {
width: 6
height: 6
radius: 3
color: "white"
opacity: (index + 1) / 8
x: spinnerRoot.width / 2 + 18 * Math.cos(index * Math.PI / 4) - width / 2
y: spinnerRoot.height / 2 + 18 * Math.sin(index * Math.PI / 4) - height / 2
}
}
}
// 提示文字
Text {
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: spinnerRoot.bottom
anchors.topMargin: 10
text: "AI 生成中..."
color: "white"
font.pixelSize: 13
}
}
+114
View File
@@ -0,0 +1,114 @@
import QtQuick
import QtQuick.Window
// ── 放大镜 ───────────────────────────────────────────────
Rectangle {
id: magnifier
width: 130
height: 170 // 额外 40px 供颜色信息显示
z: 10
color: "transparent"
visible: root.visibility === Window.FullScreen
// 跟随鼠标移动,始终显示(不随截图状态切换)
property int magnifierMouseX: 0
property int magnifierMouseY: 0
property color magnifierPixelColor: "#000000"
// 跟随鼠标,偏移 +15px,防止被遮挡;超出屏幕时翻转
x: {
var px = magnifierMouseX + 15
if (px + width > root.width) px = magnifierMouseX - width - 15
return px
}
y: {
var py = magnifierMouseY + 15
if (py + height > root.height) py = magnifierMouseY - height - 15
return py
}
// 外框
Rectangle {
id: magnifierFrame
anchors.top: parent.top
width: 130
height: 130
radius: 4
color: "#1a1a1a"
border.color: "#ffffff"
border.width: 1
clip: true
// 放大图像(来自 ImageProvider)
Image {
id: magnifierImage
anchors.centerIn: parent
width: 110
height: 110
// cache: false 确保每次坐标变化时重新请求图像
cache: false
// source 用时间戳 +坐标,强制 QML 重新加载
source: root.visibility === Window.FullScreen
? ("image://magnifier/" + magnifierMouseX + "," + magnifierMouseY + "?t=" + Date.now())
: ""
fillMode: Image.Stretch
}
// 中心像素颜色色块(8×8,叠在图像中心上方,不遮挡准线)
Rectangle {
anchors.centerIn: parent
width: 8
height: 8
color: magnifierPixelColor
border.color: "#ffffff"
border.width: 1
visible: false // 色块已在 RGB 文本行体现,避免遮挡准线
}
}
// 颜色信息栏
Rectangle {
id: colorInfoBar
anchors.top: magnifierFrame.bottom
anchors.topMargin: 2
width: 130
height: 38
radius: 4
color: "#cc1a1a1a"
border.color: "#555555"
border.width: 1
Row {
anchors.centerIn: parent
spacing: 6
// 色块
Rectangle {
width: 16
height: 16
radius: 2
color: magnifierPixelColor
border.color: "#ffffff"
border.width: 1
anchors.verticalCenter: parent.verticalCenter
}
// RGB 文本
Text {
id: rgbText
anchors.verticalCenter: parent.verticalCenter
color: "#ffffff"
font.pixelSize: 11
font.family: "monospace"
text: {
var c = magnifierPixelColor
var r = Math.round(c.r * 255)
var g = Math.round(c.g * 255)
var b = Math.round(c.b * 255)
return "R:" + r + " G:" + g + " B:" + b
}
}
}
}
}
// ── 放大镜 end ───────────────────────────────────────────
+257
View File
@@ -0,0 +1,257 @@
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
Rectangle {
id: panel
property color currentColor: "#F43F5E"
property int currentSize: 6
property bool showColorRow: true
property string toolName: ""
property string annotationText: qsTr("文本")
property bool textBackgroundEnabled: true
property int mosaicBlurLevel: 2
property bool numberAutoIncrement: true
property int numberValue: 1
width: 260
height: toolName === "text" ? 128
: (toolName === "number" ? 132
: ((toolName === "mosaic") ? 92 : (showColorRow ? 84 : 50)))
radius: 10
color: "#FFFFFF"
border.color: "#E2E8F0"
border.width: 1
ColumnLayout {
anchors.fill: parent
anchors.margins: 10
spacing: 8
RowLayout {
spacing: 6
Layout.fillWidth: true
visible: panel.showColorRow
height: panel.showColorRow ? implicitHeight : 0
Repeater {
model: ["#F43F5E","#EF4444","#F97316","#EAB308",
"#22C55E","#3B82F6","#8B5CF6","#000000","#FFFFFF"]
delegate: Rectangle {
width: 18; height: 18; radius: 9
color: modelData
border.color: panel.currentColor === modelData ? "#3B82F6" : "#CBD5E1"
border.width: panel.currentColor === modelData ? 2 : 1
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onPressed: panel.currentColor = modelData
}
}
}
}
RowLayout {
spacing: 6
Layout.fillWidth: true
Repeater {
model: [
{ size: 2, label: qsTr("细") },
{ size: 4, label: qsTr("中") },
{ size: 6, label: qsTr("粗") },
{ size: 10, label: qsTr("特粗") }
]
delegate: Rectangle {
width: 48; height: 28
radius: 6
color: panel.currentSize === modelData.size ? "#EFF6FF" : (sizeArea.containsMouse ? "#F1F5F9" : "transparent")
border.color: panel.currentSize === modelData.size ? "#3B82F6" : "#E2E8F0"
border.width: 1
Column {
anchors.centerIn: parent
spacing: 3
Rectangle {
anchors.horizontalCenter: parent.horizontalCenter
width: 24
height: Math.min(modelData.size, 6)
radius: height / 2
color: panel.currentSize === modelData.size ? "#3B82F6" : "#94A3B8"
}
Text {
anchors.horizontalCenter: parent.horizontalCenter
text: modelData.label
font.pixelSize: 10
color: panel.currentSize === modelData.size ? "#3B82F6" : "#64748B"
}
}
MouseArea {
id: sizeArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onPressed: panel.currentSize = modelData.size
}
}
}
}
RowLayout {
visible: panel.toolName === "text"
Layout.fillWidth: true
spacing: 8
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 34
radius: 6
color: "#F8FAFC"
border.color: textInput.activeFocus ? "#3B82F6" : "#E2E8F0"
border.width: 1
TextInput {
id: textInput
anchors.fill: parent
anchors.margins: 8
verticalAlignment: TextInput.AlignVCenter
color: "#0F172A"
font.pixelSize: 13
selectByMouse: true
text: panel.annotationText
onTextChanged: panel.annotationText = text
}
Text {
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
anchors.leftMargin: 8
visible: textInput.text.length === 0 && !textInput.activeFocus
text: qsTr("输入文字")
color: "#94A3B8"
font.pixelSize: 13
}
}
CheckBox {
text: qsTr("背景")
checked: panel.textBackgroundEnabled
onToggled: panel.textBackgroundEnabled = checked
}
}
RowLayout {
visible: panel.toolName === "mosaic"
Layout.fillWidth: true
spacing: 8
Text {
text: qsTr("模糊")
color: "#334155"
}
Slider {
Layout.fillWidth: true
from: 1
to: 4
stepSize: 1
value: panel.mosaicBlurLevel
onMoved: panel.mosaicBlurLevel = Math.round(value)
}
Text {
text: panel.mosaicBlurLevel
color: "#64748B"
}
}
ColumnLayout {
visible: panel.toolName === "number"
Layout.fillWidth: true
spacing: 8
CheckBox {
text: qsTr("自动递增")
checked: panel.numberAutoIncrement
onToggled: panel.numberAutoIncrement = checked
}
RowLayout {
Layout.fillWidth: true
spacing: 8
Text {
text: qsTr("数字")
color: "#334155"
}
Rectangle {
width: 28
height: 28
radius: 6
color: minusArea.containsMouse ? "#F1F5F9" : "transparent"
border.color: "#CBD5E1"
border.width: 1
Text {
anchors.centerIn: parent
text: "-"
color: "#334155"
font.pixelSize: 16
}
MouseArea {
id: minusArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onPressed: panel.numberValue = Math.max(1, panel.numberValue - 1)
}
}
Rectangle {
Layout.fillWidth: true
height: 28
radius: 6
color: "#F8FAFC"
border.color: "#E2E8F0"
border.width: 1
Text {
anchors.centerIn: parent
text: panel.numberValue
color: "#0F172A"
font.pixelSize: 13
}
}
Rectangle {
width: 28
height: 28
radius: 6
color: plusArea.containsMouse ? "#F1F5F9" : "transparent"
border.color: "#CBD5E1"
border.width: 1
Text {
anchors.centerIn: parent
text: "+"
color: "#334155"
font.pixelSize: 16
}
MouseArea {
id: plusArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onPressed: panel.numberValue = Math.min(9999, panel.numberValue + 1)
}
}
}
}
}
}
+328
View File
@@ -0,0 +1,328 @@
import QtQuick
import QtQuick.Window
import QtQuick.Controls
import QtQuick.Layouts
// TZOcrResult
// -----------
// 无边框悬浮窗,展示 OCR 识别结果。
// 由 TZWindow.qml 通过 Qt.createComponent 动态创建并显示。
Window {
id: ocrWin
flags: Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint | Qt.Tool
color: "transparent"
width: 420
height: 320
// 初始居中(创建后由调用方设置位置,此处给默认值)
x: (Screen.width - width) / 2
y: (Screen.height - height) / 2
Component.onCompleted: {
ocrWin.raise()
ocrWin.requestActivate()
}
Shortcut {
sequence: "Esc"
onActivated: ocrWin.close()
}
// ── 阴影容器 ──────────────────────────────────────────
Rectangle {
id: shadow
anchors.fill: parent
anchors.margins: 10
radius: 12
color: "white"
layer.enabled: true
border.color: "#D1D5DB"
border.width: 1
// ── 标题栏 ─────────────────────────────────────────
Rectangle {
id: titleBar
anchors.top: shadow.top
anchors.left: shadow.left
anchors.right: shadow.right
height: 40
radius: 12
color: "#F8FAFC"
// 圆角只在上半部分(下方盖住)
Rectangle {
anchors.bottom: titleBar.bottom
anchors.left: titleBar.left
anchors.right: titleBar.right
height: titleBar.radius
color: titleBar.color
}
// 标题文字
Text {
anchors.verticalCenter: titleBar.verticalCenter
anchors.left: titleBar.left
anchors.leftMargin: 14
text: qsTr("OCR 识别结果")
font.pixelSize: 14
font.weight: Font.Medium
color: "#1E293B"
}
// 关闭按钮
Rectangle {
id: closeBtn
anchors.right: titleBar.right
anchors.rightMargin: 8
anchors.verticalCenter: titleBar.verticalCenter
width: 26; height: 26; radius: 5
color: closeArea.containsMouse ? "#FEE2E2" : "transparent"
Behavior on color { ColorAnimation { duration: 80 } }
Text {
anchors.centerIn: parent
text: "✕"
color: closeArea.containsMouse ? "#DC2626" : "#94A3B8"
font.pixelSize: 13
}
MouseArea {
id: closeArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: ocrWin.close()
}
}
// 拖动标题栏移动窗口
MouseArea {
anchors.fill: titleBar
anchors.rightMargin: 40
property point pressPos
onPressed: function(mouse) {
pressPos = Qt.point(mouse.x, mouse.y)
}
onPositionChanged: function(mouse) {
if (pressed) {
ocrWin.x += mouse.x - pressPos.x
ocrWin.y += mouse.y - pressPos.y
}
}
}
}
// ── 分割线 ─────────────────────────────────────────
Rectangle {
id: divider
anchors.top: titleBar.bottom
anchors.left: shadow.left
anchors.right: shadow.right
height: 1
color: "#E2E8F0"
}
// ── 内容区 ─────────────────────────────────────────
Item {
anchors.top: divider.bottom
anchors.left: shadow.left
anchors.right: shadow.right
anchors.bottom: bottomBar.top
// Loading 遮罩
Rectangle {
anchors.fill: parent
color: "#F8FAFC"
visible: O_OcrVM.isRecognizing
radius: 6
Item {
id: spinnerRoot
anchors.centerIn: parent
width: 48; height: 48
RotationAnimator {
target: spinnerRoot
from: 0; to: 360
duration: 900
loops: Animation.Infinite
running: O_OcrVM.isRecognizing
}
Repeater {
model: 8
Rectangle {
width: 6; height: 6; radius: 3
color: "#3B82F6"
opacity: (index + 1) / 8
x: spinnerRoot.width / 2 + 18 * Math.cos(index * Math.PI / 4) - width / 2
y: spinnerRoot.height / 2 + 18 * Math.sin(index * Math.PI / 4) - height / 2
}
}
}
Text {
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: spinnerRoot.bottom
anchors.topMargin: 12
text: qsTr("正在识别中…")
color: "#64748B"
font.pixelSize: 13
}
}
// 文本区域(识别完成后显示)
ScrollView {
anchors.fill: parent
anchors.margins: 8
visible: !O_OcrVM.isRecognizing
clip: true
TextArea {
id: resultArea
text: O_OcrVM.resultText
readOnly: false // 允许用户二次编辑
wrapMode: TextEdit.Wrap
font.pixelSize: 14
color: "#1E293B"
background: null
padding: 4
selectByMouse: true
placeholderText: qsTr("识别结果将显示在这里…")
placeholderTextColor: "#94A3B8"
}
}
}
// ── 底部操作栏 ─────────────────────────────────────
Rectangle {
id: bottomBar
anchors.bottom: shadow.bottom
anchors.left: shadow.left
anchors.right: shadow.right
height: 44
radius: 12
color: "#F8FAFC"
// 圆角只在下半部分
Rectangle {
anchors.top: bottomBar.top
anchors.left: bottomBar.left
anchors.right: bottomBar.right
height: bottomBar.radius
color: bottomBar.color
}
// 分割线
Rectangle {
anchors.top: bottomBar.top
anchors.left: bottomBar.left
anchors.right: bottomBar.right
height: 1
color: "#E2E8F0"
}
Row {
anchors.centerIn: parent
spacing: 10
// 一键复制
Rectangle {
id: copyBtn
width: 100; height: 30; radius: 6
color: copyArea.containsMouse ? "#DBEAFE" : "#EFF6FF"
border.color: "#3B82F6"
border.width: 1
Behavior on color { ColorAnimation { duration: 80 } }
Text {
anchors.centerIn: parent
text: qsTr("复制全部")
color: "#3B82F6"
font.pixelSize: 13
}
MouseArea {
id: copyArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
O_OcrVM.copyResultToClipboard()
copyFeedbackTimer.start()
}
}
}
// 关闭
Rectangle {
width: 70; height: 30; radius: 6
color: closeBtn2Area.containsMouse ? "#F1F5F9" : "transparent"
border.color: "#CBD5E1"
border.width: 1
Behavior on color { ColorAnimation { duration: 80 } }
Text {
anchors.centerIn: parent
text: qsTr("关闭")
color: "#64748B"
font.pixelSize: 13
}
MouseArea {
id: closeBtn2Area
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: ocrWin.close()
}
}
}
}
// 复制成功短暂提示
Rectangle {
id: copyFeedback
anchors.horizontalCenter: shadow.horizontalCenter
anchors.bottom: bottomBar.top
anchors.bottomMargin: 6
width: 100; height: 28; radius: 6
color: "#22C55E"
visible: false
opacity: 0
Text {
anchors.centerIn: parent
text: qsTr("已复制!")
color: "white"
font.pixelSize: 12
}
Timer {
id: copyFeedbackTimer
interval: 1200
onTriggered: copyFeedback.visible = false
}
onVisibleChanged: {
if (visible) opacity = 1
}
Behavior on opacity {
NumberAnimation { duration: 300 }
}
}
Connections {
target: copyFeedbackTimer
function onTriggered() {
copyFeedback.opacity = 0
}
}
}
// 启动计时后立即显示反馈
Connections {
target: copyArea
function onClicked() {
copyFeedback.visible = true
}
}
}
+14
View File
@@ -0,0 +1,14 @@
import QtQuick
Rectangle {
// id:leftTop
// x: screenshotArea.x - handleSize/2
// y: screenshotArea.y - handleSize/2
property int handleSize: 8
width: handleSize
height: handleSize
radius: handleSize/2
color: "#0088FF"
border.color: "white"
border.width: 1
}
+296
View File
@@ -0,0 +1,296 @@
import QtQuick
import QtQuick.Window
Rectangle{
id:handleGroup
property rect area: Qt.rect(0,0,0,0)
property rect initialArea: Qt.rect(0,0,0,0)
property var mousePointStart: Qt.point(0,0)
x:area.x
y:area.y
width: area.width
height: area.height
color: "transparent"
//左上角控制点
TZResizeHandle
{
id:leftTop
x: - handleSize/2
y: - handleSize/2
// 仅当选区宽度/高度非零时显示
visible: area.width !== 0 || area.height !== 0
MouseArea {
anchors.fill: parent
cursorShape: Qt.SizeFDiagCursor
onPressed: {
let globalPos = leftTop.mapToItem(null, mouseX, mouseY);
mousePointStart.x = globalPos.x
mousePointStart.y = globalPos.y
initialArea = area
}
onReleased: {
// 重置所有临时变量
mousePointStart = Qt.point(0,0)
initialArea = area
}
onPositionChanged: {
if (pressed) {
let globalPos = leftTop.mapToItem(null, mouseX, mouseY);
let dx = globalPos.x - mousePointStart.x
let dy = globalPos.y - mousePointStart.y
area.x = initialArea.x + dx
area.y = initialArea.y + dy
area.width = initialArea.width - dx
area.height = initialArea.height - dy
}
}
}
}
//正上方控制点
TZResizeHandle
{
id: top
x: (area.width - handleSize/2)/2
y: -handleSize/2
// 仅当选区宽度/高度非零时显示
visible: area.width !== 0 || area.height !== 0
MouseArea {
anchors.fill: parent
cursorShape: Qt.SizeVerCursor
onPressed: {
let globalPos = top.mapToItem(null, mouseX, mouseY);
mousePointStart.y = globalPos.y
initialArea = area
}
onReleased: {
// 重置所有临时变量
mousePointStart = Qt.point(0,0)
initialArea = area
}
onPositionChanged: {
if (pressed) {
let globalPos = top.mapToItem(null, mouseX, mouseY);
let dy = globalPos.y - mousePointStart.y
area.y = initialArea.y + dy
area.height = initialArea.height - dy
}
}
}
}
//右上角控制点
TZResizeHandle
{
id:rightTop
x: area.width - handleSize/2
y: -handleSize/2
visible: area.width !== 0 || area.height !== 0
MouseArea {
anchors.fill: parent
cursorShape: Qt.SizeBDiagCursor
onPressed: {
let globalPos = rightTop.mapToItem(null, mouseX, mouseY);
mousePointStart.x = globalPos.x
mousePointStart.y = globalPos.y
initialArea = area
}
onReleased: {
mousePointStart = Qt.point(0,0)
initialArea = area
}
onPositionChanged: {
if (pressed) {
let globalPos = rightTop.mapToItem(null, mouseX, mouseY);
let dx = globalPos.x - mousePointStart.x
let dy = globalPos.y - mousePointStart.y
area.y = initialArea.y + dy
area.width = initialArea.width + dx
area.height = initialArea.height - dy
}
}
}
}
//右边控制点
TZResizeHandle
{
id: right
x: area.width - handleSize/2
y: (area.height -handleSize/2)/2
visible: area.width !== 0 || area.height !== 0
MouseArea {
anchors.fill: parent
cursorShape: Qt.SizeHorCursor
onPressed: {
let globalPos = right.mapToItem(null, mouseX, mouseY);
mousePointStart.x = globalPos.x
initialArea = area
}
onReleased: {
mousePointStart = Qt.point(0,0)
initialArea = area
}
onPositionChanged: {
if (pressed) {
let globalPos = right.mapToItem(null, mouseX, mouseY);
let dx = globalPos.x - mousePointStart.x
area.width = initialArea.width + dx
}
}
}
}
//右下角控制点
TZResizeHandle
{
id:rightButtom
x: area.width - handleSize/2
y: area.height - handleSize/2
visible: area.width !== 0 || area.height !== 0
z:2
MouseArea {
anchors.fill: parent
cursorShape: Qt.SizeFDiagCursor
onPressed: {
let globalPos = rightButtom.mapToItem(null, mouseX, mouseY);
mousePointStart.x = globalPos.x
mousePointStart.y = globalPos.y
initialArea = area
}
onReleased: {
mousePointStart = Qt.point(0,0)
initialArea = area
}
onPositionChanged: {
if (pressed) {
let globalPos = rightButtom.mapToItem(null, mouseX, mouseY);
let dx = globalPos.x - mousePointStart.x
let dy = globalPos.y - mousePointStart.y
area.width = initialArea.width + dx
area.height = initialArea.height + dy
}
}
}
}
//正下方控制点
TZResizeHandle
{
id:buttom
x: (area.width - handleSize/2)/2
y: area.height - handleSize/2
visible: area.width !== 0 || area.height !== 0
z:2
MouseArea {
anchors.fill: parent
cursorShape: Qt.SizeVerCursor
onPressed: {
let globalPos = buttom.mapToItem(null, mouseX, mouseY);
mousePointStart.y = globalPos.y
initialArea = area
}
onReleased: {
mousePointStart = Qt.point(0,0)
initialArea = area
}
onPositionChanged: {
if (pressed) {
let globalPos = buttom.mapToItem(null, mouseX, mouseY);
let dy = globalPos.y - mousePointStart.y
area.height = initialArea.height + dy
}
}
}
}
//左下角控制点
TZResizeHandle
{
id:leftBottom
x: -handleSize/2
y: area.height - handleSize/2
visible: area.width !== 0 || area.height !== 0
z:2
MouseArea {
anchors.fill: parent
cursorShape: Qt.SizeBDiagCursor
onPressed: {
let globalPos = leftBottom.mapToItem(null, mouseX, mouseY);
mousePointStart.x = globalPos.x
mousePointStart.y = globalPos.y
initialArea = area
}
onReleased: {
mousePointStart = Qt.point(0,0)
initialArea = area
}
onPositionChanged: {
if (pressed) {
let globalPos = leftBottom.mapToItem(null, mouseX, mouseY);
let dx = globalPos.x - mousePointStart.x
let dy = globalPos.y - mousePointStart.y
area.x = initialArea.x + dx
area.width = initialArea.width - dx
area.height = initialArea.height + dy
}
}
}
}
//左边控制点
TZResizeHandle
{
id:left
x: -handleSize/2
y: (area.height - handleSize/2)/2
visible: area.width !== 0 || area.height !== 0
z:2
MouseArea {
anchors.fill: parent
cursorShape: Qt.SizeHorCursor
onPressed: {
let globalPos = left.mapToItem(null, mouseX, mouseY);
mousePointStart.x = globalPos.x
initialArea = area
}
onReleased: {
mousePointStart = Qt.point(0,0)
initialArea = area
}
onPositionChanged: {
if (pressed) {
let globalPos = left.mapToItem(null, mouseX, mouseY);
let dx = globalPos.x - mousePointStart.x
area.x = initialArea.x + dx
area.width = initialArea.width - dx
}
}
}
}
}
+101
View File
@@ -0,0 +1,101 @@
import QtQuick
import QtQuick.Window
import QtQuick.Shapes
// TZScreenOverlay — 辅屏截图覆盖窗口
//
// 每块非主屏创建一个此窗口,覆盖该屏全部区域。
// 职责:
// 1. 显示半透明蒙层 + 选区镂空 + 窗口识别高亮
// 2. 将本地鼠标坐标转为虚拟桌面绝对坐标,委托给主窗口(delegate)处理
//
// 注入属性:
// delegate — 主窗口(root / TZWindow),提供共享截图状态和处理函数
// screenVX — 本屏 virtualX(虚拟桌面绝对 X)
// screenVY — 本屏 virtualY(虚拟桌面绝对 Y)
Window {
id: overlay
color: "transparent"
flags: Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint
visible: delegate ? (!delegate.gifRecordMode && delegate.captureUiReady) : true
property var delegate: null
property int screenVX: 0
property int screenVY: 0
// 从 delegate 读取共享状态
property rect screenshotArea: delegate ? delegate.screenshotArea : Qt.rect(0,0,0,0)
property rect highlightRect: delegate ? delegate.highlightRect : Qt.rect(0,0,0,0)
function toAbsX(lx) { return lx + screenVX }
function toAbsY(ly) { return ly + screenVY }
// 绝对坐标 → 本窗口本地坐标(供绘制用)
readonly property rect localArea: Qt.rect(
screenshotArea.x - screenVX, screenshotArea.y - screenVY,
screenshotArea.width, screenshotArea.height)
readonly property rect localHighlight: Qt.rect(
highlightRect.x - screenVX, highlightRect.y - screenVY,
highlightRect.width, highlightRect.height)
// ── 半透明蒙层 + 选区镂空 ─────────────────────────────
Shape {
anchors.fill: parent
ShapePath {
strokeWidth: -1
fillColor: "#99000000"
startX: 0; startY: 0
PathLine { x: overlay.width; y: 0 }
PathLine { x: overlay.width; y: overlay.height }
PathLine { x: 0; y: overlay.height }
PathLine { x: 0; y: 0 }
PathMove { x: overlay.localArea.x; y: overlay.localArea.y }
PathLine { x: overlay.localArea.x + overlay.localArea.width; y: overlay.localArea.y }
PathLine { x: overlay.localArea.x + overlay.localArea.width; y: overlay.localArea.y + overlay.localArea.height }
PathLine { x: overlay.localArea.x; y: overlay.localArea.y + overlay.localArea.height }
PathLine { x: overlay.localArea.x; y: overlay.localArea.y }
}
}
// ── 窗口识别高亮框 ────────────────────────────────────
Rectangle {
visible: screenshotArea.width === 0 && screenshotArea.height === 0
&& highlightRect.width > 0 && highlightRect.height > 0
x: overlay.localHighlight.x
y: overlay.localHighlight.y
width: overlay.localHighlight.width
height: overlay.localHighlight.height
color: "transparent"
border.color: "#3B82F6"
border.width: 2
z: 5
Rectangle { anchors.fill: parent; color: "#153B82F6" }
}
// ── 鼠标输入:全部转为绝对坐标,委托给主窗口处理 ─────
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.CrossCursor
onPressed: function(mouse) {
if (!delegate) return
var ax = toAbsX(mouseX), ay = toAbsY(mouseY)
delegate.handleMousePressed(ax, ay)
}
onPositionChanged: function(mouse) {
if (!delegate) return
var ax = toAbsX(mouseX), ay = toAbsY(mouseY)
delegate.handleMouseMoved(ax, ay, pressed)
}
onReleased: function(mouse) {
if (!delegate) return
delegate.handleMouseReleased()
}
onWheel: function(wheel) { wheel.accepted = true }
}
}
+678
View File
@@ -0,0 +1,678 @@
import QtQuick
import QtQuick.Window
import Qt.labs.platform 1.0 as Platform
import QtQuick.Effects
import QtQuick.Controls.Basic
import QtCore
import CustomComponents 1.0
Window {
id: stickyWin
flags: Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint | Qt.Tool
color: "transparent"
property string imageUrl: ""
property rect imgRect: Qt.rect(0, 0, 0, 0)
property bool contentActive: true
property bool cleanedUp: false
property real imgOpacity: 1.0
property string activeTool: ""
property bool toolbarVisible: true
property string toolbarHoverTipText: ""
property real toolbarHoverTipCenterX: 0
property bool contextMenuOpen: false
property int imageRevision: 0
property int imageBaseW: Math.max(1, Math.abs(imgRect.width))
property int imageBaseH: Math.max(1, Math.abs(imgRect.height))
property real zoomFactor: 1.0
readonly property int toolbarHeight: 40
readonly property int toolbarGap: 8
readonly property int toolbarTipGap: 6
readonly property int toolbarTipHeight: 24
readonly property int shadowRadius: 12
readonly property int shadowOffset: 4
readonly property int displayW: Math.max(1, Math.round(imageBaseW * zoomFactor))
readonly property int displayH: Math.max(1, Math.round(imageBaseH * zoomFactor))
readonly property int stickyShapeType: {
switch (activeTool) {
case "pencil": return 0
case "rect": return 1
case "circle": return 2
case "arrow": return 3
case "mosaic": return 4
default: return 1
}
}
width: displayW + shadowRadius * 2
height: displayH + shadowRadius * 2 + shadowOffset + toolbarGap + toolbarHeight + toolbarTipGap + toolbarTipHeight + 4
x: (imgRect.width >= 0 ? imgRect.x : imgRect.x + imgRect.width) - shadowRadius
y: (imgRect.height >= 0 ? imgRect.y : imgRect.y + imgRect.height) - shadowRadius
Settings {
id: menuPrefs
category: "sticky_menu"
property bool pinSaveMerged: true
property bool pinCopy: true
property bool pinAi: false
property bool pinToolbar: false
property bool pinClose: false
}
function setActiveTool(name) {
if (activeTool === name)
activeTool = ""
else
activeTool = name
}
function showToolbarTip(text, item) {
toolbarHoverTipText = text
if (item) {
var p = stickyToolbar.mapFromItem(item, item.width / 2, item.height)
toolbarHoverTipCenterX = p.x
} else {
toolbarHoverTipCenterX = stickyToolbar.width / 2
}
}
function hideToolbarTip(text) {
if (text === undefined || toolbarHoverTipText === text)
toolbarHoverTipText = ""
}
function displaySourceUrl() {
if (typeof stickyWin.imageUrl !== "string" || stickyWin.imageUrl.length === 0)
return ""
return stickyWin.imageUrl + "?rev=" + stickyWin.imageRevision
}
function setOpacityValue(v) {
stickyWin.imgOpacity = Math.max(0.1, Math.min(1.0, v))
}
function setZoomValue(v) {
zoomFactor = Math.max(0.3, Math.min(4.0, v))
}
function resetDisplaySize() {
zoomFactor = 1.0
}
function updateImageMetricsFromStore() {
var img = O_StickyViewModel.getImageByUrl(stickyWin.imageUrl)
if (img && img.width > 0 && img.height > 0) {
imageBaseW = img.width
imageBaseH = img.height
stickyPaintBoard.setBackgroundImg(img)
} else {
syncPaintboardBackgroundFromDisplay()
}
}
function syncPaintboardBackgroundFromDisplay() {
if (!stickyWin.visible || !stickyWin.contentActive)
return
if (stickyImage.status !== Image.Ready)
return
stickyImage.grabToImage(function(result) {
if (!result || !result.image || result.image.width <= 0 || result.image.height <= 0)
return
if (imageBaseW <= 1 || imageBaseH <= 1) {
imageBaseW = result.image.width
imageBaseH = result.image.height
}
stickyPaintBoard.setBackgroundImg(result.image)
}, Qt.size(Math.max(1, imageBaseW), Math.max(1, imageBaseH)))
}
function refreshAfterImageChanged() {
imageRevision += 1
stickyPaintBoard.reset()
updateImageMetricsFromStore()
}
function overwriteWithEdits() {
var layer = stickyPaintBoard.renderToImage()
var ok = O_StickyViewModel.overwriteWithAnnotations(stickyWin.imageUrl, layer)
if (ok)
refreshAfterImageChanged()
}
function rotateCurrent(degrees) {
if (O_StickyViewModel.rotateImage(stickyWin.imageUrl, degrees))
refreshAfterImageChanged()
}
function mirrorCurrent() {
if (O_StickyViewModel.mirrorImage(stickyWin.imageUrl, true, false))
refreshAfterImageChanged()
}
function saveMergedTo(pathUrl) {
var layer = stickyPaintBoard.renderToImage()
O_StickyViewModel.saveMergedImage(stickyWin.imageUrl, layer, pathUrl)
}
function completeEditsAndCopy() {
var layer = stickyPaintBoard.renderToImage()
if (layer && layer.width > 0 && layer.height > 0)
overwriteWithEdits()
O_StickyViewModel.copyImageToClipboard(stickyWin.imageUrl)
stickyWin.activeTool = ""
stickyWin.toolbarVisible = false
}
function cleanup() {
if (cleanedUp)
return
cleanedUp = true
stickyWin.visible = false
contentActive = false
stickyWin.releaseResources()
O_StickyViewModel.releaseImage(imageUrl)
imageUrl = ""
}
Component.onCompleted: {
updateImageMetricsFromStore()
stickyWin.raise()
stickyWin.requestActivate()
}
Shortcut {
sequence: "Esc"
onActivated: {
if (stickyWin.activeTool !== "") {
stickyWin.activeTool = ""
return
}
if (stickyWin.toolbarVisible) {
stickyWin.toolbarVisible = false
return
}
stickyWin.close()
}
}
onImageUrlChanged: {
imageRevision = 0
resetDisplaySize()
refreshAfterImageChanged()
}
onActiveToolChanged: {
if (activeTool === "mosaic")
updateImageMetricsFromStore()
}
Component.onDestruction: cleanup()
onClosing: function(close) {
close.accepted = true
chatPanel.close()
cleanup()
Qt.callLater(function() { stickyWin.destroy() })
}
Rectangle {
id: imgContainer
visible: stickyWin.contentActive
z: 10
x: stickyWin.shadowRadius
y: stickyWin.shadowRadius + stickyWin.shadowOffset / 2
width: stickyWin.displayW
height: stickyWin.displayH
radius: 2
clip: true
color: "transparent"
Item {
id: visualStage
anchors.centerIn: parent
width: stickyWin.imageBaseW
height: stickyWin.imageBaseH
scale: stickyWin.zoomFactor
Image {
id: stickyImage
anchors.fill: parent
source: displaySourceUrl()
fillMode: Image.Stretch
opacity: stickyWin.imgOpacity
cache: false
onStatusChanged: {
if (status === Image.Ready && stickyWin.activeTool === "mosaic" && !stickyPaintBoard.hasBackgroundImg())
stickyWin.syncPaintboardBackgroundFromDisplay()
}
}
PaintBoard {
id: stickyPaintBoard
anchors.fill: parent
z: 20
visible: stickyWin.contentActive
enabled: stickyWin.activeTool !== ""
shapeType: stickyWin.stickyShapeType
penColor: stickyMarkTool.currentColor
penSize: stickyMarkTool.currentSize
mosaicBlurLevel: stickyMarkTool.mosaicBlurLevel
}
}
}
MultiEffect {
visible: stickyWin.contentActive
z: 5
source: stickyImage
anchors.fill: imgContainer
shadowEnabled: true
shadowColor: "#55000000"
shadowBlur: stickyWin.shadowRadius / 20.0
shadowVerticalOffset: stickyWin.shadowOffset
shadowHorizontalOffset: 0
opacity: stickyWin.imgOpacity
}
TZLoadingSpinner {
anchors.centerIn: imgContainer
width: stickyWin.displayW
height: stickyWin.displayH
visible: O_AIViewModel.isLoading && stickyWin.contentActive
}
TZMarkToolInfo {
id: stickyMarkTool
z: 32
visible: stickyWin.toolbarVisible && stickyWin.activeTool !== ""
toolName: stickyWin.activeTool
showColorRow: stickyWin.activeTool !== "mosaic"
x: Math.max(8, stickyToolbar.x)
y: stickyToolbar.y - height - 6
}
Rectangle {
id: stickyToolbar
z: 30
visible: stickyWin.toolbarVisible
width: Math.min(stickyWin.width - 16, Math.max(560, stickyRow.implicitWidth + 16))
height: stickyWin.toolbarHeight
radius: 10
color: "#FFFFFF"
border.color: "#E2E8F0"
border.width: 1
x: Math.max(8, Math.min(stickyWin.width - width - 8, stickyWin.shadowRadius + (stickyWin.displayW - width) / 2))
y: stickyWin.shadowRadius + stickyWin.shadowOffset / 2 + stickyWin.displayH + stickyWin.toolbarGap
Row {
id: stickyRow
anchors.centerIn: parent
spacing: 6
Rectangle {
width: 30; height: 30; radius: 7
color: ocrArea.containsMouse ? "#F0FDF4" : "transparent"
Image { anchors.centerIn: parent; source: "qrc:/resource/img/lc_ocr.svg"; width: 18; height: 18 }
MouseArea {
id: ocrArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onEntered: stickyWin.showToolbarTip(qsTr("OCR"), parent)
onExited: stickyWin.hideToolbarTip(qsTr("OCR"))
onPressed: {
var img = O_StickyViewModel.getImageByUrl(stickyWin.imageUrl)
O_OcrVM.recognize(img)
}
}
}
Rectangle { width: 1; height: 18; color: "#E2E8F0"; anchors.verticalCenter: parent.verticalCenter }
Rectangle {
width: 30; height: 30; radius: 7
color: (stickyWin.activeTool === "pencil") ? "#EFF6FF" : (pencilArea.containsMouse ? "#F1F5F9" : "transparent")
border.color: (stickyWin.activeTool === "pencil") ? "#3B82F6" : "transparent"
border.width: 1
Image { anchors.centerIn: parent; source: "qrc:/resource/img/lc_pencil.svg"; width: 18; height: 18 }
MouseArea { id: pencilArea; anchors.fill: parent; hoverEnabled: true; cursorShape: Qt.PointingHandCursor; onEntered: stickyWin.showToolbarTip(qsTr("Pencil"), parent); onExited: stickyWin.hideToolbarTip(qsTr("Pencil")); onPressed: stickyWin.setActiveTool("pencil") }
}
Rectangle {
width: 30; height: 30; radius: 7
color: (stickyWin.activeTool === "rect") ? "#EFF6FF" : (rectArea.containsMouse ? "#F1F5F9" : "transparent")
border.color: (stickyWin.activeTool === "rect") ? "#3B82F6" : "transparent"
border.width: 1
Image { anchors.centerIn: parent; source: "qrc:/resource/img/lc_square.svg"; width: 18; height: 18 }
MouseArea { id: rectArea; anchors.fill: parent; hoverEnabled: true; cursorShape: Qt.PointingHandCursor; onEntered: stickyWin.showToolbarTip(qsTr("Rectangle"), parent); onExited: stickyWin.hideToolbarTip(qsTr("Rectangle")); onPressed: stickyWin.setActiveTool("rect") }
}
Rectangle {
width: 30; height: 30; radius: 7
color: (stickyWin.activeTool === "circle") ? "#EFF6FF" : (circleArea.containsMouse ? "#F1F5F9" : "transparent")
border.color: (stickyWin.activeTool === "circle") ? "#3B82F6" : "transparent"
border.width: 1
Image { anchors.centerIn: parent; source: "qrc:/resource/img/lc_circle.svg"; width: 18; height: 18 }
MouseArea { id: circleArea; anchors.fill: parent; hoverEnabled: true; cursorShape: Qt.PointingHandCursor; onEntered: stickyWin.showToolbarTip(qsTr("Circle"), parent); onExited: stickyWin.hideToolbarTip(qsTr("Circle")); onPressed: stickyWin.setActiveTool("circle") }
}
Rectangle {
width: 30; height: 30; radius: 7
color: (stickyWin.activeTool === "arrow") ? "#EFF6FF" : (arrowArea.containsMouse ? "#F1F5F9" : "transparent")
border.color: (stickyWin.activeTool === "arrow") ? "#3B82F6" : "transparent"
border.width: 1
Image { anchors.centerIn: parent; source: "qrc:/resource/img/lc_arrow.svg"; width: 18; height: 18 }
MouseArea { id: arrowArea; anchors.fill: parent; hoverEnabled: true; cursorShape: Qt.PointingHandCursor; onEntered: stickyWin.showToolbarTip(qsTr("Arrow"), parent); onExited: stickyWin.hideToolbarTip(qsTr("Arrow")); onPressed: stickyWin.setActiveTool("arrow") }
}
Rectangle {
width: 30; height: 30; radius: 7
color: (stickyWin.activeTool === "mosaic") ? "#EFF6FF" : (mosaicArea.containsMouse ? "#F1F5F9" : "transparent")
border.color: (stickyWin.activeTool === "mosaic") ? "#3B82F6" : "transparent"
border.width: 1
Image { anchors.centerIn: parent; source: "qrc:/resource/img/lc_mosaic.svg"; width: 18; height: 18 }
MouseArea {
id: mosaicArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onEntered: stickyWin.showToolbarTip(qsTr("Mosaic"), parent)
onExited: stickyWin.hideToolbarTip(qsTr("Mosaic"))
onPressed: {
var img = O_StickyViewModel.getImageByUrl(stickyWin.imageUrl)
if (img && img.width > 0 && img.height > 0)
stickyPaintBoard.setBackgroundImg(img)
stickyWin.activeTool = "mosaic"
if (stickyWin.activeTool === "mosaic" && !stickyPaintBoard.hasBackgroundImg()) {
Qt.callLater(function() {
var retryImg = O_StickyViewModel.getImageByUrl(stickyWin.imageUrl)
if (retryImg && retryImg.width > 0 && retryImg.height > 0)
stickyPaintBoard.setBackgroundImg(retryImg)
if (!stickyPaintBoard.hasBackgroundImg())
stickyWin.syncPaintboardBackgroundFromDisplay()
})
}
}
}
}
Rectangle { width: 1; height: 18; color: "#E2E8F0"; anchors.verticalCenter: parent.verticalCenter }
Rectangle { width: 30; height: 30; radius: 7; color: zoomOutArea.containsMouse ? "#F1F5F9" : "transparent"; Text { anchors.centerIn: parent; text: "-"; color: "#334155"; font.pixelSize: 16 } MouseArea { id: zoomOutArea; anchors.fill: parent; hoverEnabled: true; cursorShape: Qt.PointingHandCursor; onEntered: stickyWin.showToolbarTip(qsTr("Zoom out"), parent); onExited: stickyWin.hideToolbarTip(qsTr("Zoom out")); onPressed: setZoomValue(stickyWin.zoomFactor - 0.1) } }
Rectangle { width: 40; height: 30; radius: 7; color: "transparent"; Text { anchors.centerIn: parent; text: Math.round(stickyWin.zoomFactor * 100) + "%"; color: "#64748B"; font.pixelSize: 11 } }
Rectangle { width: 30; height: 30; radius: 7; color: zoomInArea.containsMouse ? "#F1F5F9" : "transparent"; Text { anchors.centerIn: parent; text: "+"; color: "#334155"; font.pixelSize: 16 } MouseArea { id: zoomInArea; anchors.fill: parent; hoverEnabled: true; cursorShape: Qt.PointingHandCursor; onEntered: stickyWin.showToolbarTip(qsTr("Zoom in"), parent); onExited: stickyWin.hideToolbarTip(qsTr("Zoom in")); onPressed: setZoomValue(stickyWin.zoomFactor + 0.1) } }
Rectangle { width: 30; height: 30; radius: 7; color: rotateArea.containsMouse ? "#F1F5F9" : "transparent"; Text { anchors.centerIn: parent; text: "↻"; color: "#334155"; font.pixelSize: 14 } MouseArea { id: rotateArea; anchors.fill: parent; hoverEnabled: true; cursorShape: Qt.PointingHandCursor; onEntered: stickyWin.showToolbarTip(qsTr("Rotate 90°"), parent); onExited: stickyWin.hideToolbarTip(qsTr("Rotate 90°")); onPressed: rotateCurrent(90) } }
Rectangle { width: 30; height: 30; radius: 7; color: mirrorArea.containsMouse ? "#F1F5F9" : "transparent"; Text { anchors.centerIn: parent; text: "⇋"; color: "#334155"; font.pixelSize: 14 } MouseArea { id: mirrorArea; anchors.fill: parent; hoverEnabled: true; cursorShape: Qt.PointingHandCursor; onEntered: stickyWin.showToolbarTip(qsTr("Mirror"), parent); onExited: stickyWin.hideToolbarTip(qsTr("Mirror")); onPressed: mirrorCurrent() } }
Rectangle { width: 30; height: 30; radius: 7; color: resetSizeArea.containsMouse ? "#F1F5F9" : "transparent"; Text { anchors.centerIn: parent; text: "1:1"; color: "#334155"; font.pixelSize: 10 } MouseArea { id: resetSizeArea; anchors.fill: parent; hoverEnabled: true; cursorShape: Qt.PointingHandCursor; onEntered: stickyWin.showToolbarTip(qsTr("Reset size"), parent); onExited: stickyWin.hideToolbarTip(qsTr("Reset size")); onPressed: resetDisplaySize() } }
Rectangle {
width: 30; height: 30; radius: 7
color: overwriteArea.containsMouse ? "#E0F2FE" : "transparent"
Image { anchors.centerIn: parent; source: "qrc:/resource/img/lc_check.svg"; width: 18; height: 18 }
MouseArea { id: overwriteArea; anchors.fill: parent; hoverEnabled: true; cursorShape: Qt.PointingHandCursor; onEntered: stickyWin.showToolbarTip(qsTr("Done and copy"), parent); onExited: stickyWin.hideToolbarTip(qsTr("Done and copy")); onPressed: completeEditsAndCopy() }
}
Rectangle {
width: 30; height: 30; radius: 7
color: undoArea.containsMouse ? "#EFF6FF" : "transparent"
Image { anchors.centerIn: parent; source: "qrc:/resource/img/lc_undo.svg"; width: 18; height: 18 }
MouseArea { id: undoArea; anchors.fill: parent; hoverEnabled: true; cursorShape: Qt.PointingHandCursor; onEntered: stickyWin.showToolbarTip(qsTr("Undo"), parent); onExited: stickyWin.hideToolbarTip(qsTr("Undo")); onPressed: stickyPaintBoard.undo() }
}
Rectangle {
width: 30; height: 30; radius: 7
color: clearToolArea.containsMouse ? "#FEE2E2" : "transparent"
Image { anchors.centerIn: parent; source: "qrc:/resource/img/lc_x.svg"; width: 18; height: 18 }
MouseArea {
id: clearToolArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onEntered: stickyWin.showToolbarTip(qsTr("Hide toolbar"), parent)
onExited: stickyWin.hideToolbarTip(qsTr("Hide toolbar"))
onPressed: {
stickyWin.activeTool = ""
stickyWin.toolbarVisible = false
}
}
}
}
}
Rectangle {
id: stickyToolbarTip
parent: stickyWin.contentItem
visible: stickyWin.toolbarVisible && stickyWin.toolbarHoverTipText.length > 0
z: 40
width: Math.min(stickyWin.width - 8, Math.max(64, stickyToolbarTipText.implicitWidth + 16))
height: 24
radius: 6
color: "#0F172A"
opacity: 0.92
x: Math.max(4, Math.min(stickyWin.width - width - 4, stickyToolbar.x + stickyWin.toolbarHoverTipCenterX - width / 2))
y: Math.min(stickyWin.height - height - 4, stickyToolbar.y + stickyToolbar.height + stickyWin.toolbarTipGap)
Text {
id: stickyToolbarTipText
anchors.centerIn: parent
text: stickyWin.toolbarHoverTipText
color: "#FFFFFF"
font.pixelSize: 11
}
}
readonly property int chatPanelGap: 6
TZChat {
id: chatPanel
imageUrl: stickyWin.imageUrl
onSignalSendPrompt: function(text) {
O_AIViewModel.sendPrompt(text, stickyWin.imageUrl)
}
}
Connections {
target: O_AIViewModel
function onSignalRequestComplete(oldImageUrl, newImageUrl) {
if (oldImageUrl !== stickyWin.imageUrl)
return
var old = oldImageUrl
stickyWin.imageUrl = newImageUrl
Qt.callLater(function() { O_StickyViewModel.releaseImage(old) })
}
function onSignalRequestFailed(errorMsg) {
console.warn("[TZStickyWindow] AI request failed: " + errorMsg)
}
}
function openChatPanel() {
var imgLeft = stickyWin.x + stickyWin.shadowRadius
var imgTop = stickyWin.y + stickyWin.shadowRadius
var imgBottom = imgTop + stickyWin.displayH
var screenH = Screen.desktopAvailableHeight
var screenW = Screen.desktopAvailableWidth
var cx = imgLeft
if (cx + chatPanel.win.width > screenW)
cx = imgLeft + stickyWin.displayW - chatPanel.win.width
var cy
if (imgBottom + chatPanelGap + chatPanel.win.height <= screenH)
cy = imgBottom + chatPanelGap
else
cy = imgTop - chatPanelGap - chatPanel.win.height
chatPanel.open(Math.max(0, cx), Math.max(0, cy))
}
Platform.Menu {
id: contextMenu
onAboutToShow: stickyWin.contextMenuOpen = true
onAboutToHide: stickyWin.contextMenuOpen = false
Platform.MenuItem { text: qsTr("当前透明度 %1%").arg(Math.round(stickyWin.imgOpacity * 100)); enabled: false }
Platform.MenuSeparator {}
Platform.MenuItem { visible: menuPrefs.pinSaveMerged; text: qsTr("覆盖贴图(合并标注)"); onTriggered: overwriteWithEdits() }
Platform.MenuItem { visible: menuPrefs.pinCopy; text: qsTr("复制贴图"); onTriggered: O_StickyViewModel.copyImageToClipboard(imageUrl) }
Platform.MenuItem { visible: menuPrefs.pinAi; text: qsTr("AI 编辑"); onTriggered: openChatPanel() }
Platform.MenuItem {
visible: menuPrefs.pinToolbar
text: stickyWin.toolbarVisible ? qsTr("隐藏工具栏") : qsTr("显示工具栏")
onTriggered: {
stickyWin.toolbarVisible = !stickyWin.toolbarVisible
if (!stickyWin.toolbarVisible)
stickyWin.activeTool = ""
}
}
Platform.MenuItem { visible: menuPrefs.pinClose; text: qsTr("关闭窗口"); onTriggered: stickyWin.close() }
Platform.MenuSeparator {}
Platform.MenuItem { text: qsTr("透明度 +5%"); onTriggered: setOpacityValue(stickyWin.imgOpacity + 0.05) }
Platform.MenuItem { text: qsTr("透明度 -5%"); onTriggered: setOpacityValue(stickyWin.imgOpacity - 0.05) }
Platform.MenuSeparator {}
Platform.MenuItem { text: qsTr("缩小"); onTriggered: setZoomValue(stickyWin.zoomFactor - 0.1) }
Platform.MenuItem { text: qsTr("放大"); onTriggered: setZoomValue(stickyWin.zoomFactor + 0.1) }
Platform.MenuItem { text: qsTr("旋转 90°"); onTriggered: rotateCurrent(90) }
Platform.MenuItem { text: qsTr("镜像"); onTriggered: mirrorCurrent() }
Platform.MenuItem { text: qsTr("恢复原始大小"); onTriggered: resetDisplaySize() }
Platform.MenuSeparator {}
Platform.MenuItem {
text: qsTr("重置位置")
onTriggered: {
stickyWin.x = (imgRect.width >= 0 ? imgRect.x : imgRect.x + imgRect.width) - shadowRadius
stickyWin.y = (imgRect.height >= 0 ? imgRect.y : imgRect.y + imgRect.height) - shadowRadius
}
}
Platform.MenuSeparator {}
Platform.MenuItem {
text: stickyWin.toolbarVisible ? qsTr("隐藏工具栏") : qsTr("显示工具栏")
onTriggered: {
stickyWin.toolbarVisible = !stickyWin.toolbarVisible
if (!stickyWin.toolbarVisible)
stickyWin.activeTool = ""
}
}
Platform.MenuItem { text: qsTr("AI 编辑"); onTriggered: openChatPanel() }
Platform.MenuSeparator {}
Platform.MenuItem { text: qsTr("保存原图"); onTriggered: saveDialog.open() }
Platform.MenuItem { text: qsTr("另存为(合并标注)"); onTriggered: mergedSaveDialog.open() }
Platform.MenuItem { text: qsTr("覆盖贴图(合并标注)"); onTriggered: overwriteWithEdits() }
Platform.MenuItem { text: qsTr("复制贴图"); onTriggered: O_StickyViewModel.copyImageToClipboard(imageUrl) }
Platform.MenuSeparator {}
Platform.Menu {
title: qsTr("常用项置顶")
Platform.MenuItem { text: qsTr("覆盖贴图"); checkable: true; checked: menuPrefs.pinSaveMerged; onTriggered: menuPrefs.pinSaveMerged = !menuPrefs.pinSaveMerged }
Platform.MenuItem { text: qsTr("复制贴图"); checkable: true; checked: menuPrefs.pinCopy; onTriggered: menuPrefs.pinCopy = !menuPrefs.pinCopy }
Platform.MenuItem { text: qsTr("AI 编辑"); checkable: true; checked: menuPrefs.pinAi; onTriggered: menuPrefs.pinAi = !menuPrefs.pinAi }
Platform.MenuItem { text: qsTr("显示/隐藏工具栏"); checkable: true; checked: menuPrefs.pinToolbar; onTriggered: menuPrefs.pinToolbar = !menuPrefs.pinToolbar }
Platform.MenuItem { text: qsTr("关闭窗口"); checkable: true; checked: menuPrefs.pinClose; onTriggered: menuPrefs.pinClose = !menuPrefs.pinClose }
}
Platform.MenuSeparator {}
Platform.MenuItem { text: qsTr("关闭窗口"); onTriggered: stickyWin.close() }
}
Platform.FileDialog {
id: saveDialog
title: qsTr("选择保存位置")
folder: O_ImageSaver.savePath
fileMode: Platform.FileDialog.SaveFile
nameFilters: ["PNG (*.png)", "JPG (*.jpg)", "BMP (*.bmp)"]
onAccepted: O_StickyViewModel.saveImage(imageUrl, saveDialog.file)
}
Platform.FileDialog {
id: mergedSaveDialog
title: qsTr("另存为(合并标注)")
folder: O_ImageSaver.savePath
fileMode: Platform.FileDialog.SaveFile
nameFilters: ["PNG (*.png)", "JPG (*.jpg)", "BMP (*.bmp)"]
onAccepted: saveMergedTo(mergedSaveDialog.file)
}
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.LeftButton | Qt.RightButton
enabled: stickyWin.activeTool === "" && !stickyWin.contextMenuOpen
property point dragPos: Qt.point(0, 0)
property bool leftDragActive: false
z: 0
function inImageArea(mx, my) {
return mx >= stickyWin.shadowRadius
&& mx <= stickyWin.shadowRadius + stickyWin.displayW
&& my >= stickyWin.shadowRadius + stickyWin.shadowOffset / 2
&& my <= stickyWin.shadowRadius + stickyWin.shadowOffset / 2 + stickyWin.displayH
}
onPressed: function(mouse) {
if (mouse.button === Qt.LeftButton) {
dragPos = Qt.point(mouseX, mouseY)
leftDragActive = true
} else {
leftDragActive = false
}
}
onPositionChanged: function(mouse) {
if (pressed && leftDragActive && (mouse.buttons & Qt.LeftButton) && stickyWin.activeTool === "" && !stickyWin.contextMenuOpen) {
stickyWin.x += mouseX - dragPos.x
stickyWin.y += mouseY - dragPos.y
}
}
onReleased: function(mouse) {
if (!(mouse.buttons & Qt.LeftButton))
leftDragActive = false
}
onDoubleClicked: function(mouse) {
if (mouse.button === Qt.LeftButton && inImageArea(mouseX, mouseY))
resetDisplaySize()
}
onClicked: function(mouse) {
if (mouse.button === Qt.RightButton) {
leftDragActive = false
contextMenu.open()
}
}
onWheel: function(wheel) {
if (wheel.modifiers & Qt.ControlModifier) {
if (wheel.angleDelta.y > 0)
setZoomValue(stickyWin.zoomFactor + 0.1)
else if (wheel.angleDelta.y < 0)
setZoomValue(stickyWin.zoomFactor - 0.1)
} else {
if (wheel.angleDelta.y > 0)
setOpacityValue(stickyWin.imgOpacity + 0.05)
else if (wheel.angleDelta.y < 0)
setOpacityValue(stickyWin.imgOpacity - 0.05)
}
wheel.accepted = true
}
}
}
+222
View File
@@ -0,0 +1,222 @@
import QtQuick 2.12
import QtQuick.Layouts 1.12
import QtQuick.Controls 2.5
Rectangle {
id: tool
radius: 10
color: "#FFFFFF"
visible: false
border.color: "#E2E8F0"
border.width: 1
layer.enabled: true
signal signalScreenshot
signal signalCancel
signal signalSave
signal signalSticky
signal signalUndo
signal signalGifRecord
signal signalOcr
property string activeTool: ""
property bool markToolInfoVisible: false
property string hoverTipText: ""
property real hoverTipCenterX: width / 2
readonly property int shapeType: {
switch (activeTool) {
case "pencil": return 0
case "rect": return 1
case "circle": return 2
case "arrow": return 3
case "mosaic": return 4
case "text": return 5
case "number": return 6
case "highlight": return 7
default: return 1
}
}
function setActiveTool(name) {
if (activeTool === name) {
activeTool = ""
markToolInfoVisible = false
} else {
activeTool = name
markToolInfoVisible = true
}
}
function showTip(text, targetItem) {
hoverTipText = text
if (targetItem) {
var p = tool.mapFromItem(targetItem, targetItem.width / 2, targetItem.height)
hoverTipCenterX = p.x
} else {
hoverTipCenterX = width / 2
}
}
function hideTip(text) {
if (text === undefined || hoverTipText === text)
hoverTipText = ""
}
RowLayout {
anchors.fill: parent
anchors.margins: 6
layoutDirection: Qt.RightToLeft
spacing: 2
Rectangle {
width: 32; height: 32; radius: 7
color: checkArea.containsMouse ? "#DCFCE7" : "transparent"
Image { anchors.centerIn: parent; source: "qrc:/resource/img/lc_check.svg"; width: 18; height: 18 }
MouseArea { id: checkArea; anchors.fill: parent; hoverEnabled: true; cursorShape: Qt.PointingHandCursor; onEntered: tool.showTip(qsTr("完成"), parent); onExited: tool.hideTip(qsTr("完成")); onPressed: signalScreenshot() }
}
Rectangle {
width: 32; height: 32; radius: 7
color: closeArea.containsMouse ? "#FEE2E2" : "transparent"
Image { anchors.centerIn: parent; source: "qrc:/resource/img/lc_x.svg"; width: 18; height: 18 }
MouseArea { id: closeArea; anchors.fill: parent; hoverEnabled: true; cursorShape: Qt.PointingHandCursor; onEntered: tool.showTip(qsTr("取消"), parent); onExited: tool.hideTip(qsTr("取消")); onPressed: signalCancel() }
}
Rectangle { width: 1; height: 20; color: "#E2E8F0"; Layout.alignment: Qt.AlignVCenter }
Rectangle {
width: 32; height: 32; radius: 7
color: ocrArea.containsMouse ? "#F0FDF4" : "transparent"
Image { anchors.centerIn: parent; source: "qrc:/resource/img/lc_ocr.svg"; width: 18; height: 18 }
MouseArea { id: ocrArea; anchors.fill: parent; hoverEnabled: true; cursorShape: Qt.PointingHandCursor; onEntered: tool.showTip(qsTr("OCR"), parent); onExited: tool.hideTip(qsTr("OCR")); onPressed: signalOcr() }
}
Rectangle {
id: gifRecordBtn
width: 32; height: 32; radius: 7
color: gifArea.containsMouse ? "#FFF7ED" : "transparent"
Image { anchors.centerIn: parent; source: "qrc:/resource/img/lc_gif_record.svg"; width: 18; height: 18 }
MouseArea { id: gifArea; anchors.fill: parent; hoverEnabled: true; cursorShape: Qt.PointingHandCursor; onEntered: tool.showTip(qsTr("录制GIF"), parent); onExited: tool.hideTip(qsTr("录制GIF")); onPressed: signalGifRecord() }
}
Rectangle { width: 1; height: 20; color: "#E2E8F0"; Layout.alignment: Qt.AlignVCenter }
Rectangle {
width: 32; height: 32; radius: 7
color: saveArea.containsMouse ? "#EFF6FF" : "transparent"
Image { anchors.centerIn: parent; source: "qrc:/resource/img/lc_save.svg"; width: 18; height: 18 }
MouseArea { id: saveArea; anchors.fill: parent; hoverEnabled: true; cursorShape: Qt.PointingHandCursor; onEntered: tool.showTip(qsTr("保存"), parent); onExited: tool.hideTip(qsTr("保存")); onPressed: signalSave() }
}
Rectangle {
width: 32; height: 32; radius: 7
color: fixArea.containsMouse ? "#EFF6FF" : "transparent"
Image { anchors.centerIn: parent; source: "qrc:/resource/img/lc_pin.svg"; width: 18; height: 18 }
MouseArea { id: fixArea; anchors.fill: parent; hoverEnabled: true; cursorShape: Qt.PointingHandCursor; onEntered: tool.showTip(qsTr("贴图"), parent); onExited: tool.hideTip(qsTr("贴图")); onPressed: signalSticky() }
}
Rectangle { width: 1; height: 20; color: "#E2E8F0"; Layout.alignment: Qt.AlignVCenter }
Rectangle {
width: 32; height: 32; radius: 7
color: (tool.activeTool === "rect") ? "#EFF6FF" : (rectArea.containsMouse ? "#F1F5F9" : "transparent")
border.color: (tool.activeTool === "rect") ? "#3B82F6" : "transparent"
border.width: 1
Image { anchors.centerIn: parent; source: "qrc:/resource/img/lc_square.svg"; width: 18; height: 18 }
MouseArea { id: rectArea; anchors.fill: parent; hoverEnabled: true; cursorShape: Qt.PointingHandCursor; onEntered: tool.showTip(qsTr("矩形"), parent); onExited: tool.hideTip(qsTr("矩形")); onPressed: tool.setActiveTool("rect") }
}
Rectangle {
width: 32; height: 32; radius: 7
color: (tool.activeTool === "circle") ? "#EFF6FF" : (circleArea.containsMouse ? "#F1F5F9" : "transparent")
border.color: (tool.activeTool === "circle") ? "#3B82F6" : "transparent"
border.width: 1
Image { anchors.centerIn: parent; source: "qrc:/resource/img/lc_circle.svg"; width: 18; height: 18 }
MouseArea { id: circleArea; anchors.fill: parent; hoverEnabled: true; cursorShape: Qt.PointingHandCursor; onEntered: tool.showTip(qsTr("圆形"), parent); onExited: tool.hideTip(qsTr("圆形")); onPressed: tool.setActiveTool("circle") }
}
Rectangle {
width: 32; height: 32; radius: 7
color: (tool.activeTool === "arrow") ? "#EFF6FF" : (arrowArea.containsMouse ? "#F1F5F9" : "transparent")
border.color: (tool.activeTool === "arrow") ? "#3B82F6" : "transparent"
border.width: 1
Image { anchors.centerIn: parent; source: "qrc:/resource/img/lc_arrow.svg"; width: 18; height: 18 }
MouseArea { id: arrowArea; anchors.fill: parent; hoverEnabled: true; cursorShape: Qt.PointingHandCursor; onEntered: tool.showTip(qsTr("箭头"), parent); onExited: tool.hideTip(qsTr("箭头")); onPressed: tool.setActiveTool("arrow") }
}
Rectangle {
width: 32; height: 32; radius: 7
color: (tool.activeTool === "pencil") ? "#EFF6FF" : (pencilArea.containsMouse ? "#F1F5F9" : "transparent")
border.color: (tool.activeTool === "pencil") ? "#3B82F6" : "transparent"
border.width: 1
Image { anchors.centerIn: parent; source: "qrc:/resource/img/lc_pencil.svg"; width: 18; height: 18 }
MouseArea { id: pencilArea; anchors.fill: parent; hoverEnabled: true; cursorShape: Qt.PointingHandCursor; onEntered: tool.showTip(qsTr("画笔"), parent); onExited: tool.hideTip(qsTr("画笔")); onPressed: tool.setActiveTool("pencil") }
}
Rectangle {
width: 32; height: 32; radius: 7
color: (tool.activeTool === "highlight") ? "#EFF6FF" : (highlightArea.containsMouse ? "#F1F5F9" : "transparent")
border.color: (tool.activeTool === "highlight") ? "#3B82F6" : "transparent"
border.width: 1
Image { anchors.centerIn: parent; source: "qrc:/resource/img/lc_highlighter.svg"; width: 18; height: 18 }
MouseArea { id: highlightArea; anchors.fill: parent; hoverEnabled: true; cursorShape: Qt.PointingHandCursor; onEntered: tool.showTip(qsTr("高亮"), parent); onExited: tool.hideTip(qsTr("高亮")); onPressed: tool.setActiveTool("highlight") }
}
Rectangle {
width: 32; height: 32; radius: 7
color: (tool.activeTool === "mosaic") ? "#EFF6FF" : (mosaicArea.containsMouse ? "#F1F5F9" : "transparent")
border.color: (tool.activeTool === "mosaic") ? "#3B82F6" : "transparent"
border.width: 1
Image { anchors.centerIn: parent; source: "qrc:/resource/img/lc_mosaic.svg"; width: 18; height: 18 }
MouseArea { id: mosaicArea; anchors.fill: parent; hoverEnabled: true; cursorShape: Qt.PointingHandCursor; onEntered: tool.showTip(qsTr("马赛克"), parent); onExited: tool.hideTip(qsTr("马赛克")); onPressed: tool.setActiveTool("mosaic") }
}
Rectangle {
width: 32; height: 32; radius: 7
color: (tool.activeTool === "text") ? "#EFF6FF" : (textArea.containsMouse ? "#F1F5F9" : "transparent")
border.color: (tool.activeTool === "text") ? "#3B82F6" : "transparent"
border.width: 1
Image { anchors.centerIn: parent; source: "qrc:/resource/img/lc_text.svg"; width: 18; height: 18 }
MouseArea { id: textArea; anchors.fill: parent; hoverEnabled: true; cursorShape: Qt.PointingHandCursor; onEntered: tool.showTip(qsTr("文字"), parent); onExited: tool.hideTip(qsTr("文字")); onPressed: tool.setActiveTool("text") }
}
Rectangle {
width: 32; height: 32; radius: 7
color: (tool.activeTool === "number") ? "#EFF6FF" : (numberArea.containsMouse ? "#F1F5F9" : "transparent")
border.color: (tool.activeTool === "number") ? "#3B82F6" : "transparent"
border.width: 1
Image { anchors.centerIn: parent; source: "qrc:/resource/img/lc_list_ordered.svg"; width: 18; height: 18 }
MouseArea { id: numberArea; anchors.fill: parent; hoverEnabled: true; cursorShape: Qt.PointingHandCursor; onEntered: tool.showTip(qsTr("序号"), parent); onExited: tool.hideTip(qsTr("序号")); onPressed: tool.setActiveTool("number") }
}
Rectangle {
width: 32; height: 32; radius: 7
color: undoArea.containsMouse ? "#EFF6FF" : "transparent"
Image { anchors.centerIn: parent; source: "qrc:/resource/img/lc_undo.svg"; width: 18; height: 18 }
MouseArea { id: undoArea; anchors.fill: parent; hoverEnabled: true; cursorShape: Qt.PointingHandCursor; onEntered: tool.showTip(qsTr("撤销"), parent); onExited: tool.hideTip(qsTr("撤销")); onPressed: signalUndo() }
}
}
Rectangle {
id: tipBubble
visible: tool.visible && tool.hoverTipText.length > 0
z: 100
parent: tool.parent
width: Math.max(44, tipText.implicitWidth + 14)
height: 24
radius: 6
color: "#0F172A"
opacity: 0.92
x: Math.max(tool.x, Math.min(tool.x + tool.width - width, tool.x + tool.hoverTipCenterX - width / 2))
y: tool.y + tool.height + 6
Text {
id: tipText
anchors.centerIn: parent
text: tool.hoverTipText
color: "#FFFFFF"
font.pixelSize: 11
}
}
}
+1030
View File
File diff suppressed because it is too large. Load diff
+233
View File
@@ -0,0 +1,233 @@
import QtQuick
import QtQuick.Window
import QtQuick.Shapes
import QtQuick.Layouts
import com.Tz.tray 1.0
TZWindow {
id: root
visibility: Window.Hidden
title: qsTr("TZshot")
color: "transparent"
flags: Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint
property var settingWinObj: null
property var aboutWinObj: null
property var overlayList: []
property var overlayComponent: null
function _prepareSessionState() {
captureUiReady = false
root.opacity = 0.0
screenshotArea = Qt.rect(0, 0, 0, 0)
initialScreenshotArea = Qt.rect(0, 0, 0, 0)
highlightRect = Qt.rect(0, 0, 0, 0)
screenShotUrl = ""
paintBoard.reset()
tool.visible = false
tool.activeTool = ""
tool.markToolInfoVisible = false
gifRecordMode = false
if (gifBarObj)
gifBarObj.visible = false
if (typeof resizeHandleGroup !== "undefined") {
resizeHandleGroup.area = Qt.rect(0, 0, 0, 0)
resizeHandleGroup.initialArea = Qt.rect(0, 0, 0, 0)
resizeHandleGroup.visible = false
}
if (typeof gifResizeHandleGroup !== "undefined") {
gifResizeHandleGroup.area = Qt.rect(0, 0, 0, 0)
gifResizeHandleGroup.initialArea = Qt.rect(0, 0, 0, 0)
gifResizeHandleGroup.visible = false
}
if (typeof hideTextEditor === "function")
hideTextEditor()
}
function startScreenshot() {
root.visibility = Window.Hidden
_destroyOverlays()
_prepareSessionState()
Qt.callLater(function() {
O_ScreenCapture.captureDesktop()
var screens = Qt.application.screens
if (screens.length === 0)
return
var primaryIdx = 0
for (var i = 0; i < screens.length; i++) {
if (screens[i].virtualX === 0 && screens[i].virtualY === 0) {
primaryIdx = i
break
}
}
var primary = screens[primaryIdx]
root.screenVirtualX = primary.virtualX
root.screenVirtualY = primary.virtualY
root.x = primary.virtualX
root.y = primary.virtualY
root.width = primary.width
root.height = primary.height
O_ScreenCapture.grabToPaintBoard(
root.paintBoard,
Qt.rect(root.screenVirtualX, root.screenVirtualY, root.width, root.height)
)
if (overlayComponent === null)
overlayComponent = Qt.createComponent("qrc:/qml/TZScreenOverlay.qml")
var newList = []
for (var j = 0; j < screens.length; j++) {
if (j === primaryIdx)
continue
var sc = screens[j]
if (overlayComponent.status === Component.Ready) {
var ov = overlayComponent.createObject(null, {
"delegate": root,
"screenVX": sc.virtualX,
"screenVY": sc.virtualY,
"x": sc.virtualX,
"y": sc.virtualY,
"width": sc.width,
"height": sc.height,
"visibility": Window.Windowed
})
if (ov) {
ov.raise()
newList.push(ov)
}
} else if (overlayComponent.status === Component.Error) {
console.error("TZScreenOverlay load failed:", overlayComponent.errorString())
}
}
overlayList = newList
root.visibility = Window.Windowed
root.raise()
root.requestActivate()
Qt.callLater(function() {
root.captureUiReady = true
Qt.callLater(function() {
root.opacity = 1.0
})
})
})
}
function _destroyOverlays() {
for (var i = 0; i < overlayList.length; i++)
overlayList[i].destroy()
overlayList = []
}
function resetArea() {
_destroyOverlays()
_prepareSessionState()
O_ScreenCapture.releaseDesktopSnapshot()
root.visibility = Window.Hidden
}
function showAboutWin() {
if (aboutWinObj !== null) {
aboutWinObj.raise()
aboutWinObj.requestActivate()
return
}
var component = Qt.createComponent("qrc:/qml/Setting/TZAboutWin.qml")
if (component.status === Component.Ready)
aboutWinObj = component.createObject(null)
else if (component.status === Component.Error)
console.error("About window load failed:", component.errorString())
if (aboutWinObj !== null) {
aboutWinObj.visible = true
aboutWinObj.raise()
aboutWinObj.requestActivate()
aboutWinObj.closing.connect(function() {
aboutWinObj.destroy(100)
aboutWinObj = null
})
}
}
function showSettingWin() {
if (settingWinObj !== null) {
settingWinObj.raise()
settingWinObj.requestActivate()
return
}
var component = Qt.createComponent("qrc:/qml/Setting/TZSettingWin.qml")
if (component.status === Component.Ready)
settingWinObj = component.createObject(null)
else if (component.status === Component.Error)
console.error("Setting window load failed:", component.errorString())
if (settingWinObj !== null) {
settingWinObj.visible = true
settingWinObj.raise()
settingWinObj.requestActivate()
settingWinObj.closing.connect(function() {
settingWinObj.destroy(100)
settingWinObj = null
})
}
}
Connections {
target: O_StickyViewModel
function onStickyReady(imageUrl, imgRect) {
root.createStickyWindow(imageUrl, imgRect)
}
}
Connections {
target: O_OcrVM
function onIsRecognizingChanged() {
if (O_OcrVM.isRecognizing)
root.createOcrResultWindow()
}
function onResultReady(text) {
root.createOcrResultWindow()
}
function onRecognizeFailed(errorMessage) {
trayHelper.showMessage(qsTr("OCR 识别失败"), errorMessage)
}
}
TrayIconHelper {
id: trayHelper
function onTrayClicked(clickType) {
if (clickType === 0 || clickType === 2)
startScreenshot()
}
onScreenshotTriggered: startScreenshot()
onShowSettingTriggered: { showSettingWin() }
onShowAboutTriggered: { showAboutWin() }
onExitTriggered: Qt.quit()
}
Component.onCompleted: {
trayHelper.setIcon(":/resource/img/tray_icon.svg")
trayHelper.show()
O_GlobalShortcut.screenshotActivated.connect(function() { startScreenshot() })
O_GlobalShortcut.screenshotSaveActivated.connect(function() { startScreenshot() })
O_GlobalShortcut.stickyActivated.connect(function() { startScreenshot() })
O_GlobalShortcut.toggleActivated.connect(function() {
if (root.visible)
resetArea()
else
startScreenshot()
})
}
}
+4
View File
@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M13 5H19V11"/>
<path d="M19 5L5 19"/>
</svg>

After

Width:  |  Height:  |  Size: 240 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-bot-icon lucide-bot"><path d="M12 8V4H8"/><rect width="16" height="12" x="4" y="8" rx="2"/><path d="M2 14h2"/><path d="M20 14h2"/><path d="M15 13v2"/><path d="M9 13v2"/></svg>

After

Width:  |  Height:  |  Size: 377 B

+3
View File
@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M20 6 9 17l-5-5"/>
</svg>

After

Width:  |  Height:  |  Size: 219 B

+3
View File
@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="10"/>
</svg>

After

Width:  |  Height:  |  Size: 224 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-circle-x-icon lucide-circle-x"><circle cx="12" cy="12" r="10"/><path d="m15 9-6 6"/><path d="m9 9 6 6"/></svg>

After

Width:  |  Height:  |  Size: 312 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-smile-plus-icon lucide-smile-plus"><path d="M22 11v1a10 10 0 1 1-9-10"/><path d="M8 14s1.5 2 4 2 4-2 4-2"/><line x1="9" x2="9.01" y1="9" y2="9"/><line x1="15" x2="15.01" y1="9" y2="9"/><path d="M16 5h6"/><path d="M19 2v6"/></svg>

After

Width:  |  Height:  |  Size: 431 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-file-icon lucide-file"><path d="M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z"/><path d="M14 2v5a1 1 0 0 0 1 1h5"/></svg>

After

Width:  |  Height:  |  Size: 388 B

+6
View File
@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<!-- 外圆 -->
<circle cx="12" cy="12" r="9"/>
<!-- 录制红点 -->
<circle cx="12" cy="12" r="4" fill="currentColor" stroke="none"/>
</svg>

After

Width:  |  Height:  |  Size: 333 B

+4
View File
@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<!-- 停止方块 -->
<rect x="5" y="5" width="14" height="14" rx="2" fill="currentColor" stroke="none"/>
</svg>

After

Width:  |  Height:  |  Size: 299 B

+6
View File
@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="m9 11-6 6v3h3l6-6"/>
<path d="m15 5 4 4"/>
<path d="m18 2 4 4-8 8-4-4 8-8z"/>
<path d="M4 22h8"/>
</svg>

After

Width:  |  Height:  |  Size: 304 B

+9
View File
@@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M10 6h10"/>
<path d="M10 12h10"/>
<path d="M10 18h10"/>
<path d="M4 6h1v4"/>
<path d="M3 10h3"/>
<path d="M4 18h2"/>
<path d="M5 14v4"/>
</svg>

After

Width:  |  Height:  |  Size: 349 B

+7
View File
@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect width="18" height="18" x="3" y="3" rx="2"/>
<path d="M3 9h18"/>
<path d="M3 15h18"/>
<path d="M9 3v18"/>
<path d="M15 3v18"/>
</svg>

After

Width:  |  Height:  |  Size: 331 B

+11
View File
@@ -0,0 +1,11 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<!-- 文档外框 -->
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"/>
<!-- 文字行 -->
<line x1="7" y1="8" x2="17" y2="8"/>
<line x1="7" y1="12" x2="17" y2="12"/>
<line x1="7" y1="16" x2="13" y2="16"/>
<!-- 右下角放大镜 -->
<circle cx="17" cy="17" r="2.5"/>
<line x1="19" y1="19" x2="21" y2="21"/>
</svg>

After

Width:  |  Height:  |  Size: 522 B

+4
View File
@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z"/>
<path d="m15 5 4 4"/>
</svg>

After

Width:  |  Height:  |  Size: 356 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-pin-icon lucide-pin"><path d="M12 17v5"/><path d="M9 10.76a2 2 0 0 1-1.11 1.79l-1.78.9A2 2 0 0 0 5 15.24V16a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-.76a2 2 0 0 0-1.11-1.79l-1.78-.9A2 2 0 0 1 15 10.76V7a1 1 0 0 1 1-1 2 2 0 0 0 0-4H8a2 2 0 0 0 0 4 1 1 0 0 1 1 1z"/></svg>

After

Width:  |  Height:  |  Size: 464 B

+5
View File
@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M15.2 3a2 2 0 0 1 1.4.6l3.8 3.8a2 2 0 0 1 .6 1.4V19a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2z"/>
<path d="M17 21v-7a1 1 0 0 0-1-1H8a1 1 0 0 0-1 1v7"/>
<path d="M7 3v4a1 1 0 0 0 1 1h7"/>
</svg>

After

Width:  |  Height:  |  Size: 395 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-send-icon lucide-send"><path d="M14.536 21.686a.5.5 0 0 0 .937-.024l6.5-19a.496.496 0 0 0-.635-.635l-19 6.5a.5.5 0 0 0-.024.937l7.93 3.18a2 2 0 0 1 1.112 1.11z"/><path d="m21.854 2.147-10.94 10.939"/></svg>

After

Width:  |  Height:  |  Size: 408 B

+3
View File
@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect width="18" height="18" x="3" y="3" rx="2"/>
</svg>

After

Width:  |  Height:  |  Size: 241 B

+5
View File
@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M4 7V4h16v3"/>
<path d="M9 20h6"/>
<path d="M12 4v16"/>
</svg>

After

Width:  |  Height:  |  Size: 260 B

+4
View File
@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M9 14 4 9l5-5"/>
<path d="M4 9h10.5a5.5 5.5 0 0 1 5.5 5.5v0a5.5 5.5 0 0 1-5.5 5.5H11"/>
</svg>

After

Width:  |  Height:  |  Size: 290 B

+4
View File
@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M18 6 6 18"/>
<path d="m6 6 12 12"/>
</svg>

After

Width:  |  Height:  |  Size: 239 B

+14
View File
@@ -0,0 +1,14 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="32" height="32">
<!-- 虚线选区矩形 -->
<rect x="3" y="3" width="26" height="26" rx="2"
fill="none" stroke="#FFFFFF" stroke-width="2"
stroke-dasharray="4,3"/>
<!-- 十字准心横线(中间留空) -->
<line x1="3" y1="16" x2="12" y2="16" stroke="#FFFFFF" stroke-width="2" stroke-linecap="round"/>
<line x1="20" y1="16" x2="29" y2="16" stroke="#FFFFFF" stroke-width="2" stroke-linecap="round"/>
<!-- 十字准心竖线(中间留空) -->
<line x1="16" y1="3" x2="16" y2="12" stroke="#FFFFFF" stroke-width="2" stroke-linecap="round"/>
<line x1="16" y1="20" x2="16" y2="29" stroke="#FFFFFF" stroke-width="2" stroke-linecap="round"/>
<!-- 中心圆点 -->
<circle cx="16" cy="16" r="2.5" fill="#FFFFFF"/>
</svg>

After

Width:  |  Height:  |  Size: 825 B

+112
View File
@@ -0,0 +1,112 @@
#include "ai_call_base.h"
AICallBase::AICallBase(QObject *parent)
: QObject(parent)
, m_lastErrorType(AIErrorType::NoError)
, m_timeout(30000) // 默认30秒超时
, m_timeoutTimer(new QTimer(this))
, m_currentReply(nullptr)
{
// 初始化网络管理器
m_networkManager = new QNetworkAccessManager(this);
// 连接超时定时器
m_timeoutTimer->setSingleShot(true);
connect(m_timeoutTimer, &QTimer::timeout, this, &AICallBase::onRequestTimeout);
}
AICallBase::~AICallBase()
{
cancelRequest();
}
void AICallBase::setTimeout(int msec)
{
if (msec > 0) {
m_timeout = msec;
}
}
QString AICallBase::lastErrorString() const
{
return m_lastErrorString;
}
AIErrorType AICallBase::lastErrorType() const
{
return m_lastErrorType;
}
void AICallBase::cancelRequest()
{
// 停止超时定时器
m_timeoutTimer->stop();
// 取消当前请求
if (m_currentReply) {
m_currentReply->abort();
m_currentReply->deleteLater();
m_currentReply = nullptr;
}
// 重置错误信息
m_lastErrorType = AIErrorType::NoError;
m_lastErrorString.clear();
}
void AICallBase::setError(AIErrorType errorType, const QString &errorMsg)
{
m_lastErrorType = errorType;
m_lastErrorString = errorMsg;
emit requestFailed(errorType, errorMsg);
}
void AICallBase::onReplyFinished(QNetworkReply *reply)
{
// 停止超时定时器
m_timeoutTimer->stop();
// 清理Reply
reply->deleteLater();
m_currentReply = nullptr;
if (reply->error() != QNetworkReply::NoError) {
setError(AIErrorType::NetworkError, reply->errorString());
return;
}
// 读取响应数据
QByteArray responseData = reply->readAll();
// 解析响应(子类实现)
QString result;
try {
result = parseResponse(responseData);
} catch (const std::exception &e) {
setError(AIErrorType::ApiError, QString("响应解析失败:%1").arg(e.what()));
return;
} catch (...) {
setError(AIErrorType::ApiError, "响应解析失败:未知错误");
return;
}
// 发送成功信号
emit requestSuccess(result);
}
void AICallBase::onReplyError(QNetworkReply::NetworkError error)
{
Q_UNUSED(error);
// 错误处理在onReplyFinished中统一处理
}
void AICallBase::onRequestTimeout()
{
if (m_currentReply) {
m_currentReply->abort();
m_currentReply->deleteLater();
m_currentReply = nullptr;
}
setError(AIErrorType::TimeoutError, QString("请求超时(%1毫秒)").arg(m_timeout));
}
+92
View File
@@ -0,0 +1,92 @@
#ifndef AICALLBASE_H
#define AICALLBASE_H
#include <QObject>
#include <QNetworkAccessManager>
#include <QNetworkRequest>
#include <QNetworkReply>
#include <QJsonObject>
#include <QJsonDocument>
#include <QTimer>
#include <QUrl>
// AI 调用错误类型枚举
enum class AIErrorType {
NoError, // 无错误
NetworkError, // 网络错误
ApiError, // API 返回错误
TimeoutError, // 请求超时
ParamError // 参数错误
};
// AI 调用基类(抽象类)
class AICallBase : public QObject
{
Q_OBJECT
public:
explicit AICallBase(QObject *parent = nullptr);
virtual ~AICallBase();
// 设置请求超时时间(默认30秒)
void setTimeout(int msec);
// 获取最后一次错误信息
QString lastErrorString() const;
// 获取最后一次错误类型
AIErrorType lastErrorType() const;
/**
* @brief 发送AI请求(核心接口,子类实现)
* @param prompt 提示词/问题
* @param params 额外参数(不同AI接口的自定义参数)
* @return 是否成功发起请求
*/
virtual bool sendRequest(const QString &prompt, const QJsonObject &params = QJsonObject()) = 0;
/**
* @brief 取消当前请求
*/
void cancelRequest();
signals:
// 请求成功,返回响应内容
void requestSuccess(const QString &response);
// 请求失败,返回错误类型和错误信息
void requestFailed(AIErrorType errorType, const QString &errorMsg);
// 请求进度(可选,如流式响应)
void requestProgress(int progress);
protected:
// 构建请求头(子类实现)
virtual QNetworkRequest buildRequest() = 0;
// 构建请求体(子类实现)
virtual QByteArray buildRequestBody(const QString &prompt, const QJsonObject &params) = 0;
// 解析响应数据(子类实现)
virtual QString parseResponse(const QByteArray &responseData) = 0;
// 设置错误信息
void setError(AIErrorType errorType, const QString &errorMsg);
// 网络请求管理器(所有子类共享)
QNetworkAccessManager *m_networkManager;
public slots:
// 网络响应处理
virtual void onReplyFinished(QNetworkReply *reply);
// 网络错误处理
virtual void onReplyError(QNetworkReply::NetworkError error);
// 请求超时处理
virtual void onRequestTimeout();
public:
QString m_lastErrorString; // 最后一次错误信息
AIErrorType m_lastErrorType; // 最后一次错误类型
int m_timeout; // 超时时间(毫秒)
QTimer *m_timeoutTimer; // 超时定时器
QNetworkReply *m_currentReply; // 当前请求的Reply
};
#endif // AICALLBASE_H
+14
View File
@@ -0,0 +1,14 @@
#ifndef AI_TYPE_H
#define AI_TYPE_H
// AI 模型类型枚举
// 值与 QML 设置页面 aiModel ListModel 的索引一一对应:
// 0 → 阿里云百炼(Qwen)
// 1 → 火山引擎(Seedream)
enum AIType
{
QWENAI = 0,
SEEDREAMAI = 1
};
#endif // AI_TYPE_H
+143
View File
@@ -0,0 +1,143 @@
#include "qwen_ai_call.h"
#include <QJsonArray>
#include <stdexcept>
QwenAICall::QwenAICall(const QString &apiKey, QObject *parent)
: AICallBase(parent)
, m_apiKey(apiKey)
, m_apiUrl("https://dashscope.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation")
{
}
bool QwenAICall::sendRequest(const QString &prompt, const QJsonObject &params)
{
// 参数校验
if (m_apiKey.isEmpty()) {
setError(AIErrorType::ParamError, "API Key 不能为空");
return false;
}
if (prompt.isEmpty()) {
setError(AIErrorType::ParamError, "提示词不能为空");
return false;
}
// 取消之前的请求
cancelRequest();
// 构建请求
QNetworkRequest request = buildRequest();
QByteArray requestBody = buildRequestBody(prompt, params);
// 发送POST请求
m_currentReply = m_networkManager->post(request, requestBody);
// 用局部变量捕获当前 reply,避免 cancelRequest() 将 m_currentReply 置空后
// lambda 中持有悬空指针导致 use-after-free
QNetworkReply *reply = m_currentReply;
connect(reply, &QNetworkReply::finished, this, [this, reply]() {
this->AICallBase::onReplyFinished(reply);
});
connect(reply, &QNetworkReply::errorOccurred, this, [this](QNetworkReply::NetworkError errorCode) {
this->AICallBase::onReplyError(errorCode);
});
// 启动超时定时器
m_timeoutTimer->start(m_timeout);
return true;
}
void QwenAICall::setApiUrl(const QString &url)
{
if (!url.isEmpty()) {
m_apiUrl = url;
}
}
QNetworkRequest QwenAICall::buildRequest()
{
QNetworkRequest request;
request.setUrl(QUrl(m_apiUrl));
// 设置请求头
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
request.setRawHeader("Authorization", QString("Bearer %1").arg(m_apiKey).toUtf8());
// 可选:添加User-Agent(部分API要求)
request.setRawHeader("User-Agent", "Mozilla/5.0 (X11; Linux aarch64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.5005.200 Safari/537.36 Qaxbrowser");
return request;
}
QByteArray QwenAICall::buildRequestBody(const QString &prompt, const QJsonObject &params)
{
QJsonObject requestJson;
// 默认参数
requestJson["model"] = "wan2.6-image";
// 构建消息体
QJsonObject input;
QJsonArray messages;
QJsonArray content;
QJsonObject message;
QJsonObject promptInfo;
QJsonObject imgInfo;
message["role"] = "user";
promptInfo["text"] = prompt;
imgInfo["image"] = params["img"];
content.append(promptInfo);
content.append(imgInfo);
message["content"] = content;
messages.append(message);
input["messages"] = messages;
requestJson["input"] = input;
// 转换为JSON字节数组
return QJsonDocument(requestJson).toJson(QJsonDocument::Compact);
}
QString QwenAICall::parseResponse(const QByteArray &responseData)
{
QJsonParseError parseError;
QJsonDocument doc = QJsonDocument::fromJson(responseData, &parseError);
if (parseError.error != QJsonParseError::NoError) {
throw std::runtime_error(parseError.errorString().toStdString());
}
if (!doc.isObject()) {
throw std::runtime_error("响应不是有效的JSON对象");
}
QJsonObject rootObj = doc.object();
// 检查API错误
if (rootObj.contains("error")) {
QJsonObject errorObj = rootObj["error"].toObject();
QString errorMsg = errorObj["message"].toString("未知API错误");
throw std::runtime_error(errorMsg.toStdString());
}
// 解析返回内容
QJsonObject data = rootObj["output"].toObject();
QJsonArray choices = data["choices"].toArray();
if (choices.isEmpty()) {
throw std::runtime_error("响应中无有效内容");
}
QJsonObject choiceObj = choices.first().toObject();
QJsonObject messageObj = choiceObj["message"].toObject();
QJsonArray content = messageObj["content"].toArray();
QJsonObject imgdata = content.first().toObject();
QString img = imgdata["image"].toString();
if (img.isEmpty()) {
throw std::runtime_error("响应内容为空");
}
return img;
}
bool QwenAICall::setApiKey(QString apiKey)
{
m_apiKey = apiKey;
return true;
}
+31
View File
@@ -0,0 +1,31 @@
#ifndef QWENAICALL_H
#define QWENAICALL_H
#include <QObject>
#include "ai_call_base.h"
class QwenAICall:public AICallBase
{
Q_OBJECT
public:
explicit QwenAICall(const QString &apiKey, QObject *parent = nullptr);
// 实现基类纯虚函数
bool sendRequest(const QString &prompt, const QJsonObject &params = QJsonObject()) override;
// 设置API地址(默认OpenAI官方地址,可替换为代理地址)
void setApiUrl(const QString &url);
public:
QNetworkRequest buildRequest() override;
QByteArray buildRequestBody(const QString &prompt, const QJsonObject &params) override;
QString parseResponse(const QByteArray &responseData) override;
public:
bool setApiKey(QString apiKey);
private:
QString m_apiKey; // API Key
QString m_apiUrl;
};
#endif // QWENAICALL_H
+174
View File
@@ -0,0 +1,174 @@
#include "seedream_ai_call.h"
#include <QDebug>
#include <QJsonArray>
#include <stdexcept>
// 默认模型名称(火山引擎方舟 Seedream 5.0)
static const QString kDefaultModel = "doubao-seedream-5-0-260128";
static const QString kDefaultApiUrl = "https://ark.cn-beijing.volces.com/api/v3/images/generations";
SeedreamAiCall::SeedreamAiCall(const QString &apiKey, QObject *parent)
: AICallBase(parent)
, m_apiKey(apiKey)
, m_apiUrl(kDefaultApiUrl)
, m_model(kDefaultModel)
{
// 图片生成耗时较长,默认超时 150 秒
m_timeout = 150000;
}
void SeedreamAiCall::setApiKey(const QString &key)
{
if (!key.isEmpty())
m_apiKey = key;
}
void SeedreamAiCall::setModel(const QString &model)
{
if (!model.isEmpty())
m_model = model;
}
void SeedreamAiCall::setApiUrl(const QString &url)
{
if (!url.isEmpty())
m_apiUrl = url;
}
bool SeedreamAiCall::sendRequest(const QString &prompt, const QJsonObject &params)
{
if (m_apiKey.isEmpty()) {
setError(AIErrorType::ParamError, "API Key 不能为空");
return false;
}
if (prompt.isEmpty()) {
setError(AIErrorType::ParamError, "提示词不能为空");
return false;
}
cancelRequest();
QNetworkRequest request = buildRequest();
QByteArray body = buildRequestBody(prompt, params);
if (body.isEmpty())
return false;
m_currentReply = m_networkManager->post(request, body);
QNetworkReply *reply = m_currentReply;
connect(reply, &QNetworkReply::finished, this, [this, reply]() {
this->AICallBase::onReplyFinished(reply);
});
connect(reply, &QNetworkReply::errorOccurred, this,
[this](QNetworkReply::NetworkError errorCode) {
this->AICallBase::onReplyError(errorCode);
});
m_timeoutTimer->start(m_timeout);
return true;
}
// ── 构建 HTTP 请求头 ──────────────────────────────────────
QNetworkRequest SeedreamAiCall::buildRequest()
{
QNetworkRequest request;
request.setUrl(QUrl(m_apiUrl));
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
request.setRawHeader("Authorization",
QStringLiteral("Bearer %1").arg(m_apiKey).toUtf8());
return request;
}
// ── 构建请求体 ────────────────────────────────────────────
// 请求体格式(文生图 / 图生图统一端点):
// {
// "model": "doubao-seedream-5-0-260128", // 必填
// "prompt": "<用户提示词>", // 必填
// "image": "<url 或 base64>", // 可选,有则为图生图
// "size": "2K", // 可选,如 "1K"/"2K"/"4K" 或 "WxH"
// "output_format": "png", // 可选,"png"/"jpeg"/"webp"
// "watermark": false, // 可选
// "seed": 12345 // 可选
// }
QByteArray SeedreamAiCall::buildRequestBody(const QString &prompt, const QJsonObject &params)
{
QJsonObject body;
body["model"] = m_model;
body["prompt"] = prompt;
// 可选:参考图片(字符串,非数组)
QString img = params.value("img").toString();
if (!img.isEmpty())
body["image"] = img;
// 可选:图片尺寸,如 "2K"、"1024x1024"
QString size = params.value("size").toString();
if (!size.isEmpty())
body["size"] = size;
// 可选:输出格式,默认 "png"
body["output_format"] = params.value("output_format").toString("png");
// 可选:水印,默认 false
body["watermark"] = params.value("watermark").toBool(false);
// 可选:随机种子(>= 0 时才写入)
if (params.contains("seed")) {
int seed = params.value("seed").toInt(-1);
if (seed >= 0)
body["seed"] = seed;
}
const QByteArray json = QJsonDocument(body).toJson(QJsonDocument::Compact);
//qDebug() << "[SeedreamAiCall] 请求体:" << json;
return json;
}
// ── 解析响应 ──────────────────────────────────────────────
// 成功响应格式:
// {
// "model": "doubao-seedream-5-0-260128",
// "created": 1757323224,
// "data": [
// { "url": "https://...", "size": "1760x2368" }
// ],
// "usage": { "generated_images": 1, "output_tokens": 16280, "total_tokens": 16280 }
// }
// 错误响应格式:
// {
// "error": { "code": "...", "message": "...", "param": null, "type": "..." }
// }
QString SeedreamAiCall::parseResponse(const QByteArray &responseData)
{
QJsonParseError parseError;
QJsonDocument doc = QJsonDocument::fromJson(responseData, &parseError);
if (parseError.error != QJsonParseError::NoError)
throw std::runtime_error(parseError.errorString().toStdString());
if (!doc.isObject())
throw std::runtime_error("响应不是有效的 JSON 对象");
QJsonObject root = doc.object();
// API 层错误
if (root.contains("error")) {
QJsonObject err = root["error"].toObject();
QString code = err["code"].toString();
QString msg = err["message"].toString("未知 API 错误");
throw std::runtime_error(
QStringLiteral("[%1] %2").arg(code, msg).toStdString());
}
QJsonArray data = root["data"].toArray();
if (data.isEmpty())
throw std::runtime_error("响应中 data 数组为空");
QString url = data.first().toObject()["url"].toString();
if (url.isEmpty())
throw std::runtime_error("响应中 url 字段为空");
qDebug() << "[SeedreamAiCall] 生成图片 URL:" << url;
return url;
}
+73
View File
@@ -0,0 +1,73 @@
#ifndef SEEDREAM_AI_CALL_H
#define SEEDREAM_AI_CALL_H
#include <QObject>
#include "ai_call_base.h"
// 图片生成请求参数(对应火山引擎方舟 /api/v3/images/generations)
struct SeedreamRequestParams {
// 参考图片 URL 或 Base64(image-to-image 时传入;纯文生图可留空)
QString imageUrl;
// 图片尺寸,格式 "宽x高",如 "1024x1024",默认空表示由模型决定
QString size;
// 生成数量,默认 1,最大值视模型而定
int n = 1;
// 输出格式:"png" / "jpeg" / "webp",默认 "png"
QString outputFormat = "png";
// 是否添加水印,默认 false
bool watermark = false;
// 引导强度(图生图时有效),范围 0.0~1.0,默认 0.5
double guidanceScale = 0.5;
// 随机种子,-1 表示随机
int seed = -1;
};
class SeedreamAiCall : public AICallBase
{
Q_OBJECT
public:
explicit SeedreamAiCall(const QString &apiKey,
QObject *parent = nullptr);
// 设置 API Key(覆盖构造时传入的值)
void setApiKey(const QString &key);
// 设置模型名称,默认 "doubao-seedream-5-0-260128"
void setModel(const QString &model);
// 覆盖 API 地址(一般不需要修改)
void setApiUrl(const QString &url);
// 发起请求
// prompt : 文字提示词
// params : QJsonObject,支持以下 key:
// "img" - 参考图片 URL/Base64(图生图)
// "size" - 尺寸字符串
// "n" - 生成数量
// "output_format" - 输出格式
// "watermark" - bool
// "guidance_scale"- double
// "seed" - int
bool sendRequest(const QString &prompt,
const QJsonObject &params = QJsonObject()) override;
protected:
QNetworkRequest buildRequest() override;
QByteArray buildRequestBody(const QString &prompt,
const QJsonObject &params) override;
QString parseResponse(const QByteArray &responseData) override;
private:
QString m_apiKey;
QString m_apiUrl;
QString m_model;
};
#endif // SEEDREAM_AI_CALL_H
+349
View File
@@ -0,0 +1,349 @@
// gif.h - GIF encoder single-header library
// Based on the public domain work by Charlie Tangora (gif-h)
// Modified for Qt integration with RGBA transparency support
//
// Usage:
// GifWriter g;
// GifBegin(&g, "output.gif", width, height, delay, transparentIndex);
// GifWriteFrame(&g, rgba_data, width, height, delay, 8, transparentIndex);
// GifEnd(&g);
//
// delay is in units of 10ms (so delay=2 means 20ms = 50fps, delay=7 = 70ms ~15fps)
#ifndef GIF_H
#define GIF_H
#include <stdio.h>
#include <string.h>
#include <stdint.h>
#include <stdlib.h>
// ----- Palette/Color quantization -----
static const int kGifTransIndex = 0;
struct GifPalette {
int bitDepth;
uint8_t r[256];
uint8_t g[256];
uint8_t b[256];
// k-d tree over RGB space for fast nearest-color lookup
uint8_t treeSplitElt[255];
uint8_t treeSplit[255];
};
// walks the k-d tree to find the best-matching color
static int GifGetClosestColor(const GifPalette* pPal, int r, int g, int b) {
int bestDiff = 1000000;
int bestInd = 1;
for (int ii = 1; ii < (1 << pPal->bitDepth); ++ii) {
int dr = (int)pPal->r[ii] - r;
int dg = (int)pPal->g[ii] - g;
int db = (int)pPal->b[ii] - b;
int diff = dr*dr + dg*dg + db*db;
if (diff < bestDiff) {
bestDiff = diff;
bestInd = ii;
}
}
return bestInd;
}
// simple median-cut palette generation
static void GifMakePalette(const uint8_t* lastFrame, const uint8_t* nextFrame,
uint32_t width, uint32_t height,
int bitDepth, bool buildForDither,
GifPalette* pPal) {
pPal->bitDepth = bitDepth;
// histogram
int numColors = 1 << bitDepth;
// Build a simple palette from the image data
// For simplicity we use a fixed 6-6-6 color cube for index 1+
// and reserve index 0 for transparency
// 6x6x6 = 216 colors, fits in 8-bit palette (256 entries, index 0 = transparent)
int ri = 0, gi = 0, bi = 0;
for (int i = 1; i < numColors && i < 217; ++i) {
pPal->r[i] = (uint8_t)(ri * 51);
pPal->g[i] = (uint8_t)(gi * 51);
pPal->b[i] = (uint8_t)(bi * 51);
ri++;
if (ri >= 6) { ri = 0; gi++; }
if (gi >= 6) { gi = 0; bi++; }
}
// fill remaining with greyscale
for (int i = 217; i < numColors; ++i) {
int v = (i - 217) * 256 / (numColors - 217);
pPal->r[i] = pPal->g[i] = pPal->b[i] = (uint8_t)v;
}
// index 0 is transparent (set to black)
pPal->r[0] = pPal->g[0] = pPal->b[0] = 0;
(void)lastFrame; (void)nextFrame; (void)width; (void)height; (void)buildForDither;
}
// ----- LZW Compression -----
static const int kGifMaxCodeLen = 12;
struct GifLzwNode {
uint16_t m_next[256];
};
static void GifWriteCode(FILE* f, uint32_t& curCode, uint32_t& curCodeSize,
uint8_t* buf, uint32_t& bufLen,
uint32_t code) {
curCode |= code << curCodeSize;
curCodeSize += (uint32_t)(code >= (1ul << (curCodeSize - 1)) ? 1 : 0);
// flush byte
while (curCodeSize >= 8) {
buf[bufLen++] = (uint8_t)(curCode & 0xff);
curCode >>= 8;
curCodeSize -= 8;
if (bufLen == 255) {
fputc(255, f);
fwrite(buf, 1, 255, f);
bufLen = 0;
}
}
}
// Full LZW encoder
static void GifLzwCompress(FILE* f, const uint8_t* image, uint32_t width, uint32_t height,
int bitDepth, int transparent) {
int clearCode = 1 << bitDepth;
int eofCode = clearCode + 1;
int maxCode = eofCode + 1;
int codeSize = bitDepth + 1;
fputc(bitDepth, f); // LZW minimum code size
// allocate trie
GifLzwNode* codetree = (GifLzwNode*)calloc(4096, sizeof(GifLzwNode));
uint32_t curCode = 0, curCodeSize = (uint32_t)codeSize;
uint8_t buf[256];
uint32_t bufLen = 0;
// emit clear code
curCode |= (uint32_t)clearCode << curCodeSize;
curCodeSize += (uint32_t)codeSize;
while (curCodeSize >= 8) {
buf[bufLen++] = (uint8_t)(curCode & 0xff);
curCode >>= 8; curCodeSize -= 8;
if (bufLen == 255) { fputc(255, f); fwrite(buf, 1, 255, f); bufLen = 0; }
}
int curEntry = -1;
for (uint32_t yy = 0; yy < height; ++yy) {
for (uint32_t xx = 0; xx < width; ++xx) {
const uint8_t* pixel = image + (yy * width + xx) * 4;
uint8_t alpha = pixel[3];
int nextValue;
if (alpha < 128) {
nextValue = kGifTransIndex; // transparent
} else {
// find best palette entry (skip index 0 which is transparent)
int r = pixel[0], g = pixel[1], b = pixel[2];
// Simple 6-6-6 cube lookup
int ri2 = r / 51; if (ri2 > 5) ri2 = 5;
int gi2 = g / 51; if (gi2 > 5) gi2 = 5;
int bi2 = b / 51; if (bi2 > 5) bi2 = 5;
nextValue = 1 + ri2 + gi2 * 6 + bi2 * 36;
if (nextValue >= (1 << bitDepth)) nextValue = (1 << bitDepth) - 1;
if (nextValue == kGifTransIndex) nextValue = 1;
}
if (curEntry < 0) {
curEntry = nextValue;
continue;
}
if (codetree[curEntry].m_next[nextValue]) {
curEntry = codetree[curEntry].m_next[nextValue];
} else {
// emit code for curEntry
int emitCode = curEntry;
curCode |= (uint32_t)emitCode << curCodeSize;
curCodeSize += (uint32_t)codeSize;
while (curCodeSize >= 8) {
buf[bufLen++] = (uint8_t)(curCode & 0xff);
curCode >>= 8; curCodeSize -= 8;
if (bufLen == 255) { fputc(255, f); fwrite(buf, 1, 255, f); bufLen = 0; }
}
if (maxCode == (1 << codeSize)) codeSize++;
if (maxCode == 4095) {
// emit clear code to reset
curCode |= (uint32_t)clearCode << curCodeSize;
curCodeSize += (uint32_t)codeSize;
while (curCodeSize >= 8) {
buf[bufLen++] = (uint8_t)(curCode & 0xff);
curCode >>= 8; curCodeSize -= 8;
if (bufLen == 255) { fputc(255, f); fwrite(buf, 1, 255, f); bufLen = 0; }
}
// reset trie
memset(codetree, 0, 4096 * sizeof(GifLzwNode));
maxCode = eofCode + 1;
codeSize = bitDepth + 1;
} else {
codetree[curEntry].m_next[nextValue] = (uint16_t)maxCode++;
}
curEntry = nextValue;
}
}
}
// emit final code
if (curEntry >= 0) {
curCode |= (uint32_t)curEntry << curCodeSize;
curCodeSize += (uint32_t)codeSize;
while (curCodeSize >= 8) {
buf[bufLen++] = (uint8_t)(curCode & 0xff);
curCode >>= 8; curCodeSize -= 8;
if (bufLen == 255) { fputc(255, f); fwrite(buf, 1, 255, f); bufLen = 0; }
}
}
// emit EOF
curCode |= (uint32_t)eofCode << curCodeSize;
curCodeSize += (uint32_t)codeSize;
while (curCodeSize >= 8) {
buf[bufLen++] = (uint8_t)(curCode & 0xff);
curCode >>= 8; curCodeSize -= 8;
if (bufLen == 255) { fputc(255, f); fwrite(buf, 1, 255, f); bufLen = 0; }
}
// flush remaining bits
if (curCodeSize > 0) {
buf[bufLen++] = (uint8_t)(curCode & 0xff);
}
if (bufLen > 0) {
fputc((int)bufLen, f);
fwrite(buf, 1, bufLen, f);
}
fputc(0, f); // block terminator
free(codetree);
(void)transparent;
}
// ----- GIF Writer -----
struct GifWriter {
FILE* f;
uint32_t width;
uint32_t height;
int bitDepth;
bool firstFrame;
int transparentIndex; // palette index reserved for transparency (-1 = none)
};
static bool GifBegin(GifWriter* writer, const char* filename,
uint32_t width, uint32_t height,
uint32_t delay,
int bitDepth = 8,
bool dither = false) {
writer->f = fopen(filename, "wb");
if (!writer->f) return false;
writer->width = width;
writer->height = height;
writer->bitDepth = bitDepth;
writer->firstFrame = true;
writer->transparentIndex = kGifTransIndex;
// GIF header
fputs("GIF89a", writer->f);
// Logical screen descriptor
uint16_t w16 = (uint16_t)width;
uint16_t h16 = (uint16_t)height;
fwrite(&w16, 2, 1, writer->f);
fwrite(&h16, 2, 1, writer->f);
// Global color table flag=0 (we use local), color res=7, sort=0, size=0
fputc(0x70, writer->f); // packed field: no global CT
fputc(0, writer->f); // background color index
fputc(0, writer->f); // pixel aspect ratio
// Netscape loop extension (loop forever)
fputc(0x21, writer->f); // Extension introducer
fputc(0xff, writer->f); // Application extension
fputc(11, writer->f); // block size
fputs("NETSCAPE2.0", writer->f);
fputc(3, writer->f); // sub-block size
fputc(1, writer->f); // sub-block ID
fputc(0, writer->f); // loop count low byte (0 = infinite)
fputc(0, writer->f); // loop count high byte
fputc(0, writer->f); // block terminator
(void)delay; (void)dither;
return true;
}
static bool GifWriteFrame(GifWriter* writer, const uint8_t* image,
uint32_t width, uint32_t height,
uint32_t delay,
int bitDepth = 8,
bool dither = false) {
if (!writer->f) return false;
FILE* f = writer->f;
GifPalette pal;
GifMakePalette(nullptr, image, width, height, bitDepth, dither, &pal);
// Graphic Control Extension (for transparency + delay)
fputc(0x21, f); // extension introducer
fputc(0xf9, f); // graphic control label
fputc(0x04, f); // block size
// packed: reserved=0, disposal=1(leave in place), user input=0, transparent=1
fputc(0x05, f); // disposal=1, transparent flag=1
uint16_t delay16 = (uint16_t)delay;
fwrite(&delay16, 2, 1, f); // delay in 1/100 sec units
fputc((uint8_t)kGifTransIndex, f); // transparent color index
fputc(0, f); // block terminator
// Image descriptor
fputc(0x2c, f); // image separator
uint16_t zero = 0;
fwrite(&zero, 2, 1, f); // left
fwrite(&zero, 2, 1, f); // top
uint16_t w16 = (uint16_t)width;
uint16_t h16 = (uint16_t)height;
fwrite(&w16, 2, 1, f);
fwrite(&h16, 2, 1, f);
// local color table: flag=1, interlace=0, sort=0, reserved=0, size=(bitDepth-1)
fputc(0x80 | (bitDepth - 1), f);
// write palette
for (int ii = 0; ii < (1 << bitDepth); ++ii) {
fputc(pal.r[ii], f);
fputc(pal.g[ii], f);
fputc(pal.b[ii], f);
}
// compress image data
GifLzwCompress(f, image, width, height, bitDepth, kGifTransIndex);
writer->firstFrame = false;
(void)dither;
return true;
}
static bool GifEnd(GifWriter* writer) {
if (!writer->f) return false;
fputc(0x3b, writer->f); // GIF trailer
fclose(writer->f);
writer->f = nullptr;
return true;
}
#endif // GIF_H
+99
View File
@@ -0,0 +1,99 @@
#include "gif_encoder.h"
#include "gif.h"
#include <QFileInfo>
#include <QDir>
bool GifEncoder::encode(const QVector<QImage>& frames,
const QString& filePath,
int fps,
bool enableTransparency)
{
if (frames.isEmpty()) {
m_errorString = "No frames to encode";
return false;
}
// GIF delay 单位是 1/100 秒
// fps=15 -> delay = 100/15 ≈ 7 (70ms)
uint32_t delay = (fps > 0) ? (uint32_t)(100 / fps) : 7;
if (delay < 1) delay = 1;
// 确保目录存在
QDir dir = QFileInfo(filePath).dir();
if (!dir.exists()) {
dir.mkpath(".");
}
// 以第一帧的尺寸作为 GIF 尺寸
const QImage& first = frames.first();
uint32_t width = (uint32_t)first.width();
uint32_t height = (uint32_t)first.height();
if (width == 0 || height == 0) {
m_errorString = "Invalid frame size";
return false;
}
GifWriter writer;
memset(&writer, 0, sizeof(writer));
if (!GifBegin(&writer, filePath.toLocal8Bit().constData(),
width, height, delay, 8, false)) {
m_errorString = QString("Cannot open file: %1").arg(filePath);
return false;
}
for (const QImage& frame : frames) {
// 确保图像尺寸一致(裁剪或缩放到第一帧尺寸)
QImage scaled = frame;
if ((uint32_t)scaled.width() != width || (uint32_t)scaled.height() != height) {
scaled = scaled.scaled((int)width, (int)height, Qt::IgnoreAspectRatio,
Qt::SmoothTransformation);
}
QVector<uint8_t> rgba = toRGBA(scaled, enableTransparency);
GifWriteFrame(&writer, rgba.constData(), width, height, delay, 8, false);
}
GifEnd(&writer);
m_errorString.clear();
return true;
}
QVector<uint8_t> GifEncoder::toRGBA(const QImage& img, bool enableTransparency) const
{
// 统一转换为 RGBA8888 格式
QImage rgba = img.convertToFormat(QImage::Format_RGBA8888);
int w = rgba.width();
int h = rgba.height();
QVector<uint8_t> data(w * h * 4);
for (int y = 0; y < h; ++y) {
const uint8_t* src = rgba.constScanLine(y);
uint8_t* dst = data.data() + y * w * 4;
if (enableTransparency) {
for (int x = 0; x < w; ++x) {
uint8_t a = src[x * 4 + 3];
if (a < 128) {
// 透明像素:alpha 强制为 0,RGB 任意(gif.h 会映射到透明索引)
dst[x * 4 + 0] = 0;
dst[x * 4 + 1] = 0;
dst[x * 4 + 2] = 0;
dst[x * 4 + 3] = 0;
} else {
dst[x * 4 + 0] = src[x * 4 + 0]; // R
dst[x * 4 + 1] = src[x * 4 + 1]; // G
dst[x * 4 + 2] = src[x * 4 + 2]; // B
dst[x * 4 + 3] = 255;
}
}
} else {
memcpy(dst, src + 0, (size_t)(w * 4));
}
}
return data;
}
+38
View File
@@ -0,0 +1,38 @@
#ifndef GIF_ENCODER_H
#define GIF_ENCODER_H
#include <QVector>
#include <QImage>
#include <QString>
// GifEncoder
// ----------
// 将 QImage 帧序列编码为 GIF 文件。
// - 支持透明通道(alpha < 128 的像素映射到 GIF 透明索引)
// - 编码使用 gif.h 单头文件库(6x6x6 色彩量化 + LZW 压缩)
// - encode() 是同步操作,建议在子线程(QtConcurrent)调用
class GifEncoder
{
public:
GifEncoder() = default;
// fps: 帧率(如 15)
// enableTransparency: 是否将半透明像素处理为透明
// 返回 true 表示成功,errorString() 返回错误信息
bool encode(const QVector<QImage>& frames,
const QString& filePath,
int fps,
bool enableTransparency = true);
QString errorString() const { return m_errorString; }
private:
// 将 QImage 转为 gif.h 需要的 RGBA8888 原始字节数组
// 如果 enableTransparency=true,alpha<128 的像素 alpha 强制为 0
QVector<uint8_t> toRGBA(const QImage& img, bool enableTransparency) const;
QString m_errorString;
};
#endif // GIF_ENCODER_H
+63
View File
@@ -0,0 +1,63 @@
#include "gif_frame_grabber.h"
#include <QScreen>
#include <QGuiApplication>
#include <QPixmap>
GifFrameGrabber::GifFrameGrabber(QObject* parent)
: QObject(parent)
{
connect(&m_timer, &QTimer::timeout, this, &GifFrameGrabber::onTimerTick);
}
void GifFrameGrabber::start(const QRect& captureRect, int fps)
{
if (captureRect.isEmpty() || fps <= 0) return;
m_rect = captureRect;
int intervalMs = 1000 / fps;
m_timer.start(intervalMs);
}
void GifFrameGrabber::stop()
{
m_timer.stop();
}
void GifFrameGrabber::updateRect(const QRect& captureRect)
{
if (!captureRect.isEmpty())
m_rect = captureRect;
}
void GifFrameGrabber::onTimerTick()
{
QScreen* screen = QGuiApplication::primaryScreen();
if (!screen) return;
// 抓取全屏后裁剪到选区(与 DesktopSnapshot 一致,HiDPI 安全)
QPixmap fullScreen = screen->grabWindow(0);
if (fullScreen.isNull()) return;
// 将逻辑坐标转为物理像素坐标(处理 HiDPI)
qreal dpr = screen->devicePixelRatio();
QRect physicalRect(
qRound(m_rect.x() * dpr),
qRound(m_rect.y() * dpr),
qRound(m_rect.width() * dpr),
qRound(m_rect.height() * dpr)
);
QImage fullImage = fullScreen.toImage();
QImage frame = fullImage.copy(physicalRect);
if (!frame.isNull()) {
// 缩回逻辑分辨率,确保所有帧尺寸一致
if (dpr != 1.0) {
frame = frame.scaled(m_rect.width(), m_rect.height(),
Qt::IgnoreAspectRatio,
Qt::SmoothTransformation);
}
emit frameCaptured(frame);
}
}
+43
View File
@@ -0,0 +1,43 @@
#ifndef GIF_FRAME_GRABBER_H
#define GIF_FRAME_GRABBER_H
#include <QObject>
#include <QTimer>
#include <QRect>
#include <QImage>
// GifFrameGrabber
// ---------------
// 使用 QTimer 按固定帧率从屏幕抓取指定区域的帧。
// 每帧抓取后发出 frameCaptured 信号。
// 线程:运行在主线程(QScreen::grabWindow 必须在主线程调用)。
class GifFrameGrabber : public QObject
{
Q_OBJECT
public:
explicit GifFrameGrabber(QObject* parent = nullptr);
// 开始抓帧:captureRect 为逻辑坐标选区,fps 为帧率
void start(const QRect& captureRect, int fps);
// 录制中实时更新抓取区域(下一帧起生效,线程安全)
void updateRect(const QRect& captureRect);
// 停止抓帧
void stop();
bool isRunning() const { return m_timer.isActive(); }
signals:
void frameCaptured(const QImage& frame);
private slots:
void onTimerTick();
private:
QTimer m_timer;
QRect m_rect;
};
#endif // GIF_FRAME_GRABBER_H
+52
View File
@@ -0,0 +1,52 @@
#include "language_manager.h"
#include <QCoreApplication>
#include <QProcess>
#include <QDebug>
LanguageManager::LanguageManager(AppSettings &settings, QObject *parent)
: QObject(parent), m_settings(settings)
{
// 启动时加载翻译
loadTranslator(m_settings.language());
}
QString LanguageManager::language() const
{
return m_settings.language();
}
void LanguageManager::setLanguage(const QString &lang)
{
if (m_settings.language() == lang) return;
m_settings.setLanguage(lang);
emit languageChanged(lang);
restartApp();
}
void LanguageManager::restartApp()
{
// 用当前可执行文件路径和参数重新启动,然后退出当前进程
QProcess::startDetached(QCoreApplication::applicationFilePath(),
QCoreApplication::arguments());
QCoreApplication::quit();
}
void LanguageManager::loadTranslator(const QString &lang)
{
// 中文是源语言:不加载翻译文件,qsTr() 直接返回中文源字符串
if (lang == "zh_CN") return;
m_translator = new QTranslator(this);
QString qmFile = QString(":/i18n/app_%1.qm").arg(lang);
if (!m_translator->load(qmFile)) {
qmFile = QString("app_%1.qm").arg(lang);
if (!m_translator->load(qmFile, QCoreApplication::applicationDirPath())) {
qWarning() << "[LanguageManager] 翻译文件未找到:" << qmFile;
delete m_translator;
m_translator = nullptr;
return;
}
}
QCoreApplication::installTranslator(m_translator);
}
+38
View File
@@ -0,0 +1,38 @@
#ifndef LANGUAGE_MANAGER_H
#define LANGUAGE_MANAGER_H
#include <QObject>
#include <QTranslator>
#include "model/app_settings.h"
// LanguageManager
// ───────────────
// 启动时根据 AppSettings 中保存的语言加载对应翻译文件。
// setLanguage() 只持久化设置,重启后生效。
// 暴露给 QML 为 O_LanguageManager。
class LanguageManager : public QObject
{
Q_OBJECT
Q_PROPERTY(QString language READ language NOTIFY languageChanged)
public:
explicit LanguageManager(AppSettings &settings, QObject *parent = nullptr);
QString language() const;
// 保存新语言并立即重启应用
Q_INVOKABLE void setLanguage(const QString &lang);
signals:
void languageChanged(const QString &lang);
private:
void loadTranslator(const QString &lang);
static void restartApp();
AppSettings &m_settings;
QTranslator *m_translator = nullptr;
};
#endif // LANGUAGE_MANAGER_H
+93
View File
@@ -0,0 +1,93 @@
#include "magnifier_provider.h"
#include <QPainter>
MagnifierProvider::MagnifierProvider(ScreenshotViewModel *viewModel)
: QQuickImageProvider(QQuickImageProvider::Image)
, m_viewModel(viewModel)
{
}
// requestImage
// ------------
// id 格式:"<x>,<y>"(由 QML 传入的逻辑像素鼠标坐标)
QImage MagnifierProvider::requestImage(const QString &id, QSize *size, const QSize & /*requestedSize*/)
{
const QString cleanId = id.section('?', 0, 0);
// 解析坐标
const QStringList parts = cleanId.split(',');
if (parts.size() < 2) {
QImage blank(kOutputSize, kOutputSize, QImage::Format_ARGB32_Premultiplied);
blank.fill(Qt::black);
if (size) *size = blank.size();
return blank;
}
bool okX = false;
bool okY = false;
const int cx = parts[0].toInt(&okX);
const int cy = parts[1].toInt(&okY);
if (!okX || !okY) {
QImage blank(kOutputSize, kOutputSize, QImage::Format_ARGB32_Premultiplied);
blank.fill(Qt::black);
if (size) *size = blank.size();
return blank;
}
// 从桌面快照中取一份线程安全的副本
const QImage snapshot = m_viewModel->desktopSnapshot();
if (snapshot.isNull()) {
QImage blank(kOutputSize, kOutputSize, QImage::Format_ARGB32_Premultiplied);
blank.fill(Qt::black);
if (size) *size = blank.size();
return blank;
}
// 逻辑坐标 → 快照像素坐标(按虚拟桌面逻辑尺寸与快照物理像素比例换算,支持多屏)
const QRect vg = m_viewModel->virtualGeometry();
const qreal scaleX = (vg.width() > 0) ? (qreal(snapshot.width()) / qreal(vg.width())) : 1.0;
const qreal scaleY = (vg.height() > 0) ? (qreal(snapshot.height()) / qreal(vg.height())) : 1.0;
const int pcx = qRound((cx - vg.x()) * scaleX);
const int pcy = qRound((cy - vg.y()) * scaleY);
// 采样半径按 X 轴比例缩放;Y 轴保持一致比例,保证中心点准确
const int srcRadiusX = qMax(1, qRound(kSrcRadius * scaleX));
const int srcRadiusY = qMax(1, qRound(kSrcRadius * scaleY));
const int srcDiamW = srcRadiusX * 2;
const int srcDiamH = srcRadiusY * 2;
// 计算裁剪矩形(以物理像素光标为中心)
QRect srcRect(pcx - srcRadiusX, pcy - srcRadiusY, srcDiamW, srcDiamH);
// 处理边界:当光标靠近屏幕边缘时,用黑色填充越界区域
QImage region(srcDiamW, srcDiamH, QImage::Format_ARGB32_Premultiplied);
region.fill(Qt::black);
{
QPainter rp(&region);
QRect bounded = srcRect.intersected(snapshot.rect());
if (!bounded.isEmpty()) {
int destX = bounded.x() - srcRect.x();
int destY = bounded.y() - srcRect.y();
rp.drawImage(destX, destY, snapshot, bounded.x(), bounded.y(),
bounded.width(), bounded.height());
}
rp.end();
}
// 放大到 kOutputSize×kOutputSize
QImage zoomed = region.scaled(kOutputSize, kOutputSize, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
// 绘制红色十字准线(1px 宽)
{
QPainter p(&zoomed);
p.setRenderHint(QPainter::Antialiasing, false);
QPen pen(QColor(255, 0, 0, 220), 1);
p.setPen(pen);
const int half = kOutputSize / 2;
p.drawLine(half, 0, half, kOutputSize - 1); // 竖线
p.drawLine(0, half, kOutputSize - 1, half); // 横线
p.end();
}
if (size) *size = zoomed.size();
return zoomed;
}
+36
View File
@@ -0,0 +1,36 @@
#ifndef MAGNIFIER_PROVIDER_H
#define MAGNIFIER_PROVIDER_H
#include <QQuickImageProvider>
#include "viewmodel/screenshot_view_model.h"
// MagnifierProvider
// -----------------
// QQuickImageProvider 子类,供 QML Image 通过
// source: "image://magnifier/<x>,<y>"
// 获取以 (x,y) 为中心的放大镜图像。
//
// 图像规格:
// 输出尺寸:110×110(QML 侧再用 width/height 撑到 130×130 预留边距)
// 采样区域:以 (x,y) 为中心、半径 40 像素(共 80×80)从桌面快照裁剪
// 放大倍率:~1.375x(110/80)
// 十字准线:红色,绘制在 requestImage() 内部
//
// 线程安全:requestImage() 可能在非主线程调用,只对 QImage 做只读 copy()。
class MagnifierProvider : public QQuickImageProvider
{
public:
explicit MagnifierProvider(ScreenshotViewModel *viewModel);
// QQuickImageProvider interface
QImage requestImage(const QString &id, QSize *size, const QSize &requestedSize) override;
private:
ScreenshotViewModel *m_viewModel;
static const int kSrcRadius = 20; // 采样半径(源区域 40×40)
static const int kOutputSize = 110; // 输出图像尺寸
};
#endif // MAGNIFIER_PROVIDER_H
+83
View File
@@ -0,0 +1,83 @@
#include <QApplication>
#include <QQmlApplicationEngine>
#include <QQmlContext>
#include "tray_icon_helper.h"
#include "paint_board/paint_board.h"
#include "shortcut_key/globalshortcut.h"
#include "magnifier_provider.h"
#include "sticky_image_store.h"
#include "sticky_image_provider_proxy.h"
#include "language_manager.h"
// Model
#include "model/desktop_snapshot.h"
#include "model/app_settings.h"
// ViewModel
#include "viewmodel/screenshot_view_model.h"
#include "viewmodel/sticky_view_model.h"
#include "viewmodel/ai_view_model.h"
#include "viewmodel/storage_view_model.h"
#include "viewmodel/gif_record_view_model.h"
#include "viewmodel/ocr_view_model.h"
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
QCoreApplication::setOrganizationName("TZshot");
QCoreApplication::setOrganizationDomain("tzshot.local");
QCoreApplication::setApplicationName("TZshot");
app.setQuitOnLastWindowClosed(false);
// ── Model 层 ──────────────────────────────────────────
DesktopSnapshot desktopSnapshot;
AppSettings appSettings;
StickyImageStore stickyImageStore;
// ── ViewModel 层 ──────────────────────────────────────
ScreenshotViewModel screenshotViewModel(desktopSnapshot, stickyImageStore);
StickyViewModel stickyViewModel(stickyImageStore);
AIViewModel aiViewModel(appSettings, stickyImageStore);
StorageViewModel storageViewModel(appSettings);
LanguageManager languageManager(appSettings);
GifRecordViewModel gifRecordViewModel(appSettings);
OcrViewModel ocrViewModel;
// ── 其他服务(不属于 MVVM 任何层)───────────────────
GlobalShortcut globalShortcut;
QQmlApplicationEngine engine;
// ── Qt 基础设施注册 ───────────────────────────────────
engine.addImportPath("qrc:/qml");
qmlRegisterType<PaintBoard>("CustomComponents", 1, 0, "PaintBoard");
qmlRegisterType<TrayIconHelper>("com.Tz.tray", 1, 0, "TrayIconHelper");
// MagnifierProvider 持有 ScreenshotViewModel 引用访问快照,engine 接管生命周期
engine.addImageProvider("magnifier", new MagnifierProvider(&screenshotViewModel));
// StickyImageProviderProxy 转发到 StickyImageStore,engine 接管生命周期
engine.addImageProvider("sticky", new StickyImageProviderProxy(stickyImageStore));
// ── 暴露给 QML 的 ViewModel ───────────────────────────
engine.rootContext()->setContextProperty("O_ScreenCapture", &screenshotViewModel);
engine.rootContext()->setContextProperty("O_StickyViewModel", &stickyViewModel);
engine.rootContext()->setContextProperty("O_AIViewModel", &aiViewModel);
engine.rootContext()->setContextProperty("O_ImageSaver", &storageViewModel);
engine.rootContext()->setContextProperty("O_GlobalShortcut", &globalShortcut);
engine.rootContext()->setContextProperty("O_LanguageManager", &languageManager);
engine.rootContext()->setContextProperty("O_GifRecordVM", &gifRecordViewModel);
engine.rootContext()->setContextProperty("O_OcrVM", &ocrViewModel);
const QUrl url(QStringLiteral("qrc:/qml/main.qml"));
QObject::connect(&engine, &QQmlApplicationEngine::objectCreated,
&app, [url](QObject *obj, const QUrl &objUrl) {
if (!obj && url == objUrl)
QCoreApplication::exit(-1);
}, Qt::QueuedConnection);
engine.load(url);
return app.exec();
}
+102
View File
@@ -0,0 +1,102 @@
#include "app_settings.h"
#include <QSettings>
#include <QStandardPaths>
#include <QDir>
#include <QDebug>
#include <QLocale>
AppSettings::AppSettings()
{
QSettings s;
m_apiKey = s.value("AI/apiKey", "").toString();
m_selectedModel = s.value("AI/selectedModel", 0).toInt();
// 语言:若用户从未手动设置过,则根据系统语言选择默认值
if (s.contains("App/language")) {
m_language = s.value("App/language").toString();
} else {
// QLocale::system().name() 返回形如 "zh_CN"、"en_US" 等
QString sysLang = QLocale::system().name(); // e.g. "zh_CN", "en_US"
if (sysLang.startsWith("zh")) {
m_language = "zh_CN";
} else {
m_language = "en";
}
}
QString savedPath = s.value("ImageSaver/savePath").toString();
if (!savedPath.isEmpty() && QDir(savedPath).exists()) {
m_savePath = savedPath;
} else {
QString picPath = QStandardPaths::writableLocation(QStandardPaths::PicturesLocation);
m_savePath = QDir(picPath).filePath("TZshot");
QDir dir;
if (!dir.exists(m_savePath)) {
if (!dir.mkpath(m_savePath)) {
qWarning() << "[AppSettings] 创建默认保存路径失败,回退到图片根目录";
m_savePath = picPath;
}
}
}
qInfo() << "[AppSettings] 保存路径:" << m_savePath;
}
// ── AI 配置 ───────────────────────────────────────────────
QString AppSettings::apiKey() const
{
return m_apiKey;
}
void AppSettings::setApiKey(const QString &key)
{
if (m_apiKey == key) return;
m_apiKey = key;
QSettings().setValue("AI/apiKey", m_apiKey);
}
int AppSettings::selectedModel() const
{
return m_selectedModel;
}
void AppSettings::setSelectedModel(int index)
{
if (m_selectedModel == index) return;
m_selectedModel = index;
QSettings().setValue("AI/selectedModel", m_selectedModel);
}
// ── 语言配置 ──────────────────────────────────────────────
QString AppSettings::language() const
{
return m_language;
}
void AppSettings::setLanguage(const QString &lang)
{
if (m_language == lang) return;
m_language = lang;
QSettings().setValue("App/language", m_language);
}
// ── 保存路径 ──────────────────────────────────────────────
QString AppSettings::savePath() const
{
return m_savePath;
}
void AppSettings::setSavePath(const QString &path)
{
if (m_savePath == path || path.isEmpty()) return;
m_savePath = path;
QDir dir;
if (!dir.exists(m_savePath))
dir.mkpath(m_savePath);
QSettings().setValue("ImageSaver/savePath", m_savePath);
}
+40
View File
@@ -0,0 +1,40 @@
#ifndef APP_SETTINGS_H
#define APP_SETTINGS_H
#include <QString>
#include <QUrl>
// AppSettings — Model
// -------------------
// 负责应用配置数据的读写(QSettings 持久化)。
// 不继承 QObject,不依赖 QML,纯数据存取。
// 所有 setter 在值变更时自动写入 QSettings。
class AppSettings
{
public:
AppSettings();
// AI 配置
QString apiKey() const;
void setApiKey(const QString &key);
int selectedModel() const;
void setSelectedModel(int index);
// 语言配置("zh_CN" 或 "en")
QString language() const;
void setLanguage(const QString &lang);
// 截图保存路径
QString savePath() const;
void setSavePath(const QString &path);
private:
QString m_apiKey;
int m_selectedModel = 0;
QString m_language = "zh_CN";
QString m_savePath;
};
#endif // APP_SETTINGS_H
+250
View File
@@ -0,0 +1,250 @@
#include "desktop_snapshot.h"
#include <QGuiApplication>
#include <QScreen>
#include <QPixmap>
#include <QPainter>
#include <QTimer>
#include <QEventLoop>
#include <QDebug>
/* ── grab / release / pixelColor / windowAtPoint ────────
* 这几个函数与平台无关,统一实现
* ────────────────────────────────────────────────────── */
void DesktopSnapshot::grab()
{
const QList<QScreen *> screens = QGuiApplication::screens();
if (screens.isEmpty()) {
qWarning() << "[DesktopSnapshot] 无法获取屏幕列表";
return;
}
// 等待合成器/DWM 将截图窗口从帧缓冲移除
QEventLoop loop;
QTimer::singleShot(50, &loop, &QEventLoop::quit);
loop.exec();
// 用 QScreen::virtualGeometry() 获取每块屏幕在虚拟桌面坐标系中的位置。
// virtualGeometry() 使用主屏 DPR 作为基准(与 QML Screen.virtualX/width 对齐),
// 避免混合 DPI 场景下各屏 geometry() 坐标系不一致的问题。
m_virtualGeometry = QRect();
for (QScreen *s : screens)
m_virtualGeometry = m_virtualGeometry.united(s->virtualGeometry());
qDebug() << "[DesktopSnapshot] 检测到屏幕数:" << screens.size()
<< "虚拟桌面逻辑矩形:" << m_virtualGeometry;
// 创建与虚拟桌面等大的物理像素画布(以主屏 DPR 为基准)
// 主屏 DPR 即虚拟桌面坐标系的缩放基准,与 QML 侧一致
const qreal baseDpr = QGuiApplication::primaryScreen()
? QGuiApplication::primaryScreen()->devicePixelRatio()
: 1.0;
const int canvasW = qRound(m_virtualGeometry.width() * baseDpr);
const int canvasH = qRound(m_virtualGeometry.height() * baseDpr);
m_snapshot = QImage(canvasW, canvasH, QImage::Format_ARGB32_Premultiplied);
m_snapshot.fill(Qt::black);
// 逐屏抓图并合并到画布
QPainter painter(&m_snapshot);
for (QScreen *s : screens) {
QImage img = s->grabWindow(0).toImage();
// 该屏在虚拟桌面坐标系下的位置(已对齐主屏 DPR)
const int logOffX = s->virtualGeometry().x() - m_virtualGeometry.x();
const int logOffY = s->virtualGeometry().y() - m_virtualGeometry.y();
// 转换为物理像素偏移
const int physOffX = qRound(logOffX * baseDpr);
const int physOffY = qRound(logOffY * baseDpr);
// 将该屏快照缩放到 baseDpr 下的等效物理像素尺寸
const int physW = qRound(s->virtualGeometry().width() * baseDpr);
const int physH = qRound(s->virtualGeometry().height() * baseDpr);
if (img.width() != physW || img.height() != physH)
img = img.scaled(physW, physH, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
painter.drawImage(physOffX, physOffY, img);
}
painter.end();
qDebug() << "[DesktopSnapshot] 快照已缓存,尺寸:"
<< m_snapshot.width() << "x" << m_snapshot.height();
buildWindowList();
}
void DesktopSnapshot::release()
{
m_snapshot = QImage();
m_virtualGeometry = QRect();
m_windowRects.clear();
}
QColor DesktopSnapshot::pixelColor(int logicalX, int logicalY) const
{
if (m_snapshot.isNull() || m_virtualGeometry.isEmpty()) return QColor();
// 逻辑坐标相对于虚拟桌面原点的偏移
const int relX = logicalX - m_virtualGeometry.x();
const int relY = logicalY - m_virtualGeometry.y();
// 按快照实际尺寸与虚拟桌面逻辑尺寸比例换算(统一 DPR)
const qreal scaleX = m_virtualGeometry.width() > 0
? qreal(m_snapshot.width()) / qreal(m_virtualGeometry.width()) : 1.0;
const qreal scaleY = m_virtualGeometry.height() > 0
? qreal(m_snapshot.height()) / qreal(m_virtualGeometry.height()) : 1.0;
const int px = qRound(relX * scaleX);
const int py = qRound(relY * scaleY);
if (px < 0 || py < 0 || px >= m_snapshot.width() || py >= m_snapshot.height())
return QColor();
return QColor(m_snapshot.pixel(px, py));
}
QRect DesktopSnapshot::windowAtPoint(int x, int y) const
{
for (const QRect &r : m_windowRects) {
if (r.contains(x, y))
return r;
}
return QRect();
}
/* ══════════════════════════════════════════════════════════
* buildWindowList — 平台专属实现
* ══════════════════════════════════════════════════════════ */
/* ────────────────────────────────────────────────────────
* Linux / Unix:通过 _NET_CLIENT_LIST_STACKING 枚举窗口
* ──────────────────────────────────────────────────────── */
#ifdef Q_OS_UNIX
#include <X11/Xlib.h>
#include <X11/Xatom.h>
void DesktopSnapshot::buildWindowList()
{
m_windowRects.clear();
Display *dpy = XOpenDisplay(nullptr);
if (!dpy) return;
Window root = DefaultRootWindow(dpy);
Atom netClientListStacking = XInternAtom(dpy, "_NET_CLIENT_LIST_STACKING", False);
Atom actualType;
int actualFormat;
unsigned long nitems = 0, bytesAfter = 0;
unsigned char *data = nullptr;
int ret = XGetWindowProperty(dpy, root, netClientListStacking,
0, 65536, False, XA_WINDOW,
&actualType, &actualFormat,
&nitems, &bytesAfter, &data);
if (ret != Success || !data) {
Atom netClientList = XInternAtom(dpy, "_NET_CLIENT_LIST", False);
ret = XGetWindowProperty(dpy, root, netClientList,
0, 65536, False, XA_WINDOW,
&actualType, &actualFormat,
&nitems, &bytesAfter, &data);
if (ret != Success || !data) {
XCloseDisplay(dpy);
return;
}
}
Window *wins = reinterpret_cast<Window*>(data);
for (long i = static_cast<long>(nitems) - 1; i >= 0; --i) {
Window w = wins[i];
XWindowAttributes attr;
if (!XGetWindowAttributes(dpy, w, &attr)) continue;
if (attr.map_state != IsViewable) continue;
if (attr.width < 10) continue;
if (attr.height < 10) continue;
int screenX = 0, screenY = 0;
Window child;
XTranslateCoordinates(dpy, w, root, 0, 0, &screenX, &screenY, &child);
QRect r(screenX, screenY, attr.width, attr.height);
r = r.intersected(m_virtualGeometry);
if (r.isEmpty()) continue;
m_windowRects.append(r);
}
XFree(data);
XCloseDisplay(dpy);
qDebug() << "[DesktopSnapshot] 枚举到顶层窗口数:" << m_windowRects.size();
}
#endif /* Q_OS_UNIX */
/* ────────────────────────────────────────────────────────
* Windows:通过 EnumWindows 枚举可见顶层窗口
* ──────────────────────────────────────────────────────── */
#ifdef Q_OS_WIN
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
/* EnumWindows 回调:收集所有可见且有标题栏的顶层窗口矩形 */
struct EnumWindowsParam {
QVector<QRect> *rects;
QRect snapshotRect; /* 逻辑像素,用于裁剪 */
qreal dpr; /* 主屏 DPR,用于物理像素→逻辑像素转换 */
};
static BOOL CALLBACK enumWindowsProc(HWND hwnd, LPARAM lParam)
{
EnumWindowsParam *param = reinterpret_cast<EnumWindowsParam*>(lParam);
if (!IsWindowVisible(hwnd)) return TRUE;
if (IsIconic(hwnd)) return TRUE; /* 最小化,跳过 */
/* 跳过没有 WS_CAPTION 的工具窗口、桌面壳层等 */
LONG style = GetWindowLong(hwnd, GWL_STYLE);
if (!(style & WS_CAPTION)) return TRUE;
RECT rc;
if (!GetWindowRect(hwnd, &rc)) return TRUE;
int w = rc.right - rc.left;
int h = rc.bottom - rc.top;
if (w < 10 || h < 10) return TRUE;
/* GetWindowRect 返回物理像素坐标,转换为逻辑像素(对齐 Qt 坐标系)*/
QRect r(qRound(rc.left / param->dpr),
qRound(rc.top / param->dpr),
qRound(w / param->dpr),
qRound(h / param->dpr));
r = r.intersected(param->snapshotRect);
if (!r.isEmpty())
param->rects->append(r); /* EnumWindows 本身按 Z 序(顶->底)枚举,append 才能保持从顶到底 */
return TRUE;
}
void DesktopSnapshot::buildWindowList()
{
m_windowRects.clear();
const qreal dpr = QGuiApplication::primaryScreen()
? QGuiApplication::primaryScreen()->devicePixelRatio()
: 1.0;
EnumWindowsParam param { &m_windowRects, m_virtualGeometry, dpr };
EnumWindows(enumWindowsProc, reinterpret_cast<LPARAM>(&param));
qDebug() << "[DesktopSnapshot] 枚举到顶层窗口数:" << m_windowRects.size();
}
#endif /* Q_OS_WIN */
+46
View File
@@ -0,0 +1,46 @@
#ifndef DESKTOP_SNAPSHOT_H
#define DESKTOP_SNAPSHOT_H
#include <QImage>
#include <QRect>
#include <QVector>
#include <QColor>
// DesktopSnapshot — Model
// 负责全屏快照的采集与存储,以及顶层窗口列表的枚举。
// 支持多屏:将所有屏幕的快照拼合为一张以虚拟桌面坐标系为基准的大图。
// 不依赖 QML,不继承 QObject,纯数据/系统调用。
class DesktopSnapshot
{
public:
DesktopSnapshot() = default;
// 抓取所有屏幕快照并拼合,枚举顶层窗口列表(含 50ms 延时等待合成器)
void grab();
// 释放快照和窗口列表
void release();
bool isNull() const { return m_snapshot.isNull(); }
// 拼合后的虚拟桌面快照(物理像素坐标系)
const QImage& image() const { return m_snapshot; }
// 虚拟桌面的逻辑坐标矩形(所有屏幕的 geometry 联合,逻辑像素)
QRect virtualGeometry() const { return m_virtualGeometry; }
// 将逻辑坐标 (x,y) 转换为快照中的颜色(支持多屏 DPI 混合)
QColor pixelColor(int logicalX, int logicalY) const;
// 返回包含逻辑坐标点 (x,y) 的最上层窗口矩形(逻辑坐标)
QRect windowAtPoint(int x, int y) const;
private:
void buildWindowList();
QImage m_snapshot;
QRect m_virtualGeometry; // 虚拟桌面逻辑矩形(所有屏幕 geometry 的联合)
QVector<QRect> m_windowRects; // 按 Z 轴从顶到底排列(逻辑坐标)
};
#endif // DESKTOP_SNAPSHOT_H
+307
View File
@@ -0,0 +1,307 @@
#include "ocr_engine.h"
#ifdef ENABLE_OCR
#include <tesseract/baseapi.h>
#endif
#include <QCoreApplication>
#include <QDebug>
#include <QDir>
#include <QFileInfo>
#include <QImage>
#include <QPainter>
#include <QProcess>
#include <QStandardPaths>
#include <QStringList>
#include <QTemporaryDir>
namespace {
bool hasRequiredLanguages(const QString &tessdataDir)
{
return QFileInfo::exists(tessdataDir + "/eng.traineddata")
&& QFileInfo::exists(tessdataDir + "/chi_sim.traineddata");
}
QStringList resolveTessdataDirs()
{
const QString appDir = QCoreApplication::applicationDirPath();
QStringList candidates = {
#ifdef OCR_TESSDATA_DIR
QStringLiteral(OCR_TESSDATA_DIR),
#endif
appDir + "/thirdpart/ocr-win/share/tessdata",
appDir + "/../thirdpart/ocr-win/share/tessdata",
appDir + "/../../thirdpart/ocr-win/share/tessdata",
appDir + "/thirdpart/ocr/share/tessdata",
appDir + "/../thirdpart/ocr/share/tessdata",
appDir + "/../../thirdpart/ocr/share/tessdata",
QStringLiteral("/usr/share/tesseract-ocr/5/tessdata"),
QStringLiteral("/usr/share/tesseract-ocr/4.00/tessdata")
};
const QByteArray envPrefix = qgetenv("TESSDATA_PREFIX");
if (!envPrefix.isEmpty()) {
const QString envPath = QString::fromUtf8(envPrefix);
if (QFileInfo(envPath).fileName() == QStringLiteral("tessdata")) {
candidates.prepend(envPath);
} else {
candidates.prepend(envPath + "/tessdata");
}
}
QStringList validDirs;
for (const QString &tessdataDir : candidates) {
const QString normalized = QDir(tessdataDir).absolutePath();
if (!validDirs.contains(normalized) && hasRequiredLanguages(normalized)) {
validDirs << normalized;
}
}
return validDirs;
}
QString resolveTesseractProgram()
{
const QString appDir = QCoreApplication::applicationDirPath();
const QByteArray envProgram = qgetenv("TESSERACT_PATH");
if (!envProgram.isEmpty()) {
const QString envPath = QDir::fromNativeSeparators(QString::fromUtf8(envProgram));
if (QFileInfo::exists(envPath)) {
return QDir(envPath).absolutePath();
}
}
QStringList candidatePaths = {
appDir + "/thirdpart/ocr-win/bin",
appDir + "/../thirdpart/ocr-win/bin",
appDir + "/../../thirdpart/ocr-win/bin",
appDir + "/thirdpart/ocr/bin",
appDir + "/../thirdpart/ocr/bin",
appDir + "/../../thirdpart/ocr/bin"
};
#ifdef Q_OS_WIN
candidatePaths << QStringLiteral("C:/Program Files/Tesseract-OCR")
<< QStringLiteral("C:/Program Files (x86)/Tesseract-OCR");
const QString localAppData = QDir::fromNativeSeparators(
QString::fromUtf8(qgetenv("LOCALAPPDATA"))
);
if (!localAppData.isEmpty()) {
candidatePaths << localAppData + "/Programs/Tesseract-OCR";
}
#endif
const QStringList executableNames = {
#ifdef Q_OS_WIN
QStringLiteral("tesseract.exe"),
#endif
QStringLiteral("tesseract")
};
for (const QString &name : executableNames) {
const QString resolved = QStandardPaths::findExecutable(name, candidatePaths);
if (!resolved.isEmpty()) {
return QDir::fromNativeSeparators(resolved);
}
const QString fromPath = QStandardPaths::findExecutable(name);
if (!fromPath.isEmpty()) {
return QDir::fromNativeSeparators(fromPath);
}
}
return {};
}
} // namespace
OcrEngine::OcrEngine()
{
#ifdef ENABLE_OCR
m_api = new tesseract::TessBaseAPI();
#ifdef Q_OS_WIN
m_api->SetVariable("debug_file", "NUL");
#else
m_api->SetVariable("debug_file", "/dev/null");
#endif
const QStringList tessdataDirs = resolveTessdataDirs();
bool initOk = false;
QString selectedTessdataDir;
for (const QString &tessdataDir : tessdataDirs) {
m_api->End();
qputenv("TESSDATA_PREFIX", tessdataDir.toUtf8());
if (m_api->Init(nullptr, "chi_sim+eng") == 0) {
initOk = true;
selectedTessdataDir = tessdataDir;
break;
}
}
if (!initOk) {
if (tessdataDirs.isEmpty()) {
m_lastError = QStringLiteral("Tesseract Init 失败(chi_sim+eng):未找到可用 tessdata");
} else {
m_lastError = QStringLiteral("Tesseract Init 失败(chi_sim+eng),候选目录=%1")
.arg(tessdataDirs.join(QStringLiteral("; ")));
}
qWarning() << "[OcrEngine]" << m_lastError;
delete m_api;
m_api = nullptr;
return;
}
qInfo() << "[OcrEngine] Tesseract Init success, tessdata=" << selectedTessdataDir;
m_api->SetPageSegMode(tesseract::PSM_AUTO);
m_lastError.clear();
m_ready = true;
#else
m_tesseractProgram = resolveTesseractProgram();
if (m_tesseractProgram.isEmpty()) {
m_lastError = QStringLiteral("OCR 未启用:未找到 tesseract 可执行文件。请安装 Tesseract,或设置 TESSERACT_PATH 指向可执行文件");
m_ready = false;
return;
}
const QStringList tessdataDirs = resolveTessdataDirs();
if (!tessdataDirs.isEmpty()) {
m_tessdataDir = tessdataDirs.first();
}
qInfo() << "[OcrEngine] CLI fallback enabled, tesseract=" << m_tesseractProgram
<< "tessdata=" << (m_tessdataDir.isEmpty() ? QStringLiteral("<system default>") : m_tessdataDir);
m_lastError.clear();
m_ready = true;
#endif
}
OcrEngine::~OcrEngine()
{
#ifdef ENABLE_OCR
if (m_api) {
m_api->End();
delete m_api;
m_api = nullptr;
}
#endif
}
QString OcrEngine::recognize(const QImage &image)
{
#ifdef ENABLE_OCR
if (!m_ready || !m_api) {
m_lastError = QStringLiteral("OCR 引擎未就绪");
return {};
}
if (image.isNull()) {
m_lastError = QStringLiteral("输入图像为空");
return {};
}
QImage rgb = image.convertToFormat(QImage::Format_RGB888);
if (rgb.width() < 2 || rgb.height() < 2) {
m_lastError = QStringLiteral("输入图像尺寸过小");
return {};
}
// 为图像加白边,降低文本贴边时 Leptonica 产生无效 box 警告的概率。
constexpr int kPad = 10;
QImage padded(rgb.width() + kPad * 2, rgb.height() + kPad * 2, QImage::Format_RGB888);
padded.fill(Qt::white);
{
QPainter painter(&padded);
painter.drawImage(kPad, kPad, rgb);
}
m_api->SetSourceResolution(300);
m_api->SetImage(
padded.constBits(),
padded.width(),
padded.height(),
3,
padded.bytesPerLine());
char *rawText = m_api->GetUTF8Text();
if (!rawText) {
m_lastError = QStringLiteral("Tesseract GetUTF8Text 返回 null");
return {};
}
const QString result = QString::fromUtf8(rawText).trimmed();
delete[] rawText;
m_api->Clear();
m_lastError.clear();
return result;
#else
if (!m_ready || m_tesseractProgram.isEmpty()) {
if (m_lastError.isEmpty()) {
m_lastError = QStringLiteral("OCR 未就绪:未找到 tesseract 可执行文件");
}
return {};
}
if (image.isNull()) {
m_lastError = QStringLiteral("输入图像为空");
return {};
}
QTemporaryDir tempDir;
if (!tempDir.isValid()) {
m_lastError = QStringLiteral("OCR 临时目录创建失败");
return {};
}
const QString inputPath = tempDir.path() + QStringLiteral("/ocr_input.png");
if (!image.save(inputPath, "PNG")) {
m_lastError = QStringLiteral("OCR 临时图片保存失败");
return {};
}
QStringList args;
args << inputPath << QStringLiteral("stdout") << QStringLiteral("-l") << QStringLiteral("chi_sim+eng");
if (!m_tessdataDir.isEmpty()) {
args << QStringLiteral("--tessdata-dir") << m_tessdataDir;
}
QProcess process;
process.start(m_tesseractProgram, args);
if (!process.waitForStarted(5000)) {
m_lastError = QStringLiteral("OCR 启动失败:无法启动 tesseract 进程");
return {};
}
if (!process.waitForFinished(30000)) {
process.kill();
m_lastError = QStringLiteral("OCR 超时:tesseract 执行超过 30 秒");
return {};
}
const QByteArray stdOut = process.readAllStandardOutput();
const QByteArray stdErr = process.readAllStandardError();
if (process.exitStatus() != QProcess::NormalExit || process.exitCode() != 0) {
const QString errText = QString::fromLocal8Bit(stdErr).trimmed();
m_lastError = errText.isEmpty()
? QStringLiteral("OCR 失败:tesseract 返回非零退出码")
: QStringLiteral("OCR 失败:%1").arg(errText);
return {};
}
const QString result = QString::fromUtf8(stdOut).trimmed();
if (result.isEmpty()) {
const QString errText = QString::fromLocal8Bit(stdErr).trimmed();
m_lastError = errText.isEmpty()
? QStringLiteral("OCR 未识别到文本")
: QStringLiteral("OCR 未识别到文本:%1").arg(errText);
return {};
}
m_lastError.clear();
return result;
#endif
}
+32
View File
@@ -0,0 +1,32 @@
#ifndef OCR_ENGINE_H
#define OCR_ENGINE_H
#include <QImage>
#include <QString>
namespace tesseract {
class TessBaseAPI;
}
class OcrEngine
{
public:
OcrEngine();
~OcrEngine();
// Synchronous OCR recognition, called from a worker thread.
// Returns trimmed text on success; empty string on failure.
QString recognize(const QImage &image);
QString lastError() const { return m_lastError; }
bool isReady() const { return m_ready; }
private:
tesseract::TessBaseAPI *m_api = nullptr;
bool m_ready = false;
QString m_lastError;
QString m_tesseractProgram;
QString m_tessdataDir;
};
#endif // OCR_ENGINE_H
+255
View File
@@ -0,0 +1,255 @@
#include "paint_board.h"
#include "shape/mosaic_shape.h"
#include <QDebug>
PaintBoard::PaintBoard(QQuickItem *parent)
: QQuickPaintedItem(parent)
{
setAcceptedMouseButtons(Qt::LeftButton);
}
void PaintBoard::setBackgroundImg(QImage img)
{
m_background_img = img;
update();
}
void PaintBoard::addTextAnnotation(int x, int y, const QString& text)
{
const QString resolvedText = text.trimmed().isEmpty() ? m_annotationText : text;
Shape* shape = m_factory.createTextShape(
QPoint(x, y),
resolvedText,
m_currentColor,
m_currentSize,
m_textBackgroundEnabled
);
if (!shape)
return;
m_shapes.append(shape);
update();
}
void PaintBoard::setShapeType(int type)
{
const auto t = static_cast<Shapeype>(type);
if (m_currentType == t)
return;
m_currentType = t;
emit shapeTypeChanged(type);
}
void PaintBoard::setPenColor(const QColor &color)
{
if (m_currentColor == color)
return;
m_currentColor = color;
emit penColorChanged(color);
}
void PaintBoard::setPenSize(int size)
{
if (m_currentSize == size)
return;
m_currentSize = size;
emit penSizeChanged(size);
}
void PaintBoard::setAnnotationText(const QString& text)
{
if (m_annotationText == text)
return;
m_annotationText = text;
emit annotationTextChanged(m_annotationText);
}
void PaintBoard::setTextBackgroundEnabled(bool enabled)
{
if (m_textBackgroundEnabled == enabled)
return;
m_textBackgroundEnabled = enabled;
emit textBackgroundEnabledChanged(enabled);
}
void PaintBoard::setMosaicBlurLevel(int level)
{
const int normalized = qBound(1, level, 4);
if (m_mosaicBlurLevel == normalized)
return;
m_mosaicBlurLevel = normalized;
emit mosaicBlurLevelChanged(m_mosaicBlurLevel);
}
void PaintBoard::setNumberAutoIncrement(bool enabled)
{
if (m_numberAutoIncrement == enabled)
return;
m_numberAutoIncrement = enabled;
emit numberAutoIncrementChanged(m_numberAutoIncrement);
}
void PaintBoard::setNumberValue(int value)
{
const int normalized = qBound(1, value, 9999);
if (m_numberValue == normalized)
return;
m_numberValue = normalized;
emit numberValueChanged(m_numberValue);
}
void PaintBoard::undo()
{
if (!m_shapes.isEmpty()) {
delete m_shapes.last();
m_shapes.removeLast();
update();
}
}
void PaintBoard::reset()
{
qDeleteAll(m_shapes);
m_shapes.clear();
delete m_currentShape;
m_currentShape = nullptr;
m_drawing = false;
m_background_img = QImage();
update();
}
QImage PaintBoard::renderToImage() const
{
if (m_shapes.isEmpty())
return QImage();
QImage img(qRound(width()), qRound(height()), QImage::Format_ARGB32);
img.fill(Qt::transparent);
QPainter painter(&img);
painter.setRenderHint(QPainter::Antialiasing);
for (Shape* shape : m_shapes) {
if (shape)
shape->draw(&painter);
}
painter.end();
return img;
}
void PaintBoard::paint(QPainter *painter)
{
drawBkImg(painter, m_background_img);
drawShape(painter);
}
void PaintBoard::mousePressEvent(QMouseEvent *event)
{
m_startPoint = event->position().toPoint();
m_drawing = true;
if (m_currentType == TEXT) {
// Text insertion is handled by TZWindow popup flow.
m_drawing = false;
return;
}
if (m_currentType == NUMBER) {
const int drawNumber = m_numberValue;
if (m_numberAutoIncrement)
setNumberValue(m_numberValue + 1);
m_currentShape = m_factory.createNumberShape(
m_startPoint,
drawNumber,
m_currentColor,
m_currentSize
);
m_shapes.append(m_currentShape);
m_currentShape = nullptr;
m_drawing = false;
update();
return;
}
if (m_currentType == MOSAIC) {
const int blurScale = qMax(1, m_mosaicBlurLevel);
auto* mosaicShape = new MosaicShape(
m_startPoint,
m_currentSize * 5 * blurScale,
qMax(2, m_currentSize * blurScale)
);
QImage snapshot = m_background_img;
qDebug() << "[PaintBoard][MOSAIC] press at" << m_startPoint
<< "snapshotNull=" << snapshot.isNull()
<< "snapshotSize=" << snapshot.size()
<< "penSize=" << m_currentSize
<< "blurLevel=" << m_mosaicBlurLevel;
mosaicShape->setCanvasSnapshot(snapshot);
m_currentShape = mosaicShape;
return;
}
m_currentShape = m_factory.createShape(
m_currentType,
m_startPoint,
m_startPoint,
m_currentColor,
m_currentSize
);
}
void PaintBoard::mouseMoveEvent(QMouseEvent *event)
{
if (!m_drawing || !m_currentShape)
return;
m_currentShape->setEndPoint(event->position().toPoint());
update();
}
void PaintBoard::mouseReleaseEvent(QMouseEvent *event)
{
if (!m_drawing || !m_currentShape)
return;
m_currentShape->setEndPoint(event->position().toPoint());
m_shapes.append(m_currentShape);
m_currentShape = nullptr;
m_drawing = false;
update();
}
void PaintBoard::hoverMoveEvent(QHoverEvent *event)
{
Q_UNUSED(event)
}
bool PaintBoard::drawBkImg(QPainter *painter, QImage img)
{
if (!painter || img.isNull())
return false;
QRectF drawRect = img.rect();
drawRect.moveCenter(boundingRect().center());
painter->drawImage(drawRect, img);
return true;
}
bool PaintBoard::drawShape(QPainter *painter)
{
if (!painter)
return false;
for (Shape* shape : m_shapes) {
if (shape)
shape->draw(painter);
}
if (m_currentShape)
m_currentShape->draw(painter);
return true;
}
+103
View File
@@ -0,0 +1,103 @@
#ifndef PAINTBOARD_H
#define PAINTBOARD_H
#include <QObject>
#include <QQuickPaintedItem>
#include <QPainter>
#include <QColor>
#include <QString>
#include <memory>
#include "shape/shape.h"
#include "shape/shape_type.h"
#include "shape/shape_factory.h"
#include "shape/mosaic_shape.h"
class PaintBoard : public QQuickPaintedItem
{
Q_OBJECT
Q_PROPERTY(int shapeType READ shapeType WRITE setShapeType NOTIFY shapeTypeChanged FINAL)
Q_PROPERTY(QColor penColor READ penColor WRITE setPenColor NOTIFY penColorChanged FINAL)
Q_PROPERTY(int penSize READ penSize WRITE setPenSize NOTIFY penSizeChanged FINAL)
Q_PROPERTY(QString annotationText READ annotationText WRITE setAnnotationText NOTIFY annotationTextChanged FINAL)
Q_PROPERTY(bool textBackgroundEnabled READ textBackgroundEnabled WRITE setTextBackgroundEnabled NOTIFY textBackgroundEnabledChanged FINAL)
Q_PROPERTY(int mosaicBlurLevel READ mosaicBlurLevel WRITE setMosaicBlurLevel NOTIFY mosaicBlurLevelChanged FINAL)
Q_PROPERTY(bool numberAutoIncrement READ numberAutoIncrement WRITE setNumberAutoIncrement NOTIFY numberAutoIncrementChanged FINAL)
Q_PROPERTY(int numberValue READ numberValue WRITE setNumberValue NOTIFY numberValueChanged FINAL)
public:
explicit PaintBoard(QQuickItem *parent = nullptr);
int shapeType() const { return static_cast<int>(m_currentType); }
QColor penColor() const { return m_currentColor; }
int penSize() const { return m_currentSize; }
QString annotationText() const { return m_annotationText; }
bool textBackgroundEnabled() const { return m_textBackgroundEnabled; }
int mosaicBlurLevel() const { return m_mosaicBlurLevel; }
bool numberAutoIncrement() const { return m_numberAutoIncrement; }
int numberValue() const { return m_numberValue; }
void setShapeType(int type);
void setPenColor(const QColor &color);
void setPenSize(int size);
void setAnnotationText(const QString& text);
void setTextBackgroundEnabled(bool enabled);
void setMosaicBlurLevel(int level);
void setNumberAutoIncrement(bool enabled);
void setNumberValue(int value);
public:
Q_INVOKABLE void setBackgroundImg(QImage img);
Q_INVOKABLE bool hasBackgroundImg() const { return !m_background_img.isNull(); }
Q_INVOKABLE void addTextAnnotation(int x, int y, const QString& text);
// Render all annotations onto a transparent image for screenshot merge.
Q_INVOKABLE QImage renderToImage() const;
Q_INVOKABLE void undo();
Q_INVOKABLE void reset(); // Clear all shapes and background image.
signals:
void shapeTypeChanged(int shapeType);
void penColorChanged(QColor penColor);
void penSizeChanged(int penSize);
void annotationTextChanged(const QString& text);
void textBackgroundEnabledChanged(bool enabled);
void mosaicBlurLevelChanged(int level);
void numberAutoIncrementChanged(bool enabled);
void numberValueChanged(int value);
protected:
void paint(QPainter *painter) override;
void mousePressEvent(QMouseEvent *event) override;
void mouseMoveEvent(QMouseEvent *event) override;
void mouseReleaseEvent(QMouseEvent *event) override;
void hoverMoveEvent(QHoverEvent *event) override;
bool drawBkImg(QPainter *painter, QImage img);
bool drawShape(QPainter *painter);
private:
ShapeFactory m_factory;
QList<Shape*> m_shapes;
Shape* m_currentShape = nullptr;
bool m_drawing = false;
QPoint m_startPoint;
Shapeype m_currentType = RECTANGLE;
QColor m_currentColor = Qt::red;
int m_currentSize = 6;
QString m_annotationText = QStringLiteral("Text");
bool m_textBackgroundEnabled = true;
int m_mosaicBlurLevel = 2;
bool m_numberAutoIncrement = true;
int m_numberValue = 1;
QImage m_background_img;
};
#endif // PAINTBOARD_H
+48
View File
@@ -0,0 +1,48 @@
#include "arrow_shape.h"
ArrowShape::ArrowShape(const QPoint &startPoint, const QPoint &endPoint,
const QColor &color, int size)
: TwoPointShape(startPoint, endPoint, color, size) {}
void ArrowShape::draw(QPainter *painter)
{
if (!painter) return;
// 起点和终点相同时不绘制
if (m_startPoint == m_endPoint) return;
painter->save();
QPen pen(m_color, m_size);
pen.setCapStyle(Qt::RoundCap);
pen.setJoinStyle(Qt::RoundJoin);
painter->setPen(pen);
painter->setRenderHint(QPainter::Antialiasing);
// 绘制箭杆
painter->drawLine(m_startPoint, m_endPoint);
// 计算箭头方向角
const double angle = std::atan2(
m_endPoint.y() - m_startPoint.y(),
m_endPoint.x() - m_startPoint.x()
);
// 计算箭头两侧的点
QPoint arrowP1(
m_endPoint.x() - static_cast<int>(std::cos(angle + ARROW_HEAD_ANGLE) * ARROW_HEAD_LENGTH),
m_endPoint.y() - static_cast<int>(std::sin(angle + ARROW_HEAD_ANGLE) * ARROW_HEAD_LENGTH)
);
QPoint arrowP2(
m_endPoint.x() - static_cast<int>(std::cos(angle - ARROW_HEAD_ANGLE) * ARROW_HEAD_LENGTH),
m_endPoint.y() - static_cast<int>(std::sin(angle - ARROW_HEAD_ANGLE) * ARROW_HEAD_LENGTH)
);
// 绘制实心箭头头部
QPolygon arrowHead;
arrowHead << m_endPoint << arrowP1 << arrowP2;
painter->setBrush(QBrush(m_color));
painter->drawPolygon(arrowHead);
painter->restore();
}
+22
View File
@@ -0,0 +1,22 @@
#ifndef ARROWSHAPE_H
#define ARROWSHAPE_H
#include "two_point_shape.h"
#include <cmath>
class ArrowShape : public TwoPointShape
{
public:
ArrowShape(const QPoint& startPoint = QPoint(0, 0),
const QPoint& endPoint = QPoint(0, 0),
const QColor& color = Qt::black,
int size = 2);
void draw(QPainter* painter) override;
private:
static constexpr double ARROW_HEAD_LENGTH = 15.0; // 箭头头部长度
static constexpr double ARROW_HEAD_ANGLE = M_PI / 6.0; // 箭头张角 30°
};
#endif // ARROWSHAPE_H
+19
View File
@@ -0,0 +1,19 @@
#include "ellipse_shape.h"
EllipseShape::EllipseShape(const QPoint &startPoint, const QPoint &endPoint,
const QColor &color, int size)
: TwoPointShape(startPoint, endPoint, color, size) {}
void EllipseShape::draw(QPainter *painter)
{
if (!painter) return;
painter->save();
QPen pen(m_color, m_size);
painter->setPen(pen);
painter->setBrush(Qt::NoBrush);
painter->setRenderHint(QPainter::Antialiasing);
// 用两点构造外接矩形来绘制椭圆,normalized() 处理反向拖动
painter->drawEllipse(QRect(m_startPoint, m_endPoint).normalized());
painter->restore();
}
+17
View File
@@ -0,0 +1,17 @@
#ifndef CIRCLESHAPE_H
#define CIRCLESHAPE_H
#include "two_point_shape.h"
class EllipseShape : public TwoPointShape
{
public:
EllipseShape(const QPoint& startPoint = QPoint(0, 0),
const QPoint& endPoint = QPoint(0, 0),
const QColor& color = Qt::black,
int size = 2);
void draw(QPainter* painter) override;
};
#endif // CIRCLESHAPE_H
+115
View File
@@ -0,0 +1,115 @@
#include "mosaic_shape.h"
#include <QPainter>
MosaicShape::MosaicShape(const QPoint& startPoint,
int brushRadius,
int blockSize)
: Shape(Qt::transparent, blockSize)
, m_brushRadius(brushRadius)
, m_blockSize(blockSize)
{
m_points.append(startPoint);
// 注意:此时还没有画布快照,需要在 setCanvasSnapshot 后才能真正应用马赛克
}
void MosaicShape::setCanvasSnapshot(const QImage& snapshot)
{
if (snapshot.isNull())
return;
// 转换为 ARGB32 格式以便快速像素访问
if (snapshot.format() != QImage::Format_ARGB32) {
m_canvasSnapshot = snapshot.convertToFormat(QImage::Format_ARGB32);
} else {
m_canvasSnapshot = snapshot;
}
// 如果有待处理的点,现在应用马赛克
if (!m_points.isEmpty() && m_blocks.isEmpty()) {
for (const QPoint& point : m_points) {
applyMosaicAt(point);
}
}
}
void MosaicShape::setEndPoint(const QPoint& point)
{
if (!m_points.isEmpty() && m_points.last() == point)
return;
m_points.append(point);
// 只有在有画布快照的情况下才应用马赛克
if (!m_canvasSnapshot.isNull()) {
applyMosaicAt(point);
}
}
void MosaicShape::draw(QPainter* painter)
{
if (!painter || m_blocks.isEmpty())
return;
painter->save();
painter->setPen(Qt::NoPen);
// 只绘制马赛克色块,不绘制任何背景像素,避免覆盖画板上其他图形
for (auto it = m_blocks.constBegin(); it != m_blocks.constEnd(); ++it) {
const QPoint& blockOrigin = it.key();
const QColor& color = it.value();
QRect blockRect(blockOrigin.x(), blockOrigin.y(), m_blockSize, m_blockSize);
// 无需裁剪到背景图范围,因为我们不再依赖背景图
// 但为了保证安全,可以检查坐标是否为正
if (blockOrigin.x() < 0 || blockOrigin.y() < 0)
continue;
painter->fillRect(blockRect, color);
}
painter->restore();
}
void MosaicShape::applyMosaicAt(const QPoint& center)
{
if (m_canvasSnapshot.isNull())
return;
int r = m_brushRadius;
int bs = qMax(1, m_blockSize);
int imgW = m_canvasSnapshot.width();
int imgH = m_canvasSnapshot.height();
// 遍历圆形区域内所有马赛克块,步长为 bs
int startX = ((center.x() - r) / bs) * bs;
int startY = ((center.y() - r) / bs) * bs;
int endX = center.x() + r;
int endY = center.y() + r;
for (int bx = startX; bx <= endX; bx += bs) {
for (int by = startY; by <= endY; by += bs) {
// 判断此块中心是否在笔刷圆形内
int cx = bx + bs / 2;
int cy = by + bs / 2;
int dx = cx - center.x();
int dy = cy - center.y();
if (dx * dx + dy * dy > r * r)
continue;
int x0 = qMax(0, bx);
int y0 = qMax(0, by);
if (x0 >= imgW || y0 >= imgH)
continue;
QPoint blockKey(bx, by);
// 已处理过的块不重复取色,保留首次取色
if (m_blocks.contains(blockKey))
continue;
// 从画布快照取块内左上角像素作为代表色
QRgb representative = m_canvasSnapshot.pixel(x0, y0);
m_blocks.insert(blockKey, QColor(representative));
}
}
}
+49
View File
@@ -0,0 +1,49 @@
#ifndef MOSAICSHAPE_H
#define MOSAICSHAPE_H
#include "shape.h"
#include <QImage>
#include <QVector>
#include <QHash>
#include <QPoint>
#include <QColor>
// QPoint 作为 QHash key 需要 qHash 支持
inline uint qHash(const QPoint& p, uint seed = 0)
{
return qHash(QPair<int,int>(p.x(), p.y()), seed);
}
// 马赛克图形:沿鼠标路径对当前画布做像素化处理
// 不再依赖预生成的底图,而是实时从当前画布读取像素
class MosaicShape : public Shape
{
public:
// blockSize: 马赛克块的像素大小(笔刷半径区域内每块的边长)
MosaicShape(const QPoint& startPoint,
int brushRadius = 20,
int blockSize = 10);
void setEndPoint(const QPoint& point) override;
void draw(QPainter* painter) override;
// 设置当前画布的快照,用于马赛克取色
// 每次绘制前由 PaintBoard 调用
void setCanvasSnapshot(const QImage& snapshot);
private:
// 将 center 为中心、半径 m_brushRadius 的圆形区域内所有块写入 m_blocks
void applyMosaicAt(const QPoint& center);
// key: 块左上角坐标(以 blockSize 对齐),value: 该块的代表色
// 只存已被马赛克处理的块,draw() 时仅绘制这些色块
QHash<QPoint, QColor> m_blocks;
QImage m_canvasSnapshot; // 当前画布快照,实时获取
QVector<QPoint> m_points; // 路径点列表
int m_brushRadius; // 笔刷半径(像素)
int m_blockSize; // 每个马赛克块的边长(像素)
};
#endif // MOSAICSHAPE_H
+44
View File
@@ -0,0 +1,44 @@
#include "number_shape.h"
#include <QFont>
#include <QFontMetrics>
NumberShape::NumberShape(const QPoint& center,
int number,
const QColor& color,
int size)
: Shape(color, size)
, m_center(center)
, m_number(qMax(1, number))
{
}
void NumberShape::setEndPoint(const QPoint& point)
{
m_center = point;
}
void NumberShape::draw(QPainter* painter)
{
if (!painter) return;
painter->save();
painter->setRenderHint(QPainter::Antialiasing, true);
painter->setRenderHint(QPainter::TextAntialiasing, true);
const int r = qMax(10, m_size * 2 + 6);
const QRect circleRect(m_center.x() - r, m_center.y() - r, r * 2, r * 2);
painter->setPen(QPen(Qt::white, qMax(2, m_size / 2)));
painter->setBrush(m_color);
painter->drawEllipse(circleRect);
QFont f = painter->font();
f.setBold(true);
f.setPixelSize(qMax(12, r));
painter->setFont(f);
painter->setPen(Qt::white);
painter->drawText(circleRect, Qt::AlignCenter, QString::number(m_number));
painter->restore();
}
+22
View File
@@ -0,0 +1,22 @@
#ifndef NUMBER_SHAPE_H
#define NUMBER_SHAPE_H
#include "shape.h"
class NumberShape : public Shape
{
public:
NumberShape(const QPoint& center,
int number,
const QColor& color,
int size);
void setEndPoint(const QPoint& point) override;
void draw(QPainter* painter) override;
private:
QPoint m_center;
int m_number = 1;
};
#endif // NUMBER_SHAPE_H
+39
View File
@@ -0,0 +1,39 @@
#include "pen_shape.h"
PenShape::PenShape(const QPoint &startPoint, const QColor &color, int size)
: Shape(color, size)
{
m_points.append(startPoint);
}
void PenShape::addPoint(const QPoint &point)
{
// 避免重复添加相同的点
if (!m_points.isEmpty() && m_points.last() == point)
return;
m_points.append(point);
}
void PenShape::setEndPoint(const QPoint &point)
{
addPoint(point);
}
void PenShape::draw(QPainter *painter)
{
if (!painter || m_points.size() < 2)
return;
painter->save();
QPen pen(m_color, m_size);
pen.setCapStyle(Qt::RoundCap); // 端点圆滑
pen.setJoinStyle(Qt::RoundJoin); // 转折点圆滑
painter->setPen(pen);
painter->setRenderHint(QPainter::Antialiasing);
// 将所有点连成折线
painter->drawPolyline(QPolygon(m_points));
painter->restore();
}
+27
View File
@@ -0,0 +1,27 @@
#ifndef PENSHAPE_H
#define PENSHAPE_H
#include "shape.h"
#include <QList>
// 画笔图形:记录鼠标经过的所有点,连线绘制
class PenShape : public Shape
{
public:
PenShape(const QPoint& startPoint,
const QColor& color = Qt::black,
int size = 2);
// 追加一个路径点
void addPoint(const QPoint& point);
// 实现 Shape 纯虚函数,内部调用 addPoint
void setEndPoint(const QPoint& point) override;
void draw(QPainter* painter) override;
private:
QVector<QPoint> m_points; // 路径上所有点
};
#endif // PENSHAPE_H
+19
View File
@@ -0,0 +1,19 @@
#include "rect_shape.h"
RectShape::RectShape(const QPoint &startPoint, const QPoint &endPoint,
const QColor &color, int size)
: TwoPointShape(startPoint, endPoint, color, size) {}
void RectShape::draw(QPainter *painter)
{
if (!painter) return;
painter->save();
QPen pen(m_color, m_size);
painter->setPen(pen);
painter->setBrush(Qt::NoBrush);
painter->setRenderHint(QPainter::Antialiasing);
// normalized() 处理从右下往左上拖动的情况
painter->drawRect(QRect(m_startPoint, m_endPoint).normalized());
painter->restore();
}
+16
View File
@@ -0,0 +1,16 @@
#ifndef RECTSHAPE_H
#define RECTSHAPE_H
#include "two_point_shape.h"
class RectShape : public TwoPointShape
{
public:
RectShape(const QPoint& startPoint = QPoint(0, 0),
const QPoint& endPoint = QPoint(0, 0),
const QColor& color = Qt::black,
int size = 2);
void draw(QPainter* painter) override;
};
#endif // RECTSHAPE_H
+5
View File
@@ -0,0 +1,5 @@
#include "shape.h"
Shape::Shape(const QColor &color, int size)
: m_color(color), m_size(size) {}
+29
View File
@@ -0,0 +1,29 @@
#ifndef SHAPE_H
#define SHAPE_H
#include <QPainter>
#include <QColor>
#include <QPoint>
class Shape
{
public:
Shape(const QColor& color = Qt::black, int size = 2);
virtual ~Shape() = default;
// 纯虚函数:统一的绘制接口(子类必须实现)
virtual void draw(QPainter* painter) = 0;
// 纯虚函数:更新终点(用于绘制预览)
virtual void setEndPoint(const QPoint& point) = 0;
void setColor(const QColor& color) { m_color = color; }
QColor color() const { return m_color; }
void setSize(int size) { m_size = size; }
int size() const { return m_size; }
protected:
QColor m_color;
int m_size;
};
#endif // SHAPE_H
+53
View File
@@ -0,0 +1,53 @@
#include "shape_factory.h"
#include "rect_shape.h"
#include "arrow_shape.h"
#include "ellipse_shape.h"
#include "pen_shape.h"
#include "text_shape.h"
#include "number_shape.h"
ShapeFactory::ShapeFactory() {}
Shape* ShapeFactory::createShape(Shapeype type,
const QPoint& startPoint,
const QPoint& endPoint,
const QColor& color,
int size)
{
switch (type)
{
case PEN:
// Freehand pen only needs the start point.
return new PenShape(startPoint, color, size);
case RECTANGLE:
return new RectShape(startPoint, endPoint, color, size);
case ELLIPSE:
return new EllipseShape(startPoint, endPoint, color, size);
case ARROW:
return new ArrowShape(startPoint, endPoint, color, size);
case HIGHLIGHT: {
QColor hi = color;
hi.setAlpha(100);
return new PenShape(startPoint, hi, qMax(8, size * 2));
}
default:
return nullptr;
}
}
Shape* ShapeFactory::createTextShape(const QPoint& startPoint,
const QString& text,
const QColor& color,
int size,
bool withBackground)
{
return new TextShape(startPoint, text, color, size, withBackground);
}
Shape* ShapeFactory::createNumberShape(const QPoint& center,
int number,
const QColor& color,
int size)
{
return new NumberShape(center, number, color, size);
}
+33
View File
@@ -0,0 +1,33 @@
#ifndef SHAPEFACTORY_H
#define SHAPEFACTORY_H
#include "shape.h"
#include "shape_type.h"
#include <QColor>
#include <QPoint>
#include <QString>
class ShapeFactory
{
public:
ShapeFactory();
Shape* createShape(Shapeype type,
const QPoint& startPoint,
const QPoint& endPoint,
const QColor& color,
int size);
Shape* createTextShape(const QPoint& startPoint,
const QString& text,
const QColor& color,
int size,
bool withBackground);
Shape* createNumberShape(const QPoint& center,
int number,
const QColor& color,
int size);
};
#endif // SHAPEFACTORY_H
+16
View File
@@ -0,0 +1,16 @@
#ifndef SHAPETYPE_H
#define SHAPETYPE_H
enum Shapeype
{
PEN,
RECTANGLE,
ELLIPSE,
ARROW,
MOSAIC,
TEXT,
NUMBER,
HIGHLIGHT
};
#endif // SHAPETYPE_H
+55
View File
@@ -0,0 +1,55 @@
#include "text_shape.h"
#include <QFont>
#include <QFontMetrics>
TextShape::TextShape(const QPoint& startPoint,
const QString& text,
const QColor& color,
int size,
bool withBackground)
: Shape(color, size)
, m_point(startPoint)
, m_text(text.isEmpty() ? QStringLiteral("Text") : text)
, m_withBackground(withBackground)
{
}
void TextShape::setEndPoint(const QPoint& point)
{
m_point = point;
}
void TextShape::draw(QPainter* painter)
{
if (!painter) return;
painter->save();
painter->setRenderHint(QPainter::Antialiasing, true);
painter->setRenderHint(QPainter::TextAntialiasing, true);
QFont f = painter->font();
f.setPixelSize(qMax(12, m_size * 4));
painter->setFont(f);
const QFontMetrics fm(f);
const QRect textRect = fm.boundingRect(m_text);
const int padX = qMax(4, m_size);
const int padY = qMax(2, m_size / 2);
QRect drawRect(m_point.x(),
m_point.y() - textRect.height(),
textRect.width() + padX * 2,
textRect.height() + padY * 2);
if (m_withBackground) {
QColor bg(0, 0, 0, 140);
painter->setPen(Qt::NoPen);
painter->setBrush(bg);
painter->drawRoundedRect(drawRect, 4, 4);
}
painter->setPen(m_color);
const QPoint textPos(drawRect.x() + padX, drawRect.y() + padY + fm.ascent());
painter->drawText(textPos, m_text);
painter->restore();
}
+25
View File
@@ -0,0 +1,25 @@
#ifndef TEXT_SHAPE_H
#define TEXT_SHAPE_H
#include "shape.h"
#include <QString>
class TextShape : public Shape
{
public:
TextShape(const QPoint& startPoint,
const QString& text,
const QColor& color,
int size,
bool withBackground);
void setEndPoint(const QPoint& point) override;
void draw(QPainter* painter) override;
private:
QPoint m_point;
QString m_text;
bool m_withBackground = true;
};
#endif // TEXT_SHAPE_H
+25
View File
@@ -0,0 +1,25 @@
#include "two_point_shape.h"
TwoPointShape::TwoPointShape(const QPoint &startPoint, const QPoint &endPoint,
const QColor &color, int size)
: Shape(color, size), m_startPoint(startPoint), m_endPoint(endPoint) {}
void TwoPointShape::setStartPoint(const QPoint &point)
{
m_startPoint = point;
}
QPoint TwoPointShape::startPoint() const
{
return m_startPoint;
}
void TwoPointShape::setEndPoint(const QPoint &point)
{
m_endPoint = point;
}
QPoint TwoPointShape::endPoint() const
{
return m_endPoint;
}
Loaded 100 of 131 files, more files were not shown because too many files have changed in this diff. Show more