Refactor QML capture flow and polish sticky tools

This commit is contained in:
easyz committed 2026-03-25 22:51:19 +08:00
1 parent f140eec539
commit 1efdf4dbad
40 files changed
+4756 -377

No files matched your search

+1
View File
@@ -84,6 +84,7 @@ x64/
x86/
[Bb]uild/
[Bb]uild*/
dist/
CMakeFiles/
CMakeCache.txt
cmake_install.cmake
+16 -1
View File
@@ -25,6 +25,11 @@ find_package(Qt6 REQUIRED COMPONENTS
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)
@@ -148,6 +153,10 @@ 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
@@ -393,9 +402,15 @@ if(WIN32)
target_include_directories(TZshot PRIVATE "${OCR_INSTALL_DIR}/include")
target_link_directories(TZshot PRIVATE "${OCR_INSTALL_DIR}/lib")
set(OCR_LEPTONICA_LINK_NAME "leptonica-${OCR_LEPTONICA_VERSION}$<$<CONFIG:Debug>:d>")
if(MINGW)
# Our MinGW Leptonica ExternalProject installs libleptonica.dll.a
# without a version/debug suffix, so link against that stable name.
set(OCR_LEPTONICA_LINK_NAME "leptonica")
endif()
target_link_libraries(TZshot PRIVATE
"tesseract55$<$<CONFIG:Debug>:d>"
"leptonica-${OCR_LEPTONICA_VERSION}$<$<CONFIG:Debug>:d>"
"${OCR_LEPTONICA_LINK_NAME}"
Ws2_32
)
+25 -15
View File
@@ -1,13 +1,28 @@
<RCC>
<qresource prefix="/">
<file>qml/main.qml</file>
<file>qml/TZStickyWindow.qml</file>
<file>qml/TZWindow.qml</file>
<file>qml/TZLable.qml</file>
<file>qml/TZTool.qml</file>
<file>qml/TZResizeHandle.qml</file>
<file>qml/TZResizeHandleGroup.qml</file>
<file>qml/TZChat.qml</file>
<file>qml/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/TZStickyExtraToolBar.qml</file>
<file>qml/Windows/TZOcrResult.qml</file>
<file>qml/Windows/TZChat.qml</file>
<file>qml/Windows/TZLoadingSpinner.qml</file>
<file>qml/Setting/TZSettingWin.qml</file>
<file>qml/Setting/TZGeneralSettings.qml</file>
<file>qml/Setting/TZRightPanel.qml</file>
@@ -29,19 +44,14 @@
<file>resource/img/lc_highlighter.svg</file>
<file>resource/img/lc_undo.svg</file>
<file>resource/img/tray_icon.svg</file>
<file>qml/TZMarkToolInfo.qml</file>
<file>qml/TZMagnifyingGlass.qml</file>
<file>resource/img/lc_close.svg</file>
<file>resource/img/lc_chat.svg</file>
<file>resource/img/lc_emoji.svg</file>
<file>resource/img/lc_file.svg</file>
<file>resource/img/lc_send.svg</file>
<file>qml/TZLoadingSpinner.qml</file>
<file>qml/TZGifRecordBar.qml</file>
<file>qml/TZOcrResult.qml</file>
<file>qml/TZLongCaptureBar.qml</file>
<file>qml/TZLongCapturePreview.qml</file>
<file>qml/TZLongCaptureFrame.qml</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
View File
@@ -0,0 +1,56 @@
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
@@ -0,0 +1,194 @@
import QtQuick 2.12
import QtQuick.Layouts 1.12
import QtQuick.Controls 2.5
// GIF 录制控制栏:在截图选区确定后显示
Rectangle {
id: gifBar
radius: 10
color: "#FFFFFF"
border.color: "#E2E8F0"
border.width: 1
layer.enabled: true
// 外部传入的录制区域
property rect captureRect: Qt.rect(0, 0, 0, 0)
signal signalCancel
signal signalRecordFinished
readonly property bool isRecording: O_GifRecordVM.isRecording
readonly property bool isEncoding: O_GifRecordVM.isEncoding
readonly property int elapsedSecs: O_GifRecordVM.elapsedSecs
readonly property int frameCount: O_GifRecordVM.frameCount
Connections {
target: O_GifRecordVM
function onEncodingFinished(savedPath) {
gifBar.signalRecordFinished()
}
function onEncodingFailed(error) {
console.warn("[GifRecordBar] 录制失败:", error)
gifBar.signalRecordFinished()
}
}
SequentialAnimation {
id: blinkAnim
running: gifBar.isRecording
loops: Animation.Infinite
PropertyAnimation { target: recDot; property: "opacity"; to: 0.2; duration: 500 }
PropertyAnimation { target: recDot; property: "opacity"; to: 1.0; duration: 500 }
}
RowLayout {
anchors.fill: parent
anchors.margins: 6
layoutDirection: Qt.RightToLeft
spacing: 4
// 取消按钮
Rectangle {
width: 32; height: 32; radius: 7
visible: !gifBar.isEncoding
color: cancelArea.containsMouse ? "#FEE2E2" : "transparent"
Behavior on color { ColorAnimation { duration: 100 } }
Image {
anchors.centerIn: parent
source: "qrc:/resource/img/lc_x.svg"
width: 18; height: 18
fillMode: Image.PreserveAspectFit
smooth: true
}
MouseArea {
id: cancelArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onPressed: {
O_GifRecordVM.cancelRecording()
gifBar.signalCancel()
}
}
}
// 停止录制按钮
Rectangle {
width: 32; height: 32; radius: 7
visible: gifBar.isRecording && !gifBar.isEncoding
color: stopArea.containsMouse ? "#FEF3C7" : "transparent"
Behavior on color { ColorAnimation { duration: 100 } }
Image {
anchors.centerIn: parent
source: "qrc:/resource/img/lc_gif_stop.svg"
width: 18; height: 18
fillMode: Image.PreserveAspectFit
smooth: true
layer.enabled: true
layer.effect: null
}
MouseArea {
id: stopArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onPressed: O_GifRecordVM.stopRecording()
}
}
Rectangle {
width: 1; height: 20
visible: gifBar.isRecording && !gifBar.isEncoding
color: "#E2E8F0"
Layout.alignment: Qt.AlignVCenter
}
// 录制中:时间 + 帧数
Item {
visible: gifBar.isRecording && !gifBar.isEncoding
Layout.fillWidth: true
height: 32
implicitWidth: recRow.implicitWidth + 8
Row {
id: recRow
anchors.centerIn: parent
spacing: 6
Rectangle {
id: recDot
width: 8; height: 8; radius: 4
color: "#EF4444"
anchors.verticalCenter: parent.verticalCenter
}
Text {
text: {
var m = Math.floor(gifBar.elapsedSecs / 60)
var s = gifBar.elapsedSecs % 60
return (m < 10 ? "0" + m : m) + ":" + (s < 10 ? "0" + s : s)
}
font.pixelSize: 13
font.family: "monospace"
color: "#1E293B"
anchors.verticalCenter: parent.verticalCenter
}
Text {
text: gifBar.frameCount + "f"
font.pixelSize: 11
color: "#94A3B8"
anchors.verticalCenter: parent.verticalCenter
}
}
}
// 编码中:进度提示
Item {
visible: gifBar.isEncoding
Layout.fillWidth: true
height: 32
implicitWidth: encodingRow.implicitWidth + 16
Row {
id: encodingRow
anchors.centerIn: parent
spacing: 8
Rectangle {
width: 16; height: 16
color: "transparent"
anchors.verticalCenter: parent.verticalCenter
Rectangle {
width: 16; height: 16
radius: 8
color: "transparent"
border.color: "#3B82F6"
border.width: 2
Rectangle {
width: 6; height: 6
color: "#3B82F6"
radius: 3
x: 5; y: -1
}
RotationAnimation on rotation {
running: gifBar.isEncoding
from: 0; to: 360
duration: 800
loops: Animation.Infinite
}
}
}
Text {
text: qsTr("正在生成 GIF...")
font.pixelSize: 13
color: "#1E293B"
anchors.verticalCenter: parent.verticalCenter
}
}
}
}
}
+96
View File
@@ -0,0 +1,96 @@
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
@@ -0,0 +1,26 @@
import QtQuick
// 显示当前截图大小
Rectangle {
property string lableText: ""
radius: 6
color: "#1E293B"
opacity: 0.85
border.color: "#334155"
border.width: 1
implicitWidth: labelContent.implicitWidth + 16
implicitHeight: labelContent.implicitHeight + 8
Text {
id: labelContent
anchors.centerIn: parent
font.pixelSize: 12
font.family: "Microsoft YaHei"
color: "#F1F5F9"
text: lableText
renderType: Text.NativeRendering
}
}
+116
View File
@@ -0,0 +1,116 @@
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
@@ -0,0 +1,257 @@
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
Rectangle {
id: panel
property color currentColor: "#F43F5E"
property int currentSize: 6
property bool showColorRow: true
property string toolName: ""
property string annotationText: qsTr("文本")
property bool textBackgroundEnabled: true
property int mosaicBlurLevel: 2
property bool numberAutoIncrement: true
property int numberValue: 1
width: 260
height: toolName === "text" ? 128
: (toolName === "number" ? 132
: ((toolName === "mosaic") ? 92 : (showColorRow ? 84 : 50)))
radius: 10
color: "#FFFFFF"
border.color: "#E2E8F0"
border.width: 1
ColumnLayout {
anchors.fill: parent
anchors.margins: 10
spacing: 8
RowLayout {
spacing: 6
Layout.fillWidth: true
visible: panel.showColorRow
height: panel.showColorRow ? implicitHeight : 0
Repeater {
model: ["#F43F5E","#EF4444","#F97316","#EAB308",
"#22C55E","#3B82F6","#8B5CF6","#000000","#FFFFFF"]
delegate: Rectangle {
width: 18; height: 18; radius: 9
color: modelData
border.color: panel.currentColor === modelData ? "#3B82F6" : "#CBD5E1"
border.width: panel.currentColor === modelData ? 2 : 1
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onPressed: panel.currentColor = modelData
}
}
}
}
RowLayout {
spacing: 6
Layout.fillWidth: true
Repeater {
model: [
{ size: 2, label: qsTr("细") },
{ size: 4, label: qsTr("中") },
{ size: 6, label: qsTr("粗") },
{ size: 10, label: qsTr("特粗") }
]
delegate: Rectangle {
width: 48; height: 28
radius: 6
color: panel.currentSize === modelData.size ? "#EFF6FF" : (sizeArea.containsMouse ? "#F1F5F9" : "transparent")
border.color: panel.currentSize === modelData.size ? "#3B82F6" : "#E2E8F0"
border.width: 1
Column {
anchors.centerIn: parent
spacing: 3
Rectangle {
anchors.horizontalCenter: parent.horizontalCenter
width: 24
height: Math.min(modelData.size, 6)
radius: height / 2
color: panel.currentSize === modelData.size ? "#3B82F6" : "#94A3B8"
}
Text {
anchors.horizontalCenter: parent.horizontalCenter
text: modelData.label
font.pixelSize: 10
color: panel.currentSize === modelData.size ? "#3B82F6" : "#64748B"
}
}
MouseArea {
id: sizeArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onPressed: panel.currentSize = modelData.size
}
}
}
}
RowLayout {
visible: panel.toolName === "text"
Layout.fillWidth: true
spacing: 8
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 34
radius: 6
color: "#F8FAFC"
border.color: textInput.activeFocus ? "#3B82F6" : "#E2E8F0"
border.width: 1
TextInput {
id: textInput
anchors.fill: parent
anchors.margins: 8
verticalAlignment: TextInput.AlignVCenter
color: "#0F172A"
font.pixelSize: 13
selectByMouse: true
text: panel.annotationText
onTextChanged: panel.annotationText = text
}
Text {
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
anchors.leftMargin: 8
visible: textInput.text.length === 0 && !textInput.activeFocus
text: qsTr("输入文字")
color: "#94A3B8"
font.pixelSize: 13
}
}
CheckBox {
text: qsTr("背景")
checked: panel.textBackgroundEnabled
onToggled: panel.textBackgroundEnabled = checked
}
}
RowLayout {
visible: panel.toolName === "mosaic"
Layout.fillWidth: true
spacing: 8
Text {
text: qsTr("模糊")
color: "#334155"
}
Slider {
Layout.fillWidth: true
from: 1
to: 4
stepSize: 1
value: panel.mosaicBlurLevel
onMoved: panel.mosaicBlurLevel = Math.round(value)
}
Text {
text: panel.mosaicBlurLevel
color: "#64748B"
}
}
ColumnLayout {
visible: panel.toolName === "number"
Layout.fillWidth: true
spacing: 8
CheckBox {
text: qsTr("自动递增")
checked: panel.numberAutoIncrement
onToggled: panel.numberAutoIncrement = checked
}
RowLayout {
Layout.fillWidth: true
spacing: 8
Text {
text: qsTr("数字")
color: "#334155"
}
Rectangle {
width: 28
height: 28
radius: 6
color: minusArea.containsMouse ? "#F1F5F9" : "transparent"
border.color: "#CBD5E1"
border.width: 1
Text {
anchors.centerIn: parent
text: "-"
color: "#334155"
font.pixelSize: 16
}
MouseArea {
id: minusArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onPressed: panel.numberValue = Math.max(1, panel.numberValue - 1)
}
}
Rectangle {
Layout.fillWidth: true
height: 28
radius: 6
color: "#F8FAFC"
border.color: "#E2E8F0"
border.width: 1
Text {
anchors.centerIn: parent
text: panel.numberValue
color: "#0F172A"
font.pixelSize: 13
}
}
Rectangle {
width: 28
height: 28
radius: 6
color: plusArea.containsMouse ? "#F1F5F9" : "transparent"
border.color: "#CBD5E1"
border.width: 1
Text {
anchors.centerIn: parent
text: "+"
color: "#334155"
font.pixelSize: 16
}
MouseArea {
id: plusArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onPressed: panel.numberValue = Math.min(9999, panel.numberValue + 1)
}
}
}
}
}
}
+48
View File
@@ -0,0 +1,48 @@
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
@@ -0,0 +1,14 @@
import QtQuick
Rectangle {
// id:leftTop
// x: screenshotArea.x - handleSize/2
// y: screenshotArea.y - handleSize/2
property int handleSize: 8
width: handleSize
height: handleSize
radius: handleSize/2
color: "#0088FF"
border.color: "white"
border.width: 1
}
+301
View File
@@ -0,0 +1,301 @@
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
@@ -0,0 +1,19 @@
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))
}
+147
View File
@@ -0,0 +1,147 @@
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
property var coordinateItem: null
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 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 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"
}
function reset() {
area = Qt.rect(0, 0, 0, 0)
initialArea = Qt.rect(0, 0, 0, 0)
dragOffset = Qt.point(0, 0)
dragMode = "none"
}
}
+34
View File
@@ -0,0 +1,34 @@
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
@@ -0,0 +1,101 @@
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)
}
}
}
}
}
+265
View File
@@ -0,0 +1,265 @@
import QtQuick 2.12
import QtQuick.Layouts 1.12
import QtQuick.Controls 2.5
Rectangle {
id: tool
radius: 10
color: "#FFFFFF"
visible: false
border.color: "#E2E8F0"
border.width: 1
layer.enabled: true
implicitWidth: toolRow.implicitWidth + 12
implicitHeight: 44
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 shapeType: {
switch (activeTool) {
case "pencil": return 0
case "rect": return 1
case "circle": return 2
case "arrow": return 3
case "mosaic": return 4
case "text": return 5
case "number": return 6
case "highlight": return 7
default: return 1
}
}
function setActiveTool(name) {
if (activeTool === name) {
activeTool = ""
markToolInfoVisible = false
} else {
activeTool = name
markToolInfoVisible = true
}
}
function showTip(text, targetItem) {
hoverTipText = text
if (targetItem) {
var p = tool.mapFromItem(targetItem, targetItem.width / 2, targetItem.height)
hoverTipCenterX = p.x
} else {
hoverTipCenterX = width / 2
}
}
function hideTip(text) {
if (text === undefined || hoverTipText === text)
hoverTipText = ""
}
RowLayout {
id: toolRow
anchors.fill: parent
anchors.margins: 6
layoutDirection: Qt.RightToLeft
spacing: 2
Rectangle {
visible: tool.showScreenshotButton
width: 32; height: 32; radius: 7
color: checkArea.containsMouse ? "#DCFCE7" : "transparent"
Image { anchors.centerIn: parent; source: "qrc:/resource/img/lc_check.svg"; width: 18; height: 18 }
MouseArea { id: checkArea; anchors.fill: parent; hoverEnabled: true; cursorShape: Qt.PointingHandCursor; onEntered: tool.showTip(qsTr("完成"), parent); onExited: tool.hideTip(qsTr("完成")); onPressed: signalScreenshot() }
}
Rectangle {
visible: tool.showCancelButton
width: 32; height: 32; radius: 7
color: closeArea.containsMouse ? "#FEE2E2" : "transparent"
Image { anchors.centerIn: parent; source: "qrc:/resource/img/lc_x.svg"; width: 18; height: 18 }
MouseArea { id: closeArea; anchors.fill: parent; hoverEnabled: true; cursorShape: Qt.PointingHandCursor; onEntered: tool.showTip(qsTr("取消"), parent); onExited: tool.hideTip(qsTr("取消")); onPressed: signalCancel() }
}
Rectangle { visible: tool.showScreenshotButton || tool.showCancelButton; width: 1; height: 20; color: "#E2E8F0"; Layout.alignment: Qt.AlignVCenter }
Rectangle {
visible: tool.showOcrButton
width: 32; height: 32; radius: 7
color: ocrArea.containsMouse ? "#F0FDF4" : "transparent"
Image { anchors.centerIn: parent; source: "qrc:/resource/img/lc_ocr.svg"; width: 18; height: 18 }
MouseArea { id: ocrArea; anchors.fill: parent; hoverEnabled: true; cursorShape: Qt.PointingHandCursor; onEntered: tool.showTip(qsTr("OCR"), parent); onExited: tool.hideTip(qsTr("OCR")); onPressed: signalOcr() }
}
Rectangle {
id: gifRecordBtn
visible: tool.showGifRecordButton
width: 32; height: 32; radius: 7
color: gifArea.containsMouse ? "#FFF7ED" : "transparent"
Image { anchors.centerIn: parent; source: "qrc:/resource/img/lc_gif_record.svg"; width: 18; height: 18 }
MouseArea { id: gifArea; anchors.fill: parent; hoverEnabled: true; cursorShape: Qt.PointingHandCursor; onEntered: tool.showTip(qsTr("录制GIF"), parent); onExited: tool.hideTip(qsTr("录制GIF")); onPressed: signalGifRecord() }
}
Rectangle {
visible: tool.showLongScreenshotButton
width: 32; height: 32; radius: 7
color: longShotArea.containsMouse ? "#FFF7ED" : "transparent"
Image { anchors.centerIn: parent; source: "qrc:/resource/img/lc_longshot.svg"; width: 18; height: 18 }
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: 20; color: "#E2E8F0"; Layout.alignment: Qt.AlignVCenter }
Rectangle {
visible: tool.showSaveButton
width: 32; height: 32; radius: 7
color: saveArea.containsMouse ? "#EFF6FF" : "transparent"
Image { anchors.centerIn: parent; source: "qrc:/resource/img/lc_save.svg"; width: 18; height: 18 }
MouseArea { id: saveArea; anchors.fill: parent; hoverEnabled: true; cursorShape: Qt.PointingHandCursor; onEntered: tool.showTip(qsTr("保存"), parent); onExited: tool.hideTip(qsTr("保存")); onPressed: signalSave() }
}
Rectangle {
visible: tool.showStickyButton
width: 32; height: 32; radius: 7
color: fixArea.containsMouse ? "#EFF6FF" : "transparent"
Image { anchors.centerIn: parent; source: "qrc:/resource/img/lc_pin.svg"; width: 18; height: 18 }
MouseArea { id: fixArea; anchors.fill: parent; hoverEnabled: true; cursorShape: Qt.PointingHandCursor; onEntered: tool.showTip(qsTr("贴图"), parent); onExited: tool.hideTip(qsTr("贴图")); onPressed: signalSticky() }
}
Rectangle { visible: tool.showSaveButton || tool.showStickyButton; width: 1; height: 20; color: "#E2E8F0"; Layout.alignment: Qt.AlignVCenter }
Rectangle {
visible: tool.showRectButton
width: 32; height: 32; radius: 7
color: (tool.activeTool === "rect") ? "#EFF6FF" : (rectArea.containsMouse ? "#F1F5F9" : "transparent")
border.color: (tool.activeTool === "rect") ? "#3B82F6" : "transparent"
border.width: 1
Image { anchors.centerIn: parent; source: "qrc:/resource/img/lc_square.svg"; width: 18; height: 18 }
MouseArea { id: rectArea; anchors.fill: parent; hoverEnabled: true; cursorShape: Qt.PointingHandCursor; onEntered: tool.showTip(qsTr("矩形"), parent); onExited: tool.hideTip(qsTr("矩形")); onPressed: tool.setActiveTool("rect") }
}
Rectangle {
visible: tool.showCircleButton
width: 32; height: 32; radius: 7
color: (tool.activeTool === "circle") ? "#EFF6FF" : (circleArea.containsMouse ? "#F1F5F9" : "transparent")
border.color: (tool.activeTool === "circle") ? "#3B82F6" : "transparent"
border.width: 1
Image { anchors.centerIn: parent; source: "qrc:/resource/img/lc_circle.svg"; width: 18; height: 18 }
MouseArea { id: circleArea; anchors.fill: parent; hoverEnabled: true; cursorShape: Qt.PointingHandCursor; onEntered: tool.showTip(qsTr("圆形"), parent); onExited: tool.hideTip(qsTr("圆形")); onPressed: tool.setActiveTool("circle") }
}
Rectangle {
visible: tool.showArrowButton
width: 32; height: 32; radius: 7
color: (tool.activeTool === "arrow") ? "#EFF6FF" : (arrowArea.containsMouse ? "#F1F5F9" : "transparent")
border.color: (tool.activeTool === "arrow") ? "#3B82F6" : "transparent"
border.width: 1
Image { anchors.centerIn: parent; source: "qrc:/resource/img/lc_arrow.svg"; width: 18; height: 18 }
MouseArea { id: arrowArea; anchors.fill: parent; hoverEnabled: true; cursorShape: Qt.PointingHandCursor; onEntered: tool.showTip(qsTr("箭头"), parent); onExited: tool.hideTip(qsTr("箭头")); onPressed: tool.setActiveTool("arrow") }
}
Rectangle {
visible: tool.showPencilButton
width: 32; height: 32; radius: 7
color: (tool.activeTool === "pencil") ? "#EFF6FF" : (pencilArea.containsMouse ? "#F1F5F9" : "transparent")
border.color: (tool.activeTool === "pencil") ? "#3B82F6" : "transparent"
border.width: 1
Image { anchors.centerIn: parent; source: "qrc:/resource/img/lc_pencil.svg"; width: 18; height: 18 }
MouseArea { id: pencilArea; anchors.fill: parent; hoverEnabled: true; cursorShape: Qt.PointingHandCursor; onEntered: tool.showTip(qsTr("画笔"), parent); onExited: tool.hideTip(qsTr("画笔")); onPressed: tool.setActiveTool("pencil") }
}
Rectangle {
visible: tool.showHighlightButton
width: 32; height: 32; radius: 7
color: (tool.activeTool === "highlight") ? "#EFF6FF" : (highlightArea.containsMouse ? "#F1F5F9" : "transparent")
border.color: (tool.activeTool === "highlight") ? "#3B82F6" : "transparent"
border.width: 1
Image { anchors.centerIn: parent; source: "qrc:/resource/img/lc_highlighter.svg"; width: 18; height: 18 }
MouseArea { id: highlightArea; anchors.fill: parent; hoverEnabled: true; cursorShape: Qt.PointingHandCursor; onEntered: tool.showTip(qsTr("高亮"), parent); onExited: tool.hideTip(qsTr("高亮")); onPressed: tool.setActiveTool("highlight") }
}
Rectangle {
visible: tool.showMosaicButton
width: 32; height: 32; radius: 7
color: (tool.activeTool === "mosaic") ? "#EFF6FF" : (mosaicArea.containsMouse ? "#F1F5F9" : "transparent")
border.color: (tool.activeTool === "mosaic") ? "#3B82F6" : "transparent"
border.width: 1
Image { anchors.centerIn: parent; source: "qrc:/resource/img/lc_mosaic.svg"; width: 18; height: 18 }
MouseArea { id: mosaicArea; anchors.fill: parent; hoverEnabled: true; cursorShape: Qt.PointingHandCursor; onEntered: tool.showTip(qsTr("马赛克"), parent); onExited: tool.hideTip(qsTr("马赛克")); onPressed: tool.setActiveTool("mosaic") }
}
Rectangle {
visible: tool.showTextButton
width: 32; height: 32; radius: 7
color: (tool.activeTool === "text") ? "#EFF6FF" : (textArea.containsMouse ? "#F1F5F9" : "transparent")
border.color: (tool.activeTool === "text") ? "#3B82F6" : "transparent"
border.width: 1
Image { anchors.centerIn: parent; source: "qrc:/resource/img/lc_text.svg"; width: 18; height: 18 }
MouseArea { id: textArea; anchors.fill: parent; hoverEnabled: true; cursorShape: Qt.PointingHandCursor; onEntered: tool.showTip(qsTr("文字"), parent); onExited: tool.hideTip(qsTr("文字")); onPressed: tool.setActiveTool("text") }
}
Rectangle {
visible: tool.showNumberButton
width: 32; height: 32; radius: 7
color: (tool.activeTool === "number") ? "#EFF6FF" : (numberArea.containsMouse ? "#F1F5F9" : "transparent")
border.color: (tool.activeTool === "number") ? "#3B82F6" : "transparent"
border.width: 1
Image { anchors.centerIn: parent; source: "qrc:/resource/img/lc_list_ordered.svg"; width: 18; height: 18 }
MouseArea { id: numberArea; anchors.fill: parent; hoverEnabled: true; cursorShape: Qt.PointingHandCursor; onEntered: tool.showTip(qsTr("序号"), parent); onExited: tool.hideTip(qsTr("序号")); onPressed: tool.setActiveTool("number") }
}
Rectangle {
visible: tool.showUndoButton
width: 32; height: 32; radius: 7
color: undoArea.containsMouse ? "#EFF6FF" : "transparent"
Image { anchors.centerIn: parent; source: "qrc:/resource/img/lc_undo.svg"; width: 18; height: 18 }
MouseArea { id: undoArea; anchors.fill: parent; hoverEnabled: true; cursorShape: Qt.PointingHandCursor; onEntered: tool.showTip(qsTr("撤销"), parent); onExited: tool.hideTip(qsTr("撤销")); onPressed: signalUndo() }
}
}
Rectangle {
id: tipBubble
visible: tool.visible && tool.hoverTipText.length > 0
z: 100
parent: tool.parent
width: Math.max(44, tipText.implicitWidth + 14)
height: 24
radius: 6
color: "#0F172A"
opacity: 0.92
x: Math.max(tool.x, Math.min(tool.x + tool.width - width, tool.x + tool.hoverTipCenterX - width / 2))
y: tool.y + tool.height + 6
Text {
id: tipText
anchors.centerIn: parent
text: tool.hoverTipText
color: "#FFFFFF"
font.pixelSize: 11
}
}
}
+764
View File
@@ -0,0 +1,764 @@
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()
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)
selectionOverlay.beginSelection(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
}
}
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) {
console.log("imgRect-->",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)
console.log("[Sticky] imageUrl:", url)
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
@@ -0,0 +1,116 @@
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
@@ -0,0 +1,46 @@
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
@@ -0,0 +1,110 @@
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
@@ -0,0 +1,53 @@
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
@@ -0,0 +1,83 @@
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
}
}
}
}
+3 -1
View File
@@ -13,6 +13,8 @@ Rectangle {
// 跟随鼠标移动,始终显示(不随截图状态切换)
property int magnifierMouseX: 0
property int magnifierMouseY: 0
property int magnifierAbsX: 0
property int magnifierAbsY: 0
property color magnifierPixelColor: "#000000"
// 跟随鼠标,偏移 +15px,防止被遮挡;超出屏幕时翻转
@@ -49,7 +51,7 @@ Rectangle {
cache: false
// source 用时间戳 +坐标,强制 QML 重新加载
source: root.visibility === Window.FullScreen
? ("image://magnifier/" + magnifierMouseX + "," + magnifierMouseY + "?t=" + Date.now())
? ("image://magnifier/" + magnifierAbsX + "," + magnifierAbsY + "?t=" + Date.now())
: ""
fillMode: Image.Stretch
}
+4 -11
View File
@@ -661,17 +661,10 @@ Window {
}
onWheel: function(wheel) {
if (wheel.modifiers & Qt.ControlModifier) {
if (wheel.angleDelta.y > 0)
setZoomValue(stickyWin.zoomFactor + 0.1)
else if (wheel.angleDelta.y < 0)
setZoomValue(stickyWin.zoomFactor - 0.1)
} else {
if (wheel.angleDelta.y > 0)
setOpacityValue(stickyWin.imgOpacity + 0.05)
else if (wheel.angleDelta.y < 0)
setOpacityValue(stickyWin.imgOpacity - 0.05)
}
if (wheel.angleDelta.y > 0)
setZoomValue(stickyWin.zoomFactor + 0.1)
else if (wheel.angleDelta.y < 0)
setZoomValue(stickyWin.zoomFactor - 0.1)
wheel.accepted = true
}
}
+2
View File
@@ -982,6 +982,8 @@ Window {
// Magnifier follows mouse in local coordinates
magnifier.magnifierMouseX = mouseX
magnifier.magnifierMouseY = mouseY
magnifier.magnifierAbsX = ax
magnifier.magnifierAbsY = ay
handleMouseMoved(ax, ay, pressed, mouse.modifiers)
}
+265
View File
@@ -0,0 +1,265 @@
import QtQuick
import QtQuick.Window
import QtQuick.Layouts
import QtQuick.Controls.Basic
QtObject {
id: chatPanel
// 对应贴图的图片 URL,由外部绑定
property string imageUrl: ""
signal signalSendPrompt(var text)
// 公开方法
function open(px, py) {
win.x = px
win.y = py
win.visible = true
win.raise()
win.requestActivate()
}
function close() {
win.visible = false
}
// ── 内部 Window ─────────────────────────────────────
property var win: Window {
id: win
flags: Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint | Qt.Tool
color: "transparent"
width: 420
height: 380
visible: false
Rectangle {
anchors.fill: parent
color: "#ffffff"
radius: 10
border.color: "#E2E8F0"
border.width: 1
ColumnLayout {
anchors.fill: parent
anchors.margins: 12
spacing: 8
// ── 顶部标题栏 ────────────────────────────────────
Rectangle {
Layout.fillWidth: true
height: 36
color: "transparent"
// 拖动区域(z:-1 让关闭按钮的点击不被拦截)
MouseArea {
id: dragHandle
anchors.fill: parent
z: -1
property point startPos
onPressed: startPos = Qt.point(mouseX, mouseY)
onPositionChanged: {
if (pressed) {
win.x += mouseX - startPos.x
win.y += mouseY - startPos.y
}
}
cursorShape: pressed ? Qt.ClosedHandCursor : Qt.OpenHandCursor
}
RowLayout {
anchors.fill: parent
spacing: 8
Image {
source: "qrc:/resource/img/lc_chat.svg"
width: 20
height: 20
fillMode: Image.PreserveAspectFit
}
Text {
text: qsTr("AI 图像编辑")
font.pixelSize: 14
font.weight: Font.Medium
color: "#1E293B"
}
Item { Layout.fillWidth: true }
// 关闭按钮
Rectangle {
width: 28
height: 28
radius: 6
color: clearMouse.containsMouse ? "#FEE2E2" : "transparent"
Image {
anchors.centerIn: parent
source: "qrc:/resource/img/lc_close.svg"
width: 14
height: 14
fillMode: Image.PreserveAspectFit
}
MouseArea {
id: clearMouse
anchors.fill: parent
hoverEnabled: true
onClicked: chatPanel.close()
}
}
}
// 标题栏底部分割线
Rectangle {
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
height: 1
color: "#E2E8F0"
}
}
// ── 贴图缩略图预览 ────────────────────────────────
Rectangle {
Layout.fillWidth: true
height: 64
radius: 6
color: "#F1F5F9"
border.color: "#E2E8F0"
border.width: 1
clip: true
RowLayout {
anchors.fill: parent
anchors.margins: 8
spacing: 10
// 缩略图
Rectangle {
width: 48
height: 48
radius: 4
color: "#E2E8F0"
clip: true
Image {
anchors.fill: parent
source: chatPanel.imageUrl
fillMode: Image.PreserveAspectCrop
cache: false
}
}
// 文字说明
Column {
Layout.fillWidth: true
spacing: 3
Text {
text: qsTr("当前贴图")
font.pixelSize: 11
color: "#94A3B8"
}
Text {
width: parent.width
text: chatPanel.imageUrl !== ""
? chatPanel.imageUrl.replace(/^image:\/\/[^/]+\//, "ID: ")
: qsTr("未关联贴图")
font.pixelSize: 12
color: "#475569"
elide: Text.ElideRight
}
Text {
text: qsTr("发送后将对此图执行 AI 编辑")
font.pixelSize: 10
color: "#94A3B8"
}
}
}
}
// ── 输入区域 ──────────────────────────────────────
Rectangle {
Layout.fillWidth: true
Layout.fillHeight: true
color: "#F8FAFC"
radius: 8
border.color: "#E2E8F0"
border.width: 1
TextArea {
id: input
anchors.fill: parent
anchors.margins: 12
font.family: "Segoe UI"
font.pixelSize: 13
color: "#1E293B"
wrapMode: Text.Wrap
placeholderText: qsTr("描述您想要的修改...")
background: Rectangle { color: "transparent" }
selectByMouse: true
focus: true
}
}
// ── 底部工具栏 ────────────────────────────────────
Rectangle {
Layout.fillWidth: true
height: 40
color: "transparent"
RowLayout {
anchors.fill: parent
spacing: 8
Item { Layout.fillWidth: true }
// 取消按钮
Rectangle {
width: 70; height: 32; radius: 6
color: cancelMouse.containsMouse ? "#F1F5F9" : "#F8FAFC"
border.color: "#E2E8F0"; border.width: 1
Text { anchors.centerIn: parent; text: qsTr("取消"); font.pixelSize: 13; color: "#64748B" }
MouseArea {
id: cancelMouse
anchors.fill: parent; hoverEnabled: true
onClicked: chatPanel.close()
}
}
// 发送按钮
Rectangle {
width: 70; height: 32; radius: 6
color: sendMouse.pressed ? "#0066cc" : (sendMouse.containsMouse ? "#3399ff" : "#0088ff")
RowLayout {
anchors.centerIn: parent; spacing: 4
Image { source: "qrc:/resource/img/lc_send.svg"; width: 14; height: 14 }
Text { text: qsTr("发送"); font.pixelSize: 13; color: "white" }
}
MouseArea {
id: sendMouse
anchors.fill: parent; hoverEnabled: true
onClicked: {
if (input.text.trim() !== "") {
chatPanel.signalSendPrompt(input.text)
chatPanel.close()
}
}
}
}
}
}
}
}
}
}
+52
View File
@@ -0,0 +1,52 @@
import QtQuick
// ── AI 加载动画层 ─────────────────────────────────────
Rectangle {
width: 100
height: 100
radius: 2
color: "#99000000" // 半透明黑色遮罩
// 旋转圆弧
Item {
id: spinnerRoot
anchors.centerIn: parent
width: 48
height: 48
RotationAnimator {
target: spinnerRoot
from: 0; to: 360
duration: 900
loops: Animation.Infinite
running: O_AIViewModel.isLoading
}
// 四个小圆点模拟旋转圆弧
Repeater {
model: 8
Rectangle {
width: 6
height: 6
radius: 3
color: "white"
opacity: (index + 1) / 8
x: spinnerRoot.width / 2 + 18 * Math.cos(index * Math.PI / 4) - width / 2
y: spinnerRoot.height / 2 + 18 * Math.sin(index * Math.PI / 4) - height / 2
}
}
}
// 提示文字
Text {
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: spinnerRoot.bottom
anchors.topMargin: 10
text: "AI 生成中..."
color: "white"
font.pixelSize: 13
}
}
+328
View File
@@ -0,0 +1,328 @@
import QtQuick
import QtQuick.Window
import QtQuick.Controls
import QtQuick.Layouts
// TZOcrResult
// -----------
// 无边框悬浮窗,展示 OCR 识别结果。
// 由 TZWindow.qml 通过 Qt.createComponent 动态创建并显示。
Window {
id: ocrWin
flags: Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint | Qt.Tool
color: "transparent"
width: 420
height: 320
// 初始居中(创建后由调用方设置位置,此处给默认值)
x: (Screen.width - width) / 2
y: (Screen.height - height) / 2
Component.onCompleted: {
ocrWin.raise()
ocrWin.requestActivate()
}
Shortcut {
sequence: "Esc"
onActivated: ocrWin.close()
}
// ── 阴影容器 ──────────────────────────────────────────
Rectangle {
id: shadow
anchors.fill: parent
anchors.margins: 10
radius: 12
color: "white"
layer.enabled: true
border.color: "#D1D5DB"
border.width: 1
// ── 标题栏 ─────────────────────────────────────────
Rectangle {
id: titleBar
anchors.top: shadow.top
anchors.left: shadow.left
anchors.right: shadow.right
height: 40
radius: 12
color: "#F8FAFC"
// 圆角只在上半部分(下方盖住)
Rectangle {
anchors.bottom: titleBar.bottom
anchors.left: titleBar.left
anchors.right: titleBar.right
height: titleBar.radius
color: titleBar.color
}
// 标题文字
Text {
anchors.verticalCenter: titleBar.verticalCenter
anchors.left: titleBar.left
anchors.leftMargin: 14
text: qsTr("OCR 识别结果")
font.pixelSize: 14
font.weight: Font.Medium
color: "#1E293B"
}
// 关闭按钮
Rectangle {
id: closeBtn
anchors.right: titleBar.right
anchors.rightMargin: 8
anchors.verticalCenter: titleBar.verticalCenter
width: 26; height: 26; radius: 5
color: closeArea.containsMouse ? "#FEE2E2" : "transparent"
Behavior on color { ColorAnimation { duration: 80 } }
Text {
anchors.centerIn: parent
text: "✕"
color: closeArea.containsMouse ? "#DC2626" : "#94A3B8"
font.pixelSize: 13
}
MouseArea {
id: closeArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: ocrWin.close()
}
}
// 拖动标题栏移动窗口
MouseArea {
anchors.fill: titleBar
anchors.rightMargin: 40
property point pressPos
onPressed: function(mouse) {
pressPos = Qt.point(mouse.x, mouse.y)
}
onPositionChanged: function(mouse) {
if (pressed) {
ocrWin.x += mouse.x - pressPos.x
ocrWin.y += mouse.y - pressPos.y
}
}
}
}
// ── 分割线 ─────────────────────────────────────────
Rectangle {
id: divider
anchors.top: titleBar.bottom
anchors.left: shadow.left
anchors.right: shadow.right
height: 1
color: "#E2E8F0"
}
// ── 内容区 ─────────────────────────────────────────
Item {
anchors.top: divider.bottom
anchors.left: shadow.left
anchors.right: shadow.right
anchors.bottom: bottomBar.top
// Loading 遮罩
Rectangle {
anchors.fill: parent
color: "#F8FAFC"
visible: O_OcrVM.isRecognizing
radius: 6
Item {
id: spinnerRoot
anchors.centerIn: parent
width: 48; height: 48
RotationAnimator {
target: spinnerRoot
from: 0; to: 360
duration: 900
loops: Animation.Infinite
running: O_OcrVM.isRecognizing
}
Repeater {
model: 8
Rectangle {
width: 6; height: 6; radius: 3
color: "#3B82F6"
opacity: (index + 1) / 8
x: spinnerRoot.width / 2 + 18 * Math.cos(index * Math.PI / 4) - width / 2
y: spinnerRoot.height / 2 + 18 * Math.sin(index * Math.PI / 4) - height / 2
}
}
}
Text {
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: spinnerRoot.bottom
anchors.topMargin: 12
text: qsTr("正在识别中…")
color: "#64748B"
font.pixelSize: 13
}
}
// 文本区域(识别完成后显示)
ScrollView {
anchors.fill: parent
anchors.margins: 8
visible: !O_OcrVM.isRecognizing
clip: true
TextArea {
id: resultArea
text: O_OcrVM.resultText
readOnly: false // 允许用户二次编辑
wrapMode: TextEdit.Wrap
font.pixelSize: 14
color: "#1E293B"
background: null
padding: 4
selectByMouse: true
placeholderText: qsTr("识别结果将显示在这里…")
placeholderTextColor: "#94A3B8"
}
}
}
// ── 底部操作栏 ─────────────────────────────────────
Rectangle {
id: bottomBar
anchors.bottom: shadow.bottom
anchors.left: shadow.left
anchors.right: shadow.right
height: 44
radius: 12
color: "#F8FAFC"
// 圆角只在下半部分
Rectangle {
anchors.top: bottomBar.top
anchors.left: bottomBar.left
anchors.right: bottomBar.right
height: bottomBar.radius
color: bottomBar.color
}
// 分割线
Rectangle {
anchors.top: bottomBar.top
anchors.left: bottomBar.left
anchors.right: bottomBar.right
height: 1
color: "#E2E8F0"
}
Row {
anchors.centerIn: parent
spacing: 10
// 一键复制
Rectangle {
id: copyBtn
width: 100; height: 30; radius: 6
color: copyArea.containsMouse ? "#DBEAFE" : "#EFF6FF"
border.color: "#3B82F6"
border.width: 1
Behavior on color { ColorAnimation { duration: 80 } }
Text {
anchors.centerIn: parent
text: qsTr("复制全部")
color: "#3B82F6"
font.pixelSize: 13
}
MouseArea {
id: copyArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
O_OcrVM.copyResultToClipboard()
copyFeedbackTimer.start()
}
}
}
// 关闭
Rectangle {
width: 70; height: 30; radius: 6
color: closeBtn2Area.containsMouse ? "#F1F5F9" : "transparent"
border.color: "#CBD5E1"
border.width: 1
Behavior on color { ColorAnimation { duration: 80 } }
Text {
anchors.centerIn: parent
text: qsTr("关闭")
color: "#64748B"
font.pixelSize: 13
}
MouseArea {
id: closeBtn2Area
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: ocrWin.close()
}
}
}
}
// 复制成功短暂提示
Rectangle {
id: copyFeedback
anchors.horizontalCenter: shadow.horizontalCenter
anchors.bottom: bottomBar.top
anchors.bottomMargin: 6
width: 100; height: 28; radius: 6
color: "#22C55E"
visible: false
opacity: 0
Text {
anchors.centerIn: parent
text: qsTr("已复制!")
color: "white"
font.pixelSize: 12
}
Timer {
id: copyFeedbackTimer
interval: 1200
onTriggered: copyFeedback.visible = false
}
onVisibleChanged: {
if (visible) opacity = 1
}
Behavior on opacity {
NumberAnimation { duration: 300 }
}
}
Connections {
target: copyFeedbackTimer
function onTriggered() {
copyFeedback.opacity = 0
}
}
}
// 启动计时后立即显示反馈
Connections {
target: copyArea
function onClicked() {
copyFeedback.visible = true
}
}
}
+40
View File
@@ -0,0 +1,40 @@
import QtQuick
Rectangle {
id: bar
radius: 10
color: "#FFFFFF"
border.color: "#E2E8F0"
border.width: 1
signal zoomOut()
signal zoomIn()
signal rotate90()
signal mirror()
signal resetSize()
signal doneAndCopy()
signal hideToolbar()
signal hoverEntered(string text, Item targetItem)
signal hoverExited(string text)
property real zoomFactor: 1.0
width: extraRow.implicitWidth + 16
height: 40
Row {
id: extraRow
anchors.centerIn: parent
spacing: 6
Rectangle { width: 30; height: 30; radius: 7; color: zoomOutArea.containsMouse ? "#F1F5F9" : "transparent"; Text { anchors.centerIn: parent; text: "-"; color: "#334155"; font.pixelSize: 16 } MouseArea { id: zoomOutArea; anchors.fill: parent; hoverEnabled: true; cursorShape: Qt.PointingHandCursor; onEntered: bar.hoverEntered(qsTr("Zoom out"), parent); onExited: bar.hoverExited(qsTr("Zoom out")); onPressed: bar.zoomOut() } }
Rectangle { width: 40; height: 30; radius: 7; color: "transparent"; Text { anchors.centerIn: parent; text: Math.round(bar.zoomFactor * 100) + "%"; color: "#64748B"; font.pixelSize: 11 } }
Rectangle { width: 30; height: 30; radius: 7; color: zoomInArea.containsMouse ? "#F1F5F9" : "transparent"; Text { anchors.centerIn: parent; text: "+"; color: "#334155"; font.pixelSize: 16 } MouseArea { id: zoomInArea; anchors.fill: parent; hoverEnabled: true; cursorShape: Qt.PointingHandCursor; onEntered: bar.hoverEntered(qsTr("Zoom in"), parent); onExited: bar.hoverExited(qsTr("Zoom in")); onPressed: bar.zoomIn() } }
Rectangle { width: 30; height: 30; radius: 7; color: rotateArea.containsMouse ? "#F1F5F9" : "transparent"; Text { anchors.centerIn: parent; text: "↻"; color: "#334155"; font.pixelSize: 14 } MouseArea { id: rotateArea; anchors.fill: parent; hoverEnabled: true; cursorShape: Qt.PointingHandCursor; onEntered: bar.hoverEntered(qsTr("Rotate 90°"), parent); onExited: bar.hoverExited(qsTr("Rotate 90°")); onPressed: bar.rotate90() } }
Rectangle { width: 30; height: 30; radius: 7; color: mirrorArea.containsMouse ? "#F1F5F9" : "transparent"; Text { anchors.centerIn: parent; text: "⇋"; color: "#334155"; font.pixelSize: 14 } MouseArea { id: mirrorArea; anchors.fill: parent; hoverEnabled: true; cursorShape: Qt.PointingHandCursor; onEntered: bar.hoverEntered(qsTr("Mirror"), parent); onExited: bar.hoverExited(qsTr("Mirror")); onPressed: bar.mirror() } }
Rectangle { width: 30; height: 30; radius: 7; color: resetSizeArea.containsMouse ? "#F1F5F9" : "transparent"; Text { anchors.centerIn: parent; text: "1:1"; color: "#334155"; font.pixelSize: 10 } MouseArea { id: resetSizeArea; anchors.fill: parent; hoverEnabled: true; cursorShape: Qt.PointingHandCursor; onEntered: bar.hoverEntered(qsTr("Reset size"), parent); onExited: bar.hoverExited(qsTr("Reset size")); onPressed: bar.resetSize() } }
Rectangle { width: 1; height: 18; color: "#E2E8F0"; anchors.verticalCenter: parent.verticalCenter }
Rectangle { width: 30; height: 30; radius: 7; color: overwriteArea.containsMouse ? "#E0F2FE" : "transparent"; Image { anchors.centerIn: parent; source: "qrc:/resource/img/lc_check.svg"; width: 18; height: 18 } MouseArea { id: overwriteArea; anchors.fill: parent; hoverEnabled: true; cursorShape: Qt.PointingHandCursor; onEntered: bar.hoverEntered(qsTr("Done and copy"), parent); onExited: bar.hoverExited(qsTr("Done and copy")); onPressed: bar.doneAndCopy() } }
Rectangle { width: 30; height: 30; radius: 7; color: clearToolArea.containsMouse ? "#FEE2E2" : "transparent"; Image { anchors.centerIn: parent; source: "qrc:/resource/img/lc_x.svg"; width: 18; height: 18 } MouseArea { id: clearToolArea; anchors.fill: parent; hoverEnabled: true; cursorShape: Qt.PointingHandCursor; onEntered: bar.hoverEntered(qsTr("Hide toolbar"), parent); onExited: bar.hoverExited(qsTr("Hide toolbar")); onPressed: bar.hideToolbar() } }
}
}
+640
View File
@@ -0,0 +1,640 @@
import QtQuick
import QtQuick.Window
import Qt.labs.platform 1.0 as Platform
import QtQuick.Effects
import QtQuick.Controls.Basic
import QtCore
import CustomComponents 1.0
import "../Capture"
Window {
id: stickyWin
flags: Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint | Qt.Tool
color: "transparent"
property string imageUrl: ""
property rect imgRect: Qt.rect(0, 0, 0, 0)
property bool contentActive: true
property bool cleanedUp: false
property real imgOpacity: 1.0
property string activeTool: ""
property bool toolbarVisible: true
property string toolbarHoverTipText: ""
property real toolbarHoverTipCenterX: 0
property bool contextMenuOpen: false
property int imageRevision: 0
property int sourcePixelW: 0
property int sourcePixelH: 0
property real currentDpr: (screen && screen.devicePixelRatio > 0) ? screen.devicePixelRatio : 1.0
property int imageBaseW: Math.max(1, Math.abs(imgRect.width))
property int imageBaseH: Math.max(1, Math.abs(imgRect.height))
property real zoomFactor: 1.0
readonly property int toolbarHeight: 40
readonly property int toolbarGap: 8
readonly property int toolbarTipGap: 6
readonly property int toolbarTipHeight: 24
readonly property int shadowRadius: 12
readonly property int shadowOffset: 4
readonly property int imageTopLeftX: (imgRect.width >= 0 ? imgRect.x : imgRect.x + imgRect.width)
readonly property int imageTopLeftY: (imgRect.height >= 0 ? imgRect.y : imgRect.y + imgRect.height)
property var targetScreen: null
readonly property int displayW: Math.max(1, Math.round(imageBaseW * zoomFactor))
readonly property int displayH: Math.max(1, Math.round(imageBaseH * zoomFactor))
readonly property int extraToolbarWidth: stickyExtraBar.width
readonly property int toolbarContentWidth: stickyTool.implicitWidth + extraToolbarWidth + 6
readonly property int toolbarBlockX: Math.max(8, Math.min(stickyWin.width - toolbarContentWidth - 8, (stickyWin.displayW - toolbarContentWidth) / 2))
readonly property int stickyShapeType: {
switch (activeTool) {
case "pencil": return 0
case "rect": return 1
case "circle": return 2
case "arrow": return 3
case "mosaic": return 4
default: return 1
}
}
width: Math.max(displayW, toolbarContentWidth + 16)
height: displayH + toolbarGap + toolbarHeight + toolbarTipGap + toolbarTipHeight + 4
function findScreenForPoint(px, py) {
var screens = Qt.application.screens
for (var i = 0; i < screens.length; ++i) {
var s = screens[i]
if (!s || !s.geometry)
continue
var g = s.geometry
if (px >= g.x && px < g.x + g.width && py >= g.y && py < g.y + g.height)
return s
}
return null
}
function syncInitialPosition() {
O_StickyViewModel.positionStickyWindow(stickyWin, imgRect)
}
Settings {
id: menuPrefs
category: "sticky_menu"
property bool pinSaveMerged: true
property bool pinCopy: true
property bool pinAi: false
property bool pinToolbar: false
property bool pinClose: false
}
function setActiveTool(name) {
if (stickyTool.activeTool === name)
stickyTool.activeTool = ""
else
stickyTool.activeTool = name
}
function showToolbarTip(text, item) {
toolbarHoverTipText = text
if (item) {
var p = stickyExtraBar.mapFromItem(item, item.width / 2, item.height)
toolbarHoverTipCenterX = p.x
} else {
toolbarHoverTipCenterX = stickyExtraBar.width / 2
}
}
function hideToolbarTip(text) {
if (text === undefined || toolbarHoverTipText === text)
toolbarHoverTipText = ""
}
function displaySourceUrl() {
if (typeof stickyWin.imageUrl !== "string" || stickyWin.imageUrl.length === 0)
return ""
return stickyWin.imageUrl + "?rev=" + stickyWin.imageRevision
}
function setOpacityValue(v) {
stickyWin.imgOpacity = Math.max(0.1, Math.min(1.0, v))
}
function setZoomValue(v) {
zoomFactor = Math.max(0.3, Math.min(4.0, v))
}
function resetDisplaySize() {
zoomFactor = 1.0
}
function refreshDisplayMetrics() {
currentDpr = (screen && screen.devicePixelRatio > 0) ? screen.devicePixelRatio : 1.0
if (sourcePixelW > 0 && sourcePixelH > 0) {
imageBaseW = Math.max(1, Math.round(sourcePixelW / currentDpr))
imageBaseH = Math.max(1, Math.round(sourcePixelH / currentDpr))
} else {
imageBaseW = Math.max(1, Math.abs(imgRect.width))
imageBaseH = Math.max(1, Math.abs(imgRect.height))
}
}
function updateImageMetricsFromStore() {
var img = O_StickyViewModel.getImageByUrl(stickyWin.imageUrl)
if (img && img.width > 0 && img.height > 0) {
sourcePixelW = img.width
sourcePixelH = img.height
refreshDisplayMetrics()
stickyPaintBoard.setBackgroundImg(img)
} else {
sourcePixelW = 0
sourcePixelH = 0
refreshDisplayMetrics()
syncPaintboardBackgroundFromDisplay()
}
}
function syncPaintboardBackgroundFromDisplay() {
if (!stickyWin.visible || !stickyWin.contentActive)
return
if (stickyImage.status !== Image.Ready)
return
stickyImage.grabToImage(function(result) {
if (!result || !result.image || result.image.width <= 0 || result.image.height <= 0)
return
stickyPaintBoard.setBackgroundImg(result.image)
}, Qt.size(Math.max(1, imageBaseW), Math.max(1, imageBaseH)))
}
function refreshAfterImageChanged() {
imageRevision += 1
stickyPaintBoard.reset()
updateImageMetricsFromStore()
}
function overwriteWithEdits() {
var layer = stickyPaintBoard.renderToImage()
var ok = O_StickyViewModel.overwriteWithAnnotations(stickyWin.imageUrl, layer)
if (ok)
refreshAfterImageChanged()
}
function rotateCurrent(degrees) {
if (O_StickyViewModel.rotateImage(stickyWin.imageUrl, degrees))
refreshAfterImageChanged()
}
function mirrorCurrent() {
if (O_StickyViewModel.mirrorImage(stickyWin.imageUrl, true, false))
refreshAfterImageChanged()
}
function saveMergedTo(pathUrl) {
var layer = stickyPaintBoard.renderToImage()
O_StickyViewModel.saveMergedImage(stickyWin.imageUrl, layer, pathUrl)
}
function completeEditsAndCopy() {
var layer = stickyPaintBoard.renderToImage()
if (layer && layer.width > 0 && layer.height > 0)
overwriteWithEdits()
O_StickyViewModel.copyImageToClipboard(stickyWin.imageUrl)
stickyTool.activeTool = ""
stickyWin.activeTool = ""
stickyWin.toolbarVisible = false
}
function cleanup() {
if (cleanedUp)
return
cleanedUp = true
stickyWin.visible = false
contentActive = false
stickyWin.releaseResources()
O_StickyViewModel.releaseImage(imageUrl)
imageUrl = ""
}
Component.onCompleted: {
refreshDisplayMetrics()
syncInitialPosition()
console.log("[StickyWindow] imgRect:", imgRect.x, imgRect.y, imgRect.width, imgRect.height)
console.log("[StickyWindow] imageTopLeft:", imageTopLeftX, imageTopLeftY)
console.log("[StickyWindow] windowPos:", x, y, "display:", displayW, displayH)
updateImageMetricsFromStore()
stickyWin.raise()
stickyWin.requestActivate()
}
Shortcut {
sequence: "Esc"
onActivated: {
if (stickyWin.activeTool !== "") {
stickyTool.activeTool = ""
stickyWin.activeTool = ""
return
}
if (stickyWin.toolbarVisible) {
stickyWin.toolbarVisible = false
return
}
stickyWin.close()
}
}
onImageUrlChanged: {
imageRevision = 0
resetDisplaySize()
sourcePixelW = 0
sourcePixelH = 0
refreshDisplayMetrics()
refreshAfterImageChanged()
}
onActiveToolChanged: {
if (activeTool === "mosaic")
updateImageMetricsFromStore()
}
onImgRectChanged: {
refreshDisplayMetrics()
syncInitialPosition()
}
onScreenChanged: refreshDisplayMetrics()
Component.onDestruction: cleanup()
onClosing: function(close) {
close.accepted = true
chatPanel.close()
cleanup()
Qt.callLater(function() { stickyWin.destroy() })
}
Rectangle {
id: imgContainer
visible: stickyWin.contentActive
z: 10
x: 0
y: 0
width: stickyWin.displayW
height: stickyWin.displayH
radius: 2
clip: true
color: "transparent"
Item {
id: visualStage
anchors.centerIn: parent
width: stickyWin.imageBaseW
height: stickyWin.imageBaseH
scale: stickyWin.zoomFactor
Image {
id: stickyImage
anchors.fill: parent
source: displaySourceUrl()
fillMode: Image.Stretch
opacity: stickyWin.imgOpacity
cache: false
onStatusChanged: {
if (status === Image.Ready && stickyWin.activeTool === "mosaic" && !stickyPaintBoard.hasBackgroundImg())
stickyWin.syncPaintboardBackgroundFromDisplay()
}
}
PaintBoard {
id: stickyPaintBoard
anchors.fill: parent
z: 20
visible: stickyWin.contentActive
enabled: stickyWin.activeTool !== ""
shapeType: stickyWin.stickyShapeType
penColor: stickyMarkTool.currentColor
penSize: stickyMarkTool.currentSize
mosaicBlurLevel: stickyMarkTool.mosaicBlurLevel
}
}
}
MultiEffect {
visible: stickyWin.contentActive
z: 5
source: stickyImage
anchors.fill: imgContainer
shadowEnabled: true
shadowColor: "#55000000"
shadowBlur: stickyWin.shadowRadius / 20.0
shadowVerticalOffset: stickyWin.shadowOffset
shadowHorizontalOffset: 0
opacity: stickyWin.imgOpacity
}
TZLoadingSpinner {
anchors.centerIn: imgContainer
width: stickyWin.displayW
height: stickyWin.displayH
visible: O_AIViewModel.isLoading && stickyWin.contentActive
}
TZMarkToolInfo {
id: stickyMarkTool
z: 32
visible: stickyWin.toolbarVisible && stickyWin.activeTool !== ""
toolName: stickyWin.activeTool
showColorRow: stickyWin.activeTool !== "mosaic"
x: Math.max(8, toolbarBlockX)
y: stickyTool.y - height - 6
}
TZTool {
id: stickyTool
z: 30
visible: stickyWin.toolbarVisible
width: implicitWidth
height: stickyWin.toolbarHeight + 4
x: stickyWin.toolbarBlockX
y: stickyWin.displayH + stickyWin.toolbarGap
showScreenshotButton: false
showCancelButton: false
showSaveButton: false
showStickyButton: false
showGifRecordButton: false
showLongScreenshotButton: false
showHighlightButton: false
showTextButton: false
showNumberButton: false
onActiveToolChanged: {
stickyWin.activeTool = activeTool
if (activeTool === "mosaic" && !stickyPaintBoard.hasBackgroundImg()) {
Qt.callLater(function() {
var retryImg = O_StickyViewModel.getImageByUrl(stickyWin.imageUrl)
if (retryImg && retryImg.width > 0 && retryImg.height > 0)
stickyPaintBoard.setBackgroundImg(retryImg)
if (!stickyPaintBoard.hasBackgroundImg())
stickyWin.syncPaintboardBackgroundFromDisplay()
})
}
}
onSignalOcr: {
var img = O_StickyViewModel.getImageByUrl(stickyWin.imageUrl)
O_OcrVM.recognize(img)
}
onSignalUndo: stickyPaintBoard.undo()
}
TZStickyExtraToolBar {
id: stickyExtraBar
z: 30
visible: stickyWin.toolbarVisible
height: stickyWin.toolbarHeight
x: stickyWin.toolbarBlockX + stickyTool.width + 6
y: stickyTool.y + 2
zoomFactor: stickyWin.zoomFactor
onZoomOut: setZoomValue(stickyWin.zoomFactor - 0.1)
onZoomIn: setZoomValue(stickyWin.zoomFactor + 0.1)
onRotate90: rotateCurrent(90)
onMirror: mirrorCurrent()
onResetSize: resetDisplaySize()
onDoneAndCopy: completeEditsAndCopy()
onHideToolbar: {
stickyTool.activeTool = ""
stickyWin.activeTool = ""
stickyWin.toolbarVisible = false
}
onHoverEntered: function(text, targetItem) { stickyWin.showToolbarTip(text, targetItem) }
onHoverExited: function(text) { stickyWin.hideToolbarTip(text) }
}
Rectangle {
id: stickyToolbarTip
parent: stickyWin.contentItem
visible: stickyWin.toolbarVisible && stickyWin.toolbarHoverTipText.length > 0
z: 40
width: Math.min(stickyWin.width - 8, Math.max(64, stickyToolbarTipText.implicitWidth + 16))
height: 24
radius: 6
color: "#0F172A"
opacity: 0.92
x: Math.max(4, Math.min(stickyWin.width - width - 4, stickyExtraBar.x + stickyWin.toolbarHoverTipCenterX - width / 2))
y: Math.min(stickyWin.height - height - 4, stickyExtraBar.y + stickyExtraBar.height + stickyWin.toolbarTipGap)
Text {
id: stickyToolbarTipText
anchors.centerIn: parent
text: stickyWin.toolbarHoverTipText
color: "#FFFFFF"
font.pixelSize: 11
}
}
readonly property int chatPanelGap: 6
TZChat {
id: chatPanel
imageUrl: stickyWin.imageUrl
onSignalSendPrompt: function(text) {
O_AIViewModel.sendPrompt(text, stickyWin.imageUrl)
}
}
Connections {
target: O_AIViewModel
function onSignalRequestComplete(oldImageUrl, newImageUrl) {
if (oldImageUrl !== stickyWin.imageUrl)
return
var old = oldImageUrl
stickyWin.imageUrl = newImageUrl
Qt.callLater(function() { O_StickyViewModel.releaseImage(old) })
}
function onSignalRequestFailed(errorMsg) {
console.warn("[TZStickyWindow] AI request failed: " + errorMsg)
}
}
function openChatPanel() {
var imgLeft = stickyWin.x
var imgTop = stickyWin.y
var imgBottom = imgTop + stickyWin.displayH
var screenH = Screen.desktopAvailableHeight
var screenW = Screen.desktopAvailableWidth
var cx = imgLeft
if (cx + chatPanel.win.width > screenW)
cx = imgLeft + stickyWin.displayW - chatPanel.win.width
var cy
if (imgBottom + chatPanelGap + chatPanel.win.height <= screenH)
cy = imgBottom + chatPanelGap
else
cy = imgTop - chatPanelGap - chatPanel.win.height
chatPanel.open(Math.max(0, cx), Math.max(0, cy))
}
Platform.Menu {
id: contextMenu
onAboutToShow: stickyWin.contextMenuOpen = true
onAboutToHide: stickyWin.contextMenuOpen = false
Platform.MenuItem { text: qsTr("当前透明度 %1%").arg(Math.round(stickyWin.imgOpacity * 100)); enabled: false }
Platform.MenuSeparator {}
Platform.MenuItem { visible: menuPrefs.pinSaveMerged; text: qsTr("覆盖贴图(合并标注)"); onTriggered: overwriteWithEdits() }
Platform.MenuItem { visible: menuPrefs.pinCopy; text: qsTr("复制贴图"); onTriggered: O_StickyViewModel.copyImageToClipboard(imageUrl) }
Platform.MenuItem { visible: menuPrefs.pinAi; text: qsTr("AI 编辑"); onTriggered: openChatPanel() }
Platform.MenuItem {
visible: menuPrefs.pinToolbar
text: stickyWin.toolbarVisible ? qsTr("隐藏工具栏") : qsTr("显示工具栏")
onTriggered: {
stickyWin.toolbarVisible = !stickyWin.toolbarVisible
if (!stickyWin.toolbarVisible) {
stickyTool.activeTool = ""
stickyWin.activeTool = ""
}
}
}
Platform.MenuItem { visible: menuPrefs.pinClose; text: qsTr("关闭窗口"); onTriggered: stickyWin.close() }
Platform.MenuSeparator {}
Platform.MenuItem { text: qsTr("透明度 +5%"); onTriggered: setOpacityValue(stickyWin.imgOpacity + 0.05) }
Platform.MenuItem { text: qsTr("透明度 -5%"); onTriggered: setOpacityValue(stickyWin.imgOpacity - 0.05) }
Platform.MenuSeparator {}
Platform.MenuItem { text: qsTr("缩小"); onTriggered: setZoomValue(stickyWin.zoomFactor - 0.1) }
Platform.MenuItem { text: qsTr("放大"); onTriggered: setZoomValue(stickyWin.zoomFactor + 0.1) }
Platform.MenuItem { text: qsTr("旋转 90°"); onTriggered: rotateCurrent(90) }
Platform.MenuItem { text: qsTr("镜像"); onTriggered: mirrorCurrent() }
Platform.MenuItem { text: qsTr("恢复原始大小"); onTriggered: resetDisplaySize() }
Platform.MenuSeparator {}
Platform.MenuItem {
text: qsTr("重置位置")
onTriggered: {
syncInitialPosition()
}
}
Platform.MenuSeparator {}
Platform.MenuItem {
text: stickyWin.toolbarVisible ? qsTr("隐藏工具栏") : qsTr("显示工具栏")
onTriggered: {
stickyWin.toolbarVisible = !stickyWin.toolbarVisible
if (!stickyWin.toolbarVisible) {
stickyTool.activeTool = ""
stickyWin.activeTool = ""
}
}
}
Platform.MenuItem { text: qsTr("AI 编辑"); onTriggered: openChatPanel() }
Platform.MenuSeparator {}
Platform.MenuItem { text: qsTr("保存原图"); onTriggered: saveDialog.open() }
Platform.MenuItem { text: qsTr("另存为(合并标注)"); onTriggered: mergedSaveDialog.open() }
Platform.MenuItem { text: qsTr("覆盖贴图(合并标注)"); onTriggered: overwriteWithEdits() }
Platform.MenuItem { text: qsTr("复制贴图"); onTriggered: O_StickyViewModel.copyImageToClipboard(imageUrl) }
Platform.MenuSeparator {}
Platform.Menu {
title: qsTr("常用项置顶")
Platform.MenuItem { text: qsTr("覆盖贴图"); checkable: true; checked: menuPrefs.pinSaveMerged; onTriggered: menuPrefs.pinSaveMerged = !menuPrefs.pinSaveMerged }
Platform.MenuItem { text: qsTr("复制贴图"); checkable: true; checked: menuPrefs.pinCopy; onTriggered: menuPrefs.pinCopy = !menuPrefs.pinCopy }
Platform.MenuItem { text: qsTr("AI 编辑"); checkable: true; checked: menuPrefs.pinAi; onTriggered: menuPrefs.pinAi = !menuPrefs.pinAi }
Platform.MenuItem { text: qsTr("显示/隐藏工具栏"); checkable: true; checked: menuPrefs.pinToolbar; onTriggered: menuPrefs.pinToolbar = !menuPrefs.pinToolbar }
Platform.MenuItem { text: qsTr("关闭窗口"); checkable: true; checked: menuPrefs.pinClose; onTriggered: menuPrefs.pinClose = !menuPrefs.pinClose }
}
Platform.MenuSeparator {}
Platform.MenuItem { text: qsTr("关闭窗口"); onTriggered: stickyWin.close() }
}
Platform.FileDialog {
id: saveDialog
title: qsTr("选择保存位置")
folder: O_ImageSaver.savePath
fileMode: Platform.FileDialog.SaveFile
nameFilters: ["PNG (*.png)", "JPG (*.jpg)", "BMP (*.bmp)"]
onAccepted: O_StickyViewModel.saveImage(imageUrl, saveDialog.file)
}
Platform.FileDialog {
id: mergedSaveDialog
title: qsTr("另存为(合并标注)")
folder: O_ImageSaver.savePath
fileMode: Platform.FileDialog.SaveFile
nameFilters: ["PNG (*.png)", "JPG (*.jpg)", "BMP (*.bmp)"]
onAccepted: saveMergedTo(mergedSaveDialog.file)
}
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.LeftButton | Qt.RightButton
enabled: stickyWin.activeTool === "" && !stickyWin.contextMenuOpen
property point dragPos: Qt.point(0, 0)
property bool leftDragActive: false
z: 0
function inImageArea(mx, my) {
return mx >= 0
&& mx <= stickyWin.displayW
&& my >= 0
&& my <= stickyWin.displayH
}
onPressed: function(mouse) {
if (mouse.button === Qt.LeftButton) {
dragPos = Qt.point(mouseX, mouseY)
leftDragActive = true
} else {
leftDragActive = false
}
}
onPositionChanged: function(mouse) {
//console.log("mouse.x",mouseX)
if (pressed && leftDragActive && (mouse.buttons & Qt.LeftButton) && stickyWin.activeTool === "" && !stickyWin.contextMenuOpen) {
stickyWin.x += mouseX - dragPos.x
stickyWin.y += mouseY - dragPos.y
}
}
onReleased: function(mouse) {
if (!(mouse.buttons & Qt.LeftButton))
leftDragActive = false
}
onDoubleClicked: function(mouse) {
if (mouse.button === Qt.LeftButton && inImageArea(mouseX, mouseY))
resetDisplaySize()
}
onClicked: function(mouse) {
if (mouse.button === Qt.RightButton) {
leftDragActive = false
contextMenu.open()
}
}
onWheel: function(wheel) {
if (wheel.angleDelta.y > 0)
setZoomValue(stickyWin.zoomFactor + 0.1)
else if (wheel.angleDelta.y < 0)
setZoomValue(stickyWin.zoomFactor - 0.1)
wheel.accepted = true
}
}
}
+22 -19
View File
@@ -1,8 +1,9 @@
import QtQuick
import QtQuick
import QtQuick.Window
import QtQuick.Shapes
import QtQuick.Layouts
import com.Tz.tray 1.0
import "Capture"
TZWindow {
id: root
@@ -31,6 +32,7 @@ TZWindow {
paintBoard.reset()
tool.visible = false
tool.activeTool = ""
tool.markToolInfoVisible = false
@@ -59,22 +61,12 @@ TZWindow {
Qt.callLater(function() {
O_ScreenCapture.captureDesktop()
var screens = Qt.application.screens
if (screens.length === 0)
var virtualRect = O_ScreenCapture.virtualGeometry()
if (virtualRect.width <= 0 || virtualRect.height <= 0)
return
// Single-screen mode: always use the first screen as capture target.
var primary = screens[0]
var vx = (primary.virtualX !== undefined) ? primary.virtualX : 0
var vy = (primary.virtualY !== undefined) ? primary.virtualY : 0
root.screenVirtualX = vx
root.screenVirtualY = vy
root.x = vx
root.y = vy
root.width = primary.width
root.height = primary.height
root.screenVirtualX = virtualRect.x
root.screenVirtualY = virtualRect.y
O_ScreenCapture.grabToPaintBoard(
root.paintBoard,
@@ -100,7 +92,7 @@ TZWindow {
return
}
if (longCaptureBarComponent === null)
longCaptureBarComponent = Qt.createComponent("qrc:/qml/TZLongCaptureBar.qml")
longCaptureBarComponent = Qt.createComponent("qrc:/qml/LongCapture/TZLongCaptureBar.qml")
if (longCaptureBarComponent.status === Component.Ready) {
longCaptureBarObj = longCaptureBarComponent.createObject(null)
if (longCaptureBarObj) {
@@ -136,7 +128,7 @@ TZWindow {
return
}
if (longCapturePreviewComponent === null)
longCapturePreviewComponent = Qt.createComponent("qrc:/qml/TZLongCapturePreview.qml")
longCapturePreviewComponent = Qt.createComponent("qrc:/qml/LongCapture/TZLongCapturePreview.qml")
if (longCapturePreviewComponent.status === Component.Ready) {
longCapturePreviewObj = longCapturePreviewComponent.createObject(null)
if (longCapturePreviewObj) {
@@ -152,7 +144,7 @@ TZWindow {
function showLongCaptureFrame(captureRect) {
if (longCaptureFrameComponent === null)
longCaptureFrameComponent = Qt.createComponent("qrc:/qml/TZLongCaptureFrame.qml")
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) {
@@ -343,7 +335,18 @@ TZWindow {
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()
+55 -56
View File
@@ -1,4 +1,5 @@
#include "magnifier_provider.h"
#include "magnifier_provider.h"
#include <QPainter>
MagnifierProvider::MagnifierProvider(ScreenshotViewModel *viewModel)
@@ -7,87 +8,85 @@ MagnifierProvider::MagnifierProvider(ScreenshotViewModel *viewModel)
{
}
// requestImage
// ------------
// id 格式:"<x>,<y>"(由 QML 传入的逻辑像素鼠标坐标)
QImage MagnifierProvider::requestImage(const QString &id, QSize *size, const QSize & /*requestedSize*/)
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();
if (size) {
*size = blank.size();
}
return blank;
}
bool okX = false;
bool okY = false;
const int cx = parts[0].toInt(&okX);
const int cy = parts[1].toInt(&okY);
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();
if (size) {
*size = blank.size();
}
return blank;
}
// 从桌面快照中取一份线程安全的副本
const QImage snapshot = m_viewModel->desktopSnapshot();
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();
if (size) {
*size = blank.size();
}
return blank;
}
// 逻辑坐标 → 快照像素坐标(按虚拟桌面逻辑尺寸与快照物理像素比例换算,支持多屏)
const QRect vg = m_viewModel->virtualGeometry();
const qreal scaleX = (vg.width() > 0) ? (qreal(snapshot.width()) / qreal(vg.width())) : 1.0;
const qreal scaleY = (vg.height() > 0) ? (qreal(snapshot.height()) / qreal(vg.height())) : 1.0;
const int pcx = qRound((cx - vg.x()) * scaleX);
const int pcy = qRound((cy - vg.y()) * scaleY);
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);
// 采样半径按 X 轴比例缩放;Y 轴保持一致比例,保证中心点准确
const int srcRadiusX = qMax(1, qRound(kSrcRadius * scaleX));
const int srcRadiusY = qMax(1, qRound(kSrcRadius * scaleY));
const int srcDiamW = srcRadiusX * 2;
const int srcDiamH = srcRadiusY * 2;
// 计算裁剪矩形(以物理像素光标为中心)
QRect srcRect(pcx - srcRadiusX, pcy - srcRadiusY, srcDiamW, srcDiamH);
// 处理边界:当光标靠近屏幕边缘时,用黑色填充越界区域
QImage region(srcDiamW, srcDiamH, QImage::Format_ARGB32_Premultiplied);
region.fill(Qt::black);
{
QPainter rp(&region);
QRect bounded = srcRect.intersected(snapshot.rect());
if (!bounded.isEmpty()) {
int destX = bounded.x() - srcRect.x();
int destY = bounded.y() - srcRect.y();
rp.drawImage(destX, destY, snapshot, bounded.x(), bounded.y(),
bounded.width(), bounded.height());
}
rp.end();
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();
// 放大到 kOutputSize×kOutputSize
QImage zoomed = region.scaled(kOutputSize, kOutputSize, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
QImage zoomed = region.scaled(kOutputSize,
kOutputSize,
Qt::IgnoreAspectRatio,
Qt::SmoothTransformation);
// 绘制红色十字准线(1px 宽)
{
QPainter p(&zoomed);
p.setRenderHint(QPainter::Antialiasing, false);
QPen pen(QColor(255, 0, 0, 220), 1);
p.setPen(pen);
const int half = kOutputSize / 2;
p.drawLine(half, 0, half, kOutputSize - 1); // 竖线
p.drawLine(0, half, kOutputSize - 1, half); // 横线
p.end();
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();
}
if (size) *size = zoomed.size();
return zoomed;
}
+13 -2
View File
@@ -1,6 +1,8 @@
#include <QApplication>
#include <QApplication>
#include <QQmlApplicationEngine>
#include <QQmlContext>
#include <QQuickWindow>
#include <QScreen>
#include "tray_icon_helper.h"
#include "paint_board/paint_board.h"
@@ -77,7 +79,7 @@ int main(int argc, char *argv[])
// ── 暴露给 QML 的 ViewModel ───────────────────────────
engine.rootContext()->setContextProperty("O_ScreenCapture", &screenshotViewModel);
engine.rootContext()->setContextProperty("O_StickyViewModel", &stickyViewModel);
engine.rootContext()->setContextProperty("O_StickyViewModel", &stickyViewModel);
engine.rootContext()->setContextProperty("O_AIViewModel", &aiViewModel);
engine.rootContext()->setContextProperty("O_ImageSaver", &storageViewModel);
engine.rootContext()->setContextProperty("O_GlobalShortcut", &globalShortcut);
@@ -94,5 +96,14 @@ int main(int argc, char *argv[])
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();
}
+224 -121
View File
@@ -1,134 +1,243 @@
#include "desktop_snapshot.h"
#include "desktop_snapshot.h"
#include <QGuiApplication>
#include <QScreen>
#include <QPixmap>
#include <QPainter>
#include <QTimer>
#include <QEventLoop>
#include <QDebug>
#include <QGuiApplication>
#include <QDir>
#include <QImage>
#include <QPainter>
#include <QPixmap>
#include <QScreen>
/* ── grab / release / pixelColor / windowAtPoint ────────
* 这几个函数与平台无关,统一实现
* ────────────────────────────────────────────────────── */
#ifdef Q_OS_WIN
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#endif
namespace {
QRect desktopGeometry()
{
#ifdef Q_OS_WIN
return QRect(GetSystemMetrics(SM_XVIRTUALSCREEN),
GetSystemMetrics(SM_YVIRTUALSCREEN),
GetSystemMetrics(SM_CXVIRTUALSCREEN),
GetSystemMetrics(SM_CYVIRTUALSCREEN));
#else
QRect geometry;
for (QScreen* const screen : QGuiApplication::screens()) {
QRect scrRect = screen->geometry();
// https://doc.qt.io/qt-6/highdpi.html#device-independent-screen-geometry
qreal dpr = screen->devicePixelRatio();
scrRect.moveTo(QPointF(scrRect.x() / dpr, scrRect.y() / dpr).toPoint());
geometry = geometry.united(scrRect);
}
return geometry;
#endif
}
#ifdef Q_OS_WIN
QImage captureVirtualDesktopImage(const QRect& geometry)
{
HDC screenDC = GetDC(nullptr);
HDC memoryDC = CreateCompatibleDC(screenDC);
HBITMAP bitmap = CreateCompatibleBitmap(screenDC, geometry.width(), geometry.height());
DeleteObject(SelectObject(memoryDC, bitmap));
BitBlt(memoryDC,
0,
0,
geometry.width(),
geometry.height(),
screenDC,
geometry.x(),
geometry.y(),
SRCCOPY);
QImage image(geometry.width(), geometry.height(), QImage::Format_ARGB32_Premultiplied);
BITMAPINFO bmi = { sizeof(BITMAPINFOHEADER),
static_cast<LONG>(geometry.width()),
-static_cast<LONG>(geometry.height()),
1,
32,
BI_RGB,
static_cast<DWORD>(geometry.width() * 4 * geometry.height()),
0,
0,
0,
0 };
GetDIBits(memoryDC,
bitmap,
0,
geometry.height(),
image.bits(),
&bmi,
DIB_RGB_COLORS);
DeleteDC(memoryDC);
DeleteObject(bitmap);
ReleaseDC(nullptr, screenDC);
return image;
}
#endif
}
void DesktopSnapshot::grab()
{
QScreen *primary = QGuiApplication::primaryScreen();
if (!primary) {
qWarning() << "[DesktopSnapshot] 无法获取主屏";
const QRect geometry = desktopGeometry();
if (geometry.isEmpty()) {
release();
return;
}
// 等待合成器/DWM 将截图窗口从帧缓冲移除
QEventLoop loop;
QTimer::singleShot(50, &loop, &QEventLoop::quit);
loop.exec();
#ifdef Q_OS_WIN
const QImage image = captureVirtualDesktopImage(geometry);
if (image.isNull()) {
release();
return;
}
m_snapshot = QPixmap::fromImage(image);
#else
const QList<QScreen*> screens = QGuiApplication::screens();
const int minLogicalX = geometry.x();
const int minLogicalY = geometry.y();
// 单屏模式:仅使用主屏坐标系。
m_virtualGeometry = primary->geometry();
qDebug() << "[DesktopSnapshot] 单屏模式,主屏逻辑矩形:" << m_virtualGeometry;
QPixmap desktop(geometry.size());
desktop.fill(Qt::black);
// 创建与虚拟桌面等大的物理像素画布(以主屏 DPR 为基准)
// 主屏 DPR 即虚拟桌面坐标系的缩放基准,与 QML 侧一致
const qreal baseDpr = primary->devicePixelRatio();
const int canvasW = qRound(m_virtualGeometry.width() * baseDpr);
const int canvasH = qRound(m_virtualGeometry.height() * baseDpr);
m_snapshot = QImage(canvasW, canvasH, QImage::Format_ARGB32_Premultiplied);
m_snapshot.fill(Qt::black);
// 仅抓取主屏
QPainter painter(&m_snapshot);
QImage img = primary->grabWindow(0).toImage();
if (img.width() != canvasW || img.height() != canvasH)
img = img.scaled(canvasW, canvasH, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
painter.drawImage(0, 0, img);
QPainter painter(&desktop);
painter.setCompositionMode(QPainter::CompositionMode_Source);
for (QScreen* screen : screens) {
if (!screen) {
continue;
}
const QRect screenGeom = screen->geometry();
painter.drawPixmap(QRect(screenGeom.x() - minLogicalX,
screenGeom.y() - minLogicalY,
screenGeom.width(),
screenGeom.height()),
screen->grabWindow(0));
}
painter.end();
m_snapshot = desktop;
#endif
qDebug() << "[DesktopSnapshot] 快照已缓存,尺寸:"
<< m_snapshot.width() << "x" << m_snapshot.height();
m_virtualGeometry = geometry;
buildWindowList();
}
void DesktopSnapshot::release()
{
m_snapshot = QImage();
m_snapshot = QPixmap();
m_virtualGeometry = QRect();
m_windowRects.clear();
}
QPoint DesktopSnapshot::mapLogicalPointToPhysical(const QPoint &logicalPoint) const
{
if (m_virtualGeometry.isEmpty()) {
return {};
}
return logicalPoint - m_virtualGeometry.topLeft();
}
QRect DesktopSnapshot::mapLogicalRectToPhysical(const QRect &logicalRect) const
{
const QRect normalized = logicalRect.normalized();
if (normalized.isEmpty() || m_virtualGeometry.isEmpty()) {
return {};
}
QRect translated = normalized.translated(-m_virtualGeometry.topLeft());
return translated.intersected(m_snapshot.rect());
}
QColor DesktopSnapshot::pixelColor(int logicalX, int logicalY) const
{
if (m_snapshot.isNull() || m_virtualGeometry.isEmpty()) return QColor();
#ifdef Q_OS_WIN
HDC screenDC = GetDC(nullptr);
if (!screenDC) {
return {};
}
const COLORREF color = GetPixel(screenDC, logicalX, logicalY);
ReleaseDC(nullptr, screenDC);
if (color == CLR_INVALID) {
return {};
}
return QColor(GetRValue(color), GetGValue(color), GetBValue(color));
#else
if (m_snapshot.isNull() || m_virtualGeometry.isEmpty()) {
return {};
}
// 逻辑坐标相对于虚拟桌面原点的偏移
const int relX = logicalX - m_virtualGeometry.x();
const int relY = logicalY - m_virtualGeometry.y();
// 按快照实际尺寸与虚拟桌面逻辑尺寸比例换算(统一 DPR)
const qreal scaleX = m_virtualGeometry.width() > 0
? qreal(m_snapshot.width()) / qreal(m_virtualGeometry.width()) : 1.0;
const qreal scaleY = m_virtualGeometry.height() > 0
? qreal(m_snapshot.height()) / qreal(m_virtualGeometry.height()) : 1.0;
const int px = qRound(relX * scaleX);
const int py = qRound(relY * scaleY);
if (px < 0 || py < 0 || px >= m_snapshot.width() || py >= m_snapshot.height())
return QColor();
return QColor(m_snapshot.pixel(px, py));
const QPoint pixelPoint = mapLogicalPointToPhysical(QPoint(logicalX, logicalY));
if (!m_snapshot.rect().contains(pixelPoint)) {
return {};
}
const QImage image = m_snapshot.toImage();
return QColor::fromRgba(image.pixel(pixelPoint));
#endif
}
QRect DesktopSnapshot::windowAtPoint(int x, int y) const
{
for (const QRect &r : m_windowRects) {
if (r.contains(x, y))
if (r.contains(x, y)) {
return r;
}
}
return QRect();
return {};
}
/* ══════════════════════════════════════════════════════════
* buildWindowList — 平台专属实现
* ══════════════════════════════════════════════════════════ */
/* ────────────────────────────────────────────────────────
* Linux / Unix:通过 _NET_CLIENT_LIST_STACKING 枚举窗口
* ──────────────────────────────────────────────────────── */
#ifdef Q_OS_UNIX
#include <X11/Xlib.h>
#include <X11/Xatom.h>
#include <X11/Xlib.h>
void DesktopSnapshot::buildWindowList()
{
m_windowRects.clear();
Display *dpy = XOpenDisplay(nullptr);
if (!dpy) return;
if (!dpy) {
return;
}
Window root = DefaultRootWindow(dpy);
Atom netClientListStacking = XInternAtom(dpy, "_NET_CLIENT_LIST_STACKING", False);
Atom actualType;
int actualFormat;
unsigned long nitems = 0, bytesAfter = 0;
unsigned char *data = nullptr;
int actualFormat;
unsigned long nitems = 0;
unsigned long bytesAfter = 0;
unsigned char *data = nullptr;
int ret = XGetWindowProperty(dpy, root, netClientListStacking,
0, 65536, False, XA_WINDOW,
&actualType, &actualFormat,
&nitems, &bytesAfter, &data);
int ret = XGetWindowProperty(dpy,
root,
netClientListStacking,
0,
65536,
False,
XA_WINDOW,
&actualType,
&actualFormat,
&nitems,
&bytesAfter,
&data);
if (ret != Success || !data) {
Atom netClientList = XInternAtom(dpy, "_NET_CLIENT_LIST", False);
ret = XGetWindowProperty(dpy, root, netClientList,
0, 65536, False, XA_WINDOW,
&actualType, &actualFormat,
&nitems, &bytesAfter, &data);
ret = XGetWindowProperty(dpy,
root,
netClientList,
0,
65536,
False,
XA_WINDOW,
&actualType,
&actualFormat,
&nitems,
&bytesAfter,
&data);
if (ret != Success || !data) {
XCloseDisplay(dpy);
return;
@@ -136,92 +245,89 @@ void DesktopSnapshot::buildWindowList()
}
Window *wins = reinterpret_cast<Window*>(data);
for (long i = static_cast<long>(nitems) - 1; i >= 0; --i) {
Window w = wins[i];
XWindowAttributes attr;
if (!XGetWindowAttributes(dpy, w, &attr)) continue;
if (attr.map_state != IsViewable) continue;
if (attr.width < 10) continue;
if (attr.height < 10) continue;
if (!XGetWindowAttributes(dpy, w, &attr) || attr.map_state != IsViewable ||
attr.width < 10 || attr.height < 10) {
continue;
}
int screenX = 0, screenY = 0;
int screenX = 0;
int screenY = 0;
Window child;
XTranslateCoordinates(dpy, w, root, 0, 0, &screenX, &screenY, &child);
QRect r(screenX, screenY, attr.width, attr.height);
r = r.intersected(m_virtualGeometry);
if (r.isEmpty()) continue;
m_windowRects.append(r);
if (!r.isEmpty()) {
m_windowRects.append(r);
}
}
XFree(data);
XCloseDisplay(dpy);
qDebug() << "[DesktopSnapshot] 枚举到顶层窗口数:" << m_windowRects.size();
}
#endif /* Q_OS_UNIX */
#endif
/* ────────────────────────────────────────────────────────
* Windows:通过 EnumWindows 枚举可见顶层窗口
* ──────────────────────────────────────────────────────── */
#ifdef Q_OS_WIN
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
/* EnumWindows 回调:收集所有可见且有标题栏的顶层窗口矩形 */
struct EnumWindowsParam {
QVector<QRect> *rects;
QRect snapshotRect; /* 逻辑像素,用于裁剪 */
QRect snapshotRect;
};
static qreal hwndScaleFactor(HWND hwnd)
{
using GetDpiForWindowFn = UINT (WINAPI *)(HWND);
static GetDpiForWindowFn fn =
reinterpret_cast<GetDpiForWindowFn>(
GetProcAddress(GetModuleHandleW(L"user32.dll"), "GetDpiForWindow"));
using GetDpiForWindowFn = UINT(WINAPI *)(HWND);
static GetDpiForWindowFn fn = reinterpret_cast<GetDpiForWindowFn>(
GetProcAddress(GetModuleHandleW(L"user32.dll"), "GetDpiForWindow"));
if (fn) {
const UINT dpi = fn(hwnd);
if (dpi > 0)
if (dpi > 0) {
return qreal(dpi) / 96.0;
}
}
return QGuiApplication::primaryScreen()
? QGuiApplication::primaryScreen()->devicePixelRatio()
: 1.0;
? QGuiApplication::primaryScreen()->devicePixelRatio()
: 1.0;
}
static BOOL CALLBACK enumWindowsProc(HWND hwnd, LPARAM lParam)
{
EnumWindowsParam *param = reinterpret_cast<EnumWindowsParam*>(lParam);
if (!IsWindowVisible(hwnd)) return TRUE;
if (IsIconic(hwnd)) return TRUE; /* 最小化,跳过 */
if (!IsWindowVisible(hwnd) || IsIconic(hwnd)) {
return TRUE;
}
/* 跳过没有 WS_CAPTION 的工具窗口、桌面壳层等 */
LONG style = GetWindowLong(hwnd, GWL_STYLE);
if (!(style & WS_CAPTION)) return TRUE;
if (!(style & WS_CAPTION)) {
return TRUE;
}
RECT rc;
if (!GetWindowRect(hwnd, &rc)) return TRUE;
if (!GetWindowRect(hwnd, &rc)) {
return TRUE;
}
int w = rc.right - rc.left;
int h = rc.bottom - rc.top;
if (w < 10 || h < 10) return TRUE;
const int w = rc.right - rc.left;
const int h = rc.bottom - rc.top;
if (w < 10 || h < 10) {
return TRUE;
}
/* GetWindowRect 返回物理像素坐标,按窗口自身 DPI 换算逻辑坐标 */
const qreal scale = hwndScaleFactor(hwnd);
QRect r(qRound(rc.left / scale),
qRound(rc.top / scale),
qRound(w / scale),
qRound(h / scale));
r = r.intersected(param->snapshotRect);
if (!r.isEmpty())
param->rects->append(r); /* EnumWindows 本身按 Z 序(顶->底)枚举,append 才能保持从顶到底 */
if (!r.isEmpty()) {
param->rects->append(r);
}
return TRUE;
}
@@ -229,11 +335,8 @@ static BOOL CALLBACK enumWindowsProc(HWND hwnd, LPARAM lParam)
void DesktopSnapshot::buildWindowList()
{
m_windowRects.clear();
EnumWindowsParam param { &m_windowRects, m_virtualGeometry };
EnumWindows(enumWindowsProc, reinterpret_cast<LPARAM>(&param));
qDebug() << "[DesktopSnapshot] 枚举到顶层窗口数:" << m_windowRects.size();
}
#endif /* Q_OS_WIN */
#endif
+18 -21
View File
@@ -1,46 +1,43 @@
#ifndef DESKTOP_SNAPSHOT_H
#define DESKTOP_SNAPSHOT_H
#include <QImage>
#include <QColor>
#include <QPoint>
#include <QPixmap>
#include <QRect>
#include <QVector>
#include <QColor>
// DesktopSnapshot — Model
// 负责全屏快照的采集与存储,以及顶层窗口列表的枚举。
// 支持多屏:将所有屏幕的快照拼合为一张以虚拟桌面坐标系为基准的大图。
// 不依赖 QML,不继承 QObject,纯数据/系统调用。
class DesktopSnapshot
{
public:
DesktopSnapshot() = default;
// 抓取所有屏幕快照并拼合,枚举顶层窗口列表(含 50ms 延时等待合成器)
void grab();
// 释放快照和窗口列表
void release();
bool isNull() const { return m_snapshot.isNull(); }
// 拼合后的虚拟桌面快照(物理像素坐标系)
const QImage& image() const { return m_snapshot; }
// 虚拟桌面的逻辑坐标矩形(所有屏幕的 geometry 联合,逻辑像素)
const QPixmap& image() const { return m_snapshot; }
QRect virtualGeometry() const { return m_virtualGeometry; }
// 将逻辑坐标 (x,y) 转换为快照中的颜色(支持多屏 DPI 混合)
QColor pixelColor(int logicalX, int logicalY) const;
// 返回包含逻辑坐标点 (x,y) 的最上层窗口矩形(逻辑坐标)
QPoint mapLogicalPointToPhysical(const QPoint &logicalPoint) const;
QRect mapLogicalRectToPhysical(const QRect &logicalRect) const;
QRect windowAtPoint(int x, int y) const;
private:
void buildWindowList();
struct ScreenEntry {
QRect logicalGeometry;
QRect physicalRect;
qreal dpr = 1.0;
};
QImage m_snapshot;
QRect m_virtualGeometry; // 虚拟桌面逻辑矩形(所有屏幕 geometry 的联合)
QVector<QRect> m_windowRects; // 按 Z 轴从顶到底排列(逻辑坐标)
void buildWindowList();
const ScreenEntry* screenEntryForPoint(const QPoint &logicalPoint) const;
QPixmap m_snapshot;
QRect m_virtualGeometry;
QVector<QRect> m_windowRects;
QVector<ScreenEntry> m_screenEntries;
};
#endif // DESKTOP_SNAPSHOT_H
+118 -86
View File
@@ -1,13 +1,51 @@
#include "screenshot_view_model.h"
#include "screenshot_view_model.h"
#include "paint_board/paint_board.h"
#include <QBuffer>
#include <QClipboard>
#include <QDebug>
#include <QGuiApplication>
#include <QPainter>
#include <QScreen>
#include <QDebug>
#include <QQuickWindow>
#include <QScreen>
#include <QWindow>
namespace {
qreal captureDprForRect(const QRect& rect)
{
#ifdef Q_OS_WIN
if (QScreen* screen = QGuiApplication::screenAt(rect.center())) {
return screen->devicePixelRatio();
}
#endif
if (QScreen* screen = QGuiApplication::primaryScreen()) {
return screen->devicePixelRatio();
}
return 1.0;
}
QRect captureRectInSnapshot(const DesktopSnapshot& snapshot, const QRect& rect, qreal dpr)
{
if (snapshot.isNull()) {
return {};
}
if (rect.isEmpty()) {
return snapshot.image().rect();
}
const QRect localRect = rect.translated(-snapshot.virtualGeometry().topLeft());
const QPoint topLeft = QPoint(qRound(localRect.left() * dpr),
qRound(localRect.top() * dpr));
const QPoint bottomRight = QPoint(qRound(localRect.right() * dpr),
qRound(localRect.bottom() * dpr));
QRect target;
target.setCoords(topLeft.x(), topLeft.y(), bottomRight.x(), bottomRight.y());
return target.intersected(snapshot.image().rect());
}
}
ScreenshotViewModel::ScreenshotViewModel(DesktopSnapshot &snapshot,
StickyImageStore &stickyStore,
@@ -18,118 +56,120 @@ ScreenshotViewModel::ScreenshotViewModel(DesktopSnapshot &snapshot,
{
}
// ── captureDesktop ────────────────────────────────────────
QRect ScreenshotViewModel::captureRectForDisplay(const QRect &rect) const
{
const QRect r = rect.normalized();
if (r.isEmpty() || m_snapshot.isNull()) {
return {};
}
return captureRectInSnapshot(m_snapshot, r, captureDprForRect(r));
}
void ScreenshotViewModel::captureDesktop()
{
m_snapshot.grab();
}
// ── releaseDesktopSnapshot ────────────────────────────────
void ScreenshotViewModel::releaseDesktopSnapshot()
{
m_snapshot.release();
}
// ── getPixelColor ─────────────────────────────────────────
QColor ScreenshotViewModel::getPixelColor(int x, int y) const
{
return m_snapshot.pixelColor(x, y);
}
// ── windowAtPoint ─────────────────────────────────────────
QRect ScreenshotViewModel::windowAtPoint(int x, int y) const
{
return m_snapshot.windowAtPoint(x, y);
}
// ── captureRectToClipboard ────────────────────────────────
bool ScreenshotViewModel::captureRectToClipboard(QQuickItem *paintBoard, const QRect &rect)
{
const QRect r = rect.normalized();
if (r.isEmpty()) {
qWarning() << "[ScreenshotViewModel] 截图区域为空";
qWarning() << "[ScreenshotViewModel] capture area is empty";
return false;
}
QImage img = captureScreen(paintBoard, r);
if (img.isNull()) {
qWarning() << "[ScreenshotViewModel] 截图失败,捕获到空图片";
qWarning() << "[ScreenshotViewModel] failed to capture image";
return false;
}
return writeImageToClipboard(img);
}
// ── captureRectToBase64 ───────────────────────────────────
QString ScreenshotViewModel::captureRectToBase64(QQuickItem *paintBoard, const QRect &rect)
{
const QRect r = rect.normalized();
if (r.isEmpty()) {
qWarning() << "[ScreenshotViewModel] 截图区域为空";
qWarning() << "[ScreenshotViewModel] capture area is empty";
return {};
}
QImage img = captureScreen(paintBoard, r);
if (img.isNull()) {
qWarning() << "[ScreenshotViewModel] 截图失败,捕获到空图片";
qWarning() << "[ScreenshotViewModel] failed to capture image";
return {};
}
QByteArray data;
QBuffer buf(&data);
QBuffer buf(&data);
buf.open(QIODevice::WriteOnly);
if (!img.save(&buf, "PNG")) {
qWarning() << "[ScreenshotViewModel] PNG 编码失败";
qWarning() << "[ScreenshotViewModel] failed to encode PNG";
return {};
}
return data.toBase64();
}
// ── captureRectToStickyUrl ────────────────────────────────
QString ScreenshotViewModel::captureRectToStickyUrl(QQuickItem *paintBoard, const QRect &rect)
{
const QRect r = rect.normalized();
if (r.isEmpty()) {
qWarning() << "[ScreenshotViewModel] 贴图区域为空";
qWarning() << "[ScreenshotViewModel] sticky area is empty";
return {};
}
QImage img = captureScreen(paintBoard, r);
if (img.isNull()) {
qWarning() << "[ScreenshotViewModel] 贴图截图失败";
qWarning() << "[ScreenshotViewModel] failed to capture sticky image";
return {};
}
return m_stickyStore.storeImage(img);
}
// ── grabToPaintBoard ──────────────────────────────────────
void ScreenshotViewModel::grabToPaintBoard(QQuickItem *paintBoard, const QRect &rect)
{
const QRect r = rect.normalized();
if (r.isEmpty()) return;
captureScreen(paintBoard, r, /*setBackground=*/true);
if (r.isEmpty()) {
return;
}
captureScreen(paintBoard, r, true);
}
// ── captureRectToImage ────────────────────────────────────
QImage ScreenshotViewModel::captureRectToImage(const QRect &rect)
{
const QRect r = rect.normalized();
if (r.isEmpty()) {
qWarning() << "[ScreenshotViewModel] OCR 区域为空";
qWarning() << "[ScreenshotViewModel] OCR area is empty";
return {};
}
if (!m_snapshot.isNull()) {
const QRect vg = m_snapshot.virtualGeometry();
const qreal scaleX = vg.width() > 0 ? qreal(m_snapshot.image().width()) / qreal(vg.width()) : 1.0;
const qreal scaleY = vg.height() > 0 ? qreal(m_snapshot.image().height()) / qreal(vg.height()) : 1.0;
const QRect physRect = QRect(qRound((r.x() - vg.x()) * scaleX),
qRound((r.y() - vg.y()) * scaleY),
qRound(r.width() * scaleX),
qRound(r.height() * scaleY))
.intersected(m_snapshot.image().rect());
return m_snapshot.image().copy(physRect);
const QRect captureRect = captureRectInSnapshot(m_snapshot, r, captureDprForRect(r));
return m_snapshot.image().copy(captureRect).toImage();
}
qWarning() << "[ScreenshotViewModel] 快照未缓存,OCR 回退到实时抓屏(仅主屏)";
qWarning() << "[ScreenshotViewModel] snapshot unavailable, falling back to primary screen";
QScreen *screen = QGuiApplication::primaryScreen();
if (!screen) return {};
if (!screen) {
return {};
}
return screen->grabWindow(0, r.x(), r.y(), r.width(), r.height()).toImage();
}
@@ -149,7 +189,6 @@ bool ScreenshotViewModel::copyImageToClipboard(const QImage &image)
return writeImageToClipboard(image);
}
// ── captureScreen (private) ───────────────────────────────
QImage ScreenshotViewModel::captureScreen(QQuickItem *paintBoard,
const QRect &rect,
bool setBackground)
@@ -157,93 +196,86 @@ QImage ScreenshotViewModel::captureScreen(QQuickItem *paintBoard,
PaintBoard *board = qobject_cast<PaintBoard*>(paintBoard);
QImage screenshot;
qreal scaleX = 1.0;
qreal scaleY = 1.0;
qreal dpr = 1.0;
if (!m_snapshot.isNull()) {
// 快照坐标系:逻辑坐标相对于虚拟桌面原点;物理像素按统一 DPR 缩放。
// 将逻辑选区映射到快照物理像素矩形后裁剪。
const QRect vg = m_snapshot.virtualGeometry();
scaleX = vg.width() > 0 ? qreal(m_snapshot.image().width()) / qreal(vg.width()) : 1.0;
scaleY = vg.height() > 0 ? qreal(m_snapshot.image().height()) / qreal(vg.height()) : 1.0;
const QRect physRect = rect.isEmpty()
? m_snapshot.image().rect()
: QRect(qRound((rect.x() - vg.x()) * scaleX),
qRound((rect.y() - vg.y()) * scaleY),
qRound(rect.width() * scaleX),
qRound(rect.height() * scaleY))
.intersected(m_snapshot.image().rect());
screenshot = m_snapshot.image().copy(physRect);
if (board && board->window()) {
dpr = board->window()->devicePixelRatio();
} else if (!rect.isEmpty()) {
dpr = captureDprForRect(rect);
}
const QRect captureRect = captureRectInSnapshot(m_snapshot, rect, dpr);
screenshot = m_snapshot.image().copy(captureRect).toImage();
} else {
qWarning() << "[ScreenshotViewModel] 快照未缓存,回退到实时抓屏(仅主屏)";
qWarning() << "[ScreenshotViewModel] snapshot unavailable, falling back to primary screen";
QScreen *screen = QGuiApplication::primaryScreen();
if (!screen) return {};
if (!screen) {
return {};
}
screenshot = screen->grabWindow(0).toImage();
}
if (board) {
QImage annotations = board->renderToImage();
if (!annotations.isNull()) {
// 标注层是逻辑像素,先映射到物理像素坐标系,避免高 DPI 下偏移。
const QSize physicalAnnoSize(
qMax(1, qRound(annotations.width() * scaleX)),
qMax(1, qRound(annotations.height() * scaleY)));
if (annotations.size() != physicalAnnoSize) {
annotations = annotations.scaled(physicalAnnoSize,
Qt::IgnoreAspectRatio,
Qt::SmoothTransformation);
}
QPainter p(&screenshot);
// PaintBoard 现在是全屏画布:导出选区时,需要从全屏标注层裁剪对应区域再叠加
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 boardAbsX = windowAbsX + qRound(board->x());
const int boardAbsY = windowAbsY + qRound(board->y());
const int wantX = qRound((rect.x() - boardAbsX) * scaleX);
const int wantY = qRound((rect.y() - boardAbsY) * scaleY);
QRect srcRect(wantX, wantY, screenshot.width(), screenshot.height());
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 = srcRect.x() - wantX;
const int dstY = srcRect.y() - wantY;
p.drawImage(QPoint(dstX, dstY), annotations, srcRect);
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);
}
} else {
p.drawImage(0, 0, annotations);
}
p.end();
}
if (setBackground) {
// PaintBoard 背景图使用逻辑像素尺寸,避免高 DPI 下比例异常。
QImage logicalBackground = screenshot;
const QSize logicalSize(
qMax(1, qRound(screenshot.width() / scaleX)),
qMax(1, qRound(screenshot.height() / scaleY)));
if (logicalBackground.size() != logicalSize) {
logicalBackground = logicalBackground.scaled(logicalSize,
Qt::IgnoreAspectRatio,
Qt::SmoothTransformation);
QImage background = screenshot;
if (board->window() && board->window()->devicePixelRatio() > 1.0) {
const QSize logicalSize(qRound(background.width() / board->window()->devicePixelRatio()),
qRound(background.height() / board->window()->devicePixelRatio()));
if (logicalSize.isValid() && logicalSize != background.size()) {
background = background.scaled(logicalSize,
Qt::IgnoreAspectRatio,
Qt::SmoothTransformation);
}
}
board->setBackgroundImg(logicalBackground);
board->setBackgroundImg(background);
}
}
return screenshot;
}
// ── writeImageToClipboard (private) ──────────────────────
bool ScreenshotViewModel::writeImageToClipboard(const QImage &image)
{
if (image.isNull()) return false;
if (image.isNull()) {
return false;
}
QClipboard *cb = QGuiApplication::clipboard();
if (!cb) return false;
if (!cb) {
return false;
}
cb->setImage(image, QClipboard::Clipboard);
#ifdef Q_OS_LINUX
cb->setImage(image, QClipboard::Selection);
+18 -41
View File
@@ -1,79 +1,56 @@
#ifndef SCREENSHOT_VIEW_MODEL_H
#define SCREENSHOT_VIEW_MODEL_H
#include <QObject>
#include <QRect>
#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"
// ScreenshotViewModel
// -------------------
// 编排截图完整流程:抓快照 → 裁剪 → 合并标注 → 输出(剪贴板/Base64/贴图)。
// 暴露 Q_INVOKABLE 方法和信号供 QML 调用,自身不感知任何 QML 组件 id。
// 依赖的 Model:DesktopSnapshot(数据)、StickyImageStore(贴图存储)。
class ScreenshotViewModel : public QObject
{
Q_OBJECT
public:
explicit ScreenshotViewModel(DesktopSnapshot &snapshot,
StickyImageStore &stickyStore,
QObject *parent = nullptr);
StickyImageStore &stickyStore,
QObject *parent = nullptr);
// ── 供 MagnifierProvider 只读访问快照 ────────────────
const QImage& desktopSnapshot() const { return m_snapshot.image(); }
// 虚拟桌面的逻辑矩形(所有屏幕 geometry 的并集),供 QML 定位截图窗口
const QPixmap& desktopSnapshot() const { return m_snapshot.image(); }
Q_INVOKABLE QRect virtualGeometry() const { return m_snapshot.virtualGeometry(); }
QPoint mapLogicalPointToPhysical(const QPoint &point) const
{
return m_snapshot.mapLogicalPointToPhysical(point);
}
Q_INVOKABLE QRect mapLogicalRectToPhysicalRect(const QRect &rect) const
{
return captureRectForDisplay(rect);
}
// ── Q_INVOKABLE:供 QML 调用 ─────────────────────────
// 抓取全屏快照(在截图窗口显示之前调用)
Q_INVOKABLE void captureDesktop();
// 释放快照(截图会话结束后调用)
Q_INVOKABLE void releaseDesktopSnapshot();
// 取指定逻辑坐标的像素颜色(放大镜取色)
Q_INVOKABLE QColor getPixelColor(int x, int y) const;
// 返回 (x,y) 下最上层窗口矩形(窗口识别)
Q_INVOKABLE QRect windowAtPoint(int x, int y) const;
// 截图选区并写入剪贴板,返回是否成功
Q_INVOKABLE bool captureRectToClipboard(QQuickItem *paintBoard, const QRect &rect);
// 截图选区并返回 Base64 字符串(供保存文件使用,不写剪贴板)
Q_INVOKABLE QString captureRectToBase64(QQuickItem *paintBoard, const QRect &rect);
// 截图选区并存入 StickyImageStore,返回 image://sticky/<id> URL
Q_INVOKABLE QString captureRectToStickyUrl(QQuickItem *paintBoard, const QRect &rect);
// 仅将选区写入 PaintBoard 背景(马赛克工具使用)
Q_INVOKABLE void grabToPaintBoard(QQuickItem *paintBoard, const QRect &rect);
// 截图选区并直接返回 QImage(OCR 使用)
Q_INVOKABLE QImage captureRectToImage(const QRect &rect);
// 实时抓取选区(每次都会刷新桌面快照),用于长截图连续采样。
QImage captureRectToImageLive(const QRect &rect);
// 复制任意图像到剪贴板。
bool copyImageToClipboard(const QImage &image);
private:
// 内部:从快照裁剪选区并叠加 PaintBoard 标注层
// setBackground=true 时将结果写回 PaintBoard 背景(仅马赛克路径)
QImage captureScreen(QQuickItem *paintBoard, const QRect &rect = QRect(),
QRect captureRectForDisplay(const QRect &rect) const;
QImage captureScreen(QQuickItem *paintBoard,
const QRect &rect = QRect(),
bool setBackground = false);
bool writeImageToClipboard(const QImage &image);
DesktopSnapshot &m_snapshot;
DesktopSnapshot &m_snapshot;
StickyImageStore &m_stickyStore;
};
+65 -3
View File
@@ -1,6 +1,14 @@
#include "sticky_view_model.h"
#include "sticky_view_model.h"
#include <QPainter>
#include <QTransform>
#include <QGuiApplication>
#include <QScreen>
#include <QWindow>
#include <QDebug>
#ifdef Q_OS_WIN
#include <windows.h>
#endif
StickyViewModel::StickyViewModel(StickyImageStore &store, QObject *parent)
: QObject(parent)
@@ -10,8 +18,34 @@ StickyViewModel::StickyViewModel(StickyImageStore &store, QObject *parent)
void StickyViewModel::requestSticky(const QString &imageUrl, const QRect &imgRect)
{
if (imageUrl.isEmpty()) return;
emit stickyReady(imageUrl, imgRect);
if (imageUrl.isEmpty()) {
return;
}
QRect targetRect = imgRect.normalized();
QImage image = m_store.getImageByUrl(imageUrl);
if (!image.isNull()) {
QScreen *screen = QGuiApplication::screenAt(targetRect.topLeft());
if (!screen) {
screen = QGuiApplication::screenAt(targetRect.center());
}
if (!screen) {
screen = QGuiApplication::primaryScreen();
}
const qreal dpr = screen ? screen->devicePixelRatio() : 1.0;
if (dpr > 0.0) {
if (!qFuzzyCompare(image.devicePixelRatio(), dpr)) {
image.setDevicePixelRatio(dpr);
m_store.replaceImage(imageUrl, image);
}
const QSize logicalSize(qMax(1, qRound(image.width() / dpr)),
qMax(1, qRound(image.height() / dpr)));
targetRect.setSize(logicalSize);
}
}
emit stickyReady(imageUrl, targetRect);
}
void StickyViewModel::releaseImage(const QString &imageUrl)
@@ -19,6 +53,34 @@ void StickyViewModel::releaseImage(const QString &imageUrl)
m_store.releaseImage(imageUrl);
}
void StickyViewModel::positionStickyWindow(QObject *windowObject, const QRect &imgRect) const
{
auto *window = qobject_cast<QWindow*>(windowObject);
if (!window) {
return;
}
const QRect targetRect = imgRect.normalized();
const QPoint topLeft = targetRect.topLeft();
window->setPosition(topLeft);
#ifdef Q_OS_WIN
if (HWND hwnd = reinterpret_cast<HWND>(window->winId())) {
RECT rect{};
GetWindowRect(hwnd, &rect);
qDebug() << "[StickyHWND] qt-pos:"
<< topLeft
<< "hwnd:"
<< QRect(rect.left,
rect.top,
rect.right - rect.left,
rect.bottom - rect.top)
<< "qmlPos:" << window->x() << window->y();
}
#endif
}
bool StickyViewModel::saveImage(const QString &imageUrl, const QUrl &targetUrl)
{
return m_store.saveImage(imageUrl, targetUrl);
+1
View File
@@ -29,6 +29,7 @@ public:
Q_INVOKABLE bool saveImage(const QString &imageUrl, const QUrl &targetUrl);
Q_INVOKABLE bool copyImageToClipboard(const QString &imageUrl);
Q_INVOKABLE QImage getImageByUrl(const QString &imageUrl) const;
Q_INVOKABLE void positionStickyWindow(QObject *windowObject, const QRect &imgRect) const;
Q_INVOKABLE bool overwriteWithAnnotations(const QString &imageUrl, const QImage &annotationLayer);
Q_INVOKABLE bool saveMergedImage(const QString &imageUrl, const QImage &annotationLayer, const QUrl &targetUrl);
Q_INVOKABLE bool rotateImage(const QString &imageUrl, int degreesClockwise);