枚举与 QFlags

传统的 C++ 编程中,通常使用整数来保存 enum 的逻辑运算结果 (与、或、非、异或等),在进行逻辑运算的时候没有进行类型检查,一个枚举类型可以和其他的枚举类型进行逻辑运算,运算的结果可以直接传递给接收参数为整数的函数。

Qt 中,模板类 QFlags<Enum> 提供了类型安全的方式保存 enum 的逻辑运算结果解决上面的这几个问题,这种方式在 Qt 里很常见,例如设置 QLabel 对齐方式的函数是 QLabel::setAlignment(Qt::Alignment) (typedef QFlags<Qt::AlignmentFlag> Qt::Alignment),这就意味着传给 setAlignment 的参数只能是枚举 Qt::AlignmentFlag 的变量、它们的逻辑运算结果或者 0,如果传入其他的枚举类型或者非 0 值,编译时就会报错:

1
2
3
4
label->setAlignment(0); // OK
label->setAlignment(Qt::AlignLeft | Qt::AlignTop); // OK

label->setAlignment(Qt::WA_Hover); // Error: 编译时报错

想要把我们定义的枚举类型和 QFlags 一起使用,需要用到两个宏:

  • Q_DECLARE_FLAGS(Flags, Enum):
    • Enum 是已经定义好的枚举类型
    • 展开的结果为 typedef QFlags<Enum> Flags
  • Q_DECLARE_OPERATORS_FOR_FLAGS(Flags):
    • Flags 就是类型 QFlags<Enum>
    • 给 Flags 定义了运算符 |,使得 Enum 和 Enum,Enum 和 Flags 能够使用或运算符 |,结果为 Flags

使用 QFlags 时需要留意以下几点:

  • QFlags 其实就是用于位操作,设置它保存的数值的某一位为 1 或者为 0,所以和 QFlags 一起使用的枚举类型,其变量的值需要是 2 的 n 次方,即 0x00, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, …,它们的特点是其二进制位中只有一位是 1,其他位全部为 0,这样每一个枚举值只会和 QFlags 中的某一个位对应,不会出现交叉的情况
  • 调用函数 QFlags::setFlag(Enum flag, bool on = true),on 为 true 时设置 flag 对应的位为 1,on 为 false 时设置 flag 对应的位为 0,设置 flag 对应的位为 1 还可以使用运算符 |
  • 调用函数 QFlags::testFlag(Enum flag) 测试 flag 对应的位是否为 1
  • 整数转为 QFlags: 把整数作为 QFlags 构造函数的参数创建一个 QFlags 变量
  • QFlags 转为整数: 调用 int(flags) 把 QFlags 变量转换为整数值

C++ 临时变量的析构

Qt 中经常会使用 QString::toUtf8() 获取字符串的 UTF-8 数组,例如下面这样使用:

1
2
3
4
void foo(const char *data) { ... }

const char *data = QString::toUtf8().constData();
foo(data);

看上去没啥问题,其实这个代码有很严重的问题,一般不是经验很丰富的程序员很难发现这个 Bug,很可能会导致程序崩溃退出,但就是找不到为什么。这是因为 QString::toUtf8() 返回的是一个 QByteArray 的栈变量,第 3 行语句中的 QByteArray 是一个临时变量,这行语句结束时这个变量就被析构了,指针 data 指向的内存也被回收,所以下面使用的 data 指向的内存已经被释放了,难怪程序会崩溃了。把代码修改为下面的样子则就能够正确运行了:

1
2
3
4
5
6
7
// [1]
foo(QString::toUtf8().constData()); // 很奇怪是不是?

// [2] 先保存 ba
QByteArray ba = QString::toUtf8();
const char *data = ba.constData();
foo(data);

这个问题涉及到临时变量的析构,也许还不太明白是怎么回事,下面的程序就来模拟上面的情况,一看就明白了:

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
#include <QDebug>

class B {
public:
B() {
qDebug() << "B()";
}

~B() {
qDebug() << "~B()";
}

int value = 10;
};

// A 中有一个 B* 的成员变量
class A {
public:
A() {
qDebug() << "A()";
b = new B();
}

~A() {
delete b;
qDebug() << "~A()";
}

B *b = nullptr;
};

// 返回 A 的栈变量对象
A a() {
return A();
}

void foo(B *b) {
qDebug() << b->value;
}

int main(int argc, char** argv) {
// 提示: 下面的语句中 a() 返回的变量是一个临时变量,在语句结束时才析构提示

foo(a().b); // OK: 函数 foo() 执行完时 a() 创建的 A 和 B 才析构,在函数 foo() 中放心的使用 b

B *b = a().b; // Error: 语句结束时 A 和 B 才析构,此时 b 已经被回收,所以不要在后面的语句中使用 b
qDebug() << b->value; // Error: b 已经被析构,b->value 的值不是 10,而是随机的

qDebug() << "main() 结束";
return 0;
}

输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
A()
B()
10
~B()
~A()

A()
B()
~B()
~A()
1859919056 // 注意: 这里不是 10 呢

main() 结束

Vue 尽量不用 scoped

在 Vue 中,为了减少样式的冲突,可以使用 scoped 使得生成的 DOM element 和 class 都带上随机属性,但有利有弊,总的来说,不推荐使用 scoped,因为使用 scoped 后就不能跨级设置组件的样式了。

跨级: 组件 A 中直接使用组件 B,组件 B 中直接使用组件 C,因为 C 没有在 A 中直接使用,所以我们说 A 和 C 跨级了。

线程的上下文

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=Fn-1+Fn-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_question
  • 每个回复都存储问题的 ID
  • 每个回复都保存第一级回复的 reply_id
  • 对问题进行的回复存储到表 qa_reply
    • target_reply_id 为 0
    • first_level_reply_id 为此回复的 ID
    • level 为 1
  • 对回复进行的回复存储到表 qa_reply
    • target_reply_id 为被回复的回复的 ID
    • first_level_reply_id 为此回复所在第一级回复的 ID
    • level 为被回复的回复的 level + 1
    • 回复是一个有层级的树状结构

此设计的优点: 可以使用一条 SQL 语句分页查询出问题的 N 个一级回复、以及这些一级回复下所有相关的回复:子查询得到第一级的 N 个 reply_id, 然后查找所有 first_level_reply_id 为这 N 个 reply_id 的所有回复:

1
2
3
4
5
6
7
SELECT qa_reply.*
FROM qa_reply,
(SELECT id FROM qa_reply
WHERE question_id = #{question_id} AND target_reply_id = 0
LIMIT #{offset}, #{count}
) AS fl_ids
WHERE question_id = #{question_id} AND qa_reply.first_level_reply_id = fl_ids.id

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

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

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

iView 使用 JSX render 树

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

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

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

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


MyBatis 的 ResultMap 注意事项

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

  • ResultMap 中 <result> 的 property 对应 Bean 的属性名,column 对应 SELECT 的字段名:
    • column 存在,property 对应的属性存在时正确
    • column 存在,property 对应的属性不存在时报错
    • column 存在,没有写相关 <result>不报错,Bean 有同名属性时自动设置属性的值,没有时忽略
    • column 不存在,property 对应的属性存在时不报错
    • column 不存在,property 对应的属性不存在时不报错
  • SELECT 的字段名和 ResultMap 里属性名相同时,可以不用写 <result>,MyBatis 会自动映射对应的 column 和 property,也就是说如果 SELECT 的字段名和 Bean 的属性名都是同名的,即使 ResultMap 为空,Bean 的属性都会自动设置好,例如下面的 id 和 info 都注释掉了,它们的属性值仍然被正确的设置了

  • SELECT 中的字段可以比 ResultMap 里的 <result> 多或者少

  • SELECT 的字段名和 ResultMap 中 <result> 的 property 不同时,需要使用 column 进行映射

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

Java Bean:

1
2
3
4
5
6
7
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>

常用排序

下面列举常用的排序算法的实现:

  • 冒泡排序
  • 插入排序
  • 选择排序
  • 快速排序
  • 归并排序
  • 堆排序

排序的时候, 需要大量的交换数组中的 2 个元素, 使用下面的函数 swap() 进行交换:

1
2
3
4
5
6
7
8
/**
* 交换数组中指定下标的 2 个元素的值
*/
public static void swap(int[] a, int indexA, int indexB) {
int temp = a[indexA];
a[indexA] = a[indexB];
a[indexB] = temp;
}

冒泡排序

原理:

  1. 比较相邻的元素,如果第一个比第二个大,就交换他们两个。
  2. 对每一对相邻元素做同样的工作,从开始第一对到结尾的最后一对,最后的元素应该会是最大的数。
  3. 针对所有的元素重复以上的步骤,除了最后一个。
  4. 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 冒泡排序
*/
public static void bubbleSort(int[] a) {
for (int i = 1; i < a.length; ++i) {
for (int j = 0; j < a.length-i; ++j) {
// 如果 a[j] 大于 a[j+1], 则把大的交换到后面
if (a[j] > a[j+1]) {
swap(a, j, j+1);
}
}
}
}

FastJson 反序列化 Boolean

JSON 字符串反序列化为 Java 对象时, 不只是 true 和 false 能够转换为 boolean 变量的值:

  • boolean:
    • true: true
    • false: false
  • 数字:
    • 1: true
    • 0: false
    • 其他数字: false
  • 字符串:
    • “true”: true (大小写不敏感)
    • “false”: false (大小写不敏感)
    • “1”: true
    • “0”: false
    • 其他字符串抛异常
1
2
3
4
5
6
7
8
9
10
11
{ "visible": true }    // true
{ "visible": false } // false
{ "visible": 1 } // true
{ "visible": 0 } // false
{ "visible": 3 } // false
{ "visible": "true" } // true
{ "visible": "True" } // true
{ "visible": "false" } // false
{ "visible": "falsE" } // false
{ "visible": " true" } // Exception
{ "visible": "Bla" } // Exception

Vue 中使用 TinyMCE

TinyMCE 是一个功能强大的富文本编辑器:

  • 支持从 Word 中复制的文本格式
  • 拖拽修改图片的大小
  • 表格拖拽修改单元格大小
  • 提供了三种编辑模式
    • Full featured: 默认显示工具栏
    • Inline: 编辑器的到焦点时才显示工具栏
    • Distraction Free: 选中文本后才显示工具栏
  • 同一页面中可以创建多个编辑器
  • 界面美观简洁, 使用 CSS 修改样式很方便, 工具栏按钮使用 SVG 图片
  • 插件开发简单, 甚至不需要开发插件就能向工具栏插入按钮

TinyMCE 提供了 cloud 版本, 也可以下载到本地使用.

使用 TinyMCE 只需要 3 步:

  1. 引入 TinyMCE
  2. 为 TinyMCE 准备一个 DOM
  3. 基于准备好的 DOM,初始化 TinyMCE 实例

下图是使用默认参数创建的 TinyMCE 编辑器:

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<!DOCTYPE html>
<html>

<head>
<meta charset="utf-8">
<title>TinyMCE</title>
<!-- [1] 引入 TinyMCE -->
<script src="tinymce/tinymce.min.js"></script>
</head>

<body>
<!-- [2] 为 TinyMCE 准备一个 DOM -->
<div id="editor">道格拉斯•狗</div>

<script>
// [3] 基于准备好的 DOM,初始化 TinyMCE 实例
tinymce.init({ selector: '#editor' });
</script>
</body>

</html>

官方提供了非常丰富的文档, 请访问 https://www.tiny.cloud/docs 进行阅读, 了解 TinyMCE 更多的使用方法.

Vue 中使用 Echarts

Echarts, 一个使用 JavaScript 实现的开源可视化库,可以流畅的运行在 PC 和移动设备上,兼容当前绝大部分浏览器(IE8/9/10/11,Chrome,Firefox,Safari等),底层依赖轻量级的矢量图形库 ZRender,提供直观,交互丰富,可高度个性化定制的数据可视化图表。

Vue 项目中使用 yarn 安装 Echarts: yarn add echarts, 然后在页面中使用 Echarts 步骤如下:

  1. 为 ECharts 准备一个具备大小 (宽高) 的 DOM

  2. 引入 echarts:

    • 全部引入:

      1
      import echarts from 'echarts'; // 方便, 但是也同时引入了很多不需要的组件
    • 按需引入:

      1
      2
      import echarts from 'echarts/lib/echarts'; // 引入基本模板
      import bar from 'echarts/lib/chart/bar'; // 引入柱状图组件
  3. 基于准备好的 DOM,初始化 Echarts 实例

  4. 设置 Echarts 图表数据

入门示例

使用 Echarts 实现的柱状图:

Tio WebSocket 经验

