Pythonのタートル・グラフィックス(6):木を描く

MYTurtleクラスを使って木を描くことにする。プログラムは以下のようになる:


import myturtle



angle = 60
ratio = 0.5
def ki( mikiT, length):
    if( length > 1):
        miki=mikiT.clone()
        miki.reverse()
        miki.fd(length)
        ki(miki, ratio*length)
        miki.rt(angle)
        ki(miki, ratio*length)
     

if __name__ == '__main__':
    
        
    miki = myturtle.MyTurtle()
    miki.clear()
    miki.ht()
    miki.lt(90)
    miki.setposition(0,-200)
    ki( miki, 200)

ここでは関数kiの引数に入るturtleオブジェクトがこのki関数で変化しないようにturtleオブジェクトのクローンを作って描画させている。

このクローンを使わない方法をとるにはki関数内でダミーな動きをさせて結果的にturtleオブジェクトの状態変化がなかったようにする:


import myturtle



angle = 60
ratio = 0.5
def ki( miki, length):
    if( length > 1):
        miki.reverse()
        miki.fd(length)
        ki(miki, ratio*length)
        miki.rt(angle)
        ki(miki, ratio*length)
        miki.lt(angle) #ダミー
        miki.bk(length) #ダミー
        miki.reverse() #ダミー
    

if __name__ == '__main__':
    
        
    miki = myturtle.MyTurtle()
    miki.clear()
    miki.ht()
    miki.lt(90)
    miki.setposition(0,-200)
    ki( miki, 200)

【描画の結果】

クローンkiなしのki

Pythonのタートル・グラフィックス(5):MyTurtleクラスの導入

「Turbo Graphics」(安齋利洋・伊吹龍著;1987年)にはタートルの動きに対する面白い命令がある。それがreverseである。これはタートルの右回転(rt)と左回転(lt)の役割を反転させる機能を持っている。Pythonのタートル・グラフィックスにはこの機能がない。Python上でこの機能を関数として定義してもよいが折角なのでreverseをタートルクラスの一つメソッドとして追加したMyTurtleクラスを作ることにする。

MyTurtleクラスは元々のモジュール(turtle)で定義されているTurtleクラスを継承して作る。クラスの定義は以下のようなものである:


import turtle

class MyTurtle(turtle.Turtle):
    def __init__(self):
        turtle.Turtle.__init__(self)
        self.phase = 1

    def rt(self, angle):
        self.angle = angle*self.phase
        turtle.Turtle.rt(self, self.angle)

    def lt(self, angle):
        self.angle = angle*self.phase
        turtle.Turtle.lt(self, self.angle)

    def rev(self):
        return self.phase

    def reverse(self):
        self.phase=self.phase*-1

    def clone(self):
        temp=turtle.Turtle.clone(self)
        temp.phase = self.phase
        return temp

このクラスではreverseの導入によって影響を受けるturtleモジュールの中のメッソドの書き換えは最小限に止めてある。

Pythonのタートル・グラフィックス(4):クローンkameなしの複雑再帰

先のkameの複雑な再帰描画をクローンを使わないで行うことを考える。まず問題の所在を明確にするためにプログラムにデバッグ用のprint文を追加した。プログラムの一部を載せる(再帰の深さは浅くしてある)。

def kame( position, size):
space = ' '*4*(int(200/size)-1)
print(space, 'draw kame',position.ycor(), size)
sugata( position, size)
if size > 75:
position.fd(size/2)
kame(position, size/2)
position.bk(size)
kame(position, size/2)
print(space, 'returning', position.ycor(), size)

この出力は以下のようになる:


 draw kame 0.0 200
     draw kame 100.0 100.0
             draw kame 150.0 50.0
             returning 150.0 50.0
             draw kame 50.0 50.0
             returning 50.0 50.0
     returning 50.0 100.0
     draw kame -150.0 100.0
             draw kame -100.0 50.0
             returning -100.0 50.0
             draw kame -200.0 50.0
             returning -200.0 50.0
     returning -200.0 100.0
 returning -200.0 200

draw kame 100に対応する(再帰のレベルが同じ)描画がdraw kame -150となっておりこれがずれの原因になる。

そこで関数kameに渡したオブジェクトpositionの属性がこのkame関数のなかで最終的に変化がなかったようにダミーの動きをいれる。プログラムは以下のようになる。


#coding: utf-8

