Migrate capture overlay to widgets

This commit is contained in:
easyz committed 2026-03-27 20:26:57 +08:00
1 parent 3e345b0aca
commit 409fdcb91e
72 files changed
+3607 -7832

No files matched your search

+16 -27
View File
@@ -15,21 +15,13 @@ find_package(Qt6 REQUIRED COMPONENTS
Core
Gui
Widgets
Quick
Qml
Network
Concurrent
LinguistTools
QuickDialogs2
)
qt_standard_project_setup()
# Help Qt Creator's QML code model resolve local qmldir-based modules
# such as `import TZControls 1.0`.
set(QML_IMPORT_PATH "${CMAKE_CURRENT_SOURCE_DIR}/qml" CACHE STRING "Local QML import paths" FORCE)
set(QML2_IMPORT_PATH "${CMAKE_CURRENT_SOURCE_DIR}/qml" CACHE STRING "Local QML import paths" FORCE)
# ── 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)
@@ -43,23 +35,35 @@ set(TS_FILES
# ── 平台无关源文件 ────────────────────────────────────────
set(SOURCES
src/main.cpp
src/app/tzshot_app.cpp
src/app/tzshot_app.h
src/app/tzshot_services.cpp
src/app/tzshot_services.h
src/app/tzshot_widget_runtime.cpp
src/app/tzshot_widget_runtime.h
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/instance_activation.cpp
src/instance_activation.h
src/singleton.h
src/widgets/about_dialog.cpp
src/widgets/about_dialog.h
src/widgets/sticky_pin_widget.cpp
src/widgets/sticky_pin_widget.h
src/widgets/sticky_canvas_widget.cpp
src/widgets/sticky_canvas_widget.h
src/widgets/capture_overlay_widget.cpp
src/widgets/capture_overlay_widget.h
src/widgets/long_capture_controller.cpp
src/widgets/long_capture_controller.h
src/widgets/magnifier_widget.cpp
src/widgets/magnifier_widget.h
src/widgets/selection_mask_controller.cpp
src/widgets/selection_mask_controller.h
src/widgets/ocr_result_widget.cpp
src/widgets/ocr_result_widget.h
src/widgets/settings_dialog.cpp
@@ -102,9 +106,6 @@ set(SOURCES
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
@@ -165,10 +166,6 @@ qt_add_executable(TZshot
${SOURCES}
)
set_target_properties(TZshot PROPERTIES
QT_QML_IMPORT_PATH "${CMAKE_CURRENT_SOURCE_DIR}/qml"
)
# ── include 路径 ──────────────────────────────────────────
target_include_directories(TZshot PRIVATE
src
@@ -197,11 +194,8 @@ target_link_libraries(TZshot PRIVATE
Qt6::Core
Qt6::Gui
Qt6::Widgets
Qt6::Quick
Qt6::Qml
Qt6::Network
Qt6::Concurrent
Qt6::QuickDialogs2
)
# ── 平台系统库 ────────────────────────────────────────────
@@ -453,11 +447,6 @@ if(WIN32)
)
endif()
# ── QML 导入路径 ──────────────────────────────────────────
set_target_properties(TZshot PROPERTIES
QT_QML_IMPORT_PATH "${CMAKE_CURRENT_SOURCE_DIR}/qml"
)
# ── 翻译 ──────────────────────────────────────────────────
qt_add_translations(TZshot
TS_FILES ${TS_FILES}
+2 -732
View File
@@ -1,4 +1,4 @@
<?xml version='1.0' encoding='utf-8'?>
<?xml version="1.0" encoding="utf-8"?>
<TS version="2.1" language="en_US">
<context>
<name>GifRecordViewModel</name>
@@ -62,734 +62,4 @@
<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/Windows/TZChat.qml" line="83" />
<source>AI 图像编辑</source>
<translation>AI Image Edit</translation>
</message>
<message>
<location filename="../qml/Windows/TZChat.qml" line="162" />
<source>当前贴图</source>
<translation>Current Image</translation>
</message>
<message>
<location filename="../qml/Windows/TZChat.qml" line="171" />
<source>未关联贴图</source>
<translation>No Image Linked</translation>
</message>
<message>
<location filename="../qml/Windows/TZChat.qml" line="178" />
<source>发送后将对此图执行 AI 编辑</source>
<translation>AI edit will be applied to this image after sending</translation>
</message>
<message>
<location filename="../qml/Windows/TZChat.qml" line="203" />
<source>描述您想要的修改...</source>
<translation>Describe the changes you want...</translation>
</message>
<message>
<location filename="../qml/Windows/TZChat.qml" line="228" />
<source>取消</source>
<translation>Cancel</translation>
</message>
<message>
<location filename="../qml/Windows/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/Capture/TZGifRecordBar.qml" line="186" />
<source>正在生成 GIF...</source>
<translation type="unfinished" />
</message>
</context>
<context>
<name>TZHotkeySettings</name>
<message>
<location filename="../qml/Setting/TZHotkeySettings.qml" line="49" />
<source>编辑快捷键 · %1</source>
<translation>Edit Shortcut · %1</translation>
</message>
<message>
<location filename="../qml/Setting/TZHotkeySettings.qml" line="70" />
<source>请按下新的快捷键组合...</source>
<translation>Press the new shortcut key combination...</translation>
</message>
<message>
<location filename="../qml/Setting/TZHotkeySettings.qml" line="103" />
<source>取消</source>
<translation>Cancel</translation>
</message>
<message>
<location filename="../qml/Setting/TZHotkeySettings.qml" line="133" />
<source>确认</source>
<translation>Confirm</translation>
</message>
<message>
<location filename="../qml/Setting/TZHotkeySettings.qml" line="150" />
<location filename="../qml/Setting/TZHotkeySettings.qml" line="228" />
<source>截图</source>
<translation>Screenshot</translation>
</message>
<message>
<location filename="../qml/Setting/TZHotkeySettings.qml" line="150" />
<location filename="../qml/Setting/TZHotkeySettings.qml" line="229" />
<source>截图并保存</source>
<translation>Screenshot &amp; Save</translation>
</message>
<message>
<location filename="../qml/Setting/TZHotkeySettings.qml" line="150" />
<location filename="../qml/Setting/TZHotkeySettings.qml" line="230" />
<source>贴图到桌面</source>
<translation>Pin to Desktop</translation>
</message>
<message>
<location filename="../qml/Setting/TZHotkeySettings.qml" line="150" />
<location filename="../qml/Setting/TZHotkeySettings.qml" line="231" />
<source>显示/隐藏窗口</source>
<translation>Show/Hide Window</translation>
</message>
<message>
<location filename="../qml/Setting/TZHotkeySettings.qml" line="201" />
<source>全局快捷键</source>
<translation>Global Shortcuts</translation>
</message>
<message>
<location filename="../qml/Setting/TZHotkeySettings.qml" line="293" />
<source>编辑</source>
<translation>Edit</translation>
</message>
<message>
<location filename="../qml/Setting/TZHotkeySettings.qml" line="341" />
<source>说明</source>
<translation>Notes</translation>
</message>
<message>
<location filename="../qml/Setting/TZHotkeySettings.qml" line="362" />
<source>快捷键在全局范围内生效。点击「编辑」后直接按下新的组合键,再点「确认」即可保存。
若与系统或其他应用快捷键冲突,请修改为其他组合。</source>
<translation>Shortcuts work globally. Click "Edit", press the new key combination, then click "Confirm" to save.
If it conflicts with system or other app shortcuts, change to a different combination.</translation>
</message>
</context>
<context>
<name>TZMarkToolInfo</name>
<message>
<location filename="../qml/Capture/TZMarkToolInfo.qml" line="13" />
<source>文本</source>
<translation type="unfinished" />
</message>
<message>
<location filename="../qml/Capture/TZMarkToolInfo.qml" line="62" />
<source>细</source>
<translation>Thin</translation>
</message>
<message>
<location filename="../qml/Capture/TZMarkToolInfo.qml" line="63" />
<source>中</source>
<translation>Medium</translation>
</message>
<message>
<location filename="../qml/Capture/TZMarkToolInfo.qml" line="64" />
<source>粗</source>
<translation>Thick</translation>
</message>
<message>
<location filename="../qml/Capture/TZMarkToolInfo.qml" line="65" />
<source>特粗</source>
<translation>Extra Thick</translation>
</message>
<message>
<location filename="../qml/Capture/TZMarkToolInfo.qml" line="133" />
<source>输入文字</source>
<translation type="unfinished" />
</message>
<message>
<location filename="../qml/Capture/TZMarkToolInfo.qml" line="140" />
<source>背景</source>
<translation type="unfinished" />
</message>
<message>
<location filename="../qml/Capture/TZMarkToolInfo.qml" line="152" />
<source>模糊</source>
<translation type="unfinished" />
</message>
<message>
<location filename="../qml/Capture/TZMarkToolInfo.qml" line="177" />
<source>自动递增</source>
<translation type="unfinished" />
</message>
<message>
<location filename="../qml/Capture/TZMarkToolInfo.qml" line="187" />
<source>数字</source>
<translation type="unfinished" />
</message>
</context>
<context>
<name>TZOcrResult</name>
<message>
<location filename="../qml/Windows/TZOcrResult.qml" line="67" />
<source>OCR 识别结果</source>
<translation>OCR Results</translation>
</message>
<message>
<location filename="../qml/Windows/TZOcrResult.qml" line="167" />
<source>正在识别中…</source>
<translation>Recognizing…</translation>
</message>
<message>
<location filename="../qml/Windows/TZOcrResult.qml" line="190" />
<source>识别结果将显示在这里…</source>
<translation>Recognition results will appear here…</translation>
</message>
<message>
<location filename="../qml/Windows/TZOcrResult.qml" line="239" />
<source>复制全部</source>
<translation>Copy All</translation>
</message>
<message>
<location filename="../qml/Windows/TZOcrResult.qml" line="265" />
<source>关闭</source>
<translation>Close</translation>
</message>
<message>
<location filename="../qml/Windows/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/Windows/TZStickyWindow.qml" line="515" />
<source>当前透明度 %1%</source>
<translation>Opacity: %1%</translation>
</message>
<message>
<location filename="../qml/Windows/TZStickyWindow.qml" line="518" />
<location filename="../qml/Windows/TZStickyWindow.qml" line="572" />
<source>覆盖贴图(合并标注)</source>
<translation>Overwrite Image (Merge Annotations)</translation>
</message>
<message>
<location filename="../qml/Windows/TZStickyWindow.qml" line="523" />
<location filename="../qml/Windows/TZStickyWindow.qml" line="558" />
<source>显示工具栏</source>
<translation>Show Toolbar</translation>
</message>
<message>
<location filename="../qml/Windows/TZStickyWindow.qml" line="534" />
<source>透明度 +5%</source>
<translation>Opacity +5%</translation>
</message>
<message>
<location filename="../qml/Windows/TZStickyWindow.qml" line="535" />
<source>透明度 -5%</source>
<translation>Opacity -5%</translation>
</message>
<message>
<location filename="../qml/Windows/TZStickyWindow.qml" line="541" />
<source>旋转 90°</source>
<translation>Rotate 90°</translation>
</message>
<message>
<location filename="../qml/Windows/TZStickyWindow.qml" line="548" />
<source>重置位置</source>
<translation>Reset Position</translation>
</message>
<message>
<location filename="../qml/Windows/TZStickyWindow.qml" line="520" />
<location filename="../qml/Windows/TZStickyWindow.qml" line="566" />
<location filename="../qml/Windows/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/Windows/TZStickyWindow.qml" line="519" />
<location filename="../qml/Windows/TZStickyWindow.qml" line="573" />
<location filename="../qml/Windows/TZStickyWindow.qml" line="580" />
<source>复制贴图</source>
<translation>Copy Image</translation>
</message>
<message>
<location filename="../qml/Windows/TZStickyWindow.qml" line="530" />
<location filename="../qml/Windows/TZStickyWindow.qml" line="583" />
<location filename="../qml/Windows/TZStickyWindow.qml" line="588" />
<source>关闭窗口</source>
<translation>Close Window</translation>
</message>
<message>
<location filename="../qml/Windows/TZStickyWindow.qml" line="570" />
<source>保存原图</source>
<translation>Save Original Image</translation>
</message>
<message>
<location filename="../qml/Windows/TZStickyWindow.qml" line="571" />
<location filename="../qml/Windows/TZStickyWindow.qml" line="602" />
<source>另存为(合并标注)</source>
<translation>Save As (Merge Annotations)</translation>
</message>
<message>
<location filename="../qml/Windows/TZStickyWindow.qml" line="578" />
<source>常用项置顶</source>
<translation>Pin Common Items</translation>
</message>
<message>
<location filename="../qml/Windows/TZStickyWindow.qml" line="579" />
<source>覆盖贴图</source>
<translation>Overwrite Image</translation>
</message>
<message>
<location filename="../qml/Windows/TZStickyWindow.qml" line="582" />
<source>显示/隐藏工具栏</source>
<translation>Show/Hide Toolbar</translation>
</message>
<message>
<location filename="../qml/Windows/TZStickyWindow.qml" line="593" />
<source>选择保存位置</source>
<translation>Choose Save Location</translation>
</message>
<message>
<location filename="../qml/Windows/TZStickyWindow.qml" line="341" />
<source>Pencil</source>
<translation>Pencil</translation>
</message>
<message>
<location filename="../qml/Windows/TZStickyWindow.qml" line="350" />
<source>Rectangle</source>
<translation>Rectangle</translation>
</message>
<message>
<location filename="../qml/Windows/TZStickyWindow.qml" line="359" />
<source>Circle</source>
<translation>Circle</translation>
</message>
<message>
<location filename="../qml/Windows/TZStickyWindow.qml" line="368" />
<source>Arrow</source>
<translation>Arrow</translation>
</message>
<message>
<location filename="../qml/Windows/TZStickyWindow.qml" line="382" />
<location filename="../qml/Windows/TZStickyWindow.qml" line="383" />
<source>Mosaic</source>
<translation>Mosaic</translation>
</message>
<message>
<location filename="../qml/Windows/TZStickyWindow.qml" line="404" />
<location filename="../qml/Windows/TZStickyWindow.qml" line="539" />
<source>缩小</source>
<translation>Zoom Out</translation>
</message>
<message>
<location filename="../qml/Windows/TZStickyWindow.qml" line="406" />
<location filename="../qml/Windows/TZStickyWindow.qml" line="540" />
<source>放大</source>
<translation>Zoom In</translation>
</message>
<message>
<location filename="../qml/Windows/TZStickyWindow.qml" line="407" />
<source>旋转90°</source>
<translation>Rotate 90°</translation>
</message>
<message>
<location filename="../qml/Windows/TZStickyWindow.qml" line="408" />
<location filename="../qml/Windows/TZStickyWindow.qml" line="542" />
<source>镜像</source>
<translation>Mirror</translation>
</message>
<message>
<location filename="../qml/Windows/TZStickyWindow.qml" line="409" />
<location filename="../qml/Windows/TZStickyWindow.qml" line="543" />
<source>恢复原始大小</source>
<translation>Reset Size</translation>
</message>
<message>
<location filename="../qml/Windows/TZStickyWindow.qml" line="415" />
<source>完成并复制</source>
<translation>Apply and Copy</translation>
</message>
<message>
<location filename="../qml/Windows/TZStickyWindow.qml" line="422" />
<source>撤销</source>
<translation>Undo</translation>
</message>
<message>
<location filename="../qml/Windows/TZStickyWindow.qml" line="434" />
<location filename="../qml/Windows/TZStickyWindow.qml" line="435" />
<location filename="../qml/Windows/TZStickyWindow.qml" line="523" />
<location filename="../qml/Windows/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/Windows/TZStickyWindow.qml" line="324" />
<location filename="../qml/Windows/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/Capture/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/Capture/TZTool.qml" line="76" />
<source>完成</source>
<translation>Done</translation>
</message>
<message>
<location filename="../qml/Capture/TZTool.qml" line="100" />
<source>录制GIF</source>
<translation>Record GIF</translation>
</message>
<message>
<location filename="../qml/Capture/TZTool.qml" line="109" />
<source>保存</source>
<translation>Save</translation>
</message>
<message>
<location filename="../qml/Capture/TZTool.qml" line="116" />
<source>贴图</source>
<translation>Pin</translation>
</message>
<message>
<location filename="../qml/Capture/TZTool.qml" line="127" />
<source>矩形</source>
<translation>Rectangle</translation>
</message>
<message>
<location filename="../qml/Capture/TZTool.qml" line="136" />
<source>圆形</source>
<translation>Circle</translation>
</message>
<message>
<location filename="../qml/Capture/TZTool.qml" line="145" />
<source>箭头</source>
<translation>Arrow</translation>
</message>
<message>
<location filename="../qml/Capture/TZTool.qml" line="154" />
<source>画笔</source>
<translation>Pen</translation>
</message>
<message>
<location filename="../qml/Capture/TZTool.qml" line="163" />
<source>高亮</source>
<translation>Highlight</translation>
</message>
<message>
<location filename="../qml/Capture/TZTool.qml" line="172" />
<source>马赛克</source>
<translation>Mosaic</translation>
</message>
<message>
<location filename="../qml/Capture/TZTool.qml" line="181" />
<source>文字</source>
<translation>Text</translation>
</message>
<message>
<location filename="../qml/Capture/TZTool.qml" line="190" />
<source>序号</source>
<translation>Number</translation>
</message>
<message>
<location filename="../qml/Capture/TZTool.qml" line="197" />
<source>撤销</source>
<translation>Undo</translation>
</message>
<message>
<source>序号</source>
<translation>Number</translation>
</message>
<message>
<location filename="../qml/Capture/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/Capture/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/Capture/TZWindow.qml" line="134" />
<source>Capture done</source>
<translation>Capture done</translation>
</message>
<message>
<location filename="../qml/Capture/TZWindow.qml" line="134" />
<source>Copied to clipboard</source>
<translation>Copied to clipboard</translation>
</message>
<message>
<location filename="../qml/Capture/TZWindow.qml" line="136" />
<source>Capture failed</source>
<translation>Capture failed</translation>
</message>
<message>
<location filename="../qml/Capture/TZWindow.qml" line="136" />
<source>Failed to copy image</source>
<translation>Failed to copy image</translation>
</message>
<message>
<location filename="../qml/Capture/TZWindow.qml" line="434" />
<source>Select save location</source>
<translation>Select save location</translation>
</message>
<message>
<location filename="../qml/Capture/TZWindow.qml" line="442" />
<source>Save success</source>
<translation>Save success</translation>
</message>
<message>
<location filename="../qml/Capture/TZWindow.qml" line="442" />
<source>Screenshot saved to file</source>
<translation>Screenshot saved to file</translation>
</message>
<message>
<location filename="../qml/Capture/TZWindow.qml" line="445" />
<source>Save failed</source>
<translation>Save failed</translation>
</message>
<message>
<location filename="../qml/Capture/TZWindow.qml" line="445" />
<source>Failed to save screenshot file</source>
<translation>Failed to save screenshot file</translation>
</message>
<message>
<location filename="../qml/Capture/TZWindow.qml" line="705" />
<source>Cancel</source>
<translation>Cancel</translation>
</message>
<message>
<location filename="../qml/Capture/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>
</TS>
+64 -688
View File
@@ -1,691 +1,67 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE TS>
<!DOCTYPE TS []>
<TS version="2.1" language="zh_CN">
<context>
<name>GifRecordViewModel</name>
<message>
<location filename="../src/viewmodel/gif_record_view_model.cpp" line="48"/>
<source>录制区域无效</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../src/viewmodel/gif_record_view_model.cpp" line="96"/>
<source>未采集到任何帧</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../src/viewmodel/gif_record_view_model.cpp" line="150"/>
<source>GIF 编码失败,请检查保存路径或磁盘空间</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>QObject</name>
<message>
<location filename="../src/tray_icon_helper.cpp" line="8"/>
<source>截图</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../src/tray_icon_helper.cpp" line="11"/>
<source>设置</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../src/tray_icon_helper.cpp" line="14"/>
<source>关于</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../src/tray_icon_helper.cpp" line="17"/>
<source>退出</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>S:</name>
<message>
<location filename="../thirdpart/vcpkg/buildtrees/liblzma/src/v5.8.2-30365c07c3.clean/src/liblzma/common/string_conversion.c" line="731"/>
<source></source>
<comment>Don&apos;t translate the</comment>
<translation></translation>
</message>
</context>
<context>
<name>TZAboutWin</name>
<message>
<location filename="../qml/Setting/TZAboutWin.qml" line="9"/>
<source>关于 TZshot</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/Setting/TZAboutWin.qml" line="63"/>
<source>版本 v1.0.0</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/Setting/TZAboutWin.qml" line="83"/>
<source>简洁高效的截图贴图工具,支持标注、AI 编辑、马赛克等功能。</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/Setting/TZAboutWin.qml" line="96"/>
<source>© 2025 TZshot. All rights reserved.</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/Setting/TZAboutWin.qml" line="112"/>
<source>关闭</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>TZChat</name>
<message>
<location filename="../qml/Windows/TZChat.qml" line="83"/>
<source>AI 图像编辑</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/Windows/TZChat.qml" line="162"/>
<source>当前贴图</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/Windows/TZChat.qml" line="171"/>
<source>未关联贴图</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/Windows/TZChat.qml" line="178"/>
<source>发送后将对此图执行 AI 编辑</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/Windows/TZChat.qml" line="203"/>
<source>描述您想要的修改...</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/Windows/TZChat.qml" line="228"/>
<source>取消</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/Windows/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/Capture/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/Capture/TZMarkToolInfo.qml" line="13"/>
<source>文本</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/Capture/TZMarkToolInfo.qml" line="62"/>
<source>细</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/Capture/TZMarkToolInfo.qml" line="63"/>
<source>中</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/Capture/TZMarkToolInfo.qml" line="64"/>
<source>粗</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/Capture/TZMarkToolInfo.qml" line="65"/>
<source>特粗</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/Capture/TZMarkToolInfo.qml" line="133"/>
<source>输入文字</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/Capture/TZMarkToolInfo.qml" line="140"/>
<source>背景</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/Capture/TZMarkToolInfo.qml" line="152"/>
<source>模糊</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/Capture/TZMarkToolInfo.qml" line="177"/>
<source>自动递增</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/Capture/TZMarkToolInfo.qml" line="187"/>
<source>数字</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>TZOcrResult</name>
<message>
<location filename="../qml/Windows/TZOcrResult.qml" line="67"/>
<source>OCR 识别结果</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/Windows/TZOcrResult.qml" line="167"/>
<source>正在识别中…</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/Windows/TZOcrResult.qml" line="190"/>
<source>识别结果将显示在这里…</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/Windows/TZOcrResult.qml" line="239"/>
<source>复制全部</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/Windows/TZOcrResult.qml" line="265"/>
<source>关闭</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/Windows/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/Windows/TZStickyWindow.qml" line="324"/>
<location filename="../qml/Windows/TZStickyWindow.qml" line="325"/>
<source>OCR</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/Windows/TZStickyWindow.qml" line="341"/>
<source>Pencil</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/Windows/TZStickyWindow.qml" line="350"/>
<source>Rectangle</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/Windows/TZStickyWindow.qml" line="359"/>
<source>Circle</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/Windows/TZStickyWindow.qml" line="368"/>
<source>Arrow</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/Windows/TZStickyWindow.qml" line="382"/>
<location filename="../qml/Windows/TZStickyWindow.qml" line="383"/>
<source>Mosaic</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/Windows/TZStickyWindow.qml" line="404"/>
<location filename="../qml/Windows/TZStickyWindow.qml" line="539"/>
<source>缩小</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/Windows/TZStickyWindow.qml" line="406"/>
<location filename="../qml/Windows/TZStickyWindow.qml" line="540"/>
<source>放大</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/Windows/TZStickyWindow.qml" line="407"/>
<source>旋转90°</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/Windows/TZStickyWindow.qml" line="408"/>
<location filename="../qml/Windows/TZStickyWindow.qml" line="542"/>
<source>镜像</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/Windows/TZStickyWindow.qml" line="409"/>
<location filename="../qml/Windows/TZStickyWindow.qml" line="543"/>
<source>恢复原始大小</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/Windows/TZStickyWindow.qml" line="415"/>
<source>完成并复制</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/Windows/TZStickyWindow.qml" line="422"/>
<source>撤销</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/Windows/TZStickyWindow.qml" line="434"/>
<location filename="../qml/Windows/TZStickyWindow.qml" line="435"/>
<location filename="../qml/Windows/TZStickyWindow.qml" line="523"/>
<location filename="../qml/Windows/TZStickyWindow.qml" line="558"/>
<source>隐藏工具栏</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/Windows/TZStickyWindow.qml" line="515"/>
<source>当前透明度 %1%</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/Windows/TZStickyWindow.qml" line="518"/>
<location filename="../qml/Windows/TZStickyWindow.qml" line="572"/>
<source>覆盖贴图(合并标注)</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/Windows/TZStickyWindow.qml" line="519"/>
<location filename="../qml/Windows/TZStickyWindow.qml" line="573"/>
<location filename="../qml/Windows/TZStickyWindow.qml" line="580"/>
<source>复制贴图</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/Windows/TZStickyWindow.qml" line="520"/>
<location filename="../qml/Windows/TZStickyWindow.qml" line="566"/>
<location filename="../qml/Windows/TZStickyWindow.qml" line="581"/>
<source>AI 编辑</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/Windows/TZStickyWindow.qml" line="523"/>
<location filename="../qml/Windows/TZStickyWindow.qml" line="558"/>
<source>显示工具栏</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/Windows/TZStickyWindow.qml" line="530"/>
<location filename="../qml/Windows/TZStickyWindow.qml" line="583"/>
<location filename="../qml/Windows/TZStickyWindow.qml" line="588"/>
<source>关闭窗口</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/Windows/TZStickyWindow.qml" line="534"/>
<source>透明度 +5%</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/Windows/TZStickyWindow.qml" line="535"/>
<source>透明度 -5%</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/Windows/TZStickyWindow.qml" line="541"/>
<source>旋转 90°</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/Windows/TZStickyWindow.qml" line="548"/>
<source>重置位置</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/Windows/TZStickyWindow.qml" line="570"/>
<source>保存原图</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/Windows/TZStickyWindow.qml" line="571"/>
<location filename="../qml/Windows/TZStickyWindow.qml" line="602"/>
<source>另存为(合并标注)</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/Windows/TZStickyWindow.qml" line="578"/>
<source>常用项置顶</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/Windows/TZStickyWindow.qml" line="579"/>
<source>覆盖贴图</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/Windows/TZStickyWindow.qml" line="582"/>
<source>显示/隐藏工具栏</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/Windows/TZStickyWindow.qml" line="593"/>
<source>选择保存位置</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>TZTool</name>
<message>
<location filename="../qml/Capture/TZTool.qml" line="83"/>
<source>取消</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/Capture/TZTool.qml" line="76"/>
<source>完成</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/Capture/TZTool.qml" line="92"/>
<source>OCR</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/Capture/TZTool.qml" line="100"/>
<source>录制GIF</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/Capture/TZTool.qml" line="109"/>
<source>保存</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/Capture/TZTool.qml" line="116"/>
<source>贴图</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/Capture/TZTool.qml" line="127"/>
<source>矩形</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/Capture/TZTool.qml" line="136"/>
<source>圆形</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/Capture/TZTool.qml" line="145"/>
<source>箭头</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/Capture/TZTool.qml" line="154"/>
<source>画笔</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/Capture/TZTool.qml" line="163"/>
<source>高亮</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/Capture/TZTool.qml" line="172"/>
<source>马赛克</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/Capture/TZTool.qml" line="181"/>
<source>文字</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/Capture/TZTool.qml" line="190"/>
<source>序号</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/Capture/TZTool.qml" line="197"/>
<source>撤销</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>TZWindow</name>
<message>
<location filename="../qml/Capture/TZWindow.qml" line="134"/>
<source>Capture done</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/Capture/TZWindow.qml" line="134"/>
<source>Copied to clipboard</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/Capture/TZWindow.qml" line="136"/>
<source>Capture failed</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/Capture/TZWindow.qml" line="136"/>
<source>Failed to copy image</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/Capture/TZWindow.qml" line="434"/>
<source>Select save location</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/Capture/TZWindow.qml" line="442"/>
<source>Save success</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/Capture/TZWindow.qml" line="442"/>
<source>Screenshot saved to file</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/Capture/TZWindow.qml" line="445"/>
<source>Save failed</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/Capture/TZWindow.qml" line="445"/>
<source>Failed to save screenshot file</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/Capture/TZWindow.qml" line="563"/>
<source>%1 * %2</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/Capture/TZWindow.qml" line="705"/>
<source>Cancel</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/Capture/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>
<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>
</TS>
+3 -31
View File
@@ -1,34 +1,7 @@
<RCC>
<qresource prefix="/">
<file>qml/main.qml</file>
<file>qml/Capture/TZWindow.qml</file>
<file>qml/Capture/TZSelectionOverlay.qml</file>
<file>qml/Capture/TZCaptureInputController.qml</file>
<file>qml/Capture/TZHotkeyGestureController.qml</file>
<file>qml/Capture/TZRectMaskOverlay.qml</file>
<file>qml/Capture/TZSelectionPositioner.qml</file>
<file>qml/Capture/TZSelectionInfoLabel.qml</file>
<file>qml/Capture/TZTextAnnotationEditor.qml</file>
<file>qml/Capture/TZWindowHighlightOverlay.qml</file>
<file>qml/Capture/TZWindowFactory.qml</file>
<file>qml/Capture/TZLable.qml</file>
<file>qml/Capture/TZTool.qml</file>
<file>qml/Capture/TZResizeHandle.qml</file>
<file>qml/Capture/TZResizeHandleGroup.qml</file>
<file>qml/Capture/TZMarkToolInfo.qml</file>
<file>qml/Capture/TZMagnifyingGlass.qml</file>
<file>qml/Capture/TZGifRecordBar.qml</file>
<file>qml/Windows/TZStickyWindow.qml</file>
<file>qml/Windows/TZOcrResult.qml</file>
<file>qml/Windows/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_copy.svg</file>
<file>resource/img/lc_x.svg</file>
<file>resource/img/lc_save.svg</file>
<file>resource/img/lc_pin.svg</file>
@@ -46,9 +19,6 @@
<file>resource/img/lc_close.svg</file>
<file>resource/img/lc_chat.svg</file>
<file>resource/img/lc_send.svg</file>
<file>qml/LongCapture/TZLongCaptureBar.qml</file>
<file>qml/LongCapture/TZLongCapturePreview.qml</file>
<file>qml/LongCapture/TZLongCaptureFrame.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>
@@ -56,5 +26,7 @@
<file>resource/img/lc_chevron_down.svg</file>
<file>resource/qss/sticky_pin_widget.qss</file>
<file>resource/qss/settings_dialog.qss</file>
<file>resource/qss/capture_overlay_widget.qss</file>
<file>resource/qss/long_capture_controller.qss</file>
</qresource>
</RCC>
-56
View File
@@ -1,56 +0,0 @@
import QtQuick
QtObject {
id: controller
property bool spaceHeld: false
property bool textClickPending: false
property bool textPressMoved: false
property point textClickAbsPoint: Qt.point(0, 0)
property point textClickLocalPoint: Qt.point(0, 0)
function resetTextClickTracking() {
textClickPending = false
textPressMoved = false
textClickAbsPoint = Qt.point(0, 0)
textClickLocalPoint = Qt.point(0, 0)
}
function beginTextClick(absX, absY, localX, localY) {
textClickPending = true
textPressMoved = false
textClickAbsPoint = Qt.point(absX, absY)
textClickLocalPoint = Qt.point(localX, localY)
}
function updatePendingTextClick(absX, absY, moveThreshold) {
if (!textClickPending)
return false
const threshold = moveThreshold === undefined ? 3 : moveThreshold
const dx = absX - textClickAbsPoint.x
const dy = absY - textClickAbsPoint.y
if (Math.abs(dx) > threshold || Math.abs(dy) > threshold) {
textPressMoved = true
textClickPending = false
return false
}
return true
}
function shouldOpenTextEditor() {
return textClickPending && !textPressMoved
}
function translatedRect(initialRect, startPoint, currentX, currentY) {
const dx = currentX - startPoint.x
const dy = currentY - startPoint.y
return Qt.rect(
initialRect.x + dx,
initialRect.y + dy,
initialRect.width,
initialRect.height
)
}
}
-194
View File
@@ -1,194 +0,0 @@
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
}
}
}
}
}
-96
View File
@@ -1,96 +0,0 @@
import QtQuick
Item {
id: controller
visible: false
property bool enabled: false
property bool armed: false
property bool longPressed: false
property string mainToken: ""
property int longPressInterval: 280
signal longPressTriggered()
function parseMainKeyToken(shortcutSeq) {
if (!shortcutSeq || shortcutSeq.length === 0)
return ""
var parts = shortcutSeq.split("+")
if (parts.length === 0)
return ""
return parts[parts.length - 1].trim().toUpperCase()
}
function keyMatchesToken(key, token) {
if (!token || token.length === 0)
return false
if (token.length === 1) {
if (token >= "A" && token <= "Z")
return key === (Qt.Key_A + (token.charCodeAt(0) - "A".charCodeAt(0)))
if (token >= "0" && token <= "9")
return key === (Qt.Key_0 + (token.charCodeAt(0) - "0".charCodeAt(0)))
}
if (token[0] === "F") {
var fn = Number(token.slice(1))
if (fn >= 1 && fn <= 24)
return key === (Qt.Key_F1 + fn - 1)
}
if (token === "SPACE")
return key === Qt.Key_Space
if (token === "ESC" || token === "ESCAPE")
return key === Qt.Key_Escape
if (token === "ENTER" || token === "RETURN")
return key === Qt.Key_Return || key === Qt.Key_Enter
return false
}
function beginGesture(shortcutSeq) {
enabled = true
armed = false
longPressed = false
mainToken = parseMainKeyToken(shortcutSeq)
longPressTimer.stop()
}
function clearGesture() {
enabled = false
armed = false
longPressed = false
mainToken = ""
longPressTimer.stop()
}
function armGesture() {
if (!enabled)
return
armed = true
longPressed = false
longPressTimer.restart()
}
function handleKeyRelease(key) {
if (!armed || !keyMatchesToken(key, mainToken))
return false
longPressTimer.stop()
return true
}
Timer {
id: longPressTimer
interval: controller.longPressInterval
repeat: false
onTriggered: {
if (controller.armed) {
controller.longPressed = true
controller.longPressTriggered()
}
}
}
}
-26
View File
@@ -1,26 +0,0 @@
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
}
}
-116
View File
@@ -1,116 +0,0 @@
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 int magnifierAbsX: 0
property int magnifierAbsY: 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: magnifier.visible
? ("image://magnifier/" + magnifierAbsX + "," + magnifierAbsY + "?t=" + Date.now())
: ""
fillMode: Image.Stretch
}
// 中心像素颜色色块(8×8,叠在图像中心上方,不遮挡准线)
Rectangle {
anchors.centerIn: parent
width: 8
height: 8
color: magnifierPixelColor
border.color: "#ffffff"
border.width: 1
visible: false // 色块已在 RGB 文本行体现,避免遮挡准线
}
}
// 颜色信息栏
Rectangle {
id: colorInfoBar
anchors.top: magnifierFrame.bottom
anchors.topMargin: 2
width: 130
height: 38
radius: 4
color: "#cc1a1a1a"
border.color: "#555555"
border.width: 1
Row {
anchors.centerIn: parent
spacing: 6
// 色块
Rectangle {
width: 16
height: 16
radius: 2
color: magnifierPixelColor
border.color: "#ffffff"
border.width: 1
anchors.verticalCenter: parent.verticalCenter
}
// RGB 文本
Text {
id: rgbText
anchors.verticalCenter: parent.verticalCenter
color: "#ffffff"
font.pixelSize: 11
font.family: "monospace"
text: {
var c = magnifierPixelColor
var r = Math.round(c.r * 255)
var g = Math.round(c.g * 255)
var b = Math.round(c.b * 255)
return "R:" + r + " G:" + g + " B:" + b
}
}
}
}
}
// ── 放大镜 end ───────────────────────────────────────────
-257
View File
@@ -1,257 +0,0 @@
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)
}
}
}
}
}
}
-48
View File
@@ -1,48 +0,0 @@
import QtQuick
Item {
id: overlay
property rect holeRect: Qt.rect(0, 0, 0, 0)
property color maskColor: "#99000000"
readonly property rect normalizedHoleRect: {
const left = Math.min(holeRect.x, holeRect.x + holeRect.width)
const right = Math.max(holeRect.x, holeRect.x + holeRect.width)
const top = Math.min(holeRect.y, holeRect.y + holeRect.height)
const bottom = Math.max(holeRect.y, holeRect.y + holeRect.height)
return Qt.rect(left, top, Math.max(0, right - left), Math.max(0, bottom - top))
}
Rectangle {
x: 0
y: 0
width: overlay.width
height: Math.max(0, overlay.normalizedHoleRect.y)
color: overlay.maskColor
}
Rectangle {
x: 0
y: overlay.normalizedHoleRect.y
width: Math.max(0, overlay.normalizedHoleRect.x)
height: Math.max(0, overlay.normalizedHoleRect.height)
color: overlay.maskColor
}
Rectangle {
x: overlay.normalizedHoleRect.x + overlay.normalizedHoleRect.width
y: overlay.normalizedHoleRect.y
width: Math.max(0, overlay.width - x)
height: Math.max(0, overlay.normalizedHoleRect.height)
color: overlay.maskColor
}
Rectangle {
x: 0
y: overlay.normalizedHoleRect.y + overlay.normalizedHoleRect.height
width: overlay.width
height: Math.max(0, overlay.height - y)
color: overlay.maskColor
}
}
-14
View File
@@ -1,14 +0,0 @@
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
}
-301
View File
@@ -1,301 +0,0 @@
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)
property var coordinateItem: null
x:area.x
y:area.y
width: area.width
height: area.height
color: "transparent"
function pointInCoordinateItem(item, x, y) {
if (coordinateItem)
return item.mapToItem(coordinateItem, x, y)
return item.mapToItem(null, x, y)
}
//左上角控制点
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 = handleGroup.pointInCoordinateItem(leftTop, 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 = handleGroup.pointInCoordinateItem(leftTop, 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 = handleGroup.pointInCoordinateItem(top, mouseX, mouseY);
mousePointStart.y = globalPos.y
initialArea = area
}
onReleased: {
// 重置所有临时变量
mousePointStart = Qt.point(0,0)
initialArea = area
}
onPositionChanged: {
if (pressed) {
let globalPos = handleGroup.pointInCoordinateItem(top, 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 = handleGroup.pointInCoordinateItem(rightTop, 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 = handleGroup.pointInCoordinateItem(rightTop, 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 = handleGroup.pointInCoordinateItem(right, mouseX, mouseY);
mousePointStart.x = globalPos.x
initialArea = area
}
onReleased: {
mousePointStart = Qt.point(0,0)
initialArea = area
}
onPositionChanged: {
if (pressed) {
let globalPos = handleGroup.pointInCoordinateItem(right, 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 = handleGroup.pointInCoordinateItem(rightButtom, 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 = handleGroup.pointInCoordinateItem(rightButtom, 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 = handleGroup.pointInCoordinateItem(buttom, mouseX, mouseY);
mousePointStart.y = globalPos.y
initialArea = area
}
onReleased: {
mousePointStart = Qt.point(0,0)
initialArea = area
}
onPositionChanged: {
if (pressed) {
let globalPos = handleGroup.pointInCoordinateItem(buttom, 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 = handleGroup.pointInCoordinateItem(leftBottom, 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 = handleGroup.pointInCoordinateItem(leftBottom, 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 = handleGroup.pointInCoordinateItem(left, mouseX, mouseY);
mousePointStart.x = globalPos.x
initialArea = area
}
onReleased: {
mousePointStart = Qt.point(0,0)
initialArea = area
}
onPositionChanged: {
if (pressed) {
let globalPos = handleGroup.pointInCoordinateItem(left, mouseX, mouseY);
let dx = globalPos.x - mousePointStart.x
area.x = initialArea.x + dx
area.width = initialArea.width - dx
}
}
}
}
}
-19
View File
@@ -1,19 +0,0 @@
import QtQuick
TZLable {
id: label
property rect logicalRect: Qt.rect(0, 0, 0, 0)
property rect physicalRect: Qt.rect(0, 0, 0, 0)
width: 360
height: 20
lableText: qsTr("X:%1 Y:%2 P:%3*%4 L:%5*%6 ")
.arg(logicalRect.x)
.arg(logicalRect.y)
.arg(Math.abs(physicalRect.x))
.arg(Math.abs(physicalRect.y))
.arg(Math.abs(logicalRect.width))
.arg(Math.abs(logicalRect.height))
}
-197
View File
@@ -1,197 +0,0 @@
import QtQuick
QtObject {
id: overlay
property rect area: Qt.rect(0, 0, 0, 0)
property rect initialArea: Qt.rect(0, 0, 0, 0)
property point dragOffset: Qt.point(0, 0)
property string dragMode: "none" // none | select | move | expand
property var coordinateItem: null
property bool expandLeft: false
property bool expandRight: false
property bool expandTop: false
property bool expandBottom: false
readonly property rect localArea: {
if (!coordinateItem)
return area
const topLeft = coordinateItem.mapFromGlobal(area.x, area.y)
return Qt.rect(topLeft.x, topLeft.y, area.width, area.height)
}
readonly property rect localNormalizedArea: {
const bounds = getNormalizedRectBounds(localArea)
return bounds.isValid
? Qt.rect(bounds.left, bounds.top, bounds.width, bounds.height)
: Qt.rect(0, 0, 0, 0)
}
readonly property rect normalizedArea: {
const bounds = getNormalizedRectBounds(area)
return bounds.isValid
? Qt.rect(bounds.left, bounds.top, bounds.width, bounds.height)
: Qt.rect(0, 0, 0, 0)
}
function hasArea() {
return Math.abs(area.width) > 2 && Math.abs(area.height) > 2
}
function getNormalizedRectBounds(targetRect) {
if (targetRect.width === 0 || targetRect.height === 0) {
return { left: 0, right: 0, top: 0, bottom: 0, isValid: false }
}
const left = Math.min(targetRect.x, targetRect.x + targetRect.width)
const right = Math.max(targetRect.x, targetRect.x + targetRect.width)
const top = Math.min(targetRect.y, targetRect.y + targetRect.height)
const bottom = Math.max(targetRect.y, targetRect.y + targetRect.height)
return {
left: left,
right: right,
top: top,
bottom: bottom,
width: right - left,
height: bottom - top,
isValid: true
}
}
function containsPoint(targetRect, x, y) {
const bounds = getNormalizedRectBounds(targetRect)
if (!bounds.isValid)
return false
return x >= bounds.left && x <= bounds.right
&& y >= bounds.top && y <= bounds.bottom
}
function containsCurrentPoint(x, y) {
return containsPoint(area, x, y)
}
function selectionRectFromDrag(startX, startY, endX, endY, withShift, withAlt) {
let x = startX
let y = startY
let w = endX - startX
let h = endY - startY
if (withAlt) {
x = startX - (endX - startX)
y = startY - (endY - startY)
w = (endX - startX) * 2
h = (endY - startY) * 2
}
if (withShift) {
const absW = Math.abs(w)
const absH = Math.abs(h)
const side = Math.min(absW, absH)
w = w < 0 ? -side : side
h = h < 0 ? -side : side
}
return Qt.rect(x, y, w, h)
}
function beginSelection(ax, ay) {
dragMode = "select"
initialArea = Qt.rect(ax, ay, 0, 0)
area = Qt.rect(ax, ay, 0, 0)
}
function beginMove(ax, ay) {
dragMode = "move"
dragOffset = Qt.point(ax, ay)
initialArea = area
}
function beginExpand(ax, ay) {
const bounds = getNormalizedRectBounds(area)
if (!bounds.isValid) {
beginSelection(ax, ay)
return
}
dragMode = "expand"
initialArea = Qt.rect(bounds.left, bounds.top, bounds.width, bounds.height)
area = initialArea
expandLeft = ax < bounds.left
expandRight = ax > bounds.right
expandTop = ay < bounds.top
expandBottom = ay > bounds.bottom
}
function updateMove(ax, ay) {
const dx = ax - dragOffset.x
const dy = ay - dragOffset.y
area = Qt.rect(
initialArea.x + dx,
initialArea.y + dy,
initialArea.width,
initialArea.height
)
}
function updateSelection(ax, ay, modifiers) {
const useShift = (modifiers & Qt.ShiftModifier) !== 0
const useAlt = (modifiers & Qt.AltModifier) !== 0
area = selectionRectFromDrag(
initialArea.x,
initialArea.y,
ax,
ay,
useShift,
useAlt
)
}
function updateExpand(ax, ay) {
const bounds = getNormalizedRectBounds(initialArea)
if (!bounds.isValid)
return
let left = bounds.left
let right = bounds.right
let top = bounds.top
let bottom = bounds.bottom
if (expandLeft)
left = Math.min(ax, right)
if (expandRight)
right = Math.max(ax, left)
if (expandTop)
top = Math.min(ay, bottom)
if (expandBottom)
bottom = Math.max(ay, top)
area = Qt.rect(left, top, right - left, bottom - top)
}
function normalizeCurrentArea() {
const bounds = getNormalizedRectBounds(area)
if (bounds.isValid) {
area = Qt.rect(bounds.left, bounds.top, bounds.width, bounds.height)
initialArea = area
}
}
function endInteraction() {
dragMode = "none"
expandLeft = false
expandRight = false
expandTop = false
expandBottom = false
}
function reset() {
area = Qt.rect(0, 0, 0, 0)
initialArea = Qt.rect(0, 0, 0, 0)
dragOffset = Qt.point(0, 0)
dragMode = "none"
expandLeft = false
expandRight = false
expandTop = false
expandBottom = false
}
}
-34
View File
@@ -1,34 +0,0 @@
import QtQuick
QtObject {
id: positioner
property int gap: 6
function clamp(value, minValue, maxValue) {
return Math.max(minValue, Math.min(value, maxValue))
}
function alignLeft(areaRect, itemWidth, viewportWidth) {
return clamp(areaRect.x, 0, Math.max(0, viewportWidth - itemWidth))
}
function alignRight(areaRect, itemWidth, viewportWidth) {
const preferredX = areaRect.x + areaRect.width - itemWidth
return clamp(preferredX, 0, Math.max(0, viewportWidth - itemWidth))
}
function placeBelowOrAbove(areaRect, itemHeight, viewportHeight, customGap) {
const spacing = customGap === undefined ? gap : customGap
const belowY = areaRect.y + areaRect.height + spacing
if (belowY + itemHeight <= viewportHeight)
return belowY
return areaRect.y - itemHeight - spacing
}
function placeAbove(areaRect, itemHeight, viewportHeight, customGap) {
const spacing = customGap === undefined ? gap : customGap
const preferredY = areaRect.y - itemHeight - spacing
return clamp(preferredY, 0, Math.max(0, viewportHeight - itemHeight))
}
}
-101
View File
@@ -1,101 +0,0 @@
import QtQuick
Rectangle {
id: editor
property alias editorText: textInput.text
signal accepted(string text)
signal canceled()
width: 260
height: 88
visible: false
radius: 8
color: "#FFFFFF"
border.color: "#CBD5E1"
border.width: 1
function beginEdit(textValue) {
editorText = textValue
visible = true
textInput.forceActiveFocus()
textInput.selectAll()
}
Column {
anchors.fill: parent
anchors.margins: 8
spacing: 8
Rectangle {
width: parent.width
height: 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
onAccepted: editor.accepted(text)
}
}
Row {
spacing: 8
Rectangle {
width: 72
height: 28
radius: 6
color: cancelInputArea.containsMouse ? "#F1F5F9" : "transparent"
border.color: "#CBD5E1"
border.width: 1
Text {
anchors.centerIn: parent
text: qsTr("Cancel")
color: "#334155"
font.pixelSize: 12
}
MouseArea {
id: cancelInputArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onPressed: editor.canceled()
}
}
Rectangle {
width: 72
height: 28
radius: 6
color: confirmInputArea.containsMouse ? "#2563EB" : "#3B82F6"
Text {
anchors.centerIn: parent
text: qsTr("OK")
color: "#FFFFFF"
font.pixelSize: 12
}
MouseArea {
id: confirmInputArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onPressed: editor.accepted(textInput.text)
}
}
}
}
}
-278
View File
@@ -1,278 +0,0 @@
import QtQuick 2.12
import QtQuick.Layouts 1.12
import QtQuick.Controls 2.5
Rectangle {
id: tool
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 signalLongScreenshot
signal signalOcr
property string activeTool: ""
property bool markToolInfoVisible: false
property string hoverTipText: ""
property real hoverTipCenterX: width / 2
property bool showScreenshotButton: true
property bool showCancelButton: true
property bool showSaveButton: true
property bool showStickyButton: true
property bool showUndoButton: true
property bool showGifRecordButton: true
property bool showLongScreenshotButton: true
property bool showOcrButton: true
property bool showRectButton: true
property bool showCircleButton: true
property bool showArrowButton: true
property bool showPencilButton: true
property bool showHighlightButton: true
property bool showMosaicButton: true
property bool showTextButton: true
property bool showNumberButton: true
readonly property int buttonSize: 32
readonly property int buttonRadius: 7
readonly property int iconSize: 18
readonly property int dividerHeight: 20
readonly property int outerRadius: 10
readonly property int outerMargin: 6
readonly property int bubbleRadius: 6
readonly property int bubbleHeight: 24
readonly property int bubblePadding: 14
readonly property int bubbleGap: 6
readonly property int tipFontSize: 11
readonly property int rowSpacing: 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 = ""
}
radius: outerRadius
implicitWidth: toolRow.implicitWidth + outerMargin * 2
implicitHeight: Math.max(buttonSize + outerMargin * 2, 44)
RowLayout {
id: toolRow
anchors.fill: parent
anchors.margins: tool.outerMargin
layoutDirection: Qt.RightToLeft
spacing: tool.rowSpacing
Rectangle {
visible: tool.showScreenshotButton
width: tool.buttonSize; height: tool.buttonSize; radius: tool.buttonRadius
color: checkArea.containsMouse ? "#DCFCE7" : "transparent"
Image { anchors.centerIn: parent; source: "qrc:/resource/img/lc_check.svg"; width: tool.iconSize; height: tool.iconSize }
MouseArea { id: checkArea; anchors.fill: parent; hoverEnabled: true; cursorShape: Qt.PointingHandCursor; onEntered: tool.showTip(qsTr("完成"), parent); onExited: tool.hideTip(qsTr("完成")); onPressed: signalScreenshot() }
}
Rectangle {
visible: tool.showCancelButton
width: tool.buttonSize; height: tool.buttonSize; radius: tool.buttonRadius
color: closeArea.containsMouse ? "#FEE2E2" : "transparent"
Image { anchors.centerIn: parent; source: "qrc:/resource/img/lc_x.svg"; width: tool.iconSize; height: tool.iconSize }
MouseArea { id: closeArea; anchors.fill: parent; hoverEnabled: true; cursorShape: Qt.PointingHandCursor; onEntered: tool.showTip(qsTr("取消"), parent); onExited: tool.hideTip(qsTr("取消")); onPressed: signalCancel() }
}
Rectangle { visible: tool.showScreenshotButton || tool.showCancelButton; width: 1; height: tool.dividerHeight; color: "#E2E8F0"; Layout.alignment: Qt.AlignVCenter }
Rectangle {
visible: tool.showOcrButton
width: tool.buttonSize; height: tool.buttonSize; radius: tool.buttonRadius
color: ocrArea.containsMouse ? "#F0FDF4" : "transparent"
Image { anchors.centerIn: parent; source: "qrc:/resource/img/lc_ocr.svg"; width: tool.iconSize; height: tool.iconSize }
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
visible: tool.showGifRecordButton
width: tool.buttonSize; height: tool.buttonSize; radius: tool.buttonRadius
color: gifArea.containsMouse ? "#FFF7ED" : "transparent"
Image { anchors.centerIn: parent; source: "qrc:/resource/img/lc_gif_record.svg"; width: tool.iconSize; height: tool.iconSize }
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 {
visible: tool.showLongScreenshotButton
width: tool.buttonSize; height: tool.buttonSize; radius: tool.buttonRadius
color: longShotArea.containsMouse ? "#FFF7ED" : "transparent"
Image { anchors.centerIn: parent; source: "qrc:/resource/img/lc_longshot.svg"; width: tool.iconSize; height: tool.iconSize }
MouseArea { id: longShotArea; anchors.fill: parent; hoverEnabled: true; cursorShape: Qt.PointingHandCursor; onEntered: tool.showTip(qsTr("长截图"), parent); onExited: tool.hideTip(qsTr("长截图")); onPressed: signalLongScreenshot() }
}
Rectangle { visible: tool.showOcrButton || tool.showGifRecordButton || tool.showLongScreenshotButton; width: 1; height: tool.dividerHeight; color: "#E2E8F0"; Layout.alignment: Qt.AlignVCenter }
Rectangle {
visible: tool.showSaveButton
width: tool.buttonSize; height: tool.buttonSize; radius: tool.buttonRadius
color: saveArea.containsMouse ? "#EFF6FF" : "transparent"
Image { anchors.centerIn: parent; source: "qrc:/resource/img/lc_save.svg"; width: tool.iconSize; height: tool.iconSize }
MouseArea { id: saveArea; anchors.fill: parent; hoverEnabled: true; cursorShape: Qt.PointingHandCursor; onEntered: tool.showTip(qsTr("保存"), parent); onExited: tool.hideTip(qsTr("保存")); onPressed: signalSave() }
}
Rectangle {
visible: tool.showStickyButton
width: tool.buttonSize; height: tool.buttonSize; radius: tool.buttonRadius
color: fixArea.containsMouse ? "#EFF6FF" : "transparent"
Image { anchors.centerIn: parent; source: "qrc:/resource/img/lc_pin.svg"; width: tool.iconSize; height: tool.iconSize }
MouseArea { id: fixArea; anchors.fill: parent; hoverEnabled: true; cursorShape: Qt.PointingHandCursor; onEntered: tool.showTip(qsTr("贴图"), parent); onExited: tool.hideTip(qsTr("贴图")); onPressed: signalSticky() }
}
Rectangle { visible: tool.showSaveButton || tool.showStickyButton; width: 1; height: tool.dividerHeight; color: "#E2E8F0"; Layout.alignment: Qt.AlignVCenter }
Rectangle {
visible: tool.showRectButton
width: tool.buttonSize; height: tool.buttonSize; radius: tool.buttonRadius
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: tool.iconSize; height: tool.iconSize }
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 {
visible: tool.showCircleButton
width: tool.buttonSize; height: tool.buttonSize; radius: tool.buttonRadius
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: tool.iconSize; height: tool.iconSize }
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 {
visible: tool.showArrowButton
width: tool.buttonSize; height: tool.buttonSize; radius: tool.buttonRadius
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: tool.iconSize; height: tool.iconSize }
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 {
visible: tool.showPencilButton
width: tool.buttonSize; height: tool.buttonSize; radius: tool.buttonRadius
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: tool.iconSize; height: tool.iconSize }
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 {
visible: tool.showHighlightButton
width: tool.buttonSize; height: tool.buttonSize; radius: tool.buttonRadius
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: tool.iconSize; height: tool.iconSize }
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 {
visible: tool.showMosaicButton
width: tool.buttonSize; height: tool.buttonSize; radius: tool.buttonRadius
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: tool.iconSize; height: tool.iconSize }
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 {
visible: tool.showTextButton
width: tool.buttonSize; height: tool.buttonSize; radius: tool.buttonRadius
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: tool.iconSize; height: tool.iconSize }
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 {
visible: tool.showNumberButton
width: tool.buttonSize; height: tool.buttonSize; radius: tool.buttonRadius
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: tool.iconSize; height: tool.iconSize }
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 {
visible: tool.showUndoButton
width: tool.buttonSize; height: tool.buttonSize; radius: tool.buttonRadius
color: undoArea.containsMouse ? "#EFF6FF" : "transparent"
Image { anchors.centerIn: parent; source: "qrc:/resource/img/lc_undo.svg"; width: tool.iconSize; height: tool.iconSize }
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(36, tipText.implicitWidth + tool.bubblePadding)
height: tool.bubbleHeight
radius: tool.bubbleRadius
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 + tool.bubbleGap
Text {
id: tipText
anchors.centerIn: parent
text: tool.hoverTipText
color: "#FFFFFF"
font.pixelSize: tool.tipFontSize
}
}
}
-765
View File
@@ -1,765 +0,0 @@
import QtQuick
import QtQuick.Window
import QtQuick.Layouts
import QtQuick.Dialogs
import CustomComponents 1.0
Window {
id: mainWin
color: "transparent";
// Virtual desktop offset injected by main.qml
property int screenVirtualX: 0
property int screenVirtualY: 0
property alias initialScreenshotArea: selectionOverlay.initialArea
property alias screenshotArea: selectionOverlay.area
property alias offset: selectionOverlay.dragOffset
property alias dragMode: selectionOverlay.dragMode
property string screenShotUrl: ""
property rect highlightRect: Qt.rect(0, 0, 0, 0)
property point pendingTextPoint: Qt.point(0, 0)
property string pendingTextValue: ""
property bool captureUiReady: false
property bool gifRecordMode: false
readonly property bool gifAreaLocked: O_GifRecordVM.isRecording || O_GifRecordVM.isEncoding
property alias tool: tool
property alias paintBoard: paintBoard
property alias magnifier: magnifier
property alias gifBarObj: gifBar
function hasSelectionArea() {
return selectionOverlay.hasArea()
}
function beginHotkeyGesture(shortcutSeq) {
hotkeyGesture.beginGesture(shortcutSeq)
}
function clearHotkeyGesture() {
hotkeyGesture.clearGesture()
}
function maybeArmHotkeyGesture() {
if (!hotkeyGesture.enabled || !hasSelectionArea())
return
hotkeyGesture.armGesture()
}
function confirmSelectionToClipboard() {
root.visibility = Window.Hidden
Qt.callLater(function() {
var success = O_ScreenCapture.captureRectToClipboard(paintBoard, screenshotArea)
resetArea()
if (success)
trayHelper.showMessage(qsTr("Capture done"), qsTr("Copied to clipboard"))
else
trayHelper.showMessage(qsTr("Capture failed"), qsTr("Failed to copy image"))
})
}
function handleEscAction() {
if (textEditor.visible) {
hideTextEditor()
return true
}
if (windowFactory.closeTopOcrWindow()) {
return true
}
if (gifRecordMode) {
O_GifRecordVM.cancelRecording()
gifBar.visible = false
gifRecordMode = false
if (hasSelectionArea())
tool.visible = true
return true
}
if (tool.activeTool !== "") {
tool.activeTool = ""
return true
}
if (tool.markToolInfoVisible) {
tool.markToolInfoVisible = false
return true
}
if (tool.visible) {
tool.visible = false
return true
}
if (hasSelectionArea()) {
resetArea()
return true
}
return false
}
function beginSelectionInteraction(ax, ay) {
if (screenshotArea.width !== 0 || screenshotArea.height !== 0) {
if (selectionOverlay.containsCurrentPoint(ax, ay)) {
selectionOverlay.beginMove(ax, ay)
return
}
paintBoard.reset()
tool.activeTool = ""
tool.markToolInfoVisible = false
tool.visible = false
resizeHandleGroup.visible = false
hideTextEditor()
highlightRect = Qt.rect(0, 0, 0, 0)
selectionOverlay.beginExpand(ax, ay)
return
}
paintBoard.reset()
tool.activeTool = ""
selectionOverlay.beginSelection(ax, ay)
}
function updateSelectionInteraction(ax, ay, isPressed, modifiers) {
if (isPressed)
resizeHandleGroup.visible = false
var c = O_ScreenCapture.getPixelColor(ax, ay)
if (c !== undefined && c !== null)
magnifier.magnifierPixelColor = c
if (!isPressed && screenshotArea.width === 0 && screenshotArea.height === 0) {
var winRect = O_ScreenCapture.windowAtPoint(ax, ay)
highlightRect = winRect.width > 0
? Qt.rect(winRect.x, winRect.y, winRect.width, winRect.height)
: Qt.rect(0, 0, 0, 0)
return
}
if (!isPressed) {
return
}
if (dragMode === "move") {
selectionOverlay.updateMove(ax, ay)
return
}
if (dragMode === "select") {
selectionOverlay.updateSelection(ax, ay, modifiers)
if (tool.visible) tool.visible = false
return
}
if (dragMode === "expand") {
selectionOverlay.updateExpand(ax, ay)
if (tool.visible) tool.visible = false
}
}
function finalizeSelectionInteraction() {
selectionOverlay.endInteraction()
if (Math.abs(screenshotArea.width) <= 5 && Math.abs(screenshotArea.height) <= 5) {
if (highlightRect.width > 0 && highlightRect.height > 0) {
screenshotArea = highlightRect
console.log("screenshotArea2-->",screenshotArea)
initialScreenshotArea = highlightRect
resizeHandleGroup.area = localScreenshotArea
resizeHandleGroup.initialArea = toLocalRect(initialScreenshotArea)
highlightRect = Qt.rect(0, 0, 0, 0)
Qt.callLater(function() { tool.visible = true })
}
resizeHandleGroup.visible = (highlightRect.width > 0 && highlightRect.height > 0)
return
}
selectionOverlay.normalizeCurrentArea()
resizeHandleGroup.initialArea = toLocalRect(initialScreenshotArea)
if (Math.abs(screenshotArea.width) > 2 && Math.abs(screenshotArea.height) > 2) {
Qt.callLater(function() { tool.visible = true })
}
resizeHandleGroup.area = localScreenshotArea
resizeHandleGroup.visible = true;
}
function toAbsolutePoint(localX, localY) {
return screenshotRect.mapToGlobal(localX, localY)
}
function toLocalRect(absRect) {
var topLeft = screenshotRect.mapFromGlobal(absRect.x, absRect.y)
return Qt.rect(topLeft.x, topLeft.y, absRect.width, absRect.height)
}
function toAbsoluteRect(localRect) {
var topLeft = screenshotRect.mapToGlobal(localRect.x, localRect.y)
return Qt.rect(topLeft.x, topLeft.y, localRect.width, localRect.height)
}
function syncResizeHandles() {
resizeHandleGroup.area = localScreenshotArea
resizeHandleGroup.initialArea = toLocalRect(initialScreenshotArea)
}
function syncGifResizeHandles() {
gifResizeHandleGroup.area = localScreenshotArea
gifResizeHandleGroup.initialArea = toLocalRect(initialScreenshotArea)
}
function normalizeRect(rectValue) {
var left = Math.min(rectValue.x, rectValue.x + rectValue.width)
var right = Math.max(rectValue.x, rectValue.x + rectValue.width)
var top = Math.min(rectValue.y, rectValue.y + rectValue.height)
var bottom = Math.max(rectValue.y, rectValue.y + rectValue.height)
return Qt.rect(left, top, right - left, bottom - top)
}
// screenshotArea 在本窗口本地坐标中的等价矩形(用于绘制)
readonly property rect localScreenshotArea: toLocalRect(screenshotArea)
readonly property rect localNormalizedScreenshotArea: {
return normalizeRect(localScreenshotArea)
}
readonly property rect normalizedScreenshotArea: {
return selectionOverlay.normalizedArea
}
readonly property rect physicalScreenshotArea: {
if (screenshotArea.width === 0 || screenshotArea.height === 0)
return Qt.rect(0, 0, 0, 0)
return O_ScreenCapture.mapLogicalRectToPhysicalRect(normalizedScreenshotArea)
}
readonly property rect localHighlightRect: Qt.rect(
highlightRect.x - screenVirtualX,
highlightRect.y - screenVirtualY,
highlightRect.width,
highlightRect.height
)
function createStickyWindow(imageUrl, imgRect) {
return windowFactory.createStickyWindow(imageUrl, imgRect)
}
function createOcrResultWindow() {
return windowFactory.createOcrResultWindow()
}
function openTextEditor(localX, localY) {
pendingTextPoint = Qt.point(localX, localY)
pendingTextValue = markToolInfo.annotationText
textEditor.x = Math.max(0, Math.min(localX, mainWin.width - textEditor.width))
textEditor.y = Math.max(0, Math.min(localY + 8, mainWin.height - textEditor.height))
textEditor.beginEdit(pendingTextValue)
}
function hideTextEditor() {
textEditor.visible = false
}
function submitTextEditor(textValue) {
var text = textValue.trim()
if (text.length === 0) {
hideTextEditor()
return
}
markToolInfo.annotationText = text
paintBoard.addTextAnnotation(Math.round(pendingTextPoint.x), Math.round(pendingTextPoint.y), text)
hideTextEditor()
}
function resetTextClickTracking() {
inputController.resetTextClickTracking()
}
function beginGifInteraction(ax, ay) {
if (!gifRecordMode || gifAreaLocked)
return false
if (selectionOverlay.containsCurrentPoint(ax, ay)) {
offset.x = ax
offset.y = ay
initialScreenshotArea = screenshotArea
}
return true
}
function beginSpaceMoveInteraction(ax, ay) {
if (!inputController.spaceHeld || !hasSelectionArea())
return false
offset.x = ax
offset.y = ay
initialScreenshotArea = screenshotArea
return true
}
function beginTextToolInteraction(ax, ay, localX, localY) {
if (tool.activeTool !== "text"
|| screenshotArea.width === 0
|| screenshotArea.height === 0
|| !selectionOverlay.containsCurrentPoint(ax, ay)) {
return false
}
inputController.beginTextClick(ax, ay, localX, localY)
offset.x = ax
offset.y = ay
initialScreenshotArea = screenshotArea
return true
}
function updatePendingTextInteraction(ax, ay) {
var stillPending = inputController.updatePendingTextClick(ax, ay, 3)
if (!stillPending && inputController.textPressMoved && textEditor.visible)
hideTextEditor()
return stillPending
}
function updateGifInteraction(ax, ay, isPressed) {
if (!gifRecordMode)
return false
if (gifAreaLocked)
return true
if (isPressed && selectionOverlay.containsCurrentPoint(ax, ay)) {
screenshotArea = inputController.translatedRect(initialScreenshotArea, offset, ax, ay)
O_GifRecordVM.updateCaptureRect(
screenshotArea.x,
screenshotArea.y,
screenshotArea.width,
screenshotArea.height
)
}
return true
}
function updateSpaceMoveInteraction(ax, ay, isPressed) {
if (!isPressed || !inputController.spaceHeld || !hasSelectionArea())
return false
screenshotArea = inputController.translatedRect(initialScreenshotArea, offset, ax, ay)
return true
}
function updateMagnifierFromMouse(localX, localY, ax, ay) {
magnifier.magnifierMouseX = localX
magnifier.magnifierMouseY = localY
magnifier.magnifierAbsX = ax
magnifier.magnifierAbsY = ay
}
function finalizeTextToolInteraction() {
if (inputController.shouldOpenTextEditor()) {
openTextEditor(inputController.textClickLocalPoint.x,
inputController.textClickLocalPoint.y)
resetTextClickTracking()
return true
}
resetTextClickTracking()
return false
}
function finalizeGifInteraction() {
if (!gifRecordMode)
return false
if (gifAreaLocked)
return true
initialScreenshotArea = screenshotArea
gifResizeHandleGroup.initialArea = toLocalRect(initialScreenshotArea)
return true
}
function resetArea() {
selectionOverlay.reset()
syncResizeHandles()
syncGifResizeHandles()
screenShotUrl = ""
paintBoard.reset()
tool.visible = false
tool.activeTool = ""
tool.markToolInfoVisible = false
hideTextEditor()
gifRecordMode = false
gifBar.visible = false
highlightRect = Qt.rect(0, 0, 0, 0)
resetTextClickTracking()
clearHotkeyGesture()
O_ScreenCapture.releaseDesktopSnapshot()
root.visibility = Window.Hidden;
}
TZSelectionOverlay {
id: selectionOverlay
coordinateItem: screenshotRect
}
TZWindowFactory {
id: windowFactory
}
TZSelectionPositioner {
id: selectionPositioner
}
TZCaptureInputController {
id: inputController
}
TZHotkeyGestureController {
id: hotkeyGesture
onLongPressTriggered: {
if (hasSelectionArea())
tool.visible = true
}
}
FileDialog {
id: saveDialog
title: qsTr("Select save location")
currentFolder: O_ImageSaver.savePath
fileMode: FileDialog.SaveFile
nameFilters: ["PNG (*.png)", "JPG (*.jpg)", "BMP (*.bmp)"]
onAccepted: {
var success = O_ImageSaver.saveImage("data:image/png;base64,"+screenShotUrl, saveDialog.selectedFile)
if (success) {
console.log("Save success:", saveDialog.currentFolder)
trayHelper.showMessage(qsTr("Save success"), qsTr("Screenshot saved to file"))
} else {
console.log("Save failed")
trayHelper.showMessage(qsTr("Save failed"), qsTr("Failed to save screenshot file"))
}
resetArea()
}
}
TZGifRecordBar {
id: gifBar
width: 260
height: 44
visible: false
z: 20
captureRect: screenshotArea
x: selectionPositioner.alignRight(localNormalizedScreenshotArea, width, mainWin.width)
y: selectionPositioner.placeBelowOrAbove(localNormalizedScreenshotArea, height, mainWin.height)
onSignalCancel: {
gifBar.visible = false
gifRecordMode = false
tool.visible = true
}
onSignalRecordFinished: {
resetArea()
}
}
Rectangle {
id: recordingBorder
visible: O_GifRecordVM.isRecording
// Draw border outside capture rect so it won't be included in recorded frames.
x: localNormalizedScreenshotArea.x - 2
y: localNormalizedScreenshotArea.y - 2
width: localNormalizedScreenshotArea.width + 4
height: localNormalizedScreenshotArea.height + 4
color: "transparent"
border.color: "#EF4444"
border.width: 2
z: 10
}
PaintBoard {
id: paintBoard
anchors.fill: parent
z: 1
enabled: tool.activeTool !== "" && tool.activeTool !== "text"
visible: captureUiReady && screenshotArea.width !== 0 && screenshotArea.height !== 0
shapeType: tool.shapeType
penColor: markToolInfo.currentColor
penSize: markToolInfo.currentSize
annotationText: markToolInfo.annotationText
textBackgroundEnabled: markToolInfo.textBackgroundEnabled
mosaicBlurLevel: markToolInfo.mosaicBlurLevel
numberAutoIncrement: markToolInfo.numberAutoIncrement
numberValue: markToolInfo.numberValue
}
Connections {
target: paintBoard
function onNumberValueChanged(value) {
markToolInfo.numberValue = value
}
}
Rectangle {
id: screenshotRect
anchors.fill: parent
color: "transparent";
focus: true
z: 2
visible: captureUiReady && !gifRecordMode
TZRectMaskOverlay {
anchors.fill: parent
holeRect: localNormalizedScreenshotArea
maskColor: "#99000000"
}
TZSelectionInfoLabel {
visible: screenshotArea.width !== 0 && screenshotArea.height !== 0
x: selectionPositioner.alignLeft(localNormalizedScreenshotArea, width, mainWin.width)
y: selectionPositioner.placeAbove(localNormalizedScreenshotArea, height, mainWin.height, 5)
logicalRect: normalizedScreenshotArea
physicalRect: physicalScreenshotArea
}
TZTool {
id: tool
width: 656; height: 44
z: 30
// Toolbar position (local coordinates)
x: selectionPositioner.alignRight(localNormalizedScreenshotArea, width, mainWin.width)
y: selectionPositioner.placeBelowOrAbove(localNormalizedScreenshotArea, height, mainWin.height)
onActiveToolChanged: {
if (activeTool !== "text" && textEditor.visible) {
hideTextEditor()
}
if (activeTool === "mosaic" && !paintBoard.hasBackgroundImg()) {
Qt.callLater(function() {
O_ScreenCapture.grabToPaintBoard(
paintBoard,
Qt.rect(screenVirtualX, screenVirtualY, mainWin.width, mainWin.height)
)
})
}
}
onSignalScreenshot: {
confirmSelectionToClipboard()
}
onSignalCancel: { resetArea() }
onSignalSave: {
root.visibility = Window.Hidden
Qt.callLater(function() {
screenShotUrl = O_ScreenCapture.captureRectToBase64(paintBoard, screenshotArea)
root.visibility = Window.Windowed
saveDialog.open()
})
}
onSignalSticky: {
root.visibility = Window.Hidden
Qt.callLater(function() {
var captureRect = normalizedScreenshotArea
var url = O_ScreenCapture.captureRectToStickyUrl(paintBoard, captureRect)
O_StickyViewModel.requestSticky(url, captureRect)
resetArea()
})
}
onSignalUndo: { paintBoard.undo() }
onSignalGifRecord: {
gifRecordMode = true
tool.visible = false
gifBar.visible = true
Qt.callLater(function() {
O_GifRecordVM.startRecording(
screenshotArea.x, screenshotArea.y,
screenshotArea.width, screenshotArea.height
)
})
}
onSignalLongScreenshot: {
var captureRect = screenshotArea
root.beginLongCapture(captureRect)
}
onSignalOcr: {
var captureRect = screenshotArea
Qt.callLater(function() {
var img = O_ScreenCapture.captureRectToImage(captureRect)
O_OcrVM.recognize(img)
})
}
}
TZMarkToolInfo {
id: markToolInfo
visible: tool.visible && tool.markToolInfoVisible
z: 30
x: tool.x
y: tool.y + tool.height
toolName: tool.activeTool
showColorRow: tool.activeTool !== "mosaic"
}
TZTextAnnotationEditor {
id: textEditor
z: 40
onAccepted: function(text) { submitTextEditor(text) }
onCanceled: hideTextEditor()
}
TZResizeHandleGroup {
id: resizeHandleGroup
coordinateItem: screenshotRect
// Handle control points use local coordinates
area: localScreenshotArea
initialArea: toLocalRect(initialScreenshotArea)
z: 2
onAreaChanged: {
// 转回绝对坐标
screenshotArea = toAbsoluteRect(area)
}
onInitialAreaChanged: {
initialScreenshotArea = toAbsoluteRect(initialArea)
}
}
}
TZResizeHandleGroup {
id: gifResizeHandleGroup
coordinateItem: screenshotRect
area: localScreenshotArea
initialArea: toLocalRect(initialScreenshotArea)
visible: gifRecordMode && !gifAreaLocked
enabled: !gifAreaLocked
z: 15
onAreaChanged: {
screenshotArea = toAbsoluteRect(area)
O_GifRecordVM.updateCaptureRect(
screenshotArea.x, screenshotArea.y,
screenshotArea.width, screenshotArea.height
)
}
onInitialAreaChanged: {
initialScreenshotArea = toAbsoluteRect(initialArea)
}
}
// Window highlight rectangle (local coordinates)
TZWindowHighlightOverlay {
id: windowHighlight
visible: captureUiReady && screenshotArea.width === 0 && screenshotArea.height === 0
&& highlightRect.width > 0 && highlightRect.height > 0
x: localHighlightRect.x
y: localHighlightRect.y
width: localHighlightRect.width
height: localHighlightRect.height
z: 5
}
TZMagnifyingGlass {
id: magnifier
width: 130
height: 170
z: 3
color: "transparent"
// Hide magnifier in GIF mode to avoid recording it
visible: captureUiReady && mainWin.visible && !tool.visible && !gifRecordMode
}
MouseArea {
id: mouseArea
anchors.fill: parent
enabled: captureUiReady
hoverEnabled: true
focus: true
cursorShape: gifRecordMode
? (gifAreaLocked ? Qt.ArrowCursor : Qt.SizeAllCursor)
: Qt.CrossCursor
onPressed: function(mouse) {
var globalPos = screenshotRect.mapToGlobal(mouseX, mouseY)
var ax = globalPos.x, ay = globalPos.y
resetTextClickTracking()
if (beginGifInteraction(ax, ay)) {
return
}
if (beginSpaceMoveInteraction(ax, ay)) {
return
}
if (beginTextToolInteraction(ax, ay, mouseX, mouseY)) {
return
}
if (textEditor.visible) {
hideTextEditor()
}
beginSelectionInteraction(ax, ay)
}
onPositionChanged: function(mouse) {
var globalPos = screenshotRect.mapToGlobal(mouseX, mouseY)
var ax = globalPos.x, ay = globalPos.y
if (pressed && updatePendingTextInteraction(ax, ay)) {
return
}
if (updateGifInteraction(ax, ay, pressed)) {
return
}
if (updateSpaceMoveInteraction(ax, ay, pressed)) {
return
}
updateMagnifierFromMouse(mouseX, mouseY, ax, ay)
updateSelectionInteraction(ax, ay, pressed, mouse.modifiers)
}
onReleased: function(mouse) {
if (finalizeTextToolInteraction()) {
return
}
if (finalizeGifInteraction()) {
return
}
finalizeSelectionInteraction()
}
onWheel: function(wheel) { wheel.accepted = true }
Keys.onPressed: function(event) {
if (event.key === Qt.Key_Space) {
inputController.spaceHeld = true
event.accepted = true
return
}
if (event.key === Qt.Key_Escape) {
if (handleEscAction())
event.accepted = true
return
}
}
Keys.onReleased: function(event) {
if (event.key === Qt.Key_Space) {
inputController.spaceHeld = false
event.accepted = true
return
}
if (hotkeyGesture.handleKeyRelease(event.key)) {
if (!hotkeyGesture.longPressed && hasSelectionArea())
confirmSelectionToClipboard()
clearHotkeyGesture()
event.accepted = true
}
}
}
onVisibilityChanged:
{
if(root.visibility === Window.Hidden) {
resizeHandleGroup.visible = false;
gifResizeHandleGroup.visible = false;
}
}
}
-116
View File
@@ -1,116 +0,0 @@
import QtQuick
QtObject {
id: factory
property var stickyWindows: []
property var ocrWindows: []
property var stickyComponent: null
property var ocrComponent: null
function createStickyWindow(imageUrl, imgRect) {
if (!imageUrl || imageUrl.length === 0) {
console.error("Failed to create sticky window: invalid image URL")
return null
}
if (stickyComponent === null)
stickyComponent = Qt.createComponent("qrc:/qml/Windows/TZStickyWindow.qml")
var newWindow = null
if (stickyComponent.status === Component.Ready) {
newWindow = stickyComponent.createObject(null, {
"imageUrl": imageUrl,
"imgRect": imgRect
})
} else if (stickyComponent.status === Component.Error) {
console.error("Failed to load TZStickyWindow.qml:", stickyComponent.errorString())
}
if (!newWindow) {
console.error("Failed to create sticky window object")
return null
}
newWindow.visible = true
newWindow.raise()
newWindow.requestActivate()
var stickyList = stickyWindows.slice()
stickyList.push(newWindow)
stickyWindows = stickyList
var windowRef = newWindow
function onWindowClosing(closeEvent) {
var idx = stickyWindows.indexOf(windowRef)
if (idx !== -1) {
var updatedList = stickyWindows.slice()
updatedList.splice(idx, 1)
stickyWindows = updatedList
}
windowRef.closing.disconnect(onWindowClosing)
windowRef = null
}
newWindow.closing.connect(onWindowClosing)
return newWindow
}
function createOcrResultWindow() {
if (ocrWindows.length > 0) {
var existing = ocrWindows[ocrWindows.length - 1]
if (existing) {
existing.show()
existing.raise()
existing.requestActivate()
return existing
}
}
if (ocrComponent === null)
ocrComponent = Qt.createComponent("qrc:/qml/Windows/TZOcrResult.qml")
var win = null
if (ocrComponent.status === Component.Ready) {
win = ocrComponent.createObject(null)
} else if (ocrComponent.status === Component.Error) {
console.error("Failed to load TZOcrResult.qml:", ocrComponent.errorString())
}
if (!win)
return null
win.show()
win.raise()
win.requestActivate()
var list = ocrWindows.slice()
list.push(win)
ocrWindows = list
var winRef = win
function onOcrClosing(closeEvent) {
var idx = ocrWindows.indexOf(winRef)
if (idx !== -1) {
var updatedList = ocrWindows.slice()
updatedList.splice(idx, 1)
ocrWindows = updatedList
}
winRef.closing.disconnect(onOcrClosing)
winRef = null
}
win.closing.connect(onOcrClosing)
return win
}
function closeTopOcrWindow() {
if (ocrWindows.length === 0)
return false
var topWin = ocrWindows[ocrWindows.length - 1]
if (!topWin)
return false
topWin.close()
return true
}
}
-46
View File
@@ -1,46 +0,0 @@
import QtQuick
Rectangle {
id: highlight
color: "transparent"
border.color: "#3B82F6"
border.width: 2
Rectangle {
anchors.fill: parent
color: "#153B82F6"
}
Repeater {
model: [
{ ax: 0, ay: 0, bx: 1, by: 0, cx: 0, cy: 1 },
{ ax: parent.width, ay: 0, bx: -1, by: 0, cx: 0, cy: 1 },
{ ax: 0, ay: parent.height, bx: 1, by: 0, cx: 0, cy: -1 },
{ ax: parent.width, ay: parent.height, bx: -1, by: 0, cx: 0, cy: -1 }
]
delegate: Item {
x: modelData.ax - (modelData.bx > 0 ? 0 : 3)
y: modelData.ay - (modelData.cy > 0 ? 0 : 3)
Rectangle {
x: 0
y: 0
width: 12
height: 3
color: "#3B82F6"
transform: Scale { xScale: modelData.bx }
}
Rectangle {
x: 0
y: 0
width: 3
height: 12
color: "#3B82F6"
transform: Scale { yScale: modelData.cy }
}
}
}
}
-110
View File
@@ -1,110 +0,0 @@
import QtQuick
import QtQuick.Window
import QtQuick.Layouts
Window {
id: longBar
width: 500
height: 56
visible: false
color: "transparent"
flags: Qt.Tool | Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint | Qt.WindowDoesNotAcceptFocus
signal startRequested()
signal stopRequested()
property bool running: false
property int frameCount: 0
property string message: qsTr("点击开始后,滑动内容自动拼接")
Rectangle {
anchors.fill: parent
radius: 12
color: "#111827"
border.color: "#1F2937"
border.width: 1
RowLayout {
anchors.fill: parent
anchors.margins: 10
spacing: 10
Rectangle {
width: 26
height: 26
radius: 13
color: "#F97316"
Layout.alignment: Qt.AlignVCenter
Text {
anchors.centerIn: parent
text: "长"
color: "#FFFFFF"
font.pixelSize: 14
font.bold: true
}
}
Text {
Layout.fillWidth: true
text: longBar.message
color: "#E5E7EB"
font.pixelSize: 13
elide: Text.ElideRight
verticalAlignment: Text.AlignVCenter
}
Text {
text: qsTr("%1 帧").arg(longBar.frameCount)
color: "#D1D5DB"
font.pixelSize: 12
verticalAlignment: Text.AlignVCenter
}
Rectangle {
width: 72
height: 30
radius: 6
visible: !longBar.running
color: startArea.containsMouse ? "#1D4ED8" : "#2563EB"
Layout.alignment: Qt.AlignVCenter
Behavior on color { ColorAnimation { duration: 100 } }
Text {
anchors.centerIn: parent
text: qsTr("开始")
color: "#FFFFFF"
font.pixelSize: 13
}
MouseArea {
id: startArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: longBar.startRequested()
}
}
Rectangle {
width: 72
height: 30
radius: 6
color: stopArea.containsMouse ? "#DC2626" : "#EF4444"
Layout.alignment: Qt.AlignVCenter
Behavior on color { ColorAnimation { duration: 100 } }
Text {
anchors.centerIn: parent
text: longBar.running ? qsTr("完成") : qsTr("关闭")
color: "#FFFFFF"
font.pixelSize: 13
}
MouseArea {
id: stopArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: longBar.stopRequested()
}
}
}
}
}
-53
View File
@@ -1,53 +0,0 @@
import QtQuick
import QtQuick.Window
Window {
id: longFrame
visible: false
color: "transparent"
flags: Qt.Tool | Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint | Qt.WindowTransparentForInput
property rect captureRect: Qt.rect(0, 0, 0, 0)
x: captureRect.x - 2
y: captureRect.y - 2
width: Math.max(0, captureRect.width) + 4
height: Math.max(0, captureRect.height) + 4
Rectangle {
anchors.fill: parent
color: "transparent"
border.color: "#0EA5E9"
border.width: 2
radius: 2
Rectangle {
x: 0
y: 0
width: 6
height: 6
color: "#0EA5E9"
}
Rectangle {
x: parent.width - width
y: 0
width: 6
height: 6
color: "#0EA5E9"
}
Rectangle {
x: 0
y: parent.height - height
width: 6
height: 6
color: "#0EA5E9"
}
Rectangle {
x: parent.width - width
y: parent.height - height
width: 6
height: 6
color: "#0EA5E9"
}
}
}
-83
View File
@@ -1,83 +0,0 @@
import QtQuick
import QtQuick.Window
Window {
id: longPreview
width: 260
height: 420
visible: false
color: "transparent"
flags: Qt.Tool | Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint | Qt.WindowDoesNotAcceptFocus
property string previewUrl: ""
property string statusText: ""
property int frameCount: 0
Rectangle {
anchors.fill: parent
radius: 12
color: "#111827"
border.color: "#1F2937"
border.width: 1
Column {
anchors.fill: parent
anchors.margins: 10
spacing: 8
Row {
width: parent.width
spacing: 8
Text {
text: qsTr("长截图预览")
color: "#F9FAFB"
font.pixelSize: 13
font.bold: true
}
Text {
text: qsTr("%1 帧").arg(longPreview.frameCount)
color: "#9CA3AF"
font.pixelSize: 12
}
}
Rectangle {
width: parent.width
height: parent.height - 58
radius: 8
color: "#0B1220"
border.color: "#334155"
border.width: 1
clip: true
Image {
anchors.fill: parent
anchors.margins: 6
source: longPreview.previewUrl
fillMode: Image.PreserveAspectFit
smooth: true
cache: false
}
Text {
anchors.centerIn: parent
visible: longPreview.previewUrl.length === 0
text: qsTr("暂无预览")
color: "#64748B"
font.pixelSize: 12
}
}
Text {
width: parent.width
text: longPreview.statusText
color: "#CBD5E1"
font.pixelSize: 12
elide: Text.ElideRight
}
}
}
}
-128
View File
@@ -1,128 +0,0 @@
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 }
}
}
-500
View File
@@ -1,500 +0,0 @@
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.preferredHeight: 8 }
// ── GIF 体积预设 ─────────────────────────────
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("GIF 录制")
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: gifPresetCombo
model: [qsTr("高质量"), qsTr("均衡"), qsTr("小体积")]
currentIndex: O_GifRecordVM.qualityPreset
Layout.preferredWidth: 180
Layout.preferredHeight: 36
onCurrentIndexChanged: O_GifRecordVM.setQualityPreset(currentIndex)
}
}
}
Item { Layout.preferredHeight: 8 }
// ── OCR 自检 ─────────────────────────────────
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("OCR 自检")
font.pixelSize: 13
font.bold: true
color: "#1E293B"
}
}
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 168
color: "#FFFFFF"
radius: 10
border.color: "#E2E8F0"
border.width: 1
ColumnLayout {
anchors.fill: parent
anchors.margins: 12
spacing: 10
Rectangle {
Layout.fillWidth: true
Layout.fillHeight: true
radius: 8
color: "#F8FAFC"
border.color: "#E2E8F0"
border.width: 1
ScrollView {
anchors.fill: parent
anchors.margins: 8
clip: true
TextArea {
text: O_OcrVM.selfCheckText.length > 0
? O_OcrVM.selfCheckText
: qsTr("点击“一键自检”查看 OCR 环境状态。")
readOnly: true
wrapMode: TextEdit.Wrap
selectByMouse: true
background: null
font.pixelSize: 12
color: "#334155"
}
}
}
RowLayout {
Layout.fillWidth: true
spacing: 8
Rectangle {
Layout.preferredWidth: 100
Layout.preferredHeight: 34
radius: 6
color: selfCheckMouse.containsMouse ? "#2563EB" : "#3B82F6"
Behavior on color { ColorAnimation { duration: 100 } }
Text {
anchors.centerIn: parent
text: qsTr("一键自检")
font.pixelSize: 13
color: "#FFFFFF"
}
MouseArea {
id: selfCheckMouse
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: O_OcrVM.runSelfCheck()
}
}
Rectangle {
Layout.preferredWidth: 130
Layout.preferredHeight: 34
radius: 6
color: openTessMouse.containsMouse ? "#E2E8F0" : "#F1F5F9"
border.color: "#CBD5E1"
border.width: 1
Behavior on color { ColorAnimation { duration: 100 } }
Text {
anchors.centerIn: parent
text: qsTr("打开 tessdata")
font.pixelSize: 13
color: "#334155"
}
MouseArea {
id: openTessMouse
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (!O_OcrVM.openTessdataFolder())
O_OcrVM.runSelfCheck()
}
}
}
}
}
}
Item { Layout.fillHeight: true }
}
}
}
-373
View File
@@ -1,373 +0,0 @@
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls.Basic
Rectangle {
color: "#F8FAFC"
// ── 按键录制弹窗 ──────────────────────────────────────
Rectangle {
id: recordOverlay
anchors.fill: parent
color: "#80000000"
visible: false
z: 100
property int recordingActionId: 0
property string recordingLabel: ""
property string pendingKeySeq: ""
// 捕获键盘焦点以接收按键事件
focus: visible
Keys.onPressed: function(event) {
event.accepted = true
var seq = O_GlobalShortcut.buildKeySequence(event.key, event.modifiers)
if (seq !== "") {
recordOverlay.pendingKeySeq = seq
}
}
// 弹窗卡片
Rectangle {
anchors.centerIn: parent
width: 360
height: 220
radius: 12
color: "#FFFFFF"
border.color: "#E2E8F0"
border.width: 1
ColumnLayout {
anchors.fill: parent
anchors.margins: 24
spacing: 16
// 标题
Text {
Layout.fillWidth: true
text: qsTr("编辑快捷键 · %1").arg(recordOverlay.recordingLabel)
font.pixelSize: 14
font.bold: true
color: "#1E293B"
}
// 录制区域
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 56
radius: 8
color: "#F1F5F9"
border.color: recordOverlay.pendingKeySeq !== "" ? "#3B82F6" : "#CBD5E1"
border.width: recordOverlay.pendingKeySeq !== "" ? 2 : 1
Behavior on border.color { ColorAnimation { duration: 120 } }
Text {
anchors.centerIn: parent
text: recordOverlay.pendingKeySeq !== ""
? recordOverlay.pendingKeySeq
: qsTr("请按下新的快捷键组合...")
font.pixelSize: 15
font.family: "Monospace"
color: recordOverlay.pendingKeySeq !== "" ? "#1E293B" : "#94A3B8"
}
}
// 冲突提示
Text {
id: conflictHint
Layout.fillWidth: true
font.pixelSize: 12
color: "#EF4444"
visible: false
wrapMode: Text.WordWrap
}
// 按钮行
RowLayout {
Layout.fillWidth: true
spacing: 8
// 取消
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 36
radius: 6
color: cancelArea.containsMouse ? "#F1F5F9" : "#F8FAFC"
border.color: "#E2E8F0"
Behavior on color { ColorAnimation { duration: 100 } }
Text {
anchors.centerIn: parent
text: qsTr("取消")
font.pixelSize: 13
color: "#64748B"
}
MouseArea {
id: cancelArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
recordOverlay.visible = false
recordOverlay.pendingKeySeq = ""
conflictHint.visible = false
}
}
}
// 确认
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 36
radius: 6
color: {
if (recordOverlay.pendingKeySeq === "") return "#93C5FD"
return confirmArea.containsMouse ? "#2563EB" : "#3B82F6"
}
Behavior on color { ColorAnimation { duration: 100 } }
Text {
anchors.centerIn: parent
text: qsTr("确认")
font.pixelSize: 13
color: "#FFFFFF"
}
MouseArea {
id: confirmArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
enabled: recordOverlay.pendingKeySeq !== ""
onClicked: {
var seq = recordOverlay.pendingKeySeq
var aid = recordOverlay.recordingActionId
// 检查冲突
var conflictId = O_GlobalShortcut.checkConflict(seq)
if (conflictId !== 0 && conflictId !== aid) {
var labels = ["", qsTr("截图"), qsTr("截图并保存"), qsTr("贴图到桌面"), qsTr("显示/隐藏窗口")]
conflictHint.text = "「" + seq + "」已被「" + labels[conflictId] + "」使用,请换一个"
conflictHint.visible = true
return
}
// 应用
if (O_GlobalShortcut.updateShortcut(aid, seq)) {
recordOverlay.visible = false
recordOverlay.pendingKeySeq = ""
conflictHint.visible = false
} else {
conflictHint.text = "快捷键「" + seq + "」注册失败,请换一个组合"
conflictHint.visible = true
}
}
}
}
}
}
}
}
// ── 主内容区 ──────────────────────────────────────────
ScrollView {
anchors.fill: parent
contentWidth: availableWidth
clip: true
ColumnLayout {
width: parent.width
spacing: 8
anchors.left: parent.left
anchors.right: parent.right
anchors.margins: 24
anchors.topMargin: 24
Item { Layout.preferredHeight: 4 }
// ── 标题 ────────────────────────────────────
RowLayout {
Layout.fillWidth: true
Layout.topMargin: 8
spacing: 8
Rectangle {
width: 3; height: 15; radius: 2
color: "#3B82F6"
Layout.alignment: Qt.AlignVCenter
}
Text {
text: qsTr("全局快捷键")
font.pixelSize: 13
font.bold: true
color: "#1E293B"
}
}
// ── 快捷键列表卡片 ───────────────────────────
Rectangle {
Layout.fillWidth: true
implicitHeight: hotkeyCol.implicitHeight + 32
color: "#FFFFFF"
radius: 10
border.color: "#E2E8F0"
border.width: 1
ColumnLayout {
id: hotkeyCol
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.margins: 16
spacing: 0
Repeater {
// 动态从后端读取当前快捷键
model: [
{ label: qsTr("截图"), actionId: 1, keySeq: O_GlobalShortcut.shortcutScreenshot },
{ label: qsTr("截图并保存"), actionId: 2, keySeq: O_GlobalShortcut.shortcutScreenshotSave },
{ label: qsTr("贴图到桌面"), actionId: 3, keySeq: O_GlobalShortcut.shortcutSticky },
{ label: qsTr("显示/隐藏窗口"), actionId: 4, keySeq: O_GlobalShortcut.shortcutToggle }
]
delegate: ColumnLayout {
Layout.fillWidth: true
spacing: 0
RowLayout {
Layout.fillWidth: true
Layout.topMargin: 12
Layout.bottomMargin: 12
// 功能名称
Text {
Layout.preferredWidth: 120
text: modelData.label
font.pixelSize: 13
color: "#374151"
Layout.alignment: Qt.AlignVCenter
}
// 快捷键徽章
RowLayout {
spacing: 4
Layout.alignment: Qt.AlignVCenter
Repeater {
model: modelData.keySeq.split("+")
delegate: Rectangle {
implicitWidth: kbLabel.implicitWidth + 16
implicitHeight: 28
radius: 5
color: "#F1F5F9"
border.color: "#CBD5E1"
border.width: 1
Text {
id: kbLabel
anchors.centerIn: parent
text: modelData
font.pixelSize: 12
font.family: "Monospace"
color: "#374151"
}
}
}
}
Item { Layout.fillWidth: true }
// 编辑按钮
Rectangle {
implicitWidth: 60
implicitHeight: 28
radius: 5
color: editArea.containsMouse ? "#EFF6FF" : "#F8FAFC"
border.color: editArea.containsMouse ? "#3B82F6" : "#E2E8F0"
border.width: 1
Behavior on color { ColorAnimation { duration: 100 } }
Behavior on border.color{ ColorAnimation { duration: 100 } }
Text {
anchors.centerIn: parent
text: qsTr("编辑")
font.pixelSize: 12
color: editArea.containsMouse ? "#3B82F6" : "#64748B"
Behavior on color { ColorAnimation { duration: 100 } }
}
MouseArea {
id: editArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
recordOverlay.recordingActionId = modelData.actionId
recordOverlay.recordingLabel = modelData.label
recordOverlay.pendingKeySeq = ""
conflictHint.visible = false
recordOverlay.visible = true
recordOverlay.forceActiveFocus()
}
}
}
}
// 分割线(最后一行不显示)
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 1
color: "#F1F5F9"
visible: index < 3
}
}
}
}
}
Item { Layout.preferredHeight: 8 }
// ── 说明 ────────────────────────────────────
RowLayout {
Layout.fillWidth: true
Layout.topMargin: 8
spacing: 8
Rectangle {
width: 3; height: 15; radius: 2
color: "#3B82F6"
Layout.alignment: Qt.AlignVCenter
}
Text {
text: qsTr("说明")
font.pixelSize: 13
font.bold: true
color: "#1E293B"
}
}
Rectangle {
Layout.fillWidth: true
implicitHeight: noteText.implicitHeight + 24
color: "#FFFFFF"
radius: 10
border.color: "#E2E8F0"
border.width: 1
Text {
id: noteText
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.margins: 16
text: qsTr("快捷键在全局范围内生效。点击「编辑」后直接按下新的组合键,再点「确认」即可保存。\n若与系统或其他应用快捷键冲突,请修改为其他组合。")
font.pixelSize: 13
color: "#64748B"
wrapMode: Text.WordWrap
lineHeight: 1.6
}
}
Item { Layout.fillHeight: true }
}
}
}
-166
View File
@@ -1,166 +0,0 @@
import QtQuick
import QtQuick.Layouts
Rectangle {
signal signalSetting()
signal signalHotkeySettings()
property string selectName: "setting"
color: "#F1F5F9"
ColumnLayout {
anchors.fill: parent
spacing: 0
// 顶部 Logo / 标题区
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 72
color: "transparent"
ColumnLayout {
anchors.centerIn: parent
spacing: 4
Rectangle {
width: 36
height: 36
radius: 10
color: "#3B82F6"
Layout.alignment: Qt.AlignHCenter
Text {
anchors.centerIn: parent
text: "T"
font.pixelSize: 20
font.bold: true
color: "#FFFFFF"
}
}
Text {
Layout.alignment: Qt.AlignHCenter
text: "TZshot"
font.pixelSize: 12
color: "#64748B"
}
}
}
// 分割线
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 1
Layout.leftMargin: 16
Layout.rightMargin: 16
color: "#E2E8F0"
}
Item { Layout.preferredHeight: 12 }
// ── 通用设置 菜单项 ──────────────────────
Item {
Layout.fillWidth: true
Layout.preferredHeight: 44
Layout.leftMargin: 10
Layout.rightMargin: 10
Rectangle {
anchors.fill: parent
radius: 8
color: selectName === "setting" ? "#FFFFFF" : "transparent"
border.color: selectName === "setting" ? "#E2E8F0" : "transparent"
border.width: 1
}
Rectangle {
width: 3
height: 20
radius: 2
anchors.left: parent.left
anchors.leftMargin: 6
anchors.verticalCenter: parent.verticalCenter
color: selectName === "setting" ? "#3B82F6" : "transparent"
}
Text {
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
anchors.leftMargin: 18
text: qsTr("通用设置")
font.pixelSize: 13
font.bold: selectName === "setting"
color: selectName === "setting" ? "#3B82F6" : "#64748B"
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
selectName = "setting"
signalSetting()
}
}
}
Item { Layout.preferredHeight: 4 }
// ── 快捷键设置 菜单项 ────────────────────
Item {
Layout.fillWidth: true
Layout.preferredHeight: 44
Layout.leftMargin: 10
Layout.rightMargin: 10
Rectangle {
anchors.fill: parent
radius: 8
color: selectName === "shortcutKey" ? "#FFFFFF" : "transparent"
border.color: selectName === "shortcutKey" ? "#E2E8F0" : "transparent"
border.width: 1
}
Rectangle {
width: 3
height: 20
radius: 2
anchors.left: parent.left
anchors.leftMargin: 6
anchors.verticalCenter: parent.verticalCenter
color: selectName === "shortcutKey" ? "#3B82F6" : "transparent"
}
Text {
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
anchors.leftMargin: 18
text: qsTr("快捷键设置")
font.pixelSize: 13
font.bold: selectName === "shortcutKey"
color: selectName === "shortcutKey" ? "#3B82F6" : "#64748B"
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
selectName = "shortcutKey"
signalHotkeySettings()
}
}
}
// 底部弹性空间
Item { Layout.fillHeight: true }
// 版本号
Text {
Layout.alignment: Qt.AlignHCenter
Layout.bottomMargin: 12
text: "v1.0.0"
font.pixelSize: 11
color: "#CBD5E1"
}
}
}
-77
View File
@@ -1,77 +0,0 @@
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
}
}
}
}
-128
View File
@@ -1,128 +0,0 @@
import QtQuick
import QtQuick.Controls.Basic
ComboBox {
id: control
property color fillColor: "#F8FAFC"
property color borderColor: "#E2E8F0"
property color activeBorderColor: "#3B82F6"
property color hoverColor: "#F1F5F9"
property color textColor: "#1E293B"
property color dimTextColor: "#64748B"
width: 120
height: 36
font.pixelSize: 13
font.family: "Microsoft YaHei"
// 当前选中文字
contentItem: Text {
leftPadding: 12
rightPadding: 28
text: control.displayText
font: control.font
color: control.enabled ? textColor : dimTextColor
horizontalAlignment: Text.AlignLeft
verticalAlignment: Text.AlignVCenter
elide: Text.ElideRight
}
// 背景
background: Rectangle {
implicitWidth: control.width
implicitHeight: control.height
color: control.hovered ? hoverColor : fillColor
border.color: control.activeFocus ? activeBorderColor : borderColor
border.width: control.activeFocus ? 2 : 1
radius: 6
Behavior on border.color { ColorAnimation { duration: 150 } }
Behavior on color { ColorAnimation { duration: 120 } }
}
// 下拉箭头
indicator: Canvas {
id: canvas
x: control.width - width - 10
y: (control.height - height) / 2
width: 10
height: 6
contextType: "2d"
onPaint: {
context.reset()
context.lineWidth = 1.5
context.strokeStyle = dimTextColor
context.lineCap = "round"
context.lineJoin = "round"
context.moveTo(0, 0)
context.lineTo(width / 2, height)
context.lineTo(width, 0)
context.stroke()
}
}
// 下拉列表每项
delegate: ItemDelegate {
width: control.width
height: 36
contentItem: Text {
leftPadding: 12
text: modelData
font: control.font
color: highlighted ? "#3B82F6" : textColor
verticalAlignment: Text.AlignVCenter
elide: Text.ElideRight
}
background: Rectangle {
color: highlighted ? "#EFF6FF" : fillColor
}
}
// 弹出框
popup: Popup {
y: control.height + 4
width: control.width
implicitHeight: listview.contentHeight
padding: 4
contentItem: ListView {
id: listview
clip: true
model: control.popup.visible ? control.delegateModel : null
currentIndex: control.highlightedIndex
ScrollIndicator.vertical: ScrollIndicator {}
}
background: Rectangle {
color: "#FFFFFF"
border.color: "#E2E8F0"
border.width: 1
radius: 8
// 顶部细线装饰
Rectangle {
width: parent.width - 2
height: 1
anchors.top: parent.top
anchors.topMargin: 1
anchors.horizontalCenter: parent.horizontalCenter
color: "#3B82F6"
opacity: 0.4
}
}
enter: Transition {
NumberAnimation { property: "opacity"; from: 0.0; to: 1.0; duration: 120 }
NumberAnimation { property: "scale"; from: 0.95; to: 1.0; duration: 120 }
}
exit: Transition {
NumberAnimation { property: "opacity"; from: 1.0; to: 0.0; duration: 80 }
}
}
}
-2
View File
@@ -1,2 +0,0 @@
module TZControls
TZComboBox 1.0 TZComboBox.qml
-311
View File
@@ -1,311 +0,0 @@
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, targetScreen) {
if (targetScreen)
win.screen = targetScreen
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
onPressed: function(mouse) {
mouse.accepted = true
win.startSystemMove()
}
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
Layout.minimumHeight: 140
Layout.preferredHeight: 180
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()
}
}
}
}
}
}
}
Rectangle {
id: resizeHandle
width: 16
height: 16
radius: 8
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.rightMargin: 8
anchors.bottomMargin: 8
color: resizeMouse.pressed ? "#0369A1" : "#0EA5E9"
border.color: "white"
border.width: 1
z: 10
Canvas {
anchors.fill: parent
anchors.margins: 3
onPaint: {
var ctx = getContext("2d")
ctx.reset()
ctx.strokeStyle = "white"
ctx.lineWidth = 1.2
ctx.beginPath()
ctx.moveTo(width * 0.25, height)
ctx.lineTo(width, height * 0.25)
ctx.stroke()
ctx.beginPath()
ctx.moveTo(width * 0.55, height)
ctx.lineTo(width, height * 0.55)
ctx.stroke()
}
}
MouseArea {
id: resizeMouse
anchors.fill: parent
cursorShape: Qt.SizeFDiagCursor
onPressed: function(mouse) {
mouse.accepted = true
win.startSystemResize(Qt.RightEdge | Qt.BottomEdge)
}
}
}
}
}
}
-328
View File
@@ -1,328 +0,0 @@
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
}
}
}
-317
View File
@@ -1,317 +0,0 @@
import QtQuick
import QtQuick.Window
import Qt5Compat.GraphicalEffects
import Qt.labs.platform 1.0 as Platform
import "../Capture"
import CustomComponents 1.0
Window {
id: stickyWin
flags: Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint | Qt.Tool
color: "transparent"
visible: true
property string imageUrl: ""
property rect imgRect: Qt.rect(0, 0, 0, 0)
property bool cleanedUp: false
property size sourceImageSize: Qt.size(1, 1)
property bool toolbarVisible: true
property real zoomFactor: 1.0
property int windowWidthValue: 1
property int windowHeightValue: 1
readonly property int shadowPadding: 12
readonly property color shadowColor: "#2F8FFF"
readonly property int toolbarHeight: toolBar.implicitHeight
readonly property int toolbarGap: 8
readonly property int toolbarTipReserve: 30
readonly property int markToolGap: 8
readonly property int contentX: stickyWin.shadowPadding
readonly property int contentY: stickyWin.shadowPadding
readonly property int contentWidth: Math.max(1, stickyWin.width - stickyWin.shadowPadding * 2)
readonly property int contentHeight: Math.max(1, stickyWin.height - stickyWin.shadowPadding * 2
- (stickyWin.toolbarVisible ? (stickyWin.toolbarHeight + stickyWin.toolbarGap + stickyWin.toolbarTipReserve) : 0))
width: windowWidthValue
height: windowHeightValue
minimumWidth: windowWidthValue
maximumWidth: windowWidthValue
minimumHeight: windowHeightValue
maximumHeight: windowHeightValue
function updateImageSize() {
sourceImageSize = O_StickyViewModel.getImageSizeByUrl(imageUrl)
if (!sourceImageSize || sourceImageSize.width <= 0 || sourceImageSize.height <= 0)
sourceImageSize = Qt.size(1, 1)
updateWindowSize()
}
function updateWindowSize() {
var imageWidth = Math.max(1, Math.floor(sourceImageSize.width * zoomFactor))
var imageHeight = Math.max(1, Math.floor(sourceImageSize.height * zoomFactor))
windowWidthValue = imageWidth + shadowPadding * 2
windowHeightValue = imageHeight + shadowPadding * 2
+ (toolbarVisible ? (toolbarHeight + toolbarGap + toolbarTipReserve) : 0)
Qt.callLater(applyNativeSize)
}
function applyNativeSize() {
O_StickyViewModel.resizeStickyWindow(stickyWin, windowWidthValue, windowHeightValue)
}
function setZoomFactor(value) {
zoomFactor = Math.max(0.3, Math.min(4.0, value))
updateWindowSize()
}
function resetDisplaySize() {
setZoomFactor(1.0)
}
function syncInitialPosition() {
O_StickyViewModel.positionStickyWindow(stickyWin,
Qt.rect(imgRect.x - shadowPadding,
imgRect.y - shadowPadding,
imgRect.width + shadowPadding * 2,
imgRect.height + shadowPadding * 2))
}
function cleanup() {
if (cleanedUp)
return
cleanedUp = true
chatPanel.close()
O_StickyViewModel.releaseImage(imageUrl)
imageUrl = ""
}
function syncPaintBoardBackground() {
if (!imageUrl) {
paintBoard.reset()
return
}
var bg = O_StickyViewModel.getImageByUrl(imageUrl)
paintBoard.reset()
paintBoard.setBackgroundImg(bg)
}
function openChatPanel() {
var targetScreen = stickyWin.screen
if (!targetScreen || !targetScreen.availableGeometry) {
chatPanel.open(stickyWin.x, stickyWin.y + stickyWin.height + 6, targetScreen)
return
}
var available = targetScreen.availableGeometry
var panelX = stickyWin.x
var panelY = stickyWin.y + stickyWin.height + 6
if (panelY + chatPanel.win.height > available.y + available.height)
panelY = stickyWin.y - chatPanel.win.height - 6
panelX = Math.max(available.x, Math.min(panelX, available.x + available.width - chatPanel.win.width))
panelY = Math.max(available.y, Math.min(panelY, available.y + available.height - chatPanel.win.height))
chatPanel.open(Math.round(panelX), Math.round(panelY), targetScreen)
}
Component.onCompleted: {
updateImageSize()
syncPaintBoardBackground()
syncInitialPosition()
applyNativeSize()
stickyWin.raise()
stickyWin.requestActivate()
}
onImageUrlChanged: {
updateImageSize()
syncPaintBoardBackground()
}
function onClosing(close) {
close.accepted = true
cleanup()
Qt.callLater(function() { stickyWin.destroy() })
}
RectangularGlow {
id: stickyShadow
x: stickyWin.contentX
y: stickyWin.contentY
width: stickyWin.contentWidth
height: stickyWin.contentHeight
glowRadius: 10
spread: 0.08
color: stickyWin.shadowColor
cornerRadius: 2
opacity: 0.45
cached: true
}
PaintBoard {
id: paintBoard
x: stickyWin.contentX
y: stickyWin.contentY
width: stickyWin.contentWidth
height: stickyWin.contentHeight
z: 5
visible: true
enabled: toolBar.activeTool !== ""
shapeType: toolBar.shapeType
penColor: markToolInfo.currentColor
penSize: markToolInfo.currentSize
}
TZTool {
id: toolBar
visible: stickyWin.toolbarVisible
x: Math.max(0, Math.floor((stickyWin.width - width) / 2))
y: stickyWin.contentY + stickyWin.contentHeight + stickyWin.toolbarGap
showScreenshotButton: false
showCancelButton: false
showSaveButton: false
showStickyButton: false
showGifRecordButton: false
showLongScreenshotButton: false
showHighlightButton: false
showTextButton: false
showNumberButton: false
showRectButton: false
showCircleButton: false
showArrowButton: false
showMosaicButton: false
showPencilButton: true
showUndoButton: true
onSignalOcr: {}
onSignalUndo: paintBoard.undo()
}
TZMarkToolInfo {
id: markToolInfo
visible: toolBar.visible && toolBar.markToolInfoVisible && toolBar.activeTool === "pencil"
z: 6
x: Math.max(stickyWin.shadowPadding, Math.min(stickyWin.width - width - stickyWin.shadowPadding, toolBar.x))
y: Math.max(stickyWin.shadowPadding, toolBar.y - height - stickyWin.markToolGap)
toolName: toolBar.activeTool
showColorRow: true
}
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
syncPaintBoardBackground()
Qt.callLater(function() { O_StickyViewModel.releaseImage(old) })
}
function onSignalRequestFailed(errorMsg) {
console.warn("[TZStickyWindow] AI request failed: " + errorMsg)
}
}
Platform.Menu {
id: contextMenu
Platform.MenuItem {
text: qsTr("复制")
onTriggered: O_StickyViewModel.copyImageToClipboard(stickyWin.imageUrl)
}
Platform.MenuItem {
text: qsTr("AI 编辑")
onTriggered: stickyWin.openChatPanel()
}
Platform.MenuItem {
text: stickyWin.toolbarVisible ? qsTr("隐藏工具栏") : qsTr("显示工具栏")
onTriggered: {
stickyWin.toolbarVisible = !stickyWin.toolbarVisible
updateWindowSize()
}
}
Platform.MenuItem {
text: qsTr("恢复原图大小")
onTriggered: stickyWin.resetDisplaySize()
}
Platform.MenuSeparator {}
Platform.MenuItem {
text: qsTr("保存原图")
onTriggered: saveDialog.open()
}
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(stickyWin.imageUrl, saveDialog.file)
}
MouseArea {
id: dragArea
x: stickyWin.contentX
y: stickyWin.contentY
width: stickyWin.contentWidth
height: stickyWin.contentHeight
enabled: toolBar.activeTool === ""
acceptedButtons: Qt.LeftButton
property point pressOffset: Qt.point(0, 0)
onPressed: function(mouse) {
pressOffset = Qt.point(mouse.x, mouse.y)
}
onPositionChanged: function(mouse) {
if (pressed && (mouse.buttons & Qt.LeftButton)) {
stickyWin.x += mouse.x - pressOffset.x
stickyWin.y += mouse.y - pressOffset.y
}
}
onWheel: function(wheel) {
if (wheel.angleDelta.y > 0)
stickyWin.setZoomFactor(stickyWin.zoomFactor + 0.1)
else if (wheel.angleDelta.y < 0)
stickyWin.setZoomFactor(stickyWin.zoomFactor - 0.1)
wheel.accepted = true
}
}
MouseArea {
x: stickyWin.contentX
y: stickyWin.contentY
width: stickyWin.contentWidth
height: stickyWin.contentHeight + (stickyWin.toolbarVisible ? (stickyWin.toolbarHeight + stickyWin.toolbarGap + stickyWin.toolbarTipReserve) : 0)
acceptedButtons: Qt.RightButton
onClicked: function(mouse) {
if (mouse.button === Qt.RightButton)
contextMenu.open()
}
}
}
-339
View File
@@ -1,339 +0,0 @@
import QtQuick
import QtQuick.Window
import QtQuick.Shapes
import QtQuick.Layouts
import com.Tz.tray 1.0
import "Capture"
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 longCaptureBarObj: null
property var longCaptureBarComponent: null
property var longCapturePreviewObj: null
property var longCapturePreviewComponent: null
property var longCaptureFrameObj: null
property var longCaptureFrameComponent: null
property rect pendingLongCaptureRect: Qt.rect(0, 0, 0, 0)
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
_prepareSessionState()
Qt.callLater(function() {
O_ScreenCapture.captureDesktop()
var virtualRect = O_ScreenCapture.virtualGeometry()
if (virtualRect.width <= 0 || virtualRect.height <= 0)
return
root.screenVirtualX = virtualRect.x
root.screenVirtualY = virtualRect.y
O_ScreenCapture.grabToPaintBoard(
root.paintBoard,
Qt.rect(root.screenVirtualX, root.screenVirtualY, root.width, root.height)
)
root.visibility = Window.Windowed
root.raise()
root.requestActivate()
Qt.callLater(function() {
root.captureUiReady = true
Qt.callLater(function() {
root.opacity = 1.0
})
})
})
}
function showLongCaptureBar() {
if (longCaptureBarObj !== null) {
longCaptureBarObj.visible = true
longCaptureBarObj.raise()
return
}
if (longCaptureBarComponent === null)
longCaptureBarComponent = Qt.createComponent("qrc:/qml/LongCapture/TZLongCaptureBar.qml")
if (longCaptureBarComponent.status === Component.Ready) {
longCaptureBarObj = longCaptureBarComponent.createObject(null)
if (longCaptureBarObj) {
longCaptureBarObj.x = Math.round((Screen.width - longCaptureBarObj.width) / 2)
longCaptureBarObj.y = 26
longCaptureBarObj.visible = true
longCaptureBarObj.raise()
longCaptureBarObj.startRequested.connect(function() {
if (pendingLongCaptureRect.width === 0 || pendingLongCaptureRect.height === 0)
return
longCaptureBarObj.running = true
longCaptureBarObj.message = qsTr("滚动检测中,滑动内容会自动拼接")
O_ScrollCaptureVM.start(pendingLongCaptureRect)
})
longCaptureBarObj.stopRequested.connect(function() {
if (longCaptureBarObj.running) {
O_ScrollCaptureVM.stop()
} else {
hideLongCaptureBar()
root.resetArea()
}
})
}
} else if (longCaptureBarComponent.status === Component.Error) {
console.error("LongCaptureBar load failed:", longCaptureBarComponent.errorString())
}
}
function showLongCapturePreview() {
if (longCapturePreviewObj !== null) {
longCapturePreviewObj.visible = true
longCapturePreviewObj.raise()
return
}
if (longCapturePreviewComponent === null)
longCapturePreviewComponent = Qt.createComponent("qrc:/qml/LongCapture/TZLongCapturePreview.qml")
if (longCapturePreviewComponent.status === Component.Ready) {
longCapturePreviewObj = longCapturePreviewComponent.createObject(null)
if (longCapturePreviewObj) {
longCapturePreviewObj.x = Math.max(0, Screen.width - longCapturePreviewObj.width - 26)
longCapturePreviewObj.y = Math.max(20, Math.round((Screen.height - longCapturePreviewObj.height) / 2))
longCapturePreviewObj.visible = true
longCapturePreviewObj.raise()
}
} else if (longCapturePreviewComponent.status === Component.Error) {
console.error("LongCapturePreview load failed:", longCapturePreviewComponent.errorString())
}
}
function showLongCaptureFrame(captureRect) {
if (longCaptureFrameComponent === null)
longCaptureFrameComponent = Qt.createComponent("qrc:/qml/LongCapture/TZLongCaptureFrame.qml")
if (longCaptureFrameObj === null && longCaptureFrameComponent.status === Component.Ready) {
longCaptureFrameObj = longCaptureFrameComponent.createObject(null)
} else if (longCaptureFrameComponent.status === Component.Error) {
console.error("LongCaptureFrame load failed:", longCaptureFrameComponent.errorString())
}
if (longCaptureFrameObj) {
longCaptureFrameObj.captureRect = captureRect
longCaptureFrameObj.visible = true
longCaptureFrameObj.raise()
}
}
function hideLongCaptureBar() {
if (longCaptureBarObj !== null) {
longCaptureBarObj.close()
longCaptureBarObj.destroy(100)
longCaptureBarObj = null
}
if (longCapturePreviewObj !== null) {
longCapturePreviewObj.close()
longCapturePreviewObj.destroy(100)
longCapturePreviewObj = null
}
if (longCaptureFrameObj !== null) {
longCaptureFrameObj.close()
longCaptureFrameObj.destroy(100)
longCaptureFrameObj = null
}
pendingLongCaptureRect = Qt.rect(0, 0, 0, 0)
}
function beginLongCapture(captureRect) {
pendingLongCaptureRect = captureRect
root.visibility = Window.Hidden
showLongCaptureBar()
showLongCapturePreview()
showLongCaptureFrame(captureRect)
if (longCaptureBarObj) {
longCaptureBarObj.running = false
longCaptureBarObj.frameCount = 0
longCaptureBarObj.message = qsTr("点击开始后,滑动内容自动拼接")
}
if (longCapturePreviewObj) {
longCapturePreviewObj.previewUrl = ""
longCapturePreviewObj.frameCount = 0
longCapturePreviewObj.statusText = qsTr("等待开始")
}
}
function resetArea() {
_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() {
O_WidgetWindows.showSettings()
}
Connections {
target: O_StickyViewModel
function onStickyReady(imageUrl, imgRect) {
root.createStickyWindow(imageUrl, imgRect)
}
}
Connections {
target: O_OcrVM
function onRecognizeFailed(errorMessage) {
trayHelper.showMessage(qsTr("OCR 识别失败"), errorMessage)
}
}
Connections {
target: O_ScrollCaptureVM
function onCaptureStarted() {
if (longCaptureBarObj) {
longCaptureBarObj.running = true
longCaptureBarObj.message = qsTr("滚动检测中,滑动内容会自动拼接")
}
trayHelper.showMessage(qsTr("长截图开始"), qsTr("请直接滚动目标内容,系统会在滚动变化时自动拼接"))
}
function onCaptureSucceeded(savedPath) {
if (longCaptureBarObj) {
longCaptureBarObj.running = false
longCaptureBarObj.message = qsTr("长截图完成")
}
root.hideLongCaptureBar()
root.resetArea()
trayHelper.showMessage(qsTr("长截图完成"), qsTr("已复制到剪贴板并保存到:") + savedPath)
}
function onCaptureFailed(errorMessage) {
if (longCaptureBarObj) {
longCaptureBarObj.running = false
longCaptureBarObj.message = qsTr("长截图失败")
}
root.hideLongCaptureBar()
root.resetArea()
trayHelper.showMessage(qsTr("长截图失败"), errorMessage)
}
function onCapturedFramesChanged() {
if (longCaptureBarObj)
longCaptureBarObj.frameCount = O_ScrollCaptureVM.capturedFrames
if (longCapturePreviewObj)
longCapturePreviewObj.frameCount = O_ScrollCaptureVM.capturedFrames
}
function onStatusTextChanged() {
if (longCaptureBarObj)
longCaptureBarObj.message = O_ScrollCaptureVM.statusText
if (longCapturePreviewObj)
longCapturePreviewObj.statusText = O_ScrollCaptureVM.statusText
}
function onPreviewImageUrlChanged() {
if (longCapturePreviewObj)
longCapturePreviewObj.previewUrl = O_ScrollCaptureVM.previewImageUrl
}
}
Connections {
target: O_InstanceBridge
function onActivationRequested() {
if (root.visible) {
root.raise()
root.requestActivate()
return
}
startScreenshot()
}
}
TrayIconHelper {
id: trayHelper
function onTrayClicked(clickType) {
if (clickType === 0 || clickType === 2)
startScreenshot()
}
onScreenshotTriggered: startScreenshot()
onShowSettingTriggered: { showSettingWin() }
onShowAboutTriggered: { showAboutWin() }
onExitTriggered: Qt.quit()
}
// MouseArea
// {
// anchors.fill: parent;
// MouseArea {
// anchors.fill: parent
// onClicked: {
// console.log("x->", mouseX)
// console.log("y-->", mouseY)
// }
// onDoubleClicked: 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()
})
}
}
+5
View File
@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="9" y="9" width="10" height="10" rx="2" ry="2"/>
<path d="M5 15V7a2 2 0 0 1 2-2h8"/>
</svg>

