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

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

ただいまの
回答率

87.36%

TkInter: ツールチップテキストを生成するクラスを適用し、WIDGETに登場させることができたが 削除の方法が分からない

解決済

回答 1

投稿 編集

  • 評価
  • クリップ 0
  • VIEW 123

score 188

こちらの記事で紹介されたようにフレーム上のWIDGETへ ツールチップを登場させることに成功しました。

質問

画面遷移時、フレーム上のWIDGETを初期表示段階に元に戻すことを行っています。
(同じフレームを再度rootに呼び出すと 前回当該フレームに表れていたWIDGETが 前回のまま表示されるので)

動的にヒントテキストを生成できたのは 良かったのですが、生成したヒントテキストの削除方法が分からず
同じフレームを 呼出すと 前回登場させたヒントテキストが 残ってしまう問題が生じています。

ご紹介のクラスは ラベルのWIDGETを流用した作りなのでしょうが、どういった名前で作られているのか・どうすれば削除できるのかが分かりません。( pack_forget() の利用と推察するも)

当該フレームに 自分が定義したラベルもあるので 無条件にラベルを消すこともできません。
JQueryみたいに インデックス指定で要素を消すみないなことができますでしょうか?

削除方法 お分かりの方おられましたら ご教示をよろしくお願い致します。

1501 画像追加

イメージ説明

1730 お試しソース追加

# tkinterのインポート
import tkinter as tk
import tkinter.ttk as ttk
import math
import os
import datetime

class CreateToolTip(object):
    """
    create a tooltip for a given widget
    """
    def __init__(self, widget, text='widget info'):
        self.waittime = 500     #miliseconds
        self.wraplength = 180   #pixels
        self.widget = widget
        self.text = text
        self.widget.bind("<Enter>", self.enter)
        self.widget.bind("<Leave>", self.leave)
        self.widget.bind("<ButtonPress>", self.leave)
        self.id = None
        self.tw = None

    def enter(self, event=None):
        self.schedule()

    def leave(self, event=None):
        self.unschedule()
        self.hidetip()

    def schedule(self):
        self.unschedule()
        self.id = self.widget.after(self.waittime, self.showtip)

    def unschedule(self):
        id = self.id
        self.id = None
        if id:
            self.widget.after_cancel(id)

    def showtip(self, event=None):
        x = y = 0
        x, y, cx, cy = self.widget.bbox("insert")
        x += self.widget.winfo_rootx() + 25
        y += self.widget.winfo_rooty() + 20
        # creates a toplevel window
        self.tw = tk.Toplevel(self.widget)
        # Leaves only the label and removes the app window
        self.tw.wm_overrideredirect(True)
        self.tw.wm_geometry("+%d+%d" % (x, y))
        label = tk.Label(self.tw, text=self.text, justify='left',
                       background="#ffffff", relief='solid', borderwidth=1,
                       wraplength = self.wraplength)
        label.pack(ipadx=1)

    def hidetip(self):
        tw = self.tw
        self.tw= None
        if tw:
            tw.destroy()


#################################################################################
#       画面生成                                                                #
#################################################################################
def resource_path(relative_path):
    try:
        # PyInstaller creates a temp folder and stores path in _MEIPASS
        base_path = sys._MEIPASS
    except Exception:
        base_path = os.path.abspath(".")

    return os.path.join(base_path, relative_path)

def adjust_windowsize(root):
    ww = root.winfo_screenwidth()
    wh = root.winfo_screenheight()

    lw = math.ceil(ww * 0.3208)
    lh = math.ceil(wh * 0.477)

    root.geometry(str(lw)+"x"+str(lh)+"+"+str(int(ww/2-lw/2))+"+"+str(int(wh/2-lh/2)))

#################################################################################
#       フレーム生成                                                            #
#################################################################################
def generate_frame(root):
    # フレーム切替え=画面遷移
    def change_frame(frame):
        if frame.winfo_name() == "frmIOMenu":
            btn_AssignInputFile.focus_set()
            initial_frmIOMenu()
        elif frame.winfo_name() == "frmConvMenu":
            btn_ReturnMenu.focus_set()
            initial_frmConvMenu()
        else:
            btn_RunMenu.focus_set()

        frame.tkraise()
    #############################################################################
    #      画面制御関数 変換定義                                                #
    #############################################################################
    # 初期化処理
    def initial_frmConvMenu():
        # 漏れ
        cmbox_Key.delete(0, tk.END)

        global cmbox_Key_ttp
        #if cmbox_key_ttp.winfo_exist():
        #    cmbox_key_ttp.hidetip()
        cmbox_Key_ttp.text = ""

    # コンボボックス選択
    def cmbox_KeyChange(event=None):

        cmbox_Key_ttp.text = event.widget.get()


    #############################################################################
    #      STYLE                                                           #
    #############################################################################
    style = ttk.Style()
    style.theme_use('winnative')
    style.configure("TCombobox", arrowsize=0)
    style.configure("TButton", font=("Arial", 16))
    style.configure("TFrame", background="#FFFFCC")
    style.configure("TRadiobutton", background="#FFFFCC")
    style.configure("TLabel", font=("Arial", 16), anchor='', background="#FFFFCC")
    style.configure("TLabelframe", background="#FFFFCC", relief="sunken")
    style.configure("TLabelframe.Label",  foreground="red", font=("Arial", 16), background="#FFFFCC")
    style.configure("LTREE.Treeview", background="black", foreground="white", fieldbackground="black", font=("Arial", 16), rowheight=25,)
    style.configure("UTREE.Treeview", background="white", foreground="black", fieldbackground="white", font=("Arial", 16), rowheight=25,)

    style.configure("Treeview.Heading", background="green", foreground="white", font=("Arial", 16), rowheight=25,)

    #############################################################################
    #       メインメニュー用フレーム設置                                        #
    #############################################################################
    frmMain = ttk.Frame(root, name="frmMain")
    frmMain.grid(row=0, column=0, sticky=tk.E + tk.W + tk.N + tk.S)

    # メインメニューにボタン配置
    btn_SettingMenu = ttk.Button(frmMain, text = "入出力定義")
    btn_SettingMenu.pack(fill = tk.BOTH, expand=True)

    btn_SettingMenu = ttk.Button(frmMain, text = "変換定義", command=lambda: change_frame(frmConvMenu))
    btn_SettingMenu.pack(fill = tk.BOTH, expand=True)
    btn_SettingMenu.bind('<Return>', lambda a: change_frame(frmConvMenu))

    btn_RunMenu = ttk.Button(frmMain, text = "実行メニュー")
    btn_RunMenu.pack(fill = tk.BOTH, expand=True)
    btn_RunMenu.focus_set()

    btn_Close = ttk.Button(frmMain, text = "終了", command=root.destroy)
    btn_Close.pack(fill = tk.BOTH, expand=True)
    #############################################################################
    #       変換対応表メニュー用フレーム設置                                    #
    #############################################################################
    frmConvMenu = ttk.Frame(root, name="frmConvMenu")
    frmConvMenu.grid_rowconfigure([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11], weight=1)
    frmConvMenu.grid_columnconfigure([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], weight=1, minsize=40)
    frmConvMenu.grid(row=0, column=0, sticky=tk.E + tk.W + tk.N + tk.S)

    # 1 Key選択コンボボックス
    tmp_list = [12345, 22345, 32345, 42345, 52345]
    cmbox_Key = ttk.Combobox(frmConvMenu, width=3, height=3, font=("Arial", 16), values=tmp_list)
    cmbox_Key.grid(row=1, column=6, sticky=tk.E + tk.W + tk.S)
    cmbox_Key.bind("<<ComboboxSelected>>", cmbox_KeyChange)

    global cmbox_Key_ttp
    cmbox_Key_ttp = CreateToolTip(cmbox_Key, cmbox_Key.get())

    # 11 メニューへボタン
    btn_ReturnMenu = ttk.Button(frmConvMenu, text = "閉じる", command=lambda: change_frame(frmMain))
    btn_ReturnMenu.grid(row=11, column=8, sticky=tk.E + tk.W, padx=(0,10))


    # 起動時メインのフレームを前面に
    frmMain.tkraise()

