Qt Dev

Publish by2024-08-06

Updated by2025-09-22

已经使用这门语言做过不少的项目了,写篇文章记录下一些,好吧,直接贴和 GPT 问答过来了 ~~~

Written by - kok-s0s

Qt

Qt

C++

Qt Windows 唤起触摸键盘

在 Windows 11 的桌面 Qt 应用中,点击 QLineEditQTextEdit 等输入框时,并不会自动唤起系统触摸键盘。 解决这一问题,可以封装一个 KeyboardMgr 类,通过调用系统 TabTip.exe 来实现焦点进入时弹出键盘、焦点离开时关闭键盘。


1. 实现思路

2. 代码实现

keyboardmgr.h
#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
keyboardmgr.cpp
#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);
}

3. 使用方法

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();
}

这样,当点击 QLineEditQTextEdit 时,系统触摸键盘会自动弹出; 离开这些控件时,键盘自动关闭。

4. 类图

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

5. 事件流程图

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[系统触摸键盘弹出]

6. 使用场景时序图

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

7. 总结

该方案适用于 Qt5 桌面应用,在 Windows 10/11 工业触摸屏环境下稳定可靠。

多线程操作数据而导致的奔溃问题

多个线程访问同一份数据,其中至少一个线程进行修改操作,而没有任何同步保护。

具体表现是:


为什么会崩溃?

即使操作的是一个“看起来稳定”的容器,只要是多线程访问,就可能存在:

这些都是典型的数据竞争问题,即便出错的概率不高,一旦出问题就是崩溃或严重 bug。


正确处理方式(通用)

1. 先判断是否有线程交叉访问

如果一个数据在多个执行路径中被访问(比如 UI 定时器、后台任务、事件回调),要假设它有可能发生并发冲突


2. 统一保护共享数据

用一种同步机制(如互斥锁)来确保同一时间只有一个线程可以访问这个数据:


3. 加锁区域尽量小

锁的作用范围只限定在:

访问或修改共享数据的那几行代码之内

避免在加锁期间做以下操作:

否则可能导致卡顿甚至死锁。


4. 封装成复用结构(可选)

如果有多个地方会遇到类似需求(比如线程安全的队列、列表、映射表),可以考虑封装成统一的线程安全容器类,例如:


以后遇到类似问题的标准流程

步骤操作建议
✅ 判断是否跨线程访问多线程中共享数据需要重点关注
✅ 判断是否并发读写即使只是偶尔写,也需保护
✅ 添加同步机制使用锁机制保护数据一致性
✅ 避免锁内耗时操作锁只保护数据,不要扩大作用范围
✅ 考虑封装复用针对常见模式,统一封装为可复用结构

总结一句话

只要数据在多个线程间共享访问,就必须使用同步手段保护它的读写操作;否则,一定会在某个时机出现不可预期的错误。

QWidget::update() 奔溃问题

典型的“跨平台行为差异引发的非确定性崩溃”

Windows 与 Linux 下 Qt 信号与槽执行时机、控件重绘机制以及内存分配行为存在差异。

BUG 相关代码

connect(ui->cmbWobjIndex, QOverload<int>::of(&QComboBox::currentIndexChanged), this, [this](int index) {
    ...
    this->update();
    ...
});

