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>
This commit is contained in:
sladro 2025-12-02 11:00:33 +08:00
parent 87c8f8a8aa
commit 204678026e
3 changed files with 177 additions and 4 deletions

47
ControlPanel.spec Normal file
View File

@ -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',
)

47
ControlPanel_debug.spec Normal file
View File

@ -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',
)

View File

@ -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: