「clipboard_filter」なんて機能は tkinter にはないよ、念のため。
リストボックスがアレだにも通じるんだけれど、「Text」「Entry」なども、「柔軟性の確保」のためなのかあるいは単に旧式だからなのかのどちらかによって、いわゆる「いやん、使いにきー」なテキストボックスを作れる。
入力チェック的な部分についてもちょっと前にこいつに追記しといたんだけれど、そもそもさ、「カットアンドペーストだのの基本機能」の実現が、なんともデフォルトでひじょーに中途半端というか、あまりにも最小限過ぎるわけよね。アンドゥなんか高尚すぎて、やってくれてるはずがない、のレベル、だって、右クリックメニューでの操作すら内蔵されてないんだもの。
「Pythonバッチにお飾りGUI」に追記したのは、tkinter.Entry にバリデータを追加して喜ぶ例ね。ただこれ、たとえば数値を入れさせたい Entry に対して「引用符を入力しようとするなんて万死に値する」として入れようとした入力全部を拒絶しちゃうわけよ。だから例えば「時刻入力させたい Entry」を実現したくてバリデーションで「時刻文字列を構成する文字のみ許容」と書いてしまうと、どこぞのメモからクリップボードにコピーした
1 "00:19:32.3"
というテキストを、二重引用符が含まれているという理由で、Ctrl-v でペーストすると入力が全クリアされてしまう、てことが起こる。
ここまではまぁ「そうなのか」ってことなのだが、ここからはちょっと宗教論争じみてきそうなのよね。一応『「引用符を入力しようとするなんて万死に値する」、なんて言うなんて、そんな、ひどい』対応はしてみることは出来る。ただ、それってちょっと危険な気もするんだよね。少なくともユーザがちゃんとした考えでもって自分が何をしているのか理解の上でクリップボードにテキストをコピーしているものに対して、「ほぉほぉ、きっとチミの入力した引用符は魔が差したんだよね、勝手に独断で引用符削除しとくねーっ」って、これ、「いつでも正しくていつでも安全なのか」って言われると、違うと思わない? 危険となるにはかなりの偶然が必要だけれども、たとえば核ミサイル発射のためのコマンド入力テキストボックスがあるとする。ここにどこぞの WEB ページから意図せずクリップボードにコピーされてしまった(きっと無意識の操作)テキストから「核ミサイル発射のためのコマンド入力テキストボックスが許容する文字セットだけにフィルタして」偶然「発射」コマンドになる、なんてことは…、まぁ可能性はゼロではない、と。
ちょっと最小限ではなくワタシが実際に使っている本物の例なのでごちゃごちゃしててすまんが、たとえばこんな感じね:
1 try:
2 import Tkinter as tkinter # python 2.x, but sorry, I'm not testing it.
3 import ttk
4 from Tkinter import E, W, N, S # for sticky
5 def _askopenfilename(master):
6 import tkFileDialog
7 return tkFileDialog.askopenfilename()
8 from tkSimpleDialog import Dialog
9 from ScrolledText import ScrolledText
10 except ImportError:
11 import tkinter
12 from tkinter import ttk
13 from tkinter import E, W, N, S # for sticky
14 def _askopenfilename(master):
15 from tkinter.filedialog import askopenfilenames
16 return askopenfilenames(multiple=0)
17 from tkinter.simpledialog import Dialog
18 from tkinter.scrolledtext import ScrolledText
19
20
21 class _MyEntry(tkinter.Entry):
22 def __init__(
23 self,
24 root,
25 validcharsfun,
26 clipboard_filter_fun,
27 *args, **kwargs):
28 #
29 tkinter.Entry.__init__(self, root, *args, **kwargs)
30 #
31 def _validator(*args):
32 P, d = args
33 if d == "1": # insert
34 return validcharsfun(P)
35 return True
36 #
37 # %d = Type of action (1=insert, 0=delete, -1 for others)
38 # %i = index of char string to be inserted/deleted, or -1
39 # %P = value of the entry if the edit is allowed
40 # %s = value of entry prior to editing
41 # %S = the text string being inserted or deleted, if any
42 # %v = the type of validation that is currently set
43 # %V = the type of validation that triggered the callback
44 # (key, focusin, focusout, forced)
45 # %W = the tk name of the widget
46 self.config(validate="key")
47 self.config(validatecommand=(root.register(_validator), '%P', '%d'))
48
49 def _clipboard_filter(*args):
50 nc = clipboard_filter_fun(self.clipboard_get())
51 self.clipboard_clear()
52 self.clipboard_append(nc)
53 self.bind("<Control-v>", _clipboard_filter)
54
55
56 class _MyNumEntry(_MyEntry):
57 def __init__(
58 self, root,
59 validcharsfun,
60 minval,
61 convfuns,
62 incrvals, # (Up, Control-Up, Shift-Up)
63 *args, **kwargs):
64
65 _MyEntry.__init__(
66 self, root,
67 validcharsfun,
68 lambda txt: re.sub(r"[^0-9.:,eE+-]", "", txt), # for float, time.
69 *args, **kwargs)
70 self._minval = minval
71 self._fromstrfun, self._tostrfun = convfuns
72 self._incrvals = incrvals
73 self.bind("<Up>", lambda *args: self._up(0, *args))
74 self.bind("<Control-Up>", lambda *args: self._up(1, *args))
75 self.bind("<Shift-Up>", lambda *args: self._up(2, *args))
76 self.bind("<Down>", lambda *args: self._dn(0, *args))
77 self.bind("<Control-Down>", lambda *args: self._dn(1, *args))
78 self.bind("<Shift-Down>", lambda *args: self._dn(2, *args))
79
80 def _up(self, m, *args):
81 delta = self._incrvals[m]
82 try:
83 cur = self.get()
84 rndi = len(("%g" % delta).partition(".")[-1])
85 cur = round(self._fromstrfun(cur), rndi)
86 cur += delta
87 cur = max(self._minval, cur)
88 # FIXME: idealy, i want to set via textvariable...
89 self.delete(0, "end")
90 self.insert(0, self._tostrfun(cur))
91 except ValueError:
92 pass
93
94 def _dn(self, m, *args):
95 delta = self._incrvals[m]
96 try:
97 cur = self.get()
98 rndi = len(("%g" % delta).partition(".")[-1])
99 cur = round(self._fromstrfun(cur), rndi)
100 cur -= delta
101 cur = max(self._minval, cur)
102 # FIXME: idealy, i want to set via textvariable...
103 self.delete(0, "end")
104 self.insert(0, self._tostrfun(cur))
105 except ValueError:
106 pass
107
108
109 class _MyTSEntry(_MyNumEntry):
110 @staticmethod
111 def _vch(s):
112 return (re.match(r"^[\d:.]+$", s) is not None and
113 not re.search(r"([:.]{2,}|[:.][:.])", s) and
114 not re.search(r":\d{3,}", s) and
115 not re.search(r":[6-9]\d", s) and
116 s.count(":") <= 2 and s.count(".") <= 1 and
117 not re.search(r"\..*:", s))
118
119 def __init__(self, root, *args, **kwargs):
120 _MyNumEntry.__init__(
121 self, root,
122 _MyTSEntry._vch,
123 0,
124 (parse_time, _ts_to_tss),
125 (0.1, 1, 0.01),
126 *args, **kwargs)
127
128
129 class _MyFloatEntry(_MyNumEntry):
130 @staticmethod
131 def _vch(s):
132 # should we allow exponential form and negative values?
133 # i dont think so, in this application.
134 return (re.match(r"^[\d.]+$", s) is not None and
135 s.count(".") <= 1)
136
137 def __init__(
138 self,
139 root,
140 minval,
141 fmt,
142 incrvals,
143 *args, **kwargs):
144 _MyNumEntry.__init__(
145 self, root,
146 _MyTSEntry._vch,
147 minval,
148 (float, fmt.format),
149 incrvals,
150 *args, **kwargs)
注意: parse_time と _ts_to_tss があるので上記引用した部分だけだと動かんね、実際のものがみたければ ffchopreview.py 参照。それと例によって(?)Python 2.7 対応はフリだけ。実際は正しく動いてないか動くか確認してないかどちらか。
2020-11-02追記:
「不正入力文字が入ってるとクリアしちゃうのよ」問題について、なんというかこれぞ「バッドノウハウの極み」ではあるものの、かなり強引な措置は見つけた。相当ヒドくてムゴくてやべーやり口なので、覚悟の上で参考にすべし。これね。
動かしてみた結果から言えることは、どうやら「bind」の振る舞い(もしくはイベントの伝播の仕様)からは、元から内蔵されている「Ctrl-vへバインディングされた何かしら」は、「オレサマが新たに追加したハンドラ」のあとで動いているということになるね。どうなってるのかな、オリジナルが Entry に対してハンドラが登録されているのであれば、「オレサマの bind したもの」は先頭に挿入されてることになる。そういうことかね?
どっちかだと思うんだよね、このやや偏執狂的な異論を理解の上でこの例のようにテキストを詐称するか、もしくは「新たな入力を受け入れずに元の内容を維持する」。なんで不正入力文字が入ってるとクリアしちゃうのよ、ちぅかクリアするんならアンドゥできろよ、てことな。(クリアしちゃわないバリデータの書き方は…出来ない気がするんだけどどうだろう?)
まぁどういう作業の仕方をするかによるんだよねぇ。この tkinter アプリの外に例えば「時刻のリスト」がなんらか管理されてるとして、それを emacs で保守してる、とかの場合さ、たとえばその「時刻のリスト」が書かれているテキストはたとえば json 形式だったりして:
1 {
2 "inputs": [
3 {
4 "orig": "input1.mkv",
5 "trim": ["00:07:04.9", "00:07:16.074"],
6 "target_time": [
7 ["00:07:07.7", "00:07:11.9", 0],
8 ["00:07:11.9", "00:07:12.5", [1, "", "volume=0.0025"]],
9 ["00:07:15.0", "00:07:16.074", [1, "", "volume=0.0025"]]
10 ]
11 }
12 , {
13 "orig": "input2.mkv",
14 "trim": ["00:07:16.074", "00:12:53.3"],
15 "target_time": [
16 ["00:07:57.0", "00:07:57.5", [1, "", "volume=0.0025"]],
17 ["00:07:58.6", "00:07:59.5", [1, "", "volume=0.0025"]]
18 ]
19 }
20 , {
21 "orig": "input2.mkv",
22 "trim": ["00:12:53.3", "00:19:32.3"],
23 "target_time": [
24 ]
25 }
26 ]
27 }
ここの時刻部分を GUI にクリップボード経由でもっていきたい、って場合の話ね。たとえばコマンドラインの CUI に持ってくことも考えた場合、割合として引用符まで込みでクリップボードにコピーしたい場合の方が多いんだよね、ワタシの作業のスタイルの性質上。だからこれで引用符を入れてしまっただけで「全クリア」がもう、ストレスがたまっちゃってたまっちゃって。こりゃぁもう「核ミサイルの件」は気にせずフィルタしちまえよ、と思った、て話。