12

可能是一个菜鸟问题,但互操作还不是我的强项之一。

除了限制重载的数量之外,我还有什么理由应该声明我的 DllImports,例如:

[DllImport("user32.dll")]
public static extern int SendMessage(IntPtr hWnd, int msg, int wParam, IntPtr lParam);

并像这样使用它们:

IntPtr lParam = Marshal.AllocCoTaskMem(Marshal.SizeOf(formatrange));
Marshal.StructureToPtr(formatrange, lParam, false);

int returnValue = User32.SendMessage(_RichTextBox.Handle, ApiConstants.EM_FORMATRANGE, wParam, lParam);

Marshal.FreeCoTaskMem(lParam);

而不是创建有针对性的重载:

[DllImport("user32.dll")]
public static extern int SendMessage(IntPtr hWnd, int msg, int wParam, ref FORMATRANGE lParam);

并像这样使用它:

FORMATRANGE lParam = new FORMATRANGE();
int returnValue = User32.SendMessage(_RichTextBox.Handle, ApiConstants.EM_FORMATRANGE, wParam, ref lParam);

by ref 重载最终更容易使用,但我想知道是否存在我不知道的缺点。

编辑:

到目前为止,有很多很棒的信息。

@P Daddy:你有一个基于抽象(或任何)类的结构类的例子吗?我将签名更改为:

[DllImport("user32.dll", SetLastError = true)]
public static extern int SendMessage(IntPtr hWnd, int msg, int wParam, [In, Out, MarshalAs(UnmanagedType.LPStruct)] CHARFORMAT2 lParam);

没有In,OutMarshalAsSendMessage (我的测试中的 EM_GETCHARFORMAT)失败。上面的示例运行良好,但如果我将其更改为:

[DllImport("user32.dll", SetLastError = true)]
public static extern int SendMessage(IntPtr hWnd, int msg, int wParam, [In, Out, MarshalAs(UnmanagedType.LPStruct)] NativeStruct lParam);

我得到一个 System.TypeLoadException,它说 CHARFORMAT2 格式无效(我将尝试在此处捕获它)。

例外:

无法从程序集“CC.Utilities,Version=1.0.9.1212,Culture=neutral,PublicKeyToken=111aac7a42f7965e”加载类型“CC.Utilities.WindowsApi.CHARFORMAT2”,因为格式无效。

NativeStruct 类:

public class NativeStruct
{
}

我试过abstract了,添加StructLayout属性等,我得到了同样的例外。

[StructLayout(LayoutKind.Sequential)]
public class CHARFORMAT2: NativeStruct
{
    ...
}

编辑:

我没有按照常见问题解答,我问了一个可以讨论但没有得到肯定回答的问题。除此之外,该线程中还有很多有见地的信息。所以我会把它留给读者投票给答案。第一个投票超过 10 票将是答案。如果两天内没有答案(太平洋标准时间 12 月 17 日),我将添加我自己的答案,总结线程中的所有美味知识:-)

再次编辑:

我撒了谎,接受了 P Daddy 的回答,因为他是男人并且帮了很大的忙(他也有一只可爱的小猴子 :-P)

4

5 回答 5

15

如果结构在没有自定义处理的情况下是可编组的,我非常喜欢后一种方法,在这种方法中,您将 p/invoke 函数声明为采用ref(指向)您的类型。或者,您可以将您的类型声明为类而不是结构,然后您也可以传递null.

[StructLayout(LayoutKind.Sequential)]
struct NativeType{
    ...
}

[DllImport("...")]
static extern bool NativeFunction(ref NativeType foo);

// can't pass null to NativeFunction
// unless you also include an overload that takes IntPtr

[DllImport("...")]
static extern bool NativeFunction(IntPtr foo);

// but declaring NativeType as a class works, too

[StructLayout(LayoutKind.Sequential)]
class NativeType2{
    ...
}

[DllImport("...")]
static extern bool NativeFunction(NativeType2 foo);

// and now you can pass null

<pedantry>

顺便说一句,在您将指针作为 传递的示例中IntPtr,您使用了错误的Alloc. SendMessage不是 COM 函数,因此您不应该使用 COM 分配器。使用Marshal.AllocHGlobalMarshal.FreeHGlobal。他们的名字很糟糕;这些名称只有在您完成过 Windows API 编程时才有意义,甚至可能没有。 AllocHGlobal调用GlobalAllockernel32.dll,它返回一个HGLOBAL. 这曾经不同于16 位时代HLOCAL返回的, 但在 32 位 Windows 中它们是相同的。LocalAlloc

