Python+Kivyサンプルコード

KivyとTkinterを併用する。Tkinterで入力したテキストをKivyで表示するサンプルコード

TkinterのtextをKivyで受け取るサンプルアプリ
この記事は約36分で読めます。

PythonのGUIライブラリのKivyとTkinterを併用するサンプルコードを紹介します。Tkinterで入力した文字をKivyで受け取って表示します。別々のライブラリの間でデータを共有方法や2つのウィンドウを制御する方法を学習できます。

Kivyで入力変換パネルがないので代案

何でこんなサンプルが必要なのかと疑問に思う方もいるかもしれませんが、執筆時点のKivy2.3.0では日本語入力にまだ問題があります。入力もできるし変換自体も行えますが変換パネルが表示されません。個人的には変換パネルは有っても無くても問題ないような気がしますが、何か代替案はないかということで、どうしても変換パネルが欲しいよって方向けです。もはや苦肉の策・・・改良すれば他の用途にも使えると思います。

Windowsの変換パネル

Tkinterで入力したテキストをKivyで表示するサンプルコード

アプリを起動するとTkinterのTextウィジェット入力ウィンドウと、入力された文字を取得し表示するKivyのウィンドウが表示されます。入力されたテキストは送信ボタンを介する事無くリアルタイムでKivyウィンドウに表示されます。

TkinterのtextをKivyで受け取るサンプルアプリ
Tkinterで入力された文字をKivyアプリに表示するアプリ

サンプルコード

このサンプルではpythonコード96行のコードの中で以下のことが学べます。ThreadとQueueはPythonの機能でClockはKivyの機能になります。

  1. Thread、デーモンスレッドの実装例
  2. threading.Eventで複数のウィンドウ状態を検知する実装例
  3. Queueを使用したデータ共有の実装例
  4. Clockでデータを更新する実装例
  5. 関数のネスト実装例
  6. バックグラウンドにあるウィンドウを最前面にする実装例
  7. アクティブ化されたウィンドウのTextウィジェットに強制的にフォーカスをあてる実装例
  8. Threadで起動されたメインウィンドウとサブウィンドウを同時に閉じる実装例

以下にサンプルコードとこれらの実装の解説をしています。

combinationkivytk.kv

<RootWidget>:
    orientation: 'vertical'
    Label:
        id: label
        text: ""
        size_hint: 1, 0.8
        height: self.texture_size[1]  
        text_size: self.width, None
        halign: "center"
        valign: "middle"
        font_size: min(sp(25), self.width * 0.07) 

combination_kivy_tk.py

import threading
import queue
from tkinter import Text, Tk
import tkinter.font as tkFont
from kivy.app import App
from kivy.uix.boxlayout import BoxLayout
from kivy.clock import Clock


class RootWidget(BoxLayout):  
    pass

class CombinationKivyTk(App):
    def __init__(self, text_queue, close_event, **kwargs):
        super().__init__(**kwargs)
        self.text_queue = text_queue
        self.close_event = close_event  # Kivyが閉じられたら通知するイベント

    def build(self):
        Clock.schedule_interval(self.check_for_updates, 0.1)  # 0.1秒ごとにチェック
        return RootWidget()

    def check_for_updates(self, dt):
        """Tkinterスレッドからのデータをチェックして更新"""
        try:
            new_text = self.text_queue.get_nowait()  # 新しいテキストを取得
            self.root.ids.label.text = new_text  # ラベルを更新
        except queue.Empty:
            pass

    def on_stop(self):
        """Kivyウィンドウが閉じられたときに呼び出される"""
        self.close_event.set()  # フラグを立てる

def start_kivy_app(text_queue, close_event):
    """Kivyアプリケーションを起動"""
    CombinationKivyTk(text_queue=text_queue, close_event=close_event).run()

