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

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

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

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

Python

Pythonは、コードの読みやすさが特徴的なプログラミング言語の1つです。 強い型付け、動的型付けに対応しており、後方互換性がないバージョン2系とバージョン3系が使用されています。 商用製品の開発にも無料で使用でき、OSだけでなく仮想環境にも対応。Unicodeによる文字列操作をサポートしているため、日本語処理も標準で可能です。

Q&A

解決済

2回答

2337閲覧

【tkinter】ラベルに表示する文字列が、既定の文字数を超えた場合は末尾を「...」で省略させる方法【Python】

netz-eng

総合スコア105

Tkinter

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

Python

Pythonは、コードの読みやすさが特徴的なプログラミング言語の1つです。 強い型付け、動的型付けに対応しており、後方互換性がないバージョン2系とバージョン3系が使用されています。 商用製品の開発にも無料で使用でき、OSだけでなく仮想環境にも対応。Unicodeによる文字列操作をサポートしているため、日本語処理も標準で可能です。

1グッド

0クリップ

投稿2022/05/06 08:08

編集2022/05/06 18:59

実現したいこと

tkinterにて、ラベルに表示する文字列を、既定の文字数を超えたら末尾を「...」で省略して表示したいのですが、方法がわかりません。

今回の例では、以下の要件を満たしたラベルを実装したいと思っています。
ラベルを2行まで表示することとする
wraplengthでラベル幅に合わせて改行し、2行を超える場合は、2行目の末尾を「...」に変えて表示

(元の文字列をそのまま表示した場合)
イメージ説明
(理想)
イメージ説明

該当のソースコード

Python

1import tkinter as tk 2 3app = tk.Tk() 4 5w = 25 6lbl = tk.Label(app, text="文字列文字列文字列文字列文字列文字列文字列文字列文字列...", width=w, 7 wraplength=int(w*140/20), anchor=tk.W, justify=tk.LEFT) 8lbl.grid(row=0, column=0) 9 10app.mainloop()

試したこと

フォントを游明朝 Demiboldとし、
font.measureメソッドで文字列長さを算出しました。

折り返し位置に指定したwraplengthの値は175ピクセルですが、
font.measureメソッドで算出した1行分の値は252~274(単位不明)でした。

(1)デフォルトのフォントで文字列長さを比較したところ、wraplength<1行の文字列長となることを確認しました。

Python

1from tkinter import font as tkFont 2 3# 日本語、英語でそれぞれTextウィジェット幅×1行分の長さ 4tex = "文字列文字列文字列文字列文字" 5tex2 = "StringStringStringStringStringStri" 6 7# f = tkFont.Font(family="游明朝 Demibold") 8f = tkFont.nametofont(lbl["font"])   <- ラベルのデフォルトフォントを取得 9# font.measureメソッドで文字列長さを算出 10w1 = f.measure(tex) 11w2 = f.measure(tex2) 12 13# 日本語、英語の1行分の長さ(単位:ピクセル) 14print(w1) 15>168 16print(w2) 17>172 18 19# wraplengthで指定した文字列折り返しの長さ(単位:ピクセル) 20print(w*140/20) 21>175 22

(2)「游明朝 Demibold」フォントで長さを比較しようとしたところ、エラーが発生してしまいました。

Python

1# 游明朝 Demiboldに合わせた1行分の折り返しピクセル数 2wraplength = w * 8 3 4lbl = tk.Label(app, text=tex2, width=w, font=("游明朝 Demibold", 10, ""), 5 wraplength=wraplength, anchor=tk.W, justify=tk.LEFT) 6font = tkFont.nametofont("游明朝 Demibold") 7 8>> _tkinter.TclError: named font 游明朝 Demibold does not already exist 9

補足情報(FW/ツールのバージョンなど)

Windows11
Python 3.9.7
tk 8.6.11

teamikl👍を押しています

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

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

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

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

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

guest

回答2

0

ベストアンサー

標準ライブラリの textwrap.shorten 関数が使えます
https://docs.python.org/ja/3/library/textwrap.html

  • 横幅は wraplength で指定 (単位はピクセル)
  • フォント情報から文字数を計算 (font.measureメソッド)
  • 2行目の末尾迄の文字数を計算 (横幅 x2 から "..." の幅を引いた数)

「既定の文字数」の算出が難しく、文字数が固定でも良いなら容易なのですが、(要件次第)
文字数を幅に合わせたい場合、横幅の単位 ピクセル数 <=> 文字数の計算が少し手間のかかる箇所です。
等幅フォントでない場合は、固定値にすることができません。
フォントサイズと横幅(ピクセル)からある程度辺りを付けて、
一文字ずつ増やし font.measure メソッドで計算、丁度良い文字数を探します。

wraplength を指定しない場合は、改行位置がウィンドウのリサイズの影響を受ける為、
<Configure> イベント内でリサイズの度に再計算します。

投稿2022/05/06 09:23

teamikl

総合スコア8664

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

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

netz-eng

2022/05/06 13:01 編集

ご回答ありがとうございます。 ウィンドウはリサイズ不可にするつもりなので、私の例では再計算を考慮する必要はありません。 質問文に、1行分の文字列長さをfont.measureメソッドで算出した結果を追記しました。 結果、font.measure結果と、wraplengthで指定したピクセル値とでは整合性が見られませんでした。 font.measureの単位は何でしょうか?
teamikl

2022/05/06 17:03 編集

> font.measureの単位は何でしょうか? font.measure と wraplength 共に、単位はピクセル数です。 >font.measure結果と、wraplengthで指定したピクセル値とでは整合性が見られませんでした。 実際の表示幅なので、フォントの影響を受ける点に留意してください。 質問に掲載のコードでは wraplength 指定時のfontが未指定です。 (デフォルトのフォント設定が適応されます) font次第で一行に表示される文字数も変わります。 質問の上のコードで font.measure を使う場合 font = tkinter.font.nametofont(lbl["font"]) font.measure("文字列文字列文字列文字列文字") # => 168 と、必ず wraplength 以下の値になるはずです。
netz-eng

2022/05/06 19:01

>質問に掲載のコードでは wraplength 指定時のfontが未指定です。 確かに、失しておりました。以下、確認した事項を質問文のほうにも追記いたしました。 (1)フォントを指定せず、 font = tkFont.nametofont(lbl["font"]) font.measure("文字列文字列文字列文字列文字") を実行したところ、おっしゃる通り1行分の文字列長がwraplength以下になりました。 (2)次にLabelのフォントを「游明朝 Demibold」としたところ、以下のエラーが発生しました。 font = tkFont.nametofont("游明朝 Demibold") => _tkinter.TclError: named font 游明朝 Demibold does not already exist  このため、「游明朝 Demibold」でのwraplengthが取得できておりません。  任意のフォントにおける値を取得するにはどうすればよいのでしょうか?
teamikl

2022/05/07 03:30 編集

nametofont は定義済みのフォント (label["font"])に対し使うので 新規の場合は font = Font("游明朝 Demibold") で作成します。 別のフォントを適応する場合は1行の文字数が変わるはずなので、 Labelに対しての font 指定が必要になります。 lbl = tk.Label(..., wraplength=175, font=font)
netz-eng

2022/05/07 19:25

ありがとうございます。 無事、「游明朝 Demibold」でのwraplengthを求めることができました。 重ねてお尋ねして申し訳ないのですが、 既定の文字数を超過した文字列tex(例えば200px)に対し、これを100pxで切り上げたい場合、 合計200pxのtexの内、「どこまでの文字数が100pxに収まるのか」はどのように取得すればよいでしょうか? (単純なピクセル数と文字数の比例計算は、日本語英語交じりの文で正確さに欠けるため避けたいです) 以下のように文字列をループして、100pxに収まった時点で処理する方法を考えたのですが、いささか冗長に感じています。 for i in range(len(tex)): if font.measure(tex[:i]) < 100 < font.measure(tex[:i+1]): # 切り上げ処理
teamikl

2022/05/08 02:41 編集

固定値にする場合の回避策は、等幅フォントが簡易的な方法ですが。 もしアルゴリズム的な最適解を求めるなら「配列二分法」を調べて見て下さい。 標準ライブラリに bisect モジュールがあります。 操作対象がリストなので、そのまま利用できるか解りませんが、 考え方的には同じで、中央値もしくは中間値を取って大小を比較していきます。 想定: "テストabcdあいうえお" 80px以下に収まる文字数を調べたいとします。 日本語10px 英字5px 余白は未考慮。説明の為の簡易的な想定をして 文字幅のリスト: 0, 10, 20, 30, 35, 40, 45, 50, 60, 70, 80, 90, 100 中央値との比較: 45 < 80 idx=6 (0..12間の中央) 中央値との比較: 70 < 80 idx=9 (7..12間の中央) 中央値との比較: 90 > 80 idx=11 (10..12間の中央) 9..11 の間なので idx=10 で、10文字 ※ 追記: 省略文字 ... 分の幅を含め忘れ。実際には想定幅 - ... の幅。 但し、上記の数列を得るには font.measure を文字数の回数だけ呼び出す事になるので 明らかに冗長になります。(計算量的にも O(n) なので、一つづつ調べるのと変わらない) リストの中央値ではなく、最大値の幅からある程度の中間値の予測を付ける方が 今回のケースでは効率は良くなると思います。(font.measure の呼出回数を出来る限り減らす) また明らかに長すぎる文字列の場合は無駄が多くなるので、 前処理として、予め枝切りしておく等の処理も有効です。 正確でなくても良いので、計算量を減らす為に、ピクセル数と文字数から辺りを付けて 明らかに不要な長さを省きます。正確な長さはその後に求めます。 対象の規模が2行程度であれば、おおよその辺りを付けてから、 前後を一文字づつ順に調べるでも十分な効率化が図れると思います。 検索のヒント: 二分探索、バイナリーサーチ
teamikl

2022/05/08 10:41 編集

> for i in range(len(tex)): if font.measure(tex[:i]) < 100 < font.measure(tex[:i+1]): 上記のコードの冗長な部分は、 文字列のスライスを生成してる箇所と font.measure をループ辺り2回呼び出している点ですね。 境界値の扱い(幅が丁度100の場合)も気になりますが、 普通に1文字づつ増やしていく探索でも、半分にはなります。(計算量オーダーは変わりません) for i in range(len(tex)):  if font.measure(tex[:i]) < 100:   break
teamikl

2022/05/08 03:16

日本語とアルファベット入り混じり、且つ2行の場合は 追加で、改行位置にも配慮が必要かもしれません。 tkinter.Text には改行位置の抑制オプションが有るのですが、 Label の場合はなさそうなので、 英単語の改行位置が font.measure で調べた文字数と一致しない可能性があります。
netz-eng

2022/05/08 10:17 編集

詳細に方法論を教えていただき、ありがとうございます。 結論から申し上げると、以下のコードでwraplengthに最も近い文字列を抽出、ラベルに反映できました。 for i in range(len(tex)):  if not font.measure(tex[:i]) < 100:   if font.measure(tex[:i]) == wraplength:    y = t[:i]    break   elif font.measure(tex[:i]) > wl:    y = tex[:i-1]    break lbl.configure(text=y) >日本語とアルファベット入り混じり、且つ2行の場合は >追加で、改行位置にも配慮が必要かもしれません。 確認してみると、確かに文字の配列によってwraplength以下の文字列であっても3行目に突入してしまうものがあったので、 ↑のwraplengthをあらかじめ小さめに設定しておくことで、(更に「y = y + "..."」を追加しても)2行に収まるよう調整することができました。本当にありがとうございました!
teamikl

2022/05/08 11:29 編集

目的を達成できたようで良かったです。 以下は余談になりますが、コードの改善ポイントについて 実行効率的には先程よりも冗長になってるので、 - font.measure の計算結果を複数回使う場合は、一時変数に代入する等して、 font.measure の呼び出し回数を1ループ毎に一回以内としてください。 - if文での比較回数も 3 回になってますが、整理すれば境界値以下の 1 回で済むはずです。 - 探索アルゴリズムの「枝狩り」に関しては、例えば range(25, min(175, len(tex))) みたいにすると 明らかに不要な場合(24文字以下 175文字以上)の処理を省けます。25, 175の部分は要調整。 len(tex) が開始値より小さい場合は省略の必要がないので、ループを実行しません。
netz-eng

2022/05/10 12:34

ご助言ありがとうございます。 >- font.measure の計算結果を複数回使う場合は、一時変数に代入する等して、 おっしゃる通りですね。変数を使って書き直します。 >- if文での比較回数も 3 回になってますが、整理すれば境界値以下の 1 回で済むはずです。 if文で実行されない部分は、てっきり処理が省略? されていると思い込んでおりました。改善します。 また文字列ピクセル数が境界値と一致する場合と、そうでない場合で切り上げる文字数が異なるので、比較回数は最低2回必要と認識しています。 >- 探索アルゴリズムの「枝狩り」に関しては その作業を「枝狩り」と表現することを、初めて知りました。ループ数の省略はやったほうがいいことは理解していたのですが、自分のなかで方法が具体化せず放置していました。 ご提示いただいた書き方を参考に、開始値と終了値を設定することでループ処理に掛かる時間を(ばらつきはあるものの)半減させられました。重ね重ね、ありがとうございます。
teamikl

2022/05/11 03:34 編集

オフトピなので適当なとこで切り上げたほうが良いかと思いつつ、 アルゴリズムとか最適化の話は個人的にプログラミングの楽しい部分なので、もう少しだけ > そうでない場合で切り上げる文字数が異なる ... 比較回数は最低2回必要と認識しています。 ここは私の認識不足でしたが、その場合でも境界値の場合は for 文の外で一回のみにできます ループ回数が N として、1ループに2回チェックだと 2*N 回ですが、 N+1 回に抑えられるはずです。 多分、チェックは2回でも処理は3箇所に書くことになるのではという懸念が出ると思いますが for文内に処理を直接書くのではなく、「適切な文字数を探索する関数」と別けて実装すると 関数のテストがしやすくなり、余分なコードも省けます。 > その作業を「枝狩り」と表現することを、初めて知りました。 アルゴリズムの用語で合ってるかどうかは自信ないですが、 計算量を減らすために明らかに不要になる探索を省く、という意図で枝刈りと用いました。 木構造のような再帰探索で、描画すると木から枝が伸びていくような描写になるので 途中で探索を打ち切る→枝刈りと表現されます。 アルゴリズム的に計算量を半減させるなら、for i in range(min, max, 2) でループを回し 最後に i-1 をチェックすれば、およそ半減ですね。 それをより効率化したものが 最初に説明した二分探索になります。 とはいえ、全体から見ると時間かけて詰める必要はない部分なので、 最悪のケースを想定して max 時の値が巨大にならなければ大丈夫です。 ※ループに時間がかかり過ぎるとGUIが応答なしになります。
netz-eng

2022/05/12 11:50

プログラミング関連はほぼ独学なので、いろいろ教えていただけてありがたいです。 >for文内に処理を直接書くのではなく、「適切な文字数を探索する関数」と別けて実装すると おっしゃっている意味がようやく分かりました。処理を分けて記述するため少々面倒ではありますが、確かに「N+1回」のほうが処理は速く、また安定しているようです。 >アルゴリズム的に計算量を半減させるなら、for i in range(min, max, 2) でループを回し 恥ずかしながら、rangeに刻みの概念があることを初めて知りました。確かにこの方法なら、不要な処理を大幅に省くことができますね。 いま考えているGUIで、この「末尾...化関数」を連続で呼び出すことがあるので、実装してみて処理時間を短縮する必要がある場合は、ご教示いただいた方法を試してみようと思います。
guest

0

python

1import tkinter as tk 2 3app = tk.Tk() 4 5w = 25 6text = "文字列文字列文字列文字列文字列文字列文字列文字列文字列..." 7lbl = tk.Label(app, text=f'{text[:w]}{"..." if len(text)>w else ""}', width=w, 8 wraplength=w*140//20, anchor=tk.W, justify=tk.LEFT) 9lbl.grid(row=0, column=0) 10 11app.mainloop()

投稿2022/05/06 09:14

melian

総合スコア19769

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

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

netz-eng

2022/05/06 12:46

ご回答ありがとうございます。 wは文字数なので、ご回答いただいたコードでは日本語や英字が入り混じった文では「...」が入る場所が変わってしまい、私の実現したい結果にはなってくれないようです。
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

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

ただいまの回答率
85.48%

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

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

質問する

関連した質問