Example applications

myTk ships a set of runnable example apps in mytk/example_apps/. Launch any of them as a module, for example:

python -m mytk.example_apps.example

Each section shows what the example demonstrates, a screenshot and its full source.

example.py

Comprehensive tour of myTk widgets in a single window.

Lays out Labels, entries, PopupMenu, URLLabel, Image, TableView, Figure, Slider, Level, Checkbox and a CanvasView (raw and CanvasElement APIs), and conditionally adds View3D and VideoView when their optional dependencies are available.

Loads logo.png from this directory; View3D/VideoView/drag-and-drop are optional.

Run it with python -m mytk.example_apps.example.

example.py screenshot
  1"""example.py — Comprehensive tour of myTk widgets in a single window.
  2
  3Lays out Labels, entries, PopupMenu, URLLabel, Image, TableView, Figure, Slider,
  4Level, Checkbox and a CanvasView (raw and CanvasElement APIs), and conditionally
  5adds View3D and VideoView when their optional dependencies are available.
  6
  7Loads ``logo.png`` from this directory; View3D/VideoView/drag-and-drop are optional.
  8"""
  9from pathlib import Path
 10from tkinter import DoubleVar, filedialog
 11
 12from mytk.canvasview import Arrow, CanvasLabel, Line, Oval, Rectangle
 13from mytk.vectors import Point
 14
 15if __name__ == "__main__":
 16    # package_app_script()
 17    from mytk import *
 18
 19    app = App(geometry="1350x720")
 20    app.window.widget.title("Example application myTk")
 21
 22    label_box1 = Box(label="Label (centered)")
 23    label_box1.grid_into(app.window, column=0, row=0, pady=2, padx=3, sticky="nsew")
 24    label1 = Label("Centered label")
 25    label1.grid_into(label_box1, column=0, row=0, pady=2, padx=3, sticky="")
 26
 27    label_box2 = Box(label="Label (top-aligned)")
 28    label_box2.grid_into(app.window, column=1, row=0, pady=2, padx=3, sticky="nsew")
 29    label2 = Label("Top-aligned label")
 30    label2.grid_into(label_box2, column=0, row=0, pady=2, padx=3, sticky="n")
 31
 32    box1 = Box(label="Box / LabelledEntry")
 33    box1.grid_into(app.window, column=2, row=0, pady=2, padx=3, sticky="nsew")
 34    thing1 = LabelledEntry(label="Centered entry", character_width=5)
 35    thing1.grid_into(box1, column=0, row=0, padx=6)
 36    thing2 = LabelledEntry(label="Left-aligned entry", character_width=5)
 37    thing2.grid_into(box1, column=0, row=1, padx=4, sticky='w')
 38
 39    view = Box(label="PopupMenu")
 40    view.grid_into(app.window, column=0, row=1, pady=2, padx=3, sticky="nsew")
 41    view.widget.grid_rowconfigure(0, weight=0)
 42    view.widget.grid_rowconfigure(1, weight=0)
 43    view.widget.grid_rowconfigure(2, weight=1)
 44    popup_label = Label(text='A popup menu')
 45    popup_label.grid_into(view, column=0, row=0, pady=2, padx=3, sticky="")
 46    popup = PopupMenu(menu_items=["Option 1", "Option 2", "Option 3"])
 47    popup.grid_into(view, column=1, row=0, pady=2, padx=3, sticky="")
 48    linked_label = Label(text='Label below is bound to the popup')
 49    linked_label.grid_into(view, column=0, columnspan=2, row=1, pady=2, padx=3, sticky="w")
 50    linked_label2 = Label(text='')
 51    linked_label2.grid_into(view, column=0, row=2, pady=2, padx=3, sticky="n")
 52    popup.bind_properties("value_variable", linked_label2, "value_variable")
 53    popup.selection_changed(0)
 54    def choose_file(event, button):
 55        filedialog.askopenfilename()
 56    button = Button("Choose file…", user_event_callback=choose_file)
 57    button.grid_into(view, column=0, row=3, pady=2, padx=3, sticky="n")
 58
 59    view2 = Box(label="URLLabel")
 60    view2.grid_into(app.window, column=1, row=1, pady=2, padx=3, sticky="nsew")
 61    view2.widget.grid_columnconfigure(0, weight=2)
 62    url1_label = Label(text='Clickable links:')
 63    url1_label.grid_into(view2, column=0, row=0, pady=2, padx=3)
 64    url1 = URLLabel("http://www.python.org")
 65    url1.grid_into(view2, column=1, row=0, pady=2, padx=3)
 66    url2_label = Label(text='Custom link text:')
 67    url2_label.grid_into(view2, column=0, row=1, pady=2, padx=3)
 68    url2 = URLLabel(url="http://www.python.org", text="The Python website")
 69    url2.grid_into(view2, column=1, row=1, pady=2, padx=3, sticky="nsew")
 70
 71    example_dir = Path(__file__).parent
 72    image_box = Box(label="Image")
 73    image_box.grid_into(app.window, column=2, row=1, pady=2, padx=3, sticky="")
 74    image = Image(example_dir / "logo.png")
 75    image.pil_image = image.pil_image.resize((66, 80), image.PILImage.LANCZOS)
 76    image.grid_into(image_box, column=0, row=0, pady=2, padx=3)
 77
 78    table_box = Box(label="TableView")
 79    table_box.grid_into(app.window, column=3, row=0, pady=2, padx=3, sticky="nsew")
 80
 81    columns = {"column1": "Column #1", "name": "The name", "url": "Clickable URL"}
 82    table = TableView(columns_labels=columns)
 83    table.grid_into(table_box, column=0, row=0, pady=10, padx=10, sticky="ew")
 84    for i in range(3):
 85        table.data_source.append_record({"column1": "Item {0}".format(i), "name": "Something", "url": "http://www.python.org"})
 86    table.widget.configure(height=3)
 87    table.widget.column("column1", width=70)
 88    table.widget.column("name", width=80)
 89    table.widget.column("url", width=120)
 90
 91    figure1 = Figure(figsize=(3, 2))
 92    figure1.grid_into(app.window, column=3, row=1, pady=2, padx=3)
 93    axis = figure1.figure.add_subplot()
 94    axis.plot([1, 2, 3], [4, 5, 6])
 95    axis.set_title("Figure", fontsize=9)
 96
 97    # View3D: an embedded 3D mesh viewer. Only built when its (optional) 3D
 98    # dependencies are already installed, so the example never triggers an
 99    # install prompt; otherwise a placeholder label is shown.
100    import importlib.util
101    view3d_box = Box(label="View3D")
102    view3d_box.grid_into(app.window, column=3, row=2, pady=2, padx=3, sticky="nsew")
103    _has_3d = importlib.util.find_spec("trimesh") and (
104        importlib.util.find_spec("pyrender")
105        or importlib.util.find_spec("moderngl")
106    )
107    if _has_3d:
108        try:
109            import os
110            import tempfile
111
112            import trimesh
113
114            cube = trimesh.creation.box(extents=(1.0, 1.0, 1.0))
115            cube.visual.vertex_colors = (200, 90, 40, 255)
116            cube_path = os.path.join(tempfile.gettempdir(), "example_cube.glb")
117            cube.export(cube_path)
118            viewer3d = View3D(width=160, height=160)
119            viewer3d.grid_into(view3d_box, column=0, row=0, pady=2, padx=3)
120            viewer3d.load_file(cube_path)
121            # Drop a mesh onto the viewer to load it — only when tkinterdnd2 is
122            # already present, so the demo never triggers an install prompt.
123            if importlib.util.find_spec("tkinterdnd2"):
124                viewer3d.accept_dropped_files(
125                    lambda paths: viewer3d.load_file_or_warn(paths[0])
126                )
127        except Exception:
128            Label("Unable to load View3D").grid_into(
129                view3d_box, column=0, row=0, pady=2, padx=3
130            )
131    else:
132        Label("Install pyrender or moderngl\nto see View3D").grid_into(
133            view3d_box, column=0, row=0, pady=2, padx=3
134        )
135
136    video_box = Box(label="VideoView")
137    video_box.grid_into(app.window, column=2, row=2, pady=2, padx=3, sticky="")
138    try:
139        video = VideoView(device=0, zoom_level=8)
140        video.grid_into(video_box, column=0, row=0, pady=2, padx=3, sticky="")
141    except Exception:
142        video = Label("Unable to load VideoView")
143        video.grid_into(video_box, column=0, row=0, pady=2, padx=3, sticky="")
144
145    def i_was_changed(checkbox):
146        Dialog.showwarning(message="The checkbox was modified")
147
148    view3 = Box(label="Checkbox / Slider / Level")
149    view3.grid_into(app.window, column=0, row=2, pady=2, padx=3, sticky="nsew")
150    view3.widget.grid_rowconfigure(0, weight=0)
151    view3.widget.grid_rowconfigure(1, weight=0)
152    view3.widget.grid_rowconfigure(2, weight=0)
153    checkbox = Checkbox(label="Check me!", user_callback=i_was_changed)
154    checkbox.grid_into(view3, column=0, row=0, pady=2, padx=3, sticky="nsew")
155    slider = Slider(width=50)
156    slider.grid_into(view3, column=0, row=1, pady=2, padx=3, sticky="nsew")
157    slider.value_variable.set(0)
158    indicator = NumericIndicator(value_variable=DoubleVar(value=0), format_string="Slider: {0:.1f}%")
159    slider.bind_properties('value_variable', indicator, 'value_variable')
160    indicator.grid_into(view3, column=0, row=2, pady=2, padx=3, sticky="nsew")
161    level = Level()
162    level.grid_into(view3, column=0, row=3, pady=2, padx=3, sticky="nsew")
163    level.bind_properties('value_variable', slider, 'value_variable')
164
165    # --- CanvasView demo: raw API and CanvasElement API ---
166    canvas_box = Box(label="CanvasView")
167    canvas_box.grid_into(app.window, column=1, row=2, pady=2, padx=3, sticky="nsew")
168    canvas = CanvasView(width=160, height=160)
169    canvas.grid_into(canvas_box, column=0, row=0, pady=2, padx=3)
170
171    # Raw canvas API
172    canvas.widget.create_text(7, 7, text="Raw canvas API:", anchor='w', font=('Helvetica', 9, 'bold'))
173    canvas.widget.create_rectangle(2, 16, 156, 71, outline="black", fill="white", width=1)
174    canvas.widget.create_text(15, 30, text="Rectangles, ovals, text", anchor='w')
175    canvas.widget.create_rectangle(8, 42, 30, 62, outline="black", fill="steelblue", width=2)
176    canvas.widget.create_oval(38, 42, 69, 62, outline="darkgreen", fill="lightgreen", width=2)
177    canvas.widget.create_text(77, 52, text="canvas.widget.*", anchor='w', font=('Helvetica', 8))
178
179    # CanvasElement API (canvas.place())
180    canvas.widget.create_text(7, 78, text="CanvasElement API:", anchor='w', font=('Helvetica', 9, 'bold'))
181    rect_elem = Rectangle(size=(41, 20), outline="navy", fill="lightblue", width=2)
182    canvas.place(rect_elem, position=Point(31, 100))
183    oval_elem = Oval(size=(37, 20), outline="darkred", fill="lightyellow", width=2)
184    canvas.place(oval_elem, position=Point(99, 100))
185    arrow_elem = Arrow(start=Point(0, 0), end=Point(48, 0), fill="purple")
186    canvas.place(arrow_elem, position=Point(10, 125))
187    line_elem = Line(points=[Point(0, 0), Point(48, 0)], fill="gray", width=1, dash=(4, 2))
188    canvas.place(line_elem, position=Point(78, 125))
189    lbl_elem = CanvasLabel(font_size=8, text="canvas.place(element, pos)", anchor='w')
190    canvas.place(lbl_elem, position=Point(7, 145))
191
192    # --- Enable/Disable Box demo ---
193    disable_view = Box(label="Box (disable/enable)")
194    disable_view.grid_into(app.window, column=4, row=0, rowspan=3, pady=2, padx=3, sticky="nsew")
195    disable_view.widget.grid_rowconfigure(0, weight=1)
196    disable_view.widget.grid_rowconfigure(1, weight=0)
197
198    demo_box = Box(label="Children can be disabled")
199    demo_box.grid_into(disable_view, column=0, row=0, pady=2, padx=3, sticky="nsew")
200
201    entry_in_box = LabelledEntry(label="An entry", character_width=8)
202    entry_in_box.grid_into(demo_box, column=0, row=0, padx=8, pady=2, sticky='w')
203
204    def noop(event, btn):
205        pass
206    button_in_box = Button("Click me", user_event_callback=noop)
207    button_in_box.grid_into(demo_box, column=0, row=1, padx=8, pady=2)
208
209    checkbox_in_box = Checkbox(label="A checkbox")
210    checkbox_in_box.grid_into(demo_box, column=0, row=2, padx=8, pady=2, sticky='w')
211
212    slider_in_box = Slider(width=100)
213    slider_in_box.grid_into(demo_box, column=0, row=3, padx=8, pady=2, sticky='ew')
214    slider_in_box.value_variable.set(40)
215
216    popup_in_box = PopupMenu(menu_items=["Choice A", "Choice B", "Choice C"])
217    popup_in_box.grid_into(demo_box, column=0, row=4, padx=8, pady=2)
218
219    def toggle_disabled(event, btn):
220        demo_box.is_disabled = not demo_box.is_disabled
221        btn.widget.config(text="Enable box" if demo_box.is_disabled else "Disable box")
222
223    toggle_btn = Button("Disable box", user_event_callback=toggle_disabled)
224    toggle_btn.grid_into(disable_view, column=0, row=1, pady=(2, 8), padx=3)
225
226    app.window.all_resize_weight(1)
227
228    app.mainloop()

canvas_app.py

Interactive optical ray-tracing playground built on CanvasView.

Draws lenses, apertures and rays on a CanvasView using its coordinate-system and dynamic-basis support, with a TableView to edit the optical elements and live canvas refreshes driven by property binding and notifications.

Needs the optional raytracing package (and pyperclip for “Copy script”).

Run it with python -m mytk.example_apps.canvas_app.