def start_tkinter_app(text_queue, close_event):
    """Tkinterのアプリケーションを開始"""
    root = Tk()
    root.title("Tkinter入力")
    root.geometry("600x400+400+100")  # ウィンドウのサイズと位置を指定

    # フォント設定
    font = tkFont.Font(family="Helvetica", size=20)

    # テキストウィジェット
    text_widget = Text(root, width=40, height=10, font=font)
    text_widget.pack(pady=20)

    def check_text():
        """テキストの変更を検知してキューに送信"""
        current_text = text_widget.get("1.0", "end-1c")
        text_queue.put(current_text)
        if close_event.is_set():  # Kivyウィンドウが閉じられたらTkinterも閉じる
            root.destroy()
            return
        root.after(100, check_text)  # 0.1秒ごとにチェック

    def on_close():
        """Tkinterウィンドウが閉じられたときの処理"""
        close_event.set()  # フラグを立てる
        root.destroy()

    def ensure_focus():
        """テキストウィジェットに確実にフォーカスを設定"""
        if text_widget.focus_get() != text_widget:
            text_widget.focus_force()
        root.after(100, ensure_focus)  # 0.1秒ごとに再帰的にチェック

    # ウィンドウを閉じるイベントにバインド
    root.protocol("WM_DELETE_WINDOW", on_close)

    # 監視開始
    check_text()

    # Tkinterウィンドウを最前面に移動してフォーカスを設定
    root.lift()  # Tkinterウィンドウを最前面に移動
    root.attributes('-topmost', True)  # 最前面に固定
    root.after(100, lambda: root.attributes('-topmost', False))  # 解除
    root.after(200, ensure_focus)  # 少し遅れてフォーカスを設定

    # Tkinterのメインループ
    root.mainloop()

if __name__ == "__main__":
    # TkinterとKivyで共有するキューを作成
    text_queue = queue.Queue()
    close_event = threading.Event()  # 終了を通知するフラグ

    # 別スレッドでKivyアプリケーションを起動
    threading.Thread(target=start_kivy_app, args=(text_queue, close_event), daemon=True).start()

    # Tkinterアプリケーションを起動
    start_tkinter_app(text_queue, close_event)

コード解説

このアプリでは3つの機能を使用しています。

  1. Thread(Python)
  2. Queue(Python)
  3. Clockスケジュール(Kivy)

スレッドの概念

スレッドはPythonの標準機能でプログラム内で同じメモリ空間を共有しながら並行処理を行います。通常コードは上から順番に処理が行われているので同時に処理することはできません。スレッドを使用することで同時に処理を進行させることができます。

スレッドと非スレッドの比較

キューの概念

QueueはPythonの標準機能でスレッドセーフなデータ構造になります。スレッドセーフは複数のスレッド間で安全にデータをやり取りするための仕組みです。

キューには3つの種類があります。

  1. FIFO Queue:データを入れた順番で取り出す、先入れ先出しの方式。こちらが標準のキューになります。
  2. LIFO Queue:後入れ先出しのスタック方式。
  3. Priority Queue:データに優先度を付けて処理をします。

Clockの概念

ClockはKivyのイベントループで非同期処理を管理するための仕組みです。Clockを使うことで特定の関数を一定間隔で実行したり、指定した時間後に一度だけ実行することができるなど、時間で制御したい処理がある場合に便利です。

アプリの構造

このコードは下記の構成になっています

# -----Kivyの処理-----
RootWidget(BoxLayout)
 ∟Label
CombinationKivyTk(App)
 ∟check_for_updates() 入力されたテキストを受け取り更新する関数
 ∟on_stop()      Kivyのウィンドウを閉じるための関数
start_kivy_app()      Threadに登録するKivyアプリ用の関数

# -----Tkinterの処理-----
start_tkinter_app()     Tkinterのウィンドウを構成する関数
 ∟check_text()         入力されたテキストの監視を行い更新する関数
 ∟on_close()      Tkinterのウィンドウを閉じるための関数
 ∟ensure_focus()    TkinterのTextウィジェットにフォーカスをあてるための関数

メインスレッドはTkinterのウィンドウになります。並列スレッドにKivyのLabelがTkinterのテキストを表示します。TkinterのTextウィジェットから受け取った値はキューを介してKivyに渡されています。このアプリではTextウィジェットで入力された値をボタンイベントで処理していません。Clockがテキスト入力の監視を行い0.1秒ごとに更新することで自動入力が可能になっています。

