返回介绍

Mapping modes

发布于 2025-02-22 22:19:36 字数 19199 浏览 0 评论 0 收藏 0

Speak in English, measure in Metric

The English language became the global language for communication. So did the metric system become the global system in measuremet. According to this wikipedia article , there are only three exceptions. The USA, Liberia and Myanmar. For example, Americans use Fahrenheits to measure temperature, gallons to tank their cars or pounds to weigh loads.

Even though we in Europe use the metric system, there are still exceptions. The USA is dominating the IT and we are importing their standards. So we also say that we have a 17 Inch monitor. Graphics can be put into a file, displayed on the screen of a monitor or other device (cameras, videocameras, mobile phones) or printed with a printer. Paper size can be set in millimeters, points or inches, the resolution of a screen is in pixels, the quality of a text is determined by the number of dots per inch. We have also dots, bits or samples. This is one of the reasons we have logical and device units.

Logical and device units

If we draw text or geometrical primitives on the client area, we position them using logical units.

Drawing text
Figure: Drawing text

If we want to draw some text, we provide the text parameter and the x, y positions. x, y are in logical units. The device then draws the text in device units. Logical and device units may be the same, or they may differ. Logical units are used by people (millimeters), device units are are native to a particular device. For example a native device unit for a screen is pixel. The native device unit for the HEWLETT PACKARD LaserJet 1022 is 1200 dpi. (dots per inch).

So far we have talked about various measurement units. The mapping mode of the device is a way how to convert logical units to device units. wxPython has the following mapping modes:

Mapping ModeLogical Unit
wx.MM_TEXT1 pixel
wx.MM_METRIC1 millimeter
wx.MM_LOMETRIC1/10 of a millimeter
wx.MM_POINTS1 point, 1/72 of an inch
wx.MM_TWIPS1/20 of a point or 1/1440 of an inch

The default mapping mode is wx.MM_TEXT. In this mode, the logical unit is the same as the device unit. When people position object on a screen or design a web page, they think usually in pixels. Web designers create three column pages and these columns are set in pixels. The lowest common denominator for a page is often 800 px etc. This thinking is natural as we know our monitors have e.g. 1024x768 pxs. We are not going to do convertions, rather we are accustomed to think in pixels. If we want to draw a structure in millimeters, we can use the two metric mapping modes. Drawing directly in millimeters is too thick for a screen, that's why we have the wx.MM_LOMETRIC mapping mode.

Map mode
Figure: Map mode

To set a different mapping mode, we use the SetMapMode() method.

Ruler example

The ruler measures screen objects in pixels.

#!/usr/bin/python

# ruler1.py

import wx


RW = 701 # ruler widht
RM = 10  # ruler margin
RH = 60  # ruler height


class Ruler1(wx.Frame):
  def __init__(self, parent, id, title):
    wx.Frame.__init__(self, parent, id, title, size=(RW + 2*RM, 60),
      style=wx.FRAME_NO_TASKBAR | wx.NO_BORDER | wx.STAY_ON_TOP)
    self.font = wx.Font(7, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL,
      wx.FONTWEIGHT_BOLD, False, 'Courier 10 Pitch')

    self.Bind(wx.EVT_PAINT, self.OnPaint)
    self.Bind(wx.EVT_LEFT_DOWN, self.OnLeftDown)
    self.Bind(wx.EVT_RIGHT_DOWN, self.OnRightDown)
    self.Bind(wx.EVT_MOTION, self.OnMouseMove)

    self.Centre()
    self.Show(True)

  def OnPaint(self, event):
    dc = wx.PaintDC(self)

    brush = wx.BrushFromBitmap(wx.Bitmap('granite.png'))
    dc.SetBrush(brush)
    dc.DrawRectangle(0, 0, RW+2*RM, RH)
    dc.SetFont(self.font)


    dc.SetPen(wx.Pen('#F8FF25'))
    dc.SetTextForeground('#F8FF25')


    for i in range(RW):
      if not (i % 100):
        dc.DrawLine(i+RM, 0, i+RM, 10)
        w, h = dc.GetTextExtent(str(i))
        dc.DrawText(str(i), i+RM-w/2, 11)
      elif not (i % 20):
        dc.DrawLine(i+RM, 0, i+RM, 8)
      elif not (i % 2): dc.DrawLine(i+RM, 0, i+RM, 4)

  def OnLeftDown(self, event):
    pos = event.GetPosition()
    x, y = self.ClientToScreen(event.GetPosition())
    ox, oy = self.GetPosition()
    dx = x - ox
    dy = y - oy
    self.delta = ((dx, dy))

  def OnMouseMove(self, event):
    if event.Dragging() and event.LeftIsDown():
      x, y = self.ClientToScreen(event.GetPosition())
      fp = (x - self.delta[0], y - self.delta[1])
      self.Move(fp)

  def OnRightDown(self, event):
    self.Close()

