信息学竞赛 CPP 应用篇

线程与进程

image-20220331113422795

  • 进程

    通常情况下,一个可执行程序在启动后,就是一个进程。

    它拥有独立的可支配的资源,比如:内存。

    32 位程序有独立的4G虚拟内存(由操作系统提供)

    我们所说的内存地址就是指的:虚拟内存地址。

    优点 操作系统对进程资源进行了很多保护,比如:两个进程的虚拟内存不会映射在同一物理地址上。

    缺点 进程通信很慢,很麻烦。

  • 线程

    在同一进程中,可以同时有多个指令序列在执行。

    它们共享同一虚拟内存,及其它(如:文件句柄,socket等)进程资源

    但有自己的:工作栈,局部变量,执行断点等

    可以把线程看成是轻量级的进程。

    优点

    创建线程比进程的成本要小得多,所以快速、高效,可以创建大量的线程。

    多个线程共享内存,可以直接传递指针,很快速、方便。

    缺点

    没有保护机制,一个线程出错,可能所有线程都会挂掉。

    对共享数据的访问存在竞争问题,使用锁可能会引起死锁。

c++11 之前的多线程

虽然操作系统大都支持多线程技术,但标准上并不统一。

c++一直都没有一个线程的标准使用方法,而是要调用本地系统的API

在unix/liux 下,POSIX规范,需要include

pthread_create(thread, attr, start_routine, arg)

参数分别:标识符指针,属性对象,运行函数地址,传入的参数

线程的显式退出调用 pthread_exit

windows 下则是另一种景象。

需要 include

  1. CreateThread(属性,初始栈,回调函数, 线程参数,控制参数,传出参数);

如果创建成功,则返回句柄,否则 NULL

c++11 线程

c++11 终于把多线程作为标准库的特性。

需要先包含头文件:

  1. void f()
  2. {
  3. for(int i=0; i<10; i++){
  4. cout << "thread: " << i << endl;
  5. }
  6. }
  7. int main()
  8. {
  9. thread td(f);
  10. for(int i=0; i<10; i++){
  11. cout << "main...." << i << endl;
  12. }
  13. return 0;
  14. }

通过 thread 类来创建线程对象,参数为一函数,其内容为线程执行的内容。

td 对象掌管着线程的控制权。

此后,新线程与main线程同时执行,共享本进程的资源

更进一步地,竞争着本进程的排它资源(比如:控制台输出)。

在程序员的角度来看,我们只能说,两个线程是并发的。并不知道是否并行

如果只有一个cpu,则通过系统调度,让一个线程执行一小段时间,切换到另一个线程,再执行一小段时间,再切换。。。

如果有多个cpu,可能真的是同时在执行。

无论如何,都存在着如何正确有次序地共享资源的问题。

c++11 sleep

为了观察清楚,我们可能会希望程序执行中能放慢脚步,让我们看清楚交叉的过程。

这个简单的需求也是个问题。因为c++11之前,没有统一的标准,让程序等待一会儿。

各种不同的sleep,五花八门。如果希望平台无关,看来只好自己循环了。。。

提供的是: Sleep(秒);

linux的 提供的是: sleep(秒); usleep(微秒);

c++11 采用了 boost 中的方案:

std::this_thread::sleep_for 休眠一个时间段

std::this_thread::sleep_untill 休眠到一个时间节点

时间间隔的单位有预定义好的:

  1. #include <chrono>
  2. /*
  3. std::chrono::nanoseconds
  4. std::chrono::microseconds
  5. std::chrono::milliseconds
  6. std::chrono::seconds
  7. std::chrono::minutes
  8. std::chrono::hours
  9. */
  10. // 休眠 5 秒钟:
  11. this_thread::sleep_for(chrono::seconds(5));

原来的多线程程序可以修改为:

  1. void f()
  2. {
  3. for(int i=0; i<10; i++){
  4. this_thread::sleep_for(chrono::seconds(1));
  5. cout << "thread: " << i << endl;
  6. }
  7. }
  8. int main()
  9. {
  10. thread td(f);
  11. for(int i=0; i<10; i++){
  12. this_thread::sleep_for(chrono::seconds(1));
  13. cout << "main...." << i << endl;
  14. }
  15. return 0;
  16. }

等待到某一时刻:

  1. cout << "begin" << endl;
  2. chrono::system_clock::time_point p =
  3. chrono::system_clock::now() +
  4. chrono::seconds(5);
  5. this_thread::sleep_until(p);
  6. cout << "end" << endl;

计算程序执行的时间间隔

  1. auto p1 = chrono::high_resolution_clock::now();
  2. for(int i=0; i<100; i++){
  3. cout << i << endl;
  4. }
  5. auto p2 = chrono::high_resolution_clock::now();
  6. chrono::duration<double, std::milli> ela = p2 - p1;
  7. cout << "use: " << ela.count() << " ms" << endl;
  8. return 0;

实时效果反馈

1. 关于进程与线程, 说法正确的是:__

  • A 进程中至少包含一个线程,即主线程

  • B 多个进程间通信,可以通过传递指针的方式

  • C 同一进程的多个线程共用进程的栈

  • D 一个线程崩溃不会影响到其它线程

2. 使用 sleep 而不是空循环等待的主要好处是?__

  • A 等待的时间更准确

  • B 代码更简洁

  • C 更省资源,线程sleep期间不占用 cpu 资源

  • D 兼容性更好

答案

1=>A 2=>C

多线程

image-20220401110155944

在线程启动以后,就和主线程并列地运行了。

这时,主线中的线程对象是局部变量,当超出作用域后,会调用terminiate从而引发异常。

有两个选择:

  • join

    一定要在被调线程结束前调用这个函数,它会使主线程直接进入阻塞模式。一直等到被join的线程执行结束,才会被唤醒。

    这个效果上看,与直接调用线程函数是一样的。

  • detach

    还可以让出新线程的控制权,让它自生自灭。

    detach就果让线程对象与它实际关联的线程脱钩,把控制权转交给c++的运行库。

    这有点类似 unix 守护进程的概念。

    脱钩后,线程对象就变成一个空壳,不能再通过它控制线程或与新线程通信了。

    不管有没有 detach,主线程结束后,要回收所有资源,所有线程也会强制结束。

    1. void f()
    2. {
    3. for(int i=0; i<5; i++){
    4. this_thread::sleep_for(chrono::seconds(1));
    5. cout << "thread: " << i << endl;
    6. }
    7. }
    8. int main()
    9. {
    10. thread td(f);
    11. td.detach();
    12. this_thread::sleep_for(chrono::seconds(3));
    13. cout << "main end" << endl;
    14. return 0;
    15. }

线程函数的多种形式

  • 普通函数

  • 函数对象

  • 匿名函数
  • 成员函数
  1. void f()
  2. {
  3. cout << "void f() .." << endl;
  4. }
  5. struct F{
  6. F(int a):x(a){}
  7. void operator()(){
  8. cout << "F::operator() " << x << endl;
  9. }
  10. void f(){
  11. cout << "member f " << x << endl;
  12. }
  13. int x;
  14. };
  15. int main()
  16. {
  17. thread t1(f);
  18. thread t2(F(20));
  19. thread t3([](){cout << "noname func" << endl; });
  20. F a(10);
  21. thread t4(F::f, &a); // 相当于有参的函数
  22. t1.join();
  23. t2.join();
  24. t3.join();
  25. t4.join();
  26. return 0;
  27. }

this_thread

get_id() 获得当前进程的ID

yield() 放弃本线程本轮的时间片,让给其它线程执行。

  1. thread([](){
  2. cout << this_thread::get_id() << endl;
  3. }).join();
  4. cout << this_thread::get_id() << endl;

传参问题

线程函数可以有参数,个数不限,返回值如果有,会被忽略。

当传递指针的时候,会共享变量,这时要注意,当线程还在运行的时候,要保证共享的变量不会被销毁。

引用的传递有点不同,如果不加处理,编译器无法判断我们是想传引用,还是想传值。

可以这样处理:

  1. void f(int& x){
  2. this_thread::sleep_for(chrono::seconds(1));
  3. cout << x << endl;
  4. }
  5. int main()
  6. {
  7. int x = 5;
  8. thread td(f, std::ref(x));
  9. x = 20;
  10. td.join();
  11. return 0;
  12. }

同一个函数,可以创建出多个线程

  1. void f(int x){
  2. this_thread::sleep_for(chrono::seconds(1));
  3. cout << x << endl;
  4. }
  5. int main()
  6. {
  7. thread t1(f, 10);
  8. thread t2(f, 20);
  9. t1.join();
  10. t2.join();
  11. return 0;
  12. }

实时效果反馈

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

线程互斥

image-20220402095214741

多个线程在更新共享变量的时候,会出现逻辑问题。

假设,A B 两个线程都对 变量 x 进行加的操作。

A: { x = x + 1; };

B: { x = x + 1; };

通常,解决的办法是加锁。也就是,互斥量:mutex

需要include

一般需要定义为全局变 量 lock() 加锁,unlock() 解锁

  1. mutex g1; //全局的排斥锁
  2. void f()
  3. {
  4. for(int i=0; i<10; i++){
  5. this_thread::sleep_for(chrono::milliseconds(500));
  6. g1.lock();
  7. cout << "thread: "
  8. << "<" << this_thread::get_id() << ">"
  9. << i << endl;
  10. g1.unlock();
  11. }
  12. }
  13. int main()
  14. {
  15. thread t1(f);
  16. thread t2(f);
  17. t1.join();
  18. t2.join();
  19. return 0;
  20. }

要真正理解 mutex,先需要理解 线程的状态

image-20220402065813240

  • lock 动作:

    若未锁,则锁之。

    若其它线程已锁,则自身进入阻塞态,等待唤醒

  • unlock 动作:

    若已锁,唤醒等锁线程之一,若无等待线程,则开锁

    若未锁,则==不确定行为==

另注意:对自己锁过的mutex, 再锁一次,==行为不定==

如果,线程可能多次加锁同一互斥量,

则可考虑用recursive_mutex

其lock:

如果已锁,并且是自己锁的,则仍为锁态,但计数加1,不会发生阻塞

注意,lock 多少次,就要unlock多少次才能解开销量。要求:lock, unlock 严格对称。

有时,lock时不想一直等待,想有个最大忍耐限度,即:超时机制。

  1. std::mutex; // 非递归的互斥量
  2. std::timed_mutex; // 带超时的非递归互斥量
  3. std::recursive_mutex; // 递归的互斥量
  4. std::recursive_timed_mutex; // 带超互斥量
  • mutex::try_lock()

    试锁,如果已被别人锁了,则返回 false

  • timed_mutex::try_lock_for(时间间隔)

    在一段时间内试锁,如果此时间间隔内无法得到锁,则返回 false

  • timed_mutex::try_lock_until(时刻)

    在某一时刻前试锁,如果此时刻前获得锁,则罢,否则返回false

  1. timed_mutex g1;
  2. void f()
  3. {
  4. this_thread::sleep_for(chrono::seconds(1));
  5. if(!g1.try_lock_for(chrono::seconds(3)))
  6. cout << "fail .. " << endl;
  7. else{
  8. cout << "ok.." << endl;
  9. g1.unlock();
  10. }
  11. }
  12. int main()
  13. {
  14. thread th(f);
  15. g1.lock();
  16. cin.ignore();
  17. g1.unlock();
  18. th.join();
  19. return 0;
  20. }

实时效果反馈

1. 关于线程状态, 说法正确的是:__

  • A 线程在阻塞态,经到一时间片段,会醒来,检查条件是否满足

  • B 线程在阻塞态一直等到被某个事件唤醒

  • C 线程在阻塞态被唤醒后直接进入运行态

  • D 就绪态的线程可能会进入阻塞态

2. timedmutex::trylockfor(t),行为是:_

  • A 如果是本线程已锁过的,则返回true

  • B 如果是其它线程已锁过的,则立即返回false

  • C 如果是其它线程已锁过的,则阻塞本线程,直到目标解锁,或 t 时间用尽。

  • D 如果目标未锁,则不做其它,直接返回true

答案

1=>B 2=>C

lock_guard

image-20220405094633777

mutex的使用手册:

lock与unlock必须成对出现。

lock后,千万不要忘记调用 unlock,否则其它线程遭殃了。

如果出现了异常,也要小心,容易忘记了 unlock

….

这是不是很耳熟? new delete 不就是这么说的吗?

然而,内存泄漏一直困扰程序员。

RAII 是一种很好的策略,这里也可以用。

它就是:lock_guard

lock_guard 是一个包装类型的模板类,仅仅封装某种类型的mutex,在析构时调用它的unlock成员函数而已。

  1. mutex g1;
  2. void f()
  3. {
  4. g1.lock();
  5. for(int i=0; i<10; i++){
  6. if(i==6) throw -1;
  7. cout << this_thread::get_id() << ": " << i << endl;
  8. }
  9. g1.unlock();
  10. }
  11. int main()
  12. {
  13. thread t1(f);
  14. thread t2(f);
  15. t1.join();
  16. t2.join();
  17. cout << "main end" << endl;
  18. return 0;
  19. }

会发现,当一个线程抛出异常的时候,另一个线程就一直被锁着,没有机会被唤醒。

因为主线程在等待另一个线程的结束,因而也被阻塞着。

如果换用lock_guard

  1. mutex g1;
  2. void f()
  3. {
  4. lock_guard<mutex> lg(g1);
  5. for(int i=0; i<10; i++){
  6. if(i==6) throw -1;
  7. cout << this_thread::get_id() << ": " << i << endl;
  8. }
  9. }
  10. int main()
  11. {
  12. thread t1(f);
  13. thread t2(f);
  14. t1.detach();
  15. t2.detach();
  16. this_thread::sleep_for(chrono::seconds(1));
  17. cout << "main end" << endl;
  18. return 0;
  19. }

使用注意:

  • 中途不能解锁

  • 对象不可复制

锁定应该尽量短,防止影响其它线程。有时仍需要手工控制锁定时机。

此时,可以采用“领养策略”

此策略,仍是用户自己控制锁定过程,但让lock_guard 来帮助解锁。

  1. // 确保这之前互斥量已经锁定了
  2. lock_guard<mutext> lg(g1, adopt_lock);
  3. // 这只是接管了 g1, 但不会调 lock

为了不影响其它线程,锁的粒度要尽可能地小。

示例:

不用锁的情况:

  1. int sum = 0;
  2. void f(int x)
  3. {
  4. for(int i=0; i<1000 * 100; i++) {
  5. sum += x;
  6. }
  7. }
  8. int main()
  9. {
  10. sum = 0;
  11. thread t1(f, 1);
  12. thread t2(f, -1);
  13. t1.join(); t2.join();
  14. cout << sum << endl;
  15. return 0;
  16. }

用锁的情况:

  1. int sum = 0;
  2. mutex g1;
  3. void f(int x)
  4. {
  5. for(int i=0; i<1000 * 100; i++) {
  6. lock_guard<mutex> lg(g1);
  7. sum += x;
  8. }
  9. }
  10. int main()
  11. {
  12. sum = 0;
  13. thread t1(f, 1);
  14. thread t2(f, -1);
  15. t1.join(); t2.join();
  16. cout << sum << endl;
  17. return 0;
  18. }

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

image-20220405165326507

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 是否锁成功

示例

  1. int sum = 0;
  2. mutex g1;
  3. void f(int x)
  4. {
  5. unique_lock<mutex> uk(g1, defer_lock);
  6. for(int i=0; i<1000 * 10000; i++) {
  7. while(!uk.try_lock()){
  8. cout << this_thread::get_id();
  9. cout << " lock fail.." << endl;
  10. this_thread::sleep_for(
  11. chrono::milliseconds(200));
  12. }
  13. sum += x;
  14. uk.unlock();
  15. }
  16. }
  17. int main()
  18. {
  19. sum = 0;
  20. thread t1(f, 1);
  21. thread t2(f, -1);
  22. t1.join(); t2.join();
  23. cout << sum << endl;
  24. return 0;
  25. }

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

条件变量

image-20220406111343809

mutex 的语义是互斥,为了保护共享资源,使之自动形成排队访问。

还有一种常见的需求是:

一个线程等待某个条件,条件满足了才可以执行,否则一直睡觉。

用mutex 也可以实现这个要求,但行为比较怪异。

  1. vector<int> vec;
  2. mutex g1;
  3. void f()
  4. {
  5. g1.lock();
  6. for(int i=0; i<vec.size(); i++){
  7. cout << "thread f: " << vec[i] << endl;
  8. }
  9. g1.unlock();
  10. }
  11. int main()
  12. {
  13. g1.lock();
  14. thread t1(f); // 要确保 g1 已经 lock
  15. for(int i=0; i<10; i++) vec.push_back(i*10);
  16. cout << "vec ready" << endl;
  17. g1.unlock();
  18. t1.join();
  19. return 0;
  20. }

这里要小心操作, 创建线程前,要保证 g1 已经加锁,然后,线程中的锁才会阻塞自己。

把排斥锁用于:等待-通知 的目的,很不直观,又容易出错。

改用 condition_variable 就好很多。

该对象的 .wait 方法实现在此变量下等待。直到有人调用该对象的 .notify_one, 或者 .notify_all。

需要加头文件:

  1. vector<int> vec;
  2. mutex mtx;
  3. condition_variable cv;
  4. void f()
  5. {
  6. unique_lock<mutex> lck(mtx);
  7. cv.wait(lck);
  8. for(int i=0; i<vec.size(); i++){
  9. cout << "thread f: " << vec[i] << endl;
  10. }
  11. }
  12. int main()
  13. {
  14. thread t1(f);
  15. {
  16. unique_lock<mutex> lck(mtx);
  17. for(int i=0; i<10; i++) vec.push_back(i*10);
  18. }
  19. cout << "vec ready" << endl;
  20. cv.notify_one();
  21. t1.join();
  22. return 0;
  23. }

可以看到:

在调用 wait 前确保自己已经持有 unique_lock(间接持有mutex)。

notify 的时候,则不需要持有锁。

其动作的细节是:

.wait( ) 使本线程进入阻塞状态,但在睡眠前,unlock 持有的锁。

.notify_* 可能使得本线程醒来,醒来的第一件事是:lock

从工程实践的角度来说,这段代码还是不安全的。

因为线程可能因为其它的原因醒来,不一定是有人调了 notify_*

为避免莽撞,一般醒来要查一个标志(bool量),

这几乎成为了惯例:

  1. vector<int> vec;
  2. mutex mtx;
  3. condition_variable cv;
  4. bool ready;
  5. void f()
  6. {
  7. unique_lock<mutex> lck(mtx);
  8. while(!ready) cv.wait(lck);
  9. for(int i=0; i<vec.size(); i++){
  10. cout << "thread f: " << vec[i] << endl;
  11. }
  12. }
  13. int main()
  14. {
  15. ready = false;
  16. thread t1(f);
  17. {
  18. unique_lock<mutex> lck(mtx);
  19. for(int i=0; i<10; i++) vec.push_back(i*10);
  20. ready = true;
  21. }
  22. cout << "vec ready" << endl;
  23. cv.notify_one();
  24. t1.join();
  25. return 0;
  26. }

condition_variable 的强大之处在于:可以同时唤醒多个线程,这个自己实现很麻烦。

  1. vector<int> vec;
  2. mutex mtx;
  3. condition_variable cv;
  4. bool ready;
  5. void f()
  6. {
  7. {
  8. unique_lock<mutex> lck(mtx);
  9. while(!ready) cv.wait(lck);
  10. }
  11. for(int i=0; i<vec.size(); i++){
  12. cout << "thread ";
  13. cout << this_thread::get_id() << " :";
  14. cout << vec[i] << endl;
  15. }
  16. }
  17. int main()
  18. {
  19. ready = false;
  20. thread t1(f);
  21. thread t2(f);
  22. {
  23. unique_lock<mutex> lck(mtx);
  24. for(int i=0; i<10; i++) vec.push_back(i*10);
  25. ready = true;
  26. cout << "vec ready" << endl;
  27. }
  28. cv.notify_all();
  29. t1.join();
  30. t2.join();
  31. return 0;
  32. }

总结:

  • 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

生产消费者模型

image-20220406160734957

生产-消费者模型是典型的多线程协作场景

假设有若干个生产者,若干个消费者,它们独立运行。

大家共享一个队列,生产者往队列放东西,消费者取出东西。

当队列为空时,消费者睡觉,等待唤醒

当队列为满时,生产者睡觉,等待唤醒

章节四:信息学竞赛 CPP 应用篇 - 图9

需要一个 mutex 来实现互斥

需要两个 condition_variable,一个用于表达条件:队列非空,

一个用于表达条件:队列非满

  1. #include <iostream>
  2. #include <thread>
  3. #include <mutex>
  4. #include <condition_variable>
  5. #include <queue>
  6. #include <ctime> // srand
  7. using namespace std;
  8. queue<int> Q;
  9. mutex mtx;
  10. condition_variable cv_emp, cv_ful;
  11. // consumer 消费者
  12. void f1()
  13. {
  14. srand(time(0));
  15. unique_lock<mutex> lck(mtx, defer_lock);
  16. while(1){
  17. lck.lock();
  18. while(Q.empty()) cv_emp.wait(lck);
  19. cout << this_thread::get_id();
  20. cout << " consum " << Q.front() << endl;
  21. Q.pop();
  22. if(Q.size()<=10) cv_ful.notify_all();
  23. lck.unlock();
  24. int t = rand()%1000;
  25. this_thread::sleep_for(chrono::milliseconds(t));
  26. }
  27. }
  28. // producer 生产者
  29. void f2()
  30. {
  31. srand(time(0));
  32. unique_lock<mutex> lck(mtx, defer_lock);
  33. for(int i=0; i<30; i++){
  34. lck.lock();
  35. while(Q.size()>10) cv_ful.wait(lck);
  36. cout << this_thread::get_id();
  37. cout << " produce " << i << endl;
  38. Q.push(i);
  39. cv_emp.notify_all();
  40. lck.unlock();
  41. int t = rand()%1000;
  42. this_thread::sleep_for(chrono::milliseconds(t));
  43. }
  44. }
  45. int main()
  46. {
  47. thread t1[3], t2[3];
  48. for(int i=0; i<3; i++) t1[i] = thread(f1); //消费者
  49. for(int i=0; i<3; i++) t2[i] = thread(f2); //生产者
  50. for(int i=0; i<3; i++) t1[i].join();
  51. for(int i=0; i<3; i++) t2[i].join();
  52. return 0;
  53. }

