Multi-Monitor-Setups sind heute Standard — und Apps müssen damit umgehen können. Das screen-Modul liefert die nötige Info: welche Displays gibt es, welche Auflösung, welcher ist primär, wo soll das Fenster auftauchen. Plus: Events bei Hot-Plug-Wechseln.
Wichtig: screen braucht app.ready
import { app, screen } from 'electron';
// FALSCH — vor app.ready werfen alle screen-Methoden
// const primary = screen.getPrimaryDisplay();
// RICHTIG
app.whenReady().then(() => {
const primary = screen.getPrimaryDisplay();
console.log(primary);
});Ein häufiger Anfänger-Fehler: screen wird im Top-Level genutzt — Crash, weil noch nicht initialisiert.
Display-Info
const primary = screen.getPrimaryDisplay();
console.log(primary);
// {
// id: 12345,
// bounds: { x: 0, y: 0, width: 2560, height: 1440 },
// workArea: { x: 0, y: 25, width: 2560, height: 1415 },
// scaleFactor: 2, // HiDPI / Retina
// rotation: 0,
// internal: true,
// touchSupport: 'unknown'
// }
const all = screen.getAllDisplays();
console.log(`${all.length} Displays`);bounds = ganzer Display. workArea = Bereich ohne Menüleiste/Dock/Taskleiste — wichtig für Fenster-Positionierung, sonst landet das Fenster unter der Taskleiste.
Display zum Punkt finden
// Welcher Display am Cursor?
const cursorDisplay = screen.getDisplayNearestPoint(
screen.getCursorScreenPoint()
);
// Welcher Display zu einem Fenster?
const winDisplay = screen.getDisplayMatching(mainWindow.getBounds());
// Auf einem bestimmten Display zentrieren
const { width, height } = cursorDisplay.workArea;
const winSize = mainWindow.getSize();
mainWindow.setPosition(
cursorDisplay.workArea.x + Math.floor((width - winSize[0]) / 2),
cursorDisplay.workArea.y + Math.floor((height - winSize[1]) / 2)
);Klassischer Use-Case: ein neues Fenster soll auf dem Display erscheinen, wo der Cursor ist — nicht auf dem primären (was bei Multi-Monitor unangenehm ist).
DPI-Scaling
// Scale Factor: 1.0 = klassisch, 2.0 = Retina/HiDPI, 1.5 = mittlere Skalierung
const sf = screen.getPrimaryDisplay().scaleFactor;
// dipToScreenRect / screenToDipRect: Umrechnung CSS-Pixel ↔ Hardware-Pixel
const screenRect = screen.dipToScreenRect(mainWindow, {
x: 100, y: 100, width: 800, height: 600
});Auf Retina/HiDPI-Displays ist 1 CSS-Pixel = 2 oder 3 Hardware-Pixel. Die meisten APIs in Electron arbeiten in CSS-Pixeln (DPI-aware) — bounds/getBounds ist die Ausnahme (Hardware-Pixel).
Display-Events
screen.on('display-added', (_event, display) => {
console.log('Neuer Monitor:', display);
});
screen.on('display-removed', (_event, display) => {
console.log('Monitor weg:', display);
// Falls ein Fenster auf dem entfernten Display war:
// auf primären Display umlenken
relocateOrphanWindows();
});
screen.on('display-metrics-changed', (_event, display, changedMetrics) => {
console.log('Display geändert:', changedMetrics);
// changedMetrics: ['bounds', 'workArea', 'scaleFactor', 'rotation']
});display-removed ist wichtig: User stöpselt externen Monitor ab, deine Fenster auf diesem Display würden ohne Logik off-screen verschwinden. Mit Listener das Fenster auf den primären zurückholen.
Pattern: gespeicherte Fenster-Position validieren
Beim App-Start prüfen, ob die letzte Fenster-Position noch sichtbar ist (User könnte Monitor abgesteckt haben):
function isPositionVisible(bounds) {
const displays = screen.getAllDisplays();
return displays.some(d =>
bounds.x >= d.bounds.x &&
bounds.y >= d.bounds.y &&
bounds.x + bounds.width <= d.bounds.x + d.bounds.width &&
bounds.y + bounds.height <= d.bounds.y + d.bounds.height
);
}
const lastBounds = store.get('window-bounds', null);
const useBounds = (lastBounds && isPositionVisible(lastBounds))
? lastBounds
: { width: 1200, height: 800 }; // Default — wird zentriert
const win = new BrowserWindow(useBounds);Ohne diese Prüfung wäre das Fenster nach Monitor-Wechsel unsichtbar — User-Frust.
Besonderheiten
screen nicht vor app.ready nutzen.
Häufiger Anfänger-Fehler: screen.getPrimaryDisplay() im Top-Level. Crasht, weil Display-Subsystem noch nicht initialisiert. Immer in app.whenReady() oder später.
workArea statt bounds für Fenster-Positionierung.
bounds enthält Menüleiste/Dock/Taskleiste — Fenster dort plaziert sind teilweise verdeckt. workArea zieht diese Bereiche ab. Für Fenster-Layout fast immer workArea nehmen.
display-removed abfangen — sonst verlorene Fenster.
User stöpselt externen Monitor ab → Fenster auf diesem Display sind off-screen. Listener registrieren, betroffene Fenster auf primären holen. Sonst muss der User raten, wo das Fenster ist.
Gespeicherte Bounds beim Start validieren.
User hat bei letztem Quit Fenster auf externem Monitor — beim App-Start ist der Monitor weg, Fenster wäre off-screen. Mit isPositionVisible-Check vorher prüfen, sonst auf Default zurückfallen.
scaleFactor ist auf gemischten Setups unterschiedlich.
Multi-Monitor mit Retina + Standard-Display: scaleFactor 2.0 vs. 1.0. Wer Bildschirm-Pixel hardcodet (Screenshots, Rendering), muss pro Display rechnen.
Cursor-Position cross-Display funktioniert.
screen.getCursorScreenPoint() liefert Position in der globalen Screen-Koordinate (über alle Displays). Bei Multi-Monitor wechselt die x-Koordinate sprunghaft, wenn der Cursor auf den nächsten Monitor wandert. Zu beachten bei Cursor-Tracking-Apps.