#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 #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef Q_OS_WIN #include #include #endif #include // 全局文字可复制过滤器: // 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(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(watched)) { auto *ke = static_cast(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(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; }