新的U:USER32的新内容

新的U:USER32的新内容

Kyle MarshMicrosoft Developer Network技术组 创建日期:9/29, 1993 单击此处打开或复制THRD32示例应用程序中的文件。

概述

本文将讨论有关USER32的一些新功能。USER32是USER的32位版,在32位版Microsoft Windows 操作系统中。另外,本文将要讨论的问题还有:有关从Win16移植的问题、异步输入队列、局部线程(thread-localized)输入状态、单实例应用程序、window类、以及Windows挂接(hook)问题。

简介

Windows NT 已经发布,现在,Win32s 对于 Microsoft Windows 3.1版已经可用,其后续版本的 Windows (称为Windows 95)也正在开发之中。现在,对于Windows来说,32位环境已经成为现实。而且,最多几个月,所有的Windows平台都将变为32位的平台。所以,现在最紧迫的工作是建立新的32位的应用程序,以及把您手中的16位应用程序移植到32位环境中。

USER32为Windows管理着窗口管理程序的核心,例如,窗口、菜单、控件、挂接、和对话框。在16位版的Windows系统中,USER16管理着这些任务。本文将讨论有关USER16和USER32之间的一些差异。

移植问题

16位应用程序的开发者将会发现,USER32有一个最为优秀的特征,那就是从USER16移植到USER32非常简单。

加宽的函数

USER32运行在32位版的Windows上,使用的是Win32 应用程序编程接口(API)。这就意味着过去曾经是16位的数字项,现在都变成了32位。这种变化在句柄上体现的最为明显:所有的窗口句柄现在都变成了32位,而不是16位的了。尽管这听起来是一个巨大的变化,但是实际上,对于大多数的应用程序来说,这几乎没有什么影响。所有要使用窗口句柄的函数仍然以同样的方式使用句柄,只不过窗口句柄本身变得不一样了。对SetWindowText的调用仍然是老样子:

SetWindowText(HWND hWnd, LPSTR text)

那么,LPSTR参数怎么样了呢?它是不是需要变成Win32版呢?回答是肯定,再加上否定。在Win32环境中,所有的指针都是一样的。在这里,不存在近指针和远指针的概念,所以,LPSTR同NPSTR的含义是相同的。实际上,您需要把所有对LPSTR和NPSTR的引用全都替换为对PSTR的引用,即变成一个指向字符串的指针。因为这给开发者带来了很大的工作量,所以,Win32 Software Development Kit (SDK) for Windows NT定义了下面的两个宏:

#define LPSTR PSTR

#define NPSTR PSTR

这两个宏使开发者无需为Win32而改变其程序源代码,并且保持源代码与Win16的兼容性。无论如何,随着Win32变得更加普遍,对LPSTR的引用迟早会过时。所以,如果我为Win32开发程序,我会在新的应用程序中只使用PSTR;只有对现存的应用程序,或者是同时需要16位和32位可执行程序的应用程序,我才使用LPSTR。

这种变化的结果使大多数的USER16函数加宽了,使用的是32位的参数,尽管如此,应用程序的开发者们却不需要为此而担忧。因为16位应用程序中的USER16调用在USER32环境中同样会工作正常。

考虑到窗口过程,这种参数加宽为32位的变化却的确给您的应用程序造成了影响。这些过程有4个参数,其中的第三个参数通常都被定义为下面的样子:

WORD wParam

按匈牙利命名规则命名的wParam暗示着一个“字”。而在Win32环境下,这个值将变成32位,并且定义为:

UINT uParam

实际上,UINT类型在Win16和Win32之间是可以移植的,因为在这两种操作系统之间,它都被定义为一个无符号的整型值。使用这种数据类型,将使您的程序源代码与16位和32位的Windows都保持良好兼容性。

应用程序的开发者不得不面对如何处理新的wParam的问题。把这个参数名改为uParam,会给您的已移植的(新的)应用程序带上明显的标记,使人马上就可以知道它是从Win32环境移植而来的(或专为Win32开发的)。它也是使用匈牙利命名规则编译的。然而,许多的开发者仍然倾向于使用Windows 3.1中的参数名,所以,他们使用如下的参数:

UINT wParam

为了使问题简单化,Windows的头文件定义了一个宏,允许开发者保留着过去的名称、保持与匈牙利命名规则的一致性,并且确保在16位和32位的应用程序之间的可移植性:

WPARAM wParam

而WPARAM在Win16和Win32中都被定义为:

typedef UINT WPARAM

被删除的和被弃用的函数

在移植到USER32时首先需要注意的是,在您的应用程序中的任何USER32所未提及的函数可能不能正常工作了。

下列的函数在Win32中被删除了。如果某个应用程序调用了这些函数,那么就需要从源代码中删除对它们的引用。

    •  
    • DefDriverProc
    • EnableCommNotification
    • ExitWindowsExec
    • FlushComm
    • GetCommError
    • GetCommEventMask
    • GetDriverInfo
    • GetFreeSystemResources
    • GetNextDriver
    • LockInput
    • OpenComm
    • ReadComm
    • SetCommEventMask
    • UngetCommChar
    • WriteComm
    CloseComm

被宏所替代的函数

