Content Table

线程的上下文

Qt 中每个 QObject 及其子类的对象都有自己的线程上下文环境,即对象所属的线程,对象属于创建它的函数执行时所在的线程,例如在 Ui 线程中的函数里创建了对象 ops,则 ops 属于 Ui 线程。使用 QObject::thread() 获取对象所属线程,可以使用 QObject::moveToThread(otherThread) 移动一个对象到另一个线程。由于跨线程调用函数有可能会造成程序崩溃,所以有比较了解代码在执行时它所处的线程。

下面以示例演示不同情况下代码执行时所处的线程:

  • Widget 所属线程
  • 线程对象所属线程
  • Lambda 的方式处理信号槽
  • Qt 5 函数指针的方式处理信号槽
  • 传统 signal slot 的方式处理信号槽

注意: 下面的注释都是基于示例代码的,为了不让描述的太过繁琐,便于帮助理解,有些描述理论上可能是不精确的,需要大家自行更进一步的分析。

上传 Blob URL 指向的文件

Blob 是 Binary large object 的简称,格式为 blob:http://localhost:8080/da126298-1b6b-4dfb-8a92-2e3ccbee611d, 是一种伪协议,只能在浏览器本地使用,例如在使用 TinyMCE 粘贴从 Word 复制的图片时,得到一个 Blob URL,可以使用下面的方式把 Blob 对应的图片上传到服务器:

  1. 使用 XMLHttpRequest 获取 Blob URL 源数据的 Blob 文件对象
  2. 上传 Blob 到服务器

下面以上传图片 Blob 为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
const xhr = new XMLHttpRequest();
xhr.open('GET', blobUrl); // blob:http://localhost:8080/da126298-1b6b-4dfb-8a92-2e3ccbee611d
xhr.responseType = 'blob';

xhr.onreadystatechange = function() {
if (this.readyState === 4 && this.status === 200) {
const imageBlob = this.response;
const imageType = imageBlob.type;
let imageName = null;

// 根据文件的类型确定文件名,否则服务端得到的文件名为 blob,没有后缀
if (imageType.includes('png')) {
imageName = 'uploaded-image.png';
} else if (imageType.includes('gif')) {
imageName = 'uploaded-image.gif';
} else if (imageType.includes('bmp')) {
imageName = 'uploaded-image.bmp';
} else {
imageName = 'uploaded-image.jpg';
}

// 上传 Blob 图片
const form = new FormData();
form.append('file', imageBlob, imageName); // 第三个参数为文件名

$.ajax({
type: 'POST',
url : Urls.FORM_UPLOAD_TEMPORARY_FILE,
data: form,
processData: false,
contentType: false,
}).done(function(result) {
// 把服务器返回的图片 URL 插入到需要的地方
console.log(result);
});
}
};

xhr.send();

获取 Blob 对象的核心代码为:

1
2
3
4
5
6
7
8
9
var xhr = new XMLHttpRequest();
xhr.open('GET', 'blob:http://your.blob.url.here', true);
xhr.responseType = 'blob';
xhr.onload = function(e) {
if (this.status == 200) {
var blob = this.response; // blob is now the blob that the object URL pointed to.
}
};
xhr.send();

使用 Axios 的为:

1
2
3
4
5
6
7
axios({
method: 'get',
url: blobUrl,
responseType: 'blob',
}).then(function(response) {
let blob = response.data;
})

Qt 生成报表

开发中经常会需要输出报表,方法很多,这里就不一一去列举对比各种方法的优缺点,只介绍使用字符串模板输出 PDF 报表的方式,能够满足绝大多数时候的需求:

  1. 字符串模板 + 数据输出 HTML
  2. HTML 输出 PDF

输出 HTML

为了生成 HTML,可以在程序中使用字符串拼接的方式,但这种方式对于复杂点的 HTML 就会非常麻烦,不推荐使用,我们这里将使用字符串模板引擎 Inja 来生成 HTML,使用步骤如下:

  1. 下载 Inja

  2. 解压,得到的目录结构如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    inja-2.1.0
    ...
    ├── single_include
    │   └── inja
    │   └── inja.hpp
    └── third_party
    └── include
    └── nlohmann
    ├── LICENSE.MIT
    └── json.hpp
  3. 复制 single_include 下的 inja 目录和 third_party/include 下的 nlohmann 目录到项目的 lib 目录下:

    1
    2
    3
    4
    5
    6
    lib
    ├── inja
    │   └── inja.hpp
    └── nlohmann
    ├── LICENSE.MIT
    └── json.hpp
  4. Qt 工程的 pro 文件中添加包含路径: INCLUDEPATH += lib

  5. 最简单的例子:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    #include <inja/inja.hpp>

    using namespace inja;
    using json = nlohmann::json;

    int main(int argc, char *argv[]) {
    json data;
    data["name"] = "world";

    render("Hello {{ name }}", data); // Returns std::string "Hello world"

    return 0;
    }

    Inja 的模板使用了 Mustache 风格的语法,上面例子中 { { name } }data["name"] 的值 world 替换掉,生成字符串 Hello world,其中 Inja 使用 JSON 库 nlohmann json 传递数据给模板。

