QQ斗地主记牌器开发实践

这是我2005年写的一个小程序,原来放在BOKEE上的,最近BOKEE经常打不开,打开的时候也是极慢、图片显不出来,中国最早的博客要关门了?为了不把程序弄丢了,还是转移到这里来吧。

原文:

由于前段时间玩起了QQ斗地主,从网上下载了一个QQ斗地主记牌器,但无法使用,于是就想自己开发一个。

一、记牌器实现的原理

当时查阅了一些资料,想到了三种实现的方法:

1、拦截IP包。这种方法应该是可行的,只是从查阅的资料来看,现在的游戏IP包都经过加密处理了,而且分析IP包的工作量比较大,所以就没采用这种方法。这方面使用的软件有:WPE(拦截发送封包),m2m封包分析。

2、拦截QQ斗地主显示扑克牌的函数。用W32DSM分析了斗地主的几个相关exe和dll竟然没找到显示扑克牌的函数:(。一开始觉得最可疑的应该是QQGame目录下的CardRes.dll,但分析后发现它竟没有程序出口。

3、屏幕抓图。感觉这种方法是最简单的了,觉得纯粹就是抓图,然后分析图形就行了。当时下载的记牌器应该也是应用这种方法实现的,但是它又不能用,网上又有评论说它被腾讯怎么给弄失效了。不管了,试试再说。

二、从图形分析开始

这是抓获的每张牌的区域,下图为放大后的效果,图中绿色网格为参考坐标,经过分析,发现只要使用12行X4列的区域就可以确定出牌的大小,使用20行X4列的区域就可以确定出大小和类型了。刚开始的时候,打算每次捕抓一张牌的区域,但是这样要想定的定义一系列的HBITMAP,不方便,所以就改为一列牌捕捉分析一次。考虑到一家的牌最多20张,而20张牌的屏幕区域为356X20像素。

分析牌大小和类型的函数如下:

//判断hdc中的牌
//////////////////////////////////////////////////////
BOOL CCard::Check(HDC hdc)
{
 BYTE g_color, color1, color2;
// int i=1;
 int nCardWidth=15; //每张牌的宽度为15像素
 int nOffset = 2;    //牌边框与值之类的像素,见图片说明
 for(int i=0; i<20; i++)
 {
 if(GetColor(hdc, 13, 1+i*nCardWidth)!= 0)  //找不到黑色的边框,说明没有牌
  continue;
 color1=GetColor(hdc, 1, 68+i*nCardWidth);
 if(color1 >20 && color1 <150)     //表示为底色,此值为随便取的,视底色范围
  continue;

 if(GetColor(hdc, 1, 1+nOffset+i*nCardWidth) ==0)
 {
  value[i] = 13;   //K
  continue;
 }

 if(GetColor(hdc, 1,2+nOffset+i*nCardWidth) ==0 )
  {
   if((color1=GetColor(hdc,7,2+nOffset+i*nCardWidth))==0)
   {
    value[i] = 10;  //10
    continue;
   }
   value[i] = 5;   //5
   continue;
  }

 if((g_color=GetColor(hdc, 2,2+nOffset+i*nCardWidth)) ==0)
 {
  value[i] = 12;    //Q
  continue;
 }

 if(GetColor(hdc, 12, 2+nOffset+i*nCardWidth) == 0)
 {
  value[i] = 1;    //A
  continue;
 }

 if(GetColor(hdc, 12, 1+nOffset+i*nCardWidth) == 0)
 {
  value[i] = 1;    //A
  continue;
 }

 if(GetColor(hdc, 6, 2+nOffset+i*nCardWidth) == 0)
 {
  value[i] = 4;    //4
  continue;
 }

 if(GetColor(hdc, 9, 2+nOffset+i*nCardWidth)== 0)
 {
  value[i] =11;    //J
  continue;
 }

 if((g_color=GetColor(hdc, 1, 3+nOffset+i*nCardWidth)) == 0)
 {
  if((color1=GetColor(hdc, 4,3+nOffset+i*nCardWidth)) ==0)
  {
   value[i] = 5;   //5
   continue;
  }
  if((color2=GetColor(hdc, 10, 3+nOffset+i*nCardWidth)) == 0)
  {
   value[i] = 3;   //3
   continue;
  }

  value[i] =7;    //7
  continue;
 }

 if(GetColor(hdc, 12, 3+nOffset+i*nCardWidth) ==0)
 {
  value[i] = 2;    //2
  continue;
 }

 if((color1=GetColor(hdc, 2, 3+nOffset+i*nCardWidth)) == 0)
 {
  if((GetColor(hdc, 6, 3+nOffset+i*nCardWidth)) ==0)
  {
   value[i] = 9;   //9
   continue;
  }
  value[i] = 8;    //8
  continue;
 }


 if(GetColor(hdc, 3, 3+nOffset+i*nCardWidth)==0)
 {
  value[i] = 6;    //6
  continue;
 }

 if(GetColor(hdc,1,1+nOffset+i*nCardWidth) == 255)
 {
  value[i] = 0;    //大小王
  continue;
 }

 value[i] = 14;
 }
 

 //判断花色
 ////////////////////////////////////////////////////////////////////
 for(i=0; i<20; i++)
 {
  if(value[i] == 14)   //无牌 
  {
   type[i]=4;
   continue;
  }

  if(value[i] == 0)   //王
  {
   type[i]=0;    //先默认为大王
   if(GetColor(hdc, 8, 4+nOffset+i*nCardWidth) < 250)
    type[i]=1;   //小王
   continue;
  }

  if(value[i]>0 && value[i]<11)  //牌为A、2-10时,花色的区域相同,为一种
  {
   if(GetColor(hdc, 20, 2+nOffset+i*nCardWidth) == 255) //方片A
   {
    type[i]=3;
    continue;
   }
   if(GetColor(hdc, 17, 2+nOffset+i*nCardWidth) == 0) //红桃
   {
    type[i]=1;
    continue;
   }
   if(GetColor(hdc, 18, 2+nOffset+i*nCardWidth) == 0) //方片
   {
    type[i]=3;
    continue;
   }
   if(GetColor(hdc, 18, 4+nOffset+i*nCardWidth) == 0) //黑桃
   {
    type[i]=0;
    continue;
   }
   type[i]=2;         //茶花
   continue;
  }

  if(value[i]>=11 && value[i]<=13)  //牌值为J-K时,花色区域为另一种
  {
   if(GetColor(hdc, 17, 1+nOffset+i*nCardWidth) == 0) //红桃
   {
    type[i]=1;
    continue;
   }
   if(GetColor(hdc, 19, 1+nOffset+i*nCardWidth) == 255)//方片
   {
    type[i]=3;
    continue;
   }
   if(GetColor(hdc, 16, 3+nOffset+i*nCardWidth) == 0) //黑桃
   {
    type[i]=2;
    continue;
   }
   type[i]=0;         //茶花
   continue;
  }
 }

 return TRUE;
}


//取得第row行,col列的G值,注意参数为先行后列,而且是1开始而非从0开始的
//原先是为了计算单张牌的方便,才采用了行列而非坐标,以至后来调试多花了好多时间^-^(是忘了这个规则了)
BYTE CCard::GetColor(HDC hdc, int row, int col)
{
 COLORREF crColor;
 BYTE temp;
 crColor = GetPixel(hdc, col-1, row-1);  //取得第row行,col列的RGB值

 //分离出蓝色值,为什么用蓝色呢,看看抓图的像素就知道了
 //红色的G值为0,黑色的G值为0,而白色的G值为255,所以判断一下G值就知道是什么牌了
 temp=GetGValue(crColor);     //分离出蓝色值
 return temp;
}


上图为各捕捉区域左上角的屏幕坐标,自己、左家、右家的牌一次捕捉356X20的区域,进行一次分析。底牌区域的坐标仅用于参考,视算法而定要不要进行分析(我的程序因为是旁观别人的牌进行分析的,自己的牌是一次分析出来的,后来自己玩的时候发现了一个问题,自己当地主时,底牌拿到手时跟其它的牌不是成一条线的!所以还要用到底牌区的区域来分析)。

三、屏幕抓图函数

可参见我的博客中其它的文章。我的思路是自己的牌抓一次,左右家的牌两家一起抓一次。因为左右家出的牌较多时,两家的牌会有一部分重叠在一起,但是经过分析,可以知道这两部分每张牌之间都是错开的,所以还是可以一次处理的。

另:使用spy++分析QQ斗地主的各个窗口坐标,可知:主窗口的左上角坐标为(-4,-4),而牌区的左上角坐标为(1,30),牌区的长宽为736X696像素。

我用的函数如下:

void CCaptureDlg::OnMiddle()
{
 // TODO: Add your control notification handler code here

 CCard cardLord;

 int nLordX=212, nLordY=554;
/*
 HDC hdcScreen, hMemDC;

 HBITMAP hBitmap, hOldBitmap;
 HBITMAP hBitmap1, hOldBitmap1;
 //建立一个屏幕设备环境句柄
 hdcScreen = CreateDC("DISPLAY", NULL, NULL, NULL);
 hMemDC = CreateCompatibleDC(hdcScreen);
 hMiddleDC = CreateCompatibleDC(hdcScreen);

*/ //建立一个与屏幕设备环境句柄兼容、与鼠标所在处的窗口的区域等大的位图
 hBitmap = CreateCompatibleBitmap(hdcScreen, nWidth, nHeight);
 hBitmap1 = CreateCompatibleBitmap(hdcScreen, 356, 20);
 // 把屏幕设备描述表拷贝到内存设备描述表中
 hOldBitmap = (HBITMAP) SelectObject(hMemDC, hBitmap);
 
 hOldBitmap1 = (HBITMAP) SelectObject(hMiddleDC, hBitmap1);

 BitBlt(hMemDC, 0, 0, nWidth, nHeight, hdcScreen, rectCapture.left+5, rectCapture.top+34,SRCCOPY);

 if(cardLord.GetColor(hMemDC, nLordY, nLordX-1) ==255)
  nLordX -= 22;

 BitBlt(hMiddleDC, 0, 0, 356,20, hMemDC, nLordX, nLordY, SRCCOPY);

 cardLord.Check(hMiddleDC);
 
/* CString str1, str2, str3;
 str2.Empty();
 str2.Format(_T("自己的牌:\n"));

 for(int i=0; i<20; i++)
 {
  str1.Format(_T("%4d"), cardLord.GetValue(i));
  str2 += str1;
  str1.Format(_T("%4d"), cardLord.GetType(i));
  str3 += str1;
 }
 str2+="\n";
 str2+=str3;
 AfxMessageBox(str2);
*/
 hBitmap =(HBITMAP)SelectObject(hMemDC, hOldBitmap);
 hBitmap1 =(HBITMAP)SelectObject(hMiddleDC, hOldBitmap1);

 /*
 DeleteDC(hMiddleDC);
 DeleteDC(hdcScreen);
 DeleteDC(hMemDC);

 // 返回位图句柄
 //打开剪贴板,并将位图拷到剪贴板上
 OpenClipboard() ;
 EmptyClipboard();
 SetClipboardData(CF_BITMAP, hBitmap1);
 //关闭剪贴板
 CloseClipboard();

*/
// MessageBox("屏幕内容已经拷到剪贴板上!");
 //终止鼠标捕获
 ReleaseCapture();
 //恢复窗口显示模式
// ShowWindow(SW_NORMAL);

 DeleteItem(cardLord);
}

void CCaptureDlg::OnLeft()
{
 // TODO: Add your control notification handler code here
// KillTimer(1);

 CCard cardLeft, cardRight;

 int nLeftX=172, nLeftY=272;
 int nRightX=208, nRightY=272;

/* HDC hdcScreen, hMemDC;
 HBITMAP hBitmap, hOldBitmap;
 HBITMAP hBitmap1, hOldBitmap1;
 HBITMAP hBitmap2, hOldBitmap2;

 //建立一个屏幕设备环境句柄
 hdcScreen = CreateDC("DISPLAY", NULL, NULL, NULL);
 hMemDC = CreateCompatibleDC(hdcScreen);
 hLeftDC = CreateCompatibleDC(hdcScreen);
 hRightDC = CreateCompatibleDC(hdcScreen);
*/
 //建立一个与屏幕设备环境句柄兼容、与鼠标所在处的窗口的区域等大的位图
 hBitmap = CreateCompatibleBitmap(hdcScreen, nWidth, nHeight);
 hBitmap1 = CreateCompatibleBitmap(hdcScreen, 356, 20);
 hBitmap2 = CreateCompatibleBitmap(hdcScreen, 356, 20);
 // 把屏幕设备描述表拷贝到内存设备描述表中
 hOldBitmap = (HBITMAP) SelectObject(hMemDC, hBitmap);
 hOldBitmap1 = (HBITMAP) SelectObject(hLeftDC, hBitmap1);
 hOldBitmap2 = (HBITMAP) SelectObject(hRightDC, hBitmap2);

 BitBlt(hMemDC, 0, 0, nWidth, nHeight, hdcScreen, rectCapture.left+5, rectCapture.top+34,SRCCOPY);

 BitBlt(hLeftDC, 0, 0, 356,20, hMemDC, nLeftX, nLeftY, SRCCOPY);
 BitBlt(hRightDC, 0, 0, 356,20, hMemDC, nRightX, nRightY, SRCCOPY);

 cardLeft.Check(hLeftDC);
 cardRight.Check(hRightDC);

 DeleteItem(cardLeft);
 DeleteItem(cardRight);
/*
 DeleteDC(hdcScreen);
 DeleteDC(hMemDC);
 DeleteDC(hLeftDC);
 DeleteDC(hRightDC);

*/
 hBitmap =(HBITMAP)SelectObject(hMemDC, hOldBitmap);
 hBitmap1 =(HBITMAP)SelectObject(hLeftDC, hOldBitmap1);
 hBitmap2 =(HBITMAP)SelectObject(hRightDC, hOldBitmap2);

// SetTimer(1, 2000, NULL);
}


四、主程序

主程序采用4个列表来记牌,为什么不用一个列表来实现呢?因为列表只能在第一列能够同时显示图标和文字,所以用了4个列表。下方的5个按钮是用来测试各个函数的,调试用的。代码如下:

BOOL CCaptureDlg::OnInitDialog()
{

CDialog::OnInitDialog();

 // Add "About..." menu item to system menu.

 // IDM_ABOUTBOX must be in the system command range.
 ASSERT((IDM_ABOUTBOX & 0xFFF0) == IDM_ABOUTBOX);
 ASSERT(IDM_ABOUTBOX < 0xF000);

 CMenu* pSysMenu = GetSystemMenu(FALSE);
 if (pSysMenu != NULL)
 {
  CString strAboutMenu;
  strAboutMenu.LoadString(IDS_ABOUTBOX);
  if (!strAboutMenu.IsEmpty())
  {
   pSysMenu->AppendMenu(MF_SEPARATOR);
   pSysMenu->AppendMenu(MF_STRING, IDM_ABOUTBOX, strAboutMenu);
  }
 }

 //加入WS_EX_LAYERED扩展属性
 //设置窗口为半透明
 SetWindowLong(this->GetSafeHwnd(),GWL_EXSTYLE,
 GetWindowLong(this->GetSafeHwnd(),GWL_EXSTYLE)^0x80000);
 HINSTANCE hInst = LoadLibrary("User32.DLL");
 if(hInst)
 {
  typedef BOOL (WINAPI *MYFUNC)(HWND,COLORREF,BYTE,DWORD);
  MYFUNC fun = NULL;
  //取得SetLayeredWindowAttributes函数指针
  fun=(MYFUNC)GetProcAddress(hInst, "SetLayeredWindowAttributes");
  if(fun)fun(this->GetSafeHwnd(),0,255,2);
  FreeLibrary(hInst);
 }

  //将窗口设为最前

::SetWindowPos(this->m_hWnd,HWND_TOPMOST,815,395,0,0,SWP_NOSIZE);

 // return TRUE  unless you set the focus to a control

 // Set the icon for this dialog.  The framework does this automatically
 //  when the application's main window is not a dialog
 SetIcon(m_hIcon, TRUE);   // Set big icon
 SetIcon(m_hIcon, FALSE);  // Set small icon
 
 // TODO: Add extra initialization here
 bCapture=FALSE;
 bLord = FALSE;

 bMiddle = FALSE;
 bLeft = FALSE;
 bRight = FALSE;
 bMiddle1 = FALSE;
 bLeft1 = FALSE;
 bRight1 = FALSE;
 
 hwndCapture = ::FindWindow(NULL, "斗地主");
 
 ::GetWindowRect(hwndCapture,&rectCapture);

 nWidth=rectCapture.Width();
 nHeight=rectCapture.Height();
 if(nWidth != 1032 || nHeight != 746)
 {
  MessageBox("斗地主的主窗口尺寸不对!\n\n检查显示器分辨率:请调整为1024*768\n并将斗地主窗口最大化\n\n不便之处,敬请原谅!");
  exit(0);
 }


 InitList();

 //建立一个屏幕设备环境句柄
 hdcScreen = CreateDC("DISPLAY", NULL, NULL, NULL);
 hMemDC = CreateCompatibleDC(hdcScreen);
 hMiddleDC = CreateCompatibleDC(hdcScreen);
 hLeftDC = CreateCompatibleDC(hdcScreen);
 hRightDC = CreateCompatibleDC(hdcScreen);

 SetTimer(1,2000,NULL);

 return TRUE;  // return TRUE  unless you set the focus to a control
}

//初始化4个列表

////////////////////////////////////////////////////////////////////////////////////////////////
void CCaptureDlg::InitList()
{
 int i,j;
 char str1[15][5]={"3","4","5","6","7","8","9","10","J","Q","K","A","2","王","无牌"};

 //载入4种花色的图标
 SmallImage.Create(14, 14, ILC_COLOR8 | ILC_MASK, 4, 1);
 CBitmap cBmp;
 cBmp.LoadBitmap(IDB_BITMAP1);
 SmallImage.Add(&cBmp, RGB(255,255,255));
 cBmp.DeleteObject();
 cBmp.LoadBitmap(IDB_BITMAP2);
 SmallImage.Add(&cBmp, RGB(255,255,255));
 cBmp.DeleteObject();
 cBmp.LoadBitmap(IDB_BITMAP3);
 SmallImage.Add(&cBmp, RGB(255,255,255));
 cBmp.DeleteObject();
 cBmp.LoadBitmap(IDB_BITMAP4);
 SmallImage.Add(&cBmp, RGB(255,255,255));
 cBmp.DeleteObject();

 //初始化4个列表
 for(i=0; i<4; i++)
 {
  m_list[i].SetImageList(&SmallImage, LVSIL_SMALL);
  m_list[i].InsertColumn(0,"");
  m_list[i].SetColumnWidth(0,50);
 }

 LVITEM lvi;
 CString strItem;

 for(j=0; j<4; j++)
  for(i=0;i<13;i++)
  {
   lvi.mask = LVIF_IMAGE|LVIF_TEXT;
   lvi.iItem = i;
   lvi.iSubItem = 0;
   lvi.iImage=j;
   strItem.Format(_T("%s"),str1[i]);
   lvi.pszText = (LPTSTR)(LPCTSTR)(strItem);
   m_list[j].InsertItem(&lvi);
  }

 //记下大小王
 iconJoke1 = m_joke1.GetIcon();
 iconJoke2 = m_joke2.GetIcon();

 bMiddle1 = TRUE;
}


五、存在的问题

使用主界面下方的按钮来测试各个函数,可以正常捕捉记牌,但是使用OnTimer()来自动记牌时,记一段时间就出错了,是什么问题呢?

该采用什么算法来激活捕捉分析程序呢?欢迎大家跟我探讨。


你可能感兴趣的:(VC,QQ斗地主,记牌器,qq,null,initialization,byte,fun,winapi)