From 8b708c338b057640deaa856a3259e3bafff71526 Mon Sep 17 00:00:00 2001 From: Louis Date: Fri, 25 Sep 2026 15:34:24 +0200 Subject: [PATCH] =?UTF-8?q?RDP-Tab=20unter=20Windows:=20mstsc=20als=20owne?= =?UTF-8?q?d=20window=20statt=20Kindfenster=20(war=20hinter=20DirectCompos?= =?UTF-8?q?ition=20unsichtbar);=20dynamische=20Aufl=C3=B6sung=20als=20Stan?= =?UTF-8?q?dard,=20Skalieren=20pro=20Host=20w=C3=A4hlbar?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- src/main/main.js | 5 ++++ src/main/rdp-embed.js | 54 ++++++++++++++++++++++++++++--------------- src/main/rdp.js | 11 +++++---- src/renderer/app.js | 1 + 4 files changed, 47 insertions(+), 24 deletions(-) diff --git a/src/main/main.js b/src/main/main.js index 39dd7a5..4c17e3a 100644 --- a/src/main/main.js +++ b/src/main/main.js @@ -72,6 +72,11 @@ function createWindow() { Menu.setApplicationMenu(null); win.loadFile(path.join(__dirname, '..', 'renderer', 'index.html')); win.on('maximize', () => send('win:state', true)); + // Eingebettete RDP-Fenster (Windows: owned windows) beim Verschieben/Größenändern mitführen + const followRdp = () => { for (const s of rdpSessions.values()) { try { s.emb.reposition(); } catch {} } }; + win.on('move', followRdp); + win.on('resize', followRdp); + win.on('restore', followRdp); win.on('unmaximize', () => send('win:state', false)); win.webContents.setWindowOpenHandler(({ url }) => { shell.openExternal(url); return { action: 'deny' }; }); } diff --git a/src/main/rdp-embed.js b/src/main/rdp-embed.js index 641143b..ceb4707 100644 --- a/src/main/rdp-embed.js +++ b/src/main/rdp-embed.js @@ -8,23 +8,27 @@ let koffi = null; try { koffi = require('koffi'); } catch { /* ohne koffi nur externe Fenster */ } // ------------------------------------------------------------------ Windows +// Chromium zeichnet per DirectComposition über alle nativen Kindfenster – ein per SetParent eingehängtes +// mstsc bliebe unsichtbar (schwarz). Stattdessen wird das Sitzungsfenster rahmenlos und als "owned window" +// von MrTerm exakt über den Tab gelegt: Es liegt immer über MrTerm, minimiert sich mit und hat keinen Taskleisten-Eintrag. function winApi() { if (winApi.api) return winApi.api; const u = koffi.load('user32.dll'); const EnumProc = koffi.proto('bool __stdcall MrTermEnumProc(intptr hwnd, intptr lParam)'); + const POINT = koffi.struct('MrTermPOINT', { x: 'int', y: 'int' }); winApi.api = { EnumWindows: u.func('bool __stdcall EnumWindows(MrTermEnumProc *cb, intptr lParam)'), GetWindowThreadProcessId: u.func('uint32 __stdcall GetWindowThreadProcessId(intptr hwnd, _Out_ uint32 *pid)'), GetClassNameW: u.func('int __stdcall GetClassNameW(intptr hwnd, void *buf, int max)'), IsWindowVisible: u.func('bool __stdcall IsWindowVisible(intptr hwnd)'), IsWindow: u.func('bool __stdcall IsWindow(intptr hwnd)'), - SetParent: u.func('intptr __stdcall SetParent(intptr child, intptr parent)'), GetWindowLongPtrW: u.func('intptr __stdcall GetWindowLongPtrW(intptr hwnd, int idx)'), SetWindowLongPtrW: u.func('intptr __stdcall SetWindowLongPtrW(intptr hwnd, int idx, intptr value)'), SetWindowPos: u.func('bool __stdcall SetWindowPos(intptr hwnd, intptr after, int x, int y, int cx, int cy, uint32 flags)'), ShowWindow: u.func('bool __stdcall ShowWindow(intptr hwnd, int cmd)'), SetForegroundWindow: u.func('bool __stdcall SetForegroundWindow(intptr hwnd)'), - SetFocus: u.func('intptr __stdcall SetFocus(intptr hwnd)'), + ClientToScreen: u.func('bool __stdcall ClientToScreen(intptr hwnd, _Inout_ MrTermPOINT *pt)'), + POINT, }; return winApi.api; } @@ -45,13 +49,16 @@ function findWindowsOfPid(pid, cls) { return out; } -const GWL_STYLE = -16, GWL_EXSTYLE = -20; +const GWL_STYLE = -16, GWL_EXSTYLE = -20, GWLP_HWNDPARENT = -8; const WS_CHILD = 0x40000000, WS_POPUP = 0x80000000, WS_CAPTION = 0x00c00000, WS_THICKFRAME = 0x00040000; -const WS_SYSMENU = 0x00080000, WS_MINIMIZEBOX = 0x00020000, WS_MAXIMIZEBOX = 0x00010000, WS_EX_APPWINDOW = 0x00040000; +const WS_SYSMENU = 0x00080000, WS_MINIMIZEBOX = 0x00020000, WS_MAXIMIZEBOX = 0x00010000; +const WS_EX_APPWINDOW = 0x00040000, WS_EX_TOOLWINDOW = 0x00000080; +const FRAME = WS_CAPTION | WS_THICKFRAME | WS_SYSMENU | WS_MINIMIZEBOX | WS_MAXIMIZEBOX; const SWP_NOZORDER = 0x4, SWP_NOACTIVATE = 0x10, SWP_FRAMECHANGED = 0x20, SWP_SHOWWINDOW = 0x40; +const SW_HIDE = 0, SW_SHOWNOACTIVATE = 4, SW_SHOW = 5; class WinEmbed { - constructor(parentHandle) { this.parent = Number(parentHandle.readBigUInt64LE(0)); this.cls = 'TscShellContainerClass'; } + constructor(parentHandle) { this.parent = Number(parentHandle.readBigUInt64LE(0)); this.cls = 'TscShellContainerClass'; this.visible = true; } start({ rdpFile }) { this.proc = spawn('mstsc.exe', rdpFile ? [rdpFile] : [], { stdio: 'ignore' }); @@ -66,12 +73,11 @@ class WinEmbed { const [hwnd] = findWindowsOfPid(this.proc.pid, this.cls); if (hwnd) { const api = winApi(); - let style = Number(api.GetWindowLongPtrW(hwnd, GWL_STYLE)) >>> 0; - style = ((style & ~(WS_POPUP | WS_CAPTION | WS_THICKFRAME | WS_SYSMENU | WS_MINIMIZEBOX | WS_MAXIMIZEBOX)) | WS_CHILD) >>> 0; + const style = (((Number(api.GetWindowLongPtrW(hwnd, GWL_STYLE)) >>> 0) & ~(FRAME | WS_CHILD)) | WS_POPUP) >>> 0; api.SetWindowLongPtrW(hwnd, GWL_STYLE, style); - const ex = (Number(api.GetWindowLongPtrW(hwnd, GWL_EXSTYLE)) >>> 0) & ~WS_EX_APPWINDOW; - api.SetWindowLongPtrW(hwnd, GWL_EXSTYLE, ex >>> 0); - api.SetParent(hwnd, this.parent); + const ex = ((((Number(api.GetWindowLongPtrW(hwnd, GWL_EXSTYLE)) >>> 0) & ~WS_EX_APPWINDOW) | WS_EX_TOOLWINDOW) >>> 0); + api.SetWindowLongPtrW(hwnd, GWL_EXSTYLE, ex); + api.SetWindowLongPtrW(hwnd, GWLP_HWNDPARENT, this.parent); // Besitzer = MrTerm this.hwnd = hwnd; this.setBounds(bounds); return true; @@ -81,21 +87,30 @@ class WinEmbed { return false; } + // b: physische Pixel relativ zum Client-Bereich von MrTerm -> Bildschirmkoordinaten setBounds(b) { - if (!this.hwnd) return; - this.bounds = b; - winApi().SetWindowPos(this.hwnd, 0, b.x, b.y, Math.max(50, b.width), Math.max(50, b.height), SWP_NOZORDER | SWP_FRAMECHANGED | SWP_SHOWWINDOW); + if (b) this.bounds = b; + if (!this.hwnd || !this.bounds) return; + const api = winApi(); + const pt = { x: this.bounds.x, y: this.bounds.y }; + api.ClientToScreen(this.parent, pt); + api.SetWindowPos(this.hwnd, 0, pt.x, pt.y, Math.max(50, this.bounds.width), Math.max(50, this.bounds.height), + SWP_NOZORDER | SWP_NOACTIVATE | SWP_FRAMECHANGED | (this.visible ? SWP_SHOWWINDOW : 0)); } - show() { if (this.hwnd) { winApi().ShowWindow(this.hwnd, 5); if (this.bounds) this.setBounds(this.bounds); } } - hide() { if (this.hwnd) winApi().ShowWindow(this.hwnd, 0); } - focus() { if (this.hwnd) { winApi().SetFocus(this.hwnd); } } + reposition() { this.setBounds(); } + show() { this.visible = true; if (this.hwnd) { winApi().ShowWindow(this.hwnd, SW_SHOWNOACTIVATE); this.setBounds(); } } + hide() { this.visible = false; if (this.hwnd) winApi().ShowWindow(this.hwnd, SW_HIDE); } + focus() { if (this.hwnd && this.visible) winApi().SetForegroundWindow(this.hwnd); } // Aus MrTerm lösen und als normales Fenster weiterlaufen lassen detach() { if (!this.hwnd) return; const api = winApi(); - api.SetParent(this.hwnd, 0); - const style = ((Number(api.GetWindowLongPtrW(this.hwnd, GWL_STYLE)) >>> 0) & ~WS_CHILD) | WS_CAPTION | WS_THICKFRAME | WS_SYSMENU | WS_MINIMIZEBOX | WS_MAXIMIZEBOX; - api.SetWindowLongPtrW(this.hwnd, GWL_STYLE, style >>> 0); + api.SetWindowLongPtrW(this.hwnd, GWLP_HWNDPARENT, 0); + const style = ((((Number(api.GetWindowLongPtrW(this.hwnd, GWL_STYLE)) >>> 0) & ~WS_POPUP) | FRAME) >>> 0); + api.SetWindowLongPtrW(this.hwnd, GWL_STYLE, style); + const ex = ((((Number(api.GetWindowLongPtrW(this.hwnd, GWL_EXSTYLE)) >>> 0) & ~WS_EX_TOOLWINDOW) | WS_EX_APPWINDOW) >>> 0); + api.SetWindowLongPtrW(this.hwnd, GWL_EXSTYLE, ex); + api.ShowWindow(this.hwnd, SW_SHOW); api.SetWindowPos(this.hwnd, 0, 80, 80, this.bounds?.width || 1280, this.bounds?.height || 800, SWP_NOZORDER | SWP_FRAMECHANGED | SWP_SHOWWINDOW); this.hwnd = null; } @@ -174,6 +189,7 @@ class LinuxEmbed { show() { if (!this.xwin) return; const X = x11(); X.XMapWindow(X.display, this.xwin); if (this.bounds) this.setBounds(this.bounds); X.XFlush(X.display); } hide() { if (!this.xwin) return; const X = x11(); X.XUnmapWindow(X.display, this.xwin); X.XFlush(X.display); } focus() { if (!this.xwin) return; const X = x11(); X.XSetInputFocus(X.display, this.xwin, 2, 0); X.XFlush(X.display); } + reposition() {} detach() { /* FreeRDP-Fenster lassen sich nicht sauber herauslösen – Sitzung stattdessen extern neu starten */ } kill() { try { this.proc?.kill(); } catch {} } } diff --git a/src/main/rdp.js b/src/main/rdp.js index d61b688..6e4f255 100644 --- a/src/main/rdp.js +++ b/src/main/rdp.js @@ -46,8 +46,8 @@ function writeRdpFile(host, size) { w && `desktopwidth:i:${w}`, hgt && `desktopheight:i:${hgt}`, 'dynamic resolution:i:1', - // Im Tab keine Skalierung, damit die Auflösung beim Größenändern mitwächst - `smart sizing:i:${size ? 0 : 1}`, + // Standard: Auflösung folgt der Fenstergröße (Windows 8.1 / Server 2012 R2 und neuer). "smart sizing" würde stattdessen nur skalieren. + `smart sizing:i:${host.rdpResize === 'scale' ? 1 : 0}`, 'displayconnectionbar:i:0', `redirectclipboard:i:${host.rdpClipboard === false ? 0 : 1}`, `redirectdrives:i:${host.rdpDrives ? 1 : 0}`, @@ -78,10 +78,11 @@ function freerdpArgs(host, size) { const args = [`/v:${host.address}:${port}`, '/cert:tofu', '+auto-reconnect']; if (host.username) args.push(`/u:${host.username}`); if (host.domain) args.push(`/d:${host.domain}`); - if (size) args.push(`/size:${size.width}x${size.height}`, '/dynamic-resolution'); + const resize = host.rdpResize === 'scale' ? '/smart-sizing' : '/dynamic-resolution'; + if (size) args.push(`/size:${size.width}x${size.height}`, resize); else if (host.rdpFullscreen) args.push('/f'); - else if (host.rdpWidth && host.rdpHeight) args.push(`/size:${host.rdpWidth}x${host.rdpHeight}`); - else args.push('/dynamic-resolution'); + else if (host.rdpWidth && host.rdpHeight) args.push(`/size:${host.rdpWidth}x${host.rdpHeight}`, resize); + else args.push(resize); if (host.rdpClipboard !== false) args.push('+clipboard'); if (host.rdpDrives) args.push(`/drive:home,${os.homedir()}`); if (host.rdpAudio !== false) args.push('/sound'); diff --git a/src/renderer/app.js b/src/renderer/app.js index fb1b484..a8e7b91 100644 --- a/src/renderer/app.js +++ b/src/renderer/app.js @@ -404,6 +404,7 @@ function editHost(hst = {}) { rdpBox.append( field('Domäne', 'domain', hst.domain, { placeholder: 'optional' }), row(field('Breite', 'rdpWidth', hst.rdpWidth, { type: 'number', placeholder: 'auto' }), field('Höhe', 'rdpHeight', hst.rdpHeight, { type: 'number', placeholder: 'auto' })), + field('Bei Größenänderung', 'rdpResize', hst.rdpResize || 'dynamic', { type: 'select', options: [['dynamic', 'Auflösung dynamisch anpassen'], ['scale', 'Bild skalieren (feste Auflösung)']] }), check('Vollbild', 'rdpFullscreen', hst.rdpFullscreen), check('Zwischenablage teilen', 'rdpClipboard', hst.rdpClipboard !== false), check('Audio wiedergeben', 'rdpAudio', hst.rdpAudio !== false),