如何获取COM接口的实例?(题目)

6
我一直在谷歌上搜索,试图找到获取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定义了一些类,该类被实例化,然后以与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对象。
另一个例子在这里找到,使用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用于IDesktopWallpaper的方法。
问题:
简而言之,获取COM接口实例的标准方式是什么?
如果没有通用解决方案,那么人们可以使用哪些标准方法来获取COM接口实例,并在什么情况下使用每种方法最有用?

在下载了Windows 10 SDK并参考IDesktopWallpaper接口文档的要求部分后,我发现你可以查找Shobjidl.h中的MIDL,并将其用于接口声明中的GuidAttribute,然后查找Shobjidl.idl中的CLSID,并与Type.GetTypeFromCLSID(Guid)Activator.CreateInstance(Type)一起使用,以获取实现IDesktopWallpaper的对象的实例。

我现在也看到CLSID是上述第二种方法中似乎任意对象的GuidAttribute所使用的。这种方法似乎允许您通过实例化类,然后将实例强制转换为COM接口来模拟托管实例化对象。

然而,我仍然想知道这是否是最好的方法以及与其他方法相比可能存在的优缺点。


请明确一下,你是否已经明确注册了你的服务? - Ross Bush
@RossBush 我不确定这是什么意思或如何实现,所以我想我还没有。 - Thick_propheT
1
你可能误解了COM架构。在COM中,你创建一个类的实例,然后请求所创建对象支持(实现)的某些接口。接口几乎没有对象那么重要。首先你必须找到对象,然后请求接口。有时这可以在一条语句中完成。 针对你的情况,我发现一个[创建DesktopWallpaper对象并请求IDesktopWallpaper接口的示例](https://github.com/Trellmor/WallpaperManager/blob/master/WallpaperManager/WinAPI/DesktopWallpaper.cs)。 - Alex P.
1个回答

6
您可以通过多种方法获取COM对象引用的指针:
- P/Invoke `CoCreateInstance` - P/Invoke `CLSIDFromProgID` → `CoCreateInstance` - P/Invoke `IRunningObjectTable.GetObject` - `Type.GetTypeFromCLSID` → `Activator.CreateInstance` - `Type.GetTypeFromProgID` → `Activator.CreateInstance` - `new SomeType()`,其中`SomeType`标记为`ComImport`
`Activator.CreateInstance`和`new SomeType()`最终会执行`CoCreateInstance`(如果没有被各种应用程序域截取)。对于进程外服务器的`CoCreateInstance`调用最终会使用类单体(`class moniker`)命中`IRunningObjectTable`(我想是这样)。最佳选项取决于您要做什么:
- 对于进程内服务器,只需使用`ComImport` - 对于未在.Net中实现的进程外服务器,`ComImport`将起作用,但我更喜欢调用`CoCreateInstance`以传递正确的`CLSCTX` - 对于在.Net中实现的进程外服务器,必须直接调用`CoCreateInstance`以避免由`ComImport`添加的"优化"导致服务器在进程内运行 - 如果您正在处理单体,则使用`IRunningObjectTable` - 如果您从ProgID开始而不是CLSID,则使用`CLSIDFromProgID`或`Type.GetTypeFromProgID`
无论我们如何获得对象引用,我们都会从`IUnknown`(`.Net`中的`object`)开始,然后必须调用`IUnknown->QueryInterface`以获取指向特定接口的指针。在`.Net`中,通过强制转换为标记为`ComVisible`(通常带有`GuidAttribute`)的接口来实现调用`QueryInterface`。
在您提到的示例中,最终会得到:
// 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);
    }
}

这将生成以下表单:

表格显示监视器列表和GetWallpaper的结果


感谢您详细的回答!在使用您的答案深入了解一些COM概念后,我认为我正在编写一个COM客户端(根据此处给出的定义https://msdn.microsoft.com/en-us/library/windows/desktop/ms683835(v=vs.85).aspx),而您的答案是为COM服务器提供详细信息的。在编写COM客户端的上下文中,是否有任何其他有用的信息?我无法使`var wallpaper = (IDesktopWallpaper)new DesktopWallpaper()`方法起作用。这是因为我的代码是COM客户端而不是服务器吗? - Thick_propheT
@Thick_propheT,当然。我的回答涉及到COM客户端,而不是COM服务器。C# COM服务器可以通过存在[ComVisible(true)]属性来识别,而C# COM客户端将具有[ComImport]属性。我已经添加了一个完整的翻译接口和相关枚举的示例。 - Mitch

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