canvas_app.py screenshot
  1"""canvas_app.py — Interactive optical ray-tracing playground built on CanvasView.
  2
  3Draws lenses, apertures and rays on a CanvasView using its coordinate-system and
  4dynamic-basis support, with a TableView to edit the optical elements and live
  5canvas refreshes driven by property binding and notifications.
  6
  7Needs the optional ``raytracing`` package (and ``pyperclip`` for "Copy script").
  8"""
  9import colorsys
 10from tkinter import filedialog
 11
 12import pyperclip
 13from numpy import isfinite
 14from raytracing import *
 15
 16from mytk import *
 17from mytk.base import BaseNotification
 18from mytk.canvasview import *
 19from mytk.dataviews import *
 20from mytk.labels import Label
 21from mytk.notificationcenter import NotificationCenter
 22from mytk.vectors import DynamicBasis, Point, PointDefault
 23
 24
 25class CanvasApp(App):
 26    def __init__(self):
 27        App.__init__(self, name="Raytracing Application")
 28        self.window.widget.title("Raytracing")
 29
 30        self.number_of_heights = 5
 31        self.max_height = 5
 32        self.number_of_angles = 5
 33        self.max_fan_angle = 0.1
 34        self.dont_show_blocked_rays = True
 35        self.show_raytraces = True
 36        self.show_apertures = True
 37        self.show_labels = True
 38        self.show_principal_rays = 1
 39        self.show_conjugates = True
 40        self.show_intermediate_conjugates = False
 41        self.maximum_x = 60
 42        self.initialization_completed = False
 43        self.path_has_field_stop = True
 44
 45        self.create_window_widgets()
 46        self.refresh()
 47
 48    def create_window_widgets(self):
 49        self.table_group = View(width=300, height=300)
 50        self.table_group.grid_into(
 51            self.window, row=0, column=1, pady=5, padx=5, sticky="nsew"
 52        )
 53        self.table_group.column_resize_weight(index=0, weight=1)
 54
 55        self.button_group = View(width=300, height=200)
 56        self.button_group.grid_into(
 57            self.table_group, row=1, column=0, pady=5, padx=5, sticky="nsew"
 58        )
 59
 60        self.add_lens_button = Button(
 61            "Add Lens", user_event_callback=self.click_table_buttons
 62        )
 63        self.add_lens_button.grid_into(
 64            self.button_group, row=0, column=0, pady=5, padx=5
 65        )
 66        self.add_aperture_button = Button(
 67            "Add Aperture", user_event_callback=self.click_table_buttons
 68        )
 69        self.add_aperture_button.grid_into(
 70            self.button_group, row=0, column=1, pady=5, padx=5
 71        )
 72
 73        self.delete_button = Button(
 74            "Delete element", user_event_callback=self.click_table_buttons
 75        )
 76        self.delete_button.grid_into(self.button_group, row=0, column=2, pady=5, padx=5)
 77
 78        self.copy_code_button = Button(
 79            "Copy script", user_event_callback=self.click_copy_buttons
 80        )
 81        self.copy_code_button.grid_into(
 82            self.button_group, row=0, column=3, pady=5, padx=5
 83        )
 84
 85        self.tableview = TableView(
 86            columns_labels={
 87                "element": "Element",
 88                "focal_length": "Focal length [mm]",
 89                "diameter": "Diameter [mm]",
 90                "position": "Position [mm]",
 91                "label": "Label",
 92            }
 93        )
 94        self.tableview.column_formats['focal_length'] = {'format_string':"{0:g}", 'multiplier':1, 'anchor':''}
 95        self.tableview.column_formats['diameter'] = {'format_string':"{0:g}", 'multiplier':1, 'anchor':''}
 96        self.tableview.column_formats['position'] = {'format_string':"{0:g}", 'multiplier':1, 'anchor':''}
 97        self.tableview.data_source.update_field_properties('focal_length', {'type':float})
 98        self.tableview.data_source.update_field_properties('diameter', {'type':float})
 99        self.tableview.data_source.update_field_properties('position', {'type':float})
100
101        self.tableview.grid_into(
102            self.table_group,
103            column=0,
104            row=0,
105            columnspan=2,
106            pady=5,
107            padx=5,
108            sticky="nsew",
109        )
110        self.tableview.displaycolumns = [
111            "element",
112            "position",
113            "focal_length",
114            "diameter",
115            "label",
116        ]
117        for column in self.tableview.displaycolumns:
118            self.tableview.widget.column(column, width=50, anchor=W)
119
120        self.tableview.data_source.append_record(
121            {
122                "element": "Lens",
123                "focal_length": 100,
124                "diameter": 25.4,
125                "position": 200,
126                "label": "L1",
127            }
128        )
129        # self.tableview.data_source.append_record(
130        #     {
131        #         "element": "Aperture",
132        #         "focal_length": "",
133        #         "diameter": 10,
134        #         "position": 400,
135        #         "label": "Camera",
136        #     }
137        # )
138        self.tableview.delegate = self
139
140        self.results_tableview = TableView(
141            columns_labels={
142                "property": "Property",
143                "value": "Value",
144            }
145        )
146        self.results_tableview.grid_into(
147            self.window, column=2, row=0, pady=5, padx=5, sticky="nsew"
148        )
149        self.results_tableview.all_elements_are_editable = False
150        self.results_tableview.widget.column("property", width=250)
151        self.results_tableview.widget.column("value", width=150)
152
153        self.controls = Box(label="Display", width=200)
154        self.controls.grid_into(
155            self.window, column=0, row=0, columnspan=1, pady=5, padx=5, sticky="nsew"
156        )
157
158        self.control_input_rays = Box(label="Input ray", width=200)
159        self.control_input_rays.grid_into(
160            self.controls, column=0, row=0, columnspan=1, pady=5, padx=5, sticky="nsew"
161        )
162
163        radio_principal, radio_custom = RadioButton.linked_group(
164            labels_values={"Principal rays": 1, "Custom rays": 0}
165        )
166        radio_custom.grid_into(
167            self.control_input_rays,
168            column=0,
169            row=0,
170            columnspan=1,
171            pady=5,
172            padx=5,
173            sticky="nsew",
174        )
175        radio_principal.grid_into(
176            self.control_input_rays,
177            column=0,
178            row=4,
179            columnspan=1,
180            pady=5,
181            padx=5,
182            sticky="nsew",
183        )
184
185        radio_principal.bind_properties("is_enabled", self, "path_has_field_stop")
186
187        self.number_heights_label = Label(text="# ray heights:")
188        self.number_heights_label.grid_into(
189            self.control_input_rays, column=0, row=1, pady=5, padx=5, sticky="e"
190        )
191
192        self.number_heights_entry = IntEntry(minimum=1, maximum=100, width=3)
193        self.number_heights_entry.grid_into(
194            self.control_input_rays, column=1, row=1, pady=5, padx=5, sticky="w"
195        )
196
197        self.number_angles_label = Label(text="# ray angles:")
198        self.number_angles_label.grid_into(
199            self.control_input_rays, column=0, row=2, pady=5, padx=5, sticky="e"
200        )
201
202        self.number_angles_entry = IntEntry(minimum=1, maximum=100, width=3)
203        self.number_angles_entry.grid_into(
204            self.control_input_rays, column=1, row=2, pady=5, padx=5, sticky="w"
205        )
206
207        self.max_heights_label = Label(text="Max height:")
208        self.max_heights_label.grid_into(
209            self.control_input_rays, column=3, row=1, pady=5, padx=5, sticky="w"
210        )
211
212        self.max_heights_entry = Entry(character_width=3)
213        self.max_heights_entry.grid_into(
214            self.control_input_rays, column=4, row=1, pady=5, padx=5, sticky="w"
215        )
216
217        self.fan_angles_label = Label(text="Max angle:")
218        self.fan_angles_label.grid_into(
219            self.control_input_rays, column=3, row=2, pady=5, padx=5, sticky="w"
220        )
221
222        self.fan_angles_entry = Entry(character_width=3)
223        self.fan_angles_entry.grid_into(
224            self.control_input_rays, column=4, row=2, pady=5, padx=5, sticky="w"
225        )
226
227        self.show_conjugates_checkbox = Checkbox(label="Show object/image planes")
228        self.show_conjugates_checkbox.grid_into(
229            self.controls, column=0, row=1, columnspan=4, pady=5, padx=5, sticky="w"
230        )
231
232        self.apertures_checkbox = Checkbox(
233            label="Show Aperture stop (AS) and field stop (FS)"
234        )
235        self.apertures_checkbox.grid_into(
236            self.controls, column=0, row=3, columnspan=4, pady=5, padx=5, sticky="w"
237        )
238
239        self.show_labels_checkbox = Checkbox(label="Show object labels")
240        self.show_labels_checkbox.grid_into(
241            self.controls, column=0, row=4, columnspan=4, pady=5, padx=5, sticky="w"
242        )
243
244        self.blocked_rays_checkbox = Checkbox(label="Do not show blocked rays")
245        self.blocked_rays_checkbox.grid_into(
246            self.controls, column=0, row=5, columnspan=4, pady=5, padx=5, sticky="w"
247        )
248
249        self.conjugation_box = Box(label="Conjugation")
250        self.conjugation_box.grid_into(
251            self.controls, column=0, row=6, pady=5, padx=5, sticky="nsew"
252        )
253
254        self.object_conjugate = PopupMenu(menu_items=['Finite object','Infinite object'])
255        self.object_conjugate.grid_into(
256            self.conjugation_box, column=0, row=0, pady=5, padx=5, sticky="w"
257        )
258        self.object_conjugate.selection_changed(0)
259
260        self.image_conjugate = PopupMenu(menu_items=['Finite image','Infinite image'])
261        self.image_conjugate.grid_into(
262            self.conjugation_box, column=1, row=0, pady=5, padx=5, sticky="w"
263        )
264        self.image_conjugate.selection_changed(0)
265
266        self.canvas = CanvasView(width=1000, height=400, background="white")
267        self.canvas.grid_into(
268            self.window, column=0, row=1, columnspan=3, pady=5, padx=5, sticky="nsew"
269        )
270
271        NotificationCenter().add_observer(self, self.canvas_did_resize, BaseNotification.did_resize)
272
273        self.window.column_resize_weight(index=1, weight=1)
274        self.window.row_resize_weight(index=1, weight=1)
275
276        self.coords_origin = Point(0.05, 0.5, basis=DynamicBasis(self.canvas, "relative_basis"))
277
278        size = (Vector(0.85,0, basis=DynamicBasis(self.canvas, "relative_basis")), Vector(0,-0.6, basis=DynamicBasis(self.canvas, "relative_basis")))
279        self.coords = XYCoordinateSystemElement(
280            size=size, axes_limits=((0, 400), (-25, 25)), width=2
281        )
282        self.coords.nx_major = 20
283        self.coords.ny_major = 10
284
285        self.canvas.place(self.coords, position=Point(0.05, 0.5, basis=self.canvas.relative_basis))
286        optics_basis = DynamicBasis(self.coords, "basis")
287
288        self.bind_properties(
289            "number_of_heights", self.number_heights_entry, "value_variable"
290        )
291        self.bind_properties(
292            "number_of_angles", self.number_angles_entry, "value_variable"
293        )
294
295        # self.bind_properties('maximum_x', self.maximum_x_entry, 'value_variable')
296        self.bind_properties(
297            "dont_show_blocked_rays", self.blocked_rays_checkbox, "value_variable"
298        )
299        self.bind_properties(
300            "show_apertures", self.apertures_checkbox, "value_variable"
301        )
302        self.bind_properties("show_labels", self.show_labels_checkbox, "value_variable")
303        self.bind_properties("show_principal_rays", radio_principal, "value_variable")
304
305        self.bind_properties(
306            "show_conjugates", self.show_conjugates_checkbox, "value_variable"
307        )
308        self.bind_properties("max_height", self.max_heights_entry, "value_variable")
309        self.bind_properties("max_fan_angle", self.fan_angles_entry, "value_variable")
310        self.number_heights_entry.bind_properties(
311            "is_disabled", self, "show_principal_rays"
312        )
313        self.number_angles_entry.bind_properties(
314            "is_disabled", self, "show_principal_rays"
315        )
316        self.max_heights_entry.bind_properties(
317            "is_disabled", self, "show_principal_rays"
318        )
319        self.fan_angles_entry.bind_properties(
320            "is_disabled", self, "show_principal_rays"
321        )
322        self.blocked_rays_checkbox.bind_properties(
323            "is_disabled", self, "show_principal_rays"
324        )
325
326        self.add_observer(self, "number_of_heights")
327        self.add_observer(self, "number_of_angles")
328        self.add_observer(self, "dont_show_blocked_rays")
329        self.add_observer(self, "show_apertures")
330        self.add_observer(self, "show_principal_rays")
331        self.add_observer(self, "show_labels")
332        self.add_observer(self, "show_conjugates")
333
334        self.initialization_completed = True
335
336    def canvas_did_resize(self, notification):
337        self.refresh()
338
339    def observed_property_changed(
340        self, observed_object, observed_property_name, new_value, context
341    ):
342        super().observed_property_changed(
343            observed_object, observed_property_name, new_value, context
344        )
345        if context == "refresh_graph":
346            self.canvas.widget.delete(self.coords.id)
347            self.coords.axes_limits = ((0, float(self.maximum_x)), (-50, 50))
348
349        self.refresh()
350
351    def source_data_changed(self, tableview):
352        self.refresh()
353
354    def click_copy_buttons(self, event, button):
355        if button == self.copy_code_button:
356            script = self.get_path_script()
357            pyperclip.copy(script)
358
359    def click_table_buttons(self, event, button):
360        path = self.get_path_from_ui(without_apertures=False)
361
362        if button == self.delete_button:
363            for selected_item in self.tableview.widget.selection():
364                record = self.tableview.data_source.record(selected_item)
365                self.tableview.data_source.remove_record(selected_item)
366        elif button == self.add_lens_button:
367            record = self.tableview.data_source.empty_record()
368            record["element"] = "Lens"
369            record["position"] = position = path.L + 50
370            record["focal_length"] = 50
371            record["diameter"] = 25.4
372            self.tableview.data_source.append_record(record)
373        elif button == self.add_aperture_button:
374            record = self.tableview.data_source.empty_record()
375            record["element"] = "Aperture"
376            record["position"] = position = path.L + 50
377            record["diameter"] = 25.4
378            record["focal_length"] = None
379            self.tableview.data_source.append_record(record)
380
381    def refresh(self):
382        if not self.initialization_completed:
383            return
384
385        self.tableview.sort_column(column_name='position')
386
387        self.canvas.widget.delete("ray")
388        self.canvas.widget.delete("optics")
389        self.canvas.widget.delete("apertures")
390        self.canvas.widget.delete("labels")
391        self.canvas.widget.delete("conjugates")
392        self.canvas.widget.delete("x-axis")
393        self.canvas.widget.delete("y-axis")
394        self.canvas.widget.delete("tick")
395        self.canvas.widget.delete("tick-label")
396
397        user_provided_path = self.get_path_from_ui(without_apertures=True, max_position=None)
398        finite_imaging_path = None
399        finite_path = None
400
401        conjugate = user_provided_path.forwardConjugate()
402
403        if isfinite(conjugate.d):
404            image_position = user_provided_path.L + conjugate.d
405            finite_imaging_path = self.get_path_from_ui(without_apertures=False, max_position=image_position)
406
407        finite_path = finite_imaging_path
408        if finite_path is None:
409            finite_path = self.get_path_from_ui(without_apertures=False, max_position=self.coords.axes_limits[0][1])
410
411        self.path_has_field_stop = finite_path.hasFieldStop()
412
413        self.adjust_axes_limits(finite_path)
414
415        self.coords.create_x_axis()
416        self.coords.create_x_major_ticks()
417        self.coords.create_x_major_ticks_labels()
418        self.coords.create_y_axis()
419        self.coords.create_y_major_ticks()
420        self.coords.create_y_major_ticks_labels()
421
422
423        self.calculate_imaging_path_results(finite_imaging_path)
424
425        self.create_optical_path(finite_path, self.coords)
426
427        if self.show_raytraces:
428            self.create_all_traces(finite_path)
429
430        if self.show_conjugates:
431            self.create_conjugate_planes(finite_path)
432
433        if self.show_apertures:
434            self.create_apertures_labels(finite_path)
435
436        if self.show_labels:
437            self.create_object_labels(finite_path)
438
439    def adjust_axes_limits(self, path):
440        half_diameter = max( filter(lambda e:e is not None and type(e) != str, self.tableview.data_source.field('diameter')))/2
441        raytraces = self.raytraces_to_display(path)
442        y_min, y_max = self.raytraces_limits(raytraces)
443
444        self.coords.axes_limits = ((0, path.L), (min(y_min,-half_diameter)*1.1, max(y_max,half_diameter)*1.1))
445
446
447    def raytraces_limits(self, raytraces):
448        ys = []
449        for raytrace in raytraces:
450            ys.extend([ray.y for ray in raytrace ])
451        y_max = max(ys)
452        y_min = min(ys)
453        return y_min, y_max
454
455    def raytraces_to_display(self, path):
456        if self.show_principal_rays:
457            principal_ray = path.principalRay()
458            if principal_ray is not None:
459                principal_raytrace = path.trace(principal_ray)
460                axial_ray = path.axialRay()
461                axial_raytrace = path.trace(axial_ray)
462                return [principal_raytrace, axial_raytrace]
463        else:
464            M = int(self.number_of_heights)
465            N = int(self.number_of_angles)
466            yMax = float(self.max_height)
467            thetaMax = float(self.max_fan_angle)
468
469            if M == 1:
470                yMax = 0
471            if N == 1:
472                thetaMax = 0
473            rays = UniformRays(yMax=yMax, thetaMax=thetaMax, M=M, N=N)
474            return path.traceMany(rays)
475
476    def create_all_traces(self, path):
477        if self.show_principal_rays:
478            principal_ray = path.principalRay()
479            if principal_ray is not None:
480                principal_raytrace = path.trace(principal_ray)
481                line_trace = self.create_line_from_raytrace(
482                    principal_raytrace, basis=DynamicBasis(self.coords, "basis"), color="green"
483                )
484                self.coords.place(line_trace, position=Point(0, 0))
485
486                axial_ray = path.axialRay()
487                axial_raytrace = path.trace(axial_ray)
488                line_trace = self.create_line_from_raytrace(
489                    axial_raytrace, basis=DynamicBasis(self.coords, "basis"), color="red"
490                )
491                self.coords.place(line_trace, position=Point(0, 0))
492
493        else:
494            M = int(self.number_of_heights)
495            N = int(self.number_of_angles)
496            yMax = float(self.max_height)
497            thetaMax = float(self.max_fan_angle)
498
499            if M == 1:
500                yMax = 0
501            if N == 1:
502                thetaMax = 0
503            rays = UniformRays(yMax=yMax, thetaMax=thetaMax, M=M, N=N)
504            self.create_raytraces_lines(path, rays)
505
506    def create_conjugate_planes(self, path):
507        arrow_width = 10
508        object_z = 0
509        object_height = float(self.max_height) * 2
510        if self.show_principal_rays:
511            object_height = path.fieldOfView()
512
513        basis = DynamicBasis(self.coords, "basis")
514        canvas_object = Arrow(
515            start=Point(object_z, -object_height / 2, basis=basis),
516            end=Point(object_z, object_height / 2, basis=basis),
517            fill="blue",
518            width=arrow_width,
519            tag=("conjugates"),
520        )
521        self.coords.place(canvas_object, position=Point(0, 0))
522
523        conjugate = path.forwardConjugate()
524        if conjugate.transferMatrix is not None:
525            image_z = conjugate.transferMatrix.L
526            magnification = conjugate.transferMatrix.magnification().transverse
527            image_height = magnification * object_height
528            canvas_image = Arrow(
529                start=Point(image_z, -image_height / 2, basis=basis),
530                end=Point(image_z, image_height / 2, basis=basis),
531                fill="red",
532                width=arrow_width,
533                tag=("conjugates"),
534            )
535            self.coords.place(canvas_image, position=Point(0, 0))
536
537    def create_apertures_labels(self, path):
538        position = path.apertureStop()
539        y_lims = self.coords.axes_limits[1]
540        label_position = y_lims[1] * 1.4
541
542        if position.z is not None:
543            aperture_stop_label = CanvasLabel(text="AS", tag=("apertures"))
544            self.coords.place(
545                aperture_stop_label, position=Point(position.z, label_position)
546            )
547
548        position = path.fieldStop()
549        if position.z is not None:
550            field_stop_label = CanvasLabel(text="FS", tag=("apertures"))
551            self.coords.place(
552                field_stop_label, position=Point(position.z, label_position)
553            )
554
555    def create_object_labels(self, path):
556        z = 0
557        y_lims = self.coords.axes_limits[1]
558        label_position = y_lims[1] * 1.1
559        for element in path:
560            label = CanvasLabel(text=element.label, tag=("labels"))
561            self.coords.place(label, position=Point(z, label_position))
562            z += element.L
563
564    def create_raytraces_lines(self, path, rays):
565        raytraces = path.traceMany(rays)
566
567        if self.dont_show_blocked_rays:
568            raytraces_to_show = [
569                raytrace for raytrace in raytraces if not raytrace[-1].isBlocked
570            ]
571        else:
572            raytraces_to_show = raytraces
573
574        line_traces = self.raytraces_to_lines(raytraces_to_show, DynamicBasis(self.coords, "basis"))
575
576        for line_trace in line_traces:
577            self.canvas.place(line_trace, position=self.coords_origin)
578            self.canvas.widget.tag_lower(line_trace.id)
579
580    def create_optical_path(self, path, coords):
581        z = 0
582        thickness = 3
583        for element in path:
584            if isinstance(element, Lens):
585                diameter = element.apertureDiameter
586                if not isfinite(diameter):
587                    y_lims = self.coords.axes_limits[1]
588                    diameter = 0.98 * (y_lims[1] - y_lims[0])
589                else:
590                    aperture_top = Line(
591                        points=(
592                            Point(-thickness, diameter / 2, basis=coords.basis),
593                            Point(thickness, diameter / 2, basis=coords.basis),
594                        ),
595                        fill="black",
596                        width=4,
597                        tag=("optics"),
598                    )
599                    coords.place(aperture_top, position=Point(z, 0, basis=coords.basis))
600                    aperture_bottom = Line(
601                        points=(
602                            Point(-thickness, -diameter / 2, basis=coords.basis),
603                            Point(thickness, -diameter / 2, basis=coords.basis),
604                        ),
605                        fill="black",
606                        width=4,
607                        tag=("optics"),
608                    )
609                    coords.place(
610                        aperture_bottom, position=Point(z, 0, basis=coords.basis)
611                    )
612
613                lens = Oval(
614                    size=(5, diameter),
615                    basis=coords.basis,
616                    position_is_center=True,
617                    fill="light blue",
618                    outline="black",
619                    width=2,
620                    tag=("optics"),
621                )
622                coords.place(lens, position=Point(z, 0, basis=coords.basis))
623
624            elif isinstance(element, Aperture):
625                diameter = element.apertureDiameter
626                if not isfinite(diameter):
627                    diameter = 90
628
629                aperture_top = Line(
630                    points=(
631                        Point(-thickness, diameter / 2, basis=coords.basis),
632                        Point(thickness, diameter / 2, basis=coords.basis),
633                    ),
634                    fill="black",
635                    width=4,
636                    tag=("optics"),
637                )
638                coords.place(aperture_top, position=Point(z, 0, basis=coords.basis))
639                aperture_bottom = Line(
640                    points=(
641                        Point(-thickness, -diameter / 2, basis=coords.basis),
642                        Point(thickness, -diameter / 2, basis=coords.basis),
643                    ),
644                    fill="black",
645                    width=4,
646                    tag=("optics"),
647                )
648                coords.place(aperture_bottom, position=Point(z, 0, basis=coords.basis))
649
650            z += element.L
651
652    def raytraces_to_lines(self, raytraces, basis):
653        line_traces = []
654
655        all_initial_y = [raytrace[0].y for raytrace in raytraces]
656        max_y = max(all_initial_y)
657        min_y = min(all_initial_y)
658
659        with PointDefault(basis=basis):
660            for raytrace in raytraces:
661                initial_y = raytrace[0].y
662                if float(max_y - min_y) != 0:
663                    hue = (initial_y - min_y) / float(max_y - min_y)
664                else:
665                    hue = 1.0
666                color = self.color_from_hue(hue)
667
668                line_trace = self.create_line_from_raytrace(
669                    raytrace, basis=basis, color=color
670                )
671                line_traces.append(line_trace)
672
673        return line_traces
674
675    def create_line_from_raytrace(self, raytrace, basis, color):
676        points = [Point(r.z, r.y, basis=basis) for r in raytrace]
677        return Line(points, tag=("ray"), fill=color, width=2)
678
679    def color_from_hue(self, hue):
680        rgb = colorsys.hsv_to_rgb(hue, 1, 1)
681        rgbi = (int(rgb[0] * 255), int(rgb[1] * 255), int(rgb[2] * 255))
682        return "#{0:02x}{1:02x}{2:02x}".format(*rgbi)
683
684
685    def get_path_from_ui(self, without_apertures=True, max_position=None):
686        path = ImagingPath()
687
688        z = 0
689        ordered_records = self.tableview.data_source.records
690        if without_apertures:
691            ordered_records = [ record for record in ordered_records if record['element'].lower() != 'aperture']
692
693        ordered_records.sort(key=lambda e: float(e["position"]))
694        if max_position is not None:
695            ordered_records = [ record for record in ordered_records if record['position'] <= max_position]
696
697        for element in ordered_records:
698            path_element = None
699            if element["element"].lower() == "lens":
700                focal_length = float(element["focal_length"])
701                label = element["label"]
702                next_z = float(element["position"])
703                diameter = float("+inf")
704                if element["diameter"] != "":
705                    diameter = float(element["diameter"])
706
707                path_element = Lens(f=focal_length, diameter=diameter, label=label)
708            elif element["element"].lower() == "aperture":
709                label = element["label"]
710                next_z = float(element["position"])
711                diameter = float("+inf")
712                if element["diameter"] != "":
713                    diameter = float(element["diameter"])
714                path_element = Aperture(diameter=diameter, label=label)
715            else:
716                print(f"Unable to include unknown element {element['element']}")
717
718
719            delta = next_z - z
720
721            path.append(Space(d=delta))
722            path.append(path_element)
723            z += delta
724
725        if max_position is not None:
726            if max_position > path.L:
727                path.append(Space(d=max_position-path.L))
728
729        return path
730
731    def get_path_script(self):
732        z = 0
733        ordered_records = self.tableview.data_source.records
734        ordered_records.sort(key=lambda e: float(e["position"]))
735
736        script = "from raytracing import *\n\npath = ImagingPath()\n"
737
738        for element in ordered_records:
739            if element["element"] == "Lens":
740                focal_length = float(element["focal_length"])
741                label = element["label"]
742                next_z = float(element["position"])
743                diameter = float("+inf")
744                if element["diameter"] != "":
745                    diameter = float(element["diameter"])
746                script_line = f"path.append(Lens(f={focal_length}, diameter={diameter}, label='{label}'))\n"
747            elif element["element"] == "Aperture":
748                label = element["label"]
749                next_z = float(element["position"])
750                diameter = float("+inf")
751                if element["diameter"] != "":
752                    diameter = float(element["diameter"])
753                path_element = Aperture(diameter=diameter, label=label)
754                script_line = (
755                    f"path.append(Aperture(diameter={diameter}, label='{label}'))\n"
756                )
757            else:
758                print(f"Unable to include unknown element {element['element']}")
759
760            delta = next_z - z
761            script += f"path.append(Space(d={delta}))\n"
762            script += script_line
763            z += delta
764
765        script += "\n"
766
767        display_help = '''"""
768There are many options for display:
769
7701. Use the principal rays that define the optimal field of view and image size:
771path.display(onlyPrincipalAndAxialRays=True)
772
7732. Specify the rays directly in the display function:
774path.display(diameter=10)
775
7763. Specify the rays for a given object diameter with a different (but equivalent) display function:
777path.displayWithObject(diameter=10, rayNumber=3, fanAngle=0.1, fanNumber=3, removeBlocked=True)
778
7794. Provide your own rays to the display function, independent of any imaging properties
780
781rays = RandomUniformRays(yMax=10, thetaMax=0.1, maxCount=100)
782path.display(rays=rays)
783
784rays = UniformRays(yMax=10, thetaMax=0.1, M=5, N=5)
785path.display(rays=rays)
786
7875. Provide a list of specific rays:
788rays = [Ray(y=0, theta=0.1), Ray(y=0.5, theta=0)]
789path.display(rays=rays)
790"""
791'''
792
793        script += display_help
794
795        script += "\n"
796        if self.show_principal_rays == 0:
797            script += f"rays = UniformRays(yMax={self.max_height}, thetaMax={self.max_fan_angle}, M={self.number_of_heights}, N={self.number_of_angles})\n"
798            script += f"path.display(rays=rays, onlyPrincipalAndAxialRays=False, removeBlocked={self.dont_show_blocked_rays})\n"
799        else:
800            script += "path.display(onlyPrincipalAndAxialRays=True)\n"
801
802        return script
803
804    def calculate_imaging_path_results(self, imaging_path):
805        data_source = self.results_tableview.data_source
806
807        uuids = data_source.sorted_records_uuids(field="__uuid")
808        for uid in uuids:
809            data_source.remove_record(uid)
810
811        if imaging_path is None:
812            data_source.append_record(
813                {"property": "Imaging Path", "value": "Non-imaging/infinite conjugate"}
814            )
815            return
816        """
817        Object and Image positions
818        """
819
820        image_position = imaging_path.L
821
822        data_source.append_record(
823            {"property": "Object position", "value": "0.0 (always)"}
824        )
825        data_source.append_record(
826            {"property": "Image position", "value": f"{image_position:.2f}"}
827        )
828
829        """
830        Aperture Stop and Axial ray
831        """
832        aperture_stop = imaging_path.apertureStop()
833        has_aperture_stop = False
834
835        if aperture_stop.z is not None:
836            has_aperture_stop = True
837
838        if has_aperture_stop:
839            data_source.append_record(
840                {"property": "AS position", "value": f"{aperture_stop.z:.2f}"}
841            )
842            data_source.append_record(
843                {"property": "AS size", "value": f"{aperture_stop.diameter:.2f}"}
844            )
845
846            axial_ray = imaging_path.axialRay()
847            NA = imaging_path.NA()
848            data_source.append_record(
849                {
850                    "property": "Axial ray θ_max",
851                    "value": f"{axial_ray.theta:.2f} rad / {axial_ray.theta*180/3.1416:.2f}°",
852                }
853            )
854            data_source.append_record(
855                {
856                    "property": "NA",
857                    "value": f"{NA:.1f}",
858                }
859            )
860        else:
861            data_source.append_record(
862                {"property": "AS position", "value": "Inexistent"}
863            )
864            data_source.append_record({"property": "AS size", "value": "Inexistent"})
865
866            data_source.append_record(
867                {"property": "Axial ray θ_max", "value": "Inexistent [no AS]"}
868            )
869            data_source.append_record(
870                {
871                    "property": "NA",
872                    "value": "Inexistent",
873                }
874            )
875
876
877        """
878        Field Stop
879        """
880        has_field_stop = False
881        field_stop = imaging_path.fieldStop()
882        if field_stop.z is not None:
883            has_field_stop = True
884
885
886        if has_field_stop:
887            data_source.append_record(
888                {"property": "FS position", "value": f"{field_stop.z:.2f}"}
889            )
890            data_source.append_record(
891                {"property": "FS size", "value": f"{field_stop.diameter:.2f}"}
892            )
893            if field_stop.z < image_position:
894                data_source.append_record(
895                    {"property": "Has vignetting [FS before image]", "value": "True"}
896                )
897            else:
898                data_source.append_record(
899                    {"property": "Has vignetting [FS before image]", "value": "False"}
900                )
901            principal_ray = imaging_path.principalRay()
902            data_source.append_record(
903                {"property": "Principal ray y_max", "value": f"{principal_ray.y:.2f}"}
904            )
905        else:
906            data_source.append_record(
907                {"property": "FS position", "value": "Inexistent"}
908            )
909            data_source.append_record({"property": "FS size", "value": "Inexistent"})
910            data_source.append_record(
911                {"property": "Has vignetting [FS before image]", "value": "Inexistent"}
912            )
913
914            data_source.append_record(
915                {"property": "Principal ray y_max", "value": "Inexistent [no FS]"}
916            )
917
918
919        """
920        Object [FOV] and Image Sizes, dicated by finite FOV
921        """
922        fov = imaging_path.fieldOfView()
923
924        if isfinite(fov):
925            mag_tran, mag_angle = imaging_path.magnification()
926
927            data_source.append_record(
928                {"property": "Field of view [FOV]", "value": f"{fov:.2f}"}
929            )
930            data_source.append_record(
931                {"property": "Object size [same as FOV]", "value": f"{fov:.2f}"}
932            )
933            data_source.append_record(
934                {"property": "Image size", "value": f"{imaging_path.imageSize():.2f}"}
935            )
936            data_source.append_record(
937                {"property": "Magnification [Transverse]", "value": f"{mag_tran:.2f}"}
938            )
939            data_source.append_record(
940                {"property": "Magnification [Angular]", "value": f"{mag_angle:.2f}"}
941            )
942        else:
943            data_source.append_record(
944                {"property": "Field of view [FOV]", "value": "Infinite [no FS]"}
945            )
946            data_source.append_record(
947                {"property": "Object size [same as FOV]", "value": "Infinite [no FS]"}
948            )
949            data_source.append_record(
950                {"property": "Image size", "value": "Infinite [no FS]"}
951            )
952            data_source.append_record(
953                {"property": "Magnification [Transverse]", "value": "Inexistent"}
954            )
955            data_source.append_record(
956                {"property": "Magnification [Angular]", "value": "Inexistent"}
957            )
958
959        self.results_tableview.sort_column(column_name='property')
960
961    def save(self):
962        filepath = filedialog.asksaveasfilename()
963        self.canvas.save_to_pdf(filepath=filepath)
964
965
966if __name__ == "__main__":
967    app = CanvasApp()
968
969    app.mainloop()

controlpanel_app.py

Live acquisition control panel with a real-time plot.

Simulates a continuous acquisition: an XYPlot is appended to on a scheduled timer, BooleanIndicator widgets show status, and property binding enables/disables the controls. Demonstrates the after()/ticker update pattern.

Run it with python -m mytk.example_apps.controlpanel_app.

controlpanel_app.py screenshot
 1"""controlpanel_app.py — Live acquisition control panel with a real-time plot.
 2
 3Simulates a continuous acquisition: an XYPlot is appended to on a scheduled timer,
 4BooleanIndicator widgets show status, and property binding enables/disables the
 5controls. Demonstrates the after()/ticker update pattern.
 6"""
 7import random
 8
 9from mytk import *
10
11
12class ControlPanelApp(App):
13    def __init__(self):
14        App.__init__(self, geometry="800x480", name="Control Panel")
15
16        self.acquisition_is_running = False
17
18        # Base.debug = True
19        self.window.widget.title("Control Panel")
20
21        self.plot = XYPlot(figsize=(6,6))
22        self.plot.grid_into(self.window, row=0, column=0, rowspan=2, columnspan=1, padx=10, pady=10, sticky='new')
23
24        self.controls = Box(label="Controls", width=100, height=100)
25        self.window.widget.grid_rowconfigure(0, weight=0)
26        self.window.widget.grid_rowconfigure(1, weight=1)
27        self.window.widget.grid_columnconfigure(0, weight=1)
28        self.window.widget.grid_columnconfigure(1, weight=0)
29
30        self.controls.grid_into(
31            self.window, column=1, row=0, pady=10, padx=10, sticky="nsew"
32        )
33        self.controls.widget.grid_rowconfigure(0, weight=0)
34        self.controls.widget.grid_rowconfigure(1, weight=0)
35
36        self.start_button = Button("Start", user_event_callback=self.start_process)
37        self.start_button.grid_into(self.controls, row=0, column=0, padx=10, pady=10, sticky='nw')
38        self.stop_button = Button("Stop", user_event_callback=self.stop_process)
39        self.stop_button.grid_into(self.controls, row=0, column=1, padx=10, pady=10, sticky='nw')
40
41        self.status = Box(label="Status", width=500, height=200)
42        self.status.grid_into(
43            self.window, column=1, row=1, columnspan=1, pady=10, padx=10, sticky="new"
44        )
45
46        # BooleanIndicator is a classic tk.Canvas, which is opaque; by default it
47        # now adopts its container's background, so the round indicators blend
48        # into the Status box instead of sitting on a lighter square. Pass an
49        # explicit background_color to override (see Base.background_color).
50        self.running_indicator = BooleanIndicator()
51        self.running_indicator.grid_into(self.status, row=0, column=0, padx=5, pady=5, sticky='e')
52        self.bind_property_to_widget_value('acquisition_is_running', self.running_indicator)
53        self.running_label = Label("System running")
54        self.running_label.grid_into(self.status, row=0, column=1, padx=5, pady=5, sticky='w')
55
56        self.temp_indicator = BooleanIndicator()
57        self.temp_indicator.grid_into(self.status, row=1, column=0, padx=5, pady=5, sticky='e')
58        self.temp_indicator.value_variable.set(value=True)
59        self.temp_label = Label("Temperature")
60        self.temp_label.grid_into(self.status, row=1, column=1, padx=5, pady=5, sticky='w')
61
62        self.humidity_indicator = BooleanIndicator()
63        self.humidity_indicator.grid_into(self.status, row=2, column=0, padx=5, pady=5, sticky='e')
64        self.humidity_indicator.value_variable.set(value=True)
65        self.humidity_label = Label("Humidity")
66        self.humidity_label.grid_into(self.status, row=2, column=1, padx=5, pady=5, sticky='w')
67
68
69        self.plot.widget.after(500, self.ticker)
70
71    def start_process(self, event, button):
72        self.acquisition_is_running = True
73
74    def stop_process(self, event, button):
75        self.acquisition_is_running = False
76
77    def ticker(self):
78        if self.acquisition_is_running:
79            self.append_new_data()
80            self.plot.update_plot()
81
82        self.plot.widget.after(500, self.ticker)
83
84    def append_new_data(self):
85        value =random.gauss()
86
87        if len(self.plot.x) > 0 :
88            x = max(self.plot.x) + 1
89        else:
90            x = 0
91        self.plot.append(x, value)
92
93
94if __name__ == "__main__":
95    app = ControlPanelApp()
96    app.mainloop()

dnd_app.py

drag files from Finder/Explorer onto the window.

Drop one or more files onto the label and their paths are listed. Demonstrates Base.accept_dropped_files, which enables OS file drops on any myTk widget.

python -m mytk.example_apps.dnd_app

Run it with python -m mytk.example_apps.dnd_app.

dnd_app.py screenshot
 1"""dnd_app.py — drag files from Finder/Explorer onto the window.
 2
 3Drop one or more files onto the label and their paths are listed. Demonstrates
 4`Base.accept_dropped_files`, which enables OS file drops on any myTk widget.
 5
 6    python -m mytk.example_apps.dnd_app
 7"""
 8
 9if __name__ == "__main__":
10    from mytk import App, Label
11
12    app = App(bring_to_front=True)
13    app.window.widget.title("Drag & drop files here")
14
15    label = Label("Drop files onto this window…", wrapping=True)
16    label.grid_into(
17        app.window, row=0, column=0, padx=40, pady=40, sticky="nsew"
18    )
19    app.window.row_resize_weight(0, 1)
20    app.window.column_resize_weight(0, 1)
21
22    def show(paths):
23        label.text = "Dropped:\n" + "\n".join(paths)
24
25    if not label.accept_dropped_files(show):
26        label.text = "Drag-and-drop is not available in this environment."
27
28    app.mainloop()

fileviewer_app.py

Minimal directory browser using FileViewer.

Shows a FileViewer with configurable columns (name, size, modification date) and a directory picker, using PostponeChangeCalls for efficient data-source updates.

Run it with python -m mytk.example_apps.fileviewer_app.

fileviewer_app.py screenshot
 1"""fileviewer_app.py — Minimal directory browser using FileViewer.
 2
 3Shows a FileViewer with configurable columns (name, size, modification date) and a
 4directory picker, using PostponeChangeCalls for efficient data-source updates.
 5"""
 6from tkinter import filedialog
 7
 8from mytk import *
 9
10
11class FileViewerApp(App):
12    def __init__(self):
13        App.__init__(self)
14
15        self.window.widget.title("File Viewer")
16        self.window.widget.grid_rowconfigure(0, weight=0)
17        self.window.widget.grid_rowconfigure(1, weight=1)
18        self.window.widget.grid_columnconfigure(0, weight=1)
19
20        self.current_dir = "."
21
22        self.controls = Box()
23        self.controls.grid_into(
24            self.window, column=0, row=0, pady=5, padx=15, sticky="nsew"
25        )
26        self.button = Button("Select directory…", user_event_callback=self.click_choose_directory)
27        self.button.grid_into(
28            self.controls, column=0, row=0, pady=5, padx=15, sticky="nsew"
29        )
30        self.label = Label()
31        self.bind_properties('current_dir', self.label, 'text')
32        self.label.grid_into(
33            self.controls, column=1, row=0, pady=5, padx=15, sticky="nsew"
34        )
35
36        self.fileviewer = FileViewer(self.current_dir)
37        self.fileviewer.grid_into(
38            self.window, column=0, row=1, pady=15, padx=15, sticky="nsew"
39        )
40        self.fileviewer.displaycolumns = ['name','size','modification_date']
41
42    def click_choose_directory(self, button, event):
43        self.current_dir = filedialog.askdirectory()
44        with PostponeChangeCalls(self.fileviewer.data_source):
45            self.fileviewer.data_source.records = []
46            self.fileviewer.data_source.insert_child_records_for_directory(self.current_dir)
47
48
49
50
51if __name__ == "__main__":
52    app = FileViewerApp()
53    app.mainloop()

filters_app.py

Browse a spectral-filter database and plot transmission curves.

A TableView of optical filters drives an XYPlot of the selected filter’s spectrum, with JSON/ZIP data loading (local or downloaded), a preferences Dialog and clipboard export.

Reads data from tpop_filters_data/ (or downloads it); requests/pyperclip are optional.

Run it with python -m mytk.example_apps.filters_app.