使用该术语HGLOBAL来指代(本机)用户空间内存块有点卡住了,我猜,设计这个Marshal类的人一定没有花时间去思考这对大多数.NET来说是多么不直观开发商。另一方面,大多数 .NET 开发人员不需要分配非托管内存,所以....

</pedantry>


编辑

您提到在使用类而不是结构时遇到 TypeLoadException,并要求提供示例。我使用 进行了快速测试CHARFORMAT2,因为看起来这就是您要使用的。

首先是 ABC 1

[StructLayout(LayoutKind.Sequential)]
abstract class NativeStruct{} // simple enough

StructLayout属性是必需的,否则您收到 TypeLoadException。

现在CHARFORMAT2上课:

[StructLayout(LayoutKind.Sequential, Pack=4, CharSet=CharSet.Auto)]
class CHARFORMAT2 : NativeStruct{
    public DWORD    cbSize = (DWORD)Marshal.SizeOf(typeof(CHARFORMAT2));
    public CFM      dwMask;
    public CFE      dwEffects;
    public int      yHeight;
    public int      yOffset;
    public COLORREF crTextColor;
    public byte     bCharSet;
    public byte     bPitchAndFamily;
    [MarshalAs(UnmanagedType.ByValTStr, SizeConst=32)]
    public string   szFaceName;
    public WORD     wWeight;
    public short    sSpacing;
    public COLORREF crBackColor;
    public LCID     lcid;
    public DWORD    dwReserved;
    public short    sStyle;
    public WORD     wKerning;
    public byte     bUnderlineType;
    public byte     bAnimation;
    public byte     bRevAuthor;
    public byte     bReserved1;
}

我使用using语句将别名System.UInt32DWORDLCIDCOLORREF,并别名System.UInt16WORD。我尽量让我的 P/Invoke 定义符合 SDK 规范。 CFM并且CFE包含enums这些字段的标志值。为简洁起见,我省略了它们的定义,但如果需要,可以添加它们。

我已经声明SendMessage为:

[DllImport("user32.dll", CharSet=CharSet.Auto)]
static extern IntPtr SendMessage(
    HWND hWnd, MSG msg, WPARAM wParam, [In, Out] NativeStruct lParam);

HWNDSystem.IntPtr是、MSGisSystem.UInt32WPARAMis的别名System.UIntPtr

[In, Out]需要属性 onlParam才能正常工作,否则,它似乎不会双向编组(在调用本机代码之前和之后)。

我称之为:

CHARFORMAT2 cf = new CHARFORMAT2();
SendMessage(rtfControl.Handle, (MSG)EM.GETCHARFORMAT, (WPARAM)SCF.DEFAULT, cf);

EM并且为了(SCF相对enum)简洁,我再次被遗漏了。

我通过以下方式检查成功:

Console.WriteLine(cf.szFaceName);

我得到:

微软无衬线字体

奇迹般有效!


嗯,或者不,取决于你有多少睡眠,以及你想一次做多少事情,我想。

如果是blittable类型,这将起作用。(blittable 类型是一种在托管内存中与在非托管内存中具有相同表示的类型。)例如,该类型确实如所描述的那样工作。CHARFORMAT2MINMAXINFO

[StructLayout(LayoutKind.Sequential)]
class MINMAXINFO : NativeStruct{
    public Point ptReserved;
    public Point ptMaxSize;
    public Point ptMaxPosition;
    public Point ptMinTrackSize;
    public Point ptMaxTrackSize;
}

这是因为 blittable 类型并没有真正编组。它们只是固定在内存中——这可以防止 GC 移动它们——并且它们在托管内存中的位置地址被传递给本机函数。

非 blittable 类型必须被编组。CLR 分配非托管内存并在托管对象和它的非托管表示之间复制数据,在执行过程中在格式之间进行必要的转换。

由于成员,该CHARFORMAT2结构是不可blittable string。CLR 不能只将一个指针传递给一个string预期为固定长度字符数组的 .NET 对象。所以CHARFORMAT2必须对结构进行编组。