在Win32所弃用的函数当中,有一些可以通过调用USER32中的其它函数来模拟。在Win32的头文件中定义了下列的宏,使得这种转变更加容易,同时也增加了Win16和Win32的源代码之间的兼容性:

已弃用的函数通过宏引用的新函数
AnsiLowerCharLower
AnsiLowerBuffCharLowerBuff
AnsiNextCharNext
AnsiPrevCharPrev
AnsiUpperCharUpper
AnsiUpperBuffCharUpperBuff
CopyCursorCopyIcon
DefHookProcCallNextHookEx
EnumTaskWindowsEnumThreadWindows
GetNextWindowGetWindow
GetSysModalWindowDefined to NULL
GetWindowTaskGetWindowThreadProcessId
PostAppMessagePostThreadMessage
SetSysModalWindowDefined to NULL

从USER32中移走的函数

下列的函数被从USER32中移走,并且被放入了Windows NT的其它部件中:

    •  
    • ClearCommBreak
    • CloseDriver
    • EscapeCommFunction
    • GetCommState
    • GetCurrentTime
    • GetDriverModuleHandle
    • GetTickCount
    • GlobalAddAtom
    • GlobalFindAtom
    • GlobalGetAtomName
    • lstrcmp
    • lstrcmpi
    • OpenDriver
    • SendDriverMessage
    • SetCommBreak
    • SetCommState
    • TransmitCommChar
    • WnetAddConnection
    BuildCommDCB

被移动入USER32中的函数

动态数据交换(DDE)函数,以前是在一个单独的动态链接库(DLL)中,现在被移动入USER32中了。

已改变了的消息

由于移植到32位环境,所以必须要修改使用了wParam和lParam的消息的模式。在Win16环境下,wParam和lParam包含数据项的3个16位的位置。某些消息将打包3个数据项到这些位置。而Win32有4个16位的位置。新加的位置并不需要改变数据项的打包方式,但是有些数据项已经从16位变为32位了,所以它们已经不再适合原来的位置。最普遍的情况是,句柄被包含到lParam参数中。例如,WM_COMMAND消息在Win16环境中使用wParam和lParam参数的情况如下:

  • wParam: ID
  • lParam: hWnd在LOWORD中,命令在HIWORD中

在Win32环境中,WM_COMMAND则使用:

  • uParam: ID在LOWORD中,命令在HIWORD中
  • lParam: hWnd

之所以会需要这种变化,是因为现在的hWnd需要全部的lParam参数。

在下面所列出的消息中,wParam和lParam被重新打包,并且考虑了32位的情况。

    •  
    • EM_LINESCROLL
    • EM_SETSEL
    • WM_ACTIVATE
    • WM_CHANGECBCHAIN
    • WM_CHARTOITEM
    • WM_COMMAND
    • WM_DDE_ACK
    • WM_DDE_ADVISE
    • WM_DDE_DATA
    • WM_DDE_EXECUTE
    • WM_DDE_POKE
    • WM_HSCROLL
    • WM_MDIACTIVATE
    • WM_MDISETMENU
    • WM_MENUCHAR
    • WM_MENUSELECT
    • WM_PARENTNOTIFY
    • WM_VKEYTOITEM
    • WM_VSCROLL
    EM_GETSEL

WM_CTLCOLOR消息具有3个16位的数据项,无法被重新打包到2个可用的32位参数中。在这种情况下,Win32将使用下列的消息来代替WM_CTLCOLOR消息:

    •  
    • WM_CTLCOLORDLG
    • WM_CTLCOLOREDIT
    • WM_CTLCOLORLISTBOX
    • WM_CTLCOLORMSGBOX
    • WM_CTLCOLORSCROLLBAR
    • WM_CTLCOLORSTATIC
    WM_CTLCOLORBTN

有关Window和类的字

有许多的应用程序使用GetClassWord、SetClassWord、GetWindowWord、和SetWindowWord函数来为类和窗口获取和设置信息。这些函数在USER32中仍然存在,但是它们所使用的数据值已经变成了32位数据类型。结果,GetClassLong、SetClassLong、GetWindowLong、和SetWindowLong函数的“长”版本必须使用考虑了偏移量而新定义的常数。

USER16字常数USER32的长型常数
GCW_CBCLSEXTRAGCL_CBCLSEXTRA
GCW_CBWNDEXTRAGCL_CBWNDEXTRA
GCW_HBRBACKGROUNDGCL_HBRBACKGROUND
GCW_HCURSORGCL_HCURSOR
GCW_HICONGCL_HICON
GCW_HMODULEGCL_HMODULE
GCW_STYLEGCL_STYLE
GWW_HINSTANCEGWL_HINSTANCE
GWW_HWNDPARENTGWL_HWNDPARENT
GWW_IDGWL_ID

USER32的新函数

