61340


Tkで準備されているmessageboxではなく、カスタムメッセージボックスを作りたいと考えています。

要件としては、

  1. 常にTopLevelであること  
アプリケーションのコアとなるTkオブジェクトよりも、メッセージボックスが表示されている間は、メッセージボックスが前面にしたいです。

  2. メッセージボックス以外の操作が不可（モーダル）  
メッセージボックスが押されるまで、他のTKオブジェクトは操作できないようにしたい。

  3. メッセージボックスの戻り値を待つ  
メッセージボックスの入力を待ってから次のアクション（ウィジットの表示/非表示等）に移りたい。

  4. アプリケーションの処理は継続したい  
アプリケーションのコアとなるTkオブジェクトは、afterメソッドにより定期的な処理は継続したい。

  5. カスタムメッセージボックスには標準のタイトルバーはつけたくない。  

このようなメッセージボックスをカスタムで作成したいのですが、うまくいきません。

**実行結果**  
メッセージボックスは、モーダルにならず、閉じると以下のエラーになります。

[code]

    invalid command name "1469565913800dialog_mouse_release"
        while executing
    "1469565913800dialog_mouse_release 138 1 ?? ?? ?? 264 98160984 ?? 19 21 ?? 0 ?? ?? .!frame3.!button2 5 543 480 ??"
        invoked from within
    "if {"[1469565913800dialog_mouse_release 138 1 ?? ?? ?? 264 98160984 ?? 19 21 ?? 0 ?? ?? .!frame3.!button2 5 543 480 ??]" == "break"} break"
        (command bound to event)
    
[/code]

**ソースコード**

[code]

    #! /usr/bin/env python3
    # -*- coding: utf-8 -*-
    
    import tkinter as tk
    from tkinter import ttk
    
    from tkinter import messagebox
    
    from PIL import Image,ImageTk,ImageDraw,ImageFont
    
    import datetime
    
    class CustomDialog(object):
        def __init__(self):
    
            self.title_bar_color = '#8FAADC'
            self.item_ground_color = 'whitesmoke'
            self.background_color = '#D9D9D9'
            self.select_bar_color = '#BDD7EE'
    
            self.isDrag_DlgMotion = False
            self.drag_dx = 0
            self.drag_dy = 0
        
        def dialog_left_click(self,event):
            dialog_x=self.dev_dialog.winfo_rootx()
            dialog_y=self.dev_dialog.winfo_rooty()
            point_x=self.dev_dialog.winfo_pointerx()
            point_y=self.dev_dialog.winfo_pointery()
    
            dx = point_x - dialog_x
            dy = point_y - dialog_y
    
            if (dx >= 0 and dx <= self.title_bar_width) and (dy >= 0 and dy <= self.title_bar_height):
                self.drag_dx = dx
                self.drag_dy = dy
                self.isDrag_DlgMotion = True
            return
    
        def dialog_mouse_move_on(self,event):
            if self.isDrag_DlgMotion:
                X = event.x_root - self.drag_dx
                Y = event.y_root - self.drag_dy
                self.dev_dialog.geometry('+{0}+{1}'.format(X, Y))
                pass
            return
    
        def dialog_mouse_release(self,event):
            if self.isDrag_DlgMotion:
                self.isDrag_DlgMotion = False
            return
    
    class CommonMessageBoxDialog(CustomDialog):
        def __init__(self,title,message,state,parent = None):
            self.return_state = None
    
            if not isinstance(title,str) or not isinstance(message,str) or not isinstance(state,int):
                return
            if state < 1 or state > 3 :
                return
    
            root = ttk.tkinter.Tk()
    
            #root = tk.Toplevel(parent)
    
            super().__init__()
            
            self.box_state = state
            self.box_message = message
            self.box_title = title
    
            W = 0
            H = 1
    
            self.dlg_size = [400,200]
    
            self.title_bar_width = self.dlg_size[W]
            self.title_bar_height = 40
    
            self.btn_bar_height = 42
    
            self.btn_32x32_size = 42
    
            self.row_height = 28
            self.btn_row_height = 32
    
            self.frm_space = 10
    
            self.parent = parent
            self.CreateDialog(root)
    
            root.mainloop()
        
        def CreateDialog(self,root):
    
            W = 0
            H = 1
    
            if self.parent != None:
                self.parent.update_idletasks()
                ww=self.parent.winfo_screenwidth()
                wh=self.parent.winfo_screenheight()
                x=self.parent.winfo_rootx()
                y=self.parent.winfo_rooty()
    
                parent_w = self.parent.winfo_width()
                parent_h = self.parent.winfo_height()
                parent_x = self.parent.winfo_x()
                parent_y = self.parent.winfo_y()
            else:
                root.update_idletasks()
                ww=root.winfo_screenwidth()
                wh=root.winfo_screenheight()
                x=root.winfo_rootx()
                y=root.winfo_rooty()
    
                parent_w = root.winfo_width()
                parent_h = root.winfo_height()
                parent_x = root.winfo_x()
                parent_y = root.winfo_y()
    
    
            self.dev_dialog = root
            dialog = self.dev_dialog
            dialog.overrideredirect(True)
    
            dlg_x = int((parent_x+parent_w) - (self.dlg_size[W]/2))
            dlg_y = int((parent_y+parent_h) - (self.dlg_size[H]/2))
    
            if dlg_x < 0 : dlg_x = 0 
            if dlg_y < 0 : dlg_y = 0 
    
            dialog.geometry('{}x{}+{}+{}'.format(self.dlg_size[W],self.dlg_size[H],dlg_x,dlg_y))
    
    
            self.Title_Bar = tk.Frame(
                dialog,            
                relief='flat',
                bg = self.title_bar_color ,
                )
            self.Title_Label = tk.Label(
                self.Title_Bar,
                bg = self.title_bar_color ,
                text = self.box_title,
            )
    
            dialog.bind('<Button-1>', self.dialog_left_click)
            dialog.bind('<B1-Motion>', self.dialog_mouse_move_on)
            dialog.bind('<ButtonRelease-1>',self.dialog_mouse_release)
    
            self.MsgArea_frame = tk.Frame(
                dialog,            
                relief='flat',
                bg = self.select_bar_color,
                )
            self.message_frame = tk.Frame(
                self.MsgArea_frame,
                relief='flat',
                bg = self.item_ground_color ,
                )
            self.label_message = tk.Label(
                self.message_frame,
                bg = self.item_ground_color ,
                text = self.box_message,
            )
    
            self.BtnArea_frame = tk.Frame(
                dialog,            
                relief='flat',
                bg = self.item_ground_color,
                )
                
            self.btn_ok = tk.Button(
                self.BtnArea_frame,            
                bg = self.item_ground_color,
                text = 'OK',
                command = lambda:self.btn_msgbox_clicked(1),
                )
                
            self.btn_yes = tk.Button(
                self.BtnArea_frame,            
                bg = self.item_ground_color,
                text = 'YES',
                command = lambda:self.btn_msgbox_clicked(1),
                )
                
            self.btn_no = tk.Button(
                self.BtnArea_frame,            
                bg = self.item_ground_color,
                text = 'NO',
                command = lambda:self.btn_msgbox_clicked(2),
                )
                
            self.btn_cancel = tk.Button(
                self.BtnArea_frame,            
                bg = self.item_ground_color,
                text = 'CANCEL',
                command = lambda:self.btn_msgbox_clicked(3),
                )
                    
            frm_space = self.frm_space
            msg_frm_w = 4
            btn_fram_h = 36
    
            message_area_h = self.dlg_size[H] - self.title_bar_height - frm_space *2 - btn_fram_h
    
            # Frame
            self.Title_Bar.place(
                x = 0, y = 0, 
                width = self.title_bar_width, height = self.title_bar_height
                )
            self.MsgArea_frame.place(
                x = frm_space, y = self.title_bar_height + frm_space, 
                width = self.title_bar_width - frm_space*2, height = message_area_h
                )
            self.BtnArea_frame.place(
                x = 0, y = self.title_bar_height + frm_space + message_area_h, 
                width = self.title_bar_width, height = btn_fram_h
                )
            
            self.Title_Label.grid(row = 0, column = 1,sticky = tk.W+tk.N+tk.S)
            self.Title_Bar.columnconfigure(0,minsize = self.frm_space)
            self.Title_Bar.rowconfigure(0,minsize = self.title_bar_height)
            
            self.MsgArea_frame.columnconfigure(0,minsize = self.frm_space)
            self.MsgArea_frame.rowconfigure(0,minsize = message_area_h)
            
            self.BtnArea_frame.rowconfigure(0,minsize = btn_fram_h)
    
            self.message_frame.place(
                x = msg_frm_w, y = msg_frm_w,
                width = self.title_bar_width - frm_space*2 - msg_frm_w*2, height = message_area_h - msg_frm_w*2,
                )
    
            # self.message_frame
            self.label_message.grid(row = 0, column = 1,sticky = tk.W+tk.N+tk.S)
    
            if self.box_state == 1:
                self.btn_ok.place(
                    x = (self.title_bar_width/2) - 80/2 , y = btn_fram_h/2 - 24/2,
                    )            
            if self.box_state == 2:
                self.btn_yes.place(
                    x = (self.title_bar_width/2) - (80 + frm_space) , y = btn_fram_h/2 - 24/2,
                    )            
                self.btn_no.place(
                    x = (self.title_bar_width/2) + frm_space , y = btn_fram_h/2 - 24/2,
                    )            
            if self.box_state == 3:
                self.btn_yes.place(
                    x = (self.title_bar_width/2) - (80*1.5 + frm_space*2) , y = btn_fram_h/2 - 24/2,
                    )            
                self.btn_no.place(
                    x = (self.title_bar_width/2) - 80/2 , y = btn_fram_h/2 - 24/2,
                    )            
                self.btn_cancel.place(
                    x = (self.title_bar_width/2) + 80/2 + frm_space*2 , y = btn_fram_h/2 - 24/2,
                    )
                
            dialog.grab_set_global()
    
        def btn_msgbox_clicked(self,state):
            self.return_state = state
            self.dev_dialog.destroy()
    
        def get_return_state(self):
            return self.return_state
    
    class CreateScreen(object):
        def __init__(self):
            self.cnt = 0
            W = 0
            H = 1
            self.dlg_size = [400,200]
    
            geo_string = '{}x{}'.format(self.dlg_size[W],self.dlg_size[H])
    
            self.MainWindow_obj = ttk.tkinter.Tk()
            self.MainWindow_obj.geometry(geo_string) 
    
            self.CntSting = tk.StringVar()
            self.CntSting.set('...')
    
            Label_Conter_text = tk.Label(
                self.MainWindow_obj,
                textvariable = self.CntSting,
            )
    
            self.MsgSting = tk.StringVar()
            self.MsgSting.set(str(self.cnt))
    
            Label_Message_text = tk.Label(
                self.MainWindow_obj,
                textvariable = self.MsgSting,
            )
    
            Btn_Messagebox = tk.Button(
                self.MainWindow_obj,
                text = 'Push',
                command = self.Btn_Messagebox_clicked
            )
            Label_Conter_text.pack()
            Label_Message_text.pack()
            Btn_Messagebox.pack()
    
            self.MainWindow_obj.after(1000,self.loop_msg)
    
            self.MainWindow_obj.mainloop()
    
        def Btn_Messagebox_clicked(self):
            self.dlg = CommonMessageBoxDialog(title='Test',message='Do you remember ?',state=3,parent =self.MainWindow_obj)        
            ret = self.dlg.get_return_state()
    
            if ret == 1:
                self.MsgSting.set('Yes')
            if ret == 2:
                self.MsgSting.set('No')
            if ret == 3:
                self.MsgSting.set('Cancel')
            
            return
    
        def loop_msg(self):
            self.cnt += 1
            self.MsgSting.set(str(self.cnt))
    
            self.MainWindow_obj.after(1000,self.loop_msg)
    
    if __name__ == '__main__':
        screen_obj = CreateScreen()
    
