5.3 设备上下文中的绘画函数

在这一小节里,我们来近距离的了解一下怎样在设备上下文中绘画。下表列出了设备上下文类主要的成员函数,在接下来的小节里,我们会举例介绍其中的大部分函数,你也可以从wxWidgets的手册中获取它们的详细信息。

Blit 把某个设备上下文的一部分拷贝到另外一个设备上下文上. 你可以指定的参数包括拷贝多大,拷贝到什么位置,拷贝的逻辑函数以及如果源设备上下文是内存设备上下文的时候,是不是使用透明遮罩等。
Clear 使用当前的背景刷刷新背景.
SetClippingRegion DestroyClippingRegion GetClippingBox 用来设置和释放设备上下文使用的区域。区域是用来将设备上下文中所有的绘画操作限制在某个范围内的,它可以是一个矩形,也可以是由wxRegion指定的复杂区域,使用GetClippingBox函数来取得包含当前区域的一个矩形范围。
DrawArc DrawEllipticArc 使用当前的画笔和画刷画一段圆弧或者椭圆弧线.
DrawBitmap DrawIcon 在指定位置画一副图片或者是一个图标.如果是图片可以指定一个透明遮罩.
DrawCircle 使用当前的画笔和画刷画一个圆形.
DrawEllipse 使用当前的画笔和画刷画一个椭圆形.
DrawLine DrawLines 使用当前的画笔画一条线或者多条线。最后的那个点是不被显示的。
DrawPoint 使用当前设置的画笔画一个点.
DrawPolygon DrawPolyPolygon DrawPolygon函数通过指定一个点的数组或者指向点结构的指针的列表来绘制一个封闭的多边形,还可以指定一个可选的座标平移。wxWidgets 会自动封闭第一个点和最后一个点,所以你不必在最开始和最末端指定同一个点。 DrawPolyPolygon函数则同时绘制多个多边形,在某些平台上这比多次调用DrawPolygon函数更有效率。
DrawRectangle DrawRoundedRectangle 使用当前的画笔和画刷绘制一个矩形或者圆角矩形
DrawText DrawRotatedText 使用当前字体,文本前景色和文本背景色在指定的位置绘制一段文本或者一段旋转的文本。
DrawSpline 使用当前的画笔在指定的控制点下使用云行规画一条平滑曲线.
FloodFill 以Flood填充的方式使用当前的画刷对指定起始点的范围进行填充(比如:封闭相邻同颜色区域中的颜色将被替换).
Ok 如果设备已经准备好可以开始绘画则返回true.
SetBackground GetBackground 用来设置或者获取背景画刷设置。这些设置将在Clear函数或者其它一些设置了复杂的绘画逻辑参数的函数中被使用。默认设置为wxtrANSPARENT_BRUSH。
SetBackgroundMode GetBackgroundMode 用来设置绘制文本时候的背景类型,取值为wxSOLID或者wxTRANSPARENT。通常你希望设置为后者,以便保留绘制文本区域已经存在的背景。
SetBrush GetBrush 用来设置当前画刷,默认值未定义。
SetPen GetPen 用来设置当前画笔,默认值未定义。
SetFont GetFont 用来设置当前字体,默认值未定义。
SetPalette GetPalette 用来设置当前调色板。
SetTextForeground GetTextForeground SetTextBackground GetTextBackground 用来设置文本的前景颜色和背景颜色,默认值前景为黑色背景为白色。
SetLogicalFunction GetLogicalFunction 逻辑函数用来设置画笔或者画刷或者(在Blit函数中)设备上下文自己的象素的显示和传输方式。默认值wxCOPY表示直接显示或者拷贝当前颜色。
GetPixel 返回某个点的颜色.在wxPostScriptDC和wxMetafileDC中还没有实现这个功能.
GetTextExtent GetPartialTextExtents 返回给定文本的大小。
GetSize GetSizeMM 返回以设备单位或者毫米指定的长宽。
StartDoc EndDoc 这两个函数只在打印设备上下文中使用,前者显示一条消息表明正在打印,后者则隐藏这个消息。
StartPage EndPage 在打印设备上下文中开始和结束一页。
DeviceToLogicalX DeviceToLogicalXRel DeviceToLogicalY DeviceToLogicalYRel 将设备座标转换成逻辑座标,可以是绝对值也可以是相对值。
LogicalToDeviceX LogicalToDeviceXRel LogicalToDeviceY LogicalToDeviceYRel 将逻辑座标转换成设备座标。
SetMapMode GetMapMode 象前面描述过的那样,MapMode用来(和SetUserScale一起)指定逻辑单位到设备单位的映射。
SetAxisOrientation 用来指定X轴和Y轴的方向。默认X轴为从左到右(True),Y轴为从上到下(False)。
SetDeviceOrigin GetDeviceOrigin 设置座标原点,可以用来实现平移。
SetUserScale GetUserScale 设置缩放值。该值用于逻辑单位到设备单位的转换。

