
接 WPF 桌面开发的时候几乎每个工具类项目都会遇到这个需求程序跑起来主界面要显示“当前电脑的储存和运存”。这里的“储存”指的是硬盘容量也就是磁盘存储空间“运存”是内存也就是 RAM。用户口中经常说的是“我这电脑还有多少个 G”“内存是多大的”但在程序里要拿到这些数据存在好几条完全不同的技术路线每一条都有它的坑。这篇文章我会从需求拆解开始讲然后对比三种主流取数方案给出可以直接复制的完整代码再把我实际开发中踩过的问题、用户问得最多的疑惑、以及隐藏在背后的系统机制全部讲清楚。不管是新手还是已经写过几个 WPF 界面的同行读完之后都可以直接用在你的项目里。1. 需求拆解先搞清楚“储存”和“运存”到底是什么1.1 两个热词背后的技术真相“运存”这个词是典型的消费电子向说法早年手机厂和电视厂用得多现在已经渗透到电脑领域。技术文档里的正式叫法是 Random Access Memory即随机存取存储器也就是内存条容量。它决定了一台电脑能同时跑多少程序、能缓存多少数据。“储存”在个人电脑语境里通常指外存即硬盘SSD 或机械盘的容量。固态硬盘容量 512GB、1TB 甚至 2TB机械硬盘 1TB、2TB、4TB 都有。从操作系统的角度看它能被盘符C:、D:等抽象为逻辑分区也可以按物理磁盘对象进行统计。这两个指标在实际项目里经常一起出现因为一个完整的系统体检页通常同时展示“总硬盘容量 剩余空间”和“总内存 可用内存”。从需求角度讲用户要看的不是纯数字而是“我的电脑还够不够用”这一判断依据。1.2 典型使用场景不要觉得这个功能很 trivial它广泛潜伏在几类 WPF 应用里。第一类是系统信息展示工具比如“硬件检测助手”“电脑体检软件”主界面几乎就是硬盘条、内存条、CPU 占用率的组合。第二类是启动自检类的内部工具。很多公司内部客户端会在启动时读取当前机器的磁盘剩余空间和内存总量低于阈值就提示运维人员防止在低配置机器上继续运行重任务。第三类是游戏、设计类工作站启动器。这类应用需要根据内存大小和磁盘剩余空间决定是否允许进入高负载模式比如大数据处理工具内存不够时直接禁用某些功能。第四类是装机记录、IT 资产管理场景。管理员批量部署终端时需要程序上报每台机器的存储与内存信息。不管哪种场景底层拿数据的方式都差不多。区别只在于展示形式、统计口径和是否要持久化。1.3 动手写代码之前必须想清楚的三个口径问题第一个是容量单位。磁盘厂商按十进制计算1GB 1,000,000,000 字节操作系统按二进制计算1GB 1,073,741,824 字节。这段换算若不统一界面显示会差出一道鸿沟。第二个是“总容量”和“剩余容量”必须分开。用户问“内存多大”可能想知道物理内存总数也可能想知道当前剩余多少。磁盘同理用户更关心“还剩多少”。第三个是逻辑盘符与物理磁盘的统计目标。同一个物理磁盘被分成 C 和 D 两个区后DriveInfo 遍历盘符会把总容量加两次但实际上你只插了一块硬盘。某些产品需要统计物理硬盘数量某些只需要统计可用空间总和口径必须先定下来。想清楚这三个问题后续代码才不会写歪。2. 技术方案选型三套取数打法各有取舍2.1 DriveInfo 盘符遍历轻量级首选在 .NET 体系里最简单粗暴的方案是 System.IO.DriveInfo.GetDrives()。它返回当前系统里所有可用驱动器包括固定硬盘、U 盘、光驱和网络映射盘。你可以过滤 DriveType.Fixed 来只保留本地固定磁盘。这套方案的优点是从 BCL 直接调用没有额外依赖代码量极省跨平台可运行出错概率很低。缺点是只能拿到“逻辑卷”级别信息拿不到物理磁盘的型号、接口类型等细节。如果要按物理硬盘维度统计或者要读序列号和固件信息它办不到。2.2 WMI 查询信息完整但要注意性能WMI 是 Windows 管理规范Windows Management Instrumentation在 .NET 里通过 System.Management 命名空间访问查询类如 SQL 一样的 WQL 语句。获取磁盘可以用 Win32_LogicalDisk逻辑分区或 Win32_DiskDrive物理磁盘获取内存可以用 Win32_OperatingSystem、Win32_ComputerSystem返回的内存字段覆盖总容量、可用容量、页面文件大小等粒度比 DriveInfo 细很多。WMI 的算力成本也不低首次查询会触发底层枚举轻则几百毫秒重则两三秒而且同步调用会明显卡 UI。实际项目里必须配合缓存或异步方案这个后面细讲。2.3 Windows API性能党看过来如果你对性能有极端要求可以直接 P/Invoke 调用原生 API。磁盘信息对应 GetDiskFreeSpaceEx内存信息对应 GlobalMemoryStatusEx。这套方案没有 WMI 初始化开销速度最快也最接近操作系统真实视图。代价是代码量明显变大需要定义结构体 Marshal 结构布局还要处理 IntPtr 和非托管资源。大多数 WPF 应用性能瓶颈根本不在这一处值不值得引入取决于你对项目品质的要求。2.4 方案对比方案依赖性能信息粒度代码量适用场景DriveInfo无快逻辑磁盘总/剩余容量极少常规展示、跨平台工具WMISystem.Management首次偏慢逻辑磁盘、物理磁盘、内存、硬件细节中系统体检、IT 资产采集Windows API无P/Invoke最快磁盘与内存的当前状态较大高频刷新、性能敏感场景我的实际建议是一般 WPF 工具Disk 部分用 DriveInfo内存部分用 API 或 WMI 均可如果要做完整硬件信息面板再上 WMI 全套。下面给的示例代码也会围绕这两条主线展开。3. 核心代码实现磁盘容量和内存容量的获取3.1 先定义两个数据模型为了让代码干净一点我会先建两个 POCO 数据类用来统一承载查询结果。这两个类可以直接当 WPF Binding 的源对象后面数据绑定省去很多转换逻辑。public class HddInfo { // 磁盘总容量字节 public long TotalSize { get; set; } // 磁盘剩余容量字节 public long FreeSize { get; set; } // 已用空间字节 public long UsedSize TotalSize - FreeSize; // 磁盘总数 public int DriveCount { get; set; } } public class MemoryInfo { // 物理内存总容量字节 public long TotalSize { get; set; } // 当前可用物理内存字节 public long FreeSize { get; set; } public long UsedSize TotalSize - FreeSize; }这里统一用字节做存储单位显示层再负责格式化。这样后续换界面、加图表都不需要重新算数。3.2 磁盘容量DriveInfo 实现先来最简单的一版。遍历固定磁盘累加总容量和剩余空间同时数一下有多少个固定分区。using System; using System.IO; using System.Linq; public static class DiskInfoProvider { public static HddInfo GetHddInfo() { long totalSize 0; long freeSize 0; int driveCount 0; foreach (DriveInfo drive in DriveInfo.GetDrives()) { // 只统计本地固定磁盘系统盘、数据盘 if (drive.DriveType ! DriveType.Fixed) { continue; } // 这一步很关键让 DriveInfo 真正加载磁盘状态 // 不做一下“热身”后面的 TotalSize 可能读到 0 _ drive.DriveFormat; if (!drive.IsReady) { continue; } totalSize drive.TotalSize; freeSize drive.AvailableFreeSpace; driveCount; } return new HddInfo { TotalSize totalSize, FreeSize freeSize, DriveCount driveCount }; } }注意那个_ drive.DriveFormat;这不是多余的一句。DriveInfo 内部采用延迟加载第一次创建对象时不会立刻向文件系统发起查询而是在访问某个属性时才触发底层数据填充。如果你在 new 完之后直接读 TotalSize某些机器上会得到 0。先触发一次属性访问后面读 TotalSize、AvailableFreeSpace 就都稳定了。这个方法第一次读驱动格式也有极小概率触发“设备未就绪”的 IOException现实工程中最好再包一层 try-catch至少把 IsReady 判断放在前面。3.3 磁盘容量WMI 按物理磁盘统计如果产品需要展示“这台电脑装了一块 1TB 物理硬盘”或者需要按物理盘汇总容量DriveInfo 就无能为力了。换用 Win32_DiskDrive 可以直接拿到系统物理磁盘列表Size 字段代表物理磁盘总大小。using System.Management; public static class DiskInfoWmiProvider { public static HddInfo GetPhysicalDiskInfo() { long totalSize 0; int diskCount 0; long freeSize 0; using (var searcher new ManagementObjectSearcher( SELECT Size, MediaType, InterfaceType FROM Win32_DiskDrive)) { foreach (ManagementObject disk in searcher.Get()) { if (disk[Size] null) { continue; } totalSize Convert.ToInt64(disk[Size]); diskCount; } } // 自由空间仍从逻辑盘拿按物理盘聚合其实还得做分区映射这里先简单汇总固定盘 using (var freeSearcher new ManagementObjectSearcher( SELECT FreeSpace FROM Win32_LogicalDisk WHERE DriveType 3)) { foreach (ManagementObject partition in freeSearcher.Get()) { if (partition[FreeSpace] ! null) { freeSize Convert.ToInt64(partition[FreeSpace]); } } } return new HddInfo { TotalSize totalSize, FreeSize freeSize, DriveCount diskCount }; } }这段代码里有两个值得注意的地方。第一物理磁盘数和逻辑分区数不是一回事。一块物理盘可以分成 C、D 两个区所以上方分别统计了“物理盘数量”和“分区剩余空间”。如果你的 UI 同时展示“物理磁盘数量和总容量、各分区剩余空间”这个结构正好一次拿全。第二Convert.ToInt64 而不是强转。WMI 返回的 Size 字段通常以字符串或 uint64 形式返回直接 (long)disk[Size] 在某些机器上会抛 InvalidCastException。用 Convert 系列最稳。3.4 内存容量WMI 实现内存信息的 WMI 查询常用的是 Win32_OperatingSystem 类。里面有两个字段和一个容易混淆的字段TotalVisibleMemorySize操作系统可见的总物理内存单位是 KBFreePhysicalMemory当前可用的物理内存单位是 KBTotalVirtualMemorySize虚拟内存不是物理内存条public static class MemoryInfoWmiProvider { public static MemoryInfo GetMemoryInfo() { ulong totalSize 0; ulong freeSize 0; using (var searcher new ManagementObjectSearcher( SELECT TotalVisibleMemorySize, FreePhysicalMemory FROM Win32_OperatingSystem)) { foreach (ManagementObject os in searcher.Get()) { totalSize Convert.ToUInt64(os[TotalVisibleMemorySize]) * 1024; freeSize Convert.ToUInt64(os[FreePhysicalMemory]) * 1024; } } return new MemoryInfo { TotalSize (long)totalSize, FreeSize (long)freeSize }; } }WMI 返回的内存单位是 KB所以统一乘 1024 转成字节。顺带说一个洞察为什么很多工具里显示“内存总数”比物理条子容量小Win32_OperatingSystem.TotalVisibleMemorySize 代表操作系统“可见”的内存硬件保留内存、核显占用的共享内存不计入这个字段。比如 16GB 内存但核显共享了 512MB这里可能就是 15.5GB 左右。如果产品对“物理条容量”更敏感应该改用 Win32_ComputerSystem.TotalPhysicalMemory它返回的是物理内存条容量字节更接近用户买电脑时的“16GB 内存”概念。3.5 内存容量API 实现GlobalMemoryStatusEx如果你担心 WMI 的性能或者界面需要每秒钟刷新一次可用内存比如做一个内存仪表盘建议直接上 API。using System; using System.Runtime.InteropServices; public static class MemoryInfoApiProvider { [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; } [DllImport(kernel32.dll, CharSet CharSet.Auto, SetLastError true)] [return: MarshalAs(UnmanagedType.Bool)] private static extern bool GlobalMemoryStatusEx(ref MEMORYSTATUSEX lpBuffer); public static MemoryInfo GetMemoryInfo() { var status new MEMORYSTATUSEX(); status.dwLength (uint)Marshal.SizeOf(status); if (!GlobalMemoryStatusEx(ref status)) { throw new InvalidOperationException( 调用 GlobalMemoryStatusEx 失败错误码: Marshal.GetLastWin32Error()); } return new MemoryInfo { TotalSize (long)status.ullTotalPhys, FreeSize (long)status.ullAvailPhys }; } }注意 dwLength 必须要在调用前设置为结构体大小这是 Win32 结构体版本协商的惯例漏掉这句会返回 false。GlobalMemoryStatusEx 返回的是物理内存当前真实占用状况不包含页文件也与进程占用无关是系统级口径。3.6 界面展示与数据绑定WPF 里最见效率的盘法是直接在 XAML 里绑定一个视图模型属性然后在窗口加载事件里异步填充数据。先定义一个简单的 ViewModelusing System.ComponentModel; using System.Runtime.CompilerServices; public class MainViewModel : INotifyPropertyChanged { private string _hddInfoText; private string _memoryInfoText; public string HddInfoText { get _hddInfoText; set { _hddInfoText value; OnPropertyChanged(); } } public string MemoryInfoText { get _memoryInfoText; set { _memoryInfoText value; OnPropertyChanged(); } } private void OnPropertyChanged([CallerMemberName] string propertyName null) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } public event PropertyChangedEventHandler PropertyChanged; }XAML 层可以放两个干净整洁的系统信息区块StackPanel Margin24 TextBlock Text储存空间 FontWeightBold FontSize16/ TextBlock Text{Binding HddInfoText} Margin0,4,0,16/ TextBlock Text运行内存 FontWeightBold FontSize16/ TextBlock Text{Binding MemoryInfoText} Margin0,4,0,0/ /StackPanel加载的逻辑尽量别直接写在构造函数里放 Window.Loaded 事件更适合此时窗口 Handle 已经就绪也方便做异常兜底。private async void Window_Loaded(object sender, RoutedEventArgs e) { var vm DataContext as MainViewModel; if (vm null) return; // 磁盘读取用 DriveInfo 很快可以直接跑 HddInfo hdd DiskInfoProvider.GetHddInfo(); vm.HddInfoText $已用 {FormatSize(hdd.UsedSize)} / 总计 {FormatSize(hdd.TotalSize)} $剩余 {FormatSize(hdd.FreeSize)}{hdd.DriveCount} 个本地分区; // WMI 初次查询较慢放进 Task.Run不要卡 UI MemoryInfo memory await Task.Run(MemoryInfoWmiProvider.GetMemoryInfo); vm.MemoryInfoText $已用 {FormatSize(memory.UsedSize)} / 总计 {FormatSize(memory.TotalSize)} $当前可用 {FormatSize(memory.FreeSize)}; }把耗时查询放到 Task.Run 里是 WPF 开发避免界面卡顿的基本纪律尤其是 WMI 路线务必这么做。格式化方法就顺手写在工具类里public static string FormatSize(long bytes) { string[] units { B, KB, MB, GB, TB }; double size bytes; int unitIndex 0; while (size 1024 unitIndex units.Length - 1) { size / 1024; unitIndex; } return ${size:0.##} {units[unitIndex]}; }这套逻辑已经足够跑通从采集到展示的完整链路。接下来聊聊那些不打开调试器根本察觉不到的隐藏坑。4. 实操细节与避坑经验4.1 单位显示别栽在十进制和二进制上磁盘容量允换算的坑最常被刚入门的同行忽略。Windows 资源管理器里显示一块“512GB”的固态硬盘只有约 476GB可用不是因为坑你而是因为 Windows 按 1024 进换算磁盘厂商按 1000 进“标称容量”。这里的核心口径其实是“字节数”永远准确界面格式化时统一按 1024 处理即可。我给显示层定的原则是字节永远是唯一的原始单位格式化函数统一 1024 进制。你说的“储存”和“运存”在不同语境下可能有不同展示口径但底层都能落到字节上来任何换算都不该污染原始数据。4.2 WMI 查询“冷启动”慢别直接塞 UI 线程System.Management 的第一次查询需要初始化 WMI 服务连接普通电脑要 300ms 到 2s 不等。如果直接在窗口构造函数里同步调用界面上就是一个白屏观感极差。我在项目里的处理方式是三层防御把 WMI 查询放进 Task.Run在扇区闲置期做好结果缓存第二次直接用缓存页面展示先用 DriveInfo/API 的快速结果顶上去WMI 完整数据到了再刷新这样用户第一眼看到的是基本数据几百毫秒后细节才补齐体感几乎无卡顿。4.3 盘符过滤别把光驱、U 盘和网络盘统计进来遍历 DriveInfo 时如果不做过滤光驱、读卡器、网络映射盘全部会混进来数字立刻失真。上面示例代码用的 DriveType.Fixed 是最关键的过滤条件。WMI 里对应的是 Win32_LogicalDisk 的 DriveType3本地固定磁盘这个数字常量来自 Win32 API 的 DRIVE_FIXED。有几种特殊情况需要预留思考移动硬盘在某些系统下表现为 Fixed 盘统计时会混入是否过滤看产品需求网络映射盘通常是 DriveType.Network 或 WMI DriveType4默认不统计虚拟光驱是 DriveType.CDRom一定排除4.4 物理内存数字对不上先怀疑核显保留和硬件保留你辛辛苦苦查出来的内存总数用户拿任务管理器一对发现少了 200MB 甚至 1GB立刻就质疑程序有 bug。这不是 bug而是两个概念差异Win32_ComputerSystem.TotalPhysicalMemory 是物理条子的电容量硬件保留内存不算进去但有些 BIOS 固件保留区也算在物理容量里Win32_OperatingSystem.TotalVisibleMemorySize 是操作系统可使用的内存核显会把一部分系统内存划转成显存这部分不计入可见内存硬件保留Hardware Reserved也不计入做产品时我建议把两个字段都查出来UI 上优先展示操作系统可见容量但在详情页标注“物理内存总数”。这样既符合用户对“16GB”的期待也能解释任务管理器里的 15.4GB 差异。4.5 同一块物理盘分区后别重复计总容量这个坑在“按物理磁盘统计”的场景里反复出现。你用 DriveInfo 遍历 C、D 分区并求 TotalSize 总和如果一块 1TB 硬盘被分为 C 500GB D 500GB得到的结果是 1TB没问题。但如果用户在两块盘上分了四个区你得到的还是总逻辑容量这不等于物理磁盘总容量。需要区分“存储总容量”和“物理磁盘数”时把 Win32_DiskDrive物理对象与 Win32_LogicalDisk逻辑卷两条查询线分开界面也分开展示。不要试图从逻辑盘反向推断物理盘数量中间隔着分区表推算关系复杂而且极易出错。4.6 32 位与 64 位进程的隐藏差异如果你的 WPF 程序编译成了 x8632位读容量时 longInt64本身没有溢出风险因为字节数仍然可以表示。问题出在 WMI 的某些内存字段定义是 uint32单位还是 KB。8GB 内存换算过来也只有还不到 8.4 百万 KB不会溢出。但到了 4GB 以上某些 API 在设计上有历史遗留限制GlobalMemoryStatusEx 是专门为 64 位设计的ullTotalPhys 是 ulong安全。真正的坑是编译器选了 AnyCPU程序在老旧系统上以 32 位方式跑然后你把 TotalPhysicalMemory 强转成 int 或 uint数字就爆了。解决方案是全体成员都用 long/ulong转换一律 Convert.ToInt64 / Convert.ToUInt64不该用 int 绝不用。5. 常见问题与排查技巧实录5.1 高频问题速查表现象可能原因解决方式磁盘总容量比标称少很多厂商 1000 进制 vs 系统 1024 进制按 1024 格式化显示或兼容说明DriveInfo 读 TotalSize 为 0没有先触发属性刷新或驱动器未就绪先访问 DriveFormat再判 IsReady内存总数比物理内存少几百 MB核显共享显存、硬件保留改用 Win32_ComputerSystem.TotalPhysicalMemory 取物理总量WMI 查询导致 UI 卡顿同步调用阻塞 UI 线程Task.Run 异步查询并缓存结果拿到 U 盘/光驱容量未过滤 DriveTypeDriveType.Fixed 或 DriveType3虚拟机的“内存”与宿主机一致查询的是虚拟机可见视图在文档标注虚拟机环境限制dll 引用报错未添加 System.Management 引用项目右键添加程序集引用5.2 排查思路数字不对怎么定位是谁的锅面对“数值不对”的质疑我的排查顺序固定是这样的。第一步先打印原始字节数不经过任何格式化肉眼确认数值是否稳定。第二步与任务管理器或系统信息 msinfo32 对照确认口径是否一致。第三步意识到系统不同视图确有差异任务管理器的“内存”显示的是“操作系统可见”和“当前可用”不是“物理条容量”。第四步把 DriveInfo、WMI 和 API 三个来源都查一遍看在某个环节差异从哪冒出来的基本就能定位是口径问题还是代码问题。这套方法在实际开发中帮我排掉了至少两次“产品说数据不准其实是口径不同”的拉扯。5.3 做个小缓存设计更讲究我再给一个改进方案把查询结果缓存起来而不是每次打开页面都重新查。WMI 冷启动慢的问题缓存是根治手段。public static class SystemInfoCache { private static HddInfo _hdd; private static MemoryInfo _memory; private static DateTime _lastRefreshTime; private static readonly TimeSpan RefreshInterval TimeSpan.FromMinutes(5); public static async Task(HddInfo, MemoryInfo) GetSystemInfoAsync() { if (_hdd ! null _memory ! null DateTime.Now - _lastRefreshTime RefreshInterval) { return (_hdd, _memory); } HddInfo hdd await Task.Run(DiskInfoProvider.GetHddInfo); MemoryInfo memory await Task.Run(MemoryInfoWmiProvider.GetMemoryInfo); _hdd hdd; _memory memory; _lastRefreshTime DateTime.Now; return (hdd, memory); } }这样用户在主界面、设置页、关于页反复切换时只有第一次会走完整的查询链路后面的请求瞬间返回。如果产品里有“手动刷新”按钮清空缓存重查即可。这份小设计能直接提升工具的响应体感属于花小钱办大事。我个人的习惯是把磁盘和内存查询做成一次独立服务不在 ViewModel 里散落裸调。这样界面、日志、上报逻辑都能复用同一份数据源后续想加“按物理盘聚合”“历史趋势记录”也只需在服务层扩展不用动界面一层。另外强调一句任何查询都要做好异常兜底。WMI 在某些精简版系统或权限受限环境可能抛异常DriveInfo 读取网络盘可能超时API 调用可能返回失败。我通常把异常处理放在服务层返回值用默认值或“读取失败”占位保证界面永远不死。最终交付给用户的不只是一堆代码而是一个在任何机器上都站得住的工具体验。