目的
デバッグ中に何らかの内部データを見るために、デバッガでbreakpointを設定して、逐次止めてみたり、流れていくログテキストを追うのは効率が悪いです。
MultiProcess環境ですと、breakpointで止めてしまうと正常な再開ができなかったりします。
それに、ユーザーからの入力も受け付けてほしいとき、ターミナルのキー入力を使って行うと、ログのテキストが流れて、訳が分からなくなります。
作成物
Windows用でいたってシンプルです。
機能
・インスタンス生成すると、自動的に別スレッドで動き出します。(mainloopで返ってこないということはありません。)
・インスタンス生成時にダイアログタイトルと初期メッセージを渡して表示できます。
・テキストの最終行でEnterを押すとその前の行を切り出して、Queueで返信してくれます。
→メッセージルールを作っておけば、受信したテキストに応じてアプリケーションの挙動を制御できますね。
例えば、#command が先頭にあったら何等かの動作をするなど。
・テキストを表示させる場合、Queueでテキストを送るか、text_insert関数を使用します。
・ダイアログ終了時に、'#END'がQueueに送出されますので、受信したら親スレッドでdelete関数を呼び出してください。
機能追加の修正履歴
2021/03/29 TabおよびShift+Tabキーによるコマンド履歴入力を追加しました。同じ内容を毎回入力するのも億劫なので、Tab(遡り)、Shift+Tab(進み)で過去の入力が表示されます。全く同じ入力は過去のものが更新されて、新しい履歴に再入されます。
コード
import threading
import time
from queue import Empty, Queue
import tkinter as tk
from tkinter import ttk
class class_DebugBufferView(threading.Thread):
def __init__(self,message,title=None):
threading.Thread.__init__(self)
if title is not None:
self.title = title
else:
self.title = 'Debug View'
self.message = message
self.text_index = 1.0
self.after_interval = 1000
self.sendQueue, self.recvQueu = None, None
self.isLoop = True
self.command_list = list()
self.tab_repeat_cnt = 0
self.isTab = False
self.start()
return
def CreateDialog(self):
self.Text = tk.Text(
self.dialog,
wrap = tk.NONE,
)
self.v_scroll = ttk.Scrollbar(
self.dialog,
orient = tk.VERTICAL,
command = self.Text.yview
)
self.Text['yscrollcommand'] = self.v_scroll.set
self.Text.bind('<Key-Return>',self.key_interrupt)
self.Text.bind('<Key-Tab>',self.key_repeat)
self.Text.bind('<Shift-Key-Tab>',self.key_back_repeat)
self.Text.bind('<KeyRelease-Tab>',self.tab_release)
self.h_scroll = ttk.Scrollbar(
self.dialog,
orient = tk.HORIZONTAL,
command = self.Text.xview
)
self.Text['xscrollcommand'] = self.h_scroll.set
self.v_scroll.pack(side=tk.RIGHT, fill=tk.Y)
self.h_scroll.pack(side=tk.BOTTOM, fill=tk.X)
self.Text.pack(side=tk.LEFT, fill=tk.BOTH,expand=True)
return
def run(self):
self.dialog = tk.Tk()
self.dialog.geometry('{}x{}+{}+{}'.format(800,1000,0,0))
self.dialog.title(self.title)
self.CreateDialog()
self.text_insert(self.message)
self.dialog.protocol("WM_DELETE_WINDOW", self.quit)
self.dialog.after(self.after_interval,self.loop)
self.dialog.mainloop()
def setQueue(self, send, recv):
if self.sendQueue is None and self.recvQueu is None:
self.sendQueue, self.recvQueu = recv, send
return True
else:
raise Exception('すでにQueueは設定されています。')
def text_insert(self,text):
self.Text.insert(str(self.text_index),text)
self.text_index += float(len(text))
self.Text.see('end')
def loop(self):
# dialogの処理ループ
if self.sendQueue is not None and self.recvQueu is not None:
while(not self.recvQueu.empty()):
item = self.recvQueu.get()
self.text_insert(item)
if self.isLoop:
self.id_after = self.dialog.after(self.after_interval,self.loop)
def key_interrupt(self, event):
# Enterキーが押されたら
if event.keysym == 'Return':
# Textboxの最終indexを取得
end_index = float(self.Text.index('end'))
# 1行戻した行頭のindex
start_index = end_index - 1.0
# Enterを押された改行された最終行の1行前を取得
command = self.Text.get(f'{start_index}','end-1c')
if '\t' in command:
command = command.split('\t')
command = ''.join(command)
if command:
if command in self.command_list:
index = self.command_list.index(command)
self.command_list.pop(index)
self.command_list.append(command)
# Queueで返送する
if self.sendQueue is not None:
self.sendQueue.put(command)
self.tab_repeat_cnt = 0
return
def key_repeat(self, event):
# Tabキーが押されたら
if event.keysym == 'Tab':
self.isTab = True
#Keyイベントの後に文字が挿入されるので、Modify部でInertする。
pass
def key_back_repeat(self, event):
# Shift+Tabキーが押されたら
if event.keysym == 'Tab':
end_index = float(self.Text.index('end-1c'))
end_index = float(int(end_index))
self.Text.delete(f"{end_index}","end")
self.Text.insert("end",'\n')
self.tab_repeat_cnt -= 1
index = len(self.command_list) -1 - self.tab_repeat_cnt
if len(self.command_list) > index and index >= 0:
end_index = float(self.Text.index('end-1c'))
end_index = float(int(end_index))
rep_command = self.command_list[index]
self.Text.insert(f"{end_index}",rep_command)
else:
self.tab_repeat_cnt += 1
pass
def tab_release(self, event):
if self.isTab:
self.isTab = False
end_index = float(self.Text.index('end-1c'))
end_index = float(int(end_index))
self.Text.delete(f"{end_index}","end")
self.Text.insert("end",'\n')
index = len(self.command_list) -1 - self.tab_repeat_cnt
if end_index >= 1.0:
if len(self.command_list) > index and index >= 0:
rep_command = self.command_list[index]
# Textboxの最終indexを取得
self.Text.insert(f"{end_index}",rep_command)
self.tab_repeat_cnt += 1
return
def quit(self):
if self.sendQueue is not None:
# 終了したことをメインスレッドに伝達するためにQueueで通知
self.sendQueue.put('#END')
else:
self.delete()
def delete(self):
# Tcl_AsyncDelete例外対策のため、メインスレッドから
# class_DebugBufferViewのTkinterスレッドを終了させている。
self.isLoop = False
self.dialog.after_cancel(self.id_after)
self.dialog.quit()
if __name__ == '__main__':
import json
import msvcrt as mskey
sendQueue, recvQueue = Queue(),Queue()
# タイトルと最初のメッセージを付けてインスタンス作成
instView = class_DebugBufferView(message='Start...\n',title='Input View')
# 送受信するQueueをセット
instView.setQueue(send=sendQueue,recv=recvQueue)
# 試しに、JSONデータを表示させてみる
message = {
'message':'TEST Message',
}
sendQueue.put(json.dumps(message,indent=2)+'\n')
isLoop = True
while(isLoop):
time.sleep(0.03) #0.03くらいのインターバルがキー入力にはちょうど良い。
# キー入力したものを表示させる
if mskey.kbhit():
key = mskey.getwche()
sendQueue.put(key)
while(not recvQueue.empty()):
item = recvQueue.get()
print(item)
if item == '#END':
# 終了通知を受け取ったら、スレッドを破棄させる
instView.delete()
isLoop = False
print('プログラムの終了')