feat: add instance activation, OCR self-check, and GIF size presets
This commit is contained in:
1 parent
57b4e8b207
commit
a83d244199
14 files changed
+486
-13
No files matched your search
@@ -48,6 +48,8 @@ set(SOURCES
|
||||
src/sticky_image_store.h
|
||||
src/sticky_image_provider_proxy.cpp
|
||||
src/sticky_image_provider_proxy.h
|
||||
src/instance_activation.cpp
|
||||
src/instance_activation.h
|
||||
src/singleton.h
|
||||
|
||||
# Model
|
||||
|
||||
@@ -329,8 +329,172 @@ Rectangle {
|
||||
}
|
||||
}
|
||||
|
||||
Item { Layout.preferredHeight: 8 }
|
||||
|
||||
// ── GIF 体积预设 ─────────────────────────────
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 8
|
||||
spacing: 8
|
||||
Rectangle {
|
||||
width: 3; height: 15; radius: 2
|
||||
color: "#3B82F6"
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
Text {
|
||||
text: qsTr("GIF 录制")
|
||||
font.pixelSize: 13
|
||||
font.bold: true
|
||||
color: "#1E293B"
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 72
|
||||
color: "#FFFFFF"
|
||||
radius: 10
|
||||
border.color: "#E2E8F0"
|
||||
border.width: 1
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 16
|
||||
spacing: 12
|
||||
|
||||
Text {
|
||||
Layout.preferredWidth: 90
|
||||
text: qsTr("体积预设")
|
||||
font.pixelSize: 13
|
||||
color: "#374151"
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
|
||||
TZComboBox {
|
||||
id: gifPresetCombo
|
||||
model: [qsTr("高质量"), qsTr("均衡"), qsTr("小体积")]
|
||||
currentIndex: O_GifRecordVM.qualityPreset
|
||||
Layout.preferredWidth: 180
|
||||
Layout.preferredHeight: 36
|
||||
onCurrentIndexChanged: O_GifRecordVM.setQualityPreset(currentIndex)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item { Layout.preferredHeight: 8 }
|
||||
|
||||
// ── OCR 自检 ─────────────────────────────────
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 8
|
||||
spacing: 8
|
||||
Rectangle {
|
||||
width: 3; height: 15; radius: 2
|
||||
color: "#3B82F6"
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
Text {
|
||||
text: qsTr("OCR 自检")
|
||||
font.pixelSize: 13
|
||||
font.bold: true
|
||||
color: "#1E293B"
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 168
|
||||
color: "#FFFFFF"
|
||||
radius: 10
|
||||
border.color: "#E2E8F0"
|
||||
border.width: 1
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 12
|
||||
spacing: 10
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
radius: 8
|
||||
color: "#F8FAFC"
|
||||
border.color: "#E2E8F0"
|
||||
border.width: 1
|
||||
|
||||
ScrollView {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 8
|
||||
clip: true
|
||||
TextArea {
|
||||
text: O_OcrVM.selfCheckText.length > 0
|
||||
? O_OcrVM.selfCheckText
|
||||
: qsTr("点击“一键自检”查看 OCR 环境状态。")
|
||||
readOnly: true
|
||||
wrapMode: TextEdit.Wrap
|
||||
selectByMouse: true
|
||||
background: null
|
||||
font.pixelSize: 12
|
||||
color: "#334155"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 8
|
||||
|
||||
Rectangle {
|
||||
Layout.preferredWidth: 100
|
||||
Layout.preferredHeight: 34
|
||||
radius: 6
|
||||
color: selfCheckMouse.containsMouse ? "#2563EB" : "#3B82F6"
|
||||
Behavior on color { ColorAnimation { duration: 100 } }
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: qsTr("一键自检")
|
||||
font.pixelSize: 13
|
||||
color: "#FFFFFF"
|
||||
}
|
||||
MouseArea {
|
||||
id: selfCheckMouse
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: O_OcrVM.runSelfCheck()
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.preferredWidth: 130
|
||||
Layout.preferredHeight: 34
|
||||
radius: 6
|
||||
color: openTessMouse.containsMouse ? "#E2E8F0" : "#F1F5F9"
|
||||
border.color: "#CBD5E1"
|
||||
border.width: 1
|
||||
Behavior on color { ColorAnimation { duration: 100 } }
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: qsTr("打开 tessdata")
|
||||
font.pixelSize: 13
|
||||
color: "#334155"
|
||||
}
|
||||
MouseArea {
|
||||
id: openTessMouse
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
if (!O_OcrVM.openTessdataFolder())
|
||||
O_OcrVM.runSelfCheck()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item { Layout.fillHeight: true }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -204,6 +204,18 @@ TZWindow {
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: O_InstanceBridge
|
||||
function onActivationRequested() {
|
||||
if (root.visible) {
|
||||
root.raise()
|
||||
root.requestActivate()
|
||||
return
|
||||
}
|
||||
startScreenshot()
|
||||
}
|
||||
}
|
||||
|
||||
TrayIconHelper {
|
||||
id: trayHelper
|
||||
function onTrayClicked(clickType) {
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
#include "instance_activation.h"
|
||||
|
||||
#include <QLocalServer>
|
||||
#include <QLocalSocket>
|
||||
|
||||
InstanceActivation::InstanceActivation(const QString &serverName, QObject *parent)
|
||||
: QObject(parent)
|
||||
, m_serverName(serverName)
|
||||
{
|
||||
}
|
||||
|
||||
InstanceActivation::~InstanceActivation()
|
||||
{
|
||||
if (m_server) {
|
||||
m_server->close();
|
||||
m_server->deleteLater();
|
||||
m_server = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
bool InstanceActivation::initialize()
|
||||
{
|
||||
// First try to notify an existing instance.
|
||||
QLocalSocket socket;
|
||||
socket.connectToServer(m_serverName);
|
||||
if (socket.waitForConnected(150)) {
|
||||
socket.write("ACTIVATE");
|
||||
socket.flush();
|
||||
socket.waitForBytesWritten(150);
|
||||
socket.disconnectFromServer();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Become the primary instance.
|
||||
QLocalServer::removeServer(m_serverName);
|
||||
m_server = new QLocalServer(this);
|
||||
connect(m_server, &QLocalServer::newConnection,
|
||||
this, &InstanceActivation::onNewConnection);
|
||||
if (!m_server->listen(m_serverName)) {
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void InstanceActivation::onNewConnection()
|
||||
{
|
||||
if (!m_server) {
|
||||
return;
|
||||
}
|
||||
while (m_server->hasPendingConnections()) {
|
||||
QLocalSocket *client = m_server->nextPendingConnection();
|
||||
if (!client) {
|
||||
continue;
|
||||
}
|
||||
connect(client, &QLocalSocket::readyRead, client, [this, client]() {
|
||||
client->readAll();
|
||||
emit activationRequested();
|
||||
client->disconnectFromServer();
|
||||
client->deleteLater();
|
||||
});
|
||||
connect(client, &QLocalSocket::disconnected, client, &QLocalSocket::deleteLater);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
#ifndef INSTANCE_ACTIVATION_H
|
||||
#define INSTANCE_ACTIVATION_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
|
||||
class QLocalServer;
|
||||
class QLocalSocket;
|
||||
|
||||
class InstanceActivation : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit InstanceActivation(const QString &serverName, QObject *parent = nullptr);
|
||||
~InstanceActivation() override;
|
||||
|
||||
// Returns true if this process should continue startup.
|
||||
// Returns false when another instance is already running (activation message sent).
|
||||
bool initialize();
|
||||
|
||||
signals:
|
||||
void activationRequested();
|
||||
|
||||
private slots:
|
||||
void onNewConnection();
|
||||
|
||||
private:
|
||||
QString m_serverName;
|
||||
QLocalServer *m_server = nullptr;
|
||||
};
|
||||
|
||||
#endif // INSTANCE_ACTIVATION_H
|
||||
+5
-8
@@ -1,9 +1,6 @@
|
||||
#include <QApplication>
|
||||
#include <QQmlApplicationEngine>
|
||||
#include <QQmlContext>
|
||||
#include <QLockFile>
|
||||
#include <QStandardPaths>
|
||||
#include <QDir>
|
||||
|
||||
#include "tray_icon_helper.h"
|
||||
#include "paint_board/paint_board.h"
|
||||
@@ -12,6 +9,7 @@
|
||||
#include "sticky_image_store.h"
|
||||
#include "sticky_image_provider_proxy.h"
|
||||
#include "language_manager.h"
|
||||
#include "instance_activation.h"
|
||||
|
||||
// Model
|
||||
#include "model/desktop_snapshot.h"
|
||||
@@ -29,11 +27,9 @@ int main(int argc, char *argv[])
|
||||
{
|
||||
QApplication app(argc, argv);
|
||||
|
||||
// Single-instance guard: allow only one TZshot process at a time.
|
||||
const QString lockPath = QDir(QStandardPaths::writableLocation(QStandardPaths::TempLocation))
|
||||
.filePath("TZshot.instance.lock");
|
||||
QLockFile instanceLock(lockPath);
|
||||
if (!instanceLock.tryLock(0)) {
|
||||
// Single-instance activation: second launch notifies the first one and exits.
|
||||
InstanceActivation instanceActivation(QStringLiteral("TZshot.Instance"));
|
||||
if (!instanceActivation.initialize()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -83,6 +79,7 @@ int main(int argc, char *argv[])
|
||||
engine.rootContext()->setContextProperty("O_LanguageManager", &languageManager);
|
||||
engine.rootContext()->setContextProperty("O_GifRecordVM", &gifRecordViewModel);
|
||||
engine.rootContext()->setContextProperty("O_OcrVM", &ocrViewModel);
|
||||
engine.rootContext()->setContextProperty("O_InstanceBridge", &instanceActivation);
|
||||
|
||||
const QUrl url(QStringLiteral("qrc:/qml/main.qml"));
|
||||
QObject::connect(&engine, &QQmlApplicationEngine::objectCreated,
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include <QDir>
|
||||
#include <QDebug>
|
||||
#include <QLocale>
|
||||
#include <QtGlobal>
|
||||
|
||||
AppSettings::AppSettings()
|
||||
{
|
||||
@@ -40,6 +41,9 @@ AppSettings::AppSettings()
|
||||
}
|
||||
}
|
||||
qInfo() << "[AppSettings] 保存路径:" << m_savePath;
|
||||
|
||||
const int savedPreset = s.value("GIF/qualityPreset", 1).toInt();
|
||||
m_gifQualityPreset = qBound(0, savedPreset, 2);
|
||||
}
|
||||
|
||||
// ── AI 配置 ───────────────────────────────────────────────
|
||||
@@ -100,3 +104,16 @@ void AppSettings::setSavePath(const QString &path)
|
||||
|
||||
QSettings().setValue("ImageSaver/savePath", m_savePath);
|
||||
}
|
||||
|
||||
int AppSettings::gifQualityPreset() const
|
||||
{
|
||||
return m_gifQualityPreset;
|
||||
}
|
||||
|
||||
void AppSettings::setGifQualityPreset(int preset)
|
||||
{
|
||||
const int normalized = qBound(0, preset, 2);
|
||||
if (m_gifQualityPreset == normalized) return;
|
||||
m_gifQualityPreset = normalized;
|
||||
QSettings().setValue("GIF/qualityPreset", m_gifQualityPreset);
|
||||
}
|
||||
@@ -30,11 +30,16 @@ public:
|
||||
QString savePath() const;
|
||||
void setSavePath(const QString &path);
|
||||
|
||||
// GIF 体积质量预设:0=高质量,1=均衡,2=小体积
|
||||
int gifQualityPreset() const;
|
||||
void setGifQualityPreset(int preset);
|
||||
|
||||
private:
|
||||
QString m_apiKey;
|
||||
int m_selectedModel = 0;
|
||||
QString m_language = "zh_CN";
|
||||
QString m_savePath;
|
||||
int m_gifQualityPreset = 1;
|
||||
};
|
||||
|
||||
#endif // APP_SETTINGS_H
|
||||
@@ -29,6 +29,9 @@ QStringList resolveTessdataDirs()
|
||||
#ifdef OCR_TESSDATA_DIR
|
||||
QStringLiteral(OCR_TESSDATA_DIR),
|
||||
#endif
|
||||
appDir + "/thirdpart/ocr-install/share/tessdata",
|
||||
appDir + "/../thirdpart/ocr-install/share/tessdata",
|
||||
appDir + "/../../thirdpart/ocr-install/share/tessdata",
|
||||
appDir + "/thirdpart/ocr-win/share/tessdata",
|
||||
appDir + "/../thirdpart/ocr-win/share/tessdata",
|
||||
appDir + "/../../thirdpart/ocr-win/share/tessdata",
|
||||
@@ -59,6 +62,46 @@ QStringList resolveTessdataDirs()
|
||||
return validDirs;
|
||||
}
|
||||
|
||||
QStringList resolveTessdataCandidates()
|
||||
{
|
||||
const QString appDir = QCoreApplication::applicationDirPath();
|
||||
QStringList candidates = {
|
||||
#ifdef OCR_TESSDATA_DIR
|
||||
QStringLiteral(OCR_TESSDATA_DIR),
|
||||
#endif
|
||||
appDir + "/thirdpart/ocr-install/share/tessdata",
|
||||
appDir + "/../thirdpart/ocr-install/share/tessdata",
|
||||
appDir + "/../../thirdpart/ocr-install/share/tessdata",
|
||||
appDir + "/thirdpart/ocr-win/share/tessdata",
|
||||
appDir + "/../thirdpart/ocr-win/share/tessdata",
|
||||
appDir + "/../../thirdpart/ocr-win/share/tessdata",
|
||||
appDir + "/thirdpart/ocr/share/tessdata",
|
||||
appDir + "/../thirdpart/ocr/share/tessdata",
|
||||
appDir + "/../../thirdpart/ocr/share/tessdata",
|
||||
QStringLiteral("/usr/share/tesseract-ocr/5/tessdata"),
|
||||
QStringLiteral("/usr/share/tesseract-ocr/4.00/tessdata")
|
||||
};
|
||||
|
||||
const QByteArray envPrefix = qgetenv("TESSDATA_PREFIX");
|
||||
if (!envPrefix.isEmpty()) {
|
||||
const QString envPath = QString::fromUtf8(envPrefix);
|
||||
if (QFileInfo(envPath).fileName() == QStringLiteral("tessdata")) {
|
||||
candidates.prepend(envPath);
|
||||
} else {
|
||||
candidates.prepend(envPath + "/tessdata");
|
||||
}
|
||||
}
|
||||
|
||||
QStringList normalized;
|
||||
for (const QString &candidate : candidates) {
|
||||
const QString path = QDir(candidate).absolutePath();
|
||||
if (!normalized.contains(path)) {
|
||||
normalized << path;
|
||||
}
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
QString resolveTesseractProgram()
|
||||
{
|
||||
const QString appDir = QCoreApplication::applicationDirPath();
|
||||
@@ -115,6 +158,29 @@ QString resolveTesseractProgram()
|
||||
}
|
||||
} // namespace
|
||||
|
||||
QStringList OcrEngine::tessdataSearchCandidates()
|
||||
{
|
||||
return resolveTessdataCandidates();
|
||||
}
|
||||
|
||||
QStringList OcrEngine::availableTessdataDirs()
|
||||
{
|
||||
return resolveTessdataDirs();
|
||||
}
|
||||
|
||||
QString OcrEngine::suggestedTessdataDir()
|
||||
{
|
||||
const QStringList available = availableTessdataDirs();
|
||||
if (!available.isEmpty()) {
|
||||
return available.first();
|
||||
}
|
||||
const QStringList candidates = tessdataSearchCandidates();
|
||||
if (!candidates.isEmpty()) {
|
||||
return candidates.first();
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
OcrEngine::OcrEngine()
|
||||
{
|
||||
#ifdef ENABLE_OCR
|
||||
|
||||
@@ -21,6 +21,11 @@ public:
|
||||
QString lastError() const { return m_lastError; }
|
||||
bool isReady() const { return m_ready; }
|
||||
|
||||
// Diagnostics helpers for UI self-check.
|
||||
static QStringList tessdataSearchCandidates();
|
||||
static QStringList availableTessdataDirs();
|
||||
static QString suggestedTessdataDir();
|
||||
|
||||
private:
|
||||
tesseract::TessBaseAPI *m_api = nullptr;
|
||||
bool m_ready = false;
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include <QDesktopServices>
|
||||
#include <QFileInfo>
|
||||
#include <QUrl>
|
||||
#include <QtGlobal>
|
||||
|
||||
GifRecordViewModel::GifRecordViewModel(AppSettings& settings, QObject* parent)
|
||||
: QObject(parent)
|
||||
@@ -108,6 +109,16 @@ void GifRecordViewModel::cancelRecording()
|
||||
qDebug() << "[GifRecord] 取消录制";
|
||||
}
|
||||
|
||||
void GifRecordViewModel::setQualityPreset(int preset)
|
||||
{
|
||||
const int normalized = qBound(0, preset, 2);
|
||||
if (m_settings.gifQualityPreset() == normalized) {
|
||||
return;
|
||||
}
|
||||
m_settings.setGifQualityPreset(normalized);
|
||||
emit qualityPresetChanged();
|
||||
}
|
||||
|
||||
void GifRecordViewModel::onFrameCaptured(const QImage& frame)
|
||||
{
|
||||
if (!m_recording) return;
|
||||
@@ -162,13 +173,41 @@ void GifRecordViewModel::startEncoding()
|
||||
|
||||
qDebug() << "[GifRecord] 开始编码 →" << m_pendingSavePath;
|
||||
|
||||
// 捕获当前帧序列的副本(子线程持有)
|
||||
QVector<QImage> framesToEncode = m_frames;
|
||||
// 根据质量预设做抽帧/缩放,平衡体积与清晰度。
|
||||
const int preset = m_settings.gifQualityPreset();
|
||||
int frameStep = 1;
|
||||
qreal scale = 1.0;
|
||||
int encodeFps = kFps;
|
||||
if (preset == 1) { // 均衡
|
||||
frameStep = 2;
|
||||
scale = 0.85;
|
||||
encodeFps = 12;
|
||||
} else if (preset == 2) { // 小体积
|
||||
frameStep = 3;
|
||||
scale = 0.7;
|
||||
encodeFps = 10;
|
||||
}
|
||||
|
||||
QVector<QImage> framesToEncode;
|
||||
framesToEncode.reserve((m_frames.size() + frameStep - 1) / frameStep);
|
||||
for (int i = 0; i < m_frames.size(); i += frameStep) {
|
||||
QImage frame = m_frames[i];
|
||||
if (scale < 0.999) {
|
||||
const int dstW = qMax(2, int(frame.width() * scale));
|
||||
const int dstH = qMax(2, int(frame.height() * scale));
|
||||
frame = frame.scaled(dstW, dstH, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
|
||||
}
|
||||
framesToEncode.append(frame);
|
||||
}
|
||||
if (framesToEncode.isEmpty() && !m_frames.isEmpty()) {
|
||||
framesToEncode.append(m_frames.first());
|
||||
}
|
||||
|
||||
QString savePath = m_pendingSavePath;
|
||||
|
||||
QFuture<bool> future = QtConcurrent::run([framesToEncode, savePath]() -> bool {
|
||||
QFuture<bool> future = QtConcurrent::run([framesToEncode, savePath, encodeFps]() -> bool {
|
||||
GifEncoder encoder;
|
||||
bool ok = encoder.encode(framesToEncode, savePath, GifRecordViewModel::kFps, true);
|
||||
bool ok = encoder.encode(framesToEncode, savePath, encodeFps, true);
|
||||
if (!ok) {
|
||||
qDebug() << "[GifRecord] 编码错误:" << encoder.errorString();
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ class GifRecordViewModel : public QObject
|
||||
Q_PROPERTY(bool isEncoding READ isEncoding NOTIFY isEncodingChanged)
|
||||
Q_PROPERTY(int frameCount READ frameCount NOTIFY frameCountChanged)
|
||||
Q_PROPERTY(int elapsedSecs READ elapsedSecs NOTIFY elapsedSecsChanged)
|
||||
Q_PROPERTY(int qualityPreset READ qualityPreset WRITE setQualityPreset NOTIFY qualityPresetChanged)
|
||||
|
||||
public:
|
||||
explicit GifRecordViewModel(AppSettings& settings, QObject* parent = nullptr);
|
||||
@@ -45,6 +46,7 @@ public:
|
||||
bool isEncoding() const { return m_encoding; }
|
||||
int frameCount() const { return m_frames.size(); }
|
||||
int elapsedSecs() const { return m_elapsedSecs; }
|
||||
int qualityPreset() const { return m_settings.gifQualityPreset(); }
|
||||
|
||||
// QML 调用:开始录制,captureRect 为屏幕逻辑坐标选区
|
||||
Q_INVOKABLE void startRecording(int x, int y, int w, int h);
|
||||
@@ -57,6 +59,7 @@ public:
|
||||
|
||||
// QML 调用:取消录制,丢弃所有帧
|
||||
Q_INVOKABLE void cancelRecording();
|
||||
Q_INVOKABLE void setQualityPreset(int preset);
|
||||
|
||||
// 最大录制帧数(防止内存过大),默认 15fps × 30s = 450 帧
|
||||
static constexpr int kMaxFrames = 450;
|
||||
@@ -66,6 +69,7 @@ signals:
|
||||
void isEncodingChanged();
|
||||
void frameCountChanged();
|
||||
void elapsedSecsChanged();
|
||||
void qualityPresetChanged();
|
||||
|
||||
void encodingFinished(const QString& savedPath);
|
||||
void encodingFailed(const QString& error);
|
||||
|
||||
@@ -5,6 +5,10 @@
|
||||
#include <QClipboard>
|
||||
#include <QGuiApplication>
|
||||
#include <QDebug>
|
||||
#include <QDesktopServices>
|
||||
#include <QDir>
|
||||
#include <QFileInfo>
|
||||
#include <QUrl>
|
||||
|
||||
// ── 构造 ─────────────────────────────────────────────────
|
||||
OcrViewModel::OcrViewModel(QObject *parent)
|
||||
@@ -76,3 +80,55 @@ void OcrViewModel::copyResultToClipboard()
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
QString OcrViewModel::runSelfCheck()
|
||||
{
|
||||
const QStringList available = OcrEngine::availableTessdataDirs();
|
||||
const QStringList candidates = OcrEngine::tessdataSearchCandidates();
|
||||
|
||||
QString report;
|
||||
if (!available.isEmpty()) {
|
||||
report += tr("OCR 环境检查:通过\n");
|
||||
report += tr("可用 tessdata:\n");
|
||||
for (const QString &dir : available) {
|
||||
report += QStringLiteral(" - %1\n").arg(QDir::toNativeSeparators(dir));
|
||||
}
|
||||
} else {
|
||||
report += tr("OCR 环境检查:未通过(缺少 chi_sim/eng 语言包)\n");
|
||||
report += tr("建议把以下文件放到目录中:\n");
|
||||
report += QStringLiteral(" - chi_sim.traineddata\n");
|
||||
report += QStringLiteral(" - eng.traineddata\n");
|
||||
report += tr("候选 tessdata 目录:\n");
|
||||
for (const QString &dir : candidates) {
|
||||
report += QStringLiteral(" - %1\n").arg(QDir::toNativeSeparators(dir));
|
||||
}
|
||||
}
|
||||
|
||||
OcrEngine engine;
|
||||
if (engine.isReady()) {
|
||||
report += tr("引擎初始化:成功");
|
||||
} else {
|
||||
report += tr("引擎初始化:失败\n");
|
||||
report += tr("错误信息:%1").arg(engine.lastError());
|
||||
}
|
||||
|
||||
m_selfCheckText = report.trimmed();
|
||||
emit selfCheckTextChanged();
|
||||
return m_selfCheckText;
|
||||
}
|
||||
|
||||
bool OcrViewModel::openTessdataFolder()
|
||||
{
|
||||
QString dirPath = OcrEngine::suggestedTessdataDir();
|
||||
if (dirPath.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
QDir dir(dirPath);
|
||||
if (!dir.exists()) {
|
||||
if (!QDir().mkpath(dirPath)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return QDesktopServices::openUrl(QUrl::fromLocalFile(QFileInfo(dirPath).absoluteFilePath()));
|
||||
}
|
||||
@@ -17,6 +17,7 @@ class OcrViewModel : public QObject
|
||||
|
||||
Q_PROPERTY(bool isRecognizing READ isRecognizing NOTIFY isRecognizingChanged)
|
||||
Q_PROPERTY(QString resultText READ resultText NOTIFY resultTextChanged)
|
||||
Q_PROPERTY(QString selfCheckText READ selfCheckText NOTIFY selfCheckTextChanged)
|
||||
|
||||
public:
|
||||
explicit OcrViewModel(QObject *parent = nullptr);
|
||||
@@ -24,6 +25,7 @@ public:
|
||||
|
||||
bool isRecognizing() const { return m_isRecognizing; }
|
||||
QString resultText() const { return m_resultText; }
|
||||
QString selfCheckText() const { return m_selfCheckText; }
|
||||
|
||||
// 提交识别任务(供 QML 调用)
|
||||
Q_INVOKABLE void recognize(const QImage &image);
|
||||
@@ -31,9 +33,16 @@ public:
|
||||
// 将结果文本复制到系统剪贴板
|
||||
Q_INVOKABLE void copyResultToClipboard();
|
||||
|
||||
// OCR 环境一键自检(返回文本可直接展示到 UI)
|
||||
Q_INVOKABLE QString runSelfCheck();
|
||||
|
||||
// 打开 tessdata 目录(若目录不存在会尝试创建)
|
||||
Q_INVOKABLE bool openTessdataFolder();
|
||||
|
||||
signals:
|
||||
void isRecognizingChanged();
|
||||
void resultTextChanged();
|
||||
void selfCheckTextChanged();
|
||||
void resultReady(const QString &text);
|
||||
void recognizeFailed(const QString &errorMessage);
|
||||
|
||||
@@ -43,6 +52,7 @@ private slots:
|
||||
private:
|
||||
bool m_isRecognizing = false;
|
||||
QString m_resultText;
|
||||
QString m_selfCheckText;
|
||||
|
||||
QFutureWatcher<QString> m_watcher;
|
||||
};
|
||||
|
||||
Reference in new issue
Block a user