また、双方のウィンドウの状態をチェックし、どちらかのウィンドウが閉じられたら残ったウィンドウも閉じる仕組みになっています。

スレッド間のデータの共有とウィンドウ制御イメージ図

Tkinterのウィンドウの作成

これがメインスレッドになります。

from tkinter import Text, Tk
import tkinter.font as tkFont

TkinterのTextウィジェットやフォント設定のためのライブラリをインポートしています。

def start_tkinter_app(text_queue):

この関数がTkinterのウィンドウを制御し、ウィジェットの構成を定義しています。引数text_queueは後述します。

    root = Tk()
    root.title("Tkinter入力")
    root.geometry("600x400+400+100")  # ウィンドウのサイズと位置を指定

    # フォント設定
    font = tkFont.Font(family="Helvetica", size=20)

    # テキストウィジェット
    text_widget = Text(root, width=40, height=10, font=font)
    text_widget.pack(pady=20)

ウィンドウの設定、フォントの設定、Textウィジェットの設定を定義しています。

def check_text():

check_text()はstart_tkinter_app()のネスト関数になります。Pythonでは関数を入れ子にすることができます。

ネスト関数を使用する利点

これは下記のようにネスト関数にしなくても実装できます。

#通常の関数で定義した場合の例(抜粋)

def check_text(text_widget, text_queue, root):
    current_text = text_widget.get("1.0", "end-1c")
    text_queue.put(current_text)
    root.after(100, lambda: check_text(text_widget, text_queue, root))

def start_tkinter_app(text_queue):
    root = Tk()
    text_widget = Text(root, width=40, height=10)
    text_widget.pack()
    check_text(text_widget, text_queue, root)
    root.mainloop()

このように引数のでの値の受け渡しが複雑になってしまいます。他にもネスト関数にする利点があります。

    def check_text():
        """テキストの変更を検知してキューに送信"""
        current_text = text_widget.get("1.0", "end-1c")
        text_queue.put(current_text)
        if close_event.is_set():  # Kivyウィンドウが閉じられたらTkinterも閉じる
            root.destroy()
            return
        root.after(100, check_text)  # 0.1秒ごとにチェック
➀スコープ管理が簡単になる

ネスト関数は外側の関数のスコープ内で定義されるため、外側の関数の変数や状態に直接アクセスできます。check_text()は、外側のstart_tkinter_app()にあるtext_queueやtext_widgetに直接アクセスできるのが利点です。スコープ外の変数をグローバル宣言したり、関数間でデータを明示的に渡したりする必要がないためコードが簡潔になります。

➁コードが読みやすくなる

ネスト関数を使うとその関数がどの部分で使われるのか明確になり、コードの読みやすさが向上します。check_text()はstart_tkinter_app()の一部として定義されているため、他の場所で呼び出される心配がありません。独立した関数として定義すると、コードが分散してどこで利用されるのかを確認する手間が増えるのを防止することができます。

➂関数のスコープが限定されるので安全性が向上する

ネスト関数は外側の関数のローカルな関数になるため外部から直接アクセスできなくなります。check_text()はstart_tkinter_app() 内でしか使用されないので、他の関数からの不必要な呼び出しを防ぎ、コードの安全性と保守性を向上させます。

④必要な引数を明示的に渡す手間が省ける

外部に関数を定義すると、その関数に必要なデータを引数として毎回渡す必要がありますが、ネスト関数はその必要がなくなるので手間が省けてコードもスッキリします。

⑤関連するコードを1か所にまとめることで保守性が向上する

関連する処理を1つの関数内にまとめることでコードを管理しやすくなります。check_text()はstart_tkinter_app()の一部の動作を担う補助関数です。そのため、同じ関数内に置くことで構造を明確にできます。

Kivyウィンドウの作成

こちらは並列処理させているThreadになります。KivyウィンドウではBoxLayoutにLabelのみを構成しています。

# Pythonファイルから抜粋
from kivy.app import App
from kivy.uix.boxlayout import BoxLayout

class RootWidget(BoxLayout):  
    pass

class CombinationKivyTk(App):
    def build(self):
        return RootWidget()
