上一次我們利用windows API以及xml配置來模擬鍵盤鼠標,但是並沒有對模擬中出現的邏輯錯誤或者異常進行處理(例如:模擬開啟之後,當前程序的部分窗體或者業務邏輯上出現錯誤,這時我們不只要提示相應的錯誤,而且必須要讓模擬程序將當前正常退出,以保證數據安全。),而且在窗體內部類似Label的控件也沒有辦法通過抓取窗體句柄或者遍歷子窗體句柄來獲取相應的信息,因此在一段時間的研究和整改之後,對於一些配置節點增加了ErrorTo屬性以及新增Error節點,在出現邏輯錯誤或者異常的時候,直接跳轉到對應的節點運行對應的正常退出步驟。
我設定是可將ErrorTo的屬性設置在所有節點之上,但是如果子節點不存在的話,則會使用父節點的設置。配置更改如下:
1 <Program Name="kp" Value="\BIN\kp.exe" ErrorTo="No2">SOFTWARE\航天信息\防偽開票\路徑</Program>
關於以上的變更,我們需要對AbstractStep進行修改,將默認的Init初始化方法做一些改變,代碼變化如下:
1 ///<summary>
2 /// 初始化
3 ///</summary>
4 ///<param name="step">上一步</param>
5 ///<param name="node">節點</param>
6 public virtual void Init(AbstractStep step, XElement node)
7 {
8 //原始代碼省略
9 //新增代碼
10 this.ErrorTo = this.GetAttribute("ErrorTo");
11 if (string.IsNullOrEmpty(this.ErrorTo))
12 {
13 this.ErrorTo = this.ParentStep.ErrorTo;
14 }
15 }
至於新增的Error節點,則主要的作用是做一個正常退出的步驟配置,因為我們可能開啟多個窗體,因此需要依次進行關閉,或者對於某些數據進行存儲撤消等等。除了主要節點以外,其他的子步驟還是原來的那些配置,大致如下:
1 <Error Name="No1">
2 <Form Class="TInvQueryForm" Caption="選擇發票號碼查詢">
3 <KeyBoard>{ESC}</KeyBoard>
4 <KeyBoard>{ESC}</KeyBoard>
5 <KeyBoard>{ESC}</KeyBoard>
6 <Form Class="TMainForm" Caption="增值稅防偽稅控系統防偽開票子系統">
7 <ClickTo>-5,5</ClickTo>
8 </Form>
9 <Form Class="TFaceForm" Caption="增值稅防偽稅控系統開票子系統">
10 <ClickTo>504,330</ClickTo>
11 </Form>
12 </Form>
13 </Error>
新增節點的話,則需要對解析XML配置做一些判斷,改動如下:
1 ///<summary>
2 /// 獲取子節點步驟
3 ///</summary>
4 ///<param name="parentStep">父節點步驟對象</param>
5 ///<param name="childList">子節點列表</param>
6 ///<param name="nowList">子節點步驟列表</param>
7 void GetChildStep(AbstractStep parentStep, IEnumerable<XElement> childList, IList<AbstractStep> nowList)
8 {
9 foreach (var node in childList)
10 {
11 if (node.Name.LocalName == "Error")
12 {
13 var type = node.Attribute("Name").Value;
14 this.dicError[type] = new List<AbstractStep>();
15 this.GetChildStep(parentStep, node.Elements(), this.dicError[type]);
16 }
17 else
18 {
19 //原始代碼省略
20 }
21 }
22 }
基本的配置和解析就差不多了,剩下的就是關於內部運行時需要做的一些修改了。因為我們需要對邏輯錯誤和異常進程處理,異常的話還是比較簡單的,只需要在Timer的Tick事件內,使用Try...Catch進行捕捉就可以了,邏輯錯誤的話,因為涉及到的方面比較廣泛,例如:抓取不到對應的窗體句柄、抓取的句柄值在比對數據中不存在、句柄不可用等等問題,由於我們前期已經設置了一個枚舉用於判斷步驟的狀態---EnumStepState,因此我們需要增加2個枚舉值,例如:LogicError、Exception,然後再參照可能會出現的錯誤情況,因此需要修改的步驟派生類為:FromStep、ChildStep、IfStep、EachStep以及ProgramStep。
FormStep:判斷超過等待時間後,不存在窗體時,出現邏輯錯誤。
ChildStep:與FormStep類似,只是范圍是在窗體的子窗體中尋找。
IfStep:這個就比較復雜了,因為If會引發另一個模擬裝置。因此我們要將If的模擬裝置的正常結束設定為原模擬裝置的啟動,並且將原本的模擬裝置的配置設定與If的模擬裝置上。
EachStep:類似IfStep,只是循環步驟中的返回值,需要多一個對於邏輯錯誤的狀態返回。
ProgramStep:對於需要啟動的程序進行判斷,如果存在,則邏輯錯誤。
到此,我們對於錯誤的修改也就差不多了,接下來,我們要對類似Label的取值,使用抓取內存的方式來使用。因為只是單獨的測試,因此還未加入到原本的功能裡面。這時我們需要用到另外一個windows api----kernel32.dll,主要的api如下:
1 ///<summary>
2 /// 關閉句柄資源
3 ///</summary>
4 ///<param name="processWnd">進程窗體句柄</param>
5 [DllImport("kernel32.dll")]
6 public static extern void CloseHandle(IntPtr processWnd);
7
8 ///<summary>
9 /// 打開進程句柄資源
10 ///</summary>
11 ///<param name="wndDesiredAccess">窗體需要訪問權限值,最高權限為0x1F0FFF</param>
12 ///<param name="bInheritHandle">是否繼承處理</param>
13 ///<param name="processId">進程ID</param>
14 ///<returns></returns>
15 [DllImportAttribute("kernel32.dll", EntryPoint = "OpenProcess")]
16 public static extern IntPtr OpenProcess(int dwDesiredAccess, bool bInheritHandle, int processId);
17
18 ///<summary>
19 /// 將進程指定內存的值讀入緩沖區
20 ///</summary>
21 ///<param name="processWnd">進程窗體句柄</param>
22 ///<param name="addressWnd">內存地址句柄</param>
23 ///<param name="bufferWnd">緩沖區地址句柄</param>
24 ///<param name="bufferSize">緩沖區大小</param>
25 ///<param name="bufferReadWnd">緩沖區讀取的字節數句柄</param>
26 ///<returns></returns>
27 [DllImportAttribute("kernel32.dll", EntryPoint = "ReadProcessMemory")]
28 public static extern bool ReadProcessMemory(IntPtr processWnd, IntPtr addressWnd, IntPtr bufferWnd, int bufferSize, IntPtr bufferReadWnd);
29
30 ///<summary>
31 /// 將值寫入進程指定的內存中
32 ///</summary>
33 ///<param name="processWnd">進程窗體句柄</param>
34 ///<param name="addressWnd">內存地址句柄</param>
35 ///<param name="values">值數組</param>
36 ///<param name="bufferSize">緩沖區大小</param>
37 ///<param name="bufferWriteWnd">緩沖區寫入的字節數句柄</param>
38 ///<returns></returns>
39 [DllImportAttribute("kernel32.dll", EntryPoint = "WriteProcessMemory")]
40 public static extern bool WriteProcessMemory(IntPtr processWnd, IntPtr addressWnd, int[] values, int bufferSize, IntPtr bufferWriteWnd);
另外是我們需要自己用到的方法,因為我個人比較喜歡使用擴展方法,因此擴展方法會比較多。代碼如下:
1 ///<summary>
2 /// 根據進程名獲取進程
3 ///</summary>
4 ///<param name="name">進程名</param>
5 ///<returns></returns>
6 public static Process GetProcessByName(string name)
7 {
8 Process process = null;
9 try
10 {
11 Process[] processes = Process.GetProcessesByName(name);
12 process = processes[0];
13 }
14 catch (Exception)
15 {
16 throw new Exception(string.Format("不存在進程名為{0}。", name));
17 }
18 return process;
19 }
20
21 ///<summary>
22 /// 根據進程標題獲取進程
23 ///</summary>
24 ///<param name="title">進程標題</param>
25 ///<returns></returns>
26 public static Process GetProcessByTitle(string title)
27 {
28 Process process = null;
29 try
30 {
31 Process[] processes = Process.GetProcesses();
32 foreach (var p in processes)
33 {
34 if (p.MainWindowTitle.IndexOf(title) != -1)
35 {
36 process = p;
37 }
38 }
39 }
40 catch (Exception)
41 {
42 throw new Exception(string.Format("不存在進程標題為{0}。", title));
43 }
44 return process;
45 }
46
47 --------------------------------------------------擴展方法----------------------------------------------
48 ///<summary>
49 /// 讀取進程對應內存的值
50 ///</summary>
51 ///<param name="p">進程</param>
52 ///<param name="address">內存地址</param>
53 ///<returns></returns>
54 public static int ReadProcessMemoryValue(this Process p, int address)
55 {
56 //設置緩沖區,打開進程句柄資源,讀取內存,關閉進程句柄資源
57 byte[] buffer = new byte[4];
58 IntPtr byteAddress = Marshal.UnsafeAddrOfPinnedArrayElement(buffer, 0);
59 IntPtr processWnd = Memory.OpenProcess(0x1F0FFF, false, p.Id);
60 Memory.ReadProcessMemory(processWnd, (IntPtr)address, byteAddress, 4, IntPtr.Zero);
61 Memory.CloseHandle(processWnd);
62 return Marshal.ReadInt32(byteAddress);
63 }
64
65 ///<summary>
66 /// 將值寫入進程指定內存
67 ///</summary>
68 ///<param name="baseAddress"></param>
69 ///<param name="processName"></param>
70 ///<param name="value"></param>
71 public static void WriteProcessMemoryValue(this Process p, int address, int value)
72 {
73 //打開進程句柄資源,將值寫入指定內存地址,釋放進程句柄資源
74 IntPtr processWnd = Memory.OpenProcess(0x1F0FFF, false, p.Id); //0x1F0FFF 最高權限
75 Memory.WriteProcessMemory(processWnd, (IntPtr)address, new int[] { value }, 4, IntPtr.Zero);
76 Memory.CloseHandle(processWnd);
77 }
擁有了方法之後呢,我們還需要借助一個工具的幫忙----CheatEngine,它可以幫助我們篩選需要的值,並且找到內存地址。使用代碼如下:
1 var p = Memory.GetProcessByTitle(this.txtProcessTitle.Text);
2 var address = 0x014C14DC;
3 var value = p.ReadProcessMemoryValue(address);
因為時間的關系,學習的並不多,差不多就到這裡了,謝謝各位。由於上一次忘了提供源碼,非常抱歉,這次我有補上哦,寫得差,多多包涵,呵呵。源碼在此! <------ 記得下載哦
作者 ahl5esoft