0%

只想记录一下现在的处境,希望有一天能正确的还原今天的历史。
不可多说,但是相信大家心知肚明。

2019年4月12日,“简书网”发布公告称,简书因违反《网络安全法》《互联网信息服务管理办法》《互联网新闻信息服务管理规定》等相关法律法规,严重危害互联网信息传播秩序,根据网信主管部门要求,从2019年4月13日至4月19日,暂停更新PC端上的内容,并对所有平台上的内容进行全面彻底的整改。
from https://www.jianshu.com/p/8718d4a67ef7

2019年4月13日雪球网表示因系统升级,发帖评论暂停,预计19日24时恢复。行情交易等其他功能正常,感谢理解。


上一篇文章讲述了 Celery 基本的使用方法,但是显然 Celery 的配置直接写在任务中并不是很方便。
这里我们可以选择把 Celery 的设置拆分为单独的 py 文件,这样后续管理也会更加方便。
那么这里我们就直接上代码了。

创建任务之前我们首先需要一个 Celery app 对象,为了后续更加直观我选择单独使用一个文件来创建 app 对象。这里命名为: celery_project_app.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#!/usr/bin/env python3
# -*- coding:utf-8 -*-

from celery import Celery

app = Celery(
"celery_app_name",
# include中包含的是任务所在的文件名
include=["celery_worker_tasks"]
)

# config_from_object顾名思义,从指定的文件中加载设置
app.config_from_object("celery_project_settings")

if __name__ == "__main__":
pass

Celery 通用的设置比较多,也可以单独列一个文件。这里我们写入到
celery_project_settings.py

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
67
68
69
70
71
72
73
#!/usr/bin/env python3
# -*- coding:utf-8 -*-

from kombu import Queue
import re


# 使用redis 作为消息代理
# BROKER_URL 在4.0中变更为 broker_url
# BROKER_URL = "redis://127.0.0.1:6379/0"
broker_url = "redis://:@127.0.0.1:6379/0"

# 任务结果存在Redis
# CELERY_RESULT_BACKEND 在4.0中变更为 result_backend
# result_backend = "redis://127.0.0.1:6379/0"
result_backend = "redis://:@127.0.0.1:6379/0"

# 任务信息传递的类型(默认json)
# CELERY_RESULT_SERIALIZER 在4.0中变更为 result_serializer
result_serializer = "json"
# CELERY_RESULT_SERIALIZER = "json"

# 时区
timezone = "UTC"

# 任务过期秒数
# CELERY_TASK_RESULT_EXPIRES 在4.0中变更为 result_expires
# result_expires = 60 * 10
result_expires = 60 * 10

# 任务完成指定数量后自动重启
worker_max_tasks_per_child = 1000

# List of modules to import when the Celery worker starts.
imports = ('celery_worker_tasks',)

# CELERY_QUEUES 在4.0中变更为 task_queues
task_queues = (
# 定义任务队列
# 路由键以“default_task.”开头的消息都进default队列
Queue("default", routing_key="default_task.#"),
# 路由键以“low_priority_task.”开头的消息都进low_priority_task队列
Queue("low_priority_task", routing_key="low_priority_task.#"),
# 路由键以“middle_priority_task.”开头的消息都进middle_priority_task队列
Queue("middle_priority_task", routing_key="middle_priority_task.#"),
# 路由键以“higher_priority_task.”开头的消息都进higher_priority_task队列
Queue("higher_priority_task", routing_key="higher_priority_task.#"),
)

# 设置默认队列名为 default
# CELERY_TASK_DEFAULT_QUEUE 在4.0中变更为 task_default_queue
task_default_queue = "default"
# CELERY_TASK_DEFAULT_EXCHANGE 在4.0中变更为 task_default_exchange
task_default_exchange = "default"
# CELERY_TASK_DEFAULT_EXCHANGE_TYPE 在4.0中变更为 task_default_exchange_type
task_default_exchange_type = "direct"
# CELERY_TASK_DEFAULT_ROUTING_KEY 在4.0中变更为 task_default_routing_key
task_default_routing_key = "default_task.default"

# CELERY_ROUTES 在4.0中变更为 task_routes
task_routes = (
[
(
#re.compile(r"low"),
"celery_app_name.celery_worker_tasks.low_rand",
{"queue": "low_priority_task", "routing_key": "low_priority_task.rand"},
), # 将celery_worker_tasks模块中的low_rand 分配至队列 low_priority_task
(
re.compile(r"middle"),
{"queue": "middle_priority_task", "routing_key": "low_priority_task.rand"},
), # 队列匹配支持正则表达式
],
)

注:在这里我们定义了多个队列,这样做的好处是,我们可以按照任务优先级放入不同的队列中避免有些慢速任务影响其他任务执行。如果不需要这样的功能,也可以直接使用默认队列。

Celery 设置好了,剩下的就是工作者和生产者了。
按照设置里面定义的创建生产者和工作者即可。

工作者: celery_worker_tasks.py

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
#!/usr/bin/env python3
# -*- coding:utf-8 -*-

from celery_project_app import app

import time
import random


@app.task
def low_rand(range_start, range_end):
# 模拟耗时操作
time.sleep(30)
return random.randint(range_start, range_end)


@app.task
def middle_rand(range_start, range_end):
# 模拟耗时操作
time.sleep(10)
return random.randint(range_start, range_end)


@app.task
def higher_rand(range_start, range_end):
return random.randint(range_start, range_end)


if __name__ == "__main__":
app.worker_main()

生产者为: celery_create_task.py

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
#!/usr/bin/env python3
# -*- coding: utf-8 -*-


import time
# 导入分布任务函数
from celery_worker_tasks import *


def process_task():
task_result = 0
task_object = None
try:
# 创建任务
# 设置超过60秒任务会超时
task_object = low_rand.apply_async((1,1000), expires=60)
# 等待任务结束
while not task_object.ready():
time.sleep(1)
if task_object.successful() == True:
#获取任务结果
task_result = task_object.get()
except Exception as e:
print('celery task unknow except.')
print(e)
finally:
#删除任务和结果
if task_object:
task_object.forget()
print('task return: %s' % task_result)
pass


def main():
while True:
process_task()
pass


if __name__ == '__main__':
main()
pass

最后执行起来的结果应该看起来是这样的:

1
2
3
4
5
6
7
8
9
/bin/python /opt/celery_task/celery_create_task.py
task return: 978
task return: 23
task return: 389
task return: 409
task return: 284
task return: 673
task return: 645
task return: 565

Celery大致的使用方法就是这样,下面总结一下我个人在使用过程中遇到的坑。

1、内存泄漏的问题。

撰写本文时, Celery 最新版本是4.2.1,在这个版本上调用 apply_async 会直接导致内存泄漏。此问题将会在4.3版本中修复。参见:https://github.com/celery/celery/pull/4839
除此以外还有一些其他疑似内存泄漏的地方,还需要等待后续的确认以及修复。

2、配合 redis 时行为和文档不太一致。

使用过程中,曾发现 redis 的内存占用不断增长,经过调查发现,是任务返回的数据没有被及时删除导致的。

文档原文:Backends use resources to store and transmit results. To ensure that resources are released, you must eventually call get() or forget() on EVERY AsyncResult instance returned after calling a task.

文档里说使用 backend 储存结果时,任务完成后必须使用 get 或者 forget 来释放资源。但是我个人使用中发现调用 get 并不能释放资源,仍然需要手动调用 forget 。 由于没有仔细研究,我不太清楚是不是只有搭配 redis 的时候是这样,不过如果遇到类似问题,可以尝试这样解决。

最近在项目上遇到了单机性能瓶颈,故趁此机会准备让项目中耗时的部分升级为分布式处理。考虑如果用 C++ 来进行开发的话周期太长,而且很容易出现问题,于是乎初步选择了 Python 和 Celery 库来解决问题。

注:本文将以Celery 4.X版本为基础。
Celery 简介

它是一个异步任务调度工具,用户使用 Celery 产生任务,借用中间人来传递任务,任务执行单元从中间人那里消费任务。任务执行单元可以单机部署,也可以分布式部署,因此 Celery 是一个高可用的生产者消费者模型的异步任务队列。你可以将你的任务交给 Celery 处理,也可以让 Celery 自动按 crontab 那样去自动调度任务,然后去做其他事情,你可以随时查看任务执行的状态,也可以让 Celery 执行完成后自动把执行结果告诉你。
Celery 基本概念

任务生产者 :调用 Celery 提供的 API ,函数,装饰器而产生任务并交给任务队列的都是任务生产者。

任务执行单元(Worker): Worker 是 Celery 提供的任务执行的单元, Worker 并发的运行在分布式的系统节点中

中间人(Broker):Celery 中通常使用中间人(Broker)在客户端和工作者(Worker)之间传递。在任务生产者向队列添加消息后 Broker 把消息传递给 Worker。

后端(Backend):Celery 中用于储存 Worker 执行完毕后的结果。

官方给出的实现 Broker 的工具包括:

Name Status Monitoring Remote Control
RabbitMQ Stable Yes Yes
Redis Stable Yes Yes
Amazon SQS Stable No No
Zookeeper Experimental No No

更多信息可参阅:http://docs.celeryproject.org/en/latest/getting-started/brokers/index.html

Celery 安装

正常来说安装是很简单的只需要pip install一下就可以了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
pip install celery[librabbitmq,redis,auth,msgpack]

#注:在 Python3.7 上 celery 4.2.0 之前的版本async模块和关键字async起了冲突,所以可能会提示错误。
#from . import async, base
#SyntaxError: invalid syntax
#详情参见 : https://github.com/celery/celery/issues/4500

#注:celery 4.3之前的版本存在内存泄漏的问题。
#如果遇到此类问题,建议反馈等待修复后使用开发者版本。
#解决方案:使用github上的最新版本(开发者版本)
pip install --upgrade --force https://github.com/celery/celery/zipball/master#egg=celery
pip install --upgrade --force https://github.com/celery/billiard/zipball/master#egg=billiard
pip install --upgrade --force https://github.com/celery/py-amqp/zipball/master#egg=amqp
pip install --upgrade --force https://github.com/celery/kombu/zipball/master#egg=kombu
pip install --upgrade --force https://github.com/celery/vine/zipball/master#egg=vine

第一个Celery应用程序

这里我们选择使用 Redis 作为中间人和后端。

由于 Redis 使用的非常广泛,介绍也很多,这里不单独讨论了。

1、Worker

作为分布式的程序当然需要有具体干活的代码, Worker 就是这个干活的代码。

这里直接贴出代码,可直接保存为 add_tasks.py 。

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
#!/usr/bin/env python3
# -*- coding:utf-8 -*-

from celery import Celery
import time
import os
import platform


def CeleryPlatformSet():
#使用platform来判断操作系统类型
sysstr = platform.system()
if(sysstr =="Windows"):
os.environ.setdefault('FORKED_BY_MULTIPROCESSING', '1')
elif(sysstr == "Linux"):
pass
else:
pass

CeleryPlatformSet()

#'redis://:[email protected]:port/db'
app = Celery('tasks', broker='redis://:[email protected]:6379/0',backend ='redis://:[email protected]:6379/0' )

@app.task
def add(x, y):
#模拟耗时操作
time.sleep(30)
s = x + y
print("sum is " + str(s))
return s

if __name__ == '__main__':
app.worker_main()

这个例子里添加了一个最基本的工作函数 add ,用来计算两个参数的和。

唯一需要注意的一点是,由于 Windows 下没有 fork 函数,所以如果Celery是在 Windows 下运行需要设置环境变量”FORKED_BY_MULTIPROCESSING”为1.

cmd下可以使用命令进行设置 (关闭后失效)

1
set FORKED_BY_MULTIPROCESSING=1

这里例子中直接使用了 os 包自动根据操作系统来设置。

启动worker可以使用命令

celery -A add_tasks worker -l info

或者:

python add_tasks .py worker

如果有疑问可以使用 celery –help 命令来查看 celery 命令的帮助文档。

2、生产者

有了完成任务的工作者当然也还需要一个负责添加任务的生产者。

下面是生产者的代码。命名为 start_tasks.py 。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#!/usr/bin/env python3
# -*- coding:utf-8 -*-

#导入任务函数add
from add_tasks import add
import time


def main():
#delay是异步调用,不会阻塞,程序会立即往下运行
result = add.delay(12,12)
# 循环检查任务是否执行完毕
while not result.ready():
print(time.strftime('%H:%M:%S'))
#每秒只检查一次
time.sleep(1)
#判断任务是否成功执行
print(result.successful())
#获取任务的返回结果
print(result.get())


if __name__ == '__main__':
main()

代码很简单,注释也写的比较清楚,这里不做过多的解释。保存为文件后可直接运行,通过输出我们可以观察到执行的过程和结果。
如果需要更多的工作者,可以把 worker 拷贝到更多计算机并行运行,需要注意的是 IP 是否正确, redis 这样的中间人是否能够被其他计算机正确访问。

友情提示:redis默认无密码,直接允许公网访问有极大风险,可能会直接导致服务器被入侵。请务必设置一个强大难以破解的密码并设置防火墙阻止他人访问。

最近非常的不太平,而微信的安全性和私密性始终无法让人满意,所以逼不得已只能转而去使用各种由非CN地区提供的聊天软件。
本人也试用过一些聊天软件,目前来看 Line 还算比较让人满意的一种选择。
唯独遗憾的是 Line 的 PC 客户端在因为 DNS 污染的原因无法正常登陆使用。
经过本人尝试在这里提出几个解决方法。
方法1、全平台适用,无论是 Linux、Windows 又或者 Mac OS 都可以使用 Google Chrome 版本的 Line 来登陆。
这个方法非常的简单,安装 Google Chrome 以后设置好代理,在访问 Chrome 商店安装 Line 提供的插件即可。
对于主要使用 Chrome 浏览器的人来说,如果设置全局代理不太方便,可以通过安装 SwitchyOmega 等插件的方式来管理代理。

方法2、通过 Proxifier 来代理 Line 客户端 由于 Chrome 版本提供的功能比较简单,还是需要使用完整版的 Line 客户端,这里也提供一种解决方法。
Proxifier 是一款功能非常强大的代理软件,可以让不支持通过代理服务器工作的网络程序能通过 HTTPS 或 SOCKS 代理访问网络。
首先下载 Line 客户端,这里不细说了。
官网地址:https://line.me/zh-hans/download
接着需要安装 Proxifier ( Proxifier 为付费软件,建议支持正版) 官网地址:https://www.proxifier.com/
安装完成之后 需要打开主界面找到 Profile 菜单找到 Proxy Server 设置代理服务器。
如图所示:


由于我本人使用的是 V2ray ,所以选择了 SOCKS5 ,大家需要根据自己使用的代理填写。
接着需要设置使用代理的程序,打开主界面找到 Profile 菜单找到 Proxification Rules

单击后会弹出如下的对话框

如上图所示,这里我们设置了 V2ray 等软件不通过代理直接连接,不匹配任何规则的直接连接, Line 客户端通过设置的代理连接。
接着有一点需要注意,一般的软件到这里就可以通过代理上网了,但是由于 Line 所使用的域名被 DNS 污染了无法得到正确的结果,所以还需要设置 DNS 全部走代理。 需要打开主界面找到 Profile 菜单找到 Name Resolution

弹出的对话框中,勾选通过代理解析主机名

确定后即可愉快的使用 Line 了。
注意, Proxifier 退出后则不会继续走代理,所以每次使用 Line 前需要先启动 Proxifier 。
如果嫌弃配置太过麻烦,可以直接使用我提供的配置文件,导入 Proxifier 后只需要修改一下代理服务器的地址和端口即可。 导入方法 file 菜单选择 import 即可。

PPX文件下载地址:Line_2D6C1EB6.ppx

Thunderbird用起来一直有个很讨厌的地方,如果收到邮件的邮件头中没有Date字段,那么Thunderbird就会把当前时间当作邮件的接收时间。
这样的话收邮箱中的旧邮件很有可能就会排在新邮件之前然后完全乱套了。
这里经过搜索发现解决方法,故记录下来。
1、在工具菜单中找到选项按钮,选择高级-常规,找到配置编辑器

2、找到或者新建一个配置mailnews.customDBHeaders,修改为”Received”。

3、对有问题的文件夹进行刷新,在收件箱上选择邮件属性,点击修复文件夹,Thunderbird将会对数据进行刷新。
4、做完这些步骤后日期一栏可能还是显示当前时间,可以在日期这个表头上邮件选择接收时间,即可看到正确的接收时间,然后按照接收时间排序即可。
注:针对IMAP协议,经过测试在Thunderbird 68.3.1 以上的版本中可以解决该问题。

如果 Linux 内核版本高于 4.10 则可以直接启用 BBR 算法。
对于Ubuntu来说 Ubuntu 18.04 LTS以上的版本可以直接启用BBR算法。

启用方法

1
2
3
4
echo "net.core.default_qdisc=fq" >> /etc/sysctl.conf
echo "net.ipv4.tcp_congestion_control=bbr" >> /etc/sysctl.conf
#使配置生效
sysctl -p

检查 BBR 是否开启

1
2
3
sysctl net.ipv4.tcp_available_congestion_control
sysctl net.ipv4.tcp_congestion_control
lsmod | grep bbr

wesuex 是一款用来辅助用户来管理系统权限的软件,支持最新的 Windows 10。通过 wesuex 可以使指定的进程运行在指定的权限或者账户下,以此可以达到保护系统安全,保障隐私等目的。
本产品为开源软件,采用 MIT License 进行发布。
目前发布的版本仅支持64位操作系统,并且要求系统版本高于 Windows 7 SP1 如果不能满足系统要求则无法正常使用。并且值得注意的是,由于依赖于 Windows 自身的权限管理机制,Windows 内核上的漏洞将会直接降低权限管理的安全性,所以并不推荐使用过旧的操作系统,建议采用最新的 Windows 10,并且务必为系统打好补丁。
有关源码请访问:https://github.com/wevsty/wesuex
安装包下载地址:https://github.com/wevsty/wesuex/releases
安装程序采用 NSIS 进行打包发布,安装过程十分简单,过程不再赘述。
安装完毕后在任意文件上右键可以看到 wesuex 的菜单,如下图所示。

单击菜单后程序将根据要启动的程序寻找启动配置文件。
比如:对 regedit.exe 右键选择 wesuex 打开,那么 wesuex 会在自己的安装目录中寻找 regedit.exe.ini,如果 regedit.exe.ini 这个配置文件不存在,则会使用程序默认自带的 default.ini。
default.ini 如下图所示,配置文件中可以记录程序的启动方法,用来详细管理权限。

wesuex 将会寻找 START_MODE 项下的 Program 作为启动程序 Command 作为传递给启动程序的命令行并以 MODE 选项下的指定模式来启动程序。
默认配置文件中 wesuex_starts.exe 是一个启动器,通过启动器使得程序可以开打 .docx 这样的非可执行文件,如果需要自定义启动程序也可以指定位其他的程序。
Command选项在传递过程中将会自动替换%command%为传入的命令。
START_MODE 项下的 MODE 选项用于指定程序将以什么样的权限来支持,目前支持3种选项。
LOW_PERMISSIONS:程序将默认按照低权限来启动指定的程序,这时程序会加载LOW_PERMISSIONS项下的选项。
integrity_level_sid用来指定进程完整性级别(Process integrity levels),UAC 通过进程完整性级别来限制程序,UAC开启的状态下程序启动通常为 Medium Mandatory Level 权限,这个权限将限制程序修改系统中的关键文件或者配置,作为默认配置,推荐使用。
Administrators_SID 用来去除 Administrators 组的权限,此选项无论是否存在都将强制禁用程序的 Administrators 权限。
AuthenticatedUser_SID 选项为 ”DISABLE” 是,将会禁用程序的 AuthenticatedUser 权限,如果选项不存在则不会禁用此权限。Authenticated Users 代表的是 Windows 系统中所有使用用户名、密码登录并通过身份验证的账户。
通过限制这些权限,我们就可以阻止对系统进行修改。
举例,按照默认配置低权限启动 taskmgr.exe (任务栏管理器)后我们就可以很明显的观察到,任务栏管理器中无法显示很多进程的用户组,并且也无法结束那些进程。
效果如下图所示:

也可以使用 procexp 来详细确认程序的权限。
procexp下载地址:https://technet.microsoft.com/en-us/sysinternals/bb896653/

当配置文件 START_MODE 项下的 MODE 为 LOGIN_WITH_USER 时将会加载 LOGON_INFO 中的配置,按照指定的用户名和密码登录账户并以此权限执行程序。(程序并不会自动创建账户,默认配置文件中仅作为示例)
注:配置文件中指定的账户可能需要至少登陆过一次,否则启动将可能会出现错误。
运行 taskmgr.exe (任务栏管理器)的效果如下图所示:

当配置文件 START_MODE 项下的 MODE 为 SYSTEM 时,wesuex 将会以 system 权限来启动该程序。

注:一般来说不需要使用 system 权限,如果使用此选项那么程序将拥有较大权限,这可能会产生安全问题。

我们可以为指定的程序创建指定的配置文件,这样使用右键菜单执行程序时,可以让程序以最合适的权限来进行启动。
唯一需要注意的是,如果程序已经以一个权限正在运行,那么在使用 wesuex 右键执行时可能无法降低或提高程序的权限。
比如 explorer.exe (资源管理器),如果直接对 explorer.exe 使用 wesuex ,由于 explorer.exe 并不会创建新进程而是以已经存在的进程打开一个新窗口,所以实际上更改权限的操作可能无效。

经过前面的几篇文章,相信大家对于 Qt Creator 这个 IDE 以及 Qt 基本的使用方法有所了解。这篇文章我们使用已经了解过的技术简单的定制一个计算器。

这里我们基于Qt Creator中定制GUI程序04(国际化多语言)中提供的工程在继续往下开发。
相信大家都用过 Windows 下的计算器,作为一个计算器,我们需要一个用来输入和显示结果的文本框(QTextEdit),我们还需要一些数字符号的按钮(QPushButton)。这些都是常用的控件,我们可以直接在 Qt Creator 上拖出来。

我们这里对整个窗口使用垂直布局,最上方放置了一个文本框,下方我们放置了一个 QGridLayout(珊格)布局在布局中放置了很多个按钮分别表示数字0-9和加减乘除等按钮。
放置后我们需要调整标识控件的名称(objectName 属性),控件显示的内容(比如 QPushButton 中的 text 属性),控件自动缩放的属性(sizePolicy),并为每个按钮控件设置快捷键(shortcut 属性)。
设置完成后结果如下图所示。

如果我们想对用户按下按钮作出反应,常规的做法是,为每个按钮注册Clicked的信号。不过很显然我们一个一个注册实在是太麻烦了。这里我们使用了一个偷懒的办法,按钮组(QButtonGroup)。如果我们需要处理界面上多个类似功能的按钮,我们可以把这些按钮组成一个组,然后为一个组设置一个响应函数就可以了。

在这里需要注意的是 Qt Creator 不能直接对 buttonGroup 组本身设置槽函数,所以需要手动创建一个槽函数并手动连接到信号。
这里我选择在界面初始化时 connect buttonGroup 的 buttonClicked 信号到一个 MainWindow 中的 Button_clicked 函数。

1
2
3
4
QObject::connect(ui->buttonGroup,
(void (QButtonGroup::*)(QAbstractButton *))&QButtonGroup::buttonClicked
, this
, &MainWindow::Button_clicked);

这样组内的任意按钮产生 Clicked 事件都会调用 Button_clicked 这个函数,我们可以看到 Button_clicked 函数传入了一个 QAbstractButton 对象的指针,这个指针用来确定组中具体是哪个按钮被按下了。
在计算器上,如果用户按下的按钮是数字,则显示在文本框里,如果是符号,则展开清屏或者计算操作。
这里我直接给出实现。

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
void MainWindow::Button_clicked(QAbstractButton *ojbect_button)
{
QString button_text=ojbect_button->text();
QString display_text=ui->textEdit->toPlainText();
if(display_text.length()>20)
{
ui->textEdit->setText("ERROR");
return;
}
if(display_text=="ERROR")
{
ui->textEdit->setText("");
display_text.clear();
}
if(button_text == "C")
{
ui->textEdit->setText("");
}
else if(button_text == "back")
{
display_text.remove(display_text.length()-1,display_text.length());
ui->textEdit->setText(display_text);
}
else if(button_text == "=")
{
QString str_rpn_exp=calc_string_to_rpn_string(display_text);
QString str_display=calc_rpn_string(str_rpn_exp);
ui->textEdit->setText(str_display);
}
else
{
display_text+=button_text;
ui->textEdit->setText(display_text);
}
}

我们简单的解释一下,通过 ojbect_button 指针我们可以调用 text 函数获取控件的 text 属性,通过 text 属性我们就可以知道是哪一个按钮被按下。如果是数字则直接添加到 textEdit 文本框中,如果是符号则进行相关处理。通过 setText 函数我们可以设置文本框显示的内容。
这里为了避免未知的问题,我限制了表达式的长度为20个字符,超过会显示 ERROR,大家可以自行修改此处。
我们可以看到当等号按钮被按下时,这里调用了 calc_string_to_rpn_string 和 calc_rpn_string 这两个函数。这两个函数用于把我们常见的中缀表达式转换成逆波兰表达式(也叫后缀表达式),最后在计算逆波兰表达式得到结果。
把中缀表达式转换成逆波兰式是为了更方便我们进行计算,这方面网络上有非常多的资料,我不过多赘述,直接给出有关的实现。

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
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
bool is_number(char ch)
{
if(ch >= '0' &amp;&amp; ch <= '9')
return true;
if(ch == '.')
return true;
return false;
}
QString calc_string_to_rpn_string(QString str_express)
{
QStack<char> stack_symbol;
QString str_rpnexp = "";
for(int npos = 0; npos < str_express.length(); npos++)
{
char ch = str_express[npos].toLatin1();
if(is_number(ch) == true)
{
str_rpnexp += ch;
npos++;
while(npos < str_express.length())
{
ch = str_express[npos].toLatin1();
if(is_number(ch) == true)
{
str_rpnexp += ch;
npos++;
}
else
{
break;
}
}
str_rpnexp += '#';

}
//other symbool
if(ch == '+' || ch == '-')
{
if(stack_symbol.isEmpty() != true &amp;&amp; stack_symbol.top() != '(')
{
do
{
str_rpnexp += stack_symbol.pop();
}
while(stack_symbol.isEmpty() != true &amp;&amp; stack_symbol.top() != '(');
}
stack_symbol.push(ch);
}
else if(ch == '*' || ch == '/')
{
if(stack_symbol.isEmpty() != true
&amp;&amp; (stack_symbol.top() == '*' || stack_symbol.top() == '/')
)
{
do
{
str_rpnexp += stack_symbol.pop();
}
while(stack_symbol.isEmpty() != true &amp;&amp; stack_symbol.top() != '(');
str_rpnexp += ch;
}
stack_symbol.push(ch);

}
else if(ch == '(')
{
stack_symbol.push(ch);
}
else if(ch == ')')
{
while(stack_symbol.isEmpty() != true)
{
ch = stack_symbol.pop();
if(ch != '(')
{
str_rpnexp += ch;
}
}
}
}
while(stack_symbol.isEmpty() != true)
{
char ch = stack_symbol.pop();
if(ch != '(')
{
str_rpnexp += ch;
}
}
return str_rpnexp;
}