filters_app.py screenshot
  1"""filters_app.py — Browse a spectral-filter database and plot transmission curves.
  2
  3A TableView of optical filters drives an XYPlot of the selected filter's spectrum,
  4with JSON/ZIP data loading (local or downloaded), a preferences Dialog and clipboard
  5export.
  6
  7Reads data from ``tpop_filters_data/`` (or downloads it); ``requests``/``pyperclip``
  8are optional.
  9"""
 10import os
 11import re
 12import shutil
 13import subprocess
 14import tempfile
 15import webbrowser
 16import zipfile
 17from pathlib import Path
 18
 19from mytk import *
 20
 21
 22class FilterDBApp(App):
 23    def __init__(self):
 24        App.__init__(self, name="Filter Database")
 25
 26        self.filepath_root = Path(Path(__file__).parent, 'tpop_filters_data')
 27        self.web_root = 'http://www.dccmlab.ca'
 28        self.temp_root = Path(tempfile.TemporaryDirectory().name)
 29        self.download_files = False
 30        self.webbrowser_download_path = None
 31
 32        self.window.widget.title("Filters")
 33        self.window.row_resize_weight(0,1) # Tables
 34        self.window.row_resize_weight(1,0) # Buttons
 35        self.window.row_resize_weight(2,1) # Graph
 36        self.filters = TableView(columns_labels={"part_number":"Part number", "description":"Description","dimensions":"Dimensions","supplier":"Supplier","filename":"Filename","url":"URL", "spectral_x":"Wavelength", "spectral_y":"Transmission"})
 37        self.filters.grid_into(self.window, row=0, column=0, padx=10, pady=10, sticky='nsew')
 38        self.filters.widget['displaycolumn']=["part_number","description","dimensions", "supplier","filename","url"]
 39
 40        self.filters.widget.column(column=0, width=100)
 41        self.filters.widget.column(column=1, width=200)
 42        self.filters.widget.column(column=2, width=120)
 43        self.filters.widget.column(column=3, width=70)
 44        self.filters.delegate = self
 45
 46        self.filter_data = TableView(columns_labels={"wavelength":"Wavelength", "transmission":"Transmission"})
 47        self.filter_data.grid_into(self.window, row=0, column=1, padx=10, pady=10, sticky='nsew')
 48        self.filter_data.widget.column(column=0, width=70)
 49
 50        self.controls = View(width=400, height=50)
 51        self.controls.grid_into(self.window, row=1, column=0, columnspan=2, padx=10, pady=10, sticky='nsew')
 52        self.controls.widget.grid_columnconfigure(0, weight=1)
 53        self.controls.widget.grid_columnconfigure(1, weight=1)
 54        self.controls.widget.grid_columnconfigure(2, weight=1)
 55        self.associate_file_button = Button("Lookup spectral file…", user_event_callback=self.associate_file)
 56        self.associate_file_button.grid_into(self.controls, row=0, column=0, padx=10, pady=10, sticky='nw')
 57        self.open_filter_data_button = Button("Show files", user_event_callback=self.show_files)
 58        self.open_filter_data_button.grid_into(self.controls, row=0, column=1, padx=10, pady=10, sticky='nw')
 59        self.export_filters_button = Button("Export data as Zip…", user_event_callback=self.export_filters)
 60        self.export_filters_button.grid_into(self.controls, row=0, column=2, padx=10, pady=10, sticky='nw')
 61        self.copy_data_button = Button("Copy data to clipboard", user_event_callback=self.copy_data)
 62        self.copy_data_button.grid_into(self.controls, row=0, column=3, padx=10, pady=10, sticky='ne')
 63
 64
 65        self.filter_plot = XYPlot(figsize=(4,4))
 66        self.filter_plot.grid_into(self.window, row=2, column=0, columnspan=2, padx=10, pady=10, sticky='nsew')
 67
 68        self.filters_db = None
 69        self.load()
 70
 71    def load(self):
 72        if self.download_files:
 73            self.filepath_root, filepath = self.get_files_from_web()
 74        else:
 75            filepath = self.filepath_root / "filters.json"
 76
 77            if not self.filepath_root.exists():
 78                self.filepath_root.mkdir(parents=True)
 79                self.filters.data_source.save(filepath)
 80
 81
 82        self.filters.data_source.load(filepath)
 83
 84    def get_files_from_web(self):
 85        install_modules_if_absent(modules={"requests":"requests"})
 86
 87        import requests
 88
 89        url = "/".join([self.web_root, 'filters_data.zip'])
 90        req = requests.get(url, allow_redirects=True)
 91        open('filters_data.zip', 'wb').write(req.content)
 92
 93        with zipfile.ZipFile('filters_data.zip', 'r') as zip_ref:
 94            zip_ref.extractall(self.temp_root)
 95
 96        return self.temp_root / 'filters_data', self.temp_root / 'filters_data' / 'filters.json'
 97
 98    def save(self):
 99        filepath = self.filepath_root / "filters.json"
100        self.filters.save(filepath)
101
102    def load_filter_data(self, filepath):
103        data = []
104        with open(filepath,'r') as file:
105            try:
106                lines = file.readlines()
107                for line in lines:
108                    match = re.search(r'(\d+.\d*)[\s,]+([-+]?[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?)', line)
109                    if match is not None:
110                        try:
111                            x = float(match.group(1))
112                            y = float(match.group(2))
113                            data.append((x,y))
114                        except Exception:
115                            # not an actual data line
116                            pass
117
118            except Exception:
119                if len(data) == 0:
120                    return None
121
122        return data
123
124    def load_filters_table(self, filepath):
125        data = []
126        with open(filepath,'r') as file:
127            try:
128                lines = file.readlines()
129                for line in lines:
130                    records = line.split('\t')
131                    data.append(records)
132            except Exception:
133                if len(data) == 0:
134                    return None
135
136        return data
137
138    def associate_file(self, event, button):
139        for selected_item in self.filters.widget.selection():
140            item = self.filters.widget.item(selected_item)
141            record = item['values']
142
143            part_number_idx = list(self.filters.column_names()).index('part_number')
144            description_idx = list(self.filters.column_names()).index('description')
145            supplier_idx = list(self.filters.column_names()).index('supplier')
146
147            query = str(record[part_number_idx])+"+"+str(record[description_idx])
148            query = query+f"+{record[supplier_idx]}+filter"
149
150            webbrowser.open(f"https://www.google.com/search?q={query}")
151            time.sleep(0.3)
152            browser_app = subprocess.run(["osascript","-e","return path to frontmost application as text"],capture_output=True, encoding='utf8').stdout
153
154            filepath = None
155
156            if self.webbrowser_download_path is None:
157                filepath = filedialog.askopenfilename()
158            else:
159                pre_list = os.listdir(self.webbrowser_download_path)
160                frontmost_app = subprocess.run(["osascript","-e","return path to frontmost application as text"],capture_output=True, encoding='utf8').stdout
161                while frontmost_app == browser_app:
162                    self.window.widget.update_idletasks()
163                    self.window.widget.update()
164                    frontmost_app = subprocess.run(["osascript","-e","return path to frontmost application as text"],capture_output=True, encoding='utf8').stdout
165                post_list = os.listdir(self.webbrowser_download_path)
166
167                new_filepaths = list(set(post_list) - set(pre_list))
168                if len(new_filepaths) == 1:
169                    filepath = Path(self.webbrowser_download_path) / new_filepaths[0]
170                else:
171                    filepath = ''
172
173            if filepath != '':
174                shutil.copy2(filepath, self.filepath_root)
175                filename_idx = list(self.filters.column_names()).index('filename')
176
177                record[filename_idx] = Path(filepath).name
178                self.webbrowser_download_path = Path(filepath).parent
179                self.filters.widget.item(selected_item, values=record)
180                self.save()
181
182    def export_filters(self, event, button):
183        zip_filepath = filedialog.asksaveasfilename(
184            parent=self.window.widget,
185            title="Choose a filename:",
186            filetypes=[('Zip files','.zip')],
187        )
188        if zip_filepath:
189            with zipfile.ZipFile(zip_filepath, 'w') as zip_ref:
190                zip_ref.mkdir(self.filepath_root)
191                for filepath in Path(self.filepath_root).iterdir():
192                    zip_ref.write(filepath, arcname=self.filepath_root / filepath.name)
193
194    def show_files(self, event, button):
195        self.reveal_path(self.filepath_root)
196
197    def copy_data(self, event, button):
198        ModulesManager.install_and_import_modules_if_absent(pip_modules={"pyperclip":"pyperclip"})
199        try:
200            pyperclip = ModulesManager.imported['pyperclip']
201
202            for selected_item in self.filters.widget.selection():
203                item = self.filters.widget.item(selected_item)
204                record = item['values']
205
206                filename_idx = list(self.filters.column_names()).index('filename')
207                filename = record[filename_idx]
208
209                filepath = self.filepath_root / filename
210                if filepath.is_file():
211                    data = self.load_filter_data(filepath)
212
213                    text = ""
214                    for x,y in data:
215                        text = text + "{0}\t{1}\n".format(x,y)
216
217                    pyperclip.copy(text)
218        except Exception as err:
219            print(err)
220            showerror(
221                title="Unable to copy to clipboard",
222                message="You must have the module pyperclip installed to copy the data.",
223            )
224
225    def source_data_changed(self, table):
226        pass
227
228    def selection_changed(self, event, table):
229        for selected_item in table.widget.selection():
230            item = table.widget.item(selected_item)
231            record = item['values']
232
233            filename_idx = list(self.filters.columns).index('filename')
234            filename = record[filename_idx]
235            filepath = self.filepath_root / filename
236            if filepath.exists() and not filepath.is_dir():
237
238                data = self.load_filter_data(filepath)
239                self.filter_data.empty()
240                self.filter_plot.clear_plot()
241                with PostponeChangeCalls(self.filter_data.data_source):
242                    for x,y in data:
243                        # self.filter_data.data_source.append_record({"wavelength":x,"transmission":y})
244                        self.filter_plot.append(x,y)
245
246                self.filter_plot.first_axis.set_ylabel("Transmission")
247                self.filter_plot.first_axis.set_xlabel("Wavelength [nm]")
248                self.filter_plot.update_plot()
249                self.copy_data_button.enable()
250
251            else:
252                self.filter_data.empty()
253                self.filter_plot.clear_plot()
254                self.filter_plot.update_plot()
255                self.copy_data_button.disable()
256
257    def preferences(self):
258        dlg = Dialog(title="Preferences")
259
260        radio_directory = RadioButton(label="Database directory", value="local")
261        radio_directory.grid_into(widget=dlg.widget, row=0, column=0, padx=10, pady=10, sticky='nw')
262        entry_directory = Entry()
263        entry_directory.grid_into(widget=dlg.widget, row=0, column=1, padx=10, pady=10, sticky='nw')
264
265        radio_url = RadioButton(label="Website URL", value="url")
266        radio_url.grid_into(widget=dlg.widget, row=1, column=0, padx=10, pady=10, sticky='nw')
267        entry_url = Entry()
268        entry_url.grid_into(widget=dlg.widget, row=1, column=1, padx=10, pady=10, sticky='nw')
269
270        entry_directory.bind_properties("is_enabled", radio_directory, "is_selected")
271        entry_url.bind_properties("is_enabled", radio_url, "is_selected")
272
273        source = StringVar()
274        radio_directory.bind_variable(source)
275        radio_url.bind_variable(source)
276        source.set(value='local')
277
278        button_cancel = Button("Cancel")
279        button_cancel.grid_into(widget=dlg.widget, row=2, column=0, padx=10, pady=10, sticky='e')
280        button_ok = Button("Ok")
281        button_ok.grid_into(widget=dlg.widget, row=2, column=1, padx=10, pady=10, sticky='e')
282
283        dlg.run()
284
285
286
287if __name__ == "__main__":
288    ModulesManager.validate_environment(pip_modules={"requests":"requests","pyperclip":"pyperclip"}, ask_for_confirmation=False)
289    app = FilterDBApp()
290    app.mainloop()

formatted_entry_app.py

Showcase of FormattedEntry number formatting.

Lays out FormattedEntry widgets with different format strings and reverse-parse regexes (plain, fixed decimals, scientific, percentage), bound to values via bind_property_to_widget_value inside a Box layout.

Run it with python -m mytk.example_apps.formatted_entry_app.

formatted_entry_app.py screenshot
 1"""formatted_entry_app.py — Showcase of FormattedEntry number formatting.
 2
 3Lays out FormattedEntry widgets with different format strings and reverse-parse
 4regexes (plain, fixed decimals, scientific, percentage), bound to values via
 5bind_property_to_widget_value inside a Box layout.
 6"""
 7if __name__ == "__main__":
 8    from mytk import *
 9    from mytk.entries import FormattedEntry
10
11    def make_row(parent, row, label_text, format_string, reverse_regex, initial_value):
12        """Add a labelled FormattedEntry row and a kept-value readout."""
13        Label(label_text).grid_into(parent, row=row, column=0, padx=8, pady=4, sticky="e")
14
15        entry = FormattedEntry(
16            value=initial_value,
17            format_string=format_string,
18            reverse_regex=reverse_regex,
19            character_width=14,
20        )
21        entry.grid_into(parent, row=row, column=1, padx=8, pady=4)
22
23        readout = Label("–")
24        readout.grid_into(parent, row=row, column=2, padx=8, pady=4, sticky="w")
25        entry.bind_property_to_widget_value("value", readout)
26
27        return entry
28
29    app = App(geometry="560x280")
30    app.window.widget.title("FormattedEntry demo")
31
32    box = Box(label="FormattedEntry — tab between fields to commit each value")
33    box.grid_into(app.window, row=0, column=0, padx=10, pady=10, sticky="nsew")
34    app.window.widget.grid_rowconfigure(0, weight=1)
35    app.window.widget.grid_columnconfigure(0, weight=1)
36
37    # Header labels
38    Label("format string").grid_into(box, row=0, column=1, padx=8, pady=(6, 0))
39    Label("kept value").grid_into(box, row=0, column=2, padx=8, pady=(6, 0))
40
41    rows = [
42        ("default  {0}",        r"{0}",          r"(.+)",                  3.14159),
43        ("2 decimals  {0:.2f}", r"{0:.2f}",       r"([-+]?\d*\.?\d+)",     3.14159),
44        ("4 decimals  {0:.4f}", r"{0:.4f}",       r"([-+]?\d*\.?\d+)",     3.14159),
45        ("scientific  {0:.3e}", r"{0:.3e}",       r"([-+]?\d*\.?\d+[eE][-+]?\d+|[-+]?\d*\.?\d+)", 0.000314),
46        ("percentage  {0:.1%}", r"{0:.1%}",       r"([-+]?\d*\.?\d+)",     0.856),
47    ]
48
49    for i, (label, fmt, regex, val) in enumerate(rows):
50        make_row(box, row=i + 1, label_text=label, format_string=fmt,
51                 reverse_regex=regex, initial_value=val)
52
53    app.mainloop()

jsoncanvas_app.py

Minimal viewer for JSON Canvas 1.0 documents.

Loads a built-in sample on startup. Click “Load…” to open a .canvas or .json file that follows the JSON Canvas 1.0 spec.

Run it with python -m mytk.example_apps.jsoncanvas_app.

jsoncanvas_app.py screenshot
 1"""
 2jsoncanvas_app.py — Minimal viewer for JSON Canvas 1.0 documents.
 3
 4Loads a built-in sample on startup. Click "Load…" to open a `.canvas` or
 5`.json` file that follows the JSON Canvas 1.0 spec.
 6"""
 7
 8from tkinter import filedialog
 9
