如何从控制台应用程序处理COM事件?

14
我正在使用第三方库中的COM对象,该对象会生成周期性事件。当我从Winforms应用程序中使用该库时,将对象作为类成员并在主窗体线程中创建它,一切正常。但是,如果我从另一个线程创建对象,则不会收到任何事件。

我的猜想是,在用于创建对象的同一线程中需要有某种事件循环。

我需要从控制台应用程序中使用此对象。我猜可以使用Application.DoEvents,但我宁愿不在控制台应用程序中包含Winforms命名空间。

我该如何解决这个问题?

更新3(2011-06-15):供应商终于回答了。简而言之,他们说Application.Run创建的消息泵与Thread.Join创建的消息泵之间存在差异,但他们不知道差异在哪里。

我同意他们的看法;任何关于此事的启示都将非常感激。

更新:

根据Richard对mdm答案的评论:

如果其他组件是单线程的,并且从MTA实例化,则Windows将创建工作线程+窗口+消息泵并执行必要的封送。

试图遵循他的建议,我正在执行以下操作:

更新2:

我按照João Angelo的答案更改了代码。

using System;

namespace ConsoleApplication2
{
    class Program
    {
        [STAThread]
        static void Main(string[] args)
        {
            MyComObjectWrapper wrapper = new MyComObjectWrapper();
        }
    }

    class MyComObjectWrapper
    {
        MyComObject m_Object;
        AutoResetEvent m_Event;

        public MyComObjectWrapper()
        {
            m_Event = new System.Threading.AutoResetEvent(false);

            System.Threading.Thread t = new System.Threading.Thread(() => CreateObject());
            t.SetApartmentState (System.Threading.ApartmentState.STA);
            t.Start();

            Wait();
        }

        void ObjectEvt(/*...*/)
        {
            // ...
        }

        void Wait()
        {
            m_Event.WaitOne();
        }

        void CreateObject()
        {
            m_Object = new MyComObject();
            m_Object.OnEvent += ObjectEvt;

            System.Threading.Thread.CurrentThread.Join();
        }    
    }
}

我还尝试了以下方法:
        public MyComObjectWrapper()
        {
            CreateObject();
        }

第三方组件的线程模型是什么?接收事件的COM代码的线程模型是什么?您正在运行哪个COM公寓? - Richard
@Richard:我正在STA中运行。我如何检查第三方组件的模型以及我的COM代码?什么COM代码? - raven
第三方组件的线程模型将在注册表中。 "您的COM代码":您传递给第三方组件以便其调用事件的接口实现。 - Richard
@Richard:我不确定。我在HKCR/CLSID/{UUID}/InProcServer32下找到了有关该对象的参考,但UUID不同。其中一些具有ThreadModel =“Neutral”,而另一些则具有ThreadModel =“both”。关于我的COM代码,我仍然不知道你在说什么。我只是在我的类中实例化对象并添加事件处理程序,就像Daniel Hilgarth的代码一样... - raven
6个回答

6
如果您正在使用STA,则需要一种消息循环方式。如果您不需要其他消息循环,MTA可能是最简单的方法,也是控制台应用程序的最佳选择。
需要注意的一点是,对于MTA,无论哪个线程创建了对象,所有由MTA线程创建的对象都同样属于所有MTA线程。(或者用COM术语来说,进程恰好有一个多线程公寓,其中所有MTA线程都生活在其中。)这意味着,如果您采用MTA方法,根本不需要创建单独的线程-只需从主线程创建对象即可。但是您还需要知道,传入的事件将在“随机”线程上交付,因此您需要采取单独的步骤与主线程进行通信。
using System;
using System.Threading;

class Program
{
    static MyComObject m_Object;
    static AutoResetEvent m_Event;


    [MTAThread]
    static void Main(string[] args)
    {
        m_Event = new AutoResetEvent(false);

        m_Object = new MyComObject();
        m_Object.OnEvent += ObjectEvt;

        Console.WriteLine("Main thread waiting...");
        m_Event.WaitOne();
        Console.WriteLine("Main thread got event, exiting.");
        // This exits after just one event; add loop or other logic to exit properly when appropriate.
    }

    void ObjectEvt(/*...*/)
    {
        Console.WriteLine("Received event, doing work...");

        // ... note that this could be on any random COM thread.

        Console.WriteLine("Done work, signalling event to notify main thread...");
        m_Event.Set();
    }
}

关于您之前的代码版本,我有几点评论:在CreateObject和MycomObjectWrapper构造函数中都有Wait()调用,似乎只需要一个 - 如果有两个,当m_Event.Set()被调用时,只有一个会被释放,而另一个仍然在等待。此外,建议添加一些调试代码,以便知道您的进展情况。这样,您至少可以确定是否从COM获取了事件,并且单独地确定是否成功将其传递回主线程。如果对象在注册表中标记为中性或两者都是,则应该没有问题从MTA创建它们。

谢谢你的回答。我之前使用断点来检查事件;ctor()等待调用只是为了防止主线程结束。无论如何,我已经复制了你的代码,但它卡在“主线程等待…”,事件没有被调用。与此同时,我仍在等待供应商的答复。 - raven

