🎩

D: tkinter で コンソール widgets を作ってみた

2023/02/20に公開

はじめに

はじめまして、D です。
社会からドロップアウトして数ヶ月、現在 Pythonなどなど独学中です。

最近スクレイピングに関心があり、
DarkWeb 用のスクレイピングツールを自作してます。
(Torのインストールから起動までを自動ででき、.onion にもアクセス可)
そのうち公開するかも...

初投稿で、拙い記事ですが、応援していただけますと幸いです。

記事について

ログが確認できる widget がほしいなと思い作成しました。
その流れで、Python インタラクティブシェルもあったらおもしろいな~と思って
widget として自作しました。

ソース、使い方、ライセンス等は README.md を参照してください。
Log console が割とツール開発に重宝している。

この記事では、
Python interactive consoleコア部分の解説と
自作 utils の使い方を紹介していきたいと思います。
ツール開発に役立つかも...

本題

実行環境

OS   : Widows 10  
Python : Python 3.10.8
IDE    : PyCharm 2022.2.3 (Community Edition)

インストール

サンプルコードを実行するために、パッケージのインストールをお願いします。

pip install git+https://github.com/BlackHatD/tkinter-console.git
  • アンインストール
    pip uninstall tkinter-console -y
    


Python interpreter console

1. コマンド実行処理について

該当箇所 (pyconsole.py)

標準ライブラリである code を用いて、
Python コマンドを実行しています。

  • code ライブラリの使用例

    • (・ω・) InteractiveConsole を用いてコンソールを起動してみる

      # -*- coding:utf-8 -*-
      import code
      
      # create an instance
      shell = code.InteractiveConsole(locals())
      
      # start an interactive console
      shell.interact()
      
      
      >>> Python 3.10.8 (tags/v3.10.8:aaaf517, Oct 11 2022, 16:50:30) [MSC v.1933 64 bit (AMD64)] on win32
      Type "help", "copyright", "credits" or "license" for more information.
      (InteractiveConsole)
      
      >>> 
      
    • (・ω・) 文字列のコードを実行してみる

      # -*- coding:utf-8 -*-
      import code
      
      # define a command
      command = """
      
      for i in range(5):
          print('Hello World!!')
          
      """
      
      # create an instance
      shell = code.InteractiveConsole(locals())
      
      # compile the string command
      compiled = code.compile_command(command)
      
      # execute the compiled command
      shell.runcode(compiled)
      
      
        Hello World!!
        Hello World!!
        Hello World!!
        Hello World!!
        Hello World!!
      

      コマンドが実行できた!! Σ(゚Д゚)スゲェ!!

    上記の例では、標準出力として出力されるため、
    tk.Text widget に insert するには
    標準出力を変数へ保持する必要が出てくる (´・ω・`)

    そこで、utilsstd_forker を作成した!!


  • std_forker の概要

    1. sys.stdin, sys.stdout, sys.stderr を最初にバックアップ
    2. io.TextIOWrapper インスタンスを作成し、上書き
    3. デコレートされている関数を実行
    4. io.TextIOWrapper オブジェクトの read メソッドで情報を取得し、
      変数へ格納
    5. std をリストアする
    6. callback 関数の第 1引数に、格納した値を渡し、実行


    • std_forker の使用例


      • (・ω・) help の出力結果を fork してみる

        # -*- coding:utf-8 -*-
        from tkinter_console.utils import std_forker
        
        # define a callback function
        # an argument is required!!
        def callback(result):
            print(type(result))
            ##print(result)
        
            # if use the 'get' method, use a std_forker's class parameter!!
            # like the below code
            print(result.get(std_forker.stdout).split())
        
            
        # define a decorated running function
        @std_forker(callback=callback)
        def run():
            help("keywords")
        
        # run    
        run()    
        
          <class 'dict'>
          ['Here', 'is', 'a', 'list', 'of', 'the', 'Python', 'keywords.', 'Enter', 'any', 'keyword', 'to', 'get', 'more', 'help.', 'False', 'class', 'from', 'or', 'None', 'continue', 'global', 'pass', 'True', 'def', 'if', 'raise', 'and', 'del', 'import', 'return', 'as', 'elif', 'in', 'try', 'assert', 'else', 'is', 'while', 'async', 'except', 'lambda', 'with', 'await', 'finally', 'nonlocal', 'yield', 'break', 'for', 'not']
        

        変数として保持できた!!


コマンド実行処理と widget 書き込みの流れ

該当箇所 (pyconsole.py)

  1. callback を設定した std_forker を、実行する関数にデコレート
  2. std_foker__call___ メソッドを実行
    1. Text widget に記載された文字コードを コンパイル
    2. コンパイルされたコードを実行
    3. 標準出力等の情報を callback 関数の引数に設定し、実行
  3. callback 関数 内で別の変数に保存しているため、
    その変数を用いて Text widget に書き込む


この utils のみを利用した 簡易的 一行実行GUI も作ってみた。 (・ω・)


2. ハイライトについて

Python interpreter console では、  
Enter Key が押されるまで常に入力チェックをしています。
該当箇所 (pyconsole.py)

index を指定して常時ハイライトしており複雑なため、
ここでは、utils の使い方のみを解説いたします。


  • Highlight class の使用例

    • 最低限必要な流れ
      1. 必要なパッケージを import
        # -*- coding:utf-8 -*-
        import tkinter as tk
        from tkinter.scrolledtext import ScrolledText
        
        from tkinter_console.utils import Highlight
        
      2. tk.Tk インスタンスを作成
        root = tk.Tk()
        
      3. tk.Text widget のインスタンスを作成 (この例では、ScrolledTextを利用)
        text = ScrolledText(root)
        text.pack(fill=tk.BOTH, expand=True)
        
      4. Highlight インスタンスを作成し、attach する
        highlighter = Highlight()
        highlighter.attach(text)
        
      5. 常時監視する関数を作成する
        def run_highlighter():
            # inner function
            def wrapper():
        
                highlighter.do_normal_highlighting()
                highlighter.do_regex_highlighting()
        
                root.after(100, wrapper)
            root.after(100, wrapper)
        
      6. 監視する関数を最初に実行し、mainloop させる
        run_highlighter()
        root.mainloop()
        


    • ハイライトの設定

      • do_normal_highlighting を利用する場合
        1. keyword dict を作成する
          例:while が入力されたとき、 fg を yellow にするように定義
            (もちろん複数定義OK)
          key_dict = {'while': {'foreground': 'yellow'}}
          
        2. 定義を登録する
          highlighter.register_normal_tags(key_dict)
          


      • do_regex_highlighting を利用する場合
        1. 定義を import する

          from tkinter_console.utils import PATTERN
          
        2. keyword dict を作成する
          例:" " で囲まれたとき、 fg を green にするように定義
            (もちろん複数定義OK)

          key_dict = {'double_quotation': {
              PATTERN: r'".*?"',
              'foreground': 'green'
          }}
          
        3. 定義を登録する

          highlighter.register_regex_tags(key_dict)
          


    • Sample
      Let's try it!! (´・ω・`)
      # -*- coding:utf-8 -*-
      import keyword
      import builtins
      import tkinter as tk
      from tkinter.scrolledtext import ScrolledText
      
      from tkinter_console.utils import PATTERN, Highlight
      
      if __name__ == '__main__':
      
          # define a normal highlight key dict
          key_normal_dict = {}
          # use the keyword.kwlist (builtin lib)
          for key in keyword.kwlist:
              key_normal_dict[key] = {
                  'foreground': 'orange'
              }
          # use the builtins (builtin lib)
          key_normal_dict.update({k: {'foreground': 'royalblue'} for k in dir(builtins)})
      
      
          # define a regex highlight key dict
          key_regex_dict = {
              'double_quotation': {
                  PATTERN: r'".*?"',
                  'foreground': 'forestgreen'
              },
              'single_quotation': {
                  PATTERN: r"'.*?'",
                  'foreground': 'steelblue'
              },
              'comment': {
                  PATTERN: r"#.*",
                  'foreground': 'green'
              }
          }
      
          # create an instance
          root = tk.Tk()
      
          # create a text widget
          text = ScrolledText(root)
          text.configure(background='black', foreground='white',
                      insertbackground="silver")
          text.pack(fill=tk.BOTH, expand=True)
          text.focus()
      
          # create a highlight instance and attach the text widget
          highlighter = Highlight()
          highlighter.attach(text)
      
          # register the key dicts
          highlighter.register_normal_tags(key_normal_dict)
          highlighter.register_regex_tags(key_regex_dict)
      
      
          # create a function
          def run_highlighter():
              # inner function
              def wrapper():
                  highlighter.do_normal_highlighting()
                  highlighter.do_regex_highlighting()
                  root.after(100, wrapper)
      
              root.after(100, wrapper)
      
      
          # read this file
          this_file = __file__
          with open(this_file, mode='rt', encoding='utf-8') as fd:
              data = fd.read()
          text.insert(tk.INSERT, data)
      
      
          run_highlighter()
          root.mainloop()
      
      
      

    以上です。


終わりに

ツール開発している人の助けになれたらなと思ってます(・ω・)
早く人間になりたい...

Discussion