这是我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()来自动记牌时,记一段时间就出错了,是什么问题呢?
该采用什么算法来激活捕捉分析程序呢?欢迎大家跟我探讨。