10
11SAMPLE_DOCUMENT = {
12    "nodes": [
13        {"id": "group",  "type": "group", "x": -40, "y": -40,
14         "width": 740, "height": 400, "label": "Demo group", "color": "5"},
15        {"id": "intro",  "type": "text", "x": 20, "y": 20,
16         "width": 200, "height": 80, "text": "JSON Canvas 1.0\nviewer", "color": "4"},
17        {"id": "readme", "type": "file", "x": 260, "y": 20,
18         "width": 200, "height": 80, "file": "README.md", "subpath": "#overview"},
19        {"id": "site",   "type": "link", "x": 500, "y": 20,
20         "width": 200, "height": 80, "url": "https://jsoncanvas.org"},
21        {"id": "note",   "type": "text", "x": 260, "y": 200,
22         "width": 200, "height": 100,
23         "text": "Edges connect sides.\nArrowheads follow toEnd.",
24         "color": "3"},
25    ],
26    "edges": [
27        {"id": "e1", "fromNode": "intro",  "toNode": "readme",
28         "fromSide": "right", "toSide": "left", "label": "points to"},
29        {"id": "e2", "fromNode": "readme", "toNode": "site",
30         "fromSide": "right", "toSide": "left"},
31        {"id": "e3", "fromNode": "readme", "toNode": "note",
32         "fromSide": "bottom", "toSide": "top", "color": "1"},
33        {"id": "e4", "fromNode": "intro", "toNode": "note",
34         "fromEnd": "arrow", "toEnd": "arrow", "color": "6"},
35    ],
36}
37
38
39if __name__ == "__main__":
40    from mytk import App, Button, JSONCanvas
41
42    app = App(bring_to_front=True)
43    app.window.widget.title("JSON Canvas viewer")
44
45    canvas = JSONCanvas(width=800, height=500)
46    canvas.grid_into(app.window, row=0, column=0, columnspan=2,
47                     padx=10, pady=10, sticky="nsew")
48
49    def load_file():
50        path = filedialog.askopenfilename(
51            title="Open a JSON Canvas file",
52            filetypes=[("JSON Canvas", "*.canvas *.json"), ("All files", "*.*")],
53        )
54        if path:
55            canvas.load_from_file(path)
56
57    def load_sample():
58        canvas.load(SAMPLE_DOCUMENT)
59
60    Button("Load\u2026", user_event_callback=lambda e, b: load_file()).grid_into(
61        app.window, row=1, column=0, padx=10, pady=(0, 10), sticky="w"
62    )
63    Button("Reload sample",
64           user_event_callback=lambda e, b: load_sample()).grid_into(
65        app.window, row=1, column=1, padx=10, pady=(0, 10), sticky="e"
66    )
67
68    app.window.row_resize_weight(0, 1)
69    app.window.column_resize_weight(0, 1)
70    app.window.column_resize_weight(1, 1)
71
72    load_sample()
73    app.mainloop()

lensviewer_app.py

Catalogue viewer for optical lenses.

A TableView of lens specifications (from the raytracing package) drives matplotlib Figures showing the ray-trace diagram and focal-shift dispersion of the selected lens, via the TableView delegate pattern.

Needs the optional raytracing package.

Run it with python -m mytk.example_apps.lensviewer_app.

lensviewer_app.py screenshot
  1"""lensviewer_app.py — Catalogue viewer for optical lenses.
  2
  3A TableView of lens specifications (from the raytracing package) drives matplotlib
  4Figures showing the ray-trace diagram and focal-shift dispersion of the selected
  5lens, via the TableView delegate pattern.
  6
  7Needs the optional ``raytracing`` package.
  8"""
  9from contextlib import suppress
 10
 11import raytracing as rt
 12import raytracing.eo as eo
 13import raytracing.thorlabs as thorlabs
 14from raytracing.figure import GraphicOf
 15
 16from mytk import *
 17
 18
 19class OpticalComponentViewer(App):
 20    def __init__(self):
 21        App.__init__(self, geometry="1450x750")
 22
 23        self.window.widget.title("Lens viewer")
 24        self.window.is_resizable = False
 25        self.label = None
 26        self.menu = None
 27        self.default_figsize = (7, 5)
 28        self.header = View(width=1450, height=200)
 29        self.header.grid_into(
 30            self.window, column=0, row=0, pady=5, padx=5, sticky="nsew"
 31        )
 32        self.graphs = View(width=1450, height=700)
 33        self.graphs.grid_into(
 34            self.window, column=0, row=1, pady=5, padx=5, sticky="nsew"
 35        )
 36        self.window.row_resize_weight(0, 0)
 37        self.window.row_resize_weight(1, 0)
 38        self.component = None
 39        self.dispersion = None
 40
 41        self.lenses = {}
 42        self.build_lens_dict()
 43        self.build_table()
 44
 45        self.update_figure()
 46
 47    def build_table(self):
 48        self.columns = {
 49            "label": "Part number",
 50            "backFocalLength": "Front focal length [mm]",
 51            "frontFocalLength": "Back focal length [mm]",
 52            "effectiveFocalLengths": "Effective focal length [mm]",
 53            "apertureDiameter": "Diameter [mm]",
 54            "wavelengthRef": "Design wavelength [nm]",
 55            "materials": "Material(s)",
 56            "url": "URL",
 57        }
 58        self.table = TableView(columns_labels=self.columns)
 59        self.table.delegate = self
 60        self.table.grid_into(self.header, sticky="nsew", padx=5)
 61        self.table.all_elements_are_editable = False
 62
 63        self.table.column_formats['backFocalLength'] = {'format_string':"{0:.2f}", 'multiplier':1, 'anchor':''}
 64        self.table.column_formats['frontFocalLength'] = {'format_string':"{0:.2f}", 'multiplier':1, 'anchor':''}
 65        self.table.column_formats['effectiveFocalLengths'] = {'format_string':"{0:.2f}", 'multiplier':1, 'anchor':''}
 66        self.table.column_formats['apertureDiameter'] = {'format_string':"{0:.2f}", 'multiplier':1, 'anchor':''}
 67        self.table.column_formats['wavelengthRef'] = {'format_string':"{0:g}", 'multiplier':1, 'anchor':''}
 68        self.table.data_source.update_field_properties('backFocalLength', {'type':float})
 69        self.table.data_source.update_field_properties('frontFocalLength', {'type':float})
 70        self.table.data_source.update_field_properties('effectiveFocalLengths', {'type':float})
 71        self.table.data_source.update_field_properties('apertureDiameter', {'type':float})
 72        self.table.data_source.update_field_properties('wavelengthRef', {'type':float})
 73
 74        for column in self.columns:
 75            self.table.widget.column(column, width=150, anchor=CENTER)
 76        self.table.widget.column("url", width=350, anchor=W)
 77
 78        iids = []
 79        for label, lens in self.lenses.items():
 80            if lens.wavelengthRef is not None:
 81                wavelengthRef = "{0:.1f}".format(lens.wavelengthRef * 1000)
 82            else:
 83                wavelengthRef = "N/A"
 84
 85            materials = ""
 86            if isinstance(lens, rt.AchromatDoubletLens):
 87                if lens.mat1 is not None and lens.mat2 is not None:
 88                    materials = "{0}/{1}".format(str(lens.mat1()), str(lens.mat2()))
 89            elif isinstance(lens, rt.SingletLens):
 90                if lens.mat is not None:
 91                    materials = "{0}".format(str(lens.mat()))
 92
 93
 94
 95            record = {
 96                "label": lens.label,
 97                "backFocalLength": lens.backFocalLength(),
 98                "frontFocalLength": lens.frontFocalLength(),
 99                "effectiveFocalLengths": lens.effectiveFocalLengths()[0],
100                "apertureDiameter": lens.apertureDiameter,
101                "wavelengthRef": wavelengthRef,
102                "materials": materials,
103                "url": lens.url,
104            }
105
106            # record = dict(zip(self.columns.keys(), values))
107            iid = self.table.data_source.append_record(record)
108            iids.append(iid)
109
110        self.table.widget.selection_set(iids[0]['__uuid'])
111
112        scrollbar = ttk.Scrollbar(
113            self.header.widget, orient=VERTICAL, command=self.table.widget.yview
114        )
115        self.table.widget.configure(yscroll=scrollbar.set)
116        scrollbar.grid(row=0, column=1, sticky="ns")
117
118    def update_figure(self, figure=None):
119        if figure is not None:
120            figure.set_size_inches(self.default_figsize)
121        self.component = Figure(figure, figsize=self.default_figsize)
122        self.component.grid_into(self.graphs, column=0, row=0, padx=5)
123        self.dispersion = Figure(figsize=self.default_figsize)
124        self.dispersion.grid_into(self.graphs, column=1, row=0, padx=5)
125
126    @property
127    def figure(self):
128        return self.component.figure
129
130    @figure.setter
131    def figure(self, value):
132        self.update_figure(value)
133
134    def build_lens_dict(self):
135        modules = [thorlabs, eo]
136
137        for i, lens in enumerate(rt.CompoundLens.all()):
138            for module in modules:
139                with suppress(Exception):
140                    class_ = getattr(module, lens)
141                    lens = class_()
142                    f1, f2 = lens.effectiveFocalLengths()
143                    self.lenses[lens.label] = lens
144
145    def selection_changed(self, event, table):
146        for selected_item in table.widget.selection():
147            record = table.data_source.record(selected_item)
148            lens = self.lenses[record['label']]  # label
149            self.update_figures(lens)
150
151    def source_data_changed(self, table):
152        pass
153
154    def update_figures(self, lens):
155        graphic = GraphicOf(lens)
156        self.figure = graphic.drawFigure().figure
157        self.figure.set_size_inches((5, 5), forward=True)
158
159        with suppress(Exception):
160            wavelengths, focalShifts = lens.focalShifts()
161
162            axis = self.dispersion.figure.add_subplot()
163            axis.plot(wavelengths, focalShifts, "k-")
164            axis.set_xlabel(r"Wavelength [nm]")
165            axis.set_ylabel(r"Focal shift [mm]")
166
167    def about(self):
168        showinfo(
169            title="About Lens Viewer",
170            message="A lens viewer for the Raytracing package by the DCC/M Lab.",
171        )
172
173    def help(self):
174        webbrowser.open("https://raytracing.readthedocs.io/")
175
176    def doubleclick_cell(self, item_id, column_name, table):
177        record = table.record(item_id)
178        value = record[column_name]
179        pyperclip.copy(value)
180        return True
181
182
183if __name__ == "__main__":
184    rt.silentMode()
185    app = OpticalComponentViewer()
186
187    from packaging.version import Version
188
189    if Version(rt.__version__) <= Version("1.3.10"):
190        showerror(
191            title="Minimum Raytracing version",
192            message="You need at least Raytracing 1.3.11 to run the lens viewer",
193            icon=ERROR,
194        )
195    else:
196        app.mainloop()

microscope_app.py

Live webcam microscope viewer.

Wraps a VideoView with camera controls — zoom, exposure and gain — using two-way property binding (Sliders and an IntEntry to the VideoView) and a live histogram, plus VideoView’s built-in start/save/stream buttons.

Needs a connected camera (device 0) and a VideoView backend (e.g. OpenCV).

Run it with python -m mytk.example_apps.microscope_app.

Note

Screenshot not yet available — regenerate with make example-shots.

  1"""microscope_app.py — Live webcam microscope viewer.
  2
  3Wraps a VideoView with camera controls — zoom, exposure and gain — using two-way
  4property binding (Sliders and an IntEntry to the VideoView) and a live histogram,
  5plus VideoView's built-in start/save/stream buttons.
  6
  7Needs a connected camera (device 0) and a VideoView backend (e.g. OpenCV).
  8"""
  9from mytk import *
 10
 11
 12class MicroscopeApp(App):
 13    def __init__(self):
 14        App.__init__(self)
 15
 16        self.window.widget.title("Microscope")
 17
 18        self.camera = VideoView(device=0, zoom_level=3)
 19        self.camera.grid_into(
 20            self.window, row=0, column=0, pady=10, padx=10, sticky="nw"
 21        )
 22
 23        self.controls = Box(label="Controls", width=500, height=700)
 24        self.window.widget.grid_columnconfigure(1, weight=0)
 25
 26        self.controls.grid_into(
 27            self.window, column=1, row=0, pady=10, padx=10, sticky="nsew"
 28        )
 29        self.controls.widget.grid_rowconfigure(0, weight=0)
 30        self.controls.widget.grid_rowconfigure(1, weight=0)
 31
 32        self.start_button, self.save_button, self.stream_button = (
 33            self.camera.create_behaviour_buttons()
 34        )
 35
 36        self.start_button.grid_into(
 37            self.controls, column=0, row=0, pady=5, padx=10, sticky="w"
 38        )
 39        self.save_button.grid_into(
 40            self.controls, column=1, row=0, pady=5, padx=10, sticky="w"
 41        )
 42        self.stream_button.grid_into(
 43            self.controls, column=1, row=1, pady=5, padx=10, sticky="w"
 44        )
 45
 46        self.exposure_time_label = Label("Exposure:")
 47        self.exposure_time_label.grid_into(
 48            self.controls, column=0, row=3, pady=5, padx=5, sticky="e"
 49        )
 50        self.exposure_time_slider = Slider()
 51        self.exposure_time_slider.grid_into(
 52            self.controls, column=1, row=3, pady=5, padx=10, sticky="nw"
 53        )
 54
 55        self.gain_label = Label("Gain:")
 56        self.gain_label.grid_into(
 57            self.controls, column=0, row=4, pady=5, padx=5, sticky="e"
 58        )
 59        self.gain_slider = Slider()
 60        self.gain_slider.grid_into(
 61            self.controls, column=1, row=4, pady=5, padx=10, sticky="nw"
 62        )
 63
 64        self.popup_label = Label("Camera:")
 65        self.popup_label.grid_into(
 66            self.controls, column=0, row=2, pady=5, padx=10, sticky="se"
 67        )
 68
 69        self.zoomlevel_label = Label("Zoom level:")
 70        self.zoomlevel_label.grid_into(
 71            self.controls, column=0, row=6, pady=5, padx=10, sticky="se"
 72        )
 73        self.zoom_level_control = IntEntry(value=3, width=5, minimum=1)
 74        self.zoom_level_control.grid_into(
 75            self.controls, column=1, row=6, pady=5, padx=10, sticky="w"
 76        )
 77        self.camera.bind_properties(
 78            "zoom_level", self.zoom_level_control, "value_variable"
 79        )
 80
 81        self.camera.histogram_xyplot = Histogram(figsize=(3.5, 1))
 82        self.camera.histogram_xyplot.grid_into(
 83            self.controls, column=0, columnspan=2, row=7, pady=5, padx=10, sticky="w"
 84        )
 85
 86        self.popup_camera = self.camera.create_behaviour_popups()
 87        self.popup_camera.grid_into(
 88            self.controls, column=1, row=2, pady=5, padx=10, sticky="w"
 89        )
 90
 91    def about(self):
 92        showinfo(
 93            title="About Microscope",
 94            message="An application created with myTk",
 95        )
 96
 97    def help(self):
 98        webbrowser.open("https://www.dccmlab.ca/")
 99
100
101if __name__ == "__main__":
102    app = MicroscopeApp()
103    app.mainloop()

powermeter_app.py

Power-meter readout and logging panel.

Streams readings into an XYPlot (live append), shows status via BooleanIndicator, and binds device state to Slider/LabelledEntry controls with an event-driven update loop. Runs in a debug/simulation mode when no hardware is present.

Run it with python -m mytk.example_apps.powermeter_app.

