LBD_VideoMeeting_Meeting.exe的开发日志,第一个完整项目,会掰开了揉碎了细聊很多我学习中的技术细节

2022/05/17 Qt 共 9314 字,约 27 分钟

关于整个项目我做一下总结

到现在为止我还没拿到Ui和相关的DLL,陶工现在挺忙的,之前跟他说了一下这个DLL需要加的一些接口,但是吧呃呃,现在25号了,按理说我这个月底就应该完成了,但是吧,就,嗯。

趁现在项目空窗期,我把整个项目揉碎了聊聊。

当然我的理解也不一定对,这是我第一次开发软件,这个文档大概率也不会修改了,但是如果作为第一次开发的开发经验启蒙而言,我觉得还是有很多东西可以值得说道说道的。

1.窗口底稿:Frameless

一个关于如何将widget变成一个像商用软件那样 不可拖拽大小、没有开启、关闭等按钮的固定窗口

这是一个用作所有软件的基础底稿,修改之后的窗口应该是无边框窗口,不能拖拽大小,但是可以拖拽位置。其中拖拽位置是通过鼠标事件实现的,如果单纯设定了frameless的话,则是无法实现这些功能的。

2.引入LBD_VideoMeeting.lib

这个DLL主要是用于在服务器范围内进行视频通信工作,通过向其中输入窗口句柄实现。

什么是句柄?句柄其实就是一个数字,它代表了各种事物在当前系统内的一个唯一的编号,就比如我一个程序,它每一个窗口,每一个变量,每一个内存地址,都会有一个句柄。而一个窗口句柄就指的是这个窗口(可以是一个控件,也可以是一个窗口,或者哪怕只是一个小图标,一个标签,都有自己的窗体句柄)。我们获得一个句柄的方式也很简单,如果我们是要获得一个Ui中的句柄,那么则可以直接调用这个控件指针的winId()方法

比如

ui.label->winId() 

当然这个时候的的句柄还是十六进制的形式,有的时候我们会需要掉换成十进制的,则我们在调用的时候加个int强制转换就行,数据不会出错的 (int)ui.label->winId()

引入这个lib

项目->属性->链接器->输入->附加依赖项->添加$(SolutionDir)$(Configuration)\LBD_VideoMeeting.lib;

注意这个lib文件是陶工给我的release版本,而不是debug源码,就是讲你这个不能用debug方式调用,不让就会报无法解析的外部符号

Qt 错误: LNK2019: 无法解析的外部符号 原因及解决办法

这个博客还有一些别的相关的 LNK2019 无法解析的外部符号 的可能性,不过其实我现在遇到的就这两种情况:

1.声明了函数,却没有写实现

2.调用了release版本的dll,但是自己编译的时候却用的是debug模式,这就会导致这个问题

3.Qt进程间通信的一些参考草稿与尝试

我们的进程该如何接收参数?

这个分两种情况

### 一是开启的时候,直接通过启动项来进行传参

就类似CSGO那种启动项,我们输入一些参数,这些参数会传给Main函数

 int main(int argc, char *argv[])

其中argc代表了变量的个数,*argv 则装载了传入的参数

二是直接通过Windows消息进行进程间通话

QT 中使用 WINDOWS API—-SENDMESSAGE() 进行窗体间消息传递

关于WinApi SendMessage() 中四个参数的详解:

Windows API 之SendMessage[user32]

终于也算是有时间正式来研究一下这个东西怎么用了。首先我们要知道这几个参数是什么对什么。

发送端:

代码在下面,我们在这里将SendMessage揉碎了讲讲

    //进行消息的发送
	//LRESULT SendMessage(HWND hWnd,UINT Msg,WPARAM wParam,LPARAM IParam);
	//hwnd :接收消息的窗体句柄
	//Msg:WM_COPYDATA;
	//wParam:为发送进程的窗体句柄
	//IParam:为指向COPYDATASTRUCT数据结构的指针;
	::SendMessage(hwnd, WM_COPYDATA, reinterpret_cast<WPARAM>(sender), reinterpret_cast<LPARAM>(&copydata));

其中这个WM_COPYDATA指的是一个特定的发送方式,也就是发送COPYDATASTRUCT,我们需要在前面声明一个结构体COPYDATASTRUCT copydata;我们输入这个copydata的数据就是我们通过这个IParama发送出去的消息。我们需要发送的消息都存储在这个copydata中。

这个Msg还分为很多类型,比如WM_WINDOWPOSCHANGED,WM_LBUTTONUP等,但这里作信息传输我们只要用WM_COPYDATA就行了

这个copydata包含了一些数据:

    copydata.dwData = CUSTOM_TYPE;  // 用户定义数据
    copydata.lpData = data.data();  //数据大小
    copydata.cbData = data.size();  // 指向数据的指针

这个CUSTOM_TYPE指的就是我们在传输的 时候给它的分类定义,我们常以WM_USER做分界线,因为WM_USER以下的消息码都是系统要用的,我们不要去截取了。

比如我们可以声明两个类如下:

    //由教师端发送的向主框架发送邀请申请
    const ULONG_PTR TEA_TYPE = WM_USER + 5935;
    //由主框架发送的学生掉线要求
    const ULONG_PTR  LEAVEQUEST = WM_USER + 5936;

在我们发送消息的时候就可以修改这个copydata.dwData=TEA_TYPE或者copydata.dwData=LEAVEQUEST,这样我们就可以根据这个dwData来区分数据的类型了。

接收端:

Qt有个自带的原生消息接收器,bool receiveTest::nativeEvent(const QByteArray & eventType, void * message, long * result)

只要重写就行了,也不用管它有些参数是干嘛的,另外只要使用msg就行,另外两个参数另说

使用参数前现需要将接收到的message转换成一个特定的结构体MSG

MSG *msg = static_cast<MSG*>(message);  //类型转换

