这两天一直被一个问题烦着:SendMessage在C#始终不正常。因为SendMessage是标准的WinAPI,在C/C++中,Structure可以很方便通过取地址传递给SendMessage,比如Richedit中常用的EM_GETCHARFORMAT消息:

SendMessage( hWnd, EM_GETCHARFORMAT, ( WPARAM)SCF_SELECTION, (LPARAM)cfm );

在C#乃至CLR中,并没有对应的CHARFORMAT2,所以必须自己依照MSDN中CHARFORMAT2的C++定义:

[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
private struct CHARFORMAT2
{
    public int cbSize;
    public uint dwMask;
    public uint dwEffects;
    public int yHeight;
    public int yOffset;
    public int crTextColor;
    public byte bCharSet;
    public byte bPitchAndFamily;
    [MarshalAs(UnmanagedType.ByValArray, SizeConst = 32)]
    public char[] szFaceName;
    public short wWeight;
    public short sSpacing;
    public int crBackColor;
    public int LCID;
    public uint dwReserved;
    public short sStyle;
    public short wKerning;
    public byte bUnderlineType;
    public byte bAnimation;
    public byte bRevAuthor;
    public byte bReserved1;
}

LayoutKind和UnmanagedType.ByValArray是基本的Marshal常识,偶想强调的是CharSet这个Attribute,尽管CLR默认是CharSet.Auto,但部分语言会override这个属性,C#中就会默认改为CharSet.Ansi,这也是磕绊偶两天的罪魁祸首。

Structure定义好了,DllImport就比较方便,同样,必须制定相同的CharSet。因为Microsot在Windows NT开始已经在内核全部Unicode化,使用Ansi的CharSet会使得操作系统额外多出两部CharSet之间的转换,所以推荐Unicode或者Auto,Auto模式只在Windows 98和Windows ME中是Ansi。

对于SendMessage这个定义在USER32.DLL中的导出函数来说,可以定义很多不同的DllImport,常用的有如:

[DllImport("user32", CharSet = CharSet.Auto, SetLastError = true)]
private static extern int SendMessage(HandleRef hWnd, int msg, int wParam, int lParam);

[DllImport("user32", CharSet = CharSet.Auto)]
private static extern int SendMessage(HandleRef hWnd, int msg, int wParam, ref CHARFORMAT2 lParam);

[DllImport("user32", CharSet = CharSet.Auto, SetLastError = true)]
private static extern IntPtr SendMessage(HandleRef hWnd, int msg, Int32 wParam, ref IntPtr lParam);

[DllImport("USER32", EntryPoint = "SendMessage", ExactSpelling = true, CharSet = CharSet.Auto, SetLastError = true)]
private static extern IntPtr SendMessage(IntPtr hWnd, int msg, IntPtr wp, IntPtr lp);

最简单的方法莫过于第二种定义,它直接在LPARAM参数中使用了CHARFORMAT2的ref。C#编译器会直接生成内部GCHandle以完成Unmanaged和Managed Heap之间的数据转换。对于别的Structure,可以定义其自己的DllImport,并为LPARAM使用自身的Structure。

对于其他三种定义,需要自己转换IntPtr了。其方法无外乎以下两种:

GCHandle gch = GCHandle.Alloc(fmt);
IntPtr lParam = GCHandle.ToIntPtr(gch);
SendMessage(new HandleRef(this, Handle), EM_GETCHARFORMAT, SCF_SELECTION, ref lParam);
fmt = (CHARFORMAT2)Marshal.PtrToStructure(lParam, typeof(CHARFORMAT2));
gch.Free();

或:

IntPtr lparam = IntPtr.Zero;
lparam = Marshal.AllocCoTaskMem(Marshal.SizeOf(fmtRange));
Marshal.StructureToPtr(fmtRange, lparam, false);
SendMessage(Handle, EM_FORMATRANGE, wparam, lparam);
Marshal.FreeCoTaskMem(lparam);

特别的,是对于输出为String的WinAPI,可以使用StringBuilder,而不需要制定ref或者out。如:

[DllImport("user32.dll", EntryPoint="SendMessage")]
private static extern Int32 SendMessage (IntPtr hwnd, Int32 wMsg, Int32 wParam, StringBuilder lParam);

调用时:

const Int32 MAX_SIZE = 1024;
StringBuilder buffer = new StringBuilder(MAX_SIZE);
SendMessage(tb_Input.Handle, WM_GETTEXT, MAX_SIZE, buffer);

Issued by Alva Chien 2009.11.3