powermeter_app.py screenshot
  1"""powermeter_app.py — Power-meter readout and logging panel.
  2
  3Streams readings into an XYPlot (live append), shows status via BooleanIndicator,
  4and binds device state to Slider/LabelledEntry controls with an event-driven update
  5loop. Runs in a debug/simulation mode when no hardware is present.
  6"""
  7import random
  8from tkinter import filedialog
  9
 10from mytk import *
 11from mytk.indicators import *
 12
 13
 14class PowerMeterApp(App):
 15    def __init__(self):
 16        App.__init__(self)
 17
 18        self.window.widget.title("Powermeter")
 19
 20        self.window.row_resize_weight(0,0)
 21        self.window.row_resize_weight(1,0)
 22        self.window.row_resize_weight(2,1)
 23        self.window.column_resize_weight(0,1)
 24        self.window.column_resize_weight(1,1)
 25
 26        size = 60
 27        self.bigb_font = tkFont.Font(family='Helvetica', size=size, weight='bold')
 28        self.big_font = tkFont.Font(family='Helvetica', size=size)
 29
 30        self.measurement_label = Label("--- mW", font=self.big_font)
 31        self.measurement_label.grid_into(self.window, row=1, column=1, padx=25, pady=25,sticky="nsew")
 32
 33
 34        self.plot = XYPlot(figsize=(6,4))
 35        self.plot.grid_into(self.window, row=2, column=0, columnspan=3, padx=25, pady=5, sticky="nsew")
 36
 37        self.box = Box("Actions")
 38        self.box.grid_into(self.window, row=0, column=0, columnspan=2, padx=25, pady=10, sticky="nsew")
 39
 40        self.running_indicator = BooleanIndicator(diameter=25)
 41        self.running_indicator.grid_into(self.box, row=0, column=0, padx=35, pady=25, sticky="w")
 42        self.start_button = Button("Start", user_event_callback=self.click_start)
 43        self.start_button.grid_into(self.box, row=0, column=1, padx=10, pady=10, sticky="ns")
 44        self.save_button = Button("Save data…", user_event_callback=self.click_save)
 45        self.save_button.grid_into(self.box, row=0, column=2, padx=10, pady=10)
 46        self.clear_button = Button("Clear graph", user_event_callback=self.click_clear)
 47        self.clear_button.grid_into(self.box, row=0, column=3, padx=10, pady=10)
 48        self.wavelength_entry = LabelledEntry(label="Wavelength:", character_width=6)
 49        self.wavelength_entry.grid_into(self.box, row=0, column=4, sticky="e")
 50        self.firmware_label = Label("")
 51        self.firmware_label.grid_into(self.window, row=3, column=0, columnspan=3, padx=25, pady=10, sticky="w")
 52
 53        self.device = PowerMeterDevice()
 54        self.is_refreshing = False
 55
 56
 57        self.device.bind_properties("wavelength", self.wavelength_entry.entry, "value_variable")
 58        self.device.bind_properties("firmware", self.firmware_label, "value_variable")
 59        self.bind_properties("is_refreshing", self.running_indicator, "value_variable")
 60        self.bind_properties("is_refreshing", self.save_button, "is_disabled")
 61        self.bind_properties("is_refreshing", self.wavelength_entry.entry, "is_disabled")
 62
 63        self.update_loop() # We update once at least
 64
 65    def click_start(self, event, button):
 66        if not self.is_refreshing:
 67            self.is_refreshing = True
 68            self.update_loop()
 69            button.label = "Stop"
 70        else:
 71            self.is_refreshing = False
 72            button.label = "Start"
 73
 74    def update_loop(self):
 75        self.device.update_from_device()
 76
 77        power = self.device.power
 78        self.measurement_label.value_variable.set(f"{power:.2f} mW")
 79
 80        last = len(self.plot.x)
 81        self.plot.append(last, power)
 82        self.plot.update_plot()
 83
 84        if self.is_refreshing:
 85            self.after(300, self.update_loop)
 86
 87
 88    def click_save(self, event, button):
 89        filepath = filedialog.asksaveasfilename(
 90            parent=self.window.widget,
 91            title="Choisissez un nom de fichier:",
 92            filetypes=[('Data file','.dat'),('CSV file','.csv')],
 93        )
 94        if filepath != "":
 95            x,y = self.plot.x, self.plot.y
 96            pass # Do something with x,y data
 97
 98    def click_clear(self, event, button):
 99        self.plot.clear_plot()
100
101
102class PowerMeterDevice(Bindable):
103    debug = True
104
105    def __init__(self):
106        super().__init__()
107
108        """
109        The variables are refreshed by get_xxx commands, which 
110        fetch the actual values from the device.
111        The variables represent the latest values at all times
112        and can be used direectly by the app.
113        """
114        self.power = 0
115        self.wavelength = 1064
116        self.firmware = None
117        self.temperature = None
118
119    def get_power_from_device(self):
120        if self.debug:
121            self.power = random.randrange(900,1000,1)/100
122        else:
123            pass # Update via USB
124
125        return self.power
126
127    def get_firmware_from_device(self):
128        if self.debug:
129            self.firmware = "1.0.0alpha1"
130        else:
131            pass # Update via USB
132
133        return self.power
134
135    def get_temperature_from_device(self):
136        if self.debug:
137            self.temperature = random.randrange(70,73,1)
138        else:
139            pass # Update via USB
140
141        return self.power
142
143    def get_wavelength_from_device(self):
144        if self.debug:
145            pass
146        else:
147            pass # Update via USB
148
149        return self.wavelength
150
151    def update_from_device(self):
152        self.get_power_from_device()
153        self.get_firmware_from_device()
154        self.get_temperature_from_device()
155        self.get_wavelength_from_device()
156
157if __name__ == "__main__":
158    app = PowerMeterApp()
159    app.mainloop()

progressbar_app.py

Demonstrates three ways to update a ProgressBar in myTk.

  1. Direct value assignment: bar.value = 50

  2. Model binding: model.bind_property_to_widget_value(“progress”, bar)

  3. NotificationCenter notification: NotificationCenter().post_notification(…)

Run it with python -m mytk.example_apps.progressbar_app.

progressbar_app.py screenshot
 1"""
 2progressbar_app.py — Demonstrates three ways to update a ProgressBar in myTk.
 3
 41. Direct value assignment:         bar.value = 50
 52. Model binding:                   model.bind_property_to_widget_value("progress", bar)
 63. NotificationCenter notification: NotificationCenter().post_notification(...)
 7"""
 8
 9import threading
10import time
11
12if __name__ == "__main__":
13    from mytk import *
14    from mytk.notificationcenter import NotificationCenter
15
16    app = App(bring_to_front=True)
17    app.window.widget.title("ProgressBar demo")
18
19    # ── Method 1: direct value assignment ────────────────────────────────────
20    Label("Method 1 — direct: bar.value = i").grid_into(
21        app.window, row=0, column=0, pady=(20, 2), padx=20, sticky="w"
22    )
23    bar1 = ProgressBar(maximum=100)
24    bar1.grid_into(app.window, row=1, column=0, pady=(0, 10), padx=20, sticky="ew")
25
26    # ── Method 2: model binding ───────────────────────────────────────────────
27    Label("Method 2 — binding: model.progress.set(i)").grid_into(
28        app.window, row=2, column=0, pady=(10, 2), padx=20, sticky="w"
29    )
30    bar2 = ProgressBar(maximum=100)
31    bar2.grid_into(app.window, row=3, column=0, pady=(0, 10), padx=20, sticky="ew")
32
33    class Model(Bindable):
34        def __init__(self):
35            super().__init__()
36            self.progress = DoubleVar(value=0)
37
38    model = Model()
39    model.bind_property_to_widget_value("progress", bar2)
40
41    # ── Method 3: NotificationCenter ─────────────────────────────────────────
42    Label("Method 3 — notification: post step=1").grid_into(
43        app.window, row=4, column=0, pady=(10, 2), padx=20, sticky="w"
44    )
45    bar3 = ProgressBar(maximum=100)
46    bar3.grid_into(app.window, row=5, column=0, pady=(0, 20), padx=20, sticky="ew")
47
48    # ── Background task drives all three bars, then quits ────────────────────
49    def background_task():
50        for i in range(101):
51            bar1.value = i
52            model.progress.set(i)
53            NotificationCenter().post_notification(
54                ProgressBarNotification.step, None, user_info={"step": 1}
55            )
56            time.sleep(0.05)
57        app.schedule_on_main_thread(app.quit)
58
59    app.window.column_resize_weight(0, 1)
60    thread = threading.Thread(target=background_task, daemon=True)
61    thread.start()
62
63    app.mainloop()

pydatagraph_app.py

Plot tabular data with a per-column style inspector.

Loads CSV/Excel into a TableView (with drag-and-drop) and plots columns on an XYPlot, with an inspector to set each column’s marker, colour and line style live. Built on TabularData with pandas-backed import.

Needs pandas to load files; a CSV/Excel file (e.g. test-excel.xlsx) to plot.

Run it with python -m mytk.example_apps.pydatagraph_app.

pydatagraph_app.py screenshot
  1"""pydatagraph_app.py — Plot tabular data with a per-column style inspector.
  2
  3Loads CSV/Excel into a TableView (with drag-and-drop) and plots columns on an XYPlot,
  4with an inspector to set each column's marker, colour and line style live. Built on
  5TabularData with pandas-backed import.
  6
  7Needs ``pandas`` to load files; a CSV/Excel file (e.g. ``test-excel.xlsx``) to plot.
  8"""
  9from pathlib import Path
 10from tkinter import filedialog
 11
 12from mytk import *
 13
 14
 15class PyDatagraphApp(App):
 16    def __init__(self):
 17        App.__init__(self, geometry="800x800", name="PyDatagraph")
 18
 19        self.window.widget.title("Data")
 20        self.window.column_resize_weight(0, 1)
 21
 22        self.window.row_resize_weight(0,1) # Tables
 23        self.window.row_resize_weight(1,0) # Buttons
 24        self.window.row_resize_weight(2,1) # Graph
 25        self.tableview = TableView(columns_labels={})
 26        self.tableview.grid_into(self.window, row=0, column=0, padx=10, pady=10, sticky='nsew')
 27        self.tableview.delegate = self
 28        self.data_source = self.tableview.data_source
 29        self.data_source.default_field_properties = self.default_field_properties()
 30
 31
 32        self.inspector = Box(label='Inspector', width=300, height=50)
 33        self.inspector.grid_into(self.window, row=0, column=1, padx=10, pady=5, sticky='nsew')
 34        for i in range(6):
 35            self.inspector.row_resize_weight(i,1)
 36
 37        self.column_label = Label("Column: ")
 38        self.column_label.grid_into(self.inspector, row=0, column=0, padx=10, pady=5, sticky='ne')
 39        self.name_menu = PopupMenu([])
 40        self.name_menu.grid_into(self.inspector, row=0, column=1, padx=10, pady=2, sticky='nw')
 41        self.name_menu.widget['width'] = 5
 42
 43        self.marker_label = Label("Marker: ")
 44        self.marker_label.grid_into(self.inspector, row=1, column=0, padx=10, pady=2, sticky='ne')
 45        self.marker = PopupMenu(['o','.','s','x','+'],user_callback=self.column_inspector_data_changed)
 46        self.marker.grid_into(self.inspector, row=1, column=1, padx=10, pady=2, sticky='nw')
 47        self.marker.widget['width'] = 5
 48
 49        colors = ['black','red','blue','green','white']
 50        self.facecolor_label = Label("Face color: ")
 51        self.facecolor_label.grid_into(self.inspector, row=2, column=0, padx=10, pady=2, sticky='ne')
 52        self.marker_facecolor = PopupMenu(colors,user_callback=self.column_inspector_data_changed)
 53        self.marker_facecolor.grid_into(self.inspector, row=2, column=1, padx=10, pady=2, sticky='nw')
 54        self.marker_facecolor.widget['width'] = 5
 55
 56        self.edgecolor_label = Label("Edge color: ")
 57        self.edgecolor_label.grid_into(self.inspector, row=3, column=0, padx=10, pady=2, sticky='ne')
 58        self.marker_edgecolor = PopupMenu(colors,user_callback=self.column_inspector_data_changed)
 59        self.marker_edgecolor.grid_into(self.inspector, row=3, column=1, padx=10, pady=2, sticky='nw')
 60        self.marker_edgecolor.widget['width'] = 5
 61
 62        self.linestyle_label = Label("Line Style: ")
 63        self.linestyle_label.grid_into(self.inspector, row=4, column=0, padx=10, pady=2, sticky='ne')
 64        self.linestyle = PopupMenu(['','-','--'],user_callback=self.column_inspector_data_changed)
 65        self.linestyle.grid_into(self.inspector, row=4, column=1, padx=10, pady=2, sticky='nw')
 66        self.linestyle.widget['width'] = 5
 67
 68        self.linecolor_label = Label("Line color: ")
 69        self.linecolor_label.grid_into(self.inspector, row=5, column=0, padx=10, pady=2, sticky='ne')
 70        self.linecolor = PopupMenu(colors, user_callback=self.column_inspector_data_changed)
 71        self.linecolor.grid_into(self.inspector, row=5, column=1, padx=10, pady=2, sticky='nw')
 72        self.linecolor.widget['width'] = 5
 73
 74        self.show = Checkbox(label="Visible", user_callback=self.column_inspector_checkbox_changed)
 75        self.show.grid_into(self.inspector, row=6, column=0, columnspan=2, padx=10, pady=2, sticky='w')
 76        self.is_independent = Checkbox(label="is X", user_callback=self.column_inspector_checkbox_changed)
 77        self.is_independent.grid_into(self.inspector, row=6, column=1, padx=10, pady=2, sticky='w')
 78
 79        self.marker.bind_properties('is_disabled', self.is_independent, 'value_variable')
 80        self.marker_facecolor.bind_properties('is_disabled', self.is_independent, 'value_variable')
 81        self.marker_edgecolor.bind_properties('is_disabled', self.is_independent, 'value_variable')
 82        self.linestyle.bind_properties('is_disabled', self.is_independent, 'value_variable')
 83        self.linecolor.bind_properties('is_disabled', self.is_independent, 'value_variable')
 84        self.show.bind_properties('is_disabled', self.is_independent, 'value_variable')
 85
 86        self.controls = View(width=400, height=50)
 87        self.controls.grid_into(self.window, row=1, column=0, columnspan=2, padx=10, pady=10, sticky='nsew')
 88        self.controls.widget.grid_columnconfigure(0, weight=1)
 89        self.controls.widget.grid_columnconfigure(1, weight=1)
 90        self.controls.widget.grid_columnconfigure(2, weight=1)
 91        self.load_data_button = Button("Load data… (or drop a file)", user_event_callback=self.user_click_load)
 92        self.load_data_button.grid_into(self.controls, row=0, column=0, padx=10, pady=10, sticky='nw')
 93        # self.copy_data_button = Button("Copy data to clipboard", user_event_callback=self.copy_data)
 94        # self.copy_data_button.grid_into(self.controls, row=0, column=1, padx=10, pady=10, sticky='nw')
 95
 96
 97        self.plot = XYPlot(figsize=(4,4))
 98        self.plot.grid_into(self.window, row=2, column=0, columnspan=2, padx=10, pady=10, sticky='nsew')
 99
