学习之路三十九:新手学习

时间:2021-07-14 09:48:01

来到了新公司,一开始就要做个程序去获取另外一个程序里的数据,哇,挑战性很大。

经过两周的学习,终于搞定,主要还是对Windows API有了更多的了解。

文中所有的消息常量,API,结构体都整理出来了(还不是很全):Windows.zip 

 

目录:

  1. 获取控件句柄
  2. 模拟键盘和鼠标
  3. 文本框赋值
  4. 操作DateTimePicker控件
  5. 操作TreeView控件
  6. 识别简单验证码
  7. 判断按钮状态

 

正文:

一丶怎么获取每个控件的句柄

  第一种是使用FindWindow和FindWindowEx两个API结合使用,但太累太繁琐,不爽。

  说实话第一次我是通过Spy++看我所需要的控件的顺序,然后循环几次获取这个控件的句柄,显然这种方式很麻烦。

  第二种是在网上看到了另一种获取控件句柄的方式:

  ①每个控件都有唯一的ControlID

  ②通过Spy++查看每一个ControlID,并通过EnumChildWindows来循环每一个控件

  代码如下:

 1     public class ExportForm : BaseForm
2 {
3 private string _userID = string.Empty;
4 private IntPtr _cancelButtonHandle = IntPtr.Zero;
10 11 private readonly int _cancelButtonControlID = Convert.ToInt32("00000002", 16); //通过Spy++获取你想要的ControlID
12 private readonly int _confirmButtonControlID = Convert.ToInt32("00000001", 16);
17 18 public ExportForm(string userID)
19 {
20 this._userID = userID;
21 }
43 public override sealed void GetAllHandles()
44 {
45 base.LoadFormHandle(null, CITICConfigInfo.ExportFormName);
46 WindowsAPI.EnumChildWindows(base.FormHandle, (handle, param) => //这个API是循环窗体中的所有控件
47 {
48 int flagControlID = WindowsAPI.GetWindowLong(handle, WindowsConst.GWL_ID); //通过句柄获取ControlID
49
50 if (flagControlID == this._cancelButtonControlID)
51 {
52 this._cancelButtonHandle = handle;
53 }
74 75 return true;
76 }, 0);
77 }
78 }

 

二丶模拟键盘和鼠标

  在一些特殊的情况下,没有办法发送消息通知控件触发单击事件(或其它事件),只能通过模拟键盘和鼠标来操作了。

  关于Hook的学习请看 - 学习之路三十八:Hook(钩子)的学习

  ①mouse_event - 鼠标操作

  ②keybd_event - 触发键盘

  API的定义:

 1         /// <summary>
2 /// 键盘操作指令
3 /// </summary>
4 /// <param name="bVk">键盘指令:Enter,F1等键盘按钮,使用Keys枚举即可</param>
5 /// <param name="bScan">默认都为 - 0</param>
6 /// <param name="dwFlags">默认都为 - 0</param>
7 /// <param name="dwExtraInfo">默认都为 - 0</param>
8 [DllImport("user32.dll")]
9 public static extern void keybd_event(Byte bVk, Byte bScan, Int32 dwFlags, Int32 dwExtraInfo);
10
11 /// <summary>
12 /// 设置鼠标操作指令
13 /// </summary>
14 /// <param name="flag">指令类型:单击,移动,双击</param>
15 /// <param name="x">X坐标的位置</param>
16 /// <param name="y">Y坐标的位置</param>
17 /// <param name="cButtons">默认都为 - 0</param>
18 /// <param name="dwExtraInfo">默认都为 - 0</param>
19 [DllImport("user32.dll", CharSet = CharSet.Auto, CallingConvention = CallingConvention.StdCall)]
20 public static extern void mouse_event(int flag, int x, int y, int cButtons, int dwExtraInfo);

   关于鼠标API的调用:

1          Rectangle rectangle;
2 WindowsAPI.GetWindowRect(this._treeViewHandle, out rectangle);
3 WindowsAPI.SetCursorPos(rectangle.Left + 55, rectangle.Top + 220);
4 WindowsAPI.mouse_event(WindowsConst.MOUSEEVENTF_LEFTDOWN, 0, 0, 0, 0); //第一个参数为消息指令
5 WindowsAPI.mouse_event(WindowsConst.MOUSEEVENTF_LEFTUP, 0, 0, 0, 0);

 

三丶给文本框赋值

  在Windows API我感觉比较重要的方法是SendMessage,几乎所有的发送指令都需要用到它,把它用好就成功了一大半了(瞎说的)。

  其实通过Reflector查看.NET源码,发现内部方法中SendMessage有将近20个重载方法,很多,举其中一个列子:

 1         /// <summary>
