feat: 主题切换(light/dark QSS)、QSS资源编译修复、托盘图标主题色

- 新增 main_dark.qss 暗色主题样式表
- 使用 .qrc + add_executable 方式确保 QSS 资源正确编译
- Application::applyTheme 动态切换主题和样式表
- 托盘图标 light 主题黑色、dark 主题白色
- Settings 保存后实时应用主题/字体

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
impressionyang 2026-06-11 19:13:34 +08:00
parent f8173cd0c1
commit ae35404d26
8 changed files with 541 additions and 77 deletions

View File

@ -139,7 +139,9 @@ else()
list(APPEND HEADERS src/core/caps_lock_voice_hotkey.h src/core/wayland_text_injector.h)
add_compile_definitions(PLATFORM_LINUX)
endif()
add_executable(${PROJECT_NAME} ${SOURCES} ${HEADERS})
add_executable(${PROJECT_NAME} ${SOURCES} ${HEADERS}
src/ui/resources/styles/styles.qrc
)
target_include_directories(${PROJECT_NAME} PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/src
@ -197,15 +199,6 @@ if(WIN32)
endif()
endif()
# ============================================================================
#
# ============================================================================
#
qt_add_resources(${PROJECT_NAME} "styles"
PREFIX "/"
FILES
src/ui/resources/styles/main.qss
)
# ============================================================================
#

View File

@ -3,11 +3,21 @@
#include "core/sense_voice_engine.h"
#include "utils/logger.h"
#include <QFile>
static const char* const kTag = "Application";
#include <QPalette>
#include <QColor>
#include <QStyleFactory>
#include <QStyle>
#include <QFont>
#include <QIcon>
#include <QPixmap>
#include <QPainter>
namespace impress {
static QString s_currentTheme; // 跟踪当前主题
static const char* const kTag = "Application";
Application::Application(int& argc, char** argv)
: QApplication(argc, argv)
{
@ -71,4 +81,88 @@ void Application::loadGlobalModel() {
sttEngine_->loadModelAsync(modelPath, tokensPath, device, numThreads);
}
void Application::applyTheme(const QString& theme) {
s_currentTheme = theme;
// 1. 先设置风格(必须在 palette 和 stylesheet 之前)
qApp->setStyle(QStyleFactory::create("Fusion"));
// 2. 设置调色板
QPalette palette;
if (theme == "dark") {
palette.setColor(QPalette::Window, QColor(53, 53, 53));
palette.setColor(QPalette::WindowText, Qt::white);
palette.setColor(QPalette::Base, QColor(25, 25, 25));
palette.setColor(QPalette::AlternateBase, QColor(53, 53, 53));
palette.setColor(QPalette::ToolTipBase, Qt::white);
palette.setColor(QPalette::ToolTipText, Qt::white);
palette.setColor(QPalette::Text, Qt::white);
palette.setColor(QPalette::Button, QColor(53, 53, 53));
palette.setColor(QPalette::ButtonText, Qt::white);
palette.setColor(QPalette::BrightText, Qt::cyan);
palette.setColor(QPalette::Link, QColor(42, 130, 218));
palette.setColor(QPalette::Highlight, QColor(42, 130, 218));
palette.setColor(QPalette::HighlightedText, Qt::black);
palette.setColor(QPalette::Disabled, QPalette::Text, QColor(127, 127, 127));
palette.setColor(QPalette::Disabled, QPalette::ButtonText, QColor(127, 127, 127));
palette.setColor(QPalette::Disabled, QPalette::WindowText, QColor(127, 127, 127));
palette.setColor(QPalette::Disabled, QPalette::Highlight, QColor(80, 80, 80));
palette.setColor(QPalette::Disabled, QPalette::HighlightedText, QColor(127, 127, 127));
} else {
palette = qApp->style()->standardPalette();
}
qApp->setPalette(palette);
// 3. 最后设置样式表(覆盖 palette
const QString qssPath = (theme == "dark") ? ":/styles/main_dark.qss" : ":/styles/main.qss";
QFile styleFile(qssPath);
if (styleFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
qApp->setStyleSheet(styleFile.readAll());
styleFile.close();
} else {
LOG_ERROR("Theme", QString("无法加载样式表: %1").arg(qssPath));
}
LOG_INFO("Theme", QString("主题已切换: %1").arg(theme));
}
void Application::applyFontSize(int size) {
QFont font = qApp->font();
font.setPointSize(size);
qApp->setFont(font);
LOG_INFO("Theme", QString("字体大小已设置: %1").arg(size));
}
QIcon Application::createTrayIcon(bool active) {
const QColor color = (s_currentTheme == "dark") ? Qt::white : Qt::black;
const int size = 16;
QPixmap pixmap(size, size);
pixmap.fill(Qt::transparent);
QPainter painter(&pixmap);
painter.setRenderHint(QPainter::Antialiasing);
painter.setPen(Qt::NoPen);
painter.setBrush(color);
if (active) {
// 播放图标(三角形)
const int margin = 3;
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);
}
return QIcon(pixmap);
}
bool Application::isDarkTheme() {
return s_currentTheme == "dark";
}
} // namespace impress

View File

@ -35,6 +35,18 @@ public:
/** @brief 加载全局模型(在配置加载后手动调用) */
void loadGlobalModel();
/** @brief 应用主题light/dark可在运行时调用 */
static void applyTheme(const QString& theme);
/** @brief 应用全局字体大小 */
static void applyFontSize(int size);
/** @brief 生成托盘图标light=黑色dark=白色) */
static QIcon createTrayIcon(bool active);
/** @brief 当前主题是否为 dark */
static bool isDarkTheme();
signals:
/** @brief 模型加载中(带路径) */
void modelLoading(const QString& modelPath);

View File

@ -7,6 +7,7 @@
#include <QDir>
#include <QStandardPaths>
#include <QCommandLineParser>
#include <QApplication>
int main(int argc, char* argv[])
{
@ -66,6 +67,13 @@ int main(int argc, char* argv[])
configManager->set("stt.model_path", modelPath);
}
// 应用主题和字体
QString theme = configManager->get("ui.theme").toString();
int fontSize = configManager->get("ui.font_size").toInt();
impress::Application::applyTheme(theme);
if (fontSize > 0) impress::Application::applyFontSize(fontSize);
LOG_INFO("Main", QString("主题: %1, 字体: %2").arg(theme).arg(fontSize));
// 配置加载完成后,启动全局模型加载
app.loadGlobalModel();

View File

@ -5,6 +5,7 @@
#include "core/voice_input_service.h"
#include "core/sense_voice_engine.h"
#include "app/config_manager.h"
#include "app/application.h"
#include "utils/logger.h"
#include <QMenuBar>
@ -37,7 +38,6 @@ MainWindow::MainWindow(ConfigManager* configManager,
setupMenuBar();
setupStatusBar(sttEngine);
setupTrayIcon();
loadStyleSheet();
// 初始化语音输入服务(共享全局引擎)
voiceInputService_ = new VoiceInputService(configManager_, sttEngine, this);
@ -126,9 +126,9 @@ void MainWindow::setupTrayIcon() {
trayIcon_ = new QSystemTrayIcon(this);
trayIcon_->setContextMenu(trayMenu_);
// 默认状态:停止图标SP_MediaStop
idleIcon_ = style()->standardIcon(QStyle::SP_MediaStop);
activeIcon_ = style()->standardIcon(QStyle::SP_MediaPlay);
// 默认状态:根据主题颜色生成图标
idleIcon_ = Application::createTrayIcon(false);
activeIcon_ = Application::createTrayIcon(true);
trayIcon_->setIcon(idleIcon_);
trayIcon_->setToolTip("Impress Voice Input - 语音输入就绪");
@ -236,6 +236,17 @@ void MainWindow::updateModelStatus() {
void MainWindow::onVoiceInputConfigChanged() {
if (!voiceInputService_) return;
// 动态应用主题和字体
QString theme = configManager_->get("ui.theme").toString();
int fontSize = configManager_->get("ui.font_size").toInt();
Application::applyTheme(theme);
if (fontSize > 0) Application::applyFontSize(fontSize);
// 刷新托盘图标颜色
idleIcon_ = Application::createTrayIcon(false);
activeIcon_ = Application::createTrayIcon(true);
updateTrayIcon("语音输入就绪");
// 更新模型状态显示
updateModelStatus();

View File

@ -1,143 +1,157 @@
/* Impress Voice Input - 全局样式表 */
/* Impress Voice Input - 亮色主题样式表 */
/* ========== 全局 ========== */
* {
font-family: "PingFang SC", "Microsoft YaHei", "Noto Sans CJK SC", sans-serif;
}
QMainWindow, QWidget {
QWidget {
background-color: #ffffff;
color: #1a1a1a;
}
/* 容器控件必须显式设置背景 */
QFrame {
background-color: #ffffff;
}
QScrollArea, QScrollArea > QWidget {
background-color: #ffffff;
color: #2c3e50;
}
/* ========== QTabWidget ========== */
QTabWidget::pane {
border: 1px solid #dcdfe6;
border-radius: 4px;
background: #fafafa;
border: 1px solid #e0e0e0;
border-radius: 6px;
background: #ffffff;
}
QTabBar::tab {
background: #f0f2f5;
border: 1px solid #dcdfe6;
background: #f5f5f5;
border: 1px solid #e0e0e0;
border-bottom: none;
border-top-left-radius: 4px;
border-top-right-radius: 4px;
border-top-left-radius: 6px;
border-top-right-radius: 6px;
padding: 10px 24px;
margin-right: 2px;
font-size: 14px;
color: #606266;
color: #666666;
}
QTabBar::tab:selected {
background: #ffffff;
border-bottom: 2px solid #409eff;
color: #409eff;
border-bottom: 2px solid #1976d2;
color: #1976d2;
font-weight: bold;
}
QTabBar::tab:hover {
color: #409eff;
color: #1976d2;
background: #e8eaf6;
}
/* ========== QPushButton ========== */
QPushButton {
background-color: #ffffff;
border: 1px solid #dcdfe6;
border: 1px solid #d0d0d0;
border-radius: 4px;
padding: 6px 16px;
color: #606266;
color: #333333;
font-size: 13px;
}
QPushButton:hover {
background-color: #ecf5ff;
border-color: #b3d8ff;
color: #409eff;
background-color: #e3f2fd;
border-color: #1976d2;
color: #1976d2;
}
QPushButton:pressed {
background-color: #e6f0ff;
background-color: #bbdefb;
}
/* 主要操作按钮 */
QPushButton[objectName="saveBtn"],
QPushButton[text="保存配置"] {
background-color: #409eff;
background-color: #1976d2;
color: #ffffff;
border: 1px solid #409eff;
border: 1px solid #1976d2;
}
QPushButton[objectName="saveBtn"]:hover,
QPushButton[text="保存配置"]:hover {
background-color: #66b1ff;
background-color: #1565c0;
}
/* 危险操作按钮 */
QPushButton[text="停止"],
QPushButton[text="停止录音"] {
background-color: #f56c6c;
background-color: #e53935;
color: #ffffff;
border: 1px solid #f56c6c;
border: 1px solid #e53935;
}
QPushButton[text="停止"]:hover,
QPushButton[text="停止录音"]:hover {
background-color: #f78989;
background-color: #c62828;
}
/* ========== QGroupBox ========== */
QGroupBox {
font-weight: bold;
border: 1px solid #dcdfe6;
border: 1px solid #e0e0e0;
border-radius: 6px;
margin-top: 12px;
padding-top: 16px;
color: #1a1a1a;
}
QGroupBox::title {
subcontrol-origin: margin;
subcontrol-position: top left;
padding: 0 8px;
color: #303133;
color: #1a1a1a;
}
/* ========== QLabel ========== */
QLabel {
color: #606266;
color: #333333;
background: none;
}
/* ========== QLineEdit ========== */
QLineEdit {
border: 1px solid #dcdfe6;
border: 1px solid #d0d0d0;
border-radius: 4px;
padding: 5px 10px;
background: #ffffff;
color: #1a1a1a;
}
QLineEdit:focus {
border-color: #409eff;
border-color: #1976d2;
}
QLineEdit[readOnly="true"] {
background-color: #f5f7fa;
background-color: #f5f5f5;
}
/* ========== QComboBox ========== */
QComboBox {
border: 1px solid #dcdfe6;
border: 1px solid #d0d0d0;
border-radius: 4px;
padding: 5px 10px;
background: #ffffff;
color: #1a1a1a;
min-width: 120px;
}
QComboBox:hover {
border-color: #c0c4cc;
border-color: #909090;
}
QComboBox:focus {
border-color: #409eff;
border-color: #1976d2;
}
QComboBox::drop-down {
@ -145,52 +159,62 @@ QComboBox::drop-down {
width: 24px;
}
QComboBox QAbstractItemView {
background-color: #ffffff;
color: #1a1a1a;
selection-background-color: #e3f2fd;
}
/* ========== QSpinBox / QDoubleSpinBox ========== */
QSpinBox, QDoubleSpinBox {
border: 1px solid #dcdfe6;
border: 1px solid #d0d0d0;
border-radius: 4px;
padding: 4px 8px;
background: #ffffff;
color: #1a1a1a;
}
QSpinBox:focus, QDoubleSpinBox:focus {
border-color: #409eff;
border-color: #1976d2;
}
/* ========== QProgressBar ========== */
QProgressBar {
border: 1px solid #dcdfe6;
border: 1px solid #e0e0e0;
border-radius: 4px;
text-align: center;
height: 20px;
background: #f5f7fa;
background: #f5f5f5;
color: #333333;
}
QProgressBar::chunk {
background: qlineargradient(x1:0, y1:0, x2:1, y2:0,
stop:0 #409eff, stop:1 #66b1ff);
stop:0 #1976d2, stop:1 #42a5f5);
border-radius: 3px;
}
/* ========== QTextEdit ========== */
QTextEdit {
border: 1px solid #dcdfe6;
border: 1px solid #d0d0d0;
border-radius: 4px;
padding: 8px;
background: #ffffff;
selection-background-color: #ecf5ff;
color: #1a1a1a;
selection-background-color: #e3f2fd;
}
QTextEdit:focus {
border-color: #409eff;
border-color: #1976d2;
}
/* ========== QListWidget ========== */
QListWidget {
border: 1px solid #dcdfe6;
border: 1px solid #e0e0e0;
border-radius: 4px;
background: #ffffff;
padding: 4px;
color: #1a1a1a;
}
QListWidget::item {
@ -199,58 +223,62 @@ QListWidget::item {
}
QListWidget::item:selected {
background-color: #ecf5ff;
color: #409eff;
background-color: #e3f2fd;
color: #1976d2;
}
QListWidget::item:hover {
background-color: #f5f7fa;
background-color: #f5f5f5;
}
/* ========== QCheckBox ========== */
QCheckBox {
spacing: 8px;
color: #333333;
}
QCheckBox::indicator {
width: 16px;
height: 16px;
border: 1px solid #dcdfe6;
border: 1px solid #d0d0d0;
border-radius: 3px;
background-color: #ffffff;
}
QCheckBox::indicator:checked {
background-color: #409eff;
border-color: #409eff;
background-color: #1976d2;
border-color: #1976d2;
}
/* ========== QMenu / QMenuBar ========== */
QMenuBar {
background-color: #ffffff;
border-bottom: 1px solid #dcdfe6;
border-bottom: 1px solid #e0e0e0;
padding: 2px;
color: #333333;
}
QMenuBar::item:selected {
background-color: #ecf5ff;
color: #409eff;
background-color: #e3f2fd;
color: #1976d2;
}
QMenu {
background-color: #ffffff;
border: 1px solid #dcdfe6;
border: 1px solid #e0e0e0;
border-radius: 4px;
padding: 4px;
color: #333333;
}
QMenu::item:selected {
background-color: #ecf5ff;
color: #409eff;
background-color: #e3f2fd;
color: #1976d2;
}
QMenu::separator {
height: 1px;
background-color: #ebeef5;
background-color: #e0e0e0;
margin: 4px 0;
}
@ -260,25 +288,25 @@ QMessageBox {
}
QMessageBox QLabel {
color: #303133;
color: #1a1a1a;
}
/* ========== 滚动条 ========== */
QScrollBar:vertical {
border: none;
background: #f5f7fa;
background: #f5f5f5;
width: 8px;
border-radius: 4px;
}
QScrollBar::handle:vertical {
background: #c0c4cc;
background: #bdbdbd;
border-radius: 4px;
min-height: 30px;
}
QScrollBar::handle:vertical:hover {
background: #909399;
background: #9e9e9e;
}
QScrollBar::add-line:vertical,

View File

@ -0,0 +1,312 @@
/* Impress Voice Input - 暗色主题样式表 */
/* ========== 全局 ========== */
* {
font-family: "PingFang SC", "Microsoft YaHei", "Noto Sans CJK SC", sans-serif;
}
QWidget {
background-color: #353535;
color: #ffffff;
}
QFrame {
background-color: #353535;
}
QScrollArea, QScrollArea > QWidget {
background-color: #2a2a2a;
}
/* ========== QTabWidget ========== */
QTabWidget::pane {
border: 1px solid #555555;
border-radius: 4px;
background: #2a2a2a;
}
QTabBar::tab {
background: #3a3a3a;
border: 1px solid #555555;
border-bottom: none;
border-top-left-radius: 4px;
border-top-right-radius: 4px;
padding: 10px 24px;
margin-right: 2px;
font-size: 14px;
color: #cccccc;
}
QTabBar::tab:selected {
background: #2a2a2a;
border-bottom: 2px solid #4286f4;
color: #4286f4;
font-weight: bold;
}
QTabBar::tab:hover {
color: #4286f4;
}
/* ========== QPushButton ========== */
QPushButton {
background-color: #353535;
border: 1px solid #555555;
border-radius: 4px;
padding: 6px 16px;
color: #cccccc;
font-size: 13px;
}
QPushButton:hover {
background-color: #4286f4;
border-color: #4286f4;
color: #ffffff;
}
QPushButton:pressed {
background-color: #3a6fd8;
}
/* 主要操作按钮 */
QPushButton[objectName="saveBtn"],
QPushButton[text="保存配置"] {
background-color: #4286f4;
color: #ffffff;
border: 1px solid #4286f4;
}
QPushButton[objectName="saveBtn"]:hover,
QPushButton[text="保存配置"]:hover {
background-color: #5a9aff;
}
/* 危险操作按钮 */
QPushButton[text="停止"],
QPushButton[text="停止录音"] {
background-color: #e74c3c;
color: #ffffff;
border: 1px solid #e74c3c;
}
QPushButton[text="停止"]:hover,
QPushButton[text="停止录音"]:hover {
background-color: #f05e50;
}
/* ========== QGroupBox ========== */
QGroupBox {
font-weight: bold;
border: 1px solid #555555;
border-radius: 6px;
margin-top: 12px;
padding-top: 16px;
color: #ffffff;
}
QGroupBox::title {
subcontrol-origin: margin;
subcontrol-position: top left;
padding: 0 8px;
color: #ffffff;
}
/* ========== QLabel ========== */
QLabel {
color: #cccccc;
}
/* ========== QLineEdit ========== */
QLineEdit {
border: 1px solid #555555;
border-radius: 4px;
padding: 5px 10px;
background: #1a1a1a;
color: #ffffff;
}
QLineEdit:focus {
border-color: #4286f4;
}
QLineEdit[readOnly="true"] {
background-color: #2a2a2a;
}
/* ========== QComboBox ========== */
QComboBox {
border: 1px solid #555555;
border-radius: 4px;
padding: 5px 10px;
background: #1a1a1a;
color: #ffffff;
min-width: 120px;
}
QComboBox:hover {
border-color: #888888;
}
QComboBox:focus {
border-color: #4286f4;
}
QComboBox::drop-down {
border: none;
width: 24px;
}
QComboBox QAbstractItemView {
background-color: #1a1a1a;
color: #ffffff;
selection-background-color: #4286f4;
}
/* ========== QSpinBox / QDoubleSpinBox ========== */
QSpinBox, QDoubleSpinBox {
border: 1px solid #555555;
border-radius: 4px;
padding: 4px 8px;
background: #1a1a1a;
color: #ffffff;
}
QSpinBox:focus, QDoubleSpinBox:focus {
border-color: #4286f4;
}
/* ========== QProgressBar ========== */
QProgressBar {
border: 1px solid #555555;
border-radius: 4px;
text-align: center;
height: 20px;
background: #2a2a2a;
color: #ffffff;
}
QProgressBar::chunk {
background: qlineargradient(x1:0, y1:0, x2:1, y2:0,
stop:0 #4286f4, stop:1 #5a9aff);
border-radius: 3px;
}
/* ========== QTextEdit ========== */
QTextEdit {
border: 1px solid #555555;
border-radius: 4px;
padding: 8px;
background: #1a1a1a;
color: #ffffff;
selection-background-color: #4286f4;
}
QTextEdit:focus {
border-color: #4286f4;
}
/* ========== QListWidget ========== */
QListWidget {
border: 1px solid #555555;
border-radius: 4px;
background: #1a1a1a;
padding: 4px;
color: #ffffff;
}
QListWidget::item {
padding: 8px;
border-radius: 4px;
}
QListWidget::item:selected {
background-color: #4286f4;
color: #ffffff;
}
QListWidget::item:hover {
background-color: #3a3a3a;
}
/* ========== QCheckBox ========== */
QCheckBox {
spacing: 8px;
color: #cccccc;
}
QCheckBox::indicator {
width: 16px;
height: 16px;
border: 1px solid #555555;
border-radius: 3px;
background-color: #1a1a1a;
}
QCheckBox::indicator:checked {
background-color: #4286f4;
border-color: #4286f4;
}
/* ========== QMenu / QMenuBar ========== */
QMenuBar {
background-color: #353535;
border-bottom: 1px solid #555555;
padding: 2px;
color: #cccccc;
}
QMenuBar::item:selected {
background-color: #4286f4;
color: #ffffff;
}
QMenu {
background-color: #353535;
border: 1px solid #555555;
border-radius: 4px;
padding: 4px;
color: #cccccc;
}
QMenu::item:selected {
background-color: #4286f4;
color: #ffffff;
}
QMenu::separator {
height: 1px;
background-color: #555555;
margin: 4px 0;
}
/* ========== QMessageBox ========== */
QMessageBox {
background-color: #353535;
}
QMessageBox QLabel {
color: #ffffff;
}
/* ========== 滚动条 ========== */
QScrollBar:vertical {
border: none;
background: #2a2a2a;
width: 8px;
border-radius: 4px;
}
QScrollBar::handle:vertical {
background: #666666;
border-radius: 4px;
min-height: 30px;
}
QScrollBar::handle:vertical:hover {
background: #888888;
}
QScrollBar::add-line:vertical,
QScrollBar::sub-line:vertical {
height: 0;
}

View File

@ -0,0 +1,6 @@
<RCC>
<qresource prefix="/styles">
<file>main.qss</file>
<file>main_dark.qss</file>
</qresource>
</RCC>