对于WinSock编程的初学者来说,由于控制台程序不涉及Windows的界面及消息响应机制,因此能更容易理解WinSock编程的流程,但目前大多数应用程序都是Windows界面的,因此需要学习将控制台程序改造成Windows界面的程序,而改成Windows界面程序的关键是将WinSock编程的代码嵌入到Windows界面程序的合适位置中。
3.1 Windows对话框程序
对于Windows界面的程序来说,可分为2种,一种是对话框程序,一种是窗口程序,其中窗口程序又可分为SDI(单文档窗口)和MDI(多文档窗口)。
对话框与窗口的区别在于:窗口的上方有菜单栏和工具按钮栏,下方有状态栏。例如Word软件,就是一个窗口程序;而对话框没有菜单栏和工具栏等,例如QQ的登录界面就是一个对话框。因此,对话框其实是一种简单的窗体,对于简单的Windows界面程序,一般都采用对话框程序来制作(本书所有的Windows程序都是基于对话框的)。
3.1.1 新建对话框程序
在VC6中,执行菜单命令“文件→新建”,将打开如图3-1所示的新建工程对话框中,要新建Windows界面的应用程序,有以下两种方法:
1)选择Win32 Application,这种方法是使用Windows API函数开发图形界面的应用程序,API是应用程序接口(Application Programming Interface)的意思,Windows API编程就是指调用Windows的接口函数来进行程序的编写,例如MessageBox()就是一个API函数或者说接口函数。微软提供了上千个标准的API函数,这些函数定义在微软的SDK(Software Development Kit,软件开发包)提供的头文件、库文件、工具之中。因此Windows API编程也称为Windows SDK编程。
2)选择MFC APPWizard(exe),这是使用MFC类库开发图形界面的应用程序。为了降低API函数编写Windows应用程序的难度,微软开发了微软基础类库(Microsoft Foundation Class, MFC),MFC采用面向对象技术,将几乎所有的SDK API函数封装在不同的类之中,从而提供了一套专门用于开发Windows应用程序的开发框架。
图3-1 新建工程对话框
一般来说,初学者学习Windows界面编程,应先学习Windows API编程,有了一定基础后再学习MFC编程,这样可以降低学习难度,且能更深入地理解MFC的实现原理,因此本章专门介绍Windows API编程。
MFC在新建工程的第二步提供了一个“基本对话框”选项,专门用来创建对话框程序,而Win32 Application在新建工程时没有提供新建对话框程序选项,需要手工修改“一个典型的Hello World程序”的代码,具体步骤如下:
1. 使用Win32 Application新建对话框程序
1)新建工程,选择“Win32 Application”,输入工程名(如Dem),单击“下一步”,在图3-2所示的对话框中,选择“一个典型的Hello World程序”,单击“完成”。
图3-2 建立Win32 Application
如果要在VS2010中新建项目,则执行菜单命令“文件→新建→项目”,在Visual C++中,选择“Win32项目”,在下方“名称”后输入项目名,在下一步中单击“完成”即可。
2)直接运行该工程。在图3-3所示的工作空间窗口,单击Build(F7),再单击Execute(Ctrl+F5),就会弹出如图3-4所示的Windows窗体程序,执行该窗体中的菜单命令“Help→About”,图3-4中将出现About对话框。可见该程序包含一个窗体和一个对话框。
图3-3 工作空间窗口
图3-4 Hello World程序运行效果
在图3-3中,选择底部的ResourceView选项卡,可以查看该窗口程序对应的资源文件,如图3-5所示。其中,IDD_ABOUTBOX是图3-4中弹出对话框的ID,双击它可对该对话框资源进行编辑。另外,由于该程序是一个窗口程序,所以具有菜单(Menu)资源。
在图3-3中,选择ClassView选项卡,可以查看该程序中的所有类,如图3-6所示。由于该程序中没有创建任何类(class),因此程序中所有的函数和变量在Globals目录下,表示它们是全局函数或全局变量。可见,全局函数就是不属于任何类的函数。
图3-5 ResourceView选项卡 图3-6 ClassView选项卡
3)接下来我们要去掉图3-4的窗体界面让程序直接弹出About对话框。方法是:在图3-3的左侧工作区中选择FileView选项卡,找到该工程的源文件(工程名.cpp,如Dem.cpp),将WinMain函数中的第1行(文件的第25行)开始到DialogBox上一行(147行)的代码全部删除,再从DialogBox下一行(149行)到return 0的上一行之间的代码也全部删除。也就是只保留WinMain函数中DialogBox一行(148行)和return 0; 一行,再将DialogBox函数中第3个参数由hWnd改为NULL。此时WinMain函数的代码如下:
int APIENTRY WinMain(……){
DialogBox(hInst, (LPCTSTR)IDD_ABOUTBOX, NULL, (DLGPROC)About);
return 0;
}
其中:
WinMain()函数是Windows应用程序的入口函数,相当于控制台程序中的main函数。
DialogBox函数的功能是创建一个对话框,hWnd是调用该对话框的窗口的句柄变量名,由于窗口对应的所有代码已经被删除,因此将调用该对话框的窗口句柄改成NULL,这样启动程序时,会直接加载对话框。
DialogBox()函数有4个参数,含义如下:
① hInst:设置本对话框属于当前进程,因为HINSTANCE是窗口进程的句柄;
② IDD_ABOUTBOX:设置本对话框使用哪个对话框的资源;
③ NULL:指定本对话框的父窗口是哪个,如果没有父窗口,则设为NULL;
④ About:指定本对话框的消息处理函数,用来处理对话框初始化及按钮等产生的消息。
提示:句柄(handle)是Windows编程中的重要概念,句柄是Windows中各个对象的一个唯一的、固定不变的ID;作用上,Windows使用句柄来标识应用程序中的不同对象或同类中的不同实例,诸如,一个窗口,按钮,图标,滚动条,输出设备,控件或者文件等。逻辑上,句柄相当于Windows对象指针的指针;Windows通过对象的地址来访问对象,但Windows采用虚拟内存机制,对象的地址是经常发生变化的,为此,需要使用一个内存区域来记录对象的地址,这块内存区域的地址在程序的运行中是不会变化的,而句柄的值就是这块内存区域的地址,因此,句柄相当于Windows对象指针的指针。
4)在对话框中添加控件,修改后对话框界面如图3-7所示,方法是:
①单击图3-8所示的控件工具栏中的“选择控件”工具,按住Shift键依次选中图3-4About对话框左侧的图片框和2个静态文本,按Del键将其删除。
②在图3-8所示的控件工具栏中选择相应的控件,向对话框中添加1个静态文本控件,2个编辑框和1个按钮。
③ 在图3-9所示的属性面板中设置控件的属性。方法是先选中控件,按右键单击属性(或按Alt+Enter键),将弹出如图3-9所示的控件属性面板,将各控件的ID设置为如图3-10所示。再设置静态文本控件、两个按钮和对话框的标题如图3-7所示,并在属性面板中将图3-7中下方编辑框的样式选项勾选为“只读”。
图3-7 对话框的界面
图3-8 控件工具栏
图3-9 控件的属性面板
图3-10 对话框各控件的ID值
提示:
1)在图3-9的属性面板中,ID是程序访问控件的依据,例如GetDlgItem(hDlg, IDC_ EDIT2),就能让程序获取到IDC_EDIT2这个编辑框,因此,除静态文本外,其他控件的ID名都不能重复。而标题属性用来设置控件上显示的内容,因此,静态文本和按钮通常必须设置标题属性,而编辑框等可供用户输入内容的控件是没有标题属性的。
2)如果图3-8所示的控件工具箱不见了,可在VC6主界面的工具栏的空白处上按右键,在右键菜单中勾选“控件”就能显示控件工具箱。
3)单击图3-9属性面板左上角的“图钉”按钮,可以让属性面板总是保持显示。
3.1.2 处理Windows消息
Windows程序是通过系统发送的消息来响应用户的输入的。例如,用户单击按钮、单击鼠标、移动鼠标等程序运行时所产生的动作,都称为事件,应用程序的初始化也是一个事件,每个事件都会产生一个对应的消息,应用程序通过接收消息、分发消息、处理消息来和用户进行交互。
Windows消息可分为标准Windows消息、控件通知和命令消息等类型。其中,命令消息是指用户单击按钮、菜单项、工具栏按钮产生的消息,其名称为WM_COMMAND。除WM_COMMAND外,所有以“WM_”为前缀的消息都是标准Windows消息,例如,WM_INITDIALOG表示对话框及其所有子控件都创建完毕(完成初始化),另外,还能自定义Windows消息,如WM_SOCKET。实际上,每个Windows消息都对应一个常量值。
在Win32 Application中,对消息处理的代码都要写在回调(CALLBACK)函数中,所谓回调函数,是指一个通过函数指针调用的函数。与普通函数不同,回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件触发时由其他的函数调用,用于对该事件或条件进行响应。
例如DialogBox函数的作用是从一个对话框资源中创建一个模态对话框。该函数一直到指定的回调函数通过调用EndDialog函数中止模态对话框才能返回控制,当对话框初始化时或单击对话框上的按钮,都会产生一个Windows消息,在消息发生时,DialogBox函数就会调用指定的回调函数对消息进行处理。DialogBox函数的第4个参数用来指定它的回调函数,例如:
DialogBox(hInst, (LPCTSTR)IDD_ABOUTBOX, NULL, (DLGPROC)About);
其中,第4个参数(DLGPROC)About指定了该函数的回调函数是About()。
因此,我们可在名为About的回调函数中对该对话框的消息进行处理,示例代码如下:
LRESULT CALLBACK About(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam) //4个参数为:窗口句柄,消息名,消息参数1,消息参数2
switch (message) { //处理感兴趣的消息
case WM_INITDIALOG: //处理对话框初始化消息
return TRUE;
case WM_COMMAND: //处理按钮消息
if (LOWORD(wParam) == IDOK || LOWORD(wParam) == IDCANCEL) {
EndDialog(hDlg, LOWORD(wParam)); //关闭对话框
return TRUE; }
break; }
return FALSE; }
可见,一个消息由一个消息名称(UINT)和两个参数(WPARAM,LPARAM)组成。
当用户单击某个按钮就会产生一个WM_COMMAND的通知消息,这条消息的wParam参数的低位字节中含有该控件的ID。因此通过LOWORD(wParam)的值就能判断用户单击的是哪个按钮,wParam参数的高位字节则为通知代码,lParam参数则是指向控件的句柄。
因此要处理用户单击确定按钮(IDC_QD)产生的通知消息,只要对case WM_COMMAND中的代码作如下扩充即可。
case WM_COMMAND:
if (LOWORD(wParam) == IDOK) { //处理单击退出按钮的消息
EndDialog(hDlg, LOWORD(wParam)); //关闭对话框
}
if (LOWORD(wParam) == IDC_QD) { //处理单击确定按钮的消息
……
}
提示:回调函数的返回值类型用LRESULT CALLBACK定义,LRESULT是一个数据类型,L表示long(长整型),result表示结果,说明这个函数的返回值是某个结果。CALLBACK是一个空宏,只是为了表示这是一个回调函数。
3.1.3 获取和设置控件的内容
图3-11所示程序的功能是,当用户在编辑框1中输入内容,单击“确定”按钮,编辑框2中就会显示“欢迎您,编辑框1的内容”。因此单击确定按钮的程序流程是:①获取用户在编辑框1中输入的文本内容,②将该文本内容进行修改后显示到编辑框2中。
因此程序的关键是获取和设置编辑框的内容。Windows API提供了如下2个函数:
1)GetDlgItemText():获取编辑框中的内容,例如:
GetDlgItemText(hDlg,IDC_EDIT1,user,50);
表示获取对话框hDlg中编辑框IDC_EDIT1中的内容,并将该内容保存到变量user中,最多获取50个字符。
2)SetDlgItemText():设置编辑框中的内容,例如:
SetDlgItemText(hDlg,IDC_EDIT2,wel);
表示将hDlg中编辑框IDC_EDIT2中的内容设置为字符数组变量wel的值。
因此,3.1.2节中处理单击确定按钮消息的代码如下:
char user[50]; //声明字符数组变量用于保存编辑框的内容
char wel[60]="欢迎您:";
if (LOWORD(wParam) == IDC_QD) { //处理单击确定按钮的消息
GetDlgItemText(hDlg,IDC_EDIT1,user,50); //获取IDC_EDIT1中的内容
strcat(wel,user); //连接两个字符串,连接后的值保存在wel中
SetDlgItemText(hDlg,IDC_EDIT2,wel); //设置IDC_EDIT2的内容
}
图3-11 本实例效果
3.2 Windows API程序示例
3.2.1 计算器程序
图3-12是一个计算器程序,制作方法是:首先按照3.1.1节新建对话框程序的方法新建该工程,然后绘制对话框界面,该对话框有3个编辑框,1个组合框和2个按钮,各个控件的ID如图3-12所示。
图3-12 计算器程序界面
在绘制组合框时,应注意先单击组合框右侧的下三角箭头,此时虚线框会增大(如图3-13所示),表示下拉列表的显示范围,将下方的控制点往下拖动可保证下拉菜单能完全显示出来。
图3-13 调整组合框的下拉列表范围
1. 实现仅有加法功能的计算器
计算器程序的编程步骤是:当单击计算按钮时,先获取IDC_EDIT1和IDC_EDIT2的内容,分别保存到两个变量中,然后对两个变量值进行相加运算,再把IDC_EDIT3的值设置为计算结果值。代码如下:
if (LOWORD(wParam) == IDC_CALC) { //如果单击了计算按钮
int nLeft=GetDlgItemInt(hDlg,IDC_LEFT,NULL,TRUE); //获取IDC_EDIT1的值
int nRight=GetDlgItemInt(hDlg,IDC_RIGHT,NULL,TRUE); //获取IDC_EDIT2的值
SetDlgItemInt(hDlg,IDC_RESULT,nLeft+nRight,TRUE); //设置IDC_EDIT3的值
}
本例中,获取和设置控件的内容使用了两个新的Win32 API函数:
GetDlgItemInt()和SetDlgItemInt(),它们能将获取的值自动转换为整型。
2. 扩充计算器的功能
为了实现具有加减乘除功能的计算器,可以在对话框中添加一个组合框IDC_COMBO1,在对话框初始化时对IDC_COMBO1进行初始化,也就是将四个运算符号添加到它的列表项中,为组合框添加列表项的方法如下:
SendMessage(GetDlgItem(hDlg, IDC_COMBO1), CB_ADDSTRING, 0, (LPARAM)fh[i]);
其中,第1个参数表示获取hDlg中的IDC_COMBO1控件,第2个参数表示为组合框添加列表项,第4个参数是要添加的内容。
当用户单击计算按钮时,必须获取用户在组合框中选择的符号,获取组合框的选中项的方法如下:
int cursel=SendDlgItemMessage(hDlg,IDC_COMBO1,CB_GETCURSEL,0,0);
其中,第3个参数CB_GETCURSEL表示获取组合框的选中项,CB_表示ComboBox。
下面是具有加减乘除功能和小数计算功能的计算器的完整代码。
#include "stdafx.h"
#include "resource.h"
#include <stdio.h> //sprintf函数需要这个头文件
HINSTANCE hInst;
LRESULT CALLBACK About(HWND, UINT, WPARAM, LPARAM);
int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
LPSTR lpCmdLine, int nCmdShow){
DialogBox(hInst, (LPCTSTR)IDD_ABOUTBOX, NULL, (DLGPROC)About);
return 0;
}
bool isnum(char *str1) { //函数功能:判断输入是否为数字
int i, flag=1;
for(i=0;i<strlen(str1);i++) {
if ( (str1[i]<'0' || str1[i]>'9')&&str1[i]!='.'&&str1[i]!='.') //如果str1中有非数字
{ flag=0; return FALSE; }
}
return TRUE; // 否则返回TRUE
}
LRESULT CALLBACK About(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam){
char * fh[4]={"+","-","*","/"}; //声明字符串数组
int i;
switch (message) { //选择处理感兴趣的消息
case WM_INITDIALOG: //对话框初始化消息
for (i=0; i<4; i++) { //向组合框中添加运算符
SendMessage(GetDlgItem(hDlg, IDC_COMBO1), CB_ADDSTRING, 0, (LPARAM)fh[i]);
}
return TRUE;
case WM_COMMAND:
if (LOWORD(wParam) == IDOK) { //单击了退出按钮
EndDialog(hDlg, LOWORD(wParam));
}
if (LOWORD(wParam) == IDC_CALC) //单击了计算按钮
{
TCHAR str1[256],str2[256];
GetDlgItemText(hDlg,IDC_EDIT1,str1,sizeof(str1)); //得到输入的字符串
GetDlgItemText(hDlg,IDC_EDIT2,str2,sizeof(str2));
if (isnum(str1) && isnum(str2)) { //如果获得的字符串是数值
double i1=atof(str1); //将字符串转换成浮点数
double i2=atof(str2);
double i3; //用于保存运算结果
//获取下拉框当前选中项
int cursel=SendDlgItemMessage(hDlg,IDC_COMBO1,CB_GETCURSEL,0,0);
switch(cursel) {
case 3: i3=i1+i2; break; //选中项是+号
case 2: i3=i1/i2; break;
case 1: i3=i1*i2; break;
case 0: i3=i1-i2; break;
}
TCHAR str3[256];
sprintf(str3,"%8.3lf",i3); //将浮点数i3转换为字符串
SetDlgItemText(hDlg,IDC_EDIT3,str3); //设置IDC_EDIT3的内容
}
else MessageBox(hDlg,"请输入数字","输入非法",MB_OK); }
break; }
return FALSE; }
提示:
程序中sprintf()函数常用于将数值型数据转换为字符串,也可用于多个字符串的连接:
sprintf(sbuff,"%s:%d说:%s", strip,strport,buff);
而strcat()函数只能用于2个字符串的连接,例如:strcat(strip,buff);。
3.2.2 获取主机名、IP和时间的程序
在WinSock中,用gethostname()函数可获取本机的主机名,而用gethostbyname()函数可以通过主机名获取本机的IP地址。利用这两个函数,将获取的主机名、IP和时间显示在静态文本框上,就能制作出如图3-14所示的程序。
具体步骤是:首先按照3.1.1节新建对话框程序的方法新建一个Win32 Application工程,然后绘制如图3-14对话框界面,添加6个静态文本,左侧3个的标题分别为“主机名”、“IP”和“时间”。右侧3个从上到下设置ID值依次是IDC_HOST、IDC_SHOWIP和IDC_TIME。最后编写如下代码。
图3-14 程序界面
#include "stdafx.h"
#include "resource.h"
#include "winsock2.h"
#include "iostream.h"
#include "time.h" //时间函数需要的头文件
#include "stdio.h"
#pragma comment(lib,"ws2_32.lib")
HINSTANCE hInst;
LRESULT CALLBACK About(HWND, UINT, WPARAM, LPARAM);
int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
LPSTR lpCmdLine, int nCmdShow){
WSADATA wsaData;
WSAStartup(MAKEWORD(2,2), &wsaData); //初始化协议栈
DialogBox(hInst, (LPCTSTR)IDD_ABOUTBOX, NULL, (DLGPROC)About);
return 0;
}
LRESULT CALLBACK About(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam){
char hostname[100];
hostent *hst = NULL;
in_addr inaddr;
char *pp=NULL;
time_t tt = time(NULL); //这句返回的只是一个时间戳
tm* t= localtime(&tt); //返回当地格式的时间,保存在变量t中
char szTime[20];
switch (message) {
case WM_INITDIALOG:
gethostname(hostname,sizeof(hostname)); //获取主机名,保存在变量hostname中
hst =gethostbyname(hostname); //获取IP地址,保存在变量hst中
memcpy((char*)(&inaddr),hst->h_addr_list[0],hst->h_length);
pp=inet_ntoa(inaddr); //将IP地址格式转换为本机字节
sprintf(szTime,"%d-%02d-%02d %02d:%02d:%02d", t->tm_year + 1900,t->tm_mon + 1,t->tm_mday, t->tm_hour, t->tm_min, t->tm_sec); //设置时间格式
SetDlgItemText(hDlg,IDC_HOST, hostname); //显示主机名
SetDlgItemText(hDlg,IDC_SHOWIP, pp); //显示IP
SetDlgItemText(hDlg,IDC_TIME, szTime); //显示时间
return TRUE;
case WM_COMMAND:
if (LOWORD(wParam) == IDOK) { //退出按钮
EndDialog(hDlg, LOWORD(wParam)); //关闭对话框
return TRUE; }
break;
}
return FALSE; }
总结:要获取本机IP地址,第一步是使用gethostname()函数获取本机的名称,第二步是使用gethostbyname()函数,并将本机名称作为参数,该函数返回值是一个hostent结构体的指针,IP地址存放在该结构体中的数组h_addr_list的第0个数组元素中,因此第三步是使用memcpy()函数将第0个数组元素取出并复制到inaddr变量中,第四步是使用inet_ntoa()将IP地址由in_addr无符号长整型转换成点分十进制的表示形式。
3.3 Win32 API版本的TCP通信程序
在项目2.2中,已经实现了一个能互相发送消息的TCP通信程序,但由于是控制台程序,不仅界面不够友好,还存在客户端和服务器端每次只能发送一条消息给对方的不足。本节我们将控制台程序转换成Windows对话框界面的程序。读者主要掌握控制台程序转换成Windows对话框程序的方法。该程序服务器端和客户端的运行界面如图3-15所示。
图3-15 Win32 API版本的TCP通信程序(左图为服务器端,右图为客户端)
3.3.1 控制台程序改造成Windows程序的方法
在图3-15的服务器端程序中,主要涉及如下4个Windows消息:
① 窗口初始化消息(WM_INITDIALOG);
② “发送数据”按钮消息(WM_COMMAND中的IDC_SEND);
③ “接收数据”按钮消息(WM_COMMAND中的IDC_RECV);
④ “退出”按钮消息(WM_COMMAND中的IDC_QUIT)。
如果要转换成Windows程序,控制台版TCP通信程序中各个函数对应的语句应分别放到上述4个消息的处理函数中。如图3-16所示。
socket() |
bind() |
accept() |
获取编辑框中的内容 |
send()
|
listen() |
WM_INITDIALOG |
IDC_SEND |
清空编辑框中的内容 |
将已发送的消息添加到列表框中 |
recv()
|
IDC_RECV |
将接收到的消息添加到列表框中 |
WSAStartup() |
设置编辑框初始显示的内容 |
closesocket ()
|
IDC_QUIT |
EndDialog() |
WSACleanup() ()
|
图3-16 TCP通信程序的WinSock函数调用流程
另外,控制台程序中只有一个main函数,而改造成Windows界面程序后,存在多个函数,这些函数都需要使用Socket及SOCKADDR_IN等套接字和地址变量,因此应该将套接字和套接字地址的声明写在所有函数之外,使它们成为公共变量,让所有的函数都可使用。
一般来说,编写Windows对话框程序的方法如下:
① 将公共变量的声明(包括声明套接字和套接字地址)写在所有函数之外,使所有函数都可以使用这些公共变量。
② 将初始化WinSock协议栈的代码写到WM_INITDIALOG初始化事件中。
③ 将创建套接字、以及监听、接受连接的代码写到“创建服务器”的按钮消息中。
④ 将获取用户消息、发送消息的代码写在“发送”按钮的消息处理函数中。
⑤ 将接收消息、并显示消息的代码写在“接收”按钮的消息处理函数中。
⑥ 将关闭套接字的代码写到“退出”按钮或WM_QUIT的消息处理函数中。
3.3.2 服务器端程序制作步骤
1)新建工程,选择“Win32 Application”,输入工程名(如WinApi_Server),然后选择“一个典型的Hello World程序”。
2)在左侧选择FileView选项卡,找到对应的源文件(如WinApi_Server.cpp),将WinMain函数中DialogBox一行(148行)和return 0; 保留,其他代码全部删除,再将DialogBox中第3个参数HWnd改为NULL。
3)切换到ResourceView选项卡,找到Dialog下的“IDD_ABOUTBOX”,将对话框的界面改为如图3-17所示,并设置各个控件的ID值。
图3-17 服务器端程序界面设计和控件ID值
提示:对于ListBox列表框控件,在其属性面板“样式”选项卡中,“分类”应取消勾选,可保证添加的消息从上到下按顺序显示。
4)打开WinApi_Server.cpp文件,编写如下代码:
#include "stdafx.h"
#include "resource.h"
#include<WINSOCK2.h>
#pragma comment(lib,"ws2_32.lib")
HINSTANCE hInst;
LRESULT CALLBACK About(HWND, UINT, WPARAM, LPARAM);
SOCKET sockSer,sockConn; //服务器端要创建两个套接字
SOCKADDR_IN addrSer, addrCli;
int iIndex=0; int len =sizeof(SOCKADDR);
char sendbuf[256],recvbuf[256];
char clibuf[999]="客户端: >",serbuf[999]="服务器: >";
int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
LPSTR lpCmdLine, int nCmdShow){
DialogBox(hInst, (LPCTSTR)IDD_ABOUTBOX, NULL, (DLGPROC)About);
return 0;
}
LRESULT CALLBACK About(HWND hDlg, UINT message, WPARAM wParam,
LPARAM lParam){
char ip[16],port[5];
switch (message) {
case WM_INITDIALOG: //对话框初始化
SetDlgItemText(hDlg,IDC_IP,"127.0.0.1"); //设置ip文本框的内容
SetDlgItemText(hDlg,IDC_PORT,"5566"); //设置端口文本框的内容
WSADATA wsaData;
if(WSAStartup(MAKEWORD(2,2), &wsaData)) {
MessageBox(hDlg,"Winsock加载失败","警告",0);
WSACleanup();
return -1; }
sockSer=socket(AF_INET,SOCK_STREAM,0);
return TRUE;
case WM_COMMAND:{
switch(LOWORD(wParam) )
{ case IDC_QUIT: //单击了退出按钮
EndDialog(hDlg, LOWORD(wParam));
closesocket(sockSer); //关闭套接字
WSACleanup(); //卸载WinSock协议栈
return TRUE;
case IDC_CREATE: //单击了创建服务器按钮
GetDlgItemText(hDlg,IDC_IP,ip,16); //获取编辑框中的IP值
GetDlgItemText(hDlg,IDC_PORT,port,5);
EnableWindow(GetDlgItem(hDlg,IDC_CREATE),FALSE); //禁用按钮
addrSer.sin_family=AF_INET;
addrSer.sin_port=htons(atoi(port));
addrSer.sin_addr.S_un.S_addr=inet_addr(ip);
bind(sockSer,(SOCKADDR*) &addrSer,len); //绑定套接字
listen(sockSer,5); //监听
sockConn=accept(sockSer,(SOCKADDR*) &addrCli,&len);//接受连接
if(sockConn!=INVALID_SOCKET)
SendDlgItemMessage(hDlg,IDC_RECVBUF,LB_ADDSTRING,0,
(LPARAM)"客户端已经连接上");
break;
case IDC_SEND: //单击了发送按钮
GetDlgItemText(hDlg,IDC_SENDBUF,sendbuf,256);
send(sockConn,sendbuf,strlen(sendbuf)+1,0); //发送消息
SetDlgItemText(hDlg,IDC_SENDBUF,""); //清空编辑框
strcat(serbuf,sendbuf);
//将已发送的消息添加到列表框中
SendDlgItemMessage(hDlg,IDC_RECVBUF,LB_ADDSTRING,0,(LPARAM)serbuf);
strcpy(serbuf, "服务器: >");
break;
case IDC_RECV: //单击了接收按钮
recv(sockConn,recvbuf,256,0);
strcat(clibuf,recvbuf);
//将接收到的消息添加到列表框中
SendDlgItemMessage(hDlg,IDC_RECVBUF,LB_ADDSTRING,0,(LPARAM)clibuf);
strcpy(clibuf, "客户端: >");
break;
} } }
return FALSE; }
提示:向列表框中添加内容,需要使用的函数如下:
SendDlgItemMessage(hDlg,IDC_RECVBUF,LB_ADDSTRING,0,(LPARAM)clibuf);
其中,第1个参数是对话框的句柄,第2个参数是列表框的ID,第3个参数是控件消息名,其中LB_表示Listbox,ADDSTRING表示添加字符串,第5个参数是要添加的内容。
LB_SETTOPINDEX的作用是,当列表框中的内容很多显示不下时,该消息能让列表框自动滚动使当前项能显示出来。
3.3.3 客户端程序制作步骤
1)新建工程,选择“Win32 Application”,输入工程名(如Win_client),然后选择“一个典型的Hello World程序”。
2)在左侧选择FileView选项卡,找到对应的源文件(如Win_client.cpp),将WinMain函数中DialogBox一行和return 0; 保留,其他代码全部删除,再将DialogBox中第3个参数HWnd改为NULL。
3)切换到ResourceView选项卡,找到Dialog下的“IDD_ABOUTBOX”,将对话框的界面改为如图3-18所示,并设置各个控件的ID值。
图3-18 客户端程序界面设计和控件ID值
4)打开Win_client.cpp文件,编写代码如下:
#include "stdafx.h"
#include "resource.h"
#include<WINSOCK2.h>
#pragma comment(lib,"ws2_32.lib")
HINSTANCE hInst;
LRESULT CALLBACK About(HWND, UINT, WPARAM, LPARAM);
SOCKET sockCli; //声明客户端套接字
SOCKADDR_IN addrSer, addrCli;
int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
LPSTR lpCmdLine, int nCmdShow){
DialogBox(hInst, (LPCTSTR)IDD_ABOUTBOX, NULL, (DLGPROC)About);
return 0;
}
LRESULT CALLBACK About(HWND hDlg, UINT message, WPARAM wParam,
LPARAM lParam){
char ip[16], port[5];
char sendbuf[256],recvbuf[256];
char serbuf[256]="服务器: >", clibuf[256]="客户端: >";
int len =sizeof(SOCKADDR);
switch (message){
case WM_INITDIALOG: //对话框初始化
SetDlgItemText(hDlg,IDC_IP,"127.0.0.1");
SetDlgItemText(hDlg,IDC_PORT,"5566");
WSADATA wsaData;
WSAStartup(MAKEWORD(2,2), &wsaData); //加载协议栈
sockCli=socket(AF_INET,SOCK_STREAM,0); //创建套接字
break;
case WM_COMMAND:
{ switch(LOWORD(wParam)) {
case IDC_QUIT: //单击了退出按钮
EndDialog(hDlg, LOWORD(wParam));
closesocket(sockCli);
WSACleanup();
break;
case IDC_CONN: //单击了“连接服务器”按钮
GetDlgItemText(hDlg,IDC_IP,ip,16); //获取ip
GetDlgItemText(hDlg,IDC_PORT,port,5);
EnableWindow(GetDlgItem(hDlg,IDC_CONN),FALSE); //禁用连接按钮
addrSer.sin_family=AF_INET;
addrSer.sin_port=htons(atoi(port));
addrSer.sin_addr.S_un.S_addr=inet_addr(ip);
if(connect(sockCli,(SOCKADDR*)&addrSer,sizeof(SOCKADDR)))
MessageBox(NULL,"客户端连接服务器失败","警告",0);
else
MessageBox(NULL,"客户端连接服务器成功","通知",0);
break;
case IDC_SEND: //单击了“发送”按钮
GetDlgItemText(hDlg,IDC_SENDBUF,sendbuf,256); //获得发送框的内容
send(sockCli,sendbuf,strlen(sendbuf)+1,0); //发送消息
SetDlgItemText(hDlg,IDC_SENDBUF,""); //清空发送框
strcat(clibuf,sendbuf);
SendDlgItemMessage(hDlg,IDC_RECVBUF,LB_ADDSTRING,0,(LPARAM)clibuf);
strcpy(clibuf, "客户端: >"); //重新给字符串赋值
break;
case IDC_RECV: //单击了“接收”按钮
recv(sockCli,recvbuf,256,0); //接收消息
strcat(serbuf,recvbuf);
SendDlgItemMessage(hDlg,IDC_RECVBUF,LB_ADDSTRING,0,(LPARAM)serbuf);
strcpy(serbuf, "服务器: >"); //重新给字符串赋值
break; }
} }
return FALSE; }
习题
1. 每个按钮的ID属性存放在WM_COMMAND消息的 中。 ( )
A. wParam的高位字节中 B. wParam的低位字节中
C. lParam的高位字节中 D. lParam的低位字节中
2.下列哪个函数可以返回本机的IP地址: ( )
A. gethostbyname() B. gethostname()
C. gethostbyaddr() D. gethostaddr()
3. WM_COMMAND表示 ,WM_INITDIALOG表示 。
4. 要设置编辑框显示的内容,应使用 函数。
5. 要获取编辑框显示的内容,应使用 函数。
6. 要向列表框中添加一行文本,应使用 函数。
7. 在Windows API程序中,对话框是通过 函数创建的。
8. 在VC6中,怎样保证向列表框Listbox中添加消息时,消息总是从上到下按时间顺序显示?
9. (实验)为3.3节的程序添加显示消息发送时间的功能。