Win32汇编教程 下

8.         图形界面的操作

有关GDI和位图

    GDI 即图形设备界面,是 Windows 最重要的部分之一,它大部分由 GDI32.DLL 库中的 API 来处理,GDI 的主要目的之一是支持与设备无关的图形编程,对于 Dos 下的图形编程,很多人可能“心有余悸”,因为PC 中有太多种类的显示卡,而几乎每个显示卡的处理都是不同的,即使后来有了 Vesa 编程,我们还是不能全部撇开具体的硬件,Windows GDI 使我们对图形的编程变得相对简单了很多,由于GDI 是 Windows 最庞大的部分,并不是几句话能讲清楚的,本节要讲的是 Windows 下GDI 的基本处理步骤和简单的位图处理,并没有涉及到 Directx 一类的编程。只希望能对朋友们有所启发。
    Windows 并不允许程序员访问显示硬件,它的所有对屏幕的操作是通过环境设备(DC)来处理的,屏幕上的每一个窗口对应一个DC,你可以把一个DC 想象成这个窗口的视频缓冲区,你对DC的操作结果会反映到屏幕上,在窗口的DC之外,你也可以自己建立DC,这相当于建立一个内存中的缓冲区,你对这个DC的操作结果保存在内存中。你也可以用 API 在不同的DC之间拷贝数据,比如说你可以在内存DC 中先建立好数据,然后拷贝到窗口的DC中,就相当于完成了屏幕的刷新。
    与DC的取得、建立取消有关的API有以下几种:

  1. GetDC(hWnd) - 取得某个窗口的DC,API 返回对应的 DC 句柄
  2. ReleaseDC(hWnd,hDC) - 释放用 GetDC 取得的 DC 句柄
  3. CreateCompatibleDC(hDC) - 从一个已知的 DC 句柄中建立一个内存 DC,各种参数、属性参考已知的 DC
  4. DeleteDC(hDC) - 删除用CreateCompatibleDC 建立的 DC

上面的4个API,必须成对出现,用 GetDC 取得的DC 必须用 ReleaseDC 释放,而用 CreateCompatibleDC 建立的 DC 必须用 DeleteDC 删除,不能混淆。DC 的作用范围:用 GetDC 取得的窗口 DC 必须尽快释放,你不应该在 Windows 的不同消息之间保存 DC 句柄,而用 CreateCompatibleDC 建立的 DC 可以长期保存,举例说明,如果你在 WM_PAINT 和 WM_SIZE 消息中都要对窗口的 DC 进行操作,你不能在 WM_INIT 时先 GetDC,然后保存句柄,最后在 WM_CLOSE 消息时 ReleaseDC,而是必须在 WM_PAINT 和 WM_SIZE 开始的地方 GetDC,在消息结束的地方就 ReleaseDC,而用 CreateCompatibleDC 建立的则相反,你可以在 WM_INIT 时建立,在 WM_CLOSE 时删除。
    如果想把一个位图画到 DC 中,你只需简单的用 invoke SelectObject,hDc,hBitmap 就行了,是不是很简单?但图形操作并不是单单把位图放入屏幕就行了,还要涉及到位的操作,如把前景位图的边缘去掉贴入背景位图等。 Windows 的 GDI 提供了下面一些 DC 间的拷贝 API,中间就包括了拷贝的模式:

  1. BitBlt hDcDest,XDest,YDest,Width,Height,hDcSource,XSrc,YSrc,dwRop
    这个 API 把 hDcSource 的 XSrc,YSrc 坐标处的内容拷贝到 hDcDest 的 XDest,YDest 处,拷贝大小为 Width,Height。
  2. PatBlt hDc,X,Y,Width,Height,dwRop 是用预定义的刷子等 Object 填充 DC
  3. StretchBlt,hDcDest,XDest,YDest,Width,Height,hDcSource,XSrc,YSrc,WidthSrc,HeightSrc,dwRop 是拷贝并自动缩放大小,你可以注意到它和 BitBlt 相比多了两个参数 WidthSrc 和 HeightSrc,别的都是一样的。

以上API 中的 dwRop 参数是最关键的,它的值有 SRCCOPY,SRCPAINT,SRCAND,DSTINVERT 等,表示源DC 拷贝到目标DC后象素的计算方法,SRCCOPY 表示用源DC覆盖目标DC,SRCPAINT是执行 OR 操作,SRCAND 是执行 AND 操作,DSTINVERT 是取反,举例说明,如果源DC中的某一点是黑色,目标DC对应的点是红色,那么用 SRCCOPY后,目标DC的点变成黑色,用SRCPAINT 后还是红色,因为黑 (000000) or 红(0000ff) =红(0000ff)。
     对应一般对屏幕或窗口进行图形操作的步骤如下。

  1. 用GetDC 取得目标窗口的 DC
  2. 用 CreateCompatibleDC 建立一个内存中的 DC用作缓冲区
  3. 用 SelectObject 填充内存DC 或别的办法对内存DC进行操作,一句话,先把要显示的东西处理好
  4. 用 BitBlt 把内存DC 拷贝到窗口 DC中,完成屏幕刷新。

本节的例子程序是一个屏幕放大镜,它把鼠标移动到的地方的屏幕内容放大一倍显示到自己的窗口中。

源程序 - 汇编源文件

;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;	Programmed by 罗云彬, bigluo@telekbird.com.cn
;	Website: http://asm.yeah.net
;	LuoYunBin's Win32 ASM page (罗云彬的编程乐园)
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;	版本信息
;	汇编教程附带源程序 - 屏幕放大器
;	   V1.0 ------	2000年7月1日
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

		.386
		.model flat, stdcall
		option casemap :none   ; case sensitive

;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;	Include 数据
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

include		windows.inc
include		user32.inc
include		kernel32.inc
include		comctl32.inc
include		comdlg32.inc
include		gdi32.inc

includelib	user32.lib
includelib	kernel32.lib
includelib	comctl32.lib
includelib	comdlg32.lib
includelib	gdi32.lib

;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;	Equ 数据
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

DLG_MAIN	equ		1000
ID_BITMAP	equ		1001

;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;	数据段
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

		.data?

hWinPic		dd	?
hDcMem		dd	?
hBitmap		dd	?
hWinDesktop	dd	?
hInstance	dd	?
szBuffer	db	256 dup	(?)

;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;	子程序声明
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
_ProcDlgMain	PROTO	:DWORD,:DWORD,:DWORD,:DWORD

		.data

;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;	代码段
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

		.code

include		Win.asm

;********************************************************************
_ProcDlgMain	proc	uses ebx edi esi, /
		hWnd:DWORD,wMsg:DWORD,wParam:DWORD,lParam:DWORD
		local	@stPoint:POINT
		local	@hDcDesktop,@hDcPic

		mov	eax,wMsg
		.if	eax == WM_CLOSE
			invoke	EndDialog,hWnd,NULL
			invoke	KillTimer,hWnd,1
			invoke	DeleteDC,hDcMem
			invoke	DeleteObject,hBitmap
; *******************************************************************
		.elseif	eax == WM_INITDIALOG
			invoke	GetDlgItem,hWnd,ID_BITMAP
			mov	hWinPic,eax
			invoke	GetDesktopWindow
			mov	hWinDesktop,eax
			invoke	SetWindowPos,hWnd,HWND_TOPMOST,0,0,0,0,/
				SWP_NOMOVE or SWP_NOSIZE
; *******************************************************************
			invoke	GetDC,hWinDesktop
			mov	@hDcDesktop,eax
			invoke	CreateCompatibleDC,@hDcDesktop
			mov	hDcMem,eax
			invoke	CreateCompatibleBitmap,@hDcDesktop,80,80
			mov	hBitmap,eax
			invoke	SelectObject,hDcMem,hBitmap
			invoke	ReleaseDC,hWinDesktop,@hDcDesktop
			invoke	SetTimer,hWnd,1,100,NULL
; *******************************************************************
		.elseif	eax == WM_TIMER
			invoke	GetCursorPos,addr @stPoint
			sub	@stPoint.x,20
			sub	@stPoint.y,20
			.if	@stPoint.x < 0
				mov	@stPoint.x,0
			.endif
			.if	@stPoint.y < 0
				mov	@stPoint.y,0
			.endif
			invoke	GetDC,hWinDesktop
			mov	@hDcDesktop,eax
			invoke	GetDC,hWinPic
			mov	@hDcPic,eax
			invoke	PatBlt,hDcMem,0,0,80,80,BLACKNESS
			invoke	StretchBlt,hDcMem,0,0,80,80,/
				@hDcDesktop,@stPoint.x,@stPoint.y,40,40,SRCCOPY
			invoke	BitBlt,@hDcPic,0,0,80,80,/
				hDcMem,0,0,SRCCOPY
			invoke	ReleaseDC,hWinDesktop,@hDcDesktop
			invoke	ReleaseDC,hWinPic,@hDcPic
		.else
;********************************************************************
;	注意:对话框的消息处理后,要返回 TRUE,对没有处理的消息
;	要返回 FALSE
;********************************************************************
			mov	eax,FALSE
			ret
		.endif		   
		mov	eax,TRUE
		ret
		
_ProcDlgMain	endp
;********************************************************************
start:
		invoke	GetModuleHandle,NULL
		mov	hInstance,eax
		invoke	DialogBoxParam,hInstance,DLG_MAIN,NULL,offset _ProcDlgMain,0
		invoke	ExitProcess,NULL

		end	start

程序的分析和要点

    在程序的初始化中,我们用GetDc 取的桌面的屏幕的 DC,再用 CreateCompatibleDC 建立一个内存DC做缓冲区,建立一个位图再用 SelectObject 把 hDcMem 设置为这个位图是为了是 hDcMem 的大小变为 80x80。

			invoke	GetDC,hWinDesktop
			mov	@hDcDesktop,eax
			invoke	CreateCompatibleDC,@hDcDesktop
			mov	hDcMem,eax
			invoke	CreateCompatibleBitmap,@hDcDesktop,80,80
			mov	hBitmap,eax
			invoke	SelectObject,hDcMem,hBitmap
			invoke	ReleaseDC,hWinDesktop,@hDcDesktop

然后在程序的每 0.1 秒一次的 WM_TIMER 定时器消息中,我们先用 GetDC 取得桌面和对话框中文本框的句柄,然后用 PatBlt 把内存DC清除为黑色,再用 StretchBlt 从桌面DC中拷贝 40x40的区域到内存 DC 中,新的大小是 80x80(放大功能就是这样实现的),拷贝的位置是用 GetCursorPos 取得的,也就是鼠标的当前位置,最后用 BitBlt 把内存DC 拷贝到对话框中。如果直接把桌面DC 拷贝到对话框中也可以,但是当鼠标移动到屏幕边缘上时,由于屏幕外的点是无效的,所以对话框中的一部分会花屏,大家可以改动程序试试。

			invoke	GetCursorPos,addr @stPoint
			invoke	GetDC,hWinDesktop
			mov	@hDcDesktop,eax
			invoke	GetDC,hWinPic
			mov	@hDcPic,eax
			invoke	PatBlt,hDcMem,0,0,80,80,BLACKNESS
			invoke	StretchBlt,hDcMem,0,0,80,80,/
				@hDcDesktop,@stPoint.x,@stPoint.y,40,40,SRCCOPY
			invoke	BitBlt,@hDcPic,0,0,80,80,/
				hDcMem,0,0,SRCCOPY
			invoke	ReleaseDC,hWinDesktop,@hDcDesktop
			invoke	ReleaseDC,hWinPic,@hDcPic






9.综合篇(一)复杂形状的窗口











概述

    在前面八篇的 Win32asm 教程中,已经初步讲述了消息框、对话框、菜单、资源、GDI 等内容,基本上已经设计到了 Windows 界面的大部分内容,
在继续新的 Windows 其他部分的内容如多线程、文件操作、内存操作之前,我先综合前面的内容并加上一些新内容,写上一篇综合篇。
本篇的例子程序是一个复杂形状的窗口,窗口的形状是根据位图自动计算得到的,这也就是在我编写的小闹钟中使用的技术(大家可以到我的软件发布中下载一个看看),
由于以前在网上看到的有关特殊形状窗口的例子最多就是画一个圆形,或者几个方块和椭圆结合的形状,
没有一篇文章指出如何画出如“唐老鸭”这样一个造型的窗口。本文使用的算法可以自动根据位图的形状计算窗口形状。
在源程序中,很多代码都是前面教程提到的,主要有以下部分:

  • 首先建立一个标准的窗口。(参考窗口一节)
  • 设置窗口为特殊形状。(见下面的程序分析)
  • 在窗口的 WM_PAINT 消息中更新窗口的图片。(参考图形界面一节)
  • 由于窗口没有标题栏,所以在右击窗口时弹出一个菜单。(参考菜单一节)
  • 菜单中有个“关于本程序”项,里面有超联结文本。(参考窗口子类化一节)

Windows 里有专门的 API 来实现特殊形状的窗口,步骤是首先建立区域(Region),Region 可以合并,
这样一来就可以用几个简单的区域合并出一个复杂的区域,建立、合并区域和设置窗口的 API 主要有以下几条:

  • CreateRectRgn(Left,Top,Right,Bottom) - 建立矩型区域
  • CreateEllipticRgn(Left,Top,Right,Bottom) - 建立椭圆区域
  • CreatePolygonRgn(lpPoints,NumberOfPoints,Mode) - 建立多边形区域,这些API返回区域句柄
  • CombineRgn(hDest,hSource1,hSource2,CombineMode) - 合并区域
  • SetWindowRgn(hWnd,hRgn,bRedraw) - 根据区域设置窗口形状

本程序的方法是扫描位图的点,按行设置区域,然后合并到总的区域中。

源程序 - 汇编源文件

;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;	是否包括调试代码
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
DEBUG		=	0
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;	Programmed by 罗云彬, bigluo@telekbird.com.cn
;	Website: http://asm.yeah.net
;	LuoYunBin's Win32 ASM page (罗云彬的编程乐园)
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;	版本信息
;	特殊形状窗口的演示程序 Ver 1.0
;	可以根据位图自动设置窗口的形状。
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

	.386
	.model flat, stdcall
	option casemap :none   ; case sensitive

;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;	Include 数据
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
	include		windows.inc
	include		user32.inc
	include		kernel32.inc
	include		comctl32.inc
	include		comdlg32.inc
	include		shell32.inc
	include		gdi32.inc

	includelib	user32.lib
	includelib	kernel32.lib
	includelib	comctl32.lib
	includelib	comdlg32.lib
	includelib	shell32.lib
	includelib	gdi32.lib
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;	Equ 数据
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;**************	Equ 数据 **********************************
IDI_MAIN	equ		1		;icon
IDC_HANDLE	equ		2		;Cursor
;**************	Equ 数据 **********************************
DLG_ABOUT	equ		1200		;dialog - about
ID_ABOUT_OK	equ		1201
ID_EMAIL	equ		1202
ID_HOMEPAGE	equ		1203
;**************	Equ 数据 **********************************
IDM_MAIN	equ		2000
IDM_ABOUT	equ		2001
IDM_EXIT	equ		2002
;**************	Equ 数据 **********************************
IDB_0		equ 		3000		;bitmap

;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;	数据段
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
		.data?
hInstance	dd		?
hWinMain	dd		?
hIcon		dd		?
hCursor		dd		?
hMenu		dd		?

hBmpBack	dd		?	;background bitmap
hDcBack		dd		?

;**************	数据段 ************************************
		.data

szClassName	db	'ShapeWindow',0

;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;	代码段
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

		.code

if		DEBUG
		include		Debug.asm
endif
;********************************************************************
;	设置窗口形状为BMP图形形状
;	参数:窗口句柄,BMP图形句柄
;	输入BMP图形要求:0,0处颜色为背景色
;********************************************************************
_SetWindowShape	proc	hWnd:DWORD,hBitMap:DWORD
		local	@hDC:DWORD,@hBmpDC:DWORD
		local	@stPs:PAINTSTRUCT
		local	@stRect:RECT
		local	@stBmp:BITMAP
		local	@dwX:DWORD,@dwY:DWORD,@dwStartX:DWORD
		local	@hRgn:DWORD,@hRgnTemp:DWORD
		local	@rgbBack:DWORD

		invoke	GetObject,hBitMap,sizeof BITMAP,addr @stBmp
		invoke	GetWindowRect,hWnd,addr @stRect
		invoke	ShowWindow,hWnd,SW_HIDE
		invoke	MoveWindow,hWnd,@stRect.left,@stRect.top,/
			@stBmp.bmWidth,@stBmp.bmHeight,FALSE

		invoke  GetDC,hWnd
		mov	@hDC,eax
		invoke	CreateCompatibleDC,@hDC
		mov	@hBmpDC,eax
		invoke	SelectObject,@hBmpDC,hBitMap
;*************** 计算窗口形状 ***************************************
		invoke	GetPixel,@hBmpDC,0,0
		mov	@rgbBack,eax
		invoke	CreateRectRgn,0,0,0,0
		mov	@hRgn,eax

		mov	@dwY,0
	.while	TRUE
		mov	@dwX,0
		mov	@dwStartX,-1
	   .while	TRUE
		invoke	GetPixel,@hBmpDC,@dwX,@dwY
		.if	@dwStartX == -1
		   .if	eax != @rgbBack
			mov	eax,@dwX
			mov	@dwStartX,eax
		   .endif
		.else
		   .if	eax == @rgbBack
			mov	ecx,@dwY
			inc	ecx
			invoke	CreateRectRgn,@dwStartX,@dwY,@dwX,ecx
			invoke	CombineRgn,@hRgn,@hRgn,eax,RGN_OR
			mov	@dwStartX,-1
		   .else
			mov	eax,@dwX
			.if	eax == @stBmp.bmWidth
				inc	eax
				mov	ecx,@dwY
				inc	ecx
				invoke	CreateRectRgn,@dwStartX,@dwY,eax,ecx
				invoke	CombineRgn,@hRgn,@hRgn,eax,RGN_OR
				mov	@dwStartX,-1
			.endif
		   .endif
		.endif
		inc	@dwX
		mov	eax,@dwX
		.break	.if eax > @stBmp.bmWidth
	   .endw
		inc	@dwY
		mov	eax,@dwY
		.break	.if eax > @stBmp.bmHeight
	.endw

		invoke	SetWindowRgn,hWnd,@hRgn,TRUE
;********************************************************************
		invoke	BitBlt,@hDC,0,0,@stBmp.bmWidth,@stBmp.bmHeight,/
			@hBmpDC,0,0,SRCCOPY
		invoke	DeleteDC,@hBmpDC
		invoke	ReleaseDC,hWnd,@hDC
		invoke	InvalidateRect,hWnd,NULL,-1

		ret

_SetWindowShape	endp
;********************************************************************
;	将窗口移动到屏幕中间
;	参数:窗口句柄
;********************************************************************
_CenterWindow	proc	hWnd:DWORD
		local	@stRectDeskTop:RECT,@stRectWin:RECT
		local	@dwWidth:DWORD,@dwHeight:DWORD

		invoke	GetWindowRect,hWnd,addr @stRectWin
		invoke	GetDesktopWindow
		mov	ebx,eax
		invoke	GetWindowRect,ebx,addr @stRectDeskTop

		mov	eax,@stRectWin.bottom
		sub	eax,@stRectWin.top
		mov	@dwHeight,eax
		mov	eax,@stRectWin.right
		sub	eax,@stRectWin.left
		mov	@dwWidth,eax

		mov	ebx,@stRectDeskTop.bottom
		sub	ebx,@dwHeight
		shr	ebx,1
		mov	ecx,@stRectDeskTop.right
		sub	ecx,@dwWidth
		shr	ecx,1

		invoke	MoveWindow,hWnd,ecx,ebx,@dwWidth,@dwHeight,FALSE
		ret

_CenterWindow	endp
;********************************************************************
include		About.asm

;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;	程序开始
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
start:
		call	_WinMain
		invoke	ExitProcess,NULL

;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;	主窗口程序
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
_WinMain	proc
		local	@stWcMain:WNDCLASSEX
		local	@stMsg:MSG

		invoke	InitCommonControls
		invoke	GetModuleHandle,NULL
		mov	hInstance,eax
		invoke	LoadIcon,hInstance,IDI_MAIN
		mov	hIcon,eax
		invoke	LoadMenu,hInstance,IDM_MAIN
		invoke	GetSubMenu,eax,0	;PopUp 菜单要用到子菜单
		mov	hMenu,eax
;*************** 注册窗口类 *****************************************
		invoke	LoadCursor,0,IDC_ARROW
		mov	@stWcMain.hCursor,eax
		mov	@stWcMain.cbSize,sizeof WNDCLASSEX
		mov	@stWcMain.hIconSm,0
		mov	@stWcMain.style,CS_HREDRAW or CS_VREDRAW
		mov	@stWcMain.lpfnWndProc,offset WndMainProc
		mov	@stWcMain.cbClsExtra,0
		mov	@stWcMain.cbWndExtra,0
		mov	eax,hInstance
		mov	@stWcMain.hInstance,eax
		mov	@stWcMain.hIcon,0
		mov	@stWcMain.hbrBackground,COLOR_WINDOW + 1
		mov	@stWcMain.lpszClassName,offset szClassName
		mov	@stWcMain.lpszMenuName,0
		invoke	RegisterClassEx,addr @stWcMain
;***************** 建立输出窗口	*****************************************
;	属性:没有标题栏,不显示在任务栏
;********************************************************************
		invoke	CreateWindowEx,WS_EX_TOOLWINDOW,/
			offset szClassName,NULL,/
			WS_POPUP or WS_SYSMENU,/
			0,0,1,1,/
			NULL,NULL,hInstance,NULL

		invoke	ShowWindow,hWinMain,SW_SHOWNORMAL
		invoke	UpdateWindow,hWinMain
;*************** 消息循环 *******************************************
		.while	TRUE
			invoke	GetMessage,addr @stMsg,NULL,0,0
			.break	.if eax	== 0
			invoke	TranslateMessage,addr @stMsg
			invoke	DispatchMessage,addr @stMsg
		.endw
		ret

