去掉 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 差别挺大的:

启动访问

  • 启动 MongoDB:

    • mongod

    • mongod --config C:/etc/mongod.conf

  • 访问 MongoDB:

    • mongo
    • mongo --host IP
    • 使用 IDEA 的插件 Mongo Plugin
    • 漂亮的免费客户端 dbKoda
    • 智能的免费客户端 NoSQLBooster for MongoDB (推荐使用)

使用百度 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 结果

Qt 项目中使用 OpenCV

相信很多人在 Qt 项目中使用 OpenCV 都遇到过麻烦,Windows 开发者软件推荐 一文中介绍过使用 Vcpkg 来管理第三方库,这里就使用 Vcpkg 安装 OpenCV 然后在 MSVC 的 Qt 项目中使用(因为 Vcpkg 使用的是 MSVC 编译器,OpenCV 是 C++ 库,不能够跨编译器,所以 MinGW 的项目不能使用):

  1. 安装 Vcpkg 就不用多说了,安装到 C 盘根目录下吧

  2. 安装 OpenCV: vcpkg install opencv

  3. Qt Creator 中 创建 Qt 项目

  4. 修改项目的 .pro 文件,主要是下面 2 句引入 OpenCV

    1
    2
    INCLUDEPATH += C:/vcpkg/installed/x86-windows/include
    LIBS += C:/vcpkg/installed/x86-windows/lib/opencv_*.lib

    使用 opencv_*.lib 引入所有 opencv_ 开头的 lib 文件,这样就不需要一个一个的引入 lib 了。

    引入 dll 的时候也可以使用 * 来匹配一次引入多个,例如 tiff*.dll

  5. main.cpp

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    #include <opencv2/opencv.hpp>
    using namespace cv;
    int main() {
    Mat img = imread("D:/Wallpaper/desktop.jpg");
    imshow("TEST", img);
    waitKey(6000);
    return 0;
    }
  6. 把 OpenCV 相关的 DLL 从 C:/vcpkg/installed/x86-windows/bin 目录复制到编译出的 exe 所在目录

  7. 运行程序,然后就看到打开一个窗口,图片显示在窗口中

Windows 开发者软件推荐

包管理器 Chocolatey

Chocolatey 是一款专为 Windows 系统开发的、基于 NuGet 的包管理器工具,类似于

  • Node 的 npm
  • MacOS 的 brew
  • Ubuntu 的 apt-get
  • CentOS 的 yml

Chocolatey 的设计目标是成为一个去中心化的框架,便于开发者按需快速安装应用程序和工具,官网为 https://chocolatey.org,安装很简单,根据说明安装即可。

常用命令

  • 搜索: choco search something

  • 列出: choco list -lo

  • 安装: choco install cmake

    可访问 https://chocolatey.org/packages 查看已有的包和说明

  • 卸载: choco uninstall cmake

  • 升级: choco upgrade cmake

  • 固定包的版本,防止包被升级: choco pin windirstat

MyBatis Collecton

MyBatis 中一对多的关系使用 collection 进行映射,但是怎么确定哪些行是同一个对象的数据呢?关键是使用 <id> 来归类数据(MyBatis 的文档只提到是为了提高效率),下面介绍使用 <id><result> 的区别。

数据表

name email country province street
Biao biao@gmail.com china 北京 天河街
Biao biao@icloud.com Deutschland Braunschweig Wiesenstrasse
Alice alice@gmail.com china 河北 鼓楼大街

Xml Mapper

查找所有用户

1
SELECT name, email, country, province, street FROM user

映射文件如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<mapper namespace="com.xtuer.mapper.UserMapper">
<select id="users" resultMap="userResultMap">
SELECT name, email, country, province, street FROM user
</select>
<resultMap id="userResultMap" type="User">
<id property="name" column="name"/> <!-- 关注这里 -->
<result property="email" column="email"/>
<collection property="addresses" resultMap="addressResultMap"/>
</resultMap>
<resultMap id="addressResultMap" type="Address">
<result property="country" column="country"/>
<result property="province" column="province"/>
<result property="street" column="street"/>
</resultMap>
</mapper>

Java 按照拼音排序

Java 中按照拼音序对字符串排序,只需要使用 CHINA 的 Collator 即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import java.text.Collator;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
public class Test {
public static void main(String[] args) {
List<String> tokens = new LinkedList<>();
tokens.add("黄"); // h
tokens.add("慌"); // h
tokens.add("晃"); // h
tokens.add("欢"); // h
tokens.add("中"); // z
tokens.add("这"); // z
tokens.add("国"); // g
tokens.add("古"); // g
tokens.add("安"); // a
Collator collator = Collator.getInstance(Locale.CHINA);
tokens.sort((a, b) -> collator.compare(a, b));
System.out.println(tokens);
}
}

MySQL 数据类型

MySQL 中定义数据字段的类型对你数据库的优化是非常重要的,支持多种类型,大致可以分为三类:数值、日期/时间和字符串(字符)类型。

数值类型