使用 Tio 实现 WebSocket, 可以在 tio-websocket-showcase 的基础上进行修改 (据说能够达到企业级性能, 单机支持 30 万连接). 利用 Tio 提供的绑定功能实现消息群发和给指定的用户发送消息, 并且把用户对象直接存储到 ChannelContext 上, 还能省去自己管理用户的麻烦, Tio 也提供了心跳检测功能, IP 黑名单, 流量监控等, 我们只需要关注与业务层代码即可, 下面介绍一些相关的经验:

  • 应用中只有一个 GroupContext, 发送消息, 获取小组信息等

  • 一个连接对应一个 ChannelContext (ip:port), 可以使用 setAttribute() 存储业务数据, getAttribute() 获取数据

  • 绑定 (使用 Tio 进行绑定, 可参考让网络编程更轻松和有趣 t-io):

    • userid: 一个 userid 可以绑定多个 ChannelContext (实现同一个账号多个设备登录)

      1
      2
      bindUser(ChannelContext channelContext, String userid)
      SetWithLock<ChannelContext> getChannelContextsByUserid(GroupContext groupContext, String userid)
    • token: 一个 token 可以绑定多个 ChannelContext (实现同一个账号多个设备登录)

      1
      2
      bindToken(ChannelContext channelContext, String token)
      SetWithLock<ChannelContext> getChannelContextsByToken(GroupContext groupContext, String token)
    • bsId: 一个 bsId 只能绑定一个 ChannelContext (实现同一个账号只允许登录一个设备)

      1
      2
      bindBsId(ChannelContext channelContext, String bsId)
      ChannelContext getChannelContextByBsId(GroupContext groupContext, String bsId)

      下面介绍的绑定以 bsId 为例, 其他的方式参考实现即可

简单的 Mindmap

树形结构如果作为侧边栏, 使用 zTree 比较合适, 下图所示的平铺展开整棵树, 使用 zTree 估计就不适合了, 在此我们使用 jQuery 的另一个插件 Simple jQuery Mind Map Diagram Plugin - mindmap 来实现.

文档的例子使用静态的 HTML 标签准备 mindmap 需要的树形结构的数据, 实际中大多情况下树形结构的数据是存储在文件或者通过接口返回, 我们这里展示递归的方式遍历树形结构的数据创建 mindmap 需要的 HTML 标签. 因为每个节点都是 HTML 的标签, 能很方便的使用 CSS 定制样式.

Ckeditor 5 简介

新版富文本编辑器 Ckeditor 5 比 Ckeditor 4 使用更简单, 下载 zip 包后只需要里面的 ckeditor.js (连样式文件都不需要), 像下面这样 3 步就可以了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<!DOCTYPE html>
<html lang="en" dir="ltr">

<head>
<meta charset="utf-8">
<title>Ckeditor Demo</title>
</head>

<body>
<div id="editor"></div> <!-- [1] -->

<script src="ckeditor.js"></script> <!-- [2] -->
<script>
ClassicEditor.create(document.querySelector('#editor')); // [3]
</script>
</body>

</html>

只是简单的使用, 默认的功能就可以了. 如果还需要更多的功能, 例如集成多种编辑模式, 使用非默认自带插件, 自定义插件等就不能使用直接下载的代码 (You can’t add a plugin to an existing build), 需要我们自己编译源码才行, 下面就一一进行介绍.

圆角 button and widget 组合

下图的控件估计很多数人会使用绘图的方式实现, 代码实现起来虽然不会很复杂, 但是要控制得到满意的效果就不太容易, 对于我来说, 能不使用继承就不使用继承 (例如给某些控件处理事件时, 能用 eventFilter 实现就尽量避免使用继承, 因为继承又要多出几个源码文件 =_=!!!). 巧妙的利用 QSS, 一个 QPushButton 和 QWidget 也能实现.

下面就来看实现的方法吧:

  1. 在 QtCreator 中如下图在一个 QWidget 中放一个 QPushButton, 右边放一个 Horizontal Spacer, 然后水平布局

  2. 使用下面的 QSS 就可以得到我们想要的效果了:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    #widget {
    min-height: 40px;
    max-height: 40px;
    border: 4px solid white;
    border-top-left-radius: 24px;
    border-bottom-left-radius: 24px;
    padding: 0;
    background: #AAA;
    }

    QPushButton {
    min-height: 40px;
    max-height: 40px;
    min-width: 40px;
    max-width: 40px;
    border: 4px solid white;
    border-radius: 24px;
    background: lightblue;
    }

    QPushButton:hover {
    background: orange;
    }

补充说明:

  • 高: widget 和 button 的最大最小高都设置为 40px, 这样就不会随布局的变化改变了, 圆角就不会受影响
  • 宽: button 的最大最小宽都设置为 40px, 这样就可以把按钮设置为圆形, 而不会变成圆角矩形
  • 圆角半径: 为 24px 而不是 20px, 因为圆角的计算需要把边框的宽度也计算在内, 边框的宽度是 4px, 所以圆角半径设置为 24px
  • Padding: 为了 widget 和 button 之间不留空隙, 设置 widget 的 padding 为 0
  • 边框和背景色就无需多说了, 根据自己的爱好设置即可

Gradle 管理 Scala 项目

可以使用 Gradle 来管理 Scala 项目, 和管理 Java 的项目是一样的.

目录结构

1
2
3
4
5
6
7
8
9
10
11
12
scala
├── build.gradle
└── src
├── main
│   ├── java
│   ├── resources
│   └── scala
│   └── AppDemo.scala
└── test
├── java
├── resources
└── scala

初始文件

build.gradle 的内容为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
plugins {
id 'java'
id 'scala'
id 'application'
id 'com.github.johnrengelman.shadow' version '4.0.3'
}

// [1] Scala 需要的依赖
dependencies {
compile "org.scala-lang:scala-library:2.12.7"
compile "org.scala-lang:scala-compiler:2.12.7"
compile "org.scala-lang:scala-reflect:2.12.7"
}

// [2.1] 从命令行运行默认类: gradle run
// [2.2] 从命令行运行某个类: gradle run -DmainClass=Foo
ext {
project.mainClassName = System.getProperty("mainClass", "AppDemo")
}

// [3] 打包: gradle clean shadowJar [-DmainClass=Foo]
shadowJar {
mergeServiceFiles('META-INF/spring.*')
}

插件 shadowJar 用于项目打包, 更多信息请参考 https://qtdebug.com/misc-gradle-app/

AppDemo.scala 的内容为:

