Skip to main content

3. Click Events

map with markers added by mouse clicks

Left click​

add_left_click_map_command sets a function that is called when the user clicks on the map without moving it. The function gets the position of the click as a tuple (latitude, longitude). In this example every click adds a marker:

self.map_widget.add_left_click_map_command(self.map_clicked)

def add_marker(self, coordinates):
marker = self.map_widget.set_marker(*coordinates, text=f"Point {len(self.markers) + 1}",
command=self.marker_clicked)
self.markers.append(marker)
self.show_position(marker)

Right click menu​

A right click opens a menu, whose first entry copies the coordinates of the point to the clipboard. With add_right_click_menu_command you can add your own entries. With pass_coords=True the function gets the position of the right click:

self.map_widget.add_right_click_menu_command(label="Add marker here", command=self.add_marker,
pass_coords=True)
self.map_widget.add_right_click_menu_command(label="Show address", command=self.show_address,
pass_coords=True)

Marker clicks and map clicks​

A click on a marker is also a click on the map. First the command of the marker is called, then the function for the map click, so without a check every click on a marker would add another marker. Therefore the marker click sets a flag, and the map click ignores the next click if the flag is set:

def map_clicked(self, coordinates):
if self.marker_was_clicked:
# a click on a marker is also a click on the map, so we ignore this one
self.marker_was_clicked = False
return
self.add_marker(coordinates)

def marker_clicked(self, marker):
self.marker_was_clicked = True
self.show_position(marker)

Address of a point​

To get the address of a position, we use the Nominatim service of OpenStreetMap through the geopy package, which is installed together with TkinterMapView. Like the tile server, Nominatim requires a user agent with the name of your app. We use the same one as the map, which is stored in self.map_widget.user_agent:

from geopy.geocoders import Nominatim

# the address lookup uses the same User-Agent as the map
self.geolocator = Nominatim(user_agent=self.map_widget.user_agent)

The request can take a moment, so it runs in a thread and the result is passed back to the main thread with a queue. This is the same pattern as in Long Tasks without Freezing:

def show_address(self, coordinates):
self.info_label.configure(text="Looking up the address ...")
# the request to the server can take a moment, so it runs in a thread
threading.Thread(target=self.look_up_address, args=(coordinates,), daemon=True).start()
self.check_address_queue()

def look_up_address(self, coordinates):
try:
location = self.geolocator.reverse(coordinates, language="en")
text = location.address if location else "No address found."
except Exception as error:
text = f"The address lookup failed: {error}"
self.address_queue.put(text)

The public Nominatim service allows at most one request per second, so it is fine for clicks of a user, but not for looking up many addresses in a loop. The rules are in the Nominatim usage policy.

Full code​

click_events.py
import queue
import threading
import customtkinter
import tkintermapview
from geopy.geocoders import Nominatim


class App(customtkinter.CTk):
def __init__(self):
super().__init__()

self.title("Click events")
self.geometry("900x560")
self.grid_columnconfigure(1, weight=1)
self.grid_rowconfigure(0, weight=1)

self.markers = []
self.marker_was_clicked = False
self.address_queue = queue.Queue()

self.side_frame = customtkinter.CTkFrame(self, width=220, corner_radius=0)
self.side_frame.grid(row=0, column=0, sticky="nsew")
self.side_frame.grid_propagate(False)
self.side_frame.grid_columnconfigure(0, weight=1)

self.help_label = customtkinter.CTkLabel(self.side_frame, wraplength=180, justify="left", anchor="w",
text="Left click: add a marker\nRight click: menu")
self.help_label.grid(row=0, column=0, padx=20, pady=(20, 10), sticky="ew")
self.info_label = customtkinter.CTkLabel(self.side_frame, text="", wraplength=180, justify="left",
anchor="w")
self.info_label.grid(row=1, column=0, padx=20, pady=10, sticky="ew")
self.clear_button = customtkinter.CTkButton(self.side_frame, text="Remove markers",
command=self.clear_markers)
self.clear_button.grid(row=2, column=0, padx=20, pady=10, sticky="ew")

self.map_widget = tkintermapview.TkinterMapView(self, corner_radius=0)
self.map_widget.grid(row=0, column=1, sticky="nsew")
self.map_widget.set_position(48.137154, 11.576124) # Munich
self.map_widget.set_zoom(13)

# the address lookup uses the same User-Agent as the map
self.geolocator = Nominatim(user_agent=self.map_widget.user_agent)

self.map_widget.add_left_click_map_command(self.map_clicked)
self.map_widget.add_right_click_menu_command(label="Add marker here", command=self.add_marker,
pass_coords=True)
self.map_widget.add_right_click_menu_command(label="Show address", command=self.show_address,
pass_coords=True)

def map_clicked(self, coordinates):
if self.marker_was_clicked:
# a click on a marker is also a click on the map, so we ignore this one
self.marker_was_clicked = False
return
self.add_marker(coordinates)

def add_marker(self, coordinates):
marker = self.map_widget.set_marker(*coordinates, text=f"Point {len(self.markers) + 1}",
command=self.marker_clicked)
self.markers.append(marker)
self.show_position(marker)

def marker_clicked(self, marker):
self.marker_was_clicked = True
self.show_position(marker)

def show_position(self, marker):
latitude, longitude = marker.position
self.info_label.configure(text=f"{marker.text}\n{latitude:.5f}, {longitude:.5f}")

def clear_markers(self):
for marker in self.markers:
marker.delete()
self.markers = []
self.info_label.configure(text="")

def show_address(self, coordinates):
self.info_label.configure(text="Looking up the address ...")
# the request to the server can take a moment, so it runs in a thread
threading.Thread(target=self.look_up_address, args=(coordinates,), daemon=True).start()
self.check_address_queue()

def look_up_address(self, coordinates):
try:
location = self.geolocator.reverse(coordinates, language="en")
text = location.address if location else "No address found."
except Exception as error:
text = f"The address lookup failed: {error}"
self.address_queue.put(text)

def check_address_queue(self):
if self.address_queue.empty():
self.after(100, self.check_address_queue)
else:
self.info_label.configure(text=self.address_queue.get())


app = App()
app.mainloop()

Common errors​

403 error when looking up an address

The Nominatim service rejected the request, usually because of a missing or generic user agent. Use geopy with a user agent that names your app, like above. The helper functions convert_coordinates_to_address, convert_coordinates_to_city and convert_coordinates_to_country of TkinterMapView don't let you set a user agent.

Error copying to clipboard on Linux

The default entry of the right click menu needs a clipboard program on Linux. Install xclip, for example with sudo apt-get install xclip.