# How do I draw to a device context

Now that you have your device context, you may want to actually draw some pictures of your own onto it. One advantage of the device context concept is that your program does not care which kind of device context is being used—the draw commands are the same no matter what.

Using wxPython, there are many ways to draw to a device context. The device context API defines about eighteen or so different methods that allow you to draw on the screen. Table 12.4 lists the first batch: methods that let you draw geometric shapes. Unless stated otherwise, all of these methods use the current pen to draw their lines, and the current brush to fill in their shape. We'll discuss more details about pens and brushes later in this chapter.

 Method Description CrossHair(x, y) Draws a cross-hair along the entire extent of the context—a horizontal line at the given y coordinate and a vertical line at the given x coordinate, meeting at the point (x, y). DrawArc(x1, y1, x2, y2, xc, yc) Draws a circular arc, starting at the point (xi, yi) and ending at the point (x2, y2). The center of the circle whose arc is being described is the point (xc, yc). The arc is drawn counterclockwise from the first point to the second point. The current brush is used to fill in the wedge shape. DrawCheckMark(x, y, width, height) Draws a check mark, as you'd see inside a selected check box, inside the rectangle with the upper left corner (x, y), and the given width and height. The brush is not used to fill the background. DrawCircle(x, y, radius) Draws a circle centered on (x, y) with the given radius.
 Method Description DrawEllipse(x, y, width, height) Draws an ellipse inscribed inside the rectangle with an upper left corner at (x, y) and with the given width and height. DrawEllipticArc(x, y, width, height, start, end) Draws an arc of an eclipse. The first four parameters are as in DrawEllipse. The start and end parameters are the start and end angles of the arc relative to the three-o'clock position from the center of the rectangle. Angles are specified in degrees (360 is a complete circle). Positive values mean counterclockwise motion. If start is equal to end, a complete ellipse will be drawn. DrawLine(x1, y1, x2, y2) Draws a line which starts at the point (x1, y1) and ends before the point (x2, y2) . (By long-standing and inscrutable graphic toolkit convention, the endpoint is not drawn by this method). DrawLines(points, xoffset=0, yoffset=0) Draws a series of lines. The points parameter is a list of instances of wx.Point (or two-element tuples that are converted to wx.Point). The lines are drawn from point to point until the end of the list. If the offset is used, it is applied to each point in the list, allowing a common shape to be drawn at any point in the DC. The brush is not used to fill in the shape. DrawPolygon(points, xoffset=0, yoffset=0 fillstyle= wx.ODDEVEN_RULE) Draws a series of lines, similar to DrawLines, except that a line is also drawn between the last point and the first, and the brush is used to fill the polygon. DrawPoint(x, y) Fills in the given point using the current pen. DrawRectangle(x, y, width, height) The point (x, y) is the upper left-hand corner of the rectangle, and the width and height parameters are the dimensions. DrawRoundedRectangle (x, y, width, height, radius=20) Exactly like DrawRectangle(), but with the corners replaced by 90 degrees of a circle. The radius parameter governs the curvature. If positive, it is the radius of the circle used in pixels. If negative, the value size of the circle is made proportional to the whichever dimension of the rectangle is smaller. (The exact formula is - radius * dimension) DrawSpline(points) Takes in a Python list of points and draws the appropriate spline curve. This curve is not filled in by the brush. FloodFill(x, y, color, style=wx.FLOOD SURF ACE) Fills the space with the color of the current brush. The algorithm starts at the point (x, y). If the style is wx.flood_surface, then all touching pixels which match the given color are redrawn. If the style is wx.flood_border, then all pixels are drawn until a border in the given color is reached.

For all of the Draw methods that take just an x and y parameter, there is a corresponding Draw...Point method that takes a wx.Point instance instead; for example, DrawCirclePoint(pt, radius). If the method has both an x, y and a width, height pair, then there is a method that takes both a wx.Point and a wx.Size, and is called Draw...PointSize, for example, DrawRectanglePointSize(pt, sz). Those methods also have a corresponding Draw...Rect version, that takes a wx.Rect instance, DrawRectangleRect(rect).

You can get the size of the device context using the method GetSize(), which returns a wx.Size instance. You can retrieve the color value at a specific pixel with the method GetPixel(x, y), which returns a wx.Color instance.

Figure 12.2 displays a screen shot of the picture we're going to build using the draw methods and double buffering.

 n Double Buffered Drawing EZ1O0 Sample 'Radar" Plot C D X B E 1 A F G

Figure 12.2 A sample radar graph

### Figure 12.2 A sample radar graph