import turtle

def sugata( center, size):
    for i in range(6):
        painter = center.clone()
        painter.fd(size)
        painter.rt(120)
        painter.fd(size)
        center.rt(60)

def kame( position, size):
    #space = ' '*4*(int(200/size)-1)
    #print(space, 'draw kame',position.ycor(), size)
    sugata( position, size)
    if size > 10:
        position.fd(size/2)
        kame(position, size/2)
        position.bk(size)
        kame(position, size/2)
        position.fd(size/2) #ダミーの動き
    #print(space, 'returning', position.ycor(), size)

    
if __name__ == '__main__':
    
    turtle.clearscreen()
    position = turtle.Turtle()
    position.ht()
    position.lt(90)
    kame( position, 200)

【結果の描画」

クローンkameなしの再帰描画の完成図

期待した通りの結果が得られた。

 

Pythonのタートル・グラフィックス(3):クローンkameによる複雑再帰

「Turbo Graphics」(安齋利洋・伊吹龍著;1987年)にはもう少し複雑なkameの再帰描画の例が載っている。プログラムをなぞってPythonで書くと以下のようになる:


#coding: utf-8

import turtle

def sugata( center, size):
    for i in range(6):
        painter = center.clone()
        painter.fd(size)
        painter.rt(120)
        painter.fd(size)
        center.rt(60)

def kame( position, size):
    sugata( position, size)
    if size > 10:
        position.fd(size/2)
        kame(position, size/2)
        position.bk(size)
        kame(position, size/2)

    
if __name__ == '__main__':
    
    turtle.clearscreen()
    position = turtle.Turtle()
    position.ht()
    position.lt(90)
    kame( position, 200)

描画は以下のようになる:

kameの再帰描画

ご覧のように再帰の描画がずれたものになっている。

このずれの原因として考えられるものはオブジェクトを関数に渡す仕様がTurboPascalとPythonとで異なることにあるように思われた。そこで

def kame( positionT, size):
position = positionT.clone()

として関数の中で実引数が影響を受けないようにした。結果の描画は以下のようになる:

kameの再帰描画

これで期待した通りになったが、描画のアルゴリズムを変えてクローンを使わないことにしたい。

Pythonのタートル・グラフィックス(2):kameの単純再帰描画

Pythonのタートル・グラフィックス(1)で採り上げたKameの単純な再帰プログラムを考える。プログラムは以下のようなものである:


#coding: utf-8

import turtle

def sugata( center, size):
    for i in range(6):
        painter = center.clone()
        painter.fd(size)
        painter.rt(120)
        painter.fd(size)
        center.rt(60)

def kame( position, size):
    sugata( position, size)
    if size > 10:
        kame(position, size/2)
    
if __name__ == '__main__':
    
    turtle.clearscreen()
    position = turtle.Turtle()
    position.ht()
    position.lt(90)
    kame( position, 200)

実行結果の描画

kame2の描画

期待した結果である。

Pythonのタートル・グラフィックス(1):kameによるカメの描画

LibreLogoに関するブログを書いていたらPythonのタートル・グラフィックスことが気になった。

書架を見たら「Turbo Graphics」(安齋利洋・伊吹龍著;1987年)という昔使った本が目についた。これはTurbo Pascalを使ったグラフィックスの本である。この中にタートル・グラフィックスの例題が沢山あるので今回はこれらをPythonのタートル・グラフィックスで書いてみた。なるべく忠実に例題をなぞるようにした。

例題1:kame


#coding: utf-8

import turtle

def sugata( center, size):
    for i in range(6):
        painter = center.clone()
        painter.fd(size)
        painter.rt(120)
        painter.fd(size)
        center.rt(60)

def kame( position, size):
    sugata( position, size)
    
if __name__ == '__main__':
    
    turtle.clearscreen()
    position = turtle.Turtle()
    position.ht()
    position.lt(90)
    kame( position, 200)

多少気になったのは以下の部分である:

painter = center.clone()

Pascalでは単なる代入文でpainter=centerとなっている。Pythonでは代入文によるオブジェクトの代入はできない。copyモジュールのcopyメソッドを使ってみたが描画の一部が消えてしまう。deepcopyではTkinterのエラーがでる。Pythonのタートル・グラフィックのドキュメントにあったclone()で期待した通りの描画が得られた。clone()とcopyメソッドの相違は不明。

【結果の描画】

kame

なお描画ではカメ印(?)は隠してある。

Pythonでマクロ:LibreLogoで日本語プログラミング

こんなプログラムを考える:


TO draw length  n
  IF n  = 0 [ 
    STOP ]
  angle = 60
  FORWARD length * n
  LEFT angle
  draw length n-1
  RIGHT 2 * angle
  draw length n-1
  LEFT angle
  BACK length * n
END

PENCOLOR “black”
draw 20 5
HOME
PENCOLOR “red”
draw 22 5

大文字のみの単語はLibreLogoの既約語である。この既約語を日本語で表現する。
例えば以下の様にしてみた:


やるべきこと draw length  n
  もしも n  = 0 [ 
    実行終了 ]
  angle = 60
  前へ length * n
  左へ angle
  draw length n-1
  右へ 2 * angle
  draw length n-1
  左へ angle
  後へ length * n
記述終わり

ペンの色 “black”
draw 20 5  ;やるべきことdrawの実行
定位置へ
ペンの色 “red”
draw 22 5 ;やるべきことdrawの実行

TO(やるべきこと)、END(記述終わり)あたりが難しい。プログラミングで使われている英単語は短くそれ自身では符牒のようなものだ。日本語化ではそれに捉われず多少冗長でも機能が明確になるような語句にするとよいと思った。

Pythonでマクロ:LibreLogoで再帰関数を実行する

LibreOfficeのWriter(文書処理)は一つの機能としてタートル・グラフィックスのためのプログラミング環境を提供している。それがLibreLogoである。文書処理の中にプログラミング環境を提供するという発想が面白い。コンピュータ・プログラミングの敷居を低めることができるかもしれない。LOGOに似た言語が使える。LibreLogoの詳細はここにある。

早速使ってみる。再帰関数が動くか確かめた。以下が使った再帰関数の一例である。

【再帰関数】


TO draw length  n
  IF n  = 0 [ STOP ]
  angle = 60
  FORWARD length * n
  LEFT angle
  draw length n-1
  RIGHT 2 * angle
  draw length n-1
  LEFT angle
  BACK length * n
END

draw 20 5

TO draw length nからENDまでが再帰関数の定義である。大文字ばかりの単語はLibreLogoの既約語である。これには日本語が使える。

【実行結果】

タートル・グラフィックス(中央にいるのがカメ)

LibreLogoで再帰処理ができることが解った。

Pythonでマクロ:AnimatedImageControlを使ってみる

Libreofficeのダイアログ画面上で一組の画像をアニメーションのように自動的に順次見せるAnimatedImageControlを使ってみた。

このAnimatedImageControlを使ったBasic言語による雛形はここにある。これをPythonに翻訳した。注意点は:

  • insertImageSet(0, 画像ファイルパスの配列)というメソッドがある。Basic言語では画像ファイルパスの配列にはそのまま配列名を書く。Pythonでは画像ファイルパスの配列としてリストやタプルで構成することができるが、リストではダメでタプルで作ったものを使う。
  • インポートしたモジュールで定義されている定数を実引数に使うメソッド、例えばsetPosSize(10, 10, 1000, 900, モジュール定義の定数)。Basic言語ではcom.sun.star.awt.PosSize.POSSIZEのように階層構造をそのまま書く。Pythonではunoモジュールで定義されいる関数を使う。つまりuno.getConstantByName(“com.sun.star.awt.PosSize.POSSIZE”)

【マクロ】


#coding: utf-8
import uno
import screen_io as ui
import unohelper

def python_macro_clipart_anim(*arg):
#
#シート
  doc = XSCRIPTCONTEXT.getDocument()
  sheet = doc.Sheets[0]
  #データのある行数を調べる
  sRange = sheet.getCellRangeByName("A1")
  sCursor=sheet.createCursorByRange(sRange)
  sCursor.collapseToCurrentRegion()
  MaxDataRow = sCursor.Rows.Count-1
  ui.Print(MaxDataRow)
  items=[]
  imageURLs=[]
  for  i in range(MaxDataRow):
    item = sheet.getCellByPosition(0, i+1 ).String
    filePath = sheet.getCellByPosition(1, i+1 ).String
    #ui.Print(item)
    #ui.Print(filePath)
    items.append(item)
    imageURLs.append(uno.systemPathToFileUrl(filePath))
  imageURLsT=tuple(imageURLs)
