用Python實(shí)現(xiàn)QQ游戲大家來(lái)找茬輔助工具
好久沒(méi)寫(xiě)技術(shù)相關(guān)的文章,這次寫(xiě)篇有意思的,關(guān)于一個(gè)有意思的游戲——QQ找茬,關(guān)于一種有意思的語(yǔ)言——Python,關(guān)于一個(gè)有意思的庫(kù)——Qt。
這是一個(gè)用于QQ大家來(lái)找茬(美女找茬)的輔助外掛,開(kāi)發(fā)的原因是看到老爸天天在玩這個(gè)游戲,分?jǐn)?shù)是慘不忍睹的負(fù)4000多。他玩游戲有他的樂(lè)趣,并不很在意輸贏,我做這個(gè)也只是自我?jiàn)蕵?lè),順便討他個(gè)好,畢竟我們搞編程的實(shí)在難有機(jī)會(huì)在父輩面前露露手。本來(lái)是想寫(xiě)個(gè)很簡(jiǎn)單的東西,但由于過(guò)程中老爸的多次嘲諷,逼得我不得不盡力完善,最后形成了一個(gè)小小的產(chǎn)品。
接觸Python是2010年,相見(jiàn)恨晚,去年拿它寫(xiě)了些小玩意,離職前給前公司留下了一個(gè)Python+wxPython的工作工具,還挺受歡迎。換公司后努力學(xué)習(xí)C++&Qt,很后悔當(dāng)初選擇了wxPython而不是PyQt,沒(méi)能一脈相承。使用Qt越久,不得不越來(lái)越喜歡,寫(xiě)這個(gè)東西正好就用上了。
話不多說(shuō),進(jìn)入正題。這不是一篇完整的代碼講解,只是過(guò)程中的一些技術(shù)做個(gè)分享,包括后來(lái)被放棄的一些技術(shù)點(diǎn)。當(dāng)初搜索這些東西也挺費(fèi)力的,在這做個(gè)筆記,后來(lái)者也許能搜到收益。
先上個(gè)圖:

話說(shuō)這位是游戲中出鏡最多的MM,和QQ什么關(guān)系???
輔助工具在游戲中增加了兩個(gè)按鈕,點(diǎn)擊“對(duì)比”則自動(dòng)找“茬”,用藍(lán)色小框標(biāo)識(shí),點(diǎn)擊“擦除”清除標(biāo)識(shí)。
游戲窗口探查
這得用PyWin32庫(kù),它是對(duì)windows接口的Python封裝,VC能做的它基本都行。
下載地址:http://sourceforge.net/projects/pywin32/,但不能直接點(diǎn)Download圖標(biāo),不然下下來(lái)是一個(gè)Readme.txt,點(diǎn)“Browse All Files”尋找需要的版本。
#coding=gbk import win32gui
game_hwnd = win32gui.FindWindow("#32770", "大家來(lái)找茬") print game_hwnd
QQ找茬是個(gè)對(duì)話框窗口,Class是“#32770”,這種窗口桌面上有很多,所以還配合了標(biāo)題“大家來(lái)找茬”匹配,又因?yàn)槭侵形?,所以第一行指定了使用gbk編碼,否則要么找不到,要么運(yùn)行出錯(cuò)。
游戲圖片提取
提取圖片采用了截屏的方式,找到窗口后將窗口提到最前,再作窗口截屏。截屏使用了大名鼎鼎的Python Imaging Library (PIL)庫(kù)。
import ImageGrab import win32con win32gui.ShowWindow(game_hwnd, win32con.SW_RESTORE) # 強(qiáng)行顯示界面后才好截圖 win32gui.SetForegroundWindow(game_hwnd) # 將游戲窗口提到最前 # 裁剪得到全圖 game_rect = win32gui.GetWindowRect(game_hwnd) src_image = ImageGrab.grab((game_rect[0] + 9, game_rect[1] + 190, game_rect[2] - 9, game_rect[1] + 190 + 450)) # src_image.show() # 分別裁剪左右內(nèi)容圖片 left_box = (9, 0, 500, 450) right_box = (517, 0, 517 + 500, 450) image_left = src_image.crop(left_box) image_right = src_image.crop(right_box) # image_left.show() # image_right.show()
上面用到的坐標(biāo)都為為了演示代碼簡(jiǎn)單填的,實(shí)際上使用了變量參數(shù),而且要區(qū)分分辨率什么的。
PIL是一個(gè)強(qiáng)大的Python圖形庫(kù)(使用文檔),待會(huì)的對(duì)比分析也須要用到。ImageGrab是PIL的一個(gè)模塊,用于圖像的抓取。不帶參數(shù)的ImageGrab.grab()進(jìn)行全屏截屏,返回一個(gè)Image對(duì)象,也可使用一個(gè)元組作為參數(shù)指定要截取的范圍(左上與右下兩點(diǎn)的坐標(biāo)),這兩種截屏都是不帶鼠標(biāo)指針的,還有一個(gè)ImageGrab.grabclipboard()可從系統(tǒng)剪貼板采集圖像。
得到Image圖像后可用show()方法,使用系統(tǒng)默認(rèn)的圖像查看工具打開(kāi),方便調(diào)試,也可以用save(filename)保存成文件,對(duì)應(yīng)的可以Image.open(filename)打開(kāi)獲得。
grab得到了一個(gè)包含左右圖片的Image對(duì)象后,用crop(box)方法可裁剪得到其中指定的區(qū)域,分別拿到左右兩個(gè)游戲圖片。
對(duì)比獲得兩圖內(nèi)容不同的區(qū)域
很自然想到把兩圖裁剪成N個(gè)小圖片分別對(duì)比,左右統(tǒng)一區(qū)域?qū)?yīng)的小圖片不相等則為“茬”區(qū),唯一的問(wèn)題是怎么判斷兩個(gè)圖片內(nèi)容不一致?
一開(kāi)始以為很會(huì)有些麻煩,直到發(fā)現(xiàn)了Image.histogram()函數(shù),該函數(shù)用于得到圖像的顏色直方圖。我平常也愛(ài)好攝影,知道直方圖可以表示一張圖片中各種亮度(或顏色)的數(shù)量,兩張自然圖片的直方圖基本是不一樣的,除非兩圖對(duì)稱、顏色一致但排列不一,但就算如此,將兩圖繼續(xù)分割下去,其子圖的直方圖也會(huì)不一樣。直方圖就是一種圖形到數(shù)值的轉(zhuǎn)換,對(duì)比兩圖的顏色數(shù)值就可知是否存在差異。
一張用RBG顏色格式的圖像,histogram()函數(shù)將返回一個(gè)長(zhǎng)度為768的數(shù)組,第0-255表示紅色的0-255,第256-511表色綠色的0-255,第512-767表色藍(lán)色的0-255,數(shù)值表示該顏色像素的個(gè)數(shù)。因此,histogram()列表所有成員之和等于改圖像的像素值 x 3。
寫(xiě)了一個(gè)函數(shù),用來(lái)獲得兩圖比較的數(shù)值差:
ef compare(image_a, image_b): '''返回兩圖的差異值
返回兩圖紅綠藍(lán)差值萬(wàn)分比之和''' histogram_a = image_a.histogram()
histogram_b = image_b.histogram() if len(histogram_a) != 768 or len(histogram_b) != 768: return None
red_a = 0 red_b = 0 for i in xrange(0, 256):
red_a += histogram_a[i + 0] * i
red_b += histogram_b[i + 0] * i
diff_red = 0 if red_a + red_b > 0:
diff_red = abs(red_a - red_b) * 10000 / max(red_a, red_b)
green_a = 0 green_b = 0 for i in xrange(0, 256):
green_a += histogram_a[i + 256] * i
green_b += histogram_b[i + 256] * i
diff_green = 0 if green_a + green_b > 0:
diff_green = abs(green_a - green_b) * 10000 / max(green_a, green_b)
blue_a = 0 blue_b = 0 for i in xrange(0, 256):
blue_a += histogram_a[i + 512] * i
blue_b += histogram_b[i + 512] * i
diff_blue = 0 if blue_a + blue_b > 0:
diff_blue = abs(blue_a - blue_b) * 10000 / max(blue_a, blue_b)
return diff_red, diff_green, diff_blue
將函數(shù)返回的紅綠藍(lán)差值相加,如果超過(guò)了預(yù)定定的閥值2000,則表示該區(qū)域不同。這個(gè)計(jì)算方式有點(diǎn)“土”,但對(duì)這次要解決的問(wèn)題很有效,就沒(méi)再繼續(xù)改進(jìn)。
將左右大圖裁剪成多個(gè)小圖分別進(jìn)行對(duì)比 result = [[0 for a in xrange(0, 50)] for b in xrange(0, 45)] for col in xrange(0, 50):
for row in xrange(0, 45):
clip_box = (col * 10, row * 10, (col + 1) * 10, (row + 1) * 10)
clip_image_left = image_left.crop(clip_box)
clip_image_right = image_right.crop(clip_box)
clip_diff = self.compare(clip_image_left, clip_image_right)
if sum(clip_diff) > 2000:
result[row][col] = 1
大圖是500x450,分隔成10x10的小塊,定義一個(gè)50x45的二位數(shù)組存儲(chǔ)結(jié)果,分別比較后將差值大于閥值的數(shù)組區(qū)域標(biāo)記為1.
在游戲上標(biāo)記兩邊不同的區(qū)域
最初我用了PyWin32的一些函數(shù),獲得游戲窗口句柄后直接在上面繪制,但我不太熟悉Windows編程,不知道如何解決游戲自身重繪后將我的標(biāo)記擦除的問(wèn)題,然后搬來(lái)了Qt。用Qt創(chuàng)建了一個(gè)和游戲大小一樣透明的QWidget窗口,疊加在游戲窗口上,用遮罩來(lái)繪制標(biāo)記。標(biāo)記數(shù)據(jù)已記錄在result數(shù)組中,在指定的位置繪制一個(gè)方格則表示該區(qū)域左右不同,要注意兩個(gè)方格間的邊界不要繪制,避免格子太多干擾了游戲。除標(biāo)記外,還繪制了兩個(gè)按鈕來(lái)觸發(fā)對(duì)比與擦除。
ef paintEvent(self, event): # 重置遮罩圖像 self.pixmap.fill()
# 創(chuàng)建繪制用的QPainter,筆畫(huà)粗細(xì)為2像素 # 事先已經(jīng)在Qt窗體上鋪了一個(gè)藍(lán)色的背景圖片,因此投過(guò)遮罩圖案看下去標(biāo)記線條是藍(lán)色的 p = QPainter(self.pixmap)
p.setPen(QPen(QBrush(QColor(0, 0, 0)), 2))
for row in xrange(len(self.result)): for col in xrange(len(self.result[0])): if self.result[row][col] != 0: # 定一個(gè)基點(diǎn),避免算數(shù)太難看 base_l_x = self.ANCHOR_LEFT_X + self.CLIP_WIDTH * col
base_r_x = self.ANCHOR_RIGHT_X + self.CLIP_WIDTH * col
base_y = self.ANCHOR_Y + self.CLIP_HEIGHT * row
if row == 0 or self.result[row - 1][col] == 0: # 如果是第一行,或者上面的格子為空,畫(huà)一條上邊 p.drawLine(base_l_x, base_y, base_l_x + self.CLIP_WIDTH, base_y)
p.drawLine(base_r_x, base_y, base_r_x + self.CLIP_WIDTH, base_y) if row == len(self.result) - 1 or self.result[row + 1][col] == 0: # 如果是最后一行,或者下面的格子為空,畫(huà)一條下邊 p.drawLine(base_l_x, base_y + self.CLIP_HEIGHT, base_l_x + self.CLIP_WIDTH, base_y + self.CLIP_HEIGHT)
p.drawLine(base_r_x, base_y + self.CLIP_HEIGHT, base_r_x + self.CLIP_WIDTH, base_y + self.CLIP_HEIGHT) if col == 0 or self.result[row][col - 1] == 0: # 如果是第一列,或者左邊的格子為空,畫(huà)一條左邊 p.drawLine(base_l_x, base_y, base_l_x, base_y + self.CLIP_HEIGHT)
p.drawLine(base_r_x, base_y, base_r_x, base_y + self.CLIP_HEIGHT) if col == len(self.result[0]) - 1 or self.result[row][col + 1] == 0: # 如果是第一列,或者右邊的格子為空,畫(huà)一條右邊 p.drawLine(base_l_x + self.CLIP_WIDTH, base_y, base_l_x + self.CLIP_WIDTH, base_y + self.CLIP_HEIGHT)
p.drawLine(base_r_x + self.CLIP_WIDTH, base_y, base_r_x + self.CLIP_WIDTH, base_y + self.CLIP_HEIGHT)
# 在遮罩上繪制按鈕區(qū)域,避免按鈕被遮罩擋住看不見(jiàn) p.fillRect(self.btn_compare.geometry(), QBrush(QColor(0, 0, 0)))
p.fillRect(self.btn_toggle.geometry(), QBrush(QColor(0, 0, 0)))
# 將遮罩圖像作為遮罩 self.setMask(QBitmap(self.pixmap))
這里我沒(méi)有替換變量,太麻煩了,能看清楚算法就行。
讓PyQt程序在任務(wù)欄隱藏
為了讓PyQt程序不出現(xiàn)在任務(wù)欄,構(gòu)造QWidget設(shè)置了這些屬性
self.setWindowFlags(Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint | Qt.Popup | Qt.Tool)
讓PyQt程序加入系統(tǒng)托盤(pán)、資源文件使用
PyQt添加托盤(pán)菜單非常容易,幾行代碼就可以
創(chuàng)建托盤(pán) self.icon = QIcon(":\icon.png")
self.trayIcon = QSystemTrayIcon(self) self.trayIcon.setIcon(self.icon) self.trayIcon.setToolTip(u"QQ找茬助手") self.trayIcon.show()
# 托盤(pán)氣泡消息 self.trayIcon.showMessage(u"QQ找茬助手", u"QQ找茬助手已經(jīng)待命,進(jìn)入游戲即可激活")
# 托盤(pán)菜單 self.action = QAction(u"退出QQ找茬助手", self, triggered = sys.exit) # 觸發(fā)點(diǎn)擊后調(diào)用sys.exit()命令,即退出 self.menu = QMenu(self) self.menu.addAction(self.action) self.trayIcon.setContextMenu(self.menu)

最初我是用的托盤(pán)圖標(biāo)是一個(gè).ico文件,執(zhí)行腳本可以正常顯示,但打包成exe后執(zhí)行在托盤(pán)上顯示為一個(gè)空白圖標(biāo),用Python的idle工具編譯運(yùn)行也是空白。嘗試多次后發(fā)現(xiàn):PyQt的托盤(pán)圖標(biāo)不能使用.ico文件,否則會(huì)顯示空白,換成png格式素材就沒(méi)問(wèn)題!
PyQt資源文件打包
Qt使用一個(gè).qrc格式的xml文件管理素材,代碼用可用:\xxx\xxx.png的方式引用資源文件中的素材,這在PyQt中同樣支持。
這里我創(chuàng)建了一個(gè)resources.qrc文件
<!DOCTYPE RCC> <RCC version="1.0"> <qresource> <file>icon.png</file> </qresource> </RCC>
然后用
pyrcc4 resources.qrc > resources.py
命令,將資源文件轉(zhuǎn)成一個(gè)python模塊,在代碼中import resources,則可以用這樣的方式使用圖像素材
self.icon = QIcon(":\icon.png")
打包成可執(zhí)行程序
這個(gè)工具是給別人用的,肯定不能以py腳本的形式發(fā)布,我使用了cx_Freeze來(lái)打包為可執(zhí)行程序。
為此要寫(xiě)一個(gè)打包命令腳本convert2exe.py
#!Python #coding=gbk # python轉(zhuǎn)exe腳本 # # 安裝cx_Freeze # 執(zhí)行 python convert2exe.py build # 將自動(dòng)生成build目錄, 其下所有文件都必須打包 # import sys from cx_Freeze import setup, Executable
base = None if sys.platform == "win32":
base = "Win32GUI"
buildOptions = dict(
compressed = True)
setup(
name = "ZhaoChaAssistant", version = "1.0", description = "ZhaoChaAssistant", options = dict(build_exe = buildOptions), executables = [Executable("zhaochaassistant.py", base = base, icon = "icon.ico")])
最后執(zhí)行一個(gè)命令
python convert2exe.py build
則會(huì)在當(dāng)前路徑下創(chuàng)建個(gè)build目錄,打包的程序就在其中一個(gè)exe.win-amd64-2.7的目錄中,運(yùn)行exe即可執(zhí)行,與Python無(wú)二??上н@個(gè)包太大了一些,整個(gè)目錄達(dá)到了30M。
為了讓exe程序也有一個(gè)好看的圖標(biāo),在最后一行中的executables參數(shù)中指定了icon = "icon.ico",這個(gè)圖標(biāo)就最好使用多頁(yè)的.ico格式(16x16,32x32,48x48...),讓程序在各種顯示環(huán)境下(桌面、文件夾)都有原生的顯示。
如果打包的時(shí)候必須使用獨(dú)立的資源,可在buildOptions字典參數(shù)中增加一條include_files = ['xxx.dat']配置,這樣在打包時(shí)會(huì)將python腳本目錄中的xxx.dat文件拷貝到exe目錄中,不寫(xiě)的話就得人工拷貝了。
小技巧:Python獲得自己的絕對(duì)路徑
Python中有個(gè)魔術(shù)變量可以得到腳本自身的名稱,但轉(zhuǎn)換成exe后該變量失效,這時(shí)得改用sys.executable獲得可執(zhí)行程序的名稱,可用hasattr(sys, "frozen")判斷自己是否已被打包,下面是一個(gè)方便取絕對(duì)路徑的函數(shù):
import sys def module_path(): if hasattr(sys, "frozen"): return os.path.dirname(os.path.abspath(unicode(sys.executable, sys.getfilesystemencoding()))) return os.path.dirname(os.path.abspath(unicode(__file__, sys.getfilesystemencoding())))
結(jié)束語(yǔ)
Python可能是程序員最好的玩具,什么都能粘起來(lái),日常寫(xiě)點(diǎn)小工具再合適不過(guò)了。
文中的第三方模塊都可以Google獲得下載地址,有些庫(kù)沒(méi)有Win7 64位的原始版本(比如PIL),但可到
http://www.lfd.uci.edu/~gohlke/pythonlibs/
下載別人編譯好的,也很方便。
- Python計(jì)算斗牛游戲概率算法實(shí)例分析
- Python實(shí)現(xiàn)的破解字符串找茬游戲算法示例
- Python實(shí)現(xiàn)破解猜數(shù)游戲算法示例
- python實(shí)現(xiàn)的生成隨機(jī)迷宮算法核心代碼分享(含游戲完整代碼)
- Python寫(xiě)的貪吃蛇游戲例子
- python開(kāi)發(fā)的小球完全彈性碰撞游戲代碼
- 基于Python實(shí)現(xiàn)的掃雷游戲?qū)嵗a
- Python新手實(shí)現(xiàn)2048小游戲
- python基礎(chǔ)教程之實(shí)現(xiàn)石頭剪刀布游戲示例
- python實(shí)現(xiàn)猜數(shù)字游戲(無(wú)重復(fù)數(shù)字)示例分享
- Python版的文曲星猜數(shù)字游戲代碼
- Python基于分水嶺算法解決走迷宮游戲示例
相關(guān)文章
詳解Python中Sync與Async執(zhí)行速度快慢對(duì)比
Python新的版本中支持了async/await語(yǔ)法, 很多文章都在說(shuō)這種語(yǔ)法的實(shí)現(xiàn)代碼會(huì)變得很快, 但是這種快是有場(chǎng)景限制的。這篇文章將嘗試簡(jiǎn)單的解釋為何Async的代碼在某些場(chǎng)景比Sync的代碼快2023-03-03
嘗試使用Python多線程抓取代理服務(wù)器IP地址的示例
這篇文章主要介紹了嘗試使用Python多線程抓取代理服務(wù)器IP地址的示例,盡管有GIL的存在使得Python并不能真正實(shí)現(xiàn)多線程并行,需要的朋友可以參考下2015-11-11
python轉(zhuǎn)換pkl模型文件為txt文件問(wèn)題
這篇文章主要介紹了python轉(zhuǎn)換pkl模型文件為txt文件問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-06-06
python中通過(guò)pip安裝庫(kù)文件時(shí)出現(xiàn)“EnvironmentError: [WinError 5] 拒絕訪問(wèn)”的問(wèn)題
這篇文章主要介紹了python中通過(guò)pip安裝庫(kù)文件時(shí)出現(xiàn)“EnvironmentError: [WinError 5] 拒絕訪問(wèn)”的問(wèn)題,本文給大家分享解決方案,感興趣的朋友跟隨小編一起看看吧2020-08-08
對(duì)Python中9種生成新對(duì)象的方法總結(jié)
今天小編就為大家分享一篇對(duì)Python中9種生成新對(duì)象的方法總結(jié),具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2018-05-05
Python關(guān)于拓?fù)渑判蛑R(shí)點(diǎn)講解
在本篇文章里小編給大家分享了一篇關(guān)于Python關(guān)于拓?fù)渑判蛑R(shí)點(diǎn)講解內(nèi)容,有興趣的朋友們可以學(xué)習(xí)下。2021-01-01
Python中pytest的參數(shù)化實(shí)例解析
這篇文章主要介紹了Python中pytest的參數(shù)化實(shí)例解析,pytest是一個(gè)非常成熟的全功能的Python測(cè)試框架,主要有簡(jiǎn)單靈活,容易上手,支持參數(shù)化等特點(diǎn),需要的朋友可以參考下2023-07-07
Python實(shí)現(xiàn)向服務(wù)器請(qǐng)求壓縮數(shù)據(jù)及解壓縮數(shù)據(jù)的方法示例
這篇文章主要介紹了Python實(shí)現(xiàn)向服務(wù)器請(qǐng)求壓縮數(shù)據(jù)及解壓縮數(shù)據(jù)的方法,涉及Python文件傳輸及zip文件相關(guān)操作技巧,需要的朋友可以參考下2017-06-06