_WinMain	endp
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
WndMainProc	proc	uses ebx edi esi, /
		hWnd:DWORD,uMsg:DWORD,wParam:DWORD,lParam:DWORD
		local	@stPos:POINT
		local	@stPs:PAINTSTRUCT,@hDC:DWORD

		mov	eax,uMsg
		.if	eax ==	WM_CREATE
			mov	eax,hWnd
			mov	hWinMain,eax
			call	_Init
;********************************************************************
		.elseif	eax == WM_PAINT
			invoke	BeginPaint,hWnd,addr @stPs
			mov	@hDC,eax

			mov	eax,@stPs.rcPaint.right
			sub	eax,@stPs.rcPaint.left
			mov	ecx,@stPs.rcPaint.bottom
			sub	ecx,@stPs.rcPaint.top

			invoke	BitBlt,@hDC,@stPs.rcPaint.left,@stPs.rcPaint.top,eax,ecx,/
				hDcBack,@stPs.rcPaint.left,@stPs.rcPaint.top,SRCCOPY

			invoke	EndPaint,hWnd,addr @stPs
;********************************************************************
;	由于没有菜单,下面代码用于按下右键时弹出POPUP菜单
;********************************************************************
		.elseif eax == WM_RBUTTONDOWN
		   .if wParam == MK_RBUTTON
			invoke	GetCursorPos,addr @stPos
			invoke	TrackPopupMenu,hMenu,TPM_LEFTALIGN,@stPos.x,@stPos.y,NULL,hWnd,NULL
		   .endif
;********************************************************************
;	由于没有标题栏,下面代码用于按下左键时移动窗口
;********************************************************************
		.elseif eax == WM_LBUTTONDOWN
			invoke	UpdateWindow,hWnd		;即时刷新
			invoke	ReleaseCapture
			invoke	SendMessage,hWnd,WM_NCLBUTTONDOWN,HTCAPTION,0
;********************************************************************
		.elseif	eax ==	WM_COMMAND
		   .if	lParam == 0
			mov	eax,wParam
			.if	ax == IDM_EXIT
				call	_Quit
			.elseif	ax == IDM_ABOUT
				invoke	DialogBoxParam,hInstance,DLG_ABOUT,hWnd,offset AboutDialogProc,DLG_ABOUT
			.endif
		   .endif
;********************************************************************
		.elseif	eax ==	WM_CLOSE
			call	_Quit
;********************************************************************
		.else
			invoke	DefWindowProc,hWnd,uMsg,wParam,lParam
			ret
		.endif
;********************************************************************
;	注意:WndProc 处理 Windows 消息后,必须在 Eax 中返回 0
;	但是由 DefWindowProc 处理后的返回值不能改变,否则窗口
;	将无法显示!
;********************************************************************
		xor	eax,eax
		ret

WndMainProc	endp


;********************************************************************
_Init		proc
		local	@hDC

		invoke	SendMessage,hWinMain,WM_SETTEXT,0,offset szClassName
		invoke	SendMessage,hWinMain,WM_SETICON,ICON_SMALL,hIcon

		invoke	LoadBitmap,hInstance,IDB_0	;装入背景图片
		mov	hBmpBack,eax
		invoke	_SetWindowShape,hWinMain,hBmpBack	;设置窗口形状为背景图片
		invoke  GetDC,hWinMain
		mov	@hDC,eax
		invoke	CreateCompatibleDC,@hDC			;建立背景及数字 DC
		mov	hDcBack,eax
		invoke	ReleaseDC,hWinMain,@hDC
		invoke	SelectObject,hDcBack,hBmpBack
		invoke	_CenterWindow,hWinMain

		ret

_Init		endp
;********************************************************************
_Quit		proc
		local	@stWindow:RECT

		invoke	DestroyMenu,hMenu
		invoke	DeleteDC,hDcBack
		invoke	DeleteObject,hBmpBack
		invoke	DestroyWindow,hWinMain
		invoke	PostQuitMessage,NULL

		ret

_Quit		endp
;********************************************************************
		end	start

程序的分析和要点

    创建窗口的时候,窗口风格为 WS_POPUP,所以创建的窗口没有标题栏,这样的窗口适合于设置成特殊形状的窗口

		invoke	CreateWindowEx,WS_EX_TOOLWINDOW,/
			offset szClassName,NULL,/
			WS_POPUP or WS_SYSMENU,/
			0,0,1,1,/
			NULL,NULL,hInstance,NULL

但是当窗口没有标题栏后,我们就无法用拖动标题栏的办法来移动窗口,如果让窗口一动不动呆在屏幕中间显然是不行的,这里有一个替代办法,
我们可以响应按下鼠标左键的消息,在 WM_LBUTTONDOWN 消息中想窗口发送 WM_NCLBUTTONDOWN (非客户区鼠标按下消息) 位置在 HTCAPTION 来模拟鼠标按在标题栏中来实现移动的功能。

		.elseif eax == WM_LBUTTONDOWN
			invoke	UpdateWindow,hWnd		;即时刷新
			invoke	ReleaseCapture
			invoke	SendMessage,hWnd,WM_NCLBUTTONDOWN,HTCAPTION,0








10. 定时器的应用

概述

    Windows 的定时器是一种输入设备,它周期性地在指定的间隔时间通知应用程序。它可以用向指定窗口发送 WM_TIMER 消息或者调用指定的过程来执行用户的程序。定时器的应用主要包括下面一些地方:

  1. 时钟程序 - 显然,这是定时器最直接的应用。
  2. 多任务 - 如果程序有大量的数据处理,除了用多线程的办法,还可以用定时器,在每一个定时器消息中处理一小块内容。
  3. 定时显示程序的状况 - 定时器就相当于 Dos 编程中的自己挂接在 int 1ch 上面的要定时处理的程序,它可以定时显示程序运行的情况,如发送了多少内容,接收了多到内容等等。
  4. 在游戏程序中使用定时器可以消除在不同处理器下用延时来保持速度一致所造成的误差。
  5. 用于数据流处理 - 在音频、视频的播放中,需要隔一段时间处理一段数据。

总的来说,在 Dos 下实现精确定时的唯一方法是在 int 1ch 时钟中断中处理程序,但你使用起来必须遵守很多的规范,而在 Windows 的定时器中,你可以用 SetTimer 函数分配不止一个的定时器,比如说,在你的文本编辑程序中,你可以使用一个间隔1秒的定时器来在状态栏中显示时钟,
同时分配一个10分钟的定时器来实现定时存盘的功能。定时器实际上是 Windows 对时钟中断的一种扩展,
它的本质还是基于时钟中断的,所以你实际上无法把定时器的间隔设置到55毫秒以下,另外,定时器的精度也是以55毫秒为倍数的,比如说,你设置了一个1秒的定时器,它实际上是在每989毫秒的时候发生的。和在 Dos 下使用时钟中断,windows 的定时器还有下面一些要点:

  1. 在 Dos 中,你的程序随时可能被 int 1ch 打断,而在Windows 中,Windows 通过 WM_TIMER 消息把定时器消息放入正常的消息队列中,所以你不必担心你的程序在别的处理中被定时器打断。
  2. 不可能有同时两条以上的 WM_TIMER 消息,如果在一个还在消息队列中,窗口再得到一条 WM_TIMER 消息,两条消息会被合并为一条,所以在程序比较忙的时候可能会丢失 WM_TIMER 消息。
  3. WM_TIMER 消息的级别是很低的,程序只有在消息队列中没有其他消息的情况下,才会接收 WM_TIMER 消息,你可以通过下马方法验证:在一个设置了定时器的窗口上按住标题栏移动窗口,你会发现定时器停止了工作,当你松开鼠标后,在这个过程中丢失的 WM_TIMER 消息并没有被补上,
  4. 所以如果你设计一个时钟程序,你不能使用定时器消息来计数,而必须在消息中每次获取正确的系统时间。

讲了这么多定时器的特点,下面是定时器相关的API,你会发现除了在使用中要注意的这些特性,定时器的API真是又少又简单:

  1. 建立定时器
    SetTimer(
    HWND hWnd, // handle of window for timer messages
    UINT nIDEvent, // timer identifier
    UINT uElapse, // time-out value
    TIMERPROC lpTimerFunc // address of timer procedure
    );
    hWnd 是 windows 发送 WM_TIMER 的窗口,nIDEvent 是定时器的编号,在 WM_TIMER 中出现在 wParam 参数中,
  2. 用来区分在多个定时器的情况下,这条消息是由哪个定时器产生的。uElapse 是定时器间隔的毫秒数,
  3. 如果你要设置一个1秒的定时器,这个值就是1000,lpTimerFunc 是处理定时器消息的过程,
  4. 如果这个参数不是 NULL,windows 在到时间后会调用lpTimerFunc 指定的过程,
  5. 调用的参数是 CALLBACK TimerProc(hwnd,WM_TIMER,iTimerID,dwTime),iTimerID 是定时器 ID,dwTime 是系统时间;
  6. 如果 lpTimerFunc 参数是 NULL,Windows 会把 WM_TIMER 消息放入消息循环中,消息的 hWnd 是第一个参数中指定的 hWnd,也就是说向这个窗口发送了 WM_TIMER 消息。
    另外,如果你的程序没有窗口,你也可以用这种办法建立定时器:invoke SetTimer,NULL,NULL,uElapse,TimerProc,函数会返回一个系统定义的 TimerID供你在 KillTimer 中使用。
  7. 取消定时器
    KillTimer(
    HWND hWnd, // handle of window that installed timer
    UINT uIDEvent // timer identifier
    );
    取消定时器只需对应 SetTimer 时的 hWnd 和 uIDEvent 调用 KillTimer 函数就行了。

在本节的例子程序中,我在对话框中的 WM_INIT 消息中用 SetTimer 建立两个定时器,时间分别是500ms 和 200ms,
然后在间隔0.5秒的定时器消息中更换按钮上的图片,在间隔 0.2 秒的定时器消息中更换标题栏上的小图标,你就可以看到动画的效果了。

源程序 - 汇编源文件

;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;	Programmed by 罗云彬, bigluo@telekbird.com.cn
;	Website: http://asm.yeah.net
;	LuoYunBin's Win32 ASM page (罗云彬的编程乐园)
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;	版本信息
;	汇编教程附带例子程序 - 定时器的使用
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

		.386
		.model flat, stdcall
		option casemap :none   ; case sensitive

;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;	Include 数据
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

include		windows.inc
include		user32.inc
include		kernel32.inc
include		comctl32.inc
include		comdlg32.inc

includelib	user32.lib
includelib	kernel32.lib
includelib	comctl32.lib
includelib	comdlg32.lib

;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;	Equ 数据
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

IDI_MAIN	equ		1
IDI_MOON1	equ		2
IDI_MOON2	equ		3
IDI_MOON3	equ		4
IDI_MOON4	equ		5
IDI_MOON5	equ		6
IDI_MOON6	equ		7
IDI_MOON7	equ		8
IDI_MOON8	equ		9

DLG_MAIN	equ		1000
ID_MOON		equ		1001

ID_TIMER1	equ		1
ID_TIMER2	equ		2
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;	数据段
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

		.data?

hInstance	dd	?
dwCounter1	dd	?
dwCounter2	dd	?
hIcon1		dd	?
hIcon2		dd	?
hIcon3		dd	?
hIcon4		dd	?
hIcon5		dd	?
hIcon6		dd	?
hIcon7		dd	?
hIcon8		dd	?
szBuffer	db	256 dup	(?)

		.data

