Part 7
ABAQUS INP COMPREHENSIVE ANALYZER
Under the Hood — A Deep-Dive Series
PART 7
Interface Architecture, Color Themes,
Preferences, and PyInstaller Distribution
The Systems Between the Computation and the Person Using It
Joseph P. McFadden Sr.
McFaddenCAE.com | The Holistic Analyst
© 2026 Joseph P. McFadden Sr. All rights reserved.
Setup — The Other Half of the Program
The first six parts of this series went deep into what the program does with your data — parsing, geometry, materials, recommendations, exports, relationships. All of that is computation: algorithms operating on numbers and structures.
This part covers everything that sits between that computation and the person using it: the interface architecture, the theming system, the preferences machinery, and the packaging that lets the program run on a machine with no Python installed.
These are the systems that most technical users ignore until something goes wrong — a font that is too small on a high-resolution screen, a color scheme that is unreadable in their office lighting, a preference that does not save between sessions. Understanding how they work means understanding how to fix them when they need it, and how to extend them when a new capability is needed.
Section 1 — The App Class: A Window That Knows Itself
The entire program lives inside a single Python class called App. That class inherits from tk.Tk — which is tkinter's root window class. This is not the usual pattern for tkinter applications, which more commonly create a separate root window and then build an application object on top of it. Inheriting directly from tk.Tk makes the application class and the root window the same object.
The consequence is that the App instance is the window. When you call self.configure, you are configuring the main window. When you call self.after, you are scheduling a callback on the main event loop. When you call self.geometry, you are setting the window size. Everything related to the window is a method call on the same object that holds all the parsed model data and all the analysis logic.
This tight coupling makes the code simple — there is no layer of indirection between the window and the application — and it means any method anywhere in the class has direct access to both the UI and the data without passing anything around.
Startup Sequence
When App's init method runs, it executes in a specific order. First it calls the parent tk.Tk initializer to create the window. Then it sets the title bar text. Then it calculates an initial geometry — 1,400 by 800 pixels — and adjusts it to fit the screen, centering it and accounting for the taskbar height.
Then preferences are loaded. If a saved window geometry exists in the preferences file, that geometry is applied, restoring the window to exactly the size and position the user left it at. If the saved state was maximized, the zoomed state is applied. If the preferences file does not exist or cannot be read, the default geometry is used.
Then the two font objects are created — one for text areas and one for listboxes — using the font families and sizes from the loaded preferences. Then the style engine is initialized, the color theme is applied, and finally build_ui is called to construct the entire interface.
The ordering matters. Fonts must exist before the widgets that use them are built. The theme must be applied before the notebook tabs are constructed, or the tab styling will not be correct. Preferences must be loaded before fonts and themes are initialized from them. This is a dependency chain, and the init method respects it.
Section 2 — Building the UI: Menu Bar, File Bar, and the Notebook
The build_ui method constructs the entire visible interface in one pass. It runs once at startup and produces the complete window layout.
The Menu Bar
A tkinter menu bar is created and attached to the window with self.config(menu=menubar). Three menus are added: View, Tools, and Help.
The View menu contains Font Settings and Color Theme, plus a Reset to Defaults option. These are the only items under View — everything in this menu is about how the interface looks, not about what it does.
The Tools menu contains Unit System, Check Unit Consistency, Analyze Simulation Intent, and Debug Console Output. The Debug Console Output item is dynamic — its label changes between ON and OFF depending on the current state, updated every time it is toggled. The menu item index is stored when the menu is built so the label can be updated later without rebuilding the entire menu.
The Help menu contains About, which opens a small dialog with the program version, author, contact, and copyright notice.
The File Selection Bar
Below the menu bar, a horizontal frame contains the file selection controls. A label, a wide entry widget bound to a string variable called path_var, a Browse button, and a Process button. The Process button starts in the disabled state and is only enabled when a file path is populated.
A ttk Separator — a thin horizontal line — sits below the file bar, visually separating it from the notebook area below.
The Notebook
The main content area is a ttk.Notebook — the tabbed panel widget. It is placed inside a container frame with a groove border style, which draws a subtle three-dimensional border around the tab area.
The nine tabs are built in order by calling their respective builder methods: build_tab_summary, build_tab_materials, build_tab_properties, build_tab_sections, build_tab_plotting, build_tab_parts, build_tab_recommendations, build_tab_edits, and build_tab_learning_center. Each builder method creates all the widgets for that tab and adds them to the tab's frame.
The call to setup_initial_sash_positions is scheduled with after(100) — meaning it runs 100 milliseconds after the UI is fully rendered. This delay is necessary because the paned windows need to be visible and sized before their sash positions can be set. Calling sashpos before the window is rendered has no effect. The 100-millisecond delay guarantees the window is drawn before the sash adjustment runs.
Section 3 — The Widget Tracking System
One of the quieter but more important architectural decisions in the interface is the widget tracking system. Every text area — every ScrolledText widget — in the interface is appended to a list called self.text_widgets at the time it is created. Every listbox is appended to self.list_widgets.
These two lists are the mechanism that makes program-wide font and theme changes work without rebuilding any widgets.
When you open Font Settings and click Apply, the apply_fonts method reconfigures the two font objects — text_font and list_font — with the new family and size. Then it loops through text_widgets and calls widget.configure(font=self.text_font) on every entry. Then it loops through list_widgets and applies list_font to every listbox.
One call per widget. No conditional logic, no tab-specific code. Every text area in the Summary tab, the Materials tab, the Property Viewer, the Parts tab, the Recommendations tab — they all update simultaneously because they all registered themselves at build time.
The apply_theme method works the same way for colors. It configures the ttk.Style for the notebook, tabs, frames, labels, radio buttons, and checkboxes — all the themed widgets managed by the ttk style engine. Then it loops through text_widgets and applies the theme's text background, foreground, and selection colors. Then through list_widgets for the same.
The approach is registration at construction time, bulk application at change time. This is a clean and scalable pattern. When a new tab is added — like the Learning Center was added in a later version — it just needs to append its widgets to the existing lists. Theme and font support is automatic.
Section 4 — Paned Windows and the Sash Position Problem
Every tab in the program that has a left-right split — and that is most of them — uses a ttk.PanedWindow widget. A paned window is a container that holds two or more child frames separated by a draggable divider called a sash. The user can drag the sash left or right to adjust how much space each side gets.
The challenge is defaults. By default, a paned window splits its space equally between its children — fifty percent left, fifty percent right. For this program, that is almost never ideal. The left panels — the material list, the parts list, the section list — are narrow navigation panels. The right panels — the detail text areas — are where the content lives and need to be wider.
The setup_initial_sash_positions method addresses this by explicitly positioning each sash after the window renders. The Materials tab sash is set to 200 pixels from the left. The Sections tab gets 250. The Property Viewer gets 180. The Plotting tab gets 220. These specific values were determined by trial and measurement — the narrowest useful width for the left panel in each tab.
The scheduling via after(100) is critical here. The tkinter widget model has a fundamental constraint: geometry is not calculated until the widget is actually rendered on screen. If you call sashpos(0, 200) on a paned window before it has been drawn, the paned window does not know its own size yet and the position has no meaningful reference. The 100-millisecond delay puts the sash adjustment after the first render pass, when widget dimensions are known.
This pattern — schedule a geometry adjustment after a short delay — appears in several places in the program's UI code. It is a workaround for a fundamental aspect of how immediate-mode GUI toolkits work: you can create widgets before they have a size, but you cannot position things relative to their size until they have been drawn at least once.
Section 5 — The Color Theme System
The program offers eight color themes. Each theme is defined as a dictionary with nine keys: background color, foreground color, text area background, text area foreground, selection background, selection foreground, tab background, selected tab background, and tab foreground text.
The eight themes are: Default — a standard light gray interface. Dark Mode — a dark charcoal interface with light gray text, similar to dark mode in modern code editors. Ocean Blue — a blue-tinted light theme with navy text. Forest Green — a green-tinted light theme. Warm Sand — a warm beige tone. Purple Dusk — a purple-tinted light theme. High Contrast — a pure black background with bright green text, designed for maximum visibility in challenging lighting conditions. Slate Gray — a cool gray professional tone.
The theme dictionary keys map directly to specific widget configuration parameters. When apply_theme runs, it reads those keys and passes them to the style engine and widget configuration calls. There is no color calculation, no inheritance, no derivation. Every color in every theme is explicitly stated. This is verbose but transparent — you can read the theme definition and immediately know exactly what every element will look like.
The ttk.Style Engine
tkinter has two widget families: the classic tk widgets and the themed ttk widgets. The ttk widgets are styled through a centralized ttk.Style object rather than per-widget configuration calls.
The style object is initialized once in the App init method. apply_theme calls configure and map methods on the style to set tab appearance, frame backgrounds, label colors, and checkbox and radio button backgrounds. The configure method sets static properties. The map method sets state-dependent properties — for example, a tab has one background color when it is not selected and a different color when it is selected. The map method handles that conditional styling.
Classic tk widgets — the Text widget, the Listbox — do not respond to the ttk style engine. Their colors are set directly via the bg, fg, selectbackground, and selectforeground configuration parameters. That is why apply_theme loops through text_widgets and list_widgets explicitly — these widgets are outside the ttk styling system and need direct configuration.
Section 6 — The Font System: Two Fonts, One Policy
The program uses exactly two font objects: text_font and list_font. This is a deliberate simplification.
text_font is the font used in all scrolled text areas — the Summary tab output, the material detail panels, the property data viewer, the recommendations detail area, the edit preview. It defaults to Courier at 10 points. Courier is a monospace font, meaning all characters have the same width. Monospace is the correct choice for tabular data — columns stay aligned regardless of whether a line contains mostly narrow characters like i and l or wide characters like w and m.
list_font is the font used in all listboxes — the materials list, the parts list, the sections list, the dataset list, the recommendations list, the topic list. It defaults to TkDefaultFont at 9 points. TkDefaultFont is the operating system's default UI font — on Windows that is Segoe UI, on macOS San Francisco, on Linux it varies. Using the system default ensures listboxes look native to the platform.
Font objects in tkinter are mutable. When you call font_object.config(family='Helvetica', size=12), the change propagates immediately to all widgets that use that font object — without any explicit widget update required. This is why the apply_fonts method is so short: reconfigure the two font objects, and all registered widgets update automatically through the font binding.
The Font Settings Dialog
The Font Settings dialog opened from the View menu has two sections: one for the text areas font and one for the lists font. Each section has a dropdown for font family — populated from the fonts currently installed on the system — and a spinner for font size.
A live preview panel at the bottom of the dialog shows sample text rendered in whatever font and size the text-area controls currently show. The preview updates in real time as you change the dropdowns and spinners, using a variable trace — a tkinter mechanism that calls a function whenever a variable's value changes. You can audition dozens of fonts without committing to any of them until you click Apply.
When Apply is clicked, the preferences dictionary is updated, the preferences file is saved, and apply_fonts is called to push the changes to all widgets. The dialog closes.
Section 7 — The Preferences System: Persistence Across Sessions
The program remembers your settings between sessions. This is handled by a JSON preferences file stored in the user's home directory. The file path is constructed using os.path.expanduser('~/') — which resolves to the user's home directory on all platforms — combined with the filename .abaqus-analyzer-config.json.
JSON was chosen over other formats specifically for its human readability. The preferences file is plain text that you can open in any editor. If you need to reset a specific preference without running the program, you can edit the file directly. If the file gets corrupted, you can delete it and the program will create fresh defaults on next launch.
What Is Stored
The preferences dictionary contains: the config version number, used for future migration when the format changes; the text font family and size; the list font family and size; the color theme name; the last window geometry as a position-and-size string; the window state — normal or zoomed; the last directory used in the file browser; a debug console toggle; and a dictionary of custom part colors, capped at fifty entries.
The part colors dictionary maps part names to their user-selected colors. When you choose a custom color for a part in the 3D viewer and then close the program, that color association is remembered. Next time you view that part, it uses the same color. The fifty-entry cap prevents the preferences file from growing indefinitely in projects with large part counts.
The Load and Save Functions
The load_preferences function first defines a defaults dictionary — every preference key with its default value. If the config file exists and can be parsed as valid JSON, the saved values are merged onto the defaults: a new dictionary is created from defaults with the saved preferences layered on top. Any key present in the file overrides the default; any key absent in the file gets the default. This merge pattern handles version changes gracefully — if a new preference key is added in a future version, existing config files without that key simply get the new default.
If the config file does not exist, cannot be read, or contains invalid JSON, the except block catches the error silently and returns the defaults. The program starts cleanly with defaults rather than crashing on a bad config file.
The save_preferences function serializes the preferences dictionary to JSON with indent=2 — producing a nicely formatted, human-readable file. It writes to the config file path with UTF-8 encoding. If the write fails — a permissions error, a full disk, a network drive issue — it catches the exception and prints a console message. The failure to save preferences is not a fatal error.
Section 8 — Window State Persistence
When you close the program, the on_closing method runs — registered as the handler for the WM_DELETE_WINDOW event, which is the event tkinter receives when you click the window's close button or use Alt-F4.
On closing, three things are saved to preferences before the window is destroyed. First, the current window geometry — the result of calling self.geometry() with no arguments, which returns the window's current size and position as a formatted string. Second, the window state — normal, or zoomed if maximized. Third, the current color theme, identified by comparing the current theme dictionary against all named themes in the COLOR_THEMES dictionary and recording the matching name.
After saving, save_preferences is called to write the file, and then self.destroy() closes the window and exits the program.
On the next launch, the init method reads those saved values and restores them. The window appears at the same size and position it was when closed. If it was maximized, it reopens maximized. If it was at a specific position on a secondary monitor, it reopens there.
This round-trip — save on close, restore on open — is what makes the program feel persistent. It is not a significant technical achievement, but it is one of the details that distinguishes a tool someone built for themselves from a tool someone built for other people to use day after day.
Section 9 — Debug Mode
The program has a built-in debug mode toggled from the Tools menu. When debug mode is on, the program writes detailed console output — verbose logging of what it is doing at each step of parsing, part identification, volume calculation, and all other major operations.
Debug output is handled by a module-level function called dprint. Every place in the code where diagnostic information might be useful, the code calls dprint rather than the Python built-in print. When debug mode is off, dprint is a no-op — it returns immediately without producing any output. When debug mode is on, dprint calls print, and the message appears in the console.
The set_debug function toggles the behavior. When debug is enabled, set_debug replaces the dprint function with a version that calls print. When disabled, it replaces it with a version that does nothing.
This pattern — conditional dispatch via function replacement — is more efficient than checking a flag inside every dprint call. With millions of potential dprint calls in a large analysis, the overhead of checking a boolean flag each time would be measurable. Replacing the function itself means the disabled path has zero cost: the no-op function is called and returns immediately without any conditional logic.
The debug toggle's menu label changes between DEBUG CONSOLE OUTPUT: ON with a checkmark and DEBUG CONSOLE OUTPUT: OFF whenever the item is clicked. The label update uses the stored menu reference and item index to reconfigure just that one menu entry without rebuilding the menu.
For developers working on extensions to the program: dprint is your friend. Add a dprint call wherever you are tracking state or diagnosing unexpected behavior. The output appears only when the user has explicitly turned debug mode on, so it adds no noise during normal use.
Section 10 — Graceful Degradation: Optional Dependency Architecture
The program is designed to run on a minimal Python installation. The core features — parsing, material display, recommendations — require nothing beyond Python's standard library plus tkinter. Everything else is optional.
Every optional dependency is imported inside a try-except block at the module level. If the import succeeds, a boolean availability flag is set to True and the imported functions are stored in module-level variables. If the import fails, the flag stays False and the functions are set to None.
Throughout the code, before calling any optional function, the corresponding availability flag is checked. If it is False, the UI element for that feature is disabled — greyed out — and a message explains what package would enable it.
The optional dependencies and what they enable: scipy.spatial provides the K-D tree for the nearest-parts search and the penetration check — without it, a slower brute-force fallback runs. NumPy provides vectorized volume calculation — without it, element-by-element calculation runs. Matplotlib provides the 3D viewer and the material property plots — without it, the View in 3D button and the plot buttons are disabled. The STL exporter module provides STL export. The STEP exporter module, which requires PythonOCC and OpenCASCADE, provides STEP export — without it, the STEP button is disabled.
The best practices module, the simulation intent classifier module, and the volume-mass calculator module are all handled the same way. If present, they load and contribute their capabilities. If absent, the program continues without them and their features are silently unavailable.
This architecture has a specific benefit for distribution: you can ship different tiers of the program depending on what dependencies are available on the target machine. A minimal install gives you parsing, materials analysis, and recommendations. A full install adds visualization, STL export, and all the spatial analysis tools. The same codebase, the same executable — just different results from the dependency check at startup.
Section 11 — PyInstaller: Packaging for Distribution
Python programs typically require Python to be installed on any machine that runs them. For a tool intended for use across an engineering team — where not everyone has Python, where IT policies may restrict software installation, where the program needs to run on machines without internet access — this is a significant barrier.
PyInstaller solves this by bundling the Python interpreter, all required packages, and the program's own source files into a single directory or a single executable file. The result is a standalone application that runs on any Windows machine — or macOS or Linux, depending on where PyInstaller runs — without any Python installation required.
How PyInstaller Works
PyInstaller analyzes the program's import tree — starting from the main script, it follows every import statement and collects the files needed for every imported module. It then copies the Python interpreter itself, all the module files, and any data files specified in the build configuration into an output directory. When the resulting executable runs, it extracts those files to a temporary location and runs the program against the bundled Python interpreter.
The key distinction is between a single-directory build and a single-file build. A single-directory build produces a folder — typically named dist/program-name — containing the executable plus dozens or hundreds of support files. A single-file build packages everything into one executable that extracts itself to a temporary directory at runtime. Single-file is more convenient to distribute but slower to start up, because extraction takes time on each launch.
The Frozen Check
When a PyInstaller-packaged program runs, Python's sys module has a special attribute: sys.frozen is set to True. When running as a normal Python script, sys.frozen does not exist.
The program checks this at the very top of the file, before any other imports, using getattr(sys, 'frozen', False). If it evaluates to True, the program is running as a compiled executable and needs to handle one specific difference from the normal Python environment: the matplotlib configuration directory.
Matplotlib writes configuration and cache files to a directory it expects to find in a writable location. In a normal Python installation, this directory is somewhere in the user's home folder. In a PyInstaller bundle, the program files are extracted to a temporary directory that may not be writable, or that gets cleaned up between runs.
The fix is to redirect matplotlib's configuration directory to a location that is guaranteed writable and persistent. The frozen check sets the MPLCONFIGDIR environment variable to a directory called mpl_config sitting next to the executable — in the same folder as the packaged application. This directory persists between runs, and the user has write access because they installed the application there.
The check runs before the matplotlib import. This ordering is critical — if matplotlib is imported before MPLCONFIGDIR is set, matplotlib will have already chosen its configuration directory and changing the environment variable afterward has no effect.
The Spec File
PyInstaller builds are controlled by a spec file — a Python script that specifies which files to include, what the executable name should be, whether to produce a single file or a directory, and how to handle hidden imports — modules that PyInstaller's static analysis does not discover automatically because they are imported dynamically at runtime.
The program's optional dependencies — the STL exporter, the STEP exporter, the volume calculator, the best practices module — are all separate Python files that live alongside the main script. The spec file needs to explicitly include these as data files or hidden imports so they are bundled into the distribution. Without this, the packaged executable starts up and all the try-except import blocks fail, leaving all optional features disabled.
When packaging the program, verifying each optional feature by running the packaged version is essential. The availability flags printed to the console at startup — lines like 'STL exporter loaded successfully' or 'Best practices module loaded successfully' — confirm that each module was found by the bundled Python interpreter.
Section 12 — The _MEIPASS Path and Resource Location
There is a second PyInstaller-specific detail that matters for any program that reads files at runtime — not imports Python modules, but reads files like images, data files, or reference databases.
When a PyInstaller single-file executable runs, it extracts its bundled files to a temporary directory. The path to that temporary directory is stored in sys._MEIPASS. When running as a normal Python script, sys._MEIPASS does not exist.
Any code that opens a file with a path relative to the script's location — using something like os.path.dirname(__file__) — will work correctly when running as a script but fail when running as a packaged executable, because file in the packaged version points to the temporary extraction directory, not the original source directory.
The standard pattern for handling this is a resource path function: try to get sys._MEIPASS and join the requested relative path to it; if sys._MEIPASS does not exist, fall back to the directory containing the script. This function is used anywhere the code needs to locate a file that was bundled with the program.
The material library data, the unit system definitions, and similar reference files need this treatment if they are stored as separate files rather than embedded in the source code as Python dictionaries. In this program, the material library and unit systems are defined directly in the unit_detection module as Python data structures, so they are handled by the normal module bundling mechanism and do not need the resource path treatment.
Section 13 — The Complete Interface Startup Sequence
The ordered startup sequence from launch to first-use-ready state.
1. The sys.frozen check runs before any import. If frozen, MPLCONFIGDIR is redirected. Matplotlib is then imported and configured to use the TkAgg backend — the backend that renders matplotlib figures inside tkinter windows.
2. All optional modules are imported inside try-except blocks. Each availability flag is set. Modules that loaded successfully print confirmation messages to the console.
3. Module-level constants are defined: the config file path, the COLOR_THEMES dictionary, the KNOWN_PROP_HEADERS dictionary, the element type sets, the regex patterns.
4. App.__init__ runs: the window is created and sized, load_preferences is called, the saved window geometry and state are applied, font objects are created from preferences, the ttk.Style is initialized, the current theme is loaded and apply_theme is called, and build_ui constructs the complete interface.
5. The after(100) callback fires and setup_initial_sash_positions sets the divider positions in all paned windows.
6. The event loop starts. The window is visible and responsive. The Process button is disabled, waiting for a file selection.
7. The user selects a file. The Process button enables. Analysis runs in a background thread. Dialogs surface decisions. Tabs populate. The interface is fully operational.
That is the complete path from a Python file on disk — or a packaged executable — to a running analysis session.
Closing — The Interface Is Part of the Engineering
Software interface design is not decoration. It is engineering.
The decision to track widgets in lists so font and theme changes propagate uniformly — that is an engineering decision. The decision to schedule sash positions after rendering rather than before — that is debugging the behavior of a GUI toolkit. The decision to use JSON for preferences because it is human-readable and recoverable — that is making a considered choice about failure modes. The frozen check for matplotlib configuration — that is understanding how a packaging tool changes the runtime environment and addressing it before it breaks things.
Every one of those decisions is as deliberate as any decision in the parsing logic or the geometry engine. The interface layer deserves the same analytical attention as the computation layer. Users encounter the interface on every interaction. They encounter the parsing logic only when something goes wrong.
This completes the Under the Hood series for version 15.7 of the Abaqus INP Comprehensive Analyzer. Seven parts covering model processing, the geometry engine, materials and recommendations, the export pipeline, the learning center, part relationships and spatial analysis, and this final part on interface architecture and distribution.
The program continues to develop. Future parts of this series may cover the STEP integration when it is fully bridged into the main tool, the injection mold tooling validation capabilities, and additional analysis in unit detection and material library expansion.
All source code, documentation, and companion readers for this series are at McFaddenCAE.com.
End of Part 7 — Interface Architecture, Color Themes, Preferences, and PyInstaller Distribution
This completes the Under the Hood series for V15.7
© 2026 Joseph P. McFadden Sr. All rights reserved. | McFaddenCAE.com