Is das jetzt eigentlich sowas wie StrataFx?
Experimente mit Shader-Programmmierung
-
-
Ich bin genauso wie Julius aufgestellt. Dieses ganzen Tools - und hier noch 'ne dll und da noch einen mod habe ich mit dem P3D ebenfalls hinter mir gelassen. Matthias Orgie ist ein super-negatives Beispiel; dxgi.dll, da stehen mir jetzt noch die Haare hoch, wenn ich das lese. Das waren da fast alles Verschlimmbesserungen und bei crashs etc. hat man sich dumm und dämlich gesucht; diese Schmankerl kommen mir nicht mehr durch die Hintertür wieder rein. Meine Meinung, ich will hier niemanden auf die Krawatte treten
, aber ich habe so langsam das Gefühl, das einige diese Ära des Bastelns etwas vermissen
!mfg Kai
Moin zusammen! Ich muss jetzt doch mal meinen Senf dazu geben.
„Läuft absolut stabil und ohne CTDs!“ (Oder: Wie aus einem Flugsimulator ein kleines Kernkraftwerk wird)Früher beim P3D war das Ganze ja noch ehrliche Handwerkskunst. Da musste man erst mal drei Tage lang in den ini-Dateien beten, die Registry verbrennen und den PC mit Weihrauch einräuchern, damit die Kiste überhaupt lief. Wisst Ihr noch? "Mit welchem Wetter fliegst Du?"
Aber mal ehrlich: Ich mag die Bastelei. Es ist ein Hobby. Wer will schon einen Simulator, der einfach funktioniert? Wo bleibt denn da der Nervenkitzel, wenn man nach einem 10-Stunden-Flug kurz vor dem Touchdown durch einen mysteriösen CTD beim P3D ins Nirwana befördert wurde? Das war Character Building! Aber, das habe ich wie gesagt im MSFS nicht.Nun gut, der MSFS 2024 läuft von Haus aus ja eigentlich schon verdammt gut. Mit 'nem Fenix, der Fokker, PMDG oder der MD11 habe ich null Probleme, keine CTDs, alles schick.
Aber der Sim ist halt nackt. Und ein nackter Sim ist wie ein Flugzeug ohne Flügel. Unbefriedigend.Also fangen wir mal an zu stricken:
- Erst mal die Grundversorgung: Simbrief & Navigraph (ist ja quasi Grundnahrungsmittel).
- Damit die Festplatte nicht explodiert, den MSFS Addons Linker.
- Für das echte Feeling natürlich GSX. Und weil das noch nicht reicht, baut darauf „Cockpit to Ground“ auf.
- Klar, der Airbus muss reden („V1 – Gear Up!“ etc.). Also her mit FSUIPC und ein bisschen LUA-Magie.
- Je nach Flieger und Hardware rattern im Hintergrund gemütlich SimAppPro oder MobiFlight.
- Weil ich gerne sehen will, wo ich lande, läuft VRAAS.
- Dann noch MSFS_AutoFPS weils einfach besser aussieht und smooth lauft
- Damit die Streamdecks wissen, was sie tun sollen, schnalle ich noch Axis&Ohs an die Kiste.
- GlassOut lagert mir die Instrumente aus. Man will ja schließlich was sehen.
- Ganz alleine im Sim ist doof, also Traffic2U (oder alternativ den Traffic Injector) anwerfen garniert mit den Fliegern von IVAO (man nimmt, was man kriegen kann) und damit die Jungs am Boden nicht wie ferngesteuerte Zombies wirken, noch Apron Life dazu.
- Ach ja, wenn ich die PMDG 737 ausführe, brummt natürlich auch das Tool „CFY-TQ console“ für den Hardware-Throttle-Quadrant.
- Jüngstes Mitglied in der Familie: Reshade, weil Tom da gezaubert hat und das jetzt verdammt sexy aussieht.
- Volanta läuft standardmäßig sowieso mit, und oft auch VATSIM.
Was soll ich sagen? Eigentlich kenne ich keine CTDs und der Simulator läuft absolut stabil! (schnell dreimal auf Holz klopfen, bevor der PC das hier liest und abstürzt). Und wenn dann doch mal was zickt, macht der Addons Linker die Fehlersuche zum Kinderspiel, während der GSX Profile Manager im Hintergrund heimlich meine Profile aktuell hält.
Um nicht alles haendisch zu starten habe ich mir in Python ein tool mit Gui gebaut (oder bin dabei es zu bauen.
image.png
Das sieht dann so aus:ZitatAlles anzeigen# -*- coding: utf-8 -*-
"""
Auswahlmenü auf explizitem Monitor via screeninfo.
- Fenster rahmenlos (overrideredirect) und exakt auf Monitor-Koordinaten
- Hintergrund weich eingeblendet (Pillow)
- WAV-Sound asynchron via winsound
- Titel erscheint, pausiert kurz, fährt nach oben
- Menü als zentrierter Block: linke Spalte Nummern, rechte Spalte Text
- Hover-Effekt markiert komplette Zeile (Nummer + Text) + Hand-Cursor
- Auswahl via Maus oder Zifferntaste (1–5)
- Nach Auswahl: rotes "Boarding" unten blinkend -> Fenster schließen -> Zielskript starten
- ESC beendet
Bereinigt:
- Prozessstart ohne Daemon-Thread, mit optionaler Konsole (LAUNCH_WITH_CONSOLE)
- pythonw.exe -> python.exe Fallback
- Reihenfolge der Labels an SCRIPTS angepasst
"""
import sys
import os
import threading
import subprocess
import tkinter as tk
import tkinter.font as tkfont
from pathlib import Path
from screeninfo import get_monitors
# ---------- Konfiguration ----------
BG_IMAGE = r"C:\Users\Torst\Scripts\Pictures\star.jpg"
SOUND_FILE = r"C:\Users\Torst\Scripts\Sounds\Chime.wav"
# Zielskripte je Auswahl (1..5)
SCRIPTS = [
r"C:\Users\Torst\Scripts\1_Boeing.py",
r"C:\Users\Torst\Scripts\2_Boeing_online.py",
r"C:\Users\Torst\Scripts\3_Airbus.py",
r"C:\Users\Torst\Scripts\4_Airbus_online.py",
r"C:\Users\Torst\Scripts\5_Test.py",
]
# Labels passend zur Reihenfolge oben
MENU_NUMBERS = ["1.", "2.", "3.", "4.", "5."]
MENU_LABELS = ["737", "737-online", "Airbus", "Airbus-online", "Test"]
MONITOR_INDEX = 0 # 0 = erster Monitor in get_monitors(); bei Bedarf anpassen
# Fade-In des Hintergrunds
FADE_IN_DURATION_MS = 300
FADE_IN_INTERVAL_MS = 33 # ~30 FPS
TITLE_TEXT = "Where do you want to go today?"
TITLE_FONT = ("Arial", 64, "bold")
NUM_FONT = ("Arial", 36, "bold") # Nummern leicht fett für klare Spalte
TEXT_FONT = ("Arial", 36)
TITLE_TARGET_Y = 100
TITLE_MOVE_PIXELS_PER_TICK = 20
# Farben
COLOR_TITLE = "white"
COLOR_MENU_NORMAL = "white"
COLOR_MENU_HOVER = "#00E5FF" # Cyan-Highlight
COLOR_BOARDING = "#FF2D2D" # Rot fürs Boarding
BACKGROUND_COLOR = "black"
# Layout
COLUMN_GAP = 24
LINE_SPACING = 60
# Boarding-Blinken
BOARDING_TEXT = "Boarding"
BOARDING_MARGIN_BOTTOM = 240
BOARDING_BLINK_COUNT = 4
BOARDING_ON_MS = 150
BOARDING_OFF_MS = 650
# Prozessstart-Option: Konsole zeigen?
LAUNCH_WITH_CONSOLE = True # True: neue Konsole; False: ohne neues Fenster (detached)
# ---------- WAV-Sound über winsound ----------
def play_wav_async(path_str: str

if not path_str:
return
p = Path(path_str)
if not p.exists():
print(f"[Audio] Datei nicht gefunden: {p}", file=sys.stderr)
return
def _worker():
try:
import winsound
winsound.PlaySound(str(p), winsound.SND_FILENAME | winsound.SND_ASYNC)
except Exception as e:
print(f"[Audio] Fehler beim Abspielen: {e}", file=sys.stderr)
threading.Thread(target=_worker, daemon=True).start()
class Auswahlmenu(tk.Tk

def __init__(self, bg_image_path: str, wav_path: str, monitor_index: int = 0

super().__init__()
self.title("Auswahlmenu")
self.configure(bg=BACKGROUND_COLOR)
# --- Zielmonitor finden ---
monitors = get_monitors()
if not monitors:
raise RuntimeError("Keine Monitore gefunden.")
if monitor_index < 0 or monitor_index >= len(monitors

raise IndexError(f"Monitor-Index {monitor_index} ist ungültig. Gefunden: {len(monitors)}")
self.monitor = monitors[monitor_index]
print("Monitore:", *[f"{i}: {m}" for i, m in enumerate(monitors)], sep="\n")
self.MON_W = int(self.monitor.width)
self.MON_H = int(self.monitor.height)
self.MON_X = int(self.monitor.x)
self.MON_Y = int(self.monitor.y)
# --- Fenster exakt auf Zielmonitor setzen (ohne -fullscreen) ---
self.overrideredirect(True) # rahmenlos
self.geometry(f"{self.MON_W}x{self.MON_H}+{self.MON_X}+{self.MON_Y}")
self.lift()
self.attributes("-topmost", True)
self.after(200, lambda: self.attributes("-topmost", False))
# ESC beendet
self.bind("<Escape>", lambda e: self.destroy())
# Zifferntasten 1–5 binden (oben)
for i in range(1, 6

self.bind(str(i), lambda e, idx=i-1: self._on_select(idx))
# Ziffernblock (NumPad)
for i in range(1, 6

self.bind(f"<KP_{i}>", lambda e, idx=i-1: self._on_select(idx))
# Canvas
self.canvas = tk.Canvas(self, width=self.MON_W, height=self.MON_H,
bg=BACKGROUND_COLOR, highlightthickness=0, bd=0)
self.canvas.pack(fill="both", expand=True)
# Bild laden & vorbereiten
try:
from PIL import Image, ImageTk
except ImportError:
from tkinter import messagebox
messagebox.showerror(
"Fehlendes Paket",
"Pillow ist erforderlich (pip install pillow), um JPEGs zu laden und zu skalieren."
)
self.destroy()
return
bg_path = Path(bg_image_path)
if not bg_path.exists():
from tkinter import messagebox
messagebox.showerror("Datei fehlt", f"Hintergrund nicht gefunden:\n{bg_path}")
self.destroy()
return
self._PIL_Image = Image
self._ImageTk = ImageTk
# Auf Monitor-Auflösung skalieren (Cover)
self.bg_img = self._PIL_Image.open(bg_path).convert("RGB").resize(
(self.MON_W, self.MON_H), self._PIL_Image.Resampling.LANCZOS
)
self.black_img = self._PIL_Image.new("RGB", (self.MON_W, self.MON_H), "black")
# Canvas-Bild-Handle & Referenzen
self.bg_item = None
self.bg_photo = None
self._photo_refs = [] # gegen Garbage Collection
# Titel/Menu-Status
self.title_text = TITLE_TEXT
self.title_y = self.MON_H // 2
self.target_title_y = TITLE_TARGET_Y
self.title_item = None
# Fonts (für Breitenmessung)
self.tk_num_font = tkfont.Font(family=NUM_FONT[0], size=NUM_FONT[1], weight="bold")
self.tk_text_font = tkfont.Font(family=TEXT_FONT[0], size=TEXT_FONT[1])
# Menüstruktur: je Zeile speichern wir (num_item_id, text_item_id)
self.menu_rows = [] # Liste von Dicts: {"num": id, "text": id}
self._hover_row_index = None
# Auswahlstatus (um Doppel-Trigger zu verhindern)
self._selection_in_progress = False
# Fade-In Parameter
self.fade_in_steps = max(1, FADE_IN_DURATION_MS // FADE_IN_INTERVAL_MS)
self._fade_in_step_idx = 0
# Boarding-Blinken
self._boarding_item = None
self._boarding_blinks_done = 0
# Audio starten
if wav_path:
play_wav_async(wav_path)
# Animation starten
self.after(0, self._fade_in_step)
# ---------- Fade-In & Titel ----------
def _fade_in_step(self

t = min(1.0, self._fade_in_step_idx / float(self.fade_in_steps))
blended = self._PIL_Image.blend(self.black_img, self.bg_img, t)
self.bg_photo = self._ImageTk.PhotoImage(blended)
self._photo_refs.append(self.bg_photo) # Referenz halten
if self.bg_item is None:
self.bg_item = self.canvas.create_image(0, 0, image=self.bg_photo, anchor="nw")
else:
self.canvas.itemconfig(self.bg_item, image=self.bg_photo)
if self._fade_in_step_idx < self.fade_in_steps:
self._fade_in_step_idx += 1
self.after(FADE_IN_INTERVAL_MS, self._fade_in_step)
else:
self._show_title()
def _show_title(self

if self.title_item is None:
self.title_item = self.canvas.create_text(
self.MON_W // 2,
self.title_y,
text=self.title_text,
fill=COLOR_TITLE,
font=TITLE_FONT,
anchor="center",
)
# kurze Pause (wie pygame.time.delay(500))
self.after(500, self._move_title_up)
def _move_title_up(self

if self.title_y > self.target_title_y:
self.title_y -= TITLE_MOVE_PIXELS_PER_TICK
self.canvas.coords(self.title_item, self.MON_W // 2, self.title_y)
self.after(16, self._move_title_up) # ~60 FPS
else:
self._show_menu()
# ---------- Menü-Rendering (zweispaltig, Block zentriert) ----------
def _show_menu(self

# 1) Spaltenbreiten bestimmen
num_col_width = max(self.tk_num_font.measure(s) for s in MENU_NUMBERS)
text_col_width = max(self.tk_text_font.measure(s) for s in MENU_LABELS)
block_width = num_col_width + COLUMN_GAP + text_col_width
# 2) Linke Kante des Blocks so setzen, dass Gesamtblock zentriert ist
block_left_x = (self.MON_W - block_width) // 2
num_col_x = block_left_x + num_col_width # Nummern rechtsbündig am Ende der Num-Spalte
text_col_x = block_left_x + num_col_width + COLUMN_GAP # Text links daneben (links-bündig)
# 3) Vertikal zentrieren
block_height = len(MENU_NUMBERS) * LINE_SPACING
start_y = (self.MON_H // 2) - (block_height // 2)
# 4) Zeilen erzeugen (Nummern: rechtsbündig, Text: linksbündig)
for i, (num_str, txt_str) in enumerate(zip(MENU_NUMBERS, MENU_LABELS)):
y = start_y + i * LINE_SPACING
num_id = self.canvas.create_text(
num_col_x, y,
text=num_str,
fill=COLOR_MENU_NORMAL,
font=NUM_FONT,
anchor="e", # rechtsbündig an num_col_x
tags=(f"menu_row_{i}", "menu_num", "menu_item")
)
txt_id = self.canvas.create_text(
text_col_x, y,
text=txt_str,
fill=COLOR_MENU_NORMAL,
font=TEXT_FONT,
anchor="w", # linksbündig an text_col_x
tags=(f"menu_row_{i}", "menu_text", "menu_item")
)
self.menu_rows.append({"num": num_id, "text": txt_id})
# Hover / Cursor
self.canvas.tag_bind("menu_item", "<Enter>", self._on_item_enter)
self.canvas.tag_bind("menu_item", "<Leave>", self._on_item_leave)
# Klick -> Auswahl
self.canvas.tag_bind("menu_item", "<Button-1>", self._on_item_click)
# ---------- Hover-Logik (ganze Zeile) ----------
def _on_item_enter(self, event

tags = self.canvas.gettags("current")
row_idx = None
for t in tags:
if t.startswith("menu_row_"

try:
row_idx = int(t.split("_")[-1])
except ValueError:
pass
if row_idx is None:
return
if self._hover_row_index is not None and self._hover_row_index != row_idx:
self._set_row_color(self._hover_row_index, COLOR_MENU_NORMAL)
self._set_row_color(row_idx, COLOR_MENU_HOVER)
self._hover_row_index = row_idx
self.canvas.config(cursor="hand2")
def _on_item_leave(self, event

if self._selection_in_progress:
return # während Boarding-Blinken nicht zurückfärben
# Prüfen, ob Cursor noch auf einem Menü-Item ist:
x = self.canvas.winfo_pointerx() - self.canvas.winfo_rootx()
y = self.canvas.winfo_pointery() - self.canvas.winfo_rooty()
items = self.canvas.find_overlapping(x, y, x, y)
still_on_item = any("menu_item" in self.canvas.gettags(i) for i in items)
if not still_on_item:
if self._hover_row_index is not None:
self._set_row_color(self._hover_row_index, COLOR_MENU_NORMAL)
self._hover_row_index = None
self.canvas.config(cursor="arrow")
def _set_row_color(self, row_idx: int, color: str

row = self.menu_rows[row_idx]
try:
self.canvas.itemconfig(row["num"], fill=color)
self.canvas.itemconfig(row["text"], fill=color)
except Exception:
pass
# ---------- Auswahl per Klick/Ziffer ----------
def _on_item_click(self, event

if self._selection_in_progress:
return
tags = self.canvas.gettags("current")
row_idx = None
for t in tags:
if t.startswith("menu_row_"

try:
row_idx = int(t.split("_")[-1])
except ValueError:
pass
if row_idx is None:
return
self._on_select(row_idx)
def _on_select(self, row_idx: int

if self._selection_in_progress:
return
self._selection_in_progress = True
# Visuelle Markierung des ausgewählten Eintrags (optional)
try:
self._set_row_color(row_idx, COLOR_MENU_HOVER)
except Exception:
pass
# Rotes "Boarding" unten blinken, dann schließen & starten
self._blink_boarding_then_close_and_launch(row_idx)
# ---------- Boarding-Blinken & danach starten ----------
def _blink_boarding_then_close_and_launch(self, row_idx: int

# Boarding-Text unten mittig
x = self.MON_W // 2
y = self.MON_H - BOARDING_MARGIN_BOTTOM
if self._boarding_item is None:
self._boarding_item = self.canvas.create_text(
x, y,
text=BOARDING_TEXT,
fill=COLOR_BOARDING,
font=("Arial", 88, "bold"),
anchor="center",
state="hidden", # beginnt unsichtbar
)
else:
self.canvas.coords(self._boarding_item, x, y)
self.canvas.itemconfig(self._boarding_item, text=BOARDING_TEXT, fill=COLOR_BOARDING, state="hidden")
self._boarding_blinks_done = 0
def do_on():
self.canvas.itemconfig(self._boarding_item, state="normal")
self.after(BOARDING_ON_MS, do_off)
def do_off():
self.canvas.itemconfig(self._boarding_item, state="hidden")
self._boarding_blinks_done += 1
if self._boarding_blinks_done < BOARDING_BLINK_COUNT:
self.after(BOARDING_OFF_MS, do_on)
else:
# Nach letztem OFF -> starten & Fenster schließen
self._finalize_and_launch(row_idx)
# Start des Blinkens
do_on()
def _finalize_and_launch(self, row_idx: int

"""Startet das Zielskript robust (ohne Daemon-Thread) und schließt dann das UI."""
from tkinter import messagebox
try:
if not (0 <= row_idx < len(SCRIPTS)):
print(f"[Aktion] Ungültiger Index {row_idx}", file=sys.stderr)
return
script_path = Path(SCRIPTS[row_idx])
if not script_path.exists():
messagebox.showerror("Datei fehlt", f"Script nicht gefunden:\n{script_path}")
return
# sys.executable prüfen: pythonw.exe -> python.exe (sichtbar)
exe = sys.executable
if exe and exe.lower().endswith("pythonw.exe"

alt = exe[:-1] # ...\pythonw.exe -> ...\python.exe
if os.path.exists(alt

exe = alt
# Optional: py-Launcher verwenden, wenn gewünscht/erforderlich
# (auskommentiert; nur nutzen, wenn 'py' sicher im PATH liegt)
# exe, args = "py", ["-3", str(script_path)]
# Andernfalls:
args = [exe, str(script_path)]
# Windows-creationflags
creationflags = 0
if os.name == "nt":
if LAUNCH_WITH_CONSOLE:
CREATE_NEW_CONSOLE = 0x00000010
creationflags |= CREATE_NEW_CONSOLE
else:
DETACHED_PROCESS = 0x00000008
CREATE_NEW_PROCESS_GROUP = 0x00000200
creationflags |= (DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP)
print(f"[DEBUG] sys.executable: {sys.executable}")
print(f"[DEBUG] Start: {args}")
print(f"[DEBUG] CWD: {script_path.parent}")
subprocess.Popen(
args,
cwd=str(script_path.parent),
shell=False,
creationflags=creationflags,
close_fds=True
)
except Exception as e:
try:
messagebox.showerror("Startfehler", f"{e}")
except Exception:
print(f"[Aktion] Startfehler: {e}", file=sys.stderr)
finally:
# UI schließen
try:
self.destroy()
except Exception:
pass
if __name__ == "__main__":
app = Auswahlmenu(BG_IMAGE, SOUND_FILE, monitor_index=MONITOR_INDEX)
app.mainloop()
Und fuer die 737 startet dan z.B. folgendes:
ZitatAlles anzeigenimport tkinter as tk
from PIL import Image, ImageTk
from screeninfo import get_monitors
import os
import time
import subprocess
import ctypes # Für automatisches CMD-Fenster schließen
# === Einstellungen ===
IMAGE_PATH = r"C:\Users\Torst\Scripts\Pictures\star.jpg"
# Programme und individuelle Zeiten
# -> Passe label/exe/duration_ms hier an (Reihenfolge = Anzeige/Reihenfolge)
STEPS = [
{
"label": "Starting FSUIPC",
"exe": r"F:\MSFS2024_Tools\FSUIPC v7.5.6\FSUIPC7.exe",
"duration_ms": 3000, # z. B. 3 Sekunden
},
{
"label": "Starting vRAAS",
"exe": r"C:\Users\Torst\AppData\Local\LAND3.vRAAS\vRAAS.exe",
"duration_ms": 5000, # z. B. 5 Sekunden
},
{
"label": "Starting CFY TQ Console",
"exe": r"F:\MSFS2024_Tools\CFY v2.5.05\CFY_TQ Console.exe",
"duration_ms": 11000, # z. B. 11 Sekunden
},
{
"label": "Starting SimAppPro",
"exe": r"C:\Users\Torst\AppData\Local\Programs\SimAppPro\SimAppPro.exe",
"duration_ms": 8000, # z. B. 8 Sekunden
},
{
"label": "Starting GlassOut",
"exe": r"C:\Users\Torst\AppData\Local\Programs\glassout\GlassOut.exe",
"duration_ms": 8000, # z. B. 8 Sekunden
},
{
"label": "Starting Axis & Ohs",
"exe": r"C:\Program Files\LorbyAxisAndOhs_MSFS24\LorbyAxisAndOhs_MSFS2024.exe",
"duration_ms": 8000, # z. B. 8 Sekunden
},
{
"label": "Starting Streamdeck",
"exe": r"C:\Program Files\Elgato\StreamDeck\StreamDeck.exe",
"duration_ms": 8000, # z. B. 8 Sekunden
},
{
"label": "Starting MSFS Addons Linker",
"exe": r"F:\MSFS2024_Tools\MSFS_Addons_Linker\MSFS_AddonsLinker_2024.exe",
"duration_ms": 4000,
},
]
AUTO_CLOSE_SECONDS = 8 # Minimal sichtbare Gesamtzeit
MONITOR_INDEX = 0 # 0 = Monitor 1
FILL_SCREEN = False # False: contain, True: cover
FADE_DURATION_MS = 100
FADE_INTERVAL_MS = 40
# Footer/Progressbar
PROGRESS_INTERVAL_MS = 50
BAR_WIDTH = 900
BAR_HEIGHT = 16
BAR_OUTLINE = "white"
BAR_FILL = "white"
BAR_BG = "#333333"
FOOTER_MARGIN_BOTTOM = 40
# Texte
TEXT_HAVE_FUN = "Have fun"
HAVE_FUN_MS = 3000
# Schrift
TEXT_FONT = ("Segoe UI", 48, "bold")
TEXT_SUB_FONT = ("Segoe UI", 32, "bold")
TEXT_ERR_FONT = ("Segoe UI", 24, "bold")
TEXT_FILL = "white"
TEXT_ERROR_FILL = "red"
def resize_to_fit(img, target_w, target_h, fill=False

src_w, src_h = img.size
scale_w = target_w / src_w
scale_h = target_h / src_h
scale = max(scale_w, scale_h) if fill else min(scale_w, scale_h)
new_w = max(1, int(src_w * scale))
new_h = max(1, int(src_h * scale))
resized = img.resize((new_w, new_h), Image.Resampling.LANCZOS)
if fill:
x0 = max(0, (new_w - target_w) // 2)
y0 = max(0, (new_h - target_h) // 2)
x1 = x0 + target_w
y1 = y0 + target_h
resized = resized.crop((x0, y0, x1, y1))
return resized
def start_program(exe_path

"""Startet ein externes Programm. Gibt (ok, proc_or_err) zurück."""
try:
if not exe_path or not os.path.exists(exe_path

return False, f"Bestand niet gevonden: {exe_path}"
cwd = os.path.dirname(exe_path) or None
proc = subprocess.Popen([exe_path], cwd=cwd, shell=False)
return True, proc
except Exception as e:
return False, str(e)
def main():
if not os.path.exists(IMAGE_PATH

raise FileNotFoundError(f"Afbeelding niet gevonden: {IMAGE_PATH}")
monitors = get_monitors()
if not monitors:
raise RuntimeError("Geen monitoren gedetecteerd.")
if MONITOR_INDEX >= len(monitors

raise IndexError(f"Monitor-index {MONITOR_INDEX} bestaat niet. Aantal monitoren: {len(monitors)}")
target = monitors[MONITOR_INDEX]
root = tk.Tk()
root.title(f"Afbeelding (monitor {MONITOR_INDEX+1})")
root.configure(bg="black")
root.overrideredirect(True)
root.geometry(f"{target.width}x{target.height}+{target.x}+{target.y}")
root.lift()
root.attributes("-topmost", True)
root.after(100, lambda: root.attributes("-topmost", False))
canvas = tk.Canvas(root, width=target.width, height=target.height, highlightthickness=0, bg="black")
canvas.pack(fill="both", expand=True)
timeline_start = time.perf_counter()
img = Image.open(IMAGE_PATH).convert("RGB")
scaled = resize_to_fit(img, target.width, target.height, fill=FILL_SCREEN)
black_base = Image.new("RGB", scaled.size, color="black")
center_x = target.width // 2
center_y = target.height // 2
footer_bar_y = max(0, target.height - FOOTER_MARGIN_BOTTOM - BAR_HEIGHT)
photo_frames = []
first_frame = Image.blend(black_base, scaled, alpha=0.0)
first_photo = ImageTk.PhotoImage(first_frame)
photo_frames.append(first_photo)
image_id = canvas.create_image(center_x, center_y, image=first_photo)
steps_fade = max(1, FADE_DURATION_MS // FADE_INTERVAL_MS)
line_gap = 64
# Die erste Statuszeile erscheint mittig, Folgelinien darunter
# Wir zeichnen die fertigen Labels später dynamisch (pro Schritt),
# behalten aber fixe Y-Positionen je Index (entsprechend STEPS).
y_positions = [center_y + i * line_gap for i in range(len(STEPS))]
# Wir merken uns Text-IDs, um Fehlerhinweise korrekt darunter zu platzieren
text_items = [None] * len(STEPS)
# Fortschrittsbalken-Elemente (werden pro Schritt überschrieben)
last_progress_items = {"ids": []}
def do_fade(step=0

if step >= steps_fade:
run_step(0) # direkt mit Schritt 0 starten
return
alpha = step / float(steps_fade)
frame = Image.blend(black_base, scaled, alpha=alpha)
photo = ImageTk.PhotoImage(frame)
photo_frames.append(photo)
canvas.itemconfig(image_id, image=photo)
root.after(FADE_INTERVAL_MS, lambda: do_fade(step + 1))
def draw_progress_bar_at_bottom(duration_ms, callback=None

# Vorherige Fortschrittsbalken löschen
for _id in last_progress_items["ids"]:
try:
canvas.delete(_id)
except Exception:
pass
last_progress_items["ids"].clear()
x0 = center_x - BAR_WIDTH // 2
y0 = footer_bar_y
x1 = x0 + BAR_WIDTH
y1 = y0 + BAR_HEIGHT
bg_rect = canvas.create_rectangle(x0, y0, x1, y1, fill=BAR_BG, outline=BAR_OUTLINE, width=2)
fill_rect = canvas.create_rectangle(x0, y0, x0, y1, fill=BAR_FILL, outline="", width=0)
last_progress_items["ids"].extend([bg_rect, fill_rect])
steps_local = max(1, duration_ms // PROGRESS_INTERVAL_MS)
def animate(step=0

progress = min(1.0, step / float(steps_local))
new_w = int(BAR_WIDTH * progress)
canvas.coords(fill_rect, x0, y0, x0 + new_w, y1)
if step < steps_local:
root.after(PROGRESS_INTERVAL_MS, lambda: animate(step + 1))
else:
if callback:
callback()
animate(0)
def run_step(i: int

"""Zeigt den i-ten Schritt an, startet das Programm, zeichnet Progress und ruft danach den nächsten Schritt auf."""
if i >= len(STEPS

show_have_fun_and_close()
return
step_cfg = STEPS[i]
label = step_cfg.get("label", f"Starting step {i+1}")
exe = step_cfg.get("exe", "")
dur = int(step_cfg.get("duration_ms", 3000))
# Text zeichnen (zentriert, pro Schritt eigene Zeile)
y = y_positions[i]
text_id = canvas.create_text(center_x, y, text=label, fill=TEXT_FILL, font=TEXT_SUB_FONT, anchor="center")
text_items[i] = text_id
# Programm starten
ok, info = start_program(exe)
if not ok:
# Fehler unterhalb der jeweiligen Zeile anzeigen
canvas.create_text(center_x, y + 36, text=f"Fout: {info}", fill=TEXT_ERROR_FILL, font=TEXT_ERR_FONT, anchor="center")
# Fortschritt für die individuell eingestellte Dauer, dann nächster Schritt
draw_progress_bar_at_bottom(dur, callback=lambda: run_step(i + 1))
def show_have_fun_and_close():
canvas.delete("all")
photo = ImageTk.PhotoImage(scaled)
photo_frames.append(photo)
canvas.create_image(center_x, center_y, image=photo)
canvas.create_text(center_x, center_y, text=TEXT_HAVE_FUN, fill=TEXT_FILL, font=TEXT_FONT, anchor="center")
def really_close():
root.quit()
if os.name == "nt":
try:
hwnd = ctypes.windll.kernel32.GetConsoleWindow()
if hwnd != 0:
ctypes.windll.user32.PostMessageW(hwnd, 0x0010, 0, 0)
except Exception as e:
print(f"Fenster konnte nicht automatisch geschlossen werden: {e}")
minimal_ms = AUTO_CLOSE_SECONDS * 1000
# Gesamtzeit: Fade + Summe aller individuellen Step-Dauern + HaveFun + Sicherheitsaufschlag
total_steps_ms = sum(int(s.get("duration_ms", 0)) for s in STEPS)
total_needed_ms = FADE_DURATION_MS + total_steps_ms + HAVE_FUN_MS + 500
elapsed_ms = int((time.perf_counter() - timeline_start) * 1000)
remaining_ms = max(minimal_ms, total_needed_ms) - elapsed_ms
delay = max(HAVE_FUN_MS, remaining_ms)
root.after(delay, really_close)
root.bind("<Escape>", lambda e: root.quit())
# Optional: Auto-Close CMD nach 60 Sekunden unabhängig vom Flow
if os.name == "nt":
def auto_close_console():
hwnd = ctypes.windll.kernel32.GetConsoleWindow()
if hwnd != 0:
ctypes.windll.user32.PostMessageW(hwnd, 0x0010, 0, 0)
root.after(60000, auto_close_console)
do_fade(0)
root.mainloop()
if __name__ == "__main__":
main()
Fazit: Ich mag die Bastelei halt genauso wie das Fliegen und lerne ganz nebenbei ein wenig PY.
In diesem Sinne: Frohes Modden und immer eine Handbreit RAM frei! 😉🍻 -
Also mir ist gestern das erste mal seit wahrscheinlich über einem Jahr der Sim einfach unter dem Ar**h abgestürzt, weil irgendwo ein Memoryleak war. Aber ehrlich, bei mir starten bei jedem Sim-Start 18 Apps/Programme/Prozesse whatever mit und da sind Volanta, Navigraph Charts und dergleichen noch gar nicht mit eingerechnet. Ein bisschen bekloppt ist das schon. Aber einen ungemoddeten Sim fänd ich halt auch viel zu langweilig. Da bastel ich mir lieber noch ein weiteres Script, das mir die ganzen Fenster kleinhält, damit nicht tausend Sachen aufgehen, wenn ich den Sim starte

-
v0.7.2 ist draußen mit einem Fix, damit bei niedriger Wolkendecke der Flieger nicht mehr in einer dicken Wolkensuppe verschwindet.
-
Wie verhält sich deine Mod eigentlich in Kombination mit Active Sky?
-
Du kannst beide zusammen benutzen - allerdings muss ActiveSky im Passive Mode sein. ABER: ein fleißiger Bastler hat schon einen Weg gefunden, ActiveSky Live Weather kompatibel zu machen: https://github.com/tomwhoca/AtmosphereMod/pull/41
Ich bin nur noch nicht dazu gekommen, das als Funktion mit einzubauen. Es wird aber kommen
-
Ich fliege zwar im FS2020 aber ein Dankeschön für die Mod geht trotzdem raus. Auf den Bildern und Videos die ich gesehen habe, siehts einfach klasse aus.
-
Bin ab jetzt auch Fan. Schon geil, der Mod. Tolle Arbeit! tömsche

-