Listing 12.2 displays a simple radar graph that plots a collection of values in the range of 0-100 onto a polar coordinate system designed to easily show outliers. You may use this kind of graph to monitor some sort of resource allocation metrics, and a quick glance at the graph can tell you when conditions are good (within some accepted tolerance level), or approaching critical levels (total resource consumption). In this sample, the graph is continually refreshed with random data. This is a long example that demonstrates a number of things we've shown thus far.

Listing 12.2 Drawing a radar graph import wx import math import random class RadarGraph(wx.Window):

def _init_(self, parent, title, labels):

wx.Window.__init__(self, parent) self.title = title self.labels = labels self.data = [0.0] * len(labels)

self.titleFont = wx.Font(14, wx.SWISS, wx.NORMAL, wx.BOLD) self.labelFont = wx.Font(10, wx.SWISS, wx.NORMAL, wx.NORMAL)

self.InitBuffer()

self.Bind(wx.EVT_SIZE, self.OnSize) self.Bind(wx.EVT_PAINT, self.OnPaint)

def OnSize(self, evt):

# When the window size changes we need a new buffer. self.InitBuffer()

def OnPaint(self, evt): A Refresh window dc = wx.BufferedPaintDC(self, self.buffer) <-J from the buffer

A Creating the buffer def InitBuffer(self): <—T

w, h = self.GetClientSize() self.buffer = wx.EmptyBitmap(w, h)

dc = wx.BufferedDC(wx.ClientDC(self), self.buffer) self.DrawGraph(dc)

def GetData(self):

return self.data def SetData(self, newData):

assert len(newData) == len(self.data) self.data = newData[:]

dc = wx.BufferedDC(wx.ClientDC(self), self.buffer) self.DrawGraph(dc)

def DrawGraph(self, dc): <— Drawing the graph spacer = 10 scaledmax = 150.0

dc.SetBackground(wx.Brush(self.GetBackgroundColour())) dc.Clear()

Updating when data changes dc.SetFont(self.titleFont)

tw, th = dc.GetTextExtent(self.title)

dc.DrawText(self.title, (dw-tw)/2, spacer) <— Drawing the title th = th + 2*spacer <— Finding the center point cx = dw/2

cy = (dh-th)/2 + th mindim = min(cx, (dh-th)/2) <— Calculating the scale factor scale = mindim/scaledmax dc.SetPen(wx.Pen("black", 1)) <1— Drawing the axes dc.SetBrush(wx.TRANSPARENT_BRUSH) dc.DrawCircle(cx,cy, 25*scale) dc.DrawCircle(cx,cy, 50*scale) dc.DrawCircle(cx,cy, 75*scale) dc.DrawCircle(cx,cy, 100*scale)

dc.DrawLine(cx-110*scale, cy, cx+110*scale, cy) dc.DrawLine(cx, cy-110*scale, cx, cy+110*scale)

Translating data values to polygon points dc.SetFont(self.labelFont) maxval = 0 angle = 0 polypoints = []

for i, label in enumerate(self.labels): val = self.data[i] point = self.PolarToCartesian(val*scale, angle, cx, cy) polypoints.append(point)

x, y = self.PolarToCartesian(12 5*scale, angle, cx,cy)

dc_DrawText(label, x, y) <-| Drawing the labels if val > maxval:

maxval = val angle = angle + 3 6 0/len(self.labels)

c = "yellow" if maxval > 95: c = "red"

dc.SetBrush(wx.Brush(c)) <-l Setting brush color dc.SetPen(wx.Pen("navy", 3))

dc.DrawPolygon(polypoints) <— Drawing the plot polygon class TestFrame(wx.Frame):

wx.Frame._init_(self, None, title="Double Buffered Drawing"

["A", "B", "C", "D", "E", "F", "G", "H"])

# Set some random initial data values data = []

for d in self.plot.GetData():

data.append(random.randint(0, 75)) self.plot.SetData(data)

# Create a timer to update the data values self.Bind(wx.EVT_TIMER, self.OnTimeout) self.timer = wx.Timer(self) self.timer.Start(500)

def OnTimeout(self, evt):

# simulate the positive or negative growth of each data value data = []

for d in self.plot.GetData():

val = d + random.uniform(-5, 5) if val < 0: val = 0 if val > 110: val = 110 data.append(val) self.plot.SetData(data)

app = wx.PySimpleApp() frm = TestFrame() frm.Show() app.MainLoop()

O This method does not need any drawing commands of its own. The buffered DC object automatically blits self.buffer to a wx.PaintDC when the device context is destroyed at the end of the method, Therefore, no new drawing needs to be done—we've already taken care of it.

C We create a buffer bitmap to be the same size as the window, then draw our graph to it. Since we use wx.BufferedDC whatever is drawn to the buffer will also be drawn to the window when the InitBuffer method is complete.