;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;	代码段
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

		.code
;********************************************************************
_ProcDlgMain	proc	uses ebx edi esi, /
		hWnd:DWORD,wMsg:DWORD,wParam:DWORD,lParam:DWORD

		mov	eax,wMsg
;********************************************************************
		.if	eax ==	WM_CLOSE
			invoke	KillTimer,hWnd,ID_TIMER1
			invoke	KillTimer,hWnd,ID_TIMER2
			invoke	EndDialog,hWnd,NULL
;********************************************************************
		.elseif	eax ==	WM_INITDIALOG
			mov	edi,offset hIcon1
			mov	ebx,IDI_MOON1
			mov	ecx,8
			@@:
			push	ecx
			invoke	LoadIcon,hInstance,ebx
			cld
			stosd
			inc	ebx
			pop	ecx
			loop	@B
			invoke	SetTimer,hWnd,ID_TIMER1,500,NULL
			invoke	SetTimer,hWnd,ID_TIMER2,200,NULL
			invoke	SendMessage,hWnd,WM_SETICON,ICON_SMALL,hIcon1
;********************************************************************
		.elseif	eax ==	WM_TIMER
			.if	wParam == ID_TIMER1
				inc	dwCounter1
				.if	dwCounter1 == 8
					mov	dwCounter1,0
				.endif
				mov	eax,dwCounter1
				shl	eax,2
				add	eax,offset hIcon1
				mov	eax,[eax]
				invoke	SendMessage,hWnd,WM_SETICON,ICON_SMALL,eax
			.else
				inc	dwCounter2
				.if	dwCounter2 == 8
					mov	dwCounter2,0
				.endif
				mov	eax,dwCounter2
				shl	eax,2
				add	eax,offset hIcon1
				mov	eax,[eax]
				invoke	SendDlgItemMessage,hWnd,ID_MOON,BM_SETIMAGE,IMAGE_ICON,eax
			.endif
;********************************************************************
		.else
			mov	eax,FALSE
			ret
		.endif		   
		mov	eax,TRUE
		ret
		
_ProcDlgMain	endp
;********************************************************************
start:
		invoke	GetModuleHandle,NULL
		mov	hInstance,eax
		invoke	DialogBoxParam,hInstance,DLG_MAIN,NULL,offset _ProcDlgMain,0
		invoke	ExitProcess,NULL
;********************************************************************
		end	start

程序的分析和要点

    有了上面的介绍,这个程序是很容易看懂的,在 WM_TIMER 消息中,通过 wParam 中的 TimerID 可以区分是哪个定时器产生的消息。在 WM_CLOSE 消息中,通过 KillTimer 来取消定时器。本程序中的的图标定义在资源文件中,
在对话框建立的时候,先用 LoadIcon 装入,然后为两个定时器分别保存一个图片编号 dwCounter1 和 dwCounter2,在定时器消息中分别用 WM_SETICON 和 BM_SETIMAGE 消息来对窗口标题的图标和按钮的图标进行设置。





11. 进程控制

概述

进程控制简单的说相当于在一个程序中执行另一个程序,你可以把它想象成在 Dos 下用 int 21h/4bh 功能来执行另外一个程序,如果单从执行另一个程序的目的来讲,在 Windows 中有不少方法,如使用 ShellExecute 等,
但这些 Api 仅仅是“执行”而已,进程控制的意义在于可以创建一个进程,并可以通过进程句柄结束进程,同样你也可以通过进程句柄来跟踪程序,还可以用 ReadProcessMemory 和 WriteProcessMemory 来读写子进程的内存空间。

进程控制要使用的相关 API 有下面这些:

创建进程的函数为CreateProcess,该函数比较复杂,共有十个参数,但有个好消息是使用时大部分可以用 NULL。

BOOL CreateProcess(
LPCTSTR lpApplicationName, // 执行程序文件名
LPTSTR lpCommandLine, // 参数行
LPSECURITY_ATTRIBUTES lpProcessAttributes, // 进程安全参数
LPSECURITY_ATTRIBUTES lpThreadAttributes, // 线程安全参数
BOOL bInheritHandles, // 继承标记
DWORD dwCreationFlags, // 创建标记
LPVOID lpEnvironment, // 环境变量
LPCTSTR lpCurrentDirectory, // 运行该子进程的初始目录
LPSTARTUPINFO lpStartupInfo, // 创建该子进程的相关参数
LPPROCESS_INFORMATION lpProcessInformation // 创建后用于被创建子进程的信息
);


各个参数的说明如下:

  • lpApplicationName:为执行程序的文件名,你也可以把执行文件名包括在下一个参数 lpCommandLine 中,然后把该参数置为NULL。
  • lpCommandLine:为参数行,如果无参数可以为NULL,在有参数传递给进程时可以如下设置:lpApplicationName=文件名;lpCommandLine=参数,或者 lpApplicationName=NULL;lpCommandLine=文件名 + 参数。
  • lpProcessAttributes,lpThreadAttributes:分别描述了创建的进程和线程安全属性,如果使用NULL表示使用默认的安全描述。
  • bInheritHandles:表示当前进程中的打开的句柄是否能够被创建的子进程所继承。
  • dwCreationFlags:表示创建标记,通过该标记可以设置进程的创建状态和优先级别。常用的有下面的标记:
    CREATE_NEW_CONSOLE:为子进程创建一个新的控制台。
    CREATE_SUSPENDED:子进程在创建时为挂起状态。如果指定了这个参数,那么执行 CreateProcess 后进程只是被装入内存,但不是马上开始执行,而是必须等主程序调用 ResumeThread 后才继续执行。
    HIGH_PRIORITY_CLASS/NORMAL_PRIORITY_CLASS:高/普通优先级别。
  • lpEnvironment:表示子进程所使用的环境变量,如果为NULL,则表示与当前进程使用相同的环境变量。
  • lpCurrentDirectory:表示子进程运行的初始目录。
  • lpStartupInfo:STARTUPINFO 结构,用于在创建子进程时设置各种属性。
  • lpProcessInformation:PROCESS_INFORMATION 结构,用来在进程创建后接收相关信息,该结构由系统填写。

调用 CreateProcess 函数有三个参数是必需的,一在 lpApplicationName 或 lpCommandLine 指定文件名,二是 lpStartupInfo 结构,三是 PROCESS_INFORMATION 结构,
因为 PROCESS_INFORMATION 结构返回了进程建立后的句柄,以后的一切操作将要用到这些返回的句柄,它是由系统填写的,结构说明如下:

typedef struct _PROCESS_INFORMATION {
HANDLE hProcess; //进程句柄
HANDLE hThread; //进程的主线程句柄
DWORD dwProcessId; //进程ID
DWORD dwThreadId; //进程的主线程ID
} PROCESS_INFORMATION;

另外还有一个关键的结构 STARTUPINFO,该结构定义如下:

typedef struct STARTUPINFO {
DWORD cb; //结构长度
LPTSTR lpReserved; //保留
LPTSTR lpDesktop; //保留
LPTSTR lpTitle; //如果为控制台进程则为显示的标题
DWORD dwX; //窗口位置
DWORD dwY; //窗口位置
DWORD dwXSize; //窗口大小
DWORD dwYSize; //窗口大小
DWORD dwXCountChars; //控制台窗口字符号宽度
DWORD dwYCountChars; //控制台窗口字符号高度
DWORD dwFillAttribute; //控制台窗口填充模式
DWORD dwFlags; //创建标记
WORD wShowWindow; //窗口显示标记,如同ShowWindow中的标记
WORD cbReserved2; //
LPBYTE lpReserved2; //
HANDLE hStdInput; //标准输入句柄
HANDLE hStdOutput; //标准输出句柄
HANDLE hStdError; //标准错误句柄
} STARTUPINFO, *LPSTARTUPINFO;

结构中 dwFlags 指定了其它的一些字段是否有效,如:dwFlags包含 STARTF_USESIZE 表示dwXSize和dwYSize有效,
包含STARTF_USEPOSITION表示dwX和dwY有效,等等。如果不是有特殊的要求,我们不用自己去填写这个结构,
只需用 GetStartupInfo 让 Windows 为你填写好了,这样,建立一个进程的语句就是:

...
stStartUp STARTUPINFO stProcInfo PROCESS_INFORMATION <?>
stProcInfo PROCESS_INFORMATION <?>
...

invoke GetStartupInfo,addr stStartUp
invoke CreateProcess,NULL,addr szFileName,NULL,NULL,NULL,NORMAL_PRIORITY_CLASS,NULL,NULL,offset stStartUp,offset stProcInfo

...

如果成功的话,eax 将返回非零值,注意返回在 PROCESS_INFORMATION 结构中的 hProcess,以后很多的操作都要用到它。

强制结束一个进程的 API 为 TerminateProcess

BOOL TerminateProcess(
HANDLE hProcess, // 进程句柄
UINT uExitCode // 退出代码
);

你可以使用语句 invoke TerminateProcess,structProcInfo.hProcess,0 来结束进程,要注意的是如果可能的话,尽量不要在程序中强制结束别的进程,因为使用 TerminateProcess 结束的进程,它装载的 dll 不能被正确卸载。
这样可能会引起系统资源的无效占用。最好的办法在进程中自己使用 ExitProcess 退出。

查询一个进程状态的 API 为 GetExitCodeProcess。

BOOL GetExitCodeProcess(
HANDLE hProcess, // handle to the process
LPDWORD lpExitCode // address to receive termination status
);

如果进程尚未退出,函数将会返回STILL_ACTIVE。这个 API 是马上返回的。

等待进程执行可以用 WaitForSingleObject

这个 API 并不是单用于进程的等待,其它还可以用在线程等操作,但我们一般用它来等待进程的执行,它的申明是:

DWORD WaitForSingleObject(
HANDLE hHandle, // handle of object to wait for
DWORD dwMilliseconds // time-out interval in milliseconds
);

如果我们要等待进程执行 1 秒钟,可以 invoke WaitForSingleObject,stProcInfo.hProcess,1000 如果要等到进程结束,
可以用 WaitForSingleObject,stProcInfo.hProcess,INFINITE ,参数 2 中的 INFINITE 在 Windows.inc 中有定义,意思是无穷等待。

最后,当不再使用进程句柄的时候,不要忘了使用 CloseHandle 关闭 hProcess 和 hThread,否则会浪费系统句柄的资源。

源程序 - 汇编源文件


	.386
	.model flat, stdcall
	option casemap :none   ; case sensitive
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;	Include 数据
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
include		windows.inc
include		user32.inc
include		kernel32.inc
include		comctl32.inc
include		comdlg32.inc
include		gdi32.inc

includelib	user32.lib
includelib	kernel32.lib
includelib	comctl32.lib
includelib	comdlg32.lib
includelib	gdi32.lib
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;	Equ 数据
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
DLG_MAIN	equ	3000
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
ID_BROWSE	equ	3001
ID_RUN		equ	3002
ID_EXIT		equ	3003
ID_TEXT		equ	3004

F_RUNNING	equ	0001h	;进程在运行中
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;	数据段
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
		.data?

stStartUp	STARTUPINFO		<?>
stProcInfo	PROCESS_INFORMATION	<?>
stOpenFileName	OPENFILENAME	<?>

