0

我目前正在参与一个将一些旧的 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 背景。

谢谢你的帮助!

4

3 回答 3

4

AsyncObjectCallerDelegate的不正确。它可能在 32 位代码中工作,但在 64 位代码中会惨遭失败。Windows API 函数原型为:

VOID CALLBACK TimerProc(HWND hwnd, UINT uMsg, UINT_PTR idEvent, DWORD dwTime);

在 C# 中,这将是:

delegate void AsyncObjectCallerDelegate(IntPtr hWnd, uint uMsg, IntPtr nIDEvent, uint dwTime);

此外,您的托管原型应该是:

static extern IntPtr SetTimer(IntPtr hWnd, IntPtr nIDEvent, uint uElapse, AsyncObjectCallerDelegate lpTimerFunc);

也就是说,我会回应 Alex Farber 所说的:您应该为此使用 .NET 计时器对象之一。由于这似乎不是 UI 计时器(您为窗口句柄传递 0),我建议System.Timers.Timer使用System.Threading.Timer. 如果您希望计时器引发事件,请使用System.Timers.Timer. 如果您希望计时器调用回调函数,请使用System.Threading.Timer.

请注意,事件或回调将在池线程上执行——而不是程序的主线程。因此,如果处理将访问任何共享数据,则必须牢记线程同步问题。

于 2010-09-09T17:27:47.583 回答
2

问题是您的程序没有发送消息循环或没有让 UI 线程空闲。像 SetTimer() 这样的 API 函数需要消息循环才能工作。例如,Windows 窗体项目中的 Application.Run()。回调只能在您的主线程在循环内并发送 Windows 消息时运行。

它在您使用 MessageBox.Show() 时起作用,这是一个泵送自己的消息循环的函数。这样消息框就可以响应用户点击确定按钮。但是,当然,这只会在盒子打开的情况下有效。

您可能需要重组您的程序,使其基于 Windows 窗体项目模板。在循环中调用 Application.DoEvents() 是一个非常不完美的解决方法。

于 2010-09-09T18:34:54.060 回答
0
公共外部静态int SetTimer(int hwnd,int nIDEvent,int uElapse,IntPtr lpTimerFunc);

int lngReturn = SetTimer(0, 0, 1, Marshal.GetFunctionPointerForDelegate(new AsyncObjectCallerDelegate(AsyncObjectCaller)));

我知道您的任务是只进行迁移,但无论如何,最好使用 Windows 窗体计时器而不是这个,它包装了本机 SetTimer API,并且不需要这些互操作性技巧。

于 2010-09-09T17:09:47.650 回答