求源码:知道FORM的HWND,如何获取该进程的所有线程窗口的HWND?
RT,关于这方面,本人比较陌生,请给出源代码,万分感激,谢谢。
[解决办法]
有难度。。。。
[解决办法]
EnumThreadWindows
[解决办法]
创建多线程的测试应用程序
为了测试和调试进程内部件(.dll 和 .ocx 文件),需要一个多线程的客户应用程序。创建一个简单的多线程应用程序的步骤如下:
打开一个新的 ActiveX EXE 工程,将默认的类模块命名为 MainApp 。把 MainApp 的 Instancing 属性设成为 PublicNotCreatable 。MainApp 对象将占有这个应用程序的第一个线程,并显示主用户界面。
在“工程属性”对话框的“通用”选项卡上,在“启动对象”框中选择“Sub Main”,在“线程模块”框中选择“每个对象对应一个线程”,并输入一个具有唯一性的工程名称。(工程名称决定类型库的名称;如果两个应用程序的类型库名称相同的话将会出现问题。)"ThreadDemo" 是下例所用的工程名称。在“部件”选项卡上,在“启动模式”框中选择“独立方式”。
增加一个窗体,将它命名为 frmProcess ,并将其 Visible 和 ControlBox 属性设成 False 。 这个窗体将以隐藏窗口的方式运行,其中 Sub Main 用来标识该进程的主线程。这个窗体不需要代码。
在工程中增加一个标准模块。把声明、 Sub Main 过程、以及下面显示的 EnumThreadWndMain 过程放在这个模块中。正如在相应的文字和代码注释中所说明的,在启动应用程序以及每次创建一个新的线程时,都要执行 Sub Main 。 Sub Main 的示例代码演示了如何标识第一个线程,这样就能知道何时创建 MainApp。
增加一个窗体,将它命名为 frmMTMain 。这个窗体为这个测试应用程序提供主用户界面。在这个窗体中增加简单的声明,并把紧在“测试应用程序的多个实例”标头上面的 Form_Unload事件也加进去。
在 MainApp 的 Class_Initialize 事件过程中增加代码以显示 frmMTMain。详见下面的代码。
要创建另外的测试线程,则在工程中应该至少有一个 Instancing 属性被设成 MultiUse 的类。增加一个类模块和一个窗体,插入“创建新线程”标头下的代码。由于这个工程选择了“每个对象对应一个线程”,因此每个在外部创建的公共对象都会启动一个新的线程。这就是说,可以通过使用 CreateObject 函数创建带程序标识符( ProgID )的 MultiUse 类的实例,来创建一个新的线程,见相应文字中的说明。
向 frmMTMain 中增加代码,通过创建所定义的 MultiUse 类的实例来创建新的线程。有关代码在下面这个示例的“创建新线程”标头下。
开发环境不支持多线程。如果按 F5 键运行工程,所有的对象将被创建在同一个线程中。为了测试多线程的行为,必须编译 ActiveX EXE 工程,然后运行最终的可执行程序。
重点 为了保证每个新的 MultiUse 对象都能启动一个新线程,必须使用“每个对象对应一个线程”选项而不能用“线程缓冲池”选项。
在 Sub Main 中决定主线程
每个新的线程都会执行 Sub Main 。这是因为 Visual Basic 为每个线程(即每个单元)都维护了一个全局数据的独立副本。为了初始化线程的全局数据,必须执行 Sub Main 。这就是说如果 Sub Main 加载了一个隐藏的窗口,或者显示了应用程序的主用户界面,那么在创建每个新线程时都会加载这些窗体的新副本。
下面的代码用来判断 Sub Main 是不是在第一个线程中执行,这样可以只加载一次隐藏的窗体或者只显示一次测试应用程序的主用户界面。
' 被隐藏窗口的标题的根值
Public Const PROC_CAPTION = "ApartmentDemoProcessWindow"
Public Const ERR_InternalStartup = &H600
Public Const ERR_NoAutomation = &H601
Public Const ENUM_STOP = 0
Public Const ENUM_CONTINUE = 1
Declare Function FindWindow Lib "user32" Alias "FindWindowA" _
(ByVal lpClassName As String, ByVal lpWindowName As String) As Long
Declare Function GetWindowThreadProcessId Lib "user32"_
(ByVal hwnd As Long, lpdwProcessId As Long) As Long
Declare Function EnumThreadWindows Lib "user32" _
(ByVal dwThreadId As Long, ByVal lpfn As Long, ByVal lParam As Long) _
As Long
' 通过 EnumThreadWindows 取得窗口句柄。
Private mhwndVB As Long
' 用来标识主线程的隐藏窗体。
Private mfrmProcess As New frmProcess
' 进程标识符。
Private mlngProcessID As Long
Sub Main()
Dim ma As MainApp
' 借用一个窗口句柄来获得进程
' 标识符(请参阅下面 EnumThreadWndMain 的回调)。
Call EnumThreadWindows(App.ThreadID, AddressOf EnumThreadWndMain, 0&)
If mhwndVB = 0 Then
Err.Raise ERR_InternalStartup + vbObjectError, , _
"Internal error starting thread"
Else
Call GetWindowThreadProcessId(mhwndVB, mlngProcessID)
' 进程标识符使隐藏窗口的标题具有唯一性。
If 0 = FindWindow(vbNullString, PROC_CAPTION & CStr(mlngProcessID)) Then
' 找不到窗口,因此这是第一个线程。
If App.StartMode = vbSModeStandalone Then
' 用唯一的标题创建隐藏窗体。
mfrmProcess.Caption = PROC_CAPTION & CStr(mlngProcessID)
' MainApp 的初始化事件( Instancing =
' PublicNotCreatable )显示主用户界面。
Set ma = New MainApp
' (如果没有对 MainApp 的全局引用,那么
' 关闭应用程序就更加简单;否则 MainApp 应该
' 把 Me 传递给主用户窗体,这样
' 该窗体就能保证 MainApp 不被终止。)
Else
Err.Raise ERR_NoAutomation + vbObjectError, , _
"Application can't be started with Automation"
End If
End If
End If
End Sub
' EnumThreadWindows 所使用的回调函数。
Public Function EnumThreadWndMain(ByVal hwnd As Long, ByVal _
lParam As Long) As Long
' 保存窗口句柄。
mhwndVB = hwnd
' 只需要第一个窗口。
' 一发现窗口就停止迭代。
EnumThreadWndMain = ENUM_STOP
End Function
' MainApp 在它的 Terminate 事件中调用这个子程序;
' 否则隐藏窗体将使
' 应用程序免于被关闭。
Public Sub FreeProcessWindow()
Unload mfrmProcess
Set mfrmProcess = Nothing
End Sub
注意 这种以来标识第一个线程的技术在 Visual Basic 将来的版本中可能会有问题。
可以看到 Sub Main 在第一次以后对于任何线程都不再有任何动作。在增加创建 MultiUse 对象的代码(以便启动后继的线程)时,应该确保包含了初始化这些对象的代码。
EnumThreadWindows 和回调函数 EnumThreadWndMain 一起使用,以便能确定 Visual Basic 为其内部使用而创建的一个隐藏窗口的位置。这个隐藏窗口的窗口句柄被传递给 GetWindowThreadProcessId,该函数返回进程标识符。进程标识符将被用来创建由 Sub Main 加载的隐藏窗口 (frmProcess) 的唯一标题。后继线程检测到这个窗口后就能知道它们不需要再创建 MainApp 对象了。这种转换是必需的,因为 Visual Basic 没有提供识别应用程序主线程的方法。
MainApp class 在其 Initialize 事件中显示测试应用程序的主窗体。MainApp 应该把它的 Me 引用传递给主窗体,这样该窗体就能保证 MainApp 不被终止。从主用户界面可以创建所有的后继线程。将 MainApp 的 Instancing 属性设成 PublicNotCreatable 能有助于避免显示两个用户主界面的窗体。
下面是 MainApp 类和它的相关窗体(上面步骤5和6)的简单的示例:
' MainApp 类的代码。
Private mfrmMTMain As New frmMTMain
Private Sub Class_Initialize()
Set mfrmMTMain.MainApp = Me
mfrmMTMain.Caption = mfrmMTMain.Caption & " (" & App.ThreadID & ")"
mfrmMTMain.Show
End Sub
Friend Sub Closing()
Set mfrmMTMain = Nothing
End Sub
Private Sub Class_Terminate()
' 清理隐藏窗口。
Call FreeProcessWindow
End Sub
' frmMTMain 窗体的代码。
Public MainApp As MainApp
Private Sub Form_Unload(Cancel As Integer)
Call MainApp.Closing
Set MainApp = Nothing
End Sub
测试应用程序的多个实例
在隐藏窗口的标题中包含进程标识符能够使测试应用程序的多个实例互不影响地运行。
如果调用了 CreateObject ,那么所创建的公有类的实例将会位于当前应用程序实例的一个线程上。这是因为在寻找其它正在运行的能够提供该对象的Exe 部件之前,CreateObject 总是试图在当前应用程序中创建对象。
单元的有用属性
把进程标识符作为包含 Sub Main 的模块的只读属性将是有用的:
'测试应用程序中不需要这些代码
Public Property Get ProcessID() As Long
ProcessID = mlngProcessID
End Property
这使得线程上的所有对象都能通过调用非限定的 ProcessID 属性来取得进程标识符。同样,以这种方式将 Boolean IsMainThread 属性显露出来也是有用的。
创建新线程
“每个对象对应一个线程”选项使每个在外部创建的公有对象——即使用 CreateObject 函数创建的对象——都会启动一个新的线程。要创建一个新线程,可以简单地使用一下某个 MultiUse 类的程序标识符 (ProgID):
'在测试应用程序中不需要包含这些代码
Dim tw As ThreadedWindow
Set tw = CreateObject("ThreadDemo.ThreadedWindow")
这时变量 tw 包含了对一个新线程中的对象的引用。所有使用 tw 对这个对象的属性和方法的调用都会导致交叉线程调度的额外开销。
注意 用 New 操作符创建的对象不是在新建的线程中创建。它驻留在执行 New 操作符的同一个线程中。请参阅“设计多线程进程外部件”和“在 Visual Basic 部件中对象创建是如何工作的”。
为了确保在所有其它线程结束前 MainApp 不终止,可以给每个公有类设一个 MainApp 属性。如果对象在新线程中创建了一个 MultiUse 对象,那么作为初始化过程的一部分,它可以把对 MainApp 对象的引用传递给新对象。(也还可以向 MainApp 传递一个对新对象的引用,这样 MainApp 就能有一个对所有控制该线程的对象的引用集合了;但是要记住这可能会产生循环引用。请参阅“处理循环引用”。)
如果希望控制一个线程的类来显示一个窗体,那么应该向它提供显示窗体的 Initialize 方法(不要和 Initialize 事件混淆)或 Show 方法。不要在 Class_Initialize 事件过程中显示窗体,因为这样会在创建类的实例时产生时序错误。下面是的代码是关于一个 MultiUse 的 ThreadedWindow 类和它的窗体的一个很简单的实例:
' 一个 MultiUse 的 ThreadedWindow 类的代码。
Private mMainApp As MainApp
Private mfrm As New frmThreadedWindow
Public Sub Initialize(ByVal ma As MainApp)
Set mMainApp = ma
Set mfrm.ThreadedWindow = Me
mfrm.Caption = mfrm.Caption & " (" & App.ThreadID & ")"
mfrm.Show
End Sub
Friend Sub Closing()
Set mfrm = Nothing
End Sub
' frmThreadedWindow 窗体的代码。
Public ThreadedWindow As ThreadedWindow
Private Sub Form_Unload(Cancel As Integer)
Call ThreadedWindow.Closing
Set ThreadedWindow = Nothing
End Sub
下面的代码段显示了如何初始化 ThreadedWindow 对象:
'测试应用程序的主窗体( frmMTMain )代码。
Private Sub mnuFileNewTW_Click()
Dim tw As ThreadedWindow
Set tw = CreateObject("ThreadDemo.ThreadedWindow")
' 告诉新对象显示它的窗体,并
' 将一个对主应用程序
' 对象的引用传递给它。
Call tw.Initialize(Me.MainApp)
End Sub
如果有很多可以控制线程的类,那么可以通过定义包含 Initialize 方法的 IApartment 接口来使代码更通用。在实现每个类的 IApartment 时,可以为每个类提供适当的 Initialize 方法。下面是创建线程的代码实例:
'测试应用程序中不需要这些代码
Private Sub mnuFileNewObject_Click(Index As Integer)
Dim iapt As IApartment
Select Case Index
Case otThreadedWindow
Set iapt = CreateObject("ThreadDemo.ThreadedWindow")
' (其它情况……)
End Select
' 初始化对象的公用代码。
Call iapt.Initialize(MainApp)
End Sub
注意 可以通过在独立的类型库中定义接口来产生一个只有多线程应用程序知道的 IXxxxApartment 接口。在 ActiveX Exe 工程中,需要设置对该类型库的引用。
保持对线程对象的引用
为了确保能正确地关闭一个多线程应用程序,对于用来创建和控制线程的所有 MultiUse 对象都必须仔细保存对它们的引用情况。
应该清楚地定义对象的存活期目标。举例来说,考虑一个显示窗体的 MultiUse 对象的情况。管理对象存活期的最容易办法是让对象向窗体传递一个 Me 引用;这样窗体就能够保持对象一直存活。如果用户关闭了窗体,窗体的 Unload 事件必然将所有对这个 MultiUse 对象的引用设成 Nothing ,这样对象就能终止并清理它对窗体的引用了。(最好为 MultiUse 对象提供一个友元方法来清理对窗体的引用和所有其它对内部对象引用;窗体的 Unload事件调用这个方法。)
如果控制线程的对象在线程中使用 New 操作符创建了另外的对象,那么应确保清理对这些对象的引用。对于在线程中创建的这些对象,在释放对它们的引用之前线程是无法关闭的。打开的线程要消耗系统资源。
友元方法不能在线程间使用
由于友元属性和方法不是类的公有接口的一部分,因此不能在其它线程中调用它们。对象之间交叉线程的调用只限于声明为 Public 的属性和方法。
重入
如果由于调用 DoEvents 、显示模态窗体,或者对其它线程中对象进行辅助调用而使对象的某个方法被移交控制,那么第二个调用者就可以在第一个调用结束之前进入该方法的代码。如果这个方法使用或修改了属性值或模块级变量,那么这种调用将导致该对象的一种无效内部状态。要防止重入,可以:
避免移交。
为每个方法维护一个模块级布尔标志。在开始一个方法时,它测试这个标志来决定此方法是否正在运行。如果没有,方法就将这个标志设成 True 并继续;否则就产生一个错误。在方法结束或由于任何原因退出时必须仔细地关闭这个标志。
编写重入方法——即不依赖于模块级数据的方法。
异步任务
Visual Basic 没有提供分支执行的途径——就是在一个线程中用某个方法调用一个新的线程,然后立刻在原来的线程中继续处理。通过让原来的方法调用打开一个计时器然后立即返回,可以在测试应用程序中模拟这种行为。当发生计时器事件时,可以将计时器关闭并执行异步处理。这种技术在 “异步回调和事件”中讨论,同时在 Coffee 示例应用程序中有演示(请参阅“创建 ActiveX Exe 部件”)。
使用多线程测试应用程序
要测试单元线程化的部件,必须编译这个多线程测试应用程序,这是因为 Visual Basic 开发环境目前不支持执行多线程。如果有 Visual Studio ,那么可以利用它将测试应用程序编译成带有调试信息的本机代码,这样就可以使用 Visual Studio 的调试程序。