# kvファイル
<RootWidget>:
    orientation: 'vertical'
    Label:
        id: label
        text: ""
        size_hint: 1, 0.8
        height: self.texture_size[1]  
        text_size: self.width, None
        halign: "center"
        valign: "middle"
        font_size: min(sp(25), self.width * 0.07) 

ウィンドウサイズの変更があった場合にtextサイズを自動調整するように定義しています。テキストのレスポンシブ対応については下記の記事をご覧ください。

何故スレッドを使うのか

TkinterとKivyはそれぞれ独自のイベントループを持つため、1つのスレッド内で両方を実行すると競合が発生します。そのためTkinterとKivyを独立したスレッドで並列に実行し、両方のUIが同時に動作できるようにしています。

メインスレッドの呼び出し

コードが実行されると最初に呼び出されるのはTkinterのウィンドウになります。

# Tkinterのメインループ
    root.mainloop()

if __name__ == "__main__":
    # Tkinterアプリケーションを起動
    start_tkinter_app(text_queue, close_event)

コード実行時にstart_tkinter_app()を呼び出すようにしています。またTkinterのmainloopを呼び出し画面を構成します。これの引数については後述します。

スレッドの使い方

スレッドを作成するには下記の順番で行います。

  1. threading.Threadで新しいスレッドを作成する
  2. startメソッドでスレッドを開始する

threading.Threadの引数は下記のようになります。(python公式より引用)

threading.Thread(group=None, target=None, name=None, args=(), kwargs={}, *, daemon=None)
  • group:groupはNoneにします。将来ThreadGroupクラスが実装されたときの拡張のために予約されています。
  • target:run()で呼び出される関数を指定します。デフォルトでは何も呼び出さないことを示すNoneになっています。
  • nameスレッド名を指定します。デフォルトでは、”Thread-N”(Nは小さな10進数)、またはtarget引数が指定された場合”Thread-N (target)”(“target”はtarget.__name__)という形式でユニークな名前が構成される。
  • args:targetを呼び出すときの引数のリストかタプルです。デフォルトは () です。
  • kwarg:targetを呼び出すときのキーワード引数の辞書です。デフォルトは {} です。
  • daemon:Noneでない場合、daemonはスレッドがデーモンかどうかを明示的に設定します。Noneの場合 (デフォルト)、デーモン属性は現在のスレッドから継承されます。

次にstartメソッドでスレッドを開始します。

threading.Thread().start()

デーモンスレッドとは
アプリケーション内で自立したスレッドで他のスレッドの動作をサポートするためのバックグラウンドスレッドとして機能するスレッドです。Pythonのスレッドの仕組みでは、メインスレッドが終了するとデーモンスレッドも自動的に終了するため、プログラムの終了時に不要なスレッドが動き続けることを防ぎます。またデーモンスレッドはプログラム全体が終了する際に強制的に停止されるという特徴があります。

デフォルトではデーモンスレッドではない通常のスレッドとして動作します。メインスレッドが終了しても独立して動き続け、すべてのスレッドが終了しない限りプログラムは終了しません。

並列スレッドの作成とKivyウィンドウの呼び出し

並列に処理するのはKivyのウィンドウです。

    # 別スレッドでKivyアプリケーションを起動
    threading.Thread(target=start_kivy_app, args=(text_queue, close_event), daemon=True).start()

今回は引数に下記を指定しています。

  • target:スレッドで実行する関数としてstart_kivy_app()を指定
  • args:targetに渡す引数としてtext_queueを指定(queueのデータを渡しています)
  • daemon:Trueを指定しデーモンスレッドとして実行。メインスレッドが終了すると、このスレッドも終了します。

引数argsのclose_eventについては後述します。

def start_kivy_app(text_queue, close_event):
    """Kivyアプリケーションを起動"""
    CombinationKivyTk(text_queue=text_queue, close_event=close_event).run()

スレッドがスタートすると呼び出される関数になります。内容としては通常のkivyアプリでrun()を呼び出しているのと同じでAppクラスを継承したサブクラスを呼び出しています。ただ、TkinterとKivy間のデータを共有するために引数にキューのデータとclose_eventのデータを指定しています。

