托管C++中GDI+和GDI混合编程技术
2009-06-18 20:07:05 来源:WEB开发网摘要
Visual C++.NET中,虽然MFC和托管C++均可以使用GDI+,但托管C++专为Visual C++程序员开发.NET框架应用程序而设计,它除了保留标准C++的全部功能,还可通过.NET Framework(.NET框架)来创建对象,实现自动化内存管理以及与其他.NET语言的互操作性。在托管C++中通过平台调用来实现GDI+和GDI的混合编程,不仅可以克服GDI+中的不足,如XOR(异或)的光栅操作(ROP),更主要的是可以借助MFC DLL来拓展GDI+的图形图像的开发能力。
关键词:托管; XOR; GDI; 平台调用; MFC DLL
1.引言
早期的Windows程序中,可以使用GDI(Graphics Device Interface,图形设备接口)在一个窗体中绘制图形、文本和图像,但它的功能比较有限,尤其是图像处理方面。GDI+是GDI的一个新版本,它不仅在GDI基础上添加许多新特性,而且对原有的GDI功能进行优化,并在为开发人员提供的二维矢量图形、文本、图像处理、区域、路径以及图形数据矩阵等方面构造了一系列相关的类。其中,图形类Graphics是GDI+接口中的一个核心类,许多绘图操作都可用它来完成。
与GDI相比,GDI+增加了渐变画刷、样条曲线、持久的路径对象、矩阵和矩阵变换、Alpha混色、色彩修正、消除走样以及元数据等新的特性。但是,GDI+却并不支持GDI中的AND(与)、OR(或)以及XOR(异或)等光栅操作(ROP)以及硬件加速。其中,XOR光栅操作是实现图元动态定位的橡皮条技术的最重要方法,其次GDI+中的图像处理速度上并不比GDI更具优势。为此,本文通过若干托管C++实例来探讨在托管环境下GDI+和GDI的混合编程的方法和技巧。
2.托管C++和GDI
在Visual C++ .NET 2003中,程序员可以使用MFC和托管C++( Managed Extensions for C++,C++托管扩展)等编程方式进行图形图像程序开发。MFC是一套Microsoft基础类库,它是使用面向对象技术对Windows API进行封装。因此在MFC中进行图形图像程序开发时既可以使用MFC类CDC来编程,也可直接使用GDI API中的函数和结构。
托管C++是在C++基础上建立的,用来为Visual C++程序员开发.NET框架应用程序而设计。它除了保留标准C++的全部功能,还可通过.NET Framework(.NET框架)来创建对象,实现自动化内存管理以及与其他.NET语言的互操作性。由于托管环境与非托管环境的区别,因此GDI API并不能像MFC那样直接在托管C++中进行调用。但在GDI+中的Graphics类[4]提供了与GDI交互的一些方法,如GetHdc和ReleaseHdc,分别用于获取或释放与Graphics对象相关联的设备环境句柄。
由于GDI API不使用托管代码,它使用的数据类型与托管C++中所用的数据类型不同,且它也不是COM对象,所以在托管C++使用GDI是通过平台调用(PInvoke)来实现的。
3.平台调用和数据封送
平台调用[5]是一种服务,它使托管代码能够调用DLL中实现的非托管函数,使用时需要指定Runtime::InteropServices命名空间。
3.1 调用GDI API函数的一般方法
在托管C++中调用GDI API(GDI32.DLL)一般是按标识 DLL中的函数、在托管代码中创建原型和函数调用三个部分。其中,函数调用与一般托管C++中调用相同,这里不作讨论。
在托管C++中,DLL 函数的标识是通过DllImport属性来操作的,它包括常用的EntryPoint、CharSet、ExactSpelling和CallingConvention等字段。EntryPoint字段用来指定要调用的DLL入口点的名称。CharSet字段用来指定控制名称损坏和封送字符串参数的方式。ExactSpelling字段用来指定是否在非托管DLL中搜索入口点指定的函数或方法名称。CallingConvention字段用来指定入口点的调用约定,默认为WinAPI。
需要说明的是,DLL 函数的标识中不一定全部指定上述字段,通过设置一个或多个字段可以改变DllImport属性的默认行为。例如:
using namespace System::Runtime::InteropServices;
typedef void* HDC;
[DllImport("gdi32", EntryPoint="LineTo")]
extern "C" bool LineTo(HDC hDC, int nXEnd, int nYEnd);
在托管代码中访问非托管DLL函数之前,首先需要用DllImport属性操作来指定该函数的名称以及将其导出的DLL的名称。获取以上信息后,就可以为该DLL中的非托管函数编写托管方法的定义,即在托管代码中创建函数或方法的原型。例如上面的紧跟DllImport属性操作后的代码是用来创建gdi32.dll中的LineTo函数原型,extern "C"是在托管C++中创建原型的标记,除了此标记名外,函数原型的声明和C++函数的声明是一样的,函数名可以与EntryPoint字段指定的函数名相同,也可不同。为了避免声明的函数名与托管程序中其他方法重名,通常将要调用的DLL函数放在一个自定义的命名空间或自定义类中。例如:
namespace GDI32API // 自定义的命名空间
{
using namespace System;
using namespace System::Runtime::InteropServices;
typedef void* HDC;
[DllImport("gdi32", EntryPoint="LineTo")]
extern "C" bool LineTo(HDC hDC, int nXEnd, int nYEnd);
}
此时调用LineTo函数时须指定其所在的命名空间,例如:
Graphics *g = this->panel1->CreateGraphics();
// 创建与panel1控件相关联的Graphics
IntPtr hdc = g->GetHdc();
GDI32API::LineTo( (GDI32API::HDC)hdc,100, 200 );
g->ReleaseHdc( hdc );
若将将要调用的DLL函数放在一个自定义类中,则该函数一般要定义成静态类型。但由于extern "C"标记会在自定义的类中出现编译错误,若不使用extern "C"标记,对于没有内置结构或类的GDI API函数是可以的。例如:
public __gc class GDI32API
{
public:
typedef void* HDC;
[DllImport("gdi32", EntryPoint="LineTo")]
static bool LineTo(HDC hDC, int nXEnd, int nYEnd);
};
3.2 数据封送
由于在 GDI API(在wingdi.h中列出)函数中所使用的数据类型和托管C++( .NET Framework内置值类型)存在一些区别(如表1所示),虽然在托管C++中可以不通过平台调用中的数据封送来直接调用GDI API,但对于结构、数组和字符串数据类型来说,通过使用平台调用中的属性和方法来封送数据可以更好地实现自己的数据定制。
表1 数据类型
wtypes.h | C++ | 托管C++ | .NET类名 | 说明 |
GDI句柄 | void * | void * | IntPtr, UIntPtr | 32 位 |
BYTE | unsigned char | unsigned char | Byte | 8 位 |
SHORT | short | short | Int16 | 16 位 |
WORD | unsigned | short unsigned | short UInt16 | 16 位 |
INT | int | int | Int32 | 32 位 |
UINT | unsigned int | unsigned int | UInt32 | 32 位 |
LONG | long | long | Int32 | 32 位 |
BOOL | long | bool | Boolean | 32 位 |
DWORD | unsigned long | unsigned long | UInt32 | 32 位 |
ULONG | unsigned long | unsigned long | UInt32 | 32 位 |
CHAR | char | char | Char | 用 ANSI 修饰 |
LPSTR | char * | String * [in], StringBuilder * [in, out] | String [in], StringBuilder [in, out] | 用 ANSI 修饰 |
LPCSTR | const char * | String * | String | 用 ANSI 修饰 |
LPWSTR | wchar_t * | String * [in], StringBuilder * [in, out] | String [in], StringBuilder [in, out] | 用 Unicode 修饰 |
LPCWSTR | const wchar_t * | String * | String | 用 Unicode 修饰 |
FLOAT | float | float | Single | 32 位 |
DOUBLE | double | double | Double | 64 位 |
(1) 封送结构
在托管C++中,当调用的GDI API函数有内置结构时,需要对其使用StructLayout属性来进行封送。通过该属性类的构造函数来指定被封送的结构的数据成员在非托管内存中的排列方式。当为LayoutKind::Explicit时,则每个成员必须使用FieldOffset属性来指定该字段在类型中的位置。当为LayoutKind::Sequential时,则强制将成员按其出现的顺序进行顺序布局。例如:
namespace GDI32API
{
using namespace System;
using namespace System::Runtime::InteropServices;
typedef void* HDC;
[StructLayout(LayoutKind::Sequential)]
public __value struct RECT
{
public:
long left; // long或Int32
long top;
long right;
long bottom;
};
[DllImport("gdi32", EntryPoint="GetClipBox")]
extern "C" int GetClipBox(HDC hDC, RECT* rect);
}
(2) 封送字符串和数组等
在托管C++中,使用MarshalAs属性类可以封送GDI API函数中的参数、内置结构的字段或返回值。MarshalAs属性通常需要指定UnmanagedType枚举来标识非托管数据的格式。
当需要封送String *字符串时,可指定UnmanagedType枚举中的LPStr、LPWStr或LPTStr来封送,这些类型分别对应于GDI API中的LPSTR、LPWSTR或LPTSTR字符串类型。例如:
[DllImport("gdi32", EntryPoint="TextOut")]
extern "C" bool TextOut(HDC hDC, int x, int y,
[MarshalAs(UnmanagedType::LPWStr)] String *str, int nNum);
若封送结构中的字符串成员,则需指定UnmanagedType::ByValTStr类型,并指定SizeConst值来确定要导入的字符串中的字符数。
当需要封送数组时,需指定UnmanagedType::LPArray类型,并指定SizeConst值来确定要导入的数组大小,根据需要也可用ArraySubType字段指定数组元素的数据类型。例如,若有DLL中有这样的非托管函数:
HRESULT New1(int ar[10]);
HRESULT New2(double ar[10][20]);
HRESULT New3(LPWSTR ar[10]);
则封送的托管代码如下:
void New1([MarshalAs(UnmanagedType::LPArray, SizeConst=10)] int ar __gc[]);
void New2([MarshalAs(UnmanagedType::LPArray, SizeConst=200)] double ar __gc[]);
void New2([MarshalAs(UnmanagedType::LPArray,
ArraySubType=UnmanagedType::LPWStr, SizeConst=10)] String[] ar);
若封送结构中的数组成员,则需指定UnmanagedType::ByValArray类型,并指定SizeConst值来确定要导入的数组大小。
3.3 实例
这个实例是用来显示实现绘制直线的橡皮条过程,如图1所示的窗体。其中平台调用的GDI函数和结构如下面的代码:
namespace GDI32
{
using namespace System;
using namespace System::Runtime::InteropServices;
typedef void* HDC;
typedef void* HPEN;
[StructLayout(LayoutKind::Sequential)]
public __value struct POINT
{
public:
long x; // long或Int32
long y;
};
[DllImport("gdi32", EntryPoint="SetROP2")]
extern "C" int SetROP2(HDC hDC, int fnDrawMode); // 设置光栅操作模式
[DllImport("gdi32", EntryPoint="CreatePen")]
extern "C" HPEN CreatePen(int fnPenStyle, int nWidth, unsigned long crColor);
// 创建画笔
[DllImport("gdi32", EntryPoint="SelectObject")]
extern "C" void* SelectObject(HDC hDC, void* hGdiobj);
// 选入GDI属性对象
[DllImport("gdi32", EntryPoint="LineTo")]
extern "C" bool LineTo(HDC hDC, int nXEnd, int nYEnd);
// 画线
[DllImport("gdi32", EntryPoint="MoveToEx")]
extern "C" bool MoveTo(HDC hDC, int x, int y, POINT* pt);
// 移动当前位置
}
在鼠标移动事件(MouseMove)处理方法中的主要代码如下:
private: System::Void On_MouseMove(System::Object * sender, System::Windows::Forms::MouseEventArgs * e)
{
……
Graphics *g = this->panel1->CreateGraphics();
// 创建与panel1控件相关联的Graphics
IntPtr hdc = g->GetHdc();
GDI32::HPEN hPen = GDI32::CreatePen( 0, 0, 0xA0A0A0 ); // 创建灰色画笔
GDI32::SelectObject( (GDI32::HDC)hdc, hPen ); // 选入画笔
GDI32::SetROP2( (GDI32::HDC)hdc, 7 ); // 7表示XORPEN模式
GDI32::MoveTo( (GDI32::HDC)hdc, pt.X, pt.Y, NULL );
GDI32::LineTo( (GDI32::HDC)hdc, ptPrev.X, ptPrev.Y );
ptPrev = Point( e->X, e->Y );
GDI32::MoveTo( (GDI32::HDC)hdc, pt.X, pt.Y, NULL );
GDI32::LineTo( (GDI32::HDC)hdc, ptPrev.X, ptPrev.Y );
g->ReleaseHdc( hdc );
}
图1 GDI+和GDI混合编程实例
4.调用MFC DLL封装的GDI
通过平台调用可以在托管C++中使用GDI API,但代码有时比较繁琐。事实上,还可以使用MFC DLL[6]来封装GDI API,然后再通过平台调用,则显得比较简洁。例如,创建一个扩展MFC DLL应用程序MFCGDIDLL,在MFCGDIDLL.cpp文件的最后添加下列代码:
extern "C" __declspec(dllexport)
void DrawGDIXorSolidLine( HDC hDC, DWORD color, int nWidth, int x1, int y1, int x2, int y2 )
{
HPEN pen = ::CreatePen( 0, nWidth, color );
HPEN oldPen = (HPEN)::SelectObject( hDC, pen );
int nOldDrawMode = ::SetROP2( hDC, R2_XORPEN );
::MoveToEx( hDC, x1, y1, NULL );
::LineTo( hDC, x2, y2 );
::SelectObject( hDC, oldPen );
::SetROP2( hDC, nOldDrawMode );
}
然后将编译后的mfcgdidll.dll复制到前面实例中的项目文件夹中,并添加下列平台调用的函数代码:
namespace MFCGDI
{
using namespace System;
using namespace System::Runtime::InteropServices;
typedef void* HDC;
[DllImport("mfcgdidll", EntryPoint="DrawGDIXorSolidLine")]
extern "C" void DrawGDIXorSolidLine( HDC hdc, unsigned long color,
int nWidth, int x1, int y1, int x2, int y2);
}
最后修改前面实例中的鼠标移动事件(MouseMove)处理方法中的代码:
private: System::Void On_MouseMove(System::Object * sender, System::Windows::Forms::MouseEventArgs * e)
{
……
Graphics *g = this->panel1->CreateGraphics();
// 创建与panel1控件相关联的Graphics
IntPtr hdc = g->GetHdc();
MFCGDI::DrawGDIXorSolidLine( (MFCGDI::HDC)hdc,
0xA0A0A0, 1, pt.X, pt.Y, ptPrev.X, ptPrev.Y );
ptPrev = Point( e->X, e->Y );
MFCGDI::DrawGDIXorSolidLine( (MFCGDI::HDC)hdc,
0xA0A0A0, 1, pt.X, pt.Y, ptPrev.X, ptPrev.Y );
g->ReleaseHdc( hdc );
}
5.结语
Visual C++.NET中,虽然MFC和托管C++均可以使用.NET框架中的GDI+,但托管C++专为Visual C++程序员开发.NET框架应用程序而设计,它除了保留标准C++的全部功能,还可通过.NET Framework(.NET框架)来创建对象,实现自动化内存管理以及与其他.NET语言的互操作性。在托管C++中通过平台调用来实现GDI+和GDI的混合编程,不仅可以克服GDI+中的不足,更主要的是可以借助MFC DLL来拓展GDI+的图形图像的开发能力。
更多精彩
赞助商链接