5.1 理解设备上下文

在wxWidgets中,所有的绘画相关的动作,都是由设备上下文完成的。每一个设备上下文都是wxDC的一个派生类。从来就没有直接在窗口上绘画这种事情,每次在窗口上绘画,都要先创建一个窗口绘画设备上下文,然后在这个上下文上绘画。其它一些设备上下文是和bitmap图片或者打印机绑定的,你也可以设计自己的设备上下文。这样作一个最大的好处就是,你的绘画的代码是可以共享的,如果你的代码使用一个wxDC类型的参数,顶多再增加一个用于缩放的分辨率,那么这段代码就可以被同时用于在窗口绘制和在打印机上绘制。 让我们先来描述一下设备上下文的主要属性。

一个设备上下文拥有一个座标体系,它的原点通常在画布的左上角。不过这个位置可以通过调用SetDeviceOrigin函数改变,这样的效果就相当于对随后在这个上下文上的作的画进行平移。这种使用方法在对wxScrolledWindow进行绘画的时候非常常见。你还可以调用 SetAxisOrientation来改变坐标系的放向,比如说你可以让Y轴是从下到上的放向而不是默认的从上到下的放向。

逻辑单位和设备单位是有区别的。设备单位是设备本地的单位,对于计算机屏幕来说,设备单位是一个象素,而对于一个打印机来说,设备单位是由它的分辨率决定的,这个分辨率可以用GetSize(用来取得设备单位的一页的大小)或者GetSizeMM(用来取得以毫米为单位的一页的大小)来获得。

设备上下文的映射模式定义了逻辑单位到设备单位的转换标准。要注意某些设备上下文(典型的例子比如:wxPostScriptDC)只支持wxMM_TEXT映射模式。下表列出了目前支持的映射模式:

wxMM_TWIPS 每个逻辑单位为1/20点, 或者1/1440英寸.
wxMM_POINTS 每个逻辑单位为1点, 或1/72英寸.
wxMM_METRIC 每个逻辑单位为1毫米.
wxMM_LOMETRIC 每个逻辑单位为1/10毫米.
wxMM_TEXT 每个逻辑单位为1象素.这是默认的映射模式.

你可以通过SetUserScale函数给逻辑单位指定一个缩放比例,这个比例乘以映射模式中指定的单位,可以得到实际逻辑单位和设备单位之间的关系,比如在wxMM_TEXT映射模式下,如果用户缩放比例为(1.0,1.0),那么逻辑单位和设备单位就是一样的,这也是设备上下文中映射模式和用户缩放比例的缺省值。

设备上下文还可以通过SetClippingRegion函数指定一个区域,这个区域以外的部分将不被显示,可以使用 DestroyClippingRegion函数清除设备上下文当前指定的区域。举个例子,为了将一个字符串显示的范围限制在某个矩形区域以内,你可以先指定这个矩形区域为这个设备上下文的当前区域,然后调用函数在这个设备上下文上绘制这个字符串,即使这个字符串很长,可能会超出这个矩形区域,超出的部分也将被截掉不被显示,然后再调用函数清除这个区域就可以了。

和现实世界一样,为了绘画,你需要先选择绘画的工具,如果你要画线,那么你需要选择画笔,要填充一个区域,你需要选择画刷,而当前字体,文本前景色和背景色等,则会决定你画布上的文本怎样显示。晚些时候我们会详细讨论这些。我们先来看看目前都支持那些设备上下文:

可用的设备上下文

下面列出了你可以使用的设备上下文:

  • wxClientDC. 用来在一个窗口的客户区绘画。
  • wxBufferedDC. 用来代替wxClientDC来进行双缓冲区绘画。
  • wxWindowDC. 用来在窗口的客户区和非客户区(比如标题栏)绘画.这个设备上下文极少使用而且也不是每个平台都支持。
  • wxPaintDC. 仅用在重绘事件的处理函数中,用来在窗口的客户区绘画。
  • wxBufferedPaintDC. 和wxPaintDC类似,不过采用双缓冲区进行绘画。
  • wxScreenDC. 用来直接在屏幕上绘画。
  • wxMemoryDC. 用来直接在图片上绘画。
  • wxMetafileDC. 用来创建一个图元文件(只支持Windows和Mac OS X).
  • wxPrinterDC. 用来在打印机上绘画。
  • wxPostScriptDC. 用来在PostScript文件上或者在支持PostScript的打印机上绘画。