这里要注意 wait 的动作:

进入睡眠前,unlock

醒来后 lock,这个lock 会把同伴阻住。

实时效果反馈

1. 关于条件量, 说法正确的是:__

  • A 两个不同的条件量,必须使用不同的 mutex

  • B 同一个条件量,只能用同一个 mutex

  • C 条件量可以不需要unique_lock 而单独使用

  • D 以上都不对

2. 上例中,当所有生产者线程结束一段时间后,消费者线程处于什么状态?__

  • A 结束态

  • B 阻塞态

  • C 就绪态

  • D 僵尸态

答案

1=>D 2=>B

死锁问题

image-20220410083032738

使用锁的两大魔鬼:

  • 忘记了 unlock,导致被阻塞线程一直醒不过来
  • 循环等待,产生死锁,谁也醒不过来

死锁的形成:持有锁,并申请等待另一把锁

章节四:信息学竞赛 CPP 应用篇 - 图11

产生死锁的示例

  1. mutex mtx_a;
  2. mutex mtx_b;
  3. void f()
  4. {
  5. cout << "f .. begin" << endl;
  6. mtx_a.lock();
  7. cout << "mtx_a locked" << endl;
  8. this_thread::sleep_for(chrono::milliseconds(200));
  9. mtx_b.lock();
  10. cout << "mtx_a, mtx_b locked" << endl;
  11. this_thread::sleep_for(chrono::milliseconds(100));
  12. mtx_b.unlock();
  13. mtx_a.unlock();
  14. cout << "f .. end" << endl;
  15. }
  16. void g()
  17. {
  18. cout << "g ... begin" << endl;
  19. mtx_b.lock();
  20. cout << "mtx_b locked" << endl;
  21. this_thread::sleep_for(chrono::milliseconds(200));
  22. mtx_a.lock();
  23. cout << "mtx_a, mtx_b locked" << endl;
  24. this_thread::sleep_for(chrono::milliseconds(100));
  25. mtx_a.unlock();
  26. mtx_b.unlock();
  27. cout << "g ... end" << endl;
  28. }
  29. int main()
  30. {
  31. thread t1(f);
  32. thread t2(g);
  33. t1.join();
  34. t2.join();
  35. return 0;
  36. }

解决方案

  • 检测出死锁,牺牲某个线程
  • 避免死锁的发生

按某个顺序持有锁(比如:字母序)

线程中计划锁哪些东西,要先规划好。

std::lock

章节四:信息学竞赛 CPP 应用篇 - 图12

传入多个互斥量为参数,有可能引起阻塞,但醒来一定保证拿到所有的锁。

此函数会自行决定锁的顺序和策略,可能会调用 lock ,try_lock,unlock等,

目标是保证不死锁,同时兼顾锁的使用效率。

是自动解决死锁问题的方案之一。

与之对应的,还有一个 std::try_lock 原理相仿

把前边的例子改为:

  1. void f()
  2. {
  3. cout << "f .. begin" << endl;
  4. lock(mtx_a, mtx_b);
  5. cout << "mtx_a, mtx_b locked" << endl;
  6. this_thread::sleep_for(chrono::milliseconds(100));
  7. mtx_b.unlock();
  8. mtx_a.unlock();
  9. cout << "f .. end" << endl;
  10. }
  11. void g()
  12. {
  13. cout << "g ... begin" << endl;
  14. lock(mtx_b, mtx_a);
  15. cout << "mtx_a, mtx_b locked" << endl;
  16. this_thread::sleep_for(chrono::milliseconds(100));
  17. mtx_a.unlock();
  18. mtx_b.unlock();
  19. cout << "g ... end" << endl;
  20. }

注意,解锁的时候仍是单独分别解锁。

原则是,只要可能,尽快解锁,免得影响其它线程。

另外,为了避开手工操作 mutex,也可以使用 lock_gard 来管理 mutex

  1. void g()
  2. {
  3. cout << "g ... begin" << endl;
  4. lock(mtx_b, mtx_a);
  5. cout << "mtx_a, mtx_b locked" << endl;
  6. {
  7. lock_guard<mutex> lck1(mtx_a, adopt_lock);
  8. lock_guard<mutex> lck2(mtx_b, adopt_lock);
  9. this_thread::sleep_for(chrono::milliseconds(100));
  10. }
  11. cout << "g ... end" << endl;
  12. }

因为 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

image-20220410091121448

c++11新引入的一种存储类型修饰符。

c++原有的存储类型:

  • 代码中的字面量
  • 静态变量(全局,局部静态,静态成员变量)
  • 栈变量
  • 堆变量

现在又多了一个:thread_local

以此修饰符修饰的变量,具有线程的生存周期:

线程开始时生成,线程结束时销毁。

可以修饰:全局变量,static成员变量,本地static变量。

其实质是:c语言原来的全局存储的静态类型,分离出一种对线程隔离效果的全局类型。

c语言产生的年代,还没多线程的需求,不可能那么高瞻远瞩地设计。

  • 普通的全局变量

    1. int x = 0;
    2. void g()
    3. {
    4. x++; // 理应使用 mutex 来实现互斥访问,不是本节要点
    5. cout << x << endl;
    6. }
    7. void f()
    8. {
    9. for(int i=0; i<10; i++) g();
    10. }
    11. int main()
    12. {
    13. thread t1(f);
    14. thread t2(f);
    15. t1.join();
    16. t2.join();
    17. return 0;
    18. }
  • 局限于线程中的“全局变量”

  1. thread_local int x = 0; // 实际上,只修改了这一个地方
  2. void g()
  3. {
  4. x++;
  5. cout << x << endl;
  6. }
  7. void f()
  8. {
  9. for(int i=0; i<10; i++) g();
  10. }
  11. int main()
  12. {
  13. thread t1(f);
  14. thread t2(f);
  15. t1.join();
  16. t2.join();
  17. return 0;
  18. }
  • thread_local 型变量的生存期:

每个thread_local变量在线程创建的时候创建,然后对本线程而言,相当于全局变量;

当线程销毁的时候,会同时销毁当初创建的这个thread_local变量。

这个变量当然不可能是静态存储的。

其它使用场合

  • 静态成员变量

    仅仅是面向对象的封装性的体现。

    变量还是静态的,全局的,只是使用权限上加以约束。

    改用 thread_local 后,保持对线程的全局语义,但已经不是静态的了。

  • 局部static变量

    本质上,还是静态全局的,只是作用域局限在一个函数范围。

    改用 thread_local 后,保持对线程调用该函数的全局语义,但已经不是静态的了。

  1. void g()
  2. {
  3. thread_local int x = 0;
  4. x++;
  5. cout << x << endl;
  6. }
  7. void f()
  8. {
  9. for(int i=0; i<10; i++) g();
  10. }
  11. int main()
  12. {
  13. thread t1(f);
  14. thread t2(f);
  15. t1.join();
  16. t2.join();
  17. return 0;
  18. }

实时效果反馈

1. 关于threadlocal, 说法正确的是:_

  • A thread_local变量在多个线程间共享

  • B thread_local变量存储在静态空间

  • C thread_local变量在线程创建时申请,在线程销毁时释放

  • D thread_local变量不可以是用户定义类型

答案

1=>C

未来承诺

image-20220410194047307

c++11 future & promise

我们在早前的课程中,一直有这样一个观念:

程序中,如果能不用全局变量,就尽量不用。

在学习线程时,为了通信,又不得不用全局变量,这不是灾难吗?

虽然,也可以 thread_local 变量,然后把指针传给另一个线程,但把指针传来传去的..

考虑如下的常见场景:

线程A,需要线程B的计算结果,以便进一步的计算。如果结果没出来就等待。

章节四:信息学竞赛 CPP 应用篇 - 图15

用一个全局量,加一个mutext,当然可以,但大量这样的情况,早晚会一片混乱!

c++11 的线程方案,并不能用join等待一个线程结束,并获得它的返回值。(linux的可以)

因为,c++11 提供了另一套更完备的解决方案。

大体思路是:

用一pormise 对象,关联到一个 future对象,

把promise对象传到计算线程中,

自己可以做其它事。。。

调用 future对象的 get_value,此时,如果对方没准备好结果,引起阻塞。

直到拿到了结果才返回。

此刻,在大洋彼岸,另一线程的情况:

结果一出来,就调用 promise 对象的set_value

。。。。

听上去有点头大??

90%的应用中,需求足够单纯。可以用 async 这个简化包装。

相当于一个套餐,用着省事。

  1. #include <iostream>
  2. #include <thread>
  3. #include <future>
  4. #include <chrono>
  5. using namespace std;
  6. int f(int x, int y)
  7. {
  8. this_thread::sleep_for(chrono::seconds(2));
  9. return x * 10 + y;
  10. }
  11. int main()
  12. {
  13. future<int> res = async(launch::async, f, 5, 8);
  14. cout << "do other things" << endl;
  15. int r = res.get();
  16. cout << r << endl;
  17. return 0;
  18. }

这里,

  1. launch::async //表示创建新线程
  2. launch::deferred // 表示延迟处理,同步调用,不创建新线程
  3. 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

原子变量

image-20220410192817907

加锁固然能解决多线程的一些问题。但工程实践表明,惹出的麻烦也不少。

能不能不加锁,也可以实现共享呢?

章节四:信息学竞赛 CPP 应用篇 - 图17

上图,假设 N 是个线程间共享的全局变量。

一个线程对它的操作,一般情况下可以看作 3 步:

  • 读取 N
  • 对 N 运算: ${N \longrightarrow N_1}$
  • 写回新的值 ${N_1}$

如果,能保证,这3步操作不在线程间交叉,则不会引起冲突。

某一系列动作能不被其它线程干扰,则称之为原子操作

这个不被干扰,也有两层境界:

  1. 这一系列动作,在一个线程时间片完成,中间不被打断
  2. 允许中间打断(不在一个时间片),但期间所有欲染指者均被阻塞

显然,对1,需要操作系统一级的支持。

对2, 只是算法技巧,让使用者感觉这一系列动作是原子的。

atomic

c++11 提供的 atomic 模板系统地考虑了这些问题,甚至走得更远。

  • 对自己的类型使用模板,可以保证写入、读取的原子性。
  • 对常用内置类型提供了很多保证原子性的操作
  • 对于编译器的乱序优化,提供了各种控制手段
  • compareexchange* 提供原子性的另一种解决方案(已经被干扰了,如何处理?)

简单变量的原子操作

对简单变量的操作,也需要加锁,解锁,有时感觉很麻烦

int x ; x++;

bool b; b = !b;

如果不用锁,后果很严重:

  1. int sum = 0; //没有加锁保护的全局变量
  2. void f(int x)
  3. {
  4. for(int i=0; i<x; i++) sum++;
  5. }
  6. int main()
  7. {
  8. thread t1(f, 1000*100);
  9. thread t2(f, 1000*100);
  10. t1.join();
  11. t2.join();
  12. cout << sum << endl;
  13. return 0;
  14. }

如果没有原子量一说,x++ 的动作可能是:

【读取 x 值到寄存器; 寄存器值加1; 写回到 x】

这三个动作可能不会一气呵成,中间可能被其它线程打断。这就危险了。。。

所谓原子就是一系列不可被分割的动作。

它们要么不去执行,要么就全部执行,不会出现执行了一半被其它线程插进来的窘况。

如果能保证某一系列动作的原子性,就不需要用 lock 了。

需要 include

只把定义sum的一句改掉就可以了

  1. atomic<int> sum{0};

atomic 是个模板,我们可以通过重载方法,来实现自己的原子类型。

c++11 已经替我们实现了常见类型的原子化。

章节四:信息学竞赛 CPP 应用篇 - 图18

上例中的 x++ 就是原子化的,因而不需要加锁

用 mutex 与原子量的耗时比较

  1. int sum = 0;
  2. mutex mtx;
  3. void f(int x)
  4. {
  5. for(int i=0; i<x; i++) {
  6. mtx.lock();
  7. sum++;
  8. mtx.unlock();
  9. }
  10. }
  11. int main()
  12. {
  13. auto t = clock();
  14. thread t1(f, 1000*1000);
  15. thread t2(f, 1000*1000);
  16. t1.join();
  17. t2.join();
  18. cout << sum << endl;
  19. t = clock() - t;
  20. cout << t << endl;
  21. return 0;
  22. }

实时效果反馈

1. 关于使用atomic代替锁的好处, 说法正确的是:__

  • A 执行更高效,更不容易出错

  • B 更好的操作系统兼容性

  • C 更广泛的应用领域,比如:嵌入式系统

  • D 对多核处理器更有利

2. 对于 atomic x,哪个动作不是原子的?__

  • A x = 5;

  • B x++;

  • C x.fetch_add(3);

  • D x += 6;

答案

1=>A 2=>D

GUI与Qt

image-20220411174224200

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工具,能快速构建应用,开发效率高
  • 可支持嵌入式开发

最简示例

章节四:信息学竞赛 CPP 应用篇 - 图20

章节四:信息学竞赛 CPP 应用篇 - 图21

选择编译环境时:

章节四:信息学竞赛 CPP 应用篇 - 图22

自动生成的工程:

章节四:信息学竞赛 CPP 应用篇 - 图23

使用窗体设计器

点击*.ui 进入它的设计视图

本质上,是在编辑一个XML文件

可以用 ctrl+F2 切换到XML视图,ctrl+F3切换到设计视图

建议不要手工编辑这个XML文件

左边是组件,可以拖动到form上

章节四:信息学竞赛 CPP 应用篇 - 图24

然后,在右边修改它的各种属性

章节四:信息学竞赛 CPP 应用篇 - 图25

章节四:信息学竞赛 CPP 应用篇 - 图26

再加入一个Button组件

章节四:信息学竞赛 CPP 应用篇 - 图27

为按钮添加事件:

image-20220411152722376

至此,我们没写一行代码,第一个应用程序就完成了!

实时效果反馈

1. 关于Qt的优势, 说法==错误==的是:__

  • A 跨多种平台,包括移动和嵌入式

  • B 接口简单,上手容易

  • C 有开源版本

  • D 有软件巨头背书

2. *.ui 文件是:__

  • A 窗体组件文件,以二进制形式存储

  • B 窗体组件文件,以XML 格式存储

  • C 国际资源文件,以文件格式存储

  • D 图片文件,以二进制形式存储

答案

1=>D 2=>B

Qt框架分析

image-20220411205751197

看一下工程中的主要文件:

章节四:信息学竞赛 CPP 应用篇 - 图30

文件分析

main.cpp 应用程序的入口,

其功能是:创建应用程序,创建窗口,显示窗口,运行应用程序,开启消息循环

  1. #include "mainwindow.h"
  2. #include <QApplication>
  3. int main(int argc, char *argv[])
  4. {
  5. QApplication a(argc, argv); //代表应用程序
  6. MainWindow w;  // 主窗口对象
  7. w.show();  // 显示窗口
  8. return a.exec(); // 运行程序,并等待处理消息
  9. }

mainwindow.h 定义窗体类的头文件

  1. #ifndef MAINWINDOW_H
  2. #define MAINWINDOW_H
  3. #include <QMainWindow>
  4. namespace Ui {  // 命名空间
  5. class MainWindow;  //不是下面这个MainWindow类,
  6. // 而是ui_mainwindow.h中定义的类
  7. // 这个文件是由mainwindow.ui 自动生成的
  8. } // 这相当于一个外部文件声明
  9. class MainWindow : public QMainWindow
  10. {
  11. Q_OBJECT  // 使用Qt的信号与槽机制,必须用这个宏
  12. public:
  13. explicit MainWindow(QWidget *parent = 0);
  14. ~MainWindow();
  15. private:
  16. Ui::MainWindow *ui; //就是前边声明的类,代表ui组件的入口 
  17. };
  18. #endif // MAINWINDOW_H

mainwindows.cpp 主窗体类的实现代码

  1. #include "mainwindow.h"
  2. #include "ui_mainwindow.h" // 很重要,包含所有窗体上的内容
  3. // 这个.h文件是自动生成的,不要手工修改,改了也没用
  4. MainWindow::MainWindow(QWidget *parent) :
  5. QMainWindow(parent),
  6. ui(new Ui::MainWindow)  // 向ui注入值
  7. {
  8. ui->setupUi(this); //此处创建了各个组件,建立属性,
  9. //建立信号与槽的连接等初始化工作
  10. }
  11. MainWindow::~MainWindow()
  12. {
  13. delete ui;
  14. }

mainwindow.ui 窗体设计文件

这个不是标准c++的文件,无法直接编译它

实际上,它是一个普通的XML文件

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <ui version="4.0">
  3. <class>MainWindow</class>
  4. .....
  5. <widget class="QToolBar" name="mainToolBar">
  6. <attribute name="toolBarArea">
  7. ....
  8. ....
  9. </hints>
  10. </connection>
  11. </connections>
  12. </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

信号和槽

image-20220415201748464

信号和槽是Qt编程的基础。

不同的GUI系统,体系结构可能很大不同,但都需要面对同样的需求。

比如:如何处理界面上发生的事件。

有用回调函数(callback function)的,有MVC模型的,观察者模式的。。。

Qt 发明了 signals & slots 机制,使交互机制变得灵活、直观、简单。

信号(signal) 就是在特定情况下发生的事件。类似广播出去。

槽(slot) 就是一个响应函数。

信号和槽是完全隔离的,信号发出者并不知道该信号会被谁处理。

槽是一个有某种约定的普通函数,它可以被直接调用,也可以被关联到槽。

关联的方法:

  1. QObject::connt(sender, SIGNAL(信号), receiver, SLOT(槽函数));

关联约定

  • 一个信号可以关联到多个槽
  • 多个信号可以关联到一个槽
  • 一个信号可以关联到另一个信号
  • 信号的参数与槽的参数要匹配(信号至少能满足槽的参数)
  • 一般情况下,当信号发生时,槽函数会被立即触发,就像调用槽函数一样。

关联方法

下面举例常见的三种信号与槽的关联方法:

  • 设计视图的Signals & Slots 面板

章节四:信息学竞赛 CPP 应用篇 - 图32

依次选 发送者,信号,接收者,槽

如果还觉得不直观,可以用可视化编辑

章节四:信息学竞赛 CPP 应用篇 - 图33

在弹出的框中

章节四:信息学竞赛 CPP 应用篇 - 图34

这两种方法本质上是一样的

只关联已有槽,删除也容易

  • 添加新的槽和关联

在组件上: 右键 | 转到槽,会自动添加槽,其名字是有规律的。

可以自己在槽函数中加入处理逻辑

槽的名字有特殊含义,Qt就是用这个名字来识别的

删除时,只要.h 和.cpp中删除了相应代码即可(关联是动态的)

  • 手工生成槽函数和关联

手工添加槽函数:

void myCheck(bool b);

并在其中添加逻辑代码