1
2
3
4
5
object AppDemo {
def main(args: Array[String]): Unit = {
println("Hello World")
}
}

运行打包

  • 运行: gradle run

    运行对象 AppDemo 的 main 函数, 因为 project.mainClassName 中定义了默认运行的类名为 AppDemo

  • 打包: gradle clean shadowJar

Scala 语法摘要

Scala: Scalable language, 面向对象的函数式编程语言.

类型

类型的首字母大写, 没有基本类型:

  • Boolean
  • Byte
  • Char
  • Short
  • Int
  • Long
  • Float
  • Double
  • String
  • Unit (即是 void)
  • Any
  • AnyRef
  • AnyValue

变量

常量: val greeting: String = "Hello world" (value), val π = 3.1415926

变量: var greeting: String = "Hello world" (variable)

名字: 字母, 数字, 特殊操作符如 +, -, *, /, π, θ 等, 不能以数字开头

MongoDB 账号密码登录

配置MongoDB 账号密码登录的步骤如下 (假设有 2 个数据库 admin (自带的) 和 foo):

  1. 启动 MongoDB: mongod --config /usr/local/etc/mongod.conf

  2. 进入数据库 admin: use admin

  3. 创建用户 admin:

    1
    2
    3
    4
    5
    6
    7
    db.createUser(
    {
    user: "admin",
    pwd: "ebag",
    roles: [ { role: "userAdminAnyDatabase", db: "admin" }, "readWriteAnyDatabase" ]
    }
    )
  4. 进入数据库 foo: use foo

  5. 创建用户 bar:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    db.createUser(
    {
    user: "bar",
    pwd: "bar",
    roles: [
    { role: "dbAdmin", db: "foo" },
    { role: "readWrite", db: "foo" }
    ]
    }
    )
  6. 需要授权的方式启动: mongod --auth --config /usr/local/etc/mongod.conf

  7. 授权登录

    • 方式一: mongo 先进入然后 db.auth("bar", "bar") 授权
    • 方式二: mongo --port 27017 -u "bar" -p "bar" --authenticationDatabase "foo"

Vue-Cli 3 创建多页项目

Vue-Cli 3 创建单页项目 一文中介绍了创建单页项目, 现实中复杂一点的项目使用多页项目更合适一些, Vue-Cli 3 创建多页项目很容易, 创建的项目结构如下:

下面就简要的介绍下使用 Vue-Cli 3 创建多页项目的步骤:

  1. Vue-Cli 3 创建单页项目

  2. 在 src 目录下创建目录 pages/page1, pages/page2 (每个目录表示一个单页)

  3. 把 views 目录, App.vue, main.js, router.js, store.js 等都分别复制一份到 page1 和 page2 目录 (参考上面的项目结构图)

  4. 修改 vue.config.js 中的 pages, 配置每个单页的入口 (page1, page2 就是单页访问地址):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    module.exports = {
    devServer: {
    port: 8888,
    },
    pages: {
    page1: 'src/pages/page1/main.js',
    page2: 'src/pages/page2/main.js',
    },
    };
  5. 启动项目: yarn serve

  6. 访问页面:

  7. 打包项目: yarn build

  8. 自定义单页的入口模板

    有眼神好的同学可能看到了 public/index.htmlpublic/page2.html, 但是发现没有 public/page1.html, 这是因为 Vue-Cli 在 public 目录下发现页面对应的 ${subpage}.html 文件 (pages 下配置的 page1, page2), 就使用它生成这个页面的入口 html 文件, 如果没有则会使用 public/index.html 作为模板生成这个页面的入口 html 文件.

    由于不同页面引入的 js, css 或者第三方库等都可能不一样, 所以为页面定制自己的入口模板文件也是有必要的.

vue.config.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
process.env.VUE_APP_VERSION = new Date().getTime();

module.exports = {
devServer: {
port: 8888,
proxy: 'http://localhost:8080'
},

// 多页的页面
pages: {
sample: 'src/pages/sample/main.js',
teacher: 'src/pages/teacher/main.js',
'admin-school': 'src/pages/admin-school/main.js',
'admin-system': 'src/pages/admin-system/main.js',
},

// yarn build 的输出目录
outputDir: '../ebag-web-app/src/main/webapp/WEB-INF/page-vue',
assetsDir: 'static',

css: {
loaderOptions: {
sass: {
// SCSS 全局变量
data: `
@import "@/../public/static/css/variables.scss";
`
}
}
}
};

页面的 HTML 中引入自定义 JS 和 CSS 文件时最好也加上打包时的时间戳 process.env.VUE_APP_VERSION, 避免缓存引起的问题:

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
<!DOCTYPE html>
<html>

<% hash = VUE_APP_VERSION %>

<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>老师</title>

<link rel="stylesheet" type="text/css" href="/static/css/global.css?hash=<%= hash %>">
</head>

<body>

<div id="app"></div>
<!-- 库文件 -->
<script src="/static/lib/jquery.min.js"></script>
<script src="/static/lib/jquery.rest.js"></script>

<!-- 项目的 JS: 增加了 hash 使得更新后不用刷新缓存就能使用最新的 JS -->
<script src="/static/lib/tio/Message.js?hash=<%= hash %>"></script>
<script src="/static/js/urls.js?hash=<%= hash %>"></script>

<!-- built files will be auto injected -->
</body>
</html>

ZooKeeper 实现分布式锁

在 Java 中使用 ZooKeeper 实现分布式锁可按以下几步进行:

  1. 下载安装 ZooKeeper 3.4.13,参考 本机安装 ZooKeeper 集群 进行安装单机版 ZooKeeper,有必要的时候再安装集群
  2. 引入 Curator 的依赖,它实现了 ZooKeeper 的分布式锁
  3. Java 测试程序

下载安装

参考 本机安装 ZooKeeper 集群

引入 Curator 的依赖

1
compile group: 'org.apache.curator', name: 'curator-recipes', version: '2.12.0'

注意: Curator 和 ZooKeeper 的版本需要对应,否则会报错

Curator 2.x.x: compatible with both ZooKeeper 3.4.x and ZooKeeper 3.5.x

Curator 3.x.x: compatible only with ZooKeeper 3.5.x and includes support for new features such as dynamic reconfiguration, etc.

Java 测试程序

官方文档: http://curator.apache.org/getting-started.html,先演示 Curator 连接 ZooKeeper 并使用分布式锁 InterProcessMutex:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public static void testDistributedLock() throws Exception {
// [1] This will create a connection to a ZooKeeper cluster using default values.
// The only thing that you need to specify is the retry policy. For most cases, you should use:
CuratorFramework client = CuratorFrameworkFactory.newClient("127.0.0.1:2181", new ExponentialBackoffRetry(10000, 3));