MySQL支持所有标准 SQL 数值数据类型,这些类型包括严格数值数据类型(INTEGER、SMALLINT、DECIMAL 和 NUMERIC),以及近似数值数据类型(FLOAT、REAL 和 DOUBLE)。

关键字 INT 是 INTEGER 的同义词,关键字 DEC 是 DECIMAL 的同义词。
BIT 数据类型保存位字段值,并且支持 MyISAM、MEMORY、InnoDB 和 BDB 表。
作为 SQL 标准的扩展,MySQL 也支持整数类型 TINYINT、MEDIUMINT 和 BIGINT。

数据库常用基础

列的操作

  • 增加列: 基本形式 ALTER TABLE 表名 ADD 列名 列数据类型 [约束][AFTER 插入位置]

    1
    ALTER TABLE demo ADD count INT DEFAULT 0 NOT NULL
  • 修改列: 基本形式 ALTER TABLE 表名 CHANGE 列名称 列新名称 新数据类型,数据类型一样时不会丢失数据,也就是重命名了

    1
    ALTER TABLE demo CHANGE count size INT
  • 删除列: 基本形式 ALTER TABLE 表名 DROP 列名称

    1
    ALTER TABLE demo DROP count

查表更新

使用一个表的数据更新另一个表可使用关联查询进行更新

1
2
3
4
UPDATE demo d
JOIN (SELECT course_id AS id, weight FROM course) AS c
ON d.id=c.id
SET d.count=c.weight

Windows 设置 JDK 的默认编码

现在 Java 开发项目编码一般都是 UTF-8,Windows 下 JDK 的默认编码是 GB2312,可把其默认值设置为 UTF-8:

  1. 右键点击计算机 > 属性 > 高级系统设置 > 高级 > 环境变量 > 系统变量: 设置 JAVA_TOOL_OPTIONS 的值为 -Dfile.encoding=UTF-8
  2. 命令行显示 UTF-8 字符: 执行 chcp 65001,设置命令行的属性,选择字体 Lucida Console(不要选择点阵字体)。如果要换回 GBK 执行 chcp 936 ,再把字体改成点阵字体即可。

SQL Server 导出 CSV 和 XML

导出 XML

1
bcp "select * from tableName FOR XML AUTO, ROOT('Root')" queryout C:/x.xml -S(local) -T -r -c
  • -w: 使用 UTF-16 编码,小端
  • -c: 使用 GBK 编码
  • -c -C6501: 使用 UTF-8 编码,但是有的计算机上不支持
  • -T: 本机使用 -T 表示可信连接,如果是访问其他机器使用 -U user -P pwd 输入用户名和密码

注意:

  • 如果不使用 -r,则导出的 XML 每行最多有 2033 个字符,会破坏 XML,用了 -r 后就没有换行符了,整个 XML 的内容在同一行。
  • 内容中的 & 等特殊字符不会被转义就直接放到属性值里了,此时用 XML 库解析会出错。

导出 CSV

1
sqlcmd -S localhost -d dbName -E -o "csvFile.csv" -Q "set nocount on; select * from tableName" -W -w 999 -s ","
  • -W: remove trailing spaces from each individual field
  • -s",": sets the column seperator to the comma (,)
  • -w 999: sets the row width to 999 chars(this will need to be as wide as the longest row or it will wrap to the next line)
  • -U: username
  • -P: password
  • -h-1: removes column name headers from the result
  • set nocount on: 输出时不显示 XXX 行受到影响的统计信息

注意:sqlcmd 导出为 CSV 文件时,如果列中有逗号,那么导出的 CSV 文件会被破坏,还没找到好办法。

参考 How to export data as CSV format from SQL Server using sqlcmd?

iTerm ssh 自动登录

使用 SSH 远程登录时:

  1. 输入 ssh root@host-ip
  2. 输入密码

每次都重复这样的操作,不仅麻烦,还要记忆好多东西,为了解决这个问题,借助 iTerm2 Profile 可以实现 SSH 自动登录:

  1. 编写 expect 脚本
  2. 使用此脚本创建 Profile
  3. 使用此 Profile 打开新标签页

MySQL 导入导出 SQL 文件

创建数据库

1
CREATE DATABASE IF NOT EXISTS databaseName DEFAULT CHARSET utf8 COLLATE utf8_general_ci;

导入 SQL 文件

MySQL 可以使用 GUI 客户端导入 SQL 文件,此外在命令行下有常用下面 2 种方式导入 SQL 文件(先要创建好数据库)

  • 使用 mysql 导入
    1. ./mysql -u root -p 数据库名 < 导入的文件名.sql
    2. 输入密码
  • 使用 source 导入
    1. ./mysql -u root -p
    2. 输入密码
    3. use databaseName
    4. source 导入的文件名.sql

导出 SQL 文件

导出有 2 种: 导出表结构,导出数据库(包含表结构和表中的数据)

  • 导出表结构: mysqldump -u username -p 数据库名 > 导出的文件名.sql
  • 导出数据库: mysqldump -u username -p -d 数据库名 > 导出的文件名.sql

