Files
AX-Copilot-Codex/src/AxCopilot/Views/ResourceMonitorWindow.xaml.cs

329 lines
13 KiB
C#

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;
/// <summary>
/// CPU · RAM · 드라이브 · 상위 프로세스를 실시간으로 표시하는 플로팅 위젯.
/// info cpu 또는 info ram 항목의 Enter로 열립니다.
/// </summary>
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(); } }
/// <summary>탐색기로 드라이브 루트를 여는 커맨드</summary>
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<DriveDisplayItem> _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<MEMORYSTATUSEX>() };
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 { }
}
/// <summary>Ctrl+마우스 휠로 창 높이 조절 (투명 창에서는 OS 리사이즈 그립이 없으므로)</summary>
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 ""; }
}
/// <summary>간단한 ICommand 구현 (RelayCommand)</summary>
private sealed class RelayCommand(Action execute) : ICommand
{
public event EventHandler? CanExecuteChanged { add { } remove { } }
public bool CanExecute(object? _) => true;
public void Execute(object? _) => execute();
}
}