// [2] The client must be started (and closed when no longer needed).
client.start();

InterProcessMutex lock = new InterProcessMutex(client, "/ebag/lock");

// [3] 获取全局锁
if (lock.acquire(10, TimeUnit.SECONDS)) {
try {
// [4] 业务代码
System.out.println("Do something");
} finally {
// [5] 释放全局锁
lock.release();
}
}

// [6] Close the client
client.close();
}

多线程测试分布式锁,结果 sn 是按顺序输出的,说明锁生效了:

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
public static void testZooKeeperThread() {
CuratorFramework client = CuratorFrameworkFactory.newClient("127.0.0.1:2181", new ExponentialBackoffRetry(10000, 3));
client.start();
List<Thread> threads = new LinkedList<>();

for (int i = 0; i < 100; ++i) {
threads.add(new Thread(new ZooKeeperRunnable(client, "/ebag/lock")));
}

for (Thread thread : threads) {
thread.start();
}

// client.close();
}

class ZooKeeperRunnable implements Runnable {
public static int sn = 0;
private InterProcessMutex lock;

public ZooKeeperRunnable(CuratorFramework client, String path) {
lock = new InterProcessMutex(client, path);
}

public void run() {
try {
// 获取全局锁
if (lock.acquire(10, TimeUnit.SECONDS)) {
try {
// Thread.sleep(100);
sn++;
System.out.println(sn);
} finally {
// 释放全局锁
lock.release();
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}

提示: 上面的多线程是同一个 JVM 中的,可以很简单的修改为在多个 JVM 中运行,演示真正的分布式锁的效果,共享资源 sn 可以保存到 Redis,数据库等中。

ZooKeeper 常用命令

  • 创建: create path data: create /foo "Hello"
  • 修改: set path data: set /foo "Vox"
  • 获取: get path: get /foo
  • 删除: delete path: delete /foo
  • 查看: ls path: ls /foo

CentOS7 安装 LibreOffice

使用 CentOS7 无界面版本作为服务器的操作系统,在里面安装 LibreOffice 用于转换各种文档到 PDF,LibreOffice 的安装步骤如下:

  1. 下载:

    1. 访问 https://www.libreoffice.org/download/download/
    2. 选择 Linux x86_64(rpm) 的版本
    3. 下载得到 LibreOffice_6.0.5_Linux_x86-64_rpm.tar.gz (目前最新版为 6.0.5)
  2. 安装:

    1. 删除: 在安装之前,先删除已经安装的 LibreOffice: yum remove libreoffice*
    2. 解压: tar -xvf LibreOffice_6.0.5_Linux_x86-64_rpm.tar.gz
    3. 安装:
      1. cd LibreOffice_6.0.5.2_Linux_x86-64_rpm/RPMS
      2. yum localinstall *.rpm
    4. 查看:
      1. which libreoffice6.0 看到路径为 /usr/bin/libreoffice6.0
      2. ll /usr/bin/libreoffice6.0 得到 /opt/libreoffice6.0/program/soffice,说明安装到了 /opt/libreoffice6.0
  3. 依赖:

    执行 libreoffice6.0 可能会提示库文件找不到,如 libcairo.so.2,libcups.so.2,libSM.so.6 等,执行下面几条命令安装需要的库:

    • yum install cairo -y
    • yum install cups-libs -y
    • yum install libSM -y

安装 LibreOffice 可参考 https://www.tecmint.com/install-libreoffice-on-rhel-centos-fedora-debian-ubuntu-linux-mint/

Java 中可使用 JODConverter 调用 LibreOffice 进行文件格式转换,可参考 Office 文档转为 PDF 和 HTML

QLineEdit 中增加按钮

下图为 Safari 的地址栏,在输入框右边有一个刷新按钮:

在输入框中增加按钮的设计是比较常见的,例如 Chrome 的地址栏、Firefox 的搜索框、Tim 的搜索框等,这种控件 Qt 没有提供,那应该怎么实现呢?下面提供 2 种思路:

  • QLineEdit + QPushButton 使用 QHBoxLayout 布局到一个 QWidget 中,去掉 QLineEdit 得到焦点时的高亮效果,此时应该高亮它的父控件,失去焦点时取消它的父控件的高亮效果
  • QLineEdit 作为一个普通的 QWidget,也就是它能够使用 QLayout 把 QPushButton 作为子控件布局到它里面

思路有了,第一种方式需要写很多代码进行控制,实现比较麻烦,下面只介绍第二种思路的实现。

模型视图编程

模型视图控制器 (MVC) 编程哪家讲的好,莫非是 Qt 帮助文档里的 Model/View Programming 了,可惜没有中文的,看过不少书里这方面的内容都是从这篇文档里抄的,我们就不要再花心思去创新了,直接翻译吧。

模型视图编程简介

Qt contains a set of item view classes that use a model/view architecture to manage the relationship between data and the way it is presented to the user. The separation of functionality introduced by this architecture gives developers greater flexibility to customize the presentation of items, and provides a standard model interface to allow a wide range of data sources to be used with existing item views. In this document, we give a brief introduction to the model/view paradigm, outline the concepts involved, and describe the architecture of the item view system. Each of the components in the architecture is explained, and examples are given that show how to use the classes provided.

Qt 中有几个 item view 的类 (视图),使用模型/视图架构来管理和显示数据。这种分离的设计给开发者很高的灵活性,可以自定义数据的显示方式,视图通过模型提供的标准接口可以支持各种各样的数据源。本文涉及到模型/视图编程范例、简要的概念介绍以及描述视图系统的架构。每一个部分都会进行解释以及给出相关代码展示怎么使用。

模型视图架构

Model-View-Controller (MVC) is a design pattern originating from Smalltalk that is often used when building user interfaces. In Design Patterns, Gamma et al. write:

MVC consists of three kinds of objects. The Model is the application object, the View is its screen presentation, and the Controller defines the way the user interface reacts to user input. Before MVC, user interface designs tended to lump these objects together. MVC decouples them to increase flexibility and reuse.

Smalltalk 经常用来构建用户界面,模型-视图-控制器 (MVC) 这种设计模式就是从 Smalltalk 借鉴而来的。Gamma et al. 在设计模式中写到:

MVC 由 3 种对象组成:Model 是应用的数据,View 显示数据,Controller 定义了用户界面对用户输入的响应。在使用 MVC 之前,用户界面的设计常常把这些对象耦合在一起。MVC 能够解耦它们从而提供更高的灵活性和重用性。

The model/view architecture
The model communicates with a source of data, providing an interface for the other components in the architecture. The nature of the communication depends on the type of data source, and the way the model is implemented.

The view obtains model indexes from the model; these are references to items of data. By supplying model indexes to the model, the view can retrieve items of data from the data source.
In standard views, a delegate renders the items of data. When an item is edited, the delegate communicates with the model directly using model indexes.

在模型/视图架构中,模型访问数据源中的数据,视图等控件通过模型提供的接口访问数据。通信的方式取决于数据源的类型和模型实现的方式。
数据由数据项组成,index 是数据项的引用,视图从模型中获得 index。视图通过访问模型中的 index 就能够访问数据源中的数据了。
在标准的视图中,代理用来渲染数据项。当数据项被编辑时,代理使用模型的 index 和模型交互。

Generally, the model/view classes can be separated into the three groups described above: models, views, and delegates. Each of these components is defined by abstract classes that provide common interfaces and, in some cases, default implementations of features. Abstract classes are meant to be subclassed in order to provide the full set of functionality expected by other components; this also allows specialized components to be written.

Models, views, and delegates communicate with each other using signals and slots:

  • Signals from the model inform the view about changes to the data held by the data source.
  • Signals from the view provide information about the user’s interaction with the items being displayed.
  • Signals from the delegate are used during editing to tell the model and view about the state of the editor.

根据上面的介绍,模型/视图的类可以分为三类:模型、视图和代理,它们都有相应的抽象类提供通用的接口和某些功能的默认实现。抽象类就意味着要被其他类继承,根据需求提供对应的实现。

模型、视图和代理之间使用信号槽进行通信:

  • 数据源中的数据发生变化时,模型发射信号通知视图
  • 用户和视图交互时视图会发射信号,例如点击 view item,信号的参数包含了被交互的 view item 的信息
  • 编辑数据项的时候代理会把编辑器的状态通过信号通知模型和视图

模型

All item models are based on the QAbstractItemModel class. This class defines an interface that is used by views and delegates to access data. The data itself does not have to be stored in the model; it can be held in a data structure or repository provided by a separate class, a file, a database, or some other application component.

The basic concepts surrounding models are presented in the section on Model Classes.

QAbstractItemModel provides an interface to data that is flexible enough to handle views that represent data in the form of tables, lists, and trees. However, when implementing new models for list and table-like data structures, the QAbstractListModel and QAbstractTableModel classes are better starting points because they provide appropriate default implementations of common functions. Each of these classes can be subclassed to provide models that support specialized kinds of lists and tables.

The process of subclassing models is discussed in the section on Creating New Models.

所有的 item 模型都是基于类 QAbstractItemModel 实现的。视图和代理使用 QAbstractItemModel 定义的接口访问数据。数据不一定是保存在模型中,也可以保存在其他类、文件、数据库或者其他应用中。

模型相关的概念在模型的类一节中进行介绍。

模型类 QAbstractItemModel 定义的访问数据接口 (函数) 是很灵活的,能够满足表格、列表和树用来显示模型的数据。然而,当给列表和表格自定义新的模型时,继承 QAbstractListModel 或者 QAbstractTableModel 是个很好的选择,因为他们提供了很多通用操作的默认实现,就不需要我们再重复实现。

实现自定义模型在创建新的模型类一节中进行介绍。

Qt provides some ready-made models that can be used to handle items of data:

  • QStringListModel is used to store a simple list of QString items.
  • QStandardItemModel manages more complex tree structures of items, each of which can contain arbitrary data.
  • QFileSystemModel provides information about files and directories in the local filing system.
  • QSqlQueryModel, QSqlTableModel, and QSqlRelationalTableModel are used to access databases using model/view conventions.

If these standard models do not meet your requirements, you can subclass QAbstractItemModel, QAbstractListModel, or QAbstractTableModel to create your own custom models.

Qt 已经提供了一些可用于处理数据项的模型:

  • QStringListModel 用于存储简单的列表数据,数据项为 QString
  • QStandardItemModel 用于管理有树结构关系的数据项,每个数据项可以包含任意的数据
  • QFileSystemModel 访问本地文件系统的文件和文件夹
  • QSqlQueryModel, QSqlTableModel 和 QSqlRelationalTableModel 使用模型视图的方式访问数据库

如果这些标准的模型还不能满足我们的需求,可以继承 QAbstractItemModel, QAbstractListModel, or QAbstractTableModel 实现自定义的模型类。

分组布局

在进行界面布局的时候,常把控件根据功能分组放在一起,最常用的就是使用 QGroupBox 来放置一组控件。QGroupBox 虽然使用起来很方便,但就是有点丑,在要求较高的设计中,还得使用控件组合加自定义绘图或者 QSS 等才能实现,例如下面这个软件界面,直接使用 Qt 提供的控件是满足不了的:

上图中分组的布局没有使用 QGroupBox,而是用几个控件组合起来实现的,设计如下:

异形按钮组

不少软件里看到过如下的按钮组,有 5 个按钮,中间 1 个,上下左右各一个:

可以通过绘图的方式实现:计算每一个按钮的位置、大小、图片、点击的时候判断点击到了哪个按钮然后刷新绘制它的样式,并调用相应的函数执行点击操作,难度还是相当大的。

操作图像像素,实现各种效果

Qt 中图像相关的类主要是 QPixmap 和 QImage,QPixmap 没有提供访问图像像素数据的接口,访问图像的像素数据需要使用 QImage,主要的函数有 (相关重载函数没有列出来):

1
2
3
4
5
6
7
8
// 获取图像的像素数据
QRgb pixel(int x, int y) const
QColor pixelColor(int x, int y) const
uchar* scanLine(int i)

// 设置图像的像素数据
void setPixel(int x, int y, uint index_or_rgb)
void setPixelColor(int x, int y, const QColor &color)

下面把一个图像转为灰度图为例介绍怎么操作图像的像素:

  1. 取得图像的宽、高
  2. 根据宽、高遍历每一个像素
  3. 得到每一个像素的 RGBA 颜色分量
  4. 对得到的颜色分量 RGBA 进行灰度计算得到新的颜色
  5. 使用计算得到的颜色设置对应像素

自定义按钮组

如下的按钮组相信大家都看到过,最左和最右的按钮是圆角的,中间的按钮是矩形的,同时只能有一个按钮是选中状态:

按钮的样式使用 QSS 实现,使用 setProperty 设置按钮的 class 属性为 GroupButton,就可以利用类选择器 .GroupButton 选择按钮组的按钮,避免它们的样式影响到普通按钮。为了单独设置最左和最右按钮的样式,使用 setProperty 为其设置一个属性 position,最左按钮的为 first,最右按钮的为 last,然后就能使用属性选择器 .GroupButton[position="first"].GroupButton[position="last"] 选择它们了。

SpringMVC 使用 @PathVariable 获取有 . 的 URL 中的变量

1
2
3
4
5
@GetMapping("/api/file/{filename}")
@ResponseBody
public String foo(@PathVariable String filename) {
return filename;
}

默认配置时,不同的 URL ,获取到的 filename 为:

  • /api/file/foo,filename 为 foo
  • /api/file/foo.pdf,filename 为 foo
  • /api/file/foo.pdf.png,filename 为 foo.pdf
  • /api/file/foo.pdf.png.doc,filename 为 foo.pdf.png

最后一个 . 被截断了,解决这个问题有 2 中方法:

  • 使用正则表达式进行路径匹配,映射为:@GetMapping("/api/file/{filename:.+}")

    • 缺点:每个路径的映射都要写一遍,不方便
    • 优点:缺点也是优点,只影响需要的路径
  • 配置 annotation-driven,映射为:@GetMapping("/api/file/{filename}")

    • 优点:只需要配置一次,整个应用都生效,方便
    • 缺点:优点也是缺点,影响了整个系统,不过还没有发现对整个系统有什么副作用
    1
    2
    3
    4
    <mvc:annotation-driven>
    <mvc:path-matching registered-suffixes-only="true"/>
    ...
    </mvc:annotation-driven>

QTreeView 小集

树形控件是非常常用的,例如组织结构、目录树、省市县的地区结构等都是典型的树形结构,Qt 里可以使用 QTreeView 和 QTreeWidget 来展示树形结构,这里我们只介绍 QTreeView 的使用,QTreeView 本身只用于树的显示,树的数据由 QStandardItemModel 来存储。

创建单列树

创建单列树的节点分两种情况:

  • 创建第一级节点调用函数 QStandardItemModel::appendRow(QStandardItem *item)
  • 创建第二级、第三级等非第一级节点调用函数 QStandardItem::(QStandardItem *item)

下面的例子创建省市县的树形结构展示如何创建只有一列的树,为了更好的从变量名上看出地区的关系,使用数字和层级的方式进行命名,程序运行结果如下:

去掉 png 图片的 iCCP 警告

Qt 中使用 png 图片有时候会给出警告 libpng warning: iCCP: known incorrect sRGB profile:

Libpng-1.6 is more stringent about checking ICC profiles than previous versions. You can ignore the warning. To get rid of it, remove the iCCP chunk from the PNG image.

Some applications treat warnings as errors; if you are using such an application you do have to remove the chunk.

解决办法:

  1. 安装 ImageMagick (Mac: brew install ImageMagick)
  2. 到图片文件夹,执行命令 mogrify *.png 去掉此文件夹下 png 图片的 iCCP 警告

要想找出有 iCCP 问题的 png 图片,可以使用工具 pngcrush:

  1. 安装 pngcrush (Mac: brew install pngcrush)
  2. 到图片文件夹,执行命令 pngcrush -n -q *.png 找出有 iCCP 警告的图片

更多细节请参考 libpng warning: iCCP: known incorrect sRGB profile

一次 HTTP 被运营商劫持的血泪史

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!DOCTYPE html>
<html lang="en" dir="ltr">

<head>
<script src="/js/jquery.js" charset="utf-8"></script>
<script src="/js/paper.js" charset="utf-8"></script>
</head>

<body>
<script>
$(document).ready(function() {
__Exam_PaperInit();
});
</script>
</body>

</html>

很简单的页面,jquery.js 和 paper.js 加载完,然后调用 __Exam_PaperInit() 进行初始化。在办公室、家里都没出现过问题,但是在学校的机房里访问这个页面,时不时的出错,提示如 __Exam_PaperInit() 不存在,这么简单直接的逻辑,咋会出错呢,想不明白,猜测例如是不是机房的环境网络设置有问题等,但花了很久仍然找不到原因,更要命的是,系统过几天就有几千人要用来考试了,问题解决不了的话,可以想象影响会有多大。

iTerm 设置

高亮输出

不同的输出显示为不同的颜色,例如下面的 [DEBUG] 的信息暗一些

Preferences > Profiles > Advanced > Triggers > Edit: 使用正则表达式进行设置,Action 选择为 Highlight Text...

GitBook 使用 Coding.net 的 Pages 访问

GitBook 生成的静态网页文件在 _book 目录中,下面介绍怎么把它发布到 coding.net 的 Pages 服务中,这样就能够通过网络访问了。

  1. https://coding.net 创建一个账号 xtuer(下面请换为自己的账号)
  2. 创建仓库 2 个仓库 fox 和 fox-doc (仓库名字随意取):
    • fox-doc: GitBook 源文件
    • fox: GitBook 生成的静态文件
  3. 克隆这 2 个仓库到本地的同一个文件夹下
    • git clone git@git.coding.net:xtuer/fox.git
    • git clone git@git.coding.net:xtuer/fox-doc.git

右键菜单

右键菜单有多种实现方式:

  • 设置 contextMenuPolicy 为:
    • Qt::ActionsContextMenu
    • Qt::CustomContextMenu
  • 重写 contextMenuEvent 函数

下面就分别介绍这几种右键菜单的实现。

iView 的 Table 中插入按钮

很多时候需要在 Table 的单元格中使用按钮,iView 的官方例子使用函数 createElement (简写 h) 来创建,但是代码很繁杂、不直观、难以实现复杂的 DOM 结构。还好除此之外可以使用 JSX 来实现,能够方便的增加 class、wrapper、图标、任意的 DOM 等。

JSX 实现

1
2
3
4
5
6
7
8
9
10
11
{ title: '操作', key: 'action', width: 160, align: 'center',
// 编辑和删除按钮
render: (h, params) => {
return (
<div class="cell-button-container">
<i-button type="primary" size="small" onClick={()=>{this.editSchool(params.index)}} icon="edit">编辑</i-button>
<i-button type="error" size="small" onClick={()=>{this.deleteSchool(params.index)}} icon="android-delete">删除</i-button>
</div>
);
}
}

提示:

  • 按钮的标签使用 <i-button>,不能使用 <Button>
  • 按钮的事件处理 vue 中为 on-click,但在 JSX 中为 onClick

MyBatis 传递多个参数

MyBatis 传递多个参数一般有以下几种方法:

  • 使用 Map
  • 把参数封装成 Bean,传递 Bean 的对象
  • 使用 @Param
  • 编译时使用 -parameters 参数 (推荐使用)

下面以用户名和密码作为参数查询用户为例进行介绍。

Layout 秘录

布局管理器 QHBoxLayout、QVBoxLayout、QGridLayout 相信大家都很熟悉了,对于常用的功能就不一一列举,这里将介绍一下几个不常用,在复杂的自定义界面时又可能会用到的功能:

  • QGridLayout 中多个 Widget 放在同一个位置
  • 把一个 Widget 替换为另一个 Widget
  • QHBoxLayout、QVBoxLayout 中插入 Widget
  • 从 Layout 中删除 Widget

自定义标题栏无边框阴影窗口

Qt 的默认窗口使用系统风格,不能修改标题栏和边框,满足不了高度自定义的窗口设计,这时只能把窗口的默认标题栏和边框隐藏起来,替换上我们自定义的标题栏和边框,下面就以实现自定义标题栏无边框阴影窗口为例进行介绍。

技术要点:

  • 隐藏系统标题栏和边框: QWidget::setWindowFlags(Qt::FramelessWindowHint)
  • 窗口透明隐藏默认背景: QWidget::setAttribute(Qt::WA_TranslucentBackground)
  • QWidget::paintEvent(QPaintEvent *event) 里绘制任意形状的自定义背景
  • 拖拽移动窗口
  • 缩放窗口

带阴影的圆形 Label

圆形头像大家应该都见过不少软件里用过吧,例如 QQ 的好友列表,网页里的人物头像,有没有想过在 Qt 里怎么做到呢?

这一节中就来介绍怎么实现下图中的圆形 QLabel,然后扩展到给 QLabel 添加阴影效果、模糊效果以及加上边框:

圆形 Label

最核心的就是圆形 QLabel 的实现,有很多种方法能够做到,这里使用 QSS 来实现: Border Image + Border Radius,也就是几行代码的事:

  • 圆形: 先设置 QLabel 的大小为固定大小,这样当窗口大小变化时不会影响 QLabel 的大小,并且设置 border-radius 为 QLabel 高度的一半
    • 必须正好是一半出来的效果才能是正圆
    • 大于一半 border-radius 就失去了效果,出来的是矩形,这应该是 QSS 的 Bug,CSS 里就不这样
    • 小于一半的效果是圆角矩形
  • 背景: 为了让背景图缩放填满 QLabel,需要使用 border-image 并且设置 QLabel 边框的宽度为 0
1
2
3
4
5
6
7
8
9
10
QQLabel {
min-width: 100px;
max-width: 100px;
min-height: 100px;
max-height: 100px;

border-radius: 50px;
border-width: 0 0 0 0;
border-image: url(/Users/Biao/Desktop/estas.jpg) 0 0 0 0 stretch strectch;
}

上面的 QSS 就能得到左边第一个圆形 QLabel 的效果。

MongoDB 初接触

MongoDB 的结构是:数据库 > 集合 (collection) > 文档 (document) > 属性 (field)

MySQL 的结构是: 数据库 > 表 (table) > 记录 (record or row) > 属性 (field or column)

下载安装

不同的系统安装 MongoDB 差别挺大的:

  • Mac: 使用 brew install mongodb

  • Linux: 参考 http://qtdebug.com/mac-centos7/#安装-MongoDB

  • Windows: 下载然后安装,安装的时候不要选择安装 MongoDB Compass,因为需要联网下载,国内有可能很久都装不好, 或者下载压缩版解压直接用:

    在 bin 目录中创建文件 mongod.conf, 内容如下:

    1
    2
    3
    4
    5
    6
    7
    8
    systemLog:
    destination: file
    path: D:\MongoDB\logs\mongodb.log #日志输出文件路径
    logAppend: true
    storage:
    dbPath: D:\MongoDB\data #数据库路径
    net:
    bindIp: 0.0.0.0 #允许其他电脑访问

    然后创建 path 和 dbPath 指向的文件夹 (文件夹不存在则会导致启动失败)

启动访问

  • 启动 MongoDB:
    • mongod
    • mongod --config C:/etc/mongod.conf
    • mongod --auth --config C:/etc/mongod.conf
  • 访问 MongoDB:
    • mongo
    • mongo --host IP
    • 使用 IDEA 的插件 Mongo Plugin
    • 漂亮的免费客户端 dbKoda
    • 智能的免费客户端 NoSQLBooster for MongoDB (推荐使用)
  • 删除 MongoDB:
    • CentOS
      • 关闭 MongoDB 服务: kill -9 pid
      • 查看有 MongoDB 哪些包: rpm -qa | less | grep mongo
      • 删除 MongoDB 的包: yum erase $(rpm -qa | grep mongodb-org)
      • 删除 MongoDB 的目录: rm -rf /var/log/mongodb ; rm -rf /var/lib/mongo

使用百度 OCR 服务识别图片中的文本

访问 https://cloud.baidu.com/product/ocr/general 可以先体验一下百度的 OCR 文字识别,在功能演示处上传一个含有文字的图片就可以看到识别效果,还是挺不错的,接下来就介绍使用 OCR 服务的编程实现:

  1. 点击立即使用

  2. 点击创建应用 (需要登陆)

  3. 得到应用 API KeySecret Key (在程序中需要使用,对应程序中的 APP_IDAPP_KEY)

  4. 使用 API KeySecret Key 换取 access_token,请参考鉴权认证机制

  5. 使用 OCR 服务识别图片中的文字,请参考通用文字识别

    • 把图片进行 Base64 编码成为字符串

      文档中说所有图片均需要 Base64 编码后再进行 urlencode,这里容易造成困扰,其实 Base64 后就够了,因为 Base64 包含的 64 个字符为 a-z, A-Z, 0-9, /, + 以及填充字符 = 都包含在了 urlencode 不需要进行编码的字符内。

    • 去掉图片头,如 data:image/jpg;base64,

    • 传给百度,然后就能得到识别的 JSON 结果