app = wx.App()
Ruler1(None, -1, '')
app.MainLoop()

In this example we create a ruler. This ruler measures screen objects in pixels. We left the default mapping mode, which is wx.MM_TEXT . As we have already mentioned, this mode has the same logical and device units. In our case, these are pixels.

wx.Frame.__init__(self, parent, id, title, size=(RW + 2*RM, 60), style=wx.FRAME_NO_TASKBAR | 
  wx.NO_BORDER | wx.STAY_ON_TOP)

We have created a borderless window. The ruler is 721 px wide. The ruler is RW + 2*RM = 701 + 20 = 721. The ruler shows 700 numbers. 0 ... 700 is 701 pixels. A ruler has a margin on both sides, 2*10 is 20 pixels. Together it makes 721 pixels.

brush = wx.BrushFromBitmap(wx.Bitmap('granite.png'))
dc.SetBrush(brush)
dc.DrawRectangle(0, 0, RW+2*RM, RH)

Here we draw a custom pattern onto the window. I have used a predefined pattern available in the Gimp. It is called granite.

w, h = dc.GetTextExtent(str(i))
dc.DrawText(str(i), i+RM-w/2, 11)

These lines ensure that we align the text correctly. The GetTextExtent() method returns the width and the height of the text.

We do not have a border around our window. So we must handle moving manually by additional code. The OnLeftDown() and the OnMouseMove() methods enable us to move the ruler. (TODO:link to dragging.)

Ruler example
Figure: Ruler example

Practical examples

You might ask yourself, why do we need all those lines, pens, gradients? What is it good for? The following scripts will bring some practical examples. We will utilize, what we have learnt in practice.

Charts

Creating charts is an excelent example of utilizing gdi drawing functions. Charts are not GUI widgets. No gui toolkit provides charts as part of the library. One exception is wxWidgets toolkit (and so the wxPython). But these charts are very simple and cannot be used in real applications. A developer has usually two options. To create his own charting library or use a third-party library.

In the following example we create a simple line chart. We do not dwell into all details. I kept the example intentionally simple. A lot of things still remain undone. But you can grasp the idea and follow it.

#!/usr/bin/python

# linechart.py

import wx

data = ((10, 9), (20, 22), (30, 21), (40, 30), (50, 41),
(60, 53), (70, 45), (80, 20), (90, 19), (100, 22),
(110, 42), (120, 62), (130, 43), (140, 71), (150, 89),
(160, 65), (170, 126), (180, 187), (190, 128), (200, 125),
(210, 150), (220, 129), (230, 133), (240, 134), (250, 165),
(260, 132), (270, 130), (280, 159), (290, 163), (300, 94))

years = ('2003', '2004', '2005')


class LineChart(wx.Panel): 
  def __init__(self, parent):
    wx.Panel.__init__(self, parent)
    self.SetBackgroundColour('WHITE')

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

  def OnPaint(self, event):
    dc = wx.PaintDC(self)
    dc.SetDeviceOrigin(40, 240)
    dc.SetAxisOrientation(True, True)
    dc.SetPen(wx.Pen('WHITE'))
    dc.DrawRectangle(1, 1, 300, 200)
    self.DrawAxis(dc)
    self.DrawGrid(dc)
    self.DrawTitle(dc)
    self.DrawData(dc)

  def DrawAxis(self, dc):
    dc.SetPen(wx.Pen('#0AB1FF'))
    font =  dc.GetFont()
    font.SetPointSize(8)
    dc.SetFont(font)
    dc.DrawLine(1, 1, 300, 1)
    dc.DrawLine(1, 1, 1, 201)

    for i in range(20, 220, 20):
      dc.DrawText(str(i), -30, i+5)
      dc.DrawLine(2, i, -5, i)

    for i in range(100, 300, 100):
      dc.DrawLine(i, 2, i, -5)

    for i in range(3):
      dc.DrawText(years[i], i*100-13, -10)



  def DrawGrid(self, dc):
    dc.SetPen(wx.Pen('#d5d5d5'))

    for i in range(20, 220, 20):
      dc.DrawLine(2, i, 300, i)

    for i in range(100, 300, 100):
      dc.DrawLine(i, 2, i, 200)

  def DrawTitle(self, dc):
    font =  dc.GetFont()
    font.SetWeight(wx.FONTWEIGHT_BOLD)
    dc.SetFont(font)
    dc.DrawText('Historical Prices', 90, 235)


  def DrawData(self, dc):
    dc.SetPen(wx.Pen('#0ab1ff'))
    for i in range(10, 310, 10):
      dc.DrawSpline(data)