自定义 Widget 使用 QSS

相信很多同学继承如 QWidget,QPushButton 等实现自定义的控件后,发现在此控件上 QSS 不生效了,这不行啊,设置背景、边框、字体等如果没有 QSS 那就太麻烦了。其实在自定义控件上启用 QSS 非常简单,只要调用一下 setAttribute(Qt::WA_StyledBackground) 即可,就像下面这样

1
2
3
4
Widget::Widget(QWidget *parent) : QWidget(parent) {
this->setAttribute(Qt::WA_StyledBackground); // 启用 QSS
this->setStyleSheet("border: 2px solid red; background: pink; border-radius: 10px;"); // 设置 QSS
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <QApplication>
#include "Widget.h"
int main(int argc, char *argv[]) {
QApplication app(argc, argv);
QWidget window;
Widget customWidget(&window);
customWidget.setGeometry(20, 20, 100, 100);
window.resize(300, 300);
window.show();
return app.exec();
}

如上在 main() 函数中使用自定义控件 Widget,QSS 生效了,去掉 this->setAttribute(Qt::WA_StyledBackground) 后 QSS 就没效果了:

按下鼠标拖动窗口

QWidget 已经实现了在标题栏按下鼠标移动窗口的功能,但是当实现无边框窗口时,因为没有了标题栏,移动窗口的功能就需要我们自己实现了,不过也不复杂,主要是处理鼠标的按下、移动、松开三个事件:

  • 按下鼠标:记录此时鼠标的全局坐标和窗口左上角的坐标,并且设置鼠标为按下状态
  • 移动鼠标:鼠标按下时移动鼠标,计算此时鼠标和鼠标按下时的位移差,加上按下鼠标时窗口左上角的坐标得到窗口新的坐标,移动窗口到此坐标
  • 松开鼠标:设置鼠标为未按下状态

拖拽鼠标画矩形

经常看到有同学问:如何实现用鼠标拖拽出一个矩形区域的效果,类似 QQ 截图的那个矩形区域?

很简单的一个问题,主要是处理鼠标的按下、移动、松开三个事件:

  • 按下鼠标:用一个变量标记要开始拖拽出矩形了,并记录按下的位置作为矩形的左上角顶点
  • 移动鼠标:按下时移动鼠标,鼠标的当前位置作为矩形新的右下角的顶点,每次移动事件发生时都要重新画一次矩形,因为矩形变了
  • 松开鼠标:清除拖拽矩形的标志,松开鼠标后,移动鼠标时就不要再改变矩形了

Qt Creator 简介

工欲善其事,必先利其器,顺手的开发工具,能够让我们事半功倍,Qt 开发,推荐使用 Qt Creator,因为在 Qt Creator 中集成了很多实用的功能,不需要切换到其他软件就能使用

  • 编写代码(废话)
  • 使用 UI Designer 进行可视化的布局界面
  • 搜索帮助文档
  • 学习自带的例子
  • Ctrl + K 快速搜索、定位

接下来就简单的介绍下我个人觉得 Qt Creator 使用中比较重要的地方。

线程池 QThreadPool

创建线程需要向系统申请资源,线程切换时操作系统会切换线程上下文,可能会从用户态切换到内核态,当有很多线程时,频繁地切换线程会导致消耗大量的 CPU 以及内核资源,真正用于计算的资源就减少了,反而会降低程序的效率。线程并不是越多越好,线程池的作用是管理、复用、回收一组线程,控制线程的数量,避免频繁的创建和销毁线程而浪费资源。

Qt 中的线程池类为 QThreadPool,每一个 Qt 程序都有一个全局的线程池,调用 QThreadPool::globalInstance() 得到,它默认最多创建 8 个线程,如果想改变最大线程数则调用 setMaxThreadCount() 进行修改,调用 activeThreadCount() 查看线程池中当前活跃的线程数。

使用线程池挺简单的,定一个任务类例如叫 Task,继承 QRunnable 并实现虚函数 run(),Task 的对象作为 QThreadPool::start() 的参数就可以了,线程池会自动的在线程中调用 Task 的 run() 函数,异步执行。线程池中的 QRunnable 对象太多时并不会为立即为每一个 QRunnable 对象创建一个线程,而是让它们排队执行,同时最多有 maxThreadCount() 个线程并行执行。

提交给线程池的 QRunnable 对象在它的 run() 函数执行完后会被自动 delete 掉,如果不想线程池删除它,在调用线程池的 start() 前调用 setAutoDelete(false) 即可。

创建使用动态链接库

想一想大多数时候我们的项目是不是所有代码都会放在同一个工程中?人少的时候都不是事,但当项目越来越大,开发人员越来越多,会发觉开发、管理能让人窒息,大家都绞在一起,出问题时互相推诿责任,各自有理,这时如果按照功能模块进行分组各自开发,以库的形式提供给其他人使用,就能够最大限度的并行开发,提高工作效率,而且项目的模块也很清晰,责任一目了然,此外使用动态链接库后还能够按模块升级,编译的速度也更快。下面就介绍怎么在工程中创建和使用动态链接库。

Windows 中叫动态链接库(Dynamic Link Library: .dll),Linux 中叫共享库(Shared Library: .so),Mac 下后缀为 .dylib,实际指的是一种类型的库,这里都统称为动态链接库吧。

动态链接库需要理解符号的概念,符号包含函数、变量或者类,分为公有符号和私有符号:

  • 公有符号: 在其他程序或者库使用的符号,需要根据用途进行特殊标记:

    • Q_DECL_EXPORT: 编译为动态链接库时符号要标记为 Q_DECL_EXPORT,表明是导出符号
    • Q_DECL_IMPORT: 在调用动态链接库时符号要标记为 Q_DECL_IMPORT,表明是使用符号
  • 私有符号: 除了公有符号外的其他符号,在此库外不应该能够访问,不需要进行标记

    建议: 不要在头文件中声明私有符号。

符号上的标记 Q_DECL_EXPORTQ_DECL_IMPORT 不能同时存在,为了在导出和导入时使用同一个头文件, 头文件中包含下面的宏,编译时根据条件使用不同的宏就可以了:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <QtCore/qglobal.h>
// 根据条件定义 MYLIBRARY_SHARED_SYMBOL 为不同的宏
#if defined(MYLIBRARY_LIBRARY)
# define MYLIBRARY_SHARED_SYMBOL Q_DECL_EXPORT
#else
# define MYLIBRARY_SHARED_SYMBOL Q_DECL_IMPORT
#endif
// 使用 MYLIBRARY_SHARED_SYMBOL 声明符号,编译时会根据条件替换为 Q_DECL_EXPORT 或者 Q_DECL_IMPORT
class MYLIBRARY_SHARED_SYMBOL Calculator {
...
};

为了达到了在导出和导入时使用同一个头文件的目的:

  • 生成动态链接库工程的 pro 文件中添加 DEFINES += MYLIBRARY_LIBRARY,在编译的时候 MYLIBRARY_SHARED_SYMBOL 就会被替换为 Q_DECL_EXPORT
  • 使用动态链接库工程的 pro 文件中千万不要加 DEFINES += MYLIBRARY_LIBRARY,编译的时候 MYLIBRARY_SHARED_SYMBOL 就会被替换为 Q_DECL_IMPORT

测试 Widget 的效率

Widget 的效率怎么样,来进行一个简单的测试,添加 1千,1万,2万个,……,10万个 QPushButton(修改程序中的 buttonsCount 即可),看看程序的创建好按钮,点击按钮执行槽函数,程序退出效果怎么样。

添加 1千个,1万个按钮的时候窗口显示的速度非常快。
添加 2万个的时候就需要几秒窗口才显示出来。
添加的越多窗口显示需要的时间越长,添加 10万个需要等很久。
按钮越多,程序退出的时间就越长,不过即使是 10万个按钮,退出也就是多了几秒,因为释放的内存多,这倒是没什么。
当窗口显示出来后,不管添加了多少个按钮,点击按钮,它的槽函数都是瞬间就被执行。
在实际应用中,添加上百个 widgets 在窗口上的见过,但有谁会添加上万个 widgets 到窗口上?不担心被产品经理揍的可以试试!

测试 Graphics View 的效率

Qt 说 Graphics View Framework 效率很高,到底有多高呢?来进行一个简单的测试,向 scene 中添加 10万,50万,100万个 items(修改程序中的 rowCount 和 colCount 即可),进行缩放、旋转看看效率怎么样。

在可视区域内的 items 少的话,不管 scene 里有多少个 items,10万个和 100万个的区别不大,效率都是非常高的,但可视区内 items 越多的话,越多效率越低。Qt 使用 Binary-Space-Partitioning 算法管理 items,能够快速的找出可视区内的 items 进行绘制(100万个 items 中可能只需要绘制 100 个 items),不会绘制所有的 items,绘制操作是非常耗时的,这也就是为什么影响效率最大的因素是可视区内的 items。

实际项目中添加 10万个 items 的效率和此处测试的 10万个 items 的效率是有些微区别的,绘制的消耗由其 paint() 函数决定。

Qt 全局快捷键

全局快捷键: 按下快捷键后,不管程序是否当前正在使用的程序,它都能得到此快捷键的事件通知,例如 Windows 里按下 Ctrl + Shift + A 就可以使用 QQ 截图一样。

Qt SDK 没有自带设置全局快捷键的功能,需要自己实现,在 Github 上也有人开源了全局快捷键的库,例如 QHotKey,可以直接在项目中使用。

QHotkey 有很多特点,能够满足绝大多数的需求:

  • Works on Windows, Mac and X11
  • Easy to use, can use QKeySequence for easy shortcut input
  • Supports almost all common keys (Depends on OS & Keyboard-Layout)
  • Allows direct input of Key/Modifier-Combinations
  • Supports multiple QHotkey-instances for the same shortcut (with optimisations)
  • Thread-Safe - Can be used on all threads (See section Thread safety)
  • Allows usage of native keycodes and modifiers, if needed

下面就写个 HelloWorld 级别的程序,定义全局快捷键 Alt+P 唤出程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <QHotkey>
#include <QApplication>
Widget::Widget(QWidget *parent) : QWidget(parent), ui(new Ui::Widget) {
ui->setupUi(this);
QHotkey *hotkey = new QHotkey(QKeySequence("Alt+P"), true); // Alt 和 P 之间不能有空格
qDebug() << "Is Registered: " << hotkey->isRegistered();
connect(hotkey, &QHotkey::activated, [this](){
this->show();
this->raise();
this->activateWindow();
this->raise();
QApplication::setActiveWindow(this);
this->raise();
});
}

继承 QThread 实现多线程

Qt 中使用多线程,最简单直观的方法就是继承 QThread,重写 run() 方法,需要使用多线程执行的代码放在 run() 函数中,调用 start() 函数启动线程,线程正在运行时 isRunning() 返回 true,结束运行后发出信号 finished()

实现线程

仍以读取文本显示到 QTextEdit 为例,类 ReadingThread 继承 QThread,在 run() 方法中读取文件并添加到 QTextEdit。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 文件名: ReadingThread.h
#ifndef READINGTHREAD_H
#define READINGTHREAD_H
#include <QThread>
class QTextEdit;
class ReadingThread : public QThread {
public:
ReadingThread(QTextEdit *textEdit, QObject *parent = NULL);
protected:
void run() Q_DECL_OVERRIDE;
private:
QTextEdit *textEdit;
};
#endif // READINGTHREAD_H
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
// 文件名: ReadingThread.cpp
#include "ReadingThread.h"
#include <QFile>
#include <QTextStream>
#include <QTextEdit>
#include <QMetaObject>
ReadingThread::ReadingThread(QTextEdit *textEdit, QObject *parent) : QThread(parent), textEdit(textEdit) {
}
void ReadingThread::run() {
QFile file("/Users/Biao/Desktop/data.txt");
if (!file.open(QIODevice::Text | QIODevice::ReadOnly)) {
return;
}
QTextStream in(&file);
while (!in.atEnd()) {
QString line = in.readLine();
textEdit->append(line);
}
}

多线程编程

读取文件显示到 text edit 中,一个非常简单的需求,啥也不说了,撸起袖子,打开 Qt Creator 开干。

先设计 UI 如下,中间是 QTextEdit,底部是按钮 QPushButton:

点击按钮,按行读取文件,然后添加到 QTextEdit,对我们来说也是分分钟的事(Lambda Lambda Lambda):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
ReadingWidget::ReadingWidget(QWidget *parent) : QWidget(parent), ui(new Ui::ReadingWidget) {
ui->setupUi(this);
// 点击按钮,按行读取文件,添加到 text edit 中显示出来
connect(ui->pushButton, &QPushButton::clicked, [this] {
QFile file("/Users/Biao/Desktop/data.txt"); // 文件路径自己修改一下啊
if (!file.open(QIODevice::Text | QIODevice::ReadOnly)) {
return;
}
QTextStream in(&file);
while (!in.atEnd()) {
QString line = in.readLine(); // 读取一行
ui->textEdit->append(line); // 添加到 text edit
}
});
}

分布式 ID 生成算法 Snowflake

分布式系统中,有一些需要使用全局唯一 ID 的场景,这种时候为了防止 ID 冲突可以使用 36 位的 UUID,但是 UUID 有一些缺点,首先他相对比较长,另外 UUID 一般是无序的字符串。

有些时候我们希望能使用简单一些的 ID,并且希望 ID 能够按照时间有序生成,为了解决这个问题,Twitter 发明了 SnowFlake 算法,不依赖第三方介质例如 Redis、数据库,本地生成程序生成分布式自增 ID,这个 ID 只能保证在工作组中的机器生成的 ID 唯一,不能像 UUID 那样保证时空唯一。

Snowflake 把时间戳、工作组 ID、工作机器 ID、自增序列号组合在一起,生成一个 64bits 的整数 ID,能够使用 70 年,每台机器每秒可产生约 25 万个 ID。

Snowflake 生成的 ID 的 bit 结构如下:

VS2013 使用 dll

Qt 使用 curl 一文中介绍了怎么编译 curl 并且在 Qt 项目中使用,那么在 VS 项目中应该怎么使用 curl 的 dll 呢?

动态库的使用分为隐式链接和显示链接两种方式:

  • 显示链接: 只需要 .dll 动态库文件,代码中使用 LoadLibrary + GetProcAddress 加载函数后需要自己进行函数类型转换:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // 函数类型定义
    typedef void (*DLLFunc)(int);
    // 加载 dll 中的函数
    HINSTANCE hInstLibrary = LoadLibrary("DLLSample.dll");
    DLLFunc dllFunc = (DLLFunc)GetProcAddress(hInstLibrary, "TestDLL");
    // 执行函数
    dllFunc(123);
  • 隐式链接: 需要 .h 头文件、.lib 库导入文件和 .dll 动态库文件,代码中直接使用库的函数即可

    推荐使用隐式链接,更省事,可参考 LIB 和 DLL 的区别与使用

VS2013 中隐式链接使用 dll 一般有两种方法:

  • 使用 #pragma 引入 lib

  • 设置 项目属性 引入 lib

Qt 使用 curl

Qt 已经提供了 QNetworkAccessManager 用于 Http 访问,Qt 访问网络的 HttpClient 对其进行了简单封装,如下就可以进行 GET 请求:

1
2
3
HttpClient("http://localhost:8080/device").get([](const QString &response) {
qDebug() << response;
});

但是,在非 Qt 项目中就不能使用 QNetworkAccessManager 了,还有就是因为 curl 成熟、强大、跨平台,可能有些项目更希望使用 curl,所以在此以 Windows MinGW 的 Qt 项目为例,介绍 curl 的集成使用。

Qt 安装

对于想学习 Qt 的同学来说,下载、安装 Qt 还真不是一件容易的事,但这也是学习 Qt 的基石,否则连个 Qt Hello World 都不能在电脑上编写,还玩什么呢?

下载

下载 Qt,首先想到的是到官网 https://www.qt.io 下载,悲催的是,别说对于新手,就算对于我这种业余爱好 Qt 好多年的伪骨灰,进去后都是懵逼状态,别说还要注册、登录,估计连找下载的地方都困难,大多数人在这估计就有放弃的想法了,不能从大门进,还不能直捣黄龙么,访问 http://download.qt.io/archive/qt/ 就可以无障碍的挑选想要的 Qt 版本了。

线段拟合曲线

QPainter 提供了绘制线段、矩形、椭圆、圆、圆弧、路径等的函数,如果想绘制正弦 (y=sin(x))、余弦 (y=cos(x)) 的曲线,QPainter 没有提供相应的绘制函数,应该怎么办呢?

李小龙的武术哲学: 以无法为有法,以无限为有限。

数学曲线是连续的,计算机的世界却是离散的,离散的世界使用极限的方式就可以模拟出连续的效果。可以把曲线想象成是一条一条线段连起来形成的图形,这些线段越短,连成的图形就越逼近曲线,这种方法就是线段拟合曲线,学过微积分的同学是不是感觉这个方法很熟悉?

下面以绘制正弦 (y=sin(x)) 曲线为例进行介绍:

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
void FittingCurveWidget::paintEvent(QPaintEvent *) {
QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing);
painter.translate(10, 150);
// 绘制坐标轴
painter.setPen(QPen(Qt::gray, 1, Qt::DashLine));
painter.drawLine(0, 0, 700, 0);
painter.drawLine(0, -200, 0, 200);
painter.setPen(QPen(Qt::black, 1));
// 计算正弦的坐标点,绘制线段
qreal prex = 0, prey = 0;
// [0, 314] 归一为 [0, PI]
for (int i = 0; i <= 628; ++i) {
qreal x = i;
qreal y = qSin(i/314.0*M_PI) * 100;
painter.drawLine(prex, prey, x, y);
prex = x;
prey = y;
}
}