##########################################################################################################
#      TkInter GUIアプリケーション起動                                                         #
##########################################################################################################
if __name__ == "__main__":
    # ウインドウの作成
    root = tk.Tk()

    #フォームサイズを実行端末から導き、ド真中に配置表示
    adjust_windowsize(root)

    #タイトルを指定
    root.title("TkInterの勉強")

    #フレーム切替え達成の上で とても重要、ルートのグリッド定義
    root.grid_columnconfigure(0, weight=1)
    root.grid_columnconfigure(1, weight=1)
    root.grid_columnconfigure(2, weight=1)
    root.grid_columnconfigure(3, weight=1)
    root.grid_columnconfigure(4, weight=1)
    root.grid_columnconfigure(5, weight=1)
    root.grid_columnconfigure(6, weight=1)
    root.grid_columnconfigure(7, weight=1)
    root.grid_columnconfigure(8, weight=1)
    root.grid_columnconfigure(9, weight=1)

    root.grid_rowconfigure(0, weight=1)
    root.grid_rowconfigure(1, weight=1)
    root.grid_rowconfigure(2, weight=1)
    root.grid_rowconfigure(3, weight=1)
    root.grid_rowconfigure(4, weight=1)
    root.grid_rowconfigure(5, weight=1)
    root.grid_rowconfigure(6, weight=1)
    root.grid_rowconfigure(7, weight=1)
    root.grid_rowconfigure(8, weight=1)
    root.grid_rowconfigure(9, weight=1)
    root.grid_rowconfigure(10, weight=1)
    root.grid_rowconfigure(11, weight=1)

    #フォームの最大化、×ボタン操作を無効化
    root.resizable(0,0)
    root.protocol('WM_DELETE_WINDOW', (lambda: 'pass')())

    # カーソル変更
    root["cursor"] = "hand2"

    generate_frame(root)

    root.mainloop()
# 漏れ
cmbox_Key.delete(0, tk.END)
  • 気になる質問をクリップする

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

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

    クリップを取り消します

  • 良い質問の評価を上げる

    以下のような質問は評価を上げましょう

    • 質問内容が明確
    • 自分も答えを知りたい
    • 質問者以外のユーザにも役立つ

    評価が高い質問は、TOPページの「注目」タブのフィードに表示されやすくなります。

    質問の評価を上げたことを取り消します

  • 評価を下げられる数の上限に達しました

    評価を下げることができません

    • 1日5回まで評価を下げられます
    • 1日に1ユーザに対して2回まで評価を下げられます

    質問の評価を下げる

    teratailでは下記のような質問を「具体的に困っていることがない質問」、「サイトポリシーに違反する質問」と定義し、推奨していません。

    • プログラミングに関係のない質問
    • やってほしいことだけを記載した丸投げの質問
    • 問題・課題が含まれていない質問
    • 意図的に内容が抹消された質問
    • 過去に投稿した質問と同じ内容の質問
    • 広告と受け取られるような投稿

    評価が下がると、TOPページの「アクティブ」「注目」タブのフィードに表示されにくくなります。

    質問の評価を下げたことを取り消します

    この機能は開放されていません

    評価を下げる条件を満たしてません

    評価を下げる理由を選択してください

    詳細な説明はこちら

    上記に当てはまらず、質問内容が明確になっていない質問には「情報の追加・修正依頼」機能からコメントをしてください。

    質問の評価を下げる機能の利用条件

    この機能を利用するためには、以下の事項を行う必要があります。

回答 1

checkベストアンサー

+2

どういった名前で作られているのか

cmbox_Key_ttp = CreateToolTip(cmbox_Key, cmbox_Key.get()) 

としたのであれば、削除は cmbox_key_ttp.hidetip() です。

問題点: 本来は <Enter> <Leave> イベントで非表示になるのですが、
他のイベントをトリガーに表示させた場合、マウスカーソルが予め対象のウィジェットの枠外にある場合、
想定された挙動になりません。

ボタンクリックイベントでツールチップを表示した場合、
カーソルはウィジェットの枠内なので、他のウィジェットに移った時には
<Leave>イベントが呼ばれ、削除されます。

コンボボックスで「選択時」とした場合、マウス選択の場合はドロップダウンリストにカーソルがある為
選択後はコンボボックス外が開始位置となります。
<Leave> イベントは、ウィジェット内のカーソルが外に出た時なので、イベントは起こりません。
コンボボックスにカーソルを一度当ててから、外に出すとツールチップは削除されるはずです。

対策:

  • ツールチップの生成を毎回ではなく、一度のみにして再利用する
  • もしくは、ツールチップ生成時に前のツールチップを削除

実装方法は複数考えられます (どれかひとつ)

  • cmbox_key_ttp をグローバルにする。
  • self.tw をクラス変数にする (HINT: オブジェクト指向、シングルトンパターン)
  • ウィジェットにnameを付けて、再生成を抑制する。説明は後述