# いつもの謎の呪文と同じ
if __name__ == "__main__":
    MyApp().run()

キューでデータを共有

キューを使用するにはキューを生成し変数に代入するだけです。今回は標準の先入先出し方式のFIFO Queueを使用しています。

if __name__ == "__main__":
    text_queue = queue.Queue()

この場所はPublic領域になるので、text_queueをKivyやTkinterに渡すためプログラムが起動する直後に宣言しています。

次にtext_queueを共有するためにKivyやTkinterに引数で渡しています。

Kivy側にtext_queueを渡す。下記のコードは呼び出される順番で並び替えています。コンストラクタ内でtext_queueを渡しているのはKivy側でClockスケージュールで管理するために使います。

# 別スレッドでKivyアプリケーションを起動
threading.Thread(target=start_kivy_app, args=(text_queue, close_event), daemon=True).start()

def start_kivy_app(text_queue, close_event):
    """Kivyアプリケーションを起動"""
    CombinationKivyTk(text_queue=text_queue, close_event=close_event).run()

class CombinationKivyTk(App):
    def __init__(self, text_queue, close_event, **kwargs):
        super().__init__(**kwargs)
        self.text_queue = text_queue

Tkinterにtext_queueを渡す。

# Tkinterアプリケーションを起動
start_tkinter_app(text_queue, close_event)

def start_tkinter_app(text_queue, close_event):

入力データをキューに紐づけ

check_text()ではTkinterで入力されたテキストをキューに紐づけて更新しています。

    def check_text():
        """テキストの変更を検知してキューに送信"""
        current_text = text_widget.get("1.0", "end-1c")
        text_queue.put(current_text)
        root.after(100, check_text)  # 0.1秒ごとにチェック

    # 監視開始
    check_text()
current_text = text_widget.get("1.0", "end-1c")

getメソッドで指定した範囲のテキストを取得し引数に開始位置と終了位置を指定します。”1.0″は最初の行の最初の文字位置(行1、列0)を指しています。”end-1c”はテキストの最後から1文字前の位置を指します。(”end”はテキストの末尾、-1cは1文字前)

text_queue.put(current_text)

text_widget.getで取得したテキストを、キューに追加します。このキューを使用して、TkinterとKivyでデータを共有します。

root.after(100, check_text)

Tkinterのrootウィンドウのafterメソッドを使って、指定した時間(ミリ秒単位)後に指定した関数を呼び出しています。afterメソッドはKivyのClockと同様の関数で、指定した時間に指定したメソッドを呼び出します。

ここでは、0.1秒後に再度check_text関数を呼び出しています。0.1秒ごとにcheck_text関数が実行され、テキストの変更を定期的に更新することでリアルタイムでテキストの変更を検出してキューに追加しています。

Clockで更新

Kivy側ではClock.schedule_intervalで更新をチェックしています。

schedule_interval()は指定した関数を一定の間隔で繰り返し実行させるメソッドになります。引数には下記を指定します。

Clock.schedule_interval(callback, interval)
  • callback: 実行したい関数。関数の引数としてdt (経過時間: delta time) が渡されます。
  • interval: 関数を実行する間隔 (秒単位)。例えば0.1なら0.1秒ごとに実行。
Clock.schedule_interval(self.check_for_updates, 0.1)

このコードでは実行する関数にself.check_for_updates()を指定し、0.1秒毎にこの関数を実行しています。

check_for_updates()ではキューのデータを取得し、ラベルに表示しています。

    def check_for_updates(self, dt):
        """Tkinterスレッドからのデータをチェックして更新"""
        try:
            new_text = self.text_queue.get_nowait()  # 新しいテキストを取得
            self.root.ids.label.text = new_text  # ラベルを更新
        except queue.Empty:
            pass
def check_for_updates(self, dt):

Clockのコールバック関数からdt(経過時間: delta time) が渡されているので引数にdtを指定します。今回はこれを使用していませんが、処理開始からの経過時間を調べて、意図した頻度で実行されているかを確認したりする場合に便利です。ただし正確な時間は求められません。

new_text = self.text_queue.get_nowait()

