3. Click Events
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
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.