Publish by2024-08-06
Updated by2025-09-22
已经使用这门语言做过不少的项目了,写篇文章记录下一些,好吧,直接贴和 GPT 问答过来了 ~~~
Written by - kok-s0s
在 Windows 11 的桌面 Qt 应用中,点击
QLineEdit、QTextEdit等输入框时,并不会自动唤起系统触摸键盘。 解决这一问题,可以封装一个KeyboardMgr类,通过调用系统 TabTip.exe 来实现焦点进入时弹出键盘、焦点离开时关闭键盘。
TabTip.exe 是系统触摸键盘的进程,路径为:
C:\Program Files\Common Files\Microsoft Shared\ink\TabTip.exe
当输入框获得焦点时,启动 TabTip.exe,让触摸键盘弹出。
当输入框失去焦点时,通过 FindWindow + SendMessage 查找并关闭触摸键盘。
使用 Qt 的 事件过滤器 (eventFilter) 捕获 FocusIn 事件,结合控件类型判断是否需要弹出键盘。
#ifndef KEYBOARDMGR_H
#define KEYBOARDMGR_H
#include <QObject>
#include <QVector>
class KeyboardMgr : public QObject {
Q_OBJECT
public:
KeyboardMgr(void);
// 添加需要支持键盘的控件基类名称(如 QLineEdit, QTextEdit)
void addEditWidget(const QString \_className);
protected:
bool eventFilter(QObject _\_watched, QEvent _\_event);
private:
void showKeyboard(void);
void hideKeyboard(void);
void keyBoardCtrl(QObject \*\_obj);
private:
QVector<QString> widgets\_; // 存放要监听的控件类型
};
#endif // KEYBOARDMGR_H
#include "keyboardmgr.h"
#include <Windows.h>
#include <QApplication>
#include <QEvent>
#pragma comment(lib, "User32.lib")
const char* k_TabTip = "IPTIP_Main_Window";
const char* k_TabTipPath = "C:\\Program Files\\Common Files\\Microsoft Shared\\ink\\TabTip.exe";
KeyboardMgr::KeyboardMgr(void) {
// 安装全局事件过滤器
qApp->installEventFilter(this);
}
void KeyboardMgr::addEditWidget(const QString _className) {
if (!_className.isEmpty()) {
widgets_.append(_className);
}
}
void KeyboardMgr::showKeyboard(void) {
QString command("open");
QString keyboardpath(k_TabTipPath);
ShellExecuteW(
nullptr,
reinterpret_cast<LPCWSTR>(command.utf16()),
reinterpret_cast<LPCWSTR>(keyboardpath.utf16()),
nullptr, NULL,
SW_SHOWNORMAL);
}
void KeyboardMgr::hideKeyboard(void) {
HWND hwnd = FindWindowW(reinterpret_cast<LPCWSTR>(QString::fromUtf8(k_TabTip).utf16()), nullptr);
if (hwnd != nullptr) {
SendMessage(hwnd, WM_SYSCOMMAND, SC_CLOSE, 0);
}
}
void KeyboardMgr::keyBoardCtrl(QObject* _obj) {
if (_obj == nullptr) {
return;
}
bool find = false;
foreach (const auto& widget, widgets_) {
if (_obj->isWidgetType() && _obj->inherits(widget.toLocal8Bit().data())) {
find = true;
break;
}
}
if (find) {
showKeyboard();
} else {
hideKeyboard();
}
}
bool KeyboardMgr::eventFilter(QObject* _watched, QEvent* _event) {
if (_event == nullptr) {
return false;
}
if (_watched->isWidgetType() && _event->type() == QEvent::FocusIn) {
keyBoardCtrl(_watched);
}
return QObject::eventFilter(_watched, _event);
}
在 main.cpp 或主窗口初始化代码里添加:
#include "keyboardmgr.h"
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
// 创建键盘管理器
KeyboardMgr mgr;
// 注册需要支持键盘的控件类型
mgr.addEditWidget("QLineEdit");
mgr.addEditWidget("QTextEdit");
MainWindow w;
w.show();
return app.exec();
}
这样,当点击 QLineEdit 或 QTextEdit 时,系统触摸键盘会自动弹出;
离开这些控件时,键盘自动关闭。
classDiagram
class KeyboardMgr {
- QVector<QString> widgets_
+ KeyboardMgr()
+ void addEditWidget(QString className)
+ bool eventFilter(QObject* watched, QEvent* event)
- void showKeyboard()
- void hideKeyboard()
- void keyBoardCtrl(QObject* obj)
}
QObject <|-- KeyboardMgr
flowchart TD
A[应用启动] --> B[KeyboardMgr 安装全局事件过滤器]
B --> C[用户点击输入框]
C --> D{FocusIn 事件?}
D -- 否 --> E[忽略事件]
D -- 是 --> F[判断控件类型是否在 widgets_ 中]
F -- 否 --> G[调用 hideKeyboard() 关闭键盘]
F -- 是 --> H[调用 showKeyboard() 启动 TabTip.exe]
H --> I[系统触摸键盘弹出]
sequenceDiagram
participant User as 用户
participant Widget as QLineEdit
participant KeyboardMgr as KeyboardMgr
participant Windows as Windows 系统
User->>Widget: 点击输入框
Widget->>KeyboardMgr: 触发 FocusIn 事件
KeyboardMgr->>KeyboardMgr: 判断是否在 widgets_ 列表
alt 在列表中
KeyboardMgr->>Windows: ShellExecuteW 打开 TabTip.exe
Windows->>User: 显示触摸键盘
else 不在列表中
KeyboardMgr->>Windows: FindWindow + SendMessage 关闭键盘
end
KeyboardMgr 利用了 事件过滤器 捕获控件焦点变化。TabTip.exe,实现了触摸键盘的显隐控制。widgets_ 添加不同的可编辑控件类型。该方案适用于 Qt5 桌面应用,在 Windows 10/11 工业触摸屏环境下稳定可靠。
多个线程访问同一份数据,其中至少一个线程进行修改操作,而没有任何同步保护。
具体表现是:
即使操作的是一个“看起来稳定”的容器,只要是多线程访问,就可能存在:
这些都是典型的数据竞争问题,即便出错的概率不高,一旦出问题就是崩溃或严重 bug。
如果一个数据在多个执行路径中被访问(比如 UI 定时器、后台任务、事件回调),要假设它有可能发生并发冲突。
用一种同步机制(如互斥锁)来确保同一时间只有一个线程可以访问这个数据:
锁的作用范围只限定在:
访问或修改共享数据的那几行代码之内。
避免在加锁期间做以下操作:
否则可能导致卡顿甚至死锁。
如果有多个地方会遇到类似需求(比如线程安全的队列、列表、映射表),可以考虑封装成统一的线程安全容器类,例如:
| 步骤 | 操作建议 |
|---|---|
| ✅ 判断是否跨线程访问 | 多线程中共享数据需要重点关注 |
| ✅ 判断是否并发读写 | 即使只是偶尔写,也需保护 |
| ✅ 添加同步机制 | 使用锁机制保护数据一致性 |
| ✅ 避免锁内耗时操作 | 锁只保护数据,不要扩大作用范围 |
| ✅ 考虑封装复用 | 针对常见模式,统一封装为可复用结构 |
只要数据在多个线程间共享访问,就必须使用同步手段保护它的读写操作;否则,一定会在某个时机出现不可预期的错误。
典型的“跨平台行为差异引发的非确定性崩溃”
Windows 与 Linux 下 Qt 信号与槽执行时机、控件重绘机制以及内存分配行为存在差异。
BUG 相关代码
connect(ui->cmbWobjIndex, QOverload<int>::of(&QComboBox::currentIndexChanged), this, [this](int index) {
...
this->update();
...
});
QComboBox)QComboBox::clear() + addItem() 会自动选中第一个条目currentIndexChanged()(有一定延迟,或被 block)currentIndexChanged()→ 导致在 update() 时访问了一个不完整状态下的 QWidget,直接导致 SIGSEGV。
Linux 下:
glibc 分配策略不同,释放内存后指针更容易变成“野指针”Windows 下:
delete 后的指针有保护机制(如写入 0xCCCCCCCC)QWidget::update() 在 Linux 平台更加依赖底层绘图栈Linux Qt 实现通常使用 QXcbBackingStore(X11)或 QWaylandWindow(Wayland):
update() 的时候控件正在重建(尤其是 QGraphicsItem 在变更),很可能会造成 绘图线程与 UI 操作冲突| 平台 | 是否崩溃 | 原因 |
|---|---|---|
| Windows | ❌ 不崩溃 | update() 时对象虽然已析构,但未被立即访问或触发,行为未爆发 |
| Linux | ✅ 崩溃 | clear() 导致 currentIndexChanged() 被立即触发,update() 引用到悬空对象 |
currentIndexChanged 信号 —> activated
connect(ui->cmbWobjIndex, QOverload<int>::of(&QComboBox::activated), ...);
最实用和简洁的规避方法,推荐。
| 情况 | activated() | currentIndexChanged() |
|---|---|---|
| 用户点击选择项 | ✅ 是 | ✅ 是 |
| 用户键盘选择项(确认) | ✅ 是 | ✅ 是 |
程序调用 setCurrentIndex() | ❌ 否 | ✅ 是 |
执行 clear() / addItem() 导致选中项变化 | ❌ 否 | ✅ 是 |
在 Qt 项目开发中,qmake 虽然使用 .pro 文件看起来简单,但和现代的 CMake 相比,确实缺乏模块化、语义清晰、跨平台依赖自动化等特性。通过一些结构优化和技巧,可以让 qmake 的使用更接近 CMake 的整洁性和可维护性。
.pri 文件进行模块划分(类似 CMake 的 add_subdirectory())将不同模块的源码、头文件、依赖、配置等放入各自的 .pri 文件,然后在主 .pro 中 include():
# main.pro
TEMPLATE = app
TARGET = my_app
include(src/core/core.pri)
include(src/gui/gui.pri)
例如 core.pri:
HEADERS += \
$$PWD/core.h
SOURCES += \
$$PWD/core.cpp
defineReplace(pkgConfigLibs) {
return($$system(pkg-config --libs $$1))
}
LIBS += $$pkgConfigLibs(opencv4)
也可以写一个 common.pri:
CONFIG += c++17
DEFINES += QT_DEPRECATED_WARNINGS
QMAKE_CXXFLAGS += -Wall -Wextra
win32:QMAKE_CXXFLAGS += /permissive- /W4
或者放进一个 compiler_settings.pri 里全局引用。
target_sources())RESOURCES += resources/app.qrc
或封装在模块 .pri 文件中。
.pro 文件(类似自动化 CMakeLists.txt)可以写个 Python 脚本自动扫描目录,生成 .pri / .pro 文件结构,保持一致性。
qmake + cmake hybrid 模式Qt 5.15+ 已原生支持 CMake,若项目逐渐增大,推荐迁移:
find_package(Qt5 COMPONENTS Core Widgets REQUIRED)
target_link_libraries(my_app Qt5::Core Qt5::Widgets)
| 功能 | qmake 实现方式 | 对应 CMake |
|---|---|---|
| 模块划分 | .pri + include() | add_subdirectory() |
| 通用配置 | common.pri | include() |
| 条件编译 | win32: 等 | if(WIN32) |
| 外部库 | pkg-config 调用 | find_package() |
| 构建控制 | CONFIG, DEFINES | target_compile_definitions() |
在多语言支持中,不同语言的文本长度可能会导致界面显示不全的问题,通常可以通过以下几种方式处理:
QLayout 自动调整布局Qt 的 QLayout(如 QVBoxLayout、QHBoxLayout、QGridLayout)可以自动调整控件大小,避免文本被裁剪。
✅ 解决方案:
setFixedSize(),改用 setMinimumSize() 和 setSizePolicy()QSizePolicy::Expanding 让控件自动扩展示例:
label->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred);
这样,label 在需要时会自动扩展,而不会固定大小导致内容裁剪。
QLabel 自动换行如果你的界面中有 QLabel 需要适应不同语言的文本长度,可以让它自动换行:
✅ 解决方案:
label->setWordWrap(true);
或者 QSS 方式:
QLabel {
qproperty-wordwrap: true;
}
这样,即使文本过长,也会自动换行显示,而不会被裁剪。
不同语言的字体可能宽度不一样,比如日语/中文比英文更紧凑,而德语单词通常较长。
✅ 解决方案:
Noto Sans 系列支持多语言)if (currentLanguage == "zh") {
label->setFont(QFont("Microsoft YaHei", 12));
} else if (currentLanguage == "en") {
label->setFont(QFont("Arial", 11));
}
QLabel 省略超长文本如果希望文本超出控件范围时显示省略号(...),可以使用 setElideMode()(仅适用于 QLabel 或 QPushButton 这样的控件)。
✅ 解决方案:
label->setTextFormat(Qt::PlainText);
label->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred);
QFontMetrics metrics(label->font());
QString elidedText = metrics.elidedText(longText, Qt::ElideRight, label->width());
label->setText(elidedText);
QSS 方式:
QLabel {
text-overflow: ellipsis;
}
setMinimumWidth()如果某些语言的文本普遍较长,可以为 QLabel、QPushButton 预留更大的 minimumWidth,例如:
if (currentLanguage == "de") {
label->setMinimumWidth(200);
} else {
label->setMinimumWidth(150);
}
这样,德语界面自动比英文或中文界面更宽一些,避免内容溢出。
可以设置 QWidget 的 setMinimumSize(),让整个界面可以伸缩,以适应不同语言的长度:
this->setMinimumSize(800, 600);
配合 QLayout,界面可以自动调整大小,而不是固定尺寸导致显示不全。
QPushButton 自适应文本如果按钮的文本因语言不同而长度不同,QPushButton 可能显示不全。可以使用 adjustSize() 让按钮自动适应内容:
button->adjustSize();
或者使用 QSizePolicy:
button->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred);
如果你使用 QSS,可以针对不同语言加载不同的样式:
QString qssFile = (currentLanguage == "zh") ? ":/styles/style_zh.qss" : ":/styles/style_en.qss";
loadQss(qssFile);
| 问题 | 解决方案 |
|---|---|
| 文字过长超出控件 | setWordWrap(true)(自动换行)或 elidedText()(省略号) |
| 控件尺寸固定导致内容被裁剪 | 使用 QSizePolicy::Expanding 让控件自适应 |
| 按钮文本过长显示不全 | button->adjustSize(); 或 setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred) |
| 不同语言字体不同影响布局 | label->setFont(QFont(...)) 适配不同语言的字体 |
| 整个窗口固定大小导致排版问题 | setMinimumSize() 允许窗口适应内容 |
在 Qt 中,如果你需要在 显示进度条 时 禁止用户点击界面,但仍然让界面保持响应(即不阻塞事件循环),可以采用 模态对话框 或 禁用主窗口输入 的方式:
QProgressDialog 并设为模态Qt 提供了 QProgressDialog 组件,它可以在任务进行时 自动显示 进度条,并且可以设置为 模态(Modal),禁止用户与主窗口交互。
QProgressDialog progress("正在处理数据...", "取消", 0, 100, this);
progress.setWindowModality(Qt::ApplicationModal); // 设为应用级模态,禁止操作其他窗口
progress.setCancelButton(nullptr); // 移除取消按钮,防止用户中途取消
progress.show();
for (int i = 0; i <= 100; ++i) {
QThread::sleep(1); // 模拟任务耗时
progress.setValue(i);
QApplication::processEvents(); // 处理事件循环,保证 UI 更新
}
✅ 优点:
setWindowModality(Qt::ApplicationModal) 可以完全屏蔽用户输入,同时允许 UI 更新。QApplication::processEvents() 保持事件循环,确保 UI 不卡死。setEnabled(false) 禁用主窗口如果你不想使用模态对话框,也可以直接 禁用主窗口,但保持 UI 继续刷新。
this->setEnabled(false); // 禁用整个窗口,防止用户操作
QProgressDialog progress("处理中,请稍候...", nullptr, 0, 100, this);
progress.setWindowModality(Qt::WindowModal); // 设为窗口级模态
progress.show();
for (int i = 0; i <= 100; ++i) {
QThread::sleep(1);
progress.setValue(i);
QApplication::processEvents(); // 继续处理 UI 事件
}
this->setEnabled(true); // 处理完成后恢复交互
✅ 优点:
this->setEnabled(false) 禁止所有控件的输入,用户不能点击主窗口的按钮、输入框等。QApplication::processEvents() 确保进度条 不卡 UI。setEnabled(true) 让窗口恢复正常交互。QEventLoop 创建局部事件循环如果你的任务是异步的(比如后台线程),你可以用 QEventLoop 来控制界面更新:
this->setEnabled(false);
QProgressDialog progress("请稍等...", nullptr, 0, 0, this);
progress.setWindowModality(Qt::ApplicationModal);
progress.show();
// 运行局部事件循环,保证 UI 响应
QEventLoop loop;
QTimer::singleShot(3000, &loop, SLOT(quit())); // 模拟耗时操作,3 秒后退出
loop.exec();
this->setEnabled(true);
✅ 优点:
QEventLoop 创建 局部事件循环,不会阻塞整个应用,但仍能响应 UI 事件。QTimer::singleShot() 代替 sleep(),避免主线程卡死。QThread 在后台执行任务如果任务特别耗时,最好 放到子线程,避免阻塞 UI 线程:
class WorkerThread : public QThread {
void run() override {
for (int i = 0; i <= 100; ++i) {
QThread::sleep(1); // 模拟耗时任务
emit progressUpdated(i);
}
}
signals:
void progressUpdated(int value);
};
// 主窗口代码
this->setEnabled(false);
QProgressDialog progress("处理中...", nullptr, 0, 100, this);
progress.setWindowModality(Qt::ApplicationModal);
progress.show();
WorkerThread *worker = new WorkerThread;
connect(worker, &WorkerThread::progressUpdated, &progress, &QProgressDialog::setValue);
connect(worker, &WorkerThread::finished, [&]() {
this->setEnabled(true);
progress.close();
});
worker->start();
✅ 优点:
QThread 让任务运行在后台,主线程不会被阻塞,UI 完全流畅。| 方式 | 适用场景 | UI 是否卡死 | 适用任务 |
|---|---|---|---|
QProgressDialog + processEvents() | 短任务 | ❌ 不卡 UI | 简单任务 |
setEnabled(false) + processEvents() | 短任务 | ❌ 不卡 UI | 控制整个窗口禁用 |
QEventLoop | 异步任务 | ❌ 不卡 UI | 适用于定时操作 |
QThread | 长任务 | ✅ 完全不卡 | 适用于复杂计算 |
如果你的任务特别耗时,推荐 方法 4(QThread),否则 方法 1 或 2 也能满足需求。
QCoreApplication::processEvents(); 主要用于在事件循环(Event Loop)中手动处理待处理的事件,通常用于以下情况:
processEvents() 可以让 Qt 处理窗口事件,防止界面卡死。例如:for (int i = 0; i < 100; ++i) {
doHeavyTask(); // 耗时任务
QCoreApplication::processEvents(); // 处理 UI 事件,保持界面响应
}
如果不调用 processEvents(),整个任务完成前 UI 界面可能会卡死。
执行挂起的事件 在某些情况下,事件可能不会立即被处理,比如:
QTimer 触发的超时事件QSocketNotifier 的信号QMetaObject::invokeMethod 的 QueuedConnection
调用 processEvents() 让它们立即执行,而不必等到事件循环返回。用于阻塞式等待时处理事件
如果某个函数需要等待一段时间(例如等待某个信号触发),可以在 while 循环里调用 processEvents() 以避免阻塞整个 GUI:
QElapsedTimer timer;
timer.start();
while (timer.elapsed() < 3000) { // 等待 3 秒
QCoreApplication::processEvents();
}
这样可以在等待的同时,让窗口仍然能够响应用户输入。
可能引发重入问题
如果在处理某些事件(如 mousePressEvent())时调用 processEvents(),可能会导致事件被重新进入执行,从而引发不可预测的行为。
可能导致性能问题
如果在耗时任务的循环中频繁调用 processEvents(),可能会导致 CPU 资源浪费。因此可以使用 QEventLoop::processEvents(QEventLoop::ExcludeUserInputEvents); 避免过多处理用户输入事件。
QTimer 或 QThread 进行异步处理processEvents() 里。例如:QTimer::singleShot(0, this, &MyClass::doHeavyTask);
或者:
QThread* thread = QThread::create(&doHeavyTask);
thread->start();
✅ 适用场景
❌ 不建议
更推荐使用 事件驱动编程 或 多线程 方式来解决 GUI 卡顿问题,而不是频繁调用 processEvents()。
QPixmap 这个类有个坑,如果传递的图片分辨率很高,其实例化后的对象会占用很大的内存。比如 7420 x 7074 分辨率的图片,内存占用会是 7420 x 7074 x 4 = 209,366,080 字节,约 200 MB。
在 Qt C++ 客户端开发中,翻译(国际化)通常使用 Qt Linguist 工具链,包括 .ts、.qm 文件,配合 tr() 函数实现。要高效地更新和迭代翻译,以下是推荐的流程:
tr() 包装:QLabel *label = new QLabel(tr("Hello, World!"));
tr()。lupdate,扫描项目中的 tr() 调用,生成或更新 .ts 翻译文件:lupdate MyProject.pro
.ts 文件会包含所有未翻译的文本。翻译(使用 Qt Linguist 或编辑 .ts)
.ts 文件(XML 格式)。编译翻译文件
lrelease,将 .ts 转换为 .qm(二进制格式):lrelease MyProject.pro
.qm 文件:QTranslator translator;
if (translator.load("zh_CN.qm")) {
qApp->installTranslator(&translator);
}
tr() 会自动使用翻译后的文本。要高效维护和更新翻译,建议采用以下策略:
.ts 文件lupdate 自动更新,不要手动修改 .ts,避免漏掉新增文本:lupdate MyProject.pro
.ts 原文件。lupdate 和 lrelease,确保每次构建时翻译都是最新的:lupdate MyProject.pro && lrelease MyProject.pro
pro 文件中添加 TRANSLATIONS 变量,自动管理 .ts 文件:TRANSLATIONS = translations/zh_CN.ts translations/en_US.ts
void changeLanguage(const QString &lang) {
static QTranslator translator;
qApp->removeTranslator(&translator);
if (translator.load(lang + ".qm")) {
qApp->installTranslator(&translator);
}
}
.ts 文件。.ts 文件纳入 Git,确保每次提交前 lupdate 处理最新的字符串。git add translations/*.ts
git commit -m "Update translations"
修改 .pro 文件,在项目的 .pro 文件中添加:
lupdate_only {
CONFIG += lrelease_no_location
}
lupdate,导致新增的 tr() 字符串没有出现在 .ts 文件里。tr(),导致字符串不会被翻译。.qm 版本不匹配,如果 .qm 不是最新的,翻译不会生效,确保每次构建都运行 lrelease。QObject::tr() 作用域下定义文本。✅ 标准化流程:tr() → lupdate → Qt Linguist → lrelease → QTranslator
✅ 自动化:CI/CD 脚本更新 .ts,管理 .qm,减少手动操作
✅ 团队协作:Git 版本管理 + 在线翻译平台
✅ 动态加载翻译:让用户即时切换语言,无需重启应用
这样可以最大程度地提高 Qt C++ 应用的国际化效率,减少维护成本!
Top