ここでは例外が発生させているのでtryにしています。Clock.schedule_intervalはbuildメソッドの中で定義しているので、アプリが起動された時から常に監視を行い、0.1秒ごとにこの関数を呼び出しています。つまり、キューが空の状態でも常に取得しています。

get_nowait()メソッドはキューから入力データを取得する際にブロックさせないように、データが取得できない場合は例外を発生させるメソッドです。これで例外を発生させてexceptでqueue.Emptyをキャッチし空の場合は何もしないように処理を分岐しています。

threading.Eventでウィンドウ状態を検知する

Kivyのウィンドウを先に閉じるとTkinterのウィンドウが残ったままになってしまうので、どちらかが閉じられたら残った方のウィンドウも閉じるように処理しています。

threading.Eventのフラグを利用して、KivyまたはTkinterのウィンドウが閉じられたことを通知するようにしています。この実装にはTkinterのprotocolやKivyのon_stopイベントを使用して、ウィンドウの終了時に残ったウィンドウの終了処理を呼び出しています。

close_event = threading.Event() 

threading.Eventはスレッド間での通信や制御を行うためのクラスです。このEventは内部にTrueまたはFalseのフラグを持っていてデフォルトの状態はFalseになります。フラグの状態は下記のメソッドで操作します。

  • set():フラグをTrueにし、イベントを発生させます。
  • wait():待機しているスレッドはブロックが解除され、処理を再開します。
  • clear():フラグをFalseにし、イベントを待機させます。このメソッドを使うとwait()は再びブロックされます。
  • is_set():現在のフラグの状態を取得してTrueかFlaseを返します。
  • wait(timeout=None):イベントが発生するまで待機し、フラグがTrueになると待機が解除されます。オプションでtimeout(秒)を指定可能。指定時間が経過してもフラグがFalseの場合はFalse を返します。

このコードではTkinterとKivyの両方でウィンドウの閉じるイベントが発生したらset()メソッドを使用しフラグをTrueにして、Trueを検知したら画面を閉じるようにしています。

Tkinterのon_closeイベント

Tkinterのprotocol()メソッドに、on_closeイベントをバインドすることでウィンドウが閉じられたかを検知します。

object.protocol(name=None, func=None)

protocol()メソッドはTkinterウィンドウのウィンドウマネージャーを処理するためのメソッドです。引数には下記を指定します。

name:プロトコル名
func:関数名

protocolメソッドを使用することで、特定のイベント(プロトコル)が発生したときのコールバック関数を設定できます。”WM_DELETE_WINDOW”は、ウィンドウの閉じるボタンに関連するプロトコル名です。on_closeイベントはウィンドウが閉じられるときに実行されるコールバック関数になります。

root.protocol("WM_DELETE_WINDOW", on_close)

このコードではイベントに”WM_DELETE_WINDOW”を指定し、コールバック関数にon_closeを指定しています。

def start_tkinter_app(text_queue, close_event):

Tkinterに引数でclose_eventを渡しています。

    def on_close():
        """Tkinterウィンドウが閉じられたときの処理"""
        close_event.set()  # フラグを立てる
        root.destroy()

on_close()が呼ばれるとclose_event.set()でthreading.EventのフラグをTrueにして、destroy()で画面が閉じられます。ここでset()でフラグをTrueにすることでthreading.Eventを通して、Kivy側ではTkinterのウィンドウが閉じられたことが検知されます。

    def check_text():
        if close_event.is_set():  # Kivyウィンドウが閉じられたらTkinterも閉じる
            root.destroy()

check_text()ではKivyウインドウが先に閉じられた場合の処理を記述しています。is_set()でthreading.Eventのフラグの状態をチェックしTrueだった場合にdestroy()で画面を閉じます。

Kivyのon_stopイベント

Tkinterのウィンドウが先に閉じられた場合、Kivy側ではデーモンスレッドにしているためこちらも同時に終了します。Kivyではアプリが終了するとon_stop関数が呼ばれるようになっているため、ここでフラグをTrueにします。Kivyウィンドウが先に閉じられた場合Trueにすることで、check_text()のis_set()がこれを検知しTkinterのウィンドウが閉じられます。