当USER32 API已经完成最终版本时,Windows 3.1也恰好处于最后的阶段。结果,USER32和USER16的函数非常的相似。许多的Win32新的函数都被添加到Windows 3.1中:

    •  
    • AttachThreadInput
    • CharLowerBuff
    • CharNext
    • CharPrev
    • CharToOem
    • CharToOemBuff
    • CharUpperBuff
    • CopyAcceleratorTable
    • CreateAcceleratorTable
    • CreateIconFromResource
    • CreateIconIndirect
    • CreateMDIWindow
    • DestroyAcceleratorTable
    • EnumPropsEx
    • EnumThreadWindows
    • GetForegroundWindow
    • GetIconInfo
    • GetKeyboardLayoutName
    • GetProcessWindowStation
    • GetThreadDesktop
    • GetUserObjectSecurity
    • GetWindowThreadProcessId
    • IsWindowUnicode
    • LoadKeyboardLayout
    • LookupIconIdFromDirectory
    • MessageBoxEx
    • MsgWaitForMultipleObjects
    • OemToChar
    • OemToCharBuff
    • PostThreadMessage
    • RegisterHotKey
    • SendMessageCallback
    • SendMessageTimeout
    • SendNotifyMessage
    • SetDebugErrorLevel
    • SetForegroundWindow
    • SetUserObjectSecurity
    • ToUnicode
    • UnloadKeyboardLayout
    • UnregisterHotKey
    • WaitForInputIdle
    • WindowFromDC
    ActivateKeyboardLayout

另外,USER32还增强了动态数据交换管理库(DDEML)功能,所以它还包含有一些在USER16中不包含的DDE函数。

单实例应用程序

开发者还能够注意到,在Win16和Win32之间的另一种差别是,所有的Win32应用程序都是以单实例方式来运行的,甚至当该应用程序的另一个实例正在运行之中也是如此。这是因为在Win32环境下,所有的应用程序都有一个单独的地址空间。结果,传递给一个应用程序的WinMain过程的hPrevInstance参数将总是为NULL。

一般来说,这会给应用程序的开发者带来方便。在Win16下,开发者不得不清楚的知道是否必须运行某个应用程序的多个实例,并且当这些实例共享该应用程序的代码段时,要提前报警以确保该应用程序能够正常地工作。在Win32下,开发者则不需要担心多实例的问题。也不用为了确保应用程序的正常运行而提前报警。

实际上,上面的论述并不是完全正确的。对于USER32或GDI32来说,内存访问就不需要提前报警,但是开发者仍然需要注意它。例如,如果该应用程序访问一个数据文件,开发者必须确保该应用程序的两个实例能够同时的或顺序的访问该文件。为了避免这种情况发生,开发者可以使用FindWindow函数,以确保同一时间该应用程序只有一个实例在运行:

if (hWndApp = FindWindow(szAppMainWindowClassName, NULL)) {

hWndPopup = GetLastActivePopup(hWndApp);

BringWindowToTop(hWndApp);

if ( IsIconic(hWndPopup) )

ShowWindow(hWndPopup, SW_RESTORE);

else

SetForegroundWindow(hWndPopup);

return FALSE;

}

对于Win32的应用程序来说,它工作正常,但是对于Windows NT,则带来了另外的问题。在Windows NT中,一个应用程序可以被加载多次,并且时间间隔可以非常小。例如,如果您使用“启动”命令从批处理文件启动一个应用程序,Windows NT将一个接一个的启动这些进程,但是其中的进程启动的速度是非常快的,它的前一个进程还没有完成初始化工作。在这种情况下,FindWindow函数将找不到该窗口类的另一个实例,而该应用程序的另一个实例是确实存在的。为了避免这种情况,该应用程序必须使用一个WIN32同步对象,例如Mutex,代码如下:

hMutex = CreateMutex(NULL, FALSE, "ThreadSampleMutex" );

if ( WaitForSingleObject(hMutex, 10000) == WAIT_TIMEOUT ) {

//

// 有另一个实例,但是定位它花费的时间太长,

// 退出。

return FALSE;

}

if (hWndApp = FindWindow(szAppMainWindowClassName, NULL)) {

hWndPopup = GetLastActivePopup(hWndApp);

BringWindowToTop(hWndApp);

if ( IsIconic(hWndPopup) )

ShowWindow(hWndPopup, SW_RESTORE);

else

SetForegroundWindow(hWndPopup);

ReleaseMutex(hMutex);

CloseHandle(hMutex);

return FALSE;

}

if (!Init(hInstance, cmdShow)) {

ReleaseMutex(hMutex);

CloseHandle(hMutex);

return 1;

}

ReleaseMutex(hMutex);

CloseHandle(hMutex);

异步输入队列

USER32最优秀的功能之一是它的输入处理模式。但是不幸的是,这种功能目前仅在Windows NT下可用。

过去的“艰难”日子

16位版本的Windows提供了自己的多任务能力,因为其基础操作系统,MS-DOS?,没有提供这种能力。有两点的设计决定限制了Windows的能力:

  1. Windows被设计为一种non-preemptive的操作系统。这就意味着Windows不会中断一个应用程序而允许另一个应用程序的运行。
  2. Windows仅有一个输入队列。除非一个应用程序从队列中删除该事件,该单独的输入队列将挂起用户的所有输入。

虽然限制了Windows的功能,对于在Windows刚刚发布的年代的处理器,这些设计决定还是很有意义的。那时,8088是应用得最为普遍得PC处理器,并且是绝对的快速处理器。non-preemptive环境使得处理器无需负责一些额外的工作,例如跟踪正在运行的进程,当它需要被中断时,实现进程之间的切换。而仅需要很少的必要的切换,以支持象鼠标和键盘一类的输入设备所产生的中断。具有单一的输入队列,简化了当另一个应用程序正在运行时管理输入的需求。并且使额外的开销最小化,提高了系统的性能。

这种设计同样使得实现键盘缓冲区式的功能非常简单。因为所有的事件都按照其发生的顺序被放置到系统队列中,并且以同样的顺序被其目标应用程序从队列中取出,所以总是可以实现完美的键盘缓冲区式的功能。

Windows实现多任务的机制是,要求应用程序通过调用GetMessagePeekMessage来处理消息并且从其它事件和用户那里得到输入。当Windows执行GetMessagePeekMessage时,系统将执行任何所必须的任务切换。在任何给定的时间,仅有一个应用程序(该应用程序有一个GetMessage或PeekMessage函数返回)能够运行。所有其它的应用程序都将被阻塞,直到这些函数中的一个已返回。

图1. Win16下的事件处理

这种设计的最大问题是它缺少强健性。如果某一个应用程序的消息处理停止了,则整个系统都会中止。当一个应用程序在处理某个很长的过程,例如读取一个数据库的记录集、打印、重新分页、保存一个文件、读一个文件、或其它的操作,就可能会发生这种情况。真正的问题是,当一个应用程序挂起时,它将会停止消息处理,结果导致整个系统挂起。Windows的后续版本添加了检测已停止处理消息的应用程序的能力,但是现在还不成功。

另一个问题是,如果某个应用程序处理某个消息时很慢,而此时又有事件发生,则队列将可能被充满,则Windows将无法存储任何新的消息。

为了防止系统被带入这种糟糕的处境,设计应用程序时,如果涉及到长时间进程就必须十分小心。最常用的解决方法是把一个长时间进程放在一个PeekMessage循环中。在长时间进程的过程中PeekMessage将被调用,使得其它的应用程序和系统有机会运行和完成它们自己的处理。但是有时,这种循环在设计时非常麻烦。由于它所带来的开发时的负担,有些开发者就不使用这种方法,而是直接进行长时间进程,允许系统不对用户做出响应。

愉快的日子又到来了

USER32的输入处理有两个目标:

  1. 任何的应用程序都不能与USER32的直接访问应用程序能力相冲突。这就意味着:
  • 一个应用程序可以使用原始输入处理程序的线程来挂起或执行一个长时间进程,而无需挂起系统。
  • USER32能够在任何时候直接输入到任何应用程序中。
  1. 输入处理语义必须与Win16的语义兼容。

USER32已经达到了这些目标,所以Win32比Win16要强健的多。当一个应用程序挂起或处理长时间进程时,不会使整个系统中止。为了实现这一目标,USER32利用了Windows NT的preemptive功能。

Windows NT从其设计之初,就包含了preemptive多任务。该操作系统的目标处理器比老的8088处理器功能强大了许多,所以它能够处理一个preemptive系统的后台需求。在Windows NT中,基本的处理单元被称为线程。在Windows NT上运行的每一个过程至少拥有一个线程。USER32与这些线程相互作用的方式使得系统更加强健。

当一个线程调用USER32或GDI32的函数时,系统将为该线程创建一个输入队列。该线程可能会使用该输入队列,也可能不使用,但是此时都会有创建输入队列的开销。优点是如果该线程没有调用USER32或GDI32函数,将不会产生这种额外的开销。

USER32还利用Windows NT线程的优点创建了一个具有高优先级的线程,它被称为raw input thread,它总是处于运行状态,处理用户的键盘和鼠标的输入。用户产生的事件被设备驱动程序放置到一个系统队列中,这与Win16时的情况一样,然后raw input thread立即传输该事件到输入所对应的线程的输入队列。与在Win16下不同,线程输入队列是动态地调整大小的,所以就算是该应用程序正在处理消息,线程输入队列也能为该应用程序存储新的消息。所能够存储的消息数量仅受当前系统可用内存的限制。某个应用程序可以在方便的时候处理消息,而不会影响USER32直接输入到另一个线程的输入队列的能力。用户总是可以从一个挂起或正在进行长时间进程的进程中切换走并做其它的事情。例如,在打印一个工作单时,用户可以切换到邮箱去检查邮件。这可能不用理会工作单被处理的如何。如果一个应用程序被挂起,用户仅需切换到该任务列表并终止该任务即可。

图2.Win32下的输入处理

异步输入队列使系统更加强健,这正是他们所希望的。应用程序的开发者可能再也不需要编写另一个PeekMessage循环了。无论以何种方式实现一个应用程序,系统将保持对用户动作的响应,其它的应用程序也可以被处理,所有的处理都非常完美。但是,如果某个应用程序编写的不好,也可能会使系统速度下降,使用户觉得该应用程序被挂起了。Windows NT的强健性是指,即使是编写的不好的应用程序也不会终止系统的运行,但并不意味着系统还会运行良好。编写不好的应用程序在Windows NT和Windows 3.1下的差别就在于,在Windows NT下,应用程序对系统的影响不会是致命的,因为异步输入队列使得当一个应用程序挂起时,系统不会因此而挂起。

目前,有两条原因使应用程序必须要考虑长时间进程问题:

  • 对用户负责。
  • 允许系统在任何时刻都共享处理器。

响应用户动作

如果一个应用程序不处理用户的输入,用户可能会认为该应用程序已经被挂起。用户总是希望得到立即的反馈,以确信他们的应用程序仍然在工作。就算是应用程序显示了它的状态(并且用户知道该应用程序正在工作),用户还是需要有某种方法来中断、取消、或改变进程的参数(例如,取消一个打印作业)。基于这种考虑,应用程序应该总是能够接收某种用户的输入,哪怕是仅仅在打印文档时支持一个“取消”按钮。

应用程序停止处理消息所带来的另一个问题是,该应用程序的外观会非常糟糕。在执行一个长时间进程时,用户可以切换到其它的应用程序,使其它应用程序的窗口覆盖没有响应的程序窗口。当用户移动和关闭其它的窗口时,没有响应的应用程序窗口部分将需要重画,但是由于该应用程序此时不能处理消息,所以它无法处理从Windows接收到的WM_PAINT消息。这将使得屏幕看起来非常混乱。USER32将最终检测到没有处理消息的应用程序,并且重画这些应用程序的窗口,但是仅绘制边框和空白的背景,不包含菜单和任何其它的信息。USER32的重画功能使屏幕看起来不会十分的混乱,并且把用户的注意力吸引到没有响应的应用程序上,但是可能会使没有响应的应用程序看起来更糟。

允许系统共享处理器

默认情况下Windows NT的配置是,所有拥有前台窗口(用户直接输入的窗口)的应用程序的线程,比属于其它应用程序的线程具有更高的优先级。所有属于拥有前台窗口的应用程序的线程都是前台线程。(请注意,Win32 SDK for Windows NT错误的定义只有拥有前台窗口的线程才是前台线程。)所有的其它线程都被称为后台线程

如果前台线程的基本优先级是从7到9,那么该系统对用户输入的响应将会最为迅速。通过使用“控制面板”中的“系统”应用程序改变该任务的选项,用户可以禁止这个功能。我个人不希望有很多的用户去改变这些任务参数,因为这些改变并不是非常简单的。

如果一个前台线程进入了长时间进程并中止了消息的处理,系统的其它部分实际上将被终止,因为当更高优先级的线程在处理过程中,低优先级线程无法接收到更多的处理器时间。事实上,我的实验结果显示后台线程在一分钟之内仅能接收到一次时间片(slice)。这当然对系统处理多个任务会造成反面的影响。要查看前台线程在终止消息处理时会对系统造成何种的影响,请参阅本文的THRD32示例程序。

启动THRD32的两个实例。排列这两个实例,使您可以同时看到两个实例的窗口。当您启动THRD32时,它是处于PeekMessage模式。这种模式显示以下两个内容:1.返回PeekMessage之间的毫秒数作为底部数字;2.返回PeekMessage之间的最长时间作为顶部数字。单击THRD32窗口中的一个使该窗口成为前台窗口。现在,从State菜单选择Hog the System以设置该窗口的状态。这将会使THRD32进入长时间进程状态,并在顶部数字中显示从1到500,在底部数字中显示花费的时间(以毫秒为单位)。其运行代码如下:

for (i=0; i < 500; i++ ) {

sprintf(buf, "%7.0d", i);

SetWindowText(hwndFilter, buf);

NowTime = (double)timeGetTime();

Interval = NowTime-LastTime;

sprintf(buf, "%7.0lf", Interval);

SetWindowText(hwndNoFilter, buf);

NoFlickUpdate(hwndNoFilter);

NoFlickUpdate(hwndFilter);

}

NoFlickUpdate

当运行THRD32的hogging实例时,您将会看到timing实例停止了数字的显示。如果您没有在应用程序之间切换,所有的进程都会处于等待状态,除非这个hogging THRD32结束了它的进程或者任何其它的进程都无法得到处理器时间这种状态持续大约一分钟。在大约一分钟的“停止”状态之后,Windows NT的时间分配程序(scheduler)将会给低优先级的线程一些处理器时间,但是所分配时间很短,并且在下一分钟里这种分配不会再发生。(我知道这听起来有点让人迷惑,但是如果您运行了这个hogging THRD32实例之后,就会明白我的意思。)当hogging THRD32运行结束之后,则timing THRD32将再次显示数字,但是在PeekMessages之间的最长时间将会与hogging THRD32完成其进程所花费的时间基本一致,或者大约为60,000毫秒,这两个时间大小是差不多的。再次设置hogging THRD32为终止,但这次在hogging THRD32实例结束之前切换到其它的THRD32实例。只要您一切换,timing THRD32就会再次开始显示数字,因为现在timing THRD32已变成前台窗口。然而,请注意,hogging THRD32也会显示数字。这是因为当timing实例处于PeekMessages期间,THRD32刚好处于一个PeekMessage循环,这与它们在Win16环境下一样,允许其它的进程运行。这基本上与在Win16下所使用的老的PeekMessage循环一样:

// 我从该程序片段中抽取了已部分放在下面,使它更容易读懂。

// 如果需要完整的源代码,请参阅THRD32示例中的MAIN.C。