4
如其他答案中所述,STA COM组件需要运行消息循环,以便在其他线程中发生的调用能够正确地封送到拥有该组件的STA线程。在Windows Forms中,您可以免费获得消息循环,但在控制台应用程序中,您必须通过在拥有COM组件的线程上调用Thread.CurrentThread.Join来显式执行它,并且这也可能是应用程序的主线程。此线程必须是STA。
Thread.Join的MSDN条目中,您可以看到这就是您想要的:
阻止调用线程,直到线程终止,同时继续执行标准的COM和SendMessage泵送。
如果您不想在主控制台线程中执行任何其他操作,只需无限等待,否则您可以在定期调用Thread.CurrentThread.Join以泵送消息的同时执行其他操作。
注:这假设您正在处理STA COM组件。
一个简化的示例:
class Program
{
    [STAThread]
    static void Main(string[] args)
    {
        var myComObj = new MyComObject();

        myComObj.OnEvent += ObjectEvt;

        Thread.CurrentThread.Join(); // Waits forever
    }

    static void ObjectEvt(object sender, EventArgs e) { }
}

在这个例子中,控制台应用程序将进入一个永远不会停止的循环,只需响应来自COM组件的事件。如果这不起作用,您应该尝试从COM组件供应商获取支持。

1
我认为从你的类的构造函数中永远不返回不是一个好主意。你应该在构造函数外部调用Wait 方法。 - Daniel Hilgarth
1
这样是行不通的,你需要等待 Windows 消息循环,而不是等待“什么也没有”。 - Simon Mourier
@Simon Mourier,你正在等待Thread.Join执行标准的COM和SendMessage泵。 - João Angelo
1
@Simon Mourier,对于OP正在尝试做的事情以及他正在使用的COM组件无效。如果您对此有疑问,我可以给您一个使用Word自动化工作的示例。 - João Angelo
1
有趣的是,MSDN页面明确表示不要做你正在做的事情;-)“您永远不应该从当前线程调用代表当前线程的Thread对象的Join方法。这会导致您的应用程序挂起,因为当前线程无限期地等待自己,” - Mike
显示剩余6条评论

3

2
如果我没记错的话,另一个选项是在 MTA 中运行另一个线程来完成工作。在 MTA 中,COM 调用是直接进行的,不需要窗口、消息队列和消息泵。 - Richard
1
@mdm:如果其他组件是单线程的,并且从MTA实例化,则Windows将创建工作线程+窗口+消息泵,并进行必要的封送。 - Richard
@Richard - 感谢您重新启动了这个话题。那么,您所说的“如果其他组件是单线程的,并且从MTA实例化,则Windows将创建工作线程+窗口+消息泵并执行必要的封送处理。”是什么意思?一旦我从主线程实例化COM对象,CLR难道不会自动为我创建带有消息泵的STA吗? - bavaza
@bavaza 取决于情况。如果当前(创建者)线程在MTA中,并且没有其他STA,则COM将在具有必要基础结构的(新)STA中旋转工作线程。如果当前线程在STA中,则在本地STA中创建新组件,并由当前线程来泵送消息。据我所知,CLR在这里不会做任何额外的事情:如果COM不进行消息泵送,则必须进行消息泵送。(评论对于这个问题来说真的太短了,而且你正在涉及我很久以前就没有接触过的细节。你真的需要一个全面详细的新问题来解决你的问题。) - Richard
@Richard - 有一个新问题:https://dev59.com/dGEi5IYBdhLWcg3wPadi - bavaza
显示剩余7条评论

1

我认为以下内容应该可以工作:

[STAThread]
Main(...)
{
    var comObject = new YourComObject();
    comObject.Event += EventHandler;
    Console.WriteLine("Press enter to exit.");
    Console.ReadLine();
}

void EventHandler(...)
{
    // Handle the event
}

它不会起作用。你必须至少调用Application.DoEvents(),因此这并不总是有效。 - Eugeniu Torica

1
你定义了线程公寓模型吗?
    [STAThread]
    static void Main(string[] args)
    {
        // Create the thread that will manage the COM component
        Thread th = new Thread(...); 
        // Before starting the thread
        th.SetApartmentState (ApartmentState.STA);         
    }

在线程中,只需等待事件发出终止信号。当线程等待事件时,我认为它应该在线程循环中处理消息。

1

你可以试试这个:

static class Program
{
    MyComObject m_Object;

    [STAThread]
    static void Main()
    {
        m_Object = new MyComObject();
        m_Object.OnEvent += ObjectEvt;
        System.Windows.Forms.Application.Run();
    }

    void ObjectEvt(/*...*/)
    {
        // ...
    }
}

1
OP 倾向于不将 System.Windows.Forms 引用添加到控制台应用程序项目中,由于您可以在没有该引用的情况下进行消息泵,我倾向于同意他的观点。 - João Angelo
1
@Simon,这个方法可以,但正如João所写的那样,我正在尝试避免Winforms引用。无论如何,谢谢。 - raven
@Jaime - 如果它能正常工作,而且你只使用了专门为此设计的方法,我认为引用Windows表单并没有问题,因为你已经添加了依赖于Windows基础架构的COM对象引用。PS:这与Application.DoEvents不同。 - Simon Mourier
@Jaime - 就普通的本地Windows编程而言,这看起来并不那么笨拙。对于常规的Window/Winform,当您单击关闭按钮时,PostQuitMessage就会执行。在您的情况下,您只需要定义何时以及如何调用它即可。无论如何,很高兴它能正常工作 :-) - Simon Mourier
@Jaime - 好的,我明白了。所以,是的,你可以在OnStop方法上执行PQM。而且,OnStop可以从任何线程调用,但是这种方式下PQM应该能正常工作。 - Simon Mourier
显示剩余7条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接