text_edit_object_with_tkinter.py を Blender 2.62 で動かす

リソース・チュートリアル : 最近の Blender 2.5の Text オブジェクトの日本語対応 Blender.jp
この記事で紹介されているスクリプトを修正し 2.62 で動作するところまでこぎつけました。
動作確認をしたのは Windows XP 32bit、Windows 7 64bit のみです。
Python は 3.2 です。


text_edit_object_with_tkinter_2_62.py
※この記事の最後にスクリプトの全文を記載しています。

インストール

Python のパスがうまく解決できなかったため、とりあえずファイルをコピーして動作するのを確認しています。このあたりは出来れば解決したい……
次のファイルを Blenderpython ディレクトリにコピーしてください。
(\Blender Foundation\Blender\2.62\python 以下)

  • Python32\DLLs
  • Python32\tcl
  • Python32\Lib\tkinter


スクリプトは普通にアドオンのインストールの手順でインストールしてください。
アドオンのカテゴリーは Text です。
名称は Edit text object with Tkinter となっていて元のスクリプトから変更しておりません。

使用方法

ウィンドウを表示するには、Text Object を追加し、選択し、Properties -> Object Data を選択し、一番下の Edit Text Object ボタンをクリックしてください。
Load Fonts ボタンの方は何も変更を加えていないので動かないかもしれません。
その場合フォントは適切なフォントを選択しておいてください。
デフォルトのフォントでは日本語が入っていない(?)ため、日本語の文字が表示されません。


ウィンドウを表示したときにランタイムエラーが発生する場合、次のファイルをリネーム等して読み込まれない状態にすればエラーは表示されなくなると思います。
\Blender Foundation\Blender\msvcr90.dll


ウィンドウが表示されている状態でも Blender 上で操作は可能です。
ただし、ウィンドウを表示するときに選択されていたオブジェクトを参照しているので、別の Text Object を選択しテキストを編集しても最初に選択していた Text Object に変更が反映されます。
また、最初に選択していた Text Object を削除した場合正しく動作しません。


ウィンドウ内でテキストを書き換えるとリアルタイムに 3D View 上の Text Object に反映します。
Apply ボタンを押下するか、×ボタンでウィンドウを閉じて編集を完了してください。
Cancel ボタンを押下すると、ウィンドウを立ち上げたときの内容のテキストに差し戻します。

text_edit_object_with_tkinter_2_62.py

# coding: utf-8

# ##### BEGIN GPL LICENSE BLOCK #####
#
#  This program is free software; you can redistribute it and/or
#  modify it under the terms of the GNU General Public License
#  as published by the Free Software Foundation; either version 2
#  of the License, or (at your option) any later version.
#
#  This program is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with this program; if not, write to the Free Software Foundation,
#  Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# ##### END GPL LICENSE BLOCK #####


bl_info = {
    'name': 'Edit text object with Tkinter',
    'author': 'chromoly',
    'version': (0, 0, 1),
    'blender': (2, 6, 2),
    'api': 32385,
    'location': 'Properties -> ObjectData',
    'description': '',
    'warning': '',
    'category': 'Text'}

'''
日本語入力の為、TkinterのGUIを呼び出します。GUIを呼び出している間、Blenderは止まります。
参考: http://docs.python.org/py3k/library/tkinter.html
      Pythonプログラミング入門

'''

import os
import sys
import threading

#### 環境に合わせて修正してください #############################################
## Tkinter path
'''
importエラーを吐くようなら、モジュールパスを修正してください。
'''
try:
    import tkinter as tk
except:
    if sys.platform == 'win32':  # Windows
        sys.path.append('C:\\Python32\\Lib')
        sys.path.append('C:\\Python32\\DLLs')
        os.environ['TCL_LIBRARY'] = 'C:\\Python32\\tcl\\tcl8.5'
    else:  # sys.platform == 'linux2'  etc...
        prefix = '/usr'
        dirname = 'python' + sys.version[:3]  # python3.1
        sys.path.append(os.path.join(prefix, 'lib', dirname))
        sys.path.append(os.path.join(prefix, 'lib', dirname, 'lib-dynload'))

## Load Fonts
'''
'Load Fonts'ボタンで複数のフォントを一度に読み込みます。
'''
if sys.platform == 'win32':  # Windows
    font_paths = ('C:\WINDOWS\Fonts\msgoth04.ttc',
                  'C:\WINDOWS\Fonts\msmin04.ttc')
else:  # Linux etc.
    font_paths = ('/home/hoge/.fonts/meiryo/meiryo.ttc',
                  '/home/hoge/.fonts/meiryo/meiryob.ttc')

## Tkinter GUI
'''
FONTNAMEは、利用可能なフォント名を指定してください。
読み込めない場合はデフォルトのフォントが使われます。
'''
FONTNAME = 'Meiryo'
FONTSIZE = 11
###############################################################################