#
#ダイアログ
  ctx = XSCRIPTCONTEXT.getComponentContext()
  smgr = ctx.getServiceManager()
  dialogM = smgr.createInstance('com.sun.star.awt.UnoControlDialogModel')
  # Size of DialogM
  dlgWth = 500
  dlgHgt = 400
  dialogM.Width = dlgWth
  dialogM.Height = dlgHgt
  dialogM.Title = "クリップアート・アニーション"
#ダイアログの生成とモデルの登録
  dialog = smgr.createInstance('com.sun.star.awt.UnoControlDialog')
  dialog.setModel(dialogM)
#
#
  animCtrlModel = smgr.createInstance('com.sun.star.awt.AnimatedImagesControlModel')
  animCtrlModel.insertImageSet(0, imageURLsT)
  animCtrlModel.StepTime = 1000		
  animCtrlModel.AutoRepeat = False
  animCtrlModel.ScaleMode = 1
#アニメーション・コントロールの登録
  animateCtrl = smgr.createInstance('com.sun.star.awt.AnimatedImagesControl')
  animateCtrl.setModel(animCtrlModel)
#
  dialog.addControl("animated", animateCtrl)
  POSSIZE = uno.getConstantByName("com.sun.star.awt.PosSize.POSSIZE")
  animateCtrl.setPosSize(10, 10, 1000, 900, POSSIZE)
#
#窓の生成そしてこの窓をダイアログ画面として使う
  window = smgr.createInstance('com.sun.star.awt.Toolkit')
  dialog.createPeer(window,None)		# None : OK / none : NG
#
  animateCtrl.startAnimation()

  dialog.execute()
  dialog.dispose()

属性StepTime=1000で個々画像を一秒間表示するはずだがこの値を変えて表示時間に変化はない。また属性AutoRepeat = Falseで一組の画像の表示は一度のはずだが繰り返し表示される。

Pythonでマクロ:ImageControlでクリップアートのカタログを作成

ダイアログの部品の一つにImageControlがある。ダイアログに複数の画像を表示する機能を持ったコントロールである。いろんな使い道がありそうであるがここでは多数のクリップアートを表示する一種のカタログを作ってみることにする。

クリップアート・カタログ

上図のようにCalcのシート上にクリップアートのタイトルと画像ファイルへのpathが書かれたものがあるとする。マクロではこのシート上のデータを読み込んでダイアログ上にこれらのクリップアートが閲覧できる一種のカタログを作る。

ImageControlの詳細はここにある。特に重要な属性は

  • ImageURL:イメージデータのある場所をURL形式で指定する。
  • ScaleMode:ダイアログ上の決められた枠にどのように画像を表示するかを指定する。この値が2であると兎に角画像をこの枠に納めて表示する(ANISOTROPIC)。この値が1であると画像のアスペクト比を保持して表示する(ISOTROPIC)。

ファイルpathのURL形式への変換(またはその逆)はPythonでは以下のunoモジュールが使える:

 path = "/home/foo/Documents/file.odt"
url = uno.systemPathToFileUrl(path)
path = uno.fileUrlToSystemPath(url)

カタログはダイアログ画面に納まらないので垂直スクロールの機能を付ける。

【実行画面】

カタログ最初の画面
カタログ末尾の画面

 

 

 

 

 

 

 

 

【マクロ】


#coding: utf-8
import uno
import screen_io as ui
import unohelper
from com.sun.star.awt import XActionListener
from com.sun.star.awt import XAdjustmentListener

#「スクロールバー」の処理
class MyAdjustmentListener(unohelper.Base, XAdjustmentListener):
  def __init__(self, dlgHgt, catHgt, labs, imcs):
    self.labs = labs
    self.imcs = imcs
    self.scrollVMax=100
    self.dlgHgt=dlgHgt
    self.catHgt=catHgt
    self.labsPositionY0 = []
    self.imcsPositionY0 = []
    for i, lab in enumerate(labs):
      print(lab.PositionY, imcs[i].PositionY)
      self.labsPositionY0.append(lab.PositionY)
      self.imcsPositionY0.append(imcs[i].PositionY)
  def adjustmentValueChanged(self, event):
    ratio = event.Value/self.scrollVMax
    print(ratio)
    for i,lab in enumerate(self.labs):
      lab.PositionY = self.labsPositionY0[i]-(self.catHgt - self.dlgHgt)*ratio
      self.imcs[i].PositionY = self.imcsPositionY0[i]-(self.catHgt - self.dlgHgt)*ratio
    #print(event.Type)
    #print(event.Value)

def python_macro_clipart(*arg):
#
#シート
  doc = XSCRIPTCONTEXT.getDocument()
  sheet = doc.Sheets[0]
  #データのある行数を調べる
  sRange = sheet.getCellRangeByName("A1")
  sCursor=sheet.createCursorByRange(sRange)
  sCursor.collapseToCurrentRegion()
  MaxDataRow = sCursor.Rows.Count-1
  ui.Print(MaxDataRow)
  items=[]
  filePaths=[]
  for  i in range(MaxDataRow):
    item = sheet.getCellByPosition(0, i+1 ).String
    filePath = sheet.getCellByPosition(1, i+1 ).String
    #ui.Print(item)
    #ui.Print(filePath)
    items.append(item)
    filePaths.append(filePath)
#
#ダイアログ
  ctx = XSCRIPTCONTEXT.getComponentContext()
  smgr = ctx.getServiceManager()
  dialogM = smgr.createInstance('com.sun.star.awt.UnoControlDialogModel')
  # Size of DialogM
  dlgWth = 150
  dlgHgt = 200
  dialogM.Width = dlgWth
  dialogM.Height = dlgHgt
  dialogM.Title = "クリップアート・カタログ"
#

#コントロールの作成・登録
  tabIndex = 0 
#スクロールバー
  scb1 = dialogM.createInstance('com.sun.star.awt.UnoControlScrollBarModel')
  tabIndex += 1
  scrollVMin=0
  scrollVMax=100
  scb1.Name = 'ScrollBar'
  scb1.TabIndex = tabIndex
  scb1.PositionX = dlgWth-10
  scb1.PositionY = 0
  scb1.Width = 10
  scb1.Height = dlgHgt
  scb1.Orientation=1
  scb1.ScrollValueMin = scrollVMin
  scb1.ScrollValueMax = scrollVMax
  scb1.LiveScroll= 1
  dialogM.insertByName('ScrollBar', scb1)
#タイトル・画像の登録
  imageX = 100
  imageY = int(imageX*3/4)
  labs=[]
  imcs=[]
  for i in range(MaxDataRow):
    #タイトル
    lab2 = dialogM.createInstance('com.sun.star.awt.UnoControlFixedTextModel')
    labY=10+(imageY+10+10)*i
    tabIndex += 1
    lab2.Name = items[i]
    lab2.TabIndex = tabIndex
    lab2.PositionX =10
    lab2.PositionY =labY
    lab2.Width = dlgWth - 10
    lab2.Height = 10
    lab2.Label = items[i]
    lab2.Align = 0					# 0 : Left / 1 : Center / 2 : Right
    lab2.Border = 0
    dialogM.insertByName(items[i], lab2)
    labs.append(lab2)
    #画像
    imc2 = dialogM.createInstance('com.sun.star.awt.UnoControlImageControlModel')
    imcY = labY+10
    tabIndex += 1
    imc2.Name = items[i]
    imc2.TabIndex = tabIndex
    imc2.PositionX = 10
    imc2.PositionY = imcY
    imc2.Width = imageX
    imc2.Height = imageY
    imc2.ImageURL = uno.systemPathToFileUrl(filePaths[i])
    imc2.ScaleMode = 1
    dialogM.insertByName(items[i]+'image', imc2)
    imcs.append(imc2)
#最終的なチェックボックス表の高さ
  catHgt=imcY + (imageY + 10)

# Create the dialog and set the model
#ダイアログの生成とモデルの登録
  dialog = smgr.createInstance('com.sun.star.awt.UnoControlDialog')
  dialog.setModel(dialogM)
#コントロールの登録
  cmdScb1 = dialog.getControl('ScrollBar')
#エヴェント監視(スクロールバー)
  scb1_listener = MyAdjustmentListener(dlgHgt, catHgt, labs, imcs)
  cmdScb1.addAdjustmentListener(scb1_listener)
#
#窓の生成そしてこの窓をダイアログ画面として使う
  window = smgr.createInstance('com.sun.star.awt.Toolkit')
  dialog.createPeer(window,None)		# None : OK / none : NG
#
  dialog.execute()
  dialog.dispose()