diff --git a/src/app/application.cpp b/src/app/application.cpp index 08c25f1..49fec5b 100644 --- a/src/app/application.cpp +++ b/src/app/application.cpp @@ -21,9 +21,6 @@ static const char* const kTag = "Application"; Application::Application(int& argc, char** argv) : QApplication(argc, argv) { - LOG_INFO(kTag, QString("Impress Voice Input v%1 启动").arg(applicationVersion())); - LOG_INFO(kTag, QString("编译时间: %1 %2").arg(__DATE__).arg(__TIME__)); - configManager_ = std::make_unique(this); configManager_->loadDefaults(); @@ -84,8 +81,13 @@ void Application::loadGlobalModel() { void Application::applyTheme(const QString& theme) { s_currentTheme = theme; - // 1. 先设置风格(必须在 palette 和 stylesheet 之前) +#ifdef Q_OS_WIN + // Windows 使用原生风格,避免 Fusion 的渲染问题 + qApp->setStyle(QStyleFactory::create("windows")); +#else + // 其他平台使用 Fusion qApp->setStyle(QStyleFactory::create("Fusion")); +#endif // 2. 设置调色板 QPalette palette; @@ -153,7 +155,7 @@ void Application::applyFontSize(int size) { QIcon Application::createTrayIcon(bool active) { const QColor color = (s_currentTheme == "dark") ? Qt::white : Qt::black; - const int size = 16; + const int size = 22; QPixmap pixmap(size, size); pixmap.fill(Qt::transparent); @@ -164,16 +166,19 @@ QIcon Application::createTrayIcon(bool active) { if (active) { // 播放图标(三角形) - const int margin = 3; + const int margin = 4; QPolygon triangle; triangle << QPoint(margin, margin) << QPoint(margin, size - margin) << QPoint(size - margin, size / 2); painter.drawPolygon(triangle); } else { - // 停止图标(正方形) - const int margin = 3; - painter.drawRect(margin, margin, size - 2 * margin, size - 2 * margin); + // 空闲图标(圆形轮廓,而非实心方块) + const int margin = 4; + const int radius = (size - 2 * margin) / 2; + const int cx = size / 2; + const int cy = size / 2; + painter.drawEllipse(QPoint(cx, cy), radius, radius); } return QIcon(pixmap); diff --git a/src/main.cpp b/src/main.cpp index 55f108d..730824e 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -8,6 +8,56 @@ #include #include #include +#include + +#ifdef Q_OS_WIN +#include +#include + +static BOOL CALLBACK EnumWindowsProc(HWND hwnd, LPARAM lParam) { + DWORD pid; + GetWindowThreadProcessId(hwnd, &pid); + if (pid != GetCurrentProcessId()) return TRUE; + + wchar_t title[256]; + GetWindowTextW(hwnd, title, 256); + + RECT rect; + GetWindowRect(hwnd, &rect); + + LONG style = GetWindowLong(hwnd, GWL_STYLE); + LONG exStyle = GetWindowLong(hwnd, GWL_EXSTYLE); + + bool hasTitleBar = (style & WS_CAPTION) != 0; + bool hasBorder = (style & WS_BORDER) != 0; + bool isChild = (style & WS_CHILD) != 0; + bool isVisible = IsWindowVisible(hwnd); + bool isTool = (exStyle & WS_EX_TOOLWINDOW) != 0; + + std::wstring wTitle(title); + std::string utf8Title(wTitle.begin(), wTitle.end()); + + LOG_INFO("WindowEnum", QString("HWND=%1 标题=\"%2\" 位置=[%3,%4,%5,%6] 大小=%7x%8 " + "可见=%9 标题栏=%10 边框=%11 子窗口=%12 工具窗=%13") + .arg((qulonglong)hwnd) + .arg(QString::fromStdWString(wTitle)) + .arg(rect.left).arg(rect.top).arg(rect.right).arg(rect.bottom) + .arg(rect.right - rect.left).arg(rect.bottom - rect.top) + .arg(isVisible ? "Y" : "N") + .arg(hasTitleBar ? "Y" : "N") + .arg(hasBorder ? "Y" : "N") + .arg(isChild ? "Y" : "N") + .arg(isTool ? "Y" : "N")); + + return TRUE; +} + +static void dumpAllWindows() { + LOG_INFO("WindowEnum", "===== 枚举进程所有窗口 ====="); + EnumWindows(EnumWindowsProc, 0); + LOG_INFO("WindowEnum", "===== 窗口枚举结束 ====="); +} +#endif int main(int argc, char* argv[]) { @@ -59,6 +109,15 @@ int main(int argc, char* argv[]) QString logFilePath = effectiveLogDir + "/app.log"; impress::Logger::init(logFilePath); + LOG_INFO("Main", QString("=== Impress Voice Input v%1 ===").arg(app.applicationVersion())); + LOG_INFO("Main", QString("编译时间: %1 %2").arg(__DATE__).arg(__TIME__)); + LOG_INFO("Main", QString("Qt 版本: %1").arg(qVersion())); +#ifdef Q_OS_WIN + LOG_INFO("Main", "平台: Windows"); +#elif defined(Q_OS_LINUX) + LOG_INFO("Main", "平台: Linux"); +#endif + LOG_INFO("Main", QString("日志目录: %1").arg(effectiveLogDir)); // 命令行覆盖模型路径 @@ -81,5 +140,18 @@ int main(int argc, char* argv[]) impress::MainWindow mainWindow(configManager, app.sttEngine()); mainWindow.show(); +#ifdef Q_OS_WIN + // 延迟 1 秒后枚举所有窗口,确保所有 Qt 内部窗口都已创建 + QTimer::singleShot(1000, []() { + dumpAllWindows(); + }); + + // 3 秒后再次枚举,检测是否有延迟创建的窗口 + QTimer::singleShot(3000, []() { + LOG_INFO("WindowEnum", "===== 延迟 3 秒后再次枚举 ====="); + dumpAllWindows(); + }); +#endif + return app.exec(); } diff --git a/src/ui/file_transcribe_page.cpp b/src/ui/file_transcribe_page.cpp index 5ff9f0c..4810d9b 100644 --- a/src/ui/file_transcribe_page.cpp +++ b/src/ui/file_transcribe_page.cpp @@ -71,7 +71,7 @@ void FileTranscribePage::setupUI() { // 控制栏 auto* controlLayout = new QHBoxLayout(); startBtn_ = new QPushButton("开始转写", this); - startBtn_->setStyleSheet("QPushButton { font-weight: bold; padding: 8px 16px; }"); + startBtn_->setObjectName("startTranscribeBtn"); connect(startBtn_, &QPushButton::clicked, this, &FileTranscribePage::onStartTranscribe); controlLayout->addWidget(startBtn_); diff --git a/src/ui/main_window.cpp b/src/ui/main_window.cpp index d7ab01b..915167d 100644 --- a/src/ui/main_window.cpp +++ b/src/ui/main_window.cpp @@ -21,6 +21,50 @@ #include #include #include +#include +#include +#ifdef Q_OS_WIN +#include + +// 枚举并隐藏 Qt 创建的多余工具窗口(无标题栏、无边框、非主窗口的 WS_EX_TOOLWINDOW) +static BOOL CALLBACK HideQtToolWindows(HWND hwnd, LPARAM /*lParam*/) { + DWORD pid; + GetWindowThreadProcessId(hwnd, &pid); + if (pid != GetCurrentProcessId()) return TRUE; + + LONG exStyle = GetWindowLong(hwnd, GWL_EXSTYLE); + LONG style = GetWindowLong(hwnd, GWL_STYLE); + + // 只找 WS_EX_TOOLWINDOW 的窗口 + if ((exStyle & WS_EX_TOOLWINDOW) == 0) return TRUE; + + // 排除有标题栏的窗口 + bool hasTitleBar = (style & WS_CAPTION) != 0; + if (hasTitleBar) return TRUE; + + // 排除子窗口 + if (style & WS_CHILD) return TRUE; + + // 获取窗口标题 + wchar_t title[256]; + int len = GetWindowTextW(hwnd, title, 256); + + // 排除 Qt 标题栏窗口(_q_titlebar) + if (len > 0 && QString::fromWCharArray(title, len).startsWith("_q_")) return TRUE; + + // 找到了可疑窗口,隐藏它 + RECT rect; + GetWindowRect(hwnd, &rect); + LOG_INFO("MainWindow", QString("发现并隐藏 Qt 内部工具窗口: HWND=%1 标题=\"%2\" 大小=%3x%4") + .arg((qulonglong)hwnd) + .arg(len > 0 ? QString::fromWCharArray(title, len) : "(无标题)") + .arg(rect.right - rect.left) + .arg(rect.bottom - rect.top)); + + ShowWindow(hwnd, SW_HIDE); + return TRUE; +} +#endif static const char* const kTag = "MainWindow"; @@ -32,18 +76,33 @@ MainWindow::MainWindow(ConfigManager* configManager, : QMainWindow(parent) , configManager_(configManager) { + LOG_INFO(kTag, "MainWindow 构造函数开始"); + setWindowTitle("Impress Voice Input"); resize(1000, 700); // 设置窗口图标 setWindowIcon(QIcon(":/icons/app_icon.png")); + LOG_INFO(kTag, "窗口图标已设置"); + LOG_INFO(kTag, "开始 setupUI"); setupUI(sttEngine); + LOG_INFO(kTag, "setupUI 完成"); + + LOG_INFO(kTag, "开始 setupMenuBar"); setupMenuBar(); + LOG_INFO(kTag, "setupMenuBar 完成"); + + LOG_INFO(kTag, "开始 setupStatusBar"); setupStatusBar(sttEngine); + LOG_INFO(kTag, "setupStatusBar 完成"); + + LOG_INFO(kTag, "开始 setupTrayIcon"); setupTrayIcon(); + LOG_INFO(kTag, "setupTrayIcon 完成"); // 初始化语音输入服务(共享全局引擎) + LOG_INFO(kTag, "开始创建 VoiceInputService"); voiceInputService_ = new VoiceInputService(configManager_, sttEngine, this); connect(voiceInputService_, &VoiceInputService::statusChanged, this, [this](const QString& status) { @@ -58,13 +117,25 @@ MainWindow::MainWindow(ConfigManager* configManager, this, [this](const QString& text) { LOG_INFO(kTag, QString("语音识别结果: %1").arg(text)); }); + LOG_INFO(kTag, "VoiceInputService 已创建"); // 监听配置变化,动态启停语音输入服务 connect(configManager_, &ConfigManager::configChanged, this, &MainWindow::onVoiceInputConfigChanged); // 启动时检查配置 + LOG_INFO(kTag, "开始 onVoiceInputConfigChanged"); onVoiceInputConfigChanged(); + LOG_INFO(kTag, "onVoiceInputConfigChanged 完成"); + +#ifdef Q_OS_WIN + // 延迟隐藏 Qt 在 Windows 上创建的额外工具窗口 + QTimer::singleShot(500, this, []() { + LOG_INFO(kTag, "开始检查 Qt 内部工具窗口"); + EnumWindows(HideQtToolWindows, 0); + LOG_INFO(kTag, "Qt 内部工具窗口检查完成"); + }); +#endif LOG_INFO(kTag, "主窗口已创建"); } @@ -72,17 +143,35 @@ MainWindow::MainWindow(ConfigManager* configManager, MainWindow::~MainWindow() = default; void MainWindow::setupUI(SenseVoiceEngine* sttEngine) { + LOG_INFO(kTag, "setupUI: 创建 QTabWidget"); tabWidget_ = new QTabWidget(this); + // 禁用可能导致额外窗口的功能 + tabWidget_->setDocumentMode(true); + tabWidget_->setTabBarAutoHide(false); + tabWidget_->tabBar()->setExpanding(true); + tabWidget_->tabBar()->setMovable(false); + tabWidget_->tabBar()->setDrawBase(true); + + LOG_INFO(kTag, "setupUI: 创建 STTTestPage"); sttPage_ = new STTTestPage(configManager_, sttEngine, tabWidget_); + LOG_INFO(kTag, "setupUI: STTTestPage 创建完成"); + + LOG_INFO(kTag, "setupUI: 创建 FileTranscribePage"); transcribePage_ = new FileTranscribePage(configManager_, sttEngine, tabWidget_); + LOG_INFO(kTag, "setupUI: FileTranscribePage 创建完成"); + + LOG_INFO(kTag, "setupUI: 创建 SettingsPage"); settingsPage_ = new SettingsPage(configManager_, tabWidget_); + LOG_INFO(kTag, "setupUI: SettingsPage 创建完成"); tabWidget_->addTab(sttPage_, "实时语音识别"); tabWidget_->addTab(transcribePage_, "音频文件转写"); tabWidget_->addTab(settingsPage_, "配置"); + LOG_INFO(kTag, "setupUI: Tab 页面已添加"); setCentralWidget(tabWidget_); + LOG_INFO(kTag, "setupUI: 完成"); } void MainWindow::setupStatusBar(SenseVoiceEngine* sttEngine) { diff --git a/src/ui/resources/styles/main.qss b/src/ui/resources/styles/main.qss index d3d13ed..d9aa89b 100644 --- a/src/ui/resources/styles/main.qss +++ b/src/ui/resources/styles/main.qss @@ -13,6 +13,10 @@ QTabWidget::pane { background: #ffffff; } +QTabWidget QStackedWidget { + background: #ffffff; +} + QTabBar::tab { background: #f5f5f5; border: 1px solid #e0e0e0; @@ -70,6 +74,17 @@ QPushButton[text="保存配置"]:hover { background-color: #1565c0; } +/* 录音按钮(动态属性 recording=true) */ +QPushButton[recording="true"] { + background-color: #e53935; + color: #ffffff; + border: 1px solid #e53935; +} + +QPushButton[recording="true"]:hover { + background-color: #c62828; +} + /* 危险操作按钮 */ QPushButton[text="停止"], QPushButton[text="停止录音"] { @@ -241,8 +256,14 @@ QCheckBox::indicator:checked { QMenuBar { background-color: #ffffff; border-bottom: 1px solid #e0e0e0; - padding: 2px; + padding: 2px 4px; color: #333333; + spacing: 2px; +} + +QMenuBar::item { + background-color: transparent; + padding: 4px 8px; } QMenuBar::item:selected { @@ -250,6 +271,10 @@ QMenuBar::item:selected { color: #1976d2; } +QMenuBar::item:pressed { + background-color: #bbdefb; +} + QMenu { background-color: #ffffff; border: 1px solid #e0e0e0; diff --git a/src/ui/resources/styles/main_dark.qss b/src/ui/resources/styles/main_dark.qss index 6b737f8..ce01016 100644 --- a/src/ui/resources/styles/main_dark.qss +++ b/src/ui/resources/styles/main_dark.qss @@ -14,6 +14,10 @@ QTabWidget::pane { background: #2a2a2a; } +QTabWidget QStackedWidget { + background: #2a2a2a; +} + QTabBar::tab { background: #3a3a3a; border: 1px solid #555555; @@ -70,6 +74,17 @@ QPushButton[text="保存配置"]:hover { background-color: #5a9aff; } +/* 录音按钮(动态属性 recording=true) */ +QPushButton[recording="true"] { + background-color: #e74c3c; + color: #ffffff; + border: 1px solid #e74c3c; +} + +QPushButton[recording="true"]:hover { + background-color: #f05e50; +} + /* 危险操作按钮 */ QPushButton[text="停止"], QPushButton[text="停止录音"] { diff --git a/src/ui/settings_page.cpp b/src/ui/settings_page.cpp index 57d3a4d..a799dc3 100644 --- a/src/ui/settings_page.cpp +++ b/src/ui/settings_page.cpp @@ -44,6 +44,7 @@ void SettingsPage::setupUI() { scrollArea->setFrameShape(QFrame::NoFrame); auto* contentWidget = new QWidget(scrollArea); + contentWidget->setAttribute(Qt::WA_StyledBackground, true); auto* contentLayout = new QVBoxLayout(contentWidget); contentLayout->setContentsMargins(12, 8, 12, 8); contentLayout->setSpacing(12); @@ -221,7 +222,7 @@ void SettingsPage::setupUI() { btnLayout->setContentsMargins(12, 4, 12, 8); auto* saveBtn = new QPushButton("保存配置", btnBar); - saveBtn->setStyleSheet("QPushButton { font-weight: bold; padding: 8px 16px; }"); + saveBtn->setObjectName("saveBtn"); connect(saveBtn, &QPushButton::clicked, this, &SettingsPage::onSaveConfig); btnLayout->addWidget(saveBtn); diff --git a/src/ui/stt_test_page.cpp b/src/ui/stt_test_page.cpp index cf8600d..fb3eee4 100644 --- a/src/ui/stt_test_page.cpp +++ b/src/ui/stt_test_page.cpp @@ -63,8 +63,8 @@ void STTTestPage::setupUI() { auto* btnLayout = new QHBoxLayout(); recordBtn_ = new QPushButton("开始录音", this); + recordBtn_->setObjectName("recordBtn"); recordBtn_->setMinimumWidth(120); - recordBtn_->setStyleSheet("QPushButton { font-weight: bold; padding: 8px 16px; }"); connect(recordBtn_, &QPushButton::clicked, this, &STTTestPage::onToggleRecording); btnLayout->addWidget(recordBtn_); @@ -103,10 +103,17 @@ void STTTestPage::setupUI() { void STTTestPage::updateUIState() { recordBtn_->setText(isRecording_ ? "停止录音" : "开始录音"); - recordBtn_->setStyleSheet(isRecording_ - ? "QPushButton { font-weight: bold; padding: 8px 16px; background-color: #e74c3c; color: white; }" - : "QPushButton { font-weight: bold; padding: 8px 16px; }"); + recordBtn_->setProperty("recording", isRecording_); + recordBtn_->style()->unpolish(recordBtn_); + recordBtn_->style()->polish(recordBtn_); deviceCombo_->setEnabled(!isRecording_ && !isLoadingModel_); + + // 状态标签颜色 + if (isRecording_) { + statusLabel_->setStyleSheet("color: #e74c3c;"); + } else { + statusLabel_->setStyleSheet("color: gray;"); + } } void STTTestPage::onToggleRecording() { diff --git a/src/ui/widgets/audio_waveform.cpp b/src/ui/widgets/audio_waveform.cpp index efc7f94..1bf9ba0 100644 --- a/src/ui/widgets/audio_waveform.cpp +++ b/src/ui/widgets/audio_waveform.cpp @@ -1,4 +1,5 @@ #include "audio_waveform.h" +#include "app/application.h" #include #include #include @@ -24,8 +25,12 @@ void AudioWaveform::paintEvent(QPaintEvent* /*event*/) { QPainter painter(this); painter.setRenderHint(QPainter::Antialiasing); + // 根据主题选择背景色 + const QColor bgColor = Application::isDarkTheme() + ? QColor(42, 42, 42) : QColor(245, 245, 245); + // 背景 - painter.fillRect(rect(), QColor(245, 245, 245)); + painter.fillRect(rect(), bgColor); if (samples_.isEmpty()) { painter.setPen(QColor(180, 180, 180));