feat: initial public release TZshot
No files matched your search
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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()
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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 & 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>
|
||||
@@ -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'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>
|
||||
@@ -0,0 +1,6 @@
|
||||
<RCC>
|
||||
<qresource prefix="/i18n">
|
||||
<file>app_zh_CN.qm</file>
|
||||
<file>app_en.qm</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
@@ -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>
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
module TZControls
|
||||
TZComboBox 1.0 TZComboBox.qml
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 ───────────────────────────────────────────
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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));
|
||||
}
|
||||
@@ -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 ¶ms = 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 ¶ms) = 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
|
||||
@@ -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
|
||||
@@ -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 ¶ms)
|
||||
{
|
||||
// 参数校验
|
||||
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 ¶ms)
|
||||
{
|
||||
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;
|
||||
}
|
||||
@@ -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 ¶ms = QJsonObject()) override;
|
||||
|
||||
// 设置API地址(默认OpenAI官方地址,可替换为代理地址)
|
||||
void setApiUrl(const QString &url);
|
||||
|
||||
public:
|
||||
QNetworkRequest buildRequest() override;
|
||||
QByteArray buildRequestBody(const QString &prompt, const QJsonObject ¶ms) override;
|
||||
QString parseResponse(const QByteArray &responseData) override;
|
||||
public:
|
||||
bool setApiKey(QString apiKey);
|
||||
private:
|
||||
QString m_apiKey; // API Key
|
||||
QString m_apiUrl;
|
||||
};
|
||||
|
||||
#endif // QWENAICALL_H
|
||||
@@ -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 ¶ms)
|
||||
{
|
||||
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 ¶ms)
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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 ¶ms = QJsonObject()) override;
|
||||
|
||||
protected:
|
||||
QNetworkRequest buildRequest() override;
|
||||
QByteArray buildRequestBody(const QString &prompt,
|
||||
const QJsonObject ¶ms) override;
|
||||
QString parseResponse(const QByteArray &responseData) override;
|
||||
|
||||
private:
|
||||
QString m_apiKey;
|
||||
QString m_apiUrl;
|
||||
QString m_model;
|
||||
};
|
||||
|
||||
#endif // SEEDREAM_AI_CALL_H
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
|
||||
@@ -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(®ion);
|
||||
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;
|
||||
}
|
||||
@@ -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
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
|
||||
@@ -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>(¶m));
|
||||
|
||||
qDebug() << "[DesktopSnapshot] 枚举到顶层窗口数:" << m_windowRects.size();
|
||||
}
|
||||
|
||||
#endif /* Q_OS_WIN */
|
||||
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -0,0 +1,5 @@
|
||||
#include "shape.h"
|
||||
|
||||
Shape::Shape(const QColor &color, int size)
|
||||
: m_color(color), m_size(size) {}
|
||||
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
|
||||
@@ -0,0 +1,16 @@
|
||||
#ifndef SHAPETYPE_H
|
||||
#define SHAPETYPE_H
|
||||
|
||||
enum Shapeype
|
||||
{
|
||||
PEN,
|
||||
RECTANGLE,
|
||||
ELLIPSE,
|
||||
ARROW,
|
||||
MOSAIC,
|
||||
TEXT,
|
||||
NUMBER,
|
||||
HIGHLIGHT
|
||||
};
|
||||
|
||||
#endif // SHAPETYPE_H
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||