0
0

More than 3 years have passed since last update.

Python デバッグ用ViewをTkinterで作成(Windows用)

Last updated at Posted at 2021-03-26

目的

デバッグ中に何らかの内部データを見るために、デバッガでbreakpointを設定して、逐次止めてみたり、流れていくログテキストを追うのは効率が悪いです。
MultiProcess環境ですと、breakpointで止めてしまうと正常な再開ができなかったりします。
それに、ユーザーからの入力も受け付けてほしいとき、ターミナルのキー入力を使って行うと、ログのテキストが流れて、訳が分からなくなります。

作成物

Windows用でいたってシンプルです。

機能
・インスタンス生成すると、自動的に別スレッドで動き出します。(mainloopで返ってこないということはありません。)
・インスタンス生成時にダイアログタイトルと初期メッセージを渡して表示できます。
・テキストの最終行でEnterを押すとその前の行を切り出して、Queueで返信してくれます。
→メッセージルールを作っておけば、受信したテキストに応じてアプリケーションの挙動を制御できますね。
例えば、#command が先頭にあったら何等かの動作をするなど。
・テキストを表示させる場合、Queueでテキストを送るか、text_insert関数を使用します。
・ダイアログ終了時に、'#END'がQueueに送出されますので、受信したら親スレッドでdelete関数を呼び出してください。

機能追加の修正履歴

2021/03/29 TabおよびShift+Tabキーによるコマンド履歴入力を追加しました。同じ内容を毎回入力するのも億劫なので、Tab(遡り)、Shift+Tab(進み)で過去の入力が表示されます。全く同じ入力は過去のものが更新されて、新しい履歴に再入されます。

image.png

コード

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('プログラムの終了')


0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0