Nginx 验证 Token

为了提高效率,常把 Nginx 作为静态文件服务器,把视频文件,JS,CSS 等放到 Nginx 上。例如我们要开发一个视频网站,免费视频不需要访问权限验证,收费视频就需要对用户的权限进行验证,验证通过了才能够继续访问,Nginx 可以借助 Lua 来实现访问验证,用户信息使用 token 表示

Nginx 简单的验证代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
location ~ /private/.+\.mp4$ {
root html;
access_by_lua '
-- 应用的 ID 和 key,和应用服务器上的一致
local appIdKeys = {["app_1"] = "key_1", ["app_2"] = "key_2"};
local args = ngx.req.get_uri_args();
local appId = args["appId"];
local appKey = appIdKeys[appId];
local token1 = args["token"]; -- 参数中 token
local token2 = ngx.md5(appId .. appKey); -- 用应用的 ID 找到对应的 key,然后根据算法计算 token
-- 如果参数中的 token 和计算得到的 token 不相等,则说明访问非法,禁止访问,否则放行访问
if token1 ~= token2 then
ngx.exit(ngx.HTTP_FORBIDDEN);
end
';
}

Nginx 和应用服务器上同时存储 appId 和 appKey,这样就能根据参数中的 appId 查找到对应的 appKey。至于使用 Lua 的变量存储,或者使用数据库,还是文件,根据具体的情况而定(Nginx 中 Lua 能够访问数据、Redis 等)。

