Name: [2039] John Posner
Member: 85 months
Authored: 1 videos
Description: Software technical writer in USA. Dabbler in math and computer education for younger kids. ...

Adding Numbers with BlockHead [ID:583] (1/1)

in series: BlockHead -- visual calculator for whole numbers

video tutorial by John Posner, added 03/08

(Showmedo is undergoing major changes. To report any problems viewing the videos please email us and include browser and OS specifics. Cheers - Kyran.)

This video introduces BlockHead, showing how it represents numbers as Cuisenaire-like blocks in a place-value framework. It then walks through two examples of adding 3-digit numbers.

The first example is straightforward, and includes an example of carrying from one column to the next. The second example shows multiple carries, and demonstrates how BlockHead allows you to process blocks in any order.

#################### BlockHead.py

"""
visual calculator, for adding or subtracting
two 3-digit numbers

John Posner
"""

import os, sys, operator
from time import sleep
from Tkinter import *

DATE = '24-Mar-2008'
VERSION = 1037

####################
#################### global variables
####################

# configure for 800x600 display screen, which is
# required for ShowMeDo videos
ShowMeDo_800_600 = False

# operation modes
ADD_MODE = 1
SUB_MODE = 2

# help texts
HELP_ADD = """
ADD TWO NUMBERS

Enter two numbers to
be added, then click
"Draw Blocks"

Drag each of the
blocks you've drawn
to the corresponding
(same-color)
answer column

If an answer column's
total is 10 or more,
click the "carry" arrow
that appears below the
answer column
"""

HELP_SUB = """
SUBTRACT TWO NUMBERS

Enter the larger and
smaller numbers, then
click "Draw Blocks"

Drag the blocks for
the smaller number to
the corresponding
columns of the
larger number
"""

# fonts
if os.name == 'nt':
	FONTNAME = 'verdana'
else:
	FONTNAME = 'helvetica'

if ShowMeDo_800_600:
	FONT = (FONTNAME, 8, 'bold')
	HELP_ADD_OFFSET = -95
	HELP_SUB_OFFSET = -105
	DRAW_STRING = "Draw Blocks"
else:
	FONT = (FONTNAME, 10, 'bold')
	HELP_ADD_OFFSET = -95
	HELP_SUB_OFFSET = -105
	DRAW_STRING = "Draw Blocks"

TEXT_FONT = FONT

# colors
BGND_COLOR = '#D8D8D8'
ANSW_COLOR = '#00FF00'
TGT_COLOR = '#00FF00'
CARRY_COLOR = '#FF0000'
BORROW_BLK_COLOR = '#f0f000'
SUBTRACT_COLOR = '#CCCCCC'
COLORS = {'hb': '#FF7D7D',  # hundreds block
		  'tb': '#32bfe8',
		  'ob': '#F2BC02',
		  'xc': BGND_COLOR, # thousands column (invisible)
		  'hc': '#ffd7d7',  # hundreds column
		  'tc': '#a6e2f4',
		  'oc': '#feedb1'}

# block specs
WID = 18     # block width
PAD = 8      # padding between block and column edge
UNIT = WID   # height of one unit

# column specs

COL_OFFSET = WID + 2*PAD  # make the columns contiguous
#BASE = 365       # Y-coordinate of bottom of column will be set according to window size
HGT = 10*UNIT    # height of column
ANSR_OFFSET = 30 # distance between bottom of column and digit; leave room for carry/borrow arrows!
CARRY_ARROW_HGT = 12 # height of carry/borrow arrow

# dictionary of columns: for inputs, each column's key is the same as its Tkinter "create_rectangle" tag
Cols = {}
Blocks = {}
ANSR_COLTAGS = ['xA_col', 'hA_col', 'tA_col', 'oA_col']

# suffixes to be appended to "tag root" name,
# to make a column tag or block tag or text anno
COLSUFFIX = "_col"
BLOCKSUFFIX = "_block"
TEXTSUFFIX = "_text"
CARRYSFX = "_carry"

MouseLastX = MouseLastY = MouseStartX = MouseStartY = SelectedBlockTag = DropOk = AnswerColTag = SelectableBlockTags = None
CarryCount = 0

####################
#################### classes
####################

class Block():
	"""
	represents one digit of a multiple-digit number
	"""

	def __init__(self, tag, value, color=None):

		self.value = value
		self.tag = tag
		# maybe: assign color using first letter of tag
		if color:
			self.color = color
		else:
			self.color = StdColor(self)

		# id of Tkinter widget
		# will be set to integer ID by Canvas create_rectangle()
		self.id = 0

		# register the block in the global list
		Blocks[self.tag] = self

		# will be filled in by Column's Draw()
		self.startloc = None

class Column():
	"""
	represents one column (ones, tens, hundreds)
	"""

	def __init__(self, tag, leftedge, baselevel, color=None):
		"""
		initialize a column, either input or answer
		"""
		self.tag = tag
		self.x = leftedge
		self.y = baselevel
		self.blocks = []

		# maybe: assign color using first letter of tag
		if color:
			self.color = color
		else:
			self.color = StdColor(self)

		self.id = cv.create_rectangle(self.x, self.y, self.x+WID+2*PAD, self.y-HGT,
			tags=self.tag, outline=self.color, fill=self.color)
		# carry button id will be created by Add()
		self.carrybtn = None

		# register the column in the global list
		Cols[self.tag] = self

	def ColumnToRight(self):
		"""
		return column to right of this answer column
		"""
		assert(self.tag in ANSR_COLTAGS)
		idx = ANSR_COLTAGS.index(self.tag)
		try:
			return Cols[ANSR_COLTAGS[idx+1]]
		except:
			print "no column to right"
			sys.exit(1)

	def ColumnToLeft(self):
		"""
		return column to left of this answer column
		"""
		assert(self.tag in ANSR_COLTAGS)
		idx = ANSR_COLTAGS.index(self.tag)
		try:
			return Cols[ANSR_COLTAGS[idx-1]]
		except:
			print "no column to left"
			sys.exit(1)

	def Draw(self, blk, offset=0):
		"""
		draw a block in a specified column
		"""

		# no block to draw for zero value
		if blk.value == 0:
			return

		startx = self.x + PAD
		height = blk.value*UNIT

		# might need to draw this block on top of others
		hgt_offset = offset*UNIT

		# create Tkinter rectangle widget for block
		# note: Y-axis is reversed!
		blk.id = cv.create_rectangle(
			startx, self.y-hgt_offset,
			startx+WID, self.y-height-hgt_offset,
			fill=blk.color, tags=blk.tag)

		# save location of rectangle widget, for "snap back" in cancelled move operation
		blk.startloc = cv.coords(blk.id)

		# create lines to show individual units
		for i in range(1, blk.value):
			cv.create_line(startx, self.y-i*UNIT-hgt_offset,
				startx+WID, self.y-i*UNIT-hgt_offset,
				fill='black', tags=blk.tag)

	def Add(self, blk, carry_button_suppress=False):
		"""
		send a block to this column
		"""
		global mode, CarryCount

		# save current column total, before adding new block
		old_total = self.Total()

		self.blocks.append(blk)
		self.Draw(blk, old_total)

		# update numeric value display
		self.ShowValue(self.Total())

		# maybe: create carry button below column
		if mode.get() == SUB_MODE or blk.value == 0:
			return

		# maybe: create carry icon
		carrytag = self.tag + CARRYSFX
		if self.Total() >= 10 and not carry_button_suppress and not cv.find_withtag(carrytag):
			startx, starty = LowerLeft(self.id)
			self.carrybtn = cv.create_bitmap(startx, starty+10, bitmap = "@left_arrow.xbm", tags=carrytag)
			cv.tag_bind(carrytag, "<Button-1>", self.Carry)
			CarryCount += 1

	def ShowValue(self, value=None):
		"""
		display a block's value below this column
		"""
		if value is None:
			value = self.Total()
		elif value == -1:
			value = ""

		texttag = self.tag + TEXTSUFFIX

		# delete previous value, if necessary
		cv.delete(texttag)

		# create new text widget
		cv.create_text(self.x + (WID+2*PAD)/2, self.y+ANSR_OFFSET,
			tag = texttag, text=str(value), font=FONT, justify=CENTER)

	def Total(self):
		"""
		total of column's blocks
		"""
		total = 0
		for blk in self.blocks:
			total += blk.value
		return total

	def Carry(self, event):
		"""
		calculate carry for specified column
		btn = ID of dynamically-created carry button, to be deleted
		"""
		global CarryCount

		# carry destination
		destcol = self.ColumnToLeft()

		# delete carry button
		cv.tag_unbind(self.carrybtn, "<Button-1>")
		cv.delete(self.carrybtn)
		CarryCount -= 1

		# save total value of blocks, for creation of new blocks
		total = self.Total()

		# create some block tags, specific to this column
		tenblk = self.tag + "_ten_xformblock"
		carryblk = self.tag + "_carry_xformblock"
		excessblk = self.tag + "_excess_xformblock"

		# save color, might be needed for "excess" block
		block_color = StdColor(self.blocks[0])

		# clear out all the blocks in this column
		for blk in self.blocks:
			cv.delete(blk.tag)
		self.blocks = []

		# block of 10 units
		Block(tenblk, 10, block_color)
		self.Add(Blocks[tenblk], True)

		# carry block (1 unit)
		Block(carryblk, 1, block_color)

		# "excess" block (1+ units)
		if total > 10:
			excess = total - 10
			Block(excessblk, excess, block_color)
			self.Add(Blocks[excessblk], True)

		sleep(0.25)

		# transform 10 units into 1 unit of the next column
		startx, starty, = UpperLeft(tenblk)

		cv.lift(tenblk)
		count = 20
		for i in range(count):
			sleep(1.0/count)
			cv.scale(tenblk, startx, starty, 1.0, 0.1 ** (1.0/count))
			cv.update()
		sleep(0.25)

		# move the shrunken blocks to the next column
		startx, starty, = LowerLeft(tenblk)
		endx = cv.coords(destcol.tag)[0]+PAD
		endy = self.y - destcol.Total()*UNIT

		count = 20
		for i in range(count):
			cv.move(tenblk, (endx-startx)/count, (endy-starty)/count)
			cv.update()
			sleep(1.0/count)

		# replace shrunken 10-unit block with new "carry block"
		cv.delete(tenblk)
		destcol.Add(Blocks[carryblk])

		# delete the block from this column, and update column total
		del self.blocks[0]
		self.ShowValue()

		# if there an "excess" block? if so, drop it down
		if total > 10:
			sleep(0.25)
			count = 20
			for i in range(count):
				sleep(1.0/count)
				cv.move(excessblk, 0, 10.0*UNIT/count)
				cv.update()

		# are we done?
		CalcAnswer()

	def Borrow(self, event):
		"""
		borrow 1 unit from this column:
		send 10 units to the column to the right
		"""
		# can we borrow fromn this column?
		# if not, first borrow from column to the left
		if self.Total() == 0:
			self.ColumnToLeft().Borrow(event)
			cv.update()
			sleep(0.5)

		# decompose last block in this column (ex: 8 --> 7+1)
		borrow_blk = self.blocks[-1]
		borrow_val = borrow_blk.value
		borrow_tag = borrow_blk.tag
		borrow_newblk_tag = borrow_tag + "_borrowfrom"

		# delete the topmost block from this column
		cv.delete(borrow_tag)
		del self.blocks[-1]

		# replace with block of size N-1, if value is greater than 1
		if borrow_val > 1:
			self.Add( Block(borrow_tag, borrow_val-1, StdColor(borrow_blk)) )

		# create block of size 1, which will get borrowed
		self.Add( Block(borrow_newblk_tag, 1, BORROW_BLK_COLOR) )

		startx, starty = LowerLeft(borrow_newblk_tag)
		for i in range(1,10):
			cv.create_line(startx,     starty-i/10.0*UNIT,
						   startx+WID, starty-i/10.0*UNIT, tags=borrow_newblk_tag)
		cv.update()

		# scale borrow block vertically from 1 unit to 10 units
		count = 20
		for i in range(count):
			cv.scale(borrow_newblk_tag, startx, starty, 1.0, (10.0 ** (1.0/count)))
			sleep(1.0/count)
			cv.update()

		sleep(0.25)

		# move the scaled borrow block to the next column
		destcol = self.ColumnToRight()

		try:
			# there's a block in the dest column
			tgtblk = destcol.blocks[-1]
			endx, endy = UpperLeft(tgtblk.tag)
		except:
			# there's no block in the dest column
			endx = cv.coords(destcol.tag)[0] + PAD
			endy = self.y

		count = 20
		for i in range(count):
			cv.move(borrow_newblk_tag, (endx-startx)/count, (endy-starty)/count)
			cv.update()
			sleep(1.0/count)

		# remove the borrow block from this column
		del self.blocks[-1]
		self.ShowValue()

		# destination column: replace stretched block (value=1) with new block (value=10)
		cv.delete(borrow_newblk_tag)
		destcol.Add( Block(destcol.tag+"_borrowto", 10) )

		# recalc the borrow buttons
		BorrowButtons()

