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:
parent
a3130d0d2a
commit
165c48c677
@ -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);
|
||||
|
||||
72
src/main.cpp
72
src/main.cpp
@ -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();
|
||||
}
|
||||
|
||||
@ -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_);
|
||||
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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="停止录音"] {
|
||||
|
||||
@ -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);
|
||||
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -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));
|
||||
|
||||
Loading…
Reference in New Issue
Block a user