do {

if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) {

if (msg.message == WM_QUIT) break;

if (!TranslateAccelerator(hwndMain, hAccTable, &msg)) {

TranslateMessage(&msg);

DispatchMessage(&msg);

}

} else {

if ( nState == PEEKLOOP ) {

double Interval;

double NowTime;

NowTime = (double) timeGetTime();

Interval = NowTime-LastTime;

sprintf(buf, "%7.0lf", Interval);

SetWindowText(hwndNoFilter, buf);

if ( Interval <= LastInterval ) {

Interval = LastInterval;

}

sprintf(buf, "%7.0lf", Interval);

SetWindowText(hwndFilter, buf);

LastTime=NowTime;

LastInterval = Interval;

}

else {

WaitMessage();

}

}

} while (1);

其它的处理长时间进程的方法

在USER32中,使用如上所述的方法使用PeekMessage循环并不是处理长时间进程的唯一方法。上面给出的代码能够工作的非常完美,说明在Win16环境下不能够运行良好的所有应用程序在Win32下将运行良好。但是,对于新代码,有其它的方法来处理这种情况。下面的部分给出了三种处理长时间进程的方法:使用Sleep函数;添加一个PeekMessage循环到进程本身;使用一个线程。

使用Sleep函数

有一种处理长时间进程的方法,它只是简单地给其它的应用程序运行的机会。在我所知道的所有实现这种功能的方法中,使用Sleep函数是最简单的一种。带有1毫秒的参数值而调用该函数时,将会给运行在较低优先级的线程一些处理器时间。再实验一下hogging THRD32,但是这次使用Hog with Sleep状态。请注意,timing THRD32将继续显示数字,并且hogging THRD32运行的速度比它在Hog the System状态下要慢。这正是所希望的,因为timing THRD32现在是与其它的线程共享处理器时间。改变hogging窗口的大小,可以检验出Sleep函数的调用对THRD32性能的影响。小窗口的运行速度比大窗口的运行速度要快。如果使该窗口非常大,您将会看到Sleep对一个长时间进程所产生的影响。Sleep的效果实际上是由它被调用的方式所决定的。在这种情况下,THRD32在其进程期间调用了500次Sleep函数:

for (i=0; i < 500; i++ ) {

sprintf(buf, "%7.0d", i);

SetWindowText(hwndFilter, buf);

NowTime = (double)timeGetTime();

Interval = NowTime-LastTime;

sprintf(buf, "%7.0lf", Interval);

SetWindowText(hwndNoFilter, buf);

NoFlickUpdate(hwndNoFilter);

NoFlickUpdate(hwndFilter);

Sleep(1);

}

这种计数的缺点是执行进程的应用程序不会响应用户动作。该应用程序看起来仍然象是“死”在那里。而这种情况应该尽可能避免。

使用PeekMessage循环

另一种处理长时间进程的方法是向进程本身添加一个PeekMessage循环:

for (i=0; i < 500; i++ ) {

if ( PeekMessage (&msg, NULL, 0, 0, PM_REMOVE) ) {

TranslateMessage (&msg);

DispatchMessage (&msg);

}

sprintf(buf, "%7.0d", i);

SetWindowText(hwndFilter, buf);

NowTime = (double)timeGetTime();

Interval = NowTime-LastTime;

sprintf(buf, "%7.0lf", Interval);

SetWindowText(hwndNoFilter, buf);

NoFlickUpdate(hwndNoFilter);

NoFlickUpdate(hwndFilter);

}

这种计数允许其它的进程能够得到处理器时间,并且使当前的应用程序也能够响应用户的输入。但是,要使用上面所列出的代码,您必须确信该应用程序在长时间进程结束之前没有退出。如果该应用程序收到一个WM_CLOSE消息,并且在没有检查长时间进程是否结束的情况下调用了DestroyWindowDefWindowProc函数,这种情况就会发生。THRD32示例保留了一个变量,nState,在调用DestroyWindow之前将检查该变量。如果nState指示出长时间进程没有结束,THRD32就不会调用DestroyWindow

case WM_CLOSE:

if ( nState >= HOG ) {

MessageBeep(0);

return FALSE;

}

else {

DestroyWindow(hWnd);

}

break;

如果在上面所示的代码结束其长时间进程之前该应用程序就退出,该进程将会留在系统中(a)永远留在系统中,(b)直到系统关闭,或(c)直到用户使用PVIEW应用程序来结束该进程。这是因为,虽然该应用程序从长时间进程的PeekMessage循环中退出了,但是并没有从它自身的WinMain中的主GetMessagePeekMessage循环中退出。所有属于该进程的窗口都已经被破坏,但是该进程本身被保留了下来。这实际上不是Win32所独有的;如果在Win16环境下发生同样的情况,该任务也会被保留下来永远运行。

使用一个线程

