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

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

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

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

Tkinter

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

Q&A

解決済

1回答

4414閲覧

Python3 Tkinter 時刻入力専用画面の作成

person

総合スコア224

Python 3.x

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

Tkinter

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

0グッド

0クリップ

投稿2022/04/11 06:39

編集2022/04/11 07:15

Tkinterで時刻入力専用画面を作成します。

Python3 Tkinter 時間入力やテンキーなどの専用画面をウィジェットのすぐ下に表示したい
の回答を参考に、表示・非表示を切り替えるように作成しました。

仕様として
Treeviewレコードの時刻データ列をダブルクリックすると時刻入力画面が出て、時刻の変更が可能というものです。

時刻入力画面の左上の座標が、該当時刻列の左下。
ただし、①ディスプレイ上より右にはみ出る場合は、入力画面の左端を該当時刻列の右端に揃える。
②ディスプレイ上より下にはみ出る場合(タスクバーは考慮しない)は、入力画面の下端を該当時刻列の上端に揃える。
というふうにしたいです。

作ってみたのですが、
①は、惜しいような気もしますが目標より若干右よりなきがします。
②も目標より下にずれている感じです。
単に補正が必要なのか計算が間違っているのか分かりません。
分かる方教えていただけると幸いです。

(座標計算はpopup_time_input.pyのTimeInputクラスのcall_time_input関数でやってます)

また、他に改善点あればそちらもご指摘お願いします。

目標位置と実際の位置の差
黄:目標位置
緑:実際のウィンドウの端の位置
イメージ説明

Python

1# main.py <- entry point 2 3import tkinter as tk 4from tkinter import ttk 5 6import popup_time_input 7 8class App(tk.Frame): 9 10 def __init__(self, master): 11 super().__init__(master) # tk.Frame(root) に相当 12 self.time_input = popup_time_input.TimeInput() 13 self.create_widgets() 14 15 16 def create_widgets(self): 17 self.tree = ttk.Treeview(root, selectmode="browse") 18 self.tree.pack() 19 20 # headings 21 columns = 2 22 self.tree["columns"] = tuple(range(columns)) 23 self.tree["show"] = "headings" 24 self.tree.heading(0, text="No.") 25 self.tree.heading(1, text="Time[hh:mm]") 26 27 # data 28 self.tree.insert("", "end", values=("1", "00:00")) 29 self.tree.insert("", "end", values=("2", "01:11")) 30 self.tree.insert("", "end", values=("3", "02:22")) 31 self.tree.insert("", "end", values=("4", "03:33")) 32 self.tree.insert("", "end", values=("5", "04:44")) 33 34 self.tree.bind("<Double-Button-1>", self.on_tree) 35 36 37 def on_tree(self, e): 38 tree = e.widget 39 selected = tree.selection() # 選択行 40 clickarea = tree.identify_region(e.x, e.y) # "heading" or "cell" 41 42 if len(selected) == 1 and clickarea == "cell": 43 # 列取得 44 x = tree.winfo_pointerx() - tree.winfo_rootx() 45 select_column_str = tree.identify_column(x) 46 select_column_int = int(select_column_str[1:]) 47 48 # 時刻列ダブルクリックで専用入力画面表示 49 if select_column_int == 2: 50 self.time_input.call_time_input(tree, select_column_int) 51 52 53if __name__ == "__main__": 54 root = tk.Tk() 55 app = App(root) 56 app.mainloop()

Python