class LineChartExample(wx.Frame):
  def __init__(self, parent, id, title):
    wx.Frame.__init__(self, parent, id, title, size=(390, 300))

    panel = wx.Panel(self, -1)
    panel.SetBackgroundColour('WHITE')

    hbox = wx.BoxSizer(wx.HORIZONTAL)
    linechart = LineChart(panel)
    hbox.Add(linechart, 1, wx.EXPAND | wx.ALL, 15)
    panel.SetSizer(hbox)

    self.Centre()
    self.Show(True)


app = wx.App()
LineChartExample(None, -1, 'A line chart')
app.MainLoop()
dc.SetDeviceOrigin(40, 240)
dc.SetAxisOrientation(True, True)

By default the coordinate system in wxPython begins at point [0, 0]. The beginning point is located at the upper left corner of the clinet area. The orientation of x values is from left to right and the orientation of y values is from top to bottom. The values can be only positive. This system is used in all GUI toolkits. (All I am aware of.)

For charting we use cartesian coordinate system. In cartesian system, we can have both positive and negative values. The orientation of the x values is from left to right and the orientation of y values is from bottom to top. The origin is usually in the middle. But it is not compulsory.

dc.SetDeviceOrigin(40, 240)
dc.SetAxisOrientation(True, True)

The SetDeviceOrigin() method moves the origin to a new point on the client area. This is called linear translation. Then we change the axis orientation with the SetAxisOrientation() method.

SetAxisOrientation(bool xLeftRight, bool yBottomUp)

The method signature is self-explanatory. We can put true or false values to these two parameters.

self.DrawAxis(dc)
self.DrawGrid(dc)
self.DrawTitle(dc)
self.DrawData(dc)

We separate the construction of the chart into four methods. The first will draw axis, the second will draw the grid, the third the title and the last one will draw the data.

for i in range(3):
  dc.DrawText(years[i], i*100-13, -10)

Because of the simplicity of the script, there are some magic numbers. In reality, we would have to calculate them. In the previous code example, we draw the years alongside the x axis. We subtract 13 px from the x value. This is done to center the years over the vertical lines. It works on my linux box. I might not work correctly on other platforms. It might not work even on linux boxes with different themes. You just play a bit with this example. Adjusting it to fit under the different circumstances is no rocket science. Normally, we need to calculate the width of the chart, the width of the text and center the text manually.

A line chart
Figure: A line chart

Note

Note is a small script that shows several interesting features of the GDI. We will see, how we can create a custom shaped window. There are small applications that are used to take visible notes. They work as reminders for people that work with computers a lot.

#!/usr/bin/python

# note.py

import wx

class Note(wx.Frame):
  def __init__(self, parent, id, title):
    wx.Frame.__init__(self, parent, id, title,
            style=wx.FRAME_SHAPED |
            wx.SIMPLE_BORDER |
            wx.FRAME_NO_TASKBAR)

    self.font = wx.Font(11, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, 
		wx.FONTWEIGHT_BOLD, False, 'Comic Sans MS')
    self.bitmap = wx.Bitmap('note.png', wx.BITMAP_TYPE_PNG)
    self.cross = wx.Bitmap('cross.png', wx.BITMAP_TYPE_PNG)

    w = self.bitmap.GetWidth()
    h = self.bitmap.GetHeight()
    self.SetClientSize((w, h))

    if wx.Platform == '__WXGTK__':
      self.Bind(wx.EVT_WINDOW_CREATE, self.SetNoteShape)
    else: self.SetNoteShape()

    self.Bind(wx.EVT_PAINT, self.OnPaint)
    self.Bind(wx.EVT_LEFT_DOWN, self.OnLeftDown)
    self.Bind(wx.EVT_MOTION, self.OnMouseMove)

    self.bitmapRegion = wx.RegionFromBitmap(self.bitmap)
    self.crossRegion = wx.RegionFromBitmap(self.cross)

    self.bitmapRegion.IntersectRegion(self.crossRegion)
    self.bitmapRegion.Offset(170, 10)

    dc = wx.ClientDC(self)
    dc.DrawBitmap(self.bitmap, 0, 0, True)
    self.PositionTopRight()
    self.Show(True)

  def PositionTopRight(self):
    disx, disy = wx.GetDisplaySize()
    x, y = self.GetSize()
    self.Move((disx-x, 0))

  def SetNoteShape(self, *event):
    region = wx.RegionFromBitmap(self.bitmap)
    self.SetShape(region)

  def OnLeftDown(self, event):
    pos = event.GetPosition()
    if self.bitmapRegion.ContainsPoint(pos):
      self.Close()
    x, y = self.ClientToScreen(event.GetPosition())
    ox, oy = self.GetPosition()
    dx = x - ox
    dy = y - oy
    self.delta = ((dx, dy))


  def OnMouseMove(self, event):
    if event.Dragging() and event.LeftIsDown():
      x, y = self.ClientToScreen(event.GetPosition())
      fp = (x - self.delta[0], y - self.delta[1])
      self.Move(fp)

  def OnPaint(self, event):
    dc = wx.PaintDC(self)
    dc.SetFont(self.font)
    dc.SetTextForeground('WHITE')

    dc.DrawBitmap(self.bitmap, 0, 0, True)
    dc.DrawBitmap(self.cross, 170, 10, True)
    dc.DrawText('- Go shopping', 20, 20)
    dc.DrawText('- Make a phone call', 20, 50)
    dc.DrawText('- Write an email', 20, 80)


app = wx.App()
Note(None, -1, '')
app.MainLoop()

The idea behind creating a shaped window is simple. Most applications are rectangular. They share lots of similarities. They have menus, toolbars, titles etc. This might be boring. Some developers create more fancy applications. We can make our applications more attractive by using images. The idea is as follows. We create a frame without a border. We can draw a custom image on the frame during the paint event.

wx.Frame.__init__(self, parent, id, title,
        style=wx.FRAME_SHAPED |
        wx.SIMPLE_BORDER |
        wx.FRAME_NO_TASKBAR)

In order to create a custom shaped application, we must set necessary style options. The wx.FRAME_SHAPED enables to create a shaped window. The wx.SIMPLE_BORDER removes the thick border. The wx.FRAME_NO_TASKBAR prevents the application from appearing on the taskbar.

self.font = wx.Font(11, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, 
      wx.FONTWEIGHT_BOLD, False, 'Comic Sans MS')

We use Comic Sans MS font for the note example. This is a proprietary font. Linux users must install the msttcorefonts package. If we do not have a font installed, another one is chosen.

self.bitmap = wx.Bitmap('note.png', wx.BITMAP_TYPE_PNG)
self.cross = wx.Bitmap('cross.png', wx.BITMAP_TYPE_PNG)

We have created two bitmaps. The first is a rounded rectangle. With a kind of an orange fill. The Inkscape vector illustrator was used to to create it the first image. The second one is a small cross. It is used to close the application. For this we used the Gimp image editor.

w = self.bitmap.GetWidth()
h = self.bitmap.GetHeight()
self.SetClientSize((w, h))

We are going to draw a bitmap on the frame. I order to cover the whole frame, we figure out the bitmap size. Then we set the site of the frame to the size of the bitmap.

if wx.Platform == '__WXGTK__':
  self.Bind(wx.EVT_WINDOW_CREATE, self.SetNoteShape)
else: self.SetNoteShape()

This is some platform dependent code. Linux developers should call the SetNoteShape() method immediately after the wx.WindowCreateEvent event.

dc = wx.ClientDC(self)
dc.DrawBitmap(self.bitmap, 0, 0, True)

These lines are not necessary because a paint event is generated during the creation of the application. But we believe that it makes the example smoother.

def SetNoteShape(self, *event):
  region = wx.RegionFromBitmap(self.bitmap)
  self.SetShape(region)

Here we set the shape of the frame to that of the bitmap. The pixels outside the image become transparent.

If we remove a border from the frame, we cannot move the window. The OnLeftDown() and the OnMouseMove() methods enable the user to move the window by clicking on the client area of the frame and dragging it.

dc.DrawBitmap(self.bitmap, 0, 0, True)
dc.DrawBitmap(self.cross, 170, 10, True)
dc.DrawText('- Go shopping', 20, 20)
dc.DrawText('- Make a phone call', 20, 50)
dc.DrawText('- Write an email', 20, 80)

Within the OnPaint() method we draw two bitmaps and three texts.

Finally we will talk about how we close the note script.

self.bitmapRegion = wx.RegionFromBitmap(self.bitmap)
self.crossRegion = wx.RegionFromBitmap(self.cross)

self.bitmapRegion.IntersectRegion(self.crossRegion)
self.bitmapRegion.Offset(170, 10)
...
pos = event.GetPosition()
if self.bitmapRegion.ContainsPoint(pos):
  self.Close()

We create two regions from two bitmaps. We intersect these two regions. This way we get all pixels that share both bitmaps. Finally we move the region to the point, where we draw the cross bitmap. We use the Offset() method. By default the region starts at [0, 0] point.

Inside the OnLeftDown() method we check if we clicked inside the region. If true, we close the script.

Note example
Figure: Note

In this chapter we have worked with the GDI.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文