hRunThread	dd	?
hInstance	dd	?
hWinMain	dd	?
hIcon		dd	?
szBuffer	db	512 dup	(?)

dwFlag		dd	?
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

		.data

szExcute	db	'执行(&E)',0		;按钮文字
szKill		db	'终止(&E)',0
szExcuteError	db	'启动应用程序错误!',0

szTitleOpen	db	"Open executable file...",0
szExt		db	'*.exe',0
szFilter	db	'Excutable Files',0,'*.exe;*.com',0
		db	0

;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;	代码段
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
		.code

if		DEBUG
		include		Debug.asm
endif
include		Win.asm

;********************************************************************
; 执行程序用的线程
; 1. 用 CreateProcess 建立进程
; 2. 用 WaitForSingleOject 等待进程结束
;********************************************************************
_RunThread	proc	uses ebx ecx edx esi edi,/
		dwParam:DWORD

		or	dwFlag,F_RUNNING
;********************************************************************
; 取消“退出”按钮并把“执行”按钮改为“中止”
;********************************************************************
		invoke	GetDlgItem,hWinMain,ID_EXIT
		invoke	EnableWindow,eax,FALSE
		invoke	SendDlgItemMessage,hWinMain,ID_RUN,WM_SETTEXT,0,offset szKill

;********************************************************************
; 执行文件,如果成功则等待程序结束
;********************************************************************
		invoke	GetStartupInfo,addr stStartUp
		invoke	CreateProcess,NULL,addr szBuffer,NULL,NULL,/
			NULL,NORMAL_PRIORITY_CLASS,NULL,NULL,offset stStartUp,offset stProcInfo
		.if	eax !=	0
			invoke	WaitForSingleObject,stProcInfo.hProcess,INFINITE
			invoke	CloseHandle,stProcInfo.hProcess
			invoke	CloseHandle,stProcInfo.hThread
		.else
			invoke	MessageBox,hWinMain,addr szExcuteError,NULL,MB_OK or MB_ICONERROR
		.endif
;********************************************************************
; Enable “退出”按钮并把“中止”按钮改为“执行”
;********************************************************************
		invoke	GetDlgItem,hWinMain,ID_EXIT
		invoke	EnableWindow,eax,TRUE
		invoke	SendDlgItemMessage,hWinMain,ID_RUN,WM_SETTEXT,0,offset szExcute
		and	dwFlag,not F_RUNNING
		ret

_RunThread	endp

;********************************************************************
;	窗口程序
;********************************************************************
DialogMainProc	proc	uses ebx edi esi, /
		hWnd:DWORD,wMsg:DWORD,wParam:DWORD,lParam:DWORD

		mov	eax,wMsg
;********************************************************************
		.if	eax ==	WM_INITDIALOG
			mov	eax,hWnd
			mov	hWinMain,eax
			call	_Init
;********************************************************************
		.elseif	eax ==	WM_CLOSE
			invoke	EndDialog,hWinMain,NULL
;********************************************************************
		.elseif	eax ==	WM_COMMAND
			mov	eax,wParam
			.if	ax ==	ID_BROWSE
				call	_BrowseFile
				call	_CheckText
			.elseif	ax ==	ID_TEXT
				invoke	GetDlgItemText,hWinMain,ID_TEXT,addr szBuffer,512
				call	_CheckText
			.elseif	ax ==	ID_RUN
;********************************************************************
; 如果没有在执行中(dwFlag 没有置位) 则建立线程,在线程中执行程序
; 如果已经在执行中,则用 TerminateProcess 终止执行
;********************************************************************
				test	dwFlag,F_RUNNING
				.if	ZERO?
					invoke	CreateThread,NULL,NULL,offset _RunThread,/
					NULL,NULL,offset hRunThread
				.else
					invoke	TerminateProcess,stProcInfo.hProcess,-1
				.endif
			.elseif	ax ==	ID_EXIT
				invoke	EndDialog,hWinMain,NULL
			.endif
		.else
;********************************************************************
;	注意:对话框的消息处理后,要返回 TRUE,对没有处理的消息
;	要返回 FALSE
;********************************************************************
			mov	eax,FALSE
			ret
		.endif
		mov	eax,TRUE
		ret

DialogMainProc	endp
;********************************************************************
; 程序入口
;********************************************************************
start:
		invoke	InitCommonControls
		invoke	GetModuleHandle,NULL
		mov	hInstance,eax
		invoke	DialogBoxParam,hInstance,DLG_MAIN,NULL,offset DialogMainProc,0
		invoke	ExitProcess,NULL
;********************************************************************
_Init		proc

		invoke	_CenterWindow,hWinMain

		invoke	SendDlgItemMessage,hWinMain,ID_TEXT,EM_LIMITTEXT,512,NULL
		invoke	GetDlgItem,hWinMain,ID_RUN
		invoke	EnableWindow,eax,FALSE

		ret

_Init		endp
;********************************************************************
; 根据 text control 中有无字符决定是否将“执行”按钮 Disable 掉
;********************************************************************
_CheckText	proc

		invoke	GetDlgItemText,hWinMain,ID_TEXT,addr szBuffer,512
		invoke	lstrlen,addr szBuffer
		.if	eax != 0 || (dwFlag & F_RUNNING)
			invoke	GetDlgItem,hWinMain,ID_RUN
			invoke	EnableWindow,eax,TRUE
		.else
			invoke	GetDlgItem,hWinMain,ID_RUN
			invoke	EnableWindow,eax,FALSE
		.endif
		ret

_CheckText	endp
;********************************************************************
_BrowseFile	proc

		mov	stOpenFileName.Flags,OFN_PATHMUSTEXIST or OFN_FILEMUSTEXIST
		mov	stOpenFileName.lStructSize,SIZEOF stOpenFileName
		mov	eax,hWinMain
		mov	stOpenFileName.hwndOwner,eax
		mov	stOpenFileName.lpstrFilter,offset szFilter	;扩展名
		mov	stOpenFileName.lpstrFile,offset szBuffer	;文件名缓冲
		mov	stOpenFileName.nMaxFile,512			;文件名缓冲长度
		mov	stOpenFileName.lpstrInitialDir,0
		mov	stOpenFileName.lpstrTitle,offset szTitleOpen
		mov	stOpenFileName.lpstrDefExt,offset szExt
		invoke	GetOpenFileName,offset stOpenFileName
		.if	eax == FALSE
			ret
		.endif
		invoke	SetDlgItemText,hWinMain,ID_TEXT,addr szBuffer
		ret

_BrowseFile	endp
;********************************************************************
		end	start


程序的分析和要点

    本程序在使用调用 GetOpenFileName 或者自己在文本框中输入执行文件名,然后通过 CreateProcess 建立进程,
最后用 WaitForSingleObject 等待进程结束,如果在对话框的处理过程中等待会导致程序在进程返回前无法响应,
所以程序中用 CreateThread 建立一个线程来实现这个过程,当子过程返回的时候,线程结束。dwFlag 中的 0 位作为标志位,
表示是否子过程在运行中,如果这一位置 1 的话,按下“终止”按钮会用 TerminateProcess 来强制终止子进程。



12. 管道操作

概述

Windows 引入了多进程和多线程机制。同时也提供了多个进程之间的通信手段,包括剪贴板、DDE、OLE、管道等,
和其他通信手段相比,管道有它自己的限制和特点,管道实际上是一段共享内存区,进程把共享消息放在那里。
并通过一些 API 提供信息交换。管道是两个头的东西,每个头各连接一个进程或者同一个进程的不同代码,
按照管道的类别分有两种管道,匿名的和命名的;按照管道的传输方向分也可以分成两种,单向的双向的。
根据管道的特点,命名管道通常用在网络环境下不同计算机上运行的进程之间的通信(当然也可以用在同一台机的不同进程中)
它可以是单向或双向的;而匿名管道只能用在同一台计算机中,它只能是单向的。匿名管道其实是通过用给了一个指定名字的有名管道来实现的。
使用管道的好处在于:读写它使用的是对文件操作的 api,结果操作管道就和操作文件一样。即使你在不同的计算机之间用命名管道来通信,
你也不必了解和自己去实现网络间通信的具体细节。

我们简单的介绍一下命名管道的使用。

命名管道是由服务器端的进程建立的,管道的命名必须遵循特定的命名方法,就是 "//./pipe/管道名",当作为客户端的进程要使用时,使用"//计算机名//pipe/管道名" 来打开使用,具体步骤如下:

  1. 服务端通过函数 CreateNamedPipe 创建一个命名管道的实例并返回用于今后操作的句柄,或为已存在的管道创建新的实例。
  2. 服务端侦听来自客户端的连接请求,该功能通过 ConnectNamedPipe 函数实现。
  3. 客户端通过函数 WaitNamedPipe 来等待管道的出现,如果在超时值变为零以前,有一个管道可以使用,则 WaitNamedPipe 将返回 True,并通过调用 CreateFile 或 CallNamedPipe 来呼叫对服务端的连接。
  4. 此时服务端将接受客户端的连接请求,成功建立连接,服务端 ConnectNamedPipe 返回 True
  5. 建立连接之后,客户端与服务器端即可通过 ReadFile 和 WriteFile,利用得到的管道文件句柄,彼此间进行信息交换。
  6. 当客户端与服务端的通信结束,客户端调用 CloseFile,服务端接着调用 DisconnectNamedPipe。最后调用函数CloseHandle来关闭该管道。

由于命名管道使用时作为客户端的程序必须知道管道的名称,所以更多的用在同一“作者”编写的服务器/工作站程序中,你不可能随便找出一个程序来要求它和你写的程序来通过命名管道通信。
而匿名管道的使用则完全不同,它允许你和完全不相干的进程通信,条件是这个进程通过控制台“console”来输入输出,典型的例子是老的 Dos 应用程序,
它们在运行时 Windows 为它们开了个 Dos 窗口,它们的输入输出就是 console 方式的。还有一些标准的 Win32 程序也使用控制台输入输出,如果在 Win32 编程中不想使用图形界面,
你照样可以使用 AllocConsole 得到一个控制台,然后通过 GetStdHandle 得到输入或输出句柄,再通过 WriteConsole 或 WriteFile 把结果输出到控制台(通常是一个象 Dos 窗口)的屏幕上。
虽然这些程序看起来象 Dos 程序,但它们是不折不扣的 Win32 程序,如果你在纯 Dos 下使用,就会显示“The program must run under Windows!”。

一个控制台有三个句柄:标准输入、标准输出和和标准错误句柄,标准输入、标准输出句柄是可以重新定向的,你可以用匿名管道来代替它,这样一来,你可以在管道的另一端用别的进程来接收或输入,
而控制台一方并没有感到什么不同,就象 Dos 下的 > 或者 < 可以重新定向输出或输入一样。通常控制台程序的输入输出如下:

(控制台进程output) write ----> 标准输出设备(一般是屏幕)
(控制台进程input) read <---- 标准输入设备(一般是键盘)

而用管道代替后:

(作为子进程的控制台进程output) write ----> 管道1 ----> read (父进程)
(作为子进程的控制台进程input) read <----> 管道2 <---- write (父进程)

使用匿名管道的步骤如下:

  1. 使用 CreatePipe 建立两个管道,得到管道句柄,一个用来输入,一个用来输出
  2. 准备执行控制台子进程,首先使用 GetStartupInfo 得到 StartupInfo
  3. 使用第一个管道句柄代替 StartupInfo 中的 hStdInput,第二个代替 hStdOutput、hStdError,即标准输入、输出、错误句柄
  4. 使用 CreateProcess 执行子进程,这样建立的子进程输入和输出就被定向到管道中
  5. 父进程通过 ReadFile 读第二个管道来获得子进程的输出,通过 WriteFile 写第一个管道来将输入写到子进程
  6. 父进程可以通过 PeekNamedPipe 来查询子进程有没有输出
  7. 子进程结束后,要通过 CloseHandle 来关闭两个管道。

下面是具体的说明和定义:

1. 建立匿名管道使用 CreatePipe 原形如下:

BOOL CreatePipe(
PHANDLE hReadPipe, // address of variable for read handle
PHANDLE hWritePipe, // address of variable for write handle
LPSECURITY_ATTRIBUTES lpPipeAttributes, // pointer to security attributes
DWORD nSize // number of bytes reserved for pipe
);

当管道建立后,结构中指向的 hReadPipe 和 hWritePipe 可用来读写管道,当然由于匿名管道是单向的,你只能使用其中的一个句柄,参数中的 SECURITY_ATTRIBUTES 的结构必须填写,定义如下:

typedef struct_SECURITY_ATTRIBUTES{
DWORD nLength: //定义以字节为单位的此结构的长度
LPVOID lpSecurityDescriptor; //指向控制这个对象共享的安全描述符,如果为NULL这个对象将被分配一个缺省的安全描述
BOOL bInheritHandle; //当一个新过程被创建时,定义其返回是否是继承的.供系统API函数使用.
}SECURITY_ATTRIBUTES;

2. 填写创建子进程用的 STARTUPINFO 结构,一般我们可以先用 GetStartupInfo 来填写一个缺省的结构,然后改动我们用得到的地方,它们是:

  • hStdInput -- 用其中一个管道的 hWritePipe 代替
  • hStdOutput、hStdError -- 用另一个管道的 hReadPipe 代替
  • dwFlags -- 设置为 STARTF_USESTDHANDLES or STARTF_USESHOWWINDOW 表示输入输出句柄及 wShowWindow 字段有效
  • wShowWindow -- 设置为 SW_HIDE,这样子进程执行时不显示窗口。

填写好以后,就可以用 CreateProcess 来执行子进程了,具体有关执行子进程的操作可以参考上一篇教程《进程控制

3. 在程序中可以用 PeekNamedPipe 查询子进程有没有输出,原形如下:

BOOL PeekNamedPipe(
HANDLE hNamedPipe, // handle to pipe to copy from
LPVOID lpBuffer, // pointer to data buffer
DWORD nBufferSize, // size, in bytes, of data buffer
LPDWORD lpBytesRead, // pointer to number of bytes read
LPDWORD lpTotalBytesAvail, // pointer to total number of bytes available
LPDWORD lpBytesLeftThisMessage // pointer to unread bytes in this message
);

我们可以将尝试读取 nBuffersize 大小的数据,然后可以通过返回的 BytesRead 得到管道中有多少数据,如果不等于零,则表示有数据可以读取。

4. 用 ReadFile 和 WriteFile 来读写管道,它们的参数是完全一样的,原形如下:

ReadFile or WriteFile(
HANDLE hFile, // handle of file to read 在这里使用管道句柄
LPVOID lpBuffer, // address of buffer that receives data 缓冲区地址
DWORD nNumberOfBytesToRead, // number of bytes to read 准备读写的字节数
LPDWORD lpNumberOfBytesRead, // address of number of bytes read,实际读到的或写入的字节数
LPOVERLAPPED lpOverlapped // address of structure for data 在这里用 NULL
);

5. 用 CloseHandle 关闭管道一和管道二的 hReadPipe和 hWritePipe 这四个句柄。

下面给出了一个例子程序,这个程序是上篇教程《进程控制》的例子的扩充,如果你对有的 api 感到陌生的话,请先阅读上一篇教程。

源程序 - 汇编源文件

DEBUG	equ	0
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;	Programmed by 罗云彬, bigluo@telekbird.com.cn
;	Website: http://asm.yeah.net
;	LuoYunBin's Win32 ASM page (罗云彬的编程乐园)
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;	版本信息
;	汇编教程附带例子程序 - 管道例子
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
	.386
	.model flat, stdcall
	option casemap :none   ; case sensitive
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;	Include 数据
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
include		windows.inc
include		user32.inc
include		kernel32.inc
include		comctl32.inc
include		comdlg32.inc
include		gdi32.inc

includelib	user32.lib
includelib	kernel32.lib
includelib	comctl32.lib
includelib	comdlg32.lib
includelib	gdi32.lib
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;	Equ 数据
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
ICO_MAIN	equ	1000
MENU_MAIN	equ	2000
IDM_EXEC	equ	2001
IDM_EXIT	equ	2002

F_RUNNING	equ	0001h	;进程在运行中
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;	数据段
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
		.data?

stStartUp	STARTUPINFO <?>

hInstance	dd	?
hMenu		dd	?
hWinMain	dd	?
hWinText	dd	?
hFont		dd	?
hRunThread	dd	?
hRead1		dd	?
hWrite1		dd	?
hRead2		dd	?
hWrite2		dd	?
szBuffer	db	512 dup	(?)

dwFlag		dd	?
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

		.data

szMenuExecute	db	'连接 MS-&DOS 方式',0
szExcuteError	db	'启动应用程序错误!',0
szCaption	db	'管道示例程序 ... http://asm.yeah.net',0
szClassName	db	'PipeExample',0
;szDllName	db	'riched32.dll',0
;szClassNameRedit db	'RichEdit',0
szDllName	db	'riched20.dll',0
szClassNameRedit db	'richedit20a',0
szCommand	db	'c:/command.com',0

stLogFont	LOGFONT	<24,0,0,0,FW_NORMAL,/
			0,0,0,ANSI_CHARSET,OUT_DEFAULT_PRECIS,/
			CLIP_STROKE_PRECIS,DEFAULT_QUALITY,/
			DEFAULT_PITCH or FF_SWISS,"Fixedsys">

;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;	代码段
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
		.code

if		DEBUG
		include		Debug.asm
endif
include		Win.asm

;********************************************************************
; 执行程序用的线程
; 1. 用 CreateProcess 建立进程
; 2. 用 WaitForSingleOject 等待进程结束
;********************************************************************
_RunThread	proc	uses ebx ecx edx esi edi,/
		dwParam:DWORD
		local	@stSecurity:SECURITY_ATTRIBUTES
		local	@dwExitCode
		local	@dwBytesRead
		local	@stRange:CHARRANGE

		or	dwFlag,F_RUNNING
;********************************************************************
; “执行”菜单改为“结束”
;********************************************************************
		invoke	EnableMenuItem,hMenu,IDM_EXEC,MF_GRAYED
		invoke	EnableMenuItem,hMenu,IDM_EXIT,MF_GRAYED
;********************************************************************
; 建立管道
;********************************************************************
		mov	@stSecurity.nLength,sizeof SECURITY_ATTRIBUTES
		mov	@stSecurity.lpSecurityDescriptor,NULL
		mov	@stSecurity.bInheritHandle,TRUE
		invoke	CreatePipe,addr hRead1,addr hWrite1,addr @stSecurity,NULL
		invoke	CreatePipe,addr hRead2,addr hWrite2,addr @stSecurity,NULL

;********************************************************************
; 执行文件,如果成功则等待程序结束
;********************************************************************
		invoke	GetStartupInfo,addr stStartUp
		mov	eax,hRead1
		mov	stStartUp.hStdInput,eax
		mov	eax,hWrite2
		mov	stStartUp.hStdOutput,eax
		mov	stStartUp.hStdError,eax
		mov	stStartUp.dwFlags,STARTF_USESTDHANDLES or STARTF_USESHOWWINDOW
		mov	stStartUp.wShowWindow,SW_HIDE
		invoke	CreateProcess,NULL,addr szCommand,NULL,NULL,/
			NULL,NORMAL_PRIORITY_CLASS,NULL,NULL,offset stStartUp,offset stProcInfo
		.if	eax !=	0
			.while	TRUE
				invoke	GetExitCodeProcess,stProcInfo.hProcess,addr @dwExitCode
				.break	.if @dwExitCode != STILL_ACTIVE
				invoke	PeekNamedPipe,hRead2,addr szBuffer,511,addr @dwBytesRead,NULL,NULL
				.if	@dwBytesRead !=	0
					invoke RtlZeroMemory,addr szBuffer,512
					invoke	ReadFile,hRead2,addr szBuffer,@dwBytesRead,addr @dwBytesRead,NULL
					mov	@stRange.cpMin,-1
					mov	@stRange.cpMax,-1
					invoke	SendMessage,hWinText,EM_EXSETSEL,0,addr	@stRange
					invoke	SendMessage,hWinText,EM_REPLACESEL,FALSE,addr szBuffer
					invoke	SendMessage,hWinText,EM_SCROLLCARET,NULL,NULL
					invoke	SendMessage,hWinText,WM_SETFONT,hFont,0
				.endif
			.endw
			invoke	CloseHandle,stProcInfo.hProcess
			invoke	CloseHandle,stProcInfo.hThread
		.else
			invoke	MessageBox,hWinMain,addr szExcuteError,NULL,MB_OK or MB_ICONERROR
		.endif
;********************************************************************
; 关闭管道
;********************************************************************
		invoke CloseHandle,hRead1
		invoke CloseHandle,hWrite1
		invoke CloseHandle,hRead2
		invoke CloseHandle,hWrite2
;********************************************************************
; 把“结束”菜单改为“执行”
;********************************************************************
		invoke	EnableMenuItem,hMenu,IDM_EXEC,MF_ENABLED
		invoke	EnableMenuItem,hMenu,IDM_EXIT,MF_ENABLED
		invoke	EnableWindow,hWinText,FALSE
		and	dwFlag,not F_RUNNING
		ret

_RunThread	endp

;********************************************************************
;	窗口程序
;********************************************************************
WndMainProc	proc	uses ebx edi esi, /
		hWnd:DWORD,wMsg:DWORD,wParam:DWORD,lParam:DWORD

		mov	eax,wMsg
;********************************************************************
		.if	eax ==	WM_CREATE
			mov	eax,hWnd
			mov	hWinMain,eax
			call	_Init
;********************************************************************
		.elseif	eax ==	WM_SIZE
			mov	edx,lParam
			mov	ecx,edx
			shr	ecx,16
			and	edx,0ffffh
			invoke	MoveWindow,hWinText,0,0,edx,ecx,TRUE
			invoke	PostMessage,hWinText,WM_SIZE,wParam,lParam
;********************************************************************
		.elseif	eax ==	WM_CLOSE
			test	dwFlag,F_RUNNING
			.if	ZERO?
				invoke	DestroyWindow,hWinMain
				invoke	PostQuitMessage,NULL
			.endif
;********************************************************************
		.elseif	eax ==	WM_COMMAND
			mov	eax,wParam
			.if	ax ==	IDM_EXEC
