15_国际化和本地化:tr()、ts 文件、QM 文件、多语言切换

国际化和本地化:tr()、TS 文件、QM 文件与多语言切换

引言

上一章用 QSS 把界面的颜色、边框和间距从业务逻辑中分离出来。本章解决另一件同样基础的事:用户看到的文字不能长期写死在代码里 。一个面向中文用户的 Widgets 程序,通常先以中文源文本开始;当需要英文界面时,应让翻译系统负责替换可见文字,而不是在每个槽函数里写一串 if (english)

Qt 将这件事拆成一条清晰链路:代码和 .ui 文件中的 tr() 产生源文本,lupdate 提取为 .ts 翻译目录,Qt Linguist 负责人工翻译与校验,lrelease 将结果编译成运行时读取的 .qm 文件,QTranslator 再把它安装到应用中。

本文面向 Qt 6 Widgets,按"先会用 -> 配置构建 -> 使用 Linguist -> 运行时切换 -> 追踪源码"的顺序展开。读完后应能:

  • 给一个 CMake Widgets 工程接入中文/英文翻译;
  • 理解 tr().ts.qm 各自的职责,知道哪些文件应提交到版本库;
  • 用 Qt Linguist 完成翻译、标记完成并生成 QM 文件;
  • 在不重启程序的前提下切换语言,并正确处理 LanguageChange
  • 沿公开 API 理解一次 tr() 如何查询 QTranslator
  • 区分国际化(i18n)与本地化(l10n),正确处理日期、数字和复数。

图 1:.ts 是可读、可审阅的翻译目录;.qm 是供程序快速查询的二进制结果。源码只负责声明"这段文字可翻译",不应自己保存多份语言文本。

一、先会用:一个可以即时切换语言的小窗口

下面示例有标题、说明文字、操作按钮和语言下拉框。源文本使用中文;选择 English 后,已安装的英文翻译会替换所有由 tr() 得到的文字。这个例子刻意不使用 Designer,便于先看清楚每一处可翻译文本;使用 .ui 时的处理方式会在后文说明。

1.1 工程结构

text 复制代码
i18n_demo/
├── CMakeLists.txt
├── main.cpp
└── translations/
    └── i18n_demo_en.ts       # lupdate 生成、Linguist 编辑,提交版本库

构建后会生成 i18n_demo_en.qm。本章将它嵌入资源系统,因此不需要依赖工作目录;若改为外置部署,后文会说明应如何加载。

1.2 CMake 配置

LinguistTools 提供 lupdatelrelease 的 CMake 集成。下面是 Qt 6 的推荐写法:

cmake 复制代码
cmake_minimum_required(VERSION 3.21)
project(i18n_demo LANGUAGES CXX)

find_package(Qt6 REQUIRED COMPONENTS Widgets LinguistTools)

qt_standard_project_setup(
    I18N_SOURCE_LANGUAGE zh
    I18N_TRANSLATED_LANGUAGES en
)

qt_add_executable(i18n_demo
    main.cpp
)

target_link_libraries(i18n_demo PRIVATE Qt6::Widgets)

qt_add_translations(i18n_demo
    TS_FILES
        translations/i18n_demo_en.ts
    RESOURCE_PREFIX "/i18n"
)

set_target_properties(i18n_demo PROPERTIES
    CXX_STANDARD 17
    CXX_STANDARD_REQUIRED ON
)

这里最容易混淆的是三个动作:qt_add_translations() 并不翻译文字,它为目标配置更新 TS、编译 QM、把 QM 加入资源 的构建规则;I18N_SOURCE_LANGUAGE 表示源文语言;I18N_TRANSLATED_LANGUAGES 声明需要维护的目标语言。将来增加日语时,在这里加入 ja,并为它增加一个 TS 文件即可。

先配置工程,再执行下面两个目标:

powershell 复制代码
cmake -S . -B build
cmake --build build --target update_translations
cmake --build build --target release_translations

第一次执行 update_translations 时,lupdate 会创建或填充 translations/i18n_demo_en.tsrelease_translations 调用 lrelease 生成 .qm;普通 cmake --build build 也会按依赖关系在需要时生成运行资源。不同 Qt 6 小版本的构建树中可能还会看到以目标名命名的 lupdate/lrelease 辅助目标,不必手工调用可执行文件。

.ts 是源文件,应和 C++ 一起提交、参与 Code Review;.qm 通常是构建产物。若发布方式是"资源内嵌",无需单独携带 QM;若采用外置翻译包,则把它作为安装产物处理,不要从开发机的 build 目录临时复制。

1.3 完整代码

cpp 复制代码
#include <QApplication>
#include <QComboBox>
#include <QEvent>
#include <QLabel>
#include <QPushButton>
#include <QTranslator>
#include <QVBoxLayout>
#include <QWidget>

class TranslationDemo : public QWidget
{
    Q_OBJECT

public:
    TranslationDemo()
    {
        titleLabel = new QLabel(this);
        titleLabel->setStyleSheet("font-size: 20px; font-weight: 600;");

        descriptionLabel = new QLabel(this);
        descriptionLabel->setWordWrap(true);

        languageLabel = new QLabel(this);
        languageBox = new QComboBox(this);
        // 语言名称保持用户熟悉的写法,不把它当作待翻译界面文案。
        languageBox->addItem(QStringLiteral("中文(简体)"), QStringLiteral("zh_CN"));
        languageBox->addItem(QStringLiteral("English"), QStringLiteral("en"));

        saveButton = new QPushButton(this);
        statusLabel = new QLabel(this);

        auto *layout = new QVBoxLayout(this);
        layout->addWidget(titleLabel);
        layout->addWidget(descriptionLabel);
        layout->addWidget(languageLabel);
        layout->addWidget(languageBox);
        layout->addWidget(saveButton);
        layout->addWidget(statusLabel);

        connect(languageBox, &QComboBox::currentIndexChanged,
                this, &TranslationDemo::switchLanguage);
        connect(saveButton, &QPushButton::clicked, this,
                [this] { statusLabel->setText(tr("设置已保存。")); });

        retranslateUi();
    }

protected:
    void changeEvent(QEvent *event) override
    {
        if (event->type() == QEvent::LanguageChange) {
            retranslateUi();
        }
        QWidget::changeEvent(event);
    }

private slots:
    void switchLanguage(int index)
    {
        const QString language = languageBox->itemData(index).toString();

        // 先移除旧翻译器。选择源语言时,tr() 会返回中文源文。
        qApp->removeTranslator(&translator);
        if (language == QStringLiteral("zh_CN")) {
            return;
        }

        if (!translator.load(QStringLiteral(":/i18n/i18n_demo_en.qm"))) {
            statusLabel->setText(tr("英文翻译文件加载失败。"));
            return;
        }
        qApp->installTranslator(&translator);
    }

private:
    void retranslateUi()
    {
        setWindowTitle(tr("国际化示例"));
        titleLabel->setText(tr("应用设置"));
        descriptionLabel->setText(
            tr("选择界面语言后,窗口中的可翻译文字会立即更新。"));
        languageLabel->setText(tr("界面语言:"));
        saveButton->setText(tr("保存设置"));

        if (statusLabel->text().isEmpty()) {
            statusLabel->setText(tr("尚未保存。"));
        }
    }

    QTranslator translator;
    QLabel *titleLabel = nullptr;
    QLabel *descriptionLabel = nullptr;
    QLabel *languageLabel = nullptr;
    QComboBox *languageBox = nullptr;
    QPushButton *saveButton = nullptr;
    QLabel *statusLabel = nullptr;
};

int main(int argc, char *argv[])
{
    QApplication app(argc, argv);
    TranslationDemo window;
    window.resize(440, 250);
    window.show();
    return app.exec();
}

#include "main.moc"

示例把 QTranslator 作为窗口成员,保证它在窗口存活期间有效。不能switchLanguage() 中创建一个局部 QTranslator 后安装:函数返回时对象销毁,应用保留的指针会失效。大型程序更适合让一个应用级 LanguageController 拥有翻译器,窗口只负责响应 LanguageChange

tr() 的核心意义不是"翻译函数会猜中语言",而是为源文本提供稳定的上下文(context) 。在 TranslationDemo 成员函数中,tr("保存设置") 的 context 是类名 TranslationDemo;Linguist 正是靠 context、源文、可选注释和复数数量共同定位一条翻译。

1.4 翻译配置相关说明

tr() 负责声明,TS 负责保存,Linguist 负责翻译,QM 负责运行时查询,QTranslator 负责加载,LanguageChange 负责通知界面更新。

翻译相关组件 本质 谁产生 谁使用
tr() 翻译入口 开发者 C++/Qt
.ts 翻译源文件 lupdate Linguist
Qt Linguist 翻译工具 Qt 开发者/译者
.qm 二进制翻译文件 lrelease QTranslator
QTranslator 运行时翻译器 程序 Qt
LanguageChange 语言变化事件 Qt QWidget
QLocale 地区格式化规则 Qt 程序

二、tr() 应该放在哪里

2.1 先包住所有给用户看的固定文案

cpp 复制代码
setWindowTitle(tr("下载中心"));
startButton->setText(tr("开始下载"));
messageLabel->setText(tr("已完成 %1 / %2 个任务")
                          .arg(done)
                          .arg(total));

应翻译的是用户可见的固定模板,%1%2 是运行时数据。不要把用户文件名、数据库内容、协议字段或稳定 ID 放进 tr():它们不是由翻译目录维护的文案。

cpp 复制代码
// 正确:模板可翻译,文件名只是参数。
label->setText(tr("正在处理:%1").arg(fileName));

// 不要这样做:每个文件名都被误当作翻译键。
label->setText(tr(fileName.toUtf8().constData()));

不要把动态字符串放进 tr(),下面这个例子执行翻译错误:

cpp 复制代码
QString name = "张三";

// 错误
label->setText(tr(name.toUtf8().constData()));

tr() 需要在程序构建/扫描阶段就知道 source textlupdate扫描源码找到"正在处理:%1",然后生成.ts文件。

这里有一个重要的原则:tr() 的参数应该是稳定的源文本,动态数据通过 %1%2%n 等占位符传入。

2.2 自定义类、普通函数和 QObject::tr()

继承 QObject 且带 Q_OBJECT 的类,可直接使用自己的 tr();它的类名会成为 context。自由函数没有类 context 时,可显式调用 QObject::tr()

cpp 复制代码
QString defaultProfileName()
{
    return QObject::tr("默认配置");
}

这会把 context 记为 QObject。它适合真正跨类共享的少量通用文案;不要为了省事让整个项目都使用 QObject::tr(),否则 Linguist 中很难分辨"打开"属于哪个界面和业务含义。

2.3 .ui 文件已经为你调用了翻译入口

Designer 的 uic 生成代码会把界面文字放进 retranslateUi(),形式类似:

cpp 复制代码
saveButton->setText(QCoreApplication::translate("SettingsDialog", "保存"));

因此 .ui 中设置的 textwindowTitletoolTip 等属性同样会被 lupdate 提取。运行时切换语言时,在窗口的 changeEvent() 中调用生成类的重翻译函数:

cpp 复制代码
void SettingsDialog::changeEvent(QEvent *event)
{
    if (event->type() == QEvent::LanguageChange) {
        ui->retranslateUi(this);
        updateRuntimeTexts(); // 代码动态创建的文字在这里统一重设
    }
    QDialog::changeEvent(event);
}

不要把 ui->retranslateUi(this) 当作"自动翻译所有东西"。它只处理 .ui 中由 uic 生成的设置;定时器消息、动态菜单项、模型表头、错误提示等代码生成的文本,仍要在自己的 updateRuntimeTexts() 中重新赋值。

三、从 TS 到 QM:不要手写翻译流水线

.ts 是 XML 形式的翻译目录。一条记录大致如下:

xml 复制代码
<context>
    <name>TranslationDemo</name>
    <message>
        <source>保存设置</source>
        <translation>Save Settings</translation>
    </message>
</context>

源文变更后,先重新运行 lupdate,它会按照 context 和源文更新目录;翻译完成后再用 lrelease 编译。日常流程固定为:

改 C++/.ui -> update translations -> Linguist 翻译/复查 -> release translations -> 运行验证

不要手工批量搜索替换 XML,也不要修改 .qm。前者容易破坏状态、复数和上下文,后者是二进制产物;真正需要审阅的是 TS 中源文、译文和备注。

3.1 Qt Linguist 的使用步骤

  1. 执行一次 cmake --build <构建目录> --target update_translations,确保 TS 已包含新源文。
  2. 在 Qt Creator 的项目树中打开 translations/i18n_demo_en.ts(示例),或从 Qt 安装目录启动 Qt Linguist 后选择"打开"。
  3. 在左侧 context 列表选择 TranslationDemo,在中间消息列表选择未完成项。
  4. 阅读源文和开发者备注,在下方 Translation 编辑区填入英文;确认无误后标记为完成。
  5. 使用"验证"检查占位符、加速键等问题,保存 TS。
  6. 回到构建执行 release_translations,随后启动程序验证加载与切换。

下面以工程为例,一个没有进行翻译的ts文件,通过qt语言家打开是这样:

TS 中存在 source,但翻译状态 是 unfinishedlrelease 时不会把它作为有效翻译使用,运行时最终回退到 source text

按照下图的操作顺序进行翻译并生成.qm文件,启动程序并加载qm文件就能看到翻译后的文本显示。

3.2 给译者的备注、歧义和加速键

"打开"既可以是动词,也可以是状态。不要让译者靠猜;使用 disambiguation,或在源代码旁给出 //: 开头的译者备注:

cpp 复制代码
//: 菜单命令,动词"打开文件"
openAction->setText(tr("打开", "verb"));

//: 文件当前状态,形容词"已处于打开状态"
stateLabel->setText(tr("打开", "state"));

第二个参数不显示给用户,却会参与翻译键的匹配。备注会进入 TS,帮助翻译者理解领域语义。带 & 的菜单文字也是同理:tr("&文件") 中的 & 表示快捷键助记符,翻译后应按目标语言重新选择可用字母,不能机械保留原位置。

四、运行时多语言切换:安装翻译器只是第一步

QTranslator::load() 只把 QM 读入翻译器对象;QApplication::installTranslator() 才使应用的后续 tr() 查询能看到它。移除或安装翻译器后,Qt 会触发 QEvent::LanguageChange,但已经显示在屏幕上的旧 QString 不会凭空变成新语言,窗口必须重新调用 tr() 并设置控件文本。

图 2:语言切换不需要销毁窗口。翻译器列表变化后,窗口收到 LanguageChangeretranslateUi() 再从新的翻译器中取回文本。业务数据、选择状态和输入内容不应因语言变化丢失。

4.1 安装顺序会影响覆盖关系

应用可安装多个 QTranslator,并按照后安装者优先的顺序查询。这正适合"先安装 Qt 自带翻译,再安装产品自己的翻译",让产品同名文案能够覆盖默认文本。

cpp 复制代码
QTranslator qtBaseTranslator;
qtBaseTranslator.load(QLocale("zh_CN"), "qtbase", "_",
                      QLibraryInfo::path(QLibraryInfo::TranslationsPath));
qApp->installTranslator(&qtBaseTranslator);

QTranslator appTranslator;
appTranslator.load(":/i18n/i18n_demo_zh_CN.qm");
qApp->installTranslator(&appTranslator); // 后安装,优先查询

Qt 模块翻译文件名、安装位置会随 Qt 发行版变化,应使用 QLibraryInfo::TranslationsPath 找到 Qt 的翻译目录;自己的翻译文件则建议使用资源路径或明确的安装目录。两类翻译器都必须拥有足够长的生命周期。

4.2 外置 QM 与资源内嵌怎么选

方式 加载路径 适合场景 注意点
资源内嵌 :/i18n/app_en.qm 固定语言包、小型桌面应用 语言包随可执行文件发布,更新语言需更新程序
外置文件 applicationDirPath()/translations 可独立更新语言包、插件化产品 安装规则、签名、缺失提示与路径测试要完整

外置文件不能写成相对路径 "translations/app_en.qm" 并假定总能工作。用户可能从快捷方式、命令行或 IDE 用不同工作目录启动程序;应以 QCoreApplication::applicationDirPath() 或明确的配置目录为基准,并在 load() 失败时记录实际路径。

五、本地化不只等于翻译文字

国际化是为多地区使用预留结构,tr()、TS、QM 属于这一层;本地化是针对目标地区给出正确内容和格式。切换英文 UI 不意味着数字、日期、货币和排序会自动符合英文习惯,格式化时应使用同一个目标 QLocale

cpp 复制代码
const QLocale locale("en_US");
priceLabel->setText(locale.toCurrencyString(123456.78, "USD"));
timeLabel->setText(locale.toString(QDateTime::currentDateTime(),
                                   QLocale::ShortFormat));

不要用字符串拼接实现"¥%1"或"%1 年 %2 月 %3 日"。货币符号位置、小数点、分组符、日期顺序和月名都随地区变化。语言代码和地区代码也不是一回事:en_USen_GB 同为英语,却可能使用不同日期、货币和计量习惯。

5.1 复数必须交给 tr()

cpp 复制代码
fileCountLabel->setText(tr("已选择 %n 个文件", nullptr, count));

