如何为Access子窗体中的SumatraPDF实例获取正确DDE通道?
问题描述
我开发了一个MS-Access 2010应用,使用Sumatra PDF作为PDF查看器,通过插件模式将其加载到子窗体中。当前实现运行正常,但希望通过DDE命令与PDF文档交互(Sumatra PDF 3.5及以上版本支持DDE)。单独运行Sumatra PDF时,DDE命令可以正常执行,但加载到子窗体后,常规的DDE初始化会失败——尽管Sumatra确实以进程形式运行。
此外场景存在复杂性:用户可能同时打开多个包含PDF查看器的子窗体实例,且应用支持多用户同时使用并打开PDF查看器。
我需要解决的核心问题是:
- 能否确定子窗体中运行的SumatraPDF进程对应的正确DDE通道?
- 能否通过子窗体的Hwnd调用API实现关联?
- 若可行,提供示例代码。
现有实现代码
子窗体加载Sumatra PDF的代码
Private Sub Form_Load() 'subform load procedure Dim inpFile As String, cmd As String inpFile = "D:\temp\test.pdf" cmd = CurrentProject.Path & "\SumatraPDF-3.5.2-32.exe -plugin " & _ Me.Hwnd & " """ & inpFile & """" runShell cmd End Sub Private Function runShell(ByVal cmd As String) Dim wsh As Object Set wsh = VBA.CreateObject("WScript.Shell") wsh.Run cmd, vbHide, False Set wsh = Nothing End Function
单独运行Sumatra时的DDE命令代码
Private Sub Command1_Click() Dim channelNumber As Long channelNumber = Application.DDEInitiate("SUMATRA", "control") Application.DDEExecute channelNumber, "[CmdCopySelection]" Application.DDETerminate channelNumber End Sub
解决方案
方案1:启动Sumatra时指定唯一DDE服务名(推荐)
Sumatra PDF支持通过-dde-name参数指定自定义DDE服务名,这样每个子窗体对应的Sumatra实例可以拥有唯一的服务标识,直接通过该标识建立DDE通道,无需依赖API,实现简单可靠。
修改后的子窗体加载代码
Private Sub Form_Load() 'subform load procedure Dim inpFile As String, cmd As String Dim uniqueDdeName As String inpFile = "D:\temp\test.pdf" ' 用子窗体Hwnd+当前进程ID生成唯一DDE服务名,避免多用户/多实例冲突 uniqueDdeName = "Sumatra_" & Me.Hwnd & "_" & GetCurrentProcessId() ' 将唯一服务名存入子窗体Tag属性,后续调用时使用 Me.Tag = uniqueDdeName cmd = CurrentProject.Path & "\SumatraPDF-3.5.2-32.exe -plugin " & _ Me.Hwnd & " -dde-name """ & uniqueDdeName & """ " & _ """" & inpFile & """" runShell cmd End Sub Private Function runShell(ByVal cmd As String) Dim wsh As Object Set wsh = VBA.CreateObject("WScript.Shell") wsh.Run cmd, vbHide, False Set wsh = Nothing End Function ' 需要声明获取当前进程ID的API Private Declare PtrSafe Function GetCurrentProcessId Lib "kernel32" () As Long
针对子窗体发送DDE命令的代码
' 假设此代码在主窗体或子窗体自身的按钮点击事件中 Private Sub CmdCopySelection_Click() Dim channelNumber As Long Dim uniqueDdeName As String ' 从子窗体Tag中取出唯一DDE服务名(如果是子窗体自身的事件,直接用Me.Tag) uniqueDdeName = Me.Tag ' 若在主窗体,可改为 Forms("子窗体容器").Form.Tag On Error Resume Next channelNumber = Application.DDEInitiate(uniqueDdeName, "control") If Err.Number = 0 Then Application.DDEExecute channelNumber, "[CmdCopySelection]" Application.DDETerminate channelNumber Else MsgBox "无法建立DDE连接:" & Err.Description End If On Error GoTo 0 End Sub
方案2:通过API关联子窗体Hwnd与Sumatra的DDE通道
如果无法修改启动命令,可通过Windows API枚举子窗体的子窗口,找到Sumatra的窗口句柄,再通过DDE API直接连接该窗口,而非依赖服务名。此方式相对复杂,需要处理更多API调用。
示例代码(需声明多个API)
' 声明所需API Private Declare PtrSafe Function FindWindowEx Lib "user32" Alias "FindWindowExA" ( _ ByVal hWnd1 As LongPtr, ByVal hWnd2 As LongPtr, _ ByVal lpsz1 As String, ByVal lpsz2 As String) As LongPtr Private Declare PtrSafe Function DdeInitialize Lib "user32" Alias "DdeInitializeA" ( _ ByRef idInst As Long, ByVal pfnCallback As LongPtr, _ ByVal afCmd As Long, ByVal ulRes As Long) As Long Private Declare PtrSafe Function DdeConnect Lib "user32" ( _ ByVal idInst As Long, ByVal hszService As LongPtr, _ ByVal hszTopic As LongPtr, ByVal pCC As LongPtr) As LongPtr Private Declare PtrSafe Function DdeCreateStringHandle Lib "user32" Alias "DdeCreateStringHandleA" ( _ ByVal idInst As Long, ByVal psz As String, ByVal iCodePage As Long) As LongPtr Private Declare PtrSafe Function DdeExecute Lib "user32" ( _ ByVal hConv As LongPtr, ByVal hszCmd As LongPtr) As Long Private Declare PtrSafe Function DdeFreeStringHandle Lib "user32" ( _ ByVal idInst As Long, ByVal hsz As LongPtr) As Long Private Declare PtrSafe Function DdeDisconnect Lib "user32" ( _ ByVal hConv As LongPtr) As Long Private Declare PtrSafe Function DdeUninitialize Lib "user32" ( _ ByVal idInst As Long) As Long Private Const CP_WINANSI = 1 Private Const DDE_FIRST = &H8000 Private Const DDEACK_FIRST = (DDE_FIRST + 100) Private Const DDEACK_OK = (DDEACK_FIRST + 0) Private Sub CmdCopySelection_ByHwnd() Dim sumatraHwnd As LongPtr Dim ddeInst As Long Dim hszService As LongPtr, hszTopic As LongPtr, hszCmd As LongPtr Dim hConv As LongPtr Dim result As Long ' 找到子窗体中的Sumatra窗口(Sumatra的窗口类名为"SumatraPDF") sumatraHwnd = FindWindowEx(Me.Hwnd, 0, "SumatraPDF", vbNullString) If sumatraHwnd = 0 Then MsgBox "未找到嵌入的Sumatra窗口" Exit Sub End If ' 初始化DDE实例 If DdeInitialize(ddeInst, 0, 0, 0) <> 0 Then MsgBox "DDE初始化失败" Exit Sub End If ' 创建服务名、主题名、命令的字符串句柄 hszService = DdeCreateStringHandle(ddeInst, "SUMATRA", CP_WINANSI) hszTopic = DdeCreateStringHandle(ddeInst, "control", CP_WINANSI) hszCmd = DdeCreateStringHandle(ddeInst, "[CmdCopySelection]", CP_WINANSI) ' 连接到指定窗口的DDE会话 hConv = DdeConnect(ddeInst, hszService, hszTopic, 0) If hConv <> 0 Then ' 执行DDE命令 result = DdeExecute(hConv, hszCmd) If result = DDEACK_OK Then MsgBox "命令执行成功" Else MsgBox "命令执行失败" End If ' 断开连接 DdeDisconnect hConv Else MsgBox "无法连接到DDE会话" End If ' 释放资源 DdeFreeStringHandle ddeInst, hszCmd DdeFreeStringHandle ddeInst, hszTopic DdeFreeStringHandle ddeInst, hszService DdeUninitialize ddeInst End Sub
注意事项
- 方案1更简洁可靠,推荐优先使用,因为直接通过唯一服务名区分实例,避免枚举窗口的不确定性。
- 多用户场景下,由于每个用户的Access进程独立,
GetCurrentProcessId()可以确保服务名不会跨进程冲突。 - 若使用64位Access,需确保所有API声明使用
PtrSafe,并对应LongPtr类型。
内容的提问来源于stack exchange,提问作者emphyrio
相关产品推荐
相关产品推荐

