fix: 修复 Windows 上 Qt 内部 200x200 工具窗口(空白方块)问题

通过 EnumWindows 枚举并隐藏 Qt 在 Windows 上创建的 WS_EX_TOOLWINDOW
工具窗口(无标题栏、无边框),解决启动时出现的空白方块问题。

同时包含:
- Windows 使用原生 windows 风格替代 Fusion,避免渲染问题
- 托盘图标改为圆形轮廓,AudioWaveform 背景主题适配
- QSS 完善(MenuBar、录音按钮动态属性、StackedWidget 背景)
- 内联样式表改为 objectName/dynamic property,QSS 统一管理
- 日志记录版本信息、编译时间、Qt 版本、平台信息

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
impressionyang 2026-06-12 09:21:18 +08:00
parent a3130d0d2a
commit 165c48c677
9 changed files with 236 additions and 17 deletions

View File

@ -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<ConfigManager>(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);

View File

@ -8,6 +8,56 @@
#include <QStandardPaths>
#include <QCommandLineParser>
#include <QApplication>
#include <QTimer>
#ifdef Q_OS_WIN
#include <windows.h>
#include <string>
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();
}

View File

@ -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_);

View File

@ -21,6 +21,50 @@
#include <QCloseEvent>
#include <QStyle>
#include <QIcon>
#include <QTimer>
#include <QTabBar>
#ifdef Q_OS_WIN
#include <windows.h>
// 枚举并隐藏 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) {

View File

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

View File

@ -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="停止录音"] {

View File

@ -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);

View File

@ -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() {

View File

@ -1,4 +1,5 @@
#include "audio_waveform.h"
#include "app/application.h"
#include <QPainter>
#include <QPainterPath>
#include <algorithm>
@ -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));