gdb实用的调试技巧:启动方式、堆栈信息、单步调试

对于很多开发者来说,开发过程中难免会遇到各种各样的bug, 所以,每个开发者应该考虑如何快速高效定位问题原因,而gdb是linux上很实用的调试工具,熟练掌握其调试技巧,将有助于提高解决问题的效率,也是开发者应该掌握的基本技能。

本文首先会讲解三种启动gdb的方式,然后再介绍两种查看堆栈信息的方法,最后再详细说明两种单步调试的实用技巧。

三种启动

通过gdb启动程序,通常有三种方式。下面分别进行讲解说明。

第一种方式是: gdb + 进程名。 编译程序的时候,需要加上-g选项,以便可执行程序中加入符号表信息,方便问题定位。

第二种方式,gdb –symbols=有符号表的进程 –exec=没有符号表的进程。首先创建没有符号表的进程,然后创建带有符号表的进程,最后再启动进程。

第三种方式,gdb -p 进程号。 这个方式也是最多使用的。因为进程崩溃的时候,仍然可以使用该方式,附着gdb到进程中,然后查看堆栈信息。

首先以后台运行的方式启动进程,然后查看进程的进程号,最后执行“gdb -p 进程号”的命令,把gdb附着到进程中,这样就可以很方便进行调试。

两种堆栈

当程序突然崩溃的时候,可以使用gdb附着到程序中,然后执行bt命令来查看最新的堆栈信息,这往往能够很快定位到问题的原因。

如果进程中启动很多线程,那么如何查看每个线程的堆栈信息呢,执行命令thread apply all bt,可以查看当前进程的所有线程的堆栈信息。

两种技巧

第一个技巧就是利用watch来观察某个变量的变化,当watch检测的变量发生变化的时候,gdb就会立即中断。

假设想要检测Fun函数下i_sum的变化情况,那么首先在该处设置断点,然后运行程序。

#include <string>
#include <iostream>
#include <thread>
#include <chrono>

void Fun(int iNum)
{
	int i_sum = iNum;
	for (int i = 0; i < 10000; i++)
	{
		i_sum += 2;
		std::this_thread::sleep_for(std::chrono::seconds(1));
	}
}

int main(void)
{
	std::cout <<"Start Fun" << std::endl;
	Fun(30);
	std::cout <<"End Fun" << std::endl;
	return 0;
}

运行到断点位置的时候,程序停止,那么可以使用p命令打印变量的值,还可以查看变量的地址,最后还可以使用该地址来设置watch的观察点。

第二个技巧就是单步调试,它的好处就是让开发者可以轻松跟踪代码的流程,从而不需要通过添加日志的方式来确定代码的流程。

假设有如下所示的代码段,我们通过单步调试的方式来跟踪代码流程。首先,通过gdb启动程序,或者程序附着上gdb之后,设置断点,然后从断点处开始跟踪代码流程。

#include <string>
#include <iostream>
#include <thread>
#include <chrono>

void Fun(int iNum)
{
	std::cout <<"Start Fun" << std::endl;

	int i_sum = iNum;
	for (int i = 0; i < 5; i++)
	{
		i_sum += 2;
		std::this_thread::sleep_for(std::chrono::seconds(1));
	}
	std::cout <<"i_sum: " << i_sum << std::endl;	
	std::cout <<"End Fun" << std::endl;	
}

int main(void)
{
	std::cout <<"Start Main" << std::endl;
	Fun(30);
	std::cout <<"End Main" << std::endl;
	return 0;
}

通过命令r启动程序,程序停止在断点处之后,使用n命令(next)进行单步运行,注意该命令不会进入函数内部跟踪。

那么如果想要进入函数,怎么办呢,可以使用s命令(step)来进入函数内部进行单步调试。

如果函数内部太长,不想要跟踪,那么可以输入命令finish, 直接跳出函数,注意该命令会执行完成函数之后,再退出。

而跳出函数,还有另一个命令return, 但是它不会执行完成函数再退出,而是直接退出。

最后补充一个查看局部变量的命令: info locals 。在断点位置的时候,使用该命令查看局部变量,有助于提高定位问题的效率。

总结

本文梳理了gdb的常用调试技巧,分别从三个方面进行说明,分别为三种启动、两种堆栈以及两种技巧。启动方式比较常用的是采用-p选项,将gdb附着到程序中。查看堆栈信息的命令bt适用于程序崩溃的问题,thread apply all bt命令则可以查看所有线程的堆栈信息。watch命令跟踪变量的变化情况。next则执行单步运行,但是不进入函数内部,step则会进入函数内部; 命令finish可以跳出函数,但是它会执行完成函数之后再退出;而命令return, 则不会执行完成函数,而是马上就退出。

采用样式表,实现控件同时显示图标和文本的效果

工程项目中,需要在按钮QPushButton上同时显示文本和图标,文本在左,图标在右,并且操作过程中按钮样式的能够跟随着变化,比如按下按钮的时候,文本和图标能够显示按下的状态等。

控制文本变化,可以通过qss样式表来实现,并且难道不大,但是同时需要在按钮上显示图标,并且能够控制图标的变化。由于第一次接受到这样的需求场景,所以需要研究尝试简单可行的办法。

开始的想法是自定义QPushButton按钮,但是实现难度大,而且复杂。所以,考虑是否能够通过样式表来实现图标显示要求。

本文介绍两种显示图标的方法,经过调试,第二种方法满足需求,下来就来详细说明分析实现的过程,再梳理总结两种方法的优缺点。最后扩展内容,实现QListView的item,左侧显示图标,右侧显示文本的效果。

盒子模型

正式说明功能实现之前,需要了解盒子模型,主要是关于background-origin的三个属性值,分别为border、padding、content。

属性background-origin的功能是设置背景图像相对于什么类型来定位。

盒子模型如上图所示,最外层为边框border, 中间为内边距padding, 最里面为内容content。如果设置 background-origin为border表示图像背景相对于border左上角来定位,同样的,如果设置 background-origin为content, 那么图像背景就相对于content的左上角的定位。

icon方式显示图标

基于图标icon来实现按钮同时显示图标和文本的功能,其样式表设置如下, 使用qproperty-icon属性来添加图标。

QPushButton 
{
     qproperty-icon: url(":/add_normal.png");
}

运行效果如下所示,图标在左,文本在右。

如果想要放大按钮图标,那么可以添加属性qproperty-iconSize来实现。

QPushButton 
{
    property-icon: url(":/add_normal.png");
    qproperty-iconSize: 20px; 
}

如果想要将图标显示在文本的右侧,可以通过添加Qt::LayoutDirection::RightToLeft的属性来实现。

p_one_btn->setLayoutDirection(Qt::LayoutDirection::RightToLeft);

运行效果如下所示,可以看到文本在左,图标在右。

背景图像方式显示图标

采用背景图像方式来显示图标,那么就需要background-origin属性登场。其样式表进行如下设置。background-image设置背景图像,background-repeat为none表示设置背景图像不重复,background-position设置背景图像的位置,background-origin为content表示背景图像是相对于内容框来定位的。

QPushButton 
{
    border-image: none;
    background-image: url(:/add_normal_small.png);
    background-repeat: none;    /* 背景图片不重复 */
    background-position:right;  /* 背景图片位置 */
    background-origin:content;  /* 相对于内容框定位 */
}

运行效果如下所示,可以发现图标太靠近按钮右边缘,不太美观,那么怎么修改呢?

基于盒子模型的分析,目前图标是位于内容框,那么只要调整内右边距的大小即可,比如设置内右边距的大小为20px, 即添加属性padding-right: 20px。

再次运行的效果图如下,可以发现图标已经偏离按钮的右边缘。

如果想要悬停或者按下按钮,图标发生改变,那么再添加如下样式表,悬停的时候更换背景图,按下的时候更换其他背景图。

QPushButton:hover
{
    background-image: url(:/add_hover_small.png);
}

QPushButton:pressed
{
    background-image: url(:/add_selected_small.png);
}

最后运行的动态效果图

两种方式的优缺点

icon方式显示图标的优点是简单,并且能够控制图标的大小,缺点是图标和文本之间的间隔很难自由调整,并且通过样式表无法动态更换图标。

背景图像方式显示图标的优点是图标和文本之间的间隔能够自由调整,能够动态更换图标,缺点是背景图像无法自适应,如果你的屏幕不需要自适应功能,那么这种方法是比较好的。

扩展

最后再来说明如何实现QListView的item,左侧显示图标,右侧显示文本的效果。设置的样式表内容如下,这里设置background-origin为border,表示背景图像相对于边框来定位。

QListView::item 
{
    background-image: url(:/u_disk_icon.png);
    background-repeat: none;    /* 背景图片不重复 */
    background-position:left;  /* 背景图片位置 */
    background-origin:border;
    padding-left: 30px;        /* 内左边距 */
    margin-left: 5px;
    padding-top: 5px;
    padding-bottom: 5px;
}

运行的界面效果如下,如果想要加大图标与文本之间的间隔,那么需要设置内左边距。

总结

至此,采用样式表,实现控件同时显示图标和文本信息的功能已经讲解完毕,界面的设计,需要平时不断积累和总结,熟悉了控件的各种属性之后,就能够轻松自如应对各种界面变化。另外,对于本文讲解的内容,如果有牛人,能够提供更好的解决方案,欢迎分享!

三种基本用法、五种应用场景,理清C++11新特性:Lambda表达式

lambda表达式是指能够捕获作用域中的变量的无名函数对象,狭义的理解,就是匿名函数。

无论是在项目中,还是在开源网站,总是能够看到lambda的身影。

为了能够轻松阅读代码,进而熟练地使用,本文首先将讲解lambda表达式的基本语法、三种基本用法,然后介绍五种实际的应用场景,最有总结说明lambda表达式的作用。

表达式语法

首先看下lambda表达式的基本语法,它由五部分组成,分别为捕捉列表,参数列表,修饰符,返回类型和函数体。

捕捉列表可以设定当前是值传递方式[=]或者引用传递方式[&], 参数列表则就是函数的参数,返回类型表示函数体执行完成之后返回的类型、比如整型int、布尔型bool等。函数体则是实现实现的功能。另外,由于lambda表达式默认返回是const类型,如果想要取消const属性,那么需要加上修饰符mutable。

基本用法

现在通过三个简单的例子来介绍lambda表达式的用法,加深对其的理解。

第一个例子是函数没有返回值的形式,首先定义四个变量,然后创建lambda表达式ret, 内部直接打印父作用域四个变量的值,注意[=] 表示值传递方式捕捉所有父作用域的对象。

    int i_a = 3, i_b = 4, i_c = 5, i_d = 6;
    auto ret = [=]()
    {
        std::cout << "i_a = " << i_a << std::endl;
        std::cout << "i_b = " << i_b << std::endl;
        std::cout << "i_c = " << i_c << std::endl;
        std::cout << "i_d = " << i_d << std::endl;
    };
    ret();

运行程序输出的结果如下,调用lambda表示式ret()之后,正确的输出了父作用域的所有的值。

i_a = 3
i_b = 4
i_c = 5
i_d = 6

第二个例子是增加函数的返回值,先计算父作用域所有变量的和,作为返回值。

    int i_a = 3, i_b = 4, i_c = 5, i_d = 6;
    
    auto ret_02 = [=]() ->int
    {
        return i_a + i_b + i_c + i_d;
    };
    std::cout << "ret_02 = " << ret_02() << std::endl;

从运行的结果看,调用匿名函数ret_02(), 正确返回了所有父作用域变量值的和

ret_02 = 18

第三个例子是lambda表达式修改父作用域的值,如果想要修改父作用域的值,那么需要通过引用传递的方式。

    int i_a = 3, i_b = 4, i_c = 5, i_d = 6;    
    auto ret_03 = [&i_b]() mutable->int
    {
        i_b++;
        return i_b;
    };
    std::cout << "ret_03 = " << ret_03() << std::endl;
    std::cout << "i_b = " << i_b << std::endl;

通过引用传递的方式,将父作用域的变量i_b传递给lambda表达式的捕捉列表, 从输出的结果看,父作用域的值被修改了。

ret_03 = 5
i_b = 5

注意,捕捉列表支持传递多个值,各个值之间是通过逗号隔开。

    int i_a = 3, i_b = 4, i_c = 5, i_d = 6;
    
    auto ret_02 = [=, &i_a, &i_b]() ->int
    {
        return i_a + i_b + i_c + i_d;
    };

应用场景

上面只是简单介绍了lambda表达式的使用,接下来将讲解lambda表达式的五种应用场景。

第一种场景是查找功能,首先创建list列表来存储数据,构建的数据内容如下。

struct JFaultParam
{
   int m_iFaultId;
   std::string m_strInfo;
    JFaultParam() : m_iFaultId(-1), m_strInfo("")
    {}
};

    std::list<JFaultParam> list_faults;
    JFaultParam fault_parma_01;
    fault_parma_01.m_iFaultId = 1;
    fault_parma_01.m_strInfo = "one";
    list_faults.push_back(fault_parma_01);
    JFaultParam fault_parma_02;
    fault_parma_02.m_iFaultId = 2;
    fault_parma_02.m_strInfo = "two";
    list_faults.push_back(fault_parma_02);
    JFaultParam fault_parma_03;
    fault_parma_03.m_iFaultId = 3;
    fault_parma_03.m_strInfo = "three";
    list_faults.push_back(fault_parma_03);

接着调用find_if函数来查找项目,判断逻辑则是通过lambda表达式来实现。这里为了简化,将i_fault_id声明为局部变量,其实,可以把以下功能使用函数来封装,然后i_fault_id作为参数传递。

    int i_fault_id = 2;
    auto iter = std::find_if(list_faults.begin(), list_faults.end(),
    [i_fault_id](const JFaultParam& param)->bool
    {
        return param.m_iFaultId == i_fault_id;
    });

    if (iter != list_faults.end())
    {
        // to do something
    }

第二种场景是打印功能,调用for_each来循环访问向量,lambda表达式作为第三个参数,功能是打印输出向量值。

    std::vector<int> vec_data;
    vec_data.push_back(1);
    vec_data.push_back(2);
    vec_data.push_back(3);

    std::for_each(vec_data.begin(), vec_data.end(), [&]( int i_data ){ std::cout << i_data <<",";});

第三个场景是线程等待,创建启动线程,线程的功能是使用条件变量等待,判断条件使用lambda表达式来实现,如果队列为空,那么返回false, 条件变量继续等待,如果队列不为空,那么继续往下执行。

    std::mutex mutex;
    std::condition_variable condition;
    std::queue<std::string> queue_data;

    std::thread thread_obj( [&]
    {
        std::unique_lock<std::mutex> lock_log(mutex);
        condition.wait(lock_log, [&] {return !queue_data.empty();});
        LOG(INFO) << "queue data :" << queue_data.front();
        lock_log.unlock();
    }) ;

    queue_data.push("this is my data");
    condition.notify_one();

    if (thread_obj.joinable())
    {
        thread_obj.join();
    }

第四种场景作为函数的入参,首先定义函数TestCallback,入参为函数对象。

using  FuncCallback = std::function<void(void)>;

void TestCallback(FuncCallback callback)
{
    LOG(INFO) << "Start TestCallback";
    callback();
    LOG(INFO) << "End TestCallback";
}

接着创建创建lambda表达式callback_handler,将其作为参数传递给上面实现的函数TestCallback。这里运用到开闭原则,TestCallback只接收参数并执行,具体执行什么内容则由外部传递进来的lambda表达式决定。

    auto callback_handler = [&]()
    {
        LOG(INFO) << "this is callback_handler";
    };
    TestCallback(callback_handler);

第五种场景删除功能,std::remove_if支持三个参数,前两个表示迭代器的起始和结束位置,第三个参数传递的是一个回调函数,如果回调函数返回真,那么表示应该移除。这里回调函数使用了lambda表达式。注意remove_if不会真正删除元素,它将不需要移除的元素依次替换掉序列中前面的元素,并返回应移除的第一个元素的迭代器。

如下图的例子中,std::remove_if执行完成之后,输出的值是5 6 7 4 5 6 7,即 5 6 7是不需要删除的元素,将其移到1 2 3的位置,并且返回应移除的第一个元素的迭代器,即4的位置。最后使用erase删除元素,你会发现输出的值是5 6 7。

    std::vector<int> vec_data = {1, 2, 3, 4, 5, 6, 7};
    int x = 5;
    vec_data.erase(std::remove_if(vec_data.begin(), vec_data.end(), [x](int n) { return n < x; }), vec_data.end());

    std::for_each(vec_data.begin(), vec_data.end(), [](int i){ std::cout << i << ' '; });
    std::cout << '\n';

总结

从上面介绍的lambda表达式的三种用法和五种应用场景看,可能会发现,如果不了解lambda表达式的语法,会觉得深奥难懂,但是梳理清楚之后,将会发现它其实不难,并且能够使得代码简洁易读。

一种解决多层嵌套导致卡顿问题的新思路

工作过程中,经常会收到来自测试部提出的界面跳转卡顿的相关问题,特别是在压力测试的时候。对于卡顿的问题,总是需要花费我们大量的时间去定位问题,即使定位到问题,仍然需要花费大量时间来思考优化的方案,并进行验证,如果验证不通过,还需要再次思考新的方案,如此循环反复。

最近在工作中就遇到多层嵌套导致卡顿的问题,虽然很快找到问题的原因,但是寻找问题的解决方案,还是花了一个星期的时间,所以,这里将分析解决的过程记录下来,一方面是对自己的总结,方便后续的回顾,另一方面分享出来,希望能够给遇到类似问题的人参考借鉴的作用。

问题现象

测试部搭建自动化的测试环境,在机器上创建用户,首先存储图像,然后将图像发送到服务器,并且限制发送的速度,这样不断的执行存储和发送的操作。由于网络限速,所以界面上显示待发送的数据就会不断累积,达到上千,甚至是上万。当界面上,待发送的队列达到4000多个的时候,出现操作界面卡顿的现象。

分析过程

测试部反馈问题之后,马上到现场获取相关信息,但是根据现有信息无法找到问题原因,于是推测跟网络有关,于是重启机器,发现问题不卡顿了,结合重启前后的差异,发现重启之后,是没有激活用户的。重新激活用户,结果界面又卡顿了,找到问题必现的方法之后,自然而然地找到导致卡顿的代码位置。

原来问题出在两个for循环,vct_data向量的大小为4000多个,vct_image向量的大小也为4000多个,两个嵌套for循环执行的次数就有1600万次,这导致了从开始到结束花费了8秒多时间。

// 从数据库中获取所有待发送命令
std::vector<std::map<std::string, std::string>> vct_data;
...

//  从数据库中获取当前用户所有的图像
std::vector<std::map<std::string, std::string>> vct_image;
...

// 循环比较两个向量,获取当前用户待发送的图像
for (size_t   i = 0;  i < veg_data.size(); i++)
{
    for ( size_t j = 0; j < vct_image.size(); j++)
    {
    }
}

找到问题原因 之后,接下来就来寻找解决方案。

解决方案

由于获取所有待发送命令和当前用户待发送的图像的数据来源是数据库,那么首先考虑到的解决方案就是创建视图,将两个数据库表结合起来,然后应用直接从视图获取结果信息,从而代替两个嵌套的for循环。

select a.*, b.*  from table_command as a left join table_image b on a.uid = b.uid

经验验证,该方案是可行的,但是由于要修改数据库,改动量大,所以,暂时不考虑。

接着考虑从数据结构上进行优化,由于table_image中的uid是唯一的,而table_command中uid则可能重复,那么就从尽量减少执行的次数来进行优化。从下图处理方式可以看出,当找到符合的条件之后,从vct_data获取所有数量,然后删除vct_image中对应的项,最后执行break退出循环,

int i_active_size = 0;
for (size_t  i  = 0; i < vct_data.size(); i++)
{
    auto iter = vct_image.begin();
    for (;iter != vct_image.end(); iter++)
    {
        if (vct_data的uid  == (*iter))
        {
             i_active_size += count(vct_data.begin() ,vct_data.end(),vct_data的uid);
             vct_image.erase(iter);
             break;          
        }
    }    
}

经过验证,该方案只能减少卡顿的时间,但是随着数量的不断增加,卡顿的时间也会不断增加,因此,不是最优的方案,不采用。

最后想到的解决方案是最优的,并且不需要修改数据库,改动量比较小。其解决方法是修改查询数据库的方法,借助数据库的关键字IN来达到获取相关数据的目的,从而代替了嵌套的for循环。将从数据库table_image获取的所有uid传入数据库的查询语句IN中。

select * from table_command where ... and uid IN('UID1', 'UID2', ...);

总结

工作过程中遇到难题的时候,往往需要从不同的角度去分析,从而找到解决问题的最优方案。卡顿的问题,当然是从现场获取必要的数据,从而可以尽快找到问题的根源,而解决方案可以从三个角度入手,第一个是直观想到的解决方案,第二个是数据结构,第三个查询方式。

解锁新技能,python与cmake结合,实现自动生成工厂类

工作过程中,当需要创建很多同一类对象的时候,为了方便管理和维护,通常通过创建一个工厂类,由该工厂类根据不同的参数来创建返回对象,但是在C++中,存在这样的问题,每当新建新的对象的时候,都需要修改工厂类来支持新的对象的创建。从这个过程中,我们可以看出,操作相当繁琐和重复。

基于以上的原因,我们根据头文件定义的类型,使用python来自动创建工厂类文件,同时,将其集成到cmake。所以,每次头文件添加新的类型,那么只需要重新执行cmake来自动更新工厂类文件。

生成文件

假设有如下所示的头文件JMsgDefine.h, 一个枚举值对应一个命令对象,比如接收到E_CMD_ID_LOAD,那么对应的命令类名为JLoadCmd。

#pragma once

enum E_CMD_ID
{
	E_CMD_ID_LOAD	= 1,
	E_CMD_ID_UNLOAD = 2,
	E_CMD_ID_MAX
};

有了上面的枚举定义之后,现在就需要实现python文件,命名为JCmdFactory.cpp.py。实现功能函数之前,需要先实现两个支持函数,用于后面函数的调用。

第一个支持函数是get_cmd_id_enum_name_list,该函数的功能是从文件提取枚举名称列表。

  def get_cmd_id_enum_name_list():
	'''从文件提取枚举名称列表'''
	enum_name_list = []

	with open('JMsgDefine.h', 'r') as f:
		pattern = re.compile('E_CMD_ID_[A-Z_]+')
		for line in f:
			match = pattern.search(line)
			if match and match.group(0) != "E_CMD_ID_MAX" :
				enum_name_list.append(match.group(0))
	return enum_name_list

第二个支持函数是convert_enum_name_to_class_name,该函数的功能是将枚举名转化为类名,例如E_CMD_ID_LOAD转化为JLoadCmd。

def convert_enum_name_to_class_name(enum_name):
	'''将枚举名转化为成类名,如E_CMD_ID_LOAD 转化为 JLoadCmd'''
	word_list = enum_name.split('_')
	'''删除位置0、1、2的数据,不包括位置3'''
	del word_list[0:3]

	class_name = 'J'
	for word in word_list:
		# title返回第一个字符大写的字符串
		class_name += word.title()
	class_name += 'Cmd'

	return class_name

接下来就来说明如何利用python自动生成工厂类文件。一般一个文件通常包含了注释说明、头文件的声明以及函数的实现。所以,就按照这三个步骤来实现自动生成工厂类的python文件。

写入注释

首先实现注释说明的功能函数,该注释主要提示使用者,文件是自动生成的,不能编辑和提交。

def print_comment(f):
	'''打印注释'''
	f.write('''\
/******************************************************************************
** This file is auto generated by python script, please don't edit and commit.)
** Created by : cmake and python 
** Warning!!! All changes made in this file will be lost!
****************************************************************************/''')
	f.write('\r\n')

写入头文件

然后实现头文件声明的功能函数print_include,它接受两个参数,第一个参数表示文件描述符,第二个参数表示枚举列表值。然后内部函数循环遍历枚举列表,将每个枚举字符串转化为类名,再构建头文件的声明,最后将其写入文件中。

def print_include(f, enum_name_list):
	'''包含头文件'''
	f.write('#include "JMsgDefine.h"\r\n')

	for enum_name in enum_name_list:
		class_name = convert_enum_name_to_class_name(enum_name)
		f.write('#include  "' + class_name + '.h"\r\n')
	f.write('\r\n')

写入函数功能

最后实现将根据枚举值动态创建对象的函数写入文件的功能,print_function同样接收两个参数,其参数功能与print_include是一样的,这里不再重复多说。print_function函数的功能是将创建命令工厂的方法写入文件中。

def print_function(f, enum_name_list):
	'''函数定义'''
	f.write('// 创建命令工厂方法\r\n')
	f.write('JBaseCmd* JCmdFactory::Create(FRAME *pMsg)\r\n')
	f.write('{\r\n')

	f.write('\tJBaseCmd *p_cmd = nullptr;\r\n')
	f.write('\tint i_cmd_id = GetCmdId(pMsg);\r\n')
	f.write('\tswitch (i_cmd_id)\r\n')
	f.write('\t{\r\n')	

	for enum_name in enum_name_list:
		f.write('\t\tcase '  + enum_name + ':\r\n')
		f.write('\t\t{\r\n')
		class_name = convert_enum_name_to_class_name(enum_name)
		f.write('\t\t\tp_cmd = new ' + class_name + '(pMsg);\r\n')
		f.write('\t\t\tbreak;\r\n')
		f.write('\t\t}\r\n')

	f.write('\t\tdefault: \r\n')
	f.write('\t\t{\r\n')
	f.write('\t\t\t// no support\r\n')
	f.write('\t\t\tbreak;\r\n')	
	f.write('\t\t}\r\n')

	f.write('\t}\r\n')
	f.write('\t\r\n')
	f.write('\treturn p_cmd;\r\n ')
	f.write('}\r\n')

验证效果

完成python文件JCmdFactory.cpp.py之后,手动执行该python文件,可以看到自动生成了工厂类文件,其内容如下所示:

/******************************************************************************
** This file is auto generated by python script, please don't edit and commit.)
** Created by : cmake and python 
** Warning!!! All changes made in this file will be lost!
****************************************************************************/
#include "JMsgDefine.h"
#include  "JLoadCmd.h"
#include  "JUnloadCmd.h"

// 创建命令工厂方法
JBaseCmd* JCmdFactory::Create(FRAME *pMsg)
{
	JBaseCmd *p_cmd = nullptr;
	int i_cmd_id = GetCmdId(pMsg);
	switch (i_cmd_id)
	{
		case E_CMD_ID_LOAD:
		{
			p_cmd = new JLoadCmd(pMsg);
			break;
		}
		case E_CMD_ID_UNLOAD:
		{
			p_cmd = new JUnloadCmd(pMsg);
			break;
		}
		default: 
		{
			// no support
			break;
		}
	}
	
	return p_cmd;
 }

提取路径

那么如何将python文件集成到cmake呢,首先需要说明正则表达式的几个用法,然后再详细说明将python集成到cmake的方法。

总结列出正则表达式几个关键符号的含义

关键符号含义
()标记一个子表达式的开始和结束
*匹配前面字符的零次或多次
+匹配前面自发的一次或多次
.匹配单字符
^匹配输入字符串的开始位置
$匹配输入字符串的结束位置

\1\2等符号

\1、\2等需要和正则表达式()一起使用才行。比如\1表示重复第一个()匹配到的内容, \2表示重复第二个()匹配到的内容,以此类推。

比如正则表达式“^(123)(456)\2\1$ ”, 那么字符串“123456456123”,则能够被正则表达式匹配到。

解析路径

假设现在有如下路径“the/picture/ACmd.cpp.py”, 那么如何提取目录和CPP文件呢?

如果想要提取目录,那么输入正则表达式”^(.+/).+\.py”, 则符号()匹配到的字符串为“the/picture/”。注意.py前面的符号“\”, 是转义符号,表示.不作为关键符号处理。

如果想要提取CPP文件,那么输入正则表达式“^(.+)\.py”, 则符号()匹配到的字符串为“the/picture/ACmd.cpp”

自动调用

下面列出嵌入到cmake文件中自动调用python文件的代码段。

file(GLOB_RECURSE PY_FILES ${PROJECT_SOURCE_DIR}/*.py)
message("PY_FILES")
foreach(PY_FILE ${PY_FILES})
	message(STATUS  "PY_FILE: " ${PY_FILE})
	string(REGEX REPLACE "^(.+/).+\\.py$" "\\1" PY_DIR ${PY_FILE})
	string(REGEX REPLACE "^(.+)\\.py$" "\\1" CPP_FILE ${PY_FILE})
	execute_process(COMMAND python ${PY_FILE} WORKING_DIRECTORY ${PY_DIR} RESULT_VARIABLE PY_EXE_RESULT)
	if (NOT PY_EXE_RESULT)
		message(STATUS "auto generate cpp file:" ${CPP_FILE})
	endif()
endforeach()	

现在一一解释cmake关键字的功能含义

file是递归查找${PROJECT_SOURCE_DIR}/*.py匹配到的所有文件,然后存储到PY_FILES。PROJECT_SOURCE_DIR是cmake内置的定义,表示工程源代码目录。

string则利用正则表达式来提取目录和文件,string的结构如下图所示。正则表达式的含义上一节已经说明了。这里有一个需要注意的是”\\1″, 它的功能就是正则表达式中的”\1″, 那么为什么会多出一个“\”, 个人理解符号“\”也是cmake的关键符号,所以需要先转义一层。

string(REGEX REPLACE <regular_expression>
       <replace_expression> <output variable>
       <input> [<input>...])

execute_process则是执行提取到的python文件。

总结

本文首先基于python,实现了从枚举类型中提取创建对象,然后自动生成工厂类文件的python文件,接着将实现的python文件集成到cmake中,从而在编译代码时,自动生成工厂类文件,因此,解决了创建相似代码的重复性动作的问题。

学习3D特效前的准备阶段:使用mac系统,基于cmake搭建OpenGL环境

工作中越来越多的需求是要将数据以3D的形式展示给用户,但是在实现3D特效之前,都需要有一个准备阶段。OpenGL规范描述了绘制2D和3D图形的抽象API,正所谓“工欲善其事必先利其器”,所以,学习OpenGL之前,当然是需要搭建好运行的环境。

今年的目标就是熟练掌握好OpenGL的入门知识,之前由于工作的原因,迟迟无法开展,今天终于抽空搭建好了OpenGL环境。

搭建OpenGL环境过程中,也是花费了不少时间,特别是基于cmake来搭建OpenGL的环境,网络上的资料很少,并且也没找到成功的例子,因此,这里将搭建的过程分享出来。希望能够对大家有一定的参考价值。

本文首先会介绍搭建OpenGL环境之前的准备条件,然后再总体说明搭建的流程,接着再针对每个流程进行详细的说明。这里不会涉及太多专业术语,本文的目的就是要快速搭建好环境,方便后续的学习和工作的开展。

一、准备条件

本文OpenGL环境是在mac系统上搭建,并且采用cmake来组织编译环境,使用的IDE工具是QtCreator 4.9.1, mac操作系统的版本是10.13.6。

搭建opengl环境的大致步骤为,构建glfw、构建glad, 链接系统自带的OpenGL库。

glfw的作用是创建OpenGL上下文、定义窗口参数以及处理用户输入;

glad根据版本加载所有相关的OpenGL函数。

OpenGL库则是实现了规范的抽象API的集合。

二、创建工程

使用qtcreator工具来创建C++工程,工程名称为StudyOpengl

然后选择cmake来编译系统,其他按照默认进行操作即可

三、构建GLFW

官方网站上下载glfw的源代码包,其代码的目录结构如下图所示

然后使用qtcreator来打开glfw源代码包中的CMakeLists.txt, 目的就是使用qtcreator来编译glfw代码,并生成静态库

加载成功glfw的工程目录

选中工程根目录,然后右键弹出的菜单,点击“构建”选项

如果编译成功,那么在编译目录下能够看到名称为libglfw3的静态库

最后将glfw的头文件和静态库拷贝到工程StudyOpengl下,到这里,glfw的构建工作已经完成,后面还需要添加glfw的头文件和库的搜索路径,这个等到整体编译的时候,再统一进行说明。

四、构建GLAD

构建glad, 为了方便,我们这里直接使用网络上提供的在线服务来进行创建,具体的在线服务可以网络上进行搜索。

进入glad的在线服务页面如下所示,语言选择C/C++, API版本选择最新的为4.6, Profile下选择Core

拖动页面到最底部,勾选“Generate a loader”选项,然后点击“GENERATE”

等待片刻,即可生成glad的压缩包,将压缩包里面的include文件夹和glad.c文件拷贝到工程StudyOpengl, 至此,glad的构建工作也完成,添加头文件和库的搜索路径也等到整体编译的时候,再统一说明。

五、链接OpenGL

链接OpenGL,我觉得是最重要的一步,这是自己摸索出来的解决方法。这里链接是mac系统自带的opengl库。

项目工程StudyOpengl中的CMakeLists.txt文件中,添加搜索OpenGL库的路径,创建变量来保存Cocoa、CoreVideo、IOKit的库路径

find_package(OpenGL REQUIRED)
set(SYS_FRAMEWORKS_DIR /System/Library/Frameworks)
set(COCOA_LIBS ${SYS_FRAMEWORKS_DIR}/Cocoa.framework)
set(COREVIDEO_LIBS ${SYS_FRAMEWORKS_DIR}/CoreVideo.framework)
set(IOKIT_LIBS ${SYS_FRAMEWORKS_DIR}/IOKit.framework)

接着添加opengl的头文件搜索路径

include_directories(${OPENGL_INCLUDE_DIR})

最后将OpenGL、Cocoa、CoreVideo、IOKit的库链接到工程。

target_link_libraries(${PROJECT_NAME}
        ${OPENGL_LIBRARIES}
        ${COCOA_LIBS}
        ${COREVIDEO_LIBS}
        ${IOKIT_LIBS})

六、整体编译

cmake文件增加glad和glfw的头文件搜索路径,首先创建函数include_sub_directories_recursively,该函数的功能是递归的将输入路径下的所有目录添加到头文件的搜索路径

###
### 添加头文件搜索路径
###
function(include_sub_directories_recursively root_dir)
        if (IS_DIRECTORY ${root_dir})
                # include_directories包含头文件的搜索路径
                message("root_dir : ${root_dir}")
                include_directories(${root_dir})
        endif()
        # 如果 RELATIVE 标志位被设定,将返回指定路径的相对路径
        file (GLOB ALL_SUB RELATIVE ${root_dir} ${root_dir}/*)
        foreach(sub ${ALL_SUB})
                if (IS_DIRECTORY ${root_dir}/${sub} AND (NOT ("${sub}" STREQUAL ".svn")) AND (NOT ("${sub}" STREQUAL ".DS_Store")) )
                        include_sub_directories_recursively(${root_dir}/${sub})
                endif()
        endforeach()
endfunction()

include_sub_directories_recursively(${PROJECT_SOURCE_DIR}/code/third)

include_directories(
        ${CMAKE_CURRENT_BINARY_DIR}
        ${PROJECT_SOURCE_DIR}
        ${PROJECT_SOURCE_DIR}/code/third/glad/include
)

添加完成头文件的搜索路径之后,接下来就来添加库的搜索路径,同样的,实现函数link_sub_directories_recursively用于将输入路径下的所有目录都添加到库的搜索路径中

###
### 添加库的查找路径
###
function(link_sub_directories_recursively root_dir)
        if (IS_DIRECTORY ${root_dir})
                # link_directories(directory1 directory2 ...)
                # 指定查找库的目录
                link_directories(${root_dir})
        endif()

        # 如果 RELATIVE 标志位被设定,将返回指定路径的相对路径
        file (GLOB ALL_SUB RELATIVE ${root_dir} ${root_dir}/*)
        foreach(sub ${ALL_SUB})
                if (IS_DIRECTORY ${root_dir}/${sub} AND (NOT ("${sub}" STREQUAL ".svn")))
                        link_sub_directories_recursively(${root_dir}/${sub})
                endif()
        endforeach()
endfunction()

link_sub_directories_recursively(${PROJECT_SOURCE_DIR}/depend)

最后记得将libglfw3.a的静态库链接到工程中

target_link_libraries(${PROJECT_NAME}
        glfw3
        pthread
        ${OPENGL_LIBRARIES}
        ${COCOA_LIBS}
        ${COREVIDEO_LIBS}
        ${IOKIT_LIBS})

为了测试环境是否生效,使用网络上一位大神写的代码来进行测试,直接将以下代码拷贝到main.cpp文件

#include <iostream>
#include "glad/glad.h"
#include "glfw3.h"

using namespace std;

void framebuffer_size_callback(GLFWwindow* window, int width, int height);
void processInput(GLFWwindow *window);

// settings
const unsigned int SCR_WIDTH = 800;
const unsigned int SCR_HEIGHT = 600;

int main()
{
    // glfw: initialize and configure
    // ------------------------------
    glfwInit();
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);

    #ifdef __APPLE__
    glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
    #endif

    // glfw window creation
    // --------------------
    GLFWwindow* window = glfwCreateWindow(SCR_WIDTH, SCR_HEIGHT, "LearnOpenGL", NULL, NULL);
    if (window == NULL)
    {
        std::cout << "Failed to create GLFW window" << std::endl;
        glfwTerminate();
        return -1;
    }
    glfwMakeContextCurrent(window);
    glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);

    // glad: load all OpenGL function pointers
    // ---------------------------------------
    if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
    {
        std::cout << "Failed to initialize GLAD" << std::endl;
        return -1;
    }

    // render loop
    // -----------
    while (!glfwWindowShouldClose(window))
    {
        // input
        // -----
        processInput(window);

        // render
        // ------
        glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT);

        // glfw: swap buffers and poll IO events (keys pressed/released, mouse moved etc.)
        // -------------------------------------------------------------------------------
        glfwSwapBuffers(window);
        glfwPollEvents();
    }

    // glfw: terminate, clearing all previously allocated GLFW resources.
    // ------------------------------------------------------------------
    glfwTerminate();
    return 0;
}

// process all input: query GLFW whether relevant keys are pressed/released this frame and react accordingly
// ---------------------------------------------------------------------------------------------------------
void processInput(GLFWwindow *window)
{
    if(glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
        glfwSetWindowShouldClose(window, true);
}

// glfw: whenever the window size changed (by OS or user resize) this callback function executes
// ---------------------------------------------------------------------------------------------
void framebuffer_size_callback(GLFWwindow* window, int width, int height)
{
    // make sure the viewport matches the new window dimensions; note that width and
    // height will be significantly larger than specified on retina displays.
    glViewport(0, 0, width, height);
}

不出意外的话,编译运行之后,可以看到如下所示的效果

六、概括总结

至此,基于mac系统,通过cmake搭建OpenGL环境已经完成。我们再来梳理下,首先通过q tcreator创建工程,接着构建glfw和glad, 然后添加glfw、glad、opengl的头文件搜索路径,最后链接glfw库和opengl的相关库。最后一点需要注意,要确定glad.c文件编译进工程,否则可能会出现奇怪的编译错误提示信息。

基于实例快速理解Qt模型视图的应用


Qt的开发过程中,经常会应用到它的模型视图结构,对于初学者来说,马上理解其原理,并且进行基础的应用,还是比较难的。另外,通过网络搜索查看别人发表有关模型视图的介绍,基本上都不能解决个人的一些疑惑。基于此,本文将结合实例来说明模型设计的应用,以便能够更加深刻的理解Qt模型视图的设计原理。

任何知识,如果想要能够应用自如,个人觉得首先需要对该知识,需要有一个整体的认识。所以,首先将对模型视图结构进行总体的说明,然后再结合实例来说明其应用,并且重点介绍代理的两种应用方式,最后再进行总结。

一、模型视图简介

Qt的模型视图与MVC设计模式类似,但是又不相同,MVC设计模式包括模型、视图和控制。模型表示数据,视图表示用户界面,控制定义了用户的操作。它能够有效将数据和显示分离,提高了代码的灵活性。Qt的模型视图同样有这样的效果。但是Qt的模型视图将视图和控制放在一起,以便简化框架。另外,为了能够更好的处理用户输入,Qt的模型视图加入的代理。通过代理能够自定义item的显示和编辑。

Qt的模型视图结构如下图所示,主要包含三个部分,分别为模型、视图和代理。模式与数据源通信,给其他部件提供接口。视图从模型中通过ModelIndex获取信息,代理用来显示和编辑item。

二、模型

模型的抽象基类为QAbstractItemModel,  下图显示了其继承关系。

QAbstractTableModel是表格的抽象基类,QAbstractListMode的列表的抽象基类。QAbstractProxyModel是代理的抽象基类。QDirModel是文件和目录的模型。QSqlQueryModel是有关数据库的模型。

模型的数据可以直接存在模型中,也可以由外部输入,由独立的类管理,还可以存在文件中,甚至是数据库。

模型中角色的功能是,根据不同的角色提供不同的数据。支持以下几种角色。

1、Qt::DisplayRole,   显示文字

2、Qt::DecorationRole,绘制数据

3、Qt::EditRole,  编辑器中的数据

4、Qt::ToolTipRole, 工具提示

5、Qt::StatusTipRole, 状态栏提示

6、Qt::SizeHintRole, 尺寸提示

7、Qt::FontRole, 字体

8、Qt::TextAlignmentRole,  对齐方式

9、Qt::BackgroundRole, 背景画刷

10、Qt::ForegroundRole, 前景画刷

11、Qt::CheckStateRole, 检查框状态

12、Qt::UseRole, 用户自定义数据的起始位置

三、视图

视图的抽象基类为QAbstractItemView,  它由五个基本视图类组成,分别是QTreeeView、QHeaderView、QListView、QColumnView和QTableVie w。为了用户简单方便的使用,还提供了三个模式视图集成的类,分别是QTreeWidget、QListWidget、QTableWidget。对于变动不大,并且简单的需求可以采用这三个类来快速开发。但是对于变动比较大的需求,就不建议使用这三个类,因为它们缺乏灵活性。

四、代理

代理的抽象基类为QAbstractItemDelegate,  如果需要改变item的显示或者item的编辑行为,那么可以考虑自定义代理类。

一般自定义代理类是继承QItemDelegate可以满足大部分的需求,如果直接继承QAbstractItemDelegate,则需要更多的开发工作量。

如果想要改变item的显示,那么可以通过继承QItemDelegate,然后重载paint。

如果想要改变item的编辑行为,同样的可以继承QItemDelegate,然后重载createEditor、setEditorData、setModelData和updateEditorGeometry。

下面的实例将详细介绍代理的这两种应用方式。

五、实例

首先实现模型QAbstractTableModel和表格QtableView的结合显示数据信息的实例。为了代码的清晰度,这里model直接存储了数据。

JWeaponModel模型的定义如下所示,rowCount返回行数,columnCount返回列数,data实现返回item的数据,headerData则是实现返回标题信息。flags和setData函数是为了支持代理而添加的,后面会讲解其作用。这里可以暂时不需要太关注。

class JWeaponModel : public QAbstractTableModel
{
public:
    JWeaponModel(QObject *parent = 0);

    virtual int rowCount(const QModelIndex& parent = QModelIndex()) const;
    virtual int columnCount(const QModelIndex& parent = QModelIndex()) const;
    QVariant data(const QModelIndex &index, int role) const;
    QVariant headerData(int section, Qt::Orientation orentation, int role) const;
    Qt::ItemFlags flags(const QModelIndex &index) const;

    bool setData(const QModelIndex &index, const QVariant &value, int role);

private:
    void InitData();

private:
    QVector<short>          m_army;         // 军种索引
    QVector<short>          m_weapon;       // 武器索引
    QMap<short, QString>    m_MapArmy;      // 军种映射表
    QMap<short, QString>    m_MapWeapon;    // 武器映射表
    QStringList             m_header;
};

JWeaponModel模型的实现如下,columnCount默认写死显示三列。date根据角色Qt::DisplayRole来说显示不同数据条目的数据。headerData返回水平标题信息。

JWeaponModel::JWeaponModel(QObject *parent)
    : QAbstractTableModel(parent)
{
    m_MapArmy[1] = tr("海军");
    m_MapArmy[2] = tr("空军");

    m_MapWeapon[1] = tr("战斗机");
    m_MapWeapon[2] = tr("轰炸机");

    InitData();

}

int JWeaponModel::rowCount(const QModelIndex& parent) const
{
    return m_MapArmy.size();
}

int JWeaponModel::columnCount(const QModelIndex& parent) const
{
    return 3;
}

QVariant JWeaponModel::data(const QModelIndex &index, int role) const
{
    if (!index.isValid())
    {
        return QVariant();
    }

    if (role == Qt::DisplayRole)
    {
        switch (index.column())
        {
            case 0:
            {
                return m_MapArmy[m_army[index.row()]];
            }
            case 1:
            {
                return m_MapWeapon[m_weapon[index.row()]];
            }
            default:
            {
                return QVariant();
            }
        }
    }
    return QVariant();
}

QVariant JWeaponModel::headerData(int section, Qt::Orientation orentation, int role) const
{
    if (role == Qt::DisplayRole && orentation == Qt::Horizontal)
    {
        return m_header[section];
    }

    return QAbstractTableModel::headerData(section, orentation, role);
}

void JWeaponModel::InitData()
{
    m_header << tr("军种")  << tr("种类") << tr("部门") ;
    m_army << 1 << 2;
    m_weapon << 1 << 2;
}

// 需要添加ItemIsEditable属性,否则代理创建的部件显示不出来
Qt::ItemFlags JWeaponModel::flags(const QModelIndex &index) const
{
    return Qt::ItemIsEditable | QAbstractTableModel::flags(index);
}

bool JWeaponModel::setData(const QModelIndex &index, const QVariant &value, int role)
{
    if (role == Qt::EditRole)
    {
            int row = index.row();
            m_MapArmy[row+1] =  value.toString();
            //LOG(INFO) << "row :" << row;
            return true;
    }

    return false;
}

主程序添加如下代码,QTableView设置模式为JWeaponModel。

JWeaponModel *p_weapon_model = new JWeaponModel();
QTableView *p_view = new QTableView();
p_view->setModel(p_weapon_model);
p_view->resize(640, 480);
p_view->show();

编译运行之后的效果如下,显示的数据均来自模型JWeaponModel。

如果想要改变item的编辑行为,支持双击的时候,变成选择框QComboBox,那么考虑使用代码,其定义如下,createEditor创建控件,setEditorData设置控件初始数据,setModelData将编辑数据写入model,  则model需要实现setData(参加JWeaponModel类的实现),这样才能将数据显示到视图。updateEditorGeometry管理控件位置。

#include <QModelIndex>
#include <QVariant>
#include <QItemDelegate>


class JEditorDelegate : public QItemDelegate
{
    Q_OBJECT

public:
    JEditorDelegate(QObject *parent = 0);
    ~JEditorDelegate() override;

    QWidget *createEditor(QWidget *parent,
                          const QStyleOptionViewItem &option,
                          const QModelIndex &index) const override;

    void setEditorData(QWidget *editor, const QModelIndex &index) const override;
    void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const override;

    void updateEditorGeometry(QWidget *editor,
                              const QStyleOptionViewItem &option,
                              const QModelIndex &index) const override;
};

JEditorDelegate对应的实现如下,这里只针对表格第一列进行处理。

JEditorDelegate::JEditorDelegate(QObject *parent)
    : QItemDelegate(parent)
{

}

JEditorDelegate::~JEditorDelegate()
{

}

QWidget *JEditorDelegate::createEditor(QWidget *parent,
                      const QStyleOptionViewItem &option,
                      const QModelIndex &index) const
{
    if (index.column() == 0)
    {
        QComboBox *p_editor = new QComboBox(parent);
        p_editor->addItem(tr("one"));
        p_editor->addItem(tr("two"));
        return p_editor;
    }
    else
    {
        return QItemDelegate::createEditor(parent, option, index);
    }
}

void JEditorDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const
{
    if (index.column() == 0)
    {
        QComboBox *p_combobox = qobject_cast<QComboBox*>(editor);
        if(p_combobox)
        {
            int i = p_combobox->findText("one");
            p_combobox->setCurrentIndex(i);
        }
    }
    else
    {
        return QItemDelegate::setEditorData(editor, index);
    }
}

void JEditorDelegate::setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const
{
    if (index.column() == 0)
    {
        QComboBox *p_combobox = qobject_cast<QComboBox*>(editor);
        if(p_combobox)
        {
            model->setData(index, p_combobox->currentText());
        }
    }
    else
    {
        return QItemDelegate::setModelData(editor, model, index);
    }
}

void JEditorDelegate::updateEditorGeometry(QWidget *editor,
                          const QStyleOptionViewItem &option,
                          const QModelIndex &index) const
{
    (void)index;
    editor->setGeometry(option.rect);
}

主程序添加代理,进行如下所示的修改,调用QTableView的setItemDelegate来添加代理。

JWeaponModel *p_weapon_model = new JWeaponModel();
QTableView *p_view = new QTableView();
p_view->setModel(p_weapon_model);
p_view->setItemDelegate(new JEditorDelegate());
p_view->resize(640, 480);
p_view->show();

编译运行之后,双击第一列第一行的item, 则出现如下的效果

如果想要改变item的显示,那么也是通过代理的方式来支持,其代理类定义如下,继承QItemDelegate,并重载paint。

#include <QModelIndex>
#include <QItemDelegate>

class JArrowDelegate : public QItemDelegate
{
Q_OBJECT

public:
   JArrowDelegate(QObject* parent = 0);
   virtual void paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const;
};

对应的实现如下,paint实现再item中画一个黑色的竖线。

JArrowDelegate::JArrowDelegate(QObject *parent)
   : QItemDelegate(parent)
{
}

void JArrowDelegate::paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const
{
    (void)index;
    //int i_row = index.row();
    int i_x = option.rect.x();
    int i_y = option.rect.y();
    int i_width = option.rect.width();
    int i_height = option.rect.height();
    QPen pen;
    pen.setWidth(2);
    pen.setColor(QColor(Qt::black));
    QStyleOptionViewItem my_option = option;

    QPainterPath path;
    QPoint point_one;
    QPoint point_two;
    point_one.setX(i_x + i_width / 2);
    point_one.setY(i_y);
    point_two.setX(i_x + i_width / 2);
    point_two.setY(i_y + i_height);

    path.moveTo(point_one);
    path.lineTo(point_two);

    painter->setPen(pen);
    painter->drawPath(path);
}

主程序添加代理JArrowDelegate,调用QTableView的接口setItemDelegateForColumn只对表格的第三列添加代理。

JWeaponModel *p_weapon_model = new JWeaponModel();
QTableView *p_view = new QTableView();
p_view->setModel(p_weapon_model);
p_view->setItemDelegate(new JEditorDelegate());
p_view->setItemDelegateForColumn(2, new JArrowDelegate());
p_view->resize(640, 480);
p_view->show();

编译运行效果如下,第三列显示了一条竖线。

六、总结

QT模型视图的原理以及使用说明的讲解就到这里。本文先说明了模型视图的结构,然后再依次说明模型、视图和代理的类层次结构。最后结合具体实例来说明其应用方式,从中可以看出模型提供数据给视图显示,而代理则可以改变数据条目的显示,并通知视图,代理还可以改变数据条目的编辑行为,并通知模型。

json其实不难,只需了解一点,就能轻松玩转它

工作过程中,经常需要使用json这种轻量级的数据交换格式,例如,通过它可以组织数据保存到配置文件,客户端和服务端通过json格式来进行通信等,但是,针对特定的需求场景,需要设计怎样的json格式比较合适呢,json到底可以支持多少种格式呢,有没有一种简单的概括,就能让使用者轻松使用它呢!

一般知识都有基本的理论结构,所以,本文首先将说明json的基本知识点,然后基于开源软件jsoncpp来说明如何构建/读写json,   再分享个人的使用心得,最后再总结json的关键要点,理解了这一点,玩转json不是问题。

一、Json简介

Json是轻量级的数据交换格式,它便于阅读和编写,它是完全独立于程序语言的文本格式。

二、Json结构

Json有两个结构, 分别是“名称/值”对的集合和值的有序列表。“名称/值”对的集合可以简单理解为对象集合,而值的有序列表可以理解为数组。

这里举一个“名称/值”对的集合的例子,它是以左大括号开始,以右大括号结束,中间是由多个“名称/值”对组成,各个“名称/值”对之间用逗号隔开。

{
    "cpu_name" : "special",
    "cpu_temp" : 40
}

举一个“值的有序列表”的例子,它是以左中括号开始,以右中括号结束,中间是由多个值组成,各个值之间用逗号隔开。

["apple", "pear", "banana "]

三、Json形式

Json主要由三种形式,分别为对象(object),  数组(array),  值(value)。

对象(object)是“名称/值”对集合,名称于值之间通过冒号隔开,另外对象是以左大括号开始,以右大括号结束。

数组(array)是值的有序集合,它是以左中括号开始,以右中括号结束。

值(value)可以是字符串(string)、数值(number)、对象(object)、数组(array)、true、false、null。这里我们会发现对象(object)里面有值(value),  数组(array)里面也有值(value),  而值(value)又包含有对象和数组,所以它们是可以嵌套的。

Json就是由上面简单的元素来组建复杂的信息元素。

四、Json例子

jsoncpp是C++语言编写的开源json库,通过该库,我们可以很容易的构建、读写json。接下来就基于jsoncpp来解释几个构建、读取json的例子。通过例子可以对json有更深的理解。jsoncpp最基本的对象就是Json::Value。

构建一个最简单的对象,然后输出整个json信息,最后读取json值,先调用isMember判断名称是否为root成员,如果是的话,那么就读取输出。

Json::Value root;
root["result"] = "true";

Json::StyledWriter styled_writer;
std::string str_json = styled_writer.write(root);
LOG(INFO) << "str_json: " << str_json.c_str();

if (root.isMember("result"))
{
    LOG(INFO) << "root[\"result\"] = " << root["result"];
}

输出的日志信息如下所示,大括号包含了一个“名称/值”对。

2020-05-02 17:59:32,670 INFO  [default] str_json: {
   "result" : "true"
}

2020-05-02 17:59:32,670 INFO  [default] root["result"] = "true"

构建嵌套对象,第一个根“名称/值”对中的“值”又是一个对象。

Json::Value root;
Json::Value value;
value["cpu_name"] = "arm";
root["cpu_info"] = value;

Json::StyledWriter styled_writer;
std::string str_json = styled_writer.write(root);
LOG(INFO) << "str_json: " << str_json.c_str();

if (root["cpu_info"].isMember("cpu_name"))
{
    LOG(INFO) << "root[\"cpu_info\"][\"cpu_name\"] = " << root["cpu_info"]["cpu_name"];
}

输出的日志信息如下所示

2020-05-02 17:59:32,670 INFO  [default] str_json: {
   "cpu_info" : {
      "cpu_name" : "arm"
   }
}

2020-05-02 17:59:32,670 INFO  [default] root["cpu_info"]["cpu_name"] = "arm"

构建三层嵌套对象,第一个根“名称/值”对中的“值”是一个对象,而该对象的“值”又是一个对象。依次类推,可以构建更多层的嵌套对象。

Json::Value root;
Json::Value value_01;
Json::Value value_02;

value_02["cell_number"]  = 255;
value_01["eye"] = value_02;
root["body"] = value_01;

Json::StyledWriter styled_writer;
std::string str_json = styled_writer.write(root);
LOG(INFO) << "str_json: " << str_json.c_str();

if (root["body"]["eye"].isMember("cell_number"))
{
    LOG(INFO) << "root[\"body\"][\"eye\"][\"cell_number\"]  = " << root["body"]["eye"]["cell_number"];
}

输出的日志信息如下所示

2020-05-02 17:59:32,670 INFO  [default] str_json: {
   "body" : {
      "eye" : {
         "cell_number" : 255
      }
   }
}

2020-05-02 17:59:32,670 INFO  [default] root["body"]["eye"]["cell_number"]  = 255

构建简单的数组,jsoncpp中构建数组是通过append的接口来创建的。读取数组之前,先调用isArray来判断对象是否为数组,如果是的话,再读取输出。这里需要注意数组的个数。从防御式编程的角度看,读取数组值之前,需要判断数组索引是否在有效范围内。

Json::Value array;
array.append("one");
array.append("two");

Json::StyledWriter styled_writer;
std::string str_json = styled_writer.write(array);
LOG(INFO) << "str_json: " << str_json.c_str();

if (array.isArray())
{
    LOG(INFO) << "array.size(): " << array.size();
    LOG(INFO) << "array[0]: " << array[0];
    LOG(INFO) << "array[1]: " << array[1];
}

输出的日志信息如下所示,从这里我们也可以确定数组是可以单独作为独立json串出现的。之前一直都有一个误区,就是认为json一定要用大括号包括起来。

2020-05-02 17:59:32,670 INFO  [default] str_json: [ "one", "two" ]

2020-05-02 17:59:32,671 INFO  [default] array.size(): 2
2020-05-02 17:59:32,671 INFO  [default] array[0]: "one"
2020-05-02 17:59:32,671 INFO  [default] array[1]: "two"

构建对象和数组组成的json。首先创建一个数组,然后将其作为对象的值。

Json::Value array;
array.append("one");
array.append("two");
array.append("three");

Json::Value root;
root["number"] = array;

Json::StyledWriter styled_writer;
std::string str_json = styled_writer.write(root);
LOG(INFO) << "str_json: " << str_json.c_str();

if (root["number"].isArray())
{
    LOG(INFO) << "root[\"number\"].size(): " << root["number"].size();
    LOG(INFO) << "root[\"number\"][0]: " << root["number"][0];
    LOG(INFO) << "root[\"number\"][1]: " << root["number"][1];
    LOG(INFO) << "root[\"number\"][2]: " << root["number"][2];
}

输出的日志信息如下所示

2020-05-02 17:59:32,671 INFO  [default] str_json: {
   "number" : [ "one", "two", "three" ]
}

2020-05-02 17:59:32,671 INFO  [default] root["number"].size(): 3
2020-05-02 17:59:32,671 INFO  [default] root["number"][0]: "one"
2020-05-02 17:59:32,671 INFO  [default] root["number"][1]: "two"
2020-05-02 17:59:32,671 INFO  [default] root["number"][2]: "three"

最后再构建稍微复杂一点的json串,它是由对象、数组、对象来组成的,即对象的值是一个数组,而数组内部的值是由对象组成。

Json::Value root;
Json::Value array;
Json::Value value_01;
Json::Value value_02;

value_01["peripheral"] = 1;
value_01["patient"] = 2;

value_02["image"] = 3;
value_02["auto"] = 4;

array.append(value_01);
array.append(value_02);

root["department"] = array;

Json::StyledWriter styled_writer;
std::string str_json = styled_writer.write(root);
LOG(INFO) << "str_json: " << str_json.c_str();

if (root["department"].isArray())
{
    LOG(INFO) << "root[\"department\"].size(): " << root["department"].size();
    LOG(INFO) << "root[\"department\"][0][\"patient\"]: " << root["department"][0]["patient"];
    LOG(INFO) << "root[\"department\"][1][\"auto\"]: " << root["department"][1]["auto"];
}

输出的日志信息如下所示

2020-05-02 17:59:32,671 INFO  [default] str_json: {
   "department" : [
      {
         "patient" : 2,
         "peripheral" : 1
      },
      {
         "auto" : 4,
         "image" : 3
      }
   ]
}

2020-05-02 17:59:32,671 INFO  [default] root["department"].size(): 2
2020-05-02 17:59:32,671 INFO  [default] root["department"][0]["patient"]: 2
2020-05-02 17:59:32,671 INFO  [default] root["department"][1]["auto"]: 4

五、使用心得

  1. 读取json值之前,先判断其有效性,可以结合断言机制,调用isMember或者isArray来进行判断。
  2. 使用数组的时候,需要特别注意数组下标。

六、总结

json主要是由对象或数组创建而成,而它们的嵌套使用就可以创建复杂的json串,根据特定场景的需求来创建适用的json格式。

是不是觉得Makefile很繁琐,一个cmake文件就可以解决

linux编译程序的时候,通常是使用Makefile文件来进行编译,这个是能够提高效率的,但是对于大中型的项目,每个文件夹下都需要创建Makefile,并且改变项目目录结构都需要调整Makefile文件,如果是小型项目的话,那花费的时间还是能够接受的,但是大中型项目要调整目录结构,这个工作量还是不小的。所以,我们可以通过使用cmake来解决这样的问题。

本文首先简单介绍什么是cmake,它可以用来干什么,接着给出一个简单的例子,让初学者对cmake有一个大致的了解,然后抛出cmake文件,并针对该cmake文件进行详细的解释,最后再进行总结,并分享个人的学习心得。

一、什么是cmake

cmake一款跨平台的编译工具,  它的全称是cross platform make,注意这里的make不是指linux下的make,使用它构建的工程,既可以生成linux下的makefile,也可以生成Mac系统的xcode的工程文件,还能够生成window的projects等。cmake并不会生成最终的软件程序,它只是生成标准的建构档,例如linux的Makefile文件。简单来说,cmake可以生产不同平台的建构档,然后再由建构档来生成最终的软件程序。

cmake组织档的取名都为CMakeLists.txt, 现在许多开源软件都采用cmake来组织代码,可见其用处还是很大的,学习了解cmake对于学习开源软件是有很大的帮助的。

二、入门例子

首先电脑上需要安装cmake软件,具体下载安装方法,可以网络搜索,这不是本文的主题,所以不进行说明。

linux上安装成功之后,可以执行命令cmake –version来查看当前的cmake版本

创建一个main.cpp文件,其内容如下所示,打印一句字符信息。

#include <stdio.h>

int main(int argc, char **argv)
{
    printf("this is the first cmake project.\n");
    return 0;
}

接着在同级目录下创建cmake文件 CMakeLists.txt

# CMake 最低版本号要求
cmake_minimum_required (VERSION 2.8)

# 项目信息,项目名称
project (EXAMPLE_01)

# 指定生成目标
add_executable(example_01 main.cpp)

为了代码干净,同级目录下创建build目录,进入build目录,执行“cmake ../”命令来生成Makefile文件,接着执行命令make编译,最后build目录下生成二进制文件example_01,  执行程序可以输出打印信息。

build目录下生成的文件内容如下,Makefile是生成的建构档,example_01是生成的可执行二进制程序。

三、cmake代替Makefile

上面只是cmake的一个简单的入门例子,还不能明显看出cmake的强大,对于中大型的项目来看,cmake的作用就比较明显,特别是相同代码不同平台的编译。

下面给出本章节将要详细解释说明的cmake文件,可以先熟悉下整体的流程,看不明白没有关系,后面将针对该文件进行详细的解释说明。

# CMake 最低版本号要求
cmake_minimum_required (VERSION 2.8.7 FATAL_ERROR)

# 项目信息,项目名称
project (EXAMPLE_02)

# CMAKE_INCLUDE_CURRENT_DIR 自动增加CMAKE_CURRENT_SOURCE_DIR和CMAKE_CURRENT_BINARY_DIR到每个目录的include路径
set(CMAKE_INCLUDE_CURRENT_DIR ON)
message("CMAKE_CURRENT_SOURCE_DIR : ${CMAKE_CURRENT_SOURCE_DIR}")
message("CMAKE_CURRENT_BINARY_DIR : ${CMAKE_CURRENT_BINARY_DIR}")
message("PROJECT_SOURCE_DIR : ${PROJECT_SOURCE_DIR}")

set(CROSS_TOOLCHAIN_PREFIX "")
set(CMAKE_C_COMPILER ${CROSS_TOOLCHAIN_PREFIX}gcc)
set(CMAKE_CXX_COMPILER ${CROSS_TOOLCHAIN_PREFIX}g++)
set(CROSS_OBJCOPY ${CROSS_TOOLCHAIN_PREFIX}objcopy)
set(CROSS_STRIP ${CROSS_TOOLCHAIN_PREFIX}strip)

set(EXECUTABLE_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/bin/linux)

# find_package()命令是用来查找依赖包
find_package(PkgConfig)

# export cmd 
message(STATUS “HOME dir: $ENV{HOME}”)
message(STATUS “LANG: $ENV{LANG}”)

# find_program查找可执行程序
find_program(CCACHE_FOUND ccache)
if(CCACHE_FOUND)
    message("found ccache")
    set_property(GLOBAL PROPERTH RULE_LAUNCH_COMPILE ccache)
    set_property(GLOBAL PROPERTH RULE_LAUNCH_LINK ccache)	
else()
    message("no found ccache")	
endif(CCACHE_FOUND)

if($ENV{DEBUG} MATCHES 1)
    message("debug build")
    set(CMAKE_BUILD_TYPE Debug)
else()
    message("release build")
    set(CMAKE_BUILD_TYPE RelWithDebInfo)
endif()

# 头文件的搜索路径
function(include_sub_directories_recursively root_dir)
    if (IS_DIRECTORY ${root_dir})
        include_directories(${root_dir})
    endif()

    file (GLOB ALL_SUB RELATIVE ${root_dir} ${root_dir}/*)
    foreach(sub ${ALL_SUB})
        if (IS_DIRECTORY ${root_dir}/${sub} AND (NOT ("${sub}" STREQUAL ".svn")))
            include_sub_directories_recursively(${root_dir}/${sub}) 
        endif()
    endforeach()	
endfunction()	

include_directories(
    ${CMAKE_CURRENT_BINARY_DIR}
    ${PROJECT_SOURCE_DIR}
)

set(CMAKE_CXX_FLAGS_MY "-pipe -march=armv7-a -mfpu=neon -DLINUX=1 -DEGL_API_FB=1 -mfloat-abi=hard -O2 -std=c++11 -Wall -W -D_REENTRANT -fPIC -Wformat -Werror")
set(CMAKE_C_FLAGS "-pipe -march=armv7-a -mfpu=neon -DLINUX=1 -DEGL_API_FB=1 -mfloat-abi=hard -O2 -Wall -W -D_REENTRANT -fPIE -Werror")
set(CMAKE_CXX_FLAGS "")

# 指定查找库的目录
function(link_sub_directories_recursively root_dir)
    if (IS_DIRECTORY ${root_dir})
        link_directories(${root_dir})
    endif()

    file (GLOB ALL_SUB RELATIVE ${root_dir} ${root_dir}/*)
    foreach(sub ${ALL_SUB})
        if (IS_DIRECTORY ${root_dir}/${sub} AND (NOT ("${sub}" STREQUAL ".svn")))
            link_sub_directories_recursively(${root_dir}/${sub}) 
        endif()
    endforeach()	
endfunction()

# 遍历匹配目录的所有子目录并匹配文件
file(GLOB_RECURSE SRC_FILES ${PROJECT_SOURCE_DIR}/*.cpp)

# 指定生成目标
add_executable(example_02 ${SRC_FILES})

# 标示链接的库
target_link_libraries(example_02
    dl
    pthread
    m)

if (CMAKE_BUILD_TYPE MATCHES Rel)
    add_custom_command(TARGET example_02
        POST_BUILD
        COMMAND mkdir -p ${EXECUTABLE_OUTPUT_PATH}/release	
        )
else()
    add_custom_command(TARGET example_02
        POST_BUILD
        COMMAND mkdir -p ${EXECUTABLE_OUTPUT_PATH}/debug	
        )
endif()

cmake_minimum_required指定cmake最低版本号要求,FATAL_ERROR 表示当发生警告时,用错误方式提示

make_minimum_required(VERSION <min>[...<max>] [FATAL_ERROR])

CMAKE_INCLUDE_CURRENT_DIR 自动增加CMAKE_CURRENT_SOURCE_DIR和CMAKE_CURRENT_BINARY_DIR到每个目录的include路径。CMAKE_INCLUDE_CURRENT_DIR默认是关闭的。

当前测试工程的目录结构如下:

02_exapmle
    |-- build
    |-- src
        |-- CMakeLists.txt
        |-- main.cpp

message可以打印输出变量信息, CMAKE_CURRENT_SOURCE_DIR、CMAKE_CURRENT_BINARY_DIR、PROJECT_SOURCE_DIR是cmake内置变量

message("CMAKE_CURRENT_SOURCE_DIR : ${CMAKE_CURRENT_SOURCE_DIR}")
message("CMAKE_CURRENT_BINARY_DIR : ${CMAKE_CURRENT_BINARY_DIR}")
message("PROJECT_SOURCE_DIR : ${PROJECT_SOURCE_DIR}")

运行后输出打印的信息, CMAKE_CURRENT_SOURCE_DIR表示当前正在处理的源代码目录,CMAKE_CURRENT_BINARY_DIR表示当前正在处理的二进制目录,PROJECT_SOURCE_DIR表示当前工程的顶层源代码目录

CMAKE_CURRENT_SOURCE_DIR : .../02_example/src
CMAKE_CURRENT_BINARY_DIR : .../02_example/build
PROJECT_SOURCE_DIR : .../02_example/src

EXECUTABLE_OUTPUT_PATH表示可执行文件输出路径

find_package()命令是用来查找依赖包, Pkg-Config维护它依赖库路径、头文件路径、编译选项、链接选项等信息。

关键字ENV查看的是当前环境变量,linux上的环境变量可以通过export命令来查看。

message(STATUS “LANG: $ENV{LANG}”)对应输出的内容为:

-- “LANG:zh_CN.UTF-8”

find_program查找可执行程序。一个名为<VAR>的cache条目会被创建用来存储该命令的结果。如果该程序被找到了,结果会存储在该变量CCACHE_FOUND

find_program(<VAR> name [path1 path2 ...])

set_property给定范围内设置一个命名属性,GLOBAL域是唯一的,PROPERTY后面紧跟着要设置的属性的名字。

set_property(<GLOBAL                      |
              DIRECTORY [<dir>]           |
              TARGET    [<target1> ...]   |
              SOURCE    [<src1> ...]      |
              INSTALL   [<file1> ...]     |
              TEST      [<test1> ...]     |
              CACHE     [<entry1> ...]    >
             [APPEND] [APPEND_STRING]
             PROPERTY <name> [value1 ...])

function,定义函数name, 并且参数为<arg1> … , 函数只有在调用的时候才起作用。

function(<name> [<arg1> ...])
 <commands>
endfunction()

include_directories包含头文件的搜索路径。

link_directories指定查找库的目录。

target_link_libraries标示链接的库。<target>必须时 add_executable() or add_library() 命令创建。<item>则是链接的库

target_link_libraries(<target> ... <item>... ...)

file产生一个匹配 <globbing-expressions> 的文件列表并将它存储到变量 <variable> 中,果 RELATIVE 标志位被设定,将返回指定路径的相对路径。file的第一个参数设置为GLOB_RECURSE,则表示遍历匹配目录的所有子目录并匹配文件。

file(GLOB <variable>
[LIST_DIRECTORIES true|false] [RELATIVE <path>] [CONFIGURE_DEPENDS]
[<globbing-expressions>...])

add_custom_command,定义一个跟指定目标target关联的新的命令,命令何时执行取决于PRE_BUILD | PRE_LINK | POST_BUILD这三个参数。

add_custom_command(TARGET target
                   PRE_BUILD | PRE_LINK | POST_BUILD
                   COMMAND command1 [ARGS] [args1...]
                   [COMMAND command2 [ARGS] [args2...] ...]
                   [WORKING_DIRECTORY dir]
                   [COMMENT comment] [VERBATIM])
PRE_BUILD - 所有其他依赖项之前运行
PRE_LINK - 其他依赖项之后运行
POST_BUILD - 目标建立之后运行

四、总结

创建cmake文件的过程,首先当然是先创建CMakeLists.txt文件,接着声明cmake的版本要求,然后设置项目信息,再根据具体场景设置相关属性以及生成的可执行目标。

上面讲解的例子中,主要涉及到cmake的几个知识点。具体如下:

  1. cmake的变量:内置变量、环境变量以及自定义变量。
  2. 查找命令:find_package、find_program
  3. 定义函数:function
  4. 查找文件:file
  5. 搜索路径:include_directories、link_directories、target_link_libraries
  6. 自定义命令:add_custom_command
  7. 设置目标: add_executable

五、学习心得

学习cmake也有一段时间,网络上也搜索了很多信息,但是总感觉说的不够明白和全面。最后发现要想全面的了解cmake,  最有效的方法就是直接查看cmake的官方文档。虽然是英文,但是只要耐心认真阅读,就会发现里面讲的很全面。然后再结合具体的例子进行消化理解就可以。而对于cmake的变量的含义,除了查看cmake官方文档之后,还可以通过message直接打印出变量信息来加深理解其含义。

基于面向对象的思想来使用结构体,将会有意想不到的效果

程序开发过程中,很多人都会接触到客户服务端模型,通常客户服务端模型是基于socket的网络通信,而网络通信是需要定义通信协议,通信协议结构一般是用结构体的方式来表示,而数据内容有的可能会使用json格式,对于嵌入式设备,数据内容更多的还是采用结构体的方式来表示。

本文首先会基于Qt提供的socket接口来实现一个简单的客户服务端模型,主要是为后面数据内容采用结构体通信的说明提供基础。接着定义通信协议结构体,然后再说明C语言方式使用结构体的方法,再介绍基于面向对象的思想来使用结构体,从而体会两者方式之间的区别,最后再介绍如何采用模版方式来更好的获取结构格式不定的数据内容。

一、客户服务端模式

客户服务端模式的机制是,服务端启动监听端口来等待客户端的连接,客户端创建socket启动连接,服务端成功接收到连接之后,等待客户端发送数据,客户端开始发送数据,服务端接收到数据,并进行解析处理。

下面会基于QT提供的socket接口来实现简单的客户服务端模型,实现之前需要在pro文件中添加network库的支持。

QT       += core gui network

1、定义实现简单的服务端类JTcpServer, 首先构造函数创建QTcpServer对象用来启动监听等待新的连接,当有新的连接请求的时候,则通过QTcpServer提供的接口nextPendingConnection来返回连接成功的socket, 然后等待客户端发送数据,如果有可读数据,那么读取数据进行处理。

// 定义服务端类
class JTcpServer : public QObject
{
    Q_OBJECT
public:
    JTcpServer();
    ~JTcpServer();

    void Start();

public slots:
    void AcceptConnection();
    void ReadClient();

private:
    QTcpServer* m_pTcpServer;
    QTcpSocket *m_pClientConnection;
};

// 实现服务端类
JTcpServer::JTcpServer()
    : QObject(nullptr)
{
    LOG(INFO) << " contructor";
    m_pTcpServer = new QTcpServer(this);
}

JTcpServer::~JTcpServer()
{
    LOG(INFO) << " decontructor";
    m_pTcpServer->close();
}

void JTcpServer::AcceptConnection()
{
    LOG(INFO) << "receive new connection";
    m_pClientConnection = m_pTcpServer->nextPendingConnection();
    if (m_pClientConnection->waitForReadyRead())
    {
        ReadClient();
    }
}

void JTcpServer::ReadClient()
{
    QString str = m_pClientConnection->readAll();
    LOG(INFO) << "str: " << str.toStdString().c_str();
}

void JTcpServer::Start()
{
    LOG(INFO) << "start tcp server";

    m_pTcpServer->listen(QHostAddress::Any, 9999);
    if (m_pTcpServer->waitForNewConnection(500000))
    {
        AcceptConnection();
    }

    LOG(INFO) << "end tcp server";
}

2、定义实现简单的客户端类,构造函数创建QTcpSocket用来连接服务端,并且发送数据。

// 定义客户端类
class JTcpClient : public QObject
{
    Q_OBJECT
public:
    JTcpClient();
    ~JTcpClient();

    void Start();

private:
    QTcpSocket* m_pclientSocket;
};

// 实现服务端类
JTcpClient::JTcpClient()
    : QObject(nullptr)
{
    LOG(INFO) << " contructor";
    m_pclientSocket = new QTcpSocket(this);
}

JTcpClient::~JTcpClient()
{
    LOG(INFO) << " decontructor";
    m_pclientSocket->close();
}

void JTcpClient::Start()
{
    LOG(INFO) << "start tcp client";

    m_pclientSocket->connectToHost(QHostAddress("127.0.0.1"), 9999);
    char ac_data[512] = {0};
    std::memcpy(ac_data, "hello everyone!", sizeof("hello everyone!"));
    m_pclientSocket->write(ac_data);
    m_pclientSocket->waitForBytesWritten();

    LOG(INFO) << "end tcp client";
}

3、完成客户端和服务端的实现,启动两个分离线程来分别执行客户端和服务端代码

// 启动服务端
std::thread thread_server( [&]{
    JTcpServer *p_tcp_server = new JTcpServer();
    p_tcp_server->Start();
} ) ;

std::this_thread::sleep_for(std::chrono::seconds(1));

// 启动客户端
std::thread thread_client( [&]{
    JTcpClient *p_tcp_client = new JTcpClient();
    p_tcp_client->Start();
} ) ;

if (thread_server.joinable())
{
    thread_server.detach();
}

if (thread_client.joinable())
{
    thread_client.detach();
}

4、启动运行之后,可以看到服务端成功打印了客户端发送的数据,这说明客户端和服务端之间是能够通信的。

[void JTcpServer::ReadClient():60] str: hello everyone!

二、通信协议

定义客户端和服务端的通信协议,它包括帧号,该帧号具有唯一性;帧类型,根据具体业务场景进行定义,比如命令帧、结果帧等;帧的来源表示帧的发送者; 帧的目的表示帧的接受者;数据帧长度则存储数据内容的具体长度; 数据则存放不定长度的数据内容。

三、基于C语言方式的结构体

基于C语言方式定义通信协议的结构体

typedef struct CFrame
{
    int iId;
    int iType;
    int iFrom;
    int iTo;
    int iDataLen;
    char data[0];
}CFRAME;

再定义数据内容的结构体

typedef struct CParam
{
    int iParam;
    char acInfo[32];
}CPARAM;

客户端构建数据,并发送。首先malloc申请内存,然后填充帧头和数据内容,最后发送数据,再free释放内存。

CFRAME *p_frame = nullptr;
p_frame = (CFRAME *)malloc(sizeof(CFRAME) + sizeof(CPARAM));
memset(p_frame, 0x00, sizeof(CFRAME) + sizeof(CPARAM));
p_frame->iId = 111;
p_frame->iType = 3;
p_frame->iFrom = 1;
p_frame->iTo = 2;
p_frame->iDataLen = sizeof(CPARAM);
CPARAM param;
param.iParam = 400;
memcpy(param.acInfo, "happy.", sizeof("happy."));
memcpy(p_frame->data,  &param, sizeof(CPARAM));

int i_write_len = m_pclientSocket->write((char *)p_frame, sizeof(CFRAME) + sizeof(CPARAM));
LOG(INFO) << "i_write_len: " << i_write_len;
m_pclientSocket->waitForBytesWritten();

free(p_frame);

服务端接收数据,并解析。服务端接收全部数据,然后解析并打印出来。由于TCP是流的方式,一般来说,先解析帧头,再解析帧数据,但是这里暂时不需要关注,所以没有考虑。

QByteArray array = m_pClientConnection->readAll();
LOG(INFO) << "array.size(): " << array.size();

CFRAME *p_frame = reinterpret_cast<CFRAME *>(array.data());
if (!p_frame)
{
    LOG(INFO) << "p_frame nullptr";
}

LOG(INFO) << "p_frame->iId: " << p_frame->iId;
LOG(INFO) << "p_frame->iType: " << p_frame->iType;
LOG(INFO) << "p_frame->iFrom: " << p_frame->iFrom;
LOG(INFO) << "p_frame->iTo: " << p_frame->iTo;
LOG(INFO) << "p_frame->iDataLen: " << p_frame->iDataLen;

CPARAM *p_param = reinterpret_cast<CPARAM *>(p_frame->data);
if (p_param)
{
    LOG(INFO) << "p_param->iParam: " << p_param->iParam;
    LOG(INFO) << "p_param->acInfo: " << p_param->acInfo;
}

四、基于面向对象的结构体

上面的方式是基于基于C语言的方式来使用结构体,接下来就来说明如何基于面向对象的方式来使用结构体。

基于面向对象方式定义通信协议的结构体

struct CXXFrame
{
    int iId;
    int iType;
    int iFrom;
    int iTo;
    int iDataLen;
    char data[0];
    CXXFrame()
        : iId(1)
        , iType(0)
        , iFrom(1)
        , iTo(2)
        , iDataLen(0)
    {}
};

基于面向对象方式定义数据内容结构体

struct JParam
{
    int iParam;
    char acInfo[32];
    JParam()
        : iParam(1)
    {
        memset(acInfo, 0x00, sizeof(acInfo));
    }
};

构建客户端数据,并且发送数据。为了避免申请内存而忘记释放,这里使用std::vector来定义数组来存储发送的数据,这样就避免忘记释放内存。

CXXFrame *p_frame = nullptr;
int i_frame_len = sizeof(CXXFrame) + sizeof(JParam);
std::vector<char> vec_frame(i_frame_len, 0);
p_frame =  reinterpret_cast<CXXFrame *>(vec_frame.data());
p_frame->iId = 111;
p_frame->iType = 3;
p_frame->iFrom = 1;
p_frame->iTo = 2;
p_frame->iDataLen = sizeof(JParam);
JParam *p_param = reinterpret_cast<JParam *>(p_frame->data);
p_param->iParam = 400;
memcpy(p_param->acInfo, "happy!", sizeof("happy!"));
m_pclientSocket->write(vec_frame.data(), vec_frame.size());
m_pclientSocket->waitForBytesWritten();

服务端接收数据,并且解析数据。

QByteArray array = m_pClientConnection->readAll();
LOG(INFO) << "array.size(): " << array.size();

CXXFrame *p_frame = reinterpret_cast<CXXFrame *>(array.data());
if (!p_frame)
{
    LOG(INFO) << "p_frame nullptr";
}

LOG(INFO) << "p_frame->iId: " << p_frame->iId;
LOG(INFO) << "p_frame->iType: " << p_frame->iType;
LOG(INFO) << "p_frame->iFrom: " << p_frame->iFrom;
LOG(INFO) << "p_frame->iTo: " << p_frame->iTo;
LOG(INFO) << "p_frame->iDataLen: " << p_frame->iDataLen;

JParam *p_param = reinterpret_cast<JParam *>(p_frame->data);
if (p_param)
{
    LOG(INFO) << "p_param->iParam: " << p_param->iParam;
    LOG(INFO) << "p_param->acInfo: " << p_param->acInfo;
}

五、模版方式获取数据内容

服务器接收数据的内容的长度是不确定,不同的应用场景对应不同的结构体。为了统一、并且简化代码量。可以实现模版方法来获取数据内容。

template<typename T>
T* GetFrameParam(CXXFrame *pFrame)
{
    return reinterpret_cast<T*>(pFrame->data);
}

那么服务端接收数据内容,就可以调用模版方法。

QByteArray array = m_pClientConnection->readAll();
LOG(INFO) << "array.size(): " << array.size();

CXXFrame *p_frame = reinterpret_cast<CXXFrame *>(array.data());
if (!p_frame)
{
    LOG(INFO) << "p_frame nullptr";
}

LOG(INFO) << "p_frame->iId: " << p_frame->iId;
LOG(INFO) << "p_frame->iType: " << p_frame->iType;
LOG(INFO) << "p_frame->iFrom: " << p_frame->iFrom;
LOG(INFO) << "p_frame->iTo: " << p_frame->iTo;
LOG(INFO) << "p_frame->iDataLen: " << p_frame->iDataLen;

//JParam *p_param = reinterpret_cast<JParam *>(p_frame->data);
JParam *p_param = GetFrameParam<JParam>(p_frame);
if (p_param)
{
    LOG(INFO) << "p_param->iParam: " << p_param->iParam;
    LOG(INFO) << "p_param->acInfo: " << p_param->acInfo;
}

六、总结

本文通过实现一个简单的客户服务端模型来说明C语言定义的结构体和面向对象定义的结构体的区别,通过面向对象定义的结构体可以在构造函数中初始化数据成员,这样就不必每次定义对象,还需要使用memset来初始化结构体,同时也避免了忘记初始化结构体的可能性。另外一个,我觉得可以很好简化代码量,并且让代码统一的一个点就是,使用模版方法来解析帧的数据内容。除了解析帧的数据内容之外,还可以通过模版方法来构建帧的数据内容,这样不同结构体,只需要一个模版方法就可以解决。所以,使用面向对象的设计语言的时候,尽量用面向对象的设计思想来开发,很多时候可以简化代码,甚至简化代码的逻辑。