信息学竞赛 CPP 应用篇
线程与进程
进程
通常情况下,一个可执行程序在启动后,就是一个进程。
它拥有独立的可支配的资源,比如:内存。
32 位程序有独立的4G虚拟内存(由操作系统提供)
我们所说的内存地址就是指的:虚拟内存地址。
优点 操作系统对进程资源进行了很多保护,比如:两个进程的虚拟内存不会映射在同一物理地址上。
缺点 进程通信很慢,很麻烦。
线程
在同一进程中,可以同时有多个指令序列在执行。
它们共享同一虚拟内存,及其它(如:文件句柄,socket等)进程资源
但有自己的:工作栈,局部变量,执行断点等
可以把线程看成是轻量级的进程。
优点
创建线程比进程的成本要小得多,所以快速、高效,可以创建大量的线程。
多个线程共享内存,可以直接传递指针,很快速、方便。
缺点
没有保护机制,一个线程出错,可能所有线程都会挂掉。
对共享数据的访问存在竞争问题,使用锁可能会引起死锁。
c++11 之前的多线程
虽然操作系统大都支持多线程技术,但标准上并不统一。
c++一直都没有一个线程的标准使用方法,而是要调用本地系统的API
在unix/liux 下,POSIX规范,需要include
pthread_create(thread, attr, start_routine, arg)
参数分别:标识符指针,属性对象,运行函数地址,传入的参数
线程的显式退出调用 pthread_exit
windows 下则是另一种景象。
需要 include
CreateThread(属性,初始栈,回调函数, 线程参数,控制参数,传出参数);
如果创建成功,则返回句柄,否则 NULL
c++11 线程
c++11 终于把多线程作为标准库的特性。
需要先包含头文件:
void f()
{
for(int i=0; i<10; i++){
cout << "thread: " << i << endl;
}
}
int main()
{
thread td(f);
for(int i=0; i<10; i++){
cout << "main...." << i << endl;
}
return 0;
}
通过 thread 类来创建线程对象,参数为一函数,其内容为线程执行的内容。
td 对象掌管着线程的控制权。
此后,新线程与main线程同时执行,共享本进程的资源
更进一步地,竞争着本进程的排它资源(比如:控制台输出)。
在程序员的角度来看,我们只能说,两个线程是并发的。并不知道是否并行。
如果只有一个cpu,则通过系统调度,让一个线程执行一小段时间,切换到另一个线程,再执行一小段时间,再切换。。。
如果有多个cpu,可能真的是同时在执行。
无论如何,都存在着如何正确有次序地共享资源的问题。
c++11 sleep
为了观察清楚,我们可能会希望程序执行中能放慢脚步,让我们看清楚交叉的过程。
这个简单的需求也是个问题。因为c++11之前,没有统一的标准,让程序等待一会儿。
各种不同的sleep,五花八门。如果希望平台无关,看来只好自己循环了。。。
linux的
c++11 采用了 boost 中的方案:
std::this_thread::sleep_for 休眠一个时间段
std::this_thread::sleep_untill 休眠到一个时间节点
时间间隔的单位有预定义好的:
#include <chrono>
/*
std::chrono::nanoseconds
std::chrono::microseconds
std::chrono::milliseconds
std::chrono::seconds
std::chrono::minutes
std::chrono::hours
*/
// 休眠 5 秒钟:
this_thread::sleep_for(chrono::seconds(5));
原来的多线程程序可以修改为:
void f()
{
for(int i=0; i<10; i++){
this_thread::sleep_for(chrono::seconds(1));
cout << "thread: " << i << endl;
}
}
int main()
{
thread td(f);
for(int i=0; i<10; i++){
this_thread::sleep_for(chrono::seconds(1));
cout << "main...." << i << endl;
}
return 0;
}
等待到某一时刻:
cout << "begin" << endl;
chrono::system_clock::time_point p =
chrono::system_clock::now() +
chrono::seconds(5);
this_thread::sleep_until(p);
cout << "end" << endl;
计算程序执行的时间间隔
auto p1 = chrono::high_resolution_clock::now();
for(int i=0; i<100; i++){
cout << i << endl;
}
auto p2 = chrono::high_resolution_clock::now();
chrono::duration<double, std::milli> ela = p2 - p1;
cout << "use: " << ela.count() << " ms" << endl;
return 0;
实时效果反馈
1. 关于进程与线程, 说法正确的是:__
A 进程中至少包含一个线程,即主线程
B 多个进程间通信,可以通过传递指针的方式
C 同一进程的多个线程共用进程的栈
D 一个线程崩溃不会影响到其它线程
2. 使用 sleep 而不是空循环等待的主要好处是?__
A 等待的时间更准确
B 代码更简洁
C 更省资源,线程sleep期间不占用 cpu 资源
D 兼容性更好
答案
1=>A 2=>C
多线程
在线程启动以后,就和主线程并列地运行了。
这时,主线中的线程对象是局部变量,当超出作用域后,会调用terminiate从而引发异常。
有两个选择:
join
一定要在被调线程结束前调用这个函数,它会使主线程直接进入阻塞模式。一直等到被join的线程执行结束,才会被唤醒。
这个效果上看,与直接调用线程函数是一样的。
detach
还可以让出新线程的控制权,让它自生自灭。
detach就果让线程对象与它实际关联的线程脱钩,把控制权转交给c++的运行库。
这有点类似 unix 守护进程的概念。
脱钩后,线程对象就变成一个空壳,不能再通过它控制线程或与新线程通信了。
不管有没有 detach,主线程结束后,要回收所有资源,所有线程也会强制结束。
void f()
{
for(int i=0; i<5; i++){
this_thread::sleep_for(chrono::seconds(1));
cout << "thread: " << i << endl;
}
}
int main()
{
thread td(f);
td.detach();
this_thread::sleep_for(chrono::seconds(3));
cout << "main end" << endl;
return 0;
}
线程函数的多种形式
普通函数
函数对象
- 匿名函数
- 成员函数
void f()
{
cout << "void f() .." << endl;
}
struct F{
F(int a):x(a){}
void operator()(){
cout << "F::operator() " << x << endl;
}
void f(){
cout << "member f " << x << endl;
}
int x;
};
int main()
{
thread t1(f);
thread t2(F(20));
thread t3([](){cout << "noname func" << endl; });
F a(10);
thread t4(F::f, &a); // 相当于有参的函数
t1.join();
t2.join();
t3.join();
t4.join();
return 0;
}
this_thread
get_id() 获得当前进程的ID
yield() 放弃本线程本轮的时间片,让给其它线程执行。
thread([](){
cout << this_thread::get_id() << endl;
}).join();
cout << this_thread::get_id() << endl;
传参问题
线程函数可以有参数,个数不限,返回值如果有,会被忽略。
当传递指针的时候,会共享变量,这时要注意,当线程还在运行的时候,要保证共享的变量不会被销毁。
引用的传递有点不同,如果不加处理,编译器无法判断我们是想传引用,还是想传值。
可以这样处理:
void f(int& x){
this_thread::sleep_for(chrono::seconds(1));
cout << x << endl;
}
int main()
{
int x = 5;
thread td(f, std::ref(x));
x = 20;
td.join();
return 0;
}
同一个函数,可以创建出多个线程
void f(int x){
this_thread::sleep_for(chrono::seconds(1));
cout << x << endl;
}
int main()
{
thread t1(f, 10);
thread t2(f, 20);
t1.join();
t2.join();
return 0;
}
实时效果反馈
1. 调用 a.detach() 后, a原来对应的线程,说法正确的是:__
A a 原来的线程自由运行,直到代码结束。
B a 原来的线程被挂起
C a原来的线程后台运行,直到代码结束,或因主线程结束而强制结束
D a原来的线程不受控制,无法访问它
2. a.join()的含义是:__
A 让 a 线程进入休眠状态
B 让调用方进入休眠状态,a结束才能唤醒
C 把两个线程合并成一个线程
D 强行抢占a线程的执行时间
答案
1=>C 2=>B
线程互斥
多个线程在更新共享变量的时候,会出现逻辑问题。
假设,A B 两个线程都对 变量 x 进行加的操作。
A: { x = x + 1; };
B: { x = x + 1; };
通常,解决的办法是加锁。也就是,互斥量:mutex
需要include
一般需要定义为全局变 量 lock() 加锁,unlock() 解锁
mutex g1; //全局的排斥锁
void f()
{
for(int i=0; i<10; i++){
this_thread::sleep_for(chrono::milliseconds(500));
g1.lock();
cout << "thread: "
<< "<" << this_thread::get_id() << ">"
<< i << endl;
g1.unlock();
}
}
int main()
{
thread t1(f);
thread t2(f);
t1.join();
t2.join();
return 0;
}
要真正理解 mutex,先需要理解 线程的状态
lock 动作:
若未锁,则锁之。
若其它线程已锁,则自身进入阻塞态,等待唤醒
unlock 动作:
若已锁,唤醒等锁线程之一,若无等待线程,则开锁
若未锁,则==不确定行为==
另注意:对自己锁过的mutex, 再锁一次,==行为不定==
如果,线程可能多次加锁同一互斥量,
则可考虑用recursive_mutex
其lock:
如果已锁,并且是自己锁的,则仍为锁态,但计数加1,不会发生阻塞
注意,lock 多少次,就要unlock多少次才能解开销量。要求:lock, unlock 严格对称。
有时,lock时不想一直等待,想有个最大忍耐限度,即:超时机制。
std::mutex; // 非递归的互斥量
std::timed_mutex; // 带超时的非递归互斥量
std::recursive_mutex; // 递归的互斥量
std::recursive_timed_mutex; // 带超互斥量
mutex::try_lock()
试锁,如果已被别人锁了,则返回 false
timed_mutex::try_lock_for(时间间隔)
在一段时间内试锁,如果此时间间隔内无法得到锁,则返回 false
timed_mutex::try_lock_until(时刻)
在某一时刻前试锁,如果此时刻前获得锁,则罢,否则返回false
timed_mutex g1;
void f()
{
this_thread::sleep_for(chrono::seconds(1));
if(!g1.try_lock_for(chrono::seconds(3)))
cout << "fail .. " << endl;
else{
cout << "ok.." << endl;
g1.unlock();
}
}
int main()
{
thread th(f);
g1.lock();
cin.ignore();
g1.unlock();
th.join();
return 0;
}
实时效果反馈
1. 关于线程状态, 说法正确的是:__
A 线程在阻塞态,经到一时间片段,会醒来,检查条件是否满足
B 线程在阻塞态一直等到被某个事件唤醒
C 线程在阻塞态被唤醒后直接进入运行态
D 就绪态的线程可能会进入阻塞态
2. timedmutex::trylockfor(t),行为是:_
A 如果是本线程已锁过的,则返回true
B 如果是其它线程已锁过的,则立即返回false
C 如果是其它线程已锁过的,则阻塞本线程,直到目标解锁,或 t 时间用尽。
D 如果目标未锁,则不做其它,直接返回true
答案
1=>B 2=>C
lock_guard
mutex的使用手册:
lock与unlock必须成对出现。
lock后,千万不要忘记调用 unlock,否则其它线程遭殃了。
如果出现了异常,也要小心,容易忘记了 unlock
….
这是不是很耳熟? new delete 不就是这么说的吗?
然而,内存泄漏一直困扰程序员。
RAII 是一种很好的策略,这里也可以用。
它就是:lock_guard
lock_guard 是一个包装类型的模板类,仅仅封装某种类型的mutex,在析构时调用它的unlock成员函数而已。
mutex g1;
void f()
{
g1.lock();
for(int i=0; i<10; i++){
if(i==6) throw -1;
cout << this_thread::get_id() << ": " << i << endl;
}
g1.unlock();
}
int main()
{
thread t1(f);
thread t2(f);
t1.join();
t2.join();
cout << "main end" << endl;
return 0;
}
会发现,当一个线程抛出异常的时候,另一个线程就一直被锁着,没有机会被唤醒。
因为主线程在等待另一个线程的结束,因而也被阻塞着。
如果换用lock_guard
mutex g1;
void f()
{
lock_guard<mutex> lg(g1);
for(int i=0; i<10; i++){
if(i==6) throw -1;
cout << this_thread::get_id() << ": " << i << endl;
}
}
int main()
{
thread t1(f);
thread t2(f);
t1.detach();
t2.detach();
this_thread::sleep_for(chrono::seconds(1));
cout << "main end" << endl;
return 0;
}
使用注意:
中途不能解锁
对象不可复制
锁定应该尽量短,防止影响其它线程。有时仍需要手工控制锁定时机。
此时,可以采用“领养策略”
此策略,仍是用户自己控制锁定过程,但让lock_guard 来帮助解锁。
// 确保这之前互斥量已经锁定了
lock_guard<mutext> lg(g1, adopt_lock);
// 这只是接管了 g1, 但不会调 lock
为了不影响其它线程,锁的粒度要尽可能地小。
示例:
不用锁的情况:
int sum = 0;
void f(int x)
{
for(int i=0; i<1000 * 100; i++) {
sum += x;
}
}
int main()
{
sum = 0;
thread t1(f, 1);
thread t2(f, -1);
t1.join(); t2.join();
cout << sum << endl;
return 0;
}
用锁的情况:
int sum = 0;
mutex g1;
void f(int x)
{
for(int i=0; i<1000 * 100; i++) {
lock_guard<mutex> lg(g1);
sum += x;
}
}
int main()
{
sum = 0;
thread t1(f, 1);
thread t2(f, -1);
t1.join(); t2.join();
cout << sum << endl;
return 0;
}
lock_guard 很轻量,没有特殊要求的情况下,应该使用它。
实时效果反馈
1. 关于lockguard, 说法正确的是:_
A lock_guard 是一种RAII策略,在析构时会调用 unlock
B lock_guard 在实例化对象的时候,一定会调用互斥量 lock
C lock_guard 只能用于 mutex 互斥量
D lock_guard 可以拷贝构造
2. std::adoptlock 含义是:_
A 意向锁,析构时不调用 unlock
B 半路领养,不调用 lock
C 延迟锁,稍后再调用 lock
D 读锁,排斥写,不排斥读
答案
1=>A 2=>B
unique_lock
lock_guard 的优点:速度快,省资源,使用简单
lock_guard 缺点:简单粗暴,粒度没法控制,不灵活
是lock_guard 的升级版,可以做得更细腻(效率上低点,内存上多用点)
unique_lock实例化时可以加的额外参数:
adopt_lock : 同 lock_guard, 表示已经锁过了,不调用 lock
defer_lock: 没有锁过,稍后再 明确调 lock
try_to_lock :加锁,但不阻塞,以后可以检查状态
unique_lock 的成员函数:
函数 | 含义 | 备注 |
---|---|---|
lock | 加锁 | |
unlock | 开锁 | |
try_lock | 试锁 | 如已锁,则立即返回 false |
try_lock_for | 加锁,限时阻塞 | 如阻塞超时,则被唤醒 |
try_lock_until | 加锁,限时阻塞 | 如阻塞达时间点,被唤醒 |
release | 解绑互斥量 | 不再管理该互斥量 |
owns_lock | 是否锁成功 |
示例
int sum = 0;
mutex g1;
void f(int x)
{
unique_lock<mutex> uk(g1, defer_lock);
for(int i=0; i<1000 * 10000; i++) {
while(!uk.try_lock()){
cout << this_thread::get_id();
cout << " lock fail.." << endl;
this_thread::sleep_for(
chrono::milliseconds(200));
}
sum += x;
uk.unlock();
}
}
int main()
{
sum = 0;
thread t1(f, 1);
thread t2(f, -1);
t1.join(); t2.join();
cout << sum << endl;
return 0;
}
lock_guard 有的功能,unique_lock 都有。
但,如果 lock_guard 够用,就不要用 unique_lock,
因为,维护状态是有开销的。
用锁原则总结
- 共享对象的操作要考虑加锁
锁的颗粒度越小越好
尽量不要用 mutex 裸装上阵,危险处处!
如无特殊需要,用 lock_guard 即可
- 如控制上满足不了要求(一般是粒度),改用unique_lock
实时效果反馈
1. 关于uniquelock, 说法正确的是:_
A unique_lock 比 lock_guard 更省资源
B unique_lock 比 lock_guard 更快速
C unique_lock 比 lock_guard 更灵活
D unique_lock 比 lock_guard 更安全
2. uniquelock 对象调trylock函数时,为判断加锁成功否,==错误==的方法是:__
A try_lock() 返回 true 则成功
B try_lock 后,检查 owns_lock() 返回 true 则成功
C try_lock 后,直接把 unique_lock 对象当bool量判断
D try_lock 后, 把互斥量对象当 bool 量判断
答案
1=>C 2=>D
条件变量
mutex 的语义是互斥,为了保护共享资源,使之自动形成排队访问。
还有一种常见的需求是:
一个线程等待某个条件,条件满足了才可以执行,否则一直睡觉。
用mutex 也可以实现这个要求,但行为比较怪异。
vector<int> vec;
mutex g1;
void f()
{
g1.lock();
for(int i=0; i<vec.size(); i++){
cout << "thread f: " << vec[i] << endl;
}
g1.unlock();
}
int main()
{
g1.lock();
thread t1(f); // 要确保 g1 已经 lock
for(int i=0; i<10; i++) vec.push_back(i*10);
cout << "vec ready" << endl;
g1.unlock();
t1.join();
return 0;
}
这里要小心操作, 创建线程前,要保证 g1 已经加锁,然后,线程中的锁才会阻塞自己。
把排斥锁用于:等待-通知 的目的,很不直观,又容易出错。
改用 condition_variable 就好很多。
该对象的 .wait 方法实现在此变量下等待。直到有人调用该对象的 .notify_one, 或者 .notify_all。
需要加头文件:
vector<int> vec;
mutex mtx;
condition_variable cv;
void f()
{
unique_lock<mutex> lck(mtx);
cv.wait(lck);
for(int i=0; i<vec.size(); i++){
cout << "thread f: " << vec[i] << endl;
}
}
int main()
{
thread t1(f);
{
unique_lock<mutex> lck(mtx);
for(int i=0; i<10; i++) vec.push_back(i*10);
}
cout << "vec ready" << endl;
cv.notify_one();
t1.join();
return 0;
}
可以看到:
在调用 wait 前确保自己已经持有 unique_lock(间接持有mutex)。
notify 的时候,则不需要持有锁。
其动作的细节是:
.wait( ) 使本线程进入阻塞状态,但在睡眠前,unlock 持有的锁。
.notify_* 可能使得本线程醒来,醒来的第一件事是:lock
从工程实践的角度来说,这段代码还是不安全的。
因为线程可能因为其它的原因醒来,不一定是有人调了 notify_*
为避免莽撞,一般醒来要查一个标志(bool量),
这几乎成为了惯例:
vector<int> vec;
mutex mtx;
condition_variable cv;
bool ready;
void f()
{
unique_lock<mutex> lck(mtx);
while(!ready) cv.wait(lck);
for(int i=0; i<vec.size(); i++){
cout << "thread f: " << vec[i] << endl;
}
}
int main()
{
ready = false;
thread t1(f);
{
unique_lock<mutex> lck(mtx);
for(int i=0; i<10; i++) vec.push_back(i*10);
ready = true;
}
cout << "vec ready" << endl;
cv.notify_one();
t1.join();
return 0;
}
condition_variable 的强大之处在于:可以同时唤醒多个线程,这个自己实现很麻烦。
vector<int> vec;
mutex mtx;
condition_variable cv;
bool ready;
void f()
{
{
unique_lock<mutex> lck(mtx);
while(!ready) cv.wait(lck);
}
for(int i=0; i<vec.size(); i++){
cout << "thread ";
cout << this_thread::get_id() << " :";
cout << vec[i] << endl;
}
}
int main()
{
ready = false;
thread t1(f);
thread t2(f);
{
unique_lock<mutex> lck(mtx);
for(int i=0; i<10; i++) vec.push_back(i*10);
ready = true;
cout << "vec ready" << endl;
}
cv.notify_all();
t1.join();
t2.join();
return 0;
}
总结:
condition_variable 要配合 unique_lock 使用,wait前要持有锁
unique_lock 是包装了某个全局的 mutex
- 如果希望使用其它类型的 mutex,参见 condition_variable_any
实时效果反馈
1. 关于condtionvariable, 说法正确的是:_
A 实例化的时候,需要传入锁对象
B 调用 wait 的时候,需要传入锁对象
C 调用 notify_* 的时候,需要传入锁对象
D 不需要锁对象, 可以独立工作
2. conditionvariable 对象进入wait前后 ,锁的状态变化是:_
A 锁态变为非锁态
B 非锁态变为锁态
C 没有变化
D 不确定
答案
1=>B 2=>C
生产消费者模型
生产-消费者模型是典型的多线程协作场景
假设有若干个生产者,若干个消费者,它们独立运行。
大家共享一个队列,生产者往队列放东西,消费者取出东西。
当队列为空时,消费者睡觉,等待唤醒
当队列为满时,生产者睡觉,等待唤醒
需要一个 mutex 来实现互斥
需要两个 condition_variable,一个用于表达条件:队列非空,
一个用于表达条件:队列非满
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <queue>
#include <ctime> // srand
using namespace std;
queue<int> Q;
mutex mtx;
condition_variable cv_emp, cv_ful;
// consumer 消费者
void f1()
{
srand(time(0));
unique_lock<mutex> lck(mtx, defer_lock);
while(1){
lck.lock();
while(Q.empty()) cv_emp.wait(lck);
cout << this_thread::get_id();
cout << " consum " << Q.front() << endl;
Q.pop();
if(Q.size()<=10) cv_ful.notify_all();
lck.unlock();
int t = rand()%1000;
this_thread::sleep_for(chrono::milliseconds(t));
}
}
// producer 生产者
void f2()
{
srand(time(0));
unique_lock<mutex> lck(mtx, defer_lock);
for(int i=0; i<30; i++){
lck.lock();
while(Q.size()>10) cv_ful.wait(lck);
cout << this_thread::get_id();
cout << " produce " << i << endl;
Q.push(i);
cv_emp.notify_all();
lck.unlock();
int t = rand()%1000;
this_thread::sleep_for(chrono::milliseconds(t));
}
}
int main()
{
thread t1[3], t2[3];
for(int i=0; i<3; i++) t1[i] = thread(f1); //消费者
for(int i=0; i<3; i++) t2[i] = thread(f2); //生产者
for(int i=0; i<3; i++) t1[i].join();
for(int i=0; i<3; i++) t2[i].join();
return 0;
}
这里要注意 wait 的动作:
进入睡眠前,unlock
醒来后 lock,这个lock 会把同伴阻住。
实时效果反馈
1. 关于条件量, 说法正确的是:__
A 两个不同的条件量,必须使用不同的 mutex
B 同一个条件量,只能用同一个 mutex
C 条件量可以不需要unique_lock 而单独使用
D 以上都不对
2. 上例中,当所有生产者线程结束一段时间后,消费者线程处于什么状态?__
A 结束态
B 阻塞态
C 就绪态
D 僵尸态
答案
1=>D 2=>B
死锁问题
使用锁的两大魔鬼:
- 忘记了 unlock,导致被阻塞线程一直醒不过来
- 循环等待,产生死锁,谁也醒不过来
死锁的形成:持有锁,并申请等待另一把锁
产生死锁的示例
mutex mtx_a;
mutex mtx_b;
void f()
{
cout << "f .. begin" << endl;
mtx_a.lock();
cout << "mtx_a locked" << endl;
this_thread::sleep_for(chrono::milliseconds(200));
mtx_b.lock();
cout << "mtx_a, mtx_b locked" << endl;
this_thread::sleep_for(chrono::milliseconds(100));
mtx_b.unlock();
mtx_a.unlock();
cout << "f .. end" << endl;
}
void g()
{
cout << "g ... begin" << endl;
mtx_b.lock();
cout << "mtx_b locked" << endl;
this_thread::sleep_for(chrono::milliseconds(200));
mtx_a.lock();
cout << "mtx_a, mtx_b locked" << endl;
this_thread::sleep_for(chrono::milliseconds(100));
mtx_a.unlock();
mtx_b.unlock();
cout << "g ... end" << endl;
}
int main()
{
thread t1(f);
thread t2(g);
t1.join();
t2.join();
return 0;
}
解决方案
- 检测出死锁,牺牲某个线程
- 避免死锁的发生
按某个顺序持有锁(比如:字母序)
线程中计划锁哪些东西,要先规划好。
std::lock
传入多个互斥量为参数,有可能引起阻塞,但醒来一定保证拿到所有的锁。
此函数会自行决定锁的顺序和策略,可能会调用 lock ,try_lock,unlock等,
目标是保证不死锁,同时兼顾锁的使用效率。
是自动解决死锁问题的方案之一。
与之对应的,还有一个 std::try_lock 原理相仿
把前边的例子改为:
void f()
{
cout << "f .. begin" << endl;
lock(mtx_a, mtx_b);
cout << "mtx_a, mtx_b locked" << endl;
this_thread::sleep_for(chrono::milliseconds(100));
mtx_b.unlock();
mtx_a.unlock();
cout << "f .. end" << endl;
}
void g()
{
cout << "g ... begin" << endl;
lock(mtx_b, mtx_a);
cout << "mtx_a, mtx_b locked" << endl;
this_thread::sleep_for(chrono::milliseconds(100));
mtx_a.unlock();
mtx_b.unlock();
cout << "g ... end" << endl;
}
注意,解锁的时候仍是单独分别解锁。
原则是,只要可能,尽快解锁,免得影响其它线程。
另外,为了避开手工操作 mutex,也可以使用 lock_gard 来管理 mutex
void g()
{
cout << "g ... begin" << endl;
lock(mtx_b, mtx_a);
cout << "mtx_a, mtx_b locked" << endl;
{
lock_guard<mutex> lck1(mtx_a, adopt_lock);
lock_guard<mutex> lck2(mtx_b, adopt_lock);
this_thread::sleep_for(chrono::milliseconds(100));
}
cout << "g ... end" << endl;
}
因为 mutex 已经加锁,所以 lock_guard 需要 adopt_lock 领养策略
实时效果反馈
1. 下列哪个选项, 无助于减少或避开死锁:__
A 按照统一的固定顺序申请锁
B 尽早释放不用的锁
C 使用 try_lock,不使线程进入阻塞时持有锁
D 直接使用 mutex 而不是 lock_guard
2. 对std::lock,说法==错误==的是:__
A 不一定按参数顺序来申请锁
B 不一定用 lock, 也可能会用 try_lock
C 也可能对已经持有的锁,调用 unlock
D 该函数不会阻塞当前的线程
答案
1=>D 2=>D
thread_local
c++11新引入的一种存储类型修饰符。
c++原有的存储类型:
- 代码中的字面量
- 静态变量(全局,局部静态,静态成员变量)
- 栈变量
- 堆变量
现在又多了一个:thread_local
以此修饰符修饰的变量,具有线程的生存周期:
线程开始时生成,线程结束时销毁。
可以修饰:全局变量,static成员变量,本地static变量。
其实质是:c语言原来的全局存储的静态类型,分离出一种对线程隔离效果的全局类型。
c语言产生的年代,还没多线程的需求,不可能那么高瞻远瞩地设计。
普通的全局变量
int x = 0;
void g()
{
x++; // 理应使用 mutex 来实现互斥访问,不是本节要点
cout << x << endl;
}
void f()
{
for(int i=0; i<10; i++) g();
}
int main()
{
thread t1(f);
thread t2(f);
t1.join();
t2.join();
return 0;
}
局限于线程中的“全局变量”
thread_local int x = 0; // 实际上,只修改了这一个地方
void g()
{
x++;
cout << x << endl;
}
void f()
{
for(int i=0; i<10; i++) g();
}
int main()
{
thread t1(f);
thread t2(f);
t1.join();
t2.join();
return 0;
}
- thread_local 型变量的生存期:
每个thread_local变量在线程创建的时候创建,然后对本线程而言,相当于全局变量;
当线程销毁的时候,会同时销毁当初创建的这个thread_local变量。
这个变量当然不可能是静态存储的。
其它使用场合
静态成员变量
仅仅是面向对象的封装性的体现。
变量还是静态的,全局的,只是使用权限上加以约束。
改用 thread_local 后,保持对线程的全局语义,但已经不是静态的了。
局部static变量
本质上,还是静态全局的,只是作用域局限在一个函数范围。
改用 thread_local 后,保持对线程调用该函数的全局语义,但已经不是静态的了。
void g()
{
thread_local int x = 0;
x++;
cout << x << endl;
}
void f()
{
for(int i=0; i<10; i++) g();
}
int main()
{
thread t1(f);
thread t2(f);
t1.join();
t2.join();
return 0;
}
实时效果反馈
1. 关于threadlocal, 说法正确的是:_
A thread_local变量在多个线程间共享
B thread_local变量存储在静态空间
C thread_local变量在线程创建时申请,在线程销毁时释放
D thread_local变量不可以是用户定义类型
答案
1=>C
未来承诺
c++11 future & promise
我们在早前的课程中,一直有这样一个观念:
程序中,如果能不用全局变量,就尽量不用。
在学习线程时,为了通信,又不得不用全局变量,这不是灾难吗?
虽然,也可以 thread_local 变量,然后把指针传给另一个线程,但把指针传来传去的..
考虑如下的常见场景:
线程A,需要线程B的计算结果,以便进一步的计算。如果结果没出来就等待。
用一个全局量,加一个mutext,当然可以,但大量这样的情况,早晚会一片混乱!
c++11 的线程方案,并不能用join等待一个线程结束,并获得它的返回值。(linux的可以)
因为,c++11 提供了另一套更完备的解决方案。
大体思路是:
用一pormise 对象,关联到一个 future对象,
把promise对象传到计算线程中,
自己可以做其它事。。。
调用 future对象的 get_value,此时,如果对方没准备好结果,引起阻塞。
直到拿到了结果才返回。
此刻,在大洋彼岸,另一线程的情况:
结果一出来,就调用 promise 对象的set_value
。。。。
听上去有点头大??
90%的应用中,需求足够单纯。可以用 async 这个简化包装。
相当于一个套餐,用着省事。
#include <iostream>
#include <thread>
#include <future>
#include <chrono>
using namespace std;
int f(int x, int y)
{
this_thread::sleep_for(chrono::seconds(2));
return x * 10 + y;
}
int main()
{
future<int> res = async(launch::async, f, 5, 8);
cout << "do other things" << endl;
int r = res.get();
cout << r << endl;
return 0;
}
这里,
launch::async //表示创建新线程
launch::deferred // 表示延迟处理,同步调用,不创建新线程
launch::async | launch::deferred // 表示系统自己看着办
deferred可能是lazy模式,一直到get() 才会真的去处理
另外,如果线程中抛出了异常,可以在主线程中抓住,这很令人感动。。
实时效果反馈
1. 关于async函数模板, 说法正确的是:__
A async 必然会引起创建新线程
B async 会阻塞当前线程,返回计算线程的计算结果
C async 会立即返回一个 future对象
D async 会返回一个 promise 对象
2. 如果在async创建的线程中发生了异常,以下正确的是:__
A 在新线程和async的主调线程中都可能 catch
B 只能在新线程中catch
C 只能在async的主调线程中catch
D 与操作系统有关
答案
1=>C 2=>A
原子变量
加锁固然能解决多线程的一些问题。但工程实践表明,惹出的麻烦也不少。
能不能不加锁,也可以实现共享呢?
上图,假设 N 是个线程间共享的全局变量。
一个线程对它的操作,一般情况下可以看作 3 步:
- 读取 N
- 对 N 运算: ${N \longrightarrow N_1}$
- 写回新的值 ${N_1}$
如果,能保证,这3步操作不在线程间交叉,则不会引起冲突。
某一系列动作能不被其它线程干扰,则称之为原子操作
这个不被干扰,也有两层境界:
- 这一系列动作,在一个线程时间片完成,中间不被打断
- 允许中间打断(不在一个时间片),但期间所有欲染指者均被阻塞
显然,对1,需要操作系统一级的支持。
对2, 只是算法技巧,让使用者感觉这一系列动作是原子的。
atomic
c++11 提供的 atomic 模板系统地考虑了这些问题,甚至走得更远。
- 对自己的类型使用模板,可以保证写入、读取的原子性。
- 对常用内置类型提供了很多保证原子性的操作
- 对于编译器的乱序优化,提供了各种控制手段
- compareexchange* 提供原子性的另一种解决方案(已经被干扰了,如何处理?)
简单变量的原子操作
对简单变量的操作,也需要加锁,解锁,有时感觉很麻烦
int x ; x++;
bool b; b = !b;
如果不用锁,后果很严重:
int sum = 0; //没有加锁保护的全局变量
void f(int x)
{
for(int i=0; i<x; i++) sum++;
}
int main()
{
thread t1(f, 1000*100);
thread t2(f, 1000*100);
t1.join();
t2.join();
cout << sum << endl;
return 0;
}
如果没有原子量一说,x++ 的动作可能是:
【读取 x 值到寄存器; 寄存器值加1; 写回到 x】
这三个动作可能不会一气呵成,中间可能被其它线程打断。这就危险了。。。
所谓原子就是一系列不可被分割的动作。
它们要么不去执行,要么就全部执行,不会出现执行了一半被其它线程插进来的窘况。
如果能保证某一系列动作的原子性,就不需要用 lock 了。
需要 include
只把定义sum的一句改掉就可以了
atomic<int> sum{0};
atomic 是个模板,我们可以通过重载方法,来实现自己的原子类型。
c++11 已经替我们实现了常见类型的原子化。
上例中的 x++ 就是原子化的,因而不需要加锁
用 mutex 与原子量的耗时比较
int sum = 0;
mutex mtx;
void f(int x)
{
for(int i=0; i<x; i++) {
mtx.lock();
sum++;
mtx.unlock();
}
}
int main()
{
auto t = clock();
thread t1(f, 1000*1000);
thread t2(f, 1000*1000);
t1.join();
t2.join();
cout << sum << endl;
t = clock() - t;
cout << t << endl;
return 0;
}
实时效果反馈
1. 关于使用atomic代替锁的好处, 说法正确的是:__
A 执行更高效,更不容易出错
B 更好的操作系统兼容性
C 更广泛的应用领域,比如:嵌入式系统
D 对多核处理器更有利
2. 对于 atomic
A x = 5;
B x++;
C x.fetch_add(3);
D x += 6;
答案
1=>A 2=>D
GUI与Qt
GUI(Graphical User Interface) 图形用户接口
c++标准中并没有包含GUI,开发图形化界面要依靠第三方的库。
但小到桌面应用,大到大型游戏,很多应用都是用c++开发的,主要因为c++的性能优势。
框架十分多,常见的:
microsoft MFC windows平台
Qt 跨平台,可编写 Web,桌面,移动应用,艺术级的UI组件
Fltk 跨平台 小巧 静态,快速生成应用 OpenGL 支持3D
Wx Widgets 学院派 跨平台 免费
。。。
选择时要考虑的点:
- 是否有跨平台的需求
- 是否要兼顾移动设备和 web 应用
- 是否要定制组件,对组件的丰富、细腻要求多高
- 开发环境与开发效率
- 商用有哪些限制
- 借鉴已有的成功案例
Qt 历史
1991 奇趣科技开发 挪威[TrollTech]
1996 商业领域 是Linux 流行桌面KDE的基础
2008 奇趣被诺基亚收购,成为诺基亚的编程语言
2012 诺基亚没落后,几经波折,又被Digia 公司收购
2014 发布跨平台集成环境 Qt Creator,从Qt5 开始实现了对主流平台的全面支持
Qt 按不同的版本发行,分商业版和开源版
开源版是 LGPL 协议
Qt 优点
- 跨平台,一次编写到处编译
- 接口简单,上手快
- 有配套的IDE工具,能快速构建应用,开发效率高
- 可支持嵌入式开发
最简示例
选择编译环境时:
自动生成的工程:
使用窗体设计器
点击*.ui 进入它的设计视图
本质上,是在编辑一个XML文件
可以用 ctrl+F2 切换到XML视图,ctrl+F3切换到设计视图
建议不要手工编辑这个XML文件
左边是组件,可以拖动到form上
然后,在右边修改它的各种属性
再加入一个Button组件
为按钮添加事件:
至此,我们没写一行代码,第一个应用程序就完成了!
实时效果反馈
1. 关于Qt的优势, 说法==错误==的是:__
A 跨多种平台,包括移动和嵌入式
B 接口简单,上手容易
C 有开源版本
D 有软件巨头背书
2. *.ui 文件是:__
A 窗体组件文件,以二进制形式存储
B 窗体组件文件,以XML 格式存储
C 国际资源文件,以文件格式存储
D 图片文件,以二进制形式存储
答案
1=>D 2=>B
Qt框架分析
看一下工程中的主要文件:
文件分析
main.cpp 应用程序的入口,
其功能是:创建应用程序,创建窗口,显示窗口,运行应用程序,开启消息循环
#include "mainwindow.h"
#include <QApplication>
int main(int argc, char *argv[])
{
QApplication a(argc, argv); //代表应用程序
MainWindow w; // 主窗口对象
w.show(); // 显示窗口
return a.exec(); // 运行程序,并等待处理消息
}
mainwindow.h 定义窗体类的头文件
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
namespace Ui { // 命名空间
class MainWindow; //不是下面这个MainWindow类,
// 而是ui_mainwindow.h中定义的类
// 这个文件是由mainwindow.ui 自动生成的
} // 这相当于一个外部文件声明
class MainWindow : public QMainWindow
{
Q_OBJECT // 使用Qt的信号与槽机制,必须用这个宏
public:
explicit MainWindow(QWidget *parent = 0);
~MainWindow();
private:
Ui::MainWindow *ui; //就是前边声明的类,代表ui组件的入口
};
#endif // MAINWINDOW_H
mainwindows.cpp 主窗体类的实现代码
#include "mainwindow.h"
#include "ui_mainwindow.h" // 很重要,包含所有窗体上的内容
// 这个.h文件是自动生成的,不要手工修改,改了也没用
MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::MainWindow) // 向ui注入值
{
ui->setupUi(this); //此处创建了各个组件,建立属性,
//建立信号与槽的连接等初始化工作
}
MainWindow::~MainWindow()
{
delete ui;
}
mainwindow.ui 窗体设计文件
这个不是标准c++的文件,无法直接编译它
实际上,它是一个普通的XML文件
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>MainWindow</class>
.....
<widget class="QToolBar" name="mainToolBar">
<attribute name="toolBarArea">
....
....
</hints>
</connection>
</connections>
</ui>
这里边定义了所有在设计时指定的信息,需要一个预处理过程,
把它转化生成为c++可以编译的文件。
窗体设计器 —> mainwindow.ui —-> ui_mainwindow.h
这个 ui_mainwindow.h 是可以用 c++编译的,是标准的c++语法
其中,用面向对象的方式,封装了整个窗体上的所有组件。
综上,我们拿到了ui 指针,就可以访问窗体上的所有组件,并用代码改变它的属性。
【演示】给按钮添加一个click事件,改变按钮上的文本。
实时效果反馈
1. 关于QOBJECT宏的作用, 说法正确的是:_
A 每个Qt类都必须要这个宏
B 每个想要使用 signals & slots 的类必须用这个宏
C 在窗体定文件中需要这个宏
D 在主程序文件中需要这个宏
2. mainwindow.ui 与 uimainwindow.h 的关系是:_
A mainwindow.ui 预处理后,生成ui_mainwindow.h文件
B ui_mainwindow.h 预处理后,生成 mainwindow.ui 文件
C 窗体设计器直接生成 ui_mainwindow.h
D 所有的Qt窗体程序都用 ui_mainwindow.h 作为公共定义
答案
1=>B 2=>A
信号和槽
信号和槽是Qt编程的基础。
不同的GUI系统,体系结构可能很大不同,但都需要面对同样的需求。
比如:如何处理界面上发生的事件。
有用回调函数(callback function)的,有MVC模型的,观察者模式的。。。
Qt 发明了 signals & slots 机制,使交互机制变得灵活、直观、简单。
信号(signal) 就是在特定情况下发生的事件。类似广播出去。
槽(slot) 就是一个响应函数。
信号和槽是完全隔离的,信号发出者并不知道该信号会被谁处理。
槽是一个有某种约定的普通函数,它可以被直接调用,也可以被关联到槽。
关联的方法:
QObject::connt(sender, SIGNAL(信号), receiver, SLOT(槽函数));
关联约定
- 一个信号可以关联到多个槽
- 多个信号可以关联到一个槽
- 一个信号可以关联到另一个信号
- 信号的参数与槽的参数要匹配(信号至少能满足槽的参数)
- 一般情况下,当信号发生时,槽函数会被立即触发,就像调用槽函数一样。
关联方法
下面举例常见的三种信号与槽的关联方法:
- 设计视图的Signals & Slots 面板
依次选 发送者,信号,接收者,槽
如果还觉得不直观,可以用可视化编辑
在弹出的框中
这两种方法本质上是一样的
只关联已有槽,删除也容易
- 添加新的槽和关联
在组件上: 右键 | 转到槽,会自动添加槽,其名字是有规律的。
可以自己在槽函数中加入处理逻辑
槽的名字有特殊含义,Qt就是用这个名字来识别的
删除时,只要.h 和.cpp中删除了相应代码即可(关联是动态的)
- 手工生成槽函数和关联
手工添加槽函数:
void myCheck(bool b);
并在其中添加逻辑代码
手工添加关联:
ui->setupUi(this);
connect(ui->checkBox_3, SIGNAL(clicked(bool)),
this, SLOT(myCheck(bool)));
手工删除槽和关联代码
实时效果反馈
1. 关于Qt信号和槽, 说法正确的是:__
A 信号和槽的参数个数和格式必须严格匹配
B 信号与槽必须一对一匹配
C 可以用connect函数把信号和槽关联在一起
D 槽必须有特殊的名字, 不能随意命名
2. 当使用UI设计器生成槽函数时,在哪个代码中,最终把信号和槽关联?:__
A ui->setupUi(this);
B Qt编译器内置的机制
C ui(new Ui::Dialog)
D 通过XML文件关联,运行时读取
答案
1=>C 2=>A
布局管理
早期的桌面应用,一般窗口的大小是固定的,这样就很少有布局的要求;
至多是组件间的对齐关系。
随着移动应用、嵌入设备、web应用的普及,桌面应用对布局的要求越来越强烈。
比如:横竖屏的切换,分辨率的调整。。。。
简单地说,布局管理,主要任务是处理:当窗体改变大小的时候,如何调整组件的位置,大小,以达到更人性化的呈现效果。
Qt提供了十分丰富的布局管理控制,主要在:Layouts 和 spacers 这两个面板中。
常用的布局
- vertical Layout 垂直布局
- Horizontal Layout 水平布局
- Grid Layout 网格布局
可以拖放布局到窗口中,再把组件拖放到布局里。
布局中可以再嵌套布局,形成十分复杂的行为
这是在水平布局中嵌套垂直布局的情况。
此外,也可以先放组件,以后再配布局。这通过工具栏上的布局按钮或右键菜单来完成。
占位器组件
俗称:弹簧
horizonal spacer 水平点位器
vertical spacer 垂直点位器
当需要让一些组件靠某一边对齐时,可以用弹簧配合组件的其它属性来完成。
当布局改变大小的时候,组件需要响应这种变化。
其主要的影响因素是:
整个窗体上的最外层布局
整个窗体上可以有一个最外层的布局,当窗体改变大小的时候,这个布局跟随调整大小。
可以选中窗体,再用工具栏的布局按钮来完成,
也可以在窗体上,右键 | 布局 ,来选相应的菜单。
如果需要取消窗体上的最高布局,同样方法,选:“打破布局”。
综合示例
实时效果反馈
1. 关于Qt常用的布局组件, 说法==错误==的是:__
A 垂直布局
B 水平布局
C 网格布局
D 流布局
2. 如何让一个按钮,总是在窗体的右下角:__
A 直接拖放到右下角
B 左边加弹簧,水平布局
C 左边加弹簧,水平布局,外面再其上加弹簧,垂直布局
D 无法实现
答案
1=>D 2=>C
可视化UI设计
UI可视化设计的本质是生成一个XML文件。
Qt 可以设计模式和源文件模式来查看这个XML文件
ctrl + F2 切换到源码模式
ctrl + F3 切换到设计模式
我们不要直接去修改 XML 文件,而应该在界面上修改。
- 通过 Container 可以实现“国中国”的效果。
比如,对:checkBox 和 radio Button 的分组,很常用。
container 本身可以设布局方式,如果不设,然后却参加到另一个布局中,可能会有怪异的表现。
container 可以看作窗体中的一个子窗体
伙伴关系
点击伙伴关系按钮,可以设置Label与其它组件的伙伴关系。
设置伙伴关系的目的是:可以通过快捷方式让焦点快速跳到伙伴组件上。
直接在界面上通过拖放来设置伙伴关系
- Tab序
通过按tab键,可以让焦点在各个输入组件间跳,其顺序是可以控制的。
Qt 并不是新的语言,虽然我们用了可视化设计,没有自己写代码,但这只不过是Qt 替我们堆砌了一些冗长、乏味的代码,最终依然是需要生成了cpp的源码才能工作的。
而这些代码(如果我们需要查看)在 ui_xxx.h 中
这个文件不在项目的源文件中,是每次编译之前动态生成的。
xxx.ui 是一个标准的XML文件,Qt对这个文件进行解析,生成了 ui_xxx.h
实时效果反馈
1. UI可视化设计的原理, 说法正确的是:__
A 界面设计,产生了XML文件,预处理为 .h 文件
B 界面设计,直接产生了cpp文件
C 界面设计,直接产生了 .h 文件
D 界面设计,直接产生了 二进制码
2. UI可视化设计中,设置伙伴关系的主要目的是:__
A 使标签跟随输入组件移动
B 使输入组件有一定的顺序
C 使标签的样式与伙伴一致
D 快速定位到输入组件
答案
1=>A 2=>D
代码化UI设计
虽然Qt提供了可视化的UI设计方式,但有时也会用到代码直接设计UI的情景。
直接用代码与可视化 —> XML—> ui_xxx.h 的本质是一样。
用代码会更繁琐一些,但控制能力,灵活性也会更强一些。
其基本步骤大体分为:
- 建立界面元素的对象,及其关系
- 编写槽函数
- 建立槽函数与信号的连接关系
目标
功能很简单,点击radioButton,让下面的文本框中的字变颜色。
实现
新建项目
选QWidget为继承类
widget.h 文件
#ifndef WIDGET_H
#define WIDGET_H
#include <QWidget>
#include <QRadioButton>
#include <QPlainTextEdit>
#include <QButtonGroup>
class Widget : public QWidget
{
Q_OBJECT
public:
Widget(QWidget *parent = 0);
~Widget();
private slots:
void mySetColor(int id);
private:
QRadioButton* rbtnBlack;
QRadioButton* rbtnRed;
QRadioButton* rbtnBlue;
QPlainTextEdit* txtEdit;
QButtonGroup* btngColor;
};
#endif // WIDGET_H
widget.cpp 文件
#include "widget.h"
#include <QHBoxLayout>
#include <QVBoxLayout>
#include <QButtonGroup>
Widget::Widget(QWidget *parent)
: QWidget(parent)
{
rbtnBlack = new QRadioButton("黑");
rbtnRed = new QRadioButton("红");
rbtnBlue = new QRadioButton("兰");
QHBoxLayout* lay = new QHBoxLayout;
lay->addWidget(rbtnBlack);
lay->addWidget(rbtnRed);
lay->addWidget(rbtnBlue);
txtEdit = new QPlainTextEdit;
txtEdit->setPlainText("haha \n测试");
QFont font = txtEdit->font();
font.setPointSize(20);
txtEdit->setFont(font);
QVBoxLayout* lay2 = new QVBoxLayout;
lay2->addLayout(lay);
lay2->addWidget(txtEdit);
setLayout(lay2);
btngColor = new QButtonGroup(this);
btngColor->addButton(rbtnBlack,0);
btngColor->addButton(rbtnRed,1);
btngColor->addButton(rbtnBlue,2);
connect(btngColor, SIGNAL(buttonClicked(int)),
this, SLOT(mySetColor(int)));
}
Widget::~Widget()
{
}
void Widget::mySetColor(int id)
{
QPalette pl = txtEdit->palette();
switch (id) {
case 0:
pl.setColor(QPalette::Text, Qt::black);
break;
case 1:
pl.setColor(QPalette::Text, Qt::red);
break;
case 2:
pl.setColor(QPalette::Text, Qt::blue);
}
txtEdit->setPalette(pl);
}
实时效果反馈
1. 下列说法正确的是:__
A 所有的界面效果都可以通过可视化UI设计来完成
B 所有的界面效果都可以通过纯代码UI设计来完成
C 使用可视化UI设计后,很难再用代码介入
D 代码化设计UI运行效率更高
2. 关于QButtonGroup 说法正确的是:__
A QButtonGroup 是可视化组件,类似groupBox
B QButtonGroup 是逻辑组件,只能用代码操控
C QButtonGroup 一般需要自已 delete
D QButtonGroup 可以调整布局
答案
1=>B 2=>B
菜单
主窗口与对话框窗口的区别在于:
主窗口中,一般有主菜单,工具栏,状态栏。
主菜单可以在设计时生成,也可以用代码动态生成或修改。
设计时
代码生成也可以,也可生成多级菜单
核心概念是 Action
Action
action 代表一个动作项,可以是一个主菜单的末级,也可以右键弹出菜单的末级。
也可以把Action放到工具栏上,
这样,在多个操作路径下,可以对应同一个Action
而Action的信号可以关联到槽。
Action还可以设置统一的图标,统一的悬浮提示,甚至使用统一的皮肤管理。
Action 可以独立于主菜单而存在
右键菜单
有多种方法,一种方法是:重载QMainWindow的contextMenuEvent函数
在其中实现自己的逻辑。
可以先在界面上建立Action
然后,用代码构建右键菜单,再把Action加入到菜单的末级
QMenu m;
QMenu m1("1111");
m1.addAction(ui->actionmy1);
m1.addAction(ui->actionmy2);
m.addMenu(&m1);
m.addAction(ui->actiona11);
m.exec(QCursor::pos());
实时效果反馈
1. 关于Action, 说法正确的是:__
A Action 是一种QWidget
B Action可以单独创建,可以被主菜单、工具栏、右键菜单共享
C Action有特定成员函数,点击时被触发
D Action只管理文字,不包含图标
2. 在创建右键菜单时,QMenu为什么可以用栈对象?__
A exec 在用户选菜单的过程中是阻塞的,QMenu对象在这期间一直有效
B QMenu 是特殊对象,contextMenuEvent 函数结束时,它仍在
C QMenu 不应该这样用,虽然没报错,但操作菜单时,QMenu已经销毁
D QMenu虽然销毁了,但Action还在
答案
1=>B 2=>A
Qt类库概述
Qt在使用类库的方式,在标准的c++基础上进行了扩展。
引入了元对象系统,信号与槽,属性等特征。
元对象编译器(Meta Object Compiler)是一个预处理器,在编译前把Qt特性先预处理为:c++可以识别的标准代码。
QObject 是所有使用元对象系统的类的基类。
要求其内部声明 private 的: Q_OBJECT 宏。
元对象系统提供了很多特性。
除了信号与槽外,另一个常用的是:属性
很多面向对象的高级语言,比如 delphi,都提供了属性这一个概念。
c++ 没有在语言层面提供这一概念。
Qt 通过元对象系统弥补了这一缺憾。
简单地说,属性,就是对象的虚假的“成员变量”
从外部的观点来看,某个对象具有某种类型的变量,但实际上,
对它的读写操作可能分别由不同的成员函数来完成。
这有什么用处呢?
面向对象的封装原则。对象的属性好比智能类型,不只简单的读写,至少能防误
比如: age, 设置为负数,或其它不合理数值,会被发现并拒绝
对象使用的一致性。比如,在脚本语言中,如何操作c++对象,需要一致的方法。
对属性的读写可能引发一系列复杂的动作。
比如:写age的值,可能顺带引发一个信号。
这样,我们在修改一个对象的属性的时候,可能触发一系列的动作。
属性示例
新建一个 QBoy 类,包含一个age 属性
可以 读, 可以写,
写入成功的时候,触发一个 ageChanged 信号
最后,我们把这个信号 和 一个自己定义的槽关联 起来
使用属性需要在类中用 Q_OBJECT 宏
然后用宏添加属性:
Q_PROPERTY(int age READ age WRITE setage NOTIFY ageChanged)
Qt类库
Qt不仅仅是提供了跨平台GUI快速开发能力,
其类库还包含了很多其它功能:
- 基本模块
- 附加模块
- 增值模块
- 技术预览模块
基本模块中的内容已经很丰富
模块 | 内容 |
---|---|
Qt Core | 其它模块都要用到的核心(不含图形) |
Qt GUI | 设计GUI程序用到的基础类(含 openGL) |
Qt Multimedia | 音频、视频、摄像头等 |
Qt Multimedia Widgets | 实现多媒体功能的界面类 |
Qt Network | 简化网络编程 |
Qt QML | 用于QML和 Javascript |
Qt Quick | 构建具有定制用户界面的动态应用程序的声明框架 |
Qt Quick Controls | |
Qt Quick Dialogs | |
Qt Quick Layouts | |
Qt SQL | 用于数据库的操作 |
Qt Test | 单元测试 |
Qt Widgets | 用于构建GUI界面的组件类 |
实时效果反馈
1. 关于属性, 说法正确的是:__
A 属性是c++11引入的特性
B 属性是所有面向对象语言都提供的特性
C 属性是Qt通过元对象机制提供的特性
D 属性就是成员变量
2. 下列哪个功能不在Qt的基本模块中:__
A Qt GUI
B QML
C Qt SQL
D Qt SVG
答案
1=>C 2=>D
Qt全局定义
Qt的全局定义都在
数据类型
更明确的,平台无关的类型定义
qint8, qint16, qint32, qint64, quint8, quint16….
qreal, qfloat16 见名知义
函数或模板函数
常用的
函数 | 功能 |
---|---|
T qAbs(const T&) | 函数模板,求绝对值 |
bool qFuzzyCompare(double p1, double p2) | 近似相等比较 |
bool qFuzzyIsNull(double) | 约等于0 |
double qInf() | 返回无穷大数 |
bool qIsInf(double) | 是一个无穷大数 |
bool qIsNaN(double) | 不是一个数(无意义) |
int qrand() | 标准rand() 函数的线程安全版 |
void qsrand() | 设种子,线程安全 |
qint64 qRound64(double value) | 最近整数 |
宏定义
常用的
- QT_VERSION, QT_VERSION_STR
版本信息,可用于条件编译
- Q_UNUSED(name)
假装使用一个变量,防止警告信息
forever 无限的死循环
foreach 对容器类的遍历
用于容器的遍历
c++11 已经引入了 for(:) 语法,可以不用这个
- qDebug
显示调试信息,可以通过 Qt_NO_DEBUG_OUTPUT 关闭显示
容器类
Qt 提供了很多模板类型的容器。
比STL的更灵巧、安全、易用。
(效率不作为首要考虑,因为GUI本身不是效率优先的应用)
最大特色是:存储优化、线程安全
也是分为:顺序容器和关联容器。
容器 | 类型 | 用途 |
---|---|---|
QList | 顺序 | 最常用,前后操作快,可下标访问 |
QLinkedList | 顺序 | 链表结构,不支持随机访问 |
QVector | 顺序 | 几乎与QList同,但速度快,连续存储 |
QStack | 顺序 | LIFO push, pop 操作 |
QQueue | 顺序 | FIFO enqueue dequeue 操作 |
QSet | 关联 | 内部用QHash实现,速度快 |
QMap | 关联 | 字典,要求元素能排序 |
QMultiMap | 关联 | 多值便利 |
QHash | 关联 | 字典的Hash实现,要求元素相等比较,以及qHash() 函数 |
QMultiHash | 关联 | 多值便利 |
示例
QList<QString> a;
a.append("123");
a.prepend("abc");
a.insert(1,"xxx");
a[1] = "ttt";
for(auto i: a){
qDebug() << i;
}
QQueue<int> a;
a.enqueue(1);
a.enqueue(2);
a.enqueue(3);
while(!a.isEmpty()){
qDebug() << a.dequeue();
}
实时效果反馈
1. a,b 是浮点数,Qt编程中, 解决避免浮点数精确比较的方案是:__
A fAbs(a-b) < 1e-6
B a === b
C a ~= b
D aFuzzyCompare(a,b)
2. 使用Qt容器,最重要的理由是:__
A 运行速度快
B 语法更易懂易用
C 占用存储空间小
D 线程安全
答案
1=>D 2=>D
Qt容器
Qt 容器从逻辑上看,与STL容器差异不大,但更多考虑了易用性、便利性。
简单示例
QSet<int> a;
a << 1 << 2 << 3;
QSet<int> b;
b << 3 << 4 << 5;
auto c = a + b;
qDebug() << c;
auto d = a - b;
qDebug() << d;
qDebug() << a.contains(2);
对自定义的类型
struct Stu{
Stu(const QString& s, int a){
name = s;
age = a;
}
bool operator==(const Stu& t) const{
return name == t.name && age == t.age;
}
QString name;
int age;
};
QDebug operator<<(QDebug dbg, const Stu& t)
{
dbg << "Stu(" << t.name << "," << t.age << ")";
return dbg;
}
uint qHash(const Stu& stu)
{
return qHash(stu.name) + stu.age;
}
使用:
QSet<Stu> a;
a << Stu("zhang", 15) << Stu("Li", 12);
a << Stu("zhang", 15);
qDebug() << a;
Hash 表判断相等的方法:
- 首先看 qHash(key) 是否相同,若不相同则一定不相等
- 若hash 值相同,则用 operator== 比较
QHash
QSet本身是对QHash的包装。
QHash用于储存键值对 key-value
key 不可以重复,判断重复的标准,如前所述
QHash与QMap的功能基本相同,只是QHash查找更快,且与规模无关。
比较项 | QMap | QHash |
---|---|---|
元素排序吗 | yes | no |
内部实现机制 | 排序树 | hash表 |
查找速度 | 很快 | 更快 |
对key的要求 | 重载 operator< | 重载opearator== 重载uint qHash(const key&) |
注意:
与STL容器特性的区别:
与STL一样, 使用 [ ] 查询时,如果找不到,会添加新的项。
返回值是新value的缺省构造。
如果希望指定一个其它的值,则用:value() 函数,
对多值map返回最新插入的值,要返回所有的对应值,则用 values()
QMultiHash<QString, int> a;
a.insert("zhang", 15);
a.insert("li", 12);
a.insert("zhang", 16);
qDebug() << a.value("zhang");
qDebug() << a.value("wu", -1);
qDebug() << a.values("zhang");
实时效果反馈
1. 在Qt GUI 开发的API中, 出现最多的容器是:__
A QList
B QSet
C QMap
D QHash
2. 对QMultiMap对象,当调用value(key) 时,说法==错误==的是:__
A 如果key存在,则返回最后加入值
B 如果key不存在,返回值类型的缺省构造实例
C 等价于
对象[key]
D 如果key不存在,会加入一新键值对,并返回值
答案
1=>A 2=>C
Qt迭代器
Qt容器提供了两种迭代器
java 型迭代器
好处是:直观,易用,有高级功能
STL型迭代器
好处是:性能优先
本节主要介绍Java型迭代器
容器类 | 只读 迭代器 | 读写迭代器 |
---|---|---|
QList, QQueue | QListIterator | QMutableListIterator |
QLinkedList | QLinkedListIterator | QMutableLinkedIterator |
QVector, QStack | QVectorIterator | QMutableVectorIterator |
QSet | QSetIterator | QMutableSetIterator |
QMap, QMultiMap | QMapIterator | QMutableMapIterator |
QHash, QMultiHash | QHashIterator | QMutableHashIterator |
与 STL 迭代器不同,Java型迭代器指向的位置,不是元素,而是元素间的“缝隙”
这样设计的好处是:
可以支持在遍历中的删除、修改等动作
常见操作:
函数 | 功能 |
---|---|
void toFront() | 回到头 |
void toBack() | 回到尾 |
bool hasNext() | 没有到最后位置则true |
const T& next() | 返回下后一个数据项,并向后移动迭代器 |
const T& peekNext() | 偷看后一项,不移动迭代器 |
bool hasPrevious() | 没有到最前位置,则true |
const T previous() | 返回前一个数据项,并向前移动迭代器 |
const T& peekPrevious() | 偷看前一项,不移动迭代器 |
QList<int> a = {1,2,3,4};
QListIterator<int> i(a);
while(i.hasNext()){
qDebug() << i.next();
}
当然,如果只是看一眼每个元素,也可以用c++11的语法:
QList<int> a = {1,2,3,4};
for(auto i: a){
qDebug() << i;
}
使用java型迭代器的好处之一是:可以边迭代边删除,不会有指针失效的问题。
QList<int> a = {1,2,3,4,5};
QMutableListIterator<int> i(a);
while(i.hasNext()){
if(i.next() % 2 == 0) i.remove();
}
qDebug() << a;
关联型容器的迭代器与此类似,只不过需要:key(), value() 进一步获得分量。
隐式复制
可以看到,Qt GUI编程时,很多API函数返回的类型是:QList, QStringList对象,而按照STL的风格,一般会返回对象的引用,或返回迭代器。
实际上,Qt容器都是隐式复制的,返回对象的开销很小。
隐式复制,也叫“写复制”,也就是说,只有修改容器的时候,复制才真正发生,
而其它时候,只是共享了指针而已。
实时效果反馈
1. 关于Qt容器, 说法==错误==的是:__
A QQueue 是通过 QList 实现的
B QStack 是通过 QVector实现的
C QMultiMap 继承了QMap
D QStringList 等价于 QList
2. Qt容器的java式迭代,主要优点是:__
A 易于使用,比如:可以在遍历中删除元素
B 平台无关性
C 对特定平台,经过更多的优化,性能更好
D 与Qt容器结合更紧密
答案
1=>D 2=>A
QString
与std::string 类似,QString是对串对象概念的实现。
与string相比较,QString提供了大量的方便且实用的函数,着眼点在于便捷、实用性,而不是优先考虑效率问题。
另外,QString也用更方便的方式,解决串的国际化问题。
QString 可以看成是 QChar 的容器。
QChar 是2字节的宽字符。可以直接表示中文的一个字符。即unicode的UTF-16表达。
QString s = "中国abc";
qDebug() << s.length();
for(QChar i: s){
qDebug() << i;
}
qDebug() << s[0].unicode();
可以看到,每个字符,就是它的unicode码。这样对西方字符,浪费了存储空间,但对大字符集,比如中文的处理,就容易多了。
另外,QString 是隐式共享的,因而不要害怕传递QString对象,它在不改写的时候,实际传递的只是共享指针而已。只有改变QString的时候,复制才真正发生。
与std::string, char* 的互转
QString s = "中国abc";
const char* p = s.toStdString().c_str();
qDebug() << p;
qDebug("%2x %2x", uchar(p[0]), uchar(p[1]));
qDebug() << std::strlen(p);
const char* p1 = "中文123";
s = QString::fromUtf8(p1);
qDebug() << s;
串与数值的转换
QString与数值间的转换十分常用,Qt了提供了很多方案,便于使用。
QString s = "123";
qDebug() << s.toInt() << s.toDouble();
int a = 3322;
qDebug() << QString::number(a,16);
s.setNum(a, 2);
qDebug() << s;
qDebug() << QString::asprintf("---%d---", a);
很多函数都有两个版本,一个静态函数,一个QString对象成员函数
串处理的特色函数举例
- QChar
QString s = "abc";
s += "你好";
qDebug() << s[3].unicode();
qDebug() << s.at(5).unicode(); //会引发异常
- empty 与 null
QString s = "";
qDebug() << s.isEmpty();
qDebug() << s.isNull();
s.clear();
qDebug() << s.isNull();
- section
QString s = "/usr/local/bin/app/mytxt";
qDebug() << s.section('/',2);
qDebug() << s.section('/',2,3);
qDebug() << s.section('/',-1);
qDebug() << s.section('/',-2,-2);
- trimmed() 和 simplified()
QString s = " xyz abc kkk ";
qDebug("|%s|", s.trimmed().toStdString().c_str());
qDebug("|%s|", s.simplified().toStdString().c_str());
实时效果反馈
1. 关于QString的内存模型, 说法正确的是:__
A 使用utf-8存储每个QChar
B 使用2字节存储每个QChar的unicode码
C windows下,使用GBK存储
D 使用Qt自定义的格式存储QChar
2. 使用QString的优势,说法==错误==的是:__
A 更方便处理多语言文字
B 提供了更方便、灵活的操控函数
C 与Qt GUI API 结合更自然
D 比std::string 更节约存储空间
答案
1=>B 2=>D
数值输入与显示
- SpinBox
一般用于整数的显示,也可以改变进制,加前缀和后缀等
- QSlider
滑动条,用于直观输入
设置最小值,最大值,步进等
示例:
通过3个滑动条设置红、绿、蓝配色
QColor color;
int r = ui->horizontalSlider_2->value();
int g = ui->horizontalSlider_3->value();
int b = ui->horizontalSlider_4->value();
color.setRgb(r,g,b);
QPalette pall = ui->textEdit->palette();
pall.setColor(QPalette::Base, color);
ui->textEdit->setPalette(pall);
- QProgressBar
用于显示进度,很常用
设置其各种属性
format %p% 百分比,%v%显示值,%m% 显示步数
要控制其显示的外观,不在属性里,Qt提供了更强大、更通用的方案:
样式表
在组作上,右键 | 改变样式表 …
QProgressBar {
border: 2px solid grey;
border-radius: 5px;
background-color: #FFFFFF;
}
QProgressBar::chunk {
background-color: #05B8CC;
width: 20px;
}
QProgressBar {
border: 2px solid grey;
border-radius: 5px;
text-align: center;
}
样式表的语法,与css几乎完全一样。
实时效果反馈
1. 在界面UI设计时,某组件的属性是abc,则设置它的函数名为:__
A set_abc
B setAbc
C SetAbc
D setabc
2. 在设计时改变组件的外观,除了修改属性,还可以:__
A 使用样式表
B 使用外部工具
C 使用布局
D 使用容器
答案
1=>B 2=>A
时间与日期
表示时间、日期的类型:
QTime, QDate, QDateTime
实际上QDateTime只是对QTime、QDate的聚合和封装
需要 include
QDateTime t = QDateTime::currentDateTime();
qDebug() << t.toString("yyyy.MM.dd hh:mm:ss.zzz ddd");
QDateTime t2 = QDateTime::fromString("2020-01-20",
"yyyy-MM-dd");
qDebug() << t2.daysTo(t) << endl;
// 时间戳
uint a = t2.toTime_t();
uint b = t.toTime_t();
qDebug() << (b-a);
int d = (b-a) / (24*60*60);
qDebug() << d;
// 相对日期
QDateTime x = QDateTime::fromString("2000-1-1",
"yyyy-M-d");
x = x.addDays(-5);
qDebug() << x.toString("yyyy-MM-dd");
- 与QString的互转
- 对两个时间点求差
- 与时间戳互转
- 求某一相对时间
currentDateTimeUtc() 返回utc国际标准时
对QDate操作:
QDateTime a = QDateTime::fromString("2000-2-5","yyyy-M-d");
qDebug() << a.date().dayOfWeek();
qDebug() << a.date().dayOfYear();
qDebug() << a.date().daysInMonth();
qDebug() << a.date().daysInYear();
qDebug() << a.date().weekNumber();
qDebug() << "-----";
qDebug() << a.date().year();
qDebug() << a.date().month();
qDebug() << a.date().day();
对QTime的操作:
QTime t1 = QTime::currentTime();
qDebug() << t1.hour();
qDebug() << t1.minute();
qDebug() << t1.second();
QTime t2 = QTime::currentTime();
uint d = t1.msecsTo(t2);
qDebug() << d;
实时效果反馈
1. 下列哪个==不是==QDateTime的成员函数:__
A currentDateTime
B fromString
C toTime_t
D year
2. 若要与其它厂商应用程序交换数据,使用什么类型表达时间更合适:__
A QDateTime
B std::duration
C QTime
D 时间戳uint
答案
1=>D 2=>D
时间日期组件
编辑时间、日期的组件:
QTimeEdit, QDateEdit, QDateTimeEdit, QCalendarWidget
这几组件使用相仿。
我们想要实现的功能是:
在界面上,有一个QDateTimeEdit 组件,可以直接用它来实现日期、时间的输入。
也可以按旁边的按钮,出现一个日历组件,可以更方便地输入日期。
其实现原理是:
先把 calendar组件隐藏起来,点击时,让它显示出来,并移动到恰当的位置。
当选则了某个日期,再让calendar隐藏,把它的选中值放入到QDateTimeEdit组件中。
void Widget::on_pushButton_clicked()
{
int x = ui->dateTimeEdit->geometry().x();
int y = ui->dateTimeEdit->geometry().y();
ui->calendarWidget->move(x,y);
ui->calendarWidget->show();
}
void Widget::on_calendarWidget_selectionChanged()
{
ui->calendarWidget->hide();
QDate t = ui->calendarWidget->selectedDate();
ui->dateTimeEdit->setDate(t);
}
QDateTimeEdit, QDateEdit 的主要属性: displayFormat
格式的写法与 QDateTime中介绍的一样
QDate 另一个重要属性:calendarPopup
如果勾选,会自动关联到日历组件
实时效果反馈
1. 关于QDateTimeEdit和QDateEdit, 说法正确的是:__
A QDateEdit 从 QDateTimeEdit继承
B QDateTimeEdit 从 QDateEdit继承
C QDateTimeEdit 聚合了QDateEdit
D QDateEdit 聚合了QDateTimeEdit
答案
1=>A
定时器
与时间、日期相关的概念,实际上有三个:
时间点(时刻),时间段(时间间隔),定时器
QTimer 提供了重复和单次信号触发的定时器。
与 sleep_for 不同,QTimer的计时在另一个线程,不会阻塞本线程
需要 include
- 默认是重复触发
Widget::Widget(QWidget *parent) :
QWidget(parent),
ui(new Ui::Widget)
{
ui->setupUi(this);
timer = new QTimer(this);
connect(timer, SIGNAL(timeout()),
this, SLOT(my_doing()));
}
void Widget::on_pushButton_clicked()
{
timer->start(1000);
ui->progressBar->setValue(0);
ui->pushButton->setEnabled(false);
}
void Widget::my_doing()
{
QTime t = QTime::currentTime();
ui->lcdNumber->display(t.hour());
ui->lcdNumber_2->display(t.minute());
ui->lcdNumber_3->display(t.second());
ui->progressBar->setValue(ui->progressBar->value()+1);
}
void Widget::on_pushButton_2_clicked()
{
timer->stop();
ui->pushButton->setEnabled(true);
}
- 如果希望触发一次,不用很麻烦,直接用:
QTimer::singleShot(2000, [this]{
ui->progressBar->setValue(100);
});
实时效果反馈
1. 关于QTimer, 说法正确的是:__
A QTimer 等待一段时间,然后执行一个动作,相当于 sleep(间隔) 后执行
B QTimer 的等待在另一个线程中完成,所有操作在本线程中都立即返回
C QTimer 会使得本线程进入阻塞状态
D QTimer对应了操作系统的定时硬件
2. QTimer::singleShot函数的槽函数参数,可以是__
A 已有的槽函数
B 已有的成员函数
C 匿名函数
D 以上都可以
答案
1=>B 2=>D
QComboBox
下拉列表框组件。提供一个下拉列表供用户选择,逻辑上也可以当作一个更方便使用的QLineEdit 组件来使用。
下拉列表的内容可以在设计时编辑,或者通过编码来加入。
可以使用图标,使得表达更丰富多彩些。
在项目中引入资源文件
资源文件中的“资源”指的是不变的常数
比如:图片,图标,语音,多国语言文字的翻译等
可以把资源文件先拷贝到项目的目录,再加入到资源文件中去管理。
在编译连接后,资源文件会被连到exe文件里。
设计时使用资源
QCombobox 组件上,右键 | 编辑项目…
然后按 “属性” | icon | normal on … 从资源中选择
在界面上选择,会触发 activated 信号
无论界面或代码,都会触发 currentIndexChanged 等
使用代码填充
ui->comboBox_2->clear();
ui->comboBox_2->addItem("abc");
ui->comboBox_2->addItem("abc2");
QStringList sl = {"1111", "2222", "333333"};
ui->comboBox_2->addItems(sl);
关联不可见数据
addItem 可以再多给一个参数,QVariant 类型
大致相当于常用类型的一个大union体
我们可以在用户选择了可见的文件后,获得这个关联的数据
QMap<QString, uint> a;
a.insert("张三丰", 1234);
a.insert("李时珍", 1369);
a.insert("克林顿", 8888);
a.insert("王二小", 7777);
for(auto i: a.keys()){
ui->comboBox_3->addItem(i, a.value(i));
}
实时效果反馈
1. 图标是一个小图像, 它最终被存在:__
A 以资源形式被连为exe文件中(用工具可提取出)
B 以静态库形式存在
C 以动态库形式存在
D 被编译为xml数据
2. 如果要在QComboBox中,保存用户自定义数据类型,需要用到:__
A Q_OBJECT 宏
B 继承 QObject 类
C 使用 QVariant 类型
D 使用 union 类型
答案
1=>A 2=>C
QListWidget
Qt有两组用来处理项(item)的组件:
- QListView, QTreeView, QTableView, QColumnView 等
- QListWidget, QTreeWidget, QTableWidget
前一组是基于 Model/View 结构的,作为view与代表数据的Model Data对象联合工作。
后一组是对前一组的减化,从xxxView继承,把数据直接存在某一个项里。
可视化设计
在listWidget 组件上,右键 | 编辑项目,选某个项目,点击“属性”
如果已经在资源中加载了图片,
此处可以为每个项选择相应的图标资源。
用代码来生成项
所有在设计时产生的效果,都可以通过代码来实现。
经常是添加一些新的项:
QIcon icon;
icon.addFile(":/icon/win-icon/c1.ico");
QListWidgetItem* item = new QListWidgetItem("苹果");
item->setIcon(icon);
item->setCheckState(Qt::Unchecked);
item->setFlags(Qt::ItemIsUserCheckable | Qt::ItemIsEnabled);
ui->listWidget->addItem(item);
item = new QListWidgetItem("橘子");
item->setIcon(icon);
item->setCheckState(Qt::Checked);
item->setFlags(Qt::ItemIsUserCheckable | Qt::ItemIsEnabled);
ui->listWidget->addItem(item);
项的删除
删除当前选中的项:
int i = ui->listWidget->currentRow();
auto item = ui->listWidget->takeItem(i);
delete item;
这里需要注意的是:
takeItem 只是在视觉上把 item 移除,但对象并没有释放,
需要手工调用 delete 才能真正删除一个项。
项的遍历
把勾选的项放入到plainTextEdit中。
int n = ui->listWidget->count();
for(int i=0; i<n; i++){
auto it = ui->listWidget->item(i);
if(it->checkState())
ui->plainTextEdit->appendPlainText(it->text());
}
实时效果反馈
1. 关于QListWidget, 说法正确的是:__
A QListWidget 采用 Modle/View 结构
B QListWidget 数据项只能通过代码添加
C QListWidget的数据直接存在某一个项里
D QListWidget项按添加的顺序显示
2. 应该如何通过代码删除QListWidget的某一个项?__
A 调用 QListWidget 的 takeItem,然后手工删除 item
B 调用 QListWidget 的 takeItem
C 调用QListWidget 的 deleteItem
D 调用QListWidget 的 removetem
答案
1=>C 2=>A
QTreeWidget
QTreeWidget 也是一种简化版本,它从QTreeView继承,把数据直接存在项(item)里。
它的节点可分为3种类型:顶层节点,分组节点,末端节点。
设计时建立树
QTreeWidget上 右键 | 编辑项目…
注意,QTreeWidgetItem对象代表一行
每个行可以含有多个列
可以设置一列的标题,并且设置为是否要显示出来
通过代码添加树
ui->treeWidget->clear();
ui->treeWidget->setHeaderHidden(true);
auto it = new QTreeWidgetItem(ui->treeWidget, QStringList("目录"));
new QTreeWidgetItem(it, QStringList("测试-1"));
new QTreeWidgetItem(it, QStringList("测试-2"));
new QTreeWidgetItem(it, QStringList("测试-3"));
new QTreeWidgetItem(ui->treeWidget, QStringList("目录2"));
ui->treeWidget->expandItem(it);
创建QTreeItem时,指定父组件,就可以挂接到组件系统,不需要进一步的显式添加。
增加子节点时,父节点的默认状态是折叠的。
有的时候,需要额外的数据,但并不需要显示出来,
此时,可以关联任意类型的用户数据。
auto it = new QTreeWidgetItem(ui->treeWidget, QStringList("aaa"));
it->setData(0, Qt::UserRole, QVariant(100));
子节点的遍历
QTreeWidgetItemIterator it(ui->treeWidget);
while(*it){
if((*it)->childCount()==0)
ui->plainTextEdit->appendPlainText((*it)->text(0));
++it;
}
实时效果反馈
1. QTreeWidgetItem代表的是:__
A 一行数据
B 一列数据
C 一行中的一列数据
D 中间节点数据
2. 如果希望数据行携带用户自定义类型的信息,但并不显示出来,应该__
A 在行的属性中设为隐藏
B 把可显示列数设为1
C 调用该行的 setData
D 无法实现
答案
1=>A 2=>C
QTableWidget
程序介绍
点击“设置表头”按钮,通过代码设置表头的样式、字体等。
点击“读入数据”按钮,从一个文本文件中读入数据,文本文件中的数据有多行,
每行的数据由tab分隔为多个项。
单击某个项的时候,弹出对话框,显示点击项的文本信息。
QTableWidgetItem
与QTreeWidget类似,此类是 QTreeView的简化版。
与QTreeWidgetItem不同,QTableWidgetItem 表示一个cell,而不是一个数据行。
可以通过 item(行号,列号), 或者 itemAt 来取得某个cell 对象。
需要注意,不管是标题,还是内容区,都是通过 QTreeWidgetItem来表示一个项。
所以,对它们的内容读写、格式设置等都是一样的。
表头的设置
QStringList lst;
lst << "头0" << "头1" << "头2";
ui->tableWidget->setColumnCount(3);
for(int i=0; i<3; i++){
auto it = new QTableWidgetItem(lst[i]);
QFont font = it->font();
font.setBold(true);
font.setPointSize(12);
it->setTextColor(Qt::red);
it->setFont(font);
ui->tableWidget->setHorizontalHeaderItem(i, it);
}
如果仅仅是设置文字信息,并不设置格式,则可以简化:
setHorizotalHeaderLabels
表格内容的设置
这里用了
比起c++标准的IO类,Qt的IO类使用起来,更加简单、方便,主要是与QString, QStringList等的结合更容易。
ui->tableWidget->clearContents();
QFile f("d:/t1.txt");
f.open(QIODevice::ReadOnly | QIODevice::Text);
QTextStream in(&f);
in.setCodec("utf-8");
while(!in.atEnd()){
auto lst = in.readLine().split("\t");
int row = ui->tableWidget->rowCount();
ui->tableWidget->insertRow(row);
for(int i=0; i<3; i++){
ui->tableWidget->setItem(row, i, new QTableWidgetItem(lst[i]));
}
}
需要在指定的位置先生成一个文本文件:t1.txt
内容如下:
0行0列 0行1列 0行2列 1行0列 1行1列 1行2列 2行0列 2行1列 2行2列 3行0列 3行1列 3行2列
注意,请把此文件的编码格式设置为 utf-8格式
读取table中的某个cell
双击cell的槽函数如下:
QString s = ui->tableWidget->item(row, column)->text();
QMessageBox::information(this, "提示", s, QMessageBox::Ok);
需要include
实时效果反馈
1. 关于QTableWideget, 说法正确的是:__
A QTableWidget 使用Model/View 结构,它本身不存储数据
B QTableWidget的表头项与内容项用不同的类型来表示
C QTableWidget在可视化设计时,只能设计表头项,不能设计内容项
D QTableWidget从QTableView继承
2. QTableWidgetItem代表:__
A 表头的一行
B 表中内容的一行
C 表头或表内容的一个cell
D 仅表内容的一个cell
答案
1=>D 2=>C
标准对话框
对话框是弹出的窗体,可以有模态和非模态之分。
Qt为应用 程序提供了一些常用的标准对话框。
对话框类 | 静态函数 | 功能 |
---|---|---|
QFileDialog | QString getOpenFileName() QStringList getOpenFileNames() QString getSaveFileName() QString getExistingDirectory() QUrl getOpenFileUrl() |
选择打开一个文件 选择打开多个文件 选择保存一个文件 选择一个已有的目录 可选择远程网络文件 |
QColorDialog | QColor getColor() | 选择颜色 |
QFontDialog | QFont getFont() | 选择字体 |
QInputDialog | QString getText() int getInt() double getDouble() QString getItem() QString getMultiLineText() |
输入单行文字 输入整数 输入浮点数 从一个下拉列表中选择输入 输入多行字符串 |
QMessageBox | StandardButton information() StandardButton question() StandardButton warning() StandardButton critical() void about() void aboutQt() |
信息提示对话框 询问是否确认 警告提示 错误提示 自定义关于对话框 Qt的关于对话框 |
使用流程基本一致,仅举几个最常用的例子:
打开一个文件
QString afname = QFileDialog::getOpenFileName(this,
"选个文件",
"d:/", //初始位置
"文本(*.txt);;图片(*.jpg *.png);;所有(*.*)");
if(!afname.isEmpty())
ui->plainTextEdit->appendPlainText(afname);
选择颜色
auto plt = ui->plainTextEdit->palette();
QColor acolor = QColorDialog::getColor(
plt.color(QPalette::Text),
this,
"选择颜色");
if(acolor.isValid()){
plt.setColor(QPalette::Text, acolor);
ui->plainTextEdit->setPalette(plt);
}
输入整数
bool ok = false;
int iVal = QInputDialog::getInt(this,
"输入整数",
"请输入字体大小:",
ui->plainTextEdit->font().pointSize(),
6, 40, 1,
&ok);
if(ok){
QFont font = ui->plainTextEdit->font();
font.setPointSize(iVal);
ui->plainTextEdit->setFont(font);
}
从列表选择
QStringList items;
items << "黑龙江" << "吉林" << "辽宁" << "河北" << "山东";
bool ok = false;
QString text = QInputDialog::getItem(this,
"选择项", "请选择省份",
items, 0, true, &ok,
Qt::MSWindowsFixedSizeDialogHint);
if(ok && !text.isEmpty()){
ui->plainTextEdit->appendPlainText(text);
}
输入文本
bool ok = false;
QString text = QInputDialog::getText(this,
"输入串", "请输入串:",
QLineEdit::Password, //::Normal
"1234", &ok,
Qt::MSWindowsFixedSizeDialogHint);
if(ok && !text.isEmpty()){
ui->plainTextEdit->appendPlainText(text);
}
询问确认
auto res = QMessageBox::question(this, "消息", "保存不?",
QMessageBox::Yes |
QMessageBox::No | QMessageBox::Cancel,
QMessageBox::NoButton);
if(res==QMessageBox::Yes)
ui->plainTextEdit->appendPlainText("yes");
if(res==QMessageBox::No)
ui->plainTextEdit->appendPlainText("no");
if(res==QMessageBox::Cancel)
ui->plainTextEdit->appendPlainText("cancel");
实时效果反馈
1. 要弹出Qt的标准对话框, 说法正确的是:__
A 创建对话框类的栈对象,然后调用对象的方法
B 在堆中创建对话框类对象,然后调用其方法
C 调用对话框类的静态方法
D 调用QApplication对象的方法
2. 当需要输入“职称”信息时,用哪个对话框方法较好?__
A QInputDialog::getText
B QInputDialog::getItem
C QMessageBox::question
D QMessageBox::getString
答案
1=>C 2=>B
自定义对话框
自定义的窗体是一个类,可以通过可视化设计。
可以让Qt帮助生成类的头文件和cpp文件的模板。
接下来选 Dialog 无按钮
然后,给定一个类名,系统自动生成配套文件 .h, .cpp, .ui
其中的 .ui 文件,可以进行可视化设计。
自己定义的对话框,可以显示为模态,也可以显示为非模态。
这里介绍模态用法:
直接调用对话框对象的 exec() 方法,
此时,对话框打开,并接收用户的输入,直到用户关闭对话框,
这个函数才执行结束。
换句话说,在对话框关闭之前,exec() 其后的语句并没有获得执行机会。
如果调用show() 方法,则可以弹出两种类型的对话框(模态,非模态),决定于对话框的model属性,这是一个bool值,调用exec()时会被忽略。
一般说来,需要定义与对话框交互的函数
QPoint MyDialog::getPoint() const
{
return QPoint(ui->spinBox->value(),
ui->spinBox_2->value());
}
void MyDialog::init(QPoint pt)
{
ui->spinBox->setValue(pt.x());
ui->spinBox_2->setValue(pt.y());
}
这是用于初始化界面,以及从界面取回数据。
如果是模态对话框,一般在栈上申请空间
MyDialog dlg;
dlg.init(QPoint(30,50));
int ret = dlg.exec();
if(ret==QDialog::Accepted){
QPoint pt = dlg.getPoint();
ui->plainTextEdit->appendPlainText(
QString::asprintf("%d,%d", pt.x(), pt.y()));
}
模态对话框一般用于输入一些信息,
经常是最后,让用户选“确认”或“放弃”按钮,这两个按钮的事件不需要自己写,
QDialog 已经提供了标准的槽函数。关联一下就可以。
可以把”确认”关联到 accept(),把“放弃”关联到 reject()
另外,对话框右上角的关闭按钮,也是关联到了 reject()
它们的行为都是关闭对话框,并使得exec()函数返回。
只是返回的值不同。
实时效果反馈
1. 关于模态与非模态对话框, 说法正确的是:__
A 对话框对象的exec() 方法会弹出非模态对话框
B 对话框对象的show() 方法总是弹出非模态对话框
C 对话框对象的show() 方法既可以弹出模态对话框,也可以弹出非模态
D 自定义的模态与非模态对话框需要继承自不同的类
2. 与自定义对话框相关的3个文件(.h, .cpp, .ui),说法正确的是:__
A 修改了.ui 会引起 .cpp 重编译
B 修改了 .cpp 会引起 .ui 重编译
C 修改了.ui 会引起 .h 重编译
D 修改了.h 会引起 .ui 重编译
答案
1=>C 2=>A
与文件系统接口
标准的选择文件对话框,虽然很方便,但无法深入与之交互。
比如,当点击某个文件的时候,如果是图片文件,在右边显示该图片及图片相关信息。
这是因为,该对话框是什么样子,有哪些功能,在一开始就定下来,
我们无法在它的上面再添加自己的组件。
这就启发我们:
用户界面在需求上很脆弱,容易发生变化,但,
其背后的数据逻辑较为稳定,不易变化。
表达文件系统的树结构,与显示这棵树的界面应该分离。
这样,数据逻辑这部分就可能被重用。
这里的Model对象,只表达数据的逻辑模型,与如何显示它没有关系。
同一个Model 可以配接上不同的视图,甚至可以同时连接多个视图。
如果仅仅是展示出Model中的数据,那么这么模型很简单,也很容易理解。
但,如果同时需要在视图上修改数据,再反映到Model中去,就比较头疼了,
Qt提供的方案是用代理。
我们在这里不深入讨论Model/View的原理
但提供一个具体的例子,看看最简单的Model/View 是如何工作的。
【需求】
- 用一个树形结构展示文件系统
- 当单击一个图片文件时,会显示该文件信息,并把图片显示在预览框中。
【设计】
Qt提供了用于文件系统的现成的Model:QFileSystemModel类
配合一个QTreeView来连接它。
用一个QPlainTextEdit 来显示文字信息
用一个QLabel来显示图片,需要配合 QPixmap(非可视化)
【实现】
- 先在.h文件中添加声明:
private:
Ui::Widget *ui;
QFileSystemModel* model;
QPixmap pix;
model是指针,要等待本对象构造完成后,在 init 过程中申请对象
pix 是简单类,可以直接嵌入对象。
- 还要为点击信号添加槽:
private slots:
void on_pushButton_2_clicked();
void on_pushButton_3_clicked();
void on_treeView_clicked(const QModelIndex &index);
前两个是ui设计时自动添加的,
第3个是手动加入的,注意,故意采用了规范命名法。
注意这里传递的 QModelIndex 类型是用来唯一标识Model中的一个Item
一个Model就是若于Item的集合,每个Item有唯一的标识
具体地说,它包含:行号、列号、父节点指针
- 初始化过程:
Widget::Widget(QWidget *parent) :
QWidget(parent),
ui(new Ui::Widget)
{
ui->setupUi(this);
model = new QFileSystemModel(this);
model->setRootPath("d:/");
QStringList filter;
filter << "*.jpg" << "*.png" << "*.gif";
model->setNameFilters(filter);
model->setNameFilterDisables(false);
ui->treeView->setModel(model);
// connect(ui->treeView, SIGNAL(clicked(QModelIndex)),
// this, SLOT(on_treeView_clicked(QModelIndex)));
}
- 当点击了某个文件:
if(!model->isDir(index)){
ui->plainTextEdit->appendPlainText(model->filePath(index));
ui->plainTextEdit->appendPlainText(model->fileName(index));
ui->plainTextEdit->appendPlainText(model->type(index));
pix.load(model->filePath(index));
ui->label->setPixmap(pix);
}
- 当点击“缩放”
void Widget::on_pushButton_2_clicked()
{
int w = ui->scrollArea->width();
int h = ui->scrollArea->height();
auto pix2 = ui->label->pixmap()->scaled(w,h);
ui->label->setPixmap(pix2);
}
- 当点击“原大小”
void Widget::on_pushButton_3_clicked()
{
ui->label->setPixmap(pix);
}
实时效果反馈
1. 下列,哪个==不是==使用 Model/View 结构的好处:__
A View 比 Model 更容易有需求的变化,分离后,可提高重用
B 一 Model 可以连接多个 View
C 有助于软件的扩展和维护
D 可以提高操作界面的反应速度
2. 关于QModelIndex,说法正确的是:__
A 是一个int型id
B 包含多个信息,能唯一在Model中确定某一个Item
C 是一个指针类型,指向某个Item
D 是QModel的子类型
答案
1=>D 2=>B
文件系统
很多软件都会用到对文件系统的访问。
这包括对文本文件、二进制文件的读写,对文件、目录的管理功能。
这些功能,c++很晚才推出平台无关的标准。
Qt提供的类库也是平台无关的。
最常用到的是:
文本文件的读写
最简单的用法是直接使用QFile类实现读写
读入
QFile f("d:/t1.txt");
if(!f.exists()) return;
if(!f.open(QIODevice::ReadOnly | QIODevice::Text)) return;
QByteArray b = f.readAll();
ui->plainTextEdit->setPlainText(b); //使用了默认的编码方式
QIODevice::Text 为设置把信息作为文本来处理
所谓文本观点,就是行的观点,这对文本文件的换行处理方式的影响。
不同的操作系统上,换行的方式不同,
软件应该尽量兼容这些不同的方式。
注意,readAll 返回的是字节序列,不是串。
字节序列要转为串,就涉及了编码方式问题,处理不当,就会“乱码”
再谈乱码
其实,所有的乱码都是一个原因:
串 ==> 字节序列,或者 字节序列 ==> 串
之一或者全部出错了
QFile f("d:/t1-ansi.txt");
if(!f.exists()) return;
if(!f.open(QIODevice::ReadOnly | QIODevice::Text)) return;
QByteArray by = f.readAll();
ui->plainTextEdit->setPlainText(by);
写出
QFile f("d:/t1-x.txt");
if(!f.open(QIODevice::WriteOnly | QIODevice::Text)) return;
QString s = ui->plainTextEdit->toPlainText();
//QByteArray b = s.toUtf8();
QByteArray b =
QTextCodec::codecForName("gbk")->fromUnicode(s);
// 反向的转换是: toUnicode(QByteArray& ) => QString
f.write(b);
使用QTextStream
QTextStream提供了流式的操作方法,更便捷,易于使用。
并且,QTextStream 不仅可以对QIODevice,还可以对QString,QByteArray 操作,这会获得很大的灵活性。
比如,读入,可以直接指定编码方式,还可以逐行读入。
QFile f("d:/t1.txt");
if(!f.open(QIODevice::ReadOnly | QIODevice::Text)) return;
QTextStream in(&f);
in.setCodec("utf-8");
while(!in.atEnd()){
QString s = in.readLine();
ui->plainTextEdit->appendPlainText(s);
}
对于写出,则还提供了细腻的格式控制
QFile f("d:/t2.txt");
if(!f.open(QIODevice::WriteOnly | QIODevice::Text)) return;
QTextStream out(&f);
out.reset();
//out.setCodec("gbk");
out.setCodec("utf-8");
out.setFieldAlignment(QTextStream::AlignLeft);
out.setPadChar('-');
out.setFieldWidth(10);
out << QString("中国") << "abc" << 100 << "\n";
out.reset();
out << "hello";
注意:QString(“xxxx”) 的用法,如果不这样,会直接操作 const char* ,其编码方式与源码的格式有关,问题就复杂了。
实时效果反馈
1. 中文乱码, 产生的根本原因是:__
A QString 与 std::string不兼容
B QString 与 char* 不兼容
C QString 与 QByteArray 间的转换使用了错误的编码方式
D windows系统与Linux系统的编码方式不兼容
2. 使用QTextStream,而不直接用QFile,主要好处是:__
A 提供了更方便、丰富的功能
B 能够指定编码方式
C 能够类似 cin, cout 操作
D 能提高性能
答案
1=>C 2=>A
文件系统(2)
除了QTextStream,Qt还提供了一个重要的类:QDataStream
QDataStream 提供的是所谓序列化与反序列化的功能
这个主题,也称为对象持久化。
序列化:把对象以某种规则变成字节流
反序列化:从字节流中读出信息,重建对象
Qt自己的很多类,都支持了QDataStream的序列化,我们可以直接使用
比如:最常用的QString, qint32, qreal等
当然,c++的基本类型 int, double 等标准库类型是支持的。
如果为了跨平台使用,最好用明确的类型比如:qint32 来代替 int
注意:
序列化/反序列化 并非是简单地内存的内容直接转移到存储介质上,
如果这么做,就与机器/操作系统相关了。
序列化/反序列化过程要实现可以跨机器体系,跨操作系统的交叉操作。
序列化
QFile f("d:/t1.txt");
if(!f.open(QIODevice::WriteOnly)) return;
QDataStream out(&f);
QString s("I am a 中文 test\n这是第22222行");
qint32 a = 5;
qreal b = 3.1475;
out << s << a << b;
反序列化
QFile f("d:/t1.txt");
if(!f.open(QIODevice::ReadOnly)) return;
QDataStream in(&f);
QString s;
qint32 a;
qreal b;
in >> s >> a >> b;
ui->plainTextEdit->appendPlainText(s);
ui->plainTextEdit->appendPlainText(QString::number(a));
ui->plainTextEdit->
appendPlainText(QString::asprintf("%.2f", b));
自定义类型
我们自己定义的类,也很容易支持QDataStream
这是.h 文件:
class CStudent
{
public:
CStudent();
CStudent(const char* name, qint32 age);
QString show() const;
friend QDataStream& operator<<(
QDataStream& out, const CStudent& stu);
friend QDataStream& operator>>(
QDataStream& in, CStudent& stu);
private:
QString name;
qint32 age;
};
核心思想就是定义友元函数 operator<<,operator>>
这是在 .cpp 中的具体实现:
CStudent::CStudent(const char *name, qint32 age)
{
this->name = name;
this->age = age;
}
QString CStudent::show() const
{
QString s;
QTextStream(&s) << QString("姓名:") << name
<< QString("\n年龄:") << age;
return s;
}
QDataStream& operator<<(QDataStream &out, const CStudent &stu)
{
out << stu.name << stu.age;
return out;
}
QDataStream& operator>>(QDataStream &in, CStudent &stu)
{
in >> stu.name >> stu.age;
return in;
}
有了这个类的基础工作,对CStudent的序列化就和Qt自带的类型没什么区别了。
序列化过程
QMap<int, CStudent> map;
map[111] = CStudent("张三丰", 102);
map[222] = CStudent("李时珍", 98);
QFile f("d:/t2.txt");
if(!f.open(QIODevice::WriteOnly)) return;
QDataStream out(&f);
out << map;
Qt的强大之处,就在于 QMap, QList 等十分重要的基础类型,已经实现了序列化
我们只要很少工作,就能融入到这个体系中。
反序列化过程
QFile f("d:/t2.txt");
if(!f.open(QIODevice::ReadOnly)) return;
QDataStream in(&f);
QMap<int, CStudent> map;
in >> map;
ui->plainTextEdit->appendPlainText(map[222].show());
实时效果反馈
1. 使用QDataStream序列化与反序列化, 说法正确的是:__
A 序列化与反序列化应在同一台机器上进行
B 序列化与反序列化可以在不同机器上,但需要同类型的操作系统
C 序列化与反序列化可以在不同机器,不同操作系统上执行
D 具体过程与Qt的版本和操作系统版本有关
2. 自已定义的类,若要被QDataStream支持,有什么要求?:__
A 必须从QObject继承
B 必须对QDataStream重载出合适的 operator<< operator>>
C 头文件中,必须加入 Q_OBJEC 宏
D 必须include
答案
1=>C 2=>B
文件系统(3)
Qt 还提供了文件和目录操作的一些辅助类。
总结如下:
类名 | 功能 |
---|---|
QCoreApplication | 提取应用程序的路径,文件名等信息 |
QFile | 除读写文件外,还可以复制、删除文件等 |
QFileInfo | 提取文件信息,路径、文件名、后缀、大小、日期等 |
QDir | 获取目录下的文件列表,创建、删除目录、改名等 |
QTemporaryDir, QTemporaryFile | 用于创建临时目录或临时文件 |
QFileSystemWatcher | 监听目录下的文件的添加、删除、修改等 |
示例:取得应用路径信息
QApplication 从 QCoreApplication继承,代表应用程序本身
QCoreApplication还提供了很多静态方法,对程序信息进行访问
QString path = QCoreApplication::applicationDirPath();
QString name = QCoreApplication::applicationFilePath();
QStringList lst = QCoreApplication::libraryPaths();
ui->plainTextEdit->appendPlainText(path);
ui->plainTextEdit->appendPlainText(name);
ui->plainTextEdit->appendPlainText("libs:");
for(QString s: lst)
ui->plainTextEdit->appendPlainText(s);
示例:复制文件
QFile 除了封装文件对象,还可以实现文件的复制、删除、改名等常见功能
而QFileInfo 提供了很细腻的关于文件的信息,比如:
| 函数 | 功能 | | ————————————— | ————————————- | | QString absoluteFilePath() | 带有文件名的绝对路径 | | Qstring absolutePath() | 绝对路径,不带文件名 | | QString fileName() | 去除路径的文件名 | | QString filePath() | 包含路径的文件名 | | QString path() | 路径,不含文件名 | | QString baseName() | 文件基名,第一个’.’之前 | | QString completeBaseName() | 文件基名,最后一个’.’之前 | | QString suffix() | 后缀,最后一个’.’ 之后 | | QString completeSuffix() | 后缀,第一个’.’之后 | | QDateTime created() | 文件的创建时间 | | QDateTime lastModified() | 最后一次修改时间 | | QDateTime lastRead() | 最后一次读取的时间 | | qint64 size() | 文件大小 |
下面的示例,把当前文件备份,精心安排了复制后的文件名字
QFile f("d:/t1.txt");
if(!f.exists()) return;
QFileInfo fi(f);
QString base = fi.completeBaseName();
QString suf = fi.suffix();
QString path = fi.absolutePath();
ui->plainTextEdit->appendPlainText(base);
ui->plainTextEdit->appendPlainText(suf);
ui->plainTextEdit->appendPlainText(path);
for(int i=0; i<10; i++){
QString fname;
QTextStream out(&fname);
out << path << base << ".bak" << i << "." << suf;
ui->plainTextEdit->appendPlainText(fname);
if(!QFile::exists(fname)){
f.copy(fname);
break;
}
}
示例:列出目录下的文件
QDir 提供了这个功能,还有其它的重要信息
ui->plainTextEdit->appendPlainText(QDir::tempPath());
ui->plainTextEdit->appendPlainText(QDir::rootPath());
ui->plainTextEdit->appendPlainText(QDir::homePath());
ui->plainTextEdit->appendPlainText(QDir::currentPath());
QStringList lst = QDir("d:/").entryList();
for(auto s: lst){
ui->plainTextEdit->appendPlainText(s);
}
实时效果反馈
1. 如果希望获得应用程序默认的Lib加载路径, 应该用哪个类:__
A QApplication
B QDir
C QFile
D QFileInfo
2. 如果希望获得文件的最后修改日期,应该用哪个类:__
A QApplication
B QDir
C QFile
D QFileInfo
答案
1=>A 2=>D
绘图
Qt 对绘图提供了强大的支持,这里仅就2D基本绘图作介绍。
与大多图形系统一样,qt也使用相同的API在屏幕和和打印机等不同的设备上实现绘制。
绘制工作总是与三个类有关:
QPainter:
用来执行各种绘制的操作(比如:画矩形,写字,贴图片等)
QPaintDevice:
代表一个抽象的二维空间(不是具体的设备)
QPaintEngine:
提供对不同设备的接口能力,对一般的程序员是隐藏的。
- 基本绘制过程
QPainter painter(this); //this是绘图场地
painter.drawLine(0, 0, 200, 200);
painter.setPen(Qt::red);
painter.drawRect(10, 10, 200, 100);
- 绘制的时机
如果在一般的事件中,直接对屏幕绘制,并不太可取。
因为,多个程序共享屏幕,当某种原因,窗口区域需要重新绘制的时候,我们先前绘制上去的东西就没用了。
我们与其告诉应用“画的过程”,不如想办法让程序记住“画的过程”。
绘图设备的 paintEvent 函数在需要重绘的时候会自动调用。
我们可以重载这个函数,实现“稳定的”对窗口的重绘。
可以让IDE帮助我们写重载形式,避免“笔误”
注意,
函数声明尾上的:Q_DECL_OVERRIDE 宏
它可以根据编译器设置的c++标准版本,动态加入override关键字
- 可以设置是否刷底色等信息
Widget::Widget(QWidget *parent) :
QWidget(parent),
ui(new Ui::Widget)
{
ui->setupUi(this);
setPalette(QPalette(Qt::white));
setAutoFillBackground(true);
}
- 为控制绘制的观,QPainter 几个常用属性
属性 | 类 | 功能 |
---|---|---|
pen | QPen | 控制线条的颜色、宽度、样式等 |
brush | QBrush | 设置填充的颜色、填充样式等 |
font | QFont | 设置字体的大小、样式等属性 |
各种属性的设置细节在Qt 帮助文档中有详细的说明
QPainter 提供了大量的绘图函数
这里举例一些常用的
函数 | 功能 | 备注 |
---|---|---|
drawArc(矩形,起角,跨角) | 画弧线 | 角度单位是1/16度,一周是5760 |
drawConvexPolygon(QPoint数组,点个数) | 凸多边形 | |
drawEllipse(外接矩形) | 画椭圆 | 矩形参数是左上角,右下角 |
drawImage(矩形,图片) | 给定区域内画图片 | |
drawLine(QLine) | 画直线 | |
drawLines(QVector |
画一组直线 | |
drawPoint(QPoint) | 画一个点 | |
drawPoints(QPoint数组,点个数) | 画一批点 | |
drawPolygon(QPoint数组,点个数) | 多边形 | 第一个点和最后一个点连接 |
drawPolyline(QPoint数组,点个数) | 拆线 | 第一个点与最后一个点不连 |
drawRect(QRect) | 画矩形 | |
drawText(矩形,文字) | 画文字 | |
fillRect(矩形,颜色) | 填充矩形 | 无边框 |
实时效果反馈
1. 如果要控制拆线的转角为圆角, 应该设置:__
A QPainter的setStyle
B QPen的setStyle
C QPen的setJoinStyle
D QBrush的setJoinStyle
答案
1=>C
绘图(2)
我们在窗口上绘图的时候,默认的坐标是:
原点在左上角,x轴向右,y轴向下
我们可以通过一些函数的调用改变这个坐标系统:
函数 | 功能 |
---|---|
translate(dx, dy) | 坐标原点的平移操作 |
rotate(角) | 坐标系统顺时针旋转一个角度 |
scale(rx, ry) | 缩放比例 |
shear(sx, sy) | 扭转变换 |
- QPainter状态的保存与恢复
save()
保存当前的状态,也可以理解为当前的状态压入状态栈
restore()
从状态栈中弹出一个状态,就是恢复到刚才的状态
resetTransform()
复位所有的坐标变换到初始状态
【示例】
绘制五星,效果如下图:
还是需要重载 patinEvent,添加如下代码:
QPainter painter(this);
const qreal R = 100;
const qreal PI = 3.14159;
const qreal deg = PI * 72 / 180;
QPoint points[5] = {
QPoint(R, 0),
QPoint(R*std::cos(deg), -R*std::sin(deg)),
QPoint(R*std::cos(2*deg), -R*std::sin(2*deg)),
QPoint(R*std::cos(3*deg), -R*std::sin(3*deg)),
QPoint(R*std::cos(4*deg), -R*std::sin(4*deg)),
};
QFont font;
font.setPointSize(12);
font.setBold(true);
painter.setFont(font);
QPen pen;
pen.setWidth(2);
pen.setColor(Qt::blue);
pen.setStyle(Qt::SolidLine);
pen.setCapStyle(Qt::FlatCap);
pen.setJoinStyle(Qt::BevelJoin);
painter.setPen(pen);
QBrush brush;
brush.setColor(Qt::yellow);
brush.setStyle(Qt::SolidPattern);
painter.setBrush(brush);
QPainterPath path;
path.moveTo(points[0]);
path.lineTo(points[2]);
path.lineTo(points[4]);
path.lineTo(points[1]);
path.lineTo(points[3]);
path.closeSubpath();
path.addText(points[0], font, "0");
path.addText(points[1], font, "1");
path.addText(points[2], font, "2");
path.addText(points[3], font, "3");
path.addText(points[4], font, "4");
painter.save();
painter.translate(100,120);
painter.drawPath(path);
painter.drawText(0,0,"Star1");
painter.restore();
painter.translate(300,120);
painter.scale(0.8,0.8);
painter.drawPath(path);
painter.drawText(0,0,"Star2");
painter.resetTransform();
painter.translate(500,120);
painter.rotate(-145);
painter.drawPath(path);
painter.drawText(0,0,"star3");
实时效果反馈
1. QPainter 的哪个函数, 可以把坐标系统恢复到开始的时候:__
A restore
B resetTransform
C save
D shear
2. QPainter默认的坐标系,哪个说得对?__
A x轴正方向向右,y轴正方向向上
B x轴正方向向右,y轴正方向向下
C (0,0)点在左下角
D (0,0)点在屏幕中心
答案
1=>B 2=>B
绘图(3)
本节给出一个小示例:
按住鼠标移动,在窗体上涂鸦。
重载函数,获得事件的处理机会
void mousePressEvent(QMouseEvent *event);
void mouseReleaseEvent(QMouseEvent *event);
void mouseMoveEvent(QMouseEvent *event);
void enterEvent(QEvent *event);
void leaveEvent(QEvent *event);
这里的QMouseEvent提供了额外的信息
包括:在当前窗口中的位置,鼠标各个键的状态
按住移动时,产生数据
lst.append(QPoint(event->x(), event->y()));
update();
其中的 update, 会触发paintEvent的调用
并非是直接调用paintEvent,而是产生一个重画的请求
在paintEvent中完成绘制过程
QPainter painter(this);
painter.setPen(Qt::red);
painter.setBrush(Qt::red);
for(auto i: lst){
painter.drawEllipse(i.x()-2, i.y()-2, 4, 4);
}
说明:
mouseMoveEvent 并不是只要鼠标一动,就发生事件。
它的工作模式与 mouseTracking 属性有关
update, repaint 都可以触发对paintEvent的调用,但很不同。
update是产生重画的消息到消息泵中,repaint 是直接调用 paintEvent。
update 有重载版本,可以避免大面积重画
实时效果反馈
1. 处理窗体上的鼠标事件, 与之==无关==的可override函数是:__
A enterEvent()
B leaveEvent()
C mouseReleaseEvent()
D paintEvent()
2. 关于 update() 与 repaint(),说法正确的是:__
A 多个update调用,可能只触发一次paintEvent调用
B 在paintEvent中可以调用 repaint
C update 直接调用 repaintEvent
D repaint 调用 update
答案
1=>D 2=>A
图表
Qt 提供了多套绘图体系
QPainter是最基本的一种,它是基于象素的绘图机制,在画布上绘制的内容混合为图像,无法区分。
Qt也有另一种基于GraphicsView架构的机制,绘制的每样东西都是独立的对象
这些对象可以被移动、复制、缩放等许多操作。
Qt Charts 就是基于 GraphicsView架构的
实现完的效果:
ui->setupUi(this);
QChartView* chartView = new QChartView(this);
QChart* chart = new QChart();
chart->setTitle("曲线");
chartView->setChart(chart);
this->setCentralWidget(chartView);
QLineSeries* series0 = new QLineSeries();
QLineSeries* series1 = new QLineSeries();
series0->setName("sin曲线");
series1->setName("cos曲线");
chart->addSeries(series0);
chart->addSeries(series1);
qreal t=0,y1,y2,intv=0.1;
int cnt = 100;
for(int i=0; i<cnt; i++){
y1 = qSin(t);
series0->append(t, y1);
y2 = qCos(t);
series1->append(t, y2);
t += intv;
}
QValueAxis* axisX = new QValueAxis;
axisX->setRange(0, 10);
axisX->setTitleText("time");
QValueAxis* axisY = new QValueAxis;
axisY->setRange(-2,2);
axisY->setTitleText("value");
chart->setAxisX(axisX, series0);
chart->setAxisY(axisY, series0);
chart->setAxisX(axisX, series1);
chart->setAxisY(axisY, series1);
可以帮助文档找到更多的series类型
QBarSeries, QPieSeries,QAreaSeries, 。。。。
实时效果反馈
1. 对象的生存期管理, 说法正确的是:__
A QChartView 管理 QChart
B QChart 管理 QChartView
C QXXSeries 管理 QValueAxis
D QValueAxis 管理 QXXSeries
答案
1=>A
图表(2)
本节以一个接近实用的例子,演示把一组数据通过最常用的柱状图表达出来。
准备
.pro 文件中加入 Qt += charts
.h 中加入:
#include <QtCharts>
#include <QDebug> // 仅仅为了调试方便,可以不要
using namespace QtCharts;
在.h 中加入成员:
private:
Ui::MainWindow *ui;
QChart* chart;
QBarSeries* series;
读入数据到QBarSet
每个QBarSet对象表示一组数据,其数据为double
序列下标为x值,序列值为y值
QBarSet* bs0 = new QBarSet("A");
QBarSet* bs1 = new QBarSet("B");
QBarSet* bs2 = new QBarSet("C");
QBarSet* bs3 = new QBarSet("D");
QFile file("d:/zhutu.txt");
if (!file.open(QIODevice::ReadOnly | QIODevice::Text))
return;
QTextStream in(&file);
while (!in.atEnd()) {
QStringList lst = in.readLine().trimmed().split(" ");
bs0->append(lst[0].toInt());
bs1->append(lst[1].toInt());
bs2->append(lst[2].toInt());
bs3->append(lst[3].toInt());
}
4 个QBarSet 代表4个生产组的产量
QBarSeries
```c++ series = new QBarSeries(); series->append(bs0); series->append(bs1); series->append(bs2); series->append(bs3);
chart = new QChart(); chart->addSeries(series); chart->setTitle(“半年产量”); chart->setAnimationOptions(QChart::SeriesAnimations);
QBarSeries 代表柱图
QChart 图表,其上可以放多个XXXSeries
- QChartView 是一种Widget
它不能直接放在另一个Widget上面,
但可以放在一个Layout上面
```c++
QChartView *chartView = new QChartView(chart);
ui->verticalLayout->addWidget(chartView);
设计时,主窗体已经采用了 Vertical布局
如果不做特殊设置,chart 上是没有显示坐标的。
我们可以自己设置一个定制化的坐标轴:
QStringList x;
x << "3月" << "4月" << "5月" << "6月" << "7月" << "8月";
QBarCategoryAxis *axis = new QBarCategoryAxis();
axis->append(x);
chart->createDefaultAxes();
chart->setAxisX(axis, series);
还可以设置主题:
chart->setTheme(QChart::ChartThemeBlueCerulean);
chart->legend()->setAlignment(Qt::AlignBottom);
实时效果反馈
1. 保存柱状图系列数据的类是:__
A QBarSet
B QBarSeries
C QChart
D QChartView
2. 下列说法,==错误==的是:__
A QChartView 可以放在 Layout组件上
B QChartView是从QWidget间接继承的
C QChartView可以设置为主窗体的CentralWidget
D QChartView可以直接放在某个Widget上
答案
1=>A 2=>D
访问数据库
QtSql模块提供了对数据库访问的支持。
这里指传统的关系型数据库,mysql, ms-sqlserver, oracle, postgresql 等常见数据库
Qt内置了对 sqlite 的完全支持,其它的数据库可能需要相应的 dll 文件。
此处假设已经具备了基本的关系数据库和sql基础知识。
首先,需要在 .pro 文件中加入对sql的支持:
QT += sql
需要访问数据库的地方,include
这样,会引入大量的常用类型,如果希望精确include,也可以分别include 相应的类
QtSql模块中的类,大体上可以分为三个层次:
Qt已经内置了一些常用的驱动
我们可看一下都有哪些驱动:
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
Widget w;
w.show();
QStringList ds = QSqlDatabase::drivers();
for(auto i: ds)
qDebug() << i;
return a.exec();
}
Qt 对Sqlite 数据库的支持最好,所有的功能完全内置。
对postgresql, mysql 支持也很完整
一般有两种方式连接到数据库:
一是直接用该数据库的驱动来连接,
另一个方式是通过 ODBC 驱动。ODBC是微软提供的统一的数据库访问方式。
- QSqlDatabase
代表到某个数据库的连接,以连接Sqlite为例。
Sqlite 相对简单,只单一的文件,运行速度快,文档丰富。
QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE");
db.setDatabaseName("d:/my.db3");
if(!db.open()) return;
ui->plainTextEdit->appendPlainText("打开成功!");
QSqlQuery query;
QString s = "create table stu(id int primary key, name varchar)";
if(!query.exec(s)) return;
ui->plainTextEdit->appendPlainText(s + " 执行成功");
这样会打开已有的数据库,或建立新的数据库。
- QSqlQuery
这个类可用于执行sql语句
.exec(sql语句), 返回结果表示是否成功
QSqlQuery query;
QString s = "create table stu(id int primary key, name varchar)";
if(!query.exec(s)) return;
ui->plainTextEdit->appendPlainText(s + " 执行成功");
可以用同样的方法,添加记录:
QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE");
db.setDatabaseName("d:/my.db3");
if(!db.open()) return;
ui->plainTextEdit->appendPlainText("打开成功!");
QSqlQuery query;
QString s = "insert into stu(id, name) values(1100, '张三丰')";
if(query.exec(s))
ui->plainTextEdit->appendPlainText(s + " 执行成功");
s = "insert into stu(id, name) values(1101, '李时珍')";
if(query.exec(s))
ui->plainTextEdit->appendPlainText(s + " 执行成功");
s = "insert into stu(id, name) values(1105, '克林顿')";
if(query.exec(s))
ui->plainTextEdit->appendPlainText(s + " 执行成功");
除了执行sql命令,也可以执行查询语句,这样可处理结果集:
QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE");
db.setDatabaseName("d:/my.db3");
if(!db.open()) return;
ui->plainTextEdit->appendPlainText("打开成功!");
QSqlQuery query;
query.exec("select * from stu");
while(query.next()){
int id = query.value("id").toInt();
QString name = query.value("name").toString();
ui->plainTextEdit->appendPlainText(QString::number(id)
+ "," + name);
}
实时效果反馈
1. 下面哪个类属于sql接口层:__
A QSqlQueryModel
B QSqlDatabase
C QSqlDriver
D QSqlResult
2. 关于 QSqlQuery 的 exec 方法,说法正确的是__
A 只能执行 sql 命令语句
B 只能执行 sql 的 select 语句
C 只能执行 存储过程
D 可以执行 sql 的命令语句、查询语句、存储过程等
答案
1=>B 2=>D
参数化执行
如果多次执行数据库相关操作,
显然,每次都重新打开数据库,并不高效(虽然sqlite相当快)
可以让QSqlDatabase全局有效。
在.h中:
private:
Ui::MainWindow *ui;
QSqlDatabase db;
然后,在.cpp中对 db 进行初始化。
MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::MainWindow)
{
ui->setupUi(this);
QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE");
db.setDatabaseName("d:/my.db3");
if(db.open())
ui->plainTextEdit->appendPlainText("db打开成功!");
else
ui->plainTextEdit->appendPlainText("db打开失败!");
QSqlQuery q;
q.exec("create table stu(id int primary key, name varchar)");
}
以 insert 语句为例,若要从界面上取得数据,插入表中。
最朴素的办法是拼 sql 串,这是最基本的方式,只要sql语句能描述的事情,都可以实现。
QSqlQuery query;
QString s = "insert into stu(id, name) values(" +
QString::number(ui->spinBox->value()) +
", '" +
ui->lineEdit->text() +
"')";
if(query.exec(s))
ui->plainTextEdit->appendPlainText(s + " insert 执行成功");
else
ui->plainTextEdit->appendPlainText(s + " insert 执行失败");
这个方法至少2个缺点:
- 拼接过程繁琐,容易出错
- 有sql注入攻击的风险
使用参数化的方式,可以在一定程度上避免这些问题
QSqlQuery query;
query.prepare("insert into stu(id, name) values(:id, :name)");
query.bindValue(":id", ui->spinBox->value());
query.bindValue(":name", ui->lineEdit->text());
if(query.exec())
ui->plainTextEdit->appendPlainText(" insert 执行成功");
else
ui->plainTextEdit->appendPlainText(" insert 执行失败");
如果参数个数少,也可以更简单:
QSqlQuery query;
query.prepare("insert into stu(id, name) values(?, ?)");
query.bindValue(0, ui->spinBox->value());
query.bindValue(1, ui->lineEdit->text());
if(query.exec())
ui->plainTextEdit->appendPlainText(" insert 执行成功");
else
ui->plainTextEdit->appendPlainText(" insert 执行失败");
甚至序号也可以省略
QSqlQuery query;
query.prepare("insert into stu(id, name) values(?, ?)");
query.addBindValue(ui->spinBox->value());
query.addBindValue(ui->lineEdit->text());
if(query.exec())
ui->plainTextEdit->appendPlainText(" insert 执行成功");
else
ui->plainTextEdit->appendPlainText(" insert 执行失败");
使用参数化的方式,还有一个好处,当有一大批相同模式的 sql 要执行时,可以提高效率。
QSqlQuery q;
q.prepare("insert into stu(id, name) values(?, ?)");
QVariantList ints;
ints << 2001 << 2002 << 2003;
q.addBindValue(ints);
QVariantList names;
names << "AAAA" << "BBB" << "CC";
q.addBindValue(names);
if(q.execBatch())
ui->plainTextEdit->appendPlainText(" insert 执行成功");
else
ui->plainTextEdit->appendPlainText(" insert 执行失败");
实时效果反馈
1. 关于参数化查询方式, 说法==错误==的是:__
A 参数占位符可以用 ==:参数名== 也可以用 ==?==
B 可以有效防止 sql 注入攻击
C 批量插入的时候,可以提高效率
D prepare时,可以对sql语句进行分析和优化
2. 当使用execBatch执行时,BindValue参数应该是:__
A QVariantList
B QStringList
C QList
D QString
答案
1=>D 2=>A
Meta信息
当连接数据库后,我们可能会希望知道:数据库中哪些表?每个表有哪些字段?每个字段的名字、类型等信息,这些一般称为:meta info
取得所有的表名很容易:
QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE");
db.setDatabaseName("d:/my.db3");
if(!db.open())
qDebug() << db.lastError();
else
qDebug() << db.tables();
表结构信息
当与其它系统接口时,或在调试、诊断等时候,可能会需要知道select 的结果集的结构,
这也是重要的 meta 信息。无论查询的结果集有没有记录,都可以获得这个信息。
因此,如果只想取得结果集的 meta 信息,会故意让结果集选不到记录。
QSqlQuery q("select * from stu where 0");
QSqlRecord r = q.record();
for(int i=0; i<r.count(); i++){
ui->plainTextEdit->appendPlainText(r.fieldName(i));
qDebug() << r.field(i);
}
承载meta信息的是 QField类的实例。它有丰富的函数来获得十分详尽的信息。
QField的成员函数都很简单,基本是见名知意。
.type() 返回的是 QVariant::Type 类型,是一系列的枚举值。Qt支持的类型很丰富。
详细可以参考qt文档。
实时效果反馈
1. 含有字段元信息的类是:__
A QSqlRecord
B QSqlQuery
C QSqlField
D QSqlMeta
答案
1=>C
blob字段
blob字段中存储的是二进制的原始信息,不去解析其中的内容。
在编程时,可表现为 QByteArray,即:字节序列。
blob 字段适用于存储图片、附件、简介等较大的信息。
blob字段内容一般是从文件中读入的。
其工作原理的核心是:QByteArray,即字节序列。
我们只要设法把其它位置的对象等转化为QByteArray,以后事情就很容易。
只要把QByteArray 变成 QVariant,就可以与数据库中的数据交互了。
【示例】
以下示例演示把一个图片存入数据库中,
然后,把它从数据库读出并显示出来。
- 建表
首先,需要在建立表的时候,指定为 blob 类型的字段。
QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE");
db.setDatabaseName("d:/my.db3");
db.open();
QSqlQuery q;
q.exec("create table test(name varchar primary key, data blob)");
qDebug() << db.tables();
- 从文件中读入图片数据
QString afname = QFileDialog::getOpenFileName(this,
"选个图片",
"d:/", //初始位置
"图片(*.jpg *.png);;");
if(!afname.isEmpty()){
QPixmap pix;
pix.load(afname);
ui->label->setPixmap(pix);
}
- insert blob 型
QSqlQuery q;
q.prepare("insert into test(name, data) values(?,?)");
q.addBindValue(ui->lineEdit->text());
QByteArray bytes;
QBuffer buf(&bytes);
buf.open(QIODevice::WriteOnly);
ui->label->pixmap()->save(&buf, "png");
q.addBindValue(bytes);
qDebug() << q.exec();
当然,也可以从文件中直接读出为 QByteArray
可以用 QFile::readAll() ,或者用 QDataStream, QBuffer 等
此处,是为了把不同格式的图片入库后,变成统一的格式。
- 从数据库读出
QSqlQuery q;
q.prepare("select data from test where name=?");
q.addBindValue(ui->lineEdit->text());
if(!q.exec()) ui->label->clear();
if(!q.next()) ui->label->clear();
QPixmap pix;
pix.loadFromData(q.value(0).toByteArray());
ui->label->setPixmap(pix);
读入到QPixmap对象时,并没有指定图片格式,
因为QPixmap 可以尝试猜测它的格式。
实时效果反馈
1. 哪个内容==不适合==用blob型存储:__
A 图片
B 音频
C zip包
D 电子邮件
2. 怎样把QVariant 转为 QByteArray?:__
A QVariant::toByteArray()
B QByteArray 构造方法
C QByteArray静态方法
D 强制转换
答案
1=>D 2=>A
使用事务
使用事务(transaction)可以保证一组相关的动作,要么全部完成,要么原封不动。
大多数的数据库系统都会支持事务。
如要查询本系统是否支持transaction,
可以询问驱动实例,它的类是从 QSqlDriver 继承的。
QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE");
db.setDatabaseName("d:/my.db3");
db.open();
qDebug() << db;
QSqlQuery qu("create table stu(id int primary key, name varchar)");
qDebug() << db.driver()->hasFeature(QSqlDriver::Transactions);
qDebug() << db.driver()->hasFeature(QSqlDriver::BLOB);
事务主要由 3 个动作构成:
开始事务
QSqlDatabase::transaction()
提交(确认)
QSqlDatabase::commit()
回滚(取消自开始事务以来的所有修改)
QSqlDatabase::rollback()
以一组 insert 语句为例,若其中有执行失败的情况,则全部回滚。
QStringList lst;
lst << "insert into stu(id, name) values(2001,'张三丰')"
<< "insert into stu(id, name) values(1001,'李时珍')"
<< "insert into stu(id, name) values(3001,'克林顿')";
QSqlDatabase::database().transaction();
bool ok = true;
QSqlQuery query;
for(auto i: lst){
if(!query.exec(i)){
ok =false;
break;
}
}
if(ok)
QSqlDatabase::database().commit();
else
QSqlDatabase::database().rollback();
在工程实践,此种方式仍有缺陷:
一般,sql语句不是固定的串,而是各种运算后的结果,
如果,在生成这些sql的串中,或别的什么环节出了差错,就会导致部分修改。
事务最后是默认提交的。
这可以通过 异常 捕获来处理:
QStringList lst;
lst << "insert into stu(id, name) values(4001,'张三丰')"
<< "insert into stu(id, name) values(4002,'李时珍')"
<< "insert into stu(id, name) values(1002,'err')";
QSqlDatabase::database().transaction();
try{
QSqlQuery query;
for(auto i: lst){
if(!query.exec(i)) throw -1;
}
QSqlDatabase::database().commit();
}
catch(...){
QSqlDatabase::database().rollback();
}
此外,还要注意:
QSqlDatabase::database() 表示获得当前连接对象
它与 db 并不完全等同。
为了线程安全等考虑,qt 是用连接名来识别不同的连接的。如果没名字,就是默认连接。
可以看下面的实验:
qDebug() << db.tables();
qDebug() << QSqlDatabase::database().tables();
这不一定得到相同结果。
使用:QSqlDatabase::database() 取得默认连接总是可靠的方案。
实时效果反馈
1. 关于数据库的“事务”, 说法正确的是:__
A 事务是“实体完整性”的体现
B 事务是关系型数据库才有的特性
C 使用事务会让数据的操作速度变慢
D 事务都是可嵌套的
2. 怎样取得当前应用程序的默认连接?__
A QSqlDatabase::database()
B QSqlDatabase::drivers()
C QSqlDatabase::connection()
D 保存QSqlDatabase的实例
答案
1=>C 2=>A
QSqlQueryModel
Qt为了简化数据库的操作,同时,也为了尽量不涉及SQL语法,提供了多个model类型,它们可以配合TableView, ListView等使用,大大方便基础应用的开发。
QSqlQueryModel
对QSqlQuery的封装,只读的
QSqlTableModel
从QSqlQueryModel继承,支持修改
QSqlRelationalTableModel
从QSqlTableModel继承,支持外键
建立model
首先,还是要 .pro 中增加 sql,头文件中加入
连接数据库,创建一个stu表,增加一些记录:
db = QSqlDatabase::addDatabase("QSQLITE");
db.setDatabaseName("d:/my.db3");
db.open();
QSqlQuery q;
q.exec("create table stu(id int primary key, name varchar)");
q.exec("insert into stu(id, name) values(1001,'张三丰')");
q.exec("insert into stu(id, name) values(1002,'李时珍')");
q.exec("insert into stu(id, name) values(1003,'克林顿')");
q.exec("insert into stu(id, name) values(2001,'aaaa')");
q.exec("insert into stu(id, name) values(2002,'bbb')");
q.exec("insert into stu(id, name) values(2003,'ccccc')");
且,新增一个成员变量,QSqlQueryModel 指针,初始为: NULL
然后,适当时机创建 model
if(model) return;
model = new QSqlQueryModel(this);
model->setQuery("select * from stu");
model->setHeaderData(0, Qt::Horizontal, tr("学号"));
model->setHeaderData(1, Qt::Horizontal, tr("姓名"));
注意内存管理问题,不要泄漏。
此处还设了列标题
model 与 view 的关联
建立连接很简单,还可以顺便设置一些显示特征
ui->tableView->setModel(model);
这里要注意,如果曾经关联了model,这样会放弃与旧model的关联,
但,旧model对象不会自动 delete
因为一个 model 可能关联到多个视图
ui->tableView->verticalHeader()->setHidden(true);
ui->tableView->setSelectionBehavior(
QAbstractItemView::SelectRows);
ui->tableView->setSelectionMode(
QAbstractItemView::SingleSelection);
ui->tableView->setAlternatingRowColors(true);
取得model中的数据
这里有个关键概念: QModelIndex
model index 并非是QSqlQueryModel中独有的概念,
在 model/view 体系中,任何model都有 model index的概念。
这个 index 是一组数据,用来唯一地确定model中的一个数据项。
它包含的信息主要是 3 个:(行号,列号,父节点号)
对QSqlQuery而言:在整行选中的模式下,最重要的数据就是:行号。
QModelIndex index = ui->tableView->currentIndex();
qDebug() << index.data();
qDebug() << index.row();
qDebug() << model->record(index.row());
实时效果反馈
1. 关于QSqlQueryModel, 说法==不正确==的是:__
A QSqlQueryModel 包装了 QSqlQuery
B QSqlQueryModel 是只读的
C QSqlQueryModel 可以连接多个View
D QSqlQueryModel 从 QSqlTableModel继承
2. QSqlQueryModel 中用于唯一标识它的某个项的类是:__
A QSqlRecord
B QModelIndex
C QSqlField
D QModelID
答案
1=>D 2=>B
QSqlQueryModel(2)
QSqlQueryModel 提供的是一个只读数据集,
但,一般的应用都是有增、删、改、查的功能,只提供“查”是不够的。
实际上,所谓QSqlQueryModel只读,是说它无法自动实现增、删、改的功能,
我们通过手工添加代码,来执行sql语句,当然是可以增删改的。
实现目标
初始状态:tableview 连到 model,可以展现数据
单击某条记录,右边对应展示相应字段的信息。
可以修改组件中值,点击下面的动作按钮,实现增删改的功能。
需要认识一个新的类:
QDataWidgetMapper
首先,需要用一个QDataWidgetMapper对象,从QSqlQueryModel把当前行的数据映射到一个可编辑的组件。
因为这个Model是只读的,所以,映射是单向的。
.h 中定义
QSqlDatabase m_db;
QSqlQueryModel* m_model = nullptr;
QDataWidgetMapper* m_mapper = nullptr;
初始化
//准备数据库
{
m_db = QSqlDatabase::addDatabase("QSQLITE");
m_db.setDatabaseName("d:/my.db3");
m_db.open();
QSqlQuery q;
q.exec("create table stu(id int primary key, name varchar)");
q.exec("insert into stu(id, name) values(1001,'张三丰')");
q.exec("insert into stu(id, name) values(1002,'李时珍')");
q.exec("insert into stu(id, name) values(1003,'克林顿')");
q.exec("insert into stu(id, name) values(2001,'aaaa')");
q.exec("insert into stu(id, name) values(2002,'bbb')");
q.exec("insert into stu(id, name) values(2003,'ccccc')");
}
//建model
{
m_model = new QSqlQueryModel(this);
m_model->setQuery("select * from stu");
m_model->setHeaderData(0, Qt::Horizontal, tr("学号"));
m_model->setHeaderData(1, Qt::Horizontal, tr("姓名"));
}
//关联view
{
ui->tableView->setModel(m_model);
ui->tableView->verticalHeader()->setHidden(true);
ui->tableView->setSelectionBehavior(
QAbstractItemView::SelectRows);
ui->tableView->setSelectionMode(
QAbstractItemView::SingleSelection);
ui->tableView->setAlternatingRowColors(true);
}
// mapper
{
m_mapper = new QDataWidgetMapper(this);
m_mapper->setModel(m_model);
m_mapper->setSubmitPolicy(QDataWidgetMapper::AutoSubmit);
m_mapper->addMapping(ui->lineEdit, 0);
m_mapper->addMapping(ui->lineEdit_2, 1);
}
tableview 点击
实际上是改变了当前行,需要把数据刷到右边。
m_mapper->setCurrentIndex(index.row());
增删改
增加
QSqlQuery q;
q.prepare("insert into stu(id, name) values(?,?)");
q.addBindValue(ui->lineEdit->text().toInt());
q.addBindValue(ui->lineEdit_2->text());
qDebug() << q.exec();
m_model->setQuery("select id, name from stu");
修改
QSqlQuery q;
q.prepare("update stu set name=? where id=?");
q.addBindValue(ui->lineEdit_2->text());
q.addBindValue(ui->lineEdit->text().toInt());
qDebug() << q.exec();
m_model->setQuery("select id, name from stu");
删除
QSqlQuery q;
q.prepare("delete from stu where id=?");
q.addBindValue(ui->lineEdit->text().toInt());
qDebug() << q.exec();
m_model->setQuery("select id, name from stu");
这三段代码在本质上是相同的,
都是构造出一个可执行的sql语句,执行之。
执行后,model 并不知道数据发生了变化,为了反映变化,需要model重新执行查询。
实时效果反馈
1. QDataWidgetMapper的作用, 说法正确的是:__
A 把 model 中的数据显示到 view 中
B 把 model 中的当前行的某项映射到显示或编辑的组件
C 把 model 中的字段信息,映射到相关组件
D 为model提供一个字典
2. 当手工更新数据后,如何实现对QSqlQueryModel的刷新?__
A 调用 model 的 refresh
B 调用 model 的 open
C 调用 model 的 setQuery
D 调用 model 的 invalid
答案
1=>B 2=>C
QSqlTableModel
只读的 QSqlQueryModel 虽然说也可以实现增删改,但比较繁琐,
尤其是对不熟悉sql的开发者。
很多时候,我们的目标只是去维护一个表,并不是复杂的select查询,
此时,如能尽量自动化,就可以提高开发效率,降低开发难度。
QSqlTableModel为此目的而生。它的两个主要目的
- 可以响应界面的编辑动作,就是说可读可写
- 尽量避免直接操纵sql语句
QSqlTableModel 从 QSqlQueryModel 继承过来,关联到一个表,可以把它想象为一个表格,行对应记录,列对应字段。
【示例】
可以直接在tableView上作修改,按”提交”写到数据库,按“取消“则恢复原来的数据。
此外,还提供了过滤和排序的功能。
实现步骤
建立model,关联到tableView
//准备数据库
{
m_db = QSqlDatabase::addDatabase("QSQLITE");
m_db.setDatabaseName("d:/my.db3");
m_db.open();
QSqlQuery q;
q.exec("create table stu(id int primary key, name varchar)");
q.exec("insert into stu(id, name) values(1001,'张三丰')");
q.exec("insert into stu(id, name) values(1002,'李时珍')");
q.exec("insert into stu(id, name) values(1003,'克林顿')");
q.exec("insert into stu(id, name) values(2001,'aaaa')");
q.exec("insert into stu(id, name) values(2002,'bbb')");
q.exec("insert into stu(id, name) values(2003,'ccccc')");
}
//建model
{
m_model = new QSqlTableModel(this);
m_model->setTable("stu");
m_model->setEditStrategy(QSqlTableModel::OnManualSubmit);
m_model->setHeaderData(0, Qt::Horizontal, tr("学号"));
m_model->setHeaderData(1, Qt::Horizontal, tr("姓名"));
m_model->select();
}
//关联view
{
ui->tableView->setModel(m_model);
ui->tableView->setAlternatingRowColors(true);
}
这里比较需要注意的是:
m_model->setEditStrategy(QSqlTableModel::OnManualSubmit)
这是对model设置的编辑策略:
常量 | 含义 |
---|---|
QSqlTableModel::OnManualSubmit | 所有的修改都是缓存的,并不真正更新 等待调submitAll() 或 revertAll() |
QSqlTableModel::OnRowChange | 行变化时自动提交 |
QSqlTableModel::OnFieldChange | Cell变更时自动提交 |
整体提交
如果希望支持完整提交,要用到事务和回滚
m_model->database().transaction();
if (m_model->submitAll()) {
m_model->database().commit();
} else {
m_model->database().rollback();
qDebug() << m_model->lastError();
}
设置过滤
QString s = QString("name like '%1%'")
.arg(ui->lineEdit->text());
m_model->setFilter(s);
其实就是拼凑了sql语句后的 where 条件而已。
此处用了模糊匹配
设置排序规则
m_model->setSort(0, Qt::AscendingOrder);
//m_model->setSort(0, Qt::DescendingOrder);
m_model->select();
删除
m_model->removeRow(ui->tableView->currentIndex().row());
m_model->submitAll();
这里要注意,当前行是view 的概念,不是model的概念
添加
m_model->insertRow( );
这是在最后添加一新行,只是在缓冲中,并没有真的提交。
如果要在第一行前,则:insertRow(0);
实时效果反馈
1. 关于QSqlTableModel, 说法==错误==的是:__
A QSqlTableModel 从 QSqlQueryModel 继承
B QSqlTableModel 不支持复杂的sql 查询
C QSqlTableModel 可以指定排序规则
D QSqlTableModel 只能连接 QTableView
2. 怎样在QSqlTableModel删除一条记录?:__
A 调用它的 removeRow,给定行号
B 调用它的 removeRow,给定主键
C 调用它的 delete
D 执行 “delete from” sql语句
答案
1=>D 2=>A
QComboBox代理
使用简单的 QSqlTableModel,我们很快就会遇到一个问题:
有些字段的值,并非随意填写,而是在一个列表中选择。
这个需求十分普遍。
很希望双击单元格就会出现一个ComboBox下拉组件,提供便利的选择。
比如:性别,院系 等信息。
【目标】
刚开始的时候,使用QSqlTableMoel 和 QTableView 来实现一个基本的应用。
点击 <增加代理>,会在 sex, dep 列设置自己的代理。
这样,这两个列的信息,也可以输入,也可以下拉选择,增加了方便。
【准备】
与前几讲的例子一样,还是要准备好数据库,准备好model,连接到view
ui->setupUi(this);
m_db = QSqlDatabase::addDatabase("QSQLITE");
m_db.setDatabaseName("d:/my1.db3");
m_db.open();
QSqlQuery q;
q.exec("create table stu(id int primary key,"
"name varchar,"
"birth date,"
"sex char(10),"
"dep char(20))");
q.exec("insert into stu(id, name, birth, sex, dep)"
" values(1001, '唐长老', '1789-3-1', '男', '数学系')");
m_model = new QSqlTableModel(this);
m_model->setTable("stu");
m_model->select();
ui->tableView->setModel(m_model);
ui->tableView->setAlternatingRowColors(true);
ui->tableView->setColumnWidth(0,10*10);
ui->tableView->setColumnWidth(3,4*10);
【自定义代理类】
定义代理类需要从QStyledItemDelegate 或者 QAbstractItemDelegate 继承。
对初学者,区别不大。
然后要override 四个 virtual 函数。
.h
class QMyComboDelegate : public QStyledItemDelegate
{
Q_OBJECT
public:
QMyComboDelegate(QObject* parent=0);
// QAbstractItemDelegate interface
public:
QWidget *createEditor(QWidget *parent,
const QStyleOptionViewItem &option,
const QModelIndex &index)
const Q_DECL_OVERRIDE;
void setEditorData(QWidget *editor,
const QModelIndex &index)
const Q_DECL_OVERRIDE;
void setModelData(QWidget *editor,
QAbstractItemModel *model,
const QModelIndex &index)
const Q_DECL_OVERRIDE;
void updateEditorGeometry(QWidget *editor,
const QStyleOptionViewItem &option,
const QModelIndex &index)
const Q_DECL_OVERRIDE;
QStringList lst;
};
.cpp
QMyComboDelegate::QMyComboDelegate(QObject* parent)
:QStyledItemDelegate(parent)
{
}
QWidget *QMyComboDelegate::createEditor(QWidget *parent,
const QStyleOptionViewItem &option,
const QModelIndex &index) const
{
QComboBox* box = new QComboBox(parent);
box->addItems(lst);
box->setEditable(true);
return box;
}
void QMyComboDelegate::setEditorData(QWidget *editor,
const QModelIndex &index) const
{
QString s = index.model()->data(index,
Qt::EditRole).toString();
static_cast<QComboBox*>(editor)->setCurrentText(s);
}
void QMyComboDelegate::setModelData(QWidget *editor,
QAbstractItemModel *model,
const QModelIndex &index) const
{
QString s = static_cast<QComboBox*>(editor)->currentText();
model->setData(index, s, Qt::EditRole);
}
void QMyComboDelegate::updateEditorGeometry(QWidget *editor,
const QStyleOptionViewItem &option,
const QModelIndex &index) const
{
editor->setGeometry(option.rect);
}
【代理类的使用】
代理类写好了,在tableView中设置它就很容易了。
QMyComboDelegate* a = new QMyComboDelegate(this);
a->lst << "男" << "女";
ui->tableView->setItemDelegateForColumn(
m_model->fieldIndex("sex"), a);
QMyComboDelegate* b = new QMyComboDelegate(this);
b->lst << "数学系" << "物理系" << "电机系" << "生物系" << "中文系";
ui->tableView->setItemDelegateForColumn(
m_model->fieldIndex("dep"), b);
实时效果反馈
1. 关于自定义comboBox代理, 说法正确的是:__
A 代理类必须从 QStyledItemDelegate 继承
B 每个不同的列,需要不同的代理类
C 代理类实例弹出窗口的大小必须局限在编辑单元格内
D comboBox代理类可以设置为允许编辑或不允许
2. 关于默认代理类,说法==不正确==的是:__
A QString 类型使用 QLineEdit
B int 类型使用 QSpinBox
C QDate 类型使用 QDateEdit
D double 类型使用的是 QSpinBox
答案
1=>D 2=>C
关系表格模型
QSqlTableModel 加上 comboBox代理,解决了大部分的基本需求。
但其过程仍较为繁冗。
并且,comboBox 列表内容经常是放在一个表中,所谓的“主从表”结构的“从表”。
其实,这才是关系型数据库强大功能之一,即:支持外键连接关系。
看下面的结构:
这其实是两个要求。
如果我们徒手实现,
第一个相对容易,第二个更麻烦一些。
所幸,Qt替我们实现了这些功能。
【目标】
这是用QSqlRelationalTableModel实现打开“主表”的效果,
其中的“姓名”,“科目” 都支持下拉列表选择。
这个功能使用了系统提供的 QSqlRelationalDelegate 类。
【准备】
还是老套路,.pro 加入 sql 支持。
.h 加入相关头文件。
.h 加入定义:
QSqlDatabase m_db;
QSqlRelationalTableModel* m_model;
.cpp 中初始化
ui->setupUi(this);
m_db = QSqlDatabase::addDatabase("QSQLITE");
m_db.setDatabaseName("d:/my2.db3");
m_db.open();
QSqlQuery q;
q.exec("create table course(科目号 int primary key,"
"科目名 varchar,"
"学分 int)");
q.exec("insert into course values(101, '高等数学', 4)");
q.exec("insert into course values(102, '流体力学', 3)");
q.exec("insert into course values(103, '电子线路', 3)");
q.exec("insert into course values(105, '基础化学', 3)");
q.exec("create table stu(学号 int primary key, 姓名 varchar)");
q.exec("insert into stu values(2001, '李时珍')");
q.exec("insert into stu values(2002, '张三丰')");
q.exec("insert into stu values(2003, '克林顿')");
q.exec("create table test(学号 int, 科目号 int, 成绩 double)");
q.exec("insert into test values(2001,101,90)");
q.exec("insert into test values(2001,102,81.5)");
q.exec("insert into test values(2002,101,78)");
q.exec("insert into test values(2002,103,82)");
q.exec("insert into test values(2003,105,93)");
m_model = new QSqlRelationalTableModel(this);
m_model->setEditStrategy(QSqlTableModel::OnManualSubmit);
m_model->setTable("test");
m_model->setRelation(0, QSqlRelation("stu","学号","姓名"));
m_model->setRelation(1, QSqlRelation("course","科目号","科目名"));
m_model->select();
ui->tableView->setModel(m_model);
只这样的设置,已经能在主表中显示从表中对应的名称了。
注意,
此时,并不需要主表、从表间真的有主外键关系。
【自动生成comboBox代理】
这个功能看起来麻烦,实现只一行代码:
ui->tableView->setItemDelegate(
new QSqlRelationalDelegate(ui->tableView));
至于,提交、撤销,与前述无异。
实时效果反馈
1. 关于QSqlRelationalTableModel, 说法正确的是:__
A QSqlRelationalTableModel要求主从表要建立主外键关联
B QSqlRelationalTableModel 间接从QSqlQueryModel 继承
C 该模型是只读模型,不能自动回写数据
D 该模型要求我们自定义代理类,才能实现下拉选择效果。
答案
1=>B
dom读取xml
XML(ExtensibleMarkup Language,可扩展标记语言)
是一种被广泛采用的通用数据交换格式,它很容易被机器识别与解析。
XML是W3C推荐标准。
它是一种文本结构,可供人阅读。以树型结构表达信息。
可用于信息的传递,也可用于信息的存储。
Qt的QtXml模块提供了基本支持。
三种主要的处理方法:
dom
整个文档映射到内存的一棵树,可随机访问
sax
适于大文件的顺序读入、解析。它会产生一些事件,调用用户定义的回调函数。
流
类似于sax,可读入一些token,由用户控制
需要认识几个常用的类
QDomDocument
对于整个xml文档
QDomNode
一个节点。可以是:一段文本,一段注释,一个属性,一个子结构
QDomElement
一个完整的标签及其内部,它是QDomNode的一种类型
以一份菜单为例:
<?xml version="1.0" encoding="UTF-8"?>
<memu>
<伴黄瓜 type="凉菜">
黄瓜,粉皮
</伴黄瓜>
<土豆丝 type="热菜">
土豆
</土豆丝>
<鱼香茄子 type="热菜">
<备注>
多放辣椒
</备注>
茄子
</鱼香茄子>
<地三鲜 type="热菜">
土豆,青椒,茄子
</地三鲜>
<花生米 type="凉菜">
花生
</花生米>
<米饭 type="主食" num="2碗">
大米
</米饭>aa
<蛋花汤 type="汤">
鸡蛋,西红柿
<备注>
别放葱花
</备注>
</蛋花汤>
</menu>
【任务1】
读出所有的热菜,及其主料
QFile file("d:/test.xml");
QDomDocument doc;
doc.setContent(&file);
QDomElement root = doc.documentElement();
QDomNode t = root.firstChild();
while(!t.isNull()){
if(t.isElement()){
QDomElement e = t.toElement();
if(e.attribute("type") == "热菜"){
QString s = e.tagName();
ui->plainTextEdit->appendPlainText(s);
QDomNodeList lst = e.childNodes();
for(int i=0; i<lst.count(); i++){
if(lst.at(i).isText())
s = "\t" + lst.at(i).nodeValue().trimmed();
}
ui->plainTextEdit->appendPlainText(s);
}
}
t = t.nextSibling();
}
不同的节点类型有不同的类来处理,
但,它们都是从 QDomNode继承
为了避免向子类转换的风险,提供了 toXXX 转到子类型。
比如:
toProcessingInstruction(), toElement(),toAttr(),toText()
【任务2】
列出所有带有备注的菜品,及其备注内容。
QFile file("d:/test.xml");
QDomDocument doc;
doc.setContent(&file);
ui->plainTextEdit->appendPlainText("---------------");
QDomNodeList lst = doc.documentElement().childNodes();
for(int i=0; i<lst.count(); i++){
QDomNodeList lst2 = lst.at(i).childNodes();
for(int j=0; j<lst2.count(); j++){
if(lst2.at(j).isElement()){
QDomElement e = lst2.at(j).toElement();
if(e.tagName()=="备注"){
QString s = lst.at(i).toElement().tagName();
s += "---" + e.text();
ui->plainTextEdit->appendPlainText(s);
}
}
}
}
实时效果反馈
1. 相比于sax, 对dom的优点,说法==错误==的是__
A 省内存,适用于很大的文件
B 结构直观,易理解
C 处理时可以随机访问某个节点
D 处理速度快
2. QDomElement 与 QDomNode,关系是:__
A QDomElement 从 QDomNode 继承
B QDomNode 从 QDomElement 继承
C QDomNode 内嵌了 QDomElement 对象
D QDomElement 内嵌了QDomNode 对象
答案
1=>A 2=>A
dom生成xml
与xml的读取相对应,用dom模式生成xml也是类似的:
先通过构造各种对象,在内存中建立一棵 dom 树型结构,
然后,把整个树结构一次性写出到文件中。
【准备】
.pro 文件中追加: Qt += xml
相关位置添加:
【任务1:生成xml】
QFile file("d:/my.xml");
if(!file.open(QIODevice::WriteOnly | QIODevice::Truncate))
return ;
QDomDocument doc;
QDomProcessingInstruction head;
head = doc.createProcessingInstruction("xml",
"version=\"1.0\" encoding=\"UTF-8\"");
doc.appendChild(head);
QDomElement root = doc.createElement(tr("点餐"));
doc.appendChild(root);
QDomElement e1 = doc.createElement(tr("土豆丝"));
e1.setAttribute("type", tr("热菜"));
QDomElement e2 = doc.createElement(tr("备注"));
QDomText t = doc.createTextNode(tr("不放辣椒"));
e2.appendChild(t);
e1.appendChild(e2);
e2 = doc.createElement(tr("主料"));
t = doc.createTextNode(tr("土豆"));
e2.appendChild(t);
e1.appendChild(e2);
root.appendChild(e1);
e1 = doc.createElement(tr("拌黄瓜"));
e1.setAttribute("type", tr("凉菜"));
e2 = doc.createElement(tr("主料"));
t = doc.createTextNode(tr("黄瓜,豆付皮"));
e2.appendChild(t);
e1.appendChild(e2);
root.appendChild(e1);
QTextStream out(&file);
doc.save(out, 4); // 缩进为 4
说明:
tr
这个是便于国际化时对字符串常量处理之用,
此处是用它的“副作用”,明确字符串用什么编码方式
此编码方式可以全局位置设置:
main.cpp 中加上一句:
QTextCodec::setCodecForLocale(
QTextCodec::codecForName("utf8"));
当然,需要include
这是对整个应用设置了默认的编码方案。
这样,在输出时,就不用再去指定编码格式
save
save 时指定的 4, 是子element 缩进量,这样是为了便于阅读,不影响xml主义
(严格地说,两个element间有没有换行、空格等还是有轻微差别的)
【任务2:dom显示到treeWidget】
我们不预定xml的层级,或说树的深度,因而需要递归处理
QDomDocument doc;
QFile file("d:/my.xml");
if (!file.open(QIODevice::ReadOnly)) return;
if (!doc.setContent(&file)) return;
ui->treeWidget->clear();
ui->treeWidget->setHeaderHidden(true);
QDomElement root = doc.documentElement();
QString s = QString("<%1>").arg(root.tagName());
auto it = new QTreeWidgetItem(ui->treeWidget, QStringList(s));
dom_to_tree(it, root);
ui->treeWidget->expandItem(it);
递归函数 dom_to_tree 的参数:
void Widget::dom_to_tree(QTreeWidgetItem*it, QDomElement &e)
其实现如下:
QDomNodeList lst = e.childNodes();
for(int i=0; i<lst.count(); i++){
if(lst.at(i).isText()){
new QTreeWidgetItem(it,
QStringList(lst.at(i).nodeValue()));
}
if(lst.at(i).isElement()){
QDomElement e = lst.at(i).toElement();
QString s = QString("<%1>").arg(e.tagName());
auto t = new QTreeWidgetItem(it, QStringList(s));
dom_to_tree(t, e);
}
}
说明:
生成 QTreeWidgetItem时,用QStringList 包装的原因:
QTreeWidget 的每个项是可以显示多个列的,但我们一般的应用只有一个列。
实时效果反馈
1. 关于dom写出到文件, 说法正确的是:__
A 一边添加Node,一边写出
B 必须先添加前边的 Node, 再添加它后面的 Node
C 可以分别加入开始标签和结束标签
D 先在内存建立 dom 结构,再一次写出
2. 在添加到 treeWidget过程中,为什么会用到递归?__
A 因为dom本身也是树型结构
B 因为树的深度是不确定的
C 因为 dom 用递归处理效率高
D 因为 递归更节省内存
答案
1=>D 2=>B
sax解析xml
dom解析方法,直观易懂。
但,当树的层级较深,或需求复杂时,处理代码还是比较冗长的。
另外,其最大问题是不能支持大规模的xml,因为必须把整个xml装入内存。
sax 解析则是另一种思路。
它的处理接口更简单,是一种基于事件和回调的方式。
这种处理方式不需要把整个xml读入内存,
而是边读,边产生事件。
常用的事件:
标签开始
整个开始标签都解析过了,获得了标签名,属性等
标签结束
读入了一段文本
系统提供了缺省的解析器 QXmlDefaultHandler,对所有事件定义了处理函数
我们可以定义自己的处理器,继承 QXmlDefaultHandler,并覆盖关心的事件处理函数
总之,
使用sax的过程,就是定义自己的解析器的过程。
【准备】
.pro 文件中加入 Qt += xml
相关位置加入头:
my.xml 文件:
<?xml version="1.0" encoding="UTF-8"?>
<点餐>
<土豆丝 type="热菜">
<备注>不放辣椒</备注>
<主料>土豆</主料>
</土豆丝>
<拌黄瓜 type="凉菜">
<主料>黄瓜,豆付皮</主料>
</拌黄瓜>
<aaa type="凉菜">
<备注>不放aaa</备注>
<主料>aaa....</主料>
</aaa>
<bbb type="热菜">
</bbb>
</点餐>
【任务1:显示热菜及其主料】
定义解析器:
#include "mysax1.h"
#include <QDebug>
MySax1::MySax1(QPlainTextEdit* edit)
{
m_edit = edit;
}
bool MySax1::fatalError(const QXmlParseException &exception)
{
qDebug() << exception.message();
return false;
}
bool MySax1::startElement(const QString &namespaceURI, const QString &localName, const QString &qName, const QXmlAttributes &atts)
{
if(atts.value("type")=="热菜"){
m_t1 = qName;
m_edit->appendPlainText(qName);
}
return true;
}
bool MySax1::endElement(const QString &namespaceURI, const QString &localName, const QString &qName)
{
if(qName==m_t1){
m_t1.clear();
return true;
}
if(qName=="主料" && !m_t1.isEmpty())
m_edit->appendPlainText(" -------- " + m_t2);
return true;
}
bool MySax1::characters(const QString &ch)
{
m_t2 = ch;
return true;
}
说明:
sax 是无法自动回溯历史的,
我们自己必须预见到哪些东西将来有可能用到,先用成员存起来。
这里的处理有不严密之处。。。
自定义类的使用:
QFile file("d:/my.xml");
QXmlInputSource inputSource(&file);
QXmlSimpleReader reader;
MySax1 handler(ui->plainTextEdit);
reader.setContentHandler(&handler);
reader.setErrorHandler(&handler);
reader.parse(inputSource);
实践中,我们经常要用到当前标签的外层标签的数据。
对这种需求,用栈来处理最恰当。
【任务2:输出所有的备注信息】
#include "mysax2.h"
MySax2::MySax2(QPlainTextEdit* edit)
{
m_edit = edit;
}
bool MySax2::fatalError(const QXmlParseException &exception)
{
qDebug() << exception.message();
return false;
}
bool MySax2::startElement(const QString &namespaceURI, const QString &localName, const QString &qName, const QXmlAttributes &atts)
{
m_stack.push(qName);
return true;
}
bool MySax2::endElement(const QString &namespaceURI, const QString &localName, const QString &qName)
{
m_stack.pop();
if(qName=="备注"){
QString s = m_stack.top() + " --- " + m_text;
m_edit->appendPlainText(s);
}
return true;
}
bool MySax2::characters(const QString &ch)
{
m_text = ch;
return true;
}
使用方法与上面完全一样。
实时效果反馈
1. 关于sax, 说法正确的是:__
A sax解析基于事件和回调
B sax一般不需要自己定义解析器类
C sax比dom解析需要更多内存
D sax不适用于较大的文件
2. sax解析时,为什么常用到栈结构?__
A 因为sax把所有事件都放入栈中
B 因为当前节点的处理,往往用到包含它的外层节点的信息
C 这样可自动管理内存
D 文件越大,栈就越深
答案
1=>A 2=>B
流解析xml
Qt还为xml解析提供了一种流式的方案。
QXmlStreamReader,QXmlStreamWriter
与sax十分类似,它也是从头到尾读入 xml,并不全部装入内存。
不同的是,它不是通过事件和回调来驱动。
它把读入的内容切分为一个个的 token,通过主调方来控制读取的时机。
这种方式,不需要定义解析器类,也很方便递归处理。
我们用它来完成前边同样的任务。
准备工作还是一样的。
.pro 文件中加入 xml 支持
.h 文件加入
d盘根目录提供 my.xml
【任务1:提取热菜及其备注】
这几乎就是sax方式的一个翻版,
也需要使用一个 QStack 来记录保存祖先标签。
QFile file("d:/my.xml");
if (!file.open(QFile::ReadOnly | QFile::Text)) return;
QXmlStreamReader reader;
reader.setDevice(&file);
struct MyEle{
QString name;
int type;
};
QStack<MyEle> stack;
QString text;
while (!reader.atEnd()) {
auto type = reader.readNext();
if(type == QXmlStreamReader::StartElement) {
MyEle ele{reader.name().toString(), 0};
if(reader.attributes().value("type")=="热菜")
ele.type = 1;
stack.push(ele);
if(ele.type==1)
ui->plainTextEdit->appendPlainText(ele.name);
}
if(type == QXmlStreamReader::EndElement){
stack.pop();
if(reader.name() == "备注"){
if(stack.top().type==1)
ui->plainTextEdit->appendPlainText(
" ---- " + text);
}
}
if(type == QXmlStreamReader::Characters){
text = reader.text().toString();
}
}
if (reader.hasError()) {
qDebug() << "error: " << reader.errorString();
}
如果热菜的名字和备注分行输出,则不必定义这么麻烦的结构,栈中只有一个type就可以了。如果要延期输出热菜的名字,就需要这个结构了。
【任务2:以树结构显示xml】
void Widget::on_pushButton_2_clicked()
{
QFile file("d:/my.xml");
if (!file.open(QFile::ReadOnly | QFile::Text)) return;
QXmlStreamReader reader;
reader.setDevice(&file);
while (!reader.atEnd()) {
auto type = reader.readNext();
if(type==QXmlStreamReader::StartElement) break;
}
QString s = QString("<%1>").arg(reader.name().toString());
auto it = new QTreeWidgetItem(ui->treeWidget,
QStringList(s));
token_into_tree(it, reader);
}
核心是一个递归程序 token_into_tree
void Widget::token_into_tree(QTreeWidgetItem* it, QXmlStreamReader &reader)
{
while (!reader.atEnd()) {
auto type = reader.readNext();
if(type==QXmlStreamReader::StartElement){
QString s = QString("<%1>").arg(reader.name().toString());
auto it2 = new QTreeWidgetItem(it, QStringList(s));
token_into_tree(it2, reader);
}
if(type==QXmlStreamReader::Characters && !reader.isWhitespace()){
QString s = reader.text().toString();
new QTreeWidgetItem(it, QStringList(s));
}
if(type==QXmlStreamReader::EndElement) return;
}
}
实时效果反馈
1. 关于QXmlStreamReader, 说法==错误==的是:__
A 与sax类似,顺序读入xml内容,边读边解析
B 读入的每个token,对应不同的类别
C 对不同的类别,token要转换为不同的类型,然后访问其内容
D 流式处理,适用于递归处理
答案
1=>C
网络编程导引
- 地址
在网络中的通信实体,可以看作参予通信的节点
每个节点有一个唯一的标识,称为主机地址
如果,我们使用IP协议,则为IP地址 + 端口号
每台物理机器,可能有多个IP地址(多块网卡或虚拟的地址,比如装了vmware)。
IP 有外网地址和内网地址(192.168.0.1 是典型的内网地址)
外网地址,全网可见。
内网地址对外不可见,通过可见地址的代理与外网通信。
IP地址有 IPv4, IPv6 两个版本。
目前 IPv4 基本满额
- 分层
不同的层上定义了很多常见的通信协议
tcp, udp 属于低层
http, ftp 属于高层
- 主机名与地址
QHostInfo::localHostName() 返回本机名,就是电脑系统属性里的:完整计算机名。
通过名字可以进一步取得IP地址
QString s = QHostInfo::localHostName();
ui->plainTextEdit->appendPlainText("本机名:" + s);
auto lst = QHostInfo::fromName(s).addresses();
for(auto i: lst){
if(i.protocol()==QAbstractSocket::IPv4Protocol)
ui->plainTextEdit->appendPlainText(
"v4: " + i.toString());
if(i.protocol()==QAbstractSocket::IPv6Protocol)
ui->plainTextEdit->appendPlainText(
"v6: " + i.toString());
}
用fromName() 也可以取得运程主机的ip,但是阻塞方式
- 非阻塞方式取得远程主机IP
QHostInfo::lookupHost("www.163.com",
this, SLOT(lookup_end(QHostInfo)));
这里需要提供一个槽函数
void Widget::lookup_end(const QHostInfo &info)
{
qDebug() << info.addresses();
}
当取得IP地址时,会触发事件,进而触发槽函数的调用
- 本机的网络接口信息
auto lst = QNetworkInterface::allInterfaces();
for(auto i: lst){
qDebug() << "设备: " <<i.name();
qDebug() << "硬件: " <<i.hardwareAddress();
auto lst2 = i.addressEntries();
for(auto j: lst2){
qDebug() << "IP: " << j.ip().toString();
qDebug() << "子网掩码: " << j.netmask().toString();
qDebug() << "广播地址: " << j.broadcast().toString();
}
}
取得本机IP的另一种方法:
void Widget::on_pushButton_4_clicked()
{
QList<QHostAddress> list = QNetworkInterface::allAddresses();
for(QHostAddress address: list){
ui->plainTextEdit->appendPlainText(address.toString());
}
}
实时效果反馈
1. 以下网络协议, 处于最低层的是:__
A HTTP
B FTP
C TCP
D IP
2. 下列哪个调用是异步的:__
A QHostInfo::fromName()
B QHostInfo::lookupHost()
C QNetworkInterface::allInterfaces()
D QNetworkInterface::allAddresses()
答案
1=>D 2=>B
UDP
UDP全称是:User Datagram Protocol
按OSI的7层模型,它处于网络层的上一层—传输层,具体地看,在IP协议的上一层
它传输的对象是:数据包,专业术语:报文
它的特点是:无连接,不可靠
具体说,可能会:
- 丢包
- 错包
- 重包
- 乱序
可以做为其它协议的基础,
可以直接用于要求不高的特定场合:
比如:音频,会议等。。。
由此,交换而来的优势是:快速、简单、无额外的开销
目标地址
发送报文的条件:
- 发送方需要绑定到一个 IP地址 + 端口号
- 目标地址格式:IP地址 + 端口号
这里的端口号,可以是: 1025 — 65535
IP 地址可以外网,内网地址,可以是特殊的:
广播地址,组播地址 使用广播地址,则在同一局域的所有指定端口都能收到
【任务目标】
在同一机器,或不同机器上的两个程序,实现简单报文传递
【实现】
建立Qt gui 项目
.pro 增加 network 支持
适当位置加头文件: #include
【发送】
很简单,直接创建 udp对象
QUdpSocket skt;
QByteArray by = ui->lineEdit->text().toUtf8();
int port = ui->spinBox_2->text().toInt();
skt.writeDatagram(by.data(), by.size(),
QHostAddress::Broadcast, port);
其中的IP地址,用的是广播地址
【接收】
需要一个全程的 udp 对象,负责侦听数据
private:
Ui::Widget *ui;
QUdpSocket m_skt;
自己做个槽函数:
void Widget::udp_data_ok()
{
while(m_skt.hasPendingDatagrams()){
QByteArray by;
by.resize(m_skt.pendingDatagramSize());
m_skt.readDatagram(by.data(), by.size());
ui->plainTextEdit->appendPlainText(QString(by));
}
}
当有数据报就绪,说可以接收了。
初始化的时候,要把 udp 的信号连接到这个槽上。
ui->setupUi(this);
connect(&m_skt, SIGNAL(readyRead()), this, SLOT(udp_data_ok()));
原理很简单。
请注意:
在同一台机器上实验的时候,两个程序要绑不同的端口
qt IDE 环境启动两个程序的方法:
在 工具 | 选项 …. 里边设置一下
stop application before building: 选 none 就可以。
实时效果反馈
1. UDP协议, 属于 OSI 标准模型的哪一层?__
A 应用层
B 会话层
C 网络层
D 传输层
2. 关于 UDP 协议的优点,说法==错误==的是:__
A 简单,没有额外的开销
B 无需先建立连接,快速
C 可以使用广播地址,一发多收
D 适用于发大量的数据
答案
1=>D 2=>D
TCP
TCP 全称: Transmission Control Protocol 传输控制协议
与 UDP 比较:
可靠的
没的UDP的问题(通过自动校验、重试等控制机制)
顺序也是保证的
所以,传输的对象不是报文,而是流
先建连接
可以比照打电话
服务方:侦听(被动等待,相当于被叫用户)
客户方:连接(主动发起请求,相当于主叫用户)
server 可以处理多个 client 的连接请求
适于大量数据
建立连接的消耗比重相对变小了
【示例 server, client tcp 通信】
建立两个应用程序,分别是 server, client 角色
server 侦听, client 请求连接
成功后,server 与 client 自由会话
【准备】
.pro 中加入 Qt += network 支持
.h 中加入: #include
【服务端】
需要一个持久的用于侦听的对象
QTcpServer m_server;
侦听动作:
m_server.close();
int port = ui->spinBox->text().toInt();
qDebug() << m_server.listen(QHostAddress::LocalHost, port);
响应事件:
connect(&m_server, SIGNAL(newConnection()),
this, SLOT(newlink()));
槽函数:
m_skt = m_server.nextPendingConnection();
QString s = QString("对方IP: %1, 端口: %2")
.arg(m_skt->peerAddress().toString())
.arg(m_skt->peerPort());
ui->label->setText(s);
connect(m_skt, SIGNAL(disconnected()),
m_skt, SLOT(deleteLater()));
connect(m_skt, SIGNAL(readyRead()),
this, SLOT(read_data()));
这里的 m_skt 是 QTcpSocket 类型的指针
因为后面通话全靠它,所以要持久存在
QTcpSocket* m_skt;
disconnet 的槽用了系统提供的函数,这里不做也可以,server 析构,最后会释放
真正用 QTcpSocket 的是二个地方
发送:
QString s = ui->lineEdit->text();
QByteArray by = (s + "\n").toUtf8();
m_skt->write(by);
ui->plainTextEdit->appendPlainText("我说: " + s);
接收:
void Widget::read_data()
{
while(m_skt->canReadLine()){
QString s = "他说:" + m_skt->readLine();
ui->plainTextEdit->appendPlainText(s);
}
}
实时效果反馈
1. 关于QTcpServer, 说法正确的是:__
A QTcpServer 在接收了一个连接请求后,无法再接收其它连接请求
B QTcpServer 在接受连接请求后,可以通过自身向对方发数据,和接收数据
C QTcpServer 接受请来后,创建新的 socket 对象与对方连接
D QTcpServer 并不保存新建的 Socket 对象指针,需要程序员自己管理生存期
2. QTcpSocket的connectToHost发起连接请求后,正确说法是:__
A 函数立即返回,当连接成功或失败时,会产生相应的信号
B 函数会一直阻塞,直到连接成功或失败
C 函数立即返回,需要用户在其后等待一定时间后去查询是否成功
D 函数一直不返回,直到对方关闭socket
答案
1=>C 2=>A
聊天服务器
考虑一个电话应用:
如何实现多方通话(类似电话会议)?
方案一:电信提供商开新业务
方案二:一堆电话开免提,放在一个盒子里….
我们实现的 TCP 聊天服务器,类似于方案二
【服务器】
侦听
ui->setupUi(this);
m_server.listen(QHostAddress::LocalHost, 3333);
ui->plainTextEdit->appendPlainText("listen at 3333 .... ");
connect(&m_server, SIGNAL(newConnection()),
this, SLOT(onMyConnect()));
主要代码就是调用 listen,给了固定的端口号。
以后,每当有新的呼入请求,都会触发 onMyConnect 的调用。
接受连接
QTcpSocket* p = m_server.nextPendingConnection();
QString s = QString("接受了:%1:%2")
.arg(p->peerAddress().toString())
.arg(p->peerPort());
ui->plainTextEdit->appendPlainText(s);
connect(p, SIGNAL(readyRead()), this, SLOT(onMyReady()));
每个新建立的 QTcpSocket 对象都作为 Server 的孩子来管理。
我们需要为它们增加一个信号映射,
当任何客户端发来数据的时候,都会触发onMyReady
- 当有数据到来时
void MainWindow::onMyReady()
{
auto lst = m_server.findChildren<QTcpSocket*>();
for(auto i:lst){
if(i->canReadLine()){
QByteArray by = i->readLine();
send_to_all(by);
}
}
}
void MainWindow::send_to_all(QByteArray &data)
{
auto lst = m_server.findChildren<QTcpSocket*>();
for(auto i:lst){
if(i->isOpen()){
i->write(data);
}
}
}
QTcpServer 下可能还保管着其它的对象,
所以,我们为了找到所有的 QTcpSocket 类型的对象,可以用 findChildren
这是一个模板函数,我们调用它的时候,需要给出具化的类型
【客户端】
可以使用前边课程中开发的客户端程序,基本不变。
微调的地方:
因为自己发的消息,自己也会收到,所以发送时,不再输出。
因为多个人通话,须要知道消息是谁发来的,因为每个消息都要加发送方的地址。
这个功能最好在服务器端完成
在发送给全体之前,把对方地址顺便写入:
if(i->canReadLine()){
QByteArray by = i->readLine();
QString s = i->peerAddress().toString()
+ ":" + QString::number(i->peerPort())
+ "说:";
by.insert(0, s);
send_to_all(by);
}
实时效果反馈
1. 关于QTcpServer的children, 说法正确的是:__
A 这个功能是 QTcpServer 类特有的
B 这些children的类型一定是 QTcpSocket
C 这是 QObject 提供的功能
D 这是 QWidget 提供的功能
答案
1=>C
大文件传输
TCP的传输是可靠的,但传输较大的文件,还是要考虑很多问题的。
如果使用同步的方式,就很容易,传输过程一直持续,直到传输完成。
但,这无法适应GUI应用程序的需要,
我们不应该在一个函数中做耗时的操作,而使整个界面响应陷入停顿。
况且,假如我们需要在传输的中间放弃任务,又如何处理?
本节讲的内容是:
通过异步的方式,实现大文件的传输。
其思路是:
在一个函数中只传输一小部分内容,
然后,等待传输完成,完成后再传输下一小部分。
QTcpSocket 的设计充分考虑了异步传输的问题。
skt.write(字节数组) 并不会等到传输完成才返回,而是几乎立即返回。
当传输完成时,会产生相应该的信号,我们可以映射这个信号,来做后续的处理。
【传输约定】
为了方便,我们约定,被传输的文件是:d:/u5_in.zip
接收方接收后,生成的文件是:d:/u5_out.zip
客户端连接到服务器后,立即传输数据
数据流的格式是:数据文件长度(int64) + 文件内容本身
【服务器端】
侦听在 5555 端口
m_server.listen(QHostAddress::LocalHost, 5555);
ui->plainTextEdit->appendPlainText("等待 at 5555");
映射槽函数
ui->setupUi(this);
connect(&m_server, SIGNAL(newConnection()),
this, SLOT(onMyConnect()));
当连接到来时:
m_skt = m_server.nextPendingConnection();
m_server.close(); // 只接受一个客户的连接请求
connect(m_skt, SIGNAL(readyRead()),
this, SLOT(onMyReadyRead()));
m_total = 0; // 文件的总长度
m_done = 0; // 已经写入文件的字节数
QString s = m_skt->peerAddress().toString()
+ ":" + QString::number(m_skt->peerPort());
ui->plainTextEdit->appendPlainText("已连接到:" + s);
当有数据到来 时:
void Widget::onMyReadyRead()
{
if(m_total == 0){
if(m_skt->bytesAvailable()<sizeof(qint64)) return;
QDataStream in(m_skt);
in >> m_total;
m_file.open(QFile::WriteOnly);
}
QByteArray by = m_skt->readAll();
m_file.write(by);
m_done += by.size();
if(m_done == m_total){
m_file.close();
m_skt->close();
ui->plainTextEdit->appendPlainText("接收文件结束");
}
}
在读入文件长度时,不用 m_skt 直接读,而是用 QDataStream,是为了跨平台的无关性。实际应用中,很可能服务器与客户端不是同样的系统环境。
【客户端】
界面上加一个progressBar 用来显示传输的进度
先请求服务器
m_skt.connectToHost("127.0.0.1", 5555);
需要映射信号:
connect(&m_skt, SIGNAL(connected()),
this, SLOT(onMyConnect()));
connect(&m_skt, SIGNAL(bytesWritten(qint64)),
this, SLOT(onMyBytesWritten(qint64)));
当连接成功时:
ui->plainTextEdit->appendPlainText("连接成功");
m_file.open(QFile::ReadOnly);
qint64 n = m_file.size();
QDataStream out(&m_skt);
out << n;
m_total = n; //文件大小
m_done = 0; // 已经传输的大小
ui->progressBar->setMaximum(m_total);
当写入成功时:
if(!m_file.isOpen()){
ui->plainTextEdit->appendPlainText("发送结束");
return;
}
QByteArray by = m_file.read(1024);
if(by.size() < 1024)
m_file.close();
if(by.size()>0){
m_skt.write(by);
m_done += by.size();
}
ui->progressBar->setValue(m_done);
实时效果反馈
1. 关于QTcpSocket.write(), 说法正确的是:__
A 一直阻塞,直到所有数据都正确发出
B 一直阻塞,直到对方读取了数据
C 不阻塞,送到发送缓冲区就返回了
D 不阻塞,新开一个线程,在后台执行
2. 使用QDataStream,不直接用QTcpSockt读写最主要好处是?:__
A 有缓冲,更快
B 更统一的二进制格式,跨平台是一致的
C 更友好的API
D 操作更容易
答案
1=>C 2=>B
高层协议
建立在 TCP 上层的协议很多,一般统称为高层
我们熟知是,比如: HTTP, FTP 等
它们的任务焦点,不是传输字节流,而是更具“语义”的一些任务。
比如,通过HTTP协议,获取远程服务器上的文件,即:http 下载
Qt的新版中,主要用如下类处理
类 | 用途 |
---|---|
QNetworkRequest | 封装 网络请求 |
QNetworkAccessManager | 负责调度、协调网络调用 |
QNetworkReply | 监视服务器的应答 |
需要注意的是:
如果使用了 https:// 请求头,即需要进行 SSL 认证,信息加密传输
SSL 过程也可以自动化,但需要的两个 dll 文件可能找不到,我们需要手工拷贝一下。
从tools 目录,拷贝到 Qt 编译的主目录下:
【示例功能】
在编辑框中,写入下载地址,此处用:
https://static.red-lang.org/dl/win/red-064.exe
这是 red 语言的一个下载地址
点击下载按钮,默认下载为 d:/u6.down,下载后自己改名为 exe
【实现】
先在 .pro 中添加 Qt += network
在适当位置加入头
需要的持久生命期的对象:
QFile m_file; // 存下载的文件
QNetworkAccessManager m_manager; // 管理下载过程
QNetworkReply* m_reply; // 监视应答信息
当点击“下载”按钮时,
ui->pushButton->setEnabled(false); // 防连击
QString s = ui->lineEdit->text().trimmed();
QUrl url = QUrl::fromUserInput(s);
if(!url.isValid()){
qDebug() << url.errorString();
return;
}
m_file.open(QIODevice::WriteOnly);
m_reply = m_manager.get(QNetworkRequest(url));
connect(m_reply, SIGNAL(finished()),
this, SLOT(onMyFinish()));
connect(m_reply, SIGNAL(readyRead()),
this, SLOT(onMyRead()));
connect(m_reply, SIGNAL(downloadProgress(qint64,qint64)),
this, SLOT(onMyProgress(qint64,qint64)));
当发来数据时,当传输结束时,还有专门的进度信号
void Widget::onMyFinish()
{
m_file.close();
}
void Widget::onMyRead()
{
m_file.write(m_reply->readAll());
}
void Widget::onMyProgress(qint64 a, qint64 b)
{
ui->progressBar->setMaximum(b);
ui->progressBar->setValue(a);
}
因为 QNetworkReply 对信号进行了很好安排,我们使用的时候就很容易了。
使用上面的这些类,也可以进行其它高层协议的通信。
比如,FTP,步骤十分类似。
注意,在老的 Qt 版本中提供了 QHttp 类,但已被废弃。
实时效果反馈
1. 关于HTTP协议, 说法正确的是:__
A HTTP 比 TCP 位于更高层
B HTTP 与 TCP 是同一层的协议
C HTTP 比 FTP 位于更高层
D HTTP 协议需要先和对方握手,再发请求
2. 关于QNetworkAccessManager的用途,说法正确的是:__
A 用于管理 QNetworkRequest
B 用于管理 QNetworkReply
C 用于管理 身份验证
D 以上都包括
答案
1=>A 2=>D
多媒体
Qt5.9 为多媒体应用提供了大量的支持类,使得开发基于 GUI 的多媒体应用变得很容易。
这些类涉及音频,视频,录音,录像,压缩与解码,摄像头,甚至是数字广播收音。
想实现的功能场景 | 可能用到的主要类 |
---|---|
播放压缩音频 | QMediaPlayer, QMediaPlayList |
播放音效文件 | QSoundEffect,QSound |
播放低延迟音频 | QAudioOutput |
播放原始音频输入数据 | QAudioInput |
录制编码音频数据 | QAudioRecorder |
发现音频设备 | QAudioDeviceInfo |
视频播放 | QMediaPlayer,QVideoWidget,QGraphicsVideoItem |
视频的处理 | QMediaPlayer, QVideoFrame, QAbstractVideoSurface |
摄像头,取景框 | QCamera, QVideoWidget, QGraphicsVideoItem |
取景框预览处理 | QCamera,QAbstractVideoSurface,QVideoFrame |
摄像头拍照 | QCamera,QCameraIamgeCapture |
摄像头录像 | QCamera,QMediaRecorder |
收听数字广播 | QRadioTuner,QRadioData |
此处以简单的音频播放为例,了解一下其编程模式。
【任务目标】
开发一个简单的音频播放工具。
可以选歌曲,可以显示或控制播放进度,调音量等。。。
【准备】
.pro 文件中加入 Qt += multimedia
如果需要加入视频支持,还需要 multimediawidgets
此例需要的类是:
QMediaPlayer 主类,可以播放压缩格式的音频或视频文件。
QMediaPlayList 管理播放列表,可以循环播、单曲、乱序等
需要include
【实现】
点击“播放”按钮时,
QString filter = "音频文件(*.mp3 *.wav *.wma);;";
QString fname = QFileDialog::getOpenFileName(this,"",
"d:/", filter);
m_lst.clear();
m_lst.addMedia(QUrl::fromLocalFile(fname));
m_lst.setCurrentIndex(0);
m_player.play();
m_player.setVolume(50);
已经关联的槽函数:
ui->setupUi(this);
m_player.setPlaylist(&m_lst);
connect(&m_player, SIGNAL(positionChanged(qint64)),
this, SLOT(onMyPosition(qint64)));
connect(&m_player, SIGNAL(durationChanged(qint64)),
this, SLOT(onMyDuration(qint64)));
position 的变化在播放进行时产生,给出参数是距离开始的毫秒数
duration 的变化一般是切换不同的歌曲时发生,参数是曲目的总时长,单位是毫秒
当换曲时,
void Widget::onMyDuration(qint64 a)
{
ui->horizontalSlider->setMaximum(a);
ui->label_2->setText(QString::number(a/1000));
}
当进度变化时,
void Widget::onMyPosition(qint64 pos)
{
if(ui->horizontalSlider->isSliderDown()) return;
ui->horizontalSlider->setValue(pos);
ui->9->setText(QString::number(pos/1000));
}
一个细节是,当用户正在拖动滚动条的时候,不要修改 value
其它的信号响应也很简单:
void Widget::on_horizontalSlider_valueChanged(int value)
{
m_player.setPosition(value);
}
void Widget::on_pushButton_2_clicked()
{
m_player.pause();
}
void Widget::on_pushButton_3_clicked()
{
m_player.play();
}
void Widget::on_pushButton_4_clicked()
{
m_player.stop();
}
void Widget::on_horizontalSlider_2_valueChanged(int value)
{
m_player.setVolume(value);
}
实时效果反馈
1. 以下哪个==不在== Qt 多媒体标准包支持之列:__
A 音频、视频播放
B 录像,截图
C 数字广播调谐和收听
D 虚拟现实
2. 对QMediaPlayer,当播放进度变化时,会产生什么信号?:__
A positionChanged(qint64)
B durationChanged(qint64)
C valueChanged(qint64)
D indexChanged(qint64)
答案
1=>D 2=>A