上面的验证规则比较简单,如果其他人得到了 token,就可以无限制的访问了,为了增强安全性,可以使用更多的参数生成 token,例如用户 id,限制 URL 期限的时间戳等。

Nginx 默认没有安装 Lua 模块,需要自己安装,可参考 http://qtdebug.com/mac-nginx-lua

Nginx 安装 Lua 支持

Nginx 支持 Lua 需要安装 lua-nginx-module 模块,一般常用有 2 种方法:

  • 编译 Nginx 的时候带上 lua-nginx-module 模块一起编译

  • 使用 OpenResty: Nginx + 一些模块,默认启用了 Lua 支持(推荐使用此方式)

    OpenResty is just an enhanced version of Nginx by means of addon modules anyway. You can take advantage of all the exisitng goodies in the Nginx world.

    OpenResty® 是一个基于 Nginx 与 Lua 的高性能 Web 平台,其内部集成了大量精良的 Lua 库、第三方模块以及大多数的依赖项。用于方便地搭建能够处理超高并发、扩展性极高的动态 Web 应用、Web 服务和动态网关。

    OpenResty® 通过汇聚各种设计精良的 Nginx 模块(主要由 OpenResty 团队自主开发),从而将 Nginx 有效地变成一个强大的通用 Web 应用平台。这样,Web 开发人员和系统工程师可以使用 Lua 脚本语言调动 Nginx 支持的各种 C 以及 Lua 模块,快速构造出足以胜任 10K 乃至 1000K 以上单机并发连接的高性能 Web 应用系统。

    OpenResty® 的目标是让你的Web服务直接跑在 Nginx 服务内部,充分利用 Nginx 的非阻塞 I/O 模型,不仅仅对 HTTP 客户端请求,甚至于对远程后端诸如 MySQL、PostgreSQL、Memcached 以及 Redis 等都进行一致的高性能响应。