2 /// 给句柄发送指令消息,等待消息处理完成
3 /// </summary>
4 /// <param name="handle">指定句柄</param>
5 /// <param name="message">消息指令:如click</param>
6 /// <param name="wParam">默认都为 - 0</param>
7 /// <param name="lParam">默认都为 - 0</param>
8 /// <returns>返回的结果</returns>
9 [DllImport("user32.dll")]
10 public static extern UInt32 SendMessage(IntPtr handle, UInt32 message, UInt32 wParam, UInt32 lParam);
11
12 //调用方式
13 WindowsAPI.SendMessage(this._passwordHandle, WindowsConst.WM_SETTEXT, 0, "这是我发送的消息"); //最后一个参数是给文本框赋值内容

 

四丶操作DatetimePicker控件

  操作日期控件我查找资料搞了好久,原来它并不是仅仅发送一个消息就可以搞定的,我猜测大多数复杂控件要触发事件肯定不能用SendMessage就以为搞定了。

  原来要想给控件赋值必须用到操作内存的方式,代码如下:

  步骤:①根据句柄获取进程ID

     ②打开进程并获取进程句柄

     ③在进程中申请内存空间并返回申请的内存地址

       ④把数据写入到刚刚开辟的内存空间去

     ⑤发送消息通知日期控件更新数据

     ⑥释放内存

 1         private void OperationDateTimePicker()
2 {
3 SYSTEMTIME time = new SYSTEMTIME { wYear = 1990, wMonth = 10, wDay = 10 };
4
5 int objSize = Marshal.SizeOf(typeof(SYSTEMTIME));
6 byte[] buffer = new byte[objSize];
7 IntPtr flagHandle = Marshal.AllocHGlobal(objSize);
8 Marshal.StructureToPtr(time, flagHandle, true);
9 Marshal.Copy(flagHandle, buffer, 0, objSize);
10 Marshal.FreeHGlobal(flagHandle);
11
12 //①获取远程进程ID
13 int processID = default(int);
14 WindowsAPI.GetWindowThreadProcessId(this._startTimeHandle, ref processID);
15 //②获取远程进程句柄
16 IntPtr processHandle = WindowsAPI.OpenProcess(WindowsConst.PROCESS_ALL_ACCESS, false, processID);
17 //③在远程进程申请内存空间并返回内存地址
18 IntPtr memoryAddress = WindowsAPI.VirtualAllocEx(processHandle, IntPtr.Zero, objSize, WindowsConst.MEM_COMMIT, WindowsConst.PAGE_READWRITE);
19 //④把数据写入上一步申请的内存空间
20 WindowsAPI.WriteProcessMemory(processHandle, memoryAddress, buffer, buffer.Length, 0);
21 //⑤发送消息给句柄叫它更新数据
22 WindowsAPI.SendMessage(this._startTimeHandle, WindowsConst.DTM_SETSYSTEMTIME, WindowsConst.GDT_VALID, memoryAddress);
23 //⑥释放内存并关闭句柄
24 WindowsAPI.VirtualFreeEx(processHandle, memoryAddress, objSize, WindowsConst.MEM_RELEASE);
25 WindowsAPI.CloseHandle(processHandle);
26 }

   PS:感觉往C++方面靠近了,学起来真心不容易啊,难怪说C++入门困难,领会到了,o(∩_∩)o 。

 

 五丶操作TreeView控件

  说实话对于怎么触发TreeView的Node单击事件我还没有找到资料,希望会的朋友告诉我,我感激不尽。

  首先获取TreeView控件的句柄是首要条件。

  ①获取根节点

1     //①获取根节点
2 int rootNodeNum = WindowsAPI.SendMessage(this._treeViewHandle, WindowsConst.TVM_GETNEXTITEM, WindowsConst.TVGN_ROOT, 0);
3 IntPtr rootNodeHandle = new IntPtr(rootNodeNum);

  ②选中根节点

1     //②选中根节点
2 WindowsAPI.SendMessage(this._treeViewHandle, WindowsConst.TVM_SELECTITEM, WindowsConst.TVGN_CARET, rootNodeHandle);

    ③获取指定节点句柄

1     //③遍历所有一级节点,获取我想要的节点句柄
2 IntPtr selectNodeHandle = rootNodeHandle;
3 for (int num = 1; num <= 6; num++) //记住节点的顺序,我想要的节点位置在第六个上
4 {
5 int flagNodeNum = WindowsAPI.SendMessage(this._treeViewHandle, WindowsConst.TVM_GETNEXTITEM, WindowsConst.TVGN_NEXT, selectNodeHandle);
6 selectNodeHandle = new IntPtr(flagNodeNum);
7 }

   ④选中并展开节点

