C#模擬鍵盤滑鼠之一
由於工作當中有部分任務需要使用到模擬鍵盤滑鼠來完成業務的自動呼叫,雖然原來的同事有做了一些共用方法以及使用XML配置檔來配置模擬動作流程,但是公用的方法和XML配置組合起來用的時候還是有不少的麻煩。
配置如下:
光從一些配置上,是比較難以理解的,個人覺得除了編寫這個配置的本人或者使用一段時間的開發人員以外,其他開發人員需要介入修改或者重新制作配置是有一定難度的。也許大家會對業務處理有所期待,業務判斷如下:
基本上就是屬於對每個節點的Switch判斷,通用的節點無非就那幾個,但是涉及額外業務的時候,那就必須要設定一些其他的節點作為判斷的依據,甚是複雜。
於是乎,在我進入公司差不多一個月的時間,我被分配到了這個任務上,嘗試理解了配置、公用方法以及處理程式碼後,發現還是要重新制作一個新的模組,業務處理更清晰、配置起來更容易。
首先我是從配置入手的,其實每一個節點就是一個步驟,每一步無非就是抓取父窗體、從父窗體內抓取對應的子窗體、將滑鼠定位在某一座標或窗體上、輸入值、獲取值等等。
新配置如下:
新的配置試用了比較常用的名稱作為節點的名字,對於配置也是考慮多種情況,而設定了不同的節點。root中的NameSpace屬性是用來配置反射具體的業務類的。
Program:啟動的程式,這是根據登錄檔讀取的。
Form:是單獨的窗體。
Child:窗體內部的子窗體或者控制元件,類似Label的除外。
KeyBoard:按鍵,單獨的鍵位或者組合鍵。
ClickTo:根據具體的座標或者當座標資料不存在時會定位到上一節點窗體的中央,點選
If:判斷節點,大家應該比較熟悉了,也就是當條件為真的情況下,將會執行節點內部的動作。
Each:與If類似,但是會迴圈執行。
Call:呼叫具體類方法的,配置圖中未出現。
由於步驟都是通過讀取XML每個節點的資訊而觸發動作的,而且部分子步驟需要呼叫父步驟窗體,因此我們需要提供一個基類方法給各個子類去實現,程式碼如下:
1 ///<summary> 2 /// 執行 3 ///</summary> 4 public abstract void Execute(); 5 6 ///<summary> 7 /// 初始化 8 ///</summary>9 ///<param name="step">上一步</param>10 ///<param name="node">節點</param>11 public virtual void Init(AbstractStep step, XElement node) 12 { 13 this.ParentStep = step; 14 this.Node = node; 15 this.State = EnumStepState.Normal; 16 this.Message = string.Empty; 17 }
子類如下:
1 ///<summary> 2 /// 程式說明:移動 3 /// 建立作者:ahl 4 /// 建立時間:2011-10-31 5 ///</summary> 6 class MoveToStep : AbstractStep 7 { 8 #region 變數 9 10 ///<summary>11 /// 橫座標 12 ///</summary>13 int x = 0; 14 15 ///<summary>16 /// 縱座標 17 ///</summary>18 int y = 0; 19 20 #endregion 21 22 #region 方法 23 24 ///<summary>25 /// 執行方法 26 ///</summary>27 public override void Execute() 28 { 29 if (this.ParentStep.Wnd != IntPtr.Zero) 30 { 31 var rect = this.ParentStep.Wnd.GetWindowRect(); 32 if (this.x == 0 && this.y == 0) 33 { 34 this.x = (rect.Left + rect.Right) / 2; 35 this.y = (rect.Top + rect.Bottom) / 2; 36 } 37 else 38 { 39 this.x += this.x < 0 ? rect.Right : rect.Left; 40 this.y += this.y < 0 ? rect.Bottom : rect.Top; 41 } 42 } 43 Hook.SetCursorPos(this.x, this.y); 44 } 45 46 ///<summary>47 /// 初始化 48 ///</summary>49 ///<param name="step">上一步</param>50 ///<param name="node">節點</param>51 public override void Init(AbstractStep step, XElement node) 52 { 53 base.Init(step, node); 54 var nodeValue = this.Node.Value; 55 if (!string.IsNullOrEmpty(nodeValue)) 56 { 57 var pos = nodeValue.Split(','); 58 this.x += Convert.ToInt32(pos[0]); 59 this.y += Convert.ToInt32(pos[1]); 60 } 61 } 62 63 #endregion 64 }
1 ///<summary> 2 /// 程式說明:移動至單擊 3 /// 建立作者:ahl 4 /// 建立時間:2011-10-31 5 ///</summary> 6 class ClickToStep : AbstractStep 7 { 8 #region 變數 9 10 ///<summary>11 /// 移動至 12 ///</summary>13 MoveToStep moveTo; 14 15 #endregion 16 17 #region 方法 18 19 ///<summary>20 /// 執行方法 21 ///</summary>22 public override void Execute() 23 { 24 this.moveTo = new MoveToStep(); 25 this.moveTo.Init(this.ParentStep, this.Node); 26 this.moveTo.Execute(); 27 Hook.MouseEvent(MouseEventFlag.LeftDown | MouseEventFlag.LeftUp, 0, 0, 0, IntPtr.Zero); 28 } 29 30 #endregion 31 }
其他的就省略了,以免程式碼過長,呵呵。有了一些基本的步驟以及配置之後,這時我們就需要一個可以用來解析配置的類了。主要是解析節點、遞迴子節點、對於一些特殊的節點,如If、Each、Call需要做一些額外的處理,程式碼如下:
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 SimpleReflector normalReflector = new SimpleReflector(typeof(AbstractStep), string.Format("AHL.HookHelper.SMK.{0}Step", node.Name.LocalName)); 12 AbstractStep step = normalReflector.GetClassObject() as AbstractStep; 13 step.Init(parentStep, node); 14 nowList.Add(step); 15 switch (node.Name.LocalName) 16 { 17 case "Form": 18 case "Child": 19 this.GetChildStep(step, step.Node.Elements(), nowList); 20 break; 21 case "If": 22 case "Each": 23 var eachStep = step as SMK.IfStep; 24 this.GetChildStep(step.ParentStep, step.Node.Elements(), eachStep.StepList); 25 eachStep.SetProperty(this.reflector, this.simulation, this.interval); 26 break; 27 case "Call": 28 (step as SMK.CallStep).ReflectorMethod(this.reflector); 29 break; 30 } 31 } 32 }
到這裡我們就把配置、節點以及解析等工作都完成了,於是乎就到了最後的階段,那就是使用定時器對對當前的步驟進行定時操作就可以了,程式碼如下:
1 ///<summary> 2 /// 設定定時器事件 3 ///</summary> 4 void SetTimerTick() 5 { 6 this.timer.Tick += (_s, _e) => 7 { 8 var step = this.stepList[this.index++]; 9 try 10 { 11 step.Execute(); 12 switch (step.State) 13 { 14 case EnumStepState.Normal: 15 this.stepList.Count.If(l => l <= this.index, () => 16 { 17 this.index = 0; 18 this.Finish(EnumStepState.Normal, step.Message); 19 }); 20 break; 21 case EnumStepState.Back: 22 this.index--; 23 break; 24 default: 25 this.Finish(step.State, step.Message; 26 break; 27 } 28 } 29 catch (Exception ex) 30 { 31 this.Finish(EnumStepState.Exception, ex.Message); 32 } 33 }; 34 }
由於當前的版本只是對於鍵盤滑鼠的模擬,但是當窗體使用類似Label的情況下,使用window api是沒有辦法抓取到此情況下的任何資料的,鑑於這種情況下,於是又開始了另一段新的學習,關於window api內關於程序記憶體的學習,等我完成之後再繼續討論吧,呵呵。