// MacVNC - a small VNC viewer for Windows with proper client-side scaling, // written for connecting to a Mac's built-in Screen Sharing. // // Single file, C# 5, .NET Framework 4.x (built into Windows 10/11). To build it yourself: // C:\Windows\Microsoft.NET\Framework64\v4.0.30319\csc.exe /nologo /target:winexe /optimize /out:MacVNC.exe MacVNC.cs // // Supports: VNC password auth, Raw / CopyRect / RRE / Hextile / ZRLE encodings, local cursor, // desktop resize, clipboard text both ways, fit-to-window or fixed zoom, smooth scaling, full screen. using System; using System.Collections.Generic; using System.Drawing; using System.Drawing.Drawing2D; using System.Drawing.Imaging; using System.IO; using System.Net.Sockets; using System.Runtime.InteropServices; using System.Security.Cryptography; using System.Text; using System.Threading; using System.Windows.Forms; namespace MacVnc { // ===================================================================================== // Program // ===================================================================================== static class Program { [DllImport("user32.dll")] static extern bool SetProcessDPIAware(); [STAThread] static void Main() { try { SetProcessDPIAware(); } catch { } Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); Application.ThreadException += (s, e) => MessageBox.Show(e.Exception.ToString(), "MacVNC error", MessageBoxButtons.OK, MessageBoxIcon.Error); Settings settings = Settings.Load(); while (true) { string host, password; int port; using (var dlg = new ConnectDialog(settings)) { if (dlg.ShowDialog() != DialogResult.OK) return; host = dlg.HostName; port = dlg.Port; password = dlg.Password; } settings.Save(); var client = new RfbClient(); try { Cursor.Current = Cursors.WaitCursor; client.Connect(host, port, password, 10000); } catch (Exception ex) { client.Close(); Cursor.Current = Cursors.Default; MessageBox.Show(ex.Message, "MacVNC - couldn't connect", MessageBoxButtons.OK, MessageBoxIcon.Warning); continue; } Cursor.Current = Cursors.Default; var viewer = new ViewerForm(client, settings, new ConnectionInfo(host, port, password)); Application.Run(viewer); settings.Save(); if (!viewer.ReturnToConnect) return; } } } sealed class ConnectionInfo { public readonly string Host, Password; public readonly int Port; public ConnectionInfo(string host, int port, string password) { Host = host; Port = port; Password = password; } } // ===================================================================================== // Settings (stored in %APPDATA%\MacVNC\settings.txt - passwords are never saved) // ===================================================================================== sealed class Settings { public string LastHost = ""; public string Scale = "Fit"; public bool Smooth = true; public bool CtrlIsCmd = true; public bool CmdAsMeta = false; public bool KeepAwake = false; public bool AutoReconnect = true; static string FilePath { get { return Path.Combine(Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "MacVNC"), "settings.txt"); } } public static Settings Load() { var s = new Settings(); try { if (!File.Exists(FilePath)) return s; foreach (string line in File.ReadAllLines(FilePath)) { int eq = line.IndexOf('='); if (eq <= 0) continue; string k = line.Substring(0, eq).Trim(), v = line.Substring(eq + 1).Trim(); switch (k) { case "host": s.LastHost = v; break; case "scale": s.Scale = v; break; case "smooth": s.Smooth = v == "1"; break; case "ctrlIsCmd": s.CtrlIsCmd = v == "1"; break; case "cmdAsMeta": s.CmdAsMeta = v == "1"; break; case "keepAwake": s.KeepAwake = v == "1"; break; case "autoReconnect": s.AutoReconnect = v == "1"; break; } } } catch { } return s; } public void Save() { try { Directory.CreateDirectory(Path.GetDirectoryName(FilePath)); File.WriteAllLines(FilePath, new[] { "host=" + LastHost, "scale=" + Scale, "smooth=" + (Smooth ? "1" : "0"), "ctrlIsCmd=" + (CtrlIsCmd ? "1" : "0"), "cmdAsMeta=" + (CmdAsMeta ? "1" : "0"), "keepAwake=" + (KeepAwake ? "1" : "0"), "autoReconnect=" + (AutoReconnect ? "1" : "0") }); } catch { } } } // ===================================================================================== // Connect dialog // ===================================================================================== sealed class ConnectDialog : Form { readonly Settings settings; readonly TextBox hostBox, passBox; public string HostName { get; private set; } public int Port { get; private set; } public string Password { get { return passBox.Text; } } public ConnectDialog(Settings s) { settings = s; AutoScaleDimensions = new SizeF(96F, 96F); AutoScaleMode = AutoScaleMode.Dpi; Text = "MacVNC - Connect"; FormBorderStyle = FormBorderStyle.FixedDialog; MaximizeBox = false; MinimizeBox = false; StartPosition = FormStartPosition.CenterScreen; AutoSize = true; AutoSizeMode = AutoSizeMode.GrowAndShrink; Padding = new Padding(10); Font = SystemFonts.MessageBoxFont; var grid = new TableLayoutPanel { ColumnCount = 2, AutoSize = true, AutoSizeMode = AutoSizeMode.GrowAndShrink, Location = new Point(10, 10) }; hostBox = new TextBox { Width = 300, Text = s.LastHost }; passBox = new TextBox { Width = 300, UseSystemPasswordChar = true }; grid.Controls.Add(MakeLabel("Mac address:"), 0, 0); grid.Controls.Add(hostBox, 1, 0); grid.Controls.Add(MakeHint("IP address or name, e.g. 192.168.1.20 or my-mac.local\r\n" + "(different port: my-mac.local::5901)"), 1, 1); grid.Controls.Add(MakeLabel("VNC password:"), 0, 2); grid.Controls.Add(passBox, 1, 2); grid.Controls.Add(MakeHint("Set on the Mac: System Settings > General > Sharing >\r\n" + "Screen Sharing (i) > Computer Settings > turn on\r\n" + "\"VNC viewers may control screen with password\".\r\n" + "Only the first 8 characters are used."), 1, 3); var buttons = new FlowLayoutPanel { FlowDirection = FlowDirection.RightToLeft, AutoSize = true, Anchor = AnchorStyles.Right, Margin = new Padding(0, 8, 0, 0) }; var cancel = new Button { Text = "Cancel", DialogResult = DialogResult.Cancel, AutoSize = true }; var ok = new Button { Text = "Connect", AutoSize = true }; buttons.Controls.Add(cancel); buttons.Controls.Add(ok); grid.Controls.Add(buttons, 1, 4); Controls.Add(grid); AcceptButton = ok; CancelButton = cancel; ok.Click += (o, e) => { string h; int p; if (!TryParseHost(hostBox.Text, out h, out p)) { MessageBox.Show(this, "Please enter the Mac's IP address or name.", Text, MessageBoxButtons.OK, MessageBoxIcon.Information); return; } HostName = h; Port = p; settings.LastHost = hostBox.Text.Trim(); DialogResult = DialogResult.OK; }; Shown += (o, e) => { if (hostBox.Text.Length > 0) passBox.Focus(); }; } static Label MakeLabel(string t) { return new Label { Text = t, AutoSize = true, Anchor = AnchorStyles.Left, Margin = new Padding(3, 7, 3, 3) }; } static Label MakeHint(string t) { return new Label { Text = t, AutoSize = true, ForeColor = SystemColors.GrayText, Margin = new Padding(3, 0, 3, 8) }; } // Accepts "host", "host:N" (VNC display N => port 5900+N, or a port if N >= 100) and "host::port". public static bool TryParseHost(string input, out string host, out int port) { host = null; port = 5900; input = (input ?? "").Trim(); if (input.Length == 0) return false; int dc = input.IndexOf("::", StringComparison.Ordinal); if (dc >= 0) { host = input.Substring(0, dc); if (!int.TryParse(input.Substring(dc + 2), out port)) return false; } else { int c = input.IndexOf(':'); if (c >= 0 && input.LastIndexOf(':') == c) { int n; host = input.Substring(0, c); if (!int.TryParse(input.Substring(c + 1), out n)) return false; port = n < 100 ? 5900 + n : n; } else host = input; } return host.Length > 0 && port > 0 && port < 65536; } } // ===================================================================================== // RFB (VNC) protocol client - no UI code in here // ===================================================================================== sealed class CursorShape { public int HotX, HotY, W, H; public int[] Argb; } sealed class Framebuffer { public readonly int W, H; public readonly int[] P; // 0x00RRGGBB, top-down rows public Framebuffer(int w, int h) { W = w; H = h; P = new int[Math.Max(1, w * h)]; } public void Fill(int x, int y, int w, int h, int c) { for (int r = 0; r < h; r++) { int o = (y + r) * W + x; for (int i = 0; i < w; i++) P[o + i] = c; } } public void Copy(int sx, int sy, int dx, int dy, int w, int h) { if (dy <= sy) for (int r = 0; r < h; r++) Array.Copy(P, (sy + r) * W + sx, P, (dy + r) * W + dx, w); else for (int r = h - 1; r >= 0; r--) Array.Copy(P, (sy + r) * W + sx, P, (dy + r) * W + dx, w); } } sealed class RfbClient { static readonly Encoding Latin1 = Encoding.GetEncoding(28591); TcpClient tcp; NetworkStream net; Stream rd; readonly object writeLock = new object(); volatile bool closing; byte[] buf = new byte[1 << 16]; byte[] zbuf = new byte[1 << 16]; readonly ZlibInflater zrle = new ZlibInflater(); readonly int[] palette = new int[128]; public Framebuffer Fb; public string DesktopName = ""; // Keep-alive: probe the link after this long with nothing received, then give up // if the Mac doesn't answer the probe. public int IdleProbeMs = 30000; public int ProbeTimeoutMs = 10000; volatile int lastRxTicks, probeSentTicks; volatile bool awaitingProbe; volatile string deadReason; int lastPingMs = -1; public int LastPingMs { get { return lastPingMs; } } public bool AwaitingProbe { get { return awaitingProbe; } } public bool Alive { get { return !closing && deadReason == null; } } public int SecondsSinceData { get { return Elapsed(lastRxTicks) / 1000; } } static int Elapsed(int since) { return unchecked(Environment.TickCount - since); } // Preferred first. Pseudo-encodings: -239 Cursor, -223 DesktopSize, -224 LastRect. public int[] Encodings = { 1, 16, 5, 2, 0, -239, -223, -224 }; public event Action> FrameUpdated; // raised on the network thread public event Action DesktopResized; // raised on the network thread public event Action CursorChanged; // raised on the network thread public event Action ClipboardReceived; // raised on the network thread public event Action Disconnected; // raised on the network thread // ---------------------------------------------------------------- connection public void Connect(string host, int port, string password, int timeoutMs) { tcp = new TcpClient(); IAsyncResult ar = tcp.BeginConnect(host, port, null, null); if (!ar.AsyncWaitHandle.WaitOne(timeoutMs)) { tcp.Close(); throw new IOException("Timed out connecting to " + host + ":" + port + ".\n\nCheck the address, and that Screen Sharing is turned on in the Mac's Sharing settings."); } tcp.EndConnect(ar); tcp.NoDelay = true; // Windows only probes idle sockets after two hours by default, which is no use // to us: ask for a probe after 30s of silence, retried every 5s. try { tcp.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.KeepAlive, true); var ka = new byte[12]; BitConverter.GetBytes((uint)1).CopyTo(ka, 0); BitConverter.GetBytes((uint)30000).CopyTo(ka, 4); BitConverter.GetBytes((uint)5000).CopyTo(ka, 8); tcp.Client.IOControl(IOControlCode.KeepAliveValues, ka, null); } catch { } tcp.ReceiveTimeout = timeoutMs; net = tcp.GetStream(); rd = new BufferedStream(net, 1 << 16); Handshake(password); tcp.ReceiveTimeout = 0; } public void Start() { SendSetPixelFormat(); SendSetEncodings(); RequestUpdate(false); lastRxTicks = Environment.TickCount; new Thread(ReadLoop) { IsBackground = true, Name = "RFB reader" }.Start(); new Thread(Watchdog) { IsBackground = true, Name = "RFB keep-alive" }.Start(); } // A non-incremental request for a single pixel: the server must answer straight away // even when nothing on screen has changed, which makes it a free round-trip ping. void SendProbe() { var m = new byte[10]; m[0] = 3; m[1] = 0; PutU16(m, 6, 1); PutU16(m, 8, 1); Send(m); } // A Shift tap: types nothing, changes nothing, but macOS counts it as user activity. public void SendKeepAwakeTap() { SendKey(0xffe1, true); SendKey(0xffe1, false); } void Watchdog() { while (!closing && deadReason == null) { Thread.Sleep(1000); if (closing || deadReason != null) return; if (awaitingProbe) { if (Elapsed(probeSentTicks) > ProbeTimeoutMs) { FailWith("The Mac stopped responding - the connection timed out."); return; } } else if (Elapsed(lastRxTicks) > IdleProbeMs) { probeSentTicks = Environment.TickCount; awaitingProbe = true; SendProbe(); } } } // Kill the socket so the reader thread unblocks and reports this reason. void FailWith(string reason) { deadReason = reason; try { if (tcp != null) tcp.Close(); } catch { } } public void Close() { closing = true; try { if (tcp != null) tcp.Close(); } catch { } } void Handshake(string password) { ReadExact(buf, 0, 12); string ver = Encoding.ASCII.GetString(buf, 0, 12); int major, minor; if (!ver.StartsWith("RFB ") || !int.TryParse(ver.Substring(4, 3), out major) || !int.TryParse(ver.Substring(8, 3), out minor) || major < 3) throw new IOException("That doesn't look like a VNC server (it said \"" + ver.Trim() + "\")."); // macOS reports "003.889" - it speaks standard 3.8 to other clients. int use = (major > 3 || minor >= 8) ? 8 : (minor >= 7 ? 7 : 3); Send(Encoding.ASCII.GetBytes("RFB 003.00" + use + "\n")); int sec; if (use >= 7) { int n = ReadU8(); if (n == 0) throw new IOException("The server refused the connection: " + ReadReason()); ReadExact(buf, 0, n); var offered = new List(); for (int i = 0; i < n; i++) offered.Add(buf[i]); if (offered.Contains(2)) sec = 2; else if (offered.Contains(1)) sec = 1; else throw new IOException(NoSupportedAuthMessage(offered)); Send(new byte[] { (byte)sec }); } else { sec = (int)ReadU32(); if (sec == 0) throw new IOException("The server refused the connection: " + ReadReason()); if (sec != 1 && sec != 2) throw new IOException(NoSupportedAuthMessage(new List { sec })); } if (sec == 2) { var challenge = new byte[16]; ReadExact(challenge, 0, 16); Send(VncAuthResponse(challenge, password)); } if (sec == 2 || use >= 8) { uint result = ReadU32(); if (result != 0) { string reason = ""; if (use >= 8) { try { reason = ReadReason(); } catch { } } throw new IOException("Login failed" + (reason.Length > 0 ? ": " + reason : ".") + "\n\nCheck the VNC password set in the Mac's Screen Sharing settings " + "(only the first 8 characters are used)."); } } Send(new byte[] { 1 }); // ClientInit: shared session int w = ReadU16(), h = ReadU16(); ReadExact(buf, 0, 16); // server's pixel format - we set our own int nameLen = (int)ReadU32(); if (nameLen < 0 || nameLen > 4096) throw new IOException("Bad desktop name from server."); ReadExact(buf, 0, nameLen); DesktopName = Encoding.UTF8.GetString(buf, 0, nameLen); Fb = new Framebuffer(w, h); } static string NoSupportedAuthMessage(List offered) { return "Your Mac only offered sign-in methods this viewer doesn't support (types " + string.Join(", ", offered.ConvertAll(i => i.ToString()).ToArray()) + ").\n\n" + "On the Mac, open System Settings > General > Sharing, click (i) next to Screen Sharing, " + "then Computer Settings, and turn on \"VNC viewers may control screen with password\". " + "Set a password (up to 8 characters) and use that here."; } string ReadReason() { int len = (int)ReadU32(); if (len < 0 || len > 65536) return "(unreadable reason)"; var b = new byte[len]; ReadExact(b, 0, len); return Encoding.UTF8.GetString(b); } public static byte[] VncAuthResponse(byte[] challenge, string password) { var key = new byte[8]; byte[] pw = Latin1.GetBytes(password ?? ""); for (int i = 0; i < 8 && i < pw.Length; i++) key[i] = ReverseBits(pw[i]); try { using (DES des = DES.Create()) { des.Mode = CipherMode.ECB; des.Padding = PaddingMode.None; using (ICryptoTransform enc = des.CreateEncryptor(key, new byte[8])) return enc.TransformFinalBlock(challenge, 0, 16); } } catch (CryptographicException) { throw new IOException("That password can't be used with VNC authentication (it makes a weak DES key). " + "Please set a different Screen Sharing password on the Mac."); } } static byte ReverseBits(byte b) { int r = 0; for (int i = 0; i < 8; i++) if ((b & (1 << i)) != 0) r |= 1 << (7 - i); return (byte)r; } // ---------------------------------------------------------------- receive loop void ReadLoop() { string reason = null; try { while (!closing) { int type = ReadU8(); switch (type) { case 0: ReadFramebufferUpdate(); break; case 1: // SetColourMapEntries - not used in true colour, just skip ReadU8(); ReadU16(); int n = ReadU16(); Skip(n * 6); break; case 2: break; // Bell case 3: ReadExact(buf, 0, 3); int len = (int)ReadU32(); if (len < 0 || len > 64 * 1024 * 1024) throw new InvalidDataException("Bad clipboard length."); var t = new byte[len]; ReadExact(t, 0, len); if (ClipboardReceived != null) ClipboardReceived(Latin1.GetString(t)); break; default: throw new InvalidDataException("Unexpected message type " + type + " from server."); } } } catch (Exception ex) { if (!closing) reason = deadReason ?? ((ex is EndOfStreamException) ? "The Mac closed the connection." : ex.Message); } if (!closing && Disconnected != null) Disconnected(reason ?? "Disconnected."); } void ReadFramebufferUpdate() { ReadU8(); int count = ReadU16(); var dirty = new List(); for (int i = 0; i < count; i++) { int x = ReadU16(), y = ReadU16(), w = ReadU16(), h = ReadU16(); int enc = ReadS32(); if (enc == -224) break; // LastRect if (enc == -223) // DesktopSize { Fb = new Framebuffer(w, h); if (DesktopResized != null) DesktopResized(w, h); dirty.Clear(); continue; } if (enc == -239) { ReadCursor(x, y, w, h); continue; } if (x + w > Fb.W || y + h > Fb.H) throw new InvalidDataException("Server sent a rectangle outside the screen."); switch (enc) { case 0: ReadRaw(x, y, w, h); break; case 1: ReadCopyRect(x, y, w, h); break; case 2: ReadRre(x, y, w, h); break; case 5: ReadHextile(x, y, w, h); break; case 16: ReadZrle(x, y, w, h); break; default: throw new InvalidDataException("Server used an unsupported encoding (" + enc + ")."); } if (w > 0 && h > 0) dirty.Add(new Rectangle(x, y, w, h)); } if (awaitingProbe) { lastPingMs = Elapsed(probeSentTicks); awaitingProbe = false; } if (dirty.Count > 0 && FrameUpdated != null) FrameUpdated(dirty); RequestUpdate(true); } void ReadRaw(int x, int y, int w, int h) { int rowBytes = w * 4; EnsureBuf(rowBytes); for (int r = 0; r < h; r++) { ReadExact(buf, 0, rowBytes); Buffer.BlockCopy(buf, 0, Fb.P, ((y + r) * Fb.W + x) * 4, rowBytes); } } void ReadCopyRect(int x, int y, int w, int h) { int sx = ReadU16(), sy = ReadU16(); if (sx + w > Fb.W || sy + h > Fb.H) throw new InvalidDataException("Bad CopyRect source."); Fb.Copy(sx, sy, x, y, w, h); } void ReadRre(int x, int y, int w, int h) { int n = (int)ReadU32(); Fb.Fill(x, y, w, h, ReadPixel()); for (int i = 0; i < n; i++) { int c = ReadPixel(); int sx = ReadU16(), sy = ReadU16(), sw = ReadU16(), sh = ReadU16(); if (sx + sw > w || sy + sh > h) throw new InvalidDataException("Bad RRE subrectangle."); Fb.Fill(x + sx, y + sy, sw, sh, c); } } void ReadHextile(int x, int y, int w, int h) { int bg = 0, fg = 0; for (int ty = y; ty < y + h; ty += 16) { int th = Math.Min(16, y + h - ty); for (int tx = x; tx < x + w; tx += 16) { int tw = Math.Min(16, x + w - tx); int mask = ReadU8(); if ((mask & 1) != 0) { ReadRaw(tx, ty, tw, th); continue; } if ((mask & 2) != 0) bg = ReadPixel(); Fb.Fill(tx, ty, tw, th, bg); if ((mask & 4) != 0) fg = ReadPixel(); if ((mask & 8) != 0) { int n = ReadU8(); bool coloured = (mask & 16) != 0; for (int s = 0; s < n; s++) { int c = coloured ? ReadPixel() : fg; int xy = ReadU8(), wh = ReadU8(); int sx = xy >> 4, sy = xy & 15, sw = (wh >> 4) + 1, sh = (wh & 15) + 1; if (sx >= tw || sy >= th) continue; if (sx + sw > tw) sw = tw - sx; if (sy + sh > th) sh = th - sy; Fb.Fill(tx + sx, ty + sy, sw, sh, c); } } } } } void ReadZrle(int x, int y, int w, int h) { int len = (int)ReadU32(); if (len < 0 || len > 256 * 1024 * 1024) throw new InvalidDataException("Bad ZRLE length."); if (zbuf.Length < len) zbuf = new byte[len]; ReadExact(zbuf, 0, len); int olen = zrle.Inflate(zbuf, len); byte[] d = zrle.Output; int p = 0; int[] P = Fb.P; int W = Fb.W; try { for (int ty = y; ty < y + h; ty += 64) { int th = Math.Min(64, y + h - ty); for (int tx = x; tx < x + w; tx += 64) { int tw = Math.Min(64, x + w - tx); int sub = d[p++]; if (sub == 0) { for (int r = 0; r < th; r++) { int o = (ty + r) * W + tx; for (int c = 0; c < tw; c++, p += 3) P[o + c] = d[p] | d[p + 1] << 8 | d[p + 2] << 16; } } else if (sub == 1) { Fb.Fill(tx, ty, tw, th, d[p] | d[p + 1] << 8 | d[p + 2] << 16); p += 3; } else if (sub <= 16) { for (int i = 0; i < sub; i++, p += 3) palette[i] = d[p] | d[p + 1] << 8 | d[p + 2] << 16; int bits = sub == 2 ? 1 : (sub <= 4 ? 2 : 4); int m = (1 << bits) - 1; for (int r = 0; r < th; r++) { int o = (ty + r) * W + tx; int b = 0, nb = 0; for (int c = 0; c < tw; c++) { if (nb == 0) { b = d[p++]; nb = 8; } nb -= bits; P[o + c] = palette[(b >> nb) & m]; } } } else if (sub == 128 || sub >= 130) { bool usePal = sub >= 130; if (usePal) for (int i = 0; i < sub - 128; i++, p += 3) palette[i] = d[p] | d[p + 1] << 8 | d[p + 2] << 16; int total = tw * th, done = 0, cx = 0, cy = 0; while (done < total) { int c, run; if (usePal) { int v = d[p++]; c = palette[v & 127]; if ((v & 128) != 0) { run = 1; int bb; do { bb = d[p++]; run += bb; } while (bb == 255); } else run = 1; } else { c = d[p] | d[p + 1] << 8 | d[p + 2] << 16; p += 3; run = 1; int bb; do { bb = d[p++]; run += bb; } while (bb == 255); } if (done + run > total) throw new InvalidDataException("ZRLE run too long."); done += run; while (run-- > 0) { P[(ty + cy) * W + tx + cx] = c; if (++cx == tw) { cx = 0; cy++; } } } } else throw new InvalidDataException("Bad ZRLE subencoding " + sub + "."); } } } catch (IndexOutOfRangeException) { throw new InvalidDataException("ZRLE data was truncated."); } if (p > olen) throw new InvalidDataException("ZRLE data was truncated."); } void ReadCursor(int hotX, int hotY, int w, int h) { int n = w * h; var pix = new byte[n * 4]; ReadExact(pix, 0, pix.Length); int maskRow = (w + 7) / 8; var mask = new byte[maskRow * h]; ReadExact(mask, 0, mask.Length); var argb = new int[n]; for (int yy = 0; yy < h; yy++) for (int xx = 0; xx < w; xx++) { int i = yy * w + xx; bool visible = (mask[yy * maskRow + (xx >> 3)] & (0x80 >> (xx & 7))) != 0; int c = pix[i * 4] | pix[i * 4 + 1] << 8 | pix[i * 4 + 2] << 16; argb[i] = visible ? (c | unchecked((int)0xFF000000)) : 0; } if (CursorChanged != null) CursorChanged(new CursorShape { HotX = hotX, HotY = hotY, W = w, H = h, Argb = argb }); } // ---------------------------------------------------------------- outgoing messages public void RequestUpdate(bool incremental) { Framebuffer fb = Fb; var m = new byte[10]; m[0] = 3; m[1] = (byte)(incremental ? 1 : 0); PutU16(m, 6, fb.W); PutU16(m, 8, fb.H); Send(m); } void SendSetPixelFormat() { // 32bpp, depth 24, little-endian, true colour, R<<16 G<<8 B - matches a Windows 32-bit DIB. var m = new byte[20]; m[4] = 32; m[5] = 24; m[6] = 0; m[7] = 1; PutU16(m, 8, 255); PutU16(m, 10, 255); PutU16(m, 12, 255); m[14] = 16; m[15] = 8; m[16] = 0; Send(m); } void SendSetEncodings() { var m = new byte[4 + 4 * Encodings.Length]; m[0] = 2; PutU16(m, 2, Encodings.Length); for (int i = 0; i < Encodings.Length; i++) PutU32(m, 4 + 4 * i, (uint)Encodings[i]); Send(m); } public void SendKey(uint keysym, bool down) { var m = new byte[8]; m[0] = 4; m[1] = (byte)(down ? 1 : 0); PutU32(m, 4, keysym); Send(m); } public void SendPointer(int buttons, int x, int y) { var m = new byte[6]; m[0] = 5; m[1] = (byte)buttons; PutU16(m, 2, x); PutU16(m, 4, y); Send(m); } public void SendClipboard(string text) { byte[] t = Latin1.GetBytes(text); var m = new byte[8 + t.Length]; m[0] = 6; PutU32(m, 4, (uint)t.Length); Buffer.BlockCopy(t, 0, m, 8, t.Length); Send(m); } void Send(byte[] m) { lock (writeLock) { try { net.Write(m, 0, m.Length); } catch (Exception) { if (closing) return; /* the reader thread will report the disconnect */ } } } // ---------------------------------------------------------------- helpers void EnsureBuf(int n) { if (buf.Length < n) buf = new byte[n]; } void ReadExact(byte[] b, int off, int n) { while (n > 0) { int r = rd.Read(b, off, n); if (r <= 0) throw new EndOfStreamException(); lastRxTicks = Environment.TickCount; off += r; n -= r; } } void Skip(int n) { while (n > 0) { int k = Math.Min(n, buf.Length); ReadExact(buf, 0, k); n -= k; } } int ReadU8() { int b = rd.ReadByte(); if (b < 0) throw new EndOfStreamException(); lastRxTicks = Environment.TickCount; return b; } int ReadU16() { ReadExact(buf, 0, 2); return buf[0] << 8 | buf[1]; } uint ReadU32() { ReadExact(buf, 0, 4); return (uint)(buf[0] << 24 | buf[1] << 16 | buf[2] << 8 | buf[3]); } int ReadS32() { return unchecked((int)ReadU32()); } int ReadPixel() { ReadExact(buf, 0, 4); return buf[0] | buf[1] << 8 | buf[2] << 16; } static void PutU16(byte[] b, int o, int v) { b[o] = (byte)(v >> 8); b[o + 1] = (byte)v; } static void PutU32(byte[] b, int o, uint v) { b[o] = (byte)(v >> 24); b[o + 1] = (byte)(v >> 16); b[o + 2] = (byte)(v >> 8); b[o + 3] = (byte)v; } } // ===================================================================================== // Minimal streaming zlib inflater for ZRLE. // ZRLE uses one zlib stream for the whole session, flushed at the end of each rectangle, // so we decode all complete deflate blocks in each chunk and keep the 32K history. // ===================================================================================== sealed class ZlibInflater { static readonly int[] LBase = { 3, 4, 5, 6, 7, 8, 9, 10, 11, 13, 15, 17, 19, 23, 27, 31, 35, 43, 51, 59, 67, 83, 99, 115, 131, 163, 195, 227, 258 }; static readonly int[] LExt = { 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 0 }; static readonly int[] DBase = { 1, 2, 3, 4, 5, 7, 9, 13, 17, 25, 33, 49, 65, 97, 129, 193, 257, 385, 513, 769, 1025, 1537, 2049, 3073, 4097, 6145, 8193, 12289, 16385, 24577 }; static readonly int[] DExt = { 0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 13, 13 }; static readonly int[] ClOrder = { 16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15 }; static readonly int[] FixedLit = new int[1 << 9], FixedDist = new int[1 << 5]; static readonly int FixedLitBits, FixedDistBits; readonly byte[] hist = new byte[32768]; int hpos; bool headerDone; byte[] inb; int ip, iend; uint bitbuf; int bitcnt; public byte[] Output = new byte[1 << 16]; int olen; readonly int[] litTab = new int[1 << 15], distTab = new int[1 << 15], clTab = new int[1 << 7]; readonly int[] lens = new int[320]; readonly int[] cl = new int[19]; static ZlibInflater() { var l = new int[288]; for (int i = 0; i < 288; i++) l[i] = i < 144 ? 8 : i < 256 ? 9 : i < 280 ? 7 : 8; FixedLitBits = Build(l, 0, 288, FixedLit); var d = new int[30]; for (int i = 0; i < 30; i++) d[i] = 5; FixedDistBits = Build(d, 0, 30, FixedDist); } // Decompresses all data in data[0..len). Returns the number of bytes now in Output. public int Inflate(byte[] data, int len) { inb = data; ip = 0; iend = len; olen = 0; if (!headerDone) { if (len < 2) throw new InvalidDataException("ZRLE: short zlib header."); if ((data[0] & 0x0F) != 8 || ((data[0] << 8) | data[1]) % 31 != 0) throw new InvalidDataException("ZRLE: bad zlib header."); ip = 2; headerDone = true; } while (ip < iend) Block(); return olen; } void Block() { Bits(1); // BFINAL - irrelevant, the stream never ends in ZRLE int type = Bits(2); if (type == 0) { int drop = bitcnt & 7; bitbuf >>= drop; bitcnt -= drop; int ln = Bits(16), nl = Bits(16); if ((ln ^ 0xFFFF) != nl) throw new InvalidDataException("ZRLE: bad stored block."); for (int i = 0; i < ln; i++) { byte b; if (bitcnt >= 8) { b = (byte)bitbuf; bitbuf >>= 8; bitcnt -= 8; } else { if (ip >= iend) throw new InvalidDataException("ZRLE: truncated data."); b = inb[ip++]; } Put(b); } } else if (type == 1) Codes(FixedLit, FixedLitBits, FixedDist, FixedDistBits); else if (type == 2) { int hlit = Bits(5) + 257, hdist = Bits(5) + 1, hclen = Bits(4) + 4; Array.Clear(cl, 0, 19); for (int i = 0; i < hclen; i++) cl[ClOrder[i]] = Bits(3); int clBits = Build(cl, 0, 19, clTab); int n = 0, total = hlit + hdist; while (n < total) { int sym = Decode(clTab, clBits); if (sym < 16) { lens[n++] = sym; continue; } int rep, val = 0; if (sym == 16) { if (n == 0) throw new InvalidDataException("ZRLE: bad code lengths."); val = lens[n - 1]; rep = 3 + Bits(2); } else if (sym == 17) rep = 3 + Bits(3); else rep = 11 + Bits(7); if (n + rep > total) throw new InvalidDataException("ZRLE: bad code lengths."); while (rep-- > 0) lens[n++] = val; } int lb = Build(lens, 0, hlit, litTab); int db = Build(lens, hlit, hdist, distTab); Codes(litTab, lb, distTab, db); } else throw new InvalidDataException("ZRLE: bad block type."); } void Codes(int[] lt, int lb, int[] dt, int db) { while (true) { int sym = Decode(lt, lb); if (sym < 256) { Put((byte)sym); continue; } if (sym == 256) return; sym -= 257; if (sym >= 29) throw new InvalidDataException("ZRLE: bad length code."); int len = LBase[sym] + Bits(LExt[sym]); int ds = Decode(dt, db); if (ds >= 30) throw new InvalidDataException("ZRLE: bad distance code."); int dist = DBase[ds] + Bits(DExt[ds]); if (olen + len > Output.Length) Grow(olen + len); for (int i = 0; i < len; i++) { byte b = hist[(hpos - dist) & 32767]; Output[olen++] = b; hist[hpos] = b; hpos = (hpos + 1) & 32767; } } } void Put(byte b) { if (olen == Output.Length) Grow(olen + 1); Output[olen++] = b; hist[hpos] = b; hpos = (hpos + 1) & 32767; } void Grow(int need) { int n = Output.Length; while (n < need) n *= 2; Array.Resize(ref Output, n); } int Bits(int n) { while (bitcnt < n) { if (ip >= iend) throw new InvalidDataException("ZRLE: truncated data."); bitbuf |= (uint)inb[ip++] << bitcnt; bitcnt += 8; } int v = (int)(bitbuf & ((1u << n) - 1)); bitbuf >>= n; bitcnt -= n; return v; } int Decode(int[] table, int bits) { while (bitcnt < bits && ip < iend) { bitbuf |= (uint)inb[ip++] << bitcnt; bitcnt += 8; } int e = table[(int)(bitbuf & ((1u << bits) - 1))]; int l = e & 15; if (l == 0 || l > bitcnt) throw new InvalidDataException("ZRLE: corrupt or truncated data."); bitbuf >>= l; bitcnt -= l; return e >> 4; } // Builds a direct lookup table (entry = symbol << 4 | length) for canonical Huffman codes. static int Build(int[] lengths, int off, int n, int[] table) { var count = new int[16]; int max = 0; for (int i = 0; i < n; i++) { int l = lengths[off + i]; count[l]++; if (l > max) max = l; } if (max == 0) { table[0] = 0; table[1] = 0; return 1; } count[0] = 0; var next = new int[16]; int code = 0; for (int b = 1; b <= 15; b++) { code = (code + count[b - 1]) << 1; next[b] = code; } int size = 1 << max; Array.Clear(table, 0, size); for (int i = 0; i < n; i++) { int len = lengths[off + i]; if (len == 0) continue; int c = next[len]++, r = 0; for (int k = 0; k < len; k++) { r = (r << 1) | (c & 1); c >>= 1; } for (int j = r; j < size; j += 1 << len) table[j] = (i << 4) | len; } return max; } } // ===================================================================================== // The remote screen: scaled drawing, mouse and keyboard // ===================================================================================== sealed class DisplayControl : Control { [StructLayout(LayoutKind.Sequential)] struct BITMAPINFOHEADER { public int biSize, biWidth, biHeight; public short biPlanes, biBitCount; public int biCompression, biSizeImage, biXPelsPerMeter, biYPelsPerMeter, biClrUsed, biClrImportant; } [StructLayout(LayoutKind.Sequential)] struct ICONINFO { public bool fIcon; public int xHotspot, yHotspot; public IntPtr hbmMask, hbmColor; } [DllImport("gdi32.dll")] static extern IntPtr CreateCompatibleDC(IntPtr hdc); [DllImport("gdi32.dll")] static extern bool DeleteDC(IntPtr hdc); [DllImport("gdi32.dll")] static extern IntPtr SelectObject(IntPtr hdc, IntPtr obj); [DllImport("gdi32.dll")] static extern bool DeleteObject(IntPtr obj); [DllImport("gdi32.dll")] static extern IntPtr CreateDIBSection(IntPtr hdc, ref BITMAPINFOHEADER bmi, uint usage, out IntPtr bits, IntPtr section, uint offset); [DllImport("gdi32.dll")] static extern bool BitBlt(IntPtr hdc, int x, int y, int w, int h, IntPtr src, int sx, int sy, int rop); [DllImport("gdi32.dll")] static extern bool StretchBlt(IntPtr hdc, int x, int y, int w, int h, IntPtr src, int sx, int sy, int sw, int sh, int rop); [DllImport("gdi32.dll")] static extern int SetStretchBltMode(IntPtr hdc, int mode); [DllImport("gdi32.dll")] static extern bool SetBrushOrgEx(IntPtr hdc, int x, int y, IntPtr prev); [DllImport("user32.dll")] static extern bool GetKeyboardState(byte[] state); [DllImport("user32.dll", CharSet = CharSet.Unicode)] static extern int ToUnicode(uint vk, uint scan, byte[] state, StringBuilder buf, int cch, uint flags); [DllImport("user32.dll")] static extern bool GetIconInfo(IntPtr hIcon, out ICONINFO info); [DllImport("user32.dll")] static extern IntPtr CreateIconIndirect(ref ICONINFO info); [DllImport("user32.dll")] static extern bool DestroyIcon(IntPtr h); [DllImport("user32.dll")] static extern bool DestroyCursor(IntPtr h); const int SRCCOPY = 0x00CC0020, HALFTONE = 4, COLORONCOLOR = 3; const int WM_KEYDOWN = 0x100, WM_KEYUP = 0x101, WM_SYSKEYDOWN = 0x104, WM_SYSKEYUP = 0x105, WM_CHAR = 0x102, WM_DEADCHAR = 0x103, WM_SYSCHAR = 0x106, WM_SYSDEADCHAR = 0x107, WM_MOUSEHWHEEL = 0x020E; RfbClient client; readonly object dibLock = new object(); IntPtr memDC, dib, oldBmp, bits; int fbW, fbH; public bool FitToWindow = true; public double FixedScale = 1.0; public bool Smooth = true; public bool CtrlIsCmd = true; public bool CmdAsMeta = false; readonly object invLock = new object(); Rectangle pendingInvalid = Rectangle.Empty; bool invalidatePosted; int buttonMask, lastFx, lastFy, wheelAccum, hwheelAccum; readonly Dictionary pressed = new Dictionary(); CursorShape cursorShape; IntPtr cursorHandle; public event EventHandler FramebufferSizeChanged; public event EventHandler FullScreenToggleRequested; public int FbWidth { get { return fbW; } } public int FbHeight { get { return fbH; } } int lastInputTicks = Environment.TickCount; public int SecondsSinceUserInput { get { return unchecked(Environment.TickCount - lastInputTicks) / 1000; } } // Point the view at a freshly reconnected session. public void AttachClient(RfbClient c) { Unsubscribe(); pressed.Clear(); buttonMask = 0; client = c; Subscribe(); lock (dibLock) CreateDib(c.Fb.W, c.Fb.H); if (FramebufferSizeChanged != null) FramebufferSizeChanged(this, EventArgs.Empty); Invalidate(); } void Subscribe() { client.FrameUpdated += OnFrameUpdated; client.DesktopResized += OnDesktopResized; client.CursorChanged += OnCursorChanged; } void Unsubscribe() { client.FrameUpdated -= OnFrameUpdated; client.DesktopResized -= OnDesktopResized; client.CursorChanged -= OnCursorChanged; } public DisplayControl(RfbClient c) { client = c; SetStyle(ControlStyles.UserPaint | ControlStyles.AllPaintingInWmPaint | ControlStyles.Opaque | ControlStyles.Selectable | ControlStyles.ResizeRedraw, true); SetStyle(ControlStyles.OptimizedDoubleBuffer, false); TabStop = true; BackColor = Color.Black; lock (dibLock) CreateDib(c.Fb.W, c.Fb.H); Subscribe(); } // ---------------------------------------------------------------- back buffer void CreateDib(int w, int h) { if (memDC == IntPtr.Zero) memDC = CreateCompatibleDC(IntPtr.Zero); if (dib != IntPtr.Zero) { SelectObject(memDC, oldBmp); DeleteObject(dib); dib = IntPtr.Zero; } var bi = new BITMAPINFOHEADER { biSize = 40, biWidth = Math.Max(1, w), biHeight = -Math.Max(1, h), biPlanes = 1, biBitCount = 32 }; dib = CreateDIBSection(memDC, ref bi, 0, out bits, IntPtr.Zero, 0); if (dib == IntPtr.Zero) throw new OutOfMemoryException("Couldn't create a " + w + "x" + h + " screen buffer."); oldBmp = SelectObject(memDC, dib); fbW = w; fbH = h; } void OnFrameUpdated(List rects) { Framebuffer fb = client.Fb; Rectangle union = Rectangle.Empty; lock (dibLock) { if (fb.W != fbW || fb.H != fbH) return; long basePtr = bits.ToInt64(); foreach (Rectangle r in rects) { for (int row = 0; row < r.Height; row++) { int idx = (r.Y + row) * fbW + r.X; Marshal.Copy(fb.P, idx, new IntPtr(basePtr + (long)idx * 4), r.Width); } union = union.IsEmpty ? r : Rectangle.Union(union, r); } } InvalidateFb(union); } void OnDesktopResized(int w, int h) { lock (dibLock) CreateDib(w, h); PostToUi(() => { if (FramebufferSizeChanged != null) FramebufferSizeChanged(this, EventArgs.Empty); Invalidate(); }); } void PostToUi(MethodInvoker action) { try { if (IsHandleCreated && !IsDisposed) BeginInvoke(action); } catch (InvalidOperationException) { } } void InvalidateFb(Rectangle r) { lock (invLock) { pendingInvalid = pendingInvalid.IsEmpty ? r : Rectangle.Union(pendingInvalid, r); if (invalidatePosted) return; invalidatePosted = true; } PostToUi(FlushInvalidate); } void FlushInvalidate() { Rectangle r; lock (invLock) { r = pendingInvalid; pendingInvalid = Rectangle.Empty; invalidatePosted = false; } if (r.IsEmpty) return; int ox, oy, dw, dh; double s; GetDest(out ox, out oy, out dw, out dh, out s); var cr = Rectangle.FromLTRB(ox + (int)Math.Floor(r.Left * s) - 2, oy + (int)Math.Floor(r.Top * s) - 2, ox + (int)Math.Ceiling(r.Right * s) + 2, oy + (int)Math.Ceiling(r.Bottom * s) + 2); Invalidate(cr); } // ---------------------------------------------------------------- geometry public double GetScale() { if (!FitToWindow) return FixedScale; if (fbW <= 0 || fbH <= 0 || ClientSize.Width <= 0 || ClientSize.Height <= 0) return 1.0; return Math.Min((double)ClientSize.Width / fbW, (double)ClientSize.Height / fbH); } void GetDest(out int ox, out int oy, out int dw, out int dh, out double s) { s = GetScale(); dw = Math.Max(1, (int)Math.Round(fbW * s)); dh = Math.Max(1, (int)Math.Round(fbH * s)); ox = Math.Max(0, (ClientSize.Width - dw) / 2); oy = Math.Max(0, (ClientSize.Height - dh) / 2); } static int Clamp(int v, int lo, int hi) { return v < lo ? lo : (v > hi ? hi : v); } protected override void OnPaint(PaintEventArgs e) { int ox, oy, dw, dh; double s; GetDest(out ox, out oy, out dw, out dh, out s); var dest = new Rectangle(ox, oy, dw, dh); using (var bars = new Region(e.ClipRectangle)) { bars.Exclude(dest); e.Graphics.FillRegion(Brushes.Black, bars); } Rectangle clip = Rectangle.Intersect(e.ClipRectangle, dest); if (clip.IsEmpty) return; // Only redraw the part of the screen that changed. int sx0 = Clamp((int)Math.Floor((clip.Left - ox) / s) - 1, 0, fbW); int sx1 = Clamp((int)Math.Ceiling((clip.Right - ox) / s) + 1, 0, fbW); int sy0 = Clamp((int)Math.Floor((clip.Top - oy) / s) - 1, 0, fbH); int sy1 = Clamp((int)Math.Ceiling((clip.Bottom - oy) / s) + 1, 0, fbH); if (sx1 <= sx0 || sy1 <= sy0) return; int dx0 = ox + (int)Math.Round(sx0 * s), dx1 = ox + (int)Math.Round(sx1 * s); int dy0 = oy + (int)Math.Round(sy0 * s), dy1 = oy + (int)Math.Round(sy1 * s); IntPtr hdc = e.Graphics.GetHdc(); try { lock (dibLock) { if (Math.Abs(s - 1.0) < 1e-9) BitBlt(hdc, dx0, dy0, sx1 - sx0, sy1 - sy0, memDC, sx0, sy0, SRCCOPY); else { SetStretchBltMode(hdc, Smooth ? HALFTONE : COLORONCOLOR); SetBrushOrgEx(hdc, 0, 0, IntPtr.Zero); StretchBlt(hdc, dx0, dy0, dx1 - dx0, dy1 - dy0, memDC, sx0, sy0, sx1 - sx0, sy1 - sy0, SRCCOPY); } } } finally { e.Graphics.ReleaseHdc(hdc); } } protected override void OnResize(EventArgs e) { base.OnResize(e); if (FitToWindow) RebuildCursor(); } // ---------------------------------------------------------------- local cursor void OnCursorChanged(CursorShape shape) { cursorShape = shape; PostToUi(RebuildCursor); } public void RebuildCursor() { CursorShape shape = cursorShape; if (shape == null || !IsHandleCreated) return; IntPtr handle = IntPtr.Zero; try { double s = GetScale(); bool empty = shape.W == 0 || shape.H == 0; int w = empty ? 1 : Clamp((int)Math.Round(shape.W * s), 1, 256); int h = empty ? 1 : Clamp((int)Math.Round(shape.H * s), 1, 256); using (var dst = new Bitmap(w, h, PixelFormat.Format32bppArgb)) { using (Graphics g = Graphics.FromImage(dst)) { g.Clear(Color.Transparent); if (!empty) { using (var src = new Bitmap(shape.W, shape.H, PixelFormat.Format32bppArgb)) { BitmapData bd = src.LockBits(new Rectangle(0, 0, shape.W, shape.H), ImageLockMode.WriteOnly, PixelFormat.Format32bppArgb); for (int row = 0; row < shape.H; row++) Marshal.Copy(shape.Argb, row * shape.W, new IntPtr(bd.Scan0.ToInt64() + (long)row * bd.Stride), shape.W); src.UnlockBits(bd); g.InterpolationMode = s < 1.0 ? InterpolationMode.HighQualityBicubic : InterpolationMode.NearestNeighbor; g.PixelOffsetMode = PixelOffsetMode.Half; g.DrawImage(src, new Rectangle(0, 0, w, h)); } } } IntPtr hIcon = dst.GetHicon(); ICONINFO ii; GetIconInfo(hIcon, out ii); DestroyIcon(hIcon); ii.fIcon = false; ii.xHotspot = empty ? 0 : Clamp((int)Math.Round(shape.HotX * s), 0, w - 1); ii.yHotspot = empty ? 0 : Clamp((int)Math.Round(shape.HotY * s), 0, h - 1); handle = CreateIconIndirect(ref ii); if (ii.hbmMask != IntPtr.Zero) DeleteObject(ii.hbmMask); if (ii.hbmColor != IntPtr.Zero) DeleteObject(ii.hbmColor); } } catch { handle = IntPtr.Zero; } if (handle == IntPtr.Zero) return; IntPtr old = cursorHandle; cursorHandle = handle; Cursor = new Cursor(handle); if (old != IntPtr.Zero) DestroyCursor(old); } // ---------------------------------------------------------------- mouse void SendPointerAt(Point p) { lastInputTicks = Environment.TickCount; int ox, oy, dw, dh; double s; GetDest(out ox, out oy, out dw, out dh, out s); lastFx = Clamp((int)((p.X - ox) / s), 0, Math.Max(0, fbW - 1)); lastFy = Clamp((int)((p.Y - oy) / s), 0, Math.Max(0, fbH - 1)); client.SendPointer(buttonMask, lastFx, lastFy); } static int ButtonBit(MouseButtons b) { switch (b) { case MouseButtons.Left: return 1; case MouseButtons.Middle: return 2; case MouseButtons.Right: return 4; default: return 0; } } protected override void OnMouseMove(MouseEventArgs e) { SendPointerAt(e.Location); } protected override void OnMouseDown(MouseEventArgs e) { if (!Focused) Focus(); buttonMask |= ButtonBit(e.Button); SendPointerAt(e.Location); } protected override void OnMouseUp(MouseEventArgs e) { buttonMask &= ~ButtonBit(e.Button); SendPointerAt(e.Location); } protected override void OnMouseWheel(MouseEventArgs e) { lastInputTicks = Environment.TickCount; wheelAccum += e.Delta; while (wheelAccum >= 120) { ClickButton(8); wheelAccum -= 120; } // scroll up (button 4) while (wheelAccum <= -120) { ClickButton(16); wheelAccum += 120; } // scroll down (button 5) } void ClickButton(int bit) { client.SendPointer(buttonMask | bit, lastFx, lastFy); client.SendPointer(buttonMask, lastFx, lastFy); } // ---------------------------------------------------------------- keyboard public override bool PreProcessMessage(ref Message msg) { // Send every key (Tab, arrows, Alt, F10...) straight to the Mac instead of letting WinForms eat it. if (msg.Msg >= WM_KEYDOWN && msg.Msg <= WM_SYSDEADCHAR) return false; return base.PreProcessMessage(ref msg); } protected override void WndProc(ref Message m) { switch (m.Msg) { case WM_KEYDOWN: case WM_SYSKEYDOWN: HandleKey(m, true); return; case WM_KEYUP: case WM_SYSKEYUP: HandleKey(m, false); return; case WM_CHAR: case WM_SYSCHAR: case WM_DEADCHAR: case WM_SYSDEADCHAR: return; case WM_MOUSEHWHEEL: hwheelAccum += (short)((m.WParam.ToInt64() >> 16) & 0xFFFF); while (hwheelAccum >= 120) { ClickButton(64); hwheelAccum -= 120; } // right (button 7) while (hwheelAccum <= -120) { ClickButton(32); hwheelAccum += 120; } // left (button 6) m.Result = new IntPtr(1); return; } base.WndProc(ref m); } void HandleKey(Message m, bool down) { int vk = (int)(m.WParam.ToInt64() & 0xFFFF); long lp = m.LParam.ToInt64(); int scan = (int)((lp >> 16) & 0xFF); bool ext = ((lp >> 24) & 1) != 0; bool repeat = ((lp >> 30) & 1) != 0; lastInputTicks = Environment.TickCount; if (vk == 0x7A) // F11 toggles full screen locally { if (down && !repeat && FullScreenToggleRequested != null) FullScreenToggleRequested(this, EventArgs.Empty); return; } if (vk == 0x14) return; // Caps Lock is applied locally - letters arrive already capitalised int id = vk | (scan << 16) | (ext ? 1 << 24 : 0); uint[] syms; if (down) { if (!pressed.TryGetValue(id, out syms)) { syms = Translate(vk, scan, ext); if (syms == null) return; pressed[id] = syms; } foreach (uint s in syms) client.SendKey(s, true); } else { if (pressed.TryGetValue(id, out syms)) pressed.Remove(id); else if ((syms = Translate(vk, scan, ext)) == null) return; for (int i = syms.Length - 1; i >= 0; i--) client.SendKey(syms[i], false); } } uint CmdSym { get { return CmdAsMeta ? 0xffe7u : 0xffebu; } } // Meta_L or Super_L uint[] Translate(int vk, int scan, bool ext) { uint k = 0; switch (vk) { case 0x08: k = 0xff08; break; // Backspace case 0x09: k = 0xff09; break; // Tab case 0x0D: k = ext ? 0xff8du : 0xff0du; break; // Enter / keypad Enter case 0x1B: k = 0xff1b; break; // Esc case 0x21: k = 0xff55; break; // Page Up case 0x22: k = 0xff56; break; // Page Down case 0x23: k = 0xff57; break; // End case 0x24: k = 0xff50; break; // Home case 0x25: k = 0xff51; break; // Left case 0x26: k = 0xff52; break; // Up case 0x27: k = 0xff53; break; // Right case 0x28: k = 0xff54; break; // Down case 0x2D: k = 0xff63; break; // Insert case 0x2E: k = 0xffff; break; // Delete (forward delete) case 0x10: k = scan == 0x36 ? 0xffe2u : 0xffe1u; break; // Shift R / L case 0x11: k = CtrlIsCmd ? CmdSym : (ext ? 0xffe4u : 0xffe3u); break; // Ctrl case 0x12: k = ext ? 0xffeau : 0xffe9u; break; // Alt = Option case 0x5B: case 0x5C: k = CtrlIsCmd ? 0xffe3u : CmdSym; break; // Windows key case 0x5D: k = 0xff67; break; // Menu key case 0x6A: k = 0xffaa; break; // keypad * case 0x6B: k = 0xffab; break; // keypad + case 0x6D: k = 0xffad; break; // keypad - case 0x6E: k = 0xffae; break; // keypad . case 0x6F: k = 0xffaf; break; // keypad / case 0x90: k = 0xff7f; break; // Num Lock (Clear on a Mac) } if (vk >= 0x70 && vk <= 0x87) k = (uint)(0xffbe + (vk - 0x70)); // F1-F24 if (vk >= 0x60 && vk <= 0x69) k = (uint)(0xffb0 + (vk - 0x60)); // keypad 0-9 if (k != 0) return new[] { k }; // Printable key: ask Windows what character it makes with the current Shift / Caps // state but without Ctrl and Alt, so Ctrl+C / Cmd+C arrive as a modifier plus 'c'. var state = new byte[256]; GetKeyboardState(state); state[0x11] = state[0xA2] = state[0xA3] = 0; state[0x12] = state[0xA4] = state[0xA5] = 0; var sb = new StringBuilder(8); int n = ToUnicode((uint)vk, (uint)scan, state, sb, sb.Capacity, 4); if (n < 1) return null; char c = sb[0]; if (c < 0x20 || c == 0x7F) return null; return new[] { c < 0x100 ? (uint)c : 0x01000000u | c }; } protected override void OnLostFocus(EventArgs e) { base.OnLostFocus(e); ReleaseAll(); } public void ReleaseAll() { foreach (uint[] syms in pressed.Values) for (int i = syms.Length - 1; i >= 0; i--) client.SendKey(syms[i], false); pressed.Clear(); if (buttonMask != 0) { buttonMask = 0; client.SendPointer(0, lastFx, lastFy); } } protected override void Dispose(bool disposing) { Unsubscribe(); lock (dibLock) { if (dib != IntPtr.Zero) { SelectObject(memDC, oldBmp); DeleteObject(dib); dib = IntPtr.Zero; } if (memDC != IntPtr.Zero) { DeleteDC(memDC); memDC = IntPtr.Zero; } } if (cursorHandle != IntPtr.Zero) { Cursor = Cursors.Default; DestroyCursor(cursorHandle); cursorHandle = IntPtr.Zero; } base.Dispose(disposing); } } // ===================================================================================== // Main viewer window // ===================================================================================== sealed class ViewerForm : Form { static readonly int[] Zooms = { 25, 33, 50, 67, 75, 100, 125, 150, 200 }; RfbClient client; readonly ConnectionInfo info; readonly Settings settings; readonly DisplayControl display; readonly Panel host; readonly ToolStrip bar; readonly ToolStripComboBox scaleBox; readonly ToolStripButton smoothBtn; readonly ToolStripMenuItem ctrlCmdItem, superItem, metaItem, keepAwakeItem, autoReconnectItem; readonly ToolStripLabel statusLabel; readonly Panel reconnectBar; readonly Label reconnectLabel; readonly System.Windows.Forms.Timer tick; volatile bool reconnecting, cancelReconnect, shuttingDown; int lastNudgeTicks = Environment.TickCount; const int AwakeIdleSeconds = 120; // only nudge after this long with no input from you const int AwakeRepeatSeconds = 60; // ...and at most this often bool fullScreen; FormWindowState prevState; Rectangle prevBounds; string lastClipSent, lastClipReceived; public bool ReturnToConnect; public ViewerForm(RfbClient c, Settings s, ConnectionInfo ci) { client = c; settings = s; info = ci; StartPosition = FormStartPosition.CenterScreen; KeyPreview = false; BackColor = Color.Black; bar = new ToolStrip { GripStyle = ToolStripGripStyle.Hidden, Dock = DockStyle.Top }; bar.Items.Add(new ToolStripLabel("Scale:")); scaleBox = new ToolStripComboBox { DropDownStyle = ComboBoxStyle.DropDownList, AutoSize = false, Width = 120 }; scaleBox.Items.Add("Fit to window"); foreach (int z in Zooms) scaleBox.Items.Add(z + "%"); bar.Items.Add(scaleBox); smoothBtn = new ToolStripButton("Smooth") { CheckOnClick = true, Checked = s.Smooth, ToolTipText = "Smooth (higher quality) scaling" }; bar.Items.Add(smoothBtn); bar.Items.Add(new ToolStripSeparator()); var kb = new ToolStripDropDownButton("Keyboard"); ctrlCmdItem = new ToolStripMenuItem("Ctrl key acts as Cmd (Ctrl+C = copy on the Mac)") { CheckOnClick = true, Checked = s.CtrlIsCmd }; superItem = new ToolStripMenuItem("Cmd sends Super (default)") { Checked = !s.CmdAsMeta }; metaItem = new ToolStripMenuItem("Cmd sends Meta (try this if Cmd doesn't work)") { Checked = s.CmdAsMeta }; kb.DropDownItems.Add(ctrlCmdItem); kb.DropDownItems.Add(new ToolStripSeparator()); kb.DropDownItems.Add(superItem); kb.DropDownItems.Add(metaItem); bar.Items.Add(kb); var ka = new ToolStripDropDownButton("Keep alive"); keepAwakeItem = new ToolStripMenuItem("Keep the Mac awake (tap Shift when idle)") { CheckOnClick = true, Checked = s.KeepAwake }; autoReconnectItem = new ToolStripMenuItem("Reconnect automatically") { CheckOnClick = true, Checked = s.AutoReconnect }; ka.DropDownItems.Add(keepAwakeItem); ka.DropDownItems.Add(autoReconnectItem); bar.Items.Add(ka); var fsBtn = new ToolStripButton("Full screen (F11)"); bar.Items.Add(fsBtn); var discBtn = new ToolStripButton("Disconnect") { Alignment = ToolStripItemAlignment.Right }; bar.Items.Add(discBtn); statusLabel = new ToolStripLabel("connecting") { Alignment = ToolStripItemAlignment.Right, ToolTipText = "Link status and last round-trip time" }; bar.Items.Add(statusLabel); reconnectLabel = new Label { Dock = DockStyle.Fill, TextAlign = ContentAlignment.MiddleLeft, Padding = new Padding(8, 0, 0, 0), ForeColor = Color.FromArgb(120, 60, 0) }; var cancelRc = new Button { Text = "Stop trying", Dock = DockStyle.Right, Width = 100, FlatStyle = FlatStyle.System }; reconnectBar = new Panel { Dock = DockStyle.Top, Height = 28, BackColor = Color.FromArgb(255, 244, 214), Visible = false }; reconnectBar.Controls.Add(reconnectLabel); reconnectBar.Controls.Add(cancelRc); cancelRc.Click += (o, e) => { cancelReconnect = true; }; host = new Panel { Dock = DockStyle.Fill, AutoScroll = true, BackColor = Color.Black }; display = new DisplayControl(client) { Smooth = s.Smooth, CtrlIsCmd = s.CtrlIsCmd, CmdAsMeta = s.CmdAsMeta }; host.Controls.Add(display); Controls.Add(host); Controls.Add(reconnectBar); Controls.Add(bar); // Initial window size: whole remote screen, shrunk to fit this monitor if needed. Rectangle wa = Screen.PrimaryScreen.WorkingArea; int barH = bar.PreferredSize.Height; double fit = Math.Min(1.0, Math.Min(wa.Width * 0.9 / c.Fb.W, (wa.Height * 0.9 - barH) / c.Fb.H)); ClientSize = new Size(Math.Max(400, (int)(c.Fb.W * fit)), Math.Max(300, (int)(c.Fb.H * fit) + barH)); int sel = 0; for (int i = 0; i < Zooms.Length; i++) if (s.Scale == Zooms[i].ToString()) sel = i + 1; scaleBox.SelectedIndex = sel; ApplyScale(); scaleBox.SelectedIndexChanged += (o, e) => { ApplyScale(); display.Focus(); }; smoothBtn.CheckedChanged += (o, e) => { display.Smooth = settings.Smooth = smoothBtn.Checked; display.Invalidate(); display.Focus(); }; ctrlCmdItem.CheckedChanged += (o, e) => { display.ReleaseAll(); display.CtrlIsCmd = settings.CtrlIsCmd = ctrlCmdItem.Checked; }; superItem.Click += (o, e) => SetCmdMeta(false); metaItem.Click += (o, e) => SetCmdMeta(true); fsBtn.Click += (o, e) => ToggleFullScreen(); discBtn.Click += (o, e) => { shuttingDown = true; ReturnToConnect = true; Close(); }; host.Resize += (o, e) => { if (!display.FitToWindow) PositionFixed(); UpdateTitle(); }; display.Resize += (o, e) => UpdateTitle(); display.FramebufferSizeChanged += (o, e) => ApplyScale(); display.FullScreenToggleRequested += (o, e) => ToggleFullScreen(); keepAwakeItem.CheckedChanged += (o, e) => { settings.KeepAwake = keepAwakeItem.Checked; lastNudgeTicks = Environment.TickCount; }; autoReconnectItem.CheckedChanged += (o, e) => settings.AutoReconnect = autoReconnectItem.Checked; WireClient(client); tick = new System.Windows.Forms.Timer { Interval = 1000 }; tick.Tick += OnTick; tick.Start(); UpdateTitle(); } void PostToUi(MethodInvoker a) { try { if (IsHandleCreated && !IsDisposed) BeginInvoke(a); } catch (InvalidOperationException) { } } void WireClient(RfbClient c) { c.Disconnected += reason => PostToUi(() => OnClientLost(c, reason)); c.ClipboardReceived += text => PostToUi(() => { lastClipReceived = lastClipSent = text; try { if (text.Length > 0) Clipboard.SetText(text.Replace("\r\n", "\n").Replace("\n", "\r\n")); } catch { } }); } void OnClientLost(RfbClient who, string reason) { if (who != client || shuttingDown || reconnecting) return; if (!autoReconnectItem.Checked) { MessageBox.Show(this, reason, "MacVNC - disconnected", MessageBoxButtons.OK, MessageBoxIcon.Information); ReturnToConnect = true; Close(); return; } BeginReconnect(reason); } // ---------------------------------------------------------------- keep alive void OnTick(object sender, EventArgs e) { if (shuttingDown) return; RfbClient c = client; if (reconnecting) { statusLabel.Text = "reconnecting"; statusLabel.ForeColor = Color.FromArgb(160, 80, 0); return; } if (c == null || !c.Alive) { statusLabel.Text = "offline"; statusLabel.ForeColor = Color.FromArgb(150, 40, 40); return; } int ms = c.LastPingMs; if (c.AwaitingProbe) { statusLabel.Text = "checking link"; statusLabel.ForeColor = Color.FromArgb(160, 80, 0); } else if (ms < 0) { statusLabel.Text = "connected"; statusLabel.ForeColor = Color.FromArgb(20, 110, 60); } else { statusLabel.Text = "link " + ms + " ms"; statusLabel.ForeColor = ms < 250 ? Color.FromArgb(20, 110, 60) : Color.FromArgb(160, 80, 0); } if (keepAwakeItem.Checked && display.SecondsSinceUserInput >= AwakeIdleSeconds && unchecked(Environment.TickCount - lastNudgeTicks) >= AwakeRepeatSeconds * 1000) { c.SendKeepAwakeTap(); lastNudgeTicks = Environment.TickCount; } } // ---------------------------------------------------------------- reconnecting void BeginReconnect(string reason) { reconnecting = true; cancelReconnect = false; display.ReleaseAll(); client.Close(); reconnectLabel.Text = "Connection lost: " + reason + " Reconnecting..."; reconnectBar.Visible = true; new Thread(ReconnectLoop) { IsBackground = true, Name = "MacVNC reconnect" }.Start(); } void ReconnectLoop() { int[] delays = { 1, 2, 4, 8, 15, 15, 30 }; for (int attempt = 0; !cancelReconnect && !shuttingDown; attempt++) { int n = attempt + 1; PostToUi(() => reconnectLabel.Text = "Reconnecting to " + info.Host + " (attempt " + n + ")..."); var c = new RfbClient(); try { c.Connect(info.Host, info.Port, info.Password, 8000); } catch (Exception ex) { c.Close(); int wait = delays[Math.Min(attempt, delays.Length - 1)]; string msg = ex.Message.Split('\n')[0]; PostToUi(() => reconnectLabel.Text = "Attempt " + n + " failed: " + msg + " Retrying in " + wait + "s..."); for (int i = 0; i < wait * 10 && !cancelReconnect && !shuttingDown; i++) Thread.Sleep(100); continue; } PostToUi(() => FinishReconnect(c)); return; } PostToUi(() => { reconnecting = false; reconnectBar.Visible = false; if (shuttingDown) return; ReturnToConnect = true; Close(); }); } void FinishReconnect(RfbClient c) { if (shuttingDown) { c.Close(); return; } client = c; WireClient(c); display.AttachClient(c); c.Start(); reconnecting = false; reconnectBar.Visible = false; ApplyScale(); UpdateTitle(); display.Focus(); } void SetCmdMeta(bool meta) { display.ReleaseAll(); display.CmdAsMeta = settings.CmdAsMeta = meta; superItem.Checked = !meta; metaItem.Checked = meta; } void ApplyScale() { int i = scaleBox.SelectedIndex; if (i <= 0) { settings.Scale = "Fit"; display.FitToWindow = true; host.AutoScrollMinSize = Size.Empty; display.Dock = DockStyle.Fill; } else { int z = Zooms[i - 1]; settings.Scale = z.ToString(); display.FitToWindow = false; display.FixedScale = z / 100.0; display.Dock = DockStyle.None; PositionFixed(); } display.Invalidate(); display.RebuildCursor(); UpdateTitle(); } void PositionFixed() { int w = (int)Math.Round(display.FbWidth * display.FixedScale); int h = (int)Math.Round(display.FbHeight * display.FixedScale); display.Size = new Size(w, h); Size cs = host.ClientSize; display.Location = new Point(Math.Max(0, (cs.Width - w) / 2) + host.AutoScrollPosition.X, Math.Max(0, (cs.Height - h) / 2) + host.AutoScrollPosition.Y); } void UpdateTitle() { int pct = (int)Math.Round(display.GetScale() * 100); string name = client.DesktopName.Length > 0 ? client.DesktopName : "Mac"; Text = name + " - MacVNC - " + (display.FitToWindow ? "Fit (" + pct + "%)" : pct + "%"); } void ToggleFullScreen() { if (!fullScreen) { prevState = WindowState; prevBounds = WindowState == FormWindowState.Normal ? Bounds : RestoreBounds; bar.Visible = false; FormBorderStyle = FormBorderStyle.None; WindowState = FormWindowState.Normal; Bounds = Screen.FromControl(this).Bounds; fullScreen = true; } else { FormBorderStyle = FormBorderStyle.Sizable; Bounds = prevBounds; WindowState = prevState; bar.Visible = true; fullScreen = false; } display.Focus(); } void SyncClipboardToMac() { try { if (!Clipboard.ContainsText()) return; string t = Clipboard.GetText().Replace("\r\n", "\n"); if (t == lastClipSent || t == lastClipReceived) return; lastClipSent = t; client.SendClipboard(t); } catch { } } protected override void OnShown(EventArgs e) { base.OnShown(e); client.Start(); display.Focus(); } protected override void OnActivated(EventArgs e) { base.OnActivated(e); SyncClipboardToMac(); display.Focus(); } protected override void OnFormClosed(FormClosedEventArgs e) { shuttingDown = true; cancelReconnect = true; if (tick != null) tick.Stop(); client.Close(); base.OnFormClosed(e); } } }