def start_kivy_app(text_queue, close_event):
    """Kivyアプリケーションを起動"""
    CombinationKivyTk(text_queue=text_queue, close_event=close_event).run()

Kivyに引数でclose_eventを渡しています。

    def __init__(self, text_queue, close_event, **kwargs):
        super().__init__(**kwargs)
        self.close_event = close_event  # Kivyが閉じられたら通知するイベント

コンストラクタでclose_eventを初期化しています。

    def on_stop(self):
        """Kivyウィンドウが閉じられたときに呼び出される"""
        self.close_event.set()  # フラグを立てる

ここでset()でフラグをTrueにしています。

Tkinterのウィンドウを最前面に持ってくる

この処理をしない場合、Tkinterのウィンドウが最初に起動され、後からKivyのウィンドウが起動されます。そのため、Kivyのウィンドウがアクティブになり(フォアグラウンド)、Tkinterのウィンドウはバックグラウンドになります。

ユーザーがクリックすることなくすぐに入力できるようにTkinterのウィンドウを最前面に持ってくる処理をしています。

    # Tkinterウィンドウを最前面に移動してフォーカスを設定
    root.lift()  # Tkinterウィンドウを最前面に移動
    root.attributes('-topmost', True)  # 最前面に固定
    root.after(100, lambda: root.attributes('-topmost', False))  # 解除

ウィンドウを最前面にするには下記の手順で行います。

  1. lift()でウィンドウを最前面に移動。
  2. attributes()でウィンドウを最前面に固定。
  3. after()で100ミリ秒後に固定を解除。
root.lift() 

lift()はTkinterウィンドウを他のすべてのウィンドウの前に移動させるメソッドです。ウィンドウのスタック順を変更して、ウィンドウを最前面に表示します。

root.attributes('-topmost', True)

attributesメソッドは、ウィンドウを設定または取得するために使用されます。’-topmost’属性をTrueに設定することで、このウィンドウを常に他のすべてのウィンドウの上に表示してそれを固定化します。

attributes(attribute, value)
  • -alpha:ウィンドウの透過度を設定する。0.0~1.0で指定。
  • -disabled:ウィンドウを有効にするかを設定する。TrueまたはFalseで指定。
  • -fullscreen:ウィンドウを全画面に設定する。TrueまたはFalseで指定。
  • -toolwindow:タイトルバーを持つウィンドウで表示するかを設定する。TrueまたはFalseで指定。
  • -topmost:ウィンドウを最前面に表示するかを設定(固定する)。TrueまたはFalseで指定。
  • -transparentcolor:ウィンドウの色を透明に設定する。透明にする色を指定。
root.after(100, lambda: root.attributes('-topmost', False))

topmostで最前面に持ってきたウィンドウはそのまま固定されてしまいます。これを固定したままにしておくと他のアプリケーションのウィンドウ操作に影響を与えてしまいます。そのため一時的に最前面に固定した後、すぐに元の状態に戻すことで、他のウィンドウの妨げにならないようしています。

after()で0.1秒後に再度attributes()を呼び出してウィンドウの固定を解除しています。

TkinterのTextウィジェットに強制的にフォーカスをあてる

最前面に持っていきたTkinterのウィンドウのTextウィジェットはカーソルがあたってないのでこれにフォーカスをあてるようにしています。通常、TkinterのTextウィジェットにフォーカスをあてるにはfocus_set()を使いますがこれは機能しませんでした。そこで強制的にフォーカスをあてるensure_focus関数を作成し呼び出しています。

root.after(200, ensure_focus)  # 少し遅れてフォーカスを設定

ウィンドウを最前面に持ってきて固定化解除した後の0.2秒後にensure_focus()を呼び出しています。

    def ensure_focus():
        """テキストウィジェットに確実にフォーカスを設定"""
        if text_widget.focus_get() != text_widget:
            text_widget.focus_force()

focus_get()でフォーカスを持っているウィジェットを取得し、Textウィジェットでなければフォーカスをあてる処理をしています。

focus_get()
Tkinterのウィジェットメソッドで、現在フォーカスを持っているウィジェットを返します。