绘制文本

设备上下文绘制文本的方式取决于以下几个参数:当前字体,字体背景模式,文本背景色和文本前景色。如果背景模式是wxSOLID,文本背后的部分将会以当前的文本背景色擦除,如果是wxTRANSPARENT,则文本的背景将保留原先的背景。

传递给DrawText的参数是一个字符串和一个点(或者两个整数)。其中点(或者两个整数)指定的位置将会是文本最左上角的位置。下面是一个例子:

void DrawTextString(wxDC& dc, const wxString& text,
                    const wxPoint& pt)
{
    wxFont font(12, wxFONTFAMILY_SWISS, wxNORMAL, wxBOLD);
    dc.SetFont(font);
    dc.SetBackgroundMode(wxTRANSPARENT);
    dc.SetTextForeground(*wxBLACK);
    dc.SetTextBackground(*wxWHITE);
    dc.DrawText(text, pt);
}

你也可以使用DrawRotatedText函数来绘制一段旋转的文本,其中角度的值由函数的最后一个参数指定,下面的代码演示了一段以45度角增加的文本:

wxFont font(20, wxFONTFAMILY_SWISS, wxNORMAL, wxNORMAL);
dc.SetFont(font);
dc.SetTextForeground(wxBLACK);
dc.SetBackgroundMode(wxTRANSPARENT);
for (int angle = 0; angle < 360; angle += 45)
    dc.DrawRotatedText(wxT("Rotated text..."), 300, 300, angle);

运行结果如下图所示:

在Windows平台上,只有TrueType类型的字体才可以实现旋转输出,要注意wxNORMAL_FONT指定的字体并不是TrueType字体。

通常情况下,你需要知道当前正要绘制的文本将占用设备上下文中多大的地方,你可以通过GetTextExtent函数来作到这一点,它的原型如下:

void GetTextExtent(const wxString& string,
    wxCoord* width, wxCoord* height,
    wxCoord* descent = NULL, wxCoord* externalLeading = NULL,
    wxFont* font = NULL);

从这个函数的原型中可以看出,后面的三个参数是可选的,其中descent参数和externalLeading参数(译者注:在汉字里面用处不大)的含义是:英文字符的基线通常不是字符的最底端,descent用来获取某个字符的最底端到基线的距离,而externalLeading则用来获取descent到下一行顶端的距离。最后一个参数font可以用来指定以这个字体为基准(而不是设备上下文自己的字体)来获取测量值。

下面的代码把一段文本在窗口的中间位置显示:

void CenterText(const wxString& text, wxDC& dc, wxWindow* win)
{
    dc.SetFont(*wxNORMAL_FONT);
    dc.SetBackgroundMode(wxTRANSPARENT);
    dc.SetTextForeground(*wxRED);
    // 获取窗口大小和文本大小
    wxSize sz = win->GetClientSize();
    wxCoord w, h;
    dc.GetTextExtent(text, & w, & h);
    // 计算为了居中显示需要的文本开始位置
    // 并保证其不为负数.
    int x = wxMax(0, (sz.x - w)/2);
    int y = wxMax(0, (sz.y - h)/2);
    dc.DrawText(msg, x, y);
}

如果你需要知道每个字符的精确占用大小,你可以使用GetPartialTextExtents函数,它使用wxString和一个wxArrayInt的引用作为参数。这个函数的效率在某些平台上优于对每个单个的字符使用GetTextExtent函数。

绘制线段和形状

这里用到的函数原型包括那些用来画点,画线,矩形,圆形以及椭圆等的函数。它们都使用当前的画笔设置和画刷设置,画笔用来决定轮廓线的颜色和模式,画刷用来决定填充的颜色和方式:

下面演示了一段代码和这段代码产生的图形:

void DrawSimpleShapes(wxDC& dc)
{
    // 设置黑色的轮廓线,绿色的填充色
    dc.SetPen(wxPen(*wxBLACK, 2, wxSOLID));
    dc.SetBrush(wxBrush(*wxGREEN, wxSOLID));

    // 画点
    dc.DrawPoint(5, 5);

    // 画线
    dc.DrawLine(10, 10, 100, 100);

    // 画矩形
    dc.SetBrush(wxBrush(*wxBLACK, wxCROSS_HATCH));
    dc.DrawRectangle(50, 50, 150, 100);

    // 改成红色画刷
    dc.SetBrush(*wxRED_BRUSH);

    // 画圆角矩形
    dc.DrawRoundedRectangle(150, 20, 100, 50, 10);

    // 没有轮廓线的圆角矩形
    dc.SetPen(*wxTRANSPARENT_PEN);
    dc.SetBrush(wxBrush(*wxBLUE));
    dc.DrawRoundedRectangle(250, 80, 100, 50, 10);

    // 改变颜色
    dc.SetPen(wxPen(*wxBLACK, 2, wxSOLID));
    dc.SetBrush(*wxBLACK);

    // 画圆
    dc.DrawCircle(100, 150, 60);

    // 再次改变画刷颜色
    dc.SetBrush(*wxWHITE);

    // 画一个椭圆
    dc.DrawEllipse(wxRect(120, 120, 150, 50));
}

注意一个约定俗成的规矩,线上的最后一个点将不会绘制。

要绘制一段圆弧,需要使用DrawArc函数,提供一个起点,一个终点和一个圆心的位置。这段圆弧将以逆时针方向从起点画至终点,举例如下:

int x = 10, y = 200, radius = 20;
dc.DrawArc(xradius, y, x + radius, y, x, y);

绘制椭圆弧的函数DrawEllipticArc采用一个容纳这个椭圆的矩形的四个顶点,以及椭圆弧开始和结束的角度作为参数,如果开始和结束的角度相同,则将绘制一个完整的椭圆。

// 绘制一个包含在顶点为(10, 100),
// 大小为 200x40\. 圆弧角度从 270到420度的圆弧.
dc.DrawEllipticArc(10, 100, 200, 40, 270, 420);

如果你需要快速绘制很多条线段,那么DrawLines将会比多次调用DrawLine拥有更高的效率,下面的例子演示了快速绘制10个点之间的线段,并且指定了一个(100,100)的偏移量:

wxPoint points[10];
for (size_t i = 0; i < 10; i++)
{
  pt.x = i*10; pt.y = i*20;
}
int offsetX = 100;
int offsetY = 100;
dc.DrawLines(10, points, offsetX, offsetY);

DrawLines不会填充线段环绕的区域。如果你想在画线的同时填充其环绕区域,你需要使用DrawPolygon函数,它的参数包括点的个数,点的列表,可选的平移参数以及填充类型。填充类型默认为wxODDEVEN_RULE,也可以使用wxWINDING_RULE。而 DrawPolygonPolygon用来同时绘制多个Polygon,它的额外的一个参数是另外一个整数的数组,用来指定在前面的点的数组中每个 Polygon中点的个数。

下面代码演示了怎样使用这两个函数绘制polygons:

void DrawPolygons(wxDC& dc)
{
    wxBrush brushHatch(*wxRED, wxFDIAGONAL_HATCH);
    dc.SetBrush(brushHatch);
    wxPoint star[5];
    star[0] = wxPoint(100, 60);
    star[1] = wxPoint(60, 150);
    star[2] = wxPoint(160, 100);
    star[3] = wxPoint(40, 100);
    star[4] = wxPoint(140, 150);
    dc.DrawPolygon(WXSIZEOF(star), star, 0, 30);
    dc.DrawPolygon(WXSIZEOF(star), star, 160, 30, wxWINDING_RULE);
    wxPoint star2[10];
    star2[0] = wxPoint(0, 100);
    star2[1] = wxPoint(-59, -81);
    star2[2] = wxPoint(95, 31);
    star2[3] = wxPoint(-95, 31);
    star2[4] = wxPoint(59, -81);
    star2[5] = wxPoint(0, 80);
    star2[6] = wxPoint(-47, -64);
    star2[7] = wxPoint(76, 24);
    star2[8] = wxPoint(-76, 24);
    star2[9] = wxPoint(47, -64);
    int count[2] = {5, 5};
    dc.DrawPolyPolygon(WXSIZEOF(count), count, star2, 450, 150);
}