接下来我们会描述一下怎样来创建和使用这些设备上下文。打印机设备上下文将会在本章稍后的“使用打印机的架构”小节中进行更详细的讨论。

使用wxClientDC在窗口客户区进行绘画

wxClientDC用来在非重绘事件处理函数中对窗口的客户区进行绘制。例如,如果你想作一个信手涂鸦的程序,你可以在你的鼠标事件处理函数中创建一个wxClientDC。这个设备上下文也可以用于擦除背景事件处理函数。

下面的例子演示了怎样使用鼠标在窗口上随便乱画:

BEGIN_EVENT_TABLE(MyWindow, wxWindow)
    EVT_MOTION(MyWindow::OnMotion)
END_EVENT_TABLE()
void MyWindow::OnMotion(wxMouseEvent& event)
{
    if (event.Dragging())
    {
        wxClientDC dc(this);
        wxPen pen(*wxRED, 1); // red pen of width 1
        dc.SetPen(pen);
        dc.DrawPoint(event.GetPosition());
        dc.SetPen(wxNullPen);
    }
}

在第19章,“使用文档和视图”中,实现了一个更具有可用性的涂鸦工具,它使用线段代替了点,并且实现了重做和撤消的操作。而且还存储了所有的线段,以便在windows需要重绘的时候重绘所有的线段。而使用上面的代码,你所作的画在下次windows重绘的时候将会消失。当然你还可以使用CaptureMouse和ReleaseMouse函数,以便在鼠标按下的时候,直接把鼠标限制在你的绘画窗口的客户区范围内。

一种替代wxClientDC的方法是使用wxBufferedDC,后者将你的所有绘画的结果保存在内存中,然后当自己被释放的时候一次性把所有的内容传输到窗口上。这样作的结果是使得整个绘画过程更平滑,如果你不希望用户看到绘画的结果一点一点的更新,你可以使用这种方法,使用 wxBufferedDC和使用wxClientDC的代码是完全一样的,另外为了提高效率,你可以在wxBufferedDC的构造函数中传递一个已经创建好的bitmap对象,以避免在每次创建wxBufferedDC的时候创建一个新的图片。

擦除窗口背景

窗口类通常会收到两种和绘画相关的事件:wxPaintEvent事件用来绘制窗口客户区的主要图形,而wxEraseEvent事件则用来通知应用程序擦除背景。如果你只拦截并处理了wxPaintEvent事件,则默认的wxEraseEvent事件处理函数会用最近一次的 wxWindow::SetBackgroundColour函数调用设置的颜色或者别的合适的颜色清除整个背景。

这也许看上去有些混乱,但是这种把背景和前景分开的方法可以更好的支持那些使用这种架构的平台(比如windows)上的控件。举个例子,如果你希望在一个窗口上使用某种纹路的背景,如果你在OnPaint函数中是平铺纹理贴图,你可能会看到一些闪烁,因为在绘画之前,系统进行了清除背景的动作,在这种背景和前景分离的架构下,你可以拦截wxEraseEvent事件,然后在其处理函数中什么事情都不做,或者你干脆在擦除背景事件处理函数中平铺你的背景图,而在前景绘制事件的处理函数中绘制前景(当然,这样也会带来一些双缓冲绘画方面的问题,我们接下来会谈到).

在某些平台上。仅仅是拦截wxEraseEvent事件仍然不足以阻止所有的系统默认的重绘动作,让你的窗口的背景不只是一个纯色背景的最安全的方法是调用wxWindow::SetBackgroundStyle然后传递wxBG_STYLE_CUSTOM参数。这将告诉 wxWidgets把所有背景重绘的动作留给应用程序自己处理。

如果你真的决定实现自己的背景擦除事件处理函数,你可以先调用wxEraseEvent::GetDC来尝试返回一个已经创建的设备上下文,如果返回的值为空,再创建一个wxClientDC设备上下文。这将允许wxWidgets不在擦除背景事件中固定创建一个设备上下文,象前面例子中提到的那样,擦除背景事件处理函数中可能根本不会用到设备上下文,因此在事件中创建一个设备上下文可能是不必要的。下面的代码演示了怎样在擦除背景事件使用平铺图片的方法绘制窗口背景:

BEGIN_EVENT_TABLE(MyWindow, wxWindow)
  EVT_ERASE_BACKGROUND(MyWindow::OnErase)
