RDP-Tab unter Windows: mstsc als owned window statt Kindfenster (war hinter DirectComposition unsichtbar); dynamische Auflösung als Standard, Skalieren pro Host wählbar
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -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' }; });
|
||||
}
|
||||
|
||||
+35
-19
@@ -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 {} }
|
||||
}
|
||||
|
||||
+6
-5
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user