动态加载 JS 和 CSS

动态加载 JS 文件的正确姿势中列举了多种加载 JS 的方式,下面摘出个人最喜欢的方式,用于动态加载 JS 和 CSS。

动态加载 JS

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<title>Load JS</title>
</head>

<body>
<script>
/**
* 使用 Promise 异步加载 JS
*
* @param {String} url JS 的路径
* @param {String} id JS 的 <style> 的 ID,如果已经存在则不再重复加载,默认为时间戳+随机数
* @return 返回 Promise 对象, then 的参数为加载成功的信息,无多大意义
*/
function loadJs(url, id = Date.now() + '-' + Math.random()) {
return new Promise(function(resolve, reject) {
// 避免重复加载
if (document.getElementById(id)) {
resolve('success: ' + url);
return;
}

var script = document.createElement('script');

if (script.readyState) { // IE
script.onreadystatechange = function() {
if (script.readyState == 'loaded' || script.readyState == 'complete') {
script.onreadystatechange = null;
resolve('success: ' + url);
}
};
} else { // Other Browsers
script.onload = function() {
resolve('success: ' + url);
};
}

script.onerror = function() {
reject(Error(url + ' load error!'));
};

script.type = 'text/javascript';
script.id = id;
script.src = `${url}?hash=${id}`;
document.getElementsByTagName('head').item(0).appendChild(script);
});
}

// 加载 JS 后执行 then 的回调函数
loadJs('http://cdn.bootcss.com/jquery/1.9.1/jquery.min.js', 'jq').then(msg => {
console.log(msg);
});

// 加载完所有 JS 后执行 then 的回调函数
const one = 'http://cdn.bootcss.com/jquery/1.9.1/jquery.min.js';
const two = 'https://github.githubassets.com/assets/frameworks-5c304257.js';
Promise.all([loadJs(one, 'jq'), loadJs(two)]).then(results => {
console.log(results);
})
</script>
</body>

</html>

汉若塔

传说越南河内某间寺院有三根银棒,上串 64 个金盘,金盘尺寸由下到上依次变小。寺院里的僧侣依照一个古老的预言,以下述规则把这些盘子从第一根银棒移至第三根银棒上 (可以借助第二根银棒):

  • 每次只能移动一个圆盘
  • 大盘不能迭在小盘上面

预言说当这些盘子移动完毕,世界就会灭亡。这个传说叫做梵天寺之塔问题,即汉若塔问题。

用递归实现的代码很简洁:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class Test {
public static void main(String[] args) {
hanoi(3, 'A', 'B', 'C');
}

/**
* 把 n 个盘子从 a 柱移动到 c 柱,可以借助 b 柱
*
* @param n: 盘子的数量
*/
public static void hanoi(int n, char a, char b, char c) {
if (n == 1) {
move(1, a, c);
return;
}

hanoi(n-1, a, c, b); // 小和尚借助 c 柱把 a 柱最上面的 n-1 个盘子移动到 b 柱
move(n, a, c); // 老和尚把最下面的一个盘子从 a 柱移动到 c 柱
hanoi(n-1, b, a, c); // 小和尚再借助 a 柱把 b 柱上的 n-1 个盘子移动到 c 柱
}

// sn 是盘子的序号: 开始时 a 柱上的盘子序号从上到下依次为 1, 2, 3, ..., n
public static void move(int sn, char from, char to) {
System.out.printf("%d: %c -> %c\n", sn, from, to);
}
}

代码不多,但是一直理解不了这个递归为什么要这么写,直到大学时人工智能的老师给了个记忆的办法,从此就理解了:

老和尚比较懒 (有点大不敬,忽略之),于是就叫小和尚把第一根柱子最上面的 n-1 个盘子借助第三根柱子先搬到第二根柱子上,然后老和尚自己把第一根柱子最下面的那个大盘子搬到第三根柱子上,再叫小和尚把第二根柱子上的 n-1 个盘子借助第一根柱子搬到第三根柱子上。

斐波那契数列