手工添加关联:

  1. ui->setupUi(this);
  2. connect(ui->checkBox_3, SIGNAL(clicked(bool)),
  3. 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

布局管理

image-20220417094646722

早期的桌面应用,一般窗口的大小是固定的,这样就很少有布局的要求;

至多是组件间的对齐关系。

随着移动应用、嵌入设备、web应用的普及,桌面应用对布局的要求越来越强烈。

比如:横竖屏的切换,分辨率的调整。。。。

简单地说,布局管理,主要任务是处理:当窗体改变大小的时候,如何调整组件的位置,大小,以达到更人性化的呈现效果。

Qt提供了十分丰富的布局管理控制,主要在:Layouts 和 spacers 这两个面板中。

章节四:信息学竞赛 CPP 应用篇 - 图36

常用的布局

  • vertical Layout 垂直布局
  • Horizontal Layout 水平布局
  • Grid Layout 网格布局

可以拖放布局到窗口中,再把组件拖放到布局里。

布局中可以再嵌套布局,形成十分复杂的行为

章节四:信息学竞赛 CPP 应用篇 - 图37

这是在水平布局中嵌套垂直布局的情况。

此外,也可以先放组件,以后再配布局。这通过工具栏上的布局按钮或右键菜单来完成。

章节四:信息学竞赛 CPP 应用篇 - 图38

占位器组件

俗称:弹簧

  • horizonal spacer 水平点位器

  • vertical spacer 垂直点位器

当需要让一些组件靠某一边对齐时,可以用弹簧配合组件的其它属性来完成。

当布局改变大小的时候,组件需要响应这种变化。

其主要的影响因素是:

章节四:信息学竞赛 CPP 应用篇 - 图39

整个窗体上的最外层布局

整个窗体上可以有一个最外层的布局,当窗体改变大小的时候,这个布局跟随调整大小。

可以选中窗体,再用工具栏的布局按钮来完成,

也可以在窗体上,右键 | 布局 ,来选相应的菜单。

如果需要取消窗体上的最高布局,同样方法,选:“打破布局”。

综合示例

章节四:信息学竞赛 CPP 应用篇 - 图40

实时效果反馈

1. 关于Qt常用的布局组件, 说法==错误==的是:__

  • A 垂直布局

  • B 水平布局

  • C 网格布局

  • D 流布局

2. 如何让一个按钮,总是在窗体的右下角:__

  • A 直接拖放到右下角

  • B 左边加弹簧,水平布局

  • C 左边加弹簧,水平布局,外面再其上加弹簧,垂直布局

  • D 无法实现

答案

1=>D 2=>C

可视化UI设计

image-20220418103448841

UI可视化设计的本质是生成一个XML文件。

Qt 可以设计模式和源文件模式来查看这个XML文件

ctrl + F2 切换到源码模式

ctrl + F3 切换到设计模式

我们不要直接去修改 XML 文件,而应该在界面上修改。

  • 通过 Container 可以实现“国中国”的效果。

比如,对:checkBox 和 radio Button 的分组,很常用。

container 本身可以设布局方式,如果不设,然后却参加到另一个布局中,可能会有怪异的表现。

container 可以看作窗体中的一个子窗体

  • 伙伴关系

    章节四:信息学竞赛 CPP 应用篇 - 图42

点击伙伴关系按钮,可以设置Label与其它组件的伙伴关系。

设置伙伴关系的目的是:可以通过快捷方式让焦点快速跳到伙伴组件上。

直接在界面上通过拖放来设置伙伴关系

  • Tab序

通过按tab键,可以让焦点在各个输入组件间跳,其顺序是可以控制的。

章节四:信息学竞赛 CPP 应用篇 - 图43

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设计

image-20220418123413156

虽然Qt提供了可视化的UI设计方式,但有时也会用到代码直接设计UI的情景。

直接用代码与可视化 —> XML—> ui_xxx.h 的本质是一样。

用代码会更繁琐一些,但控制能力,灵活性也会更强一些。

其基本步骤大体分为:

  • 建立界面元素的对象,及其关系
  • 编写槽函数
  • 建立槽函数与信号的连接关系

目标

章节四:信息学竞赛 CPP 应用篇 - 图45

功能很简单,点击radioButton,让下面的文本框中的字变颜色。

实现

新建项目

选QWidget为继承类

widget.h 文件

  1. #ifndef WIDGET_H
  2. #define WIDGET_H
  3. #include <QWidget>
  4. #include <QRadioButton>
  5. #include <QPlainTextEdit>
  6. #include <QButtonGroup>
  7. class Widget : public QWidget
  8. {
  9. Q_OBJECT
  10. public:
  11. Widget(QWidget *parent = 0);
  12. ~Widget();
  13. private slots:
  14. void mySetColor(int id);
  15. private:
  16. QRadioButton* rbtnBlack;
  17. QRadioButton* rbtnRed;
  18. QRadioButton* rbtnBlue;
  19. QPlainTextEdit* txtEdit;
  20. QButtonGroup* btngColor;
  21. };
  22. #endif // WIDGET_H

widget.cpp 文件

  1. #include "widget.h"
  2. #include <QHBoxLayout>
  3. #include <QVBoxLayout>
  4. #include <QButtonGroup>
  5. Widget::Widget(QWidget *parent)
  6. : QWidget(parent)
  7. {
  8. rbtnBlack = new QRadioButton("黑");
  9. rbtnRed = new QRadioButton("红");
  10. rbtnBlue = new QRadioButton("兰");
  11. QHBoxLayout* lay = new QHBoxLayout;
  12. lay->addWidget(rbtnBlack);
  13. lay->addWidget(rbtnRed);
  14. lay->addWidget(rbtnBlue);
  15. txtEdit = new QPlainTextEdit;
  16. txtEdit->setPlainText("haha \n测试");
  17. QFont font = txtEdit->font();
  18. font.setPointSize(20);
  19. txtEdit->setFont(font);
  20. QVBoxLayout* lay2 = new QVBoxLayout;
  21. lay2->addLayout(lay);
  22. lay2->addWidget(txtEdit);
  23. setLayout(lay2);
  24. btngColor = new QButtonGroup(this);
  25. btngColor->addButton(rbtnBlack,0);
  26. btngColor->addButton(rbtnRed,1);
  27. btngColor->addButton(rbtnBlue,2);
  28. connect(btngColor, SIGNAL(buttonClicked(int)),
  29. this, SLOT(mySetColor(int)));
  30. }
  31. Widget::~Widget()
  32. {
  33. }
  34. void Widget::mySetColor(int id)
  35. {
  36. QPalette pl = txtEdit->palette();
  37. switch (id) {
  38. case 0:
  39. pl.setColor(QPalette::Text, Qt::black);
  40. break;
  41. case 1:
  42. pl.setColor(QPalette::Text, Qt::red);
  43. break;
  44. case 2:
  45. pl.setColor(QPalette::Text, Qt::blue);
  46. }
  47. txtEdit->setPalette(pl);
  48. }

实时效果反馈

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

菜单

image-20220418174917892

主窗口与对话框窗口的区别在于:

主窗口中,一般有主菜单,工具栏,状态栏。

主菜单可以在设计时生成,也可以用代码动态生成或修改。

  • 设计时

    章节四:信息学竞赛 CPP 应用篇 - 图47

  • 代码生成也可以,也可生成多级菜单

核心概念是 Action

Action

action 代表一个动作项,可以是一个主菜单的末级,也可以右键弹出菜单的末级。

也可以把Action放到工具栏上,

这样,在多个操作路径下,可以对应同一个Action

而Action的信号可以关联到槽。

Action还可以设置统一的图标,统一的悬浮提示,甚至使用统一的皮肤管理。

Action 可以独立于主菜单而存在

右键菜单

有多种方法,一种方法是:重载QMainWindow的contextMenuEvent函数

在其中实现自己的逻辑。

可以先在界面上建立Action

然后,用代码构建右键菜单,再把Action加入到菜单的末级

  1. QMenu m;
  2. QMenu m1("1111");
  3. m1.addAction(ui->actionmy1);
  4. m1.addAction(ui->actionmy2);
  5. m.addMenu(&m1);
  6. m.addAction(ui->actiona11);
  7. 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类库概述

image-20220418205102003

Qt在使用类库的方式,在标准的c++基础上进行了扩展。

引入了元对象系统,信号与槽,属性等特征。

元对象编译器(Meta Object Compiler)是一个预处理器,在编译前把Qt特性先预处理为:c++可以识别的标准代码。

QObject 是所有使用元对象系统的类的基类。

要求其内部声明 private 的: Q_OBJECT 宏。

元对象系统提供了很多特性。

除了信号与槽外,另一个常用的是:属性

很多面向对象的高级语言,比如 delphi,都提供了属性这一个概念。

c++ 没有在语言层面提供这一概念。

Qt 通过元对象系统弥补了这一缺憾。

简单地说,属性,就是对象的虚假的“成员变量”

从外部的观点来看,某个对象具有某种类型的变量,但实际上,

对它的读写操作可能分别由不同的成员函数来完成。

这有什么用处呢?

章节四:信息学竞赛 CPP 应用篇 - 图49

  1. 面向对象的封装原则。对象的属性好比智能类型,不只简单的读写,至少能防误

    比如: age, 设置为负数,或其它不合理数值,会被发现并拒绝

  2. 对象使用的一致性。比如,在脚本语言中,如何操作c++对象,需要一致的方法。

  3. 对属性的读写可能引发一系列复杂的动作。

    比如:写age的值,可能顺带引发一个信号。

    这样,我们在修改一个对象的属性的时候,可能触发一系列的动作。

属性示例

新建一个 QBoy 类,包含一个age 属性

可以 读, 可以写,

写入成功的时候,触发一个 ageChanged 信号

最后,我们把这个信号 和 一个自己定义的槽关联 起来

使用属性需要在类中用 Q_OBJECT

然后用宏添加属性:

  1. 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 关联 多值便利

示例

  1. QList<QString> a;
  2. a.append("123");
  3. a.prepend("abc");
  4. a.insert(1,"xxx");
  5. a[1] = "ttt";
  6. for(auto i: a){
  7. qDebug() << i;
  8. }
  1. QQueue<int> a;
  2. a.enqueue(1);
  3. a.enqueue(2);
  4. a.enqueue(3);
  5. while(!a.isEmpty()){
  6. qDebug() << a.dequeue();
  7. }

实时效果反馈

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容器

image-20220422091530104

Qt 容器从逻辑上看,与STL容器差异不大,但更多考虑了易用性、便利性。

简单示例

  1. QSet<int> a;
  2. a << 1 << 2 << 3;
  3. QSet<int> b;
  4. b << 3 << 4 << 5;
  5. auto c = a + b;
  6. qDebug() << c;
  7. auto d = a - b;
  8. qDebug() << d;
  9. qDebug() << a.contains(2);

对自定义的类型

  1. struct Stu{
  2. Stu(const QString& s, int a){
  3. name = s;
  4. age = a;
  5. }
  6. bool operator==(const Stu& t) const{
  7. return name == t.name && age == t.age;
  8. }
  9. QString name;
  10. int age;
  11. };
  12. QDebug operator<<(QDebug dbg, const Stu& t)
  13. {
  14. dbg << "Stu(" << t.name << "," << t.age << ")";
  15. return dbg;
  16. }
  17. uint qHash(const Stu& stu)
  18. {
  19. return qHash(stu.name) + stu.age;
  20. }

使用:

  1. QSet<Stu> a;
  2. a << Stu("zhang", 15) << Stu("Li", 12);
  3. a << Stu("zhang", 15);
  4. qDebug() << a;

Hash 表判断相等的方法:

  1. 首先看 qHash(key) 是否相同,若不相同则一定不相等
  2. 若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()

  1. QMultiHash<QString, int> a;
  2. a.insert("zhang", 15);
  3. a.insert("li", 12);
  4. a.insert("zhang", 16);
  5. qDebug() << a.value("zhang");
  6. qDebug() << a.value("wu", -1);
  7. qDebug() << a.values("zhang");

章节四:信息学竞赛 CPP 应用篇 - 图51

实时效果反馈

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迭代器

image-20220422104915054

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型迭代器指向的位置,不是元素,而是元素间的“缝隙”

章节四:信息学竞赛 CPP 应用篇 - 图53

这样设计的好处是:

可以支持在遍历中的删除、修改等动作

常见操作:

函数 功能
void toFront() 回到头
void toBack() 回到尾
bool hasNext() 没有到最后位置则true
const T& next() 返回下后一个数据项,并向后移动迭代器
const T& peekNext() 偷看后一项,不移动迭代器
bool hasPrevious() 没有到最前位置,则true
const T previous() 返回前一个数据项,并向前移动迭代器
const T& peekPrevious() 偷看前一项,不移动迭代器
  1. QList<int> a = {1,2,3,4};
  2. QListIterator<int> i(a);
  3. while(i.hasNext()){
  4. qDebug() << i.next();
  5. }

当然,如果只是看一眼每个元素,也可以用c++11的语法:

  1. QList<int> a = {1,2,3,4};
  2. for(auto i: a){
  3. qDebug() << i;
  4. }

使用java型迭代器的好处之一是:可以边迭代边删除,不会有指针失效的问题。

  1. QList<int> a = {1,2,3,4,5};
  2. QMutableListIterator<int> i(a);
  3. while(i.hasNext()){
  4. if(i.next() % 2 == 0) i.remove();
  5. }
  6. qDebug() << a;

章节四:信息学竞赛 CPP 应用篇 - 图54

关联型容器的迭代器与此类似,只不过需要: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

image-20220424142009174

与std::string 类似,QString是对串对象概念的实现。

与string相比较,QString提供了大量的方便且实用的函数,着眼点在于便捷、实用性,而不是优先考虑效率问题。

另外,QString也用更方便的方式,解决串的国际化问题。

QString 可以看成是 QChar 的容器。

QChar 是2字节的宽字符。可以直接表示中文的一个字符。即unicode的UTF-16表达。

  1. QString s = "中国abc";
  2. qDebug() << s.length();
  3. for(QChar i: s){
  4. qDebug() << i;
  5. }
  6. qDebug() << s[0].unicode();

章节四:信息学竞赛 CPP 应用篇 - 图56

可以看到,每个字符,就是它的unicode码。这样对西方字符,浪费了存储空间,但对大字符集,比如中文的处理,就容易多了。

另外,QString 是隐式共享的,因而不要害怕传递QString对象,它在不改写的时候,实际传递的只是共享指针而已。只有改变QString的时候,复制才真正发生。

与std::string, char* 的互转

  1. QString s = "中国abc";
  2. const char* p = s.toStdString().c_str();
  3. qDebug() << p;
  4. qDebug("%2x %2x", uchar(p[0]), uchar(p[1]));
  5. qDebug() << std::strlen(p);
  6. const char* p1 = "中文123";
  7. s = QString::fromUtf8(p1);
  8. qDebug() << s;

章节四:信息学竞赛 CPP 应用篇 - 图57

串与数值的转换

QString与数值间的转换十分常用,Qt了提供了很多方案,便于使用。

  1. QString s = "123";
  2. qDebug() << s.toInt() << s.toDouble();
  3. int a = 3322;
  4. qDebug() << QString::number(a,16);
  5. s.setNum(a, 2);
  6. qDebug() << s;
  7. qDebug() << QString::asprintf("---%d---", a);

很多函数都有两个版本,一个静态函数,一个QString对象成员函数

串处理的特色函数举例

  • QChar
  1. QString s = "abc";
  2. s += "你好";
  3. qDebug() << s[3].unicode();
  4. qDebug() << s.at(5).unicode(); //会引发异常
  • empty 与 null
  1. QString s = "";
  2. qDebug() << s.isEmpty();
  3. qDebug() << s.isNull();
  4. s.clear();
  5. qDebug() << s.isNull();
  • section
  1. QString s = "/usr/local/bin/app/mytxt";
  2. qDebug() << s.section('/',2);
  3. qDebug() << s.section('/',2,3);
  4. qDebug() << s.section('/',-1);
  5. qDebug() << s.section('/',-2,-2);

章节四:信息学竞赛 CPP 应用篇 - 图58

  • trimmed() 和 simplified()
  1. QString s = " xyz abc kkk ";
  2. qDebug("|%s|", s.trimmed().toStdString().c_str());
  3. qDebug("|%s|", s.simplified().toStdString().c_str());

章节四:信息学竞赛 CPP 应用篇 - 图59

实时效果反馈

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

数值输入与显示

image-20220425195752998

  • SpinBox

一般用于整数的显示,也可以改变进制,加前缀和后缀等

  • QSlider

滑动条,用于直观输入

设置最小值,最大值,步进等

示例:

通过3个滑动条设置红、绿、蓝配色

章节四:信息学竞赛 CPP 应用篇 - 图61

  1. QColor color;
  2. int r = ui->horizontalSlider_2->value();
  3. int g = ui->horizontalSlider_3->value();
  4. int b = ui->horizontalSlider_4->value();
  5. color.setRgb(r,g,b);
  6. QPalette pall = ui->textEdit->palette();
  7. pall.setColor(QPalette::Base, color);
  8. ui->textEdit->setPalette(pall);
  • QProgressBar

用于显示进度,很常用

设置其各种属性

format %p% 百分比,%v%显示值,%m% 显示步数

要控制其显示的外观,不在属性里,Qt提供了更强大、更通用的方案:

样式表

在组作上,右键 | 改变样式表 …

  1. QProgressBar {
  2. border: 2px solid grey;
  3. border-radius: 5px;
  4. background-color: #FFFFFF;
  5. }
  6. QProgressBar::chunk {
  7. background-color: #05B8CC;
  8. width: 20px;
  9. }
  10. QProgressBar {
  11. border: 2px solid grey;
  12. border-radius: 5px;
  13. text-align: center;
  14. }

样式表的语法,与css几乎完全一样。

实时效果反馈

1. 在界面UI设计时,某组件的属性是abc,则设置它的函数名为:__

  • A set_abc

  • B setAbc

  • C SetAbc

  • D setabc

2. 在设计时改变组件的外观,除了修改属性,还可以:__

  • A 使用样式表

  • B 使用外部工具

  • C 使用布局

  • D 使用容器

答案

1=>B 2=>A

时间与日期

章节四:信息学竞赛 CPP 应用篇 - 图62

表示时间、日期的类型:

QTime, QDate, QDateTime

实际上QDateTime只是对QTime、QDate的聚合和封装

需要 include

  1. QDateTime t = QDateTime::currentDateTime();
  2. qDebug() << t.toString("yyyy.MM.dd hh:mm:ss.zzz ddd");
  3. QDateTime t2 = QDateTime::fromString("2020-01-20",
  4. "yyyy-MM-dd");
  5. qDebug() << t2.daysTo(t) << endl;
  6. // 时间戳
  7. uint a = t2.toTime_t();
  8. uint b = t.toTime_t();
  9. qDebug() << (b-a);
  10. int d = (b-a) / (24*60*60);
  11. qDebug() << d;
  12. // 相对日期
  13. QDateTime x = QDateTime::fromString("2000-1-1",
  14. "yyyy-M-d");
  15. x = x.addDays(-5);
  16. qDebug() << x.toString("yyyy-MM-dd");

章节四:信息学竞赛 CPP 应用篇 - 图63

  • 与QString的互转
  • 对两个时间点求差
  • 与时间戳互转
  • 求某一相对时间

currentDateTimeUtc() 返回utc国际标准时

对QDate操作:

  1. QDateTime a = QDateTime::fromString("2000-2-5","yyyy-M-d");
  2. qDebug() << a.date().dayOfWeek();
  3. qDebug() << a.date().dayOfYear();
  4. qDebug() << a.date().daysInMonth();
  5. qDebug() << a.date().daysInYear();
  6. qDebug() << a.date().weekNumber();
  7. qDebug() << "-----";
  8. qDebug() << a.date().year();
  9. qDebug() << a.date().month();
  10. qDebug() << a.date().day();

对QTime的操作:

  1. QTime t1 = QTime::currentTime();
  2. qDebug() << t1.hour();
  3. qDebug() << t1.minute();
  4. qDebug() << t1.second();
  5. QTime t2 = QTime::currentTime();
  6. uint d = t1.msecsTo(t2);
  7. qDebug() << d;

章节四:信息学竞赛 CPP 应用篇 - 图64

实时效果反馈

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

时间日期组件

章节四:信息学竞赛 CPP 应用篇 - 图65

编辑时间、日期的组件:

QTimeEdit, QDateEdit, QDateTimeEdit, QCalendarWidget

章节四:信息学竞赛 CPP 应用篇 - 图66

这几组件使用相仿。

我们想要实现的功能是:

在界面上,有一个QDateTimeEdit 组件,可以直接用它来实现日期、时间的输入。

也可以按旁边的按钮,出现一个日历组件,可以更方便地输入日期。

其实现原理是:

先把 calendar组件隐藏起来,点击时,让它显示出来,并移动到恰当的位置。

当选则了某个日期,再让calendar隐藏,把它的选中值放入到QDateTimeEdit组件中。

  1. void Widget::on_pushButton_clicked()
  2. {
  3. int x = ui->dateTimeEdit->geometry().x();
  4. int y = ui->dateTimeEdit->geometry().y();
  5. ui->calendarWidget->move(x,y);
  6. ui->calendarWidget->show();
  7. }
  8. void Widget::on_calendarWidget_selectionChanged()
  9. {
  10. ui->calendarWidget->hide();
  11. QDate t = ui->calendarWidget->selectedDate();
  12. ui->dateTimeEdit->setDate(t);
  13. }

QDateTimeEdit, QDateEdit 的主要属性: displayFormat

格式的写法与 QDateTime中介绍的一样

QDate 另一个重要属性:calendarPopup

如果勾选,会自动关联到日历组件

实时效果反馈

1. 关于QDateTimeEdit和QDateEdit, 说法正确的是:__

  • A QDateEdit 从 QDateTimeEdit继承

  • B QDateTimeEdit 从 QDateEdit继承

  • C QDateTimeEdit 聚合了QDateEdit

  • D QDateEdit 聚合了QDateTimeEdit

答案

1=>A

定时器

章节四:信息学竞赛 CPP 应用篇 - 图67

与时间、日期相关的概念,实际上有三个:

时间点(时刻),时间段(时间间隔),定时器

QTimer 提供了重复和单次信号触发的定时器。

与 sleep_for 不同,QTimer的计时在另一个线程,不会阻塞本线程

需要 include

  • 默认是重复触发
  1. Widget::Widget(QWidget *parent) :
  2. QWidget(parent),
  3. ui(new Ui::Widget)
  4. {
  5. ui->setupUi(this);
  6. timer = new QTimer(this);
  7. connect(timer, SIGNAL(timeout()),
  8. this, SLOT(my_doing()));
  9. }
  10. void Widget::on_pushButton_clicked()
  11. {
  12. timer->start(1000);
  13. ui->progressBar->setValue(0);
  14. ui->pushButton->setEnabled(false);
  15. }
  16. void Widget::my_doing()
  17. {
  18. QTime t = QTime::currentTime();
  19. ui->lcdNumber->display(t.hour());
  20. ui->lcdNumber_2->display(t.minute());
  21. ui->lcdNumber_3->display(t.second());
  22. ui->progressBar->setValue(ui->progressBar->value()+1);
  23. }
  24. void Widget::on_pushButton_2_clicked()
  25. {
  26. timer->stop();
  27. ui->pushButton->setEnabled(true);
  28. }
  • 如果希望触发一次,不用很麻烦,直接用:
  1. QTimer::singleShot(2000, [this]{
  2. ui->progressBar->setValue(100);
  3. });

实时效果反馈

1. 关于QTimer, 说法正确的是:__

  • A QTimer 等待一段时间,然后执行一个动作,相当于 sleep(间隔) 后执行

  • B QTimer 的等待在另一个线程中完成,所有操作在本线程中都立即返回

  • C QTimer 会使得本线程进入阻塞状态

  • D QTimer对应了操作系统的定时硬件

2. QTimer::singleShot函数的槽函数参数,可以是__

  • A 已有的槽函数

  • B 已有的成员函数

  • C 匿名函数

  • D 以上都可以

答案

1=>B 2=>D

QComboBox

image-20220426171437456

下拉列表框组件。提供一个下拉列表供用户选择,逻辑上也可以当作一个更方便使用的QLineEdit 组件来使用。

下拉列表的内容可以在设计时编辑,或者通过编码来加入。

可以使用图标,使得表达更丰富多彩些。

  • 在项目中引入资源文件

    资源文件中的“资源”指的是不变的常数

    比如:图片,图标,语音,多国语言文字的翻译等

    可以把资源文件先拷贝到项目的目录,再加入到资源文件中去管理。

    在编译连接后,资源文件会被连到exe文件里。

设计时使用资源

QCombobox 组件上,右键 | 编辑项目…

然后按 “属性” | icon | normal on … 从资源中选择

章节四:信息学竞赛 CPP 应用篇 - 图69

在界面上选择,会触发 activated 信号

无论界面或代码,都会触发 currentIndexChanged 等

使用代码填充

  1. ui->comboBox_2->clear();
  2. ui->comboBox_2->addItem("abc");
  3. ui->comboBox_2->addItem("abc2");
  4. QStringList sl = {"1111", "2222", "333333"};
  5. ui->comboBox_2->addItems(sl);

关联不可见数据

addItem 可以再多给一个参数,QVariant 类型

大致相当于常用类型的一个大union体

我们可以在用户选择了可见的文件后,获得这个关联的数据

  1. QMap<QString, uint> a;
  2. a.insert("张三丰", 1234);
  3. a.insert("李时珍", 1369);
  4. a.insert("克林顿", 8888);
  5. a.insert("王二小", 7777);
  6. for(auto i: a.keys()){
  7. ui->comboBox_3->addItem(i, a.value(i));
  8. }

实时效果反馈

1. 图标是一个小图像, 它最终被存在:__

  • A 以资源形式被连为exe文件中(用工具可提取出)

  • B 以静态库形式存在

  • C 以动态库形式存在

  • D 被编译为xml数据

2. 如果要在QComboBox中,保存用户自定义数据类型,需要用到:__

  • A Q_OBJECT 宏

  • B 继承 QObject 类

  • C 使用 QVariant 类型

  • D 使用 union 类型

答案

1=>A 2=>C

QListWidget

image-20220503213759844

Qt有两组用来处理项(item)的组件:

  • QListView, QTreeView, QTableView, QColumnView 等
  • QListWidget, QTreeWidget, QTableWidget

前一组是基于 Model/View 结构的,作为view与代表数据的Model Data对象联合工作。

后一组是对前一组的减化,从xxxView继承,把数据直接存在某一个项里。

可视化设计

在listWidget 组件上,右键 | 编辑项目,选某个项目,点击“属性”

章节四:信息学竞赛 CPP 应用篇 - 图71

如果已经在资源中加载了图片,

此处可以为每个项选择相应的图标资源。

用代码来生成项

所有在设计时产生的效果,都可以通过代码来实现。

经常是添加一些新的项:

  1. QIcon icon;
  2. icon.addFile(":/icon/win-icon/c1.ico");
  3. QListWidgetItem* item = new QListWidgetItem("苹果");
  4. item->setIcon(icon);
  5. item->setCheckState(Qt::Unchecked);
  6. item->setFlags(Qt::ItemIsUserCheckable | Qt::ItemIsEnabled);
  7. ui->listWidget->addItem(item);
  8. item = new QListWidgetItem("橘子");
  9. item->setIcon(icon);
  10. item->setCheckState(Qt::Checked);
  11. item->setFlags(Qt::ItemIsUserCheckable | Qt::ItemIsEnabled);
  12. ui->listWidget->addItem(item);

项的删除

删除当前选中的项:

  1. int i = ui->listWidget->currentRow();
  2. auto item = ui->listWidget->takeItem(i);
  3. delete item;

这里需要注意的是:

takeItem 只是在视觉上把 item 移除,但对象并没有释放,

需要手工调用 delete 才能真正删除一个项。

项的遍历

把勾选的项放入到plainTextEdit中。

  1. int n = ui->listWidget->count();
  2. for(int i=0; i<n; i++){
  3. auto it = ui->listWidget->item(i);
  4. if(it->checkState())
  5. ui->plainTextEdit->appendPlainText(it->text());
  6. }

实时效果反馈

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

image-20220505144036770

QTreeWidget 也是一种简化版本,它从QTreeView继承,把数据直接存在项(item)里。

它的节点可分为3种类型:顶层节点,分组节点,末端节点。

设计时建立树

QTreeWidget上 右键 | 编辑项目…

章节四:信息学竞赛 CPP 应用篇 - 图73

注意,QTreeWidgetItem对象代表一行

每个行可以含有多个列

可以设置一列的标题,并且设置为是否要显示出来

通过代码添加树

  1. ui->treeWidget->clear();
  2. ui->treeWidget->setHeaderHidden(true);
  3. auto it = new QTreeWidgetItem(ui->treeWidget, QStringList("目录"));
  4. new QTreeWidgetItem(it, QStringList("测试-1"));
  5. new QTreeWidgetItem(it, QStringList("测试-2"));
  6. new QTreeWidgetItem(it, QStringList("测试-3"));
  7. new QTreeWidgetItem(ui->treeWidget, QStringList("目录2"));
  8. ui->treeWidget->expandItem(it);

创建QTreeItem时,指定父组件,就可以挂接到组件系统,不需要进一步的显式添加。

增加子节点时,父节点的默认状态是折叠的。

有的时候,需要额外的数据,但并不需要显示出来,

此时,可以关联任意类型的用户数据。

  1. auto it = new QTreeWidgetItem(ui->treeWidget, QStringList("aaa"));
  2. it->setData(0, Qt::UserRole, QVariant(100));

子节点的遍历

  1. QTreeWidgetItemIterator it(ui->treeWidget);
  2. while(*it){
  3. if((*it)->childCount()==0)
  4. ui->plainTextEdit->appendPlainText((*it)->text(0));
  5. ++it;
  6. }

实时效果反馈

1. QTreeWidgetItem代表的是:__

  • A 一行数据

  • B 一列数据

  • C 一行中的一列数据

  • D 中间节点数据

2. 如果希望数据行携带用户自定义类型的信息,但并不显示出来,应该__

  • A 在行的属性中设为隐藏

  • B 把可显示列数设为1

  • C 调用该行的 setData

  • D 无法实现

答案

1=>A 2=>C

QTableWidget

image-20220506201305088

程序介绍

点击“设置表头”按钮,通过代码设置表头的样式、字体等。

点击“读入数据”按钮,从一个文本文件中读入数据,文本文件中的数据有多行,

每行的数据由tab分隔为多个项。

单击某个项的时候,弹出对话框,显示点击项的文本信息。

章节四:信息学竞赛 CPP 应用篇 - 图75

QTableWidgetItem

与QTreeWidget类似,此类是 QTreeView的简化版。

与QTreeWidgetItem不同,QTableWidgetItem 表示一个cell,而不是一个数据行。

可以通过 item(行号,列号), 或者 itemAt 来取得某个cell 对象。

需要注意,不管是标题,还是内容区,都是通过 QTreeWidgetItem来表示一个项。

所以,对它们的内容读写、格式设置等都是一样的。

表头的设置

  1. QStringList lst;
  2. lst << "头0" << "头1" << "头2";
  3. ui->tableWidget->setColumnCount(3);
  4. for(int i=0; i<3; i++){
  5. auto it = new QTableWidgetItem(lst[i]);
  6. QFont font = it->font();
  7. font.setBold(true);
  8. font.setPointSize(12);
  9. it->setTextColor(Qt::red);
  10. it->setFont(font);
  11. ui->tableWidget->setHorizontalHeaderItem(i, it);
  12. }

如果仅仅是设置文字信息,并不设置格式,则可以简化:

setHorizotalHeaderLabels

表格内容的设置

这里用了, ,需要include进来

比起c++标准的IO类,Qt的IO类使用起来,更加简单、方便,主要是与QString, QStringList等的结合更容易。

  1. ui->tableWidget->clearContents();
  2. QFile f("d:/t1.txt");
  3. f.open(QIODevice::ReadOnly | QIODevice::Text);
  4. QTextStream in(&f);
  5. in.setCodec("utf-8");
  6. while(!in.atEnd()){
  7. auto lst = in.readLine().split("\t");
  8. int row = ui->tableWidget->rowCount();
  9. ui->tableWidget->insertRow(row);
  10. for(int i=0; i<3; i++){
  11. ui->tableWidget->setItem(row, i, new QTableWidgetItem(lst[i]));
  12. }
  13. }

需要在指定的位置先生成一个文本文件: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的槽函数如下:

  1. QString s = ui->tableWidget->item(row, column)->text();
  2. 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

标准对话框

image-20220507092121884

对话框是弹出的窗体,可以有模态和非模态之分。

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的关于对话框

使用流程基本一致,仅举几个最常用的例子:

  • 打开一个文件

    1. QString afname = QFileDialog::getOpenFileName(this,
    2. "选个文件",
    3. "d:/", //初始位置
    4. "文本(*.txt);;图片(*.jpg *.png);;所有(*.*)");
    5. if(!afname.isEmpty())
    6. ui->plainTextEdit->appendPlainText(afname);
  • 选择颜色

    1. auto plt = ui->plainTextEdit->palette();
    2. QColor acolor = QColorDialog::getColor(
    3. plt.color(QPalette::Text),
    4. this,
    5. "选择颜色");
    6. if(acolor.isValid()){
    7. plt.setColor(QPalette::Text, acolor);
    8. ui->plainTextEdit->setPalette(plt);
    9. }
  • 输入整数

    1. bool ok = false;
    2. int iVal = QInputDialog::getInt(this,
    3. "输入整数",
    4. "请输入字体大小:",
    5. ui->plainTextEdit->font().pointSize(),
    6. 6, 40, 1,
    7. &ok);
    8. if(ok){
    9. QFont font = ui->plainTextEdit->font();
    10. font.setPointSize(iVal);
    11. ui->plainTextEdit->setFont(font);
    12. }
  • 从列表选择

    1. QStringList items;
    2. items << "黑龙江" << "吉林" << "辽宁" << "河北" << "山东";
    3. bool ok = false;
    4. QString text = QInputDialog::getItem(this,
    5. "选择项", "请选择省份",
    6. items, 0, true, &ok,
    7. Qt::MSWindowsFixedSizeDialogHint);
    8. if(ok && !text.isEmpty()){
    9. ui->plainTextEdit->appendPlainText(text);
    10. }
  • 输入文本

    1. bool ok = false;
    2. QString text = QInputDialog::getText(this,
    3. "输入串", "请输入串:",
    4. QLineEdit::Password, //::Normal
    5. "1234", &ok,
    6. Qt::MSWindowsFixedSizeDialogHint);
    7. if(ok && !text.isEmpty()){
    8. ui->plainTextEdit->appendPlainText(text);
    9. }
  • 询问确认

  1. auto res = QMessageBox::question(this, "消息", "保存不?",
  2. QMessageBox::Yes |
  3. QMessageBox::No | QMessageBox::Cancel,
  4. QMessageBox::NoButton);
  5. if(res==QMessageBox::Yes)
  6. ui->plainTextEdit->appendPlainText("yes");
  7. if(res==QMessageBox::No)
  8. ui->plainTextEdit->appendPlainText("no");
  9. if(res==QMessageBox::Cancel)
  10. 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

自定义对话框

image-20220507095350079

自定义的窗体是一个类,可以通过可视化设计。

可以让Qt帮助生成类的头文件和cpp文件的模板。

章节四:信息学竞赛 CPP 应用篇 - 图78

接下来选 Dialog 无按钮

章节四:信息学竞赛 CPP 应用篇 - 图79

然后,给定一个类名,系统自动生成配套文件 .h, .cpp, .ui

其中的 .ui 文件,可以进行可视化设计。

章节四:信息学竞赛 CPP 应用篇 - 图80

自己定义的对话框,可以显示为模态,也可以显示为非模态。

这里介绍模态用法:

直接调用对话框对象的 exec() 方法,

此时,对话框打开,并接收用户的输入,直到用户关闭对话框,

这个函数才执行结束。

换句话说,在对话框关闭之前,exec() 其后的语句并没有获得执行机会。

如果调用show() 方法,则可以弹出两种类型的对话框(模态,非模态),决定于对话框的model属性,这是一个bool值,调用exec()时会被忽略。

  • 一般说来,需要定义与对话框交互的函数

    1. QPoint MyDialog::getPoint() const
    2. {
    3. return QPoint(ui->spinBox->value(),
    4. ui->spinBox_2->value());
    5. }
    6. void MyDialog::init(QPoint pt)
    7. {
    8. ui->spinBox->setValue(pt.x());
    9. ui->spinBox_2->setValue(pt.y());
    10. }

这是用于初始化界面,以及从界面取回数据。

  • 如果是模态对话框,一般在栈上申请空间

    1. MyDialog dlg;
    2. dlg.init(QPoint(30,50));
    3. int ret = dlg.exec();
    4. if(ret==QDialog::Accepted){
    5. QPoint pt = dlg.getPoint();
    6. ui->plainTextEdit->appendPlainText(
    7. QString::asprintf("%d,%d", pt.x(), pt.y()));
    8. }

模态对话框一般用于输入一些信息,

经常是最后,让用户选“确认”或“放弃”按钮,这两个按钮的事件不需要自己写,

QDialog 已经提供了标准的槽函数。关联一下就可以。

可以把”确认”关联到 accept(),把“放弃”关联到 reject()

另外,对话框右上角的关闭按钮,也是关联到了 reject()

章节四:信息学竞赛 CPP 应用篇 - 图81

它们的行为都是关闭对话框,并使得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

与文件系统接口

image-20220509182449622

标准的选择文件对话框,虽然很方便,但无法深入与之交互。

比如,当点击某个文件的时候,如果是图片文件,在右边显示该图片及图片相关信息。

这是因为,该对话框是什么样子,有哪些功能,在一开始就定下来,

我们无法在它的上面再添加自己的组件。

这就启发我们:

用户界面在需求上很脆弱,容易发生变化,但,

其背后的数据逻辑较为稳定,不易变化。

表达文件系统的树结构,与显示这棵树的界面应该分离。

这样,数据逻辑这部分就可能被重用。

章节四:信息学竞赛 CPP 应用篇 - 图83

这里的Model对象,只表达数据的逻辑模型,与如何显示它没有关系。

同一个Model 可以配接上不同的视图,甚至可以同时连接多个视图。

如果仅仅是展示出Model中的数据,那么这么模型很简单,也很容易理解。

但,如果同时需要在视图上修改数据,再反映到Model中去,就比较头疼了,

Qt提供的方案是用代理。

我们在这里不深入讨论Model/View的原理

但提供一个具体的例子,看看最简单的Model/View 是如何工作的。

【需求】

  • 用一个树形结构展示文件系统
  • 当单击一个图片文件时,会显示该文件信息,并把图片显示在预览框中。

【设计】

Qt提供了用于文件系统的现成的Model:QFileSystemModel类

配合一个QTreeView来连接它。

用一个QPlainTextEdit 来显示文字信息

用一个QLabel来显示图片,需要配合 QPixmap(非可视化)

【实现】

  • 先在.h文件中添加声明:
  1. private:
  2. Ui::Widget *ui;
  3. QFileSystemModel* model;
  4. QPixmap pix;

model是指针,要等待本对象构造完成后,在 init 过程中申请对象

pix 是简单类,可以直接嵌入对象。

  • 还要为点击信号添加槽:
  1. private slots:
  2. void on_pushButton_2_clicked();
  3. void on_pushButton_3_clicked();
  4. void on_treeView_clicked(const QModelIndex &index);

前两个是ui设计时自动添加的,

第3个是手动加入的,注意,故意采用了规范命名法。

注意这里传递的 QModelIndex 类型是用来唯一标识Model中的一个Item

一个Model就是若于Item的集合,每个Item有唯一的标识

具体地说,它包含:行号、列号、父节点指针

  • 初始化过程:
  1. Widget::Widget(QWidget *parent) :
  2. QWidget(parent),
  3. ui(new Ui::Widget)
  4. {
  5. ui->setupUi(this);
  6. model = new QFileSystemModel(this);
  7. model->setRootPath("d:/");
  8. QStringList filter;
  9. filter << "*.jpg" << "*.png" << "*.gif";
  10. model->setNameFilters(filter);
  11. model->setNameFilterDisables(false);
  12. ui->treeView->setModel(model);
  13. // connect(ui->treeView, SIGNAL(clicked(QModelIndex)),
  14. // this, SLOT(on_treeView_clicked(QModelIndex)));
  15. }
  • 当点击了某个文件:
  1. if(!model->isDir(index)){
  2. ui->plainTextEdit->appendPlainText(model->filePath(index));
  3. ui->plainTextEdit->appendPlainText(model->fileName(index));
  4. ui->plainTextEdit->appendPlainText(model->type(index));
  5. pix.load(model->filePath(index));
  6. ui->label->setPixmap(pix);
  7. }
  • 当点击“缩放”
  1. void Widget::on_pushButton_2_clicked()
  2. {
  3. int w = ui->scrollArea->width();
  4. int h = ui->scrollArea->height();
  5. auto pix2 = ui->label->pixmap()->scaled(w,h);
  6. ui->label->setPixmap(pix2);
  7. }
  • 当点击“原大小”
  1. void Widget::on_pushButton_3_clicked()
  2. {
  3. ui->label->setPixmap(pix);
  4. }

实时效果反馈

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

文件系统

image-20220510192354705

很多软件都会用到对文件系统的访问。

这包括对文本文件、二进制文件的读写,对文件、目录的管理功能。

这些功能,c++很晚才推出平台无关的标准。

Qt提供的类库也是平台无关的。

最常用到的是:

文本文件的读写

最简单的用法是直接使用QFile类实现读写

  • 读入

    1. QFile f("d:/t1.txt");
    2. if(!f.exists()) return;
    3. if(!f.open(QIODevice::ReadOnly | QIODevice::Text)) return;
    4. QByteArray b = f.readAll();
    5. ui->plainTextEdit->setPlainText(b); //使用了默认的编码方式

    QIODevice::Text 为设置把信息作为文本来处理

    所谓文本观点,就是的观点,这对文本文件的换行处理方式的影响。

    不同的操作系统上,换行的方式不同,

    软件应该尽量兼容这些不同的方式。

    注意,readAll 返回的是字节序列,不是串。

    字节序列要转为串,就涉及了编码方式问题,处理不当,就会“乱码”

  • 再谈乱码

    章节四:信息学竞赛 CPP 应用篇 - 图85

    其实,所有的乱码都是一个原因:

    串 ==> 字节序列,或者 字节序列 ==> 串

    之一或者全部出错了

  1. QFile f("d:/t1-ansi.txt");
  2. if(!f.exists()) return;
  3. if(!f.open(QIODevice::ReadOnly | QIODevice::Text)) return;
  4. QByteArray by = f.readAll();
  5. ui->plainTextEdit->setPlainText(by);
  • 写出

    1. QFile f("d:/t1-x.txt");
    2. if(!f.open(QIODevice::WriteOnly | QIODevice::Text)) return;
    3. QString s = ui->plainTextEdit->toPlainText();
    4. //QByteArray b = s.toUtf8();
    5. QByteArray b =
    6. QTextCodec::codecForName("gbk")->fromUnicode(s);
    7. // 反向的转换是: toUnicode(QByteArray& ) => QString
    8. f.write(b);
  • 使用QTextStream

    QTextStream提供了流式的操作方法,更便捷,易于使用。

    并且,QTextStream 不仅可以对QIODevice,还可以对QString,QByteArray 操作,这会获得很大的灵活性。

    比如,读入,可以直接指定编码方式,还可以逐行读入。

    1. QFile f("d:/t1.txt");
    2. if(!f.open(QIODevice::ReadOnly | QIODevice::Text)) return;
    3. QTextStream in(&f);
    4. in.setCodec("utf-8");
    5. while(!in.atEnd()){
    6. QString s = in.readLine();
    7. ui->plainTextEdit->appendPlainText(s);
    8. }

对于写出,则还提供了细腻的格式控制

  1. QFile f("d:/t2.txt");
  2. if(!f.open(QIODevice::WriteOnly | QIODevice::Text)) return;
  3. QTextStream out(&f);
  4. out.reset();
  5. //out.setCodec("gbk");
  6. out.setCodec("utf-8");
  7. out.setFieldAlignment(QTextStream::AlignLeft);
  8. out.setPadChar('-');
  9. out.setFieldWidth(10);
  10. out << QString("中国") << "abc" << 100 << "\n";
  11. out.reset();
  12. 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)

