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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
Direct value assignment: bar.value = 50
Model binding: model.bind_property_to_widget_value(“progress”, bar)
NotificationCenter notification: NotificationCenter().post_notification(…)
Run it with python -m mytk.example_apps.progressbar_app.
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.
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.
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.
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()