第三个数量参数会让 lupdate 创建 numerus 翻译项。英语需要区分 1 file2 files,其他语言的复数规则还可能更多;不要先把 count 转为字符串再拼接单复数。TS 中的每种复数形式都应由 Linguist 完成。

5.2 数据与界面文案的边界

数据库的"状态码 -> 本地化名称"应由领域映射表或资源维护,而不是让 UI 直接翻译任意外部字符串。一个稳定的枚举值可以映射为固定 tr() 模板:

cpp 复制代码
QString statusText(TaskStatus status)
{
    switch (status) {
    case TaskStatus::Queued: return tr("等待中");
    case TaskStatus::Running: return tr("运行中");
    case TaskStatus::Done: return tr("已完成");
    }
    return {};
}

这样数据层依然只保存 TaskStatus,界面层才负责本地化显示;切换语言后可重新渲染模型或刷新视图,而不用修改业务数据。

六、从 tr() 到 QM:源码调用链怎么看

下面的私有实现名称来自 Qt 6 Core 源码,用于建立心智模型,不是应用程序应链接或包含的 private API。公开入口保持稳定:业务代码只使用 tr()QCoreApplication::translate()QTranslator 与事件处理。

图 3:翻译键不是只有一段可见文本。context、source text、disambiguation 和复数数量共同决定能否命中 QM 中的翻译;未命中时 Qt 用源文兜底。

6.1 tr() 先把类名变成 context

在带 Q_OBJECT 的类中,tr("保存设置")QObject::tr() 的类级便利入口。它通过该类的静态元对象取得类名,并概念上转交为:

cpp 复制代码
QCoreApplication::translate("TranslationDemo", "保存设置", nullptr, -1);

这里的 TranslationDemo 是翻译 context,不是窗口标题。改类名会改变翻译键并使既有 TS 条目失配,因此不要把类重命名当作无影响的纯重构;重命名后必须更新 TS,并让译者确认上下文是否仍正确。

6.2 QCoreApplication 逆序询问已安装翻译器

QCoreApplication::translate() 的实现位于 Qt Core 的应用程序逻辑中。它读取当前已安装的翻译器列表,按后安装优先的顺序调用每个 QTranslator::translate()。第一个返回非空译文的翻译器决定结果;全部未命中时,tr() 回退到原始 source text。

这解释了两个常见现象:没有加载 QM 时,中文源文仍能正常显示;同一 source text 在不同 context 下可以得到不同译文。也解释了为什么将"临时补丁翻译器"最后安装可覆盖旧译文,但这种覆盖应有清楚的所有权和卸载顺序。

6.3 QTranslator 用复合键检索 QM

QTranslator::load() 会读取 QM 的二进制翻译数据;QTranslator::translate() 使用 context、sourceText、disambiguation 和 n 查询其中的消息。QM 的设计目标是运行时高效查询,不是供人工编辑,所以源码分析时应把它理解为"已编译索引",而不是另一个 XML 文件。

翻译缺失的诊断顺序也因此固定:先确认 load() 返回 true,再确认已调用 installTranslator(),再检查类 context、源文、备注和复数参数是否与 TS 条目一致,最后检查当前显示文字是否在 LanguageChange 后重新计算。不要先在字符串外面补一个英文 if,那会掩盖真正的翻译键问题。

6.4 安装/卸载为何触发 LanguageChange

翻译器列表改变后,Qt 的应用层会向顶层 Widgets 发出语言变更通知,并将事件传播到控件树。QWidget::event() 最终使重写的 changeEvent() 获得 QEvent::LanguageChange;你的重翻译函数在这里重新设置文字。这个过程是事件驱动的,不应在每个控件上手工遍历并调用 repaint(),文本更新后控件会在正常事件循环中重绘。

这条路径只改变显示层。当前编辑框的用户输入、表格选择、任务进度和业务对象都不应被"重新初始化";因此 retranslateUi() 应只做文字、菜单、表头、提示等显示赋值,避免顺手重建模型或清空控件状态。

七、常见问题与排查顺序

7.1 TS 中没有新文字

确认文案被 tr()QCoreApplication::translate().ui 的可翻译属性包住,然后执行 update_translations。仅运行普通构建不会替代源文扫描;此外,改了源码却没有重新配置或使用了错误 build 目录,也会看到旧 TS。

7.2 Linguist 已翻译,运行时仍显示源文