After

Width:  |  Height:  |  Size: 291 B

+186
View File
@@ -0,0 +1,186 @@
#captureOverlayToolbar {
background: #FFFFFF;
border: 1px solid rgba(96, 165, 250, 170);
border-radius: 10px;
}
#captureOverlayGifBar {
background: #FFFFFF;
border: 1px solid #E2E8F0;
border-radius: 10px;
}
#captureOverlayTipBubble {
background: rgba(15, 23, 42, 235);
border: none;
border-radius: 6px;
}
#captureOverlayTipLabel {
color: #FFFFFF;
font-size: 11px;
}
#captureOverlayGifProgress {
background: #E2E8F0;
border: none;
border-radius: 3px;
}
#captureOverlayGifProgress::chunk {
background: #3B82F6;
border-radius: 3px;
}
QToolTip {
background: #0F172A;
color: #FFFFFF;
border: none;
border-radius: 6px;
padding: 6px 10px;
}
#captureOverlayToolbar QToolButton {
background: transparent;
border: 1px solid transparent;
border-radius: 7px;
padding: 0px;
margin: 0px;
color: #0F172A;
min-width: 32px;
max-width: 32px;
min-height: 32px;
max-height: 32px;
}
#captureOverlayGifBar QToolButton {
background: transparent;
border: 1px solid transparent;
border-radius: 7px;
padding: 0px;
margin: 0px;
color: #0F172A;
min-width: 32px;
max-width: 32px;
min-height: 32px;
max-height: 32px;
}
#captureOverlayGifBar QToolButton:hover {
background: #F8FAFC;
}
#captureOverlayGifBar QToolButton:pressed {
background: #EFF6FF;
border-color: #3B82F6;
}
#captureOverlayToolbar QToolButton:hover {
background: #F1F5F9;
}
#captureOverlayToolbar QToolButton:pressed {
background: #EFF6FF;
border-color: #3B82F6;
}
#captureOverlayToolbar QToolButton:checked {
background: #EFF6FF;
border-color: #3B82F6;
}
#captureOverlayGifStopButton:hover {
background: #FEF3C7;
}
#captureOverlayGifCancelButton:hover {
background: #FEE2E2;
}
#captureOverlayToolbar QToolTip {
background: #0F172A;
color: #FFFFFF;
border: none;
border-radius: 6px;
padding: 6px 10px;
}
#captureOverlayToolbarDivider {
background: #E2E8F0;
min-width: 1px;
max-width: 1px;
}
#captureOverlayGifStatus {
color: #1E293B;
font-size: 13px;
font-family: "Consolas";
font-weight: 600;
}
#captureOverlayGifFrames {
color: #94A3B8;
font-size: 11px;
}
#captureOverlayGifDot {
background: #EF4444;
border-radius: 4px;
}
#captureOverlayToolOptions {
background-color: #FFFFFF;
border: 1px solid #E2E8F0;
border-radius: 10px;
}
#captureOverlayToolOptions QToolButton {
background-color: transparent;
border: 1px solid #E2E8F0;
border-radius: 6px;
color: #334155;
}
#captureOverlayToolOptions QToolButton:checked {
background-color: #EFF6FF;
border-color: #3B82F6;
color: #3B82F6;
}
#captureOverlayToolOptions QToolButton:hover {
background-color: #F1F5F9;
}
#captureOverlayToolOptions QLineEdit,
#captureOverlayToolOptions QSpinBox {
background-color: #F8FAFC;
border: 1px solid #E2E8F0;
border-radius: 6px;
padding: 4px 6px;
color: #0F172A;
}
#captureOverlayToolOptions QCheckBox {
color: #334155;
}
#captureOverlayInlineTextEditor {
background-color: #FFFFFF;
border: 1px solid #3B82F6;
border-radius: 6px;
padding: 4px 6px;
color: #0F172A;
}
#captureOverlayInlineTextPanel {
background-color: #FFFFFF;
border: 1px solid #CBD5E1;
border-radius: 10px;
}
#captureOverlayNumberLabel {
background-color: #F8FAFC;
border: 1px solid #E2E8F0;
border-radius: 6px;
color: #0F172A;
}
+76
View File
@@ -0,0 +1,76 @@
#longCaptureBar {
background: #111827;
border: 1px solid #1F2937;
border-radius: 12px;
}
#longCaptureBar QLabel {
color: #E5E7EB;
}
#longCaptureBadge {
background: #F97316;
color: white;
border-radius: 13px;
font-weight: 700;
}
#longCaptureFrameLabel {
color: #D1D5DB;
}
#longCapturePrimaryBtn {
background: #2563EB;
color: white;
border: none;
border-radius: 6px;
padding: 0 12px;
}
#longCapturePrimaryBtn:hover {
background: #1D4ED8;
}
#longCaptureDangerBtn {
background: #EF4444;
color: white;
border: none;
border-radius: 6px;
padding: 0 12px;
}
#longCaptureDangerBtn:hover {
background: #DC2626;
}
#longCapturePreview {
background: #111827;
border: 1px solid #1F2937;
border-radius: 12px;
}
#longCapturePreview QLabel {
color: #F9FAFB;
}
#longCapturePreviewTitle {
font-weight: 700;
}
#longCapturePreviewSub {
color: #9CA3AF;
}
#longCapturePreviewStatus {
color: #CBD5E1;
}
#longCapturePreviewImageBox {
background: #0B1220;
border: 1px solid #334155;
border-radius: 8px;
}
#longCapturePreviewEmpty {
color: #64748B;
}
+90
View File
@@ -0,0 +1,90 @@
#include "app/tzshot_app.h"
#include <QApplication>
#include <QCoreApplication>
#include "app/tzshot_services.h"
#include "app/tzshot_widget_runtime.h"
#include "instance_activation.h"
TZShotApp::TZShotApp(QApplication &app, QObject *parent)
: QObject(parent)
, m_app(app)
{
}
bool TZShotApp::initialize()
{
configureApplication();
m_instanceActivation = new InstanceActivation(QStringLiteral("TZshot.Instance"), this);
if (!m_instanceActivation->initialize()) {
return false;
}
m_services = new TZShotServices(this);
m_runtime = new TZShotWidgetRuntime(*m_services, this);
setupConnections();
return true;
}
void TZShotApp::configureApplication() const
{
QCoreApplication::setOrganizationName("TZshot");
QCoreApplication::setOrganizationDomain("tzshot.local");
QCoreApplication::setApplicationName("TZshot");
m_app.setQuitOnLastWindowClosed(false);
}
void TZShotApp::setupConnections()
{
connect(&m_services->ocrViewModel(), &OcrViewModel::isRecognizingChanged, this, [this]() {
if (m_services->ocrViewModel().isRecognizing()) {
m_runtime->ocrResultWidget().showAndActivate();
}
});
connect(&m_services->ocrViewModel(), &OcrViewModel::resultReady, this, [this](const QString &) {
m_runtime->ocrResultWidget().showAndActivate();
});
connect(&m_services->ocrViewModel(), &OcrViewModel::recognizeFailed, this, [this](const QString &errorMessage) {
m_runtime->trayHelper().showMessage(tr("OCR 识别失败"), errorMessage);
});
connect(m_instanceActivation, &InstanceActivation::activationRequested, this, [this]() {
showCaptureOverlay(QStringLiteral("copy"));
});
connect(&m_runtime->trayHelper(), &TrayIconHelper::trayClicked, this, [this](int clickType) {
if (clickType == 0 || clickType == 2) {
showCaptureOverlay(QStringLiteral("copy"));
}
});
connect(&m_runtime->trayHelper(), &TrayIconHelper::screenshotTriggered, this, [this]() {
showCaptureOverlay(QStringLiteral("copy"));
});
connect(&m_runtime->trayHelper(), &TrayIconHelper::showSettingTriggered, this, [this]() {
m_runtime->widgetWindowBridge().showSettings();
});
connect(&m_runtime->trayHelper(), &TrayIconHelper::showAboutTriggered, this, [this]() {
m_runtime->aboutDialog().showAndActivate();
});
connect(&m_runtime->trayHelper(), &TrayIconHelper::exitTriggered, &m_app, &QApplication::quit);
connect(&m_services->globalShortcut(), &GlobalShortcut::screenshotActivated, this, [this]() {
showCaptureOverlay(QStringLiteral("copy"));
});
connect(&m_services->globalShortcut(), &GlobalShortcut::screenshotSaveActivated, this, [this]() {
showCaptureOverlay(QStringLiteral("save"));
});
connect(&m_services->globalShortcut(), &GlobalShortcut::stickyActivated, this, [this]() {
showCaptureOverlay(QStringLiteral("sticky"));
});
connect(&m_services->globalShortcut(), &GlobalShortcut::toggleActivated, this, [this]() {
m_runtime->widgetWindowBridge().toggleCaptureOverlay(QStringLiteral("copy"));
});
}
void TZShotApp::showCaptureOverlay(const QString &mode)
{
m_runtime->widgetWindowBridge().showCaptureOverlay(mode);
}
+33
View File
@@ -0,0 +1,33 @@
#ifndef TZSHOT_APP_H
#define TZSHOT_APP_H
#include <QObject>
class QApplication;
class QString;
class InstanceActivation;
class TZShotServices;
class TZShotWidgetRuntime;
class TZShotApp : public QObject
{
Q_OBJECT
public:
explicit TZShotApp(QApplication &app, QObject *parent = nullptr);
bool initialize();
private:
void configureApplication() const;
void setupConnections();
void showCaptureOverlay(const QString &mode);
QApplication &m_app;
InstanceActivation *m_instanceActivation = nullptr;
TZShotServices *m_services = nullptr;
TZShotWidgetRuntime *m_runtime = nullptr;
};
#endif // TZSHOT_APP_H
+19
View File
@@ -0,0 +1,19 @@
#include "app/tzshot_services.h"
TZShotServices::TZShotServices(QObject *parent)
: QObject(parent)
, m_desktopSnapshot()
, m_appSettings()
, m_stickyImageStore(this)
, m_screenshotViewModel(m_desktopSnapshot, m_stickyImageStore, this)
, m_stickyViewModel(m_stickyImageStore, this)
, m_aiViewModel(m_appSettings, m_stickyImageStore, this)
, m_storageViewModel(m_appSettings, this)
, m_languageManager(m_appSettings, this)
, m_gifRecordViewModel(m_appSettings, this)
, m_scrollCaptureViewModel(m_screenshotViewModel, m_appSettings, this)
, m_ocrViewModel(this)
, m_globalShortcut(this)
{
m_stickyViewModel.setAiViewModel(&m_aiViewModel);
}
+54
View File
@@ -0,0 +1,54 @@
#ifndef TZSHOT_SERVICES_H
#define TZSHOT_SERVICES_H
#include <QObject>
#include "language_manager.h"
#include "model/app_settings.h"
#include "model/desktop_snapshot.h"
#include "shortcut_key/globalshortcut.h"
#include "sticky_image_store.h"
#include "viewmodel/ai_view_model.h"
#include "viewmodel/gif_record_view_model.h"
#include "viewmodel/ocr_view_model.h"
#include "viewmodel/screenshot_view_model.h"
#include "viewmodel/scroll_capture_view_model.h"
#include "viewmodel/sticky_view_model.h"
#include "viewmodel/storage_view_model.h"
class TZShotServices : public QObject
{
Q_OBJECT
public:
explicit TZShotServices(QObject *parent = nullptr);
DesktopSnapshot& desktopSnapshot() { return m_desktopSnapshot; }
AppSettings& appSettings() { return m_appSettings; }
StickyImageStore& stickyImageStore() { return m_stickyImageStore; }
ScreenshotViewModel& screenshotViewModel() { return m_screenshotViewModel; }
StickyViewModel& stickyViewModel() { return m_stickyViewModel; }
AIViewModel& aiViewModel() { return m_aiViewModel; }
StorageViewModel& storageViewModel() { return m_storageViewModel; }
LanguageManager& languageManager() { return m_languageManager; }
GifRecordViewModel& gifRecordViewModel() { return m_gifRecordViewModel; }
ScrollCaptureViewModel& scrollCaptureViewModel() { return m_scrollCaptureViewModel; }
OcrViewModel& ocrViewModel() { return m_ocrViewModel; }
GlobalShortcut& globalShortcut() { return m_globalShortcut; }
private:
DesktopSnapshot m_desktopSnapshot;
AppSettings m_appSettings;
StickyImageStore m_stickyImageStore;
ScreenshotViewModel m_screenshotViewModel;
StickyViewModel m_stickyViewModel;
AIViewModel m_aiViewModel;
StorageViewModel m_storageViewModel;
LanguageManager m_languageManager;
GifRecordViewModel m_gifRecordViewModel;
ScrollCaptureViewModel m_scrollCaptureViewModel;
OcrViewModel m_ocrViewModel;
GlobalShortcut m_globalShortcut;
};
#endif // TZSHOT_SERVICES_H
+30
View File
@@ -0,0 +1,30 @@
#include "app/tzshot_widget_runtime.h"
#include "app/tzshot_services.h"
TZShotWidgetRuntime::TZShotWidgetRuntime(TZShotServices &services, QObject *parent)
: QObject(parent)
, m_ocrResultWidget(&services.ocrViewModel())
, m_captureOverlayWidget(&services.screenshotViewModel(),
&services.stickyViewModel(),
&services.storageViewModel(),
&services.ocrViewModel(),
&services.gifRecordViewModel())
, m_settingsDialog(&services.aiViewModel(),
&services.storageViewModel(),
&services.languageManager(),
&services.gifRecordViewModel(),
&services.ocrViewModel(),
&services.globalShortcut())
, m_aboutDialog()
, m_longCaptureController(&services.scrollCaptureViewModel())
, m_widgetWindowBridge(&m_settingsDialog,
&m_captureOverlayWidget,
&m_longCaptureController,
this)
, m_trayHelper(this)
{
m_captureOverlayWidget.setWidgetWindowBridge(&m_widgetWindowBridge);
m_trayHelper.setIcon(QStringLiteral(":/resource/img/tray_icon.svg"));
m_trayHelper.show();
}
+41
View File
@@ -0,0 +1,41 @@
#ifndef TZSHOT_WIDGET_RUNTIME_H
#define TZSHOT_WIDGET_RUNTIME_H
#include <QObject>
#include "tray_icon_helper.h"
#include "widgets/about_dialog.h"
#include "widgets/capture_overlay_widget.h"
#include "widgets/long_capture_controller.h"
#include "widgets/ocr_result_widget.h"
#include "widgets/settings_dialog.h"
#include "widgets/widget_window_bridge.h"
class TZShotServices;
class TZShotWidgetRuntime : public QObject
{
Q_OBJECT
public:
explicit TZShotWidgetRuntime(TZShotServices &services, QObject *parent = nullptr);
OcrResultWidget& ocrResultWidget() { return m_ocrResultWidget; }
CaptureOverlayWidget& captureOverlayWidget() { return m_captureOverlayWidget; }
SettingsDialog& settingsDialog() { return m_settingsDialog; }
AboutDialog& aboutDialog() { return m_aboutDialog; }
LongCaptureController& longCaptureController() { return m_longCaptureController; }
WidgetWindowBridge& widgetWindowBridge() { return m_widgetWindowBridge; }
TrayIconHelper& trayHelper() { return m_trayHelper; }
private:
OcrResultWidget m_ocrResultWidget;
CaptureOverlayWidget m_captureOverlayWidget;
SettingsDialog m_settingsDialog;
AboutDialog m_aboutDialog;
LongCaptureController m_longCaptureController;
WidgetWindowBridge m_widgetWindowBridge;
TrayIconHelper m_trayHelper;
};
#endif // TZSHOT_WIDGET_RUNTIME_H
+116 -23
View File
@@ -3,6 +3,108 @@
#include <QScreen>
#include <QGuiApplication>
#include <QPixmap>
#include <QPainter>
#ifdef Q_OS_WIN
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#endif
namespace {
#ifdef Q_OS_WIN
QImage captureVirtualDesktopImageWin(QRect *virtualGeometryOut = nullptr)
{
const int vx = GetSystemMetrics(SM_XVIRTUALSCREEN);
const int vy = GetSystemMetrics(SM_YVIRTUALSCREEN);
const int vw = GetSystemMetrics(SM_CXVIRTUALSCREEN);
const int vh = GetSystemMetrics(SM_CYVIRTUALSCREEN);
if (virtualGeometryOut) {
*virtualGeometryOut = QRect(vx, vy, vw, vh);
}
if (vw <= 0 || vh <= 0) {
return {};
}
HDC screenDc = GetDC(nullptr);
if (!screenDc) {
return {};
}
HDC memDc = CreateCompatibleDC(screenDc);
HBITMAP bitmap = CreateCompatibleBitmap(screenDc, vw, vh);
HGDIOBJ oldBitmap = SelectObject(memDc, bitmap);
BitBlt(memDc, 0, 0, vw, vh, screenDc, vx, vy, SRCCOPY | CAPTUREBLT);
BITMAPINFO bmi{};
bmi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
bmi.bmiHeader.biWidth = vw;
bmi.bmiHeader.biHeight = -vh;
bmi.bmiHeader.biPlanes = 1;
bmi.bmiHeader.biBitCount = 32;
bmi.bmiHeader.biCompression = BI_RGB;
QImage image(vw, vh, QImage::Format_ARGB32);
if (!image.isNull()) {
GetDIBits(memDc,
bitmap,
0,
static_cast<UINT>(vh),
image.bits(),
&bmi,
DIB_RGB_COLORS);
}
SelectObject(memDc, oldBitmap);
DeleteObject(bitmap);
DeleteDC(memDc);
ReleaseDC(nullptr, screenDc);
return image;
}
#endif
QImage captureVirtualDesktopImageQt(QRect *virtualGeometryOut = nullptr)
{
const QList<QScreen*> screens = QGuiApplication::screens();
if (screens.isEmpty()) {
return {};
}
QRect virtualGeometry;
for (QScreen *screen : screens) {
if (screen) {
virtualGeometry = virtualGeometry.united(screen->geometry());
}
}
if (virtualGeometryOut) {
*virtualGeometryOut = virtualGeometry;
}
if (virtualGeometry.isEmpty()) {
return {};
}
QImage image(virtualGeometry.size(), QImage::Format_ARGB32);
image.fill(Qt::black);
QPainter painter(&image);
for (QScreen *screen : screens) {
if (!screen) {
continue;
}
const QRect screenGeom = screen->geometry();
painter.drawPixmap(QRect(screenGeom.x() - virtualGeometry.x(),
screenGeom.y() - virtualGeometry.y(),
screenGeom.width(),
screenGeom.height()),
screen->grabWindow(0));
}
painter.end();
return image;
}
}
GifFrameGrabber::GifFrameGrabber(QObject* parent)
: QObject(parent)
@@ -32,32 +134,23 @@ void GifFrameGrabber::updateRect(const QRect& captureRect)
void GifFrameGrabber::onTimerTick()
{
QScreen* screen = QGuiApplication::primaryScreen();
if (!screen) return;
QRect virtualGeometry;
#ifdef Q_OS_WIN
QImage fullImage = captureVirtualDesktopImageWin(&virtualGeometry);
#else
QImage fullImage = captureVirtualDesktopImageQt(&virtualGeometry);
#endif
if (fullImage.isNull() || virtualGeometry.isEmpty()) {
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);
const QRect localRect = m_rect.translated(-virtualGeometry.topLeft()).intersected(fullImage.rect());
if (localRect.isEmpty()) {
return;
}
const QImage frame = fullImage.copy(localRect);
if (!frame.isNull()) {
// 缩回逻辑分辨率,确保所有帧尺寸一致
if (dpr != 1.0) {
frame = frame.scaled(m_rect.width(), m_rect.height(),
Qt::IgnoreAspectRatio,
Qt::SmoothTransformation);
}
emit frameCaptured(frame);
}
}
-92
View File
@@ -1,92 +0,0 @@
#include "magnifier_provider.h"
#include <QPainter>
MagnifierProvider::MagnifierProvider(ScreenshotViewModel *viewModel)
: QQuickImageProvider(QQuickImageProvider::Image)
, m_viewModel(viewModel)
{
}
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 logicalX = parts[0].toInt(&okX);
const int logicalY = 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 int srcDiameter = kSrcRadius * 2;
QImage region(srcDiameter, srcDiameter, QImage::Format_ARGB32_Premultiplied);
region.fill(Qt::black);
const QPixmap 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 virtualRect = m_viewModel->virtualGeometry();
const QPoint center(logicalX - virtualRect.x(), logicalY - virtualRect.y());
QRect srcRect(center.x() - kSrcRadius,
center.y() - kSrcRadius,
srcDiameter,
srcDiameter);
QPainter regionPainter(&region);
const QRect bounded = srcRect.intersected(snapshot.rect());
if (!bounded.isEmpty()) {
const int destX = bounded.x() - srcRect.x();
const int destY = bounded.y() - srcRect.y();
regionPainter.drawPixmap(destX,
destY,
snapshot,
bounded.x(),
bounded.y(),
bounded.width(),
bounded.height());
}
regionPainter.end();
QImage zoomed = region.scaled(kOutputSize,
kOutputSize,
Qt::IgnoreAspectRatio,
Qt::SmoothTransformation);
QPainter p(&zoomed);
p.setRenderHint(QPainter::Antialiasing, false);
p.setPen(QPen(QColor(255, 0, 0, 220), 1));
const int half = kOutputSize / 2;
p.drawLine(half, 0, half, kOutputSize - 1);
p.drawLine(0, half, kOutputSize - 1, half);
p.end();
if (size) {
*size = zoomed.size();
}
return zoomed;
}
-36
View File
@@ -1,36 +0,0 @@
#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
+4 -121
View File
@@ -1,132 +1,15 @@
#include <QApplication>
#include <QQmlApplicationEngine>
#include <QQmlContext>
#include <QQuickWindow>
#include <QScreen>
#include <QApplication>
#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"
#include "instance_activation.h"
#include "widgets/ocr_result_widget.h"
#include "widgets/settings_dialog.h"
#include "widgets/widget_window_bridge.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/scroll_capture_view_model.h"
#include "viewmodel/ocr_view_model.h"
#include "app/tzshot_app.h"
int main(int argc, char *argv[])
{
// 强制让窗口跟随系统DPI,不使用虚拟像素
// QGuiApplication::setHighDpiScaleFactorRoundingPolicy(
// Qt::HighDpiScaleFactorRoundingPolicy::RoundPreferFloor);
QApplication app(argc, argv);
// Single-instance activation: second launch notifies the first one and exits.
InstanceActivation instanceActivation(QStringLiteral("TZshot.Instance"));
if (!instanceActivation.initialize()) {
TZShotApp tzshotApp(app);
if (!tzshotApp.initialize()) {
return 0;
}
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);
stickyViewModel.setAiViewModel(&aiViewModel);
StorageViewModel storageViewModel(appSettings);
LanguageManager languageManager(appSettings);
GifRecordViewModel gifRecordViewModel(appSettings);
ScrollCaptureViewModel scrollCaptureViewModel(screenshotViewModel, appSettings);
OcrViewModel ocrViewModel;
OcrResultWidget ocrResultWidget(&ocrViewModel);
// ── 其他服务(不属于 MVVM 任何层)───────────────────
GlobalShortcut globalShortcut;
SettingsDialog settingsDialog(&aiViewModel,
&storageViewModel,
&languageManager,
&gifRecordViewModel,
&ocrViewModel,
&globalShortcut);
WidgetWindowBridge widgetWindowBridge(&settingsDialog);
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_ScrollCaptureVM", &scrollCaptureViewModel);
engine.rootContext()->setContextProperty("O_OcrVM", &ocrViewModel);
engine.rootContext()->setContextProperty("O_InstanceBridge", &instanceActivation);
engine.rootContext()->setContextProperty("O_WidgetWindows", &widgetWindowBridge);
QObject::connect(&ocrViewModel, &OcrViewModel::isRecognizingChanged, &app, [&]() {
if (ocrViewModel.isRecognizing()) {
ocrResultWidget.showAndActivate();
}
});
QObject::connect(&ocrViewModel, &OcrViewModel::resultReady, &app, [&](const QString &) {
ocrResultWidget.showAndActivate();
});
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);
if (!engine.rootObjects().isEmpty()) {
if (auto *window = qobject_cast<QQuickWindow*>(engine.rootObjects().constFirst())) {
if (QScreen *screen = QGuiApplication::primaryScreen()) {
window->setGeometry(screen->virtualGeometry());
}
}
}
return app.exec();
}
-261
View File
@@ -1,261 +0,0 @@
#include "paint_board.h"
#include "shape/mosaic_shape.h"
#include <QDebug>
PaintBoard::PaintBoard(QQuickItem *parent)
: QQuickPaintedItem(parent)
{
setRenderTarget(QQuickPaintedItem::FramebufferObject);
setAcceptedMouseButtons(Qt::LeftButton);
}
void PaintBoard::setBackgroundImg(QImage img)
{
// if (!img.isNull()) {
// img.setDevicePixelRatio(1.0);
// }
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.setDevicePixelRatio(1.0);
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)
{
// painter->save();
// painter->scale(1.0 / m_canvasScale, 1.0 / m_canvasScale);
drawBkImg(painter, m_background_img);
drawShape(painter);
// painter->restore();
}
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;
painter->drawImage(QRectF(0, 0, width() , height() ), img);
return true;
}
bool PaintBoard::drawShape(QPainter *painter)
{
if (!painter)
return false;
for (Shape* shape : m_shapes) {
if (shape)
shape->draw(painter);
}
if (m_currentShape)
m_currentShape->draw(painter);
return true;
}
-103
View File
@@ -1,103 +0,0 @@
#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
-1
View File
@@ -29,7 +29,6 @@ public:
void draw(QPainter* painter) override;
// 设置当前画布的快照,用于马赛克取色
// 每次绘制前由 PaintBoard 调用
void setCanvasSnapshot(const QImage& snapshot);
private:
-31
View File
@@ -1,31 +0,0 @@
#include "sticky_image_provider_proxy.h"
StickyImageProviderProxy::StickyImageProviderProxy(StickyImageStore &store)
: QQuickImageProvider(QQuickImageProvider::Image)
, m_store(store)
{
}
QImage StickyImageProviderProxy::requestImage(const QString &id, QSize *size, const QSize &requestedSize)
{
Q_UNUSED(requestedSize)
QString cleanId = id;
const int queryIndex = cleanId.indexOf('?');
if (queryIndex >= 0) {
cleanId = cleanId.left(queryIndex);
}
QImage image = m_store.getImage(cleanId);
if (size) {
const qreal dpr = image.devicePixelRatio();
if (!image.isNull() && dpr > 0.0) {
*size = QSize(qMax(1, qRound(image.width() / dpr)),
qMax(1, qRound(image.height() / dpr)));
} else {
*size = image.size();
}
}
return image;
}
-24
View File
@@ -1,24 +0,0 @@
#ifndef STICKY_IMAGE_PROVIDER_PROXY_H
#define STICKY_IMAGE_PROVIDER_PROXY_H
#include <QQuickImageProvider>
#include "sticky_image_store.h"
// StickyImageProviderProxy
// ------------------------
// 纯粹的 QQuickImageProvider 实现,不继承 QObject。
// 生命周期由 QQmlApplicationEngine 通过 addImageProvider() 接管并负责释放。
// 所有实际数据操作委托给 StickyImageStore,自身不持有任何图片数据。
class StickyImageProviderProxy : public QQuickImageProvider
{
public:
explicit StickyImageProviderProxy(StickyImageStore &store);
QImage requestImage(const QString &id, QSize *size, const QSize &requestedSize) override;
private:
StickyImageStore &m_store; // 引用,不持有所有权
};
#endif // STICKY_IMAGE_PROVIDER_PROXY_H
+3 -4
View File
@@ -11,8 +11,7 @@
// StickyImageStore
// ----------------
// 纯业务类(QObject),负责贴图的存储、释放、保存、复制到剪贴板等操作。
// 不继承 QQuickImageProvider,生命周期完全由调用方控制,不受 engine 影响。
// 图片数据通过 storeImage() 写入,由 StickyImageProviderProxy 的 requestImage() 读取。
// 生命周期完全由调用方控制,不依赖 Qt Quick 运行时。
class StickyImageStore : public QObject
{
@@ -21,11 +20,11 @@ class StickyImageStore : public QObject
public:
explicit StickyImageStore(QObject *parent = nullptr);
// 存储图片,返回可供 QML Image.source 使用的 URL 字符串
// 存储图片,返回统一的贴图 URL 字符串
// 格式:"image://sticky/<uuid>"
QString storeImage(const QImage &image);
// 供 StickyImageProviderProxy 调用,线程安全
// 线程安全读取
QImage getImage(const QString &id) const;
QImage getImageByUrl(const QString &imageUrl) const;
+25 -62
View File
@@ -1,13 +1,12 @@
#include "screenshot_view_model.h"
#include "paint_board/paint_board.h"
#include "screenshot_view_model.h"
#include "widgets/sticky_canvas_widget.h"
#include <QBuffer>
#include <QClipboard>
#include <QDebug>
#include <QGuiApplication>
#include <QPainter>
#include <QQuickWindow>
#include <QScreen>
#include <QWidget>
#include <QWindow>
#ifdef Q_OS_WIN
@@ -38,19 +37,23 @@ qreal captureDprForRect(const QRect& rect)
}
}
#endif
if (QScreen* screen = QGuiApplication::primaryScreen()) {
if (QScreen *screen = QGuiApplication::primaryScreen()) {
return screen->devicePixelRatio();
}
return 1.0;
}
qreal effectiveCaptureDpr(QQuickItem *paintBoard, const QRect &rect)
qreal effectiveCaptureDpr(StickyCanvasWidget *paintBoard, const QRect &rect)
{
if (!rect.isEmpty()) {
return captureDprForRect(rect);
}
if (paintBoard && paintBoard->window()) {
return paintBoard->window()->devicePixelRatio();
if (paintBoard) {
if (QWidget *topLevel = paintBoard->window()) {
if (QWindow *handle = topLevel->windowHandle()) {
return handle->devicePixelRatio();
}
}
}
if (QScreen *screen = QGuiApplication::primaryScreen()) {
return screen->devicePixelRatio();
@@ -124,7 +127,7 @@ QRect ScreenshotViewModel::windowAtPoint(int x, int y) const
return m_snapshot.windowAtPoint(x, y);
}
bool ScreenshotViewModel::captureRectToClipboard(QQuickItem *paintBoard, const QRect &rect)
bool ScreenshotViewModel::captureRectToClipboard(StickyCanvasWidget *paintBoard, const QRect &rect)
{
const QRect r = rect.normalized();
if (r.isEmpty()) {
@@ -141,7 +144,7 @@ bool ScreenshotViewModel::captureRectToClipboard(QQuickItem *paintBoard, const Q
return writeImageToClipboard(img);
}
QString ScreenshotViewModel::captureRectToBase64(QQuickItem *paintBoard, const QRect &rect)
QString ScreenshotViewModel::captureRectToBase64(StickyCanvasWidget *paintBoard, const QRect &rect)
{
const QRect r = rect.normalized();
if (r.isEmpty()) {
@@ -165,7 +168,7 @@ QString ScreenshotViewModel::captureRectToBase64(QQuickItem *paintBoard, const Q
return data.toBase64();
}
QString ScreenshotViewModel::captureRectToStickyUrl(QQuickItem *paintBoard, const QRect &rect)
QString ScreenshotViewModel::captureRectToStickyUrl(StickyCanvasWidget *paintBoard, const QRect &rect)
{
const QRect r = rect.normalized();
if (r.isEmpty()) {
@@ -187,15 +190,6 @@ QString ScreenshotViewModel::captureRectToStickyUrl(QQuickItem *paintBoard, cons
return m_stickyStore.storeImage(img);
}
void ScreenshotViewModel::grabToPaintBoard(QQuickItem *paintBoard, const QRect &rect)
{
const QRect r = rect.normalized();
if (r.isEmpty()) {
return;
}
captureScreen(paintBoard, r, true);
}
QImage ScreenshotViewModel::captureRectToImage(const QRect &rect)
{
const QRect r = rect.normalized();
@@ -233,17 +227,14 @@ bool ScreenshotViewModel::copyImageToClipboard(const QImage &image)
return writeImageToClipboard(image);
}
QImage ScreenshotViewModel::captureScreen(QQuickItem *paintBoard,
QImage ScreenshotViewModel::captureScreen(StickyCanvasWidget *paintBoard,
const QRect &rect,
bool setBackground)
{
PaintBoard *board = qobject_cast<PaintBoard*>(paintBoard);
QImage screenshot;
qreal dpr = 1.0;
if (!m_snapshot.isNull()) {
dpr = effectiveCaptureDpr(paintBoard, rect);
const qreal dpr = effectiveCaptureDpr(paintBoard, rect);
const QRect captureRect = captureRectInSnapshot(m_snapshot, rect, dpr);
screenshot = m_snapshot.image().copy(captureRect).toImage();
} else {
@@ -255,50 +246,22 @@ QImage ScreenshotViewModel::captureScreen(QQuickItem *paintBoard,
screenshot = screen->grabWindow(0).toImage();
}
if (board) {
QImage annotations = board->renderToImage();
if (!annotations.isNull()) {
QPainter p(&screenshot);
if (paintBoard) {
const QImage composited = paintBoard->compositedImage();
if (!composited.isNull()) {
if (!rect.isEmpty()) {
QQuickWindow *win = board->window();
const int windowAbsX = win ? win->x() : 0;
const int windowAbsY = win ? win->y() : 0;
const int boardAbsX = windowAbsX + qRound(board->x());
const int boardAbsY = windowAbsY + qRound(board->y());
const int sourceX = rect.x() - boardAbsX;
const int sourceY = rect.y() - boardAbsY;
QRect srcRect(sourceX, sourceY, rect.width(), rect.height());
srcRect = srcRect.intersected(annotations.rect());
if (!srcRect.isEmpty()) {
const int dstX = qRound((srcRect.x() - sourceX) * dpr);
const int dstY = qRound((srcRect.y() - sourceY) * dpr);
const int dstW = qMax(1, qRound(srcRect.width() * dpr));
const int dstH = qMax(1, qRound(srcRect.height() * dpr));
p.drawImage(QRect(dstX, dstY, dstW, dstH),
annotations,
srcRect);
const QRect localRect = rect.translated(-m_snapshot.virtualGeometry().topLeft())
.intersected(composited.rect());
if (!localRect.isEmpty()) {
screenshot = composited.copy(localRect);
}
} else {
p.drawImage(0, 0, annotations);
screenshot = composited;
}
p.end();
}
if (setBackground) {
QImage background = screenshot;
if (dpr > 1.0) {
const QSize logicalSize(qRound(background.width() / dpr),
qRound(background.height() / dpr));
if (logicalSize.isValid() && logicalSize != background.size()) {
background = background.scaled(logicalSize,
Qt::IgnoreAspectRatio,
Qt::SmoothTransformation);
}
}
board->setBackgroundImg(background);
paintBoard->setBackgroundImage(screenshot);
}
}
+6 -7
View File
@@ -3,13 +3,13 @@
#include <QColor>
#include <QObject>
#include <QQuickItem>
#include <QRect>
#include <QWindow>
#include "model/desktop_snapshot.h"
#include "model/app_settings.h"
#include "sticky_image_store.h"
class StickyCanvasWidget;
class ScreenshotViewModel : public QObject
{
Q_OBJECT
@@ -34,10 +34,9 @@ public:
Q_INVOKABLE void releaseDesktopSnapshot();
Q_INVOKABLE QColor getPixelColor(int x, int y) const;
Q_INVOKABLE QRect windowAtPoint(int x, int y) const;
Q_INVOKABLE bool captureRectToClipboard(QQuickItem *paintBoard, const QRect &rect);
Q_INVOKABLE QString captureRectToBase64(QQuickItem *paintBoard, const QRect &rect);
Q_INVOKABLE QString captureRectToStickyUrl(QQuickItem *paintBoard, const QRect &rect);
Q_INVOKABLE void grabToPaintBoard(QQuickItem *paintBoard, const QRect &rect);
Q_INVOKABLE bool captureRectToClipboard(StickyCanvasWidget *paintBoard, const QRect &rect);
Q_INVOKABLE QString captureRectToBase64(StickyCanvasWidget *paintBoard, const QRect &rect);
Q_INVOKABLE QString captureRectToStickyUrl(StickyCanvasWidget *paintBoard, const QRect &rect);
Q_INVOKABLE QImage captureRectToImage(const QRect &rect);
QImage captureRectToImageLive(const QRect &rect);
@@ -45,7 +44,7 @@ public:
private:
QRect captureRectForDisplay(const QRect &rect) const;
QImage captureScreen(QQuickItem *paintBoard,
QImage captureScreen(StickyCanvasWidget *paintBoard,
const QRect &rect = QRect(),
bool setBackground = false);
bool writeImageToClipboard(const QImage &image);
+13
View File
@@ -42,6 +42,19 @@ void StickyViewModel::requestSticky(const QString &imageUrl, const QRect &imgRec
emit stickyReady(imageUrl, targetRect);
}
void StickyViewModel::requestStickyImage(const QImage &image, const QRect &imgRect)
{
if (image.isNull()) {
return;
}
const QString imageUrl = m_store.storeImage(image);
if (imageUrl.isEmpty()) {
return;
}
requestSticky(imageUrl, imgRect);
}
void StickyViewModel::releaseImage(const QString &imageUrl)
{
m_store.releaseImage(imageUrl);
+6 -6
View File
@@ -13,8 +13,8 @@ class AIViewModel;
// ---------------
// 负责贴图会话的业务逻辑:
// - 从 StickyImageStore 读写贴图数据(save/copy/release)
// - 通过信号通知 QML 创建贴图窗口(View 监听信号后自行创建 TZStickyWindow)
// QML 侧只需监听 stickyReady 信号,无需知道截图细节。
// - 在 Windows 上直接驱动 QWidget 贴图窗口
// - 在其他平台保留信号式窗口创建接口作为回退路径
class StickyViewModel : public QObject
{
@@ -24,11 +24,11 @@ public:
explicit StickyViewModel(StickyImageStore &store, QObject *parent = nullptr);
void setAiViewModel(AIViewModel *aiViewModel);
// 通知 QML 创建贴图窗口,由 ScreenshotViewModel::captureRectToStickyUrl 产生的 URL 触发
// QML 监听 stickyReady(imageUrl, imgRect) 信号后创建 TZStickyWindow
// 在 Windows 上直接创建 QWidget 贴图;其他平台通过 stickyReady 回退。
Q_INVOKABLE void requestSticky(const QString &imageUrl, const QRect &imgRect);
Q_INVOKABLE void requestStickyImage(const QImage &image, const QRect &imgRect);
// 以下三个方法供 TZStickyWindow.qml 直接调用(贴图窗口内部操作)
// 贴图窗口内部操作
Q_INVOKABLE void releaseImage(const QString &imageUrl);
Q_INVOKABLE bool saveImage(const QString &imageUrl, const QUrl &targetUrl);
Q_INVOKABLE bool copyImageToClipboard(const QString &imageUrl);
@@ -42,7 +42,7 @@ public:
Q_INVOKABLE bool mirrorImage(const QString &imageUrl, bool horizontal, bool vertical);
signals:
// QML 监听此信号,收到后创建并显示 TZStickyWindow
// 非 Windows 平台可监听此信号自行创建贴图窗口
void stickyReady(const QString &imageUrl, const QRect &imgRect);
private:
+98
View File
@@ -0,0 +1,98 @@
#include "widgets/about_dialog.h"
#include <QFrame>
#include <QGuiApplication>
#include <QHBoxLayout>
#include <QLabel>
#include <QPushButton>
#include <QScreen>
#include <QVBoxLayout>
AboutDialog::AboutDialog(QWidget *parent)
: QDialog(parent)
{
setWindowTitle(tr("关于 TZshot"));
setModal(false);
setFixedSize(400, 350);
setStyleSheet(QStringLiteral("background:#F8FAFC;"));
auto *layout = new QVBoxLayout(this);
layout->setContentsMargins(32, 32, 32, 24);
layout->setSpacing(0);
auto *logoWrap = new QWidget(this);
auto *logoLayout = new QHBoxLayout(logoWrap);
logoLayout->setContentsMargins(0, 0, 0, 0);
auto *logo = new QLabel(QStringLiteral("T"), logoWrap);
logo->setAlignment(Qt::AlignCenter);
logo->setFixedSize(72, 72);
logo->setStyleSheet(QStringLiteral(
"background:#3B82F6; color:#FFFFFF; border-radius:18px; font-size:40px; font-weight:700;"));
logoLayout->addStretch(1);
logoLayout->addWidget(logo);
logoLayout->addStretch(1);
layout->addWidget(logoWrap);
layout->addSpacing(16);
auto *title = new QLabel(QStringLiteral("TZshot"), this);
title->setAlignment(Qt::AlignCenter);
title->setStyleSheet(QStringLiteral("font-size:22px; font-weight:700; color:#1E293B;"));
layout->addWidget(title);
layout->addSpacing(6);
auto *version = new QLabel(tr("版本 v1.0.0"), this);
version->setAlignment(Qt::AlignCenter);
version->setStyleSheet(QStringLiteral("font-size:13px; color:#64748B;"));
layout->addWidget(version);
layout->addSpacing(16);
auto *divider = new QFrame(this);
divider->setFrameShape(QFrame::HLine);
divider->setStyleSheet(QStringLiteral("color:#E2E8F0;"));
layout->addWidget(divider);
layout->addSpacing(16);
auto *desc = new QLabel(tr("简洁高效的截图贴图工具,支持标注、AI 编辑、马赛克等功能。"), this);
desc->setWordWrap(true);
desc->setAlignment(Qt::AlignCenter);
desc->setStyleSheet(QStringLiteral("font-size:13px; color:#475569;"));
layout->addWidget(desc);
layout->addStretch(1);
auto *copyright = new QLabel(tr("© 2025 TZshot. All rights reserved."), this);
copyright->setAlignment(Qt::AlignCenter);
copyright->setStyleSheet(QStringLiteral("font-size:11px; color:#94A3B8;"));
layout->addWidget(copyright);
layout->addSpacing(8);
auto *buttonWrap = new QWidget(this);
auto *buttonLayout = new QHBoxLayout(buttonWrap);
buttonLayout->setContentsMargins(0, 0, 0, 0);
auto *closeButton = new QPushButton(tr("关闭"), buttonWrap);
closeButton->setFixedSize(88, 34);
closeButton->setStyleSheet(QStringLiteral(
"QPushButton { background:#3B82F6; color:white; border:none; border-radius:8px; font-size:13px; }"
"QPushButton:hover { background:#2563EB; }"));
connect(closeButton, &QPushButton::clicked, this, &QDialog::close);
buttonLayout->addStretch(1);
buttonLayout->addWidget(closeButton);
buttonLayout->addStretch(1);
layout->addWidget(buttonWrap);
}
void AboutDialog::showAndActivate()
{
if (QScreen *screen = QGuiApplication::primaryScreen()) {
const QRect work = screen->availableGeometry();
move(work.center() - rect().center());
}
show();
raise();
activateWindow();
}
+16
View File
@@ -0,0 +1,16 @@
#ifndef ABOUT_DIALOG_H
#define ABOUT_DIALOG_H
#include <QDialog>
class AboutDialog : public QDialog
{
Q_OBJECT
public:
explicit AboutDialog(QWidget *parent = nullptr);
void showAndActivate();
};
#endif // ABOUT_DIALOG_H
File diff suppressed because it is too large. Load diff
+160
View File
@@ -0,0 +1,160 @@
#ifndef CAPTURE_OVERLAY_WIDGET_H
#define CAPTURE_OVERLAY_WIDGET_H
#include <QPointer>
#include <QColor>
#include <QPoint>
#include <QString>
#include <QVector>
#include <QWidget>
#include "paint_board/shape/shape_type.h"
#include "widgets/selection_mask_controller.h"
class QToolButton;
class QLabel;
class QProgressBar;
class QTimer;
class QCheckBox;
class QLineEdit;
class ScreenshotViewModel;
class StickyViewModel;
class StorageViewModel;
class OcrViewModel;
class GifRecordViewModel;
class MagnifierWidget;
class WidgetWindowBridge;
class StickyCanvasWidget;
class CaptureOverlayWidget : public QWidget
{
Q_OBJECT
public:
explicit CaptureOverlayWidget(ScreenshotViewModel *screenCapture,
StickyViewModel *stickyViewModel,
StorageViewModel *storageViewModel,
OcrViewModel *ocrViewModel,
GifRecordViewModel *gifRecordViewModel = nullptr,
WidgetWindowBridge *widgetWindowBridge = nullptr,
QWidget *parent = nullptr);
void showAndActivate(const QString &mode = QStringLiteral("copy"));
void toggleCapture(const QString &mode = QStringLiteral("copy"));
void setWidgetWindowBridge(WidgetWindowBridge *widgetWindowBridge);
protected:
void paintEvent(QPaintEvent *event) override;
void resizeEvent(QResizeEvent *event) override;
void mousePressEvent(QMouseEvent *event) override;
void mouseMoveEvent(QMouseEvent *event) override;
void mouseReleaseEvent(QMouseEvent *event) override;
void keyPressEvent(QKeyEvent *event) override;
void closeEvent(QCloseEvent *event) override;
bool eventFilter(QObject *watched, QEvent *event) override;
private:
enum class CaptureAction {
Copy,
Save,
Sticky,
Ocr,
LongCapture,
Gif
};
void resetState();
void refreshSnapshot();
QRect selectionInGlobal() const;
QRect currentSelectionRect() const;
QImage currentCaptureImage() const;
QImage currentOutputImage() const;
void refreshCanvasForSelection(bool clearAnnotations);
void setActiveTool(Shapeype type, QToolButton *button);
void syncCanvasToolSettings();
void updateToolOptionsPanel();
QToolButton *currentActiveToolButton() const;
void updateToolbarGeometry();
void updateGifBarGeometry();
void updateGifStatusText();
void updateGifBarState();
void updateGifDotState();
void updateMagnifier(const QPoint &localPos);
void updateToolOptionsGeometry();
QRect toolOptionsRect() const;
void showTipBubble(QToolButton *button);
void hideTipBubble();
void setDefaultAction(const QString &mode);
void performAction(CaptureAction action);
void finishCapture();
QString defaultSaveFilePath() const;
QPointer<ScreenshotViewModel> m_screenCapture;
QPointer<StickyViewModel> m_stickyViewModel;
QPointer<StorageViewModel> m_storageViewModel;
QPointer<OcrViewModel> m_ocrViewModel;
QPointer<GifRecordViewModel> m_gifRecordViewModel;
QPointer<WidgetWindowBridge> m_widgetWindowBridge;
QRect m_virtualGeometry;
CaptureAction m_defaultAction = CaptureAction::Copy;
SelectionMaskController m_selection;
bool m_gifRecordingMode = false;
QRect m_gifCaptureRect;
QRect m_gifGlobalCaptureRect;
QWidget *m_toolbar = nullptr;
StickyCanvasWidget *m_canvas = nullptr;
QWidget *m_toolOptions = nullptr;
QWidget *m_inlineTextPanel = nullptr;
QLineEdit *m_inlineTextEditor = nullptr;
QToolButton *m_inlineTextConfirmButton = nullptr;
QToolButton *m_inlineTextCancelButton = nullptr;
QLineEdit *m_textInput = nullptr;
QCheckBox *m_textBackgroundCheck = nullptr;
QCheckBox *m_numberAutoIncrementCheck = nullptr;
QToolButton *m_numberMinusButton = nullptr;
QLabel *m_numberLabel = nullptr;
QToolButton *m_numberPlusButton = nullptr;
QToolButton *m_copyButton = nullptr;
QToolButton *m_saveButton = nullptr;
QToolButton *m_stickyButton = nullptr;
QToolButton *m_ocrButton = nullptr;
QToolButton *m_longCaptureButton = nullptr;
QToolButton *m_gifButton = nullptr;
QToolButton *m_cancelButton = nullptr;
QToolButton *m_pencilButton = nullptr;
QToolButton *m_rectButton = nullptr;
QToolButton *m_circleButton = nullptr;
QToolButton *m_arrowButton = nullptr;
QToolButton *m_mosaicButton = nullptr;
QToolButton *m_blurButton = nullptr;
QToolButton *m_textButton = nullptr;
QToolButton *m_numberButton = nullptr;
QToolButton *m_undoButton = nullptr;
QVector<QToolButton*> m_colorButtons;
QVector<QToolButton*> m_sizeButtons;
QColor m_currentPenColor = QColor("#F43F5E");
int m_currentPenSize = 6;
QString m_annotationText = QStringLiteral("文本");
bool m_textBackgroundEnabled = true;
bool m_numberAutoIncrement = true;
int m_numberValue = 1;
QPoint m_pendingTextPoint;
QWidget *m_gifBar = nullptr;
QWidget *m_gifBorderOverlay = nullptr;
QLabel *m_gifDot = nullptr;
QLabel *m_gifStatusLabel = nullptr;
QLabel *m_gifFrameLabel = nullptr;
QProgressBar *m_gifProgressBar = nullptr;
QToolButton *m_gifStopButton = nullptr;
QToolButton *m_gifCancelButton = nullptr;
QTimer *m_gifBlinkTimer = nullptr;
bool m_gifDotVisible = true;
MagnifierWidget *m_magnifier = nullptr;
QWidget *m_tipBubble = nullptr;
QLabel *m_tipLabel = nullptr;
};
#endif // CAPTURE_OVERLAY_WIDGET_H
+388
View File
@@ -0,0 +1,388 @@
#include "widgets/long_capture_controller.h"
#include "viewmodel/scroll_capture_view_model.h"
#include <QApplication>
#include <QBuffer>
#include <QEvent>
#include <QFile>
#include <QGuiApplication>
#include <QHBoxLayout>
#include <QKeyEvent>
#include <QLabel>
#include <QPainter>
#include <QPixmap>
#include <QPushButton>
#include <QScreen>
#include <QStyle>
#include <QVBoxLayout>
#include <QWidget>
#ifdef Q_OS_WIN
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#endif
namespace {
QString loadLongCaptureStyleSheet()
{
QFile file(QStringLiteral(":/resource/qss/long_capture_controller.qss"));
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
return {};
}
return QString::fromUtf8(file.readAll());
}
class LongCaptureFrameWidget : public QWidget
{
public:
explicit LongCaptureFrameWidget(QWidget *parent = nullptr)
: QWidget(parent)
{
setWindowFlags(Qt::FramelessWindowHint | Qt::Tool | Qt::WindowStaysOnTopHint);
setAttribute(Qt::WA_TranslucentBackground, true);
setAttribute(Qt::WA_TransparentForMouseEvents, true);
setFocusPolicy(Qt::NoFocus);
}
protected:
void paintEvent(QPaintEvent *) override
{
QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing, false);
QPen pen(QColor(QStringLiteral("#0EA5E9")));
pen.setWidth(2);
painter.setPen(pen);
painter.setBrush(Qt::NoBrush);
painter.drawRect(rect().adjusted(1, 1, -1, -1));
painter.fillRect(QRect(0, 0, 6, 6), QColor(QStringLiteral("#0EA5E9")));
painter.fillRect(QRect(width() - 6, 0, 6, 6), QColor(QStringLiteral("#0EA5E9")));
painter.fillRect(QRect(0, height() - 6, 6, 6), QColor(QStringLiteral("#0EA5E9")));
painter.fillRect(QRect(width() - 6, height() - 6, 6, 6), QColor(QStringLiteral("#0EA5E9")));
}
};
void placeTopMost(QWidget *widget, const QRect &geometry)
{
if (!widget) {
return;
}
widget->setGeometry(geometry);
#ifdef Q_OS_WIN
if (HWND hwnd = reinterpret_cast<HWND>(widget->winId())) {
SetWindowPos(hwnd,
HWND_TOPMOST,
geometry.x(),
geometry.y(),
geometry.width(),
geometry.height(),
SWP_NOOWNERZORDER | SWP_SHOWWINDOW | SWP_NOACTIVATE);
}
#endif
}
QPixmap decodePreviewPixmap(const QString &previewUrl)
{
if (!previewUrl.startsWith(QStringLiteral("data:image/png;base64,"))) {
return {};
}
const QByteArray data = QByteArray::fromBase64(previewUrl.mid(22).toLatin1());
QPixmap pixmap;
pixmap.loadFromData(data, "PNG");
return pixmap;
}
}
LongCaptureController::LongCaptureController(ScrollCaptureViewModel *scrollCaptureViewModel,
QObject *parent)
: QObject(parent)
, m_scrollCaptureViewModel(scrollCaptureViewModel)
{
if (!m_scrollCaptureViewModel) {
return;
}
connect(m_scrollCaptureViewModel, &ScrollCaptureViewModel::captureStarted, this, [this]() {
setRunning(true);
updateStatusText(tr("滚动检测中,滑动内容会自动拼接"));
});
connect(m_scrollCaptureViewModel, &ScrollCaptureViewModel::captureSucceeded, this, [this](const QString &) {
closeAll();
});
connect(m_scrollCaptureViewModel, &ScrollCaptureViewModel::captureFailed, this, [this](const QString &) {
closeAll();
});
connect(m_scrollCaptureViewModel, &ScrollCaptureViewModel::capturedFramesChanged, this, [this]() {
updateFrameCount(m_scrollCaptureViewModel->capturedFrames());
});
connect(m_scrollCaptureViewModel, &ScrollCaptureViewModel::statusTextChanged, this, [this]() {
updateStatusText(m_scrollCaptureViewModel->statusText());
});
connect(m_scrollCaptureViewModel, &ScrollCaptureViewModel::previewImageUrlChanged, this, [this]() {
updatePreviewImage(m_scrollCaptureViewModel->previewImageUrl());
});
}
void LongCaptureController::beginCapture(const QRect &captureRect)
{
m_captureRect = captureRect.normalized();
if (m_captureRect.isEmpty()) {
return;
}
ensureWidgets();
setRunning(false);
updateFrameCount(0);
updateStatusText(tr("点击开始后,滑动内容自动拼接"));
updatePreviewImage(QString());
updateWidgetGeometry();
if (m_barWidget) {
m_barWidget->show();
m_barWidget->raise();
}
if (m_previewWidget) {
m_previewWidget->show();
m_previewWidget->raise();
}
if (m_frameWidget) {
m_frameWidget->show();
m_frameWidget->raise();
}
}
bool LongCaptureController::eventFilter(QObject *watched, QEvent *event)
{
Q_UNUSED(watched);
if (event->type() == QEvent::KeyPress) {
auto *keyEvent = static_cast<QKeyEvent *>(event);
if (keyEvent->key() == Qt::Key_Escape) {
if (m_running && m_scrollCaptureViewModel) {
m_scrollCaptureViewModel->stop();
} else {
closeAll();
}
return true;
}
}
return QObject::eventFilter(watched, event);
}
void LongCaptureController::closeAll()
{
setRunning(false);
if (m_barWidget) {
m_barWidget->hide();
}
if (m_previewWidget) {
m_previewWidget->hide();
}
if (m_frameWidget) {
m_frameWidget->hide();
}
m_captureRect = {};
}
void LongCaptureController::ensureWidgets()
{
if (!m_barWidget) {
m_barWidget = new QWidget(nullptr);
m_barWidget->setWindowFlags(Qt::FramelessWindowHint | Qt::Tool | Qt::WindowStaysOnTopHint | Qt::WindowDoesNotAcceptFocus);
m_barWidget->setAttribute(Qt::WA_ShowWithoutActivating, true);
m_barWidget->installEventFilter(this);
m_barWidget->setObjectName(QStringLiteral("longCaptureBar"));
m_barWidget->setStyleSheet(loadLongCaptureStyleSheet());
m_barWidget->setFixedSize(500, 56);
auto *layout = new QHBoxLayout(m_barWidget);
layout->setContentsMargins(10, 10, 10, 10);
layout->setSpacing(10);
auto *badge = new QLabel(QStringLiteral("长"), m_barWidget);
badge->setObjectName(QStringLiteral("longCaptureBadge"));
badge->setAlignment(Qt::AlignCenter);
badge->setFixedSize(26, 26);
layout->addWidget(badge);
m_barMessageLabel = new QLabel(tr("点击开始后,滑动内容自动拼接"), m_barWidget);
m_barMessageLabel->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred);
layout->addWidget(m_barMessageLabel, 1);
m_barFrameLabel = new QLabel(tr("0 帧"), m_barWidget);
m_barFrameLabel->setObjectName(QStringLiteral("longCaptureFrameLabel"));
layout->addWidget(m_barFrameLabel);
m_startButton = new QPushButton(tr("开始"), m_barWidget);
m_startButton->setObjectName(QStringLiteral("longCapturePrimaryBtn"));
m_startButton->setFixedSize(72, 30);
layout->addWidget(m_startButton);
m_stopButton = new QPushButton(tr("关闭"), m_barWidget);
m_stopButton->setObjectName(QStringLiteral("longCaptureDangerBtn"));
m_stopButton->setFixedSize(72, 30);
layout->addWidget(m_stopButton);
connect(m_startButton, &QPushButton::clicked, this, [this]() {
if (m_captureRect.isEmpty() || !m_scrollCaptureViewModel) {
return;
}
setRunning(true);
updateStatusText(tr("滚动检测中,滑动内容会自动拼接"));
m_scrollCaptureViewModel->start(m_captureRect);
});
connect(m_stopButton, &QPushButton::clicked, this, [this]() {
if (m_running && m_scrollCaptureViewModel) {
m_scrollCaptureViewModel->stop();
} else {
closeAll();
}
});
}
if (!m_previewWidget) {
m_previewWidget = new QWidget(nullptr);
m_previewWidget->setWindowFlags(Qt::FramelessWindowHint | Qt::Tool | Qt::WindowStaysOnTopHint | Qt::WindowDoesNotAcceptFocus);
m_previewWidget->setAttribute(Qt::WA_ShowWithoutActivating, true);
m_previewWidget->installEventFilter(this);
m_previewWidget->setObjectName(QStringLiteral("longCapturePreview"));
m_previewWidget->setStyleSheet(loadLongCaptureStyleSheet());
m_previewWidget->setFixedSize(260, 420);
auto *layout = new QVBoxLayout(m_previewWidget);
layout->setContentsMargins(10, 10, 10, 10);
layout->setSpacing(8);
auto *headerLayout = new QHBoxLayout();
headerLayout->setContentsMargins(0, 0, 0, 0);
headerLayout->setSpacing(8);
auto *title = new QLabel(tr("长截图预览"), m_previewWidget);
title->setObjectName(QStringLiteral("longCapturePreviewTitle"));
headerLayout->addWidget(title);
headerLayout->addStretch(1);
m_previewFrameLabel = new QLabel(tr("0 帧"), m_previewWidget);
m_previewFrameLabel->setObjectName(QStringLiteral("longCapturePreviewSub"));
headerLayout->addWidget(m_previewFrameLabel);
layout->addLayout(headerLayout);
auto *imageBox = new QWidget(m_previewWidget);
imageBox->setObjectName(QStringLiteral("longCapturePreviewImageBox"));
imageBox->setFixedHeight(352);
auto *imageLayout = new QVBoxLayout(imageBox);
imageLayout->setContentsMargins(6, 6, 6, 6);
imageLayout->setSpacing(0);
m_previewImageLabel = new QLabel(imageBox);
m_previewImageLabel->setObjectName(QStringLiteral("longCapturePreviewEmpty"));
m_previewImageLabel->setAlignment(Qt::AlignCenter);
m_previewImageLabel->setText(tr("暂无预览"));
imageLayout->addWidget(m_previewImageLabel, 1);
layout->addWidget(imageBox, 1);
m_previewStatusLabel = new QLabel(tr("等待开始"), m_previewWidget);
m_previewStatusLabel->setObjectName(QStringLiteral("longCapturePreviewStatus"));
m_previewStatusLabel->setWordWrap(false);
layout->addWidget(m_previewStatusLabel);
}
if (!m_frameWidget) {
m_frameWidget = new LongCaptureFrameWidget(nullptr);
m_frameWidget->setAttribute(Qt::WA_ShowWithoutActivating, true);
m_frameWidget->installEventFilter(this);
}
}
void LongCaptureController::updateWidgetGeometry()
{
if (m_captureRect.isEmpty()) {
return;
}
QScreen *screen = QGuiApplication::screenAt(m_captureRect.center());
QRect workArea;
if (screen) {
workArea = screen->availableGeometry();
} else if (QScreen *primary = QGuiApplication::primaryScreen()) {
workArea = primary->availableGeometry();
}
if (workArea.isEmpty()) {
workArea = QRect(0, 0, 1920, 1080);
}
if (m_barWidget) {
const QSize size = m_barWidget->size();
const int x = qBound(workArea.left(),
m_captureRect.x() + m_captureRect.width() - size.width(),
qMax(workArea.left(), workArea.right() - size.width() + 1));
int y = m_captureRect.bottom() + 12;
if (y + size.height() > workArea.bottom() + 1) {
y = qMax(workArea.top(), m_captureRect.top() - size.height() - 12);
}
placeTopMost(m_barWidget, QRect(x, y, size.width(), size.height()));
}
if (m_previewWidget) {
const QSize size = m_previewWidget->size();
const int x = qMax(workArea.left(), workArea.right() - size.width() - 26);
const int y = qMax(workArea.top() + 20, workArea.center().y() - size.height() / 2);
placeTopMost(m_previewWidget, QRect(x, y, size.width(), size.height()));
}
if (m_frameWidget) {
const QRect frameRect = m_captureRect.adjusted(-2, -2, 2, 2);
placeTopMost(m_frameWidget, frameRect);
}
}
void LongCaptureController::updateFrameCount(int value)
{
if (m_barFrameLabel) {
m_barFrameLabel->setText(tr("%1 帧").arg(value));
}
if (m_previewFrameLabel) {
m_previewFrameLabel->setText(tr("%1 帧").arg(value));
}
}
void LongCaptureController::updateStatusText(const QString &text)
{
if (m_barMessageLabel) {
m_barMessageLabel->setText(text);
}
if (m_previewStatusLabel) {
m_previewStatusLabel->setText(text);
}
}
void LongCaptureController::updatePreviewImage(const QString &previewUrl)
{
if (!m_previewImageLabel) {
return;
}
const QPixmap pixmap = decodePreviewPixmap(previewUrl);
if (pixmap.isNull()) {
m_previewImageLabel->setPixmap(QPixmap());
m_previewImageLabel->setText(tr("暂无预览"));
m_previewImageLabel->setObjectName(QStringLiteral("longCapturePreviewEmpty"));
m_previewImageLabel->style()->unpolish(m_previewImageLabel);
m_previewImageLabel->style()->polish(m_previewImageLabel);
return;
}
m_previewImageLabel->setText(QString());
m_previewImageLabel->setObjectName(QString());
m_previewImageLabel->style()->unpolish(m_previewImageLabel);
m_previewImageLabel->style()->polish(m_previewImageLabel);
m_previewImageLabel->setPixmap(pixmap.scaled(236, 340, Qt::KeepAspectRatio, Qt::SmoothTransformation));
}
void LongCaptureController::setRunning(bool running)
{
m_running = running;
if (m_startButton) {
m_startButton->setVisible(!running);
}
if (m_stopButton) {
m_stopButton->setText(running ? tr("完成") : tr("关闭"));
}
}
+54
View File
@@ -0,0 +1,54 @@
#ifndef LONG_CAPTURE_CONTROLLER_H
#define LONG_CAPTURE_CONTROLLER_H
#include <QObject>
#include <QPointer>
#include <QRect>
class QLabel;
class QPushButton;
class QWidget;
class ScrollCaptureViewModel;
class LongCaptureController : public QObject
{
Q_OBJECT
public:
explicit LongCaptureController(ScrollCaptureViewModel *scrollCaptureViewModel,
QObject *parent = nullptr);
void beginCapture(const QRect &captureRect);
void closeAll();
protected:
bool eventFilter(QObject *watched, QEvent *event) override;
private:
void ensureWidgets();
void updateWidgetGeometry();
void updateFrameCount(int value);
void updateStatusText(const QString &text);
void updatePreviewImage(const QString &previewUrl);
void setRunning(bool running);
QPointer<ScrollCaptureViewModel> m_scrollCaptureViewModel;
QRect m_captureRect;
QWidget *m_barWidget = nullptr;
QWidget *m_previewWidget = nullptr;
QWidget *m_frameWidget = nullptr;
QLabel *m_barMessageLabel = nullptr;
QLabel *m_barFrameLabel = nullptr;
QPushButton *m_startButton = nullptr;
QPushButton *m_stopButton = nullptr;
QLabel *m_previewFrameLabel = nullptr;
QLabel *m_previewStatusLabel = nullptr;
QLabel *m_previewImageLabel = nullptr;
bool m_running = false;
};
#endif // LONG_CAPTURE_CONTROLLER_H
+147
View File
@@ -0,0 +1,147 @@
#include "widgets/magnifier_widget.h"
#include <QPainter>
namespace {
constexpr int kFrameSize = 130;
constexpr int kInfoHeight = 48;
constexpr int kOuterWidth = 130;
constexpr int kOuterHeight = 180;
constexpr int kLensPadding = 10;
constexpr int kSampleRadius = 11;
constexpr int kOffset = 15;
}
MagnifierWidget::MagnifierWidget(QWidget *parent)
: QWidget(parent)
{
setAttribute(Qt::WA_TransparentForMouseEvents, true);
setAttribute(Qt::WA_NoSystemBackground, true);
setFixedSize(kOuterWidth, kOuterHeight);
hide();
}
void MagnifierWidget::setSnapshot(const QPixmap &snapshot, const QRect &virtualGeometry)
{
m_snapshot = snapshot;
m_virtualGeometry = virtualGeometry;
update();
}
void MagnifierWidget::updateView(const QPoint &localPoint,
const QPoint &globalPoint,
const QColor &pixelColor,
const QSize &overlaySize)
{
m_localPoint = localPoint;
m_globalPoint = globalPoint;
m_pixelColor = pixelColor;
int x = localPoint.x() + kOffset;
if (x + width() > overlaySize.width()) {
x = localPoint.x() - width() - kOffset;
}
int y = localPoint.y() + kOffset;
if (y + height() > overlaySize.height()) {
y = localPoint.y() - height() - kOffset;
}
move(x, y);
show();
raise();
update();
}
void MagnifierWidget::paintEvent(QPaintEvent *event)
{
Q_UNUSED(event);
QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing, true);
const QRect frameRect(0, 0, kFrameSize, kFrameSize);
const QRect infoRect(0, kFrameSize + 2, kOuterWidth, kInfoHeight);
painter.setPen(QPen(Qt::white, 1));
painter.setBrush(QColor(26, 26, 26));
painter.drawRoundedRect(frameRect, 4, 4);
const QRect innerRect(kLensPadding, kLensPadding, 110, 110);
if (!m_snapshot.isNull()) {
const QRect srcRect(m_localPoint.x() - kSampleRadius,
m_localPoint.y() - kSampleRadius,
kSampleRadius * 2,
kSampleRadius * 2);
const QSize sampleSize(srcRect.width(), srcRect.height());
QPixmap region(sampleSize);
region.fill(Qt::black);
QPainter regionPainter(&region);
const QRect bounded = srcRect.intersected(m_snapshot.rect());
if (!bounded.isEmpty()) {
const int destX = bounded.x() - srcRect.x();
const int destY = bounded.y() - srcRect.y();
regionPainter.drawPixmap(destX,
destY,
m_snapshot,
bounded.x(),
bounded.y(),
bounded.width(),
bounded.height());
}
regionPainter.end();
painter.setRenderHint(QPainter::SmoothPixmapTransform, false);
painter.drawPixmap(innerRect, region, region.rect());
const int sampleCount = sampleSize.width();
const qreal cellW = qreal(innerRect.width()) / qreal(sampleCount);
const qreal cellH = qreal(innerRect.height()) / qreal(sampleCount);
painter.setPen(QPen(QColor(255, 255, 255, 38), 1));
for (int i = 1; i < sampleCount; ++i) {
const int x = qRound(innerRect.left() + i * cellW);
const int y = qRound(innerRect.top() + i * cellH);
painter.drawLine(x, innerRect.top(), x, innerRect.bottom());
painter.drawLine(innerRect.left(), y, innerRect.right(), y);
}
painter.setPen(QPen(QColor(255, 0, 0, 220), 1));
const int centerX = innerRect.center().x();
const int centerY = innerRect.center().y();
painter.drawLine(centerX, innerRect.top(), centerX, innerRect.bottom());
painter.drawLine(innerRect.left(), centerY, innerRect.right(), centerY);
}
painter.setPen(QPen(QColor("#555555"), 1));
painter.setBrush(QColor(26, 26, 26, 204));
painter.drawRoundedRect(infoRect, 4, 4);
const QRect swatchRect(10, infoRect.y() + 11, 16, 16);
painter.setPen(QPen(Qt::white, 1));
painter.setBrush(m_pixelColor);
painter.drawRoundedRect(swatchRect, 2, 2);
painter.setPen(Qt::white);
QFont textFont = painter.font();
textFont.setPixelSize(11);
textFont.setFamily(QStringLiteral("Consolas"));
painter.setFont(textFont);
const QString rgbText = QStringLiteral("R:%1 G:%2 B:%3")
.arg(m_pixelColor.red())
.arg(m_pixelColor.green())
.arg(m_pixelColor.blue());
painter.drawText(QRect(34, infoRect.y() + 4, 90, 18),
Qt::AlignLeft | Qt::AlignVCenter,
rgbText);
painter.setPen(QColor("#CBD5E1"));
QFont coordFont = painter.font();
coordFont.setPixelSize(10);
coordFont.setFamily(QStringLiteral("Consolas"));
painter.setFont(coordFont);
const QString pointText = QStringLiteral("X:%1 Y:%2")
.arg(m_globalPoint.x())
.arg(m_globalPoint.y());
painter.drawText(QRect(34, infoRect.y() + 22, 90, 16),
Qt::AlignLeft | Qt::AlignVCenter,
pointText);
}
+30
View File
@@ -0,0 +1,30 @@
#ifndef MAGNIFIER_WIDGET_H
#define MAGNIFIER_WIDGET_H
#include <QColor>
#include <QPixmap>
#include <QRect>
#include <QWidget>
class MagnifierWidget : public QWidget
{
Q_OBJECT
public:
explicit MagnifierWidget(QWidget *parent = nullptr);
void setSnapshot(const QPixmap &snapshot, const QRect &virtualGeometry);
void updateView(const QPoint &localPoint, const QPoint &globalPoint, const QColor &pixelColor, const QSize &overlaySize);
protected:
void paintEvent(QPaintEvent *event) override;
private:
QPixmap m_snapshot;
QRect m_virtualGeometry;
QPoint m_localPoint;
QPoint m_globalPoint;
QColor m_pixelColor = Qt::black;
};
#endif // MAGNIFIER_WIDGET_H
+386
View File
@@ -0,0 +1,386 @@
#include "widgets/selection_mask_controller.h"
#include <QPainter>
#include <QPainterPath>
namespace {
bool nearValue(int value, int target, int margin)
{
return value >= target - margin && value <= target + margin;
}
bool isResizeMode(SelectionMaskController::DragMode mode)
{
return mode != SelectionMaskController::DragMode::None
&& mode != SelectionMaskController::DragMode::Selecting
&& mode != SelectionMaskController::DragMode::Moving;
}
}
void SelectionMaskController::reset()
{
m_rect = QRect();
m_pressRect = QRect();
m_pressPos = QPoint();
m_dragMode = DragMode::None;
m_hoverMode = DragMode::None;
}
void SelectionMaskController::beginInteraction(const QPoint &pos)
{
m_pressPos = pos;
m_pressRect = rect();
if (hasSelection()) {
const DragMode hit = hitTest(pos);
if (hit != DragMode::None) {
m_dragMode = hit;
return;
}
}
m_dragMode = DragMode::Selecting;
m_rect = QRect(pos, pos);
}
void SelectionMaskController::updateInteraction(const QPoint &pos, const QSize &bounds)
{
const QPoint clampedPos = clampPointToBounds(pos, bounds);
switch (m_dragMode) {
case DragMode::Selecting:
m_rect = clampRectToBounds(QRect(m_pressPos, clampedPos).normalized(), bounds);
break;
case DragMode::Moving: {
QRect moved = m_pressRect.translated(clampedPos - m_pressPos);
if (moved.left() < 0) moved.moveLeft(0);
if (moved.top() < 0) moved.moveTop(0);
if (moved.right() > bounds.width()) moved.moveRight(bounds.width());
if (moved.bottom() > bounds.height()) moved.moveBottom(bounds.height());
m_rect = moved;
break;
}
case DragMode::ResizeTopLeft: {
QRect r = m_pressRect;
r.setTopLeft(clampedPos);
m_rect = clampRectToBounds(r.normalized(), bounds);
break;
}
case DragMode::ResizeTop: {
QRect r = m_pressRect;
r.setTop(clampedPos.y());
m_rect = clampRectToBounds(r.normalized(), bounds);
break;
}
case DragMode::ResizeTopRight: {
QRect r = m_pressRect;
r.setTopRight(clampedPos);
m_rect = clampRectToBounds(r.normalized(), bounds);
break;
}
case DragMode::ResizeRight: {
QRect r = m_pressRect;
r.setRight(clampedPos.x());
m_rect = clampRectToBounds(r.normalized(), bounds);
break;
}
case DragMode::ResizeBottomRight: {
QRect r = m_pressRect;
r.setBottomRight(clampedPos);
m_rect = clampRectToBounds(r.normalized(), bounds);
break;
}
case DragMode::ResizeBottom: {
QRect r = m_pressRect;
r.setBottom(clampedPos.y());
m_rect = clampRectToBounds(r.normalized(), bounds);
break;
}
case DragMode::ResizeBottomLeft: {
QRect r = m_pressRect;
r.setBottomLeft(clampedPos);
m_rect = clampRectToBounds(r.normalized(), bounds);
break;
}
case DragMode::ResizeLeft: {
QRect r = m_pressRect;
r.setLeft(clampedPos.x());
m_rect = clampRectToBounds(r.normalized(), bounds);
break;
}
case DragMode::None:
break;
}
}
void SelectionMaskController::endInteraction()
{
if (m_rect.width() < 2 || m_rect.height() < 2) {
m_rect = QRect();
}
m_dragMode = DragMode::None;
}
void SelectionMaskController::updateHover(const QPoint &pos)
{
if (!hasSelection()) {
m_hoverMode = DragMode::None;
return;
}
m_hoverMode = hitTest(pos);
}
Qt::CursorShape SelectionMaskController::cursorShape() const
{
const DragMode mode = (m_dragMode != DragMode::None) ? m_dragMode : m_hoverMode;
switch (mode) {
case DragMode::Moving: return Qt::SizeAllCursor;
case DragMode::ResizeTopLeft:
case DragMode::ResizeBottomRight: return Qt::SizeFDiagCursor;
case DragMode::ResizeTopRight:
case DragMode::ResizeBottomLeft: return Qt::SizeBDiagCursor;
case DragMode::ResizeTop:
case DragMode::ResizeBottom: return Qt::SizeVerCursor;
case DragMode::ResizeLeft:
case DragMode::ResizeRight: return Qt::SizeHorCursor;
case DragMode::Selecting:
case DragMode::None:
return Qt::CrossCursor;
}
return Qt::CrossCursor;
}
void SelectionMaskController::paint(QPainter &painter, const QRect &canvasRect) const
{
const QRect selection = rect();
const DragMode activeMode = (m_dragMode != DragMode::None) ? m_dragMode : m_hoverMode;
painter.setBrush(QColor(0, 0, 0, 120));
painter.setPen(Qt::NoPen);
QPainterPath path;
path.addRect(canvasRect.adjusted(-1, -1, 1, 1));
if (!selection.isEmpty()) {
path.addRect(selection);
}
painter.drawPath(path);
if (selection.isEmpty()) {
return;
}
painter.setBrush(Qt::NoBrush);
painter.setPen(QPen(QColor(255, 255, 255, 235), 2.0));
painter.drawRect(selection);
painter.setPen(QPen(QColor(37, 99, 235, 235), activeMode == DragMode::Moving ? 1.6 : 1.2));
painter.drawRect(selection);
const QString text = QStringLiteral("X:%1 Y:%2 宽:%3 高:%4")
.arg(selection.x()).arg(selection.y())
.arg(selection.width()).arg(selection.height());
QFont font = painter.font();
font.setPointSizeF(font.pointSizeF() > 0 ? font.pointSizeF() + 1.5 : 12.5);
font.setBold(true);
painter.setFont(font);
const QFontMetrics fm(font);
const int textWidth = fm.horizontalAdvance(text);
const int textHeight = fm.height();
QRect labelRect;
if (selection.y() - 25 >= 0) {
labelRect = QRect(selection.x(), selection.y() - 32, textWidth + 22, textHeight + 10);
} else {
labelRect = QRect(selection.x() + 2, selection.y() + 2, textWidth + 22, textHeight + 10);
}
painter.setPen(QPen(QColor(125, 211, 252, 210), 1));
painter.setBrush(QColor(15, 23, 42, 210));
painter.drawRoundedRect(labelRect, 5, 5);
painter.setPen(Qt::white);
painter.setBrush(Qt::NoBrush);
painter.drawText(labelRect, Qt::AlignCenter, text);
const QList<DragMode> handleModes {
DragMode::ResizeTopLeft,
DragMode::ResizeTop,
DragMode::ResizeTopRight,
DragMode::ResizeRight,
DragMode::ResizeBottomRight,
DragMode::ResizeBottom,
DragMode::ResizeBottomLeft,
DragMode::ResizeLeft
};
painter.setPen(Qt::NoPen);
for (DragMode mode : handleModes) {
const QRect r = handleRect(mode);
if (r.isEmpty()) {
continue;
}
const bool highlighted = (activeMode == mode);
const QRect outerRect = r.adjusted(-1, -1, 1, 1);
painter.setBrush(QColor(255, 255, 255, highlighted ? 255 : 235));
painter.setPen(Qt::NoPen);
painter.drawEllipse(outerRect);
painter.setBrush(highlighted ? QColor("#2563EB") : QColor("#0EA5E9"));
painter.drawEllipse(r);
if (highlighted) {
painter.setBrush(QColor(37, 99, 235, 50));
painter.drawEllipse(outerRect.adjusted(-2, -2, 2, 2));
}
}
if (activeMode != DragMode::None && activeMode != DragMode::Selecting && activeMode != DragMode::Moving) {
painter.setPen(QPen(QColor(37, 99, 235, 180), 1, Qt::DashLine));
switch (activeMode) {
case DragMode::ResizeTop:
painter.drawLine(selection.topLeft(), selection.topRight());
break;
case DragMode::ResizeBottom:
painter.drawLine(selection.bottomLeft(), selection.bottomRight());
break;
case DragMode::ResizeLeft:
painter.drawLine(selection.topLeft(), selection.bottomLeft());
break;
case DragMode::ResizeRight:
painter.drawLine(selection.topRight(), selection.bottomRight());
break;
case DragMode::ResizeTopLeft:
painter.drawLine(selection.topLeft(), selection.topRight());
painter.drawLine(selection.topLeft(), selection.bottomLeft());
break;
case DragMode::ResizeTopRight:
painter.drawLine(selection.topLeft(), selection.topRight());
painter.drawLine(selection.topRight(), selection.bottomRight());
break;
case DragMode::ResizeBottomLeft:
painter.drawLine(selection.topLeft(), selection.bottomLeft());
painter.drawLine(selection.bottomLeft(), selection.bottomRight());
break;
case DragMode::ResizeBottomRight:
painter.drawLine(selection.topRight(), selection.bottomRight());
painter.drawLine(selection.bottomLeft(), selection.bottomRight());
break;
default:
break;
}
}
}
QRect SelectionMaskController::clampRectToBounds(const QRect &rect, const QSize &bounds)
{
QRect r = rect;
if (r.left() < 0) r.moveLeft(0);
if (r.top() < 0) r.moveTop(0);
if (r.right() > bounds.width()) r.setRight(bounds.width());
if (r.bottom() > bounds.height()) r.setBottom(bounds.height());
return r.normalized();
}
QPoint SelectionMaskController::clampPointToBounds(const QPoint &point, const QSize &bounds)
{
return QPoint(qBound(0, point.x(), bounds.width()),
qBound(0, point.y(), bounds.height()));
}
SelectionMaskController::DragMode SelectionMaskController::hitTest(const QPoint &pos) const
{
const QRect r = rect();
if (r.isEmpty()) {
return DragMode::None;
}
const int left = r.left();
const int top = r.top();
const int right = r.right();
const int bottom = r.bottom();
const int x = pos.x();
const int y = pos.y();
if (nearValue(x, left, m_hitMargin) && nearValue(y, top, m_hitMargin)) {
return DragMode::ResizeTopLeft;
}
if (nearValue(y, top, m_hitMargin) && x >= left && x <= right) {
return DragMode::ResizeTop;
}
if (nearValue(x, right, m_hitMargin) && nearValue(y, top, m_hitMargin)) {
return DragMode::ResizeTopRight;
}
if (nearValue(x, right, m_hitMargin) && y >= top && y <= bottom) {
return DragMode::ResizeRight;
}
if (nearValue(x, right, m_hitMargin) && nearValue(y, bottom, m_hitMargin)) {
return DragMode::ResizeBottomRight;
}
if (nearValue(y, bottom, m_hitMargin) && x >= left && x <= right) {
return DragMode::ResizeBottom;
}
if (nearValue(x, left, m_hitMargin) && nearValue(y, bottom, m_hitMargin)) {
return DragMode::ResizeBottomLeft;
}
if (nearValue(x, left, m_hitMargin) && y >= top && y <= bottom) {
return DragMode::ResizeLeft;
}
if (isMoveHit(pos)) {
return DragMode::Moving;
}
return DragMode::None;
}
bool SelectionMaskController::isMoveHit(const QPoint &pos) const
{
const QRect r = rect();
if (r.isEmpty()) {
return false;
}
return pos.x() > r.left() + 1
&& pos.y() > r.top() + 1
&& pos.x() < r.right() - 1
&& pos.y() < r.bottom() - 1;
}
QRect SelectionMaskController::handleRect(DragMode mode) const
{
const QRect r = rect();
if (r.isEmpty() || !isResizeMode(mode)) {
return {};
}
QPoint center;
switch (mode) {
case DragMode::ResizeTopLeft:
center = r.topLeft();
break;
case DragMode::ResizeTop:
center = QPoint(r.center().x(), r.top());
break;
case DragMode::ResizeTopRight:
center = r.topRight();
break;
case DragMode::ResizeRight:
center = QPoint(r.right(), r.center().y());
break;
case DragMode::ResizeBottomRight:
center = r.bottomRight();
break;
case DragMode::ResizeBottom:
center = QPoint(r.center().x(), r.bottom());
break;
case DragMode::ResizeBottomLeft:
center = r.bottomLeft();
break;
case DragMode::ResizeLeft:
center = QPoint(r.left(), r.center().y());
break;
default:
return {};
}
return QRect(center.x() - m_handleSize / 2,
center.y() - m_handleSize / 2,
m_handleSize,
m_handleSize);
}
+60
View File
@@ -0,0 +1,60 @@
#ifndef SELECTION_MASK_CONTROLLER_H
#define SELECTION_MASK_CONTROLLER_H
#include <QPoint>
#include <QRect>
#include <QSize>
#include <Qt>
class QPainter;
class SelectionMaskController
{
public:
enum class DragMode {
None,
Selecting,
Moving,
ResizeTopLeft,
ResizeTop,
ResizeTopRight,
ResizeRight,
ResizeBottomRight,
ResizeBottom,
ResizeBottomLeft,
ResizeLeft
};
void reset();
QRect rect() const { return m_rect.normalized(); }
QRect rawRect() const { return m_rect; }
bool hasSelection() const { return !m_rect.normalized().isEmpty(); }
DragMode dragMode() const { return m_dragMode; }
void beginInteraction(const QPoint &pos);
void updateInteraction(const QPoint &pos, const QSize &bounds);
void endInteraction();
void updateHover(const QPoint &pos);
Qt::CursorShape cursorShape() const;
void paint(QPainter &painter, const QRect &canvasRect) const;
private:
static QRect clampRectToBounds(const QRect &rect, const QSize &bounds);
static QPoint clampPointToBounds(const QPoint &point, const QSize &bounds);
DragMode hitTest(const QPoint &pos) const;
bool isMoveHit(const QPoint &pos) const;
QRect handleRect(DragMode mode) const;
QRect m_rect;
QRect m_pressRect;
QPoint m_pressPos;
DragMode m_dragMode = DragMode::None;
DragMode m_hoverMode = DragMode::None;
int m_hitMargin = 8;
int m_handleSize = 10;
};
#endif // SELECTION_MASK_CONTROLLER_H
+46 -2
View File
@@ -32,6 +32,15 @@ void StickyCanvasWidget::setBackgroundImage(const QImage &image)
update();
}
void StickyCanvasWidget::setBackgroundVisible(bool visible)
{
if (m_backgroundVisible == visible) {
return;
}
m_backgroundVisible = visible;
update();
}
void StickyCanvasWidget::setViewScale(qreal scale)
{
const qreal next = qMax<qreal>(0.01, scale);
@@ -110,6 +119,39 @@ void StickyCanvasWidget::addTextAnnotation(const QPoint &point, const QString &t
update();
}
QImage StickyCanvasWidget::compositedImage() const
{
if (size().isEmpty()) {
return {};
}
QImage image(size(), QImage::Format_ARGB32_Premultiplied);
image.fill(Qt::transparent);
QPainter painter(&image);
painter.setRenderHint(QPainter::Antialiasing, true);
if (!m_backgroundImage.isNull()) {
painter.drawImage(QRect(QPoint(0, 0), size()), m_backgroundImage);
} else {
painter.fillRect(image.rect(), Qt::white);
}
painter.save();
painter.scale(m_viewScale, m_viewScale);
for (Shape *shape : m_shapes) {
if (shape) {
shape->draw(&painter);
}
}
if (m_currentShape) {
m_currentShape->draw(&painter);
}
painter.restore();
return image;
}
void StickyCanvasWidget::undo()
{
if (!m_shapes.isEmpty()) {
@@ -131,9 +173,11 @@ void StickyCanvasWidget::paintEvent(QPaintEvent *event)
Q_UNUSED(event);
QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing, true);
painter.fillRect(rect(), Qt::white);
if (m_backgroundVisible) {
painter.fillRect(rect(), Qt::white);
}
if (!m_backgroundImage.isNull()) {
if (m_backgroundVisible && !m_backgroundImage.isNull()) {
painter.drawImage(rect(), m_backgroundImage);
}
+4
View File
@@ -19,6 +19,7 @@ public:
~StickyCanvasWidget() override;
void setBackgroundImage(const QImage &image);
void setBackgroundVisible(bool visible);
void setViewScale(qreal scale);
void setActiveShapeType(Shapeype type);
void setPenColor(const QColor &color);
@@ -30,6 +31,8 @@ public:
void setNumberAutoIncrement(bool enabled);
bool drawingEnabled() const { return m_drawingEnabled; }
Shapeype activeShapeType() const { return m_shapeType; }
bool hasAnnotations() const { return !m_shapes.isEmpty(); }
QImage compositedImage() const;
void addTextAnnotation(const QPoint &point, const QString &text);
void undo();
void reset();
@@ -52,6 +55,7 @@ private:
QList<Shape*> m_shapes;
Shape *m_currentShape = nullptr;
QImage m_backgroundImage;
bool m_backgroundVisible = true;
QColor m_penColor = Qt::red;
int m_penSize = 6;
Shapeype m_shapeType = PEN;
+29 -1
View File
@@ -1,10 +1,17 @@
#include "widgets/widget_window_bridge.h"
#include "widgets/capture_overlay_widget.h"
#include "widgets/long_capture_controller.h"
#include "widgets/settings_dialog.h"
WidgetWindowBridge::WidgetWindowBridge(SettingsDialog *settingsDialog, QObject *parent)
WidgetWindowBridge::WidgetWindowBridge(SettingsDialog *settingsDialog,
CaptureOverlayWidget *captureOverlay,
LongCaptureController *longCaptureController,
QObject *parent)
: QObject(parent)
, m_settingsDialog(settingsDialog)
, m_captureOverlay(captureOverlay)
, m_longCaptureController(longCaptureController)
{
}
@@ -14,3 +21,24 @@ void WidgetWindowBridge::showSettings()
m_settingsDialog->showAndActivate();
}
}
void WidgetWindowBridge::showCaptureOverlay(const QString &mode)
{
if (m_captureOverlay) {
m_captureOverlay->showAndActivate(mode);
}
}
void WidgetWindowBridge::toggleCaptureOverlay(const QString &mode)
{
if (m_captureOverlay) {
m_captureOverlay->toggleCapture(mode);
}
}
void WidgetWindowBridge::requestLongCapture(const QRect &captureRect)
{
if (m_longCaptureController) {
m_longCaptureController->beginCapture(captureRect);
}
}
+13 -1
View File
@@ -2,20 +2,32 @@
#define WIDGET_WINDOW_BRIDGE_H
#include <QObject>
#include <QRect>
#include <QString>
class SettingsDialog;
class CaptureOverlayWidget;
class LongCaptureController;
class WidgetWindowBridge : public QObject
{
Q_OBJECT
public:
explicit WidgetWindowBridge(SettingsDialog *settingsDialog, QObject *parent = nullptr);
explicit WidgetWindowBridge(SettingsDialog *settingsDialog,
CaptureOverlayWidget *captureOverlay,
LongCaptureController *longCaptureController,
QObject *parent = nullptr);
Q_INVOKABLE void showSettings();
Q_INVOKABLE void showCaptureOverlay(const QString &mode = QStringLiteral("copy"));
Q_INVOKABLE void toggleCaptureOverlay(const QString &mode = QStringLiteral("copy"));
Q_INVOKABLE void requestLongCapture(const QRect &captureRect);
private:
SettingsDialog *m_settingsDialog = nullptr;
CaptureOverlayWidget *m_captureOverlay = nullptr;
LongCaptureController *m_longCaptureController = nullptr;
};
#endif // WIDGET_WINDOW_BRIDGE_H