END_EVENT_TABLE()
void MyWindow::OnErase(wxEraseEvent& event)
{
    wxClientDC* clientDC = NULL;
    if (!event.GetDC())
        clientDC = new wxClientDC(this);
    wxDC* dc = clientDC ? clientDC : event.GetDC() ;
    wxSize sz = GetClientSize();
    wxEffects effects;
    effects.TileBitmap(wxRect(0, 0, sz.x, sz.y), *dc, m_bitmap);
    if (clientDC)
        delete clientDC;
}

和重绘窗口事件一样,wxEraseEvent::GetDC返回的设备上下文已经设置了区域,使得只有需要重绘的部分才会被重绘。

使用wxPaintDC在窗口上绘画

如果你定义了一个窗口重画事件处理函数,则必须在这个处理函数中产生一个wxPaintDC设备上下文(即使你根本不使用它),并且使用它来进行你需要的绘画动作。产生这个对象将告诉wxWidgets的窗口体系这个窗口的需要重画的区域已经被重画了,这样窗口系统就不会重复的发送重画消息给这个窗口了。在重画事件中,你还可以调用wxWindow::GetUpdateRegion函数来获得需要重画的区域,或者使用wxWindow:: IsExposed函数来判断某个点或者某个矩形区域是否需要重画,然后优化代码使得仅在这个范围内的内容被重画,虽然在重画事件中创建的 wxPaintDC设备上下文会自动将自己限制在需要重画的区域内,不过你自己知道需要重画的区域的话,可以对代码进行相应的优化。

重画事件是由于用户和窗口系统的交互造成的,但是它也可以通过调用wxWindow::Refresh和wxWindow:: RefreshRect函数手动产生。如果你准确的知道窗口的哪个部分需要重画,你可以指定只重画那一部分区域以便尽可能的减少闪烁。这样作的一个问题是,并不能保证窗口在调用Refresh函数以后会马上重画。如果你真的需要立刻调用你的重画事件处理函数,比如说你在进行一个很耗时的计算,需要即时显示一些进度,你可以在调用Refresh或者RefreshRect以后调用wxWindow::Update函数。

下面的代码演示了如何在窗口正中位置画一个黑边红色的矩形区域,并且会判断这个区域是否位于需要更新的区域范围内以便决定是否需要重画。

BEGIN_EVENT_TABLE(MyWindow, wxWindow)
  EVT_PAINT(MyWindow::OnPaint)
END_EVENT_TABLE()
void MyWindow::OnPaint(wxPaintEvent& event)
{
    wxPaintDC dc(this);
    dc.SetPen(*wxBLACK_PEN);
    dc.SetBrush(*wxRED_BRUSH);
    // 获取窗口大小
    wxSize sz = GetClientSize();
    // 要绘制的矩形的大小
    wxCoord w = 100, h = 50;
    // 将我们的矩形设置在窗口正中,
    // 但是不为负数的位置
    int x = wxMax(0, (sz.xw)/2);
    int y = wxMax(0, (sz.yh)/2);
    wxRect rectToDraw(x, y, w, h);
    // 只有在需要的时候才重画以便提高效率
    if (IsExposed(rectToDraw))
        DrawRectangle(rectToDraw);
}

注意在默认情况下,当窗口大小改变时,只有那些需要重画的地方才会被更新,指定wxFULL_REPAINT_ON_RESIZE窗口类型可以覆盖这种默认情况以使得整个窗口都被刷新。在我们上面的例子中,就需要指定这种情况,因为我们矩形的位置是根据窗口大小计算出来的,如果窗口变大而我们只更新需要更新的部位,则可能在原来的窗口中留下半个矩形或者在屏幕上出现两个矩形,这和我们的初衷是不一致的。

wxBufferedPaintDC是wxPaintDC的双缓冲区版本。只需要简单的将重绘事件处理函数中的wxPaintDC换成 wxBufferedPaintDC就可以了,它会首先将所有的图片画在一个内存位图上,然后在自己被释放的时候一次性将其画在窗口上以减小闪烁。

