質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

新規登録して質問してみよう
ただいま回答率
85.35%
PostgreSQL

PostgreSQLはオープンソースのオブジェクトリレーショナルデータベース管理システムです。 Oracle Databaseで使われるPL/SQLを参考に実装されたビルトイン言語で、Windows、 Mac、Linux、UNIX、MSなどいくつものプラットフォームに対応しています。

Python 3.x

Python 3はPythonプログラミング言語の最新バージョンであり、2008年12月3日にリリースされました。

Tkinter

Tkinterは、GUIツールキットである“Tk”をPythonから利用できるようにした標準ライブラリである。

Q&A

解決済

2回答

1605閲覧

Python3 Tkinter DBのデータ抽出をキャンセルしたい

person

総合スコア224

PostgreSQL

PostgreSQLはオープンソースのオブジェクトリレーショナルデータベース管理システムです。 Oracle Databaseで使われるPL/SQLを参考に実装されたビルトイン言語で、Windows、 Mac、Linux、UNIX、MSなどいくつものプラットフォームに対応しています。

Python 3.x

Python 3はPythonプログラミング言語の最新バージョンであり、2008年12月3日にリリースされました。

Tkinter

Tkinterは、GUIツールキットである“Tk”をPythonから利用できるようにした標準ライブラリである。

0グッド

1クリップ

投稿2023/04/16 11:38

編集2023/04/19 07:55

TkinterでPostgreSQLにあるデータを表示します。
ただ、取得するにはどうしても時間がかかってしまいます。
その取得中にデータの取得を中断したいことがあります。

そこでTkinterのボタンを押したら、SELECT文の処理を中断したいのですが可能ですか?

GUIの前提として、DBのデータの表示・追加・変更・削除を想定しているので、DB処理中は処理中であることを示すフレームを最前面に表示し他の画面の操作を不可とします。処理が終わったら、処理前のフレームを最前面に戻します。
下記のソースコードのように。
想定では、"Processing now." のフレームにボタンを付けて、SELECTをキャンセルしたいです。

Python

1import time 2import tkinter as tk 3from tkinter import ttk 4 5def on_button1(): 6 frame0.tkraise() 7 frame0.update() 8 long_process() 9 frame1.tkraise() 10 11def long_process(): 12 time.sleep(1) 13 14if __name__ == "__main__": 15 root = tk.Tk() 16 17 root.grid_rowconfigure(0, weight=1) 18 root.grid_columnconfigure(0, weight=1) 19 20 frame0 = ttk.Frame(root) 21 frame1 = ttk.Frame(root) 22 frame0.grid(row=0, column=0, sticky=tk.NSEW) 23 frame1.grid(row=0, column=0, sticky=tk.NSEW) 24 25 frame1.tkraise() 26 27 text0 = "Processing now." 28 label0 = tk.Label(frame0, text=text0) 29 label0.pack(anchor=tk.CENTER, expand=1) 30 31 text1 = "Button" 32 button1 = ttk.Button(frame1, text=text1, command=on_button1) 33 button1.pack(anchor=tk.CENTER, expand=1) 34 35 root.mainloop()

ただ、上のコードではDB操作を試みるとアプリが固まってボタンの操作もできなくなるので、過去の質問の回答にある実装方法に変えるつもりでいます。(GUIを動かすメインスレッドとは別のスレッドをガン回しする)

追記

メモ

psycopg2のキャンセルサンプル。
ボタン1を押すと、SELECTクエリ実行。ボタン2を押すと、クエリ中断。
(これを画面遷移と組み合わせるのが課題)

Python

1import queue 2import threading 3import time 4import tkinter as tk 5from tkinter import ttk 6 7import psycopg2 8 9 10def monitor_variable1(): 11 global monitor_flag1, gval1 12 13 while monitor_flag1: 14 try: 15 data = gval1.get_nowait() 16 except queue.Empty as e: 17 pass 18 except Exception as e: 19 print(e, type(e)) 20 else: 21 if data == "SELECT": 22 threading.Thread(target=select, daemon=True).start() 23 elif data == "CANCEL": 24 threading.Thread(target=cancel, daemon=True).start() 25 26 time.sleep(0.1) 27 28 29def monitor_variable2(): 30 global monitor_flag2, gval2 31 32 while monitor_flag2: 33 try: 34 data = gval2.get_nowait() 35 except queue.Empty as e: 36 pass 37 except Exception as e: 38 print(e, type(e)) 39 else: 40 pass 41 42 time.sleep(0.1) 43 44 45def select(): 46 global con, cur 47 48 print("select()") 49 50 try: 51 with psycopg2.connect( 52 host = "192.168.0.100", 53 user = "user01", 54 password = "pass01", 55 dbname = "testdb" 56 ) as con: 57 58 with con.cursor() as cur: 59 print("SQL...") 60 sql = "SELECT * FROM testview;" 61 cur.execute(sql) 62 results = cur.fetchall() 63 print("rows: {}".format(len(results))) 64 65 except psycopg2.errors.QueryCanceled as e: 66 print("cancel") 67 68 except Exception as e: 69 print(type(e), e) 70 71 finally: 72 con = None 73 cur = None 74 75 76def cancel(): 77 global con 78 79 print("cancel()") 80 81 if con: 82 con.cancel() 83 84 85def on_button1(): 86 global gval1 87 88 gval1.put("SELECT") 89 90 91def on_button2(): 92 global gval1 93 94 gval1.put("CANCEL") 95 96 97def on_close(): 98 global monitor_flag1, monitor_flag2, con 99 100 if con: 101 return 102 103 monitor_flag1 = False 104 monitor_flag2 = False 105 thread1.join() 106 thread2.join() 107 root.destroy() 108 109 110if __name__ == "__main__": 111 gval1 = queue.Queue() 112 gval2 = queue.Queue() 113 monitor_flag1= True 114 monitor_flag2= True 115 con = None 116 cur = None 117 118 thread1 = threading.Thread(target=monitor_variable1) 119 thread2 = threading.Thread(target=monitor_variable2) 120 thread1.start() 121 thread2.start() 122 123 root = tk.Tk() 124 125 button1 = ttk.Button(root, text="Button1", command=on_button1) 126 button1.grid() 127 128 button2 = ttk.Button(root, text="Button2", command=on_button2) 129 button2.grid() 130 131 root.protocol("WM_DELETE_WINDOW", on_close) 132 root.mainloop()

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

退会済みユーザー

退会済みユーザー

2023/04/16 19:11

ちゃんと設計し、最小限のコードで「できない」という説明をしましょう。 それすらしていないのに質問してる時点でもう丸投げと同じです。 PostgreSQLは pg_cancel_backend でクエリを止められます(要権限)。 https://www.postgresql.org/docs/15/functions-admin.html#FUNCTIONS-ADMIN-SIGNAL
退会済みユーザー

退会済みユーザー

2023/04/17 06:36

GUIはスレッド間で同期を取ってアクセスすればいいだけですよ。UIをいじるスレッドを1つだけにして、時間のかかる処理はワーカースレッドにやらせる、というのがセオリーです。同期はteamiklさんのおっしゃるようにキューを使うのが汎用ですが、別にスレッドをjoinするだけで良ければそこまでする必要もありません。 何が「出来ない」のか明確に最小限のコードで表しましょう。
guest

回答2

0

ベストアンサー

中断自体は可能ですが、そのようにプログラムの設計自体を見直す必要があります。

ここでの「処理の中断」には、2つのトピックがあります。

  • Python の処理の中断
  • DB のクエリの中断