看起来,要进行正确的封送处理,必须使用要封送处理的类型声明互操作函数。换句话说,给定上述定义,CLR 必须根据NativeStruct. 我猜它正确地检测到对象需要被封送,但随后只“封送”一个零字节对象,即其NativeStruct自身的大小。

因此,为了使您的代码适用于CHARFORMAT2(以及您可能使用的任何其他非 blittable 类型),您必须返回声明SendMessage为获取CHARFORMAT2对象。抱歉,我在这件事上让你误入歧途。


先前编辑的验证码:

小鞭子

是的,鞭子很好!


科里,

这是题外话,但我注意到您正在制作的应用程序中存在潜在问题。

富文本框控件使用标准的 GDI 文本测量和文本绘图功能。为什么这是个问题?因为,尽管声称 TrueType 字体在屏幕上看起来与在纸上看起来一样,但 GDI 并没有准确地放置字符。问题是四舍五入。

GDI 使用全整数例程来测量文本和放置字符。每个字符的宽度(以及每行的高度,就此而言)四舍五入到最接近的整数像素,没有错误校正。

在您的测试应用程序中可以很容易地看到该错误。将字体设置为 Courier New 12 磅。这种固定宽度的字体应该每英寸恰好间隔 10 个字符,或每字符 0.1 英寸。这应该意味着,给定 5.5 英寸的起始行宽,在换行发生之前,您应该能够在第一行放置 55 个字符。

ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz123

但是,如果您尝试,您会发现仅在 54 个字符之后发生换行。更重要的是,第 54字符和第 53 个字符的一部分悬垂在标尺栏上显示的明显边距上。

这假设您的设置为标准 96 DPI(普通字体)。如果您使用 120 DPI(大字体),您不会看到此问题,尽管在这种情况下您的控件大小似乎不正确。您也不太可能在打印页面上看到这一点。

这里发生了什么?问题是 0.1 英寸(一个字符的宽度)是 9.6 像素(同样,使用 96 DPI)。GDI 不使用浮点数分隔字符,因此它会将其四舍五入为 10 像素。所以 55 个字符占用 55 * 10 = 550 像素 / 96 DPI = 5.7291666... 英寸,而我们期望的是 5.5 英寸。

虽然这在文字处理器程序的正常用例中可能不太明显,但有可能出现自动换行出现在屏幕上与页面上的不同位置的情况,或者一旦打印出来的东西排列不一样他们在屏幕上做了。如果这是您正在开发的商业应用程序,这对您来说可能是个问题。

不幸的是,要解决这个问题并不容易。这意味着您将不得不放弃富文本框控件,这意味着您自己实现它为您所做的一切会非常麻烦,这是相当多的。这也意味着您必须实现的文本绘制代码变得相当复杂。我有代码可以做到这一点,但是在这里发布太复杂了。但是,您可能会发现此示例示例很有帮助。

祝你好运!


1抽象基类

于 2009-12-15T22:53:42.403 回答
3

我有一些有趣的案例,其中参数类似于ref Guid parent,相应的文档说:

“指向指定父级的 GUID 的指针。传递一个空指针以使用[插入一些系统定义的项目] 。”

如果null(或IntPtr.Zero对于IntPtr参数)确实是无效参数,那么您可以使用ref参数 - 可能会更好,因为它更清楚您需要传递什么。

如果null是有效参数,则可以传递ClassType而不是ref StructType. 引用类型 ( class) 的对象作为指针传递,它们允许null.

于 2009-12-15T22:44:12.860 回答
2

不,您不能重载 SendMessage 并使 wparam 参数为 int。这将使您的程序在 64 位版本的操作系统上失败。它必须是一个指针,可以是 IntPtr、blittable 引用或 out 或 ref 值类型。重载 out/ref 类型也可以。


编辑:正如 OP 所指出的,这实际上不是问题。64 位函数调用约定通过寄存器而不是堆栈传递前 4 个参数。因此,wparam 和 lparam 参数不存在堆栈错位的危险。

于 2009-12-16T02:09:53.733 回答
1

我看不出有什么缺点。

By-ref 对于简单的类型和简单的结构通常就足够了。

如果结构具有可变大小或您想要进行自定义处理,则应优先使用 IntPtr。

于 2009-12-15T22:38:38.737 回答
1

使用ref比手动操作指针更简单,更不容易出错,所以我认为没有充分的理由不使用它......使用的另一个好处ref是您不必担心释放非托管分配的内存

于 2009-12-15T22:44:29.680 回答