在库中开启Qt事件循环

示例代码

对于很多开发者来说,Qt仅仅是一个用于开发GUI程序的库,但实际上,Qt官方一直在致力于将Qt打造成一个跨平台的开发框架。Qt中提供了大量的基础设施和非GUI库可供我们在开发非GUI程序时所用,如网络相关的QNetwork模块,音视频相关的QMutilMedia模块,还有核心的QtCore模块。这些模块都已经相当成熟和完善,而且都具有优秀的跨平台性能。在和其他语言,如Java,python相比,库的相对不够丰富是C++的一个缺陷所在,除了标准库,boost等之外,Qt其实也是一个可以考虑的强大的补充。

如果我们只是打算使用诸如QString,QTL等基础设施,只需要链接QtCore这个库即可。然而Qt很多更为强大的特性和类,如信号槽,QTimer等,必须依赖Qt事件循环才能正常工作。如果使用我们开发的库的可执行程序恰好也是基于Qt开发,那么一切工作正常,然而,作为库的开发者,我们不能对库的使用者做出假设,因此如果我们需要使用信号槽等依赖Qt事件循环的特性时,我们就需要在库中开启Qt事件循序。

修改Windows系统环境变量

每个接触过Windows系统的开发人员都会接触到环境变量,相当一部分软件安装完成后必做的一步操作就是设置环境变量。Windows下的环境变量分为用户环境变量和系统环境变量。本文主要分享如何正确设置系统环境变量。

GUI界面

这是我们最常用的方式,即右键此电脑->属性->高级系统设置->环境变量,然后在系统变量栏中修改系统环境变量。该方法十分简单,但我们有时候可能需要通过程序来修改,又该怎么做呢?

环境变量

命令行

CMD中,我们可以通过set命令来设置环境变量,powershell中也可以通过$ENV:key=value的形式设置环境变量,但这只能影响当前命令行环境,命令行程序退出后就失效了。我们可以通过SetX来修改系统环境变量。官方描述如下:

在用户或系统环境创建或修改环境变量。能基于参数、注册表项或文件输入设置变量。

默认情况下,该命令是修改当前用户的的环境变量,如果我们需要修改系统环境变量,可以使用/M参数。但正如官方描述所说,该命令只能 创建或修改 环境变量,也就是说该命令无法用于删除一个环境变量。

注册表

根据微软的官方文档, Windows系统环境变量其实是保存在注册表中的,路径为HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Session Manager\Environment,我们可以手动或通过代码修改注册表的方式来修改系统环境变量。

To programmatically add or modify system environment variables, add them to the HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Session Manager\Environment registry key, then broadcast a WM_SETTINGCHANGE message with lParam set to the string “Environment”. This allows applications, such as the shell, to pick up your updates.

修改完成后我们需要广播一个消息类型为WM_SETTINGCHANGElParam参数为"Environment"的窗口消息,该消息将会通知应用程序获取我们的更新。经过测试我发现,目前Windows下的explorer.exe是会处理这个消息然后更新其环境变量。这也就解释了之前一直以来的一个困惑,就是在修改完系统环境变量后,新启动的程序有的可以获取到更新后的环境变量信息,而有的必须则不行。这其实是因为新进程是会继承父进程的环境变量,如果父进程处理了上述的窗口消息且主动更新了,那么子进程就会使用更新后的环境变量信息,例如通过explorer.exe(这是Windows的窗口管理器,包括开始菜单,桌面等都是由该程序管理)启动的程序。而如果某些程序没有处理这个窗口消息,那么其不会更新自己的环境变量,进而导致通过其启动的新进程也就是旧的环境变量信息。例如我使用的一款启动器软件就没有处理这个消息,导致如果我通过其启动的软件都是旧环境变量信息,只能重新启动该启动器或重启系统。

在发送这条窗口消息时,还有两个需要注意的问题。

  1. 如何将一个字符串设置给lParam参数?

    Windows下发送窗口消息的几种方法中,参数lParam都是是LPARAM类型,在Win10 x64系统下,其定义如下

    1
    2
    typedef LONG_PTR            LPARAM;
    typedef _W64 long LONG_PTR, *PLONG_PTR;

    也就是说lParam是一个整型,那如何将一个字符串赋值给整型呢?其实通过中间的LONG_PTR我们可以看出,这里Windows是用一个整型来存储字符串的地址。我们可以直接将字符串强转成一个整型。

    1
    SendMessage(HWND_BROADCAST, WM_SETTINGCHANGE, NULL, LPARAM("Environment"));
  2. 调用无效?

    上面的代码调用看上去已经满足文档上的要求了,但在实际测试时,在Win10 中文版系统上,发送该消息后,再通过explorer.exe启动新程序,新的程序还是旧的环境变量信息。难道文档错了么?这个问题困扰了我非常久,后来才发现这是由于Windows为了兼容不同的字符编码环境,绝大部分API都提供了三套方法,除了普通的形式外,还有一个以A结尾,一个以W的方法,其中A代表ASCII码,即窄字符串版本,W代表宽字符版本。普通形式的方法实际上会根据是否有定义__UNICODE__这个宏来define为以A结尾或以W结尾的方法。如SendMessage的定义如下:

    1
    2
    3
    4
    5
    #ifdef UNICODE
    #define SendMessage SendMessageW
    #else
    #define SendMessage SendMessageA
    #endif // !UNICODE

    在我的电脑环境下是有定义UNICODE宏的,因此SendMessage方法实际上是调用了SendMessageW方法,然而第四个参数"Environment"是一个ASCII字符串,这就导致参数和方法不匹配,进而导致该消息没有被正确处理。下面是几种可行的修改方式

    1
    2
    3
    SendMessageA(HWND_BROADCAST, WM_SETTINGCHANGE, NULL, LPARAM("Environment")); // 明确指定使用
    SendMessageW(HWND_BROADCAST, WM_SETTINGCHANGE, NULL, LPARAM(L"Environment")); // 明确指定使用宽字符版本
    SendMessage(HWND_BROADCAST, WM_SETTINGCHANGE, NULL, LPARAM(_T"Environment")); // _T是一个根据UNICODE宏调整的宏,分别会被定义为空或L

总结

下面是修改系统环境变量的几种方法的对比

方式 支持程序调用 支持手动修改 添加 修改 删除 即时生效
GUI界面
命令行
修改注册表并发送窗体消息

踩坑记之Jenkins无权限克隆的代码

踩坑

最近部门新采购了一台Mac Mini用作Jenkins自动构建服务器,由于之前搭建过Windows构建服务器,因此这次的Mac构建服务器的任务自然落在我的身上。开始一切都非常顺利,安装好必须的软件,配置好环境,但在正式上线,构建一个跨平台项目时,却发现macOS服务器始终无法从公司自建的Gitlab代码托管平台上克隆代码,而同一项目的其他平台(Windows和Linux)服务器却一切正常。macOS服务器的出错提示信息如下(部分信息用*代替)。

