329 lines
13 KiB
C#
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();
|
|
}
|
|
}
|