Pythonの変数とは別に、tcl/tk 側での名前もあります。

root = tk.Tk()
frame = tk.Frame(root)
button = tk.Button(frame)

# tcl/tk 側での識別名、特に指定がない場合は 2つ目以降は連番
# ファイルのフルパスの様な、ドット区切りの階層表記
print(root) # .
print(frame) # .!frame
print(button) # .!frame.!button

# 「同じ親」で「同じ名前」を付けると、2回目は再生成ではなく、既存のオブジェクトになります

button1 = tk.Button(root, name="test_button")
button2 = tk.Button(root, name="test_button") # 一つ目と同じボタン

# Toplevel に同じ親・同じ名前を与えれば、2つ目のツールチップとはならず
# 一つ目のツールチップを再利用します。
# 但し、コードは2回実行されるので、ラベル生成やイベント登録が2回実行されないように
# 対処は必要です。

ウィジェット生成時に name= オプションで任意の名前を付けられ、
nametowidgetメソッドで任意の名前のtkinterのオブジェクトにアクセスできます。


想定するツールチップが残り続ける問題のあるコード

import tkinter as tk
from tkinter import ttk

# ここで class CreateToolTip を定義 (投稿時の文字数制限のため省略)
from tooltip import CreateToolTip

root = tk.Tk()
combo = ttk.Combobox(root, values=["ITEM01", "ITEM02"])
combo.pack()

## 問題個所 ... 毎回ツールチップを生成する
def onSelect(event):
    tooltip = CreateToolTip(combo, combo.get())

combo.bind("<<ComboboxSelected>>", onSelect)
root.mainloop()

コメントの別案として提示した解消法

import tkinter as tk
from tkinter import ttk

# ここで class CreateToolTip を定義 (投稿時の文字数制限のため省略)
from tooltip import CreateToolTip

root = tk.Tk()
combo = ttk.Combobox(root, values=["ITEM01", "ITEM02"])
combo.pack()

## 解決案
tooltip = CreateToolTip(combo) # 予め作成しておく
def onSelect(event):
    tooltip.text = event.widget.get() # ToolTip の内容のみ変更

combo.bind("<<ComboboxSelected>>", onSelect)
root.mainloop()

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2021/11/27 17:39

    teamiklさん 何から何までご対応頂き 誠にありがとうございます。
    仰られるように 試しにdefaultのテーマを設定してみたら コンボボックスの矢印表示がなくなりました。
    これ以上本件で ご迷惑をおかけするのは大変申し訳ないので ある程度自分で考え 場合によって諦めようと思うのですが、arrowsizeの調整から コンボボックス内選択アイテムを より幅広く見せる方針ではなく
    state=tk.DISABLED時みたいに 矢印を最上位に表示させ 選択アイテムを強引に見切れた表示にできないものですかね

    前回の解決時も矢印は現れていた記憶があるので 何かの作用で 矢印を最上位に表示させることができ
    、選択アイテムを見切れた表示にさせるうことができたのかも

    色々とありがとうございました。

    キャンセル

  • 2021/11/27 21:54

    矢印だけ上にというのは難しそうです。
    コンボボックスの矢印の部分は独立したボタンウィジェットではなく、
    入力部分のスタイルの要素として実装されてる為、
    外部から詳細な制御ができるような構成になってません。

    ttk のスタイルの実装内に手を入れれば対応出来るかもしれませんが、
    この辺りは情報が少なく、ライブラリの実装内部を調べる事になります。
    内部というのは tkinter (Python言語)ではなく、テーマのリソースを定義してる Tcl/Tk 言語や、
    場合によっては スタイルエンジンを実装してる C言語のソースに及びます。

    他の代案については、前回の質問の回答に書いた通りですが、
    独自にウィジェットを作った方が良いかもしれないです。

    キャンセル

  • 2021/11/28 01:55

    了解致しました、もう一度元の記事を読み直します。

    きっと前回DISABLEDにした状況で ぬか喜びしていたのかもしれません。

    キャンセル

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

  • ただいまの回答率 87.36%
  • 質問をまとめることで、思考を整理して素早く解決
  • テンプレート機能で、簡単に質問をまとめられる

関連した質問

同じタグがついた質問を見る

  • トップ
  • Pythonに関する質問
  • TkInter: ツールチップテキストを生成するクラスを適用し、WIDGETに登場させることができたが 削除の方法が分からない