文章来源地址https://www.toymoban.com/news/detail-785917.html
先贴上效果图:
本文将从客户端说起,从客户端到服务端(如何搭建云电脑连接外网)开始一步步实现聊天室。
全程采用c/c++语言,教程会用到MFC,不熟悉的朋友可以先去了解一些基础知识,有用到的知识点会在文章中进行详细的介绍以及解释。
先从客户端开始做起,我们先根据效果图对客户端功能进行分析:
这是一个基于对话框的窗口。
我们要实现等待功能如下:
1.建立一个文本框,并且将聊天内容展示到文本框内。
2.建立两个编辑框,一个发送按钮和一个自动回复的可选择按钮,用于实现发送和自动发送功能。
3.建立编辑框保存昵称,保存成功后在当前目录下生成配置文件,方便下次读取。
4.设立两个编辑框分别写入端口号和IP地址,并设立连接和断开按钮。
5.聊天室还可以支持字体颜色的改变。
我们先从最简单的布局开始。
在新建一个MFC工程后,我们点击资源视图,找到菜单界面。
注意:创建工程的时候选择"基于对话框"类型。
然后生成的类选项选择"Dlg",其余的保持默认即可。
完成操作后编辑软件上会显示这个界面,我们对里面的控件进行删除,并且修改后,最终得到我们客户端的界面。(都是一些简单的控件拉取,控制大小,对齐等等操作,不懂得可以在b站或者其他教学视频上找来看看,一看就懂了。界面不一定要这么设置,大家可以根据自己的喜好进行布局,只要功能控件能够一一对应就好啦!)
需要注意的是,添加完控件后,我们需要点击它,修改它的控件ID,这是为了方便我们后面的操作,最好能做到控件ID见名知意,这样后面操作起来会非常的方便。
完成界面后,我们就要开始实现具体的功能了。温馨提示一下,开发过程中可能会遇见很多的bug,我们会对代码进行反复修改,这个过程最好能将代码有意识的进行备份,特别是在我们实现一个功能后,将代码备份是个好习惯。可以利用git仓库或者其他软件工具进行备份和对比,这里我就不过多赘述了,大家可以自行百度了解,学习一下如何把代码备份。
我们先对联网功能进行实现,由于还没有开始搞服务器外网的搭建。所有这里的IP地址一律用127.1.1(即本机IP的意思)。
功能其实也不难,就是当我鼠标点击连接的时候,我能够从上面两个框框里获得具体信息,然后联网。但是这里会用到socket编程和TCP协议,感兴趣的朋友可以先去了解一下。
我们要在我鼠标点击"连接"按钮的时候进行响应,所以我要为"鼠标点击连接按钮"这个事件添加一个回调响应函数。MFC中添加这个响应函数的操作很简单,我们找到具体按键,双击它,就会自动跳转了。
如果你在这一步操作的时候,不小心点错了按钮, 这个时候直接把上面那个函数体删除是会报错的,因为我们在UI界面双击按钮的时候编辑器不止为我们创建了函数体,它还会创建头文件和一个注册响应函数的操作。我们按Ctrl+z撤回刚刚的操作就可以了。
接着便逐行分析一下点击连接后要进行的操作:
void CMFCTALLINGROOMDlg::OnBnClickedConnectBtn()
{
//定义两个保存串口和IP地址的变量
CString strPort, strIP;
//下面两个函数是从我的端口编辑框和IP编辑框里获取字符串
GetDlgItem(IDC_PORT)->GetWindowText(strPort);
GetDlgItem(IDC_IPADDRESS1)->GetWindowText(strIP);
//-----------CString 转 char*------------
USES_CONVERSION;
char* szPort = (char*)T2A(strPort);
char* szIP = (char*)T2A(strIP);
//-----------------------------------------
//--------字符串转数字-------------------
int iPort = _ttoi(strPort);
//这里是给我的socket指针 new 一个socket对象
//至于为什么类型是CMysocket,我们后面再详细解释
m_clientSocket = new CMySocket;
//检测是否创建成功
if (!m_clientSocket->Create())
{
return;
}
//检测是否连接成功
if (m_clientSocket->Connect(strIP, iPort)== 0)
{
return;
}
}
这串代码有几个要注意的点:
1.用socket连接的时候是要用到c语言的字符数组的,所以我们这里用了一个常用的字符串转char数组的方法,以及字符串转int的方法。后面还会经常用到,大家可以记在笔记上方便以后查阅。
2.在我的m_clientSocket成员new一个socket对象的时候,用的并不是常规的socket类,而是CMySocket,这是一个我自己定义的重写类,继承自CAsyncSocket。至于为什么要对它重写,我们后面再做详细解释,大家做到这一步可以学着添加一个继承自CAsyncSocket类的socket类,名字取什么无所谓,好记就行.
3.从文本编辑框获取文本的方法:
GetDlgItem(ID)->GetWindowText(str);
这是一个非常常用的方法,ID指的是编辑框控件的ID,意思是指定你要操作的控件。而GetWindowText表示你对控件的操作是读取它的文本内容,str自然是读取后的保存位置。
如果前面的控件ID你是见名知意的话,那么这里你应该可以很容易写出来,如果不是,那么你可能要重新回到菜单设计界面,去找一下IP编辑框和端口编辑框的控件ID了。
这串代码实现了我们第一个功能,就是从编辑框上获取内容并且连接。但是不管是测试还是实际,我们所使用的ip和端口都是比较固定的,因此我们可以在类的初始化构造函数上加两行代码,让这两个编辑框一打开就有一个默认值。
//_T是编码问题
GetDlgItem(IDC_PORT)->SetWindowTextW(_T("2400"));
GetDlgItem(IDC_IPADDRESS1)->SetWindowTextW(_T("43.139.254.236"));
对话框类的初始化在这里:
方法实现的下面也有这样的提示:
通过以上步骤,我们已经能将客户端连上网络的某个端口了。那么我们如何让它在收到信息的时候显示到最上面的文本框内,又如何在我们点击发送的时候让其通过socket发送呢?
分析一下发现:发送功能比较好实现,我只要在发送点击响应函数里,获取发送框的信息然后通过m_ClientSocket套接字发出去就好了。但是接收却不好实现,如果我们还是用的CAsyncSocket,那么这个套接字类在当前套接字收到信息的时候根本不会对这串信息做出反应,更不可能把它显示到我们想要显示的文本框内。
所以我们需要将CAsyncSocket 进行重写,将我们需要的一些功能加载到这个类内。
在这个类中,主要重写OnConnect和OnReceive方法,重写析构函数是为了删除执行前面方法过程中创建的一些资源。
OnConnect方法是套接字连接成功后调用的方法,我们需要重写它,让其连接成功后在文本框内显示一串文字通知用户,而OnReceive方法则是该类收到信息后的调用函数,我们需要重写它,接收它收到的信息并列在文本框中。
void CMySocket::OnConnect(int nErrorCode)
//connect成功连接后的回调函数
//相当于connect连接成功后返回的数据指令是这一整个函数
{
CMFCTALLINGROOMDlg* dlg =
(CMFCTALLINGROOMDlg*)AfxGetApp()->GetMainWnd();//获取当前主窗口对象
CString str = dlg->CTimeAndShow(_T("与服务器连接成功!"));//str保存要上传的信息
dlg->m_msg_list.AddString(str);//把消息显示到文本框去
CAsyncSocket::OnConnect(nErrorCode); //类内方法标准格式,可以忽略
}
void CMySocket::OnReceive(int nErrorCode)
{
CMFCTALLINGROOMDlg* dlg =
(CMFCTALLINGROOMDlg*)AfxGetApp()->GetMainWnd();//获取窗口对象
char recv_buff[MSG_LEN] = { 0 }; // 定义字符串保存信息
Receive(recv_buff, MSG_LEN, 0); //从socket中接收信息
//2.把信息显示到屏幕上
//定义CString类,把信息转到CString类中去,因为文本显示要求的是CString
CString str_show;
CString str_msg;
//str_msg保存我接受到的信息,str_show是将时间轴和信息拼接过后的显示内容
str_msg += recv_buff;
str_show = dlg->CTimeAndShow(str_msg);
//对信息内容进行显示
dlg->m_msg_list.AddString(str_show);
}
注意到:每条信息前面都有时间轴,于是我们不仅仅需要其显示打印的内容,还需要把时间加上去。CTimeAndShow(Str)就是实现这个功能的。
CString CMFCTALLINGROOMDlg::CTimeAndShow(CString msg)
{
CString str_show;//定义返回值
CTime tm_now;//定义CTime获取时间,这个是MFC中的一个类
//--------获取当前时间-----------------
tm_now = CTime::GetCurrentTime();
//-----------将时间与传进来的字符串进行拼接,其中时间以一定格式返回--------------
str_show = tm_now.Format("%X");
str_show += msg;
return str_show;
}
这里需要注意一个点:在CMFCTALLINGROOMDlg类内定义方法的时候,我们可以使用里面的一些函数非常方便的给其控件添加内容,但是重写的CMySocket是外部的类,要在CMFCTALLINGROOMDlg类的窗口上显示一些信息,需要先定义并对当前窗口进行获取才行。(我们CMySocket是在哪个窗口定义的,就会返回哪个窗口的指针给它)
至此,在我们连接成功的时候它会提示用户连接成功,在我们收到信息的时候也会得到显示。并且每条信息前面都带了时间轴。
是时候给发送按钮添加一个函数,让它能够发送信息了。
还是先分析一下思路:点击发送的时候,我们不仅仅要把信息发出去,还要显示在自己的文本框内。并且我们是有用户ID的,那么发送出去的时候我们需要还需要把我们的ID也发出去。
发送功能实现如下:
当发送内容为空的时候我们需要提示一下用户,并取消发送。
发送内容后,自动将编辑框清除:
代码如下:
void CMFCTALLINGROOMDlg::OnBnClickedSendBtn()
{
//1.获取编辑框的内容
CString send_buff = 0; //保存消息编辑框内的信息
CString send_all; //保存对话框内信息+时间抽等
GetDlgItem(IDC_SEND_EDIT)->GetWindowTextW(send_buff);//获取对话框信息
if (send_buff.GetLength() <= 0)//如果获取不到就给用户提示
{
MessageBox(_T("消息内容为空!"));
return;
}
//2.将编辑框的内容拼合自己的名字发送出去
send_all = name + _T(":") + send_buff;
//前面提到的转换
USES_CONVERSION;
char* sz_send_buff = (char*)T2A(send_all);
m_clientSocket->Send(sz_send_buff, MSG_LEN, 0);
//3.将编辑框的内容拼合数据后显示在聊天界面上
CString str_show;
//在自己本地上显示的时候,除了拼接好的信息以外,记得还要加上时间轴
str_show = CTimeAndShow(send_all);
//显示在自己的客户端文本框上
m_msg_list.AddString(str_show);
//发送完成后,清空编辑框。
GetDlgItem(IDC_SEND_EDIT)->SetWindowTextW(_T(""));
//4.更新数据
//将变量更新到控件里去
UpdateData(FALSE);
}
这串代码没什么好注意的,无非就是接收编辑框的信息,然后拼接ID发送,再将发送的信息拼接时间轴展示到本地的文本框上罢了。需要注意的是,客户端内我们需要自己添加一个name变量来保存我们的ID,后面我们还要用一些方法来将我们这个ID初始化。现在这一步你可以将它初始化成未命名。
补充:m_msg_list是文本编辑框的关联变量,这里忘记进行说明了,大家可以直接跳到下面改变字体颜色的地方查看如何生成控件的关联变量。
下一步我们来设计一下自动回复功能,功能如下:
自动回复内容为空 时,同样提示一下用户,并且不让它设置成功。
自动回复功能:
写这个功能也不难,在我们接收到信息并展示到文本框的时候,判断一下自动回复按钮是否被选中,如果被选中,就读取自动回复框内的内容再次发出去。
代码如下:
点击自动回复按钮时,需要判断框内容是否为空:
void CMFCTALLINGROOMDlg::OnBnClickedAutosendRadio()
{
//点击的时候将按键标签设为true
((CButton*)(GetDlgItem(IDC_AUTOSEND_RADIO)))->SetCheck(true);
CString auto_msg; //保存自动回复编辑框的信息
GetDlgItem(IDC_AUTOMSG_EDIT)->GetWindowTextW(auto_msg);//获取自动回复编辑框的信息
if (auto_msg.GetLength() <= 0)//如果没有
{
MessageBox(_T("你的自动恢复内容为空!"));//提示
//然后将按钮取消选中
((CButton*)(GetDlgItem(IDC_AUTOSEND_RADIO)))->SetCheck(false);
return;
}
}
OnReceive添加部分:
if (((CButton*)(dlg->GetDlgItem(IDC_AUTOSEND_RADIO)))->GetCheck())
{
CString auto_msg;
dlg->GetDlgItem(IDC_AUTOMSG_EDIT)->GetWindowTextW(auto_msg);
auto_msg = dlg->name + _T(":") + auto_msg;
USES_CONVERSION;
char* sz_send_buff = (char*)T2A(auto_msg);
dlg->m_clientSocket->Send(sz_send_buff, MSG_LEN);
CString str_show = dlg->CTimeAndShow(auto_msg);
dlg->m_msg_list.AddString(str_show);
}
dlg->UpdateData(FALSE);
CAsyncSocket::OnReceive(nErrorCode);
这串自动回复的代码和发送代码大同小异,就不一一解释了,仔细琢磨一下你一定可以写出来的。
自动回复的时候还有一个小bug,就是当你接收到信息和发出去信息后将它显示到聊天框内间隔很短的时候,它有时候会出现顺序颠倒的情况,我们文本框的排序属性改为false就可以解决了。
然后我们再来分析一下保存昵称的功能,我们之前已经定义了一个name来保存用户ID(在发送信息的时候使用过),当用户点击按钮的时候,我们把昵称编辑框的内容保存到name上。这是最基础的,在此基础上我们添加一个功能,就是在当前目录下生成一个.ini的配置文件,每次启动的时候读取配置文件的ID,然后将它设为用户的默认ID,这样用户下次开启软件的时候就会自动显示上次的ID了。
从配置文件的默认ID:
修改昵称时提示一下:(不过这里好像没有添加取消按钮,大家可以自己琢磨一下怎么再添加一个取消按钮上去)
如果用户ID为空白字符,我们依旧给它设置,不过进行的提示是这样子的,你们也可以设置成空白昵称设置失败,这个就仁者见仁智者见智了。
看代码之前先解释一下函数及其生成配置文件的格式
WritePrivateProfileStringW(_T("CLIENT"), _T("NAME"), name, str_file);
函数结构如下:
BOOL WritePrivateProfileStringA(
LPCSTR lpAppName,
LPCSTR lpKeyName,
LPCSTR lpString,
LPCSTR lpFileName
);
LPCSTR lpAppName:这个参数叫节(如果不存在就创建它)
LPCSTR lpKeyName:这个参数叫键(一样的,不存在就创建一个新的)
LPCSTR lpString:这个参数是你往当前文件下的节和键下面需要写的内容,如果填空就会删除lpKeyName指向的键。
LPCSTR lpFileName:这个是你要写入节,键的文件名,如果文件不存在他会自动创建。在下面的函数中,我的文件名是包含了整个路径的。
节和键是该函数生成配置文件时一种特有的数据保存方式,当我们需要读取数据的时候,也是通过这两个定位到想要的数据的。
代码如下:
void CMFCTALLINGROOMDlg::OnBnClickedSavenameBtn()
{
GetDlgItem(IDC_ID_EDIT)->GetWindowTextW(name);//将编辑框内容获取并保存到name变量中去
//如果长度小于0就设置成空白字符
if (name.GetLength() <= 0)
{
MessageBox(_T("昵称为空白字符!!"));
return;
}
//如果不是就提示用户是否需要修改昵称
else if (IDOK ==
AfxMessageBox(_T("你真的要修改昵称吗?")), MB_OKCANCEL)
{
//2.定义路径变量并且获取当前路径
WCHAR str_path[MAX_PATH] = { 0 };//路径变量
//获取当前路径是因为我们的配置文件就在当前路径下
GetCurrentDirectoryW(MAX_PATH, str_path);
//3.定义一个字符串,将文件名拼接上去
CString str_file;
str_file.Format(L"%ls//Test.ini", str_path);
//4.用函数以特定的方式生成配置文件
WritePrivateProfileStringW(_T("CLIENT"),
_T("NAME"), name, str_file);
}
}
生成的配置文件如下图:
我们再看看初始化时,如何去读取信息到name成员里面去:
WCHAR str_name[MAX_PATH];//定义保存名字的变量,函数内必须使用这个格式
WCHAR str_path[MAX_PATH] = { 0 };//定义当前路径
GetCurrentDirectoryW(MAX_PATH, str_path);//获取当前路径,这里MAX_PATH是路径内存最大值
CString str_file;//定义文件名
str_file.Format(L"%ls//Test.ini", str_path);//拼接文件名和路径
int ret =GetPrivateProfileStringW(_T("CLIENT"), _T("NAME"),NULL,
str_name,MAX_PATH, str_file);//从文件名路径中根据节名和键名获取到内容并且保存到str_name
if (ret > 0)
{
//数据正常直接赋值给name,并显示在编辑框上给用户看
name = str_name;
GetDlgItem(IDC_ID_EDIT)->SetWindowTextW(str_name);
}
else
{
//数据不正常就显示未命名。(第一次打开程序的时候是没有配置文件并且读取不到数据的)
name = _T("未命名");
GetDlgItem(IDC_ID_EDIT)->SetWindowTextW(name);
}
这里有个不严谨的地方就是我名字变量的内存最大值跟路径变量的内存最大值都是用的同一个宏MAX_PATH,你们可以自行修改,定义两个宏,一个MAX_NAME,一个MAX_PATH。
这样我们的昵称功能就完成了。主要是对配置文件的一个理解,下面还要一个字体颜色选择的功能。
我们字体选择功能用的控件是一个可选择的COMBO控件,使用它的时候我们要添加一个控件变量
这里为了演示,用到的名字跟下面代码的名字是不一样的...
(文本编辑框控件变量实现同下)
添加完成后编译器会自动把控件和变量进行关联。
我们还需要生成一个信号:
实现代码如下:
//初始化代码,给控件添加这些内容,并让它默认选择黑色0
m_WordColor.AddString(_T("黑色"));
m_WordColor.AddString(_T("红色"));
m_WordColor.AddString(_T("绿色"));
m_WordColor.AddString(_T("蓝色"));
m_WordColor.SetCurSel(0);
下面这串代码比较长,一点点分析:
HBRUSH CMFCTALLINGSRVDlg::OnCtlColor(CDC* pDC, CWnd* pWnd, UINT nCtlColor)
{
HBRUSH hbr = CDialogEx::OnCtlColor(pDC, pWnd, nCtlColor);
CString strColor;
m_WordColor.GetWindowTextW(strColor);
if (IDC_MSG_LIST == pWnd->GetDlgCtrlID() ||
IDC_SEND_EDIT == pWnd->GetDlgCtrlID())
{
if (strColor == "黑色")
{
pDC->SetTextColor(RGB(0, 0, 0));
}
else if (strColor == "红色")
{
pDC->SetTextColor(RGB(255, 0, 0));
}
else if (strColor == "绿色")
{
pDC->SetTextColor(RGB(0, 255, 0));
}
else if (strColor == "蓝色")
{
pDC->SetTextColor(RGB(0, 0, 255));
}
}
return hbr;
}
在聊天项目实现改变字体颜色的需求中,用这串代码可以实现。
if (IDC_MSG_LIST == pWnd->GetDlgCtrlID() ||
IDC_SEND_EDIT == pWnd->GetDlgCtrlID())
一开始对上面这串代码感到奇怪,因为它是一个判断语句而不是一个赋值语句。
一开始我以为改变字体颜色的代码会拿到窗口的控件ID,然后将控件ID和颜色丢给某个函数来实现。但是下面代码很明显不是这样的:
if (IDC_MSG_LIST == pWnd->GetDlgCtrlID() ||
IDC_SEND_EDIT == pWnd->GetDlgCtrlID())
{
if (strColor == "黑色")
{
pDC->SetTextColor(RGB(0, 0, 0));
}
}
他是在判断某个ID等于一个指定的控件ID后,在用另外一个类对象去改变它的颜色。
后来通过Debugview研究可以发现,原来这个消息的运行机制就是对窗口的所有控件进行不断地扫描变色,当这个消息机制扫描到我指定的那个ID后,就能够对它执行相应的变色操作。
OnCtlColor是一个不断重复运行监测的消息。它会不停的扫描MFC窗口中的所有控件,并根据后面的代码执行相应的操作。
这样一看,整个代码的逻辑就很清晰了。
HBRUSH CMFCTALLINGSRVDlg::OnCtlColor(CDC* pDC, CWnd* pWnd, UINT nCtlColor)
{
HBRUSH hbr = CDialogEx::OnCtlColor(pDC, pWnd, nCtlColor);//拿到当前扫描的控件的一些信息
CString strColor;//用来保存当前的颜色
m_WordColor.GetWindowTextW(strColor);//在选择框里面获取当前是什么颜色
if (IDC_MSG_LIST == pWnd->GetDlgCtrlID() ||
IDC_SEND_EDIT == pWnd->GetDlgCtrlID())
//当扫描到发送编辑框和聊天编辑框这两个控件的时候
//根据当前字符串的颜色对当前扫描到的两个空间之一进行改变
{
if (strColor == "黑色")
{
pDC->SetTextColor(RGB(0, 0, 0));
}
else if (strColor == "红色")
{
pDC->SetTextColor(RGB(255, 0, 0));
}
else if (strColor == "绿色")
{
pDC->SetTextColor(RGB(0, 255, 0));
}
else if (strColor == "蓝色")
{
pDC->SetTextColor(RGB(0, 0, 255));
}
}
return hbr;
}
最后还有一个清屏操作,一行代码就可以实现:
m_msg_list.ResetContent();
这里忘记说明了,前面的消息文本显示框也采用定义变量的方式实现的。
到这里客户端的主体功能就全部完成了,大家可以试试连接主机本机的IP地址127.1.1进行通信发送,但是这种发送仅仅局限于本机,如果想实现真正的外网群聊发送的话,需要另外搭建服务器并且写一个客户端程序处理数据(后面出教程),但是这样实现依旧会有很多的小bug,比如程序一开始还没有连接就能点击取消连接,发送信息,比如修改昵称确定对话框那里没有添加一个取消按钮等等,还可以进行一些功能的添加,例如插入背景图片,利用按钮打开计算机本机的程序等。
这是一开始没连接的时候让一些控件不能被点击的代码,那么什么时候让这些按键可以被点击呢,大家可以自己思考一下。相信做到这一步的你可以将客户端功能进行进一步完善的!
//控件初始化
GetDlgItem(IDC_CONNECT_BTN)->EnableWindow(TRUE);
GetDlgItem(IDC_DISCONNECT_BTN)->EnableWindow(FALSE);
GetDlgItem(IDC_SEND_BTN)->EnableWindow(FALSE);
GetDlgItem(IDC_AUTOSEND_RADIO)->EnableWindow(FALSE);
GetDlgItem(IDC_COLOR_COMBO)->EnableWindow(FALSE);
教程到此结束,更多的是对之前做过项目的总结,感谢观看。文章来源:https://www.toymoban.com/news/detail-785917.html
到了这里,关于C/C++手把手从零搭建多人群聊聊天室(客户端)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!