这样就可以直接读取msg里面的消息码用作判断了,但是不能直接用,判断完毕后我们还要将其强制转换成COPYDATASTRUCT类型

    //判断当前是否是WM_COPYDATA类型
     if (msg->message == WM_COPYDATA)
         {
            //强制转换
          COPYDATASTRUCT *cds = reinterpret_cast<COPYDATASTRUCT*>(msg->lParam);
            //检查这个接收到的数据的标签,用于判断操作的类型
          if (cds->dwData == TEA_TYPE)
          {
            //do something...
           return true;
          }

源码如下:

接收windows事件消息nativeEvent:

bool LBD_VideoMeeting_Meeting::nativeEvent(const QByteArray & eventType, void * message, long * result)
{

 Q_UNUSED(eventType);
 MSG *msg = static_cast<MSG*>(message);  //类型转换
           /*此处的结构也可用switch来代替*/
 if (msg->message == WM_COPYDATA)
 {

  COPYDATASTRUCT *cds = reinterpret_cast<COPYDATASTRUCT*>(msg->lParam);
  if (cds->dwData == CUSTOM_TYPE)
  {
   QString strMessage = QString::fromUtf8(reinterpret_cast<char*>(cds->lpData), cds->cbData);

   QMessageBox::information(this, QStringLiteral("提示"), strMessage);

   *result = 1;
   return true;
  }
  return false;  //返回值为false表示该事件还会继续向上传递,被其他捕获
 }
 return false;
}

发送windows事件消息:onSendMessage

void LBD_VideoMeeting_Meeting::onSendMessage(QString strMessage,QString Recivepath) {
   HWND hwnd = NULL;
   LPWSTR path = (LPWSTR)Recivepath.utf16(); //定义一个接收消息的窗口,这里我们应该是发送消息给主框架,这里具体还没定
   hwnd = ::FindWindowW(NULL, path);//找到接收消息窗体的窗口句柄


   if (::IsWindow(hwnd)) {

    QByteArray data = strMessage.toUtf8();
    COPYDATASTRUCT copydata;
    copydata.dwData = CUSTOM_TYPE; //The type of the data to be passed to the receiving application. The receiving application defines the valid types.
    copydata.lpData = data.data();// The data to be passed to the receiving application. This member can be NULL.
    copydata.cbData = data.size(); //The size, in bytes, of the data pointed to by the lpData member.
    //获得当前WinId
    HWND sender = (HWND)effectiveWinId();
    //进行消息的发送
    ::SendMessage(hwnd, WM_COPYDATA, reinterpret_cast<WPARAM>(sender), reinterpret_cast<LPARAM>(&copydata));
 }


}

4.关于左上角那个计时器的写法

就是在ue中,有一个计时器,用于记录当前进行了多久的对讲活动,参考文章如下:

QT实现简单计时器

这里我不知道我为什么这个qlabel一直没有反应,就是将updatelabel槽函数和timeout方法链接后,label怎么都不修改。我一开始以为是我自己这个label方法写的不对,最后才发现原来其实是我的整个窗体进程死掉了,我也不知道为什么,但是这次其实也算是给我了一个教训,出现bug的时候要多检查一下当前进程是否正常,否则不一定能得到想要的结果。

5.关于QString 转 char*类型的方案

因为dll结构给的是一个char* 的接口,但是因为要组合端口、姓名等用QString比较方便,所以从一开始的输入就是用的QString,对此需要写一个方法,从QString类型转换到char*类型,方法如下:

char* CodeTestQt::QString2Char(QString test){
//先将QString转成std:string类型
  std::string str = test.toStdString();
  //展示输入QString内容
  qDebug() << "QString:" << test;
  //将str转换成const char*内容
  const char* ch = str.c_str();
  qDebug() << "const char*" << ch;
  //记录下本块const char* ch 的长度,为后续初始化char* ch2做准备
  	int nLen = strlen(ch);
   
   //记住ch2需要先初始化大小,开辟内存才可以正常使用,因为c++是底层语言,如果不开辟内存,则指针可能无法使用。
  	char* ch2 = new char[nLen+2];
   //将ch内容复制到ch2中去
  strcpy(ch2, ch);
  return ch2;
}


注意一点就是,既然我们写了这么个方法,让QString转char*,那么其实大部分情况下,都不必要传char*参数,因为 char*本身是一点都不好用的。除此之外要善用Qt自带的一些方法和类型,比如QList,相比起直接用结构体指针,QList明显会理想的多。

6.关于用户的进入和离开,如何用用户池来表示

我对应用的初步设想是两个数组来表示当前应用人数和窗口的状态。

就是用一个 激活状态bool数组 和一个 排序int数组 来进行排序

舍弃这种方案,讲个简单的,一个用户池就可以解决了

也就是两个链表,链表中包括 用户的信息、窗口句柄、激活状态、下一个节点。一个链表用来装激活了的窗体,另一个链表用来装未激活的窗体,两个链表的长度加起来应该正好等于8,注意这个链表的重点是窗口句柄,也就是说窗口句柄是不变的,而用户的信息是可替换的。另外值得注意的一点就是,实现排序的方法是从链表头走到尾,然后以此为排序的根据。

关于链表,最开始我的想法是自己写一个双向链表,这样就可以完成 从末尾插,但是从头开始读的想法,但是如果从底层开始实现起来的话比较复杂,而且我自己手捏的轮子我不敢保证有效,所以要借助c++和Qt自带的库。一开始想用的是C++自带的STL,不过既然qt有一个专门的链表QList,那么何乐而不为呢?

中文的文档和博客都是一坨屎,干脆看英文算了

QList Class

结构体中运算符的重载

简单说说吧,就是有关这个链表。

链表是什么东西我们都比较懂,就是一个自动排序的工具,为什么要用这个东西呢?因为我们当前其实这个摄像头框的这个排序,它不是说你有人进出了之后,就整个窗体的所有人都重新排序,然后重新绑定窗口句柄,这样的效率肯定是很低的。所以我想的是当一个用户离开视频会议的时候,是所有的摄像头控件重新排序,比如 1 2 3 4 5 6,其中4走了,就变成了 1 2 3 5 6,后面的自动补上,这个很明显就是链表的逻辑。

那么我们该怎么实现呢?如果是单链表的形式,那么我们离开了一个用户之后,比如现在变成了1 2 3 5 6,那我们当前就没法知道 空着的窗口句柄是哪些了。

经过郑哥的提点,给出了一个新的数据结构逻辑,就是用户池。

我们设置八个已经绑定好了窗口句柄数据的结构体,如下

Struct SelectCam{
    Char* name;
    Char* seat;
    int HWND;
}

然后我们建立两个链表,分别是 QList *Activated 和 QList *UnActivated;

这是什么意思呢,就是 这个窗口句柄只能通过一个初始化方法给定,也就是说这相当于是八个坑,不管你萝卜怎么拔插,剩下的总是这八个坑。其中HWND只能初始化,而name和seat则可以不停地交换。

Activated链表代表的是当前正在聊天的窗口,而UnActivated则代表的是未在聊天的窗口。

### 如果进入一个学生:检查当前正在连线成员是否达到数量上限->从UnActivated中取出一个节点,修改其姓名和座次->将这个节点插入到Activated中去->通过DLL占据该窗口句柄->根据当前Activated链表中的人数修改窗口句柄的排列情况

### 如果离开一个学生:检查当前正在连线成员是否达到0个->从Activated中取出该座次节点->将这个节点插入到UnActivated中去->根据当前Activated链表中的人数修改窗口句柄的排列情况

其中修改窗口句柄排列情况会让UnActivated链表中的窗口隐形。

## 7.收发消息的解析

这里就要聊到关于输入输出参数了,在这里我写了个关于如何对输入字符串参数进行解析的正则模式如下:

最后我总结了一下如何在Qt中使用正则匹配模式,实例及参考文档详情请见博客页面 Qt有关正则表达式的写法与应用实例以及常见的正则匹配模式

8.关于数字类型int转char,有个有意思的点是以前没注意到的\

整型变量int和字符变量char都是通过ascii码表示的,所以其实两个类型是比较接近的,但是这两个类型之间当然可以直接强制类型转换,但是也有一些奇技淫巧

int转char

int i = 5;

char c1 = i;                  // 越界
char c2 = i - 0;              // 越界
char c3 = i - '0';            // 越界
char c4 = i + '0';            // 5

char转int char c = ‘0’;

int i1 = c;                    // 48
int i2 = c - 0;                // 48
int i3 = c - '0';              // 0
int i4 = c + '0';              // 96

9.同一进程下的窗体间交互,也就是connect函数的广泛使用方法

其实这个内容我一开始是琢磨了一下的,就是该怎么做,很多网上的博客都在告诉我就是,你用connect函数就可以连接了,但是为什么我实际做起来却感觉困难重重呢,其实这个和我对C++类 和 对Ui的理解不够有关,在这里我简单解释一下我的疑惑和我如何解决这些问题的理解方案。

首先一点就是,我们既然是在一个进程下,那么窗体之间肯定是可以进行交互的,那我们应该如何交互呢,这里我们就要从connect函数开始介绍了。

我们如何写connect函数?网上有很多说法,但是其实说的都不太明白,我这里详细说道说道。

首先,最基础的一点,我们要说道说道connect这个函数的参数

它能接收的是参数模式是connect(窗口指针,SIGNAL(),窗口指针,SLOT()); 这里问题就来了,我们如何获得窗口指针呢?这就是为什么在网上很多人说,connect绑定信号是绑定的父子窗体之间的信号,因为如果是父子关系的话,窗口指针就非常好获取,事实上直接就能获得。但是connect函数 之所以是Qt整个框架的框架之光,就在于它这个connect函数可以在全局内连接任何信号和槽函数。这也是Qt为什么方便的原因,方便的传值,方便的传指,这在实际的开发中确实可以省很多事,这也是很多高级语言也难以企及的。

扯了这么多,来谈一下实际应用。

我也就从0开始介绍这个东西怎么用,然后最后再来总结一下吧。

首先,我们要有两个窗口,也就是两个Ui文件。当然你可能下意识地以为,一个窗体ui类,就对应了一个.h和.cpp文件对不对,但是实际上并不是这么回事,其实一个.ui文件对应一个类就可以了。

这是怎么个意思呢?就比如我现在有两个Ui文件,一个是主窗体 mainFrame.ui,一个是子窗体 secFrame.ui,然后我们一般初始时命名会有一个mainFrame.cpp和mainFrame.h,其中编译器已经替我们写好了外壳

mainFrame.h下:

#include <QtWidgets/QMainWindow>
#include "ui_mainFrame.h"

class mainFrame : public QMainWidget{

Q_OBJECT

public:

    mainFrame(QWidget *parent =Q_NULLPTR);

private:

    Ui::mainFrame ui;
}

一条条解释一下,首先是#include <QtWidgets/QMainWindow> 这条没什么好说的,这条就是引入QWidget和QMainWindow

#include “ui_mainFrame.h” 这条就是引入你这个ui文件的头文件,其实也就相当于是让这个ui文件进入你这个头文件,这样你就可以在Ui::mainFrame ui这行语句中获得这个ui的对象了,也就是你在后续可能经常会用到什么ui.控件->方法 什么的,就是通过这行语句引入到你的类中来的。在构造函数中,常常有这么句话,就是ui.setupUi(this),代表了在这个类内装载了这个ui,装载了之后就可以对this指针做一些窗体操作了。

Q_OBJECt只是一个宏命令,不用管是做什么的,相当于是一种声明,具体作用可以看百度。

然后如果我们要在这个类内引入一个新的窗体,我们该怎么做呢?

姑且假设我们这个新的窗体叫secFrame,也就是我们前面提到的子窗体。我们首先需要新建一个ui文件,这一步就不说了,你只要新建ui 文件就行,你建立完成之后,保存编译一下,编译器会自动生成一个ui文件,名字就叫做ui_secFrame.h

我们引入这么个文件,好,我们该怎么做呢?这个Ui,或者说我们该怎么使用呢?难道也跟之前那个ui一样?

戳啦,我们新建一个类。为什么这么说呢,因为其实你想想也知道,每一个窗体都应该是独立的,那么我们的每一个窗体是不是都应该有一个类来为这个窗体托底?或者怎么说呢,如果我两个ui绑定的一个类…..也不是不行,当然应该是可以的,但是能不能好使,我也不好说。总而言之我这里告诉你就新建一个类,如果是一个主窗体另外一个是临时的小窗体可能还可以,如果是两个体量较大的窗体,那么分开来封装好肯定是更合适的。当然这里只介绍分开来做的情况,如果你分开做都会了,那么合并对你来说肯定更加简单了不是吗。

然后我们新建一个类,跟上述那个类差不多,但是要注意的就是这个Ui的声明不同了,变成了这样

Ui::secFrame ui;

然后我们要怎么调用呢?其实也很简单,这时候就要回到我们的主程序main,众所周知,一个程序主要是从main函数开始的,而且我们一般情况下不会随意地动主程序main,

#include "LBD_VideoMeeting_Meeting.h"
#include <QtWidgets/QApplication>

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    LBD_VideoMeeting_Meeting w;
    w.show();
    return a.exec();
} 看,我们其实主程序也没干什么别的,就是初始化了一个一个类,然后让他show了起来,就这么简单。

那么我们如果想要让子窗口亮起来?该怎么办呢?在main里面声明吗?那connect怎么办?

所以我们最好其实直接在mainFrame这个类内声明一个secFrame类的实例就可以了,然后直接 操作这个指针

具体细节可以见我的仓库,这里不再详述

10.关于背景图片

背景图片的话常常需要拉伸,而且我们的摄像头窗体控件又经常要移动,如果只是单纯的用绑定拉伸,是没有什么用的,每次修改之后可能要重新绘制窗体才能有用。我现在的想法就是,让窗体控件的底图就直接是加载画面,然后画面出来之后会自动覆盖掉底图的做法。后续如果说当软件接到底层DLL的回调函数信息之后,就让这个窗体重新绘制即可。

setStyleSheet(“border-image: url(图片路径)”)这种

image

文档信息

Search

    Table of Contents