HTML5 使用 MQTT

HTML5 中也能使用 MQTT:

  1. ActiveMQ 启用 MQTT,可参考 http://qtdebug.com/misc-activemq/

  2. 启动 ActiveMQ: activemq start

  3. 使用 MQTT 的 HTML 如下:

    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
    <html>
    <head>
    <title>Test Ws mqtt.js</title>
    </head>
    <body>
    <script src="./browserMqtt.js"></script>
    <script>
    // 虽然使用的是 MQTT,但底层还是使用 WebSocket 实现的,所以这里的端口需要使用 ActiveMQ 里 WS 的端口 61614,而不是 MQTT 的端口 1883
    var client = mqtt.connect('ws://127.0.0.1:61614'); // you add a ws:// url here
    client.subscribe('foo'); // 订阅 Topic
    client.on('message', function(topic, payload) {
    console.log([payload].join('')); // 提取消息需要使用 [].join()
    })
    client.publish('foo', 'Hello World!'); // 发送消息
    // 不停的发送消息进行测试
    setInterval(function() {
    client.publish('foo', 'Time: ' + new Date().getTime());
    }, 1000);
    </script>
    </body>
    </html>
  4. 写一个 Java 的 MQTT 发布和订阅的程序一起测试,可参考 http://qtdebug.com/misc-mqtt/

