从 C# 代码调用的 SetTimer Windows API 中的回调方法存在问题
我目前参与的一个项目是将一些旧的 VB6 代码迁移到 C# (.Net Framework 3.5)。我的任务是仅仅进行迁移;任何功能增强或重构都将被推迟到项目的后期阶段。不太理想,但就这样吧。
因此,VB6 代码的一部分调用了 Windows API SetTimer 函数。我已经迁移了它,但无法让它工作。
迁移的项目构建为 DLL;我创建了一个小型 WinForms 测试工具,它链接到 DLL 并调用相关代码。很简单,只是证明可以调用。
迁移后的 DLL 中的相关代码如下:
[DllImport("user32.dll", CharSet = CharSet.Ansi, SetLastError = true, ExactSpelling = true)]
public extern static int SetTimer(int hwnd, int nIDEvent, int uElapse, AsyncObjectCallerDelegate lpTimerFunc);
public delegate void AsyncObjectCallerDelegate(int hwnd, int uMsg, int idEvent, int dwTime);
static public int StartTimer( AsyncGeoServer.GeoWrapper AsyncObj)
{
m_objGeoWrapper = AsyncObj;
int lngReturn = SetTimer(0, 0, 1, new AsyncObjectCallerDelegate(AsyncObjectCaller));
// When the line below is removed, the call functions correctly.
// MessageBox.Show("This is a temp message box!", "Temp Msg Box", MessageBoxButtons.OKCancel);
return lngReturn;
}
static private void AsyncObjectCaller(int hwnd, int uMsg, int idEvent, int dwTime)
{
// Perform processing here - details removed for clarity
}
static public void StopTimer( int TimerID)
{
try { KillTimer(0, TimerID); }
catch { }
}
上述调用被 DLL 包装在外部 DoProcessing() 方法中;这会在调用 StartTimer(两个 Windows 内核调用)之前使用 CreateEvent 创建一个事件,然后在继续处理之前调用 WaitForSingleObject。 AsyncObjectCaller 函数将设置事件作为其执行的一部分,以允许处理继续。
所以我的问题是这样的:如果按照上面列出的方式调用代码,它就会失败。 AsyncObjectCaller 回调方法永远不会被触发,并且 WaitForSingleObject 调用会超时。
但是,如果我取消注释 StartTimer 中的 MessageBox.Show 调用,它就会按预期工作......有点。 AsyncObjectCaller 回调方法在调用 MessageBox.Show 后立即触发。我尝试将 MessageBox.Show 放在代码中的各个位置,无论我把它放在哪里都是一样的(只要在调用 SetTimer 之后调用它) - 回调函数不会被触发,直到消息框被触发显示。
我完全被难住了,而且对 VB6 或 Windows API 编码都不太熟悉,主要来自 .Net 背景。
感谢您的帮助!
I'm currently involved in a project that is migrating some old VB6 code to C# (.Net Framework 3.5). My mandate is to just do the migration; any functional enhancements or refactoring is to be pushed to a later phase of the project. Not ideal, but there you go.
So part of the VB6 code makes a call out to the Windows API SetTimer function. I've migrated this and cannot get it to work.
The migrated project builds as a DLL; I've created a small WinForms test harness that links to the DLL and calls the code in question. Very simple, just to prove that the call can be made.
The relevant code in the migrated DLL is as follows:
[DllImport("user32.dll", CharSet = CharSet.Ansi, SetLastError = true, ExactSpelling = true)]
public extern static int SetTimer(int hwnd, int nIDEvent, int uElapse, AsyncObjectCallerDelegate lpTimerFunc);
public delegate void AsyncObjectCallerDelegate(int hwnd, int uMsg, int idEvent, int dwTime);
static public int StartTimer( AsyncGeoServer.GeoWrapper AsyncObj)
{
m_objGeoWrapper = AsyncObj;
int lngReturn = SetTimer(0, 0, 1, new AsyncObjectCallerDelegate(AsyncObjectCaller));
// When the line below is removed, the call functions correctly.
// MessageBox.Show("This is a temp message box!", "Temp Msg Box", MessageBoxButtons.OKCancel);
return lngReturn;
}
static private void AsyncObjectCaller(int hwnd, int uMsg, int idEvent, int dwTime)
{
// Perform processing here - details removed for clarity
}
static public void StopTimer( int TimerID)
{
try { KillTimer(0, TimerID); }
catch { }
}
The above calls are wrapped by the DLL in an outer DoProcessing() method; this creates an event using CreateEvent before calling StartTimer (both Windows Kernel calls), then calls WaitForSingleObject before continuing processing. The AsyncObjectCaller function will set the event as part of its execution to allow processing to continue.
So my issue is this: if the code is called as listed above, it fails. The AsyncObjectCaller callback method never gets triggered and the WaitForSingleObject call times out.
If, however, I uncomment the MessageBox.Show call in StartTimer, it works as expected... sort of. The AsyncObjectCaller callback method gets triggered immediately after the call to MessageBox.Show. I've tried putting MessageBox.Show in various locations in the code, and it's the same no matter where I put it (as long as it's called after the call to SetTimer) - the callback function doesn't get triggered until the messagebox is displayed.
I'm completely stumped, and none too familiar with either VB6 or Windows API coding, coming from a mainly .Net background.
Thanks for any help!
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(3)
我知道您的任务只是进行迁移,但无论如何,最好使用 Windows 窗体计时器而不是这个,它包装了本机 SetTimer API,并且不需要这些互操作性技巧。
I understand that your mandate is to just do the migration, but i any case, it is better to use Windows Forms timer instead of this, it wraps native SetTimer API, and there is no need in these interoperability tricks.
问题在于您的程序没有启动消息循环或没有让 UI 线程闲置。像 SetTimer() 这样的 API 函数需要消息循环才能工作。例如,Windows 窗体项目中的 Application.Run()。仅当主线程位于循环内并分派 Windows 消息时,回调才能运行。
当您使用 MessageBox.Show() 时它会起作用,这是一个泵送自己的消息循环的函数。以便消息框可以响应用户单击“确定”按钮。但当然,这只有在盒子打开的情况下才有效。
您可能需要重组您的程序,使其基于 Windows 窗体项目模板。在循环中调用 Application.DoEvents() 是一个非常不完美的解决方法。
The problem is that your program is not pumping a message loop or is not letting the UI thread go idle. An API function like SetTimer() requires a message loop to work. Application.Run() in a Windows Forms project for example. The callback can only run when the your main thread is inside the loop, dispatching Windows messages.
It works when you use MessageBox.Show(), that's a function that pumps its own message loop. So that the message box can respond to the user clicking the OK button. But of course, that will only work for as long as the box is up.
You'll probably need to restructure your program so it is based on a Windows Forms project template. Calling Application.DoEvents() in a loop is a very imperfect workaround.
您的
AsyncObjectCallerDelegate
不正确。它可能在 32 位代码中工作,但在 64 位代码中会严重失败。 Windows API 函数原型是:在 C# 中,这将是:
另外,您的托管原型应该是:
也就是说,我会回应 Alex Farber 所说的:您应该为此使用 .NET 计时器对象之一。由于这似乎不是一个 UI 计时器(您为窗口句柄传递 0),我建议使用
System.Timers.Timer
或System.Threading.Timer
代码>.如果您希望计时器引发事件,请使用 System.Timers.Timer。如果您希望计时器调用回调函数,请使用System.Threading.Timer
。请注意,事件或回调将在池线程上执行,而不是在程序的主线程上执行。因此,如果处理将访问任何共享数据,您必须牢记线程同步问题。
Your
AsyncObjectCallerDelegate
is incorrect. It might work in 32-bit code, but will fail miserably in 64-bit. The Windows API function prototype is:In C#, that would be:
Also, your managed prototype should be:
That said, I'd echo what Alex Farber said: you should use one of the .NET timer objects for this. Since this doesn't appear to be a UI timer (you're passing 0 for the window handle), I'd suggest
System.Timers.Timer
orSystem.Threading.Timer
. If you want the timer to raise an event, useSystem.Timers.Timer
. If you want the timer to call a callback function, useSystem.Threading.Timer
.Note that the event or callback will be executed on a pool thread--NOT the program's main thread. So if the processing will be accessing any shared data, you'll have to keep thread synchronization issues in mind.