####################
#################### functions
####################

def MouseDown(event):
	"""
	set AnswerColTag = tag of one column, based on which block was selected
	"""

	global MouseLastX, MouseLastY, MouseStartX, MouseStartY, SelectedBlockTag, AnswerColTag, DropOk

	MouseLastX = MouseStartX = cv.canvasx(event.x)
	MouseLastY = MouseStartY = cv.canvasy(event.y)
	try:
		SelectedBlockTag = cv.gettags(cv.find_closest(MouseStartX, MouseStartY))[0]
		#print "selected:", SelectedBlockTag, event.x, event.y
	except:
		SelectedBlockTag = None
		return

	# only certain block widgets are selectable
	if SelectedBlockTag not in SelectableBlockTags:
		SelectedBlockTag = None
		return

	AnswerColTag = AnswerColumnTag(SelectedBlockTag)
	cv.lift(SelectedBlockTag)

def MouseMotion(event):
	global MouseLastX, MouseLastY, MouseStartX, MouseStartY, SelectedBlockTag, AnswerColTag, DropOk
	if not SelectedBlockTag:
		return
	cx = cv.canvasx(event.x)
	cy = cv.canvasy(event.y)
	cv.move(SelectedBlockTag, cx-MouseLastX, cy-MouseLastY)
	MouseLastX = cx
	MouseLastY = cy

	if InAnswerColumn():
		if mode.get() == ADD_MODE:
			cv.itemconfig(AnswerColTag, outline=TGT_COLOR, fill=TGT_COLOR)
			DropOk = True
		elif mode.get() == SUB_MODE:
			if Blocks[SelectedBlockTag].value <= Cols[AnswerColTag].Total():
				# can subtract now, without borrowing
				cv.itemconfig(AnswerColTag, outline=TGT_COLOR, fill=TGT_COLOR)
				DropOk = True
			else:
				# cannot subtract now, need to borrow
				cv.itemconfig(AnswerColTag, outline='black', fill='black')
				DropOk = False
	else:
		# reset target column background
		clr = Cols[AnswerColTag].color
		cv.itemconfig(AnswerColTag, outline=clr, fill=clr)
		DropOk = False

def MouseUp(event):
	"""
	dispatch a mouse-up event, depending on the ADD/SUB mode
	"""
	global MouseLastX, MouseLastY, mode

	if not SelectedBlockTag:
		return

	# make sure that an original block is selected
	try:
		widgetlist = cv.gettags(CURRENT)
	except:
		return
	if len(widgetlist) == 0 or not widgetlist[0].endswith(BLOCKSUFFIX):
		return

	# update global vars
	MouseLastX = cv.canvasx(event.x)
	MouseLastY = cv.canvasy(event.y)

	# dispatch ADD/SUB
	junk = 1
	if mode.get() == ADD_MODE or len(Cols[AnswerColTag].blocks) == 0:
		MouseUp_Add(event)
	elif mode.get() == SUB_MODE:
		MouseUp_Sub(event)

	CalcAnswer()

def MouseUp_Sub(event):
	"""
	handle a mouse-up event in SUB mode
	"""
	global MouseLastX, MouseLastY, MouseStartX, MouseStartY, SelectedBlockTag, AnswerColTag, DropOk, CarryData

	if DropOk:
		# superimpose block at top of current set of blocks

		sub_offset = 10

		current_value = Cols[AnswerColTag].Total()
		sub_value = Blocks[SelectedBlockTag].value

		startx, starty = UpperLeft(SelectedBlockTag)
		endx, endy = UpperLeft(Cols[AnswerColTag].blocks[-1].tag)

		# adjust destination to right a bit
		endx += sub_offset

		# do the move
		count = 10
		for i in range(count):
			cv.move(SelectedBlockTag, (endx-startx)/count, (endy-starty)/count)
			cv.update()
			sleep(0.25/count)
		sleep(0.25)

		def _HexString2List(hexstr):
			"""
			convert a hex string: e.g. "#00FF03"
			to a list: [0, 255, 3]
			"""
			rtn = []
			rtn.append(eval('0x' + hexstr[1:3]))
			rtn.append(eval('0x' + hexstr[3:5]))
			rtn.append(eval('0x' + hexstr[5:]))

			return rtn

		# fade and move the bar to be subtracted
		def _FadeToBgnd(start, end, count):
			s = _HexString2List(start)
			e = _HexString2List(end)

			# gradually migrate start color toward end color
			for i in range(count):
				color = '#'
				for n in range(3):
					level = s[n] + i*((e[n] - s[n])/count)
					# make sure roundoff error doesn't take us out-of-bounds
					if level > 255: level = 255
					if level < 0: level = 0
					color += '%02x' % level

				cv.itemconfig(SelectedBlockTag, fill=color)
				cv.move(SelectedBlockTag, -sub_offset/count, 0)
				cv.update()
				sleep(0.5/count)

		_FadeToBgnd(Blocks[SelectedBlockTag].color, SUBTRACT_COLOR, 10)
		cv.itemconfig(SelectedBlockTag, fill=SUBTRACT_COLOR)
		cv.update()
		sleep(0.75)

		# delete block from original column
		cv.delete(SelectedBlockTag)
		origcol = OrigColumn(SelectedBlockTag)
		origcol.blocks = []
		origcol.ShowValue(-1)

		#
		# process result
		#
		result =  current_value - sub_value

		# clear target column
		for blk in Cols[AnswerColTag].blocks:
			cv.delete(blk.tag)
		Cols[AnswerColTag].blocks = []

		# create result block (maybe) and show value
		if result > 0:
			Cols[AnswerColTag].Add( Block(AnswerColTag+"result", result) )
		Cols[AnswerColTag].ShowValue()

		# recalc the borrow buttons
		BorrowButtons()

		# reset target column background
		cv.itemconfig(AnswerColTag, outline=Cols[AnswerColTag].color, fill=Cols[AnswerColTag].color)

		# a block can be moved only once
		SelectableBlockTags.remove(SelectedBlockTag)

	else:
		# snap-back: return the block to its original position
		# where is block now?
		newX, newY = UpperLeft(SelectedBlockTag)
		# what is original position?
		origX, origY = Blocks[SelectedBlockTag].startloc[:2]

		count = 10
		for i in range(count):
			cv.move(SelectedBlockTag, (origX-newX)/count, (origY-newY)/count)
			cv.update()
			sleep(0.5/count)

		# reset target column background
		clr = Cols[AnswerColTag].color
		cv.itemconfig(AnswerColTag, outline=clr, fill=clr)

	# in all cases, reset flag
	DropOk = False

def MouseUp_Add(event):
	"""
	handle a mouse-up event in ADD mode
	"""
	global SelectedBlockTag, AnswerColTag, DropOk, CarryData

	if DropOk:
		# move block toward target location
		# where is block now?
		startx, starty = LowerLeft(SelectedBlockTag)
		destcol = Cols[AnswerColTag]
		# where should block go?
		endx, endy = LowerLeft(destcol.tag)
		endx += PAD
		endy -= destcol.Total()*UNIT

		count = 10
		for i in range(count):
			cv.move(SelectedBlockTag, (endx-startx)/count, (endy-starty)/count)
			cv.update()
			sleep(0.5/count)

		# delete block from original column
		cv.delete(SelectedBlockTag)
		origcol = OrigColumn(SelectedBlockTag)
		origcol.blocks = []
		origcol.ShowValue(-1)

		# recreate block at the target location, with its top
		# aligned with the top of the column's existing blocks
		Cols[AnswerColTag].Add(Blocks[SelectedBlockTag])

		# reset target column background, and drop flag
		clr = Cols[AnswerColTag].color
		cv.itemconfig(AnswerColTag, outline=clr, fill=clr)

		# a block can be moved only once
		SelectableBlockTags.remove(SelectedBlockTag)

	else:
		# snap-back: return the block to its original position
		# (1) where is block now?
		newX, newY = UpperLeft(SelectedBlockTag)
		# (2) what is original position?
		origX, origY = Blocks[SelectedBlockTag].startloc[:2]

		# move the block
		count = 10
		for i in range(count):
			cv.move(SelectedBlockTag, (origX-newX)/count, (origY-newY)/count)
			cv.update()
			sleep(0.5/count)

	# in all cases, reset flag
	DropOk = False

def UpperLeft(tag):
	"""
	return the coordinates of the UPPER-LEFT corner
	of the rectangle with the specified tag
	"""
	a,b,c,d = BlockCoords(tag)
	return (a,b)

def LowerLeft(tag):
	"""
	return the coordinates of the LOWER-LEFT corner
	of the rectangle with the specified tag
	"""
	a,b,c,d = BlockCoords(tag)
	return (a,d)

def BlockCoords(blktag):
	"""
	return the coordinates of the rectangle with the specified tag
	"""
	# assumption: rectangle is the FIRST (i.e. 0th) object created with the tag
	# or the only such object
	rectID = cv.find_withtag(blktag)[0]
	return cv.coords(rectID)

def OrigColumn(blktag):
	"""
	return the column object where the block with a specified tag originated
	"""
	coltag = blktag.replace("block", "col")
	return Cols[coltag]

def AnswerColumnTag(blktag):
	"""
	return the tag of the "answer" column that corresponds to the selected block,
	using the first letter of the tags: 'h' or 't' or 'o'
	"""
	return blktag[0] + "A_col"

def InAnswerColumn():
	"""
	is the mouse in the bounding box of the target column? (ID'd by global: AnswerColTag)
	"""
	leftx, topy, rightx, boty = cv.bbox(AnswerColTag)
	return (MouseLastX > leftx and MouseLastX < rightx and MouseLastY > topy and MouseLastY < boty)

def StdColor(arg):
	"""
	return standard color for a column or block,
	based on the first letter of its tag
	"""
	if isinstance(arg, Block):
		return COLORS[arg.tag[0] + 'b']
	elif isinstance(arg, Column):
		return COLORS[arg.tag[0] + 'c']

def ValidateKey(event=None):
	"""
	after a keystroke, determine whether the two input fields have valid numbers
	"""
	# if not a digit, delete it
	if event and event.char not in '0123456789':
		event.widget.delete( len(event.widget.get()) - 1 )
		return

	# Draw button disabled by default
	drawbtn.config(state=DISABLED)

	a1 = a2 = 0
	try:
		a1 = cv.input_1.get()
		a2 = cv.input_2.get()
	except:
		# this code path if one or both of the input fields is blank
		return

	# enable Draw button if entries are valid
	if (a1 < 1000 and a2 < 1000 and	(mode.get()==ADD_MODE or (mode.get()==SUB_MODE and a2 <= a1))):
		drawbtn.config(state=NORMAL)

def GetEntries():
	"""
	get inputs from Entry fields
	"""
	# NOTE: this should always succeed, given ValidateKey()

	arg1 = cv.input_1.get()
	arg2 = cv.input_2.get()

	# derive each digit: should always succeed
	try:
		h1 = arg1 // 100
		t1 = (arg1 % 100) // 10
		o1 = arg1 - h1*100 - t1*10

		h2 = arg2 // 100
		t2 = (arg2 % 100) // 10
		o2 = arg2 - h2*100 - t2*10
	except:
		print "bad args"
		sys.exit(1)

	# disable button and entry fields
	for obj in (drawbtn, addbtn, subbtn, entry_1, entry_2):
		obj.config(state=DISABLED)

	# configure and draw columns and blocks
	DrawColumnsAndBlocks(h1, t1, o1, h2, t2, o2)

	# maybe: enable some borrow buttons
	if mode.get() == SUB_MODE:
		BorrowButtons()

def DrawColumnsAndBlocks(h1_val, t1_val, o1_val, h2_val, t2_val, o2_val):
	"""
	draw columns and blocks, using specified values
	"""
	global SelectableBlockTags

	# configure column locations, based on positions of entry/answer fields
	if mode.get() == ADD_MODE:
		# first set of columns
		center = entry_1.winfo_rootx() - root.winfo_rootx() + 0.5*entry_1.winfo_width()
		huns_1 = center - COL_OFFSET*1.5
		tens_1 = huns_1 + COL_OFFSET
		ones_1 = huns_1 + COL_OFFSET*2
		# second set of columns
		center = entry_2.winfo_rootx() - root.winfo_rootx() + 0.5*entry_2.winfo_width()
		huns_2 = center - COL_OFFSET*1.5
		tens_2 = huns_2 + COL_OFFSET
		ones_2 = huns_2 + COL_OFFSET*2
		# answer columns
		center = answ.winfo_rootx() - root.winfo_rootx() + 0.5*answ.winfo_width()
		huns_A = center - COL_OFFSET*1.5
		tens_A = huns_A + COL_OFFSET
		ones_A = huns_A + COL_OFFSET*2
		thou_A = huns_A - COL_OFFSET
	elif mode.get() == SUB_MODE:
		# answer columns are at left, above first number
		center = entry_1.winfo_rootx() - root.winfo_rootx() + 0.5*entry_1.winfo_width()
		huns_A = center - COL_OFFSET*1.5
		tens_A = huns_A + COL_OFFSET
		ones_A = huns_A + COL_OFFSET*2
		# second set of columns
		center = entry_2.winfo_rootx() - root.winfo_rootx() + 0.5*entry_2.winfo_width()
		huns_2 = center - COL_OFFSET*1.5
		tens_2 = huns_2 + COL_OFFSET
		ones_2 = huns_2 + COL_OFFSET*2

	# determine y-coordinate of bottom of a column
	border_y = ctrl.winfo_y()
	# allow room for carry/borrow arrows (which are 12 pixels high) and current-value digits
	baselvl = border_y - ANSR_OFFSET - 20

	# create the columns
	if mode.get() == ADD_MODE:
		Column('h1_col', huns_1, baselvl)
		Column('t1_col', tens_1, baselvl)
		Column('o1_col', ones_1, baselvl)
		Column('h2_col', huns_2, baselvl)
		Column('t2_col', tens_2, baselvl)
		Column('o2_col', ones_2, baselvl)
		Column('xA_col', thou_A, baselvl)
		Column('hA_col', huns_A, baselvl)
		Column('tA_col', tens_A, baselvl)
		Column('oA_col', ones_A, baselvl)
	elif mode.get() == SUB_MODE:
		# raise first columns up a bit
		Column('hA_col', huns_A, baselvl-15)
		Column('tA_col', tens_A, baselvl-15)
		Column('oA_col', ones_A, baselvl-15)
		Column('h2_col', huns_2, baselvl)
		Column('t2_col', tens_2, baselvl)
		Column('o2_col', ones_2, baselvl)

	# create blocks and place them in columns
	if mode.get() == ADD_MODE:
		Cols['h1_col'].Add( Block('h1_block', h1_val) )
		Cols['t1_col'].Add( Block('t1_block', t1_val) )
		Cols['o1_col'].Add( Block('o1_block', o1_val) )

		Cols['h2_col'].Add( Block('h2_block', h2_val) )
		Cols['t2_col'].Add( Block('t2_block', t2_val) )
		Cols['o2_col'].Add( Block('o2_block', o2_val) )

		SelectableBlockTags = ['h1_block', 't1_block', 'o1_block', 'h2_block', 't2_block', 'o2_block']

		# answer columns get zeros
		for coltag in ['hA_col', 'tA_col', 'oA_col']:
			Cols[coltag].ShowValue(0)

	elif mode.get() == SUB_MODE:
		Cols['hA_col'].Add( Block('hA_block', h1_val) )
		Cols['tA_col'].Add( Block('tA_block', t1_val) )
		Cols['oA_col'].Add( Block('oA_block', o1_val) )

		Cols['h2_col'].Add( Block('h2_block', h2_val) )
		Cols['t2_col'].Add( Block('t2_block', t2_val) )
		Cols['o2_col'].Add( Block('o2_block', o2_val) )

		SelectableBlockTags = ['h2_block', 't2_block', 'o2_block']

def BorrowButtons():
	"""
	reconfigure borrow buttons for multiple columns
	"""
	for colchar in ('t', 'o'):
		to_tag = AnswerColumnTag(colchar)
		to_col = Cols[to_tag]
		from_col = to_col.ColumnToLeft()
		from_tag = from_col.tag

		current_val = to_col.Total()
		subtract_val = Cols['%s2_col' % colchar].Total()
		borrow_tag = '%s_borrow' % colchar

		# remove any existing borrow image
		cv.delete(borrow_tag)

		# as appropariate, create borrow image and set binding
		if current_val < subtract_val:
			startx, starty = LowerLeft(to_tag)
			cv.create_bitmap(
				startx, starty+10,
				bitmap = "@right_arrow.xbm", tags=borrow_tag)
			cv.tag_bind(borrow_tag, "<Button-1>", from_col.Borrow)

def CalcAnswer():
	"""
	show the final answer, if all original blocks have been "played"
	and no more carrying/borrowing needs to be done
	"""
	if mode.get() == ADD_MODE:
		input_tags = ['h1_col', 't1_col', 'o1_col', 'h2_col', 't2_col', 'o2_col']
		answ_tags = ['xA_col', 'hA_col', 'tA_col', 'oA_col']
	elif mode.get() == SUB_MODE:
		input_tags = ['h2_col', 't2_col', 'o2_col']
		answ_tags = ['hA_col', 'tA_col', 'oA_col']

	col_totals = [Cols[tag].Total() for tag in input_tags]
	remaining_input = reduce(operator.add, col_totals)

	# ready to show the answer? (note: there is no BorrowCount to check)
	if remaining_input == 0 and CarryCount == 0:
		anslist = [Cols[tag].Total() for tag in answ_tags]
		if mode.get() == ADD_MODE:
			answ.config(text="%d" % (1000*anslist[0] + 100*anslist[1] + 10*anslist[2] + anslist[3]))
		elif mode.get() == SUB_MODE:
			answ.config(text="%d" % (100*anslist[0] + 10*anslist[1] + anslist[2]))
		answ.config(bg=ANSW_COLOR)
		cv.bell()

def SetMode():
	"""
	adjust labels on entry fields for ADD/SUB mode
	"""
	if mode.get() == ADD_MODE:
		label_1.config(text="First number")
		label_2.config(text="Second number")
		signlabel.config(text="+")
		# display appropriate help text
		cv.delete('help_sub')
		aaa = cv.create_text(root.winfo_width()+HELP_ADD_OFFSET,  20,
							 font=TEXT_FONT, text=HELP_ADD, tags='help_add',
							 anchor=N, justify=RIGHT)

	else:
		label_1.config(text="Larger number")
		label_2.config(text="Smaller number")
		signlabel.config(text=u"\u2014")
		# display appropriate help text
		cv.delete('help_add')
		aaa = cv.create_text(root.winfo_width()+HELP_SUB_OFFSET, 20,
							 font=TEXT_FONT, text=HELP_SUB, tags='help_sub',
							 anchor=N, justify=RIGHT)

	ValidateKey()
	cv.update()
	zzz = 2

def Debug():
	pass

def NewCmd():
	"""
	start over
	"""
	# empty the canvas
	for obj in cv.find_all():
		cv.delete(obj)

	# reinit entry fields
	cv.input_1.set("")
	cv.input_2.set("")
	for obj in (addbtn, subbtn, entry_1, entry_2):
		obj.config(state=NORMAL)
	drawbtn.config(state=DISABLED)
	entry_1.focus()
	answ.config(bg=BGND_COLOR, text="")

	SetMode()

def ExitCmd():
	sys.exit(0)

def ClearSplash():
	cv.delete(splash_id)

####################
#################### main routine
####################

if __name__ == '__main__':
	# set up TK window
	root = Tk()

	# window geometry handling needs to be smarter!

	if ShowMeDo_800_600:
		root.geometry('%dx%d+%d+%d' % (760, 470, 0, 0))
	else:
		root.minsize(width=int(root.winfo_screenwidth()*0.75),
					 height=int(root.winfo_screenheight()*0.6))

	root.option_add('*font', FONT)
	root.title("BlockHead by John Posner")

	# --REMOVED FOR SIMPLICITY--
	# set up window's menu
	#mbar = Menu(root)
	#mbar.add_command(label="Exit", command=sys.exit)
	#root.config(menu=mbar)

	# set up canvas
	cv = Canvas(root)
	cv.pack(side=TOP, expand=True, fill=BOTH)
	cv.config(bg=BGND_COLOR)

	# set up mouse bindings
	cv.bind('<Button-1>', MouseDown)
	cv.bind('<Button1-Motion>', MouseMotion)
	cv.bind('<Button1-ButtonRelease>', MouseUp)

	# variables for entry fields
	cv.input_1 = IntVar()
	cv.input_2 = IntVar()

	###
	### control panel
	###

	ctrl = Frame(root)
	ctrl.pack(side=TOP)

	# subframes of control panel
	first_frm = Frame(ctrl); first_frm.pack(side=LEFT)
	sign_frm = Frame(ctrl); sign_frm.pack(side=LEFT)
	second_frm = Frame(ctrl); second_frm.pack(side=LEFT)
	equ_frm = Frame(ctrl); equ_frm.pack(side=LEFT)
	answ_frm = Frame(ctrl); answ_frm.pack(side=LEFT, padx=30)
	addsub_frm = Frame(ctrl); addsub_frm.pack(side=LEFT)
	btns_frm = Frame(ctrl); btns_frm.pack(side=LEFT, padx=5)

	# widths for controls
	labelwid = 13
	entrywid = 5

	# first number
	entry_1 = Entry(first_frm, width=entrywid, justify=CENTER, bg=ANSW_COLOR, textvariable=cv.input_1)
	entry_1.pack(side=TOP)
	label_1 = Label(first_frm, text="?", width=labelwid)
	label_1.pack(side=TOP)

	# sign (+ or -)
	signlabel = Label(sign_frm, text="?", width=3, font=(FONTNAME, 20, 'bold'))
	signlabel.pack(side=TOP)
	Frame(sign_frm, height=22).pack(side=TOP) # spacer pushes the sign upward

	# second number
	entry_2 = Entry(second_frm, width=entrywid, justify=CENTER, bg=ANSW_COLOR, textvariable=cv.input_2)
	entry_2.pack(side=TOP)
	label_2 = Label(second_frm, text="?", width=labelwid)
	label_2.pack(side=TOP)

	# equals
	Label(equ_frm, text="=", width=3, font=(FONTNAME, 20, 'bold')).pack(side=TOP)
	Frame(equ_frm, height=22).pack(side=TOP)

	# answer
	answ = Label(answ_frm, width=entrywid, justify=CENTER, bg=ANSW_COLOR, text="")
	answ.pack(side=TOP)
	label_A = Label(answ_frm, text="Answer")
	label_A.pack(side=TOP)

	# Add/Sub mode buttons
	mode = IntVar()
	addbtn = Radiobutton(addsub_frm, text="Add",      value=ADD_MODE, variable=mode, command=SetMode)
	addbtn.pack(side=TOP, anchor=W)
	subbtn = Radiobutton(addsub_frm, text="Subtract", value=SUB_MODE, variable=mode, command=SetMode)
	subbtn.pack(side=TOP, anchor=W)

	# control buttons
	drawbtn = Button(btns_frm, text=DRAW_STRING, relief=RAISED, borderwidth=2, command=GetEntries)
	drawbtn.pack(side=LEFT)
	Button(btns_frm, text="New", relief=RAISED, borderwidth=2, command=NewCmd).pack(side=LEFT)
	Button(btns_frm, text="Exit", relief=RAISED, borderwidth=2, command=ExitCmd).pack(side=LEFT)

	# initialize control panel
	mode.set(ADD_MODE)
	cv.input_1.set("")
	cv.input_2.set("")
	entry_1.bind('<KeyRelease>', ValidateKey)
	entry_2.bind('<KeyRelease>', ValidateKey)
	answ.config(bg=BGND_COLOR)

	# splash screen
	cv.update()
	splash_id = cv.create_text(root.winfo_width()*0.5, root.winfo_height()*0.4,
			text = "BlockHead\nby John Posner",
			justify=CENTER, font='helvetica 18 bold italic')
	# remove splash screen after 3 seconds
	root.after(3000, ClearSplash)
	SetMode()

	# don't allow window resizing
	root.resizable(0,0)

	# go
	root.mainloop()
	sys.exit(0)

Got any questions?

Get answers in the ShowMeDo Learners Google Group.

Video statistics:

  • Video's rank shown in the most popular listing
  • Video plays: 176 (since July 30th)
  • Plays in last week: 0
  • Published: 77 months ago

Thank-yous, questions and comments

If this video tutorial was helpful please take some time to say thank-you to the authors for their hard work. Feel free to ask questions. Let the author know why their video tutorial was useful - what are you learning about? Did the video tutorial save you time? Would you like to see more?

You may also want to see our ShowMeDo Google Group to speak to our active users and authors.

Your email address will not be published.

Show some quick comments >>








All comments excluding tick-boxed quick-comments

3. anonymous Wed, 26 Aug 2009 15:21

Quick UI question. Should the colour bar areas be 1 block shorter than it is now? So that the 10th block is outside the colour area and visually needs to be put in the next coloured box to the left?


That is very cool actually, I suppose it could be extended to show the manual division as well. Suppose you have 700 + 1100, you have 3 bars to begin with, 1s, 10s, 100s, and the 1000s is the same color as the 100s, maybe coloring this darker to make it more clear that this is a higher number.


Video published, thanks for contributing to ShowMeDo


Showmedo is a peer-produced video-tutorials and screencasts site for free and open-source software (FOSS)- with the exception of some club videos, the large majority are free to watch and download.

how to help » about » faq »

Educating the Open-source Community With Showmedo

Although as important as the software it supports, education and documentation are relatively neglected in the Open-source world. Coders love to code, and explaining how best to use or improve the software tends to be deferred or even sidelined.

At Showmedo we believe the community can play a vital role here and also say thanks for the tools and software that make our lives easier. If you have a piece of software you love or a programming langugage you are enthusiastic about, why not make a screencast showing others how to use it? All the stuff you wish you'd been told, the tips, tricks, insights that would have saved you time and frustration.

Screencasting is easier than you think, and we're happy to help you. You can emailus for advice or just use some of the how-to screencasts on the site. This screencasting learning-pathis a good place to start.

By the Same Author

Content

Feedback

Showmedo's development is fairly rapid and bugs will inevitably creep in. If you have any problems please drop us a line using the contact address below. Likewise, any suggestions for improvements to the site are gratefully received.

feedback@showmedo.com