{ "cells": [ { "cell_type": "markdown", "id": "harmful-custody", "metadata": {}, "source": [ "# GUI / tkinter (12/5-2021)" ] }, { "cell_type": "markdown", "id": "continuing-functionality", "metadata": {}, "source": [ "## Exercise\n", "\n", "Create a GUI using ``tkinter`` to interactively draw points and visualize the **minimum enclosing circle** (as computed in the optimization lecture using ``scipy.optimize.minimize``).\n", "\n", "_Note_: The circle found using ``minimize`` might not be smallest possible, since ``minimize`` is not guaranteed to find the find the minimum. For (more complicated) algorithms guaranteed to find a minimum enclosing circle see https://en.wikipedia.org/wiki/Smallest-circle_problem." ] }, { "cell_type": "code", "execution_count": 13, "id": "protected-costume", "metadata": {}, "outputs": [], "source": [ "from math import sqrt\n", "from scipy.optimize import minimize\n", "import tkinter as tk\n", "\n", "points = [] # current set of points (latest created last)\n", "deleted_points = [] # deleted points (most recent last)\n", "\n", "current_point = None # Point currently being moved\n", "current_point_moved = False # if False, the current point should be deleted when button released\n", "\n", "# Smallest enclosing circle computation\n", "\n", "def dist(p, q):\n", " '''Compute distance squared (avoid square root)'''\n", " \n", " return sum((a - b) ** 2 for a, b in zip(p, q))\n", "\n", "def max_distance(p, S):\n", " return max(dist(p, q) for q in S)\n", "\n", "def minimum_enclosing_circle(S):\n", " '''Use scipy minimize to find compute minimum enclosing circle'''\n", " \n", " def f(p):\n", " return max_distance(p, S)\n", "\n", " solution = minimize(f, [0.0, 0.0]) # this might fail to find correct solution\n", " center = solution.x\n", " radius = solution.fun ** 0.5\n", "\n", " return center, radius\n", "\n", "# Button event handlers\n", "\n", "def do_quit(event=None):\n", " # When called by a key binding, an event is passed\n", " # When called by a button command, no arguments are passed\n", " root_window.destroy()\n", "\n", "def undo(event=None):\n", " if points:\n", " deleted_points.append(points.pop())\n", " redraw()\n", "\n", "def redo(event=None):\n", " if deleted_points:\n", " points.append(deleted_points.pop())\n", " redraw()\n", "\n", "def clear_all(event=None):\n", " deleted_points[:] = reversed(points)\n", " points.clear()\n", " redraw()\n", "\n", "# Canvas drawing\n", " \n", "def draw_circle(canvas, center, radius=3, fill='white'):\n", " x, y = center\n", " canvas.create_oval(x - radius, y - radius,\n", " x + radius, y + radius, fill=fill)\n", "\n", "def redraw():\n", " canvas.delete('all')\n", " if len(points) >= 2:\n", " center, radius = minimum_enclosing_circle(points)\n", " draw_circle(canvas, center, radius, fill='red')\n", " draw_circle(canvas, center, 5, fill='yellow')\n", " info_var.set(f'{radius = :.2f}')\n", " else:\n", " info_var.set('Draw points...')\n", " \n", " for point in points:\n", " draw_circle(canvas, point)\n", "\n", "def print_points(event):\n", " print('Current point set:')\n", " print(points)\n", "\n", "# Left mouse button events\n", "\n", "def do_select_point(event):\n", " global current_point, current_point_moved\n", " \n", " point = (event.x, event.y)\n", " nearest = min(points,\n", " key=lambda p: dist(p, point),\n", " default=None)\n", "\n", " if nearest and dist(point, nearest) <= 5:\n", " current_point = points.index(nearest)\n", " current_point_moved = False\n", " else:\n", " points.append(point)\n", " current_point = len(points) - 1\n", " current_point_moved = True\n", " redraw()\n", "\n", "def do_move_point(event):\n", " global current_point_moved\n", "\n", " if current_point != None:\n", " current_point_moved = True\n", " point = (event.x, event.y)\n", " points[current_point] = point\n", " redraw()\n", "\n", "def do_unselect(event):\n", " global current_point\n", "\n", " if current_point != None:\n", " if not current_point_moved:\n", " deleted_points.append(points[current_point])\n", " del points[current_point]\n", " redraw()\n", " current_point = None\n", "\n", "# Setup window widgets\n", " \n", "root_window = tk.Tk() # create root window\n", "root_window.title('Minimum enclosing circle') # add title in titlebar\n", "\n", "#quit_button = tk.Button(root_window, text='Quit (Ctrl-q)', command=do_quit)\n", "#clear_button = tk.Button(root_window, text='Clear all', command=clear_all)\n", "botton_frame = tk.Frame(root_window)\n", "botton_frame.pack(side=tk.BOTTOM, fill=tk.X, expand=False)\n", "quit_button = tk.Button(botton_frame, text='Quit (Ctrl-q)', \n", " command=do_quit, padx=5, pady=5)\n", "clear_button = tk.Button(botton_frame, text='Clear all (Ctrl-x)',\n", " command=clear_all, padx=5, pady=5)\n", "clear_button.pack(side=tk.LEFT, padx=5, pady=5)\n", "quit_button.pack(side=tk.LEFT, pady=5)\n", "#cycle_info = tk.Label(botton_frame, text = 'Draw points...', fg='red')\n", "info_var = tk.StringVar()\n", "info_label = tk.Label(botton_frame, textvariable = info_var, fg='red', padx=10)\n", "#info_var.set('Draw points...')\n", "info_label = info_label.pack(side=tk.RIGHT)\n", "\n", "canvas = tk.Canvas(root_window, width=600, height=300, bg='green')\n", "canvas.pack(side=tk.TOP, expand = tk.YES, fill = tk.BOTH)\n", "\n", "#canvas.create_oval(10, 30, 20, 40, fill=\"white\")\n", "#draw_circle(canvas, (100, 100))\n", "\n", "# Key bindings\n", "root_window.bind('', do_quit)\n", "root_window.bind('', undo)\n", "root_window.bind('', redo)\n", "root_window.bind('', clear_all)\n", "root_window.bind('p', print_points)\n", "\n", "# Mouse bindings\n", "canvas.bind(\"\", do_select_point) # Button-1 == ButtonPress-1\n", "canvas.bind(\"\", do_move_point)\n", "canvas.bind(\"\", do_unselect)\n", "\n", "redraw()\n", "tk.mainloop() # Wait for events and handle them" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.9.4" } }, "nbformat": 4, "nbformat_minor": 5 }