Files
T
Barış Keser 48bcd0ca8d GUI: thread yaşam döngüsü, iptal, profil dayanıklılığı ve gerçek hata raporu
closeEvent eklendi. Önceden pencere optimizasyon sırasında kapatılınca çalışan
QThread yok ediliyor ve Qt abort() çağırıyordu; --windowed derlemede konsol
olmadığı için kullanıcı sadece pencerenin kaybolduğunu görüyordu.

Start düğmesi çalışma boyunca devre dışı. Önceden ikinci tıklama self.worker'ı
yeniden atayıp çalışan thread'in son referansını düşürüyor, aynı abort()'a yol
açıyor ve iki thread aynı çıktıya yazıyordu.

İptal düğmesi eklendi: isInterruptionRequested, should_cancel üzerinden
optimize_vmd'ye geçiriliyor ve kanal döngülerinde kontrol ediliyor.

Profiller doğrulanıyor ve kalıcı. İçe alınan JSON hiç doğrulanmıyordu;
{"P": "aggressive"} gibi bir dosya slot içinde AttributeError'a, oradan
qFatal()'a düşüyordu. Ayrıca profiller yalnızca bellekteydi - "Profili Kaydet"
uygulama kapanınca kayboluyordu. Artık QStandardPaths.AppDataLocation altında
saklanıyor, içe alma yerleşik profilleri silmek yerine üzerine ekliyor ve
14 ayarın tamamı kaydediliyor (önceden yalnızca 4'ü).

Hata raporu: tür adı + traceback. Önceden yalnızca str(e) gönderiliyordu;
MemoryError'da hata kutusu bomboş çıkıyordu. sys.excepthook da eklendi.

Çıktı == girdi kontrolü, üzerine yazma onayı, eksik uzantı tamamlama,
sürükle-bırak, son klasörü hatırlama, %50'de donmayan ilerleme çubuğu ve
sınırlandırılmış log tamponu.
2026-08-03 10:12:31 +03:00

585 lines
24 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# VMD Motion Optimizer by Barış Keser (barkeser2002)
# License: GNU General Public License v3.0 (GPL-3.0)
# See LICENSE for details.
import os
import sys
import json
import traceback
from pathlib import Path
from typing import Optional
from PyQt6 import QtCore, QtWidgets, QtGui
from optimize_vmd import optimize_vmd, CancelledError
DEFAULT_PROFILES = {
"Quality": {"pos_eps": 0.02, "rot_eps_deg": 0.25, "morph_eps": 0.0005, "key_step": 1},
"Balanced": {"pos_eps": 0.05, "rot_eps_deg": 0.5, "morph_eps": 0.001, "key_step": 1},
"Aggressive": {"pos_eps": 0.1, "rot_eps_deg": 1.0, "morph_eps": 0.002, "key_step": 2},
}
# Profillerde saklanan tüm alanlar (v1.0.4 yalnızca ilk dördünü kaydediyordu,
# yani bir "profil" bir çalıştırmayı yeniden üretemiyordu).
PROFILE_FIELDS = (
"pos_eps", "rot_eps_deg", "morph_eps", "key_step",
"depth_check", "depth_smooth", "depth_scale",
"ground_check", "ground_target", "ground_smooth", "ground_scale",
"ground_all_bones", "ground_exclude_ik", "flatten_interp",
)
def _resource_path(name: str) -> Optional[str]:
# PyInstaller (MEIPASS) ve kaynak dizinlerini dene
candidates = []
base = os.path.dirname(os.path.abspath(__file__))
candidates.append(os.path.join(base, name))
candidates.append(os.path.join(os.path.dirname(base), name))
if getattr(sys, '_MEIPASS', None):
candidates.insert(0, os.path.join(sys._MEIPASS, name)) # type: ignore[attr-defined]
for p in candidates:
if os.path.exists(p):
return p
return None
def _read_version() -> str:
for rel in ("version.txt", os.path.join("..", "version.txt")):
p = _resource_path(rel) if not os.path.isabs(rel) else rel
if p and os.path.exists(p):
try:
with open(p, 'r', encoding='utf-8') as f:
return f.read().strip()
except Exception:
pass
return "dev"
def _profiles_file() -> str:
"""Profiller kullanıcı veri klasöründe saklanır.
exe'nin yanına yazmak Program Files altında izin hatası verirdi.
"""
base = QtCore.QStandardPaths.writableLocation(
QtCore.QStandardPaths.StandardLocation.AppDataLocation)
if not base:
base = str(Path.home() / ".vmd-optimizer")
os.makedirs(base, exist_ok=True)
return os.path.join(base, "profiles.json")
def _sanitize_profiles(raw) -> dict:
"""İçe alınan JSON'u doğrula. Bozuk girdiler slot içinde çökmeye yol açıyordu."""
if not isinstance(raw, dict):
raise ValueError("Profil dosyası bir JSON nesnesi olmalı")
clean = {}
for name, body in raw.items():
if not isinstance(name, str) or not isinstance(body, dict):
continue
entry = {}
for key, value in body.items():
if key not in PROFILE_FIELDS:
continue
if isinstance(value, bool):
entry[key] = value
elif isinstance(value, (int, float)):
entry[key] = value
if entry:
clean[name] = entry
if not clean:
raise ValueError("Dosyada geçerli profil bulunamadı")
return clean
class Worker(QtCore.QThread):
progress_signal = QtCore.pyqtSignal(str, int, int)
done_signal = QtCore.pyqtSignal(str)
error_signal = QtCore.pyqtSignal(str, str)
log_signal = QtCore.pyqtSignal(str)
cancelled_signal = QtCore.pyqtSignal()
def __init__(self, params: dict, parent=None):
super().__init__(parent)
self.params = params
def run(self):
try:
out = optimize_vmd(
progress=lambda s, i, t: self.progress_signal.emit(s, i, t),
log=lambda msg: self.log_signal.emit(str(msg)),
should_cancel=self.isInterruptionRequested,
**self.params,
)
self.done_signal.emit(out)
except CancelledError:
self.cancelled_signal.emit()
except Exception as e:
# v1.0.4 yalnızca str(e) gönderiyordu; MemoryError gibi hatalarda
# kullanıcı bomboş bir hata kutusu görüyordu.
summary = f"{type(e).__name__}: {e}" if str(e) else type(e).__name__
self.error_signal.emit(summary, traceback.format_exc())
class MainWindow(QtWidgets.QWidget):
def __init__(self):
super().__init__()
ver = _read_version()
self.setWindowTitle(f"VMD Motion Optimizer by Barış Keser (barkeser2002) v{ver}")
self.resize(720, 640)
self.setAcceptDrops(True)
self._last_dir = str(Path.home())
# Üstte logo
logo_path = _resource_path('logo.png')
self.logo_label = QtWidgets.QLabel()
if logo_path:
pix = QtGui.QPixmap(logo_path)
if not pix.isNull():
scaled = pix.scaledToWidth(640, QtCore.Qt.TransformationMode.SmoothTransformation)
self.logo_label.setPixmap(scaled)
self.logo_label.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
self.logo_label.setSizePolicy(QtWidgets.QSizePolicy.Policy.Ignored,
QtWidgets.QSizePolicy.Policy.Fixed)
# Inputs
self.input_edit = QtWidgets.QLineEdit()
self.in_btn = QtWidgets.QPushButton("VMD Seç...")
self.output_edit = QtWidgets.QLineEdit()
self.out_btn = QtWidgets.QPushButton("Çıktı...")
self.pos_eps = QtWidgets.QDoubleSpinBox(); self.pos_eps.setRange(0.0, 10.0); self.pos_eps.setValue(0.05); self.pos_eps.setSingleStep(0.01)
self.rot_eps = QtWidgets.QDoubleSpinBox(); self.rot_eps.setRange(0.0, 45.0); self.rot_eps.setValue(0.5); self.rot_eps.setSingleStep(0.1)
self.morph_eps = QtWidgets.QDoubleSpinBox(); self.morph_eps.setRange(0.0, 1.0); self.morph_eps.setDecimals(5); self.morph_eps.setValue(0.001); self.morph_eps.setSingleStep(0.0005)
self.key_step = QtWidgets.QSpinBox(); self.key_step.setRange(1, 10); self.key_step.setValue(1)
self.flatten_interp = QtWidgets.QCheckBox("İnterpolasyon eğrilerini düzleştir (MMD varsayılanı)")
# Depth alignment removal
self.depth_check = QtWidgets.QCheckBox("Depth (Z) sürüklenmesini kaldır")
self.depth_smooth = QtWidgets.QSpinBox(); self.depth_smooth.setRange(0, 200); self.depth_smooth.setValue(30)
self.depth_smooth.setToolTip("Eğilim penceresi. 2'den küçükse depth işlemi atlanır.")
self.depth_scale = QtWidgets.QDoubleSpinBox(); self.depth_scale.setRange(0.0, 10.0); self.depth_scale.setValue(1.0); self.depth_scale.setSingleStep(0.1)
# Ground stabilization
self.ground_check = QtWidgets.QCheckBox("Ground stabilization (Y)")
self.ground_target = QtWidgets.QDoubleSpinBox(); self.ground_target.setRange(-1000.0, 1000.0); self.ground_target.setValue(0.0)
self.ground_smooth = QtWidgets.QSpinBox(); self.ground_smooth.setRange(0, 200); self.ground_smooth.setValue(0)
self.ground_scale = QtWidgets.QDoubleSpinBox(); self.ground_scale.setRange(0.0, 10.0); self.ground_scale.setValue(1.0); self.ground_scale.setSingleStep(0.1)
self.ground_all_bones = QtWidgets.QCheckBox("Tüm kemikleri kullan (varsayılan: ayak)")
self.ground_exclude_ik = QtWidgets.QCheckBox("IK kemiklerini ölçümden hariç tut")
self.ground_exclude_ik.setChecked(True)
self.ground_exclude_ik.setToolTip(
"IK hariç bırakıldığında ölçülebilir Y verisi kalmazsa IK kemikleri "
"otomatik olarak geri alınır (log'a yazılır).")
# Profiles
self.profile_combo = QtWidgets.QComboBox()
self.save_profile_btn = QtWidgets.QPushButton("Profili Kaydet")
self.export_profiles_btn = QtWidgets.QPushButton("Dışa Aktar")
self.import_profiles_btn = QtWidgets.QPushButton("İçe Al")
self.start_btn = QtWidgets.QPushButton("Optimize Et")
self.cancel_btn = QtWidgets.QPushButton("İptal")
self.cancel_btn.setEnabled(False)
self.progress = QtWidgets.QProgressBar()
self.progress.setRange(0, 100)
self.progress.setFormat("%p% %v")
self.log = QtWidgets.QPlainTextEdit(); self.log.setReadOnly(True)
self.log.setMaximumBlockCount(5000)
# Credit etiketi
self.credit = QtWidgets.QLabel(
f"VMD Motion Optimizer by Barış Keser (barkeser2002) — GPL-3.0 — v{ver}")
self.credit.setStyleSheet("color: gray; font-size: 11px;")
form = QtWidgets.QFormLayout()
form.addRow("Girdi VMD:", self._with_btn(self.input_edit, self.in_btn))
form.addRow("Çıktı VMD:", self._with_btn(self.output_edit, self.out_btn))
form.addRow("Pozisyon eps:", self.pos_eps)
form.addRow("Rotasyon eps (deg):", self.rot_eps)
form.addRow("Morph eps:", self.morph_eps)
form.addRow("Key step:", self.key_step)
form.addRow(self.flatten_interp)
form.addRow(self.depth_check)
form.addRow("Depth eğilim penceresi:", self.depth_smooth)
form.addRow("Depth scale:", self.depth_scale)
form.addRow(self.ground_check)
form.addRow("Ground target Y:", self.ground_target)
form.addRow("Ground smooth window:", self.ground_smooth)
form.addRow("Ground scale:", self.ground_scale)
form.addRow(self.ground_all_bones)
form.addRow(self.ground_exclude_ik)
prof_row = QtWidgets.QHBoxLayout()
prof_row.addWidget(self.profile_combo, 1)
prof_row.addWidget(self.save_profile_btn)
prof_row.addWidget(self.export_profiles_btn)
prof_row.addWidget(self.import_profiles_btn)
prof_widget = QtWidgets.QWidget(); prof_widget.setLayout(prof_row)
form.addRow("Profil:", prof_widget)
btn_row = QtWidgets.QHBoxLayout()
btn_row.addWidget(self.start_btn, 3)
btn_row.addWidget(self.cancel_btn, 1)
v = QtWidgets.QVBoxLayout(self)
if logo_path:
v.addWidget(self.logo_label)
v.addLayout(form)
v.addLayout(btn_row)
v.addWidget(self.progress)
v.addWidget(self.log)
v.addWidget(self.credit)
# signals
self.in_btn.clicked.connect(self.select_input)
self.out_btn.clicked.connect(self.select_output)
self.start_btn.clicked.connect(self.start)
self.cancel_btn.clicked.connect(self.cancel)
self.save_profile_btn.clicked.connect(self.save_profile)
self.export_profiles_btn.clicked.connect(self.export_profiles)
self.import_profiles_btn.clicked.connect(self.import_profiles)
self._profiles = dict(DEFAULT_PROFILES)
self._load_profiles_from_disk()
self._refresh_profile_combo()
# Combo doldurulduktan SONRA bağla; aksi halde addItems sırasında
# apply_profile tetiklenir.
self.profile_combo.currentTextChanged.connect(self.apply_profile)
self.apply_profile(self.profile_combo.currentText())
self.worker: Optional[Worker] = None
# ---------- yardımcılar ----------
def _with_btn(self, widget, btn):
h = QtWidgets.QHBoxLayout(); w = QtWidgets.QWidget()
h.addWidget(widget); h.addWidget(btn); w.setLayout(h)
return w
def log_text(self, s: str):
self.log.appendPlainText(s)
def _warn(self, title: str, msg: str):
self.log_text(msg)
QtWidgets.QMessageBox.warning(self, title, msg)
def _is_running(self) -> bool:
return self.worker is not None and self.worker.isRunning()
# ---------- sürükle-bırak ----------
def dragEnterEvent(self, event: QtGui.QDragEnterEvent):
urls = event.mimeData().urls()
if urls and urls[0].toLocalFile().lower().endswith('.vmd'):
event.acceptProposedAction()
def dropEvent(self, event: QtGui.QDropEvent):
path = event.mimeData().urls()[0].toLocalFile()
self._set_input(path)
event.acceptProposedAction()
# ---------- dosya seçimi ----------
def _set_input(self, path: str):
self.input_edit.setText(path)
self._last_dir = os.path.dirname(path) or self._last_dir
if not self.output_edit.text():
self.output_edit.setText(os.path.splitext(path)[0] + "_optimized.vmd")
def select_input(self):
path, _ = QtWidgets.QFileDialog.getOpenFileName(
self, "VMD seç", self._last_dir, "VMD Files (*.vmd *.VMD)")
if path:
self._set_input(path)
def select_output(self):
dlg_start = self.output_edit.text() or self._last_dir
path, _ = QtWidgets.QFileDialog.getSaveFileName(
self, "Çıktı yolu", dlg_start, "VMD Files (*.vmd)")
if path:
if not os.path.splitext(path)[1]:
path += ".vmd"
self.output_edit.setText(path)
# ---------- profiller ----------
def _current_settings(self) -> dict:
return {
"pos_eps": self.pos_eps.value(),
"rot_eps_deg": self.rot_eps.value(),
"morph_eps": self.morph_eps.value(),
"key_step": self.key_step.value(),
"depth_check": self.depth_check.isChecked(),
"depth_smooth": self.depth_smooth.value(),
"depth_scale": self.depth_scale.value(),
"ground_check": self.ground_check.isChecked(),
"ground_target": self.ground_target.value(),
"ground_smooth": self.ground_smooth.value(),
"ground_scale": self.ground_scale.value(),
"ground_all_bones": self.ground_all_bones.isChecked(),
"ground_exclude_ik": self.ground_exclude_ik.isChecked(),
"flatten_interp": self.flatten_interp.isChecked(),
}
def _refresh_profile_combo(self, select: Optional[str] = None):
blocked = self.profile_combo.blockSignals(True)
current = select or self.profile_combo.currentText()
self.profile_combo.clear()
self.profile_combo.addItems(self._profiles.keys())
if current in self._profiles:
self.profile_combo.setCurrentText(current)
self.profile_combo.blockSignals(blocked)
def _load_profiles_from_disk(self):
path = _profiles_file()
if not os.path.exists(path):
return
try:
with open(path, 'r', encoding='utf-8') as f:
self._profiles.update(_sanitize_profiles(json.load(f)))
except Exception as e:
self.log_text(f"Kayıtlı profiller okunamadı ({type(e).__name__}: {e})")
def _save_profiles_to_disk(self):
try:
with open(_profiles_file(), 'w', encoding='utf-8') as f:
json.dump(self._profiles, f, ensure_ascii=False, indent=2)
except Exception as e:
self._warn("Profil", f"Profiller kaydedilemedi: {type(e).__name__}: {e}")
def apply_profile(self, name: str):
p = self._profiles.get(name)
if not isinstance(p, dict):
return
self.pos_eps.setValue(float(p.get("pos_eps", self.pos_eps.value())))
self.rot_eps.setValue(float(p.get("rot_eps_deg", self.rot_eps.value())))
self.morph_eps.setValue(float(p.get("morph_eps", self.morph_eps.value())))
self.key_step.setValue(int(p.get("key_step", self.key_step.value())))
self.depth_check.setChecked(bool(p.get("depth_check", self.depth_check.isChecked())))
self.depth_smooth.setValue(int(p.get("depth_smooth", self.depth_smooth.value())))
self.depth_scale.setValue(float(p.get("depth_scale", self.depth_scale.value())))
self.ground_check.setChecked(bool(p.get("ground_check", self.ground_check.isChecked())))
self.ground_target.setValue(float(p.get("ground_target", self.ground_target.value())))
self.ground_smooth.setValue(int(p.get("ground_smooth", self.ground_smooth.value())))
self.ground_scale.setValue(float(p.get("ground_scale", self.ground_scale.value())))
self.ground_all_bones.setChecked(bool(p.get("ground_all_bones", self.ground_all_bones.isChecked())))
self.ground_exclude_ik.setChecked(bool(p.get("ground_exclude_ik", self.ground_exclude_ik.isChecked())))
self.flatten_interp.setChecked(bool(p.get("flatten_interp", self.flatten_interp.isChecked())))
def save_profile(self):
name, ok = QtWidgets.QInputDialog.getText(self, "Profil adı", "Ad")
name = (name or '').strip()
if not ok or not name:
return
if name in self._profiles:
confirm = QtWidgets.QMessageBox.question(
self, "Profil", f"'{name}' profili zaten var. Üzerine yazılsın mı?")
if confirm != QtWidgets.QMessageBox.StandardButton.Yes:
return
self._profiles[name] = self._current_settings()
self._save_profiles_to_disk()
self._refresh_profile_combo(select=name)
self.log_text(f"Profil kaydedildi: {name}")
def export_profiles(self):
path, _ = QtWidgets.QFileDialog.getSaveFileName(
self, "Profilleri dışa aktar",
str(Path(self._last_dir) / "profiles.json"), "JSON (*.json)")
if not path:
return
try:
with open(path, 'w', encoding='utf-8') as f:
json.dump(self._profiles, f, ensure_ascii=False, indent=2)
self.log_text("Profiller kaydedildi: " + path)
except Exception as e:
self._warn("Profil", f"Dışa aktarılamadı: {type(e).__name__}: {e}")
def import_profiles(self):
path, _ = QtWidgets.QFileDialog.getOpenFileName(
self, "Profilleri içe al", self._last_dir, "JSON (*.json)")
if not path:
return
try:
with open(path, 'r', encoding='utf-8') as f:
incoming = _sanitize_profiles(json.load(f))
except Exception as e:
self._warn("Profil", f"İçe alınamadı: {type(e).__name__}: {e}")
return
# Yerleşik profilleri silme, üzerine ekle
self._profiles.update(incoming)
self._save_profiles_to_disk()
self._refresh_profile_combo()
self.log_text(f"{len(incoming)} profil yüklendi: {path}")
# ---------- çalıştırma ----------
def start(self):
if self._is_running():
return
inp = self.input_edit.text().strip()
outp = self.output_edit.text().strip()
if not inp or not os.path.isfile(inp):
self._warn("Girdi", "Geçerli bir VMD dosyası seçin.")
return
if not outp:
outp = os.path.splitext(inp)[0] + "_optimized.vmd"
self.output_edit.setText(outp)
if os.path.exists(outp) and os.path.realpath(inp) == os.path.realpath(outp):
self._warn("Çıktı",
"Çıktı yolu girdi ile aynı. Kaynak dosyanın üzerine yazılmaması "
"için farklı bir çıktı seçin.")
return
if os.path.exists(outp):
confirm = QtWidgets.QMessageBox.question(
self, "Çıktı", f"'{os.path.basename(outp)}' zaten var. Üzerine yazılsın mı?")
if confirm != QtWidgets.QMessageBox.StandardButton.Yes:
return
params = dict(
input_path=inp,
output_path=outp,
pos_eps=self.pos_eps.value(),
rot_eps_deg=self.rot_eps.value(),
morph_eps=self.morph_eps.value(),
key_step=self.key_step.value(),
preserve_end_keys=True,
flatten_interp=self.flatten_interp.isChecked(),
remove_depth=self.depth_check.isChecked(),
depth_smooth_window=self.depth_smooth.value(),
depth_scale=self.depth_scale.value(),
stabilize_ground_flag=self.ground_check.isChecked(),
ground_target_y=self.ground_target.value(),
ground_use_feet_only=not self.ground_all_bones.isChecked(),
ground_smooth_window=self.ground_smooth.value(),
ground_scale=self.ground_scale.value(),
ground_exclude_ik=self.ground_exclude_ik.isChecked(),
)
self.progress.setValue(0)
self.log_text("Başladı...")
self._set_running(True)
self.worker = Worker(params, parent=self)
self.worker.progress_signal.connect(self.on_progress)
self.worker.done_signal.connect(self.on_done)
self.worker.error_signal.connect(self.on_error)
self.worker.log_signal.connect(self.on_log)
self.worker.cancelled_signal.connect(self.on_cancelled)
self.worker.start()
def cancel(self):
if self._is_running():
self.log_text("İptal isteniyor...")
self.cancel_btn.setEnabled(False)
self.worker.requestInterruption()
def _set_running(self, running: bool):
self.start_btn.setEnabled(not running)
self.cancel_btn.setEnabled(running)
for w in (self.in_btn, self.out_btn, self.input_edit, self.output_edit,
self.save_profile_btn, self.import_profiles_btn, self.profile_combo):
w.setEnabled(not running)
def _finish(self):
self._set_running(False)
if self.worker is not None:
self.worker.deleteLater()
self.worker = None
@QtCore.pyqtSlot(str)
def on_log(self, s: str):
self.log_text(s)
@QtCore.pyqtSlot(str, int, int)
def on_progress(self, section: str, i: int, total: int):
# Bones %0-70, Morphs %70-100
frac = i / max(total, 1)
pct = int(frac * 70) if section == 'Bones' else 70 + int(frac * 30)
self.progress.setValue(max(0, min(100, pct)))
self.progress.setFormat(f"{section} %p%")
@QtCore.pyqtSlot(str)
def on_done(self, out: str):
self.progress.setValue(100)
self.progress.setFormat("Tamamlandı %p%")
self.log_text("Bitti. Kaydedildi: " + out)
self._finish()
@QtCore.pyqtSlot()
def on_cancelled(self):
self.progress.setValue(0)
self.progress.setFormat("İptal edildi")
self.log_text("İşlem iptal edildi; çıktı dosyası yazılmadı.")
self._finish()
@QtCore.pyqtSlot(str, str)
def on_error(self, summary: str, detail: str):
self.progress.setValue(0)
self.progress.setFormat("Hata")
self.log_text("Hata: " + summary)
box = QtWidgets.QMessageBox(self)
box.setIcon(QtWidgets.QMessageBox.Icon.Critical)
box.setWindowTitle("Hata")
box.setText(summary)
box.setDetailedText(detail)
box.exec()
self._finish()
# ---------- kapanış ----------
def closeEvent(self, event: QtGui.QCloseEvent):
"""Çalışan bir QThread yok edilirse Qt abort() çağırır (sessiz çökme)."""
if not self._is_running():
event.accept()
return
confirm = QtWidgets.QMessageBox.question(
self, "Çıkış", "Optimizasyon sürüyor. İptal edilip çıkılsın mı?")
if confirm != QtWidgets.QMessageBox.StandardButton.Yes:
event.ignore()
return
self.worker.requestInterruption()
if not self.worker.wait(5000):
self.worker.terminate()
self.worker.wait(2000)
event.accept()
def _install_excepthook():
"""--windowed derlemede konsol yoktur; yakalanmayan hata sessizce öldürür."""
def hook(exc_type, exc, tb):
text = ''.join(traceback.format_exception(exc_type, exc, tb))
try:
sys.stderr.write(text)
except Exception:
pass
try:
box = QtWidgets.QMessageBox()
box.setIcon(QtWidgets.QMessageBox.Icon.Critical)
box.setWindowTitle("Beklenmeyen hata")
box.setText(f"{exc_type.__name__}: {exc}")
box.setDetailedText(text)
box.exec()
except Exception:
pass
sys.excepthook = hook
def main():
app = QtWidgets.QApplication(sys.argv)
app.setApplicationName("VMDOptimizer")
app.setOrganizationName("barkeser2002")
_install_excepthook()
# Uygulama ikonu: önce icon.ico, yoksa logo.png
icon_path = _resource_path('icon.ico') or _resource_path('logo.png')
if icon_path:
app.setWindowIcon(QtGui.QIcon(icon_path))
w = MainWindow()
if icon_path:
w.setWindowIcon(QtGui.QIcon(icon_path))
w.show()
sys.exit(app.exec())
if __name__ == '__main__':
main()