最后一种我将讲述的处理长时间进程的方法是,使用一个线程来执行任务,而在同时,原始线程还保持响应用户输入的能力。首先,我尝试使用CreateThread来创建一个新的线程,它将执行长时间处理,同时,原始进程继续保持用户交互处理。我还确保该应用程序在线程运行期间不会退出。实际上,我使用的机制与我在上一节中讲到PeekMessage循环时所使用的机制是一样的。您可能已经注意到,虽然我使用了两个线程来访问同一个全局数据元素,但是我没有使用同步对象。在这里,这样做是没有问题的,因为一旦线程启动,只有该线程能够改变该数据元素的值。在您自己的应用程序中,应该总是检查并保证多个线程能够访问全局对象而不会出现任何问题。已经完成了所有这些代码之后,我又从头开始并加载了该应用程序。当我以Hog with Thread状态启动了hogging实例后,我非常奇怪的发现它同没有使用线程时的效果一样:当长时间进程运行时,系统的其它部分都停止了。我原来以为把该进程放到后台线程中会解决我的问题。实际上,这样是能够解决问题的,但是我并没有使用后台线程。要牢记的一点是,系统会自动地提升所有前台进程的线程优先级,所以我所启动的仅仅是另外一个前台线程。这一点非常重要,请牢记:如果您的应用程序使用了第二个线程来执行一个长时间的任务,并且该线程不允许低优先级的线程运行,则后台线程将会得不到处理器时间。

为了解决这个问题,我可以添加一个对Sleep的调用,就象在上面"使用Sleep"一节中那样。但是我尝试了其它的方法。我使用了SetThreadPriority来降低我的线程的优先级。这种方法非常有效,我的线程变成了一个后台线程,因为它失去了其前台优先级boost。我再次运行该应用程序,这次情况看起来好得多。但是当我切换到其它的应用程序时,它所包含的所有线程都失去了前台boost。这就使得我的自行指定的后台线程的优先级更加低了。结果,它仍然无法得到处理器时间。在某些情况下,这可能是期望的结果。例如,如果用户没有向一个字处理应用程序中输入文本,那么就不需要在后台为该文档重新分页。但是,在许多其它的情况下,降低到更低的优先级则不是所期望的结果。为了解决这个问题,我添加了一段处理WM_ACTIVATEAPP消息的代码,它根据我的应用程序是处于前台还是后台来设置我的后台线程的优先级。下面就是最终对于THRD32能够正常工作的代码:

case WM_ACTIVATEAPP:

if ( nState == HOGTHREAD ) {

if ( uParam ) {

SetThreadPriority(hThread, THREAD_PRIORITY_LOWEST );

}

else {

SetThreadPriority(hThread, THREAD_PRIORITY_NORMAL );

}

}

else {

return DefWindowProc(hWnd, msg, uParam, lParam);

}

break;

case IDM_HOGTHREAD:

if ( nState >= HOG ) {

MessageBeep(0);

break;

}

nState = HOGTHREAD;

LastTime = (double)timeGetTime();

LastInterval = 0.0;

SetWindowText(hWnd,"Hog the System - Thread");

hThread = CreateThread(NULL, 0, HogThread, hWnd, 0, &amp;idThread);

break;

LRESULT HogThread(HWND hWnd)

{

int i;

double Interval;

double NowTime;

SetThreadPriority(hThread, THREAD_PRIORITY_LOWEST );

for (i=0; i < 500; i++ ) {

sprintf(buf, "%7.0d", i);

SetWindowText(hwndFilter, buf);

NowTime = (double)timeGetTime();

Interval = NowTime-LastTime;

sprintf(buf, "%7.0lf", Interval);

SetWindowText(hwndNoFilter, buf);

NoFlickUpdate(hwndNoFilter);

NoFlickUpdate(hwndFilter);

}

nState = 0;

SetWindowText(hwndFilter, "Done");

NowTime = (double)timeGetTime();

Interval = NowTime-LastTime;

sprintf(buf, "%7.0lf", Interval);

SetWindowText(hwndNoFilter, buf);

ExitThread(0);

return 0;

}

关于异步输入队列的一点注释

异步输入队列是Windows NT的一项优秀的特性。它使系统能够非常清晰地处理编写不好的或挂起的应用程序。但是,这并不是说它是针对所有编写不好的应用程序的灵丹妙药。编写不好的应用程序同样会影响Windows NT的整体性能,就象在16位版的Windows下的情况一样。但是,效果不会再象从前那样是灾难性的,而且永远不会是致命的。

局部线程(thread-localized)输入状态

在Windows NT中,如果输入是由用户所产生的,则来自用户的输入将被分配到正确的线程。这意味着在任何的时间点,对于多于一个的应用程序都可以有可用的输入。实际上,多个应用程序可以同时处理输入。每一个应用程序需要它自身的输入状态、焦点窗口、键盘状态、鼠标捕获、和活动窗口以反映产生输入时的现存状态。Win16仅包含一个输入队列,所以其输入状态函数只反映这个输入队列的状态。有效地给出了一个有关输入状态的全局概念。现在,Win32具有多个输入队列,输入状态函数必须分别为每一个线程服务,而不是为整个系统服务。这意味着一个单个线程所拥有的每一个输入队列,都有其自身的焦点状态、键盘状态、鼠标捕获状态、和活动窗口状态。