[/code]


CC BY-SA 4.0
2019-12-11T04:22:41.250
61331
2022-01-23T03:07:45.927
2020-09-15T00:41:53.123
3060
32891
question
python python3 tkinter
Python Tkでカスタムメッセージボックスを作りたい


どうやら自己解決できたようです。  
エラーのメッセージdialog_mouse_releaseから、マウスのリリースイベントを通知したいのに、ダイアログが破棄されてしまっていることが原因と推定しました。

CreateDialogメソッドにおいて、Button-1、B1-Motion、ButtonRelease-1をバインドしています。これは、タイトルバーに代えてself.Title_Barをドラッグするとボックスを移動させるためにバインドしています。

そこで、  
btn_msgbox_clickedメソッドにおいては、フラグ変数：self.isDestroyを用いて、  
その後にバインドされたdialog_mouse_releaseメソッドが呼び出されるので、そこからdestroyメソッドを呼ぶように変更しました。

[code]

    #! /usr/bin/env python3
    # -*- coding: utf-8 -*-
    
    import tkinter as tk
    from tkinter import ttk
    
    from tkinter import messagebox
    
    from PIL import Image,ImageTk,ImageDraw,ImageFont
    
    import datetime
    
    class CustomDialog(object):
        def __init__(self):
    
            self.title_bar_color = '#8FAADC'
            self.item_ground_color = 'whitesmoke'
            self.background_color = '#D9D9D9'
            self.select_bar_color = '#BDD7EE'
    
            self.isDrag_DlgMotion = False
            self.drag_dx = 0
            self.drag_dy = 0
    
        def dialog_left_click(self,event):
            dialog_x=self.dev_dialog.winfo_rootx()
            dialog_y=self.dev_dialog.winfo_rooty()
            point_x=self.dev_dialog.winfo_pointerx()
            point_y=self.dev_dialog.winfo_pointery()
    
            dx = point_x - dialog_x
            dy = point_y - dialog_y
    
            if (dx >= 0 and dx <= self.title_bar_width) and (dy >= 0 and dy <= self.title_bar_height):
                self.drag_dx = dx
                self.drag_dy = dy
                self.isDrag_DlgMotion = True
            return
    
        def dialog_mouse_move_on(self,event):
            if self.isDrag_DlgMotion:
                X = event.x_root - self.drag_dx
                Y = event.y_root - self.drag_dy
                self.dev_dialog.geometry('+{0}+{1}'.format(X, Y))
                pass
            return
    
        def dialog_mouse_release(self,event):
            if self.isDrag_DlgMotion:
                self.isDrag_DlgMotion = False
            return
    
    class CommonMessageBoxDialog(CustomDialog):
        def __init__(self,title,message,state,parent = None):
            self.return_state = None
            self.isDestroy = False
    
            if not isinstance(title,str) or not isinstance(message,str) or not isinstance(state,int):
                return
            if state < 1 or state > 3 :
                return
    
            root = ttk.tkinter.Tk()
    
            #root = tk.Toplevel(parent)
            #root.overrideredirect(True)
    
            super().__init__()
    
            self.box_state = state
            self.box_message = message
            self.box_title = title
    
            W = 0
            H = 1
    
            self.dlg_size = [400,200]
    
            self.title_bar_width = self.dlg_size[W]
            self.title_bar_height = 40
    
            self.btn_bar_height = 42
    
            self.btn_32x32_size = 42
    
            self.row_height = 28
            self.btn_row_height = 32
    
            self.frm_space = 10
    
            self.parent = parent
            self.CreateDialog(root)
    
            root.wait_window(root)
    
            #root.mainloop()
    
        def CreateDialog(self,root):
    
            W = 0
            H = 1
    
            if self.parent != None:
                self.parent.update_idletasks()
                ww=self.parent.winfo_screenwidth()
                wh=self.parent.winfo_screenheight()
                x=self.parent.winfo_rootx()
                y=self.parent.winfo_rooty()
    
                parent_w = self.parent.winfo_width()
                parent_h = self.parent.winfo_height()
                parent_x = self.parent.winfo_x()
                parent_y = self.parent.winfo_y()
            else:
                root.update_idletasks()
                ww=root.winfo_screenwidth()
                wh=root.winfo_screenheight()
                x=root.winfo_rootx()
                y=root.winfo_rooty()
    
                parent_w = root.winfo_width()
                parent_h = root.winfo_height()
                parent_x = root.winfo_x()
                parent_y = root.winfo_y()
    
    
            self.dev_dialog = root
            dialog = self.dev_dialog
            dialog.overrideredirect(True)
    
            dlg_x = int((parent_x+parent_w) - (self.dlg_size[W]/2))
            dlg_y = int((parent_y+parent_h) - (self.dlg_size[H]/2))
    
            if dlg_x < 0 : dlg_x = 0 
            if dlg_y < 0 : dlg_y = 0 
    
            dialog.geometry('{}x{}+{}+{}'.format(self.dlg_size[W],self.dlg_size[H],dlg_x,dlg_y))
    
    
            self.Title_Bar = tk.Frame(
                dialog,            
                relief='flat',
                bg = self.title_bar_color ,
                )
            self.Title_Label = tk.Label(
                self.Title_Bar,
                bg = self.title_bar_color ,
                text = self.box_title,
            )
    
            dialog.bind('<Button-1>', self.dialog_left_click)
            dialog.bind('<B1-Motion>', self.dialog_mouse_move_on)
            dialog.bind('<ButtonRelease-1>',self.dialog_mouse_release)
    
            self.MsgArea_frame = tk.Frame(
                dialog,            
                relief='flat',
                bg = self.select_bar_color,
                )
            self.message_frame = tk.Frame(
                self.MsgArea_frame,
                relief='flat',
                bg = self.item_ground_color ,
                )
            self.label_message = tk.Label(
                self.message_frame,
                bg = self.item_ground_color ,
                text = self.box_message,
            )
    
            self.BtnArea_frame = tk.Frame(
                dialog,            
                relief='flat',
                bg = self.item_ground_color,
                )
    
            self.btn_ok = tk.Button(
                self.BtnArea_frame,            
                bg = self.item_ground_color,
                text = 'OK',
                command = lambda:self.btn_msgbox_clicked(1),
                )
    
            self.btn_yes = tk.Button(
                self.BtnArea_frame,            
                bg = self.item_ground_color,
                text = 'YES',
                command = lambda:self.btn_msgbox_clicked(1),
                )
    
            self.btn_no = tk.Button(
                self.BtnArea_frame,            
                bg = self.item_ground_color,
                text = 'NO',
                command = lambda:self.btn_msgbox_clicked(2),
                )
    
            self.btn_cancel = tk.Button(
                self.BtnArea_frame,            
                bg = self.item_ground_color,
                text = 'CANCEL',
                command = lambda:self.btn_msgbox_clicked(3),
                )
    
            frm_space = self.frm_space
            msg_frm_w = 4
            btn_fram_h = 36
    
            message_area_h = self.dlg_size[H] - self.title_bar_height - frm_space *2 - btn_fram_h
    
            # Frame
            self.Title_Bar.place(
                x = 0, y = 0, 
                width = self.title_bar_width, height = self.title_bar_height
                )
            self.MsgArea_frame.place(
                x = frm_space, y = self.title_bar_height + frm_space, 
                width = self.title_bar_width - frm_space*2, height = message_area_h
                )
            self.BtnArea_frame.place(
                x = 0, y = self.title_bar_height + frm_space + message_area_h, 
                width = self.title_bar_width, height = btn_fram_h
                )
    
            self.Title_Label.grid(row = 0, column = 1,sticky = tk.W+tk.N+tk.S)
            self.Title_Bar.columnconfigure(0,minsize = self.frm_space)
            self.Title_Bar.rowconfigure(0,minsize = self.title_bar_height)
    
            self.MsgArea_frame.columnconfigure(0,minsize = self.frm_space)
            self.MsgArea_frame.rowconfigure(0,minsize = message_area_h)
    
            self.BtnArea_frame.rowconfigure(0,minsize = btn_fram_h)
    
            self.message_frame.place(
                x = msg_frm_w, y = msg_frm_w,
                width = self.title_bar_width - frm_space*2 - msg_frm_w*2, height = message_area_h - msg_frm_w*2,
                )
    
            # self.message_frame
            self.label_message.grid(row = 0, column = 1,sticky = tk.W+tk.N+tk.S)
    
            if self.box_state == 1:
                self.btn_ok.place(
                    x = (self.title_bar_width/2) - 80/2 , y = btn_fram_h/2 - 24/2,
                    )            
            if self.box_state == 2:
                self.btn_yes.place(
                    x = (self.title_bar_width/2) - (80 + frm_space) , y = btn_fram_h/2 - 24/2,
                    )            
                self.btn_no.place(
                    x = (self.title_bar_width/2) + frm_space , y = btn_fram_h/2 - 24/2,
                    )            
            if self.box_state == 3:
                self.btn_yes.place(
                    x = (self.title_bar_width/2) - (80*1.5 + frm_space*2) , y = btn_fram_h/2 - 24/2,
                    )            
                self.btn_no.place(
                    x = (self.title_bar_width/2) - 80/2 , y = btn_fram_h/2 - 24/2,
                    )            
                self.btn_cancel.place(
                    x = (self.title_bar_width/2) + 80/2 + frm_space*2 , y = btn_fram_h/2 - 24/2,
                    )            
    
            #dialog.grab_set()
            dialog.grab_set_global()
    
        def btn_msgbox_clicked(self,state):
            self.return_state = state
            self.isDestroy = True
    
        def get_return_state(self):
            return self.return_state
    
        def dialog_mouse_release(self,event):
            if self.isDrag_DlgMotion:
                self.isDrag_DlgMotion = False
    
            if self.isDestroy:
                self._quit()
            return
    
        def _quit(self):
            self.dev_dialog.grab_release()
            self.dev_dialog.destroy()
    
    
    class CreateScreen(object):
        def __init__(self):
            self.cnt = 0
            W = 0
            H = 1
            self.dlg_size = [400,200]
    
            geo_string = '{}x{}'.format(self.dlg_size[W],self.dlg_size[H])
    
            self.MainWindow_obj = ttk.tkinter.Tk()
            self.MainWindow_obj.geometry(geo_string) 
    
            self.CntSting = tk.StringVar()
            self.CntSting.set(str(self.cnt))
    
            Label_Conter_text = tk.Label(
                self.MainWindow_obj,
                textvariable = self.CntSting,
            )
    
            self.MsgSting = tk.StringVar()
            self.MsgSting.set('...')
    
            Label_Message_text = tk.Label(
                self.MainWindow_obj,
                textvariable = self.MsgSting,
            )
    
            Btn_Messagebox = tk.Button(
                self.MainWindow_obj,
                text = 'Push',
                command = self.Btn_Messagebox_clicked
            )
            Label_Conter_text.pack()
            Label_Message_text.pack()
            Btn_Messagebox.pack()
    
            self.MainWindow_obj.after(1000,self.loop_msg)
    
            self.MainWindow_obj.mainloop()
    
        def Btn_Messagebox_clicked(self):
            self.dlg = CommonMessageBoxDialog(title='Test',message='Do you remember ?',state=3,parent =self.MainWindow_obj)        
            ret = self.dlg.get_return_state()
    
            if ret == 1:
                self.MsgSting.set('Yes')
            if ret == 2:
                self.MsgSting.set('No')
            if ret == 3:
                self.MsgSting.set('Cancel')
    
            return
    
        def loop_msg(self):
            self.cnt += 1
            self.CntSting.set(str(self.cnt))
    
            self.MainWindow_obj.after(1000,self.loop_msg)
    
    if __name__ == '__main__':
        screen_obj = CreateScreen()
    
    
[/code]


CC BY-SA 4.0
2019-12-11T07:49:46.657
61340
2019-12-11T07:49:46.657
32891
61331
answer