focus_force()
このメソッドは、特定のウィジェットに強制的にフォーカスを設定します。通常のfocus_set()メソッドとは違い、focus_force()はウィジェットが無効になっていても強制的にフォーカスを設定します

Tkinter側の処理とKivy側の処理

最後にTkinter側の処理とKivy側の処理を別々に見やすくしたものを載せておきます。これは動作しないので実行しないでください。実は両者で同じことを実装し、キューのデータとclose_eventフラグを共有しています。

Tkinter側の処理

import queue
from tkinter import Text, Tk
import tkinter.font as tkFont

class CombinationKivyTk(App):

def start_tkinter_app(text_queue, close_event):
    """Tkinterのアプリケーションを開始"""
    root = Tk()
    root.title("Tkinter入力")
    root.geometry("600x400+400+100")  # ウィンドウのサイズと位置を指定

    # フォント設定
    font = tkFont.Font(family="Helvetica", size=20)

    # テキストウィジェット
    text_widget = Text(root, width=40, height=10, font=font)
    text_widget.pack(pady=20)

    def check_text():
        """テキストの変更を検知してキューに送信"""
        current_text = text_widget.get("1.0", "end-1c")
        text_queue.put(current_text)
        if close_event.is_set():  # Kivyウィンドウが閉じられたらTkinterも閉じる
            root.destroy()
            return
        root.after(100, check_text)  # 0.1秒ごとにチェック

    def on_close():
        """Tkinterウィンドウが閉じられたときの処理"""
        close_event.set()  # フラグを立てる
        root.destroy()

    def ensure_focus():
        """テキストウィジェットに確実にフォーカスを設定"""
        if text_widget.focus_get() != text_widget:
            text_widget.focus_force()
        root.after(100, ensure_focus)  # 0.1秒ごとに再帰的にチェック

    # ウィンドウを閉じるイベントにバインド
    root.protocol("WM_DELETE_WINDOW", on_close)

    # 監視開始
    check_text()

    # Tkinterウィンドウを最前面に移動してフォーカスを設定
    root.lift()  # Tkinterウィンドウを最前面に移動
    root.attributes('-topmost', True)  # 最前面に固定
    root.after(100, lambda: root.attributes('-topmost', False))  # 解除
    root.after(200, ensure_focus)  # 少し遅れてフォーカスを設定

    # Tkinterのメインループ
    root.mainloop()

if __name__ == "__main__":
    # TkinterとKivyで共有するキューを作成
    text_queue = queue.Queue()
    close_event = threading.Event()  # 終了を通知するフラグ

    # Tkinterアプリケーションを起動
    start_tkinter_app(text_queue, close_event)

Kivy側の処理

import threading
import queue
from kivy.app import App
from kivy.uix.boxlayout import BoxLayout
from kivy.clock import Clock


class RootWidget(BoxLayout):  
    pass

class CombinationKivyTk(App):
    def __init__(self, text_queue, close_event, **kwargs):
        super().__init__(**kwargs)
        self.text_queue = text_queue
        self.close_event = close_event  # Kivyが閉じられたら通知するイベント

    def build(self):
        Clock.schedule_interval(self.check_for_updates, 0.1)  # 0.1秒ごとにチェック
        return RootWidget()

    def check_for_updates(self, dt):
        """Tkinterスレッドからのデータをチェックして更新"""
        try:
            new_text = self.text_queue.get_nowait()  # 新しいテキストを取得
            self.root.ids.label.text = new_text  # ラベルを更新
        except queue.Empty:
            pass

    def on_stop(self):
        """Kivyウィンドウが閉じられたときに呼び出される"""
        self.close_event.set()  # フラグを立てる

def start_kivy_app(text_queue, close_event):
    """Kivyアプリケーションを起動"""
    CombinationKivyTk(text_queue=text_queue, close_event=close_event).run()


if __name__ == "__main__":
    # TkinterとKivyで共有するキューを作成
    text_queue = queue.Queue()
    close_event = threading.Event()  # 終了を通知するフラグ

    # 別スレッドでKivyアプリケーションを起動
    threading.Thread(target=start_kivy_app, args=(text_queue, close_event), daemon=True).start()

Comment

タイトルとURLをコピーしました