using System.Collections.ObjectModel; using System.ComponentModel; using System.Diagnostics; using System.IO; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Windows; using System.Windows.Interop; using System.Windows.Input; using System.Windows.Media; using System.Windows.Threading; namespace AxCopilot.Views; /// /// CPU · RAM · 드라이브 · 상위 프로세스를 실시간으로 표시하는 플로팅 위젯. /// info cpu 또는 info ram 항목의 Enter로 열립니다. /// public partial class ResourceMonitorWindow : Window { // ─── P/Invoke ─────────────────────────────────────────────────────────── [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] private static extern bool GlobalMemoryStatusEx(ref MEMORYSTATUSEX lpBuffer); [DllImport("user32.dll")] private static extern void ReleaseCapture(); [DllImport("user32.dll")] private static extern IntPtr SendMessage(IntPtr hWnd, int msg, IntPtr wParam, IntPtr lParam); private const int WM_NCLBUTTONDOWN = 0xA1; private const int HTBOTTOMRIGHT = 17; [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)] private struct MEMORYSTATUSEX { public uint dwLength; public uint dwMemoryLoad; public ulong ullTotalPhys; public ulong ullAvailPhys; public ulong ullTotalPageFile; public ulong ullAvailPageFile; public ulong ullTotalVirtual; public ulong ullAvailVirtual; public ulong ullAvailExtendedVirtual; } // ─── 내부 모델 ────────────────────────────────────────────────────────── public sealed class DriveDisplayItem : INotifyPropertyChanged { private string _label = ""; private string _detail = ""; private double _barWidth = 0; private Brush _barColor = Brushes.Green; public string Label { get => _label; set { _label = value; OnPropertyChanged(); } } public string Detail { get => _detail; set { _detail = value; OnPropertyChanged(); } } public double BarWidth { get => _barWidth; set { _barWidth = value; OnPropertyChanged(); } } public Brush BarColor { get => _barColor; set { _barColor = value; OnPropertyChanged(); } } /// 탐색기로 드라이브 루트를 여는 커맨드 public ICommand OpenCommand { get; init; } = null!; public event PropertyChangedEventHandler? PropertyChanged; private void OnPropertyChanged([CallerMemberName] string? n = null) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(n)); } public sealed class ProcessDisplayItem { public string Rank { get; init; } = ""; public string Name { get; init; } = ""; public string MemText { get; init; } = ""; public double BarWidth{ get; init; } } // ─── 필드 ─────────────────────────────────────────────────────────────── private readonly DispatcherTimer _timer; private static PerformanceCounter? _cpuCounter; private static float _cpuCached; private static DateTime _cpuUpdated = DateTime.MinValue; private readonly ObservableCollection _drives = new(); // 드라이브 진행바 최대 너비 (px) private const double MaxBarWidth = 160.0; public ResourceMonitorWindow() { InitializeComponent(); DriveList.ItemsSource = _drives; // 첫 CPU 카운터 초기화 (첫 샘플은 0이라 무의미) if (_cpuCounter == null) { try { _cpuCounter = new PerformanceCounter("Processor", "% Processor Time", "_Total"); _cpuCounter.NextValue(); } catch { /* PerformanceCounter 미지원 환경 */ } } // 드라이브 목록 초기 구성 (클릭 커맨드 포함) InitDrives(); // 1초 주기 갱신 _timer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(1) }; _timer.Tick += (_, _) => Refresh(); _timer.Start(); Refresh(); // 즉시 첫 갱신 } // ─── 드라이브 목록 초기화 ─────────────────────────────────────────────── private void InitDrives() { _drives.Clear(); foreach (var d in DriveInfo.GetDrives().Where(d => d.IsReady && d.DriveType == DriveType.Fixed)) { var root = d.RootDirectory.FullName; _drives.Add(new DriveDisplayItem { OpenCommand = new RelayCommand(() => { try { Process.Start(new ProcessStartInfo("explorer.exe", root) { UseShellExecute = true }); } catch { /* 무시 */ } }) }); } } // ─── 갱신 ──────────────────────────────────────────────────────────────── private void Refresh() { RefreshCpu(); RefreshRam(); RefreshDrives(); RefreshProcesses(); RefreshUptime(); } private void RefreshCpu() { try { if (_cpuCounter == null) { CpuValueText.Text = "—"; return; } if ((DateTime.Now - _cpuUpdated).TotalMilliseconds > 800) { _cpuCached = _cpuCounter.NextValue(); _cpuUpdated = DateTime.Now; } var pct = (int)Math.Clamp(_cpuCached, 0, 100); CpuValueText.Text = $"{pct}%"; // 색상: 낮음=파랑, 높음=빨강 var barBrush = pct > 80 ? new SolidColorBrush(Color.FromRgb(0xDC, 0x26, 0x26)) : pct > 50 ? new SolidColorBrush(Color.FromRgb(0xD9, 0x77, 0x06)) : new SolidColorBrush(Color.FromRgb(0x4B, 0x9E, 0xFC)); CpuValueText.Foreground = barBrush; CpuBar.Background = barBrush; CpuBar.Width = (CpuBar.Parent as FrameworkElement)?.ActualWidth * pct / 100.0 ?? 0; // 프로세서 이름 (최초 1회) if (string.IsNullOrEmpty(CpuNameText.Text)) CpuNameText.Text = GetProcessorName(); } catch { CpuValueText.Text = "—"; } } private void RefreshRam() { var mem = new MEMORYSTATUSEX { dwLength = (uint)Marshal.SizeOf() }; if (!GlobalMemoryStatusEx(ref mem)) return; var totalGb = mem.ullTotalPhys / 1024.0 / 1024 / 1024; var usedGb = (mem.ullTotalPhys - mem.ullAvailPhys) / 1024.0 / 1024 / 1024; var freeGb = mem.ullAvailPhys / 1024.0 / 1024 / 1024; var pct = (int)mem.dwMemoryLoad; RamValueText.Text = $"{pct}%"; RamDetailText.Text = $"사용: {usedGb:F1} GB / 전체: {totalGb:F1} GB / 여유: {freeGb:F1} GB"; RamBar.Width = (RamBar.Parent as FrameworkElement)?.ActualWidth * pct / 100.0 ?? 0; var barBrush = pct > 85 ? new SolidColorBrush(Color.FromRgb(0xDC, 0x26, 0x26)) : pct > 65 ? new SolidColorBrush(Color.FromRgb(0xD9, 0x77, 0x06)) : new SolidColorBrush(Color.FromRgb(0x7C, 0x3A, 0xED)); RamValueText.Foreground = barBrush; RamBar.Background = barBrush; } private void RefreshDrives() { var drives = DriveInfo.GetDrives() .Where(d => d.IsReady && d.DriveType == DriveType.Fixed) .ToList(); // 드라이브 수가 바뀌면 재초기화 if (drives.Count != _drives.Count) InitDrives(); for (int i = 0; i < drives.Count && i < _drives.Count; i++) { var d = drives[i]; var totalGb = d.TotalSize / 1024.0 / 1024 / 1024; var freeGb = d.AvailableFreeSpace / 1024.0 / 1024 / 1024; var usedGb = totalGb - freeGb; var pct = (int)(usedGb / totalGb * 100); var lbl = string.IsNullOrWhiteSpace(d.VolumeLabel) ? d.Name.TrimEnd('\\') : $"{d.Name.TrimEnd('\\')} ({d.VolumeLabel})"; _drives[i].Label = lbl; _drives[i].Detail = $"{freeGb:F1}GB 여유"; _drives[i].BarWidth = MaxBarWidth * pct / 100.0; _drives[i].BarColor = pct > 90 ? new SolidColorBrush(Color.FromRgb(0xDC, 0x26, 0x26)) : pct > 75 ? new SolidColorBrush(Color.FromRgb(0xD9, 0x77, 0x06)) : new SolidColorBrush(Color.FromRgb(0x05, 0x96, 0x69)); } } private void RefreshProcesses() { try { // 메모리 기준 상위 7개 (WorkingSet64 — 비동기 수집 없이도 빠름) var procs = Process.GetProcesses() .OrderByDescending(p => { try { return p.WorkingSet64; } catch { return 0L; } }) .Take(7) .ToList(); long maxMem = procs.Count > 0 ? (long)(procs[0].WorkingSet64 > 0 ? procs[0].WorkingSet64 : 1) : 1; var items = procs.Select((p, i) => { long ws = 0; string name = ""; try { ws = p.WorkingSet64; name = p.ProcessName; } catch { name = "—"; } var mb = ws / 1024.0 / 1024; return new ProcessDisplayItem { Rank = $"{i + 1}", Name = name, MemText = mb > 1024 ? $"{mb / 1024:F1} GB" : $"{mb:F0} MB", BarWidth = Math.Max(2, 46.0 * ws / maxMem) }; }).ToList(); ProcessList.ItemsSource = items; } catch { /* 권한 부족 등 무시 */ } } private void RefreshUptime() { var uptime = TimeSpan.FromMilliseconds(Environment.TickCount64); var d = (int)uptime.TotalDays; var h = uptime.Hours; var m = uptime.Minutes; UptimeText.Text = d > 0 ? $"가동: {d}일 {h}시간 {m}분" : h > 0 ? $"가동: {h}시간 {m}분" : $"가동: {m}분 {uptime.Seconds}초"; } // ─── 이벤트 ───────────────────────────────────────────────────────────── private void ResizeGrip_MouseLeftButtonDown(object sender, System.Windows.Input.MouseButtonEventArgs e) { if (e.ButtonState != System.Windows.Input.MouseButtonState.Pressed) return; ReleaseCapture(); SendMessage(new WindowInteropHelper(this).Handle, WM_NCLBUTTONDOWN, (IntPtr)HTBOTTOMRIGHT, IntPtr.Zero); } private void Close_Click(object sender, RoutedEventArgs e) => Close(); private void Window_KeyDown(object sender, KeyEventArgs e) { if (e.Key == Key.Escape) Close(); } private void Window_MouseDown(object sender, System.Windows.Input.MouseButtonEventArgs e) { if (e.ChangedButton == MouseButton.Left && e.LeftButton == System.Windows.Input.MouseButtonState.Pressed) try { DragMove(); } catch { } } /// Ctrl+마우스 휠로 창 높이 조절 (투명 창에서는 OS 리사이즈 그립이 없으므로) protected override void OnMouseWheel(System.Windows.Input.MouseWheelEventArgs e) { if (Keyboard.Modifiers == ModifierKeys.Control) { var delta = e.Delta > 0 ? 40 : -40; var newH = Height + delta; if (newH >= MinHeight && newH <= 900) Height = newH; e.Handled = true; return; } base.OnMouseWheel(e); } protected override void OnClosed(EventArgs e) { _timer.Stop(); base.OnClosed(e); } // ─── 헬퍼 ─────────────────────────────────────────────────────────────── private static string GetProcessorName() { try { using var key = Microsoft.Win32.Registry.LocalMachine .OpenSubKey(@"HARDWARE\DESCRIPTION\System\CentralProcessor\0"); return key?.GetValue("ProcessorNameString") as string ?? ""; } catch { return ""; } } /// 간단한 ICommand 구현 (RelayCommand) private sealed class RelayCommand(Action execute) : ICommand { public event EventHandler? CanExecuteChanged { add { } remove { } } public bool CanExecute(object? _) => true; public void Execute(object? _) => execute(); } }