diff --git a/waypoint_manager/scripts/devel_GUI.py b/waypoint_manager/scripts/devel_GUI.py index ee89696..457493a 100755 --- a/waypoint_manager/scripts/devel_GUI.py +++ b/waypoint_manager/scripts/devel_GUI.py @@ -12,595 +12,595 @@ #===== Applicationクラスの定義 tk.Frameクラスを継承 =====# class Application(tk.Frame): - """ - +++++ コンストラクタ +++++ - プログラムを実行したときに最初にすべき処理を全て記述する - """ - def __init__(self, master): - super().__init__(master) # スーパークラスのコンストラクタを実行 - self.master.title("Waypoints Manager") + """ + +++++ コンストラクタ +++++ + プログラムを実行したときに最初にすべき処理を全て記述する + """ + def __init__(self, master): + super().__init__(master) # スーパークラスのコンストラクタを実行 + self.master.title("Waypoints Manager") - ## 画面上部のメニューを作成 - self.menu_bar = tk.Menu(self) # メニューバーを配置 - self.file_menu = tk.Menu(self.menu_bar, tearoff=tk.OFF) # バーに追加するメニューを作成 - self.menu_bar.add_cascade(label="File", menu=self.file_menu) # Fileメニューとしてバーに追加 - self.file_menu.add_command(label="Save", # FileメニューにSaveコマンドを追加 - command=self.menu_save_clicked, #コールバック関数を設定 - accelerator="Ctrl+S"# 右側に表示するキーボードショートカット - ) - self.file_menu.add_command(label="Save As", # 同様にSave Asコマンドを追加 - command=self.menu_saveas_clicked, - accelerator="Ctrl+Shift+S" - ) - self.bind_all("", self.menu_save_clicked) #キーボードショートカットを設定 - self.bind_all("", self.menu_saveas_clicked) - self.master.config(menu=self.menu_bar) # 大元に作成したメニューバーを設定 + ## 画面上部のメニューを作成 + self.menu_bar = tk.Menu(self) # メニューバーを配置 + self.file_menu = tk.Menu(self.menu_bar, tearoff=tk.OFF) # バーに追加するメニューを作成 + self.menu_bar.add_cascade(label="File", menu=self.file_menu) # Fileメニューとしてバーに追加 + self.file_menu.add_command(label="Save", # FileメニューにSaveコマンドを追加 + command=self.menu_save_clicked, #コールバック関数を設定 + accelerator="Ctrl+S"# 右側に表示するキーボードショートカット + ) + self.file_menu.add_command(label="Save As", # 同様にSave Asコマンドを追加 + command=self.menu_saveas_clicked, + accelerator="Ctrl+Shift+S" + ) + self.bind_all("", self.menu_save_clicked) #キーボードショートカットを設定 + self.bind_all("", self.menu_saveas_clicked) + self.master.config(menu=self.menu_bar) # 大元に作成したメニューバーを設定 - ## 右クリックしたときに表示するポップアップメニューを作成 - self.popup_menu = tk.Menu(self, tearoff=tk.OFF) - self.popup_menu.add_command(label="Add waypoint here", command=self.add_waypoint) - self.right_click_coord = None # 右クリックしたときの座標を保持する変数 + ## 右クリックしたときに表示するポップアップメニューを作成 + self.popup_menu = tk.Menu(self, tearoff=tk.OFF) + self.popup_menu.add_command(label="Add waypoint here", command=self.add_waypoint) + self.right_click_coord = None # 右クリックしたときの座標を保持する変数 - ## map.pgm, map.yaml, waypoints.yaml の読み込み 3つの変数は再代入禁止 - self.__map_img_pil, self.__map_yaml = self.get_map_info() - self.__waypoints, self.waypoint_filepath = self.get_waypoints() - self.current_waypoints = self.__waypoints # 編集中のウェイポイント情報を保持する + ## map.pgm, map.yaml, waypoints.yaml の読み込み 3つの変数は再代入禁止 + self.__map_img_pil, self.__map_yaml = self.get_map_info() + self.__waypoints, self.waypoint_filepath = self.get_waypoints() + self.current_waypoints = self.__waypoints # 編集中のウェイポイント情報を保持する - ## canvasを配置 - self.canvas = tk.Canvas(self.master, background="#AAA") # 画像を描画するcanvas - self.canvas.pack(expand=True, fill=tk.BOTH) # canvasを配置 - self.update() # 情報の更新をする(canvasのサイズなどの情報が更新される) + ## canvasを配置 + self.canvas = tk.Canvas(self.master, background="#AAA") # 画像を描画するcanvas + self.canvas.pack(expand=True, fill=tk.BOTH) # canvasを配置 + self.update() # 情報の更新をする(canvasのサイズなどの情報が更新される) - ## 画像をcanvasのサイズにフィッティングして描画 - self.canv_w = self.canvas.winfo_width() # canvasの幅を取得 - self.canv_h = self.canvas.winfo_height() # canvasの高さを取得 - self.mat_affine = np.eye(3) # 同次変換行列の初期化 - scale = 1 - offset_x = 0 - offset_y = 0 - if (self.canv_w / self.canv_h) > (self.__map_img_pil.width / self.__map_img_pil.height): - # canvasの方が横長 画像をcanvasの縦に合わせてリサイズ - scale = self.canv_h / self.__map_img_pil.height - offset_x = (self.canv_w - self.__map_img_pil.width*scale) / 2 - else: - # canvasの方が縦長 画像をcanvasの横に合わせてリサイズ - scale = self.canv_w / self.__map_img_pil.width - offset_y = (self.canv_h - self.__map_img_pil.height*scale) / 2 - - self.scale_mat(scale) # 同次変換行列を更新 - self.translate_mat(offset_x, offset_y) # 同次変換行列を更新 - self.draw_img_tk = ImageTk.PhotoImage(image=self.__map_img_pil) # 描画する画像を保持する変数を初期化 - self.draw_image() # 画像を描画 + ## 画像をcanvasのサイズにフィッティングして描画 + self.canv_w = self.canvas.winfo_width() # canvasの幅を取得 + self.canv_h = self.canvas.winfo_height() # canvasの高さを取得 + self.mat_affine = np.eye(3) # 同次変換行列の初期化 + scale = 1 + offset_x = 0 + offset_y = 0 + if (self.canv_w / self.canv_h) > (self.__map_img_pil.width / self.__map_img_pil.height): + # canvasの方が横長 画像をcanvasの縦に合わせてリサイズ + scale = self.canv_h / self.__map_img_pil.height + offset_x = (self.canv_w - self.__map_img_pil.width*scale) / 2 + else: + # canvasの方が縦長 画像をcanvasの横に合わせてリサイズ + scale = self.canv_w / self.__map_img_pil.width + offset_y = (self.canv_h - self.__map_img_pil.height*scale) / 2 + + self.scale_mat(scale) # 同次変換行列を更新 + self.translate_mat(offset_x, offset_y) # 同次変換行列を更新 + self.draw_img_tk = ImageTk.PhotoImage(image=self.__map_img_pil) # 描画する画像を保持する変数を初期化 + self.draw_image() # 画像を描画 - ## map.yamlから原点と解像度を取得し、地図上に原点を示す円を描画 - self.point_rad = 10 - origin = self.__map_yaml['origin'] # originは地図上の原点から画像の左下までの距離[x,y](m) - self.map_resolution = self.__map_yaml['resolution'] - # 画像上の原点座標を保持(左上から横x, 縦y) - self.img_origin = [-origin[0]/self.map_resolution, self.__map_img_pil.height+origin[1]/self.map_resolution, 1] - self.plot_origin() + ## map.yamlから原点と解像度を取得し、地図上に原点を示す円を描画 + self.point_rad = 10 + origin = self.__map_yaml['origin'] # originは地図上の原点から画像の左下までの距離[x,y](m) + self.map_resolution = self.__map_yaml['resolution'] + # 画像上の原点座標を保持(左上から横x, 縦y) + self.img_origin = [-origin[0]/self.map_resolution, self.__map_img_pil.height+origin[1]/self.map_resolution, 1] + self.plot_origin() - ## waypoints.yamlからウェイポイント情報を取得し、画像にポイントを描画 - self.waypoints_id = np.array([], np.uint16) - self.plot_waypoints() + ## waypoints.yamlからウェイポイント情報を取得し、画像にポイントを描画 + self.waypoints_id = np.array([], np.uint16) + self.plot_waypoints() - ## マウスイベントに対するコールバックを設定 - self.master.bind("", self.mouse_wheel) - self.master.bind("", self.left_click_move) - self.master.bind("", self.left_click) - self.master.bind("", self.left_click_release) - self.master.bind("", self.right_click) - ## ウィンドウに関するコールバック - self.master.bind("", self.window_resize_callback) + ## マウスイベントに対するコールバックを設定 + self.master.bind("", self.mouse_wheel) + self.master.bind("", self.left_click_move) + self.master.bind("", self.left_click) + self.master.bind("", self.left_click_release) + self.master.bind("", self.right_click) + ## ウィンドウに関するコールバック + self.master.bind("", self.window_resize_callback) - self.old_click_point = None - self.wp_info_win = None - self.editing_waypoint_id = None - self.moving_waypoint = False - self.add_wp_win = None - self.adding_waypoint = False - return + self.old_click_point = None + self.wp_info_win = None + self.editing_waypoint_id = None + self.moving_waypoint = False + self.add_wp_win = None + self.adding_waypoint = False + return - """ - +++++ mapのファイルパスを受け取り、map画像とmapの設定ファイルを読み込む +++++ - --- 今はパスを直接指定して読み込んでいるので、rospy.get_param()を使って読み込めるように --- - """ - def get_map_info(self): - map_path = Path('..','..','waypoint_nav','maps','map') # .pgmと.yamlの手前までのパス - map_img_pil = Image.open(Path(str(map_path)+'.pgm')) # .pgmをplillowで読み込む - with open(str(map_path)+'.yaml') as file: # .yamlを読み込む - map_yaml = ruamel.yaml.YAML().load(file) - self.master.title(map_path.name + " - " + self.master.title()) - return map_img_pil, map_yaml # この2つの変数を戻り値とする - + """ + +++++ mapのファイルパスを受け取り、map画像とmapの設定ファイルを読み込む +++++ + --- 今はパスを直接指定して読み込んでいるので、rospy.get_param()を使って読み込めるように --- + """ + def get_map_info(self): + map_path = Path('waypoint_nav','maps','map') # .pgmと.yamlの手前までのパス + map_img_pil = Image.open(Path(str(map_path)+'.pgm')) # .pgmをplillowで読み込む + with open(str(map_path)+'.yaml') as file: # .yamlを読み込む + map_yaml = ruamel.yaml.YAML().load(file) + self.master.title(map_path.name + " - " + self.master.title()) + return map_img_pil, map_yaml # この2つの変数を戻り値とする + - """ - +++++ waypointsのパスを受け取り読み込んだデータを返す +++++ - --- これもget_param()でパスを受け取り、読み込めるようにする --- - """ - def get_waypoints(self): - file_path = Path('..','..','waypoint_nav','param','test.yaml') - with open(file_path) as file: - waypoints = ruamel.yaml.YAML().load(file) - self.master.title(file_path.name + " - " + self.master.title()) - return waypoints, file_path + """ + +++++ waypointsのパスを受け取り読み込んだデータを返す +++++ + --- これもget_param()でパスを受け取り、読み込めるようにする --- + """ + def get_waypoints(self): + file_path = Path('waypoint_nav','param','test.yaml') + with open(file_path) as file: + waypoints = ruamel.yaml.YAML().load(file) + self.master.title(file_path.name + " - " + self.master.title()) + return waypoints, file_path - """ - +++++ 地図上の原点に円を描画する +++++ - """ - def plot_origin(self): - origin_affine = np.dot(self.mat_affine, self.img_origin) # キャンバス上の座標に変換 - r = self.point_rad # 円の半径(ピクセル) - x1 = origin_affine[0] - r - y1 = origin_affine[1] - r - x2 = origin_affine[0] + r + 1 - y2 = origin_affine[1] + r + 1 - if self.canvas.find_withtag("origin"): - self.canvas.moveto("origin", x1, y1) - else: - self.canvas.create_oval(x1, y1, x2, y2, tags="origin", fill='cyan', outline='blue') - return + """ + +++++ 地図上の原点に円を描画する +++++ + """ + def plot_origin(self): + origin_affine = np.dot(self.mat_affine, self.img_origin) # キャンバス上の座標に変換 + r = self.point_rad # 円の半径(ピクセル) + x1 = origin_affine[0] - r + y1 = origin_affine[1] - r + x2 = origin_affine[0] + r + 1 + y2 = origin_affine[1] + r + 1 + if self.canvas.find_withtag("origin"): + self.canvas.moveto("origin", x1, y1) + else: + self.canvas.create_oval(x1, y1, x2, y2, tags="origin", fill='cyan', outline='blue') + return - """ - +++++ 地図上にウェイポイントを示す円を描画する +++++ - """ - def plot_waypoints(self): - points = self.current_waypoints['waypoints'] # ウェイポイントのリスト - r = self.point_rad - # ウェイポイントの追加または削除が行なわれている場合、初期化 - if len(self.waypoints_id) != len(points): - for id in self.waypoints_id: - self.canvas.delete(id) - self.waypoints_id = np.array([], np.uint16) - # 全てのウェイポイントを描画 - for i in range(len(points)): - img_x = float(points[i]['point']['x']) / self.map_resolution + self.img_origin[0] - img_y = -float(points[i]['point']['y']) / self.map_resolution + self.img_origin[1] - xy_affine = np.dot(self.mat_affine, [img_x, img_y, 1]) - x1 = xy_affine[0] - r - y1 = xy_affine[1] - r - x2 = xy_affine[0] + r + 1 - y2 = xy_affine[1] + r + 1 - if len(self.waypoints_id) < len(points): - id = self.canvas.create_oval(x1, y1, x2, y2, - fill='#FDD', outline='red', activefill='red') - self.waypoints_id = np.append(self.waypoints_id, id) - else: - id = self.waypoints_id[i] - self.canvas.moveto(id, x1, y1) - return + """ + +++++ 地図上にウェイポイントを示す円を描画する +++++ + """ + def plot_waypoints(self): + points = self.current_waypoints['waypoints'] # ウェイポイントのリスト + r = self.point_rad + # ウェイポイントの追加または削除が行なわれている場合、初期化 + if len(self.waypoints_id) != len(points): + for id in self.waypoints_id: + self.canvas.delete(id) + self.waypoints_id = np.array([], np.uint16) + # 全てのウェイポイントを描画 + for i in range(len(points)): + img_x = float(points[i]['point']['x']) / self.map_resolution + self.img_origin[0] + img_y = -float(points[i]['point']['y']) / self.map_resolution + self.img_origin[1] + xy_affine = np.dot(self.mat_affine, [img_x, img_y, 1]) + x1 = xy_affine[0] - r + y1 = xy_affine[1] - r + x2 = xy_affine[0] + r + 1 + y2 = xy_affine[1] + r + 1 + if len(self.waypoints_id) < len(points): + id = self.canvas.create_oval(x1, y1, x2, y2, + fill='#FDD', outline='red', activefill='red') + self.waypoints_id = np.append(self.waypoints_id, id) + else: + id = self.waypoints_id[i] + self.canvas.moveto(id, x1, y1) + return - """ - +++++ 左クリックされたときに実行されるコールバック関数 +++++ - """ - def left_click(self, event): - self.popup_menu.unpost() # 右クリックで出るポップアップメニューを非表示 - # クリックした座標の近くにあるオブジェクトを取得 - clicked_obj = self.canvas.find_enclosed(event.x-20, event.y-20, event.x+20, event.y+20) - if not clicked_obj: return # オブジェクト以外の領域 - id = clicked_obj[0] - tag = self.canvas.gettags(id) - if (len(tag) == 0): return - if (tag[0] == "origin"): return - # ↓ウェイポイントがクリックされた場合、情報を表示 - if id != self.editing_waypoint_id: # 編集中のウェイポイントを切り替え - self.canvas.itemconfig(self.editing_waypoint_id, fill='#FDD') - self.editing_waypoint_id = id - self.canvas.itemconfig(id, fill='red') - self.disp_waypoint_info(id) - return + """ + +++++ 左クリックされたときに実行されるコールバック関数 +++++ + """ + def left_click(self, event): + self.popup_menu.unpost() # 右クリックで出るポップアップメニューを非表示 + # クリックした座標の近くにあるオブジェクトを取得 + clicked_obj = self.canvas.find_enclosed(event.x-20, event.y-20, event.x+20, event.y+20) + if not clicked_obj: return # オブジェクト以外の領域 + id = clicked_obj[0] + tag = self.canvas.gettags(id) + if (len(tag) == 0): return + if (tag[0] == "origin"): return + # ↓ウェイポイントがクリックされた場合、情報を表示 + if id != self.editing_waypoint_id: # 編集中のウェイポイントを切り替え + self.canvas.itemconfig(self.editing_waypoint_id, fill='#FDD') + self.editing_waypoint_id = id + self.canvas.itemconfig(id, fill='red') + self.disp_waypoint_info(id) + return - """ - +++++ ウェイポイントが左クリックされたとき、別窓で情報を表示する関数 +++++ - """ - def disp_waypoint_info(self, id): - wp_num = np.where(self.waypoints_id == id)[0][0] + 1 # クリックしたウェイポイントの番号を取得 - point = self.current_waypoints['waypoints'][wp_num-1]['point'] - if (self.wp_info_win is None) or (not self.wp_info_win.winfo_exists()): - # ウィンドウが表示されてない場合、初期化 - self.wp_info_win = tk.Toplevel() - self.wp_info_win.protocol("WM_DELETE_WINDOW", self.close_wp_info) - self.wp_info_win.attributes('-topmost', True) # サブウィンドウを最前面で固定 - # ウェイポイントファイルのキーを取得し、ラベルを配置 - label_w = 4 - for i, key in enumerate(point): - key_label = tk.Label(self.wp_info_win, text=key+":", width=label_w, font=("Consolas",15), anchor=tk.E) - key_label.grid(column=0, row=i, padx=2, pady=5) - txt_box = tk.Entry(self.wp_info_win, width=20, font=("Consolas", 15)) - txt_box.insert(tk.END, point[key]) - txt_box.grid(column=1, row=i, padx=2, pady=2, ipady=3, sticky=tk.EW) - # ボタンを配置 - canv = tk.Canvas(self.wp_info_win) - canv.grid(column=0, columnspan=2, row=self.wp_info_win.grid_size()[1], sticky=tk.EW) - apply_btn = tk.Button(canv, text="Apply", width=5, height=1, background="#FDD", - command=self.apply_btn_callback) - apply_btn.pack(side=tk.RIGHT, anchor=tk.SE, padx=5, pady=5) - dnd_btn = tk.Button(canv, text="DnD", width=5, height=1, background="#EEE", - command=self.dnd_btn_callback) - dnd_btn.pack(side=tk.RIGHT, anchor=tk.SE, padx=5, pady=5) - remove_btn = tk.Button(canv, text="Remove", width=7, height=1, background="#F00", - command=self.remove_btn_callback) - remove_btn.pack(side=tk.LEFT, anchor=tk.SE, padx=5, pady=5) - # 位置とサイズを設定 - self.wp_info_win.update() - w = self.wp_info_win.winfo_width() - h = self.wp_info_win.winfo_height() - x = self.master.winfo_x() + self.canv_w - w - y = self.master.winfo_y() + self.canv_h - h - geometry = "{}x{}+{}+{}".format(w, h, x, y) - self.wp_info_win.geometry(geometry) - else: - # 既にウィンドウが表示されている場合、テキストボックスの中身を変える - for i, key in enumerate(point): - txt_box = self.wp_info_win.grid_slaves(column=1, row=i)[0] - txt_box.delete(0, tk.END) - txt_box.insert(tk.END, point[key]) - self.wp_info_win.title("Waypoint " + str(wp_num)) # タイトルを設定 - return + """ + +++++ ウェイポイントが左クリックされたとき、別窓で情報を表示する関数 +++++ + """ + def disp_waypoint_info(self, id): + wp_num = np.where(self.waypoints_id == id)[0][0] + 1 # クリックしたウェイポイントの番号を取得 + point = self.current_waypoints['waypoints'][wp_num-1]['point'] + if (self.wp_info_win is None) or (not self.wp_info_win.winfo_exists()): + # ウィンドウが表示されてない場合、初期化 + self.wp_info_win = tk.Toplevel() + self.wp_info_win.protocol("WM_DELETE_WINDOW", self.close_wp_info) + self.wp_info_win.attributes('-topmost', True) # サブウィンドウを最前面で固定 + # ウェイポイントファイルのキーを取得し、ラベルを配置 + label_w = 4 + for i, key in enumerate(point): + key_label = tk.Label(self.wp_info_win, text=key+":", width=label_w, font=("Consolas",15), anchor=tk.E) + key_label.grid(column=0, row=i, padx=2, pady=5) + txt_box = tk.Entry(self.wp_info_win, width=20, font=("Consolas", 15)) + txt_box.insert(tk.END, point[key]) + txt_box.grid(column=1, row=i, padx=2, pady=2, ipady=3, sticky=tk.EW) + # ボタンを配置 + canv = tk.Canvas(self.wp_info_win) + canv.grid(column=0, columnspan=2, row=self.wp_info_win.grid_size()[1], sticky=tk.EW) + apply_btn = tk.Button(canv, text="Apply", width=5, height=1, background="#FDD", + command=self.apply_btn_callback) + apply_btn.pack(side=tk.RIGHT, anchor=tk.SE, padx=5, pady=5) + dnd_btn = tk.Button(canv, text="DnD", width=5, height=1, background="#EEE", + command=self.dnd_btn_callback) + dnd_btn.pack(side=tk.RIGHT, anchor=tk.SE, padx=5, pady=5) + remove_btn = tk.Button(canv, text="Remove", width=7, height=1, background="#F00", + command=self.remove_btn_callback) + remove_btn.pack(side=tk.LEFT, anchor=tk.SE, padx=5, pady=5) + # 位置とサイズを設定 + self.wp_info_win.update() + w = self.wp_info_win.winfo_width() + h = self.wp_info_win.winfo_height() + x = self.master.winfo_x() + self.canv_w - w + y = self.master.winfo_y() + self.canv_h - h + geometry = "{}x{}+{}+{}".format(w, h, x, y) + self.wp_info_win.geometry(geometry) + else: + # 既にウィンドウが表示されている場合、テキストボックスの中身を変える + for i, key in enumerate(point): + txt_box = self.wp_info_win.grid_slaves(column=1, row=i)[0] + txt_box.delete(0, tk.END) + txt_box.insert(tk.END, point[key]) + self.wp_info_win.title("Waypoint " + str(wp_num)) # タイトルを設定 + return - """ - +++++ Applyボタンを押したときのコールバック +++++ - """ - def apply_btn_callback(self): - wp_num = np.where(self.waypoints_id == self.editing_waypoint_id)[0][0] + 1 # クリックしたウェイポイントの番号を取得 - point = self.current_waypoints['waypoints'][wp_num-1]['point'] - for i, key in enumerate(point): - txt_box = self.wp_info_win.grid_slaves(column=1, row=i)[0] - self.current_waypoints['waypoints'][wp_num-1]['point'][key] = float(txt_box.get()) - self.plot_waypoints() - return + """ + +++++ Applyボタンを押したときのコールバック +++++ + """ + def apply_btn_callback(self): + wp_num = np.where(self.waypoints_id == self.editing_waypoint_id)[0][0] + 1 # クリックしたウェイポイントの番号を取得 + point = self.current_waypoints['waypoints'][wp_num-1]['point'] + for i, key in enumerate(point): + txt_box = self.wp_info_win.grid_slaves(column=1, row=i)[0] + self.current_waypoints['waypoints'][wp_num-1]['point'][key] = float(txt_box.get()) + self.plot_waypoints() + return - """ - +++++ ドラッグ&ドロップボタン(DnD)を押したときのコールバック +++++ - """ - def dnd_btn_callback(self): - c = self.wp_info_win.grid_slaves(row=self.wp_info_win.grid_size()[1]-1, column=0)[0] - btn = c.pack_slaves()[1] - # 押された状態とそうでない状態を切り替える - if btn["relief"] == tk.RAISED: - btn["relief"] = tk.SUNKEN - btn["background"] = "#AAA" - elif btn["relief"] == tk.SUNKEN: - btn["relief"] = tk.RAISED - btn["background"] = "#EEE" - return - + """ + +++++ ドラッグ&ドロップボタン(DnD)を押したときのコールバック +++++ + """ + def dnd_btn_callback(self): + c = self.wp_info_win.grid_slaves(row=self.wp_info_win.grid_size()[1]-1, column=0)[0] + btn = c.pack_slaves()[1] + # 押された状態とそうでない状態を切り替える + if btn["relief"] == tk.RAISED: + btn["relief"] = tk.SUNKEN + btn["background"] = "#AAA" + elif btn["relief"] == tk.SUNKEN: + btn["relief"] = tk.RAISED + btn["background"] = "#EEE" + return + - """ - +++++ removeボタンを押したときのコールバック +++++ - """ - def remove_btn_callback(self): - wp_num = np.where(self.waypoints_id == self.editing_waypoint_id)[0][0] + 1 # クリックしたウェイポイントの番号を取得 - self.current_waypoints['waypoints'].pop(wp_num-1) # ウェイポイントを削除 - self.plot_waypoints() # ウェイポイントを再描画、waypoints_idを更新 - self.close_wp_info() - return - + """ + +++++ removeボタンを押したときのコールバック +++++ + """ + def remove_btn_callback(self): + wp_num = np.where(self.waypoints_id == self.editing_waypoint_id)[0][0] + 1 # クリックしたウェイポイントの番号を取得 + self.current_waypoints['waypoints'].pop(wp_num-1) # ウェイポイントを削除 + self.plot_waypoints() # ウェイポイントを再描画、waypoints_idを更新 + self.close_wp_info() + return + - """ - +++++ ウェイポイント情報を表示するサブウィンドウを閉じたときのコールバック +++++ - """ - def close_wp_info(self): - self.canvas.itemconfig(self.editing_waypoint_id, fill='#FDD') - self.editing_waypoint_id = None - self.wp_info_win.destroy() - return + """ + +++++ ウェイポイント情報を表示するサブウィンドウを閉じたときのコールバック +++++ + """ + def close_wp_info(self): + self.canvas.itemconfig(self.editing_waypoint_id, fill='#FDD') + self.editing_waypoint_id = None + self.wp_info_win.destroy() + return - """ - +++++ マウスを左クリックしながらドラッグしたときのコールバック関数 +++++ - """ - def left_click_move(self, event): - if (self.wp_info_win is not None) and (self.wp_info_win.winfo_exists()) and (not self.moving_waypoint): - # ウェイポイント情報が表示されていて、ウェイポイントを動かしていないとき - c = self.wp_info_win.grid_slaves(row=self.wp_info_win.grid_size()[1]-1, column=0)[0] - dnd_btn = c.pack_slaves()[1] # dndボタンの状態を取得 - clicked_obj = self.canvas.find_enclosed(event.x-20, event.y-20, event.x+20, event.y+20) - if (clicked_obj) and (clicked_obj[0] == self.editing_waypoint_id) and (dnd_btn["relief"] == tk.SUNKEN): - # dndボタンが押された状態で、編集中のウェイポイントがクリックされたとき - self.moving_waypoint = True # ウェイポイントをドラッグで移動させるモードへ - self.old_click_point = [event.x, event.y] - return - if self.old_click_point is None: - self.old_click_point = [event.x, event.y] - return - # カーソルの移動量を計算 - delta_x = event.x-self.old_click_point[0] - delta_y = event.y-self.old_click_point[1] - #ウェイポイントをドラッグで動かしているとき - if self.moving_waypoint: - self.canvas.move(self.editing_waypoint_id, delta_x, delta_y) - box = self.canvas.bbox(self.editing_waypoint_id) - px = (box[2] + box[0]) / 2 # ウィンドウ上の座標 - py = (box[3] + box[1]) / 2 - mat_inv = np.linalg.inv(self.mat_affine) - img_xy = np.dot(mat_inv, [px, py, 1]) # マップ画像上の座標 - # マップ画像上の座標を、実際の座標に変換 - x = (img_xy[0] - self.img_origin[0]) * self.map_resolution - y = (-img_xy[1] + self.img_origin[1]) * self.map_resolution - x = round(x, 6) - y = round(y, 6) - wp_num = np.where(self.waypoints_id == self.editing_waypoint_id)[0][0] - # 編集中のウェイポイント情報を更新 - self.current_waypoints['waypoints'][wp_num]['point']['x'] = x - self.current_waypoints['waypoints'][wp_num]['point']['y'] = y - # 表示中のウェイポイント情報を更新 - txt_box = self.wp_info_win.grid_slaves(column=1, row=0)[0] - txt_box.delete(0, tk.END) - txt_box.insert(tk.END, x) - txt_box = self.wp_info_win.grid_slaves(column=1, row=1)[0] - txt_box.delete(0, tk.END) - txt_box.insert(tk.END, y) - self.old_click_point = [event.x, event.y] - return - # ウェイポイント移動モードでないとき、地図を平行移動 - self.translate_mat(delta_x, delta_y) - self.draw_image() - self.canvas.move("origin", delta_x, delta_y) #self.plot_origin() - self.plot_waypoints() - self.old_click_point = [event.x, event.y] - return - + """ + +++++ マウスを左クリックしながらドラッグしたときのコールバック関数 +++++ + """ + def left_click_move(self, event): + if (self.wp_info_win is not None) and (self.wp_info_win.winfo_exists()) and (not self.moving_waypoint): + # ウェイポイント情報が表示されていて、ウェイポイントを動かしていないとき + c = self.wp_info_win.grid_slaves(row=self.wp_info_win.grid_size()[1]-1, column=0)[0] + dnd_btn = c.pack_slaves()[1] # dndボタンの状態を取得 + clicked_obj = self.canvas.find_enclosed(event.x-20, event.y-20, event.x+20, event.y+20) + if (clicked_obj) and (clicked_obj[0] == self.editing_waypoint_id) and (dnd_btn["relief"] == tk.SUNKEN): + # dndボタンが押された状態で、編集中のウェイポイントがクリックされたとき + self.moving_waypoint = True # ウェイポイントをドラッグで移動させるモードへ + self.old_click_point = [event.x, event.y] + return + if self.old_click_point is None: + self.old_click_point = [event.x, event.y] + return + # カーソルの移動量を計算 + delta_x = event.x-self.old_click_point[0] + delta_y = event.y-self.old_click_point[1] + #ウェイポイントをドラッグで動かしているとき + if self.moving_waypoint: + self.canvas.move(self.editing_waypoint_id, delta_x, delta_y) + box = self.canvas.bbox(self.editing_waypoint_id) + px = (box[2] + box[0]) / 2 # ウィンドウ上の座標 + py = (box[3] + box[1]) / 2 + mat_inv = np.linalg.inv(self.mat_affine) + img_xy = np.dot(mat_inv, [px, py, 1]) # マップ画像上の座標 + # マップ画像上の座標を、実際の座標に変換 + x = (img_xy[0] - self.img_origin[0]) * self.map_resolution + y = (-img_xy[1] + self.img_origin[1]) * self.map_resolution + x = round(x, 6) + y = round(y, 6) + wp_num = np.where(self.waypoints_id == self.editing_waypoint_id)[0][0] + # 編集中のウェイポイント情報を更新 + self.current_waypoints['waypoints'][wp_num]['point']['x'] = x + self.current_waypoints['waypoints'][wp_num]['point']['y'] = y + # 表示中のウェイポイント情報を更新 + txt_box = self.wp_info_win.grid_slaves(column=1, row=0)[0] + txt_box.delete(0, tk.END) + txt_box.insert(tk.END, x) + txt_box = self.wp_info_win.grid_slaves(column=1, row=1)[0] + txt_box.delete(0, tk.END) + txt_box.insert(tk.END, y) + self.old_click_point = [event.x, event.y] + return + # ウェイポイント移動モードでないとき、地図を平行移動 + self.translate_mat(delta_x, delta_y) + self.draw_image() + self.canvas.move("origin", delta_x, delta_y) #self.plot_origin() + self.plot_waypoints() + self.old_click_point = [event.x, event.y] + return + - """ - +++++ マウスの左クリックを離したときのコールバック関数 +++++ - """ - def left_click_release(self, event): - self.old_click_point = None - self.moving_waypoint = False - return + """ + +++++ マウスの左クリックを離したときのコールバック関数 +++++ + """ + def left_click_release(self, event): + self.old_click_point = None + self.moving_waypoint = False + return - """ - +++++ 右クリックしたときのコールバック関数 +++++ - """ - def right_click(self, event): - # クリックした座標の近くにあるオブジェクトを取得 - clicked_obj = self.canvas.find_enclosed(event.x-20, event.y-20, event.x+20, event.y+20) - if clicked_obj: # 何かオブジェクトがクリックされていた場合 - return - # クリックされた座標 => 元画像の座標 の変換 - mat_inv = np.linalg.inv(self.mat_affine) - img_xy = np.dot(mat_inv, [event.x, event.y, 1]) - # 変換後の元画像の座標がサイズから外れている場合(地図画像の外をクリックしている) - if (img_xy[0] < 0) or (img_xy[1] < 0) or \ - (img_xy[0] > self.__map_img_pil.width) or (img_xy[1] > self.__map_img_pil.height): - return - self.popup_menu.post(event.x_root, event.y_root) # メニューをポップアップ - self.right_click_coord = img_xy[0:2] # クリックされた元画像上の座標を変数に格納 - return + """ + +++++ 右クリックしたときのコールバック関数 +++++ + """ + def right_click(self, event): + # クリックした座標の近くにあるオブジェクトを取得 + clicked_obj = self.canvas.find_enclosed(event.x-20, event.y-20, event.x+20, event.y+20) + if clicked_obj: # 何かオブジェクトがクリックされていた場合 + return + # クリックされた座標 => 元画像の座標 の変換 + mat_inv = np.linalg.inv(self.mat_affine) + img_xy = np.dot(mat_inv, [event.x, event.y, 1]) + # 変換後の元画像の座標がサイズから外れている場合(地図画像の外をクリックしている) + if (img_xy[0] < 0) or (img_xy[1] < 0) or \ + (img_xy[0] > self.__map_img_pil.width) or (img_xy[1] > self.__map_img_pil.height): + return + self.popup_menu.post(event.x_root, event.y_root) # メニューをポップアップ + self.right_click_coord = img_xy[0:2] # クリックされた元画像上の座標を変数に格納 + return - """ - +++++ 右クリックしてポップアップメニューのadd waypointをクリックしたときのコールバック関数 +++++ - """ - def add_waypoint(self): - if (self.wp_info_win is not None) and (self.wp_info_win.winfo_exists()): - self.close_wp_info() - img_xy = self.right_click_coord - if self.__map_img_pil.getpixel((img_xy[0], img_xy[1])) == 0: - print("There is obstacles") - return - # 何番目のウェイポイントの次に追加するか入力させる - self.add_wp_win = tk.Toplevel() - self.add_wp_win.title("Add waypoint") - self.add_wp_win.protocol("WM_DELETE_WINDOW") - self.add_wp_win.attributes('-topmost', True) # サブウィンドウを最前面で固定 - msg = tk.Label(self.add_wp_win, text="Add waypoint after no. ", font=("Consolas",15), anchor=tk.E) - msg.grid(column=0, row=0, padx=10, pady=10, sticky=tk.EW) - txt_box = tk.Entry(self.add_wp_win, width=4, font=("Consolas",15)) - txt_box.grid(column=1, row=0, pady=10, sticky=tk.W) - add_btn = tk.Button(self.add_wp_win, text="Add", width=5, height=1, command=self.add_btn_callback) - add_btn.grid(column=0, columnspan=2, row=1, padx=10, pady=10) - self.add_wp_win.update() - w = self.add_wp_win.winfo_width() + 10 - h = self.add_wp_win.winfo_height() - x = int((self.canv_w - w) / 2) - y = int((self.canv_h - h) / 2) - geometry = "{}x{}+{}+{}".format(w, h, x, y) - self.add_wp_win.geometry(geometry) - return - + """ + +++++ 右クリックしてポップアップメニューのadd waypointをクリックしたときのコールバック関数 +++++ + """ + def add_waypoint(self): + if (self.wp_info_win is not None) and (self.wp_info_win.winfo_exists()): + self.close_wp_info() + img_xy = self.right_click_coord + if self.__map_img_pil.getpixel((img_xy[0], img_xy[1])) == 0: + print("There is obstacles") + return + # 何番目のウェイポイントの次に追加するか入力させる + self.add_wp_win = tk.Toplevel() + self.add_wp_win.title("Add waypoint") + self.add_wp_win.protocol("WM_DELETE_WINDOW") + self.add_wp_win.attributes('-topmost', True) # サブウィンドウを最前面で固定 + msg = tk.Label(self.add_wp_win, text="Add waypoint after no. ", font=("Consolas",15), anchor=tk.E) + msg.grid(column=0, row=0, padx=10, pady=10, sticky=tk.EW) + txt_box = tk.Entry(self.add_wp_win, width=4, font=("Consolas",15)) + txt_box.grid(column=1, row=0, pady=10, sticky=tk.W) + add_btn = tk.Button(self.add_wp_win, text="Add", width=5, height=1, command=self.add_btn_callback) + add_btn.grid(column=0, columnspan=2, row=1, padx=10, pady=10) + self.add_wp_win.update() + w = self.add_wp_win.winfo_width() + 10 + h = self.add_wp_win.winfo_height() + x = int((self.canv_w - w) / 2) + y = int((self.canv_h - h) / 2) + geometry = "{}x{}+{}+{}".format(w, h, x, y) + self.add_wp_win.geometry(geometry) + return + - """ - +++++ add waypoint hereをクリックして開いた別窓のボタンのコールバック +++++ - """ - def add_btn_callback(self): - num_box = self.add_wp_win.grid_slaves(row=0, column=1)[0] - num = num_box.get() - if (num == ""): - print("Please enter number") - return - self.add_wp_win.destroy() - num = int(num) - img_xy = self.right_click_coord - # ウェイポイント座標を計算 - x = (img_xy[0] - self.img_origin[0]) * self.map_resolution - y = (-img_xy[1] + self.img_origin[1]) * self.map_resolution - x = round(x, 6) - y = round(y, 6) - # ウェイポイントを追加 - data = CommentedMap() - data['point'] = CommentedMap() - for key in self.current_waypoints['waypoints'][0]['point']: - if (key == 'x'): val = x - elif (key == 'y'): val = y - else: val = 0.0 - data['point'][key] = val - self.current_waypoints['waypoints'].insert(num, data) - self.plot_waypoints() # ウェイポイントを再描画、waypoints_idも初期化 - id = self.waypoints_id[num] - self.editing_waypoint_id = id - self.canvas.itemconfig(id, fill='red') - self.disp_waypoint_info(id) - return - + """ + +++++ add waypoint hereをクリックして開いた別窓のボタンのコールバック +++++ + """ + def add_btn_callback(self): + num_box = self.add_wp_win.grid_slaves(row=0, column=1)[0] + num = num_box.get() + if (num == ""): + print("Please enter number") + return + self.add_wp_win.destroy() + num = int(num) + img_xy = self.right_click_coord + # ウェイポイント座標を計算 + x = (img_xy[0] - self.img_origin[0]) * self.map_resolution + y = (-img_xy[1] + self.img_origin[1]) * self.map_resolution + x = round(x, 6) + y = round(y, 6) + # ウェイポイントを追加 + data = CommentedMap() + data['point'] = CommentedMap() + for key in self.current_waypoints['waypoints'][0]['point']: + if (key == 'x'): val = x + elif (key == 'y'): val = y + else: val = 0.0 + data['point'][key] = val + self.current_waypoints['waypoints'].insert(num, data) + self.plot_waypoints() # ウェイポイントを再描画、waypoints_idも初期化 + id = self.waypoints_id[num] + self.editing_waypoint_id = id + self.canvas.itemconfig(id, fill='red') + self.disp_waypoint_info(id) + return + - """ - +++++ マウスホイールを回転したとき(タッチパッドをドラッグしたとき)のコールバック関数 +++++ - --- docker コンテナ上だとtkinterでマウスホイールイベントが拾えないっぽいので、これは使えないかも --- - """ - def mouse_wheel(self, event): - self.translate_mat(-event.x, -event.y) - if event.delta > 0: - # 上に回転(タッチパッドなら下にドラッグ)=> 拡大 - self.scale_mat(1.1) - else: - # 下に回転(タッチパッドなら上にドラッグ)=> 縮小 - self.scale_mat(0.9) - self.translate_mat(event.x, event.y) - self.draw_image() - self.plot_origin() - self.plot_waypoints() - return - + """ + +++++ マウスホイールを回転したとき(タッチパッドをドラッグしたとき)のコールバック関数 +++++ + --- docker コンテナ上だとtkinterでマウスホイールイベントが拾えないっぽいので、これは使えないかも --- + """ + def mouse_wheel(self, event): + self.translate_mat(-event.x, -event.y) + if event.delta > 0: + # 上に回転(タッチパッドなら下にドラッグ)=> 拡大 + self.scale_mat(1.1) + else: + # 下に回転(タッチパッドなら上にドラッグ)=> 縮小 + self.scale_mat(0.9) + self.translate_mat(event.x, event.y) + self.draw_image() + self.plot_origin() + self.plot_waypoints() + return + - """ - +++++ Fileメニューの"Save"がクリックされたときに実行されるコールバック関数 +++++ - """ - def menu_save_clicked(self, event=None): - self.save_waypoints(self.waypoint_filepath) - print("Saved changes!") - return - + """ + +++++ Fileメニューの"Save"がクリックされたときに実行されるコールバック関数 +++++ + """ + def menu_save_clicked(self, event=None): + self.save_waypoints(self.waypoint_filepath) + print("Saved changes!") + return + - """ - +++++ Fileメニューの"Save As"がクリックされたときに実行されるコールバック関数 +++++ - """ - def menu_saveas_clicked(self, event=None): - new_filepath = tkinter.filedialog.asksaveasfilename( - parent=self.master, - title="Save As", - initialdir=str(Path('..','..','waypoint_nav','param')), - filetypes=[("YAML", ".yaml")], - defaultextension="yaml" - ) - if len(new_filepath) == 0: return # cancel - self.save_waypoints(new_filepath) - self.waypoint_filepath = new_filepath - print("Save As", "\"", new_filepath, "\"") - return - + """ + +++++ Fileメニューの"Save As"がクリックされたときに実行されるコールバック関数 +++++ + """ + def menu_saveas_clicked(self, event=None): + new_filepath = tkinter.filedialog.asksaveasfilename( + parent=self.master, + title="Save As", + initialdir=str(Path('..','..','waypoint_nav','param')), + filetypes=[("YAML", ".yaml")], + defaultextension="yaml" + ) + if len(new_filepath) == 0: return # cancel + self.save_waypoints(new_filepath) + self.waypoint_filepath = new_filepath + print("Save As", "\"", new_filepath, "\"") + return + - """ - +++++ 現在のウェイポイント情報を指定のファイルに書き込む +++++ - """ - def save_waypoints(self, path): - points = self.current_waypoints['waypoints'] - fin_pose = self.current_waypoints['finish_pose'] - with open(path, 'w') as f: - f.write("waypoints:\n") - for point in points: - f.write("- point: {") - for i, key in enumerate(point['point']): - if i == 0: - f.write(key + ": " + str(point['point'][key])) - else: - f.write(", " + key + ": " + str(point['point'][key])) - f.write("}\n") - f.write("finish_pose:\n") - seq = fin_pose['header']['seq'] - stamp = fin_pose['header']['stamp'] - fid = fin_pose['header']['frame_id'] - f.write(" header: {" + "seq: {}, stamp: {}, frame_id: {}".format(seq, stamp, fid) + "}\n") - f.write(" pose:\n") - p = fin_pose['pose']['position'] - o = fin_pose['pose']['orientation'] - f.write(" position: {" + "x: {}, y: {}, z: {}".format(p["x"], p["y"], p["z"]) + "}\n") - f.write(" orientation: {" + "x: {}, y: {}, z: {}, w: {}".format(o["x"], o["y"], o["z"], o["w"]) + "}") - return + """ + +++++ 現在のウェイポイント情報を指定のファイルに書き込む +++++ + """ + def save_waypoints(self, path): + points = self.current_waypoints['waypoints'] + fin_pose = self.current_waypoints['finish_pose'] + with open(path, 'w') as f: + f.write("waypoints:\n") + for point in points: + f.write("- point: {") + for i, key in enumerate(point['point']): + if i == 0: + f.write(key + ": " + str(point['point'][key])) + else: + f.write(", " + key + ": " + str(point['point'][key])) + f.write("}\n") + f.write("finish_pose:\n") + seq = fin_pose['header']['seq'] + stamp = fin_pose['header']['stamp'] + fid = fin_pose['header']['frame_id'] + f.write(" header: {" + "seq: {}, stamp: {}, frame_id: {}".format(seq, stamp, fid) + "}\n") + f.write(" pose:\n") + p = fin_pose['pose']['position'] + o = fin_pose['pose']['orientation'] + f.write(" position: {" + "x: {}, y: {}, z: {}".format(p["x"], p["y"], p["z"]) + "}\n") + f.write(" orientation: {" + "x: {}, y: {}, z: {}, w: {}".format(o["x"], o["y"], o["z"], o["w"]) + "}") + return - """ - +++++ 引数の同次変換行列(mat_affine)にx,yの平行移動を加えた同次変換行列を返す +++++ - """ - def translate_mat(self, x, y): - mat = np.eye(3) - mat[0, 2] = float(x) - mat[1, 2] = float(y) - self.mat_affine = np.dot(mat, self.mat_affine) - return - + """ + +++++ 引数の同次変換行列(mat_affine)にx,yの平行移動を加えた同次変換行列を返す +++++ + """ + def translate_mat(self, x, y): + mat = np.eye(3) + mat[0, 2] = float(x) + mat[1, 2] = float(y) + self.mat_affine = np.dot(mat, self.mat_affine) + return + - """ - +++++ 引数のmat_affineにscale倍のリサイズを加えた同次変換行列を返す +++++ - """ - def scale_mat(self, scale): - mat = np.eye(3) - mat[0, 0] = scale - mat[1, 1] = scale - self.mat_affine = np.dot(mat, self.mat_affine) - return + """ + +++++ 引数のmat_affineにscale倍のリサイズを加えた同次変換行列を返す +++++ + """ + def scale_mat(self, scale): + mat = np.eye(3) + mat[0, 0] = scale + mat[1, 1] = scale + self.mat_affine = np.dot(mat, self.mat_affine) + return - + - """ - +++++ 元画像をaffne変換して描画、その画像を返す +++++ - """ - def draw_image(self): - mat_inv = np.linalg.inv(self.mat_affine) - img = self.__map_img_pil.transform( - (self.canv_w, self.canv_w), - Image.Transform.AFFINE, tuple(mat_inv.flatten()), - Image.Resampling.NEAREST, - fillcolor = 160 - ) - self.draw_img_tk = ImageTk.PhotoImage(image=img) # 描画する画像を変数に保持 - if not self.canvas.find_withtag("map_image"): # 初めて画像を描画するとき - self.canvas.create_image(0, 0, anchor='nw', image=self.draw_img_tk, tags="map_image") # 画像の描画 - else: - self.canvas.itemconfig("map_image", image=self.draw_img_tk) # 既に描画された画像を差し替える - return + """ + +++++ 元画像をaffne変換して描画、その画像を返す +++++ + """ + def draw_image(self): + mat_inv = np.linalg.inv(self.mat_affine) + img = self.__map_img_pil.transform( + (self.canv_w, self.canv_w), + Image.Transform.AFFINE, tuple(mat_inv.flatten()), + Image.Resampling.NEAREST, + fillcolor = 160 + ) + self.draw_img_tk = ImageTk.PhotoImage(image=img) # 描画する画像を変数に保持 + if not self.canvas.find_withtag("map_image"): # 初めて画像を描画するとき + self.canvas.create_image(0, 0, anchor='nw', image=self.draw_img_tk, tags="map_image") # 画像の描画 + else: + self.canvas.itemconfig("map_image", image=self.draw_img_tk) # 既に描画された画像を差し替える + return - """ - +++++ ウィンドウサイズが変更されたとき、情報を更新する +++++ - """ - def window_resize_callback(self, event): - cw = self.canvas.winfo_width() - ch = self.canvas.winfo_height() - if (self.canv_w != cw) or (self.canv_h != ch): - self.canv_w = cw - self.canv_h = ch - return + """ + +++++ ウィンドウサイズが変更されたとき、情報を更新する +++++ + """ + def window_resize_callback(self, event): + cw = self.canvas.winfo_width() + ch = self.canvas.winfo_height() + if (self.canv_w != cw) or (self.canv_h != ch): + self.canv_w = cw + self.canv_h = ch + return #===== メイン処理 プログラムはここから実行される =====# if __name__ == "__main__": - #rospy.init_node("manager_GUI") - root = tk.Tk() # 大元になるウィンドウ - w, h = root.winfo_screenwidth()-100, root.winfo_screenheight()-300 - root.geometry("%dx%d+0+0" % (w, h)) - app = Application(master=root) # tk.Frameを継承したApplicationクラスのインスタンス - app.mainloop() + #rospy.init_node("manager_GUI") + root = tk.Tk() # 大元になるウィンドウ + w, h = root.winfo_screenwidth()-100, root.winfo_screenheight()-300 + root.geometry("%dx%d+0+0" % (w, h)) + app = Application(master=root) # tk.Frameを継承したApplicationクラスのインスタンス + app.mainloop()