结果如下图所示:

使用云行规画平滑曲线

DrawSpline函数让你可以绘制一个称为�云行规�的平滑曲线.这个函数有三个点和多个点两个形式,下面的代码对两者都进行了演示:

// 三点云行规曲线
dc.DrawSpline(10, 100, 200, 200, 50, 230);
// 五点云行规曲线
wxPoint star[5];
star[0] = wxPoint(100, 60);
star[1] = wxPoint(60, 150);
star[2] = wxPoint(160, 100);
star[3] = wxPoint(40, 100);
star[4] = wxPoint(140, 150);
dc.DrawSpline(WXSIZEOF(star), star);

结果如下图所示:

绘制位图

在设备上下文上绘制位图有两种主要的方法:DrawBitmap和Blit。DrawBitmap其实是Blit的一种简写形式,它使用一个位图,一个位置和一个bool类型的透明标志参数。根据图像的制作和读取过程的不同,位图的透明绘制可以通过指定一个透明遮罩或者一个Alpha通道(通常用来实现半透明)来实现,下面的代码演示了在一段文本上显示的半透明的图片:

wxString msg = wxT("Some text will appear mixed in the image's shadow...");
int y = 75;
for (size_t i = 0; i < 10; i++)
{
    y += dc.GetCharHeight() + 5;
    dc.DrawText(msg, 200, y);
}
wxBitmap bmp(wxT("toucan.png"), wxBITMAP_TYPE_PNG);
dc.DrawBitmap(bmp, 250, 100, true);

运行结果如下图所示:文本在图片下面以半透明的方式隐隐浮现。

Blit函数就略微显的复杂些,它允许你拷贝一个设备上下文的一部分到另外一个设备上下文,下面是这个函数的原型:

bool Blit(wxCoord destX, wxCoord destY,
          wxCoord width, wxCoord height, wxDC* dcSource,
          wxCoord srcX, wxCoord srcY,
          int logicalFunc = wxCOPY,
          bool useMask = false,
          wxCoord srcMaskX = -1, wxCoord srcMaskY = -1);

这个函数将dcSource参数指定的设备上下文中开始于srcX,srcY的位置,大小为width,height的区域拷贝到目标设备上下文(函数调用者自己)的destX,destY的位置,并且使用指定的逻辑函数进行拷贝。默认的逻辑函数是wxCOPY,意味着直接把源中的象素原封不动的传输到目标去。其它的逻辑函数依照平台的不同有所不同,不是所有的逻辑函数都支持所有的平台。我们将在本节稍后对逻辑函数进行专门介绍。

最后三个参数仅在园设备上下文为透明位图的时候才有效。useMask参数指定是否使用透明遮罩,而srcMaskX和srcMaskY则可以通过其设置不采用和主位图一致的遮罩位置。

下面的代码演示了怎样读取一个位图并将其平铺在另外一个更大的设备上下文上,并且保留图片本身的透明属性:

wxMemoryDC dcDest;
wxMemoryDC dcSource;
int destWidth = 200, destHeight = 200;
// 创建目标位图
wxBitmap bitmapDest(destWidth, destHeight);
// 加载调色板位图
wxBitmap bitmapSource(wxT("pattern.png"), wxBITMAP_TYPE_PNG);
int sourceWidth = bitmapSource.GetWidth();
int sourceHeight = bitmapSource.GetHeight();
// 用白色清除目标背景
dcDest.SelectObject(bitmapDest);
dcDest.SetBackground(*wxWHITE_BRUSH);
dcDest.Clear();
dcSource.SelectObject(bitmapSource);
// 将小的位图平铺到大的位图
for (int i = 0; i < destWidth; i += sourceWidth)
    for (int j = 0; j < destHeight; j += sourceHeight)
    {
        dcDest.Blit(i, j, sourceWidth, sourceHeight,
                    & dcSource, 0, 0, wxCOPY, true);
    }
