使用鼠标
C语言Windows程序设计 -> 第十一天 -> 使用鼠标
鼠标的使用同样是通过获取Windows鼠标消息来获取用户当前的鼠标状态的。
一、鼠标的介绍
鼠标是计算机的输入设备之一, 在图形化的操作系统上, 鼠标的使用使一些复杂的操作变得简单, 随着科技的进步, 鼠标的种类也越来越多,
按接口类型可分为串行鼠标、PS/2鼠标、总线鼠标、USB鼠标(多为光电鼠标)四种。按其工作原理及其内部结构的不同可以分为机械式,光机式和光电式。
这里我们不讨论鼠标的硬件构造, 更多关于鼠标的硬件知识请自行查阅相关资料。
1>. 鼠标所在的位置
在Windows系统下, 用户移动鼠标时, 在屏幕上一般会以一个斜式的箭头来表示鼠标当前的位置,
这个箭头实际上是一个位图格式的小图标, 称为"鼠标指针", 鼠标指针具有一个单像素精度的"热点"(hot spot), 当鼠标样式为箭头时,
这个"热点"就是鼠标箭头的顶点, 还有一些样式是"十"字样式, 这样的指针"热点"位于"十"字的中心位置,
热点在显示设备上指示了一个精确的位置。 当我们去捕获鼠标指针的位置时, 实际上是指鼠标指针的这个"热点"所在的像素单元的位置。
2>. 鼠标的术语
①. 单击 : 按下鼠标按键, 然后松开;
②. 双击 : 连续快速的按下鼠标同一个按键然后松开;
③. 拖动 : 保持按键按下状态, 并移动鼠标。
现在我们常见的三键鼠标, 三个按键分布称为左键、中键和右键, 其中左键的标识符简写为LBUTTON, 中键的标识符为MBUTTON, 右键的标识符为RBUTTON。 双键鼠标只有左键和右键, 单键鼠标只有左键。
3>. 鼠标的样式
Windows系统为鼠标提供了几种默认的鼠标样式, 如: 箭头、沙漏、十字瞄准等, 在以前学习的过程中实际上我们已经接触了使用默认的鼠标样式, 回忆这行代码:
wndclass.hCursor = LoadCursor( NULL, IDC_ARROW ) ;
这样就是使用一个默认的斜式箭头作为鼠标的指针样式, 斜式箭头样式的标识符为 IDC_ARROW, 这些标识符定义在 WINUSER.H 头文件中, 此外还有以下标识符及其对应的样式:
二、使用鼠标的简单示例
1>. 示例一: 获取鼠标指针位置
在这个示例中演示如何获取鼠标的位置, 先说下相关的消息标识符以及函数。
消息标识符: WM_MOUSEMOVE 当鼠标指针在客户区内移动或鼠标指针经过客户区窗口时会得到这个消息。
获取鼠标位置的函数: GetCursorPos 该函数的原型: BOOL GetCursorPos(LPPOINT lpPoint) ;
代码片段:
1 switch(message) 2 { 3 case WM_PAINT: //处理重绘消息 4 hdc = BeginPaint( hwnd, &ps ) ; 5 wsprintf( szBuffer, "屏幕坐标:(%i, %i)", pt.x, pt.y ); 6 TextOut( hdc, 10, 10, szBuffer, lstrlen(szBuffer) ) ; 7 ScreenToClient( hwnd, &pt ) ; //将相对于屏幕的坐标转换为相对于窗口客户区的坐标 8 wsprintf( szBuffer, "客户区坐标:(%i, %i)", pt.x, pt.y ); 9 TextOut( hdc, 10, 30, szBuffer, lstrlen(szBuffer) ) ; 10 EndPaint( hwnd, &ps ) ; 11 return 0 ; 12 13 case WM_MOUSEMOVE: //处理鼠标移动时发来的消息 14 GetCursorPos(&pt) ; 15 InvalidateRect( hwnd, NULL, TRUE ) ; 16 return 0 ;
完整的示例代码:
1 #include<windows.h> 2 3 LRESULT CALLBACK WndProc( HWND, UINT, WPARAM, LPARAM ) ; 4 5 int WINAPI WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow ) 6 { 7 static TCHAR szAppName[] = TEXT( "UseMouse_Demo" ) ; 8 HWND hwnd ; 9 MSG msg ; 10 WNDCLASS wndclass ; 11 12 wndclass.hInstance = hInstance ; 13 wndclass.lpfnWndProc = WndProc ; 14 wndclass.lpszClassName = szAppName ; 15 wndclass.style = CS_HREDRAW | CS_VREDRAW | CS_DBLCLKS ; 16 wndclass.hbrBackground = (HBRUSH) GetStockObject( WHITE_BRUSH ) ; 17 wndclass.hCursor = LoadCursor( NULL, IDC_ARROW ) ; 18 wndclass.hIcon = LoadIcon( NULL, IDI_APPLICATION ) ; 19 wndclass.cbClsExtra = 0 ; 20 wndclass.cbWndExtra = 0 ; 21 wndclass.lpszMenuName = 0 ; 22 23 if( !RegisterClass(&wndclass) ) 24 { 25 MessageBox( NULL, TEXT("错误, 无法注册窗口类."), TEXT("错误"), MB_OK ) ; 26 return 0 ; 27 } 28 29 hwnd = CreateWindow( szAppName, TEXT("获取鼠标指针位置 - Demo"), 30 WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, 31 CW_USEDEFAULT, CW_USEDEFAULT, 32 NULL, NULL, hInstance, NULL ) ; 33 34 ShowWindow( hwnd, iCmdShow ) ; 35 UpdateWindow( hwnd ) ; 36 37 while( GetMessage(&msg, NULL, 0, 0) ) 38 { 39 TranslateMessage( &msg ) ; 40 DispatchMessage( &msg ) ; 41 } 42 43 return msg.wParam ; 44 } 45 46 LRESULT CALLBACK WndProc( HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam ) 47 { 48 HDC hdc ; 49 PAINTSTRUCT ps ; 50 static POINT pt ; 51 TCHAR szBuffer[128] ; 52 53 switch(message) 54 { 55 case WM_PAINT: 56 hdc = BeginPaint( hwnd, &ps ) ; 57 wsprintf( szBuffer, "屏幕坐标:(%i, %i)", pt.x, pt.y ); 58 TextOut( hdc, 10, 10, szBuffer, lstrlen(szBuffer) ) ; 59 ScreenToClient( hwnd, &pt ) ; 60 wsprintf( szBuffer, "客户区坐标:(%i, %i)", pt.x, pt.y ); 61 TextOut( hdc, 10, 30, szBuffer, lstrlen(szBuffer) ) ; 62 EndPaint( hwnd, &ps ) ; 63 return 0 ; 64 65 case WM_MOUSEMOVE: 66 GetCursorPos(&pt) ; 67 InvalidateRect( hwnd, NULL, TRUE ) ; 68 return 0 ; 69 70 case WM_DESTROY: 71 PostQuitMessage(0) ; 72 return 0 ; 73 } 74 75 return DefWindowProc( hwnd, message, wParam, lParam ) ; 76 }
说一下整体的思路, 要即时跟踪获取鼠标在屏幕中的坐标, 首先要捕获鼠标的移动消息 WM_MOUSEMOVE, 当Windows向我们发来这个消息时就代码鼠标在进行移动, 随后我们对这个鼠标移动消息进行处理, 调用 GetCursorPos(&pt) ; 这个函数获取鼠标现在的位置, 获取到鼠标位置后为了能够立即在窗口中显示出来, 再调用 InvalidateRect( hwnd, NULL, TRUE ) ; 使整个客户区变成无效状态, 从而引发 WM_PAINT 需要重绘客户区内容的消息, 在处理重绘消息时输出刚刚获得的鼠标指针坐标位置。
关于 ScreenToClient( hwnd, &pt ) ; :
这个函数的功能是将屏幕坐标(相对于整个屏幕左上角的坐标)转换成相对于窗口客户区的坐标, 屏幕坐标与窗口客户区坐标的含义如图所示:
GetCursorPos获得的鼠标位置是屏幕坐标, 如果想知道他在窗口客户区内的相对位置就需要调用ScreenToClient函数将其转化为客户区坐标。
此外还有一个WIndows函数是将窗口客户区坐标转成屏幕坐标的, 函数为: ClientToScreen( hwnd, &pt ) ;
获取鼠标指针的位置还有其他的方法, 这里只是其中的一种。
2>. 示例二: 处理鼠标左键单击事件
鼠标左键在客户区被单击时发来的消息: WM_LBUTTONDOWN
1 switch(message) 2 { 3 case WM_PAINT: 4 hdc = BeginPaint( hwnd, &ps ) ; 5 EndPaint( hwnd, &ps ) ; 6 return 0 ; 7 8 case WM_LBUTTONDOWN: //处理鼠标左键单击被按下时产生的消息 9 x = LOWORD( lParam ) ; //获取鼠标位置x坐标信息 10 y = HIWORD( lParam ) ; //获取鼠标位置y坐标信息 11 wsprintf( szBuffer, "鼠标左键被单击, 击中位置: (%i, %i)", x, y ); 12 MessageBox( hwnd, szBuffer, TEXT("鼠标动作"), MB_OK ) ; 13 return 0 ; 14 15 case WM_DESTROY: 16 PostQuitMessage(0) ; 17 return 0 ; 18 }
完整的示例代码:
1 #include<windows.h> 2 3 LRESULT CALLBACK WndProc( HWND, UINT, WPARAM, LPARAM ) ; 4 5 int WINAPI WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow ) 6 { 7 static TCHAR szAppName[] = TEXT( "UseMouse_Demo" ) ; 8 HWND hwnd ; 9 MSG msg ; 10 WNDCLASS wndclass ; 11 12 wndclass.hInstance = hInstance ; 13 wndclass.lpfnWndProc = WndProc ; 14 wndclass.lpszClassName = szAppName ; 15 wndclass.style = CS_HREDRAW | CS_VREDRAW | CS_DBLCLKS ; 16 wndclass.hbrBackground = (HBRUSH) GetStockObject( WHITE_BRUSH ) ; 17 wndclass.hCursor = LoadCursor( NULL, IDC_ARROW ) ; 18 wndclass.hIcon = LoadIcon( NULL, IDI_APPLICATION ) ; 19 wndclass.cbClsExtra = 0 ; 20 wndclass.cbWndExtra = 0 ; 21 wndclass.lpszMenuName = 0 ; 22 23 if( !RegisterClass(&wndclass) ) 24 { 25 MessageBox( NULL, TEXT("错误, 无法注册窗口类."), TEXT("错误"), MB_OK ) ; 26 return 0 ; 27 } 28 29 hwnd = CreateWindow( szAppName, TEXT("处理鼠标单击事件 - Demo"), 30 WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, 31 CW_USEDEFAULT, CW_USEDEFAULT, 32 NULL, NULL, hInstance, NULL ) ; 33 34 ShowWindow( hwnd, iCmdShow ) ; 35 UpdateWindow( hwnd ) ; 36 37 while( GetMessage(&msg, NULL, 0, 0) ) 38 { 39 TranslateMessage( &msg ) ; 40 DispatchMessage( &msg ) ; 41 } 42 43 return msg.wParam ; 44 } 45 46 LRESULT CALLBACK WndProc( HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam ) 47 { 48 HDC hdc ; 49 PAINTSTRUCT ps ; 50 static POINT pt ; 51 TCHAR szBuffer[128] ; 52 static int x, y ; 53 54 switch(message) 55 { 56 case WM_PAINT: 57 hdc = BeginPaint( hwnd, &ps ) ; 58 EndPaint( hwnd, &ps ) ; 59 return 0 ; 60 61 case WM_LBUTTONDOWN: 62 x = LOWORD( lParam ) ; 63 y = HIWORD( lParam ) ; 64 wsprintf( szBuffer, "鼠标左键被单击, 击中位置: (%i, %i)", x, y ); 65 MessageBox( hwnd, szBuffer, TEXT("鼠标动作"), MB_OK ) ; 66 return 0 ; 67 68 case WM_DESTROY: 69 PostQuitMessage(0) ; 70 return 0 ; 71 } 72 73 return DefWindowProc( hwnd, message, wParam, lParam ) ; 74 }
这个示例演示的是当鼠标在客户区按下时弹出一个对话框, 对话框的内容是鼠标被按下时鼠标指针的位置信息, 可以看到, 这里我们没有使用 GetCursorPos 函数来获取鼠标指针的位置, 而是通过
x = LOWORD( lParam ) ; //获取鼠标位置x坐标信息 y = HIWORD( lParam ) ; //获取鼠标位置y坐标信息
来获取的, 参数lParam包含了鼠标指针的位置信息, 其中低位字节表示x坐标, 高位字节表示y坐标, 利用LOWORD和HIWORD宏可以取得这些坐标值, 这里获取的坐标指的是相对于窗口客户区的坐标。
三、客户区鼠标消息
与键盘消息不同, 在键盘消息中, Windows只把键盘消息发送到当前具有输入焦点的窗口, 而鼠标消息无论窗口是否获取焦点,
只要鼠标经过客户区, 或者在客户区内被单击窗口过程都会收到鼠标消息,
被点击(包括双击/单击/拖动)的窗口将变成活动窗口。与客户区消息相对应的称为非客户区消息,
非客户区消息是指鼠标指针在窗口内并在在客户区外的移动或单击/双击等, 非客户区包括窗口的标题栏、菜单栏、滚动条、窗口的边框,
这些将在后面进行讨论, 这里先说客户区鼠标消息。
1>. 鼠标单击
鼠标在客户区单击时各个鼠标按键所产生的消息如下:
鼠标按键 | 按下时产生的消息 | 释放时产生的消息 |
左键 | WM_LBUTTONDOWN | WM_LBUTTONUP |
中键 | WM_MBUTTONDOWN | WM_MBUTTONUP |
右键 | WM_RBUTTONDOWN | WM_RBUTTONUP |
示例2中已经演示了一个处理鼠标左键单击的示例, 对于中键和右键处理的方法是相同的, 只要等待Windows发来消息然后处理这些消息就行了。
2>. wParam参数中的内容
参数wParam中的值表示了鼠标按钮、Shift键和Ctrl键的状态。 将wParam与"鼠标键"标识符进行按位与(&)运算可以得到鼠标按键与鼠标键的状态, 以前缀MK_为开头的标识符称为"鼠标键", 有如下鼠标键:
#define MK_LBUTTON 0x0001 //按下左键 #define MK_RBUTTON 0x0002 //按下右键
#define MK_MBUTTON 0x0010 //按下中键 #define MK_SHIFT 0x0004 //按下Shift键 #define MK_CONTROL 0x0008 //按下Ctrl键
例如, 当接收到 WM_LBUTTONDOWN 消息时, 若
wParam & MK_SHIFT
的值为TRUE(非零), 则表示按下左键的同时也按下了Shift键。
例如:
case WM_LBUTTONDOWN: if( wParam & MK_CONTROL ) { MessageBox( hwnd, TEXT("Ctrl键与鼠标左键同时被按下!"), TEXT("鼠标动作"), MB_OK ) ; return 0 ; } return 0 ;
只有当鼠标左键与键盘的Ctrl键同时被按下时我们弹出个对话框说明"Ctrl键与鼠标左键同时被按下!", 否则什么也不做。
3>. 鼠标双击
双击对两次击中的位置以及时间间隔都有一定要求, 只有当两次快速的单击在物理位置上靠的很近并且时间间隔很短的情况下才算双击。
如果想让窗口过程接收鼠标双击消息, 需要在注册窗口类(RegisterClass)时, 初始化wndclass中的style成员的属性中再加上CS_DBLCLKS标识符:
wndclass.style = CS_HREDRAW | CS_VREDRAW | CS_DBLCLKS ;
如果在窗口类的style成员中没有包含 CS_DBLCLKS 标识符, 那么即使当用户双击时不会产生双击消息, 而是产生一串如下的消息:
WM_LBUTTONDOWN
WM_LBUTTONUP
WM_LBUTTONDOWN
WM_LBUTTONUP
由于用户在连续两次按下鼠标左键时需要一定时间, 即使这个时间比较短暂, 但是在这个过程中程序还是有可能收到其他消息的, 比如用户在快速的两次单击中手的微微抖动就会在其中插入一个WM_MOUSEMOVE的消息, 这里暂时忽略其中插入的消息, 假设消息就是连续的这些。
当窗口类的style成员只中包含CS_DBLCLKS标识符后, 用户再次双击就会产生这样的一串消息:
WM_LBUTTONDOWN
WM_LBUTTONUP
WM_LBUTTONDBLCLK
WM_LBUTTONUP
可以看到, 在加入 CS_DBLCLKS 标识符后, 第三个消息 WM_LBUTTONDOWN 只是被简单的替换成了 WM_LBUTTONDBLCLK 消息。
鼠标各个按键双击时第三个消息所对应替换的消息如下:
#define WM_LBUTTONDBLCLK 0x0203 //左键 #define WM_MBUTTONDBLCLK 0x0209 //中键 #define WM_RBUTTONDBLCLK 0x0206 //右键
四、非客户区鼠标消息
非客户区消息几乎与客户区消息完全对应, 只是在标识符中多了一个"NC"字符(noclient), 当鼠标指针在窗口的非客户区移动时(比如标题栏), 窗口过程就会接收到 WM_NCMOUSEMOVE 消息, 在客户区外鼠标按下产生的消息如下:
鼠标按键 | 按下 | 释放 | 第二次按下(双击) |
左键 | WM_NCLBUTTONDWON | WM_NCLBUTTONUP | WM_NCLBUTTONDBLCLK |
中键 | WM_NCMBUTTONDOWN | WM_NCMBUTTONUP | WM_NCMBUTTONDBLCLK |
右键 | WM_NCRBUTTONDOWN | WM_NCRBUTTONUP | WM_NCRBUTTONDBLCLK |
另外与客户区消息不同的是, 这里的 wParam 参数中的值与客户区中的含义有所不同, 这里的 wParam 表示非客户区鼠标移动或单击的位置, 他的值被设定成一些以 HT 开头的标识符中, 表示 "击中测试"(Hit Test), 关于击中测试与以HT开头的标识符将在下面讲到。
参数 lParam 中的值依然是鼠标指针的位置信息, 但此时的信息正好与客户区中的 lParam 的坐标信息相反, 客户区中的 lParam
的值是相对于窗口客户区的坐标, 而这里的(非客户区) lParam 中所包含的坐标信息是屏幕坐标, 在上面已经提到过, 使用ScreenToClient和ClientToScreen可以实现屏幕坐标与客户区坐标之间的转化。
处理非客户区左键单击示例:
switch(message) { case WM_PAINT: hdc = BeginPaint( hwnd, &ps ) ; EndPaint( hwnd, &ps ) ; return 0 ; case WM_NCLBUTTONDOWN: //处理非客户区鼠标左键单击事件 MessageBox( hwnd, TEXT("非客户区鼠标左键被单击"), TEXT("鼠标动作"), MB_OK ) ; return 0 ; case WM_DESTROY: PostQuitMessage(0) ; return 0 ; }
五、关于"击中测试"消息 WM_NCHITTEST
WM_NCHITTEST表示"非客户区击中测试", 所谓的击中测试就是测试鼠标当前所在的位置, 这个消息的优先级高于其他所有的客户区和非客户区消息, 参数 lParam 中包含相对于屏幕坐标的x值与y值, wParam 参数另有用途。
一般来说, WM_NCHITTEST 消息是交给 DefWindowProc
默认的消息处理函数进行处理的, 对于客户区中, Windows会利用 WM_NCHITTEST
消息来产生所有和其他鼠标位置相关的鼠标消息。对于非客户区消息来说, DefWindowProc 处理 WM_NCHITTEST 消息后返回一个
wParam 值, 这个值可以是任意一个非客户区鼠标消息的 wParam 参数的值, 这个 wParam 值用来判断鼠标的所在的位置。
举例来说, 如果 DefWindowProc 函数在处理 WM_NCHITTEST 消息后返回一个 HTCLIENT , HTCLIENT 表示鼠标在客户区, 这时Windows会将屏幕坐标转换成客户区坐标,并产生一个相关的客户区的鼠标消息;
当返回值为 HTCAPTION 表示鼠标此时在一个标题栏中, 所以Windows会将此时鼠标的坐标位置转成屏幕坐标并发送相关的非客户区消息。
这些返回的标识符定义在WINUSER.H头文件中, 相关的定义如下:
#define HTERROR (-2) //在屏幕的后面或在窗体之间的线上(使函数DefWindowProc产生一个警示音) #define HTTRANSPARENT (-1) //在一个被其它窗口覆盖的窗口中 #define HTNOWHERE 0 //在屏幕背景或窗口之间的分界线 #define HTCLIENT 1 //在客户区中 #define HTCAPTION 2 //在标题栏中 #define HTSYSMENU 3 //在一个窗口菜单栏或子窗口的关闭按钮上 #define HTGROWBOX 4 //在尺寸框中 #define HTSIZE HTGROWBOX //同HTGROWBOX #define HTMENU 5 //在菜单区域 #define HTHSCROLL 6 //在水平滚动条上 #define HTVSCROLL 7 //在垂直滚动条上 #define HTMINBUTTON 8 //在最小化按钮上 #define HTMAXBUTTON 9 //在最大化按钮上 #define HTLEFT 10 //在窗口的左边框上 #define HTRIGHT 11 //在窗口的右边框上 #define HTTOP 12 //在窗口水平边框的上方 #define HTTOPLEFT 13 //在窗口边框的左上角 #define HTTOPRIGHT 14 //在窗口边框的右上角 #define HTBOTTOM 15 //在窗口的水平边框的底部 #define HTBOTTOMLEFT 16 //在窗口边框的左下角 #define HTBOTTOMRIGHT 17 //在窗口边框的右下角 #define HTBORDER 18 //在不具有可变大小边框的窗口的边框上 #define HTREDUCE HTMINBUTTON //同HTMINBUTTON #define HTZOOM HTMAXBUTTON //同HTMAXBUTTON #define HTSIZEFIRST HTLEFT //同HTLEFT #define HTSIZELAST HTBOTTOMRIGHT //同HTBOTTOMRIGHT #define HTOBJECT 19 //忽略该标识符, 已废弃 #define HTCLOSE 20 //在关闭按钮上 #define HTHELP 21 //在帮助按钮上
这样在获取非客户区消息时我们就可以根据 wParam 中的值判断鼠标在窗口的位置了, 像这样:
case WM_NCLBUTTONDOWN: //处理非客户区的鼠标左键单击事件 x = LOWORD( lParam ) ; //通过lParam获取鼠标位置 y = HIWORD( lParam ) ; switch(wParam) //通过wParam判断鼠标在窗口的位置 { case HTCAPTION: //在标题上 wsprintf( szBuffer, "鼠标左键在标题栏中被单击, 击中位置: (%i, %i)", x, y ) ; MessageBox( hwnd, szBuffer, TEXT("鼠标动作"), MB_OK ) ; break ; case HTMINBUTTON: //在最小化按钮上 wsprintf( szBuffer, "鼠标左键在最小化按钮上被单击, 击中位置: (%i, %i)", x, y ) ; MessageBox( hwnd, szBuffer, TEXT("鼠标动作"), MB_OK ) ; break ; case HTMAXBUTTON: //在最大化按钮上 wsprintf( szBuffer, "鼠标左键在最大化按钮上被单击, 击中位置: (%i, %i)", x, y ) ; MessageBox( hwnd, szBuffer, TEXT("鼠标动作"), MB_OK ) ; break ; } return 0 ;
首先捕获 鼠标左键在非客户区的单击事件, 然后再通过 wParam 判断鼠标在窗口的位置, 这里获取鼠标位置是通过 LOWORD 和 HIWORD 宏完成的, 还有两个功能相同的宏也可以用来获取lParam中的鼠标信息, 他们是 GET_X_LPARAM 宏和 GET_Y_LPARAM, 不过这两个宏是定义在 WINDOWSX.H 头文件中的, 如果要使用这两个宏需要将 WINDOWSX.H 包含进来。
举例:
xPos = GET_X_LPARAM(lParam) ;
yPos = GET_Y_LPARAM(lParam) ;
--------------------
转自:http://www.cnblogs.com/mr-wid/archive/2012/11/30/2795739.html
微信公众号:
猿人谷
如果您认为阅读这篇博客让您有些收获,不妨点击一下右下角的【推荐】
如果您希望与我交流互动,欢迎关注微信公众号
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接。