time.sleep() が時間のかかる処理だと思いますが、
現状のコードの構成で中断する手段は long_process関数 の内容次第です。

  • 別スレッドにする ... スレッドの中断はプラットフォーム依存。PythonのThreadでは未対応。
    時間のかかるループ内に中断フラグを設ける実装が必要。
  • 別プロセスにする ... プロセスの強制終了 (kill)
  • 非同期処理 (asyncio等) ... プログラムの大幅な設計見直しが必要
  • シグナルを使った割り込み・中断 (プラットフォーム依存)
  • 実行に時間のかかる関数をジェネレーターとして実装し、タイマーで分割して実行

加えて、GUIを応答なしにさせないようにするには、
イベントハンドラーで呼ばれる関数は即時に抜けて、
GUIのイベントループ側に処理を戻さなければなりません。


DBのクエリの中断については、Python のDB APIの範囲外の為、
各DB・ライブラリにより対応状況が異なります。
(例: psycopgライブラリであれば、connectionクラスのcancelメソッド)

対応してない場合は、別プロセスにしてプロセスを強制終了というのが
ライブラリに依存しない実装手段です。


質問の要点を、時間のかかる処理の中断とされてるようですすが
内容の詳細次第では、全く別の解決策が取れる場合もあります。

例えば、極端な例を挙げると
数万件規模のデータを全て表示したい場合などは、

  • メモリには限りがあり、一度に格納できる件数には限りがある
  • 画面サイズにも限りがあるので、一度に表示できる件数は限られる

といった制約があるため、解決策の候補には
時間のかかるクエリ自体を発行せずに、クエリ自体を分割する手法もあります。

この辺りは、時間のかかる処理の内容次第なので、
より良い解決策が必要な場合は、具体的な詳細を記載してください。(データ件数の規模など)

投稿2023/04/16 20:22

編集2023/04/16 20:22
teamikl

総合スコア8760

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

person

2023/04/17 00:20 編集

回答ありがとうございます。 時間のかかる処理は、DBの抽出~DBデータのGUI反映等を暫定的にtime.sleep()で表現しているだけです。 DBのライブラリはpsycopg2を使う予定です。cancelメソッド知りませんでした。ドキュメントで確認してみます。 DB絡みのレスポンスの悪さについては、大量のデータがDBに保存されているかつwindowsPC, raspberry piなどを数百台同時に無線接続で使うなどの想定によるものです。ここは回避できないです。(有線にしたり、損失の低いケーブル使う等極力レスポンスの悪さを改良する策もあるにはありますが、ソフト的にもある程度対応できていたい。) 1台に表示するデータの件数は多くないです。いろいろなテーブルのデータを表示しますが、最大でも1画面に50レコード前後ぐらいです(複数のテーブルを結合したビューを参照)。
teamikl

2023/04/17 05:36 編集

クライアント側で所得する件数が多くないのであれば、 課題は時間がかかった時の timeout 処理ですね。 追記のコードに関しては、 gval はリストですが、マルチスレッドで使う場合は 同期キューを使いましょう。(標準ライブラリの queue モジュール) 複数のスレッドから同時アクセスした際の挙動に問題があり、 小さなテストプログラムでは発生しないが、実運用した際には 問題の再現が難しい不具合の原因となりえます。 ==== スレッドの使い方を以下のように スレッドA: DB接続・切断、CANCEL, SELECT 等の命令を待ち受ける スレッドB: SELECT した際にクエリを発行する作業用スレッド スレッドA はいつでも CANCEL を待ち受け出来るようにしておきます。 データ受け渡し用の queue は、GUI -> DB と DB -> GUI の2つが必要です。 キューA: SELECT, CANCEL 等の命令を送る キューB: DB の所得結果を GUI のスレッドへ送る GUI 側でのキューの読み出しは、get_nowait を使い、キューが空の場合は例外を捕捉します。 注意: get だとブロッキング処理なので、キューが空の時にGUIが応答なしになります。 追記:過去の質問のリンク先のコメントにサンプルコード投稿有 ==== DBのクエリ中断について https://www.psycopg.org/docs/connection.html#connection.cancel https://www.postgresql.org/docs/current/libpq-cancel.html#LIBPQ-PQCANCEL マルチスレッドで安全に使えるかどうかの情報について、抜粋すると > You can call this function from a different thread than the one currently executing a database operation > PQcancel can safely be invoked from a signal handler DB用スレッドを一つにすることもできますが、難易度は少し上がります。 - シグナルハンドラで割り込む - OSのselect システムコールを使ったタイムアウト (ドキュメントの Advanced topics) プラットフォームによっては使えない場合もあり。
person