依次检查:TS 是否已保存,release_translations 是否重新生成 QM,QTranslator::load() 是否返回 true,资源路径是否为 :/i18n/...,翻译器是否仍存活并被安装。最后检查 context 是否因类名或 QObject::tr() 的使用方式而不同。

7.3 切换语言后只有部分控件改变

.ui 控件需调用 ui->retranslateUi(this);代码创建的控件需在 changeEvent() 中重新 setText(tr(...));模型表头、菜单、状态栏和延迟创建的对话框也必须纳入同一个重翻译入口。不要在语言切换槽中只改当前点击的按钮。

7.4 翻译器装上后程序异常或切回语言失效

检查 QTranslator 生命周期和移除顺序。翻译器必须在 removeTranslator() 后才允许析构;切换时先移除旧翻译器、成功加载新 QM 后再安装。对于失败路径,要让界面保留可读的源文并记录错误,而不是留下半安装状态。

7.5 %1%n& 被翻坏了

这些是格式和交互协议的一部分。Linguist 验证能发现一部分占位符不一致;发布前还应实际点击菜单、触发复数、传入多个参数。不要依赖肉眼检查一条英文样例覆盖所有复数形式。

八、入门阶段 API 速查表

API / 工具 职责 入门阶段的正确使用
tr("...") 声明类上下文中的可翻译源文 所有需要随界面语言变化的固定用户可见文案,都应该进入翻译系统;运行数据用 .arg() 传入
QObject::tr("...") 自由函数或公共文案的翻译入口 少量跨类文案可用,不要让它吞掉业务 context
QCoreApplication::translate() 显式指定 context 的底层公开入口 通常由 tr()uic 生成代码调用
lupdate / update_translations 从 C++、.ui 提取和更新 TS 每次源文修改后执行
Qt Linguist 编辑、校验、完成 TS 翻译 不手改 QM;发布前处理 unfinished
lrelease / release_translations 将 TS 编译成 QM 翻译确认后、打包前执行
QTranslator::load() 读取 QM 检查返回值,使用资源绝对路径或稳定安装路径
installTranslator() 让后续查询使用翻译器 保证翻译器生命周期;后安装者优先
QEvent::LanguageChange 通知窗口重新取文本 changeEvent() 中调用 retranslateUi()
QLocale 本地化数字、日期、货币、排序 与 UI 目标地区一致,避免手工字符串拼接

总结

Qt 国际化的上手点很小:把界面固定文字写成 tr("..."),用 qt_add_translations() 配置 TS 与 QM,然后在 Linguist 中完成翻译。真正可靠的工程实践是把它当成一条构建流水线:源文更新由 lupdate 提取,TS 是协作资产,QM 是运行产物,QTranslator 负责查询,LanguageChange 负责让已显示的 Widgets 重新取文案。

从源码角度看,tr() 并不是全局字符串替换。它携带类 context,将查询交给 QCoreApplication 已安装的翻译器列表;QTranslator 再用 context、源文、备注和复数数量命中 QM 索引。理解这条链路后,翻译不生效、切换不刷新、同词不同义和临时覆盖等问题都有了可验证的排查入口。

最后记住边界:翻译文字是 i18n 的一部分,日期、货币、数字和复数规则同样需要本地化;显示层语言切换不能污染业务数据,也不应该丢失用户正在编辑或选择的状态。


第一阶段至此完成。下一阶段将从信号与槽进入 Qt 的核心机制:先理解它为什么能替代许多手写回调,再逐步走进连接、分发与队列投递的源码路径。

相关推荐
程与留2 小时前
14_Qt 样式表(QSS)入门(语法、选择器、美化实战)
c++·qt
Quz9 小时前
QML 拖拽篇:接收拖拽与数据交换
qt
欧特克_Glodon11 小时前
OpenCV计算机视觉开发入门与实践<二十七>:图像分割概述
c++·人工智能·opencv·计算机视觉
蒸蒸yyyyzwd11 小时前
cpp 选手秋招学习笔记 day21
c++·面试·八股
「QT(C++)开发工程师」1 天前
C++ auto 用法详解
开发语言·c++
OPEN-F1 天前
C++STL教程:容器适配器与实用工具
开发语言·c++
OPEN-F1 天前
C++模板教程:变参模板、折叠表达式与SFINAE
java·开发语言·c++
有点。1 天前
C++二叉搜索树进阶
开发语言·c++
HugoStudio_SWAN1 天前
【擦除重绘】C++ 控制台动画:弹跳 Logo DVD 屏保效果
开发语言·c++·学习·程序人生