好久沒寫技術相關的文章,這次寫篇有意思的,關于一個有意思的游戲――QQ找茬,關于一種有意思的語言――Python,關于一個有意思的庫――Qt。
這是一個用于QQ大家來找茬(美女找茬)的輔助外掛,開發的原因是看到老爸天天在玩這個游戲,分數是慘不忍睹的負4000多。他玩游戲有他的樂趣,并不很在意輸贏,我做這個也只是自我娛樂,順便討他個好,畢竟我們搞編程的實在難有機會在父輩面前露露手。本來是想寫個很簡單的東西,但由于過程中老爸的多次嘲諷,逼得我不得不盡力完善,最后形成了一個小小的產品。
接觸Python是2010年,相見恨晚,去年拿它寫了些小玩意,離職前給前公司留下了一個Python+wxPython的工作工具,還挺受歡迎。換公司后努力學習C++&Qt,很后悔當初選擇了wxPython而不是PyQt,沒能一脈相承。使用Qt越久,不得不越來越喜歡,寫這個東西正好就用上了。
話不多說,進入正題。這不是一篇完整的代碼講解,只是過程中的一些技術做個分享,包括后來被放棄的一些技術點。當初搜索這些東西也挺費力的,在這做個筆記,后來者也許能搜到收益。
先上個圖:
話說這位是游戲中出鏡最多的MM,和QQ什么關系啊?
輔助工具在游戲中增加了兩個按鈕,點擊“對比”則自動找“茬”,用藍色小框標識,點擊“擦除”清除標識。
游戲窗口探查
這得用PyWin32庫,它是對windows接口的Python封裝,VC能做的它基本都行。
下載地址:http://sourceforge.net/projects/pywin32/,但不能直接點Download圖標,不然下下來是一個Readme.txt,點“Browse All Files”尋找需要的版本。
#coding=gbk import win32gui game_hwnd = win32gui.FindWindow("#32770", "大家來找茬") print game_hwnd
QQ找茬是個對話框窗口,Class是“#32770”,這種窗口桌面上有很多,所以還配合了標題“大家來找茬”匹配,又因為是中文,所以第一行指定了使用gbk編碼,否則要么找不到,要么運行出錯。
游戲圖片提取
提取圖片采用了截屏的方式,找到窗口后將窗口提到最前,再作窗口截屏。截屏使用了大名鼎鼎的Python Imaging Library (PIL)庫。
import ImageGrab import win32con win32gui.ShowWindow(game_hwnd, win32con.SW_RESTORE) # 強行顯示界面后才好截圖 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() # 分別裁剪左右內容圖片 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()
上面用到的坐標都為為了演示代碼簡單填的,實際上使用了變量參數,而且要區分分辨率什么的。
PIL是一個強大的Python圖形庫(使用文檔),待會的對比分析也須要用到。ImageGrab是PIL的一個模塊,用于圖像的抓取。不帶參數的ImageGrab.grab()進行全屏截屏,返回一個Image對象,也可使用一個元組作為參數指定要截取的范圍(左上與右下兩點的坐標),這兩種截屏都是不帶鼠標指針的,還有一個ImageGrab.grabclipboard()可從系統剪貼板采集圖像。
得到Image圖像后可用show()方法,使用系統默認的圖像查看工具打開,方便調試,也可以用save(filename)保存成文件,對應的可以Image.open(filename)打開獲得。
grab得到了一個包含左右圖片的Image對象后,用crop(box)方法可裁剪得到其中指定的區域,分別拿到左右兩個游戲圖片。
對比獲得兩圖內容不同的區域
很自然想到把兩圖裁剪成N個小圖片分別對比,左右統一區域對應的小圖片不相等則為“茬”區,唯一的問題是怎么判斷兩個圖片內容不一致?
一開始以為很會有些麻煩,直到發現了Image.histogram()函數,該函數用于得到圖像的顏色直方圖。我平常也愛好攝影,知道直方圖可以表示一張圖片中各種亮度(或顏色)的數量,兩張自然圖片的直方圖基本是不一樣的,除非兩圖對稱、顏色一致但排列不一,但就算如此,將兩圖繼續分割下去,其子圖的直方圖也會不一樣。直方圖就是一種圖形到數值的轉換,對比兩圖的顏色數值就可知是否存在差異。
一張用RBG顏色格式的圖像,histogram()函數將返回一個長度為768的數組,第0-255表示紅色的0-255,第256-511表色綠色的0-255,第512-767表色藍色的0-255,數值表示該顏色像素的個數。因此,histogram()列表所有成員之和等于改圖像的像素值 x 3。
寫了一個函數,用來獲得兩圖比較的數值差:
ef compare(image_a, image_b): '''返回兩圖的差異值 返回兩圖紅綠藍差值萬分比之和''' 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
將函數返回的紅綠藍差值相加,如果超過了預定定的閥值2000,則表示該區域不同。這個計算方式有點“土”,但對這次要解決的問題很有效,就沒再繼續改進。
將左右大圖裁剪成多個小圖分別進行對比 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的小塊,定義一個50x45的二位數組存儲結果,分別比較后將差值大于閥值的數組區域標記為1.
在游戲上標記兩邊不同的區域
最初我用了PyWin32的一些函數,獲得游戲窗口句柄后直接在上面繪制,但我不太熟悉Windows編程,不知道如何解決游戲自身重繪后將我的標記擦除的問題,然后搬來了Qt。用Qt創建了一個和游戲大小一樣透明的QWidget窗口,疊加在游戲窗口上,用遮罩來繪制標記。標記數據已記錄在result數組中,在指定的位置繪制一個方格則表示該區域左右不同,要注意兩個方格間的邊界不要繪制,避免格子太多干擾了游戲。除標記外,還繪制了兩個按鈕來觸發對比與擦除。
ef paintEvent(self, event): # 重置遮罩圖像 self.pixmap.fill() # 創建繪制用的QPainter,筆畫粗細為2像素 # 事先已經在Qt窗體上鋪了一個藍色的背景圖片,因此投過遮罩圖案看下去標記線條是藍色的 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: # 定一個基點,避免算數太難看 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: # 如果是第一行,或者上面的格子為空,畫一條上邊 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: # 如果是最后一行,或者下面的格子為空,畫一條下邊 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: # 如果是第一列,或者左邊的格子為空,畫一條左邊 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: # 如果是第一列,或者右邊的格子為空,畫一條右邊 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) # 在遮罩上繪制按鈕區域,避免按鈕被遮罩擋住看不見 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))
這里我沒有替換變量,太麻煩了,能看清楚算法就行。
讓PyQt程序在任務欄隱藏
為了讓PyQt程序不出現在任務欄,構造QWidget設置了這些屬性
self.setWindowFlags(Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint | Qt.Popup | Qt.Tool)
讓PyQt程序加入系統托盤、資源文件使用
PyQt添加托盤菜單非常容易,幾行代碼就可以
創建托盤 self.icon = QIcon(":\icon.png") self.trayIcon = QSystemTrayIcon(self) self.trayIcon.setIcon(self.icon) self.trayIcon.setToolTip(u"QQ找茬助手") self.trayIcon.show() # 托盤氣泡消息 self.trayIcon.showMessage(u"QQ找茬助手", u"QQ找茬助手已經待命,進入游戲即可激活") # 托盤菜單 self.action = QAction(u"退出QQ找茬助手", self, triggered = sys.exit) # 觸發點擊后調用sys.exit()命令,即退出 self.menu = QMenu(self) self.menu.addAction(self.action) self.trayIcon.setContextMenu(self.menu)
最初我是用的托盤圖標是一個.ico文件,執行腳本可以正常顯示,但打包成exe后執行在托盤上顯示為一個空白圖標,用Python的idle工具編譯運行也是空白。嘗試多次后發現:PyQt的托盤圖標不能使用.ico文件,否則會顯示空白,換成png格式素材就沒問題!
PyQt資源文件打包
Qt使用一個.qrc格式的xml文件管理素材,代碼用可用:\xxx\xxx.png的方式引用資源文件中的素材,這在PyQt中同樣支持。
這里我創建了一個resources.qrc文件
icon.png
然后用
pyrcc4 resources.qrc > resources.py
命令,將資源文件轉成一個python模塊,在代碼中import resources,則可以用這樣的方式使用圖像素材
self.icon = QIcon(":\icon.png")
打包成可執行程序
這個工具是給別人用的,肯定不能以py腳本的形式發布,我使用了cx_Freeze來打包為可執行程序。
為此要寫一個打包命令腳本convert2exe.py
#!Python #coding=gbk # python轉exe腳本 # # 安裝cx_Freeze # 執行 python convert2exe.py build # 將自動生成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")])
最后執行一個命令
python convert2exe.py build
則會在當前路徑下創建個build目錄,打包的程序就在其中一個exe.win-amd64-2.7的目錄中,運行exe即可執行,與Python無二。可惜這個包太大了一些,整個目錄達到了30M。
為了讓exe程序也有一個好看的圖標,在最后一行中的executables參數中指定了icon = "icon.ico",這個圖標就最好使用多頁的.ico格式(16x16,32x32,48x48...),讓程序在各種顯示環境下(桌面、文件夾)都有原生的顯示。
如果打包的時候必須使用獨立的資源,可在buildOptions字典參數中增加一條include_files = ['xxx.dat']配置,這樣在打包時會將python腳本目錄中的xxx.dat文件拷貝到exe目錄中,不寫的話就得人工拷貝了。
小技巧:Python獲得自己的絕對路徑
Python中有個魔術變量可以得到腳本自身的名稱,但轉換成exe后該變量失效,這時得改用sys.executable獲得可執行程序的名稱,可用hasattr(sys, "frozen")判斷自己是否已被打包,下面是一個方便取絕對路徑的函數:
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())))
結束語
Python可能是程序員最好的玩具,什么都能粘起來,日常寫點小工具再合適不過了。
文中的第三方模塊都可以Google獲得下載地址,有些庫沒有Win7 64位的原始版本(比如PIL),但可到
http://www.lfd.uci.edu/~gohlke/pythonlibs/
下載別人編譯好的,也很方便。
更多文章、技術交流、商務合作、聯系博主
微信掃碼或搜索:z360901061

微信掃一掃加我為好友
QQ號聯系: 360901061
您的支持是博主寫作最大的動力,如果您喜歡我的文章,感覺我的文章對您有幫助,請用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點擊下面給點支持吧,站長非常感激您!手機微信長按不能支付解決辦法:請將微信支付二維碼保存到相冊,切換到微信,然后點擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對您有幫助就好】元