image-20220516091315616

除了QTextStream,Qt还提供了一个重要的类:QDataStream

QDataStream 提供的是所谓序列化反序列化的功能

这个主题,也称为对象持久化

序列化:把对象以某种规则变成字节流

反序列化:从字节流中读出信息,重建对象

章节四:信息学竞赛 CPP 应用篇 - 图87

Qt自己的很多类,都支持了QDataStream的序列化,我们可以直接使用

比如:最常用的QString, qint32, qreal等

当然,c++的基本类型 int, double 等标准库类型是支持的。

如果为了跨平台使用,最好用明确的类型比如:qint32 来代替 int

注意:

序列化/反序列化 并非是简单地内存的内容直接转移到存储介质上,

如果这么做,就与机器/操作系统相关了。

序列化/反序列化过程要实现可以跨机器体系,跨操作系统的交叉操作。

  • 序列化

    1. QFile f("d:/t1.txt");
    2. if(!f.open(QIODevice::WriteOnly)) return;
    3. QDataStream out(&f);
    4. QString s("I am a 中文 test\n这是第22222行");
    5. qint32 a = 5;
    6. qreal b = 3.1475;
    7. out << s << a << b;
  • 反序列化

    1. QFile f("d:/t1.txt");
    2. if(!f.open(QIODevice::ReadOnly)) return;
    3. QDataStream in(&f);
    4. QString s;
    5. qint32 a;
    6. qreal b;
    7. in >> s >> a >> b;
    8. ui->plainTextEdit->appendPlainText(s);
    9. ui->plainTextEdit->appendPlainText(QString::number(a));
    10. ui->plainTextEdit->
    11. appendPlainText(QString::asprintf("%.2f", b));

