mytk 0.9.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mytk-0.9.3/LICENSE +21 -0
- mytk-0.9.3/PKG-INFO +555 -0
- mytk-0.9.3/README.md +514 -0
- mytk-0.9.3/mytk/__init__.py +47 -0
- mytk-0.9.3/mytk/app.py +150 -0
- mytk-0.9.3/mytk/base.py +318 -0
- mytk-0.9.3/mytk/bindable.py +211 -0
- mytk-0.9.3/mytk/button.py +59 -0
- mytk-0.9.3/mytk/canvasview.py +216 -0
- mytk-0.9.3/mytk/checkbox.py +40 -0
- mytk-0.9.3/mytk/controls.py +88 -0
- mytk-0.9.3/mytk/dataviews.py +336 -0
- mytk-0.9.3/mytk/dialog.py +169 -0
- mytk-0.9.3/mytk/entries.py +285 -0
- mytk-0.9.3/mytk/example_apps/__init__.py +0 -0
- mytk-0.9.3/mytk/example_apps/canvas_app.py +962 -0
- mytk-0.9.3/mytk/example_apps/controlpanel_app.py +84 -0
- mytk-0.9.3/mytk/example_apps/example.py +149 -0
- mytk-0.9.3/mytk/example_apps/file_calculator_app.py +67 -0
- mytk-0.9.3/mytk/example_apps/fileviewer_app.py +46 -0
- mytk-0.9.3/mytk/example_apps/filters_app.py +281 -0
- mytk-0.9.3/mytk/example_apps/lensviewer_app.py +185 -0
- mytk-0.9.3/mytk/example_apps/microscope_app.py +107 -0
- mytk-0.9.3/mytk/example_apps/powermeter_app.py +95 -0
- mytk-0.9.3/mytk/example_apps/pydatagraph_app.py +307 -0
- mytk-0.9.3/mytk/figures.py +315 -0
- mytk-0.9.3/mytk/fileviewer.py +216 -0
- mytk-0.9.3/mytk/images.py +250 -0
- mytk-0.9.3/mytk/indicators.py +107 -0
- mytk-0.9.3/mytk/lab/tklab.py +170 -0
- mytk-0.9.3/mytk/labels.py +64 -0
- mytk-0.9.3/mytk/modulesmanager.py +63 -0
- mytk-0.9.3/mytk/notificationcenter.py +162 -0
- mytk-0.9.3/mytk/popupmenu.py +61 -0
- mytk-0.9.3/mytk/radiobutton.py +55 -0
- mytk-0.9.3/mytk/resources/error.png +0 -0
- mytk-0.9.3/mytk/resources/info.png +0 -0
- mytk-0.9.3/mytk/resources/warning.png +0 -0
- mytk-0.9.3/mytk/tableview.py +481 -0
- mytk-0.9.3/mytk/tabulardata.py +361 -0
- mytk-0.9.3/mytk/tests/envtest.py +38 -0
- mytk-0.9.3/mytk/tests/testBaseWidgets.py +182 -0
- mytk-0.9.3/mytk/tests/testBindings.py +247 -0
- mytk-0.9.3/mytk/tests/testBox.py +71 -0
- mytk-0.9.3/mytk/tests/testButton.py +94 -0
- mytk-0.9.3/mytk/tests/testCanvasView.py +19 -0
- mytk-0.9.3/mytk/tests/testCheckbox.py +98 -0
- mytk-0.9.3/mytk/tests/testDialogs.py +81 -0
- mytk-0.9.3/mytk/tests/testEntries.py +217 -0
- mytk-0.9.3/mytk/tests/testImages.py +160 -0
- mytk-0.9.3/mytk/tests/testLabel.py +81 -0
- mytk-0.9.3/mytk/tests/testModulesManager.py +53 -0
- mytk-0.9.3/mytk/tests/testMyApp.py +90 -0
- mytk-0.9.3/mytk/tests/testNotificationCenter.py +154 -0
- mytk-0.9.3/mytk/tests/testPopupMenu.py +72 -0
- mytk-0.9.3/mytk/tests/testRadioButtons.py +66 -0
- mytk-0.9.3/mytk/tests/testSlider.py +88 -0
- mytk-0.9.3/mytk/tests/testStyles.py +43 -0
- mytk-0.9.3/mytk/tests/testTableView.py +403 -0
- mytk-0.9.3/mytk/tests/testTabularData.py +323 -0
- mytk-0.9.3/mytk/tests/testTreeData.py +289 -0
- mytk-0.9.3/mytk/tests/testTreeTableView.py +218 -0
- mytk-0.9.3/mytk/tests/testURLLabel.py +82 -0
- mytk-0.9.3/mytk/tests/testView.py +48 -0
- mytk-0.9.3/mytk/utils.py +26 -0
- mytk-0.9.3/mytk/vectors.py +658 -0
- mytk-0.9.3/mytk/videoview.py +311 -0
- mytk-0.9.3/mytk/views.py +40 -0
- mytk-0.9.3/mytk/window.py +32 -0
- mytk-0.9.3/mytk.egg-info/PKG-INFO +555 -0
- mytk-0.9.3/mytk.egg-info/SOURCES.txt +75 -0
- mytk-0.9.3/mytk.egg-info/dependency_links.txt +1 -0
- mytk-0.9.3/mytk.egg-info/requires.txt +6 -0
- mytk-0.9.3/mytk.egg-info/top_level.txt +1 -0
- mytk-0.9.3/pyproject.toml +41 -0
- mytk-0.9.3/requirements.txt +6 -0
- mytk-0.9.3/setup.cfg +4 -0
mytk-0.9.3/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Daniel Côté
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
mytk-0.9.3/PKG-INFO
ADDED
|
@@ -0,0 +1,555 @@
|
|
|
1
|
+
Metadata-Version: 2.2
|
|
2
|
+
Name: mytk
|
|
3
|
+
Version: 0.9.3
|
|
4
|
+
Summary: A wrapper for Tkinter for busy scientists
|
|
5
|
+
Author-email: Daniel Côté <dccote@cervo.ulaval.ca>
|
|
6
|
+
Maintainer-email: Daniel Côté <dccote@cervo.ulaval.ca>
|
|
7
|
+
License: MIT License
|
|
8
|
+
|
|
9
|
+
Copyright (c) 2024 Daniel Côté
|
|
10
|
+
|
|
11
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
12
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
13
|
+
in the Software without restriction, including without limitation the rights
|
|
14
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
15
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
16
|
+
furnished to do so, subject to the following conditions:
|
|
17
|
+
|
|
18
|
+
The above copyright notice and this permission notice shall be included in all
|
|
19
|
+
copies or substantial portions of the Software.
|
|
20
|
+
|
|
21
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
22
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
23
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
24
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
25
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
26
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
27
|
+
SOFTWARE.
|
|
28
|
+
|
|
29
|
+
Classifier: Development Status :: 3 - Alpha
|
|
30
|
+
Classifier: Topic :: Software Development :: Build Tools
|
|
31
|
+
Classifier: Programming Language :: Python :: 3
|
|
32
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
33
|
+
Description-Content-Type: text/markdown
|
|
34
|
+
License-File: LICENSE
|
|
35
|
+
Requires-Dist: matplotlib==3.9.2
|
|
36
|
+
Requires-Dist: numpy
|
|
37
|
+
Requires-Dist: packaging
|
|
38
|
+
Requires-Dist: pyperclip
|
|
39
|
+
Requires-Dist: Requests
|
|
40
|
+
Requires-Dist: scipy
|
|
41
|
+
|
|
42
|
+
# myTk
|
|
43
|
+
by Daniel C. Côté
|
|
44
|
+
|
|
45
|
+
## What is it?
|
|
46
|
+
Making a UI interface should not be complicated. **myTk** is a set of UI classes that simplifies the use of Tkinter to make simple (and not so simple!) GUIs in Python.
|
|
47
|
+
|
|
48
|
+
## Why Tk?
|
|
49
|
+
Tk comes standard with Python. It is highly portable to all main platforms.
|
|
50
|
+
|
|
51
|
+
I know Qt, wxWidgets, and the many other ways to make an interface in Python, and I have programmed macOS since System 7 (Quickdraw, Powerplant) and Mac OS X (Carbon, Aqua, .nib and .xib files in Objective-C and in Swift). The issues I have found with UIs in Python is either the lack of portability or the complexity of the UI strategy:
|
|
52
|
+
**Qt** is very powerful but for most applications (and most scientific programmers) it is too complex, and my experience is that it is fairly fragile to transport to another device (same or different OS). On the other hand, `Tkinter` is standard on Python, but uses UI strategies that are showing their age (for example, raw function callbacks for events, inconsistent nomenclature, and some hoop jumping to get simple things done). It was easier to encapsulate `Tkinter` into something easy to use than to simplify Qt or other UI frameworks. This is therefore the objective of this micro-project: make `Tkinter` very simple to use for non-professional programmers. Many common tasks have classes ready to use.
|
|
53
|
+
|
|
54
|
+
## Design
|
|
55
|
+
Having been a macOS programmer for a long time, I have lived through the many incarnations of UI frameworks. Over the years, I have come to first understand, and second appreciate, good design patterns. If interested in Design Patterns, I would recommend reading [Design Patterns](https://refactoring.guru/design-patterns). I sought to make `Tkinter` a bit more mac-like because many design patterns in Apple's libraries are particularly mature and useful. For instance, Tkinter requires the parent of the UI-element to be specified at creation, even though there is no reason for it to be required. In addition, the many callbacks get complicated to organize when they are all over the place, therefore when appropriate I implemented a simple strategy to handle many cases by default for the Table, and offer the option to extend the behaviour with delegate-functions (which are a bit cleaner than raw callbacks).
|
|
56
|
+
|
|
57
|
+
* All `Tkinter` widgets are encapsulated into a `View` that provides easy access to many behaviours, but the `widget` remains accessible for you to call functions directly.
|
|
58
|
+
* You can `bind` the property of a GUI-object (`View`) to the value of a control (another `View`). They will always be synchronized, via the interface or even if you change them programmatically
|
|
59
|
+
* You can register for changes to Tkinter.Vars
|
|
60
|
+
* You can register a callback for an event
|
|
61
|
+
* You can set a delegate to manage the details
|
|
62
|
+
|
|
63
|
+
## Layout manager
|
|
64
|
+
The most important aspect to understand with Tk is how to position things on screen, and I have found it quite confusing. There are three "layout managers" in Tk: `grid`, `pack` and `place`. Grid allows you to conceptually separate a view (or widget in Tk) into a grid, and place objects on that grid. The grid may adjust its size to fit the objects (or not) depending on the options that are passed. If the window is resized, then some columns and rows may resize, depending on options (`column/row` `weight`) and the widget itself may also resize (depending on its `sticky` options ). When adding objects, they may adjust their size or force the size of the grid element (`grid_propagate`). Finally, you can place an element in a range of rows and columns by using the `rowspan` and `columnspan` keywords.
|
|
65
|
+
|
|
66
|
+
The tutorials that helped me the most are: [pythonguis.com](https://www.pythonguis.com/faq/pack-place-and-grid-in-tkinter/) and [TkDocs](https://tkdocs.com/tutorial/index.html).
|
|
67
|
+
|
|
68
|
+
The key elements to remember when using the grid layout manager:
|
|
69
|
+
|
|
70
|
+
1.
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
## Classes
|
|
74
|
+
|
|
75
|
+
Anything visible on screen is a referred to as a View, except the Window.
|
|
76
|
+
|
|
77
|
+
`App`: The main Application class, that holds the reference to the main window.
|
|
78
|
+
|
|
79
|
+
`Window`: A window that can hold other views
|
|
80
|
+
|
|
81
|
+
`Base`: A class grouping functions common to all View classes
|
|
82
|
+
|
|
83
|
+
`View`: A plain, empty view. It can be used as a container for other views in grid, so that the View is a single element of the grid even if it holds several elements itself also in a grid.
|
|
84
|
+
|
|
85
|
+
`PopupMenu`: A popup menu button to select an item in a list
|
|
86
|
+
|
|
87
|
+
`Label`: Static Onscreen text
|
|
88
|
+
|
|
89
|
+
`URLLabel`: Static URL that can be clicked and opened in your webbrowser.
|
|
90
|
+
|
|
91
|
+
`Box`: A box with an optional title at the top and possibly an outline
|
|
92
|
+
|
|
93
|
+
`Entry`: An entry box for single line text
|
|
94
|
+
|
|
95
|
+
`TableView`: A Table of items. You provide headers and items in list. You can sort columns by clicking on the header. Headers can also be used to resize the columns. If a cell is a URL, it is clickable. The table can be editable.
|
|
96
|
+
|
|
97
|
+
`Figure`: A matplotlib figure. You can let Figure create the actual matplotlib.figure or provide your own.
|
|
98
|
+
|
|
99
|
+
## Getting started
|
|
100
|
+
|
|
101
|
+
The best way to learn is to look at the examples applicateion (`mytk.py`, `lensviewer_app.py`, `filters_app.py`, `microscope_app.py`). But here it is:
|
|
102
|
+
|
|
103
|
+
1. Create a subclass of `App`.
|
|
104
|
+
2. In you `__init__`, first call `super().__init__`, then add you interface to the window at `self.window`. See below for examples.
|
|
105
|
+
1. If you add a `Tableview`, set the `delegate` to an object of your own so that the functions are called when appropriate. Everything is managed automatically. The delegate can implement any or all of the following methods.
|
|
106
|
+
* `selection_changed(event)`: there is no default behaviour
|
|
107
|
+
* `click_header(column)`: the default behaviour will sort the rows by this column
|
|
108
|
+
* `click_cell(item_id, column_id)`: if the text starts with `http`, will try to open the link in your default browser
|
|
109
|
+
* `doubleclick_header(column)`: there is no default behaviour`
|
|
110
|
+
* `doubleclick_cell(item_id, column_id)`: there is no default behaviour
|
|
111
|
+
2. If you add a `PopupMenu`, set the `user_callback` to act upon a change.
|
|
112
|
+
3. The matplotlib `Figure` can be used by providing your own plt.figure or using the one provided by the class. There is a `toolbar` that you can add to the interface.
|
|
113
|
+
3. You can override the method `about(self)` or `help(self)` to provide more than the default behaviour.
|
|
114
|
+
4. Instantiate your app (`app = MyApp()`), then call `app.mainloop()`
|
|
115
|
+
|
|
116
|
+
The real difficulty is to understand the Layout managers of Tkinter.
|
|
117
|
+
|
|
118
|
+
## Examples
|
|
119
|
+
|
|
120
|
+
### Example 1: Demo of capabilities
|
|
121
|
+
The myTk code includes an example:
|
|
122
|
+

|
|
123
|
+
|
|
124
|
+
### A filter database
|
|
125
|
+
|
|
126
|
+
The following filter database was created with **myTk**. As it is, it gets the data from our web server, but the code can be changed to use a local file. If you run it, it will work with our database.
|
|
127
|
+
|
|
128
|
+

|
|
129
|
+
|
|
130
|
+
```python
|
|
131
|
+
from mytk import *
|
|
132
|
+
|
|
133
|
+
import os
|
|
134
|
+
import re
|
|
135
|
+
import json
|
|
136
|
+
import tempfile
|
|
137
|
+
import shutil
|
|
138
|
+
import webbrowser
|
|
139
|
+
import urllib
|
|
140
|
+
import zipfile
|
|
141
|
+
import subprocess
|
|
142
|
+
from pathlib import Path
|
|
143
|
+
|
|
144
|
+
class FilterDBApp(App):
|
|
145
|
+
def __init__(self):
|
|
146
|
+
App.__init__(self, geometry="1100x650", name="Filter Database")
|
|
147
|
+
self.filepath_root = 'filters_data'
|
|
148
|
+
self.web_root = 'http://www.dccmlab.ca'
|
|
149
|
+
self.temp_root = os.path.join(tempfile.TemporaryDirectory().name)
|
|
150
|
+
self.download_files = True
|
|
151
|
+
self.webbrowser_download_path = None
|
|
152
|
+
|
|
153
|
+
self.window.widget.title("Filters")
|
|
154
|
+
self.window.row_resize_weight(0,1) # Tables
|
|
155
|
+
self.window.row_resize_weight(1,0) # Buttons
|
|
156
|
+
self.window.row_resize_weight(2,1) # Graph
|
|
157
|
+
self.filters = TableView(columns={"part_number":"Part number", "description":"Description","dimensions":"Dimensions","supplier":"Supplier","filename":"Filename","url":"URL", "spectral_x":"Wavelength", "spectral_y":"Transmission"})
|
|
158
|
+
self.filters.grid_into(self.window, row=0, column=0, padx=10, pady=10, sticky='nsew')
|
|
159
|
+
self.filters.widget['displaycolumn']=["part_number","description","dimensions", "supplier","filename","url"]
|
|
160
|
+
|
|
161
|
+
self.filters.widget.column(column=0, width=100)
|
|
162
|
+
self.filters.widget.column(column=1, width=200)
|
|
163
|
+
self.filters.widget.column(column=2, width=120)
|
|
164
|
+
self.filters.widget.column(column=3, width=70)
|
|
165
|
+
self.filters.delegate = self
|
|
166
|
+
|
|
167
|
+
self.filter_data = TableView(columns={"wavelength":"Wavelength", "transmission":"Transmission"})
|
|
168
|
+
self.filter_data.grid_into(self.window, row=0, column=1, padx=10, pady=10, sticky='nsew')
|
|
169
|
+
self.filter_data.widget.column(column=0, width=70)
|
|
170
|
+
|
|
171
|
+
self.controls = View(width=400, height=50)
|
|
172
|
+
self.controls.grid_into(self.window, row=1, column=0, columnspan=2, padx=10, pady=10, sticky='nsew')
|
|
173
|
+
self.controls.widget.grid_columnconfigure(0, weight=1)
|
|
174
|
+
self.controls.widget.grid_columnconfigure(1, weight=1)
|
|
175
|
+
self.controls.widget.grid_columnconfigure(2, weight=1)
|
|
176
|
+
self.associate_file_button = Button("Associate spectral file…", user_event_callback=self.associate_file)
|
|
177
|
+
self.associate_file_button.grid_into(self.controls, row=0, column=0, padx=10, pady=10, sticky='nw')
|
|
178
|
+
self.open_filter_data_button = Button("Show files", user_event_callback=self.show_files)
|
|
179
|
+
self.open_filter_data_button.grid_into(self.controls, row=0, column=1, padx=10, pady=10, sticky='nw')
|
|
180
|
+
self.export_filters_button = Button("Export data as Zip…", user_event_callback=self.export_filters)
|
|
181
|
+
self.export_filters_button.grid_into(self.controls, row=0, column=2, padx=10, pady=10, sticky='nw')
|
|
182
|
+
self.copy_data_button = Button("Copy data to clipboard", user_event_callback=self.copy_data)
|
|
183
|
+
self.copy_data_button.grid_into(self.controls, row=0, column=3, padx=10, pady=10, sticky='ne')
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
self.filter_plot = XYPlot(figsize=(4,4))
|
|
187
|
+
self.filter_plot.grid_into(self.window, row=2, column=0, columnspan=2, padx=10, pady=10, sticky='nsew')
|
|
188
|
+
|
|
189
|
+
self.filters_db = None
|
|
190
|
+
self.load()
|
|
191
|
+
|
|
192
|
+
def load(self):
|
|
193
|
+
if self.download_files:
|
|
194
|
+
self.filepath_root, filepath = self.get_files_from_web()
|
|
195
|
+
else:
|
|
196
|
+
filepath = os.path.join(self.filepath_root, "filters.json")
|
|
197
|
+
|
|
198
|
+
self.filters.load(filepath)
|
|
199
|
+
|
|
200
|
+
def get_files_from_web(self):
|
|
201
|
+
install_modules_if_absent(modules={"requests":"requests"})
|
|
202
|
+
|
|
203
|
+
import requests
|
|
204
|
+
|
|
205
|
+
url = "/".join([self.web_root, 'filters_data.zip'])
|
|
206
|
+
req = requests.get(url, allow_redirects=True)
|
|
207
|
+
open('filters_data.zip', 'wb').write(req.content)
|
|
208
|
+
|
|
209
|
+
with zipfile.ZipFile('filters_data.zip', 'r') as zip_ref:
|
|
210
|
+
zip_ref.extractall(self.temp_root)
|
|
211
|
+
|
|
212
|
+
return os.path.join(self.temp_root, 'filters_data'), os.path.join(self.temp_root, 'filters_data', 'filters.json')
|
|
213
|
+
|
|
214
|
+
def save(self):
|
|
215
|
+
filepath = os.path.join(self.filepath_root, "filters.json")
|
|
216
|
+
self.filters.save(filepath)
|
|
217
|
+
|
|
218
|
+
def load_filter_data(self, filepath):
|
|
219
|
+
data = []
|
|
220
|
+
with open(filepath,'r') as file:
|
|
221
|
+
try:
|
|
222
|
+
lines = file.readlines()
|
|
223
|
+
for line in lines:
|
|
224
|
+
match = re.search(r'(\d+.\d*)[\s,]+([-+]?[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?)', line)
|
|
225
|
+
if match is not None:
|
|
226
|
+
try:
|
|
227
|
+
x = float(match.group(1))
|
|
228
|
+
y = float(match.group(2))
|
|
229
|
+
data.append((x,y))
|
|
230
|
+
except Exception as err:
|
|
231
|
+
# not an actual data line
|
|
232
|
+
pass
|
|
233
|
+
|
|
234
|
+
except Exception as err:
|
|
235
|
+
if len(data) == 0:
|
|
236
|
+
return None
|
|
237
|
+
|
|
238
|
+
return data
|
|
239
|
+
|
|
240
|
+
def load_filters_table(self, filepath):
|
|
241
|
+
data = []
|
|
242
|
+
with open(filepath,'r') as file:
|
|
243
|
+
try:
|
|
244
|
+
lines = file.readlines()
|
|
245
|
+
for line in lines:
|
|
246
|
+
records = line.split('\t')
|
|
247
|
+
data.append(records)
|
|
248
|
+
except Exception as err:
|
|
249
|
+
if len(data) == 0:
|
|
250
|
+
return None
|
|
251
|
+
|
|
252
|
+
return data
|
|
253
|
+
|
|
254
|
+
def associate_file(self, event, button):
|
|
255
|
+
for selected_item in self.filters.widget.selection():
|
|
256
|
+
item = self.filters.widget.item(selected_item)
|
|
257
|
+
record = item['values']
|
|
258
|
+
|
|
259
|
+
part_number_idx = list(self.filters.columns.keys()).index('part_number')
|
|
260
|
+
description_idx = list(self.filters.columns.keys()).index('description')
|
|
261
|
+
supplier_idx = list(self.filters.columns.keys()).index('supplier')
|
|
262
|
+
|
|
263
|
+
query = str(record[part_number_idx])+"+"+str(record[description_idx])
|
|
264
|
+
query = query+f"+{record[supplier_idx]}+filter"
|
|
265
|
+
|
|
266
|
+
webbrowser.open(f"https://www.google.com/search?q={query}")
|
|
267
|
+
time.sleep(0.3)
|
|
268
|
+
browser_app = subprocess.run(["osascript","-e","return path to frontmost application as text"],capture_output=True, encoding='utf8').stdout
|
|
269
|
+
|
|
270
|
+
filepath = None
|
|
271
|
+
|
|
272
|
+
if self.webbrowser_download_path is None:
|
|
273
|
+
filepath = filedialog.askopenfilename()
|
|
274
|
+
else:
|
|
275
|
+
pre_list = os.listdir(self.webbrowser_download_path)
|
|
276
|
+
frontmost_app = subprocess.run(["osascript","-e","return path to frontmost application as text"],capture_output=True, encoding='utf8').stdout
|
|
277
|
+
while frontmost_app == browser_app:
|
|
278
|
+
self.window.widget.update_idletasks()
|
|
279
|
+
self.window.widget.update()
|
|
280
|
+
frontmost_app = subprocess.run(["osascript","-e","return path to frontmost application as text"],capture_output=True, encoding='utf8').stdout
|
|
281
|
+
post_list = os.listdir(self.webbrowser_download_path)
|
|
282
|
+
|
|
283
|
+
new_filepaths = list(set(post_list) - set(pre_list))
|
|
284
|
+
if len(new_filepaths) == 1:
|
|
285
|
+
filepath = os.path.join(self.webbrowser_download_path, new_filepaths[0])
|
|
286
|
+
else:
|
|
287
|
+
filepath = ''
|
|
288
|
+
|
|
289
|
+
if filepath != '':
|
|
290
|
+
shutil.copy2(filepath, self.filepath_root)
|
|
291
|
+
filename_idx = list(self.filters.columns.keys()).index('filename')
|
|
292
|
+
|
|
293
|
+
record[filename_idx] = os.path.basename(filepath)
|
|
294
|
+
self.webbrowser_download_path = os.path.dirname(filepath)
|
|
295
|
+
self.filters.widget.item(selected_item, values=record)
|
|
296
|
+
self.save()
|
|
297
|
+
|
|
298
|
+
def export_filters(self, event, button):
|
|
299
|
+
zip_filepath = filedialog.asksaveasfilename(
|
|
300
|
+
parent=self.window.widget,
|
|
301
|
+
title="Choose a filename:",
|
|
302
|
+
filetypes=[('Zip files','.zip')],
|
|
303
|
+
)
|
|
304
|
+
if zip_filepath:
|
|
305
|
+
with zipfile.ZipFile(zip_filepath, 'w') as zip_ref:
|
|
306
|
+
zip_ref.mkdir(self.filepath_root)
|
|
307
|
+
for filepath in Path(self.filepath_root).iterdir():
|
|
308
|
+
zip_ref.write(filepath, arcname=os.path.join(self.filepath_root,filepath.name))
|
|
309
|
+
|
|
310
|
+
def show_files(self, event, button):
|
|
311
|
+
self.reveal_path(self.filepath_root)
|
|
312
|
+
|
|
313
|
+
def copy_data(self, event, button):
|
|
314
|
+
install_modules_if_absent(modules={"pyperclip":"pyperclip"})
|
|
315
|
+
try:
|
|
316
|
+
import pyperclip
|
|
317
|
+
|
|
318
|
+
for selected_item in self.filters.widget.selection():
|
|
319
|
+
item = self.filters.widget.item(selected_item)
|
|
320
|
+
record = item['values']
|
|
321
|
+
|
|
322
|
+
filename_idx = list(self.filters.columns.keys()).index('filename')
|
|
323
|
+
filename = record[filename_idx]
|
|
324
|
+
|
|
325
|
+
filepath = os.path.join(self.filepath_root, filename)
|
|
326
|
+
if os.path.isfile(filepath):
|
|
327
|
+
data = self.load_filter_data(filepath)
|
|
328
|
+
|
|
329
|
+
text = ""
|
|
330
|
+
for x,y in data:
|
|
331
|
+
text = text + "{0}\t{1}\n".format(x,y)
|
|
332
|
+
|
|
333
|
+
pyperclip.copy(text)
|
|
334
|
+
except Exception as err:
|
|
335
|
+
print(err)
|
|
336
|
+
showerror(
|
|
337
|
+
title="Unable to copy to clipboard",
|
|
338
|
+
message="You must have the module pyperclip installed to copy the data.",
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def selection_changed(self, event, table):
|
|
343
|
+
for selected_item in table.widget.selection():
|
|
344
|
+
item = table.widget.item(selected_item)
|
|
345
|
+
record = item['values']
|
|
346
|
+
|
|
347
|
+
filename_idx = list(self.filters.columns.keys()).index('filename')
|
|
348
|
+
filename = record[filename_idx]
|
|
349
|
+
filepath = os.path.join(self.filepath_root, filename)
|
|
350
|
+
|
|
351
|
+
if os.path.exists(filepath) and not os.path.isdir(filepath):
|
|
352
|
+
|
|
353
|
+
data = self.load_filter_data(filepath)
|
|
354
|
+
|
|
355
|
+
self.filter_data.empty()
|
|
356
|
+
self.filter_plot.clear_plot()
|
|
357
|
+
for x,y in data:
|
|
358
|
+
self.filter_data.append((x,y))
|
|
359
|
+
self.filter_plot.append(x,y)
|
|
360
|
+
self.filter_plot.first_axis.set_ylabel("Transmission")
|
|
361
|
+
self.filter_plot.first_axis.set_xlabel("Wavelength [nm]")
|
|
362
|
+
self.filter_plot.update_plot()
|
|
363
|
+
self.copy_data_button.enable()
|
|
364
|
+
else:
|
|
365
|
+
self.filter_data.empty()
|
|
366
|
+
self.filter_plot.clear_plot()
|
|
367
|
+
self.filter_plot.update_plot()
|
|
368
|
+
self.copy_data_button.disable()
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
if __name__ == "__main__":
|
|
372
|
+
package_app_script(__file__)
|
|
373
|
+
install_modules_if_absent(modules={"requests":"requests","pyperclip":"pyperclip"}, ask_for_confirmation=False)
|
|
374
|
+
app = FilterDBApp()
|
|
375
|
+
app.mainloop()
|
|
376
|
+
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
### Example 2: Raytracing lens viewer
|
|
382
|
+
|
|
383
|
+
The following interface to the module ["Raytracing"](https://github.com/DCC-Lab/RayTracing) was created with **myTk**. It shows a list of lenses with their properties in a Tableview, clicking on the headers will sort the rows, clicking on a link will open the URL
|
|
384
|
+
in a browser. The figures underneath will reflect the properties of the selected item.
|
|
385
|
+
<img width="1451" alt="image" src="./README.assets/filter_database.png">
|
|
386
|
+
|
|
387
|
+
The code that generates this application is the following:
|
|
388
|
+
```python
|
|
389
|
+
from mytk import *
|
|
390
|
+
import raytracing as rt
|
|
391
|
+
import raytracing.thorlabs as thorlabs
|
|
392
|
+
import raytracing.eo as eo
|
|
393
|
+
from raytracing.figure import GraphicOf
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
class OpticalComponentViewer(App):
|
|
397
|
+
def __init__(self):
|
|
398
|
+
App.__init__(self, geometry="1450x750")
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
self.window.widget.title("Lens viewer")
|
|
402
|
+
self.window.resizable = False
|
|
403
|
+
self.label = None
|
|
404
|
+
self.menu = None
|
|
405
|
+
self.default_figsize = (7, 5)
|
|
406
|
+
self.header = View(width=1450, height=200)
|
|
407
|
+
self.header.grid_into(
|
|
408
|
+
self.window, column=0, row=0, pady=5, padx=5, sticky="nsew"
|
|
409
|
+
)
|
|
410
|
+
self.graphs = View(width=1450, height=700)
|
|
411
|
+
self.graphs.grid_into(
|
|
412
|
+
self.window, column=0, row=1, pady=5, padx=5, sticky="nsew"
|
|
413
|
+
)
|
|
414
|
+
self.component = None
|
|
415
|
+
self.dispersion = None
|
|
416
|
+
|
|
417
|
+
self.lenses = {}
|
|
418
|
+
self.build_lens_dict()
|
|
419
|
+
self.build_table()
|
|
420
|
+
|
|
421
|
+
self.update_figure()
|
|
422
|
+
|
|
423
|
+
def build_table(self):
|
|
424
|
+
self.columns = {
|
|
425
|
+
"label": "Part number",
|
|
426
|
+
"backFocalLength": "Front focal length [mm]",
|
|
427
|
+
"frontFocalLength": "Back focal length [mm]",
|
|
428
|
+
"effectiveFocalLengths": "Effective focal length [mm]",
|
|
429
|
+
"apertureDiameter": "Diameter [mm]",
|
|
430
|
+
"wavelengthRef": "Design wavelength [nm]",
|
|
431
|
+
"materials": "Material(s)",
|
|
432
|
+
"url": "URL",
|
|
433
|
+
}
|
|
434
|
+
self.table = TableView(columns=self.columns)
|
|
435
|
+
self.table.delegate = self
|
|
436
|
+
self.table.grid_into(self.header, sticky="nsew", padx=5)
|
|
437
|
+
|
|
438
|
+
for column in self.columns:
|
|
439
|
+
self.table.widget.column(column, width=150, anchor=CENTER)
|
|
440
|
+
self.table.widget.column("url", width=350, anchor=W)
|
|
441
|
+
|
|
442
|
+
iids = []
|
|
443
|
+
for label, lens in self.lenses.items():
|
|
444
|
+
if lens.wavelengthRef is not None:
|
|
445
|
+
wavelengthRef = "{0:.1f}".format(lens.wavelengthRef * 1000)
|
|
446
|
+
else:
|
|
447
|
+
wavelengthRef = "N/A"
|
|
448
|
+
|
|
449
|
+
materials = ""
|
|
450
|
+
if isinstance(lens, rt.AchromatDoubletLens):
|
|
451
|
+
if lens.mat1 is not None and lens.mat2 is not None:
|
|
452
|
+
materials = "{0}/{1}".format(str(lens.mat1()), str(lens.mat2()))
|
|
453
|
+
elif isinstance(lens, rt.SingletLens):
|
|
454
|
+
if lens.mat is not None:
|
|
455
|
+
materials = "{0}".format(str(lens.mat()))
|
|
456
|
+
|
|
457
|
+
iid = self.table.append(
|
|
458
|
+
values=(
|
|
459
|
+
lens.label,
|
|
460
|
+
"{0:.1f}".format(lens.backFocalLength()),
|
|
461
|
+
"{0:.1f}".format(lens.frontFocalLength()),
|
|
462
|
+
"{0:.1f}".format(lens.effectiveFocalLengths()[0]),
|
|
463
|
+
"{0:.1f}".format(lens.apertureDiameter),
|
|
464
|
+
wavelengthRef,
|
|
465
|
+
materials,
|
|
466
|
+
lens.url,
|
|
467
|
+
)
|
|
468
|
+
)
|
|
469
|
+
iids.append(iid)
|
|
470
|
+
|
|
471
|
+
self.table.widget.selection_set(iids[0])
|
|
472
|
+
|
|
473
|
+
scrollbar = ttk.Scrollbar(
|
|
474
|
+
self.header.widget, orient=VERTICAL, command=self.table.widget.yview
|
|
475
|
+
)
|
|
476
|
+
self.table.widget.configure(yscroll=scrollbar.set)
|
|
477
|
+
scrollbar.grid(row=0, column=1, sticky="ns")
|
|
478
|
+
|
|
479
|
+
def update_figure(self, figure=None):
|
|
480
|
+
if figure is not None:
|
|
481
|
+
figure.set_size_inches(self.default_figsize)
|
|
482
|
+
self.component = Figure(figure, figsize=self.default_figsize)
|
|
483
|
+
self.component.grid_into(self.graphs, column=0, row=0, padx=5)
|
|
484
|
+
self.dispersion = Figure(figsize=self.default_figsize)
|
|
485
|
+
self.dispersion.grid_into(self.graphs, column=1, row=0, padx=5)
|
|
486
|
+
|
|
487
|
+
@property
|
|
488
|
+
def figure(self):
|
|
489
|
+
return self.component.figure
|
|
490
|
+
|
|
491
|
+
@figure.setter
|
|
492
|
+
def figure(self, value):
|
|
493
|
+
self.update_figure(value)
|
|
494
|
+
|
|
495
|
+
def build_lens_dict(self):
|
|
496
|
+
modules = [thorlabs, eo]
|
|
497
|
+
|
|
498
|
+
for i, lens in enumerate(rt.CompoundLens.all()):
|
|
499
|
+
for module in modules:
|
|
500
|
+
try:
|
|
501
|
+
class_ = getattr(module, lens)
|
|
502
|
+
lens = class_()
|
|
503
|
+
f1, f2 = lens.effectiveFocalLengths()
|
|
504
|
+
self.lenses[lens.label] = lens
|
|
505
|
+
except Exception as err:
|
|
506
|
+
pass
|
|
507
|
+
|
|
508
|
+
def selection_changed(self, event):
|
|
509
|
+
for selected_item in self.table.widget.selection():
|
|
510
|
+
item = self.table.widget.item(selected_item)
|
|
511
|
+
record = item["values"]
|
|
512
|
+
lens = self.lenses[record[0]] # label
|
|
513
|
+
self.update_figures(lens)
|
|
514
|
+
|
|
515
|
+
def update_figures(self, lens):
|
|
516
|
+
graphic = GraphicOf(lens)
|
|
517
|
+
self.figure = graphic.drawFigure().figure
|
|
518
|
+
self.figure.set_size_inches((5, 5), forward=True)
|
|
519
|
+
|
|
520
|
+
try:
|
|
521
|
+
wavelengths, focalShifts = lens.focalShifts()
|
|
522
|
+
|
|
523
|
+
axis = self.dispersion.figure.add_subplot()
|
|
524
|
+
axis.plot(wavelengths, focalShifts, "k-")
|
|
525
|
+
axis.set_xlabel(r"Wavelength [nm]")
|
|
526
|
+
axis.set_ylabel(r"Focal shift [mm]")
|
|
527
|
+
except Exception as err:
|
|
528
|
+
pass
|
|
529
|
+
|
|
530
|
+
def about(self):
|
|
531
|
+
showinfo(
|
|
532
|
+
title="About Lens Viewer",
|
|
533
|
+
message="A lens viewer for the Raytracing package by the DCC/M Lab.",
|
|
534
|
+
)
|
|
535
|
+
|
|
536
|
+
def help(self):
|
|
537
|
+
webbrowser.open("https://raytracing.readthedocs.io/")
|
|
538
|
+
|
|
539
|
+
def doubleclick_cell(self, item_id, column_id, item_dict):
|
|
540
|
+
value = item_dict["values"][column_id - 1]
|
|
541
|
+
pyperclip.copy(value)
|
|
542
|
+
return True
|
|
543
|
+
|
|
544
|
+
|
|
545
|
+
if __name__ == "__main__":
|
|
546
|
+
rt.silentMode()
|
|
547
|
+
app = OpticalComponentViewer()
|
|
548
|
+
|
|
549
|
+
from packaging.version import Version
|
|
550
|
+
if Version(rt.__version__) <= Version("1.3.10"):
|
|
551
|
+
showerror(title="Minimum Raytracing version", message="You need at least Raytracing 1.3.11 to run the lens viewer", icon=ERROR)
|
|
552
|
+
else:
|
|
553
|
+
app.mainloop()
|
|
554
|
+
|
|
555
|
+
```
|