1
2
3
4
5
6
7
8
9
10
11
12
13
> git rev-parse --is-inside-work-tree # timeout=10
Setting origin to xxx.com:user/repo.git
> git config remote.origin.url xxx.com:path/repo.git # timeout=10
Fetching origin...
Fetching upstream changes from origin
> git --version # timeout=10
using GIT_SSH to set credentials **** private ssh key for git submodule
> git fetch --tags --progress origin +refs/heads/*:refs/remotes/origin/*
hudson.plugins.git.GitException: Command "git fetch --tags --progress origin +refs/heads/*:refs/remotes/origin/*" returned status code 128:
stdout:
stderr: Permission denied (publickey).
fatal: Could not read from remote repository.
Please make sure you have the correct access rights

由于这个项目是多个平台同时构建,各个平台的Jenkins项目配置是完全一致的,因此可以排除掉Jenkins配置问题,难道是macOS服务器配置错了?

网络分析实践之Dukto

最近在学习 wireshark 抓包进行网络分析,粗略地翻阅了几本相关的书籍后总感觉影响不够深刻,对于工具性的技术,最好的办法就是在实践中运用。刚好一直对于一直在用的一个工具Dukto的工作原理一直比较好奇,今天就来通过 wireshark 一探究竟。
Dukto是一款用于局域网文本/文件传输的小工具,自带局域网发现功能。本文主要分析Dukto如何实现局域网发现和数据传输。

踩坑记 - 消失的代码

示例代码

踩坑

最近在开发一个跨平台的 C++库时,在 Linux 平台运行非常正常,但在 Windows 下却怎么都没有预期的输出。为了简化问题,这里有一个存在同样问题的示例,可以看到示例中的代码非常简单,很显然预期输出应该是 5。

1
2
3
4
5
6
7
8
9
10
11
12
#include <iostream>

using namespace std;

int main()
{
int a = 0;
// 赋一个非零的值
a = 5;
cout << a << endl;
return 0;
}

然而这个示例项目使用 VS 打开并编译运行后,输出结果却是 0。
VS运行输出

搭建Windows符号服务器

在Windows桌面软件开发完成并发布给用户使用后,难免会遇到崩溃问题。通过CrashDump文件来分析崩溃问题是一个行之有效的途径。我们可以在程序崩溃时在客户电脑上生成CrashDump文件并收集这些文件。但要打开并分析CrashDump文件,我们还需要产生CrashDump文件的可执行文件(后续称为PE文件,包括exedll文件)以及编译该PE文件时产生的符号文件(PDB)。比较基础的做法一般是在从客户电脑上拿到CrashDump文件和可执行文件后,再根据可执行文件的版本,编译时间等信息从构建服务器等地方找到相对应的PDB文件。这样带来的一个问题是我们需要手动做文件的匹配,由于PE文件,PDB文件和CrashDump文件三者是严格匹配的,即使是相同的代码,每次编译产生的文件都不一样,因此手动匹配稍有差错就会找不到正确的符号,导致无法分析CrashDump。为了解决这一问题,一个更好的方法是搭建一个符号服务器,在符号服务器上保存每次自动构建出的PE文件和PDB文件,让调试器根据CrashDump文件自动识别和匹配。下面将会介绍如何搭建一个私有的符号服务器。

加快Qt程序的编译速度

在项目开发中,随着产品的更新迭代,项目会变得越来越大。项目的增大就会导致编译速度也会变得越来越慢。本人负责的一个项目的代码量如下

1
2
3
4
5
6
7
8
9
10
github.com/AlDanial/cloc v 1.80  T=0.07 s (2139.2 files/s, 210543.4 lines/s)
-------------------------------------------------------------------------------
Language files blank comment code
-------------------------------------------------------------------------------
C++ 73 1560 453 7993
C/C++ Header 78 1167 865 2919
Markdown 1 1 0 2
-------------------------------------------------------------------------------
SUM: 152 2728 1318 10914
-------------------------------------------------------------------------------

在不同的环境下全新构建一次耗费时间(单位:秒)如下

Qt Creator Visual Studio 2017 命令行
本地 81 241 246
服务器 - - 183

Qt从0到1之机制篇 - Qt样式表

本节示例代码

在Web前端开发中,有一个很重要的部分是CSS,用于修饰Html中的控件,如按钮,输入框等元素。CSS的出现分离了交互逻辑和视觉效果的开发,极大地方便了前端开发。因此这套设计理念被很多其他前端开发框架所借鉴,如Qt,Gtk,JavaFX等。在Qt中,默认的控件样式是平台相关的原始风格,很多时候难以满足视觉设计的要求,因此我们可以借助Qt中的CSSQt Style Sheet(一般简称为QSS)来对控件进行修饰美化,定制化出我们想要的效果。

Qt从0到1之机制篇 - 事件

本节示例代码

相信大家在初学C/C++等编程语言时,编写的都是在命令行下运行的程序,也就是运行时只用一个黑框的应用程序。和我们日常使用的GUI程序相比,这些程序除了没有一个GUI界面外,还有一个很大的区别在于这些程序是一种过程式的执行方式,执行完一行代码后再去执行下一句,执行完一个函数再去执行下一个,执行完之后程序就退出了,类似于一个批处理或脚本程序,而GUI程序是可以根据内部或外部的输入来确定执行哪些代码,以什么样的顺序去执行。这些输入可能是系统的分辨率进行了修改,或者是用户点击了一个按钮,按下了键盘上的某个键,触发了一个定时器等等。程序具体如何运行,是受到这些输入控制的,如果没有任何输入,程序将不执行任何代码。这就是批处理程序设计(batch programming)和事件驱动程序设计(Event-driven programming)的一个显著差异。

在事件驱动程序设计中,事件是核心要素,一个事件就是一个来自外界的信息输入。接触过Windows编程的人应该会了解过Windows的消息循环机制,这里的消息就类似于事件。Windows程序在收到系统派发的消息后,可以根据消息类型及消息参数的不同,进行不同的处理。例如当我们按下键盘上的Alt + F4快捷键后,Windows就会向当前的活动窗口发送一个类型为WM_CLOSE的消息,这个消息一般用于通过窗口进行关闭。窗口在收到这个消息后,原则上需要将自己关闭,但也可以选择忽略这个消息,这样我们的快捷键就“失效”了。

Qt从0到1之机制篇 - 对象树

本节示例代码

C/C++一直为人诟病的一点就是内存管理,C/C++提供了直接的内存操作接口给用户,这虽然效率上的优势,但也带来了内存泄漏等问题。虽然C++中引入了智能指针,但这对于管理一个GUI程序来说,一个控件中可能包含多个子控件,子控件也可能包含多个子控件,控件上可能还包含一些不可见对象,当一个控件被删除后,附在其上的子控件和不可见对象也需要一并删除,智能指针也难以实现这样的效果。因此Qt引入了对象树系统。

|