Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,9 @@ it's a self-contained single file, no .NET install required (Windows 10/11 x64).

- **RDP** — Microsoft's official ActiveX control (`mstscax`): dynamic resolution that follows the
window/full-screen size (falls back to smart-sizing on older hosts) or a **fixed resolution** of your
choice, **remote desktop scaling (DPI)** so text stays readable on 4K/HiDPI screens
(*Settings → Connection*), clipboard/drive/printer/audio redirection, server-identity verification
choice, **remote desktop scaling** from 50% to 200% — larger, crisp text on 4K/HiDPI screens or a
smaller UI with more desktop area (*Settings → Connection*),
clipboard/drive/printer/audio redirection, server-identity verification
level, RD Gateway / jump-host and admin session.
- **SSH** — embedded **xterm.js** terminal: password and private-key auth (including
**passphrase-protected keys**), host-key verification (trust-on-first-use with fingerprint prompt),
Expand Down
3 changes: 2 additions & 1 deletion docs/ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ current priorities.
window controls move onto the strip. Distinct from true full screen (F11).
- ✅ **Full-screen** with auto-hiding, draggable, pinnable bar + "other connections" flyout.
- ✅ **Dynamic resolution** — session = window/monitor physical pixels, DPI-safe; optionally a
fixed resolution, plus remote desktop scaling (DPI) for readable text on 4K/HiDPI screens.
fixed resolution, plus remote desktop scaling 50–200% (session DPI above 100%, oversized
desktop scaled into the window below it).
- ✅ **Credentials in Windows Credential Manager** (DPAPI); prompt at connect; "connect as".
- ✅ **Server tree** — groups, favorites (pinned), collapsible groups, one-click group
rename, search, drag-and-drop reorder, background reachability dots.
Expand Down
69 changes: 59 additions & 10 deletions src/RdpManager/Core/RdpDisplay.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,30 @@ namespace RdpManager.Core
/// Dwa niezależne pokrętła (Ustawienia → Połączenie → Domyślne RDP):
/// - ROZDZIELCZOŚĆ: 0×0 = „dopasuj do okna" (domyślne — sesja trzyma natywne piksele panelu, obraz 1:1),
/// wartość stała (np. 1600×900) = sesja negocjuje ten rozmiar, a SmartSizing rozciąga go do okna.
/// - SKALOWANIE (DPI): 0 = „jak ekran lokalny" (mnożnik DPI okna — tak robi mstsc), inaczej stałe procenty.
/// Trafia do ulDesktopScaleFactor (UpdateSessionDisplaySettings) i „DesktopScaleFactor" przed Connect,
/// więc zdalny pulpit RYSUJE interfejs większy — bez utraty ostrości (odwrotnie niż SmartSizing).
/// Na ekranie 4K/HiDPI sesja 1:1 przy 100% daje mikroskopijny tekst; to jest na to lekarstwo.
/// - SKALOWANIE: 0 = „jak ekran lokalny" (mnożnik DPI okna — tak robi mstsc), inaczej stałe procenty.
/// Dwa mechanizmy, bo protokół zna DPI tylko w zakresie 100..500%:
/// • >= 100% → ulDesktopScaleFactor (UpdateSessionDisplaySettings) i „DesktopScaleFactor" przed
/// Connect, więc zdalny pulpit RYSUJE interfejs większy — bez utraty ostrości. Na ekranie 4K/HiDPI
/// sesja 1:1 przy 100% daje mikroskopijny tekst; to jest na to lekarstwo.
/// • &lt; 100% → DPI zostaje 100%, a żądamy PULPITU WIĘKSZEGO niż panel (<see cref="ResolutionMultiplier"/>)
/// i zmniejszamy obraz SmartSizingiem: więcej pulpitu, mniejszy UI, kosztem lekkiego rozmycia.
/// </summary>
public static class RdpDisplay
{
// Zakresy z dokumentacji IMsRdpClient9::UpdateSessionDisplaySettings: ulDesktopScaleFactor 100..500,
// ulDeviceScaleFactor TYLKO 100/140/180 (inna wartość => E_INVALIDARG, cała zmiana odrzucona).
// Device trzymamy na 100 („bez dodatkowego skalowania urządzenia") — DPI sesji ustala desktopScaleFactor.
public const int MinScale = 100;
public const int MaxScale = 500;
public const uint DeviceScaleFactor = 100u;

/// <summary>Dół protokołu: DPI sesji nie zejdzie poniżej 100% (mniejsze wartości serwer IGNORUJE).</summary>
public const int ProtocolMinScale = 100;

/// <summary>Dół naszego suwaka. Poniżej 100% DPI nie wchodzi w grę, więc pomniejszenie robimy drugą
/// drogą — patrz <see cref="ResolutionMultiplier"/>. Niżej niż 50% nie ma sensu (rozdzielczość rośnie
/// dwukrotnie, tekst i tak nieczytelny).</summary>
public const int MinScale = 50;

/// <summary>Czy ustawienie opisuje STAŁĄ rozdzielczość (a nie „dopasuj do okna").</summary>
public static bool IsFixed(int width, int height)
=> width >= RdpUtils.MinDim && height >= RdpUtils.MinDim;
Expand Down Expand Up @@ -69,18 +79,57 @@ public static int ParseScale(string tag)
}

/// <summary>
/// Skala DPI wysyłana serwerowi. <paramref name="configured"/> &gt; 0 = wybór użytkownika (przycięty do
/// [100..500]); 0 = „jak ekran lokalny", czyli mnożnik DPI okna (1.5 → 150%). Zaokrąglamy do 5%, bo
/// Skala wybrana przez użytkownika. <paramref name="configured"/> &gt; 0 = jego wartość (przycięta do
/// [50..500]); 0 = „jak ekran lokalny", czyli mnożnik DPI okna (1.5 → 150%). Zaokrąglamy do 5%, bo
/// mnożnik z <c>VisualTreeHelper.GetDpi</c> bywa niecałkowity (np. 1.4999), a serwer i tak operuje
/// na okrągłych krokach DPI.
/// na okrągłych krokach DPI. W trybie „auto" nie schodzimy poniżej 100% — ekran lokalny poniżej 100%
/// to zwykle artefakt pomiaru, a nie życzenie użytkownika (pomniejszenie wybiera się świadomie).
/// </summary>
public static int EffectiveScale(int configured, double localDpiScale)
{
if (configured > 0) return Math.Clamp(configured, MinScale, MaxScale);
if (double.IsNaN(localDpiScale) || double.IsInfinity(localDpiScale) || localDpiScale <= 0) return MinScale;
if (double.IsNaN(localDpiScale) || double.IsInfinity(localDpiScale) || localDpiScale <= 0) return ProtocolMinScale;

int pct = (int)Math.Round(localDpiScale * 100.0 / 5.0, MidpointRounding.AwayFromZero) * 5;
return Math.Clamp(pct, MinScale, MaxScale);
return Math.Clamp(pct, ProtocolMinScale, MaxScale);
}

/// <summary>
/// Wartość do <c>ulDesktopScaleFactor</c>: dla skali &gt;= 100% to wprost skala (serwer rysuje UI
/// większe, obraz zostaje ostry). Poniżej 100% protokół nie pozwala zejść, więc DPI zostaje na 100%,
/// a pomniejszenie realizuje <see cref="ResolutionMultiplier"/>.
/// </summary>
public static uint DesktopScaleFactor(int effectiveScale)
=> (uint)Math.Clamp(effectiveScale, ProtocolMinScale, MaxScale);

/// <summary>
/// Mnożnik ROZDZIELCZOŚCI dla skali poniżej 100%: żądamy pulpitu większego niż panel (100/75 = 1.33×)
/// i zmniejszamy obraz SmartSizingiem. Skutek jak DPI &lt; 100% — więcej pulpitu, mniejszy interfejs —
/// kosztem lekkiego rozmycia (skalowanie w dół wygląda znacznie lepiej niż w górę). Dla &gt;= 100%
/// zwraca 1.0, bo tam pracuje już desktopScaleFactor i obraz ma zostać 1:1.
/// </summary>
public static double ResolutionMultiplier(int effectiveScale)
=> effectiveScale >= ProtocolMinScale || effectiveScale <= 0
? 1.0
: ProtocolMinScale / (double)Math.Max(effectiveScale, MinScale);

/// <summary>
/// Nakłada <see cref="ResolutionMultiplier"/> na wymiary bazowe (piksele panelu albo stała
/// rozdzielczość) i normalizuje wynik. Gdy po przemnożeniu któryś wymiar wychodzi za limit sesji
/// (8192), przycinamy MNOŻNIK, nie pojedynczy wymiar — inaczej pulpit dostałby inne proporcje
/// niż okno i obraz byłby rozciągnięty.
/// </summary>
public static (int Width, int Height) ScaleResolution(int width, int height, int effectiveScale)
{
if (width < RdpUtils.MinDim || height < RdpUtils.MinDim) return (0, 0);

double mult = ResolutionMultiplier(effectiveScale);
if (mult > 1.0)
mult = Math.Min(mult, Math.Min(RdpUtils.MaxDim / (double)width, RdpUtils.MaxDim / (double)height));
if (mult <= 1.0) return (RdpUtils.NormalizeDim(width), RdpUtils.NormalizeDim(height));

return (RdpUtils.NormalizeDim((int)Math.Round(width * mult)),
RdpUtils.NormalizeDim((int)Math.Round(height * mult)));
}
}
}
5 changes: 5 additions & 0 deletions src/RdpManager/MainWindow.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -851,6 +851,11 @@
</StackPanel>
<ComboBox x:Name="SetRdpScale" Grid.Column="1" Width="190" VerticalAlignment="Center">
<ComboBoxItem Content="{DynamicResource S.scale.auto}" Tag=""/>
<ComboBoxItem Content="50%" Tag="50"/>
<ComboBoxItem Content="60%" Tag="60"/>
<ComboBoxItem Content="75%" Tag="75"/>
<ComboBoxItem Content="80%" Tag="80"/>
<ComboBoxItem Content="90%" Tag="90"/>
<ComboBoxItem Content="100%" Tag="100"/>
<ComboBoxItem Content="125%" Tag="125"/>
<ComboBoxItem Content="150%" Tag="150"/>
Expand Down
56 changes: 34 additions & 22 deletions src/RdpManager/RdpDynamicResolution.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,10 @@ namespace RdpManager
/// przeliczane przez <see cref="RdpDisplay"/>) modyfikują oba wymiary tego zachowania:
/// - stała rozdzielczość → nie mierzymy panelu, wysyłamy wybrany rozmiar i włączamy SmartSizing
/// (pulpit i tak wypełnia okno, kosztem skalowania);
/// - skala DPI → jedzie w ulDesktopScaleFactor, więc zdalny pulpit rysuje UI większe zamiast
/// upychać je w natywnych pikselach 4K (to ratunek na „nic nie da się odczytać").
/// - skala &gt;= 100% → jedzie w ulDesktopScaleFactor, więc zdalny pulpit rysuje UI większe zamiast
/// upychać je w natywnych pikselach 4K (to ratunek na „nic nie da się odczytać");
/// - skala &lt; 100% → DPI zostaje 100%, ale żądamy pulpitu WIĘKSZEGO niż panel i wciskamy go
/// SmartSizingiem (pomniejszenie — protokół nie zna DPI poniżej 100%).
/// </summary>
public sealed class RdpDynamicResolution : IDisposable
{
Expand Down Expand Up @@ -80,16 +82,27 @@ public RdpDynamicResolution(Session session, WindowsFormsHost host, AppSettings
return (w, h);
}

/// <summary>Skala DPI dla serwera: wybór użytkownika, a przy „auto" mnożnik DPI okna (jak mstsc).</summary>
private uint EffectiveScale()
/// <summary>Skala wybrana przez użytkownika, a przy „auto" mnożnik DPI okna (jak mstsc). Wartości
/// poniżej 100% nie są DPI — realizuje je mnożnik rozdzielczości (patrz <see cref="RdpDisplay"/>).</summary>
private int EffectiveScale()
{
double dpi = TryGetDpiScale(out var sx, out _) ? sx : 1.0;
return (uint)RdpDisplay.EffectiveScale(_settings?.RdpScalePercent ?? 0, dpi);
return RdpDisplay.EffectiveScale(_settings?.RdpScalePercent ?? 0, dpi);
}

/// <summary>Przy stałej rozdzielczości SmartSizing musi zostać włączony niezależnie od trybu podziału —
/// inaczej wybrany pulpit siedziałby w letterboxie zamiast wypełniać okno.</summary>
private bool WantSmartSizing(bool hasFixedRes) => _fit || hasFixedRes;
/// <summary>Wymiary do wysłania: baza (piksele panelu albo stała rozdzielczość) po nałożeniu mnożnika
/// dla skali poniżej 100%.</summary>
private (int W, int H) TargetRes(int baseW, int baseH)
{
var (w, h) = RdpDisplay.ScaleResolution(baseW, baseH, EffectiveScale());
return (w, h);
}

/// <summary>SmartSizing musi być włączony niezależnie od trybu podziału, gdy: (a) rozdzielczość jest
/// stała — inaczej pulpit siedziałby w letterboxie zamiast wypełniać okno; (b) skala jest poniżej 100%
/// — tam pomniejszenie POLEGA na wciśnięciu większego pulpitu w panel.</summary>
private bool WantSmartSizing(bool hasFixedRes)
=> _fit || hasFixedRes || EffectiveScale() < RdpDisplay.ProtocolMinScale;

private bool _fit;
/// <summary>„Dopasuj do okna": host skaluje pulpit do swojego rozmiaru (SmartSizing), więc pulpit zawsze
Expand Down Expand Up @@ -130,13 +143,14 @@ public void ApplyPreConnect()
{
// Tylko dla stałej rozdzielczości. W trybie „dopasuj do okna" zostawiamy domyślny rozmiar
// kontrolki i renegocjujemy po zalogowaniu (ApplyInitial) — tak działało to do tej pory.
try { rdp.DesktopWidth = fx.W; rdp.DesktopHeight = fx.H; }
var target = TargetRes(fx.W, fx.H);
try { rdp.DesktopWidth = target.W; rdp.DesktopHeight = target.H; }
catch (COMException) { }
catch (InvalidComObjectException) { }
}

TrySetSmartSizing(rdp, WantSmartSizing(fx.W > 0));
TrySetPreConnectScale(rdp, EffectiveScale());
TrySetPreConnectScale(rdp, RdpDisplay.DesktopScaleFactor(EffectiveScale()));
}

/// <summary>
Expand Down Expand Up @@ -166,9 +180,9 @@ public void ApplyExact(int physW, int physH)
var fx = FixedRes();
if (fx.W > 0) { physW = fx.W; physH = fx.H; }

physW = RdpUtils.NormalizeDim(physW);
physH = RdpUtils.NormalizeDim(physH);
if (physW < MinDim || physH < MinDim) return;
var target = TargetRes(physW, physH); // skala < 100% => pulpit większy niż monitor
if (target.W < MinDim || target.H < MinDim) return;
physW = target.W; physH = target.H; // TargetRes już normalizuje (parzyste, [200..8192])

var rdp = _session.Rdp;
if (rdp == null) return;
Expand Down Expand Up @@ -216,18 +230,16 @@ private void OnDebounceTick(object sender, EventArgs e)
catch (COMException) { return; }
if (!live) return;

int w, h;
int baseW, baseH;
var fx = FixedRes();
if (fx.W > 0)
{
w = fx.W; h = fx.H; // stała rozdzielczość: nie mierzymy panelu, tylko wysyłamy wybraną
}
else
{
if (!TryGetPhysicalPixels(out w, out h)) return;
w = RdpUtils.NormalizeDim(w);
h = RdpUtils.NormalizeDim(h);
baseW = fx.W; baseH = fx.H; // stała rozdzielczość: nie mierzymy panelu, tylko wysyłamy wybraną
}
else if (!TryGetPhysicalPixels(out baseW, out baseH)) return;

// Skala < 100% => pulpit większy niż baza, wciśnięty w panel SmartSizingiem (pomniejszenie).
var (w, h) = TargetRes(baseW, baseH);
if (w < MinDim || h < MinDim) return;
if (w == _lastW && h == _lastH) return;

Expand Down Expand Up @@ -269,7 +281,7 @@ private void OnDebounceTick(object sender, EventArgs e)
/// </summary>
private void UpdateDisplay(AxMsRdpClient11NotSafeForScripting rdp, int w, int h)
{
uint scale = EffectiveScale();
uint scale = RdpDisplay.DesktopScaleFactor(EffectiveScale());
if (scale == 100u)
{
rdp.UpdateSessionDisplaySettings((uint)w, (uint)h, 0u, 0u, 0u, 100u, RdpDisplay.DeviceScaleFactor);
Expand Down
4 changes: 2 additions & 2 deletions src/RdpManager/Themes/Strings.en.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -110,8 +110,8 @@
<sys:String x:Key="S.set.resolution">RDP session resolution</sys:String>
<sys:String x:Key="S.set.resolution.sub">“Fit to window” keeps the resolution equal to the window (crisp 1:1 image). A fixed value is stretched to the window.</sys:String>
<sys:String x:Key="S.res.auto">Fit to window</sys:String>
<sys:String x:Key="S.set.rdpscale">Remote desktop scaling (DPI)</sys:String>
<sys:String x:Key="S.set.rdpscale.sub">Enlarges text and icons inside the session without blurring. Helps on 4K/HiDPI displays; requires Windows 10 / Server 2016 or newer.</sys:String>
<sys:String x:Key="S.set.rdpscale">Remote desktop scaling</sys:String>
<sys:String x:Key="S.set.rdpscale.sub">Above 100% the server draws text and icons larger, without blurring (session DPI; requires Windows 10 / Server 2016 or newer). Below 100% the client requests a higher resolution and scales the image down — more desktop, smaller interface, slightly softer.</sys:String>
<sys:String x:Key="S.scale.auto">Match local display</sys:String>
<sys:String x:Key="S.set.autoreconnect">Automatically resume dropped connections</sys:String>
<sys:String x:Key="S.set.behavior">Behavior</sys:String>
Expand Down
4 changes: 2 additions & 2 deletions src/RdpManager/Themes/Strings.pl.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -110,8 +110,8 @@
<sys:String x:Key="S.set.resolution">Rozdzielczość sesji RDP</sys:String>
<sys:String x:Key="S.set.resolution.sub">„Dopasuj do okna" trzyma rozdzielczość równą oknu (ostry obraz 1:1). Stała wartość jest rozciągana do okna.</sys:String>
<sys:String x:Key="S.res.auto">Dopasuj do okna</sys:String>
<sys:String x:Key="S.set.rdpscale">Skalowanie pulpitu zdalnego (DPI)</sys:String>
<sys:String x:Key="S.set.rdpscale.sub">Powiększa tekst i ikony w sesji bez rozmycia. Pomaga na ekranach 4K/HiDPI; wymaga serwera Windows 10 / Server 2016 lub nowszego.</sys:String>
<sys:String x:Key="S.set.rdpscale">Skalowanie pulpitu zdalnego</sys:String>
<sys:String x:Key="S.set.rdpscale.sub">Powyżej 100% serwer rysuje tekst i ikony większe, bez rozmycia (DPI sesji; wymaga Windows 10 / Server 2016 lub nowszego). Poniżej 100% klient żąda wyższej rozdzielczości i zmniejsza obraz — więcej pulpitu, drobniejszy interfejs, nieco mniej ostro.</sys:String>
<sys:String x:Key="S.scale.auto">Jak ekran lokalny</sys:String>
<sys:String x:Key="S.set.autoreconnect">Automatycznie wznawiaj zerwane połączenie</sys:String>
<sys:String x:Key="S.set.behavior">Zachowanie</sys:String>
Expand Down
Loading
Loading