//释放内存设备上下文的位图部分
dcDest.SelectBitmap(wxNullBitmap);
dcSource.SelectBitmap(wxNullBitmap);

你可以使用DrawIcon函数直接在设备上下文的某个位置显示图标,图标将总以透明方式显示:

#include "file.xpm"
wxIcon icon(file_xpm);
dc.DrawIcon(icon, 20, 30);

填充特定区域

FloodFill函数采用三个参数来填充某个特定的区域。一个起始点参数,一个颜色参数用来确定填充的边界和一个填充类型参数,设备上下文将使用当前的画刷定义进行填充。

下面的例子演示了绘制一个绿色矩形区域,它的轮廓线是红色,先进行黑色填充,进行蓝色填充:

// 画一个红边绿色的矩形
dc.SetPen(*wxRED_PEN);
dc.SetBrush(*wxGREEN_BRUSH);
dc.DrawRectangle(10, 10, 100, 100);
dc.SetBrush(*wxBLACK_BRUSH);
// 将绿色区域变成黑色
dc.FloodFill(50, 50, *wxGREEN, wxFLOOD_SURFACE);
dc.SetBrush(*wxBLUE_BRUSH);
// 开始填充蓝色(直到遇到红色)
dc.FloodFill(50, 50, *wxRED, wxFLOOD_BORDER);

如果找不到指定的颜色,这个函数可能返回失败,如果指定的点在当前区域以外,这个函数也将返回失败。这个函数不支持打印设备上下文以及wxMetafileDC。

逻辑函数

逻辑函数指定在绘画时,源象素怎样和目标象素进行合并操作,默认的wxCOPY只是使用源象素取代目标象素。其它的值则指定了一种逻辑操作。比如wxINVERT指定将目标象素的值取反作为新的值,这通常被用来绘制临时边框,因为使用这种方法进行绘图在第二次以同样的方法绘图的时候将会恢复原样。

下面的例子演示了怎样绘制一条点线段,然后再将相关区域恢复原来的样子。

wxPen pen(*wxBLACK, 1, wxDOT);
dc.SetPen(pen);
// 以取反逻辑函数绘制
dc.SetLogicalFunction(wxINVERT);
dc.DrawLine(10, 10, 100, 100);
// 再次绘制
dc.DrawLine(10, 10, 100, 100);
// 恢复正常绘制方法
dc.SetLogicalFunction(wxCOPY);

逻辑函数的另外一个用法是通过组合的方式创建新的图形。例如,我们可以用下面的方法来通过一个图片创建一组对应的拼图游戏需要的方块。首先在一幅白色背景的图片上使用固定但是随机的大小创建一个黑色轮廓线的网格作为拼图板的边界,然后对于每一个网格小块,使用flood-fill的方法将其填充成黑色以便创建一个白色背景上的黑色小方块,然后使用wxAND_REVERSE逻辑函数将原图Blit到这个模板,这样作的结果是使得方块内的部分变成原图,而白色的背景则变成黑色的背景,然后我们再指定黑色为透明色创建一个wxImage,然后将其转换成透明的wxBitmap对象,就可以直接在拼图游戏中使用了。(注意这样的作法需要原图中没有黑色,否则在拼图小方块中就会出现没有颜色的窟窿了)。

下表列出了所有的逻辑函数以及它们的含义:

逻辑函数 含义 (src = 源, dst = 目的)
wxAND src AND dst
wxAND_INVERT (NOT src) AND dst
wxAND_REVERSE src AND (NOT dst)
wxCLEAR 0
wxCOPY src
wxEQUIV (NOT src) XOR dst
wxINVERT NOT dst
wxNAND (NOT src) OR (NOT dst)
wxNOR (NOT src) AND (NOT dst)
wxNO_OP dst
wxOR src OR dst
wxOR_INVERT (NOT src) OR dst
wxOR_REVERSE src OR (NOT dst)
wxSET 1
wxSRC_INVERT NOT src
wxXOR src XOR dst