1#popup_time_input.py 2 3import tkinter as tk 4from tkinter import ttk 5 6class TimeInput: 7 8 def __init__(self): 9 self.width = 400 10 self.height = 300 11 self.tree = None 12 self.tree_iid = None 13 self.select_column = None 14 15 self.input_window = tk.Toplevel() 16 self.input_window.withdraw() 17 self.input_window.title("InputWindow") 18 self.create_widgets() 19 self.input_window.protocol("WM_DELETE_WINDOW", lambda:None) 20 21 22 def create_widgets(self): 23 self.input_window.rowconfigure((0,1,2,3), weight=1) 24 self.input_window.columnconfigure((0,2), weight=1) 25 26 self.hour = tk.StringVar() 27 hour_entry = ttk.Entry(self.input_window, textvariable=self.hour) 28 hour_entry.grid(row=0, column=0) 29 colon = ttk.Label(self.input_window, text=":") 30 colon.grid(row=0, column=1) 31 self.minu = tk.StringVar() 32 minu_entry = ttk.Entry(self.input_window, textvariable=self.minu) 33 minu_entry.grid(row=0, column=2) 34 35 hourplus = ttk.Button(self.input_window, text="+", command=self.on_hourplus) 36 hourplus.grid(row=1, column=0, sticky="nsew") 37 minuplus = ttk.Button(self.input_window, text="+", command=self.on_minuplus) 38 minuplus.grid(row=1, column=2, sticky="nsew") 39 40 hourminus = ttk.Button(self.input_window, text="-", command=self.on_hourminus) 41 hourminus.grid(row=2, column=0, sticky="nsew") 42 minuminus = ttk.Button(self.input_window, text="-", command=self.on_minuminus) 43 minuminus.grid(row=2, column=2, sticky="nsew") 44 45 ok = ttk.Button(self.input_window, text="OK", command=self.on_ok) 46 ok.grid(row=3, column=0) 47 48 cancel = ttk.Button(self.input_window, text="cancel", command=self.on_cancel) 49 cancel.grid(row=3, column=2) 50 51 52 def on_hourplus(self): 53 # hour + 54 txt = self.hour.get() 55 try: 56 if int(txt) < 23: 57 txt = str((int(txt) + 1)).zfill(2) 58 else: 59 txt = "00" 60 self.hour.set(txt) 61 except: 62 pass 63 64 65 def on_minuplus(self): 66 # minutes + 67 txt = self.minu.get() 68 try: 69 if int(txt) < 59: 70 txt = str((int(txt) + 1)).zfill(2) 71 else: 72 txt = "00" 73 self.minu.set(txt) 74 except: 75 pass 76 77 78 def on_hourminus(self): 79 # hour - - 80 txt = self.hour.get() 81 try: 82 if int(txt) > 0: 83 txt = str((int(txt) - 1)).zfill(2) 84 else: 85 txt = "23" 86 self.hour.set(txt) 87 except: 88 pass 89 90 91 def on_minuminus(self): 92 # minutes - 93 txt = self.minu.get() 94 try: 95 if int(txt) > 0: 96 txt = str((int(txt) - 1)).zfill(2) 97 else: 98 txt = "59" 99 self.minu.set(txt) 100 except: 101 pass 102 103 104 def on_ok(self): 105 children = self.tree.get_children() 106 new_data = self.hour.get() + ":" + self.minu.get() 107 self.tree.set(self.tree_iid, self.select_column - 1, new_data) 108 self.input_window.grab_release() 109 self.input_window.withdraw() 110 111 112 def on_cancel(self): 113 self.input_window.grab_release() 114 self.input_window.withdraw() 115 116 117 def call_time_input(self, tree, select_column): 118 # 表示用関数(外部から呼ぶ) 119 self.tree = tree 120 self.select_column = select_column 121 122 is_select = tree.selection() 123 124 if is_select: 125 self.tree_iid = is_select[0] 126 value = tree.item(self.tree_iid)["values"][select_column - 1] 127 print("value: {}".format(value)) 128 self.hour.set(value[0:2]) 129 self.minu.set(value[3:5]) 130 131 # Treeview該当レコードの左下にウィンドウの左上を設定 132 x, y, w, h = tree.bbox(self.tree_iid, select_column - 1) 133 x += tree.winfo_rootx() 134 y += tree.winfo_rooty() + h 135 136 print(y) 137 print(h) 138 print(self.input_window.winfo_screenheight()) 139 print(self.input_window.winfo_height()) 140 141 # はみ出すようなら、Treeview該当レコードの右上にウィンドウの右下を設定 142 limitx = self.input_window.winfo_screenwidth() - self.input_window.winfo_width() 143 limity = self.input_window.winfo_screenheight() - self.input_window.winfo_height() 144 145 if x >= limitx: 146 x = x - self.input_window.winfo_width() 147 if y >= limity: 148 y = y - h - self.input_window.winfo_height() 149 150 geometry = f"{self.width}x{self.height}+{x}+{y}" 151 print("geometry: {}".format(geometry)) 152 self.input_window.geometry(geometry) 153 154 self.input_window.deiconify() 155 self.input_window.grab_set()

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

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

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

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

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

guest

回答1

0

ベストアンサー

サイズ調整は、タイトルバーなしのものなので
まずは、タイトルバーを消して試してみてください。

python

1 self.input_window.wm_overrideredirect(True) 2 # self.input_window.title("InputWindow")

もしタイトルバーが必要な場合は、デフォルトのタイトルバーは使わずに
ウィンドウ内にラベル等で擬似的にタイトルっぽく表示させたほうが容易かもしれません。但し
ドラッグを許可するかの挙動は変わるので、必要ならば、その辺りの調整は追加で必要です。

ちなみに、タイトルバーのサイズを足し引きして調整は可能ですが、
タイトルバーのサイズを所得する関数は tkinter にはなく、
プラットフォームやモニター環境依存になってきます。


追記: ポップアップウィンドウの挙動に寄せるなら

  • ウィンドウ表示時に入力欄へ focus 移動
  • ポップアップウィンドウ(Toplevel)の"<Leave>" イベントで、フォーカスが外れた時に非表示 (withdraw)
  • WM_DELETE_WINDOW 時に非表示 (withdraw)

とすると、Combobox のリスト選択ででてくるようなポップアップ・ウィンドウの挙動に近くなります。


FocusOut による挙動のサンプル

python

1#!/usr/bin/env python3.9 2 3import tkinter as tk 4from tkinter import ttk 5from functools import partial 6 7TimeSpinbox = partial(ttk.Spinbox, 8 from_=0, to=60, format="%02.0f") 9 10 11class TimeEdit(ttk.Frame): 12 def __init__(self, *args, **kw): 13 super().__init__(*args, **kw) 14 15 var_hh = tk.IntVar(self) 16 var_mm = tk.IntVar(self) 17 18 entry_hh = TimeSpinbox(self, 19 textvariable=var_hh) 20 entry_mm = TimeSpinbox(self, 21 textvariable=var_mm) 22 separator = ttk.Label(self, text=":") 23 24 entry_hh.grid(row=0, column=0) 25 separator.grid(row=0, column=1) 26 entry_mm.grid(row=0, column=2) 27 28 entry_hh.set("00") 29 entry_mm.set("00") 30 31 self._var_hh = var_hh 32 self._var_mm = var_mm 33 self._entry_hh = entry_hh 34 self._entry_mm = entry_mm 35 36 37def main(): 38 root = tk.Tk() 39 40 top = tk.Toplevel(root) 41 top.wm_overrideredirect (True) 42 top.withdraw() 43 top.bind("<FocusOut>", lambda e:top.withdraw()) 44 top.protocol("WM_DELETE_WINDOW", top.withdraw) 45 edit = TimeEdit(top) 46 edit.pack() 47 48 49 50 def enterEditCell(event): 51 tree = event.widget 52 region = tree.identify_region(event.x, event.y) 53 if region == "cell": 54 col = tree.identify_column(event.x) 55 item = tree.selection()[0] 56 x, y, w, h = tree.bbox(item, col) 57 58 x += tree.winfo_rootx() 59 y += tree.winfo_rooty() + h 60 61 w2 = top.winfo_reqwidth() 62 h2 = top.winfo_reqheight() 63 W = root.winfo_screenwidth() 64 H = root.winfo_screenheight() 65 if (y + h2) > H: 66 y -= h 67 y -= h2 68 69 top.geometry(f"{w2}x{h2}+{x}+{y}") 70 top.deiconify() 71 top.focus_set() 72 73 COLUMNS = ["Value", "Time (hh:mm)"] 74 tree = ttk.Treeview(root, columns=COLUMNS, show="headings") 75 for col in COLUMNS: 76 tree.heading(col, text=col) 77 tree.bind("<Double-ButtonRelease-1>", enterEditCell) 78 for num in range(10): 79 tree.insert("", tk.END, values=(num,num*10)) 80 tree.pack() 81 82 root.mainloop() 83 84 85if __name__ == '__main__': 86 main()

投稿2022/04/11 10:53

編集2022/04/12 07:05
teamikl

総合スコア8760

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

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

person

2022/04/12 00:31 編集

> ポップアップウィンドウ(Toplevel)の"<Leave>" イベントで、フォーカスが外れた時に非表示 (withdraw) フォーカスがエントリ以外にあたっていて、ボタンからボタンへマウス移動するとき(厳密にはボタンから出たとき)、ポップアップウィンドウ(Toplevel)の"<Leave>" イベント反応してしまいます。 これだとボタンを押してから別のボタンへマウス移動しようとするとウィンドウが消えてしまいます。 ボタンを押したときにfocus_set()呼んで防ぐ感じでしょうか。 →ボタン押しながらずらすと反応するのでダメでした もしくは「エントリかボタンにフォーカスが当たっていない状態でLeaveしたら」に変更するか、でしょうか。 →逆にLeaveに引っかかってほしいときも引っかかりませんでした。
teamikl

2022/04/12 03:36

> ボタンからボタンへマウス移動するとき、ポップアップウィンドウの"<Leave>" イベント反応してしまいます。 ここは確かに、おかしな挙動になりますね。 combobox との相違点では、ポップアップで表示するのが単一ウィジェットだったり、 モーダル・ウィンドウかどうか等が異なるので、 候補としては <FocusOut> イベントだけど、ボタンを押したときもイベントが発生してしまうので 複数の部品があるポップアップウィンドウには、そもそも適応出来ないかもしれません。 →フォーカス中のウィジェットを調べて、ポップアップ内外を判別する。  ウィジェットID は、親からの path 表記なので、文字列に変換して startswith で判別できます → grab_set では外へのフォーカスが発生しない  
teamikl

2022/04/12 04:21 編集

> 初回のgeometry設定忘れたことによるものでした。 こちらは確認できませんでした。コードで言うとどの部分ですか、 もしくは既に修正済み? タイトルバーを含める場合の注意点は、 タイトルバーのサイズは、プラットフォームやモニターの解像度等に影響される点に注意してください。 座標・サイズを固定値決め打ちで調整すると、他の環境ではズレて表示されることがあります。 また、geometry で設定するウィンドウのサイズは、タイトルバーを除く部分のサイズです。 winfo_height で得られるサイズも同様に、タイトルバーの高さは含まれません。
person

2022/04/13 07:45 編集

>> 初回のgeometry設定忘れたことによるものでした。 > > こちらは確認できませんでした。コードで言うとどの部分ですか、 popup_time_input.pyのcall_time_input()で limitx, limityを求める際に、winfo_width(), winfo_height()を使っています。 クラスをインスタンス化した段階では、geometryを設定していないため、自動でウィンドウサイズが割り振られていて、それが原因で初回のみlimitx, limityの値がおかしくなってしまったと考えます。 → limitx, limityの計算に使うwinfo_width, winfo_heightがおかしかったので、計算結果もおかしくなった → そのため、コンストラクタにself.input_window.geometry(f"{self.width}x{self.height}")を入れることで防止
teamikl

2022/04/13 08:42

winfo_width, winfo_height は、タイミングによってはウィジェットのサイズが調整前の場合があるので そういった場合(大抵は、mainloop や update が呼び出される前) は、 winfo_reqwidth, winfo_reqheight を用いてサイズを得ます。
person

2022/04/13 10:39 編集

そんなメソッドあったんですね。 winfo_xxx と winfo_reqxxx ってどう使い分けるのでしょうか。 > winfo_width, winfo_height は、タイミングによってはウィジェットのサイズが調整前の場合があるので こういった場合のみ winfo_reqxxx を使い、他では使わないのでしょうか。 https://www.tcl.tk/man/tcl8.6/TkCmd/winfo.html#M28 winfo reqwidth window Returns a decimal string giving window's requested width, in pixels. This is the value used by window's geometry manager to compute its geometry. winfo width window Returns a decimal string giving window's width in pixels. When a window is first created its width will be 1 pixel; the width will eventually be changed by a geometry manager to fulfil the window's needs. If you need the true width immediately after creating a widget, invoke update to force the geometry manager to arrange it, or use winfo reqwidth to get the window's requested width instead of its actual width.
teamikl

2022/04/13 11:02

winfo の width,height は、ウィジェットの「現在の」サイズを所得します。 reqwidth, reqheight では、変更要求があったサイズの方を所得できます。 ウィジェットのサイズは、レイアウトマネージャにより決定されるのですが、 ここでの要点は、実際にサイズが変更されるのは配置時ではなく、 mainloop や update により描画イベントが発生した後になります。 各ウィジェットの初期化のタイミングが、mainloop 内で発生するイベント内なのか それとも mainloop 呼び出し前に実行されるコードなのかに着目して見てください。 (イベント内の場合は、その関数の実行が終わり、mainloopに処理が戻った後に実際のサイズが変更される)
person

2022/04/14 01:49 編集

今気づいたんですが、grab_set()しててもrootウィンドウ動かせるんですね。 入力が面の表示位置だけ動かないのは、不自然ですね。 root.overrideredirect(1)すると外観がToplevel表示前と変わり不自然になってしまいますし、 rootが動いたときに表示位置をずらすとなると、少し重くなりそう? > FocusOut による挙動のサンプル に提示して頂いた挙動がベストなんでしょうね。
teamikl

2022/04/14 07:19 編集

モーダルウィンドウにするにはもう一手必要で、 root.wait_window(sub_window) のようにします。 > root.overrideredirect(1)すると外観がToplevel表示前と変わり不自然になってしまいますし、 overredirect して、外観は自分で作り込むというのが順当な解決策です。 ポップアップ自体をドラッグにより移動可能にしたい、 等の要望があればその辺も独自に作り込むことになります。 > rootが動いたときに表示位置をずらすとなると、少し重くなりそう? こういった部分が手間なら、place での配置も候補ですが、 wait_window で root の移動は禁止できます。 追記: フォーカスを外れた時に非表示も解決策の一つ。 追記2: タイトルバーありだと、root の移動時に root が全面に出てしまいます。
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

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

ただいまの回答率
85.35%

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

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

質問する

関連した質問