1      //④展开节点
2 WindowsAPI.SendMessage(this._treeViewHandle, WindowsConst.TVM_SELECTITEM, WindowsConst.TVGN_CARET, selectNodeHandle); //最后一个参数为第三步获取的节点句柄
3 WindowsAPI.SendMessage(this._treeViewHandle, WindowsConst.TVM_EXPAND, WindowsConst.TVE_EXPAND, selectNodeHandle);

   ⑤寻找二级节点:注意消息常量的运用

1      //⑤继续循环当前节点,获取我想要的二级节点
2 IntPtr childrenNodeHandle = selectNodeHandle;
3 for (int num = 1; num <= 5; num++)
4 {
5 int flagNode = WindowsAPI.SendMessage(this._treeViewHandle, WindowsConst.TVM_GETNEXTITEM, WindowsConst.TVGN_CHILD, childrenNodeHandle);
6 childrenNodeHandle = new IntPtr(flagNode);
7 }

  ⑥节点的单击事件

  说实话我没有真正的通过发送消息来实现事件通知,只有通过模拟鼠标来操作的,希望懂得朋友教教我。

1      //⑥单击节点--模拟鼠标单击
2 Rectangle rectangle;
3 WindowsAPI.GetWindowRect(this._treeViewHandle, out rectangle);
4 WindowsAPI.SetCursorPos(rectangle.Left + 55, rectangle.Top + 220);
5 WindowsAPI.mouse_event(WindowsConst.MOUSEEVENTF_LEFTDOWN, 0, 0, 0, 0);
6 WindowsAPI.mouse_event(WindowsConst.MOUSEEVENTF_LEFTUP, 0, 0, 0, 0);

 

 六丶识别简单验证码

  图像识别这个领域高深莫测,不是那么容易搞得定的,前几天搞了一个对简单验证码的识别。

  基本步骤是:

  ①获取图片对象

  ②进行灰度化处理

  ③阈值 - 也就是换成白底黑字的图片

  ④分割图片

  ⑤处理每一张分割的图片(获取黑色像素点)

  ★:因为我的验证相对比较简单(纯数字,只是加了一些颜色干扰),我采取了一种简洁方式,通过观察我发现每个数字占的像素点都是一致的,所以前期我都把每个数字占的像素点都算出来了。

  从面前测试情况下来看正确率为100%。

 1     <add key="0" value="38"/>
2 <add key="1" value="19"/>
3 <add key="2" value="37"/>
4 <add key="3" value="52"/>
5 <add key="4" value="50"/>
6 <add key="5" value="35"/>
7 <add key="6" value="41"/>
8 <add key="7" value="28"/>
9 <add key="8" value="64"/>
10 <add key="9" value="42"/>

  代码如下(展不开,重新刷新下就可以了):

学习之路三十九:新手学习学习之路三十九:新手学习
  1 namespace BLL
2 {
3 public class IdentifyingCode
4 {
5 private string BlackFlag = "1";
6 private string WhiteFlag = "0";
7 private Dictionary<int, string> Values;
8 public static readonly IdentifyingCode Instance = new IdentifyingCode();
9
10 private IdentifyingCode()
11 {
12 this.Values = this.LoadData();
13 }
14
15 private Dictionary<int, string> LoadData()
16 {
17 Dictionary<int, string> values = new Dictionary<int, string>(10);
18 for (int index = 0; index < 10; index++)
19 {
20 string value = index.ToString(CultureInfo.InvariantCulture);
21 string key = ConfigurationManager.AppSettings[value];
22 values.Add(key.ToInt32(), value);
23 }
24 return values;
25 }
26
27 /// <summary>
28 /// 获取验证码内容
29 /// </summary>
30 /// <param name="filePath">图片路径</param>
31 /// <returns>验证码值</returns>
32 public string Check(string filePath)
33 {
34 return this.Check(new Bitmap(filePath));
35 }
36
37 /// <summary>
38 /// 获取验证码内容
39 /// </summary>
40 /// <param name="bitmap">图片对象</param>
41 /// <returns>验证码值</returns>
42 public string Check(Bitmap bitmap)
43 {
44 using (bitmap)
45 {
46 this.Gray(bitmap);
47 this.Threshold(bitmap, 190);
48 List<TempSize> tempSizes = this.Carve(bitmap);
49 return this.GetValue(bitmap, tempSizes.ToArray());
50 }
51 }
52
53 /// <summary>
54 /// 灰度化
55 /// </summary>
56 /// <param name="bitmap">图片对象</param>
57 private void Gray(Bitmap bitmap)
58 {
59 for (int x = 0; x < bitmap.Width; x++)
60 {
61 for (int y = 0; y < bitmap.Height; y++)
62 {
63 int grayNumber = GetGrayNumber(bitmap.GetPixel(x, y));
64 bitmap.SetPixel(x, y, Color.FromArgb(grayNumber, grayNumber, grayNumber));
65 }
66 }
67 }
68
69 /// <summary>
70 /// 获取灰度化的临界点
71 /// </summary>
72 /// <param name="color">每个像素的颜色对象</param>
73 /// <returns>临界值</returns>
74 private int GetGrayNumber(Color color)
75 {
76 return (int)(color.R * 0.3 + color.G * 0.59 + color.B * 0.11);
77 }
78
79 /// <summary>
80 /// 阈值,也就是转换成白底黑字的图片
81 /// </summary>
82 /// <param name="bitmap">图片对象</param>
83 /// <param name="criticalValue">临界值</param>
84 private void Threshold(Bitmap bitmap, int criticalValue)
85 {
86 for (int x = 0; x < bitmap.Width; x++)
87 {
88 for (int y = 0; y < bitmap.Height; y++)
89 {
90 Color color = bitmap.GetPixel(x, y);
91 bitmap.SetPixel(x, y, color.R >= criticalValue ? Color.White : Color.Black);
92 }
93 }
94 }
95
96 /// <summary>
97 /// 分割图片
98 /// </summary>
99 /// <param name="originalBitmap">图片对象</param>
100 /// <returns>每个图片的范围</returns>
101 private List<TempSize> Carve(Bitmap originalBitmap)
102 {
103 string blackPointFlags = GetBlackPointFlags(originalBitmap);
104
105 bool flag = true;
106 int xStart = default(int);
107 List<TempSize> tempSizes = new List<TempSize>();
108
109 for (int x = 0; x < originalBitmap.Width; x++)
110 {
111 if (blackPointFlags.Substring(x, 1) == BlackFlag)
112 {
113 if (flag)
114 {
115 flag = false;
116 xStart = x;
117 }
118 if (x < originalBitmap.Width)
119 {
120 if (blackPointFlags.Substring(x + 1, 1) == WhiteFlag)
121 {
122 int xEnd = x;
123 TempSize tempSize = new TempSize
124 {
125 XStart = xStart,
126 XWidth = (xEnd - xStart) + 1
127 };
128 tempSizes.Add(tempSize);
129 }
130 }
131 }
132 else
133 {
134 flag = true; //重新开始
135 }
136 }
137 return tempSizes;
138 }
139
140 private string GetBlackPointFlags(Bitmap originalBitmap)
141 {
142 string everyColumnHasBlackPoints = string.Empty;
143
144 for (int x = 0; x < originalBitmap.Width; x++)
145 {
146 for (int y = 0; y < originalBitmap.Height; y++)
147 {
148 if (originalBitmap.GetPixel(x, y).R == Color.Black.R)
149 {
150 everyColumnHasBlackPoints += "1";
151 break;
152 }
153 }
154 if (everyColumnHasBlackPoints.Length != x + 1)
155 {
156 everyColumnHasBlackPoints += "0";
157 }
158 }
159 return everyColumnHasBlackPoints;
160 }
161
162 private string GetValue(Bitmap originalBitmap, TempSize[] tempSizes)
163 {
164 string result = string.Empty;
165 for (int index = 0; index < tempSizes.Length; index++)
166 {
167 string pointValues = string.Empty;
168 TempSize tempSize = tempSizes[index];
169 for (int x = tempSize.XStart; x < tempSize.XStart + tempSize.XWidth; x++)
170 {
171 for (int y = 0; y < originalBitmap.Height; y++)
172 {
173 var color = originalBitmap.GetPixel(x, y);
174 pointValues += color.R == 0 ? "1" : "0";
175 }
176 }
177
178 int blackPointCount = pointValues.Count(p => p == '1');
179 if (this.Values.ContainsKey(blackPointCount))
180 {
181 result += this.Values[blackPointCount];
182 }
183 }
184 return result;
185 }
186 }
187
188 public struct TempSize
189 {
190 public int XStart;
191 public int XWidth;
192 }
193 }
View Code

 

七丶判断按钮的状态

  当我点击一个按钮去查询数据的时候,可能要花点时间,所以会把按钮的状态设置为不可用,那么Windows API是这样调用的:

1     //这边我需要检查“查询”按钮的状态,如果为灰色要等待,否则继续下去
2 bool selectButtonStatus = false;
3 while (selectButtonStatus == false)
4 {
5 selectButtonStatus = WindowsAPI.IsWindowEnabled(this._selectButtonHandle);
6 }

 

就这么多了,也只是把用到的记录了一下,没用到也不去学它,:-)。

以同步至:个人文章目录索引