100
101        self.name_menu.add_observer(self, "value_variable", 'inspected_variable_changed')
102
103        self.disable_inspector_values_changed = False
104
105        # Drop an Excel or CSV file onto the table or the graph to load it. (The
106        # root window itself can't be a drop target — tkdnd attaches to child
107        # widgets — so we register the two main areas.) Any file is accepted;
108        # load_data reports unreadable ones with its error dialog.
109        def load_dropped(paths):
110            self.load_data(Path(paths[0]))
111
112        self.tableview.accept_dropped_files(load_dropped)
113        self.plot.accept_dropped_files(load_dropped)
114
115        parent = Path(__file__).parent.resolve()
116        test_file = Path(parent, 'test-excel.xlsx')
117        if test_file.exists():
118            self.load_data(test_file)
119
120        self.after(delay=100, function=self.refresh_plot)
121
122    @property
123    def has_one_independent_variable(self):
124        return len(self.independent_variables()) == 1
125
126    def default_field_properties(self):
127        return {'marker':'o','markeredgecolor':'black','markerfacecolor':'black','linestyle':'','color':'black','visible':True, 'is_independent':False}.copy()
128
129    def click_cell(self, item_id, column_name, table):
130        self.name_menu.value = self.tableview.columns_labels[column_name]
131
132    def column_name_from_heading(self, heading_name):
133        column_idx = self.tableview.headings.index(heading_name)
134        return self.tableview.columns[column_idx]
135
136    def observed_property_changed(
137        self, observed_object, observed_property_name, new_value, context
138    ):
139        if context == "inspected_variable_changed":
140            column_name = self.column_name_from_heading(self.name_menu.value)
141
142            properties = self.data_source.get_field_properties(column_name)
143            self.properties_to_column_inspector(properties=properties)
144        else:
145            super().observed_property_changed(observed_object, observed_property_name, new_value, context)
146
147    def column_inspector_to_properties(self):
148        properties = {}
149        properties['marker'] = self.marker.value
150        properties['markerfacecolor'] = self.marker_facecolor.value
151        properties['markeredgecolor'] = self.marker_edgecolor.value
152        properties['linestyle'] = self.linestyle.value
153        properties['color'] = self.linecolor.value
154        properties['visible'] = self.show.value
155        properties['is_independent'] = self.is_independent.value
156
157        return properties
158
159    def properties_to_column_inspector(self, properties):
160        assert properties is not None
161
162        self.marker.value = properties['marker']
163        self.marker_facecolor.value = properties['markerfacecolor']
164        self.marker_edgecolor.value = properties['markeredgecolor']
165        self.linestyle.value = properties['linestyle']
166        self.linecolor.value = properties['color']
167        self.show.value = properties['visible']
168        self.is_independent.value = properties['is_independent']
169
170    def update_column_properties_from_inspector(self):
171        column_name = self.column_name_from_heading(self.name_menu.value)
172
173        properties = self.column_inspector_to_properties()
174        self.data_source.update_field_properties(column_name, properties)
175
176        self.after(delay=100, function=self.refresh_plot)
177
178    def update_inspector_from_column_properties(self):
179        column_name = self.column_name_from_heading(self.name_menu.value)
180
181        properties = self.data_source.get_field_properties(column_name)
182        self.update_inspector_from_column_properties(properties)
183
184    def column_inspector_checkbox_changed(self, selected_index):
185        self.update_column_properties_from_inspector()
186
187    def column_inspector_data_changed(self, menu, selected_index):
188        self.update_column_properties_from_inspector()
189
190    def column_headings_changed(self, new_names):
191        self.name_menu.clear_menu_items()
192        self.name_menu.add_menu_items(new_names)
193
194        if len(new_names) >= 2:
195            selected_name = new_names[1]
196        else:
197            selected_name = new_names[0]
198
199        self.name_menu.value = selected_name
200
201    def user_click_load(self, event, button):
202        filepath = filedialog.askopenfilename()
203        if filepath != '':
204            self.load_data(Path(filepath))
205
206    def load_data(self, filepath):
207        try:
208            df = self.data_source.load_tabular_data(filepath)
209        except TabularData.UnrecognizedFileFormatError:
210            diag=Dialog.showerror(
211                title="Unknown file format",
212                message=f"The file '{filepath}' does not have readable data organized in a table manner. The module pandas is used, check the supported file formats.",
213            )
214            return
215
216
217        # FIXME: this always overwrites columns
218        rows, cols = df.shape
219        if cols <= 3:
220            first_heading = ord('x')
221        else:
222            first_heading = ord('a')
223        df.columns = [ chr(first_heading + c) for c in range(cols)]
224
225        with PostponeChangeCalls(self.data_source):
226            # Reset data_source
227            self.data_source._field_properties = {}
228            self.data_source.records = []
229
230            # Reset tableview
231            self.tableview.clear_widget_content()
232            self.tableview.columns = list(df.columns).copy()
233            self.tableview.headings = [ name.upper() for name in self.tableview.columns]
234
235            for column_name in self.tableview.columns:
236                self.tableview.widget.column(column_name, width=30)
237
238            default_styles = self.plot.styles_pointmarker(linestyle='')
239            for i, column_name in enumerate(self.tableview.columns):
240                properties = self.data_source.get_field_properties(column_name)
241                properties['type'] = float
242
243                if i == 0:
244                    properties['is_independent'] =  True
245                else:
246                    properties['is_independent'] =  False
247                    style_properties = default_styles[(i-1)%len(default_styles)]
248                    properties.update(style_properties)
249
250                self.data_source.update_field_properties(field_name=column_name, new_properties=properties)
251                self.tableview.column_formats[column_name] = {'format_string':'{0:.3f}', 'multiplier':1, 'type':float,'anchor':"w" }
252
253            # Load with new data
254            self.data_source.set_records_from_dataframe(df)
255
256        self.column_headings_changed(self.tableview.headings)
257
258    def source_data_changed(self, table):
259        self.after(delay=100, function=self.refresh_plot)
260
261    def dependent_variable(self):
262        columns = self.data_source.record_fields()
263        dependent_variable = [ column_name for column_name in columns if self.data_source.get_field_property(field_name=column_name, property_name='is_independent')]
264
265        if len(dependent_variable) == 1:
266            dependent_variable = dependent_variable[0]
267        else:
268            dependent_variable = None
269        return dependent_variable
270
271    def independent_variables(self):
272        columns = self.data_source.record_fields()
273
274        return [ column_name for column_name in columns if not self.data_source.get_field_property(column_name, 'is_independent')]
275
276    def refresh_plot(self):
277        self.plot.clear_plot()
278        records = self.data_source.records
279        variable_names= self.data_source.record_fields()
280
281        dependent_variable = self.dependent_variable()
282        independent_variables = self.independent_variables()
283
284        style_keys = ['marker','markeredgecolor','markerfacecolor','linestyle','color']
285        for i, variable_name in enumerate(independent_variables):
286            column_properties = self.data_source.get_field_properties(variable_name)
287            style_properties = { key:column_properties[key] for key in column_properties.keys() if key in style_keys}
288
289            is_visible = column_properties['visible']
290            if is_visible:
291                y = [ float(record[variable_name]) for record in records]
292                if dependent_variable is not None:
293                    x = [ float(record[dependent_variable]) for record in records]
294                else:
295                    x = list(range(len(records)))
296
297                self.plot.first_axis.plot(x,y, label=f"Column {variable_name}", **(style_properties) )
298
299        self.plot.first_axis.set_ylabel('Y',fontsize=18)
300        if dependent_variable is not None:
301            self.plot.first_axis.set_xlabel(dependent_variable,fontsize=18)
302        else:
303            self.plot.first_axis.set_xlabel("index #",fontsize=18)
304
305        if len(independent_variables) > 1:
306            self.plot.first_axis.legend(loc='upper right', framealpha=1.0, edgecolor='black')
307
308        self.plot.first_axis.tick_params(axis='both', which='major', direction='in', labelsize=14)
309        self.plot.figure.subplots_adjust(bottom=0.2)
310        self.plot.figure.canvas.draw()
311        self.plot.figure.canvas.flush_events()
312
313if __name__ == "__main__":
314    ModulesManager.install_and_import_modules_if_absent(pip_modules={"requests":"requests","pyperclip":"pyperclip"}, ask_for_confirmation=False)
315    app = PyDatagraphApp()
316    app.mainloop()

svgviewer_app.py

Minimal SVG viewer.

Opens a window with an SVGImage. Pass an SVG file on the command line, drag an .svg file onto the viewer, or click “Load…” to open one. A built-in sample is shown when no file is given.

python -m mytk.example_apps.svgviewer_app [path/to/drawing.svg]

SVGImage rasterizes the document with the resvg engine (full SVG support, including text, gradients and clipping) and rescales to fill the window. See mytk.images.SVGImage.

Run it with python -m mytk.example_apps.svgviewer_app.

svgviewer_app.py screenshot
 1"""
 2svgviewer_app.py — Minimal SVG viewer.
 3
 4Opens a window with an `SVGImage`. Pass an SVG file on the command line, drag an
 5`.svg` file onto the viewer, or click "Load…" to open one. A built-in sample is
 6shown when no file is given.
 7
 8    python -m mytk.example_apps.svgviewer_app [path/to/drawing.svg]
 9
10`SVGImage` rasterizes the document with the `resvg` engine (full SVG support,
11including text, gradients and clipping) and rescales to fill the window. See
12`mytk.images.SVGImage`.
13"""
14
15import sys
16from tkinter import filedialog
17
18
19SAMPLE_SVG = """\
20<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200">
21  <rect x="10" y="10" width="180" height="180"
22        fill="#f5f5f5" stroke="#606060" stroke-width="2"/>
23  <circle cx="60" cy="60" r="35" fill="#3498db" stroke="#1f618d"
24          stroke-width="3"/>
25  <ellipse cx="140" cy="60" rx="45" ry="28" fill="#e74c3c" fill-opacity="0.7"/>
26  <line x1="20" y1="110" x2="180" y2="110" stroke="#27ae60"
27        stroke-width="3" stroke-dasharray="8 4"/>
28  <polygon points="40,180 70,130 100,180" fill="#f1c40f" stroke="#b7950b"/>
29  <path d="M110 170 q 20 -50 40 0 t 40 0" fill="none"
30        stroke="#9b59b6" stroke-width="3"/>
31  <g transform="translate(100 100) rotate(15)">
32    <text x="0" y="0" font-size="16" text-anchor="middle"
33          fill="#202020">SVG on Tk</text>
34  </g>
35</svg>
36"""
37
38
39if __name__ == "__main__":
40    from mytk import App, Button, SVGImage
41
42    app = App(bring_to_front=True)
43    app.window.widget.title("SVG viewer")
44
45    svg_view = SVGImage(data=SAMPLE_SVG)
46    svg_view.grid_into(
47        app.window, row=0, column=0,
48        padx=10, pady=10, sticky="nsew"
49    )
50
51    def load_file():
52        path = filedialog.askopenfilename(
53            title="Open an SVG file",
54            filetypes=[("SVG", "*.svg"), ("All files", "*.*")],
55        )
56        if path and svg_view.load_file_or_warn(path):
57            app.window.widget.title(f"SVG viewer — {path}")
58
59    Button("Load…", user_event_callback=lambda e, b: load_file()).grid_into(
60        app.window, row=1, column=0, padx=10, pady=(0, 10), sticky="w"
61    )
62
63    # Drop an .svg file onto the viewer; other files are ignored.
64    svg_view.accept_dropped_svg_files(
65        on_load=lambda path: app.window.widget.title(f"SVG viewer — {path}")
66    )
67
68    app.window.row_resize_weight(0, 1)
69    app.window.column_resize_weight(0, 1)
70    # Re-render to fill the window on resize. Throttle the re-render so the
71    # stream of <Configure> events while dragging doesn't re-rasterize on every
72    # pixel (and to damp the resize/redraw feedback loop). Enable this only
73    # after the cell has a resize weight, since a rescalable image measures its
74    # cell immediately.
75    svg_view.resize_update_delay = 50  # milliseconds
76    svg_view.is_rescalable = True
77
78    if len(sys.argv) > 1:
79        svg_view.load_file_or_warn(sys.argv[1])
80        app.window.widget.title(f"SVG viewer — {sys.argv[1]}")
81
82    app.mainloop()

view3d_app.py

Minimal 3D mesh viewer.

Opens a window with a View3D. Pass a mesh file on the command line, drag a mesh file onto the viewer, or click “Load…” to open a GLB/GLTF/OBJ/PLY file. Drag to orbit, scroll to zoom.

python -m mytk.example_apps.view3d_app [path/to/scene.glb]

Run it with python -m mytk.example_apps.view3d_app.

view3d_app.py screenshot
 1"""
 2view3d_app.py — Minimal 3D mesh viewer.
 3
 4Opens a window with a `View3D`. Pass a mesh file on the command line, drag a
 5mesh file onto the viewer, or click "Load…" to open a GLB/GLTF/OBJ/PLY file.
 6Drag to orbit, scroll to zoom.
 7
 8    python -m mytk.example_apps.view3d_app [path/to/scene.glb]
 9"""
10
11import sys
12from tkinter import filedialog
13
14
15if __name__ == "__main__":
16    from mytk import App, Button, View3D
17
18    app = App(bring_to_front=True)
19    app.window.widget.title("3D mesh viewer")
20
21    # The View3D factory picks a backend (pyrender if available, else moderngl).
22    mesh_view = View3D(width=820, height=620)
23    mesh_view.grid_into(
24        app.window, row=0, column=0, padx=10, pady=10, sticky="nsew"
25    )
26
27    def load_file():
28        path = filedialog.askopenfilename(
29            title="Open a 3D mesh",
30            filetypes=[
31                ("3D meshes", "*.glb *.gltf *.obj *.ply *.stl"),
32                ("All files", "*.*"),
33            ],
34        )
35        if path:
36            mesh_view.load_file_or_warn(path)
37
38    Button("Load…", user_event_callback=lambda e, b: load_file()).grid_into(
39        app.window, row=1, column=0, padx=10, pady=(0, 10), sticky="w"
40    )
41
42    # Drop any file onto the viewer; unrecognised ones raise a warning dialog.
43    mesh_view.accept_dropped_files(
44        lambda paths: mesh_view.load_file_or_warn(paths[0])
45    )
46
47    app.window.row_resize_weight(0, 1)
48    app.window.column_resize_weight(0, 1)
49
50    if len(sys.argv) > 1:
51        mesh_view.load_file(sys.argv[1])
52
53    app.mainloop()