正象我们前面提到的那样,另外一个减少闪烁的方法,是把背景和前景统一在窗口重画事件处理函数中,而不是将它们分开处理,配合 wxBufferedPaintDC,那么所有的绘画动作在完成之前都是在内存中进行的,这样在窗口被重绘之前你将看不到窗口背景被更新。你需要增加一个空的背景擦除事件处理函数,并且使用SetBackgroundStyle函数设置背景类型为wxBG_STYLE_CUSTOM以便告诉某些系统不要自动擦除背景。在wxScrolledWindow中你还有注意需要对绘画设备上下文的原点进行平移,并据此重新计算你自己的图片位置。(译者注:在 wxScrolledWindow窗口中的wxPaintDC的原点是当前窗口滚动位置下的原点,这通常不是我们所需要的,因为我们绘画通常要基于整个滚动窗口本身的原点,调用PrepareDC函数可以将其设置成滚动窗口本身的原点,在绘画的时候可以通过CalcUnscrolledPosition函数将当前客户区中的某个点转换成相对于整个滚动窗口区的座标,如下面的代码演示的那样)下面的代码演示了怎样在一个继承自 wxScrolledWindow的窗口类中进行绘画并且尽可能的避免出现闪烁。

#include "wx/dcbuffer.h"
BEGIN_EVENT_TABLE(MyCustomCtrl, wxScrolledWindow)
    EVT_PAINT(MyCustomCtrl::OnPaint)
    EVT_ERASE_BACKGROUND(MyCustomCtrl::OnEraseBackground)
END_EVENT_TABLE()
//重画事件处理函数
void MyCustomCtrl::OnPaint(wxPaintEvent& event)
{
    wxBufferedPaintDC dc(this);
    // 平移设备座标以便我们不需要关心当前滚动窗口的位置
    PrepareDC(dc);
    // 在重画绘制函数中绘制背景
    PaintBackground(dc);
    // 然后绘制前景
    ...
}
/// 绘制背景
void MyCustomCtrl::PaintBackground(wxDC& dc)
{
    wxColour backgroundColour = GetBackgroundColour();
    if (!backgroundColour.Ok())
        backgroundColour =
            wxSystemSettings::GetColour(wxSYS_COLOUR_3DFACE);
    dc.SetBrush(wxBrush(backgroundColour));
    dc.SetPen(wxPen(backgroundColour, 1));
    wxRect windowRect(wxPoint(0, 0), GetClientSize());   
    //我们需要平移当前客户区矩形的座标以便将其转换成相对于整个滚动窗口而不是当前窗口的座标
    //因为在前面我们已经对设备上下文进行了PrepareDC的动作。
    CalcUnscrolledPosition(windowRect.x, windowRect.y,
                           & windowRect.x, & windowRect.y);
    dc.DrawRectangle(windowRect);
}
// 空函数 只为了防止闪烁
void MyCustomCtrl::OnEraseBackground(wxEraseEvent& event)
{
}

为了提高性能,当你使用wxBufferedPaintDC时,你可以维护一个足够大的(比如屏幕大小的)位图,然后将其传递给 wxBufferedPaintDC的构造函数作为第二个参数,这可是避免每次使用wxBufferedPaintDC的时候创建和释放一个位图。

wxBufferedPaintDC通常会从其缓冲区中拷贝整个客户区(用户可见部分)大小,在滚动窗口中,其内部创建的设备上下文并不会随着PrepareDC的调用平移其座标系。不过你可以通过在wxBufferedPaintDC的构造函数中指定 wxBUFFER_VIRTUAL_AREA(默认为wxBUFFER_CLIENT_AREA)参数来避免这一问题。不过这种情况下,你需要提供一个和整个滚动窗口的虚拟大小一样的缓冲区,而这通常效率是很低的,应该尽量避免。另外一个需要注意的是对于设置为wxBUFFER_CLIENT_AREA的 wxBufferedPaintDC到目前为止还不支持缩放(SetUserScale)。

你可以在随书光盘的examples/chap12/thumbnail例子的wxThumbnailCtrl控件中,找到使用wxBufferedPaintDC的完整的例子。

使用wxMemoryDC在位图上绘图

内存设备上下文是和一个位图绑定的设备上下文,在这个设备上下文上的所有绘画都将画在那个位图上面。使用的方法是先使用默认的构造函数创建一个内存设备上下文,然后使用SelectObject函数将其和一个位图绑定,在完成所有的绘画以后再调用SelectObject函数参数为 wxNullBitmap来移除绑定的位图,代码如下所示:

wxBitmap CreateRedOutlineBitmap()
{
    wxMemoryDC memDC;
    wxBitmap bitmap(200, 200);
    memDC.SelectObject(bitmap);
    memDC.SetBackground(*wxWHITE_BRUSH);
    memDC.Clear();
    memDC.SetPen(*wxRED_PEN);
    memDC.SetBrush(*wxTRANSPARENT_BRUSH);
    memDC.DrawRectangle(wxRect(10, 10, 100, 100));
    memDC.SelectObject(wxNullBitmap);
    return bitmap;
}

你也可以使用Blit函数将内存设备上下文中的某一个区域拷贝到别的设备上下文上,在本章稍后的地方我们会对此进行介绍。

使用wxMetafileDC创建图元文件

wxMetafileDC适用于Windows和Mac OS X,它在这两个平台上分别提供了绘制Windows图元文件和Mac PICT的画布。允许在wxMetafile对象上绘画,这个对象保留了一组绘画动作记录,可以被其它应用程序使用或者通过wxMetafile:: Play函数将其绘制在别的设备上下文上。

使用wxScreenDC访问屏幕

使用wxScreenDC可以在整个屏幕的任何位置绘画。通常这在给拖放操作提供可见响应的时候比较有用(比如拖放分割窗口的分割条的时候)。处于性能方面的考虑。你可以将其操作的屏幕区域限制在一个矩形区域(通常是程序窗口所在的区域)内。当然,除了在屏幕上绘画,我们还可以把绘画从屏幕上拷贝到其它设备上下文中,以便实现屏幕捕获。因为无法限制别的应用程序的行为,所以wxScreenDC类通常在当前应用程序的窗口内工作的最好。

下面是将屏幕捕获到位图文件的例子:

wxBitmap GetScreenShot()
{
    wxSize screenSize = wxGetDisplaySize();
    wxBitmap bitmap(screenSize.x, screenSize.y);
    wxScreenDC dc;
    wxMemoryDC memDC;
    memDC.SelectObject(bitmap);
    memDC.Blit(0, 0, screenSize.x, screenSize.y, & dc, 0, 0);
    memDC.SelectObject(wxNullBitmap);
    return bitmap;
}

使用wxPrinterDC和wxPostScriptDC实现打印

wxPrinterDC用来实现打印机接口。在windows和Mac平台上,这个接口实现到标准打印接口的映射。在其它类Unix系统上,没有标准的打印接口,因此需要使用wxPostScriptDC代替(除非打开了Gnome打印支持,参考接下来的小节,“在类Unix系统上使用 GTK+进行打印”)。

可以通过多种途径创建wxPrinterDC对象,你还可以传递设置了纸张类型,横向或者纵向,打印份数等参数的 wxPrintData对象给它。一个简单的创建wxPrinterDC设备上下文的方法是显示一个wxPrintDialog对话框,在用户选择各种参数以后,使用wxPrintDialog::GetPrintDC的方法获取一个对应的wxPrinterDC对象。作为更高级的用法,你可以从 wxPrintout定义一个自己的派生类,以便更精确定义打印以及打印预览的行为,然后把它的一个实例传递给wxPrinter对象(在后面小节中还会详细介绍)。

如果你要打印的内容主要是文本,你可以考虑使用wxHtmlEasyPrinting类,以便忽略wxPrinterDC和 wxPrintout排版的细节:你只需要按照wxWidgets'实现的那些HTML的语法编写HTML文件,然后创建一个 wxHtmlEasyPrinting对象用来实现打印和预览,这通常可以节省你几天到几周的时候来对那些文本,表格和图片进行排版。详情请参考第12 章,“高级窗口类”。

wxPostScriptDC用来打印到PostScript文件。虽然这种方式主要应用在类Unix系统上,不过在别的系统上你一样可以使用它。用这种方式打印需要先产生PostScript文件,而且还要保证你拥有一个支持打印PostScript文件的打印机。

你可以通过传递wxPrintData参数,或者传递一个文件名,一个bool值以确定是否显示一个打印设置对话框和一个父窗口来创建一个wxPostScriptDC,如下所示:

#include "wx/dcps.h"
wxPostScriptDC dc(wxT("output.ps"), true, wxGetApp().GetTopWindow());
if (dc.Ok())
{
    // 告诉它在哪里找到AFM字体文件。
    dc.GetPrintData().SetFontMetricPath(wxGetApp().GetFontPath());
    // 设置分辨率(每英寸多少个点,默认720)
    dc.SetResolution(1440);
    // 开始绘画
    ...
}

wxPostScriptDC的一个特殊的地方在于你不能直接使用GetTextExtent来获取文本大小的信息。你必须先用 wxPrintData::SetFontMetricPath指定AFM(Adobe Font Metric)文件的路径,就象上面例子中的那样。你可以从下面的路径下载GhostScript AFM文件。

ftp://biolpc22.york.ac.uk/pub/support/gs_afm.tar.gz