关闭 Microsoft Dynamics GP 报表目标窗口






4.88/5 (6投票s)
本文将介绍如何通过 .NET 插件库和独立的 Windows Forms 应用程序来关闭 Microsoft Dynamics GP 报表目标窗口。
引言
在创建 Microsoft Dynamics GP 集成时,我们经常需要自动化涉及报表的现有流程。报表目标窗口是一个模式对话框,当在 run report
语句调用中未指定目标而执行 Dexterity 报表时,该对话框将默认显示。我们无法修改该调用,并且 Dexterity 开发系统也未能提供关闭报表目标窗口的一致方法。自动化中断,最终用户必须手动关闭该对话框。
本文提出的解决方案可以完全控制报表目标窗口的关闭。它最初开发并随后部署为 Microsoft Dynamics GP 2010 的 32 位插件集成库,运行在 Windows 7 和 .NET 3.5 上。
解决方案
为了关闭报表目标窗口,我们需要一种机制来检测该窗口何时显示。报表目标窗口的行为类似于标准的 Windows 模式对话框。打开时,该对话框将显示在 Dynamics GP 应用程序窗口之上,等待用户手动关闭。如果 Dynamics GP 应用程序是活动应用程序,报表目标窗口将成为屏幕的最顶层窗口。
Windows 通过 Win32 API 事件挂钩提供了一个应用程序定义的跨进程回调函数,系统会在每次最顶层窗口发生更改时响应生成的事件来调用该函数。每次调用都附带一个生成事件的窗口句柄。使用此句柄,我们可以获取最顶层窗口的标题,并将其与已知的报表目标窗口标题进行比较。当匹配时,我们就知道报表目标对话框是屏幕的最顶层窗口。
检测到报表目标对话框后,我们需要一种方法来关闭它。关闭模式对话框的一种方法是发送 Escape 序列来取消它,这是本文选择的解决方案。
最后,我们希望能够根据需要启用或禁用“关闭”功能。“关闭”代码封装在一个 Closer 类中,该类公开了两个用于此目的的公共方法:EnableClosing()
和 DisableClosing()
。
步骤 1:检测报表目标窗口
我们的代码设置了一个 Win32 API 事件挂钩函数,开始“监听”Dynamics GP 应用程序的所有前台窗口更改。
Process[] localProcesses = Process.GetProcesses();
int DynamicsPID = 0;
foreach (Process p in localProcesses)
{
if (p.ProcessName == DYNAMICS_GP_PROCESS_NAME)
{
DynamicsPID = p.Id;
break;
}
}
hHook = SetWinEventHook(EVENT_SYSTEM_FOREGROUND, EVENT_SYSTEM_FOREGROUND,IntPtr.Zero, callbackProc, (uint) DynamicsPID, 0, WINEVENT_OUTOFCONTEXT);
我们仅为 Dynamics GP 进程注册挂钩。如果我们未能检测到 Dynamics GP 应用程序,我们将监听系统中所有进程的前台更改。代码在这两种情况下都能工作,尽管减少干扰总是有益的。当从进程外部设置事件挂钩进行监听时,如本文稍后所述,时序成为一个关键问题。
每次 Dynamics GP 最顶层窗口发生更改时,都会调用事件挂钩函数 callbackProc
。我们获取调用者的标题,并将其与 REPORT_DESTINATION_CAPTION
进行比较以查找匹配项。
//the method for the callback delegate
private static void WinEventProc(IntPtr hWinEventHook, uint eventType, IntPtr hWnd,int idObject, int idChild, uint dwEventThread, uint dwmsEventTime)
{
[...]
SendMessage(hWnd, WM_GETTEXT, MAX_CAPTION_SIZE, captionDlg);
if (captionDlg.ToString() == REPORT_DESTINATION_CAPTION)
{
System.Diagnostics.Debug.Print("Detected [...]");
[...]
}
}
//the callback delegate
private delegate void WinEventDelegate(IntPtr hWinEventHook, uint eventType, IntPtr hWnd, int idObject, int idChild, uint dwEventThread, uint dwmsEventTime);
//the callback delegate instance
private static WinEventDelegate callbackProc = WinEventProc;
需要注意的是,事件挂钩函数运行在执行 SetWinEventHook()
调用的同一进程中。如果调用来自插件库,callbackProc
将在与 Dynamics GP 相同的进程中运行。如果调用来自独立应用程序,callbackProc
将在应用程序的进程中运行。
要停止检测,我们使用另一个 Win32 调用注销事件挂钩:UnhookWinEvent(hHook)
。
步骤 2:关闭报表目标窗口。
报表目标窗口似乎更接近标准的 Windows 模式对话框,而不是真正的 Dexterity 窗口。假设它是一个标准的 Windows 模式对话框,那么它就有自己的消息处理器。该消息处理器在对话框的生命周期内控制 Dynamics GP 应用程序的消息处理。顺便说一句,这也解释了为什么针对报表目标窗口注册的 Dexterity 触发器永远无法激活。
如果我们想通过发送 escape 按键序列来关闭对话框,我们可能需要根据发送按键的来源遵循不同的路径。要发送按键,我们将使用 .NET 提供的 SendKeys
类。
假设我们在事件挂钩函数 callbackProc
中刚刚获得执行控制权,并且我们想关闭对话框。
从插件库关闭
使用插件时,我们没有自己的消息循环。我们在 Dynamics GP 的同一进程中运行,我们所有的插件操作都由模式对话框的消息处理器控制。SendKeys
类提供了两个发送按键的方法:Send()
和 SendWait()
。Send()
需要一个消息循环,因此我们不能使用它。SendWait()
不需要消息循环,但会因其自己的同步对象而中断消息流。如果我们在回调中调用它,我们会冻结整个 Dynamics GP。解决方案是在另一个线程上运行的计时器的 Elapsed 事件上运行 SendWait()
,就像 System.Timers
命名空间提供的 Timer
组件一样。在事件挂钩函数中,我们不进行其他处理,只启用计时器。执行控制权返回到对话框窗口。
private static void WinEventProc(IntPtr hWinEventHook, uint eventType, IntPtr hWnd, int idObject, int idChild, uint dwEventThread, uint dwmsEventTime)
{
[...]
// in-process integration
closerTimer.Enabled = true; //start timer
}
计时器大部分时间都处于禁用状态。当启用时,在 closerTimer_Elapsed
事件上,我们发出按键以关闭报表目标对话框。
private void closerTimer_Elapsed(object sender, ElapsedEventArgs e)
{
closerTimer.Enabled = false;
System.Windows.Forms.SendKeys.SendWait("{TAB}");
System.Windows.Forms.SendKeys.SendWait("{ESC}");
}
从独立的 Windows Forms 应用程序关闭
我们有自己的消息循环,并且我们的进程与 Dynamics GP 完全不同。理论上,我们可以使用 Send()
或 SendWait()
方法来生成我们的按键消息。而且我们不需要计时器。不仅不需要,而且使用计时器可能会有问题。
因为我们的进程运行独立的、自己的消息循环,事件挂钩函数中的重入成为一个问题。在我们的回调事件中,我们已经在调用 SendMessage()
来读取生成事件的窗口的标题。关于事件挂钩,Microsoft 特别提到:“由于事件处理被中断,当挂钩函数调用任何可能导致拥有线程的消息队列被检查的函数时,可能会随时收到其他事件。在挂钩函数中调用以下任何一个都会发生这种情况:SendMessage、PeekMessage...”。
为了避免这种情况,我们在回调中发送 Escape 序列,并使用 Send()
。
private static void WinEventProc(IntPtr hWinEventHook, uint eventType, IntPtr hWnd, int idObject, int idChild, uint dwEventThread, uint dwmsEventTime)
{
[...]
// out-of-process integration
System.Windows.Forms.SendKeys.Send("{TAB}");
System.Windows.Forms.SendKeys.Send("{ESC}");
}
人们会注意到我们发送了 {TAB}
然后是 {ESC}
。这是无数测试的结果,这些测试表明此按键序列能够关闭我们在所有遇到的情况下都会遇到的报表目标窗口。
为了在运行时区分我们的类是作为库还是可执行文件的一部分,我们重载了 Closer
构造函数,查找程序集的 EntryPoint。库(通常)没有 EntryPoint。
示例代码
该项目使用 VS 2010 和 .NET 3.5 编译。
为了冗余,本文中的一些代码片段省略了源代码中存在的代码部分。
范围
为了可用性,本演示中的 Closer
类被集成作为一个独立的应用程序。然而,代码的编写考虑了 Microsoft Dynamics GP 插件集成库。
以下是 Closer
代码如何附加到 Microsoft Dynamics GP 插件的示例。
public class CloserGPAddIn : IDexterityAddIn
{
// IDexterityAddIn interface
private Closer cls1 = new Closer();
public void Initialize()
{
ProjectAccounting.Forms.PaProjectMaintenance.PaProjectMaintenance.ActivateAfterOriginal +=
new EventHandler(PaProjectMaintenance_ActivateAfterOriginal);
ProjectAccounting.Forms.PaProjectMaintenance.PaProjectMaintenance.CloseAfterOriginal +=
new EventHandler(PaProjectMaintenance_CloseAfterOriginal);
}
private void PaProjectMaintenance_ActivateAfterOriginal(object sender, EventArgs e)
{
cls1.StartClosing();
}
private void PaProjectMaintenance_CloseAfterOriginal(object sender, EventArgs e)
{
cls1.StopClosing();
}
}
插件库类 CloserGPAddIn
实例化 Closer
类,然后在 Dynamics GP 的 Project Maintenance
窗口打开后启用关闭。当 Project Maintenance
窗口关闭后,触发禁用。只要 Project Maintenance
窗口保持打开状态,报表目标窗口就会自动关闭。
结论
在 Dexterity 和 C# 中,关闭报表目标窗口比预期的要复杂。虽然本文中的代码不能保证在所有情况下都能关闭报表目标窗口,但它提供了足够的信息,可能有助于某人避免重复造轮子,至少在本文描述的具体路径上。
阅读更多
我为这段代码添加了一个替代方案,我们使用轮询而不是事件通知来检测报表目标窗口(请参阅主页上的“Alternatives”)。