2023/04/17 06:46 編集

すみません。 キューについては過去の質問でそのように回答頂いていましたね。 追記コードを修正しました。 スレッドについてですが、DBの接続・クエリ・切断とキャンセルを同じにしてしまうと、同じwhileで条件分岐することになるため、クエリを実行してから終了するまでキャンセルを受け付けられませんでした。 そのため、追記したコードのように別のスレッドにしました。 これは改善策あるのでしょうか? 回答にあるスレッドAは都度生成するイメージでしょうか?
teamikl

2023/04/17 10:08

> クエリを実行してから終了するまでキャンセルを受け付けられませんでした。 スレッドAはコマンドの受け取りのみで、実際の処理はそのループ内では行いません。 スレッドAのループではいつでもコマンドを受け付けられるようにして、 SELECT クエリ発行はスレッドBで処理するようにします。 >回答にあるスレッドAは都度生成するイメージでしょうか? 複数回のSELECT を許容するかどうかにもよりますが、 スレッドBを SELECT の度に生成する感じです。 このスレッドは稼働中にプログラムが終了すると、スレッドが残り続けることがあるので、 daemon スレッドのフラグを設定しておくと良いです。
person

2023/04/18 23:41 編集

> 複数回のSELECT を許容するかどうかにもよりますが、 > スレッドBを SELECT の度に生成する感じです。 SELECTやINSERT、UPDATE、DELETEを複数回呼ぶこともある想定です。 あと、DB処理中にウィンドウの×を押してアプリ終了されるのは避けたいので(タスクマネージャー等で強制終了はやむなしで見逃す)、queue使ってスレッドの中で無限ループをガン回しにして、閉じるときにjoin()呼ぶようにした方が良いでしょうか?
teamikl

2023/04/19 06:41 編集

> SELECTやINSERT、UPDATE、DELETEを複数回呼ぶこともある想定です。 クエリの同時発行を許容するかどうかにもよりますが、 - メインスレッド (GUI) - GUIからのコマンド(CANCEL, SELECT 等) 待ち受け スレッド - DB へのクエリ (SELECT, INSERT, UPDATE, DELETE) メインスレッド以外に2つのスレッドを準備して、 それぞれのスレッド間をqueue でやりとりします。 DBの中断手段は、2通り(どちらか) A: GUIからのコマンド待ち受けスレッドから、conn.cancel() を呼ぶ B: DB用のスレッドでシグナルハンドラを設定して、シグナルを送る。 通常のマルチスレッドであれば、スレッドに関連するリソースの生存期間の管理の都合、 DDへのアクセスはDBのスレッドからのみ行うように設計したい所ですが、 conn.ccancel メソッドのみは、例外的に別スレッドやシグナルハンドラから呼んでも 安全なように設計されてます。 まずは A の実装方法を検討してみてください。 B の方法は、DBへのアクセスを単一スレッドからのみにできるので、 クラス・モジュールを有効に使う設計では役に立つかもしれませが、 シグナルの挙動に関して、プラットフォーム間で違いが出る場合があります。 ---- > (タスクマネージャー等で強制終了はやむなしで見逃す) シグナルハンドラで補足できる場合があります。(※ windows以外) 強制終了時にスレッドが残り続けないようにするには、 スレッドのdaemonフラグをTrueにしておくと良いです。 > queue使ってスレッドの中で無限ループをガン回しにして、閉じるときにjoin()呼ぶようにした方が良いでしょうか おおむねその通りですが、join 前に、ループを確実に抜けるようにしておきます。 ループが阻害されるケースはいくつかあって、それぞれに対応が必要です - queue が空の時、queue.get で処理が止まっている → queue に適当な終了用のメッセージをいれる - queue が詰まっていて、終了メッセージ読み取りまで時間がかかる -> ループ毎に終了フラグをチェック - time.sleep で長期間スリープ中 -> スレッドでは threading.Event.wait を使う。キャンセル可能なsleep - 時間のかかる for ループを実行中 → 定期的に終了フラグをチェックし、ループを抜ける GUIを閉じる(root.destroy)前に join すると GUI が応答なしになるので、その点も注意。
person

2023/04/19 08:08 編集

思い違いのため、返信の内容を変更しました。 > まずは A の実装方法を検討してみてください。 追記メモの内容を変更しました。 この認識で合ってますか? ----- (ごめんなさい、Button1とButton2を相互に連打したら<class 'AttributeError'> 'NoneType' object has no attribute 'fetchall'出てしまいました。追記メモは残しますが見直してみます。 ----- スレッド使っているので、if con: 辺りを修正する必要がありそうです・・・。
teamikl

2023/04/19 09:43 編集

> 追記メモの内容を変更しました。 > この認識で合ってますか? 変更点が追えないのですが、スレッドは2,3個固定で済みます。 SELECT を別スレッドで実行 ... ok CANCEL を別スレッドで実行 ... 不要です 間違いではないが、無駄に冗長になっている箇所。 スレッドを用いたサンプルコードは過去の質問のコメント欄に載せてます。 今回の質問の要点は、時間のかかるクエリの実行・中断なので 解決策は、 - 時間のかかる処理を別スレッドで実行するように変更 ... ここはクリア。応答なし状態は解消しているはずです。 - con.cancel() に当たる部分を呼び出す。... "CANCEL" を受け取った時にその場で実行で構いません。  念のため注意点を書いておくと、con.cancel() 呼び出し自体は  別スレッドから呼び出しても安全ですが、  con 自体がタイミングによって None になる可能性があります。  if con: の時点で None でなくても、その直後に None になる、等。
person

2023/04/19 11:03

Noneはtry-exceptで、できるときだけcancel()するでもいいですかね。
teamikl

2023/04/19 11:19

マルチスレッドでは、タイミングに起因する問題は 問題の再現がとても難しいので、極力そのような構造は排除した方が良いです。 (長期間稼働した後や、ある特定条件でのみ発生したり、他の人が問題を再現できなくて、デバッグが困難になる傾向があります) 2つ以上のスレッドから共通のリソースにアクセスする場合、 if con: と con.cancel() の間に con = None が同時発生しないように 双方で threading.Lock 等を用いて排他制御を行います。
person

2023/04/20 00:20

相互連打で <class 'psycopg2.ProgrammingError'> no results to fetch も発生。おそらく原因は同じだと思います。 ロックする場合、下記のように値変えている部分だけロックすれば良いでしょうか? with threading.Lock(): con = None cur = None
person

2023/04/20 00:46 編集

lock = threading.Lock() with lock: con = None cur = None with lock: if con: con.cancel() キャンセルにもロック付けたが、button1, button2 連打で下記例外発生。 <class 'AttributeError'> 'NoneType' object has no attribute 'execute' <class 'AttributeError'> 'NoneType' object has no attribute 'fetchall'
teamikl

2023/04/20 03:15

> 値変えている部分だけロックすれば良いでしょうか? いいえ。 execute, fetchall メソッドがないと言われているので、 別の場所で起こった例外です。 但し、fetchall のループ全体をロック内にすると、 ロックが解除されるまでcancel が実行されなくなるので、中断処理を阻害してしまう点に注意。
person

2023/04/20 03:51 編集

> 但し、fetchall のループ全体をロック内にすると、 > ロックが解除されるまでcancel が実行されなくなるので、中断処理を阻害してしまう点に注意。 そうなんですよね。そのため、conとcurをNoneに戻すときだけをロックしたつもりなのですが・・・。 作りを変える必要があるのでしょうか?
guest

0

回答ではありません

tkは多くの人には不要であり、本質的でもないので、省いています。dockerとpython3が使えるLinux環境を想定したpsycopg2によるpostgres14へのアクセスとクエリのキャンセル処理のサンプルを置いておきます。python部分はtkがないことを除き質問者さんのものと似たようなものです。
環境構築にはvenvを使っています。

環境構築スクリプト

空のディレクトリから、sh create_env.shなどとして動かせばOKです。

create_env.sh

sh

1docker run -d --rm -e POSTGRES_PASSWORD=pass -v $(pwd)/data:/var/lib/postgresql/data -p 5432:5432 --name postgres_$$ postgres:14-alpine 2python3 -m venv env 3. ./env/bin/activate 4pip install -U pip 5pip install -U setuptools 6pip install psycopg2-binary 7cat >hoge.py <<EOF 8import psycopg2 9from threading import Thread 10from time import sleep 11def cancel(t, con): 12 sleep(t) 13 con.cancel() 14with psycopg2.connect( 15 host = "localhost", 16 user = "postgres", 17 password = "pass", 18 dbname = "postgres" 19 ) as con: 20 with con.cursor() as cur: 21 try: 22 t = Thread(target = cancel, args = [1, con]) 23 t.start() 24 cur.execute('select pg_sleep(5), version()') 25 t.join() 26 print(cur.fetchall()) 27 except Exception as e: 28 import traceback 29 traceback.print_exc() 30EOF 31python hoge.py 32deactivate 33docker stop postgres_$$

詳細

PostgreSQL14について

環境構築スクリプト内先頭で起動しています。

sh

1docker run -d --rm -e POSTGRES_PASSWORD=pass -v $(pwd)/data:/var/lib/postgresql/data -p 5432:5432 --name postgres_$$ postgres:14-alpine

環境構築スクリプト内最後で終了しています。

sh

1docker stop postgres_$$

PostgreSQLの公式docker image
https://hub.docker.com/_/postgres

venvについて

一度構築した後は、以下で環境に入れます

sh

1. env/bin/activate

抜けるには、以下。

sh

1deactivate

python公式のvenv
https://docs.python.org/ja/3/library/venv.html

psycog2を使ったキャンセル例

with使用、GUI排除、pg_sleepによるクエリ結果の遅延により確実な自動キャンセルとスリム化を図っていますが、大体同じです。
ただ本来の主な用途はCtrl+CによるSIGINT/TERMの処理、pythonだとKeyboardInterruptに当たるので、スレッドを作ることはあまりないかと思います。
hoge.py

python

1import psycopg2 2from threading import Thread 3from time import sleep 4def cancel(t, con): 5 sleep(t) 6 con.cancel() 7with psycopg2.connect( 8 host = "localhost", 9 user = "postgres", 10 password = "pass", 11 dbname = "postgres" 12 ) as con: 13 with con.cursor() as cur: 14 try: 15 t = Thread(target = cancel, args = [1, con]) 16 t.start() 17 cur.execute('select pg_sleep(5), version()') 18 t.join() 19 print(cur.fetchall()) 20 except Exception as e: 21 import traceback 22 traceback.print_exc()

psycopg2公式ドキュメント
https://www.psycopg.org/docs/


(追記)

環境構築後の環境で動作するtkinterキャンセル例

質問がいつまで経っても良くならないので、例だけつけておきました。
質問のコメント欄に書いたキューを使わず、スレッドのjoinをポーリングで待ち合わせる例です。
基本的にtkinterはtkをどこまで信用できるのか分からないので、アバウトな制御しかしていません。
ご参考までにw

python

1import tkinter as tk 2from tkinter import ttk 3from time import sleep 4from threading import Thread 5import psycopg2 6 7def query(obj): 8 with psycopg2.connect( 9 host = "localhost", 10 user = "postgres", 11 password = "pass", 12 dbname = "postgres" 13 ) as con: 14 with con.cursor() as cur: 15 try: 16 obj['con'] = con 17 cur.execute('select pg_sleep(10), version(), timeofday()') 18 obj['con'] = None 19 data = cur.fetchall() 20 obj['result'] = data 21 except Exception as e: 22 import traceback 23 traceback.print_exc() 24 obj['con'] = None 25 obj['finished'] = True 26 27query_index = 1 28def new_query_index(): 29 global query_index 30 result = query_index 31 query_index += 1 32 return result 33 34def add_list(queries, listbox, comp): 35 number = new_query_index() 36 key = f'query-{number}' 37 listbox.insert(listbox.size(), key) 38 obj = {'name' : key, 'number' : number, 'finished' : False, 'thread' : None, 'result' : None, 'con': None} 39 t = Thread(target = query, args=[obj]) 40 obj['thread'] = t 41 queries[key] = obj 42 t.start() 43 comp.pack() 44 45def search_index_from_listbox(value, listbox): 46 content = listbox.get(0, listbox.size()) 47 for i, name in enumerate(content): 48 if name == value: 49 return i 50 return -1 51 52def cancel(queries, listbox): 53 tpl = listbox.curselection() 54 for i in tpl: 55 name = listbox.get(i) 56 obj = queries[name] 57 con = obj['con'] 58 if con and not obj['finished']: 59 con.cancel() 60 61def join_finished_threads(queries, listbox, comp): 62 for k, v in list(queries.items()): 63 if v['finished']: 64 v['thread'].join() 65 queries.pop(k) 66 listbox.delete(search_index_from_listbox(v['name'], listbox)) 67 print(v['result']) 68 comp.pack() 69 70def main(): 71 queries = {} 72 def polling(): 73 join_finished_threads(queries, listbox, frame) 74 frame.after(100, polling) 75 76 root = tk.Tk() 77 root.geometry('220x300') 78 root.title('スレッド終了のポーリング監視') 79 frame = ttk.Frame(root, padding=10) 80 listbox = tk.Listbox(frame, selectmode="multiple", height=10) 81 listbox.grid(column=0, row=0, columnspan=2) 82 frame.grid() 83 button_add = ttk.Button(frame, text='追加', command=lambda: add_list(queries, listbox, frame)).grid(column=0, row=1) 84 button_cancel = ttk.Button(frame, text='キャンセル', command=lambda: cancel(queries, listbox)).grid(column=1, row=1) 85 frame.pack(padx=10, pady=20) 86 polling() 87 root.mainloop() 88 89if __name__ == '__main__': 90 main()

ついでにqueue版。別スレッドからbindを使うのは怖かったので(使えれば自前Queueも不要だし)、結局UIスレッドからポーリング。

python

1import tkinter as tk 2from tkinter import ttk 3from time import sleep 4from threading import Thread 5import psycopg2 6from queue import Queue 7 8def query(obj, commands, queries, listbox, comp): 9 with psycopg2.connect( 10 host = "localhost", 11 user = "postgres", 12 password = "pass", 13 dbname = "postgres" 14 ) as con: 15 with con.cursor() as cur: 16 try: 17 obj['con'] = con 18 cur.execute('select pg_sleep(10), version(), timeofday()') 19 obj['con'] = None 20 data = cur.fetchall() 21 obj['result'] = data 22 except Exception as e: 23 import traceback 24 traceback.print_exc() 25 obj['con'] = None 26 obj['finished'] = True 27 28 def query_on_ui_thread(obj, queries, listbox, comp): 29 obj['thread'].join() 30 listbox.delete(search_index_from_listbox(obj['name'], listbox)) 31 print(obj['result']) 32 queries.pop(obj['name']) 33 comp.pack() 34 35 commands.put(lambda: query_on_ui_thread(obj, queries, listbox, comp)) 36 37query_index = 1 38def new_query_index(): 39 global query_index 40 result = query_index 41 query_index += 1 42 return result 43 44def add_list(queries, listbox, comp, commands): 45 number = new_query_index() 46 key = f'query-{number}' 47 listbox.insert(listbox.size(), key) 48 obj = {'name' : key, 'number' : number, 'finished' : False, 'thread' : None, 'result' : None, 'con': None} 49 t = Thread(target = query, args=[obj, commands, queries, listbox, comp]) 50 obj['thread'] = t 51 queries[key] = obj 52 t.start() 53 comp.pack() 54 55def search_index_from_listbox(value, listbox): 56 content = listbox.get(0, listbox.size()) 57 for i, name in enumerate(content): 58 if name == value: 59 return i 60 return -1 61 62def cancel(queries, listbox): 63 tpl = listbox.curselection() 64 for i in tpl: 65 name = listbox.get(i) 66 obj = queries[name] 67 con = obj['con'] 68 if con and not obj['finished']: 69 con.cancel() 70 71def main(): 72 queries = {} 73 commands = Queue() 74 def polling(): 75 while not commands.empty(): 76 commands.get()() 77 frame.after(100, polling) 78 79 root = tk.Tk() 80 root.geometry('220x300') 81 root.title('スレッド終了のポーリング監視') 82 frame = ttk.Frame(root, padding=10) 83 listbox = tk.Listbox(frame, selectmode="multiple", height=10) 84 listbox.grid(column=0, row=0, columnspan=2) 85 frame.grid() 86 button_add = ttk.Button(frame, text='追加', command=lambda: add_list(queries, listbox, frame, commands)).grid(column=0, row=1) 87 button_cancel = ttk.Button(frame, text='キャンセル', command=lambda: cancel(queries, listbox)).grid(column=1, row=1) 88 frame.pack(padx=10, pady=20) 89 polling() 90 root.mainloop() 91 92if __name__ == '__main__': 93 main()

投稿2023/04/17 05:50

編集2023/04/17 12:39
退会済みユーザー

退会済みユーザー

総合スコア0

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

person

2023/04/20 07:17

質問に対する回答付け足していただきありがとうございます。 実行しながら確認してみます。
person

2023/04/21 06:31 編集

query_on_ui_thread()で、join()してGUIを更新した後のcomp.pack()はなぜ入れているのでしょうか?
退会済みユーザー

退会済みユーザー

2023/04/21 06:45

あまり深い意味はありません。 tkを信用してないので、UIいじったらウィジェットの形が変わる可能性を考慮して、こまめにpackしてるだけです。 質問だけはして、自分は依頼に応じないスタンスはどうかと思いますよ。 何を知りたいのか、詰まっている部分を明確にし、そこだけをピンポイントで聞いてください。 我々はあなたのメンターではありません。
person

2023/04/21 07:06

すみません。 行き詰まる内容によっては、質問することで別の不明点がでてきたり、本来の質問とは関連性がない些細なことも気になってしまいます。何卒ご容赦ください。 あとtkって信用できないのですか?
退会済みユーザー

退会済みユーザー

2023/04/21 07:09 編集

容赦できるレベルじゃないので注意しています。 私は信用していませんね。 1つ1つ別の質問としてあげてください。
person

2023/04/21 08:12

回答上の環境構築の辺は知識無いのでよくわかりませんが、 その下のtkinterの実装のスレッド間の処理の流れは理解しました。 画面遷移絡みのGUIになるので、そのままは使えませんが、そういった実装ができるかどうかの確認も含めて参考にさせていただきます。
退会済みユーザー

退会済みユーザー

2023/04/21 08:24

回答ではないただの参考情報なので、teamiklさんの有益な回答をベストアンサーにしてください。 コメントのQueue2つの話はやや冗長な気がしなくもないし、セオリーという感じでもないですが、マルチスレッドに対する手堅い姿勢は評価に値します。
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.35%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問