237 lines
9.8 KiB
C++
237 lines
9.8 KiB
C++
#include "src/mainwindow/mainwindow.h"
|
|
#include "ElaApplication.h"
|
|
#include "ElaTheme.h"
|
|
#include "src/utils/configmanager.h"
|
|
#include "src/utils/logmanager.h"
|
|
#include "src/utils/Constants.h"
|
|
#include "src/utils/version.h"
|
|
#include <QApplication>
|
|
#include <QElapsedTimer>
|
|
#include <QIcon>
|
|
#include <QCoreApplication>
|
|
#include <QEvent>
|
|
#include <QKeyEvent>
|
|
#include <QClipboard>
|
|
#include <QKeySequence>
|
|
#include <QLabel>
|
|
#include <QAbstractItemView>
|
|
#include <QModelIndex>
|
|
#include <QSplashScreen>
|
|
#include <QPainter>
|
|
#include <QScreen>
|
|
#include <QSettings>
|
|
#include <QLockFile>
|
|
#include <QMessageBox>
|
|
#include <QStandardPaths>
|
|
#ifdef Q_OS_WIN
|
|
#include <windows.h>
|
|
#include <dwmapi.h>
|
|
#endif
|
|
#include <algorithm>
|
|
|
|
// 全局文字可复制过滤器:
|
|
// 1) 所有 QLabel(含 ElaText)文字可用鼠标选中并复制;
|
|
// 2) 表格/树(QAbstractItemView)选中的单元格支持 Ctrl+C 复制,制表符分隔列、换行分隔行。
|
|
class TextSelectableFilter : public QObject
|
|
{
|
|
public:
|
|
using QObject::QObject;
|
|
|
|
protected:
|
|
bool eventFilter(QObject *watched, QEvent *event) override
|
|
{
|
|
if (event->type() == QEvent::Polish) {
|
|
if (QLabel *label = qobject_cast<QLabel *>(watched)) {
|
|
// 跳过用作图标按钮的 QLabel(有 pixmap 无文本),
|
|
// 否则会被强制设为竖线(IBeamCursor),影响点击类图标控件
|
|
if (label->pixmap().isNull()) {
|
|
label->setTextInteractionFlags(label->textInteractionFlags()
|
|
| Qt::TextSelectableByMouse);
|
|
label->setCursor(Qt::IBeamCursor);
|
|
}
|
|
}
|
|
} else if (event->type() == QEvent::KeyPress) {
|
|
if (auto *view = qobject_cast<QAbstractItemView *>(watched)) {
|
|
auto *ke = static_cast<QKeyEvent *>(event);
|
|
if (ke->matches(QKeySequence::Copy)) {
|
|
return copySelectedCells(view);
|
|
}
|
|
}
|
|
}
|
|
return QObject::eventFilter(watched, event);
|
|
}
|
|
|
|
private:
|
|
static bool copySelectedCells(QAbstractItemView *view)
|
|
{
|
|
if (!view->selectionModel()) {
|
|
return false;
|
|
}
|
|
QModelIndexList idxs = view->selectionModel()->selectedIndexes();
|
|
if (idxs.isEmpty()) {
|
|
return false;
|
|
}
|
|
// 按行、列排序,保证复制顺序与显示一致
|
|
std::sort(idxs.begin(), idxs.end(),
|
|
[](const QModelIndex &a, const QModelIndex &b) {
|
|
if (a.row() != b.row()) {
|
|
return a.row() < b.row();
|
|
}
|
|
return a.column() < b.column();
|
|
});
|
|
|
|
QString text;
|
|
int lastRow = -1;
|
|
for (const QModelIndex &idx : idxs) {
|
|
if (idx.row() != lastRow) {
|
|
if (lastRow != -1) {
|
|
text += QLatin1Char('\n');
|
|
}
|
|
lastRow = idx.row();
|
|
} else {
|
|
text += QLatin1Char('\t');
|
|
}
|
|
text += idx.data(Qt::DisplayRole).toString();
|
|
}
|
|
if (!text.isEmpty()) {
|
|
QApplication::clipboard()->setText(text);
|
|
return true; // 已处理,消费该事件
|
|
}
|
|
return false;
|
|
}
|
|
};
|
|
|
|
int main(int argc, char *argv[])
|
|
{
|
|
// 启动分段计时: 每段耗时写入日志(带累计值), 用于定位启动慢的具体环节
|
|
QElapsedTimer startupTimer;
|
|
startupTimer.start();
|
|
auto mark = [&startupTimer](const char *stage) {
|
|
LogManager::instance()->logInfo(
|
|
QString("[启动计时] %1 - 累计 %2 ms").arg(QString::fromUtf8(stage)).arg(startupTimer.elapsed()));
|
|
};
|
|
|
|
QApplication a(argc, argv);
|
|
a.setWindowIcon(QIcon(":resources/images/ChangeCode.png"));
|
|
|
|
// 日志初始化提前到单实例检测之前: 双实例退出路径的警告必须能落盘,
|
|
// 否则 writeLog 的文件分支静默跳过, "检测到已有实例"只上控制台无痕迹。
|
|
// QApplication 已就绪, initLogConfig 依赖的 applicationDirPath 可用
|
|
LogManager::instance()->initLogConfig();
|
|
LogManager::instance()->initLogFile();
|
|
|
|
// 单实例保护: 双开会并发互写日志文件、并发覆盖 config.ini, 还会重复弹出升级提示。
|
|
// QLockFile 持有方进程崩溃后残留锁可被下次启动自动接管(按锁内 PID 探测存活)
|
|
const QString lockPath = QStandardPaths::writableLocation(QStandardPaths::TempLocation)
|
|
+ "/UFT30ChangeCode.lock";
|
|
QLockFile lockFile(lockPath);
|
|
if (!lockFile.tryLock(0)) {
|
|
LogManager::instance()->logWarning("检测到已有实例在运行, 本次启动退出");
|
|
QMessageBox::warning(nullptr, "程序已在运行",
|
|
"UFT30 Change Code 已经在运行中,请勿重复启动。");
|
|
return 0;
|
|
}
|
|
|
|
// 启动画面: main() 最先显示, 覆盖后续主窗口首帧的小几何闪现("跳动框"),
|
|
// 主窗口显示完成后由 finish() 自动关闭
|
|
QPixmap splashPix(640, 400);
|
|
splashPix.fill(QColor(0x27, 0x43, 0x5B));
|
|
{
|
|
QPainter p(&splashPix);
|
|
p.setRenderHint(QPainter::Antialiasing);
|
|
QPixmap icon(":resources/images/ChangeCode.png");
|
|
if (!icon.isNull()) {
|
|
p.drawPixmap((640 - 128) / 2, 96, 128, 128, icon);
|
|
}
|
|
QFont titleFont = p.font();
|
|
titleFont.setPixelSize(30);
|
|
titleFont.setBold(true);
|
|
p.setFont(titleFont);
|
|
p.setPen(Qt::white);
|
|
p.drawText(QRect(0, 248, 640, 44), Qt::AlignHCenter,
|
|
QString("UFT30 Change Code %1").arg(Version::getVersionString()));
|
|
QFont subFont = p.font();
|
|
subFont.setPixelSize(16);
|
|
p.setFont(subFont);
|
|
p.drawText(QRect(0, 302, 640, 28), Qt::AlignHCenter, QString("正在启动..."));
|
|
}
|
|
QSplashScreen splash(splashPix);
|
|
// 置顶显示: 启动画面默认非 topmost, 期间出现的任何窗口(主窗口半透明首帧、
|
|
// 系统提示等)都可能绘制到它之上形成"残影", 置顶后可完全覆盖
|
|
splash.setWindowFlag(Qt::WindowStaysOnTopHint, true);
|
|
splash.show();
|
|
// 排除用户输入事件: 启动阶段积压的鼠标/键盘事件若被分发, 可能触发意料之外的
|
|
// 焦点/点击行为(例如误点即将出现的窗口), 只处理绘制类事件保证 splash 正常上屏
|
|
a.processEvents(QEventLoop::ExcludeUserInputEvents);
|
|
// 强制全量同步重绘: show 后首个 paint 可能只覆盖部分暴露区域(分块 flush),
|
|
// 而后续 MainWindow 构造期约 340ms 事件循环饥饿, 未 flush 的区域保持
|
|
// 未初始化白色, 表现为启动画面"上半蓝、下半白"残影
|
|
splash.update();
|
|
splash.repaint();
|
|
|
|
// 安装全局过滤器:所有页面文字 / 表格树单元格允许用户复制
|
|
TextSelectableFilter textSelectableFilter;
|
|
a.installEventFilter(&textSelectableFilter);
|
|
|
|
mark("QApplication 就绪");
|
|
|
|
eApp->init();
|
|
mark("ElaApplication::init 完成");
|
|
|
|
// 应用上次保存的主题模式(未保存过时为夜间模式, 与 Ela 默认一致):
|
|
// 必须在主窗口构造前设置, 保证主窗口首帧即为正确主题, 避免启动后闪色。
|
|
// 此前主题从未被应用(设置页 onThemeChanged 为空), 界面始终是 Ela 默认夜间模式
|
|
{
|
|
QSettings settings(Constants::AppSettings::Org, Constants::AppSettings::App);
|
|
const QString theme = settings.value(Constants::AppSettings::ThemeKey,
|
|
Constants::AppSettings::ThemeDark).toString();
|
|
ElaTheme::getInstance()->setThemeMode(
|
|
theme == Constants::AppSettings::ThemeLight ? ElaThemeType::Light : ElaThemeType::Dark);
|
|
mark("主题模式应用完成");
|
|
}
|
|
|
|
ConfigManager::instance()->loadConfig();
|
|
mark("config.ini 加载完成");
|
|
|
|
int exitCode = 0;
|
|
// 启动异常兜底: MainWindow 构造涉及 8 页面 + 缓存库 + 索引扫描, 任何一环抛出
|
|
// 未捕获异常都会直接 std::terminate 无提示退出; 这里统一转弹窗 + 日志后有序退出
|
|
try {
|
|
MainWindow w;
|
|
mark("MainWindow 构造完成(8 页面 + 缓存库)");
|
|
|
|
// show 前把窗口几何预设为屏幕可用区域: 使 native 窗口创建即为最大化尺寸,
|
|
// 避免 showMaximized 首帧从默认小尺寸几何跳变到最大化("跳动框")
|
|
w.setGeometry(QGuiApplication::primaryScreen()->availableGeometry());
|
|
|
|
#ifdef Q_OS_WIN
|
|
// 禁用 DWM 窗口过渡动画: 最大化显示时不再从旧几何帧动画跳变, 消除首帧"闪动框"。
|
|
// 必须放在 setGeometry 之后: winId() 会提前创建 native 窗口, 若在几何预设前调用,
|
|
// native 窗口将以默认小尺寸创建, setGeometry 的预设效果被削弱
|
|
HWND hwnd = reinterpret_cast<HWND>(w.winId());
|
|
BOOL disableTransitions = TRUE;
|
|
DwmSetWindowAttribute(hwnd, DWMWA_TRANSITIONS_FORCEDISABLED,
|
|
&disableTransitions, sizeof(disableTransitions));
|
|
#endif
|
|
|
|
w.showMaximized();
|
|
mark("窗口已显示");
|
|
splash.finish(&w); // 主窗口已上屏, 关闭启动画面
|
|
exitCode = a.exec();
|
|
} catch (const std::exception &e) {
|
|
splash.close();
|
|
LogManager::instance()->logError(
|
|
QString("启动阶段未捕获异常: %1").arg(QString::fromUtf8(e.what())));
|
|
QMessageBox::critical(nullptr, "启动失败",
|
|
QString("程序初始化发生异常: %1\n详情请查看日志文件。").arg(QString::fromUtf8(e.what())));
|
|
exitCode = 1;
|
|
} catch (...) {
|
|
splash.close();
|
|
LogManager::instance()->logError("启动阶段未捕获未知异常");
|
|
QMessageBox::critical(nullptr, "启动失败",
|
|
"程序初始化发生未知异常。\n详情请查看日志文件。");
|
|
exitCode = 1;
|
|
}
|
|
return exitCode;
|
|
}
|