自定义类型

我们自己定义的类,也很容易支持QDataStream

这是.h 文件:

  1. class CStudent
  2. {
  3. public:
  4. CStudent();
  5. CStudent(const char* name, qint32 age);
  6. QString show() const;
  7. friend QDataStream& operator<<(
  8. QDataStream& out, const CStudent& stu);
  9. friend QDataStream& operator>>(
  10. QDataStream& in, CStudent& stu);
  11. private:
  12. QString name;
  13. qint32 age;
  14. };

核心思想就是定义友元函数 operator<<,operator>>

这是在 .cpp 中的具体实现:

  1. CStudent::CStudent(const char *name, qint32 age)
  2. {
  3. this->name = name;
  4. this->age = age;
  5. }
  6. QString CStudent::show() const
  7. {
  8. QString s;
  9. QTextStream(&s) << QString("姓名:") << name
  10. << QString("\n年龄:") << age;
  11. return s;
  12. }
  13. QDataStream& operator<<(QDataStream &out, const CStudent &stu)
  14. {
  15. out << stu.name << stu.age;
  16. return out;
  17. }
  18. QDataStream& operator>>(QDataStream &in, CStudent &stu)
  19. {
  20. in >> stu.name >> stu.age;
  21. return in;
  22. }

有了这个类的基础工作,对CStudent的序列化就和Qt自带的类型没什么区别了。

  • 序列化过程

    1. QMap<int, CStudent> map;
    2. map[111] = CStudent("张三丰", 102);
    3. map[222] = CStudent("李时珍", 98);
    4. QFile f("d:/t2.txt");
    5. if(!f.open(QIODevice::WriteOnly)) return;
    6. QDataStream out(&f);
    7. out << map;

    Qt的强大之处,就在于 QMap, QList 等十分重要的基础类型,已经实现了序列化

    我们只要很少工作,就能融入到这个体系中。

  • 反序列化过程

    1. QFile f("d:/t2.txt");
    2. if(!f.open(QIODevice::ReadOnly)) return;
    3. QDataStream in(&f);
    4. QMap<int, CStudent> map;
    5. in >> map;
    6. 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)

image-20220516120058049

Qt 还提供了文件和目录操作的一些辅助类。

总结如下:

类名 功能
QCoreApplication 提取应用程序的路径,文件名等信息
QFile 除读写文件外,还可以复制、删除文件等
QFileInfo 提取文件信息,路径、文件名、后缀、大小、日期等
QDir 获取目录下的文件列表,创建、删除目录、改名等
QTemporaryDir, QTemporaryFile 用于创建临时目录或临时文件
QFileSystemWatcher 监听目录下的文件的添加、删除、修改等
  • 示例:取得应用路径信息

    QApplication 从 QCoreApplication继承,代表应用程序本身

    QCoreApplication还提供了很多静态方法,对程序信息进行访问

    1. QString path = QCoreApplication::applicationDirPath();
    2. QString name = QCoreApplication::applicationFilePath();
    3. QStringList lst = QCoreApplication::libraryPaths();
    4. ui->plainTextEdit->appendPlainText(path);
    5. ui->plainTextEdit->appendPlainText(name);
    6. ui->plainTextEdit->appendPlainText("libs:");
    7. for(QString s: lst)
    8. 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() | 文件大小 |

下面的示例,把当前文件备份,精心安排了复制后的文件名字

  1. QFile f("d:/t1.txt");
  2. if(!f.exists()) return;
  3. QFileInfo fi(f);
  4. QString base = fi.completeBaseName();
  5. QString suf = fi.suffix();
  6. QString path = fi.absolutePath();
  7. ui->plainTextEdit->appendPlainText(base);
  8. ui->plainTextEdit->appendPlainText(suf);
  9. ui->plainTextEdit->appendPlainText(path);
  10. for(int i=0; i<10; i++){
  11. QString fname;
  12. QTextStream out(&fname);
  13. out << path << base << ".bak" << i << "." << suf;
  14. ui->plainTextEdit->appendPlainText(fname);
  15. if(!QFile::exists(fname)){
  16. f.copy(fname);
  17. break;
  18. }
  19. }
  • 示例:列出目录下的文件

    QDir 提供了这个功能,还有其它的重要信息

    1. ui->plainTextEdit->appendPlainText(QDir::tempPath());
    2. ui->plainTextEdit->appendPlainText(QDir::rootPath());
    3. ui->plainTextEdit->appendPlainText(QDir::homePath());
    4. ui->plainTextEdit->appendPlainText(QDir::currentPath());
    5. QStringList lst = QDir("d:/").entryList();
    6. for(auto s: lst){
    7. ui->plainTextEdit->appendPlainText(s);
    8. }

实时效果反馈

1. 如果希望获得应用程序默认的Lib加载路径, 应该用哪个类:__

  • A QApplication

  • B QDir

  • C QFile

  • D QFileInfo

2. 如果希望获得文件的最后修改日期,应该用哪个类:__

  • A QApplication

  • B QDir

  • C QFile

  • D QFileInfo

答案

1=>A 2=>D

绘图

image-20220516182152418

Qt 对绘图提供了强大的支持,这里仅就2D基本绘图作介绍。

与大多图形系统一样,qt也使用相同的API在屏幕和和打印机等不同的设备上实现绘制。

绘制工作总是与三个类有关:

章节四:信息学竞赛 CPP 应用篇 - 图90

QPainter:

用来执行各种绘制的操作(比如:画矩形,写字,贴图片等)

QPaintDevice:

代表一个抽象的二维空间(不是具体的设备)

QPaintEngine:

提供对不同设备的接口能力,对一般的程序员是隐藏的。

  • 基本绘制过程
  1. QPainter painter(this); //this是绘图场地
  2. painter.drawLine(0, 0, 200, 200);
  3. painter.setPen(Qt::red);
  4. painter.drawRect(10, 10, 200, 100);
  • 绘制的时机

如果在一般的事件中,直接对屏幕绘制,并不太可取。

因为,多个程序共享屏幕,当某种原因,窗口区域需要重新绘制的时候,我们先前绘制上去的东西就没用了。

我们与其告诉应用“画的过程”,不如想办法让程序记住“画的过程”。

绘图设备的 paintEvent 函数在需要重绘的时候会自动调用。

我们可以重载这个函数,实现“稳定的”对窗口的重绘。

可以让IDE帮助我们写重载形式,避免“笔误”

注意,

函数声明尾上的:Q_DECL_OVERRIDE 宏

它可以根据编译器设置的c++标准版本,动态加入override关键字

  • 可以设置是否刷底色等信息
  1. Widget::Widget(QWidget *parent) :
  2. QWidget(parent),
  3. ui(new Ui::Widget)
  4. {
  5. ui->setupUi(this);
  6. setPalette(QPalette(Qt::white));
  7. setAutoFillBackground(true);
  8. }
  • 为控制绘制的观,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)

image-20220517090207942

我们在窗口上绘图的时候,默认的坐标是:

原点在左上角,x轴向右,y轴向下

章节四:信息学竞赛 CPP 应用篇 - 图92

我们可以通过一些函数的调用改变这个坐标系统:

函数 功能
translate(dx, dy) 坐标原点的平移操作
rotate(角) 坐标系统顺时针旋转一个角度
scale(rx, ry) 缩放比例
shear(sx, sy) 扭转变换
  • QPainter状态的保存与恢复

save()

保存当前的状态,也可以理解为当前的状态压入状态栈

restore()

从状态栈中弹出一个状态,就是恢复到刚才的状态

resetTransform()

复位所有的坐标变换到初始状态

【示例】

绘制五星,效果如下图:

章节四:信息学竞赛 CPP 应用篇 - 图93

还是需要重载 patinEvent,添加如下代码:

  1. QPainter painter(this);
  2. const qreal R = 100;
  3. const qreal PI = 3.14159;
  4. const qreal deg = PI * 72 / 180;
  5. QPoint points[5] = {
  6. QPoint(R, 0),
  7. QPoint(R*std::cos(deg), -R*std::sin(deg)),
  8. QPoint(R*std::cos(2*deg), -R*std::sin(2*deg)),
  9. QPoint(R*std::cos(3*deg), -R*std::sin(3*deg)),
  10. QPoint(R*std::cos(4*deg), -R*std::sin(4*deg)),
  11. };
  12. QFont font;
  13. font.setPointSize(12);
  14. font.setBold(true);
  15. painter.setFont(font);
  16. QPen pen;
  17. pen.setWidth(2);
  18. pen.setColor(Qt::blue);
  19. pen.setStyle(Qt::SolidLine);
  20. pen.setCapStyle(Qt::FlatCap);
  21. pen.setJoinStyle(Qt::BevelJoin);
  22. painter.setPen(pen);
  23. QBrush brush;
  24. brush.setColor(Qt::yellow);
  25. brush.setStyle(Qt::SolidPattern);
  26. painter.setBrush(brush);
  27. QPainterPath path;
  28. path.moveTo(points[0]);
  29. path.lineTo(points[2]);
  30. path.lineTo(points[4]);
  31. path.lineTo(points[1]);
  32. path.lineTo(points[3]);
  33. path.closeSubpath();
  34. path.addText(points[0], font, "0");
  35. path.addText(points[1], font, "1");
  36. path.addText(points[2], font, "2");
  37. path.addText(points[3], font, "3");
  38. path.addText(points[4], font, "4");
  39. painter.save();
  40. painter.translate(100,120);
  41. painter.drawPath(path);
  42. painter.drawText(0,0,"Star1");
  43. painter.restore();
  44. painter.translate(300,120);
  45. painter.scale(0.8,0.8);
  46. painter.drawPath(path);
  47. painter.drawText(0,0,"Star2");
  48. painter.resetTransform();
  49. painter.translate(500,120);
  50. painter.rotate(-145);
  51. painter.drawPath(path);
  52. 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)

image-20220517172215578

本节给出一个小示例:

按住鼠标移动,在窗体上涂鸦。

  • 重载函数,获得事件的处理机会

    1. void mousePressEvent(QMouseEvent *event);
    2. void mouseReleaseEvent(QMouseEvent *event);
    3. void mouseMoveEvent(QMouseEvent *event);
    4. void enterEvent(QEvent *event);
    5. void leaveEvent(QEvent *event);

    这里的QMouseEvent提供了额外的信息

    包括:在当前窗口中的位置,鼠标各个键的状态

  • 按住移动时,产生数据

    1. lst.append(QPoint(event->x(), event->y()));
    2. update();

    其中的 update, 会触发paintEvent的调用

    并非是直接调用paintEvent,而是产生一个重画的请求

  • 在paintEvent中完成绘制过程

    1. QPainter painter(this);
    2. painter.setPen(Qt::red);
    3. painter.setBrush(Qt::red);
    4. for(auto i: lst){
    5. painter.drawEllipse(i.x()-2, i.y()-2, 4, 4);
    6. }

说明:

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

图表

image-20220517202330701

Qt 提供了多套绘图体系

QPainter是最基本的一种,它是基于象素的绘图机制,在画布上绘制的内容混合为图像,无法区分。

Qt也有另一种基于GraphicsView架构的机制,绘制的每样东西都是独立的对象

这些对象可以被移动、复制、缩放等许多操作。

Qt Charts 就是基于 GraphicsView架构的

实现完的效果:

章节四:信息学竞赛 CPP 应用篇 - 图96

  • 准备

    .pro 文件中加入:

    QT += charts

    源文件中加入:

    include

    using namespace QtCharts

  • 在构造函数中直接代码加入chart功能

  1. ui->setupUi(this);
  2. QChartView* chartView = new QChartView(this);
  3. QChart* chart = new QChart();
  4. chart->setTitle("曲线");
  5. chartView->setChart(chart);
  6. this->setCentralWidget(chartView);
  7. QLineSeries* series0 = new QLineSeries();
  8. QLineSeries* series1 = new QLineSeries();
  9. series0->setName("sin曲线");
  10. series1->setName("cos曲线");
  11. chart->addSeries(series0);
  12. chart->addSeries(series1);
  13. qreal t=0,y1,y2,intv=0.1;
  14. int cnt = 100;
  15. for(int i=0; i<cnt; i++){
  16. y1 = qSin(t);
  17. series0->append(t, y1);
  18. y2 = qCos(t);
  19. series1->append(t, y2);
  20. t += intv;
  21. }
  22. QValueAxis* axisX = new QValueAxis;
  23. axisX->setRange(0, 10);
  24. axisX->setTitleText("time");
  25. QValueAxis* axisY = new QValueAxis;
  26. axisY->setRange(-2,2);
  27. axisY->setTitleText("value");
  28. chart->setAxisX(axisX, series0);
  29. chart->setAxisY(axisY, series0);
  30. chart->setAxisX(axisX, series1);
  31. chart->setAxisY(axisY, series1);
  • 可以帮助文档找到更多的series类型

    QBarSeries, QPieSeries,QAreaSeries, 。。。。

实时效果反馈

1. 对象的生存期管理, 说法正确的是:__

  • A QChartView 管理 QChart

  • B QChart 管理 QChartView

  • C QXXSeries 管理 QValueAxis

  • D QValueAxis 管理 QXXSeries

答案

1=>A

图表(2)

image-20220521091024961

本节以一个接近实用的例子,演示把一组数据通过最常用的柱状图表达出来。