QString calc_rpn_string(QString str_rpn_express)
{
QStack<double> stack_num;
for(int npos = 0; npos < str_rpn_express.length(); npos++)
{
char ch = str_rpn_express[npos].toLatin1();
if(is_number(ch) == true)
{

QString str_num;
str_num += ch;
npos++;
while(npos < str_rpn_express.length())
{
ch = str_rpn_express[npos].toLatin1();
if(is_number(ch) == true)
{
str_num += ch;
npos++;
}
else
{
break;
}
}
bool b_conversion = false;
double do_num = str_num.toDouble(&amp;b_conversion);
if(b_conversion == false)
{
return "ERROR";
}
stack_num.push(do_num);
}
if(ch == '#')
{
continue;
}
else
{
if(stack_num.length() < 2)
{
return "ERROR";
}
double b = stack_num.pop();
double a = stack_num.pop();
switch(ch)
{
case '+':
stack_num.push(a + b);
break;
case '-':
stack_num.push(a + b);
break;
case '*':
stack_num.push(a * b);
break;
case '/':
stack_num.push(a / b);
break;
default:
return "ERROR";
}
}
}
if(stack_num.isEmpty() == true)
{
return "ERROR";
}
double calc = stack_num.pop();
return QString("").setNum(calc);
}

这里我们简单的说明一下。
QString 这个类与 std::string 虽然有些类似但是也有一些区别。
我们都知道 std::string 实际上就像个 std::vector 是一个单字节的字符串实现。很显然 char 类型容纳不了所有的汉字,为了能储存显示汉字(或者其他的语言) std::string 在不同平台可能有不同的编码实现,比如在 Linux 上 std::string 中的内容采用的是 UTF-8 编码,而在 Windows 下使用的则是 ANSI 编码(对于中文版的 Windows 来说就是 GBK 编码)。如果全都是 UTF-8 可能没有什么问题,但是对不同语言版本的 Windows 编码就成为了大难题。
如果开发者想一劳永逸的解决各个语言版本的编码兼容性,Microsoft 推荐大家使用 UNIOCDE 编码(对于高于 Windows 2000 的系统版本,就是 UTF-16 编码,至于低于 Windows 2000 的系统版本,我建议选择拒绝兼容)但是使用 UTF-16 编码则不能直接与 Linux 系统兼容。
多系统的兼容很显然是一件非常麻烦的事情,作为传统的开发方法,开发者必须考虑每个平台的特点作出取舍或者写上一堆转换编码的函数互相转换。
在 Qt 中 QString 这个类为我们提供了统一的解决方案,QString 内部采用了 UNIOCDE 编码,这样使得任何文字都能正确的储存在 QString 中,在 Windows 上也可以直接获得良好的兼容性。当然作为牺牲,如果想用我们熟悉的 char 来解决问题,需要使用 toLatin1 函数转换成 Latin1 编码。
QStack 和 std::stack 类似,用来作为栈这种数据结构使用,熟悉栈的同学应该对 pop,push,top 等操作一目了然。
最后我们可以编译看运行结果。运行结果如下图所示。

当用户按下等号后将会计算出正确结果14。
工程源代码下载地址:
/uploads/public/qt_tutorial/qt_calculator_FCE2BED0.7z

绝大多数的 GUI 界面都是直接由各种控件组合堆叠而成的,有的程序界面设计的优良能从容面对各种状况(比如界面拉伸,多语言扩展),也有一些界面本身就不太友好(比如界面文字显示不全,控件不对齐),那么想设计一个友好的界面我们就必须得用到界面布局这样的功能。这里我们介绍一下 Qt 中界面布局的使用方法。
在 Qt 中进行界面布局有2种方法,第一种绝对定位,第二种布局定位。
我们首先来看第一种。我们平常使用的界面大多都是长方形或者正方形,我们可以把界面想象成大棋盘,棋盘上的每一个点都有固定的坐标,我们把控件放到这些坐标点上就能堆叠出我们想要的样子。这样的布局方法就是绝对定位。绝对定位很方便,因为控件在固定的位置不会变动,所以把空间拖到想要放的地方就行了。但是很明显,如果窗体的大小做出了改变控件并不会因此做出任何响应,我们可能会因此看到界面上出现大片的留白。我们也可以通过设置窗体属性限制用户改变窗口的大小,但这显然并不一定是个好方法。
当然我们也可以自己拦截窗口的消息事件通过自己的代码动态的确定各种控件的位置和大小,但是很显然这样太麻烦了。
这种时候我们我们就在 Qt 中就可以使用布局定位的方法使得控件能够自动的拉伸各种控件或者改变控件的各种坐标。
我们先看一张示意图。

界面布局中通常是这样设计的,这里有几种布局器可以使用。
QHBoxLayout:按照水平方向从左到右布局。
QVBoxLayout:按照竖直方向从上到下布局。
QGridLayout:在一个网格中进行布局,类似于excel中单元格那样。
QFormLayout:按照表格布局,每一行前面是一段文本,文本后面跟随一个组件(通常是输入框)。
我们可以在窗口布局中嵌入其他的布局,最后形成我们看到的界面。
我们可以先创建自己的布局控件放入布局中做出自己想要的样子。这个时候可能运行起来还是不能自动缩放布局的,我们还得为窗口创建布局,并且得确保所有控件都处于可缩放的状态。
窗口创建布局:

此外所有需要缩放的控件都需要设置 SizePolicy 属性。这个属性来控制布局管理中的控件的尺寸自适应方式。
有下面几种属性可以选择。
Fixed:控件不能放大或者缩小,控件的大小就是它的 sizeHint。 Minimum:控件的 sizeHint 为控件的最小尺寸。控件不能小于这个 sizeHint,但是可以放大。
Maximum:控件的 sizeHint 为控件的最大尺寸,控件不能放大,但是可以缩小到它的最小的允许尺寸。
Expandint:控件可以自行增大或者缩小。
注:sizeHint(布局管理中的控件默认尺寸,如果控件不在布局管理中就为无效的值)
我们都选择为Expandint即可实现自适应窗口大小。
通过 Qt Creator 我们可以很方便的实现可视化的界面设计,当然如果有特别的需要也可以通过手工编写代码的方式来完成界面布局。
Qt文档中给出的例子如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
QWidget *window = new QWidget;
QPushButton *button1 = new QPushButton("One");
QPushButton *button2 = new QPushButton("Two");
QPushButton *button3 = new QPushButton("Three");
QPushButton *button4 = new QPushButton("Four");
QPushButton *button5 = new QPushButton("Five");

QHBoxLayout *layout = new QHBoxLayout;
layout->addWidget(button1);
layout->addWidget(button2);
layout->addWidget(button3);
layout->addWidget(button4);
layout->addWidget(button5);

window->setLayout(layout);
window->show();

该例中创建了5个 Button 控件,然后把控件都加入 QHBoxLayout,最后形成的效果就是5个按钮按照顺序水平方向排列。

作为一个设计良好的 GUI 程序,支持多国语言是显而易见的需求,毕竟需要照顾到的用户人群也许是不分国界的,那么如何使得程序拥有多语言则是一个很大的问题,这里我们继续使用之前的例子来演示多语言的处理。
要设计多语言,我们首先向工程中加入 ts 文件。
如下图所示:

也就是向pro文件加入

1
2
3
4
5
TRANSLATIONS += \
zh_cn.ts

DISTFILES += \
zh_cn.ts

这里加入 DISTFILES 是给项目增加了其他文件的的意思,个人推荐加上这样方便点,当然也可以不加。
之后需要使用 Qt Creator 中的更新翻译菜单,更新翻译文件。
如下图所示:

*注意 .ts 文件不要自己手工创建,如果创建了一个空文件再更新翻译,可能会收到类似这样的错误“zh_cn.ts:1:0: 文档过早的结束。” *
更新完毕之后我们选择启动Qt语言家(Qt Linguist)对字符进行翻译。

启动Qt语言家后我们需要首先选择语言文件对应的是哪种语言。
如下图所示:

随后我们就能看到翻译界面,选择需要翻译的短语,然后填上相应的翻译内容最后保存即可。

这里注意的是,部分控件使用了 html 代码来控制格式内容,如果我们想保持和之前一致的显示风格,则需要连同 html 代码一起复制。这里我们为了演示效果,并没有留下 html 代码。 翻译完成后选择菜单的发布翻译即可,最后会生成一个 qm 文件。
为了让程序加载我们的翻译文件,我们需要在代码里面增加加载翻译的代码。
这里我们直接修改主函数,让程序根据系统设置的国家自动选择语言。

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
#include "mainwindow.h"
#include <QApplication>
#include <QTranslator>
#include <QLocale>
#include <QString>

int main(int argc, char *argv[])
{
QApplication a(argc, argv);
//部分教程会教大家使用QSystemLocale,但是最新版的Qt中该类已经取消
QLocale locale;
QTranslator translator;
QString load_name="";
QLocale::Country country = locale.country();
switch(country)
{
case QLocale::China:
load_name = "zh_cn.qm";
break;
default:
break;
}
if(load_name.length()!=0)
{
bool b_translator_load = false;
b_translator_load = translator.load(load_name);
if(b_translator_load==true)
{
a.installTranslator(&translator);
}
}

MainWindow w;
w.show();

return a.exec();
}

通过这些代码,如果系统设置为中国地区,程序在启动时会加载启动目录下的 zh_cn.qm 这个文件。这里需要注意的是,编译产生的可执行文件往往不在源代码目录,所以需要自行复制 qm 文件到可执行文件所在的目录。
最后运行效果如图:

工程源代码下载地址: /uploads/public/qt_tutorial/mainwindow_language_609C0483.7z