恭喜;您已经设法偶然发现了我最喜欢的 COM 怪癖之一,在这种情况下,是 IOleWindow 的 GetWindow 方法的一个令人愉快的模糊限制 - 以及一条错误消息,让您对正在发生的事情几乎没有任何线索。这里的根本问题是 GetWindow() 方法被标记为 [input_sync] - 来自 SDK 中的 include\oleidl.idl 文件:
interface IOleWindow : IUnknown
{
...
[input_sync]
HRESULT GetWindow
(
[out] HWND *phwnd
);
不幸的是,IOleWindow 的文档没有提到这个属性,但是其他一些的文档,例如IOleDocumentView::SetRect() 这样做:
此方法使用 [input_sync] 属性定义,这意味着视图对象在执行此方法时不能产生或进行另一个非 input_sync RPC 调用。
此属性背后的想法是向调用者(可能是 Word 等应用程序或其他一些 OLE 控件主机)保证它可以安全地调用这些方法而不必担心重入。
事情变得棘手的是 COM 决定强制执行:如果它认为它可能违反这些约束,它将拒绝对 [input_sync] 方法的跨单元调用。因此,IIRC,如果您在 SendMessage() 内,则不能进行跨公寓 [input_sync] 调用 - 这就是错误消息有点暗示的情况。而且 - 这就是让你来到这里的那个 - 你不能从 MTA 线程调用跨公寓 [input_sync] 方法。也许 COM 在这里的执行有点过分热心,但无论如何这就是你必须处理的事情。
(关于 MTA 与 STA 线程的简要评论:在 COM 中,线程和对象是 STA 或 MTA。STA,Single-Threaded-Aparment,是 Windows UI 的工作方式;单个线程拥有 UI 和与之关联的所有对象它,并且那些对象期望被该线程单独调用。MTA,或多线程-Aparment,更像是一个免费的;对象可以期望在任何时候从任何线程调用,所以需要这样做它们自己的同步是线程安全的。MTA 线程通常用于工作和后台任务。因此您可以在单个 STA 线程上管理 UI,但在使用一个或多个 MTA 线程时在后台下载一堆文件。COM 确实一堆工作,以允许两者相互操作并试图隐藏一些复杂性。这里的部分问题是你混合了这些隐喻:ThreadPools 与后台工作相关联,MTA 也是如此,但 IOleWindow 是 UI-以中心为中心,STA 也是 - 而 GetWindow 恰好是一种对 en 非常严格的方法强迫这个。)
长话短说,您不能从 ThreadPool thead 调用此方法,因为它们是 MTA 线程。此外,默认情况下,新线程是 MTA,因此仅创建一个新线程来完成工作是不够的。
相反,创建新线程,但在启动之前使用tempThread.SetApartmentState(ApartmentState.STA);,这将为您提供一个 STA 线程。您可能需要将处理 shell COM 对象的所有代码实际放在该 STA 线程中,而不仅仅是对 GetWindow() 的单个调用 - 我不记得确切的细节,但是如果您最终在 MTA ThreadPool 线程上获取了原始 COM 对象(这里似乎是 ShellWindows 对象),即使您尝试从 STA 调用它,它也会与该 MTA 保持关联。
如果您可以改为从 STA 线程而不是来自 ThreadPool 的 MTA 线程来完成所有工作,那就更好了,这样一开始就可以避免这种情况。与其使用为后台/非 UI 代码设计的 System.Threading.Timer,不如尝试使用以 UI 为中心的System.Windows.Forms.Timer。这确实需要一个消息循环 - 如果您的应用程序中已经有了窗口和窗体,那么您已经有了一个,但如果没有,在测试代码中执行此操作的最简单方法是在相同的情况下执行 MessageBox()主行代码等待退出的地方(通常使用 Sleep 或 Console.ReadLine 或类似的)。