结果在Windows NT下,输入状态函数与在16位Windows系统下的工作情况已大不相同了:

    • GetActiveWindow: 在使用SetActiveWindow时要非常小心。一般情况下,用户应该决定哪个窗口是活动的。该函数可以用于设置当前活动窗口为另一个线程所拥有的窗口,但是在这种情况下,后续的对GetActiveWindow的调用将返回NULL。
    • SetActiveWindow: 如果调用GetFocus函数的线程没有具有焦点的窗口,GetFocus将返回NULL。在Win16下,该函数将返回具有焦点的窗口,而无论哪个任务拥有该窗口。
    • GetFocus: 可以用来对其它线程中的窗口设置焦点,但是在这种情况下,后续对GetFocus的调用将返回NULL。
    • SetFocus:SetFocus 如果所调用线程中没有捕获鼠标的窗口,GetCapture将返回NULL。在Win16下,该函数将返回捕获鼠标的窗口,而无论哪个任务拥有该窗口。
    • GetCapture: 仅能被用来设置线程所拥有的窗口的捕获。
    • SetCapture:SetCapture 将释放线程所拥有的任何窗口的捕获。
    • ReleaseCapture:ReleaseCapture
    如果调用 GetActiveWindow的线程没有活动的窗口, GetActiveWindow将返回NULL。在Win16下,该函数为系统返回活动的窗口。

捕获鼠标

在Win16中,在任何给定时间仅有系统中的一个窗口能够设置鼠标捕获。在Win32中,在一个线程中仅有一个窗口能捕获鼠标,但是其它的线程也能够捕获鼠标。在Win16中,所有的鼠标事件都被传送到能够捕获鼠标的窗口。在Win32中,如果鼠标事件是发生在设置了鼠标捕获的线程所拥有的窗口中,那么它才传送给该捕获窗口。换句话说,一个应用程序不能使用SetCapture来获取其它应用程序的鼠标消息。

Windows NT中的16位应用程序

异步输入队列是USER32的基础,Windows on Windows (WOW) box与其它应用程序一样使用它。但是,因为WOW box的目的是在Windows NT环境下模拟Win16,所以对于它所运行的16位Windows应用程序,WOW box并不提供输入队列。相反,它模拟Win16系统队列的方式,使用其自身的线程输入队列。

图3.Windows NT中的Win16系统队列

Window类

Win16有三种类型的类:系统全局(system global)、应用程序全局(application global)、以及应用程序局部(application local)。为了保持兼容性,从表面上看这些类在Win32中都存在。但是,所有Win32的类实际上都是应用程序的局部类。就算是一个应用程序使用了一个注册自身为应用程序全局类的自定义控件,该类实际上仍然是应用程序局部类。这是因为每一个过程都有其自身的地址空间,并且该应用程序所使用的任何DLL都映射到该地址空间。

在大多数情况下,这种差异不会对应用程序产生影响,因为类的工作情况与它们在Win16下完全相同。但是,还是有一点重要的差异,在Win16下,如果您子类化(subclassed)一个系统全局类,所有由该类所创建的窗口都将被子类化(subclassed),而不会受创建它们的应用程序的影响。在Win32下,情况则不是这样。子类化(Subclassing)一个系统全局类棗例如,EDIT类棗仅影响该应用程序的edit控件。这是Win32所具有的一种非常方便的功能。假设您想要在您的应用程序中的每一个edit控件都添加一些功能。在Win16下,您需要跟踪每一个edit控件的创建地点或创建一个edit控件的超类(superclass)。您也可以局部地超类(superclass)该edit控件,但是那不是推荐的方法。在Win32下,您可以简单地子类化(subclass)该类本身,问题就解决了。

子类窗口实例

子类化(Subclassing)一个窗口实例与Win16下差不多,但是有一点大的差别:您不能子类化(subclass)不属于您的应用程序的窗口,因为Windows NT使用了分开的地址空间。有一些办法能够解决这个问题;这些办法都需要获取在其它进程地址空间中的一个过程。例如,通过使用一个具有系统范围挂接(hook)的DLL。您的子类过程的地址在其它应用程序的地址空间中毫无意义,SetWindowLong函数将不允许子类化(subclassing)发生。

Windows挂接(Hooks)

Win32挂接(hooks)并入了许多的改变,包括下面内容:

  • 已经添加了一个新的名为WH_FOREGROUNDIDLE的挂接。
  • 未实现WH_HARDWARE挂接(hook)。
  • 一个应用程序不能为桌面挂接(hook)事件,就算是使用系统范围的挂接(hook)。
  • 所有的系统范围的挂接(hooks)必须在一个DLL。(这在Win16下也被假定为真实的,但是Win16的挂接(hooks)在应用程序中工作。)
  • 当用户按下ctrl+esc、alt+esc、或ctrl+alt+del时,日记挂接(Journal hooks)可以被取消。
  • 如果用户已经取消了一个日记挂接(journal hook),Win32将发送一个新的消息(WM_CANCELJOURNAL)通知应用程序。

有关在Win32中挂接(hooks)是如何工作的详细信息,请参阅Microsoft Developer Network CD中的"Win32 Hooks"一文。

还有什么其它的新内容?

本文仅仅涉及了USER32新功能中最重要的部分。在未来的文章中,我将会详细讨论这些功能中的某几个,例如在USER32中使用线程,而且将要介绍另外一些USER32的新功能,例如Unicode?。

 
例程的功能仅是在屏幕上简单地绘制当前窗口文本。在该循环中THRD32根本不会处理消息。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值