章节四:信息学竞赛 CPP 应用篇 - 图98

  • 准备

    .pro 文件中加入 Qt += charts

    .h 中加入:

    1. #include <QtCharts>
    2. #include <QDebug> // 仅仅为了调试方便,可以不要
    3. using namespace QtCharts;

    在.h 中加入成员:

    1. private:
    2. Ui::MainWindow *ui;
    3. QChart* chart;
    4. QBarSeries* series;
  • 读入数据到QBarSet

    每个QBarSet对象表示一组数据,其数据为double

    序列下标为x值,序列值为y值

    1. QBarSet* bs0 = new QBarSet("A");
    2. QBarSet* bs1 = new QBarSet("B");
    3. QBarSet* bs2 = new QBarSet("C");
    4. QBarSet* bs3 = new QBarSet("D");
    5. QFile file("d:/zhutu.txt");
    6. if (!file.open(QIODevice::ReadOnly | QIODevice::Text))
    7. return;
    8. QTextStream in(&file);
    9. while (!in.atEnd()) {
    10. QStringList lst = in.readLine().trimmed().split(" ");
    11. bs0->append(lst[0].toInt());
    12. bs1->append(lst[1].toInt());
    13. bs2->append(lst[2].toInt());
    14. bs3->append(lst[3].toInt());
    15. }

    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);

  1. QBarSeries 代表柱图
  2. QChart 图表,其上可以放多个XXXSeries
  3. - QChartView 是一种Widget
  4. 它不能直接放在另一个Widget上面,
  5. 但可以放在一个Layout上面
  6. ```c++
  7. QChartView *chartView = new QChartView(chart);
  8. ui->verticalLayout->addWidget(chartView);

设计时,主窗体已经采用了 Vertical布局

如果不做特殊设置,chart 上是没有显示坐标的。

我们可以自己设置一个定制化的坐标轴:

  1. QStringList x;
  2. x << "3月" << "4月" << "5月" << "6月" << "7月" << "8月";
  3. QBarCategoryAxis *axis = new QBarCategoryAxis();
  4. axis->append(x);
  5. chart->createDefaultAxes();
  6. chart->setAxisX(axis, series);

还可以设置主题:

  1. chart->setTheme(QChart::ChartThemeBlueCerulean);
  2. 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

访问数据库

image-20220522072355340

QtSql模块提供了对数据库访问的支持。

这里指传统的关系型数据库,mysql, ms-sqlserver, oracle, postgresql 等常见数据库

Qt内置了对 sqlite 的完全支持,其它的数据库可能需要相应的 dll 文件。

此处假设已经具备了基本的关系数据库和sql基础知识。

首先,需要在 .pro 文件中加入对sql的支持:

  1. QT += sql

需要访问数据库的地方,include

这样,会引入大量的常用类型,如果希望精确include,也可以分别include 相应的类

QtSql模块中的类,大体上可以分为三个层次:

章节四:信息学竞赛 CPP 应用篇 - 图100

Qt已经内置了一些常用的驱动

我们可看一下都有哪些驱动:

  1. int main(int argc, char *argv[])
  2. {
  3. QApplication a(argc, argv);
  4. Widget w;
  5. w.show();
  6. QStringList ds = QSqlDatabase::drivers();
  7. for(auto i: ds)
  8. qDebug() << i;
  9. return a.exec();
  10. }

Qt 对Sqlite 数据库的支持最好,所有的功能完全内置。

对postgresql, mysql 支持也很完整

一般有两种方式连接到数据库:

一是直接用该数据库的驱动来连接,

另一个方式是通过 ODBC 驱动。ODBC是微软提供的统一的数据库访问方式。

  • QSqlDatabase

代表到某个数据库的连接,以连接Sqlite为例。

Sqlite 相对简单,只单一的文件,运行速度快,文档丰富。

  1. QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE");
  2. db.setDatabaseName("d:/my.db3");
  3. if(!db.open()) return;
  4. ui->plainTextEdit->appendPlainText("打开成功!");
  5. QSqlQuery query;
  6. QString s = "create table stu(id int primary key, name varchar)";
  7. if(!query.exec(s)) return;
  8. ui->plainTextEdit->appendPlainText(s + " 执行成功");

这样会打开已有的数据库,或建立新的数据库。

  • QSqlQuery

这个类可用于执行sql语句

.exec(sql语句), 返回结果表示是否成功

  1. QSqlQuery query;
  2. QString s = "create table stu(id int primary key, name varchar)";
  3. if(!query.exec(s)) return;
  4. ui->plainTextEdit->appendPlainText(s + " 执行成功");

可以用同样的方法,添加记录:

  1. QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE");
  2. db.setDatabaseName("d:/my.db3");
  3. if(!db.open()) return;
  4. ui->plainTextEdit->appendPlainText("打开成功!");
  5. QSqlQuery query;
  6. QString s = "insert into stu(id, name) values(1100, '张三丰')";
  7. if(query.exec(s))
  8. ui->plainTextEdit->appendPlainText(s + " 执行成功");
  9. s = "insert into stu(id, name) values(1101, '李时珍')";
  10. if(query.exec(s))
  11. ui->plainTextEdit->appendPlainText(s + " 执行成功");
  12. s = "insert into stu(id, name) values(1105, '克林顿')";
  13. if(query.exec(s))
  14. ui->plainTextEdit->appendPlainText(s + " 执行成功");

除了执行sql命令,也可以执行查询语句,这样可处理结果集:

  1. QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE");
  2. db.setDatabaseName("d:/my.db3");
  3. if(!db.open()) return;
  4. ui->plainTextEdit->appendPlainText("打开成功!");
  5. QSqlQuery query;
  6. query.exec("select * from stu");
  7. while(query.next()){
  8. int id = query.value("id").toInt();
  9. QString name = query.value("name").toString();
  10. ui->plainTextEdit->appendPlainText(QString::number(id)
  11. + "," + name);
  12. }

实时效果反馈

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

参数化执行

image-20220522140753406

如果多次执行数据库相关操作,

显然,每次都重新打开数据库,并不高效(虽然sqlite相当快)

可以让QSqlDatabase全局有效。

在.h中:

  1. private:
  2. Ui::MainWindow *ui;
  3. QSqlDatabase db;

然后,在.cpp中对 db 进行初始化。

  1. MainWindow::MainWindow(QWidget *parent) :
  2. QMainWindow(parent),
  3. ui(new Ui::MainWindow)
  4. {
  5. ui->setupUi(this);
  6. QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE");
  7. db.setDatabaseName("d:/my.db3");
  8. if(db.open())
  9. ui->plainTextEdit->appendPlainText("db打开成功!");
  10. else
  11. ui->plainTextEdit->appendPlainText("db打开失败!");
  12. QSqlQuery q;
  13. q.exec("create table stu(id int primary key, name varchar)");
  14. }

以 insert 语句为例,若要从界面上取得数据,插入表中。

最朴素的办法是拼 sql 串,这是最基本的方式,只要sql语句能描述的事情,都可以实现。

  1. QSqlQuery query;
  2. QString s = "insert into stu(id, name) values(" +
  3. QString::number(ui->spinBox->value()) +
  4. ", '" +
  5. ui->lineEdit->text() +
  6. "')";
  7. if(query.exec(s))
  8. ui->plainTextEdit->appendPlainText(s + " insert 执行成功");
  9. else
  10. ui->plainTextEdit->appendPlainText(s + " insert 执行失败");

这个方法至少2个缺点:

  1. 拼接过程繁琐,容易出错
  2. 有sql注入攻击的风险

使用参数化的方式,可以在一定程度上避免这些问题

  1. QSqlQuery query;
  2. query.prepare("insert into stu(id, name) values(:id, :name)");
  3. query.bindValue(":id", ui->spinBox->value());
  4. query.bindValue(":name", ui->lineEdit->text());
  5. if(query.exec())
  6. ui->plainTextEdit->appendPlainText(" insert 执行成功");
  7. else
  8. ui->plainTextEdit->appendPlainText(" insert 执行失败");

如果参数个数少,也可以更简单:

  1. QSqlQuery query;
  2. query.prepare("insert into stu(id, name) values(?, ?)");
  3. query.bindValue(0, ui->spinBox->value());
  4. query.bindValue(1, ui->lineEdit->text());
  5. if(query.exec())
  6. ui->plainTextEdit->appendPlainText(" insert 执行成功");
  7. else
  8. ui->plainTextEdit->appendPlainText(" insert 执行失败");

甚至序号也可以省略

  1. QSqlQuery query;
  2. query.prepare("insert into stu(id, name) values(?, ?)");
  3. query.addBindValue(ui->spinBox->value());
  4. query.addBindValue(ui->lineEdit->text());
  5. if(query.exec())
  6. ui->plainTextEdit->appendPlainText(" insert 执行成功");
  7. else
  8. ui->plainTextEdit->appendPlainText(" insert 执行失败");

使用参数化的方式,还有一个好处,当有一大批相同模式的 sql 要执行时,可以提高效率。

  1. QSqlQuery q;
  2. q.prepare("insert into stu(id, name) values(?, ?)");
  3. QVariantList ints;
  4. ints << 2001 << 2002 << 2003;
  5. q.addBindValue(ints);
  6. QVariantList names;
  7. names << "AAAA" << "BBB" << "CC";
  8. q.addBindValue(names);
  9. if(q.execBatch())
  10. ui->plainTextEdit->appendPlainText(" insert 执行成功");
  11. else
  12. 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信息

image-20220522164755029

当连接数据库后,我们可能会希望知道:数据库中哪些表?每个表有哪些字段?每个字段的名字、类型等信息,这些一般称为:meta info

  • 取得所有的表名很容易:

    1. QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE");
    2. db.setDatabaseName("d:/my.db3");
    3. if(!db.open())
    4. qDebug() << db.lastError();
    5. else
    6. qDebug() << db.tables();
  • 表结构信息

当与其它系统接口时,或在调试、诊断等时候,可能会需要知道select 的结果集的结构,

这也是重要的 meta 信息。无论查询的结果集有没有记录,都可以获得这个信息。

因此,如果只想取得结果集的 meta 信息,会故意让结果集选不到记录。

  1. QSqlQuery q("select * from stu where 0");
  2. QSqlRecord r = q.record();
  3. for(int i=0; i<r.count(); i++){
  4. ui->plainTextEdit->appendPlainText(r.fieldName(i));
  5. qDebug() << r.field(i);
  6. }

承载meta信息的是 QField类的实例。它有丰富的函数来获得十分详尽的信息。

章节四:信息学竞赛 CPP 应用篇 - 图103

QField的成员函数都很简单,基本是见名知意。

.type() 返回的是 QVariant::Type 类型,是一系列的枚举值。Qt支持的类型很丰富。

详细可以参考qt文档。

实时效果反馈

1. 含有字段元信息的类是:__

  • A QSqlRecord

  • B QSqlQuery

  • C QSqlField

  • D QSqlMeta

答案

1=>C

blob字段

image-20220523195029515

blob字段中存储的是二进制的原始信息,不去解析其中的内容。

在编程时,可表现为 QByteArray,即:字节序列。

blob 字段适用于存储图片、附件、简介等较大的信息。

blob字段内容一般是从文件中读入的。

章节四:信息学竞赛 CPP 应用篇 - 图105

其工作原理的核心是:QByteArray,即字节序列。

我们只要设法把其它位置的对象等转化为QByteArray,以后事情就很容易。

只要把QByteArray 变成 QVariant,就可以与数据库中的数据交互了。

【示例】

以下示例演示把一个图片存入数据库中,

然后,把它从数据库读出并显示出来。

  • 建表

首先,需要在建立表的时候,指定为 blob 类型的字段。

  1. QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE");
  2. db.setDatabaseName("d:/my.db3");
  3. db.open();
  4. QSqlQuery q;
  5. q.exec("create table test(name varchar primary key, data blob)");
  6. qDebug() << db.tables();
  • 从文件中读入图片数据
  1. QString afname = QFileDialog::getOpenFileName(this,
  2. "选个图片",
  3. "d:/", //初始位置
  4. "图片(*.jpg *.png);;");
  5. if(!afname.isEmpty()){
  6. QPixmap pix;
  7. pix.load(afname);
  8. ui->label->setPixmap(pix);
  9. }
  • insert blob 型
  1. QSqlQuery q;
  2. q.prepare("insert into test(name, data) values(?,?)");
  3. q.addBindValue(ui->lineEdit->text());
  4. QByteArray bytes;
  5. QBuffer buf(&bytes);
  6. buf.open(QIODevice::WriteOnly);
  7. ui->label->pixmap()->save(&buf, "png");
  8. q.addBindValue(bytes);
  9. qDebug() << q.exec();

当然,也可以从文件中直接读出为 QByteArray

可以用 QFile::readAll() ,或者用 QDataStream, QBuffer 等

此处,是为了把不同格式的图片入库后,变成统一的格式。

  • 从数据库读出
  1. QSqlQuery q;
  2. q.prepare("select data from test where name=?");
  3. q.addBindValue(ui->lineEdit->text());
  4. if(!q.exec()) ui->label->clear();
  5. if(!q.next()) ui->label->clear();
  6. QPixmap pix;
  7. pix.loadFromData(q.value(0).toByteArray());
  8. 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

使用事务

image-20220524194511032

使用事务(transaction)可以保证一组相关的动作,要么全部完成,要么原封不动。

大多数的数据库系统都会支持事务。

如要查询本系统是否支持transaction,

可以询问驱动实例,它的类是从 QSqlDriver 继承的。

  1. QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE");
  2. db.setDatabaseName("d:/my.db3");
  3. db.open();
  4. qDebug() << db;
  5. QSqlQuery qu("create table stu(id int primary key, name varchar)");
  6. qDebug() << db.driver()->hasFeature(QSqlDriver::Transactions);
  7. qDebug() << db.driver()->hasFeature(QSqlDriver::BLOB);

事务主要由 3 个动作构成:

  • 开始事务

    QSqlDatabase::transaction()

  • 提交(确认)

    QSqlDatabase::commit()

  • 回滚(取消自开始事务以来的所有修改)

    QSqlDatabase::rollback()

以一组 insert 语句为例,若其中有执行失败的情况,则全部回滚。

  1. QStringList lst;
  2. lst << "insert into stu(id, name) values(2001,'张三丰')"
  3. << "insert into stu(id, name) values(1001,'李时珍')"
  4. << "insert into stu(id, name) values(3001,'克林顿')";
  5. QSqlDatabase::database().transaction();
  6. bool ok = true;
  7. QSqlQuery query;
  8. for(auto i: lst){
  9. if(!query.exec(i)){
  10. ok =false;
  11. break;
  12. }
  13. }
  14. if(ok)
  15. QSqlDatabase::database().commit();
  16. else
  17. QSqlDatabase::database().rollback();

在工程实践,此种方式仍有缺陷:

一般,sql语句不是固定的串,而是各种运算后的结果,

如果,在生成这些sql的串中,或别的什么环节出了差错,就会导致部分修改。

事务最后是默认提交的。

这可以通过 异常 捕获来处理:

  1. QStringList lst;
  2. lst << "insert into stu(id, name) values(4001,'张三丰')"
  3. << "insert into stu(id, name) values(4002,'李时珍')"
  4. << "insert into stu(id, name) values(1002,'err')";
  5. QSqlDatabase::database().transaction();
  6. try{
  7. QSqlQuery query;
  8. for(auto i: lst){
  9. if(!query.exec(i)) throw -1;
  10. }
  11. QSqlDatabase::database().commit();
  12. }
  13. catch(...){
  14. QSqlDatabase::database().rollback();
  15. }

此外,还要注意:

QSqlDatabase::database() 表示获得当前连接对象

它与 db 并不完全等同。

为了线程安全等考虑,qt 是用连接名来识别不同的连接的。如果没名字,就是默认连接。

可以看下面的实验:

  1. qDebug() << db.tables();
  2. 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

image-20220526140943270

Qt为了简化数据库的操作,同时,也为了尽量不涉及SQL语法,提供了多个model类型,它们可以配合TableView, ListView等使用,大大方便基础应用的开发。

  • QSqlQueryModel

    对QSqlQuery的封装,只读的

  • QSqlTableModel

    从QSqlQueryModel继承,支持修改

  • QSqlRelationalTableModel

    从QSqlTableModel继承,支持外键

建立model

首先,还是要 .pro 中增加 sql,头文件中加入

连接数据库,创建一个stu表,增加一些记录:

  1. db = QSqlDatabase::addDatabase("QSQLITE");
  2. db.setDatabaseName("d:/my.db3");
  3. db.open();
  4. QSqlQuery q;
  5. q.exec("create table stu(id int primary key, name varchar)");
  6. q.exec("insert into stu(id, name) values(1001,'张三丰')");
  7. q.exec("insert into stu(id, name) values(1002,'李时珍')");
  8. q.exec("insert into stu(id, name) values(1003,'克林顿')");
  9. q.exec("insert into stu(id, name) values(2001,'aaaa')");
  10. q.exec("insert into stu(id, name) values(2002,'bbb')");
  11. q.exec("insert into stu(id, name) values(2003,'ccccc')");

且,新增一个成员变量,QSqlQueryModel 指针,初始为: NULL

然后,适当时机创建 model

  1. if(model) return;
  2. model = new QSqlQueryModel(this);
  3. model->setQuery("select * from stu");
  4. model->setHeaderData(0, Qt::Horizontal, tr("学号"));
  5. model->setHeaderData(1, Qt::Horizontal, tr("姓名"));

注意内存管理问题,不要泄漏。

此处还设了列标题

model 与 view 的关联

建立连接很简单,还可以顺便设置一些显示特征

  1. ui->tableView->setModel(model);

这里要注意,如果曾经关联了model,这样会放弃与旧model的关联,

但,旧model对象不会自动 delete

因为一个 model 可能关联到多个视图

  1. ui->tableView->verticalHeader()->setHidden(true);
  2. ui->tableView->setSelectionBehavior(
  3. QAbstractItemView::SelectRows);
  4. ui->tableView->setSelectionMode(
  5. QAbstractItemView::SingleSelection);
  6. ui->tableView->setAlternatingRowColors(true);

取得model中的数据

这里有个关键概念: QModelIndex

model index 并非是QSqlQueryModel中独有的概念,

在 model/view 体系中,任何model都有 model index的概念。

这个 index 是一组数据,用来唯一地确定model中的一个数据项。

它包含的信息主要是 3 个:(行号,列号,父节点号)

对QSqlQuery而言:在整行选中的模式下,最重要的数据就是:行号。

  1. QModelIndex index = ui->tableView->currentIndex();
  2. qDebug() << index.data();
  3. qDebug() << index.row();
  4. 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)

image-20220526142119724

QSqlQueryModel 提供的是一个只读数据集,

但,一般的应用都是有增、删、改、查的功能,只提供“查”是不够的。

实际上,所谓QSqlQueryModel只读,是说它无法自动实现增、删、改的功能,

我们通过手工添加代码,来执行sql语句,当然是可以增删改的。

实现目标

章节四:信息学竞赛 CPP 应用篇 - 图109

初始状态:tableview 连到 model,可以展现数据

单击某条记录,右边对应展示相应字段的信息。

可以修改组件中值,点击下面的动作按钮,实现增删改的功能。

需要认识一个新的类:

  • QDataWidgetMapper

    首先,需要用一个QDataWidgetMapper对象,从QSqlQueryModel把当前行的数据映射到一个可编辑的组件。

    因为这个Model是只读的,所以,映射是单向的。

.h 中定义

  1. QSqlDatabase m_db;
  2. QSqlQueryModel* m_model = nullptr;
  3. QDataWidgetMapper* m_mapper = nullptr;

初始化

  1. //准备数据库
  2. {
  3. m_db = QSqlDatabase::addDatabase("QSQLITE");
  4. m_db.setDatabaseName("d:/my.db3");
  5. m_db.open();
  6. QSqlQuery q;
  7. q.exec("create table stu(id int primary key, name varchar)");
  8. q.exec("insert into stu(id, name) values(1001,'张三丰')");
  9. q.exec("insert into stu(id, name) values(1002,'李时珍')");
  10. q.exec("insert into stu(id, name) values(1003,'克林顿')");
  11. q.exec("insert into stu(id, name) values(2001,'aaaa')");
  12. q.exec("insert into stu(id, name) values(2002,'bbb')");
  13. q.exec("insert into stu(id, name) values(2003,'ccccc')");
  14. }
  15. //建model
  16. {
  17. m_model = new QSqlQueryModel(this);
  18. m_model->setQuery("select * from stu");
  19. m_model->setHeaderData(0, Qt::Horizontal, tr("学号"));
  20. m_model->setHeaderData(1, Qt::Horizontal, tr("姓名"));
  21. }
  22. //关联view
  23. {
  24. ui->tableView->setModel(m_model);
  25. ui->tableView->verticalHeader()->setHidden(true);
  26. ui->tableView->setSelectionBehavior(
  27. QAbstractItemView::SelectRows);
  28. ui->tableView->setSelectionMode(
  29. QAbstractItemView::SingleSelection);
  30. ui->tableView->setAlternatingRowColors(true);
  31. }
  32. // mapper
  33. {
  34. m_mapper = new QDataWidgetMapper(this);
  35. m_mapper->setModel(m_model);
  36. m_mapper->setSubmitPolicy(QDataWidgetMapper::AutoSubmit);
  37. m_mapper->addMapping(ui->lineEdit, 0);
  38. m_mapper->addMapping(ui->lineEdit_2, 1);
  39. }

tableview 点击

实际上是改变了当前行,需要把数据刷到右边。

  1. m_mapper->setCurrentIndex(index.row());

增删改

  • 增加

    1. QSqlQuery q;
    2. q.prepare("insert into stu(id, name) values(?,?)");
    3. q.addBindValue(ui->lineEdit->text().toInt());
    4. q.addBindValue(ui->lineEdit_2->text());
    5. qDebug() << q.exec();
    6. m_model->setQuery("select id, name from stu");
  • 修改

    1. QSqlQuery q;
    2. q.prepare("update stu set name=? where id=?");
    3. q.addBindValue(ui->lineEdit_2->text());
    4. q.addBindValue(ui->lineEdit->text().toInt());
    5. qDebug() << q.exec();
    6. m_model->setQuery("select id, name from stu");
  • 删除

    1. QSqlQuery q;
    2. q.prepare("delete from stu where id=?");
    3. q.addBindValue(ui->lineEdit->text().toInt());
    4. qDebug() << q.exec();
    5. 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

image-20220527084703939

只读的 QSqlQueryModel 虽然说也可以实现增删改,但比较繁琐,

尤其是对不熟悉sql的开发者。

很多时候,我们的目标只是去维护一个表,并不是复杂的select查询,

此时,如能尽量自动化,就可以提高开发效率,降低开发难度。

QSqlTableModel为此目的而生。它的两个主要目的

  • 可以响应界面的编辑动作,就是说可读可写
  • 尽量避免直接操纵sql语句

QSqlTableModel 从 QSqlQueryModel 继承过来,关联到一个表,可以把它想象为一个表格,行对应记录,列对应字段。

【示例】

章节四:信息学竞赛 CPP 应用篇 - 图111

可以直接在tableView上作修改,按”提交”写到数据库,按“取消“则恢复原来的数据。

此外,还提供了过滤和排序的功能。

实现步骤

  • 建立model,关联到tableView

    1. //准备数据库
    2. {
    3. m_db = QSqlDatabase::addDatabase("QSQLITE");
    4. m_db.setDatabaseName("d:/my.db3");
    5. m_db.open();
    6. QSqlQuery q;
    7. q.exec("create table stu(id int primary key, name varchar)");
    8. q.exec("insert into stu(id, name) values(1001,'张三丰')");
    9. q.exec("insert into stu(id, name) values(1002,'李时珍')");
    10. q.exec("insert into stu(id, name) values(1003,'克林顿')");
    11. q.exec("insert into stu(id, name) values(2001,'aaaa')");
    12. q.exec("insert into stu(id, name) values(2002,'bbb')");
    13. q.exec("insert into stu(id, name) values(2003,'ccccc')");
    14. }
    15. //建model
    16. {
    17. m_model = new QSqlTableModel(this);
    18. m_model->setTable("stu");
    19. m_model->setEditStrategy(QSqlTableModel::OnManualSubmit);
    20. m_model->setHeaderData(0, Qt::Horizontal, tr("学号"));
    21. m_model->setHeaderData(1, Qt::Horizontal, tr("姓名"));
    22. m_model->select();
    23. }
    24. //关联view
    25. {
    26. ui->tableView->setModel(m_model);
    27. ui->tableView->setAlternatingRowColors(true);
    28. }

这里比较需要注意的是:

m_model->setEditStrategy(QSqlTableModel::OnManualSubmit)

这是对model设置的编辑策略:

常量 含义
QSqlTableModel::OnManualSubmit 所有的修改都是缓存的,并不真正更新
等待调submitAll() 或 revertAll()
QSqlTableModel::OnRowChange 行变化时自动提交
QSqlTableModel::OnFieldChange Cell变更时自动提交
  • 整体提交

    如果希望支持完整提交,要用到事务和回滚

    1. m_model->database().transaction();
    2. if (m_model->submitAll()) {
    3. m_model->database().commit();
    4. } else {
    5. m_model->database().rollback();
    6. qDebug() << m_model->lastError();
    7. }
  • 设置过滤

    1. QString s = QString("name like '%1%'")
    2. .arg(ui->lineEdit->text());
    3. m_model->setFilter(s);

其实就是拼凑了sql语句后的 where 条件而已。

此处用了模糊匹配

  • 设置排序规则

    1. m_model->setSort(0, Qt::AscendingOrder);
    2. //m_model->setSort(0, Qt::DescendingOrder);
    3. m_model->select();
  • 删除

    1. m_model->removeRow(ui->tableView->currentIndex().row());
    2. m_model->submitAll();

这里要注意,当前行是view 的概念,不是model的概念

  • 添加

    1. 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代理

image-20220527184907808

使用简单的 QSqlTableModel,我们很快就会遇到一个问题:

有些字段的值,并非随意填写,而是在一个列表中选择。

这个需求十分普遍。

很希望双击单元格就会出现一个ComboBox下拉组件,提供便利的选择。

比如:性别,院系 等信息。

【目标】

章节四:信息学竞赛 CPP 应用篇 - 图113

刚开始的时候,使用QSqlTableMoel 和 QTableView 来实现一个基本的应用。

点击 <增加代理>,会在 sex, dep 列设置自己的代理。

这样,这两个列的信息,也可以输入,也可以下拉选择,增加了方便。

【准备】

与前几讲的例子一样,还是要准备好数据库,准备好model,连接到view

  1. ui->setupUi(this);
  2. m_db = QSqlDatabase::addDatabase("QSQLITE");
  3. m_db.setDatabaseName("d:/my1.db3");
  4. m_db.open();
  5. QSqlQuery q;
  6. q.exec("create table stu(id int primary key,"
  7. "name varchar,"
  8. "birth date,"
  9. "sex char(10),"
  10. "dep char(20))");
  11. q.exec("insert into stu(id, name, birth, sex, dep)"
  12. " values(1001, '唐长老', '1789-3-1', '男', '数学系')");
  13. m_model = new QSqlTableModel(this);
  14. m_model->setTable("stu");
  15. m_model->select();
  16. ui->tableView->setModel(m_model);
  17. ui->tableView->setAlternatingRowColors(true);
  18. ui->tableView->setColumnWidth(0,10*10);
  19. ui->tableView->setColumnWidth(3,4*10);

【自定义代理类】

定义代理类需要从QStyledItemDelegate 或者 QAbstractItemDelegate 继承。

对初学者,区别不大。

然后要override 四个 virtual 函数。

.h

  1. class QMyComboDelegate : public QStyledItemDelegate
  2. {
  3. Q_OBJECT
  4. public:
  5. QMyComboDelegate(QObject* parent=0);
  6. // QAbstractItemDelegate interface
  7. public:
  8. QWidget *createEditor(QWidget *parent,
  9. const QStyleOptionViewItem &option,
  10. const QModelIndex &index)
  11. const Q_DECL_OVERRIDE;
  12. void setEditorData(QWidget *editor,
  13. const QModelIndex &index)
  14. const Q_DECL_OVERRIDE;
  15. void setModelData(QWidget *editor,
  16. QAbstractItemModel *model,
  17. const QModelIndex &index)
  18. const Q_DECL_OVERRIDE;
  19. void updateEditorGeometry(QWidget *editor,
  20. const QStyleOptionViewItem &option,
  21. const QModelIndex &index)
  22. const Q_DECL_OVERRIDE;
  23. QStringList lst;
  24. };

.cpp

  1. QMyComboDelegate::QMyComboDelegate(QObject* parent)
  2. :QStyledItemDelegate(parent)
  3. {
  4. }
  5. QWidget *QMyComboDelegate::createEditor(QWidget *parent,
  6. const QStyleOptionViewItem &option,
  7. const QModelIndex &index) const
  8. {
  9. QComboBox* box = new QComboBox(parent);
  10. box->addItems(lst);
  11. box->setEditable(true);
  12. return box;
  13. }
  14. void QMyComboDelegate::setEditorData(QWidget *editor,
  15. const QModelIndex &index) const
  16. {
  17. QString s = index.model()->data(index,
  18. Qt::EditRole).toString();
  19. static_cast<QComboBox*>(editor)->setCurrentText(s);
  20. }
  21. void QMyComboDelegate::setModelData(QWidget *editor,
  22. QAbstractItemModel *model,
  23. const QModelIndex &index) const
  24. {
  25. QString s = static_cast<QComboBox*>(editor)->currentText();
  26. model->setData(index, s, Qt::EditRole);
  27. }
  28. void QMyComboDelegate::updateEditorGeometry(QWidget *editor,
  29. const QStyleOptionViewItem &option,
  30. const QModelIndex &index) const
  31. {
  32. editor->setGeometry(option.rect);
  33. }

【代理类的使用】

代理类写好了,在tableView中设置它就很容易了。

  1. QMyComboDelegate* a = new QMyComboDelegate(this);
  2. a->lst << "男" << "女";
  3. ui->tableView->setItemDelegateForColumn(
  4. m_model->fieldIndex("sex"), a);
  5. QMyComboDelegate* b = new QMyComboDelegate(this);
  6. b->lst << "数学系" << "物理系" << "电机系" << "生物系" << "中文系";
  7. ui->tableView->setItemDelegateForColumn(
  8. 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

关系表格模型

image-20220528191628071

QSqlTableModel 加上 comboBox代理,解决了大部分的基本需求。

但其过程仍较为繁冗。

并且,comboBox 列表内容经常是放在一个表中,所谓的“主从表”结构的“从表”。

其实,这才是关系型数据库强大功能之一,即:支持外键连接关系。

看下面的结构:

image-20220528181010951

这其实是两个要求。

如果我们徒手实现,

第一个相对容易,第二个更麻烦一些。

所幸,Qt替我们实现了这些功能。

【目标】

章节四:信息学竞赛 CPP 应用篇 - 图116

这是用QSqlRelationalTableModel实现打开“主表”的效果,

其中的“姓名”,“科目” 都支持下拉列表选择。

这个功能使用了系统提供的 QSqlRelationalDelegate 类。

【准备】

还是老套路,.pro 加入 sql 支持。

.h 加入相关头文件。

.h 加入定义:

  1. QSqlDatabase m_db;
  2. QSqlRelationalTableModel* m_model;

.cpp 中初始化

  1. ui->setupUi(this);
  2. m_db = QSqlDatabase::addDatabase("QSQLITE");
  3. m_db.setDatabaseName("d:/my2.db3");
  4. m_db.open();
  5. QSqlQuery q;
  6. q.exec("create table course(科目号 int primary key,"
  7. "科目名 varchar,"
  8. "学分 int)");
  9. q.exec("insert into course values(101, '高等数学', 4)");
  10. q.exec("insert into course values(102, '流体力学', 3)");
  11. q.exec("insert into course values(103, '电子线路', 3)");
  12. q.exec("insert into course values(105, '基础化学', 3)");
  13. q.exec("create table stu(学号 int primary key, 姓名 varchar)");
  14. q.exec("insert into stu values(2001, '李时珍')");
  15. q.exec("insert into stu values(2002, '张三丰')");
  16. q.exec("insert into stu values(2003, '克林顿')");
  17. q.exec("create table test(学号 int, 科目号 int, 成绩 double)");
  18. q.exec("insert into test values(2001,101,90)");
  19. q.exec("insert into test values(2001,102,81.5)");
  20. q.exec("insert into test values(2002,101,78)");
  21. q.exec("insert into test values(2002,103,82)");
  22. q.exec("insert into test values(2003,105,93)");
  23. m_model = new QSqlRelationalTableModel(this);
  24. m_model->setEditStrategy(QSqlTableModel::OnManualSubmit);
  25. m_model->setTable("test");
  26. m_model->setRelation(0, QSqlRelation("stu","学号","姓名"));
  27. m_model->setRelation(1, QSqlRelation("course","科目号","科目名"));
  28. m_model->select();
  29. ui->tableView->setModel(m_model);

只这样的设置,已经能在主表中显示从表中对应的名称了。

注意,

此时,并不需要主表、从表间真的有主外键关系。

【自动生成comboBox代理】

这个功能看起来麻烦,实现只一行代码:

  1. ui->tableView->setItemDelegate(
  2. new QSqlRelationalDelegate(ui->tableView));

至于,提交、撤销,与前述无异。

实时效果反馈

1. 关于QSqlRelationalTableModel, 说法正确的是:__

  • A QSqlRelationalTableModel要求主从表要建立主外键关联

  • B QSqlRelationalTableModel 间接从QSqlQueryModel 继承

  • C 该模型是只读模型,不能自动回写数据

  • D 该模型要求我们自定义代理类,才能实现下拉选择效果。

答案

1=>B

dom读取xml

image-20220529161424368

XML(ExtensibleMarkup Language,可扩展标记语言)

是一种被广泛采用的通用数据交换格式,它很容易被机器识别与解析。

XML是W3C推荐标准。

它是一种文本结构,可供人阅读。以树型结构表达信息。

可用于信息的传递,也可用于信息的存储。

Qt的QtXml模块提供了基本支持。

三种主要的处理方法:

  • dom

    整个文档映射到内存的一棵树,可随机访问

  • sax

    适于大文件的顺序读入、解析。它会产生一些事件,调用用户定义的回调函数。

  • 类似于sax,可读入一些token,由用户控制

需要认识几个常用的类

  • QDomDocument

    对于整个xml文档

  • QDomNode

    一个节点。可以是:一段文本,一段注释,一个属性,一个子结构

  • QDomElement

    一个完整的标签及其内部,它是QDomNode的一种类型

以一份菜单为例:

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <memu>
  3. <伴黄瓜 type="凉菜">
  4. 黄瓜,粉皮
  5. </伴黄瓜>
  6. <土豆丝 type="热菜">
  7. 土豆
  8. </土豆丝>
  9. <鱼香茄子 type="热菜">
  10. <备注>
  11. 多放辣椒
  12. </备注>
  13. 茄子
  14. </鱼香茄子>
  15. <地三鲜 type="热菜">
  16. 土豆,青椒,茄子
  17. </地三鲜>
  18. <花生米 type="凉菜">
  19. 花生
  20. </花生米>
  21. <米饭 type="主食" num="2碗">
  22. 大米
  23. </米饭>aa
  24. <蛋花汤 type="汤">
  25. 鸡蛋,西红柿
  26. <备注>
  27. 别放葱花
  28. </备注>
  29. </蛋花汤>
  30. </menu>

【任务1】

读出所有的热菜,及其主料

  1. QFile file("d:/test.xml");
  2. QDomDocument doc;
  3. doc.setContent(&file);
  4. QDomElement root = doc.documentElement();
  5. QDomNode t = root.firstChild();
  6. while(!t.isNull()){
  7. if(t.isElement()){
  8. QDomElement e = t.toElement();
  9. if(e.attribute("type") == "热菜"){
  10. QString s = e.tagName();
  11. ui->plainTextEdit->appendPlainText(s);
  12. QDomNodeList lst = e.childNodes();
  13. for(int i=0; i<lst.count(); i++){
  14. if(lst.at(i).isText())
  15. s = "\t" + lst.at(i).nodeValue().trimmed();
  16. }
  17. ui->plainTextEdit->appendPlainText(s);
  18. }
  19. }
  20. t = t.nextSibling();
  21. }

不同的节点类型有不同的类来处理,

但,它们都是从 QDomNode继承

为了避免向子类转换的风险,提供了 toXXX 转到子类型。

比如:

toProcessingInstruction(), toElement(),toAttr(),toText()

【任务2】

列出所有带有备注的菜品,及其备注内容。

  1. QFile file("d:/test.xml");
  2. QDomDocument doc;
  3. doc.setContent(&file);
  4. ui->plainTextEdit->appendPlainText("---------------");
  5. QDomNodeList lst = doc.documentElement().childNodes();
  6. for(int i=0; i<lst.count(); i++){
  7. QDomNodeList lst2 = lst.at(i).childNodes();
  8. for(int j=0; j<lst2.count(); j++){
  9. if(lst2.at(j).isElement()){
  10. QDomElement e = lst2.at(j).toElement();
  11. if(e.tagName()=="备注"){
  12. QString s = lst.at(i).toElement().tagName();
  13. s += "---" + e.text();
  14. ui->plainTextEdit->appendPlainText(s);
  15. }
  16. }
  17. }
  18. }

实时效果反馈

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

image-20220530192157475

与xml的读取相对应,用dom模式生成xml也是类似的:

先通过构造各种对象,在内存中建立一棵 dom 树型结构,

然后,把整个树结构一次性写出到文件中。

【准备】

.pro 文件中追加: Qt += xml

相关位置添加: 头文件

【任务1:生成xml】

  1. QFile file("d:/my.xml");
  2. if(!file.open(QIODevice::WriteOnly | QIODevice::Truncate))
  3. return ;
  4. QDomDocument doc;
  5. QDomProcessingInstruction head;
  6. head = doc.createProcessingInstruction("xml",
  7. "version=\"1.0\" encoding=\"UTF-8\"");
  8. doc.appendChild(head);
  9. QDomElement root = doc.createElement(tr("点餐"));
  10. doc.appendChild(root);
  11. QDomElement e1 = doc.createElement(tr("土豆丝"));
  12. e1.setAttribute("type", tr("热菜"));
  13. QDomElement e2 = doc.createElement(tr("备注"));
  14. QDomText t = doc.createTextNode(tr("不放辣椒"));
  15. e2.appendChild(t);
  16. e1.appendChild(e2);
  17. e2 = doc.createElement(tr("主料"));
  18. t = doc.createTextNode(tr("土豆"));
  19. e2.appendChild(t);
  20. e1.appendChild(e2);
  21. root.appendChild(e1);
  22. e1 = doc.createElement(tr("拌黄瓜"));
  23. e1.setAttribute("type", tr("凉菜"));
  24. e2 = doc.createElement(tr("主料"));
  25. t = doc.createTextNode(tr("黄瓜,豆付皮"));
  26. e2.appendChild(t);
  27. e1.appendChild(e2);
  28. root.appendChild(e1);
  29. QTextStream out(&file);
  30. doc.save(out, 4); // 缩进为 4

说明:

  • tr

    这个是便于国际化时对字符串常量处理之用,

    此处是用它的“副作用”,明确字符串用什么编码方式

    此编码方式可以全局位置设置:

    main.cpp 中加上一句:

    1. QTextCodec::setCodecForLocale(
    2. QTextCodec::codecForName("utf8"));

    当然,需要include

    这是对整个应用设置了默认的编码方案。

    这样,在输出时,就不用再去指定编码格式

  • save

    save 时指定的 4, 是子element 缩进量,这样是为了便于阅读,不影响xml主义

    (严格地说,两个element间有没有换行、空格等还是有轻微差别的)

【任务2:dom显示到treeWidget】

我们不预定xml的层级,或说树的深度,因而需要递归处理

  1. QDomDocument doc;
  2. QFile file("d:/my.xml");
  3. if (!file.open(QIODevice::ReadOnly)) return;
  4. if (!doc.setContent(&file)) return;
  5. ui->treeWidget->clear();
  6. ui->treeWidget->setHeaderHidden(true);
  7. QDomElement root = doc.documentElement();
  8. QString s = QString("<%1>").arg(root.tagName());
  9. auto it = new QTreeWidgetItem(ui->treeWidget, QStringList(s));
  10. dom_to_tree(it, root);
  11. ui->treeWidget->expandItem(it);

递归函数 dom_to_tree 的参数:

  1. void Widget::dom_to_tree(QTreeWidgetItem*it, QDomElement &e)

其实现如下:

  1. QDomNodeList lst = e.childNodes();
  2. for(int i=0; i<lst.count(); i++){
  3. if(lst.at(i).isText()){
  4. new QTreeWidgetItem(it,
  5. QStringList(lst.at(i).nodeValue()));
  6. }
  7. if(lst.at(i).isElement()){
  8. QDomElement e = lst.at(i).toElement();
  9. QString s = QString("<%1>").arg(e.tagName());
  10. auto t = new QTreeWidgetItem(it, QStringList(s));
  11. dom_to_tree(t, e);
  12. }
  13. }

说明:

生成 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

image-20220530192806897

dom解析方法,直观易懂。

但,当树的层级较深,或需求复杂时,处理代码还是比较冗长的。

另外,其最大问题是不能支持大规模的xml,因为必须把整个xml装入内存。

sax 解析则是另一种思路。

它的处理接口更简单,是一种基于事件和回调的方式。

这种处理方式不需要把整个xml读入内存,

而是边读,边产生事件。

常用的事件:

  • 标签开始

    整个开始标签都解析过了,获得了标签名,属性等

  • 标签结束

  • 读入了一段文本

系统提供了缺省的解析器 QXmlDefaultHandler,对所有事件定义了处理函数

我们可以定义自己的处理器,继承 QXmlDefaultHandler,并覆盖关心的事件处理函数

总之,

使用sax的过程,就是定义自己的解析器的过程。

【准备】

.pro 文件中加入 Qt += xml

相关位置加入头:

my.xml 文件:

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <点餐>
  3. <土豆丝 type="热菜">
  4. <备注>不放辣椒</备注>
  5. <主料>土豆</主料>
  6. </土豆丝>
  7. <拌黄瓜 type="凉菜">
  8. <主料>黄瓜,豆付皮</主料>
  9. </拌黄瓜>
  10. <aaa type="凉菜">
  11. <备注>不放aaa</备注>
  12. <主料>aaa....</主料>
  13. </aaa>
  14. <bbb type="热菜">
  15. </bbb>
  16. </点餐>

【任务1:显示热菜及其主料】

定义解析器:

  1. #include "mysax1.h"
  2. #include <QDebug>
  3. MySax1::MySax1(QPlainTextEdit* edit)
  4. {
  5. m_edit = edit;
  6. }
  7. bool MySax1::fatalError(const QXmlParseException &exception)
  8. {
  9. qDebug() << exception.message();
  10. return false;
  11. }
  12. bool MySax1::startElement(const QString &namespaceURI, const QString &localName, const QString &qName, const QXmlAttributes &atts)
  13. {
  14. if(atts.value("type")=="热菜"){
  15. m_t1 = qName;
  16. m_edit->appendPlainText(qName);
  17. }
  18. return true;
  19. }
  20. bool MySax1::endElement(const QString &namespaceURI, const QString &localName, const QString &qName)
  21. {
  22. if(qName==m_t1){
  23. m_t1.clear();
  24. return true;
  25. }
  26. if(qName=="主料" && !m_t1.isEmpty())
  27. m_edit->appendPlainText(" -------- " + m_t2);
  28. return true;
  29. }
  30. bool MySax1::characters(const QString &ch)
  31. {
  32. m_t2 = ch;
  33. return true;
  34. }

说明:

sax 是无法自动回溯历史的,

我们自己必须预见到哪些东西将来有可能用到,先用成员存起来。

这里的处理有不严密之处。。。

自定义类的使用:

  1. QFile file("d:/my.xml");
  2. QXmlInputSource inputSource(&file);
  3. QXmlSimpleReader reader;
  4. MySax1 handler(ui->plainTextEdit);
  5. reader.setContentHandler(&handler);
  6. reader.setErrorHandler(&handler);
  7. reader.parse(inputSource);

实践中,我们经常要用到当前标签的外层标签的数据。

对这种需求,用来处理最恰当。

【任务2:输出所有的备注信息】

  1. #include "mysax2.h"
  2. MySax2::MySax2(QPlainTextEdit* edit)
  3. {
  4. m_edit = edit;
  5. }
  6. bool MySax2::fatalError(const QXmlParseException &exception)
  7. {
  8. qDebug() << exception.message();
  9. return false;
  10. }
  11. bool MySax2::startElement(const QString &namespaceURI, const QString &localName, const QString &qName, const QXmlAttributes &atts)
  12. {
  13. m_stack.push(qName);
  14. return true;
  15. }
  16. bool MySax2::endElement(const QString &namespaceURI, const QString &localName, const QString &qName)
  17. {
  18. m_stack.pop();
  19. if(qName=="备注"){
  20. QString s = m_stack.top() + " --- " + m_text;
  21. m_edit->appendPlainText(s);
  22. }
  23. return true;
  24. }
  25. bool MySax2::characters(const QString &ch)
  26. {
  27. m_text = ch;
  28. return true;
  29. }

使用方法与上面完全一样。

实时效果反馈

1. 关于sax, 说法正确的是:__

  • A sax解析基于事件和回调

  • B sax一般不需要自己定义解析器类

  • C sax比dom解析需要更多内存

  • D sax不适用于较大的文件

2. sax解析时,为什么常用到栈结构?__

  • A 因为sax把所有事件都放入栈中

  • B 因为当前节点的处理,往往用到包含它的外层节点的信息

  • C 这样可自动管理内存

  • D 文件越大,栈就越深

答案

1=>A 2=>B

流解析xml

image-20220531121418794

Qt还为xml解析提供了一种流式的方案。

  1. QXmlStreamReaderQXmlStreamWriter

与sax十分类似,它也是从头到尾读入 xml,并不全部装入内存。

不同的是,它不是通过事件和回调来驱动。

它把读入的内容切分为一个个的 token,通过主调方来控制读取的时机。

这种方式,不需要定义解析器类,也很方便递归处理。

我们用它来完成前边同样的任务。

准备工作还是一样的。

.pro 文件中加入 xml 支持

.h 文件加入

d盘根目录提供 my.xml

【任务1:提取热菜及其备注】

这几乎就是sax方式的一个翻版,

也需要使用一个 QStack 来记录保存祖先标签。

  1. QFile file("d:/my.xml");
  2. if (!file.open(QFile::ReadOnly | QFile::Text)) return;
  3. QXmlStreamReader reader;
  4. reader.setDevice(&file);
  5. struct MyEle{
  6. QString name;
  7. int type;
  8. };
  9. QStack<MyEle> stack;
  10. QString text;
  11. while (!reader.atEnd()) {
  12. auto type = reader.readNext();
  13. if(type == QXmlStreamReader::StartElement) {
  14. MyEle ele{reader.name().toString(), 0};
  15. if(reader.attributes().value("type")=="热菜")
  16. ele.type = 1;
  17. stack.push(ele);
  18. if(ele.type==1)
  19. ui->plainTextEdit->appendPlainText(ele.name);
  20. }
  21. if(type == QXmlStreamReader::EndElement){
  22. stack.pop();
  23. if(reader.name() == "备注"){
  24. if(stack.top().type==1)
  25. ui->plainTextEdit->appendPlainText(
  26. " ---- " + text);
  27. }
  28. }
  29. if(type == QXmlStreamReader::Characters){
  30. text = reader.text().toString();
  31. }
  32. }
  33. if (reader.hasError()) {
  34. qDebug() << "error: " << reader.errorString();
  35. }

如果热菜的名字和备注分行输出,则不必定义这么麻烦的结构,栈中只有一个type就可以了。如果要延期输出热菜的名字,就需要这个结构了。

【任务2:以树结构显示xml】

  1. void Widget::on_pushButton_2_clicked()
  2. {
  3. QFile file("d:/my.xml");
  4. if (!file.open(QFile::ReadOnly | QFile::Text)) return;
  5. QXmlStreamReader reader;
  6. reader.setDevice(&file);
  7. while (!reader.atEnd()) {
  8. auto type = reader.readNext();
  9. if(type==QXmlStreamReader::StartElement) break;
  10. }
  11. QString s = QString("<%1>").arg(reader.name().toString());
  12. auto it = new QTreeWidgetItem(ui->treeWidget,
  13. QStringList(s));
  14. token_into_tree(it, reader);
  15. }

核心是一个递归程序 token_into_tree

  1. void Widget::token_into_tree(QTreeWidgetItem* it, QXmlStreamReader &reader)
  2. {
  3. while (!reader.atEnd()) {
  4. auto type = reader.readNext();
  5. if(type==QXmlStreamReader::StartElement){
  6. QString s = QString("<%1>").arg(reader.name().toString());
  7. auto it2 = new QTreeWidgetItem(it, QStringList(s));
  8. token_into_tree(it2, reader);
  9. }
  10. if(type==QXmlStreamReader::Characters && !reader.isWhitespace()){
  11. QString s = reader.text().toString();
  12. new QTreeWidgetItem(it, QStringList(s));
  13. }
  14. if(type==QXmlStreamReader::EndElement) return;
  15. }
  16. }

实时效果反馈

1. 关于QXmlStreamReader, 说法==错误==的是:__

  • A 与sax类似,顺序读入xml内容,边读边解析

  • B 读入的每个token,对应不同的类别

  • C 对不同的类别,token要转换为不同的类型,然后访问其内容

  • D 流式处理,适用于递归处理

答案

1=>C

网络编程导引

image-20220531163728683

  • 地址

在网络中的通信实体,可以看作参予通信的节点

每个节点有一个唯一的标识,称为主机地址

如果,我们使用IP协议,则为IP地址 + 端口号

每台物理机器,可能有多个IP地址(多块网卡或虚拟的地址,比如装了vmware)。

IP 有外网地址和内网地址(192.168.0.1 是典型的内网地址)

外网地址,全网可见。

内网地址对外不可见,通过可见地址的代理与外网通信。

IP地址有 IPv4, IPv6 两个版本。

目前 IPv4 基本满额

  • 分层

章节四:信息学竞赛 CPP 应用篇 - 图122

不同的层上定义了很多常见的通信协议

tcp, udp 属于低层

http, ftp 属于高层

  • 主机名与地址

QHostInfo::localHostName() 返回本机名,就是电脑系统属性里的:完整计算机名。

通过名字可以进一步取得IP地址

  1. QString s = QHostInfo::localHostName();
  2. ui->plainTextEdit->appendPlainText("本机名:" + s);
  3. auto lst = QHostInfo::fromName(s).addresses();
  4. for(auto i: lst){
  5. if(i.protocol()==QAbstractSocket::IPv4Protocol)
  6. ui->plainTextEdit->appendPlainText(
  7. "v4: " + i.toString());
  8. if(i.protocol()==QAbstractSocket::IPv6Protocol)
  9. ui->plainTextEdit->appendPlainText(
  10. "v6: " + i.toString());
  11. }

用fromName() 也可以取得运程主机的ip,但是阻塞方式

  • 非阻塞方式取得远程主机IP
  1. QHostInfo::lookupHost("www.163.com",
  2. this, SLOT(lookup_end(QHostInfo)));

这里需要提供一个槽函数

  1. void Widget::lookup_end(const QHostInfo &info)
  2. {
  3. qDebug() << info.addresses();
  4. }

当取得IP地址时,会触发事件,进而触发槽函数的调用

  • 本机的网络接口信息
  1. auto lst = QNetworkInterface::allInterfaces();
  2. for(auto i: lst){
  3. qDebug() << "设备: " <<i.name();
  4. qDebug() << "硬件: " <<i.hardwareAddress();
  5. auto lst2 = i.addressEntries();
  6. for(auto j: lst2){
  7. qDebug() << "IP: " << j.ip().toString();
  8. qDebug() << "子网掩码: " << j.netmask().toString();
  9. qDebug() << "广播地址: " << j.broadcast().toString();
  10. }
  11. }

取得本机IP的另一种方法:

  1. void Widget::on_pushButton_4_clicked()
  2. {
  3. QList<QHostAddress> list = QNetworkInterface::allAddresses();
  4. for(QHostAddress address: list){
  5. ui->plainTextEdit->appendPlainText(address.toString());
  6. }
  7. }

实时效果反馈

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

image-20220602165023896

UDP全称是:User Datagram Protocol

按OSI的7层模型,它处于网络层的上一层—传输层,具体地看,在IP协议的上一层

它传输的对象是:数据包,专业术语:报文

它的特点是:无连接,不可靠

具体说,可能会:

  • 丢包
  • 错包
  • 重包
  • 乱序

可以做为其它协议的基础,

可以直接用于要求不高的特定场合:

比如:音频,会议等。。。

由此,交换而来的优势是:快速、简单、无额外的开销

目标地址

发送报文的条件:

  • 发送方需要绑定到一个 IP地址 + 端口号
  • 目标地址格式:IP地址 + 端口号

这里的端口号,可以是: 1025 — 65535

IP 地址可以外网,内网地址,可以是特殊的:

广播地址,组播地址 使用广播地址,则在同一局域的所有指定端口都能收到

【任务目标】

在同一机器,或不同机器上的两个程序,实现简单报文传递

【实现】

建立Qt gui 项目

.pro 增加 network 支持

适当位置加头文件: #include

【发送】

很简单,直接创建 udp对象

  1. QUdpSocket skt;
  2. QByteArray by = ui->lineEdit->text().toUtf8();
  3. int port = ui->spinBox_2->text().toInt();
  4. skt.writeDatagram(by.data(), by.size(),
  5. QHostAddress::Broadcast, port);

其中的IP地址,用的是广播地址

【接收】

需要一个全程的 udp 对象,负责侦听数据

  1. private:
  2. Ui::Widget *ui;
  3. QUdpSocket m_skt;

自己做个槽函数:

  1. void Widget::udp_data_ok()
  2. {
  3. while(m_skt.hasPendingDatagrams()){
  4. QByteArray by;
  5. by.resize(m_skt.pendingDatagramSize());
  6. m_skt.readDatagram(by.data(), by.size());
  7. ui->plainTextEdit->appendPlainText(QString(by));
  8. }
  9. }

当有数据报就绪,说可以接收了。

初始化的时候,要把 udp 的信号连接到这个槽上。

  1. ui->setupUi(this);
  2. connect(&m_skt, SIGNAL(readyRead()), this, SLOT(udp_data_ok()));

原理很简单。

请注意:

在同一台机器上实验的时候,两个程序要绑不同的端口

qt IDE 环境启动两个程序的方法:

章节四:信息学竞赛 CPP 应用篇 - 图124

在 工具 | 选项 …. 里边设置一下

stop application before building: 选 none 就可以。

实时效果反馈

1. UDP协议, 属于 OSI 标准模型的哪一层?__

  • A 应用层

  • B 会话层

  • C 网络层

  • D 传输层

2. 关于 UDP 协议的优点,说法==错误==的是:__

  • A 简单,没有额外的开销

  • B 无需先建立连接,快速

  • C 可以使用广播地址,一发多收

  • D 适用于发大量的数据

答案

1=>D 2=>D

TCP

image-20220606173501368

TCP 全称: Transmission Control Protocol 传输控制协议

与 UDP 比较:

  • 可靠的

    没的UDP的问题(通过自动校验、重试等控制机制)

    顺序也是保证的

    所以,传输的对象不是报文,而是

  • 先建连接

    可以比照打电话

    服务方:侦听(被动等待,相当于被叫用户)

    客户方:连接(主动发起请求,相当于主叫用户)

    server 可以处理多个 client 的连接请求

    image-20220606163354343

  • 适于大量数据

    建立连接的消耗比重相对变小了

【示例 server, client tcp 通信】

建立两个应用程序,分别是 server, client 角色

server 侦听, client 请求连接

成功后,server 与 client 自由会话

章节四:信息学竞赛 CPP 应用篇 - 图127

【准备】

.pro 中加入 Qt += network 支持

.h 中加入: #include

【服务端】

需要一个持久的用于侦听的对象

  1. QTcpServer m_server;

侦听动作:

  1. m_server.close();
  2. int port = ui->spinBox->text().toInt();
  3. qDebug() << m_server.listen(QHostAddress::LocalHost, port);

响应事件:

  1. connect(&m_server, SIGNAL(newConnection()),
  2. this, SLOT(newlink()));

槽函数:

  1. m_skt = m_server.nextPendingConnection();
  2. QString s = QString("对方IP: %1, 端口: %2")
  3. .arg(m_skt->peerAddress().toString())
  4. .arg(m_skt->peerPort());
  5. ui->label->setText(s);
  6. connect(m_skt, SIGNAL(disconnected()),
  7. m_skt, SLOT(deleteLater()));
  8. connect(m_skt, SIGNAL(readyRead()),
  9. this, SLOT(read_data()));

这里的 m_skt 是 QTcpSocket 类型的指针

因为后面通话全靠它,所以要持久存在

  1. QTcpSocket* m_skt;

disconnet 的槽用了系统提供的函数,这里不做也可以,server 析构,最后会释放

真正用 QTcpSocket 的是二个地方

发送:

  1. QString s = ui->lineEdit->text();
  2. QByteArray by = (s + "\n").toUtf8();
  3. m_skt->write(by);
  4. ui->plainTextEdit->appendPlainText("我说: " + s);

接收:

  1. void Widget::read_data()
  2. {
  3. while(m_skt->canReadLine()){
  4. QString s = "他说:" + m_skt->readLine();
  5. ui->plainTextEdit->appendPlainText(s);
  6. }
  7. }

实时效果反馈

1. 关于QTcpServer, 说法正确的是:__

  • A QTcpServer 在接收了一个连接请求后,无法再接收其它连接请求

  • B QTcpServer 在接受连接请求后,可以通过自身向对方发数据,和接收数据

  • C QTcpServer 接受请来后,创建新的 socket 对象与对方连接

  • D QTcpServer 并不保存新建的 Socket 对象指针,需要程序员自己管理生存期

2. QTcpSocket的connectToHost发起连接请求后,正确说法是:__

  • A 函数立即返回,当连接成功或失败时,会产生相应的信号

  • B 函数会一直阻塞,直到连接成功或失败

  • C 函数立即返回,需要用户在其后等待一定时间后去查询是否成功

  • D 函数一直不返回,直到对方关闭socket

答案

1=>C 2=>A

聊天服务器

image-20220607190451596

考虑一个电话应用:

如何实现多方通话(类似电话会议)?

方案一:电信提供商开新业务

方案二:一堆电话开免提,放在一个盒子里….

我们实现的 TCP 聊天服务器,类似于方案二

章节四:信息学竞赛 CPP 应用篇 - 图129

【服务器】

  • 侦听

    1. ui->setupUi(this);
    2. m_server.listen(QHostAddress::LocalHost, 3333);
    3. ui->plainTextEdit->appendPlainText("listen at 3333 .... ");
    4. connect(&m_server, SIGNAL(newConnection()),
    5. this, SLOT(onMyConnect()));

主要代码就是调用 listen,给了固定的端口号。

以后,每当有新的呼入请求,都会触发 onMyConnect 的调用。

  • 接受连接

    1. QTcpSocket* p = m_server.nextPendingConnection();
    2. QString s = QString("接受了:%1:%2")
    3. .arg(p->peerAddress().toString())
    4. .arg(p->peerPort());
    5. ui->plainTextEdit->appendPlainText(s);
    6. connect(p, SIGNAL(readyRead()), this, SLOT(onMyReady()));

每个新建立的 QTcpSocket 对象都作为 Server 的孩子来管理。

我们需要为它们增加一个信号映射,

当任何客户端发来数据的时候,都会触发onMyReady

  • 当有数据到来时
  1. void MainWindow::onMyReady()
  2. {
  3. auto lst = m_server.findChildren<QTcpSocket*>();
  4. for(auto i:lst){
  5. if(i->canReadLine()){
  6. QByteArray by = i->readLine();
  7. send_to_all(by);
  8. }
  9. }
  10. }
  11. void MainWindow::send_to_all(QByteArray &data)
  12. {
  13. auto lst = m_server.findChildren<QTcpSocket*>();
  14. for(auto i:lst){
  15. if(i->isOpen()){
  16. i->write(data);
  17. }
  18. }
  19. }

QTcpServer 下可能还保管着其它的对象,

所以,我们为了找到所有的 QTcpSocket 类型的对象,可以用 findChildren

这是一个模板函数,我们调用它的时候,需要给出具化的类型

【客户端】

可以使用前边课程中开发的客户端程序,基本不变。

微调的地方:

因为自己发的消息,自己也会收到,所以发送时,不再输出。

因为多个人通话,须要知道消息是谁发来的,因为每个消息都要加发送方的地址。

这个功能最好在服务器端完成

在发送给全体之前,把对方地址顺便写入:

  1. if(i->canReadLine()){
  2. QByteArray by = i->readLine();
  3. QString s = i->peerAddress().toString()
  4. + ":" + QString::number(i->peerPort())
  5. + "说:";
  6. by.insert(0, s);
  7. send_to_all(by);
  8. }

实时效果反馈

1. 关于QTcpServer的children, 说法正确的是:__

  • A 这个功能是 QTcpServer 类特有的

  • B 这些children的类型一定是 QTcpSocket

  • C 这是 QObject 提供的功能

  • D 这是 QWidget 提供的功能

答案

1=>C

大文件传输

image-20220608180830093

TCP的传输是可靠的,但传输较大的文件,还是要考虑很多问题的。

如果使用同步的方式,就很容易,传输过程一直持续,直到传输完成。

但,这无法适应GUI应用程序的需要,

我们不应该在一个函数中做耗时的操作,而使整个界面响应陷入停顿。

况且,假如我们需要在传输的中间放弃任务,又如何处理?

本节讲的内容是:

通过异步的方式,实现大文件的传输。

其思路是:

在一个函数中只传输一小部分内容,

然后,等待传输完成,完成后再传输下一小部分。

QTcpSocket 的设计充分考虑了异步传输的问题。

skt.write(字节数组) 并不会等到传输完成才返回,而是几乎立即返回。

当传输完成时,会产生相应该的信号,我们可以映射这个信号,来做后续的处理。

【传输约定】

为了方便,我们约定,被传输的文件是:d:/u5_in.zip

接收方接收后,生成的文件是:d:/u5_out.zip

客户端连接到服务器后,立即传输数据

数据流的格式是:数据文件长度(int64) + 文件内容本身

【服务器端】

侦听在 5555 端口

  1. m_server.listen(QHostAddress::LocalHost, 5555);
  2. ui->plainTextEdit->appendPlainText("等待 at 5555");

映射槽函数

  1. ui->setupUi(this);
  2. connect(&m_server, SIGNAL(newConnection()),
  3. this, SLOT(onMyConnect()));

当连接到来时:

  1. m_skt = m_server.nextPendingConnection();
  2. m_server.close(); // 只接受一个客户的连接请求
  3. connect(m_skt, SIGNAL(readyRead()),
  4. this, SLOT(onMyReadyRead()));
  5. m_total = 0; // 文件的总长度
  6. m_done = 0; // 已经写入文件的字节数
  7. QString s = m_skt->peerAddress().toString()
  8. + ":" + QString::number(m_skt->peerPort());
  9. ui->plainTextEdit->appendPlainText("已连接到:" + s);

当有数据到来 时:

  1. void Widget::onMyReadyRead()
  2. {
  3. if(m_total == 0){
  4. if(m_skt->bytesAvailable()<sizeof(qint64)) return;
  5. QDataStream in(m_skt);
  6. in >> m_total;
  7. m_file.open(QFile::WriteOnly);
  8. }
  9. QByteArray by = m_skt->readAll();
  10. m_file.write(by);
  11. m_done += by.size();
  12. if(m_done == m_total){
  13. m_file.close();
  14. m_skt->close();
  15. ui->plainTextEdit->appendPlainText("接收文件结束");
  16. }
  17. }

在读入文件长度时,不用 m_skt 直接读,而是用 QDataStream,是为了跨平台的无关性。实际应用中,很可能服务器与客户端不是同样的系统环境。

【客户端】

界面上加一个progressBar 用来显示传输的进度

先请求服务器

  1. m_skt.connectToHost("127.0.0.1", 5555);

需要映射信号:

  1. connect(&m_skt, SIGNAL(connected()),
  2. this, SLOT(onMyConnect()));
  3. connect(&m_skt, SIGNAL(bytesWritten(qint64)),
  4. this, SLOT(onMyBytesWritten(qint64)));

当连接成功时:

  1. ui->plainTextEdit->appendPlainText("连接成功");
  2. m_file.open(QFile::ReadOnly);
  3. qint64 n = m_file.size();
  4. QDataStream out(&m_skt);
  5. out << n;
  6. m_total = n; //文件大小
  7. m_done = 0; // 已经传输的大小
  8. ui->progressBar->setMaximum(m_total);

当写入成功时:

  1. if(!m_file.isOpen()){
  2. ui->plainTextEdit->appendPlainText("发送结束");
  3. return;
  4. }
  5. QByteArray by = m_file.read(1024);
  6. if(by.size() < 1024)
  7. m_file.close();
  8. if(by.size()>0){
  9. m_skt.write(by);
  10. m_done += by.size();
  11. }
  12. ui->progressBar->setValue(m_done);

实时效果反馈

1. 关于QTcpSocket.write(), 说法正确的是:__

  • A 一直阻塞,直到所有数据都正确发出

  • B 一直阻塞,直到对方读取了数据

  • C 不阻塞,送到发送缓冲区就返回了

  • D 不阻塞,新开一个线程,在后台执行

2. 使用QDataStream,不直接用QTcpSockt读写最主要好处是?:__

  • A 有缓冲,更快

  • B 更统一的二进制格式,跨平台是一致的

  • C 更友好的API

  • D 操作更容易

答案

1=>C 2=>B

高层协议

image-20220609183808980

建立在 TCP 上层的协议很多,一般统称为高层

我们熟知是,比如: HTTP, FTP 等

它们的任务焦点,不是传输字节流,而是更具“语义”的一些任务。

比如,通过HTTP协议,获取远程服务器上的文件,即:http 下载

Qt的新版中,主要用如下类处理

用途
QNetworkRequest 封装 网络请求
QNetworkAccessManager 负责调度、协调网络调用
QNetworkReply 监视服务器的应答

需要注意的是:

如果使用了 https:// 请求头,即需要进行 SSL 认证,信息加密传输

SSL 过程也可以自动化,但需要的两个 dll 文件可能找不到,我们需要手工拷贝一下。

章节四:信息学竞赛 CPP 应用篇 - 图132

从tools 目录,拷贝到 Qt 编译的主目录下:

章节四:信息学竞赛 CPP 应用篇 - 图133

【示例功能】

章节四:信息学竞赛 CPP 应用篇 - 图134

在编辑框中,写入下载地址,此处用:

https://static.red-lang.org/dl/win/red-064.exe

这是 red 语言的一个下载地址

点击下载按钮,默认下载为 d:/u6.down,下载后自己改名为 exe

【实现】

先在 .pro 中添加 Qt += network

在适当位置加入头

需要的持久生命期的对象:

  1. QFile m_file; // 存下载的文件
  2. QNetworkAccessManager m_manager; // 管理下载过程
  3. QNetworkReply* m_reply; // 监视应答信息

当点击“下载”按钮时,

  1. ui->pushButton->setEnabled(false); // 防连击
  2. QString s = ui->lineEdit->text().trimmed();
  3. QUrl url = QUrl::fromUserInput(s);
  4. if(!url.isValid()){
  5. qDebug() << url.errorString();
  6. return;
  7. }
  8. m_file.open(QIODevice::WriteOnly);
  9. m_reply = m_manager.get(QNetworkRequest(url));
  10. connect(m_reply, SIGNAL(finished()),
  11. this, SLOT(onMyFinish()));
  12. connect(m_reply, SIGNAL(readyRead()),
  13. this, SLOT(onMyRead()));
  14. connect(m_reply, SIGNAL(downloadProgress(qint64,qint64)),
  15. this, SLOT(onMyProgress(qint64,qint64)));

当发来数据时,当传输结束时,还有专门的进度信号

  1. void Widget::onMyFinish()
  2. {
  3. m_file.close();
  4. }
  5. void Widget::onMyRead()
  6. {
  7. m_file.write(m_reply->readAll());
  8. }
  9. void Widget::onMyProgress(qint64 a, qint64 b)
  10. {
  11. ui->progressBar->setMaximum(b);
  12. ui->progressBar->setValue(a);
  13. }

因为 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

多媒体

image-20220610170946207

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

此处以简单的音频播放为例,了解一下其编程模式。

【任务目标】

开发一个简单的音频播放工具。

可以选歌曲,可以显示或控制播放进度,调音量等。。。

章节四:信息学竞赛 CPP 应用篇 - 图136

【准备】

.pro 文件中加入 Qt += multimedia

如果需要加入视频支持,还需要 multimediawidgets

此例需要的类是:

QMediaPlayer 主类,可以播放压缩格式的音频或视频文件。

QMediaPlayList 管理播放列表,可以循环播、单曲、乱序等

需要include

【实现】

点击“播放”按钮时,

  1. QString filter = "音频文件(*.mp3 *.wav *.wma);;";
  2. QString fname = QFileDialog::getOpenFileName(this,"",
  3. "d:/", filter);
  4. m_lst.clear();
  5. m_lst.addMedia(QUrl::fromLocalFile(fname));
  6. m_lst.setCurrentIndex(0);
  7. m_player.play();
  8. m_player.setVolume(50);

已经关联的槽函数:

  1. ui->setupUi(this);
  2. m_player.setPlaylist(&m_lst);
  3. connect(&m_player, SIGNAL(positionChanged(qint64)),
  4. this, SLOT(onMyPosition(qint64)));
  5. connect(&m_player, SIGNAL(durationChanged(qint64)),
  6. this, SLOT(onMyDuration(qint64)));

position 的变化在播放进行时产生,给出参数是距离开始的毫秒数

duration 的变化一般是切换不同的歌曲时发生,参数是曲目的总时长,单位是毫秒

当换曲时,

  1. void Widget::onMyDuration(qint64 a)
  2. {
  3. ui->horizontalSlider->setMaximum(a);
  4. ui->label_2->setText(QString::number(a/1000));
  5. }

当进度变化时,

  1. void Widget::onMyPosition(qint64 pos)
  2. {
  3. if(ui->horizontalSlider->isSliderDown()) return;
  4. ui->horizontalSlider->setValue(pos);
  5. ui->9->setText(QString::number(pos/1000));
  6. }

一个细节是,当用户正在拖动滚动条的时候,不要修改 value

其它的信号响应也很简单:

  1. void Widget::on_horizontalSlider_valueChanged(int value)
  2. {
  3. m_player.setPosition(value);
  4. }
  5. void Widget::on_pushButton_2_clicked()
  6. {
  7. m_player.pause();
  8. }
  9. void Widget::on_pushButton_3_clicked()
  10. {
  11. m_player.play();
  12. }
  13. void Widget::on_pushButton_4_clicked()
  14. {
  15. m_player.stop();
  16. }
  17. void Widget::on_horizontalSlider_2_valueChanged(int value)
  18. {
  19. m_player.setVolume(value);
  20. }

实时效果反馈

1. 以下哪个==不在== Qt 多媒体标准包支持之列:__

  • A 音频、视频播放

  • B 录像,截图

  • C 数字广播调谐和收听

  • D 虚拟现实

2. 对QMediaPlayer,当播放进度变化时,会产生什么信号?:__

  • A positionChanged(qint64)

  • B durationChanged(qint64)

  • C valueChanged(qint64)

  • D indexChanged(qint64)

答案

1=>D 2=>A