Welcome, guest | Sign In | My Account | Store | Cart

If you're writing some Tkinter software and you're sizing something in inches or centimeters and tkinter only gives you feedback in pixel distances then you may need a way to get back to your prefered units of measure. I recently found myself in this situation. There's nothing fancy about the set of functions here, it's more about the little known winfo_fpixels() function. Once you know about this everything else is a piece of cake. These functions are simple, but convenient.

Python, 112 lines
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
class tkMath( object ):
   PIXELS_PER_INCH    = 0
   PIXELS_PER_CM      = 0
   PIXELS_PER_MM      = 0
   PIXELS_PER_POINT   = 0
   
   @staticmethod
   def setup( root ):
      '''Must be called before any of the methods are used to initialize
      the conversion constants.'''
      tkMath.PIXELS_PER_INCH  = root.winfo_fpixels( '1i' )
      tkMath.PIXELS_PER_CM    = root.winfo_fpixels( '1c' )
      tkMath.PIXELS_PER_MM    = root.winfo_fpixels( '1m' )
      tkMath.PIXELS_PER_POINT = root.winfo_fpixels( '1p' )
   
   @staticmethod
   def pixelsToInches( pixels ):
      '''Convert pixels (python float or int) to inches.'''
      return pixels / tkMath.PIXELS_PER_INCH
   
   @staticmethod
   def pixelsToCM( pixels ):
      '''Convert pixels (python float or int) to centimeters.'''
      return pixels / tkMath.PIXELS_PER_CM
   
   @staticmethod
   def pixelsToMM( pixels ):
      '''Convert pixels (python float or int) to millimeters.'''
      return pixels / tkMath.PIXELS_PER_MM
   
   @staticmethod
   def pixelsToPoints( pixels ):
      '''Convert pixels (python float or int) to points.'''
      return pixels / tkMath.PIXELS_PER_POINT
   
   @staticmethod
   def inchesToPixels( inches ):
      '''Convert inches (python float or int) to pixels.'''
      return inches * tkMath.PIXELS_PER_INCH
   
   @staticmethod
   def cmToPixels( cm ):
      '''Convert centimeters (python float or int) to pixels.'''
      return cm * tkMath.PIXELS_PER_CM

   def mmToPixels( mm ):
      '''Convert millimeters (python float or int) to pixels.'''
      return mm * tkMath.PIXELS_PER_MM

   def pointsToPixels( points ):
      '''Convert points (python float or int) to pixels.'''
      return points * tkMath.PIXELS_PER_POINTS

   @staticmethod
   def toPixels( tkCoord ):
      '''Convenience function for inches, cm, mm and pointsToPixels().
      Convert a tkCoord (string appended by 'i', 'c', 'm' or 'p') to pixels.'''
      if isinstance( tkCoord, str ):
         if tkCoord[-1] == 'i':
            return tkMath.inchesToPixels( float(tkCoord[:-1]) )
         elif tkCoord[-1] == 'c':
            return tkMath.cmToPixels( float(tkCoord[:-1]) )
         elif tkCoord[-1] == 'm':
            return tkMath.mmToPixels( float(tkCoord[:-1]) )
         elif tkCoord[-1] == 'p':
            return tkMath.pointsToPixels( float(tkCoord[:-1]) )
         else:
            return float(tkCoord)
      else:
         return tkCoord

   @staticmethod
   def compare( coord1, coord2 ):
      '''Compare two tk measures -- they need not be in the same units.'''
      return tkMath.coordToPixels(coord1) - tkMath.coordToPixels(coord2)

   @staticmethod
   def add( coord1, coord2 ):
      '''Add two tk measures -- they need not be in the same units.'''
      return tkMath.coordToPixels(coord1) + tkMath.coordToPixels(coord2)
   
   @staticmethod
   def sub( coord1, coord2 ):
      '''Subtract two tk measures -- they need not be in the same units.'''
      return tkMath.coordToPixels(coord1) - tkMath.coordToPixels(coord2)
   
   @staticmethod
   def tkPolar( x1, y1, x2, y2 ):
      '''Calculate the direction (in radians, 3 o'clock is 0,
      down is 1/2 PI, etc.) and distance (in pixels) between to points.
      All arguments should be in the same units (python float or int).
      The result is in the same units as the arguments.'''
      import math
      deltaX = math.fabs( x1 - x2 )
      deltaY = math.fabs( y1 - y2 )

      direction = math.atan2( deltaY, deltaX )
      distance  = math.sqrt( math.pow(deltaX, 2) + math.pow(deltaY, 2) )

      return direction, distance

   @staticmethod
   def tkCartesian( x, y, direction, distance ):
      '''Complementary to tkPolar().  Given a x,y point, direction in
      radians (0 is at 3 o'clock, 1/2 PI is straight down, etc.) and a
      distance (all as python float or int).  This function returns
      the x and y of the end-point.'''
      import math
      deltaX = distance * math.cos( direction )
      deltaY = distance * math.sin( direction )
      
      return x + deltaX, y + deltaY

Clearly I didn't need to define a class since all the methods are static. It's just a design preference. Before any method in this class is called, it's necessary to call setup() and pass it a widget. Any ol' widget will do. This only needs to be done one time. setup() will use the widget to determine the conversion constants. I prefer to call setup immediately after I create my root widget so I called my parameter 'root'.

Each of the six conversion functions requires its argument as a python float or integer value. Python floats are always returned.

tkDistance is a simple Pythagorean Theorem implementation. All four arguments must be in python float or integer values, all in the same units (pixels, inches, cm or mm). The result is a python float in the same units as was entered. I've found common idiom to their usage to be:

root = Tk() tkMath.setup( root )

... def myEventCB( event ): distance = tkMatho.tkDistance( initialx, initialy, event.x, event.y )

distanceInInches = tkMath.pixelsToInches( distance )

...

vector requires all arguments as python floats and/or ints. direction should be provided in radians with zero at 3 o'clock and values incrementing clock-wise: So straight down is 1/2 * PI, to the left if PI and straight up is 3/2 * PI. With x1,y1 being the starting coordinate for the vector, the result is a tuple x,y of the ending coordinates.

I've found tk measurements to be highly accurate on any display I've experimented with. That is if I do something such as draw a 3 inch line on a canvas widget and measure it with a ruler up against the screen, I get just about three inches.

5 comments

Jean Brouwers 16 years ago  # | flag

In the function setup, in these 3 lines

tkUnits.PIXELS_PER_INCH = root.winfo_fpixels( '1i' )
tkUnits.PIXELS_PER_CM   = root.winfo_fpixels( '1c' )
tkUnits.PIXELS_PER_MM   = root.winfo_fpixels( '1m' )

should tkUnits not be replaced with tkMath?

Jean Brouwers 16 years ago  # | flag

Actually, should all tkUnits not be replaced with tkMath?

Jean Brouwers 16 years ago  # | flag

Here is another function for the distance between 2 points, especially useful for comparison.

@staticmethod
def tkDistance2(x1, y1, x2, y2):
   '''Calculate the approximate distance (in pixels) between to points.'''
   dX = abs(x1 - x2)
   dY = abs(y1 - y2)
   if dX > dY:
      return dX + dY / 2
   else:
      return dY + dX / 2
Jean Brouwers 16 years ago  # | flag

In the last line, x2 should be y1.

Jean Brouwers 16 years ago  # | flag

In tkPolar, math.atan should be math.atan2.

Created by Ronald Longo on Sun, 30 Mar 2008 (PSF)
Python recipes (4591)
Ronald Longo's recipes (2)

Required Modules

Other Information and Tasks