C# 如何获取 COM 接口的实例

C# How do you get an instance of a COM interface

我一直在谷歌搜索,试图找到获取 COM 接口实例的标准方法。

Microsoft 在他们的文章 COM Interop Part 1: Client Tutorial:

中提供了一个例子
// Create an instance of a COM coclass:
FilgraphManager graphManager = new FilgraphManager();

// See if it supports the IMediaControl COM interface. 
// Note that this will throw a System.InvalidCastException if 
// the cast fails. This is equivalent to QueryInterface for 
// COM objects:
IMediaControl mc = (IMediaControl) graphManager;

// Now you call a method on a COM interface: 
mc.Run();

但是,他们似乎正在实例化 COM 对象并将其转换为 COM 接口。

对于我感兴趣的接口,IDesktopWallpaper,似乎没有可实例化的实现 COM 对象。

我找到的一个示例 here 定义了一些 class 实例化,然后将其强制转换为接口,方法与 msdn 示例相同:

[ComImport, Guid("C2CF3110-460E-4fc1-B9D0-8A1C0C9CC4BD")]
internal class IDesktopWallpaper
{

}

[Guid("B92B56A9-8B55-4E14-9A89-0199BBB6F93B"), //B92B56A9-8B55-4E14-9A89-0199BBB6F93B
InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
internal interface DesktopWallpaperInterface
{
    // declared members
}

我不明白实例化对象是什么。它看起来像一个任意对象,它有一个 GuidAttribute,这似乎表明它是一个实际的 COM 对象。

我发现 here System.TypeSystem.Runtime.InteropServices.Marshal 实例化对象然后将其转换为接口的另一个示例:

IntPtr ptrRet;
SHGetMalloc(out ptrRet);

System.Type mallocType = System.Type.GetType("IMalloc");
Object obj = Marshal.GetTypedObjectForIUnknown(ptrRet,mallocType);
IMalloc pMalloc = (IMalloc)obj;

此方法似乎在请求指向接口的现有实例的指针。我在 Windows Shell 文档中找不到 SHGetMalloc for IDesktopWallpaper 之类的任何方法。

问题

那么,长话短说,获取 COM 接口实例的标准方法是什么?

如果没有放之四海而皆准的解决方案,可以使用哪些标准方式来获取 COM 接口的实例并在这些方法在什么情况下最有用?

编辑

下载 Windows 10 SDK 并参照 IDesktopWallpaper interface documentation 的要求部分后,我发现您可以从 Shobjidl.h 中查找 MIDL 并将其用于GuidAttribute 用于接口声明,然后从 Shobjidl.idl 中查找 CLSID 并将其与 Type.GetTypeFromCLSID(Guid)Activator.CreateInstance(Type) 结合使用以获取实现 IDesktopWallpaper.

我现在也看到,CLSID 就是上面列出的第二种方法中用于看似任意对象的 GuidAttribute 的 CLSID。似乎此方法允许您通过实例化 class 然后将实例转换为 COM 接口来模拟对象的托管实例化。

不过 我仍然很想知道这是否是最好的方法,以及这种方法与其他方法相比有哪些优缺点。

您可以通过多种方法获取指向 COM 对象引用的指针:

  • P/Invoke CoCreateInstance
  • P/Invoke CLSIDFromProgIDCoCreateInstance
  • P/Invoke IRunningObjectTable.GetObject
  • Type.GetTypeFromCLSIDActivator.CreateInstance
  • Type.GetTypeFromProgIDActivator.CreateInstance
  • new SomeType() 其中 SomeType 标记为 ComImport

Activator.CreateInstancenew SomeType() 最终命中 CoCreateInstance(如果它们没有被各种 in-app-domain 东西拦截)。对 out-of-process 服务器的 CoCreateInstance 的调用最终将以 class 名字(我认为)命中 IRunningObjectTable。最佳选择取决于您要执行的操作:

  • 对于 in-process 服务器,只需使用 ComImport
  • 对于未在 .Net 中实现的 out-of-process 服务器,ComImport 可以工作,我更愿意调用 CoCreateInstance 来传递正确的 CLSCTX
  • 对于在 .Net 中实现的 .net 实现的 out-of-process 服务器,您必须直接调用 CoCreateInstance 以避免 ComImport 添加的 "optimizations" 会导致服务器 运行in-process
  • 如果您要处理绰号,请使用 IRunningObjectTable
  • 如果您开始使用 ProgID 而不是 CLSID,请使用 CLSIDFromProgIDType.GetTypeFromProgID

无论我们如何获取对象的引用,我们都是从 IUnknown(.Net 中的 object)开始,然后必须调用 IUnknown->QueryInterface 来获取指针到特定的界面。在 .Net 中调用 QueryInterface 是通过转换为标记为 ComVisible 的接口(通常用 GuidAttribute 注释)来实现的。

在您命名的示例中,您最终会得到:

// based off of https://bitbucket.org/ciniml/desktopwallpaper
[ComImport]
[Guid("B92B56A9-8B55-4E14-9A89-0199BBB6F93B")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface IDesktopWallpaper
{
    void SetWallpaper([MarshalAs(UnmanagedType.LPWStr)] string monitorID, [MarshalAs(UnmanagedType.LPWStr)] string wallpaper);

    [return: MarshalAs(UnmanagedType.LPWStr)]
    string GetWallpaper([MarshalAs(UnmanagedType.LPWStr)] string monitorID);

    [return: MarshalAs(UnmanagedType.LPWStr)]
    string GetMonitorDevicePathAt(uint monitorIndex);

    [return: MarshalAs(UnmanagedType.U4)]
    uint GetMonitorDevicePathCount();

    [return: MarshalAs(UnmanagedType.Struct)]
    Rect GetMonitorRECT([MarshalAs(UnmanagedType.LPWStr)] string monitorID);

    void SetBackgroundColor([MarshalAs(UnmanagedType.U4)] uint color);

    [return: MarshalAs(UnmanagedType.U4)]
    uint GetBackgroundColor();

    void SetPosition([MarshalAs(UnmanagedType.I4)] DesktopWallpaperPosition position);

    [return: MarshalAs(UnmanagedType.I4)]
    DesktopWallpaperPosition GetPosition();

    void SetSlideshow(IntPtr items);

    IntPtr GetSlideshow();

    void SetSlideshowOptions(DesktopSlideshowDirection options, uint slideshowTick);

    void GetSlideshowOptions(out DesktopSlideshowDirection options, out uint slideshowTick);

    void AdvanceSlideshow([MarshalAs(UnmanagedType.LPWStr)] string monitorID, [MarshalAs(UnmanagedType.I4)] DesktopSlideshowDirection direction);

    DesktopSlideshowDirection GetStatus();

    bool Enable();
}

[ComImport]
[Guid("C2CF3110-460E-4fc1-B9D0-8A1C0C9CC4BD")]
public class DesktopWallpaper
{
}

[Flags]
public enum DesktopSlideshowOptions
{
    None = 0,
    ShuffleImages = 0x01
}

[Flags]
public enum DesktopSlideshowState
{
    None = 0,
    Enabled = 0x01,
    Slideshow = 0x02,
    DisabledByRemoteSession = 0x04
}

public enum DesktopSlideshowDirection
{
    Forward = 0,
    Backward = 1
}

public enum DesktopWallpaperPosition
{
    Center = 0,
    Tile = 1,
    Stretch = 2,
    Fit = 3,
    Fill = 4,
    Span = 5,
}

[StructLayout(LayoutKind.Sequential)]
public struct Rect
{
    public int Left;
    public int Top;
    public int Right;
    public int Bottom;
}

使用示例如下:

public partial class Form1 : Form
{
    private IDesktopWallpaper Wallpaper;

    public Form1()
    {
        InitializeComponent();
    }

    protected override void OnLoad(EventArgs e)
    {
        base.OnLoad(e);

        this.Wallpaper = (IDesktopWallpaper)new DesktopWallpaper();

        uint monitorCount = Wallpaper.GetMonitorDevicePathCount();
        for (uint i = 0; i < monitorCount; i++)
        {
            lbMonitors.Items.Add(Wallpaper.GetMonitorDevicePathAt(i));
        }
    }

    private void lbMonitors_SelectedValueChanged(object sender, EventArgs e)
    {
        var path = (string)lbMonitors.SelectedItem;

        tbWallpaper.Text = Wallpaper.GetWallpaper(path);
    }
}

生成表格: