PythonでGUIプログラミング キー入力を受け付ける

 今日も見に来て下さって、ありがとうございます。地道ながらに更新を続けたせいか、コロナウィルスで引きこもり中の余裕のある人たちのおかげか、週間のユーザ数が三桁に達するようになってまいりました。以前書いた「Pythonでプログラミング キー入力を受け付ける」のアクセス数がなぜか多いので、調子にのってGUI版を書いてみることにしました。

出来上がりイメージ

 出来上がりイメージです。ウィンドウの中にキャンバスをつくって、その中に黄色い丸と青い丸を書きました。マウスを動かすと、丸がついて回ります。そして、矢印キーを押すと、キーを押した方向へ丸が移動します。という、単純なものです。

出来上がりソースコード

とりあえず、動かしてみたいんじゃ、という忙しい方のために、まずはソースを貼っておきます。

import tkinter as tk

class App(tk.Tk):
    def __init__(self):
        super().__init__()
        self.title("Moving in Canvas")
        self.pos = (0,0)
        self.pressed = {}
        self.canvas = tk.Canvas(width=600, height=400, background="white")
        self.canvas.pack()
        self.item = self.canvas.create_oval(10, 10, 40, 40, fill="yellow", tag="t")
        self.inner_item = self.canvas.create_oval(20, 20, 30, 30, fill="blue", tag="t")
        self.canvas.bind("<Motion>",self.move_by_mouse)
        self.bind("<KeyPress>",self.key_pressed)
        self.bind("<KeyRelease>",self.key_released)
        self.move_by_key()

    def move_by_mouse(self, event):
        if self.pos == (0,0):
            x0, y0, x1, y1 = self.canvas.coords(self.item)
            px = x0 + (x1 - x0) // 2
            py = y0 + (y1 - y0) // 2
            dx = event.x - px
            dy = event.y - py
            self.canvas.move("t", dx, dy)
            self.pos = (event.x, event.y)
            return
        dx = event.x - self.pos[0]
        dy = event.y - self.pos[1]
        self.pos = (event.x, event.y)
        self.canvas.move("t", dx, dy)
    
    def key_pressed(self, event):
        self.pressed[event.keysym] = True
        self.pos = (0,0)
    
    def key_released(self, event):
        self.pressed.pop(event.keysym, None)
    
    def move_by_key(self):
        dx, dy = 0, 0
        m = 5
        if "Up" in self.pressed:
            dy -= m
        if "Down" in self.pressed:
            dy += m
        if "Left" in self.pressed:
            dx -= m
        if "Right" in self.pressed:
            dx += m
        x0, y0, x1, y1 = self.canvas.coords(self.item)
        px = x0 + (x1 - x0) // 2 + dx
        py = y0 + (y1 - y0) // 2 + dy
        if 0 <= px <= self.canvas.winfo_width() and 0 <= py <= self.canvas.winfo_height():
            self.canvas.move("t", dx, dy)
        
        self.after(10, self.move_by_key)

if __name__ == '__main__':
    app = App()
    app.mainloop()

内容説明

 まずは、1行目、「import tkinter as tk」で、tkinterモジュールをインポートします。「as tk」と指定してあるのは、グローバルな名前空間が汚されないようにするためです。チュートリアルでありがちな、「from tkinter import *」はダメな例なのでまねしないようにしましょう。

 次に、3行目、「class App(tk.Tk):」GUIアプリケーションのトップレベルは、必ずtk.Tkになりますので、これを継承してアプリケーションを作成します。

 4、5行目「def __init__(self):」では、アプリケーションの初期化を行います。まずは、親のメソッドを呼び出すため、「super().__init__()」をコールしています。ここまでは、もうお約束ですので完全に記憶しましょう。

 6行目「self.title("Moving in Canvas")」ウィンドウのタイトルをセットしています。tkinterでは特に何も意識せず日本語も使えます。

 7、8行目は、あとで使う変数を初期化しています。

 9~12行目で、Canvasをつくって、その上に丸を書いています。ポイントは、この丸を書くときに「tag="t"」とタグをセットしているところでしょうか。あとで出てきますが、キャンバス上のモノは、IDかタグで動かしたり属性を変更したりすることができます。IDだと一つしか動かせませんので、今回は二つの丸を動かすために、タグを指定しました。

 13~15行目は、イベントにバインドしています。どういうことかというと、例えばこの「self.canvas.bind("<Motion>",self.move_by_mouse)」の場合だと、self.canvas<Motion>(マウスポインタがキャンバスで動いた!)というイベントが発生したときには「self.move_by_mouse」を呼び出してね、と、割り当てている、ということになります。日本語のニュアンスだと結び付けている、というのが適当でしょうか。
 ここでは、3つのイベントをそれぞれ割り当てています。

  • <Motion> マウスが動いた → self.move_by_mouse
  • <KeyPress> キーが押された → self.key_pressed
  • <KeyRelease> キーが離された → self.key_released

 マウスによる移動と、キー入力による移動はそれぞれ独立しています。まずはマウスによる移動の方から説明します。

マウスによる移動

 マウスが動くたびに、<Motion>イベントが発生します。14行目でバインドしたので、マウスの動きに合わせてself.move_by_mouseが呼び出されます。マウスに合わせて動作させるのは、これだけで充分です。ポイントは、self.canvas.moveで指定できるのは、指定したタグを移動するオフセット値になることです。現在位置からx軸に+10、y軸にー20といった風に設定することになります。eventで取得できるのが左上のコーナーを0,0との基準にしてプラスに増えていく座標になっています。このため、最初の動き出しの時だけは、動かずに動作開始点を保持するだけにして、次のイベントが発生したときに、前の位置から5,3動く、といった指定になるようにしました。

 ちなみに、x0, y0, x1, y1 = self.canvas.coords(self.item)は、self.itemの左上の原点からの座標を取得するメソッドです。self.itemを矩形で切り取って左上の座標と右下の座標を同時に取得しています。self.itemの中心座標を計算するのに、px = x0 + (x1 - x0) // 2py = y0 + (y1 - y0) // 2としています。その後、dx = event.x - pxdy = event.y - pyにて、中心点からマウスカーソルの座標までのそれぞれの移動距離を計算しています。その後、self.canvas.move("t", dx, dy)として、マウスカーソルまでself.itemを移動します。

キーによる移動

 キーが押されるたびに、<KeyPress>イベントが発生します。押したキーを話すたびに、<KeyRelease>イベントが発生します。イベントにはそれぞれ、key_pressedkey_releasedがバインドされていました。このため、キーが押されると、8行目で初期化されたディクショナリ「self.pressed = {}」の中に、押されたキーのシンボルがTrueとして登録されます。例えば、右キーを押すと、ディクショナリの中身は{'Right': True}という風になります。キーは同時に押すこともできますので、例えば上と右キーを同時に入力するとディクショナリの中身は{'Up': True, 'Right': True}のようになります。ソースコードを見ればわかると思いますが、キーのシンボルはevent.keysymで取得しています。

 初期化の説明の時にはさらりと飛ばしましたが、初期化(__init__(self))の最後の行、16行目で、self.move_by_key()を呼び出しています。このmove_by_keyは呼び出されると、最後にself.after(10, self.move_by_key)を呼び出すことで、自分自身を10ミリ秒後に呼び出すことで、無限ループを開始します。このループにて、キー入力を処理しています。

 具体的には、キー入力で上下左右の移動距離(ここでは5)をセットして、キャンバスのmoveメソッドを呼び出すself.canvas.move("t", dx, dy)ことでアイテムを動かしています。その直前のif文は、画面の外へはみ出して移動しないように制御しています。

まとめ

 上記のキー入力制御のアプローチは、個別にイベントをバインドするやりかたよりも好ましいと思います。個別にバインドした場合は、キー入力しない限りイベントが発生しないので、位置を飛ばして移動するような移動には使えますが、よりスムーズな移動には今回のようなイベントループで制御する必要があります。

 これでtkinterを使うときにキー入力やマウスによるイベントの制御はバッチリですね!

Tkinterのレイアウト方法は3種類

 今日も見に来てくださってありがとうございます。着々と読者が増えているようでうれしいです。がんばって書いていきます。

 ここのところtkinterで遊んでいます。tkinterではウィジェットと呼ばれる部品が用意されているのですが、それをウィンドウ内の適切な位置に配置する必要があります。tkinterではその配置する方法が、pack、grid、placeの3種類あります。全部使ってみました、という例を作ってみました。ちなみにこれらは、ジオメトリマネージャ(geometry manager)と呼ばれています。配置を管理するための仕組みですね。これらが組み合わせられるか、お試しでプログラムを作ってみました。

ソースコードは、以下の通りです。

import tkinter as tk

class App(tk.Tk):
    def __init__(self):
        super().__init__()
        self.title("geometry manager test")

        gridframe = tk.LabelFrame(self, text="グリッドのフレーム")
        gridframe.pack()
        g = GridFrame(gridframe)
        g.pack()
        
        packframe = tk.LabelFrame(self, text="パックのフレーム")
        packframe.pack()
        pa = PackFrame(packframe)
        pa.pack()

        placeframe = tk.LabelFrame(self, text="プレースのフレーム")
        placeframe.pack(ipadx=10, ipady=5, expand=True, fill=tk.BOTH)
        pl = PlaceFrame(placeframe)
        pl.pack()

        label = tk.Label(text="ラベル")
        button = tk.Button(text="ボタン",command=self.destroy)
        label.pack()
        button.pack()

class GridFrame(tk.Frame):
    def __init__(self, master=None):
        super().__init__(master)
        label = tk.Label(self,text="gridのラベル")
        label.grid(column=0,row=0,padx=10,pady=10)
        button = tk.Button(self,text="gridのボタン")
        button.grid(column=1,row=1,padx=10,pady=10)

class PackFrame(tk.Frame):
    def __init__(self, master=None):
        super().__init__(master)
        label = tk.Label(self, text="packのラベル")
        button = tk.Button(self, text="packのボタン")
        label.pack(padx=20,pady=20)
        button.pack(padx=20,pady=20)
        
class PlaceFrame(tk.Frame):
    def __init__(self, master=None):
        super().__init__(master,width=300,height=50)
        label = tk.Label(self, text="placeのラベル")
        button = tk.Button(self, text="placeのボタン")
        label.place(relwidth=0.25, relheight=0.25)
        button.place(anchor=tk.N, x=200, y=20, width=80, height=30)

if __name__ == "__main__":
    app = App()
    app.mainloop()

gridは格子状に分けた場所に部品を配置するイメージです。行と列で位置を指定することができます。また、columnspanrowspanを指定することで幅や高さを増やすことができます。packは真空パックのイメージでしょうか。部品をつくって、詰める感じです。placeは位置や大きさを直接指定するイメージですね。それぞれの詳細はまたいずれどこかでまとめようと思います。

ひとつのコンテナの中で異なる方法を混ぜて使うことはできません。エラーが発生して処理が中断してしまいます。今回のこの例のようにFrameを継承したコンテナの中に部品を配置して、それらを組み合わせる、というのがコツのようです。こうすることで、それぞれが独立して影響を及ぼさないようにつくることができます。

たったこれだけの部品を並べるのに、こんなに書かないといけないのは、けっこう面倒ですよね。でもまあ、つくって、並べて、つくって、並べて、と、これより短くするのはかなり難しい(ムリ)でしょうね。

Tkinter情報を取得するダイアログ

 今日も見に来てくださって、ありがとうございます。

先日からの続きですが、tkinterに用意されているダイアログで、情報取得用のダイアログをまとめてみました。

実行結果
askopenfilename
asksaveasfilename
askopenfilenames
askdirectory
askcolor
askfloat
askinteger
askstring

画像を張り付けるとそれだけで記事が多くなるなぁ、というのは置いといて、上記の画面を実行するためのスクリプトは以下の通りです。

import tkinter.filedialog as fd
import tkinter.colorchooser as cc
import tkinter.simpledialog as sd

class OtherDialogSample(tk.Tk):
    
    def create_dialog_button(self, group, dialog, argcount=None):
        def command():
            if argcount == 2:
                ret = dialog(title=dialog.__name__, prompt=dialog.__doc__)
            else:
                ret = dialog()
            print(ret)
        button = tk.Button(group, text=dialog.__name__, command=command)
        button.pack(padx=10,pady=10,fill=tk.BOTH)

    def __init__(self):
        super().__init__()
        self.title("Other Dialog Sample")
        self.g1 = tk.LabelFrame(self, text="ファイルダイアログのサンプル")
        self.g1.pack(padx=10, pady=10,side=tk.LEFT)
        self.create_dialog_button(self.g1, fd.askopenfilename)
        self.create_dialog_button(self.g1, fd.asksaveasfilename)
        self.create_dialog_button(self.g1, fd.askopenfilenames)
        self.create_dialog_button(self.g1, fd.askdirectory)
        self.g2 = tk.LabelFrame(self, text="その他のダイアログのサンプル")
        self.g2.pack(padx=10,pady=10,fill=tk.BOTH,side=tk.LEFT)
        self.create_dialog_button(self.g2, cc.askcolor)
        self.create_dialog_button(self.g2, sd.askfloat, 2)
        self.create_dialog_button(self.g2, sd.askinteger, 2)
        self.create_dialog_button(self.g2, sd.askstring, 2)

if __name__ == "__main__":
    ods = OtherDialogSample()
    ods.mainloop()

いろいろと説明したいことはありますが、、、とりあえずまとめます。

ダイアログタイトル戻り値キャンセル時備考
askopenfilename開くフルパス文字列空文字列指定ファイルが存在しない場合メッセージ出力、続行不可
asksaveasfilename名前を付けて保存フルパス文字列空文字列既存ファイル選択時は警告メッセージ出力
askopenfilenames開くフルパス文字列のタプル空文字列複数ファイル選択可能
askdirectoryフォルダの選択フォルダ文字列空文字列
askcolor色の設定タプル((R,G,B),色表現文字列)(None,None)
askfloat指定文字列(必須)float値Nonefloat値以外はエラー
askinteger指定文字列(必須)int値Noneint値以外はエラー
askstring指定文字列(必須)string値None

気になったのはファイルダイアログ関連の戻り値で、なぜか区切り文字がUnix風の「/(スラッシュ)」でした。ぼくの環境はWindowsなので、「\(円マーク)」を期待していたのですけど違っていました。これはおそらくTkがもともとUnix環境用として開発され始めたことが原因なんじゃないかなぁ。

askcolorの戻り値も気になりました。R,G,Bがなぜかfloat型なのですよねぇ。Tkのページでは0~65535のRed、Green、Blueの値を返す、となっているのですけど、tkinter内では受け取った値を256で割り算しているのです。取得した値を調べてみると2バイト値には同じ値が入力されていました。(例:0xFFFF、0xC0C0など)16ビットのうち、実質8ビットだけ必要なので、256で割り算して算出していたのでしょうね。これは、Python2系の処理が/で割り算した答えをint型に返していた名残なのだと思います。Python3系では、//で割り算すればint型にしてくれるのですけど、過去の経緯もあって変えられないのかな。

その他の特記事項としては、ファイルダイアログではパラメータのinitialdirで初期ディレクトリを指定可能で、今回選択したディレクトリが次回のinitialdirにセットされます。filetypeで表示するファイルタイプを指定可能です。ファイルタイプはfiletype=(("すべてのファイル","*.*"), ("テキストファイル", "*.txt *.log *.csv"))のように、表示するラベルとファイル名のマッチングパターンの二つの文字列を含むタプルのタプルで指定します。拡張子が未指定の時のために、defaultextensionで拡張子の初期値をセットすることも可能です。
simpledialogで定義されているaskfloataskintegeraskstringについては、初期値と最小値、最大値がそれぞれ、initialvalueminvaluemaxvalueで指定可能です。

まずは、用意されているダイアログをうまく使えるようになりたいですね。

Tkinterダイアログいろいろ

 今日も見に来てくださって、ありがとうございます。

今回は、tkinterで用意されているダイアログを見てみましょう。英語でダイアログとは、会話のことで、コンピューター用語としては、メッセージを出力するポップアップ画面のことを言います。ここでは、情報を出力して、ユーザーにアクションを促すダイアログを紹介します。ボタンがひとつのダイアログは3種類、ボタンがふたつのダイアログは4種類、ボタンが三つのタイプはひとつ用意されています。ダイアログを表示するためのメニュー画面を作ってみました。こんな感じです。

実行結果

それぞれボタンを押すと以下のダイアログが開きます。ダイアログのボタンを押すと、コンソールに戻り値を出力します。

showinfo
showwarning
showerror
askokcancel
askquestion
askretrycancel
askyesno
askyesnocancel

上記の画面を出力するスクリプトは、以下の通りです。

import tkinter as tk
import tkinter.messagebox as mb

class DialogSample(tk.Tk):

    def create_button(self, group, dialog):
        def command():
            ret = dialog(master=self, title=dialog.__name__, message=dialog.__doc__)
            print(ret)
        b = tk.Button(group, text=dialog.__name__, command=command)
        b.pack(padx=20, pady=10, fill=tk.BOTH)

    def __init__(self):
        super().__init__()
        self.title("Dialog Sample")
        self.g1 = tk.LabelFrame(self, text="ボタンひとつのダイアログ")
        self.g1.pack(padx=10, pady=10,side=tk.LEFT, fill=tk.BOTH)
        self.create_button(self.g1, mb.showinfo)
        self.create_button(self.g1, mb.showwarning)
        self.create_button(self.g1, mb.showerror)
        self.g2 = tk.LabelFrame(self, text="ボタンふたつのダイアログ")
        self.g2.pack(padx=10, pady=10, side=tk.LEFT, fill=tk.BOTH)
        self.create_button(self.g2, mb.askokcancel)
        self.create_button(self.g2, mb.askquestion)
        self.create_button(self.g2, mb.askretrycancel)
        self.create_button(self.g2, mb.askyesno)
        self.g3 = tk.LabelFrame(self, text="ボタンみっつのダイアログ")
        self.g3.pack(padx=10, pady=10, side=tk.RIGHT, fill=tk.BOTH)
        self.create_button(self.g3, mb.askyesnocancel)
            
if __name__ == "__main__":
    d = DialogSample()
    d.mainloop()

ダイアログを実行すると値が帰ってきます。ボタンがひとつのダイアログは、okの文字列が、ボタンがふたつのダイアログはaskquestionyes/noで、それ以外はTrue/Falseが戻されました。ボタンがみっつのダイアログはなんと、それぞれのボタンでTrue/False/Noneを戻してきました。まとめると以下の表のようになります。

ダイアログボタン1ボタン2ボタン3戻り値1戻り値2戻り値3×ボタン
showinfoOKokok
showwarningOKokok
showerrorOKokok
askokcancelOKキャンセルTrueFalseFalse
askquestionはいいいえyesno使用不可
askretrycancel再試行(R)キャンセルTrueFalseFalse
askyesnoはい(Y)いいえ(N)TrueFalse使用不可
askyesnocancelはい(Y)いいえ(N)キャンセルTrueFalseNoneNone

面白いなぁ、と、思ったのは、戻り値の違いです。TrueFalseNoneはそうかな、という気持ちになるのですけど、yesnookは微妙な気持になります。ま、okはチェックしないだろうから問題ないですけど、yesnoは、どちらか絶対答えてほしいという気持ちの表れなのかな。そして、askquestionaskyesnoは絶対混乱すると思いますよね。(笑)

【追記】

tkinterのmessagebox.pyのソースコードを見ていたら、ABORTRETRYIGNOREという未使用の変数が定義されていました。本家のTkで使えるから定義だけはしておいたのでしょうか。messagebox.pyでダイアログまで作っておいてくれればいいのにね。と、いうことで作ってみました。

abortretryignore

戻り値は、それぞれabortretryignoreとなっていました。
tkinterでダイアログは未定義なので、一覧を修正するのはどうかなぁ、と思ったので、どうやって作ったのか、ソースだけ載せておきます。

    def create_abortretryignore_button(self, group):
        def command():
            ret = mb._show(master=self,
                           title="おまけ(名無しダイアログ)",
                           message="このダイアログはtkinterでは定義されていません。",
                           icon=mb.QUESTION,
                           type=mb.ABORTRETRYIGNORE)
            print(ret)
        b = tk.Button(group, text=mb.ABORTRETRYIGNORE, command=command)
        b.pack(padx=20, pady=10, fill=tk.BOTH) 

Tkinterカーソルの種類(マウスポインターの種類)

 今日も見に来てくださって、ありがとうございます。最近コロナウイルスの話題で世間は騒がしいですが、皆さんはどうでしょうか。

 ここのところ、tkinterにハマっています。今日は、tkinterで用意されているカーソルにどんなものがあるのか、ということで調べてみました。ご存知の通りTkで定義されているものが利用されます。本家のホームーページのここに利用可能なカーソルの一覧がありました。どんなカーソルがあるか、画像を取れればよかったのですが、実際に動かせるソースの方がいいかも、ということで、残しておきます。実行すると、以下のような画面が開きます。白いラベルをポイントすると、カーソルが変わります。

実行結果画面
ポイントしたところ(カーソルがhand2に変わっている)

はい、ソースは以下の通りです。

import tkinter as tk

class CursorTest(tk.Tk):
    def __init__(self):
        super().__init__()
        self.title("Curosor demo")
        self.group1 = tk.LabelFrame(self, padx=15, pady=10, text="ラベルを選択してください。カーソルが変更されます。")
        self.group1.pack(padx=10, pady=5)
        self.windows_native = ["X_cursor","arrow","based_arrow_down","based_arrow_up",
                               "boat","bogosity","bottom_left_corner","bottom_right_corner",
                               "bottom_side","bottom_tee","box_spiral","center_ptr",
                               "circle","clock","coffee_mug","cross",
                               "cross_reverse","crosshair","diamond_cross","dot",
                               "dotbox","double_arrow","draft_large","draft_small",
                               "draped_box","exchange","fleur","gobbler",
                               "gumby","hand1","hand2","heart",
                               "ibeam","icon","iron_cross","left_ptr",
                               "left_side","left_tee","leftbutton","ll_angle",
                               "lr_angle","man","middlebutton","mouse",
                               "none","pencil","pirate","plus",
                               "question_arrow","right_ptr","right_side","right_tee",
                               "rightbutton","rtl_logo","sailboat","sb_down_arrow",
                               "sb_h_double_arrow","sb_left_arrow","sb_right_arrow","sb_up_arrow",
                               "sb_v_double_arrow","shuttle","sizing","spider",
                               "spraycan","star","target","tcross",
                               "top_left_arrow","top_left_corner","top_right_corner","top_side",
                               "top_tee","trek","ul_angle","umbrella",
                               "ur_angle","watch","xterm",]
        for i, c in enumerate(self.windows_native):
            l = tk.Label(self.group1, text=c, cursor=c, bg="white", font=(22))
            l.grid(row=i//4, column=i%4, padx=3, pady=3, sticky=tk.W+tk.E)

if __name__ == "__main__":
    cursorTest = CursorTest()
    cursorTest.mainloop()

 ぼくの使っている環境はWindow10のAnaconda3で、それで動作確認しました。ちょっと美しくないカーソルもあるので、使えるものは限定されそうですね。

 ちなみに、カーソルを指定しているのは、forループの中にある、Labelを作成するところのcursor=cという指定のみです。Tkのドキュメントにもあるように、ざっと見たところ、すべてのウィジェットでこの指定でポイントしたときのカーソルを指定できるようです。

PythonでGUI

 今日も見に来てくださって、ありがとうございます。

 Pythonを使ったGUIプログラミング、何を使ったらいいのでしょうね。ネットを調べてみると、いろいろとあって悩みます。標準搭載のGUIはTkinterなのですが、ビジネスでちょっと使ってみるGUIだと、これで充分ですね。ということで、helpに記載のあった以下の例題を実行してみます。

import tkinter
from tkinter.constants import *
tk = tkinter.Tk()
frame = tkinter.Frame(tk, relief=RIDGE, borderwidth=2)
frame.pack(fill=BOTH,expand=1)
label = tkinter.Label(frame, text="Hello, World")
label.pack(fill=X, expand=1)
button = tkinter.Button(frame,text="Exit",command=tk.destroy)
button.pack(side=BOTTOM)
tk.mainloop()

でました!
定番の、Hello, Worldです♪

 何から始めるのがいいか、ちょっと迷うところではありますが、チュートリアルとかはもう概ね経験済みなので、tkinterのソースを見てみようと思います。ぼくの環境だとインストールされているフォルダは「C:\ProgramData\Anaconda3\lib\tkinter」でした。このフォルダがモジュール名なので、最初に読み込まれる「__init__.py」をざっと見てみました。基本的には、何をやるにも「self.tk.call(…)」と、呼び出しているようです。self.tkの正体は、というと、最初に「_tkinter.create(screenName, baseName, className, interactive, wantobjects, useTk, sync, use)」のように呼び出されています。「_tkinter」は何者かわかりませんが、最初に「import _tkinter」と読み込まれていました。今使っているのがAnaconda3の環境なので、このフォルダ以下を「_tkinter」で検索してみました。すると、 libsフォルダ、DLLsフォルダ から「_tkinter.lib」「_tkinter.pyd」が検索されました。これらのライブラリを使ってます、ということでしょうね。利用者側としてはひとまずこれくらいの理解でよいでしょうか。

 ライブラリを使ってできることは、tkのライブラリがどうなっているのか知る必要がある、ということですね。ライブラリを参照しに行く前に、とりあえず、定義されているclassについて見ていくことにしましょう。っと、一行ずつコピペしましたが、多いなぁ。内部的なクラス(Internal class)、と、書いてあったのは、線を引いていきましょうか。む~、まだよくわかりませんねぇ。親子関係になっているものは、インデントを付けてみましょうか。

class EventType(str, enum.Enum):
class Event:
class Variable:
  class StringVar(Variable):
  class IntVar(Variable):
  class DoubleVar(Variable):
  class BooleanVar(Variable):
class Misc:
class CallWrapper:
class XView:
class YView:
class Wm:
  class Tk(Misc, Wm):
class Pack:
class Place:
class Grid:
  class BaseWidget(Misc):
    class Widget(BaseWidget, Pack, Place, Grid):
    class Toplevel(BaseWidget, Wm):
      class Button(Widget):
      class Canvas(Widget, XView, YView):
      class Checkbutton(Widget):
      class Entry(Widget, XView):
      class Frame(Widget):
      class Label(Widget):
      class Listbox(Widget, XView, YView):
      class Menu(Widget):
      class Menubutton(Widget):
      class Message(Widget):
      class Radiobutton(Widget):
      class Scale(Widget):
      class Scrollbar(Widget):
      class Text(Widget, XView, YView):
class _setit:
        class OptionMenu(Menubutton):
class Image:
  class PhotoImage(Image):
  class BitmapImage(Image):
      class Spinbox(Widget, XView):
      class LabelFrame(Widget):
      class PanedWindow(Widget):

 インデントを付けてみて、なんとなくわかってきました。画面の要素はWidgetを継承していて、→BaseWidget→Miscとなっているので、Miscが一番の先祖になっているのですね。他に、イベントを管理するためのEventクラスがあって、Variableを継承している何らかの値を保持するクラスがあって、Widegetの中でも見え方に特徴のあるものがXViewやYViewを多重継承して見せ方に特徴を持たせてそう。他のWmはウィンドウマネージャーかなぁ、何となくGUIの全体を管理しそうな、、、そして、Pack、Place、Gridは位置決めに使うクラス、あとは、メニュー用とイメージ用、と、ざっとこんな感じでしょうか。
 それ以外にtkinterのフォルダを見てみました。

colorchooser.py ネイティブのカラーダイアログ(Chooserクラス)
commondialog.py 共通のダイアログ関連(Dialogクラス)
constants.py コンスタント値関連
dialog.py ダイアログ関連(Dialogクラス)
dnd.py ドラッグアンドドロップ関連
filedialog.py ファイル関連のダイアログ関連
font.py フォント関連
messagebox.py メッセージボックス関連
scrolledtext.py スクロールするテキスト関連
simpledialog.py 簡単なダイアログ関連
test テスト用かな
tix.py Tkの拡張ウィジェット(3.6から非推奨。使いません。ttkを使いましょう。)
ttk.py Tk8.5から新しく追加された、テーマ付きウィジェットに関するもの
__init.py
__main.py
__pycache

 全体感は、これで何となくわかりました。あとは、個別の利用方法ですかね。

 と、いうことで、最近gitLaboというサービスを触り始めたので、そちらの勉強もかねてもろもろが分かりやすくなるように簡単なサンプル集でもつくりますかねぇ。動くものがないと、理解できないですよね、きっと。
サンプルをつくっていく中で、いろいろと書きたいことが増えてくると思います。

2021年1月10日 追記

 こちらに、クラス図を追記いたしました。概要を把握するのにどうぞ。