1. Qt 在不同平台上控件行为细节不同(尤其是 QComboBox

Windows 下:

Linux (特别是使用 X11 / Wayland)

→ 导致在 update() 时访问了一个不完整状态下的 QWidget,直接导致 SIGSEGV


2. 内存布局行为不同

Linux 下:

Windows 下:


3. Qt 的 QWidget::update() 在 Linux 平台更加依赖底层绘图栈

Linux Qt 实现通常使用 QXcbBackingStore(X11)或 QWaylandWindow(Wayland):

结论总结

平台是否崩溃原因
Windows❌ 不崩溃update() 时对象虽然已析构,但未被立即访问或触发,行为未爆发
Linux✅ 崩溃clear() 导致 currentIndexChanged() 被立即触发,update() 引用到悬空对象

修复策略(跨平台安全)

currentIndexChanged 信号 —> activated

connect(ui->cmbWobjIndex, QOverload<int>::of(&QComboBox::activated), ...);

最实用和简洁的规避方法,推荐

信号触发情况

情况activated()currentIndexChanged()
用户点击选择项✅ 是✅ 是
用户键盘选择项(确认)✅ 是✅ 是
程序调用 setCurrentIndex()❌ 否✅ 是
执行 clear() / addItem() 导致选中项变化❌ 否✅ 是

QT qmake 如何做到和 CMake 一样简洁明了

在 Qt 项目开发中,qmake 虽然使用 .pro 文件看起来简单,但和现代的 CMake 相比,确实缺乏模块化、语义清晰、跨平台依赖自动化等特性。通过一些结构优化和技巧,可以让 qmake 的使用更接近 CMake 的整洁性和可维护性。

1. 使用 .pri 文件进行模块划分(类似 CMake 的 add_subdirectory()

将不同模块的源码、头文件、依赖、配置等放入各自的 .pri 文件,然后在主 .proinclude()

# 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

2. 使用变量封装通用配置(模拟 CMake 的函数/宏)

defineReplace(pkgConfigLibs) {
    return($$system(pkg-config --libs $$1))
}

LIBS += $$pkgConfigLibs(opencv4)

也可以写一个 common.pri

CONFIG += c++17
DEFINES += QT_DEPRECATED_WARNINGS

3. 简洁设置编译选项(兼容 MSVC / GCC)

QMAKE_CXXFLAGS += -Wall -Wextra
win32:QMAKE_CXXFLAGS += /permissive- /W4

或者放进一个 compiler_settings.pri 里全局引用。


4. 统一资源管理(类似 CMake target_sources()

RESOURCES += resources/app.qrc

或封装在模块 .pri 文件中。


5. 借助工具生成 .pro 文件(类似自动化 CMakeLists.txt

可以写个 Python 脚本自动扫描目录,生成 .pri / .pro 文件结构,保持一致性。


6. 考虑迁移到 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.priinclude()
条件编译win32:if(WIN32)
外部库pkg-config 调用find_package()
构建控制CONFIG, DEFINEStarget_compile_definitions()

Qt 应用在多语言环境下保持良好的 UI 体验

在多语言支持中,不同语言的文本长度可能会导致界面显示不全的问题,通常可以通过以下几种方式处理:


1. 使用 QLayout 自动调整布局

Qt 的 QLayout(如 QVBoxLayoutQHBoxLayoutQGridLayout)可以自动调整控件大小,避免文本被裁剪。

解决方案

示例:

label->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred);

这样,label 在需要时会自动扩展,而不会固定大小导致内容裁剪。


2. 让 QLabel 自动换行

如果你的界面中有 QLabel 需要适应不同语言的文本长度,可以让它自动换行:

解决方案

label->setWordWrap(true);

或者 QSS 方式:

QLabel {
  qproperty-wordwrap: true;
}

这样,即使文本过长,也会自动换行显示,而不会被裁剪。


3. 适配不同语言的字体

不同语言的字体可能宽度不一样,比如日语/中文比英文更紧凑,而德语单词通常较长。

解决方案

if (currentLanguage == "zh") {
    label->setFont(QFont("Microsoft YaHei", 12));
} else if (currentLanguage == "en") {
    label->setFont(QFont("Arial", 11));
}

4. 允许 QLabel 省略超长文本

如果希望文本超出控件范围时显示省略号(...),可以使用 setElideMode()(仅适用于 QLabelQPushButton 这样的控件)。

解决方案

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

5. 预留足够空间,并使用 setMinimumWidth()

如果某些语言的文本普遍较长,可以为 QLabelQPushButton 预留更大的 minimumWidth,例如:

if (currentLanguage == "de") {
    label->setMinimumWidth(200);
} else {
    label->setMinimumWidth(150);
}

这样,德语界面自动比英文或中文界面更宽一些,避免内容溢出。


6. 允许界面伸缩

可以设置 QWidgetsetMinimumSize(),让整个界面可以伸缩,以适应不同语言的长度:

this->setMinimumSize(800, 600);

配合 QLayout,界面可以自动调整大小,而不是固定尺寸导致显示不全。


7. 让 QPushButton 自适应文本

如果按钮的文本因语言不同而长度不同,QPushButton 可能显示不全。可以使用 adjustSize() 让按钮自动适应内容:

button->adjustSize();

或者使用 QSizePolicy

button->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred);

8. 适配不同语言的 QSS

如果你使用 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 中,如果你需要在 显示进度条禁止用户点击界面,但仍然让界面保持响应(即不阻塞事件循环),可以采用 模态对话框禁用主窗口输入 的方式:


方法 1:使用 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 更新
}

优点


方法 2:使用 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); // 处理完成后恢复交互