import tkinter as tk
from tkinter.scrolledtext import ScrolledText
from tkinter.filedialog import askopenfilename, asksaveasfilename
import bpy
from bpy.props import *
import time

class Scroller(tk.Frame):
    def __init__(self, master=None, textobj=None):
        tk.Frame.__init__(self, master)
        self.pack()
        self.cached_text = ""

        # <Title>
        master.title('Edit TextObject')

        # <Menu>
        self.mb = tk.Menu(master, font=(FONTNAME, FONTSIZE))

        self.fm = tk.Menu(self.mb, font=(FONTNAME, FONTSIZE), tearoff=0)
        self.fm.add_command(label='開く', command=self.openFile)
        self.fm.add_command(label='別名で保存', command=self.saveFile)
        self.fm.add_separator()
        self.fm.add_command(label='終了', command=self.quit)  # master.quit
        self.mb.add_cascade(label='ファイル', menu=self.fm)

        self.em = tk.Menu(self.mb, font=(FONTNAME, FONTSIZE), tearoff=0)
        self.em.add_command(label='切り取り', command=self.cutSelection)
        self.em.add_command(label='複製', command=self.copySelection)
        self.em.add_command(label='貼り付け', command=self.pasteClip)
        self.mb.add_cascade(label='編集', menu=self.em)

        master.config(menu=self.mb)

        # <Shortcut>  master.bindだと二回繰り返す事に
        self.bind('<Control-KeyPress-x>', self.cutSelection)
        self.bind('<Control-KeyPress-c>', self.copySelection)
        self.bind('<Control-KeyPress-v>', self.pasteClip)

        # <Button>
        f1 = tk.Frame(master, relief=tk.SUNKEN, bd=1)
        f1.pack()
        self.bpy_clear = tk.Button(f1, text='Clear', \
                                   font=(FONTNAME, FONTSIZE), \
                                   command=self.bpyClear, width=12)
        self.bpy_clear.pack(side='left')
        '''
        self.bpy_read = tk.Button(f1, text='Read', \
                                  command=self.bpyRead, width=12)
        self.bpy_read.pack(side='left')
        self.bpy_apply = tk.Button(f1, text='Apply', width=12)
        self.bpy_apply['command'] = self.bpyApply  # こんな書き方も
        self.bpy_apply.pack(side='left')
        '''
        self.bpy_applyquit = tk.Button(f1, text='Apply', \
                                       font=(FONTNAME, FONTSIZE),\
                                       command=self.bpyApplyQuit, width=12)
        self.bpy_applyquit.pack(side='left')

        self.bpy_quit = tk.Button(f1, text='Cancel', \
                                  font=(FONTNAME, FONTSIZE), \
                                  command=self.bpyCancel, width=12)
        self.bpy_quit.pack(side='right')

        # <Text>
        self.st = ScrolledText(master, font=(FONTNAME, FONTSIZE))
        self.st.pack(fill=tk.BOTH, expand=1)
        self.st.focus_set()
        
        # <Read from bpy.context.active_object>
        self.setTextObject(textobj)

    def setTextObject(self, txtobj):
        self.text_active_object = txtobj
        
    def getTextObject(self):
        ob = self.text_active_object
        if ob and ob.type == 'FONT':
            return ob
        return None

    def bpyClear(self):
        self.st.delete('1.0', tk.END)

    def bpyRead(self):
        ob = self.getTextObject()
        if ob:
            editmode = ob.mode
            if editmode == 'EDIT':
                bpy.ops.object.mode_set(mode='OBJECT')
            text = ob.data.body
            self.cached_text = text
            self.st.delete('1.0', tk.END)
            self.st.insert(tk.END, text)
            if editmode == 'EDIT':
                bpy.ops.object.mode_set(mode='EDIT')

    def setText(self, str):
        ob = self.getTextObject()
        if ob:
            editmode = ob.mode
            if editmode == 'EDIT':
                bpy.ops.object.mode_set(mode='OBJECT')
            ob.data.body = str
            if editmode == 'EDIT':
                bpy.ops.object.mode_set(mode='EDIT')        
                
    def bpyApply(self):
        self.setText(self.st.get('1.0', tk.END))

    def bpyApplyQuit(self):
        self.bpyApply()
        self.quit()
        
    def bpyCancel(self):
        self.setText(self.cached_text)
        self.quit()

    def openFile(self):
        fn = askopenfilename()
        if fn:
            fi = open(fn, 'r')
            b = fi.read()
            fi.close()
            self.st.delete('1.0', tk.END)
            self.st.insert(tk.END, b)

    def saveFile(self):
        fn = asksaveasfilename()
        if fn:
            fo = open(fn, 'w')
            fo.write(self.st.get('1.0', tk.END))
            fo.close()

    def cutSelection(self, *tmp):
        if self.st.tag_ranges(tk.SEL):
            self.copySelection()
            self.st.delete(tk.SEL_FIRST, tk.SEL_LAST)

    def copySelection(self, *tmp):
        if self.st.tag_ranges(tk.SEL):
            t = self.st.get(tk.SEL_FIRST, tk.SEL_LAST)
            self.st.clipboard_clear()
            self.st.clipboard_append(t)

    def pasteClip(self, *tmp):
        if self.st.tag_ranges(tk.SEL):
            self.st.delete(tk.SEL_FIRST, tk.SEL_LAST)
        t = self.st.selection_get(selection='CLIPBOARD')
        self.st.insert(tk.INSERT, t)

