Files
change_code/main.cpp
T
2026-09-22 15:15:22 +08:00

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;
}