;********************************************************************
; 如果没有在执行中(dwFlag 没有置位) 则建立线程,在线程中执行程序
; 如果已经在执行中,则用 TerminateProcess 终止执行
;********************************************************************
				test	dwFlag,F_RUNNING
				.if	ZERO?
					invoke	EnableWindow,hWinText,TRUE
					invoke	SetFocus,hWinText
					invoke	CreateThread,NULL,NULL,offset _RunThread,/
						NULL,NULL,offset hRunThread
				.else
					invoke	TerminateProcess,stProcInfo.hProcess,-1
				.endif
			.elseif	ax ==	IDM_EXIT
				invoke	DestroyWindow,hWinMain
				invoke	PostQuitMessage,NULL
			.endif
		.else
			invoke	DefWindowProc,hWnd,wMsg,wParam,lParam
			ret
		.endif
		xor	eax,eax
		ret

WndMainProc	endp
;********************************************************************
; 程序入口
;********************************************************************
start:
		call	_WinMain
		invoke	ExitProcess,NULL
;********************************************************************
_WinMain	proc
		local	@stWcMain:WNDCLASSEX
		local	@stMsg:MSG
		local	@hRichEdit

		invoke	LoadLibrary,offset szDllName
		mov	@hRichEdit,eax

		invoke	InitCommonControls
		invoke	GetModuleHandle,NULL
		mov	hInstance,eax
		invoke	LoadMenu,hInstance,MENU_MAIN
		mov	hMenu,eax
;***************** 注册窗口类 ***************************************
		invoke	LoadCursor,0,IDC_ARROW
		mov	@stWcMain.hCursor,eax
		mov	@stWcMain.cbSize,sizeof	WNDCLASSEX
		mov	@stWcMain.hIconSm,0
		mov	@stWcMain.style,CS_HREDRAW or CS_VREDRAW
		mov	@stWcMain.lpfnWndProc,offset WndMainProc
		mov	@stWcMain.cbClsExtra,0
		mov	@stWcMain.cbWndExtra,0
		mov	eax,hInstance
		mov	@stWcMain.hInstance,eax
		invoke	LoadIcon,hInstance,ICO_MAIN
		mov	@stWcMain.hIcon,eax
		mov	@stWcMain.hbrBackground,COLOR_BTNFACE+1
		mov	@stWcMain.lpszClassName,offset szClassName
		mov	@stWcMain.lpszMenuName,0
		invoke	RegisterClassEx,addr @stWcMain
;***************** 建立输出窗口	*****************************************
		invoke	CreateWindowEx,NULL,/
			offset szClassName,offset szCaption,/
			WS_OVERLAPPEDWINDOW,/
			0,0,680,420,/
			NULL,hMenu,hInstance,NULL

		invoke	ShowWindow,hWinMain,SW_SHOWNORMAL
		invoke	UpdateWindow,hWinMain
;********************************************************************
		.while	TRUE
			invoke	GetMessage,addr @stMsg,NULL,0,0
			.break	.if eax	== 0
			invoke	TranslateMessage,addr @stMsg
			invoke	DispatchMessage,addr @stMsg
		.endw
		invoke	FreeLibrary,@hRichEdit
		invoke	DeleteObject,hFont
		ret

_WinMain	endp

;********************************************************************
;	输入程序
;********************************************************************
_InputProc	proc	uses ebx edi esi, /
		hWnd:DWORD,uMsg:DWORD,wParam:DWORD,lParam:DWORD
		local	@szBuffer[4]:BYTE
		local	@dwBytesWrite

		mov	eax,uMsg
		.if	eax ==	WM_CHAR
			mov	eax,wParam
			movzx	eax,al
			mov	dword ptr @szBuffer,eax
			test	dwFlag,F_RUNNING
			.if	!ZERO?
				invoke	WriteFile,hWrite1,addr @szBuffer,1,addr @dwBytesWrite,NULL
			.endif
			xor	eax,eax
			ret
		.endif
		invoke	GetWindowLong,hWnd,GWL_USERDATA
		invoke	CallWindowProc,eax,hWnd,uMsg,wParam,lParam
		ret

_InputProc	endp
;********************************************************************
_Init		proc

;*************** 建立输出 RICHEDIT 窗口	***********************************
		invoke	CreateWindowEx,WS_EX_CLIENTEDGE,offset szClassNameRedit,/
			NULL,WS_CHILD OR WS_VISIBLE OR WS_VSCROLL OR WS_HSCROLL/
			OR ES_MULTILINE	OR ES_AUTOHSCROLL OR ES_AUTOVSCROLL,/
			0,0,0,0,/
			hWinMain,NULL,hInstance,NULL
		mov	hWinText,eax
;*************** 设置字体 ***********************************************
		invoke	CreateFontIndirect,offset stLogFont
		mov	hFont,eax
		invoke	SendMessage,hWinText,WM_SETFONT,hFont,0
		invoke	SendMessage,hWinText,EM_SETREADONLY,TRUE,NULL

		invoke	SetWindowLong,hWinText,GWL_WNDPROC,offset _InputProc
		invoke	SetWindowLong,hWinText,GWL_USERDATA,eax
		invoke	EnableWindow,hWinText,FALSE

		invoke	_CenterWindow,hWinMain
		invoke	SetFocus,hWinText

		ret

_Init		endp
;********************************************************************
		end	start

程序的分析和要点

    在程序中,我先建立了一个 Richedit 控件用来显示子进程的输出,同时将 RichEdit 子类化,截取它的键盘输入以便把它发给子进程

invoke SetWindowLong,hWinText,GWL_WNDPROC,offset _InputProc

这条语句将 RichEdit 的过程指到了 _InputProc 中,然后在 _InputProc 的 WM_CHAR 中将键入的字符 WriteFile 到管道中,
我在程序中先建立了两个管道,然后执行 c:/command.com,这样就得到了一个 dos 的命令行进程,
然后在循环中通过 PeekNamedPipe 检测子进程有无输出,如果有的话则通过 ReadFile 读出,在显示到 RichEdit 中。

在运行例子程序的时候要注意,你可以在这个“Command.com” 中执行几乎所有的别的程序,但是不要执行
如 ucdos,pctools 之类不使用标准输入输出的程序(就是在 dos 下用不了“>”或者“<”重定向的程序),由于我们在装载子进程的时候用了 WS_HIDE,
所以原来的 command.com 的窗口是隐藏的,如果你执行了这种程序那就意味着你失去的对子进程的控制,因为它们不使用标准输入来接收键盘,你也就无法通过管道让它们退出。

在这里还可以引申出匿名管道的另一个用法,如果你执行的不是 command.com 而是类似于 arj.exe 的程序,然后也不用把它的输出显示到 RichEdit 中,而是在程序中处理,
那么,你就可以编写一个 winarj,当然你只需编写窗口界面和 arj.exe 之间的配合而已。


13. INI 文件的操作

概述

在程序中经常要用到设置或者其他少量数据的存盘,以便程序在下一次执行的时候可以使用,比如说保存本次程序执行时窗口的位置、大小、一些用户设置的数据等等,
在 Dos 下编程的时候,我们一般自己产生一个文件,由自己把这些数据写到文件中,然后在下一次执行的时候再读出来使用。
在 Win32 编程中当然你也可以这样干,但 Windows 已经为我们提供了两种方便的办法,那就是使用注册表或者 ini 文件(Profile)来保存少量数据。本文中先介绍一下 .ini 文件的使用。

ini 文件是文本文件,中间的数据格式一般为:
[Section1 Name]
KeyName1=value1
KeyName2=value2
...

[Section2 Name]
KeyName1=value1
KeyName2=value2

ini 文件可以分为几个 Section,每个 Section 的名称用 [] 括起来,在一个 Section 中,可以有很多的 Key,每一个 Key 可以有一个值并占用一行,格式是 Key=value,Win32 对 ini 文件操作的 api 中,
有一部分是对 win.ini 操作的,有一部分是对用户自定义的 ini 文件操作的。Win.in 和 system.ini 是Windows的两个非常重要的初始化文件,Windows将用户所作的选择以及各种变化的系统信息记录在这两个文件中。
System.ini 描述了系统硬件的当前状态,Win.ini 文件则包含了Windows 系统运行环境的当前配置。由于 Win.ini 文件的重要性和常用性,Win32 中有专门对 Win.ini 进行操作的 api,它们是:

  1. GetProfileInt - 从 Win.ini 文件的某个 Section 取得一个 key 的整数值,它的原形是:

    GetProfileInt(
    LPCTSTR lpAppName, // 指向包含 Section 名称的字符串地址
    LPCTSTR lpKeyName, // 指向包含 Key 名称的字符串地址
    INT nDefault // 如果 Key 值没有找到,则返回缺省的值是多少
    );


    如果 Key 值没有找到的话,返回值是 nDefault 指定的缺省值,如果 Key 中的值是负数,则返回 0,如果 Key 指定的是数字和字符串的混合,则返回数字部分的值,比如说 x=1234abcd,则返回 1234

  2. GetProfileString - 从 Win.ini 文件的某个 Section 取得一个 key 的字符串,它的原形是:

    GetProfileString(
    LPCTSTR lpAppName, // 指向包含 Section 名称的字符串地址
    LPCTSTR lpKeyName, // 指向包含 Key 名称的字符串地址
    LPCTSTR lpDefault, // 如果 Key 值没有找到,则返回缺省的字符串的地址
    LPTSTR lpReturnedString, // 返回字符串的缓冲区地址
    DWORD nSize // 缓冲区的长度
    );


    返回的字符串在缓冲区内,返回的 eax 值是返回的字符串的长度(不包括尾部的0)

  3. GetProfileSection - 从 Win.ini 文件中读出整个 Section 的内容,它的原形是:

    GetProfileSection(
    LPCTSTR lpAppName, // 指向包含 Section 名称的字符串地址
    LPTSTR lpReturnedString, // 返回数据的缓冲区地址
    DWORD nSize // 返回数据的缓冲区长度
    );


  4. WriteProfileSection - 将一个整个 Section 的值 写入 Win.ini 文件的指定 Section 中,它的原形是:

    WriteProfileSection(
    LPCTSTR lpAppName, // 指向包含 Section 名称的字符串地址
    LPCTSTR lpString // 要写入的数据的地址
    );


    如果 Win.ini 没有指定的 Section,API 会新建立一个并写入数据,如果已经存在,则先删除原来 Seciton 中所有的 Key 值然后写入新的。

  5. WriteProfileString - 将一个 Key 值写入 Win.ini 文件的指定 Section 中,它的原形是:

    WriteProfileString(
    LPCTSTR lpAppName, // 指向包含 Section 名称的字符串地址
    LPCTSTR lpKeyName, // 指向包含 Key 名称的字符串地址
    LPCTSTR lpString // 要写的字符串地址
    );


    如果 Win.ini 没有指定的 Section,API 会新建 Section,如果没有指定的 Key 则新建一个 Key 并写入数据,如果已经存在,则用字符串代替原来的值。

以上的 Api 是对 Win.ini 操作的,当然对于我们来说,用的更多的是在程序运行的目录中建立自己的 ini 文件,如果需要对自己的 ini 文件操作,就要用到另一组 Api,这一组 api 和上面的很象,
只要把上面一组的 Profile 换成 PrivateProfile(私有的)就可以了,参数中也相应的多了一个 ini 文件名的参数。
例如 GetPrivateProfileInt、GetPrivateProfileSection、WritePrivateProfileString 等等, 下面分别介绍:

  1. GetPrivateProfileInt - 从 ini 文件的某个 Section 取得一个 key 的整数值,它的原形是:

    GetPrivateProfileInt(
    LPCTSTR lpAppName, // 指向包含 Section 名称的字符串地址
    LPCTSTR lpKeyName, // 指向包含 Key 名称的字符串地址
    INT nDefault // 如果 Key 值没有找到,则返回缺省的值是多少
    LPCTSTR lpFileName // ini 文件的文件名
    );


    中间参数和返回值的定义和 GetProfileInt 是一样的。

  2. GetPrivateProfileString - 从 ini 文件的某个 Section 取得一个 key 的字符串,它的原形是:

    GetPrivateProfileString(
    LPCTSTR lpAppName, // 指向包含 Section 名称的字符串地址
    LPCTSTR lpKeyName, // 指向包含 Key 名称的字符串地址
    LPCTSTR lpDefault, // 如果 Key 值没有找到,则返回缺省的字符串的地址
    LPTSTR lpReturnedString, // 返回字符串的缓冲区地址
    DWORD nSize // 缓冲区的长度
    LPCTSTR lpFileName // ini 文件的文件名
    );


  3. GetPrivateProfileSection - 从 ini 文件中读出整个 Section 的内容,它的原形是:

    GetPrivateProfileSection(
    LPCTSTR lpAppName, // 指向包含 Section 名称的字符串地址
    LPTSTR lpReturnedString, // 返回数据的缓冲区地址
    DWORD nSize // 返回数据的缓冲区长度
    LPCTSTR lpFileName // ini 文件的文件名
    );


    这个 api 可以读出整个 section 的内容,当你不知道 section 中有哪些 key 的时候,可以使用这个 api 将整个 section 读出后再处理。

  4. GetPrivateProfileSectionNames - 从 ini 文件中获得 Section 的名称,它的原形是:

    GetPrivateProfileSectionNames(
    LPTSTR lpszReturnBuffer, // 返回数据的缓冲区地址
    DWORD nSize // 返回数据的缓冲区长度
    LPCTSTR lpFileName // ini 文件的文件名
    );


    如果 ini 中有两个 Section: [sec1] 和 [sec2],则返回的是 'sec1',0,'sec2',0,0 ,当你不知道 ini 中有哪些 section 的时候可以用这个 api 来获取名称

  5. WritePrivateProfileSection - 将一个整个 Section 的内容入 ini 文件的指定 Section 中,它的原形是:

    WritePrivateProfileSection(
    LPCTSTR lpAppName, // 指向包含 Section 名称的字符串地址
    LPCTSTR lpString // 要写入的数据的地址
    LPCTSTR lpFileName // ini 文件的文件名
    );


  6. WritePrivateProfileString - 将一个 Key 值写入 ini 文件的指定 Section 中,它的原形是:

    WritePrivateProfileString(
    LPCTSTR lpAppName, // 指向包含 Section 名称的字符串地址
    LPCTSTR lpKeyName, // 指向包含 Key 名称的字符串地址
    LPCTSTR lpString // 要写的字符串地址
    LPCTSTR lpFileName // ini 文件的文件名
    );


    如果 ini 中没有指定的 Section,API 会新建 Section,如果没有指定的 Key 则新建一个 Key 并写入数据,

如果已经存在,则用字符串代替原来的值。当指定的 ini 也不存在的时候,API 会自动建立一个新的文件,

所以使用 ini 的好处是我们不必为了保存少量的数据涉及到文件操作,就连查找文件是否存在的操作都不必要。

使用要点:

在我们实际使用的时候,用的最多的是 GetPrivateProfileString 和 WritePrivateProfileString,但在对自定义 ini
文件操作的时候要注意的是,如果 lpFileName 指定的文件没有路径的话,Api 会去 Windows 的安装目录去找而不会在当前目录找,
但是每次用到 ini 函数要获取当前路径显然太麻烦了,这里有一个变通的办法,
你只要在 ini 文件名前面加上 ./ 就可以了,比如说要对本目录下的 user.ini 操作,那么文件名就是 './user.ini' 这样显然比较方便。
另外,当你要把一个 Key 清除的时候,可以使用把 lpString 指向一个空的字符串然后使用 WritePrivateProfileString。
当你要把一个 section 的全部内容清空的时候,也不必把 key 一个个的清除,
可以使用把 lpString 指向一个空的字符串然后使用 WritePrivateProfileSection。


14.Win32ASM经验点滴

Q1. 如何隐藏/显示任务栏?
Q2.
如何禁止/允许/显示/隐藏开始按钮?
Q3.
如何创建一个真正的"总在最上面"窗口?
Q4.
如何创建热键?比如CTRL + ALT + A
Q5.
如何获得Windows目录和系统目录?
Q6.
如何从我的程序打开开始菜单?
Q7.
如何关闭被正激活的程序?
Q8.
如何去掉窗口标题?
Q9.
如何知道窗口是否在任务栏中(或可见) ?
Q10.
如何隐藏一个窗口?
Q11.
如何将窗口置于前台?
Q12.
如何屏蔽CTRL+ALT+DEL,ALT+TAB+CTRL+ESC这些键?
Q13.
如何确定Windows任务栏的自动隐藏特性是否被激活?
Q14.
如何使用默认的浏览器或邮件程序?
Q15.
如何用Win32 API显示网络连接对话框?


- 如何隐藏/显示任务栏?

shell db "Shell_TrayWnd",0 ; 任务栏的类名
invoke FindWindow,addr shell,NULL ; 先获得句柄,之后隐藏.
.if eax != 0
invoke ShowWindow,eax,SW_HIDE ;
SW_SHOW显示
.endif


- 如何禁止/允许/显示/隐藏开始按钮?

.data?
buffer db 127 dup(?)

.data
shell db "Shell_TrayWnd",0
sbar db "BUTTON",0
child dd ?
slen dd ?

.code

invoke FindWindow,addr shell,NULL ; 获得状态栏句柄
mov tray, eax
invoke GetWindow,tray, GW_CHILD ;
获得状态栏的子窗口(如果有的话)
mov child, eax
.if child != 0
invoke GetClassName,child,offset buffer, sizeof buffer ;
获得子窗口类名
.if eax > 0
invoke lstrlen, offset buffer ;
获得类名长度
mov slen,eax
invoke CharUpperBuff,offset buffer,slen ;
转为大写
invoke lstrcmp,addr buffer, addr sbar ;将类名与'BUTTON'比较
.if eax == 0
invoke ShowWindow,child,SW_HIDE ;
隐藏开始按钮
; invoke ShowWindow,child,SW_SHOW ; 显示开始按钮
; invoke EnableWindow,child,FALSE ; 禁止开始按钮
; invoke EnableWindow,child,TRUE ; 允许开始按钮
.endif
.endif
.endif


- 如何创建一个真正的"总在最上面"窗口?

invoke SetWindowPos,hWin, HWND_TOPMOST,NULL,NULL,NULL,NULL,SWP_NOACTIVATE or SWP_NOMOVE or SWP_NOSIZE


- 如何创建热键?比如CTRL + ALT + A

.data
hmsg db "HotKey CTRL + ALT + A Works good!",0
hcap db "Hotkey Example",0

.code
.if uMsg == WM_CREATE
invoke RegisterHotKey,hWnd,065h,MOD_CONTROL or MOD_ALT, 041h ; CTRL + ALT + A (041h is 65 - 065h is 101)

.elseif uMsg == WM_HOTKEY
invoke MessageBox,hWin,addr hmsg,addr hcap, MB_OK or MB_ICONINFORMATION

.elseif uMsg == WM_DESTROY
invoke UnregisterHotKey,hWin,065h
invoke PostQuitMessage,NULL
return 0
.endif


-如何获得Windows目录和系统目录

.data
buffer db 50 dup(?)
hCap db "WindowsDirectory",0

.code
invoke GetWindowsDirectory, addr buffer, sizeof buffer ;
Windows目录于缓冲区中
; invoke GetSystemDirectory, addr buffer, sizeof buffer ;置系统目录于缓冲区中
invoke MessageBox,hWnd, addr buffer, addr hCap, MB_OK or MB_ICONINFORMATION


- 如何从我的程序打开开始菜单?

invoke SendMessage,hWnd,WM_SYSCOMMAND,SC_TASKLIST,NULL


- 如何关闭正被激活的程序 ?

.data
fwin dd ?

.code
invoke GetForegroundWindow
mov fwin,eax
invoke SendMessage, fwin, WM_CLOSE,NULL


- 如何去掉窗口标题 ?

invoke GetWindowLong,hWnd,GWL_STYLE ; 获得当前窗口类
and eax,not WS_CAPTION ; 去掉WS_CAPTION
invoke SetWindowLong,hWnd,GWL_STYLE,eax ;
设置窗口类

 


- 如何知道窗口是否在任务栏中(或可见)?

invoke IsWindowVisible,hWin
.if eax == TRUE
;
窗口可见
.else
;
窗口不可见
.endif

 


- 如何隐藏一个窗口?

.data
mirc db "mIRC32",0
mhand dd ?

.code
invoke FindWindow,addr mirc, NULL ;
寻找mIRC32
mov mhand,eax
.if mhand != 0 ;
找到?
invoke ShowWindow,mhand,SW_SHOW ;
显示窗口
; invoke ShowWindow,mhand,SW_HIDE ; 隐藏窗口
.else
; mIRC32
未运行...
.endif

 


- 如何将窗口置于前台?

invoke SetForegroundWindow, mhand

 


- 如何屏蔽CTRL+ALT+DEL,ALT+TAB+CTRL+ESC这些键?

invoke SystemParametersInfo,SPI_SCREENSAVERRUNNING,1,NULL,NULL
; Windows98 only 1
关闭 0 允许

 


- 如何确定Windows任务栏的自动隐藏特性是否被激活?

.data
AppBar APPBARDATA {} ; {}
指使用默认值... Thanks to TTom

.code
mov AppBar.cbSize, sizeof AppBar
invoke SHAppBarMessage, ABM_GETSTATE, addr AppBar ; ShellApi
命令
and eax, ABS_AUTOHIDE
.if eax == TRUE
;
任务栏被隐藏
.else
;
任务栏未被隐藏
.endif


- 如何使用默认的浏览器或邮件程序?

.data
lpPage db "http://win32asm.cjb.net",0
lpMail db "ates@anet.net.tr",0
lpOperation db "open",0

.code
invoke ShellExecute,hWin,addr lpOperation, addr lpPage, NULL, NULL, SW_SHOWNORMAL
invoke ShellExecute,hWin,addr lpOperation, addr lpMail, NULL, NULL, SW_SHOWNORMAL


- 如何用Win32 API显示网络连接对话框?

include /MASM32/INCLUDE/mpr.inc
includelib /MASM32/LIB/mpr.lib
invoke WNetConnectionDialog,hWnd,RESOURCETYPE_DISK


Designed By Atilla Yurtseven

Chinese Translation By Orochi,2000.12.16

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值