class Invoker(threading.Thread):
    def __init__(self, m):
        threading.Thread.__init__(self)
        self.method = m

    def run(self):
        self.method()

class TEXT_OT_edit_object_with_tkinter(bpy.types.Operator):
    '''Call Tkinter GUI'''
    bl_description = 'Call Tkinter GUI and Blender stop'
    bl_idname = 'text.edit_object_with_tkinter'
    bl_label = 'Call Tkinter GUI'

    @classmethod
    def poll(cls, context):
        actob = context.active_object
        return actob and actob.type == 'FONT'

    def pre_call(self):
        self.root = tk.Tk()
        self.app = Scroller(master=self.root, textobj=bpy.context.active_object)
        
        self.app.st.bind("<ButtonRelease>", self.callback)
        self.app.st.bind("<KeyRelease>", self.callback)
        
    def callback(self, event):
        self.app.bpyApply()
        
    def call_tk(self):
        self.app.bpyRead()
        self.app.mainloop()
        try:
            self.root.destroy()
        except:
            pass
        del self.app
        del self.root
        del self.t
        self._timer = bpy.context.window_manager.event_timer_add(0.1, bpy.context.window)
            
    def execute(self, context):
        context.window_manager.modal_handler_add(self)        
        self.pre_call()        
        self.t = Invoker(self.call_tk)
        self.t.start()        
        return {'RUNNING_MODAL'}
        
    def cancel(self, context):
        return {'CANCELLED'}

    def modal(self, context, event):
        if event.type == 'ESC':  # Cancel
            return self.cancel(context)
        if event.type == 'TIMER':
            context.window_manager.event_timer_remove(self._timer)
            del self._timer
            return {'CANCELLED'}
        
        return {'PASS_THROUGH'}


class TEXT_OT_load_fonts(bpy.types.Operator):
    '''Load fonts'''
    bl_description = 'Load fonts. ' + \
                   '(edit "text_edit_object_with_tkinter.py":62~76:font_paths)'
    bl_idname = 'text.load_fonts'
    bl_label = 'Load Fonts'

    setactob = BoolProperty(default=False)

    '''
    @classmethod
    def poll(cls, context):
        actob = context.active_object
        return actob and actob.type == 'FONT'
    '''

    def execute(self, context=None):
        if not font_paths:
            return {'FINISHED'}
        loadedpaths = [f.filepath for f in bpy.data.fonts]
        for path in font_paths:
            if not path in loadedpaths:
                bpy.ops.font.open(filepath=path)

        if self.setactob:
            actob = bpy.context.active_object
            if actob and actob.type == 'FONT':
                for font in bpy.data.fonts:
                    if font.filepath == font_paths[0]:
                        actob.data.font = font
                        break
        return {'FINISHED'}


class DATA_PT_call_tkinter(bpy.types.Panel):
    bl_label = "Edit Text with Tkinter"
    COMPAT_ENGINES = {'BLENDER_RENDER', 'BLENDER_GAME'}
    bl_space_type = 'PROPERTIES'
    bl_region_type = 'WINDOW'
    bl_context = 'data'

    @classmethod
    def poll(cls, context):
        '''engine = context.scene.render.engine
        actob = context.active_object
        return (actob and actob.type == 'FONT' and \
                (engine in cls.COMPAT_ENGINES))
        '''
        actob = context.active_object
        return (actob and actob.type == 'FONT')

    def draw(self, context):
        layout = self.layout
        row = layout.row()
        row = row.split(percentage=0.7)
        row.operator_context = 'INVOKE_DEFAULT'
        op = row.operator('text.edit_object_with_tkinter', text='Edit Text Object')
        op = row.operator('text.load_fonts', text='Load Fonts')
        op.setactob = True


# Register
def register():
    bpy.utils.register_class(DATA_PT_call_tkinter)
    bpy.utils.register_class(TEXT_OT_edit_object_with_tkinter)
    bpy.utils.register_class(TEXT_OT_load_fonts)


def unregister():
    bpy.utils.unregister_class(DATA_PT_call_tkinter)
    bpy.utils.unregister_class(TEXT_OT_edit_object_with_tkinter)
    bpy.utils.unregister_class(TEXT_OT_load_fonts)


if __name__ == '__main__':
    register()