From 204678026e73238be5c4810f73df03a5fbffdf2c Mon Sep 17 00:00:00 2001 From: sladro Date: Tue, 2 Dec 2025 11:00:33 +0800 Subject: [PATCH] fix: resolve Tcl/Tk init.tcl not found error when running from explorer - Move _configure_tk_runtime() before tkinter import to set TCL_LIBRARY early - Add PyInstaller 6.x _internal directory support for Tcl/Tk paths - Switch ControlPanel.spec to onedir mode for reliable Tcl/Tk bundling Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> --- ControlPanel.spec | 47 ++++++++++++++++++++++ ControlPanel_debug.spec | 47 ++++++++++++++++++++++ src/ui/control_panel.py | 87 +++++++++++++++++++++++++++++++++++++++-- 3 files changed, 177 insertions(+), 4 deletions(-) create mode 100644 ControlPanel.spec create mode 100644 ControlPanel_debug.spec diff --git a/ControlPanel.spec b/ControlPanel.spec new file mode 100644 index 0000000..ee1a472 --- /dev/null +++ b/ControlPanel.spec @@ -0,0 +1,47 @@ +# -*- mode: python ; coding: utf-8 -*- + + +a = Analysis( + ['D:\\App\\python\\YantaiVisionX\\src\\ui\\control_panel.py'], + pathex=[], + binaries=[], + datas=[('C:\\Users\\sladr\\anaconda3\\envs\\YantaiVisionX\\Library\\lib\\tcl8.6', 'tcl/tcl8.6'), ('C:\\Users\\sladr\\anaconda3\\envs\\YantaiVisionX\\Library\\lib\\tk8.6', 'tcl/tk8.6')], + hiddenimports=[], + hookspath=[], + hooksconfig={}, + runtime_hooks=[], + excludes=[], + noarchive=False, + optimize=0, +) +pyz = PYZ(a.pure) + +exe = EXE( + pyz, + a.scripts, + [], + exclude_binaries=True, + name='ControlPanel', + debug=False, + bootloader_ignore_signals=False, + strip=False, + upx=True, + upx_exclude=[], + runtime_tmpdir=None, + console=False, + disable_windowed_traceback=False, + argv_emulation=False, + target_arch=None, + codesign_identity=None, + entitlements_file=None, +) + +coll = COLLECT( + exe, + a.binaries, + a.datas, + strip=False, + upx=True, + upx_exclude=[], + name='ControlPanel', +) diff --git a/ControlPanel_debug.spec b/ControlPanel_debug.spec new file mode 100644 index 0000000..72f747c --- /dev/null +++ b/ControlPanel_debug.spec @@ -0,0 +1,47 @@ +# -*- mode: python ; coding: utf-8 -*- +# Debug version with onedir mode to inspect file structure + +a = Analysis( + ['D:\\App\\python\\YantaiVisionX\\src\\ui\\control_panel.py'], + pathex=[], + binaries=[], + datas=[('C:\\Users\\sladr\\anaconda3\\envs\\YantaiVisionX\\Library\\lib\\tcl8.6', 'tcl/tcl8.6'), ('C:\\Users\\sladr\\anaconda3\\envs\\YantaiVisionX\\Library\\lib\\tk8.6', 'tcl/tk8.6')], + hiddenimports=[], + hookspath=[], + hooksconfig={}, + runtime_hooks=[], + excludes=[], + noarchive=False, + optimize=0, +) +pyz = PYZ(a.pure) + +exe = EXE( + pyz, + a.scripts, + [], + exclude_binaries=True, + name='ControlPanel_debug', + debug=False, + bootloader_ignore_signals=False, + strip=False, + upx=True, + upx_exclude=[], + runtime_tmpdir=None, + console=True, # Enable console to see errors + disable_windowed_traceback=False, + argv_emulation=False, + target_arch=None, + codesign_identity=None, + entitlements_file=None, +) + +coll = COLLECT( + exe, + a.binaries, + a.datas, + strip=False, + upx=True, + upx_exclude=[], + name='ControlPanel_debug', +) diff --git a/src/ui/control_panel.py b/src/ui/control_panel.py index dcf313c..9c70614 100644 --- a/src/ui/control_panel.py +++ b/src/ui/control_panel.py @@ -2,17 +2,96 @@ from __future__ import annotations +import os +import sys +from pathlib import Path + + +def _configure_tk_runtime() -> None: + """Ensure Tcl/Tk resource paths are set when running from a frozen bundle.""" + + def _env_path_is_valid(var_name: str) -> bool: + path = os.environ.get(var_name) + return bool(path and Path(path).exists()) + + def _preferred_candidates(folder: str) -> list[Path]: + candidates: list[Path] = [] + if getattr(sys, "frozen", False): + base = Path(getattr(sys, "_MEIPASS", Path(__file__).parent)) + candidates.extend( + [ + base / "_internal" / "tcl" / folder, # PyInstaller 6.x onedir + base / "tcl" / folder, + base / folder, + ] + ) + special_map = { + "tcl8.6": [base / "_internal" / "_tcl_data", base / "_tcl_data"], + "tk8.6": [base / "_internal" / "_tk_data", base / "_tk_data"], + } + candidates.extend(special_map.get(folder, [])) + return candidates + + def _common_roots() -> list[Path]: + roots: list[Path] = [] + try: + roots.append(Path(__file__).resolve().parents[2]) + except ValueError: + pass + roots.extend([Path(sys.base_prefix), Path(sys.prefix)]) + + for var_name in ("TCL_LIBRARY", "TK_LIBRARY"): + env_value = os.environ.get(var_name) + if env_value: + roots.append(Path(env_value).parent) + + deduped: list[Path] = [] + seen: set[Path] = set() + for root in roots: + try: + resolved = root.resolve() + except OSError: + continue + if resolved in seen: + continue + seen.add(resolved) + deduped.append(resolved) + return deduped + + def _set_library(var_name: str, folder_name: str) -> None: + forced = False + if getattr(sys, "frozen", False): + for candidate in _preferred_candidates(folder_name): + if candidate.exists() and candidate.is_dir(): + os.environ[var_name] = str(candidate) + forced = True + break + if forced or _env_path_is_valid(var_name): + return + + for root in _common_roots(): + for candidate in (root / "tcl" / folder_name, root / folder_name): + if candidate.exists() and candidate.is_dir(): + os.environ[var_name] = str(candidate) + return + + _set_library("TCL_LIBRARY", "tcl8.6") + _set_library("TK_LIBRARY", "tk8.6") + + +# Configure Tcl/Tk paths BEFORE importing tkinter +_configure_tk_runtime() + import queue +import subprocess import threading import tkinter as tk -from tkinter import ttk, filedialog, messagebox -import subprocess -from pathlib import Path +from tkinter import filedialog, messagebox, ttk from typing import Any, Dict, Optional from main import YantaiVisionXSystem +from src.utils.path_utils import get_app_root, resolve_app_path from tools.roi_calibration_tool import ROICalibrationTool -from src.utils.path_utils import resolve_app_path, get_app_root class ControlPanelApp: