点阵完成,加入opencv

This commit is contained in:
2026-01-20 19:55:56 +08:00
parent 59564fd312
commit bc9f2824ed
367 changed files with 162001 additions and 52 deletions

View File

@@ -0,0 +1,616 @@
// ColorPickerWindow.qml
import QtQuick
import QtQuick.Window
import QtQuick.Controls
import QtQuick.Layouts
import QtQuick.Controls.Material
import TactileIPC 1.0
Window {
id: root
width: 360
height: 650
visible: false
modality: Qt.ApplicationModal
flags: Qt.Dialog
title: qsTr("颜色选择")
Material.theme: Backend.lightMode ? Material.Light : Material.Dark
Material.accent: Material.Green
Material.primary: Material.Green
readonly property bool isDark: !Backend.lightMode
readonly property color windowBg: isDark ? "#1F1F1F" : "#F7F7F7"
readonly property color panelBorder: isDark ? "#2E2E2E" : "#D9D9D9"
readonly property color surfaceBorder: isDark ? "#343434" : "#CFCFCF"
readonly property color textPrimary: isDark ? "#EDEDED" : "#1F1F1F"
readonly property color textSecondary: isDark ? "#D8D8D8" : "#616161"
readonly property color textMuted: isDark ? "#BEBEBE" : "#6E6E6E"
readonly property color controlBg: isDark ? "#2A2A2A" : "#FFFFFF"
readonly property color controlBorder: isDark ? "#3A3A3A" : "#CFCFCF"
readonly property color highlightBg: isDark ? "#55FFFFFF" : "#33000000"
readonly property color highlightText: isDark ? "#111111" : "#FFFFFF"
readonly property color indicatorBorder: isDark ? "#6A6A6A" : "#9E9E9E"
readonly property color accentColor: Material.color(Material.Green)
// ===== API =====
property color color: "#FF7032D2" // AARRGGBB
signal accepted(color c)
signal rejected()
function openWith(c) {
syncFromColor(c ?? root.color)
visible = true
requestActivate()
}
Keys.onEscapePressed: {
visible = false
rejected()
}
// ===== internal HSV(A) =====
property real h: 0.75 // 0..1
property real s: 0.45
property real v: 0.82
property real a: 1.0
property bool _lock: false
function clamp01(x) { return Math.max(0, Math.min(1, x)) }
function clampInt(x, lo, hi) { return Math.max(lo, Math.min(hi, Math.round(x))) }
function hsvToRgb(hh, ss, vv) {
const h6 = (hh % 1) * 6
const c = vv * ss
const x = c * (1 - Math.abs((h6 % 2) - 1))
const m = vv - c
let r1=0, g1=0, b1=0
if (0 <= h6 && h6 < 1) { r1=c; g1=x; b1=0 }
else if (1 <= h6 && h6 < 2) { r1=x; g1=c; b1=0 }
else if (2 <= h6 && h6 < 3) { r1=0; g1=c; b1=x }
else if (3 <= h6 && h6 < 4) { r1=0; g1=x; b1=c }
else if (4 <= h6 && h6 < 5) { r1=x; g1=0; b1=c }
else { r1=c; g1=0; b1=x }
return { r: r1 + m, g: g1 + m, b: b1 + m }
}
function rgbToHsv(r, g, b) {
const max = Math.max(r, g, b)
const min = Math.min(r, g, b)
const d = max - min
let hh = 0
if (d !== 0) {
if (max === r) hh = ((g - b) / d) % 6
else if (max === g) hh = (b - r) / d + 2
else hh = (r - g) / d + 4
hh /= 6
if (hh < 0) hh += 1
}
const ss = (max === 0) ? 0 : d / max
const vv = max
return { h: hh, s: ss, v: vv }
}
function toHex2(v01) {
const n = clampInt(clamp01(v01) * 255, 0, 255)
const s = n.toString(16).toUpperCase()
return (s.length === 1) ? ("0" + s) : s
}
function colorToHexAARRGGBB(c) {
return "#" + toHex2(c.a) + toHex2(c.r) + toHex2(c.g) + toHex2(c.b)
}
function parseHex(str) {
let t = ("" + str).trim()
if (t.startsWith("0x") || t.startsWith("0X")) t = t.slice(2)
if (t.startsWith("#")) t = t.slice(1)
if (t.length === 6) {
const rr = parseInt(t.slice(0,2), 16)
const gg = parseInt(t.slice(2,4), 16)
const bb = parseInt(t.slice(4,6), 16)
if ([rr,gg,bb].some(x => isNaN(x))) return null
return Qt.rgba(rr/255, gg/255, bb/255, 1)
}
if (t.length === 8) {
const aa = parseInt(t.slice(0,2), 16)
const rr = parseInt(t.slice(2,4), 16)
const gg = parseInt(t.slice(4,6), 16)
const bb = parseInt(t.slice(6,8), 16)
if ([aa,rr,gg,bb].some(x => isNaN(x))) return null
return Qt.rgba(rr/255, gg/255, bb/255, aa/255)
}
return null
}
function syncFromHSV() {
if (_lock) return
_lock = true
const rgb = hsvToRgb(h, s, v)
color = Qt.rgba(rgb.r, rgb.g, rgb.b, a)
// 更新 UI
hexField.text = colorToHexAARRGGBB(color)
alphaPercent.text = clampInt(a*100, 0, 100) + "%"
rField.value = clampInt(color.r * 255, 0, 255)
gField.value = clampInt(color.g * 255, 0, 255)
bField.value = clampInt(color.b * 255, 0, 255)
// HSV 显示用度/百分比
hField.value = clampInt(h * 360, 0, 360)
sField.value = clampInt(s * 100, 0, 100)
vField.value = clampInt(v * 100, 0, 100)
svCanvas.requestPaint()
hueCanvas.requestPaint()
alphaCanvas.requestPaint()
_lock = false
}
function syncFromColor(c) {
if (_lock) return
_lock = true
const hsv = rgbToHsv(c.r, c.g, c.b)
h = hsv.h; s = hsv.s; v = hsv.v; a = c.a
color = Qt.rgba(c.r, c.g, c.b, a)
hexField.text = colorToHexAARRGGBB(color)
alphaPercent.text = clampInt(a*100, 0, 100) + "%"
rField.value = clampInt(c.r * 255, 0, 255)
gField.value = clampInt(c.g * 255, 0, 255)
bField.value = clampInt(c.b * 255, 0, 255)
hField.value = clampInt(h * 360, 0, 360)
sField.value = clampInt(s * 100, 0, 100)
vField.value = clampInt(v * 100, 0, 100)
svCanvas.requestPaint()
hueCanvas.requestPaint()
alphaCanvas.requestPaint()
_lock = false
}
function applyRGB(r255, g255, b255) {
if (_lock) return
const rr = clampInt(r255, 0, 255) / 255
const gg = clampInt(g255, 0, 255) / 255
const bb = clampInt(b255, 0, 255) / 255
const hsv = rgbToHsv(rr, gg, bb)
h = hsv.h; s = hsv.s; v = hsv.v
syncFromHSV()
}
function applyHSV(hDeg, sPct, vPct) {
if (_lock) return
h = clamp01(hDeg / 360)
s = clamp01(sPct / 100)
v = clamp01(vPct / 100)
syncFromHSV()
}
// 初始同步
Component.onCompleted: syncFromColor(root.color)
// ====== UI: 深色窗口面板(不是卡片)======
Rectangle {
anchors.fill: parent
radius: 0
color: root.windowBg
border.width: 1
border.color: root.panelBorder
ColumnLayout {
anchors.fill: parent
anchors.margins: 10
spacing: 10
// 顶部:标题 + 勾选(参考图右上角)
// RowLayout {
// Layout.fillWidth: true
// spacing: 8
// Label {
// text: root.title
// color: root.textPrimary
// font.pixelSize: 14
// font.weight: Font.DemiBold
// Layout.fillWidth: true
// }
// CheckBox {
// id: alphaToggle
// checked: true
// contentItem: Label {
// text: qsTr("实时预览")
// color: root.textSecondary
// verticalAlignment: Text.AlignVCenter
// elide: Text.ElideRight
// }
// indicator: Rectangle {
// implicitWidth: 16
// implicitHeight: 16
// radius: 3
// border.width: 1
// border.color: root.indicatorBorder
// color: alphaToggle.checked ? root.accentColor : "transparent"
// // 选中勾
// Canvas {
// anchors.fill: parent
// onPaint: {
// const ctx = getContext("2d")
// ctx.clearRect(0,0,width,height)
// if (!alphaToggle.checked) return
// ctx.strokeStyle = "white"
// ctx.lineWidth = 2
// ctx.lineCap = "round"
// ctx.beginPath()
// ctx.moveTo(width*0.25, height*0.55)
// ctx.lineTo(width*0.45, height*0.72)
// ctx.lineTo(width*0.78, height*0.30)
// ctx.stroke()
// }
// }
// }
// }
// ToolButton {
// text: "✕"
// onClicked: { root.visible = false; root.rejected() }
// }
// }
// HSV 面板
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 185
radius: 8
border.width: 1
border.color: root.surfaceBorder
clip: true
Canvas {
id: svCanvas
anchors.fill: parent
onPaint: {
const ctx = getContext("2d")
const w = width, hh = height
ctx.clearRect(0,0,w,hh)
const rgbHue = root.hsvToRgb(root.h, 1, 1)
// X: white -> hue
const gx = ctx.createLinearGradient(0,0,w,0)
gx.addColorStop(0, "rgb(255,255,255)")
gx.addColorStop(1, "rgb(" + Math.round(rgbHue.r*255) + "," + Math.round(rgbHue.g*255) + "," + Math.round(rgbHue.b*255) + ")")
ctx.fillStyle = gx
ctx.fillRect(0,0,w,hh)
// Y: transparent -> black
const gy = ctx.createLinearGradient(0,0,0,hh)
gy.addColorStop(0, "rgba(0,0,0,0)")
gy.addColorStop(1, "rgba(0,0,0,1)")
ctx.fillStyle = gy
ctx.fillRect(0,0,w,hh)
// handle
const x = root.s * w
const y = (1 - root.v) * hh
ctx.beginPath()
ctx.arc(x, y, 7, 0, Math.PI*2)
ctx.lineWidth = 2
ctx.strokeStyle = "rgba(255,255,255,0.95)"
ctx.stroke()
ctx.beginPath()
ctx.arc(x, y, 6, 0, Math.PI*2)
ctx.lineWidth = 1
ctx.strokeStyle = "rgba(0,0,0,0.55)"
ctx.stroke()
}
}
MouseArea {
anchors.fill: parent
function apply(mx, my) {
root.s = root.clamp01(mx / width)
root.v = root.clamp01(1 - (my / height))
root.syncFromHSV()
}
onPressed: (e) => apply(e.x, e.y)
onPositionChanged: (e) => { if (pressed) apply(e.x, e.y) }
}
}
// Hue 彩虹条(参考图那种)
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 16
radius: 8
border.width: 1
border.color: root.surfaceBorder
clip: true
Canvas {
id: hueCanvas
anchors.fill: parent
onPaint: {
const ctx = getContext("2d")
const w = width, hh = height
ctx.clearRect(0,0,w,hh)
const grad = ctx.createLinearGradient(0,0,w,0)
for (let i=0; i<=6; i++) {
const t = i/6
const rgb = root.hsvToRgb(t, 1, 1)
grad.addColorStop(t, "rgb(" + Math.round(rgb.r*255) + "," + Math.round(rgb.g*255) + "," + Math.round(rgb.b*255) + ")")
}
ctx.fillStyle = grad
ctx.fillRect(0,0,w,hh)
const x = root.h * w
ctx.beginPath()
ctx.arc(x, hh/2, 7, 0, Math.PI*2)
ctx.lineWidth = 2
ctx.strokeStyle = "rgba(255,255,255,0.95)"
ctx.stroke()
ctx.beginPath()
ctx.arc(x, hh/2, 6, 0, Math.PI*2)
ctx.lineWidth = 1
ctx.strokeStyle = "rgba(0,0,0,0.55)"
ctx.stroke()
}
}
MouseArea {
anchors.fill: parent
function apply(mx) {
root.h = root.clamp01(mx / width)
root.syncFromHSV()
}
onPressed: (e) => apply(e.x)
onPositionChanged: (e) => { if (pressed) apply(e.x) }
}
}
// Alpha 条(棋盘 + 渐变 + 右侧圆点)
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 16
radius: 8
border.width: 1
border.color: root.surfaceBorder
clip: true
// checker
Canvas {
id: checkerCanvas
anchors.fill: parent
onPaint: {
const ctx = getContext("2d")
const w = width, hh = height
ctx.clearRect(0,0,w,hh)
const s = 8
for (let y=0; y<hh; y+=s) {
for (let x=0; x<w; x+=s) {
const on = ((x/s + y/s) % 2) === 0
ctx.fillStyle = on ? "rgba(255,255,255,0.16)" : "rgba(0,0,0,0.0)"
ctx.fillRect(x,y,s,s)
}
}
}
}
Canvas {
id: alphaCanvas
anchors.fill: parent
onPaint: {
const ctx = getContext("2d")
const w = width, hh = height
ctx.clearRect(0,0,w,hh)
const rgb = root.hsvToRgb(root.h, root.s, root.v)
const grad = ctx.createLinearGradient(0,0,w,0)
grad.addColorStop(0, "rgba(" + Math.round(rgb.r*255) + "," + Math.round(rgb.g*255) + "," + Math.round(rgb.b*255) + ",0)")
grad.addColorStop(1, "rgba(" + Math.round(rgb.r*255) + "," + Math.round(rgb.g*255) + "," + Math.round(rgb.b*255) + ",1)")
ctx.fillStyle = grad
ctx.fillRect(0,0,w,hh)
const x = root.a * w
ctx.beginPath()
ctx.arc(x, hh/2, 7, 0, Math.PI*2)
ctx.lineWidth = 2
ctx.strokeStyle = "rgba(255,255,255,0.95)"
ctx.stroke()
ctx.beginPath()
ctx.arc(x, hh/2, 6, 0, Math.PI*2)
ctx.lineWidth = 1
ctx.strokeStyle = "rgba(0,0,0,0.55)"
ctx.stroke()
}
}
MouseArea {
anchors.fill: parent
function apply(mx) {
root.a = root.clamp01(mx / width)
root.syncFromHSV()
}
onPressed: (e) => apply(e.x)
onPositionChanged: (e) => { if (pressed) apply(e.x) }
}
}
// 下方:色块 + Hex + 透明度百分比
RowLayout {
Layout.fillWidth: true
spacing: 10
// 预览色块(带棋盘底)
Rectangle {
width: 44
height: 44
radius: 8
border.width: 1
border.color: root.surfaceBorder
clip: true
Canvas {
anchors.fill: parent
onPaint: {
const ctx = getContext("2d")
const w = width, hh = height
ctx.clearRect(0,0,w,hh)
const s = 10
for (let y=0; y<hh; y+=s) {
for (let x=0; x<w; x+=s) {
const on = ((x/s + y/s) % 2) === 0
ctx.fillStyle = on ? "rgba(255,255,255,0.14)" : "rgba(0,0,0,0.0)"
ctx.fillRect(x,y,s,s)
}
}
}
}
Rectangle {
anchors.fill: parent
color: root.color
}
}
TextField {
id: hexField
Layout.fillWidth: true
text: "#FF7032D2"
placeholderText: "#AARRGGBB"
inputMethodHints: Qt.ImhPreferUppercase | Qt.ImhNoPredictiveText
// 用 palette而不是 color/selectionColor/selectedTextColor
palette.text: root.textPrimary
palette.placeholderText: root.textMuted
palette.highlight: root.highlightBg
palette.highlightedText: root.highlightText
palette.base: root.controlBg // 输入框底色(有的 style 会用它)
palette.buttonText: root.textPrimary
background: Rectangle {
radius: 8
color: root.controlBg
border.width: 1
border.color: root.controlBorder
}
onEditingFinished: {
const c = root.parseHex(text)
if (c) root.syncFromColor(c)
else text = root.colorToHexAARRGGBB(root.color)
}
}
Rectangle {
width: 54
height: 44
radius: 8
color: root.controlBg
border.width: 1
border.color: root.controlBorder
Label {
id: alphaPercent
anchors.centerIn: parent
text: "100%"
color: root.textPrimary
font.pixelSize: 12
}
}
}
// RGB / HSV 数值区(像参考图那样两行块)
GridLayout {
Layout.fillWidth: true
columns: 3
columnSpacing: 8
rowSpacing: 8
// ---- Row1: RGB ----
Label { text: "RGB"; color: root.textMuted; Layout.alignment: Qt.AlignVCenter }
SpinBox {
id: rField
from: 0; to: 255; editable: true
value: 112
Layout.fillWidth: true
onValueModified: root.applyRGB(value, gField.value, bField.value)
}
SpinBox {
id: gField
from: 0; to: 255; editable: true
value: 50
Layout.fillWidth: true
onValueModified: root.applyRGB(rField.value, value, bField.value)
}
Item { width: 1; height: 1 } // 占位,让下一行对齐
SpinBox {
id: bField
from: 0; to: 255; editable: true
value: 210
Layout.fillWidth: true
Layout.columnSpan: 2
onValueModified: root.applyRGB(rField.value, gField.value, value)
}
// ---- Row2: HSV ----
Label { text: "HSV"; color: root.textMuted; Layout.alignment: Qt.AlignVCenter }
SpinBox {
id: hField
from: 0; to: 360; editable: true
value: 263
Layout.fillWidth: true
onValueModified: root.applyHSV(value, sField.value, vField.value)
}
SpinBox {
id: sField
from: 0; to: 100; editable: true
value: 64
Layout.fillWidth: true
onValueModified: root.applyHSV(hField.value, value, vField.value)
}
Item { width: 1; height: 1 }
SpinBox {
id: vField
from: 0; to: 100; editable: true
value: 51
Layout.fillWidth: true
Layout.columnSpan: 2
onValueModified: root.applyHSV(hField.value, sField.value, value)
}
}
// 底部按钮(像窗口)
RowLayout {
Layout.fillWidth: true
spacing: 10
Item { Layout.fillWidth: true }
Button {
text: qsTr("Cancel")
onClicked: { root.visible = false; root.rejected() }
}
Button {
text: qsTr("OK")
highlighted: true
onClicked: { root.visible = false; root.accepted(root.color) }
}
}
}
}
// 每次 HSV 改变UI 同步
onHChanged: if (!_lock) syncFromHSV()
onSChanged: if (!_lock) syncFromHSV()
onVChanged: if (!_lock) syncFromHSV()
onAChanged: if (!_lock) syncFromHSV()
}