下载 browserMqtt.js,也可以自己编译(新版本好像有问题,只能发消息,不能订阅消息),详细文档请参考 https://github.com/mqttjs/MQTT.js

简单的 Mock 工具 RestServerMock

前后端分离,如果前端需要等到服务器端接口开发完成后才能继续的话,效率太低,使用 Mock 工具能够更好的使得前后端分离各自开发,RestServerMock 一个是简单的静态 Web 服务器的 Mock 工具:

A Simple REST HTTP server that serves the configured JSON responses rest-server-mock.

Currently it servers JSON responses with status code 200. Future versions will support more options including : status codes, headers, encodings, etc.

Enum 注入

注入 Enum 可以使用字符串直接注入,或者借助 org.springframework.beans.factory.config.FieldRetrievingFactoryBean,下面以注入 FastJson 的 SerializerFeature 和自定义 enum 为例.

1
2
3
4
5
6
7
8
9
10
package com.alibaba.fastjson.serializer;
public enum SerializerFeature {
QuoteFieldNames,
UseSingleQuotes,
WriteMapNullValue,
WriteEnumUsingToString,
WriteEnumUsingName,
...
}
1
2
3
public enum Color {
RED, GREEN, BLUE
}

Spring Boot 热更新

热更新在开发中对于提高效率是非常重要的,SpringBoot 带了一个 org.springframework.boot:spring-boot-devtools,但是在 SpringBoot + Gradle + IDEA 的配合中不怎么好用,下面介绍另一种热更新的方式:

  • 引入 springloaded(不需要配置其他的):

    1
    2
    3
    4
    5
    6
    7
    8
    buildscript {
    repositories {
    jcenter()
    }
    dependencies {
    classpath 'org.springframework:springloaded:1.2.8.RELEASE'
    }
    }
  • 终端进入项目目录,执行 gradle -t classes 启动一个监听任务,当发现项目中的 Java 类发生变化时进行自动编译,模版文件变化时自动复制到 build 对应的目录中

  • 终端进入项目目录,执行 gradle bootRun 启动项目

    修改 Java 文件和模版文件等看看效果

Spring Boot Thymeleaf

SpringBoot 默认使用 Thymeleaf 2,为了使用 Thymeleaf 3,需要引入下面的依赖:

1
2
3
4
compile('org.springframework.boot:spring-boot-starter-thymeleaf')
compile('org.thymeleaf:thymeleaf:3.0.7.RELEASE')
compile('org.thymeleaf:thymeleaf-spring4:3.0.7.RELEASE')
compile('nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect:2.2.2')

application.properties 中配置:

1
2
3
4
5
spring.thymeleaf.mode=HTML5
spring.thymeleaf.cache=false
spring.thymeleaf.suffix=.html
spring.thymeleaf.encoding=UTF-8
spring.thymeleaf.content-type=text/html

Controller:

1
2
3
4
@GetMapping("/hello")
public String hello() {
return "hello"; // View 的名字为 hello.html
}

hello.html:

1
2
3
4
5
6
7
8
9
10
11
12
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
</head>
<body>
Thymeleaf
</body>
</html>

SpringBoot Redis

SpringBoot 中使用 Redis 非常简单:

  1. 引入依赖

    1
    compile('org.springframework.boot:spring-boot-starter-data-redis')
  2. 在 application.properties 中配置 Redis

    1
    2
    3
    4
    5
    6
    7
    8
    9
    spring.redis.host=localhost
    spring.redis.port=6379
    spring.redis.password=
    spring.redis.database=0
    spring.redis.pool.max-active=8
    spring.redis.pool.max-wait=-1
    spring.redis.pool.max-idle=8
    spring.redis.pool.min-idle=0
    spring.redis.timeout=0
  3. 使用 StringRedisTemplate 访问 Redis

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    @RestController
    public class HelloController {
    @Autowired
    private StringRedisTemplate redisTemplate;
    @GetMapping("/redis")
    public String redis() {
    return redisTemplate.opsForValue().get("user");
    }
    }

SpringBoot MyBatis

SpringBoot 使用 MyBatis 主要为以下 4 步:

  1. 引入依赖

    1
    2
    compile('org.mybatis.spring.boot:mybatis-spring-boot-starter:1.3.1')
    runtime('mysql:mysql-connector-java')
  2. 配置数据源: 配置 application.properties

    1
    2
    3
    4
    spring.datasource.username=root
    spring.datasource.password=root
    spring.datasource.url=jdbc:mysql://localhost:3306/test
    spring.datasource.driver-class-name=com.mysql.jdbc.Driver
  3. 编写 Mapper: 使用注解 @Mapper 自动生成 Mapper 对象

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    package com.xtuer.mapper;
    import org.apache.ibatis.annotations.Mapper;
    import org.apache.ibatis.annotations.Select;
    import java.util.Map;
    @Mapper
    public interface UserMapper {
    @Select("SELECT * FROM user WHERE username=#{username}")
    public Map findUserByUsername(String username);
    }
  4. 使用 Mapper: 使用 @Autowired 装配 mapper

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    package com.xtuer.controller;
    import com.xtuer.mapper.UserMapper;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RestController;
    import java.util.Map;
    @RestController
    public class HelloController {
    @Autowired
    private UserMapper userMapper;
    @GetMapping("/hello")
    public Map hello(@RequestParam String username) {
    return userMapper.findUserByUsername(username);
    }
    }

Hexo 跳过指定文件的渲染

Hexo 博客中所见文章都是经由渲染的静态网页,而静态网页的样式都直接由 Hexo 的主题控制,所以 Hexo 博客大部分都呈现出一种高度的统一化与规范化。不过 Hexo 提供了跳过渲染功能,使得我们可以直接在博客中放入自定义网页: 在 _config.yml 配置中配置 skip_render:

如果要跳过 source 文件夹下的test.html,可以这样配置:

1
skip_render: test.html

注意,千万不要加上个/写成/test.html,这里只能填相对于 source 文件夹的相对路径

如果要忽略 source 下的 test 文件夹下所有文件,可以这样配置:

1
skip_render: test/*

如果要忽略 source 下的 test 文件夹下.html文件,可以这样配置:

1
skip_render: test/*.html

如果要忽略 source 下的 test 文件夹内所有文件包括子文件夹以及子文件夹内的文件,可以这样配置:

1
skip_render: test/**

如果要忽略多个路径的文件或目录,可以这样配置:

1
2
3
skip_render:
- test.html
- test/*

参考:

九宫格绘图

很多时候都会使用图片作为 widget 的背景,如果图片和 widget 一样大的话那就没什么好说的,背景效果和图片的效果看上去一样,可更多的时候我们会面临图片和 widget 不一样大,如果把图片简单的缩放到和 widget 一样大作为背景的话,背景常常会变形、有锯齿等,如下面的背景图大小为 128 x 108,要作为 300 x 200 大小的背景,直接缩放绘制的效果很不好,如若使用接下来将要介绍的九宫格绘图技术来绘制背景的话,效果正是我们期望的:

  • 左边是直接缩放绘制的效果,背景发虚,有锯齿,圆角被放大
  • 右边是九宫格技术绘制的效果,圆角和背景的圆角一样