JJServiceManagerEnt/Windows/RemoteScreenWindows.cs

484 lines
19 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.Json.Nodes;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Threading;
using JJServiceManager.Remote;
namespace JJServiceManager
{
/// <summary>대상 PC: 원격 화면 연결 승인 창 (30초 안에 답하지 않으면 거부)</summary>
public sealed class ApprovalWindow : Window
{
private readonly TaskCompletionSource<bool> _result = new TaskCompletionSource<bool>();
private readonly DispatcherTimer _timer;
private readonly TextBlock _countdown;
private int _left = 30;
private ApprovalWindow(string who, bool control)
{
Title = "원격관리 연결 요청" + AppInstance.TitleTag;
Width = 460;
SizeToContent = SizeToContent.Height;
ResizeMode = ResizeMode.NoResize;
WindowStartupLocation = WindowStartupLocation.CenterScreen;
Topmost = true;
FontFamily = new FontFamily("Malgun Gothic");
FontSize = 13;
Background = Brushes2.Make("#FAFAFA");
AppIcon.Apply(this);
var root = new StackPanel { Margin = new Thickness(20, 18, 20, 16) };
root.Children.Add(new TextBlock
{
Text = control ? "원격 화면 보기 + 키보드·마우스 제어 요청" : "원격 화면 보기 요청",
FontSize = 15,
FontWeight = FontWeights.Bold,
Foreground = Brushes2.Make(control ? "#B71C1C" : "#155E75"),
Margin = new Thickness(0, 0, 0, 10)
});
root.Children.Add(new TextBlock
{
Text = $"관리 PC [{who}] 가 이 PC 화면을 {(control ? "보고 조작" : "보")}려고 합니다.\n허용할까요?",
TextWrapping = TextWrapping.Wrap,
Margin = new Thickness(0, 0, 0, 10)
});
_countdown = new TextBlock { Foreground = Brushes.Gray };
root.Children.Add(_countdown);
var buttons = new StackPanel { Orientation = Orientation.Horizontal, HorizontalAlignment = HorizontalAlignment.Right, Margin = new Thickness(0, 12, 0, 0) };
var allow = new Button { Content = "허용", MinWidth = 90, FontWeight = FontWeights.Bold };
var deny = new Button { Content = "거부", MinWidth = 90, IsCancel = true };
allow.Click += (s, e) => Finish(true);
deny.Click += (s, e) => Finish(false);
buttons.Children.Add(allow);
buttons.Children.Add(deny);
root.Children.Add(buttons);
Content = root;
_timer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(1) };
_timer.Tick += (s, e) =>
{
_left--;
UpdateCountdown();
if (_left <= 0) Finish(false);
};
UpdateCountdown();
Closed += (s, e) => { _timer.Stop(); _result.TrySetResult(false); };
Loaded += (s, e) => { _timer.Start(); Activate(); };
}
private void UpdateCountdown() => _countdown.Text = $"{_left}초 뒤 자동으로 거부됩니다.";
private void Finish(bool ok)
{
_result.TrySetResult(ok);
Close();
}
/// <summary>UI 스레드에서 호출</summary>
public static Task<bool> AskAsync(string who, bool control)
{
var w = new ApprovalWindow(who, control);
w.Show();
return w._result.Task;
}
}
/// <summary>대상 PC: 원격 화면 연결 중 화면 위쪽 표시줄 (항상 표시)</summary>
public sealed class RemoteIndicatorWindow : Window
{
private readonly TextBlock _text;
private readonly Border _bar;
public event Action DisconnectRequested;
public RemoteIndicatorWindow()
{
WindowStyle = WindowStyle.None;
ResizeMode = ResizeMode.NoResize;
AllowsTransparency = true;
Background = Brushes.Transparent;
Topmost = true;
ShowInTaskbar = false;
ShowActivated = false;
SizeToContent = SizeToContent.WidthAndHeight;
FontFamily = new FontFamily("Malgun Gothic");
FontSize = 13;
_bar = new Border
{
CornerRadius = new CornerRadius(0, 0, 8, 8),
Padding = new Thickness(14, 6, 8, 6),
Background = Brushes2.Make("#E65100")
};
var panel = new StackPanel { Orientation = Orientation.Horizontal };
_text = new TextBlock { Foreground = Brushes.White, FontWeight = FontWeights.Bold, VerticalAlignment = VerticalAlignment.Center };
var stop = new Button
{
Content = "연결 끊기",
Margin = new Thickness(12, 0, 0, 0),
MinWidth = 0,
MinHeight = 0,
Padding = new Thickness(8, 2, 8, 2),
Background = Brushes.White
};
stop.Click += (s, e) => DisconnectRequested?.Invoke();
panel.Children.Add(_text);
panel.Children.Add(stop);
_bar.Child = panel;
Content = _bar;
SizeChanged += (s, e) => Reposition();
}
public void Update(IReadOnlyList<ScreenViewer> viewers)
{
if (viewers.Count == 0)
{
Hide();
return;
}
bool control = viewers.Any(v => v.Control);
_bar.Background = Brushes2.Make(control ? "#C62828" : "#E65100");
_text.Text = $"● 원격 {(control ? "제어" : "보기")} 중: {string.Join(", ", viewers.Select(v => v.Who).Distinct())}";
if (!IsVisible) Show();
Reposition();
}
private void Reposition()
{
var wa = SystemParameters.WorkArea;
Left = wa.Left + (wa.Width - ActualWidth) / 2;
Top = wa.Top;
}
}
/// <summary>관리 PC: 원격 화면 창 (보기 / 제어)</summary>
public sealed class RemoteViewerWindow : Window
{
private readonly RemoteClient _client;
private readonly string _label;
private readonly Image _image;
private readonly TextBlock _status, _overlay;
private readonly StackPanel _monitorButtons;
private readonly HashSet<int> _pressedKeys = new HashSet<int>();
private int _sid;
private bool _control;
private bool _ended;
private int _monitor = 1;
private int _frames;
private DateTime _fpsStart = DateTime.UtcNow;
private int _decoding;
private DateTime _lastMove = DateTime.MinValue;
public string ViewerKey { get; }
public RemoteViewerWindow(string key, RemoteClient client, string label)
{
ViewerKey = key;
_client = client;
_label = label;
Title = $"원격관리 - {label}" + AppInstance.TitleTag;
Width = 1280;
Height = 800;
WindowStartupLocation = WindowStartupLocation.CenterScreen;
FontFamily = new FontFamily("Malgun Gothic");
FontSize = 13;
Background = Brushes2.Make("#263238");
AppIcon.Apply(this);
// 한글 입력기는 대상 PC에서 처리하도록 이 창에서는 끔
InputMethod.SetIsInputMethodEnabled(this, false);
var root = new DockPanel();
// ── 도구 모음
var bar = new DockPanel { Background = Brushes2.Make("#ECEFF1") };
DockPanel.SetDock(bar, Dock.Top);
var close = new Button { Content = "연결 끊기" };
close.Click += (s, e) => Close();
DockPanel.SetDock(close, Dock.Right);
bar.Children.Add(close);
_status = new TextBlock { VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(12, 0, 12, 0), Foreground = Brushes2.Make("#37474F") };
DockPanel.SetDock(_status, Dock.Right);
bar.Children.Add(_status);
_monitorButtons = new StackPanel { Orientation = Orientation.Horizontal, Margin = new Thickness(4) };
bar.Children.Add(_monitorButtons);
root.Children.Add(bar);
// ── 화면
var stage = new Grid { Background = Brushes.Black, ClipToBounds = true };
_image = new Image
{
Stretch = Stretch.Uniform,
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center,
Focusable = true,
Cursor = Cursors.Arrow
};
RenderOptions.SetBitmapScalingMode(_image, BitmapScalingMode.HighQuality);
_overlay = new TextBlock
{
Text = "연결 중…",
Foreground = Brushes.White,
FontSize = 16,
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center,
TextAlignment = TextAlignment.Center,
TextWrapping = TextWrapping.Wrap
};
stage.Children.Add(_image);
stage.Children.Add(_overlay);
root.Children.Add(stage);
Content = root;
_image.MouseMove += OnMouseMove;
_image.MouseDown += OnMouseDown;
_image.MouseUp += OnMouseUp;
_image.MouseWheel += OnMouseWheel;
PreviewKeyDown += OnKey;
PreviewKeyUp += OnKey;
Deactivated += (s, e) => ReleaseKeys();
_client.ScreenEvent += OnScreenEvent;
Loaded += async (s, e) => await StartAsync();
Closed += (s, e) => Stop();
}
// ───────────────────────── 시작 / 끝 ─────────────────────────
private async Task StartAsync()
{
try
{
_overlay.Text = "대상 PC 정보를 확인하는 중…";
var info = await _client.RequestAsync("screenInfo", null, TimeSpan.FromSeconds(15));
if (!Json.Bool(info, "allowView"))
{
End("대상 PC에서 화면 보기를 허용하지 않았습니다.\n(대상 PC 설정 → 원격 접속 허용 → 화면 보기 허용)");
return;
}
var monitors = (info["monitors"] as JsonArray)?.OfType<JsonObject>().ToList() ?? new List<JsonObject>();
var primary = monitors.FirstOrDefault(m => Json.Bool(m, "primary"));
_monitor = primary != null ? Json.Int(primary, "index", 1) : 1;
foreach (var m in monitors)
{
int idx = Json.Int(m, "index", 1);
AddMonitorButton($"모니터 {idx}" + (Json.Bool(m, "primary") ? " (주)" : ""), idx,
$"{Json.Int(m, "width")} × {Json.Int(m, "height")}");
}
if (monitors.Count > 1) AddMonitorButton("전체", 0, "모든 모니터를 한 화면에");
HighlightMonitor();
_overlay.Text = "대상 PC의 승인을 기다리는 중…";
var r = await _client.RequestAsync("screenStart", new JsonObject { ["monitor"] = _monitor }, TimeSpan.FromSeconds(45));
_sid = Json.Int(r, "sid");
_control = Json.Bool(r, "control");
_overlay.Text = "";
UpdateStatus();
_image.Cursor = _control ? Cursors.Cross : Cursors.Arrow;
_image.Focus();
}
catch (Exception ex)
{
End(RemoteClient.Friendly(ex));
}
}
private void AddMonitorButton(string text, int index, string tip)
{
var b = new Button { Content = text, Tag = index, ToolTip = tip, MinWidth = 70 };
b.Click += async (s, e) =>
{
_monitor = index;
HighlightMonitor();
try { await _client.RequestAsync("screenMonitor", new JsonObject { ["monitor"] = index }, TimeSpan.FromSeconds(10)); }
catch { }
_image.Focus();
};
_monitorButtons.Children.Add(b);
}
private void HighlightMonitor()
{
foreach (Button b in _monitorButtons.Children)
{
bool on = (int)b.Tag == _monitor;
b.FontWeight = on ? FontWeights.Bold : FontWeights.Normal;
b.Background = on ? Brushes2.Make("#BBDEFB") : Brushes.White;
}
}
private void End(string reason)
{
_ended = true;
_overlay.Text = reason;
_status.Text = "연결 끝";
_image.Cursor = Cursors.Arrow;
}
private void Stop()
{
_client.ScreenEvent -= OnScreenEvent;
ReleaseKeys();
if (_sid > 0 && !_ended)
_ = _client.RequestAsync("screenStop", null, TimeSpan.FromSeconds(5)).ContinueWith(t => { _ = t.Exception; });
}
private void UpdateStatus()
{
var secs = (DateTime.UtcNow - _fpsStart).TotalSeconds;
double fps = secs > 0 ? _frames / secs : 0;
if (secs > 3) { _frames = 0; _fpsStart = DateTime.UtcNow; }
_status.Text = $"{(_control ? "● 제어 중" : "보기 전용")} · {fps:0.0}장/초";
_status.Foreground = _control ? Brushes2.Stopped : Brushes2.Make("#37474F");
}
// ───────────────────────── 화면 받기 ─────────────────────────
private void OnScreenEvent(JsonObject msg)
{
var ev = Json.Str(msg, "event");
int sid = Json.Int(msg, "sid");
if (ev == "screenEnd")
{
if (sid == 0 || sid == _sid)
{
var reason = Json.Str(msg, "reason");
Dispatcher.BeginInvoke(new Action(() => End("원격 화면 연결이 끝났습니다.\n" + reason)));
}
return;
}
if (ev != "frame" || sid != _sid || _ended) return;
// 화면 그리기가 밀리면 이전 장면은 건너뜀
if (Interlocked.Exchange(ref _decoding, 1) == 1) return;
try
{
var bytes = Convert.FromBase64String(Json.Str(msg, "data"));
var bmp = new BitmapImage();
bmp.BeginInit();
bmp.CacheOption = BitmapCacheOption.OnLoad;
bmp.StreamSource = new MemoryStream(bytes);
bmp.EndInit();
bmp.Freeze();
Dispatcher.BeginInvoke(new Action(() =>
{
_image.Source = bmp;
_frames++;
UpdateStatus();
Interlocked.Exchange(ref _decoding, 0);
}), DispatcherPriority.Render);
}
catch
{
Interlocked.Exchange(ref _decoding, 0);
}
}
// ───────────────────────── 입력 보내기 ─────────────────────────
private bool CanControl => _control && !_ended && _sid > 0;
private (double x, double y) Norm(MouseEventArgs e)
{
var p = e.GetPosition(_image);
double w = Math.Max(1, _image.ActualWidth), h = Math.Max(1, _image.ActualHeight);
return (Math.Clamp(p.X / w, 0, 1), Math.Clamp(p.Y / h, 0, 1));
}
private void SendMouse(MouseEventArgs e, string action, string button = "", int delta = 0)
{
var (x, y) = Norm(e);
_client.SendNoReply(new JsonObject
{
["op"] = "input",
["kind"] = "mouse",
["action"] = action,
["button"] = button,
["x"] = Math.Round(x, 5),
["y"] = Math.Round(y, 5),
["delta"] = delta
});
}
private static string ButtonName(MouseButton b) => b switch
{
MouseButton.Right => "right",
MouseButton.Middle => "middle",
_ => "left"
};
private void OnMouseMove(object sender, MouseEventArgs e)
{
if (!CanControl) return;
// 너무 자주 보내지 않도록 (약 30회/초)
if ((DateTime.UtcNow - _lastMove).TotalMilliseconds < 33) return;
_lastMove = DateTime.UtcNow;
SendMouse(e, "move");
}
private void OnMouseDown(object sender, MouseButtonEventArgs e)
{
_image.Focus();
if (!CanControl) return;
_image.CaptureMouse();
SendMouse(e, "down", ButtonName(e.ChangedButton));
e.Handled = true;
}
private void OnMouseUp(object sender, MouseButtonEventArgs e)
{
if (!CanControl) return;
SendMouse(e, "up", ButtonName(e.ChangedButton));
if (Mouse.LeftButton == MouseButtonState.Released && Mouse.RightButton == MouseButtonState.Released && Mouse.MiddleButton == MouseButtonState.Released)
_image.ReleaseMouseCapture();
e.Handled = true;
}
private void OnMouseWheel(object sender, MouseWheelEventArgs e)
{
if (!CanControl) return;
SendMouse(e, "wheel", "", e.Delta);
e.Handled = true;
}
private void OnKey(object sender, KeyEventArgs e)
{
if (!CanControl) return;
var key = e.Key switch
{
Key.System => e.SystemKey,
Key.ImeProcessed => e.ImeProcessedKey,
Key.DeadCharProcessed => e.DeadCharProcessedKey,
_ => e.Key
};
int vk = KeyInterop.VirtualKeyFromKey(key);
if (vk <= 0) return;
bool down = e.RoutedEvent == Keyboard.PreviewKeyDownEvent;
if (down) _pressedKeys.Add(vk); else _pressedKeys.Remove(vk);
_client.SendNoReply(new JsonObject { ["op"] = "input", ["kind"] = "key", ["vk"] = vk, ["down"] = down });
e.Handled = true;
}
/// <summary>창을 벗어나면 눌린 키가 대상 PC에 남지 않도록 모두 뗌</summary>
private void ReleaseKeys()
{
if (_pressedKeys.Count == 0) return;
foreach (var vk in _pressedKeys.ToList())
_client.SendNoReply(new JsonObject { ["op"] = "input", ["kind"] = "key", ["vk"] = vk, ["down"] = false });
_pressedKeys.Clear();
}
}
}