斐波那契数列 (意大利语: Successione di Fibonacci),又称黄金分割数列、费波那西数列、费波拿契数、费氏数列,指的是这样一个数列:1、1、2、3、5、8、13、21、……在数学上,斐波那契数列以如下被以递归的方法定义:F1=1,F2=1,Fn=F(n-1) + F(n-2) (n>2,n∈N*),使用递归实现如下:

1
2
3
4
5
public static int fib(int n) {
if (n == 1 || n == 2) { return 1; }

return fib(n-1) + fib(n-2);
}

求 fib(6) 斐波那契数列的递归过程如图:

> 重叠子问题 (Overlap subproblem): 把大问题被分解为多个小问题进行求解,小问题重复的出现多次。

答疑表设计

答疑主要涉及 2 个表,问题表 qa_queston 和回复表 qa_reply,核心字段如下:

**数据存储逻辑:** * 问题存储到表 qa_question * 每个回复都存储问题 ID: `question_id` * 每个回复都保存第一级回复 ID: `top_reply_id`,为了方便找到**回复树**下的所有回复 * 对问题进行的回复存储到表 qa_reply * `parent_id` 为 0 * `top_reply_id` 为此回复的 ID * 对回复进行的回复存储到表 qa_reply * 回复是一个有层级的树状结构,所以使用了 `parent_id` * `parent_id` 为被回复的`回复 ID` * `top_reply_id` 为此回复所在第一级`回复 ID`

此设计的优点:

可以使用一条 SQL 语句分页查询出问题的 N 个一级回复、以及这些一级回复下所有相关的回复:

  • 查找到问题的 n 个第一级回复的 top_reply_id x
  • 查找问题的回复中所有第一级回复为 x 的回复
1
2
3
4
5
SELECT q.*, r.*
FROM qa_question q
JOIN (SELECT question_id, top_reply_id FROM qa_reply WHERE question_id = 1 AND parent_id = 0 LIMIT 0, 10) t ON t.question_id = q.id
LEFT JOIN qa_reply r ON r.question_id = q.id AND r.top_reply_id = t.top_reply_id
ORDER BY r.created_at DESC;

parent_id 为 0,说明是第一级回复。

如果回复再增加 level 属性,还可以对回复层数进行过滤,当层数很多时,可以动态请求更深层次的回复,避免一次获取太多数据。

此外,根据业务,可以对问题表增加字段如是否加精、推荐置顶、点赞数量、回复数量等,回复表增加点赞数量、被回复者的名字 (方便显示) 等,对点赞、取消点赞、点击推荐等行为记录到日志表中。


以下部分用于测试

问题和回复

1
2
3
4
5
6
7
8
9
1. 特别定制「乌龙茶」,有人跟我一样只喜欢有味道的,或者冰的饮品嘛?
1. 东方树叶,每天基本都会喝 1,2 瓶
2. 抹茶粉确实很流行,但是和我提的浓缩粉其实是两种产品
3. 炭焙的一般口味略重
4. 好处就是完全不伤胃
2. 找一个靠谱的设计师长期合作
5. 明天早上起来加你
6. 怎么联系
7. 本人做一些兼职

清空数据库

1
2
TRUNCATE TABLE qa_question;
TRUNCATE TABLE qa_reply;

问题一

1
2
3
4
5
6
INSERT INTO qa_question(id, clazz_id, user_id, user_name, content) VALUES (1, 1, 30, 'Bob', '特别定制「乌龙茶」,有人跟我一样只喜欢有味道的,或者冰的饮品嘛?');

INSERT INTO qa_reply(id, question_id, parent_id, top_reply_id, user_id, user_name, content) VALUES (1, 1, 0, 1, 31, 'Tom', '东方树叶,每天基本都会喝 1,2 瓶');
INSERT INTO qa_reply(id, question_id, parent_id, top_reply_id, user_id, user_name, content) VALUES (2, 1, 0, 2, 31, 'Tom', '抹茶粉确实很流行,但是和我提的浓缩粉其实是两种产品');
INSERT INTO qa_reply(id, question_id, parent_id, top_reply_id, user_id, user_name, content) VALUES (3, 1, 2, 2, 31, 'Tom', '炭焙的一般口味略重');
INSERT INTO qa_reply(id, question_id, parent_id, top_reply_id, user_id, user_name, content) VALUES (4, 1, 2, 2, 31, 'Tom', '好处就是完全不伤胃');

问题二

1
2
3
4
5
INSERT INTO qa_question(id, clazz_id, user_id, user_name, content) VALUES (2, 1, 30, 'Bob', '找一个靠谱的设计师长期合作?');

INSERT INTO qa_reply(id, question_id, parent_id, top_reply_id, user_id, user_name, content) VALUES (5, 2, 0, 5, 32, 'Max', '明天早上起来加你');
INSERT INTO qa_reply(id, question_id, parent_id, top_reply_id, user_id, user_name, content) VALUES (6, 2, 5, 5, 32, 'Max', '怎么联系');
INSERT INTO qa_reply(id, question_id, parent_id, top_reply_id, user_id, user_name, content) VALUES (7, 2, 0, 7, 32, 'Max', '本人做一些兼职');

查询问题和回复

只在第一级回复上分页,同时查询出这个回复的所有后代:

  1. 查找到问题的 n 个第一级回复的 top_reply_id x
  2. 查找问题的回复中所有第一级回复为 x 的回复
1
2
3
4
5
SELECT q.*, r.*
FROM qa_question q
JOIN (SELECT question_id, top_reply_id FROM qa_reply WHERE question_id = 1 AND parent_id = 0 LIMIT 0, 10) t ON t.question_id = q.id
LEFT JOIN qa_reply r ON r.question_id = q.id AND r.top_reply_id = t.top_reply_id
ORDER BY r.created_at DESC;

修改 question_id 查询不同问题的回复。

iView 使用 JSX render 树

iView 提供了树的组件 Tree,复杂的节点可以使用 render 自定义显示:

  • <Tree>:render 渲染所有节点
  • 节点的属性 render 覆盖全局的 render,优先级更高

文档中使用的 render 语法比较繁琐,下面使用 JSX 进行改写,实现下面的树,只是额外增加了样式的变化:

  • 高亮当前行
  • 高亮被点击的节点名字 (使用了 jQuery)
  • 鼠标移动到节点所在的行时才显示这一行的按钮

MyBatis 的 ResultMap 注意事项

MyBatis 的 ResultMap 比较灵活,使用时需要注意以下几点:

  • ResultMap 中 <result> 的 property 对应 Bean 的属性名,column 对应 SELECT 的字段名:
    • column 在 SELECT 中字段中:
      • 正确: property 对应的属性存在时正确
      • 报错: property 对应的属性不存在时报错
      • 说明: 没有写相关 <result> 时不报错,Bean 有与 column 同名属性时自动设置属性的值,没有时忽略
    • column 不在 SELECT 中字段中:
      • 正确: property 对应的属性存在时不报错
      • 正确: property 对应的属性不存在时不报错
    • 结果: 只要 column 在 SELECT 字段中就需要注意,否则可忽视:
  • SELECT 的字段名和 ResultMap 里属性名相同时,可以不用写 <result>,MyBatis 会自动映射对应的 column 和 property,也就是说如果 SELECT 的字段名和 Bean 的属性名都是同名的,即使 ResultMap 为空,Bean 的属性都会自动设置好,例如下面的 id 和 info 都注释掉了,它们的属性值仍然被正确的设置了
    • 但是,如果 ResultMap 中使用了 <collection> 或者 <association>,只有明确使用 <result> 或者 <id> 设置的属性会被赋值,Bean 中其他属性不会被赋值,也就是自动映射此时默认是关闭的,要开启的话需要在 resultMap 中设置属性 autoMapping="true",可参考 MyBatis Auto Mapping,但是也需要手动写 <id> 标签,否则就可能出问题 (不能正确分组)
  • SELECT 中的字段可以比 ResultMap 里的 <result> 多或者少
  • SELECT 的字段名和 ResultMap 中 <result> 的 property 不同时,需要使用 column 进行映射
  • 自动映射:
    • if column name are equals java bean field name ignore case, then auto mapping works
    • for nested collection/association, we need to add autoMapping=true manually
    • if column name or alias are different from java bean field name, we need to map them manually

下面的代码能够正常运行,对这几条进行了演示:

Java Bean:

1
2
3
4
5
6
7
8
public class Demo {
private int id;
private String info;
private boolean marked;

// Getters and setters
}

SQL:

1
2
3
<select id="findDemoById" parameterType="int" resultMap="DemoResultMap">
SELECT id, info, is_marked FROM demo WHERE id=#{id}
</select>

ResultMap:

1
2
3
4
5
6
7
8
9
<resultMap id="DemoResultMap" type="Demo">
<!--<id property="id" column="id"/>-->
<!--<result property="info" column="info"/>-->

<result property="marked" column="is_marked"/>

<!-- extra 在 SELECT 的字段名和 Bean 的属性里都不存在: 不报错,忽略即可 -->
<result property="extra" column="extra"/>
</resultMap>