优点


方法 3:使用 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);

优点


方法 4:使用 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();

优点


总结

方式适用场景UI 是否卡死适用任务
QProgressDialog + processEvents()短任务❌ 不卡 UI简单任务
setEnabled(false) + processEvents()短任务❌ 不卡 UI控制整个窗口禁用
QEventLoop异步任务❌ 不卡 UI适用于定时操作
QThread长任务✅ 完全不卡适用于复杂计算

如果你的任务特别耗时,推荐 方法 4(QThread),否则 方法 1 或 2 也能满足需求。

保持界面响应

QCoreApplication::processEvents(); 主要用于在事件循环(Event Loop)中手动处理待处理的事件,通常用于以下情况:

作用

  1. 保持界面响应 在耗时的操作(如文件 IO、计算、网络请求等)中调用 processEvents() 可以让 Qt 处理窗口事件,防止界面卡死。例如:
for (int i = 0; i < 100; ++i) {
    doHeavyTask();  // 耗时任务
    QCoreApplication::processEvents();  // 处理 UI 事件,保持界面响应
}

如果不调用 processEvents(),整个任务完成前 UI 界面可能会卡死。

  1. 执行挂起的事件 在某些情况下,事件可能不会立即被处理,比如:

  2. 用于阻塞式等待时处理事件 如果某个函数需要等待一段时间(例如等待某个信号触发),可以在 while 循环里调用 processEvents() 以避免阻塞整个 GUI:

QElapsedTimer timer;
timer.start();
while (timer.elapsed() < 3000) { // 等待 3 秒
    QCoreApplication::processEvents();
}

这样可以在等待的同时,让窗口仍然能够响应用户输入。

常见问题

替代方案

QTimer::singleShot(0, this, &MyClass::doHeavyTask);

或者:

QThread* thread = QThread::create(&doHeavyTask);
thread->start();

总结

适用场景

不建议

更推荐使用 事件驱动编程多线程 方式来解决 GUI 卡顿问题,而不是频繁调用 processEvents()

QPixmap 图像处理

QPixmap 这个类有个坑,如果传递的图片分辨率很高,其实例化后的对象会占用很大的内存。比如 7420 x 7074 分辨率的图片,内存占用会是 7420 x 7074 x 4 = 209,366,080 字节,约 200 MB。

如何避免这种问题?

翻译工作

在 Qt C++ 客户端开发中,翻译(国际化)通常使用 Qt Linguist 工具链,包括 .ts.qm 文件,配合 tr() 函数实现。要高效地更新和迭代翻译,以下是推荐的流程:


1. 翻译的基本流程

  1. 标记可翻译字符串
QLabel *label = new QLabel(tr("Hello, World!"));
  1. 生成翻译源文件(.ts)
lupdate MyProject.pro
  1. 翻译(使用 Qt Linguist 或编辑 .ts)

  2. 编译翻译文件

lrelease MyProject.pro
  1. 加载翻译
QTranslator translator;
if (translator.load("zh_CN.qm")) {
    qApp->installTranslator(&translator);
}

2. 高效更新和迭代翻译的方法

要高效维护和更新翻译,建议采用以下策略:

(1)避免手动管理 .ts 文件

lupdate MyProject.pro

(2)自动化翻译流程

lupdate MyProject.pro && lrelease MyProject.pro
TRANSLATIONS = translations/zh_CN.ts translations/en_US.ts

(3)动态加载语言

void changeLanguage(const QString &lang) {
    static QTranslator translator;
    qApp->removeTranslator(&translator);
    if (translator.load(lang + ".qm")) {
        qApp->installTranslator(&translator);
    }
}

(4)使用翻译管理平台

(5)对翻译条目做版本管理

git add translations/*.ts
git commit -m "Update translations"

(6)想要在生成的 .ts 文件中不显示 location 字段的数据

修改 .pro 文件,在项目的 .pro 文件中添加:

lupdate_only {
    CONFIG += lrelease_no_location
}

3. 避免的坑


4. 总结

标准化流程tr()lupdate → Qt Linguist → lreleaseQTranslator
自动化:CI/CD 脚本更新 .ts,管理 .qm,减少手动操作
团队协作:Git 版本管理 + 在线翻译平台
动态加载翻译:让用户即时切换语言,无需重启应用

这样可以最大程度地提高 Qt C++ 应用的国际化效率,减少维护成本!

Top